[WIP][TrimmableTypeMap] Add [Export] support to TypeMap generators#10811
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds comprehensive [Export] and [ExportField] attribute support to the TrimmableTypeMap generators, completing the marshal method generation pipeline. It builds upon PR #10808 by adding full marshal body generation for Export-attributed methods and constructors, replacing the legacy TypeManager.Activate pattern with GetUninitializedObject + SetHandle for constructor activation.
Changes:
- Scanner enhancements to collect [Export]/[ExportField] attributes and managed type metadata
- Model builder refactoring to generate ExportMarshalMethod entries instead of UcoConstructor entries for all constructors
- Assembly emitter additions for full marshal body IL generation with try/catch/finally, parameter unmarshaling, array marshaling, enum/CharSequence support
- JCW Java generator updates to emit private native method declarations and ExportField initializers
- Comprehensive test coverage with 254+ tests across scanner, model builder, and emitter
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs | Adds comprehensive [Export]/[ExportField] test types covering constructors, methods, arrays, enums, CharSequence, static methods, and field declarations |
| tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs | Updates ExportAttribute to support constructors, adds ExportFieldAttribute stub |
| tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs | Adds tests for Export constructor, method, and field scanning with signature validation |
| tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs | Updates tests to reflect constructor/method marshal body consolidation into ExportMarshalMethods |
| tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs | Adds tests for Export marshal method IL generation, registration, and array/enum marshaling |
| tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs | Adds tests for JCW output with Export constructors, methods, static members, and ExportFields |
| src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs | Implements CollectMarshalMethodsAndExportFields, ParseExportFieldAsRegisterInfo, managed type resolution for marshal bodies |
| src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs | Adds ExportFields property, ManagedReturnType/IsStatic to MarshalMethodInfo, ExportFieldInfo/JavaConstructorInfo.IsExport |
| src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs | Adds EmitExportMarshalMethod with full marshal body generation, SetHandle reference, array/enum/CharSequence marshaling helpers |
| src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs | Refactors BuildUcoConstructors to generate ExportMarshalMethod for all constructors, adds IgnoresAccessChecksTo logic for SetHandle |
| src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs | Replaces UcoConstructorData with ExportMarshalMethodData, adds ExportParamData for managed parameter metadata |
| src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs | Corrects Boolean → byte mapping (JNI jboolean is unsigned), adds ParseSingleTypeFromDescriptor |
| src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs | Adds WriteExportFields, updates WriteMethods/WriteConstructors for private native declarations and Export support |
| // Constructor: create uninitialized object, call activation ctor, then user ctor | ||
| // var __this = (T)RuntimeHelpers.GetUninitializedObject(typeof(T)); | ||
| encoder.OpCode (ILOpCode.Ldtoken); | ||
| encoder.Token (declaringTypeRef); | ||
| encoder.Call (_getTypeFromHandleRef); | ||
| encoder.Call (_getUninitializedObjectRef); | ||
| encoder.OpCode (ILOpCode.Castclass); | ||
| encoder.Token (declaringTypeRef); | ||
| encoder.OpCode (ILOpCode.Stloc_3); // store in local 3: __this | ||
|
|
||
| // __this.SetHandle(native__this, JniHandleOwnership.DoNotTransfer) | ||
| // — registers the peer with the runtime and sets up the JNI handle association | ||
| encoder.OpCode (ILOpCode.Ldloc_3); // __this | ||
| encoder.LoadArgument (1); // native__this | ||
| encoder.OpCode (ILOpCode.Ldc_i4_0); // JniHandleOwnership.DoNotTransfer = 0 | ||
| encoder.Call (_setHandleRef); |
There was a problem hiding this comment.
The comment on line 739 mentions "call activation ctor, then user ctor", but the implementation only calls SetHandle and then the user ctor. This is inconsistent with the comment. The activation ctor pattern (IntPtr, JniHandleOwnership) is NOT called here - instead, SetHandle is called directly to register the peer. The comment should be updated to accurately reflect the implementation: "create uninitialized object, SetHandle to register peer, then user ctor".
| foreach (var mm in peer.MarshalMethods) { | ||
| if (mm.IsConstructor) { | ||
| marshalMethodsBySignature [mm.JniSignature] = mm; | ||
| } |
There was a problem hiding this comment.
This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.
| foreach (var mm in peer.MarshalMethods) { | |
| if (mm.IsConstructor) { | |
| marshalMethodsBySignature [mm.JniSignature] = mm; | |
| } | |
| foreach (var mm in peer.MarshalMethods.Where (m => m.IsConstructor)) { | |
| marshalMethodsBySignature [mm.JniSignature] = mm; |
| foreach (var candidate in assemblyCache.Values) { | ||
| if (candidate.TypesByFullName.TryGetValue (managedTypeName, out handle)) { | ||
| resolvedIndex = candidate; | ||
| return true; | ||
| } | ||
| } |
There was a problem hiding this comment.
This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.
| foreach (var line in lines) { | ||
| if (line.Contains ("VALUE = GetValue ()")) { | ||
| foundValue = true; | ||
| Assert.DoesNotContain ("static", line); | ||
| break; | ||
| } |
There was a problem hiding this comment.
This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.
| foreach (var line in lines) { | |
| if (line.Contains ("VALUE = GetValue ()")) { | |
| foundValue = true; | |
| Assert.DoesNotContain ("static", line); | |
| break; | |
| } | |
| var valueLine = lines.FirstOrDefault (line => line.Contains ("VALUE = GetValue ()")); | |
| if (valueLine != null) { | |
| foundValue = true; | |
| Assert.DoesNotContain ("static", valueLine); |
| foreach (var caHandle in methodDef.GetCustomAttributes ()) { | ||
| var ca = index.Reader.GetCustomAttribute (caHandle); | ||
| var attrName = AssemblyIndex.GetCustomAttributeName (ca, index.Reader); | ||
|
|
||
| if (attrName == "RegisterAttribute") { | ||
| registerInfo = AssemblyIndex.ParseRegisterAttribute (ca, index.customAttributeTypeProvider); | ||
| break; | ||
| } | ||
|
|
||
| if (attrName == "ExportAttribute") { | ||
| registerInfo = ParseExportAttribute (ca, methodDef, index); | ||
| break; | ||
| } | ||
|
|
||
| if (attrName == "ExportFieldAttribute") { | ||
| registerInfo = ParseExportFieldAsRegisterInfo (methodDef, index); | ||
| exportFieldName = ParseExportFieldName (ca, index); | ||
| break; | ||
| } | ||
| } |
There was a problem hiding this comment.
This foreach loop immediately maps its iteration variable to another variable - consider mapping the sequence explicitly using '.Select(...)'.
| MemberReferenceHandle managedMethodRef; | ||
| if (export.IsConstructor) { | ||
| managedMethodRef = BuildExportCtorRef (metadata, export, declaringTypeRef); | ||
| } else { | ||
| managedMethodRef = BuildExportMethodRef (metadata, export, declaringTypeRef); | ||
| } |
There was a problem hiding this comment.
Both branches of this 'if' statement write to the same variable - consider using '?' to express intent better.
| MemberReferenceHandle managedMethodRef; | |
| if (export.IsConstructor) { | |
| managedMethodRef = BuildExportCtorRef (metadata, export, declaringTypeRef); | |
| } else { | |
| managedMethodRef = BuildExportMethodRef (metadata, export, declaringTypeRef); | |
| } | |
| MemberReferenceHandle managedMethodRef = export.IsConstructor | |
| ? BuildExportCtorRef (metadata, export, declaringTypeRef) | |
| : BuildExportMethodRef (metadata, export, declaringTypeRef); |
61ac7ed to
ddddd2f
Compare
6c4f4b9 to
eae0c44
Compare
ddddd2f to
83a50be
Compare
eae0c44 to
64b0c99
Compare
83a50be to
4e7d06c
Compare
64b0c99 to
42c195c
Compare
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>
Add test infrastructure for the Java peer scanner: - TestFixtures project with stub Mono.Android attributes ([Register], [Activity], [Service], etc.) and test types covering MCW bindings, user types, generics, nested types, interfaces, and component types - JavaPeerScannerTests with test helpers (ScanFixtures, FindByJavaName) and foundational assertions: type discovery, DoNotGenerateAcw flags, component unconditional marking, interface/abstract/generic metadata Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Test marshal method collection, JNI signature decoding, activation constructor resolution, base type chain walking, interface resolution, compat JNI names, and component attribute metadata merging. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Cover generic base/interface type specification resolution, component- only base detection, unregistered nested type naming, deep nesting, empty namespace types, plain subclass CRC64 naming, and unregistered types with interfaces or [Export] methods. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add TrimmableTypeMap, TrimmableTypeMap.Tests, and TestFixtures projects to Xamarin.Android.sln. Add CI step to run scanner unit tests and publish results on the Windows build pipeline. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add IntegrationTests and UserTypesFixture projects to Xamarin.Android.sln. Add CI step to run integration tests and publish results. Add InternalsVisibleTo for the integration test assembly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implements generators for #10799: - JcwJavaSourceGenerator: generates .java files for ACW types from JavaPeerInfo - TypeMapModelBuilder: transforms JavaPeerInfo → TypeMapAssemblyModel (IR/AST) - TypeMapAssemblyEmitter: mechanical SRM-based PE emitter from IR model - TypeMapAssemblyGenerator: high-level API composing builder + emitter - JniSignatureHelper: JNI signature parsing and CLR type encoding Key design: - 1 typemap assembly per input assembly for better caching - IR model separates 'what to generate' from 'how to serialize to IL' - Model builder tests are the primary unit tests (148 total, all passing) Generated assemblies contain: - [assembly: TypeMap] attributes per JNI type - Proxy types (JavaPeerProxy subclasses) with CreateInstance, TargetType - [UnmanagedCallersOnly] UCO wrappers for marshal methods/constructors - RegisterNatives with function pointer registration - IgnoresAccessChecksToAttribute for cross-assembly calls
- TypeMapAssemblyModel → TypeMapAssemblyData - TypeMapEntryModel → TypeMapAttributeData - ProxyTypeModel → JavaPeerProxyData - TypeRefModel → TypeRefData - UcoMethodModel → UcoMethodData - UcoConstructorModel → UcoConstructorData - NativeRegistrationModel → NativeRegistrationData - TypeMapModelBuilder → ModelBuilder
30 new tests covering every test fixture type: - MCW types: Object, Activity, Throwable, Exception, Service, Context, View, Button - User ACW types: MainActivity, MyHelper, TouchHandler, CustomView, AbstractBase - Interface types: IOnClickListener (with invoker dedup) - Nested types: Outer$Inner, ICallback$Result proxy naming - Multi-interface: ClickableView, MultiInterfaceView - Export methods: ExportExample - Generic types: GenericHolder - Full pipeline tests: scan → model → emit → read back PE Validates UCO wrapper signatures for all JNI types (bool, int, float, long, double, object, array), constructor wrappers, native registrations, TypeMap attribute counts, and proxy type names in emitted assemblies. 178 tests pass, 1 skipped.
…erator
Critical changes for TrimmableTypeMap:
- TypeMapAttributeData now supports 2-arg (unconditional) and 3-arg (trimmable):
- 2-arg: ACW user types (Android can instantiate), essential runtime types
(java/lang/Object, Throwable, Exception, etc.)
- 3-arg: MCW bindings and interfaces — trimmer preserves proxy only if
target type is referenced by the app
- Alias detection: when multiple .NET types share the same JNI name,
they get indexed entries ("jni/name", "jni/name[1]", "jni/name[2]")
with distinct proxy types
- RootTypeMapAssemblyGenerator: generates _Microsoft.Android.TypeMaps.dll
with [assembly: TypeMapAssemblyTarget("name")] for each per-assembly
typemap assembly
208 tests pass, 1 skipped.
…rows Add test fixtures and tests ported from legacy JavaCallableWrapperGeneratorTests: - GenerateConstructors → full JCW output comparison for [Export] ctors - GenerateConstructors_WithThrows → throws clause on [Export] ctors - GenerateExportedMembers → name override, throws, empty throws array - ExportCtorWithSuperArgs → SuperArgumentsString in super() call New scanner tests verify ThrownNames, name override callback names, and SuperArgumentsString propagation.
Generate full marshal method bodies for [Export] methods and constructors instead of skipping them or using TypeManager.Activate. For [Export] methods, the UCO wrapper emits: - BeginMarshalMethod guard - GetObject<T> to retrieve the managed peer - Parameter unmarshaling (primitives pass through, strings via JNIEnv.GetString, objects via GetObject<T>) - Managed method call - Return marshaling (strings via JNIEnv.NewString, objects via JNIEnv.ToLocalJniHandle) - catch/finally with OnUserUnhandledException and EndMarshalMethod For [Export] constructors, the wrapper additionally calls ActivateInstance before GetObject<T> to create the peer, then calls the user constructor body. JCW Java output now uses nctor_N native methods for ALL constructors (both [Register] and [Export]), removing the TypeManager.Activate codepath entirely. Changes: - TypeMapAssemblyEmitter: EmitExportMarshalMethod with full IL try/catch/finally using ControlFlowBuilder - ModelBuilder: [Export] methods/ctors populate ExportMarshalMethods list with managed parameter and return type info - JcwJavaSourceGenerator: Removed WriteTypeManagerActivate, unified constructor handling - Scanner: Captures ManagedReturnType for [Export] methods - 5 new emitter-level tests verifying export assembly generation - Updated JCW and model tests for new patterns
The 'leave' IL instruction clears the evaluation stack, so non-void export methods were losing their return values. Add a local variable (local 3) to store the return value before 'leave' and load it before 'ret'. Void methods are unaffected (3 locals as before).
The managed method ref for [Export] methods with object return types incorrectly encoded the return type as IntPtr instead of the actual managed type. The CLR would fail to find the method at runtime. Also assembly-qualify the ManagedReturnType in the scanner (matching how parameters are already handled) and add a test assertion for it.
RegisterNatives must use the name matching the JCW native method
declaration. For methods, JCW declares 'n_<ManagedMethodName>' but
RegisterNatives was using 'n_<JniName>' (stripped from wrapper name).
When the export name differs from the managed name (e.g.,
[Export("attributeOverridesNames")] CompletelyDifferentName),
the JNI runtime would fail to connect them.
Fix: pass NativeCallbackName explicitly through ExportMarshalMethodData.
For methods: 'n_<ManagedMethodName>' (matches JCW).
For constructors: 'nctor_N' (matches JCW).
[Export] methods are new declarations, not overrides of base class methods. The @OverRide annotation should only appear on [Register] methods. Also change native method declarations from 'public native' to 'private native' to match legacy JCW output. Added tests: @OverRide suppression for [Export], private native visibility assertions.
Static [Export] methods: - Added IsStatic to MarshalMethodInfo (scanner) and ExportMarshalMethodData (model) - Scanner captures MethodAttributes.Static for all methods - JCW emits 'public static' wrapper and 'private static native' declaration - IL emitter: static methods skip GetObject<T>/callvirt, use direct call - Method signature uses isInstanceMethod: false for static exports [ExportField]: - Added ExportFieldInfo data type with FieldName, MethodName, IsStatic - Scanner: ExportFieldAttribute parsed, method registered as marshal method (Connector = null, like [Export]) and field info collected separately - JCW: field declarations emitted after static initializer, before ctors Format: 'public [static] <type> FIELD_NAME = MethodName ();' 17 new tests across scanner, model, emitter, and JCW layers. 299 tests pass.
- Merge CollectMarshalMethods + CollectExportFields into single-pass CollectMarshalMethodsAndExportFields - Inline TryGetMethodRegisterInfo into merged method - Replace CollectExportFields with ParseExportFieldName helper - Extract WriteThrowsClause helper in JCW generator - Deduplicate parameter unmarshal loop in emitter - Fix stale IsExport comment
Verify that [Export] methods, constructors, static exports, and [ExportField] backing methods all appear in the RegisterNatives IL body with correct JNI names and signatures. This catches bugs where wrappers are generated but not registered, which would cause runtime UnsatisfiedLinkError.
JNI's jboolean is an unsigned 8-bit type. The legacy MarshalMethodsAssemblyRewriter maps System.Boolean → System.Byte. We were using SByte (signed), which is incorrect.
All constructors (both [Register] and [Export]) now generate full marshal bodies with BeginMarshalMethod + try/catch/finally. There are no pre-existing n_* callback methods for constructors — they only exist for [Register] methods where the connector points to a real static method. Constructor callback pattern (nctor_N_uco): GetUninitializedObject(typeof(T)) -> SetHandle(native__this, DoNotTransfer) -> .ctor(params) CreateInstance for inherited activation ctor calls BaseType::.ctor(IntPtr, JniHandleOwnership) on the first base class that declares it, rather than SetHandle directly. Removed ActivateInstance, UcoConstructorData, and the forwarding approach for constructors. Scanner now populates ManagedType on constructor parameters regardless of Connector value.
- Support arrays in export marshal wrappers using JNIEnv.GetArray<T>/NewArray<T>/CopyArray<T> including copy-back cleanup for array parameters - Support enum signatures and marshaling as JNI int while calling managed enum methods - Map Java.Lang.ICharSequence to Ljava/lang/CharSequence; in export signature generation - Resolve JNI object descriptors from [Register] metadata for managed object types (e.g. View[] -> [Landroid/view/View; instead of Object[]) - Improve ManagedTypeToAssemblyQualifiedName to resolve non-BCL types and array element types - Generate proxies for export-only ACW types (no activation ctor / no [Register] methods) Add fixture coverage and tests for export-only proxies plus array/enum/charsequence signatures and registration. All 312 tests pass.
42c195c to
2b1860b
Compare
7800276 to
7b5ecb7
Compare
e3e4f05 to
9449651
Compare
Stacked on #10808.\n\nThis PR contains [Export]/[ExportField] generator support and marshal parity work, including constructor marshal bodies and remaining marshaling gaps (arrays/enums/CharSequence), with tests.