Skip to content

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

Draft
simonrozsival wants to merge 26 commits intomainfrom
dev/simonrozsival/trimmable-typemap-export-attribute
Draft

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

Conversation

@simonrozsival
Copy link
Copy Markdown
Member

Summary

Add UCO (UnmanagedCallersOnly) wrapper codegen for [Export] methods and [ExportField] fields in the trimmable typemap pipeline, plus test pruning and stabilization for the CoreCLRTrimmable test lane.

Part of #10788

Changes

Export support

  • Scanner: Detect [Export] and [ExportField] attributes, resolve JNI signatures for non-primitive Java-bound parameter types, collect Java access modifiers and throws clauses
  • Model: MarshalMethodInfo.IsExport / JavaAccess / ThrownNames / SuperArgumentsString carry all 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] methods
  • Mono.Android.Export exclusion: Exclude Mono.Android.Export from trimmable packages (its dynamic DynamicMethod codegen is incompatible with AOT/trimming)

Test stabilization

  • Prune and reorganize CoreCLRTrimmable test exclusions
  • Propagate deferred registerNatives to base classes
  • Fix test plumbing for trimmable test lane

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.
Comment on lines +40 to +45
<PropertyGroup>
<UseMonoRuntime Condition=" '$(_AndroidTypeMapImplementation)' == 'trimmable' and '$(UseMonoRuntime)' == '' and '$(PublishAot)' != 'true' ">false</UseMonoRuntime>
<TestsFlavor Condition=" '$(TestsFlavor)' == '' and '$(_AndroidTypeMapImplementation)' == 'trimmable' ">CoreCLRTrimmable</TestsFlavor>
<ExcludeCategories Condition=" '$(_AndroidTypeMapImplementation)' == 'trimmable' ">$(ExcludeCategories):NativeTypeMap:TrimmableIgnore:SSL</ExcludeCategories>
<DefineConstants Condition=" '$(_AndroidTypeMapImplementation)' == 'trimmable' ">$(DefineConstants);TRIMMABLE_TYPEMAP</DefineConstants>
</PropertyGroup>
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.

The UseMonoRuntime override for the trimmable typemap lane is gated on $(UseMonoRuntime) == '', but the SDK defaults UseMonoRuntime=true in Debug. This means a Debug build with _AndroidTypeMapImplementation=trimmable can still end up targeting MonoVM and then fail _ValidateTrimmableTypeMapRuntime. Consider removing the $(UseMonoRuntime) == '' check (or explicitly overriding Debug defaults) so trimmable builds reliably select CoreCLR when PublishAot != true.

Copilot uses AI. Check for mistakes.
Comment on lines +267 to +310
[Fact]
public void PropagateDeferredRegistration_PropagatesCannotRegisterToBaseClasses ()
{
var basePeer = new JavaPeerInfo {
JavaName = "crc64aaa/TestInstrumentation_1", CompatJniName = "crc64aaa/TestInstrumentation_1",
ManagedTypeName = "Tests.TestInstrumentation`1", ManagedTypeNamespace = "Tests", ManagedTypeShortName = "TestInstrumentation`1",
AssemblyName = "Tests", IsUnconditional = false,
BaseJavaName = "android/app/Instrumentation",
};
var midPeer = new JavaPeerInfo {
JavaName = "crc64bbb/NUnitTestInstrumentation", CompatJniName = "crc64bbb/NUnitTestInstrumentation",
ManagedTypeName = "Tests.NUnitTestInstrumentation", ManagedTypeNamespace = "Tests", ManagedTypeShortName = "NUnitTestInstrumentation",
AssemblyName = "Tests", IsUnconditional = false,
BaseJavaName = "crc64aaa/TestInstrumentation_1",
};
var leafPeer = new JavaPeerInfo {
JavaName = "crc64ccc/NUnitInstrumentation", CompatJniName = "crc64ccc/NUnitInstrumentation",
ManagedTypeName = "Tests.NUnitInstrumentation", ManagedTypeNamespace = "Tests", ManagedTypeShortName = "NUnitInstrumentation",
AssemblyName = "Tests", IsUnconditional = false,
BaseJavaName = "crc64bbb/NUnitTestInstrumentation",
};
var peers = new List<JavaPeerInfo> { basePeer, midPeer, leafPeer };

var doc = System.Xml.Linq.XDocument.Parse ("""
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example">
<instrumentation android:name="crc64ccc.NUnitInstrumentation" />
</manifest>
""");

var generator = CreateGenerator ();
generator.RootManifestReferencedTypes (peers, doc);

// Execute calls PropagateDeferredRegistrationToBaseClasses internally,
// but we test the generator method through the public Execute path indirectly.
// For unit testing, call RootManifestReferencedTypes + verify the propagation
// by invoking the static helper through a full Execute run.
// Instead, use reflection or just verify after calling Execute with a manifest.

// RootManifestReferencedTypes sets the flag on the leaf only
Assert.True (leafPeer.CannotRegisterInStaticConstructor, "Leaf instrumentation should have deferred registration.");
Assert.False (midPeer.CannotRegisterInStaticConstructor, "Mid peer should NOT have deferred registration yet (before propagation).");
Assert.False (basePeer.CannotRegisterInStaticConstructor, "Base peer should NOT have deferred registration yet (before propagation).");
}
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.

This test name/comments indicate it should validate propagation of CannotRegisterInStaticConstructor to base classes, but it only calls RootManifestReferencedTypes and then asserts that propagation has not happened (mid/base are false). Either rename the test to reflect what it actually asserts, or update it to exercise the propagation logic (e.g., via Execute(...) or by invoking the propagation helper through a supported path) and assert that mid/base become true.

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 from 7c81e12 to d429c27 Compare April 16, 2026 15:22
simonrozsival and others added 22 commits April 18, 2026 22:21
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>
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>
- 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>
simonrozsival and others added 4 commits April 18, 2026 22:27
- 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>
@simonrozsival simonrozsival force-pushed the dev/simonrozsival/trimmable-typemap-export-attribute branch from 2b68085 to 9007196 Compare April 18, 2026 20:29
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