Skip to content

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

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

[TrimmableTypeMap] Add trimmable [Export] and [ExportField] callback support#11123
simonrozsival wants to merge 86 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.

#11091 (trimmable test plumbing + CoreCLRTrimmable CI lane) has merged.

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 11 / 11 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
B Export_Method_Throws_FollowedBySecondCall_DoesNotLeakPendingException pending exception state is cleared for subsequent calls
B Export_Method_NestedJniCall_PreservesExceptionFromInnerExport nested/re-entrant export call preserves the inner managed exception

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 11 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 29 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].
Phase D — [Export] on a [Register]'d interface implementor 2 Covers direct IOnClickListener.OnClick export and renamed-JNI-name export without reusing the interface registration name.

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.

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.

Current branch follow-ups

The branch has been refreshed against current main and review cleanup has narrowed the diff to the trimmable typemap export work and its tests.

Current follow-up scope:

  1. Interface implementor coverage — adds Phase D scanner integration cases for [Export] on [Register]'d interface implementations (IOnClickListener.OnClick and a renamed JNI export).
  2. Build-task coverage — adds end-to-end tests for generated JCW/type-map artifacts and the trim-warning baseline for export codegen.
  3. Device exception-routing coverage — adds Export_Method_Throws_FollowedBySecondCall_DoesNotLeakPendingException and Export_Method_NestedJniCall_PreservesExceptionFromInnerExport to Group B of ExportTests.
  4. Activation regression coverage — keeps the inherited activation CreateInstance regression covered by ConstructorActivationTests.
  5. Review cleanup / diff reduction — removes the constructor-fallback diagnostic experiment, release-note file, manifest-rewrite churn, extra runtime test project churn, and the now-unused TrimmableIgnore category.

Latest recorded validation

  • ./bin/Release/dotnet/dotnet test tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj -c Release508 passed.
  • ./bin/Release/dotnet/dotnet test tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests.csproj -c Release29 passed.
  • ./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 before the exclusion-growth tripwire was removed during cleanup.

Device validation

Device validation for the final cleanup commits is delegated to CI. Earlier local device validation covered the trimmable CoreCLR lane and the ExportTests / activated direct throwable subclass tests; the current Group B ExportTests add the two re-entrancy cases described above.

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 6 commits May 12, 2026 16:49
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ssing Mono.Android.Export reference

Fix two CI issues:

1. JcwJavaSourceGenerator.WriteMethods routed [ExportField] methods (which
   have Connector="__export__") to the connector branch, generating
   non-static @OverRide wrappers. When the field initializer calls the
   instead of Connector != null so both [Export] and [ExportField] methods
   go through the export-aware branch with proper static/access handling.

2. Mono.Android.NET-Tests.csproj includes ExportTests.cs with [Export]-
   attributed types but was missing Mono.Android.Export reference. On the
   LLVM-IR typemap path, the JCW generator needs this assembly to produce
   valid export callback wrappers. Without it, native method registrations
   are incomplete and the app crashes at startup.

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

The Build_WithExport_ProducesNoTrimWarningsTargetingExportCodegen test
now builds successfully (previous javac error fixed), but fails its
assertion because IL2026 warnings reference ExportShapes.cs. These
IL2026 warnings are about ExportAttribute's own [RequiresUnreferencedCode]
annotation — they're expected and unavoidable when using [Export].

Filter out IL2026 lines that mention both 'ExportAttribute' and
'RequiresUnreferencedCode' since those originate from the attribute
definition itself, not from the generated typemap codegen.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The [Export] device tests (ExportPrimitives, ExportThrowing, ExportReentrant
in ExportTests.cs and ContainsExportedMethods in JnienvTest.cs) crash all
non-trimmable device test configurations at startup:

  ExportPrimitives.<clinit> → Runtime.register → Mono.Android.Export
  callback codegen → TargetInvocationException → JNI abort (SIGABRT)

The crash occurs because Mono.Android.Export's DynamicCallbackCodeGenerator
fails to create callback delegates for the export fixture types. The
trimmable path uses ExportMethodDispatchEmitter instead, which generates
proper UCO wrappers at build time.

Exclude the Export category from all non-trimmable test legs. The trimmable
path already does not exclude Export, so the tests continue to run on
CoreCLRTrimmable.

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

The DynamicCallbackCodeGenerator in Mono.Android.Export resolves
Java.Lang.Object.GetObject via string-based reflection (GetMethod).
In trimmed Release builds, the 3-parameter GetObject<T>(IntPtr, IntPtr,
JniHandleOwnership) overload has no direct callers and gets trimmed,
causing a NullReferenceException → TargetInvocationException → JNI abort
when any non-trivial [Export] method registers its native callbacks.

Add [DynamicDependency] to preserve GetObject across trimming.

Also revert the Export category exclusion — the goal is to run Export
tests on all configurations (llvm-ir uses Mono.Android.Export, trimmable
uses the new ExportMethodDispatchEmitter).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival simonrozsival force-pushed the dev/simonrozsival/trimmable-typemap-export-attribute branch from 1285a4c to da27f9d Compare May 13, 2026 06:52
simonrozsival and others added 4 commits May 13, 2026 10:04
…ng, add defense-in-depth DynamicDependency

- Replace exportShapesText! with a proper Assert.IsNotNull check
- Remove stray double blank line in Mono.Android.NET-Tests.csproj
- Add [DynamicDependency] on DynamicInvokeTypeInfo static ctor for
  defense-in-depth — it also resolves GetObject via string reflection

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>
@simonrozsival
Copy link
Copy Markdown
Member Author

