Skip to content

[Mono.Android] Root Mono.Android.Export from the [Export]/[ExportField] loader, not the attributes#11804

Open
simonrozsival wants to merge 13 commits into
mainfrom
dev/simonrozsival/export-dynamic-dependency
Open

[Mono.Android] Root Mono.Android.Export from the [Export]/[ExportField] loader, not the attributes#11804
simonrozsival wants to merge 13 commits into
mainfrom
dev/simonrozsival/export-dynamic-dependency

Conversation

@simonrozsival

@simonrozsival simonrozsival commented Jun 29, 2026

Copy link
Copy Markdown
Member

Problem

ExportAttribute and ExportFieldAttribute each carried [DynamicDependency(..., "Java.Interop.DynamicCallbackCodeGenerator", "Mono.Android.Export")] (and ExportAttribute a class-level [RequiresUnreferencedCode]) to keep Mono.Android.Export.dll alive whenever [Export]/[ExportField] is used.

These attributes are shared with the trimmable type map. On the trimmable path, [Export]/[ExportField] members are handled by generated JavaPeerProxy + .java code, so Mono.Android.Export must not be rooted. Rooting it from the shared attributes is therefore wrong for trimmable apps. But simply removing the annotations (as the trimmable work does) breaks the managed/MonoVM/CoreCLR path: linked Release apps trim Mono.Android.Export.dll, so AndroidRuntime.CreateDynamicCallback's Assembly.Load("Mono.Android.Export") throws FileNotFoundException, leaving a pending JNI exception that aborts the app (SIGABRT) — observed via MonoAndroidExportReferencedAppStarts on CoreCLR.

Fix

Root Mono.Android.Export where it is actually loaded, not on the shared attributes:

  • AndroidRuntime.CreateDynamicCallback gets the [DynamicDependency]. This is the single runtime loader for both [Export] and [ExportField] — their generated members register with the __export__ connector, which resolves to CreateDynamicCallback. It is only retained by the linker on the reflection path; the trimmable path trims this method, so it stays free of the dependency. Its null-forgiving return is also replaced with an explicit is not Delegate check that throws InvalidOperationException.
  • AndroidTypeManager is marked [RequiresUnreferencedCode] (it is reflection-backed). The single resulting warning cascade in JNIEnvInit.CreateTypeManager is suppressed — that branch is only reached when RuntimeFeature.TrimmableTypeMap is false and is trimmed in trimmable apps.
  • ExportAttribute and ExportFieldAttribute drop their [DynamicDependency] (and ExportAttribute its [RequiresUnreferencedCode]). Removing the public [RequiresUnreferencedCode] is an API change, baselined in acceptable-breakages-vReference-net11.0.txt.

Context

