Skip to content

[TrimmableTypeMap] Add JavaPeerScanner using System.Reflection.Metadata#10805

Closed
simonrozsival wants to merge 6 commits intomainfrom
dev/simonrozsival/trimmable-typemap-02-scanner
Closed

[TrimmableTypeMap] Add JavaPeerScanner using System.Reflection.Metadata#10805
simonrozsival wants to merge 6 commits intomainfrom
dev/simonrozsival/trimmable-typemap-02-scanner

Conversation

@simonrozsival
Copy link
Member

@simonrozsival simonrozsival commented Feb 12, 2026

Part of #10789
Closes #10798

Summary

This PR adds the Microsoft.Android.Sdk.TrimmableTypeMap project with the JavaPeerScanner — a System.Reflection.Metadata-based assembly scanner that identifies Java peer types, their JNI signatures, and type relationships needed for the trimmable typemap.

What's included

Scanner (JavaPeerScanner)

  • Scans assemblies for types with [Register] attributes (Java peers)
  • Resolves JNI type names and method signatures using SRM
  • Computes CRC64-based JNI names for user types without explicit [Register]
  • Computes CompatJniName (lowercased namespace) for acw-map.txt backward compatibility
  • Detects base class relationships (ExtendsJavaPeer with cycle detection + caching)
  • Resolves base Java type names and implemented Java interfaces (including generic instantiations)
  • Extracts [Application] attribute properties (BackupAgent, ManageSpaceActivity) for unconditional forcing
  • Detects component attributes ([Activity], [Service], etc.) for unconditional marking
  • Supports IJniNameProviderAttribute for custom JNI name resolution
  • Collects marshal methods ([Register] + [Export]) and Java constructors in a single pass
  • Resolves activation constructors (XI and JI styles) with hierarchy walking

Supporting types

  • AssemblyIndex — Phase 1 index builder with shared static helpers for cross-assembly type resolution
  • CustomAttributeTypeProvider — Decodes custom attribute blobs (including enums and nested types)
  • SignatureTypeProvider — Resolves method signatures from metadata (singleton, handles nested types)
  • JavaPeerInfo — Data model for scanner results (all fields needed by downstream JCW/typemap generators)

Edge cases found and handled via side-by-side testing

Testing against Mono.Android.dll (~8,000 types) gives false confidence — all MCW binding types have explicit [Register] attributes, so the hard logic paths are never exercised. A UserTypesFixture.dll with real user-type patterns was essential:

  • Nested type JNI name inheritance: OuterClass+InnerHelper where OuterClass has [Register("com/example/OuterClass")] must produce com/example/OuterClass_InnerHelper, not an independent CRC64 name. The scanner walks up declaring types and uses the parent's registered JNI name as prefix, matching JavaNativeTypeManager.ToJniName behavior.

  • Component-attributed base types: A DerivedActivity extending a BaseActivity that has [Activity] but no [Register] must still be recognized as a Java peer. ExtendsJavaPeerCore checks both RegisterInfoByType and AttributesByType.

  • Generic interface implementations: IList<T> is a TypeSpecification in metadata, not a TypeReference. The scanner reads the raw signature blob (GENERICINST → CLASS/VALUETYPE → coded token) to resolve the underlying type definition.

  • Nested TypeReference resolution scope: A nested type defined in another assembly has a ResolutionScope that points to another TypeReference (its declaring type), not directly to an AssemblyReference. The shared ResolveTypeReference helper recurses up the scope chain.

  • Assembly-qualified type names: [Application(BackupAgent=typeof(X))] serializes as "Ns.Type, Assembly, Version=..." — must strip the assembly qualification before looking up in resultsByManagedName.

  • [Export] methods: [Export("doWork")] generates Java callable wrappers via a different mechanism than [Register]. The scanner handles both via ParseExportAttribute, including ThrownNames and SuperArgumentsString.

CRC64 polynomial

The scanner uses System.IO.Hashing.Crc64 (ECMA polynomial) instead of the legacy Crc64Helper (Jones polynomial). This is intentional — the CRC64 hash only needs to be self-consistent within a single compilation. The scanner computes crc64{hash}/TypeName, the JCW generator creates a .java file with that name, and the typemap records it. All consumers read from the same scan pass.

Tests

  • 75 unit tests covering scanner logic, JNI name computation, parsing, edge cases
  • 10 integration tests — side-by-side comparison with legacy Cecil-based scanner on both Mono.Android.dll and UserTypesFixture.dll

Follow-up

