Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
882ce7c
Use trimmable Java proxy runtime sources
simonrozsival May 3, 2026
0644b70
Address trimmable runtime review comments
simonrozsival May 4, 2026
dc13b42
Use trimmable virtual-constructor fixtures
simonrozsival May 3, 2026
b1d1947
Merge branch 'main' into trimmable-java-proxy-object
simonrozsival May 5, 2026
80d4af4
Address Java proxy review comments
simonrozsival May 5, 2026
ac015d1
Merge remote-tracking branch 'origin/main' into trimmable-java-proxy-…
simonrozsival May 7, 2026
2cb67ee
Merge branch 'trimmable-java-proxy-object' into trimmable-virtual-con…
simonrozsival May 7, 2026
43bc2ed
Simplify trimmable runtime artifacts
simonrozsival May 8, 2026
92a4836
Merge main into trimmable-java-proxy-object to pick up CI fixes
simonrozsival May 9, 2026
5a7e957
Merge trimmable-java-proxy-object into trimmable-virtual-constructor …
simonrozsival May 9, 2026
244c81f
Merge remote-tracking branch 'origin/main' into trimmable-virtual-con…
simonrozsival May 11, 2026
f863ba1
Drop unrelated obsolete-jar cleanup from java-runtime.targets and Tri…
simonrozsival May 11, 2026
7a6dde8
Refuse [JniAddNativeMethodRegistrationAttribute] in the trimmable typ…
simonrozsival May 11, 2026
5f1ce53
Move XA4251 reporting into JavaPeerScanner
simonrozsival May 11, 2026
6b47cef
Restore tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/ne…
simonrozsival May 11, 2026
3dfd44e
Exclude InvokeVirtualFromConstructor fixtures for trimmable
simonrozsival May 11, 2026
7d607c5
Detect [JniAddNativeMethodRegistrationAttribute] on non-peer types too
simonrozsival May 11, 2026
1a71bb3
Skip per-method XA4251 walk when assembly doesn't reference the attri…
simonrozsival May 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ public interface ITrimmableTypeMapLogger
void LogGeneratedJcwFilesInfo (int sourceCount);
void LogRootingManifestReferencedTypeInfo (string javaTypeName, string managedTypeName);
void LogManifestReferencedTypeNotFoundWarning (string javaTypeName);
void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeName);
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,15 @@ sealed class AssemblyIndex : IDisposable
/// </summary>
public Dictionary<TypeDefinitionHandle, TypeAttributeInfo> AttributesByType { get; } = new ();

/// <summary>
/// True iff the assembly's metadata mentions a type named
/// <c>JniAddNativeMethodRegistrationAttribute</c> (as a TypeReference or
/// TypeDefinition). The trimmable typemap forbids that attribute (XA4251);
/// this flag lets the scanner short-circuit the per-method attribute walk
/// for the overwhelmingly common case of assemblies that don't use it.
/// </summary>
public bool MayUseJniAddNativeMethodRegistrationAttribute { get; private set; }

