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..9ac98afe134
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs
@@ -0,0 +1,291 @@
+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) {
+ var ctorParams = JniSignatureHelper.ParseParameters (ctor.JniSignature);
+ string parameters = FormatParameterList (ctorParams);
+ string superArgs = ctor.SuperArgumentsString ?? FormatArgumentList (ctorParams);
+ string args = FormatArgumentList (ctorParams);
+
+ 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) {
+ var nativeCtorParams = JniSignatureHelper.ParseParameters (ctor.JniSignature);
+ string parameters = FormatParameterList (nativeCtorParams);
+ 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 jniReturnType = JniSignatureHelper.ParseReturnTypeString (method.JniSignature);
+ string javaReturnType = JniSignatureHelper.JniTypeToJava (jniReturnType);
+ bool isVoid = jniReturnType == "V";
+ var methodParams = JniSignatureHelper.ParseParameters (method.JniSignature);
+ string parameters = FormatParameterList (methodParams);
+ string args = FormatArgumentList (methodParams);
+ 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
new file mode 100644
index 00000000000..abf66a4ffad
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs
@@ -0,0 +1,231 @@
+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 → byte (JNI jboolean is unsigned byte; must be blittable for UCO)
+ 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 JNI parameter type descriptors into JniParameterInfo records.
+ ///
+ public static List ParseParameters (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;
+ }
+
+ ///
+ /// Extracts the return type descriptor from a JNI method signature.
+ ///
+ public static string ParseReturnTypeString (string jniSignature)
+ {
+ int paren = jniSignature.IndexOf (')');
+ if (paren < 0)
+ throw new ArgumentException ($"Malformed JNI signature '{jniSignature}': missing ')'");
+ return jniSignature.Substring (paren + 1);
+ }
+
+ ///
+ /// Parses the return type from a JNI method signature.
+ ///
+ public static JniParamKind ParseReturnType (string jniSignature)
+ {
+ int paren = jniSignature.IndexOf (')');
+ if (paren < 0)
+ throw new ArgumentException ($"Malformed JNI signature '{jniSignature}': missing ')'");
+ int i = paren + 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 semi = sig.IndexOf (';', i);
+ if (semi < 0)
+ throw new ArgumentException ($"Malformed object type in '{sig}' at index {i}: missing ';'");
+ i = semi + 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.Byte (); break; // JNI jboolean is unsigned byte; must be blittable for UCO
+ 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 701f9cd4f2c..56484f7a99a 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs
@@ -113,6 +113,26 @@ sealed class JavaPeerProxyData
/// True if this is an open generic type definition. CreateInstance throws NotSupportedException.
///
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 ();
}
///
@@ -131,6 +151,78 @@ 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 949b034571a..3cb37a217d6 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs
@@ -85,6 +85,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);
}
@@ -110,10 +113,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, usedProxyNames);
+ proxy = BuildProxyType (peer, usedProxyNames, isAcw);
model.ProxyTypes.Add (proxy);
}
@@ -165,7 +169,7 @@ static void AddIfCrossAssembly (SortedSet set, string? asmName, string o
}
}
- static JavaPeerProxyData BuildProxyType (JavaPeerInfo peer, HashSet usedProxyNames)
+ static JavaPeerProxyData BuildProxyType (JavaPeerInfo peer, HashSet usedProxyNames, 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).
@@ -188,6 +192,7 @@ static JavaPeerProxyData BuildProxyType (JavaPeerInfo peer, HashSet used
ManagedTypeName = peer.ManagedTypeName,
AssemblyName = peer.AssemblyName,
},
+ IsAcw = isAcw,
IsGenericDefinition = peer.IsGenericDefinition,
};
@@ -210,9 +215,80 @@ static JavaPeerProxyData BuildProxyType (JavaPeerInfo peer, HashSet used
};
}
+ 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 f878997f04a..775f48af607 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs
@@ -19,8 +19,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() { }
///
@@ -35,9 +35,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")]
///
///
@@ -54,9 +70,12 @@ sealed class TypeMapAssemblyEmitter
TypeReferenceHandle _jniHandleOwnershipRef;
TypeReferenceHandle _jniObjectReferenceRef;
TypeReferenceHandle _jniObjectReferenceOptionsRef;
+ TypeReferenceHandle _iAndroidCallableWrapperRef;
TypeReferenceHandle _jniEnvRef;
TypeReferenceHandle _systemTypeRef;
TypeReferenceHandle _runtimeTypeHandleRef;
+ TypeReferenceHandle _jniTypeRef;
+ TypeReferenceHandle _trimmableNativeRegistrationRef;
TypeReferenceHandle _notSupportedExceptionRef;
TypeReferenceHandle _runtimeHelpersRef;
@@ -66,6 +85,10 @@ sealed class TypeMapAssemblyEmitter
MemberReferenceHandle _notSupportedExceptionCtorRef;
MemberReferenceHandle _jniObjectReferenceCtorRef;
MemberReferenceHandle _jniEnvDeleteRefRef;
+ MemberReferenceHandle _activateInstanceRef;
+ MemberReferenceHandle _registerMethodRef;
+ MemberReferenceHandle _ucoAttrCtorRef;
+ BlobHandle _ucoAttrBlobHandle;
MemberReferenceHandle _typeMapAttrCtorRef2Arg;
MemberReferenceHandle _typeMapAttrCtorRef3Arg;
MemberReferenceHandle _typeMapAssociationAttrCtorRef;
@@ -124,8 +147,11 @@ void EmitCore (TypeMapAssemblyData model)
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) {
@@ -154,10 +180,16 @@ void EmitTypeReferences ()
metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniObjectReference"));
_jniObjectReferenceOptionsRef = metadata.AddTypeReference (_javaInteropRef,
metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniObjectReferenceOptions"));
+ _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,
@@ -200,6 +232,33 @@ void EmitMemberReferences ()
p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true);
}));
+ _activateInstanceRef = _pe.AddMemberRef (_trimmableNativeRegistrationRef, "ActivateInstance",
+ sig => sig.MethodSignature ().Parameters (2,
+ rt => rt.Void (),
+ p => {
+ p.AddParameter ().Type ().IntPtr ();
+ p.AddParameter ().Type ().Type (_systemTypeRef, false);
+ }));
+
+ _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 ();
}
@@ -253,10 +312,10 @@ 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),
@@ -264,6 +323,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,
@@ -286,6 +349,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)
@@ -550,6 +629,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 c34d7f2009c..c563b970690 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs
@@ -31,12 +31,12 @@ sealed record JavaPeerInfo
///
/// Managed type namespace, e.g., "Android.App".
///
- public string ManagedTypeNamespace { get; init; } = "";
+ public required string ManagedTypeNamespace { get; init; }
///
/// Managed type short name (without namespace), e.g., "Activity".
///
- public string ManagedTypeShortName { get; init; } = "";
+ public required string ManagedTypeShortName { get; init; }
///
/// Assembly name the type belongs to, e.g., "Mono.Android".
@@ -80,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; } = [];
+
///
/// Information about the activation constructor for this type.
/// May reference a base type's constructor if the type doesn't define its own.
@@ -129,6 +135,25 @@ sealed record MarshalMethodInfo
///
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; }
+
///
/// True if this is a constructor registration.
///
@@ -147,6 +172,44 @@ sealed record MarshalMethodInfo
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; }
+
+ ///
+ /// 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 fc3627224f5..427bfb9507c 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
@@ -228,6 +228,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary results
DoNotGenerateAcw = doNotGenerateAcw,
IsUnconditional = isUnconditional,
MarshalMethods = marshalMethods,
+ JavaConstructors = BuildJavaConstructors (marshalMethods),
ActivationCtor = activationCtor,
InvokerTypeName = invokerTypeName,
IsGenericDefinition = isGenericDefinition,
@@ -276,12 +277,17 @@ static void AddMarshalMethod (List methods, RegisterInfo regi
return;
}
+ bool isConstructor = registerInfo.JniName == "" || registerInfo.JniName == ".ctor";
+ string managedName = index.Reader.GetString (methodDef.Name);
+ string jniSignature = registerInfo.Signature ?? "()V";
+
methods.Add (new MarshalMethodInfo {
JniName = registerInfo.JniName,
- JniSignature = registerInfo.Signature ?? "()V",
+ JniSignature = jniSignature,
Connector = registerInfo.Connector,
- ManagedMethodName = index.Reader.GetString (methodDef.Name),
- IsConstructor = registerInfo.JniName == "" || registerInfo.JniName == ".ctor",
+ ManagedMethodName = managedName,
+ NativeCallbackName = isConstructor ? "n_ctor" : $"n_{managedName}",
+ IsConstructor = isConstructor,
ThrownNames = exportInfo?.ThrownNames,
SuperArgumentsString = exportInfo?.SuperArgumentsString,
});
@@ -752,4 +758,22 @@ 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,
+ 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 70471f62e13..e73b18c460a 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs
@@ -77,7 +77,23 @@ private protected static JavaPeerInfo MakePeerWithActivation (string jniName, st
}
private protected static JavaPeerInfo MakeAcwPeer (string jniName, string managedName, string asmName)
- => MakePeerWithActivation (jniName, managedName, asmName);
+ {
+ return MakePeerWithActivation (jniName, managedName, asmName) with {
+ DoNotGenerateAcw = false,
+ JavaConstructors = new List {
+ new JavaConstructorInfo { ConstructorIndex = 0, JniSignature = "()V" },
+ },
+ MarshalMethods = new List {
+ new MarshalMethodInfo {
+ JniName = "",
+ NativeCallbackName = "n_ctor",
+ JniSignature = "()V",
+ ManagedMethodName = ".ctor",
+ IsConstructor = true,
+ },
+ },
+ };
+ }
private protected static JavaPeerInfo MakeInterfacePeer (
string 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..7a92683ffca
--- /dev/null
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs
@@ -0,0 +1,309 @@
+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 void AssertContainsLine (string expected, string actual)
+ {
+ Assert.Contains (
+ expected.ReplaceLineEndings ("\n"),
+ actual.ReplaceLineEndings ("\n")
+ );
+ }
+
+ 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
+ {
+
+ [Fact]
+ public void Generate_SkipsMcwTypes ()
+ {
+ var peers = ScanFixtures ();
+ var acwTypes = peers.Where (p => !p.DoNotGenerateAcw && !p.IsInterface).ToList ();
+ Assert.DoesNotContain (acwTypes, p => p.JavaName == "java/lang/Object");
+ Assert.DoesNotContain (acwTypes, p => p.JavaName == "android/app/Activity");
+ Assert.Contains (acwTypes, p => p.JavaName == "my/app/MainActivity");
+ }
+
+ }
+
+ 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");
+ AssertContainsLine ("static {\n", java);
+ AssertContainsLine ("mono.android.Runtime.registerNatives (MainActivity.class);\n", java);
+ }
+
+ }
+
+ public class Constructor
+ {
+
+ [Fact]
+ public void Generate_CustomView_HasExpectedConstructorElements ()
+ {
+ var java = GenerateFixture ("my/app/CustomView");
+ AssertContainsLine ("public CustomView ()\n", java);
+ AssertContainsLine ("public CustomView (android.content.Context p0)\n", java);
+ AssertContainsLine ("private native void nctor_0 ();\n", java);
+ AssertContainsLine ("private native void nctor_1 (android.content.Context p0);\n", java);
+ AssertContainsLine ("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",
+ CompatJniName = "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,
+ 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",
+ CompatJniName = "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,
+ 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",
+ CompatJniName = "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,
+ },
+ },
+ };
+
+ 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");
+ AssertContainsLine ("@Override\n", java);
+ AssertContainsLine ("public void onCreate (android.os.Bundle p0)\n", java);
+ AssertContainsLine ("n_OnCreate (p0);\n", java);
+ AssertContainsLine ("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 JniNameValidation
+ {
+
+ [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 ValidateJniName_InvalidName_Throws (string badJniName)
+ {
+ Assert.Throws (() => JniSignatureHelper.ValidateJniName (badJniName));
+ }
+
+ [Theory]
+ [InlineData ("com/example/MainActivity")]
+ [InlineData ("my/app/Outer$Inner")]
+ [InlineData ("SingleSegment")]
+ [InlineData ("com/example/_Private")]
+ [InlineData ("com/example/$Generated")]
+ public void ValidateJniName_ValidName_DoesNotThrow (string validJniName)
+ {
+ JniSignatureHelper.ValidateJniName (validJniName);
+ }
+
+ }
+
+ public class ExportWithThrowsClause
+ {
+
+ [Fact]
+ public void Generate_ExportWithThrows_HasThrowsClause ()
+ {
+ var java = GenerateFixture ("my/app/ExportWithThrows");
+ AssertContainsLine ("throws java.io.IOException, java.lang.IllegalStateException\n", java);
+ }
+
+ }
+
+ public class MethodReturnTypesAndParams
+ {
+
+ [Fact]
+ public void Generate_TouchHandler_HasExpectedMethodSignatures ()
+ {
+ var java = GenerateFixture ("my/app/TouchHandler");
+ AssertContainsLine ("public boolean onTouch (android.view.View p0, int p1)\n", java);
+ AssertContainsLine ("public void onScroll (int p0, float p1, long p2, double p3)\n", java);
+ AssertContainsLine ("public java.lang.String getText ()\n", java);
+ AssertContainsLine ("public void setItems (java.lang.String[] p0)\n", java);
+ }
+
+ }
+}
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs
index 088ccdab8d9..44d357f911b 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs
@@ -10,6 +10,7 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests;
public class RootTypeMapAssemblyGeneratorTests : FixtureTestBase
{
+
static MemoryStream GenerateRootAssembly (IReadOnlyList perAssemblyNames, string? assemblyName = null)
{
var stream = new MemoryStream ();
@@ -64,17 +65,25 @@ public void Generate_ReferencesGenericTypeMapAssemblyTargetAttribute ()
reader.GetString (t.Name).Contains ("TypeMapAssemblyTarget"));
}
- [Theory]
- [InlineData (0, 0)]
- [InlineData (3, 3)]
- public void Generate_AttributeCount_MatchesTargetCount (int targetCount, int expectedCount)
+ [Fact]
+ public void Generate_EmptyList_ProducesValidAssemblyWithNoTargetAttributes ()
+ {
+ using var stream = GenerateRootAssembly ([]);
+ using var pe = new PEReader (stream);
+ var reader = pe.GetMetadataReader ();
+ var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition);
+ Assert.Empty (asmAttrs);
+ }
+
+ [Fact]
+ public void Generate_MultipleTargets_HasCorrectAttributeCount ()
{
- var targets = Enumerable.Range (0, targetCount).Select (i => $"_Target{i}.TypeMap").ToArray ();
+ string[] targets = ["_App.TypeMap", "_Mono.Android.TypeMap", "_Java.Interop.TypeMap"];
using var stream = GenerateRootAssembly (targets);
using var pe = new PEReader (stream);
var reader = pe.GetMetadataReader ();
var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition);
- Assert.Equal (expectedCount, asmAttrs.Count ());
+ Assert.Equal (3, asmAttrs.Count ());
}
[Fact]
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs
index 596528b742f..5fb770fd3a5 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs
@@ -410,4 +410,98 @@ public void Generate_IdenticalContent_ProducesIdenticalMVIDs ()
Assert.Equal (mvid1, mvid2);
}
-}
\ No newline at end of file
+
+ [Fact]
+ public void Generate_AcwProxy_HasRegisterNativesAndUcoMethods ()
+ {
+ var peers = ScanFixtures ();
+ var acwPeer = peers.First (p => p.JavaName == "my/app/MainActivity");
+ Assert.False (acwPeer.DoNotGenerateAcw);
+ Assert.True (acwPeer.MarshalMethods.Count > 0, "ACW peer should have marshal methods");
+
+ using var stream = GenerateAssembly (new [] { acwPeer }, "AcwTest");
+ using var pe = new PEReader (stream);
+ var reader = pe.GetMetadataReader ();
+
+ var memberNames = GetMemberRefNames (reader);
+
+ // RegisterNatives is a method definition on the proxy type, not a member reference
+ var methodDefs = reader.MethodDefinitions
+ .Select (h => reader.GetMethodDefinition (h))
+ .Select (m => reader.GetString (m.Name))
+ .ToList ();
+ Assert.Contains ("RegisterNatives", methodDefs);
+ }
+
+ [Fact]
+ public void Generate_AcwProxy_HasUnmanagedCallersOnlyAttribute ()
+ {
+ var peers = ScanFixtures ();
+ var acwPeer = peers.First (p => p.JavaName == "my/app/MainActivity");
+
+ using var stream = GenerateAssembly (new [] { acwPeer }, "UcoTest");
+ using var pe = new PEReader (stream);
+ var reader = pe.GetMetadataReader ();
+
+ var typeNames = GetTypeRefNames (reader);
+ Assert.Contains ("UnmanagedCallersOnlyAttribute", typeNames);
+
+ // Verify UCO wrapper methods exist — they should have names like n__uco_
+ var methodDefs = reader.MethodDefinitions
+ .Select (h => reader.GetMethodDefinition (h))
+ .Select (m => reader.GetString (m.Name))
+ .ToList ();
+ Assert.Contains (methodDefs, name => name.Contains ("_uco_"));
+ }
+
+ [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", 1)] // JniParamKind.Boolean
+ [InlineData ("(Ljava/lang/String;)V", 9)] // JniParamKind.Object
+ public void ParseParameterTypes_SingleParam_MapsToCorrectKind (string signature, int expectedKind)
+ {
+ var types = JniSignatureHelper.ParseParameterTypes (signature);
+ Assert.Single (types);
+ Assert.Equal ((JniParamKind) expectedKind, types [0]);
+ }
+
+ [Theory]
+ [InlineData ("()V", 0)] // JniParamKind.Void
+ [InlineData ("()I", 5)] // JniParamKind.Int
+ [InlineData ("()Z", 1)] // JniParamKind.Boolean
+ [InlineData ("()Ljava/lang/String;", 9)] // JniParamKind.Object
+ public void ParseReturnType_MapsToCorrectKind (string signature, int expectedKind)
+ {
+ Assert.Equal ((JniParamKind) expectedKind, JniSignatureHelper.ParseReturnType (signature));
+ }
+
+ [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 ("("));
+ }
+}
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs
index 31a06dec2e9..7a91bc0a029 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs
@@ -457,8 +457,8 @@ public void Fixture_HelperType_IsUnconditional (string javaName, string kind)
var entry = model.Entries.FirstOrDefault ();
Assert.NotNull (entry);
// Implementor/EventDispatcher types are treated as unconditional ACW types.
- // Future optimization (see issue tracking Implementor trimming) may make them trimmable.
- Assert.True (entry!.IsUnconditional, $"{kind} should be unconditional");
+ // Future optimization (see #10911) may make them trimmable.
+ Assert.True (entry.IsUnconditional, $"{kind} should be unconditional");
}
}
@@ -736,4 +736,187 @@ static void EmitAndVerify (TypeMapAssemblyData model, string assemblyName, Actio
}
return result;
}
-}
\ No newline at end of file
+
+ public class UcoMethods
+ {
+ [Fact]
+ public void Build_AcwWithMarshalMethods_CreatesUcoMethods ()
+ {
+ var peer = MakeAcwPeer ("my/app/Foo", "MyApp.Foo", "App") with {
+ MarshalMethods = new List {
+ new MarshalMethodInfo {
+ JniName = "", NativeCallbackName = "n_ctor",
+ JniSignature = "()V", ManagedMethodName = ".ctor",
+ IsConstructor = true,
+ },
+ new MarshalMethodInfo {
+ JniName = "onClick", NativeCallbackName = "n_OnClick",
+ JniSignature = "(Landroid/view/View;)V", ManagedMethodName = "OnClick",
+ },
+ },
+ };
+ var model = BuildModel (new [] { peer });
+
+ Assert.Single (model.ProxyTypes);
+ var proxy = model.ProxyTypes [0];
+ Assert.True (proxy.IsAcw);
+ // Only non-constructor methods become UCO methods
+ Assert.Single (proxy.UcoMethods);
+ Assert.Equal ("n_OnClick", proxy.UcoMethods [0].CallbackMethodName);
+ }
+
+ [Fact]
+ public void Build_ConstructorsInMarshalMethods_SkippedFromUcoMethods ()
+ {
+ var peer = MakeAcwPeer ("my/app/Bar", "MyApp.Bar", "App") with {
+ MarshalMethods = new List {
+ new MarshalMethodInfo {
+ JniName = "", NativeCallbackName = "n_ctor",
+ JniSignature = "()V", ManagedMethodName = ".ctor",
+ IsConstructor = true,
+ },
+ },
+ };
+ var model = BuildModel (new [] { peer });
+ Assert.Empty (model.ProxyTypes [0].UcoMethods);
+ }
+
+ [Fact]
+ public void Build_McwType_IsAcwFalse ()
+ {
+ var peer = MakePeerWithActivation ("android/app/Activity", "Android.App.Activity", "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 = MakeInterfacePeer ("android/view/View$OnClickListener",
+ "Android.Views.View+IOnClickListener", "Mono.Android",
+ "Android.Views.View+IOnClickListenerInvoker") with {
+ MarshalMethods = new List {
+ new MarshalMethodInfo {
+ JniName = "onClick", NativeCallbackName = "n_OnClick",
+ JniSignature = "(Landroid/view/View;)V", ManagedMethodName = "OnClick",
+ },
+ },
+ };
+ var model = BuildModel (new [] { peer });
+ Assert.Single (model.ProxyTypes);
+ Assert.False (model.ProxyTypes [0].IsAcw);
+ }
+ }
+
+ public class UcoConstructors
+ {
+ [Fact]
+ public void Build_AcwWithConstructors_CreatesUcoConstructors ()
+ {
+ var peer = MakeAcwPeer ("my/app/Baz", "MyApp.Baz", "App") with {
+ JavaConstructors = new List {
+ new JavaConstructorInfo { ConstructorIndex = 0, JniSignature = "()V" },
+ new JavaConstructorInfo { ConstructorIndex = 1, JniSignature = "(Landroid/content/Context;)V" },
+ },
+ };
+ var model = BuildModel (new [] { peer });
+ Assert.Equal (2, model.ProxyTypes [0].UcoConstructors.Count);
+ Assert.Contains ("nctor_0_uco", model.ProxyTypes [0].UcoConstructors [0].WrapperName);
+ Assert.Contains ("nctor_1_uco", model.ProxyTypes [0].UcoConstructors [1].WrapperName);
+ }
+
+ [Fact]
+ public void Build_PeerWithoutActivationCtor_NoUcoConstructors ()
+ {
+ var peer = MakeMcwPeer ("test/NoActivation", "Test.NoActivation", "Asm");
+ var model = BuildModel (new [] { peer });
+ Assert.Empty (model.ProxyTypes);
+ }
+ }
+
+ public class NativeRegistrations
+ {
+ [Fact]
+ public void Build_NativeRegistrations_MatchUcoMethods ()
+ {
+ var peer = MakeAcwPeer ("my/app/Reg", "MyApp.Reg", "App") with {
+ MarshalMethods = new List {
+ new MarshalMethodInfo {
+ JniName = "", NativeCallbackName = "n_ctor",
+ JniSignature = "()V", ManagedMethodName = ".ctor",
+ IsConstructor = true,
+ },
+ new MarshalMethodInfo {
+ JniName = "doWork", NativeCallbackName = "n_DoWork",
+ JniSignature = "(I)V", ManagedMethodName = "DoWork",
+ },
+ },
+ JavaConstructors = new List {
+ new JavaConstructorInfo { ConstructorIndex = 0, JniSignature = "()V" },
+ },
+ };
+ var model = BuildModel (new [] { peer });
+ var proxy = model.ProxyTypes [0];
+
+ // Should have 1 UCO method + 1 UCO constructor = 2 native registrations
+ Assert.Single (proxy.UcoMethods);
+ Assert.Single (proxy.UcoConstructors);
+ Assert.Equal (2, proxy.NativeRegistrations.Count);
+ }
+
+ [Fact]
+ public void Build_NonAcwProxy_NoNativeRegistrations ()
+ {
+ var peer = MakePeerWithActivation ("test/Mcw", "Test.Mcw", "Mono.Android");
+ var model = BuildModel (new [] { peer });
+ Assert.Single (model.ProxyTypes);
+ Assert.Empty (model.ProxyTypes [0].NativeRegistrations);
+ }
+ }
+
+ public class FixtureUcoMethods
+ {
+ [Fact]
+ public void Fixture_MainActivity_UcoMethods ()
+ {
+ var peer = FindFixtureByJavaName ("my/app/MainActivity");
+ var model = BuildModel (new [] { peer }, "TypeMap");
+ var proxy = model.ProxyTypes.FirstOrDefault ();
+ Assert.NotNull (proxy);
+ Assert.True (proxy.IsAcw);
+ Assert.NotEmpty (proxy.UcoMethods);
+ }
+
+ [Fact]
+ public void Fixture_ClickableView_HasOnClickUcoWrapper ()
+ {
+ var peer = FindFixtureByJavaName ("my/app/ClickableView");
+ var model = BuildModel (new [] { peer }, "TypeMap");
+ var proxy = model.ProxyTypes.FirstOrDefault ();
+ Assert.NotNull (proxy);
+ var ucoNames = proxy.UcoMethods.Select (u => u.CallbackMethodName).ToList ();
+ Assert.Contains ("n_OnClick", ucoNames);
+ }
+
+ [Fact]
+ public void Fixture_TouchHandler_AllUcoMethods ()
+ {
+ var peer = FindFixtureByJavaName ("my/app/TouchHandler");
+ var model = BuildModel (new [] { peer }, "TypeMap");
+ var proxy = model.ProxyTypes.FirstOrDefault ();
+ Assert.NotNull (proxy);
+ Assert.True (proxy.UcoMethods.Count >= 2, "TouchHandler should have multiple UCO methods");
+ }
+
+ [Fact]
+ public void Fixture_CustomView_HasTwoConstructorWrappers ()
+ {
+ var peer = FindFixtureByJavaName ("my/app/CustomView");
+ var model = BuildModel (new [] { peer }, "TypeMap");
+ var proxy = model.ProxyTypes.FirstOrDefault ();
+ Assert.NotNull (proxy);
+ Assert.Equal (2, proxy.UcoConstructors.Count);
+ }
+ }
+}
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs
index 4a9ebb1d079..c7410a44074 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs
@@ -117,6 +117,7 @@ namespace Java.Interop
public sealed class ExportAttribute : Attribute
{
public string? Name { get; set; }
+ public string[]? ThrownNames { get; set; }
public ExportAttribute () { }
public ExportAttribute (string name) => Name = name;
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs
index d516be0de5b..f9ccd8637c3 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs
@@ -340,6 +340,15 @@ public class GenericCallbackImpl : Java.Lang.Object, IGenericCallback
protected GenericCallbackImpl (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { }
}
+ [Register ("my/app/ExportWithThrows")]
+ public class ExportWithThrows : Java.Lang.Object
+ {
+ protected ExportWithThrows (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { }
+
+ [Java.Interop.Export ("riskyMethod", ThrownNames = new [] { "java.io.IOException", "java.lang.IllegalStateException" })]
+ public void RiskyMethod () { }
+ }
+
[Register ("my/app/JiStylePeer", DoNotGenerateAcw = true)]
public class JiStylePeer : Java.Lang.Object
{