在通过P / Invoke获得的C ++结构上设置C#回调

时间:2018-06-12 13:22:56

标签: c# c++ .net interop dllimport

我正在尝试使用外部C ++ DLL我已经给出了(我没有源代码)。

DLL有一个函数返回指向struct的指针。 struct定义了一系列函数指针,用作我的应用程序的回调。

根据我收到的“文档”,我只是通过将指针设置为我自己的回调方法来“注册”我的回调,如下所示:

server->OnConnectionRequest = &myObj.OnConnectionRequest;

但是,我试图在C#中实现这一点。 我部分成功了。我可以:

  • 加载DLL;
  • 从函数中获取struct*指针;
  • 调用在对象上预定义的一些方法。

我不能做的是在对象上设置我自己的回调:编译器没有抱怨,运行时没有抱怨,但回调仍未被调用。

我将委托类型和类定义为(注意以前这被定义为结构,并且它的工作方式不同):

// Original C++ signature from the header:
// void (*OnRemoteConnectionRequest)(void *caller, const char *ip, int &accept);

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void OnRemoteConnectionRequestDelegate(IntPtr caller, string ip, ref int accept);

[StructLayout(LayoutKind.Sequential)]
public class RemoteServerPluginI
{
    public OnRemoteConnectionRequestDelegate OnRemoteConnectionRequest;
    // another dozen callbacks omitted
}

我有一个静态助手来从dll中检索实例:

public static class RemoteControlPlugin
{

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

    private static RemoteServerPluginI _instance = null;

    public static RemoteServerPluginI Instance
    {
        get
        {
            if (_instance != null)
                return _instance;

            var ptr = GetServerPluginInterface();
            _instance = Marshal.PtrToStructure<RemoteServerPluginI>(ptr);
            if (_instance == null)
                throw new InvalidOperationException("Could not obtain the server instance");

            return _instance;
        }
    }
}

最后这是我用来注册回调的方式:

public class CallBacks
{
    public CallBacks(RemoteServerPluginI server)
    {
        server.OnRemoteConnectionRequest = this.OnRemoteConnectionRequest;
    }

    public void OnRemoteConnectionRequest(IntPtr caller, string ip, ref int accept)
    {
        Console.WriteLine($"Remote connection request from {ip}");

        // I try to force a reject to see an error on the client,
        // but the client always connects successfully, implying
        // we never get to run this
        accept = 0;
    }
}

static void Main()
{
    var cb = new Callbacks(RemoteControlPlugin.Instance);
    RemoteControlPlugin.Instance.StartServer();
}

然而,当我使用客户端应用程序尝试连接到我的服务器时,我的回调永远不会运行。如你所见,在我的回调中我拒绝连接,因此客户端应该退出并出现错误,但事实并非如此。

我做错了什么?

1 个答案:

答案 0 :(得分:2)

这一个:

_instance = Marshal.PtrToStructure<RemoteServerPluginI>(ptr);

将创建RemoteServerPluginI的副本,因此您将处理该副本。显然是错的。

使用Marshal.WriteIntPtr()直接写入ptr,例如:

Marshal.WriteIntPtr(remoteServerPluginIPtr, 0, Marshal.GetFunctionPointerForDelegate(OnRemoteConnectionRequest));

您应该将委托指针的偏移量放在0中而不是struct

然后你没有向我们展示回调的C签名......也许你甚至在那里犯了一些错误。

正如Voigt所写,其他非常重要的是委托必须在本机库可以使用它的所有时间保持活动状态。执行此操作的标准方法是将其放在对象的字段/属性中,然后确保使对象保持活动状态(例如保持对它的引用)。您正在使用class RemoteServerPluginI执行此操作。另一种方法是GCHandle.Alloc(yourdelegate, GCHandleType.Normal)然后GCHandle.Free(),当您确定本机代码不会永远调用它时。

一些简单的示例代码。

C方:

extern "C"
{
    typedef struct _RemoteServerPluginI
    {
        void(*OnRemoteConnectionRequest)(void *caller, wchar_t *ip, int *accept);
        void(*StartServer)(void);
    } RemoteServerPluginI;

    void StartServer();

    RemoteServerPluginI _callbacks = { NULL, StartServer };

    void StartServer()
    {
        int accept = 0;
        _callbacks.OnRemoteConnectionRequest(NULL, L"127.0.0.1", &accept);
        wprintf(L"Accept: %d", accept);
    }

    __declspec(dllexport) RemoteServerPluginI* GetServerPluginInterface()
    {
        return &_callbacks;
    }
}

