Skip to content

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

Open
simonrozsival wants to merge 68 commits intomainfrom
dev/simonrozsival/trimmable-typemap-export-attribute
Open

[TrimmableTypeMap] Add trimmable [Export] and [ExportField] callback support#11123
simonrozsival wants to merge 68 commits intomainfrom
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 test runs under both the legacy llvm-ir typemap (which defines the contract) and the trimmable typemap (which must match it). Verified locally: all 11 / 11 Export tests 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: the legacy JCW emitter CecilImporter.GetJniSignature rejects these same types when generating Java callable wrappers (it returns null, which fails the build), so the new marshalling paths are not yet exercised end-to-end through real JCW callbacks. Teaching the JCW emitter to widen these types is filed as a separate item.

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 on lines +210 to +217
_baseCtorRef = _pe.AddMemberRef (_javaPeerProxyRef, ".ctor",
sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2,
rt => rt.Void (),
p => {
p.AddParameter ().Type ().Type (_systemTypeRef, false);
p.AddParameter ().Type ().Type (_systemTypeRef, false);
}));

Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

_baseCtorRef is assigned but never used, and the encoded .ctor signature here doesn’t match JavaPeerProxy<T> (it encodes two System.Type parameters instead of string + Type). This should be removed to avoid dead/incorrect metadata and to prevent future accidental use of a wrong member reference.

Suggested change
_baseCtorRef = _pe.AddMemberRef (_javaPeerProxyRef, ".ctor",
sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2,
rt => rt.Void (),
p => {
p.AddParameter ().Type ().Type (_systemTypeRef, false);
p.AddParameter ().Type ().Type (_systemTypeRef, false);
}));

Copilot uses AI. Check for mistakes.
@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 and others added 5 commits April 24, 2026 19:10
… run

Fix several trimmable typemap generator and runtime bugs that prevented
Mono.Android.NET-Tests from passing in Release+CoreCLR+trimmable mode:

Generator:
- Use (managedName, assemblyName) tuple as scanner dictionary key to prevent
  duplicate-type crashes when two assemblies define the same managed type name
  (e.g. Java.Lang.Throwable in both Java.Interop and Mono.Android)
- Add MergeCrossAssemblyAliases to propagate aliases across assembly boundaries
  before splitting peers into per-assembly typemap universes (Release only)
- Emit TypeMapAssociationAttribute for all entries with proxies so the runtime
  proxy type map is populated correctly (fixes CreatePeer for interface types)
- ForceUnconditionalEntries=true workaround for dotnet/runtime#127004 (trimmer
  strips TypeMapAssociation attributes when TypeMap references the same type)
- Fix invalid ParameterHandle (was default/row-0); use valid 1-based row index
- Skip inner per-RID builds in _GenerateTrimmableTypeMap (they lack the full
  assembly set needed for correct deferred-registration propagation)

Runtime:
- Add JavaPeerProxy.ShouldSkipActivation(IntPtr) to detect existing managed
  peers and prevent duplicate peer creation during UCO constructor callbacks
- Use ShouldSkipActivation in UCO nctor callbacks instead of WithinNewObjectScope

Build:
- Add _PrepareTrimmableNativeConfigAssemblies target to populate native config
  with typemap DLL paths (must run unconditionally, not inside _GenerateJavaStubs)
- Add typemap DLLs to _ShrunkAssemblies to keep _RemoveRegisterAttribute counts
  in sync with _ResolvedAssemblies

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Configure Mono.Android.NET-Tests to build and run successfully with
_AndroidTypeMapImplementation=trimmable and UseMonoRuntime=false.

Changes:
- Root test and runner assemblies in TrimmerRoots.xml so NUnit can
  discover and execute tests after trimming
