microsoft / vs-streamjsonrpc

The StreamJsonRpc library offers JSON-RPC 2.0 over any .NET Stream, WebSocket, or Pipe. With bonus support for request cancellation, client proxy generation, and more.
Other
750 stars 149 forks source link

Error getting value from 'RequestId' on net6.0-ios app #840

Open mmoraga opened 2 years ago

mmoraga commented 2 years ago
Exception thrown while transmitting message: Newtonsoft.Json.JsonSerializationException: Error writing JSON RPC Message: JsonSerializationException: Error getting value from 'RequestId' on 'StreamJsonRpc.JsonMessageFormatter+JsonRpcError'.
 ---> Newtonsoft.Json.JsonSerializationException: Error getting value from 'RequestId' on 'StreamJsonRpc.JsonMessageFormatter+JsonRpcError'.
 ---> System.ExecutionEngineException: Attempting to JIT compile method '(wrapper delegate-invoke) StreamJsonRpc.RequestId <Module>:invoke_callvirt_RequestId_JsonRpcError (StreamJsonRpc.Protocol.JsonRpcError)' while running in aot-only mode. See https://docs.microsoft.com/xamarin/ios/internals/limitations for more information.

This only happens on AOT compiled Release builds of net6.0-ios. Debug builds don't have AOT compilation enabled, so I suspect this might have something to do with trimming/aot and reflection.

AArnott commented 2 years ago

Thanks for reporting. We don't currently test StreamJsonRpc in AOT environments, so this may be one of many such issues. I have very little experience with AOT runtimes in general, so I'm not great at parsing these error messages. It appears that it's failing to retrieve the value of a simple property, so there's nothing particularly special like dynamic code compilation happening here. My guess is that the code that fails isn't statically invoked anywhere in the assembly so it didn't compile the property getter. Then when serializing, reflection led to invoking that property getter, leading to failure. It seems the solution would be to avoid trimming, or at least avoid trimming this assembly. Is that something that can be done?

mmoraga commented 2 years ago

I tried adding <MtouchLink>SdkOnly</MtouchLink> to only link SDK assemblies but the issue is still there. I'll see if I can get some help from the Xamarin guys in disabling linker trimming for StreamJsonRpc.

mmoraga commented 2 years ago

Unfortunately disabling linking altogether brought in a ton of other issues and is not really an option. Is there anything else I could try or do to workaround the issue? Otherwise I'd be happy to help in finding a fix.

AArnott commented 2 years ago

I'm not sure that linking is the problem as much as the AOT compiler. IIRC there's an xml file somewhere where you can list APIs to force them to compile for cases like this. But I have no idea where that file might be. I suppose the general workaround is call the API yourself so that the AOT compiler sees it's in use. But you'd have to call it from code that actually executes as well (or at least fools the compiler into believing it will execute).

If there's an attribute we can add to such APIs, we'd be willing to take a PR. But we'd want a test submitted as well that demonstrates AOT and that it all works, so that we can keep it working across versions.

mmoraga commented 2 years ago

I tried adding a separate linker.xml file as seen here: https://docs.microsoft.com/en-us/xamarin/cross-platform/deploy-test/linker

with the following configuration:

<linker>
    <assembly fullname="StreamJsonRpc" preserve="all" />
</linker>

checking msbuild binlog I see it is being used:

/usr/local/share/dotnet/dotnet "/usr/local/share/dotnet/sdk/6.0.400/Sdks/Microsoft.NET.ILLink.Tasks/tools/net6.0/illink.dll" -x "Linker.xml"

But the resulting app still ends up throwing when getting the value of 'RequestId'. Perhaps this is more of a Xamarin issue.

AArnott commented 2 years ago

It was a good try. But at this point ya, I think reaching out to Xamarin for help may be more profitable for you as we just don't have the expertise over here. But again, if you learn anything interesting, please share. Especially if there's something StreamJsonRpc can do to Just Work by default in this environment, I'd like to learn what that is, and we may be able to make those changes.

mmoraga commented 2 years ago

I created an issue in their issue tracker https://github.com/xamarin/xamarin-macios/issues/15961 it includes a small test repo where I managed to reproduce it.

brettfo commented 1 year ago

If it helps anybody, I was able to get this library working in .NET 7 AOT through some trial and error, so something similar may be used for your project.

In my scenario I added a rd.xml to my project:

...
  <ItemGroup>
    <RdXmlFile Include="rd.xml" />
  </ItemGroup>
...

And the contents of my rd.xml had to be tweaked over time to this:

<Directives xmlns="http://schemas.microsoft.com/netfx/2013/01/metadata">
  <Application>
    <!-- I'm sure this could be reduced to specific types, but I didn't have the time to explore this further -->
    <Assembly Name="StreamJsonRpc" Dynamic="Required All" />

    <!-- Include everything from my assembly that contains types going over the wire.  Again, this could be reduced. -->
    <Assembly Name="MyJsonRpcAssembly" Dynamic="Required All" />

    <Assembly Name="mscorlib">
      <!-- If any of my serialized types had array properties, e.g.,

      class SomeType
      {
          public SomeProperty[] PropertyArray { get; set; }
      }

      then I had to add the following line.
      Note that I had to assembly-qualify the type with the double brackets and comma.
      Repeat as necessary. -->
      <Type Name="System.Collections.Generic.List`1[[SomeProperty,MyJsonRpcAssembly]]" Dynamic="Required All" />

      <!-- Finally, if any of my RPC methods were async and returned `Task<Something>`, I had to add each of those,
      like shown below.  If any of the Task result types were from your assembly, you'll need to do the double bracket
      and comma resolution mentioned above. -->
      <Type Name="System.Threading.Tasks.Task`1[System.Boolean]" Dynamic="Required All" />
      <Type Name="System.Threading.Tasks.Task`1[[SomeType,MyJsonRpcAssembly]]" Dynamic="Required All" />
    </Assembly>
  <Application>
</Directives>

To discover this, I added a trace listener that logged all messages to a text file which I then examined after-the-fact. Rinse and repeat.