C#方:

[DllImport("CPlusPlusSide.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr GetServerPluginInterface();

public static void RemoteConnectionRequestTest(IntPtr caller, string ip, ref int accept)
{
    Console.WriteLine("C#: ip = {0}", ip);
    accept = 1;
}

public class Callbacks
{
    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    public delegate void OnRemoteConnectionRequestDelegate(IntPtr caller, [MarshalAs(UnmanagedType.LPWStr)]string ip, ref int accept);

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    public delegate void StartServerDelegate();

    public OnRemoteConnectionRequestDelegate RemoteConnectionRequest { get; set; }

    public StartServerDelegate StartServer { get; set; }
}

然后:

IntPtr rsp = GetServerPluginInterface();

var callbacks = new Callbacks
{
    RemoteConnectionRequest = RemoteConnectionRequestTest
};

Marshal.WriteIntPtr(rsp, 0, Marshal.GetFunctionPointerForDelegate(callbacks.RemoteConnectionRequest));
callbacks.StartServer = Marshal.GetDelegateForFunctionPointer<Callbacks.StartServerDelegate>(Marshal.ReadIntPtr(rsp, IntPtr.Size));

callbacks.StartServer();

请注意,通过您的示例,StartServerRemoteServerPluginI C结构中包含的委托。所以我们必须用Marshal.ReadIntPtr检索它的值并为它创建一个.NET委托。请注意使用GC.KeepAlive()以确保对象在代码中的某个点之前保持活动状态。其他常用方法是使用static变量(static变量的生命周期直到程序结束)

封装各种Marshal

的示例
public class Callbacks
{
    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    public delegate void OnRemoteConnectionRequestDelegate(IntPtr caller, [MarshalAs(UnmanagedType.LPWStr)]string ip, ref int accept);

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    public delegate void StartServerDelegate();

    private IntPtr ptr;

    public static implicit operator Callbacks(IntPtr ptr)
    {
        return new Callbacks(ptr);
    }

    public Callbacks(IntPtr ptr)
    {
        this.ptr = ptr;

        {
            IntPtr del = Marshal.ReadIntPtr(ptr, 0);

            if (del != IntPtr.Zero)
            {
                remoteConnectionRequest = Marshal.GetDelegateForFunctionPointer<OnRemoteConnectionRequestDelegate>(del);
            }
        }

        {
            IntPtr del = Marshal.ReadIntPtr(ptr, IntPtr.Size);

            if (del != IntPtr.Zero)
            {
                startServer = Marshal.GetDelegateForFunctionPointer<StartServerDelegate>(del);
            }
        }
    }

    private OnRemoteConnectionRequestDelegate remoteConnectionRequest;

    private StartServerDelegate startServer;

    public OnRemoteConnectionRequestDelegate RemoteConnectionRequest
    {
        get => remoteConnectionRequest;
        set
        {
            if (value != remoteConnectionRequest)
            {
                remoteConnectionRequest = value;
                Marshal.WriteIntPtr(ptr, 0, remoteConnectionRequest != null ? Marshal.GetFunctionPointerForDelegate(remoteConnectionRequest) : IntPtr.Zero);
            }
        }
    }

    public StartServerDelegate StartServer
    {
        get => startServer;
        set
        {
            if (value != startServer)
            {
                startServer = value;
                Marshal.WriteIntPtr(ptr, IntPtr.Size, startServer != null ? Marshal.GetFunctionPointerForDelegate(startServer) : IntPtr.Zero);
            }
        }
    }
}

然后

Callbacks callbacks = GetServerPluginInterface();
callbacks.RemoteConnectionRequest = RemoteConnectionRequestTest;
callbacks.StartServer();

while (true)
{
}

请注意,然后我会强制键入所有内容,完全隐藏IntPtr

[DllImport("CPlusPlusSide.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern CallbacksPtr GetServerPluginInterface();

[StructLayout(LayoutKind.Sequential)]
public struct CallbacksPtr
{
    public IntPtr Ptr;
}

public class Callbacks
{
    public static implicit operator Callbacks(CallbacksPtr ptr)
    {
        return new Callbacks(ptr.Ptr);
    }

    private Callbacks(IntPtr ptr)
    {
        ...
}

添加CallbacksPtr,它是IntPtr的垫片,可以隐式转换为完整的Callbacks对象。