可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I have a Delphi DLL that I did not write, but need to call from a C# ASP.NET 3.5 app. Here is the function definition I got from the developers:
function CreateCode(SerialID : String;
StartDateOfYear, YearOfStartDate, YearOfEndDate, DatePeriod : Word;
CodeType,RecordNumber,StartHour,EndHour : Byte) : PChar;
external 'CreateCodeDLL.dll';
And here is my C# code:
[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);
And finally, my call to this method:
//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);
Every time I re-compile this exact code and run it, it returns a different value. The expected value is a 10-digit code comprised of numbers. The returned value is actually 12 digits.
The last important piece of information is that I have a test .EXE that has a GUI that allows me to test the DLL. Every test using the .EXE returns the same 10-digit number (the expected result).
So, I have to believe that I have declared my call to the DLL incorrectly. Thoughts?
回答1:
Delphi uses the so called fastcall calling convention by default. This means that the compiler tries to pass parameters to a function in the CPU registers and only uses the stack if there are more parameters than free registers. For example Delphi uses (EAX, EDX, ECX) for the first three parameters to a function.
In your C# code you're actually using the stdcall calling convention, which instructs the compiler to pass parameters via the stack (in reverse order, i.e. last param is pushed first) and to let the callee cleanup the stack.
In contrast, the cdecl calling used by C/C++ compilers forces the caller to cleanup the stack.
Just make sure you're using the same calling convention on both sides. Stdcall is mostly used because it can be used nearly everywhere and is supported by every compiler (Win32 APIs also use this convention).
Note that fastcall isn't supported by .NET anyway.
回答2:
jn is right. The function prototype, as given, cannot be easily called directly from C# as long as it is in Delphi's register
calling convention. You either need to write a stdcall
wrapper function for it - perhaps in another DLL if you don't have source - or you need to get the people who maintain the function to change its calling convention to stdcall
.
Update: I also see that the first argument is a Delphi string. This isn't something that C# can supply either. It should be a PChar instead. Also, it's important to be clear about whether the function is Ansi or Unicode; if the DLL is written with Delphi 2009 (or later), then it is Unicode, otherwise it is Ansi.
回答3:
The return value might be another problem. It is probably either a memory leak(They allocate a buffer on the heap and never free it) or an access to already free memory(They return a local string variable cast to PChar).
Returning strings(or variable sized data in general) from a function to another module is problematic in general.
One solution(used by winapi) is to require the caller to pass in a buffer and its size. The disadvantage of that is that if the buffer is too small the function fails, and the caller must call it again with a larger buffer.
Another possible solution is to allocate the buffer from the heap in the function and return it. Then you need to export another function which the caller must use to free the allocated memory again. This ensures that the memory is freed by the same runtime which allocated it.
Passing a (Delphi)string parameter between different(not borland) languages is probably impossible. And even between Delphi modules you to ensure both modules use the same instance of the memory manager. Usually this means adding "uses ShareMem" as the first uses to all modules.
Another difference is the calling convention "register" which is a fastcall convention, but not identical with the fastcall MS compilers use.
A completely different solution could be recompiling the Delphi dll with one of the Delphi.net compilers. How much work that is depends on their code.
回答4:
I've never done this but try changing your code to:
function CreateCode(SerialID : String;
StartDateOfYear, YearOfStartDate, YearOfEndDate, DatePeriod : Word;
CodeType,RecordNumber,StartHour,EndHour : Byte) : PChar; stdcall;
external 'CreateCodeDLL.dll';
Note the extra stdcall.
Edit2: As you can see from the other replies you either have to do the change above or write a wrapper dll that does the same thing.
回答5:
Create a COM wrapper in Delphi and call that in your C# code via interop. Voila.. easy to use from C# or any other future platform.
回答6:
I was messing around the other day trying to learn about calling conventions and I wrote some methods to convert between various ones. Here is one for 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;
}
}
}
回答7:
While you are asking them to change the calling convention, you should also ask them to change the first parameter so that it is not a "string". Get them to use a pointer to a (null-terminated) char or widechar array instead. Using Delphi strings as DLL parameters is a bad idea even without the added complexity of trying to achieve cross-language compatibility. In addition the string variable will either contain ASCII or Unicode content depending on which version of Delphi they are using.