Skip to content
Merged
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 @@ -182,7 +182,7 @@ sealed record UcoMethodData
/// An [UnmanagedCallersOnly] static wrapper for a constructor callback.
/// Signature must match the full JNI native method signature (jnienv + self + ctor params)
/// so the ABI is correct when JNI dispatches the call.
/// Body: TrimmableNativeRegistration.ActivateInstance(self, typeof(TargetType)).
/// Body: directly activates the target type using its generated activation ctor.
/// </summary>
sealed record UcoConstructorData
{
Expand All @@ -192,7 +192,7 @@ sealed record UcoConstructorData
public required string WrapperName { get; init; }

/// <summary>
/// Target type to pass to ActivateInstance.
/// Target type to activate in the generated wrapper.
/// </summary>
public required TypeRefData TargetType { get; init; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ TypeDefinitionHandle GetOrCreateSizedType (int size)
int typeMethodStart = Metadata.GetRowCount (TableIndex.MethodDef) + 1;

var handle = Metadata.AddTypeDefinition (
TypeAttributes.NestedPrivate | TypeAttributes.ExplicitLayout | TypeAttributes.Sealed | TypeAttributes.AnsiClass,
TypeAttributes.NestedAssembly | TypeAttributes.ExplicitLayout | TypeAttributes.Sealed | TypeAttributes.AnsiClass,
default,
Metadata.GetOrAddString ($"__utf8_{size}"),
Metadata.AddTypeReference (SystemRuntimeRef,
Expand All @@ -259,7 +259,7 @@ TypeDefinitionHandle GetOrCreateSizedType (int size)
/// </summary>
public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs,
Action<BlobEncoder> encodeSig, Action<InstructionEncoder> emitIL)
=> EmitBody (name, attrs, encodeSig, emitIL, encodeLocals: null);
=> EmitBody (name, attrs, encodeSig, emitIL, encodeLocals: null, useBranches: false);

/// <summary>
/// Emits a method body and definition with optional local variable declarations.
Expand All @@ -269,9 +269,19 @@ public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs,
/// <see cref="BlobBuilder"/> and must write the full <c>LOCAL_SIG</c> blob (header 0x07,
/// compressed count, then each variable type).
/// </param>
/// <param name="useBranches">
/// If true, creates a <see cref="ControlFlowBuilder"/> so the emitted IL can use
/// <see cref="InstructionEncoder.DefineLabel"/>, <see cref="InstructionEncoder.Branch"/>,
/// and <see cref="InstructionEncoder.MarkLabel"/>.
/// </param>
public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs,
Action<BlobEncoder> encodeSig, Action<InstructionEncoder> emitIL,
Action<BlobBuilder>? encodeLocals)
=> EmitBody (name, attrs, encodeSig, emitIL, encodeLocals, useBranches: false);

public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs,
Action<BlobEncoder> encodeSig, Action<InstructionEncoder> emitIL,
Action<BlobBuilder>? encodeLocals, bool useBranches)
{
_sigBlob.Clear ();
encodeSig (new BlobEncoder (_sigBlob));
Expand All @@ -287,7 +297,8 @@ public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs,
}

_codeBlob.Clear ();
var encoder = new InstructionEncoder (_codeBlob);
ControlFlowBuilder? cfb = useBranches ? new ControlFlowBuilder () : null;
var encoder = new InstructionEncoder (_codeBlob, cfb);
emitIL (encoder);

while (ILBuilder.Count % 4 != 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap;
///
/// [UnmanagedCallersOnly]
/// public static void nctor_0_uco(IntPtr jnienv, IntPtr self)
/// =&gt; TrimmableNativeRegistration.ActivateInstance(self, typeof(Activity));
/// =&gt; new Activity(self, JniHandleOwnership.DoNotTransfer);
/// // or: var obj = (Activity)RuntimeHelpers.GetUninitializedObject(typeof(Activity));
/// // obj.BaseCtor(self, JniHandleOwnership.DoNotTransfer);
///
/// // Registers JNI native methods (ACWs only):
/// public void RegisterNatives(JniType jniType)
Expand Down Expand Up @@ -77,7 +79,6 @@ sealed class TypeMapAssemblyEmitter
TypeReferenceHandle _systemTypeRef;
TypeReferenceHandle _runtimeTypeHandleRef;
TypeReferenceHandle _jniTypeRef;
TypeReferenceHandle _trimmableNativeRegistrationRef;
TypeReferenceHandle _notSupportedExceptionRef;
TypeReferenceHandle _runtimeHelpersRef;

Expand All @@ -87,7 +88,7 @@ sealed class TypeMapAssemblyEmitter
MemberReferenceHandle _notSupportedExceptionCtorRef;
MemberReferenceHandle _jniObjectReferenceCtorRef;
MemberReferenceHandle _jniEnvDeleteRefRef;
MemberReferenceHandle _activateInstanceRef;
MemberReferenceHandle _withinNewObjectScopeRef;
MemberReferenceHandle _ucoAttrCtorRef;
BlobHandle _ucoAttrBlobHandle;
MemberReferenceHandle _typeMapAttrCtorRef2Arg;
Expand Down Expand Up @@ -184,8 +185,6 @@ void EmitTypeReferences ()
metadata.GetOrAddString ("System"), metadata.GetOrAddString ("RuntimeTypeHandle"));
_jniTypeRef = metadata.AddTypeReference (_javaInteropRef,
metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniType"));
_trimmableNativeRegistrationRef = metadata.AddTypeReference (_pe.MonoAndroidRef,
metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("TrimmableNativeRegistration"));
_notSupportedExceptionRef = metadata.AddTypeReference (_pe.SystemRuntimeRef,
metadata.GetOrAddString ("System"), metadata.GetOrAddString ("NotSupportedException"));
_runtimeHelpersRef = metadata.AddTypeReference (_pe.SystemRuntimeRef,
Expand Down Expand Up @@ -240,13 +239,11 @@ void EmitMemberReferences ()
p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true);
}));