AssemblyIndex (PEReader peReader, MetadataReader reader, string assemblyName)
{
this.peReader = peReader;
Expand All @@ -51,9 +60,27 @@ public static AssemblyIndex Create (PEReader peReader, string assemblyName)

void Build ()
{
const string JniAddNativeMethodRegistrationAttribute = "JniAddNativeMethodRegistrationAttribute";

// Cheap first pass over TypeReferences / TypeDefinitions to decide whether
// the assembly is even capable of carrying [JniAddNativeMethodRegistration].
// The per-method attribute walk in the scanner can then skip entirely for
// the common case where the attribute is neither imported nor declared here.
foreach (var trHandle in Reader.TypeReferences) {
if (Reader.GetString (Reader.GetTypeReference (trHandle).Name) == JniAddNativeMethodRegistrationAttribute) {
MayUseJniAddNativeMethodRegistrationAttribute = true;
break;
}
}

foreach (var typeHandle in Reader.TypeDefinitions) {
var typeDef = Reader.GetTypeDefinition (typeHandle);

if (!MayUseJniAddNativeMethodRegistrationAttribute &&
Reader.GetString (typeDef.Name) == JniAddNativeMethodRegistrationAttribute) {
MayUseJniAddNativeMethodRegistrationAttribute = true;
}

var fullName = MetadataTypeNameResolver.GetFullName (typeDef, Reader);
if (fullName.Length == 0) {
continue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ public sealed class JavaPeerScanner : IDisposable
{
readonly Dictionary<string, AssemblyIndex> assemblyCache = new (StringComparer.Ordinal);
readonly Dictionary<(string typeName, string assemblyName), ActivationCtorInfo> activationCtorCache = new ();
readonly ITrimmableTypeMapLogger? logger;

public JavaPeerScanner (ITrimmableTypeMapLogger? logger = null)
{
this.logger = logger;
}

/// <summary>
/// Resolves a type name + assembly name to a TypeDefinitionHandle + AssemblyIndex.
Expand Down Expand Up @@ -168,6 +174,19 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A
continue;
}

// [JniAddNativeMethodRegistrationAttribute] is not supported by the trimmable typemap
// by design (see XA4251). Detect the attribute *before* any per-type filters below
// (array type, no JNI name, etc.) so the diagnostic fires uniformly regardless of
// whether the type would otherwise have ended up in the typemap.
//
// Skip the per-method walk entirely for the overwhelmingly common case where
// the assembly doesn't even reference the attribute type — the per-assembly
// flag was computed cheaply in AssemblyIndex.Build.
if (index.MayUseJniAddNativeMethodRegistrationAttribute &&
HasJniAddNativeMethodRegistrationAttribute (typeDef, index)) {
logger?.LogJniAddNativeMethodRegistrationAttributeError (MetadataTypeNameResolver.GetFullName (typeDef, index.Reader));
}

// Determine the JNI name and whether this is a known Java peer.
// Priority:
// 1. [Register] attribute → use JNI name from attribute
Expand Down Expand Up @@ -338,6 +357,20 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A
return (methods, fields);
}

static bool HasJniAddNativeMethodRegistrationAttribute (TypeDefinition typeDef, AssemblyIndex index)
{
foreach (var methodHandle in typeDef.GetMethods ()) {
var methodDef = index.Reader.GetMethodDefinition (methodHandle);
foreach (var attrHandle in methodDef.GetCustomAttributes ()) {
var attr = index.Reader.GetCustomAttribute (attrHandle);
if (AssemblyIndex.GetCustomAttributeName (attr, index.Reader) == "JniAddNativeMethodRegistrationAttribute") {
return true;
}
}
}
return false;
}

/// <summary>
/// For each virtual override method on <paramref name="typeDef"/> that wasn't already
/// collected (no direct [Register]), walks up the base type hierarchy to find a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ GeneratedManifest GenerateManifest (List<JavaPeerInfo> allPeers, AssemblyManifes

(List<JavaPeerInfo> peers, AssemblyManifestInfo manifestInfo) ScanAssemblies (IReadOnlyList<(string Name, PEReader Reader)> assemblies)
{
using var scanner = new JavaPeerScanner ();
using var scanner = new JavaPeerScanner (logger);
var peers = scanner.Scan (assemblies);
var manifestInfo = scanner.ScanAssemblyManifestInfo ();
logger.LogJavaPeerScanInfo (assemblies.Count, peers.Count);
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Xamarin.Android.Build.Tasks/Properties/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1079,6 +1079,11 @@ To use a custom JDK path for a command line build, set the 'JavaSdkDirectory' MS
<value>Manifest-referenced type '{0}' was not found in any scanned assembly. It may be a framework type.</value>
<comment>The following are literal names and should not be translated: Manifest, framework.
{0} - Java type name from AndroidManifest.xml</comment>
</data>
<data name="XA4251" xml:space="preserve">
<value>Type '{0}' uses [JniAddNativeMethodRegistrationAttribute], which is not supported by the trimmable type map. To work around this, do not target the trimmable type map (for example, by switching to the 'llvm-ir' type map implementation), and please report this scenario at https://github.com/dotnet/android/issues so the team can evaluate whether to support it.</value>
<comment>The following are literal names and should not be translated: JniAddNativeMethodRegistrationAttribute, llvm-ir.
{0} - Fully-qualified managed type name</comment>
</data>
<data name="XA0142" xml:space="preserve">
<value>Command '{0}' failed.\n{1}</value>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ public void LogRootingManifestReferencedTypeInfo (string javaTypeName, string ma
log.LogMessage (MessageImportance.Low, $"Rooting manifest-referenced type '{javaTypeName}' ({managedTypeName}) as unconditional.");
public void LogManifestReferencedTypeNotFoundWarning (string javaTypeName) =>
log.LogCodedWarning ("XA4250", Properties.Resources.XA4250, javaTypeName);
public void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeName) =>
log.LogCodedError ("XA4251", Properties.Resources.XA4251, managedTypeName);
}

public override string TaskPrefix => "GTT";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ public void LogRootingManifestReferencedTypeInfo (string javaTypeName, string ma
logMessages.Add ($"Rooting manifest-referenced type '{javaTypeName}' ({managedTypeName}) as unconditional.");
public void LogManifestReferencedTypeNotFoundWarning (string javaTypeName) =>
warnings?.Add ($"Manifest-referenced type '{javaTypeName}' was not found in any scanned assembly. It may be a framework type.");
public void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeName) =>
logMessages.Add ($"XA4251: Type '{managedTypeName}' uses [JniAddNativeMethodRegistrationAttribute], which is not supported by the trimmable type map.");
}

[Fact]
Expand Down Expand Up @@ -72,6 +74,17 @@ public void Execute_WithTestFixtures_ProducesOutputs ()
Assert.Contains (result.GeneratedAssemblies, a => a.Name == "_TestFixtures.TypeMap");
}

[Fact]
public void Execute_WithJniAddNativeMethodRegistrationAttribute_ReportsXA4251 ()
{
// TestFixtures.HandWrittenNativeRegistrationPeer carries [JniAddNativeMethodRegistrationAttribute].
// The trimmable typemap does not support that attribute by design — the orchestrator must
// emit XA4251 so MSBuild fails the build via HasLoggedErrors.
using var peReader = CreateTestFixturePEReader ();
CreateGenerator ().Execute (new List<(string, PEReader)> { ("TestFixtures", peReader) }, new Version (11, 0), new HashSet<string> ());
Assert.Contains (logMessages, m => m.Contains ("XA4251") && m.Contains ("HandWrittenNativeRegistrationPeer"));
}

[Fact]
public void Execute_CollectsDeferredRegistrationTypes_ForAllApplicationAndInstrumentationSubtypes ()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
using Xunit;

namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests;
Expand Down Expand Up @@ -42,6 +45,41 @@ public void Scan_IsUnconditional (string javaName, bool expected)
Assert.Equal (expected, FindFixtureByJavaName (javaName).IsUnconditional);
}

