Skip to content

[TrimmableTypeMap] Fix trimmable Java object activation#11275

Open
simonrozsival wants to merge 6 commits into
mainfrom
trimmable-object-activation
Open

[TrimmableTypeMap] Fix trimmable Java object activation#11275
simonrozsival wants to merge 6 commits into
mainfrom
trimmable-object-activation

Conversation

@simonrozsival
Copy link
Copy Markdown
Member

@simonrozsival simonrozsival commented May 3, 2026

Summary

  • completes Java-side constructor activation for the trimmable typemap by emitting constructor UCO bodies that directly activate managed peers, set the JNI peer reference/identity hash, and mark temporary activation peers replaceable
  • supports default, parameterized, existing-peer, invoker, and exception paths without a parameterless-constructor special case; constructor metadata now tracks whether each registered Java constructor has a matching managed constructor
  • expands constructor parameter marshalling coverage for primitives, strings, Java objects, arrays, nested arrays, null values, and multi-argument constructors
  • keeps legacy typemap behavior explicit in tests: cases that legacy TypeManager.n_Activate cannot marshal are ignored there, while trimmable typemap validates the new generated UCO path

Contribution to the trimmable typemap epic

This PR closes the Java object/constructor activation gap required for trimmable typemap parity. Generated native callbacks can now create or re-enter the correct managed peer for Java-initiated construction instead of falling back to unsupported reflection/native-member registration paths, which moves the trimmable typemap closer to replacing the legacy runtime typemap for app/device execution.

Validation

  • ./dotnet-local.sh test tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj -v:minimal — 465 passed
  • MSBUILDDISABLENODEREUSE=1 ./dotnet-local.sh msbuild tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj -restore -t:RunTestApp -p:Configuration=Debug -p:_AndroidTypeMapImplementation=llvm-ir -p:UseMonoRuntime=true -p:TestFixture=Java.InteropTests.ConstructorActivationTests -nr:false -v:minimal — constructor fixture passed; legacy-incompatible cases ignored by design
  • MSBUILDDISABLENODEREUSE=1 ./dotnet-local.sh msbuild tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj -restore -t:RunTestApp -p:Configuration=Debug -p:_AndroidTypeMapImplementation=trimmable -p:UseMonoRuntime=false -p:TestFixture=Java.InteropTests.ConstructorActivationTests -nr:false -v:minimal — all 35 constructor activation cases passed; command exits nonzero only because the target still runs unrelated tests and JavaObjectTest.DisposeAccessesThis currently fails in the trimmable path
  • rm -rf src/Mono.Android/obj/Release && ./dotnet-local.sh restore Xamarin.Android.sln -p:Configuration=Release -v:minimal && ./dotnet-local.sh restore src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Xamarin.Android.Build.Tests.csproj -p:Configuration=Release -p:DotNetStableTargetFramework=net10.0 -v:minimal && make all CONFIGURATION=Release MSBUILD_ARGS="--no-restore -m:1"
  • MSBUILDDISABLENODEREUSE=1 ./dotnet-local.sh build tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj -t:RunTestApp -c Release -p:_AndroidTypeMapImplementation=trimmable -p:UseMonoRuntime=false -p:TestFixture=Java.InteropTests.ConstructorActivationTests -nr:false -v:minimal — all 35 constructor activation cases passed; same unrelated JavaObjectTest.DisposeAccessesThis trimmable failure remains outside this fixture

Related issues

@simonrozsival simonrozsival changed the title Fix trimmable Java object activation [TrimmableTypeMap] Fix trimmable Java object activation May 3, 2026
@simonrozsival simonrozsival added copilot `copilot-cli` or other AIs were used to author this trimmable-type-map labels May 3, 2026
@simonrozsival simonrozsival force-pushed the trimmable-virtual-constructor branch from 711aeae to 7e2737b Compare May 4, 2026 18:23
@simonrozsival simonrozsival force-pushed the trimmable-object-activation branch from a8491be to 3bcb461 Compare May 4, 2026 18:23
@simonrozsival simonrozsival force-pushed the trimmable-virtual-constructor branch from 7e2737b to dc13b42 Compare May 4, 2026 18:31
@simonrozsival simonrozsival force-pushed the trimmable-object-activation branch 2 times, most recently from 3bec3d2 to ddcddd4 Compare May 11, 2026 17:29
@simonrozsival simonrozsival changed the base branch from trimmable-virtual-constructor to main May 11, 2026 17:29
@simonrozsival simonrozsival marked this pull request as ready for review May 11, 2026 17:32
Copilot AI review requested due to automatic review settings May 11, 2026 17:32
@simonrozsival simonrozsival force-pushed the trimmable-object-activation branch from ddcddd4 to daeb39a Compare May 11, 2026 17:39
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 updates the TrimmableTypeMap pipeline to improve Java→managed activation behavior (notably around constructor callbacks), marks certain activation peers as replaceable to support Java-side virtual dispatch scenarios, and re-enables previously excluded device tests as those behaviors are restored.

