Skip to content

Enable trimmable typemap for Mono.Android.NET-Tests#11087

Closed
simonrozsival wants to merge 34 commits intomainfrom
dev/simonrozsival/enable-trimmable-type-map-in-tests
Closed

Enable trimmable typemap for Mono.Android.NET-Tests#11087
simonrozsival wants to merge 34 commits intomainfrom
dev/simonrozsival/enable-trimmable-type-map-in-tests

Conversation

@simonrozsival
Copy link
Copy Markdown
Member

Summary

Adds a single MSBuild property MonoAndroidTypeMapFlavor=legacy|trimmable to run Mono.Android.NET-Tests with the trimmable typemap on CoreCLR, and adds a separate CoreCLRTrimmable CI lane.

Test results: 792 passed, 0 failed, 6 skipped (vs legacy: 885 passed, 2 failed, 6 skipped)

Changes by area

1. Generator fixes (Scanner/JCW/Emitter)

  • CRC64 mismatch fix: Generator used System.IO.Hashing.Crc64 but runtime uses Jones CRC64 (Crc64Helper)
  • Inherited override detection: Walk past DoNotGenerateAcw intermediate MCW base types
  • Instrumentation targetPackage: Pass package name for manifest instrumentation elements
  • JCW lazy registerNatives: Application/Instrumentation types use deferred registration
  • Generic proxy names: Sanitize backticks, emit no-op UCO for open generics
  • UTF-8 helpers: Changed from NestedPrivate to NestedAssembly to fix FieldAccessException
  • Manifest name promotion: Promote compat JNI names for manifest-rooted components

2. UCO constructor refactoring

  • Inline activation ctor: UCO wrappers directly call the activation ctor instead of going through ActivateInstance → typemap re-lookup
  • WithinNewObjectScope guard: Prevents double peer creation when objects are constructed from managed code
  • ControlFlowBuilder: Added useBranches parameter to PEAssemblyBuilder.EmitBody
  • Removed: TrimmableNativeRegistration wrapper, ActivateInstance method

3. Runtime trimmable typemap support

  • TrimmableTypeMap.TryGetType: Unwraps proxy to return real managed type
  • TryGetJniName: Fallback chain: [Register] → compat Android component names → JavaNativeTypeManager.ToJniName
  • GetProxyForPeer: Walks actual JNI class hierarchy with caching
  • CreatePeer: Added IsAssignableFrom validation to prevent invalid JavaCast peers
  • TypeManager/JNIEnv: Safety guards for trimmable mode in legacy APIs
  • JNIEnvInit: JniRuntime.SetCurrent for background thread support

4. Test plumbing and CI

  • MonoAndroidTypeMapFlavor: Single MSBuild property switch in test csproj
  • CI lane: Mono.Android.NET_Tests-CoreCLRTrimmable in stage-package-tests.yaml
  • Category exclusions: NativeTypeMap, TrimmableIgnore, SSL
  • Java.Interop-Tests exclusions: Fixtures using JavaObject types (not Java.Lang.Object)

Proposed PR split

This PR is large and should be split into 4 vertical slices:

  1. Generator fixessrc/Microsoft.Android.Sdk.TrimmableTypeMap/ + unit tests
  2. UCO refactoringTypeMapAssemblyEmitter.cs, PEAssemblyBuilder.cs (depends on 1)
  3. Runtime fixessrc/Mono.Android/ (depends on 2)
  4. Test plumbing + CItests/Mono.Android-Tests/, build-tools/ (depends on 3)

Tests excluded for trimmable (93 tests)

  • NativeTypeMap (2): directly call native typemap P/Invokes
  • TrimmableIgnore (2): NewOpenGenericTypeThrows, ActivatedDirectThrowableSubclassesShouldBeRegistered
  • SSL (~20): emulator networking issue, not trimmable-specific
  • Java.Interop-Tests fixtures (~69): use JavaObject types whose JCW Java classes are not generated

simonrozsival and others added 30 commits April 7, 2026 08:11
Parse the user's AndroidManifest.xml template for activity, service, receiver,
and provider elements with android:name attributes. Mark matching scanned
Java peer types as IsUnconditional = true so the ILLink TypeMap step preserves
them even if no managed code references them directly.

