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

Tuesday, March 27, 2007

More on Delphi.Net's handling of "array of" parameter.

I have raised a concern and claimed that Delphi.Net generated CLI incompatible assembly to handle method that contained "array of String" in my previous blog.

In this message, I would like to make one correction that I made in that blog, though it does not invalidate the claim, to set the record straight and to report the complete analysis of the issue in handling "array of" in Delphi.Net.

The correction I would like to make is in relation to the Array.Clone() in which I allegedly and mistakenly claimed that it would invoke the ICloneable.Clone() and would perform deep copy.

System.Array.Clone() does not use deep copy. In fact according to the standard documentation, System.Array.Clone() performs only shallow copy. This operation will be proven to be Delphi's Achilles heel in its naive attempt to prevent code from changing elements of an array when ECMA clearly permits.

It will be shown that Delphi.Net's naive implementation restricts the design of a method of allowing null array of anything to be passed.

Passing an array of immutable objects

This is actually a generalised case of "array of String" because System.String is an immutable class. The situation is no different from that reported in my previous blog.

In dealing with an array of anything, Delphi compiler immediately generates a call to the Array.Clone() to overwrite the incoming array with its cloned version in vein attempt to prevent changes propagating back to the caller upon returning. The following IL fragment confirms this:

.method public newslot virtual final instance
void PlayWithData(string[] ar) cil managed
{
.param [1]
.custom instance void [mscorlib]System.ParamArrayAttribute::.ctor()
.override [CloneDemoSupport]LMar.CloneDemoSupport.IDataChanger2::PlayWithData
.maxstack 6
.locals init (
[0] int32 n,
[1] int32 num,
[2] int32 len)
L_0000: ldarg.1
L_0001: callvirt instance object [mscorlib]System.Array::Clone()
L_0006: castclass string[]
L_000b: starg.s ar
So any user code will be accessing this cloned version. The minute the clone is made, each element is referring to the same address since shallow copy simply duplicates the object reference (ie. the address).

However, because each element is an immutable object, its state cannot be changed unless a new object, with therefore a new object reference, is created. But this object reference is now stored in an array that is inside the function, a la an auto-variable of array type. Its value will not be passed back to the caller since only the address or reference to the array is pushed onto the stack in passing to the function.

In this scenario, Delphi.Net is successful in preventing changes made in the function from propagating back to the caller. In so doing, it has damaged the ECMA-CLI compatibility because such an assembly produces a different behaviour that a conforming one.

Passing an array of mutable objects

Previously we have considered an array of immutable objects and have found that Delphi.Net is successful in creating ECMA-CLI incompatible methods. How successful is it in dealing with an array of mutable objects?

Once again, we have discovered that Delphi.Net does not distinguish this and blindly invokes the Array.Clone() as shown here:
.method public newslot virtual final instance
void PlayWithData(class [CloneDemoSupport]CloneDemoSupport.Foo[] ar) cil managed
{
.param [1]
.custom instance void [mscorlib]System.ParamArrayAttribute::.ctor()
.override [CloneDemoSupport]CloneDemoSupport.IDataChanger::PlayWithData
.maxstack 5
.locals init (
[0] int32 n,
[1] int32 num,
[2] int32 len)
L_0000: ldarg.1
L_0001: callvirt instance object [mscorlib]System.Array::Clone()
L_0006: castclass [CloneDemoSupport]CloneDemoSupport.Foo[]
L_000b: starg.s ar

If the function does not attempt to replace each element with a brand new element, one can use the class' methods to change the state because by definition of mutable class, the class supports methods to change the state.

Since the object's reference is not changed in the cloned version, it is now addressing the same object as that held by the caller. In so doing changing the state of that object inside the function is the same as if the caller is changing the state of the same object.

In this usage, Delphi.Net's attempt to prevent method from changing the array fails and the Array.Clone() operation is superfluous wasting CPU and memory.

It can only be successful when one replaces an element with a brand new one.

Hence depending on implementation technique, Delphi.Net can produce assembly that sometimes conforms to ECMA-CLI specification while other time it becomes incompatible. This kind of inconsistency is not acceptable.

Array of any parameter is not allow to receive nil in Delphi.Net

If you examine the above listed IL code fragment, it becomes obvious that any Delphi.Net method having a "array of" anything parameter is not allowed to pass in a nil (or null in C#) array. The designer has no choice!

The reason is that if an array is nil, calling its Clone() method will cause NullReferenceException to be thrown.

In fact, we encountered this situation in practice but at that time we naively trusting Delphi.Net compiler to be doing the right thing. Now we realise that it is due to its naive implementation. The bug was made harder to track down because the exception pointed to none of the user code but to the method's entry.

For all these problems, Borland/CodeGear should reconsider its folly in trying to make Delphi.Net, which is governed by an ECMA standard, to be compatible, without consistent success I may add, with something proprietary! It is sheer madness.

Given Borland/CodeGear's pass history in addressing their bug, I doubt Delphi.Net will be corrected. Hence anyone contemplating of developing .Net solution should take these bugs into consideration.

No comments:

Blog Archive