Remove trim and AOT warning suppressions#1416
Conversation
Add an ActivatePeer overload that carries the declaring type's constructor preservation contract and align native registration annotations with the methods and nested types they use. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Update the Java.Interop submodule to include trim annotations for peer activation and native registration. This points at dotnet/java-interop#1416. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Set IsAotCompatible for Java.Interop so trim and AOT analyzer diagnostics are reported for this assembly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove UnconditionalSuppressMessage attributes from the Java.Interop project and keep the IsAotCompatible build warning-clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove the DynamicTypeConstruction constant left unused after replacing the analyzer suppressions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove the IL analyzer pragmas from JNI type lookup by separating constructor-only type lookup from native-registration lookup and avoiding runtime generic object-array marshaler construction. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove local MakeArrayType and MakeGenericType wrappers now that trim and dynamic-code requirements are modeled on the callers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep dynamic object-array marshaler construction behind RUC/RDC boundaries and restore peer/native registration behavior covered by the Java.Interop tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Emit the generic JNIEnv.GetArray<T>() overload from generated array marshaling code so bindings use statically typed array conversion instead of the runtime Type-based overload.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep generated bindings on the existing RegisterAttribute surface and restore Java.Base-ref.cs to the previous API shape. The broad JniTypeSignatureAttribute generation was unrelated to the trim/AOT warning fixes and caused unnecessary API churn.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Opt Java.Base and Java.Runtime.Environment into IsAotCompatible and model the stronger native-registration type preservation in JreTypeManager so the projects build without IL diagnostics.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace Java.Runtime.Environment trim suppressions with explicit member preservation on uninitialized activation helpers and use AppContext.BaseDirectory instead of Assembly.Location for runtime file discovery.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
/review |
|
✅ Java.Interop PR Reviewer completed successfully! |
There was a problem hiding this comment.
Analysis Summary
I reviewed the 10 requested files. Here are my findings:
Test Files
JavaVMFixture.cs — GetTypeForNativeRegistration override:
- The implementation is correct. It does call
base.GetTypeForNativeRegistration()(line 113), but with reversed priority vs the other overrides: localTypeMappingsare checked first,baseis the fallback. This is reasonable — the base implementation currently returnsnull, so checking local mappings first avoids a pointlessnullreturn. See inline comment.
Test coverage for new API surface:
GetType— Covered viaJniTypeManagerTests.GetType()(line 122) andGetTypeSignature()(line 208), plusJavaVMFixtureexercises the override.GetTypeAssignableTo— Indirectly tested viaJniValueManagerTests.GetValue_FindBestMatchType()which triggers the peer creation path (now callingGetTypeAssignableTo). TheJavaVMFixtureoverride exercises it directly.GetTypeForNativeRegistration— Override inJavaVMFixtureexists. No dedicated unit test for the base class behavior (returnsnull), but integration coverage comes fromManagedPeercall sites.GetInvokerTypeForPeerCreation— This isinternal, tested indirectly viaTryCreatePeerInstancein the value manager path. The newGetInvokerType_Generictest (line 33 ofJniRuntime.JniTypeManagerTests.cs) validates the publicGetInvokerTypewith generics but notGetInvokerTypeForPeerCreationspecifically — however, the latter intentionally rejects generics (returnsnullforarguments.Length > 0), so this is fine.ActivatePeernew overload: BothJniRuntimeTest.csandJniRuntime.JniValueManagerTests.csupdated theirActivatePeeroverrides to include the newType declaringTypeparameter. The old overload is kept as a compatibility shim in the base class.
JniValueMarshalerContractTests.cs — Expression.Constant validation:
- The change from
new {marshalerType}()to{marshalerValue}(wheremarshalerValue = FullName) correctly validates theExpression.New→Expression.Constantshift. The same pattern is correctly applied in theJniInt32ArrayValueMarshalerContractTestsoverride (line 507). See inline comment.
Generator Files
ClassGen.cs — __TypeRegistrations pattern change:
- Moving
RequiresDynamicCode/RequiresUnreferencedCodefromLookupmethod to class level is correct and properly replaces the oldUnconditionalSuppressMessageonLookup. See inline comment.
ArraySymbol.cs — JNIEnv.GetArray changes:
- Both
FromNative(line 95) andPreCallback(line 160) correctly switch fromJNIEnv.GetArray(handle, ownership, typeof(T))toJNIEnv.GetArray<T>(handle, ownership). This eliminatestypeof()usage that would trigger trim warnings.
GeneratedEnumSymbol.cs — FromNative change:
- Line 30: Same pattern —
JNIEnv.GetArray(...)withtypeof()is replaced byJNIEnv.GetArray<{0}>(...). Correct.
Expected output __TypeRegistrations.cs:
- The expected test output correctly reflects the generator changes: attributes moved to class level (lines 17-20),
UnconditionalSuppressMessageremoved fromLookup(lines 37-39 old → removed). This will keep generator tests passing.
No Issues Found
All changes are internally consistent and correctly implement the trim/AOT annotation strategy described in the PR.
Generated by Java.Interop PR Reviewer for issue #1416 · ● 44.5M
| [return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors | DynamicallyAccessedMemberTypes.AllMethods | DynamicallyAccessedMemberTypes.NonPublicNestedTypes)] | ||
| public override Type? GetTypeForNativeRegistration (JniTypeSignature typeSignature) | ||
| { | ||
| if (!typeSignature.IsValid || typeSignature.SimpleReference == null || typeSignature.ArrayRank != 0) { | ||
| return null; | ||
| } | ||
| if (TypeMappings.TryGetValue (typeSignature.SimpleReference, out var target)) { | ||
| return target; | ||
| } | ||
| return base.GetTypeForNativeRegistration (typeSignature); |
There was a problem hiding this comment.
Note: Unlike GetType (line 59) and GetTypeAssignableTo (line 86), which call base first and fall back to local mappings, GetTypeForNativeRegistration does the opposite — it checks local mappings first and falls back to base last.
This ordering difference is intentional and correct for native registration (you want the test's registered types to take priority over the base class's default null return), but it's worth noting the asymmetry. If the base class implementation ever gains real logic (currently returns null), the reversed priority order will matter.
| $"(ref {var_name}, {transfer})"; | ||
| } | ||
| return String.Format ("({0}[]{4}) JNIEnv.GetArray ({1}, {2}, typeof ({3}))", opt.GetOutputName (ElementType), var_name, owned ? "JniHandleOwnership.TransferLocalRef" : "JniHandleOwnership.DoNotTransfer", opt.GetOutputName (sym.FullName), opt.NullableOperator); | ||
| return String.Format ("({0}[]{3}) JNIEnv.GetArray<{2}> ({1}, {4})", opt.GetOutputName (ElementType), var_name, opt.GetOutputName (sym.FullName), opt.NullableOperator, owned ? "JniHandleOwnership.TransferLocalRef" : "JniHandleOwnership.DoNotTransfer"); |
There was a problem hiding this comment.
The change from JNIEnv.GetArray({1}, {2}, typeof({3})) to JNIEnv.GetArray<{2}>({1}, {4}) switches from the non-generic GetArray overload (which takes a Type parameter via typeof(), triggering trim warnings) to the generic GetArray<T>() overload. This is correct — the generic overload preserves the type statically, so the trimmer can analyze it without warnings. The format string index renumbering is correct.
| @@ -246,7 +247,7 @@ protected virtual string GetExpectedReturnValueFromManagedExpression (string jvm | |||
|
|
|||
| try | |||
| {{ | |||
| {value}_marshaler = new {marshalerType}(); | |||
| {value}_marshaler = {marshalerValue}; | |||
There was a problem hiding this comment.
The change from new {marshalerType}() to {marshalerValue} (using FullName) correctly reflects the underlying Expression.New → Expression.Constant shift in JniValueMarshaler.CreateSelf(). The ToCSharpCode() renderer for Expression.Constant emits the value's ToString() (which for types defaults to FullName), so using marshalerValue = marshaler.GetType().FullName here is the right expected string.
This is a sound change: Expression.Constant(this, marshalerType) captures the existing marshaler instance rather than constructing a new one, which avoids the trimmer warning from Expression.New needing to preserve the parameterless constructor. The test correctly validates this new behavior.
| sw.WriteLine ("#if NET5_0_OR_GREATER"); | ||
| sw.WriteLine ("\t[System.Diagnostics.CodeAnalysis.RequiresDynamicCode (\"Generated type registrations use legacy TypeManager mappings.\")]"); | ||
| sw.WriteLine ("\t[System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode (\"Generated type registrations use legacy TypeManager mappings that cannot be statically analyzed.\")]"); | ||
| sw.WriteLine ("#endif"); | ||
| sw.WriteLine ("\tpartial class __TypeRegistrations {"); |
There was a problem hiding this comment.
Moving RequiresDynamicCode/RequiresUnreferencedCode from the Lookup method to the class level is the right approach. The Lookup method calls Type.GetType(managedType) which triggers IL2057 (the trim warning about Type.GetType with a dynamically-computed string). Placing the attributes at the class level instead of only on Lookup:
- Ensures all members (including the
lookup_*_packagedelegates that callLookup) are covered - Replaces the per-method
UnconditionalSuppressMessagewith a proper caller-notification via the class-level attributes - The
#if NET5_0_OR_GREATERguard is correct since these attributes were introduced in .NET 5
This is a clean improvement — the whole registration class is inherently trim-incompatible, so annotating the class is more accurate than suppressing on individual methods.
| [return: DynamicallyAccessedMembers (ConstructorsMethodsNestedTypes)] | ||
| static Type GetNativeRegistrationTypeFromSignature (JniRuntime.JniTypeManager typeManager, JniTypeSignature typeSignature) |
There was a problem hiding this comment.
GetNativeRegistrationTypeFromSignature is dead code — it's defined but never called anywhere in the codebase. The actual RegisterNativeMembers path now calls TypeManager.GetTypeForNativeRegistration directly (lines 294 and 314). Consider removing this unused method to avoid confusion.
| [return: DynamicallyAccessedMembers (JniRuntime.JniTypeManager.MethodsConstructors)] | ||
| public override Type? GetTypeForNativeRegistration (JniTypeSignature typeSignature) | ||
| { | ||
| var type = base.GetTypeForNativeRegistration (typeSignature); | ||
| if (type != null) { | ||
| return type; | ||
| } | ||
|
|
||
| if (!typeSignature.IsValid || typeSignature.SimpleReference == null || typeSignature.ArrayRank != 0 || typeMappings == null) { | ||
| return null; | ||
| } | ||
|
|
||
| return typeMappings.TryGetValue (typeSignature.SimpleReference, out var target) | ||
| ? target | ||
| : null; |
There was a problem hiding this comment.
The [return: DynamicallyAccessedMembers(MethodsConstructors)] annotation claims the returned Type preserves constructors, all methods, and non-public nested types. Since typeMappings stores arbitrary Type values provided by the caller with no DAM contract, this is technically an unsound annotation — the trimmer will trust it, but there's no static guarantee the returned types actually have those members preserved.
Same concern as the base GetTypeForNativeRegistration — this is safe as long as callers (e.g., dotnet/android's linker steps) ensure the types in typeMappings are fully preserved.
|
|
||
| [UnconditionalSuppressMessage ("Trimming", "IL3000", Justification = "We check for a null Assembly.Location value!")] | ||
| internal static string? GetAssemblyLocation (Assembly assembly) | ||
| internal static string GetBaseDirectory () | ||
| { |
There was a problem hiding this comment.
Good: The GetBaseDirectory() change from Assembly.Location to AppContext.BaseDirectory properly removes the IL3000 suppression. AppContext.BaseDirectory is trim/AOT-safe and works in single-file/NativeAOT scenarios where Assembly.Location returns empty string. This is a clean improvement. 👍
| static Type? GetListType(Type type) | ||
| { | ||
| foreach (var iface in GetInterfaces (type).Concat (new [] { type })) { | ||
| if (typeof (IList<>).IsAssignableFrom (iface.IsGenericType ? iface.GetGenericTypeDefinition () : iface)) | ||
| return iface; | ||
| } | ||
| if (type.IsArray) | ||
| return type; | ||
| if (type.IsGenericType) { | ||
| var genericDefinition = type.GetGenericTypeDefinition (); | ||
| if (genericDefinition == typeof (JavaArray<>) || | ||
| genericDefinition == typeof (JavaObjectArray<>) || | ||
| genericDefinition == typeof (JavaPrimitiveArray<>)) | ||
| return type; | ||
| } | ||
| if (type.IsGenericType && type.GetGenericTypeDefinition () == typeof (IList<>)) | ||
| return type; | ||
| return null; |
There was a problem hiding this comment.
The old GetListType iterated Type.GetInterfaces() to find any type implementing IList<T> (e.g. List<string>, JavaObjectArray<Foo> subclasses, etc.). The new version only matches: raw arrays, the three exact Java generic array types, and IList<T> itself.
This is clearly intentional to eliminate the trim-unsafe GetInterfaces() call, but it narrows the matching: a concrete type like List<string> will no longer be routed to the object-array marshaler path. The impact should be limited since Java interop types are typically JavaObjectArray<T> or plain arrays, but it's worth a quick sanity check that no test or downstream consumer relies on List<T> → array-marshaler resolution.
| [return: DynamicallyAccessedMembers (ConstructorsMethodsNestedTypes)] | ||
| static Type GetNativeRegistrationTypeFromSignature (JniRuntime.JniTypeManager typeManager, JniTypeSignature typeSignature) | ||
| { | ||
| return typeManager.GetTypeForNativeRegistration (typeSignature) ?? | ||
| throw CreateMissingNativeRegistrationTypeException (typeSignature); | ||
| } |
There was a problem hiding this comment.
GetNativeRegistrationTypeFromSignature is defined but never called anywhere in the codebase. It appears to be dead code left over from the refactoring — RegisterNativeMembers now calls GetTypeForNativeRegistration inline (lines 294, 314) and CreateMissingNativeRegistrationTypeException directly.
Consider removing it to keep the codebase clean, or was this intended as a convenience for future callers?
| [return: DynamicallyAccessedMembers (JniRuntime.JniTypeManager.MethodsConstructors)] | ||
| public override Type? GetTypeForNativeRegistration (JniTypeSignature typeSignature) | ||
| { | ||
| var type = base.GetTypeForNativeRegistration (typeSignature); | ||
| if (type != null) { | ||
| return type; | ||
| } | ||
|
|
||
| if (!typeSignature.IsValid || typeSignature.SimpleReference == null || typeSignature.ArrayRank != 0 || typeMappings == null) { | ||
| return null; | ||
| } | ||
|
|
||
| return typeMappings.TryGetValue (typeSignature.SimpleReference, out var target) | ||
| ? target | ||
| : null; |
There was a problem hiding this comment.
Nit: The return type annotation [return: DynamicallyAccessedMembers (MethodsConstructors)] promises that the returned Type has methods, constructors, and nested types preserved. However, when the type comes from typeMappings (line 78), there's no static guarantee that the dictionary values carry that preservation — the IDictionary<string, Type> parameter in the constructor has no DAM annotation.
This is the same trust pattern the old code had (the suppression was essentially asserting the same thing), so it's not a regression. But since the goal of this PR is to move away from suppressions, it may be worth adding a [DynamicallyAccessedMembers] constraint to the typeMappings dictionary entries via a wrapper or documenting why this trust is safe (e.g. "callers must ensure registered types are preserved").
Related to dotnet/android#10794.
This models Java peer activation, JNI type lookup, and native registration reflection requirements for trim/AOT analysis so dotnet/android can remove suppressions instead of hiding analyzer warnings.
IsAotCompatibleforJava.Interop.csproj.ActivatePeeroverload that carries the declaring type constructor preservation contract.ConstructorInfo-based overload as a RUC compatibility shim.UnconditionalSuppressMessageusage from the Java.Interop project source.Validation:
./dotnet-local.sh build external/Java.Interop/src/Java.Interop/Java.Interop.csproj -v:minimal -nr:false./dotnet-local.sh build external/Java.Interop/src/Java.Runtime.Environment/Java.Runtime.Environment.csproj -v:minimal -nr:falserg "UnconditionalSuppressMessage|pragma warning disable IL" external/Java.Interop/src/Java.Interop -g "*.cs"Both builds succeeded with 0 warnings and 0 errors, and the scan has no matches.