Skip to content

[TrimmableTypeMap] Add trimmable [Export] and [ExportField] callback support#11123

Open
simonrozsival wants to merge 58 commits into
mainfrom
dev/simonrozsival/trimmable-typemap-export-attribute
Open

[TrimmableTypeMap] Add trimmable [Export] and [ExportField] callback support#11123
simonrozsival wants to merge 58 commits into
mainfrom
dev/simonrozsival/trimmable-typemap-export-attribute

Conversation

@simonrozsival
Copy link
Copy Markdown
Member

@simonrozsival simonrozsival commented Apr 16, 2026

Summary

Add UCO (UnmanagedCallersOnly) wrapper codegen for [Export] methods and [ExportField] fields in the trimmable typemap pipeline, and extend UCO constructor codegen to invoke user-visible managed ctors (parameterless and object-reference parameterized) so types whose Java-side activation triggers user-defined ctor logic — including all Throwable subclasses with (Throwable cause) ctors and types using [Export] ctors — work correctly under trimming + CoreCLR.

Depends on #11091 (trimmable test plumbing + CoreCLRTrimmable CI lane).

Part of #10788

Changes

Export method dispatch support

  • Scanner: Detect [Export] and [ExportField] attributes on Java peer types; resolve JNI signatures for non-primitive Java-bound parameter types; collect Java access modifiers and throws clauses
  • Model: MarshalMethodInfo carries IsExport, JavaAccess, ThrownNames, and SuperArgumentsString for export metadata
  • JCW Java codegen: Generate Java native methods with correct access modifiers and throws clauses for [Export] methods; generate Java field declarations for [ExportField]
  • UCO wrappers + RegisterNatives: Export methods get the same UCO wrapper + JNI native registration as [Register] native callbacks
  • ExportMethodDispatchEmitter: New emitter class handling the PE metadata generation for export dispatch, separate from the UCO constructor emitter
  • Exclude Mono.Android.Export: Exclude the Mono.Android.Export assembly from trimmable packages — its DynamicMethod-based codegen is incompatible with AOT/trimming; uses [ExportAttribute]/[ExportFieldAttribute] from the user assembly's [Register]-scanned types instead

UCO constructor wrappers — invoke user-visible managed ctors

The legacy UCO constructor codegen always built the managed peer via the JI activation ctor (IntPtr, JniHandleOwnership), which is sufficient for plain [Register] types but skips any user-defined ctor body. That broke types whose Java-side activation must run user code — most notably [Export]-using types (whose Constructed = true / SuperArgumentsString initialization happens in the user ctor) and Throwable subclasses that need to forward the cause argument.

This PR generalizes UCO constructor codegen to mirror Java.Interop.TypeManager.Activate:

  • Scanner: JavaPeerScanner.TryFindMatchingManagedCtorParams matches each registered Java ctor signature to a managed ..ctor. The match requires equal arity and (currently) all-object-reference JNI args; primitive args fall through to the legacy activation-ctor path. Match results are recorded on JavaConstructorInfo.ManagedParameterTypes and plumbed through ModelBuilderUcoConstructorData.
  • Emitter: New EmitUserVisibleCtorWrapper helper emits, when a match exists:
    var obj = (T) RuntimeHelpers.GetUninitializedObject (typeof (T));
    ((IJavaPeerable) obj).SetPeerReference (new JniObjectReference (self));
    obj..ctor (
        (TParam0) Java.Lang.Object.GetObject (arg0, JniHandleOwnership.DoNotTransfer, typeof (TParam0)),
        ...);
    The internal Java.Lang.Object.GetObject (IntPtr, JniHandleOwnership, Type) helper is reachable from the generated assembly via the always-on [IgnoresAccessChecksTo("Mono.Android")] attribute that ModelBuilder emits.
  • Safe fallback: When no managed ctor matches by arity (e.g. Java.Lang.Thread+RunnableImplementor, which only has parameterized managed ctors but registers a ()V Java ctor via JCW codegen) or when the JNI signature contains a primitive arg, the emitter falls back to the legacy activation-ctor path so we never emit a metadata reference to a non-existent method.

Bug fixes

  • Fix missing static keyword in Java codegen for static [Export] methods
  • Fix stack corruption in TryEmitExportParameterArgument (wrong local variable index)
  • Fix instrumentation targetPackage defaulting to use the passed-in package name parameter
  • Propagate deferred registerNatives to base classes so inherited exports are registered correctly
  • Fix RunnableImplementor crash (MissingMethodException: Default constructor not found for type Java.Lang.Thread+RunnableImplementor) — see "UCO constructor wrappers" above

Tests

New device tests in Mono.Android-Tests covering the activation contract:

  • ActivatedDirectThrowableSubclasses_ThrowableCtor_ShouldForwardArgs — single-arg (Throwable cause) ctor invoked from Java
  • ActivatedDirectThrowableSubclasses_MultipleCtors_ShouldDispatchToCorrectCtor — multi-arity dispatch (()V and (Throwable)V)
  • Plus the existing ContainsExportedMethods / [Export] test fixtures, which exercise the parameterless user-ctor path

