[TrimmableTypeMap] SafeJavaCollectionFactory per-container construction#12159
[TrimmableTypeMap] SafeJavaCollectionFactory per-container construction#12159simonrozsival wants to merge 12 commits into
Conversation
…rterFactory Pure rename in preparation for the container converter redesign: the factory will grow per-container factories and own non-generic collection targets, so "Collection" no longer describes its scope. Also renames the entry point TryGetFromJniHandleConverter -> TryCreateConverter and updates the JavaConvert and ValueTypeFactory references. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f9ec4f09-ffc5-4f66-99bf-ec40ea2728e8
…ner factories Split the container converter into explicit, per-container factories (JavaListTypeFactory, JavaCollectionTypeFactory, JavaDictionaryTypeFactory) chained via TryCreateFromJniHandle(...) || ..., following the AOT prototype: - reference element/key/value arguments -> typeof(<>).MakeGenericType + Activator, riding NativeAOT's __Canon canonical templates; - mapped primitive/nullable arguments -> ValueTypeFactory, rooting the exact instantiation with a direct `new` (no reflection); - any other value type (custom struct) -> NotSupportedException, instead of the previous silent fall-through to a non-generic wrapper. TryCreateConverter now also owns non-generic JavaList/JavaCollection/JavaDictionary targets (and the raw IList/ICollection/IDictionary interfaces), delegating to the existing FromJniHandle helpers (direct construction, no reflection). JavaConvert's trimmable branch moves out of the IsGenericType guard so the factory is the single entry point for all Java collection construction on the trimmable path. NOTE: not yet built locally (requires the full framework); pending CI/local build. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f9ec4f09-ffc5-4f66-99bf-ec40ea2728e8
The explicit <Compile> item still referenced the old SafeJavaCollectionFactory.cs filename, breaking the build with CS2001. Point it at the renamed SafeContainerConverterFactory.cs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f9ec4f09-ffc5-4f66-99bf-ec40ea2728e8
Convert ISafeContainerTypeFactory into an abstract SafeContainerTypeFactory base class and move the MakeGenericType and CreateInstance reflection helpers (with their trimming/AOT suppressions) onto it as protected members, so the per-container factories use them via inheritance instead of sharing them through the entry-point class. MakeGenericType now takes a concrete reference-argument wrapper exemplar (e.g. JavaList<IJavaPeerable>) whose DynamicallyAccessedMembers annotation roots the __Canon template, making the AOT safety obvious. The dishonest [return: DAM] is dropped in favor of scoping the constructor suppression to CreateInstance. ValueTypeFactory's mixed reference/value dictionaries adopt the same exemplar pattern, removing the dedicated rooting-token fields. Also rename the dictionary GetValueTypeFactoryOrThrow helper to a pure TryGetPrimitiveValueTypeFactory Try-pattern method, with an up-front EnsureReferenceOrPrimitive validation, and revert the entry-point class name back to SafeJavaCollectionFactory to reduce churn. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 50ab60e5-5b4d-4e5f-abd2-2878d87a10c9
…stification Revert the JavaConvert.cs restructuring that hoisted the trimmable-typemap branch out of the generic-target guard. Non-generic collection targets are already handled identically by the existing IDictionary/IList/ICollection fallback below, so routing them through SafeJavaCollectionFactory was redundant. JavaConvert.cs now matches main except for the entry-point method name. Drop the resulting dead TryCreateNonGenericConverter helper (and the now-unused System.Collections import) from SafeJavaCollectionFactory. Also correct the IL2055 suppression justifications on MakeGenericType and CreateReferenceMixedDictionary: IL2055 is raised because the runtime element arguments cannot be proven to satisfy the DynamicallyAccessedMembers (Constructors) requirement on the wrapper's element type parameters, not because of the wrapper's own constructor. On the trimmable typemap path the wrapper never activates its elements (peer creation goes through JavaConvert and the typemap), so the requirement is never exercised. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 50ab60e5-5b4d-4e5f-abd2-2878d87a10c9
…static factories Replace the DynamicallyAccessedMembers-based rooting of the reference-argument collection wrappers with a concrete-literal approach that is easier to reason about. Each factory now branches on the IJavaPeerable element shape: that branch is a statically reachable reference to the concrete JavaList<IJavaPeerable> / JavaCollection<IJavaPeerable> / JavaDictionary<IJavaPeerable,IJavaPeerable> instantiation, so the trimmer/ILC keeps the (IntPtr, JniHandleOwnership) activation constructor of the shared __Canon template that the reflective else branch reuses for every other reference-argument instantiation. Also: - Reject open and partially-open constructed target types (ContainsGenericParameters) up front so they fail cleanly instead of throwing ArgumentException during activation. - Convert the per-container factories to static classes with a single static TryCreateFromJniHandle method (no abstract base class or instances). - Trim the trimming/AOT suppressions to the verified minimum (IL3050 + IL2055 on MakeGenericType) and correct the IL2055 justification: it fires because MakeGenericType cannot prove the runtime element type satisfies the wrapper's DAM(Constructors) element-parameter requirement, which is never exercised on the trimmable path since the wrapper does not activate its elements. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 50ab60e5-5b4d-4e5f-abd2-2878d87a10c9
Merge the three static per-container factory classes into three private static methods on SafeJavaCollectionFactory (TryCreateListFromJniHandle, TryCreateCollectionFromJniHandle, TryCreateDictionaryFromJniHandle), moving their suppressions and helpers onto the class and making ActivationConstructorBinding a private const. No behavior change. Also rename the entry point TryCreateConverter back to TryGetFromJniHandleConverter, which restores the original name and leaves JavaConvert.cs identical to main. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 50ab60e5-5b4d-4e5f-abd2-2878d87a10c9
Add the missing trim suppressions, clarify activation behavior, simplify the mixed dictionary helper, and cover unsupported value-type containers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 70f63eb7-6599-414c-a947-d860705aa0fa
There was a problem hiding this comment.
Pull request overview
Refactors the trimmable typemap’s Java-collection conversion path to make NativeAOT-safe construction of JavaList<T>, JavaCollection<T>, and JavaDictionary<K,V> explicit (reference-template vs mapped-primitive vs unsupported value-type), and adds a regression test to ensure unsupported value-type container arguments throw NotSupportedException on the trimmable path.
Changes:
- Reworks
SafeJavaCollectionFactoryinto explicit per-container construction paths with early rejection of open/partially-open generic targets. - Updates
ValueTypeFactorymixed reference/value dictionary construction to root the canonical templates via exemplar instantiations rather than static “token” types. - Adds a
JavaConvertTestregression test and unwrapsTargetInvocationExceptionso assertions observe the inner exception type.
Show a summary per file
| File | Description |
|---|---|
| tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaConvertTest.cs | Adds regression coverage for unsupported value-type generic arguments on the trimmable typemap path; improves reflection-invocation exception unwrapping for accurate assertions. |
| src/Mono.Android/Java.Interop/ValueTypeFactory.cs | Adjusts mixed dictionary creation to use exemplar-rooted canonical templates and centralizes reflective activation for those mixed shapes. |
| src/Mono.Android/Java.Interop/SafeJavaCollectionFactory.cs | Splits list/collection/dictionary creation into explicit, non-overlapping paths and hardens behavior for open generic targets and unsupported value-type arguments. |
Copilot's findings
- Files reviewed: 3/3 changed files
- Comments generated: 0
|
/review |
|
✅ Android PR Reviewer completed successfully! |
There was a problem hiding this comment.
🤖 Code Review — ⚠️ Minor comments
Verdict: Looks solid. The rework of SafeJavaCollectionFactory into explicit, non-overlapping per-container paths (TryCreateListFromJniHandle / TryCreateCollectionFromJniHandle / TryCreateDictionaryFromJniHandle) is a clear readability win over the previous shape-based dispatch, and the concrete-literal IJavaPeerable rooting branches are a defensible way to keep the __Canon activation constructors alive without leaning on [DAM] reflection flow.
What I verified
- Dictionary argument routing is correct for all four mixed/uniform combinations (
<ref,val>,<val,ref>,<val,val>,<ref,ref>), and unsupported value-type args (DateTime) are rejected up front viaEnsureReferenceOrPrimitivebefore any reflection — matching the newFromJniHandle_UnsupportedValueTypeThrowstest cases. ContainsGenericParametershardening correctly rejects open/partially-open targets beforeMakeGenericType.- Suppression justifications (
IL3050/IL2055/IL2072) are specific and accurately describe the__Canontemplate sharing; the correctedIL2055rationale reads well. - Test harness change (
ExceptionDispatchInfounwrapping ofTargetInvocationException) is the right way to assert the innerNotSupportedException.
Comments (both 💡, non-blocking)
- Behavioral change: unsupported value-type containers now throw at conversion time instead of falling through to the non-generic
JavaList/JavaCollection/JavaDictionary.FromJniHandlefallback — please confirm no consumer depended on that path. (inline) - Null-check inconsistency between
SafeJavaCollectionFactory'sActivator.CreateInstanceresults andValueTypeFactory.CreateReferenceMixedDictionary. (inline)
Notes
- CI checks were still pending at review time — this is not an approval; verify green CI (and ideally the recommended NativeAOT round-trip called out in the PR description) before merge.
Nice, well-documented change overall. 👍
Generated by Android PR Reviewer for #12159 · 112.3 AIC · ⌖ 13.2 AIC · ⊞ 6.8K
Comment /review to run again
| result = Activator.CreateInstance (typeof (JavaList<IJavaPeerable>), ActivationConstructorBinding, binder: null, args: [handle, transfer], culture: CultureInfo.InvariantCulture); | ||
| } else { | ||
| var listType = typeof (JavaList<>).MakeGenericType (elementType); | ||
| result = Activator.CreateInstance (listType, ActivationConstructorBinding, binder: null, args: [handle, transfer], culture: CultureInfo.InvariantCulture); |
There was a problem hiding this comment.
🤖 💡 Resource management — Minor consistency gap: the reference-path Activator.CreateInstance (...) results here (and in the JavaCollection/JavaDictionary variants) are assigned directly to result with no null check, whereas the sibling ValueTypeFactory.CreateReferenceMixedDictionary explicitly throws InvalidOperationException when Activator.CreateInstance returns null. If activation ever returns null, the converter here silently yields null instead of a diagnosable error. Consider mirroring the ValueTypeFactory null-check for symmetry and clearer diagnostics.
(Rule: Consistent error handling across sibling code paths)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 51cb0e43-153b-4458-8cf1-bb5def48b541
…tory Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 51cb0e43-153b-4458-8cf1-bb5def48b541
Keep exact NativeAOT factories limited to primitive and nullable value types. Unsupported structs now decline the typed converter so JavaConvert retains its existing untyped collection fallback instead of requiring DateTime-family special cases. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3deb271e-aea1-4ba1-bad5-2cdc4cf847bb
Note
Replacement for #12131 because its fork head repository was deleted. This branch now lives in
dotnet/android.What
Reworks the trimmable-typemap Java-collection converter (
SafeJavaCollectionFactory) so the AOT-safe construction ofJavaList<T>/JavaCollection<T>/JavaDictionary<K,V>is explicit and easy to reason about, and hardens a couple of edge cases. Net change touches the two implementation files,SafeJavaCollectionFactory.csandValueTypeFactory.cs, plus a targetedJavaConvertTestregression test (the factory name, public entry point name, andJavaConvert.csare unchanged vsmain).Construction split into explicit, non-overlapping paths
Per-container private methods (
TryCreateListFromJniHandle/TryCreateCollectionFromJniHandle/TryCreateDictionaryFromJniHandle) chained via... || ..., each handling exactly one wrapper:typeof(JavaList<>).MakeGenericType(...)+Activator.CreateInstance, riding NativeAOT's__Canoncanonical templates;ValueTypeFactory, rooting the exact instantiation with a directnew(no reflection);NotSupportedException(no AOT-unsafe reflection fallback), covered for list, collection, and dictionary interface/concrete targets by a trimmable-path regression test.Concrete-literal rooting (replaces
[DynamicallyAccessedMembers]for reference wrappers)Each reference path has an
if (elementType == typeof(IJavaPeerable))branch that constructs the concreteJavaList<IJavaPeerable>/JavaCollection<IJavaPeerable>/JavaDictionary<IJavaPeerable,IJavaPeerable>literal. That branch is effectively unreachable at runtime, but being a statically reachable reference to the concrete instantiation it forces the trimmer/ILC to keep the(IntPtr, JniHandleOwnership)activation constructor of the shared__Canontemplate. Theelsebranch reuses exactly that canonical constructor for every other reference-argument instantiation. This roots the constructor through real code rather than relying on[DAM]flowing through reflection.ValueTypeFactory's mixed reference/value dictionaries (JavaDictionary<string,int>, …) use the analogousCreateReferenceMixedDictionary<JavaDictionary<IJavaPeerable,T>>/<JavaDictionary<T,IJavaPeerable>>exemplars to root theJavaDictionary<__Canon,T>/JavaDictionary<T,__Canon>templates, replacing the previous static rooting-token fields. Value/value dictionaries continue to be rooted by the statically-emitted GVM cross-product (new JavaDictionary<TKey,T>()).Hardening
IList<>,IDictionary<T,int>, …) via aContainsGenericParameterscheck up front, so they fail cleanly instead of throwingArgumentExceptionduring activation.Suppressions
The reflective paths carry the minimum verified suppressions:
IL3050(RequiresDynamicCode) andIL2055onMakeGenericType, plusIL2072on theActivator.CreateInstancecalls whose constructed types ride the rooted canonical templates. TheIL2055justification is corrected to state the real cause —MakeGenericTypecannot prove the runtime element type satisfies the[DAM(Constructors)]requirement on the wrapper's element parameter, a requirement only exercised by the dynamic-code path (the trimmable path never activates elements through the wrapper).Validation
Mono.Android, Debug,net11.0) with no new warnings and no IL trim/AOT analysis errors.JavaList<string>,JavaDictionary<string,object>,JavaDictionary<string,int>) is still recommended to confirm the concrete-literal anchor roots the canonical-shared activation constructor as intended; relying on CI/device tests for full confirmation.Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com