[Fact]
public void Scan_JniAddNativeMethodRegistrationAttribute_LogsError ()
{
// The trimmable typemap refuses to support [JniAddNativeMethodRegistrationAttribute]
// by design (XA4251). The scanner reports each offending type via the logger.
var errors = new List<string> ();
var logger = new RecordingLogger (errors);

using var scanner = new JavaPeerScanner (logger);
using var peReader = new PEReader (File.OpenRead (TestFixtureAssemblyPath));
var reader = peReader.GetMetadataReader ();
var assemblyName = reader.GetString (reader.GetAssemblyDefinition ().Name);
_ = scanner.Scan (new List<(string, PEReader)> { (assemblyName, peReader) });

Assert.Contains (errors, e => e.Contains ("HandWrittenNativeRegistrationPeer"));
Assert.Contains (errors, e => e.Contains ("NonPeerNativeRegistration"));
Assert.DoesNotContain (errors, e => e.Contains ("MyHelper"));
}

sealed class RecordingLogger (List<string> errors) : ITrimmableTypeMapLogger
{
public void LogNoJavaPeerTypesFound () { }
public void LogJavaPeerScanInfo (int assemblyCount, int peerCount) { }
public void LogGeneratingJcwFilesInfo (int jcwPeerCount, int totalPeerCount) { }
public void LogDeferredRegistrationTypesInfo (int typeCount) { }
public void LogGeneratedTypeMapAssemblyInfo (string assemblyName, int typeCount) { }
public void LogGeneratedRootTypeMapInfo (int assemblyReferenceCount) { }
public void LogGeneratedTypeMapAssembliesInfo (int assemblyCount) { }
public void LogGeneratedJcwFilesInfo (int sourceCount) { }
public void LogRootingManifestReferencedTypeInfo (string javaTypeName, string managedTypeName) { }
public void LogManifestReferencedTypeNotFoundWarning (string javaTypeName) { }
public void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeName) =>
errors.Add ($"XA4251: {managedTypeName}");
}

