I need to negotiate a function call, from my Delphi app, into provided DLL made in Clarion 6.3.
I need to pass one or two string parameters (either one functon wit htwo params or two single-params functions).
We quickly settled on using 1-byte 0-ended strings (char*
in C terms, CSTRING
in Clarion terms, PAnsiChar
in Delphi terms), and that is where things got a bit unpredictable and hard too understand.
The working solution we got was passing untyped pointers disguised as 32-bit integers, which Clarion-made DLL then uses to traverse memory with something Clarion programmer called "pick" or maybe "peek". There are also forum articles on interop between Clarion and Visual Basic which address passing strings from VB into Clarion and glancing upon which from behind my shoulder the Clarion developer said something like "i don't need copy of it, i already know it, it is typical".
This however puts more burden on us long-term, as low-level untyped code is much "richer" on boilerplate and error-prone. Typed code would feel better solution.
What i seek here is less of "That is the pattern to copy-paste and make things work without thinking" - we already have it - and more of understanding, what is going on behind the hood, and how can i rely on it, and what should i expect from Clarion DLLs. To avoid getting eventually stuck in "works by chance" solution.
As i was glancing into Clarion 6.3 help from behind his shoulder, the help was not helful on low-level details. It was all about calling DLLs from Clarion, but not about being called. I also don't have Clarion on my machine, not do i want to, ahem, borrow it. And as i've been told sources of Clarion 6.3 runtime are not available to developers either.
Articles like interop between Clarion and VB or between Clarion and C# are not helpful, because they fuse idiosyncrasies of both languages and give yet less information about "bare metal" level.
Google Books pointed to "Clarion Tips & Techniques - David Harms" - and it seem to have interesting insights for Clarion seasoned ones, but i am Clarion zero. At least i was not able to figure out low-level interop-enabling details from it.
Is there maybe a way to make Clarion 6.3 save 'listing files' for the DLLs it make, a standard *.H header file maybe?
So, to repeat, what works, as expected was a function that was passing pointers on Delphi side ( procedure ...(const param1, param2: PAnsiChar); stdcall;
which should translate to C stdcall void ...(char* p1, char* p2)
and which allegedly look in Clarion something like (LONG, LONG), LONG, pascal, RAW
.
This function takes two 32-bit parameters from stack in reverse order, uses them, and exits, passing return value (actually, unused garbage) in EAX register and clearing parameters from stack. Almost exactly stdcall
, except that it seems to preserve EBX register for some obscure reason.
Clarion function entry:
04E5D38C 83EC04 sub esp,$04 ' allocate local vars
04E5D38F 53 push ebx ' ????????
04E5D390 8B44240C mov eax,[esp+$0c]
04E5D394 BBB4DDEB04 mov ebx,$04ebddb4
04E5D399 B907010000 mov ecx,$00000107
04E5D39E E889A1FBFF call $04e1752c ' clear off local vars before use
And its exit
00B8D500 8B442406 mov eax,[esp+$06] ' pick return value
00B8D504 5B pop ebx ' ????
00B8D505 83C41C add esp,$1c ' remove local vars
00B8D508 C20800 ret $0008 ' remove two 32-bits params from stack
Except for unexplainable for me manipulation with EBX and returning garbage result - it works as expected. But - untyped low-level operations in Clarion sources required.
Now the function that allegedly only takes one string parameter: on Delphi side - procedure ...(const param1: PAnsiChar); stdcall;
which should translate to C stdcall void ...(char* p1)
and which allegedly look in Clarion something like (*CSTRING), LONG, pascal, RAW
.
Clarion function entry:
00B8D47C 83EC1C sub esp,$1c ' allocate local vars
00B8D47F 53 push ebx ' ????????
00B8D480 8D44240A lea eax,[esp+$0a]
00B8D484 BB16000000 mov ebx,$00000016
00B8D489 B990FEBD00 mov ecx,$00bdfe90
00B8D48E BA15000000 mov edx,$00000015
00B8D493 E82002FBFF call $00b3d6b8 ' clear off local vars before use
And its exit
04E5D492 8B442404 mov eax,[esp+$04] ' pick return value
04E5D496 5B pop ebx ' ????
04E5D497 83C404 add esp,$04 ' remove local vars
04E5D49A C20800 ret $0008 ' remove TWO 32-bits params from stack
What strucks here is that somehow TWO parameters are expected by the function, and only the second one is used (i did not see any reference to the first parameter in the x86 asm code). The function seems to work fine, if being called as procedure ...(const garbage: integer; const param1: PAnsiChar); stdcall;
which should translate to C stdcall void ...(int garbage, char* p1)
.
This "invisible" parameter would look much like a Self/This pointer in object-oriented languages method functions, but the Clarion programmer told me with certainty there was no any objects involved. More so, his 'double-int' function does not seem expect invisible parameter either.
The aforementioned 'Tips' book describes &CSTRING
and &STRING
Clarion types as actually being two parameters behind the hood, pointer to the buffer and the buffer length. It however gives no information upon how specifically they are passed on stack though. But i was said Clation refused to make a DLL with exported &CSTRING
-parametrized function.
I could suppose the invisible parameter is where Clarion wants to store function's return value (if there would had been any assignment to it in Clarion sources), crossing stdcall
/PASCAL
convention, but the assembler epilogue code shows clear use of EAX register for that, and again the 'double-LONG' function does not use it.
And, so, while i made the "works on my machine" quality code, that successfully calls that Clarion function, by voluntarily inserted a garbage-parameter - i feel rather fuzzy, because i can not understand what and why Clarion is doing there, and hence, what it can suddenly start doing in future after any seemingly unrelated changes.
What is that invisible parameter? Why can it happen there? What to expect from it?
If you are consuming a DLL from Clarion you can prototype with RAW - but procedures in a Clarion DLL cannot do this. So in the Clarion DLL they can prototype as
Whatever Procedure(*Cstring parm1, *Cstring parm2),C,name('whatever')
And, as you note, from your side you should see this as 4 parameters, length, pointer, length, pointer. (knowing explicit max lengths is not a bad thing from a safety point of view anyway.)
the alternative is
Whatever Procedure(Long parm1, Long parm2),C,name('whatever')
Then from your side it's just 2 addresses. But there's a bit more code on his side turning those incoming addresses into memory pointers. (yes, he can use PEEK and POKE but that's a bit of overkill) (From memory he could just declare local variables as
parm1String &cstring,over(parm1)
parm2String &cstring,over(parm2)
but it's been decades since I did this, so I'm not 100% that syntax is legal.)