Recent

Author Topic: Calling convention is lost when specializing generic function pointer  (Read 1129 times)

ase

  • New member
  • *
  • Posts: 8
I have several libraries written in lazarus which represent different types of devices. The basic design of the libraries' API is the same. They more or less differ in an enumeration type that is used in conjunction with a get/set parameter style interface.

In the internal implementation there is a generic base class that provides the implementation of get/set parameter stuff.
Those libraries support callback functions which need to be binary compatible of course. For those callbacks a function definition exists and it is defined as stdcall.

Now I stumbled upon the fact that the calling convention in generic function pointer definitions seems to be lost when specializing to a real function type. In this example I only use integers for clarity (and it avoids crashing when the wrong calling convention is used):
Code: Pascal  [Select][+][-]
  1. type
  2.   generic TCallProcStdCall<T> = procedure(aArg1:T;aArg2:Integer;aArg3:Integer) of object;stdcall;
  3.  
  4.   TGenericCallProcIntegerStdCall = specialize TCallProcStdCall<Integer>;

When I declare a function pointer variable and try to assign the implementation to it, I get an "incompatible types" error:
Code: Pascal  [Select][+][-]
  1. var
  2.   obj       : TIntTest;
  3.   stdCallPtr: TGenericCallProcIntegerStdCall;
  4. begin
  5.   obj := TIntTest.Create;
  6.   try
  7.     //project1.lpr(51,23) Error:
  8.     //Incompatible types:
  9.     //got      "<procedure variable type of procedure(LongInt;LongInt;LongInt) of object;[b]StdCall[/b]>"
  10.     //expected "<procedure variable type of procedure(LongInt;LongInt;LongInt) of object;[b]Register[/b]>"
  11.  
  12.     //stdCallPtr := @obj.StdCalling;
  13.  
  14.     stdCallPtr := TGenericCallProcIntegerStdCall(@obj.StdCalling);

There is a workaround that includes the definition of the real - non generic - type and casting to it so that the function call is made in the desired calling convention. That's acutally what I have done in the different libraries (to be more precise, I declared a default type that has the correct size of all the generic enumerations):
Code: Pascal  [Select][+][-]
  1. type
  2.   TCallProcIntegerStdCall = procedure(aArg1:Integer;aArg2:Integer;aArg3:Integer) of object;stdcall;
  3. begin
  4.     //call is made with correct calling convention
  5.     TCallProcIntegerStdCall(stdCallPtr)(1,2,3);

Is this a bug? I found nothing about the calling convention in conjunction with generics.

The whole program to demonstrate the situation:
Code: Pascal  [Select][+][-]
  1. program project1;
  2.  
  3. uses
  4.   TypInfo, SysUtils;
  5.  
  6. type
  7.   generic TCallProcStdCall<T> = procedure(aArg1:T;aArg2:Integer;aArg3:Integer) of object;stdcall;
  8.   generic TCallProcReg<T>     = procedure(aArg1:T;aArg2:Integer;aArg3:Integer) of object;
  9.  
  10.   TGenericCallProcIntegerStdCall = specialize TCallProcStdCall<Integer>;
  11.   TGenericCallProcIntegerReg     = specialize TCallProcReg<Integer>;
  12.  
  13.   TCallProcIntegerStdCall = procedure(aArg1:Integer;aArg2:Integer;aArg3:Integer) of object;stdcall;
  14.  
  15.   { TTest }
  16.   generic TGenericTest<T> = class
  17.   public
  18.     procedure StdCalling(aArg1:T;aArg2:Integer;aArg3:Integer);stdcall;
  19.     procedure RegCalling(aArg1:T;aArg2:Integer;aArg3:Integer);
  20.   end;
  21.  
  22.   TIntTest = specialize TGenericTest<Integer>;
  23.  
  24. { TTest }
  25.  
  26. procedure TGenericTest.StdCalling(aArg1:T;aArg2:Integer;aArg3:Integer); stdcall;
  27. begin
  28.   WriteLn('Self=0x'+IntToHex(IntPtr(self),SizeOf(self)*2)+
  29.           ' Arg1='+IntToStr(PtrInt(aArg1))+
  30.           ' Arg2='+IntToStr(aArg2)+
  31.           ' Arg3='+IntToStr(aArg3));
  32. end;
  33.  
  34. procedure TGenericTest.RegCalling(aArg1:T;aArg2:Integer;aArg3:Integer);
  35. begin
  36.   WriteLn('Self=0x'+IntToHex(IntPtr(self),SizeOf(self)*2)+
  37.           ' Arg1='+IntToStr(PtrInt(aArg1))+
  38.           ' Arg2='+IntToStr(aArg2)+
  39.           ' Arg3='+IntToStr(aArg3));
  40. end;
  41.  
  42. var
  43.   obj       : TIntTest;
  44.   stdCallPtr: TGenericCallProcIntegerStdCall;
  45.   regCallPtr: TGenericCallProcIntegerReg;
  46. begin
  47.   obj := TIntTest.Create;
  48.   try
  49.     //project1.lpr(51,23) Error:
  50.     //Incompatible types:
  51.     //got      "<procedure variable type of procedure(LongInt;LongInt;LongInt) of object;StdCall>"
  52.     //expected "<procedure variable type of procedure(LongInt;LongInt;LongInt) of object;Register>"
  53.  
  54.     //stdCallPtr := @obj.StdCalling;
  55.  
  56.     stdCallPtr := TGenericCallProcIntegerStdCall(@obj.StdCalling);
  57.     regCallPtr := @obj.RegCalling;
  58.  
  59.     obj.StdCalling(1,2,3);
  60.     obj.RegCalling(1,2,3);
  61.  
  62.     //call is made with wrong calling convention
  63.     stdCallPtr(1,2,3);
  64.     regCallPtr(1,2,3);
  65.  
  66.     //call is made with correct calling convention
  67.     TCallProcIntegerStdCall(stdCallPtr)(1,2,3);
  68.  
  69.     readln;
  70.   finally
  71.     obj.Free;
  72.   end;
  73. end.