[Fact]
public void Scan_TypeMetadata_IsCorrect ()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,14 @@ public sealed class JniTypeSignatureAttribute : Attribute
}
}

namespace Java.Interop
{
[AttributeUsage (AttributeTargets.Method, AllowMultiple = false)]
public sealed class JniAddNativeMethodRegistrationAttribute : Attribute
{
}
}

namespace MyApp
{
[AttributeUsage (AttributeTargets.Class)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,28 @@ public class MyHelper : Java.Lang.Object
public virtual void DoSomething () { }
}

// Fixture for the trimmable typemap's [JniAddNativeMethodRegistrationAttribute] detection.
// The trimmable typemap deliberately does not support this attribute (XA4251).
[Register ("my/app/HandWrittenNativeRegistrationPeer", DoNotGenerateAcw = true)]
public class HandWrittenNativeRegistrationPeer : Java.Lang.Object
{
[Java.Interop.JniAddNativeMethodRegistration]
static void RegisterNativeMembers ()
{
}
}

// Non-peer type carrying the attribute (no [Register], no Java peer base).
// The scanner must still emit XA4251 even though this type wouldn't otherwise
// have been added to the typemap.
public class NonPeerNativeRegistration
{
[Java.Interop.JniAddNativeMethodRegistration]
static void RegisterNativeMembers ()
{
}
}

[Service (Name = "my.app.MyService")]
public class MyService : Android.App.Service
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,19 @@
<Compile Remove="$(JavaInteropTestDirectory)obj\Release\net7.0-android/designtime/Resource.designer.cs" />
</ItemGroup>

<!--
[JniAddNativeMethodRegistrationAttribute] is not supported under the trimmable
typemap (XA4251). Exclude the upstream Java.Interop fixtures that use it (and
their dependents) when building for the trimmable typemap so the assembly can
compile through the generator. The matching NUnit test class is excluded by
name in tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/NUnitInstrumentation.cs.
-->
<ItemGroup Condition=" '$(_AndroidTypeMapImplementation)' == 'trimmable' ">
<Compile Remove="$(JavaInteropTestDirectory)Java.Interop\CallVirtualFromConstructorBase.cs" />
<Compile Remove="$(JavaInteropTestDirectory)Java.Interop\CallVirtualFromConstructorDerived.cs" />
<Compile Remove="$(JavaInteropTestDirectory)Java.Interop\InvokeVirtualFromConstructorTests.cs" />
</ItemGroup>

<!-- Some of tests included in the files below fail on CoreCLR. This is PROBABLY caused by managed typemaps, which
will be replaced at some point by a different implementation. Until then, these tests can remain disabled. -->
<!-- TODO: put these back when new typemap implementation is available -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,13 @@ protected NUnitInstrumentation(IntPtr handle, JniHandleOwnership transfer)
// trimmable typemap. These cannot use [Category("TrimmableIgnore")] because
// we don't control that assembly — they must be excluded by name here.
ExcludedTestNames = new [] {
// net.dot.jni.test.CallVirtualFromConstructorDerived Java class not in APK
// [JniAddNativeMethodRegistrationAttribute] is not supported by design under
// the trimmable typemap. This Java.Interop-Tests fixture uses that attribute
// to register native callbacks on a hand-written Java peer (an obsolete code
// path whose primary consumer, jnimarshalmethod-gen, was removed in
// dotnet/java-interop#1405). The trimmable typemap generator emits XA4251
// when it encounters the attribute and instructs users to either avoid it or
// switch off the trimmable typemap. See https://github.com/dotnet/android/issues/11170.
"Java.InteropTests.InvokeVirtualFromConstructorTests",

// JNI method remapping not supported in trimmable typemap
Expand Down