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/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs
new file mode 100644
index 00000000000..ee8af1ffa83
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs
@@ -0,0 +1,123 @@
+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 JNI parameter type descriptors from a JNI method signature
+/// and returns them as records.
+///
+public static List ParseParameterTypes (string jniSignature)
+{
+var result = new List ();
+int i = 1; // skip opening '('
+while (i < jniSignature.Length && jniSignature [i] != ')') {
+int start = i;
+ParseSingleType (jniSignature, ref i);
+result.Add (new JniParameterInfo { JniType = jniSignature.Substring (start, i - start) });
+}
+return result;
+}
+
+///
+/// Parses the parameter types from a JNI method signature as values.
+///
+public static List ParseParameterKinds (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;
+}
+
+///
+/// Extracts the return type descriptor from a JNI method signature.
+///
+public static string ParseReturnTypeString (string jniSignature)
+{
+int i = jniSignature.IndexOf (')') + 1;
+return jniSignature.Substring (i);
+}
+
+///
+/// Parses the return type from a JNI method signature.
+///
+public static JniParamKind ParseReturnKind (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': 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':
+int end = sig.IndexOf (';', i);
+if (end < 0) {
+throw new ArgumentException ($"Malformed JNI signature: missing ';' after 'L' at index {i} in '{sig}'");
+}
+i = end + 1;
+return JniParamKind.Object;
+case '[':
+i++;
+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 13d6952378e..8390630ee1a 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)
@@ -369,6 +449,106 @@ void EmitTypeGetter (string methodName, TypeRefData typeRef, MethodAttributes at
});
}
+ MethodDefinitionHandle EmitUcoMethod (UcoMethodData uco)
+ {
+ var jniParams = JniSignatureHelper.ParseParameterKinds (uco.JniSignature);
+ var returnKind = JniSignatureHelper.ParseReturnKind (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.ParseParameterKinds (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 df714fdb52d..e76c3810ea7 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs
@@ -1,3 +1,6 @@
+using System;
+using System.Collections.Generic;
+
namespace Microsoft.Android.Sdk.TrimmableTypeMap;
///
@@ -13,6 +16,13 @@ sealed record JavaPeerInfo
///
public required string JavaName { get; init; }
+ ///
+ /// Compat JNI type name, e.g., "myapp.namespace/MyType" for user types (uses raw namespace, not CRC64).
+ /// For MCW binding types (with [Register]), this equals .
+ /// Used by acw-map.txt to support legacy custom view name resolution in layout XMLs.
+ ///
+ public required string CompatJniName { get; init; }
+
///
/// Full managed type name, e.g., "Android.App.Activity".
///
@@ -49,6 +59,20 @@ sealed record JavaPeerInfo
///
public bool IsUnconditional { get; init; }
+ ///
+ /// Marshal methods: methods with [Register(name, sig, connector)], [Export], or
+ /// constructor registrations ([Register(".ctor", sig, "")] / [JniConstructorSignature]).
+ /// Constructors are identified by .
+ /// Ordered — the index in this list is the method's ordinal for RegisterNatives.
+ ///
+ 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.
@@ -68,6 +92,127 @@ sealed record JavaPeerInfo
public bool IsGenericDefinition { get; init; }
}
+///
+/// Describes a marshal method (a method with [Register] or [Export]) on a Java peer type.
+/// Contains all data needed to generate a UCO wrapper, a JCW native declaration,
+/// and a RegisterNatives call.
+///
+sealed record MarshalMethodInfo
+{
+ ///
+ /// JNI method name, e.g., "onCreate".
+ /// This is the Java method name (without n_ prefix).
+ ///
+ public required string JniName { get; init; }
+
+ ///
+ /// JNI method signature, e.g., "(Landroid/os/Bundle;)V".
+ /// Contains both parameter types and return type.
+ ///
+ public required string JniSignature { get; init; }
+
+ ///
+ /// The connector string from [Register], e.g., "GetOnCreate_Landroid_os_Bundle_Handler".
+ /// Null for [Export] methods.
+ ///
+ public string? Connector { get; init; }
+
+ ///
+ /// Name of the managed method this maps to, e.g., "OnCreate".
+ ///
+ public required string ManagedMethodName { get; init; }
+
+ ///
+ /// Full name of the type that declares the managed method (may be a base type).
+ /// Empty when the declaring type is the same as the peer type.
+ ///
+ public string DeclaringTypeName { get; init; } = "";
+
+ ///
+ /// Assembly name of the type that declares the managed method.
+ /// Needed for cross-assembly UCO wrapper generation.
+ /// Empty when the declaring type is the same as the peer type.
+ ///
+ public string DeclaringAssemblyName { get; init; } = "";
+
+ ///
+ /// The native callback method name, e.g., "n_onCreate".
+ /// This is the actual method the UCO wrapper delegates to.
+ ///
+ public required string NativeCallbackName { get; init; }
+
+ ///
+ /// JNI parameter types for UCO generation.
+ ///
+ public IReadOnlyList Parameters { get; init; } = Array.Empty ();
+
+ ///
+ /// JNI return type descriptor, e.g., "V", "Landroid/os/Bundle;".
+ ///
+ public required string JniReturnType { get; init; }
+
+ ///
+ /// True if this is a constructor registration.
+ ///
+ public bool IsConstructor { get; init; }
+
+ ///
+ /// For [Export] methods: Java exception types that the method declares it can throw.
+ /// Null for [Register] methods.
+ ///
+ public IReadOnlyList? ThrownNames { get; init; }
+
+ ///
+ /// For [Export] methods: super constructor arguments string.
+ /// Null for [Register] methods.
+ ///
+ public string? SuperArgumentsString { get; init; }
+}
+
+///
+/// Describes a JNI parameter for UCO method generation.
+///
+sealed record JniParameterInfo
+{
+ ///
+ /// JNI type descriptor, e.g., "Landroid/os/Bundle;", "I", "Z".
+ ///
+ public required string JniType { get; init; }
+
+ ///
+ /// Managed parameter type name, e.g., "Android.OS.Bundle", "System.Int32".
+ ///
+ 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 82b13d1cb3f..52b318a98d2 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Reflection.Metadata;
@@ -163,6 +164,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary results
// 3. Extends a known Java peer → auto-compute JNI name via CRC64
// 4. None of the above → not a Java peer, skip
string? jniName = null;
+ string? compatJniName = null;
bool doNotGenerateAcw = false;
index.RegisterInfoByType.TryGetValue (typeHandle, out var registerInfo);
@@ -170,15 +172,17 @@ void ScanAssembly (AssemblyIndex index, Dictionary results
if (registerInfo is not null && !string.IsNullOrEmpty (registerInfo.JniName)) {
jniName = registerInfo.JniName;
+ compatJniName = jniName;
doNotGenerateAcw = registerInfo.DoNotGenerateAcw;
} else if (attrInfo?.JniName is not null) {
// User type with [Activity(Name = "...")] but no [Register]
jniName = attrInfo.JniName;
+ compatJniName = jniName;
} else {
// No explicit JNI name — check if this type extends a known Java peer.
// If so, auto-compute JNI name from the managed type name via CRC64.
if (ExtendsJavaPeer (typeDef, index)) {
- jniName = ComputeAutoJniName (typeDef, index);
+ (jniName, compatJniName) = ComputeAutoJniNames (typeDef, index);
} else {
continue;
}
@@ -193,6 +197,9 @@ void ScanAssembly (AssemblyIndex index, Dictionary results
var isUnconditional = attrInfo is not null;
string? invokerTypeName = null;
+ // Collect marshal methods (including constructors) in a single pass over methods
+ var marshalMethods = CollectMarshalMethods (typeDef, index);
+
// Resolve activation constructor
var activationCtor = ResolveActivationCtor (fullName, typeDef, index);
@@ -203,6 +210,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary results
var peer = new JavaPeerInfo {
JavaName = jniName,
+ CompatJniName = compatJniName,
ManagedTypeName = fullName,
ManagedTypeNamespace = ExtractNamespace (fullName),
ManagedTypeShortName = ExtractShortName (fullName),
@@ -211,6 +219,8 @@ void ScanAssembly (AssemblyIndex index, Dictionary results
IsAbstract = isAbstract,
DoNotGenerateAcw = doNotGenerateAcw,
IsUnconditional = isUnconditional,
+ MarshalMethods = marshalMethods,
+ JavaConstructors = BuildJavaConstructors (marshalMethods),
ActivationCtor = activationCtor,
InvokerTypeName = invokerTypeName,
IsGenericDefinition = isGenericDefinition,
@@ -220,6 +230,186 @@ void ScanAssembly (AssemblyIndex index, Dictionary results
}
}
+ List CollectMarshalMethods (TypeDefinition typeDef, AssemblyIndex index)
+ {
+ var methods = new List ();
+
+ // Single pass over methods: collect marshal methods (including constructors)
+ foreach (var methodHandle in typeDef.GetMethods ()) {
+ var methodDef = index.Reader.GetMethodDefinition (methodHandle);
+ if (!TryGetMethodRegisterInfo (methodDef, index, out var registerInfo, out var exportInfo) || registerInfo is null) {
+ continue;
+ }
+
+ AddMarshalMethod (methods, registerInfo, methodDef, index, exportInfo);
+ }
+
+ // Collect [Register] from properties (attribute is on the property, not the getter)
+ foreach (var propHandle in typeDef.GetProperties ()) {
+ var propDef = index.Reader.GetPropertyDefinition (propHandle);
+ var propRegister = TryGetPropertyRegisterInfo (propDef, index);
+ if (propRegister is null) {
+ continue;
+ }
+
+ var accessors = propDef.GetAccessors ();
+ if (!accessors.Getter.IsNil) {
+ var getterDef = index.Reader.GetMethodDefinition (accessors.Getter);
+ AddMarshalMethod (methods, propRegister, getterDef, index);
+ }
+ }
+
+ return methods;
+ }
+
+ static void AddMarshalMethod (List methods, RegisterInfo registerInfo, MethodDefinition methodDef, AssemblyIndex index, ExportInfo? exportInfo = null)
+ {
+ // Skip methods that are just the JNI name (type-level [Register])
+ if (registerInfo.Signature is null && registerInfo.Connector is null) {
+ return;
+ }
+
+ bool isConstructor = registerInfo.JniName == "" || registerInfo.JniName == ".ctor";
+ string nativeCallbackName = $"n_{index.Reader.GetString (methodDef.Name)}";
+ if (isConstructor) {
+ int ctorIndex = 0;
+ foreach (var method in methods) {
+ if (method.IsConstructor) {
+ ctorIndex++;
+ }
+ }
+ nativeCallbackName = ctorIndex == 0 ? "n_ctor" : $"n_ctor_{ctorIndex}";
+ }
+
+ methods.Add (new MarshalMethodInfo {
+ JniName = registerInfo.JniName,
+ JniSignature = registerInfo.Signature ?? "()V",
+ Connector = registerInfo.Connector,
+ ManagedMethodName = index.Reader.GetString (methodDef.Name),
+ NativeCallbackName = nativeCallbackName,
+ JniReturnType = JniSignatureHelper.ParseReturnTypeString (registerInfo.Signature ?? "()V"),
+ Parameters = JniSignatureHelper.ParseParameterTypes (registerInfo.Signature ?? "()V"),
+ IsConstructor = isConstructor,
+ ThrownNames = exportInfo?.ThrownNames,
+ SuperArgumentsString = exportInfo?.SuperArgumentsString,
+ });
+ }
+
+ static bool TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex index, out RegisterInfo? registerInfo, out ExportInfo? exportInfo)
+ {
+ exportInfo = null;
+ foreach (var caHandle in methodDef.GetCustomAttributes ()) {
+ var ca = index.Reader.GetCustomAttribute (caHandle);
+ var attrName = AssemblyIndex.GetCustomAttributeName (ca, index.Reader);
+
+ if (attrName == "RegisterAttribute") {
+ registerInfo = index.ParseRegisterAttribute (ca);
+ return true;
+ }
+
+ if (attrName == "ExportAttribute") {
+ (registerInfo, exportInfo) = ParseExportAttribute (ca, methodDef, index);
+ return true;
+ }
+ }
+ registerInfo = null;
+ return false;
+ }
+
+ static RegisterInfo? TryGetPropertyRegisterInfo (PropertyDefinition propDef, AssemblyIndex index)
+ {
+ foreach (var caHandle in propDef.GetCustomAttributes ()) {
+ var ca = index.Reader.GetCustomAttribute (caHandle);
+ var attrName = AssemblyIndex.GetCustomAttributeName (ca, index.Reader);
+
+ if (attrName == "RegisterAttribute") {
+ return index.ParseRegisterAttribute (ca);
+ }
+ }
+ return null;
+ }
+
+ static (RegisterInfo registerInfo, ExportInfo exportInfo) ParseExportAttribute (CustomAttribute ca, MethodDefinition methodDef, AssemblyIndex index)
+ {
+ var value = index.DecodeAttribute (ca);
+
+ // [Export("name")] or [Export] (uses method name)
+ string? exportName = null;
+ if (value.FixedArguments.Length > 0) {
+ exportName = (string?)value.FixedArguments [0].Value;
+ }
+
+ List? thrownNames = null;
+ string? superArguments = null;
+
+ // Check Named arguments
+ foreach (var named in value.NamedArguments) {
+ if (named.Name == "Name" && named.Value is string name) {
+ exportName = name;
+ } else if (named.Name == "ThrownNames" && named.Value is ImmutableArray> names) {
+ thrownNames = new List (names.Length);
+ foreach (var item in names) {
+ if (item.Value is string s) {
+ thrownNames.Add (s);
+ }
+ }
+ } else if (named.Name == "SuperArgumentsString" && named.Value is string superArgs) {
+ superArguments = superArgs;
+ }
+ }
+
+ if (string.IsNullOrEmpty (exportName)) {
+ exportName = index.Reader.GetString (methodDef.Name);
+ }
+ string resolvedExportName = exportName ?? throw new InvalidOperationException ("Export name should not be null at this point.");
+
+ // Build JNI signature from method signature
+ var sig = methodDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default);
+ var jniSig = BuildJniSignatureFromManaged (sig);
+
+ return (
+ new RegisterInfo { JniName = resolvedExportName, Signature = jniSig, Connector = null, DoNotGenerateAcw = false },
+ new ExportInfo { ThrownNames = thrownNames, SuperArgumentsString = superArguments }
+ );
+ }
+
+ static string BuildJniSignatureFromManaged (MethodSignature sig)
+ {
+ var sb = new System.Text.StringBuilder ();
+ sb.Append ('(');
+ foreach (var param in sig.ParameterTypes) {
+ sb.Append (ManagedTypeToJniDescriptor (param));
+ }
+ sb.Append (')');
+ sb.Append (ManagedTypeToJniDescriptor (sig.ReturnType));
+ return sb.ToString ();
+ }
+
+ static string ManagedTypeToJniDescriptor (string managedType)
+ {
+ switch (managedType) {
+ case "System.Void": return "V";
+ case "System.Boolean": return "Z";
+ case "System.Byte":
+ case "System.SByte": return "B";
+ case "System.Char": return "C";
+ case "System.Int16":
+ case "System.UInt16": return "S";
+ case "System.Int32":
+ case "System.UInt32": return "I";
+ case "System.Int64":
+ case "System.UInt64": return "J";
+ case "System.Single": return "F";
+ case "System.Double": return "D";
+ case "System.String": return "Ljava/lang/String;";
+ default:
+ if (managedType.EndsWith ("[]")) {
+ return $"[{ManagedTypeToJniDescriptor (managedType.Substring (0, managedType.Length - 2))}";
+ }
+ return "Ljava/lang/Object;";
+ }
+ }
+
ActivationCtorInfo? ResolveActivationCtor (string typeName, TypeDefinition typeDef, AssemblyIndex index)
{
var cacheKey = (typeName, index.AssemblyName);
@@ -426,21 +616,29 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index)
}
///
- /// Compute JNI name for a type without [Register] or component Name.
+ /// Compute both JNI name and compat JNI name for a type without [Register] or component Name.
/// JNI name uses CRC64 hash of "namespace:assemblyName" for the package.
- /// If a declaring type has [Register], its JNI name is used as prefix.
+ /// Compat JNI name uses the raw managed namespace (lowercased).
+ /// If a declaring type has [Register], its JNI name is used as prefix for both.
/// Generic backticks are replaced with _.
///
- static string ComputeAutoJniName (TypeDefinition typeDef, AssemblyIndex index)
+ static (string jniName, string compatJniName) ComputeAutoJniNames (TypeDefinition typeDef, AssemblyIndex index)
{
var (typeName, parentJniName, ns) = ComputeTypeNameParts (typeDef, index);
if (parentJniName is not null) {
- return $"{parentJniName}_{typeName}";
+ var name = $"{parentJniName}_{typeName}";
+ return (name, name);
}
var packageName = GetCrc64PackageName (ns, index.AssemblyName);
- return $"{packageName}/{typeName}";
+ var jniName = $"{packageName}/{typeName}";
+
+ string compatName = ns.Length == 0
+ ? typeName
+ : $"{ns.ToLowerInvariant ().Replace ('.', '/')}/{typeName}";
+
+ return (jniName, compatName);
}
///
@@ -514,4 +712,23 @@ static string ExtractShortName (string fullName)
int lastPlus = typePart.LastIndexOf ('+');
return (lastPlus >= 0 ? typePart.Slice (lastPlus + 1) : typePart).ToString ();
}
+
+ 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 605a8127fba..3b62a8be94b 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs
@@ -40,6 +40,7 @@ protected static JavaPeerInfo MakeMcwPeer (string jniName, string managedName, s
var shortName = typePart.Contains ('+') ? typePart.Substring (typePart.LastIndexOf ('+') + 1) : typePart;
return new JavaPeerInfo {
JavaName = jniName,
+ CompatJniName = jniName,
ManagedTypeName = managedName,
ManagedTypeNamespace = ns,
ManagedTypeShortName = shortName,
@@ -57,18 +58,36 @@ protected static JavaPeerInfo MakePeerWithActivation (string jniName, string man
}
protected static JavaPeerInfo MakeAcwPeer (string jniName, string managedName, string asmName)
- => MakePeerWithActivation (jniName, managedName, asmName);
+ {
+ 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 = "",
+ NativeCallbackName = "n_ctor",
+ JniSignature = "()V",
+ JniReturnType = "V",
+ ManagedMethodName = ".ctor",
+ IsConstructor = true,
+ },
+ };
+ return peer;
+ }
protected static JavaPeerInfo MakeInterfacePeer (
- string jniName,
- string managedName,
- string asmName,
- string invokerName)
+ string jniName = "android/view/View$OnClickListener",
+ string managedName = "Android.Views.View+IOnClickListener",
+ string asmName = "Mono.Android",
+ string invokerName = "Android.Views.View+IOnClickListenerInvoker")
{
var ns = managedName.Contains ('.') ? managedName.Substring (0, managedName.LastIndexOf ('.')) : "";
var shortName = managedName.Contains ('.') ? managedName.Substring (managedName.LastIndexOf ('.') + 1) : managedName;
return new JavaPeerInfo {
JavaName = jniName,
+ CompatJniName = jniName,
ManagedTypeName = managedName,
ManagedTypeNamespace = ns,
ManagedTypeShortName = shortName,
@@ -77,4 +96,16 @@ protected static JavaPeerInfo MakeInterfacePeer (
InvokerTypeName = invokerName,
};
}
+
+ protected static MarshalMethodInfo MakeMarshalMethod (string jniName, string callbackName, string jniSig, bool isConstructor = false)
+ {
+ return new MarshalMethodInfo {
+ JniName = jniName,
+ NativeCallbackName = callbackName,
+ JniSignature = jniSig,
+ JniReturnType = JniSignatureHelper.ParseReturnTypeString (jniSig),
+ ManagedMethodName = jniName,
+ IsConstructor = isConstructor,
+ };
+ }
}
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs
index cceb2e20a62..f8d5ed0f4d5 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);
@@ -158,6 +216,7 @@ public class Alias : IDisposable
static List MakeDuplicateAliasPeers () => new List {
new JavaPeerInfo {
JavaName = "test/Duplicate",
+ CompatJniName = "test/Duplicate",
ManagedTypeName = "Test.Duplicate1",
ManagedTypeNamespace = "Test",
ManagedTypeShortName = "Duplicate1",
@@ -170,6 +229,7 @@ public class Alias : IDisposable
},
new JavaPeerInfo {
JavaName = "test/Duplicate",
+ CompatJniName = "test/Duplicate",
ManagedTypeName = "Test.Duplicate2",
ManagedTypeNamespace = "Test",
ManagedTypeShortName = "Duplicate2",
@@ -228,7 +288,69 @@ 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 ParseParameterKinds_ParsesCorrectCount (string signature, int expectedCount)
+{
+var actual = JniSignatureHelper.ParseParameterKinds (signature);
+Assert.Equal (expectedCount, actual.Count);
+}
+
+[Theory]
+[InlineData ("(Z)V", JniParamKind.Boolean)]
+[InlineData ("(Ljava/lang/String;)V", JniParamKind.Object)]
+public void ParseParameterKinds_SingleParam_MapsToCorrectKind (string signature, JniParamKind expectedKind)
+{
+var types = JniSignatureHelper.ParseParameterKinds (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.ParseReturnKind (signature));
+}
+
+}
+
+public class NegativeEdgeCase
+{
+
+[Fact]
+public void ParseParameterKinds_EmptyString_ReturnsEmptyList ()
+{
+Assert.Empty (JniSignatureHelper.ParseParameterKinds (""));
+}
+
+[Fact]
+public void ParseParameterKinds_InvalidSignature_Throws ()
+{
+Assert.ThrowsAny (() => JniSignatureHelper.ParseParameterKinds ("not-a-sig"));
+}
+
+[Fact]
+public void ParseParameterKinds_UnterminatedSignature_ReturnsEmptyList ()
+{
+Assert.Empty (JniSignatureHelper.ParseParameterKinds ("("));
+}
+
+}
+
+
+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 9e1f167edd4..3f6a3487e29 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
@@ -195,7 +214,7 @@ public void Build_PeerWithActivationCtor_CreatesProxy ()
[Fact]
public void Build_PeerWithInvoker_CreatesProxy ()
{
- var peer = MakeInterfacePeer ("android/view/View$OnClickListener", "Android.Views.View+IOnClickListener", "Mono.Android", "Android.Views.View+IOnClickListenerInvoker");
+ var peer = MakeInterfacePeer ();
var model = BuildModel (new [] { peer });
Assert.Single (model.ProxyTypes);
@@ -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);
+ }
}
}
@@ -476,9 +859,39 @@ public void Fixture_AcwType_HasProxy (string javaName, string expectedProxyName)
var model = BuildModel (new [] { peer }, "TypeMap");
- if (peer.ActivationCtor != null) {
+ 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.ParseParameterKinds (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);
+ }
});
}
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs
index 7a654f4693d..81aabe9ad74 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs
@@ -1,9 +1,61 @@
+using System.Linq;
using Xunit;
namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests;
public partial class JavaPeerScannerTests
{
+ [Theory]
+ [InlineData ("android/app/Activity", "OnCreate", "onCreate", "(Landroid/os/Bundle;)V")]
+ [InlineData ("android/app/Activity", "OnStart", "onStart", "()V")]
+ [InlineData ("my/app/MainActivity", "OnCreate", "onCreate", "(Landroid/os/Bundle;)V")]
+ [InlineData ("my/app/AbstractBase", "DoWork", "doWork", "()V")]
+ [InlineData ("java/lang/Throwable", "Message", "getMessage", "()Ljava/lang/String;")]
+ [InlineData ("my/app/TouchHandler", "OnTouch", "onTouch", "(Landroid/view/View;I)Z")]
+ [InlineData ("my/app/TouchHandler", "OnFocusChange", "onFocusChange", "(Landroid/view/View;Z)V")]
+ [InlineData ("my/app/TouchHandler", "OnScroll", "onScroll", "(IFJD)V")]
+ [InlineData ("my/app/TouchHandler", "SetItems", "setItems", "([Ljava/lang/String;)V")]
+ public void Scan_MarshalMethod_HasCorrectSignature (string javaName, string managedName, string jniName, string jniSig)
+ {
+ var peers = ScanFixtures ();
+ var method = FindByJavaName (peers, javaName)
+ .MarshalMethods.FirstOrDefault (m => m.ManagedMethodName == managedName || m.JniName == jniName);
+ Assert.NotNull (method);
+ Assert.Equal (jniName, method.JniName);
+ Assert.Equal (jniSig, method.JniSignature);
+ }
+
+ [Fact]
+ public void Scan_MarshalMethod_ConstructorsAndSpecialCases ()
+ {
+ var peers = ScanFixtures ();
+
+ var ctors = FindByJavaName (peers, "my/app/CustomView")
+ .MarshalMethods.Where (m => m.IsConstructor).ToList ();
+ Assert.Equal (2, ctors.Count);
+ Assert.Equal ("()V", ctors [0].JniSignature);
+ Assert.Equal ("(Landroid/content/Context;)V", ctors [1].JniSignature);
+
+ Assert.DoesNotContain (FindByJavaName (peers, "my/app/MyHelper").MarshalMethods, m => m.IsConstructor);
+
+ var exportMethod = FindByJavaName (peers, "my/app/ExportExample").MarshalMethods.Single ();
+ Assert.Equal ("myExportedMethod", exportMethod.JniName);
+ Assert.Null (exportMethod.Connector);
+
+ var onStart = FindByJavaName (peers, "android/app/Activity")
+ .MarshalMethods.FirstOrDefault (m => m.JniName == "onStart");
+ Assert.NotNull (onStart);
+ Assert.Equal ("", onStart.Connector);
+
+ var onClick = FindByManagedName (peers, "Android.Views.IOnClickListener")
+ .MarshalMethods.FirstOrDefault (m => m.JniName == "onClick");
+ Assert.NotNull (onClick);
+ Assert.Equal ("(Landroid/view/View;)V", onClick.JniSignature);
+
+ Assert.Equal ("Android.Views.IOnClickListenerInvoker",
+ FindByManagedName (peers, "Android.Views.IOnClickListener").InvokerTypeName);
+ }
+
[Theory]
[InlineData ("android/app/Activity", "Android.App.Activity")]
[InlineData ("my/app/SimpleActivity", "Android.App.Activity")]
@@ -44,6 +96,24 @@ public void Scan_MultipleInterfaces_AllResolved ()
Assert.Empty (FindByJavaName (peers, "my/app/MyHelper").ImplementedInterfaceJavaNames);
}
+ [Theory]
+ [InlineData ("android/app/Activity", "android/app/Activity")]
+ [InlineData ("my/app/MainActivity", "my/app/MainActivity")]
+ public void Scan_CompatJniName (string javaName, string expectedCompat)
+ {
+ var peers = ScanFixtures ();
+ Assert.Equal (expectedCompat, FindByJavaName (peers, javaName).CompatJniName);
+ }
+
+ [Fact]
+ public void Scan_CompatJniName_UnregisteredType_UsesRawNamespace ()
+ {
+ var peers = ScanFixtures ();
+ var unregistered = FindByManagedName (peers, "MyApp.UnregisteredHelper");
+ Assert.StartsWith ("crc64", unregistered.JavaName);
+ Assert.Equal ("myapp/UnregisteredHelper", unregistered.CompatJniName);
+ }
+
[Fact]
public void Scan_CustomJniNameProviderAttribute_UsesNameFromAttribute ()
{
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs
index 4c7fa70aee1..ec368ed6a44 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs
@@ -1,3 +1,4 @@
+using System.Linq;
using Xunit;
namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests;
@@ -41,6 +42,8 @@ public void Scan_EmptyNamespace_Handled ()
{
var peers = ScanFixtures ();
Assert.Equal ("GlobalType", FindByJavaName (peers, "my/app/GlobalType").ManagedTypeName);
+ Assert.Equal ("GlobalUnregisteredType",
+ FindByManagedName (peers, "GlobalUnregisteredType").CompatJniName);
}
[Theory]
@@ -54,4 +57,13 @@ public void Scan_UnregisteredType_DiscoveredWithCrc64Name (string managedName)
Assert.StartsWith ("crc64", FindByManagedName (peers, managedName).JavaName);
}
+ [Fact]
+ public void Scan_ExportOnUnregisteredType_MethodDiscovered ()
+ {
+ var peers = ScanFixtures ();
+ var exportMethod = FindByManagedName (peers, "MyApp.UnregisteredExporter")
+ .MarshalMethods.FirstOrDefault (m => m.JniName == "doExportedWork");
+ Assert.NotNull (exportMethod);
+ Assert.Null (exportMethod.Connector);
+ }
}