Changes:
- JavaPeerInfo.IsUnconditional: init → set (must be mutated after scanning)
- TrimmableTypeMapGenerator: add warn callback, RootManifestReferencedTypes()
  called between scanning and typemap generation
- GenerateTrimmableTypeMap task: pass Log.LogWarning as warn callback
- 4 new xUnit tests covering rooting, unresolved warnings, already-unconditional
  skip, and empty manifest

Replaces #11016 (closed — depended on old PR shape with TaskLoggingHelper).

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>
- Remove file-path overload (IO belongs in MSBuild task, not generator)
- Accept XDocument? directly, handle null with pattern match
- Add ResolveManifestClassName to resolve dot-relative (.MyActivity)
  and simple (MyService) names against the manifest package attribute
- Fix '$' handling in peer lookup: manifests use '$' for nested types,
  don't replace it with '.' in the lookup key

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>
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>
The generated TypeMap assembly emits IL that references
Android.Runtime.TrimmableNativeRegistration.ActivateInstance(IntPtr, Type)
in Mono.Android. This class was referenced by TypeMapAssemblyEmitter but
never created in this branch, causing a TypeLoadException at runtime when
any UCO constructor wrapper (nctor_*_uco) was invoked — for example when
managed code creates an ACW instance like RunnableImplementor.

The new class is a thin wrapper that delegates to the existing
Microsoft.Android.Runtime.TrimmableTypeMap.ActivateInstance, which is
internal to Mono.Android. The generated TypeMap assembly accesses
TrimmableNativeRegistration via [IgnoresAccessChecksTo("Mono.Android")].

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add a single MSBuild property MonoAndroidTypeMapFlavor=legacy|trimmable to
run Mono.Android.NET-Tests with the trimmable typemap on CoreCLR. Add a
separate CoreCLRTrimmable package-test CI lane.

Fix many runtime bugs exposed by running real tests with trimmable CoreCLR:
- Fix native SIGSEGV by routing trimmable typemap lookups through the managed
  TrimmableTypeMap instead of native libxamarin-app.so P/Invokes
- Fix open generic type activation crash in [UnmanagedCallersOnly] UCO
  constructor wrappers by checking IsGenericTypeDefinition early and using
  RaisePendingException instead of throwing across the JNI boundary
- Fix CRC64 package name mismatch between generator and runtime by reusing
  the Java.Interop Jones CRC64 algorithm in the trimmable scanner
- Add PeekObject/WithinNewObjectScope guards to ActivateInstance to match
  legacy TypeManager.n_Activate behavior
- Set JniRuntime.SetCurrent() during initialization for background threads

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Remove unreachable trimmable branches from TypeManager.GetJavaToManagedTypeCore
  and JNIEnv.TypemapManagedToJava (these codepaths are never hit because
  TrimmableTypeMapTypeManager and JavaMarshalValueManager handle all lookups)
- Remove null guard on TrimmableTypeMap.Instance in JavaMarshalValueManager.CreatePeer
  (null Instance is a fatal initialization error, not a recoverable case)
- Remove '?? mappedType' fallback in TryGetType — if the proxy attribute is missing,
  the entry is invalid and should not be returned
- Remove TypeManager.GetJniTypeName fallback from TryGetJniName to avoid depending
  on the legacy TypeManager (which we want to eventually delete)
- Add JNI class name caching to GetProxyForPeer via _peerProxyCache

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The generated typemap assemblies already have [IgnoresAccessChecksTo("Mono.Android")]
so they can call TrimmableTypeMap.ActivateInstance directly. The TrimmableNativeRegistration
wrapper class was an unnecessary indirection.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The generated nctor_*_uco wrappers now directly call the activation
constructor instead of going through TrimmableTypeMap.ActivateInstance.
Each UCO already knows the exact type to create at build time, so the
typemap re-lookup was unnecessary indirection.

- EmitUcoConstructor now emits the same ctor call that CreateInstance
  uses (newobj for leaf types, GetUninitializedObject+call for inherited),
  handling both XamarinAndroid and JavaInterop ctor styles
