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

Posted

技术标签:

【中文标题】在通过 P/Invoke 获得的 C++ 结构上设置 C# 回调【英文标题】:Set C# callback on a C++ struct obtained via P/Invoke 【发布时间】:2018-06-12 13:22:56 【问题描述】:

我正在尝试使用我已获得的外部 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();

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

我做错了什么?

【问题讨论】:

数据流错了,你要告诉原生代码你的回调函数。所以 Marshal.StructureToPtr()。确保将结构的副本保存在静态变量中,它的委托对象不会被垃圾收集。 谢谢汉斯,但我不确定我们是否在同一页面上。该结构已由 C++ dll 分配,我应该在 他们 创建的实例上设置回调。除非我完全误解了你的答案? 您不是在“分配”一个结构,而是在更新它。我们无法猜测您是否还需要使用 Marshal.PtrToStructure() 来保留结构中您不想修改的现有字段。如果它只包含函数指针,则不会。 好吧,C++ dll 中的代码为结构找到了一些内存,它给了我指针。该结构仅包含函数指针:其中一些已由 dll 分配并指向我需要调用的代码,其他一些在我检索指针时为空,我需要将回调分配给它们。 【参考方案1】:

这个:

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

将创建RemoteServerPluginI 的副本,以便您处理该副本。明显错了。

使用Marshal.WriteIntPtr()直接写信给ptr,比如:

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

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

那么你没有向我们展示回调的 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();

请注意,在您的示例中,StartServer 是包含在 RemoteServerPluginI 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 对象。

【讨论】:

我只是要评论副本以及将委托转换为函数指针的失败——你解释了它们。需要注意的另一件重要的事情是,当使用GetFunctionPointerForDelegate 时,必须保持代理实例处于活动状态,因为当它变得无法访问时,蹦床将与它一起被释放。 @BenVoigt 我打算添加它...但是它有一个 RemoteServerPluginI 类作为代表的容器,所以他是在正确的道路上。 删除有害副本后他就没有了(PtrToStructure 电话) @BenVoigt 和我想的一样:cry: @MicheleIppolito:更改GetServerPluginInterface 的返回类型可能有效,但您的类与C 结构不兼容,因此它不会像您想象的那么有用。 (委托类型的字段与与委托具有相同签名的函数指针不同 - 两者都是指针,但委托是指向数据对象的指针,而不是直接指向代码)你不能避免@ 987654352@电话。

以上是关于在通过 P/Invoke 获得的 C++ 结构上设置 C# 回调的主要内容,如果未能解决你的问题,请参考以下文章

p/invoke 方法返回空结构

P/Invoke Struct with Pointers, C++ from C#

P/Invoke 编组和解组 C# 和非托管 DLL 之间的二维数组、结构和指针

如何使用 p/invoke 终止从 C# 调用的 C++ 函数

使用 P/Invoke 时为非托管代码存储数据

在 C# 中为 p/invoke 创建一个基本的 C++ .dll