A site devoted to discussing techniques that promote quality and ethical practices in software development.

Saturday, February 17, 2007

A Delphi.Net IL generation bug

A Delphi.Net IL code generation bug has been discovered that can mislead non-Delphi.Net assembly. This is case that shows that Delphi.Net is generating assemblies that not only use CLR metadata but also Delphi.Net metadata, which is unknown to any CLR compiler.

Here is the structure that demonstrates the bug.

I have a Delphi.Net (Delphi 2006) package, called PackageA, which contains UnitA that contains an interface IA. It is irrelevant what method IA prescribes but for argument sake, let's say if specifies a method called SayHi() like this.

unit UnitA;

interface

type IA = interface
procedure SayHi();
end;

Then we have a second Delphi.Net package, called PackageB, which contains a UnitB that contains IB as follows:

unit UnitB;

interface

uses UnitA;

type IB = interface( IA )
procedue Yell();
end;
Everything compiled and linked provided that you have defined the references correctly. Now let's consider PackageA.dll and PackageB.dll as kind of interfaces publishing assemblies and we then implements the following concrete class that implements IB in C#:

using namespace UnitB;
public class CSharpClass : IB {
public void Yell() { Console.WriteLine( "I am yelling" ); }
};
When I used VS2003 to develop this and pressing the tab key after typing IB the IDE only added the implementation of Yell() method. I was puzzled.

Even more bizarre is that that assembly not only compiled and built but it also runs!

I then check the PackageB.dll with Lutz's Reflector and to my horror, I cannot see the base interface of IB and this explains why the C# class compiled and built.

Digging this further I disassembly the IB in PackageB and this is the IL code Delphi.Net generates:

.class interface public abstract auto ansi IB
{
.custom instance void .ctor(string) = ( ...... ) // ...
.method public hidebysig newslot abstract virtual
instance void Yell() cil managed
{
} // end of method IB::Yell

} // end of class IB

No wonder the C# only requires to implement just Yell(). I am wondering will this make any difference if I implement a concrete class in Delphi.Net just like the C# class.

To my astonishment, my class

type MyDelphiClass = class( System.Object, IB )
public:
procedure Yell();
end;

did not compiled and was complaining about the need to implement IA.SayHi() and the need to include UnitA.

According to the IL code of PackageB.dll it does not contain any IL code of the base (required interface) of IB, where does Delphi get the information. The only conclusion one can draw is that Delphi.Net is not playing by the rule properly again! It is digging into its Delphi metadata rather than relying purely on CLR metadata.

According to ECMA-335 CLI standard Partition II, 12.1 this is what it says:
Interfaces shall declare that they require the implementation of zero or more other interfaces. If one interface, A, declares that it requires the implementation of another interface, B, then A implicitly declares that it requires the implementation of all interfaces required by B.

Now if PackageB.dll is rewritten in C# or even VB.Net, it would generate the following IL code for the interface IB:

.class interface public abstract auto ansi IB
implements [UnitA]UnitA.IA
{
} // end of class IB
Note that now IB specifies that it requires IA unambiguously.

You may ask what is the big deal when the final concrete class's class IL code is the same. Yes, that is so provided that you are using Delphi.Net throughout and that you are not using Reflection. This is not .Net!

If you use reflection to examine PackageB.dll for IB, you will discover that:
# typeIB.IsInterface is true
# (typeIB.GetInterfaces()).Length = 0

This means Delphi.Net is generating wrong IL code misleading the user of that assemblies.

Digging this further, I discovered that the missing IA from interface IB is caused by the now infamous symbol tossing bug. Delphi.Net has this bug that when it detects no one references a symbol - be it in a package or an executable application, the compiler simply tosses it away.

The only way to keep them is to litter your program with variables of the type you want to keep. This not only promotes dangerous trap for maintenance worker but also being stupid particularly in a package. By definition of the non-.Net world, any public symbols in a package is for someone outside to use. In .Net, executable assembly is being loaded just like a package. So how can the package foretell no one outside the package is using it and then tossing it away.

Either way, it is a compiler bug that does not generate CLI conforming code. Beware when you are planning to migrate to .Net world in Delphi. You could be producing solution that you, in your all Delphi.Net solution, works fine but your customer using your public artifacts to extend finding their assemblies not working.

No comments:

Blog Archive