diff --git a/external/Java.Interop b/external/Java.Interop
index 5d55b251071..b8f2c2b64a1 160000
--- a/external/Java.Interop
+++ b/external/Java.Interop
@@ -1 +1 @@
-Subproject commit 5d55b2510711f76a2fece20e2d07952313daed5b
+Subproject commit b8f2c2b64a1299ddd72bc040502647dd8b2f2710
diff --git a/external/debugger-libs b/external/debugger-libs
index e7fbb713d15..f2572777467 160000
--- a/external/debugger-libs
+++ b/external/debugger-libs
@@ -1 +1 @@
-Subproject commit e7fbb713d156d11193ed404783ad6fe9c4042a6d
+Subproject commit f2572777467b3dc19a2febc3642a87bd737b8bc0
diff --git a/external/xamarin-android-tools b/external/xamarin-android-tools
index ebd3aaf34b6..604940c3c74 160000
--- a/external/xamarin-android-tools
+++ b/external/xamarin-android-tools
@@ -1 +1 @@
-Subproject commit ebd3aaf34b6650b0d0b763f824d5ba3f2d6802e3
+Subproject commit 604940c3c74ba6af59ec06733de68d5cae306189
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs
index d07bb062bd3..0e2b867c4f9 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs
@@ -1,15 +1,49 @@
using System;
using System.Collections.Generic;
+using System.Reflection.Metadata;
+using System.Reflection.Metadata.Ecma335;
namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+///
+/// JNI primitive type kinds used for mapping JNI signatures → CLR types.
+///
+enum JniParamKind
+{
+Void, // V
+Boolean, // Z → sbyte
+Byte, // B → sbyte
+Char, // C → char
+Short, // S → short
+Int, // I → int
+Long, // J → long
+Float, // F → float
+Double, // D → double
+Object, // L...; or [ → IntPtr
+}
+
///
/// Helpers for parsing JNI method signatures.
///
static class JniSignatureHelper
{
///
+ /// Parses the parameter types from a JNI method signature like "(Landroid/os/Bundle;)V".
+ ///
+ public static List ParseParameterTypes (string jniSignature)
+ {
+ var result = new List ();
+ int i = 1; // skip opening '('
+ while (i < jniSignature.Length && jniSignature [i] != ')') {
+ result.Add (ParseSingleType (jniSignature, ref i));
+ }
+ return result;
+ }
+
+ ///
+
/// Parses the raw JNI type descriptor strings from a JNI method signature.
+
///
public static List ParseParameterTypeStrings (string jniSignature)
{
@@ -17,14 +51,16 @@ public static List ParseParameterTypeStrings (string jniSignature)
int i = 1; // skip opening '('
while (i < jniSignature.Length && jniSignature [i] != ')') {
int start = i;
- SkipSingleType (jniSignature, ref i);
+ ParseSingleType (jniSignature, ref i);
result.Add (jniSignature.Substring (start, i - start));
}
return result;
}
///
+
/// Extracts the return type descriptor from a JNI method signature.
+
///
public static string ParseReturnTypeString (string jniSignature)
{
@@ -32,22 +68,59 @@ public static string ParseReturnTypeString (string jniSignature)
return jniSignature.Substring (i);
}
- static void SkipSingleType (string sig, ref int i)
+ ///
+
+ /// Parses the return type from a JNI method signature.
+
+ ///
+ public static JniParamKind ParseReturnType (string jniSignature)
+ {
+ int i = jniSignature.IndexOf (')') + 1;
+ return ParseSingleType (jniSignature, ref i);
+ }
+
+ static JniParamKind ParseSingleType (string sig, ref int i)
{
switch (sig [i]) {
- case 'V': case 'Z': case 'B': case 'C': case 'S':
- case 'I': case 'J': case 'F': case 'D':
- i++;
- break;
+ case 'V': i++; return JniParamKind.Void;
+ case 'Z': i++; return JniParamKind.Boolean;
+ case 'B': i++; return JniParamKind.Byte;
+ case 'C': i++; return JniParamKind.Char;
+ case 'S': i++; return JniParamKind.Short;
+ case 'I': i++; return JniParamKind.Int;
+ case 'J': i++; return JniParamKind.Long;
+ case 'F': i++; return JniParamKind.Float;
+ case 'D': i++; return JniParamKind.Double;
case 'L':
i = sig.IndexOf (';', i) + 1;
- break;
+ return JniParamKind.Object;
case '[':
i++;
- SkipSingleType (sig, ref i);
- break;
+ ParseSingleType (sig, ref i); // skip element type
+ return JniParamKind.Object;
default:
throw new ArgumentException ($"Unknown JNI type character '{sig [i]}' in '{sig}' at index {i}");
}
}
+
+ ///
+
+ /// Encodes the CLR type for a JNI parameter kind into a signature type encoder.
+
+ ///
+ public static void EncodeClrType (SignatureTypeEncoder encoder, JniParamKind kind)
+ {
+ switch (kind) {
+ case JniParamKind.Boolean: encoder.Boolean (); break;
+ case JniParamKind.Byte: encoder.SByte (); break;
+ case JniParamKind.Char: encoder.Char (); break;
+ case JniParamKind.Short: encoder.Int16 (); break;
+ case JniParamKind.Int: encoder.Int32 (); break;
+ case JniParamKind.Long: encoder.Int64 (); break;
+ case JniParamKind.Float: encoder.Single (); break;
+ case JniParamKind.Double: encoder.Double (); break;
+ case JniParamKind.Object: encoder.IntPtr (); break;
+ default: throw new ArgumentException ($"Cannot encode JNI param kind {kind} as CLR type");
+ }
+ }
}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs
index 279d3e15519..ecc97fd0d82 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs
@@ -136,8 +136,34 @@ sealed class JavaPeerProxyData
///
public bool IsGenericDefinition { get; init; }
-}
+ ///
+
+ /// Whether this proxy needs ACW support (RegisterNatives + UCO wrappers + IAndroidCallableWrapper).
+
+ ///
+ public bool IsAcw { get; init; }
+
+ ///
+
+ /// UCO method wrappers for marshal methods (non-constructor).
+
+ ///
+ public List UcoMethods { get; } = new ();
+ ///
+
+ /// UCO constructor wrappers.
+
+ ///
+ public List UcoConstructors { get; } = new ();
+
+ ///
+
+ /// RegisterNatives registrations (method name, JNI signature, wrapper name).
+
+ ///
+ public List NativeRegistrations { get; } = new ();
+}
///
/// A cross-assembly type reference (assembly name + full managed type name).
@@ -157,6 +183,92 @@ sealed record TypeRefData
public required string AssemblyName { get; init; }
}
+///
+/// An [UnmanagedCallersOnly] static wrapper for a marshal method.
+/// Body: load all args → call n_* callback → ret.
+///
+sealed record UcoMethodData
+{
+ ///
+ /// Name of the generated wrapper method, e.g., "n_onCreate_uco_0".
+ ///
+ public required string WrapperName { get; init; }
+
+ ///
+
+ /// Name of the n_* callback to call, e.g., "n_OnCreate".
+
+ ///
+ public required string CallbackMethodName { get; init; }
+
+ ///
+
+ /// Type containing the callback method.
+
+ ///
+ public required TypeRefData CallbackType { get; init; }
+
+ ///
+
+ /// JNI method signature, e.g., "(Landroid/os/Bundle;)V". Used to determine CLR parameter types.
+
+ ///
+ public required string JniSignature { get; init; }
+}
+
+///
+/// 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)).
+///
+sealed record UcoConstructorData
+{
+ ///
+ /// Name of the generated wrapper, e.g., "nctor_0_uco".
+ ///
+ public required string WrapperName { get; init; }
+
+ ///
+
+ /// Target type to pass to ActivateInstance.
+
+ ///
+ public required TypeRefData TargetType { get; init; }
+
+ ///
+
+ /// JNI constructor signature, e.g., "(Landroid/content/Context;)V". Used for RegisterNatives registration.
+
+ ///
+ public required string JniSignature { get; init; }
+}
+
+///
+/// One JNI native method registration in RegisterNatives.
+///
+sealed record NativeRegistrationData
+{
+ ///
+ /// JNI method name to register, e.g., "n_onCreate" or "nctor_0".
+ ///
+ public required string JniMethodName { get; init; }
+
+ ///
+
+ /// JNI method signature, e.g., "(Landroid/os/Bundle;)V".
+
+ ///
+ public required string JniSignature { get; init; }
+
+ ///
+
+ /// Name of the UCO wrapper method whose function pointer to register.
+
+ ///
+ public required string WrapperMethodName { get; init; }
+}
+
///
/// Describes how the proxy's CreateInstance should construct the managed peer.
///
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs
index e64d14849a9..19ba83374ce 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs
@@ -83,6 +83,9 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri
var referencedAssemblies = new SortedSet (StringComparer.Ordinal);
foreach (var proxy in model.ProxyTypes) {
AddIfCrossAssembly (referencedAssemblies, proxy.TargetType?.AssemblyName, assemblyName);
+ foreach (var uco in proxy.UcoMethods) {
+ AddIfCrossAssembly (referencedAssemblies, uco.CallbackType.AssemblyName, assemblyName);
+ }
if (proxy.ActivationCtor != null && !proxy.ActivationCtor.IsOnLeafType) {
AddIfCrossAssembly (referencedAssemblies, proxy.ActivationCtor.DeclaringType.AssemblyName, assemblyName);
}
@@ -103,10 +106,11 @@ static void EmitPeers (TypeMapAssemblyData model, string jniName,
string entryJniName = i == 0 ? jniName : $"{jniName}[{i}]";
bool hasProxy = peer.ActivationCtor != null || peer.InvokerTypeName != null;
+ bool isAcw = !peer.DoNotGenerateAcw && !peer.IsInterface && peer.MarshalMethods.Count > 0;
JavaPeerProxyData? proxy = null;
if (hasProxy) {
- proxy = BuildProxyType (peer);
+ proxy = BuildProxyType (peer, isAcw);
model.ProxyTypes.Add (proxy);
}
@@ -178,7 +182,7 @@ static void AddIfCrossAssembly (SortedSet set, string? asmName, string o
}
}
- static JavaPeerProxyData BuildProxyType (JavaPeerInfo peer)
+ static JavaPeerProxyData BuildProxyType (JavaPeerInfo peer, bool isAcw)
{
// Use managed type name for proxy naming to guarantee uniqueness across aliases
// (two types with the same JNI name will have different managed names).
@@ -190,6 +194,7 @@ static JavaPeerProxyData BuildProxyType (JavaPeerInfo peer)
ManagedTypeName = peer.ManagedTypeName,
AssemblyName = peer.AssemblyName,
},
+ IsAcw = isAcw,
IsGenericDefinition = peer.IsGenericDefinition,
};
@@ -212,9 +217,80 @@ static JavaPeerProxyData BuildProxyType (JavaPeerInfo peer)
};
}
+ if (isAcw) {
+ BuildUcoMethods (peer, proxy);
+ BuildUcoConstructors (peer, proxy);
+ BuildNativeRegistrations (proxy);
+ }
+
return proxy;
}
+ static void BuildUcoMethods (JavaPeerInfo peer, JavaPeerProxyData proxy)
+ {
+ int ucoIndex = 0;
+ for (int i = 0; i < peer.MarshalMethods.Count; i++) {
+ var mm = peer.MarshalMethods [i];
+ if (mm.IsConstructor) {
+ continue;
+ }
+
+ proxy.UcoMethods.Add (new UcoMethodData {
+ WrapperName = $"n_{mm.JniName}_uco_{ucoIndex}",
+ CallbackMethodName = mm.NativeCallbackName,
+ CallbackType = new TypeRefData {
+ ManagedTypeName = !string.IsNullOrEmpty (mm.DeclaringTypeName) ? mm.DeclaringTypeName : peer.ManagedTypeName,
+ AssemblyName = !string.IsNullOrEmpty (mm.DeclaringAssemblyName) ? mm.DeclaringAssemblyName : peer.AssemblyName,
+ },
+ JniSignature = mm.JniSignature,
+ });
+ ucoIndex++;
+ }
+ }
+
+ static void BuildUcoConstructors (JavaPeerInfo peer, JavaPeerProxyData proxy)
+ {
+ if (peer.ActivationCtor == null || peer.JavaConstructors.Count == 0) {
+ return;
+ }
+
+ foreach (var ctor in peer.JavaConstructors) {
+ proxy.UcoConstructors.Add (new UcoConstructorData {
+ WrapperName = $"nctor_{ctor.ConstructorIndex}_uco",
+ JniSignature = ctor.JniSignature,
+ TargetType = new TypeRefData {
+ ManagedTypeName = peer.ManagedTypeName,
+ AssemblyName = peer.AssemblyName,
+ },
+ });
+ }
+ }
+
+ static void BuildNativeRegistrations (JavaPeerProxyData proxy)
+ {
+ foreach (var uco in proxy.UcoMethods) {
+ proxy.NativeRegistrations.Add (new NativeRegistrationData {
+ JniMethodName = uco.CallbackMethodName,
+ JniSignature = uco.JniSignature,
+ WrapperMethodName = uco.WrapperName,
+ });
+ }
+
+ foreach (var uco in proxy.UcoConstructors) {
+ string jniName = uco.WrapperName;
+ int ucoSuffix = jniName.LastIndexOf ("_uco", StringComparison.Ordinal);
+ if (ucoSuffix >= 0) {
+ jniName = jniName.Substring (0, ucoSuffix);
+ }
+
+ proxy.NativeRegistrations.Add (new NativeRegistrationData {
+ JniMethodName = jniName,
+ JniSignature = uco.JniSignature,
+ WrapperMethodName = uco.WrapperName,
+ });
+ }
+ }
+
static TypeMapAttributeData BuildEntry (JavaPeerInfo peer, JavaPeerProxyData? proxy,
string outputAssemblyName, string jniName)
{
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs
index f96d3448647..ce34dfbb4fa 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs
@@ -18,8 +18,8 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap;
/// [assembly: TypeMap<Java.Lang.Object>("android/widget/TextView", typeof(TextView_Proxy), typeof(TextView))] // trimmable (MCW)
/// [assembly: TypeMapAssociation(typeof(MyTextView), typeof(Android_Widget_TextView_Proxy))] // alias
///
-/// // One proxy type per Java peer that needs activation:
-/// public sealed class Activity_Proxy : JavaPeerProxy
+/// // One proxy type per Java peer that needs activation or UCO wrappers:
+/// public sealed class Activity_Proxy : JavaPeerProxy, IAndroidCallableWrapper // IAndroidCallableWrapper for ACWs only
/// {
/// public Activity_Proxy() : base() { }
///
@@ -34,9 +34,25 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap;
///
/// public override Type TargetType => typeof(Activity);
/// public Type InvokerType => typeof(IOnClickListenerInvoker); // interfaces only
+///
+/// // UCO wrappers — [UnmanagedCallersOnly] entry points for JNI native methods (ACWs only):
+/// [UnmanagedCallersOnly]
+/// public static void n_OnCreate_uco_0(IntPtr jnienv, IntPtr self, IntPtr p0)
+/// => Activity.n_OnCreate(jnienv, self, p0);
+///
+/// [UnmanagedCallersOnly]
+/// public static void nctor_0_uco(IntPtr jnienv, IntPtr self)
+/// => TrimmableNativeRegistration.ActivateInstance(self, typeof(Activity));
+///
+/// // Registers JNI native methods (ACWs only):
+/// public void RegisterNatives(JniType jniType)
+/// {
+/// TrimmableNativeRegistration.RegisterMethod(jniType, "n_OnCreate", "(Landroid/os/Bundle;)V", &n_OnCreate_uco_0);
+/// TrimmableNativeRegistration.RegisterMethod(jniType, "nctor_0", "()V", &nctor_0_uco);
+/// }
/// }
///
-/// // Emitted so the proxy assembly can access internal members in the target assembly:
+/// // Emitted so the proxy assembly can access internal n_* callbacks in the target assembly:
/// [assembly: IgnoresAccessChecksTo("Mono.Android")]
///
///
@@ -51,8 +67,11 @@ sealed class TypeMapAssemblyEmitter
TypeReferenceHandle _javaPeerProxyRef;
TypeReferenceHandle _iJavaPeerableRef;
TypeReferenceHandle _jniHandleOwnershipRef;
+ TypeReferenceHandle _iAndroidCallableWrapperRef;
TypeReferenceHandle _systemTypeRef;
TypeReferenceHandle _runtimeTypeHandleRef;
+ TypeReferenceHandle _jniTypeRef;
+ TypeReferenceHandle _trimmableNativeRegistrationRef;
TypeReferenceHandle _notSupportedExceptionRef;
TypeReferenceHandle _runtimeHelpersRef;
@@ -60,6 +79,10 @@ sealed class TypeMapAssemblyEmitter
MemberReferenceHandle _getTypeFromHandleRef;
MemberReferenceHandle _getUninitializedObjectRef;
MemberReferenceHandle _notSupportedExceptionCtorRef;
+ MemberReferenceHandle _activateInstanceRef;
+ MemberReferenceHandle _registerMethodRef;
+ MemberReferenceHandle _ucoAttrCtorRef;
+ BlobHandle _ucoAttrBlobHandle;
MemberReferenceHandle _typeMapAttrCtorRef2Arg;
MemberReferenceHandle _typeMapAttrCtorRef3Arg;
MemberReferenceHandle _typeMapAssociationAttrCtorRef;
@@ -96,8 +119,11 @@ public void Emit (TypeMapAssemblyData model, string outputPath)
EmitTypeReferences ();
EmitMemberReferences ();
+ // Track wrapper method names → handles for RegisterNatives
+ var wrapperHandles = new Dictionary ();
+
foreach (var proxy in model.ProxyTypes) {
- EmitProxyType (proxy);
+ EmitProxyType (proxy, wrapperHandles);
}
foreach (var entry in model.Entries) {
@@ -121,10 +147,16 @@ void EmitTypeReferences ()
metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("IJavaPeerable"));
_jniHandleOwnershipRef = metadata.AddTypeReference (_pe.MonoAndroidRef,
metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("JniHandleOwnership"));
+ _iAndroidCallableWrapperRef = metadata.AddTypeReference (_pe.MonoAndroidRef,
+ metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("IAndroidCallableWrapper"));
_systemTypeRef = metadata.AddTypeReference (_pe.SystemRuntimeRef,
metadata.GetOrAddString ("System"), metadata.GetOrAddString ("Type"));
_runtimeTypeHandleRef = metadata.AddTypeReference (_pe.SystemRuntimeRef,
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,
@@ -151,6 +183,33 @@ void EmitMemberReferences ()
rt => rt.Void (),
p => p.AddParameter ().Type ().String ()));
+ _activateInstanceRef = _pe.AddMemberRef (_trimmableNativeRegistrationRef, "ActivateInstance",
+ sig => sig.MethodSignature ().Parameters (2,
+ rt => rt.Void (),
+ p => {
+ p.AddParameter ().Type ().IntPtr ();
+ p.AddParameter ().Type ().Type (_systemTypeRef, false);
+ }));
+
+ _registerMethodRef = _pe.AddMemberRef (_trimmableNativeRegistrationRef, "RegisterMethod",
+ sig => sig.MethodSignature ().Parameters (4,
+ rt => rt.Void (),
+ p => {
+ p.AddParameter ().Type ().Type (_jniTypeRef, false);
+ p.AddParameter ().Type ().String ();
+ p.AddParameter ().Type ().String ();
+ p.AddParameter ().Type ().IntPtr ();
+ }));
+
+ var ucoAttrTypeRef = _pe.Metadata.AddTypeReference (_pe.SystemRuntimeInteropServicesRef,
+ _pe.Metadata.GetOrAddString ("System.Runtime.InteropServices"),
+ _pe.Metadata.GetOrAddString ("UnmanagedCallersOnlyAttribute"));
+ _ucoAttrCtorRef = _pe.AddMemberRef (ucoAttrTypeRef, ".ctor",
+ sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { }));
+
+ // Pre-compute the UCO attribute blob — it's always the same 4 bytes (prolog + no named args)
+ _ucoAttrBlobHandle = _pe.BuildAttributeBlob (b => { });
+
EmitTypeMapAttributeCtorRef ();
EmitTypeMapAssociationAttributeCtorRef ();
}
@@ -204,10 +263,11 @@ void EmitTypeMapAssociationAttributeCtorRef ()
}));
}
- void EmitProxyType (JavaPeerProxyData proxy)
+
+ void EmitProxyType (JavaPeerProxyData proxy, Dictionary wrapperHandles)
{
var metadata = _pe.Metadata;
- metadata.AddTypeDefinition (
+ var typeDefHandle = metadata.AddTypeDefinition (
TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.Class,
metadata.GetOrAddString (proxy.Namespace),
metadata.GetOrAddString (proxy.TypeName),
@@ -215,6 +275,10 @@ void EmitProxyType (JavaPeerProxyData proxy)
MetadataTokens.FieldDefinitionHandle (metadata.GetRowCount (TableIndex.Field) + 1),
MetadataTokens.MethodDefinitionHandle (metadata.GetRowCount (TableIndex.MethodDef) + 1));
+ if (proxy.IsAcw) {
+ metadata.AddInterfaceImplementation (typeDefHandle, _iAndroidCallableWrapperRef);
+ }
+
// .ctor
_pe.EmitBody (".ctor",
MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName,
@@ -237,6 +301,22 @@ void EmitProxyType (JavaPeerProxyData proxy)
EmitTypeGetter ("get_InvokerType", proxy.InvokerType,
MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig);
}
+
+ // UCO wrappers
+ foreach (var uco in proxy.UcoMethods) {
+ var handle = EmitUcoMethod (uco);
+ wrapperHandles [uco.WrapperName] = handle;
+ }
+
+ foreach (var uco in proxy.UcoConstructors) {
+ var handle = EmitUcoConstructor (uco);
+ wrapperHandles [uco.WrapperName] = handle;
+ }
+
+ // RegisterNatives
+ if (proxy.IsAcw) {
+ EmitRegisterNatives (proxy.NativeRegistrations, wrapperHandles);
+ }
}
void EmitCreateInstance (JavaPeerProxyData proxy)
@@ -348,6 +428,106 @@ void EmitTypeGetter (string methodName, TypeRefData typeRef, MethodAttributes at
});
}
+ MethodDefinitionHandle EmitUcoMethod (UcoMethodData uco)
+ {
+ var jniParams = JniSignatureHelper.ParseParameterTypes (uco.JniSignature);
+ var returnKind = JniSignatureHelper.ParseReturnType (uco.JniSignature);
+ int paramCount = 2 + jniParams.Count;
+ bool isVoid = returnKind == JniParamKind.Void;
+
+ Action encodeSig = sig => sig.MethodSignature ().Parameters (paramCount,
+ rt => { if (isVoid) rt.Void (); else JniSignatureHelper.EncodeClrType (rt.Type (), returnKind); },
+ p => {
+ p.AddParameter ().Type ().IntPtr ();
+ p.AddParameter ().Type ().IntPtr ();
+ for (int j = 0; j < jniParams.Count; j++)
+ JniSignatureHelper.EncodeClrType (p.AddParameter ().Type (), jniParams [j]);
+ });
+
+ var callbackTypeHandle = _pe.ResolveTypeRef (uco.CallbackType);
+ var callbackRef = _pe.AddMemberRef (callbackTypeHandle, uco.CallbackMethodName, encodeSig);
+
+ var handle = _pe.EmitBody (uco.WrapperName,
+ MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig,
+ encodeSig,
+ encoder => {
+ for (int p = 0; p < paramCount; p++)
+ encoder.LoadArgument (p);
+ encoder.Call (callbackRef);
+ encoder.OpCode (ILOpCode.Ret);
+ });
+
+ AddUnmanagedCallersOnlyAttribute (handle);
+ return handle;
+ }
+
+ MethodDefinitionHandle EmitUcoConstructor (UcoConstructorData uco)
+ {
+ var userTypeRef = _pe.ResolveTypeRef (uco.TargetType);
+
+ // 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, ),
+ // 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
+ // 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);
+ });
+
+ AddUnmanagedCallersOnlyAttribute (handle);
+ return handle;
+ }
+
+ void EmitRegisterNatives (List registrations,
+ Dictionary wrapperHandles)
+ {
+ _pe.EmitBody ("RegisterNatives",
+ MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig |
+ MethodAttributes.NewSlot | MethodAttributes.Final,
+ sig => sig.MethodSignature (isInstanceMethod: true).Parameters (1,
+ rt => rt.Void (),
+ p => p.AddParameter ().Type ().Type (_jniTypeRef, false)),
+ encoder => {
+ foreach (var reg in registrations) {
+ if (!wrapperHandles.TryGetValue (reg.WrapperMethodName, out var wrapperHandle)) {
+ continue;
+ }
+ encoder.LoadArgument (1);
+ encoder.LoadString (_pe.Metadata.GetOrAddUserString (reg.JniMethodName));
+ encoder.LoadString (_pe.Metadata.GetOrAddUserString (reg.JniSignature));
+ encoder.OpCode (ILOpCode.Ldftn);
+ encoder.Token (wrapperHandle);
+ encoder.Call (_registerMethodRef);
+ }
+ encoder.OpCode (ILOpCode.Ret);
+ });
+ }
+
+ void AddUnmanagedCallersOnlyAttribute (MethodDefinitionHandle handle)
+ {
+ _pe.Metadata.AddCustomAttribute (handle, _ucoAttrCtorRef, _ucoAttrBlobHandle);
+ }
+
void EmitTypeMapAttribute (TypeMapAttributeData entry)
{
var ctorRef = entry.IsUnconditional ? _typeMapAttrCtorRef2Arg : _typeMapAttrCtorRef3Arg;
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs
index 2de7a49ead9..e76c3810ea7 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs
@@ -67,6 +67,12 @@ sealed record JavaPeerInfo
///
public IReadOnlyList MarshalMethods { get; init; } = Array.Empty ();
+ ///
+ /// Java constructors to emit in the JCW .java file.
+ /// Each has a JNI signature and an ordinal index for the nctor_N native method.
+ ///
+ public IReadOnlyList JavaConstructors { get; init; } = Array.Empty ();
+
///
/// Information about the activation constructor for this type.
/// May reference a base type's constructor if the type doesn't define its own.
@@ -179,6 +185,34 @@ sealed record JniParameterInfo
public string ManagedType { get; init; } = "";
}
+///
+/// Describes a Java constructor to emit in the JCW .java source file.
+///
+sealed record JavaConstructorInfo
+{
+ ///
+ /// JNI constructor signature, e.g., "(Landroid/content/Context;)V".
+ ///
+ public required string JniSignature { get; init; }
+
+ ///
+ /// Ordinal index for the native constructor method (nctor_0, nctor_1, ...).
+ ///
+ public required int ConstructorIndex { get; init; }
+
+ ///
+ /// JNI parameter types parsed from the signature.
+ /// Used to generate the Java constructor parameter list.
+ ///
+ public IReadOnlyList Parameters { get; init; } = Array.Empty ();
+
+ ///
+ /// For [Export] constructors: super constructor arguments string.
+ /// Null for [Register] constructors.
+ ///
+ public string? SuperArgumentsString { get; init; }
+}
+
///
/// Describes how to call the activation constructor for a Java peer type.
///
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
index 28941747b30..ca9057ed747 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
@@ -220,6 +220,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary results
DoNotGenerateAcw = doNotGenerateAcw,
IsUnconditional = isUnconditional,
MarshalMethods = marshalMethods,
+ JavaConstructors = BuildJavaConstructors (marshalMethods),
ActivationCtor = activationCtor,
InvokerTypeName = invokerTypeName,
IsGenericDefinition = isGenericDefinition,
@@ -708,4 +709,23 @@ static List ParseJniParameters (string jniSignature)
}
return result;
}
+
+ static List BuildJavaConstructors (List marshalMethods)
+ {
+ var ctors = new List ();
+ int ctorIndex = 0;
+ foreach (var mm in marshalMethods) {
+ if (!mm.IsConstructor) {
+ continue;
+ }
+ ctors.Add (new JavaConstructorInfo {
+ JniSignature = mm.JniSignature,
+ ConstructorIndex = ctorIndex,
+ Parameters = mm.Parameters,
+ SuperArgumentsString = mm.SuperArgumentsString,
+ });
+ ctorIndex++;
+ }
+ return ctors;
+ }
}
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs
index bb446ff3029..4a168b636a9 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs
@@ -67,6 +67,9 @@ protected static JavaPeerInfo MakeAcwPeer (string jniName, string managedName, s
{
var peer = MakePeerWithActivation (jniName, managedName, asmName);
peer.DoNotGenerateAcw = false;
+ peer.JavaConstructors = new List {
+ new JavaConstructorInfo { ConstructorIndex = 0, JniSignature = "()V" },
+ };
peer.MarshalMethods = new List {
new MarshalMethodInfo {
JniName = "",
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs
index cceb2e20a62..b2282243273 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs
@@ -127,7 +127,65 @@ public void Generate_ProxyType_HasCtorAndCreateInstance ()
}
- public class IgnoresAccessChecksTo : IDisposable
+public class AcwProxy : IDisposable
+{
+readonly string _outputDir = CreateTempDir ();
+public void Dispose () => DeleteTempDir (_outputDir);
+
+[Fact]
+public void Generate_AcwProxy_HasRegisterNativesAndUcoMethods ()
+{
+var peers = ScanFixtures ();
+var acwPeer = peers.First (p => p.JavaName == "my/app/TouchHandler");
+var path = GenerateAssembly (new [] { acwPeer }, _outputDir, "AcwTest");
+var (pe, reader) = OpenAssembly (path);
+using (pe) {
+var proxy = reader.TypeDefinitions
+.Select (h => reader.GetTypeDefinition (h))
+.First (t => reader.GetString (t.Name) == "MyApp_TouchHandler_Proxy");
+
+var methods = proxy.GetMethods ()
+.Select (h => reader.GetMethodDefinition (h))
+.Select (m => reader.GetString (m.Name))
+.ToList ();
+
+Assert.Contains ("RegisterNatives", methods);
+Assert.Contains (methods, m => m.StartsWith ("n_") && m.EndsWith ("_uco_0"));
+}
+}
+
+[Fact]
+public void Generate_AcwProxy_HasUnmanagedCallersOnlyAttribute ()
+{
+var peers = ScanFixtures ();
+var acwPeer = peers.First (p => p.JavaName == "my/app/TouchHandler");
+var path = GenerateAssembly (new [] { acwPeer }, _outputDir, "UcoTest");
+var (pe, reader) = OpenAssembly (path);
+using (pe) {
+var proxy = reader.TypeDefinitions
+.Select (h => reader.GetTypeDefinition (h))
+.First (t => reader.GetString (t.Name) == "MyApp_TouchHandler_Proxy");
+
+var ucoMethod = proxy.GetMethods ()
+.Select (h => reader.GetMethodDefinition (h))
+.First (m => reader.GetString (m.Name).Contains ("_uco_"));
+
+var attrNames = ucoMethod.GetCustomAttributes ()
+.Select (h => reader.GetCustomAttribute (h))
+.Select (a => {
+var ctorHandle = (MemberReferenceHandle) a.Constructor;
+var ctor = reader.GetMemberReference (ctorHandle);
+var typeRef = reader.GetTypeReference ((TypeReferenceHandle) ctor.Parent);
+return $"{reader.GetString (typeRef.Namespace)}.{reader.GetString (typeRef.Name)}";
+})
+.ToList ();
+Assert.Contains ("System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute", attrNames);
+}
+}
+
+}
+
+public class IgnoresAccessChecksTo : IDisposable
{
readonly string _outputDir = CreateTempDir ();
public void Dispose () => DeleteTempDir (_outputDir);
@@ -228,7 +286,68 @@ public void Generate_EmptyPeerList_ProducesValidAssembly ()
}
- public class CreateInstancePaths : IDisposable
+public class JniSignatureHelperTests
+{
+
+[Theory]
+[InlineData ("()V", 0)]
+[InlineData ("(I)V", 1)]
+[InlineData ("(Landroid/os/Bundle;)V", 1)]
+[InlineData ("(IFJ)V", 3)]
+[InlineData ("(ZLandroid/view/View;I)Z", 3)]
+[InlineData ("([Ljava/lang/String;)V", 1)]
+public void ParseParameterTypes_ParsesCorrectCount (string signature, int expectedCount)
+{
+var actual = JniSignatureHelper.ParseParameterTypes (signature);
+Assert.Equal (expectedCount, actual.Count);
+}
+
+[Theory]
+[InlineData ("(Z)V", JniParamKind.Boolean)]
+[InlineData ("(Ljava/lang/String;)V", JniParamKind.Object)]
+public void ParseParameterTypes_SingleParam_MapsToCorrectKind (string signature, JniParamKind expectedKind)
+{
+var types = JniSignatureHelper.ParseParameterTypes (signature);
+Assert.Single (types);
+Assert.Equal (expectedKind, types [0]);
+}
+
+[Theory]
+[InlineData ("()V", JniParamKind.Void)]
+[InlineData ("()I", JniParamKind.Int)]
+[InlineData ("()Z", JniParamKind.Boolean)]
+[InlineData ("()Ljava/lang/String;", JniParamKind.Object)]
+public void ParseReturnType_MapsToCorrectKind (string signature, JniParamKind expectedKind)
+{
+Assert.Equal (expectedKind, JniSignatureHelper.ParseReturnType (signature));
+}
+
+}
+
+public class NegativeEdgeCase
+{
+
+[Fact]
+public void ParseParameterTypes_EmptyString_ReturnsEmptyList ()
+{
+Assert.Empty (JniSignatureHelper.ParseParameterTypes (""));
+}
+
+[Fact]
+public void ParseParameterTypes_InvalidSignature_Throws ()
+{
+Assert.ThrowsAny (() => JniSignatureHelper.ParseParameterTypes ("not-a-sig"));
+}
+
+[Fact]
+public void ParseParameterTypes_UnterminatedSignature_ReturnsEmptyList ()
+{
+Assert.Empty (JniSignatureHelper.ParseParameterTypes ("("));
+}
+
+}
+
+public class CreateInstancePaths : IDisposable
{
readonly string _outputDir = CreateTempDir ();
public void Dispose () => DeleteTempDir (_outputDir);
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs
index 54d66fc03f7..0b9ae5e7fdd 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs
@@ -45,6 +45,25 @@ public void Build_ExplicitAssemblyName_OverridesOutputPath ()
Assert.Equal ("MyAssembly", model.AssemblyName);
}
+ [Fact]
+ public void Build_ComputesIgnoresAccessChecksToFromCrossAssemblyCallbacks ()
+ {
+ var peer = MakeAcwPeer ("my/app/MainActivity", "MyApp.MainActivity", "MyApp");
+ ((List) peer.MarshalMethods).Add (new MarshalMethodInfo {
+ JniName = "onCreate",
+ NativeCallbackName = "n_OnCreate",
+ JniSignature = "(Landroid/os/Bundle;)V",
+ IsConstructor = false,
+ DeclaringTypeName = "Android.App.Activity",
+ DeclaringAssemblyName = "Mono.Android",
+ });
+ var model = BuildModel (new [] { peer });
+ // The UCO callback type references Mono.Android, which is cross-assembly
+ Assert.Contains ("Mono.Android", model.IgnoresAccessChecksTo);
+ // The output assembly itself should not appear
+ Assert.DoesNotContain (model.AssemblyName, model.IgnoresAccessChecksTo);
+ }
+
}
public class TypeMapEntries
@@ -216,6 +235,257 @@ public void Build_ProxyNaming_ReplacesDotAndPlus ()
}
+ public class AcwDetection
+ {
+
+ [Fact]
+ public void Build_AcwType_IsAcwTrue ()
+ {
+ var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App");
+ var model = BuildModel (new [] { peer });
+
+ Assert.Single (model.ProxyTypes);
+ Assert.True (model.ProxyTypes [0].IsAcw);
+ }
+
+ [Fact]
+ public void Build_McwType_IsAcwFalse ()
+ {
+ var peer = MakePeerWithActivation ("java/lang/Object", "Java.Lang.Object", "Mono.Android");
+ var model = BuildModel (new [] { peer });
+
+ Assert.Single (model.ProxyTypes);
+ Assert.False (model.ProxyTypes [0].IsAcw);
+ }
+
+ [Fact]
+ public void Build_InterfaceWithMarshalMethods_IsNotAcw ()
+ {
+ var peer = new JavaPeerInfo {
+ JavaName = "android/view/View$OnClickListener",
+ ManagedTypeName = "Android.Views.View+IOnClickListener",
+ ManagedTypeNamespace = "Android.Views",
+ ManagedTypeShortName = "IOnClickListener",
+ AssemblyName = "Mono.Android",
+ IsInterface = true,
+ InvokerTypeName = "Android.Views.View+IOnClickListenerInvoker",
+ MarshalMethods = new List {
+ MakeMarshalMethod ("onClick", "n_OnClick", "(Landroid/view/View;)V"),
+ },
+ };
+
+ var model = BuildModel (new [] { peer });
+ Assert.Single (model.ProxyTypes);
+ // Interface is NOT an ACW even with marshal methods
+ Assert.False (model.ProxyTypes [0].IsAcw);
+ }
+
+ [Fact]
+ public void Build_DoNotGenerateAcw_IsNotAcw ()
+ {
+ var peer = MakePeerWithActivation ("java/lang/Object", "Java.Lang.Object", "Mono.Android");
+ peer.DoNotGenerateAcw = true;
+ peer.MarshalMethods = new List {
+ MakeMarshalMethod ("toString", "n_ToString", "()Ljava/lang/String;"),
+ };
+
+ var model = BuildModel (new [] { peer });
+ Assert.Single (model.ProxyTypes);
+ Assert.False (model.ProxyTypes [0].IsAcw);
+ }
+
+ }
+
+ public class UcoMethods
+ {
+
+ [Fact]
+ public void Build_AcwWithMarshalMethods_CreatesUcoMethods ()
+ {
+ var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App");
+ peer.MarshalMethods = new List {
+ MakeMarshalMethod ("", "n_ctor", "()V", isConstructor: true),
+ MakeMarshalMethod ("onCreate", "n_OnCreate", "(Landroid/os/Bundle;)V"),
+ MakeMarshalMethod ("onResume", "n_OnResume", "()V"),
+ };
+
+ var model = BuildModel (new [] { peer });
+ var proxy = model.ProxyTypes [0];
+
+ Assert.Equal (2, proxy.UcoMethods.Count);
+ Assert.Equal ("n_onCreate_uco_0", proxy.UcoMethods [0].WrapperName);
+ Assert.Equal ("n_OnCreate", proxy.UcoMethods [0].CallbackMethodName);
+ Assert.Equal ("(Landroid/os/Bundle;)V", proxy.UcoMethods [0].JniSignature);
+
+ Assert.Equal ("n_onResume_uco_1", proxy.UcoMethods [1].WrapperName);
+ Assert.Equal ("n_OnResume", proxy.UcoMethods [1].CallbackMethodName);
+ }
+
+ [Fact]
+ public void Build_UcoMethod_CallbackTypeIsDeclaringType ()
+ {
+ var mm = MakeMarshalMethod ("toString", "n_ToString", "()Ljava/lang/String;");
+ mm.DeclaringTypeName = "Java.Lang.Object";
+ mm.DeclaringAssemblyName = "Mono.Android";
+
+ var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App");
+ peer.MarshalMethods = new List {
+ MakeMarshalMethod ("", "n_ctor", "()V", isConstructor: true),
+ mm,
+ };
+
+ var model = BuildModel (new [] { peer });
+ var uco = model.ProxyTypes [0].UcoMethods [0];
+ Assert.Equal ("Java.Lang.Object", uco.CallbackType.ManagedTypeName);
+ Assert.Equal ("Mono.Android", uco.CallbackType.AssemblyName);
+ }
+
+ [Fact]
+ public void Build_UcoMethod_FallsBackToPeerType_WhenDeclaringTypeEmpty ()
+ {
+ var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App");
+ peer.MarshalMethods = new List {
+ MakeMarshalMethod ("", "n_ctor", "()V", isConstructor: true),
+ MakeMarshalMethod ("onPause", "n_OnPause", "()V"),
+ };
+
+ var model = BuildModel (new [] { peer });
+ var uco = model.ProxyTypes [0].UcoMethods [0];
+ Assert.Equal ("MyApp.MainActivity", uco.CallbackType.ManagedTypeName);
+ Assert.Equal ("App", uco.CallbackType.AssemblyName);
+ }
+
+ [Fact]
+ public void Build_ConstructorsInMarshalMethods_SkippedFromUcoMethods ()
+ {
+ var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App");
+ peer.MarshalMethods = new List {
+ MakeMarshalMethod ("", "n_ctor", "()V", isConstructor: true),
+ MakeMarshalMethod ("", "n_ctor2", "()V", isConstructor: true),
+ MakeMarshalMethod ("onStart", "n_OnStart", "()V"),
+ };
+
+ var model = BuildModel (new [] { peer });
+ var proxy = model.ProxyTypes [0];
+
+ // Only 1 UCO method (constructors are skipped from UcoMethods)
+ Assert.Single (proxy.UcoMethods);
+ Assert.Equal ("n_onStart_uco_0", proxy.UcoMethods [0].WrapperName);
+ }
+
+ }
+
+ public class UcoConstructors
+ {
+
+ [Fact]
+ public void Build_AcwWithConstructors_CreatesUcoConstructors ()
+ {
+ var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App");
+
+ var model = BuildModel (new [] { peer });
+ var proxy = model.ProxyTypes [0];
+
+ Assert.Single (proxy.UcoConstructors);
+ Assert.Equal ("nctor_0_uco", proxy.UcoConstructors [0].WrapperName);
+ Assert.Equal ("MyApp.MainActivity", proxy.UcoConstructors [0].TargetType.ManagedTypeName);
+ }
+
+ [Fact]
+ public void Build_PeerWithoutActivationCtor_NoUcoConstructors ()
+ {
+ // Peer with marshal methods but no activation ctor
+ var peer = new JavaPeerInfo {
+ JavaName = "my/app/Foo",
+ ManagedTypeName = "MyApp.Foo",
+ ManagedTypeNamespace = "MyApp",
+ ManagedTypeShortName = "Foo",
+ AssemblyName = "App",
+ InvokerTypeName = "MyApp.FooInvoker", // has invoker → will create proxy
+ MarshalMethods = new List {
+ MakeMarshalMethod ("bar", "n_Bar", "()V"),
+ },
+ JavaConstructors = new List {
+ new JavaConstructorInfo { ConstructorIndex = 0, JniSignature = "()V" },
+ },
+ };
+
+ var model = BuildModel (new [] { peer });
+ var proxy = model.ProxyTypes [0];
+
+ Assert.Empty (proxy.UcoConstructors);
+ }
+
+ }
+
+ public class NativeRegistrations
+ {
+
+ [Fact]
+ public void Build_NativeRegistrations_MatchUcoMethods ()
+ {
+ var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App");
+ peer.MarshalMethods = new List {
+ MakeMarshalMethod ("", "n_ctor", "()V", isConstructor: true),
+ MakeMarshalMethod ("onCreate", "n_OnCreate", "(Landroid/os/Bundle;)V"),
+ };
+
+ var model = BuildModel (new [] { peer });
+ var proxy = model.ProxyTypes [0];
+
+ // 1 registration for method + 1 for constructor
+ Assert.Equal (2, proxy.NativeRegistrations.Count);
+
+ var methodReg = proxy.NativeRegistrations [0];
+ Assert.Equal ("n_OnCreate", methodReg.JniMethodName);
+ Assert.Equal ("(Landroid/os/Bundle;)V", methodReg.JniSignature);
+ Assert.Equal ("n_onCreate_uco_0", methodReg.WrapperMethodName);
+
+ var ctorReg = proxy.NativeRegistrations [1];
+ Assert.Equal ("nctor_0", ctorReg.JniMethodName);
+ Assert.Equal ("()V", ctorReg.JniSignature);
+ Assert.Equal ("nctor_0_uco", ctorReg.WrapperMethodName);
+ }
+
+ [Fact]
+ public void Build_NativeRegistrations_ParameterizedConstructor_HasCorrectJniSignature ()
+ {
+ var peer = MakeAcwPeer ("my/app/MyView", "MyApp.MyView", "App");
+ peer.JavaConstructors = new List {
+ new JavaConstructorInfo { ConstructorIndex = 0, JniSignature = "()V" },
+ new JavaConstructorInfo { ConstructorIndex = 1, JniSignature = "(Landroid/content/Context;)V",
+ Parameters = new List {
+ new JniParameterInfo { JniType = "Landroid/content/Context;" },
+ }
+ },
+ };
+ peer.MarshalMethods = new List {
+ MakeMarshalMethod ("", "n_ctor", "()V", isConstructor: true),
+ MakeMarshalMethod ("", "n_ctor", "(Landroid/content/Context;)V", isConstructor: true),
+ };
+
+ var model = BuildModel (new [] { peer });
+ var proxy = model.ProxyTypes [0];
+
+ var ctorRegs = proxy.NativeRegistrations.Where (r => r.JniMethodName.StartsWith ("nctor_")).ToList ();
+ Assert.Equal (2, ctorRegs.Count);
+
+ Assert.Equal ("()V", ctorRegs [0].JniSignature);
+ Assert.Equal ("(Landroid/content/Context;)V", ctorRegs [1].JniSignature);
+ }
+
+ [Fact]
+ public void Build_NonAcwProxy_NoNativeRegistrations ()
+ {
+ var peer = MakePeerWithActivation ("java/lang/Object", "Java.Lang.Object", "Mono.Android");
+ var model = BuildModel (new [] { peer });
+
+ Assert.Single (model.ProxyTypes);
+ Assert.Empty (model.ProxyTypes [0].NativeRegistrations);
+ }
+
+ }
+
public class FixtureScan
{
@@ -245,6 +515,20 @@ public void ScanFixtures_ManagedTypeShortName_IsCorrect (string javaName, string
Assert.Equal (expectedShortName, peer.ManagedTypeShortName);
}
+ [Fact]
+ public void Build_FromScannedFixtures_AcwTypesHaveUcoMethods ()
+ {
+ var peers = ScanFixtures ();
+ var model = BuildModel (peers);
+
+ var acwProxies = model.ProxyTypes.Where (p => p.IsAcw).ToList ();
+ Assert.NotEmpty (acwProxies);
+
+ foreach (var proxy in acwProxies) {
+ Assert.NotEmpty (proxy.NativeRegistrations);
+ }
+ }
+
}
public class FixtureConditionalAttributes
@@ -302,6 +586,11 @@ public void Fixture_McwType_HasActivation_CreatesProxy (string javaName, string
Assert.NotNull (proxy);
Assert.True (proxy!.HasActivation);
Assert.Equal (expectedManagedName, proxy.TargetType.ManagedTypeName);
+ // MCW types with DoNotGenerateAcw → not ACW
+ Assert.False (proxy.IsAcw);
+ Assert.Empty (proxy.UcoMethods);
+ Assert.Empty (proxy.UcoConstructors);
+ Assert.Empty (proxy.NativeRegistrations);
}
[Fact]
@@ -331,19 +620,113 @@ public void Fixture_Service_NoActivation_NoProxy ()
}
}
+ public class FixtureAcwTypes
+ {
+
+ [Fact]
+ public void Fixture_MainActivity_IsAcw ()
+ {
+ var peer = FindFixtureByJavaName ("my/app/MainActivity");
+ Assert.False (peer.DoNotGenerateAcw);
+ Assert.NotEmpty (peer.MarshalMethods);
+ Assert.NotNull (peer.ActivationCtor);
+
+ var model = BuildModel (new [] { peer }, "TypeMap");
+ var proxy = FindProxy (model, "MyApp_MainActivity_Proxy");
+ Assert.NotNull (proxy);
+ Assert.True (proxy!.IsAcw);
+ Assert.True (proxy.HasActivation);
+ }
+
+ [Fact]
+ public void Fixture_MainActivity_UcoMethods ()
+ {
+ var peer = FindFixtureByJavaName ("my/app/MainActivity");
+ var model = BuildModel (new [] { peer }, "TypeMap");
+ var proxy = FindProxy (model, "MyApp_MainActivity_Proxy")!;
+
+ var nonCtorMethods = peer.MarshalMethods.Where (m => !m.IsConstructor).ToList ();
+ Assert.Equal (nonCtorMethods.Count, proxy.UcoMethods.Count);
+
+ var onCreateUco = proxy.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_OnCreate");
+ Assert.NotNull (onCreateUco);
+ Assert.Equal ("(Landroid/os/Bundle;)V", onCreateUco!.JniSignature);
+ Assert.StartsWith ("n_onCreate_uco_", onCreateUco.WrapperName);
+ }
+
+ }
+
+ public class FixtureTouchHandler
+ {
+
+ [Fact]
+ public void Fixture_TouchHandler_AllUcoMethods ()
+ {
+ var peer = FindFixtureByJavaName ("my/app/TouchHandler");
+ var model = BuildModel (new [] { peer }, "TypeMap");
+ var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_TouchHandler_Proxy");
+ Assert.NotNull (proxy);
+
+ var nonCtorMethods = peer.MarshalMethods.Where (m => !m.IsConstructor).ToList ();
+ Assert.Equal (nonCtorMethods.Count, proxy!.UcoMethods.Count);
+
+ // onTouch: (Landroid/view/View;I)Z
+ var onTouchUco = proxy.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_OnTouch");
+ Assert.NotNull (onTouchUco);
+ Assert.Equal ("(Landroid/view/View;I)Z", onTouchUco!.JniSignature);
+
+ // onFocusChange: (Landroid/view/View;Z)V
+ var onFocusUco = proxy.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_OnFocusChange");
+ Assert.NotNull (onFocusUco);
+ Assert.Equal ("(Landroid/view/View;Z)V", onFocusUco!.JniSignature);
+
+ // onScroll: (IFJD)V
+ var onScrollUco = proxy.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_OnScroll");
+ Assert.NotNull (onScrollUco);
+ Assert.Equal ("(IFJD)V", onScrollUco!.JniSignature);
+
+ // getText: ()Ljava/lang/String;
+ var getTextUco = proxy.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_GetText");
+ Assert.NotNull (getTextUco);
+ Assert.Equal ("()Ljava/lang/String;", getTextUco!.JniSignature);
+
+ // setItems: ([Ljava/lang/String;)V
+ var setItemsUco = proxy.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_SetItems");
+ Assert.NotNull (setItemsUco);
+ Assert.Equal ("([Ljava/lang/String;)V", setItemsUco!.JniSignature);
+ }
+
}
public class FixtureCustomView
{
[Fact]
- public void Fixture_CustomView_HasTwoConstructors ()
+ public void Fixture_CustomView_HasTwoConstructorWrappers ()
{
var peer = FindFixtureByJavaName ("my/app/CustomView");
var model = BuildModel (new [] { peer }, "TypeMap");
var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_CustomView_Proxy");
Assert.NotNull (proxy);
+
+ if (proxy!.IsAcw) {
+ Assert.Equal (2, proxy.UcoConstructors.Count);
+ Assert.Equal ("nctor_0_uco", proxy.UcoConstructors [0].WrapperName);
+ Assert.Equal ("nctor_1_uco", proxy.UcoConstructors [1].WrapperName);
+ Assert.Equal ("MyApp.CustomView", proxy.UcoConstructors [0].TargetType.ManagedTypeName);
+ Assert.Equal ("MyApp.CustomView", proxy.UcoConstructors [1].TargetType.ManagedTypeName);
+
+ // Constructor JNI signatures should be propagated
+ Assert.Equal ("()V", proxy.UcoConstructors [0].JniSignature);
+ Assert.Equal ("(Landroid/content/Context;)V", proxy.UcoConstructors [1].JniSignature);
+
+ // Constructor registrations must use the actual JNI signatures
+ var ctorRegs = proxy.NativeRegistrations.Where (r => r.JniMethodName.StartsWith ("nctor_")).ToList ();
+ Assert.Equal (2, ctorRegs.Count);
+ Assert.Equal ("()V", ctorRegs [0].JniSignature);
+ Assert.Equal ("(Landroid/content/Context;)V", ctorRegs [1].JniSignature);
+ }
}
}
@@ -479,6 +862,36 @@ public void Fixture_AcwType_HasProxy (string javaName, string expectedProxyName)
if (peer.ActivationCtor != null && peer.MarshalMethods.Count > 0) {
var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == expectedProxyName);
Assert.NotNull (proxy);
+ Assert.True (proxy!.IsAcw);
+ }
+ }
+
+ [Fact]
+ public void Fixture_ClickableView_HasOnClickUcoWrapper ()
+ {
+ var peer = FindFixtureByJavaName ("my/app/ClickableView");
+ var model = BuildModel (new [] { peer }, "TypeMap");
+
+ if (peer.ActivationCtor != null && peer.MarshalMethods.Count > 0) {
+ var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_ClickableView_Proxy");
+ Assert.NotNull (proxy);
+ var onClick = proxy!.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_OnClick");
+ Assert.NotNull (onClick);
+ Assert.Equal ("(Landroid/view/View;)V", onClick!.JniSignature);
+ }
+ }
+
+ [Fact]
+ public void Fixture_MultiInterfaceView_HasAllUcoMethods ()
+ {
+ var peer = FindFixtureByJavaName ("my/app/MultiInterfaceView");
+ var model = BuildModel (new [] { peer }, "TypeMap");
+
+ if (peer.ActivationCtor != null && peer.MarshalMethods.Count > 0) {
+ var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_MultiInterfaceView_Proxy");
+ Assert.NotNull (proxy);
+ Assert.NotNull (proxy!.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_OnClick"));
+ Assert.NotNull (proxy.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_OnLongClick"));
}
}
@@ -589,6 +1002,31 @@ public void FullPipeline_AllFixtures_TypeMapAttributeCountMatchesEntries ()
});
}
+ [Fact]
+ public void FullPipeline_TouchHandler_AcwProxyHasUcoAttributes ()
+ {
+ var peer = FindFixtureByJavaName ("my/app/TouchHandler");
+ var model = BuildModel (new [] { peer }, "UcoAttrTest");
+
+ EmitAndVerify (model, "UcoAttrTest", (pe, reader) => {
+ var proxy = reader.TypeDefinitions
+ .Select (h => reader.GetTypeDefinition (h))
+ .First (t => reader.GetString (t.Name) == "MyApp_TouchHandler_Proxy");
+
+ var methods = proxy.GetMethods ()
+ .Select (h => reader.GetMethodDefinition (h))
+ .ToList ();
+
+ var ucoMethods = methods.Where (m => reader.GetString (m.Name).Contains ("_uco_")).ToList ();
+ Assert.NotEmpty (ucoMethods);
+
+ foreach (var uco in ucoMethods) {
+ var attrs = uco.GetCustomAttributes ().Select (h => reader.GetCustomAttribute (h)).ToList ();
+ Assert.NotEmpty (attrs);
+ }
+ });
+ }
+
[Fact]
public void FullPipeline_CustomView_HasConstructorAndMethodWrappers ()
{
@@ -607,6 +1045,47 @@ public void FullPipeline_CustomView_HasConstructorAndMethodWrappers ()
Assert.Contains (".ctor", methodNames);
Assert.Contains ("CreateInstance", methodNames);
Assert.Contains ("get_TargetType", methodNames);
+
+ if (model.ProxyTypes [0].IsAcw) {
+ Assert.Contains ("RegisterNatives", methodNames);
+ Assert.Contains (methodNames, m => m.StartsWith ("nctor_") && m.EndsWith ("_uco"));
+ }
+ });
+ }
+
+ [Fact]
+ public void FullPipeline_CustomView_UcoConstructorMatchesJniSignature ()
+ {
+ var peer = FindFixtureByJavaName ("my/app/CustomView");
+ var model = BuildModel (new [] { peer }, "CtorSigTest");
+
+ EmitAndVerify (model, "CtorSigTest", (pe, reader) => {
+ var proxy = reader.TypeDefinitions
+ .Select (h => reader.GetTypeDefinition (h))
+ .First (t => reader.GetString (t.Name) == "MyApp_CustomView_Proxy");
+
+ var ucoCtors = proxy.GetMethods ()
+ .Select (h => reader.GetMethodDefinition (h))
+ .Where (m => reader.GetString (m.Name).StartsWith ("nctor_") && reader.GetString (m.Name).EndsWith ("_uco"))
+ .ToList ();
+
+ Assert.NotEmpty (ucoCtors);
+
+ foreach (var uco in ucoCtors) {
+ var name = reader.GetString (uco.Name);
+ var modelUco = model.ProxyTypes
+ .SelectMany (p => p.UcoConstructors)
+ .First (u => u.WrapperName == name);
+
+ // UCO constructor signature: jnienv + self + JNI params
+ int expectedJniParams = JniSignatureHelper.ParseParameterTypes (modelUco.JniSignature).Count;
+ int expectedTotal = 2 + expectedJniParams;
+
+ var sig = reader.GetBlobReader (uco.Signature);
+ var header = sig.ReadSignatureHeader ();
+ int paramCount = sig.ReadCompressedInteger ();
+ Assert.Equal (expectedTotal, paramCount);
+ }
});
}