Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -121,8 +121,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 @@ -156,8 +157,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);

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") {
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;

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