Skip to content

[TrimmableTypeMap] Java peer scanner core#10823

Merged
jonathanpeppers merged 7 commits intodotnet:mainfrom
simonrozsival:dev/simonrozsival/java-peer-scanner-core
Feb 18, 2026
Merged

[TrimmableTypeMap] Java peer scanner core#10823
jonathanpeppers merged 7 commits intodotnet:mainfrom
simonrozsival:dev/simonrozsival/java-peer-scanner-core

Conversation

@simonrozsival
Copy link
Copy Markdown
Member

@simonrozsival simonrozsival commented Feb 16, 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.

@simonrozsival simonrozsival added Area: NativeAOT Issues that only occur when using NativeAOT. Area: CoreCLR Issues that only occur when using CoreCLR. copilot `copilot-cli` or other AIs were used to author this trimmable-type-map labels Feb 16, 2026
@simonrozsival simonrozsival force-pushed the dev/simonrozsival/java-peer-scanner-core branch from b6895af to cf78bbe Compare February 16, 2026 15:31
@simonrozsival simonrozsival marked this pull request as ready for review February 16, 2026 15:32
Copilot AI review requested due to automatic review settings February 16, 2026 15:32
Copy link
Copy Markdown
Contributor

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

Adds the core System.Reflection.Metadata-based Java peer scanning pipeline for the new Microsoft.Android.Sdk.TrimmableTypeMap feature area. This introduces a new netstandard2.0 project responsible for indexing assemblies and producing a JavaPeerInfo model for downstream generators without using Cecil.

Changes:

  • Introduces the Microsoft.Android.Sdk.TrimmableTypeMap project and supporting polyfills/extensions for nullable + modern C# features on netstandard2.0.
  • Adds phase-1 AssemblyIndex (per-assembly metadata indexing) and phase-2 JavaPeerScanner (cross-assembly analysis) to discover Java peer types and marshal methods.
  • Adds minimal SRM type providers (SignatureTypeProvider, CustomAttributeTypeProvider) and the core data model (JavaPeerInfo, MarshalMethodInfo, activation ctor metadata).

Reviewed changes

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

Show a summary per file
File Description
src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/SignatureTypeProvider.cs SRM signature decoder producing managed type-name strings.
src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs Main scanner pipeline: builds indices, resolves peers, computes JNI names, collects marshal methods, resolves activation ctors.
src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs Data model for discovered peers, marshal methods, and activation constructor info.
src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/CustomAttributeTypeProvider.cs SRM custom-attribute decoder with enum underlying-type support and nested type handling.
src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs Per-assembly indexer for type names, [Register] info, and component attribute metadata.
src/Microsoft.Android.Sdk.TrimmableTypeMap/NullableExtensions.cs NRT-friendly IsNullOrEmpty/IsNullOrWhiteSpace helpers for netstandard2.0.
src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj New netstandard2.0 project wiring + package references + InternalsVisibleTo for tests.
src/Microsoft.Android.Sdk.TrimmableTypeMap/CompilerFeaturePolyfills.cs Polyfills for init/required-member compiler features on netstandard2.0.
eng/Versions.props Adds a pinned package version property for System.Reflection.Metadata.

@simonrozsival simonrozsival force-pushed the dev/simonrozsival/java-peer-scanner-core branch 4 times, most recently from 1520328 to 265b874 Compare February 16, 2026 20:54
simonrozsival and others added 3 commits February 16, 2026 22:03
Add the TrimmableTypeMap scanner project (netstandard2.0) with:

- JavaPeerInfo, MarshalMethodInfo, ActivationCtorInfo records — the
  core data model representing Java peer types discovered in assemblies
- SignatureTypeProvider — decodes method signatures from metadata to
  extract parameter types for marshal method and activation constructor
  matching
- CustomAttributeTypeProvider — decodes custom attribute arguments with
  lazy enum type caching and correct nested type resolution
- CompilerFeaturePolyfills — netstandard2.0 shims for required/init
- NullableExtensions — IsNullOrEmpty/IsNullOrWhiteSpace helpers
- System.Reflection.Metadata 11.0.0-preview.1 package reference

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add AssemblyIndex — the first phase of the scanner pipeline that reads
a single assembly and indexes all Java peer metadata:

- Discovers [Register], [Export] attributes on types and methods
- Builds RegisterInfo/ExportInfo records from custom attribute blobs
- Resolves TypeAttributeInfo for component attributes ([Activity],
  [Service], [BroadcastReceiver], [ContentProvider]) including their
  JNI name properties
- Maps type definitions to their Java peer registration data for
  downstream consumption by JavaPeerScanner

Key design: uses System.Reflection.Metadata directly (no Cecil) and
produces immutable record types. The index is per-assembly so scanning
can be parallelized across the app closure.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add JavaPeerScanner — the second phase that consumes AssemblyIndex
results and produces the final list of JavaPeerInfo for the app:

- Walks all indexed types and resolves their Java peer registrations
- Handles inheritance: traverses base types across assemblies to find
  the nearest registered Java peer ancestor
- Detects activation constructors (IntPtr+JniHandleOwnership) and
  distinguishes between direct declarations and inherited ones
- Collects marshal methods ([Register] on methods) and exported
  methods ([Export]) with their JNI signatures
- Merges component attribute metadata ([Activity], etc.) and resolves
  JNI names from attribute properties
- Flags types for unconditional preservation when component attributes
  with non-default JNI names are present