Changes:

  • Updates typemap proxy/UCO emission to avoid reflection for default managed constructors and to mark activation peers as Replaceable.
  • Extends the typemap scanner/model to treat JniAddNativeMethodRegistrationAttribute types (hand-written Java peers) as requiring ACW/proxy support.
  • Adjusts test infrastructure and test inputs (C#/Java) to run trimmable-specific Java.Interop constructor-virtual-call scenarios and to remove now-unneeded exclusions.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/NUnitInstrumentation.cs Removes trimmable exclusions for tests that are now expected to pass.
tests/Mono.Android-Tests/Mono.Android-Tests/Java.Lang/ObjectTest.cs Removes an outdated TODO tied to trimmable typemap behavior.
tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JnienvTest.cs Cleans up throwable activation test logging; adds TODO for open generic activation behavior.
tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-trimmable/CallVirtualFromConstructorDerived.cs Adds a trimmable-specific managed peer used for constructor virtual-dispatch testing.
tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets Adjusts Java test-jar inputs so trimmable builds use java-trimmable sources for specific classes.
tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.NET.csproj Conditionally swaps managed CallVirtualFromConstructorDerived source between desktop and trimmable variants.
tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallVirtualFromConstructorDerived.java Adds trimmable Java peer for derived constructor-virtual-call scenario.
tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallVirtualFromConstructorBase.java Adds trimmable Java peer base class for the same scenario.
src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs Expands SDK pack assertions to ensure obsolete java-runtime artifacts aren’t shipped.
src/Mono.Android/Java.Interop/JavaPeerProxy.cs Adds helper to mark activation peers as Replaceable.
src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs Records presence of JniAddNativeMethodRegistrationAttribute for downstream model decisions.
src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs Adds model flag for JniAddNativeMethodRegistrationAttribute usage.
src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs Changes proxy/UCO emission logic (default ctor activation path + replaceable marking; also refactors typemap attribute emission).
src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs Ensures handwritten Java peers with native registrations can trigger proxy/ACW generation.
src/java-runtime/java-runtime.targets Deletes/cleans obsolete java-runtime artifacts to prevent stale outputs and packaging.
Comments suppressed due to low confidence (3)

src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs:184

  • EmitCore() no longer emits per-rank __ArrayMapRank{N} sentinel TypeDefs (previously via EmitRankSentinels(model)), but ModelBuilder can still produce array entries (TypeMapAttributeData.AnchorRank / model.MaxArrayRank) and the root typemap loader expects those anchors to exist for array lookups. This will break TrimmableTypeMap.Instance.TryGetArrayType(...) / JNIEnv array creation when _AndroidTrimmableTypeMapMaxArrayRank > 0. Consider restoring rank-sentinel emission (and the corresponding per-rank TypeMap grouping) or removing/rewriting the array-entry pipeline end-to-end so the root loader and runtime agree.
			throw new ArgumentNullException (nameof (model));
		}
		if (stream is null) {
			throw new ArgumentNullException (nameof (stream));
		}

		EmitCore (model, useSharedTypemapUniverse);
		_pe.WritePE (stream);
	}

	void EmitCore (TypeMapAssemblyData model, bool useSharedTypemapUniverse)
	{
		_pe.EmitPreamble (model.AssemblyName, model.ModuleName, MetadataHelper.ComputeContentFingerprint (model));

		_javaInteropRef = _pe.AddAssemblyRef ("Java.Interop", new Version (0, 0, 0, 0));

		EmitTypeReferences ();
		if (useSharedTypemapUniverse) {

src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs:1402

  • EmitTypeMapAttribute() now ignores entry.AnchorRank and always uses the default anchor’s TypeMapAttribute<TGroup> ctor. Any array entries created with AnchorRank (see TypeMapAttributeData.AnchorRank) will end up in the wrong typemap universe/group, so the root loader’s per-rank TypeMapping.GetOrCreateExternalTypeMapping<__ArrayMapRankN>() maps won’t contain them. Either reintroduce the per-rank anchor handling here, or stop generating AnchorRank entries (and update the runtime/root loader accordingly).
					encoder.Token (nameFields [i]);

					// byte* signature
					encoder.OpCode (ILOpCode.Ldsflda);
					encoder.Token (sigFields [i]);

					// IntPtr functionPointer
					encoder.OpCode (ILOpCode.Ldftn);
					encoder.Token (validRegs [i].Wrapper);

					// Construct the struct on the evaluation stack and store it
					// at the destination address. This matches the Roslyn pattern:
					//   newobj JniNativeMethod::.ctor(byte*, byte*, IntPtr)
					//   stobj  JniNativeMethod

src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs:1008

  • For proxy.IsGenericDefinition, EmitUcoConstructor() now emits a no-op wrapper that silently returns. This changes behavior from “fail activation” to “succeed without a managed peer”, which can leave an allocated Java instance without a corresponding managed wrapper and also conflicts with existing expectations that open generic activation is unsupported (e.g., JnienvTest.NewOpenGenericTypeThrows). It would be safer to preserve the previous behavior of throwing a NotSupportedException (or otherwise surfacing an error) so the failure is explicit and debuggable.
		encoder.Call (_waitForBridgeProcessingRef);
		encoder.MarkLabel (tryStart);
		emitCallback (encoder);
		if (!isVoid) {
			encoder.StoreLocal (0);
		}
		encoder.Branch (ILOpCode.Leave, afterAll);

		encoder.MarkLabel (catchStart);
		encoder.StoreLocal (isVoid ? 0 : 1);

@simonrozsival simonrozsival marked this pull request as draft May 11, 2026 17:43
@simonrozsival simonrozsival force-pushed the trimmable-object-activation branch 2 times, most recently from 50e081e to 02e0cf4 Compare May 11, 2026 17:55
@simonrozsival simonrozsival marked this pull request as ready for review May 11, 2026 17:56
simonrozsival and others added 3 commits May 12, 2026 08:13
Run default managed constructors for generated no-arg constructor callbacks, and mark generated constructor activation peers as replaceable so managed wrappers can replace temporary peers created during Java-side virtual dispatch.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Avoid reflection for generated no-arg Java constructor callbacks by emitting IL that attaches the JNI peer and invokes the managed default constructor directly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
After rebasing onto main (which merged #11260 adding maxstack tracking),
the new 0-arg nctor codegen path in EmitUcoMethod was using the
pre-#11260 bare OpCode + Token style. Switch to the tracked encoder
helpers (LoadToken, CastClass, Call(parameterCount,…), Callvirt(parameterCount,…))
so the emitted method's maxstack is computed correctly, matching every
other IL site in TypeMapAssemblyEmitter.

Also update Generate_InheritedCtor_* / Generate_InheritedJavaInteropCtor_*
generator tests to reflect the new nctor contract: the default-ctor
branch references the target type's parameterless .ctor(),
SetPeerReference and MarkActivationPeerReplaceable — and no longer
references the legacy (IntPtr, JniHandleOwnership) /
(JniObjectReference&, JniObjectReferenceOptions) activation ctor on the
inherited base.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival simonrozsival force-pushed the trimmable-object-activation branch from 02e0cf4 to b22aef2 Compare May 12, 2026 06:19
simonrozsival and others added 3 commits May 12, 2026 13:23
Add broad constructor activation coverage and generate direct constructor UCO activation for default and parameterized Java constructors. The generated path now reuses activation peers, skips managed construction recursion, forwards JNI constructor arguments, and stamps the JNI identity hash on freshly activated peers.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Route generated constructor UCO activation through the invoker type when a proxy represents an abstract or interface peer. Cover both Xamarin.Android and Java.Interop activation constructor styles.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace parameterless-only constructor metadata with general managed-constructor availability, extract repeated constructor UCO emission through helpers, add a tracked PopValue helper, and expand runtime coverage with mixed multi-argument and throwing constructors.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

[TrimmableTypeMap] UCO nctor should re-invoke ctor on Activatable/Replaceable peers

2 participants