从C#调用Delphi DLL会产生意外结果

我有一个Delphi DLL,我没有写,但需要从C#ASP.NET 3.5应用程序调用。 这是我从开发人员那里得到的函数定义:

function CreateCode(SerialID : String; StartDateOfYear, YearOfStartDate, YearOfEndDate, DatePeriod : Word; CodeType,RecordNumber,StartHour,EndHour : Byte) : PChar; external 'CreateCodeDLL.dll'; 

这是我的C#代码:

 [DllImport( "CreateCodeDLL.dll", CallingConvention = CallingConvention.StdCall, CharSet=CharSet.Ansi)] public static extern IntPtr CreateCode( string SerialID, UInt16 StartDateOfYear, UInt16 YearOfStartDate, UInt16 YearOfEndDate, UInt16 DatePeriod, Byte CodeType, Byte RecordNumber, Byte StartHour, Byte EndHour); 

最后,我对这种方法的调用:

 //The Inputs String serialID = "92F00000B4FBE"; UInt16 StartDateOfYear = 20; UInt16 YearOfStartDate = 2009; UInt16 YearOfEndDate = 2009; UInt16 DatePeriod = 7; Byte CodeType = 1; Byte RecordNumber = 0; Byte StartHour = 15; Byte EndHour = 14; // The DLL call IntPtr codePtr = CodeGenerator.CreateCode(serialID, StartDateOfYear, YearOfStartDate, YearOfEndDate, DatePeriod, CodeType, RecordNumber, StartHour, EndHour); // Take the pointer and extract the code in a string String code = Marshal.PtrToStringAnsi(codePtr); 

每次我重新编译这个确切的代码并运行它,它返回一个不同的值。 预期值是由数字组成的10位数代码。 返回的值实际上是12位数。

最后一个重要的信息是我有一个测试.EXE,它有一个允许我测试DLL的GUI。 使用.EXE的每个测试都返回相同的10位数字(预期结果)。

所以,我必须相信我已经错误地声明了我对DLL的调用。 思考?

Delphi默认使用所谓的fastcall调用约定。 这意味着编译器尝试将参数传递给CPU寄存器中的函数,并且如果参数多于空闲寄存器,则仅使用堆栈。 例如,Delphi使用(EAX,EDX,ECX)作为函数的前三个参数。
在您的C#代码中,您实际上正在使用stdcall调用约定,该约定指示编译器通过堆栈传递参数(以相反的顺序,即先推送最后一个参数)并让被调用者清理堆栈。
相反,C / C ++编译器使用的cdecl调用强制调用者清理堆栈。
只要确保你在双方都使用相同的调用约定。 Stdcall主要用于因为它几乎可以在任何地方使用,并且每个编译器都支持它(Win32 API也使用此约定)。
请注意,.NET不支持fastcall

jn是对的。 给定的函数原型不能直接从C#中调用,只要它在Delphi的register调用约定中即可。 您需要为它编写stdcall包装函数 – 如果您没有源代码,可能在另一个DLL中 – 或者您需要让维护该函数的人员将其调用约定更改为stdcall

更新:我还看到第一个参数是Delphi字符串。 这不是C#可以提供的东西。 应该是PChar而不是。 此外,重要的是要明确该函数是Ansi还是Unicode; 如果DLL是用Delphi 2009(或更高版本)编写的,那么它是Unicode,否则它是Ansi。

返回值可能是另一个问题。 它可能是内存泄漏(它们在堆上分配缓冲区而从不释放它)或访问已经空闲的内存(它们返回一个本地字符串变量强制转换为PChar)。

一般来说,将字符串(或一般的可变大小的数据)从函数返回到另一个模块是有问题的。

一个解决方案(由winapi使用)是要求调用者传入缓冲区及其大小。 缺点是如果缓冲区太小,则函数失败,调用者必须使用更大的缓冲区再次调用它。

另一种可能的解决方案是从函数中的堆中分配缓冲区并返回它。 然后,您需要导出另一个函数,调用者必须使用该函数再次释放分配的内存。 这可确保内存由分配它的相同运行时释放。

在不同(非borland)语言之间传递(Delphi)字符串参数可能是不可能的。 甚至在Delphi模块之间,您要确保两个模块使用相同的内存管理器实例。 通常这意味着添加“使用ShareMem”作为所有模块的第一次使用。 另一个区别是调用约定“register”是一个fastcall约定,但与fastcall MS编译器使用的不一样。

