Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
21 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
414c224
Merge branch 'main' into trimmable-virtual-constructor
simonrozsival May 12, 2026
c9c0800
Check JNI registration attribute namespace
simonrozsival May 13, 2026
fc71573
Merge branch 'main' into trimmable-virtual-constructor
jonathanpeppers May 13, 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 @@ -132,8 +132,9 @@ static void EmitPeers (TypeMapAssemblyData model, string jniName,
if (!isAliasGroup) {
// Single peer — no aliases needed, emit directly with the base JNI name
var peer = peersForName [0];
bool hasProxy = peer.ActivationCtor != null || peer.InvokerTypeName != null;
bool isAcw = !peer.DoNotGenerateAcw && !peer.IsInterface && peer.MarshalMethods.Count > 0;
isAcw |= peer.DoNotGenerateAcw && peer.HasJniAddNativeMethodRegistrationAttribute && !peer.IsInterface && peer.MarshalMethods.Count > 0;
bool hasProxy = peer.ActivationCtor != null || peer.InvokerTypeName != null || isAcw;

JavaPeerProxyData? proxy = null;
if (hasProxy) {
Expand Down Expand Up @@ -167,8 +168,9 @@ static void EmitPeers (TypeMapAssemblyData model, string jniName,
string entryJniName = $"{jniName}[{i}]";
aliasKeys.Add (entryJniName);

bool hasProxy = peer.ActivationCtor != null || peer.InvokerTypeName != null;
bool isAcw = !peer.DoNotGenerateAcw && !peer.IsInterface && peer.MarshalMethods.Count > 0;
isAcw |= peer.DoNotGenerateAcw && peer.HasJniAddNativeMethodRegistrationAttribute && !peer.IsInterface && peer.MarshalMethods.Count > 0;
bool hasProxy = peer.ActivationCtor != null || peer.InvokerTypeName != null || isAcw;

JavaPeerProxyData? proxy = null;
if (hasProxy) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ public sealed record JavaPeerInfo
/// </summary>
public bool DoNotGenerateAcw { get; init; }

/// <summary>
/// True when the managed type uses <c>JniAddNativeMethodRegistrationAttribute</c>
/// to provide native methods for a hand-written Java peer.
/// </summary>
public bool HasJniAddNativeMethodRegistrationAttribute { get; init; }

/// <summary>
/// True when the type was discovered via <c>[JniTypeSignatureAttribute]</c>
/// rather than <c>[RegisterAttribute]</c>. Used to resolve cross-assembly
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A
var isInterface = (typeDef.Attributes & TypeAttributes.Interface) != 0;
var isAbstract = (typeDef.Attributes & TypeAttributes.Abstract) != 0;
var isGenericDefinition = typeDef.GetGenericParameters ().Count > 0;
var hasJniAddNativeMethodRegistrationAttribute = HasJniAddNativeMethodRegistrationAttribute (typeDef, index);
Comment thread
jonathanpeppers marked this conversation as resolved.
Outdated

var isUnconditional = attrInfo is not null;
var cannotRegisterInStaticConstructor = attrInfo is ApplicationAttributeInfo or InstrumentationAttributeInfo;
Expand Down Expand Up @@ -258,6 +259,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A
IsInterface = isInterface,
IsAbstract = isAbstract,
DoNotGenerateAcw = doNotGenerateAcw,
HasJniAddNativeMethodRegistrationAttribute = hasJniAddNativeMethodRegistrationAttribute,
IsFromJniTypeSignature = registerInfo?.IsFromJniTypeSignature ?? false,
IsUnconditional = isUnconditional,
CannotRegisterInStaticConstructor = cannotRegisterInStaticConstructor,
Expand Down Expand Up @@ -338,6 +340,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") {
Comment thread
jonathanpeppers marked this conversation as resolved.
Outdated
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 @@ -25,6 +25,8 @@
<Compile Include="$(JavaInteropTestDirectory)**\*.cs" Exclude="$(JavaInteropTestDirectory)obj\**;$(JavaInteropTestDirectory)bin\**" />
<Compile Remove="$(JavaInteropTestDirectory)Java.Interop\JavaVMFixture.cs" />
<Compile Remove="$(JavaInteropTestDirectory)Java.Interop\JniReferenceSafeHandleTest.cs" />
<Compile Remove="$(JavaInteropTestDirectory)Java.Interop\CallVirtualFromConstructorDerived.cs" Condition=" '$(_AndroidTypeMapImplementation)' == 'trimmable' " />
<Compile Remove="Java.Interop-trimmable\CallVirtualFromConstructorDerived.cs" Condition=" '$(_AndroidTypeMapImplementation)' != 'trimmable' " />
<Compile Remove="$(JavaInteropTestDirectory)obj\Release\net7.0-android/designtime/Resource.designer.cs" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
java-trimmable/net/dot/jni/test/GetThis.java is added explicitly via
TestJarEntry below when building for the trimmable typemap; it must be
removed from the implicit AndroidJavaSource glob so the .NET SDK does
not also try to compile it as a regular AndroidJavaSource (which would
collide with the desktop GetThis.java in the test JAR).
not also try to compile these files as regular AndroidJavaSource items
(which would collide with the desktop Java files in the test JAR).
-->
<AndroidJavaSource Remove="java-trimmable\**\*.java" />
</ItemGroup>
Expand All @@ -38,12 +38,16 @@
MSBuild's up-to-date check doesn't track the conditional swap automatically.
-->
<TestJarEntry Include="$(MSBuildThisFileDirectory)..\..\..\external\Java.Interop\tests\Java.Interop-Tests\java\**\*.java"
Exclude="$(MSBuildThisFileDirectory)..\..\..\external\Java.Interop\tests\Java.Interop-Tests\java\net\dot\jni\test\GetThis.java"
Exclude="$(MSBuildThisFileDirectory)..\..\..\external\Java.Interop\tests\Java.Interop-Tests\java\net\dot\jni\test\GetThis.java;$(MSBuildThisFileDirectory)..\..\..\external\Java.Interop\tests\Java.Interop-Tests\java\net\dot\jni\test\CallVirtualFromConstructorBase.java;$(MSBuildThisFileDirectory)..\..\..\external\Java.Interop\tests\Java.Interop-Tests\java\net\dot\jni\test\CallVirtualFromConstructorDerived.java"
Condition=" '$(_AndroidTypeMapImplementation)' == 'trimmable' " />
<TestJarEntry Include="$(MSBuildThisFileDirectory)..\..\..\external\Java.Interop\tests\Java.Interop-Tests\java\**\*.java"
Condition=" '$(_AndroidTypeMapImplementation)' != 'trimmable' " />
<TestJarEntry Include="$(MSBuildThisFileDirectory)java-trimmable\net\dot\jni\test\GetThis.java"
Condition=" '$(_AndroidTypeMapImplementation)' == 'trimmable' " />
<TestJarEntry Include="$(MSBuildThisFileDirectory)java-trimmable\net\dot\jni\test\CallVirtualFromConstructorBase.java"
Condition=" '$(_AndroidTypeMapImplementation)' == 'trimmable' " />
<TestJarEntry Include="$(MSBuildThisFileDirectory)java-trimmable\net\dot\jni\test\CallVirtualFromConstructorDerived.java"
Condition=" '$(_AndroidTypeMapImplementation)' == 'trimmable' " />
</ItemGroup>
<Target Name="_CopyTestJarFiles">
<Copy
Expand All @@ -54,4 +58,4 @@
<Target Name="CleanLocal">
<RemoveDir Directories="Jars"/>
</Target>
</Project>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
#nullable enable

using System;
using System.Runtime.CompilerServices;
Comment thread
jonathanpeppers marked this conversation as resolved.
Outdated

using Java.Interop;

namespace Java.InteropTests
{
[JniTypeSignature (CallVirtualFromConstructorDerived.JniTypeName, GenerateJavaPeer=false)]
public class CallVirtualFromConstructorDerived : CallVirtualFromConstructorBase {
new internal const string JniTypeName = "net/dot/jni/test/CallVirtualFromConstructorDerived";
static readonly JniPeerMembers _members = new JniPeerMembers (JniTypeName, typeof (CallVirtualFromConstructorDerived));

[JniAddNativeMethodRegistrationAttribute]
static void RegisterNativeMembers (JniNativeMethodRegistrationArguments args)
{
args.Registrations.Add (new JniNativeMethodRegistration ("calledFromConstructor", "(I)V", (CalledFromConstructorMarshalMethod)CalledFromConstructorHandler));
}

public override JniPeerMembers JniPeerMembers {
get {return _members;}
}

int calledValue;

public bool InvokedConstructor;

[Register (".ctor", "(I)V", "")]
public CallVirtualFromConstructorDerived (int value)
: this (value, useNewObject: false)
{
}

public CallVirtualFromConstructorDerived (int value, bool useNewObject)
: base (value, useNewObject)
{
InvokedConstructor = true;

if (useNewObject && calledValue != 0) {
// calledValue was set on a *different* instance! So it's 0 here.
throw new ArgumentException (
string.Format ("value '{0}' doesn't match expected value '{1}'.", value, 0),
"value");
}
if (!useNewObject && value != calledValue)
throw new ArgumentException (
string.Format ("value '{0}' doesn't match expected value '{1}'.", value, calledValue),
"value");
}

public bool InvokedActivationConstructor;

public static CallVirtualFromConstructorDerived? Intermediate_FromCalledFromConstructor;
public static CallVirtualFromConstructorDerived? Intermediate_FromActivationConstructor;

public CallVirtualFromConstructorDerived (ref JniObjectReference reference, JniObjectReferenceOptions options)
: base (ref reference, options)
{
InvokedActivationConstructor = true;

Intermediate_FromActivationConstructor = this;
}

public bool Called;

[Register ("calledFromConstructor", "(I)V", "")]
public override void CalledFromConstructor (int value)
{
Called = true;
calledValue = value;

Intermediate_FromCalledFromConstructor = this;
}

public static unsafe CallVirtualFromConstructorDerived NewInstance (int value)
{
JniArgumentValue* args = stackalloc JniArgumentValue [1];
args [0] = new JniArgumentValue (value);
var o = _members.StaticMethods.InvokeObjectMethod ("newInstance.(I)Lnet/dot/jni/test/CallVirtualFromConstructorDerived;", args);
var result = JniEnvironment.Runtime.ValueManager.GetValue<CallVirtualFromConstructorDerived> (ref o, JniObjectReferenceOptions.CopyAndDispose);
if (result == null)
throw new InvalidOperationException ("newInstance returned null.");
return result;
}

delegate void CalledFromConstructorMarshalMethod (IntPtr jnienv, IntPtr n_self, int value);
static void CalledFromConstructorHandler (IntPtr jnienv, IntPtr n_self, int value)
{
n_CalledFromConstructor (jnienv, n_self, value);
}

static void n_CalledFromConstructor (IntPtr jnienv, IntPtr n_self, int value)
{
var envp = new JniTransition (jnienv);
try {
var r_self = new JniObjectReference (n_self);
var self = JniEnvironment.Runtime.ValueManager.GetValue<CallVirtualFromConstructorDerived>(ref r_self, JniObjectReferenceOptions.Copy);
if (self == null)
throw new InvalidOperationException ("calledFromConstructor received null self.");
self.CalledFromConstructor (value);
self.InvokedConstructor = true;
((IJavaPeerable) self).SetJniManagedPeerState (self.JniManagedPeerState | JniManagedPeerStates.Replaceable);
self.DisposeUnlessReferenced ();
}
catch (Exception e) when (JniEnvironment.Runtime.ExceptionShouldTransitionToJni (e)) {
envp.SetPendingException (e);
}
finally {
envp.Dispose ();
}
}
}

[AttributeUsage (AttributeTargets.Constructor | AttributeTargets.Method)]
sealed class RegisterAttribute : Attribute {
public RegisterAttribute (string name, string signature, string connector)
{
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package net.dot.jni.test;

import java.util.ArrayList;

import net.dot.jni.GCUserPeerable;

public class CallVirtualFromConstructorBase implements GCUserPeerable {

static {
registerNatives ();
}

ArrayList<Object> managedReferences = new ArrayList<Object>();

public CallVirtualFromConstructorBase (int value) {
if (CallVirtualFromConstructorBase.class == getClass ()) {
nctor_0 (value);
}
calledFromConstructor (value);
}

private native void nctor_0 (int value);

public void calledFromConstructor (int value) {
}

public void jiAddManagedReference (java.lang.Object obj)
{
managedReferences.add (obj);
}

public void jiClearManagedReferences ()
{
managedReferences.clear ();
}

static void registerNatives ()
{
try {
Class<?> runtime = Class.forName ("mono.android.Runtime");
java.lang.reflect.Method registerNatives = runtime.getMethod ("registerNatives", Class.class);
registerNatives.invoke (null, CallVirtualFromConstructorBase.class);
} catch (Exception e) {
throw new Error (e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package net.dot.jni.test;

import java.util.ArrayList;

import net.dot.jni.GCUserPeerable;

public class CallVirtualFromConstructorDerived
extends CallVirtualFromConstructorBase
implements GCUserPeerable
{
static {
registerNatives ();
}

ArrayList<Object> managedReferences = new ArrayList<Object>();
boolean calledFromConstructorInvoked;

public CallVirtualFromConstructorDerived (int value) {
super (value);
if (CallVirtualFromConstructorDerived.class == getClass () && !calledFromConstructorInvoked) {
nctor_0 (value);
}
}

public static CallVirtualFromConstructorDerived newInstance (int value)
{
return new CallVirtualFromConstructorDerived (value);
}

public void calledFromConstructor (int value) {
calledFromConstructorInvoked = true;
n_CalledFromConstructor (value);
}

public native void n_CalledFromConstructor (int value);

private native void nctor_0 (int value);

public void jiAddManagedReference (java.lang.Object obj)
{
managedReferences.add (obj);
}

public void jiClearManagedReferences ()
{
managedReferences.clear ();
}

static void registerNatives ()
{
try {
Class<?> runtime = Class.forName ("mono.android.Runtime");
java.lang.reflect.Method registerNatives = runtime.getMethod ("registerNatives", Class.class);
registerNatives.invoke (null, CallVirtualFromConstructorDerived.class);
} catch (Exception e) {
throw new Error (e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,6 @@ 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
"Java.InteropTests.InvokeVirtualFromConstructorTests",

// JNI method remapping not supported in trimmable typemap
"Java.InteropTests.JniPeerMembersTests.ReplaceInstanceMethodName",
"Java.InteropTests.JniPeerMembersTests.ReplaceInstanceMethodWithStaticMethod",
Expand Down