不更新C++ DLL源码,将带虚方法的实例指针从C#传入C++ DLL
嘿,这个问题我之前处理过类似的场景,核心就是要让C#的对象能被C++的虚函数接口正确调用,下面给你分步拆解两种可行的方案:
方案1:利用COM互操作(推荐,实现简单)
如果C++的IReporter可以按COM接口的逻辑兼容处理,这是最省心的方式,.NET会自动帮我们处理跨语言的调用适配:
1. 在C#中定义匹配的接口
必须保证方法顺序、参数类型和C++的IReporter完全一致,同时加上互操作属性:
[ComVisible(true)] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] public interface IReporter { // 对应C++的virtual void SetProgress(int iProgress) void SetProgress(int iProgress); // 对应C++的virtual void SetMessage(const char* pszMsg) // 用LPStr封送ANSI字符串,如果C++用的是宽字符就换成LPWStr void SetMessage([MarshalAs(UnmanagedType.LPStr)] string pszMsg); }
2. 声明P/Invoke调用SetTrace
注意调用约定要和C++的__stdcall匹配:
public static class NativeDll { // 替换成你的DLL文件名,如果C++导出的函数有名字粉碎(比如实际导出名是_SetTrace@8),要加上EntryPoint参数指定真实名字 [DllImport("YourDllName.dll", CallingConvention = CallingConvention.StdCall)] public static extern void SetTrace(int level, [MarshalAs(UnmanagedType.IUnknown)] IReporter reporter); }
3. 实现接口的C#业务类
这个类就是实际接收进度和消息的处理逻辑:
public class MyProgressReporter : IReporter { public void SetProgress(int iProgress) { // 这里写你的业务逻辑,比如更新UI进度条、记录日志 Console.WriteLine($"当前进度:{iProgress}%"); } public void SetMessage(string pszMsg) { Console.WriteLine($"收到通知消息:{pszMsg}"); } }
4. 调用示例
直接创建实例传给SetTrace即可,CLR会自动生成COM调用包装器(CCW)对接C++的虚函数调用:
var reporter = new MyProgressReporter(); NativeDll.SetTrace(1, reporter);
方案2:手动构建虚函数表(适配纯自定义虚类)
如果C的IReporter完全没有COM相关的虚函数(比如没继承IUnknown),那我们需要手动模拟C的虚表内存布局来实现对接:
1. 定义对应虚函数的委托
C++的虚函数调用是thiscall,所以每个委托的第一个参数是对象的this指针:
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void SetProgressDelegate(IntPtr thisPtr, int iProgress); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void SetMessageDelegate(IntPtr thisPtr, [MarshalAs(UnmanagedType.LPStr)] string pszMsg);
2. 定义匹配C++布局的结构体
模拟C++的虚表和对象内存结构:
[StructLayout(LayoutKind.Sequential)] private struct ReporterVTable { public SetProgressDelegate SetProgress; public SetMessageDelegate SetMessage; } [StructLayout(LayoutKind.Sequential)] public struct ReporterHandle { public IntPtr VTablePtr; }
3. 实现带手动虚表的C#类
需要自己管理虚表的非托管内存,同时防止实例被GC回收:
public class CustomProgressReporter { private ReporterVTable _vtable; private ReporterHandle _handle; private GCHandle _gcHandle; // 固定实例,避免被GC意外回收 public CustomProgressReporter() { // 绑定委托到实际处理方法 _vtable.SetProgress = OnSetProgress; _vtable.SetMessage = OnSetMessage; // 分配非托管内存存储虚表 IntPtr vtablePtr = Marshal.AllocHGlobal(Marshal.SizeOf(_vtable)); Marshal.StructureToPtr(_vtable, vtablePtr, false); // 设置对象的虚表指针 _handle.VTablePtr = vtablePtr; // 固定当前实例 _gcHandle = GCHandle.Alloc(this, GCHandleType.Normal); } public ReporterHandle GetHandle() => _handle; private void OnSetProgress(IntPtr thisPtr, int iProgress) { Console.WriteLine($"进度更新:{iProgress}"); } private void OnSetMessage(IntPtr thisPtr, string msg) { Console.WriteLine($"收到消息:{msg}"); } // 析构函数释放资源 ~CustomProgressReporter() { if (_handle.VTablePtr != IntPtr.Zero) { Marshal.FreeHGlobal(_handle.VTablePtr); _handle.VTablePtr = IntPtr.Zero; } if (_gcHandle.IsAllocated) { _gcHandle.Free(); } } }
4. 调用示例
var reporter = new CustomProgressReporter(); NativeDll.SetTrace(1, ref reporter.GetHandle()); // 注意这里的SetTrace P/Invoke声明要改成接收ref ReporterHandle
额外注意点
- 如果DLL的导出函数有名字修饰(比如C++编译后的
_SetTrace@8),一定要在DllImport里用EntryPoint参数指定真实的导出名。 - 字符串封送要和C++的字符集匹配:
char*对应LPStr,wchar_t*对应LPWStr。 - 手动管理内存的方案一定要记得在析构函数里释放非托管资源,避免内存泄漏。
内容的提问来源于stack exchange,提问作者Dragimi Smehurko
相关产品推荐
相关产品推荐

