Repository navigation
Description
Description
A NativeAOT application using a source-generated PolyType shape fails at runtime because the compiler does not retain a reachable generic virtual interface method implementation. The concrete implementation is created by a statically visible factory; there is no reflection or dynamic loading involved.
This was discovered while adding NativeAOT test execution to AArnott/Nerdbank.JsonRpc. It reduces to one POCO and one direct call to the published Nerdbank.Json package.
Reproduction steps
Create Repro.csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<OutputType>Exe</OutputType>
<PublishAot>true</PublishAot>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Nerdbank.Json" Version="0.1.1041-alpha" />
<PackageReference Include="PolyType" Version="1.4.1" />
</ItemGroup>
</Project>Create Program.cs:
using Nerdbank.Json;
using PolyType;
Poco value = new() { Number = 42 };
Console.WriteLine(new JsonSerializer().Serialize(value, GetShape<Poco, Poco>()));
static ITypeShape<T> GetShape<T, TProvider>()
where TProvider : IShapeable<T>
=> TProvider.GetTypeShape();
[GenerateShape]
internal partial class Poco
{
public int Number { get; init; }
}Then run:
dotnet publish -c Release -r win-x64
.\bin\Release\net10.0\win-x64\publish\Repro.exeExpected behavior
The executable prints serialized JSON for Poco.
Actual behavior
Publishing succeeds without trimming/AOT warnings. Running the executable fail-fasts:
Process terminated. Failed to create generic virtual method implementation
Declaring type: Nerdbank.Json.JsonStandardVisitor
Method name: PolyType.Abstractions.ITypeShapeFunc.Invoke
Instantiation:
Argument 00000000: Poco
at System.RuntimeExceptionHelpers.FailFast(...)
at Internal.Runtime.TypeLoader.TypeLoaderEnvironment.ResolveGenericVirtualMethodTarget(...)
at System.Runtime.TypeLoaderExports.CacheMiss(...)
at System.Runtime.TypeLoaderExports.GVMLookupForSlotSlow(...)
at PolyType.Utilities.TypeGenerationContext.GetOrAdd(ITypeShape, Object)
at PolyType.Utilities.TypeCache.AddValue(ITypeShape)
at PolyType.Utilities.TypeCache.GetOrAdd(ITypeShape)
at Nerdbank.Json.ConverterCache.GetOrAddConverter[T](ITypeShape`1)
at Nerdbank.Json.JsonSerializer.Serialize[T](...)
at Program.<Main>$(String[] args)
Reduction and A/B results
The relevant reachable path is:
// Nerdbank.Json configures PolyType's cache with a concrete factory:
ValueBuilderFactory = ctx => new JsonStandardVisitor(this, ctx);
// PolyType later dispatches through the erased shape:
value = typeShape.Invoke(ValueBuilder, state);
// Generated/source shape dispatch reaches this interface GVM:
object? ITypeShapeFunc.Invoke<T>(ITypeShape<T> typeShape, object? state)
=> owner.CreateConverter(typeShape, this);Observed A/B behavior:
- Adding
<TrimmerRootAssembly Include="Nerdbank.Json" />makes the executable pass, but broadly rooting a shipping library is not a viable consumer requirement. - Replacing the
Invoke<T>implementation with the direct expression below gets past the missing-GVM failure and enters normal converter traversal:
object? ITypeShapeFunc.Invoke(ITypeShape typeShape, object? state)
=> typeShape.Accept(this, state);
- Keeping the complete converter-selection logic in
Invoke<T>still fails. A minimal directAcceptbody succeeds, while the shipping implementation's delegation to a generic helper fails. - Moving the visitor to a nested type, making the implementation public, eagerly constructing the cache, and using a static cache factory do not fix it.
- Publishing Nerdbank.Json itself as net10.0 does not fix it.
- PolyType's own
JsonSerializerTSexample uses the sameMultiProviderTypeCache/ITypeShapeFunc.Invoke<T>pattern and passes NativeAOT. Its implementation directly callstypeShape.Accept(this), matching the successful side of the A/B test.
This looks like an ILC GVM reachability/code-generation issue rather than reflection usage or a missing user type shape. It is distinct from #113664, which fixed dynamic static generic virtual dispatch and is already present in .NET 10.
Configuration
Reproduced on Windows x64 with:
- .NET SDK 10.0.401 / Microsoft.DotNet.ILCompiler 10.0.8
- .NET SDK 11.0.100-preview.5.26302.115 / matching .NET 11 ILCompiler
- Nerdbank.Json 0.1.1041-alpha
- PolyType 1.4.1
The original test suite reproduces on Windows x64, Linux x64, and macOS arm64 under .NET 10 NativeAOT. Its IL executions pass on all three platforms.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsNo status
Activity
dotnet-policy-service commentedon Sep 26, 2026
Tagging subscribers to this area: @agocke, @dotnet/ilc-contrib
See info in area-owners.md if you want to be subscribed.
AArnott commentedon Sep 26, 2026
The
0.1.1050-alphaworkaround removes the first failure atJsonStandardVisitor.ITypeShapeFunc.Invoke<T>, but it only moves the same NativeAOT GVM reachability problem to nested shape traversal.A standalone unrooted NativeAOT app with no test framework now reproduces this by serializing one generated object containing another generated object:
The published net10.0 executable fail-fasts with:
The top-level direct
typeShape.Accept(this, state)workaround is active, soInvoke<Payload>succeeds andVisitObject<Payload>begins. It fails whenIPropertyShape.Accept(TypeShapeVisitor, object)dispatches toVisitProperty<Payload, NestedPayload>.I also verified that none of these retain the required GVM implementation:
DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(JsonStandardVisitor))Typeflow annotated withDynamicallyAccessedMembers(All)SourceGenPropertyShape<InternalRoot, object>passed toAcceptJsonStandardVisitornon-sealedTrimmerRootAssemblystill masks the failure, but is not a viable consumer requirement. This demonstrates the bug applies to the generic visitor overrides as well asITypeShapeFunc.Invoke<T>and cannot be comprehensively worked around merely by direct-dispatching the top-level shape.MichalStrehovsky commentedon Sep 28, 2026
The top-post issue is due to
[B8780EBD] ERROR: Cannot find a suitable template for type Nerdbank.Json.RuntimeUnionCase``1[Poco]. So the workaround would be to trick the compiler into thinking a type loader template for this type is needed. For example withActivator.CreateInstance(typeof(RuntimeUnionCase<>).MakeGenericType(someReferenceType).Not sure if the second one is the same bug, the snippet doesn't compile.
Ensure type loader templates for interface dispatch cells (#134774)
AArnott commentedon Sep 28, 2026
Verified that the second, nested-property repro is fixed by #134774 as well.
I built NativeAOT compiler and runtime support libraries from the PR commit
2c83245e193a39ee0bd07bb99732abf7fcbdeba6, then published this complete standalone repro (the helper omitted from my earlier abbreviated comment is included here):A/B result:
JsonStandardVisitor.VisitProperty<Payload, NestedPayload>(exit0xC0000409).aotsdkand framework libraries built from2c83245e193: serialization succeeded and the native executable exited0.I used the PR's bootstrapped
11.0.100-rc.1.26420.103SDK and overrodeIlcToolsPath,IlcSdkPath,IlcFrameworkPath, andIlcFrameworkNativePathwith outputs from that exact source build. So the second repro exercises the fixed compiler/runtime artifacts rather than an older public SDK.AArnott commentedon Sep 28, 2026
@MichalStrehovsky Thanks for fixing this so quickly.
What are the odds we can backport this fix to .NET 10? To be clear, my repro isn't from a Microsoft work-related library (at least not yet). So this isn't a 'partner ask'. But I'm interested in my repro library targeting .NET 10 rather than requiring .NET 11 if possible.
MichalStrehovsky commentedon Sep 28, 2026
@agocke would this make the bar for .NET 10? (And then we would need .NET 11 too I guess since it's currently only fixed in .NET 12.)
This bug has existed since .NET 7.0 (and likely .NET Native for UWP apps too, but I didn't bother to check if it reproes there since it would be ARM64 only.).
agocke commentedon Sep 28, 2026
Definitely approved for 11, also valid for 10.0 -- it's an incorrect compilation, and hard to work around.
AArnott commentedon Sep 30, 2026
There's a lot of interest in the library I'm developing that is blocked by this bug, so the sooner we can backport to 11 and 10, the better. :)
agocke commentedon Sep 30, 2026
Note that this code uses generic interface methods so it is going to scale very poorly. I had to remove similar code from LINQ because it was adding MBs of overhead, slowing down compilation, and hurting throughout. I would not recommend using this approach at scale.
AArnott commentedon Oct 17, 2026
Can we get this backported to .NET 11 and 10 then?
agocke commentedon Oct 18, 2026
It already was. #134820
AArnott commentedon Oct 18, 2026
Marvelous! I (now) see #134821 for .NET 10 and #134820 for .NET 11.
Curious that although each of those PRs have a link back to this issue, this issue's history doesn't have the typical link back to them. If it had, I wouldn't have posted asking when the backporting would happen. :)
jkotas commentedon Oct 18, 2026
The PRs have a link back to the original PR in main only (#134774). They do not have a link back to this issue.
AArnott commentedon Oct 19, 2026
Oh. Maybe they should. :) It would make it more discoverable.