Cysharp / MemoryPack

Zero encoding extreme performance binary serializer for C# and Unity.
MIT License
3.25k stars 190 forks source link

MemoryPackSerializationException when deserializing a union interface on Android/iOS #270

Open carmichaelalonso opened 6 months ago

carmichaelalonso commented 6 months ago

We are implementing MemoryPack v1.21.0 on our cross platform app (net8-windows, net8-ios, net8-android). Firstly, thanks for a great library!

Issue Summary

On Windows, we are experiencing no issues at all with our implementation, however we started running into random crashes on Android and iOS with specific data.

When we attempt to deserialize a Union into an interface using the generics API on iOS and Android, we are seeing this crash:

MemoryPack.MemoryPackSerializationException: MemoryPackAotCrash.IUnionSample is failed in provider at creating formatter.
 ---> System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation.
 ---> System.TypeInitializationException: The type initializer for 'MemoryPackAotCrash.IUnionSample' threw an exception.
 ---> System.BadImageFormatException: Method has no body
File name: 'MemoryPack.Core'
   at MemoryPack.MemoryPackFormatterProvider.Register[IUnionSample]()
   at MemoryPackAotCrash.IUnionSample..cctor() in C:\[path]\MemoryPackAotCrash\MemoryPackAotCrash\obj\Debug

The expected behaviour is that it creates an instance of FooClass which works as intended on Windows without NativeAOT.

I assume this is related to reflection usage and the use of Mono AOT on mobile platforms (iOS must use AOT and NativeAOT is still experimental). I upgraded to 1.21.0 today thinking that perhaps #237 would have solved this but it doesn't impact Mono AOT.

Repro Case

I've been able to reproduce this with a small test case using the sample code from the Polymorphism section of the readme and a clean Maui app:

public static void TestUnionSerializationDeserialization()
{
    IUnionSample data = new FooClass() { XYZ = 999 };

    // Serialize as interface type.
    var bin = MemoryPackSerializer.Serialize(data);

    // Deserialize as interface type.
    var reData = MemoryPackSerializer.Deserialize<IUnionSample>(bin);

    switch (reData)
    {
        case FooClass x:
            Console.WriteLine(x.XYZ);
            break;
        case BarClass x:
            Console.WriteLine(x.OPQ);
            break;
        default:
            break;
    }
}

[MemoryPackable]
[MemoryPackUnion(0, typeof(FooClass))]
[MemoryPackUnion(1, typeof(BarClass))]
public partial interface IUnionSample
{
}

[MemoryPackable]
public partial class FooClass : IUnionSample
{
    public int XYZ { get; set; }
}

[MemoryPackable]
public partial class BarClass : IUnionSample
{
    public string? OPQ { get; set; }
}

Running MemoryPack.Deserialize() here causes the crash on mobile platforms.

The easiest way to repro is to run the Maui project on Android with the emulator; iOS also works but debugging is a bit slower. I'll include a minimal sample project attached with the offending code in MauiProgram.cs

Minimal Repro Project - MemoryPackAotCrash.zip

Workaround

If I use an abstract class as the Union base instead of an interface (as the docs say), I don't get this crash, as below:

  [MemoryPackable]
  [MemoryPackUnion(0, typeof(FooClass))]
  [MemoryPackUnion(1, typeof(BarClass))]
  //public partial interface IUnionSample                   // REPRO: Using this interface will cause the crash
  public abstract partial class IUnionSample                // WORKAROUND: Using this abstract class will prevent the crash
  {
  }

I tried this in both my repro project and in our main project and it seems to fix the issue.

Let me know if you need me to test anything specific on mobile to try and debug this. Thank you!

neuecc commented 6 months ago

Thank you. To fully apply Native AOT, a fundamental understanding may be necessary. Currently, we're only applying stopgap measures.

SerratedSharp commented 1 month ago

This also occurs in a Blazor WebAssembly Standalone project. I have attached a minimal reproduction project in .NET 8.

BlazorAppMemoryPackBug.zip

This may be more related to trimming than it is AOT. There are attributes such as the DynamicDependencyAttribute that can be applied to types to flag them as dynamically accessible, but it gets pretty nuanced. I'm not clear on what is trying to be accessed dynamically that is failing. From the stack trace it appears to try and call a Constructor ctor on the interface IBob?