一个完全不同的解决方案是使用Delphi.net编译器之一重新编译Delphi dll。 这需要多少工作取决于他们的代码。

我从来没有这样做,但尝试将代码更改为:

 function CreateCode(SerialID : String; StartDateOfYear, YearOfStartDate, YearOfEndDate, DatePeriod : Word; CodeType,RecordNumber,StartHour,EndHour : Byte) : PChar; stdcall; external 'CreateCodeDLL.dll'; 

注意额外的stdcall。

编辑2:从其他回复中可以看出,您要么必须进行上述更改,要么编写一个执行相同操作的包装器DLL。

在Delphi中创建一个COM包装器,并通过interop在C#代码中调用它。 Voila …易于使用C#或任何其他未来平台。

前几天我在试图学习调用约定时弄得一团糟,我写了一些方法来转换各种方法。 这是一个StdCall-> FastCall。

 typedef struct { USHORT ParameterOneOffset; // The offset of the first parameter in dwords starting at one USHORT ParameterTwoOffset; // The offset of the second parmaeter in dwords starting at one } FastCallParameterInfo; __declspec( naked,dllexport ) void __stdcall InvokeFast() { FastCallParameterInfo paramInfo; int functionAddress; int retAddress; int paramOne, paramTwo; __asm { // Pop the return address and parameter info. Store in memory. pop retAddress; pop paramInfo; pop functionAddress; // Check if any parameters should be stored in edx movzx ecx, paramInfo.ParameterOneOffset; cmp ecx,0; je NoRegister; // Calculate the offset for parameter one. movzx ecx, paramInfo.ParameterOneOffset; // Move the parameter one offset to ecx dec ecx; // Decrement by 1 mov eax, 4; // Put 4 in eax mul ecx; // Multiple offset by 4 // Copy the value from the stack on to the register. mov ecx, esp; // Move the stack pointer to ecx add ecx, eax; // Subtract the offset. mov eax, ecx; // Store in eax for later. mov ecx, [ecx]; // Derefernce the value mov paramOne, ecx; // Store the value in memory. // Fix up stack add esp,4; // Decrement the stack pointer movzx edx, paramInfo.ParameterOneOffset; // Move the parameter one offset to edx dec edx; // Decrement by 1 cmp edx,0; // Compare offset with zero je ParamOneNoShift; // If first parameter then no shift. ParamOneShiftLoop: mov ecx, eax; sub ecx, 4; mov ecx, [ecx] mov [eax], ecx; // Copy value over sub eax, 4; // Go to next dec edx; // decrement edx jnz ParamOneShiftLoop; // Loop ParamOneNoShift: // Check if any parameters should be stored in edx movzx ecx, paramInfo.ParameterTwoOffset; cmp ecx,0; je NoRegister; movzx ecx, paramInfo.ParameterTwoOffset; // Move the parameter two offset to ecx sub ecx, 2; // Increment the offset by two. One extra for since we already shifted for ecx mov eax, 4; // Put 4 in eax mul ecx; // Multiple by 4 // Copy the value from the stack on to the register. mov ecx, esp; // Move the stack pointer to ecx add ecx, eax; // Subtract the offset. mov eax, ecx; // Store in eax for later. mov ecx, [ecx]; // Derefernce the value mov paramTwo, ecx; // Store the value in memory. // Fix up stack add esp,4; // Decrement the stack pointer movzx edx, paramInfo.ParameterTwoOffset; // Move the parameter two offset to ecx dec edx; // Decrement by 1 cmp edx,0; // Compare offset with zero je NoRegister; // If first parameter then no shift. ParamTwoShiftLoop: mov ecx, eax; sub ecx, 4; mov ecx, [ecx] mov [eax], ecx; // Copy value over sub eax, 4; // Go to next dec edx; // decrement edx jnz ParamTwoShiftLoop; // Loop NoRegister: mov ecx, paramOne; // Copy value from memory to ecx register mov edx, paramTwo; // push retAddress; jmp functionAddress; } } 

}

当您要求他们更改调用约定时,您还应该要求他们更改第一个参数,使其不是“字符串”。 让他们使用指向(以null结尾的)char或widechar数组的指针。 使用Delphi字符串作为DLL参数是一个坏主意,即使没有增加尝试实现跨语言兼容性的复杂性。 此外,字符串变量将包含ASCII或Unicode内容,具体取决于他们使用的Delphi版本。