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

3

1

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?
Mickens answered 16/3, 2019 at 9:36 Comment(7)
Also, I'm aware the buffer length of 65 is implementation defined, but I'm not sure how to get around that, since I can't rely on the implementation to define the struct for me. Would be interested in how to get around that...Mickens
Your "move into a wrapper function" also includes using a different way of extracting the data. Have you tried moving the exact working code to a different method instead? (The one that copies the byte array before extracting the text.) I would also note that you probably don't want a string that treats the whole byte array as text - you probably want to find the first 0 in the array and only decode the bytes leading up to that.Xylene
Good point - I've updated the wrapper function with a safer strcpy-like implementation. Following it through on the debugger, the program silently crashes at the uname(ref buf) call.Mickens
You've still got differences between the version that works and the version that doesn't. One step at a time: if you just move the working code out of the Main method into a separate method, does that really still break things? (I can't see why just changing which method is calling it would do that.) But then I wonder if you're showing us the real code... the code you've shown doesn't compile due to a difference in casing. I would personally remove the aspects about other processes and the native library - stick to asking a very specific question.Xylene
(It would be worth mentioning the other approaches with a sentence for each, but at the moment they're more of a distraction.)Xylene
To avoid the issue with getting the right structure size, which can vary with releases of the kernel you should probably read the data from the filesystem. /proc/sys/kernel/osrelease will give the same result. From a performance perspective, this is not a real disk file, but a virtual file that exposes kernel info so it is essentially just reading from memory.Michaud
File.ReadAllText("/proc/sys/kernel/osrelease"); will give you the OS releaseMichaud
M
0

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);
        }
}
Michaud answered 17/3, 2019 at 0:4 Comment(1)
Ah! I saw the field, but all the documentation had it wrapped in an ifdef -- couldn't get a straight answer as to whether it was a real field. I tried putting it in a few times, but should have persisted. Thanks! Also, will follow your advice on reading from /proc/sys/kernel/osrelease.Mickens
D
1

Version that doesn't require unsafe code nor Mono:

using System;
using System.Runtime.InteropServices;
using System.Text;

namespace UnameTest
{
    [StructLayout(LayoutKind.Sequential, Pack = 1)]
    internal struct Utsname
    {
        [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 65)]
        public byte[] sysname;
        [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 65)]
        public byte[] nodename;
        [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 65)]
        public byte[] release;
        [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 65)]
        public byte[] version;
        [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 65)]
        public byte[] machine;
        [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U1, SizeConst = 65)]
        public byte[] domainname;
    }

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

        public static void PrintUtsname()
        {
            Utsname buf = new Utsname();
            uname(ref buf);

            Console.WriteLine($"Utsname:");
            Console.WriteLine($"---------------------------------");
            Console.WriteLine($"sysname: {GetString(buf.sysname)}");
            Console.WriteLine($"nodename: {GetString(buf.nodename)}");
            Console.WriteLine($"release: {GetString(buf.release)}");
            Console.WriteLine($"version: {GetString(buf.version)}");
            Console.WriteLine($"machine: {GetString(buf.machine)}");
            Console.WriteLine($"domainname: {GetString(buf.domainname)}");
        }


        private static string GetString(in byte[] data)
        {
            var pos = Array.IndexOf<byte>(data, 0);
            return Encoding.ASCII.GetString(data, 0, (pos < 0) ? data.Length : pos);
        }
    }
}
Decarbonate answered 29/10, 2020 at 15:4 Comment(0)
M
0

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);
        }
}
Michaud answered 17/3, 2019 at 0:4 Comment(1)
Ah! I saw the field, but all the documentation had it wrapped in an ifdef -- couldn't get a straight answer as to whether it was a real field. I tried putting it in a few times, but should have persisted. Thanks! Also, will follow your advice on reading from /proc/sys/kernel/osrelease.Mickens
S
0

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
Sea answered 14/1, 2020 at 16:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.