Calling Delphi DLL from C++ using GetProcAddress:

2019-05-05 07:39发布

问题:

I have a 3rd party Delphi DLL which I am calling from C++. Unfortunately, I have no access to the Pascal DLL code, and am not a Pascal programmer.

There is no lib file, so I'm using GetProcAddress to call many DLL functions, successfully passing parameters by value, address and reference. I also register a callback function which gets called when expected.

My problem is that in the callback function, one of the two parameters cannot be evaluated (address 0x000001).

Here are the Pascal DLL function declarations

type
HANDLE = Pointer;       /// handle

(** This function Registers the callback function OnACLNeeded 
  *)
function RegisterCallback(
    h: HANDLE; 
    OnACLNeeded: MyCallbackFunc; 
    UserData: DWORD): Integer; stdcall;

This is the pascal version of the calling application, the callback function. Both parameters are passed by reference (var).

function TSNAPICongigF.OnACLNeeded(var keySettings, numOfKeys: Byte): Integer; stdcall;
begin
  keySettings:=$0F;
  numOfKeys:=1;
  Result:=0;
end;

This is my C++ version of the callback function

int __stdcall OnACLNeeded(byte& keySettings, byte& numOfKeys)
{
  keySettings = 0x0F;
  numOfKeys = 1;
  return 1;
}

This is my C++ calling code

int _tmain()
{
    HMODULE hLib = LoadLibrary(PASCAL_DLL);

    // DLL function pointer 
    typedef int (__stdcall *FnRegisterCallback)(HANDLE hKeyProvider,
        int (__stdcall *)(byte&, byte&),
        DWORD);

    FnRegisterCallback pfnRegisterCallback =
        (FnRegisterCallback)GetProcAddress(hLib, "RegisterCallback");

    // register my callback function
    int ret = (*pfnRegisteraCllback)(h, OnACLNeeded, (DWORD)1);
}

When running in the debugger, I reach the breakpoint on the first line of the callback function keySettings = 0x0F;
I find that numOfKeys is valid, but keySettings has an address of 0x00000001 and cannot be assigned to.
The application will crash with an AccessViolation if I continue.

