Skip to content

Remove trim and AOT warning suppressions#1416

Draft
simonrozsival wants to merge 13 commits intomainfrom
dev/simonrozsival/10794-trim-annotations
Draft

Remove trim and AOT warning suppressions#1416
simonrozsival wants to merge 13 commits intomainfrom
dev/simonrozsival/10794-trim-annotations

Conversation

@simonrozsival
Copy link
Copy Markdown
Member

@simonrozsival simonrozsival commented May 1, 2026

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.

  • Enables IsAotCompatible for Java.Interop.csproj.
  • Adds an ActivatePeer overload that carries the declaring type constructor preservation contract.
  • Keeps the existing ConstructorInfo-based overload as a RUC compatibility shim.
  • Splits constructor-only JNI type lookup from native-registration lookup so DAM requirements are explicit.
  • Removes UnconditionalSuppressMessage usage from the Java.Interop project source.
  • Removes IL analyzer pragmas from Java.Interop trim/AOT paths.

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:false
  • rg "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.

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>
@simonrozsival simonrozsival changed the base branch from release/10.0.1xx-preview1 to main May 1, 2026 12:31
simonrozsival added a commit to dotnet/android that referenced this pull request May 1, 2026
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>
simonrozsival and others added 12 commits May 1, 2026 20:47
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>
@simonrozsival simonrozsival changed the title Model peer activation for trimming Remove trim and AOT warning suppressions May 4, 2026
@jonathanpeppers
Copy link
Copy Markdown
Member

/review

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 6, 2026

Java.Interop PR Reviewer completed successfully!

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Analysis Summary

I reviewed the 10 requested files. Here are my findings:

Test Files

JavaVMFixture.csGetTypeForNativeRegistration override:

  • The implementation is correct. It does call base.GetTypeForNativeRegistration() (line 113), but with reversed priority vs the other overrides: local TypeMappings are checked first, base is the fallback. This is reasonable — the base implementation currently returns null, so checking local mappings first avoids a pointless null return. See inline comment.

Test coverage for new API surface:

  • GetType — Covered via JniTypeManagerTests.GetType() (line 122) and GetTypeSignature() (line 208), plus JavaVMFixture exercises the override.
  • GetTypeAssignableTo — Indirectly tested via JniValueManagerTests.GetValue_FindBestMatchType() which triggers the peer creation path (now calling GetTypeAssignableTo). The JavaVMFixture override exercises it directly.
  • GetTypeForNativeRegistration — Override in JavaVMFixture exists. No dedicated unit test for the base class behavior (returns null), but integration coverage comes from ManagedPeer call sites.
  • GetInvokerTypeForPeerCreation — This is internal, tested indirectly via TryCreatePeerInstance in the value manager path. The new GetInvokerType_Generic test (line 33 of JniRuntime.JniTypeManagerTests.cs) validates the public GetInvokerType with generics but not GetInvokerTypeForPeerCreation specifically — however, the latter intentionally rejects generics (returns null for arguments.Length > 0), so this is fine.
  • ActivatePeer new overload: Both JniRuntimeTest.cs and JniRuntime.JniValueManagerTests.cs updated their ActivatePeer overrides to include the new Type declaringType parameter. 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} (where marshalerValue = FullName) correctly validates the Expression.NewExpression.Constant shift. The same pattern is correctly applied in the JniInt32ArrayValueMarshalerContractTests override (line 507). See inline comment.

Generator Files

ClassGen.cs__TypeRegistrations pattern change:

  • Moving RequiresDynamicCode/RequiresUnreferencedCode from Lookup method to class level is correct and properly replaces the old UnconditionalSuppressMessage on Lookup. See inline comment.

ArraySymbol.csJNIEnv.GetArray changes:

  • Both FromNative (line 95) and PreCallback (line 160) correctly switch from JNIEnv.GetArray(handle, ownership, typeof(T)) to JNIEnv.GetArray<T>(handle, ownership). This eliminates typeof() usage that would trigger trim warnings.

GeneratedEnumSymbol.csFromNative change:

  • Line 30: Same pattern — JNIEnv.GetArray(...) with typeof() is replaced by JNIEnv.GetArray<{0}>(...). Correct.

Expected output __TypeRegistrations.cs:

  • The expected test output correctly reflects the generator changes: attributes moved to class level (lines 17-20), UnconditionalSuppressMessage removed from Lookup (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

Comment on lines +104 to +113
[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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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");
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Comment on lines 239 to +250
@@ -246,7 +247,7 @@ protected virtual string GetExpectedReturnValueFromManagedExpression (string jvm

try
{{
{value}_marshaler = new {marshalerType}();
{value}_marshaler = {marshalerValue};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The change from new {marshalerType}() to {marshalerValue} (using FullName) correctly reflects the underlying Expression.NewExpression.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.

Comment on lines +200 to 204
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 {");
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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:

  1. Ensures all members (including the lookup_*_package delegates that call Lookup) are covered
  2. Replaces the per-method UnconditionalSuppressMessage with a proper caller-notification via the class-level attributes
  3. The #if NET5_0_OR_GREATER guard 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.

Comment on lines +340 to +341
[return: DynamicallyAccessedMembers (ConstructorsMethodsNestedTypes)]
static Type GetNativeRegistrationTypeFromSignature (JniRuntime.JniTypeManager typeManager, JniTypeSignature typeSignature)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Comment on lines +66 to +80
[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;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Comment on lines 150 to 152

[UnconditionalSuppressMessage ("Trimming", "IL3000", Justification = "We check for a null Assembly.Location value!")]
internal static string? GetAssemblyLocation (Assembly assembly)
internal static string GetBaseDirectory ()
{
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Comment on lines 729 to 742
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;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Comment on lines +340 to +345
[return: DynamicallyAccessedMembers (ConstructorsMethodsNestedTypes)]
static Type GetNativeRegistrationTypeFromSignature (JniRuntime.JniTypeManager typeManager, JniTypeSignature typeSignature)
{
return typeManager.GetTypeForNativeRegistration (typeSignature) ??
throw CreateMissingNativeRegistrationTypeException (typeSignature);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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?

Comment on lines +66 to +80
[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;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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").

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants