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

时间:2009-02-05 21:14:28

标签: 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的调用。想法?

7 个答案:

答案 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版本。