dotnet / fsharp

The F# compiler, F# core library, F# language service, and F# tooling integration for Visual Studio
https://dotnet.microsoft.com/languages/fsharp
MIT License
3.9k stars 782 forks source link

BadImageFormatException : Bad IL format when using `base` #13926

Closed abelbraaksma closed 7 months ago

abelbraaksma commented 2 years ago

Using base.XXX to call a base method causes a BadImageFormatException to be raised.

EDIT: it looks like F# allowing me to call an abstract method, as the base class is abstract. This, I believe, should not be allowed and JIT therefore throws a BadImageFormatException. If you try to write code in F# with an AbstractClass and a derived class, you'd receive a normal compile-time error.

Repro steps

The following code (requires Giraffe and FsUnit to run the test) throws a BadImageFormatException

namespace Test

open Giraffe
open Xunit
open FsUnit.Xunit

open System.Text.Json
open System.Text.Json.Serialization

type StringTrimJsonSerializer(o: JsonSerializerOptions) =
    inherit JsonConverter<string>()

    override this.Read(reader, _, _) =
        match reader.TokenType with
        | JsonTokenType.String -> reader.GetString().Trim()
        | _ -> JsonException("Type is not a string") |> raise

    /// This causes a BadImageFormatException
    override this.Write(writer, objectToWrite, options) = base.Write(writer, objectToWrite, options)

module SerializationTests =

    type SomeType = { Amount: decimal; Currency: string }

    let serialize item =
        let options = SystemTextJson.Serializer.DefaultOptions
        StringTrimJsonSerializer options |> options.Converters.Add
        JsonSerializer.Serialize(item, options)

    let deserialize<'T> (stringValue: string) =
        let options = SystemTextJson.Serializer.DefaultOptions
        StringTrimJsonSerializer options |> options.Converters.Add
        JsonSerializer.Deserialize<'T>(stringValue, options)

    [<Fact>]
    let ``Roundtip type should trim currency whitespace`` () =
        { Amount = 42.99M; Currency = "  USD   " }
        |> serialize
        |> deserialize<SomeType>
        |> should equal { Amount = 42.99M; Currency = "USD" }

Expected behavior

Should call the base method or give compile error if such method doesn't exist.

Actual behavior

Throws:

BadImageFormatException : Bad IL format.

Known workarounds

Don't use base in a derived method.

Related information

On dotnet 6, using System.Text.Json serialization BCL classes, Windows 10/11.

EDIT, the IL of the offending code looks as follows (Release build):

.method public hidebysig virtual 
    instance void Write (
        class [System.Text.Json]System.Text.Json.Utf8JsonWriter writer,
        string objectToWrite,
        class [System.Text.Json]System.Text.Json.JsonSerializerOptions options
    ) cil managed 
{
    // Method begins at RVA 0x316c
    // Header size: 1
    // Code size: 10 (0xa)
    .maxstack 8

    // base.Write(writer, objectToWrite, options);
    IL_0000: ldarg.0
    IL_0001: ldarg.1
    IL_0002: ldarg.2
    IL_0003: ldarg.3
    IL_0004: call instance void class [System.Text.Json]System.Text.Json.Serialization.JsonConverter`1<string>::Write(class [System.Text.Json]System.Text.Json.Utf8JsonWriter, !0, class [System.Text.Json]System.Text.Json.JsonSerializerOptions)
    // }
    IL_0009: ret
} 
vzarytovskii commented 2 years ago

Reproduces on

module Testing

open System.Text.Json
open System.Text.Json.Serialization

type StringTrimJsonSerializer(o: JsonSerializerOptions) =
    inherit JsonConverter<string>()
    override this.Read(reader, _, _) =
        match reader.TokenType with
        | JsonTokenType.String -> reader.GetString().Trim()
        | _ -> JsonException("Type is not a string") |> raise
    override this.Write(writer, objectToWrite, options) = base.Write(writer, objectToWrite, options)

type SomeType = { Foo: string}

let serialize item =
    let options = JsonSerializerOptions()
    StringTrimJsonSerializer options |> options.Converters.Add
    JsonSerializer.Serialize(item, options)

[<EntryPoint>]
let main _ =
    { Foo = "a" } |> serialize
    0

Does not reproduce on:

module Testing

open System.Text.Json
open System.Text.Json.Serialization

type StringTrimJsonSerializer(o: JsonSerializerOptions) =
    inherit JsonConverter<string>()
    override this.Read(reader, _, _) =
        match reader.TokenType with
        | JsonTokenType.String -> reader.GetString().Trim()
        | _ -> JsonException("Type is not a string") |> raise
    override this.Write(writer, objectToWrite, options) = base.Write(writer, objectToWrite, options)

type SomeType = { Foo: int }

let serialize item =
    let options = JsonSerializerOptions()
    StringTrimJsonSerializer options |> options.Converters.Add
    JsonSerializer.Serialize(item, options)

[<EntryPoint>]
let main _ =
    { Foo = 1 } |> serialize
    0

Both release and debug

abelbraaksma commented 2 years ago

@vzarytovskii The latter never calls the StringTrimJsonSerializer.Write method (there's no string in the type), so it makes sense that it doesn't fail. The JIT never has to emit the method, so you won't get the BadImageFormatException.

The problem seems to be that base here is an abstract class and the Write method itself is also abstract. In other words, it should never compile. If you try this with F# classes (abstract A, concrete B derives from A) then if B uses base.Foo it'll give a compile error saying that you cannot call an abstract method.

The same is true here, but for some reason, F# doesn't consider it an abstract method.