[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
4e7d06c to
7088777
Compare
Add JniParameterInfo, JavaConstructorInfo, and additional properties to MarshalMethodInfo and JavaPeerInfo that generators require (JniReturnType, NativeCallbackName, Parameters, JavaConstructors, ManagedTypeNamespace, ManagedTypeShortName). Extend JniSignatureHelper with raw type string parsing. Enrich scanner output with these derived fields.
…Export] ctors [Export] methods and constructors use a different activation pattern than [Register] methods. [Export] has no n_* callback on the declaring type, so UCO wrapper generation would produce invalid IL. Skip them. For JCW Java source: [Export] constructors now generate TypeManager.Activate() calls instead of nctor_N native methods. Propagate IsExport, ThrownNames, and ManagedType info from the scanner to the JCW generator. Add test fixtures: ExportsConstructors, ExportsThrowsConstructors, ExportMethodWithParams. Add scanner, model builder, and JCW generator tests verifying correct [Export] exclusion and output.
…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
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.