int __stdcall OnACLNeeded(byte& keySettings, byte& numOfKeys)
{
    keySettings = 0x0F;

I've tried declaring as __cdecl to no effect.
I've tried declaring the byte params as byte*, and I get the same invalid parameter.

I'm using Visual Studio 2010 running on Win7 64-bit, but compiling as Win32.
These are my compile and link commands

/ZI /nologo /W3 /WX- /Od /Oy- /D "WIN32" /D "_DEBUG" /D "_CONSOLE" /D "_UNICODE" /D "UNICODE" /Gm /EHsc /RTC1 /GS /fp:precise /Zc:wchar_t /Zc:forScope /Fp"Debug\CallPascal.pch" /Fa"Debug\" /Fo"Debug\" /Fd"Debug\vc100.pdb" /Gd /analyze- /errorReport:queue 

/OUT:"...\Debug\CallPascal.exe" /INCREMENTAL /NOLOGO "kernel32.lib" "user32.lib" "gdi32.lib" "winspool.lib" "comdlg32.lib" "advapi32.lib" "shell32.lib" "ole32.lib" "oleaut32.lib" "uuid.lib" "odbc32.lib" "odbccp32.lib" /MANIFEST /ManifestFile:"Debug\CallPascal.exe.intermediate.manifest" /ALLOWISOLATION /MANIFESTUAC:"level='asInvoker' uiAccess='false'" /DEBUG /PDB:"...\Debug\CallPascal.pdb" /SUBSYSTEM:CONSOLE /PGD:"...y\Debug\CallPascal.pgd" /TLBID:1 /DYNAMICBASE /NXCOMPAT /MACHINE:X86 /ERRORREPORT:QUEUE

Any suggestions very gratefully received. Thank you.

------ edit -----

I added the struct

struct Bodge {
  void* code;
  void* instance;
};

Bodge bodge;

bodge.code = OnACLNeeded;
bodge.instance = (void*)0x99; // just to test

My callback becomes

Integer __stdcall OnACLNeeded(void* instance, Byte& keySettings, Byte& numOfKeys);

And my function call to register the callback becomes

typedef Integer (__stdcall *FnRegisterCallback)(
    HANDLE,
    Bodge,
    DWORD);

and is called like this

ret = (*pfnRegisterCallback)(
    h, 
    bodge, 
    (DWORD)1);

This call produces an error

The value of ESP was not properly saved across a function call. This is usually a result of calling a function declared with one calling convention with a function pointer declared with a different calling convention.

which may also indicate a corrupt stack, I think.
BUT if I ignore the error and continue, I get into the callback function body, and BOTH parameters are now valid!
So success of a kind, but also, the void* instance param has a value of zero, not the 0x99 that I set.
I feel we are getting there though!

----- edit -----

This is the function call to register the callback, from the original Pascal calling code.

  * @param hKeyProvider the key provider handle for Desfire card created previously with LASSeOKeyProvider_CreateHandle
  * @param OnACLNeeded supplies a callback to be called for quering host application for the PICC master key settings
  * @param UserData unsigned integer values specifiing any custom provided data to be returned when the callback is called
  * @return 0 - on success; <>0 - denotes error code

  RegisterCallback(hKeyProv,
                  @TSNAPICongigF.OnACLNeeded,
                  (Self));

Note the "self" reference. What would be the C++ equivalent? (I'm not using classes)

回答1:

The problem is that the Delphi version of the callback is an instance method. Your C++ callback it not. This is a significant mismatch. This Delphi interface is badly designed and not callable from C++ without some trickery.

A Delphi instance method is passed as two pointers, one to the code and one to the instance. You can fake this in C++ by declaring the RegisterCallback function like this:

typedef int (__stdcall *FnRegisterCallback)(
    HANDLE hKeyProvider,
    void* code,
    void* instance,
    DWORD
);

Then, once you have loaded it with GetProcAddress, call it like this:

int ret = (*pfnRegisterCallback)(h, OnACLNeeded, NULL, (DWORD)1);

It doesn't matter what the instance parameter is since we are going to ignore that when it is passed on to OnACLNeeded.

The final step is to arrange that your function behaves like a Delphi instance method. Do that by adding an extra parameter to represent the instance.

int __stdcall OnACLNeeded(void* instance, byte& keySettings, byte& numOfKeys)
{
    keySettings = 0x0F;
    numOfKeys = 1;
    return 1;
}

You will receive in the instance parameter, whatever you passed as the instance parameter when you called RegisterCallback.

With these changes you should be able to fake out the Delphi DLL into believing that your code is a Delphi instance method!

For reference I suggest your read the Program Control topic of the Delphi Language Guide.

Finally, good luck!


Update

The latest edit to the question sheds some new light on what is happening. Specifically this code:

RegisterCallback(
    hKeyProv,
    @TSNAPICongigF.OnACLNeeded,
    DWORD(Self)
);

The @TSNAPICongigF.OnACLNeeded parameter results in just the code part of the method. That is it is just a single pointer. The instance is passed in the following parameter with DWORD(Self). The assumption that the author of this code has made is that the callback function will be passed three parameters, the user data followed by the two by var bytes. The trick is that such a function is equivalent to a method call because a method call is implemented behind the scenes by passing the instance as a hidden, implicit, parameter before the actual parameters.

So, I believe you can solve the problem quite easily. Simply roll all the way back to where your code was when you asked the question. Then change your callback function to accept this extra parameter:

int __stdcall OnACLNeeded(DWORD UserData, byte& keySettings, byte& numOfKeys)
{
    keySettings = 0x0F;
    numOfKeys = 1;
    return 1;
}

I am now confident that this will work.

You call the register function like this:

int ret = (*pfnRegisteraCllback)(h, OnACLNeeded, (DWORD)1);

and your callback function should see the value of 1 in the UserData parameter.