Thursday, February 01, 2018

F# under the covers XVI -- Constructor weirdness

Occasionally, even in F#, one needs to do OO stuff, like implementing a concrete subclass of some abstract framework type to feed into some other framework API. In my case, I recently needed to add a SerializationBinder to a BinaryFormatter to handle assembly versioning.

So of course I wrote

formatter.Binder <- { new System.Runtime.Serialization.SerializationBinder()
  with member self.BindToType (a:string, t:string) = ... }

which worked perfectly happily, but threw up a warning from Gendarme about suspicious recursion in the constructor.

So I decompiled the type to find it looked like

[Serializable]
[StructLayout(LayoutKind.Auto, CharSet = CharSet.Auto)]
[CompilationMapping(SourceConstructFlags.Closure)]
internal sealed class ReadResults@64 : SerializationBinder
{
 public ReadResults@64()
 {
  ((SerializationBinder)this)..ctor();
 }

 public override Type BindToType(string _arg1, string _arg2)
 {
  ...
 }
}

or, as IL

.method public specialname rtspecialname 
 instance void .ctor () cil managed 
{
 // Method begins at RVA 0x3ce0
 // Code size 9 (0x9)
 .maxstack 8

 IL_0000: ldarg.0
 IL_0001: callvirt instance void [mscorlib]System.Runtime.Serialization.SerializationBinder::.ctor()
 IL_0006: ldarg.0
 IL_0007: pop
 IL_0008: ret
}

Writing the type as an explicit class, like

type UpdateBinder () =
  inherit System.Runtime.Serialization.SerializationBinder()
  override self.BindToType ...

yields exactly the same sort of IL.

Revising the class yet again as

type MonoTypeBinder (``type``:Type) =
  inherit System.Runtime.Serialization.SerializationBinder()
  override self.BindToType (_:string, _:string) =
    ``type``

because I only have one type of interest, did produce the expected decompiled constructor, looking like

public MonoTypeBinder(Type type)
  : this()
 {
  this.type = type;
 }

even though the actual IL just adds the field assignment

IL_0000: ldarg.0
 IL_0001: callvirt instance void [mscorlib]System.Runtime.Serialization.SerializationBinder::.ctor()
 IL_0006: ldarg.0
 IL_0007: pop
 IL_0008: ldarg.0
 IL_0009: ldarg.1
 IL_000a: stfld class [mscorlib]System.Type AltCover.MonoTypeBinder::'type'
 IL_000f: ret

However, now we've changed the signature, the call no longer looks like a recursion. And, for once, this is a case where the virtual call in a constructor is safe.

By contrast, a C# equivalent

class UpdateBinder : System.Runtime.Serialization.SerializationBinder
    {
        public override Type BindToType(string a, string t)
        {
            ...
        }
    }

generates a default constructor with IL that makes a non-virtual call to the base type

IL_0000: ldarg.0
  IL_0001: call instance void [mscorlib]System.Runtime.Serialization.SerializationBinder::.ctor()
  IL_0006: nop
  IL_0007: ret

and the call remains non-virtual when adding a constructor argument.

No comments :