C#调用Delphi DLL事件出现参数异常与崩溃,求原因分析
问题背景
你提供的Delphi 7 DLL和C#调用代码在运行时出现了参数值错乱,随后抛出System.AccessViolationException异常,提示内存访问错误。我们来一步步分析问题原因并解决它。
原代码展示
Delphi7 test.dll 代码
unit DLLFunction; interface uses Sysutils, classes, Dialogs; type TEvent = procedure(index, status, percent: integer) of object; stdcall; IMyDll = interface ['{42C845F8-F45C-4BC7-8CB5-E76658262C4A}'] procedure SetEvent(const value: TEvent); stdcall; end; TMyDllClass = class(TInterfacedObject, IMyDll) public procedure SetEvent(const value: TEvent); stdcall; end; procedure CreateDelphiClass(out intf: IMyDll); stdcall; implementation procedure TMyDllClass.SetEvent(const value: TEvent); stdcall; begin if Assigned (value) then begin ShowMessage('Event call'); value(1,2,3); end; end; exports createDelphiClass; end.
C# 定义代码
// event public delegate void TEvent(int index, int status, int percent); [ComImport, Guid("42C845F8-F45C-4BC7-8CB5-E76658262C4A"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] public interface IMyDll { // event [MethodImplAttribute(MethodImplOptions.PreserveSig)] void SetEvent([MarshalAs(UnmanagedType.FunctionPtr)]TEvent eventCallback); } class TestInterface { const string dllPath = "testDelphiDll.DLL"; [DllImport(dllPath, CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] public static extern void CreateDelphiClass(out IMyDll dll); public IMyDll getDelphiInterface() { IMyDll mydll = null; CreateDelphiClass(out mydll); return mydll; } }
C# 调用示例
TestInterface tInterface = new TestInterface(); var delphi = tInterface.getDelphiInterface(); delphi.SetEvent(delegate(int index, int status, int percent) { MessageBox.Show("index: " + index.ToString() + "\nstatus: " + status.ToString() + " \npercent: " + percent.ToString()); });
运行结果
index: -19238192731
status: 1
percent: 2随后应用程序崩溃,抛出异常:
发生未处理的System.AccessViolationException异常,位于DLLCallTest.exe中。
提示:您尝试读取或写入受保护的内存。大多数情况下,这表示其他内存已损坏。
问题根源
问题出在Delphi定义的TEvent类型上:
TEvent = procedure(index, status, percent: integer) of object; stdcall;
这里的of object是Delphi特有的语法,它定义的是对象方法指针——这种指针实际上包含两个部分:一个指向对象实例的Self指针,一个指向方法的代码指针。而你在C#中定义的TEvent委托是普通的函数指针,只包含代码指针,没有Self部分。
当Delphi尝试调用value(1,2,3)时,它会把Self指针压入栈中作为第一个隐藏参数,再传入你的三个显式参数,但C#的委托并没有准备这个Self参数,导致栈结构完全混乱:原本的index参数被错误地读取成了栈里的垃圾值,后续参数偏移错误,最终触发内存访问异常。
解决方案
最简单的修复方式是修改Delphi的事件定义,去掉of object,把它改成普通的stdcall函数指针,让它和C#的委托类型匹配:
修改后的Delphi代码
unit DLLFunction; interface uses Sysutils, classes, Dialogs; type // 去掉 of object,改成普通函数指针 TEvent = procedure(index, status, percent: integer); stdcall; IMyDll = interface ['{42C845F8-F45C-4BC7-8CB5-E76658262C4A}'] procedure SetEvent(const value: TEvent); stdcall; end; TMyDllClass = class(TInterfacedObject, IMyDll) public procedure SetEvent(const value: TEvent); stdcall; end; procedure CreateDelphiClass(out intf: IMyDll); stdcall; implementation procedure TMyDllClass.SetEvent(const value: TEvent); stdcall; begin if Assigned (value) then begin ShowMessage('Event call'); value(1,2,3); end; end; exports createDelphiClass; end.
C#代码无需修改(保持原定义即可)
调用示例也不变,此时运行就能得到正确的结果:
index: 1
status: 2
percent: 3
不会再抛出内存访问异常。
额外说明
如果你因为某些原因必须保留Delphi中的of object方法指针(比如需要在Delphi内部绑定对象方法),那需要在Delphi层做一个适配层:
- 定义一个普通函数指针类型,用于接收C#的委托
- 在Delphi类中保存这个普通函数指针,同时定义一个对象方法作为中间转发器
- 当需要触发事件时,先调用中间转发器,再由转发器调用C#的委托
不过这种方式会增加复杂度,对于你的场景来说,直接去掉of object是最简洁可靠的解决方案。
内容的提问来源于stack exchange,提问作者InKwon Devik Kim