Carved out of the trimmable type map work (#11617) so the shared [Export]/[ExportField] attributes are trimmable-safe independently. No behavior change on the managed path — Mono.Android.Export is still packaged and [Export]/[ExportField] still work.

Mono.Android builds clean in Release (IL trim + API compat pass).

…Attribute

ExportAttribute is shared with the trimmable type map, which generates JavaPeerProxy
and .java for [Export] members and must NOT pull in Mono.Android.Export. So the
[DynamicDependency] and [RequiresUnreferencedCode] are removed from ExportAttribute
and the dependency is rooted where the managed/reflection [Export] path actually
loads it instead:

* AndroidRuntime.CreateDynamicCallback gets the [DynamicDependency] on
  DynamicCallbackCodeGenerator/Mono.Android.Export. It is only kept by the linker on
  the reflection path; the trimmable path trims it and stays free of the dependency.
* AndroidTypeManager is marked [RequiresUnreferencedCode] (it is reflection-backed),
  replacing the per-method IL2026/IL2075 suppressions; the single resulting cascade in
  JNIEnvInit.CreateTypeManager is suppressed (that branch is trimmed in trimmable apps).
* ExportAttribute drops [RequiresUnreferencedCode] (API breakage baselined).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 29, 2026 14:31
@simonrozsival simonrozsival added copilot `copilot-cli` or other AIs were used to author this trimmable-type-map labels Jun 29, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 adjusts trimming annotations so Mono.Android.Export is preserved only when the legacy reflection-based [Export] callback loader is actually used, avoiding incorrectly rooting Mono.Android.Export for trimmable typemap apps.

Changes:

  • Removes [DynamicDependency]/[RequiresUnreferencedCode] from Java.Interop.ExportAttribute and baselines the resulting API-compat attribute removal.
  • Roots Mono.Android.Export from AndroidRuntime.CreateDynamicCallback via [DynamicDependency] and marks AndroidTypeManager as [RequiresUnreferencedCode], adding a targeted trim warning suppression in JNIEnvInit.
  • Adds an ApiCompatLinesToAdd.txt artifact under src/Mono.Android/ (currently committed with content).
Show a summary per file
File Description
tests/api-compatibility/acceptable-breakages-vReference-net11.0.txt Baselines removal of RequiresUnreferencedCodeAttribute from Java.Interop.ExportAttribute.
src/Xamarin.Android.NamingCustomAttributes/Java.Interop/ExportAttribute.cs Drops trim annotations from the shared [Export] attribute.
src/Mono.Android/ApiCompatLinesToAdd.txt Adds an apicompat “lines to add” output file (currently non-empty).
src/Mono.Android/Android.Runtime/JNIEnvInit.cs Suppresses IL2026 at the type-manager selection point for the non-trimmable branch.
src/Mono.Android/Android.Runtime/AndroidRuntime.cs Marks AndroidTypeManager as RUC and moves DynamicDependency to the loader (CreateDynamicCallback).

Copilot's findings

  • Files reviewed: 5/5 changed files
  • Comments generated: 4

Comment thread src/Mono.Android/ApiCompatLinesToAdd.txt Outdated
Comment thread src/Mono.Android/Android.Runtime/AndroidRuntime.cs Outdated
Comment thread src/Mono.Android/Android.Runtime/AndroidRuntime.cs Outdated
simonrozsival and others added 2 commits June 29, 2026 16:38
[RequiresUnreferencedCode] on the class already suppresses trim-analysis warnings
within its members, so the class-level [UnconditionalSuppressMessage IL2026] is no
longer needed.

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

* ExportFieldAttribute: drop [DynamicDependency] too — CreateDynamicCallback is the
  single runtime loader for both [Export] and [ExportField] (__export__ connector),
  so the loader-side dependency covers it; the shared attributes are now fully
  trimmable-safe.
* Remove the accidentally-committed ApiCompatLinesToAdd.txt (CheckApiCompatibility
  task output; the breakage is baselined in acceptable-breakages).
* CreateDynamicCallback: reword the now-stale 'See ExportAttribute.cs' comment and
  replace the null-forgiving operator with an explicit 'is not Delegate' check that
  throws InvalidOperationException, matching the surrounding version-mismatch throws.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival simonrozsival changed the title [Mono.Android] Root Mono.Android.Export from the [Export] loader, not ExportAttribute [Mono.Android] Root Mono.Android.Export from the [Export]/[ExportField] loader, not the attributes Jun 29, 2026
Rooting Mono.Android.Export (and System.Reflection.Emit via
DynamicCallbackCodeGenerator) from AndroidRuntimeManager.CreateDynamicCallback
on the managed/reflection path grows the SimpleDotNet.CoreCLR app, so the
BuildReleaseArm64(False,CoreCLR) size-regression reference needs refreshing.

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

# Conflicts:
#	src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.CoreCLR.apkdesc
#	tests/api-compatibility/acceptable-breakages-vReference-net11.0.txt
@simonrozsival

Copy link
Copy Markdown
Member Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

simonrozsival and others added 6 commits June 30, 2026 18:12
Rooting Mono.Android.Export from the [Export]/[ExportField] loader grows
libassembly-store.so and libxamarin-app.so, pushing BuildReleaseArm64
(False,CoreCLR) past the apkdiff size thresholds. Update the reference
apkdesc with the expected sizes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The [Export]/[ExportField] loader only reflects on the single public
static DynamicCallbackCodeGenerator.Create(MethodInfo) method, so use
the (memberSignature, typeName, assemblyName) DynamicDependency overload
to pin exactly that member instead of DynamicallyAccessedMemberTypes.All.

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

Replace Assembly.Load ("Mono.Android.Export") + GetType with an
assembly-qualified Type.GetType ("...,  Mono.Android.Export") feeding a
constant GetMethod ("Create"). Both ILLink and the NativeAOT ILC have
intrinsics for Type.GetType with a constant string and GetMethod with a
constant name on a statically-known type, so the [Export]/[ExportField]
codegen entry point is now statically analyzable and the assembly is
preserved without the explicit [DynamicDependency] attribute.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The trimmer only recognizes the GetMethod intrinsic when the constant
Type value flows directly into it. Routing the Type.GetType result
through an intermediate local + null-check breaks the dataflow, so chain
Type.GetType ("...")?.GetMethod ("Create") ?? throw into a single
expression.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival simonrozsival added the ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable). label Jul 1, 2026
@simonrozsival

