Get uname release field from C# in .NET Core on Linux

▼魔方 西西 提交于 2020-01-25 09:24:17

问题


I'm trying to get the output of uname -r in C# in .NET Core 2.2 running on Ubuntu 18.04.

I'm writing this with performance in mind, so have been trying to use a P/Invoke to achieve it.

The uname(2) docs indicate I need to pass a struct in with the relevant sized fields. After playing with a lot of variations, I came up with:

[StructLayout(LayoutKind.Sequential)]
unsafe internal struct Utsname
{
    public fixed byte sysname[65];

    public fixed byte nodename[65];

    public fixed byte release[65];

    public fixed byte version[65];

    public fixed byte machine[65];
}

public static class Main
{
    [DllImport("libc.so.6", CallingConvention = CallingConvention.Cdecl)]
    internal static extern int uname(ref Utsname buf);

    public static void Main(string[] args)
    {
        byte[] bs = new byte[65];
        unsafe
        {
            var buf = new utsname();
            uname(ref buf);
            Marshal.Copy((IntPtr)buf.release, bs, 0, 65);
        }

        Console.WriteLine(Encoding.UTF8.GetString(bs));
    }
}

This seems to work, but moving it into a wrapper function like:

public static class Main
{

...

    public static string GetUnameRelease()
    {
        var bs = new List<byte>();
        unsafe
        {
            var buf = new utsname();
            uname(ref buf);

            int i = 0;
            byte* p = buf.release;
            while (i < 65 && *p != 0)
            {
                bs.Add(*p);
                p++;
                i++;
            }
        }
        return Encoding.UTF8.GetString(bs.ToArray());
    }

    public static void Main(string[] args)
    {
        Console.WriteLine(GetUnameRelease());
    }
}

Seems to cause it to fail. I'm just not sure what I'm doing wrong. It fails silently, presumably due to a segfault, although I'm not sure where/how to get a trace of that.

Other struct marshalling methods I've tried

I also tried a few other ways to get the struct back.

The simplest seemed to be the string fields with fixed-length values (but I assume this fails because the caller needs to allocate mutable fields for the callee to set):

internal struct Utsname
{
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 65)]
    public string sysname;

    ...
}

Or a simple byte array:

internal struct Utsname
{
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 65)]
    public byte[] sysname;

    ...
}

In this case, I assume the problem is something to do with the In/Out calling convention when passing a managed array into the call.

I tried using out instead of ref to simplify the P/Invoke as well, but I get the impression uname() expects the caller to allocate the memory before the call.

I also tried using the [In] and [Out] attributes, but not sure what the defaults are or how using them would change things.

Writing an external C library to wrap the call

I also wrote a small C library to wrap the call to make the calling convention easier to handle:

#include <string.h>
#include <stdlib.h>
#include <sys/utsname.h>

char *get_uname_release()
{
    struct utsname buf;

    uname(&buf);

    size_t len = strlen(buf.release);

    char *release = malloc(len * sizeof(char));

    strcpy(release, buf.release);

    return release;
}

I compiled this with gcc -shared -o libget_uname.so -fPIC get_uname.c and put it next to the main managed DLL.

Calling this was much easier, with just:

public static class Main
{
    ...

