7

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
TWith2Sugars
  • 3,384
  • 2
  • 26
  • 43

1 Answers1

6

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.

plinth
  • 48,267
  • 11
  • 78
  • 120
  • I'm not that proficient in IL but where does it instantiates it with 1? – TWith2Sugars Jan 07 '14 at 20:56
  • 1
    Added comments for you. – plinth Jan 07 '14 at 21:03
  • Ah I see now, I'll dig around the F# compiler source and see if I can find where this happens. – TWith2Sugars Jan 07 '14 at 21:07
  • @TWith2Sugars I'm pretty sure this is a bug in the F# compiler (not understanding `Nullable` properly). Going to file a bug about it. – Reed Copsey Jan 07 '14 at 22:20
  • 6
    This is indeed a compiler bug, was independently reported to fsbugs@Microsoft.com a little while back. It only occurs when you have nullable, optional args, and you omit some of them in the call (i.e. filling in all optional args will work). This is not a new bug in 3.1, it has existed for a while. For those interested, issue is in [tc.fs](http://fsharppowerpack.codeplex.com/SourceControl/latest#compiler/3.1/Nov2013/src/fsharp/tc.fs) where it handles optional args. Search for `Constant fieldInit`. – latkin Jan 07 '14 at 22:30
  • @latkin Do you happen to know if there is plans to fix this or if there is a bug tracker for f# (apart from the github issues) that I could look at? – TWith2Sugars Jan 08 '14 at 10:49
  • It's filed in the msft internal bug system, and it will probably get fixed during some future bugfix push :) That's a pretty lame answer but I don't have any specifics available in terms of a date when a fix might be in your hands. – latkin Jan 09 '14 at 01:19
  • 1
    Update: it's fixed https://visualfsharp.codeplex.com/SourceControl/changeset/14f88ddaad4437466dd84639a2751d9c10ea126d – latkin Jun 17 '14 at 22:10
  • 1
    Thanks, I got round the problem by not using C# at all, seems to have made everything better. – TWith2Sugars Jun 18 '14 at 14:33