Copy link
Copy Markdown
Member Author

/review

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

Android PR Reviewer completed successfully!

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 Android PR Review — ⚠️ Needs Changes

Reviewed the full diff and surrounding source. The refactor itself is clean; the main item is a measurable size regression worth confirming, plus a couple of smaller diagnostics/documentation points.

Independent read of the rooting mechanism: What actually keeps Mono.Android.Export alive on the managed path is the constant-string Type.GetType("..., Mono.Android.Export") intrinsic in CreateDynamicCallback (an ILLink reflection pattern), not a [DynamicDependency]. The PR description says CreateDynamicCallback "gets the [DynamicDependency]," but there is no [DynamicDependency] in the final code — worth correcting so future readers understand how the assembly is rooted (and why it is unconditional on the reflection path).

Inline comments

  • ⚠️ PerformanceBuildReleaseArm64SimpleDotNet.CoreCLR.apkdesc grows ~2 MB for an app that never uses [Export]; the Type.GetType intrinsic roots Mono.Android.Export (+ Reflection.Emit) unconditionally on the reflection path, whereas the old attribute [DynamicDependency] was conditional on [Export] usage. Please confirm this is intended.
  • 💡 Error handlingCreateDynamicCallback loses the "version mismatch" diagnostic; a stale Mono.Android.Export.dll now reports the misleading "needs to be referenced."
  • 💡 Trimmer/AOT — the new GetCustomAttribute<ExportAttribute>() check's stated trimming rationale doesn't seem to hold (Export.dll is rooted elsewhere; ExportAttribute lives in Mono.Android.dll), and it's effectively always-true at runtime.

Also worth noting (non-blocking)

  • Removing the public [RequiresUnreferencedCode] from ExportAttribute is a real (baselined) API change: user code applying [Export] no longer gets an IL2026 trim warning on the reflection path. Reasonable given Export.dll is now force-rooted there and codegen-handled on the trimmable path — but it is a behavior change beyond "no behavior change on the managed path."
  • BuildReleaseArm64SimpleDotNet.MonoVM.apkdesc wasn't updated, but BuildReleaseArm64 only runs for CoreCLR/NativeAOT, so that's fine; NativeAOT is unchanged because AndroidTypeManager/CreateDynamicCallback are trimmed on that path (consistent with the JNIEnvInit suppression justification).

Nice work 👍

  • Replacing the null-forgiving ! with explicit ?? throw / as Delegate ?? throw — aligns with the repo's no-! rule.
  • Consolidating the loader and moving the reflection annotation onto the internal AndroidTypeManager.
  • The JNIEnvInit.CreateTypeManager IL2026 suppression is correctly justified by the RuntimeFeature.TrimmableTypeMap feature-switch substitution.

Note: the dotnet-android pipeline was still running at review time, so this assumes CI goes green. Posted as a non-blocking review comment.

