【问题标题】:Calling a Delphi DLL from C# produces unexpected results从 C# 调用 Delphi DLL 会产生意外结果
【发布时间】:2009-02-05 21:14:27
【问题描述】:

我有一个我没有编写的 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 的调用。想法?

【问题讨论】:

    标签: c# delphi dll


    【解决方案1】:

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

    【讨论】:

    • 请注意,“fastcall”在不同的上下文中意味着不同的东西。 Microsoft 的版本与 Embarcadero 的不同,我怀疑 GCC 的版本与它们都不同。在 Delphi 中,调用约定甚至不称为“fastcall”。这是“注册”调用约定。
    【解决方案2】:

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

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

    【讨论】:

      【解决方案3】:

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

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

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

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

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

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

      【讨论】:

        【解决方案4】:

        我从未这样做过,但尝试将您的代码更改为:

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

        注意额外的标准调用。

        Edit2:正如您从其他回复中看到的那样,您要么必须进行上述更改,要么编写一个执行相同操作的包装 dll。

        【讨论】:

        • Pascal 作为调用类型不起作用,因为 Delphi 使用寄存器调用(也称为 __fastcall,不是 __msfastcall)。
        • 根据msdn.microsoft.com/en-us/library/…,.net 不支持任何类型的 FastCall。所以这意味着他必须要求他们更改帕斯卡密码?
        【解决方案5】:

        在 Delphi 中创建一个 COM 包装器,并通过互操作在您的 C# 代码中调用它。瞧.. 易于在 C# 或任何其他未来平台上使用。

        【讨论】:

          【解决方案6】:

          前几天我在搞乱,试图学习调用约定,我写了一些方法来在各种方法之间进行转换。这是 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】:

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

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2012-03-30
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2012-09-03
              相关资源
              最近更新 更多