Delphi调用实现接口的C++ DLL触发Access Violation问题排查
Delphi调用C++ DLL时MonitorProc触发Access Violation的问题分析与解决
我之前也踩过跨语言调用虚函数的坑,结合你的代码来看,问题核心在于Delphi和C++的虚函数表(VMT)结构不兼容,再加上你调用了一个根本没有实现的纯虚函数,直接触发了内存访问错误。下面一步步拆解问题和解决方法:
先回顾你的场景代码
原C++头文件
class MyINTERFACE { public: virtual void MonitorProc(int value1, int value2) = 0; }; extern MY_API MyHANDLE* Init(); extern MY_API UINT InitMonitorProc(MyHANDLE * handle, MyINTERFACE * pMyProc);
你的Delphi转换单元
unit myCdllUnit; interface const TMyHANDLE = DWORD; MyINTERFACE = class public procedure MonitorProc(value1: longint; value2: longint); virtual; cdecl; abstract; end; function Init(): TMyHANDLE; cdecl; external 'myCdll.dll'; function InitMonitorProc(handle: TMyHANDLE; pMyProc: MyINTERFACE):DWORD; cdecl; external 'myCdll.dll'; implementation end.
主窗体中的重写代码
TMyMonitor = class(MyINTERFACE) public procedure MonitorProc(value1: longint; value2: longint); override; end; procedure TMyMonitor.MonitorProc(value1: longint; value2: longint); begin inherited; // <--------------------------- 这里就是触发AV的直接原因 end;
初始化调用
MyMonitor := TMyMonitor.Create; InitMonitorProc(Handle, MyMonitor);
问题到底出在哪?
- 调用纯虚函数的致命错误:C++里的
virtual void MonitorProc(...) = 0是纯虚函数,完全没有实现代码。你在Delphi里调用inherited;,本质就是去执行这个不存在的函数,直接导致内存访问违规。 - VMT结构不兼容:就算你去掉
inherited,后续DLL调用MonitorProc时还是会出问题。因为Delphi的类虚函数表(VMT)和C的完全不一样——Delphi的VMT开头有类类型指针、实例大小等额外字段,而C的VMT直接是虚函数指针列表。当C++ DLL按照自己的规则去寻址虚函数时,会拿到错误的内存地址,最终还是触发AV。
正确的解决方法:用接口+手工模拟VMT适配
不能直接用Delphi的类去继承C的抽象类,必须做一层适配,让C能识别虚函数表结构。这里推荐用Delphi的接口来实现业务逻辑,再用一个包装类模拟C++的VMT结构:
1. 改写Delphi单元
unit myCdllUnit; interface type TMyHANDLE = DWORD; // 定义Delphi接口,对应C++的抽象类方法 IMyINTERFACE = interface ['{5B2F87A1-7E7D-4F0C-8F3A-1D7E4F9B2C3D}'] // 按Ctrl+Shift+G生成唯一GUID procedure MonitorProc(value1: Integer; value2: Integer); cdecl; end; // 包装类:模拟C++的类结构和虚函数表 TMyInterfaceWrapper = class public VMT: Pointer; // 对应C++类的第一个成员:虚函数表指针 FIntf: IMyINTERFACE; // 持有Delphi接口实例 constructor Create(const AIntf: IMyINTERFACE); procedure MonitorProc(value1: Integer; value2: Integer); cdecl; end; function Init(): TMyHANDLE; cdecl; external 'myCdll.dll'; // 注意这里参数改成Pointer,因为我们传的是模拟的C++类实例 function InitMonitorProc(handle: TMyHANDLE; pMyProc: Pointer): DWORD; cdecl; external 'myCdll.dll'; implementation { TMyInterfaceWrapper } constructor TMyInterfaceWrapper.Create(const AIntf: IMyINTERFACE); type // 模拟C++的虚函数表:按声明顺序排列函数指针 TVMT = array[0..0] of Pointer; var LocalVMT: TVMT; begin inherited Create; FIntf := AIntf; // 把MonitorProc的地址放到VMT的第一个位置(对应C++的第一个虚函数) LocalVMT[0] := @TMyInterfaceWrapper.MonitorProc; // 把VMT的地址赋值给类的VMT字段 Self.VMT := @LocalVMT; end; procedure TMyInterfaceWrapper.MonitorProc(value1, value2: Integer); begin // 转发到Delphi接口的实现 FIntf.MonitorProc(value1, value2); end; end.
2. 修改主窗体的实现
TMyMonitor = class(TInterfacedObject, IMyINTERFACE) public procedure MonitorProc(value1: Integer; value2: Integer); cdecl; end; procedure TMyMonitor.MonitorProc(value1: Integer; value2: Integer); begin // 这里写你的业务逻辑,不需要inherited ShowMessageFmt('收到监控值:Value1=%d, Value2=%d', [value1, value2]); end;
3. 调整FormCreate的调用
var MyMonitor: IMyINTERFACE; Wrapper: TMyInterfaceWrapper; begin MyMonitor := TMyMonitor.Create; Wrapper := TMyInterfaceWrapper.Create(MyMonitor); InitMonitorProc(Handle, Wrapper); // 注意:如果DLL会长期持有Wrapper,要把它保存到窗体的字段中,避免被释放 end;
额外注意事项
- 虚函数的调用约定必须严格一致:C++里如果是默认的
__cdecl,Delphi里也要用cdecl,不能错用stdcall或者其他约定。 - 模拟VMT时,函数指针的顺序必须和C类里的虚函数声明顺序完全一致,C的VMT是按声明顺序排列的。
- 不要让Wrapper被提前释放:如果DLL在后台会持续调用
MonitorProc,必须确保Wrapper实例一直存在,比如把它赋值给窗体的全局字段。
内容的提问来源于stack exchange,提问作者John Vantelli
相关产品推荐
相关产品推荐

