我有一个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的调用。想法?
答案 0 :(得分:22)
默认情况下,Delphi使用所谓的 fastcall 调用约定。这意味着编译器尝试将参数传递给CPU寄存器中的函数,并且如果参数多于空闲寄存器,则仅使用堆栈。例如,Delphi使用(EAX,EDX,ECX)作为函数的前三个参数
在您的C#代码中,您实际上正在使用 stdcall 调用约定,该约定指示编译器通过堆栈传递参数(以相反的顺序,即先推送最后一个参数)并让被叫者清理堆栈
相反,C / C ++编译器使用的 cdecl 调用会强制调用者清理堆栈。
只要确保你在双方都使用相同的调用约定。 Stdcall主要用于因为它几乎可以在任何地方使用,并且每个编译器都支持它(Win32 API也使用此约定)。
请注意,.NET不支持 fastcall 。
答案 1 :(得分:16)
jn是对的。给定的函数原型不能直接从C#中调用,只要它在Delphi的register
调用约定中即可。您需要为它编写stdcall
包装函数 - 如果您没有源代码,可能在另一个DLL中 - 或者您需要让维护该函数的人员将其调用约定更改为stdcall
更新:我还看到第一个参数是Delphi字符串。这不是C#可以提供的东西。应该是PChar而不是。此外,重要的是要明确该函数是Ansi还是Unicode;如果DLL是用Delphi 2009(或更高版本)编写的,那么它是Unicode,否则它是Ansi。
答案 2 :(得分:2)
返回值可能是另一个问题。它可能是内存泄漏(它们在堆上分配缓冲区而从不释放它)或访问已经空闲的内存(它们返回一个本地字符串变量强制转换为PChar)。
从一个函数向另一个模块返回字符串(或一般的可变大小的数据)通常会产生问题。
一种解决方案(由winapi使用)是要求调用者传入缓冲区及其大小。缺点是如果缓冲区太小,则函数失败,调用者必须使用更大的缓冲区再次调用它。
另一种可能的解决方案是从函数中的堆中分配缓冲区并将其返回。然后,您需要导出另一个函数,调用者必须使用该函数再次释放分配的内存。这可以确保内存被分配它的相同运行时释放。
在不同(非borland)语言之间传递(Delphi)字符串参数可能是不可能的。甚至在Delphi模块之间,您要确保两个模块使用相同的内存管理器实例。通常这意味着添加“使用ShareMem”作为所有模块的第一次使用。 另一个区别是调用约定“register”是一个fastcall约定,但与fastcall MS编译器使用的不一样。
完全不同的解决方案可能是使用Delphi.net编译器之一重新编译Delphi dll。这需要做多少工作取决于他们的代码。
答案 3 :(得分:1)
我从未这样做,但尝试将代码更改为:
function CreateCode(SerialID : String;
StartDateOfYear, YearOfStartDate, YearOfEndDate, DatePeriod : Word;
CodeType,RecordNumber,StartHour,EndHour : Byte) : PChar; stdcall;
external 'CreateCodeDLL.dll';
请注意额外的stdcall。
Edit2:正如你从其他回复中看到的那样,你要么必须做上面的改变,要么写一个做同样事情的包装dll。
答案 4 :(得分:1)
在Delphi中创建一个COM包装器,并通过interop在C#代码中调用它。 Voila ...易于使用C#或任何其他未来平台。
答案 5 :(得分:1)
前几天我正在试图学习调用约定,我写了一些方法来转换各种方法。这是一个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;
}
}
}
答案 6 :(得分:0)
当您要求他们更改调用约定时,您还应该要求他们更改第一个参数,使其不是“字符串”。让他们使用指向(以null结尾的)char或widechar数组的指针。使用Delphi字符串作为DLL参数是一个坏主意,即使没有增加尝试实现跨语言兼容性的复杂性。此外,字符串变量将包含ASCII或Unicode内容,具体取决于他们使用的Delphi版本。