2

I'm working on porting a .Net Core 3.1 application from windows to Linux. I have a ourdevice.dll on windows, and the equivalent ourdevice.so built for Linux. This dll is a native dll that we are using on windows with a pinvoke wrapper.

We load the functions from the native dll using DllImports from kernel32.dll

    [DllImport("kernel32.dll", EntryPoint = "LoadLibrary", SetLastError = true)]
    public static extern IntPtr LoadLibrary(string dllToLoad);

    [DllImport("kernel32.dll")]
    public static extern IntPtr GetProcAddress(IntPtr hModule, string procedureName);

    [DllImport("kernel32.dll")]
    public static extern bool FreeLibrary(IntPtr hModule);

We create delegates for each of the funtions to import:

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    private delegate short AdcOpen([MarshalAs(UnmanagedType.LPStr)] string adcName, [MarshalAs(UnmanagedType.LPStr)] string protocol, [MarshalAs(UnmanagedType.LPStr)] string port, ref short handle, byte performSwReset);
    private AdcOpen adcOpen;

Then we map all the native functions something like this:

        IntPtr pAddressOfFunction;

        pDll = LoadLibrary(LibraryName);

        // check if library is loaded
        if (pDll != IntPtr.Zero)
        {
            // get proc address of the native functions
            pAddressOfFunction = GetProcAddress(pDll, "AdcOpen");
            if (pAddressOfFunction != IntPtr.Zero)
                adcOpen = (AdcOpen)Marshal.GetDelegateForFunctionPointer(pAddressOfFunction, typeof(AdcOpen));
            else
                message += "Function AdcOpen not found\n";
          
            //snip loading all the exported functions

        }

In the Linux application, obviously, we can't use kernel32.dll. We are writing C# in .Net core on Linux, and we have the native module ourdevice.so. How can I import the functions from the native Linux module into our C# wrapper? Is it even possible?

trashrobber
  • 727
  • 2
  • 9
  • 26

2 Answers2

5

LoadLibrary:

[DllImport("libdl", ExactSpelling = true)]
public static extern IntPtr dlopen(string filename, int flags);

GetProcAddress:

[DllImport("libdl", ExactSpelling = true)]
public static extern IntPtr dlsym(IntPtr handle, string symbol);

FreeLibrary:

[DllImport("libdl", ExactSpelling = true)]
public static extern int dlclose(IntPtr handle);

Sample usage:

const int RTLD_NOW = 0x002;
IntPtr pDll = dlopen("ourdevice.so.0", RTLD_NOW);
IntPtr pAddressOfFunction = dlsym(pDll, "AdcOpen");
...
dlclose(pDll);
mm8
  • 163,881
  • 10
  • 57
  • 88
4

A couple of thoughts.

First, yes, DllImport (P/Invoke) works on Linux (and macOS) too.

This seems a bit strange:

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    private delegate short AdcOpen([MarshalAs(UnmanagedType.LPStr)] string adcName, [MarshalAs(UnmanagedType.LPStr)] string protocol, [MarshalAs(UnmanagedType.LPStr)] string port, ref short handle, byte performSwReset);
    private AdcOpen adcOpen;

With the mapping code (LoadLibrary, GetProcAddress, FreeLibrary), this seems like you are manually doing the work that .NET itself does if you do something like this - where you let .NET do the function finding and binding:

    [DllImport("ourdevice.dll", CallingConvention = CallingConvention.Cdecl)]
    private static extern short AdcOpen([MarshalAs(UnmanagedType.LPStr)] string adcName, [MarshalAs(UnmanagedType.LPStr)] string protocol, [MarshalAs(UnmanagedType.LPStr)] string port, ref short handle, byte performSwReset);

Any reason for doing it so differently?

Because if you were to do it that way, I expect the Linux version will look like this and work pretty much the same way as it does on Windows:

    [DllImport("libourdevice.so", CallingConvention = CallingConvention.Cdecl)]
    private static extern short AdcOpen([MarshalAs(UnmanagedType.LPStr)] string adcName, [MarshalAs(UnmanagedType.LPStr)] string protocol, [MarshalAs(UnmanagedType.LPStr)] string port, ref short handle, byte performSwReset);

Assuming you are using the same calling convention and argument marshalling. The only difference is the name change from "ourdevice.dll" to "libdevice.so". You will also need to place libdevice.so somewhere where it can be found; next to the application executable should work.

If you still want to do it manually via the LoadLibrary/GetProcAddress/FreeLibrary approach, you can use the roughly-equivalent methods on Linux:

  • dlopen (similar to LoadLibrary)
  • dlsym (similar to GetProcAddress)
  • dlclose (similar to FreeLibrary)

See https://stackoverflow.com/a/13492645/3561275 for examples and sample code on how to use these methods.

omajid
  • 14,165
  • 4
  • 47
  • 64
  • Thanks for your answer, very helpful. I see what you mean about the way we load the dll/object, and I could just load the functions from the module directly without the LoadLibrary and GetProcAddress calls. I didn't write this wrapper, so I'm not sure why they did it that way, but it looks like its handling possible multiple versions of the module and checking if each function exists before assigning it to the delegate. – trashrobber Jan 06 '22 at 22:02
  • 1
    I'm marking this as the answer. Unfortunately I have now realized this can never work for me, because the native module is x86/32bit, but the Linux .Net runtime is only x64. – trashrobber Jan 07 '22 at 18:24