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); + } }); }