This repository was archived by the owner on Aug 27, 2026. It is now read-only.

Split reflection-based JniTypeManager and JniValueManager behavior - #1441

Merged
jonathanpeppers merged 30 commits into
mainfrom
dev/simonrozsival/fix-aot-warnings-jni-type-manager
Jun 9, 2026
Merged

Split reflection-based JniTypeManager and JniValueManager behavior#1441
jonathanpeppers merged 30 commits into
mainfrom
dev/simonrozsival/fix-aot-warnings-jni-type-manager

Conversation

@simonrozsival

@simonrozsivalsimonrozsival commented Jun 4, 2026

Copy link
Copy Markdown
Member

Splits the reflection-backed paths out of JniRuntime.JniTypeManager and JniRuntime.JniValueManager so callers that don't need reflection (e.g. NativeAOT) can opt out, while keeping the public API surface as compatible as possible for callers migrating from .NET 10.

What changed

  • JniRuntime.ReflectionJniTypeManager (new): all reflection-based type lookup, signature discovery, and native-method registration moves here. Annotated with [RequiresDynamicCode] / [RequiresUnreferencedCode].
  • JniRuntime.ReflectionJniValueManager (new): all reflection-based peer construction, value marshaling, and activation moves here.
  • JniRuntime.JniTypeManager stays a non-abstract base class. Methods that previously had reflection defaults are now virtual no-ops (or return null/empty). Subclasses that want the old default behavior should derive from ReflectionJniTypeManager.
  • JniRuntime.JniValueManager stays abstract (it already was). A few previously-virtual methods (CreatePeer, GetValueMarshalerCore(Type)) become abstract since their reflection-based defaults moved out — public consumers (dotnet/android's value managers) already override these.
  • In-tree consumers updated: JreTypeManager, ManagedValueManager, MonoRuntimeValueManager now derive from the new Reflection* bases. The NativeAOT samples keep deriving from the abstract bases with explicit type mappings.

Public API impact

PublicAPI.Shipped.txt is unchanged. All breaking changes are tracked in PublicAPI.Unshipped.txt with *REMOVED* markers:

  • JniTypeManager.TryRegisterNativeMembers (both overloads) — moved to ReflectionJniTypeManager as protected helpers.
  • JniValueManager.CreatePeer and GetValueMarshalerCore(Type)virtualabstract.

A non-abstract JniTypeManager keeps new JniRuntime.JniTypeManager() and direct subclasses compiling without changes.

Validation

  • CI is green.
  • Local dotnet build Java.Interop.sln passes (PublicApiAnalyzer enforces tracking is consistent).

@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/fix-aot-warnings-jni-type-manager branch from 5ff4c17 to 6d72140CompareJune 4, 2026 14:24
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/fix-aot-warnings-jni-type-manager branch from 6d72140 to 58f0dbdCompareJune 4, 2026 14:30
@simonrozsival

Copy link
Copy Markdown
MemberAuthor

/azp run

@azure-pipelines

Copy link
Copy Markdown
No pipelines are associated with this pull request.

@simonrozsival
simonrozsival marked this pull request as ready for review June 8, 2026 11:55
CopilotAI review requested due to automatic review settings June 8, 2026 11:55

CopilotAI 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.

Pull request overview

This draft PR refactors JniRuntime.JniTypeManager into an abstract base and moves the default reflection-based behavior into a new JniRuntime.DynamicJniTypeManager, with corresponding updates across tests, JRE runtime helpers, samples, and public API baselines.

Changes:

  • Introduces JniRuntime.DynamicJniTypeManager and makes JniRuntime.JniTypeManager abstract, shifting default/reflection-backed logic into the new derived type.
  • Updates test and JRE type managers to inherit from DynamicJniTypeManager, and adjusts tests for new behaviors (e.g., generic invoker handling, GetType array behavior).
  • Updates trimmer/AOT annotations and PublicAPI baselines to reflect the split and new abstract members.
Show a summary per file
FileDescription
tests/Java.Interop-Tests/Java.Interop/JniTypeManagerTests.csUpdates expectations around GetType behavior (notably array signatures and generic holder mapping).
tests/Java.Interop-Tests/Java.Interop/JniRuntimeTest.csSwitches proxy manager to DynamicJniTypeManager and adds trim suppression for test-only reflection use.
tests/Java.Interop-Tests/Java.Interop/JniRuntime.JniTypeManagerTests.csAdds a generic invoker negative test and updates test manager base type.
tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.csUpdates fixture type manager to DynamicJniTypeManager and refines type lookup with trimming annotations.
src/Java.Runtime.Environment/Java.Interop/JreTypeManager.csMoves to DynamicJniTypeManager and adds a DAM-annotated GetTypeForSimpleReference override.
src/Java.Interop/PublicAPI.Unshipped.txtRecords newly introduced/reshaped APIs (new type, newly-abstract members, overrides).
src/Java.Interop/PublicAPI.Shipped.txtUpdates shipped API surface to reflect newly-abstract members and removed methods from JniTypeManager.
src/Java.Interop/Java.Interop/JniValueMarshaler.csSimplifies expression-based marshaler instantiation and adjusts trimming annotations.
src/Java.Interop/Java.Interop/JniRuntime.JniValueManager.csSwitches peer-type selection to GetReflectionConstructibleTypes() and refactors uninitialized peer creation.
src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.csMakes JniTypeManager abstract and factors previously-inlined logic into abstract core methods.
src/Java.Interop/Java.Interop/JniRuntime.DynamicJniTypeManager.csAdds the new reflection-backed type manager implementation and related registration behavior.
src/Java.Interop/Java.Interop/JniRuntime.csRemoves the default new JniTypeManager() fallback; requires an explicit type manager in creation options.
src/Java.Interop/Java.Interop/JavaPrimitiveArrays.ttRetargets primitive array helper generation to DynamicJniTypeManager.
src/Java.Interop/Java.Interop/JavaPrimitiveArrays.csUpdates the generated primitive array helper container type to DynamicJniTypeManager.
src/Java.Interop/GlobalSuppressions.csAdds CA1034 suppression for the new nested DynamicJniTypeManager type.
samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.csUpdates sample type manager implementation to new hierarchy and adds explicit native member registration.
samples/Hello-NativeAOTFromJNI/ManagedType.csChanges the native registration hook to internal for sample-driven registration.
samples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.csUpdates sample type manager implementation to new hierarchy and implements abstract members.

Copilot's findings

  • Files reviewed: 18/18 changed files
  • Comments generated: 3

Comment threadsamples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs Outdated
Comment threadsamples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs Outdated
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival
simonrozsival marked this pull request as draft June 8, 2026 14:24
simonrozsivaland others added 9 commits June 8, 2026 16:32
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>
Map Java.Interop's internal proxy peer types in the base type manager so custom AOT-safe managers do not need reflection-based DynamicJniTypeManager behavior for JavaProxyThrowable/JavaProxyObject.
Register built-in proxy native members by JNI type name before asking custom managers for a managed Type, avoiding DAM-annotated lookups of internal Exception/Delegate-derived types.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Let the base JniTypeManager resolve primitive JNI keyword signatures without requiring custom AOT-safe managers to duplicate the dynamic type manager's primitive mappings.
Use that base lookup as a fallback when matching Java-to-managed constructor signatures so signatures such as (I)V resolve to System.Int32.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival
simonrozsival marked this pull request as ready for review June 9, 2026 07:23
simonrozsivaland others added 2 commits June 9, 2026 09:31
Rename DynamicJniTypeManager to ReflectionJniTypeManager so the type name describes the reflection-based behavior that is incompatible with Native AOT.
Update the matching source file name, API baseline, JRE type manager inheritance, generated primitive-array partial, and tests.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsivalsimonrozsival changed the title Split reflection-based JniTypeManager behaviorSplit reflection-based JniTypeManager and JniValueManager behaviorJun 9, 2026

CopilotAI commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved the merge conflicts in commit Resolve merge conflicts: remove #if NET guards, keep abstract methods in base JniTypeManager.

The two new commits in main since the last merge (Remove Java.Interop.GenericMarshaler (#1450) and Remove legacy NET preprocessor paths (#1451)) conflicted with our branch in three files:

  • JniRuntime.JniTypeManager.cs (two conflicts): Kept the abstract method declarations (GetInvokerTypeCore, GetStaticMethodFallbackTypesCore, RegisterNativeMembers) from our branch while adopting the #if NET guard removals from main. The implementation helpers (TryRegisterNativeMembers, TryLoadJniMarshalMethods, etc.) remain in DynamicJniTypeManager where our PR moved them.
  • ManagedPeer.cs: Removed the #if NET guard that was wrapping the ReadOnlySpan<char> methods block.
  • JreRuntime.cs: Kept our branch's structure (TypeManager default set in the [RequiresDynamicCode]-annotated CreateJreVM, not in CreateJreVMCore), discarding main's addition of builder.TypeManager ??= new JreTypeManager(...) to CreateJreVMCore which would have bypassed the AOT warning.

Also removed the remaining #if NET guards from JniRuntime.ReflectionJniTypeManager.cs and JreRuntime.cs that were in our branch but no longer needed after the main merge.

@simonrozsival

Copy link
Copy Markdown
MemberAuthor

/review

@github-actions

github-actionsBot commented Jun 9, 2026

Copy link
Copy Markdown

Java.Interop PR Reviewer completed successfully!

@github-actionsgithub-actionsBot 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.

✅ LGTM — Clean architectural split

Summary: This PR cleanly separates the reflection-based JniTypeManager / JniValueManager behavior into dedicated ReflectionJniTypeManager and ReflectionJniValueManager subclasses, making the base classes abstract and properly annotating the reflection-based paths with [RequiresDynamicCode] / [RequiresUnreferencedCode]. The NativeAOT samples are updated to implement the abstract base directly, and tests/downstream types are updated accordingly.

What looks good:

  • The split is well-structured — base classes contain only the contract and built-in type handling; all reflection goes into the Reflection* subclasses.
  • [RequiresDynamicCode] and [RequiresUnreferencedCode] annotations are applied consistently throughout the chain (samples, tests, JRE runtime types).
  • Public API baselines are updated correctly.
  • TryRegisterBuiltInNativeMembers is a nice extraction that NativeAOT consumers can call without needing the full reflection machinery.
  • The JreRuntimeOptions.CreateJreVM(JniTypeManager) overload is a good ergonomic addition.

Issues by severity:

SeverityCount
⚠️ Warning1
💡 Suggestion3

Notes:

  • CI only shows license/cla — no build/test results visible yet. The PR is not mergeable until CI validates the full build.
  • The large abstract surface area for NativeAOT consumers (~12 methods) may be worth revisiting — see inline suggestion about making unused extension points virtual with default null returns.

Generated by Java.Interop PR Reviewer for issue #1441 · ● 14.3M

Comment threadsamples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs
Comment threadsamples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs
Comment threadsrc/Java.Interop/Java.Interop/JniRuntime.ReflectionJniTypeManager.cs Outdated
simonrozsivaland others added 2 commits June 9, 2026 22:20
Map JNI primitive and boxed primitive signatures in the NativeAOT sample type managers so constructor signature matching can resolve primitive parameters such as I to System.Int32.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
To minimize migration pain for callers moving from .NET 10 to .NET 11,
revert JniTypeManager from `abstract` back to a regular class. Methods
that were turned into `abstract` overrides now have `virtual` defaults
so existing subclasses keep compiling:
* Type lookup methods return null/empty defaults.
* RegisterNativeMembers is a no-op by default; reflection-based
behavior remains in ReflectionJniTypeManager (override to opt-in).
Restore PublicAPI.Shipped.txt to its main-branch state (per the
`Microsoft.CodeAnalysis.PublicApiAnalyzers` convention, never edit
shipped entries in place) and track every breaking change in
PublicAPI.Unshipped.txt:
* 5 new JniTypeManager.* APIs are now `virtual` (not `abstract`).
* `*REMOVED*` entries for the two `JniTypeManager.TryRegisterNativeMembers`
overloads that moved to `ReflectionJniTypeManager`.
* `*REMOVED*virtual` + `abstract` pairs for the two virtual->abstract
transitions on `JniValueManager` (CreatePeer, GetValueMarshalerCore(Type)).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jonathanpeppers
jonathanpeppers merged commit 0244c10 into mainJun 9, 2026
2 checks passed
@jonathanpeppers
jonathanpeppers deleted the dev/simonrozsival/fix-aot-warnings-jni-type-manager branch June 9, 2026 23:17
jonathanpeppers pushed a commit that referenced this pull request Jun 12, 2026
Follow-up to #1441 We need this method virtual so that `JavaMarshalValueManager` can override it and try to invoke the `XAConstructorSignature`. The method was previously virtual but as an oversight I removed it in #1441:
- https://github.com/dotnet/java-interop/pull/1441/changes#diff-8ead12d157bc7712affd3d920bb93cddbff3885147fe6297957a44cc522d5430R7
- https://github.com/dotnet/java-interop/pull/1441/changes#diff-b92884de5db7f82df63b483b8319360cd9bfdac3cd97222eb9aff7292a01a07bL449-L467
I noticed we're not disposing the `reference` correctly which might cause a ref leak. In the Mono `AndroidValueManager` we never called into the base method so this would not be observed on mono previously (https://github.com/dotnet/android/blob/main/src/Mono.Android/Java.Interop/TypeManager.cs#L416-L423).
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
simonrozsival added a commit that referenced this pull request Jun 15, 2026
#1441 added `JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers ()`
-- a reflection-free direct call to `JavaProxyObject.RegisterNativeMembers`
-- and bumped that method to `internal` so the call would compile.
That method is dead code: nothing in Java.Interop (or dotnet/android) ever
calls it, and the base `JniTypeManager.RegisterNativeMembers` is a no-op.
`JavaProxyObject`'s native members are -- and were before #1441 --
registered purely via reflection in `ReflectionJniTypeManager`
(`FindAndCallRegisterMethod` discovering the
`[JniAddNativeMethodRegistrationAttribute]`-annotated method), which finds
private methods just fine via `GetRuntimeMethods ()`.
Remove the unused `TryRegisterBuiltInNativeMembers` and restore
`RegisterNativeMembers` to `private` (its state before #1441). Making it
private again also strips the attribute-bearing method from the reference
assembly, which fixes dotnet/android's trimmable typemap scanner falsely
rejecting the built-in `JavaProxyObject` with XA4251 (dotnet/android#11622).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit that referenced this pull request Jun 15, 2026
Replaces the production TryRegisterBuiltInNativeMembers helper with a
samples-only solution, restoring the pre-#1441 behavior for the NativeAOT
type managers.
Background: making JavaProxyObject.RegisterNativeMembers private (the XA4251
fix) is sufficient for the default Android runtime, which discovers and invokes
it via reflection (FindAndCallRegisterMethod -> GetRuntimeMethods(), which
returns private methods). The only thing that needed JavaProxyObject's
registration via a non-reflection path was the two NativeAOT samples, which
#1441 had rewritten to derive from the reflection-free JniRuntime.JniTypeManager
base. That is why #1441 introduced TryRegisterBuiltInNativeMembers.
Instead of carrying that helper in production, have the sample type managers
derive from JniRuntime.ReflectionJniTypeManager again - exactly what the base
JniRuntime.JniTypeManager provided before #1441. Built-in types such as
JavaProxyObject/JavaProxyThrowable are then registered automatically via
reflection, and the samples no longer need any custom registration code.
Changes:
- JavaProxyObject.RegisterNativeMembers: internal -> private. The marshalers
stay (the default reflection runtime registers them by reflecting over this
type); only the visibility changes, which strips the attribute from the
reference assembly and fixes the dotnet/android XA4251 scanner failure.
- Delete JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers (+ its
PublicAPI.Unshipped.txt entry); no longer needed.
- Hello-NativeAOTFromJNI / Hello-NativeAOTFromAndroid: derive from
JniRuntime.ReflectionJniTypeManager and drop the reflection-free overrides and
hand-written JavaProxyObject registration. ReflectionJniTypeManager is
[RequiresDynamicCode]/[RequiresUnreferencedCode], so the constructor suppresses
IL2026/IL3050 with [UnconditionalSuppressMessage]. A #pragma is insufficient
here: it silences the Roslyn analyzer but not the ILLink/ILC publish passes,
which only honor the attribute (verified with a trim-publish of the real type
manager against Java.Interop.dll).
Net: production loses code (1-line visibility change + deletions) and the samples
shrink substantially.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit that referenced this pull request Jun 16, 2026
Build 1465402's "run Hello-NativeAOTFromJNI" step failed at runtime:
System.NotSupportedException: Could not find System.Type corresponding to
Java type JniTypeSignature(TypeName=example/ManagedType ...)
at Java.Interop.ManagedPeer.RegisterNativeMembers(...)
at example.ManagedType.<clinit>(ManagedType.java:15)
Root cause: after #1441, JniRuntime.JniTypeManager.GetType() dispatches through
GetTypeForSimpleReference (singular), not GetTypesForSimpleReference (plural).
The previous sample rewrite only overrode the plural method, so app types like
example/ManagedType were never resolved (the base ReflectionJniTypeManager's
GetTypeForSimpleReference only knows built-in types), and ManagedPeer
registration threw before it could register the type.
Fix: override GetTypeForSimpleReference (singular) in both NativeAOT sample type
managers to resolve the sample's own managed types, falling back to the base for
built-ins. Registration and the reverse Type->JNI mapping continue to be handled
by the reflection base (the pre-#1441 behavior). The override carries the same
[return: DynamicallyAccessedMembers(...)] as the base to satisfy IL2093.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit that referenced this pull request Jun 16, 2026
Add a class-level comment to both NativeAOT sample type managers explaining the
rationale for the [UnconditionalSuppressMessage] IL2026/IL3050 suppressions:
- These are *samples*, not product code. .NET for Android (what we ship) does not
pair ReflectionJniTypeManager with NativeAOT, so it isn't worth the effort to make
these samples fully trim/AOT-clean right now.
- The reflection paths were always trim/AOT-unsafe. Before #1441 the equivalent
suppressions lived inside JniTypeManager itself (justified "NotUsedInAndroid");
#1441 simply moved that responsibility to callers via
[RequiresDynamicCode]/[RequiresUnreferencedCode].
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit to dotnet/android that referenced this pull request Jun 16, 2026
Revert the external/Java.Interop submodule bump. Pinning an unmerged
java-interop PR-branch commit is fragile, and advancing to java-interop
main HEAD would drag in unrelated changes (e.g. dotnet/java-interop#1441)
that need their own coordination. The generator-side removal lives in
dotnet/java-interop#1470 and will reach Mono.Android through the normal
submodule bump once it merges; this PR keeps only the independent runtime
cleanup of the now-dead `packageLookup` plumbing.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers pushed a commit that referenced this pull request Jun 16, 2026
…in (#1468)
#1441 changed `JavaProxyObject.RegisterNativeMembers` from `private` to
`internal` so the new reflection-free
`JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers ()` could call
it directly.
A side effect is that the `[JniAddNativeMethodRegistrationAttribute]`-
annotated method now appears in Java.Interop's *reference* assembly.
dotnet/android's trimmable typemap scanner reads reference assemblies and
rejects any type carrying that attribute (XA4251), so every trimmable
typemap build now fails on the built-in `JavaProxyObject`
(see dotnet/android#11622).
Restore the method to `private` (so it is stripped from the reference
assembly) and expose a small attribute-free `internal` entry point,
`RegisterBuiltInNativeMembers (JniType)`, for the built-in registration
path to call. Runtime behavior is unchanged.
### [Java.Interop] Restore pure-reflection registration for JavaProxyObject
#1441 added `JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers ()`
-- a reflection-free direct call to `JavaProxyObject.RegisterNativeMembers`
-- and bumped that method to `internal` so the call would compile.
That method is dead code: nothing in Java.Interop (or dotnet/android) ever
calls it, and the base `JniTypeManager.RegisterNativeMembers` is a no-op.
`JavaProxyObject`'s native members are -- and were before #1441 --
registered purely via reflection in `ReflectionJniTypeManager`
(`FindAndCallRegisterMethod` discovering the
`[JniAddNativeMethodRegistrationAttribute]`-annotated method), which finds
private methods just fine via `GetRuntimeMethods ()`.
Remove the unused `TryRegisterBuiltInNativeMembers` and restore
`RegisterNativeMembers` to `private` (its state before #1441). Making it
private again also strips the attribute-bearing method from the reference
assembly, which fixes dotnet/android's trimmable typemap scanner falsely
rejecting the built-in `JavaProxyObject` with XA4251 (dotnet/android#11622).
### Address review: restore TryRegisterBuiltInNativeMembers (not dead code)
The previous commit deleted JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers
claiming it was unused. That was wrong: it has two live callers in the NativeAOT
samples (samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs and
samples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs), both JniTypeManager
subclasses. Deleting it broke them (CS0103) and removed the only path that
registers JavaProxyObject's equals/hashCode/toString natives for NativeAOT type
managers (which don't use the reflection-based FindAndCallRegisterMethod path).
The breakage was hidden in CI only because the NativeAOT sample steps use
continueOnError: true.
Keep the XA4251 fix (RegisterNativeMembers stays private, so the
[JniAddNativeMethodRegistrationAttribute] is stripped from the reference assembly)
and restore TryRegisterBuiltInNativeMembers by extracting the registration logic
into a new attribute-free internal helper, JavaProxyObject.AddBuiltInRegistrations.
Both the private attributed RegisterNativeMembers (reflection path) and
TryRegisterBuiltInNativeMembers (NativeAOT path) call it. Restore the
PublicAPI.Unshipped.txt entry as well.
Verified: Java.Interop.dll builds clean; the reference assembly contains no method
carrying [JniAddNativeMethodRegistration]; the sample's call to
TryRegisterBuiltInNativeMembers resolves (no CS0103).
### Keep built-in registration in the samples via ReflectionJniTypeManager
Replaces the production TryRegisterBuiltInNativeMembers helper with a
samples-only solution, restoring the pre-#1441 behavior for the NativeAOT
type managers.
Background: making JavaProxyObject.RegisterNativeMembers private (the XA4251
fix) is sufficient for the default Android runtime, which discovers and invokes
it via reflection (FindAndCallRegisterMethod -> GetRuntimeMethods(), which
returns private methods). The only thing that needed JavaProxyObject's
registration via a non-reflection path was the two NativeAOT samples, which
#1441 had rewritten to derive from the reflection-free JniRuntime.JniTypeManager
base. That is why #1441 introduced TryRegisterBuiltInNativeMembers.
Instead of carrying that helper in production, have the sample type managers
derive from JniRuntime.ReflectionJniTypeManager again - exactly what the base
JniRuntime.JniTypeManager provided before #1441. Built-in types such as
JavaProxyObject/JavaProxyThrowable are then registered automatically via
reflection, and the samples no longer need any custom registration code.
Changes:
- JavaProxyObject.RegisterNativeMembers: internal -> private. The marshalers
stay (the default reflection runtime registers them by reflecting over this
type); only the visibility changes, which strips the attribute from the
reference assembly and fixes the dotnet/android XA4251 scanner failure.
- Delete JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers (+ its
PublicAPI.Unshipped.txt entry); no longer needed.
- Hello-NativeAOTFromJNI / Hello-NativeAOTFromAndroid: derive from
JniRuntime.ReflectionJniTypeManager and drop the reflection-free overrides and
hand-written JavaProxyObject registration. ReflectionJniTypeManager is
[RequiresDynamicCode]/[RequiresUnreferencedCode], so the constructor suppresses
IL2026/IL3050 with [UnconditionalSuppressMessage]. A #pragma is insufficient
here: it silences the Roslyn analyzer but not the ILLink/ILC publish passes,
which only honor the attribute (verified with a trim-publish of the real type
manager against Java.Interop.dll).
Net: production loses code (1-line visibility change + deletions) and the samples
shrink substantially.
### Drop verbose explanatory comments from the NativeAOT sample type managers
The [UnconditionalSuppressMessage] Justification strings already convey the
necessary context.
### Fix NativeAOT sample type resolution: override GetTypeForSimpleReference
Build 1465402's "run Hello-NativeAOTFromJNI" step failed at runtime:
System.NotSupportedException: Could not find System.Type corresponding to
Java type JniTypeSignature(TypeName=example/ManagedType ...)
at Java.Interop.ManagedPeer.RegisterNativeMembers(...)
at example.ManagedType.<clinit>(ManagedType.java:15)
Root cause: after #1441, JniRuntime.JniTypeManager.GetType() dispatches through
GetTypeForSimpleReference (singular), not GetTypesForSimpleReference (plural).
The previous sample rewrite only overrode the plural method, so app types like
example/ManagedType were never resolved (the base ReflectionJniTypeManager's
GetTypeForSimpleReference only knows built-in types), and ManagedPeer
registration threw before it could register the type.
Fix: override GetTypeForSimpleReference (singular) in both NativeAOT sample type
managers to resolve the sample's own managed types, falling back to the base for
built-ins. Registration and the reverse Type->JNI mapping continue to be handled
by the reflection base (the pre-#1441 behavior). The override carries the same
[return: DynamicallyAccessedMembers(...)] as the base to satisfy IL2093.
### Address review: drop unreachable null guard in FromAndroid sample
`typeMappings` is assigned in its field initializer and never set to null, so the
`if (typeMappings == null) yield break;` guard in CreateSimpleReferencesEnumerator
was unreachable dead code. Remove it.
### Document why the NativeAOT sample trim/AOT suppressions are acceptable
Add a class-level comment to both NativeAOT sample type managers explaining the
rationale for the [UnconditionalSuppressMessage] IL2026/IL3050 suppressions:
- These are *samples*, not product code. .NET for Android (what we ship) does not
pair ReflectionJniTypeManager with NativeAOT, so it isn't worth the effort to make
these samples fully trim/AOT-clean right now.
- The reflection paths were always trim/AOT-unsafe. Before #1441 the equivalent
suppressions lived inside JniTypeManager itself (justified "NotUsedInAndroid");
#1441 simply moved that responsibility to callers via
[RequiresDynamicCode]/[RequiresUnreferencedCode].
### Reword NativeAOT sample suppression justifications to describe why it's safe
The previous IL2026/IL3050 justifications ("does not require unreferenced code /
runtime code generation") read as the opposite of reality, since
ReflectionJniTypeManager is exactly [RequiresUnreferencedCode]/[RequiresDynamicCode].
Reword to describe why the suppression is correct for this sample:
- IL2026: the assembly is rooted via TrimmerRootAssembly and the reflected
registration members are preserved by the [DynamicallyAccessedMembers]
annotations on the RegisterNativeMembers(Type) -> FindAndCallRegisterMethod path.
- IL3050: registration uses CreateDelegate on compile-time-known static methods
(no MakeGenericType / expression compilation), so no runtime codegen is required.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers pushed a commit to dotnet/android that referenced this pull request Jun 16, 2026
…mbing (#11667)
The binding generator used to emit a `Java.Interop.__TypeRegistrations`
class whose `RegisterPackages ()` populated `TypeManager.packageLookup`.
The only reader of that dictionary -- the `TypeRegistrationFallback` path
in `GetJavaToManagedTypeCore` -- was removed in #9471, so the dictionary
has been write-only (and, since nothing generated calls it, effectively
unused) ever since. Type resolution now goes through the native /
trimmable type map.
This bumps external/Java.Interop to the generator change that stops
emitting the dead class (dotnet/java-interop#1470) and removes the
now-dead runtime plumbing:
* Delete the `packageLookup` field and `LazyInitPackageLookup ()`.
* `TypeManager.RegisterPackage`/`RegisterPackages` are shipped public
API, so they are kept as validated no-op stubs rather than removed.
Fixes#11663
### [Mono.Android] Throw NotSupportedException with an explanatory message
`TypeManager.RegisterPackage`/`RegisterPackages` are retired shipped public
APIs whose registration mechanism no longer exists. Rather than silently
doing nothing, they now throw `NotSupportedException` carrying an explanation
of why the call is unsupported and what replaced it ("Java package type
registration is no longer supported. Java-to-managed type resolution now goes
through the native and trimmable type maps."), so a caller that still invokes
them gets a clear, actionable reason instead of a bare exception.
### [Mono.Android] Don't bump external/Java.Interop in this PR
Revert the external/Java.Interop submodule bump. Pinning an unmerged
java-interop PR-branch commit is fragile, and advancing to java-interop
main HEAD would drag in unrelated changes (e.g. dotnet/java-interop#1441)
that need their own coordination. The generator-side removal lives in
dotnet/java-interop#1470 and will reach Mono.Android through the normal
submodule bump once it merges; this PR keeps only the independent runtime
cleanup of the now-dead `packageLookup` plumbing.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers added a commit to dotnet/android that referenced this pull request Jun 16, 2026
Bumps [external/Java.Interop](https://github.com/dotnet/java-interop) from `b881d21` to [`6820a9cb`](dotnet/java-interop@6820a9c).
## Java.Interop changes pulled in
- [`6820a9c`](dotnet/java-interop@6820a9c) [Java.Interop] Make JavaProxyObject.RegisterNativeMembers private again ([#1468](dotnet/java-interop#1468))
- [`a2159d7`](dotnet/java-interop@a2159d70) Remove NuGet dependency grouping from Dependabot config ([#1459](dotnet/java-interop#1459))
- [`a0ca676`](dotnet/java-interop@a0ca6760) [ci] Update agentic workflows to claude-opus-4.8 ([#1461](dotnet/java-interop#1461))
- [`6d8aef7`](dotnet/java-interop@6d8aef7b) Bump to dotnet/android-tools@1fb68b6 ([#1457](dotnet/java-interop#1457))
- [`cfca8ad`](dotnet/java-interop@cfca8ad) Upgrade gh-aw to v0.79.6 and recompile workflows ([#1460](dotnet/java-interop#1460))
- [`0104236`](dotnet/java-interop@0104236e) [JniValueManager] Make TryConstructPeer virtual ([#1456](dotnet/java-interop#1456))
- [`d7dbad5`](dotnet/java-interop@d7dbad5) Remove Java.Interop.Dynamic ([#1449](dotnet/java-interop#1449))
- [`0244c10`](dotnet/java-interop@0244c10) Split reflection-based JniTypeManager and JniValueManager behavior ([#1441](dotnet/java-interop#1441))
- [`9ca7064`](dotnet/java-interop@9ca7064) [Java.Interop] Remove legacy `NET` preprocessor paths ([#1451](dotnet/java-interop#1451))
- [`cd2fc12`](dotnet/java-interop@cd2fc12) Remove Java.Interop.GenericMarshaler ([#1450](dotnet/java-interop#1450))
See full diff in [compare view](dotnet/java-interop@b881d21...6820a9c).
## Changes in this PR (beyond the submodule bump)
The Java.Interop bump — particularly [#1441](dotnet/java-interop#1441) (split reflection-based `JniTypeManager` and `JniValueManager`) and [#1449](dotnet/java-interop#1449) (removal of `Java.Interop.Dynamic`) — required a number of follow-on changes in `dotnet/android`:
### Runtime / `Mono.Android`
- **Adapt Android runtime to Java.Interop reflection managers** — switch the `JavaInteropTypeManager` / `JavaInteropValueManager` hierarchy to the new reflection-based base types and update type/value manager construction to match.
- **Change base type of value managers and type managers** so the runtime composes correctly with the new Java.Interop split.
- **Suppress reflection manager trim warnings only** — narrow the existing trim suppressions so they apply to the reflection-only managers and don't hide other warnings.
- **Suppress IL2068 on `ManagedTypeManager.GetTypeForSimpleReference`** — annotate the entry point that legitimately requires reflection so trim analysis is satisfied.
### Tests / apkdescs
- **Regenerate `BuildReleaseArm64SimpleDotNet.MonoVM.apkdesc`** to track size changes from the Java.Interop bump.
- **Regenerate Simple `BuildReleaseArm64` apkdescs** (`MonoVM` + `NativeAOT`) from a `Release` SDK so they match what CI produces.
- **Update NativeAOT warning expectations** — `Mono.Android` now produces fewer trim/AOT warnings, so loosen the strict `4 Warning(s)` checks in `SupportedOSPlatformVersion` and `CheckSignApk` to `AssertHasAtMostWarnings (2)` (new helper in `AssertionExtensions`) and drop the `BuildHasTrimmerWarnings` data counts (`3 → 2`, `4 → 3`) accordingly.
### Merges
- Two merges from `origin/main` to keep the branch current and resolve a conflict in `Microsoft.Android.Sdk.TypeMap.Trimmable.targets`.
Co-authored-by: Simon Rozsival <simon@rozsival.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Jonathan Peppers <jonathan.peppers@microsoft.com>
jonathanpeppers pushed a commit that referenced this pull request Jun 17, 2026
… "trimmable type map" (#1454)
Follow-up to #1441.
Prerequisite for dotnet/android#11617.
This keeps the Java.Interop changes focused on the small base hook dotnet/android needs for the trimmable type-map integration. The reflection value manager continues to use value marshalers internally, while Android's generated/trimmable value manager can provide the only production `JavaObjectArray<T>` element-assignment object-reference path without implementing `GetValueMarshaler*()`.
## Changes
- Add minimal `JniValueManager` object-reference API for `JavaObjectArray<T>.SetElementAt()`:
- `CreateLocalObjectReferenceArgument(Type type, object? value)` returns an owned local `JniObjectReference` for element assignment. Callers must dispose the returned reference.
- Make the matching core method abstract so non-reflection value managers can implement this path directly.
- Keep value marshalers as a `ReflectionJniValueManager` implementation detail: reflection creates marshaler state, copies out an independent local reference, then destroys the state immediately.
- Update `JavaObjectArray<T>` to call the value manager directly instead of calling `GetValueMarshaler<T>()` in production paths.
- Simplify `JavaObjectArray<T>.Clear()` to set array slots to Java null directly; it no longer needs value-manager or value-marshaler state.
- Remove the earlier exposed proxy/peerable marshaler accessors, broad/generic state overloads, default-value state API, destroy-state API, and `ParameterAttributes synchronize` from this value-manager object-reference path.
- Keep ManagedPeer-dependent tests categorized as unsupported for the Android trimmable configuration rather than carrying Android-specific Java fixture workarounds in this PR.
- Include the small type-manager/test cleanups needed by the dotnet/android integration branch.
## Non-goals
- This PR does not make value marshalers public trimmable API.
- This PR does not require trimmable Android value managers to implement or use `GetValueMarshaler*()`.
- This PR does not remove or replace ManagedPeer-dependent Java.Interop test fixtures.
## Validation
- `dotnet build external/Java.Interop/src/Java.Interop/Java.Interop.csproj -p:Configuration=Debug -m:1 -nodeReuse:false --no-restore -v:minimal`
- From the dotnet/android integration branch:
- `dotnet test tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj -v minimal --no-restore` (`562` passed)
- `dotnet build src/Mono.Android/Mono.Android.csproj -p:Configuration=Debug -p:AndroidSdkDirectory=/Users/simonrozsival/android-toolchain/sdk -m:1 -nodeReuse:false --no-restore -v:minimal` compiled `Mono.Android.Runtime.dll`; the remaining local failure is Android SDK provisioning (`extras/android/m2repository.staging` and `docs.staging` missing), not C# or trim-analyzer errors.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers added a commit to dotnet/android that referenced this pull request Jun 30, 2026
The ReflectionJniTypeManager [RequiresDynamicCode] attribute predates this submodule bump (added in dotnet/java-interop#1441), so the ManagedTypeManager..ctor() IL3050 is already part of the BuildHasNoWarnings test's baseline of 4 warnings. Only the RegisterNativeMembers warning is new from this PR; suppressing the ctor warning too would drop the count to 2 and fail the test.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

ready-to-reviewThis PR is ready to review/merge, thanks!

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@simonrozsival@jonathanpeppers
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
This repository was archived by the owner on Aug 27, 2026. It is now read-only.

Split reflection-based JniTypeManager and JniValueManager behavior - #1441

Merged
jonathanpeppers merged 30 commits into
mainfrom
dev/simonrozsival/fix-aot-warnings-jni-type-manager
Jun 9, 2026
Merged

Split reflection-based JniTypeManager and JniValueManager behavior#1441
jonathanpeppers merged 30 commits into
mainfrom
dev/simonrozsival/fix-aot-warnings-jni-type-manager

Conversation

@simonrozsival

@simonrozsivalsimonrozsival commented Jun 4, 2026

Copy link
Copy Markdown
Member

Splits the reflection-backed paths out of JniRuntime.JniTypeManager and JniRuntime.JniValueManager so callers that don't need reflection (e.g. NativeAOT) can opt out, while keeping the public API surface as compatible as possible for callers migrating from .NET 10.

What changed

  • JniRuntime.ReflectionJniTypeManager (new): all reflection-based type lookup, signature discovery, and native-method registration moves here. Annotated with [RequiresDynamicCode] / [RequiresUnreferencedCode].
  • JniRuntime.ReflectionJniValueManager (new): all reflection-based peer construction, value marshaling, and activation moves here.
  • JniRuntime.JniTypeManager stays a non-abstract base class. Methods that previously had reflection defaults are now virtual no-ops (or return null/empty). Subclasses that want the old default behavior should derive from ReflectionJniTypeManager.
  • JniRuntime.JniValueManager stays abstract (it already was). A few previously-virtual methods (CreatePeer, GetValueMarshalerCore(Type)) become abstract since their reflection-based defaults moved out — public consumers (dotnet/android's value managers) already override these.
  • In-tree consumers updated: JreTypeManager, ManagedValueManager, MonoRuntimeValueManager now derive from the new Reflection* bases. The NativeAOT samples keep deriving from the abstract bases with explicit type mappings.

Public API impact

PublicAPI.Shipped.txt is unchanged. All breaking changes are tracked in PublicAPI.Unshipped.txt with *REMOVED* markers:

  • JniTypeManager.TryRegisterNativeMembers (both overloads) — moved to ReflectionJniTypeManager as protected helpers.
  • JniValueManager.CreatePeer and GetValueMarshalerCore(Type)virtualabstract.

A non-abstract JniTypeManager keeps new JniRuntime.JniTypeManager() and direct subclasses compiling without changes.

Validation

  • CI is green.
  • Local dotnet build Java.Interop.sln passes (PublicApiAnalyzer enforces tracking is consistent).

@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/fix-aot-warnings-jni-type-manager branch from 5ff4c17 to 6d72140CompareJune 4, 2026 14:24
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/fix-aot-warnings-jni-type-manager branch from 6d72140 to 58f0dbdCompareJune 4, 2026 14:30
@simonrozsival

Copy link
Copy Markdown
MemberAuthor

/azp run

@azure-pipelines

Copy link
Copy Markdown
No pipelines are associated with this pull request.

@simonrozsival
simonrozsival marked this pull request as ready for review June 8, 2026 11:55
CopilotAI review requested due to automatic review settings June 8, 2026 11:55

CopilotAI 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.

Pull request overview

This draft PR refactors JniRuntime.JniTypeManager into an abstract base and moves the default reflection-based behavior into a new JniRuntime.DynamicJniTypeManager, with corresponding updates across tests, JRE runtime helpers, samples, and public API baselines.

Changes:

  • Introduces JniRuntime.DynamicJniTypeManager and makes JniRuntime.JniTypeManager abstract, shifting default/reflection-backed logic into the new derived type.
  • Updates test and JRE type managers to inherit from DynamicJniTypeManager, and adjusts tests for new behaviors (e.g., generic invoker handling, GetType array behavior).
  • Updates trimmer/AOT annotations and PublicAPI baselines to reflect the split and new abstract members.
Show a summary per file
FileDescription
tests/Java.Interop-Tests/Java.Interop/JniTypeManagerTests.csUpdates expectations around GetType behavior (notably array signatures and generic holder mapping).
tests/Java.Interop-Tests/Java.Interop/JniRuntimeTest.csSwitches proxy manager to DynamicJniTypeManager and adds trim suppression for test-only reflection use.
tests/Java.Interop-Tests/Java.Interop/JniRuntime.JniTypeManagerTests.csAdds a generic invoker negative test and updates test manager base type.
tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.csUpdates fixture type manager to DynamicJniTypeManager and refines type lookup with trimming annotations.
src/Java.Runtime.Environment/Java.Interop/JreTypeManager.csMoves to DynamicJniTypeManager and adds a DAM-annotated GetTypeForSimpleReference override.
src/Java.Interop/PublicAPI.Unshipped.txtRecords newly introduced/reshaped APIs (new type, newly-abstract members, overrides).
src/Java.Interop/PublicAPI.Shipped.txtUpdates shipped API surface to reflect newly-abstract members and removed methods from JniTypeManager.
src/Java.Interop/Java.Interop/JniValueMarshaler.csSimplifies expression-based marshaler instantiation and adjusts trimming annotations.
src/Java.Interop/Java.Interop/JniRuntime.JniValueManager.csSwitches peer-type selection to GetReflectionConstructibleTypes() and refactors uninitialized peer creation.
src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.csMakes JniTypeManager abstract and factors previously-inlined logic into abstract core methods.
src/Java.Interop/Java.Interop/JniRuntime.DynamicJniTypeManager.csAdds the new reflection-backed type manager implementation and related registration behavior.
src/Java.Interop/Java.Interop/JniRuntime.csRemoves the default new JniTypeManager() fallback; requires an explicit type manager in creation options.
src/Java.Interop/Java.Interop/JavaPrimitiveArrays.ttRetargets primitive array helper generation to DynamicJniTypeManager.
src/Java.Interop/Java.Interop/JavaPrimitiveArrays.csUpdates the generated primitive array helper container type to DynamicJniTypeManager.
src/Java.Interop/GlobalSuppressions.csAdds CA1034 suppression for the new nested DynamicJniTypeManager type.
samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.csUpdates sample type manager implementation to new hierarchy and adds explicit native member registration.
samples/Hello-NativeAOTFromJNI/ManagedType.csChanges the native registration hook to internal for sample-driven registration.
samples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.csUpdates sample type manager implementation to new hierarchy and implements abstract members.

Copilot's findings

  • Files reviewed: 18/18 changed files
  • Comments generated: 3

Comment threadsamples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs Outdated
Comment threadsamples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs Outdated
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival
simonrozsival marked this pull request as draft June 8, 2026 14:24
simonrozsivaland others added 9 commits June 8, 2026 16:32
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>
Map Java.Interop's internal proxy peer types in the base type manager so custom AOT-safe managers do not need reflection-based DynamicJniTypeManager behavior for JavaProxyThrowable/JavaProxyObject.
Register built-in proxy native members by JNI type name before asking custom managers for a managed Type, avoiding DAM-annotated lookups of internal Exception/Delegate-derived types.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Let the base JniTypeManager resolve primitive JNI keyword signatures without requiring custom AOT-safe managers to duplicate the dynamic type manager's primitive mappings.
Use that base lookup as a fallback when matching Java-to-managed constructor signatures so signatures such as (I)V resolve to System.Int32.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival
simonrozsival marked this pull request as ready for review June 9, 2026 07:23
simonrozsivaland others added 2 commits June 9, 2026 09:31
Rename DynamicJniTypeManager to ReflectionJniTypeManager so the type name describes the reflection-based behavior that is incompatible with Native AOT.
Update the matching source file name, API baseline, JRE type manager inheritance, generated primitive-array partial, and tests.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsivalsimonrozsival changed the title Split reflection-based JniTypeManager behaviorSplit reflection-based JniTypeManager and JniValueManager behaviorJun 9, 2026

CopilotAI commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved the merge conflicts in commit Resolve merge conflicts: remove #if NET guards, keep abstract methods in base JniTypeManager.

The two new commits in main since the last merge (Remove Java.Interop.GenericMarshaler (#1450) and Remove legacy NET preprocessor paths (#1451)) conflicted with our branch in three files:

  • JniRuntime.JniTypeManager.cs (two conflicts): Kept the abstract method declarations (GetInvokerTypeCore, GetStaticMethodFallbackTypesCore, RegisterNativeMembers) from our branch while adopting the #if NET guard removals from main. The implementation helpers (TryRegisterNativeMembers, TryLoadJniMarshalMethods, etc.) remain in DynamicJniTypeManager where our PR moved them.
  • ManagedPeer.cs: Removed the #if NET guard that was wrapping the ReadOnlySpan<char> methods block.
  • JreRuntime.cs: Kept our branch's structure (TypeManager default set in the [RequiresDynamicCode]-annotated CreateJreVM, not in CreateJreVMCore), discarding main's addition of builder.TypeManager ??= new JreTypeManager(...) to CreateJreVMCore which would have bypassed the AOT warning.

Also removed the remaining #if NET guards from JniRuntime.ReflectionJniTypeManager.cs and JreRuntime.cs that were in our branch but no longer needed after the main merge.

@simonrozsival

Copy link
Copy Markdown
MemberAuthor

/review

@github-actions

github-actionsBot commented Jun 9, 2026

Copy link
Copy Markdown

Java.Interop PR Reviewer completed successfully!

@github-actionsgithub-actionsBot 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.

✅ LGTM — Clean architectural split

Summary: This PR cleanly separates the reflection-based JniTypeManager / JniValueManager behavior into dedicated ReflectionJniTypeManager and ReflectionJniValueManager subclasses, making the base classes abstract and properly annotating the reflection-based paths with [RequiresDynamicCode] / [RequiresUnreferencedCode]. The NativeAOT samples are updated to implement the abstract base directly, and tests/downstream types are updated accordingly.

What looks good:

  • The split is well-structured — base classes contain only the contract and built-in type handling; all reflection goes into the Reflection* subclasses.
  • [RequiresDynamicCode] and [RequiresUnreferencedCode] annotations are applied consistently throughout the chain (samples, tests, JRE runtime types).
  • Public API baselines are updated correctly.
  • TryRegisterBuiltInNativeMembers is a nice extraction that NativeAOT consumers can call without needing the full reflection machinery.
  • The JreRuntimeOptions.CreateJreVM(JniTypeManager) overload is a good ergonomic addition.

Issues by severity:

SeverityCount
⚠️ Warning1
💡 Suggestion3

Notes:

  • CI only shows license/cla — no build/test results visible yet. The PR is not mergeable until CI validates the full build.
  • The large abstract surface area for NativeAOT consumers (~12 methods) may be worth revisiting — see inline suggestion about making unused extension points virtual with default null returns.

Generated by Java.Interop PR Reviewer for issue #1441 · ● 14.3M

Comment threadsamples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs
Comment threadsamples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs
Comment threadsrc/Java.Interop/Java.Interop/JniRuntime.ReflectionJniTypeManager.cs Outdated
simonrozsivaland others added 2 commits June 9, 2026 22:20
Map JNI primitive and boxed primitive signatures in the NativeAOT sample type managers so constructor signature matching can resolve primitive parameters such as I to System.Int32.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
To minimize migration pain for callers moving from .NET 10 to .NET 11,
revert JniTypeManager from `abstract` back to a regular class. Methods
that were turned into `abstract` overrides now have `virtual` defaults
so existing subclasses keep compiling:
* Type lookup methods return null/empty defaults.
* RegisterNativeMembers is a no-op by default; reflection-based
behavior remains in ReflectionJniTypeManager (override to opt-in).
Restore PublicAPI.Shipped.txt to its main-branch state (per the
`Microsoft.CodeAnalysis.PublicApiAnalyzers` convention, never edit
shipped entries in place) and track every breaking change in
PublicAPI.Unshipped.txt:
* 5 new JniTypeManager.* APIs are now `virtual` (not `abstract`).
* `*REMOVED*` entries for the two `JniTypeManager.TryRegisterNativeMembers`
overloads that moved to `ReflectionJniTypeManager`.
* `*REMOVED*virtual` + `abstract` pairs for the two virtual->abstract
transitions on `JniValueManager` (CreatePeer, GetValueMarshalerCore(Type)).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jonathanpeppers
jonathanpeppers merged commit 0244c10 into mainJun 9, 2026
2 checks passed
@jonathanpeppers
jonathanpeppers deleted the dev/simonrozsival/fix-aot-warnings-jni-type-manager branch June 9, 2026 23:17
jonathanpeppers pushed a commit that referenced this pull request Jun 12, 2026
Follow-up to #1441 We need this method virtual so that `JavaMarshalValueManager` can override it and try to invoke the `XAConstructorSignature`. The method was previously virtual but as an oversight I removed it in #1441:
- https://github.com/dotnet/java-interop/pull/1441/changes#diff-8ead12d157bc7712affd3d920bb93cddbff3885147fe6297957a44cc522d5430R7
- https://github.com/dotnet/java-interop/pull/1441/changes#diff-b92884de5db7f82df63b483b8319360cd9bfdac3cd97222eb9aff7292a01a07bL449-L467
I noticed we're not disposing the `reference` correctly which might cause a ref leak. In the Mono `AndroidValueManager` we never called into the base method so this would not be observed on mono previously (https://github.com/dotnet/android/blob/main/src/Mono.Android/Java.Interop/TypeManager.cs#L416-L423).
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
simonrozsival added a commit that referenced this pull request Jun 15, 2026
#1441 added `JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers ()`
-- a reflection-free direct call to `JavaProxyObject.RegisterNativeMembers`
-- and bumped that method to `internal` so the call would compile.
That method is dead code: nothing in Java.Interop (or dotnet/android) ever
calls it, and the base `JniTypeManager.RegisterNativeMembers` is a no-op.
`JavaProxyObject`'s native members are -- and were before #1441 --
registered purely via reflection in `ReflectionJniTypeManager`
(`FindAndCallRegisterMethod` discovering the
`[JniAddNativeMethodRegistrationAttribute]`-annotated method), which finds
private methods just fine via `GetRuntimeMethods ()`.
Remove the unused `TryRegisterBuiltInNativeMembers` and restore
`RegisterNativeMembers` to `private` (its state before #1441). Making it
private again also strips the attribute-bearing method from the reference
assembly, which fixes dotnet/android's trimmable typemap scanner falsely
rejecting the built-in `JavaProxyObject` with XA4251 (dotnet/android#11622).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit that referenced this pull request Jun 15, 2026
Replaces the production TryRegisterBuiltInNativeMembers helper with a
samples-only solution, restoring the pre-#1441 behavior for the NativeAOT
type managers.
Background: making JavaProxyObject.RegisterNativeMembers private (the XA4251
fix) is sufficient for the default Android runtime, which discovers and invokes
it via reflection (FindAndCallRegisterMethod -> GetRuntimeMethods(), which
returns private methods). The only thing that needed JavaProxyObject's
registration via a non-reflection path was the two NativeAOT samples, which
#1441 had rewritten to derive from the reflection-free JniRuntime.JniTypeManager
base. That is why #1441 introduced TryRegisterBuiltInNativeMembers.
Instead of carrying that helper in production, have the sample type managers
derive from JniRuntime.ReflectionJniTypeManager again - exactly what the base
JniRuntime.JniTypeManager provided before #1441. Built-in types such as
JavaProxyObject/JavaProxyThrowable are then registered automatically via
reflection, and the samples no longer need any custom registration code.
Changes:
- JavaProxyObject.RegisterNativeMembers: internal -> private. The marshalers
stay (the default reflection runtime registers them by reflecting over this
type); only the visibility changes, which strips the attribute from the
reference assembly and fixes the dotnet/android XA4251 scanner failure.
- Delete JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers (+ its
PublicAPI.Unshipped.txt entry); no longer needed.
- Hello-NativeAOTFromJNI / Hello-NativeAOTFromAndroid: derive from
JniRuntime.ReflectionJniTypeManager and drop the reflection-free overrides and
hand-written JavaProxyObject registration. ReflectionJniTypeManager is
[RequiresDynamicCode]/[RequiresUnreferencedCode], so the constructor suppresses
IL2026/IL3050 with [UnconditionalSuppressMessage]. A #pragma is insufficient
here: it silences the Roslyn analyzer but not the ILLink/ILC publish passes,
which only honor the attribute (verified with a trim-publish of the real type
manager against Java.Interop.dll).
Net: production loses code (1-line visibility change + deletions) and the samples
shrink substantially.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit that referenced this pull request Jun 16, 2026
Build 1465402's "run Hello-NativeAOTFromJNI" step failed at runtime:
System.NotSupportedException: Could not find System.Type corresponding to
Java type JniTypeSignature(TypeName=example/ManagedType ...)
at Java.Interop.ManagedPeer.RegisterNativeMembers(...)
at example.ManagedType.<clinit>(ManagedType.java:15)
Root cause: after #1441, JniRuntime.JniTypeManager.GetType() dispatches through
GetTypeForSimpleReference (singular), not GetTypesForSimpleReference (plural).
The previous sample rewrite only overrode the plural method, so app types like
example/ManagedType were never resolved (the base ReflectionJniTypeManager's
GetTypeForSimpleReference only knows built-in types), and ManagedPeer
registration threw before it could register the type.
Fix: override GetTypeForSimpleReference (singular) in both NativeAOT sample type
managers to resolve the sample's own managed types, falling back to the base for
built-ins. Registration and the reverse Type->JNI mapping continue to be handled
by the reflection base (the pre-#1441 behavior). The override carries the same
[return: DynamicallyAccessedMembers(...)] as the base to satisfy IL2093.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit that referenced this pull request Jun 16, 2026
Add a class-level comment to both NativeAOT sample type managers explaining the
rationale for the [UnconditionalSuppressMessage] IL2026/IL3050 suppressions:
- These are *samples*, not product code. .NET for Android (what we ship) does not
pair ReflectionJniTypeManager with NativeAOT, so it isn't worth the effort to make
these samples fully trim/AOT-clean right now.
- The reflection paths were always trim/AOT-unsafe. Before #1441 the equivalent
suppressions lived inside JniTypeManager itself (justified "NotUsedInAndroid");
#1441 simply moved that responsibility to callers via
[RequiresDynamicCode]/[RequiresUnreferencedCode].
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit to dotnet/android that referenced this pull request Jun 16, 2026
Revert the external/Java.Interop submodule bump. Pinning an unmerged
java-interop PR-branch commit is fragile, and advancing to java-interop
main HEAD would drag in unrelated changes (e.g. dotnet/java-interop#1441)
that need their own coordination. The generator-side removal lives in
dotnet/java-interop#1470 and will reach Mono.Android through the normal
submodule bump once it merges; this PR keeps only the independent runtime
cleanup of the now-dead `packageLookup` plumbing.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers pushed a commit that referenced this pull request Jun 16, 2026
…in (#1468)
#1441 changed `JavaProxyObject.RegisterNativeMembers` from `private` to
`internal` so the new reflection-free
`JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers ()` could call
it directly.
A side effect is that the `[JniAddNativeMethodRegistrationAttribute]`-
annotated method now appears in Java.Interop's *reference* assembly.
dotnet/android's trimmable typemap scanner reads reference assemblies and
rejects any type carrying that attribute (XA4251), so every trimmable
typemap build now fails on the built-in `JavaProxyObject`
(see dotnet/android#11622).
Restore the method to `private` (so it is stripped from the reference
assembly) and expose a small attribute-free `internal` entry point,
`RegisterBuiltInNativeMembers (JniType)`, for the built-in registration
path to call. Runtime behavior is unchanged.
### [Java.Interop] Restore pure-reflection registration for JavaProxyObject
#1441 added `JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers ()`
-- a reflection-free direct call to `JavaProxyObject.RegisterNativeMembers`
-- and bumped that method to `internal` so the call would compile.
That method is dead code: nothing in Java.Interop (or dotnet/android) ever
calls it, and the base `JniTypeManager.RegisterNativeMembers` is a no-op.
`JavaProxyObject`'s native members are -- and were before #1441 --
registered purely via reflection in `ReflectionJniTypeManager`
(`FindAndCallRegisterMethod` discovering the
`[JniAddNativeMethodRegistrationAttribute]`-annotated method), which finds
private methods just fine via `GetRuntimeMethods ()`.
Remove the unused `TryRegisterBuiltInNativeMembers` and restore
`RegisterNativeMembers` to `private` (its state before #1441). Making it
private again also strips the attribute-bearing method from the reference
assembly, which fixes dotnet/android's trimmable typemap scanner falsely
rejecting the built-in `JavaProxyObject` with XA4251 (dotnet/android#11622).
### Address review: restore TryRegisterBuiltInNativeMembers (not dead code)
The previous commit deleted JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers
claiming it was unused. That was wrong: it has two live callers in the NativeAOT
samples (samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs and
samples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs), both JniTypeManager
subclasses. Deleting it broke them (CS0103) and removed the only path that
registers JavaProxyObject's equals/hashCode/toString natives for NativeAOT type
managers (which don't use the reflection-based FindAndCallRegisterMethod path).
The breakage was hidden in CI only because the NativeAOT sample steps use
continueOnError: true.
Keep the XA4251 fix (RegisterNativeMembers stays private, so the
[JniAddNativeMethodRegistrationAttribute] is stripped from the reference assembly)
and restore TryRegisterBuiltInNativeMembers by extracting the registration logic
into a new attribute-free internal helper, JavaProxyObject.AddBuiltInRegistrations.
Both the private attributed RegisterNativeMembers (reflection path) and
TryRegisterBuiltInNativeMembers (NativeAOT path) call it. Restore the
PublicAPI.Unshipped.txt entry as well.
Verified: Java.Interop.dll builds clean; the reference assembly contains no method
carrying [JniAddNativeMethodRegistration]; the sample's call to
TryRegisterBuiltInNativeMembers resolves (no CS0103).
### Keep built-in registration in the samples via ReflectionJniTypeManager
Replaces the production TryRegisterBuiltInNativeMembers helper with a
samples-only solution, restoring the pre-#1441 behavior for the NativeAOT
type managers.
Background: making JavaProxyObject.RegisterNativeMembers private (the XA4251
fix) is sufficient for the default Android runtime, which discovers and invokes
it via reflection (FindAndCallRegisterMethod -> GetRuntimeMethods(), which
returns private methods). The only thing that needed JavaProxyObject's
registration via a non-reflection path was the two NativeAOT samples, which
#1441 had rewritten to derive from the reflection-free JniRuntime.JniTypeManager
base. That is why #1441 introduced TryRegisterBuiltInNativeMembers.
Instead of carrying that helper in production, have the sample type managers
derive from JniRuntime.ReflectionJniTypeManager again - exactly what the base
JniRuntime.JniTypeManager provided before #1441. Built-in types such as
JavaProxyObject/JavaProxyThrowable are then registered automatically via
reflection, and the samples no longer need any custom registration code.
Changes:
- JavaProxyObject.RegisterNativeMembers: internal -> private. The marshalers
stay (the default reflection runtime registers them by reflecting over this
type); only the visibility changes, which strips the attribute from the
reference assembly and fixes the dotnet/android XA4251 scanner failure.
- Delete JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers (+ its
PublicAPI.Unshipped.txt entry); no longer needed.
- Hello-NativeAOTFromJNI / Hello-NativeAOTFromAndroid: derive from
JniRuntime.ReflectionJniTypeManager and drop the reflection-free overrides and
hand-written JavaProxyObject registration. ReflectionJniTypeManager is
[RequiresDynamicCode]/[RequiresUnreferencedCode], so the constructor suppresses
IL2026/IL3050 with [UnconditionalSuppressMessage]. A #pragma is insufficient
here: it silences the Roslyn analyzer but not the ILLink/ILC publish passes,
which only honor the attribute (verified with a trim-publish of the real type
manager against Java.Interop.dll).
Net: production loses code (1-line visibility change + deletions) and the samples
shrink substantially.
### Drop verbose explanatory comments from the NativeAOT sample type managers
The [UnconditionalSuppressMessage] Justification strings already convey the
necessary context.
### Fix NativeAOT sample type resolution: override GetTypeForSimpleReference
Build 1465402's "run Hello-NativeAOTFromJNI" step failed at runtime:
System.NotSupportedException: Could not find System.Type corresponding to
Java type JniTypeSignature(TypeName=example/ManagedType ...)
at Java.Interop.ManagedPeer.RegisterNativeMembers(...)
at example.ManagedType.<clinit>(ManagedType.java:15)
Root cause: after #1441, JniRuntime.JniTypeManager.GetType() dispatches through
GetTypeForSimpleReference (singular), not GetTypesForSimpleReference (plural).
The previous sample rewrite only overrode the plural method, so app types like
example/ManagedType were never resolved (the base ReflectionJniTypeManager's
GetTypeForSimpleReference only knows built-in types), and ManagedPeer
registration threw before it could register the type.
Fix: override GetTypeForSimpleReference (singular) in both NativeAOT sample type
managers to resolve the sample's own managed types, falling back to the base for
built-ins. Registration and the reverse Type->JNI mapping continue to be handled
by the reflection base (the pre-#1441 behavior). The override carries the same
[return: DynamicallyAccessedMembers(...)] as the base to satisfy IL2093.
### Address review: drop unreachable null guard in FromAndroid sample
`typeMappings` is assigned in its field initializer and never set to null, so the
`if (typeMappings == null) yield break;` guard in CreateSimpleReferencesEnumerator
was unreachable dead code. Remove it.
### Document why the NativeAOT sample trim/AOT suppressions are acceptable
Add a class-level comment to both NativeAOT sample type managers explaining the
rationale for the [UnconditionalSuppressMessage] IL2026/IL3050 suppressions:
- These are *samples*, not product code. .NET for Android (what we ship) does not
pair ReflectionJniTypeManager with NativeAOT, so it isn't worth the effort to make
these samples fully trim/AOT-clean right now.
- The reflection paths were always trim/AOT-unsafe. Before #1441 the equivalent
suppressions lived inside JniTypeManager itself (justified "NotUsedInAndroid");
#1441 simply moved that responsibility to callers via
[RequiresDynamicCode]/[RequiresUnreferencedCode].
### Reword NativeAOT sample suppression justifications to describe why it's safe
The previous IL2026/IL3050 justifications ("does not require unreferenced code /
runtime code generation") read as the opposite of reality, since
ReflectionJniTypeManager is exactly [RequiresUnreferencedCode]/[RequiresDynamicCode].
Reword to describe why the suppression is correct for this sample:
- IL2026: the assembly is rooted via TrimmerRootAssembly and the reflected
registration members are preserved by the [DynamicallyAccessedMembers]
annotations on the RegisterNativeMembers(Type) -> FindAndCallRegisterMethod path.
- IL3050: registration uses CreateDelegate on compile-time-known static methods
(no MakeGenericType / expression compilation), so no runtime codegen is required.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers pushed a commit to dotnet/android that referenced this pull request Jun 16, 2026
…mbing (#11667)
The binding generator used to emit a `Java.Interop.__TypeRegistrations`
class whose `RegisterPackages ()` populated `TypeManager.packageLookup`.
The only reader of that dictionary -- the `TypeRegistrationFallback` path
in `GetJavaToManagedTypeCore` -- was removed in #9471, so the dictionary
has been write-only (and, since nothing generated calls it, effectively
unused) ever since. Type resolution now goes through the native /
trimmable type map.
This bumps external/Java.Interop to the generator change that stops
emitting the dead class (dotnet/java-interop#1470) and removes the
now-dead runtime plumbing:
* Delete the `packageLookup` field and `LazyInitPackageLookup ()`.
* `TypeManager.RegisterPackage`/`RegisterPackages` are shipped public
API, so they are kept as validated no-op stubs rather than removed.
Fixes#11663
### [Mono.Android] Throw NotSupportedException with an explanatory message
`TypeManager.RegisterPackage`/`RegisterPackages` are retired shipped public
APIs whose registration mechanism no longer exists. Rather than silently
doing nothing, they now throw `NotSupportedException` carrying an explanation
of why the call is unsupported and what replaced it ("Java package type
registration is no longer supported. Java-to-managed type resolution now goes
through the native and trimmable type maps."), so a caller that still invokes
them gets a clear, actionable reason instead of a bare exception.
### [Mono.Android] Don't bump external/Java.Interop in this PR
Revert the external/Java.Interop submodule bump. Pinning an unmerged
java-interop PR-branch commit is fragile, and advancing to java-interop
main HEAD would drag in unrelated changes (e.g. dotnet/java-interop#1441)
that need their own coordination. The generator-side removal lives in
dotnet/java-interop#1470 and will reach Mono.Android through the normal
submodule bump once it merges; this PR keeps only the independent runtime
cleanup of the now-dead `packageLookup` plumbing.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers added a commit to dotnet/android that referenced this pull request Jun 16, 2026
Bumps [external/Java.Interop](https://github.com/dotnet/java-interop) from `b881d21` to [`6820a9cb`](dotnet/java-interop@6820a9c).
## Java.Interop changes pulled in
- [`6820a9c`](dotnet/java-interop@6820a9c) [Java.Interop] Make JavaProxyObject.RegisterNativeMembers private again ([#1468](dotnet/java-interop#1468))
- [`a2159d7`](dotnet/java-interop@a2159d70) Remove NuGet dependency grouping from Dependabot config ([#1459](dotnet/java-interop#1459))
- [`a0ca676`](dotnet/java-interop@a0ca6760) [ci] Update agentic workflows to claude-opus-4.8 ([#1461](dotnet/java-interop#1461))
- [`6d8aef7`](dotnet/java-interop@6d8aef7b) Bump to dotnet/android-tools@1fb68b6 ([#1457](dotnet/java-interop#1457))
- [`cfca8ad`](dotnet/java-interop@cfca8ad) Upgrade gh-aw to v0.79.6 and recompile workflows ([#1460](dotnet/java-interop#1460))
- [`0104236`](dotnet/java-interop@0104236e) [JniValueManager] Make TryConstructPeer virtual ([#1456](dotnet/java-interop#1456))
- [`d7dbad5`](dotnet/java-interop@d7dbad5) Remove Java.Interop.Dynamic ([#1449](dotnet/java-interop#1449))
- [`0244c10`](dotnet/java-interop@0244c10) Split reflection-based JniTypeManager and JniValueManager behavior ([#1441](dotnet/java-interop#1441))
- [`9ca7064`](dotnet/java-interop@9ca7064) [Java.Interop] Remove legacy `NET` preprocessor paths ([#1451](dotnet/java-interop#1451))
- [`cd2fc12`](dotnet/java-interop@cd2fc12) Remove Java.Interop.GenericMarshaler ([#1450](dotnet/java-interop#1450))
See full diff in [compare view](dotnet/java-interop@b881d21...6820a9c).
## Changes in this PR (beyond the submodule bump)
The Java.Interop bump — particularly [#1441](dotnet/java-interop#1441) (split reflection-based `JniTypeManager` and `JniValueManager`) and [#1449](dotnet/java-interop#1449) (removal of `Java.Interop.Dynamic`) — required a number of follow-on changes in `dotnet/android`:
### Runtime / `Mono.Android`
- **Adapt Android runtime to Java.Interop reflection managers** — switch the `JavaInteropTypeManager` / `JavaInteropValueManager` hierarchy to the new reflection-based base types and update type/value manager construction to match.
- **Change base type of value managers and type managers** so the runtime composes correctly with the new Java.Interop split.
- **Suppress reflection manager trim warnings only** — narrow the existing trim suppressions so they apply to the reflection-only managers and don't hide other warnings.
- **Suppress IL2068 on `ManagedTypeManager.GetTypeForSimpleReference`** — annotate the entry point that legitimately requires reflection so trim analysis is satisfied.
### Tests / apkdescs
- **Regenerate `BuildReleaseArm64SimpleDotNet.MonoVM.apkdesc`** to track size changes from the Java.Interop bump.
- **Regenerate Simple `BuildReleaseArm64` apkdescs** (`MonoVM` + `NativeAOT`) from a `Release` SDK so they match what CI produces.
- **Update NativeAOT warning expectations** — `Mono.Android` now produces fewer trim/AOT warnings, so loosen the strict `4 Warning(s)` checks in `SupportedOSPlatformVersion` and `CheckSignApk` to `AssertHasAtMostWarnings (2)` (new helper in `AssertionExtensions`) and drop the `BuildHasTrimmerWarnings` data counts (`3 → 2`, `4 → 3`) accordingly.
### Merges
- Two merges from `origin/main` to keep the branch current and resolve a conflict in `Microsoft.Android.Sdk.TypeMap.Trimmable.targets`.
Co-authored-by: Simon Rozsival <simon@rozsival.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Jonathan Peppers <jonathan.peppers@microsoft.com>
jonathanpeppers pushed a commit that referenced this pull request Jun 17, 2026
… "trimmable type map" (#1454)
Follow-up to #1441.
Prerequisite for dotnet/android#11617.
This keeps the Java.Interop changes focused on the small base hook dotnet/android needs for the trimmable type-map integration. The reflection value manager continues to use value marshalers internally, while Android's generated/trimmable value manager can provide the only production `JavaObjectArray<T>` element-assignment object-reference path without implementing `GetValueMarshaler*()`.
## Changes
- Add minimal `JniValueManager` object-reference API for `JavaObjectArray<T>.SetElementAt()`:
- `CreateLocalObjectReferenceArgument(Type type, object? value)` returns an owned local `JniObjectReference` for element assignment. Callers must dispose the returned reference.
- Make the matching core method abstract so non-reflection value managers can implement this path directly.
- Keep value marshalers as a `ReflectionJniValueManager` implementation detail: reflection creates marshaler state, copies out an independent local reference, then destroys the state immediately.
- Update `JavaObjectArray<T>` to call the value manager directly instead of calling `GetValueMarshaler<T>()` in production paths.
- Simplify `JavaObjectArray<T>.Clear()` to set array slots to Java null directly; it no longer needs value-manager or value-marshaler state.
- Remove the earlier exposed proxy/peerable marshaler accessors, broad/generic state overloads, default-value state API, destroy-state API, and `ParameterAttributes synchronize` from this value-manager object-reference path.
- Keep ManagedPeer-dependent tests categorized as unsupported for the Android trimmable configuration rather than carrying Android-specific Java fixture workarounds in this PR.
- Include the small type-manager/test cleanups needed by the dotnet/android integration branch.
## Non-goals
- This PR does not make value marshalers public trimmable API.
- This PR does not require trimmable Android value managers to implement or use `GetValueMarshaler*()`.
- This PR does not remove or replace ManagedPeer-dependent Java.Interop test fixtures.
## Validation
- `dotnet build external/Java.Interop/src/Java.Interop/Java.Interop.csproj -p:Configuration=Debug -m:1 -nodeReuse:false --no-restore -v:minimal`
- From the dotnet/android integration branch:
- `dotnet test tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj -v minimal --no-restore` (`562` passed)
- `dotnet build src/Mono.Android/Mono.Android.csproj -p:Configuration=Debug -p:AndroidSdkDirectory=/Users/simonrozsival/android-toolchain/sdk -m:1 -nodeReuse:false --no-restore -v:minimal` compiled `Mono.Android.Runtime.dll`; the remaining local failure is Android SDK provisioning (`extras/android/m2repository.staging` and `docs.staging` missing), not C# or trim-analyzer errors.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers added a commit to dotnet/android that referenced this pull request Jun 30, 2026
The ReflectionJniTypeManager [RequiresDynamicCode] attribute predates this submodule bump (added in dotnet/java-interop#1441), so the ManagedTypeManager..ctor() IL3050 is already part of the BuildHasNoWarnings test's baseline of 4 warnings. Only the RegisterNativeMembers warning is new from this PR; suppressing the ctor warning too would drop the count to 2 and fail the test.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

ready-to-reviewThis PR is ready to review/merge, thanks!

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@simonrozsival@jonathanpeppers
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
This repository was archived by the owner on Aug 27, 2026. It is now read-only.

Split reflection-based JniTypeManager and JniValueManager behavior - #1441

Merged
jonathanpeppers merged 30 commits into
mainfrom
dev/simonrozsival/fix-aot-warnings-jni-type-manager
Jun 9, 2026
Merged

Split reflection-based JniTypeManager and JniValueManager behavior#1441
jonathanpeppers merged 30 commits into
mainfrom
dev/simonrozsival/fix-aot-warnings-jni-type-manager

Conversation

@simonrozsival

@simonrozsivalsimonrozsival commented Jun 4, 2026

Copy link
Copy Markdown
Member

Splits the reflection-backed paths out of JniRuntime.JniTypeManager and JniRuntime.JniValueManager so callers that don't need reflection (e.g. NativeAOT) can opt out, while keeping the public API surface as compatible as possible for callers migrating from .NET 10.

What changed

  • JniRuntime.ReflectionJniTypeManager (new): all reflection-based type lookup, signature discovery, and native-method registration moves here. Annotated with [RequiresDynamicCode] / [RequiresUnreferencedCode].
  • JniRuntime.ReflectionJniValueManager (new): all reflection-based peer construction, value marshaling, and activation moves here.
  • JniRuntime.JniTypeManager stays a non-abstract base class. Methods that previously had reflection defaults are now virtual no-ops (or return null/empty). Subclasses that want the old default behavior should derive from ReflectionJniTypeManager.
  • JniRuntime.JniValueManager stays abstract (it already was). A few previously-virtual methods (CreatePeer, GetValueMarshalerCore(Type)) become abstract since their reflection-based defaults moved out — public consumers (dotnet/android's value managers) already override these.
  • In-tree consumers updated: JreTypeManager, ManagedValueManager, MonoRuntimeValueManager now derive from the new Reflection* bases. The NativeAOT samples keep deriving from the abstract bases with explicit type mappings.

Public API impact

PublicAPI.Shipped.txt is unchanged. All breaking changes are tracked in PublicAPI.Unshipped.txt with *REMOVED* markers:

  • JniTypeManager.TryRegisterNativeMembers (both overloads) — moved to ReflectionJniTypeManager as protected helpers.
  • JniValueManager.CreatePeer and GetValueMarshalerCore(Type)virtualabstract.

A non-abstract JniTypeManager keeps new JniRuntime.JniTypeManager() and direct subclasses compiling without changes.

Validation

  • CI is green.
  • Local dotnet build Java.Interop.sln passes (PublicApiAnalyzer enforces tracking is consistent).

@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/fix-aot-warnings-jni-type-manager branch from 5ff4c17 to 6d72140CompareJune 4, 2026 14:24
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/fix-aot-warnings-jni-type-manager branch from 6d72140 to 58f0dbdCompareJune 4, 2026 14:30
@simonrozsival

Copy link
Copy Markdown
MemberAuthor

/azp run

@azure-pipelines

Copy link
Copy Markdown
No pipelines are associated with this pull request.

@simonrozsival
simonrozsival marked this pull request as ready for review June 8, 2026 11:55
CopilotAI review requested due to automatic review settings June 8, 2026 11:55

CopilotAI 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.

Pull request overview

This draft PR refactors JniRuntime.JniTypeManager into an abstract base and moves the default reflection-based behavior into a new JniRuntime.DynamicJniTypeManager, with corresponding updates across tests, JRE runtime helpers, samples, and public API baselines.

Changes:

  • Introduces JniRuntime.DynamicJniTypeManager and makes JniRuntime.JniTypeManager abstract, shifting default/reflection-backed logic into the new derived type.
  • Updates test and JRE type managers to inherit from DynamicJniTypeManager, and adjusts tests for new behaviors (e.g., generic invoker handling, GetType array behavior).
  • Updates trimmer/AOT annotations and PublicAPI baselines to reflect the split and new abstract members.
Show a summary per file
FileDescription
tests/Java.Interop-Tests/Java.Interop/JniTypeManagerTests.csUpdates expectations around GetType behavior (notably array signatures and generic holder mapping).
tests/Java.Interop-Tests/Java.Interop/JniRuntimeTest.csSwitches proxy manager to DynamicJniTypeManager and adds trim suppression for test-only reflection use.
tests/Java.Interop-Tests/Java.Interop/JniRuntime.JniTypeManagerTests.csAdds a generic invoker negative test and updates test manager base type.
tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.csUpdates fixture type manager to DynamicJniTypeManager and refines type lookup with trimming annotations.
src/Java.Runtime.Environment/Java.Interop/JreTypeManager.csMoves to DynamicJniTypeManager and adds a DAM-annotated GetTypeForSimpleReference override.
src/Java.Interop/PublicAPI.Unshipped.txtRecords newly introduced/reshaped APIs (new type, newly-abstract members, overrides).
src/Java.Interop/PublicAPI.Shipped.txtUpdates shipped API surface to reflect newly-abstract members and removed methods from JniTypeManager.
src/Java.Interop/Java.Interop/JniValueMarshaler.csSimplifies expression-based marshaler instantiation and adjusts trimming annotations.
src/Java.Interop/Java.Interop/JniRuntime.JniValueManager.csSwitches peer-type selection to GetReflectionConstructibleTypes() and refactors uninitialized peer creation.
src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.csMakes JniTypeManager abstract and factors previously-inlined logic into abstract core methods.
src/Java.Interop/Java.Interop/JniRuntime.DynamicJniTypeManager.csAdds the new reflection-backed type manager implementation and related registration behavior.
src/Java.Interop/Java.Interop/JniRuntime.csRemoves the default new JniTypeManager() fallback; requires an explicit type manager in creation options.
src/Java.Interop/Java.Interop/JavaPrimitiveArrays.ttRetargets primitive array helper generation to DynamicJniTypeManager.
src/Java.Interop/Java.Interop/JavaPrimitiveArrays.csUpdates the generated primitive array helper container type to DynamicJniTypeManager.
src/Java.Interop/GlobalSuppressions.csAdds CA1034 suppression for the new nested DynamicJniTypeManager type.
samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.csUpdates sample type manager implementation to new hierarchy and adds explicit native member registration.
samples/Hello-NativeAOTFromJNI/ManagedType.csChanges the native registration hook to internal for sample-driven registration.
samples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.csUpdates sample type manager implementation to new hierarchy and implements abstract members.

Copilot's findings

  • Files reviewed: 18/18 changed files
  • Comments generated: 3

Comment threadsamples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs Outdated
Comment threadsamples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs Outdated
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival
simonrozsival marked this pull request as draft June 8, 2026 14:24
simonrozsivaland others added 9 commits June 8, 2026 16:32
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>
Map Java.Interop's internal proxy peer types in the base type manager so custom AOT-safe managers do not need reflection-based DynamicJniTypeManager behavior for JavaProxyThrowable/JavaProxyObject.
Register built-in proxy native members by JNI type name before asking custom managers for a managed Type, avoiding DAM-annotated lookups of internal Exception/Delegate-derived types.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Let the base JniTypeManager resolve primitive JNI keyword signatures without requiring custom AOT-safe managers to duplicate the dynamic type manager's primitive mappings.
Use that base lookup as a fallback when matching Java-to-managed constructor signatures so signatures such as (I)V resolve to System.Int32.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival
simonrozsival marked this pull request as ready for review June 9, 2026 07:23
simonrozsivaland others added 2 commits June 9, 2026 09:31
Rename DynamicJniTypeManager to ReflectionJniTypeManager so the type name describes the reflection-based behavior that is incompatible with Native AOT.
Update the matching source file name, API baseline, JRE type manager inheritance, generated primitive-array partial, and tests.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsivalsimonrozsival changed the title Split reflection-based JniTypeManager behaviorSplit reflection-based JniTypeManager and JniValueManager behaviorJun 9, 2026

CopilotAI commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved the merge conflicts in commit Resolve merge conflicts: remove #if NET guards, keep abstract methods in base JniTypeManager.

The two new commits in main since the last merge (Remove Java.Interop.GenericMarshaler (#1450) and Remove legacy NET preprocessor paths (#1451)) conflicted with our branch in three files:

  • JniRuntime.JniTypeManager.cs (two conflicts): Kept the abstract method declarations (GetInvokerTypeCore, GetStaticMethodFallbackTypesCore, RegisterNativeMembers) from our branch while adopting the #if NET guard removals from main. The implementation helpers (TryRegisterNativeMembers, TryLoadJniMarshalMethods, etc.) remain in DynamicJniTypeManager where our PR moved them.
  • ManagedPeer.cs: Removed the #if NET guard that was wrapping the ReadOnlySpan<char> methods block.
  • JreRuntime.cs: Kept our branch's structure (TypeManager default set in the [RequiresDynamicCode]-annotated CreateJreVM, not in CreateJreVMCore), discarding main's addition of builder.TypeManager ??= new JreTypeManager(...) to CreateJreVMCore which would have bypassed the AOT warning.

Also removed the remaining #if NET guards from JniRuntime.ReflectionJniTypeManager.cs and JreRuntime.cs that were in our branch but no longer needed after the main merge.

@simonrozsival

Copy link
Copy Markdown
MemberAuthor

/review

@github-actions

github-actionsBot commented Jun 9, 2026

Copy link
Copy Markdown

Java.Interop PR Reviewer completed successfully!

@github-actionsgithub-actionsBot 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.

✅ LGTM — Clean architectural split

Summary: This PR cleanly separates the reflection-based JniTypeManager / JniValueManager behavior into dedicated ReflectionJniTypeManager and ReflectionJniValueManager subclasses, making the base classes abstract and properly annotating the reflection-based paths with [RequiresDynamicCode] / [RequiresUnreferencedCode]. The NativeAOT samples are updated to implement the abstract base directly, and tests/downstream types are updated accordingly.

What looks good:

  • The split is well-structured — base classes contain only the contract and built-in type handling; all reflection goes into the Reflection* subclasses.
  • [RequiresDynamicCode] and [RequiresUnreferencedCode] annotations are applied consistently throughout the chain (samples, tests, JRE runtime types).
  • Public API baselines are updated correctly.
  • TryRegisterBuiltInNativeMembers is a nice extraction that NativeAOT consumers can call without needing the full reflection machinery.
  • The JreRuntimeOptions.CreateJreVM(JniTypeManager) overload is a good ergonomic addition.

Issues by severity:

SeverityCount
⚠️ Warning1
💡 Suggestion3

Notes:

  • CI only shows license/cla — no build/test results visible yet. The PR is not mergeable until CI validates the full build.
  • The large abstract surface area for NativeAOT consumers (~12 methods) may be worth revisiting — see inline suggestion about making unused extension points virtual with default null returns.

Generated by Java.Interop PR Reviewer for issue #1441 · ● 14.3M

Comment threadsamples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs
Comment threadsamples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs
Comment threadsrc/Java.Interop/Java.Interop/JniRuntime.ReflectionJniTypeManager.cs Outdated
simonrozsivaland others added 2 commits June 9, 2026 22:20
Map JNI primitive and boxed primitive signatures in the NativeAOT sample type managers so constructor signature matching can resolve primitive parameters such as I to System.Int32.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
To minimize migration pain for callers moving from .NET 10 to .NET 11,
revert JniTypeManager from `abstract` back to a regular class. Methods
that were turned into `abstract` overrides now have `virtual` defaults
so existing subclasses keep compiling:
* Type lookup methods return null/empty defaults.
* RegisterNativeMembers is a no-op by default; reflection-based
behavior remains in ReflectionJniTypeManager (override to opt-in).
Restore PublicAPI.Shipped.txt to its main-branch state (per the
`Microsoft.CodeAnalysis.PublicApiAnalyzers` convention, never edit
shipped entries in place) and track every breaking change in
PublicAPI.Unshipped.txt:
* 5 new JniTypeManager.* APIs are now `virtual` (not `abstract`).
* `*REMOVED*` entries for the two `JniTypeManager.TryRegisterNativeMembers`
overloads that moved to `ReflectionJniTypeManager`.
* `*REMOVED*virtual` + `abstract` pairs for the two virtual->abstract
transitions on `JniValueManager` (CreatePeer, GetValueMarshalerCore(Type)).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jonathanpeppers
jonathanpeppers merged commit 0244c10 into mainJun 9, 2026
2 checks passed
@jonathanpeppers
jonathanpeppers deleted the dev/simonrozsival/fix-aot-warnings-jni-type-manager branch June 9, 2026 23:17
jonathanpeppers pushed a commit that referenced this pull request Jun 12, 2026
Follow-up to #1441 We need this method virtual so that `JavaMarshalValueManager` can override it and try to invoke the `XAConstructorSignature`. The method was previously virtual but as an oversight I removed it in #1441:
- https://github.com/dotnet/java-interop/pull/1441/changes#diff-8ead12d157bc7712affd3d920bb93cddbff3885147fe6297957a44cc522d5430R7
- https://github.com/dotnet/java-interop/pull/1441/changes#diff-b92884de5db7f82df63b483b8319360cd9bfdac3cd97222eb9aff7292a01a07bL449-L467
I noticed we're not disposing the `reference` correctly which might cause a ref leak. In the Mono `AndroidValueManager` we never called into the base method so this would not be observed on mono previously (https://github.com/dotnet/android/blob/main/src/Mono.Android/Java.Interop/TypeManager.cs#L416-L423).
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
simonrozsival added a commit that referenced this pull request Jun 15, 2026
#1441 added `JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers ()`
-- a reflection-free direct call to `JavaProxyObject.RegisterNativeMembers`
-- and bumped that method to `internal` so the call would compile.
That method is dead code: nothing in Java.Interop (or dotnet/android) ever
calls it, and the base `JniTypeManager.RegisterNativeMembers` is a no-op.
`JavaProxyObject`'s native members are -- and were before #1441 --
registered purely via reflection in `ReflectionJniTypeManager`
(`FindAndCallRegisterMethod` discovering the
`[JniAddNativeMethodRegistrationAttribute]`-annotated method), which finds
private methods just fine via `GetRuntimeMethods ()`.
Remove the unused `TryRegisterBuiltInNativeMembers` and restore
`RegisterNativeMembers` to `private` (its state before #1441). Making it
private again also strips the attribute-bearing method from the reference
assembly, which fixes dotnet/android's trimmable typemap scanner falsely
rejecting the built-in `JavaProxyObject` with XA4251 (dotnet/android#11622).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit that referenced this pull request Jun 15, 2026
Replaces the production TryRegisterBuiltInNativeMembers helper with a
samples-only solution, restoring the pre-#1441 behavior for the NativeAOT
type managers.
Background: making JavaProxyObject.RegisterNativeMembers private (the XA4251
fix) is sufficient for the default Android runtime, which discovers and invokes
it via reflection (FindAndCallRegisterMethod -> GetRuntimeMethods(), which
returns private methods). The only thing that needed JavaProxyObject's
registration via a non-reflection path was the two NativeAOT samples, which
#1441 had rewritten to derive from the reflection-free JniRuntime.JniTypeManager
base. That is why #1441 introduced TryRegisterBuiltInNativeMembers.
Instead of carrying that helper in production, have the sample type managers
derive from JniRuntime.ReflectionJniTypeManager again - exactly what the base
JniRuntime.JniTypeManager provided before #1441. Built-in types such as
JavaProxyObject/JavaProxyThrowable are then registered automatically via
reflection, and the samples no longer need any custom registration code.
Changes:
- JavaProxyObject.RegisterNativeMembers: internal -> private. The marshalers
stay (the default reflection runtime registers them by reflecting over this
type); only the visibility changes, which strips the attribute from the
reference assembly and fixes the dotnet/android XA4251 scanner failure.
- Delete JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers (+ its
PublicAPI.Unshipped.txt entry); no longer needed.
- Hello-NativeAOTFromJNI / Hello-NativeAOTFromAndroid: derive from
JniRuntime.ReflectionJniTypeManager and drop the reflection-free overrides and
hand-written JavaProxyObject registration. ReflectionJniTypeManager is
[RequiresDynamicCode]/[RequiresUnreferencedCode], so the constructor suppresses
IL2026/IL3050 with [UnconditionalSuppressMessage]. A #pragma is insufficient
here: it silences the Roslyn analyzer but not the ILLink/ILC publish passes,
which only honor the attribute (verified with a trim-publish of the real type
manager against Java.Interop.dll).
Net: production loses code (1-line visibility change + deletions) and the samples
shrink substantially.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit that referenced this pull request Jun 16, 2026
Build 1465402's "run Hello-NativeAOTFromJNI" step failed at runtime:
System.NotSupportedException: Could not find System.Type corresponding to
Java type JniTypeSignature(TypeName=example/ManagedType ...)
at Java.Interop.ManagedPeer.RegisterNativeMembers(...)
at example.ManagedType.<clinit>(ManagedType.java:15)
Root cause: after #1441, JniRuntime.JniTypeManager.GetType() dispatches through
GetTypeForSimpleReference (singular), not GetTypesForSimpleReference (plural).
The previous sample rewrite only overrode the plural method, so app types like
example/ManagedType were never resolved (the base ReflectionJniTypeManager's
GetTypeForSimpleReference only knows built-in types), and ManagedPeer
registration threw before it could register the type.
Fix: override GetTypeForSimpleReference (singular) in both NativeAOT sample type
managers to resolve the sample's own managed types, falling back to the base for
built-ins. Registration and the reverse Type->JNI mapping continue to be handled
by the reflection base (the pre-#1441 behavior). The override carries the same
[return: DynamicallyAccessedMembers(...)] as the base to satisfy IL2093.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit that referenced this pull request Jun 16, 2026
Add a class-level comment to both NativeAOT sample type managers explaining the
rationale for the [UnconditionalSuppressMessage] IL2026/IL3050 suppressions:
- These are *samples*, not product code. .NET for Android (what we ship) does not
pair ReflectionJniTypeManager with NativeAOT, so it isn't worth the effort to make
these samples fully trim/AOT-clean right now.
- The reflection paths were always trim/AOT-unsafe. Before #1441 the equivalent
suppressions lived inside JniTypeManager itself (justified "NotUsedInAndroid");
#1441 simply moved that responsibility to callers via
[RequiresDynamicCode]/[RequiresUnreferencedCode].
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit to dotnet/android that referenced this pull request Jun 16, 2026
Revert the external/Java.Interop submodule bump. Pinning an unmerged
java-interop PR-branch commit is fragile, and advancing to java-interop
main HEAD would drag in unrelated changes (e.g. dotnet/java-interop#1441)
that need their own coordination. The generator-side removal lives in
dotnet/java-interop#1470 and will reach Mono.Android through the normal
submodule bump once it merges; this PR keeps only the independent runtime
cleanup of the now-dead `packageLookup` plumbing.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers pushed a commit that referenced this pull request Jun 16, 2026
…in (#1468)
#1441 changed `JavaProxyObject.RegisterNativeMembers` from `private` to
`internal` so the new reflection-free
`JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers ()` could call
it directly.
A side effect is that the `[JniAddNativeMethodRegistrationAttribute]`-
annotated method now appears in Java.Interop's *reference* assembly.
dotnet/android's trimmable typemap scanner reads reference assemblies and
rejects any type carrying that attribute (XA4251), so every trimmable
typemap build now fails on the built-in `JavaProxyObject`
(see dotnet/android#11622).
Restore the method to `private` (so it is stripped from the reference
assembly) and expose a small attribute-free `internal` entry point,
`RegisterBuiltInNativeMembers (JniType)`, for the built-in registration
path to call. Runtime behavior is unchanged.
### [Java.Interop] Restore pure-reflection registration for JavaProxyObject
#1441 added `JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers ()`
-- a reflection-free direct call to `JavaProxyObject.RegisterNativeMembers`
-- and bumped that method to `internal` so the call would compile.
That method is dead code: nothing in Java.Interop (or dotnet/android) ever
calls it, and the base `JniTypeManager.RegisterNativeMembers` is a no-op.
`JavaProxyObject`'s native members are -- and were before #1441 --
registered purely via reflection in `ReflectionJniTypeManager`
(`FindAndCallRegisterMethod` discovering the
`[JniAddNativeMethodRegistrationAttribute]`-annotated method), which finds
private methods just fine via `GetRuntimeMethods ()`.
Remove the unused `TryRegisterBuiltInNativeMembers` and restore
`RegisterNativeMembers` to `private` (its state before #1441). Making it
private again also strips the attribute-bearing method from the reference
assembly, which fixes dotnet/android's trimmable typemap scanner falsely
rejecting the built-in `JavaProxyObject` with XA4251 (dotnet/android#11622).
### Address review: restore TryRegisterBuiltInNativeMembers (not dead code)
The previous commit deleted JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers
claiming it was unused. That was wrong: it has two live callers in the NativeAOT
samples (samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs and
samples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs), both JniTypeManager
subclasses. Deleting it broke them (CS0103) and removed the only path that
registers JavaProxyObject's equals/hashCode/toString natives for NativeAOT type
managers (which don't use the reflection-based FindAndCallRegisterMethod path).
The breakage was hidden in CI only because the NativeAOT sample steps use
continueOnError: true.
Keep the XA4251 fix (RegisterNativeMembers stays private, so the
[JniAddNativeMethodRegistrationAttribute] is stripped from the reference assembly)
and restore TryRegisterBuiltInNativeMembers by extracting the registration logic
into a new attribute-free internal helper, JavaProxyObject.AddBuiltInRegistrations.
Both the private attributed RegisterNativeMembers (reflection path) and
TryRegisterBuiltInNativeMembers (NativeAOT path) call it. Restore the
PublicAPI.Unshipped.txt entry as well.
Verified: Java.Interop.dll builds clean; the reference assembly contains no method
carrying [JniAddNativeMethodRegistration]; the sample's call to
TryRegisterBuiltInNativeMembers resolves (no CS0103).
### Keep built-in registration in the samples via ReflectionJniTypeManager
Replaces the production TryRegisterBuiltInNativeMembers helper with a
samples-only solution, restoring the pre-#1441 behavior for the NativeAOT
type managers.
Background: making JavaProxyObject.RegisterNativeMembers private (the XA4251
fix) is sufficient for the default Android runtime, which discovers and invokes
it via reflection (FindAndCallRegisterMethod -> GetRuntimeMethods(), which
returns private methods). The only thing that needed JavaProxyObject's
registration via a non-reflection path was the two NativeAOT samples, which
#1441 had rewritten to derive from the reflection-free JniRuntime.JniTypeManager
base. That is why #1441 introduced TryRegisterBuiltInNativeMembers.
Instead of carrying that helper in production, have the sample type managers
derive from JniRuntime.ReflectionJniTypeManager again - exactly what the base
JniRuntime.JniTypeManager provided before #1441. Built-in types such as
JavaProxyObject/JavaProxyThrowable are then registered automatically via
reflection, and the samples no longer need any custom registration code.
Changes:
- JavaProxyObject.RegisterNativeMembers: internal -> private. The marshalers
stay (the default reflection runtime registers them by reflecting over this
type); only the visibility changes, which strips the attribute from the
reference assembly and fixes the dotnet/android XA4251 scanner failure.
- Delete JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers (+ its
PublicAPI.Unshipped.txt entry); no longer needed.
- Hello-NativeAOTFromJNI / Hello-NativeAOTFromAndroid: derive from
JniRuntime.ReflectionJniTypeManager and drop the reflection-free overrides and
hand-written JavaProxyObject registration. ReflectionJniTypeManager is
[RequiresDynamicCode]/[RequiresUnreferencedCode], so the constructor suppresses
IL2026/IL3050 with [UnconditionalSuppressMessage]. A #pragma is insufficient
here: it silences the Roslyn analyzer but not the ILLink/ILC publish passes,
which only honor the attribute (verified with a trim-publish of the real type
manager against Java.Interop.dll).
Net: production loses code (1-line visibility change + deletions) and the samples
shrink substantially.
### Drop verbose explanatory comments from the NativeAOT sample type managers
The [UnconditionalSuppressMessage] Justification strings already convey the
necessary context.
### Fix NativeAOT sample type resolution: override GetTypeForSimpleReference
Build 1465402's "run Hello-NativeAOTFromJNI" step failed at runtime:
System.NotSupportedException: Could not find System.Type corresponding to
Java type JniTypeSignature(TypeName=example/ManagedType ...)
at Java.Interop.ManagedPeer.RegisterNativeMembers(...)
at example.ManagedType.<clinit>(ManagedType.java:15)
Root cause: after #1441, JniRuntime.JniTypeManager.GetType() dispatches through
GetTypeForSimpleReference (singular), not GetTypesForSimpleReference (plural).
The previous sample rewrite only overrode the plural method, so app types like
example/ManagedType were never resolved (the base ReflectionJniTypeManager's
GetTypeForSimpleReference only knows built-in types), and ManagedPeer
registration threw before it could register the type.
Fix: override GetTypeForSimpleReference (singular) in both NativeAOT sample type
managers to resolve the sample's own managed types, falling back to the base for
built-ins. Registration and the reverse Type->JNI mapping continue to be handled
by the reflection base (the pre-#1441 behavior). The override carries the same
[return: DynamicallyAccessedMembers(...)] as the base to satisfy IL2093.
### Address review: drop unreachable null guard in FromAndroid sample
`typeMappings` is assigned in its field initializer and never set to null, so the
`if (typeMappings == null) yield break;` guard in CreateSimpleReferencesEnumerator
was unreachable dead code. Remove it.
### Document why the NativeAOT sample trim/AOT suppressions are acceptable
Add a class-level comment to both NativeAOT sample type managers explaining the
rationale for the [UnconditionalSuppressMessage] IL2026/IL3050 suppressions:
- These are *samples*, not product code. .NET for Android (what we ship) does not
pair ReflectionJniTypeManager with NativeAOT, so it isn't worth the effort to make
these samples fully trim/AOT-clean right now.
- The reflection paths were always trim/AOT-unsafe. Before #1441 the equivalent
suppressions lived inside JniTypeManager itself (justified "NotUsedInAndroid");
#1441 simply moved that responsibility to callers via
[RequiresDynamicCode]/[RequiresUnreferencedCode].
### Reword NativeAOT sample suppression justifications to describe why it's safe
The previous IL2026/IL3050 justifications ("does not require unreferenced code /
runtime code generation") read as the opposite of reality, since
ReflectionJniTypeManager is exactly [RequiresUnreferencedCode]/[RequiresDynamicCode].
Reword to describe why the suppression is correct for this sample:
- IL2026: the assembly is rooted via TrimmerRootAssembly and the reflected
registration members are preserved by the [DynamicallyAccessedMembers]
annotations on the RegisterNativeMembers(Type) -> FindAndCallRegisterMethod path.
- IL3050: registration uses CreateDelegate on compile-time-known static methods
(no MakeGenericType / expression compilation), so no runtime codegen is required.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers pushed a commit to dotnet/android that referenced this pull request Jun 16, 2026
…mbing (#11667)
The binding generator used to emit a `Java.Interop.__TypeRegistrations`
class whose `RegisterPackages ()` populated `TypeManager.packageLookup`.
The only reader of that dictionary -- the `TypeRegistrationFallback` path
in `GetJavaToManagedTypeCore` -- was removed in #9471, so the dictionary
has been write-only (and, since nothing generated calls it, effectively
unused) ever since. Type resolution now goes through the native /
trimmable type map.
This bumps external/Java.Interop to the generator change that stops
emitting the dead class (dotnet/java-interop#1470) and removes the
now-dead runtime plumbing:
* Delete the `packageLookup` field and `LazyInitPackageLookup ()`.
* `TypeManager.RegisterPackage`/`RegisterPackages` are shipped public
API, so they are kept as validated no-op stubs rather than removed.
Fixes#11663
### [Mono.Android] Throw NotSupportedException with an explanatory message
`TypeManager.RegisterPackage`/`RegisterPackages` are retired shipped public
APIs whose registration mechanism no longer exists. Rather than silently
doing nothing, they now throw `NotSupportedException` carrying an explanation
of why the call is unsupported and what replaced it ("Java package type
registration is no longer supported. Java-to-managed type resolution now goes
through the native and trimmable type maps."), so a caller that still invokes
them gets a clear, actionable reason instead of a bare exception.
### [Mono.Android] Don't bump external/Java.Interop in this PR
Revert the external/Java.Interop submodule bump. Pinning an unmerged
java-interop PR-branch commit is fragile, and advancing to java-interop
main HEAD would drag in unrelated changes (e.g. dotnet/java-interop#1441)
that need their own coordination. The generator-side removal lives in
dotnet/java-interop#1470 and will reach Mono.Android through the normal
submodule bump once it merges; this PR keeps only the independent runtime
cleanup of the now-dead `packageLookup` plumbing.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers added a commit to dotnet/android that referenced this pull request Jun 16, 2026
Bumps [external/Java.Interop](https://github.com/dotnet/java-interop) from `b881d21` to [`6820a9cb`](dotnet/java-interop@6820a9c).
## Java.Interop changes pulled in
- [`6820a9c`](dotnet/java-interop@6820a9c) [Java.Interop] Make JavaProxyObject.RegisterNativeMembers private again ([#1468](dotnet/java-interop#1468))
- [`a2159d7`](dotnet/java-interop@a2159d70) Remove NuGet dependency grouping from Dependabot config ([#1459](dotnet/java-interop#1459))
- [`a0ca676`](dotnet/java-interop@a0ca6760) [ci] Update agentic workflows to claude-opus-4.8 ([#1461](dotnet/java-interop#1461))
- [`6d8aef7`](dotnet/java-interop@6d8aef7b) Bump to dotnet/android-tools@1fb68b6 ([#1457](dotnet/java-interop#1457))
- [`cfca8ad`](dotnet/java-interop@cfca8ad) Upgrade gh-aw to v0.79.6 and recompile workflows ([#1460](dotnet/java-interop#1460))
- [`0104236`](dotnet/java-interop@0104236e) [JniValueManager] Make TryConstructPeer virtual ([#1456](dotnet/java-interop#1456))
- [`d7dbad5`](dotnet/java-interop@d7dbad5) Remove Java.Interop.Dynamic ([#1449](dotnet/java-interop#1449))
- [`0244c10`](dotnet/java-interop@0244c10) Split reflection-based JniTypeManager and JniValueManager behavior ([#1441](dotnet/java-interop#1441))
- [`9ca7064`](dotnet/java-interop@9ca7064) [Java.Interop] Remove legacy `NET` preprocessor paths ([#1451](dotnet/java-interop#1451))
- [`cd2fc12`](dotnet/java-interop@cd2fc12) Remove Java.Interop.GenericMarshaler ([#1450](dotnet/java-interop#1450))
See full diff in [compare view](dotnet/java-interop@b881d21...6820a9c).
## Changes in this PR (beyond the submodule bump)
The Java.Interop bump — particularly [#1441](dotnet/java-interop#1441) (split reflection-based `JniTypeManager` and `JniValueManager`) and [#1449](dotnet/java-interop#1449) (removal of `Java.Interop.Dynamic`) — required a number of follow-on changes in `dotnet/android`:
### Runtime / `Mono.Android`
- **Adapt Android runtime to Java.Interop reflection managers** — switch the `JavaInteropTypeManager` / `JavaInteropValueManager` hierarchy to the new reflection-based base types and update type/value manager construction to match.
- **Change base type of value managers and type managers** so the runtime composes correctly with the new Java.Interop split.
- **Suppress reflection manager trim warnings only** — narrow the existing trim suppressions so they apply to the reflection-only managers and don't hide other warnings.
- **Suppress IL2068 on `ManagedTypeManager.GetTypeForSimpleReference`** — annotate the entry point that legitimately requires reflection so trim analysis is satisfied.
### Tests / apkdescs
- **Regenerate `BuildReleaseArm64SimpleDotNet.MonoVM.apkdesc`** to track size changes from the Java.Interop bump.
- **Regenerate Simple `BuildReleaseArm64` apkdescs** (`MonoVM` + `NativeAOT`) from a `Release` SDK so they match what CI produces.
- **Update NativeAOT warning expectations** — `Mono.Android` now produces fewer trim/AOT warnings, so loosen the strict `4 Warning(s)` checks in `SupportedOSPlatformVersion` and `CheckSignApk` to `AssertHasAtMostWarnings (2)` (new helper in `AssertionExtensions`) and drop the `BuildHasTrimmerWarnings` data counts (`3 → 2`, `4 → 3`) accordingly.
### Merges
- Two merges from `origin/main` to keep the branch current and resolve a conflict in `Microsoft.Android.Sdk.TypeMap.Trimmable.targets`.
Co-authored-by: Simon Rozsival <simon@rozsival.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Jonathan Peppers <jonathan.peppers@microsoft.com>
jonathanpeppers pushed a commit that referenced this pull request Jun 17, 2026
… "trimmable type map" (#1454)
Follow-up to #1441.
Prerequisite for dotnet/android#11617.
This keeps the Java.Interop changes focused on the small base hook dotnet/android needs for the trimmable type-map integration. The reflection value manager continues to use value marshalers internally, while Android's generated/trimmable value manager can provide the only production `JavaObjectArray<T>` element-assignment object-reference path without implementing `GetValueMarshaler*()`.
## Changes
- Add minimal `JniValueManager` object-reference API for `JavaObjectArray<T>.SetElementAt()`:
- `CreateLocalObjectReferenceArgument(Type type, object? value)` returns an owned local `JniObjectReference` for element assignment. Callers must dispose the returned reference.
- Make the matching core method abstract so non-reflection value managers can implement this path directly.
- Keep value marshalers as a `ReflectionJniValueManager` implementation detail: reflection creates marshaler state, copies out an independent local reference, then destroys the state immediately.
- Update `JavaObjectArray<T>` to call the value manager directly instead of calling `GetValueMarshaler<T>()` in production paths.
- Simplify `JavaObjectArray<T>.Clear()` to set array slots to Java null directly; it no longer needs value-manager or value-marshaler state.
- Remove the earlier exposed proxy/peerable marshaler accessors, broad/generic state overloads, default-value state API, destroy-state API, and `ParameterAttributes synchronize` from this value-manager object-reference path.
- Keep ManagedPeer-dependent tests categorized as unsupported for the Android trimmable configuration rather than carrying Android-specific Java fixture workarounds in this PR.
- Include the small type-manager/test cleanups needed by the dotnet/android integration branch.
## Non-goals
- This PR does not make value marshalers public trimmable API.
- This PR does not require trimmable Android value managers to implement or use `GetValueMarshaler*()`.
- This PR does not remove or replace ManagedPeer-dependent Java.Interop test fixtures.
## Validation
- `dotnet build external/Java.Interop/src/Java.Interop/Java.Interop.csproj -p:Configuration=Debug -m:1 -nodeReuse:false --no-restore -v:minimal`
- From the dotnet/android integration branch:
- `dotnet test tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj -v minimal --no-restore` (`562` passed)
- `dotnet build src/Mono.Android/Mono.Android.csproj -p:Configuration=Debug -p:AndroidSdkDirectory=/Users/simonrozsival/android-toolchain/sdk -m:1 -nodeReuse:false --no-restore -v:minimal` compiled `Mono.Android.Runtime.dll`; the remaining local failure is Android SDK provisioning (`extras/android/m2repository.staging` and `docs.staging` missing), not C# or trim-analyzer errors.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers added a commit to dotnet/android that referenced this pull request Jun 30, 2026
The ReflectionJniTypeManager [RequiresDynamicCode] attribute predates this submodule bump (added in dotnet/java-interop#1441), so the ManagedTypeManager..ctor() IL3050 is already part of the BuildHasNoWarnings test's baseline of 4 warnings. Only the RegisterNativeMembers warning is new from this PR; suppressing the ctor warning too would drop the count to 2 and fail the test.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

ready-to-reviewThis PR is ready to review/merge, thanks!

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@simonrozsival@jonathanpeppers
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
This repository was archived by the owner on Aug 27, 2026. It is now read-only.

Split reflection-based JniTypeManager and JniValueManager behavior - #1441

Merged
jonathanpeppers merged 30 commits into
mainfrom
dev/simonrozsival/fix-aot-warnings-jni-type-manager
Jun 9, 2026
Merged

Split reflection-based JniTypeManager and JniValueManager behavior#1441
jonathanpeppers merged 30 commits into
mainfrom
dev/simonrozsival/fix-aot-warnings-jni-type-manager

Conversation

@simonrozsival

@simonrozsivalsimonrozsival commented Jun 4, 2026

Copy link
Copy Markdown
Member

Splits the reflection-backed paths out of JniRuntime.JniTypeManager and JniRuntime.JniValueManager so callers that don't need reflection (e.g. NativeAOT) can opt out, while keeping the public API surface as compatible as possible for callers migrating from .NET 10.

What changed

  • JniRuntime.ReflectionJniTypeManager (new): all reflection-based type lookup, signature discovery, and native-method registration moves here. Annotated with [RequiresDynamicCode] / [RequiresUnreferencedCode].
  • JniRuntime.ReflectionJniValueManager (new): all reflection-based peer construction, value marshaling, and activation moves here.
  • JniRuntime.JniTypeManager stays a non-abstract base class. Methods that previously had reflection defaults are now virtual no-ops (or return null/empty). Subclasses that want the old default behavior should derive from ReflectionJniTypeManager.
  • JniRuntime.JniValueManager stays abstract (it already was). A few previously-virtual methods (CreatePeer, GetValueMarshalerCore(Type)) become abstract since their reflection-based defaults moved out — public consumers (dotnet/android's value managers) already override these.
  • In-tree consumers updated: JreTypeManager, ManagedValueManager, MonoRuntimeValueManager now derive from the new Reflection* bases. The NativeAOT samples keep deriving from the abstract bases with explicit type mappings.

Public API impact

PublicAPI.Shipped.txt is unchanged. All breaking changes are tracked in PublicAPI.Unshipped.txt with *REMOVED* markers:

  • JniTypeManager.TryRegisterNativeMembers (both overloads) — moved to ReflectionJniTypeManager as protected helpers.
  • JniValueManager.CreatePeer and GetValueMarshalerCore(Type)virtualabstract.

A non-abstract JniTypeManager keeps new JniRuntime.JniTypeManager() and direct subclasses compiling without changes.

Validation

  • CI is green.
  • Local dotnet build Java.Interop.sln passes (PublicApiAnalyzer enforces tracking is consistent).

@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/fix-aot-warnings-jni-type-manager branch from 5ff4c17 to 6d72140CompareJune 4, 2026 14:24
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/fix-aot-warnings-jni-type-manager branch from 6d72140 to 58f0dbdCompareJune 4, 2026 14:30
@simonrozsival

Copy link
Copy Markdown
MemberAuthor

/azp run

@azure-pipelines

Copy link
Copy Markdown
No pipelines are associated with this pull request.

@simonrozsival
simonrozsival marked this pull request as ready for review June 8, 2026 11:55
CopilotAI review requested due to automatic review settings June 8, 2026 11:55

CopilotAI 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.

Pull request overview

This draft PR refactors JniRuntime.JniTypeManager into an abstract base and moves the default reflection-based behavior into a new JniRuntime.DynamicJniTypeManager, with corresponding updates across tests, JRE runtime helpers, samples, and public API baselines.

Changes:

  • Introduces JniRuntime.DynamicJniTypeManager and makes JniRuntime.JniTypeManager abstract, shifting default/reflection-backed logic into the new derived type.
  • Updates test and JRE type managers to inherit from DynamicJniTypeManager, and adjusts tests for new behaviors (e.g., generic invoker handling, GetType array behavior).
  • Updates trimmer/AOT annotations and PublicAPI baselines to reflect the split and new abstract members.
Show a summary per file
FileDescription
tests/Java.Interop-Tests/Java.Interop/JniTypeManagerTests.csUpdates expectations around GetType behavior (notably array signatures and generic holder mapping).
tests/Java.Interop-Tests/Java.Interop/JniRuntimeTest.csSwitches proxy manager to DynamicJniTypeManager and adds trim suppression for test-only reflection use.
tests/Java.Interop-Tests/Java.Interop/JniRuntime.JniTypeManagerTests.csAdds a generic invoker negative test and updates test manager base type.
tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.csUpdates fixture type manager to DynamicJniTypeManager and refines type lookup with trimming annotations.
src/Java.Runtime.Environment/Java.Interop/JreTypeManager.csMoves to DynamicJniTypeManager and adds a DAM-annotated GetTypeForSimpleReference override.
src/Java.Interop/PublicAPI.Unshipped.txtRecords newly introduced/reshaped APIs (new type, newly-abstract members, overrides).
src/Java.Interop/PublicAPI.Shipped.txtUpdates shipped API surface to reflect newly-abstract members and removed methods from JniTypeManager.
src/Java.Interop/Java.Interop/JniValueMarshaler.csSimplifies expression-based marshaler instantiation and adjusts trimming annotations.
src/Java.Interop/Java.Interop/JniRuntime.JniValueManager.csSwitches peer-type selection to GetReflectionConstructibleTypes() and refactors uninitialized peer creation.
src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.csMakes JniTypeManager abstract and factors previously-inlined logic into abstract core methods.
src/Java.Interop/Java.Interop/JniRuntime.DynamicJniTypeManager.csAdds the new reflection-backed type manager implementation and related registration behavior.
src/Java.Interop/Java.Interop/JniRuntime.csRemoves the default new JniTypeManager() fallback; requires an explicit type manager in creation options.
src/Java.Interop/Java.Interop/JavaPrimitiveArrays.ttRetargets primitive array helper generation to DynamicJniTypeManager.
src/Java.Interop/Java.Interop/JavaPrimitiveArrays.csUpdates the generated primitive array helper container type to DynamicJniTypeManager.
src/Java.Interop/GlobalSuppressions.csAdds CA1034 suppression for the new nested DynamicJniTypeManager type.
samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.csUpdates sample type manager implementation to new hierarchy and adds explicit native member registration.
samples/Hello-NativeAOTFromJNI/ManagedType.csChanges the native registration hook to internal for sample-driven registration.
samples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.csUpdates sample type manager implementation to new hierarchy and implements abstract members.

Copilot's findings

  • Files reviewed: 18/18 changed files
  • Comments generated: 3

Comment threadsamples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs Outdated
Comment threadsamples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs Outdated
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival
simonrozsival marked this pull request as draft June 8, 2026 14:24
simonrozsivaland others added 9 commits June 8, 2026 16:32
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>
Map Java.Interop's internal proxy peer types in the base type manager so custom AOT-safe managers do not need reflection-based DynamicJniTypeManager behavior for JavaProxyThrowable/JavaProxyObject.
Register built-in proxy native members by JNI type name before asking custom managers for a managed Type, avoiding DAM-annotated lookups of internal Exception/Delegate-derived types.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Let the base JniTypeManager resolve primitive JNI keyword signatures without requiring custom AOT-safe managers to duplicate the dynamic type manager's primitive mappings.
Use that base lookup as a fallback when matching Java-to-managed constructor signatures so signatures such as (I)V resolve to System.Int32.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival
simonrozsival marked this pull request as ready for review June 9, 2026 07:23
simonrozsivaland others added 2 commits June 9, 2026 09:31
Rename DynamicJniTypeManager to ReflectionJniTypeManager so the type name describes the reflection-based behavior that is incompatible with Native AOT.
Update the matching source file name, API baseline, JRE type manager inheritance, generated primitive-array partial, and tests.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsivalsimonrozsival changed the title Split reflection-based JniTypeManager behaviorSplit reflection-based JniTypeManager and JniValueManager behaviorJun 9, 2026

CopilotAI commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved the merge conflicts in commit Resolve merge conflicts: remove #if NET guards, keep abstract methods in base JniTypeManager.

The two new commits in main since the last merge (Remove Java.Interop.GenericMarshaler (#1450) and Remove legacy NET preprocessor paths (#1451)) conflicted with our branch in three files:

  • JniRuntime.JniTypeManager.cs (two conflicts): Kept the abstract method declarations (GetInvokerTypeCore, GetStaticMethodFallbackTypesCore, RegisterNativeMembers) from our branch while adopting the #if NET guard removals from main. The implementation helpers (TryRegisterNativeMembers, TryLoadJniMarshalMethods, etc.) remain in DynamicJniTypeManager where our PR moved them.
  • ManagedPeer.cs: Removed the #if NET guard that was wrapping the ReadOnlySpan<char> methods block.
  • JreRuntime.cs: Kept our branch's structure (TypeManager default set in the [RequiresDynamicCode]-annotated CreateJreVM, not in CreateJreVMCore), discarding main's addition of builder.TypeManager ??= new JreTypeManager(...) to CreateJreVMCore which would have bypassed the AOT warning.

Also removed the remaining #if NET guards from JniRuntime.ReflectionJniTypeManager.cs and JreRuntime.cs that were in our branch but no longer needed after the main merge.

@simonrozsival

Copy link
Copy Markdown
MemberAuthor

/review

@github-actions

github-actionsBot commented Jun 9, 2026

Copy link
Copy Markdown

Java.Interop PR Reviewer completed successfully!

@github-actionsgithub-actionsBot 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.

✅ LGTM — Clean architectural split

Summary: This PR cleanly separates the reflection-based JniTypeManager / JniValueManager behavior into dedicated ReflectionJniTypeManager and ReflectionJniValueManager subclasses, making the base classes abstract and properly annotating the reflection-based paths with [RequiresDynamicCode] / [RequiresUnreferencedCode]. The NativeAOT samples are updated to implement the abstract base directly, and tests/downstream types are updated accordingly.

What looks good:

  • The split is well-structured — base classes contain only the contract and built-in type handling; all reflection goes into the Reflection* subclasses.
  • [RequiresDynamicCode] and [RequiresUnreferencedCode] annotations are applied consistently throughout the chain (samples, tests, JRE runtime types).
  • Public API baselines are updated correctly.
  • TryRegisterBuiltInNativeMembers is a nice extraction that NativeAOT consumers can call without needing the full reflection machinery.
  • The JreRuntimeOptions.CreateJreVM(JniTypeManager) overload is a good ergonomic addition.

Issues by severity:

SeverityCount
⚠️ Warning1
💡 Suggestion3

Notes:

  • CI only shows license/cla — no build/test results visible yet. The PR is not mergeable until CI validates the full build.
  • The large abstract surface area for NativeAOT consumers (~12 methods) may be worth revisiting — see inline suggestion about making unused extension points virtual with default null returns.

Generated by Java.Interop PR Reviewer for issue #1441 · ● 14.3M

Comment threadsamples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs
Comment threadsamples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs
Comment threadsrc/Java.Interop/Java.Interop/JniRuntime.ReflectionJniTypeManager.cs Outdated
simonrozsivaland others added 2 commits June 9, 2026 22:20
Map JNI primitive and boxed primitive signatures in the NativeAOT sample type managers so constructor signature matching can resolve primitive parameters such as I to System.Int32.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
To minimize migration pain for callers moving from .NET 10 to .NET 11,
revert JniTypeManager from `abstract` back to a regular class. Methods
that were turned into `abstract` overrides now have `virtual` defaults
so existing subclasses keep compiling:
* Type lookup methods return null/empty defaults.
* RegisterNativeMembers is a no-op by default; reflection-based
behavior remains in ReflectionJniTypeManager (override to opt-in).
Restore PublicAPI.Shipped.txt to its main-branch state (per the
`Microsoft.CodeAnalysis.PublicApiAnalyzers` convention, never edit
shipped entries in place) and track every breaking change in
PublicAPI.Unshipped.txt:
* 5 new JniTypeManager.* APIs are now `virtual` (not `abstract`).
* `*REMOVED*` entries for the two `JniTypeManager.TryRegisterNativeMembers`
overloads that moved to `ReflectionJniTypeManager`.
* `*REMOVED*virtual` + `abstract` pairs for the two virtual->abstract
transitions on `JniValueManager` (CreatePeer, GetValueMarshalerCore(Type)).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jonathanpeppers
jonathanpeppers merged commit 0244c10 into mainJun 9, 2026
2 checks passed
@jonathanpeppers
jonathanpeppers deleted the dev/simonrozsival/fix-aot-warnings-jni-type-manager branch June 9, 2026 23:17
jonathanpeppers pushed a commit that referenced this pull request Jun 12, 2026
Follow-up to #1441 We need this method virtual so that `JavaMarshalValueManager` can override it and try to invoke the `XAConstructorSignature`. The method was previously virtual but as an oversight I removed it in #1441:
- https://github.com/dotnet/java-interop/pull/1441/changes#diff-8ead12d157bc7712affd3d920bb93cddbff3885147fe6297957a44cc522d5430R7
- https://github.com/dotnet/java-interop/pull/1441/changes#diff-b92884de5db7f82df63b483b8319360cd9bfdac3cd97222eb9aff7292a01a07bL449-L467
I noticed we're not disposing the `reference` correctly which might cause a ref leak. In the Mono `AndroidValueManager` we never called into the base method so this would not be observed on mono previously (https://github.com/dotnet/android/blob/main/src/Mono.Android/Java.Interop/TypeManager.cs#L416-L423).
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
simonrozsival added a commit that referenced this pull request Jun 15, 2026
#1441 added `JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers ()`
-- a reflection-free direct call to `JavaProxyObject.RegisterNativeMembers`
-- and bumped that method to `internal` so the call would compile.
That method is dead code: nothing in Java.Interop (or dotnet/android) ever
calls it, and the base `JniTypeManager.RegisterNativeMembers` is a no-op.
`JavaProxyObject`'s native members are -- and were before #1441 --
registered purely via reflection in `ReflectionJniTypeManager`
(`FindAndCallRegisterMethod` discovering the
`[JniAddNativeMethodRegistrationAttribute]`-annotated method), which finds
private methods just fine via `GetRuntimeMethods ()`.
Remove the unused `TryRegisterBuiltInNativeMembers` and restore
`RegisterNativeMembers` to `private` (its state before #1441). Making it
private again also strips the attribute-bearing method from the reference
assembly, which fixes dotnet/android's trimmable typemap scanner falsely
rejecting the built-in `JavaProxyObject` with XA4251 (dotnet/android#11622).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit that referenced this pull request Jun 15, 2026
Replaces the production TryRegisterBuiltInNativeMembers helper with a
samples-only solution, restoring the pre-#1441 behavior for the NativeAOT
type managers.
Background: making JavaProxyObject.RegisterNativeMembers private (the XA4251
fix) is sufficient for the default Android runtime, which discovers and invokes
it via reflection (FindAndCallRegisterMethod -> GetRuntimeMethods(), which
returns private methods). The only thing that needed JavaProxyObject's
registration via a non-reflection path was the two NativeAOT samples, which
#1441 had rewritten to derive from the reflection-free JniRuntime.JniTypeManager
base. That is why #1441 introduced TryRegisterBuiltInNativeMembers.
Instead of carrying that helper in production, have the sample type managers
derive from JniRuntime.ReflectionJniTypeManager again - exactly what the base
JniRuntime.JniTypeManager provided before #1441. Built-in types such as
JavaProxyObject/JavaProxyThrowable are then registered automatically via
reflection, and the samples no longer need any custom registration code.
Changes:
- JavaProxyObject.RegisterNativeMembers: internal -> private. The marshalers
stay (the default reflection runtime registers them by reflecting over this
type); only the visibility changes, which strips the attribute from the
reference assembly and fixes the dotnet/android XA4251 scanner failure.
- Delete JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers (+ its
PublicAPI.Unshipped.txt entry); no longer needed.
- Hello-NativeAOTFromJNI / Hello-NativeAOTFromAndroid: derive from
JniRuntime.ReflectionJniTypeManager and drop the reflection-free overrides and
hand-written JavaProxyObject registration. ReflectionJniTypeManager is
[RequiresDynamicCode]/[RequiresUnreferencedCode], so the constructor suppresses
IL2026/IL3050 with [UnconditionalSuppressMessage]. A #pragma is insufficient
here: it silences the Roslyn analyzer but not the ILLink/ILC publish passes,
which only honor the attribute (verified with a trim-publish of the real type
manager against Java.Interop.dll).
Net: production loses code (1-line visibility change + deletions) and the samples
shrink substantially.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit that referenced this pull request Jun 16, 2026
Build 1465402's "run Hello-NativeAOTFromJNI" step failed at runtime:
System.NotSupportedException: Could not find System.Type corresponding to
Java type JniTypeSignature(TypeName=example/ManagedType ...)
at Java.Interop.ManagedPeer.RegisterNativeMembers(...)
at example.ManagedType.<clinit>(ManagedType.java:15)
Root cause: after #1441, JniRuntime.JniTypeManager.GetType() dispatches through
GetTypeForSimpleReference (singular), not GetTypesForSimpleReference (plural).
The previous sample rewrite only overrode the plural method, so app types like
example/ManagedType were never resolved (the base ReflectionJniTypeManager's
GetTypeForSimpleReference only knows built-in types), and ManagedPeer
registration threw before it could register the type.
Fix: override GetTypeForSimpleReference (singular) in both NativeAOT sample type
managers to resolve the sample's own managed types, falling back to the base for
built-ins. Registration and the reverse Type->JNI mapping continue to be handled
by the reflection base (the pre-#1441 behavior). The override carries the same
[return: DynamicallyAccessedMembers(...)] as the base to satisfy IL2093.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit that referenced this pull request Jun 16, 2026
Add a class-level comment to both NativeAOT sample type managers explaining the
rationale for the [UnconditionalSuppressMessage] IL2026/IL3050 suppressions:
- These are *samples*, not product code. .NET for Android (what we ship) does not
pair ReflectionJniTypeManager with NativeAOT, so it isn't worth the effort to make
these samples fully trim/AOT-clean right now.
- The reflection paths were always trim/AOT-unsafe. Before #1441 the equivalent
suppressions lived inside JniTypeManager itself (justified "NotUsedInAndroid");
#1441 simply moved that responsibility to callers via
[RequiresDynamicCode]/[RequiresUnreferencedCode].
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit to dotnet/android that referenced this pull request Jun 16, 2026
Revert the external/Java.Interop submodule bump. Pinning an unmerged
java-interop PR-branch commit is fragile, and advancing to java-interop
main HEAD would drag in unrelated changes (e.g. dotnet/java-interop#1441)
that need their own coordination. The generator-side removal lives in
dotnet/java-interop#1470 and will reach Mono.Android through the normal
submodule bump once it merges; this PR keeps only the independent runtime
cleanup of the now-dead `packageLookup` plumbing.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers pushed a commit that referenced this pull request Jun 16, 2026
…in (#1468)
#1441 changed `JavaProxyObject.RegisterNativeMembers` from `private` to
`internal` so the new reflection-free
`JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers ()` could call
it directly.
A side effect is that the `[JniAddNativeMethodRegistrationAttribute]`-
annotated method now appears in Java.Interop's *reference* assembly.
dotnet/android's trimmable typemap scanner reads reference assemblies and
rejects any type carrying that attribute (XA4251), so every trimmable
typemap build now fails on the built-in `JavaProxyObject`
(see dotnet/android#11622).
Restore the method to `private` (so it is stripped from the reference
assembly) and expose a small attribute-free `internal` entry point,
`RegisterBuiltInNativeMembers (JniType)`, for the built-in registration
path to call. Runtime behavior is unchanged.
### [Java.Interop] Restore pure-reflection registration for JavaProxyObject
#1441 added `JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers ()`
-- a reflection-free direct call to `JavaProxyObject.RegisterNativeMembers`
-- and bumped that method to `internal` so the call would compile.
That method is dead code: nothing in Java.Interop (or dotnet/android) ever
calls it, and the base `JniTypeManager.RegisterNativeMembers` is a no-op.
`JavaProxyObject`'s native members are -- and were before #1441 --
registered purely via reflection in `ReflectionJniTypeManager`
(`FindAndCallRegisterMethod` discovering the
`[JniAddNativeMethodRegistrationAttribute]`-annotated method), which finds
private methods just fine via `GetRuntimeMethods ()`.
Remove the unused `TryRegisterBuiltInNativeMembers` and restore
`RegisterNativeMembers` to `private` (its state before #1441). Making it
private again also strips the attribute-bearing method from the reference
assembly, which fixes dotnet/android's trimmable typemap scanner falsely
rejecting the built-in `JavaProxyObject` with XA4251 (dotnet/android#11622).
### Address review: restore TryRegisterBuiltInNativeMembers (not dead code)
The previous commit deleted JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers
claiming it was unused. That was wrong: it has two live callers in the NativeAOT
samples (samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs and
samples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs), both JniTypeManager
subclasses. Deleting it broke them (CS0103) and removed the only path that
registers JavaProxyObject's equals/hashCode/toString natives for NativeAOT type
managers (which don't use the reflection-based FindAndCallRegisterMethod path).
The breakage was hidden in CI only because the NativeAOT sample steps use
continueOnError: true.
Keep the XA4251 fix (RegisterNativeMembers stays private, so the
[JniAddNativeMethodRegistrationAttribute] is stripped from the reference assembly)
and restore TryRegisterBuiltInNativeMembers by extracting the registration logic
into a new attribute-free internal helper, JavaProxyObject.AddBuiltInRegistrations.
Both the private attributed RegisterNativeMembers (reflection path) and
TryRegisterBuiltInNativeMembers (NativeAOT path) call it. Restore the
PublicAPI.Unshipped.txt entry as well.
Verified: Java.Interop.dll builds clean; the reference assembly contains no method
carrying [JniAddNativeMethodRegistration]; the sample's call to
TryRegisterBuiltInNativeMembers resolves (no CS0103).
### Keep built-in registration in the samples via ReflectionJniTypeManager
Replaces the production TryRegisterBuiltInNativeMembers helper with a
samples-only solution, restoring the pre-#1441 behavior for the NativeAOT
type managers.
Background: making JavaProxyObject.RegisterNativeMembers private (the XA4251
fix) is sufficient for the default Android runtime, which discovers and invokes
it via reflection (FindAndCallRegisterMethod -> GetRuntimeMethods(), which
returns private methods). The only thing that needed JavaProxyObject's
registration via a non-reflection path was the two NativeAOT samples, which
#1441 had rewritten to derive from the reflection-free JniRuntime.JniTypeManager
base. That is why #1441 introduced TryRegisterBuiltInNativeMembers.
Instead of carrying that helper in production, have the sample type managers
derive from JniRuntime.ReflectionJniTypeManager again - exactly what the base
JniRuntime.JniTypeManager provided before #1441. Built-in types such as
JavaProxyObject/JavaProxyThrowable are then registered automatically via
reflection, and the samples no longer need any custom registration code.
Changes:
- JavaProxyObject.RegisterNativeMembers: internal -> private. The marshalers
stay (the default reflection runtime registers them by reflecting over this
type); only the visibility changes, which strips the attribute from the
reference assembly and fixes the dotnet/android XA4251 scanner failure.
- Delete JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers (+ its
PublicAPI.Unshipped.txt entry); no longer needed.
- Hello-NativeAOTFromJNI / Hello-NativeAOTFromAndroid: derive from
JniRuntime.ReflectionJniTypeManager and drop the reflection-free overrides and
hand-written JavaProxyObject registration. ReflectionJniTypeManager is
[RequiresDynamicCode]/[RequiresUnreferencedCode], so the constructor suppresses
IL2026/IL3050 with [UnconditionalSuppressMessage]. A #pragma is insufficient
here: it silences the Roslyn analyzer but not the ILLink/ILC publish passes,
which only honor the attribute (verified with a trim-publish of the real type
manager against Java.Interop.dll).
Net: production loses code (1-line visibility change + deletions) and the samples
shrink substantially.
### Drop verbose explanatory comments from the NativeAOT sample type managers
The [UnconditionalSuppressMessage] Justification strings already convey the
necessary context.
### Fix NativeAOT sample type resolution: override GetTypeForSimpleReference
Build 1465402's "run Hello-NativeAOTFromJNI" step failed at runtime:
System.NotSupportedException: Could not find System.Type corresponding to
Java type JniTypeSignature(TypeName=example/ManagedType ...)
at Java.Interop.ManagedPeer.RegisterNativeMembers(...)
at example.ManagedType.<clinit>(ManagedType.java:15)
Root cause: after #1441, JniRuntime.JniTypeManager.GetType() dispatches through
GetTypeForSimpleReference (singular), not GetTypesForSimpleReference (plural).
The previous sample rewrite only overrode the plural method, so app types like
example/ManagedType were never resolved (the base ReflectionJniTypeManager's
GetTypeForSimpleReference only knows built-in types), and ManagedPeer
registration threw before it could register the type.
Fix: override GetTypeForSimpleReference (singular) in both NativeAOT sample type
managers to resolve the sample's own managed types, falling back to the base for
built-ins. Registration and the reverse Type->JNI mapping continue to be handled
by the reflection base (the pre-#1441 behavior). The override carries the same
[return: DynamicallyAccessedMembers(...)] as the base to satisfy IL2093.
### Address review: drop unreachable null guard in FromAndroid sample
`typeMappings` is assigned in its field initializer and never set to null, so the
`if (typeMappings == null) yield break;` guard in CreateSimpleReferencesEnumerator
was unreachable dead code. Remove it.
### Document why the NativeAOT sample trim/AOT suppressions are acceptable
Add a class-level comment to both NativeAOT sample type managers explaining the
rationale for the [UnconditionalSuppressMessage] IL2026/IL3050 suppressions:
- These are *samples*, not product code. .NET for Android (what we ship) does not
pair ReflectionJniTypeManager with NativeAOT, so it isn't worth the effort to make
these samples fully trim/AOT-clean right now.
- The reflection paths were always trim/AOT-unsafe. Before #1441 the equivalent
suppressions lived inside JniTypeManager itself (justified "NotUsedInAndroid");
#1441 simply moved that responsibility to callers via
[RequiresDynamicCode]/[RequiresUnreferencedCode].
### Reword NativeAOT sample suppression justifications to describe why it's safe
The previous IL2026/IL3050 justifications ("does not require unreferenced code /
runtime code generation") read as the opposite of reality, since
ReflectionJniTypeManager is exactly [RequiresUnreferencedCode]/[RequiresDynamicCode].
Reword to describe why the suppression is correct for this sample:
- IL2026: the assembly is rooted via TrimmerRootAssembly and the reflected
registration members are preserved by the [DynamicallyAccessedMembers]
annotations on the RegisterNativeMembers(Type) -> FindAndCallRegisterMethod path.
- IL3050: registration uses CreateDelegate on compile-time-known static methods
(no MakeGenericType / expression compilation), so no runtime codegen is required.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers pushed a commit to dotnet/android that referenced this pull request Jun 16, 2026
…mbing (#11667)
The binding generator used to emit a `Java.Interop.__TypeRegistrations`
class whose `RegisterPackages ()` populated `TypeManager.packageLookup`.
The only reader of that dictionary -- the `TypeRegistrationFallback` path
in `GetJavaToManagedTypeCore` -- was removed in #9471, so the dictionary
has been write-only (and, since nothing generated calls it, effectively
unused) ever since. Type resolution now goes through the native /
trimmable type map.
This bumps external/Java.Interop to the generator change that stops
emitting the dead class (dotnet/java-interop#1470) and removes the
now-dead runtime plumbing:
* Delete the `packageLookup` field and `LazyInitPackageLookup ()`.
* `TypeManager.RegisterPackage`/`RegisterPackages` are shipped public
API, so they are kept as validated no-op stubs rather than removed.
Fixes#11663
### [Mono.Android] Throw NotSupportedException with an explanatory message
`TypeManager.RegisterPackage`/`RegisterPackages` are retired shipped public
APIs whose registration mechanism no longer exists. Rather than silently
doing nothing, they now throw `NotSupportedException` carrying an explanation
of why the call is unsupported and what replaced it ("Java package type
registration is no longer supported. Java-to-managed type resolution now goes
through the native and trimmable type maps."), so a caller that still invokes
them gets a clear, actionable reason instead of a bare exception.
### [Mono.Android] Don't bump external/Java.Interop in this PR
Revert the external/Java.Interop submodule bump. Pinning an unmerged
java-interop PR-branch commit is fragile, and advancing to java-interop
main HEAD would drag in unrelated changes (e.g. dotnet/java-interop#1441)
that need their own coordination. The generator-side removal lives in
dotnet/java-interop#1470 and will reach Mono.Android through the normal
submodule bump once it merges; this PR keeps only the independent runtime
cleanup of the now-dead `packageLookup` plumbing.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers added a commit to dotnet/android that referenced this pull request Jun 16, 2026
Bumps [external/Java.Interop](https://github.com/dotnet/java-interop) from `b881d21` to [`6820a9cb`](dotnet/java-interop@6820a9c).
## Java.Interop changes pulled in
- [`6820a9c`](dotnet/java-interop@6820a9c) [Java.Interop] Make JavaProxyObject.RegisterNativeMembers private again ([#1468](dotnet/java-interop#1468))
- [`a2159d7`](dotnet/java-interop@a2159d70) Remove NuGet dependency grouping from Dependabot config ([#1459](dotnet/java-interop#1459))
- [`a0ca676`](dotnet/java-interop@a0ca6760) [ci] Update agentic workflows to claude-opus-4.8 ([#1461](dotnet/java-interop#1461))
- [`6d8aef7`](dotnet/java-interop@6d8aef7b) Bump to dotnet/android-tools@1fb68b6 ([#1457](dotnet/java-interop#1457))
- [`cfca8ad`](dotnet/java-interop@cfca8ad) Upgrade gh-aw to v0.79.6 and recompile workflows ([#1460](dotnet/java-interop#1460))
- [`0104236`](dotnet/java-interop@0104236e) [JniValueManager] Make TryConstructPeer virtual ([#1456](dotnet/java-interop#1456))
- [`d7dbad5`](dotnet/java-interop@d7dbad5) Remove Java.Interop.Dynamic ([#1449](dotnet/java-interop#1449))
- [`0244c10`](dotnet/java-interop@0244c10) Split reflection-based JniTypeManager and JniValueManager behavior ([#1441](dotnet/java-interop#1441))
- [`9ca7064`](dotnet/java-interop@9ca7064) [Java.Interop] Remove legacy `NET` preprocessor paths ([#1451](dotnet/java-interop#1451))
- [`cd2fc12`](dotnet/java-interop@cd2fc12) Remove Java.Interop.GenericMarshaler ([#1450](dotnet/java-interop#1450))
See full diff in [compare view](dotnet/java-interop@b881d21...6820a9c).
## Changes in this PR (beyond the submodule bump)
The Java.Interop bump — particularly [#1441](dotnet/java-interop#1441) (split reflection-based `JniTypeManager` and `JniValueManager`) and [#1449](dotnet/java-interop#1449) (removal of `Java.Interop.Dynamic`) — required a number of follow-on changes in `dotnet/android`:
### Runtime / `Mono.Android`
- **Adapt Android runtime to Java.Interop reflection managers** — switch the `JavaInteropTypeManager` / `JavaInteropValueManager` hierarchy to the new reflection-based base types and update type/value manager construction to match.
- **Change base type of value managers and type managers** so the runtime composes correctly with the new Java.Interop split.
- **Suppress reflection manager trim warnings only** — narrow the existing trim suppressions so they apply to the reflection-only managers and don't hide other warnings.
- **Suppress IL2068 on `ManagedTypeManager.GetTypeForSimpleReference`** — annotate the entry point that legitimately requires reflection so trim analysis is satisfied.
### Tests / apkdescs
- **Regenerate `BuildReleaseArm64SimpleDotNet.MonoVM.apkdesc`** to track size changes from the Java.Interop bump.
- **Regenerate Simple `BuildReleaseArm64` apkdescs** (`MonoVM` + `NativeAOT`) from a `Release` SDK so they match what CI produces.
- **Update NativeAOT warning expectations** — `Mono.Android` now produces fewer trim/AOT warnings, so loosen the strict `4 Warning(s)` checks in `SupportedOSPlatformVersion` and `CheckSignApk` to `AssertHasAtMostWarnings (2)` (new helper in `AssertionExtensions`) and drop the `BuildHasTrimmerWarnings` data counts (`3 → 2`, `4 → 3`) accordingly.
### Merges
- Two merges from `origin/main` to keep the branch current and resolve a conflict in `Microsoft.Android.Sdk.TypeMap.Trimmable.targets`.
Co-authored-by: Simon Rozsival <simon@rozsival.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Jonathan Peppers <jonathan.peppers@microsoft.com>
jonathanpeppers pushed a commit that referenced this pull request Jun 17, 2026
… "trimmable type map" (#1454)
Follow-up to #1441.
Prerequisite for dotnet/android#11617.
This keeps the Java.Interop changes focused on the small base hook dotnet/android needs for the trimmable type-map integration. The reflection value manager continues to use value marshalers internally, while Android's generated/trimmable value manager can provide the only production `JavaObjectArray<T>` element-assignment object-reference path without implementing `GetValueMarshaler*()`.
## Changes
- Add minimal `JniValueManager` object-reference API for `JavaObjectArray<T>.SetElementAt()`:
- `CreateLocalObjectReferenceArgument(Type type, object? value)` returns an owned local `JniObjectReference` for element assignment. Callers must dispose the returned reference.
- Make the matching core method abstract so non-reflection value managers can implement this path directly.
- Keep value marshalers as a `ReflectionJniValueManager` implementation detail: reflection creates marshaler state, copies out an independent local reference, then destroys the state immediately.
- Update `JavaObjectArray<T>` to call the value manager directly instead of calling `GetValueMarshaler<T>()` in production paths.
- Simplify `JavaObjectArray<T>.Clear()` to set array slots to Java null directly; it no longer needs value-manager or value-marshaler state.
- Remove the earlier exposed proxy/peerable marshaler accessors, broad/generic state overloads, default-value state API, destroy-state API, and `ParameterAttributes synchronize` from this value-manager object-reference path.
- Keep ManagedPeer-dependent tests categorized as unsupported for the Android trimmable configuration rather than carrying Android-specific Java fixture workarounds in this PR.
- Include the small type-manager/test cleanups needed by the dotnet/android integration branch.
## Non-goals
- This PR does not make value marshalers public trimmable API.
- This PR does not require trimmable Android value managers to implement or use `GetValueMarshaler*()`.
- This PR does not remove or replace ManagedPeer-dependent Java.Interop test fixtures.
## Validation
- `dotnet build external/Java.Interop/src/Java.Interop/Java.Interop.csproj -p:Configuration=Debug -m:1 -nodeReuse:false --no-restore -v:minimal`
- From the dotnet/android integration branch:
- `dotnet test tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj -v minimal --no-restore` (`562` passed)
- `dotnet build src/Mono.Android/Mono.Android.csproj -p:Configuration=Debug -p:AndroidSdkDirectory=/Users/simonrozsival/android-toolchain/sdk -m:1 -nodeReuse:false --no-restore -v:minimal` compiled `Mono.Android.Runtime.dll`; the remaining local failure is Android SDK provisioning (`extras/android/m2repository.staging` and `docs.staging` missing), not C# or trim-analyzer errors.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers added a commit to dotnet/android that referenced this pull request Jun 30, 2026
The ReflectionJniTypeManager [RequiresDynamicCode] attribute predates this submodule bump (added in dotnet/java-interop#1441), so the ManagedTypeManager..ctor() IL3050 is already part of the BuildHasNoWarnings test's baseline of 4 warnings. Only the RegisterNativeMembers warning is new from this PR; suppressing the ctor warning too would drop the count to 2 and fail the test.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

ready-to-reviewThis PR is ready to review/merge, thanks!

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@simonrozsival@jonathanpeppers
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
This repository was archived by the owner on Aug 27, 2026. It is now read-only.

Split reflection-based JniTypeManager and JniValueManager behavior - #1441

Merged
jonathanpeppers merged 30 commits into
mainfrom
dev/simonrozsival/fix-aot-warnings-jni-type-manager
Jun 9, 2026
Merged

Split reflection-based JniTypeManager and JniValueManager behavior#1441
jonathanpeppers merged 30 commits into
mainfrom
dev/simonrozsival/fix-aot-warnings-jni-type-manager

Conversation

@simonrozsival

@simonrozsivalsimonrozsival commented Jun 4, 2026

Copy link
Copy Markdown
Member

Splits the reflection-backed paths out of JniRuntime.JniTypeManager and JniRuntime.JniValueManager so callers that don't need reflection (e.g. NativeAOT) can opt out, while keeping the public API surface as compatible as possible for callers migrating from .NET 10.

What changed

  • JniRuntime.ReflectionJniTypeManager (new): all reflection-based type lookup, signature discovery, and native-method registration moves here. Annotated with [RequiresDynamicCode] / [RequiresUnreferencedCode].
  • JniRuntime.ReflectionJniValueManager (new): all reflection-based peer construction, value marshaling, and activation moves here.
  • JniRuntime.JniTypeManager stays a non-abstract base class. Methods that previously had reflection defaults are now virtual no-ops (or return null/empty). Subclasses that want the old default behavior should derive from ReflectionJniTypeManager.
  • JniRuntime.JniValueManager stays abstract (it already was). A few previously-virtual methods (CreatePeer, GetValueMarshalerCore(Type)) become abstract since their reflection-based defaults moved out — public consumers (dotnet/android's value managers) already override these.
  • In-tree consumers updated: JreTypeManager, ManagedValueManager, MonoRuntimeValueManager now derive from the new Reflection* bases. The NativeAOT samples keep deriving from the abstract bases with explicit type mappings.

Public API impact

PublicAPI.Shipped.txt is unchanged. All breaking changes are tracked in PublicAPI.Unshipped.txt with *REMOVED* markers:

  • JniTypeManager.TryRegisterNativeMembers (both overloads) — moved to ReflectionJniTypeManager as protected helpers.
  • JniValueManager.CreatePeer and GetValueMarshalerCore(Type)virtualabstract.

A non-abstract JniTypeManager keeps new JniRuntime.JniTypeManager() and direct subclasses compiling without changes.

Validation

  • CI is green.
  • Local dotnet build Java.Interop.sln passes (PublicApiAnalyzer enforces tracking is consistent).

@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/fix-aot-warnings-jni-type-manager branch from 5ff4c17 to 6d72140CompareJune 4, 2026 14:24
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/fix-aot-warnings-jni-type-manager branch from 6d72140 to 58f0dbdCompareJune 4, 2026 14:30
@simonrozsival

Copy link
Copy Markdown
MemberAuthor

/azp run

@azure-pipelines

Copy link
Copy Markdown
No pipelines are associated with this pull request.

@simonrozsival
simonrozsival marked this pull request as ready for review June 8, 2026 11:55
CopilotAI review requested due to automatic review settings June 8, 2026 11:55

CopilotAI 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.

Pull request overview

This draft PR refactors JniRuntime.JniTypeManager into an abstract base and moves the default reflection-based behavior into a new JniRuntime.DynamicJniTypeManager, with corresponding updates across tests, JRE runtime helpers, samples, and public API baselines.

Changes:

  • Introduces JniRuntime.DynamicJniTypeManager and makes JniRuntime.JniTypeManager abstract, shifting default/reflection-backed logic into the new derived type.
  • Updates test and JRE type managers to inherit from DynamicJniTypeManager, and adjusts tests for new behaviors (e.g., generic invoker handling, GetType array behavior).
  • Updates trimmer/AOT annotations and PublicAPI baselines to reflect the split and new abstract members.
Show a summary per file
FileDescription
tests/Java.Interop-Tests/Java.Interop/JniTypeManagerTests.csUpdates expectations around GetType behavior (notably array signatures and generic holder mapping).
tests/Java.Interop-Tests/Java.Interop/JniRuntimeTest.csSwitches proxy manager to DynamicJniTypeManager and adds trim suppression for test-only reflection use.
tests/Java.Interop-Tests/Java.Interop/JniRuntime.JniTypeManagerTests.csAdds a generic invoker negative test and updates test manager base type.
tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.csUpdates fixture type manager to DynamicJniTypeManager and refines type lookup with trimming annotations.
src/Java.Runtime.Environment/Java.Interop/JreTypeManager.csMoves to DynamicJniTypeManager and adds a DAM-annotated GetTypeForSimpleReference override.
src/Java.Interop/PublicAPI.Unshipped.txtRecords newly introduced/reshaped APIs (new type, newly-abstract members, overrides).
src/Java.Interop/PublicAPI.Shipped.txtUpdates shipped API surface to reflect newly-abstract members and removed methods from JniTypeManager.
src/Java.Interop/Java.Interop/JniValueMarshaler.csSimplifies expression-based marshaler instantiation and adjusts trimming annotations.
src/Java.Interop/Java.Interop/JniRuntime.JniValueManager.csSwitches peer-type selection to GetReflectionConstructibleTypes() and refactors uninitialized peer creation.
src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.csMakes JniTypeManager abstract and factors previously-inlined logic into abstract core methods.
src/Java.Interop/Java.Interop/JniRuntime.DynamicJniTypeManager.csAdds the new reflection-backed type manager implementation and related registration behavior.
src/Java.Interop/Java.Interop/JniRuntime.csRemoves the default new JniTypeManager() fallback; requires an explicit type manager in creation options.
src/Java.Interop/Java.Interop/JavaPrimitiveArrays.ttRetargets primitive array helper generation to DynamicJniTypeManager.
src/Java.Interop/Java.Interop/JavaPrimitiveArrays.csUpdates the generated primitive array helper container type to DynamicJniTypeManager.
src/Java.Interop/GlobalSuppressions.csAdds CA1034 suppression for the new nested DynamicJniTypeManager type.
samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.csUpdates sample type manager implementation to new hierarchy and adds explicit native member registration.
samples/Hello-NativeAOTFromJNI/ManagedType.csChanges the native registration hook to internal for sample-driven registration.
samples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.csUpdates sample type manager implementation to new hierarchy and implements abstract members.

Copilot's findings

  • Files reviewed: 18/18 changed files
  • Comments generated: 3

Comment threadsamples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs Outdated
Comment threadsamples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs Outdated
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival
simonrozsival marked this pull request as draft June 8, 2026 14:24
simonrozsivaland others added 9 commits June 8, 2026 16:32
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>
Map Java.Interop's internal proxy peer types in the base type manager so custom AOT-safe managers do not need reflection-based DynamicJniTypeManager behavior for JavaProxyThrowable/JavaProxyObject.
Register built-in proxy native members by JNI type name before asking custom managers for a managed Type, avoiding DAM-annotated lookups of internal Exception/Delegate-derived types.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Let the base JniTypeManager resolve primitive JNI keyword signatures without requiring custom AOT-safe managers to duplicate the dynamic type manager's primitive mappings.
Use that base lookup as a fallback when matching Java-to-managed constructor signatures so signatures such as (I)V resolve to System.Int32.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival
simonrozsival marked this pull request as ready for review June 9, 2026 07:23
simonrozsivaland others added 2 commits June 9, 2026 09:31
Rename DynamicJniTypeManager to ReflectionJniTypeManager so the type name describes the reflection-based behavior that is incompatible with Native AOT.
Update the matching source file name, API baseline, JRE type manager inheritance, generated primitive-array partial, and tests.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsivalsimonrozsival changed the title Split reflection-based JniTypeManager behaviorSplit reflection-based JniTypeManager and JniValueManager behaviorJun 9, 2026

CopilotAI commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved the merge conflicts in commit Resolve merge conflicts: remove #if NET guards, keep abstract methods in base JniTypeManager.

The two new commits in main since the last merge (Remove Java.Interop.GenericMarshaler (#1450) and Remove legacy NET preprocessor paths (#1451)) conflicted with our branch in three files:

  • JniRuntime.JniTypeManager.cs (two conflicts): Kept the abstract method declarations (GetInvokerTypeCore, GetStaticMethodFallbackTypesCore, RegisterNativeMembers) from our branch while adopting the #if NET guard removals from main. The implementation helpers (TryRegisterNativeMembers, TryLoadJniMarshalMethods, etc.) remain in DynamicJniTypeManager where our PR moved them.
  • ManagedPeer.cs: Removed the #if NET guard that was wrapping the ReadOnlySpan<char> methods block.
  • JreRuntime.cs: Kept our branch's structure (TypeManager default set in the [RequiresDynamicCode]-annotated CreateJreVM, not in CreateJreVMCore), discarding main's addition of builder.TypeManager ??= new JreTypeManager(...) to CreateJreVMCore which would have bypassed the AOT warning.

Also removed the remaining #if NET guards from JniRuntime.ReflectionJniTypeManager.cs and JreRuntime.cs that were in our branch but no longer needed after the main merge.

@simonrozsival

Copy link
Copy Markdown
MemberAuthor

/review

@github-actions

github-actionsBot commented Jun 9, 2026

Copy link
Copy Markdown

Java.Interop PR Reviewer completed successfully!

@github-actionsgithub-actionsBot 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.

✅ LGTM — Clean architectural split

Summary: This PR cleanly separates the reflection-based JniTypeManager / JniValueManager behavior into dedicated ReflectionJniTypeManager and ReflectionJniValueManager subclasses, making the base classes abstract and properly annotating the reflection-based paths with [RequiresDynamicCode] / [RequiresUnreferencedCode]. The NativeAOT samples are updated to implement the abstract base directly, and tests/downstream types are updated accordingly.

What looks good:

  • The split is well-structured — base classes contain only the contract and built-in type handling; all reflection goes into the Reflection* subclasses.
  • [RequiresDynamicCode] and [RequiresUnreferencedCode] annotations are applied consistently throughout the chain (samples, tests, JRE runtime types).
  • Public API baselines are updated correctly.
  • TryRegisterBuiltInNativeMembers is a nice extraction that NativeAOT consumers can call without needing the full reflection machinery.
  • The JreRuntimeOptions.CreateJreVM(JniTypeManager) overload is a good ergonomic addition.

Issues by severity:

SeverityCount
⚠️ Warning1
💡 Suggestion3

Notes:

  • CI only shows license/cla — no build/test results visible yet. The PR is not mergeable until CI validates the full build.
  • The large abstract surface area for NativeAOT consumers (~12 methods) may be worth revisiting — see inline suggestion about making unused extension points virtual with default null returns.

Generated by Java.Interop PR Reviewer for issue #1441 · ● 14.3M

Comment threadsamples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs
Comment threadsamples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs
Comment threadsrc/Java.Interop/Java.Interop/JniRuntime.ReflectionJniTypeManager.cs Outdated
simonrozsivaland others added 2 commits June 9, 2026 22:20
Map JNI primitive and boxed primitive signatures in the NativeAOT sample type managers so constructor signature matching can resolve primitive parameters such as I to System.Int32.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
To minimize migration pain for callers moving from .NET 10 to .NET 11,
revert JniTypeManager from `abstract` back to a regular class. Methods
that were turned into `abstract` overrides now have `virtual` defaults
so existing subclasses keep compiling:
* Type lookup methods return null/empty defaults.
* RegisterNativeMembers is a no-op by default; reflection-based
behavior remains in ReflectionJniTypeManager (override to opt-in).
Restore PublicAPI.Shipped.txt to its main-branch state (per the
`Microsoft.CodeAnalysis.PublicApiAnalyzers` convention, never edit
shipped entries in place) and track every breaking change in
PublicAPI.Unshipped.txt:
* 5 new JniTypeManager.* APIs are now `virtual` (not `abstract`).
* `*REMOVED*` entries for the two `JniTypeManager.TryRegisterNativeMembers`
overloads that moved to `ReflectionJniTypeManager`.
* `*REMOVED*virtual` + `abstract` pairs for the two virtual->abstract
transitions on `JniValueManager` (CreatePeer, GetValueMarshalerCore(Type)).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jonathanpeppers
jonathanpeppers merged commit 0244c10 into mainJun 9, 2026
2 checks passed
@jonathanpeppers
jonathanpeppers deleted the dev/simonrozsival/fix-aot-warnings-jni-type-manager branch June 9, 2026 23:17
jonathanpeppers pushed a commit that referenced this pull request Jun 12, 2026
Follow-up to #1441 We need this method virtual so that `JavaMarshalValueManager` can override it and try to invoke the `XAConstructorSignature`. The method was previously virtual but as an oversight I removed it in #1441:
- https://github.com/dotnet/java-interop/pull/1441/changes#diff-8ead12d157bc7712affd3d920bb93cddbff3885147fe6297957a44cc522d5430R7
- https://github.com/dotnet/java-interop/pull/1441/changes#diff-b92884de5db7f82df63b483b8319360cd9bfdac3cd97222eb9aff7292a01a07bL449-L467
I noticed we're not disposing the `reference` correctly which might cause a ref leak. In the Mono `AndroidValueManager` we never called into the base method so this would not be observed on mono previously (https://github.com/dotnet/android/blob/main/src/Mono.Android/Java.Interop/TypeManager.cs#L416-L423).
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
simonrozsival added a commit that referenced this pull request Jun 15, 2026
#1441 added `JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers ()`
-- a reflection-free direct call to `JavaProxyObject.RegisterNativeMembers`
-- and bumped that method to `internal` so the call would compile.
That method is dead code: nothing in Java.Interop (or dotnet/android) ever
calls it, and the base `JniTypeManager.RegisterNativeMembers` is a no-op.
`JavaProxyObject`'s native members are -- and were before #1441 --
registered purely via reflection in `ReflectionJniTypeManager`
(`FindAndCallRegisterMethod` discovering the
`[JniAddNativeMethodRegistrationAttribute]`-annotated method), which finds
private methods just fine via `GetRuntimeMethods ()`.
Remove the unused `TryRegisterBuiltInNativeMembers` and restore
`RegisterNativeMembers` to `private` (its state before #1441). Making it
private again also strips the attribute-bearing method from the reference
assembly, which fixes dotnet/android's trimmable typemap scanner falsely
rejecting the built-in `JavaProxyObject` with XA4251 (dotnet/android#11622).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit that referenced this pull request Jun 15, 2026
Replaces the production TryRegisterBuiltInNativeMembers helper with a
samples-only solution, restoring the pre-#1441 behavior for the NativeAOT
type managers.
Background: making JavaProxyObject.RegisterNativeMembers private (the XA4251
fix) is sufficient for the default Android runtime, which discovers and invokes
it via reflection (FindAndCallRegisterMethod -> GetRuntimeMethods(), which
returns private methods). The only thing that needed JavaProxyObject's
registration via a non-reflection path was the two NativeAOT samples, which
#1441 had rewritten to derive from the reflection-free JniRuntime.JniTypeManager
base. That is why #1441 introduced TryRegisterBuiltInNativeMembers.
Instead of carrying that helper in production, have the sample type managers
derive from JniRuntime.ReflectionJniTypeManager again - exactly what the base
JniRuntime.JniTypeManager provided before #1441. Built-in types such as
JavaProxyObject/JavaProxyThrowable are then registered automatically via
reflection, and the samples no longer need any custom registration code.
Changes:
- JavaProxyObject.RegisterNativeMembers: internal -> private. The marshalers
stay (the default reflection runtime registers them by reflecting over this
type); only the visibility changes, which strips the attribute from the
reference assembly and fixes the dotnet/android XA4251 scanner failure.
- Delete JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers (+ its
PublicAPI.Unshipped.txt entry); no longer needed.
- Hello-NativeAOTFromJNI / Hello-NativeAOTFromAndroid: derive from
JniRuntime.ReflectionJniTypeManager and drop the reflection-free overrides and
hand-written JavaProxyObject registration. ReflectionJniTypeManager is
[RequiresDynamicCode]/[RequiresUnreferencedCode], so the constructor suppresses
IL2026/IL3050 with [UnconditionalSuppressMessage]. A #pragma is insufficient
here: it silences the Roslyn analyzer but not the ILLink/ILC publish passes,
which only honor the attribute (verified with a trim-publish of the real type
manager against Java.Interop.dll).
Net: production loses code (1-line visibility change + deletions) and the samples
shrink substantially.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit that referenced this pull request Jun 16, 2026
Build 1465402's "run Hello-NativeAOTFromJNI" step failed at runtime:
System.NotSupportedException: Could not find System.Type corresponding to
Java type JniTypeSignature(TypeName=example/ManagedType ...)
at Java.Interop.ManagedPeer.RegisterNativeMembers(...)
at example.ManagedType.<clinit>(ManagedType.java:15)
Root cause: after #1441, JniRuntime.JniTypeManager.GetType() dispatches through
GetTypeForSimpleReference (singular), not GetTypesForSimpleReference (plural).
The previous sample rewrite only overrode the plural method, so app types like
example/ManagedType were never resolved (the base ReflectionJniTypeManager's
GetTypeForSimpleReference only knows built-in types), and ManagedPeer
registration threw before it could register the type.
Fix: override GetTypeForSimpleReference (singular) in both NativeAOT sample type
managers to resolve the sample's own managed types, falling back to the base for
built-ins. Registration and the reverse Type->JNI mapping continue to be handled
by the reflection base (the pre-#1441 behavior). The override carries the same
[return: DynamicallyAccessedMembers(...)] as the base to satisfy IL2093.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit that referenced this pull request Jun 16, 2026
Add a class-level comment to both NativeAOT sample type managers explaining the
rationale for the [UnconditionalSuppressMessage] IL2026/IL3050 suppressions:
- These are *samples*, not product code. .NET for Android (what we ship) does not
pair ReflectionJniTypeManager with NativeAOT, so it isn't worth the effort to make
these samples fully trim/AOT-clean right now.
- The reflection paths were always trim/AOT-unsafe. Before #1441 the equivalent
suppressions lived inside JniTypeManager itself (justified "NotUsedInAndroid");
#1441 simply moved that responsibility to callers via
[RequiresDynamicCode]/[RequiresUnreferencedCode].
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit to dotnet/android that referenced this pull request Jun 16, 2026
Revert the external/Java.Interop submodule bump. Pinning an unmerged
java-interop PR-branch commit is fragile, and advancing to java-interop
main HEAD would drag in unrelated changes (e.g. dotnet/java-interop#1441)
that need their own coordination. The generator-side removal lives in
dotnet/java-interop#1470 and will reach Mono.Android through the normal
submodule bump once it merges; this PR keeps only the independent runtime
cleanup of the now-dead `packageLookup` plumbing.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers pushed a commit that referenced this pull request Jun 16, 2026
…in (#1468)
#1441 changed `JavaProxyObject.RegisterNativeMembers` from `private` to
`internal` so the new reflection-free
`JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers ()` could call
it directly.
A side effect is that the `[JniAddNativeMethodRegistrationAttribute]`-
annotated method now appears in Java.Interop's *reference* assembly.
dotnet/android's trimmable typemap scanner reads reference assemblies and
rejects any type carrying that attribute (XA4251), so every trimmable
typemap build now fails on the built-in `JavaProxyObject`
(see dotnet/android#11622).
Restore the method to `private` (so it is stripped from the reference
assembly) and expose a small attribute-free `internal` entry point,
`RegisterBuiltInNativeMembers (JniType)`, for the built-in registration
path to call. Runtime behavior is unchanged.
### [Java.Interop] Restore pure-reflection registration for JavaProxyObject
#1441 added `JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers ()`
-- a reflection-free direct call to `JavaProxyObject.RegisterNativeMembers`
-- and bumped that method to `internal` so the call would compile.
That method is dead code: nothing in Java.Interop (or dotnet/android) ever
calls it, and the base `JniTypeManager.RegisterNativeMembers` is a no-op.
`JavaProxyObject`'s native members are -- and were before #1441 --
registered purely via reflection in `ReflectionJniTypeManager`
(`FindAndCallRegisterMethod` discovering the
`[JniAddNativeMethodRegistrationAttribute]`-annotated method), which finds
private methods just fine via `GetRuntimeMethods ()`.
Remove the unused `TryRegisterBuiltInNativeMembers` and restore
`RegisterNativeMembers` to `private` (its state before #1441). Making it
private again also strips the attribute-bearing method from the reference
assembly, which fixes dotnet/android's trimmable typemap scanner falsely
rejecting the built-in `JavaProxyObject` with XA4251 (dotnet/android#11622).
### Address review: restore TryRegisterBuiltInNativeMembers (not dead code)
The previous commit deleted JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers
claiming it was unused. That was wrong: it has two live callers in the NativeAOT
samples (samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs and
samples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs), both JniTypeManager
subclasses. Deleting it broke them (CS0103) and removed the only path that
registers JavaProxyObject's equals/hashCode/toString natives for NativeAOT type
managers (which don't use the reflection-based FindAndCallRegisterMethod path).
The breakage was hidden in CI only because the NativeAOT sample steps use
continueOnError: true.
Keep the XA4251 fix (RegisterNativeMembers stays private, so the
[JniAddNativeMethodRegistrationAttribute] is stripped from the reference assembly)
and restore TryRegisterBuiltInNativeMembers by extracting the registration logic
into a new attribute-free internal helper, JavaProxyObject.AddBuiltInRegistrations.
Both the private attributed RegisterNativeMembers (reflection path) and
TryRegisterBuiltInNativeMembers (NativeAOT path) call it. Restore the
PublicAPI.Unshipped.txt entry as well.
Verified: Java.Interop.dll builds clean; the reference assembly contains no method
carrying [JniAddNativeMethodRegistration]; the sample's call to
TryRegisterBuiltInNativeMembers resolves (no CS0103).
### Keep built-in registration in the samples via ReflectionJniTypeManager
Replaces the production TryRegisterBuiltInNativeMembers helper with a
samples-only solution, restoring the pre-#1441 behavior for the NativeAOT
type managers.
Background: making JavaProxyObject.RegisterNativeMembers private (the XA4251
fix) is sufficient for the default Android runtime, which discovers and invokes
it via reflection (FindAndCallRegisterMethod -> GetRuntimeMethods(), which
returns private methods). The only thing that needed JavaProxyObject's
registration via a non-reflection path was the two NativeAOT samples, which
#1441 had rewritten to derive from the reflection-free JniRuntime.JniTypeManager
base. That is why #1441 introduced TryRegisterBuiltInNativeMembers.
Instead of carrying that helper in production, have the sample type managers
derive from JniRuntime.ReflectionJniTypeManager again - exactly what the base
JniRuntime.JniTypeManager provided before #1441. Built-in types such as
JavaProxyObject/JavaProxyThrowable are then registered automatically via
reflection, and the samples no longer need any custom registration code.
Changes:
- JavaProxyObject.RegisterNativeMembers: internal -> private. The marshalers
stay (the default reflection runtime registers them by reflecting over this
type); only the visibility changes, which strips the attribute from the
reference assembly and fixes the dotnet/android XA4251 scanner failure.
- Delete JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers (+ its
PublicAPI.Unshipped.txt entry); no longer needed.
- Hello-NativeAOTFromJNI / Hello-NativeAOTFromAndroid: derive from
JniRuntime.ReflectionJniTypeManager and drop the reflection-free overrides and
hand-written JavaProxyObject registration. ReflectionJniTypeManager is
[RequiresDynamicCode]/[RequiresUnreferencedCode], so the constructor suppresses
IL2026/IL3050 with [UnconditionalSuppressMessage]. A #pragma is insufficient
here: it silences the Roslyn analyzer but not the ILLink/ILC publish passes,
which only honor the attribute (verified with a trim-publish of the real type
manager against Java.Interop.dll).
Net: production loses code (1-line visibility change + deletions) and the samples
shrink substantially.
### Drop verbose explanatory comments from the NativeAOT sample type managers
The [UnconditionalSuppressMessage] Justification strings already convey the
necessary context.
### Fix NativeAOT sample type resolution: override GetTypeForSimpleReference
Build 1465402's "run Hello-NativeAOTFromJNI" step failed at runtime:
System.NotSupportedException: Could not find System.Type corresponding to
Java type JniTypeSignature(TypeName=example/ManagedType ...)
at Java.Interop.ManagedPeer.RegisterNativeMembers(...)
at example.ManagedType.<clinit>(ManagedType.java:15)
Root cause: after #1441, JniRuntime.JniTypeManager.GetType() dispatches through
GetTypeForSimpleReference (singular), not GetTypesForSimpleReference (plural).
The previous sample rewrite only overrode the plural method, so app types like
example/ManagedType were never resolved (the base ReflectionJniTypeManager's
GetTypeForSimpleReference only knows built-in types), and ManagedPeer
registration threw before it could register the type.
Fix: override GetTypeForSimpleReference (singular) in both NativeAOT sample type
managers to resolve the sample's own managed types, falling back to the base for
built-ins. Registration and the reverse Type->JNI mapping continue to be handled
by the reflection base (the pre-#1441 behavior). The override carries the same
[return: DynamicallyAccessedMembers(...)] as the base to satisfy IL2093.
### Address review: drop unreachable null guard in FromAndroid sample
`typeMappings` is assigned in its field initializer and never set to null, so the
`if (typeMappings == null) yield break;` guard in CreateSimpleReferencesEnumerator
was unreachable dead code. Remove it.
### Document why the NativeAOT sample trim/AOT suppressions are acceptable
Add a class-level comment to both NativeAOT sample type managers explaining the
rationale for the [UnconditionalSuppressMessage] IL2026/IL3050 suppressions:
- These are *samples*, not product code. .NET for Android (what we ship) does not
pair ReflectionJniTypeManager with NativeAOT, so it isn't worth the effort to make
these samples fully trim/AOT-clean right now.
- The reflection paths were always trim/AOT-unsafe. Before #1441 the equivalent
suppressions lived inside JniTypeManager itself (justified "NotUsedInAndroid");
#1441 simply moved that responsibility to callers via
[RequiresDynamicCode]/[RequiresUnreferencedCode].
### Reword NativeAOT sample suppression justifications to describe why it's safe
The previous IL2026/IL3050 justifications ("does not require unreferenced code /
runtime code generation") read as the opposite of reality, since
ReflectionJniTypeManager is exactly [RequiresUnreferencedCode]/[RequiresDynamicCode].
Reword to describe why the suppression is correct for this sample:
- IL2026: the assembly is rooted via TrimmerRootAssembly and the reflected
registration members are preserved by the [DynamicallyAccessedMembers]
annotations on the RegisterNativeMembers(Type) -> FindAndCallRegisterMethod path.
- IL3050: registration uses CreateDelegate on compile-time-known static methods
(no MakeGenericType / expression compilation), so no runtime codegen is required.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers pushed a commit to dotnet/android that referenced this pull request Jun 16, 2026
…mbing (#11667)
The binding generator used to emit a `Java.Interop.__TypeRegistrations`
class whose `RegisterPackages ()` populated `TypeManager.packageLookup`.
The only reader of that dictionary -- the `TypeRegistrationFallback` path
in `GetJavaToManagedTypeCore` -- was removed in #9471, so the dictionary
has been write-only (and, since nothing generated calls it, effectively
unused) ever since. Type resolution now goes through the native /
trimmable type map.
This bumps external/Java.Interop to the generator change that stops
emitting the dead class (dotnet/java-interop#1470) and removes the
now-dead runtime plumbing:
* Delete the `packageLookup` field and `LazyInitPackageLookup ()`.
* `TypeManager.RegisterPackage`/`RegisterPackages` are shipped public
API, so they are kept as validated no-op stubs rather than removed.
Fixes#11663
### [Mono.Android] Throw NotSupportedException with an explanatory message
`TypeManager.RegisterPackage`/`RegisterPackages` are retired shipped public
APIs whose registration mechanism no longer exists. Rather than silently
doing nothing, they now throw `NotSupportedException` carrying an explanation
of why the call is unsupported and what replaced it ("Java package type
registration is no longer supported. Java-to-managed type resolution now goes
through the native and trimmable type maps."), so a caller that still invokes
them gets a clear, actionable reason instead of a bare exception.
### [Mono.Android] Don't bump external/Java.Interop in this PR
Revert the external/Java.Interop submodule bump. Pinning an unmerged
java-interop PR-branch commit is fragile, and advancing to java-interop
main HEAD would drag in unrelated changes (e.g. dotnet/java-interop#1441)
that need their own coordination. The generator-side removal lives in
dotnet/java-interop#1470 and will reach Mono.Android through the normal
submodule bump once it merges; this PR keeps only the independent runtime
cleanup of the now-dead `packageLookup` plumbing.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers added a commit to dotnet/android that referenced this pull request Jun 16, 2026
Bumps [external/Java.Interop](https://github.com/dotnet/java-interop) from `b881d21` to [`6820a9cb`](dotnet/java-interop@6820a9c).
## Java.Interop changes pulled in
- [`6820a9c`](dotnet/java-interop@6820a9c) [Java.Interop] Make JavaProxyObject.RegisterNativeMembers private again ([#1468](dotnet/java-interop#1468))
- [`a2159d7`](dotnet/java-interop@a2159d70) Remove NuGet dependency grouping from Dependabot config ([#1459](dotnet/java-interop#1459))
- [`a0ca676`](dotnet/java-interop@a0ca6760) [ci] Update agentic workflows to claude-opus-4.8 ([#1461](dotnet/java-interop#1461))
- [`6d8aef7`](dotnet/java-interop@6d8aef7b) Bump to dotnet/android-tools@1fb68b6 ([#1457](dotnet/java-interop#1457))
- [`cfca8ad`](dotnet/java-interop@cfca8ad) Upgrade gh-aw to v0.79.6 and recompile workflows ([#1460](dotnet/java-interop#1460))
- [`0104236`](dotnet/java-interop@0104236e) [JniValueManager] Make TryConstructPeer virtual ([#1456](dotnet/java-interop#1456))
- [`d7dbad5`](dotnet/java-interop@d7dbad5) Remove Java.Interop.Dynamic ([#1449](dotnet/java-interop#1449))
- [`0244c10`](dotnet/java-interop@0244c10) Split reflection-based JniTypeManager and JniValueManager behavior ([#1441](dotnet/java-interop#1441))
- [`9ca7064`](dotnet/java-interop@9ca7064) [Java.Interop] Remove legacy `NET` preprocessor paths ([#1451](dotnet/java-interop#1451))
- [`cd2fc12`](dotnet/java-interop@cd2fc12) Remove Java.Interop.GenericMarshaler ([#1450](dotnet/java-interop#1450))
See full diff in [compare view](dotnet/java-interop@b881d21...6820a9c).
## Changes in this PR (beyond the submodule bump)
The Java.Interop bump — particularly [#1441](dotnet/java-interop#1441) (split reflection-based `JniTypeManager` and `JniValueManager`) and [#1449](dotnet/java-interop#1449) (removal of `Java.Interop.Dynamic`) — required a number of follow-on changes in `dotnet/android`:
### Runtime / `Mono.Android`
- **Adapt Android runtime to Java.Interop reflection managers** — switch the `JavaInteropTypeManager` / `JavaInteropValueManager` hierarchy to the new reflection-based base types and update type/value manager construction to match.
- **Change base type of value managers and type managers** so the runtime composes correctly with the new Java.Interop split.
- **Suppress reflection manager trim warnings only** — narrow the existing trim suppressions so they apply to the reflection-only managers and don't hide other warnings.
- **Suppress IL2068 on `ManagedTypeManager.GetTypeForSimpleReference`** — annotate the entry point that legitimately requires reflection so trim analysis is satisfied.
### Tests / apkdescs
- **Regenerate `BuildReleaseArm64SimpleDotNet.MonoVM.apkdesc`** to track size changes from the Java.Interop bump.
- **Regenerate Simple `BuildReleaseArm64` apkdescs** (`MonoVM` + `NativeAOT`) from a `Release` SDK so they match what CI produces.
- **Update NativeAOT warning expectations** — `Mono.Android` now produces fewer trim/AOT warnings, so loosen the strict `4 Warning(s)` checks in `SupportedOSPlatformVersion` and `CheckSignApk` to `AssertHasAtMostWarnings (2)` (new helper in `AssertionExtensions`) and drop the `BuildHasTrimmerWarnings` data counts (`3 → 2`, `4 → 3`) accordingly.
### Merges
- Two merges from `origin/main` to keep the branch current and resolve a conflict in `Microsoft.Android.Sdk.TypeMap.Trimmable.targets`.
Co-authored-by: Simon Rozsival <simon@rozsival.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Jonathan Peppers <jonathan.peppers@microsoft.com>
jonathanpeppers pushed a commit that referenced this pull request Jun 17, 2026
… "trimmable type map" (#1454)
Follow-up to #1441.
Prerequisite for dotnet/android#11617.
This keeps the Java.Interop changes focused on the small base hook dotnet/android needs for the trimmable type-map integration. The reflection value manager continues to use value marshalers internally, while Android's generated/trimmable value manager can provide the only production `JavaObjectArray<T>` element-assignment object-reference path without implementing `GetValueMarshaler*()`.
## Changes
- Add minimal `JniValueManager` object-reference API for `JavaObjectArray<T>.SetElementAt()`:
- `CreateLocalObjectReferenceArgument(Type type, object? value)` returns an owned local `JniObjectReference` for element assignment. Callers must dispose the returned reference.
- Make the matching core method abstract so non-reflection value managers can implement this path directly.
- Keep value marshalers as a `ReflectionJniValueManager` implementation detail: reflection creates marshaler state, copies out an independent local reference, then destroys the state immediately.
- Update `JavaObjectArray<T>` to call the value manager directly instead of calling `GetValueMarshaler<T>()` in production paths.
- Simplify `JavaObjectArray<T>.Clear()` to set array slots to Java null directly; it no longer needs value-manager or value-marshaler state.
- Remove the earlier exposed proxy/peerable marshaler accessors, broad/generic state overloads, default-value state API, destroy-state API, and `ParameterAttributes synchronize` from this value-manager object-reference path.
- Keep ManagedPeer-dependent tests categorized as unsupported for the Android trimmable configuration rather than carrying Android-specific Java fixture workarounds in this PR.
- Include the small type-manager/test cleanups needed by the dotnet/android integration branch.
## Non-goals
- This PR does not make value marshalers public trimmable API.
- This PR does not require trimmable Android value managers to implement or use `GetValueMarshaler*()`.
- This PR does not remove or replace ManagedPeer-dependent Java.Interop test fixtures.
## Validation
- `dotnet build external/Java.Interop/src/Java.Interop/Java.Interop.csproj -p:Configuration=Debug -m:1 -nodeReuse:false --no-restore -v:minimal`
- From the dotnet/android integration branch:
- `dotnet test tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj -v minimal --no-restore` (`562` passed)
- `dotnet build src/Mono.Android/Mono.Android.csproj -p:Configuration=Debug -p:AndroidSdkDirectory=/Users/simonrozsival/android-toolchain/sdk -m:1 -nodeReuse:false --no-restore -v:minimal` compiled `Mono.Android.Runtime.dll`; the remaining local failure is Android SDK provisioning (`extras/android/m2repository.staging` and `docs.staging` missing), not C# or trim-analyzer errors.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers added a commit to dotnet/android that referenced this pull request Jun 30, 2026
The ReflectionJniTypeManager [RequiresDynamicCode] attribute predates this submodule bump (added in dotnet/java-interop#1441), so the ManagedTypeManager..ctor() IL3050 is already part of the BuildHasNoWarnings test's baseline of 4 warnings. Only the RegisterNativeMembers warning is new from this PR; suppressing the ctor warning too would drop the count to 2 and fail the test.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

ready-to-reviewThis PR is ready to review/merge, thanks!

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@simonrozsival@jonathanpeppers
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
This repository was archived by the owner on Aug 27, 2026. It is now read-only.

Split reflection-based JniTypeManager and JniValueManager behavior - #1441

Merged
jonathanpeppers merged 30 commits into
mainfrom
dev/simonrozsival/fix-aot-warnings-jni-type-manager
Jun 9, 2026
Merged

Split reflection-based JniTypeManager and JniValueManager behavior#1441
jonathanpeppers merged 30 commits into
mainfrom
dev/simonrozsival/fix-aot-warnings-jni-type-manager

Conversation

@simonrozsival

@simonrozsivalsimonrozsival commented Jun 4, 2026

Copy link
Copy Markdown
Member

Splits the reflection-backed paths out of JniRuntime.JniTypeManager and JniRuntime.JniValueManager so callers that don't need reflection (e.g. NativeAOT) can opt out, while keeping the public API surface as compatible as possible for callers migrating from .NET 10.

What changed

  • JniRuntime.ReflectionJniTypeManager (new): all reflection-based type lookup, signature discovery, and native-method registration moves here. Annotated with [RequiresDynamicCode] / [RequiresUnreferencedCode].
  • JniRuntime.ReflectionJniValueManager (new): all reflection-based peer construction, value marshaling, and activation moves here.
  • JniRuntime.JniTypeManager stays a non-abstract base class. Methods that previously had reflection defaults are now virtual no-ops (or return null/empty). Subclasses that want the old default behavior should derive from ReflectionJniTypeManager.
  • JniRuntime.JniValueManager stays abstract (it already was). A few previously-virtual methods (CreatePeer, GetValueMarshalerCore(Type)) become abstract since their reflection-based defaults moved out — public consumers (dotnet/android's value managers) already override these.
  • In-tree consumers updated: JreTypeManager, ManagedValueManager, MonoRuntimeValueManager now derive from the new Reflection* bases. The NativeAOT samples keep deriving from the abstract bases with explicit type mappings.

Public API impact

PublicAPI.Shipped.txt is unchanged. All breaking changes are tracked in PublicAPI.Unshipped.txt with *REMOVED* markers:

  • JniTypeManager.TryRegisterNativeMembers (both overloads) — moved to ReflectionJniTypeManager as protected helpers.
  • JniValueManager.CreatePeer and GetValueMarshalerCore(Type)virtualabstract.

A non-abstract JniTypeManager keeps new JniRuntime.JniTypeManager() and direct subclasses compiling without changes.

Validation

  • CI is green.
  • Local dotnet build Java.Interop.sln passes (PublicApiAnalyzer enforces tracking is consistent).

@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/fix-aot-warnings-jni-type-manager branch from 5ff4c17 to 6d72140CompareJune 4, 2026 14:24
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/fix-aot-warnings-jni-type-manager branch from 6d72140 to 58f0dbdCompareJune 4, 2026 14:30
@simonrozsival

Copy link
Copy Markdown
MemberAuthor

/azp run

@azure-pipelines

Copy link
Copy Markdown
No pipelines are associated with this pull request.

@simonrozsival
simonrozsival marked this pull request as ready for review June 8, 2026 11:55
CopilotAI review requested due to automatic review settings June 8, 2026 11:55

CopilotAI 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.

Pull request overview

This draft PR refactors JniRuntime.JniTypeManager into an abstract base and moves the default reflection-based behavior into a new JniRuntime.DynamicJniTypeManager, with corresponding updates across tests, JRE runtime helpers, samples, and public API baselines.

Changes:

  • Introduces JniRuntime.DynamicJniTypeManager and makes JniRuntime.JniTypeManager abstract, shifting default/reflection-backed logic into the new derived type.
  • Updates test and JRE type managers to inherit from DynamicJniTypeManager, and adjusts tests for new behaviors (e.g., generic invoker handling, GetType array behavior).
  • Updates trimmer/AOT annotations and PublicAPI baselines to reflect the split and new abstract members.
Show a summary per file
FileDescription
tests/Java.Interop-Tests/Java.Interop/JniTypeManagerTests.csUpdates expectations around GetType behavior (notably array signatures and generic holder mapping).
tests/Java.Interop-Tests/Java.Interop/JniRuntimeTest.csSwitches proxy manager to DynamicJniTypeManager and adds trim suppression for test-only reflection use.
tests/Java.Interop-Tests/Java.Interop/JniRuntime.JniTypeManagerTests.csAdds a generic invoker negative test and updates test manager base type.
tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.csUpdates fixture type manager to DynamicJniTypeManager and refines type lookup with trimming annotations.
src/Java.Runtime.Environment/Java.Interop/JreTypeManager.csMoves to DynamicJniTypeManager and adds a DAM-annotated GetTypeForSimpleReference override.
src/Java.Interop/PublicAPI.Unshipped.txtRecords newly introduced/reshaped APIs (new type, newly-abstract members, overrides).
src/Java.Interop/PublicAPI.Shipped.txtUpdates shipped API surface to reflect newly-abstract members and removed methods from JniTypeManager.
src/Java.Interop/Java.Interop/JniValueMarshaler.csSimplifies expression-based marshaler instantiation and adjusts trimming annotations.
src/Java.Interop/Java.Interop/JniRuntime.JniValueManager.csSwitches peer-type selection to GetReflectionConstructibleTypes() and refactors uninitialized peer creation.
src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.csMakes JniTypeManager abstract and factors previously-inlined logic into abstract core methods.
src/Java.Interop/Java.Interop/JniRuntime.DynamicJniTypeManager.csAdds the new reflection-backed type manager implementation and related registration behavior.
src/Java.Interop/Java.Interop/JniRuntime.csRemoves the default new JniTypeManager() fallback; requires an explicit type manager in creation options.
src/Java.Interop/Java.Interop/JavaPrimitiveArrays.ttRetargets primitive array helper generation to DynamicJniTypeManager.
src/Java.Interop/Java.Interop/JavaPrimitiveArrays.csUpdates the generated primitive array helper container type to DynamicJniTypeManager.
src/Java.Interop/GlobalSuppressions.csAdds CA1034 suppression for the new nested DynamicJniTypeManager type.
samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.csUpdates sample type manager implementation to new hierarchy and adds explicit native member registration.
samples/Hello-NativeAOTFromJNI/ManagedType.csChanges the native registration hook to internal for sample-driven registration.
samples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.csUpdates sample type manager implementation to new hierarchy and implements abstract members.

Copilot's findings

  • Files reviewed: 18/18 changed files
  • Comments generated: 3

Comment threadsamples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs Outdated
Comment threadsamples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs Outdated
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival
simonrozsival marked this pull request as draft June 8, 2026 14:24
simonrozsivaland others added 9 commits June 8, 2026 16:32
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>
Map Java.Interop's internal proxy peer types in the base type manager so custom AOT-safe managers do not need reflection-based DynamicJniTypeManager behavior for JavaProxyThrowable/JavaProxyObject.
Register built-in proxy native members by JNI type name before asking custom managers for a managed Type, avoiding DAM-annotated lookups of internal Exception/Delegate-derived types.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Let the base JniTypeManager resolve primitive JNI keyword signatures without requiring custom AOT-safe managers to duplicate the dynamic type manager's primitive mappings.
Use that base lookup as a fallback when matching Java-to-managed constructor signatures so signatures such as (I)V resolve to System.Int32.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival
simonrozsival marked this pull request as ready for review June 9, 2026 07:23
simonrozsivaland others added 2 commits June 9, 2026 09:31
Rename DynamicJniTypeManager to ReflectionJniTypeManager so the type name describes the reflection-based behavior that is incompatible with Native AOT.
Update the matching source file name, API baseline, JRE type manager inheritance, generated primitive-array partial, and tests.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsivalsimonrozsival changed the title Split reflection-based JniTypeManager behaviorSplit reflection-based JniTypeManager and JniValueManager behaviorJun 9, 2026

CopilotAI commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved the merge conflicts in commit Resolve merge conflicts: remove #if NET guards, keep abstract methods in base JniTypeManager.

The two new commits in main since the last merge (Remove Java.Interop.GenericMarshaler (#1450) and Remove legacy NET preprocessor paths (#1451)) conflicted with our branch in three files:

  • JniRuntime.JniTypeManager.cs (two conflicts): Kept the abstract method declarations (GetInvokerTypeCore, GetStaticMethodFallbackTypesCore, RegisterNativeMembers) from our branch while adopting the #if NET guard removals from main. The implementation helpers (TryRegisterNativeMembers, TryLoadJniMarshalMethods, etc.) remain in DynamicJniTypeManager where our PR moved them.
  • ManagedPeer.cs: Removed the #if NET guard that was wrapping the ReadOnlySpan<char> methods block.
  • JreRuntime.cs: Kept our branch's structure (TypeManager default set in the [RequiresDynamicCode]-annotated CreateJreVM, not in CreateJreVMCore), discarding main's addition of builder.TypeManager ??= new JreTypeManager(...) to CreateJreVMCore which would have bypassed the AOT warning.

Also removed the remaining #if NET guards from JniRuntime.ReflectionJniTypeManager.cs and JreRuntime.cs that were in our branch but no longer needed after the main merge.

@simonrozsival

Copy link
Copy Markdown
MemberAuthor

/review

@github-actions

github-actionsBot commented Jun 9, 2026

Copy link
Copy Markdown

Java.Interop PR Reviewer completed successfully!

@github-actionsgithub-actionsBot 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.

✅ LGTM — Clean architectural split

Summary: This PR cleanly separates the reflection-based JniTypeManager / JniValueManager behavior into dedicated ReflectionJniTypeManager and ReflectionJniValueManager subclasses, making the base classes abstract and properly annotating the reflection-based paths with [RequiresDynamicCode] / [RequiresUnreferencedCode]. The NativeAOT samples are updated to implement the abstract base directly, and tests/downstream types are updated accordingly.

What looks good:

  • The split is well-structured — base classes contain only the contract and built-in type handling; all reflection goes into the Reflection* subclasses.
  • [RequiresDynamicCode] and [RequiresUnreferencedCode] annotations are applied consistently throughout the chain (samples, tests, JRE runtime types).
  • Public API baselines are updated correctly.
  • TryRegisterBuiltInNativeMembers is a nice extraction that NativeAOT consumers can call without needing the full reflection machinery.
  • The JreRuntimeOptions.CreateJreVM(JniTypeManager) overload is a good ergonomic addition.

Issues by severity:

SeverityCount
⚠️ Warning1
💡 Suggestion3

Notes:

  • CI only shows license/cla — no build/test results visible yet. The PR is not mergeable until CI validates the full build.
  • The large abstract surface area for NativeAOT consumers (~12 methods) may be worth revisiting — see inline suggestion about making unused extension points virtual with default null returns.

Generated by Java.Interop PR Reviewer for issue #1441 · ● 14.3M

Comment threadsamples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs
Comment threadsamples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs
Comment threadsrc/Java.Interop/Java.Interop/JniRuntime.ReflectionJniTypeManager.cs Outdated
simonrozsivaland others added 2 commits June 9, 2026 22:20
Map JNI primitive and boxed primitive signatures in the NativeAOT sample type managers so constructor signature matching can resolve primitive parameters such as I to System.Int32.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
To minimize migration pain for callers moving from .NET 10 to .NET 11,
revert JniTypeManager from `abstract` back to a regular class. Methods
that were turned into `abstract` overrides now have `virtual` defaults
so existing subclasses keep compiling:
* Type lookup methods return null/empty defaults.
* RegisterNativeMembers is a no-op by default; reflection-based
behavior remains in ReflectionJniTypeManager (override to opt-in).
Restore PublicAPI.Shipped.txt to its main-branch state (per the
`Microsoft.CodeAnalysis.PublicApiAnalyzers` convention, never edit
shipped entries in place) and track every breaking change in
PublicAPI.Unshipped.txt:
* 5 new JniTypeManager.* APIs are now `virtual` (not `abstract`).
* `*REMOVED*` entries for the two `JniTypeManager.TryRegisterNativeMembers`
overloads that moved to `ReflectionJniTypeManager`.
* `*REMOVED*virtual` + `abstract` pairs for the two virtual->abstract
transitions on `JniValueManager` (CreatePeer, GetValueMarshalerCore(Type)).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jonathanpeppers
jonathanpeppers merged commit 0244c10 into mainJun 9, 2026
2 checks passed
@jonathanpeppers
jonathanpeppers deleted the dev/simonrozsival/fix-aot-warnings-jni-type-manager branch June 9, 2026 23:17
jonathanpeppers pushed a commit that referenced this pull request Jun 12, 2026
Follow-up to #1441 We need this method virtual so that `JavaMarshalValueManager` can override it and try to invoke the `XAConstructorSignature`. The method was previously virtual but as an oversight I removed it in #1441:
- https://github.com/dotnet/java-interop/pull/1441/changes#diff-8ead12d157bc7712affd3d920bb93cddbff3885147fe6297957a44cc522d5430R7
- https://github.com/dotnet/java-interop/pull/1441/changes#diff-b92884de5db7f82df63b483b8319360cd9bfdac3cd97222eb9aff7292a01a07bL449-L467
I noticed we're not disposing the `reference` correctly which might cause a ref leak. In the Mono `AndroidValueManager` we never called into the base method so this would not be observed on mono previously (https://github.com/dotnet/android/blob/main/src/Mono.Android/Java.Interop/TypeManager.cs#L416-L423).
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
simonrozsival added a commit that referenced this pull request Jun 15, 2026
#1441 added `JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers ()`
-- a reflection-free direct call to `JavaProxyObject.RegisterNativeMembers`
-- and bumped that method to `internal` so the call would compile.
That method is dead code: nothing in Java.Interop (or dotnet/android) ever
calls it, and the base `JniTypeManager.RegisterNativeMembers` is a no-op.
`JavaProxyObject`'s native members are -- and were before #1441 --
registered purely via reflection in `ReflectionJniTypeManager`
(`FindAndCallRegisterMethod` discovering the
`[JniAddNativeMethodRegistrationAttribute]`-annotated method), which finds
private methods just fine via `GetRuntimeMethods ()`.
Remove the unused `TryRegisterBuiltInNativeMembers` and restore
`RegisterNativeMembers` to `private` (its state before #1441). Making it
private again also strips the attribute-bearing method from the reference
assembly, which fixes dotnet/android's trimmable typemap scanner falsely
rejecting the built-in `JavaProxyObject` with XA4251 (dotnet/android#11622).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit that referenced this pull request Jun 15, 2026
Replaces the production TryRegisterBuiltInNativeMembers helper with a
samples-only solution, restoring the pre-#1441 behavior for the NativeAOT
type managers.
Background: making JavaProxyObject.RegisterNativeMembers private (the XA4251
fix) is sufficient for the default Android runtime, which discovers and invokes
it via reflection (FindAndCallRegisterMethod -> GetRuntimeMethods(), which
returns private methods). The only thing that needed JavaProxyObject's
registration via a non-reflection path was the two NativeAOT samples, which
#1441 had rewritten to derive from the reflection-free JniRuntime.JniTypeManager
base. That is why #1441 introduced TryRegisterBuiltInNativeMembers.
Instead of carrying that helper in production, have the sample type managers
derive from JniRuntime.ReflectionJniTypeManager again - exactly what the base
JniRuntime.JniTypeManager provided before #1441. Built-in types such as
JavaProxyObject/JavaProxyThrowable are then registered automatically via
reflection, and the samples no longer need any custom registration code.
Changes:
- JavaProxyObject.RegisterNativeMembers: internal -> private. The marshalers
stay (the default reflection runtime registers them by reflecting over this
type); only the visibility changes, which strips the attribute from the
reference assembly and fixes the dotnet/android XA4251 scanner failure.
- Delete JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers (+ its
PublicAPI.Unshipped.txt entry); no longer needed.
- Hello-NativeAOTFromJNI / Hello-NativeAOTFromAndroid: derive from
JniRuntime.ReflectionJniTypeManager and drop the reflection-free overrides and
hand-written JavaProxyObject registration. ReflectionJniTypeManager is
[RequiresDynamicCode]/[RequiresUnreferencedCode], so the constructor suppresses
IL2026/IL3050 with [UnconditionalSuppressMessage]. A #pragma is insufficient
here: it silences the Roslyn analyzer but not the ILLink/ILC publish passes,
which only honor the attribute (verified with a trim-publish of the real type
manager against Java.Interop.dll).
Net: production loses code (1-line visibility change + deletions) and the samples
shrink substantially.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit that referenced this pull request Jun 16, 2026
Build 1465402's "run Hello-NativeAOTFromJNI" step failed at runtime:
System.NotSupportedException: Could not find System.Type corresponding to
Java type JniTypeSignature(TypeName=example/ManagedType ...)
at Java.Interop.ManagedPeer.RegisterNativeMembers(...)
at example.ManagedType.<clinit>(ManagedType.java:15)
Root cause: after #1441, JniRuntime.JniTypeManager.GetType() dispatches through
GetTypeForSimpleReference (singular), not GetTypesForSimpleReference (plural).
The previous sample rewrite only overrode the plural method, so app types like
example/ManagedType were never resolved (the base ReflectionJniTypeManager's
GetTypeForSimpleReference only knows built-in types), and ManagedPeer
registration threw before it could register the type.
Fix: override GetTypeForSimpleReference (singular) in both NativeAOT sample type
managers to resolve the sample's own managed types, falling back to the base for
built-ins. Registration and the reverse Type->JNI mapping continue to be handled
by the reflection base (the pre-#1441 behavior). The override carries the same
[return: DynamicallyAccessedMembers(...)] as the base to satisfy IL2093.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit that referenced this pull request Jun 16, 2026
Add a class-level comment to both NativeAOT sample type managers explaining the
rationale for the [UnconditionalSuppressMessage] IL2026/IL3050 suppressions:
- These are *samples*, not product code. .NET for Android (what we ship) does not
pair ReflectionJniTypeManager with NativeAOT, so it isn't worth the effort to make
these samples fully trim/AOT-clean right now.
- The reflection paths were always trim/AOT-unsafe. Before #1441 the equivalent
suppressions lived inside JniTypeManager itself (justified "NotUsedInAndroid");
#1441 simply moved that responsibility to callers via
[RequiresDynamicCode]/[RequiresUnreferencedCode].
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit to dotnet/android that referenced this pull request Jun 16, 2026
Revert the external/Java.Interop submodule bump. Pinning an unmerged
java-interop PR-branch commit is fragile, and advancing to java-interop
main HEAD would drag in unrelated changes (e.g. dotnet/java-interop#1441)
that need their own coordination. The generator-side removal lives in
dotnet/java-interop#1470 and will reach Mono.Android through the normal
submodule bump once it merges; this PR keeps only the independent runtime
cleanup of the now-dead `packageLookup` plumbing.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers pushed a commit that referenced this pull request Jun 16, 2026
…in (#1468)
#1441 changed `JavaProxyObject.RegisterNativeMembers` from `private` to
`internal` so the new reflection-free
`JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers ()` could call
it directly.
A side effect is that the `[JniAddNativeMethodRegistrationAttribute]`-
annotated method now appears in Java.Interop's *reference* assembly.
dotnet/android's trimmable typemap scanner reads reference assemblies and
rejects any type carrying that attribute (XA4251), so every trimmable
typemap build now fails on the built-in `JavaProxyObject`
(see dotnet/android#11622).
Restore the method to `private` (so it is stripped from the reference
assembly) and expose a small attribute-free `internal` entry point,
`RegisterBuiltInNativeMembers (JniType)`, for the built-in registration
path to call. Runtime behavior is unchanged.
### [Java.Interop] Restore pure-reflection registration for JavaProxyObject
#1441 added `JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers ()`
-- a reflection-free direct call to `JavaProxyObject.RegisterNativeMembers`
-- and bumped that method to `internal` so the call would compile.
That method is dead code: nothing in Java.Interop (or dotnet/android) ever
calls it, and the base `JniTypeManager.RegisterNativeMembers` is a no-op.
`JavaProxyObject`'s native members are -- and were before #1441 --
registered purely via reflection in `ReflectionJniTypeManager`
(`FindAndCallRegisterMethod` discovering the
`[JniAddNativeMethodRegistrationAttribute]`-annotated method), which finds
private methods just fine via `GetRuntimeMethods ()`.
Remove the unused `TryRegisterBuiltInNativeMembers` and restore
`RegisterNativeMembers` to `private` (its state before #1441). Making it
private again also strips the attribute-bearing method from the reference
assembly, which fixes dotnet/android's trimmable typemap scanner falsely
rejecting the built-in `JavaProxyObject` with XA4251 (dotnet/android#11622).
### Address review: restore TryRegisterBuiltInNativeMembers (not dead code)
The previous commit deleted JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers
claiming it was unused. That was wrong: it has two live callers in the NativeAOT
samples (samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs and
samples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs), both JniTypeManager
subclasses. Deleting it broke them (CS0103) and removed the only path that
registers JavaProxyObject's equals/hashCode/toString natives for NativeAOT type
managers (which don't use the reflection-based FindAndCallRegisterMethod path).
The breakage was hidden in CI only because the NativeAOT sample steps use
continueOnError: true.
Keep the XA4251 fix (RegisterNativeMembers stays private, so the
[JniAddNativeMethodRegistrationAttribute] is stripped from the reference assembly)
and restore TryRegisterBuiltInNativeMembers by extracting the registration logic
into a new attribute-free internal helper, JavaProxyObject.AddBuiltInRegistrations.
Both the private attributed RegisterNativeMembers (reflection path) and
TryRegisterBuiltInNativeMembers (NativeAOT path) call it. Restore the
PublicAPI.Unshipped.txt entry as well.
Verified: Java.Interop.dll builds clean; the reference assembly contains no method
carrying [JniAddNativeMethodRegistration]; the sample's call to
TryRegisterBuiltInNativeMembers resolves (no CS0103).
### Keep built-in registration in the samples via ReflectionJniTypeManager
Replaces the production TryRegisterBuiltInNativeMembers helper with a
samples-only solution, restoring the pre-#1441 behavior for the NativeAOT
type managers.
Background: making JavaProxyObject.RegisterNativeMembers private (the XA4251
fix) is sufficient for the default Android runtime, which discovers and invokes
it via reflection (FindAndCallRegisterMethod -> GetRuntimeMethods(), which
returns private methods). The only thing that needed JavaProxyObject's
registration via a non-reflection path was the two NativeAOT samples, which
#1441 had rewritten to derive from the reflection-free JniRuntime.JniTypeManager
base. That is why #1441 introduced TryRegisterBuiltInNativeMembers.
Instead of carrying that helper in production, have the sample type managers
derive from JniRuntime.ReflectionJniTypeManager again - exactly what the base
JniRuntime.JniTypeManager provided before #1441. Built-in types such as
JavaProxyObject/JavaProxyThrowable are then registered automatically via
reflection, and the samples no longer need any custom registration code.
Changes:
- JavaProxyObject.RegisterNativeMembers: internal -> private. The marshalers
stay (the default reflection runtime registers them by reflecting over this
type); only the visibility changes, which strips the attribute from the
reference assembly and fixes the dotnet/android XA4251 scanner failure.
- Delete JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers (+ its
PublicAPI.Unshipped.txt entry); no longer needed.
- Hello-NativeAOTFromJNI / Hello-NativeAOTFromAndroid: derive from
JniRuntime.ReflectionJniTypeManager and drop the reflection-free overrides and
hand-written JavaProxyObject registration. ReflectionJniTypeManager is
[RequiresDynamicCode]/[RequiresUnreferencedCode], so the constructor suppresses
IL2026/IL3050 with [UnconditionalSuppressMessage]. A #pragma is insufficient
here: it silences the Roslyn analyzer but not the ILLink/ILC publish passes,
which only honor the attribute (verified with a trim-publish of the real type
manager against Java.Interop.dll).
Net: production loses code (1-line visibility change + deletions) and the samples
shrink substantially.
### Drop verbose explanatory comments from the NativeAOT sample type managers
The [UnconditionalSuppressMessage] Justification strings already convey the
necessary context.
### Fix NativeAOT sample type resolution: override GetTypeForSimpleReference
Build 1465402's "run Hello-NativeAOTFromJNI" step failed at runtime:
System.NotSupportedException: Could not find System.Type corresponding to
Java type JniTypeSignature(TypeName=example/ManagedType ...)
at Java.Interop.ManagedPeer.RegisterNativeMembers(...)
at example.ManagedType.<clinit>(ManagedType.java:15)
Root cause: after #1441, JniRuntime.JniTypeManager.GetType() dispatches through
GetTypeForSimpleReference (singular), not GetTypesForSimpleReference (plural).
The previous sample rewrite only overrode the plural method, so app types like
example/ManagedType were never resolved (the base ReflectionJniTypeManager's
GetTypeForSimpleReference only knows built-in types), and ManagedPeer
registration threw before it could register the type.
Fix: override GetTypeForSimpleReference (singular) in both NativeAOT sample type
managers to resolve the sample's own managed types, falling back to the base for
built-ins. Registration and the reverse Type->JNI mapping continue to be handled
by the reflection base (the pre-#1441 behavior). The override carries the same
[return: DynamicallyAccessedMembers(...)] as the base to satisfy IL2093.
### Address review: drop unreachable null guard in FromAndroid sample
`typeMappings` is assigned in its field initializer and never set to null, so the
`if (typeMappings == null) yield break;` guard in CreateSimpleReferencesEnumerator
was unreachable dead code. Remove it.
### Document why the NativeAOT sample trim/AOT suppressions are acceptable
Add a class-level comment to both NativeAOT sample type managers explaining the
rationale for the [UnconditionalSuppressMessage] IL2026/IL3050 suppressions:
- These are *samples*, not product code. .NET for Android (what we ship) does not
pair ReflectionJniTypeManager with NativeAOT, so it isn't worth the effort to make
these samples fully trim/AOT-clean right now.
- The reflection paths were always trim/AOT-unsafe. Before #1441 the equivalent
suppressions lived inside JniTypeManager itself (justified "NotUsedInAndroid");
#1441 simply moved that responsibility to callers via
[RequiresDynamicCode]/[RequiresUnreferencedCode].
### Reword NativeAOT sample suppression justifications to describe why it's safe
The previous IL2026/IL3050 justifications ("does not require unreferenced code /
runtime code generation") read as the opposite of reality, since
ReflectionJniTypeManager is exactly [RequiresUnreferencedCode]/[RequiresDynamicCode].
Reword to describe why the suppression is correct for this sample:
- IL2026: the assembly is rooted via TrimmerRootAssembly and the reflected
registration members are preserved by the [DynamicallyAccessedMembers]
annotations on the RegisterNativeMembers(Type) -> FindAndCallRegisterMethod path.
- IL3050: registration uses CreateDelegate on compile-time-known static methods
(no MakeGenericType / expression compilation), so no runtime codegen is required.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers pushed a commit to dotnet/android that referenced this pull request Jun 16, 2026
…mbing (#11667)
The binding generator used to emit a `Java.Interop.__TypeRegistrations`
class whose `RegisterPackages ()` populated `TypeManager.packageLookup`.
The only reader of that dictionary -- the `TypeRegistrationFallback` path
in `GetJavaToManagedTypeCore` -- was removed in #9471, so the dictionary
has been write-only (and, since nothing generated calls it, effectively
unused) ever since. Type resolution now goes through the native /
trimmable type map.
This bumps external/Java.Interop to the generator change that stops
emitting the dead class (dotnet/java-interop#1470) and removes the
now-dead runtime plumbing:
* Delete the `packageLookup` field and `LazyInitPackageLookup ()`.
* `TypeManager.RegisterPackage`/`RegisterPackages` are shipped public
API, so they are kept as validated no-op stubs rather than removed.
Fixes#11663
### [Mono.Android] Throw NotSupportedException with an explanatory message
`TypeManager.RegisterPackage`/`RegisterPackages` are retired shipped public
APIs whose registration mechanism no longer exists. Rather than silently
doing nothing, they now throw `NotSupportedException` carrying an explanation
of why the call is unsupported and what replaced it ("Java package type
registration is no longer supported. Java-to-managed type resolution now goes
through the native and trimmable type maps."), so a caller that still invokes
them gets a clear, actionable reason instead of a bare exception.
### [Mono.Android] Don't bump external/Java.Interop in this PR
Revert the external/Java.Interop submodule bump. Pinning an unmerged
java-interop PR-branch commit is fragile, and advancing to java-interop
main HEAD would drag in unrelated changes (e.g. dotnet/java-interop#1441)
that need their own coordination. The generator-side removal lives in
dotnet/java-interop#1470 and will reach Mono.Android through the normal
submodule bump once it merges; this PR keeps only the independent runtime
cleanup of the now-dead `packageLookup` plumbing.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers added a commit to dotnet/android that referenced this pull request Jun 16, 2026
Bumps [external/Java.Interop](https://github.com/dotnet/java-interop) from `b881d21` to [`6820a9cb`](dotnet/java-interop@6820a9c).
## Java.Interop changes pulled in
- [`6820a9c`](dotnet/java-interop@6820a9c) [Java.Interop] Make JavaProxyObject.RegisterNativeMembers private again ([#1468](dotnet/java-interop#1468))
- [`a2159d7`](dotnet/java-interop@a2159d70) Remove NuGet dependency grouping from Dependabot config ([#1459](dotnet/java-interop#1459))
- [`a0ca676`](dotnet/java-interop@a0ca6760) [ci] Update agentic workflows to claude-opus-4.8 ([#1461](dotnet/java-interop#1461))
- [`6d8aef7`](dotnet/java-interop@6d8aef7b) Bump to dotnet/android-tools@1fb68b6 ([#1457](dotnet/java-interop#1457))
- [`cfca8ad`](dotnet/java-interop@cfca8ad) Upgrade gh-aw to v0.79.6 and recompile workflows ([#1460](dotnet/java-interop#1460))
- [`0104236`](dotnet/java-interop@0104236e) [JniValueManager] Make TryConstructPeer virtual ([#1456](dotnet/java-interop#1456))
- [`d7dbad5`](dotnet/java-interop@d7dbad5) Remove Java.Interop.Dynamic ([#1449](dotnet/java-interop#1449))
- [`0244c10`](dotnet/java-interop@0244c10) Split reflection-based JniTypeManager and JniValueManager behavior ([#1441](dotnet/java-interop#1441))
- [`9ca7064`](dotnet/java-interop@9ca7064) [Java.Interop] Remove legacy `NET` preprocessor paths ([#1451](dotnet/java-interop#1451))
- [`cd2fc12`](dotnet/java-interop@cd2fc12) Remove Java.Interop.GenericMarshaler ([#1450](dotnet/java-interop#1450))
See full diff in [compare view](dotnet/java-interop@b881d21...6820a9c).
## Changes in this PR (beyond the submodule bump)
The Java.Interop bump — particularly [#1441](dotnet/java-interop#1441) (split reflection-based `JniTypeManager` and `JniValueManager`) and [#1449](dotnet/java-interop#1449) (removal of `Java.Interop.Dynamic`) — required a number of follow-on changes in `dotnet/android`:
### Runtime / `Mono.Android`
- **Adapt Android runtime to Java.Interop reflection managers** — switch the `JavaInteropTypeManager` / `JavaInteropValueManager` hierarchy to the new reflection-based base types and update type/value manager construction to match.
- **Change base type of value managers and type managers** so the runtime composes correctly with the new Java.Interop split.
- **Suppress reflection manager trim warnings only** — narrow the existing trim suppressions so they apply to the reflection-only managers and don't hide other warnings.
- **Suppress IL2068 on `ManagedTypeManager.GetTypeForSimpleReference`** — annotate the entry point that legitimately requires reflection so trim analysis is satisfied.
### Tests / apkdescs
- **Regenerate `BuildReleaseArm64SimpleDotNet.MonoVM.apkdesc`** to track size changes from the Java.Interop bump.
- **Regenerate Simple `BuildReleaseArm64` apkdescs** (`MonoVM` + `NativeAOT`) from a `Release` SDK so they match what CI produces.
- **Update NativeAOT warning expectations** — `Mono.Android` now produces fewer trim/AOT warnings, so loosen the strict `4 Warning(s)` checks in `SupportedOSPlatformVersion` and `CheckSignApk` to `AssertHasAtMostWarnings (2)` (new helper in `AssertionExtensions`) and drop the `BuildHasTrimmerWarnings` data counts (`3 → 2`, `4 → 3`) accordingly.
### Merges
- Two merges from `origin/main` to keep the branch current and resolve a conflict in `Microsoft.Android.Sdk.TypeMap.Trimmable.targets`.
Co-authored-by: Simon Rozsival <simon@rozsival.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Jonathan Peppers <jonathan.peppers@microsoft.com>
jonathanpeppers pushed a commit that referenced this pull request Jun 17, 2026
… "trimmable type map" (#1454)
Follow-up to #1441.
Prerequisite for dotnet/android#11617.
This keeps the Java.Interop changes focused on the small base hook dotnet/android needs for the trimmable type-map integration. The reflection value manager continues to use value marshalers internally, while Android's generated/trimmable value manager can provide the only production `JavaObjectArray<T>` element-assignment object-reference path without implementing `GetValueMarshaler*()`.
## Changes
- Add minimal `JniValueManager` object-reference API for `JavaObjectArray<T>.SetElementAt()`:
- `CreateLocalObjectReferenceArgument(Type type, object? value)` returns an owned local `JniObjectReference` for element assignment. Callers must dispose the returned reference.
- Make the matching core method abstract so non-reflection value managers can implement this path directly.
- Keep value marshalers as a `ReflectionJniValueManager` implementation detail: reflection creates marshaler state, copies out an independent local reference, then destroys the state immediately.
- Update `JavaObjectArray<T>` to call the value manager directly instead of calling `GetValueMarshaler<T>()` in production paths.
- Simplify `JavaObjectArray<T>.Clear()` to set array slots to Java null directly; it no longer needs value-manager or value-marshaler state.
- Remove the earlier exposed proxy/peerable marshaler accessors, broad/generic state overloads, default-value state API, destroy-state API, and `ParameterAttributes synchronize` from this value-manager object-reference path.
- Keep ManagedPeer-dependent tests categorized as unsupported for the Android trimmable configuration rather than carrying Android-specific Java fixture workarounds in this PR.
- Include the small type-manager/test cleanups needed by the dotnet/android integration branch.
## Non-goals
- This PR does not make value marshalers public trimmable API.
- This PR does not require trimmable Android value managers to implement or use `GetValueMarshaler*()`.
- This PR does not remove or replace ManagedPeer-dependent Java.Interop test fixtures.
## Validation
- `dotnet build external/Java.Interop/src/Java.Interop/Java.Interop.csproj -p:Configuration=Debug -m:1 -nodeReuse:false --no-restore -v:minimal`
- From the dotnet/android integration branch:
- `dotnet test tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj -v minimal --no-restore` (`562` passed)
- `dotnet build src/Mono.Android/Mono.Android.csproj -p:Configuration=Debug -p:AndroidSdkDirectory=/Users/simonrozsival/android-toolchain/sdk -m:1 -nodeReuse:false --no-restore -v:minimal` compiled `Mono.Android.Runtime.dll`; the remaining local failure is Android SDK provisioning (`extras/android/m2repository.staging` and `docs.staging` missing), not C# or trim-analyzer errors.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers added a commit to dotnet/android that referenced this pull request Jun 30, 2026
The ReflectionJniTypeManager [RequiresDynamicCode] attribute predates this submodule bump (added in dotnet/java-interop#1441), so the ManagedTypeManager..ctor() IL3050 is already part of the BuildHasNoWarnings test's baseline of 4 warnings. Only the RegisterNativeMembers warning is new from this PR; suppressing the ctor warning too would drop the count to 2 and fail the test.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

ready-to-reviewThis PR is ready to review/merge, thanks!

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@simonrozsival@jonathanpeppers
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
This repository was archived by the owner on Aug 27, 2026. It is now read-only.

Split reflection-based JniTypeManager and JniValueManager behavior - #1441

Merged
jonathanpeppers merged 30 commits into
mainfrom
dev/simonrozsival/fix-aot-warnings-jni-type-manager
Jun 9, 2026
Merged

Split reflection-based JniTypeManager and JniValueManager behavior#1441
jonathanpeppers merged 30 commits into
mainfrom
dev/simonrozsival/fix-aot-warnings-jni-type-manager

Conversation

@simonrozsival

@simonrozsivalsimonrozsival commented Jun 4, 2026

Copy link
Copy Markdown
Member

Splits the reflection-backed paths out of JniRuntime.JniTypeManager and JniRuntime.JniValueManager so callers that don't need reflection (e.g. NativeAOT) can opt out, while keeping the public API surface as compatible as possible for callers migrating from .NET 10.

What changed

  • JniRuntime.ReflectionJniTypeManager (new): all reflection-based type lookup, signature discovery, and native-method registration moves here. Annotated with [RequiresDynamicCode] / [RequiresUnreferencedCode].
  • JniRuntime.ReflectionJniValueManager (new): all reflection-based peer construction, value marshaling, and activation moves here.
  • JniRuntime.JniTypeManager stays a non-abstract base class. Methods that previously had reflection defaults are now virtual no-ops (or return null/empty). Subclasses that want the old default behavior should derive from ReflectionJniTypeManager.
  • JniRuntime.JniValueManager stays abstract (it already was). A few previously-virtual methods (CreatePeer, GetValueMarshalerCore(Type)) become abstract since their reflection-based defaults moved out — public consumers (dotnet/android's value managers) already override these.
  • In-tree consumers updated: JreTypeManager, ManagedValueManager, MonoRuntimeValueManager now derive from the new Reflection* bases. The NativeAOT samples keep deriving from the abstract bases with explicit type mappings.

Public API impact

PublicAPI.Shipped.txt is unchanged. All breaking changes are tracked in PublicAPI.Unshipped.txt with *REMOVED* markers:

  • JniTypeManager.TryRegisterNativeMembers (both overloads) — moved to ReflectionJniTypeManager as protected helpers.
  • JniValueManager.CreatePeer and GetValueMarshalerCore(Type)virtualabstract.

A non-abstract JniTypeManager keeps new JniRuntime.JniTypeManager() and direct subclasses compiling without changes.

Validation

  • CI is green.
  • Local dotnet build Java.Interop.sln passes (PublicApiAnalyzer enforces tracking is consistent).

@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/fix-aot-warnings-jni-type-manager branch from 5ff4c17 to 6d72140CompareJune 4, 2026 14:24
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/fix-aot-warnings-jni-type-manager branch from 6d72140 to 58f0dbdCompareJune 4, 2026 14:30
@simonrozsival

Copy link
Copy Markdown
MemberAuthor

/azp run

@azure-pipelines

Copy link
Copy Markdown
No pipelines are associated with this pull request.

@simonrozsival
simonrozsival marked this pull request as ready for review June 8, 2026 11:55
CopilotAI review requested due to automatic review settings June 8, 2026 11:55

CopilotAI 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.

Pull request overview

This draft PR refactors JniRuntime.JniTypeManager into an abstract base and moves the default reflection-based behavior into a new JniRuntime.DynamicJniTypeManager, with corresponding updates across tests, JRE runtime helpers, samples, and public API baselines.

Changes:

  • Introduces JniRuntime.DynamicJniTypeManager and makes JniRuntime.JniTypeManager abstract, shifting default/reflection-backed logic into the new derived type.
  • Updates test and JRE type managers to inherit from DynamicJniTypeManager, and adjusts tests for new behaviors (e.g., generic invoker handling, GetType array behavior).
  • Updates trimmer/AOT annotations and PublicAPI baselines to reflect the split and new abstract members.
Show a summary per file
FileDescription
tests/Java.Interop-Tests/Java.Interop/JniTypeManagerTests.csUpdates expectations around GetType behavior (notably array signatures and generic holder mapping).
tests/Java.Interop-Tests/Java.Interop/JniRuntimeTest.csSwitches proxy manager to DynamicJniTypeManager and adds trim suppression for test-only reflection use.
tests/Java.Interop-Tests/Java.Interop/JniRuntime.JniTypeManagerTests.csAdds a generic invoker negative test and updates test manager base type.
tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.csUpdates fixture type manager to DynamicJniTypeManager and refines type lookup with trimming annotations.
src/Java.Runtime.Environment/Java.Interop/JreTypeManager.csMoves to DynamicJniTypeManager and adds a DAM-annotated GetTypeForSimpleReference override.
src/Java.Interop/PublicAPI.Unshipped.txtRecords newly introduced/reshaped APIs (new type, newly-abstract members, overrides).
src/Java.Interop/PublicAPI.Shipped.txtUpdates shipped API surface to reflect newly-abstract members and removed methods from JniTypeManager.
src/Java.Interop/Java.Interop/JniValueMarshaler.csSimplifies expression-based marshaler instantiation and adjusts trimming annotations.
src/Java.Interop/Java.Interop/JniRuntime.JniValueManager.csSwitches peer-type selection to GetReflectionConstructibleTypes() and refactors uninitialized peer creation.
src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.csMakes JniTypeManager abstract and factors previously-inlined logic into abstract core methods.
src/Java.Interop/Java.Interop/JniRuntime.DynamicJniTypeManager.csAdds the new reflection-backed type manager implementation and related registration behavior.
src/Java.Interop/Java.Interop/JniRuntime.csRemoves the default new JniTypeManager() fallback; requires an explicit type manager in creation options.
src/Java.Interop/Java.Interop/JavaPrimitiveArrays.ttRetargets primitive array helper generation to DynamicJniTypeManager.
src/Java.Interop/Java.Interop/JavaPrimitiveArrays.csUpdates the generated primitive array helper container type to DynamicJniTypeManager.
src/Java.Interop/GlobalSuppressions.csAdds CA1034 suppression for the new nested DynamicJniTypeManager type.
samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.csUpdates sample type manager implementation to new hierarchy and adds explicit native member registration.
samples/Hello-NativeAOTFromJNI/ManagedType.csChanges the native registration hook to internal for sample-driven registration.
samples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.csUpdates sample type manager implementation to new hierarchy and implements abstract members.

Copilot's findings

  • Files reviewed: 18/18 changed files
  • Comments generated: 3

Comment threadsamples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs Outdated
Comment threadsamples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs Outdated
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival
simonrozsival marked this pull request as draft June 8, 2026 14:24
simonrozsivaland others added 9 commits June 8, 2026 16:32
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>
Map Java.Interop's internal proxy peer types in the base type manager so custom AOT-safe managers do not need reflection-based DynamicJniTypeManager behavior for JavaProxyThrowable/JavaProxyObject.
Register built-in proxy native members by JNI type name before asking custom managers for a managed Type, avoiding DAM-annotated lookups of internal Exception/Delegate-derived types.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Let the base JniTypeManager resolve primitive JNI keyword signatures without requiring custom AOT-safe managers to duplicate the dynamic type manager's primitive mappings.
Use that base lookup as a fallback when matching Java-to-managed constructor signatures so signatures such as (I)V resolve to System.Int32.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival
simonrozsival marked this pull request as ready for review June 9, 2026 07:23
simonrozsivaland others added 2 commits June 9, 2026 09:31
Rename DynamicJniTypeManager to ReflectionJniTypeManager so the type name describes the reflection-based behavior that is incompatible with Native AOT.
Update the matching source file name, API baseline, JRE type manager inheritance, generated primitive-array partial, and tests.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsivalsimonrozsival changed the title Split reflection-based JniTypeManager behaviorSplit reflection-based JniTypeManager and JniValueManager behaviorJun 9, 2026

CopilotAI commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved the merge conflicts in commit Resolve merge conflicts: remove #if NET guards, keep abstract methods in base JniTypeManager.

The two new commits in main since the last merge (Remove Java.Interop.GenericMarshaler (#1450) and Remove legacy NET preprocessor paths (#1451)) conflicted with our branch in three files:

  • JniRuntime.JniTypeManager.cs (two conflicts): Kept the abstract method declarations (GetInvokerTypeCore, GetStaticMethodFallbackTypesCore, RegisterNativeMembers) from our branch while adopting the #if NET guard removals from main. The implementation helpers (TryRegisterNativeMembers, TryLoadJniMarshalMethods, etc.) remain in DynamicJniTypeManager where our PR moved them.
  • ManagedPeer.cs: Removed the #if NET guard that was wrapping the ReadOnlySpan<char> methods block.
  • JreRuntime.cs: Kept our branch's structure (TypeManager default set in the [RequiresDynamicCode]-annotated CreateJreVM, not in CreateJreVMCore), discarding main's addition of builder.TypeManager ??= new JreTypeManager(...) to CreateJreVMCore which would have bypassed the AOT warning.

Also removed the remaining #if NET guards from JniRuntime.ReflectionJniTypeManager.cs and JreRuntime.cs that were in our branch but no longer needed after the main merge.

@simonrozsival

Copy link
Copy Markdown
MemberAuthor

/review

@github-actions

github-actionsBot commented Jun 9, 2026

Copy link
Copy Markdown

Java.Interop PR Reviewer completed successfully!

@github-actionsgithub-actionsBot 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.

✅ LGTM — Clean architectural split

Summary: This PR cleanly separates the reflection-based JniTypeManager / JniValueManager behavior into dedicated ReflectionJniTypeManager and ReflectionJniValueManager subclasses, making the base classes abstract and properly annotating the reflection-based paths with [RequiresDynamicCode] / [RequiresUnreferencedCode]. The NativeAOT samples are updated to implement the abstract base directly, and tests/downstream types are updated accordingly.

What looks good:

  • The split is well-structured — base classes contain only the contract and built-in type handling; all reflection goes into the Reflection* subclasses.
  • [RequiresDynamicCode] and [RequiresUnreferencedCode] annotations are applied consistently throughout the chain (samples, tests, JRE runtime types).
  • Public API baselines are updated correctly.
  • TryRegisterBuiltInNativeMembers is a nice extraction that NativeAOT consumers can call without needing the full reflection machinery.
  • The JreRuntimeOptions.CreateJreVM(JniTypeManager) overload is a good ergonomic addition.

Issues by severity:

SeverityCount
⚠️ Warning1
💡 Suggestion3

Notes:

  • CI only shows license/cla — no build/test results visible yet. The PR is not mergeable until CI validates the full build.
  • The large abstract surface area for NativeAOT consumers (~12 methods) may be worth revisiting — see inline suggestion about making unused extension points virtual with default null returns.

Generated by Java.Interop PR Reviewer for issue #1441 · ● 14.3M

Comment threadsamples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs
Comment threadsamples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs
Comment threadsrc/Java.Interop/Java.Interop/JniRuntime.ReflectionJniTypeManager.cs Outdated
simonrozsivaland others added 2 commits June 9, 2026 22:20
Map JNI primitive and boxed primitive signatures in the NativeAOT sample type managers so constructor signature matching can resolve primitive parameters such as I to System.Int32.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
To minimize migration pain for callers moving from .NET 10 to .NET 11,
revert JniTypeManager from `abstract` back to a regular class. Methods
that were turned into `abstract` overrides now have `virtual` defaults
so existing subclasses keep compiling:
* Type lookup methods return null/empty defaults.
* RegisterNativeMembers is a no-op by default; reflection-based
behavior remains in ReflectionJniTypeManager (override to opt-in).
Restore PublicAPI.Shipped.txt to its main-branch state (per the
`Microsoft.CodeAnalysis.PublicApiAnalyzers` convention, never edit
shipped entries in place) and track every breaking change in
PublicAPI.Unshipped.txt:
* 5 new JniTypeManager.* APIs are now `virtual` (not `abstract`).
* `*REMOVED*` entries for the two `JniTypeManager.TryRegisterNativeMembers`
overloads that moved to `ReflectionJniTypeManager`.
* `*REMOVED*virtual` + `abstract` pairs for the two virtual->abstract
transitions on `JniValueManager` (CreatePeer, GetValueMarshalerCore(Type)).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jonathanpeppers
jonathanpeppers merged commit 0244c10 into mainJun 9, 2026
2 checks passed
@jonathanpeppers
jonathanpeppers deleted the dev/simonrozsival/fix-aot-warnings-jni-type-manager branch June 9, 2026 23:17
jonathanpeppers pushed a commit that referenced this pull request Jun 12, 2026
Follow-up to #1441 We need this method virtual so that `JavaMarshalValueManager` can override it and try to invoke the `XAConstructorSignature`. The method was previously virtual but as an oversight I removed it in #1441:
- https://github.com/dotnet/java-interop/pull/1441/changes#diff-8ead12d157bc7712affd3d920bb93cddbff3885147fe6297957a44cc522d5430R7
- https://github.com/dotnet/java-interop/pull/1441/changes#diff-b92884de5db7f82df63b483b8319360cd9bfdac3cd97222eb9aff7292a01a07bL449-L467
I noticed we're not disposing the `reference` correctly which might cause a ref leak. In the Mono `AndroidValueManager` we never called into the base method so this would not be observed on mono previously (https://github.com/dotnet/android/blob/main/src/Mono.Android/Java.Interop/TypeManager.cs#L416-L423).
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
simonrozsival added a commit that referenced this pull request Jun 15, 2026
#1441 added `JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers ()`
-- a reflection-free direct call to `JavaProxyObject.RegisterNativeMembers`
-- and bumped that method to `internal` so the call would compile.
That method is dead code: nothing in Java.Interop (or dotnet/android) ever
calls it, and the base `JniTypeManager.RegisterNativeMembers` is a no-op.
`JavaProxyObject`'s native members are -- and were before #1441 --
registered purely via reflection in `ReflectionJniTypeManager`
(`FindAndCallRegisterMethod` discovering the
`[JniAddNativeMethodRegistrationAttribute]`-annotated method), which finds
private methods just fine via `GetRuntimeMethods ()`.
Remove the unused `TryRegisterBuiltInNativeMembers` and restore
`RegisterNativeMembers` to `private` (its state before #1441). Making it
private again also strips the attribute-bearing method from the reference
assembly, which fixes dotnet/android's trimmable typemap scanner falsely
rejecting the built-in `JavaProxyObject` with XA4251 (dotnet/android#11622).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit that referenced this pull request Jun 15, 2026
Replaces the production TryRegisterBuiltInNativeMembers helper with a
samples-only solution, restoring the pre-#1441 behavior for the NativeAOT
type managers.
Background: making JavaProxyObject.RegisterNativeMembers private (the XA4251
fix) is sufficient for the default Android runtime, which discovers and invokes
it via reflection (FindAndCallRegisterMethod -> GetRuntimeMethods(), which
returns private methods). The only thing that needed JavaProxyObject's
registration via a non-reflection path was the two NativeAOT samples, which
#1441 had rewritten to derive from the reflection-free JniRuntime.JniTypeManager
base. That is why #1441 introduced TryRegisterBuiltInNativeMembers.
Instead of carrying that helper in production, have the sample type managers
derive from JniRuntime.ReflectionJniTypeManager again - exactly what the base
JniRuntime.JniTypeManager provided before #1441. Built-in types such as
JavaProxyObject/JavaProxyThrowable are then registered automatically via
reflection, and the samples no longer need any custom registration code.
Changes:
- JavaProxyObject.RegisterNativeMembers: internal -> private. The marshalers
stay (the default reflection runtime registers them by reflecting over this
type); only the visibility changes, which strips the attribute from the
reference assembly and fixes the dotnet/android XA4251 scanner failure.
- Delete JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers (+ its
PublicAPI.Unshipped.txt entry); no longer needed.
- Hello-NativeAOTFromJNI / Hello-NativeAOTFromAndroid: derive from
JniRuntime.ReflectionJniTypeManager and drop the reflection-free overrides and
hand-written JavaProxyObject registration. ReflectionJniTypeManager is
[RequiresDynamicCode]/[RequiresUnreferencedCode], so the constructor suppresses
IL2026/IL3050 with [UnconditionalSuppressMessage]. A #pragma is insufficient
here: it silences the Roslyn analyzer but not the ILLink/ILC publish passes,
which only honor the attribute (verified with a trim-publish of the real type
manager against Java.Interop.dll).
Net: production loses code (1-line visibility change + deletions) and the samples
shrink substantially.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit that referenced this pull request Jun 16, 2026
Build 1465402's "run Hello-NativeAOTFromJNI" step failed at runtime:
System.NotSupportedException: Could not find System.Type corresponding to
Java type JniTypeSignature(TypeName=example/ManagedType ...)
at Java.Interop.ManagedPeer.RegisterNativeMembers(...)
at example.ManagedType.<clinit>(ManagedType.java:15)
Root cause: after #1441, JniRuntime.JniTypeManager.GetType() dispatches through
GetTypeForSimpleReference (singular), not GetTypesForSimpleReference (plural).
The previous sample rewrite only overrode the plural method, so app types like
example/ManagedType were never resolved (the base ReflectionJniTypeManager's
GetTypeForSimpleReference only knows built-in types), and ManagedPeer
registration threw before it could register the type.
Fix: override GetTypeForSimpleReference (singular) in both NativeAOT sample type
managers to resolve the sample's own managed types, falling back to the base for
built-ins. Registration and the reverse Type->JNI mapping continue to be handled
by the reflection base (the pre-#1441 behavior). The override carries the same
[return: DynamicallyAccessedMembers(...)] as the base to satisfy IL2093.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit that referenced this pull request Jun 16, 2026
Add a class-level comment to both NativeAOT sample type managers explaining the
rationale for the [UnconditionalSuppressMessage] IL2026/IL3050 suppressions:
- These are *samples*, not product code. .NET for Android (what we ship) does not
pair ReflectionJniTypeManager with NativeAOT, so it isn't worth the effort to make
these samples fully trim/AOT-clean right now.
- The reflection paths were always trim/AOT-unsafe. Before #1441 the equivalent
suppressions lived inside JniTypeManager itself (justified "NotUsedInAndroid");
#1441 simply moved that responsibility to callers via
[RequiresDynamicCode]/[RequiresUnreferencedCode].
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit to dotnet/android that referenced this pull request Jun 16, 2026
Revert the external/Java.Interop submodule bump. Pinning an unmerged
java-interop PR-branch commit is fragile, and advancing to java-interop
main HEAD would drag in unrelated changes (e.g. dotnet/java-interop#1441)
that need their own coordination. The generator-side removal lives in
dotnet/java-interop#1470 and will reach Mono.Android through the normal
submodule bump once it merges; this PR keeps only the independent runtime
cleanup of the now-dead `packageLookup` plumbing.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers pushed a commit that referenced this pull request Jun 16, 2026
…in (#1468)
#1441 changed `JavaProxyObject.RegisterNativeMembers` from `private` to
`internal` so the new reflection-free
`JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers ()` could call
it directly.
A side effect is that the `[JniAddNativeMethodRegistrationAttribute]`-
annotated method now appears in Java.Interop's *reference* assembly.
dotnet/android's trimmable typemap scanner reads reference assemblies and
rejects any type carrying that attribute (XA4251), so every trimmable
typemap build now fails on the built-in `JavaProxyObject`
(see dotnet/android#11622).
Restore the method to `private` (so it is stripped from the reference
assembly) and expose a small attribute-free `internal` entry point,
`RegisterBuiltInNativeMembers (JniType)`, for the built-in registration
path to call. Runtime behavior is unchanged.
### [Java.Interop] Restore pure-reflection registration for JavaProxyObject
#1441 added `JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers ()`
-- a reflection-free direct call to `JavaProxyObject.RegisterNativeMembers`
-- and bumped that method to `internal` so the call would compile.
That method is dead code: nothing in Java.Interop (or dotnet/android) ever
calls it, and the base `JniTypeManager.RegisterNativeMembers` is a no-op.
`JavaProxyObject`'s native members are -- and were before #1441 --
registered purely via reflection in `ReflectionJniTypeManager`
(`FindAndCallRegisterMethod` discovering the
`[JniAddNativeMethodRegistrationAttribute]`-annotated method), which finds
private methods just fine via `GetRuntimeMethods ()`.
Remove the unused `TryRegisterBuiltInNativeMembers` and restore
`RegisterNativeMembers` to `private` (its state before #1441). Making it
private again also strips the attribute-bearing method from the reference
assembly, which fixes dotnet/android's trimmable typemap scanner falsely
rejecting the built-in `JavaProxyObject` with XA4251 (dotnet/android#11622).
### Address review: restore TryRegisterBuiltInNativeMembers (not dead code)
The previous commit deleted JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers
claiming it was unused. That was wrong: it has two live callers in the NativeAOT
samples (samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs and
samples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs), both JniTypeManager
subclasses. Deleting it broke them (CS0103) and removed the only path that
registers JavaProxyObject's equals/hashCode/toString natives for NativeAOT type
managers (which don't use the reflection-based FindAndCallRegisterMethod path).
The breakage was hidden in CI only because the NativeAOT sample steps use
continueOnError: true.
Keep the XA4251 fix (RegisterNativeMembers stays private, so the
[JniAddNativeMethodRegistrationAttribute] is stripped from the reference assembly)
and restore TryRegisterBuiltInNativeMembers by extracting the registration logic
into a new attribute-free internal helper, JavaProxyObject.AddBuiltInRegistrations.
Both the private attributed RegisterNativeMembers (reflection path) and
TryRegisterBuiltInNativeMembers (NativeAOT path) call it. Restore the
PublicAPI.Unshipped.txt entry as well.
Verified: Java.Interop.dll builds clean; the reference assembly contains no method
carrying [JniAddNativeMethodRegistration]; the sample's call to
TryRegisterBuiltInNativeMembers resolves (no CS0103).
### Keep built-in registration in the samples via ReflectionJniTypeManager
Replaces the production TryRegisterBuiltInNativeMembers helper with a
samples-only solution, restoring the pre-#1441 behavior for the NativeAOT
type managers.
Background: making JavaProxyObject.RegisterNativeMembers private (the XA4251
fix) is sufficient for the default Android runtime, which discovers and invokes
it via reflection (FindAndCallRegisterMethod -> GetRuntimeMethods(), which
returns private methods). The only thing that needed JavaProxyObject's
registration via a non-reflection path was the two NativeAOT samples, which
#1441 had rewritten to derive from the reflection-free JniRuntime.JniTypeManager
base. That is why #1441 introduced TryRegisterBuiltInNativeMembers.
Instead of carrying that helper in production, have the sample type managers
derive from JniRuntime.ReflectionJniTypeManager again - exactly what the base
JniRuntime.JniTypeManager provided before #1441. Built-in types such as
JavaProxyObject/JavaProxyThrowable are then registered automatically via
reflection, and the samples no longer need any custom registration code.
Changes:
- JavaProxyObject.RegisterNativeMembers: internal -> private. The marshalers
stay (the default reflection runtime registers them by reflecting over this
type); only the visibility changes, which strips the attribute from the
reference assembly and fixes the dotnet/android XA4251 scanner failure.
- Delete JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers (+ its
PublicAPI.Unshipped.txt entry); no longer needed.
- Hello-NativeAOTFromJNI / Hello-NativeAOTFromAndroid: derive from
JniRuntime.ReflectionJniTypeManager and drop the reflection-free overrides and
hand-written JavaProxyObject registration. ReflectionJniTypeManager is
[RequiresDynamicCode]/[RequiresUnreferencedCode], so the constructor suppresses
IL2026/IL3050 with [UnconditionalSuppressMessage]. A #pragma is insufficient
here: it silences the Roslyn analyzer but not the ILLink/ILC publish passes,
which only honor the attribute (verified with a trim-publish of the real type
manager against Java.Interop.dll).
Net: production loses code (1-line visibility change + deletions) and the samples
shrink substantially.
### Drop verbose explanatory comments from the NativeAOT sample type managers
The [UnconditionalSuppressMessage] Justification strings already convey the
necessary context.
### Fix NativeAOT sample type resolution: override GetTypeForSimpleReference
Build 1465402's "run Hello-NativeAOTFromJNI" step failed at runtime:
System.NotSupportedException: Could not find System.Type corresponding to
Java type JniTypeSignature(TypeName=example/ManagedType ...)
at Java.Interop.ManagedPeer.RegisterNativeMembers(...)
at example.ManagedType.<clinit>(ManagedType.java:15)
Root cause: after #1441, JniRuntime.JniTypeManager.GetType() dispatches through
GetTypeForSimpleReference (singular), not GetTypesForSimpleReference (plural).
The previous sample rewrite only overrode the plural method, so app types like
example/ManagedType were never resolved (the base ReflectionJniTypeManager's
GetTypeForSimpleReference only knows built-in types), and ManagedPeer
registration threw before it could register the type.
Fix: override GetTypeForSimpleReference (singular) in both NativeAOT sample type
managers to resolve the sample's own managed types, falling back to the base for
built-ins. Registration and the reverse Type->JNI mapping continue to be handled
by the reflection base (the pre-#1441 behavior). The override carries the same
[return: DynamicallyAccessedMembers(...)] as the base to satisfy IL2093.
### Address review: drop unreachable null guard in FromAndroid sample
`typeMappings` is assigned in its field initializer and never set to null, so the
`if (typeMappings == null) yield break;` guard in CreateSimpleReferencesEnumerator
was unreachable dead code. Remove it.
### Document why the NativeAOT sample trim/AOT suppressions are acceptable
Add a class-level comment to both NativeAOT sample type managers explaining the
rationale for the [UnconditionalSuppressMessage] IL2026/IL3050 suppressions:
- These are *samples*, not product code. .NET for Android (what we ship) does not
pair ReflectionJniTypeManager with NativeAOT, so it isn't worth the effort to make
these samples fully trim/AOT-clean right now.
- The reflection paths were always trim/AOT-unsafe. Before #1441 the equivalent
suppressions lived inside JniTypeManager itself (justified "NotUsedInAndroid");
#1441 simply moved that responsibility to callers via
[RequiresDynamicCode]/[RequiresUnreferencedCode].
### Reword NativeAOT sample suppression justifications to describe why it's safe
The previous IL2026/IL3050 justifications ("does not require unreferenced code /
runtime code generation") read as the opposite of reality, since
ReflectionJniTypeManager is exactly [RequiresUnreferencedCode]/[RequiresDynamicCode].
Reword to describe why the suppression is correct for this sample:
- IL2026: the assembly is rooted via TrimmerRootAssembly and the reflected
registration members are preserved by the [DynamicallyAccessedMembers]
annotations on the RegisterNativeMembers(Type) -> FindAndCallRegisterMethod path.
- IL3050: registration uses CreateDelegate on compile-time-known static methods
(no MakeGenericType / expression compilation), so no runtime codegen is required.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers pushed a commit to dotnet/android that referenced this pull request Jun 16, 2026
…mbing (#11667)
The binding generator used to emit a `Java.Interop.__TypeRegistrations`
class whose `RegisterPackages ()` populated `TypeManager.packageLookup`.
The only reader of that dictionary -- the `TypeRegistrationFallback` path
in `GetJavaToManagedTypeCore` -- was removed in #9471, so the dictionary
has been write-only (and, since nothing generated calls it, effectively
unused) ever since. Type resolution now goes through the native /
trimmable type map.
This bumps external/Java.Interop to the generator change that stops
emitting the dead class (dotnet/java-interop#1470) and removes the
now-dead runtime plumbing:
* Delete the `packageLookup` field and `LazyInitPackageLookup ()`.
* `TypeManager.RegisterPackage`/`RegisterPackages` are shipped public
API, so they are kept as validated no-op stubs rather than removed.
Fixes#11663
### [Mono.Android] Throw NotSupportedException with an explanatory message
`TypeManager.RegisterPackage`/`RegisterPackages` are retired shipped public
APIs whose registration mechanism no longer exists. Rather than silently
doing nothing, they now throw `NotSupportedException` carrying an explanation
of why the call is unsupported and what replaced it ("Java package type
registration is no longer supported. Java-to-managed type resolution now goes
through the native and trimmable type maps."), so a caller that still invokes
them gets a clear, actionable reason instead of a bare exception.
### [Mono.Android] Don't bump external/Java.Interop in this PR
Revert the external/Java.Interop submodule bump. Pinning an unmerged
java-interop PR-branch commit is fragile, and advancing to java-interop
main HEAD would drag in unrelated changes (e.g. dotnet/java-interop#1441)
that need their own coordination. The generator-side removal lives in
dotnet/java-interop#1470 and will reach Mono.Android through the normal
submodule bump once it merges; this PR keeps only the independent runtime
cleanup of the now-dead `packageLookup` plumbing.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers added a commit to dotnet/android that referenced this pull request Jun 16, 2026
Bumps [external/Java.Interop](https://github.com/dotnet/java-interop) from `b881d21` to [`6820a9cb`](dotnet/java-interop@6820a9c).
## Java.Interop changes pulled in
- [`6820a9c`](dotnet/java-interop@6820a9c) [Java.Interop] Make JavaProxyObject.RegisterNativeMembers private again ([#1468](dotnet/java-interop#1468))
- [`a2159d7`](dotnet/java-interop@a2159d70) Remove NuGet dependency grouping from Dependabot config ([#1459](dotnet/java-interop#1459))
- [`a0ca676`](dotnet/java-interop@a0ca6760) [ci] Update agentic workflows to claude-opus-4.8 ([#1461](dotnet/java-interop#1461))
- [`6d8aef7`](dotnet/java-interop@6d8aef7b) Bump to dotnet/android-tools@1fb68b6 ([#1457](dotnet/java-interop#1457))
- [`cfca8ad`](dotnet/java-interop@cfca8ad) Upgrade gh-aw to v0.79.6 and recompile workflows ([#1460](dotnet/java-interop#1460))
- [`0104236`](dotnet/java-interop@0104236e) [JniValueManager] Make TryConstructPeer virtual ([#1456](dotnet/java-interop#1456))
- [`d7dbad5`](dotnet/java-interop@d7dbad5) Remove Java.Interop.Dynamic ([#1449](dotnet/java-interop#1449))
- [`0244c10`](dotnet/java-interop@0244c10) Split reflection-based JniTypeManager and JniValueManager behavior ([#1441](dotnet/java-interop#1441))
- [`9ca7064`](dotnet/java-interop@9ca7064) [Java.Interop] Remove legacy `NET` preprocessor paths ([#1451](dotnet/java-interop#1451))
- [`cd2fc12`](dotnet/java-interop@cd2fc12) Remove Java.Interop.GenericMarshaler ([#1450](dotnet/java-interop#1450))
See full diff in [compare view](dotnet/java-interop@b881d21...6820a9c).
## Changes in this PR (beyond the submodule bump)
The Java.Interop bump — particularly [#1441](dotnet/java-interop#1441) (split reflection-based `JniTypeManager` and `JniValueManager`) and [#1449](dotnet/java-interop#1449) (removal of `Java.Interop.Dynamic`) — required a number of follow-on changes in `dotnet/android`:
### Runtime / `Mono.Android`
- **Adapt Android runtime to Java.Interop reflection managers** — switch the `JavaInteropTypeManager` / `JavaInteropValueManager` hierarchy to the new reflection-based base types and update type/value manager construction to match.
- **Change base type of value managers and type managers** so the runtime composes correctly with the new Java.Interop split.
- **Suppress reflection manager trim warnings only** — narrow the existing trim suppressions so they apply to the reflection-only managers and don't hide other warnings.
- **Suppress IL2068 on `ManagedTypeManager.GetTypeForSimpleReference`** — annotate the entry point that legitimately requires reflection so trim analysis is satisfied.
### Tests / apkdescs
- **Regenerate `BuildReleaseArm64SimpleDotNet.MonoVM.apkdesc`** to track size changes from the Java.Interop bump.
- **Regenerate Simple `BuildReleaseArm64` apkdescs** (`MonoVM` + `NativeAOT`) from a `Release` SDK so they match what CI produces.
- **Update NativeAOT warning expectations** — `Mono.Android` now produces fewer trim/AOT warnings, so loosen the strict `4 Warning(s)` checks in `SupportedOSPlatformVersion` and `CheckSignApk` to `AssertHasAtMostWarnings (2)` (new helper in `AssertionExtensions`) and drop the `BuildHasTrimmerWarnings` data counts (`3 → 2`, `4 → 3`) accordingly.
### Merges
- Two merges from `origin/main` to keep the branch current and resolve a conflict in `Microsoft.Android.Sdk.TypeMap.Trimmable.targets`.
Co-authored-by: Simon Rozsival <simon@rozsival.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Jonathan Peppers <jonathan.peppers@microsoft.com>
jonathanpeppers pushed a commit that referenced this pull request Jun 17, 2026
… "trimmable type map" (#1454)
Follow-up to #1441.
Prerequisite for dotnet/android#11617.
This keeps the Java.Interop changes focused on the small base hook dotnet/android needs for the trimmable type-map integration. The reflection value manager continues to use value marshalers internally, while Android's generated/trimmable value manager can provide the only production `JavaObjectArray<T>` element-assignment object-reference path without implementing `GetValueMarshaler*()`.
## Changes
- Add minimal `JniValueManager` object-reference API for `JavaObjectArray<T>.SetElementAt()`:
- `CreateLocalObjectReferenceArgument(Type type, object? value)` returns an owned local `JniObjectReference` for element assignment. Callers must dispose the returned reference.
- Make the matching core method abstract so non-reflection value managers can implement this path directly.
- Keep value marshalers as a `ReflectionJniValueManager` implementation detail: reflection creates marshaler state, copies out an independent local reference, then destroys the state immediately.
- Update `JavaObjectArray<T>` to call the value manager directly instead of calling `GetValueMarshaler<T>()` in production paths.
- Simplify `JavaObjectArray<T>.Clear()` to set array slots to Java null directly; it no longer needs value-manager or value-marshaler state.
- Remove the earlier exposed proxy/peerable marshaler accessors, broad/generic state overloads, default-value state API, destroy-state API, and `ParameterAttributes synchronize` from this value-manager object-reference path.
- Keep ManagedPeer-dependent tests categorized as unsupported for the Android trimmable configuration rather than carrying Android-specific Java fixture workarounds in this PR.
- Include the small type-manager/test cleanups needed by the dotnet/android integration branch.
## Non-goals
- This PR does not make value marshalers public trimmable API.
- This PR does not require trimmable Android value managers to implement or use `GetValueMarshaler*()`.
- This PR does not remove or replace ManagedPeer-dependent Java.Interop test fixtures.
## Validation
- `dotnet build external/Java.Interop/src/Java.Interop/Java.Interop.csproj -p:Configuration=Debug -m:1 -nodeReuse:false --no-restore -v:minimal`
- From the dotnet/android integration branch:
- `dotnet test tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj -v minimal --no-restore` (`562` passed)
- `dotnet build src/Mono.Android/Mono.Android.csproj -p:Configuration=Debug -p:AndroidSdkDirectory=/Users/simonrozsival/android-toolchain/sdk -m:1 -nodeReuse:false --no-restore -v:minimal` compiled `Mono.Android.Runtime.dll`; the remaining local failure is Android SDK provisioning (`extras/android/m2repository.staging` and `docs.staging` missing), not C# or trim-analyzer errors.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers added a commit to dotnet/android that referenced this pull request Jun 30, 2026
The ReflectionJniTypeManager [RequiresDynamicCode] attribute predates this submodule bump (added in dotnet/java-interop#1441), so the ManagedTypeManager..ctor() IL3050 is already part of the BuildHasNoWarnings test's baseline of 4 warnings. Only the RegisterNativeMembers warning is new from this PR; suppressing the ctor warning too would drop the count to 2 and fail the test.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

ready-to-reviewThis PR is ready to review/merge, thanks!

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@simonrozsival@jonathanpeppers
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
This repository was archived by the owner on Aug 27, 2026. It is now read-only.

Split reflection-based JniTypeManager and JniValueManager behavior - #1441

Merged
jonathanpeppers merged 30 commits into
mainfrom
dev/simonrozsival/fix-aot-warnings-jni-type-manager
Jun 9, 2026
Merged

Split reflection-based JniTypeManager and JniValueManager behavior#1441
jonathanpeppers merged 30 commits into
mainfrom
dev/simonrozsival/fix-aot-warnings-jni-type-manager

Conversation

@simonrozsival

@simonrozsivalsimonrozsival commented Jun 4, 2026

Copy link
Copy Markdown
Member

Splits the reflection-backed paths out of JniRuntime.JniTypeManager and JniRuntime.JniValueManager so callers that don't need reflection (e.g. NativeAOT) can opt out, while keeping the public API surface as compatible as possible for callers migrating from .NET 10.

What changed

  • JniRuntime.ReflectionJniTypeManager (new): all reflection-based type lookup, signature discovery, and native-method registration moves here. Annotated with [RequiresDynamicCode] / [RequiresUnreferencedCode].
  • JniRuntime.ReflectionJniValueManager (new): all reflection-based peer construction, value marshaling, and activation moves here.
  • JniRuntime.JniTypeManager stays a non-abstract base class. Methods that previously had reflection defaults are now virtual no-ops (or return null/empty). Subclasses that want the old default behavior should derive from ReflectionJniTypeManager.
  • JniRuntime.JniValueManager stays abstract (it already was). A few previously-virtual methods (CreatePeer, GetValueMarshalerCore(Type)) become abstract since their reflection-based defaults moved out — public consumers (dotnet/android's value managers) already override these.
  • In-tree consumers updated: JreTypeManager, ManagedValueManager, MonoRuntimeValueManager now derive from the new Reflection* bases. The NativeAOT samples keep deriving from the abstract bases with explicit type mappings.

Public API impact

PublicAPI.Shipped.txt is unchanged. All breaking changes are tracked in PublicAPI.Unshipped.txt with *REMOVED* markers:

  • JniTypeManager.TryRegisterNativeMembers (both overloads) — moved to ReflectionJniTypeManager as protected helpers.
  • JniValueManager.CreatePeer and GetValueMarshalerCore(Type)virtualabstract.

A non-abstract JniTypeManager keeps new JniRuntime.JniTypeManager() and direct subclasses compiling without changes.

Validation

  • CI is green.
  • Local dotnet build Java.Interop.sln passes (PublicApiAnalyzer enforces tracking is consistent).

@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/fix-aot-warnings-jni-type-manager branch from 5ff4c17 to 6d72140CompareJune 4, 2026 14:24
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/fix-aot-warnings-jni-type-manager branch from 6d72140 to 58f0dbdCompareJune 4, 2026 14:30
@simonrozsival

Copy link
Copy Markdown
MemberAuthor

/azp run

@azure-pipelines

Copy link
Copy Markdown
No pipelines are associated with this pull request.

@simonrozsival
simonrozsival marked this pull request as ready for review June 8, 2026 11:55
CopilotAI review requested due to automatic review settings June 8, 2026 11:55

CopilotAI 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.

Pull request overview

This draft PR refactors JniRuntime.JniTypeManager into an abstract base and moves the default reflection-based behavior into a new JniRuntime.DynamicJniTypeManager, with corresponding updates across tests, JRE runtime helpers, samples, and public API baselines.

Changes:

  • Introduces JniRuntime.DynamicJniTypeManager and makes JniRuntime.JniTypeManager abstract, shifting default/reflection-backed logic into the new derived type.
  • Updates test and JRE type managers to inherit from DynamicJniTypeManager, and adjusts tests for new behaviors (e.g., generic invoker handling, GetType array behavior).
  • Updates trimmer/AOT annotations and PublicAPI baselines to reflect the split and new abstract members.
Show a summary per file
FileDescription
tests/Java.Interop-Tests/Java.Interop/JniTypeManagerTests.csUpdates expectations around GetType behavior (notably array signatures and generic holder mapping).
tests/Java.Interop-Tests/Java.Interop/JniRuntimeTest.csSwitches proxy manager to DynamicJniTypeManager and adds trim suppression for test-only reflection use.
tests/Java.Interop-Tests/Java.Interop/JniRuntime.JniTypeManagerTests.csAdds a generic invoker negative test and updates test manager base type.
tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.csUpdates fixture type manager to DynamicJniTypeManager and refines type lookup with trimming annotations.
src/Java.Runtime.Environment/Java.Interop/JreTypeManager.csMoves to DynamicJniTypeManager and adds a DAM-annotated GetTypeForSimpleReference override.
src/Java.Interop/PublicAPI.Unshipped.txtRecords newly introduced/reshaped APIs (new type, newly-abstract members, overrides).
src/Java.Interop/PublicAPI.Shipped.txtUpdates shipped API surface to reflect newly-abstract members and removed methods from JniTypeManager.
src/Java.Interop/Java.Interop/JniValueMarshaler.csSimplifies expression-based marshaler instantiation and adjusts trimming annotations.
src/Java.Interop/Java.Interop/JniRuntime.JniValueManager.csSwitches peer-type selection to GetReflectionConstructibleTypes() and refactors uninitialized peer creation.
src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.csMakes JniTypeManager abstract and factors previously-inlined logic into abstract core methods.
src/Java.Interop/Java.Interop/JniRuntime.DynamicJniTypeManager.csAdds the new reflection-backed type manager implementation and related registration behavior.
src/Java.Interop/Java.Interop/JniRuntime.csRemoves the default new JniTypeManager() fallback; requires an explicit type manager in creation options.
src/Java.Interop/Java.Interop/JavaPrimitiveArrays.ttRetargets primitive array helper generation to DynamicJniTypeManager.
src/Java.Interop/Java.Interop/JavaPrimitiveArrays.csUpdates the generated primitive array helper container type to DynamicJniTypeManager.
src/Java.Interop/GlobalSuppressions.csAdds CA1034 suppression for the new nested DynamicJniTypeManager type.
samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.csUpdates sample type manager implementation to new hierarchy and adds explicit native member registration.
samples/Hello-NativeAOTFromJNI/ManagedType.csChanges the native registration hook to internal for sample-driven registration.
samples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.csUpdates sample type manager implementation to new hierarchy and implements abstract members.

Copilot's findings

  • Files reviewed: 18/18 changed files
  • Comments generated: 3

Comment threadsamples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs Outdated
Comment threadsamples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs Outdated
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival
simonrozsival marked this pull request as draft June 8, 2026 14:24
simonrozsivaland others added 9 commits June 8, 2026 16:32
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>
Map Java.Interop's internal proxy peer types in the base type manager so custom AOT-safe managers do not need reflection-based DynamicJniTypeManager behavior for JavaProxyThrowable/JavaProxyObject.
Register built-in proxy native members by JNI type name before asking custom managers for a managed Type, avoiding DAM-annotated lookups of internal Exception/Delegate-derived types.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Let the base JniTypeManager resolve primitive JNI keyword signatures without requiring custom AOT-safe managers to duplicate the dynamic type manager's primitive mappings.
Use that base lookup as a fallback when matching Java-to-managed constructor signatures so signatures such as (I)V resolve to System.Int32.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival
simonrozsival marked this pull request as ready for review June 9, 2026 07:23
simonrozsivaland others added 2 commits June 9, 2026 09:31
Rename DynamicJniTypeManager to ReflectionJniTypeManager so the type name describes the reflection-based behavior that is incompatible with Native AOT.
Update the matching source file name, API baseline, JRE type manager inheritance, generated primitive-array partial, and tests.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsivalsimonrozsival changed the title Split reflection-based JniTypeManager behaviorSplit reflection-based JniTypeManager and JniValueManager behaviorJun 9, 2026

CopilotAI commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved the merge conflicts in commit Resolve merge conflicts: remove #if NET guards, keep abstract methods in base JniTypeManager.

The two new commits in main since the last merge (Remove Java.Interop.GenericMarshaler (#1450) and Remove legacy NET preprocessor paths (#1451)) conflicted with our branch in three files:

  • JniRuntime.JniTypeManager.cs (two conflicts): Kept the abstract method declarations (GetInvokerTypeCore, GetStaticMethodFallbackTypesCore, RegisterNativeMembers) from our branch while adopting the #if NET guard removals from main. The implementation helpers (TryRegisterNativeMembers, TryLoadJniMarshalMethods, etc.) remain in DynamicJniTypeManager where our PR moved them.
  • ManagedPeer.cs: Removed the #if NET guard that was wrapping the ReadOnlySpan<char> methods block.
  • JreRuntime.cs: Kept our branch's structure (TypeManager default set in the [RequiresDynamicCode]-annotated CreateJreVM, not in CreateJreVMCore), discarding main's addition of builder.TypeManager ??= new JreTypeManager(...) to CreateJreVMCore which would have bypassed the AOT warning.

Also removed the remaining #if NET guards from JniRuntime.ReflectionJniTypeManager.cs and JreRuntime.cs that were in our branch but no longer needed after the main merge.

@simonrozsival

Copy link
Copy Markdown
MemberAuthor

/review

@github-actions

github-actionsBot commented Jun 9, 2026

Copy link
Copy Markdown

Java.Interop PR Reviewer completed successfully!

@github-actionsgithub-actionsBot 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.

✅ LGTM — Clean architectural split

Summary: This PR cleanly separates the reflection-based JniTypeManager / JniValueManager behavior into dedicated ReflectionJniTypeManager and ReflectionJniValueManager subclasses, making the base classes abstract and properly annotating the reflection-based paths with [RequiresDynamicCode] / [RequiresUnreferencedCode]. The NativeAOT samples are updated to implement the abstract base directly, and tests/downstream types are updated accordingly.

What looks good:

  • The split is well-structured — base classes contain only the contract and built-in type handling; all reflection goes into the Reflection* subclasses.
  • [RequiresDynamicCode] and [RequiresUnreferencedCode] annotations are applied consistently throughout the chain (samples, tests, JRE runtime types).
  • Public API baselines are updated correctly.
  • TryRegisterBuiltInNativeMembers is a nice extraction that NativeAOT consumers can call without needing the full reflection machinery.
  • The JreRuntimeOptions.CreateJreVM(JniTypeManager) overload is a good ergonomic addition.

Issues by severity:

SeverityCount
⚠️ Warning1
💡 Suggestion3

Notes:

  • CI only shows license/cla — no build/test results visible yet. The PR is not mergeable until CI validates the full build.
  • The large abstract surface area for NativeAOT consumers (~12 methods) may be worth revisiting — see inline suggestion about making unused extension points virtual with default null returns.

Generated by Java.Interop PR Reviewer for issue #1441 · ● 14.3M

Comment threadsamples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs
Comment threadsamples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs
Comment threadsrc/Java.Interop/Java.Interop/JniRuntime.ReflectionJniTypeManager.cs Outdated
simonrozsivaland others added 2 commits June 9, 2026 22:20
Map JNI primitive and boxed primitive signatures in the NativeAOT sample type managers so constructor signature matching can resolve primitive parameters such as I to System.Int32.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
To minimize migration pain for callers moving from .NET 10 to .NET 11,
revert JniTypeManager from `abstract` back to a regular class. Methods
that were turned into `abstract` overrides now have `virtual` defaults
so existing subclasses keep compiling:
* Type lookup methods return null/empty defaults.
* RegisterNativeMembers is a no-op by default; reflection-based
behavior remains in ReflectionJniTypeManager (override to opt-in).
Restore PublicAPI.Shipped.txt to its main-branch state (per the
`Microsoft.CodeAnalysis.PublicApiAnalyzers` convention, never edit
shipped entries in place) and track every breaking change in
PublicAPI.Unshipped.txt:
* 5 new JniTypeManager.* APIs are now `virtual` (not `abstract`).
* `*REMOVED*` entries for the two `JniTypeManager.TryRegisterNativeMembers`
overloads that moved to `ReflectionJniTypeManager`.
* `*REMOVED*virtual` + `abstract` pairs for the two virtual->abstract
transitions on `JniValueManager` (CreatePeer, GetValueMarshalerCore(Type)).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jonathanpeppers
jonathanpeppers merged commit 0244c10 into mainJun 9, 2026
2 checks passed
@jonathanpeppers
jonathanpeppers deleted the dev/simonrozsival/fix-aot-warnings-jni-type-manager branch June 9, 2026 23:17
jonathanpeppers pushed a commit that referenced this pull request Jun 12, 2026
Follow-up to #1441 We need this method virtual so that `JavaMarshalValueManager` can override it and try to invoke the `XAConstructorSignature`. The method was previously virtual but as an oversight I removed it in #1441:
- https://github.com/dotnet/java-interop/pull/1441/changes#diff-8ead12d157bc7712affd3d920bb93cddbff3885147fe6297957a44cc522d5430R7
- https://github.com/dotnet/java-interop/pull/1441/changes#diff-b92884de5db7f82df63b483b8319360cd9bfdac3cd97222eb9aff7292a01a07bL449-L467
I noticed we're not disposing the `reference` correctly which might cause a ref leak. In the Mono `AndroidValueManager` we never called into the base method so this would not be observed on mono previously (https://github.com/dotnet/android/blob/main/src/Mono.Android/Java.Interop/TypeManager.cs#L416-L423).
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
simonrozsival added a commit that referenced this pull request Jun 15, 2026
#1441 added `JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers ()`
-- a reflection-free direct call to `JavaProxyObject.RegisterNativeMembers`
-- and bumped that method to `internal` so the call would compile.
That method is dead code: nothing in Java.Interop (or dotnet/android) ever
calls it, and the base `JniTypeManager.RegisterNativeMembers` is a no-op.
`JavaProxyObject`'s native members are -- and were before #1441 --
registered purely via reflection in `ReflectionJniTypeManager`
(`FindAndCallRegisterMethod` discovering the
`[JniAddNativeMethodRegistrationAttribute]`-annotated method), which finds
private methods just fine via `GetRuntimeMethods ()`.
Remove the unused `TryRegisterBuiltInNativeMembers` and restore
`RegisterNativeMembers` to `private` (its state before #1441). Making it
private again also strips the attribute-bearing method from the reference
assembly, which fixes dotnet/android's trimmable typemap scanner falsely
rejecting the built-in `JavaProxyObject` with XA4251 (dotnet/android#11622).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit that referenced this pull request Jun 15, 2026
Replaces the production TryRegisterBuiltInNativeMembers helper with a
samples-only solution, restoring the pre-#1441 behavior for the NativeAOT
type managers.
Background: making JavaProxyObject.RegisterNativeMembers private (the XA4251
fix) is sufficient for the default Android runtime, which discovers and invokes
it via reflection (FindAndCallRegisterMethod -> GetRuntimeMethods(), which
returns private methods). The only thing that needed JavaProxyObject's
registration via a non-reflection path was the two NativeAOT samples, which
#1441 had rewritten to derive from the reflection-free JniRuntime.JniTypeManager
base. That is why #1441 introduced TryRegisterBuiltInNativeMembers.
Instead of carrying that helper in production, have the sample type managers
derive from JniRuntime.ReflectionJniTypeManager again - exactly what the base
JniRuntime.JniTypeManager provided before #1441. Built-in types such as
JavaProxyObject/JavaProxyThrowable are then registered automatically via
reflection, and the samples no longer need any custom registration code.
Changes:
- JavaProxyObject.RegisterNativeMembers: internal -> private. The marshalers
stay (the default reflection runtime registers them by reflecting over this
type); only the visibility changes, which strips the attribute from the
reference assembly and fixes the dotnet/android XA4251 scanner failure.
- Delete JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers (+ its
PublicAPI.Unshipped.txt entry); no longer needed.
- Hello-NativeAOTFromJNI / Hello-NativeAOTFromAndroid: derive from
JniRuntime.ReflectionJniTypeManager and drop the reflection-free overrides and
hand-written JavaProxyObject registration. ReflectionJniTypeManager is
[RequiresDynamicCode]/[RequiresUnreferencedCode], so the constructor suppresses
IL2026/IL3050 with [UnconditionalSuppressMessage]. A #pragma is insufficient
here: it silences the Roslyn analyzer but not the ILLink/ILC publish passes,
which only honor the attribute (verified with a trim-publish of the real type
manager against Java.Interop.dll).
Net: production loses code (1-line visibility change + deletions) and the samples
shrink substantially.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit that referenced this pull request Jun 16, 2026
Build 1465402's "run Hello-NativeAOTFromJNI" step failed at runtime:
System.NotSupportedException: Could not find System.Type corresponding to
Java type JniTypeSignature(TypeName=example/ManagedType ...)
at Java.Interop.ManagedPeer.RegisterNativeMembers(...)
at example.ManagedType.<clinit>(ManagedType.java:15)
Root cause: after #1441, JniRuntime.JniTypeManager.GetType() dispatches through
GetTypeForSimpleReference (singular), not GetTypesForSimpleReference (plural).
The previous sample rewrite only overrode the plural method, so app types like
example/ManagedType were never resolved (the base ReflectionJniTypeManager's
GetTypeForSimpleReference only knows built-in types), and ManagedPeer
registration threw before it could register the type.
Fix: override GetTypeForSimpleReference (singular) in both NativeAOT sample type
managers to resolve the sample's own managed types, falling back to the base for
built-ins. Registration and the reverse Type->JNI mapping continue to be handled
by the reflection base (the pre-#1441 behavior). The override carries the same
[return: DynamicallyAccessedMembers(...)] as the base to satisfy IL2093.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit that referenced this pull request Jun 16, 2026
Add a class-level comment to both NativeAOT sample type managers explaining the
rationale for the [UnconditionalSuppressMessage] IL2026/IL3050 suppressions:
- These are *samples*, not product code. .NET for Android (what we ship) does not
pair ReflectionJniTypeManager with NativeAOT, so it isn't worth the effort to make
these samples fully trim/AOT-clean right now.
- The reflection paths were always trim/AOT-unsafe. Before #1441 the equivalent
suppressions lived inside JniTypeManager itself (justified "NotUsedInAndroid");
#1441 simply moved that responsibility to callers via
[RequiresDynamicCode]/[RequiresUnreferencedCode].
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsival added a commit to dotnet/android that referenced this pull request Jun 16, 2026
Revert the external/Java.Interop submodule bump. Pinning an unmerged
java-interop PR-branch commit is fragile, and advancing to java-interop
main HEAD would drag in unrelated changes (e.g. dotnet/java-interop#1441)
that need their own coordination. The generator-side removal lives in
dotnet/java-interop#1470 and will reach Mono.Android through the normal
submodule bump once it merges; this PR keeps only the independent runtime
cleanup of the now-dead `packageLookup` plumbing.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers pushed a commit that referenced this pull request Jun 16, 2026
…in (#1468)
#1441 changed `JavaProxyObject.RegisterNativeMembers` from `private` to
`internal` so the new reflection-free
`JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers ()` could call
it directly.
A side effect is that the `[JniAddNativeMethodRegistrationAttribute]`-
annotated method now appears in Java.Interop's *reference* assembly.
dotnet/android's trimmable typemap scanner reads reference assemblies and
rejects any type carrying that attribute (XA4251), so every trimmable
typemap build now fails on the built-in `JavaProxyObject`
(see dotnet/android#11622).
Restore the method to `private` (so it is stripped from the reference
assembly) and expose a small attribute-free `internal` entry point,
`RegisterBuiltInNativeMembers (JniType)`, for the built-in registration
path to call. Runtime behavior is unchanged.
### [Java.Interop] Restore pure-reflection registration for JavaProxyObject
#1441 added `JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers ()`
-- a reflection-free direct call to `JavaProxyObject.RegisterNativeMembers`
-- and bumped that method to `internal` so the call would compile.
That method is dead code: nothing in Java.Interop (or dotnet/android) ever
calls it, and the base `JniTypeManager.RegisterNativeMembers` is a no-op.
`JavaProxyObject`'s native members are -- and were before #1441 --
registered purely via reflection in `ReflectionJniTypeManager`
(`FindAndCallRegisterMethod` discovering the
`[JniAddNativeMethodRegistrationAttribute]`-annotated method), which finds
private methods just fine via `GetRuntimeMethods ()`.
Remove the unused `TryRegisterBuiltInNativeMembers` and restore
`RegisterNativeMembers` to `private` (its state before #1441). Making it
private again also strips the attribute-bearing method from the reference
assembly, which fixes dotnet/android's trimmable typemap scanner falsely
rejecting the built-in `JavaProxyObject` with XA4251 (dotnet/android#11622).
### Address review: restore TryRegisterBuiltInNativeMembers (not dead code)
The previous commit deleted JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers
claiming it was unused. That was wrong: it has two live callers in the NativeAOT
samples (samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs and
samples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs), both JniTypeManager
subclasses. Deleting it broke them (CS0103) and removed the only path that
registers JavaProxyObject's equals/hashCode/toString natives for NativeAOT type
managers (which don't use the reflection-based FindAndCallRegisterMethod path).
The breakage was hidden in CI only because the NativeAOT sample steps use
continueOnError: true.
Keep the XA4251 fix (RegisterNativeMembers stays private, so the
[JniAddNativeMethodRegistrationAttribute] is stripped from the reference assembly)
and restore TryRegisterBuiltInNativeMembers by extracting the registration logic
into a new attribute-free internal helper, JavaProxyObject.AddBuiltInRegistrations.
Both the private attributed RegisterNativeMembers (reflection path) and
TryRegisterBuiltInNativeMembers (NativeAOT path) call it. Restore the
PublicAPI.Unshipped.txt entry as well.
Verified: Java.Interop.dll builds clean; the reference assembly contains no method
carrying [JniAddNativeMethodRegistration]; the sample's call to
TryRegisterBuiltInNativeMembers resolves (no CS0103).
### Keep built-in registration in the samples via ReflectionJniTypeManager
Replaces the production TryRegisterBuiltInNativeMembers helper with a
samples-only solution, restoring the pre-#1441 behavior for the NativeAOT
type managers.
Background: making JavaProxyObject.RegisterNativeMembers private (the XA4251
fix) is sufficient for the default Android runtime, which discovers and invokes
it via reflection (FindAndCallRegisterMethod -> GetRuntimeMethods(), which
returns private methods). The only thing that needed JavaProxyObject's
registration via a non-reflection path was the two NativeAOT samples, which
#1441 had rewritten to derive from the reflection-free JniRuntime.JniTypeManager
base. That is why #1441 introduced TryRegisterBuiltInNativeMembers.
Instead of carrying that helper in production, have the sample type managers
derive from JniRuntime.ReflectionJniTypeManager again - exactly what the base
JniRuntime.JniTypeManager provided before #1441. Built-in types such as
JavaProxyObject/JavaProxyThrowable are then registered automatically via
reflection, and the samples no longer need any custom registration code.
Changes:
- JavaProxyObject.RegisterNativeMembers: internal -> private. The marshalers
stay (the default reflection runtime registers them by reflecting over this
type); only the visibility changes, which strips the attribute from the
reference assembly and fixes the dotnet/android XA4251 scanner failure.
- Delete JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers (+ its
PublicAPI.Unshipped.txt entry); no longer needed.
- Hello-NativeAOTFromJNI / Hello-NativeAOTFromAndroid: derive from
JniRuntime.ReflectionJniTypeManager and drop the reflection-free overrides and
hand-written JavaProxyObject registration. ReflectionJniTypeManager is
[RequiresDynamicCode]/[RequiresUnreferencedCode], so the constructor suppresses
IL2026/IL3050 with [UnconditionalSuppressMessage]. A #pragma is insufficient
here: it silences the Roslyn analyzer but not the ILLink/ILC publish passes,
which only honor the attribute (verified with a trim-publish of the real type
manager against Java.Interop.dll).
Net: production loses code (1-line visibility change + deletions) and the samples
shrink substantially.
### Drop verbose explanatory comments from the NativeAOT sample type managers
The [UnconditionalSuppressMessage] Justification strings already convey the
necessary context.
### Fix NativeAOT sample type resolution: override GetTypeForSimpleReference
Build 1465402's "run Hello-NativeAOTFromJNI" step failed at runtime:
System.NotSupportedException: Could not find System.Type corresponding to
Java type JniTypeSignature(TypeName=example/ManagedType ...)
at Java.Interop.ManagedPeer.RegisterNativeMembers(...)
at example.ManagedType.<clinit>(ManagedType.java:15)
Root cause: after #1441, JniRuntime.JniTypeManager.GetType() dispatches through
GetTypeForSimpleReference (singular), not GetTypesForSimpleReference (plural).
The previous sample rewrite only overrode the plural method, so app types like
example/ManagedType were never resolved (the base ReflectionJniTypeManager's
GetTypeForSimpleReference only knows built-in types), and ManagedPeer
registration threw before it could register the type.
Fix: override GetTypeForSimpleReference (singular) in both NativeAOT sample type
managers to resolve the sample's own managed types, falling back to the base for
built-ins. Registration and the reverse Type->JNI mapping continue to be handled
by the reflection base (the pre-#1441 behavior). The override carries the same
[return: DynamicallyAccessedMembers(...)] as the base to satisfy IL2093.
### Address review: drop unreachable null guard in FromAndroid sample
`typeMappings` is assigned in its field initializer and never set to null, so the
`if (typeMappings == null) yield break;` guard in CreateSimpleReferencesEnumerator
was unreachable dead code. Remove it.
### Document why the NativeAOT sample trim/AOT suppressions are acceptable
Add a class-level comment to both NativeAOT sample type managers explaining the
rationale for the [UnconditionalSuppressMessage] IL2026/IL3050 suppressions:
- These are *samples*, not product code. .NET for Android (what we ship) does not
pair ReflectionJniTypeManager with NativeAOT, so it isn't worth the effort to make
these samples fully trim/AOT-clean right now.
- The reflection paths were always trim/AOT-unsafe. Before #1441 the equivalent
suppressions lived inside JniTypeManager itself (justified "NotUsedInAndroid");
#1441 simply moved that responsibility to callers via
[RequiresDynamicCode]/[RequiresUnreferencedCode].
### Reword NativeAOT sample suppression justifications to describe why it's safe
The previous IL2026/IL3050 justifications ("does not require unreferenced code /
runtime code generation") read as the opposite of reality, since
ReflectionJniTypeManager is exactly [RequiresUnreferencedCode]/[RequiresDynamicCode].
Reword to describe why the suppression is correct for this sample:
- IL2026: the assembly is rooted via TrimmerRootAssembly and the reflected
registration members are preserved by the [DynamicallyAccessedMembers]
annotations on the RegisterNativeMembers(Type) -> FindAndCallRegisterMethod path.
- IL3050: registration uses CreateDelegate on compile-time-known static methods
(no MakeGenericType / expression compilation), so no runtime codegen is required.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers pushed a commit to dotnet/android that referenced this pull request Jun 16, 2026
…mbing (#11667)
The binding generator used to emit a `Java.Interop.__TypeRegistrations`
class whose `RegisterPackages ()` populated `TypeManager.packageLookup`.
The only reader of that dictionary -- the `TypeRegistrationFallback` path
in `GetJavaToManagedTypeCore` -- was removed in #9471, so the dictionary
has been write-only (and, since nothing generated calls it, effectively
unused) ever since. Type resolution now goes through the native /
trimmable type map.
This bumps external/Java.Interop to the generator change that stops
emitting the dead class (dotnet/java-interop#1470) and removes the
now-dead runtime plumbing:
* Delete the `packageLookup` field and `LazyInitPackageLookup ()`.
* `TypeManager.RegisterPackage`/`RegisterPackages` are shipped public
API, so they are kept as validated no-op stubs rather than removed.
Fixes#11663
### [Mono.Android] Throw NotSupportedException with an explanatory message
`TypeManager.RegisterPackage`/`RegisterPackages` are retired shipped public
APIs whose registration mechanism no longer exists. Rather than silently
doing nothing, they now throw `NotSupportedException` carrying an explanation
of why the call is unsupported and what replaced it ("Java package type
registration is no longer supported. Java-to-managed type resolution now goes
through the native and trimmable type maps."), so a caller that still invokes
them gets a clear, actionable reason instead of a bare exception.
### [Mono.Android] Don't bump external/Java.Interop in this PR
Revert the external/Java.Interop submodule bump. Pinning an unmerged
java-interop PR-branch commit is fragile, and advancing to java-interop
main HEAD would drag in unrelated changes (e.g. dotnet/java-interop#1441)
that need their own coordination. The generator-side removal lives in
dotnet/java-interop#1470 and will reach Mono.Android through the normal
submodule bump once it merges; this PR keeps only the independent runtime
cleanup of the now-dead `packageLookup` plumbing.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers added a commit to dotnet/android that referenced this pull request Jun 16, 2026
Bumps [external/Java.Interop](https://github.com/dotnet/java-interop) from `b881d21` to [`6820a9cb`](dotnet/java-interop@6820a9c).
## Java.Interop changes pulled in
- [`6820a9c`](dotnet/java-interop@6820a9c) [Java.Interop] Make JavaProxyObject.RegisterNativeMembers private again ([#1468](dotnet/java-interop#1468))
- [`a2159d7`](dotnet/java-interop@a2159d70) Remove NuGet dependency grouping from Dependabot config ([#1459](dotnet/java-interop#1459))
- [`a0ca676`](dotnet/java-interop@a0ca6760) [ci] Update agentic workflows to claude-opus-4.8 ([#1461](dotnet/java-interop#1461))
- [`6d8aef7`](dotnet/java-interop@6d8aef7b) Bump to dotnet/android-tools@1fb68b6 ([#1457](dotnet/java-interop#1457))
- [`cfca8ad`](dotnet/java-interop@cfca8ad) Upgrade gh-aw to v0.79.6 and recompile workflows ([#1460](dotnet/java-interop#1460))
- [`0104236`](dotnet/java-interop@0104236e) [JniValueManager] Make TryConstructPeer virtual ([#1456](dotnet/java-interop#1456))
- [`d7dbad5`](dotnet/java-interop@d7dbad5) Remove Java.Interop.Dynamic ([#1449](dotnet/java-interop#1449))
- [`0244c10`](dotnet/java-interop@0244c10) Split reflection-based JniTypeManager and JniValueManager behavior ([#1441](dotnet/java-interop#1441))
- [`9ca7064`](dotnet/java-interop@9ca7064) [Java.Interop] Remove legacy `NET` preprocessor paths ([#1451](dotnet/java-interop#1451))
- [`cd2fc12`](dotnet/java-interop@cd2fc12) Remove Java.Interop.GenericMarshaler ([#1450](dotnet/java-interop#1450))
See full diff in [compare view](dotnet/java-interop@b881d21...6820a9c).
## Changes in this PR (beyond the submodule bump)
The Java.Interop bump — particularly [#1441](dotnet/java-interop#1441) (split reflection-based `JniTypeManager` and `JniValueManager`) and [#1449](dotnet/java-interop#1449) (removal of `Java.Interop.Dynamic`) — required a number of follow-on changes in `dotnet/android`:
### Runtime / `Mono.Android`
- **Adapt Android runtime to Java.Interop reflection managers** — switch the `JavaInteropTypeManager` / `JavaInteropValueManager` hierarchy to the new reflection-based base types and update type/value manager construction to match.
- **Change base type of value managers and type managers** so the runtime composes correctly with the new Java.Interop split.
- **Suppress reflection manager trim warnings only** — narrow the existing trim suppressions so they apply to the reflection-only managers and don't hide other warnings.
- **Suppress IL2068 on `ManagedTypeManager.GetTypeForSimpleReference`** — annotate the entry point that legitimately requires reflection so trim analysis is satisfied.
### Tests / apkdescs
- **Regenerate `BuildReleaseArm64SimpleDotNet.MonoVM.apkdesc`** to track size changes from the Java.Interop bump.
- **Regenerate Simple `BuildReleaseArm64` apkdescs** (`MonoVM` + `NativeAOT`) from a `Release` SDK so they match what CI produces.
- **Update NativeAOT warning expectations** — `Mono.Android` now produces fewer trim/AOT warnings, so loosen the strict `4 Warning(s)` checks in `SupportedOSPlatformVersion` and `CheckSignApk` to `AssertHasAtMostWarnings (2)` (new helper in `AssertionExtensions`) and drop the `BuildHasTrimmerWarnings` data counts (`3 → 2`, `4 → 3`) accordingly.
### Merges
- Two merges from `origin/main` to keep the branch current and resolve a conflict in `Microsoft.Android.Sdk.TypeMap.Trimmable.targets`.
Co-authored-by: Simon Rozsival <simon@rozsival.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Jonathan Peppers <jonathan.peppers@microsoft.com>
jonathanpeppers pushed a commit that referenced this pull request Jun 17, 2026
… "trimmable type map" (#1454)
Follow-up to #1441.
Prerequisite for dotnet/android#11617.
This keeps the Java.Interop changes focused on the small base hook dotnet/android needs for the trimmable type-map integration. The reflection value manager continues to use value marshalers internally, while Android's generated/trimmable value manager can provide the only production `JavaObjectArray<T>` element-assignment object-reference path without implementing `GetValueMarshaler*()`.
## Changes
- Add minimal `JniValueManager` object-reference API for `JavaObjectArray<T>.SetElementAt()`:
- `CreateLocalObjectReferenceArgument(Type type, object? value)` returns an owned local `JniObjectReference` for element assignment. Callers must dispose the returned reference.
- Make the matching core method abstract so non-reflection value managers can implement this path directly.
- Keep value marshalers as a `ReflectionJniValueManager` implementation detail: reflection creates marshaler state, copies out an independent local reference, then destroys the state immediately.
- Update `JavaObjectArray<T>` to call the value manager directly instead of calling `GetValueMarshaler<T>()` in production paths.
- Simplify `JavaObjectArray<T>.Clear()` to set array slots to Java null directly; it no longer needs value-manager or value-marshaler state.
- Remove the earlier exposed proxy/peerable marshaler accessors, broad/generic state overloads, default-value state API, destroy-state API, and `ParameterAttributes synchronize` from this value-manager object-reference path.
- Keep ManagedPeer-dependent tests categorized as unsupported for the Android trimmable configuration rather than carrying Android-specific Java fixture workarounds in this PR.
- Include the small type-manager/test cleanups needed by the dotnet/android integration branch.
## Non-goals
- This PR does not make value marshalers public trimmable API.
- This PR does not require trimmable Android value managers to implement or use `GetValueMarshaler*()`.
- This PR does not remove or replace ManagedPeer-dependent Java.Interop test fixtures.
## Validation
- `dotnet build external/Java.Interop/src/Java.Interop/Java.Interop.csproj -p:Configuration=Debug -m:1 -nodeReuse:false --no-restore -v:minimal`
- From the dotnet/android integration branch:
- `dotnet test tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj -v minimal --no-restore` (`562` passed)
- `dotnet build src/Mono.Android/Mono.Android.csproj -p:Configuration=Debug -p:AndroidSdkDirectory=/Users/simonrozsival/android-toolchain/sdk -m:1 -nodeReuse:false --no-restore -v:minimal` compiled `Mono.Android.Runtime.dll`; the remaining local failure is Android SDK provisioning (`extras/android/m2repository.staging` and `docs.staging` missing), not C# or trim-analyzer errors.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers added a commit to dotnet/android that referenced this pull request Jun 30, 2026
The ReflectionJniTypeManager [RequiresDynamicCode] attribute predates this submodule bump (added in dotnet/java-interop#1441), so the ManagedTypeManager..ctor() IL3050 is already part of the BuildHasNoWarnings test's baseline of 4 warnings. Only the RegisterNativeMembers warning is new from this PR; suppressing the ctor warning too would drop the count to 2 and fail the test.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

ready-to-reviewThis PR is ready to review/merge, thanks!

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@simonrozsival@jonathanpeppers