Вызов DLL Delphi из C # приводит к неожиданным результатам

У меня есть 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. Мысли?


person Doug Hays    schedule 05.02.2009    source источник


Ответы (7)


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

person newgre    schedule 05.02.2009
comment
Обратите внимание, что fastcall означает разные вещи в разных контекстах. Версия Microsoft отличается от версии Embarcadero, и я подозреваю, что версия GCC отличается от них обоих. В Delphi соглашение о вызовах даже не называется fastcall; это соглашение о вызове регистров. - person Rob Kennedy; 18.10.2009

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

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

person Barry Kelly    schedule 05.02.2009

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

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

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

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

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

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

person Community    schedule 06.02.2009

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

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

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

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

person PetriW    schedule 05.02.2009
comment
Паскаль в качестве типа вызова не будет работать, потому что Delphi использует вызов регистра (также известный как __fastcall, который не является __msfastcall). - person Andreas Hausladen; 06.02.2009
comment
Согласно msdn.microsoft.com/en-us/ library / никакой вид FastCall не поддерживается .net. Значит, он должен попросить их изменить паскаль-код? - person PetriW; 06.02.2009

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

person Ben Ark    schedule 06.02.2009

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

}

person AbdElRaheim    schedule 26.01.2010

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

person frogb    schedule 06.02.2009