The scanner is designed as a pure function: assemblies in → peer info
out, with no side effects or global state.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival simonrozsival force-pushed the dev/simonrozsival/java-peer-scanner-core branch from 265b874 to e709c94 Compare February 16, 2026 21:04
Address unresolved review comments in scanner core:
- make AssemblyIndex.customAttributeTypeProvider private
- simplify component Name parsing via TryGetTypeProperty path
- merge duplicate named-argument helpers into a single generic
  TryGetNamedArgument<T>(..., out T) with notnull constraint
- remove boolean-specific helper and reuse generic method
- simplify enum cache assignment in CustomAttributeTypeProvider
- route JavaPeerScanner attribute decoding through AssemblyIndex helpers

Build verified for Microsoft.Android.Sdk.TrimmableTypeMap.csproj.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Apply PR10823-style simplifications across TrimmableTypeMap scanner code:
- centralize namespace/nested type name joins in MetadataTypeNameResolver
- reuse those helpers in JavaPeerScanner ResolveTypeReference
- simplify AssemblyIndex register parsing by removing provider overload
  and decoding through a single path
- reduce duplicated DecodeValue call sites via DecodeAttribute helper

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment on lines +114 to +121
static readonly HashSet<string> KnownComponentAttributes = new (StringComparer.Ordinal) {
"ActivityAttribute",
"ServiceAttribute",
"BroadcastReceiverAttribute",
"ContentProviderAttribute",
"ApplicationAttribute",
"InstrumentationAttribute",
};
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So, I believe there is an interface, Java.Interop.IJniNameProviderAttribute, that the old code was looking for? All these attributes implement it. I think the idea was if Android introduced a new attribute, it could work without updating this list?

There is probably not any customers using Java.Interop.IJniNameProviderAttribute, but do we need a code path that looks for it?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I already had this implemented but it complicates things quite a lot. It's not trivial to check if a type implements an interface with SRM. For now, I chose to ignore it. It shouldn't have practical implications, but I agree that we shold eventually implement it for completeness. I'll make sure to add this to the issue for future work. Is that OK?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

So efficient way to implement this:

  • only check custom attributes on Java.Lang.Object or Java.Lang.Throwable subclasses
  • ignore attributes which don't have the Name named argument
  • ignore well known attributes (Register, Application, Activity, ...)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I updated the "Future work" section of #10788

Move CompilerFeaturePolyfills.cs from the TrimmableTypeMap project folder to src-ThirdParty/System.Runtime.CompilerServices and link it from the csproj via a Compile item, matching the repository pattern for shared polyfills.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival simonrozsival enabled auto-merge (squash) February 17, 2026 20:27
@simonrozsival
Copy link
Copy Markdown
Member Author

/azp run Xamarin.Android-PR

@azure-pipelines
Copy link
Copy Markdown

No pipelines are associated with this pull request.

@simonrozsival
Copy link
Copy Markdown
Member Author

/azp run Xamarin.Android-PR

@azure-pipelines
Copy link
Copy Markdown

No pipelines are associated with this pull request.

@jonathanpeppers jonathanpeppers merged commit 03926b5 into dotnet:main Feb 18, 2026
27 of 28 checks passed
@simonrozsival simonrozsival deleted the dev/simonrozsival/java-peer-scanner-core branch February 18, 2026 15:44
@simonrozsival
Copy link
Copy Markdown
Member Author

Part of #10798

jonathanpeppers pushed a commit that referenced this pull request Feb 20, 2026
Stacked on #10823. Adds comprehensive unit test coverage for the Java peer scanner.
Part of #10798

## What this PR adds

New `Microsoft.Android.Sdk.TrimmableTypeMap.Tests` project with xUnit tests:

### 1. Test fixtures
> `TestFixtures/StubAttributes.cs`, `TestFixtures/TestTypes.cs`

Stub Mono.Android attributes and a representative set of test types covering MCW bindings, user types with component attributes, generics, nested types, interfaces with invokers, and edge-case types (unregistered, empty namespace, deep nesting).

### 2. Foundational tests
> `Scanner/JavaPeerScannerTests.cs`

Core discovery assertions: all peers found, DoNotGenerateAcw flags, component types marked unconditional, interface/abstract/generic metadata, assembly name populated.

### 3. Behavior and contract tests
> `Scanner/JavaPeerScannerTests.Behavior.cs`

Marshal method collection, JNI signature decoding for all primitive types and arrays, activation constructor inheritance, base type chain resolution, compat JNI names, custom JNI name provider attributes, nested type discovery.

### 4. Edge-case regression tests
> `Scanner/JavaPeerScannerTests.EdgeCases.cs`

Generic TypeSpecification resolution, component-only base detection, unregistered nested type naming, 3-level deep nesting, empty namespace handling, plain subclass CRC64 naming, unregistered types with interfaces/exports.
jonathanpeppers added a commit that referenced this pull request Mar 10, 2026
Stacked on #10823.
Part of #10798

## Scope

- Integration test project setup
- UserTypes fixture assembly used by parity tests
- Side-by-side scanner parity tests against legacy behavior
- CI wiring for integration test execution

## Review guide

Two commits — review in order:
1. **Integration parity test slice** — test project, UserTypes fixture, and scanner comparison tests
2. **Wire integration tests into solution and CI** — .sln entries, CI yaml, InternalsVisibleTo

Co-authored-by: Jonathan Peppers <jonathan.peppers@microsoft.com>
@github-actions github-actions bot locked and limited conversation to collaborators Mar 21, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

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.

3 participants