From 981726e7d96fb4a0e2ddddfed39e2663afc520ef Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 11 Mar 2026 18:46:46 +0100 Subject: [PATCH 01/21] [TrimmableTypeMap] Add UCO wrappers, JCW generator, and JNI signature helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the UCO (UnifiedCallableObject) wrapper generation, JCW (Java Callable Wrapper) Java source generator, and JNI signature parsing utilities on top of the TypeMap proxy groundwork from PR #10808. ### Generator additions - JcwJavaSourceGenerator: generates .java source files for JCW types - JniSignatureHelper: JNI signature parsing (parameter types, return types, CLR type encoding) - Extended ModelBuilder with UCO method/constructor handling - Extended TypeMapAssemblyEmitter with UCO wrapper IL emission ### Scanner enrichment - JavaPeerInfo: added BaseJavaName, ImplementedInterfaceJavaNames, JavaConstructors, JniParameterInfo for downstream generators - JavaPeerScanner: resolver methods for base types, interfaces, constructors ### Tests - 219 tests covering all generators, model builder scenarios, scanner behavior, and edge cases - Test fixtures extended with ExportWithThrows type Rebased on main after PR #10808 merge — adapted test code to use init-only record patterns (with {}) and added missing required members. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/JcwJavaSourceGenerator.cs | 287 ++++++++ .../Generator/JniSignatureHelper.cs | 231 ++++++ .../Generator/MetadataHelper.cs | 38 +- .../Generator/Model/TypeMapAssemblyData.cs | 144 ++++ .../Generator/ModelBuilder.cs | 137 +++- .../Generator/PEAssemblyBuilder.cs | 54 +- .../Generator/RootTypeMapAssemblyGenerator.cs | 33 +- .../Generator/TypeMapAssemblyEmitter.cs | 460 ++++++------ .../Generator/TypeMapAssemblyGenerator.cs | 14 - .../Scanner/JavaPeerInfo.cs | 83 ++- .../Scanner/JavaPeerScanner.cs | 47 +- .../Generator/FixtureTestBase.cs | 52 +- .../Generator/JcwJavaSourceGeneratorTests.cs | 336 +++++++++ .../RootTypeMapAssemblyGeneratorTests.cs | 56 +- .../TypeMapAssemblyGeneratorTests.cs | 687 +++++++++--------- .../Generator/TypeMapModelBuilderTests.cs | 657 +++++++++++++++-- .../TestFixtures/StubAttributes.cs | 1 + .../TestFixtures/TestTypes.cs | 9 + 18 files changed, 2522 insertions(+), 804 deletions(-) create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs 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 new file mode 100644 index 00000000000..0e9ac52f93e --- /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 → 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) + { + var result = new List (); + int i = 1; // skip opening '(' + while (i < jniSignature.Length && jniSignature [i] != ')') { + int start = 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) + { + int i = jniSignature.IndexOf (')') + 1; + return jniSignature.Substring (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': 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; + return JniParamKind.Object; + case '[': + i++; + ParseSingleType (sig, ref i); // skip element type + return JniParamKind.Object; + default: + throw new ArgumentException ($"Unknown JNI type character '{sig [i]}' in '{sig}' at index {i}"); + } + } + + /// + + /// Encodes the CLR type for a JNI parameter kind into a signature type encoder. + + /// + public static void EncodeClrType (SignatureTypeEncoder encoder, JniParamKind kind) + { + switch (kind) { + case JniParamKind.Boolean: encoder.Boolean (); break; + case JniParamKind.Byte: encoder.SByte (); break; + case JniParamKind.Char: encoder.Char (); break; + case JniParamKind.Short: encoder.Int16 (); break; + case JniParamKind.Int: encoder.Int32 (); break; + case JniParamKind.Long: encoder.Int64 (); break; + case JniParamKind.Float: encoder.Single (); break; + case JniParamKind.Double: encoder.Double (); break; + case JniParamKind.Object: encoder.IntPtr (); break; + default: throw new ArgumentException ($"Cannot encode JNI param kind {kind} as CLR type"); + } + } + + /// + /// 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/MetadataHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs index 9c0867c0875..2f62bb468f1 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs @@ -7,46 +7,14 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; static class MetadataHelper { /// - /// Produces a deterministic MVID by hashing the module name together with content-dependent data. - /// Assemblies with the same name but different content will have different MVIDs. + /// Produces a deterministic MVID from the module name so that identical inputs produce identical assemblies. /// - public static Guid DeterministicMvid (string moduleName, ReadOnlySpan contentBytes = default) + public static Guid DeterministicMvid (string moduleName) { using var sha = SHA256.Create (); - byte [] nameBytes = Encoding.UTF8.GetBytes (moduleName); - byte [] input = new byte [nameBytes.Length + contentBytes.Length]; - nameBytes.CopyTo (input, 0); - contentBytes.CopyTo (input.AsSpan (nameBytes.Length)); - byte [] hash = sha.ComputeHash (input); + byte [] hash = sha.ComputeHash (Encoding.UTF8.GetBytes (moduleName)); byte [] guidBytes = new byte [16]; Array.Copy (hash, guidBytes, 16); return new Guid (guidBytes); } - - /// - /// Computes a content fingerprint for the given . - /// - public static byte [] ComputeContentFingerprint (TypeMapAssemblyData data) - { - using var sha = SHA256.Create (); - using var stream = new System.IO.MemoryStream (); - using var writer = new System.IO.BinaryWriter (stream, Encoding.UTF8); - foreach (var entry in data.Entries) { - writer.Write (entry.JniName); - writer.Write (entry.ProxyTypeReference); - writer.Write (entry.TargetTypeReference ?? ""); - } - foreach (var proxy in data.ProxyTypes) { - writer.Write (proxy.TypeName); - writer.Write (proxy.TargetType.ManagedTypeName); - writer.Write (proxy.TargetType.AssemblyName); - writer.Write ((byte)(proxy.ActivationCtor?.Style ?? 0)); - } - foreach (var assoc in data.Associations) { - writer.Write (assoc.SourceTypeReference); - writer.Write (assoc.AliasProxyTypeReference); - } - writer.Flush (); - return sha.ComputeHash (stream.ToArray ()); - } } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs index 701f9cd4f2c..ecc97fd0d82 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -16,27 +16,37 @@ sealed class TypeMapAssemblyData public required string AssemblyName { get; init; } /// + /// Module file name (e.g., "_MyApp.TypeMap.dll"). + /// public required string ModuleName { get; init; } /// + /// TypeMap entries — one per unique JNI name. + /// public List Entries { get; } = new (); /// + /// Proxy types to emit in the assembly. + /// public List ProxyTypes { get; } = new (); /// + /// TypeMapAssociation entries for alias groups (multiple managed types → same JNI name). + /// public List Associations { get; } = new (); /// + /// Assembly names that need [IgnoresAccessChecksTo] for cross-assembly n_* calls. + /// public List IgnoresAccessChecksTo { get; } = new (); } @@ -69,7 +79,9 @@ sealed record TypeMapAttributeData public string? TargetTypeReference { get; init; } /// + /// True for 2-arg unconditional entries (ACW types, essential runtime types). + /// public bool IsUnconditional => TargetTypeReference == null; } @@ -85,22 +97,30 @@ sealed class JavaPeerProxyData public required string TypeName { get; init; } /// + /// Namespace for all proxy types. + /// public string Namespace { get; init; } = "_TypeMap.Proxies"; /// + /// Reference to the managed type this proxy wraps (for ldtoken in TargetType property). + /// public required TypeRefData TargetType { get; init; } /// + /// Reference to the invoker type (for interfaces/abstract types). Null if not applicable. + /// public TypeRefData? InvokerType { get; set; } /// + /// Whether this proxy has a CreateInstance that can actually create instances. + /// public bool HasActivation => ActivationCtor != null || InvokerType != null; @@ -110,9 +130,39 @@ sealed class JavaPeerProxyData public ActivationCtorData? ActivationCtor { get; set; } /// + /// 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 (); } /// @@ -126,11 +176,99 @@ sealed record TypeRefData public required string ManagedTypeName { get; init; } /// + /// Assembly containing the type, e.g., "Mono.Android". + /// 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. /// @@ -142,12 +280,16 @@ sealed record ActivationCtorData public required TypeRefData DeclaringType { get; init; } /// + /// True when the leaf type itself declares the activation ctor. + /// public required bool IsOnLeafType { get; init; } /// + /// The style of activation ctor (XamarinAndroid or JavaInterop). + /// public required ActivationCtorStyle Style { get; init; } } @@ -164,7 +306,9 @@ sealed record TypeMapAssociationData public required string SourceTypeReference { get; init; } /// + /// Assembly-qualified proxy type reference (the alias holder proxy). + /// public required string AliasProxyTypeReference { get; init; } } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index 949b034571a..19ba83374ce 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -50,7 +50,7 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri // Invoker types are NOT emitted as separate proxies or TypeMap entries — // they only appear as a TypeRef in the interface proxy's get_InvokerType property. var invokerTypeNames = new HashSet ( - peers.Select (p => p.InvokerTypeName).OfType (), + peers.Where (p => p.InvokerTypeName != null).Select (p => p.InvokerTypeName!), StringComparer.Ordinal); // Group non-invoker peers by JNI name to detect aliases (multiple .NET types → same Java class). @@ -67,8 +67,6 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri list.Add (peer); } - var usedProxyNames = new HashSet (StringComparer.Ordinal); - foreach (var kvp in groups) { string jniName = kvp.Key; var peersForName = kvp.Value; @@ -78,29 +76,27 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri peersForName.Sort ((a, b) => StringComparer.Ordinal.Compare (a.ManagedTypeName, b.ManagedTypeName)); } - EmitPeers (model, jniName, peersForName, assemblyName, usedProxyNames); + EmitPeers (model, jniName, peersForName, assemblyName); } // Compute IgnoresAccessChecksTo from cross-assembly references 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); } } - - // Always include Mono.Android — the emitter calls internal JNIEnv.DeleteRef - // for JI-style activation cleanup (matching legacy TypeManager.CreateProxy behavior). - referencedAssemblies.Add ("Mono.Android"); - model.IgnoresAccessChecksTo.AddRange (referencedAssemblies); return model; } static void EmitPeers (TypeMapAssemblyData model, string jniName, - List peersForName, string assemblyName, HashSet usedProxyNames) + List peersForName, string assemblyName) { // First peer is the "primary" — it gets the base JNI name entry. // Remaining peers get indexed alias entries: "jni/name[1]", "jni/name[2]", ... @@ -110,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, usedProxyNames); + proxy = BuildProxyType (peer, isAcw); model.ProxyTypes.Add (proxy); } @@ -126,8 +123,8 @@ static void EmitPeers (TypeMapAssemblyData model, string jniName, // Emit TypeMapAssociation linking alias types to the primary proxy if (i > 0 && primaryProxy != null) { model.Associations.Add (new TypeMapAssociationData { - SourceTypeReference = AssemblyQualify (peer.ManagedTypeName, peer.AssemblyName), - AliasProxyTypeReference = AssemblyQualify ($"{primaryProxy.Namespace}.{primaryProxy.TypeName}", assemblyName), + SourceTypeReference = $"{peer.ManagedTypeName}, {peer.AssemblyName}", + AliasProxyTypeReference = $"{primaryProxy.Namespace}.{primaryProxy.TypeName}, {assemblyName}", }); } } @@ -144,6 +141,12 @@ static bool IsUnconditionalEntry (JavaPeerInfo peer) return true; } + // Implementor/EventDispatcher types are only created from .NET (e.g., when a C# event + // is subscribed). They should NOT be unconditional — they're trimmable. + if (IsImplementorOrEventDispatcher (peer)) { + return false; + } + // User-defined ACW types (not MCW bindings, not interfaces) are unconditional // because Android can instantiate them from Java at any time. if (!peer.DoNotGenerateAcw && !peer.IsInterface) { @@ -158,6 +161,20 @@ static bool IsUnconditionalEntry (JavaPeerInfo peer) return false; } + /// + /// Implementor and EventDispatcher types are generated by the binding generator + /// and are only instantiated from .NET. They should be trimmable. + /// NOTE: This is a name-based heuristic. Ideally the scanner would provide a dedicated flag. + /// User types whose names happen to end in "Implementor" or "EventDispatcher" would be + /// misclassified as trimmable. This is acceptable for now since such naming in user code + /// is unlikely and would only affect trimming behavior, not correctness. + /// + static bool IsImplementorOrEventDispatcher (JavaPeerInfo peer) + { + return peer.ManagedTypeName.EndsWith ("Implementor", StringComparison.Ordinal) || + peer.ManagedTypeName.EndsWith ("EventDispatcher", StringComparison.Ordinal); + } + static void AddIfCrossAssembly (SortedSet set, string? asmName, string outputAssemblyName) { if (asmName != null && !string.Equals (asmName, outputAssemblyName, StringComparison.Ordinal)) { @@ -165,29 +182,19 @@ static void AddIfCrossAssembly (SortedSet set, string? asmName, string o } } - static JavaPeerProxyData BuildProxyType (JavaPeerInfo peer, HashSet usedProxyNames) + 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). var proxyTypeName = peer.ManagedTypeName.Replace ('.', '_').Replace ('+', '_') + "_Proxy"; - // Guard against name collisions (e.g., "My.Type" and "My_Type" both map to "My_Type_Proxy") - if (!usedProxyNames.Add (proxyTypeName)) { - int suffix = 2; - string candidate; - do { - candidate = $"{proxyTypeName}_{suffix}"; - suffix++; - } while (!usedProxyNames.Add (candidate)); - proxyTypeName = candidate; - } - var proxy = new JavaPeerProxyData { TypeName = proxyTypeName, TargetType = new TypeRefData { ManagedTypeName = peer.ManagedTypeName, AssemblyName = peer.AssemblyName, }, + IsAcw = isAcw, IsGenericDefinition = peer.IsGenericDefinition, }; @@ -210,23 +217,94 @@ 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) { string proxyRef; if (proxy != null) { - proxyRef = AssemblyQualify ($"{proxy.Namespace}.{proxy.TypeName}", outputAssemblyName); + proxyRef = $"{proxy.Namespace}.{proxy.TypeName}, {outputAssemblyName}"; } else { - proxyRef = AssemblyQualify (peer.ManagedTypeName, peer.AssemblyName); + proxyRef = $"{peer.ManagedTypeName}, {peer.AssemblyName}"; } bool isUnconditional = IsUnconditionalEntry (peer); string? targetRef = null; if (!isUnconditional) { - targetRef = AssemblyQualify (peer.ManagedTypeName, peer.AssemblyName); + targetRef = $"{peer.ManagedTypeName}, {peer.AssemblyName}"; } return new TypeMapAttributeData { @@ -235,7 +313,4 @@ static TypeMapAttributeData BuildEntry (JavaPeerInfo peer, JavaPeerProxyData? pr TargetTypeReference = targetRef, }; } - - static string AssemblyQualify (string typeName, string assemblyName) - => $"{typeName}, {assemblyName}"; } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs index b862cc2b29f..9cae7720e6a 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs @@ -34,7 +34,9 @@ sealed class PEAssemblyBuilder public BlobBuilder ILBuilder { get; } = new BlobBuilder (); public AssemblyReferenceHandle SystemRuntimeRef { get; private set; } + public AssemblyReferenceHandle SystemRuntimeInteropServicesRef { get; private set; } + public AssemblyReferenceHandle MonoAndroidRef { get; private set; } public PEAssemblyBuilder (Version systemRuntimeVersion) @@ -46,7 +48,7 @@ public PEAssemblyBuilder (Version systemRuntimeVersion) /// Emits the assembly definition, module definition, common assembly references, and <Module> type. /// Call this first. /// - public void EmitPreamble (string assemblyName, string moduleName, ReadOnlySpan contentFingerprint = default) + public void EmitPreamble (string assemblyName, string moduleName) { _asmRefCache.Clear (); _typeRefCache.Clear (); @@ -62,7 +64,7 @@ public void EmitPreamble (string assemblyName, string moduleName, ReadOnlySpan - /// Serialises the metadata + IL into a PE DLL and writes it to the given . - /// - public void WritePE (Stream stream) - { var peBuilder = new ManagedPEBuilder ( new PEHeaderBuilder (imageCharacteristics: Characteristics.Dll), new MetadataRootBuilder (Metadata), ILBuilder); var peBlob = new BlobBuilder (); peBuilder.Serialize (peBlob); - peBlob.WriteContentTo (stream); + using var fs = File.Create (outputPath); + peBlob.WriteContentTo (fs); } /// @@ -127,7 +121,12 @@ public AssemblyReferenceHandle AddAssemblyRef (string name, Version version, byt /// Finds an existing assembly reference or adds one with version 0.0.0.0. /// public AssemblyReferenceHandle FindOrAddAssemblyRef (string assemblyName) - => AddAssemblyRef (assemblyName, new Version (0, 0, 0, 0)); + { + if (_asmRefCache.TryGetValue (assemblyName, out var handle)) { + return handle; + } + return AddAssemblyRef (assemblyName, new Version (0, 0, 0, 0)); + } /// /// Adds a member reference using the reusable signature blob builder. @@ -172,32 +171,9 @@ TypeReferenceHandle MakeTypeRefForManagedName (EntityHandle scope, string manage /// public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, Action encodeSig, Action emitIL) - => EmitBody (name, attrs, encodeSig, emitIL, encodeLocals: null); - - /// - /// Emits a method body and definition with optional local variable declarations. - /// - /// - /// If non-null, writes the local variable signature blob. The callback receives a fresh - /// and must write the full LOCAL_SIG blob (header 0x07, - /// compressed count, then each variable type). - /// - public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, - Action encodeSig, Action emitIL, - Action? encodeLocals) { _sigBlob.Clear (); encodeSig (new BlobEncoder (_sigBlob)); - // Capture the sig blob handle before emitIL, because emitIL callbacks - // may call AddMemberRef which clears and repopulates _sigBlob. - var sigBlobHandle = Metadata.GetOrAddBlob (_sigBlob); - - StandaloneSignatureHandle localSigHandle = default; - if (encodeLocals != null) { - var localSigBlob = new BlobBuilder (32); - encodeLocals (localSigBlob); - localSigHandle = Metadata.AddStandaloneSignature (Metadata.GetOrAddBlob (localSigBlob)); - } _codeBlob.Clear (); var encoder = new InstructionEncoder (_codeBlob); @@ -207,14 +183,12 @@ public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, ILBuilder.WriteByte (0); } var bodyEncoder = new MethodBodyStreamEncoder (ILBuilder); - int bodyOffset = localSigHandle.IsNil - ? bodyEncoder.AddMethodBody (encoder) - : bodyEncoder.AddMethodBody (encoder, maxStack: 8, localSigHandle, MethodBodyAttributes.InitLocals); + int bodyOffset = bodyEncoder.AddMethodBody (encoder); return Metadata.AddMethodDefinition ( attrs, MethodImplAttributes.IL, Metadata.GetOrAddString (name), - sigBlobHandle, + Metadata.GetOrAddBlob (_sigBlob), bodyOffset, default); } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs index 14b49cfe986..ef538272ea5 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs @@ -32,45 +32,22 @@ public RootTypeMapAssemblyGenerator (Version systemRuntimeVersion) } /// - /// Generates the root typemap assembly and writes it to a file. + /// Generates the root typemap assembly. /// /// Names of per-assembly typemap assemblies to reference. /// Path to write the output .dll. /// Optional assembly name (defaults to _Microsoft.Android.TypeMaps). public void Generate (IReadOnlyList perAssemblyTypeMapNames, string outputPath, string? assemblyName = null) - { - if (outputPath is null) { - throw new ArgumentNullException (nameof (outputPath)); - } - - var dir = Path.GetDirectoryName (outputPath); - if (!string.IsNullOrEmpty (dir)) { - Directory.CreateDirectory (dir); - } - - var moduleName = Path.GetFileName (outputPath); - using var fs = File.Create (outputPath); - Generate (perAssemblyTypeMapNames, fs, assemblyName, moduleName); - } - - /// - /// Generates the root typemap assembly and writes it to the given stream. - /// - /// Names of per-assembly typemap assemblies to reference. - /// Stream to write the output PE to. - /// Optional assembly name (defaults to _Microsoft.Android.TypeMaps). - /// Optional module name for the PE metadata. - public void Generate (IReadOnlyList perAssemblyTypeMapNames, Stream stream, string? assemblyName = null, string? moduleName = null) { if (perAssemblyTypeMapNames is null) { throw new ArgumentNullException (nameof (perAssemblyTypeMapNames)); } - if (stream is null) { - throw new ArgumentNullException (nameof (stream)); + if (outputPath is null) { + throw new ArgumentNullException (nameof (outputPath)); } assemblyName ??= DefaultAssemblyName; - moduleName ??= assemblyName + ".dll"; + var moduleName = Path.GetFileName (outputPath); var pe = new PEAssemblyBuilder (_systemRuntimeVersion); pe.EmitPreamble (assemblyName, moduleName); @@ -99,6 +76,6 @@ public void Generate (IReadOnlyList perAssemblyTypeMapNames, Stream stre pe.Metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, ctorRef, blobHandle); } - pe.WritePE (stream); + pe.WritePE (outputPath); } } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index f878997f04a..ce34dfbb4fa 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.IO; using System.Reflection; using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; @@ -19,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() { } /// @@ -35,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")] /// /// @@ -45,18 +60,18 @@ sealed class TypeMapAssemblyEmitter { readonly Version _systemRuntimeVersion; - readonly PEAssemblyBuilder _pe; + PEAssemblyBuilder _pe = null!; AssemblyReferenceHandle _javaInteropRef; TypeReferenceHandle _javaPeerProxyRef; TypeReferenceHandle _iJavaPeerableRef; TypeReferenceHandle _jniHandleOwnershipRef; - TypeReferenceHandle _jniObjectReferenceRef; - TypeReferenceHandle _jniObjectReferenceOptionsRef; - TypeReferenceHandle _jniEnvRef; + TypeReferenceHandle _iAndroidCallableWrapperRef; TypeReferenceHandle _systemTypeRef; TypeReferenceHandle _runtimeTypeHandleRef; + TypeReferenceHandle _jniTypeRef; + TypeReferenceHandle _trimmableNativeRegistrationRef; TypeReferenceHandle _notSupportedExceptionRef; TypeReferenceHandle _runtimeHelpersRef; @@ -64,8 +79,10 @@ sealed class TypeMapAssemblyEmitter MemberReferenceHandle _getTypeFromHandleRef; MemberReferenceHandle _getUninitializedObjectRef; MemberReferenceHandle _notSupportedExceptionCtorRef; - MemberReferenceHandle _jniObjectReferenceCtorRef; - MemberReferenceHandle _jniEnvDeleteRefRef; + MemberReferenceHandle _activateInstanceRef; + MemberReferenceHandle _registerMethodRef; + MemberReferenceHandle _ucoAttrCtorRef; + BlobHandle _ucoAttrBlobHandle; MemberReferenceHandle _typeMapAttrCtorRef2Arg; MemberReferenceHandle _typeMapAttrCtorRef3Arg; MemberReferenceHandle _typeMapAssociationAttrCtorRef; @@ -80,7 +97,6 @@ sealed class TypeMapAssemblyEmitter public TypeMapAssemblyEmitter (Version systemRuntimeVersion) { _systemRuntimeVersion = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion)); - _pe = new PEAssemblyBuilder (_systemRuntimeVersion); } /// @@ -95,37 +111,19 @@ public void Emit (TypeMapAssemblyData model, string outputPath) throw new ArgumentNullException (nameof (outputPath)); } - EmitCore (model); - _pe.WritePE (outputPath); - } - - /// - /// Emits a PE assembly from the given model and writes it to . - /// - public void Emit (TypeMapAssemblyData model, Stream stream) - { - if (model is null) { - throw new ArgumentNullException (nameof (model)); - } - if (stream is null) { - throw new ArgumentNullException (nameof (stream)); - } - - EmitCore (model); - _pe.WritePE (stream); - } - - void EmitCore (TypeMapAssemblyData model) - { - _pe.EmitPreamble (model.AssemblyName, model.ModuleName, MetadataHelper.ComputeContentFingerprint (model)); + _pe = new PEAssemblyBuilder (_systemRuntimeVersion); + _pe.EmitPreamble (model.AssemblyName, model.ModuleName); _javaInteropRef = _pe.AddAssemblyRef ("Java.Interop", new Version (0, 0, 0, 0)); 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) { @@ -137,6 +135,7 @@ void EmitCore (TypeMapAssemblyData model) } _pe.EmitIgnoresAccessChecksToAttribute (model.IgnoresAccessChecksTo); + _pe.WritePE (outputPath); } void EmitTypeReferences () @@ -148,16 +147,16 @@ void EmitTypeReferences () metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("IJavaPeerable")); _jniHandleOwnershipRef = metadata.AddTypeReference (_pe.MonoAndroidRef, metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("JniHandleOwnership")); - _jniEnvRef = metadata.AddTypeReference (_pe.MonoAndroidRef, - metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("JNIEnv")); - _jniObjectReferenceRef = metadata.AddTypeReference (_javaInteropRef, - 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, @@ -184,22 +183,33 @@ void EmitMemberReferences () rt => rt.Void (), p => p.AddParameter ().Type ().String ())); - _jniObjectReferenceCtorRef = _pe.AddMemberRef (_jniObjectReferenceRef, ".ctor", - sig => sig.MethodSignature (isInstanceMethod: true).Parameters (1, + _activateInstanceRef = _pe.AddMemberRef (_trimmableNativeRegistrationRef, "ActivateInstance", + sig => sig.MethodSignature ().Parameters (2, rt => rt.Void (), - p => p.AddParameter ().Type ().IntPtr ())); + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().Type (_systemTypeRef, false); + })); - // JNIEnv.DeleteRef(IntPtr, JniHandleOwnership) — static, internal - // Used by JI-style activation to clean up the original handle after constructing the peer. - // Matches the legacy TypeManager.CreateProxy behavior. - _jniEnvDeleteRefRef = _pe.AddMemberRef (_jniEnvRef, "DeleteRef", - sig => sig.MethodSignature ().Parameters (2, + _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 (); - p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); })); + 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 +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), @@ -264,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, @@ -286,39 +301,55 @@ 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) { if (!proxy.HasActivation) { - EmitCreateInstanceNoActivation (); + EmitCreateInstanceBody (encoder => { + encoder.OpCode (ILOpCode.Ldnull); + encoder.OpCode (ILOpCode.Ret); + }); return; } + // Generic type definitions cannot be instantiated if (proxy.IsGenericDefinition) { - EmitCreateInstanceGenericDefinition (); - return; - } - - // JavaInterop-style activation ctors (ref JniObjectReference, JniObjectReferenceOptions) - // require parameter conversion from (IntPtr, JniHandleOwnership). - if (proxy.ActivationCtor?.Style == ActivationCtorStyle.JavaInterop) { - if (proxy.InvokerType != null) { - EmitCreateInstanceViaJavaInteropNewobj (_pe.ResolveTypeRef (proxy.InvokerType)); - } else { - var targetRef = _pe.ResolveTypeRef (proxy.TargetType); - var jiCtor = proxy.ActivationCtor ?? throw new InvalidOperationException ("ActivationCtor should not be null"); - if (jiCtor.IsOnLeafType) { - EmitCreateInstanceViaJavaInteropNewobj (targetRef); - } else { - EmitCreateInstanceInheritedJavaInteropCtor (targetRef, jiCtor); - } - } + EmitCreateInstanceBody (encoder => { + encoder.LoadString (_pe.Metadata.GetOrAddUserString ("Cannot create instance of open generic type.")); + encoder.OpCode (ILOpCode.Newobj); + encoder.Token (_notSupportedExceptionCtorRef); + encoder.OpCode (ILOpCode.Throw); + }); return; } + // Interface with invoker: new TInvoker(IntPtr, JniHandleOwnership) if (proxy.InvokerType != null) { - EmitCreateInstanceViaNewobj (_pe.ResolveTypeRef (proxy.InvokerType)); + var invokerCtorRef = AddActivationCtorRef (_pe.ResolveTypeRef (proxy.InvokerType)); + EmitCreateInstanceBody (encoder => { + encoder.OpCode (ILOpCode.Ldarg_1); + encoder.OpCode (ILOpCode.Ldarg_2); + encoder.OpCode (ILOpCode.Newobj); + encoder.Token (invokerCtorRef); + encoder.OpCode (ILOpCode.Ret); + }); return; } @@ -327,112 +358,19 @@ void EmitCreateInstance (JavaPeerProxyData proxy) var targetTypeRef = _pe.ResolveTypeRef (proxy.TargetType); if (activationCtor.IsOnLeafType) { - EmitCreateInstanceViaNewobj (targetTypeRef); - } else { - EmitCreateInstanceInheritedCtor (targetTypeRef, activationCtor); - } - } - - void EmitCreateInstanceNoActivation () - { - EmitCreateInstanceBody (encoder => { - encoder.OpCode (ILOpCode.Ldnull); - encoder.OpCode (ILOpCode.Ret); - }); - } - - void EmitCreateInstanceGenericDefinition () - { - EmitCreateInstanceBody (encoder => { - encoder.LoadString (_pe.Metadata.GetOrAddUserString ("Cannot create instance of open generic type.")); - encoder.OpCode (ILOpCode.Newobj); - encoder.Token (_notSupportedExceptionCtorRef); - encoder.OpCode (ILOpCode.Throw); - }); - } - - void EmitCreateInstanceViaNewobj (EntityHandle typeRef) - { - var ctorRef = AddActivationCtorRef (typeRef); - EmitCreateInstanceBody (encoder => { - encoder.OpCode (ILOpCode.Ldarg_1); - encoder.OpCode (ILOpCode.Ldarg_2); - encoder.OpCode (ILOpCode.Newobj); - encoder.Token (ctorRef); - encoder.OpCode (ILOpCode.Ret); - }); - } - - void EmitCreateInstanceInheritedCtor (EntityHandle targetTypeRef, ActivationCtorData activationCtor) - { - var baseActivationCtorRef = AddActivationCtorRef (_pe.ResolveTypeRef (activationCtor.DeclaringType)); - EmitCreateInstanceBody (encoder => { - encoder.OpCode (ILOpCode.Ldtoken); - encoder.Token (targetTypeRef); - encoder.Call (_getTypeFromHandleRef); - encoder.Call (_getUninitializedObjectRef); - encoder.OpCode (ILOpCode.Castclass); - encoder.Token (targetTypeRef); - - encoder.OpCode (ILOpCode.Dup); - encoder.OpCode (ILOpCode.Ldarg_1); - encoder.OpCode (ILOpCode.Ldarg_2); - encoder.Call (baseActivationCtorRef); - - encoder.OpCode (ILOpCode.Ret); - }); - } - - /// - /// Emits CreateInstance for JavaInterop-style activation (leaf type): - /// var jniRef = new JniObjectReference(handle); - /// var result = new TargetType(ref jniRef, JniObjectReferenceOptions.Copy); - /// JNIEnv.DeleteRef(handle, ownership); - /// return result; - /// - void EmitCreateInstanceViaJavaInteropNewobj (EntityHandle typeRef) - { - var ctorRef = AddJavaInteropActivationCtorRef (typeRef); - EmitCreateInstanceBodyWithLocals ( - EncodeJniObjectReferenceAndObjectLocals, - encoder => { - // var jniRef = new JniObjectReference(handle); - encoder.LoadLocalAddress (0); - encoder.OpCode (ILOpCode.Ldarg_1); // handle - encoder.Call (_jniObjectReferenceCtorRef); - - // var result = new TargetType(ref jniRef, JniObjectReferenceOptions.Copy); - encoder.LoadLocalAddress (0); - encoder.LoadConstantI4 (1); // JniObjectReferenceOptions.Copy + // Leaf type has its own ctor: new T(IntPtr, JniHandleOwnership) + var ctorRef = AddActivationCtorRef (targetTypeRef); + EmitCreateInstanceBody (encoder => { + encoder.OpCode (ILOpCode.Ldarg_1); + encoder.OpCode (ILOpCode.Ldarg_2); encoder.OpCode (ILOpCode.Newobj); encoder.Token (ctorRef); - encoder.StoreLocal (1); // save result - - // JNIEnv.DeleteRef(handle, ownership); - encoder.OpCode (ILOpCode.Ldarg_1); // handle - encoder.OpCode (ILOpCode.Ldarg_2); // ownership - encoder.Call (_jniEnvDeleteRefRef); - - encoder.LoadLocal (1); // load result encoder.OpCode (ILOpCode.Ret); }); - } - - /// - /// Emits CreateInstance for JavaInterop-style activation (inherited ctor): - /// var obj = (TargetType)RuntimeHelpers.GetUninitializedObject(typeof(TargetType)); - /// var jniRef = new JniObjectReference(handle); - /// obj.BaseCtor(ref jniRef, JniObjectReferenceOptions.Copy); - /// JNIEnv.DeleteRef(handle, ownership); - /// return obj; - /// - void EmitCreateInstanceInheritedJavaInteropCtor (EntityHandle targetTypeRef, ActivationCtorData activationCtor) - { - var baseCtorRef = AddJavaInteropActivationCtorRef (_pe.ResolveTypeRef (activationCtor.DeclaringType)); - EmitCreateInstanceBodyWithLocals ( - EncodeJniObjectReferenceLocal, - encoder => { - // var obj = (TargetType)RuntimeHelpers.GetUninitializedObject(typeof(TargetType)); + } else { + // Inherited ctor: GetUninitializedObject(typeof(T)) + call Base::.ctor(IntPtr, JniHandleOwnership) + var baseActivationCtorRef = AddActivationCtorRef (_pe.ResolveTypeRef (activationCtor.DeclaringType)); + EmitCreateInstanceBody (encoder => { encoder.OpCode (ILOpCode.Ldtoken); encoder.Token (targetTypeRef); encoder.Call (_getTypeFromHandleRef); @@ -440,60 +378,14 @@ void EmitCreateInstanceInheritedJavaInteropCtor (EntityHandle targetTypeRef, Act encoder.OpCode (ILOpCode.Castclass); encoder.Token (targetTypeRef); - // dup obj (one copy for the call, one for the return) encoder.OpCode (ILOpCode.Dup); - - // var jniRef = new JniObjectReference(handle); - encoder.LoadLocalAddress (0); - encoder.OpCode (ILOpCode.Ldarg_1); // handle - encoder.Call (_jniObjectReferenceCtorRef); - - // obj.BaseCtor(ref jniRef, JniObjectReferenceOptions.Copy); - encoder.LoadLocalAddress (0); - encoder.LoadConstantI4 (1); // JniObjectReferenceOptions.Copy - encoder.Call (baseCtorRef); - - // JNIEnv.DeleteRef(handle, ownership); - encoder.OpCode (ILOpCode.Ldarg_1); // handle - encoder.OpCode (ILOpCode.Ldarg_2); // ownership - encoder.Call (_jniEnvDeleteRefRef); + encoder.OpCode (ILOpCode.Ldarg_1); + encoder.OpCode (ILOpCode.Ldarg_2); + encoder.Call (baseActivationCtorRef); encoder.OpCode (ILOpCode.Ret); }); - } - - void EncodeJniObjectReferenceLocal (BlobBuilder blob) - { - // LOCAL_SIG header (0x07), count = 1, ELEMENT_TYPE_VALUETYPE + compressed token - blob.WriteByte (0x07); // LOCAL_SIG - blob.WriteCompressedInteger (1); // 1 local variable - blob.WriteByte (0x11); // ELEMENT_TYPE_VALUETYPE - blob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (_jniObjectReferenceRef)); - } - - void EncodeJniObjectReferenceAndObjectLocals (BlobBuilder blob) - { - // LOCAL_SIG header (0x07), count = 2: - // local 0: JniObjectReference (valuetype) - // local 1: object (for storing the newobj result across the DeleteRef call) - blob.WriteByte (0x07); // LOCAL_SIG - blob.WriteCompressedInteger (2); // 2 local variables - blob.WriteByte (0x11); // ELEMENT_TYPE_VALUETYPE - blob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (_jniObjectReferenceRef)); - blob.WriteByte (0x1c); // ELEMENT_TYPE_OBJECT - } - - MemberReferenceHandle AddJavaInteropActivationCtorRef (EntityHandle declaringTypeRef) - { - return _pe.AddMemberRef (declaringTypeRef, ".ctor", - sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, - rt => rt.Void (), - p => { - // ref JniObjectReference — encoded as byref valuetype - p.AddParameter ().Type (isByRef: true).Type (_jniObjectReferenceRef, true); - // JniObjectReferenceOptions — encoded as valuetype (enum) - p.AddParameter ().Type ().Type (_jniObjectReferenceOptionsRef, true); - })); + } } void EmitCreateInstanceBody (Action emitIL) @@ -509,20 +401,6 @@ void EmitCreateInstanceBody (Action emitIL) emitIL); } - void EmitCreateInstanceBodyWithLocals (Action encodeLocals, Action emitIL) - { - _pe.EmitBody ("CreateInstance", - MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig, - sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, - rt => rt.Type ().Type (_iJavaPeerableRef, false), - p => { - p.AddParameter ().Type ().IntPtr (); - p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); - }), - emitIL, - encodeLocals); - } - MemberReferenceHandle AddActivationCtorRef (EntityHandle declaringTypeRef) { return _pe.AddMemberRef (declaringTypeRef, ".ctor", @@ -550,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; @@ -557,10 +535,7 @@ void EmitTypeMapAttribute (TypeMapAttributeData entry) b.WriteSerializedString (entry.JniName); b.WriteSerializedString (entry.ProxyTypeReference); if (!entry.IsUnconditional) { - if (entry.TargetTypeReference is null) { - throw new InvalidOperationException ($"TargetTypeReference must not be null for conditional entry '{entry.JniName}'"); - } - b.WriteSerializedString (entry.TargetTypeReference); + b.WriteSerializedString (entry.TargetTypeReference!); } }); _pe.Metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, ctorRef, blob); @@ -574,4 +549,5 @@ void EmitTypeMapAssociationAttribute (TypeMapAssociationData assoc) }); _pe.Metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, _typeMapAssociationAttrCtorRef, blob); } + } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs index f6586218d6a..927346fbf10 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.IO; namespace Microsoft.Android.Sdk.TrimmableTypeMap; @@ -30,17 +29,4 @@ public void Generate (IReadOnlyList peers, string outputPath, stri var emitter = new TypeMapAssemblyEmitter (_systemRuntimeVersion); emitter.Emit (model, outputPath); } - - /// - /// Generates a TypeMap PE assembly from the given Java peer info records and writes it to . - /// - /// Scanned Java peer types. - /// Stream to write the output PE assembly to. - /// Assembly name for the generated assembly. - public void Generate (IReadOnlyList peers, Stream stream, string assemblyName) - { - var model = ModelBuilder.Build (peers, assemblyName + ".dll", assemblyName); - var emitter = new TypeMapAssemblyEmitter (_systemRuntimeVersion); - emitter.Emit (model, stream); - } } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index c34d7f2009c..e8d0c5d6ba3 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; } = 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. @@ -129,6 +135,35 @@ 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; } + + /// + /// JNI parameter types for UCO generation. + /// + public IReadOnlyList Parameters { get; init; } = Array.Empty (); + + /// + /// JNI return type descriptor, e.g., "V", "Landroid/os/Bundle;". + /// + public required string JniReturnType { get; init; } + /// /// True if this is a constructor registration. /// @@ -147,6 +182,50 @@ 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; } + + /// + /// 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 fc3627224f5..eabc73f7ee9 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, @@ -281,6 +282,9 @@ static void AddMarshalMethod (List methods, RegisterInfo regi JniSignature = registerInfo.Signature ?? "()V", Connector = registerInfo.Connector, ManagedMethodName = index.Reader.GetString (methodDef.Name), + NativeCallbackName = $"n_{index.Reader.GetString (methodDef.Name)}", + JniReturnType = JniSignatureHelper.ParseReturnTypeString (registerInfo.Signature ?? "()V"), + Parameters = ParseJniParameters (registerInfo.Signature ?? "()V"), IsConstructor = registerInfo.JniName == "" || registerInfo.JniName == ".ctor", ThrownNames = exportInfo?.ThrownNames, SuperArgumentsString = exportInfo?.SuperArgumentsString, @@ -737,19 +741,44 @@ static string GetCrc64PackageName (string ns, string assemblyName) static string ExtractNamespace (string fullName) { - // Strip nested type suffix (e.g., "My.Namespace.Outer+Inner" → "My.Namespace.Outer") - int plusIndex = fullName.IndexOf ('+'); - var nameForNamespace = plusIndex >= 0 ? fullName.Substring (0, plusIndex) : fullName; - int lastDot = nameForNamespace.LastIndexOf ('.'); - return lastDot >= 0 ? nameForNamespace.Substring (0, lastDot) : ""; + int lastDot = fullName.LastIndexOf ('.'); + return lastDot >= 0 ? fullName.Substring (0, lastDot) : ""; } static string ExtractShortName (string fullName) { - var span = fullName.AsSpan (); - int lastDot = span.LastIndexOf ('.'); - var typePart = lastDot >= 0 ? span.Slice (lastDot + 1) : span; + int lastDot = fullName.LastIndexOf ('.'); + string typePart = lastDot >= 0 ? fullName.Substring (lastDot + 1) : fullName; int lastPlus = typePart.LastIndexOf ('+'); - return (lastPlus >= 0 ? typePart.Slice (lastPlus + 1) : typePart).ToString (); + return lastPlus >= 0 ? typePart.Substring (lastPlus + 1) : typePart; + } + + static List ParseJniParameters (string jniSignature) + { + var typeStrings = JniSignatureHelper.ParseParameterTypeStrings (jniSignature); + var result = new List (typeStrings.Count); + foreach (var t in typeStrings) { + result.Add (new JniParameterInfo { JniType = t }); + } + 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 70471f62e13..21956fd019c 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs @@ -77,13 +77,30 @@ 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", + JniReturnType = "V", + IsConstructor = true, + }, + }, + }; + } private protected static JavaPeerInfo MakeInterfacePeer ( - string jniName, - string managedName, - string asmName, - string invokerName) + string jniName = "android/view/View$OnClickListener", + string managedName = "Android.Views.View+IOnClickListener", + string asmName = "Mono.Android", + string invokerName = "Android.Views.View+IOnClickListenerInvoker") { var (ns, shortName) = ParseManagedTypeName (managedName); return new JavaPeerInfo { @@ -98,6 +115,18 @@ private protected static JavaPeerInfo MakeInterfacePeer ( }; } + private protected static MarshalMethodInfo MakeMarshalMethod (string jniName, string callbackName, string jniSig, bool isConstructor = false) + { + return new MarshalMethodInfo { + JniName = jniName, + NativeCallbackName = callbackName, + JniSignature = jniSig, + ManagedMethodName = isConstructor ? ".ctor" : callbackName.StartsWith ("n_") ? callbackName.Substring (2) : callbackName, + JniReturnType = jniSig.Contains (')') ? jniSig.Substring (jniSig.IndexOf (')') + 1) : "V", + IsConstructor = isConstructor, + }; + } + private protected static List GetTypeRefNames (MetadataReader reader) => reader.TypeReferences .Select (h => reader.GetTypeReference (h)) @@ -109,4 +138,17 @@ private protected static List GetMemberRefNames (MetadataReader reader) .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i))) .Select (m => reader.GetString (m.Name)) .ToList (); + + private protected static string CreateTempDir () + { + var dir = Path.Combine (Path.GetTempPath (), $"typemap-test-{Guid.NewGuid ():N}"); + Directory.CreateDirectory (dir); + return dir; + } + + private protected static void DeleteTempDir (string dir) + { + if (Directory.Exists (dir)) + try { Directory.Delete (dir, true); } catch { } + } } 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..f6b6578254c --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs @@ -0,0 +1,336 @@ +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", + 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, + 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", + 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, + 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", + 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, + 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/RootTypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs index 088ccdab8d9..b6dfe06c6b2 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs @@ -8,22 +8,26 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; -public class RootTypeMapAssemblyGeneratorTests : FixtureTestBase +public class RootTypeMapAssemblyGeneratorTests : FixtureTestBase, IDisposable { - static MemoryStream GenerateRootAssembly (IReadOnlyList perAssemblyNames, string? assemblyName = null) + readonly string _outputDir = CreateTempDir (); + public void Dispose () => DeleteTempDir (_outputDir); + + string GenerateRootAssembly (IReadOnlyList perAssemblyNames, string? assemblyName = null) { - var stream = new MemoryStream (); + var outputPath = Path.Combine (_outputDir, + (assemblyName ?? "_Microsoft.Android.TypeMaps") + ".dll"); var generator = new RootTypeMapAssemblyGenerator (new Version (11, 0, 0, 0)); - generator.Generate (perAssemblyNames, stream, assemblyName); - stream.Position = 0; - return stream; + generator.Generate (perAssemblyNames, outputPath, assemblyName); + return outputPath; } [Fact] public void Generate_ProducesValidPEAssembly () { - using var stream = GenerateRootAssembly (new [] { "_App.TypeMap", "_Mono.Android.TypeMap" }); - using var pe = new PEReader (stream); + var path = GenerateRootAssembly (new [] { "_App.TypeMap", "_Mono.Android.TypeMap" }); + Assert.True (File.Exists (path)); + using var pe = new PEReader (File.OpenRead (path)); Assert.True (pe.HasMetadata); } @@ -32,8 +36,8 @@ public void Generate_ProducesValidPEAssembly () [InlineData ("MyRoot", "MyRoot")] public void Generate_AssemblyName_MatchesExpected (string? assemblyName, string expectedName) { - using var stream = GenerateRootAssembly ([], assemblyName); - using var pe = new PEReader (stream); + var path = GenerateRootAssembly (Array.Empty (), assemblyName); + using var pe = new PEReader (File.OpenRead (path)); var reader = pe.GetMetadataReader (); var asmDef = reader.GetAssemblyDefinition (); Assert.Equal (expectedName, reader.GetString (asmDef.Name)); @@ -42,8 +46,8 @@ public void Generate_AssemblyName_MatchesExpected (string? assemblyName, string [Fact] public void Generate_ReferencesGenericTypeMapAssemblyTargetAttribute () { - using var stream = GenerateRootAssembly (new [] { "_App.TypeMap" }); - using var pe = new PEReader (stream); + var path = GenerateRootAssembly (new [] { "_App.TypeMap" }); + using var pe = new PEReader (File.OpenRead (path)); var reader = pe.GetMetadataReader (); var typeRefs = reader.TypeReferences @@ -64,25 +68,33 @@ 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 () + { + var path = GenerateRootAssembly (Array.Empty ()); + using var pe = new PEReader (File.OpenRead (path)); + 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 (); - using var stream = GenerateRootAssembly (targets); - using var pe = new PEReader (stream); + var targets = new [] { "_App.TypeMap", "_Mono.Android.TypeMap", "_Java.Interop.TypeMap" }; + var path = GenerateRootAssembly (targets); + using var pe = new PEReader (File.OpenRead (path)); var reader = pe.GetMetadataReader (); var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); - Assert.Equal (expectedCount, asmAttrs.Count ()); + Assert.Equal (3, asmAttrs.Count ()); } [Fact] public void Generate_AttributeBlobValues_MatchTargetNames () { var targets = new [] { "_App.TypeMap", "_Mono.Android.TypeMap" }; - using var stream = GenerateRootAssembly (targets); - using var pe = new PEReader (stream); + var path = GenerateRootAssembly (targets); + using var pe = new PEReader (File.OpenRead (path)); var reader = pe.GetMetadataReader (); var attrValues = new List (); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 596528b742f..a66a17c7ab2 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -12,98 +12,196 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; public class TypeMapAssemblyGeneratorTests : FixtureTestBase { - static MemoryStream GenerateAssembly (IReadOnlyList peers, string assemblyName = "TestTypeMap") + static string GenerateAssembly (IReadOnlyList peers, string outputDir, string? assemblyName = null) { - var stream = new MemoryStream (); + var outputPath = Path.Combine (outputDir, (assemblyName ?? "TestTypeMap") + ".dll"); var generator = new TypeMapAssemblyGenerator (new Version (11, 0, 0, 0)); - generator.Generate (peers, stream, assemblyName); - stream.Position = 0; - return stream; + generator.Generate (peers, outputPath, assemblyName); + return outputPath; } - [Fact] - public void Generate_ProducesValidPEAssembly () + static (PEReader pe, MetadataReader reader) OpenAssembly (string path) { - var peers = ScanFixtures (); - using var stream = GenerateAssembly (peers); - using var pe = new PEReader (stream); - Assert.True (pe.HasMetadata); - var reader = pe.GetMetadataReader (); - Assert.NotNull (reader); + var pe = new PEReader (File.OpenRead (path)); + return (pe, pe.GetMetadataReader ()); } - [Fact] - public void Generate_HasRequiredAssemblyReferences () + public class BasicAssemblyStructure : IDisposable { - var peers = ScanFixtures (); - using var stream = GenerateAssembly (peers); - using var pe = new PEReader (stream); - var reader = pe.GetMetadataReader (); - var asmRefs = reader.AssemblyReferences - .Select (h => reader.GetString (reader.GetAssemblyReference (h).Name)) - .ToList (); - Assert.Contains ("System.Runtime", asmRefs); - Assert.Contains ("Mono.Android", asmRefs); - Assert.Contains ("Java.Interop", asmRefs); - Assert.Contains ("System.Runtime.InteropServices", asmRefs); + readonly string _outputDir = CreateTempDir (); + public void Dispose () => DeleteTempDir (_outputDir); + + [Fact] + public void Generate_ProducesValidPEAssembly () + { + var peers = ScanFixtures (); + var path = GenerateAssembly (peers, _outputDir); + Assert.True (File.Exists (path)); + using var pe = new PEReader (File.OpenRead (path)); + Assert.True (pe.HasMetadata); + var reader = pe.GetMetadataReader (); + Assert.NotNull (reader); + } + } - [Fact] - public void Generate_CreatesProxyTypes () + public class AssemblyReference : IDisposable { - var peers = ScanFixtures (); - using var stream = GenerateAssembly (peers); - using var pe = new PEReader (stream); - var reader = pe.GetMetadataReader (); - var proxyTypes = reader.TypeDefinitions - .Select (h => reader.GetTypeDefinition (h)) - .Where (t => reader.GetString (t.Namespace) == "_TypeMap.Proxies") - .ToList (); - - Assert.NotEmpty (proxyTypes); - Assert.Contains (proxyTypes, t => reader.GetString (t.Name) == "Java_Lang_Object_Proxy"); + readonly string _outputDir = CreateTempDir (); + public void Dispose () => DeleteTempDir (_outputDir); + + [Fact] + public void Generate_HasRequiredAssemblyReferences () + { + var peers = ScanFixtures (); + var path = GenerateAssembly (peers, _outputDir); + var (pe, reader) = OpenAssembly (path); + using (pe) { + var asmRefs = reader.AssemblyReferences + .Select (h => reader.GetString (reader.GetAssemblyReference (h).Name)) + .ToList (); + Assert.Contains ("System.Runtime", asmRefs); + Assert.Contains ("Mono.Android", asmRefs); + Assert.Contains ("Java.Interop", asmRefs); + Assert.Contains ("System.Runtime.InteropServices", asmRefs); + } + } + } - [Fact] - public void Generate_ProxyType_HasCtorAndCreateInstance () + public class ProxyType : IDisposable { - var peers = ScanFixtures (); - using var stream = GenerateAssembly (peers); - using var pe = new PEReader (stream); - var reader = pe.GetMetadataReader (); - var objectProxy = reader.TypeDefinitions - .Select (h => reader.GetTypeDefinition (h)) - .First (t => reader.GetString (t.Name) == "Java_Lang_Object_Proxy"); - - var methods = objectProxy.GetMethods () - .Select (h => reader.GetMethodDefinition (h)) - .Select (m => reader.GetString (m.Name)) - .ToList (); - - Assert.Contains (".ctor", methods); - Assert.Contains ("CreateInstance", methods); - Assert.Contains ("get_TargetType", methods); + readonly string _outputDir = CreateTempDir (); + public void Dispose () => DeleteTempDir (_outputDir); + + [Fact] + public void Generate_CreatesProxyTypes () + { + var peers = ScanFixtures (); + var path = GenerateAssembly (peers, _outputDir); + var (pe, reader) = OpenAssembly (path); + using (pe) { + var proxyTypes = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .Where (t => reader.GetString (t.Namespace) == "_TypeMap.Proxies") + .ToList (); + + Assert.NotEmpty (proxyTypes); + Assert.Contains (proxyTypes, t => reader.GetString (t.Name) == "Java_Lang_Object_Proxy"); + } + } + + [Fact] + public void Generate_ProxyType_HasCtorAndCreateInstance () + { + var peers = ScanFixtures (); + var path = GenerateAssembly (peers, _outputDir); + var (pe, reader) = OpenAssembly (path); + using (pe) { + var objectProxy = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .First (t => reader.GetString (t.Name) == "Java_Lang_Object_Proxy"); + + var methods = objectProxy.GetMethods () + .Select (h => reader.GetMethodDefinition (h)) + .Select (m => reader.GetString (m.Name)) + .ToList (); + + Assert.Contains (".ctor", methods); + Assert.Contains ("CreateInstance", methods); + Assert.Contains ("get_TargetType", methods); + } + } + + } + + 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); + } + } + } - [Fact] - public void Generate_HasIgnoresAccessChecksToAttribute () + public class IgnoresAccessChecksTo : IDisposable { - var peers = ScanFixtures (); - using var stream = GenerateAssembly (peers); - using var pe = new PEReader (stream); - var reader = pe.GetMetadataReader (); - var types = reader.TypeDefinitions - .Select (h => reader.GetTypeDefinition (h)) - .ToList (); - Assert.Contains (types, t => - reader.GetString (t.Name) == "IgnoresAccessChecksToAttribute" && - reader.GetString (t.Namespace) == "System.Runtime.CompilerServices"); + readonly string _outputDir = CreateTempDir (); + public void Dispose () => DeleteTempDir (_outputDir); + + [Fact] + public void Generate_HasIgnoresAccessChecksToAttribute () + { + var peers = ScanFixtures (); + var path = GenerateAssembly (peers, _outputDir); + var (pe, reader) = OpenAssembly (path); + using (pe) { + var types = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .ToList (); + Assert.Contains (types, t => + reader.GetString (t.Name) == "IgnoresAccessChecksToAttribute" && + reader.GetString (t.Namespace) == "System.Runtime.CompilerServices"); + } + } + } - [Fact] - public void Generate_DuplicateJniNames_CreatesAliasEntriesAndAssociationAttribute () + public class Alias : IDisposable { - var peers = new List { + readonly string _outputDir = CreateTempDir (); + public void Dispose () => DeleteTempDir (_outputDir); + + static List MakeDuplicateAliasPeers () => new List { new JavaPeerInfo { JavaName = "test/Duplicate", CompatJniName = "test/Duplicate", @@ -127,287 +225,226 @@ public void Generate_DuplicateJniNames_CreatesAliasEntriesAndAssociationAttribut }, }; - using var stream = GenerateAssembly (peers, "AliasTest"); - using var pe = new PEReader (stream); - var reader = pe.GetMetadataReader (); - var assemblyAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); - Assert.True (assemblyAttrs.Count () >= 3); + [Fact] + public void Generate_DuplicateJniNames_CreatesAliasEntries () + { + var peers = MakeDuplicateAliasPeers (); + var path = GenerateAssembly (peers, _outputDir, "AliasTest"); + var (pe, reader) = OpenAssembly (path); + using (pe) { + var assemblyAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); + Assert.True (assemblyAttrs.Count () >= 3); + } + } - var typeNames = GetTypeRefNames (reader); - Assert.Contains ("TypeMapAssociationAttribute", typeNames); - } + [Fact] + public void Generate_DuplicateJniNames_EmitsTypeMapAssociationAttribute () + { + var peers = MakeDuplicateAliasPeers (); + var path = GenerateAssembly (peers, _outputDir, "AliasAssocTest"); + var (pe, reader) = OpenAssembly (path); + using (pe) { + var memberRefs = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) + .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i))) + .Where (m => reader.GetString (m.Name) == ".ctor") + .ToList (); + + var typeNames = GetTypeRefNames (reader); + Assert.Contains ("TypeMapAssociationAttribute", typeNames); + } + } - [Fact] - public void Generate_EmptyPeerList_ProducesValidAssembly () - { - using var stream = GenerateAssembly ([], "EmptyTest"); - using var pe = new PEReader (stream); - var reader = pe.GetMetadataReader (); - Assert.NotNull (reader); - var asmDef = reader.GetAssemblyDefinition (); - Assert.Equal ("EmptyTest", reader.GetString (asmDef.Name)); } - [Fact] - public void Generate_SimpleActivity_UsesGetUninitializedObject () + public class EmptyInput : IDisposable { - var peers = ScanFixtures (); - var simpleActivity = peers.First (p => p.JavaName == "my/app/SimpleActivity"); - Assert.NotNull (simpleActivity.ActivationCtor); - Assert.NotEqual (simpleActivity.ManagedTypeName, simpleActivity.ActivationCtor.DeclaringTypeName); - - using var stream = GenerateAssembly (new [] { simpleActivity }, "InheritedCtorTest"); - using var pe = new PEReader (stream); - var reader = pe.GetMetadataReader (); - var typeNames = GetTypeRefNames (reader); - Assert.Contains ("RuntimeHelpers", typeNames); - - var memberNames = GetMemberRefNames (reader); - Assert.DoesNotContain ("CreateManagedPeer", memberNames); - Assert.Contains ("GetUninitializedObject", memberNames); - } + readonly string _outputDir = CreateTempDir (); + public void Dispose () => DeleteTempDir (_outputDir); + + [Fact] + public void Generate_EmptyPeerList_ProducesValidAssembly () + { + var path = GenerateAssembly (Array.Empty (), _outputDir, "EmptyTest"); + Assert.True (File.Exists (path)); + var (pe, reader) = OpenAssembly (path); + using (pe) { + Assert.NotNull (reader); + var asmDef = reader.GetAssemblyDefinition (); + Assert.Equal ("EmptyTest", reader.GetString (asmDef.Name)); + } + } - [Fact] - public void Generate_LeafCtor_DoesNotUseCreateManagedPeer () - { - var peers = ScanFixtures (); - // ClickableView has its own (IntPtr, JniHandleOwnership) ctor - var clickableView = peers.First (p => p.JavaName == "my/app/ClickableView"); - Assert.NotNull (clickableView.ActivationCtor); - Assert.Equal (clickableView.ManagedTypeName, clickableView.ActivationCtor.DeclaringTypeName); - - using var stream = GenerateAssembly (new [] { clickableView }, "LeafCtorTest"); - using var pe = new PEReader (stream); - var reader = pe.GetMetadataReader (); - var memberNames = GetMemberRefNames (reader); - Assert.DoesNotContain ("CreateManagedPeer", memberNames); - - var ctorRefs = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) - .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i))) - .Where (m => reader.GetString (m.Name) == ".ctor") - .ToList (); - Assert.True (ctorRefs.Count >= 2, "Should have ctor refs for proxy base + target type"); } - [Fact] - public void Generate_GenericType_ThrowsNotSupportedException () + public class JniSignatureHelperTests { - var peers = ScanFixtures (); - var generic = peers.First (p => p.JavaName == "my/app/GenericHolder"); - Assert.True (generic.IsGenericDefinition); - - using var stream = GenerateAssembly (new [] { generic }, "GenericTest"); - using var pe = new PEReader (stream); - var reader = pe.GetMetadataReader (); - var typeNames = GetTypeRefNames (reader); - Assert.Contains ("NotSupportedException", typeNames); - } - [Fact] - public void Generate_InheritedCtor_IncludesBaseCtorAssembly () - { - // SimpleActivity inherits activation ctor from Activity — both in TestFixtures - // but the generated assembly is "IgnoresAccessTest", so TestFixtures must be - // in IgnoresAccessChecksTo - var peers = ScanFixtures (); - var simpleActivity = peers.First (p => p.JavaName == "my/app/SimpleActivity"); - - using var stream = GenerateAssembly (new [] { simpleActivity }, "IgnoresAccessTest"); - using var pe = new PEReader (stream); - var reader = pe.GetMetadataReader (); - var ignoresAttrType = reader.TypeDefinitions - .Select (h => reader.GetTypeDefinition (h)) - .FirstOrDefault (t => reader.GetString (t.Name) == "IgnoresAccessChecksToAttribute"); - Assert.True (ignoresAttrType.Attributes != 0, "IgnoresAccessChecksToAttribute should be defined"); - - var assemblyAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); - var attrBlobs = new List (); - foreach (var attrHandle in assemblyAttrs) { - var attr = reader.GetCustomAttribute (attrHandle); - var blob = reader.GetBlobBytes (attr.Value); - var blobStr = System.Text.Encoding.UTF8.GetString (blob); - attrBlobs.Add (blobStr); + [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); } - // Activity is in TestFixtures, so IgnoresAccessChecksTo must include TestFixtures - Assert.Contains (attrBlobs, b => b.Contains ("TestFixtures")); - } - [Fact] - public void Generate_JiStyleCtor_EmitsJavaInteropActivation () - { - var peers = ScanFixtures (); - var jiPeer = peers.First (p => p.JavaName == "my/app/JiStylePeer"); - Assert.NotNull (jiPeer.ActivationCtor); - Assert.Equal (ActivationCtorStyle.JavaInterop, jiPeer.ActivationCtor.Style); - - using var stream = GenerateAssembly (new [] { jiPeer }, "JiStyleTest"); - using var pe = new PEReader (stream); - var reader = pe.GetMetadataReader (); - - // JI-style activation should emit JniObjectReference and JniObjectReferenceOptions type refs - var typeNames = GetTypeRefNames (reader); - Assert.Contains ("JniObjectReference", typeNames); - Assert.Contains ("JniObjectReferenceOptions", typeNames); - - // The proxy still exists (with a TargetType property) - var proxyTypes = reader.TypeDefinitions - .Select (h => reader.GetTypeDefinition (h)) - .Where (t => reader.GetString (t.Namespace) == "_TypeMap.Proxies") - .ToList (); - Assert.Single (proxyTypes); - } + [Fact] + public void ParseParameterTypes_Boolean_MapsToCorrectKind () + { + var types = JniSignatureHelper.ParseParameterTypes ("(Z)V"); + Assert.Single (types); + Assert.Equal (JniParamKind.Boolean, types [0]); + } - [Fact] - public void Emit_CalledTwice_Throws () - { - var model = ModelBuilder.Build ([], "Double.dll", "Double"); - var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); - emitter.Emit (model, new MemoryStream ()); - // MetadataBuilder.AddAssembly throws on second call (only one assembly definition per PE) - Assert.ThrowsAny (() => emitter.Emit (model, new MemoryStream ())); - } + [Fact] + public void ParseParameterTypes_Object_MapsToCorrectKind () + { + var types = JniSignatureHelper.ParseParameterTypes ("(Ljava/lang/String;)V"); + Assert.Single (types); + Assert.Equal (JniParamKind.Object, types [0]); + } + + [Fact] + public void ParseReturnType_Void () => Assert.Equal (JniParamKind.Void, JniSignatureHelper.ParseReturnType ("()V")); + + [Fact] + public void ParseReturnType_Int () => Assert.Equal (JniParamKind.Int, JniSignatureHelper.ParseReturnType ("()I")); + + [Fact] + public void ParseReturnType_Boolean () => Assert.Equal (JniParamKind.Boolean, JniSignatureHelper.ParseReturnType ("()Z")); + + [Fact] + public void ParseReturnType_Object () => Assert.Equal (JniParamKind.Object, JniSignatureHelper.ParseReturnType ("()Ljava/lang/String;")); - [Fact] - public void EmitBody_ILCallbackCallsAddMemberRef_SignatureNotCorrupted () - { - // Regression test: EmitBody uses shared _sigBlob for the method signature. - // If the emitIL callback calls AddMemberRef (which also uses _sigBlob), - // the method signature must not be corrupted. - var pe = new PEAssemblyBuilder (new Version (11, 0, 0, 0)); - pe.EmitPreamble ("SigTest", "SigTest.dll"); - - var objectRef = pe.Metadata.AddTypeReference (pe.SystemRuntimeRef, - pe.Metadata.GetOrAddString ("System"), pe.Metadata.GetOrAddString ("Object")); - - // already defined; add a type to host the method - pe.Metadata.AddTypeDefinition ( - System.Reflection.TypeAttributes.Public | System.Reflection.TypeAttributes.Class, - pe.Metadata.GetOrAddString ("Test"), - pe.Metadata.GetOrAddString ("MyType"), - objectRef, - MetadataTokens.FieldDefinitionHandle (pe.Metadata.GetRowCount (TableIndex.Field) + 1), - MetadataTokens.MethodDefinitionHandle (pe.Metadata.GetRowCount (TableIndex.MethodDef) + 1)); - - // EmitBody with an IL callback that calls AddMemberRef (clearing _sigBlob) - pe.EmitBody ("TestMethod", - MethodAttributes.Public | MethodAttributes.Static, - sig => sig.MethodSignature ().Parameters (1, - rt => rt.Void (), - p => p.AddParameter ().Type ().Int32 ()), - encoder => { - // This AddMemberRef call clears and repopulates _sigBlob - pe.AddMemberRef (objectRef, ".ctor", - s => s.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { })); - encoder.OpCode (ILOpCode.Ret); - }); - - // If the sig blob was corrupted, the PE metadata will have a wrong signature. - // Write and read back to verify. - var stream = new MemoryStream (); - pe.WritePE (stream); - stream.Position = 0; - - using var peReader = new PEReader (stream); - var reader = peReader.GetMetadataReader (); - var methods = reader.TypeDefinitions - .SelectMany (h => reader.GetTypeDefinition (h).GetMethods ()) - .Select (h => reader.GetMethodDefinition (h)) - .ToList (); - - var testMethod = methods.First (m => reader.GetString (m.Name) == "TestMethod"); - var sig = testMethod.DecodeSignature (SignatureTypeProvider.Instance, null); - var paramType = Assert.Single (sig.ParameterTypes); - Assert.Equal ("System.Int32", paramType); } - [Fact] - public void Generate_JiStyleCtor_FirstParamIsByRef () + public class NegativeEdgeCase { - var peers = ScanFixtures (); - var jiPeer = peers.First (p => p.JavaName == "my/app/JiStylePeer"); - Assert.Equal (ActivationCtorStyle.JavaInterop, jiPeer.ActivationCtor!.Style); - - using var stream = GenerateAssembly (new [] { jiPeer }, "JiByRefTest"); - using var pe = new PEReader (stream); - var reader = pe.GetMetadataReader (); - - // Find the .ctor member reference whose parent type is the JI peer's declaring type - var ctorRefs = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) - .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i))) - .Where (m => reader.GetString (m.Name) == ".ctor") - .ToList (); - - // Decode each .ctor signature and find the JI-style one (2 params, first is byref JniObjectReference) - bool foundByRefCtor = false; - foreach (var ctor in ctorRefs) { - var sig = ctor.DecodeMethodSignature (SignatureTypeProvider.Instance, null); - if (sig.ParameterTypes.Length == 2 && - sig.ParameterTypes [0].Contains ("JniObjectReference")) { - // The byref encoding should produce "Java.Interop.JniObjectReference&" - Assert.True (sig.ParameterTypes [0].EndsWith ("&"), - $"JI-style .ctor first param must be byref, got: {sig.ParameterTypes [0]}"); - foundByRefCtor = true; - } + + [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 ("(")); } - Assert.True (foundByRefCtor, "Expected to find a .ctor with byref JniObjectReference parameter"); - } - [Fact] - public void Generate_JiStyleCtor_EmitsDeleteRefCall () - { - var peers = ScanFixtures (); - var jiPeer = peers.First (p => p.JavaName == "my/app/JiStylePeer"); - - using var stream = GenerateAssembly (new [] { jiPeer }, "JiDeleteRefTest"); - using var pe = new PEReader (stream); - var reader = pe.GetMetadataReader (); - - // The JI-style activation path must emit a call to JNIEnv.DeleteRef(IntPtr, JniHandleOwnership) - // to match the legacy TypeManager.CreateProxy behavior. - var memberRefs = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) - .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i))) - .ToList (); - - var deleteRefRef = memberRefs.FirstOrDefault (m => reader.GetString (m.Name) == "DeleteRef"); - Assert.True (!deleteRefRef.Equals (default (MemberReference)), - "JI-style activation must emit a DeleteRef member reference for JNI handle cleanup"); - - // Verify it's on the JNIEnv type - var parentTypeRef = reader.GetTypeReference ((TypeReferenceHandle)deleteRefRef.Parent); - Assert.Equal ("JNIEnv", reader.GetString (parentTypeRef.Name)); - Assert.Equal ("Android.Runtime", reader.GetString (parentTypeRef.Namespace)); } - [Fact] - public void Generate_DifferentContent_ProducesDifferentMVIDs () + public class CreateInstancePaths : IDisposable { - var peer1 = MakePeerWithActivation ("test/TypeA", "Test.TypeA", "TestAsm"); - var peer2 = MakePeerWithActivation ("test/TypeB", "Test.TypeB", "TestAsm"); + readonly string _outputDir = CreateTempDir (); + public void Dispose () => DeleteTempDir (_outputDir); + + [Fact] + public void Generate_SimpleActivity_UsesGetUninitializedObject () + { + var peers = ScanFixtures (); + var simpleActivity = peers.First (p => p.JavaName == "my/app/SimpleActivity"); + Assert.NotNull (simpleActivity.ActivationCtor); + Assert.NotEqual (simpleActivity.ManagedTypeName, simpleActivity.ActivationCtor.DeclaringTypeName); + + var path = GenerateAssembly (new [] { simpleActivity }, _outputDir, "InheritedCtorTest"); + var (pe, reader) = OpenAssembly (path); + using (pe) { + var typeNames = GetTypeRefNames (reader); + Assert.Contains ("RuntimeHelpers", typeNames); + + var memberNames = GetMemberRefNames (reader); + Assert.DoesNotContain ("CreateManagedPeer", memberNames); + Assert.Contains ("GetUninitializedObject", memberNames); + } + } - using var stream1 = GenerateAssembly (new [] { peer1 }, "SameName"); - using var stream2 = GenerateAssembly (new [] { peer2 }, "SameName"); + [Fact] + public void Generate_LeafCtor_DoesNotUseCreateManagedPeer () + { + var peers = ScanFixtures (); + // ClickableView has its own (IntPtr, JniHandleOwnership) ctor + var clickableView = peers.First (p => p.JavaName == "my/app/ClickableView"); + Assert.NotNull (clickableView.ActivationCtor); + Assert.Equal (clickableView.ManagedTypeName, clickableView.ActivationCtor.DeclaringTypeName); + + var path = GenerateAssembly (new [] { clickableView }, _outputDir, "LeafCtorTest"); + var (pe, reader) = OpenAssembly (path); + using (pe) { + var memberNames = GetMemberRefNames (reader); + Assert.DoesNotContain ("CreateManagedPeer", memberNames); + + var ctorRefs = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) + .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i))) + .Where (m => reader.GetString (m.Name) == ".ctor") + .ToList (); + Assert.True (ctorRefs.Count >= 2, "Should have ctor refs for proxy base + target type"); + } + } - using var pe1 = new PEReader (stream1); - using var pe2 = new PEReader (stream2); - var mvid1 = pe1.GetMetadataReader ().GetGuid (pe1.GetMetadataReader ().GetModuleDefinition ().Mvid); - var mvid2 = pe2.GetMetadataReader ().GetGuid (pe2.GetMetadataReader ().GetModuleDefinition ().Mvid); + [Fact] + public void Generate_GenericType_ThrowsNotSupportedException () + { + var peers = ScanFixtures (); + var generic = peers.First (p => p.JavaName == "my/app/GenericHolder"); + Assert.True (generic.IsGenericDefinition); + + var path = GenerateAssembly (new [] { generic }, _outputDir, "GenericTest"); + var (pe, reader) = OpenAssembly (path); + using (pe) { + var typeNames = GetTypeRefNames (reader); + Assert.Contains ("NotSupportedException", typeNames); + } + } - Assert.NotEqual (mvid1, mvid2); } - [Fact] - public void Generate_IdenticalContent_ProducesIdenticalMVIDs () + public class IgnoresAccessChecksToForBaseCtor : IDisposable { - var peer = MakePeerWithActivation ("test/TypeA", "Test.TypeA", "TestAsm"); - - using var stream1 = GenerateAssembly (new [] { peer }, "SameName"); - using var stream2 = GenerateAssembly (new [] { peer }, "SameName"); - - using var pe1 = new PEReader (stream1); - using var pe2 = new PEReader (stream2); - var mvid1 = pe1.GetMetadataReader ().GetGuid (pe1.GetMetadataReader ().GetModuleDefinition ().Mvid); - var mvid2 = pe2.GetMetadataReader ().GetGuid (pe2.GetMetadataReader ().GetModuleDefinition ().Mvid); + readonly string _outputDir = CreateTempDir (); + public void Dispose () => DeleteTempDir (_outputDir); + + [Fact] + public void Generate_InheritedCtor_IncludesBaseCtorAssembly () + { + // SimpleActivity inherits activation ctor from Activity — both in TestFixtures + // but the generated assembly is "IgnoresAccessTest", so TestFixtures must be + // in IgnoresAccessChecksTo + var peers = ScanFixtures (); + var simpleActivity = peers.First (p => p.JavaName == "my/app/SimpleActivity"); + + var path = GenerateAssembly (new [] { simpleActivity }, _outputDir, "IgnoresAccessTest"); + var (pe, reader) = OpenAssembly (path); + using (pe) { + var ignoresAttrType = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .FirstOrDefault (t => reader.GetString (t.Name) == "IgnoresAccessChecksToAttribute"); + Assert.True (ignoresAttrType.Attributes != 0, "IgnoresAccessChecksToAttribute should be defined"); + + var assemblyAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); + var attrBlobs = new List (); + foreach (var attrHandle in assemblyAttrs) { + var attr = reader.GetCustomAttribute (attrHandle); + var blob = reader.GetBlobBytes (attr.Value); + var blobStr = System.Text.Encoding.UTF8.GetString (blob); + attrBlobs.Add (blobStr); + } + // Activity is in TestFixtures, so IgnoresAccessChecksTo must include TestFixtures + Assert.Contains (attrBlobs, b => b.Contains ("TestFixtures")); + } + } - Assert.Equal (mvid1, mvid2); } + } \ No newline at end of file diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 31a06dec2e9..7c7a757fa4c 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -12,34 +12,69 @@ public class ModelBuilderTests : FixtureTestBase { static TypeMapAssemblyData BuildModel (IReadOnlyList peers, string? assemblyName = null) { - var outputPath = Path.Combine (Path.GetTempPath (), (assemblyName ?? "TestTypeMap") + ".dll"); + var outputPath = Path.Combine ("/tmp", (assemblyName ?? "TestTypeMap") + ".dll"); return ModelBuilder.Build (peers, outputPath, assemblyName); } + public class BasicStructure { + [Fact] public void Build_EmptyPeers_ProducesEmptyModel () { - var model = BuildModel ([], "Empty"); + var model = BuildModel (Array.Empty (), "Empty"); Assert.Equal ("Empty", model.AssemblyName); Assert.Equal ("Empty.dll", model.ModuleName); Assert.Empty (model.Entries); Assert.Empty (model.ProxyTypes); } - [Theory] - [InlineData ("Foo.Bar.dll", null, "Foo.Bar")] - [InlineData ("Foo.dll", "MyAssembly", "MyAssembly")] - public void Build_AssemblyName_ResolvedCorrectly (string outputPath, string? explicitName, string expected) + [Fact] + public void Build_AssemblyNameDerivedFromOutputPath () + { + var model = ModelBuilder.Build (Array.Empty (), "/some/path/Foo.Bar.dll"); + Assert.Equal ("Foo.Bar", model.AssemblyName); + Assert.Equal ("Foo.Bar.dll", model.ModuleName); + } + + [Fact] + public void Build_ExplicitAssemblyName_OverridesOutputPath () { - var model = ModelBuilder.Build ([], outputPath, explicitName); - Assert.Equal (expected, model.AssemblyName); + var model = ModelBuilder.Build (Array.Empty (), "/some/path/Foo.dll", "MyAssembly"); + Assert.Equal ("MyAssembly", model.AssemblyName); } + + [Fact] + public void Build_ComputesIgnoresAccessChecksToFromCrossAssemblyCallbacks () + { + var peer = MakeAcwPeer ("my/app/MainActivity", "MyApp.MainActivity", "MyApp") with { + MarshalMethods = new List { + MakeMarshalMethod ("", "n_ctor", "()V", isConstructor: true), + new MarshalMethodInfo { + JniName = "onCreate", + NativeCallbackName = "n_OnCreate", + JniSignature = "(Landroid/os/Bundle;)V", + ManagedMethodName = "OnCreate", + JniReturnType = "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 { + [Fact] public void Build_CreatesOneEntryPerPeer () { @@ -69,14 +104,13 @@ public void Build_DuplicateJniNames_CreatesAliasEntries () Assert.Contains ("Test.First", model.Entries [0].ProxyTypeReference); Assert.Equal ("test/Dup[1]", model.Entries [1].JniName); Assert.Contains ("Test.Second", model.Entries [1].ProxyTypeReference); - - // No associations when neither peer has a proxy (no activation ctor or invoker) - Assert.Empty (model.Associations); } + } public class ConditionalAttributes { + [Theory] [InlineData ("java/lang/Object")] [InlineData ("java/lang/Throwable")] @@ -88,7 +122,9 @@ public class ConditionalAttributes [InlineData ("java/lang/Thread")] public void Build_AllEssentialRuntimeTypes_AreUnconditional (string jniName) { - var peer = MakeMcwPeer (jniName, "Java.Lang.SomeType", "Mono.Android") with { DoNotGenerateAcw = true }; + var peer = MakeMcwPeer (jniName, "Java.Lang.SomeType", "Mono.Android") with { + DoNotGenerateAcw = true, + }; var model = BuildModel (new [] { peer }); Assert.True (model.Entries [0].IsUnconditional, $"{jniName} should be unconditional"); } @@ -110,7 +146,9 @@ public void Build_UserAcwType_IsUnconditional () public void Build_McwBinding_IsTrimmable () { // MCW binding types (DoNotGenerateAcw=true) are trimmable unless essential - var peer = MakeMcwPeer ("android/app/Activity", "Android.App.Activity", "Mono.Android") with { DoNotGenerateAcw = true }; + var peer = MakeMcwPeer ("android/app/Activity", "Android.App.Activity", "Mono.Android") with { + DoNotGenerateAcw = true, + }; var model = BuildModel (new [] { peer }); Assert.Single (model.Entries); @@ -131,10 +169,12 @@ public void Build_UnconditionalScannedType_IsUnconditional () Assert.True (model.Entries [0].IsUnconditional); } + } public class Aliases { + [Fact] public void Build_AliasedPeersWithActivation_GetDistinctProxies () { @@ -159,31 +199,31 @@ public void Build_McwPeerWithoutActivation_NoProxy () Assert.Single (model.Entries); Assert.Contains ("Java.Lang.Object, Mono.Android", model.Entries [0].ProxyTypeReference); } + } public class ProxyTypes { - [Theory] - [InlineData ("java/lang/Object", "Java.Lang.Object", "Mono.Android", "Java_Lang_Object_Proxy")] - [InlineData ("com/example/Outer$Inner", "Com.Example.Outer.Inner", "App", "Com_Example_Outer_Inner_Proxy")] - public void Build_PeerWithActivation_CreatesNamedProxy (string jniName, string managedName, string asmName, string expectedProxyName) + + [Fact] + public void Build_PeerWithActivationCtor_CreatesProxy () { - var peer = MakePeerWithActivation (jniName, managedName, asmName); + var peer = MakePeerWithActivation ("java/lang/Object", "Java.Lang.Object", "Mono.Android"); var model = BuildModel (new [] { peer }, "MyTypeMap"); Assert.Single (model.ProxyTypes); var proxy = model.ProxyTypes [0]; - Assert.Equal (expectedProxyName, proxy.TypeName); + Assert.Equal ("Java_Lang_Object_Proxy", proxy.TypeName); Assert.Equal ("_TypeMap.Proxies", proxy.Namespace); Assert.True (proxy.HasActivation); - Assert.Equal (managedName, proxy.TargetType.ManagedTypeName); - Assert.Equal (asmName, proxy.TargetType.AssemblyName); + Assert.Equal ("Java.Lang.Object", proxy.TargetType.ManagedTypeName); + Assert.Equal ("Mono.Android", proxy.TargetType.AssemblyName); } [Fact] public void Build_PeerWithInvoker_CreatesProxy () { - var peer = MakeInterfacePeer ("android/view/View$OnClickListener", "Android.Views.View+IOnClickListener", "Mono.Android", "Android.Views.View+IOnClickListenerInvoker"); + var peer = MakeInterfacePeer (); var model = BuildModel (new [] { peer }); Assert.Single (model.ProxyTypes); @@ -191,10 +231,283 @@ public void Build_PeerWithInvoker_CreatesProxy () Assert.NotNull (proxy.InvokerType); Assert.Equal ("Android.Views.View+IOnClickListenerInvoker", proxy.InvokerType!.ManagedTypeName); } + + [Fact] + public void Build_ProxyNaming_ReplacesDotAndPlus () + { + var peer = MakePeerWithActivation ("com/example/Outer$Inner", "Com.Example.Outer.Inner", "App"); + var model = BuildModel (new [] { peer }); + + Assert.Single (model.ProxyTypes); + Assert.Equal ("Com_Example_Outer_Inner_Proxy", model.ProxyTypes [0].TypeName); + } + + } + + 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", + CompatJniName = "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") with { + DoNotGenerateAcw = true, + 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") with { + 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;") with { + DeclaringTypeName = "Java.Lang.Object", + DeclaringAssemblyName = "Mono.Android", + }; + + var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App") with { + 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") with { + 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") with { + 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", + CompatJniName = "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") with { + 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") with { + 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;" }, + } + }, + }, + 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 { + [Fact] public void Build_FromScannedFixtures_ProducesValidModel () { @@ -220,10 +533,26 @@ public void ScanFixtures_ManagedTypeShortName_IsCorrect (string javaName, string var peer = FindFixtureByJavaName (javaName); 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 { + [Theory] [InlineData ("my/app/MainActivity")] [InlineData ("my/app/TouchHandler")] @@ -245,6 +574,7 @@ public void Fixture_McwBinding_IsTrimmable (string javaName) var model = BuildModel (new [] { peer }); Assert.False (model.Entries [0].IsUnconditional); } + } static JavaPeerProxyData? FindProxy (TypeMapAssemblyData model, string proxyTypeName) @@ -257,8 +587,10 @@ public void Fixture_McwBinding_IsTrimmable (string javaName) return model.Entries.FirstOrDefault (e => e.JniName == jniName); } + public class FixtureMcwTypes { + [Theory] [InlineData ("java/lang/Object", "Java_Lang_Object_Proxy", "Java.Lang.Object")] [InlineData ("android/app/Activity", "Android_App_Activity_Proxy", "Android.App.Activity")] @@ -273,6 +605,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] @@ -303,21 +640,120 @@ 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); + } } + } public class FixtureInterfaces { + [Fact] public void Fixture_IOnClickListener_HasInvokerProxy () { @@ -333,10 +769,12 @@ public void Fixture_IOnClickListener_HasInvokerProxy () Assert.NotNull (proxy!.InvokerType); Assert.Equal ("Android.Views.IOnClickListenerInvoker", proxy.InvokerType!.ManagedTypeName); } + } public class FixtureNestedTypes { + [Theory] [InlineData ("my/app/Outer$Inner", "MyApp_Outer_Inner_Proxy", "MyApp.Outer+Inner")] [InlineData ("my/app/ICallback$Result", "MyApp_ICallback_Result_Proxy", "MyApp.ICallback+Result")] @@ -354,10 +792,12 @@ public void Fixture_NestedType_ProxyNaming (string javaName, string expectedProx Assert.Equal (expectedManagedName, proxy!.TargetType.ManagedTypeName); } } + } public class FixtureInvokers { + [Fact] public void Fixture_InterfaceAndInvoker_ShareJniName_InvokerSeparated () { @@ -386,7 +826,9 @@ public void Build_InvokerType_NoProxyNoEntry () // Invoker types should never get their own proxy or TypeMap entry. // They only appear as a TypeRef in the interface proxy's InvokerType/CreateInstance. var ifacePeer = MakeInterfacePeer ("my/app/IFoo", "MyApp.IFoo", "App", "MyApp.FooInvoker"); - var invokerPeer = MakePeerWithActivation ("my/app/IFoo", "MyApp.FooInvoker", "App") with { DoNotGenerateAcw = true }; + var invokerPeer = MakePeerWithActivation ("my/app/IFoo", "MyApp.FooInvoker", "App") with { + DoNotGenerateAcw = true, + }; var model = BuildModel (new [] { ifacePeer, invokerPeer }); @@ -404,10 +846,12 @@ public void Build_InvokerType_NoProxyNoEntry () // Interface proxy has activation because it will create the invoker Assert.True (proxy.HasActivation); } + } public class FixtureGenericHolder { + [Fact] public void Fixture_GenericHolder_Entry () { @@ -418,10 +862,12 @@ public void Fixture_GenericHolder_Entry () var entry = FindEntry (model, "my/app/GenericHolder"); Assert.NotNull (entry); } + } public class FixtureAcwTypeHasProxy { + [Theory] [InlineData ("my/app/AbstractBase", "MyApp_AbstractBase_Proxy")] [InlineData ("my/app/ClickableView", "MyApp_ClickableView_Proxy")] @@ -434,19 +880,51 @@ public void Fixture_AcwType_HasProxy (string javaName, string expectedProxyName) var model = BuildModel (new [] { peer }, "TypeMap"); - if (peer.ActivationCtor != null) { + if (peer.ActivationCtor != null && peer.MarshalMethods.Count > 0) { var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == expectedProxyName); Assert.NotNull (proxy); + Assert.True (proxy!.IsAcw); + } + } + + [Fact] + public void Fixture_ClickableView_HasOnClickUcoWrapper () + { + var peer = FindFixtureByJavaName ("my/app/ClickableView"); + var model = BuildModel (new [] { peer }, "TypeMap"); + + if (peer.ActivationCtor != null && peer.MarshalMethods.Count > 0) { + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_ClickableView_Proxy"); + Assert.NotNull (proxy); + var onClick = proxy!.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_OnClick"); + Assert.NotNull (onClick); + Assert.Equal ("(Landroid/view/View;)V", onClick!.JniSignature); + } + } + + [Fact] + public void Fixture_MultiInterfaceView_HasAllUcoMethods () + { + var peer = FindFixtureByJavaName ("my/app/MultiInterfaceView"); + var model = BuildModel (new [] { peer }, "TypeMap"); + + if (peer.ActivationCtor != null && peer.MarshalMethods.Count > 0) { + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_MultiInterfaceView_Proxy"); + Assert.NotNull (proxy); + Assert.NotNull (proxy!.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_OnClick")); + Assert.NotNull (proxy.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_OnLongClick")); } } + } public class FixtureImplementorsAndDispatchers { + [Theory] [InlineData ("mono/android/view/View_IOnClickListenerImplementor", "Implementor")] [InlineData ("mono/android/view/View_ClickEventDispatcher", "EventDispatcher")] - public void Fixture_HelperType_IsUnconditional (string javaName, string kind) + public void Fixture_HelperType_IsTrimmable_NotUnconditional (string javaName, string kind) { var peer = FindFixtureByJavaName (javaName); Assert.False (peer.DoNotGenerateAcw); @@ -456,20 +934,39 @@ 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"); + Assert.False (entry!.IsUnconditional, $"{kind} should NOT be unconditional"); + Assert.NotNull (entry.TargetTypeReference); } + } - public class InvokerDetection + public class NameBasedDetection { + + [Fact] + public void Build_UserTypeNamedImplementor_IsTreatedAsTrimmable () + { + // Limitation: name-based heuristic means a user type ending in "Implementor" + // will be treated as trimmable even if it's genuinely a user ACW type. + // This test documents the known behavior. + var peer = MakeAcwPeer ("my/app/MyImplementor", "MyApp.MyImplementor", "App"); + var model = BuildModel (new [] { peer }); + + var entry = model.Entries.FirstOrDefault (); + Assert.NotNull (entry); + // The heuristic treats this as an Implementor → trimmable (not unconditional) + Assert.False (entry!.IsUnconditional, + "Name-based heuristic: types ending in 'Implementor' are treated as trimmable"); + } + [Fact] public void Build_TypeIsInvoker_OnlyWhenReferencedByAnotherPeer () { // A type is only treated as an invoker when another peer's InvokerTypeName references it. // A type named "MyInvoker" with DoNotGenerateAcw is NOT automatically an invoker. - var invokerPeer = MakePeerWithActivation ("my/app/MyInvoker", "MyApp.MyInvoker", "App") with { DoNotGenerateAcw = true }; + var invokerPeer = MakePeerWithActivation ("my/app/MyInvoker", "MyApp.MyInvoker", "App") with { + DoNotGenerateAcw = true, + }; // Without a referencing peer, it gets a normal entry var model1 = BuildModel (new [] { invokerPeer }); @@ -482,10 +979,12 @@ public void Build_TypeIsInvoker_OnlyWhenReferencedByAnotherPeer () Assert.Single (model2.Entries); Assert.Equal ("MyApp.IMyInterface", model2.ProxyTypes [0].TargetType.ManagedTypeName); } + } public class PipelineTests { + [Fact] public void FullPipeline_AllFixtures_ProducesLoadableAssembly () { @@ -520,27 +1019,33 @@ public void FullPipeline_AllFixtures_TypeMapAttributeCountMatchesEntries () var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); int totalAttrs = asmAttrs.Count (); - int expected = model.Entries.Count + model.Associations.Count + model.IgnoresAccessChecksTo.Count; + int expected = model.Entries.Count + model.IgnoresAccessChecksTo.Count; Assert.Equal (expected, totalAttrs); }); } [Fact] - public void FullPipeline_AliasGroup_TypeMapAttributeCountIncludesAssociations () + public void FullPipeline_TouchHandler_AcwProxyHasUcoAttributes () { - // Two peers with the same JNI name, both with activation → generates an association - var peers = new List { - MakePeerWithActivation ("test/Alias", "Test.Primary", "Asm"), - MakePeerWithActivation ("test/Alias", "Test.Secondary", "Asm"), - }; - var model = BuildModel (peers, "AliasAttrCount"); - Assert.NotEmpty (model.Associations); + var peer = FindFixtureByJavaName ("my/app/TouchHandler"); + var model = BuildModel (new [] { peer }, "UcoAttrTest"); - EmitAndVerify (model, "AliasAttrCount", (pe, reader) => { - var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); - int totalAttrs = asmAttrs.Count (); - int expected = model.Entries.Count + model.Associations.Count + model.IgnoresAccessChecksTo.Count; - Assert.Equal (expected, totalAttrs); + 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); + } }); } @@ -562,6 +1067,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); + } }); } @@ -580,10 +1126,12 @@ public void FullPipeline_GenericHolder_ProducesValidAssembly () Assert.NotEmpty (asmAttrs); }); } + } public class PeBlobValidation { + [Fact] public void FullPipeline_Mixed2ArgAnd3Arg_BothSurviveRoundTrip () { @@ -649,10 +1197,12 @@ public void FullPipeline_McwBinding_Emits3ArgAttribute () Assert.Contains ("Android.App.Activity", targetRef!); }); } + } public class DeterminismTests { + [Fact] public void Build_SameInput_ProducesDeterministicOutput () { @@ -668,16 +1218,21 @@ public void Build_SameInput_ProducesDeterministicOutput () Assert.Equal (model1.Entries [i].TargetTypeReference, model2.Entries [i].TargetTypeReference); } } + } static void EmitAndVerify (TypeMapAssemblyData model, string assemblyName, Action verify) { - var stream = new MemoryStream (); - var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); - emitter.Emit (model, stream); - stream.Position = 0; - using var pe = new PEReader (stream); - verify (pe, pe.GetMetadataReader ()); + var outputDir = CreateTempDir (); + try { + var outputPath = Path.Combine (outputDir, $"{assemblyName}.dll"); + var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); + emitter.Emit (model, outputPath); + using var pe = new PEReader (File.OpenRead (outputPath)); + verify (pe, pe.GetMetadataReader ()); + } finally { + DeleteTempDir (outputDir); + } } /// 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 { From 663ee69800572686bbc05f10fde41baeffc0307c Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 11 Mar 2026 19:09:29 +0100 Subject: [PATCH 02/21] Address review patterns from PR #10808 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Proactively address feedback patterns from the #10808 code review: - Remove null! from TypeMapAssemblyEmitter: make _pe readonly, init in ctor - Fix misleading JniParamKind.Boolean comment (Z → bool, not sbyte) - Make AddActivationCtorRef style-aware: support both XamarinAndroid (IntPtr, JniHandleOwnership) and JavaInterop (ref JniObjectReference, JniObjectReferenceOptions) activation constructor styles - Remove default parameter values from MakeInterfacePeer test helper - Replace disk I/O with MemoryStream in generator tests: add Stream overloads to Emit/Generate, use MemoryStream in tests for faster and more reliable test execution Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/JniSignatureHelper.cs | 2 +- .../Generator/PEAssemblyBuilder.cs | 12 +- .../Generator/RootTypeMapAssemblyGenerator.cs | 28 ++++- .../Generator/TypeMapAssemblyEmitter.cs | 55 +++++++-- .../Generator/TypeMapAssemblyGenerator.cs | 14 +++ .../Generator/FixtureTestBase.cs | 8 +- .../RootTypeMapAssemblyGeneratorTests.cs | 39 +++--- .../TypeMapAssemblyGeneratorTests.cs | 114 ++++++++---------- .../Generator/TypeMapModelBuilderTests.cs | 2 +- 9 files changed, 167 insertions(+), 107 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs index 0e9ac52f93e..81592f6166e 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs @@ -11,7 +11,7 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; enum JniParamKind { Void, // V -Boolean, // Z → sbyte +Boolean, // Z → bool Byte, // B → sbyte Char, // C → char Short, // S → short diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs index 9cae7720e6a..6f38811f2f1 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs @@ -92,14 +92,22 @@ public void WritePE (string outputPath) Directory.CreateDirectory (dir); } + using var fs = File.Create (outputPath); + WritePE (fs); + } + + /// + /// Serialises the metadata + IL into a PE DLL written to . + /// + public void WritePE (Stream output) + { var peBuilder = new ManagedPEBuilder ( new PEHeaderBuilder (imageCharacteristics: Characteristics.Dll), new MetadataRootBuilder (Metadata), ILBuilder); var peBlob = new BlobBuilder (); peBuilder.Serialize (peBlob); - using var fs = File.Create (outputPath); - peBlob.WriteContentTo (fs); + peBlob.WriteContentTo (output); } /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs index ef538272ea5..ca95513cbdd 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs @@ -48,7 +48,33 @@ public void Generate (IReadOnlyList perAssemblyTypeMapNames, string outp assemblyName ??= DefaultAssemblyName; var moduleName = Path.GetFileName (outputPath); + var pe = GenerateCore (perAssemblyTypeMapNames, assemblyName, moduleName); + pe.WritePE (outputPath); + } + /// + /// Generates the root typemap assembly and writes it to . + /// + /// Names of per-assembly typemap assemblies to reference. + /// Stream to write the output PE assembly to. + /// Optional assembly name (defaults to _Microsoft.Android.TypeMaps). + public void Generate (IReadOnlyList perAssemblyTypeMapNames, Stream output, string? assemblyName = null) + { + if (perAssemblyTypeMapNames is null) { + throw new ArgumentNullException (nameof (perAssemblyTypeMapNames)); + } + if (output is null) { + throw new ArgumentNullException (nameof (output)); + } + + assemblyName ??= DefaultAssemblyName; + var moduleName = assemblyName + ".dll"; + var pe = GenerateCore (perAssemblyTypeMapNames, assemblyName, moduleName); + pe.WritePE (output); + } + + PEAssemblyBuilder GenerateCore (IReadOnlyList perAssemblyTypeMapNames, string assemblyName, string moduleName) + { var pe = new PEAssemblyBuilder (_systemRuntimeVersion); pe.EmitPreamble (assemblyName, moduleName); @@ -76,6 +102,6 @@ public void Generate (IReadOnlyList perAssemblyTypeMapNames, string outp pe.Metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, ctorRef, blobHandle); } - pe.WritePE (outputPath); + return pe; } } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index ce34dfbb4fa..2bb68233310 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Reflection; using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; @@ -60,13 +61,15 @@ sealed class TypeMapAssemblyEmitter { readonly Version _systemRuntimeVersion; - PEAssemblyBuilder _pe = null!; + readonly PEAssemblyBuilder _pe; AssemblyReferenceHandle _javaInteropRef; TypeReferenceHandle _javaPeerProxyRef; TypeReferenceHandle _iJavaPeerableRef; TypeReferenceHandle _jniHandleOwnershipRef; + TypeReferenceHandle _jniObjectReferenceRef; + TypeReferenceHandle _jniObjectReferenceOptionsRef; TypeReferenceHandle _iAndroidCallableWrapperRef; TypeReferenceHandle _systemTypeRef; TypeReferenceHandle _runtimeTypeHandleRef; @@ -97,6 +100,7 @@ sealed class TypeMapAssemblyEmitter public TypeMapAssemblyEmitter (Version systemRuntimeVersion) { _systemRuntimeVersion = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion)); + _pe = new PEAssemblyBuilder (_systemRuntimeVersion); } /// @@ -111,7 +115,28 @@ public void Emit (TypeMapAssemblyData model, string outputPath) throw new ArgumentNullException (nameof (outputPath)); } - _pe = new PEAssemblyBuilder (_systemRuntimeVersion); + EmitCore (model); + _pe.WritePE (outputPath); + } + + /// + /// Emits a PE assembly from the given model and writes it to . + /// + public void Emit (TypeMapAssemblyData model, Stream output) + { + if (model is null) { + throw new ArgumentNullException (nameof (model)); + } + if (output is null) { + throw new ArgumentNullException (nameof (output)); + } + + EmitCore (model); + _pe.WritePE (output); + } + + void EmitCore (TypeMapAssemblyData model) + { _pe.EmitPreamble (model.AssemblyName, model.ModuleName); _javaInteropRef = _pe.AddAssemblyRef ("Java.Interop", new Version (0, 0, 0, 0)); @@ -135,7 +160,6 @@ public void Emit (TypeMapAssemblyData model, string outputPath) } _pe.EmitIgnoresAccessChecksToAttribute (model.IgnoresAccessChecksTo); - _pe.WritePE (outputPath); } void EmitTypeReferences () @@ -147,6 +171,10 @@ void EmitTypeReferences () metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("IJavaPeerable")); _jniHandleOwnershipRef = metadata.AddTypeReference (_pe.MonoAndroidRef, metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("JniHandleOwnership")); + _jniObjectReferenceRef = metadata.AddTypeReference (_javaInteropRef, + 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, @@ -342,7 +370,7 @@ void EmitCreateInstance (JavaPeerProxyData proxy) // Interface with invoker: new TInvoker(IntPtr, JniHandleOwnership) if (proxy.InvokerType != null) { - var invokerCtorRef = AddActivationCtorRef (_pe.ResolveTypeRef (proxy.InvokerType)); + var invokerCtorRef = AddActivationCtorRef (_pe.ResolveTypeRef (proxy.InvokerType), ActivationCtorStyle.XamarinAndroid); EmitCreateInstanceBody (encoder => { encoder.OpCode (ILOpCode.Ldarg_1); encoder.OpCode (ILOpCode.Ldarg_2); @@ -358,8 +386,8 @@ void EmitCreateInstance (JavaPeerProxyData proxy) var targetTypeRef = _pe.ResolveTypeRef (proxy.TargetType); if (activationCtor.IsOnLeafType) { - // Leaf type has its own ctor: new T(IntPtr, JniHandleOwnership) - var ctorRef = AddActivationCtorRef (targetTypeRef); + // Leaf type has its own ctor: new T(IntPtr, JniHandleOwnership) or new T(ref JniObjectReference, JniObjectReferenceOptions) + var ctorRef = AddActivationCtorRef (targetTypeRef, activationCtor.Style); EmitCreateInstanceBody (encoder => { encoder.OpCode (ILOpCode.Ldarg_1); encoder.OpCode (ILOpCode.Ldarg_2); @@ -368,8 +396,8 @@ void EmitCreateInstance (JavaPeerProxyData proxy) encoder.OpCode (ILOpCode.Ret); }); } else { - // Inherited ctor: GetUninitializedObject(typeof(T)) + call Base::.ctor(IntPtr, JniHandleOwnership) - var baseActivationCtorRef = AddActivationCtorRef (_pe.ResolveTypeRef (activationCtor.DeclaringType)); + // Inherited ctor: GetUninitializedObject(typeof(T)) + call Base::.ctor(IntPtr, JniHandleOwnership) or (ref JniObjectReference, JniObjectReferenceOptions) + var baseActivationCtorRef = AddActivationCtorRef (_pe.ResolveTypeRef (activationCtor.DeclaringType), activationCtor.Style); EmitCreateInstanceBody (encoder => { encoder.OpCode (ILOpCode.Ldtoken); encoder.Token (targetTypeRef); @@ -401,14 +429,19 @@ void EmitCreateInstanceBody (Action emitIL) emitIL); } - MemberReferenceHandle AddActivationCtorRef (EntityHandle declaringTypeRef) + MemberReferenceHandle AddActivationCtorRef (EntityHandle declaringTypeRef, ActivationCtorStyle style) { return _pe.AddMemberRef (declaringTypeRef, ".ctor", sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, rt => rt.Void (), p => { - p.AddParameter ().Type ().IntPtr (); - p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); + if (style == ActivationCtorStyle.JavaInterop) { + p.AddParameter ().Type (isByRef: true).Type (_jniObjectReferenceRef, true); + p.AddParameter ().Type ().Type (_jniObjectReferenceOptionsRef, true); + } else { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); + } })); } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs index 927346fbf10..f50582dd3cd 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; namespace Microsoft.Android.Sdk.TrimmableTypeMap; @@ -29,4 +30,17 @@ public void Generate (IReadOnlyList peers, string outputPath, stri var emitter = new TypeMapAssemblyEmitter (_systemRuntimeVersion); emitter.Emit (model, outputPath); } + + /// + /// Generates a TypeMap PE assembly and writes it to . + /// + /// Scanned Java peer types. + /// Stream to write the output PE assembly to. + /// Assembly name for the generated assembly. + public void Generate (IReadOnlyList peers, Stream output, string assemblyName) + { + var model = ModelBuilder.Build (peers, assemblyName + ".dll", assemblyName); + var emitter = new TypeMapAssemblyEmitter (_systemRuntimeVersion); + emitter.Emit (model, output); + } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs index 21956fd019c..07e44ff6f05 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs @@ -97,10 +97,10 @@ private protected static JavaPeerInfo MakeAcwPeer (string jniName, string manage } private protected static JavaPeerInfo MakeInterfacePeer ( - string jniName = "android/view/View$OnClickListener", - string managedName = "Android.Views.View+IOnClickListener", - string asmName = "Mono.Android", - string invokerName = "Android.Views.View+IOnClickListenerInvoker") + string jniName, + string managedName, + string asmName, + string invokerName) { var (ns, shortName) = ParseManagedTypeName (managedName); return new JavaPeerInfo { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs index b6dfe06c6b2..805f24174a3 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs @@ -8,26 +8,23 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; -public class RootTypeMapAssemblyGeneratorTests : FixtureTestBase, IDisposable +public class RootTypeMapAssemblyGeneratorTests : FixtureTestBase { - readonly string _outputDir = CreateTempDir (); - public void Dispose () => DeleteTempDir (_outputDir); - string GenerateRootAssembly (IReadOnlyList perAssemblyNames, string? assemblyName = null) + MemoryStream GenerateRootAssembly (IReadOnlyList perAssemblyNames, string? assemblyName = null) { - var outputPath = Path.Combine (_outputDir, - (assemblyName ?? "_Microsoft.Android.TypeMaps") + ".dll"); + var stream = new MemoryStream (); var generator = new RootTypeMapAssemblyGenerator (new Version (11, 0, 0, 0)); - generator.Generate (perAssemblyNames, outputPath, assemblyName); - return outputPath; + generator.Generate (perAssemblyNames, stream, assemblyName); + stream.Position = 0; + return stream; } [Fact] public void Generate_ProducesValidPEAssembly () { - var path = GenerateRootAssembly (new [] { "_App.TypeMap", "_Mono.Android.TypeMap" }); - Assert.True (File.Exists (path)); - using var pe = new PEReader (File.OpenRead (path)); + using var stream = GenerateRootAssembly (new [] { "_App.TypeMap", "_Mono.Android.TypeMap" }); + using var pe = new PEReader (stream); Assert.True (pe.HasMetadata); } @@ -36,8 +33,8 @@ public void Generate_ProducesValidPEAssembly () [InlineData ("MyRoot", "MyRoot")] public void Generate_AssemblyName_MatchesExpected (string? assemblyName, string expectedName) { - var path = GenerateRootAssembly (Array.Empty (), assemblyName); - using var pe = new PEReader (File.OpenRead (path)); + using var stream = GenerateRootAssembly (Array.Empty (), assemblyName); + using var pe = new PEReader (stream); var reader = pe.GetMetadataReader (); var asmDef = reader.GetAssemblyDefinition (); Assert.Equal (expectedName, reader.GetString (asmDef.Name)); @@ -46,8 +43,8 @@ public void Generate_AssemblyName_MatchesExpected (string? assemblyName, string [Fact] public void Generate_ReferencesGenericTypeMapAssemblyTargetAttribute () { - var path = GenerateRootAssembly (new [] { "_App.TypeMap" }); - using var pe = new PEReader (File.OpenRead (path)); + using var stream = GenerateRootAssembly (new [] { "_App.TypeMap" }); + using var pe = new PEReader (stream); var reader = pe.GetMetadataReader (); var typeRefs = reader.TypeReferences @@ -71,8 +68,8 @@ public void Generate_ReferencesGenericTypeMapAssemblyTargetAttribute () [Fact] public void Generate_EmptyList_ProducesValidAssemblyWithNoTargetAttributes () { - var path = GenerateRootAssembly (Array.Empty ()); - using var pe = new PEReader (File.OpenRead (path)); + using var stream = GenerateRootAssembly (Array.Empty ()); + using var pe = new PEReader (stream); var reader = pe.GetMetadataReader (); var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); Assert.Empty (asmAttrs); @@ -82,8 +79,8 @@ public void Generate_EmptyList_ProducesValidAssemblyWithNoTargetAttributes () public void Generate_MultipleTargets_HasCorrectAttributeCount () { var targets = new [] { "_App.TypeMap", "_Mono.Android.TypeMap", "_Java.Interop.TypeMap" }; - var path = GenerateRootAssembly (targets); - using var pe = new PEReader (File.OpenRead (path)); + using var stream = GenerateRootAssembly (targets); + using var pe = new PEReader (stream); var reader = pe.GetMetadataReader (); var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); Assert.Equal (3, asmAttrs.Count ()); @@ -93,8 +90,8 @@ public void Generate_MultipleTargets_HasCorrectAttributeCount () public void Generate_AttributeBlobValues_MatchTargetNames () { var targets = new [] { "_App.TypeMap", "_Mono.Android.TypeMap" }; - var path = GenerateRootAssembly (targets); - using var pe = new PEReader (File.OpenRead (path)); + using var stream = GenerateRootAssembly (targets); + using var pe = new PEReader (stream); var reader = pe.GetMetadataReader (); var attrValues = new List (); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index a66a17c7ab2..c5165f18dbc 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -12,50 +12,47 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; public class TypeMapAssemblyGeneratorTests : FixtureTestBase { - static string GenerateAssembly (IReadOnlyList peers, string outputDir, string? assemblyName = null) + static MemoryStream GenerateAssembly (IReadOnlyList peers, string? assemblyName = null) { - var outputPath = Path.Combine (outputDir, (assemblyName ?? "TestTypeMap") + ".dll"); + var stream = new MemoryStream (); var generator = new TypeMapAssemblyGenerator (new Version (11, 0, 0, 0)); - generator.Generate (peers, outputPath, assemblyName); - return outputPath; + generator.Generate (peers, stream, assemblyName ?? "TestTypeMap"); + stream.Position = 0; + return stream; } - static (PEReader pe, MetadataReader reader) OpenAssembly (string path) + static (PEReader pe, MetadataReader reader) OpenAssembly (Stream stream) { - var pe = new PEReader (File.OpenRead (path)); + var pe = new PEReader (stream); return (pe, pe.GetMetadataReader ()); } - public class BasicAssemblyStructure : IDisposable + public class BasicAssemblyStructure : FixtureTestBase { - readonly string _outputDir = CreateTempDir (); - public void Dispose () => DeleteTempDir (_outputDir); [Fact] public void Generate_ProducesValidPEAssembly () { var peers = ScanFixtures (); - var path = GenerateAssembly (peers, _outputDir); - Assert.True (File.Exists (path)); - using var pe = new PEReader (File.OpenRead (path)); - Assert.True (pe.HasMetadata); - var reader = pe.GetMetadataReader (); - Assert.NotNull (reader); + using var stream = GenerateAssembly (peers); + var (pe, reader) = OpenAssembly (stream); + using (pe) { + Assert.True (pe.HasMetadata); + Assert.NotNull (reader); + } } } - public class AssemblyReference : IDisposable + public class AssemblyReference : FixtureTestBase { - readonly string _outputDir = CreateTempDir (); - public void Dispose () => DeleteTempDir (_outputDir); [Fact] public void Generate_HasRequiredAssemblyReferences () { var peers = ScanFixtures (); - var path = GenerateAssembly (peers, _outputDir); - var (pe, reader) = OpenAssembly (path); + using var stream = GenerateAssembly (peers); + var (pe, reader) = OpenAssembly (stream); using (pe) { var asmRefs = reader.AssemblyReferences .Select (h => reader.GetString (reader.GetAssemblyReference (h).Name)) @@ -69,17 +66,15 @@ public void Generate_HasRequiredAssemblyReferences () } - public class ProxyType : IDisposable + public class ProxyType : FixtureTestBase { - readonly string _outputDir = CreateTempDir (); - public void Dispose () => DeleteTempDir (_outputDir); [Fact] public void Generate_CreatesProxyTypes () { var peers = ScanFixtures (); - var path = GenerateAssembly (peers, _outputDir); - var (pe, reader) = OpenAssembly (path); + using var stream = GenerateAssembly (peers); + var (pe, reader) = OpenAssembly (stream); using (pe) { var proxyTypes = reader.TypeDefinitions .Select (h => reader.GetTypeDefinition (h)) @@ -95,8 +90,8 @@ public void Generate_CreatesProxyTypes () public void Generate_ProxyType_HasCtorAndCreateInstance () { var peers = ScanFixtures (); - var path = GenerateAssembly (peers, _outputDir); - var (pe, reader) = OpenAssembly (path); + using var stream = GenerateAssembly (peers); + var (pe, reader) = OpenAssembly (stream); using (pe) { var objectProxy = reader.TypeDefinitions .Select (h => reader.GetTypeDefinition (h)) @@ -115,18 +110,16 @@ public void Generate_ProxyType_HasCtorAndCreateInstance () } - public class AcwProxy : IDisposable + public class AcwProxy : FixtureTestBase { - 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 var stream = GenerateAssembly (new [] { acwPeer }, "AcwTest"); + var (pe, reader) = OpenAssembly (stream); using (pe) { var proxy = reader.TypeDefinitions .Select (h => reader.GetTypeDefinition (h)) @@ -147,8 +140,8 @@ 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 var stream = GenerateAssembly (new [] { acwPeer }, "UcoTest"); + var (pe, reader) = OpenAssembly (stream); using (pe) { var proxy = reader.TypeDefinitions .Select (h => reader.GetTypeDefinition (h)) @@ -173,17 +166,15 @@ public void Generate_AcwProxy_HasUnmanagedCallersOnlyAttribute () } - public class IgnoresAccessChecksTo : IDisposable + public class IgnoresAccessChecksTo : FixtureTestBase { - readonly string _outputDir = CreateTempDir (); - public void Dispose () => DeleteTempDir (_outputDir); [Fact] public void Generate_HasIgnoresAccessChecksToAttribute () { var peers = ScanFixtures (); - var path = GenerateAssembly (peers, _outputDir); - var (pe, reader) = OpenAssembly (path); + using var stream = GenerateAssembly (peers); + var (pe, reader) = OpenAssembly (stream); using (pe) { var types = reader.TypeDefinitions .Select (h => reader.GetTypeDefinition (h)) @@ -196,10 +187,8 @@ public void Generate_HasIgnoresAccessChecksToAttribute () } - public class Alias : IDisposable + public class Alias : FixtureTestBase { - readonly string _outputDir = CreateTempDir (); - public void Dispose () => DeleteTempDir (_outputDir); static List MakeDuplicateAliasPeers () => new List { new JavaPeerInfo { @@ -229,8 +218,8 @@ public class Alias : IDisposable public void Generate_DuplicateJniNames_CreatesAliasEntries () { var peers = MakeDuplicateAliasPeers (); - var path = GenerateAssembly (peers, _outputDir, "AliasTest"); - var (pe, reader) = OpenAssembly (path); + using var stream = GenerateAssembly (peers, "AliasTest"); + var (pe, reader) = OpenAssembly (stream); using (pe) { var assemblyAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); Assert.True (assemblyAttrs.Count () >= 3); @@ -241,8 +230,8 @@ public void Generate_DuplicateJniNames_CreatesAliasEntries () public void Generate_DuplicateJniNames_EmitsTypeMapAssociationAttribute () { var peers = MakeDuplicateAliasPeers (); - var path = GenerateAssembly (peers, _outputDir, "AliasAssocTest"); - var (pe, reader) = OpenAssembly (path); + using var stream = GenerateAssembly (peers, "AliasAssocTest"); + var (pe, reader) = OpenAssembly (stream); using (pe) { var memberRefs = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i))) @@ -256,17 +245,14 @@ public void Generate_DuplicateJniNames_EmitsTypeMapAssociationAttribute () } - public class EmptyInput : IDisposable + public class EmptyInput : FixtureTestBase { - readonly string _outputDir = CreateTempDir (); - public void Dispose () => DeleteTempDir (_outputDir); [Fact] public void Generate_EmptyPeerList_ProducesValidAssembly () { - var path = GenerateAssembly (Array.Empty (), _outputDir, "EmptyTest"); - Assert.True (File.Exists (path)); - var (pe, reader) = OpenAssembly (path); + using var stream = GenerateAssembly (Array.Empty (), "EmptyTest"); + var (pe, reader) = OpenAssembly (stream); using (pe) { Assert.NotNull (reader); var asmDef = reader.GetAssemblyDefinition (); @@ -345,10 +331,8 @@ public void ParseParameterTypes_UnterminatedSignature_ReturnsEmptyList () } - public class CreateInstancePaths : IDisposable + public class CreateInstancePaths : FixtureTestBase { - readonly string _outputDir = CreateTempDir (); - public void Dispose () => DeleteTempDir (_outputDir); [Fact] public void Generate_SimpleActivity_UsesGetUninitializedObject () @@ -358,8 +342,8 @@ public void Generate_SimpleActivity_UsesGetUninitializedObject () Assert.NotNull (simpleActivity.ActivationCtor); Assert.NotEqual (simpleActivity.ManagedTypeName, simpleActivity.ActivationCtor.DeclaringTypeName); - var path = GenerateAssembly (new [] { simpleActivity }, _outputDir, "InheritedCtorTest"); - var (pe, reader) = OpenAssembly (path); + using var stream = GenerateAssembly (new [] { simpleActivity }, "InheritedCtorTest"); + var (pe, reader) = OpenAssembly (stream); using (pe) { var typeNames = GetTypeRefNames (reader); Assert.Contains ("RuntimeHelpers", typeNames); @@ -379,8 +363,8 @@ public void Generate_LeafCtor_DoesNotUseCreateManagedPeer () Assert.NotNull (clickableView.ActivationCtor); Assert.Equal (clickableView.ManagedTypeName, clickableView.ActivationCtor.DeclaringTypeName); - var path = GenerateAssembly (new [] { clickableView }, _outputDir, "LeafCtorTest"); - var (pe, reader) = OpenAssembly (path); + using var stream = GenerateAssembly (new [] { clickableView }, "LeafCtorTest"); + var (pe, reader) = OpenAssembly (stream); using (pe) { var memberNames = GetMemberRefNames (reader); Assert.DoesNotContain ("CreateManagedPeer", memberNames); @@ -400,8 +384,8 @@ public void Generate_GenericType_ThrowsNotSupportedException () var generic = peers.First (p => p.JavaName == "my/app/GenericHolder"); Assert.True (generic.IsGenericDefinition); - var path = GenerateAssembly (new [] { generic }, _outputDir, "GenericTest"); - var (pe, reader) = OpenAssembly (path); + using var stream = GenerateAssembly (new [] { generic }, "GenericTest"); + var (pe, reader) = OpenAssembly (stream); using (pe) { var typeNames = GetTypeRefNames (reader); Assert.Contains ("NotSupportedException", typeNames); @@ -410,10 +394,8 @@ public void Generate_GenericType_ThrowsNotSupportedException () } - public class IgnoresAccessChecksToForBaseCtor : IDisposable + public class IgnoresAccessChecksToForBaseCtor : FixtureTestBase { - readonly string _outputDir = CreateTempDir (); - public void Dispose () => DeleteTempDir (_outputDir); [Fact] public void Generate_InheritedCtor_IncludesBaseCtorAssembly () @@ -424,8 +406,8 @@ public void Generate_InheritedCtor_IncludesBaseCtorAssembly () var peers = ScanFixtures (); var simpleActivity = peers.First (p => p.JavaName == "my/app/SimpleActivity"); - var path = GenerateAssembly (new [] { simpleActivity }, _outputDir, "IgnoresAccessTest"); - var (pe, reader) = OpenAssembly (path); + using var stream = GenerateAssembly (new [] { simpleActivity }, "IgnoresAccessTest"); + var (pe, reader) = OpenAssembly (stream); using (pe) { var ignoresAttrType = reader.TypeDefinitions .Select (h => reader.GetTypeDefinition (h)) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 7c7a757fa4c..230803e4d16 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -223,7 +223,7 @@ public void Build_PeerWithActivationCtor_CreatesProxy () [Fact] public void Build_PeerWithInvoker_CreatesProxy () { - var peer = MakeInterfacePeer (); + var peer = MakeInterfacePeer ("android/view/View$OnClickListener", "Android.Views.View+IOnClickListener", "Mono.Android", "Android.Views.View+IOnClickListenerInvoker"); var model = BuildModel (new [] { peer }); Assert.Single (model.ProxyTypes); From b0b0fcde0fe3affad1c35cfc1c5270d242410221 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 11 Mar 2026 19:21:08 +0100 Subject: [PATCH 03/21] Simplify: fix constructor callbacks, inline helpers, extract methods - Fix NativeCallbackName bug: constructors now produce 'n_ctor' instead of invalid 'n_.ctor' (derived from managed method name '.ctor') - Merge ParseParameterTypeStrings + ParseJniParameters into single JniSignatureHelper.ParseParameters returning List - Extract EmitCreateInstance into 5 named methods (EmitNullCreateInstance, EmitGenericCreateInstance, EmitInvokerCreateInstance, etc.) - Inline MakeMarshalMethod helper: replace with direct MarshalMethodInfo initializers at all 16 call sites (reviewer feedback) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/JniSignatureHelper.cs | 10 +- .../Generator/TypeMapAssemblyEmitter.cs | 127 +++++++++----- .../Scanner/JavaPeerScanner.cs | 26 ++- .../Generator/FixtureTestBase.cs | 12 -- .../Generator/TypeMapModelBuilderTests.cs | 161 ++++++++++++++++-- 5 files changed, 239 insertions(+), 97 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs index 81592f6166e..5c54572a5b9 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs @@ -41,18 +41,16 @@ public static List ParseParameterTypes (string jniSignature) } /// - - /// Parses the raw JNI type descriptor strings from a JNI method signature. - + /// Parses JNI parameter type descriptors into JniParameterInfo records. /// - public static List ParseParameterTypeStrings (string jniSignature) + public static List ParseParameters (string jniSignature) { - var result = new List (); + var result = new List (); int i = 1; // skip opening '(' while (i < jniSignature.Length && jniSignature [i] != ')') { int start = i; ParseSingleType (jniSignature, ref i); - result.Add (jniSignature.Substring (start, i - start)); + result.Add (new JniParameterInfo { JniType = jniSignature.Substring (start, i - start) }); } return result; } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 2bb68233310..2f53da66d3c 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -350,34 +350,17 @@ void EmitProxyType (JavaPeerProxyData proxy, Dictionary { - encoder.OpCode (ILOpCode.Ldnull); - encoder.OpCode (ILOpCode.Ret); - }); + EmitNullCreateInstance (); return; } - // Generic type definitions cannot be instantiated if (proxy.IsGenericDefinition) { - EmitCreateInstanceBody (encoder => { - encoder.LoadString (_pe.Metadata.GetOrAddUserString ("Cannot create instance of open generic type.")); - encoder.OpCode (ILOpCode.Newobj); - encoder.Token (_notSupportedExceptionCtorRef); - encoder.OpCode (ILOpCode.Throw); - }); + EmitGenericCreateInstance (); return; } - // Interface with invoker: new TInvoker(IntPtr, JniHandleOwnership) if (proxy.InvokerType != null) { - var invokerCtorRef = AddActivationCtorRef (_pe.ResolveTypeRef (proxy.InvokerType), ActivationCtorStyle.XamarinAndroid); - EmitCreateInstanceBody (encoder => { - encoder.OpCode (ILOpCode.Ldarg_1); - encoder.OpCode (ILOpCode.Ldarg_2); - encoder.OpCode (ILOpCode.Newobj); - encoder.Token (invokerCtorRef); - encoder.OpCode (ILOpCode.Ret); - }); + EmitInvokerCreateInstance (proxy.InvokerType); return; } @@ -386,34 +369,88 @@ void EmitCreateInstance (JavaPeerProxyData proxy) var targetTypeRef = _pe.ResolveTypeRef (proxy.TargetType); if (activationCtor.IsOnLeafType) { - // Leaf type has its own ctor: new T(IntPtr, JniHandleOwnership) or new T(ref JniObjectReference, JniObjectReferenceOptions) - var ctorRef = AddActivationCtorRef (targetTypeRef, activationCtor.Style); - EmitCreateInstanceBody (encoder => { - encoder.OpCode (ILOpCode.Ldarg_1); - encoder.OpCode (ILOpCode.Ldarg_2); - encoder.OpCode (ILOpCode.Newobj); - encoder.Token (ctorRef); - encoder.OpCode (ILOpCode.Ret); - }); + EmitLeafCreateInstance (targetTypeRef, activationCtor.Style); } else { - // Inherited ctor: GetUninitializedObject(typeof(T)) + call Base::.ctor(IntPtr, JniHandleOwnership) or (ref JniObjectReference, JniObjectReferenceOptions) - var baseActivationCtorRef = AddActivationCtorRef (_pe.ResolveTypeRef (activationCtor.DeclaringType), activationCtor.Style); - EmitCreateInstanceBody (encoder => { - encoder.OpCode (ILOpCode.Ldtoken); - encoder.Token (targetTypeRef); - encoder.Call (_getTypeFromHandleRef); - encoder.Call (_getUninitializedObjectRef); - encoder.OpCode (ILOpCode.Castclass); - encoder.Token (targetTypeRef); + EmitInheritedCreateInstance (targetTypeRef, activationCtor); + } + } - encoder.OpCode (ILOpCode.Dup); - encoder.OpCode (ILOpCode.Ldarg_1); - encoder.OpCode (ILOpCode.Ldarg_2); - encoder.Call (baseActivationCtorRef); + /// + /// Emits a CreateInstance body that returns null (no activation possible). + /// + void EmitNullCreateInstance () + { + EmitCreateInstanceBody (encoder => { + encoder.OpCode (ILOpCode.Ldnull); + encoder.OpCode (ILOpCode.Ret); + }); + } - encoder.OpCode (ILOpCode.Ret); - }); - } + /// + /// Emits a CreateInstance body that throws NotSupportedException for open generic types. + /// + void EmitGenericCreateInstance () + { + EmitCreateInstanceBody (encoder => { + encoder.LoadString (_pe.Metadata.GetOrAddUserString ("Cannot create instance of open generic type.")); + encoder.OpCode (ILOpCode.Newobj); + encoder.Token (_notSupportedExceptionCtorRef); + encoder.OpCode (ILOpCode.Throw); + }); + } + + /// + /// Emits a CreateInstance body that instantiates the interface invoker type: new TInvoker(IntPtr, JniHandleOwnership). + /// + void EmitInvokerCreateInstance (TypeRefData invokerType) + { + var invokerCtorRef = AddActivationCtorRef (_pe.ResolveTypeRef (invokerType), ActivationCtorStyle.XamarinAndroid); + EmitCreateInstanceBody (encoder => { + encoder.OpCode (ILOpCode.Ldarg_1); + encoder.OpCode (ILOpCode.Ldarg_2); + encoder.OpCode (ILOpCode.Newobj); + encoder.Token (invokerCtorRef); + encoder.OpCode (ILOpCode.Ret); + }); + } + + /// + /// Emits a CreateInstance body for a leaf type that has its own activation ctor: new T(IntPtr, JniHandleOwnership). + /// + void EmitLeafCreateInstance (EntityHandle targetTypeRef, ActivationCtorStyle style) + { + var ctorRef = AddActivationCtorRef (targetTypeRef, style); + EmitCreateInstanceBody (encoder => { + encoder.OpCode (ILOpCode.Ldarg_1); + encoder.OpCode (ILOpCode.Ldarg_2); + encoder.OpCode (ILOpCode.Newobj); + encoder.Token (ctorRef); + encoder.OpCode (ILOpCode.Ret); + }); + } + + /// + /// Emits a CreateInstance body for types that inherit activation from a base type: + /// GetUninitializedObject(typeof(T)) + call Base::.ctor(IntPtr, JniHandleOwnership). + /// + void EmitInheritedCreateInstance (EntityHandle targetTypeRef, ActivationCtorData activationCtor) + { + var baseActivationCtorRef = AddActivationCtorRef (_pe.ResolveTypeRef (activationCtor.DeclaringType), activationCtor.Style); + EmitCreateInstanceBody (encoder => { + encoder.OpCode (ILOpCode.Ldtoken); + encoder.Token (targetTypeRef); + encoder.Call (_getTypeFromHandleRef); + encoder.Call (_getUninitializedObjectRef); + encoder.OpCode (ILOpCode.Castclass); + encoder.Token (targetTypeRef); + + encoder.OpCode (ILOpCode.Dup); + encoder.OpCode (ILOpCode.Ldarg_1); + encoder.OpCode (ILOpCode.Ldarg_2); + encoder.Call (baseActivationCtorRef); + + encoder.OpCode (ILOpCode.Ret); + }); } void EmitCreateInstanceBody (Action emitIL) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index eabc73f7ee9..51f661e2218 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -277,15 +277,19 @@ 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), - NativeCallbackName = $"n_{index.Reader.GetString (methodDef.Name)}", - JniReturnType = JniSignatureHelper.ParseReturnTypeString (registerInfo.Signature ?? "()V"), - Parameters = ParseJniParameters (registerInfo.Signature ?? "()V"), - IsConstructor = registerInfo.JniName == "" || registerInfo.JniName == ".ctor", + ManagedMethodName = managedName, + NativeCallbackName = isConstructor ? "n_ctor" : $"n_{managedName}", + JniReturnType = JniSignatureHelper.ParseReturnTypeString (jniSignature), + Parameters = JniSignatureHelper.ParseParameters (jniSignature), + IsConstructor = isConstructor, ThrownNames = exportInfo?.ThrownNames, SuperArgumentsString = exportInfo?.SuperArgumentsString, }); @@ -753,16 +757,6 @@ static string ExtractShortName (string fullName) return lastPlus >= 0 ? typePart.Substring (lastPlus + 1) : typePart; } - static List ParseJniParameters (string jniSignature) - { - var typeStrings = JniSignatureHelper.ParseParameterTypeStrings (jniSignature); - var result = new List (typeStrings.Count); - foreach (var t in typeStrings) { - result.Add (new JniParameterInfo { JniType = t }); - } - return result; - } - static List BuildJavaConstructors (List marshalMethods) { var ctors = new List (); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs index 07e44ff6f05..56f9a92dc85 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs @@ -115,18 +115,6 @@ private protected static JavaPeerInfo MakeInterfacePeer ( }; } - private protected static MarshalMethodInfo MakeMarshalMethod (string jniName, string callbackName, string jniSig, bool isConstructor = false) - { - return new MarshalMethodInfo { - JniName = jniName, - NativeCallbackName = callbackName, - JniSignature = jniSig, - ManagedMethodName = isConstructor ? ".ctor" : callbackName.StartsWith ("n_") ? callbackName.Substring (2) : callbackName, - JniReturnType = jniSig.Contains (')') ? jniSig.Substring (jniSig.IndexOf (')') + 1) : "V", - IsConstructor = isConstructor, - }; - } - private protected static List GetTypeRefNames (MetadataReader reader) => reader.TypeReferences .Select (h => reader.GetTypeReference (h)) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 230803e4d16..3f4d69ad915 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -50,7 +50,14 @@ public void Build_ComputesIgnoresAccessChecksToFromCrossAssemblyCallbacks () { var peer = MakeAcwPeer ("my/app/MainActivity", "MyApp.MainActivity", "MyApp") with { MarshalMethods = new List { - MakeMarshalMethod ("", "n_ctor", "()V", isConstructor: true), + new MarshalMethodInfo { + JniName = "", + NativeCallbackName = "n_ctor", + JniSignature = "()V", + ManagedMethodName = ".ctor", + JniReturnType = "V", + IsConstructor = true, + }, new MarshalMethodInfo { JniName = "onCreate", NativeCallbackName = "n_OnCreate", @@ -280,7 +287,14 @@ public void Build_InterfaceWithMarshalMethods_IsNotAcw () IsInterface = true, InvokerTypeName = "Android.Views.View+IOnClickListenerInvoker", MarshalMethods = new List { - MakeMarshalMethod ("onClick", "n_OnClick", "(Landroid/view/View;)V"), + new MarshalMethodInfo { + JniName = "onClick", + NativeCallbackName = "n_OnClick", + JniSignature = "(Landroid/view/View;)V", + ManagedMethodName = "OnClick", + JniReturnType = "V", + IsConstructor = false, + }, }, }; @@ -296,7 +310,14 @@ public void Build_DoNotGenerateAcw_IsNotAcw () var peer = MakePeerWithActivation ("java/lang/Object", "Java.Lang.Object", "Mono.Android") with { DoNotGenerateAcw = true, MarshalMethods = new List { - MakeMarshalMethod ("toString", "n_ToString", "()Ljava/lang/String;"), + new MarshalMethodInfo { + JniName = "toString", + NativeCallbackName = "n_ToString", + JniSignature = "()Ljava/lang/String;", + ManagedMethodName = "ToString", + JniReturnType = "Ljava/lang/String;", + IsConstructor = false, + }, }, }; @@ -315,9 +336,30 @@ public void Build_AcwWithMarshalMethods_CreatesUcoMethods () { var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App") with { MarshalMethods = new List { - MakeMarshalMethod ("", "n_ctor", "()V", isConstructor: true), - MakeMarshalMethod ("onCreate", "n_OnCreate", "(Landroid/os/Bundle;)V"), - MakeMarshalMethod ("onResume", "n_OnResume", "()V"), + new MarshalMethodInfo { + JniName = "", + NativeCallbackName = "n_ctor", + JniSignature = "()V", + ManagedMethodName = ".ctor", + JniReturnType = "V", + IsConstructor = true, + }, + new MarshalMethodInfo { + JniName = "onCreate", + NativeCallbackName = "n_OnCreate", + JniSignature = "(Landroid/os/Bundle;)V", + ManagedMethodName = "OnCreate", + JniReturnType = "V", + IsConstructor = false, + }, + new MarshalMethodInfo { + JniName = "onResume", + NativeCallbackName = "n_OnResume", + JniSignature = "()V", + ManagedMethodName = "OnResume", + JniReturnType = "V", + IsConstructor = false, + }, }, }; @@ -336,14 +378,27 @@ public void Build_AcwWithMarshalMethods_CreatesUcoMethods () [Fact] public void Build_UcoMethod_CallbackTypeIsDeclaringType () { - var mm = MakeMarshalMethod ("toString", "n_ToString", "()Ljava/lang/String;") with { + var mm = new MarshalMethodInfo { + JniName = "toString", + NativeCallbackName = "n_ToString", + JniSignature = "()Ljava/lang/String;", + ManagedMethodName = "ToString", + JniReturnType = "Ljava/lang/String;", + IsConstructor = false, DeclaringTypeName = "Java.Lang.Object", DeclaringAssemblyName = "Mono.Android", }; var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App") with { MarshalMethods = new List { - MakeMarshalMethod ("", "n_ctor", "()V", isConstructor: true), + new MarshalMethodInfo { + JniName = "", + NativeCallbackName = "n_ctor", + JniSignature = "()V", + ManagedMethodName = ".ctor", + JniReturnType = "V", + IsConstructor = true, + }, mm, }, }; @@ -359,8 +414,22 @@ public void Build_UcoMethod_FallsBackToPeerType_WhenDeclaringTypeEmpty () { var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App") with { MarshalMethods = new List { - MakeMarshalMethod ("", "n_ctor", "()V", isConstructor: true), - MakeMarshalMethod ("onPause", "n_OnPause", "()V"), + new MarshalMethodInfo { + JniName = "", + NativeCallbackName = "n_ctor", + JniSignature = "()V", + ManagedMethodName = ".ctor", + JniReturnType = "V", + IsConstructor = true, + }, + new MarshalMethodInfo { + JniName = "onPause", + NativeCallbackName = "n_OnPause", + JniSignature = "()V", + ManagedMethodName = "OnPause", + JniReturnType = "V", + IsConstructor = false, + }, }, }; @@ -375,9 +444,30 @@ public void Build_ConstructorsInMarshalMethods_SkippedFromUcoMethods () { var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App") with { MarshalMethods = new List { - MakeMarshalMethod ("", "n_ctor", "()V", isConstructor: true), - MakeMarshalMethod ("", "n_ctor2", "()V", isConstructor: true), - MakeMarshalMethod ("onStart", "n_OnStart", "()V"), + new MarshalMethodInfo { + JniName = "", + NativeCallbackName = "n_ctor", + JniSignature = "()V", + ManagedMethodName = ".ctor", + JniReturnType = "V", + IsConstructor = true, + }, + new MarshalMethodInfo { + JniName = "", + NativeCallbackName = "n_ctor2", + JniSignature = "()V", + ManagedMethodName = ".ctor", + JniReturnType = "V", + IsConstructor = true, + }, + new MarshalMethodInfo { + JniName = "onStart", + NativeCallbackName = "n_OnStart", + JniSignature = "()V", + ManagedMethodName = "OnStart", + JniReturnType = "V", + IsConstructor = false, + }, }, }; @@ -420,7 +510,14 @@ public void Build_PeerWithoutActivationCtor_NoUcoConstructors () AssemblyName = "App", InvokerTypeName = "MyApp.FooInvoker", // has invoker → will create proxy MarshalMethods = new List { - MakeMarshalMethod ("bar", "n_Bar", "()V"), + new MarshalMethodInfo { + JniName = "bar", + NativeCallbackName = "n_Bar", + JniSignature = "()V", + ManagedMethodName = "Bar", + JniReturnType = "V", + IsConstructor = false, + }, }, JavaConstructors = new List { new JavaConstructorInfo { ConstructorIndex = 0, JniSignature = "()V" }, @@ -443,8 +540,22 @@ public void Build_NativeRegistrations_MatchUcoMethods () { var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App") with { MarshalMethods = new List { - MakeMarshalMethod ("", "n_ctor", "()V", isConstructor: true), - MakeMarshalMethod ("onCreate", "n_OnCreate", "(Landroid/os/Bundle;)V"), + new MarshalMethodInfo { + JniName = "", + NativeCallbackName = "n_ctor", + JniSignature = "()V", + ManagedMethodName = ".ctor", + JniReturnType = "V", + IsConstructor = true, + }, + new MarshalMethodInfo { + JniName = "onCreate", + NativeCallbackName = "n_OnCreate", + JniSignature = "(Landroid/os/Bundle;)V", + ManagedMethodName = "OnCreate", + JniReturnType = "V", + IsConstructor = false, + }, }, }; @@ -478,8 +589,22 @@ public void Build_NativeRegistrations_ParameterizedConstructor_HasCorrectJniSign }, }, MarshalMethods = new List { - MakeMarshalMethod ("", "n_ctor", "()V", isConstructor: true), - MakeMarshalMethod ("", "n_ctor", "(Landroid/content/Context;)V", isConstructor: true), + new MarshalMethodInfo { + JniName = "", + NativeCallbackName = "n_ctor", + JniSignature = "()V", + ManagedMethodName = ".ctor", + JniReturnType = "V", + IsConstructor = true, + }, + new MarshalMethodInfo { + JniName = "", + NativeCallbackName = "n_ctor", + JniSignature = "(Landroid/content/Context;)V", + ManagedMethodName = ".ctor", + JniReturnType = "V", + IsConstructor = true, + }, }, }; From b6a5dc70f7355ed5b396731a689f0c7d221757ba Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 11 Mar 2026 19:56:49 +0100 Subject: [PATCH 04/21] Consolidate JniSignatureHelper Facts into Theories Merge 6 individual [Fact] tests into 2 [Theory] tests with [InlineData]: - ParseParameterTypes_SingleParam_MapsToCorrectKind (2 cases) - ParseReturnType_MapsToCorrectKind (4 cases) Uses int casts in [InlineData] to work around JniParamKind being internal. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TypeMapAssemblyGeneratorTests.cs | 34 +++++++------------ 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index c5165f18dbc..b6a1c7bed10 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -278,34 +278,26 @@ public void ParseParameterTypes_ParsesCorrectCount (string signature, int expect Assert.Equal (expectedCount, actual.Count); } - [Fact] - public void ParseParameterTypes_Boolean_MapsToCorrectKind () + [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 ("(Z)V"); + var types = JniSignatureHelper.ParseParameterTypes (signature); Assert.Single (types); - Assert.Equal (JniParamKind.Boolean, types [0]); + Assert.Equal ((JniParamKind) expectedKind, types [0]); } - [Fact] - public void ParseParameterTypes_Object_MapsToCorrectKind () + [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) { - var types = JniSignatureHelper.ParseParameterTypes ("(Ljava/lang/String;)V"); - Assert.Single (types); - Assert.Equal (JniParamKind.Object, types [0]); + Assert.Equal ((JniParamKind) expectedKind, JniSignatureHelper.ParseReturnType (signature)); } - [Fact] - public void ParseReturnType_Void () => Assert.Equal (JniParamKind.Void, JniSignatureHelper.ParseReturnType ("()V")); - - [Fact] - public void ParseReturnType_Int () => Assert.Equal (JniParamKind.Int, JniSignatureHelper.ParseReturnType ("()I")); - - [Fact] - public void ParseReturnType_Boolean () => Assert.Equal (JniParamKind.Boolean, JniSignatureHelper.ParseReturnType ("()Z")); - - [Fact] - public void ParseReturnType_Object () => Assert.Equal (JniParamKind.Object, JniSignatureHelper.ParseReturnType ("()Ljava/lang/String;")); - } public class NegativeEdgeCase From 88a40e826c8c7de16b96d9e23df9080d3ba1a58c Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 11 Mar 2026 20:13:16 +0100 Subject: [PATCH 05/21] Address android-reviewer findings - Remove null-forgiving operator on TargetTypeReference; use explicit null check with throw instead - Validate IndexOf results in JniSignatureHelper: throw ArgumentException for malformed signatures missing ')' or ';' instead of silently producing incorrect results - Replace Array.Empty() with [] per repo convention - Add #nullable enable to new files (JcwJavaSourceGenerator, JniSignatureHelper, TypeMapAssemblyData) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/JcwJavaSourceGenerator.cs | 1 + .../Generator/JniSignatureHelper.cs | 17 +++++++++++++---- .../Generator/Model/TypeMapAssemblyData.cs | 1 + .../Generator/TypeMapAssemblyEmitter.cs | 4 +++- .../Scanner/JavaPeerInfo.cs | 6 +++--- .../RootTypeMapAssemblyGeneratorTests.cs | 4 ++-- .../Generator/TypeMapAssemblyGeneratorTests.cs | 2 +- .../Generator/TypeMapModelBuilderTests.cs | 6 +++--- 8 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs index 8a0d02b49ac..f7a3f24289c 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs @@ -1,3 +1,4 @@ +#nullable enable using System; using System.Collections.Generic; using System.IO; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs index 5c54572a5b9..5dea85fea5b 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs @@ -1,3 +1,4 @@ +#nullable enable using System; using System.Collections.Generic; using System.Reflection.Metadata; @@ -62,8 +63,10 @@ public static List ParseParameters (string jniSignature) /// public static string ParseReturnTypeString (string jniSignature) { - int i = jniSignature.IndexOf (')') + 1; - return jniSignature.Substring (i); + int paren = jniSignature.IndexOf (')'); + if (paren < 0) + throw new ArgumentException ($"Malformed JNI signature '{jniSignature}': missing ')'"); + return jniSignature.Substring (paren + 1); } /// @@ -73,7 +76,10 @@ public static string ParseReturnTypeString (string jniSignature) /// public static JniParamKind ParseReturnType (string jniSignature) { - int i = jniSignature.IndexOf (')') + 1; + int paren = jniSignature.IndexOf (')'); + if (paren < 0) + throw new ArgumentException ($"Malformed JNI signature '{jniSignature}': missing ')'"); + int i = paren + 1; return ParseSingleType (jniSignature, ref i); } @@ -90,7 +96,10 @@ static JniParamKind ParseSingleType (string sig, ref int i) case 'F': i++; return JniParamKind.Float; case 'D': i++; return JniParamKind.Double; case 'L': - i = sig.IndexOf (';', i) + 1; + 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++; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs index ecc97fd0d82..38112b3175f 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -1,3 +1,4 @@ +#nullable enable using System; using System.Collections.Generic; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 2f53da66d3c..4698f05d949 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -605,7 +605,9 @@ void EmitTypeMapAttribute (TypeMapAttributeData entry) b.WriteSerializedString (entry.JniName); b.WriteSerializedString (entry.ProxyTypeReference); if (!entry.IsUnconditional) { - b.WriteSerializedString (entry.TargetTypeReference!); + var targetRef = entry.TargetTypeReference + ?? throw new InvalidOperationException ("TargetTypeReference must be set for conditional entries"); + b.WriteSerializedString (targetRef); } }); _pe.Metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, ctorRef, blob); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index e8d0c5d6ba3..2105eaba6c4 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -84,7 +84,7 @@ sealed record JavaPeerInfo /// 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 (); + public IReadOnlyList JavaConstructors { get; init; } = []; /// /// Information about the activation constructor for this type. @@ -157,7 +157,7 @@ sealed record MarshalMethodInfo /// /// JNI parameter types for UCO generation. /// - public IReadOnlyList Parameters { get; init; } = Array.Empty (); + public IReadOnlyList Parameters { get; init; } = []; /// /// JNI return type descriptor, e.g., "V", "Landroid/os/Bundle;". @@ -217,7 +217,7 @@ sealed record JavaConstructorInfo /// JNI parameter types parsed from the signature. /// Used to generate the Java constructor parameter list. /// - public IReadOnlyList Parameters { get; init; } = Array.Empty (); + public IReadOnlyList Parameters { get; init; } = []; /// /// For [Export] constructors: super constructor arguments string. diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs index 805f24174a3..8bb4fa3d81e 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs @@ -33,7 +33,7 @@ public void Generate_ProducesValidPEAssembly () [InlineData ("MyRoot", "MyRoot")] public void Generate_AssemblyName_MatchesExpected (string? assemblyName, string expectedName) { - using var stream = GenerateRootAssembly (Array.Empty (), assemblyName); + using var stream = GenerateRootAssembly ([], assemblyName); using var pe = new PEReader (stream); var reader = pe.GetMetadataReader (); var asmDef = reader.GetAssemblyDefinition (); @@ -68,7 +68,7 @@ public void Generate_ReferencesGenericTypeMapAssemblyTargetAttribute () [Fact] public void Generate_EmptyList_ProducesValidAssemblyWithNoTargetAttributes () { - using var stream = GenerateRootAssembly (Array.Empty ()); + using var stream = GenerateRootAssembly ([]); using var pe = new PEReader (stream); var reader = pe.GetMetadataReader (); var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index b6a1c7bed10..daaa13802c3 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -251,7 +251,7 @@ public class EmptyInput : FixtureTestBase [Fact] public void Generate_EmptyPeerList_ProducesValidAssembly () { - using var stream = GenerateAssembly (Array.Empty (), "EmptyTest"); + using var stream = GenerateAssembly ([], "EmptyTest"); var (pe, reader) = OpenAssembly (stream); using (pe) { Assert.NotNull (reader); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 3f4d69ad915..bd3e6e38090 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -23,7 +23,7 @@ public class BasicStructure [Fact] public void Build_EmptyPeers_ProducesEmptyModel () { - var model = BuildModel (Array.Empty (), "Empty"); + var model = BuildModel ([], "Empty"); Assert.Equal ("Empty", model.AssemblyName); Assert.Equal ("Empty.dll", model.ModuleName); Assert.Empty (model.Entries); @@ -33,7 +33,7 @@ public void Build_EmptyPeers_ProducesEmptyModel () [Fact] public void Build_AssemblyNameDerivedFromOutputPath () { - var model = ModelBuilder.Build (Array.Empty (), "/some/path/Foo.Bar.dll"); + var model = ModelBuilder.Build ([], "/some/path/Foo.Bar.dll"); Assert.Equal ("Foo.Bar", model.AssemblyName); Assert.Equal ("Foo.Bar.dll", model.ModuleName); } @@ -41,7 +41,7 @@ public void Build_AssemblyNameDerivedFromOutputPath () [Fact] public void Build_ExplicitAssemblyName_OverridesOutputPath () { - var model = ModelBuilder.Build (Array.Empty (), "/some/path/Foo.dll", "MyAssembly"); + var model = ModelBuilder.Build ([], "/some/path/Foo.dll", "MyAssembly"); Assert.Equal ("MyAssembly", model.AssemblyName); } From 69420f5dd16a6d5bee2f34635acb2b5758479006 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 11 Mar 2026 20:24:19 +0100 Subject: [PATCH 06/21] Fix null! and empty catch --- .../Generator/ModelBuilder.cs | 2 +- .../Generator/FixtureTestBase.cs | 2 +- .../Generator/TypeMapModelBuilderTests.cs | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index 19ba83374ce..41aa67fe1a7 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -50,7 +50,7 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri // Invoker types are NOT emitted as separate proxies or TypeMap entries — // they only appear as a TypeRef in the interface proxy's get_InvokerType property. var invokerTypeNames = new HashSet ( - peers.Where (p => p.InvokerTypeName != null).Select (p => p.InvokerTypeName!), + peers.Where (p => p.InvokerTypeName != null).Select (p => p.InvokerTypeName ?? ""), StringComparer.Ordinal); // Group non-invoker peers by JNI name to detect aliases (multiple .NET types → same Java class). diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs index 56f9a92dc85..1027eecfa56 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs @@ -137,6 +137,6 @@ private protected static string CreateTempDir () private protected static void DeleteTempDir (string dir) { if (Directory.Exists (dir)) - try { Directory.Delete (dir, true); } catch { } + try { Directory.Delete (dir, true); } catch (IOException) { } } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index bd3e6e38090..553ccc10807 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -788,7 +788,8 @@ 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 proxy = FindProxy (model, "MyApp_MainActivity_Proxy"); + Assert.NotNull (proxy); var nonCtorMethods = peer.MarshalMethods.Where (m => !m.IsConstructor).ToList (); Assert.Equal (nonCtorMethods.Count, proxy.UcoMethods.Count); From 56d86e0c645f6705f09933484b7d799dba9e1090 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 11 Mar 2026 22:31:31 +0100 Subject: [PATCH 07/21] Clean up formatting, restore content fingerprint, remove redundant nullable - Remove #nullable enable from files (project-level Nullable already enabled) - Remove blank lines inside /// doc comment blocks - Fix JniParamKind enum indentation (was missing tab indent) - Fix DeleteTempDir: add braces around if body, expand try-catch - Restore MetadataHelper.ComputeContentFingerprint and content-dependent DeterministicMvid that were accidentally simplified away - Restore PEAssemblyBuilder.EmitPreamble contentFingerprint parameter Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/JcwJavaSourceGenerator.cs | 1 - .../Generator/JniSignatureHelper.cs | 27 ++++------ .../Generator/MetadataHelper.cs | 38 +++++++++++-- .../Generator/Model/TypeMapAssemblyData.cs | 53 ------------------- .../Generator/PEAssemblyBuilder.cs | 6 +-- .../Generator/TypeMapAssemblyEmitter.cs | 2 +- .../Generator/FixtureTestBase.cs | 8 ++- 7 files changed, 54 insertions(+), 81 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs index f7a3f24289c..8a0d02b49ac 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs @@ -1,4 +1,3 @@ -#nullable enable using System; using System.Collections.Generic; using System.IO; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs index 5dea85fea5b..5fe185d2926 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs @@ -1,4 +1,3 @@ -#nullable enable using System; using System.Collections.Generic; using System.Reflection.Metadata; @@ -11,16 +10,16 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// enum JniParamKind { -Void, // V -Boolean, // Z → bool -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 + Void, // V + Boolean, // Z → bool + 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 } /// @@ -57,9 +56,7 @@ public static List ParseParameters (string jniSignature) } /// - /// Extracts the return type descriptor from a JNI method signature. - /// public static string ParseReturnTypeString (string jniSignature) { @@ -70,9 +67,7 @@ public static string ParseReturnTypeString (string jniSignature) } /// - /// Parses the return type from a JNI method signature. - /// public static JniParamKind ParseReturnType (string jniSignature) { @@ -111,9 +106,7 @@ static JniParamKind ParseSingleType (string sig, ref int i) } /// - /// Encodes the CLR type for a JNI parameter kind into a signature type encoder. - /// public static void EncodeClrType (SignatureTypeEncoder encoder, JniParamKind kind) { diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs index 2f62bb468f1..9c0867c0875 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs @@ -7,14 +7,46 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; static class MetadataHelper { /// - /// Produces a deterministic MVID from the module name so that identical inputs produce identical assemblies. + /// Produces a deterministic MVID by hashing the module name together with content-dependent data. + /// Assemblies with the same name but different content will have different MVIDs. /// - public static Guid DeterministicMvid (string moduleName) + public static Guid DeterministicMvid (string moduleName, ReadOnlySpan contentBytes = default) { using var sha = SHA256.Create (); - byte [] hash = sha.ComputeHash (Encoding.UTF8.GetBytes (moduleName)); + byte [] nameBytes = Encoding.UTF8.GetBytes (moduleName); + byte [] input = new byte [nameBytes.Length + contentBytes.Length]; + nameBytes.CopyTo (input, 0); + contentBytes.CopyTo (input.AsSpan (nameBytes.Length)); + byte [] hash = sha.ComputeHash (input); byte [] guidBytes = new byte [16]; Array.Copy (hash, guidBytes, 16); return new Guid (guidBytes); } + + /// + /// Computes a content fingerprint for the given . + /// + public static byte [] ComputeContentFingerprint (TypeMapAssemblyData data) + { + using var sha = SHA256.Create (); + using var stream = new System.IO.MemoryStream (); + using var writer = new System.IO.BinaryWriter (stream, Encoding.UTF8); + foreach (var entry in data.Entries) { + writer.Write (entry.JniName); + writer.Write (entry.ProxyTypeReference); + writer.Write (entry.TargetTypeReference ?? ""); + } + foreach (var proxy in data.ProxyTypes) { + writer.Write (proxy.TypeName); + writer.Write (proxy.TargetType.ManagedTypeName); + writer.Write (proxy.TargetType.AssemblyName); + writer.Write ((byte)(proxy.ActivationCtor?.Style ?? 0)); + } + foreach (var assoc in data.Associations) { + writer.Write (assoc.SourceTypeReference); + writer.Write (assoc.AliasProxyTypeReference); + } + writer.Flush (); + return sha.ComputeHash (stream.ToArray ()); + } } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs index 38112b3175f..56484f7a99a 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -1,4 +1,3 @@ -#nullable enable using System; using System.Collections.Generic; @@ -17,37 +16,27 @@ sealed class TypeMapAssemblyData public required string AssemblyName { get; init; } /// - /// Module file name (e.g., "_MyApp.TypeMap.dll"). - /// public required string ModuleName { get; init; } /// - /// TypeMap entries — one per unique JNI name. - /// public List Entries { get; } = new (); /// - /// Proxy types to emit in the assembly. - /// public List ProxyTypes { get; } = new (); /// - /// TypeMapAssociation entries for alias groups (multiple managed types → same JNI name). - /// public List Associations { get; } = new (); /// - /// Assembly names that need [IgnoresAccessChecksTo] for cross-assembly n_* calls. - /// public List IgnoresAccessChecksTo { get; } = new (); } @@ -80,9 +69,7 @@ sealed record TypeMapAttributeData public string? TargetTypeReference { get; init; } /// - /// True for 2-arg unconditional entries (ACW types, essential runtime types). - /// public bool IsUnconditional => TargetTypeReference == null; } @@ -98,30 +85,22 @@ sealed class JavaPeerProxyData public required string TypeName { get; init; } /// - /// Namespace for all proxy types. - /// public string Namespace { get; init; } = "_TypeMap.Proxies"; /// - /// Reference to the managed type this proxy wraps (for ldtoken in TargetType property). - /// public required TypeRefData TargetType { get; init; } /// - /// Reference to the invoker type (for interfaces/abstract types). Null if not applicable. - /// public TypeRefData? InvokerType { get; set; } /// - /// Whether this proxy has a CreateInstance that can actually create instances. - /// public bool HasActivation => ActivationCtor != null || InvokerType != null; @@ -131,37 +110,27 @@ sealed class JavaPeerProxyData public ActivationCtorData? ActivationCtor { get; set; } /// - /// 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 (); } @@ -177,9 +146,7 @@ sealed record TypeRefData public required string ManagedTypeName { get; init; } /// - /// Assembly containing the type, e.g., "Mono.Android". - /// public required string AssemblyName { get; init; } } @@ -196,23 +163,17 @@ sealed record UcoMethodData 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; } } @@ -231,16 +192,12 @@ sealed record UcoConstructorData 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; } } @@ -256,16 +213,12 @@ sealed record NativeRegistrationData 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; } } @@ -281,16 +234,12 @@ sealed record ActivationCtorData public required TypeRefData DeclaringType { get; init; } /// - /// True when the leaf type itself declares the activation ctor. - /// public required bool IsOnLeafType { get; init; } /// - /// The style of activation ctor (XamarinAndroid or JavaInterop). - /// public required ActivationCtorStyle Style { get; init; } } @@ -307,9 +256,7 @@ sealed record TypeMapAssociationData public required string SourceTypeReference { get; init; } /// - /// Assembly-qualified proxy type reference (the alias holder proxy). - /// public required string AliasProxyTypeReference { get; init; } } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs index 6f38811f2f1..1b458400ec1 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs @@ -34,9 +34,7 @@ sealed class PEAssemblyBuilder public BlobBuilder ILBuilder { get; } = new BlobBuilder (); public AssemblyReferenceHandle SystemRuntimeRef { get; private set; } - public AssemblyReferenceHandle SystemRuntimeInteropServicesRef { get; private set; } - public AssemblyReferenceHandle MonoAndroidRef { get; private set; } public PEAssemblyBuilder (Version systemRuntimeVersion) @@ -48,7 +46,7 @@ public PEAssemblyBuilder (Version systemRuntimeVersion) /// Emits the assembly definition, module definition, common assembly references, and <Module> type. /// Call this first. /// - public void EmitPreamble (string assemblyName, string moduleName) + public void EmitPreamble (string assemblyName, string moduleName, ReadOnlySpan contentFingerprint = default) { _asmRefCache.Clear (); _typeRefCache.Clear (); @@ -64,7 +62,7 @@ public void EmitPreamble (string assemblyName, string moduleName) Metadata.AddModule ( generation: 0, Metadata.GetOrAddString (moduleName), - Metadata.GetOrAddGuid (MetadataHelper.DeterministicMvid (moduleName)), + Metadata.GetOrAddGuid (MetadataHelper.DeterministicMvid (moduleName, contentFingerprint)), encId: default, encBaseId: default); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 4698f05d949..53ffcfe15c0 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -137,7 +137,7 @@ public void Emit (TypeMapAssemblyData model, Stream output) void EmitCore (TypeMapAssemblyData model) { - _pe.EmitPreamble (model.AssemblyName, model.ModuleName); + _pe.EmitPreamble (model.AssemblyName, model.ModuleName, MetadataHelper.ComputeContentFingerprint (model)); _javaInteropRef = _pe.AddAssemblyRef ("Java.Interop", new Version (0, 0, 0, 0)); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs index 1027eecfa56..ce48496738a 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs @@ -136,7 +136,11 @@ private protected static string CreateTempDir () private protected static void DeleteTempDir (string dir) { - if (Directory.Exists (dir)) - try { Directory.Delete (dir, true); } catch (IOException) { } + if (Directory.Exists (dir)) { + try { + Directory.Delete (dir, true); + } catch (IOException) { + } + } } } From 9a18f49bd4fe4051d2af7d4dc152de7e4c1f9f31 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 11 Mar 2026 22:53:36 +0100 Subject: [PATCH 08/21] Restore JI-style activation from main and add UCO support Restores the JI-style activation code (EmitCreateInstanceViaJavaInteropNewobj, EmitCreateInstanceInheritedJavaInteropCtor, JNIEnv.DeleteRef, etc.) that was inadvertently removed from TypeMapAssemblyEmitter.cs and ModelBuilder.cs. Re-applies UCO (UnifiedCallableObject) additions on top: - EmitUcoMethod, EmitUcoConstructor, EmitRegisterNatives - IAndroidCallableWrapper interface implementation for ACW types - IsImplementorOrEventDispatcher heuristic for trimmability - BuildUcoMethods, BuildUcoConstructors, BuildNativeRegistrations in ModelBuilder - Restores 5-arg EmitBody overload with local variables support Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/ModelBuilder.cs | 41 ++- .../Generator/PEAssemblyBuilder.cs | 29 ++- .../Generator/TypeMapAssemblyEmitter.cs | 245 ++++++++++++++---- 3 files changed, 246 insertions(+), 69 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index 41aa67fe1a7..d97b2a9a93d 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -50,7 +50,7 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri // Invoker types are NOT emitted as separate proxies or TypeMap entries — // they only appear as a TypeRef in the interface proxy's get_InvokerType property. var invokerTypeNames = new HashSet ( - peers.Where (p => p.InvokerTypeName != null).Select (p => p.InvokerTypeName ?? ""), + peers.Select (p => p.InvokerTypeName).OfType (), StringComparer.Ordinal); // Group non-invoker peers by JNI name to detect aliases (multiple .NET types → same Java class). @@ -67,6 +67,8 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri list.Add (peer); } + var usedProxyNames = new HashSet (StringComparer.Ordinal); + foreach (var kvp in groups) { string jniName = kvp.Key; var peersForName = kvp.Value; @@ -76,7 +78,7 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri peersForName.Sort ((a, b) => StringComparer.Ordinal.Compare (a.ManagedTypeName, b.ManagedTypeName)); } - EmitPeers (model, jniName, peersForName, assemblyName); + EmitPeers (model, jniName, peersForName, assemblyName, usedProxyNames); } // Compute IgnoresAccessChecksTo from cross-assembly references @@ -90,13 +92,18 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri AddIfCrossAssembly (referencedAssemblies, proxy.ActivationCtor.DeclaringType.AssemblyName, assemblyName); } } + + // Always include Mono.Android — the emitter calls internal JNIEnv.DeleteRef + // for JI-style activation cleanup (matching legacy TypeManager.CreateProxy behavior). + referencedAssemblies.Add ("Mono.Android"); + model.IgnoresAccessChecksTo.AddRange (referencedAssemblies); return model; } static void EmitPeers (TypeMapAssemblyData model, string jniName, - List peersForName, string assemblyName) + List peersForName, string assemblyName, HashSet usedProxyNames) { // First peer is the "primary" — it gets the base JNI name entry. // Remaining peers get indexed alias entries: "jni/name[1]", "jni/name[2]", ... @@ -110,7 +117,7 @@ static void EmitPeers (TypeMapAssemblyData model, string jniName, JavaPeerProxyData? proxy = null; if (hasProxy) { - proxy = BuildProxyType (peer, isAcw); + proxy = BuildProxyType (peer, usedProxyNames, isAcw); model.ProxyTypes.Add (proxy); } @@ -123,8 +130,8 @@ static void EmitPeers (TypeMapAssemblyData model, string jniName, // Emit TypeMapAssociation linking alias types to the primary proxy if (i > 0 && primaryProxy != null) { model.Associations.Add (new TypeMapAssociationData { - SourceTypeReference = $"{peer.ManagedTypeName}, {peer.AssemblyName}", - AliasProxyTypeReference = $"{primaryProxy.Namespace}.{primaryProxy.TypeName}, {assemblyName}", + SourceTypeReference = AssemblyQualify (peer.ManagedTypeName, peer.AssemblyName), + AliasProxyTypeReference = AssemblyQualify ($"{primaryProxy.Namespace}.{primaryProxy.TypeName}", assemblyName), }); } } @@ -182,12 +189,23 @@ static void AddIfCrossAssembly (SortedSet set, string? asmName, string o } } - static JavaPeerProxyData BuildProxyType (JavaPeerInfo peer, bool isAcw) + 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). var proxyTypeName = peer.ManagedTypeName.Replace ('.', '_').Replace ('+', '_') + "_Proxy"; + // Guard against name collisions (e.g., "My.Type" and "My_Type" both map to "My_Type_Proxy") + if (!usedProxyNames.Add (proxyTypeName)) { + int suffix = 2; + string candidate; + do { + candidate = $"{proxyTypeName}_{suffix}"; + suffix++; + } while (!usedProxyNames.Add (candidate)); + proxyTypeName = candidate; + } + var proxy = new JavaPeerProxyData { TypeName = proxyTypeName, TargetType = new TypeRefData { @@ -296,15 +314,15 @@ static TypeMapAttributeData BuildEntry (JavaPeerInfo peer, JavaPeerProxyData? pr { string proxyRef; if (proxy != null) { - proxyRef = $"{proxy.Namespace}.{proxy.TypeName}, {outputAssemblyName}"; + proxyRef = AssemblyQualify ($"{proxy.Namespace}.{proxy.TypeName}", outputAssemblyName); } else { - proxyRef = $"{peer.ManagedTypeName}, {peer.AssemblyName}"; + proxyRef = AssemblyQualify (peer.ManagedTypeName, peer.AssemblyName); } bool isUnconditional = IsUnconditionalEntry (peer); string? targetRef = null; if (!isUnconditional) { - targetRef = $"{peer.ManagedTypeName}, {peer.AssemblyName}"; + targetRef = AssemblyQualify (peer.ManagedTypeName, peer.AssemblyName); } return new TypeMapAttributeData { @@ -313,4 +331,7 @@ static TypeMapAttributeData BuildEntry (JavaPeerInfo peer, JavaPeerProxyData? pr TargetTypeReference = targetRef, }; } + + static string AssemblyQualify (string typeName, string assemblyName) + => $"{typeName}, {assemblyName}"; } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs index 1b458400ec1..5460e695fab 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs @@ -177,9 +177,32 @@ TypeReferenceHandle MakeTypeRefForManagedName (EntityHandle scope, string manage /// public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, Action encodeSig, Action emitIL) + => EmitBody (name, attrs, encodeSig, emitIL, encodeLocals: null); + + /// + /// Emits a method body and definition with optional local variable declarations. + /// + /// + /// If non-null, writes the local variable signature blob. The callback receives a fresh + /// and must write the full LOCAL_SIG blob (header 0x07, + /// compressed count, then each variable type). + /// + public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, + Action encodeSig, Action emitIL, + Action? encodeLocals) { _sigBlob.Clear (); encodeSig (new BlobEncoder (_sigBlob)); + // Capture the sig blob handle before emitIL, because emitIL callbacks + // may call AddMemberRef which clears and repopulates _sigBlob. + var sigBlobHandle = Metadata.GetOrAddBlob (_sigBlob); + + StandaloneSignatureHandle localSigHandle = default; + if (encodeLocals != null) { + var localSigBlob = new BlobBuilder (32); + encodeLocals (localSigBlob); + localSigHandle = Metadata.AddStandaloneSignature (Metadata.GetOrAddBlob (localSigBlob)); + } _codeBlob.Clear (); var encoder = new InstructionEncoder (_codeBlob); @@ -189,12 +212,14 @@ public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, ILBuilder.WriteByte (0); } var bodyEncoder = new MethodBodyStreamEncoder (ILBuilder); - int bodyOffset = bodyEncoder.AddMethodBody (encoder); + int bodyOffset = localSigHandle.IsNil + ? bodyEncoder.AddMethodBody (encoder) + : bodyEncoder.AddMethodBody (encoder, maxStack: 8, localSigHandle, MethodBodyAttributes.InitLocals); return Metadata.AddMethodDefinition ( attrs, MethodImplAttributes.IL, Metadata.GetOrAddString (name), - Metadata.GetOrAddBlob (_sigBlob), + sigBlobHandle, bodyOffset, default); } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 53ffcfe15c0..775f48af607 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -71,6 +71,7 @@ sealed class TypeMapAssemblyEmitter TypeReferenceHandle _jniObjectReferenceRef; TypeReferenceHandle _jniObjectReferenceOptionsRef; TypeReferenceHandle _iAndroidCallableWrapperRef; + TypeReferenceHandle _jniEnvRef; TypeReferenceHandle _systemTypeRef; TypeReferenceHandle _runtimeTypeHandleRef; TypeReferenceHandle _jniTypeRef; @@ -82,6 +83,8 @@ sealed class TypeMapAssemblyEmitter MemberReferenceHandle _getTypeFromHandleRef; MemberReferenceHandle _getUninitializedObjectRef; MemberReferenceHandle _notSupportedExceptionCtorRef; + MemberReferenceHandle _jniObjectReferenceCtorRef; + MemberReferenceHandle _jniEnvDeleteRefRef; MemberReferenceHandle _activateInstanceRef; MemberReferenceHandle _registerMethodRef; MemberReferenceHandle _ucoAttrCtorRef; @@ -120,19 +123,19 @@ public void Emit (TypeMapAssemblyData model, string outputPath) } /// - /// Emits a PE assembly from the given model and writes it to . + /// Emits a PE assembly from the given model and writes it to . /// - public void Emit (TypeMapAssemblyData model, Stream output) + public void Emit (TypeMapAssemblyData model, Stream stream) { if (model is null) { throw new ArgumentNullException (nameof (model)); } - if (output is null) { - throw new ArgumentNullException (nameof (output)); + if (stream is null) { + throw new ArgumentNullException (nameof (stream)); } EmitCore (model); - _pe.WritePE (output); + _pe.WritePE (stream); } void EmitCore (TypeMapAssemblyData model) @@ -171,6 +174,8 @@ void EmitTypeReferences () metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("IJavaPeerable")); _jniHandleOwnershipRef = metadata.AddTypeReference (_pe.MonoAndroidRef, metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("JniHandleOwnership")); + _jniEnvRef = metadata.AddTypeReference (_pe.MonoAndroidRef, + metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("JNIEnv")); _jniObjectReferenceRef = metadata.AddTypeReference (_javaInteropRef, metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniObjectReference")); _jniObjectReferenceOptionsRef = metadata.AddTypeReference (_javaInteropRef, @@ -211,6 +216,22 @@ void EmitMemberReferences () rt => rt.Void (), p => p.AddParameter ().Type ().String ())); + _jniObjectReferenceCtorRef = _pe.AddMemberRef (_jniObjectReferenceRef, ".ctor", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (1, + rt => rt.Void (), + p => p.AddParameter ().Type ().IntPtr ())); + + // JNIEnv.DeleteRef(IntPtr, JniHandleOwnership) — static, internal + // Used by JI-style activation to clean up the original handle after constructing the peer. + // Matches the legacy TypeManager.CreateProxy behavior. + _jniEnvDeleteRefRef = _pe.AddMemberRef (_jniEnvRef, "DeleteRef", + sig => sig.MethodSignature ().Parameters (2, + rt => rt.Void (), + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); + })); + _activateInstanceRef = _pe.AddMemberRef (_trimmableNativeRegistrationRef, "ActivateInstance", sig => sig.MethodSignature ().Parameters (2, rt => rt.Void (), @@ -291,7 +312,6 @@ void EmitTypeMapAssociationAttributeCtorRef () })); } - void EmitProxyType (JavaPeerProxyData proxy, Dictionary wrapperHandles) { var metadata = _pe.Metadata; @@ -350,17 +370,34 @@ void EmitProxyType (JavaPeerProxyData proxy, Dictionary - /// Emits a CreateInstance body that returns null (no activation possible). - /// - void EmitNullCreateInstance () + void EmitCreateInstanceNoActivation () { EmitCreateInstanceBody (encoder => { encoder.OpCode (ILOpCode.Ldnull); @@ -386,10 +420,7 @@ void EmitNullCreateInstance () }); } - /// - /// Emits a CreateInstance body that throws NotSupportedException for open generic types. - /// - void EmitGenericCreateInstance () + void EmitCreateInstanceGenericDefinition () { EmitCreateInstanceBody (encoder => { encoder.LoadString (_pe.Metadata.GetOrAddUserString ("Cannot create instance of open generic type.")); @@ -399,27 +430,9 @@ void EmitGenericCreateInstance () }); } - /// - /// Emits a CreateInstance body that instantiates the interface invoker type: new TInvoker(IntPtr, JniHandleOwnership). - /// - void EmitInvokerCreateInstance (TypeRefData invokerType) - { - var invokerCtorRef = AddActivationCtorRef (_pe.ResolveTypeRef (invokerType), ActivationCtorStyle.XamarinAndroid); - EmitCreateInstanceBody (encoder => { - encoder.OpCode (ILOpCode.Ldarg_1); - encoder.OpCode (ILOpCode.Ldarg_2); - encoder.OpCode (ILOpCode.Newobj); - encoder.Token (invokerCtorRef); - encoder.OpCode (ILOpCode.Ret); - }); - } - - /// - /// Emits a CreateInstance body for a leaf type that has its own activation ctor: new T(IntPtr, JniHandleOwnership). - /// - void EmitLeafCreateInstance (EntityHandle targetTypeRef, ActivationCtorStyle style) + void EmitCreateInstanceViaNewobj (EntityHandle typeRef) { - var ctorRef = AddActivationCtorRef (targetTypeRef, style); + var ctorRef = AddActivationCtorRef (typeRef); EmitCreateInstanceBody (encoder => { encoder.OpCode (ILOpCode.Ldarg_1); encoder.OpCode (ILOpCode.Ldarg_2); @@ -429,13 +442,9 @@ void EmitLeafCreateInstance (EntityHandle targetTypeRef, ActivationCtorStyle sty }); } - /// - /// Emits a CreateInstance body for types that inherit activation from a base type: - /// GetUninitializedObject(typeof(T)) + call Base::.ctor(IntPtr, JniHandleOwnership). - /// - void EmitInheritedCreateInstance (EntityHandle targetTypeRef, ActivationCtorData activationCtor) + void EmitCreateInstanceInheritedCtor (EntityHandle targetTypeRef, ActivationCtorData activationCtor) { - var baseActivationCtorRef = AddActivationCtorRef (_pe.ResolveTypeRef (activationCtor.DeclaringType), activationCtor.Style); + var baseActivationCtorRef = AddActivationCtorRef (_pe.ResolveTypeRef (activationCtor.DeclaringType)); EmitCreateInstanceBody (encoder => { encoder.OpCode (ILOpCode.Ldtoken); encoder.Token (targetTypeRef); @@ -453,6 +462,119 @@ void EmitInheritedCreateInstance (EntityHandle targetTypeRef, ActivationCtorData }); } + /// + /// Emits CreateInstance for JavaInterop-style activation (leaf type): + /// var jniRef = new JniObjectReference(handle); + /// var result = new TargetType(ref jniRef, JniObjectReferenceOptions.Copy); + /// JNIEnv.DeleteRef(handle, ownership); + /// return result; + /// + void EmitCreateInstanceViaJavaInteropNewobj (EntityHandle typeRef) + { + var ctorRef = AddJavaInteropActivationCtorRef (typeRef); + EmitCreateInstanceBodyWithLocals ( + EncodeJniObjectReferenceAndObjectLocals, + encoder => { + // var jniRef = new JniObjectReference(handle); + encoder.LoadLocalAddress (0); + encoder.OpCode (ILOpCode.Ldarg_1); // handle + encoder.Call (_jniObjectReferenceCtorRef); + + // var result = new TargetType(ref jniRef, JniObjectReferenceOptions.Copy); + encoder.LoadLocalAddress (0); + encoder.LoadConstantI4 (1); // JniObjectReferenceOptions.Copy + encoder.OpCode (ILOpCode.Newobj); + encoder.Token (ctorRef); + encoder.StoreLocal (1); // save result + + // JNIEnv.DeleteRef(handle, ownership); + encoder.OpCode (ILOpCode.Ldarg_1); // handle + encoder.OpCode (ILOpCode.Ldarg_2); // ownership + encoder.Call (_jniEnvDeleteRefRef); + + encoder.LoadLocal (1); // load result + encoder.OpCode (ILOpCode.Ret); + }); + } + + /// + /// Emits CreateInstance for JavaInterop-style activation (inherited ctor): + /// var obj = (TargetType)RuntimeHelpers.GetUninitializedObject(typeof(TargetType)); + /// var jniRef = new JniObjectReference(handle); + /// obj.BaseCtor(ref jniRef, JniObjectReferenceOptions.Copy); + /// JNIEnv.DeleteRef(handle, ownership); + /// return obj; + /// + void EmitCreateInstanceInheritedJavaInteropCtor (EntityHandle targetTypeRef, ActivationCtorData activationCtor) + { + var baseCtorRef = AddJavaInteropActivationCtorRef (_pe.ResolveTypeRef (activationCtor.DeclaringType)); + EmitCreateInstanceBodyWithLocals ( + EncodeJniObjectReferenceLocal, + encoder => { + // var obj = (TargetType)RuntimeHelpers.GetUninitializedObject(typeof(TargetType)); + encoder.OpCode (ILOpCode.Ldtoken); + encoder.Token (targetTypeRef); + encoder.Call (_getTypeFromHandleRef); + encoder.Call (_getUninitializedObjectRef); + encoder.OpCode (ILOpCode.Castclass); + encoder.Token (targetTypeRef); + + // dup obj (one copy for the call, one for the return) + encoder.OpCode (ILOpCode.Dup); + + // var jniRef = new JniObjectReference(handle); + encoder.LoadLocalAddress (0); + encoder.OpCode (ILOpCode.Ldarg_1); // handle + encoder.Call (_jniObjectReferenceCtorRef); + + // obj.BaseCtor(ref jniRef, JniObjectReferenceOptions.Copy); + encoder.LoadLocalAddress (0); + encoder.LoadConstantI4 (1); // JniObjectReferenceOptions.Copy + encoder.Call (baseCtorRef); + + // JNIEnv.DeleteRef(handle, ownership); + encoder.OpCode (ILOpCode.Ldarg_1); // handle + encoder.OpCode (ILOpCode.Ldarg_2); // ownership + encoder.Call (_jniEnvDeleteRefRef); + + encoder.OpCode (ILOpCode.Ret); + }); + } + + void EncodeJniObjectReferenceLocal (BlobBuilder blob) + { + // LOCAL_SIG header (0x07), count = 1, ELEMENT_TYPE_VALUETYPE + compressed token + blob.WriteByte (0x07); // LOCAL_SIG + blob.WriteCompressedInteger (1); // 1 local variable + blob.WriteByte (0x11); // ELEMENT_TYPE_VALUETYPE + blob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (_jniObjectReferenceRef)); + } + + void EncodeJniObjectReferenceAndObjectLocals (BlobBuilder blob) + { + // LOCAL_SIG header (0x07), count = 2: + // local 0: JniObjectReference (valuetype) + // local 1: object (for storing the newobj result across the DeleteRef call) + blob.WriteByte (0x07); // LOCAL_SIG + blob.WriteCompressedInteger (2); // 2 local variables + blob.WriteByte (0x11); // ELEMENT_TYPE_VALUETYPE + blob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (_jniObjectReferenceRef)); + blob.WriteByte (0x1c); // ELEMENT_TYPE_OBJECT + } + + MemberReferenceHandle AddJavaInteropActivationCtorRef (EntityHandle declaringTypeRef) + { + return _pe.AddMemberRef (declaringTypeRef, ".ctor", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, + rt => rt.Void (), + p => { + // ref JniObjectReference — encoded as byref valuetype + p.AddParameter ().Type (isByRef: true).Type (_jniObjectReferenceRef, true); + // JniObjectReferenceOptions — encoded as valuetype (enum) + p.AddParameter ().Type ().Type (_jniObjectReferenceOptionsRef, true); + })); + } + void EmitCreateInstanceBody (Action emitIL) { _pe.EmitBody ("CreateInstance", @@ -466,19 +588,28 @@ void EmitCreateInstanceBody (Action emitIL) emitIL); } - MemberReferenceHandle AddActivationCtorRef (EntityHandle declaringTypeRef, ActivationCtorStyle style) + void EmitCreateInstanceBodyWithLocals (Action encodeLocals, Action emitIL) + { + _pe.EmitBody ("CreateInstance", + MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig, + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, + rt => rt.Type ().Type (_iJavaPeerableRef, false), + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); + }), + emitIL, + encodeLocals); + } + + MemberReferenceHandle AddActivationCtorRef (EntityHandle declaringTypeRef) { return _pe.AddMemberRef (declaringTypeRef, ".ctor", sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, rt => rt.Void (), p => { - if (style == ActivationCtorStyle.JavaInterop) { - p.AddParameter ().Type (isByRef: true).Type (_jniObjectReferenceRef, true); - p.AddParameter ().Type ().Type (_jniObjectReferenceOptionsRef, true); - } else { - p.AddParameter ().Type ().IntPtr (); - p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); - } + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); })); } @@ -605,9 +736,10 @@ void EmitTypeMapAttribute (TypeMapAttributeData entry) b.WriteSerializedString (entry.JniName); b.WriteSerializedString (entry.ProxyTypeReference); if (!entry.IsUnconditional) { - var targetRef = entry.TargetTypeReference - ?? throw new InvalidOperationException ("TargetTypeReference must be set for conditional entries"); - b.WriteSerializedString (targetRef); + if (entry.TargetTypeReference is null) { + throw new InvalidOperationException ($"TargetTypeReference must not be null for conditional entry '{entry.JniName}'"); + } + b.WriteSerializedString (entry.TargetTypeReference); } }); _pe.Metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, ctorRef, blob); @@ -621,5 +753,4 @@ void EmitTypeMapAssociationAttribute (TypeMapAssociationData assoc) }); _pe.Metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, _typeMapAssociationAttrCtorRef, blob); } - } From 81d8af43091dfc885c8f02cc2904ec9dadbc1d12 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 11 Mar 2026 23:04:17 +0100 Subject: [PATCH 09/21] Fix JNI boolean encoding: use byte for UCO blittability JNI jboolean is an unsigned byte. UnmanagedCallersOnly methods require blittable parameter types, so Boolean must be encoded as byte (not CLR bool) in UCO wrapper signatures. This matches the issue requirement: 'Non-blittable params converted correctly (byte for bool)'. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/JniSignatureHelper.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs index 5fe185d2926..abf66a4ffad 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs @@ -11,7 +11,7 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; enum JniParamKind { Void, // V - Boolean, // Z → bool + Boolean, // Z → byte (JNI jboolean is unsigned byte; must be blittable for UCO) Byte, // B → sbyte Char, // C → char Short, // S → short @@ -111,7 +111,7 @@ static JniParamKind ParseSingleType (string sig, ref int i) public static void EncodeClrType (SignatureTypeEncoder encoder, JniParamKind kind) { switch (kind) { - case JniParamKind.Boolean: encoder.Boolean (); break; + 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; From 9bc727cd66083001838d89b0f129ae69512ddcde Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 12 Mar 2026 06:30:39 +0100 Subject: [PATCH 10/21] Restore 7 tests from PR #10808: JI-style, determinism, emitter behavior Tests were accidentally dropped during the rebase refactoring: - EmitterBehavior: Emit_CalledTwice_Throws, signature blob corruption - DeterminismTests: different/identical content MVIDs - JavaInteropActivation: JI-style ctor type refs, byref param, DeleteRef Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TypeMapAssemblyGeneratorTests.cs | 199 ++++++++++++++++++ 1 file changed, 199 insertions(+) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index daaa13802c3..a8cdaf609be 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -421,4 +421,203 @@ public void Generate_InheritedCtor_IncludesBaseCtorAssembly () } + public class EmitterBehavior + { + + [Fact] + public void Emit_CalledTwice_Throws () + { + var model = ModelBuilder.Build ([], "Double.dll", "Double"); + var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); + emitter.Emit (model, new MemoryStream ()); + // MetadataBuilder.AddAssembly throws on second call (only one assembly definition per PE) + Assert.ThrowsAny (() => emitter.Emit (model, new MemoryStream ())); + } + + [Fact] + public void EmitBody_ILCallbackCallsAddMemberRef_SignatureNotCorrupted () + { + // Regression test: EmitBody uses shared _sigBlob for the method signature. + // If the emitIL callback calls AddMemberRef (which also uses _sigBlob), + // the method signature must not be corrupted. + var pe = new PEAssemblyBuilder (new Version (11, 0, 0, 0)); + pe.EmitPreamble ("SigTest", "SigTest.dll"); + + var objectRef = pe.Metadata.AddTypeReference (pe.SystemRuntimeRef, + pe.Metadata.GetOrAddString ("System"), pe.Metadata.GetOrAddString ("Object")); + + // already defined; add a type to host the method + pe.Metadata.AddTypeDefinition ( + System.Reflection.TypeAttributes.Public | System.Reflection.TypeAttributes.Class, + pe.Metadata.GetOrAddString ("Test"), + pe.Metadata.GetOrAddString ("MyType"), + objectRef, + MetadataTokens.FieldDefinitionHandle (pe.Metadata.GetRowCount (TableIndex.Field) + 1), + MetadataTokens.MethodDefinitionHandle (pe.Metadata.GetRowCount (TableIndex.MethodDef) + 1)); + + // EmitBody with an IL callback that calls AddMemberRef (clearing _sigBlob) + pe.EmitBody ("TestMethod", + MethodAttributes.Public | MethodAttributes.Static, + sig => sig.MethodSignature ().Parameters (1, + rt => rt.Void (), + p => p.AddParameter ().Type ().Int32 ()), + encoder => { + // This AddMemberRef call clears and repopulates _sigBlob + pe.AddMemberRef (objectRef, ".ctor", + s => s.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { })); + encoder.OpCode (ILOpCode.Ret); + }); + + // If the sig blob was corrupted, the PE metadata will have a wrong signature. + // Write and read back to verify. + var stream = new MemoryStream (); + pe.WritePE (stream); + stream.Position = 0; + + using var peReader = new PEReader (stream); + var reader = peReader.GetMetadataReader (); + var methods = reader.TypeDefinitions + .SelectMany (h => reader.GetTypeDefinition (h).GetMethods ()) + .Select (h => reader.GetMethodDefinition (h)) + .ToList (); + + var testMethod = methods.First (m => reader.GetString (m.Name) == "TestMethod"); + var sig = testMethod.DecodeSignature (SignatureTypeProvider.Instance, null); + var paramType = Assert.Single (sig.ParameterTypes); + Assert.Equal ("System.Int32", paramType); + } + + } + + public class DeterminismTests : FixtureTestBase + { + + [Fact] + public void Generate_DifferentContent_ProducesDifferentMVIDs () + { + var peer1 = MakePeerWithActivation ("test/TypeA", "Test.TypeA", "TestAsm"); + var peer2 = MakePeerWithActivation ("test/TypeB", "Test.TypeB", "TestAsm"); + + using var stream1 = GenerateAssembly (new [] { peer1 }, "SameName"); + using var stream2 = GenerateAssembly (new [] { peer2 }, "SameName"); + + var (pe1, reader1) = OpenAssembly (stream1); + var (pe2, reader2) = OpenAssembly (stream2); + using (pe1) + using (pe2) { + var mvid1 = reader1.GetGuid (reader1.GetModuleDefinition ().Mvid); + var mvid2 = reader2.GetGuid (reader2.GetModuleDefinition ().Mvid); + Assert.NotEqual (mvid1, mvid2); + } + } + + [Fact] + public void Generate_IdenticalContent_ProducesIdenticalMVIDs () + { + var peer = MakePeerWithActivation ("test/TypeA", "Test.TypeA", "TestAsm"); + + using var stream1 = GenerateAssembly (new [] { peer }, "SameName"); + using var stream2 = GenerateAssembly (new [] { peer }, "SameName"); + + var (pe1, reader1) = OpenAssembly (stream1); + var (pe2, reader2) = OpenAssembly (stream2); + using (pe1) + using (pe2) { + var mvid1 = reader1.GetGuid (reader1.GetModuleDefinition ().Mvid); + var mvid2 = reader2.GetGuid (reader2.GetModuleDefinition ().Mvid); + Assert.Equal (mvid1, mvid2); + } + } + + } + + public class JavaInteropActivation : FixtureTestBase + { + + [Fact] + public void Generate_JiStyleCtor_EmitsJavaInteropActivation () + { + var peers = ScanFixtures (); + var jiPeer = peers.First (p => p.JavaName == "my/app/JiStylePeer"); + Assert.NotNull (jiPeer.ActivationCtor); + Assert.Equal (ActivationCtorStyle.JavaInterop, jiPeer.ActivationCtor.Style); + + using var stream = GenerateAssembly (new [] { jiPeer }, "JiStyleTest"); + var (pe, reader) = OpenAssembly (stream); + using (pe) { + // JI-style activation should emit JniObjectReference and JniObjectReferenceOptions type refs + var typeNames = GetTypeRefNames (reader); + Assert.Contains ("JniObjectReference", typeNames); + Assert.Contains ("JniObjectReferenceOptions", typeNames); + + // The proxy still exists (with a TargetType property) + var proxyTypes = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .Where (t => reader.GetString (t.Namespace) == "_TypeMap.Proxies") + .ToList (); + Assert.Single (proxyTypes); + } + } + + [Fact] + public void Generate_JiStyleCtor_FirstParamIsByRef () + { + var peers = ScanFixtures (); + var jiPeer = peers.First (p => p.JavaName == "my/app/JiStylePeer"); + Assert.NotNull (jiPeer.ActivationCtor); + Assert.Equal (ActivationCtorStyle.JavaInterop, jiPeer.ActivationCtor.Style); + + using var stream = GenerateAssembly (new [] { jiPeer }, "JiByRefTest"); + var (pe, reader) = OpenAssembly (stream); + using (pe) { + // Find the .ctor member reference whose parent type is the JI peer's declaring type + var ctorRefs = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) + .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i))) + .Where (m => reader.GetString (m.Name) == ".ctor") + .ToList (); + + // Decode each .ctor signature and find the JI-style one (2 params, first is byref JniObjectReference) + bool foundByRefCtor = false; + foreach (var ctor in ctorRefs) { + var sig = ctor.DecodeMethodSignature (SignatureTypeProvider.Instance, null); + if (sig.ParameterTypes.Length == 2 && + sig.ParameterTypes [0].Contains ("JniObjectReference")) { + // The byref encoding should produce "Java.Interop.JniObjectReference&" + Assert.True (sig.ParameterTypes [0].EndsWith ("&"), + $"JI-style .ctor first param must be byref, got: {sig.ParameterTypes [0]}"); + foundByRefCtor = true; + } + } + Assert.True (foundByRefCtor, "Expected to find a .ctor with byref JniObjectReference parameter"); + } + } + + [Fact] + public void Generate_JiStyleCtor_EmitsDeleteRefCall () + { + var peers = ScanFixtures (); + var jiPeer = peers.First (p => p.JavaName == "my/app/JiStylePeer"); + + using var stream = GenerateAssembly (new [] { jiPeer }, "JiDeleteRefTest"); + var (pe, reader) = OpenAssembly (stream); + using (pe) { + // The JI-style activation path must emit a call to JNIEnv.DeleteRef(IntPtr, JniHandleOwnership) + // to match the legacy TypeManager.CreateProxy behavior. + var memberRefs = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) + .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i))) + .ToList (); + + var deleteRefRef = memberRefs.FirstOrDefault (m => reader.GetString (m.Name) == "DeleteRef"); + Assert.True (!deleteRefRef.Equals (default (MemberReference)), + "JI-style activation must emit a DeleteRef member reference for JNI handle cleanup"); + + // Verify it's on the JNIEnv type + var parentTypeRef = reader.GetTypeReference ((TypeReferenceHandle) deleteRefRef.Parent); + Assert.Equal ("JNIEnv", reader.GetString (parentTypeRef.Name)); + Assert.Equal ("Android.Runtime", reader.GetString (parentTypeRef.Namespace)); + } + } + + } + } \ No newline at end of file From baaf6003a77648bcfb1600a099d0a92514133cf9 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 12 Mar 2026 06:36:27 +0100 Subject: [PATCH 11/21] Restore PEAssemblyBuilder, TypeMapAssemblyGenerator, RootTypeMapAssemblyGenerator to main MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Undo unnecessary renames (stream → output) and refactoring in files that don't need changes for this PR. Main already has the stream overloads needed by tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/PEAssemblyBuilder.cs | 13 +++---- .../Generator/RootTypeMapAssemblyGenerator.cs | 35 +++++++++---------- .../Generator/TypeMapAssemblyGenerator.cs | 8 ++--- 3 files changed, 24 insertions(+), 32 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs index 5460e695fab..b862cc2b29f 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs @@ -95,9 +95,9 @@ public void WritePE (string outputPath) } /// - /// Serialises the metadata + IL into a PE DLL written to . + /// Serialises the metadata + IL into a PE DLL and writes it to the given . /// - public void WritePE (Stream output) + public void WritePE (Stream stream) { var peBuilder = new ManagedPEBuilder ( new PEHeaderBuilder (imageCharacteristics: Characteristics.Dll), @@ -105,7 +105,7 @@ public void WritePE (Stream output) ILBuilder); var peBlob = new BlobBuilder (); peBuilder.Serialize (peBlob); - peBlob.WriteContentTo (output); + peBlob.WriteContentTo (stream); } /// @@ -127,12 +127,7 @@ public AssemblyReferenceHandle AddAssemblyRef (string name, Version version, byt /// Finds an existing assembly reference or adds one with version 0.0.0.0. /// public AssemblyReferenceHandle FindOrAddAssemblyRef (string assemblyName) - { - if (_asmRefCache.TryGetValue (assemblyName, out var handle)) { - return handle; - } - return AddAssemblyRef (assemblyName, new Version (0, 0, 0, 0)); - } + => AddAssemblyRef (assemblyName, new Version (0, 0, 0, 0)); /// /// Adds a member reference using the reusable signature blob builder. diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs index ca95513cbdd..14b49cfe986 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs @@ -32,49 +32,46 @@ public RootTypeMapAssemblyGenerator (Version systemRuntimeVersion) } /// - /// Generates the root typemap assembly. + /// Generates the root typemap assembly and writes it to a file. /// /// Names of per-assembly typemap assemblies to reference. /// Path to write the output .dll. /// Optional assembly name (defaults to _Microsoft.Android.TypeMaps). public void Generate (IReadOnlyList perAssemblyTypeMapNames, string outputPath, string? assemblyName = null) { - if (perAssemblyTypeMapNames is null) { - throw new ArgumentNullException (nameof (perAssemblyTypeMapNames)); - } if (outputPath is null) { throw new ArgumentNullException (nameof (outputPath)); } - assemblyName ??= DefaultAssemblyName; + var dir = Path.GetDirectoryName (outputPath); + if (!string.IsNullOrEmpty (dir)) { + Directory.CreateDirectory (dir); + } + var moduleName = Path.GetFileName (outputPath); - var pe = GenerateCore (perAssemblyTypeMapNames, assemblyName, moduleName); - pe.WritePE (outputPath); + using var fs = File.Create (outputPath); + Generate (perAssemblyTypeMapNames, fs, assemblyName, moduleName); } /// - /// Generates the root typemap assembly and writes it to . + /// Generates the root typemap assembly and writes it to the given stream. /// /// Names of per-assembly typemap assemblies to reference. - /// Stream to write the output PE assembly to. + /// Stream to write the output PE to. /// Optional assembly name (defaults to _Microsoft.Android.TypeMaps). - public void Generate (IReadOnlyList perAssemblyTypeMapNames, Stream output, string? assemblyName = null) + /// Optional module name for the PE metadata. + public void Generate (IReadOnlyList perAssemblyTypeMapNames, Stream stream, string? assemblyName = null, string? moduleName = null) { if (perAssemblyTypeMapNames is null) { throw new ArgumentNullException (nameof (perAssemblyTypeMapNames)); } - if (output is null) { - throw new ArgumentNullException (nameof (output)); + if (stream is null) { + throw new ArgumentNullException (nameof (stream)); } assemblyName ??= DefaultAssemblyName; - var moduleName = assemblyName + ".dll"; - var pe = GenerateCore (perAssemblyTypeMapNames, assemblyName, moduleName); - pe.WritePE (output); - } + moduleName ??= assemblyName + ".dll"; - PEAssemblyBuilder GenerateCore (IReadOnlyList perAssemblyTypeMapNames, string assemblyName, string moduleName) - { var pe = new PEAssemblyBuilder (_systemRuntimeVersion); pe.EmitPreamble (assemblyName, moduleName); @@ -102,6 +99,6 @@ PEAssemblyBuilder GenerateCore (IReadOnlyList perAssemblyTypeMapNames, s pe.Metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, ctorRef, blobHandle); } - return pe; + pe.WritePE (stream); } } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs index f50582dd3cd..f6586218d6a 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs @@ -32,15 +32,15 @@ public void Generate (IReadOnlyList peers, string outputPath, stri } /// - /// Generates a TypeMap PE assembly and writes it to . + /// Generates a TypeMap PE assembly from the given Java peer info records and writes it to . /// /// Scanned Java peer types. - /// Stream to write the output PE assembly to. + /// Stream to write the output PE assembly to. /// Assembly name for the generated assembly. - public void Generate (IReadOnlyList peers, Stream output, string assemblyName) + public void Generate (IReadOnlyList peers, Stream stream, string assemblyName) { var model = ModelBuilder.Build (peers, assemblyName + ".dll", assemblyName); var emitter = new TypeMapAssemblyEmitter (_systemRuntimeVersion); - emitter.Emit (model, output); + emitter.Emit (model, stream); } } From e3adef5a57cb5234491429901a6e41d80719946b Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 12 Mar 2026 06:39:20 +0100 Subject: [PATCH 12/21] Minimize TypeMapAssemblyGeneratorTests changes: append new tests only Restore main's flat test structure instead of reorganizing into nested classes. Only add 10 new tests (ACW proxy, UCO, JniSignatureHelper, edge cases) as pure additions with zero deletions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TypeMapAssemblyGeneratorTests.cs | 914 ++++++++---------- 1 file changed, 399 insertions(+), 515 deletions(-) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index a8cdaf609be..4f6c595a5ba 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -12,185 +12,98 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; public class TypeMapAssemblyGeneratorTests : FixtureTestBase { - static MemoryStream GenerateAssembly (IReadOnlyList peers, string? assemblyName = null) + static MemoryStream GenerateAssembly (IReadOnlyList peers, string assemblyName = "TestTypeMap") { var stream = new MemoryStream (); var generator = new TypeMapAssemblyGenerator (new Version (11, 0, 0, 0)); - generator.Generate (peers, stream, assemblyName ?? "TestTypeMap"); + generator.Generate (peers, stream, assemblyName); stream.Position = 0; return stream; } - static (PEReader pe, MetadataReader reader) OpenAssembly (Stream stream) + [Fact] + public void Generate_ProducesValidPEAssembly () { - var pe = new PEReader (stream); - return (pe, pe.GetMetadataReader ()); + var peers = ScanFixtures (); + using var stream = GenerateAssembly (peers); + using var pe = new PEReader (stream); + Assert.True (pe.HasMetadata); + var reader = pe.GetMetadataReader (); + Assert.NotNull (reader); } - public class BasicAssemblyStructure : FixtureTestBase + [Fact] + public void Generate_HasRequiredAssemblyReferences () { - - [Fact] - public void Generate_ProducesValidPEAssembly () - { - var peers = ScanFixtures (); - using var stream = GenerateAssembly (peers); - var (pe, reader) = OpenAssembly (stream); - using (pe) { - Assert.True (pe.HasMetadata); - Assert.NotNull (reader); - } - } - - } - - public class AssemblyReference : FixtureTestBase - { - - [Fact] - public void Generate_HasRequiredAssemblyReferences () - { - var peers = ScanFixtures (); - using var stream = GenerateAssembly (peers); - var (pe, reader) = OpenAssembly (stream); - using (pe) { - var asmRefs = reader.AssemblyReferences - .Select (h => reader.GetString (reader.GetAssemblyReference (h).Name)) - .ToList (); - Assert.Contains ("System.Runtime", asmRefs); - Assert.Contains ("Mono.Android", asmRefs); - Assert.Contains ("Java.Interop", asmRefs); - Assert.Contains ("System.Runtime.InteropServices", asmRefs); - } - } - + var peers = ScanFixtures (); + using var stream = GenerateAssembly (peers); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + var asmRefs = reader.AssemblyReferences + .Select (h => reader.GetString (reader.GetAssemblyReference (h).Name)) + .ToList (); + Assert.Contains ("System.Runtime", asmRefs); + Assert.Contains ("Mono.Android", asmRefs); + Assert.Contains ("Java.Interop", asmRefs); + Assert.Contains ("System.Runtime.InteropServices", asmRefs); } - public class ProxyType : FixtureTestBase + [Fact] + public void Generate_CreatesProxyTypes () { - - [Fact] - public void Generate_CreatesProxyTypes () - { - var peers = ScanFixtures (); - using var stream = GenerateAssembly (peers); - var (pe, reader) = OpenAssembly (stream); - using (pe) { - var proxyTypes = reader.TypeDefinitions - .Select (h => reader.GetTypeDefinition (h)) - .Where (t => reader.GetString (t.Namespace) == "_TypeMap.Proxies") - .ToList (); - - Assert.NotEmpty (proxyTypes); - Assert.Contains (proxyTypes, t => reader.GetString (t.Name) == "Java_Lang_Object_Proxy"); - } - } - - [Fact] - public void Generate_ProxyType_HasCtorAndCreateInstance () - { - var peers = ScanFixtures (); - using var stream = GenerateAssembly (peers); - var (pe, reader) = OpenAssembly (stream); - using (pe) { - var objectProxy = reader.TypeDefinitions - .Select (h => reader.GetTypeDefinition (h)) - .First (t => reader.GetString (t.Name) == "Java_Lang_Object_Proxy"); - - var methods = objectProxy.GetMethods () - .Select (h => reader.GetMethodDefinition (h)) - .Select (m => reader.GetString (m.Name)) - .ToList (); - - Assert.Contains (".ctor", methods); - Assert.Contains ("CreateInstance", methods); - Assert.Contains ("get_TargetType", methods); - } - } - + var peers = ScanFixtures (); + using var stream = GenerateAssembly (peers); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + var proxyTypes = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .Where (t => reader.GetString (t.Namespace) == "_TypeMap.Proxies") + .ToList (); + + Assert.NotEmpty (proxyTypes); + Assert.Contains (proxyTypes, t => reader.GetString (t.Name) == "Java_Lang_Object_Proxy"); } - public class AcwProxy : FixtureTestBase + [Fact] + public void Generate_ProxyType_HasCtorAndCreateInstance () { - - [Fact] - public void Generate_AcwProxy_HasRegisterNativesAndUcoMethods () - { - var peers = ScanFixtures (); - var acwPeer = peers.First (p => p.JavaName == "my/app/TouchHandler"); - using var stream = GenerateAssembly (new [] { acwPeer }, "AcwTest"); - var (pe, reader) = OpenAssembly (stream); - 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"); - using var stream = GenerateAssembly (new [] { acwPeer }, "UcoTest"); - var (pe, reader) = OpenAssembly (stream); - 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); - } - } - + var peers = ScanFixtures (); + using var stream = GenerateAssembly (peers); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + var objectProxy = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .First (t => reader.GetString (t.Name) == "Java_Lang_Object_Proxy"); + + var methods = objectProxy.GetMethods () + .Select (h => reader.GetMethodDefinition (h)) + .Select (m => reader.GetString (m.Name)) + .ToList (); + + Assert.Contains (".ctor", methods); + Assert.Contains ("CreateInstance", methods); + Assert.Contains ("get_TargetType", methods); } - public class IgnoresAccessChecksTo : FixtureTestBase + [Fact] + public void Generate_HasIgnoresAccessChecksToAttribute () { - - [Fact] - public void Generate_HasIgnoresAccessChecksToAttribute () - { - var peers = ScanFixtures (); - using var stream = GenerateAssembly (peers); - var (pe, reader) = OpenAssembly (stream); - using (pe) { - var types = reader.TypeDefinitions - .Select (h => reader.GetTypeDefinition (h)) - .ToList (); - Assert.Contains (types, t => - reader.GetString (t.Name) == "IgnoresAccessChecksToAttribute" && - reader.GetString (t.Namespace) == "System.Runtime.CompilerServices"); - } - } - + var peers = ScanFixtures (); + using var stream = GenerateAssembly (peers); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + var types = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .ToList (); + Assert.Contains (types, t => + reader.GetString (t.Name) == "IgnoresAccessChecksToAttribute" && + reader.GetString (t.Namespace) == "System.Runtime.CompilerServices"); } - public class Alias : FixtureTestBase + [Fact] + public void Generate_DuplicateJniNames_CreatesAliasEntriesAndAssociationAttribute () { - - static List MakeDuplicateAliasPeers () => new List { + var peers = new List { new JavaPeerInfo { JavaName = "test/Duplicate", CompatJniName = "test/Duplicate", @@ -214,410 +127,381 @@ public class Alias : FixtureTestBase }, }; - [Fact] - public void Generate_DuplicateJniNames_CreatesAliasEntries () - { - var peers = MakeDuplicateAliasPeers (); - using var stream = GenerateAssembly (peers, "AliasTest"); - var (pe, reader) = OpenAssembly (stream); - using (pe) { - var assemblyAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); - Assert.True (assemblyAttrs.Count () >= 3); - } - } - - [Fact] - public void Generate_DuplicateJniNames_EmitsTypeMapAssociationAttribute () - { - var peers = MakeDuplicateAliasPeers (); - using var stream = GenerateAssembly (peers, "AliasAssocTest"); - var (pe, reader) = OpenAssembly (stream); - using (pe) { - var memberRefs = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) - .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i))) - .Where (m => reader.GetString (m.Name) == ".ctor") - .ToList (); - - var typeNames = GetTypeRefNames (reader); - Assert.Contains ("TypeMapAssociationAttribute", typeNames); - } - } + using var stream = GenerateAssembly (peers, "AliasTest"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + var assemblyAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); + Assert.True (assemblyAttrs.Count () >= 3); + var typeNames = GetTypeRefNames (reader); + Assert.Contains ("TypeMapAssociationAttribute", typeNames); } - public class EmptyInput : FixtureTestBase + [Fact] + public void Generate_EmptyPeerList_ProducesValidAssembly () { - - [Fact] - public void Generate_EmptyPeerList_ProducesValidAssembly () - { - using var stream = GenerateAssembly ([], "EmptyTest"); - var (pe, reader) = OpenAssembly (stream); - using (pe) { - Assert.NotNull (reader); - var asmDef = reader.GetAssemblyDefinition (); - Assert.Equal ("EmptyTest", reader.GetString (asmDef.Name)); - } - } - + using var stream = GenerateAssembly ([], "EmptyTest"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + Assert.NotNull (reader); + var asmDef = reader.GetAssemblyDefinition (); + Assert.Equal ("EmptyTest", reader.GetString (asmDef.Name)); } - public class JniSignatureHelperTests + [Fact] + public void Generate_SimpleActivity_UsesGetUninitializedObject () { - - [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)); - } - + var peers = ScanFixtures (); + var simpleActivity = peers.First (p => p.JavaName == "my/app/SimpleActivity"); + Assert.NotNull (simpleActivity.ActivationCtor); + Assert.NotEqual (simpleActivity.ManagedTypeName, simpleActivity.ActivationCtor.DeclaringTypeName); + + using var stream = GenerateAssembly (new [] { simpleActivity }, "InheritedCtorTest"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + var typeNames = GetTypeRefNames (reader); + Assert.Contains ("RuntimeHelpers", typeNames); + + var memberNames = GetMemberRefNames (reader); + Assert.DoesNotContain ("CreateManagedPeer", memberNames); + Assert.Contains ("GetUninitializedObject", memberNames); } - public class NegativeEdgeCase + [Fact] + public void Generate_LeafCtor_DoesNotUseCreateManagedPeer () { + var peers = ScanFixtures (); + // ClickableView has its own (IntPtr, JniHandleOwnership) ctor + var clickableView = peers.First (p => p.JavaName == "my/app/ClickableView"); + Assert.NotNull (clickableView.ActivationCtor); + Assert.Equal (clickableView.ManagedTypeName, clickableView.ActivationCtor.DeclaringTypeName); + + using var stream = GenerateAssembly (new [] { clickableView }, "LeafCtorTest"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + var memberNames = GetMemberRefNames (reader); + Assert.DoesNotContain ("CreateManagedPeer", memberNames); + + var ctorRefs = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) + .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i))) + .Where (m => reader.GetString (m.Name) == ".ctor") + .ToList (); + Assert.True (ctorRefs.Count >= 2, "Should have ctor refs for proxy base + target type"); + } - [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 Generate_GenericType_ThrowsNotSupportedException () + { + var peers = ScanFixtures (); + var generic = peers.First (p => p.JavaName == "my/app/GenericHolder"); + Assert.True (generic.IsGenericDefinition); + + using var stream = GenerateAssembly (new [] { generic }, "GenericTest"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + var typeNames = GetTypeRefNames (reader); + Assert.Contains ("NotSupportedException", typeNames); + } - [Fact] - public void ParseParameterTypes_UnterminatedSignature_ReturnsEmptyList () - { - Assert.Empty (JniSignatureHelper.ParseParameterTypes ("(")); + [Fact] + public void Generate_InheritedCtor_IncludesBaseCtorAssembly () + { + // SimpleActivity inherits activation ctor from Activity — both in TestFixtures + // but the generated assembly is "IgnoresAccessTest", so TestFixtures must be + // in IgnoresAccessChecksTo + var peers = ScanFixtures (); + var simpleActivity = peers.First (p => p.JavaName == "my/app/SimpleActivity"); + + using var stream = GenerateAssembly (new [] { simpleActivity }, "IgnoresAccessTest"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + var ignoresAttrType = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .FirstOrDefault (t => reader.GetString (t.Name) == "IgnoresAccessChecksToAttribute"); + Assert.True (ignoresAttrType.Attributes != 0, "IgnoresAccessChecksToAttribute should be defined"); + + var assemblyAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); + var attrBlobs = new List (); + foreach (var attrHandle in assemblyAttrs) { + var attr = reader.GetCustomAttribute (attrHandle); + var blob = reader.GetBlobBytes (attr.Value); + var blobStr = System.Text.Encoding.UTF8.GetString (blob); + attrBlobs.Add (blobStr); } + // Activity is in TestFixtures, so IgnoresAccessChecksTo must include TestFixtures + Assert.Contains (attrBlobs, b => b.Contains ("TestFixtures")); + } + [Fact] + public void Generate_JiStyleCtor_EmitsJavaInteropActivation () + { + var peers = ScanFixtures (); + var jiPeer = peers.First (p => p.JavaName == "my/app/JiStylePeer"); + Assert.NotNull (jiPeer.ActivationCtor); + Assert.Equal (ActivationCtorStyle.JavaInterop, jiPeer.ActivationCtor.Style); + + using var stream = GenerateAssembly (new [] { jiPeer }, "JiStyleTest"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + // JI-style activation should emit JniObjectReference and JniObjectReferenceOptions type refs + var typeNames = GetTypeRefNames (reader); + Assert.Contains ("JniObjectReference", typeNames); + Assert.Contains ("JniObjectReferenceOptions", typeNames); + + // The proxy still exists (with a TargetType property) + var proxyTypes = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .Where (t => reader.GetString (t.Namespace) == "_TypeMap.Proxies") + .ToList (); + Assert.Single (proxyTypes); } - public class CreateInstancePaths : FixtureTestBase + [Fact] + public void Emit_CalledTwice_Throws () { + var model = ModelBuilder.Build ([], "Double.dll", "Double"); + var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); + emitter.Emit (model, new MemoryStream ()); + // MetadataBuilder.AddAssembly throws on second call (only one assembly definition per PE) + Assert.ThrowsAny (() => emitter.Emit (model, new MemoryStream ())); + } - [Fact] - public void Generate_SimpleActivity_UsesGetUninitializedObject () - { - var peers = ScanFixtures (); - var simpleActivity = peers.First (p => p.JavaName == "my/app/SimpleActivity"); - Assert.NotNull (simpleActivity.ActivationCtor); - Assert.NotEqual (simpleActivity.ManagedTypeName, simpleActivity.ActivationCtor.DeclaringTypeName); - - using var stream = GenerateAssembly (new [] { simpleActivity }, "InheritedCtorTest"); - var (pe, reader) = OpenAssembly (stream); - using (pe) { - var typeNames = GetTypeRefNames (reader); - Assert.Contains ("RuntimeHelpers", typeNames); - - var memberNames = GetMemberRefNames (reader); - Assert.DoesNotContain ("CreateManagedPeer", memberNames); - Assert.Contains ("GetUninitializedObject", memberNames); - } - } + [Fact] + public void EmitBody_ILCallbackCallsAddMemberRef_SignatureNotCorrupted () + { + // Regression test: EmitBody uses shared _sigBlob for the method signature. + // If the emitIL callback calls AddMemberRef (which also uses _sigBlob), + // the method signature must not be corrupted. + var pe = new PEAssemblyBuilder (new Version (11, 0, 0, 0)); + pe.EmitPreamble ("SigTest", "SigTest.dll"); + + var objectRef = pe.Metadata.AddTypeReference (pe.SystemRuntimeRef, + pe.Metadata.GetOrAddString ("System"), pe.Metadata.GetOrAddString ("Object")); + + // already defined; add a type to host the method + pe.Metadata.AddTypeDefinition ( + System.Reflection.TypeAttributes.Public | System.Reflection.TypeAttributes.Class, + pe.Metadata.GetOrAddString ("Test"), + pe.Metadata.GetOrAddString ("MyType"), + objectRef, + MetadataTokens.FieldDefinitionHandle (pe.Metadata.GetRowCount (TableIndex.Field) + 1), + MetadataTokens.MethodDefinitionHandle (pe.Metadata.GetRowCount (TableIndex.MethodDef) + 1)); + + // EmitBody with an IL callback that calls AddMemberRef (clearing _sigBlob) + pe.EmitBody ("TestMethod", + MethodAttributes.Public | MethodAttributes.Static, + sig => sig.MethodSignature ().Parameters (1, + rt => rt.Void (), + p => p.AddParameter ().Type ().Int32 ()), + encoder => { + // This AddMemberRef call clears and repopulates _sigBlob + pe.AddMemberRef (objectRef, ".ctor", + s => s.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { })); + encoder.OpCode (ILOpCode.Ret); + }); + + // If the sig blob was corrupted, the PE metadata will have a wrong signature. + // Write and read back to verify. + var stream = new MemoryStream (); + pe.WritePE (stream); + stream.Position = 0; - [Fact] - public void Generate_LeafCtor_DoesNotUseCreateManagedPeer () - { - var peers = ScanFixtures (); - // ClickableView has its own (IntPtr, JniHandleOwnership) ctor - var clickableView = peers.First (p => p.JavaName == "my/app/ClickableView"); - Assert.NotNull (clickableView.ActivationCtor); - Assert.Equal (clickableView.ManagedTypeName, clickableView.ActivationCtor.DeclaringTypeName); - - using var stream = GenerateAssembly (new [] { clickableView }, "LeafCtorTest"); - var (pe, reader) = OpenAssembly (stream); - using (pe) { - var memberNames = GetMemberRefNames (reader); - Assert.DoesNotContain ("CreateManagedPeer", memberNames); - - var ctorRefs = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) - .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i))) - .Where (m => reader.GetString (m.Name) == ".ctor") - .ToList (); - Assert.True (ctorRefs.Count >= 2, "Should have ctor refs for proxy base + target type"); - } - } + using var peReader = new PEReader (stream); + var reader = peReader.GetMetadataReader (); + var methods = reader.TypeDefinitions + .SelectMany (h => reader.GetTypeDefinition (h).GetMethods ()) + .Select (h => reader.GetMethodDefinition (h)) + .ToList (); + + var testMethod = methods.First (m => reader.GetString (m.Name) == "TestMethod"); + var sig = testMethod.DecodeSignature (SignatureTypeProvider.Instance, null); + var paramType = Assert.Single (sig.ParameterTypes); + Assert.Equal ("System.Int32", paramType); + } - [Fact] - public void Generate_GenericType_ThrowsNotSupportedException () - { - var peers = ScanFixtures (); - var generic = peers.First (p => p.JavaName == "my/app/GenericHolder"); - Assert.True (generic.IsGenericDefinition); - - using var stream = GenerateAssembly (new [] { generic }, "GenericTest"); - var (pe, reader) = OpenAssembly (stream); - using (pe) { - var typeNames = GetTypeRefNames (reader); - Assert.Contains ("NotSupportedException", typeNames); + [Fact] + public void Generate_JiStyleCtor_FirstParamIsByRef () + { + var peers = ScanFixtures (); + var jiPeer = peers.First (p => p.JavaName == "my/app/JiStylePeer"); + Assert.Equal (ActivationCtorStyle.JavaInterop, jiPeer.ActivationCtor!.Style); + + using var stream = GenerateAssembly (new [] { jiPeer }, "JiByRefTest"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + // Find the .ctor member reference whose parent type is the JI peer's declaring type + var ctorRefs = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) + .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i))) + .Where (m => reader.GetString (m.Name) == ".ctor") + .ToList (); + + // Decode each .ctor signature and find the JI-style one (2 params, first is byref JniObjectReference) + bool foundByRefCtor = false; + foreach (var ctor in ctorRefs) { + var sig = ctor.DecodeMethodSignature (SignatureTypeProvider.Instance, null); + if (sig.ParameterTypes.Length == 2 && + sig.ParameterTypes [0].Contains ("JniObjectReference")) { + // The byref encoding should produce "Java.Interop.JniObjectReference&" + Assert.True (sig.ParameterTypes [0].EndsWith ("&"), + $"JI-style .ctor first param must be byref, got: {sig.ParameterTypes [0]}"); + foundByRefCtor = true; } } + Assert.True (foundByRefCtor, "Expected to find a .ctor with byref JniObjectReference parameter"); + } + [Fact] + public void Generate_JiStyleCtor_EmitsDeleteRefCall () + { + var peers = ScanFixtures (); + var jiPeer = peers.First (p => p.JavaName == "my/app/JiStylePeer"); + + using var stream = GenerateAssembly (new [] { jiPeer }, "JiDeleteRefTest"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + // The JI-style activation path must emit a call to JNIEnv.DeleteRef(IntPtr, JniHandleOwnership) + // to match the legacy TypeManager.CreateProxy behavior. + var memberRefs = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) + .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i))) + .ToList (); + + var deleteRefRef = memberRefs.FirstOrDefault (m => reader.GetString (m.Name) == "DeleteRef"); + Assert.True (!deleteRefRef.Equals (default (MemberReference)), + "JI-style activation must emit a DeleteRef member reference for JNI handle cleanup"); + + // Verify it's on the JNIEnv type + var parentTypeRef = reader.GetTypeReference ((TypeReferenceHandle)deleteRefRef.Parent); + Assert.Equal ("JNIEnv", reader.GetString (parentTypeRef.Name)); + Assert.Equal ("Android.Runtime", reader.GetString (parentTypeRef.Namespace)); } - public class IgnoresAccessChecksToForBaseCtor : FixtureTestBase + [Fact] + public void Generate_DifferentContent_ProducesDifferentMVIDs () { + var peer1 = MakePeerWithActivation ("test/TypeA", "Test.TypeA", "TestAsm"); + var peer2 = MakePeerWithActivation ("test/TypeB", "Test.TypeB", "TestAsm"); - [Fact] - public void Generate_InheritedCtor_IncludesBaseCtorAssembly () - { - // SimpleActivity inherits activation ctor from Activity — both in TestFixtures - // but the generated assembly is "IgnoresAccessTest", so TestFixtures must be - // in IgnoresAccessChecksTo - var peers = ScanFixtures (); - var simpleActivity = peers.First (p => p.JavaName == "my/app/SimpleActivity"); - - using var stream = GenerateAssembly (new [] { simpleActivity }, "IgnoresAccessTest"); - var (pe, reader) = OpenAssembly (stream); - using (pe) { - var ignoresAttrType = reader.TypeDefinitions - .Select (h => reader.GetTypeDefinition (h)) - .FirstOrDefault (t => reader.GetString (t.Name) == "IgnoresAccessChecksToAttribute"); - Assert.True (ignoresAttrType.Attributes != 0, "IgnoresAccessChecksToAttribute should be defined"); - - var assemblyAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); - var attrBlobs = new List (); - foreach (var attrHandle in assemblyAttrs) { - var attr = reader.GetCustomAttribute (attrHandle); - var blob = reader.GetBlobBytes (attr.Value); - var blobStr = System.Text.Encoding.UTF8.GetString (blob); - attrBlobs.Add (blobStr); - } - // Activity is in TestFixtures, so IgnoresAccessChecksTo must include TestFixtures - Assert.Contains (attrBlobs, b => b.Contains ("TestFixtures")); - } - } + using var stream1 = GenerateAssembly (new [] { peer1 }, "SameName"); + using var stream2 = GenerateAssembly (new [] { peer2 }, "SameName"); + using var pe1 = new PEReader (stream1); + using var pe2 = new PEReader (stream2); + var mvid1 = pe1.GetMetadataReader ().GetGuid (pe1.GetMetadataReader ().GetModuleDefinition ().Mvid); + var mvid2 = pe2.GetMetadataReader ().GetGuid (pe2.GetMetadataReader ().GetModuleDefinition ().Mvid); + + Assert.NotEqual (mvid1, mvid2); } - public class EmitterBehavior + [Fact] + public void Generate_IdenticalContent_ProducesIdenticalMVIDs () { + var peer = MakePeerWithActivation ("test/TypeA", "Test.TypeA", "TestAsm"); - [Fact] - public void Emit_CalledTwice_Throws () - { - var model = ModelBuilder.Build ([], "Double.dll", "Double"); - var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); - emitter.Emit (model, new MemoryStream ()); - // MetadataBuilder.AddAssembly throws on second call (only one assembly definition per PE) - Assert.ThrowsAny (() => emitter.Emit (model, new MemoryStream ())); - } + using var stream1 = GenerateAssembly (new [] { peer }, "SameName"); + using var stream2 = GenerateAssembly (new [] { peer }, "SameName"); - [Fact] - public void EmitBody_ILCallbackCallsAddMemberRef_SignatureNotCorrupted () - { - // Regression test: EmitBody uses shared _sigBlob for the method signature. - // If the emitIL callback calls AddMemberRef (which also uses _sigBlob), - // the method signature must not be corrupted. - var pe = new PEAssemblyBuilder (new Version (11, 0, 0, 0)); - pe.EmitPreamble ("SigTest", "SigTest.dll"); - - var objectRef = pe.Metadata.AddTypeReference (pe.SystemRuntimeRef, - pe.Metadata.GetOrAddString ("System"), pe.Metadata.GetOrAddString ("Object")); - - // already defined; add a type to host the method - pe.Metadata.AddTypeDefinition ( - System.Reflection.TypeAttributes.Public | System.Reflection.TypeAttributes.Class, - pe.Metadata.GetOrAddString ("Test"), - pe.Metadata.GetOrAddString ("MyType"), - objectRef, - MetadataTokens.FieldDefinitionHandle (pe.Metadata.GetRowCount (TableIndex.Field) + 1), - MetadataTokens.MethodDefinitionHandle (pe.Metadata.GetRowCount (TableIndex.MethodDef) + 1)); - - // EmitBody with an IL callback that calls AddMemberRef (clearing _sigBlob) - pe.EmitBody ("TestMethod", - MethodAttributes.Public | MethodAttributes.Static, - sig => sig.MethodSignature ().Parameters (1, - rt => rt.Void (), - p => p.AddParameter ().Type ().Int32 ()), - encoder => { - // This AddMemberRef call clears and repopulates _sigBlob - pe.AddMemberRef (objectRef, ".ctor", - s => s.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { })); - encoder.OpCode (ILOpCode.Ret); - }); - - // If the sig blob was corrupted, the PE metadata will have a wrong signature. - // Write and read back to verify. - var stream = new MemoryStream (); - pe.WritePE (stream); - stream.Position = 0; - - using var peReader = new PEReader (stream); - var reader = peReader.GetMetadataReader (); - var methods = reader.TypeDefinitions - .SelectMany (h => reader.GetTypeDefinition (h).GetMethods ()) - .Select (h => reader.GetMethodDefinition (h)) - .ToList (); - - var testMethod = methods.First (m => reader.GetString (m.Name) == "TestMethod"); - var sig = testMethod.DecodeSignature (SignatureTypeProvider.Instance, null); - var paramType = Assert.Single (sig.ParameterTypes); - Assert.Equal ("System.Int32", paramType); - } + using var pe1 = new PEReader (stream1); + using var pe2 = new PEReader (stream2); + var mvid1 = pe1.GetMetadataReader ().GetGuid (pe1.GetMetadataReader ().GetModuleDefinition ().Mvid); + var mvid2 = pe2.GetMetadataReader ().GetGuid (pe2.GetMetadataReader ().GetModuleDefinition ().Mvid); + Assert.Equal (mvid1, mvid2); } - public class DeterminismTests : FixtureTestBase + [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_DifferentContent_ProducesDifferentMVIDs () - { - var peer1 = MakePeerWithActivation ("test/TypeA", "Test.TypeA", "TestAsm"); - var peer2 = MakePeerWithActivation ("test/TypeB", "Test.TypeB", "TestAsm"); - - using var stream1 = GenerateAssembly (new [] { peer1 }, "SameName"); - using var stream2 = GenerateAssembly (new [] { peer2 }, "SameName"); - - var (pe1, reader1) = OpenAssembly (stream1); - var (pe2, reader2) = OpenAssembly (stream2); - using (pe1) - using (pe2) { - var mvid1 = reader1.GetGuid (reader1.GetModuleDefinition ().Mvid); - var mvid2 = reader2.GetGuid (reader2.GetModuleDefinition ().Mvid); - Assert.NotEqual (mvid1, mvid2); - } - } - - [Fact] - public void Generate_IdenticalContent_ProducesIdenticalMVIDs () - { - var peer = MakePeerWithActivation ("test/TypeA", "Test.TypeA", "TestAsm"); - - using var stream1 = GenerateAssembly (new [] { peer }, "SameName"); - using var stream2 = GenerateAssembly (new [] { peer }, "SameName"); - - var (pe1, reader1) = OpenAssembly (stream1); - var (pe2, reader2) = OpenAssembly (stream2); - using (pe1) - using (pe2) { - var mvid1 = reader1.GetGuid (reader1.GetModuleDefinition ().Mvid); - var mvid2 = reader2.GetGuid (reader2.GetModuleDefinition ().Mvid); - Assert.Equal (mvid1, mvid2); - } - } - + [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_")); } - public class JavaInteropActivation : FixtureTestBase + [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); + } - [Fact] - public void Generate_JiStyleCtor_EmitsJavaInteropActivation () - { - var peers = ScanFixtures (); - var jiPeer = peers.First (p => p.JavaName == "my/app/JiStylePeer"); - Assert.NotNull (jiPeer.ActivationCtor); - Assert.Equal (ActivationCtorStyle.JavaInterop, jiPeer.ActivationCtor.Style); - - using var stream = GenerateAssembly (new [] { jiPeer }, "JiStyleTest"); - var (pe, reader) = OpenAssembly (stream); - using (pe) { - // JI-style activation should emit JniObjectReference and JniObjectReferenceOptions type refs - var typeNames = GetTypeRefNames (reader); - Assert.Contains ("JniObjectReference", typeNames); - Assert.Contains ("JniObjectReferenceOptions", typeNames); - - // The proxy still exists (with a TargetType property) - var proxyTypes = reader.TypeDefinitions - .Select (h => reader.GetTypeDefinition (h)) - .Where (t => reader.GetString (t.Namespace) == "_TypeMap.Proxies") - .ToList (); - Assert.Single (proxyTypes); - } - } + [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]); + } - [Fact] - public void Generate_JiStyleCtor_FirstParamIsByRef () - { - var peers = ScanFixtures (); - var jiPeer = peers.First (p => p.JavaName == "my/app/JiStylePeer"); - Assert.NotNull (jiPeer.ActivationCtor); - Assert.Equal (ActivationCtorStyle.JavaInterop, jiPeer.ActivationCtor.Style); - - using var stream = GenerateAssembly (new [] { jiPeer }, "JiByRefTest"); - var (pe, reader) = OpenAssembly (stream); - using (pe) { - // Find the .ctor member reference whose parent type is the JI peer's declaring type - var ctorRefs = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) - .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i))) - .Where (m => reader.GetString (m.Name) == ".ctor") - .ToList (); - - // Decode each .ctor signature and find the JI-style one (2 params, first is byref JniObjectReference) - bool foundByRefCtor = false; - foreach (var ctor in ctorRefs) { - var sig = ctor.DecodeMethodSignature (SignatureTypeProvider.Instance, null); - if (sig.ParameterTypes.Length == 2 && - sig.ParameterTypes [0].Contains ("JniObjectReference")) { - // The byref encoding should produce "Java.Interop.JniObjectReference&" - Assert.True (sig.ParameterTypes [0].EndsWith ("&"), - $"JI-style .ctor first param must be byref, got: {sig.ParameterTypes [0]}"); - foundByRefCtor = true; - } - } - Assert.True (foundByRefCtor, "Expected to find a .ctor with byref JniObjectReference parameter"); - } - } + [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 Generate_JiStyleCtor_EmitsDeleteRefCall () - { - var peers = ScanFixtures (); - var jiPeer = peers.First (p => p.JavaName == "my/app/JiStylePeer"); - - using var stream = GenerateAssembly (new [] { jiPeer }, "JiDeleteRefTest"); - var (pe, reader) = OpenAssembly (stream); - using (pe) { - // The JI-style activation path must emit a call to JNIEnv.DeleteRef(IntPtr, JniHandleOwnership) - // to match the legacy TypeManager.CreateProxy behavior. - var memberRefs = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) - .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i))) - .ToList (); - - var deleteRefRef = memberRefs.FirstOrDefault (m => reader.GetString (m.Name) == "DeleteRef"); - Assert.True (!deleteRefRef.Equals (default (MemberReference)), - "JI-style activation must emit a DeleteRef member reference for JNI handle cleanup"); - - // Verify it's on the JNIEnv type - var parentTypeRef = reader.GetTypeReference ((TypeReferenceHandle) deleteRefRef.Parent); - Assert.Equal ("JNIEnv", reader.GetString (parentTypeRef.Name)); - Assert.Equal ("Android.Runtime", reader.GetString (parentTypeRef.Namespace)); - } - } + [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 ("(")); + } } \ No newline at end of file From 97aad0eabbc24f74a57b3822e76a021f25e603fc Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 12 Mar 2026 07:18:49 +0100 Subject: [PATCH 13/21] Minimize ModelBuilderTests changes: restore from main, append UCO tests Restore main's test structure and append only new UCO tests as pure additions. Fix Implementor/EventDispatcher test to match new trimmability heuristic. Reduces diff from +730/-49 to +206/-4. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/TypeMapModelBuilderTests.cs | 997 +++++------------- 1 file changed, 259 insertions(+), 738 deletions(-) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 553ccc10807..ce3ea1362d4 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -12,14 +12,12 @@ public class ModelBuilderTests : FixtureTestBase { static TypeMapAssemblyData BuildModel (IReadOnlyList peers, string? assemblyName = null) { - var outputPath = Path.Combine ("/tmp", (assemblyName ?? "TestTypeMap") + ".dll"); + var outputPath = Path.Combine (Path.GetTempPath (), (assemblyName ?? "TestTypeMap") + ".dll"); return ModelBuilder.Build (peers, outputPath, assemblyName); } - public class BasicStructure { - [Fact] public void Build_EmptyPeers_ProducesEmptyModel () { @@ -30,58 +28,18 @@ public void Build_EmptyPeers_ProducesEmptyModel () Assert.Empty (model.ProxyTypes); } - [Fact] - public void Build_AssemblyNameDerivedFromOutputPath () - { - var model = ModelBuilder.Build ([], "/some/path/Foo.Bar.dll"); - Assert.Equal ("Foo.Bar", model.AssemblyName); - Assert.Equal ("Foo.Bar.dll", model.ModuleName); - } - - [Fact] - public void Build_ExplicitAssemblyName_OverridesOutputPath () - { - var model = ModelBuilder.Build ([], "/some/path/Foo.dll", "MyAssembly"); - Assert.Equal ("MyAssembly", model.AssemblyName); - } - - [Fact] - public void Build_ComputesIgnoresAccessChecksToFromCrossAssemblyCallbacks () + [Theory] + [InlineData ("Foo.Bar.dll", null, "Foo.Bar")] + [InlineData ("Foo.dll", "MyAssembly", "MyAssembly")] + public void Build_AssemblyName_ResolvedCorrectly (string outputPath, string? explicitName, string expected) { - var peer = MakeAcwPeer ("my/app/MainActivity", "MyApp.MainActivity", "MyApp") with { - MarshalMethods = new List { - new MarshalMethodInfo { - JniName = "", - NativeCallbackName = "n_ctor", - JniSignature = "()V", - ManagedMethodName = ".ctor", - JniReturnType = "V", - IsConstructor = true, - }, - new MarshalMethodInfo { - JniName = "onCreate", - NativeCallbackName = "n_OnCreate", - JniSignature = "(Landroid/os/Bundle;)V", - ManagedMethodName = "OnCreate", - JniReturnType = "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); + var model = ModelBuilder.Build ([], outputPath, explicitName); + Assert.Equal (expected, model.AssemblyName); } - } public class TypeMapEntries { - [Fact] public void Build_CreatesOneEntryPerPeer () { @@ -111,13 +69,14 @@ public void Build_DuplicateJniNames_CreatesAliasEntries () Assert.Contains ("Test.First", model.Entries [0].ProxyTypeReference); Assert.Equal ("test/Dup[1]", model.Entries [1].JniName); Assert.Contains ("Test.Second", model.Entries [1].ProxyTypeReference); - } + // No associations when neither peer has a proxy (no activation ctor or invoker) + Assert.Empty (model.Associations); + } } public class ConditionalAttributes { - [Theory] [InlineData ("java/lang/Object")] [InlineData ("java/lang/Throwable")] @@ -129,9 +88,7 @@ public class ConditionalAttributes [InlineData ("java/lang/Thread")] public void Build_AllEssentialRuntimeTypes_AreUnconditional (string jniName) { - var peer = MakeMcwPeer (jniName, "Java.Lang.SomeType", "Mono.Android") with { - DoNotGenerateAcw = true, - }; + var peer = MakeMcwPeer (jniName, "Java.Lang.SomeType", "Mono.Android") with { DoNotGenerateAcw = true }; var model = BuildModel (new [] { peer }); Assert.True (model.Entries [0].IsUnconditional, $"{jniName} should be unconditional"); } @@ -153,9 +110,7 @@ public void Build_UserAcwType_IsUnconditional () public void Build_McwBinding_IsTrimmable () { // MCW binding types (DoNotGenerateAcw=true) are trimmable unless essential - var peer = MakeMcwPeer ("android/app/Activity", "Android.App.Activity", "Mono.Android") with { - DoNotGenerateAcw = true, - }; + var peer = MakeMcwPeer ("android/app/Activity", "Android.App.Activity", "Mono.Android") with { DoNotGenerateAcw = true }; var model = BuildModel (new [] { peer }); Assert.Single (model.Entries); @@ -176,12 +131,10 @@ public void Build_UnconditionalScannedType_IsUnconditional () Assert.True (model.Entries [0].IsUnconditional); } - } public class Aliases { - [Fact] public void Build_AliasedPeersWithActivation_GetDistinctProxies () { @@ -206,25 +159,25 @@ public void Build_McwPeerWithoutActivation_NoProxy () Assert.Single (model.Entries); Assert.Contains ("Java.Lang.Object, Mono.Android", model.Entries [0].ProxyTypeReference); } - } public class ProxyTypes { - - [Fact] - public void Build_PeerWithActivationCtor_CreatesProxy () + [Theory] + [InlineData ("java/lang/Object", "Java.Lang.Object", "Mono.Android", "Java_Lang_Object_Proxy")] + [InlineData ("com/example/Outer$Inner", "Com.Example.Outer.Inner", "App", "Com_Example_Outer_Inner_Proxy")] + public void Build_PeerWithActivation_CreatesNamedProxy (string jniName, string managedName, string asmName, string expectedProxyName) { - var peer = MakePeerWithActivation ("java/lang/Object", "Java.Lang.Object", "Mono.Android"); + var peer = MakePeerWithActivation (jniName, managedName, asmName); var model = BuildModel (new [] { peer }, "MyTypeMap"); Assert.Single (model.ProxyTypes); var proxy = model.ProxyTypes [0]; - Assert.Equal ("Java_Lang_Object_Proxy", proxy.TypeName); + Assert.Equal (expectedProxyName, proxy.TypeName); Assert.Equal ("_TypeMap.Proxies", proxy.Namespace); Assert.True (proxy.HasActivation); - Assert.Equal ("Java.Lang.Object", proxy.TargetType.ManagedTypeName); - Assert.Equal ("Mono.Android", proxy.TargetType.AssemblyName); + Assert.Equal (managedName, proxy.TargetType.ManagedTypeName); + Assert.Equal (asmName, proxy.TargetType.AssemblyName); } [Fact] @@ -238,401 +191,10 @@ public void Build_PeerWithInvoker_CreatesProxy () Assert.NotNull (proxy.InvokerType); Assert.Equal ("Android.Views.View+IOnClickListenerInvoker", proxy.InvokerType!.ManagedTypeName); } - - [Fact] - public void Build_ProxyNaming_ReplacesDotAndPlus () - { - var peer = MakePeerWithActivation ("com/example/Outer$Inner", "Com.Example.Outer.Inner", "App"); - var model = BuildModel (new [] { peer }); - - Assert.Single (model.ProxyTypes); - Assert.Equal ("Com_Example_Outer_Inner_Proxy", model.ProxyTypes [0].TypeName); - } - - } - - 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", - CompatJniName = "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 { - new MarshalMethodInfo { - JniName = "onClick", - NativeCallbackName = "n_OnClick", - JniSignature = "(Landroid/view/View;)V", - ManagedMethodName = "OnClick", - JniReturnType = "V", - IsConstructor = false, - }, - }, - }; - - 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") with { - DoNotGenerateAcw = true, - MarshalMethods = new List { - new MarshalMethodInfo { - JniName = "toString", - NativeCallbackName = "n_ToString", - JniSignature = "()Ljava/lang/String;", - ManagedMethodName = "ToString", - JniReturnType = "Ljava/lang/String;", - IsConstructor = false, - }, - }, - }; - - 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") with { - MarshalMethods = new List { - new MarshalMethodInfo { - JniName = "", - NativeCallbackName = "n_ctor", - JniSignature = "()V", - ManagedMethodName = ".ctor", - JniReturnType = "V", - IsConstructor = true, - }, - new MarshalMethodInfo { - JniName = "onCreate", - NativeCallbackName = "n_OnCreate", - JniSignature = "(Landroid/os/Bundle;)V", - ManagedMethodName = "OnCreate", - JniReturnType = "V", - IsConstructor = false, - }, - new MarshalMethodInfo { - JniName = "onResume", - NativeCallbackName = "n_OnResume", - JniSignature = "()V", - ManagedMethodName = "OnResume", - JniReturnType = "V", - IsConstructor = false, - }, - }, - }; - - 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 = new MarshalMethodInfo { - JniName = "toString", - NativeCallbackName = "n_ToString", - JniSignature = "()Ljava/lang/String;", - ManagedMethodName = "ToString", - JniReturnType = "Ljava/lang/String;", - IsConstructor = false, - DeclaringTypeName = "Java.Lang.Object", - DeclaringAssemblyName = "Mono.Android", - }; - - var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App") with { - MarshalMethods = new List { - new MarshalMethodInfo { - JniName = "", - NativeCallbackName = "n_ctor", - JniSignature = "()V", - ManagedMethodName = ".ctor", - JniReturnType = "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") with { - MarshalMethods = new List { - new MarshalMethodInfo { - JniName = "", - NativeCallbackName = "n_ctor", - JniSignature = "()V", - ManagedMethodName = ".ctor", - JniReturnType = "V", - IsConstructor = true, - }, - new MarshalMethodInfo { - JniName = "onPause", - NativeCallbackName = "n_OnPause", - JniSignature = "()V", - ManagedMethodName = "OnPause", - JniReturnType = "V", - IsConstructor = false, - }, - }, - }; - - 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") with { - MarshalMethods = new List { - new MarshalMethodInfo { - JniName = "", - NativeCallbackName = "n_ctor", - JniSignature = "()V", - ManagedMethodName = ".ctor", - JniReturnType = "V", - IsConstructor = true, - }, - new MarshalMethodInfo { - JniName = "", - NativeCallbackName = "n_ctor2", - JniSignature = "()V", - ManagedMethodName = ".ctor", - JniReturnType = "V", - IsConstructor = true, - }, - new MarshalMethodInfo { - JniName = "onStart", - NativeCallbackName = "n_OnStart", - JniSignature = "()V", - ManagedMethodName = "OnStart", - JniReturnType = "V", - IsConstructor = false, - }, - }, - }; - - 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", - CompatJniName = "my/app/Foo", - ManagedTypeName = "MyApp.Foo", - ManagedTypeNamespace = "MyApp", - ManagedTypeShortName = "Foo", - AssemblyName = "App", - InvokerTypeName = "MyApp.FooInvoker", // has invoker → will create proxy - MarshalMethods = new List { - new MarshalMethodInfo { - JniName = "bar", - NativeCallbackName = "n_Bar", - JniSignature = "()V", - ManagedMethodName = "Bar", - JniReturnType = "V", - IsConstructor = false, - }, - }, - 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") with { - MarshalMethods = new List { - new MarshalMethodInfo { - JniName = "", - NativeCallbackName = "n_ctor", - JniSignature = "()V", - ManagedMethodName = ".ctor", - JniReturnType = "V", - IsConstructor = true, - }, - new MarshalMethodInfo { - JniName = "onCreate", - NativeCallbackName = "n_OnCreate", - JniSignature = "(Landroid/os/Bundle;)V", - ManagedMethodName = "OnCreate", - JniReturnType = "V", - IsConstructor = false, - }, - }, - }; - - 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") with { - 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;" }, - } - }, - }, - MarshalMethods = new List { - new MarshalMethodInfo { - JniName = "", - NativeCallbackName = "n_ctor", - JniSignature = "()V", - ManagedMethodName = ".ctor", - JniReturnType = "V", - IsConstructor = true, - }, - new MarshalMethodInfo { - JniName = "", - NativeCallbackName = "n_ctor", - JniSignature = "(Landroid/content/Context;)V", - ManagedMethodName = ".ctor", - JniReturnType = "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 { - [Fact] public void Build_FromScannedFixtures_ProducesValidModel () { @@ -658,26 +220,10 @@ public void ScanFixtures_ManagedTypeShortName_IsCorrect (string javaName, string var peer = FindFixtureByJavaName (javaName); 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 { - [Theory] [InlineData ("my/app/MainActivity")] [InlineData ("my/app/TouchHandler")] @@ -699,7 +245,6 @@ public void Fixture_McwBinding_IsTrimmable (string javaName) var model = BuildModel (new [] { peer }); Assert.False (model.Entries [0].IsUnconditional); } - } static JavaPeerProxyData? FindProxy (TypeMapAssemblyData model, string proxyTypeName) @@ -712,10 +257,8 @@ public void Fixture_McwBinding_IsTrimmable (string javaName) return model.Entries.FirstOrDefault (e => e.JniName == jniName); } - public class FixtureMcwTypes { - [Theory] [InlineData ("java/lang/Object", "Java_Lang_Object_Proxy", "Java.Lang.Object")] [InlineData ("android/app/Activity", "Android_App_Activity_Proxy", "Android.App.Activity")] @@ -730,11 +273,6 @@ 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] @@ -750,136 +288,36 @@ public void Fixture_Activity_Entry_PointsToProxy () } [Fact] - public void Fixture_Service_NoActivation_NoProxy () - { - // Service in fixtures has no activation ctor on its own — it inherits from J.L.Object - // but Service itself has `protected Service(IntPtr, JniHandleOwnership)` which IS an activation ctor - var peer = FindFixtureByJavaName ("android/app/Service"); - var model = BuildModel (new [] { peer }, "TypeMap"); - - if (peer.ActivationCtor != null) { - Assert.Single (model.ProxyTypes); - } else { - Assert.Empty (model.ProxyTypes); - } - } - } - - 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"); - Assert.NotNull (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 () + public void Fixture_Service_NoActivation_NoProxy () { - var peer = FindFixtureByJavaName ("my/app/TouchHandler"); + // Service in fixtures has no activation ctor on its own — it inherits from J.L.Object + // but Service itself has `protected Service(IntPtr, JniHandleOwnership)` which IS an activation ctor + var peer = FindFixtureByJavaName ("android/app/Service"); 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); + if (peer.ActivationCtor != null) { + Assert.Single (model.ProxyTypes); + } else { + Assert.Empty (model.ProxyTypes); + } } - } public class FixtureCustomView { - [Fact] - public void Fixture_CustomView_HasTwoConstructorWrappers () + public void Fixture_CustomView_HasTwoConstructors () { 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); - } } - } public class FixtureInterfaces { - [Fact] public void Fixture_IOnClickListener_HasInvokerProxy () { @@ -895,12 +333,10 @@ public void Fixture_IOnClickListener_HasInvokerProxy () Assert.NotNull (proxy!.InvokerType); Assert.Equal ("Android.Views.IOnClickListenerInvoker", proxy.InvokerType!.ManagedTypeName); } - } public class FixtureNestedTypes { - [Theory] [InlineData ("my/app/Outer$Inner", "MyApp_Outer_Inner_Proxy", "MyApp.Outer+Inner")] [InlineData ("my/app/ICallback$Result", "MyApp_ICallback_Result_Proxy", "MyApp.ICallback+Result")] @@ -918,12 +354,10 @@ public void Fixture_NestedType_ProxyNaming (string javaName, string expectedProx Assert.Equal (expectedManagedName, proxy!.TargetType.ManagedTypeName); } } - } public class FixtureInvokers { - [Fact] public void Fixture_InterfaceAndInvoker_ShareJniName_InvokerSeparated () { @@ -952,9 +386,7 @@ public void Build_InvokerType_NoProxyNoEntry () // Invoker types should never get their own proxy or TypeMap entry. // They only appear as a TypeRef in the interface proxy's InvokerType/CreateInstance. var ifacePeer = MakeInterfacePeer ("my/app/IFoo", "MyApp.IFoo", "App", "MyApp.FooInvoker"); - var invokerPeer = MakePeerWithActivation ("my/app/IFoo", "MyApp.FooInvoker", "App") with { - DoNotGenerateAcw = true, - }; + var invokerPeer = MakePeerWithActivation ("my/app/IFoo", "MyApp.FooInvoker", "App") with { DoNotGenerateAcw = true }; var model = BuildModel (new [] { ifacePeer, invokerPeer }); @@ -972,12 +404,10 @@ public void Build_InvokerType_NoProxyNoEntry () // Interface proxy has activation because it will create the invoker Assert.True (proxy.HasActivation); } - } public class FixtureGenericHolder { - [Fact] public void Fixture_GenericHolder_Entry () { @@ -988,12 +418,10 @@ public void Fixture_GenericHolder_Entry () var entry = FindEntry (model, "my/app/GenericHolder"); Assert.NotNull (entry); } - } public class FixtureAcwTypeHasProxy { - [Theory] [InlineData ("my/app/AbstractBase", "MyApp_AbstractBase_Proxy")] [InlineData ("my/app/ClickableView", "MyApp_ClickableView_Proxy")] @@ -1006,47 +434,15 @@ public void Fixture_AcwType_HasProxy (string javaName, string expectedProxyName) var model = BuildModel (new [] { peer }, "TypeMap"); - if (peer.ActivationCtor != null && peer.MarshalMethods.Count > 0) { + if (peer.ActivationCtor != null) { 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")); } } - } public class FixtureImplementorsAndDispatchers { - [Theory] [InlineData ("mono/android/view/View_IOnClickListenerImplementor", "Implementor")] [InlineData ("mono/android/view/View_ClickEventDispatcher", "EventDispatcher")] @@ -1060,39 +456,20 @@ public void Fixture_HelperType_IsTrimmable_NotUnconditional (string javaName, st var entry = model.Entries.FirstOrDefault (); Assert.NotNull (entry); - Assert.False (entry!.IsUnconditional, $"{kind} should NOT be unconditional"); - Assert.NotNull (entry.TargetTypeReference); + // Implementor/EventDispatcher types are only created from .NET (e.g., when a C# event + // is subscribed). They should NOT be unconditional — they're trimmable. + Assert.False (entry.IsUnconditional, $"{kind} should be trimmable, not unconditional"); } - } - public class NameBasedDetection + public class InvokerDetection { - - [Fact] - public void Build_UserTypeNamedImplementor_IsTreatedAsTrimmable () - { - // Limitation: name-based heuristic means a user type ending in "Implementor" - // will be treated as trimmable even if it's genuinely a user ACW type. - // This test documents the known behavior. - var peer = MakeAcwPeer ("my/app/MyImplementor", "MyApp.MyImplementor", "App"); - var model = BuildModel (new [] { peer }); - - var entry = model.Entries.FirstOrDefault (); - Assert.NotNull (entry); - // The heuristic treats this as an Implementor → trimmable (not unconditional) - Assert.False (entry!.IsUnconditional, - "Name-based heuristic: types ending in 'Implementor' are treated as trimmable"); - } - [Fact] public void Build_TypeIsInvoker_OnlyWhenReferencedByAnotherPeer () { // A type is only treated as an invoker when another peer's InvokerTypeName references it. // A type named "MyInvoker" with DoNotGenerateAcw is NOT automatically an invoker. - var invokerPeer = MakePeerWithActivation ("my/app/MyInvoker", "MyApp.MyInvoker", "App") with { - DoNotGenerateAcw = true, - }; + var invokerPeer = MakePeerWithActivation ("my/app/MyInvoker", "MyApp.MyInvoker", "App") with { DoNotGenerateAcw = true }; // Without a referencing peer, it gets a normal entry var model1 = BuildModel (new [] { invokerPeer }); @@ -1105,12 +482,10 @@ public void Build_TypeIsInvoker_OnlyWhenReferencedByAnotherPeer () Assert.Single (model2.Entries); Assert.Equal ("MyApp.IMyInterface", model2.ProxyTypes [0].TargetType.ManagedTypeName); } - } public class PipelineTests { - [Fact] public void FullPipeline_AllFixtures_ProducesLoadableAssembly () { @@ -1145,33 +520,27 @@ public void FullPipeline_AllFixtures_TypeMapAttributeCountMatchesEntries () var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); int totalAttrs = asmAttrs.Count (); - int expected = model.Entries.Count + model.IgnoresAccessChecksTo.Count; + int expected = model.Entries.Count + model.Associations.Count + model.IgnoresAccessChecksTo.Count; Assert.Equal (expected, totalAttrs); }); } [Fact] - public void FullPipeline_TouchHandler_AcwProxyHasUcoAttributes () + public void FullPipeline_AliasGroup_TypeMapAttributeCountIncludesAssociations () { - 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); + // Two peers with the same JNI name, both with activation → generates an association + var peers = new List { + MakePeerWithActivation ("test/Alias", "Test.Primary", "Asm"), + MakePeerWithActivation ("test/Alias", "Test.Secondary", "Asm"), + }; + var model = BuildModel (peers, "AliasAttrCount"); + Assert.NotEmpty (model.Associations); - foreach (var uco in ucoMethods) { - var attrs = uco.GetCustomAttributes ().Select (h => reader.GetCustomAttribute (h)).ToList (); - Assert.NotEmpty (attrs); - } + EmitAndVerify (model, "AliasAttrCount", (pe, reader) => { + var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); + int totalAttrs = asmAttrs.Count (); + int expected = model.Entries.Count + model.Associations.Count + model.IgnoresAccessChecksTo.Count; + Assert.Equal (expected, totalAttrs); }); } @@ -1193,47 +562,6 @@ 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); - } }); } @@ -1252,12 +580,10 @@ public void FullPipeline_GenericHolder_ProducesValidAssembly () Assert.NotEmpty (asmAttrs); }); } - } public class PeBlobValidation { - [Fact] public void FullPipeline_Mixed2ArgAnd3Arg_BothSurviveRoundTrip () { @@ -1323,12 +649,10 @@ public void FullPipeline_McwBinding_Emits3ArgAttribute () Assert.Contains ("Android.App.Activity", targetRef!); }); } - } public class DeterminismTests { - [Fact] public void Build_SameInput_ProducesDeterministicOutput () { @@ -1344,21 +668,16 @@ public void Build_SameInput_ProducesDeterministicOutput () Assert.Equal (model1.Entries [i].TargetTypeReference, model2.Entries [i].TargetTypeReference); } } - } static void EmitAndVerify (TypeMapAssemblyData model, string assemblyName, Action verify) { - var outputDir = CreateTempDir (); - try { - var outputPath = Path.Combine (outputDir, $"{assemblyName}.dll"); - var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); - emitter.Emit (model, outputPath); - using var pe = new PEReader (File.OpenRead (outputPath)); - verify (pe, pe.GetMetadataReader ()); - } finally { - DeleteTempDir (outputDir); - } + var stream = new MemoryStream (); + var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); + emitter.Emit (model, stream); + stream.Position = 0; + using var pe = new PEReader (stream); + verify (pe, pe.GetMetadataReader ()); } /// @@ -1417,4 +736,206 @@ static void EmitAndVerify (TypeMapAssemblyData model, string assemblyName, Actio } return result; } + + 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", + JniReturnType = "V", IsConstructor = true, + }, + new MarshalMethodInfo { + JniName = "onClick", NativeCallbackName = "n_OnClick", + JniSignature = "(Landroid/view/View;)V", ManagedMethodName = "OnClick", + JniReturnType = "V", + }, + }, + }; + 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", + JniReturnType = "V", 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", + JniReturnType = "V", + }, + }, + }; + 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", + JniReturnType = "V", IsConstructor = true, + }, + new MarshalMethodInfo { + JniName = "doWork", NativeCallbackName = "n_DoWork", + JniSignature = "(I)V", ManagedMethodName = "DoWork", + JniReturnType = "V", + }, + }, + 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); + } + } + + public class ImplementorTrimmability + { + [Fact] + public void Build_UserTypeNamedImplementor_IsTreatedAsTrimmable () + { + var peer = MakeAcwPeer ("my/app/MyImplementor", "MyApp.MyImplementor", "App") with { + DoNotGenerateAcw = false, + }; + var model = BuildModel (new [] { peer }); + var entry = model.Entries.FirstOrDefault (); + Assert.NotNull (entry); + Assert.False (entry.IsUnconditional, + "Name-based heuristic: types ending in 'Implementor' are treated as trimmable"); + } + } } \ No newline at end of file From 06eb978e065f66af1d25f0837aa7d92eaa1276d7 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 12 Mar 2026 08:29:02 +0100 Subject: [PATCH 14/21] =?UTF-8?q?Revert=20Implementor/EventDispatcher=20he?= =?UTF-8?q?uristic=20=E2=80=94=20deferred=20to=20#10911?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove IsImplementorOrEventDispatcher name-based heuristic and related test. This optimization is tracked in #10911 and should not be part of this PR. Restore main's behavior where these types are unconditional. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/ModelBuilder.cs | 20 ---------------- .../Generator/TypeMapModelBuilderTests.cs | 24 ++++--------------- 2 files changed, 4 insertions(+), 40 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index d97b2a9a93d..3cb37a217d6 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -148,12 +148,6 @@ static bool IsUnconditionalEntry (JavaPeerInfo peer) return true; } - // Implementor/EventDispatcher types are only created from .NET (e.g., when a C# event - // is subscribed). They should NOT be unconditional — they're trimmable. - if (IsImplementorOrEventDispatcher (peer)) { - return false; - } - // User-defined ACW types (not MCW bindings, not interfaces) are unconditional // because Android can instantiate them from Java at any time. if (!peer.DoNotGenerateAcw && !peer.IsInterface) { @@ -168,20 +162,6 @@ static bool IsUnconditionalEntry (JavaPeerInfo peer) return false; } - /// - /// Implementor and EventDispatcher types are generated by the binding generator - /// and are only instantiated from .NET. They should be trimmable. - /// NOTE: This is a name-based heuristic. Ideally the scanner would provide a dedicated flag. - /// User types whose names happen to end in "Implementor" or "EventDispatcher" would be - /// misclassified as trimmable. This is acceptable for now since such naming in user code - /// is unlikely and would only affect trimming behavior, not correctness. - /// - static bool IsImplementorOrEventDispatcher (JavaPeerInfo peer) - { - return peer.ManagedTypeName.EndsWith ("Implementor", StringComparison.Ordinal) || - peer.ManagedTypeName.EndsWith ("EventDispatcher", StringComparison.Ordinal); - } - static void AddIfCrossAssembly (SortedSet set, string? asmName, string outputAssemblyName) { if (asmName != null && !string.Equals (asmName, outputAssemblyName, StringComparison.Ordinal)) { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index ce3ea1362d4..a15c12343c1 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -446,7 +446,7 @@ public class FixtureImplementorsAndDispatchers [Theory] [InlineData ("mono/android/view/View_IOnClickListenerImplementor", "Implementor")] [InlineData ("mono/android/view/View_ClickEventDispatcher", "EventDispatcher")] - public void Fixture_HelperType_IsTrimmable_NotUnconditional (string javaName, string kind) + public void Fixture_HelperType_IsUnconditional (string javaName, string kind) { var peer = FindFixtureByJavaName (javaName); Assert.False (peer.DoNotGenerateAcw); @@ -456,9 +456,9 @@ public void Fixture_HelperType_IsTrimmable_NotUnconditional (string javaName, st var entry = model.Entries.FirstOrDefault (); Assert.NotNull (entry); - // Implementor/EventDispatcher types are only created from .NET (e.g., when a C# event - // is subscribed). They should NOT be unconditional — they're trimmable. - Assert.False (entry.IsUnconditional, $"{kind} should be trimmable, not unconditional"); + // Implementor/EventDispatcher types are treated as unconditional ACW types. + // Future optimization (see #10911) may make them trimmable. + Assert.True (entry.IsUnconditional, $"{kind} should be unconditional"); } } @@ -922,20 +922,4 @@ public void Fixture_CustomView_HasTwoConstructorWrappers () Assert.Equal (2, proxy.UcoConstructors.Count); } } - - public class ImplementorTrimmability - { - [Fact] - public void Build_UserTypeNamedImplementor_IsTreatedAsTrimmable () - { - var peer = MakeAcwPeer ("my/app/MyImplementor", "MyApp.MyImplementor", "App") with { - DoNotGenerateAcw = false, - }; - var model = BuildModel (new [] { peer }); - var entry = model.Entries.FirstOrDefault (); - Assert.NotNull (entry); - Assert.False (entry.IsUnconditional, - "Name-based heuristic: types ending in 'Implementor' are treated as trimmable"); - } - } } \ No newline at end of file From e70971e8e89d883ec32d3cb5608903c47508d497 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 12 Mar 2026 08:34:34 +0100 Subject: [PATCH 15/21] Minimize JavaPeerScanner changes: keep main's ExtractNamespace/ExtractShortName Restore main's implementations and only add the necessary changes: - n_ctor fix for constructor callback names - NativeCallbackName, JniReturnType, Parameters fields in AddMarshalMethod - BuildJavaConstructors method - JavaConstructors assignment in peer building Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Scanner/JavaPeerScanner.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 51f661e2218..cda727920c0 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -745,16 +745,20 @@ static string GetCrc64PackageName (string ns, string assemblyName) static string ExtractNamespace (string fullName) { - int lastDot = fullName.LastIndexOf ('.'); - return lastDot >= 0 ? fullName.Substring (0, lastDot) : ""; + // Strip nested type suffix (e.g., "My.Namespace.Outer+Inner" → "My.Namespace.Outer") + int plusIndex = fullName.IndexOf ('+'); + var nameForNamespace = plusIndex >= 0 ? fullName.Substring (0, plusIndex) : fullName; + int lastDot = nameForNamespace.LastIndexOf ('.'); + return lastDot >= 0 ? nameForNamespace.Substring (0, lastDot) : ""; } static string ExtractShortName (string fullName) { - int lastDot = fullName.LastIndexOf ('.'); - string typePart = lastDot >= 0 ? fullName.Substring (lastDot + 1) : fullName; + var span = fullName.AsSpan (); + int lastDot = span.LastIndexOf ('.'); + var typePart = lastDot >= 0 ? span.Slice (lastDot + 1) : span; int lastPlus = typePart.LastIndexOf ('+'); - return lastPlus >= 0 ? typePart.Substring (lastPlus + 1) : typePart; + return (lastPlus >= 0 ? typePart.Slice (lastPlus + 1) : typePart).ToString (); } static List BuildJavaConstructors (List marshalMethods) From 7c3ee1041f118745b05087d6d2918ea9282a5ee4 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 12 Mar 2026 08:45:07 +0100 Subject: [PATCH 16/21] Remove pre-parsed JniReturnType and Parameters from records MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compute return type and parameter lists on-demand from JniSignature in JcwJavaSourceGenerator instead of storing them on MarshalMethodInfo and JavaConstructorInfo. Simplifies the data model — JniSignature is the single source of truth. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/JcwJavaSourceGenerator.cs | 20 +++++++++++-------- .../Scanner/JavaPeerInfo.cs | 16 --------------- .../Scanner/JavaPeerScanner.cs | 3 --- .../Generator/FixtureTestBase.cs | 1 - .../Generator/JcwJavaSourceGeneratorTests.cs | 11 ---------- .../Generator/TypeMapModelBuilderTests.cs | 9 +++------ 6 files changed, 15 insertions(+), 45 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs index 8a0d02b49ac..9ac98afe134 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs @@ -153,9 +153,10 @@ 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); + 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}}) @@ -170,7 +171,8 @@ static void WriteConstructors (JavaPeerInfo type, TextWriter writer) // Write native constructor declarations foreach (var ctor in type.JavaConstructors) { - string parameters = FormatParameterList (ctor.Parameters); + var nativeCtorParams = JniSignatureHelper.ParseParameters (ctor.JniSignature); + string parameters = FormatParameterList (nativeCtorParams); writer.WriteLine ($"\tprivate native void nctor_{ctor.ConstructorIndex} ({parameters});"); } @@ -186,10 +188,12 @@ static void WriteMethods (JavaPeerInfo type, TextWriter writer) continue; } - string javaReturnType = JniSignatureHelper.JniTypeToJava (method.JniReturnType); - bool isVoid = method.JniReturnType == "V"; - string parameters = FormatParameterList (method.Parameters); - string args = FormatArgumentList (method.Parameters); + 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 diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index 2105eaba6c4..c563b970690 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -154,16 +154,6 @@ sealed record MarshalMethodInfo /// public required string NativeCallbackName { get; init; } - /// - /// JNI parameter types for UCO generation. - /// - public IReadOnlyList Parameters { get; init; } = []; - - /// - /// JNI return type descriptor, e.g., "V", "Landroid/os/Bundle;". - /// - public required string JniReturnType { get; init; } - /// /// True if this is a constructor registration. /// @@ -213,12 +203,6 @@ sealed record JavaConstructorInfo /// 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; } = []; - /// /// For [Export] constructors: super constructor arguments string. /// Null for [Register] constructors. diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index cda727920c0..427bfb9507c 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -287,8 +287,6 @@ static void AddMarshalMethod (List methods, RegisterInfo regi Connector = registerInfo.Connector, ManagedMethodName = managedName, NativeCallbackName = isConstructor ? "n_ctor" : $"n_{managedName}", - JniReturnType = JniSignatureHelper.ParseReturnTypeString (jniSignature), - Parameters = JniSignatureHelper.ParseParameters (jniSignature), IsConstructor = isConstructor, ThrownNames = exportInfo?.ThrownNames, SuperArgumentsString = exportInfo?.SuperArgumentsString, @@ -772,7 +770,6 @@ static List BuildJavaConstructors (List ctors.Add (new JavaConstructorInfo { JniSignature = mm.JniSignature, ConstructorIndex = ctorIndex, - Parameters = mm.Parameters, SuperArgumentsString = mm.SuperArgumentsString, }); ctorIndex++; diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs index ce48496738a..4065a69c406 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs @@ -89,7 +89,6 @@ private protected static JavaPeerInfo MakeAcwPeer (string jniName, string manage NativeCallbackName = "n_ctor", JniSignature = "()V", ManagedMethodName = ".ctor", - JniReturnType = "V", IsConstructor = true, }, }, diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs index f6b6578254c..a0c0846b8e4 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs @@ -153,10 +153,6 @@ public void Generate_Constructor_WithSuperArgumentsString_UsesCustomSuperArgs () 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", }, }, @@ -183,9 +179,6 @@ public void Generate_Constructor_WithEmptySuperArgumentsString_EmptySuper () new JavaConstructorInfo { JniSignature = "(Landroid/content/Context;)V", ConstructorIndex = 0, - Parameters = new List { - new JniParameterInfo { JniType = "Landroid/content/Context;" }, - }, SuperArgumentsString = "", }, }, @@ -212,10 +205,6 @@ public void Generate_Constructor_WithoutSuperArgumentsString_ForwardsAllParams ( 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;" }, - }, }, }, }; diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index a15c12343c1..6a21c2b9c0d 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -747,12 +747,11 @@ public void Build_AcwWithMarshalMethods_CreatesUcoMethods () new MarshalMethodInfo { JniName = "", NativeCallbackName = "n_ctor", JniSignature = "()V", ManagedMethodName = ".ctor", - JniReturnType = "V", IsConstructor = true, + IsConstructor = true, }, new MarshalMethodInfo { JniName = "onClick", NativeCallbackName = "n_OnClick", JniSignature = "(Landroid/view/View;)V", ManagedMethodName = "OnClick", - JniReturnType = "V", }, }, }; @@ -774,7 +773,7 @@ public void Build_ConstructorsInMarshalMethods_SkippedFromUcoMethods () new MarshalMethodInfo { JniName = "", NativeCallbackName = "n_ctor", JniSignature = "()V", ManagedMethodName = ".ctor", - JniReturnType = "V", IsConstructor = true, + IsConstructor = true, }, }, }; @@ -801,7 +800,6 @@ public void Build_InterfaceWithMarshalMethods_IsNotAcw () new MarshalMethodInfo { JniName = "onClick", NativeCallbackName = "n_OnClick", JniSignature = "(Landroid/view/View;)V", ManagedMethodName = "OnClick", - JniReturnType = "V", }, }, }; @@ -847,12 +845,11 @@ public void Build_NativeRegistrations_MatchUcoMethods () new MarshalMethodInfo { JniName = "", NativeCallbackName = "n_ctor", JniSignature = "()V", ManagedMethodName = ".ctor", - JniReturnType = "V", IsConstructor = true, + IsConstructor = true, }, new MarshalMethodInfo { JniName = "doWork", NativeCallbackName = "n_DoWork", JniSignature = "(I)V", ManagedMethodName = "DoWork", - JniReturnType = "V", }, }, JavaConstructors = new List { From d06b51666bbeb80296d498458110f795f05f1bce Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 12 Mar 2026 08:46:10 +0100 Subject: [PATCH 17/21] Use collection expression in RootTypeMapAssemblyGeneratorTests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/RootTypeMapAssemblyGeneratorTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs index 8bb4fa3d81e..033d5c05750 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs @@ -78,7 +78,7 @@ public void Generate_EmptyList_ProducesValidAssemblyWithNoTargetAttributes () [Fact] public void Generate_MultipleTargets_HasCorrectAttributeCount () { - var targets = new [] { "_App.TypeMap", "_Mono.Android.TypeMap", "_Java.Interop.TypeMap" }; + 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 (); From 38d0694c3086ee81274fce804f61d9246f872adc Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 12 Mar 2026 08:46:49 +0100 Subject: [PATCH 18/21] Restore static on GenerateRootAssembly helper Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/RootTypeMapAssemblyGeneratorTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs index 033d5c05750..44d357f911b 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs @@ -11,7 +11,7 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; public class RootTypeMapAssemblyGeneratorTests : FixtureTestBase { - MemoryStream GenerateRootAssembly (IReadOnlyList perAssemblyNames, string? assemblyName = null) + static MemoryStream GenerateRootAssembly (IReadOnlyList perAssemblyNames, string? assemblyName = null) { var stream = new MemoryStream (); var generator = new RootTypeMapAssemblyGenerator (new Version (11, 0, 0, 0)); From d6a6051f78ba672531e48388de7aa2390f493532 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 12 Mar 2026 14:04:23 +0100 Subject: [PATCH 19/21] Fix JcwJavaSourceGenerator test assertions for cross-platform line endings Add AssertContainsLine helper that normalizes line endings before comparing, so tests pass on Windows where raw string literals in the generator produce \r\n. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/JcwJavaSourceGeneratorTests.cs | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs index a0c0846b8e4..2ab03eb9853 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs @@ -16,6 +16,14 @@ static string GenerateToString (JavaPeerInfo type) 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); @@ -117,8 +125,8 @@ public class StaticInitializer 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); + AssertContainsLine ("static {\n", java); + AssertContainsLine ("mono.android.Runtime.registerNatives (MainActivity.class);\n", java); } } @@ -130,11 +138,11 @@ public class Constructor 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); + 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] @@ -222,10 +230,10 @@ public class Method 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); + 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); } } @@ -303,7 +311,7 @@ public class ExportWithThrowsClause public void Generate_ExportWithThrows_HasThrowsClause () { var java = GenerateFixture ("my/app/ExportWithThrows"); - Assert.Contains ("throws java.io.IOException, java.lang.IllegalStateException\n", java); + AssertContainsLine ("throws java.io.IOException, java.lang.IllegalStateException\n", java); } } @@ -315,10 +323,10 @@ public class MethodReturnTypesAndParams 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); + 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); } } From b3df2d3ba64ce38121a0041cf81861ea0dcf19c9 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 12 Mar 2026 15:17:48 +0100 Subject: [PATCH 20/21] Remove disk IO from JcwJavaSourceGenerator tests Refactor tests to avoid temp directory creation and cleanup: - Filtering test now checks the scanned peer list directly - JNI name validation tests call ValidateJniName directly - Remove CreateTempDir/DeleteTempDir helpers and the empty catch block - Remove Generate_CreatesCorrectFileStructure integration test All content generation tests already use in-memory StringWriter. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/FixtureTestBase.cs | 17 ------- .../Generator/JcwJavaSourceGeneratorTests.cs | 46 +++++-------------- 2 files changed, 11 insertions(+), 52 deletions(-) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs index 4065a69c406..e73b18c460a 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs @@ -125,21 +125,4 @@ private protected static List GetMemberRefNames (MetadataReader reader) .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i))) .Select (m => reader.GetString (m.Name)) .ToList (); - - private protected static string CreateTempDir () - { - var dir = Path.Combine (Path.GetTempPath (), $"typemap-test-{Guid.NewGuid ():N}"); - Directory.CreateDirectory (dir); - return dir; - } - - private protected static void DeleteTempDir (string dir) - { - if (Directory.Exists (dir)) { - try { - Directory.Delete (dir, true); - } catch (IOException) { - } - } - } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs index 2ab03eb9853..7a92683ffca 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs @@ -70,20 +70,17 @@ public void JniTypeToJava_ConvertsCorrectly (string jniType, string expected) } - public class Filtering : IDisposable + public class Filtering { - 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")); + 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"); } } @@ -251,25 +248,8 @@ public void Generate_NestedType_HasCorrectPackageAndClassName () } - public class OutputFilePath : IDisposable + public class JniNameValidation { - 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 ("")] @@ -282,11 +262,9 @@ public void Generate_CreatesCorrectFileStructure () [InlineData ("C:\\Windows\\System32")] [InlineData ("com/Ex:ample")] [InlineData ("/absolute/path")] - public void Generate_InvalidJniName_Throws (string badJniName) + public void ValidateJniName_InvalidName_Throws (string badJniName) { - var peer = MakeAcwPeer (badJniName, "Test.Bad", "TestApp"); - var generator = new JcwJavaSourceGenerator (); - Assert.Throws (() => generator.Generate (new [] { peer }, _outputDir)); + Assert.Throws (() => JniSignatureHelper.ValidateJniName (badJniName)); } [Theory] @@ -295,11 +273,9 @@ public void Generate_InvalidJniName_Throws (string badJniName) [InlineData ("SingleSegment")] [InlineData ("com/example/_Private")] [InlineData ("com/example/$Generated")] - public void Generate_ValidJniName_DoesNotThrow (string validJniName) + public void ValidateJniName_ValidName_DoesNotThrow (string validJniName) { - var peer = MakeAcwPeer (validJniName, "Test.Valid", "TestApp"); - var generator = new JcwJavaSourceGenerator (); - generator.Generate (new [] { peer }, _outputDir); + JniSignatureHelper.ValidateJniName (validJniName); } } @@ -330,4 +306,4 @@ public void Generate_TouchHandler_HasExpectedMethodSignatures () } } -} \ No newline at end of file +} From 04e0b2e1918c84f75f7a9ae420b72aeb78ede726 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 12 Mar 2026 15:25:04 +0100 Subject: [PATCH 21/21] Add trailing newlines to test files Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/TypeMapAssemblyGeneratorTests.cs | 2 +- .../Generator/TypeMapModelBuilderTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 4f6c595a5ba..5fb770fd3a5 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -504,4 +504,4 @@ public void ParseParameterTypes_UnterminatedSignature_ReturnsEmptyList () { Assert.Empty (JniSignatureHelper.ParseParameterTypes ("(")); } -} \ No newline at end of file +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 6a21c2b9c0d..7a91bc0a029 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -919,4 +919,4 @@ public void Fixture_CustomView_HasTwoConstructorWrappers () Assert.Equal (2, proxy.UcoConstructors.Count); } } -} \ No newline at end of file +}