Java.Interop/ExportTests.cs — new fixture exercising [Export] parameter / return marshalling end-to-end via JNIEnv.GetMethodID + Call*Method. Each enabled device test runs under both the legacy llvm-ir typemap (which defines the contract) and the trimmable typemap (which must match it). Verified locally after rebase: all 9 / 9 currently enabled ExportTests pass on _AndroidTypeMapImplementation=trimmable + UseMonoRuntime=false:

Group Test Coverage
A Export_Method_Primitive_RoundTrip int -> int
A Export_Method_Bool_RoundTrip bool -> bool (byte / bool ABI)
A Export_Method_String_RoundTrip string -> string
A Export_Method_PeerArg_RoundTrip Java.Lang.Object arg unwrap
A Export_Method_PeerArg_NullArg_HandledGracefully null arg → C# null
A Export_Method_IntArray_RoundTrip_AndCopyBack int[] arg + copy-back
A Export_Method_PeerArray_RoundTrip Java.Lang.Object[] arg/return
B Export_Method_Throws_PrimitiveReturn_SurfacesAsManagedException exception preserved through OnUserUnhandledException
B Export_Method_Throws_ObjectReturn_SurfacesAsManagedException exception preserved through OnUserUnhandledException

Group B verifies the new [Export] UCO marshal-method wrapper: each exported method body now runs inside BeginMarshalMethod / try / catch (route via JniRuntime.OnUserUnhandledException) / finally (EndMarshalMethod), mirroring the trimmable UCO ctor wrapper. Without this, an unhandled managed exception aborts the CoreCLR process. Note: unlike legacy AndroidEnvironment.UnhandledException (which translated to Java.Lang.Throwable), JniRuntime.OnUserUnhandledException preserves the original managed exception via JniTransition.SetPendingException, so callers see the original C# exception type with the original message.

Also: JavaPeerScanner.TryFindMatchingManagedCtorParams now skips parameterized [Export] ctors with generic / by-ref / pointer parameter types (falling back to the activation-ctor path), fixing a pre-existing build failure on Xamarin.Android.NUnitLite's TestDataAdapter ctor whose JavaList<T> parameter triggered XAGTT7015.

Verified locally on _AndroidTypeMapImplementation=trimmable × UseMonoRuntime=false: 928 passed / 0 failed / 56 ignored, plus the 9 new Export tests.

Scanner integration coverage for [Export] shapes

A dedicated integration test project (Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests) walks the new scanner over a fixture assembly (UserTypesFixture) and asserts JNI signatures / connectors / metadata for every shape the scanner needs to handle. Coverage was expanded over the course of the PR to 27 test cases and surfaced 5 real scanner bugs that the unit suite did not catch:

Area Cases Bugs caught & fixed
Advanced [Export] shapes (enum, ICharSequence, non-generic collections, [ExportField], [ExportParameter]) 8 [ExportField] returning a user-peer type emitted Ljava/lang/Object; instead of the peer's CRC64 JNI name. Fixed by extending TryResolveJniObjectDescriptor to fall back to ComputeAutoJniNames for types extending a Java peer without [Register].
Phase A — dispatch & declaration shapes (static [Export], Throws, mixed [Register]+[Export], virtual + derived, custom JNI name) 5 (1) [Export(Throws = …)] Type[] was silently dropped — scanner only read the internal ThrownNames (string[]). Fixed by resolving each typeof() arg to its JNI internal name. (2) ExportAttribute is Inherited=false, but FindBaseRegisteredMethodInfo propagated base [Export] registrations to derived overrides. Fixed by restricting propagation to [Register]/[JniConstructorSignature]-direct registrations.
Phase B — edge marshalling (Java.Lang.Object explicit, array of user-peer, protected/private visibility, primitive [ExportField], overloaded names) 5 All green on first run.
Phase C — robustness (generic method, [Export] on [Register]'d-base override) 2 Pass 1 unconditionally added every registered method to the dedup key set, so [Export] on a [Register]'d-base override prevented Pass 3 from also emitting the override entry (only onCreateExport, no onCreate). Fixed by skipping the dedup key for [Export]/[ExportField].

Marshalling parity follow-ups (commits in this PR)

These extend the trimmable typemap's [Export] parameter/return marshalling to mirror legacy Mono.Android.Export/CallbackCode for reference / value types whose JNI ABI requires more than the generic IJavaObject path:

Type JNI descriptor Runtime helper Commit
enum (Int32 / Byte / Int16 / Int64 backed) underlying primitive (I / B / S / J) n/a — primitive ABI "Marshal enum [Export] params/returns via underlying primitive JNI ABI"
Java.Lang.ICharSequence Ljava/lang/CharSequence; Android.Runtime.CharSequence.ToLocalJniHandle (ICharSequence) "Marshal ICharSequence and non-generic collection [Export] returns via dedicated runtime helpers"
System.Collections.IList Ljava/util/List; Android.Runtime.JavaList.ToLocalJniHandle (IList) (same)
System.Collections.IDictionary Ljava/util/Map; Android.Runtime.JavaDictionary.ToLocalJniHandle (IDictionary) (same)
System.Collections.ICollection Ljava/util/Collection; Android.Runtime.JavaCollection.ToLocalJniHandle (ICollection) (same)

Scanner unit tests cover each new descriptor; emitter changes are exercised through the existing JavaPeerScannerTests / TypeMapAssemblyGeneratorTests coverage (468 unit tests passing).

Device-test follow-up: device tests for enum, ICharSequence, non-generic collection, and [ExportField] shapes are intentionally deferred. The legacy JCW emitter CecilImporter.GetJniSignature rejects some of these types when generating Java callable wrappers (it returns null, which fails the build), and [ExportField] currently needs separate JCW field-initializer work before runtime coverage is possible. The scanner/emitter paths are covered by host tests in this PR; end-to-end device coverage is tracked by #11289.

Related issues

Rebase validation (2026-05-05)

Rebased onto current main and resolved the test-project trimmable root conflict. Additional rebase fallout fixed in 16382c25d:

  • Use InvokerActivationCtorStyle when generating interface invoker activation so Java.Interop-style invokers use the by-ref JniObjectReference constructor path.
  • Retarget the export UCO exception-region test to an actual [Export] wrapper and assert the catch/finally wrapper shape emitted by export dispatch.

Validation after the rebase:

  • git diff --check
  • make prepare CONFIGURATION=Release
  • ./bin/Release/dotnet/dotnet test tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj -v minimal --no-restore — 476 passed
  • ./bin/Release/dotnet/dotnet test tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests.csproj -c Release -v minimal --no-restore — 27 passed
  • make all CONFIGURATION=Release
  • MSBUILDDISABLENODEREUSE=1 ANDROID_SERIAL=emulator-5554 ./dotnet-local.sh build -t:RunTestApp tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj -c Release -p:_AndroidTypeMapImplementation=trimmable -p:UseMonoRuntime=false -p:AdbTarget="-s emulator-5554" -nr:false — Passed: 878, Failed: 0, Skipped: 56, Total: 934. Confirmed ExportTests and the activated direct throwable subclass tests executed successfully.

Rebase + follow-ups (current branch)

The branch has been rebased onto current main (replacing the merge-driven catch-up commits). The NUnitInstrumentation.cs rebase conflict in tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/ was resolved by taking main's 4-entry exclusion list verbatim; no new trimmable test exclusions are introduced (#11270 and #11271, now in main, fix the JavaProxyObject / JavaProxyThrowable / JNI-method-replacement tests that this PR had previously excluded).

Six follow-up commits address production-readiness gaps identified after the rebase:

  1. Surface diagnostic for silent user-ctor fallback in trimmable typemap — adds CtorFallbackReason on JavaConstructorInfo plus ITrimmableTypeMapLogger.LogUserCtorFallbackInfo, so silent fallback from a registered Java ctor to the legacy (IntPtr, JniHandleOwnership) activation path is visible in MSBuild -bl output. No new logger dependency inside the scanner.
  2. Scanner regression tests for HasMatchingManagedCtor + CtorFallbackReason — guards the RunnableImplementor-style activation-ctor fallback fixed earlier in this PR.
  3. Phase D integration: [Export] on a [Register]'d interface implementor — adds two scanner integration cases (onClick and renamed-JNI-name variant on an IOnClickListener impl).
  4. Build-task coverage: Build_WithExportAndExportField_GeneratesJcwAndTypeMap end-to-end test, Build_WithExport_ProducesNoTrimWarningsTargetingExportCodegen trim-warning baseline, and TrimmableTypeMap_ExcludedTestNames_DoesNotGrow tripwire guarding the exclusion count from creeping up.
  5. Device tests for nested + re-entrant exception routing — adds Export_Method_Throws_FollowedBySecondCall_DoesNotLeakPendingException and Export_Method_NestedJniCall_PreservesExceptionFromInnerExport to Group B of ExportTests.
  6. Release notesDocumentation/release-notes/11123-trimmable-typemap-export-attribute.md documents [Export] / [ExportField] support, the Mono.Android.Export exclusion, user-visible ctor invocation, and the new exception-routing behaviour.

Host validation (current commits)

  • ./bin/Release/dotnet/dotnet test tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj -c Release508 passed (was 476; +5 ctor scanner tests in this PR's follow-ups, plus +27 from earlier PR commits).
  • ./bin/Release/dotnet/dotnet test tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests.csproj -c Release29 passed (was 27; +2 Phase D cases).
  • ./bin/Release/dotnet/dotnet test src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Xamarin.Android.Build.Tests.csproj -c Release --filter "FullyQualifiedName~TrimmableTypeMap_ExcludedTestNames" — passed (the exclusion-growth tripwire).

Device validation

Device validation for the new commits is delegated to CI (the local rebuild was blocked by a pre-existing BG4304 XPath / path-separator issue in src/Mono.Android/metadata that is unrelated to this PR and also reproduces on origin/main). The Group B device tests in ExportTests exercise the existing exception-routing wrapper plus the two new re-entrancy cases; the [Export]-bearing fixtures used today (ExportPrimitives, ExportThrowing) gain a new ExportReentrant peer.

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

This PR extends the TrimmableTypeMap pipeline to support legacy [Export] / [ExportField] callbacks (including UCO wrapper + RegisterNatives generation and richer signature/metadata scanning), and adjusts test/build plumbing to stabilize the CoreCLRTrimmable test lane (including excluding Mono.Android.Export from app packaging on the trimmable path).

Changes:

  • Add scanner + model support for [Export] / [ExportField], including signature resolution for Java-bound types and [ExportParameter] legacy marshalling shapes.
  • Add generator support for direct-dispatch UCO wrappers for [Export] (new ExportMethodDispatchEmitter) and align typemap/manifest generation behavior.
  • Stabilize CI/test lanes: introduce CoreCLRTrimmable flavor + categories, defer registerNatives up base class chains, and ensure Mono.Android.Export isn’t packaged on the trimmable path.

Reviewed changes

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

Show a summary per file
File Description
src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs Detect/export [Export] metadata, compute JNI signatures with legacy marshalling shapes, and record precise managed type/assembly info.
src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ExportMethodDispatchEmitter.cs New emitter that generates UCO wrappers which dispatch directly to managed [Export] targets (avoids legacy dynamic callback generation).
src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs Integrate export dispatch emitter, refactor RegisterNatives emission, and adjust proxy/UCO emission flow.
src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs Manifest rooting rewrite + propagation of deferred registration flags to base classes; pass prepared manifest through generation.
src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets Mark Mono.Android.Export references with AndroidSkipAddToPackage=true for trimmable typemap builds.
src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs Skip assemblies marked AndroidSkipAddToPackage when generating native app config sources.
tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/* New/updated fixtures and assertions covering export scanning, export dispatch generation, manifest rewriting, and instrumentation defaults.
tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj Add CoreCLRTrimmable configuration defaults (runtime selection, categories, constants).
build-tools/automation/yaml-templates/stage-package-tests.yaml Add CoreCLRTrimmable instrumentation lane and adjust CoreCLR lane args.

Comment thread src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs Outdated
@simonrozsival simonrozsival marked this pull request as draft April 16, 2026 15:07
@simonrozsival simonrozsival added the copilot `copilot-cli` or other AIs were used to author this label Apr 16, 2026
@simonrozsival simonrozsival force-pushed the dev/simonrozsival/trimmable-typemap-export-attribute branch 2 times, most recently from 2b68085 to 9007196 Compare April 18, 2026 20:29
Copy link
Copy Markdown
Member Author

@simonrozsival simonrozsival left a comment

Choose a reason for hiding this comment

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

🤖 Code Review — PR #11123

Verdict: ⚠️ Needs Changes (1 warning, 2 suggestions; CI still pending)

Summary

Solid implementation of [Export]/[ExportField] for the trimmable typemap pipeline. The static UCO dispatch via [UnmanagedCallersOnly] + RegisterNatives is correct, the IL generation handles all primitive/object/array/stream/XML marshalling shapes, and the test coverage is thorough (162 new test assertions across scanner, model builder, assembly generator, JCW codegen, and build integration). The Mono.Android.Export.dll exclusion from the packaged APK is properly wired.

Positive callouts

  • ExportMethodDispatchEmitterContext factory — single-allocation, reused for the entire emit pass. Clean separation from the parent emitter.
  • ExportParameterKind support — properly resolves InputStream, OutputStream, XmlPullParser, XmlResourceParser marshalling in both directions (JNI→managed and managed→JNI return).
  • Array copy-back with null guards — the Brfalse_s skip pattern correctly avoids null-array copy-back crashes.
  • Cross-assembly type resolutionTypeRefSignatureTypeProvider + MetadataTypeNameResolver properly chase ResolutionScope to the correct assembly reference, and the test Generate_ExportProxy_UsesExactCrossAssemblyTypeReferences validates it end-to-end.

CI (Xamarin.Android-PR) is still pending — review is based on code analysis only.

Comment thread src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs
@simonrozsival simonrozsival marked this pull request as ready for review April 22, 2026 16:13
@simonrozsival
Copy link
Copy Markdown
Member Author

Blocked by #11091

@simonrozsival simonrozsival force-pushed the dev/simonrozsival/trimmable-typemap-export-attribute branch from cc98948 to 68aafbe Compare April 26, 2026 13:53
@simonrozsival simonrozsival changed the base branch from main to dev/simonrozsival/trimmable-test-plumbing April 26, 2026 13:55
Base automatically changed from dev/simonrozsival/trimmable-test-plumbing to main April 27, 2026 21:15
@simonrozsival
Copy link
Copy Markdown
Member Author

/android-reviewer

@simonrozsival simonrozsival force-pushed the dev/simonrozsival/trimmable-typemap-export-attribute branch from 4824296 to d7a4d4f Compare May 4, 2026 18:45
simonrozsival and others added 13 commits May 5, 2026 14:46
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Propagate CannotRegisterInStaticConstructor through the base class chain
  so that base types like TestInstrumentation_1 also use the deferred
  __md_registerNatives() pattern instead of static { registerNatives(...); }
  which crashes before the managed runtime registers the JNI native.

- Revert C++ host-jni.cc/hh registerNatives bridge — the managed
  [UnmanagedCallersOnly] registration in TrimmableTypeMap.RegisterNatives()
  handles this without needing a C++ bridge.

- Add targetPackage default for instrumentation in ComponentElementBuilder.

- Switch proxy base type to generic JavaPeerProxy<T> in TypeMapAssemblyEmitter.

- Add CannotRegisterInStaticConstructor to JavaPeerProxyData model.

- Normalize manifest android:name to actual JNI names.

- Add test exclusions for TrimmableIgnore and SSL categories.

- Add TRIMMABLE_TYPEMAP define constant for conditional compilation.

- Add unit tests for base class propagation and manifest normalization.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… refactoring

Revert files that are not about [Export] support:
- CI lane (stage-package-tests.yaml)
- Test exclusions/categories (TrimmableIgnore, DoNotGenerateAcw)
- NUnitInstrumentation test plumbing
- Mono.Android.NET-Tests.csproj trimmable setup
- TrimmableTypeMapGenerator manifest refactoring (from #11105)
- TrimmableTypeMapGeneratorTests manifest/propagation tests

Keep only Export-related changes:
- CoreCLRIgnore removal from Export tests in JnienvTest.cs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Revert cast spacing changes in AssemblyIndex.cs, MetadataTypeNameResolver.cs
- Revert indentation changes in JavaPeerScanner.cs, GenerateNativeApplicationConfigSources.cs
- Revert whitespace in PackagingTest.cs, GeneratePackageManagerJavaTests.cs, TypeMapAssemblyGeneratorTests.cs
- Move EmitRegisterNatives back after EmitUcoConstructor to match main's method ordering

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
LoadArgument + LoadConstantI4(0) were emitted unconditionally before the
switch statement. When exportKind is Unspecified (the default for
parameters without [ExportParameter] attributes), the method returned
false without consuming those two stack values, corrupting the IL
evaluation stack.

Move the LoadArgument + LoadConstantI4(0) into each case block so they
are only emitted when the method will also emit the consuming Call.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The _AndroidTypeMapImplementation=trimmable forcing and Assert.Ignore
removal for NativeAOT belong in the separate CI setup PR, not here.
This PR should only add the Export code generation support without
modifying CI configuration or device test behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The JcwJavaSourceGenerator was not emitting the 'static' keyword for
static [Export] methods, which would cause a runtime crash. Add the
keyword to both the wrapper method and the native declaration when
method.IsStatic is true. Add a regression test.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival and others added 13 commits May 5, 2026 14:47
The original managed exception is preserved across the JNI boundary
when re-raised on the calling thread (JniRuntime.OnUserUnhandledException
just calls JniTransition.SetPendingException), unlike legacy
AndroidEnvironment.UnhandledException which wrapped to Java.Lang.Throwable.
Tests now assert the process did not abort and the exception with
the original 'boom' message surfaces.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Mirrors legacy CallbackCode/MonoAndroidExport behaviour: enum parameters
and return values use their underlying integer JNI ABI (typically I, but
also B / S / J depending on the enum's underlying type), not the object
peer marshalling path.

Changes:
- Scanner: walk loaded assemblies for the export type's parameter/return
  managed names, detect 'System.Enum'-derived types, and emit the
  underlying primitive JNI descriptor instead of falling through to
  'Ljava/lang/Object;'.
- TypeRefData: new IsEnum flag plumbed from the scanner so the IL emitter
  encodes the type as ELEMENT_TYPE_VALUETYPE in callback member-refs and
  signatures (was previously emitted as ELEMENT_TYPE_CLASS, which would
  fail metadata resolution at runtime).
- Tests: new ExportEnumShapes fixture + scanner unit tests covering
  Int32-, Byte-, and Int64-backed enums.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… dedicated runtime helpers

Mirrors legacy Mono.Android.Export/CallbackCode behaviour for reference
types whose JNI ABI requires a dedicated marshaller — the generic
JNIEnv.ToLocalJniHandle (IJavaObject) fallback used by the trimmable
typemap is wrong for these:

- ICharSequence: must dispatch through CharSequence.ToLocalJniHandle so
  that a managed 'string' returned as ICharSequence gets wrapped into a
  Java String (legacy SymbolKind.CharSequence).
- IList / IDictionary / ICollection: legacy walked the type to find a
  static ToLocalJniHandle method on JavaList / JavaDictionary /
  JavaCollection. Reproduce that with strongly-typed MemberRefs so the
  IL emitter calls the right helper directly.

Changes:
- ExportMethodDispatchEmitterContext: new MemberRefs to
  CharSequence/JavaList/JavaDictionary/JavaCollection.ToLocalJniHandle,
  resolving Mono.Android types Android.Runtime.{CharSequence, JavaList,
  JavaDictionary, JavaCollection} and the System.Collections.{IList,
  IDictionary, ICollection} parameter types.
- ExportMethodDispatchEmitter.ConvertManagedReturnValue: dispatch
  ICharSequence / IList / IDictionary / ICollection returns through the
  matching helper instead of the generic IJavaObject path.
- Scanner.ManagedTypeToJniDescriptor: emit Ljava/lang/CharSequence; /
  Ljava/util/{List,Map,Collection}; for those well-known managed types
  instead of falling through to Ljava/lang/Object;.
- Tests: ExportCharSequenceShapes / ExportCollectionShapes fixtures + 4
  scanner unit tests covering the new descriptors. ICharSequence stub
  added under Java.Lang in TestTypes.cs to mirror Mono.Android's
  unregistered interface.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Mark enum / ICharSequence / non-generic collection rows in §2, §5, §7
as fixed (commits 634af35 and 86e94d7). Add a new §7 subsection
documenting the JCW-emitter blocker (CecilImporter.GetJniSignature)
that prevents device-level exercise of those marshalling paths until a
separate follow-up PR teaches the legacy callable-wrapper emitter to
widen those types. Update §8 'Done in this PR' / 'Still open' lists
accordingly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…isions

Code review caught two related issues in TryFindEnumTypeDefinition:

1. The TypeRefData.AssemblyName carried by every parameter / return type
   was being discarded. Two assemblies that happen to define types with
   identical fully-qualified names (one enum, one not) resolved
   non-deterministically based on Dictionary enumeration order.

2. When a same-named non-enum type was encountered first, the lookup
   returned null immediately instead of continuing to scan the remaining
   loaded assemblies. A legitimate enum in a later-enumerated assembly
   was therefore silently dropped, producing the wrong JNI descriptor
   ('Ljava/lang/Object;' instead of the underlying primitive).

Fix: Plumb the AssemblyName hint through TryResolveEnumUnderlyingDescriptor
/ IsEnumOrEnumArray / TryFindEnumTypeDefinition. When the hint resolves
to an enum, use it directly; otherwise fall through to scanning every
loaded assembly and continue past same-named non-enums.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Drop the null-forgiving operator (forbidden by repo conventions) — use
  an 'is { Length: > 0 }' pattern instead, which the C# compiler tracks
  for null-flow without requiring [NotNullWhen] on netstandard2.0.
- Trim redundant XML doc and historical-archaeology comment.

No functional change. All 468 unit tests still pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This document was a working artifact for the marshalling-parity gap
analysis. It belongs in the PR conversation (or a follow-up internal
doc), not in the repository. Keeping the trail in git history via the
forward-commit deletion (no force-push).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extends the UserTypesFixture with the [Export] parameter / return shapes
the trimmable scanner now handles (enum, ICharSequence, non-generic
IList / IDictionary / ICollection — Phase 1.1/1.2/1.3).

The legacy JCW emitter (CecilImporter.GetJniSignature) cannot encode
these types — that is the documented JCW emitter blocker. ScannerRunner
now catches the resulting ArgumentNullException and falls back to direct
[Register] extraction so the legacy↔new comparison tests continue to
pass without those types.

ScannerExportShapesTests asserts the new scanner produces the right JNI
signatures end-to-end:

  - echoEnum (I)I, echoByteEnum (B)B, echoLongEnum (J)J
  - echoCharSequence (Ljava/lang/CharSequence;)Ljava/lang/CharSequence;
  - echoList (Ljava/util/List;)Ljava/util/List;
  - echoMap (Ljava/util/Map;)Ljava/util/Map;
  - echoCollection (Ljava/util/Collection;)Ljava/util/Collection;

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ser-peer JNI

The new integration tests caught a real bug: `TryResolveJniObjectDescriptor`
only honored types with explicit [Register], so [Export]/[ExportField]
methods returning a user peer (e.g. an [ExportField] getter returning
itself) emitted Ljava/lang/Object; instead of the actual peer JNI name.

Fix: when a managed type lacks [Register] but extends a Java peer, fall
back to the same CRC64-based JNI name that ScanAssembly assigns it via
ComputeAutoJniNames. Mirrors the legacy CecilImporter behaviour.

Tests:
* New ScannerExportShapesTests cases for [ExportField] (3 getters) and
  [ExportParameter] (4 Stream/XmlReader override shapes).
* Legacy↔new comparison normaliser now strips embedded crc64 segments
  in JNI signatures (regex-based), so the [ExportField] getter returning
  its own peer type compares cleanly across the two scanners.

15/15 integration tests pass, 468/468 unit tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add 5 [Export]-shape integration tests + fix 2 real bugs they surfaced:

- A.1 Static [Export] method: ()V dispatch on non-instance
- A.2 [Export(Throws = …)]: declared exception types
- A.3 Mixed [Register] override + [Export] new on same type
- A.4 Virtual [Export] in base, derived override without [Export]
- A.5 Custom JNI name differing from C# method name

Bugs fixed:

1. JavaPeerScanner.ParseExportAttribute did not read the user-facing
   Throws (Type[]) named arg — only the internal ThrownNames (string[]).
   User code overwhelmingly writes `Throws = new[] { typeof(IOException) }`,
   so declared exceptions were silently dropped. Resolve each typeof()
   argument via TryResolveJniObjectDescriptor and surface as JNI internal
   names (java/io/IOException).

2. FindBaseRegisteredMethodInfo treated [Export]/[ExportField] base
   registrations as inheritable, producing duplicate marshal-method
   entries on derived overrides. ExportAttribute is Inherited=false; the
   override should not inherit the base [Export] registration. Restrict
   propagation to [Register]/[JniConstructorSignature] only.

Tests: 20/20 integration (was 15), 468/468 unit.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add 5 [Export]/[ExportField] integration tests for edge JNI shapes:

- B.1 [Export] returning Java.Lang.Object explicitly: keeps the
  unwrapped Object descriptor (distinct from the user-peer fallback).
- B.2 [Export] of array of user-peer type: exercises [] recursion
  through the user-peer JNI resolver fixed earlier.
- B.3 [Export] on protected/private methods: visibility doesn't gate
  registration.
- B.4 [ExportField] returning a primitive: confirms ()I and the
  '__export__' connector.
- B.5 [Export] overloads with same Java name + different signatures:
  no dedup; both register distinctly.

All 5 cases passed on first run — no scanner bugs surfaced.
Tests: 25/25 integration (was 20), 468/468 unit.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add 2 robustness integration tests + fix 1 real bug surfaced:

- C.1 (property [Export]): gated by [AttributeUsage(Method|Constructor)]
  at compile time — skipped.
- C.2 Generic method with [Export]: scanner doesn't crash; legal Java
  targets are filtered upstream, but the scan itself is robust.
- C.3 [Export] on a [Register]'d-base override: BOTH entries register
  — the [Register]-driven override (so Activity.onCreate dispatch keeps
  working) AND the [Export]-driven new method.

Bug fixed: Pass 1 unconditionally added every method that yielded a
RegisterInfo to the dedup key set, so a subsequent [Export]/[ExportField]
hit prevented Pass 3 (base-override detection) from also adding the
[Register]-driven entry. [Export] is orthogonal to [Register]
inheritance, so only [Register]-direct hits should preempt Pass 3.

Tests: 27/27 integration (was 25), 468/468 unit.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Use the Java.Interop activation constructor style when creating interface invokers, and align export UCO wrapper tests with the catch/finally shape emitted by export dispatch.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival
Copy link
Copy Markdown
Member Author

@copilot resolve the merge conflicts in this pull request

…ttribute

Resolve merge conflicts:
- TypeMapAssemblyEmitter.cs: keep ExportMethodDispatchEmitter field + new array handling fields from main
- FixtureTestBase.cs: take refactored ILContainsOpcodeToken helper from main
- NUnitInstrumentation.cs: remove stale test exclusions fixed in main (#11238, #11273)

Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI commented May 6, 2026

@copilot resolve the merge conflicts in this pull request

Resolved in a730499. Three files had conflicts:

500 unit tests + 27 integration tests pass after the merge.

simonrozsival and others added 9 commits May 7, 2026 07:44
When JavaPeerScanner.TryFindMatchingManagedCtorParams declines to bind a
registered Java constructor to a user-visible managed constructor, the
generator silently falls back to the legacy (IntPtr, JniHandleOwnership)
activation path. This is the right runtime behaviour, but until now there
was no signal in the build output that a user-defined ctor body would not
run — making the failure mode (e.g. parameterless [Export] ctor body
silently skipped) very hard to diagnose.

- Add a CtorFallbackReason enum on JavaConstructorInfo capturing why the
  scanner could not bind a managed ctor: NoMatchingArity (the case fixed
  for Java.Lang.Thread+RunnableImplementor) or UnsupportedParameterType
  (generic / by-ref / pointer params).

- TryFindMatchingManagedCtorParams keeps scanning all same-arity ctors so
  an unsupported overload doesn't shadow a later usable one, only setting
  UnsupportedParameterType when no usable ctor was found.

- ITrimmableTypeMapLogger.LogUserCtorFallbackInfo surfaces the reason via
  the MSBuild task's LogDebugMessage (visible in -bl output), so silent
  legacy fallbacks are discoverable without any new logger dependency
  inside the scanner.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…kReason

Guards the RunnableImplementor-style activation-ctor fallback added in
this PR: a managed type whose only ctor is the (IntPtr, JniHandleOwnership)
activation ctor (or whose only user-visible ctors are parameterized) must
have HasMatchingManagedCtor=false on the inherited ()V Java ctor so the
generator falls back to activation rather than emitting a member ref to
a non-existent ..ctor(). Without these tests, a refactor of
TryFindMatchingManagedCtorParams could silently reintroduce the
MissingMethodException crash on Java.Lang.Thread+RunnableImplementor that
this PR fixed.

Also covers CtorFallbackReason: None for the happy path (MainActivity has
an explicit public ctor) and NoMatchingArity for the silent-fallback case.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Existing Phase A-C integration coverage exercises [Export] on classes
(static, throws, mixed [Register]+[Export], virtual+derived, custom JNI
name) and edge marshalling shapes, but not the case where [Export] sits
on a method that implements a [Register]'d interface method. That's a
distinct path: legacy bindings dispatch through the interface's invoker,
not directly on the implementor. With [Export] the implementor must
surface its own UCO marshal method so the user's opt-in actually wires
up.

Adds two scanner integration tests against the UserTypesFixture:

- ExportInterfaceImplShape: [Export("onClick")] on the IOnClickListener
  implementor. Scanner must produce a (Landroid/view/View;)V marshal
  method entry on the implementor with no real connector.

- ExportInterfaceRenameShape: [Export("onClickRenamed")] on the same
  interface implementation. Verifies the renamed entry is registered
  alongside (not collapsed into) the interface's onClick contract.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…m warnings + exclusion tripwire

Three new tests in TrimmableTypeMapBuildTests covering build-task-level
behaviour of the trimmable [Export] pipeline:

- Build_WithExportAndExportField_GeneratesJcwAndTypeMap: end-to-end build
  with [Export] and [ExportField] members. Asserts the generated JCW Java
  source contains a 'native' method declaration for [Export] and a field
  declaration for [ExportField], plus a *.TypeMap.dll is produced. Closes
  the gap between the host-only integration coverage of the scanner and
  the device tests in Mono.Android-Tests.

- Build_WithExport_ProducesNoTrimWarningsTargetingExportCodegen: builds
  the same shape in Release + TrimMode=full and asserts no IL2xxx /
  IL3xxx warning lines reference either the generated *.TypeMap.dll or
  the user's [Export] source. Targeted rather than total — unrelated
  framework warnings don't fail the test — so the trim baseline tracks
  regressions in our codegen specifically.

- TrimmableTypeMap_ExcludedTestNames_DoesNotGrow: tripwire that parses
  NUnitInstrumentation.cs and asserts the trimmable lane's
  ExcludedTestNames array has no more than 4 entries (today's count).
  Adding new exclusions silently degrades the trimmable CoreCLR test
  signal; this guard forces an explicit constant bump with rationale.
  See #11170.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Group B in ExportTests covers single-frame exception propagation through
the [Export] UCO wrapper (managed throw -> OnUserUnhandledException ->
SetPendingException -> RaisePendingException on managed return). The
wrapper's JniTransition state machine is novel codegen and its corner
cases — nested calls and back-to-back invocations — were not directly
exercised.

Adds two new tests:

- Export_Method_Throws_FollowedBySecondCall_DoesNotLeakPendingException:
  after a throwing [Export], a subsequent non-throwing [Export] on a
  different peer must return its real value (a stale pending-exception
  would either re-raise or corrupt the return).

- Export_Method_NestedJniCall_PreservesExceptionFromInnerExport: an
  outer [Export] uses JNI reflection to call an inner [Export] on the
  same peer that throws. The original 'reentrant-boom' message must
  propagate through both [Export] wrappers and reach the managed caller.

Both tests run under llvm-ir (which defines the contract) and trimmable
lanes, matching the rest of Group B.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ilename

The trimmable JCW generator puts user-peer Java files under a
crc64<hash>/ subdirectory keyed off the CRC64 of the type's package
path, so the file is rarely named after the C# class. Searching all
generated .java files for one that mentions both the [Export] method
and the [ExportField] initializer makes the assertion stable across
hash collisions and naming-convention changes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…no.Android.Export exclusion

Customer-facing summary of the trimmable typemap [Export] support added
in this PR. Covers:

- [Export]/[ExportField] is supported on Java.Lang.Object subclasses
  via [UnmanagedCallersOnly] wrappers — no DynamicMethod codegen at
  runtime.

- The Mono.Android.Export assembly is excluded from trimmable builds.
  Customers consuming only the [Export]/[ExportField] attribute types
  are unaffected; customers calling Mono.Android.Export APIs directly
  are not supported under the trimmable type map.

- User-visible managed constructors are now invoked when Java
  instantiates a peer via a registered Java constructor (closes the
  gap that broke Throwable subclass (Throwable cause) ctors and
  parameterized [Export] ctors).

- Unhandled managed exceptions thrown from [Export] are routed
  through JniRuntime.OnUserUnhandledException, preserving the
  original managed exception type and message. Differs from legacy
  AndroidEnvironment.UnhandledException which converted the
  exception into a Java.Lang.Throwable.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival simonrozsival force-pushed the dev/simonrozsival/trimmable-typemap-export-attribute branch from 424eb11 to fee4ce4 Compare May 11, 2026 14:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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