It will print something like this:
Quote
Self=0x01805A98 Arg1=1 Arg2=2 Arg3=3
Self=0x01805A98 Arg1=1 Arg2=2 Arg3=3
Self=0x00000003 Arg1=21167964 Arg2=4252960 Arg3=21167952
Self=0x01805A98 Arg1=1 Arg2=2 Arg3=3
Self=0x01805A98 Arg1=1 Arg2=2 Arg3=3

dje

  • Full Member
  • ***
  • Posts: 134
Re: Calling convention is lost when specializing generic function pointer
« Reply #1 on: August 18, 2022, 08:38:03 am »
I spent some time looking into this one. I'm using FPC 3.2.0, which is a little old, so maybe someone else can test. I first thought generic methods ignore the calling convention, but after checking the RTTI data, I found the calling conventions for generic methods are stored correctly. I think I found the crux of the issue.

First here is a simplified version of the test class:
Code: Pascal  [Select][+][-]
  1. type
  2.   generic TGenericTest<T> = class
  3.     procedure StdCalling(A: T); stdcall;
  4.     procedure RegCalling(A: T); cdecl;
  5.   end;
  6.  
  7.   procedure TGenericTest.StdCalling(A: T); stdcall;
  8.   begin
  9.   end;
  10.   procedure TGenericTest.RegCalling(A: T); cdecl;
  11.   begin
  12.   end;  

This code compiles as expected:
Code: Pascal  [Select][+][-]
  1. procedure ThisWorks;
  2. var
  3.   A: procedure(A: integer) of object; stdcall;
  4.   B: procedure(A: integer) of object; cdecl;
  5. begin
  6.   with specialize TGenericTest<integer>.Create do begin
  7.     try
  8.       A := @StdCalling;
  9.       B := @RegCalling;
  10.     finally
  11.       Free;
  12.     end;
  13.   end;
  14. end;

But, if you also want to define the vars A and B using generic "procedure of object"'s, the compiler fails. eg:
Code: Pascal  [Select][+][-]
  1. type
  2.   generic TCallProcStdCall<T> = procedure(A: T) of object; stdcall;
  3.   generic TCallProcCDecl<T> = procedure(A: T) of object; cdecl;
  4.  
  5. procedure ThisDoesNotWork;
  6. var
  7.   A: specialize TCallProcStdCall<integer>;
  8.   B: specialize TCallProcCDecl<integer>;
  9. begin
  10.   with specialize TGenericTest<integer>.Create do begin
  11.     try
  12.       A := @StdCalling;
  13.       B := @RegCalling; // <<< This fails to compile
  14.       // Error: Incompatible types:
  15.       //  got "<procedure variable type of procedure(LongInt) of object;CDecl>"
  16.       //  expected "<procedure variable type of procedure(LongInt) of object;StdCall>"
  17.     finally
  18.       Free;
  19.     end;
  20.   end;
  21. end;

As far as I can tell, there should be no difference between the two.

To simplify the problem even more, the following code uses basic functions. Assignment to procedure pointers with defined calling definitions, works.
Code: Pascal  [Select][+][-]
  1.   procedure Test1(A: integer); stdcall;
  2.   begin
  3.   end;
  4.   procedure Test2(A: integer); cdecl;
  5.   begin
  6.   end;
  7.   procedure ThisWorks;
  8.   var
  9.     A: procedure(A: integer); stdcall;
  10.     B: procedure(A: integer); cdecl;
  11.   begin
  12.     A := @Test1;
  13.     B := @Test2;
  14.   end;
  15.  

