This blog post is about an annoying bug I have found in Delphi, and the consequences of it.
The Invokable variants bug
During preparation for an Android 64 bit update of one of the apps I have made and that currently is released as Android 32 bit, which is no longer supported by the latest Samsung phones (amongst others), I found that the application, which otherwise works fine in Win32, Win64, Android 32 and IOS 64 bit mode, experienced a weird problem when compiled for Android 64 bit mode.
I have latest Delphi 10.4.2 installed with up to date patches.
The problem cause the kbmMW SmartClient syntax not to be usable on Android 64 bit, which is a bummer. However it is fortunately easy to circumvent, since the SmartClient syntax is just a syntactical sugar alternative to the old style SmartClient Request method call, so it is not a showstopper for me, but it does cause me to rewrite a few lines making calls to the kbmMW application server.
I have already reported this problem to Embarcadero as RSP-34063
To illustrate the problem I made a simple FMX application with a basic form and some code.
The form looks like this:
unit Unit6; interface uses System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants, FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs, FMX.Controls.Presentation, FMX.StdCtrls, FMX.Memo.Types, FMX.ScrollBox, FMX.Memo; type TTestVariantType = class(TInvokeableVariantType) public procedure Clear(var V: TVarData); override; procedure Copy(var Dest: TVarData; const Source:TVarData; const Indirect:boolean); override; function DoProcedure(const V:TVarData; const Name:string; const Arguments:TVarDataArray):boolean; override; function DoFunction(var Dest:TVarData; const V:TVarData; const Name:string; const Arguments:TVarDataArray):boolean; override; end; TForm6 = class(TForm) Button1: TButton; Memo1: TMemo; procedure Button1Click(Sender: TObject); private { Private declarations } FTestVariantType:TTestVariantType; public { Public declarations } constructor Create(Owner:TComponent); override; function CreateTestVariant:variant; procedure Test; end; var Form6:TForm6; implementation {$R *.fmx} procedure TTestVariantType.Clear(var V: TVarData); begin V.VType:=varEmpty; end; procedure TTestVariantType.Copy(var Dest:TVarData; const Source:TVarData; const Indirect:boolean); begin if Indirect and VarDataIsByRef(Source) then VarDataCopyNoInd(Dest,Source) else begin TVarData(Dest).VType:=TVarData(Source).VType; TVarData(Dest).VPointer:=nil; end; end; function TTestVariantType.DoProcedure(const V:TVarData; const Name:string; const Arguments:TVarDataArray):boolean; begin Result:=True; end; function TTestVariantType.DoFunction(var Dest:TVarData; const V:TVarData; const Name:string; const Arguments:TVarDataArray):boolean; var s:string; i,n:integer; v1:variant; vd:TVarData; begin n:=length(Arguments); Form6.Memo1.Lines.Add('Will attempt to show '+inttostr(n)+' arguments:'); for i:=0 to n-1 do begin vd:=Arguments[i]; v1:=Variant(vd); s:=v1; Form6.Memo1.Lines.Add(inttostr(i)+') VType='+inttostr(vd.VType)+'='+s); end; Form6.Memo1.Lines.Add('-----'); Result:=true; end; // --------------------------------------- // TForm6 // --------------------------------------- constructor TForm6.Create(Owner:TComponent); begin inherited Create(Owner); FTestVariantType:=nil; end; procedure TForm6.Button1Click(Sender: TObject); begin FTestVariantType:=TTestVariantType.Create; try Test; finally FreeAndNil(FTestVariantType); end; end; function TForm6.CreateTestVariant:variant; begin TVarData(Result).VType:=FTestVariantType.VarType; end; procedure TForm6.Test; var V1,V2:variant; begin V1:=CreateTestVariant; // Make two function calls. // Win32, Win64, IOS 64, Android 32 all works well. // Android 64 provides incorrect argument management invalidating the 3 string arguments. V2:=V1.func('DEF','',''); // Extra call to see if argument order makes any distinction to the outcome of the error. It does not. V2:=V1.func('','','ABC'); end; end.
What the code does is to create a sample invokable variant. An invokable variant is a variant that can be used for calling late bound functions and procedures. In other words you can say v.MyProc(..) without ever having a true function in your code called MyProc. What happens is that the Delphi compiler generates code that will instead call a special default abstract function that you will need to override, called DoProcedure, providing the name of the late bound function name (MyProc) as a string, and the arguments as an array of TVarData.
The programmer can then choose to do what he want to do in that situation. From the outside, your code looks like you are calling a true function that is linked in, but in reality it is always ending up in your invokable variants DoProcedure or DoFunction method.
Hence the above code continues to call the late bound (non existing in reality) function called func with 3 string arguments, and then again, just with a different set of string arguments.
When it runs fine, which it does on Win32, Win64, IOS 64 and Android 32 (I have not checked Linux), the form will look like this (true screenshot from a Samsung S9+ with Android 10):
But when compiling for Android 64 bit and running it on the same Samsung S9+, this is the result:
I have tried to debug what happens, to see if I could patch the bad behaviour from the outside (without changing system code in Delphi), but I did not find the solution.
So for the time being I can unfortunately not recommend using one of SmartClient’s smartest features, the late bound function call.
https://quality.embarcadero.com/browse/RSP-28097
Hi,
I think that the issue is somewhat different. I do provide Unicode strings as the arguments, and it does work on IOS 64, which afaik mimics MacOS closely API and handling wise. But I have not tested it as an OSX application so I do not know if “my” problem will be there on OSX or not.
/Kim