_activateInstanceRef = _pe.AddMemberRef (_trimmableNativeRegistrationRef, "ActivateInstance",
sig => sig.MethodSignature ().Parameters (2,
rt => rt.Void (),
p => {
p.AddParameter ().Type ().IntPtr ();
p.AddParameter ().Type ().Type (_systemTypeRef, false);
}));
// JniEnvironment.get_WithinNewObjectScope() -> bool (static property)
_withinNewObjectScopeRef = _pe.AddMemberRef (_jniEnvironmentRef, "get_WithinNewObjectScope",
sig => sig.MethodSignature ().Parameters (0,
rt => rt.Type ().Boolean (),
p => { }));

// JniNativeMethod..ctor(byte*, byte*, IntPtr)
_jniNativeMethodCtorRef = _pe.AddMemberRef (_jniNativeMethodRef, ".ctor",
Expand Down Expand Up @@ -347,6 +344,16 @@ void EmitTypeMapAssociationAttributeCtorRef ()

void EmitProxyType (JavaPeerProxyData proxy, Dictionary<string, MethodDefinitionHandle> wrapperHandles)
{
if (proxy.IsAcw) {
// RegisterNatives uses RVA-backed UTF-8 fields under <PrivateImplementationDetails>.
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.

🤖 💡 Testing — this eager GetOrAddUtf8Field() loop is fixing a very subtle metadata-ordering problem in the generated assembly. Could we strengthen Generate_AcwProxy_HasRegisterNativesAndUcoMethods() to assert that RegisterNatives is owned by the proxy type, not just present somewhere in MethodDefinitions? That would lock in the exact behavior this workaround depends on and make future cleanup safer.

{Rule: Missing regression tests for subtle generator fixes}

// Materialize those helper types before adding the proxy TypeDef, otherwise the
// later RegisterNatives method can be attached to the helper type instead.
foreach (var reg in proxy.NativeRegistrations) {
_pe.GetOrAddUtf8Field (reg.JniMethodName);
_pe.GetOrAddUtf8Field (reg.JniSignature);
}
}

var metadata = _pe.Metadata;
var typeDefHandle = metadata.AddTypeDefinition (
TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.Class,
Expand Down Expand Up @@ -390,7 +397,7 @@ void EmitProxyType (JavaPeerProxyData proxy, Dictionary<string, MethodDefinition
}

foreach (var uco in proxy.UcoConstructors) {
var handle = EmitUcoConstructor (uco);
var handle = EmitUcoConstructor (uco, proxy);
wrapperHandles [uco.WrapperName] = handle;
}

Expand Down Expand Up @@ -695,39 +702,123 @@ MethodDefinitionHandle EmitUcoMethod (UcoMethodData uco)
return handle;
}

MethodDefinitionHandle EmitUcoConstructor (UcoConstructorData uco)
MethodDefinitionHandle EmitUcoConstructor (UcoConstructorData uco, JavaPeerProxyData proxy)
{
var userTypeRef = _pe.ResolveTypeRef (uco.TargetType);
var targetTypeRef = _pe.ResolveTypeRef (uco.TargetType);
var activationCtor = proxy.ActivationCtor ?? throw new InvalidOperationException (
$"UCO constructor wrapper requires an activation ctor for '{uco.TargetType.ManagedTypeName}'");

// UCO constructor wrappers must match the JNI native method signature exactly.
// The Java JCW declares e.g. "private native void nctor_0(Context p0)" and calls
// it with arguments. JNI dispatches with (JNIEnv*, jobject, <ctor params...>),
// so the wrapper signature must include all parameters to match the ABI.
// Only jnienv (arg 0) and self (arg 1) are used — the constructor parameters
// are not forwarded because ActivateInstance creates the managed peer using the
// are not forwarded because we create the managed peer using the
// activation ctor (IntPtr, JniHandleOwnership), not the user-visible constructor.
var jniParams = JniSignatureHelper.ParseParameterTypes (uco.JniSignature);
int paramCount = 2 + jniParams.Count;

var handle = _pe.EmitBody (uco.WrapperName,
MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig,
sig => sig.MethodSignature ().Parameters (paramCount,
rt => rt.Void (),
p => {
p.AddParameter ().Type ().IntPtr (); // jnienv
p.AddParameter ().Type ().IntPtr (); // self
for (int j = 0; j < jniParams.Count; j++)
JniSignatureHelper.EncodeClrType (p.AddParameter ().Type (), jniParams [j]);
}),
encoder => {
encoder.LoadArgument (1); // self
encoder.OpCode (ILOpCode.Ldtoken);
encoder.Token (userTypeRef);
encoder.Call (_getTypeFromHandleRef);
encoder.Call (_activateInstanceRef);
encoder.OpCode (ILOpCode.Ret);
Action<BlobEncoder> encodeSig = sig => sig.MethodSignature ().Parameters (paramCount,
rt => rt.Void (),
p => {
p.AddParameter ().Type ().IntPtr (); // jnienv
p.AddParameter ().Type ().IntPtr (); // self
for (int j = 0; j < jniParams.Count; j++)
JniSignatureHelper.EncodeClrType (p.AddParameter ().Type (), jniParams [j]);
});

// Open generic types can't be activated — emit a no-op UCO.
if (proxy.IsGenericDefinition) {
var noopHandle = _pe.EmitBody (uco.WrapperName,
MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig,
encodeSig,
encoder => {
encoder.OpCode (ILOpCode.Ret);
});
AddUnmanagedCallersOnlyAttribute (noopHandle);
return noopHandle;
}

MethodDefinitionHandle handle;
if (activationCtor.Style == ActivationCtorStyle.JavaInterop) {
var ctorRef = AddJavaInteropActivationCtorRef (
activationCtor.IsOnLeafType ? targetTypeRef : _pe.ResolveTypeRef (activationCtor.DeclaringType));

handle = _pe.EmitBody (uco.WrapperName,
MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig,
encodeSig,
encoder => {
// Skip activation if the object is being created from managed code
// (e.g., JNIEnv.StartCreateInstance / JNIEnv.NewObject).
var skipLabel = encoder.DefineLabel ();
encoder.Call (_withinNewObjectScopeRef);
encoder.Branch (ILOpCode.Brtrue, skipLabel);

if (!activationCtor.IsOnLeafType) {
encoder.OpCode (ILOpCode.Ldtoken);
encoder.Token (targetTypeRef);
encoder.Call (_getTypeFromHandleRef);
encoder.Call (_getUninitializedObjectRef);
encoder.OpCode (ILOpCode.Castclass);
encoder.Token (targetTypeRef);
}

encoder.LoadLocalAddress (0);
encoder.LoadArgument (1); // self
encoder.Call (_jniObjectReferenceCtorRef);

if (activationCtor.IsOnLeafType) {
encoder.LoadLocalAddress (0);
encoder.LoadConstantI4 (1); // JniObjectReferenceOptions.Copy
encoder.OpCode (ILOpCode.Newobj);
encoder.Token (ctorRef);
encoder.OpCode (ILOpCode.Pop);
} else {
encoder.LoadLocalAddress (0);
encoder.LoadConstantI4 (1); // JniObjectReferenceOptions.Copy
encoder.Call (ctorRef);
}

encoder.MarkLabel (skipLabel);
encoder.OpCode (ILOpCode.Ret);
},
EncodeJniObjectReferenceLocal,
useBranches: true);
} else {
var ctorRef = AddActivationCtorRef (
activationCtor.IsOnLeafType ? targetTypeRef : _pe.ResolveTypeRef (activationCtor.DeclaringType));

handle = _pe.EmitBody (uco.WrapperName,
MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig,
encodeSig,
encoder => {
// Skip activation if the object is being created from managed code
var skipLabel = encoder.DefineLabel ();
encoder.Call (_withinNewObjectScopeRef);
encoder.Branch (ILOpCode.Brtrue, skipLabel);

if (activationCtor.IsOnLeafType) {
encoder.LoadArgument (1); // self
encoder.LoadConstantI4 (0); // JniHandleOwnership.DoNotTransfer
encoder.OpCode (ILOpCode.Newobj);
encoder.Token (ctorRef);
encoder.OpCode (ILOpCode.Pop);
} else {
encoder.OpCode (ILOpCode.Ldtoken);
encoder.Token (targetTypeRef);
encoder.Call (_getTypeFromHandleRef);
encoder.Call (_getUninitializedObjectRef);
encoder.OpCode (ILOpCode.Castclass);
encoder.Token (targetTypeRef);

encoder.LoadArgument (1); // self
encoder.LoadConstantI4 (0); // JniHandleOwnership.DoNotTransfer
encoder.Call (ctorRef);
}

encoder.MarkLabel (skipLabel);
encoder.OpCode (ILOpCode.Ret);
},
encodeLocals: null,
useBranches: true);
}
AddUnmanagedCallersOnlyAttribute (handle);
return handle;
}
Expand Down
34 changes: 1 addition & 33 deletions src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -147,39 +147,6 @@ internal bool TryCreatePeer (Type type, IntPtr handle, JniHandleOwnership transf
return GetProxyForManagedType (type)?.GetContainerFactory ();
}

/// <summary>
/// Creates a managed peer instance for a Java object being constructed.
/// Called from generated UCO constructor wrappers (nctor_*_uco).
/// </summary>
internal static void ActivateInstance (IntPtr self, Type targetType)
{
var instance = s_instance;
if (instance is null) {
throw new InvalidOperationException ("TrimmableTypeMap has not been initialized.");
}

// Look up the proxy via JNI class name → TypeMap dictionary.
// We can't use targetType.GetCustomAttribute<JavaPeerProxy>() because the
// self-application attribute is on the proxy type, not the target type.
var selfRef = new JniObjectReference (self);
var jniClass = JniEnvironment.Types.GetObjectClass (selfRef);
var className = JniEnvironment.Types.GetJniTypeNameFromClass (jniClass);
JniObjectReference.Dispose (ref jniClass);

if (className is null || !instance._typeMap.TryGetValue (className, out var proxyType)) {
throw new InvalidOperationException (
$"Failed to create peer for type '{targetType.FullName}' (jniClass='{className}'). " +
"Ensure the type has a generated proxy in the TypeMap assembly.");
}

var proxy = proxyType.GetCustomAttribute<JavaPeerProxy> (inherit: false);
if (proxy is null || proxy.CreateInstance (self, JniHandleOwnership.DoNotTransfer) is null) {
throw new InvalidOperationException (
$"Failed to create peer for type '{targetType.FullName}'. " +
"Ensure the type has a generated proxy in the TypeMap assembly.");
}
}

[UnmanagedCallersOnly]
static void OnRegisterNatives (IntPtr jnienv, IntPtr klass, IntPtr nativeClassHandle)
{
Expand Down Expand Up @@ -208,4 +175,5 @@ static void OnRegisterNatives (IntPtr jnienv, IntPtr klass, IntPtr nativeClassHa
Environment.FailFast ($"TrimmableTypeMap: Failed to register natives for class '{className}'.", ex);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,25 @@ public void Generate_LeafCtor_DoesNotUseCreateManagedPeer ()
Assert.True (ctorRefs.Count >= 2, "Should have ctor refs for proxy base + target type");
}

[Fact]
public void Generate_InheritedCtor_UcoUsesGuardAndInlinedActivation ()
{
var peers = ScanFixtures ();
var simpleActivity = peers.First (p => p.JavaName == "my/app/SimpleActivity");
Assert.NotNull (simpleActivity.ActivationCtor);
Assert.NotEqual (simpleActivity.ManagedTypeName, simpleActivity.ActivationCtor.DeclaringTypeName);

using var stream = GenerateAssembly (new [] { simpleActivity }, "InheritedCtorUcoTest");
using var pe = new PEReader (stream);
var reader = pe.GetMetadataReader ();
var memberNames = GetMemberRefNames (reader);

Assert.Contains ("get_WithinNewObjectScope", memberNames);
Assert.Contains ("GetUninitializedObject", memberNames);
Assert.DoesNotContain ("ActivateInstance", memberNames);
Assert.DoesNotContain ("ActivatePeerFromJavaConstructor", memberNames);
}

[Fact]
public void Generate_GenericType_ThrowsNotSupportedException ()
{
Expand Down Expand Up @@ -422,14 +441,26 @@ public void Generate_AcwProxy_HasRegisterNativesAndUcoMethods ()
using var pe = new PEReader (stream);
var reader = pe.GetMetadataReader ();

var memberNames = GetMemberRefNames (reader);
var proxyType = reader.TypeDefinitions
.Select (h => reader.GetTypeDefinition (h))
.Single (t =>
reader.GetString (t.Namespace) == "_TypeMap.Proxies" &&
reader.GetString (t.Name) == "MyApp_MainActivity_Proxy");
var proxyMethodNames = proxyType.GetMethods ()
.Select (h => reader.GetMethodDefinition (h))
.Select (m => reader.GetString (m.Name))
.ToList ();
Assert.Contains ("RegisterNatives", proxyMethodNames);
Assert.Contains (proxyMethodNames, name => name.Contains ("_uco_"));

// RegisterNatives is a method definition on the proxy type, not a member reference
var methodDefs = reader.MethodDefinitions
var privateImplDetailsType = reader.TypeDefinitions
.Select (h => reader.GetTypeDefinition (h))
.Single (t => reader.GetString (t.Name) == "<PrivateImplementationDetails>");
var privateImplMethodNames = privateImplDetailsType.GetMethods ()
.Select (h => reader.GetMethodDefinition (h))
.Select (m => reader.GetString (m.Name))
.ToList ();
Assert.Contains ("RegisterNatives", methodDefs);
Assert.DoesNotContain ("RegisterNatives", privateImplMethodNames);
}

[Fact]
Expand Down