Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

F# interop with C# class that has an optional nullable parameter set to anything but null causes NullReferenceException / AccessViolationException

I have the following C# classes

public class BadClass
{
    public BadClass(int? bad = 1)
    {
    }
}

public class GoodClass
{
    public GoodClass(int? good = null)
    {
    }
}

As you can see they both have optional nullable parameters as part of their constructors, the only difference is that BadClass has the parameter default set to something other than null.

If I attempt to create an instance of these classes in F# this is what I get:

This works fine:

let g = GoodClass()

This throws a NullReferenceException:

let b = BadClass()

And this throws an AccessViolationException

let asyncB = async { return BadClass() } |> Async.RunSynchronously

Any idea why this is?

EDIT

Using ILSpy to decompile it this is the output of the F#

The C# classes are in an assembly called InteopTest [sic]

ILSpy to C#

GoodClass g = new GoodClass(null);
    BadClass b = new BadClass(1);
    FSharpAsyncBuilder defaultAsyncBuilder = ExtraTopLevelOperators.DefaultAsyncBuilder;
    FSharpAsync<BadClass> fSharpAsync = defaultAsyncBuilder.Delay<BadClass>(new Program.asyncB@10(defaultAsyncBuilder));
    FSharpAsync<BadClass> computation = fSharpAsync;
    BadClass asyncB = FSharpAsync.RunSynchronously<BadClass>(computation, null, null);
    FSharpFunc<string[], Unit> fSharpFunc = ExtraTopLevelOperators.PrintFormatLine<FSharpFunc<string[], Unit>>(new PrintfFormat<FSharpFunc<string[], Unit>, TextWriter, Unit, Unit, string[]>("%A"));
    fSharpFunc.Invoke(argv);
    return 0;

and this is the IL