- Skip UCO constructor generation for open generic type definitions
  (they can't be activated from Java)
- Remove ActivateInstance from TrimmableTypeMap (no longer called)
- Remove _trimmableNativeRegistrationRef and _activateInstanceRef fields

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
NativeTypeMap tests call TypeManager.GetJavaToManagedType and
JNIEnv.TypemapManagedToJava directly, which call into the native
typemap tables that don't exist in the trimmable path. Exclude
these tests when MonoAndroidTypeMapFlavor=trimmable.

Also add defensive guards in both methods to delegate to the
managed TrimmableTypeMap when RuntimeFeature.TrimmableTypeMap is
enabled, preventing crashes if these APIs are called outside of
the excluded test categories.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Open generic types (e.g., GenericHolder<T>) cannot be activated from Java
because the type parameters are unknown. The ModelBuilder already skips
generating UCO constructor wrappers for these, but the JCW Java source
generator still emitted the 'private native void nctor_0()' declaration
and 'nctor_0(args)' call in the constructor. This caused UnsatisfiedLinkError
at runtime because no native method was registered.

Now the JCW generator checks IsGenericDefinition and skips both the nctor
call in the constructor body and the native method declaration.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When a managed type is constructed via JNIEnv.StartCreateInstance/NewObject,
the JCW constructor calls nctor_0 which invokes the UCO. Without this guard,
the UCO creates a second managed peer instance for the same Java object handle,
corrupting JNI references and causing GC crashes (SIGSEGV in ART's
ConcurrentCopying::FlipThreadRoots).

The fix mirrors the legacy TypeManager.n_Activate behavior: check
JniEnvironment.WithinNewObjectScope and skip activation when true.
Updated PEAssemblyBuilder.EmitBody to accept useBranches parameter
for ControlFlowBuilder support.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Revert the JCW nctor skip for generics — the nctor_0 native declaration
and call must still exist to avoid UnsatisfiedLinkError. Instead, emit a
no-op UCO constructor wrapper for open generic type definitions that just
returns immediately. The test NewOpenGenericTypeThrows expects the JCW
constructor to be called (it triggers FinishCreateInstance which handles
the open generic case).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The no-op UCO for open generic types must call
ThrowIfOpenGenericActivation() to raise NotSupportedException when
called outside WithinNewObjectScope (i.e., from FinishCreateInstance).
This matches the legacy TypeManager.n_Activate behavior that the
NewOpenGenericTypeThrows test expects.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival and others added 4 commits April 8, 2026 11:27
The generic type's UCO is shared across all closed instantiations
(GenericHolder<int>, GenericHolder<string>, etc.), so it can't
distinguish open vs closed. ThrowIfOpenGenericActivation breaks
NewClosedGenericTypeWorks. Revert to no-op UCO and exclude the
legacy NewOpenGenericTypeThrows test for trimmable via TrimmableIgnore.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move the trimmable ExcludeCategories to after the MonoAndroidTypeMapFlavor
property group so it appends to categories already set by UseMonoRuntime
conditions. Without this, the NativeTypeMap/TrimmableIgnore/SSL exclusions
were evaluated before UseMonoRuntime was derived from the flavor.

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

TrimmableTypeMap.CreatePeer was creating peers for incompatible Java types
(e.g., IAppendableInvoker wrapping java.lang.Integer) because the
GetProxyForManagedType fallback didn't validate Java-side IsAssignableFrom.
Added the check to match the legacy base.CreatePeer behavior.

Also exclude JavaObjectTest.Dispose and Dispose_Finalized by name when
running in trimmable mode — these tests create JavaObject types whose JCW
Java classes don't exist in the trimmable APK, causing ClassNotFoundException
on background threads that crash the process.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Exclude Java.Interop-Tests fixtures that use JavaObject types (not
Java.Lang.Object) whose JCW Java classes don't exist in the trimmable
APK: JavaObjectTest, InvokeVirtualFromConstructorTests,
JniPeerMembersTests, JniTypeManagerTests, JniValueMarshaler tests,
and JavaExceptionTests.InnerExceptionIsNotAProxy.

Also exclude ActivatedDirectThrowableSubclassesShouldBeRegistered
(uses legacy n_Activate default-ctor flow that differs from trimmable
UCO activation-ctor) and JavaCast_BaseToGenericWrapper (generic
JavaList cast needs further investigation).

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

Superseded by the 4-PR split: #11088#11089#11090#11091

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant