2009-02-05 16 views
8

Tengo una DLL de Delphi que no escribí, pero necesito llamar desde una aplicación C# ASP.NET 3.5. Esta es la definición de la función que obtuve de los desarrolladores:Llamar a una DLL de Delphi desde C# produce resultados inesperados

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

Y aquí está mi código 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); 

Y, por último, mi llamada a este método:

//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); 

Cada vez Re-compilo este código exacto y lo ejecuto, devuelve un valor diferente. El valor esperado es un código de 10 dígitos compuesto de números. El valor devuelto es en realidad de 12 dígitos.

La última pieza importante de información es que tengo una prueba .EXE que tiene una GUI que me permite probar la DLL. Cada prueba que usa el .EXE devuelve el mismo número de 10 dígitos (el resultado esperado).

Por lo tanto, tengo que creer que he declarado incorrectamente mi llamada a la DLL. ¿Pensamientos?

Respuesta

22

Delphi utiliza el llamado fastcall convención de llamada por defecto. Esto significa que el compilador intenta pasar parámetros a una función en los registros de la CPU y solo usa la pila si hay más parámetros que registros libres. Por ejemplo, Delphi usa (EAX, EDX, ECX) los primeros tres parámetros de una función.
En su código C# en realidad estás usando el stdcall convención de llamada, que indica al compilador para pasar parámetros a través de la pila (en orden inverso, es decir, la última parámetro es empujado primero) y dejar que la limpieza del destinatario de la llamada de la pila .
Por el contrario, el cdecl llamadas utilizadas por los compiladores C/C++ obliga a la persona que llama a limpiar la pila.
Solo asegúrese de estar usando la misma convención de llamadas en ambos lados. Stdcall se usa principalmente porque se puede usar en casi todas partes y es compatible con todos los compiladores (las API Win32 también usan esta convención).
Tenga en cuenta que fastcall no es compatible con .NET de todos modos.

+4

Tenga en cuenta que "llamada rápida" significa cosas diferentes en diferentes contextos. La versión de Microsoft no es la misma que la de Embarcadero, y sospecho que la de GCC es diferente de ambas. En Delphi, la convención de llamadas ni siquiera se llama "llamada rápida"; es la convención de llamadas de "registro". –

1

que nunca he hecho esto, pero trate de cambiar su código para:

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

Nota del stdcall adicional.

Edit2: Como puede ver en las otras respuestas, o bien tiene que hacer el cambio anterior o escribir un contenedor dll que haga lo mismo.

+0

Pascal como el tipo de llamada no funcionaría porque Delphi utiliza la llamada registro (también conocido como __fastcall que no se __msfastcall). –

+0

Según http://msdn.microsoft.com/en-us/library/system.runtime.interopservices.callingconvention.aspx, .net no admite ningún tipo de FastCall. Entonces, ¿esto significa que tiene que pedirles que cambien el código pascal? – PetriW

16

jn tiene razón. El prototipo de la función, tal como se da, no se puede llamar fácilmente directamente desde C#, siempre que esté en la convención de llamadas de Delphi register. Para ello, debe escribir una función de contenedor stdcall, quizás en otra DLL si no tiene la fuente, o debe hacer que las personas que mantienen la función cambien su convención de llamadas al stdcall.

Actualización: También veo que el primer argumento es una cadena Delphi. Esto tampoco es algo que C# pueda proporcionar. Debería ser un PChar en su lugar.Además, es importante tener claro si la función es Ansi o Unicode; si el archivo DLL está escrito con Delphi 2009 (o posterior), entonces es Unicode, de lo contrario, es Ansi.

0

Mientras les pide que cambien la convención de llamadas, también debe pedirles que cambien el primer parámetro para que no sea una "cadena". Pídales que usen un puntero a un carácter de caracteres (terminado en nulo) o a un conjunto de ancho ancho. Usar cadenas de Delphi como parámetros de DLL es una mala idea, incluso sin la complejidad adicional de tratar de lograr la compatibilidad entre idiomas. Además, la variable de cadena contendrá contenido ASCII o Unicode según la versión de Delphi que esté utilizando.

2

El valor de retorno puede ser otro problema. Probablemente sea una pérdida de memoria (asignan un búfer en el montón y nunca lo liberan) o un acceso a la memoria ya libre (Devuelven un molde de variable de cadena local a PChar).

La devolución de cadenas (o datos de tamaño variable en general) desde una función a otro módulo es problemática en general.

Una solución (utilizada por winapi) es requerir que la persona que llama pase en un búfer y su tamaño. La desventaja de esto es que si el buffer es demasiado pequeño, la función falla, y el que llama debe llamarlo nuevamente con un buffer más grande.

Otra posible solución es asignar el búfer del montón en la función y devolverlo. Luego necesita exportar otra función que la persona que llama debe usar para liberar nuevamente la memoria asignada. Esto asegura que la memoria se libera por el mismo tiempo de ejecución que lo asignó.

Pasar un parámetro de cadena (Delphi) entre diferentes idiomas (no borland) es probablemente imposible. E incluso entre los módulos de Delphi, asegúrese de que ambos módulos usen la misma instancia del administrador de memoria. Por lo general, esto significa agregar "usos ShareMem" como el primer uso para todos los módulos. Otra diferencia es el "registro" de la convención de llamadas, que es una convención de llamada rápida, pero no idéntica al uso de los compiladores rápidos de MS.

Una solución completamente diferente podría estar recompilando el Delphi dll con uno de los compiladores de Delphi.net. Cuánto trabajo es depende de su código.

1

Crea un contenedor COM en Delphi y llámalo en tu código C# por interoperabilidad. Voila .. fácil de usar desde C# o cualquier otra plataforma futura.

1

Estaba jugando el otro día tratando de aprender sobre convenciones de llamadas y escribí algunos métodos para convertir entre varios. Aquí hay uno para 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; 
    } 
} 

}

Cuestiones relacionadas