Generated by Android PR Reviewer for #11804 · 366.1 AIC · ⌖ 31.3 AIC · ⊞ 6.8K
Comment /review to run again

},
"lib/arm64-v8a/libassembly-store.so": {
"Size": 3229120
"Size": 5290544

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 ⚠️ Performance — This is the reference apkdesc for the default XamarinAndroidApplicationProject ("Simple" flavor), which never uses [Export]/[ExportField], yet libassembly-store.so grows ~2 MB (3.23 MB → 5.29 MB) and the package ~1.7 MB.

The cause is the switch in CreateDynamicCallback from Assembly.Load("Mono.Android.Export") (opaque to ILLink) to Type.GetType("Java.Interop.DynamicCallbackCodeGenerator, Mono.Android.Export"). A constant-string Type.GetType is an ILLink reflection intrinsic: the trimmer resolves and roots the named type, pulling in Mono.Android.Export plus its Mono.CodeGeneration / System.Reflection.Emit dependencies. Because CreateDynamicCallback is always reachable from RegisterNativeMembers on the reflection path, this now happens for every CoreCLR app, not just apps that use [Export]. The previous [DynamicDependency] on the ExportAttribute constructors kept the rooting conditional on actual [Export] usage.

Is this unconditional ~2 MB regression on all reflection-path apps intended? If not, consider keeping the dependency conditional on [Export] usage. Either way, the PR description ("No behavior change on the managed path") should call out that non-[Export] CoreCLR apps now grow.

Rule: Watch for app-size regressions in reference apkdescs

}
return (Delegate)dynamic_callback_gen.Invoke (null, new object [] { method })!;
// We're loading the Mono.Android.Export assembly dynamically to avoid problems with circular dependencies.
dynamic_callback_gen ??= Type.GetType ("Java.Interop.DynamicCallbackCodeGenerator, Mono.Android.Export")?.GetMethod ("Create")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 💡 Error handling — Folding Type.GetType(...)?.GetMethod("Create") ?? throw into a single null check collapses the previously-distinct failure diagnostics. If Mono.Android.Export.dll is present but out of date (type renamed, or Create removed), GetMethod("Create") returns null and this throws "...Mono.Android.Export.dll needs to be referenced in the application" — misleading, since the assembly is referenced. The old code reported "does not match the expected version" for that case. Consider separating the type-resolution null check from the method null check so the version-mismatch diagnostic is preserved.

Rule: Preserve actionable error diagnostics


// This check is for trimming purposes: if `ExportAttribute` is trimmed from the app, so should be `CreateDynamicCallback`
// and with it the implicit dependency on `Mono.Android.Export.dll`.
if (minfo.GetCustomAttribute<ExportAttribute> () is not ExportAttribute)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 💡 Trimmer/AOT — The comment says this check ensures "if ExportAttribute is trimmed ... so should be CreateDynamicCallback and with it the implicit dependency on Mono.Android.Export.dll." That chain doesn't appear to hold:

  • Mono.Android.Export.dll is rooted by the constant-string Type.GetType(...) intrinsic in CreateDynamicCallback, independently of ExportAttribute.
  • ExportAttribute is compiled into Mono.Android.dll (via the shared Xamarin.Android.NamingCustomAttributes projitems), not Mono.Android.Export.dll.
  • RegisterNativeMembers unconditionally references both ExportAttribute (through this call) and CreateDynamicCallback, so this doesn't make either conditionally trimmable.

And since the __export__ connector is only emitted by the JCW generator for [Export]/[ExportField] members, minfo here always carries [Export] — so this adds a per-registration GetCustomAttribute reflection call that is effectively always true. Could you clarify the intended trimming effect? If it's purely defensive, the comment is misleading and should be corrected.

Rule: Trimming annotations/comments must match actual trimmer behavior

@simonrozsival simonrozsival removed the ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable). label Jul 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

copilot `copilot-cli` or other AIs were used to author this trimmable-type-map

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants