Skip to content

crossgen2: NotImplementedException in ModuleTokenResolver.GetModuleTokenForType for type map types named only in a custom attribute blob #131527

Description

@rolfbjarne

Description

crossgen2 throws NotImplementedException when it ReadyToRun-compiles an assembly that carries a type map (System.Runtime.InteropServices.TypeMapAssociationAttribute<TGroup> / TypeMapAttribute<TGroup>) whose mapped types are named only in the custom attribute blob — i.e. the assembly has no TypeRef row for them.

That metadata shape is valid (see the ECMA-335 analysis below), and it is what you get from any type map producer that doesn't go through Roslyn. It's how dotnet/macios generates its type map assemblies: its trimmable-static registrar emits _<platform>.TypeMap.dll with Mono.Cecil from a custom ILLink step, and Cecil (correctly) does not emit a TypeRef row for a System.Type custom attribute argument.

Reproduction Steps

Minimal shape — an assembly whose entire relevant content is:

[assembly: TypeMapAssociation<ObjCRuntime.ProtocolProxyAttribute> (
    typeof (UserNotificationsUI.IUNNotificationContentExtension),         // defined in a -r: reference
    typeof (UserNotificationsUI.IUNNotificationContentExtension_Proxy))]  // defined in a -r: reference

...where the assembly's metadata has no TypeRef row for either type (they appear only as assembly-qualified name strings in the #Blob heap), and neither type is used from IL anywhere in the compilation inputs.

Compiling that assembly stand-alone reproduces it deterministically:

crossgen2 --targetos:osx --targetarch:arm64 -O --obj-format:pe --instruction-set:apple-m1 \
    -r:Microsoft.macOS.dll -r:System.Private.CoreLib.dll \
    --out:out/_Microsoft.macOS.TypeMap.dll _Microsoft.macOS.TypeMap.dll

I have two reduced repros:

  1. Dependency-free (13 MB zip, crossgen2-typemap-bug.zip): a bundled crossgen2 plus 3 assemblies and a run.sh. No .NET SDK and no dotnet/macios tooling needed. The input assembly is ~2 KB with a single type map entry; the reference assembly is ~2 KB with 3 empty interface types.
  2. Source-based (11 KB zip, crossgen2-typemap-bug-csharp.zip): five C# projects (thelib, satellite, typemaps, app, rewriter) where a plain dotnet publish reproduces it.

Note for (2): a pure-Roslyn repro is impossible. typeof (X) in an attribute argument always makes Roslyn emit a TypeRef row for X, and crossgen2 then resolves the type through ReadyToRunCompilationModuleGroupBase.TryGetModuleTokenForExternalType, hiding the bug. The repro therefore has a small Mono.Cecil rewriter step that rebuilds the satellite assembly with the same identity and the same attribute, but without the TypeRef rows — reproducing what a non-Roslyn producer emits.

Expected behavior

crossgen2 emits the R2R image successfully (or, if it genuinely cannot embed the map, falls back to deferring the map to the runtime the way the ThrowingMethodStub path already does).

Actual behavior

