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 @@ -125,9 +125,15 @@ static void WriteStaticInitializer (JavaPeerInfo type, TextWriter writer)
string className = JniSignatureHelper.GetJavaSimpleName (type.JavaName);

// Application and Instrumentation types cannot call registerNatives in their
// static initializer — the runtime isn't ready yet at that point. Emit a
// lazy one-time helper instead so the first managed callback can register
// the class just before invoking its native method.
// static initializer — the runtime isn't ready yet at that point. Instead we
// rely on ApplicationRegistration.registerApplications() (invoked from
// MonoPackageManager.LoadApplication, which runs from MonoRuntimeProvider.attachInfo)
// to register natives for these classes. However, Application.attachBaseContext
// fires *before* ContentProvider.attachInfo, so an override of attachBaseContext
// on a user Application subclass can invoke a native callback before
// registerApplications() has run. To cover that early-callback window we also
// emit a lazy one-time __md_registerNatives() helper and call it at the top of
// each generated native-method wrapper.
if (type.CannotRegisterInStaticConstructor) {
writer.Write ($$"""
private static boolean __md_natives_registered;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,9 @@ static void EmitPeers (TypeMapAssemblyData model, string jniName,

// Emit TypeMapAssociation for all proxy-backed types so managed → proxy
// lookup works even when the final JNI name differs from the type's attributes.
// Generic definitions are included — their proxy types derive from the
// non-generic `JavaPeerProxy` base so the CLR can load them without
// resolving an open generic argument.
var assocProxy = (i > 0 && primaryProxy != null) ? primaryProxy : proxy;
if (assocProxy != null) {
model.Associations.Add (new TypeMapAssociationData {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ sealed class TypeMapAssemblyEmitter
AssemblyReferenceHandle _javaInteropRef;

TypeReferenceHandle _javaPeerProxyRef;
TypeReferenceHandle _javaPeerProxyNonGenericRef;
TypeReferenceHandle _iJavaPeerableRef;
TypeReferenceHandle _jniHandleOwnershipRef;
TypeReferenceHandle _jniObjectReferenceRef;
Expand Down Expand Up @@ -165,6 +166,8 @@ void EmitTypeReferences ()
var metadata = _pe.Metadata;
_javaPeerProxyRef = metadata.AddTypeReference (_pe.MonoAndroidRef,
metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JavaPeerProxy`1"));
_javaPeerProxyNonGenericRef = metadata.AddTypeReference (_pe.MonoAndroidRef,
metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JavaPeerProxy"));
_iJavaPeerableRef = metadata.AddTypeReference (_javaInteropRef,
metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("IJavaPeerable"));
_jniHandleOwnershipRef = metadata.AddTypeReference (_pe.MonoAndroidRef,
Expand Down Expand Up @@ -352,14 +355,36 @@ void EmitProxyType (JavaPeerProxyData proxy, Dictionary<string, MethodDefinition

var metadata = _pe.Metadata;
var targetTypeRef = _pe.ResolveTypeRef (proxy.TargetType);
var proxyBaseType = _pe.MakeGenericTypeSpec (_javaPeerProxyRef, targetTypeRef);
var baseCtorRef = _pe.AddMemberRef (proxyBaseType, ".ctor",
sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2,
rt => rt.Void (),
p => {
p.AddParameter ().Type ().String ();
p.AddParameter ().Type ().Type (_systemTypeRef, false);
}));

// Open generic definitions derive from the non-generic `JavaPeerProxy` abstract base.
// Using `JavaPeerProxy<T>` with an open T would force the CLR to resolve a generic
// argument that isn't available via the TypeMapLazyDictionary loader, and using a
// placeholder like `Java.Lang.Object` leaks an incorrect TargetType into the typemap.
// The non-generic base takes `targetType` as a ctor parameter, so we can pass the real
// open-generic type token (a TypeRef, not a closed TypeSpec) and keep TargetType correct.
EntityHandle proxyBaseType;
MemberReferenceHandle baseCtorRef;
if (proxy.IsGenericDefinition) {
proxyBaseType = _javaPeerProxyNonGenericRef;
baseCtorRef = _pe.AddMemberRef (_javaPeerProxyNonGenericRef, ".ctor",
sig => sig.MethodSignature (isInstanceMethod: true).Parameters (3,
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,
rt => rt.Void (),
p => {
p.AddParameter ().Type ().String ();
p.AddParameter ().Type ().Type (_systemTypeRef, false);
}));
}

var typeDefHandle = metadata.AddTypeDefinition (
TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.Class,
Expand All @@ -373,13 +398,29 @@ void EmitProxyType (JavaPeerProxyData proxy, Dictionary<string, MethodDefinition
metadata.AddInterfaceImplementation (typeDefHandle, _iAndroidCallableWrapperRef);
}

// .ctor — pass the resolved JNI name and optional invoker type to the generic base proxy
// Self-apply: the proxy type is its own [JavaPeerProxy] attribute.
// This enables type.GetCustomAttribute<JavaPeerProxy>() to instantiate the proxy
// at runtime for AOT-safe type resolution.
var selfAttrCtorRef = _pe.AddMemberRef (typeDefHandle, ".ctor",
sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { }));
var selfAttrBlob = _pe.BuildAttributeBlob (b => { });
metadata.AddCustomAttribute (typeDefHandle, selfAttrCtorRef, selfAttrBlob);

// .ctor — pass the resolved JNI name, (for generic-definition base) target type, and
// optional invoker type to the base proxy constructor.
_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 (proxy.IsGenericDefinition) {
// Non-generic base ctor signature: (string, Type, Type?). Push the open-generic
// target type as the second argument.
encoder.OpCode (ILOpCode.Ldtoken);
encoder.Token (targetTypeRef);
encoder.Call (_getTypeFromHandleRef);
}
if (proxy.InvokerType != null) {
encoder.OpCode (ILOpCode.Ldtoken);
encoder.Token (_pe.ResolveTypeRef (proxy.InvokerType));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public TrimmableTypeMapResult Execute (

// Collect Application/Instrumentation types that need deferred registerNatives
var appRegTypes = allPeers
.Where (p => p.CannotRegisterInStaticConstructor && !p.IsAbstract)
.Where (p => p.CannotRegisterInStaticConstructor && !p.DoNotGenerateAcw)
.Select (p => JniSignatureHelper.JniNameToJavaName (p.JavaName))
.ToList ();
if (appRegTypes.Count > 0) {
Expand Down
84 changes: 78 additions & 6 deletions src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,16 @@ internal static void Initialize ()

unsafe void RegisterNatives ()
{
using var runtimeClass = new JniType ("mono/android/Runtime"u8);
// Use the `string` overload of `JniType` deliberately. Its underlying
// `JniEnvironment.Types.TryFindClass(string, bool)` tries raw JNI `FindClass`
// first and, if that fails, falls back to `Class.forName(name, true, info.Runtime.ClassLoader)`,
// which resolves via the runtime's app ClassLoader — the same one that loads
// `mono.android.Runtime` from the APK.
// The `ReadOnlySpan<byte>` overload (see external/Java.Interop/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs)
// only calls raw JNI `FindClass`, which resolves via the system ClassLoader on
// Android and returns a different `Class` instance from the one JCWs reference.
// Registering natives on that other instance is silently wrong.
using var runtimeClass = new JniType ("mono/android/Runtime");
fixed (byte* name = "registerNatives"u8, sig = "(Ljava/lang/Class;)V"u8) {
var onRegisterNatives = (IntPtr)(delegate* unmanaged<IntPtr, IntPtr, IntPtr, void>)&OnRegisterNatives;
var method = new JniNativeMethod (name, sig, onRegisterNatives);
Expand All @@ -73,14 +82,34 @@ internal bool TryGetTargetType (string jniSimpleReference, [NotNullWhen (true)]
return type is not null;
}

/// <summary>
/// Resolves the <see cref="JavaPeerProxy"/> for a managed type via the CLR
/// <c>TypeMapping</c> proxy dictionary.
/// </summary>
/// <remarks>
/// The generator emits exactly one <c>TypeMapAssociation</c> per generic peer,
/// keyed by the open generic definition (Java erases generics, so one proxy
/// fits every closed instantiation). Closed instantiations are normalised to
/// their generic type definition before the lookup because the CLR lazy
/// dictionary does identity-based key matching
/// (see <c>dotnet/runtime</c> <c>TypeMapLazyDictionary.cs</c>).
/// <see cref="Type.GetGenericTypeDefinition"/> is safe under full AOT + trim
/// (it is not <c>RequiresDynamicCode</c>). Java→managed construction of a
/// closed generic peer still requires a closed <see cref="Type"/> at the call
/// site and is tracked separately.
/// </remarks>
JavaPeerProxy? GetProxyForManagedType (Type managedType)
{
if (managedType.IsGenericType && !managedType.IsGenericTypeDefinition) {
managedType = managedType.GetGenericTypeDefinition ();
}

var proxy = _proxyCache.GetOrAdd (managedType, static (type, self) => {
if (!self._proxyTypeMap.TryGetValue (type, out var proxyType)) {
return s_noPeerSentinel;
if (self._proxyTypeMap.TryGetValue (type, out var proxyType)) {
return proxyType.GetCustomAttribute<JavaPeerProxy> (inherit: false) ?? s_noPeerSentinel;
}

return proxyType.GetCustomAttribute<JavaPeerProxy> (inherit: false) ?? s_noPeerSentinel;
return s_noPeerSentinel;
}, this);
return ReferenceEquals (proxy, s_noPeerSentinel) ? null : proxy;
}
Expand Down Expand Up @@ -130,7 +159,7 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true)
var className = JniEnvironment.Types.GetJniTypeNameFromClass (jniClass);
if (className != null) {
var proxy = self.GetProxyForJavaType (className);
if (proxy != null && (targetType is null || targetType.IsAssignableFrom (proxy.TargetType))) {
if (proxy != null && (targetType is null || TargetTypeMatches (targetType, proxy.TargetType))) {
return proxy;
}
}
Expand Down Expand Up @@ -176,6 +205,47 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true)

const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors;

/// <summary>
/// Match the proxy's stored target type against a hint from the caller.
/// The proxy's target type is the open generic definition for generic peers
/// (Java erases generics, so one proxy fits every closed instantiation),
/// so a plain <see cref="Type.IsAssignableFrom"/> check misses when the hint
/// is a closed instantiation. Walk the hint's base chain to find a generic
/// type whose definition equals the proxy's open target type. This covers
/// closed subclasses of an open generic class peer.
/// </summary>
/// <remarks>
/// Implementers of an open generic <em>interface</em> peer are intentionally
/// not matched here: <see cref="TryGetProxyFromHierarchy"/> walks only the
/// JNI class chain (<c>getSuperclass</c>), never JNI interfaces, so the
/// proxy returned from that walk is always a class peer. Matching on
/// <c>Type.GetInterfaces()</c> would also force a trimmer
/// <c>DynamicallyAccessedMembers(Interfaces)</c> annotation up the chain
/// (ultimately into Java.Interop's <c>CreatePeer</c> API). If we ever need
/// to discover interface peers, the generator should emit an explicit
/// implementer→interface map so runtime can avoid reflection over
/// interface lists.
/// </remarks>
internal static bool TargetTypeMatches (Type targetType, Type proxyTargetType)
{
if (targetType.IsAssignableFrom (proxyTargetType)) {
return true;
}

if (!proxyTargetType.IsGenericTypeDefinition) {
return false;
}

for (Type? t = targetType; t is not null; t = t.BaseType) {
if (t.IsGenericType && !t.IsGenericTypeDefinition &&
t.GetGenericTypeDefinition () == proxyTargetType) {
return true;
}
}

return false;
}

/// <summary>
/// Gets the invoker type for an interface or abstract class from the proxy attribute.
/// </summary>
Expand Down Expand Up @@ -215,7 +285,9 @@ static void OnRegisterNatives (IntPtr jnienv, IntPtr klass, IntPtr nativeClassHa

var proxy = type.GetCustomAttribute<JavaPeerProxy> (inherit: false);
if (proxy is IAndroidCallableWrapper acw) {
using var jniType = new JniType (className);
// Use the class reference passed from Java (via C++) — not JniType(className)
// which resolves via FindClass and may get a different class from a different ClassLoader.
using var jniType = new JniType (ref classRef, JniObjectReferenceOptions.Copy);
acw.RegisterNatives (jniType);
}
} catch (Exception ex) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,11 +172,4 @@
</ItemGroup>
</Target>

<!-- Override _RemoveRegisterAttribute: trimmable types need [Register] attributes
at runtime for type resolution (TryGetJniNameForType uses IJniNameProviderAttribute).
The legacy target strips them to reduce assembly size, but the trimmable path
requires them. _ShrunkAssemblies is already set to _ResolvedAssemblies by
AssemblyResolution.targets, so no copy is needed. -->
<Target Name="_RemoveRegisterAttribute" />

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public override bool RunTask ()
ResolvedSymbols = symbols.Values.ToArray ();

// Set ShrunkAssemblies for _RemoveRegisterAttribute and <BuildApk/>
// This should match the Condition on the _RemoveRegisterAttribute target
// This should match the Condition on the _RemoveRegisterAttribute target.
if (PublishTrimmed) {
if (!AndroidIncludeDebugSymbols) {
var shrunkAssemblies = new List<ITaskItem> (OutputAssemblies.Length);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,16 +73,19 @@ public void Execute_WithTestFixtures_ProducesOutputs ()
}

[Fact]
public void Execute_CollectsDeferredRegistrationTypes_ForConcreteApplicationAndInstrumentation ()
public void Execute_CollectsDeferredRegistrationTypes_ForAllApplicationAndInstrumentationSubtypes ()
{
using var peReader = CreateTestFixturePEReader ();
var result = CreateGenerator ().Execute (new List<(string, PEReader)> { ("TestFixtures", peReader) }, new Version (11, 0), new HashSet<string> ());

// Abstract Instrumentation/Application subtypes are included too: their native
// methods (e.g. n_OnCreate, n_OnStart) are declared on the abstract base class
// and must be registered via ApplicationRegistration.registerApplications ().
Comment thread
simonrozsival marked this conversation as resolved.
Assert.Contains ("my.app.MyApplication", result.ApplicationRegistrationTypes);
Assert.Contains ("my.app.MyInstrumentation", result.ApplicationRegistrationTypes);
Assert.DoesNotContain ("my.app.BaseApplication", result.ApplicationRegistrationTypes);
Assert.DoesNotContain ("my.app.BaseInstrumentation", result.ApplicationRegistrationTypes);
Assert.DoesNotContain ("my.app.IntermediateInstrumentation", result.ApplicationRegistrationTypes);
Assert.Contains ("my.app.BaseApplication", result.ApplicationRegistrationTypes);
Assert.Contains ("my.app.BaseInstrumentation", result.ApplicationRegistrationTypes);
Assert.Contains ("my.app.IntermediateInstrumentation", result.ApplicationRegistrationTypes);
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,23 @@ public void Generate_ProxyType_UsesGenericJavaPeerProxyBase ()

Assert.NotEmpty (proxyTypes);
Assert.All (proxyTypes, proxyType => {
Assert.Equal (HandleKind.TypeSpecification, proxyType.BaseType.Kind);

var baseTypeSpec = reader.GetTypeSpecification ((TypeSpecificationHandle) proxyType.BaseType);
var baseTypeName = baseTypeSpec.DecodeSignature (SignatureTypeProvider.Instance, genericContext: null);

Assert.StartsWith ("Java.Interop.JavaPeerProxy`1<", baseTypeName, StringComparison.Ordinal);
switch (proxyType.BaseType.Kind) {
case HandleKind.TypeSpecification:
// Non-generic target types derive from the closed `JavaPeerProxy<T>`.
var baseTypeSpec = reader.GetTypeSpecification ((TypeSpecificationHandle) proxyType.BaseType);
var baseTypeName = baseTypeSpec.DecodeSignature (SignatureTypeProvider.Instance, genericContext: null);
Assert.StartsWith ("Java.Interop.JavaPeerProxy`1<", baseTypeName, StringComparison.Ordinal);
break;
case HandleKind.TypeReference:
// Open generic target types derive from the non-generic `JavaPeerProxy`.
var baseTypeRef = reader.GetTypeReference ((TypeReferenceHandle) proxyType.BaseType);
Assert.Equal ("Java.Interop", reader.GetString (baseTypeRef.Namespace));
Assert.Equal ("JavaPeerProxy", reader.GetString (baseTypeRef.Name));
break;
default:
Assert.Fail ($"Unexpected BaseType handle kind: {proxyType.BaseType.Kind}");
break;
}
});

var objectProxy = proxyTypes.First (t => reader.GetString (t.Name) == "Java_Lang_Object_Proxy");
Expand All @@ -112,6 +123,62 @@ public void Generate_ProxyType_UsesGenericJavaPeerProxyBase ()
objectProxyBaseType.DecodeSignature (SignatureTypeProvider.Instance, genericContext: null));
}

// 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
// JavaPeerProxy instances are resolved at runtime via
// type.GetCustomAttribute<JavaPeerProxy>() — losing the self-application means the
// runtime can't construct the proxy. This has regressed twice; keep it covered.
[Fact]
public void Generate_ProxyType_IsSelfAppliedAsCustomAttribute ()
{
var peers = ScanFixtures ();
using var stream = GenerateAssembly (peers);
using var pe = new PEReader (stream);
var reader = pe.GetMetadataReader ();

var proxyTypeHandles = reader.TypeDefinitions
.Where (h => reader.GetString (reader.GetTypeDefinition (h).Namespace) == "_TypeMap.Proxies")
.ToList ();

Assert.NotEmpty (proxyTypeHandles);

foreach (var proxyHandle in proxyTypeHandles) {
var proxy = reader.GetTypeDefinition (proxyHandle);
var proxyName = reader.GetString (proxy.Name);

bool selfApplied = false;
foreach (var caHandle in proxy.GetCustomAttributes ()) {
var ca = reader.GetCustomAttribute (caHandle);

switch (ca.Constructor.Kind) {
case HandleKind.MemberReference:
var ctorRef = reader.GetMemberReference ((MemberReferenceHandle) ca.Constructor);
if (ctorRef.Parent.Kind == HandleKind.TypeDefinition &&
(TypeDefinitionHandle) ctorRef.Parent == proxyHandle) {
selfApplied = true;
}
break;
case HandleKind.MethodDefinition:
var ctorDef = reader.GetMethodDefinition ((MethodDefinitionHandle) ca.Constructor);
if (ctorDef.GetDeclaringType () == proxyHandle) {
selfApplied = true;
}
break;
}

if (selfApplied) {
break;
}
}

Assert.True (selfApplied,
$"Proxy type '{proxyName}' is missing its self-applied custom attribute. " +
"Every proxy must carry itself as a [JavaPeerProxy] attribute so the runtime " +
"can instantiate it via Type.GetCustomAttribute<JavaPeerProxy> ().");
}
}

[Fact]
public void Generate_HasIgnoresAccessChecksToAttribute ()
{
Expand Down
Loading