Skip to content
Open
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 @@ -10,6 +10,7 @@ namespace Java.InteropTests {
public class JniRuntimeJniTypeManagerTests : JavaVMFixture {

[Test]
[Category ("TrimmableTypeMapUnsupported")]
[RequiresDynamicCode ("This test uses ReflectionJniTypeManager, which is reflection-based and not NativeAOT-compatible.")]
[RequiresUnreferencedCode ("This test uses ReflectionJniTypeManager, which is reflection-based and not trimming-compatible.")]
public void GetInvokerType ()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap;
/// // or: null; // no activation
/// // or: throw new NotSupportedException(...); // open generic
///
/// // JniName / TargetType / InvokerType are supplied by the base JavaPeerProxy constructor.
/// // JniName / TargetType are supplied by the base JavaPeerProxy constructor.
///
/// // UCO wrappers — [UnmanagedCallersOnly] entry points for JNI native methods (ACWs only):
/// public static void n_OnCreate_uco_0(IntPtr jnienv, IntPtr self, IntPtr p0)
Expand Down Expand Up @@ -731,22 +731,20 @@ void EmitProxyType (JavaPeerProxyData proxy, Dictionary<UcoWrapperTargetData, Me
if (useNonGenericBase) {
proxyBaseType = _javaPeerProxyNonGenericRef;
baseCtorRef = _pe.AddMemberRef (_javaPeerProxyNonGenericRef, ".ctor",
sig => sig.MethodSignature (isInstanceMethod: true).Parameters (3,
sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2,
rt => rt.Void (),
p => {
p.AddParameter ().Type ().String ();
p.AddParameter ().Type ().Type (_systemTypeRef, false);
p.AddParameter ().Type ().Type (_systemTypeRef, false);
}));
} else {
var genericProxyBase = _pe.MakeGenericTypeSpec (_javaPeerProxyRef, targetTypeRef);
proxyBaseType = genericProxyBase;
baseCtorRef = _pe.AddMemberRef (genericProxyBase, ".ctor",
sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2,
sig => sig.MethodSignature (isInstanceMethod: true).Parameters (1,
rt => rt.Void (),
p => {
p.AddParameter ().Type ().String ();
p.AddParameter ().Type ().Type (_systemTypeRef, false);
}));
}

Expand All @@ -762,27 +760,22 @@ void EmitProxyType (JavaPeerProxyData proxy, Dictionary<UcoWrapperTargetData, Me
metadata.AddInterfaceImplementation (typeDefHandle, _iAndroidCallableWrapperRef);
}

// .ctor — pass the resolved JNI name, (for generic-definition base) target type, and
// optional invoker type to the base proxy constructor.
// .ctor — pass the resolved JNI name and (for proxies using the non-generic
// base, i.e. open generic definitions and interfaces) the target type
// to the base proxy constructor.
var selfAttrCtorDef = _pe.EmitBody (".ctor",
MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName,
sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { }),
encoder => {
encoder.OpCode (ILOpCode.Ldarg_0);
encoder.LoadString (metadata.GetOrAddUserString (proxy.JniName));
if (useNonGenericBase) {
// Non-generic base ctor signature: (string, Type, Type?). Push the
// Non-generic base ctor signature: (string, Type). Push the
// target type (open-generic definition or interface) as the second argument.
encoder.LoadToken (targetTypeRef);
encoder.Call (_getTypeFromHandleRef, parameterCount: 1, returnsValue: true);
}
if (proxy.InvokerType != null) {
encoder.LoadToken (_pe.ResolveTypeRef (proxy.InvokerType));
encoder.Call (_getTypeFromHandleRef, parameterCount: 1, returnsValue: true);
} else {
encoder.OpCode (ILOpCode.Ldnull);
}
encoder.Call (baseCtorRef, parameterCount: useNonGenericBase ? 3 : 2, isInstance: true);
encoder.Call (baseCtorRef, parameterCount: useNonGenericBase ? 2 : 1, isInstance: true);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 💡 Testing — This is the crux of the change: the generated .ctor now calls the base ctor with arity 2/1 instead of 3/2. But TypeMapAssemblyGeneratorTests only assert that a .ctor exists by name (Assert.Contains (".ctor", methods)) and that ctorRefs.Count >= 2 — none of them decode the emitted .ctor body. A base-ctor arity/token mismatch here would sail past metadata inspection and only surface as an InvalidProgramException/TypeLoadException on the CoreCLR device legs. There's already precedent for body-level decoding (AssertCreateInstanceReturnsNull, and the JI-ctor signature decode), so consider asserting that the emitted .ctor invokes the base ctor with the expected arity for both the generic (concrete) and non-generic (interface/open-generic) paths. Not blocking — the device legs cover it at runtime.

Rule: Generator tests should validate emitted IL bodies, not just member presence

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good call — addressed in e9647b2. Added Generate_ProxyCtor_ChainsToExpectedBaseConstructor, a [Theory] that decodes the emitted .ctor body and asserts it chains to the expected base ctor for all three paths:

  • concrete class → generic JavaPeerProxy<T> base, arity 1 (string), no GetTypeFromHandle;
  • interface → non-generic JavaPeerProxy base, arity 2 (string, Type), with a Type.GetTypeFromHandle call;
  • open-generic → same non-generic base, arity 2.

It resolves the call token to the base-ctor MemberRef and decodes its signature arity (reusing ReadCallTokens/TypeNameSignatureDecoder), so an arity/token mismatch now fails in-suite instead of only on the device legs. 608/608 passing.

encoder.Return ();
});

Expand Down
13 changes: 3 additions & 10 deletions src/Mono.Android/Java.Interop/JavaPeerProxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,13 @@ public sealed class JavaPeerAliasesAttribute : Attribute
[AttributeUsage (AttributeTargets.Class | AttributeTargets.Interface, Inherited = false, AllowMultiple = false)]
public abstract class JavaPeerProxy : Attribute
{
private protected JavaPeerProxy (string jniName, Type targetType, Type? invokerType)
private protected JavaPeerProxy (string jniName, Type targetType)
Comment thread
simonrozsival marked this conversation as resolved.
{
ArgumentNullException.ThrowIfNull (jniName);
ArgumentNullException.ThrowIfNull (targetType);

JniName = jniName;
TargetType = targetType;
InvokerType = invokerType;
}

/// <summary>
Expand All @@ -71,12 +70,6 @@ private protected JavaPeerProxy (string jniName, Type targetType, Type? invokerT
/// </summary>
public Type TargetType { get; }

/// <summary>
/// Gets the invoker type for interfaces and abstract classes.
/// Returns null for concrete types that can be directly instantiated.
/// </summary>
public Type? InvokerType { get; }

/// <summary>
/// Gets a factory for creating containers (arrays, collections) of the target type.
/// Enables AOT-safe creation of generic collections without <c>MakeGenericType()</c>.
Expand Down Expand Up @@ -144,8 +137,8 @@ public abstract class JavaPeerProxy<[DynamicallyAccessedMembers (Constructors)]
{
const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors;

protected JavaPeerProxy (string jniName, Type? invokerType)
: base (jniName, typeof (T), invokerType)
protected JavaPeerProxy (string jniName)
: base (jniName, typeof (T))
{
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -493,14 +493,6 @@ internal static bool TargetTypeMatches (Type targetType, Type proxyTargetType)
return targetType.IsAssignableFrom (proxyTargetType);
}

/// <summary>
/// Gets the invoker type for an interface or abstract class from the proxy attribute.
/// </summary>
internal Type? GetInvokerType (Type type)
{
return GetProxyForManagedType (type)?.InvokerType;
}

/// <summary>
/// Gets the container factory for a type from its proxy attribute.
/// Used for AOT-safe array/collection/dictionary creation.
Expand Down Expand Up @@ -612,7 +604,7 @@ static void OnRegisterNatives (IntPtr jnienv, IntPtr klass, IntPtr nativeClassHa

sealed class MissingJavaPeerProxy : JavaPeerProxy
{
public MissingJavaPeerProxy () : base ("<missing>", typeof (Java.Lang.Object), null)
public MissingJavaPeerProxy () : base ("<missing>", typeof (Java.Lang.Object))
{
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,60 @@ public void Generate_InterfaceProxyType_UsesNonGenericJavaPeerProxyBase ()
Assert.Equal ("JavaPeerProxy", reader.GetString (baseTypeRef.Name));
}

// Regression test: decode the emitted proxy `.ctor` *body* (not just member presence)
// and verify it chains to the correct base constructor. Concrete (constructible) proxies
// derive from `JavaPeerProxy<T>` and call the single-arg base ctor `(string)`; interface
// and open-generic proxies derive from the non-generic `JavaPeerProxy` and call the
// two-arg base ctor `(string, Type)` — pushing the target type via `Type.GetTypeFromHandle`.
// A base-ctor arity/token mismatch here would pass metadata inspection but blow up as an
// InvalidProgramException/TypeLoadException on the CoreCLR device legs, so assert it directly.
[Theory]
[InlineData ("Java_Lang_Object_Proxy", 1, false)] // concrete class -> generic base (string)
[InlineData ("Android_Views_IOnClickListener_Proxy", 2, true)] // interface -> non-generic base (string, Type)
[InlineData ("MyApp_Generic_GenericHolder_1_Proxy", 2, true)] // open generic -> non-generic base (string, Type)
public void Generate_ProxyCtor_ChainsToExpectedBaseConstructor (string proxyTypeName, int expectedBaseCtorArity, bool expectsGetTypeFromHandle)
{
var peers = ScanFixtures ();
using var stream = GenerateAssembly (peers);
using var pe = new PEReader (stream);
var reader = pe.GetMetadataReader ();

var proxyTypeHandle = reader.TypeDefinitions.First (h => {
var type = reader.GetTypeDefinition (h);
return reader.GetString (type.Namespace) == "_TypeMap.Proxies" &&
reader.GetString (type.Name) == proxyTypeName;
});
var proxyType = reader.GetTypeDefinition (proxyTypeHandle);
var ctorHandle = proxyType.GetMethods ().First (h =>
reader.GetString (reader.GetMethodDefinition (h).Name) == ".ctor");
var ctor = reader.GetMethodDefinition (ctorHandle);
var body = pe.GetMethodBody (ctor.RelativeVirtualAddress);
Assert.NotNull (body);
var ilBytes = body.GetILBytes ();
Assert.NotNull (ilBytes);

var decoder = new TypeNameSignatureDecoder (reader);
int? baseCtorArity = null;
bool sawGetTypeFromHandle = false;
foreach (var token in ReadCallTokens (ilBytes!)) {
var handle = MetadataTokens.EntityHandle (token);
if (handle.Kind != HandleKind.MemberReference)
continue;
var mref = reader.GetMemberReference ((MemberReferenceHandle) handle);
var name = reader.GetString (mref.Name);
if (name == "GetTypeFromHandle") {
sawGetTypeFromHandle = true;
} else if (name == ".ctor") {
var sig = mref.DecodeMethodSignature (decoder, genericContext: null);
baseCtorArity = sig.RequiredParameterCount;
}
}

Assert.True (baseCtorArity.HasValue, $"Proxy '{proxyTypeName}' .ctor must chain to a base ctor");
Assert.Equal (expectedBaseCtorArity, baseCtorArity!.Value);
Assert.Equal (expectsGetTypeFromHandle, sawGetTypeFromHandle);
}

// Regression test: every generated proxy type must carry a custom attribute whose
// constructor points at the proxy's own TypeDefinitionHandle (either as a MemberRef
// parented on the TypeDef, or as a MethodDefinition on the TypeDef). This is how
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,15 @@ public void Constructor_StoresJniNameAndTargetType ()

Assert.AreEqual ("custom/ExplicitName", proxy.JniName);
Assert.AreEqual (typeof (ProxyTestPeer), proxy.TargetType);
Assert.IsNull (proxy.InvokerType);
}

[Test]
public void Constructor_StoresInvokerType ()
public void GenericConstructor_StoresJniNameAndTargetType ()
{
var proxy = new InvokerProxy ();
var proxy = new GenericProxy ();

Assert.AreEqual ("custom/InvokerProxy", proxy.JniName);
Assert.AreEqual ("custom/GenericProxy", proxy.JniName);
Assert.AreEqual (typeof (ProxyTestPeer), proxy.TargetType);
Assert.AreEqual (typeof (ProxyTestPeerInvoker), proxy.InvokerType);
}
}

Expand All @@ -45,32 +43,20 @@ public ProxyTestPeer (IntPtr handle, JniHandleOwnership transfer)
}
}

sealed class ProxyTestPeerInvoker : Java.Lang.Object
{
public ProxyTestPeerInvoker ()
{
}

public ProxyTestPeerInvoker (IntPtr handle, JniHandleOwnership transfer)
: base (handle, transfer)
{
}
}

sealed class ExplicitNameProxy : JavaPeerProxy
{
public ExplicitNameProxy ()
: base ("custom/ExplicitName", typeof (ProxyTestPeer), invokerType: null)
: base ("custom/ExplicitName", typeof (ProxyTestPeer))
{
}

public override IJavaPeerable? CreateInstance (IntPtr handle, JniHandleOwnership transfer) => null;
}

sealed class InvokerProxy : JavaPeerProxy<ProxyTestPeer>
sealed class GenericProxy : JavaPeerProxy<ProxyTestPeer>
{
public InvokerProxy ()
: base ("custom/InvokerProxy", typeof (ProxyTestPeerInvoker))
public GenericProxy ()
: base ("custom/GenericProxy")
{
}

Expand Down
Loading