Jump to content
msohn

Calling C DLL with an 8-byte struct return value crashes on Win32 works on Win64

Recommended Posts

I'm trying to write a Delphi binding for tree-sitter over on GitHub and am struggling with calling this C API:

typedef struct TSNode {
  //content doesn't really matter here
} TSNode;

typedef struct TSPoint {
  uint32_t row;
  uint32_t column;
} TSPoint;

TSPoint ts_node_end_point(TSNode self);

On Win64 SizeOf(TSNode) = SizeOf(Pointer) this works just fine. On Win32 it crashes. 

 

According to the Language Guide 64bit Integer function results are returned in EDX:EAX. So I have tried declaring the method as returning Int64 and hard-casting that to my TSPoint record:

{$IFDEF WIN32}
function ts_node_end_point(self: TSNode): Int64; cdecl; external ModuleName;
{$ELSE}
function ts_node_end_point(self: TSNode): TSPoint; cdecl; external ModuleName;
{$ENDIF}

function TTSNodeHelper.EndPoint: TTSPoint;
begin
{$IFDEF WIN32}
  Result:= TTSPoint(ts_node_end_point(Self));
{$ELSE}
  Result:= ts_node_end_point(Self);
{$ENDIF}
end;

This solves the crash, but it seems rather a coincident that it's working partially (row seems to be right, column is off and the similar ts_node_start_point API is completely off).

 

Is there even a way to call this API reliably on Win32?

Share this post


Link to post

See:

https://stackoverflow.com/questions/45241488/how-does-the-cdecl-calling-convention-returns-a-struct

https://stackoverflow.com/questions/16119116/passing-record-as-a-function-result-from-delphi-dll-to-c

 

The C code is returning the struct wholly in the EDX:EAX register, but Delphi is expecting it to be passed via a hidden reference parameter instead, according to the same Language Guide you linked to:

Quote

For static-array, record, and set results, if the value occupies one byte it is returned in AL; if the value occupies two bytes it is returned in AX; and if the value occupies four bytes it is returned in EAX. Otherwise, the result is returned in an additional var parameter that is passed to the function after the declared parameters.

So, your Int64 hack is likely the only way to go for 32bit, unless you have access to change the C code to match Delphi's use of an output parameter.  Also, make sure your TPoint record in Delphi has the right size and alignment to match the C type.  You did not show your declaration of the record, but if it is not declared with 'packed' or {$ALIGN} then it may have extra padding in it, making it larger than the C type, which would explain why you are having trouble accessing the column field.

Share this post


Link to post
17 hours ago, Remy Lebeau said:

So, your Int64 hack is likely the only way to go for 32bit

Thanks for the links and confirming that my thinking wasn't totally off.

 

TSPoint only contains two UInt32 so AFAICS it is always 8 bytes in size, regardless of packed or $A settings:

 

  TSPoint = record
    row: UInt32;
    column: UInt32;
  end;
 

(sorry for the missing formatting, it just wouldn't insert anything using the insert code popup; worked fine for the initial post)

 

Also, the column isn't totally random, it's just that 1 or 2 bits are set in addition. And to make things worse, the identical API ts_node_start_point does not return anything in EDX:EAX, i.e. when stepping over the call, both registers remain unchanged.

 

The 2nd stackoverflow link has an answer from David Heffernan stating that Delphi is the compiler being wrong here in that it does not follow the ABI. I'm afraid that makes it not very likely to convince other developers to change their API.

 

FWIW all the necessary code is on GitHub (https://github.com/modersohn/delphi-tree-sitter), for the binding I started and for the DLL as well (linked from my repo). 

Share this post


Link to post
Posted (edited)
7 hours ago, msohn said:

TSPoint only contains two UInt32 so AFAICS it is always 8 bytes in size, regardless of packed or $A settings:

If the record is 8-byte aligned, then it would have padding between the 2 integers. That is why I suggested you verify the record's actual size and alignment on the Delphi side.  If you need to ensure the record is 8 bytes, then applying 'packed' or ($ALIGN 4} to the record is the way to go.

7 hours ago, msohn said:

And to make things worse, the identical API ts_node_start_point does not return anything in EDX:EAX, i.e. when stepping over the call, both registers remain unchanged.

You are missing the 'cdecl' calling convention on your declaration of ts_node_start_point() under 32bit, so the Delphi compiler uses the 'register' calling convention instead.

Edited by Remy Lebeau

Share this post


Link to post
1 hour ago, Remy Lebeau said:

If the record is 8-byte aligned, then it would have padding between the 2 integers. That is why I suggested you verify the record's actual size and alignment on the Delphi side.  If you need to ensure the record is 8 bytes, then applying 'packed' or ($ALIGN 4} to the record is the way to go.

You are missing the 'cdecl' calling convention on your declaration of ts_node_start_point() under 32bit, so the Delphi compiler uses the 'register' calling convention instead.

Regarding the alignment: I initially thought the same thing, but even with an explicit {$ALIGN 8} the record size is still 8.

 

About the missing cdecl: man, I can't believe I made such a silly mistake! Thanks for having a look and spotting this - that does indeed fix it and now the workaround via Int64 is working - wonderful!

 

It's not the first time and I'm sure it won't be the last: thanks for your help, I very much appreciate it!

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×