@simonrozsival simonrozsival changed the title [Trimmable TypeMap PR2] Add JavaPeerScanner using System.Reflection.Metadata [TrimmableTypeMap] Add JavaPeerScanner using System.Reflection.Metadata Feb 12, 2026
@simonrozsival simonrozsival force-pushed the dev/simonrozsival/trimmable-typemap-02-scanner branch 10 times, most recently from b5fa14b to 991a2a6 Compare February 12, 2026 14:46
@simonrozsival simonrozsival force-pushed the dev/simonrozsival/trimmable-typemap-02-scanner branch 7 times, most recently from 96c7f2b to 409a83b Compare February 12, 2026 15:57
Base automatically changed from android-trimmabletypemap-01-build-targets-refactoring to main February 12, 2026 20:37
@simonrozsival simonrozsival force-pushed the dev/simonrozsival/trimmable-typemap-02-scanner branch from 409a83b to 8fbb99a Compare February 12, 2026 21:51
@simonrozsival simonrozsival marked this pull request as ready for review February 12, 2026 21:56
Copilot AI review requested due to automatic review settings February 12, 2026 21:56
@simonrozsival simonrozsival added Area: NativeAOT Issues that only occur when using NativeAOT. Area: CoreCLR Issues that only occur when using CoreCLR. trimmable-type-map labels Feb 12, 2026
@simonrozsival

This comment was marked as duplicate.

@simonrozsival

This comment was marked as duplicate.

@simonrozsival

This comment was marked as outdated.

@simonrozsival
Copy link
Member Author

simonrozsival commented Feb 14, 2026

This PR has been split into 3 stacked PRs to simplify the review:

  1. [TrimmableTypeMap] Java peer scanner core #10823

    • Scanner project, data model, metadata providers, AssemblyIndex, JavaPeerScanner logic.
  2. [TrimmableTypeMap] Scanner unit tests #10813

    • Test fixtures, foundational tests, behavior/contract tests, edge-case regressions, CI wiring.
  3. [TrimmableTypeMap] Integration parity tests #10827

    • Side-by-side comparison tests against the existing scanner, CI wiring.

