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 {