After countless hours of debugging 5.07.00, I’ve discovered an annoying bug in Delphi’s System.Variants.pas unit which results in an unfortunate leak when using custom variants that internally either allocate memory or reference count an interface (which is the case in kbmMW’s smart arguments).
Basically it happens in these situations (using kbmMW client side code):
var c:IkbmMWSmartClient; b:boolean; v:variant; person:TPerson; begin Transport.Host:=eIP.Text; // Get a client which establishes connection over the given transport // to the given service which is set to be default for this client. c:=TkbmMWSmartRemoteClientFactory.GetClient(Transport,'SMARTDEMO'); // Call with regular object instance. // The lifetime of the object is handed over to the framework. person:=TPerson.Create; b:=c.Service.StorePerson(Use.Arg(person)); end;
Use.Arg(person) creates and returns a custom variant which holds a reference counted version of person. The idea is that the person object must stay alive long enough that we can stream it to the server. Thus we have to pass on ownership of person to the kbmMW framework, which is done via the custom variant.
This is imo a beautiful construction that removes the need for developer management of data lifetime, specially when data is to be either returned from a server call or used as arguments to a server call.
It all works fine… until we provide the custom variant as argument to the StorePerson method which in reality do not exist at all in the client side code. kbmMW is taking advantage of a Delphi feature called TInvokeableVariantType, which is yet another type custom variant, which typically is used when calling OLE/COM. That sounds horrible I know, but it is not at all as it does not contain any OLE or COM internally but is pure portable Delphi code that runs on all platforms.
The interesting detail about TInvokableVariantType is that the Delphi compiler accepts compilation of apparent function/procedure calls that do not in fact exist in the source code being compiled, as long as the function/procedure name is called via a variant of this type.
In the above code example, c.Service is in fact referring to the custom invokable variant Service. Hence Delphi will without questions, compile the code, and instead attempt to do late calling which kbmMW intercepts and converts into a regular kbmMW client side server request for the function StorePerson.
Again a very beautiful (although behind the scenes fairly code complex) feature.
This translates internally in Delphi to a series of calls, starting with DispInvoke, which calls GetDispatchInvokeArgs that converts provided arguments to the StorePerson method to an internal array of variants containing the arguments. Since our person argument is reference counted, its reference count will be incremented at this point.
When the call has been made (we skip lots of steps here which are uninteresting for the problem at hand), DispInvoke finally calls FinalizeDispatchInvokeArgs that should clean up if needed.
The problem is that FinalizeDispatchInvokeArgs contains a bug that results in custom variants not receiving the attention they should. The following is the problematic code.
// Only ByVal Variant or Array parameters have been copied and need to be released // Strings have been released via the use of the TStringRefList parameter to GetDispatchInvokeArgs if ((ArgType and atByRef) <> atByRef) and ((VType = varVariant) or ((VType and varArray) = varArray)) then VarClear(PVariant(PVarParm)^);
VarClear is skipped, because VType <> varVariant. VType for custom variants has a Delphi generated unique value ranging from $110 (272) to $7FF (2047). That results in our reference count not being decremented correctly.
This sucks big time, and there are no way that I can circumvent the problem to make FinalizeDispatchInvokeArgs work correctly without actively patching it. That’s not an option.
Interestingly if I change the code slightly to use a local variable, the call scenario changes fairly drastically.
var c:IkbmMWSmartClient; b:boolean; v:variant; person:TPerson; begin Transport.Host:=eIP.Text; // Get a client which establishes connection over the given transport // to the given service which is set to be default for this client. c:=TkbmMWSmartRemoteClientFactory.GetClient(Transport,'SMARTDEMO'); // Call with regular object instance. // The lifetime of the object is handed over to the framework. person:=TPerson.Create; v:=Use.Arg(person); b:=c.Service.StorePerson(v); end;
This will not leak! Whats the logic behind that?
The Delphi compiler decides that the argument type of StorePerson is a so called by reference argument, rather than a by value argument. This results in the whole DispInvoke call train to behave differently, avoiding deep copying of variants in the GetDispatchInvokeArgs method, which in turn results in no leaks.
Unfortunately I have no way to influence the compilers decision in when to mark the argument type as by reference or by value.
Fortunately is is possible to override the DispInvoke method in kbmMW’s TkbmMWSmartClientVariantType which handles all the smart client calling. Having access to DispInvoke, also means having access to the compilers understanding of the argument types (by ref or by val), which makes it possible for us to reliably detect if the kbmMW custom variant type contents should be cleared specifically, resulting in the missing _Release reference call.
This has been implemented in the upcoming bugfix release. It should fix the problem for all affected Delphi versions that kbmMW supports, including the latest 10.3 Rio.
For the curious look in the unit kbmMWSmartClient.pas for the TkbmMWSmartClientVariantType.DispInvoke and TkbmMWSmartClientVariantType.FixDelphiBug methods which are only compiled in, if the conditionally compile definition KBMMW_FIX_DISPINVOKE_LEAK_BUG is defined. It default is, until a version of Delphi is released that fix this problem. Check end of kbmMW.inc to find it.
If you like our products please “like” our posts and share them widely to support our continued effort in constantly improving the most complete middleware for Delphi.