@simonrozsival simonrozsival added the copilot `copilot-cli` or other AIs were used to author this label Feb 14, 2026
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 2 comments.

Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Android.Sdk.TrimmableTypeMap", "src\Microsoft.Android.Sdk.TrimmableTypeMap\Microsoft.Android.Sdk.TrimmableTypeMap.csproj", "{507759AE-93DF-411B-8645-31F680319F5C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests", "tests\Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests\Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests.csproj", "{A14CB0A1-7A05-4F27-88B2-383798CE1DEE}"
EndProject
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Microsoft.Android.Sdk.TrimmableTypeMap.Tests project is missing from the solution file. While the CI pipeline explicitly references the test project and runs the tests, developers won't be able to build or run these tests from Visual Studio or other IDEs that use the solution file. This creates an inconsistent developer experience.

Add a project entry for Microsoft.Android.Sdk.TrimmableTypeMap.Tests similar to the IntegrationTests project already included here.

Suggested change
EndProject
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Android.Sdk.TrimmableTypeMap.Tests", "tests\Microsoft.Android.Sdk.TrimmableTypeMap.Tests\Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj", "{B6D8D9E3-4C8B-4B3E-9F37-6A4A5F0D8E21}"
EndProject

Copilot uses AI. Check for mistakes.
Comment on lines 60 to 65
var peReader = new PEReader (File.OpenRead (filePath));
var reader = peReader.GetMetadataReader ();
var assemblyName = reader.GetString (reader.GetAssemblyDefinition ().Name);
var index = new AssemblyIndex (peReader, reader, assemblyName, filePath);
index.Build ();
return index;
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential resource leak: File.OpenRead returns a FileStream that's passed to PEReader. If an exception occurs in Build() before completion, the FileStream may not be properly disposed. PEReader will dispose the stream when it's disposed, but only if the PEReader construction succeeds.

Consider wrapping the Create method body in a try-catch to ensure proper cleanup, or use a using statement for the FileStream before passing it to PEReader.

Suggested change
var peReader = new PEReader (File.OpenRead (filePath));
var reader = peReader.GetMetadataReader ();
var assemblyName = reader.GetString (reader.GetAssemblyDefinition ().Name);
var index = new AssemblyIndex (peReader, reader, assemblyName, filePath);
index.Build ();
return index;
FileStream? stream = null;
PEReader? peReader = null;
try {
stream = File.OpenRead (filePath);
peReader = new PEReader (stream);
var reader = peReader.GetMetadataReader ();
var assemblyName = reader.GetString (reader.GetAssemblyDefinition ().Name);
var index = new AssemblyIndex (peReader, reader, assemblyName, filePath);
index.Build ();
return index;
} catch {
if (peReader != null) {
peReader.Dispose ();
} else if (stream != null) {
stream.Dispose ();
}
throw;
}

Copilot uses AI. Check for mistakes.
@simonrozsival simonrozsival force-pushed the dev/simonrozsival/trimmable-typemap-02-scanner branch 8 times, most recently from 2388cf1 to e92938e Compare February 16, 2026 21:04
jonathanpeppers pushed a commit that referenced this pull request Feb 18, 2026
Sliced from #10805. Adds the core scanner pipeline for the TrimmableTypeMap feature — the component that reads .NET assemblies and discovers all Java peer types using `System.Reflection.Metadata`.

## What this PR adds

New `Microsoft.Android.Sdk.TrimmableTypeMap` project (`netstandard2.0`, ~1500 lines) with three logical layers:

### 1. Data model and metadata type providers
> `Scanner/JavaPeerInfo.cs`, `Scanner/SignatureTypeProvider.cs`, `Scanner/CustomAttributeTypeProvider.cs`

- **`JavaPeerInfo`** record — represents a discovered Java peer with its JNI name, marshal methods, activation constructor info, and component attributes
- **`SignatureTypeProvider`** — decodes method signatures to extract parameter types (needed for marshal method and activation constructor matching)
- **`CustomAttributeTypeProvider`** — decodes `[Register]`, `[Export]`, `[Activity]` etc. attribute blobs; includes a lazy enum type cache and correct nested type resolution

### 2. AssemblyIndex — per-assembly metadata indexer
> `Scanner/AssemblyIndex.cs`

First phase of the pipeline. Reads a single assembly and builds an index of:
- `[Register]`/`[Export]` attributes on types and methods → `RegisterInfo`/`ExportInfo` records
- Component attributes (`[Activity]`, `[Service]`, `[BroadcastReceiver]`, `[ContentProvider]`) with their JNI name properties
- Type definition → Java peer registration mapping

Designed for per-assembly parallelism — each assembly gets its own index.

### 3. JavaPeerScanner — execution logic
> `Scanner/JavaPeerScanner.cs`

Second phase. Consumes all `AssemblyIndex` results and produces the final `JavaPeerInfo` list:
- Resolves inheritance chains across assemblies to find the nearest registered Java ancestor
- Detects activation constructors (`IntPtr` + `JniHandleOwnership`)
- Collects marshal methods and exported methods with JNI signatures
- Merges component attribute metadata and resolves JNI names
- Flags types for unconditional preservation when component attributes specify non-default JNI names

Pure function: assemblies in → peer info out, no side effects.

## Review guide

Three commits, one per layer — review in order:
1. **Data model and metadata type providers** — start here for the type system
2. **AssemblyIndex** — the per-assembly indexing phase
3. **JavaPeerScanner** — the cross-assembly resolution phase

Scanner unit tests follow in #10813.
Introduce Microsoft.Android.Build.TypeMap assembly with a two-phase
scanner that extracts Java peer type information from .NET assemblies
using SRM (no Mono.Cecil dependency).

Key components:
- JavaPeerScanner: scans assemblies for [Register], [Export], and
  component attributes to build JavaPeerInfo entries
- AssemblyIndex: per-assembly O(1) lookup index built in phase 1
- JavaPeerInfo/MarshalMethodInfo: rich data model for downstream
  generators (PR2/PR3)
- CRC64 package name computation via System.IO.Hashing

Tests:
- 62 xUnit tests covering all scanner code paths
- 3 integration tests comparing side-by-side with legacy Cecil-based
  scanner on Mono.Android.dll (all pass)
Rename project to match Microsoft.Android.Sdk.ILLink/Analysis naming.

Review fixes:
- Fix ParseJniType crash on malformed JNI signatures (+7 unit tests)
- Add cycle detection to ExtendsJavaPeer
- Fix SignatureTypeProvider nested type resolution via ResolutionScope
- Implement GetUnderlyingEnumType via value__ field inspection
- Implement TryGetTypeProperty for BackupAgent and ManageSpaceActivity
- Force BackupAgent/ManageSpaceActivity unconditional when referenced
  from [Application] attribute (+3 unit tests)
- Fix InternalsVisibleTo: remove AssemblyName hack, add proper entries
- Use Dictionary<string, JavaPeerInfo> for O(1) cross-reference lookups
- Minor cleanup: fix indentation, avoid FindAll allocation
@simonrozsival simonrozsival force-pushed the dev/simonrozsival/trimmable-typemap-02-scanner branch from e92938e to 9215de9 Compare February 18, 2026 15:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area: CoreCLR Issues that only occur when using CoreCLR. Area: NativeAOT Issues that only occur when using NativeAOT. copilot `copilot-cli` or other AIs were used to author this trimmable-type-map

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[TrimmableTypeMap] Java peer scanner with SRM + test project

2 participants