Вызов Delphi DLL из C # приводит к неожиданным результатам - PullRequest
8 голосов
/ 06 февраля 2009

У меня есть 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. Каждый тест с использованием .EXE возвращает одно и то же 10-значное число (ожидаемый результат).

Итак, я должен верить, что я неправильно объявил свой вызов DLL. Мысли?

Ответы [ 7 ]

22 голосов
/ 06 февраля 2009

Delphi по умолчанию использует так называемое fastcall соглашение о вызовах. Это означает, что компилятор пытается передать параметры функции в регистрах ЦП и использует стек только в том случае, если имеется больше параметров, чем свободных регистров. Например, Delphi использует (EAX, EDX, ECX) для первых трех параметров функции.
В вашем коде C # вы фактически используете stdcall соглашение о вызовах, которое инструктирует компилятор передавать параметры через стек (в обратном порядке, то есть последний параметр передается первым) и пусть вызываемый очищает стек.
Напротив, вызов cdecl , используемый компиляторами C / C ++, заставляет вызывающую программу очищать стек.
Просто убедитесь, что вы используете одно и то же соглашение о вызовах с обеих сторон. Stdcall в основном используется, потому что он может использоваться почти везде и поддерживается каждым компилятором (Win32 API также используют это соглашение).
Обратите внимание, что fastcall в любом случае не поддерживается .NET.

16 голосов
/ 06 февраля 2009

Джин прав. Прототип функции, как дано, не может быть легко вызван напрямую из C #, если он есть в соглашении о вызовах Delphi register. Вам нужно либо написать для нее stdcall функцию-обертку - возможно, в другой DLL, если у вас нет исходного кода - либо вам нужно, чтобы люди, которые поддерживают функцию, изменили соглашение о вызовах на stdcall.

Обновление: Я также вижу, что первым аргументом является строка Delphi. Это не то, что может поставить C #. Это должен быть PChar вместо этого. Кроме того, важно четко понимать, является ли функция Ansi или Unicode; если библиотека DLL написана на Delphi 2009 (или более поздней версии), то это Unicode, в противном случае это Ansi.

2 голосов
/ 06 февраля 2009

Возвращаемое значение может быть другой проблемой. Вероятно, это либо утечка памяти (они выделяют буфер в куче и никогда не освобождают его), либо доступ к уже свободной памяти (они возвращают локальную строковую переменную, приведенную к PChar).

Возвращение строк (или данных переменного размера в целом) из функции в другой модуль в целом проблематично.

Одним из решений (используется winapi) является требование, чтобы вызывающая сторона передавала буфер и его размер. Недостатком этого является то, что если буфер слишком мал, функция завершается ошибкой, и вызывающая сторона должна вызывать ее снова с большим буфером.

Другое возможное решение - выделить буфер из кучи в функции и вернуть его. Затем вам нужно экспортировать другую функцию, которую вызывающая сторона должна снова использовать для освобождения выделенной памяти. Это гарантирует, что память освобождается той же самой средой выполнения, которая ее выделила.

Передача (Delphi) строкового параметра между разными (не Borland) языками, вероятно, невозможна. И даже между модулями Delphi вы должны убедиться, что оба модуля используют один и тот же экземпляр диспетчера памяти. Обычно это означает добавление «использует ShareMem» в качестве первого использования для всех модулей. Другим отличием является соглашение о вызовах «register», которое является соглашением fastcall, но не совпадает с использованием компиляторов fastcall MS.

Совершенно другим решением может быть перекомпиляция DLL Delphi с одним из компиляторов Delphi.net. Сколько работы это зависит от их кода.

1 голос
/ 26 января 2010

Я на днях бездельничал, пытаясь узнать о соглашениях о вызовах, и я написал несколько методов для преобразования между различными. Вот один для 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;
    }
}

}

1 голос
/ 06 февраля 2009

Создайте оболочку COM в Delphi и вызовите ее в своем коде C # через взаимодействие. Вуаля .. проста в использовании из C # или любой другой будущей платформы.

1 голос
/ 06 февраля 2009

Я никогда не делал этого, но попробуйте изменить ваш код на:

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

Обратите внимание на дополнительный стандартный вызов.

Edit2: Как вы можете видеть из других ответов, вы должны либо внести изменения выше или написать dll-оболочку, которая делает то же самое.

0 голосов
/ 06 февраля 2009

Когда вы просите их изменить соглашение о вызовах, вам также следует попросить их изменить первый параметр, чтобы он не был «строкой». Заставьте их использовать указатель на массив с нулевым символом в конце или массив char. Использование строк Delphi в качестве параметров DLL является плохой идеей даже без дополнительной сложности, связанной с попыткой обеспечить межязыковую совместимость. Кроме того, строковая переменная будет содержать содержимое ASCII или Unicode в зависимости от используемой версии Delphi.

...