[TrimmableTypeMap] Add JavaPeerScanner using System.Reflection.Metadata#10805
[TrimmableTypeMap] Add JavaPeerScanner using System.Reflection.Metadata#10805simonrozsival wants to merge 6 commits intomainfrom
Conversation
b5fa14b to
991a2a6
Compare
96c7f2b to
409a83b
Compare
409a83b to
8fbb99a
Compare
This comment was marked as duplicate.
This comment was marked as duplicate.
This comment was marked as duplicate.
This comment was marked as duplicate.
This comment was marked as outdated.
This comment was marked as outdated.
|
This PR has been split into 3 stacked PRs to simplify the review:
|
| 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 |
There was a problem hiding this comment.
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.
| 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 |
| 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; |
There was a problem hiding this comment.
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.
| 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; | |
| } |
2388cf1 to
e92938e
Compare
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
e92938e to
9215de9
Compare
Part of #10789
Closes #10798
Summary
This PR adds the
Microsoft.Android.Sdk.TrimmableTypeMapproject with theJavaPeerScanner— 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)[Register]attributes (Java peers)[Register]CompatJniName(lowercased namespace) foracw-map.txtbackward compatibilityExtendsJavaPeerwith cycle detection + caching)[Application]attribute properties (BackupAgent, ManageSpaceActivity) for unconditional forcing[Activity],[Service], etc.) for unconditional markingIJniNameProviderAttributefor custom JNI name resolution[Register]+[Export]) and Java constructors in a single passSupporting types
AssemblyIndex— Phase 1 index builder with shared static helpers for cross-assembly type resolutionCustomAttributeTypeProvider— 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. AUserTypesFixture.dllwith real user-type patterns was essential:Nested type JNI name inheritance:
OuterClass+InnerHelperwhereOuterClasshas[Register("com/example/OuterClass")]must producecom/example/OuterClass_InnerHelper, not an independent CRC64 name. The scanner walks up declaring types and uses the parent's registered JNI name as prefix, matchingJavaNativeTypeManager.ToJniNamebehavior.Component-attributed base types: A
DerivedActivityextending aBaseActivitythat has[Activity]but no[Register]must still be recognized as a Java peer.ExtendsJavaPeerCorechecks bothRegisterInfoByTypeandAttributesByType.Generic interface implementations:
IList<T>is aTypeSpecificationin metadata, not aTypeReference. 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
ResolutionScopethat points to anotherTypeReference(its declaring type), not directly to anAssemblyReference. The sharedResolveTypeReferencehelper 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 inresultsByManagedName.[Export]methods:[Export("doWork")]generates Java callable wrappers via a different mechanism than[Register]. The scanner handles both viaParseExportAttribute, includingThrownNamesandSuperArgumentsString.CRC64 polynomial
The scanner uses
System.IO.Hashing.Crc64(ECMA polynomial) instead of the legacyCrc64Helper(Jones polynomial). This is intentional — the CRC64 hash only needs to be self-consistent within a single compilation. The scanner computescrc64{hash}/TypeName, the JCW generator creates a.javafile with that name, and the typemap records it. All consumers read from the same scan pass.Tests
Mono.Android.dllandUserTypesFixture.dllFollow-up
_GenerateJavaStubsmega-target for shared tasks