What is going on here that it is trying to call a constructor on the interface? Is this normal behavior for MemoryPack, or would it be expected that it would normally identify the underlying derived concrete type and try to construct it instead?

at BlazorAppMemoryPackBug.IBob..cctor()

You can set a breakpoint in the catch block, or open the browser debug console as this is where WebAssembly Console.WriteLine outputs to: image

MemoryPack.MemoryPackSerializationException: BlazorAppMemoryPackBug.IBob is failed in provider at creating formatter.
blazor.webassembly.js:1  ---> System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation.
blazor.webassembly.js:1  ---> System.TypeInitializationException: The type initializer for 'BlazorAppMemoryPackBug.IBob' threw an exception.
blazor.webassembly.js:1  ---> System.BadImageFormatException: Method has no body
blazor.webassembly.js:1 File name: 'MemoryPack.Core'
blazor.webassembly.js:1    at MemoryPack.MemoryPackFormatterProvider.Register[IBob]()
blazor.webassembly.js:1    at BlazorAppMemoryPackBug.IBob..cctor() in C:\Users\Aaron\source\repos\BlazorAppMemoryPackBug\obj\Debug\net8.0\MemoryPack.Generator\MemoryPack.Generator.MemoryPackGenerator\BlazorAppMemoryPackBug.IBob.MemoryPackFormatter.g.cs:line 31
blazor.webassembly.js:1    --- End of inner exception stack trace ---
blazor.webassembly.js:1    at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args)
blazor.webassembly.js:1    at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)
blazor.webassembly.js:1    --- End of inner exception stack trace ---
blazor.webassembly.js:1    at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)
blazor.webassembly.js:1    at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
blazor.webassembly.js:1    at System.Reflection.MethodBase.Invoke(Object obj, Object[] parameters)
blazor.webassembly.js:1    at MemoryPack.MemoryPackFormatterProvider.TryInvokeRegisterFormatter(Type type)
blazor.webassembly.js:1    at MemoryPack.MemoryPackFormatterProvider.Cache`1[[BlazorAppMemoryPackBug.IBob, BlazorAppMemoryPackBug, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]..cctor()
blazor.webassembly.js:1    --- End of inner exception stack trace ---
blazor.webassembly.js:1    at MemoryPack.MemoryPackSerializationException.ThrowRegisterInProviderFailed(Type type, Exception innerException)
blazor.webassembly.js:1    at MemoryPack.ErrorMemoryPackFormatter`1[[BlazorAppMemoryPackBug.IBob, BlazorAppMemoryPackBug, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]].Throw()
blazor.webassembly.js:1    at MemoryPack.ErrorMemoryPackFormatter`1[[BlazorAppMemoryPackBug.IBob, BlazorAppMemoryPackBug, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]].Serialize[ReusableLinkedArrayBufferWriter](MemoryPackWriter`1& writer, IBob& value)
blazor.webassembly.js:1    at MemoryPack.MemoryPackSerializer.Serialize[IBob](IBob& value, MemoryPackSerializerOptions options)
blazor.webassembly.js:1    at BlazorAppMemoryPackBug.Program.Main(String[] args) in \source\repos\BlazorAppMemoryPackBug\Program.cs:line 30
SerratedSharp commented 1 month ago

I was able to workaround this by registering programmatically with MemoryPackFormatterProvider.Register(

@carmichaelalonso Does this approach work in your case?

[MemoryPackable]
public partial class Bob: IBob
{
    public string Name { get; set; }
}

[MemoryPackable]
[MemoryPackUnion(0, typeof(Bob))]
public partial interface IBob
{
    string Name { get; set; }
}

public class Program
{
    public static async Task Main(string[] args)
    {
        var formatter = new DynamicUnionFormatter<IBob>(
            (0, typeof(Bob))
        );
        MemoryPackFormatterProvider.Register(formatter);

        var bob = new Bob() { Name = "Bob"};
        var ibob = (IBob)bob;
        MemoryPackSerializer.Serialize(ibob);
SequPL commented 1 hour ago

Hi, same for me (Android device), but @SerratedSharp 's workaround works