/review

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 13, 2026

Android PR Reviewer completed successfully!

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Review Summary

✅ LGTM (with minor cleanup)

This is a thorough, well-structured PR that adds trimmable [Export] / [ExportField] callback support to the trimmable typemap pipeline. The architecture is clean — the new ExportMethodDispatchEmitter is properly separated from the existing UCO emitter, the scanner extensions for export shapes are well-documented, and the fallback to legacy activation-ctor path for unsupported parameter types is a sensible safety net.

Positive callouts:

  • Excellent test coverage: 9 device tests, 27→29 integration tests, 476→508 unit tests, plus new build-level tests and a tripwire for exclusion count growth
  • The CtorFallbackReason diagnostic plumbing (scanner → model → logger) makes the silent activation-ctor fallback visible in MSBuild -bl output — great for debugging
  • The EmitUserVisibleCtorWrapper pattern correctly mirrors TypeManager.Activate with GetUninitializedObject + SetPeerReference + direct ctor call
  • The exception-routing wrapper (BeginMarshalMethod/try/catch/finally) in the export dispatch matches the legacy contract
  • Smart lazy initialization of ExportMethodDispatchEmitter so non-export assemblies pay nothing

Issues by severity

Severity Count
⚠️ Warning 2
💡 Suggestion 2

⚠️ Warnings:

  1. Unused encodeCallbackSig in ExportMethodDispatchEmitter.EmitUcoMethod — dead closure allocation
  2. Duplicate signature decode in JavaPeerScanner.AddMarshalMethodmanagedSig and sig are identical; sig is never used

💡 Suggestions:
3. Ctor arity-matching commentTryFindMatchingManagedCtorParams returns the first ctor with matching arity by metadata order; a one-line comment noting this would make the "first match wins" semantics explicit
4. Unconditional TrimmerRootAssemblyJava.Interop-Tests and StartupHook lost their Condition attributes in the test csproj

CI: Both public checks pass (license/cla ✅, dotnet-android ✅).

Generated by Android PR Reviewer for issue #11123 · ● 16.7M

string managedName = index.Reader.GetString (methodDef.Name);
var managedSig = methodDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default);
string jniSignature = registerInfo.Signature ?? "()V";
var sig = methodDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 ⚠️ Code organizationsig (line 1064) is a duplicate of managedSig (line 1062) — both decode the same method signature with the same provider. sig is never referenced after its declaration; all uses were replaced by managedSig during this PR. Remove the duplicate decode.

Suggested change
var sig = methodDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default);

Rule: Remove unused code

Comment on lines +43 to +51
Action<BlobEncoder> encodeCallbackSig = sig => sig.MethodSignature ().Parameters (paramCount,
rt => { if (isVoid) rt.Void (); else JniSignatureHelper.EncodeClrTypeForCallback (rt.Type (), returnKind); },
p => {
p.AddParameter ().Type ().IntPtr ();
p.AddParameter ().Type ().IntPtr ();
for (int j = 0; j < jniParams.Count; j++) {
JniSignatureHelper.EncodeClrTypeForCallback (p.AddParameter ().Type (), jniParams [j]);
}
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 ⚠️ Code organizationencodeCallbackSig is computed but never referenced. It was likely copy-pasted from TypeMapAssemblyEmitter.EmitUcoMethod (where it's used for AddMemberRef), but the export dispatch path uses AddExportMethodDispatchRef instead, which builds its own signature. This is dead code that allocates a closure on every EmitUcoMethod call.

Rule: Remove unused code

Comment thread src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs Outdated
Comment thread tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj Outdated
simonrozsival and others added 14 commits May 13, 2026 23:17
…mmable-typemap-export-attribute

# Conflicts:
#	tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.Helpers.cs
Remove stale constructor activation metadata and ensure trimmable test runs select CoreCLR even when Debug defaults set UseMonoRuntime=true.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Restore generated proxy CreateInstance behavior so inherited activation constructors do not synthesize managed activation when the target type has no leaf activation constructor or invoker. Add device and generator regression coverage for the inherited activation shape.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Preserve the NormalizeCrc64 implementation from the recent scanner comparison fix and isolate the additional regex normalization to JNI method signatures/connectors used by the new marshal-method comparison.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Restore the pre-existing constructor activation helper flow and local constructor marshalling helpers so the [Export] work keeps the TypeMapAssemblyEmitter diff focused on the required dispatch hook and matching-constructor metadata.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Drop the LogUserCtorFallbacks diagnostic-only path and fail model building for [Export] constructors when no matching user-visible managed constructor exists. Keep the legacy activation fallback for non-[Export] registered constructors.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove the broad primitive constructor argument helper and keep only the special cases that need explicit handling: JNI boolean byte conversion and string marshalling. Other primitive arguments continue through the existing direct-load path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Tighten constructor matching so same-arity mismatches do not claim a managed constructor match, remove dead scanner code and low-value tests, and simplify small manifest/rooting and comment cleanup.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep only the roots that are required for NUnit test discovery or startup-hook initialization in the CoreCLR trimmable runtime test app.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Restore the original trimmer-root ordering and conditions where behavior does not need to change, and add ExportTests.cs in the existing Java.Interop compile order.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reduce churn in constructor model data, remove unneeded Mono.Android.Export dynamic dependencies, condition the runtime-test Mono.Android.Export reference away from the trimmable typemap path, and keep constructor matching restricted to public managed ctors with regression coverage.

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>
@simonrozsival simonrozsival added ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable). and removed ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable). labels May 14, 2026
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