Soon after I released the free version of MMX Code Explorer, I got complaints from people having problems after they tried it in Delphi 10.2 Tokyo initial release and with Update 1 installed. The version I released was compiled with Delphi 10.2 Update 3 and when loaded in a Delphi 10.2 with no update or only Update 1 it popped up with an error message about a missing procedure entry point “@Dockform@TDockableForm@DoEndDock”.
As a quick fix to this problem I released another version compiled with Delphi 10.2 Tokyo (no updates), which solved it for those versions as well as apparently made no harm to installations with Update 2 and 3 – apparently!
After I got the complaining users satisfied for the moment I investigated a bit more closely what the real cause of the problem actually was. Despite seeing no direct problems with the workaround above, I somehow felt a bit uncomfortable. Perhaps there was some little beast lurking in the dark, which may raise its ugly head some day when no one expects it. In addition, having to compile with a not up-to-date version doesn’t make life easier on the long run, too.
A straight forward search revealed one class in the code inherited from TDockableForm overriding DoEndDock. As expected there was a call to inherited DoEndDock in the implementation. All looked quite reasonable to me.
Then I moved the mouse over the inherited DoEndDock and waited for the Help Insight window to popup. This is what it looked with the initial Tokyo release:
We can see that the inherited call goes to TControl.DoEndDock. Still nothing to worry about here.
Now lets see what the same code reveals in Update 3:
Hey, that looks indeed a bit suspicious. In Update 3 the inherited call will go to TDockableForm.DoEndDock instead of TControl.DoEndDock.
Now comes the tricky part: Virtual method calling is a one way process!
When a virtual method is called, the most significant override of this method is called through the Virtual Method Table (VMT) – we know that, don’t we? Thus we don’t have to know the concrete class when we make that call. The VMT takes care of the correct method to be called.
Virtual method calling is a one way process!
On the other hand, when we call inherited in this overridden method, the compiler knows, which inherited method of which class is going to be called and resolves that call to the correct address. Seems legit – well, somehow, but yes.
Everything works as expected when we compile a single executable where all methods are linked in and entry points are known. Things look different when packages come into play.
When compiling with packages the compiler uses the DCP file to learn which method has to be called with inherited. At runtime the corresponding BPL will then provide the proper procedure entry point. Unfortunately this mechanism fails, when the runtime package version is different. In that case the procedure entry points don’t match or are simply non-existent.
In this case Update 2 introduces an override of TDockableForm.DoEndDock adding a new procedure entry point. A compiler seeing this override will rightly call this method instead of TControl.DoEndDock. Now when a package before Update 2 is used during runtime, we cannot find that entry point any more. That is exactly the error we got in the beginning.
So, does compiling the expert against the older DCP really solve the problem? At least it makes the error message go away when run in the older environment. But what about running under Update 2 or 3? The method called for inherited is still TControl.DoEndDock and that exists and the call succeeds. The bad thing about this is that we miss to call TDockableForm.DoEndDock in this case. I have no idea what this method does, but with the current version of the expert it is simply never called for all dockforms created by the expert. I could feel a deep grumbling in my stomach when I realized that.
The conclusion to be drawn from this is: When it comes to packages and you want to be binary compatible between your package versions, you better not touch the interface sections of your units! Introducing a new inheritance level for a virtual method – or worse, removing one – will break binary compatibility. That is a direct consequence of how the compiler resolves inherited calls.
For Delphi this limits the possibilities of what can be implemented in a so called point release, where binary compatibility is key.
How can we overcome this limitation? It depends. In this case I removed the overridden method and wired an event handler for OnEndDock, hoping that nowhere in the IDE some other event handler will steel this link. I know that this will not always be an option.
The next release will incorporate this fix, which also allows me to compile with Delphi 10.2 Update 3 again. Simplifying the build process is currently my main task regarding MMX Code Explorer. The need to use a specific Tokyo version for compiling the expert threw a massive burden on my build system. Having things back to normal is a big relief.