    [DllImport("libget_uname.so", EntryPoint = "uname_get_release", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
    internal static extern string GetUnameRelease();
}

This seemed to work every time I used it.

But I'm averse to including a native library in code, if it might be possible to just P/Invoke directly instead.

Using a Process call instead

The other obvious simple choice would just be to call the uname coreutil as a subprocess:

public static class Main
{
    ...

    public static string GetUnameRelease()
    {
        var unameProc = new Process()
        {
            StartInfo = new ProcessStartInfo()
            {
                FileName = "uname",
                Arguments = "-r",
                UseShellExecute = false,
                RedirectStandardOutput = true,
                CreateNoWindow = true
            }
        };

        unameProc.Start();
        unameProc.WaitForExit();
        return unameProc.StandardOutput.ReadToEnd();
    }
}

But I was hoping to avoid the overhead of a subprocess... Perhaps it's not so bad on Linux and just worth doing?

But I've spent a while looking into the PInvoke now, so I would like to know if it's possible.

Questions

So my questions are:

  • What's the best (fastest reliable) way to get the release field from uname from C#?
  • How would I P/Invoke the uname() syscall in libc reliably to get the utsname struct back?

回答1:


The reason it is not working when you move the code to a function is that your structure does not include the domainname member, so when you call uname it is clobbering memory beyond the memory you allocated for your structure.

using System;
using System.Runtime.InteropServices;

[StructLayout(LayoutKind.Sequential)]
unsafe internal struct Utsname
{
        public fixed byte sysname[65];
        public fixed byte nodename[65];
        public fixed byte release[65];
        public fixed byte version[65];
        public fixed byte machine[65];
        public fixed byte domainname[65];
}

public static class Program
{
        [DllImport("libc.so.6", CallingConvention = CallingConvention.Cdecl)]
        internal static extern int uname(ref Utsname buf);

        public static void Main(string[] args)
        {
                Console.WriteLine(GetUnameRelease());
        }

        static unsafe string GetUnameRelease()
        {
                Utsname buf;
                uname(ref buf);
                return Marshal.PtrToStringAnsi((IntPtr)buf.release);
        }
}



回答2:


Here's a version that doesn't require unsafe code:

public class Utsname
{
    public string SysName; // char[65]
    public string NodeName; // char[65]
    public string Release; // char[65]
    public string Version; // char[65]
    public string Machine; // char[65]
    public string DomainName; // char[65]

    public void Print()
    {
        System.Console.Write("SysName:\t");
        System.Console.WriteLine(this.SysName);

        System.Console.Write("NodeName:\t");
        System.Console.WriteLine(this.NodeName);

        System.Console.Write("Release:\t");
        System.Console.WriteLine(this.Release);

        System.Console.Write("Version:\t");
        System.Console.WriteLine(this.Version);

        System.Console.Write("Machine:\t");
        System.Console.WriteLine(this.Machine);

        System.Console.Write("DomainName:\t");
        System.Console.WriteLine(this.DomainName);


        Mono.Unix.Native.Utsname buf;
        Mono.Unix.Native.Syscall.uname(out buf);

        System.Console.WriteLine(buf.sysname);
        System.Console.WriteLine(buf.nodename);
        System.Console.WriteLine(buf.release);
        System.Console.WriteLine(buf.version);
        System.Console.WriteLine(buf.machine);
        System.Console.WriteLine(buf.domainname);
    }


}


[System.Runtime.InteropServices.DllImport("libc", EntryPoint = "uname", CallingConvention = System.Runtime.InteropServices.CallingConvention.Cdecl)]
private static extern int uname_syscall(System.IntPtr buf);

// https://github.com/jpobst/Pinta/blob/master/Pinta.Core/Managers/SystemManager.cs
private static Utsname Uname()
{
    Utsname uts = null;
    System.IntPtr buf = System.IntPtr.Zero;

    buf = System.Runtime.InteropServices.Marshal.AllocHGlobal(8192);
    // This is a hacktastic way of getting sysname from uname ()
    if (uname_syscall(buf) == 0)
    {
        uts = new Utsname();
        uts.SysName = System.Runtime.InteropServices.Marshal.PtrToStringAnsi(buf);

        long bufVal = buf.ToInt64();
        uts.NodeName = System.Runtime.InteropServices.Marshal.PtrToStringAnsi(new System.IntPtr(bufVal + 1 * 65));
        uts.Release = System.Runtime.InteropServices.Marshal.PtrToStringAnsi(new System.IntPtr(bufVal + 2 * 65));
        uts.Version = System.Runtime.InteropServices.Marshal.PtrToStringAnsi(new System.IntPtr(bufVal + 3 * 65));
        uts.Machine = System.Runtime.InteropServices.Marshal.PtrToStringAnsi(new System.IntPtr(bufVal + 4 * 65));
        uts.DomainName = System.Runtime.InteropServices.Marshal.PtrToStringAnsi(new System.IntPtr(bufVal + 5 * 65));

        if (buf != System.IntPtr.Zero)
            System.Runtime.InteropServices.Marshal.FreeHGlobal(buf);
    } // End if (uname_syscall(buf) == 0) 

    return uts;
} // End Function Uname


来源:https://stackoverflow.com/questions/55195344/get-uname-release-field-from-c-sharp-in-net-core-on-linux

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!