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/JcwJavaSourceGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs
new file mode 100644
index 00000000000..8a0d02b49ac
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs
@@ -0,0 +1,287 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+///
+/// Generates JCW (Java Callable Wrapper) .java source files from scanned records.
+/// Only processes ACW types (where is false).
+///
+///
+/// Each generated .java file looks like this (pseudo-Java):
+///
+/// package com.example;
+///
+/// public class MainActivity
+/// extends android.app.Activity
+/// implements
+/// mono.android.IGCUserPeer,
+/// android.view.View.OnClickListener
+/// {
+/// static {
+/// mono.android.Runtime.registerNatives (MainActivity.class);
+/// }
+///
+/// public MainActivity (android.content.Context p0)
+/// {
+/// super (p0);
+/// if (getClass () == MainActivity.class) nctor_0 (p0);
+/// }
+/// private native void nctor_0 (android.content.Context p0);
+///
+/// @Override
+/// public void onCreate (android.os.Bundle p0)
+/// {
+/// n_onCreate (p0);
+/// }
+/// public native void n_onCreate (android.os.Bundle p0);
+/// }
+///
+///
+sealed class JcwJavaSourceGenerator
+{
+ ///
+ /// Generates .java source files for all ACW types and writes them to the output directory.
+ /// Returns the list of generated file paths.
+ ///
+ public IReadOnlyList Generate (IReadOnlyList types, string outputDirectory)
+ {
+ if (types is null) {
+ throw new ArgumentNullException (nameof (types));
+ }
+ if (outputDirectory is null) {
+ throw new ArgumentNullException (nameof (outputDirectory));
+ }
+
+ var generatedFiles = new List ();
+
+ foreach (var type in types) {
+ if (type.DoNotGenerateAcw || type.IsInterface) {
+ continue;
+ }
+
+ string filePath = GetOutputFilePath (type, outputDirectory);
+ string? dir = Path.GetDirectoryName (filePath);
+ if (dir != null) {
+ Directory.CreateDirectory (dir);
+ }
+
+ using var writer = new StreamWriter (filePath);
+ Generate (type, writer);
+ generatedFiles.Add (filePath);
+ }
+
+ return generatedFiles;
+ }
+
+ ///
+ /// Generates a single .java source file for the given type.
+ ///
+ internal void Generate (JavaPeerInfo type, TextWriter writer)
+ {
+ writer.NewLine = "\n";
+ WritePackageDeclaration (type, writer);
+ WriteClassDeclaration (type, writer);
+ WriteStaticInitializer (type, writer);
+ WriteConstructors (type, writer);
+ WriteMethods (type, writer);
+ WriteGCUserPeerMethods (writer);
+ WriteClassClose (writer);
+ }
+
+ static string GetOutputFilePath (JavaPeerInfo type, string outputDirectory)
+ {
+ JniSignatureHelper.ValidateJniName (type.JavaName);
+ string relativePath = type.JavaName + ".java";
+ return Path.Combine (outputDirectory, relativePath);
+ }
+
+ ///
+ /// Validates that the JNI name is well-formed: non-empty, each segment separated by '/'
+ /// contains only valid Java identifier characters (letters, digits, '_', '$').
+ /// This also prevents path traversal (e.g., ".." segments, rooted paths, backslashes).
+ ///
+ static void WritePackageDeclaration (JavaPeerInfo type, TextWriter writer)
+ {
+ string? package = JniSignatureHelper.GetJavaPackageName (type.JavaName);
+ if (package != null) {
+ writer.Write ("package ");
+ writer.Write (package);
+ writer.WriteLine (';');
+ writer.WriteLine ();
+ }
+ }
+
+ static void WriteClassDeclaration (JavaPeerInfo type, TextWriter writer)
+ {
+ string abstractModifier = type.IsAbstract && !type.IsInterface ? "abstract " : "";
+ string className = JniSignatureHelper.GetJavaSimpleName (type.JavaName);
+
+ writer.Write ($"public {abstractModifier}class {className}\n");
+
+ // extends clause
+ if (type.BaseJavaName != null) {
+ writer.WriteLine ($"\textends {JniSignatureHelper.JniNameToJavaName (type.BaseJavaName)}");
+ }
+
+ // implements clause — always includes IGCUserPeer, plus any implemented interfaces
+ writer.Write ("\timplements\n\t\tmono.android.IGCUserPeer");
+
+ foreach (var iface in type.ImplementedInterfaceJavaNames) {
+ writer.Write ($",\n\t\t{JniSignatureHelper.JniNameToJavaName (iface)}");
+ }
+
+ writer.WriteLine ();
+ writer.WriteLine ('{');
+ }
+
+ static void WriteStaticInitializer (JavaPeerInfo type, TextWriter writer)
+ {
+ string className = JniSignatureHelper.GetJavaSimpleName (type.JavaName);
+ writer.Write ($$"""
+ static {
+ mono.android.Runtime.registerNatives ({{className}}.class);
+ }
+
+
+""");
+ }
+
+ static void WriteConstructors (JavaPeerInfo type, TextWriter writer)
+ {
+ string simpleClassName = JniSignatureHelper.GetJavaSimpleName (type.JavaName);
+
+ foreach (var ctor in type.JavaConstructors) {
+ string parameters = FormatParameterList (ctor.Parameters);
+ string superArgs = ctor.SuperArgumentsString ?? FormatArgumentList (ctor.Parameters);
+ string args = FormatArgumentList (ctor.Parameters);
+
+ writer.Write ($$"""
+ public {{simpleClassName}} ({{parameters}})
+ {
+ super ({{superArgs}});
+ if (getClass () == {{simpleClassName}}.class) nctor_{{ctor.ConstructorIndex}} ({{args}});
+ }
+
+
+""");
+ }
+
+ // Write native constructor declarations
+ foreach (var ctor in type.JavaConstructors) {
+ string parameters = FormatParameterList (ctor.Parameters);
+ writer.WriteLine ($"\tprivate native void nctor_{ctor.ConstructorIndex} ({parameters});");
+ }
+
+ if (type.JavaConstructors.Count > 0) {
+ writer.WriteLine ();
+ }
+ }
+
+ static void WriteMethods (JavaPeerInfo type, TextWriter writer)
+ {
+ foreach (var method in type.MarshalMethods) {
+ if (method.IsConstructor) {
+ continue;
+ }
+
+ string javaReturnType = JniSignatureHelper.JniTypeToJava (method.JniReturnType);
+ bool isVoid = method.JniReturnType == "V";
+ string parameters = FormatParameterList (method.Parameters);
+ string args = FormatArgumentList (method.Parameters);
+ string returnPrefix = isVoid ? "" : "return ";
+
+ // throws clause for [Export] methods
+ string throwsClause = "";
+ if (method.ThrownNames != null && method.ThrownNames.Count > 0) {
+ throwsClause = $"\n\t\tthrows {string.Join (", ", method.ThrownNames)}";
+ }
+
+ if (method.Connector != null) {
+ writer.Write ($$"""
+
+ @Override
+ public {{javaReturnType}} {{method.JniName}} ({{parameters}}){{throwsClause}}
+ {
+ {{returnPrefix}}{{method.NativeCallbackName}} ({{args}});
+ }
+ public native {{javaReturnType}} {{method.NativeCallbackName}} ({{parameters}});
+
+""");
+ } else {
+ writer.Write ($$"""
+
+ public {{javaReturnType}} {{method.JniName}} ({{parameters}}){{throwsClause}}
+ {
+ {{returnPrefix}}{{method.NativeCallbackName}} ({{args}});
+ }
+ public native {{javaReturnType}} {{method.NativeCallbackName}} ({{parameters}});
+
+""");
+ }
+ }
+ }
+
+ static void WriteGCUserPeerMethods (TextWriter writer)
+ {
+ writer.Write ("""
+
+ private java.util.ArrayList refList;
+ public void monodroidAddReference (java.lang.Object obj)
+ {
+ if (refList == null)
+ refList = new java.util.ArrayList ();
+ refList.add (obj);
+ }
+
+ public void monodroidClearReferences ()
+ {
+ if (refList != null)
+ refList.clear ();
+ }
+
+""");
+ }
+
+ static void WriteClassClose (TextWriter writer)
+ {
+ writer.WriteLine ('}');
+ }
+
+ static string FormatParameterList (IReadOnlyList parameters)
+ {
+ if (parameters.Count == 0) {
+ return "";
+ }
+
+ var sb = new System.Text.StringBuilder ();
+ for (int i = 0; i < parameters.Count; i++) {
+ if (i > 0) {
+ sb.Append (", ");
+ }
+ sb.Append (JniSignatureHelper.JniTypeToJava (parameters [i].JniType));
+ sb.Append (" p");
+ sb.Append (i);
+ }
+ return sb.ToString ();
+ }
+
+ static string FormatArgumentList (IReadOnlyList parameters)
+ {
+ if (parameters.Count == 0) {
+ return "";
+ }
+
+ var sb = new System.Text.StringBuilder ();
+ for (int i = 0; i < parameters.Count; i++) {
+ if (i > 0) {
+ sb.Append (", ");
+ }
+ sb.Append ('p');
+ sb.Append (i);
+ }
+ return sb.ToString ();
+ }
+
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs
index d07bb062bd3..0e9ac52f93e 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,164 @@ 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");
+ }
+ }
+
+ ///
+ /// Validates that a JNI type name has the expected structure (e.g., "com/example/MyClass").
+ ///
+ internal static void ValidateJniName (string jniName)
+ {
+ if (string.IsNullOrEmpty (jniName)) {
+ throw new ArgumentException ("JNI name must not be null or empty.", nameof (jniName));
+ }
+
+ int segmentStart = 0;
+ for (int i = 0; i <= jniName.Length; i++) {
+ if (i == jniName.Length || jniName [i] == '/') {
+ if (i == segmentStart) {
+ throw new ArgumentException ($"JNI name '{jniName}' has an empty segment.", nameof (jniName));
+ }
+
+ // First char of a segment must not be a digit
+ char first = jniName [segmentStart];
+ if (first >= '0' && first <= '9') {
+ throw new ArgumentException ($"JNI name '{jniName}' has a segment starting with a digit.", nameof (jniName));
+ }
+
+ // All chars in the segment must be valid Java identifier chars
+ for (int j = segmentStart; j < i; j++) {
+ char c = jniName [j];
+ bool valid = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
+ (c >= '0' && c <= '9') || c == '_' || c == '$';
+ if (!valid) {
+ throw new ArgumentException ($"JNI name '{jniName}' contains invalid character '{c}'.", nameof (jniName));
+ }
+ }
+
+ segmentStart = i + 1;
+ }
+ }
+ }
+
+ ///
+ /// Converts a JNI type name to a Java source type name.
+ /// e.g., "android/app/Activity" \u2192 "android.app.Activity"
+ ///
+ internal static string JniNameToJavaName (string jniName)
+ {
+ return jniName.Replace ('/', '.');
+ }
+
+ ///
+ /// Extracts the Java package name from a JNI type name.
+ /// e.g., "com/example/MainActivity" \u2192 "com.example"
+ /// Returns null for types without a package.
+ ///
+ internal static string? GetJavaPackageName (string jniName)
+ {
+ int lastSlash = jniName.LastIndexOf ('/');
+ if (lastSlash < 0) {
+ return null;
+ }
+ return jniName.Substring (0, lastSlash).Replace ('/', '.');
+ }
+
+ ///
+ /// Extracts the simple Java class name from a JNI type name.
+ /// e.g., "com/example/MainActivity" \u2192 "MainActivity"
+ /// e.g., "com/example/Outer$Inner" \u2192 "Outer$Inner" (preserves nesting separator)
+ ///
+ internal static string GetJavaSimpleName (string jniName)
+ {
+ int lastSlash = jniName.LastIndexOf ('/');
+ return lastSlash >= 0 ? jniName.Substring (lastSlash + 1) : jniName;
+ }
+
+ ///
+ /// Converts a JNI type descriptor to a Java source type.
+ /// e.g., "V" \u2192 "void", "I" \u2192 "int", "Landroid/os/Bundle;" \u2192 "android.os.Bundle"
+ ///
+ internal static string JniTypeToJava (string jniType)
+ {
+ if (jniType.Length == 1) {
+ return jniType [0] switch {
+ 'V' => "void",
+ 'Z' => "boolean",
+ 'B' => "byte",
+ 'C' => "char",
+ 'S' => "short",
+ 'I' => "int",
+ 'J' => "long",
+ 'F' => "float",
+ 'D' => "double",
+ _ => throw new ArgumentException ($"Unknown JNI primitive type: {jniType}"),
+ };
+ }
+
+ // Array types: "[I" \u2192 "int[]", "[Ljava/lang/String;" \u2192 "java.lang.String[]"
+ if (jniType [0] == '[') {
+ return JniTypeToJava (jniType.Substring (1)) + "[]";
+ }
+
+ // Object types: "Landroid/os/Bundle;" \u2192 "android.os.Bundle"
+ if (jniType [0] == 'L' && jniType [jniType.Length - 1] == ';') {
+ return JniNameToJavaName (jniType.Substring (1, jniType.Length - 2));
+ }
+
+ throw new ArgumentException ($"Unknown JNI type descriptor: {jniType}");
+ }
}
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..e8d0c5d6ba3 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs
@@ -43,6 +43,19 @@ sealed record JavaPeerInfo
///
public required string AssemblyName { get; init; }
+ ///
+ /// JNI name of the base Java type, e.g., "android/app/Activity" for a type
+ /// that extends Activity. Null for java/lang/Object or types without a Java base.
+ /// Needed by JCW Java source generation ("extends" clause).
+ ///
+ public string? BaseJavaName { get; init; }
+
+ ///
+ /// JNI names of Java interfaces this type implements, e.g., ["android/view/View$OnClickListener"].
+ /// Needed by JCW Java source generation ("implements" clause).
+ ///
+ public IReadOnlyList ImplementedInterfaceJavaNames { get; init; } = Array.Empty ();
+
public bool IsInterface { get; init; }
public bool IsAbstract { get; init; }
@@ -67,6 +80,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 +198,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..eabc73f7ee9 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
@@ -197,6 +197,12 @@ void ScanAssembly (AssemblyIndex index, Dictionary results
var isUnconditional = attrInfo is not null;
string? invokerTypeName = null;
+ // Resolve base Java type name
+ var baseJavaName = ResolveBaseJavaName (typeDef, index, results);
+
+ // Resolve implemented Java interface names
+ var implementedInterfaces = ResolveImplementedInterfaceJavaNames (typeDef, index);
+
// Collect marshal methods (including constructors) in a single pass over methods
var marshalMethods = CollectMarshalMethods (typeDef, index);
@@ -215,11 +221,14 @@ void ScanAssembly (AssemblyIndex index, Dictionary results
ManagedTypeNamespace = ExtractNamespace (fullName),
ManagedTypeShortName = ExtractShortName (fullName),
AssemblyName = index.AssemblyName,
+ BaseJavaName = baseJavaName,
+ ImplementedInterfaceJavaNames = implementedInterfaces,
IsInterface = isInterface,
IsAbstract = isAbstract,
DoNotGenerateAcw = doNotGenerateAcw,
IsUnconditional = isUnconditional,
MarshalMethods = marshalMethods,
+ JavaConstructors = BuildJavaConstructors (marshalMethods),
ActivationCtor = activationCtor,
InvokerTypeName = invokerTypeName,
IsGenericDefinition = isGenericDefinition,
@@ -282,6 +291,51 @@ static void AddMarshalMethod (List methods, RegisterInfo regi
});
}
+ string? ResolveBaseJavaName (TypeDefinition typeDef, AssemblyIndex index, Dictionary results)
+ {
+ var baseInfo = GetBaseTypeInfo (typeDef, index);
+ if (baseInfo is null) {
+ return null;
+ }
+
+ var (baseTypeName, baseAssemblyName) = baseInfo.Value;
+
+ // First try [Register] attribute
+ var registerJniName = ResolveRegisterJniName (baseTypeName, baseAssemblyName);
+ if (registerJniName is not null) {
+ return registerJniName;
+ }
+
+ // Fall back to already-scanned results (component-attributed or CRC64-computed peers)
+ if (results.TryGetValue (baseTypeName, out var basePeer)) {
+ return basePeer.JavaName;
+ }
+
+ return null;
+ }
+
+ List ResolveImplementedInterfaceJavaNames (TypeDefinition typeDef, AssemblyIndex index)
+ {
+ var result = new List ();
+ var interfaceImpls = typeDef.GetInterfaceImplementations ();
+
+ foreach (var implHandle in interfaceImpls) {
+ var impl = index.Reader.GetInterfaceImplementation (implHandle);
+ var ifaceJniName = ResolveInterfaceJniName (impl.Interface, index);
+ if (ifaceJniName is not null) {
+ result.Add (ifaceJniName);
+ }
+ }
+
+ return result;
+ }
+
+ string? ResolveInterfaceJniName (EntityHandle interfaceHandle, AssemblyIndex index)
+ {
+ var resolved = ResolveEntityHandle (interfaceHandle, index);
+ return resolved is not null ? ResolveRegisterJniName (resolved.Value.typeName, resolved.Value.assemblyName) : null;
+ }
+
static bool TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex index, out RegisterInfo? registerInfo, out ExportInfo? exportInfo)
{
exportInfo = null;
@@ -708,4 +762,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..201db530238 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs
@@ -33,10 +33,16 @@ protected static JavaPeerInfo FindFixtureByJavaName (string javaName)
return peer;
}
- protected static void CleanUpDir (string path)
+ protected static string CreateTempDir ()
{
- var dir = Path.GetDirectoryName (path);
- if (dir != null && Directory.Exists (dir))
+ var dir = Path.Combine (Path.GetTempPath (), $"typemap-test-{Guid.NewGuid ():N}");
+ Directory.CreateDirectory (dir);
+ return dir;
+ }
+
+ protected static void DeleteTempDir (string dir)
+ {
+ if (Directory.Exists (dir))
try { Directory.Delete (dir, true); } catch { }
}
@@ -67,6 +73,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/JcwJavaSourceGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs
new file mode 100644
index 00000000000..049f4dbbeed
--- /dev/null
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs
@@ -0,0 +1,333 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Xunit;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests;
+
+public class JcwJavaSourceGeneratorTests : FixtureTestBase
+{
+ static string GenerateToString (JavaPeerInfo type)
+ {
+ var generator = new JcwJavaSourceGenerator ();
+ using var writer = new StringWriter ();
+ generator.Generate (type, writer);
+ return writer.ToString ();
+ }
+
+ static string GenerateFixture (string javaName)
+ {
+ var peer = FindFixtureByJavaName (javaName);
+ return GenerateToString (peer);
+ }
+
+
+ public class JniNameConversion
+ {
+
+ [Theory]
+ [InlineData ("android/app/Activity", "android.app.Activity")]
+ [InlineData ("java/lang/Object", "java.lang.Object")]
+ [InlineData ("android/view/View$OnClickListener", "android.view.View$OnClickListener")]
+ public void JniNameToJavaName_ConvertsCorrectly (string jniName, string expected)
+ {
+ Assert.Equal (expected, JniSignatureHelper.JniNameToJavaName (jniName));
+ }
+
+ [Theory]
+ [InlineData ("com/example/MainActivity", "com.example")]
+ [InlineData ("java/lang/Object", "java.lang")]
+ [InlineData ("TopLevelClass", null)]
+ public void GetJavaPackageName_ExtractsCorrectly (string jniName, string? expected)
+ {
+ Assert.Equal (expected, JniSignatureHelper.GetJavaPackageName (jniName));
+ }
+
+ [Theory]
+ [InlineData ("V", "void")]
+ [InlineData ("Z", "boolean")]
+ [InlineData ("B", "byte")]
+ [InlineData ("I", "int")]
+ [InlineData ("J", "long")]
+ [InlineData ("F", "float")]
+ [InlineData ("D", "double")]
+ [InlineData ("Landroid/os/Bundle;", "android.os.Bundle")]
+ [InlineData ("[I", "int[]")]
+ [InlineData ("[Ljava/lang/String;", "java.lang.String[]")]
+ public void JniTypeToJava_ConvertsCorrectly (string jniType, string expected)
+ {
+ Assert.Equal (expected, JniSignatureHelper.JniTypeToJava (jniType));
+ }
+
+ }
+
+ public class Filtering : IDisposable
+ {
+ readonly string _outputDir = CreateTempDir ();
+ public void Dispose () => DeleteTempDir (_outputDir);
+
+ [Fact]
+ public void Generate_SkipsMcwTypes ()
+ {
+ var peers = ScanFixtures ();
+ var generator = new JcwJavaSourceGenerator ();
+ var files = generator.Generate (peers, _outputDir);
+ Assert.DoesNotContain (files, f => f.EndsWith ("java/lang/Object.java"));
+ Assert.DoesNotContain (files, f => f.EndsWith ("android/app/Activity.java"));
+ Assert.Contains (files, f => f.Replace ('\\', '/').Contains ("my/app/MainActivity.java"));
+ }
+
+ }
+
+ public class ClassDeclaration
+ {
+
+ [Fact]
+ public void Generate_MainActivity_HasClassDeclaration ()
+ {
+ var java = GenerateFixture ("my/app/MainActivity");
+ Assert.Contains ("public class MainActivity\n", java);
+ Assert.Contains ("\textends android.app.Activity\n", java);
+ Assert.Contains ("\t\tmono.android.IGCUserPeer\n", java);
+ }
+
+ [Fact]
+ public void Generate_MainActivity_HasIGCUserPeerMethods ()
+ {
+ var java = GenerateFixture ("my/app/MainActivity");
+ Assert.Contains ("private java.util.ArrayList refList;", java);
+ Assert.Contains ("public void monodroidAddReference (java.lang.Object obj)", java);
+ Assert.Contains ("public void monodroidClearReferences ()", java);
+ }
+
+ [Fact]
+ public void Generate_AbstractType_HasAbstractModifier ()
+ {
+ var java = GenerateFixture ("my/app/AbstractBase");
+ Assert.Contains ("public abstract class AbstractBase\n", java);
+ }
+
+ }
+
+ public class StaticInitializer
+ {
+
+ [Fact]
+ public void Generate_AcwType_HasRegisterNativesStaticBlock ()
+ {
+ var java = GenerateFixture ("my/app/MainActivity");
+ Assert.Contains ("static {\n", java);
+ Assert.Contains ("mono.android.Runtime.registerNatives (MainActivity.class);\n", java);
+ }
+
+ }
+
+ public class Constructor
+ {
+
+ [Fact]
+ public void Generate_CustomView_HasExpectedConstructorElements ()
+ {
+ var java = GenerateFixture ("my/app/CustomView");
+ Assert.Contains ("public CustomView ()\n", java);
+ Assert.Contains ("public CustomView (android.content.Context p0)\n", java);
+ Assert.Contains ("private native void nctor_0 ();\n", java);
+ Assert.Contains ("private native void nctor_1 (android.content.Context p0);\n", java);
+ Assert.Contains ("if (getClass () == CustomView.class) nctor_0 ();\n", java);
+ }
+
+ [Fact]
+ public void Generate_Constructor_WithSuperArgumentsString_UsesCustomSuperArgs ()
+ {
+ // [Export] constructors with SuperArgumentsString should use it in super() call
+ var type = new JavaPeerInfo {
+ JavaName = "my/app/CustomService",
+ ManagedTypeName = "MyApp.CustomService",
+ ManagedTypeNamespace = "MyApp",
+ ManagedTypeShortName = "CustomService",
+ AssemblyName = "App",
+ BaseJavaName = "android/app/Service",
+ JavaConstructors = new List {
+ new JavaConstructorInfo {
+ JniSignature = "(Landroid/content/Context;I)V",
+ ConstructorIndex = 0,
+ Parameters = new List {
+ new JniParameterInfo { JniType = "Landroid/content/Context;" },
+ new JniParameterInfo { JniType = "I" },
+ },
+ SuperArgumentsString = "p0",
+ },
+ },
+ };
+
+ var java = GenerateToString (type);
+ Assert.Contains ("super (p0);", java);
+ Assert.DoesNotContain ("super (p0, p1);", java);
+ }
+
+ [Fact]
+ public void Generate_Constructor_WithEmptySuperArgumentsString_EmptySuper ()
+ {
+ // Empty string means super() with no arguments
+ var type = new JavaPeerInfo {
+ JavaName = "my/app/MyWidget",
+ ManagedTypeName = "MyApp.MyWidget",
+ ManagedTypeNamespace = "MyApp",
+ ManagedTypeShortName = "MyWidget",
+ AssemblyName = "App",
+ BaseJavaName = "android/appwidget/AppWidgetProvider",
+ JavaConstructors = new List {
+ new JavaConstructorInfo {
+ JniSignature = "(Landroid/content/Context;)V",
+ ConstructorIndex = 0,
+ Parameters = new List {
+ new JniParameterInfo { JniType = "Landroid/content/Context;" },
+ },
+ SuperArgumentsString = "",
+ },
+ },
+ };
+
+ var java = GenerateToString (type);
+ Assert.Contains ("super ();", java);
+ Assert.DoesNotContain ("super (p0);", java);
+ }
+
+ [Fact]
+ public void Generate_Constructor_WithoutSuperArgumentsString_ForwardsAllParams ()
+ {
+ // null SuperArgumentsString means forward all params (default behavior)
+ var type = new JavaPeerInfo {
+ JavaName = "my/app/MyView",
+ ManagedTypeName = "MyApp.MyView",
+ ManagedTypeNamespace = "MyApp",
+ ManagedTypeShortName = "MyView",
+ AssemblyName = "App",
+ BaseJavaName = "android/view/View",
+ JavaConstructors = new List {
+ new JavaConstructorInfo {
+ JniSignature = "(Landroid/content/Context;Landroid/util/AttributeSet;)V",
+ ConstructorIndex = 0,
+ Parameters = new List {
+ new JniParameterInfo { JniType = "Landroid/content/Context;" },
+ new JniParameterInfo { JniType = "Landroid/util/AttributeSet;" },
+ },
+ },
+ },
+ };
+
+ var java = GenerateToString (type);
+ Assert.Contains ("super (p0, p1);", java);
+ }
+
+ }
+
+ public class Method
+ {
+
+ [Fact]
+ public void Generate_MarshalMethod_HasOverrideAndNativeDeclaration ()
+ {
+ var java = GenerateFixture ("my/app/MainActivity");
+ Assert.Contains ("@Override\n", java);
+ Assert.Contains ("public void onCreate (android.os.Bundle p0)\n", java);
+ Assert.Contains ("n_OnCreate (p0);\n", java);
+ Assert.Contains ("public native void n_OnCreate (android.os.Bundle p0);\n", java);
+ }
+
+ }
+
+ public class NestedType
+ {
+
+ [Fact]
+ public void Generate_NestedType_HasCorrectPackageAndClassName ()
+ {
+ var java = GenerateFixture ("my/app/Outer$Inner");
+ Assert.Contains ("package my.app;\n", java);
+ Assert.Contains ("public class Outer$Inner\n", java);
+ }
+
+ }
+
+ public class OutputFilePath : IDisposable
+ {
+ readonly string _outputDir = CreateTempDir ();
+ public void Dispose () => DeleteTempDir (_outputDir);
+
+ [Fact]
+ public void Generate_CreatesCorrectFileStructure ()
+ {
+ var peers = ScanFixtures ();
+ var generator = new JcwJavaSourceGenerator ();
+ var files = generator.Generate (peers, _outputDir);
+ Assert.NotEmpty (files);
+
+ foreach (var file in files) {
+ Assert.StartsWith (_outputDir, file);
+ Assert.True (File.Exists (file), $"Generated file should exist: {file}");
+ Assert.EndsWith (".java", file);
+ }
+ }
+
+ [Theory]
+ [InlineData ("")]
+ [InlineData ("com//Example")]
+ [InlineData ("/com/Example")]
+ [InlineData ("com/Example/")]
+ [InlineData ("com/1Invalid")]
+ [InlineData ("com/../etc/passwd")]
+ [InlineData ("com\\..\\.\\secret")]
+ [InlineData ("C:\\Windows\\System32")]
+ [InlineData ("com/Ex:ample")]
+ [InlineData ("/absolute/path")]
+ public void Generate_InvalidJniName_Throws (string badJniName)
+ {
+ var peer = MakeAcwPeer (badJniName, "Test.Bad", "TestApp");
+ var generator = new JcwJavaSourceGenerator ();
+ Assert.Throws (() => generator.Generate (new [] { peer }, _outputDir));
+ }
+
+ [Theory]
+ [InlineData ("com/example/MainActivity")]
+ [InlineData ("my/app/Outer$Inner")]
+ [InlineData ("SingleSegment")]
+ [InlineData ("com/example/_Private")]
+ [InlineData ("com/example/$Generated")]
+ public void Generate_ValidJniName_DoesNotThrow (string validJniName)
+ {
+ var peer = MakeAcwPeer (validJniName, "Test.Valid", "TestApp");
+ var generator = new JcwJavaSourceGenerator ();
+ generator.Generate (new [] { peer }, _outputDir);
+ }
+
+ }
+
+ public class ExportWithThrowsClause
+ {
+
+ [Fact]
+ public void Generate_ExportWithThrows_HasThrowsClause ()
+ {
+ var java = GenerateFixture ("my/app/ExportWithThrows");
+ Assert.Contains ("throws java.io.IOException, java.lang.IllegalStateException\n", java);
+ }
+
+ }
+
+ public class MethodReturnTypesAndParams
+ {
+
+ [Fact]
+ public void Generate_TouchHandler_HasExpectedMethodSignatures ()
+ {
+ var java = GenerateFixture ("my/app/TouchHandler");
+ Assert.Contains ("public boolean onTouch (android.view.View p0, int p1)\n", java);
+ Assert.Contains ("public void onScroll (int p0, float p1, long p2, double p3)\n", java);
+ Assert.Contains ("public java.lang.String getText ()\n", java);
+ Assert.Contains ("public void setItems (java.lang.String[] p0)\n", java);
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs
index cceb2e20a62..cbb65cc2af2 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs
@@ -127,6 +127,64 @@ public void Generate_ProxyType_HasCtorAndCreateInstance ()
}
+ 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 ();
@@ -228,6 +286,67 @@ public void Generate_EmptyPeerList_ProducesValidAssembly ()
}
+ 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 ();
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);
+ }
});
}