While, pointers to procedures created via a specialized generic definition, does not:

Code: Pascal  [Select][+][-]
  1. type
  2.   generic TProcedureStdCall<T> = procedure(A: T); stdcall;
  3.   generic TProcedureCDecl<T> = procedure(A: T); cdecl;
  4.  
  5.   procedure ThisDoesNotWork;
  6.   var
  7.     A: specialize TProcedureStdCall<integer>;
  8.     B: specialize TProcedureCDecl<integer>;
  9.   begin
  10.     A := @Test1;
  11.     B := @Test2; // << This fails to compile
  12.     // Error: Incompatible types:
  13.     //  got "<address of procedure(LongInt);CDecl>"
  14.     //  expected "<procedure variable type of procedure(LongInt);StdCall>"
  15.   end;  

This might be a bug which has been fixed. Maybe someone can check.
« Last Edit: August 18, 2022, 08:42:06 am by derek.john.evans »

dje

  • Full Member
  • ***
  • Posts: 134
Re: Calling convention is lost when specializing generic function pointer
« Reply #2 on: August 18, 2022, 08:56:25 am »
A proposed workaround. The following works.

Code: Pascal  [Select][+][-]
  1. type
  2.   generic TGenericTest<T> = class
  3.   type
  4.     TStdCall = procedure(A: T) of object; stdcall;
  5.     TCDecl = procedure(A: T) of object; cdecl;
  6.   public
  7.     procedure StdCalling(A: T); stdcall;
  8.     procedure RegCalling(A: T); cdecl;
  9.   end;
  10.  
  11.   procedure TGenericTest.StdCalling(A: T); stdcall;
  12.   begin end;
  13.  
  14.   procedure TGenericTest.RegCalling(A: T); cdecl;
  15.   begin end;
  16.  
  17.   procedure ThisWorks;
  18.   var
  19.     A: specialize TGenericTest<integer>.TStdCall;
  20.     B: specialize TGenericTest<integer>.TCDecl;
  21.   begin
  22.     with specialize TGenericTest<integer>.Create do begin
  23.       try
  24.         A := @StdCalling;
  25.         B := @RegCalling;
  26.       finally
  27.         Free;
  28.       end;
  29.     end;
  30.   end;

PascalDragon

  • Hero Member
  • *****
  • Posts: 5446
  • Compiler Developer
Re: Calling convention is lost when specializing generic function pointer
« Reply #3 on: August 18, 2022, 09:27:40 am »
Please report a bug with a self contained example.

ase

  • New member
  • *
  • Posts: 8
Re: Calling convention is lost when specializing generic function pointer
« Reply #4 on: August 18, 2022, 12:58:50 pm »
I filed a bug report and attached a program that shows the compiler error and includes the workaround that derek.john.evans has suggested.
Thank you derek.john.evans for the input.

https://gitlab.com/freepascal.org/fpc/source/-/issues/39869

ase

  • New member
  • *
  • Posts: 8
Re: Calling convention is lost when specializing generic function pointer
« Reply #5 on: August 22, 2022, 10:24:01 am »
Please report a bug with a self contained example.
Just compiled the latest fpc trunk version - it works now. Thank you for the good work!

It might be off topic, but I must say Free Pascal is so much better than Delphi when it comes to generics. E.g. we implemented a class of generic linked lists and are using pointers to the specialized record types.
The pointers are specialized within the generic class. Delphi is unable to resolve those types - which renders the whole construct useless there.

PascalDragon

  • Hero Member
  • *****
  • Posts: 5446
  • Compiler Developer
Re: Calling convention is lost when specializing generic function pointer
« Reply #6 on: August 22, 2022, 01:48:01 pm »
Please report a bug with a self contained example.
Just compiled the latest fpc trunk version - it works now. Thank you for the good work!

Good. Let me see whether I'll merge this to 3.2.3 as well.

It might be off topic, but I must say Free Pascal is so much better than Delphi when it comes to generics. E.g. we implemented a class of generic linked lists and are using pointers to the specialized record types.
The pointers are specialized within the generic class. Delphi is unable to resolve those types - which renders the whole construct useless there.

It might either be a bug in Delphi or due to the differences in how FPC and Delphi approach the concept of generics (there are some things you simply can't do in Delphi that you can in FPC, just as there are a few things that work in Delphi, but not (yet) in FPC).

 

TinyPortal © 2005-2018