Skip to content

[TrimmableTypeMap] Add [Export] support to TypeMap generators#10811

Draft
simonrozsival wants to merge 14 commits intodev/simonrozsival/trimmable-typemap-03-generatorsfrom
dev/simonrozsival/trimmable-typemap-03-generators-export
Draft

[TrimmableTypeMap] Add [Export] support to TypeMap generators#10811
simonrozsival wants to merge 14 commits intodev/simonrozsival/trimmable-typemap-03-generatorsfrom
dev/simonrozsival/trimmable-typemap-03-generators-export

Conversation

@simonrozsival
Copy link
Member

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.

Copilot AI review requested due to automatic review settings February 13, 2026 10:44
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

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

Comment on lines +739 to +754
// 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);
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

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".

Copilot uses AI. Check for mistakes.
Comment on lines +280 to +283
foreach (var mm in peer.MarshalMethods) {
if (mm.IsConstructor) {
marshalMethodsBySignature [mm.JniSignature] = mm;
}
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.

Suggested change
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;

Copilot uses AI. Check for mistakes.
Comment on lines +648 to +653
foreach (var candidate in assemblyCache.Values) {
if (candidate.TypesByFullName.TryGetValue (managedTypeName, out handle)) {
resolvedIndex = candidate;
return true;
}
}
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.

Copilot uses AI. Check for mistakes.
Comment on lines +721 to +726
foreach (var line in lines) {
if (line.Contains ("VALUE = GetValue ()")) {
foundValue = true;
Assert.DoesNotContain ("static", line);
break;
}
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.

Suggested change
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);

Copilot uses AI. Check for mistakes.
Comment on lines +254 to +273
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;
}
}
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

This foreach loop immediately maps its iteration variable to another variable - consider mapping the sequence explicitly using '.Select(...)'.

Copilot uses AI. Check for mistakes.
Comment on lines +702 to +707
MemberReferenceHandle managedMethodRef;
if (export.IsConstructor) {
managedMethodRef = BuildExportCtorRef (metadata, export, declaringTypeRef);
} else {
managedMethodRef = BuildExportMethodRef (metadata, export, declaringTypeRef);
}
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

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

Both branches of this 'if' statement write to the same variable - consider using '?' to express intent better.

Suggested change
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);

Copilot uses AI. Check for mistakes.
@simonrozsival simonrozsival marked this pull request as draft February 13, 2026 11:05
@simonrozsival simonrozsival force-pushed the dev/simonrozsival/trimmable-typemap-03-generators branch from 61ac7ed to ddddd2f Compare February 13, 2026 11:13
@simonrozsival simonrozsival force-pushed the dev/simonrozsival/trimmable-typemap-03-generators-export branch 2 times, most recently from 6c4f4b9 to eae0c44 Compare February 13, 2026 11:22
@simonrozsival simonrozsival force-pushed the dev/simonrozsival/trimmable-typemap-03-generators branch from ddddd2f to 83a50be Compare February 13, 2026 11:22
@simonrozsival simonrozsival force-pushed the dev/simonrozsival/trimmable-typemap-03-generators-export branch from eae0c44 to 64b0c99 Compare February 13, 2026 11:22
@simonrozsival simonrozsival force-pushed the dev/simonrozsival/trimmable-typemap-03-generators branch from 83a50be to 4e7d06c Compare February 13, 2026 17:12
@simonrozsival simonrozsival force-pushed the dev/simonrozsival/trimmable-typemap-03-generators-export branch from 64b0c99 to 42c195c Compare February 13, 2026 17:21
@simonrozsival simonrozsival added copilot `copilot-cli` or other AIs were used to author this trimmable-type-map Area: CoreCLR Issues that only occur when using CoreCLR. Area: NativeAOT Issues that only occur when using NativeAOT. labels Feb 14, 2026
@simonrozsival simonrozsival force-pushed the dev/simonrozsival/trimmable-typemap-03-generators branch from 4e7d06c to 7088777 Compare February 16, 2026 22:25
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.
@simonrozsival simonrozsival force-pushed the dev/simonrozsival/trimmable-typemap-03-generators-export branch from 42c195c to 2b1860b Compare February 16, 2026 22:32
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.

1 participant