- Exclude 48 tests that are incompatible with the trimmable typemap
  (tracked in #11170):
  - 5 tests that require net.dot.jni.test.*/net.dot.jni.internal.* Java classes
  - 24 tests relying on JavaProxyObject/JavaProxyThrowable (not in APK)
  - 4 proxy resolution / trimmer gaps
  - 4 JavaCast proxy resolution failures
  - 4 open generic type / registration failures
  - 4 JNI method remapping not supported
  - 3 other
- Exclusions are guarded by RuntimeFeature.TrimmableTypeMap so they
  only apply to trimmable builds
- Add TODO comments to tests with known trimmable-specific failures
  (issue #11170) so they are visible to contributors

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add a new CI lane that runs Mono.Android.NET-Tests with the trimmable
typemap implementation on CoreCLR:
  _AndroidTypeMapImplementation=trimmable
  UseMonoRuntime=false

The lane runs after the existing CoreCLR lane and uses the same APK
artifact from the CoreCLRTrimmable build flavor.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
In non-trimmed (Debug) builds, _AndroidEnableObjectReferenceLogging defaults
to true, so RuntimeFeature.ObjectReferenceLogging returns true. The existing
Meter test already uses the same #if DEBUG pattern; apply it here too.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The feature switch (ObjectReferenceLoggingEnabledByDefault) is the source of
truth; its default value varies by build configuration (true in non-trimmed
Debug builds, false in trimmed Release builds). Hardcoding an expected value
in the test is fragile. Remove the test case entirely.

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

Blocked by #11091

simonrozsival and others added 13 commits April 26, 2026 14:24
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fix RootManifestReferencedTypes to resolve relative android:name
  values (.MyActivity, MyActivity) using manifest package attribute
- Keep $ separator in peer lookup keys so nested types (Outer$Inner)
  match correctly against manifest class names
- Guard Path.GetDirectoryName against null return for acw-map path
- Fix pre-existing compilation error: load XDocument from template
  path before passing to ManifestGenerator.Generate
- Add tests for relative name resolution and nested type matching

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>
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>
simonrozsival and others added 27 commits April 27, 2026 07:27
Revert pure cosmetic/tangential changes from the rebase:

* TypeMapAssemblyEmitter.cs: restore EmitRegisterNatives to its original
  location with its descriptive comments intact (it had been moved
  earlier in the file and stripped of comments). Also revert a few
  unrelated whitespace/brace-style changes (extra blank line before a
  closing brace, gratuitous `for { ... }` brace insertions, indentation
  damage in the generic-proxy ctor signature lambda).
* TrimmableTypeMapGenerator.cs: revert a cosmetic brace-style change on
  PropagateDeferredRegistrationToBaseClasses; the surrounding
  refactoring stays because it's required by the new manifest-rewriting
  feature on this PR.
* GenerateNativeApplicationConfigSources.cs: revert `using` reordering
  and `ITaskItem[]?` -> `ITaskItem []?` whitespace; only the new
  ShouldSkipAssembly helper + its two call sites are kept.
* Mono.Android.NET-Tests.csproj: drop the unused
  `;TRIMMABLE_TYPEMAP` define constant. Nothing in the codebase
  references it; the runtime feature switch covers the build-time
  selection instead.

All 453 generator unit tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The trimmable typemap path uses managed JniEnvironment.Types.RegisterNatives
directly from the generated proxy types' RegisterNatives method, so the
native JNI shim added during the rebase was not needed. Runtime registration
of natives is already solved via the managed code path on this branch.

Reverts the native bits to match base verbatim:
* src/native/clr/host/host-jni.cc
* src/native/clr/include/host/host-jni.hh
* src/native/clr/libnet-android.map.txt

Verified end-to-end: full Release build + on-device test run with
_AndroidTypeMapImplementation=trimmable, UseMonoRuntime=false:
917 tests / 0 errors / 0 failures / 57 ignored.

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

These methods were unintentionally moved earlier in the file during the
rebase, which made the diff against base look like a big delete + big add
of identical content (no clear signal of actual changes).

Restore them to their pre-PR positions (after EncodeUcoConstructorLocals_JavaInterop)
so the diff against base shows only genuine additions: the new
[Export] dispatch wiring, member refs, comments, and the parameterless
UCO ctor branch.

No behavior change. 453/453 unit tests pass.

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

The previous comment leaked a test-fixture detail (`Constructed = true`) into
the implementation explanation. Rephrase to describe the general invariant
("user-visible ctor body runs when the peer is created from the Java side")
and explicitly point at the safety guard that makes the approach safe:
`if (PeerReference.IsValid) return;` in Java.Lang.Object's chain, which
prevents the user-visible ctor's :base() from creating a second Java peer.

No code change.

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

- TypeMapAssemblyEmitter: lazy-initialize ExportMethodDispatchEmitter via
  GetExportMethodDispatchEmitter() so we only pay for it in assemblies
  that actually contain [Export]-attributed methods.
- Inline the parameterless ctor MemberRef helper at its single call site
  (it was a 1-use helper).
- TrimmableTypeMapGenerator: hoist the loop-invariant 'if
  (deferredRegistration)' check out of the per-peer foreach to make the
  intent clearer (it applies to all peers of a manifest entry).
- NUnitInstrumentation: drop the duplicate ExcludedCategories assignment
  (csproj is the source of truth for category exclusions). Drop the
  Java.InteropTests.JavaObjectTest.Dispose_Finalized exclusion - the
  Java.Interop submodule realignment with main makes this pass again.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
These tests lock in the legacy llvm-ir typemap behavior for
parameterized ctor activation from the Java side: when Java
instantiates a managed subclass via JNIEnv.StartCreateInstance with
non-()V signatures, the user-visible managed ctor body must run with
the JNI args correctly marshalled.

Three new test classes derive from java.lang.Throwable to exploit its
registered ctor surface ("()V", "(Ljava/lang/String;)V",
"(Ljava/lang/String;Ljava/lang/Throwable;)V"):

* StringActivatedFromJava           — single ref-arg ctor
* StringThrowableActivatedFromJava  — multi ref-arg ctor
* MultiCtorActivatedFromJava        — multiple registered ctors,
                                      verifies dispatch correctness

Each test exercises both StartCreateInstance and FinishCreateInstance
with the JNI args, then asserts that the corresponding managed ctor
recorded the args on the instance. Under llvm-ir these tests pass
(TypeManager.Activate reflectively invokes the matching managed ctor).
Under trimmable they currently fail for non-()V signatures because
EmitUcoConstructor ignores the JNI args — this is the regression a
follow-up commit will address.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Legacy mono.android.TypeManager.Activate routes ctor args through
JNIEnv.GetObjectArray, which only supports IJavaObject-derived element
types. The original string-arg tests would have failed under llvm-ir
itself (not just trimmable), so they don't capture a useful contract.

Use Java.Lang.Throwable args instead so the tests stay inside the
supported legacy contract while still exercising single ref-arg, multi
ref-arg, and ctor-dispatch scenarios.

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

The trimmable typemap's UCO ctor codegen mirrors TypeManager.Activate's
"run the user-visible ctor body so user-defined initialization executes"
behavior by emitting:

    var obj = (T) RuntimeHelpers.GetUninitializedObject (typeof (T));
    ((IJavaPeerable) obj).SetPeerReference (new JniObjectReference (self));
    obj..ctor ();

This is correct only when T actually defines a parameterless managed
ctor. Some types — e.g. `Java.Lang.Thread+RunnableImplementor` —
register a `()V` Java ctor via JCW codegen but only define
parameterized managed ctors (`(Action)`, `(Action, bool)`). For those,
emitting a member ref to `T..ctor()` resolves to a non-existent method
at runtime, producing `MissingMethodException` and a SIGSEGV when Java
calls into the UCO wrapper (e.g. via `Handler.Post(Action)`).

Fix: plumb a `HasMatchingManagedCtor` bool from the scanner through the
model to the emitter. The scanner now decodes `TypeDefinition` to check
whether a parameterless `.ctor` actually exists before claiming the UCO
should call it. The emitter's user-ctor branch is gated on
`uco.JniSignature == "()V" && uco.HasMatchingManagedCtor`; otherwise
we fall through to the legacy activation-ctor (`(IntPtr,
JniHandleOwnership)`) path.

Test coverage:
* Existing `Generate_UcoConstructor_Parameterless_InvokesUserVisibleCtorViaSetPeerReference`
  continues to pass; `MakeAcwPeer` now defaults
  `HasMatchingManagedCtor = true`.
* New `Generate_UcoConstructor_Parameterless_NoMatchingManagedCtor_FallsBackToActivationCtor`
  locks in the fallback IL shape (call-or-newobj of the activation ctor,
  no call to the user ctor).

Verified locally: 454 unit tests pass; `make all CONFIGURATION=Release`
+ trimmable CoreCLR device tests yield 976 passes with the
RunnableImplementor crash gone — only the parameterized-ctor tests
intentionally added in 172f6ca still fail (expected; tracked in
the next phase of EmitUcoConstructor work).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Previously the trimmable UCO constructor codegen only mirrored
TypeManager.Activate semantics for the parameterless `()V` Java
constructor; any other registered Java ctor signature silently fell
through to the legacy activation-ctor path, which adopts the JNI handle
but never invokes the user-visible managed ctor body.

Extend EmitUcoConstructor to handle Java ctors whose JNI parameter list
is composed entirely of object references (`L...;`), and for which the
managed type defines a matching .ctor. The emitter now generates IL
that mirrors the reflection-based activator:

  var obj = (TargetType) RuntimeHelpers.GetUninitializedObject (typeof (TargetType));
  ((IJavaPeerable) obj).SetPeerReference (new JniObjectReference (self));
  obj..ctor (
    (TParam0) Java.Lang.Object.GetObject (arg0, JniHandleOwnership.DoNotTransfer, typeof (TParam0)),
    ...
  );

The scanner side (JavaPeerScanner.TryFindMatchingManagedCtorParams)
locates the matching managed .ctor and records its parameter types on
JavaConstructorInfo.ManagedParameterTypes; this list is plumbed through
ModelBuilder onto UcoConstructorData.

Java.Lang.Object.GetObject (IntPtr, JniHandleOwnership, Type) is
internal; it is reachable from the generated assembly via the always-on
[IgnoresAccessChecksTo("Mono.Android")] attribute that ModelBuilder
emits.

Primitive JNI args (Z/B/C/S/I/J/F/D) are not yet supported and continue
to fall through to the legacy activation-ctor path; the scanner returns
null for any signature containing a non-Object JNI param so the emitter
takes the safe fallback.

This fixes the previously-failing device tests:
  Java.InteropTests.JnienvTest.ActivatedDirectThrowableSubclasses_ThrowableCtor_ShouldForwardArgs
  Java.InteropTests.JnienvTest.ActivatedDirectThrowableSubclasses_MultipleCtors_ShouldDispatchToCorrectCtor

Verified locally:
  * 454 unit tests pass
  * Mono.Android.NET-Tests under _AndroidTypeMapImplementation=trimmable
    UseMonoRuntime=false: 919 passed / 0 failed / 56 ignored

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Tidy-ups in the parameterized-ctor support landed in the previous
commit:

* Use the C# 12 collection literal `[]` for the empty default of
  `IReadOnlyList<TypeRefData>` (matches the surrounding code style for
  the other `IReadOnlyList<...>` defaults in JavaPeerInfo.cs and
  TypeMapAssemblyData.cs) instead of `Array.Empty<T> ()`.
* Replace the manual `new TypeRefData [n] + for-copy` of
  `MethodSignature<TypeRefData>.ParameterTypes` with a collection
  literal spread (`[.. sig.ParameterTypes]`) that produces a
  `TypeRefData[]` directly.
* Extract the `allRefs` boolean+break loop into a small helper,
  `AllParametersAreReferenceTypes`, so the matching method reads as a
  flat sequence of guards.

Functionally a no-op:
  * 454 unit tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two small clarity tidy-ups in TypeMapAssemblyEmitter:

* Update the stale comment at the top of EmitUcoConstructor that
  claimed JNI ctor parameters are never forwarded — they now are, on
  the user-visible ctor path added in the previous commit.
* Extract the user-visible ctor wrapper IL emit (~50 lines) into a
  dedicated EmitUserVisibleCtorWrapper helper with an XML doc that
  shows the C# shape of the generated body. EmitUcoConstructor now
  calls into it as a single line, mirroring how the JavaInterop and
  legacy activation-ctor branches read.

Functionally a no-op:
  * 454 unit tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Previously, the trimmable user-visible-ctor wrapper only handled object-ref
JNI args by inlining a Java.Lang.Object.GetObject + castclass dance, and the
scanner refused to match any managed ctor whose JNI signature contained
primitive params. Both restrictions were self-imposed; the export method
dispatch emitter already handles primitives (with byte → bool conversion),
strings (via JNIEnv.GetString), arrays, and object peers.

Delegate per-arg marshalling in EmitUserVisibleCtorWrapper to
ExportMethodDispatchEmitter.LoadManagedArgument, drop the JNI-Object-only
restriction and the AllParametersAreReferenceTypes / IsPrimitiveTypeRef
helpers in JavaPeerScanner.TryFindMatchingManagedCtorParams. The duplicate
Java.Lang.Object.GetObject member ref previously declared in
TypeMapAssemblyEmitter is removed in favour of the one already owned by
ExportMethodDispatchEmitterContext.

Add IL-level regression tests covering object-ref, primitive int, bool
(with byte→bool conv), string, mixed (int + Throwable), and the
HasMatchingManagedCtor=false fallback to the legacy activation ctor.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ExportTests.cs
exercising 9 [Export] method shapes that the existing JnienvTest suite did
not cover end-to-end. Each test uses JNIEnv.GetMethodID + Call*Method to
drive the Java side of an [Export]-bearing peer and assert what C# observed,
running under both the legacy llvm-ir typemap and the trimmable typemap.

Group A (parameter / return marshalling):
  - Export_Method_Primitive_RoundTrip      (int -> int)
  - Export_Method_Bool_RoundTrip           (bool -> bool, byte/bool ABI)
  - Export_Method_String_RoundTrip         (string -> string)
  - Export_Method_PeerArg_RoundTrip        (Java.Lang.Object arg)
  - Export_Method_PeerArg_NullArg_HandledGracefully
  - Export_Method_IntArray_RoundTrip_AndCopyBack (int[] arg + copy-back)
  - Export_Method_PeerArray_RoundTrip      (Java.Lang.Object[] arg/return)

Group B (exception routing, marked TrimmableIgnore until the trimmable
[Export] UCO mirrors the marshal-method exception wrapper):
  - Export_Method_Throws_PrimitiveReturn_SurfacesAsJavaException
  - Export_Method_Throws_ObjectReturn_SurfacesAsJavaException

Verified locally: 9 / 9 non-throws Export tests pass on
`_AndroidTypeMapImplementation=trimmable` + `UseMonoRuntime=false` on
arm64 emulator. The two throws tests are intentionally TrimmableIgnore'd
until the trimmable codegen wraps [Export] UCOs in
BeginMarshalMethod / OnUserUnhandledException / EndMarshalMethod.

Out of scope (deferred to follow-up codegen work):
  - enum / IList / ICharSequence return marshalling — JCW emitter
    (CecilImporter.GetJniSignature) returns null and the build fails
    for both typemaps.
  - [ExportField] runtime visibility — JCW emits a static field
    initializer that calls the [ExportField] method as a non-static
    member, which fails javac for static C# methods.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Mirrors the trimmable UCO ctor wrapper: BeginMarshalMethod / try /
catch (route through JniRuntime.OnUserUnhandledException) /
finally (EndMarshalMethod). Without this, an unhandled managed
exception thrown from an [Export] method body aborts the CoreCLR
process instead of surfacing as a Java exception.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
JavaPeerScanner.TryFindMatchingManagedCtorParams now returns null
when any parameter has a generic, by-ref, or pointer type — falling
back to the (IntPtr, JniHandleOwnership) activation-ctor path,
matching legacy semantics. Fixes a pre-existing build failure on
Xamarin.Android.NUnitLite's TestDataAdapter ctor whose JavaList<T>
parameter triggered XAGTT7015 under the trimmable typemap.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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>
Base automatically changed from dev/simonrozsival/trimmable-test-plumbing to main April 27, 2026 21:15
…xport-attribute

Resolved conflicts:
- src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs:
  kept the 2-param JniObjectReference..ctor signature (both branches converged)
  and the new IJavaPeerable.SetPeerReference member ref added on this branch.
- tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs:
  kept this branch's superset (5 MergeCrossAssemblyAliases tests +
  3 RootManifestReferencedTypes tests); main added only the 5 dup tests.
- tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj:
  kept this branch's TrimmableIgnore-based exclusion (replaces main's
  blanket :Export category exclusion which we no longer need).
- tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/NUnitInstrumentation.cs:
  kept this branch's superset of name-based exclusions.

Verified: 468/468 unit tests, 27/27 integration tests pass.

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

/android-reviewer

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.

2 participants