0

I have been given an external c++ dll that I need to load and use in my C# project. The dll comes with a header file, which is this (simplified / anonymized):

typedef struct
{
    int (*GetVersion)();
    int (*StartServer)(const char *ip, int port);
    void (*OnRemoteError)(void *caller, int error);
} RemoteServerPluginI;

extern "C" __declspec(dllexport) RemoteServerPluginI* GetServerPluginInterface();

I have a few questions on how to use this in my C# project:

  • do I translate "void*" with object?
  • do I translate the char* array to a string or to a char[] ?
  • OnRemoteError is supposed to be a callback; to register my callback, should I simply assign my callback function to this field?

Any link to the relevant documentation is most appreciated.

Michele Ippolito
  • 686
  • 1
  • 5
  • 12
  • This is pretty bad to have to work with directly in C#. `UnmanagedFunctionPointerAttribute` and `Marshal.GetDelegateForFunctionPointer` help, but even then you're dealing with a lot of clumsy and error-prone translation. Consider using a C++/CLI intermediate layer to help translate this to a regular .NET interface. – Jeroen Mostert Jun 06 '18 at 15:24
  • Well, I do have the option to wrap this dll with its own wrapper assembly, so I'll look into that. I never used managed C++ to wrap an assembly before though: do you have any pointer (pun intended) to start? – Michele Ippolito Jun 06 '18 at 15:28
  • I'm afraid not. I've only ever used it once myself, to write [this answer](https://stackoverflow.com/a/45470469/4137916). That does show how to mix managed and unmanaged code, even though it's far simpler than your scenario. – Jeroen Mostert Jun 06 '18 at 15:34
  • Thank you very much, I'll start from there! – Michele Ippolito Jun 06 '18 at 15:41
  • void* is IntPtr and char* here is string. You'll need to apply cdecl calling convention to all delegates. And that's it. I'd persevere in C# unless there's loads more interop like this. – David Heffernan Jun 06 '18 at 16:59
  • There is a memory management problem in this code, the pinvoke marshaller does not know how to release the storage for the struct. It *probably* does not have to be released, even though it is not declared as a const pointer. Well, hopefully, because you can't. Notably a problem in a C program as well, that never gets better when you pinvoke. You need to declare the return type as IntPtr and marshal it yourself with Marshal.PtrToStructure(). – Hans Passant Jun 06 '18 at 17:06
  • Thanks for pointing that out. Luckily it's not an issue for me as that object is really a singleton that will live through the whole application. – Michele Ippolito Jun 07 '18 at 10:14

1 Answers1

0

I might have figured it out, after a ton of reading and helpful pointers both here on SO and reddit (a special thank you to this comment).

BIG DISCLAIMER: at this time I haven't been able to interface with the actual system, so this might be wrong. However I've successfully loaded the dll and read the version, which makes me think I might have solved it. If anything comes up I will update the answer.

First thing is to declare a struct to map the C++ struct into our C# code. We can use the MarshalAs attribute to tell the marshaller that these delegates are really just function pointers:

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate int GetVersionT();

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate int StartServerT(string ip, int port);

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void OnRemoteErrorT(object caller, int error);

public struct RemoteServerPluginI
{
    [MarshalAs(UnmanagedType.FunctionPtr)] public GetVersionT GetVersion;
    [MarshalAs(UnmanagedType.FunctionPtr)] public StartServerT StartServer;
    [MarshalAs(UnmanagedType.FunctionPtr)] public OnRemoteErrorT OnRemoteError;
    // a lot of other methods not shown
}

Then we make a helper class that loads the DLL using DLLImport and calls the method that was defined in the dll.

This is easily done using an extern method:

[DllImport("data/remoteplugin.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr GetServerPluginInterface();

Note that we had to specify the calling convention. Another important thing to note: this method returns an IntPtr object. Luckily we are done now, we just need to cast this to the correct type:

var ptr = GetServerPluginInterface();
var server = (RemoteServerPluginI)Marshal.PtrToStructure(ptr, typeof(RemoteServerPluginI));

At this point I just wrapped everything into a convenience class to manage access, and voilà! Here is the final code:

public static class IntPtrExtensions
{
    // I'm lazy
    public static T ToStruct<T>(this IntPtr ptr)
        => (T)Marshal.PtrToStructure(ptr, typeof(T));
}

public static class RemoteControlPlugin
{
    [DllImport("path/to/remoteplugin.dll", CallingConvention = CallingConvention.Cdecl)]
    private static extern IntPtr GetServerPluginInterface();

    private static RemoteServerPluginI? _instance = null;

    public static RemoteServerPluginI Instance =>
        (RemoteServerPluginI)(
            _instance ??
            (_instance = GetServerPluginInterface().ToStruct<RemoteServerPluginI>())
        );
}

internal static class Program
{
    private static void Main(string[] args)
    {
        var remoteServer = RemoteControlPlugin.Instance;
        Console.WriteLine(remoteServer.GetVersion());  // Easy!
    }
}
Michele Ippolito
  • 686
  • 1
  • 5
  • 12