.method public static 
    int32 main (
        string[] argv
    ) cil managed 
{
    .custom instance void [FSharp.Core]Microsoft.FSharp.Core.EntryPointAttribute::.ctor() = (
        01 00 00 00
    )
    // Method begins at RVA 0x2050
    // Code size 92 (0x5c)
    .maxstack 5
    .entrypoint
    .locals init (
        [0] class [InteopTest]InteopTest.GoodClass g,
        [1] valuetype [mscorlib]System.Nullable`1<int32>,
        [2] class [InteopTest]InteopTest.BadClass b,
        [3] class [InteopTest]InteopTest.BadClass asyncB,
        [4] class [FSharp.Core]Microsoft.FSharp.Control.FSharpAsync`1<class [InteopTest]InteopTest.BadClass>,
        [5] class [FSharp.Core]Microsoft.FSharp.Control.FSharpAsyncBuilder builder@,
        [6] class [FSharp.Core]Microsoft.FSharp.Control.FSharpAsync`1<class [InteopTest]InteopTest.BadClass>,
        [7] class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<string[], class [FSharp.Core]Microsoft.FSharp.Core.Unit>,
        [8] string[]
    )

    IL_0000: nop
    IL_0001: ldloca.s 1
    IL_0003: initobj valuetype [mscorlib]System.Nullable`1<int32>
    IL_0009: ldloc.1
    IL_000a: newobj instance void [InteopTest]InteopTest.GoodClass::.ctor(valuetype [mscorlib]System.Nullable`1<int32>)
    IL_000f: stloc.0
    IL_0010: ldc.i4.1
    IL_0011: newobj instance void [InteopTest]InteopTest.BadClass::.ctor(valuetype [mscorlib]System.Nullable`1<int32>)
    IL_0016: stloc.2
    IL_0017: call class [FSharp.Core]Microsoft.FSharp.Control.FSharpAsyncBuilder [FSharp.Core]Microsoft.FSharp.Core.ExtraTopLevelOperators::get_DefaultAsyncBuilder()
    IL_001c: stloc.s builder@
    IL_001e: ldloc.s builder@
    IL_0020: ldloc.s builder@
    IL_0022: newobj instance void Program/asyncB@10::.ctor(class [FSharp.Core]Microsoft.FSharp.Control.FSharpAsyncBuilder)
    IL_0027: callvirt instance class [FSharp.Core]Microsoft.FSharp.Control.FSharpAsync`1<!!0> [FSharp.Core]Microsoft.FSharp.Control.FSharpAsyncBuilder::Delay<class [InteopTest]InteopTest.BadClass>(class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<class [FSharp.Core]Microsoft.FSharp.Core.Unit, class [FSharp.Core]Microsoft.FSharp.Control.FSharpAsync`1<!!0>>)
    IL_002c: stloc.s 4
    IL_002e: ldloc.s 4
    IL_0030: stloc.s 6
    IL_0032: ldloc.s 6
    IL_0034: ldnull
    IL_0035: ldnull
    IL_0036: call !!0 [FSharp.Core]Microsoft.FSharp.Control.FSharpAsync::RunSynchronously<class [InteopTest]InteopTest.BadClass>(class [FSharp.Core]Microsoft.FSharp.Control.FSharpAsync`1<!!0>, class [FSharp.Core]Microsoft.FSharp.Core.FSharpOption`1<int32>, class [FSharp.Core]Microsoft.FSharp.Core.FSharpOption`1<valuetype [mscorlib]System.Threading.CancellationToken>)
    IL_003b: stloc.3
    IL_003c: ldstr "%A"
    IL_0041: newobj instance void class [FSharp.Core]Microsoft.FSharp.Core.PrintfFormat`5<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<string[], class [FSharp.Core]Microsoft.FSharp.Core.Unit>, class [mscorlib]System.IO.TextWriter, class [FSharp.Core]Microsoft.FSharp.Core.Unit, class [FSharp.Core]Microsoft.FSharp.Core.Unit, string[]>::.ctor(string)
    IL_0046: call !!0 [FSharp.Core]Microsoft.FSharp.Core.ExtraTopLevelOperators::PrintFormatLine<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<string[], class [FSharp.Core]Microsoft.FSharp.Core.Unit>>(class [FSharp.Core]Microsoft.FSharp.Core.PrintfFormat`4<!!0, class [mscorlib]System.IO.TextWriter, class [FSharp.Core]Microsoft.FSharp.Core.Unit, class [FSharp.Core]Microsoft.FSharp.Core.Unit>)
    IL_004b: stloc.s 7
    IL_004d: ldarg.0
    IL_004e: stloc.s 8
    IL_0050: ldloc.s 7
    IL_0052: ldloc.s 8
    IL_0054: callvirt instance !1 class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<string[], class [FSharp.Core]Microsoft.FSharp.Core.Unit>::Invoke(!0)
    IL_0059: pop
    IL_005a: ldc.i4.0
    IL_005b: ret
} // end of method Program::main
like image 849
TWith2Sugars Avatar asked Jan 07 '14 20:01

TWith2Sugars


1 Answers

To me, this looks like a bug in the F# compiler. If you write some extra C#:

public class OtherClass
{
    private static BadClass _bc = new BadClass();
}

and look at the IL, you'll see this:

// push 1 on the stack
IL_0000:  ldc.i4.1
// call Nullable<int32> constructor, leaving object on stack
IL_0001:  newobj     instance void valuetype [mscorlib]System.Nullable`1<int32>::.ctor(!0)
// call BadClass constructor with int?
IL_0006:  newobj     instance void Nullabool.BadClass::.ctor(valuetype [mscorlib]System.Nullable`1<int32>)
// store in _bc
IL_000b:  stsfld     class Nullabool.BadClass Nullabool.OtherClass::_bc

Which clearly instantiates Nullable`1 with 1.

Whereas the F# code for b ends up like this:

// push a 1 on the stack
IL_0016:  ldc.i4.1
// call BadClass constructor with 1 - this fails IL verification
IL_0017:  newobj     instance void [Nullabool]Nullabool.BadClass::.ctor(valuetype [mscorlib]System.Nullable`1<int32>)

which leaves an int on the stack instead of int?. When I try to run this code, I get an IL verification error since the type doesn't match.

like image 81
plinth Avatar answered Nov 17 '22 01:11

plinth