Emitting R2R PE file: ./out/_Microsoft.macOS.TypeMap.dll
Error: [Microsoft.macOS]UserNotificationsUI.IUNNotificationContentExtension
System.NotImplementedException: [Microsoft.macOS]UserNotificationsUI.IUNNotificationContentExtension
   at ILCompiler.DependencyAnalysis.ReadyToRun.ModuleTokenResolver.GetModuleTokenForType(TypeDesc, Boolean, Boolean) in .../ModuleTokenResolver.cs:line 92
   at ILCompiler.DependencyAnalysis.ReadyToRun.SignatureContext.GetTargetModule(TypeDesc) in .../SignatureContext.cs:line 64
   at ILCompiler.DependencyAnalysis.ReadyToRun.TypeFixupSignature.GetData(NodeFactory, Boolean) in .../TypeFixupSignature.cs:line 49
   at ILCompiler.DependencyAnalysis.ReadyToRun.ImportSectionNode.MaterializeSignature(NodeFactory) in .../ImportSectionNode.cs:line 67
   at ILCompiler.DependencyAnalysis.ReadyToRun.ImportSectionsTableNode.MaterializeSignature() in .../ImportSectionsTableNode.cs:line 25
   at ILCompiler.DependencyAnalysis.ReadyToRun.ManifestMetadataTableNode.ComputeLastSetOfModuleIndices() in .../ManifestMetadataTableNode.cs:line 277
   at ILCompiler.DependencyAnalysis.ReadyToRun.ManifestMetadataTableNode.GetManifestAssemblyMvidTableData() in .../ManifestMetadataTableNode.cs:line 326
   at ILCompiler.DependencyAnalysis.ReadyToRun.ManifestAssemblyMvidHeaderNode.GetData(NodeFactory, Boolean) in .../ManifestAssemblyMvidHeaderNode.cs:line 58
   at ILCompiler.ObjectWriter.ObjectWriter.EmitObject(Stream, IReadOnlyCollection`1, IObjectDumper, Logger) in .../ObjectWriter.cs:line 391
   at ILCompiler.DependencyAnalysis.ReadyToRunObjectWriter.EmitReadyToRunObjects(ReadyToRunContainerFormat, Logger) in .../ReadyToRunObjectWriter.cs:line 193
   at ILCompiler.ReadyToRunCodegenCompilation.Compile(String) in .../ReadyToRunCodegenCompilation.cs:line 431

Analysis

ReadyToRunProxyTypeMapNode.GetStaticDependencies / ReadyToRunExternalTypeMapNode.GetStaticDependencies request an import for every key and value type in the map, which ends up in ModuleTokenResolver.GetModuleTokenForType. That method has four resolution paths, and a type named only by a type map fails all of them:

# Path Why it fails
1 VersionsWithType The type lives in a -r: reference, not in the assembly being compiled.
2 _typeToRefTokens Only populated while decoding IL/signature metadata; a blob-only type name never goes through it.
3 TryGetModuleTokenForExternalType Scans the TypeRef tables of the compilation inputs — and there is no TypeRef row for the type.
4 _manifestMutableModule.TryGetExistingEntityHandle Only checks for an existing handle, it doesn't create one, and nothing created one earlier.

...so it falls through to throw new NotImplementedException (type.ToString ()).

The input assembly is valid — the missing TypeRef row is not a producer bug

I checked this carefully, because the obvious reaction is "the producer should have emitted a TypeRef":

  • ECMA-335 §II.23.3 ("Custom attributes"): for a parameter of kind System.Type, "its value is stored as a SerString … representing its canonical name. The canonical name is its full type name, followed optionally by the assembly where it is defined, its version, culture and public-key-token." That is the only structural requirement stated; the section never mentions the TypeRef table.
  • §II.22.10 (CustomAttribute validity rules) constrains the blob (Prolog 0x0001, argument counts, Elem encoding: "If this is a string or a Type, then Elem consists of a SerString"). None of the rules require a TypeRef row for a type named in the blob.
  • §II.22.38 (TypeRef validity rules) only constrains TypeRef rows that already exist; it creates no obligation to emit one.
  • docs/design/specs/Ecma-335-Augments.md is silent on the topic.

The runtime agrees in practice — these types are resolved by parsing the blob string, not by consulting the TypeRef table. crossgen2's own TypeMapMetadata.ProcessTypeMapAssociationAttribute takes its TypeDescs from CustomAttributeValue<TypeDesc>.FixedArguments (i.e. from the SerString) and only later assumes a module token is findable. And the type map design document makes exactly this point about the sibling attribute:

An assembly name mentioned in the TypeMapAssemblyTargetAttribute does not need to map to an AssemblyRef row in the module's metadata. As long as a given name can be resolved by the runtime or by whatever trimming tool is run on the application, it can be used.

— docs/design/features/typemap.md

So Roslyn emitting a TypeRef for typeof (X) in an attribute argument is an implementation choice, not an invariant crossgen2 can rely on.

Suggested fix

Either:

  • have the type map nodes create a manifest-mutable-module handle (TryGetEntityHandle) for each type they pull out of the attribute blobs, so path (4) succeeds; or
  • let GetModuleTokenForType create a manifest reference rather than throwing; or
  • fall back to the existing "invalid type map state / defer the map to the runtime" path that ReadyToRunExternalTypeMapNode already implements for the ThrowingMethodStub case.

Regardless of which is chosen, NotImplementedException is a poor outcome for valid input.

Configuration

  • crossgen2 11.0.0-preview.7.26365.101+cb8306a63c5cf24e9381108a3a9eb58907fd0f60, osx-arm64, targeting osx-arm64.
  • Also reproduces via the SDK with PublishReadyToRun=true + PublishReadyToRunComposite=false.
  • Not specific to macOS as far as I can tell — the failing code is target-independent.

Other information

Workaround (what dotnet/macios does for now): emit an unused method into the generated type map assembly that does ldtoken + pop for every externally defined type the map names. That forces the TypeRef rows to exist, so path (3) succeeds. ldtoken rather than a field or parameter, because it also works for open generic types. The method is never called, so ILLink trims it away again.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

  • Status
    No status

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions