From 9449651e8a447cefa2467563a5b1839be23a6c2b Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 18 Feb 2026 18:25:26 +0100 Subject: [PATCH 01/40] [TrimmableTypeMap] Add TypeMap proxy groundwork with CreateInstance Add the core pipeline that transforms scanned JavaPeerInfo records into TypeMap .dll assemblies with proxy types and CreateInstance support: - TypeMapAssemblyData model: proxy types, TypeMap attributes, associations - ModelBuilder: transforms JavaPeerInfo into model with alias detection, ACW filtering, proxy naming, cross-assembly IgnoresAccessChecksTo - TypeMapAssemblyEmitter: PE/IL emission of proxy types with CreateInstance, get_TargetType, get_InvokerType properties - TypeMapAssemblyGenerator: orchestrates Build + Emit pipeline - RootTypeMapAssemblyGenerator: generates root _Microsoft.Android.TypeMaps.dll - JniSignatureHelper: JNI signature parsing utility - Scanner enrichment for generator consumption - Comprehensive tests for model builder, assembly generator, root generator UCO wrappers, RegisterNatives, and IAndroidCallableWrapper support will be added in a follow-up PR. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/JniSignatureHelper.cs | 123 +++ .../Generator/MetadataHelper.cs | 20 + .../Generator/Model/TypeMapAssemblyData.cs | 130 +++ .../Generator/ModelBuilder.cs | 240 ++++++ .../Generator/PEAssemblyBuilder.cs | 204 +++++ .../Generator/RootTypeMapAssemblyGenerator.cs | 88 ++ .../Generator/TypeMapAssemblyEmitter.cs | 424 ++++++++++ .../Generator/TypeMapAssemblyGenerator.cs | 32 + .../Scanner/JavaPeerInfo.cs | 89 ++ .../Scanner/JavaPeerScanner.cs | 49 ++ .../Generator/FixtureTestBase.cs | 112 +++ .../Generator/JcwJavaSourceGeneratorTests.cs | 350 ++++++++ .../RootTypeMapAssemblyGeneratorTests.cs | 138 +++ .../TypeMapAssemblyGeneratorTests.cs | 424 ++++++++++ .../Generator/TypeMapModelBuilderTests.cs | 793 ++++++++++++++++++ 15 files changed, 3216 insertions(+) create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs 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..2cbcd7f5c3e --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs @@ -0,0 +1,123 @@ +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; + SkipSingleType (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}"); + } + } + + static void SkipSingleType (string sig, ref int i) + { + switch (sig [i]) { + case 'V': case 'Z': case 'B': case 'C': case 'S': + case 'I': case 'J': case 'F': case 'D': + i++; + break; + case 'L': + i = sig.IndexOf (';', i) + 1; + break; + case '[': + i++; + SkipSingleType (sig, ref i); + break; + 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"); + } + } +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs new file mode 100644 index 00000000000..2f62bb468f1 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs @@ -0,0 +1,20 @@ +using System; +using System.Security.Cryptography; +using System.Text; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +static class MetadataHelper +{ + /// + /// Produces a deterministic MVID from the module name so that identical inputs produce identical assemblies. + /// + public static Guid DeterministicMvid (string moduleName) + { + using var sha = SHA256.Create (); + byte [] hash = sha.ComputeHash (Encoding.UTF8.GetBytes (moduleName)); + byte [] guidBytes = new byte [16]; + Array.Copy (hash, guidBytes, 16); + return new Guid (guidBytes); + } +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs new file mode 100644 index 00000000000..4ab45ecca37 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Data model for a single TypeMap output assembly. +/// Describes what to emit — the emitter writes this directly into a PE assembly. +/// Built by , consumed by . +/// +sealed class TypeMapAssemblyData +{ + /// Assembly name (e.g., "_MyApp.TypeMap"). + 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 (); +} + +/// +/// One [assembly: TypeMap("jni/name", typeof(Proxy))] or +/// [assembly: TypeMap("jni/name", typeof(Proxy), typeof(Target))] entry. +/// +/// 2-arg (unconditional): proxy is always preserved — used for ACW types and essential runtime types. +/// 3-arg (trimmable): proxy is preserved only if Target type is referenced by the app. +/// +sealed record TypeMapAttributeData +{ + /// JNI type name, e.g., "android/app/Activity". + public required string JniName { get; init; } + + /// + /// Assembly-qualified proxy type reference string. + /// Either points to a generated proxy or to the original managed type. + /// + public required string ProxyTypeReference { get; init; } + + /// + /// Assembly-qualified target type reference for the trimmable (3-arg) variant. + /// Null for unconditional (2-arg) entries. + /// The trimmer preserves the proxy only if this target type is used by the app. + /// + public string? TargetTypeReference { get; init; } + + /// True for 2-arg unconditional entries (ACW types, essential runtime types). + public bool IsUnconditional => TargetTypeReference == null; +} + +/// +/// A proxy type to generate in the TypeMap assembly (subclass of JavaPeerProxy). +/// +sealed class JavaPeerProxyData +{ + /// Simple type name, e.g., "Java_Lang_Object_Proxy". + 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; + + /// + /// Activation constructor details. Determines how CreateInstance instantiates the managed peer. + /// + public ActivationCtorData? ActivationCtor { get; set; } + + /// True if this is an open generic type definition. CreateInstance throws NotSupportedException. + public bool IsGenericDefinition { get; init; } + +} + + +/// +/// A cross-assembly type reference (assembly name + full managed type name). +/// +sealed record TypeRefData +{ + /// Full managed type name, e.g., "Android.App.Activity" or "MyApp.Outer+Inner". + public required string ManagedTypeName { get; init; } + + /// Assembly containing the type, e.g., "Mono.Android". + public required string AssemblyName { get; init; } +} + +/// +/// Describes how the proxy's CreateInstance should construct the managed peer. +/// +sealed record ActivationCtorData +{ + /// Type that declares the activation constructor (may be a base type). + 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; } +} + +/// +/// One [assembly: TypeMapAssociation(typeof(Source), typeof(AliasProxy))] entry. +/// Links a managed type to the proxy that holds its alias TypeMap entry. +/// +sealed record TypeMapAssociationData +{ + /// Assembly-qualified source type reference (the managed alias type). + 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 new file mode 100644 index 00000000000..e64d14849a9 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -0,0 +1,240 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Builds a from scanned records. +/// All decision logic (deduplication, alias detection, ACW filtering, 2-arg vs 3-arg attribute +/// selection, callback resolution, proxy naming) lives here. +/// The output model is a plain data structure that the emitter writes directly into a PE assembly. +/// +static class ModelBuilder +{ + static readonly HashSet EssentialRuntimeTypes = new (StringComparer.Ordinal) { + "java/lang/Object", + "java/lang/Class", + "java/lang/String", + "java/lang/Throwable", + "java/lang/Exception", + "java/lang/RuntimeException", + "java/lang/Error", + "java/lang/Thread", + }; + + /// + /// Builds a TypeMap assembly model for the given peers. + /// + /// Scanned Java peer types (typically from a single input assembly). + /// Output .dll path — used to derive assembly/module names if not specified. + /// Explicit assembly name. If null, derived from . + public static TypeMapAssemblyData Build (IReadOnlyList peers, string outputPath, string? assemblyName = null) + { + if (peers is null) { + throw new ArgumentNullException (nameof (peers)); + } + if (outputPath is null) { + throw new ArgumentNullException (nameof (outputPath)); + } + + assemblyName ??= Path.GetFileNameWithoutExtension (outputPath); + string moduleName = Path.GetFileName (outputPath); + + var model = new TypeMapAssemblyData { + AssemblyName = assemblyName, + ModuleName = moduleName, + }; + + // 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!), + StringComparer.Ordinal); + + // Group non-invoker peers by JNI name to detect aliases (multiple .NET types → same Java class). + // Use an ordered dictionary to ensure deterministic output across runs. + var groups = new SortedDictionary> (StringComparer.Ordinal); + foreach (var peer in peers) { + if (invokerTypeNames.Contains (peer.ManagedTypeName)) { + continue; + } + if (!groups.TryGetValue (peer.JavaName, out var list)) { + list = new List (); + groups [peer.JavaName] = list; + } + list.Add (peer); + } + + foreach (var kvp in groups) { + string jniName = kvp.Key; + var peersForName = kvp.Value; + + // Sort aliases by managed type name for deterministic proxy naming + if (peersForName.Count > 1) { + peersForName.Sort ((a, b) => StringComparer.Ordinal.Compare (a.ManagedTypeName, b.ManagedTypeName)); + } + + 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); + if (proxy.ActivationCtor != null && !proxy.ActivationCtor.IsOnLeafType) { + AddIfCrossAssembly (referencedAssemblies, proxy.ActivationCtor.DeclaringType.AssemblyName, assemblyName); + } + } + model.IgnoresAccessChecksTo.AddRange (referencedAssemblies); + + return model; + } + + static void EmitPeers (TypeMapAssemblyData model, string jniName, + 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]", ... + JavaPeerProxyData? primaryProxy = null; + for (int i = 0; i < peersForName.Count; i++) { + var peer = peersForName [i]; + string entryJniName = i == 0 ? jniName : $"{jniName}[{i}]"; + + bool hasProxy = peer.ActivationCtor != null || peer.InvokerTypeName != null; + + JavaPeerProxyData? proxy = null; + if (hasProxy) { + proxy = BuildProxyType (peer); + model.ProxyTypes.Add (proxy); + } + + if (i == 0) { + primaryProxy = proxy; + } + + model.Entries.Add (BuildEntry (peer, proxy, assemblyName, entryJniName)); + + // 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}", + }); + } + } + } + + /// + /// Determines whether a type should use the unconditional (2-arg) TypeMap attribute. + /// Unconditional types are always preserved by the trimmer. + /// + static bool IsUnconditionalEntry (JavaPeerInfo peer) + { + // Essential runtime types needed by the Java interop runtime + if (EssentialRuntimeTypes.Contains (peer.JavaName)) { + 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) { + return true; + } + + // Types marked unconditional by the scanner (component attributes: Activity, Service, etc.) + if (peer.IsUnconditional) { + return true; + } + + 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)) { + set.Add (asmName); + } + } + + static JavaPeerProxyData BuildProxyType (JavaPeerInfo peer) + { + // 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"; + + var proxy = new JavaPeerProxyData { + TypeName = proxyTypeName, + TargetType = new TypeRefData { + ManagedTypeName = peer.ManagedTypeName, + AssemblyName = peer.AssemblyName, + }, + IsGenericDefinition = peer.IsGenericDefinition, + }; + + if (peer.InvokerTypeName != null) { + proxy.InvokerType = new TypeRefData { + ManagedTypeName = peer.InvokerTypeName, + AssemblyName = peer.AssemblyName, + }; + } + + if (peer.ActivationCtor != null) { + bool isOnLeaf = string.Equals (peer.ActivationCtor.DeclaringTypeName, peer.ManagedTypeName, StringComparison.Ordinal); + proxy.ActivationCtor = new ActivationCtorData { + DeclaringType = new TypeRefData { + ManagedTypeName = peer.ActivationCtor.DeclaringTypeName, + AssemblyName = peer.ActivationCtor.DeclaringAssemblyName, + }, + IsOnLeafType = isOnLeaf, + Style = peer.ActivationCtor.Style, + }; + } + + return proxy; + } + + static TypeMapAttributeData BuildEntry (JavaPeerInfo peer, JavaPeerProxyData? proxy, + string outputAssemblyName, string jniName) + { + string proxyRef; + if (proxy != null) { + proxyRef = $"{proxy.Namespace}.{proxy.TypeName}, {outputAssemblyName}"; + } else { + proxyRef = $"{peer.ManagedTypeName}, {peer.AssemblyName}"; + } + + bool isUnconditional = IsUnconditionalEntry (peer); + string? targetRef = null; + if (!isUnconditional) { + targetRef = $"{peer.ManagedTypeName}, {peer.AssemblyName}"; + } + + return new TypeMapAttributeData { + JniName = jniName, + ProxyTypeReference = proxyRef, + TargetTypeReference = targetRef, + }; + } +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs new file mode 100644 index 00000000000..01c73df6b49 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs @@ -0,0 +1,204 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using System.Reflection.PortableExecutable; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Shared plumbing for building PE assemblies with System.Reflection.Metadata. +/// Owns the , common assembly/type references, scratch blob builders, +/// and the final PE serialisation. Both and +/// delegate to this instead of duplicating boilerplate. +/// +sealed class PEAssemblyBuilder +{ + // Mono.Android strong name public key token (84e04ff9cfb79065) + static readonly byte [] MonoAndroidPublicKeyToken = { 0x84, 0xe0, 0x4f, 0xf9, 0xcf, 0xb7, 0x90, 0x65 }; + + readonly Dictionary _asmRefCache = new (StringComparer.OrdinalIgnoreCase); + readonly Dictionary<(string Assembly, string Type), EntityHandle> _typeRefCache = new (); + + // Reusable scratch BlobBuilders — avoids allocating a new one per method body / attribute / member ref. + // Each is Clear()'d before use. Safe because all emission is single-threaded and non-reentrant. + readonly BlobBuilder _sigBlob = new BlobBuilder (64); + readonly BlobBuilder _codeBlob = new BlobBuilder (256); + readonly BlobBuilder _attrBlob = new BlobBuilder (64); + + readonly Version _systemRuntimeVersion; + + public MetadataBuilder Metadata { get; } = new MetadataBuilder (); + public BlobBuilder ILBuilder { get; } = new BlobBuilder (); + + /// Reference to the System.Runtime assembly. + public AssemblyReferenceHandle SystemRuntimeRef { get; private set; } + + /// Reference to the System.Runtime.InteropServices assembly. + public AssemblyReferenceHandle SystemRuntimeInteropServicesRef { get; private set; } + + /// Reference to the Mono.Android assembly. + public AssemblyReferenceHandle MonoAndroidRef { get; private set; } + + public PEAssemblyBuilder (Version systemRuntimeVersion) + { + _systemRuntimeVersion = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion)); + } + + /// + /// Emits the assembly definition, module definition, common assembly references, and <Module> type. + /// Call this first. + /// + public void EmitPreamble (string assemblyName, string moduleName) + { + _asmRefCache.Clear (); + _typeRefCache.Clear (); + + Metadata.AddAssembly ( + Metadata.GetOrAddString (assemblyName), + new Version (1, 0, 0, 0), + culture: default, + publicKey: default, + flags: 0, + hashAlgorithm: AssemblyHashAlgorithm.None); + + Metadata.AddModule ( + generation: 0, + Metadata.GetOrAddString (moduleName), + Metadata.GetOrAddGuid (MetadataHelper.DeterministicMvid (moduleName)), + encId: default, + encBaseId: default); + + // Common assembly references + SystemRuntimeRef = AddAssemblyRef ("System.Runtime", _systemRuntimeVersion); + SystemRuntimeInteropServicesRef = AddAssemblyRef ("System.Runtime.InteropServices", _systemRuntimeVersion); + MonoAndroidRef = AddAssemblyRef ("Mono.Android", new Version (0, 0, 0, 0), MonoAndroidPublicKeyToken); + + // type + Metadata.AddTypeDefinition ( + default, default, + Metadata.GetOrAddString (""), + default, + MetadataTokens.FieldDefinitionHandle (1), + MetadataTokens.MethodDefinitionHandle (1)); + } + + /// Serialises the metadata + IL into a PE DLL at . + public void WritePE (string outputPath) + { + var dir = Path.GetDirectoryName (outputPath); + if (!string.IsNullOrEmpty (dir)) { + Directory.CreateDirectory (dir); + } + + 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); + } + + // ---- Assembly / type / member reference helpers ---- + + /// Adds (or retrieves from cache) an assembly reference. + public AssemblyReferenceHandle AddAssemblyRef (string name, Version version, byte []? publicKeyOrToken = null) + { + if (_asmRefCache.TryGetValue (name, out var existing)) { + return existing; + } + var handle = Metadata.AddAssemblyReference ( + Metadata.GetOrAddString (name), version, default, + publicKeyOrToken != null ? Metadata.GetOrAddBlob (publicKeyOrToken) : default, 0, default); + _asmRefCache [name] = handle; + return handle; + } + + /// 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)); + } + + /// Adds a member reference using the reusable signature blob builder. + public MemberReferenceHandle AddMemberRef (EntityHandle parent, string name, Action encodeSig) + { + _sigBlob.Clear (); + encodeSig (new BlobEncoder (_sigBlob)); + return Metadata.AddMemberReference (parent, Metadata.GetOrAddString (name), Metadata.GetOrAddBlob (_sigBlob)); + } + + /// Resolves a to a TypeReference/TypeSpecification handle, with caching. + public EntityHandle ResolveTypeRef (TypeRefData typeRef) + { + var cacheKey = (typeRef.AssemblyName, typeRef.ManagedTypeName); + if (_typeRefCache.TryGetValue (cacheKey, out var cached)) { + return cached; + } + var asmRef = FindOrAddAssemblyRef (typeRef.AssemblyName); + var result = MakeTypeRefForManagedName (asmRef, typeRef.ManagedTypeName); + _typeRefCache [cacheKey] = result; + return result; + } + + TypeReferenceHandle MakeTypeRefForManagedName (EntityHandle scope, string managedTypeName) + { + int plusIndex = managedTypeName.IndexOf ('+'); + if (plusIndex >= 0) { + var outerRef = MakeTypeRefForManagedName (scope, managedTypeName.Substring (0, plusIndex)); + return MakeTypeRefForManagedName (outerRef, managedTypeName.Substring (plusIndex + 1)); + } + int lastDot = managedTypeName.LastIndexOf ('.'); + var ns = lastDot >= 0 ? managedTypeName.Substring (0, lastDot) : ""; + var name = lastDot >= 0 ? managedTypeName.Substring (lastDot + 1) : managedTypeName; + return Metadata.AddTypeReference (scope, Metadata.GetOrAddString (ns), Metadata.GetOrAddString (name)); + } + + // ---- Method body emission ---- + + /// Emits a method body and definition in one call. + public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, + Action encodeSig, Action emitIL) + { + _sigBlob.Clear (); + encodeSig (new BlobEncoder (_sigBlob)); + + _codeBlob.Clear (); + var encoder = new InstructionEncoder (_codeBlob); + emitIL (encoder); + + while (ILBuilder.Count % 4 != 0) { + ILBuilder.WriteByte (0); + } + var bodyEncoder = new MethodBodyStreamEncoder (ILBuilder); + int bodyOffset = bodyEncoder.AddMethodBody (encoder); + + return Metadata.AddMethodDefinition ( + attrs, MethodImplAttributes.IL, + Metadata.GetOrAddString (name), + Metadata.GetOrAddBlob (_sigBlob), + bodyOffset, default); + } + + // ---- Attribute blob helpers ---- + + /// + /// Writes a custom attribute blob. Calls to fill in the + /// payload between the prolog and NumNamed footer. + /// + public BlobHandle BuildAttributeBlob (Action writePayload) + { + _attrBlob.Clear (); + _attrBlob.WriteUInt16 (0x0001); // Prolog + writePayload (_attrBlob); + _attrBlob.WriteUInt16 (0x0000); // NumNamed + return Metadata.GetOrAddBlob (_attrBlob); + } +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs new file mode 100644 index 00000000000..bdd5f0ee996 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Generates the root _Microsoft.Android.TypeMaps.dll assembly that references +/// all per-assembly typemap assemblies via +/// [assembly: TypeMapAssemblyTargetAttribute<Java.Lang.Object>("name")]. +/// +/// +/// The generated assembly looks like this (pseudo-C#): +/// +/// // One attribute per per-assembly typemap assembly — tells the runtime where to find TypeMap entries: +/// [assembly: TypeMapAssemblyTarget<Java.Lang.Object>("_Mono.Android.TypeMap")] +/// [assembly: TypeMapAssemblyTarget<Java.Lang.Object>("_MyApp.TypeMap")] +/// +/// +sealed class RootTypeMapAssemblyGenerator +{ + const string DefaultAssemblyName = "_Microsoft.Android.TypeMaps"; + + readonly Version _systemRuntimeVersion; + + /// Version for System.Runtime assembly references. + public RootTypeMapAssemblyGenerator (Version systemRuntimeVersion) + { + _systemRuntimeVersion = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion)); + } + + /// + /// 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 (perAssemblyTypeMapNames is null) { + throw new ArgumentNullException (nameof (perAssemblyTypeMapNames)); + } + if (outputPath is null) { + throw new ArgumentNullException (nameof (outputPath)); + } + + assemblyName ??= DefaultAssemblyName; + var moduleName = Path.GetFileName (outputPath); + + var pe = new PEAssemblyBuilder (_systemRuntimeVersion); + pe.EmitPreamble (assemblyName, moduleName); + + // Reference the open generic TypeMapAssemblyTargetAttribute`1 from System.Runtime.InteropServices + var openAttrRef = pe.Metadata.AddTypeReference (pe.SystemRuntimeInteropServicesRef, + pe.Metadata.GetOrAddString ("System.Runtime.InteropServices"), + pe.Metadata.GetOrAddString ("TypeMapAssemblyTargetAttribute`1")); + + // Reference Java.Lang.Object from Mono.Android (the type universe) + var javaLangObjectRef = pe.Metadata.AddTypeReference (pe.MonoAndroidRef, + pe.Metadata.GetOrAddString ("Java.Lang"), pe.Metadata.GetOrAddString ("Object")); + + // Build TypeSpec for TypeMapAssemblyTargetAttribute + var genericInstBlob = new BlobBuilder (); + genericInstBlob.WriteByte (0x15); // ELEMENT_TYPE_GENERICINST + genericInstBlob.WriteByte (0x12); // ELEMENT_TYPE_CLASS + genericInstBlob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (openAttrRef)); + genericInstBlob.WriteCompressedInteger (1); // generic arity = 1 + genericInstBlob.WriteByte (0x12); // ELEMENT_TYPE_CLASS + genericInstBlob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (javaLangObjectRef)); + var closedAttrTypeSpec = pe.Metadata.AddTypeSpecification (pe.Metadata.GetOrAddBlob (genericInstBlob)); + + // MemberRef for .ctor(string) on the closed generic type + var ctorRef = pe.AddMemberRef (closedAttrTypeSpec, ".ctor", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (1, + rt => rt.Void (), + p => p.AddParameter ().Type ().String ())); + + // Add [assembly: TypeMapAssemblyTargetAttribute("name")] for each per-assembly typemap + foreach (var name in perAssemblyTypeMapNames) { + var blobHandle = pe.BuildAttributeBlob (blob => blob.WriteSerializedString (name)); + pe.Metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, ctorRef, blobHandle); + } + + pe.WritePE (outputPath); + } +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs new file mode 100644 index 00000000000..722c0f38bbc --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -0,0 +1,424 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Emits a per-assembly TypeMap PE assembly from a . +/// This is a mechanical translation — all decision logic lives in . +/// +/// +/// The generated assembly looks like this (pseudo-C#): +/// +/// // Assembly-level TypeMap attributes — one per Java peer type: +/// [assembly: TypeMap<Java.Lang.Object>("android/app/Activity", typeof(Activity_Proxy))] // unconditional (ACW) +/// [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 +/// { +/// public Activity_Proxy() : base() { } +/// +/// // Creates the managed peer when Java calls into .NET +/// public override IJavaPeerable CreateInstance(IntPtr handle, JniHandleOwnership ownership) +/// => new Activity(handle, ownership); // leaf ctor +/// // or: (Activity)RuntimeHelpers.GetUninitializedObject(typeof(Activity)); +/// // obj.BaseCtor(handle, ownership); // inherited ctor +/// // or: new IOnClickListenerInvoker(handle, ownership); // interface invoker +/// // or: null; // no activation +/// // or: throw new NotSupportedException(...); // open generic +/// +/// public override Type TargetType => typeof(Activity); +/// public Type InvokerType => typeof(IOnClickListenerInvoker); // interfaces only +/// } +/// +/// // Emitted so the proxy assembly can access internal members in the target assembly: +/// [assembly: IgnoresAccessChecksTo("Mono.Android")] +/// +/// +sealed class TypeMapAssemblyEmitter +{ + readonly Version _systemRuntimeVersion; + + PEAssemblyBuilder _pe = null!; + + AssemblyReferenceHandle _javaInteropRef; + + TypeReferenceHandle _javaPeerProxyRef; + TypeReferenceHandle _iJavaPeerableRef; + TypeReferenceHandle _jniHandleOwnershipRef; + TypeReferenceHandle _systemTypeRef; + TypeReferenceHandle _runtimeTypeHandleRef; + TypeReferenceHandle _notSupportedExceptionRef; + TypeReferenceHandle _runtimeHelpersRef; + + MemberReferenceHandle _baseCtorRef; + MemberReferenceHandle _getTypeFromHandleRef; + MemberReferenceHandle _getUninitializedObjectRef; + MemberReferenceHandle _notSupportedExceptionCtorRef; + MemberReferenceHandle _typeMapAttrCtorRef2Arg; + MemberReferenceHandle _typeMapAttrCtorRef3Arg; + MemberReferenceHandle _typeMapAssociationAttrCtorRef; + + /// + /// Creates a new emitter. + /// + /// + /// Version for System.Runtime assembly references. + /// Will be derived from $(DotNetTargetVersion) MSBuild property in the build task. + /// + public TypeMapAssemblyEmitter (Version systemRuntimeVersion) + { + _systemRuntimeVersion = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion)); + } + + /// + /// Emits a PE assembly from the given model and writes it to . + /// + public void Emit (TypeMapAssemblyData model, string outputPath) + { + if (model is null) { + throw new ArgumentNullException (nameof (model)); + } + if (outputPath is null) { + throw new ArgumentNullException (nameof (outputPath)); + } + + _pe = new PEAssemblyBuilder (_systemRuntimeVersion); + _pe.EmitPreamble (model.AssemblyName, model.ModuleName); + + _javaInteropRef = _pe.AddAssemblyRef ("Java.Interop", new Version (0, 0, 0, 0)); + + EmitTypeReferences (); + EmitMemberReferences (); + + foreach (var proxy in model.ProxyTypes) { + EmitProxyType (proxy); + } + + foreach (var entry in model.Entries) { + EmitTypeMapAttribute (entry); + } + + foreach (var assoc in model.Associations) { + EmitTypeMapAssociationAttribute (assoc); + } + + EmitIgnoresAccessChecksToAttribute (model.IgnoresAccessChecksTo); + _pe.WritePE (outputPath); + } + + // ---- Type / Member references ---- + + void EmitTypeReferences () + { + var metadata = _pe.Metadata; + _javaPeerProxyRef = metadata.AddTypeReference (_pe.MonoAndroidRef, + metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JavaPeerProxy")); + _iJavaPeerableRef = metadata.AddTypeReference (_javaInteropRef, + metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("IJavaPeerable")); + _jniHandleOwnershipRef = metadata.AddTypeReference (_pe.MonoAndroidRef, + metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("JniHandleOwnership")); + _systemTypeRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, + metadata.GetOrAddString ("System"), metadata.GetOrAddString ("Type")); + _runtimeTypeHandleRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, + metadata.GetOrAddString ("System"), metadata.GetOrAddString ("RuntimeTypeHandle")); + _notSupportedExceptionRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, + metadata.GetOrAddString ("System"), metadata.GetOrAddString ("NotSupportedException")); + _runtimeHelpersRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, + metadata.GetOrAddString ("System.Runtime.CompilerServices"), metadata.GetOrAddString ("RuntimeHelpers")); + } + + void EmitMemberReferences () + { + _baseCtorRef = _pe.AddMemberRef (_javaPeerProxyRef, ".ctor", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { })); + + _getTypeFromHandleRef = _pe.AddMemberRef (_systemTypeRef, "GetTypeFromHandle", + sig => sig.MethodSignature ().Parameters (1, + rt => rt.Type ().Type (_systemTypeRef, false), + p => p.AddParameter ().Type ().Type (_runtimeTypeHandleRef, true))); + + _getUninitializedObjectRef = _pe.AddMemberRef (_runtimeHelpersRef, "GetUninitializedObject", + sig => sig.MethodSignature ().Parameters (1, + rt => rt.Type ().Object (), + p => p.AddParameter ().Type ().Type (_systemTypeRef, false))); + + _notSupportedExceptionCtorRef = _pe.AddMemberRef (_notSupportedExceptionRef, ".ctor", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (1, + rt => rt.Void (), + p => p.AddParameter ().Type ().String ())); + + EmitTypeMapAttributeCtorRef (); + EmitTypeMapAssociationAttributeCtorRef (); + } + + void EmitTypeMapAttributeCtorRef () + { + var metadata = _pe.Metadata; + var typeMapAttrOpenRef = metadata.AddTypeReference (_pe.SystemRuntimeInteropServicesRef, + metadata.GetOrAddString ("System.Runtime.InteropServices"), + metadata.GetOrAddString ("TypeMapAttribute`1")); + var javaLangObjectRef = metadata.AddTypeReference (_pe.MonoAndroidRef, + metadata.GetOrAddString ("Java.Lang"), metadata.GetOrAddString ("Object")); + + var genericInstBlob = new BlobBuilder (); + genericInstBlob.WriteByte (0x15); // ELEMENT_TYPE_GENERICINST + genericInstBlob.WriteByte (0x12); // ELEMENT_TYPE_CLASS + genericInstBlob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (typeMapAttrOpenRef)); + genericInstBlob.WriteCompressedInteger (1); + genericInstBlob.WriteByte (0x12); // ELEMENT_TYPE_CLASS + genericInstBlob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (javaLangObjectRef)); + var closedAttrTypeSpec = metadata.AddTypeSpecification (metadata.GetOrAddBlob (genericInstBlob)); + + // 2-arg: TypeMap(string jniName, Type proxyType) — unconditional + _typeMapAttrCtorRef2Arg = _pe.AddMemberRef (closedAttrTypeSpec, ".ctor", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, + rt => rt.Void (), + p => { + p.AddParameter ().Type ().String (); + p.AddParameter ().Type ().Type (_systemTypeRef, false); + })); + + // 3-arg: TypeMap(string jniName, Type proxyType, Type targetType) — trimmable + _typeMapAttrCtorRef3Arg = _pe.AddMemberRef (closedAttrTypeSpec, ".ctor", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (3, + rt => rt.Void (), + p => { + p.AddParameter ().Type ().String (); + p.AddParameter ().Type ().Type (_systemTypeRef, false); + p.AddParameter ().Type ().Type (_systemTypeRef, false); + })); + } + + void EmitTypeMapAssociationAttributeCtorRef () + { + var metadata = _pe.Metadata; + // TypeMapAssociationAttribute is in System.Runtime.InteropServices, takes 2 Type args: + // TypeMapAssociation(Type sourceType, Type aliasProxyType) + var typeMapAssociationAttrRef = metadata.AddTypeReference (_pe.SystemRuntimeInteropServicesRef, + metadata.GetOrAddString ("System.Runtime.InteropServices"), + metadata.GetOrAddString ("TypeMapAssociationAttribute")); + + _typeMapAssociationAttrCtorRef = _pe.AddMemberRef (typeMapAssociationAttrRef, ".ctor", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, + rt => rt.Void (), + p => { + p.AddParameter ().Type ().Type (_systemTypeRef, false); + p.AddParameter ().Type ().Type (_systemTypeRef, false); + })); + } + + // ---- Proxy types ---- + + void EmitProxyType (JavaPeerProxyData proxy) + { + var metadata = _pe.Metadata; + metadata.AddTypeDefinition ( + TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.Class, + metadata.GetOrAddString (proxy.Namespace), + metadata.GetOrAddString (proxy.TypeName), + _javaPeerProxyRef, + MetadataTokens.FieldDefinitionHandle (metadata.GetRowCount (TableIndex.Field) + 1), + MetadataTokens.MethodDefinitionHandle (metadata.GetRowCount (TableIndex.MethodDef) + 1)); + + // .ctor + _pe.EmitBody (".ctor", + MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName, + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { }), + encoder => { + encoder.OpCode (ILOpCode.Ldarg_0); + encoder.Call (_baseCtorRef); + encoder.OpCode (ILOpCode.Ret); + }); + + // CreateInstance + EmitCreateInstance (proxy); + + // get_TargetType + EmitTypeGetter ("get_TargetType", proxy.TargetType, + MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.SpecialName | MethodAttributes.HideBySig); + + // get_InvokerType + if (proxy.InvokerType != null) { + EmitTypeGetter ("get_InvokerType", proxy.InvokerType, + MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig); + } + } + + void EmitCreateInstance (JavaPeerProxyData proxy) + { + if (!proxy.HasActivation) { + EmitCreateInstanceBody (encoder => { + encoder.OpCode (ILOpCode.Ldnull); + encoder.OpCode (ILOpCode.Ret); + }); + 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); + }); + return; + } + + // Interface with invoker: new TInvoker(IntPtr, JniHandleOwnership) + if (proxy.InvokerType != null) { + 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; + } + + // At this point, ActivationCtor is guaranteed non-null (HasActivation && InvokerType == null) + var activationCtor = proxy.ActivationCtor ?? throw new InvalidOperationException ("ActivationCtor should not be null when HasActivation is true and InvokerType is null"); + var targetTypeRef = _pe.ResolveTypeRef (proxy.TargetType); + + if (activationCtor.IsOnLeafType) { + // 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.OpCode (ILOpCode.Ret); + }); + } 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); + 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) + { + _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); + } + + MemberReferenceHandle AddActivationCtorRef (EntityHandle declaringTypeRef) + { + 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); + })); + } + + void EmitTypeGetter (string methodName, TypeRefData typeRef, MethodAttributes attrs) + { + var handle = _pe.ResolveTypeRef (typeRef); + + _pe.EmitBody (methodName, attrs, + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, + rt => rt.Type ().Type (_systemTypeRef, false), + p => { }), + encoder => { + encoder.OpCode (ILOpCode.Ldtoken); + encoder.Token (handle); + encoder.Call (_getTypeFromHandleRef); + encoder.OpCode (ILOpCode.Ret); + }); + } + + // ---- TypeMap attributes ---- + + void EmitTypeMapAttribute (TypeMapAttributeData entry) + { + var ctorRef = entry.IsUnconditional ? _typeMapAttrCtorRef2Arg : _typeMapAttrCtorRef3Arg; + var blob = _pe.BuildAttributeBlob (b => { + b.WriteSerializedString (entry.JniName); + b.WriteSerializedString (entry.ProxyTypeReference); + if (!entry.IsUnconditional) { + b.WriteSerializedString (entry.TargetTypeReference!); + } + }); + _pe.Metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, ctorRef, blob); + } + + void EmitTypeMapAssociationAttribute (TypeMapAssociationData assoc) + { + var blob = _pe.BuildAttributeBlob (b => { + b.WriteSerializedString (assoc.SourceTypeReference); + b.WriteSerializedString (assoc.AliasProxyTypeReference); + }); + _pe.Metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, _typeMapAssociationAttrCtorRef, blob); + } + + // ---- IgnoresAccessChecksTo ---- + + void EmitIgnoresAccessChecksToAttribute (List assemblyNames) + { + var metadata = _pe.Metadata; + var attributeTypeRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, + metadata.GetOrAddString ("System"), metadata.GetOrAddString ("Attribute")); + + int typeFieldStart = metadata.GetRowCount (TableIndex.Field) + 1; + int typeMethodStart = metadata.GetRowCount (TableIndex.MethodDef) + 1; + + var baseAttrCtorRef = _pe.AddMemberRef (attributeTypeRef, ".ctor", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { })); + + var ctorDef = _pe.EmitBody (".ctor", + MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName, + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (1, + rt => rt.Void (), + p => p.AddParameter ().Type ().String ()), + encoder => { + encoder.LoadArgument (0); + encoder.Call (baseAttrCtorRef); + encoder.OpCode (ILOpCode.Ret); + }); + + metadata.AddTypeDefinition ( + TypeAttributes.NotPublic | TypeAttributes.Sealed | TypeAttributes.BeforeFieldInit, + metadata.GetOrAddString ("System.Runtime.CompilerServices"), + metadata.GetOrAddString ("IgnoresAccessChecksToAttribute"), + attributeTypeRef, + MetadataTokens.FieldDefinitionHandle (typeFieldStart), + MetadataTokens.MethodDefinitionHandle (typeMethodStart)); + + foreach (var asmName in assemblyNames) { + var blob = _pe.BuildAttributeBlob (b => b.WriteSerializedString (asmName)); + metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, ctorDef, blob); + } + } +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs new file mode 100644 index 00000000000..927346fbf10 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// High-level API: builds the model from peers, then emits the PE assembly. +/// Composes + . +/// +sealed class TypeMapAssemblyGenerator +{ + readonly Version _systemRuntimeVersion; + + /// Version for System.Runtime assembly references. + public TypeMapAssemblyGenerator (Version systemRuntimeVersion) + { + _systemRuntimeVersion = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion)); + } + + /// + /// Generates a TypeMap PE assembly from the given Java peer info records. + /// + /// Scanned Java peer types. + /// Path where the output .dll will be written. + /// Optional explicit assembly name. Derived from outputPath if null. + public void Generate (IReadOnlyList peers, string outputPath, string? assemblyName = null) + { + var model = ModelBuilder.Build (peers, outputPath, assemblyName); + var emitter = new TypeMapAssemblyEmitter (_systemRuntimeVersion); + emitter.Emit (model, outputPath); + } +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index 85472f1b3ba..e8d0c5d6ba3 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -28,6 +28,16 @@ sealed record JavaPeerInfo /// public required string ManagedTypeName { get; init; } + /// + /// Managed type namespace, e.g., "Android.App". + /// + public required string ManagedTypeNamespace { get; init; } + + /// + /// Managed type short name (without namespace), e.g., "Activity". + /// + public required string ManagedTypeShortName { get; init; } + /// /// Assembly name the type belongs to, e.g., "Mono.Android". /// @@ -70,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. @@ -119,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. /// @@ -137,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 469d6345596..eabc73f7ee9 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -218,6 +218,8 @@ void ScanAssembly (AssemblyIndex index, Dictionary results JavaName = jniName, CompatJniName = compatJniName, ManagedTypeName = fullName, + ManagedTypeNamespace = ExtractNamespace (fullName), + ManagedTypeShortName = ExtractShortName (fullName), AssemblyName = index.AssemblyName, BaseJavaName = baseJavaName, ImplementedInterfaceJavaNames = implementedInterfaces, @@ -226,6 +228,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary results DoNotGenerateAcw = doNotGenerateAcw, IsUnconditional = isUnconditional, MarshalMethods = marshalMethods, + JavaConstructors = BuildJavaConstructors (marshalMethods), ActivationCtor = activationCtor, InvokerTypeName = invokerTypeName, IsGenericDefinition = isGenericDefinition, @@ -279,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, @@ -732,4 +738,47 @@ static string GetCrc64PackageName (string ns, string assemblyName) var hash = System.IO.Hashing.Crc64.Hash (data); return $"crc64{BitConverter.ToString (hash).Replace ("-", "").ToLowerInvariant ()}"; } + + static string ExtractNamespace (string fullName) + { + int lastDot = fullName.LastIndexOf ('.'); + return lastDot >= 0 ? fullName.Substring (0, lastDot) : ""; + } + + static string ExtractShortName (string fullName) + { + int lastDot = fullName.LastIndexOf ('.'); + string typePart = lastDot >= 0 ? fullName.Substring (lastDot + 1) : fullName; + int lastPlus = typePart.LastIndexOf ('+'); + 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 new file mode 100644 index 00000000000..4a168b636a9 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Xunit; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; + +public abstract class FixtureTestBase +{ + static string TestFixtureAssemblyPath { + get { + var testAssemblyDir = Path.GetDirectoryName (typeof (FixtureTestBase).Assembly.Location)!; + var fixtureAssembly = Path.Combine (testAssemblyDir, "TestFixtures.dll"); + Assert.True (File.Exists (fixtureAssembly), + $"TestFixtures.dll not found at {fixtureAssembly}. Ensure the TestFixtures project builds."); + return fixtureAssembly; + } + } + + static readonly Lazy> _cachedFixtures = new (() => { + using var scanner = new JavaPeerScanner (); + return scanner.Scan (new [] { TestFixtureAssemblyPath }); + }); + + protected static List ScanFixtures () => _cachedFixtures.Value; + + protected static JavaPeerInfo FindFixtureByJavaName (string javaName) + { + var peers = ScanFixtures (); + var peer = peers.FirstOrDefault (p => p.JavaName == javaName); + Assert.NotNull (peer); + return peer; + } + + protected static void CleanUpDir (string path) + { + var dir = Path.GetDirectoryName (path); + if (dir != null && Directory.Exists (dir)) + try { Directory.Delete (dir, true); } catch { } + } + + protected static JavaPeerInfo MakeMcwPeer (string jniName, string managedName, string asmName) + { + var ns = managedName.Contains ('.') ? managedName.Substring (0, managedName.LastIndexOf ('.')) : ""; + var typePart = managedName.Contains ('.') ? managedName.Substring (managedName.LastIndexOf ('.') + 1) : managedName; + var shortName = typePart.Contains ('+') ? typePart.Substring (typePart.LastIndexOf ('+') + 1) : typePart; + return new JavaPeerInfo { + JavaName = jniName, + ManagedTypeName = managedName, + ManagedTypeNamespace = ns, + ManagedTypeShortName = shortName, + AssemblyName = asmName, + }; + } + + protected static JavaPeerInfo MakePeerWithActivation (string jniName, string managedName, string asmName) + { + var peer = MakeMcwPeer (jniName, managedName, asmName); + peer.ActivationCtor = new ActivationCtorInfo { + Style = ActivationCtorStyle.XamarinAndroid, + }; + return peer; + } + + protected static JavaPeerInfo MakeAcwPeer (string jniName, string managedName, string asmName) + { + var peer = MakePeerWithActivation (jniName, managedName, asmName); + peer.DoNotGenerateAcw = false; + peer.JavaConstructors = new List { + new JavaConstructorInfo { ConstructorIndex = 0, JniSignature = "()V" }, + }; + peer.MarshalMethods = new List { + new MarshalMethodInfo { + JniName = "", + NativeCallbackName = "n_ctor", + JniSignature = "()V", + IsConstructor = true, + }, + }; + return peer; + } + + 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") + { + var ns = managedName.Contains ('.') ? managedName.Substring (0, managedName.LastIndexOf ('.')) : ""; + var shortName = managedName.Contains ('.') ? managedName.Substring (managedName.LastIndexOf ('.') + 1) : managedName; + return new JavaPeerInfo { + JavaName = jniName, + ManagedTypeName = managedName, + ManagedTypeNamespace = ns, + ManagedTypeShortName = shortName, + AssemblyName = asmName, + IsInterface = true, + InvokerTypeName = invokerName, + }; + } + + protected static MarshalMethodInfo MakeMarshalMethod (string jniName, string callbackName, string jniSig, bool isConstructor = false) + { + return new MarshalMethodInfo { + JniName = jniName, + NativeCallbackName = callbackName, + JniSignature = jniSig, + IsConstructor = isConstructor, + }; + } +} 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..17fc6ade84a --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs @@ -0,0 +1,350 @@ +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, JcwJavaSourceGenerator.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, JcwJavaSourceGenerator.GetJavaPackageName (jniName)); + } + + [Theory] + [InlineData ("com/example/MainActivity", "MainActivity")] + [InlineData ("com/example/Outer$Inner", "Outer$Inner")] + [InlineData ("TopLevelClass", "TopLevelClass")] + public void GetJavaSimpleName_ExtractsCorrectly (string jniName, string expected) + { + Assert.Equal (expected, JcwJavaSourceGenerator.GetJavaSimpleName (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, JcwJavaSourceGenerator.JniTypeToJava (jniType)); + } + + } + + public class Filtering + { + + [Fact] + public void Generate_SkipsMcwTypes () + { + var peers = ScanFixtures (); + var generator = new JcwJavaSourceGenerator (); + var outputDir = Path.Combine (Path.GetTempPath (), $"jcw-test-{Guid.NewGuid ():N}"); + try { + 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")); + } finally { + if (Directory.Exists (outputDir)) { + Directory.Delete (outputDir, true); + } + } + } + + } + + public class PackageDeclaration + { + + [Fact] + public void Generate_MainActivity_HasPackageDeclaration () + { + var java = GenerateFixture ("my/app/MainActivity"); + Assert.StartsWith ("package my.app;\n", 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_AbstractType_HasAbstractModifier () + { + var java = GenerateFixture ("my/app/AbstractBase"); + Assert.Contains ("public abstract class AbstractBase\n", java); + } + + [Fact] + public void Generate_TypeWithInterfaces_HasImplementsClause () + { + var java = GenerateFixture ("my/app/MultiInterfaceView"); + Assert.Contains ("\timplements\n", java); + Assert.Contains ("\t\tmono.android.IGCUserPeer", java); + Assert.Contains ("android.view.View$OnClickListener", java); + Assert.Contains ("android.view.View$OnLongClickListener", 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 + { + + [Theory] + [InlineData ("public CustomView ()\n")] + [InlineData ("public CustomView (android.content.Context p0)\n")] + [InlineData ("private native void nctor_0 ();\n")] + [InlineData ("private native void nctor_1 (android.content.Context p0);\n")] + [InlineData ("if (getClass () == CustomView.class) nctor_0 ();\n")] + public void Generate_CustomView_HasExpectedConstructorElement (string expectedContent) + { + var java = GenerateFixture ("my/app/CustomView"); + Assert.Contains (expectedContent, java); + } + + [Fact] + public void Generate_Constructor_WithSuperArgumentsString_UsesCustomSuperArgs () + { + // [Export] constructors with SuperArgumentsString should use it in super() call + var type = new JavaPeerInfo { + JavaName = "my/app/CustomService", + ManagedTypeName = "MyApp.CustomService", + ManagedTypeNamespace = "MyApp", + ManagedTypeShortName = "CustomService", + AssemblyName = "App", + BaseJavaName = "android/app/Service", + JavaConstructors = new List { + new JavaConstructorInfo { + JniSignature = "(Landroid/content/Context;I)V", + ConstructorIndex = 0, + Parameters = new List { + new JniParameterInfo { JniType = "Landroid/content/Context;" }, + new JniParameterInfo { JniType = "I" }, + }, + SuperArgumentsString = "p0", + }, + }, + }; + + var java = GenerateToString (type); + Assert.Contains ("super (p0);", java); + Assert.DoesNotContain ("super (p0, p1);", java); + } + + [Fact] + public void Generate_Constructor_WithEmptySuperArgumentsString_EmptySuper () + { + // Empty string means super() with no arguments + var type = new JavaPeerInfo { + JavaName = "my/app/MyWidget", + ManagedTypeName = "MyApp.MyWidget", + ManagedTypeNamespace = "MyApp", + ManagedTypeShortName = "MyWidget", + AssemblyName = "App", + BaseJavaName = "android/appwidget/AppWidgetProvider", + JavaConstructors = new List { + new JavaConstructorInfo { + JniSignature = "(Landroid/content/Context;)V", + ConstructorIndex = 0, + Parameters = new List { + new JniParameterInfo { JniType = "Landroid/content/Context;" }, + }, + SuperArgumentsString = "", + }, + }, + }; + + var java = GenerateToString (type); + Assert.Contains ("super ();", java); + Assert.DoesNotContain ("super (p0);", java); + } + + [Fact] + public void Generate_Constructor_WithoutSuperArgumentsString_ForwardsAllParams () + { + // null SuperArgumentsString means forward all params (default behavior) + var type = new JavaPeerInfo { + JavaName = "my/app/MyView", + ManagedTypeName = "MyApp.MyView", + ManagedTypeNamespace = "MyApp", + ManagedTypeShortName = "MyView", + AssemblyName = "App", + BaseJavaName = "android/view/View", + JavaConstructors = new List { + new JavaConstructorInfo { + JniSignature = "(Landroid/content/Context;Landroid/util/AttributeSet;)V", + ConstructorIndex = 0, + Parameters = new List { + new JniParameterInfo { JniType = "Landroid/content/Context;" }, + new JniParameterInfo { JniType = "Landroid/util/AttributeSet;" }, + }, + }, + }, + }; + + var java = GenerateToString (type); + Assert.Contains ("super (p0, p1);", java); + } + + } + + public class Method + { + + [Fact] + public void Generate_MarshalMethod_HasOverrideAndNativeDeclaration () + { + var java = GenerateFixture ("my/app/MainActivity"); + Assert.Contains ("@Override\n", java); + Assert.Contains ("public void onCreate (android.os.Bundle p0)\n", java); + Assert.Contains ("n_OnCreate (p0);\n", java); + Assert.Contains ("public native void n_OnCreate (android.os.Bundle p0);\n", java); + } + + [Fact] + public void Generate_MethodWithReturnValue_HasReturnStatement () + { + var java = GenerateFixture ("my/app/TouchHandler"); + Assert.Contains ("return n_OnTouch (p0, p1);\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 + { + + [Fact] + public void Generate_CreatesCorrectFileStructure () + { + var peers = ScanFixtures (); + var generator = new JcwJavaSourceGenerator (); + var outputDir = Path.Combine (Path.GetTempPath (), $"jcw-test-{Guid.NewGuid ():N}"); + try { + 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); + } + } finally { + if (Directory.Exists (outputDir)) { + Directory.Delete (outputDir, true); + } + } + } + + } + + 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); + } + + [Fact] + public void Generate_ExportWithoutThrows_HasNoThrowsClause () + { + var java = GenerateFixture ("my/app/ExportExample"); + Assert.DoesNotContain ("throws", java); + } + + } + + public class MethodReturnTypesAndParams + { + + [Theory] + [InlineData ("public boolean onTouch (android.view.View p0, int p1)")] + [InlineData ("public void onScroll (int p0, float p1, long p2, double p3)")] + [InlineData ("public java.lang.String getText ()")] + [InlineData ("public void setItems (java.lang.String[] p0)")] + public void Generate_TouchHandler_HasExpectedMethodSignature (string expectedSignature) + { + var java = GenerateFixture ("my/app/TouchHandler"); + Assert.Contains (expectedSignature + "\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 new file mode 100644 index 00000000000..000c97f11f3 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; +using Xunit; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; + +public class RootTypeMapAssemblyGeneratorTests : FixtureTestBase +{ + string GenerateRootAssembly (IReadOnlyList perAssemblyNames, string? assemblyName = null) + { + var outputPath = Path.Combine (Path.GetTempPath (), $"root-typemap-{Guid.NewGuid ():N}", + (assemblyName ?? "_Microsoft.Android.TypeMaps") + ".dll"); + var generator = new RootTypeMapAssemblyGenerator (new Version (11, 0, 0, 0)); + generator.Generate (perAssemblyNames, outputPath, assemblyName); + return outputPath; + } + + [Fact] + public void Generate_ProducesValidPEAssembly () + { + var path = GenerateRootAssembly (new [] { "_App.TypeMap", "_Mono.Android.TypeMap" }); + try { + Assert.True (File.Exists (path)); + using var pe = new PEReader (File.OpenRead (path)); + Assert.True (pe.HasMetadata); + } finally { + CleanUpDir (path); + } + } + + [Theory] + [InlineData (null, "_Microsoft.Android.TypeMaps")] + [InlineData ("MyRoot", "MyRoot")] + public void Generate_AssemblyName_MatchesExpected (string? assemblyName, string expectedName) + { + var path = GenerateRootAssembly (Array.Empty (), assemblyName); + try { + using var pe = new PEReader (File.OpenRead (path)); + var reader = pe.GetMetadataReader (); + var asmDef = reader.GetAssemblyDefinition (); + Assert.Equal (expectedName, reader.GetString (asmDef.Name)); + } finally { + CleanUpDir (path); + } + } + + [Fact] + public void Generate_ReferencesGenericTypeMapAssemblyTargetAttribute () + { + var path = GenerateRootAssembly (new [] { "_App.TypeMap" }); + try { + using var pe = new PEReader (File.OpenRead (path)); + var reader = pe.GetMetadataReader (); + + var typeRefs = reader.TypeReferences + .Select (h => reader.GetTypeReference (h)) + .ToList (); + Assert.Contains (typeRefs, t => + reader.GetString (t.Name) == "TypeMapAssemblyTargetAttribute`1" && + reader.GetString (t.Namespace) == "System.Runtime.InteropServices"); + + Assert.Contains (typeRefs, t => + reader.GetString (t.Name) == "Object" && + reader.GetString (t.Namespace) == "Java.Lang"); + + var typeDefs = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .ToList (); + Assert.DoesNotContain (typeDefs, t => + reader.GetString (t.Name).Contains ("TypeMapAssemblyTarget")); + } finally { + CleanUpDir (path); + } + } + + [Fact] + public void Generate_EmptyList_ProducesValidAssemblyWithNoTargetAttributes () + { + var path = GenerateRootAssembly (Array.Empty ()); + try { + using var pe = new PEReader (File.OpenRead (path)); + var reader = pe.GetMetadataReader (); + var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); + Assert.Empty (asmAttrs); + } finally { + CleanUpDir (path); + } + } + + [Fact] + public void Generate_MultipleTargets_HasCorrectAttributeCount () + { + var targets = new [] { "_App.TypeMap", "_Mono.Android.TypeMap", "_Java.Interop.TypeMap" }; + var path = GenerateRootAssembly (targets); + try { + using var pe = new PEReader (File.OpenRead (path)); + var reader = pe.GetMetadataReader (); + var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); + Assert.Equal (3, asmAttrs.Count ()); + } finally { + CleanUpDir (path); + } + } + + [Fact] + public void Generate_AttributeBlobValues_MatchTargetNames () + { + var targets = new [] { "_App.TypeMap", "_Mono.Android.TypeMap" }; + var path = GenerateRootAssembly (targets); + try { + using var pe = new PEReader (File.OpenRead (path)); + var reader = pe.GetMetadataReader (); + + var attrValues = new List (); + foreach (var attrHandle in reader.GetCustomAttributes (EntityHandle.AssemblyDefinition)) { + var attr = reader.GetCustomAttribute (attrHandle); + var blob = reader.GetBlobReader (attr.Value); + + // Custom attribute blob: prolog (2 bytes) + SerString value + var prolog = blob.ReadUInt16 (); + Assert.Equal (1, prolog); // ECMA-335 prolog + var value = blob.ReadSerializedString (); + Assert.NotNull (value); + attrValues.Add (value!); + } + + Assert.Equal (2, attrValues.Count); + Assert.Contains ("_App.TypeMap", attrValues); + Assert.Contains ("_Mono.Android.TypeMap", attrValues); + } finally { + CleanUpDir (path); + } + } +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs new file mode 100644 index 00000000000..d83d7db35eb --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -0,0 +1,424 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using System.Reflection.PortableExecutable; +using Xunit; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; + +public class TypeMapAssemblyGeneratorTests : FixtureTestBase +{ + static string GenerateAssembly (IReadOnlyList peers, string? assemblyName = null) + { + var outputPath = Path.Combine (Path.GetTempPath (), $"typemap-test-{Guid.NewGuid ():N}", + (assemblyName ?? "TestTypeMap") + ".dll"); + var generator = new TypeMapAssemblyGenerator (new Version (11, 0, 0, 0)); + generator.Generate (peers, outputPath, assemblyName); + return outputPath; + } + + static (PEReader pe, MetadataReader reader) OpenAssembly (string path) + { + var pe = new PEReader (File.OpenRead (path)); + return (pe, pe.GetMetadataReader ()); + } + + static List GetMemberRefNames (MetadataReader reader) => + Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) + .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i))) + .Select (m => reader.GetString (m.Name)) + .ToList (); + + static List GetTypeRefNames (MetadataReader reader) => + reader.TypeReferences + .Select (h => reader.GetTypeReference (h)) + .Select (t => reader.GetString (t.Name)) + .ToList (); + + public class BasicAssemblyStructure + { + + [Fact] + public void Generate_ProducesValidPEAssembly () + { + var peers = ScanFixtures (); + var path = GenerateAssembly (peers); + try { + Assert.True (File.Exists (path)); + using var pe = new PEReader (File.OpenRead (path)); + Assert.True (pe.HasMetadata); + var reader = pe.GetMetadataReader (); + Assert.NotNull (reader); + } finally { + CleanUpDir (path); + } + } + + } + + public class AssemblyReference + { + + [Fact] + public void Generate_HasRequiredAssemblyReferences () + { + var peers = ScanFixtures (); + var path = GenerateAssembly (peers); + try { + 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); + } + } finally { + CleanUpDir (path); + } + } + + } + + public class ProxyType + { + + [Fact] + public void Generate_CreatesProxyTypes () + { + var peers = ScanFixtures (); + var path = GenerateAssembly (peers); + try { + 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"); + } + } finally { + CleanUpDir (path); + } + } + + [Fact] + public void Generate_ProxyType_HasCtorAndCreateInstance () + { + var peers = ScanFixtures (); + var path = GenerateAssembly (peers); + try { + 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); + } + } finally { + CleanUpDir (path); + } + } + + } + + public class IgnoresAccessChecksTo + { + + [Fact] + public void Generate_HasIgnoresAccessChecksToAttribute () + { + var peers = ScanFixtures (); + var path = GenerateAssembly (peers); + try { + 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"); + } + } finally { + CleanUpDir (path); + } + } + + } + + public class Alias + { + + static List MakeDuplicateAliasPeers () => new List { + new JavaPeerInfo { + JavaName = "test/Duplicate", + ManagedTypeName = "Test.Duplicate1", + ManagedTypeNamespace = "Test", + ManagedTypeShortName = "Duplicate1", + AssemblyName = "TestAssembly", + ActivationCtor = new ActivationCtorInfo { + DeclaringTypeName = "Test.Duplicate1", + DeclaringAssemblyName = "TestAssembly", + Style = ActivationCtorStyle.XamarinAndroid, + }, + }, + new JavaPeerInfo { + JavaName = "test/Duplicate", + ManagedTypeName = "Test.Duplicate2", + ManagedTypeNamespace = "Test", + ManagedTypeShortName = "Duplicate2", + AssemblyName = "TestAssembly", + }, + }; + + [Fact] + public void Generate_DuplicateJniNames_CreatesAliasEntries () + { + var peers = MakeDuplicateAliasPeers (); + + var path = GenerateAssembly (peers, "AliasTest"); + try { + var (pe, reader) = OpenAssembly (path); + using (pe) { + var assemblyAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); + Assert.True (assemblyAttrs.Count () >= 3); + } + } finally { + CleanUpDir (path); + } + } + + [Fact] + public void Generate_DuplicateJniNames_EmitsTypeMapAssociationAttribute () + { + var peers = MakeDuplicateAliasPeers (); + + var path = GenerateAssembly (peers, "AliasAssocTest"); + try { + 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); + } + } finally { + CleanUpDir (path); + } + } + + } + + public class EmptyInput + { + + [Fact] + public void Generate_EmptyPeerList_ProducesValidAssembly () + { + var path = GenerateAssembly (Array.Empty (), "EmptyTest"); + try { + 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)); + } + } finally { + CleanUpDir (path); + } + } + + } + + public class JniSignatureHelperTests + { + + [Theory] + [InlineData ("()V", 0)] + [InlineData ("(I)V", 1)] + [InlineData ("(Landroid/os/Bundle;)V", 1)] + [InlineData ("(IFJ)V", 3)] + [InlineData ("(ZLandroid/view/View;I)Z", 3)] + [InlineData ("([Ljava/lang/String;)V", 1)] + public void ParseParameterTypes_ParsesCorrectCount (string signature, int expectedCount) + { + var actual = JniSignatureHelper.ParseParameterTypes (signature); + Assert.Equal (expectedCount, actual.Count); + } + + [Theory] + [InlineData ("(Z)V", JniParamKind.Boolean)] + [InlineData ("(Ljava/lang/String;)V", JniParamKind.Object)] + public void ParseParameterTypes_SingleParam_MapsToCorrectKind (string signature, JniParamKind expectedKind) + { + var types = JniSignatureHelper.ParseParameterTypes (signature); + Assert.Single (types); + Assert.Equal (expectedKind, types [0]); + } + + [Theory] + [InlineData ("()V", JniParamKind.Void)] + [InlineData ("()I", JniParamKind.Int)] + [InlineData ("()Z", JniParamKind.Boolean)] + [InlineData ("()Ljava/lang/String;", JniParamKind.Object)] + public void ParseReturnType_MapsToCorrectKind (string signature, JniParamKind expectedKind) + { + Assert.Equal (expectedKind, JniSignatureHelper.ParseReturnType (signature)); + } + + } + + public class NegativeEdgeCase + { + + [Theory] + [InlineData ("")] + [InlineData ("not-a-sig")] + [InlineData ("(")] + public void ParseParameterTypes_InvalidSignature_ThrowsOrReturnsEmpty (string signature) + { + try { + var result = JniSignatureHelper.ParseParameterTypes (signature); + Assert.NotNull (result); + } catch (Exception ex) when (ex is ArgumentException || ex is IndexOutOfRangeException || ex is FormatException) { + } + } + + } + + public class CreateInstancePaths + { + + [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 }, "InheritedCtorTest"); + try { + 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); + } + } finally { + CleanUpDir (path); + } + } + + [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 }, "LeafCtorTest"); + try { + 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"); + } + } finally { + CleanUpDir (path); + } + } + + [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 }, "GenericTest"); + try { + var (pe, reader) = OpenAssembly (path); + using (pe) { + var typeNames = GetTypeRefNames (reader); + Assert.Contains ("NotSupportedException", typeNames); + } + } finally { + CleanUpDir (path); + } + } + + } + + public class IgnoresAccessChecksToForBaseCtor + { + + [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 }, "IgnoresAccessTest"); + try { + 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")); + } + } finally { + CleanUpDir (path); + } + } + + } + +} \ 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 new file mode 100644 index 00000000000..cb2cf936100 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -0,0 +1,793 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; +using Xunit; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; + +public class ModelBuilderTests : FixtureTestBase +{ + static TypeMapAssemblyData BuildModel (IReadOnlyList peers, string? assemblyName = null) + { + 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 (Array.Empty (), "Empty"); + Assert.Equal ("Empty", model.AssemblyName); + Assert.Equal ("Empty.dll", model.ModuleName); + Assert.Empty (model.Entries); + Assert.Empty (model.ProxyTypes); + } + + [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 (Array.Empty (), "/some/path/Foo.dll", "MyAssembly"); + Assert.Equal ("MyAssembly", model.AssemblyName); + } + + } + + public class TypeMapEntries + { + + [Fact] + public void Build_CreatesOneEntryPerPeer () + { + var peers = new List { + MakeMcwPeer ("java/lang/Object", "Java.Lang.Object", "Mono.Android"), + MakeMcwPeer ("android/app/Activity", "Android.App.Activity", "Mono.Android"), + }; + + var model = BuildModel (peers); + Assert.Equal (2, model.Entries.Count); + Assert.Equal ("android/app/Activity", model.Entries [0].JniName); + Assert.Equal ("java/lang/Object", model.Entries [1].JniName); + } + + [Fact] + public void Build_DuplicateJniNames_CreatesAliasEntries () + { + var peers = new List { + MakeMcwPeer ("test/Dup", "Test.First", "A"), + MakeMcwPeer ("test/Dup", "Test.Second", "A"), + }; + + var model = BuildModel (peers); + // Two entries: primary "test/Dup" and alias "test/Dup[1]" + Assert.Equal (2, model.Entries.Count); + Assert.Equal ("test/Dup", model.Entries [0].JniName); + 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); + } + + } + + public class ConditionalAttributes + { + + [Theory] + [InlineData ("java/lang/Object")] + [InlineData ("java/lang/Throwable")] + [InlineData ("java/lang/Exception")] + [InlineData ("java/lang/RuntimeException")] + [InlineData ("java/lang/Error")] + [InlineData ("java/lang/Class")] + [InlineData ("java/lang/String")] + [InlineData ("java/lang/Thread")] + public void Build_AllEssentialRuntimeTypes_AreUnconditional (string jniName) + { + var peer = MakeMcwPeer (jniName, "Java.Lang.SomeType", "Mono.Android"); + peer.DoNotGenerateAcw = true; + var model = BuildModel (new [] { peer }); + Assert.True (model.Entries [0].IsUnconditional, $"{jniName} should be unconditional"); + } + + [Fact] + public void Build_UserAcwType_IsUnconditional () + { + // User-defined ACW types (not MCW, not interface) are unconditional + // because Android can instantiate them from Java + var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App"); + var model = BuildModel (new [] { peer }); + + var mainEntry = model.Entries.First (e => e.JniName == "my/app/Main"); + Assert.True (mainEntry.IsUnconditional); + Assert.Null (mainEntry.TargetTypeReference); + } + + [Fact] + 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"); + peer.DoNotGenerateAcw = true; + var model = BuildModel (new [] { peer }); + + Assert.Single (model.Entries); + Assert.False (model.Entries [0].IsUnconditional); + Assert.NotNull (model.Entries [0].TargetTypeReference); + Assert.Contains ("Android.App.Activity, Mono.Android", model.Entries [0].TargetTypeReference!); + } + + [Fact] + public void Build_UnconditionalScannedType_IsUnconditional () + { + // Types with IsUnconditional from scanner (e.g., from [Activity], [Service] attrs) + var peer = MakeMcwPeer ("my/app/MySvc", "MyApp.MyService", "App"); + peer.DoNotGenerateAcw = true; // simulate MCW-like + peer.IsUnconditional = true; // scanner marked it + var model = BuildModel (new [] { peer }); + + Assert.True (model.Entries [0].IsUnconditional); + } + + } + + public class Aliases + { + + [Fact] + public void Build_AliasedPeersWithActivation_GetDistinctProxies () + { + var peers = new List { + MakePeerWithActivation ("test/Dup", "Test.First", "A"), + MakePeerWithActivation ("test/Dup", "Test.Second", "A"), + }; + + var model = BuildModel (peers, "TypeMap"); + Assert.Equal (2, model.ProxyTypes.Count); + Assert.Equal ("Test_First_Proxy", model.ProxyTypes [0].TypeName); + Assert.Equal ("Test_Second_Proxy", model.ProxyTypes [1].TypeName); + } + + [Fact] + public void Build_McwPeerWithoutActivation_NoProxy () + { + var peer = MakeMcwPeer ("java/lang/Object", "Java.Lang.Object", "Mono.Android"); + var model = BuildModel (new [] { peer }); + + Assert.Empty (model.ProxyTypes); + Assert.Single (model.Entries); + Assert.Contains ("Java.Lang.Object, Mono.Android", model.Entries [0].ProxyTypeReference); + } + + } + + public class ProxyTypes + { + + [Fact] + public void Build_PeerWithActivationCtor_CreatesProxy () + { + 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 ("Java_Lang_Object_Proxy", 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); + } + + [Fact] + public void Build_PeerWithInvoker_CreatesProxy () + { + var peer = MakeInterfacePeer (); + + var model = BuildModel (new [] { peer }); + Assert.Single (model.ProxyTypes); + var proxy = model.ProxyTypes [0]; + 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 FixtureScan + { + + [Fact] + public void Build_FromScannedFixtures_ProducesValidModel () + { + var peers = ScanFixtures (); + var model = BuildModel (peers, "TestTypeMap"); + + Assert.Equal ("TestTypeMap", model.AssemblyName); + Assert.NotEmpty (model.Entries); + Assert.NotEmpty (model.ProxyTypes); + + Assert.All (model.Entries, e => Assert.False (string.IsNullOrEmpty (e.JniName))); + Assert.All (model.Entries, e => Assert.False (string.IsNullOrEmpty (e.ProxyTypeReference))); + } + + [Theory] + [InlineData ("my/app/MainActivity", "MainActivity")] + [InlineData ("android/app/Activity", "Activity")] + [InlineData ("java/lang/Object", "Object")] + [InlineData ("my/app/Outer$Inner", "Inner")] + [InlineData ("my/app/ICallback$Result", "Result")] + public void ScanFixtures_ManagedTypeShortName_IsCorrect (string javaName, string expectedShortName) + { + var peer = FindFixtureByJavaName (javaName); + Assert.Equal (expectedShortName, peer.ManagedTypeShortName); + } + + } + + public class FixtureConditionalAttributes + { + + [Theory] + [InlineData ("my/app/MainActivity")] + [InlineData ("my/app/TouchHandler")] + public void Fixture_UserAcwType_IsUnconditional (string javaName) + { + var peer = FindFixtureByJavaName (javaName); + Assert.False (peer.DoNotGenerateAcw); + var model = BuildModel (new [] { peer }); + Assert.True (model.Entries [0].IsUnconditional); + } + + [Theory] + [InlineData ("android/app/Activity")] + [InlineData ("android/widget/Button")] + public void Fixture_McwBinding_IsTrimmable (string javaName) + { + var peer = FindFixtureByJavaName (javaName); + Assert.True (peer.DoNotGenerateAcw); + var model = BuildModel (new [] { peer }); + Assert.False (model.Entries [0].IsUnconditional); + } + + } + + static JavaPeerProxyData? FindProxy (TypeMapAssemblyData model, string proxyTypeName) + { + return model.ProxyTypes.FirstOrDefault (p => p.TypeName == proxyTypeName); + } + + static TypeMapAttributeData? FindEntry (TypeMapAssemblyData model, string jniName) + { + 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")] + [InlineData ("java/lang/Throwable", "Java_Lang_Throwable_Proxy", "Java.Lang.Throwable")] + [InlineData ("java/lang/Exception", "Java_Lang_Exception_Proxy", "Java.Lang.Exception")] + public void Fixture_McwType_HasActivation_CreatesProxy (string javaName, string expectedProxyName, string expectedManagedName) + { + var peer = FindFixtureByJavaName (javaName); + var model = BuildModel (new [] { peer }, "TypeMap"); + + var proxy = FindProxy (model, expectedProxyName); + Assert.NotNull (proxy); + Assert.True (proxy!.HasActivation); + Assert.Equal (expectedManagedName, proxy.TargetType.ManagedTypeName); + } + + [Fact] + public void Fixture_Activity_Entry_PointsToProxy () + { + var peer = FindFixtureByJavaName ("android/app/Activity"); + var model = BuildModel (new [] { peer }, "MyTypeMap"); + + var entry = FindEntry (model, "android/app/Activity"); + Assert.NotNull (entry); + Assert.Contains ("Android_App_Activity_Proxy", entry!.ProxyTypeReference); + Assert.Contains ("MyTypeMap", entry.ProxyTypeReference); + } + + [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 FixtureCustomView + { + + [Fact] + public void Fixture_CustomView_HasTwoConstructors () + { + var peer = FindFixtureByJavaName ("my/app/CustomView"); + Assert.Equal (2, peer.JavaConstructors.Count); + + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_CustomView_Proxy"); + Assert.NotNull (proxy); + } + + } + + public class FixtureInterfaces + { + + [Fact] + public void Fixture_IOnClickListener_HasInvokerProxy () + { + var peers = ScanFixtures (); + var listener = peers.FirstOrDefault (p => p.ManagedTypeName == "Android.Views.IOnClickListener"); + Assert.NotNull (listener); + Assert.True (listener!.IsInterface); + Assert.NotNull (listener.InvokerTypeName); + + var model = BuildModel (new [] { listener }, "TypeMap"); + var proxy = model.ProxyTypes.FirstOrDefault (); + Assert.NotNull (proxy); + 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")] + public void Fixture_NestedType_ProxyNaming (string javaName, string expectedProxyName, string expectedManagedName) + { + var peer = FindFixtureByJavaName (javaName); + var model = BuildModel (new [] { peer }, "TypeMap"); + + var entry = FindEntry (model, javaName); + Assert.NotNull (entry); + + if (peer.ActivationCtor != null) { + var proxy = FindProxy (model, expectedProxyName); + Assert.NotNull (proxy); + Assert.Equal (expectedManagedName, proxy!.TargetType.ManagedTypeName); + } + } + + } + + public class FixtureInvokers + { + + [Fact] + public void Fixture_InterfaceAndInvoker_ShareJniName_InvokerSeparated () + { + var peers = ScanFixtures (); + // IOnClickListener and IOnClickListenerInvoker share "android/view/View$OnClickListener" + var clickPeers = peers.Where (p => p.JavaName == "android/view/View$OnClickListener").ToList (); + Assert.Equal (2, clickPeers.Count); + + var model = BuildModel (clickPeers, "TypeMap"); + + // Invoker is excluded entirely — no TypeMap entry, no proxy. + // Only the interface gets a TypeMap entry and a proxy. + Assert.Single (model.Entries); + Assert.Equal ("android/view/View$OnClickListener", model.Entries [0].JniName); + + // Only the interface proxy exists; the invoker type is referenced + // only as a TypeRef in the interface proxy's InvokerType property. + Assert.Single (model.ProxyTypes); + Assert.NotNull (model.ProxyTypes [0].InvokerType); + Assert.Equal ("Android.Views.IOnClickListenerInvoker", model.ProxyTypes [0].InvokerType!.ManagedTypeName); + } + + [Fact] + 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"); + invokerPeer.DoNotGenerateAcw = true; + + var model = BuildModel (new [] { ifacePeer, invokerPeer }); + + // Only the interface gets a TypeMap entry — its ProxyTypeReference points to the generated proxy + Assert.Single (model.Entries); + Assert.Contains ("MyApp_IFoo_Proxy", model.Entries [0].ProxyTypeReference); + + // Only the interface gets a proxy — the invoker is referenced, not proxied + Assert.Single (model.ProxyTypes); + var proxy = model.ProxyTypes [0]; + Assert.Equal ("MyApp.IFoo", proxy.TargetType.ManagedTypeName); + Assert.NotNull (proxy.InvokerType); + Assert.Equal ("MyApp.FooInvoker", proxy.InvokerType!.ManagedTypeName); + + // Interface proxy has activation because it will create the invoker + Assert.True (proxy.HasActivation); + } + + } + + public class FixtureGenericHolder + { + + [Fact] + public void Fixture_GenericHolder_Entry () + { + var peer = FindFixtureByJavaName ("my/app/GenericHolder"); + Assert.True (peer.IsGenericDefinition); + + var model = BuildModel (new [] { peer }, "TypeMap"); + 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")] + [InlineData ("my/app/MultiInterfaceView", "MyApp_MultiInterfaceView_Proxy")] + [InlineData ("my/app/ExportExample", "MyApp_ExportExample_Proxy")] + public void Fixture_AcwType_HasProxy (string javaName, string expectedProxyName) + { + var peer = FindFixtureByJavaName (javaName); + Assert.False (peer.DoNotGenerateAcw); + + var model = BuildModel (new [] { peer }, "TypeMap"); + + if (peer.ActivationCtor != null && peer.MarshalMethods.Count > 0) { + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == expectedProxyName); + Assert.NotNull (proxy); + } + } + + } + + public class FixtureImplementorsAndDispatchers + { + + [Theory] + [InlineData ("android/view/View_IOnClickListenerImplementor", "Implementor")] + [InlineData ("android/view/View_ClickEventDispatcher", "EventDispatcher")] + public void Fixture_HelperType_IsTrimmable_NotUnconditional (string javaName, string kind) + { + var peer = FindFixtureByJavaName (javaName); + Assert.False (peer.DoNotGenerateAcw); + Assert.False (peer.IsInterface); + + var model = BuildModel (new [] { peer }, "TypeMap"); + + var entry = model.Entries.FirstOrDefault (); + Assert.NotNull (entry); + Assert.False (entry!.IsUnconditional, $"{kind} should NOT be unconditional"); + Assert.NotNull (entry.TargetTypeReference); + } + + } + + 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"); + invokerPeer.DoNotGenerateAcw = true; + + // Without a referencing peer, it gets a normal entry + var model1 = BuildModel (new [] { invokerPeer }); + Assert.Single (model1.Entries); + + // When an interface references it as invoker, it is excluded + var ifacePeer = MakeInterfacePeer ("my/app/MyInvoker", "MyApp.IMyInterface", "App", "MyApp.MyInvoker"); + var model2 = BuildModel (new [] { ifacePeer, invokerPeer }); + // Only the interface gets entries/proxies, the invoker is excluded + Assert.Single (model2.Entries); + Assert.Equal ("MyApp.IMyInterface", model2.ProxyTypes [0].TargetType.ManagedTypeName); + } + + } + + public class PipelineTests + { + + [Fact] + public void FullPipeline_AllFixtures_ProducesLoadableAssembly () + { + var peers = ScanFixtures (); + var model = BuildModel (peers, "FullPipeline"); + + EmitAndVerify (model, "FullPipeline", (pe, reader) => { + Assert.True (pe.HasMetadata); + + var asmDef = reader.GetAssemblyDefinition (); + Assert.Equal ("FullPipeline", reader.GetString (asmDef.Name)); + + var proxyTypes = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .Where (t => reader.GetString (t.Namespace) == "_TypeMap.Proxies") + .ToList (); + Assert.Equal (model.ProxyTypes.Count, proxyTypes.Count); + + var proxyNames = proxyTypes.Select (t => reader.GetString (t.Name)).OrderBy (n => n).ToList (); + var modelNames = model.ProxyTypes.Select (p => p.TypeName).OrderBy (n => n).ToList (); + Assert.Equal (modelNames, proxyNames); + }); + } + + [Fact] + public void FullPipeline_AllFixtures_TypeMapAttributeCountMatchesEntries () + { + var peers = ScanFixtures (); + var model = BuildModel (peers, "AttrCount"); + + EmitAndVerify (model, "AttrCount", (pe, reader) => { + var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); + int totalAttrs = asmAttrs.Count (); + + int expected = model.Entries.Count + model.IgnoresAccessChecksTo.Count; + Assert.Equal (expected, totalAttrs); + }); + } + + [Fact] + public void FullPipeline_CustomView_HasConstructorAndMethodWrappers () + { + var peer = FindFixtureByJavaName ("my/app/CustomView"); + var model = BuildModel (new [] { peer }, "CtorTest"); + + EmitAndVerify (model, "CtorTest", (pe, reader) => { + var proxy = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .First (t => reader.GetString (t.Name) == "MyApp_CustomView_Proxy"); + + var methodNames = proxy.GetMethods () + .Select (h => reader.GetString (reader.GetMethodDefinition (h).Name)) + .ToList (); + + Assert.Contains (".ctor", methodNames); + Assert.Contains ("CreateInstance", methodNames); + Assert.Contains ("get_TargetType", methodNames); + }); + } + + [Fact] + public void FullPipeline_GenericHolder_ProducesValidAssembly () + { + var peer = FindFixtureByJavaName ("my/app/GenericHolder"); + var model = BuildModel (new [] { peer }, "GenericTest"); + + EmitAndVerify (model, "GenericTest", (pe, reader) => { + Assert.True (pe.HasMetadata); + var entry = FindEntry (model, "my/app/GenericHolder"); + Assert.NotNull (entry); + + var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); + Assert.NotEmpty (asmAttrs); + }); + } + + } + + public class PeBlobValidation + { + + [Fact] + public void FullPipeline_Mixed2ArgAnd3Arg_BothSurviveRoundTrip () + { + // java/lang/Object → essential → 2-arg unconditional + var objectPeer = FindFixtureByJavaName ("java/lang/Object"); + // android/app/Activity → MCW → 3-arg trimmable + var activityPeer = FindFixtureByJavaName ("android/app/Activity"); + + var model = BuildModel (new [] { objectPeer, activityPeer }, "MixedBlob"); + Assert.Equal (2, model.Entries.Count); + + EmitAndVerify (model, "MixedBlob", (pe, reader) => { + var attrs = ReadAllTypeMapAttributeBlobs (reader); + Assert.Equal (2, attrs.Count); + + var unconditional = attrs.FirstOrDefault (a => a.jniName == "java/lang/Object"); + Assert.NotNull (unconditional.jniName); + Assert.Null (unconditional.targetRef); + + var trimmable = attrs.FirstOrDefault (a => a.jniName == "android/app/Activity"); + Assert.NotNull (trimmable.jniName); + Assert.NotNull (trimmable.targetRef); + Assert.Contains ("Android.App.Activity", trimmable.targetRef!); + }); + } + + [Theory] + [InlineData ("java/lang/Object", "Blob2Arg", "Java_Lang_Object_Proxy")] + [InlineData ("my/app/MainActivity", "BlobAcw", "MyApp_MainActivity_Proxy")] + public void FullPipeline_UnconditionalType_Emits2ArgAttribute (string javaName, string assemblyName, string expectedProxyName) + { + var peer = FindFixtureByJavaName (javaName); + var model = BuildModel (new [] { peer }, assemblyName); + Assert.Single (model.Entries); + Assert.True (model.Entries [0].IsUnconditional); + + EmitAndVerify (model, assemblyName, (pe, reader) => { + var (jniName2, proxyRef, targetRef) = ReadFirstTypeMapAttributeBlob (reader); + + Assert.Equal (javaName, jniName2); + Assert.NotNull (proxyRef); + Assert.Contains (expectedProxyName, proxyRef!); + Assert.Null (targetRef); + }); + } + + [Fact] + public void FullPipeline_McwBinding_Emits3ArgAttribute () + { + // android/app/Activity is MCW → trimmable 3-arg attribute + var peer = FindFixtureByJavaName ("android/app/Activity"); + var model = BuildModel (new [] { peer }, "Blob3Arg"); + Assert.Single (model.Entries); + Assert.False (model.Entries [0].IsUnconditional); + + EmitAndVerify (model, "Blob3Arg", (pe, reader) => { + var (jniName, proxyRef, targetRef) = ReadFirstTypeMapAttributeBlob (reader); + + Assert.Equal ("android/app/Activity", jniName); + Assert.NotNull (proxyRef); + Assert.Contains ("Android_App_Activity_Proxy", proxyRef!); + Assert.NotNull (targetRef); + Assert.Contains ("Android.App.Activity", targetRef!); + }); + } + + } + + public class DeterminismTests + { + + [Fact] + public void Build_SameInput_ProducesDeterministicOutput () + { + var peers = ScanFixtures (); + + var model1 = BuildModel (peers, "DetTest"); + var model2 = BuildModel (peers, "DetTest"); + + Assert.Equal (model1.Entries.Count, model2.Entries.Count); + for (int i = 0; i < model1.Entries.Count; i++) { + Assert.Equal (model1.Entries [i].JniName, model2.Entries [i].JniName); + Assert.Equal (model1.Entries [i].ProxyTypeReference, model2.Entries [i].ProxyTypeReference); + Assert.Equal (model1.Entries [i].TargetTypeReference, model2.Entries [i].TargetTypeReference); + } + } + + } + + static void EmitAndVerify (TypeMapAssemblyData model, string assemblyName, Action verify) + { + var outputPath = Path.Combine (Path.GetTempPath (), $"{assemblyName.ToLowerInvariant ()}-{Guid.NewGuid ():N}", $"{assemblyName}.dll"); + try { + 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 { + CleanUpDir (outputPath); + } + } + + /// + /// Reads the first TypeMap assembly-level attribute blob and returns (jniName, proxyRef, targetRef). + /// targetRef is null for 2-arg attributes. + /// + static (string? jniName, string? proxyRef, string? targetRef) ReadFirstTypeMapAttributeBlob (MetadataReader reader) + { + var all = ReadAllTypeMapAttributeBlobs (reader); + if (all.Count == 0) { + throw new InvalidOperationException ("No TypeMap attribute found on assembly"); + } + return all [0]; + } + + /// + /// Reads TypeMap attribute blobs from a PE assembly's metadata. + /// + /// NOTE: This is a PE-level integration test helper, not a primary unit test mechanism. + /// The model-level tests (which verify TypeMapAssemblyData directly) are the main unit tests. + /// These PE round-trip tests exist to catch encoding bugs in the emitter and to verify that + /// the full scan→model→emit pipeline produces a valid, loadable assembly. + /// + /// The distinction between TypeMap and IgnoresAccessChecksTo attributes relies on + /// attr.Constructor.Kind: TypeMap attributes reference their ctor via MemberReference + /// (because the attribute type is a TypeSpec — generic), while IgnoresAccessChecksTo + /// uses MethodDefinition (the attribute type is defined in the same assembly as a TypeDef). + /// If this logic breaks, the test will either fail to find TypeMap attributes or + /// misidentify IgnoresAccessChecksTo as TypeMap — both cause obvious assertion failures. + /// + static List<(string? jniName, string? proxyRef, string? targetRef)> ReadAllTypeMapAttributeBlobs (MetadataReader reader) + { + var result = new List<(string?, string?, string?)> (); + var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); + foreach (var attrHandle in asmAttrs) { + var attr = reader.GetCustomAttribute (attrHandle); + // Skip IgnoresAccessChecksTo attributes (their ctor is a MethodDefinition, not MemberRef) + if (attr.Constructor.Kind == HandleKind.MethodDefinition) + continue; + + var blobReader = reader.GetBlobReader (attr.Value); + ushort prolog = blobReader.ReadUInt16 (); + if (prolog != 1) + continue; + + string? jniName = blobReader.ReadSerializedString (); + string? proxyRef = blobReader.ReadSerializedString (); + + // Try to read third arg (target type) — if remaining bytes are just NumNamed (2 bytes), it's 2-arg + string? targetRef = null; + if (blobReader.RemainingBytes > 2) { + targetRef = blobReader.ReadSerializedString (); + } + + result.Add ((jniName, proxyRef, targetRef)); + } + return result; + } +} \ No newline at end of file From cb5bd678ae182ac2a42dc80349dacac757c78472 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 19 Feb 2026 11:33:18 +0100 Subject: [PATCH 02/40] =?UTF-8?q?Remove=20JcwJavaSourceGeneratorTests.cs?= =?UTF-8?q?=20=E2=80=94=20belongs=20in=20PR=20#10830?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This entire test file references JcwJavaSourceGenerator which doesn't exist in this PR or anywhere in the repo yet. It belongs with PR #10830 (JCW Java Source Generation) which introduces that class. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/JcwJavaSourceGeneratorTests.cs | 350 ------------------ 1 file changed, 350 deletions(-) delete mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs deleted file mode 100644 index 17fc6ade84a..00000000000 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs +++ /dev/null @@ -1,350 +0,0 @@ -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, JcwJavaSourceGenerator.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, JcwJavaSourceGenerator.GetJavaPackageName (jniName)); - } - - [Theory] - [InlineData ("com/example/MainActivity", "MainActivity")] - [InlineData ("com/example/Outer$Inner", "Outer$Inner")] - [InlineData ("TopLevelClass", "TopLevelClass")] - public void GetJavaSimpleName_ExtractsCorrectly (string jniName, string expected) - { - Assert.Equal (expected, JcwJavaSourceGenerator.GetJavaSimpleName (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, JcwJavaSourceGenerator.JniTypeToJava (jniType)); - } - - } - - public class Filtering - { - - [Fact] - public void Generate_SkipsMcwTypes () - { - var peers = ScanFixtures (); - var generator = new JcwJavaSourceGenerator (); - var outputDir = Path.Combine (Path.GetTempPath (), $"jcw-test-{Guid.NewGuid ():N}"); - try { - 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")); - } finally { - if (Directory.Exists (outputDir)) { - Directory.Delete (outputDir, true); - } - } - } - - } - - public class PackageDeclaration - { - - [Fact] - public void Generate_MainActivity_HasPackageDeclaration () - { - var java = GenerateFixture ("my/app/MainActivity"); - Assert.StartsWith ("package my.app;\n", 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_AbstractType_HasAbstractModifier () - { - var java = GenerateFixture ("my/app/AbstractBase"); - Assert.Contains ("public abstract class AbstractBase\n", java); - } - - [Fact] - public void Generate_TypeWithInterfaces_HasImplementsClause () - { - var java = GenerateFixture ("my/app/MultiInterfaceView"); - Assert.Contains ("\timplements\n", java); - Assert.Contains ("\t\tmono.android.IGCUserPeer", java); - Assert.Contains ("android.view.View$OnClickListener", java); - Assert.Contains ("android.view.View$OnLongClickListener", 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 - { - - [Theory] - [InlineData ("public CustomView ()\n")] - [InlineData ("public CustomView (android.content.Context p0)\n")] - [InlineData ("private native void nctor_0 ();\n")] - [InlineData ("private native void nctor_1 (android.content.Context p0);\n")] - [InlineData ("if (getClass () == CustomView.class) nctor_0 ();\n")] - public void Generate_CustomView_HasExpectedConstructorElement (string expectedContent) - { - var java = GenerateFixture ("my/app/CustomView"); - Assert.Contains (expectedContent, java); - } - - [Fact] - public void Generate_Constructor_WithSuperArgumentsString_UsesCustomSuperArgs () - { - // [Export] constructors with SuperArgumentsString should use it in super() call - var type = new JavaPeerInfo { - JavaName = "my/app/CustomService", - ManagedTypeName = "MyApp.CustomService", - ManagedTypeNamespace = "MyApp", - ManagedTypeShortName = "CustomService", - AssemblyName = "App", - BaseJavaName = "android/app/Service", - JavaConstructors = new List { - new JavaConstructorInfo { - JniSignature = "(Landroid/content/Context;I)V", - ConstructorIndex = 0, - Parameters = new List { - new JniParameterInfo { JniType = "Landroid/content/Context;" }, - new JniParameterInfo { JniType = "I" }, - }, - SuperArgumentsString = "p0", - }, - }, - }; - - var java = GenerateToString (type); - Assert.Contains ("super (p0);", java); - Assert.DoesNotContain ("super (p0, p1);", java); - } - - [Fact] - public void Generate_Constructor_WithEmptySuperArgumentsString_EmptySuper () - { - // Empty string means super() with no arguments - var type = new JavaPeerInfo { - JavaName = "my/app/MyWidget", - ManagedTypeName = "MyApp.MyWidget", - ManagedTypeNamespace = "MyApp", - ManagedTypeShortName = "MyWidget", - AssemblyName = "App", - BaseJavaName = "android/appwidget/AppWidgetProvider", - JavaConstructors = new List { - new JavaConstructorInfo { - JniSignature = "(Landroid/content/Context;)V", - ConstructorIndex = 0, - Parameters = new List { - new JniParameterInfo { JniType = "Landroid/content/Context;" }, - }, - SuperArgumentsString = "", - }, - }, - }; - - var java = GenerateToString (type); - Assert.Contains ("super ();", java); - Assert.DoesNotContain ("super (p0);", java); - } - - [Fact] - public void Generate_Constructor_WithoutSuperArgumentsString_ForwardsAllParams () - { - // null SuperArgumentsString means forward all params (default behavior) - var type = new JavaPeerInfo { - JavaName = "my/app/MyView", - ManagedTypeName = "MyApp.MyView", - ManagedTypeNamespace = "MyApp", - ManagedTypeShortName = "MyView", - AssemblyName = "App", - BaseJavaName = "android/view/View", - JavaConstructors = new List { - new JavaConstructorInfo { - JniSignature = "(Landroid/content/Context;Landroid/util/AttributeSet;)V", - ConstructorIndex = 0, - Parameters = new List { - new JniParameterInfo { JniType = "Landroid/content/Context;" }, - new JniParameterInfo { JniType = "Landroid/util/AttributeSet;" }, - }, - }, - }, - }; - - var java = GenerateToString (type); - Assert.Contains ("super (p0, p1);", java); - } - - } - - public class Method - { - - [Fact] - public void Generate_MarshalMethod_HasOverrideAndNativeDeclaration () - { - var java = GenerateFixture ("my/app/MainActivity"); - Assert.Contains ("@Override\n", java); - Assert.Contains ("public void onCreate (android.os.Bundle p0)\n", java); - Assert.Contains ("n_OnCreate (p0);\n", java); - Assert.Contains ("public native void n_OnCreate (android.os.Bundle p0);\n", java); - } - - [Fact] - public void Generate_MethodWithReturnValue_HasReturnStatement () - { - var java = GenerateFixture ("my/app/TouchHandler"); - Assert.Contains ("return n_OnTouch (p0, p1);\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 - { - - [Fact] - public void Generate_CreatesCorrectFileStructure () - { - var peers = ScanFixtures (); - var generator = new JcwJavaSourceGenerator (); - var outputDir = Path.Combine (Path.GetTempPath (), $"jcw-test-{Guid.NewGuid ():N}"); - try { - 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); - } - } finally { - if (Directory.Exists (outputDir)) { - Directory.Delete (outputDir, true); - } - } - } - - } - - 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); - } - - [Fact] - public void Generate_ExportWithoutThrows_HasNoThrowsClause () - { - var java = GenerateFixture ("my/app/ExportExample"); - Assert.DoesNotContain ("throws", java); - } - - } - - public class MethodReturnTypesAndParams - { - - [Theory] - [InlineData ("public boolean onTouch (android.view.View p0, int p1)")] - [InlineData ("public void onScroll (int p0, float p1, long p2, double p3)")] - [InlineData ("public java.lang.String getText ()")] - [InlineData ("public void setItems (java.lang.String[] p0)")] - public void Generate_TouchHandler_HasExpectedMethodSignature (string expectedSignature) - { - var java = GenerateFixture ("my/app/TouchHandler"); - Assert.Contains (expectedSignature + "\n", java); - } - - } -} \ No newline at end of file From 25fe1b7c73a29fb7e9b40f0da61fdfc403c30f9c Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 19 Feb 2026 11:55:05 +0100 Subject: [PATCH 03/40] Move task-specific code to downstream PRs Move UCO-specific JniSignatureHelper methods (JniParamKind, ParseParameterTypes, ParseReturnType, EncodeClrType, ParseSingleType) and their tests to PR #10831. Move JCW-specific scanner properties (BaseJavaName, ImplementedInterfaceJavaNames, JavaConstructors, JavaConstructorInfo) and resolver methods (ResolveBaseJavaName, ResolveImplementedInterfaceJavaNames, ResolveInterfaceJniName, BuildJavaConstructors) to PR #10830. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/JniSignatureHelper.cs | 76 ------------------- .../Scanner/JavaPeerInfo.cs | 47 ------------ .../Scanner/JavaPeerScanner.cs | 73 ------------------ .../Generator/FixtureTestBase.cs | 3 - .../TypeMapAssemblyGeneratorTests.cs | 56 -------------- .../Generator/TypeMapModelBuilderTests.cs | 1 - 6 files changed, 256 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs index 2cbcd7f5c3e..336fec59a7f 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs @@ -1,39 +1,11 @@ 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) { @@ -54,37 +26,6 @@ public static string ParseReturnTypeString (string jniSignature) 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}"); - } - } - static void SkipSingleType (string sig, ref int i) { switch (sig [i]) { @@ -103,21 +44,4 @@ static void SkipSingleType (string sig, ref int i) 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"); - } - } } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index e8d0c5d6ba3..2de7a49ead9 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -43,19 +43,6 @@ sealed record JavaPeerInfo /// public required string AssemblyName { get; init; } - /// - /// JNI name of the base Java type, e.g., "android/app/Activity" for a type - /// that extends Activity. Null for java/lang/Object or types without a Java base. - /// Needed by JCW Java source generation ("extends" clause). - /// - public string? BaseJavaName { get; init; } - - /// - /// JNI names of Java interfaces this type implements, e.g., ["android/view/View$OnClickListener"]. - /// Needed by JCW Java source generation ("implements" clause). - /// - public IReadOnlyList ImplementedInterfaceJavaNames { get; init; } = Array.Empty (); - public bool IsInterface { get; init; } public bool IsAbstract { get; init; } @@ -80,12 +67,6 @@ 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. @@ -198,34 +179,6 @@ sealed record JniParameterInfo public string ManagedType { get; init; } = ""; } -/// -/// Describes a Java constructor to emit in the JCW .java source file. -/// -sealed record JavaConstructorInfo -{ - /// - /// JNI constructor signature, e.g., "(Landroid/content/Context;)V". - /// - public required string JniSignature { get; init; } - - /// - /// Ordinal index for the native constructor method (nctor_0, nctor_1, ...). - /// - public required int ConstructorIndex { get; init; } - - /// - /// JNI parameter types parsed from the signature. - /// Used to generate the Java constructor parameter list. - /// - public IReadOnlyList Parameters { get; init; } = Array.Empty (); - - /// - /// For [Export] constructors: super constructor arguments string. - /// Null for [Register] constructors. - /// - public string? SuperArgumentsString { get; init; } -} - /// /// Describes how to call the activation constructor for a Java peer type. /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index eabc73f7ee9..28941747b30 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -197,12 +197,6 @@ void ScanAssembly (AssemblyIndex index, Dictionary results var isUnconditional = attrInfo is not null; string? invokerTypeName = null; - // Resolve base Java type name - var baseJavaName = ResolveBaseJavaName (typeDef, index, results); - - // Resolve implemented Java interface names - var implementedInterfaces = ResolveImplementedInterfaceJavaNames (typeDef, index); - // Collect marshal methods (including constructors) in a single pass over methods var marshalMethods = CollectMarshalMethods (typeDef, index); @@ -221,14 +215,11 @@ void ScanAssembly (AssemblyIndex index, Dictionary results ManagedTypeNamespace = ExtractNamespace (fullName), ManagedTypeShortName = ExtractShortName (fullName), AssemblyName = index.AssemblyName, - BaseJavaName = baseJavaName, - ImplementedInterfaceJavaNames = implementedInterfaces, IsInterface = isInterface, IsAbstract = isAbstract, DoNotGenerateAcw = doNotGenerateAcw, IsUnconditional = isUnconditional, MarshalMethods = marshalMethods, - JavaConstructors = BuildJavaConstructors (marshalMethods), ActivationCtor = activationCtor, InvokerTypeName = invokerTypeName, IsGenericDefinition = isGenericDefinition, @@ -291,51 +282,6 @@ static void AddMarshalMethod (List methods, RegisterInfo regi }); } - string? ResolveBaseJavaName (TypeDefinition typeDef, AssemblyIndex index, Dictionary results) - { - var baseInfo = GetBaseTypeInfo (typeDef, index); - if (baseInfo is null) { - return null; - } - - var (baseTypeName, baseAssemblyName) = baseInfo.Value; - - // First try [Register] attribute - var registerJniName = ResolveRegisterJniName (baseTypeName, baseAssemblyName); - if (registerJniName is not null) { - return registerJniName; - } - - // Fall back to already-scanned results (component-attributed or CRC64-computed peers) - if (results.TryGetValue (baseTypeName, out var basePeer)) { - return basePeer.JavaName; - } - - return null; - } - - List ResolveImplementedInterfaceJavaNames (TypeDefinition typeDef, AssemblyIndex index) - { - var result = new List (); - var interfaceImpls = typeDef.GetInterfaceImplementations (); - - foreach (var implHandle in interfaceImpls) { - var impl = index.Reader.GetInterfaceImplementation (implHandle); - var ifaceJniName = ResolveInterfaceJniName (impl.Interface, index); - if (ifaceJniName is not null) { - result.Add (ifaceJniName); - } - } - - return result; - } - - string? ResolveInterfaceJniName (EntityHandle interfaceHandle, AssemblyIndex index) - { - var resolved = ResolveEntityHandle (interfaceHandle, index); - return resolved is not null ? ResolveRegisterJniName (resolved.Value.typeName, resolved.Value.assemblyName) : null; - } - static bool TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex index, out RegisterInfo? registerInfo, out ExportInfo? exportInfo) { exportInfo = null; @@ -762,23 +708,4 @@ static List ParseJniParameters (string jniSignature) } return result; } - - static List BuildJavaConstructors (List marshalMethods) - { - var ctors = new List (); - int ctorIndex = 0; - foreach (var mm in marshalMethods) { - if (!mm.IsConstructor) { - continue; - } - ctors.Add (new JavaConstructorInfo { - JniSignature = mm.JniSignature, - ConstructorIndex = ctorIndex, - Parameters = mm.Parameters, - SuperArgumentsString = mm.SuperArgumentsString, - }); - ctorIndex++; - } - return ctors; - } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs index 4a168b636a9..bb446ff3029 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs @@ -67,9 +67,6 @@ protected static JavaPeerInfo MakeAcwPeer (string jniName, string managedName, s { var peer = MakePeerWithActivation (jniName, managedName, asmName); peer.DoNotGenerateAcw = false; - peer.JavaConstructors = new List { - new JavaConstructorInfo { ConstructorIndex = 0, JniSignature = "()V" }, - }; peer.MarshalMethods = new List { new MarshalMethodInfo { JniName = "", diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index d83d7db35eb..f7b08365470 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -251,62 +251,6 @@ public void Generate_EmptyPeerList_ProducesValidAssembly () } - public class JniSignatureHelperTests - { - - [Theory] - [InlineData ("()V", 0)] - [InlineData ("(I)V", 1)] - [InlineData ("(Landroid/os/Bundle;)V", 1)] - [InlineData ("(IFJ)V", 3)] - [InlineData ("(ZLandroid/view/View;I)Z", 3)] - [InlineData ("([Ljava/lang/String;)V", 1)] - public void ParseParameterTypes_ParsesCorrectCount (string signature, int expectedCount) - { - var actual = JniSignatureHelper.ParseParameterTypes (signature); - Assert.Equal (expectedCount, actual.Count); - } - - [Theory] - [InlineData ("(Z)V", JniParamKind.Boolean)] - [InlineData ("(Ljava/lang/String;)V", JniParamKind.Object)] - public void ParseParameterTypes_SingleParam_MapsToCorrectKind (string signature, JniParamKind expectedKind) - { - var types = JniSignatureHelper.ParseParameterTypes (signature); - Assert.Single (types); - Assert.Equal (expectedKind, types [0]); - } - - [Theory] - [InlineData ("()V", JniParamKind.Void)] - [InlineData ("()I", JniParamKind.Int)] - [InlineData ("()Z", JniParamKind.Boolean)] - [InlineData ("()Ljava/lang/String;", JniParamKind.Object)] - public void ParseReturnType_MapsToCorrectKind (string signature, JniParamKind expectedKind) - { - Assert.Equal (expectedKind, JniSignatureHelper.ParseReturnType (signature)); - } - - } - - public class NegativeEdgeCase - { - - [Theory] - [InlineData ("")] - [InlineData ("not-a-sig")] - [InlineData ("(")] - public void ParseParameterTypes_InvalidSignature_ThrowsOrReturnsEmpty (string signature) - { - try { - var result = JniSignatureHelper.ParseParameterTypes (signature); - Assert.NotNull (result); - } catch (Exception ex) when (ex is ArgumentException || ex is IndexOutOfRangeException || ex is FormatException) { - } - } - - } - public class CreateInstancePaths { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index cb2cf936100..486b1948449 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -340,7 +340,6 @@ public class FixtureCustomView public void Fixture_CustomView_HasTwoConstructors () { var peer = FindFixtureByJavaName ("my/app/CustomView"); - Assert.Equal (2, peer.JavaConstructors.Count); var model = BuildModel (new [] { peer }, "TypeMap"); var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_CustomView_Proxy"); From 163f8927575f0f77e291c697621e49eb5e11e5db Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 19 Feb 2026 12:55:43 +0100 Subject: [PATCH 04/40] Replace try-finally cleanup with IDisposable temp directories Use xUnit's IDisposable pattern for test cleanup instead of manual try-finally blocks. Each nested test class that generates temp files now creates a per-instance temp directory in a field initializer and cleans it up in Dispose(). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/PEAssemblyBuilder.cs | 59 ++++ .../Generator/RootTypeMapAssemblyGenerator.cs | 9 +- .../Generator/TypeMapAssemblyEmitter.cs | 49 +-- .../RootTypeMapAssemblyGeneratorTests.cs | 125 +++---- .../TypeMapAssemblyGeneratorTests.cs | 313 ++++++++---------- .../Generator/TypeMapModelBuilderTests.cs | 5 +- 6 files changed, 256 insertions(+), 304 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs index 01c73df6b49..d0b28c2b638 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs @@ -187,6 +187,25 @@ public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, bodyOffset, default); } + // ---- TypeSpec helpers ---- + + /// + /// Builds a TypeSpec for a closed generic type with a single type argument. + /// For example, MakeGenericTypeSpec(openAttrRef, javaLangObjectRef) produces + /// TypeMapAttribute<Java.Lang.Object>. + /// + public TypeSpecificationHandle MakeGenericTypeSpec (EntityHandle openType, EntityHandle typeArg) + { + _sigBlob.Clear (); + _sigBlob.WriteByte (0x15); // ELEMENT_TYPE_GENERICINST + _sigBlob.WriteByte (0x12); // ELEMENT_TYPE_CLASS + _sigBlob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (openType)); + _sigBlob.WriteCompressedInteger (1); // generic arity = 1 + _sigBlob.WriteByte (0x12); // ELEMENT_TYPE_CLASS + _sigBlob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (typeArg)); + return Metadata.AddTypeSpecification (Metadata.GetOrAddBlob (_sigBlob)); + } + // ---- Attribute blob helpers ---- /// @@ -201,4 +220,44 @@ public BlobHandle BuildAttributeBlob (Action writePayload) _attrBlob.WriteUInt16 (0x0000); // NumNamed return Metadata.GetOrAddBlob (_attrBlob); } + + /// + /// Emits the IgnoresAccessChecksToAttribute type and applies + /// [assembly: IgnoresAccessChecksTo("...")] for each assembly name. + /// + public void EmitIgnoresAccessChecksToAttribute (List assemblyNames) + { + var attributeTypeRef = Metadata.AddTypeReference (SystemRuntimeRef, + Metadata.GetOrAddString ("System"), Metadata.GetOrAddString ("Attribute")); + + int typeFieldStart = Metadata.GetRowCount (TableIndex.Field) + 1; + int typeMethodStart = Metadata.GetRowCount (TableIndex.MethodDef) + 1; + + var baseAttrCtorRef = AddMemberRef (attributeTypeRef, ".ctor", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { })); + + var ctorDef = EmitBody (".ctor", + MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName, + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (1, + rt => rt.Void (), + p => p.AddParameter ().Type ().String ()), + encoder => { + encoder.LoadArgument (0); + encoder.Call (baseAttrCtorRef); + encoder.OpCode (ILOpCode.Ret); + }); + + Metadata.AddTypeDefinition ( + TypeAttributes.NotPublic | TypeAttributes.Sealed | TypeAttributes.BeforeFieldInit, + Metadata.GetOrAddString ("System.Runtime.CompilerServices"), + Metadata.GetOrAddString ("IgnoresAccessChecksToAttribute"), + attributeTypeRef, + MetadataTokens.FieldDefinitionHandle (typeFieldStart), + MetadataTokens.MethodDefinitionHandle (typeMethodStart)); + + foreach (var asmName in assemblyNames) { + var blob = BuildAttributeBlob (b => b.WriteSerializedString (asmName)); + Metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, ctorDef, blob); + } + } } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs index bdd5f0ee996..ef538272ea5 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs @@ -62,14 +62,7 @@ public void Generate (IReadOnlyList perAssemblyTypeMapNames, string outp pe.Metadata.GetOrAddString ("Java.Lang"), pe.Metadata.GetOrAddString ("Object")); // Build TypeSpec for TypeMapAssemblyTargetAttribute - var genericInstBlob = new BlobBuilder (); - genericInstBlob.WriteByte (0x15); // ELEMENT_TYPE_GENERICINST - genericInstBlob.WriteByte (0x12); // ELEMENT_TYPE_CLASS - genericInstBlob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (openAttrRef)); - genericInstBlob.WriteCompressedInteger (1); // generic arity = 1 - genericInstBlob.WriteByte (0x12); // ELEMENT_TYPE_CLASS - genericInstBlob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (javaLangObjectRef)); - var closedAttrTypeSpec = pe.Metadata.AddTypeSpecification (pe.Metadata.GetOrAddBlob (genericInstBlob)); + var closedAttrTypeSpec = pe.MakeGenericTypeSpec (openAttrRef, javaLangObjectRef); // MemberRef for .ctor(string) on the closed generic type var ctorRef = pe.AddMemberRef (closedAttrTypeSpec, ".ctor", diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 722c0f38bbc..8bb789239fa 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -108,7 +108,7 @@ public void Emit (TypeMapAssemblyData model, string outputPath) EmitTypeMapAssociationAttribute (assoc); } - EmitIgnoresAccessChecksToAttribute (model.IgnoresAccessChecksTo); + _pe.EmitIgnoresAccessChecksToAttribute (model.IgnoresAccessChecksTo); _pe.WritePE (outputPath); } @@ -166,14 +166,7 @@ void EmitTypeMapAttributeCtorRef () var javaLangObjectRef = metadata.AddTypeReference (_pe.MonoAndroidRef, metadata.GetOrAddString ("Java.Lang"), metadata.GetOrAddString ("Object")); - var genericInstBlob = new BlobBuilder (); - genericInstBlob.WriteByte (0x15); // ELEMENT_TYPE_GENERICINST - genericInstBlob.WriteByte (0x12); // ELEMENT_TYPE_CLASS - genericInstBlob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (typeMapAttrOpenRef)); - genericInstBlob.WriteCompressedInteger (1); - genericInstBlob.WriteByte (0x12); // ELEMENT_TYPE_CLASS - genericInstBlob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (javaLangObjectRef)); - var closedAttrTypeSpec = metadata.AddTypeSpecification (metadata.GetOrAddBlob (genericInstBlob)); + var closedAttrTypeSpec = _pe.MakeGenericTypeSpec (typeMapAttrOpenRef, javaLangObjectRef); // 2-arg: TypeMap(string jniName, Type proxyType) — unconditional _typeMapAttrCtorRef2Arg = _pe.AddMemberRef (closedAttrTypeSpec, ".ctor", @@ -383,42 +376,4 @@ void EmitTypeMapAssociationAttribute (TypeMapAssociationData assoc) _pe.Metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, _typeMapAssociationAttrCtorRef, blob); } - // ---- IgnoresAccessChecksTo ---- - - void EmitIgnoresAccessChecksToAttribute (List assemblyNames) - { - var metadata = _pe.Metadata; - var attributeTypeRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, - metadata.GetOrAddString ("System"), metadata.GetOrAddString ("Attribute")); - - int typeFieldStart = metadata.GetRowCount (TableIndex.Field) + 1; - int typeMethodStart = metadata.GetRowCount (TableIndex.MethodDef) + 1; - - var baseAttrCtorRef = _pe.AddMemberRef (attributeTypeRef, ".ctor", - sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { })); - - var ctorDef = _pe.EmitBody (".ctor", - MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName, - sig => sig.MethodSignature (isInstanceMethod: true).Parameters (1, - rt => rt.Void (), - p => p.AddParameter ().Type ().String ()), - encoder => { - encoder.LoadArgument (0); - encoder.Call (baseAttrCtorRef); - encoder.OpCode (ILOpCode.Ret); - }); - - metadata.AddTypeDefinition ( - TypeAttributes.NotPublic | TypeAttributes.Sealed | TypeAttributes.BeforeFieldInit, - metadata.GetOrAddString ("System.Runtime.CompilerServices"), - metadata.GetOrAddString ("IgnoresAccessChecksToAttribute"), - attributeTypeRef, - MetadataTokens.FieldDefinitionHandle (typeFieldStart), - MetadataTokens.MethodDefinitionHandle (typeMethodStart)); - - foreach (var asmName in assemblyNames) { - var blob = _pe.BuildAttributeBlob (b => b.WriteSerializedString (asmName)); - metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, ctorDef, blob); - } - } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs index 000c97f11f3..b6dfe06c6b2 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs @@ -8,11 +8,14 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; -public class RootTypeMapAssemblyGeneratorTests : FixtureTestBase +public class RootTypeMapAssemblyGeneratorTests : FixtureTestBase, IDisposable { + readonly string _outputDir = CreateTempDir (); + public void Dispose () => DeleteTempDir (_outputDir); + string GenerateRootAssembly (IReadOnlyList perAssemblyNames, string? assemblyName = null) { - var outputPath = Path.Combine (Path.GetTempPath (), $"root-typemap-{Guid.NewGuid ():N}", + var outputPath = Path.Combine (_outputDir, (assemblyName ?? "_Microsoft.Android.TypeMaps") + ".dll"); var generator = new RootTypeMapAssemblyGenerator (new Version (11, 0, 0, 0)); generator.Generate (perAssemblyNames, outputPath, assemblyName); @@ -23,13 +26,9 @@ string GenerateRootAssembly (IReadOnlyList perAssemblyNames, string? ass public void Generate_ProducesValidPEAssembly () { var path = GenerateRootAssembly (new [] { "_App.TypeMap", "_Mono.Android.TypeMap" }); - try { - Assert.True (File.Exists (path)); - using var pe = new PEReader (File.OpenRead (path)); - Assert.True (pe.HasMetadata); - } finally { - CleanUpDir (path); - } + Assert.True (File.Exists (path)); + using var pe = new PEReader (File.OpenRead (path)); + Assert.True (pe.HasMetadata); } [Theory] @@ -38,57 +37,45 @@ public void Generate_ProducesValidPEAssembly () public void Generate_AssemblyName_MatchesExpected (string? assemblyName, string expectedName) { var path = GenerateRootAssembly (Array.Empty (), assemblyName); - try { - using var pe = new PEReader (File.OpenRead (path)); - var reader = pe.GetMetadataReader (); - var asmDef = reader.GetAssemblyDefinition (); - Assert.Equal (expectedName, reader.GetString (asmDef.Name)); - } finally { - CleanUpDir (path); - } + using var pe = new PEReader (File.OpenRead (path)); + var reader = pe.GetMetadataReader (); + var asmDef = reader.GetAssemblyDefinition (); + Assert.Equal (expectedName, reader.GetString (asmDef.Name)); } [Fact] public void Generate_ReferencesGenericTypeMapAssemblyTargetAttribute () { var path = GenerateRootAssembly (new [] { "_App.TypeMap" }); - try { - using var pe = new PEReader (File.OpenRead (path)); - var reader = pe.GetMetadataReader (); + using var pe = new PEReader (File.OpenRead (path)); + var reader = pe.GetMetadataReader (); - var typeRefs = reader.TypeReferences - .Select (h => reader.GetTypeReference (h)) - .ToList (); - Assert.Contains (typeRefs, t => - reader.GetString (t.Name) == "TypeMapAssemblyTargetAttribute`1" && - reader.GetString (t.Namespace) == "System.Runtime.InteropServices"); + var typeRefs = reader.TypeReferences + .Select (h => reader.GetTypeReference (h)) + .ToList (); + Assert.Contains (typeRefs, t => + reader.GetString (t.Name) == "TypeMapAssemblyTargetAttribute`1" && + reader.GetString (t.Namespace) == "System.Runtime.InteropServices"); - Assert.Contains (typeRefs, t => - reader.GetString (t.Name) == "Object" && - reader.GetString (t.Namespace) == "Java.Lang"); + Assert.Contains (typeRefs, t => + reader.GetString (t.Name) == "Object" && + reader.GetString (t.Namespace) == "Java.Lang"); - var typeDefs = reader.TypeDefinitions - .Select (h => reader.GetTypeDefinition (h)) - .ToList (); - Assert.DoesNotContain (typeDefs, t => - reader.GetString (t.Name).Contains ("TypeMapAssemblyTarget")); - } finally { - CleanUpDir (path); - } + var typeDefs = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .ToList (); + Assert.DoesNotContain (typeDefs, t => + reader.GetString (t.Name).Contains ("TypeMapAssemblyTarget")); } [Fact] public void Generate_EmptyList_ProducesValidAssemblyWithNoTargetAttributes () { var path = GenerateRootAssembly (Array.Empty ()); - try { - using var pe = new PEReader (File.OpenRead (path)); - var reader = pe.GetMetadataReader (); - var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); - Assert.Empty (asmAttrs); - } finally { - CleanUpDir (path); - } + using var pe = new PEReader (File.OpenRead (path)); + var reader = pe.GetMetadataReader (); + var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); + Assert.Empty (asmAttrs); } [Fact] @@ -96,14 +83,10 @@ public void Generate_MultipleTargets_HasCorrectAttributeCount () { var targets = new [] { "_App.TypeMap", "_Mono.Android.TypeMap", "_Java.Interop.TypeMap" }; var path = GenerateRootAssembly (targets); - try { - using var pe = new PEReader (File.OpenRead (path)); - var reader = pe.GetMetadataReader (); - var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); - Assert.Equal (3, asmAttrs.Count ()); - } finally { - CleanUpDir (path); - } + using var pe = new PEReader (File.OpenRead (path)); + var reader = pe.GetMetadataReader (); + var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); + Assert.Equal (3, asmAttrs.Count ()); } [Fact] @@ -111,28 +94,24 @@ public void Generate_AttributeBlobValues_MatchTargetNames () { var targets = new [] { "_App.TypeMap", "_Mono.Android.TypeMap" }; var path = GenerateRootAssembly (targets); - try { - using var pe = new PEReader (File.OpenRead (path)); - var reader = pe.GetMetadataReader (); + using var pe = new PEReader (File.OpenRead (path)); + var reader = pe.GetMetadataReader (); - var attrValues = new List (); - foreach (var attrHandle in reader.GetCustomAttributes (EntityHandle.AssemblyDefinition)) { - var attr = reader.GetCustomAttribute (attrHandle); - var blob = reader.GetBlobReader (attr.Value); + var attrValues = new List (); + foreach (var attrHandle in reader.GetCustomAttributes (EntityHandle.AssemblyDefinition)) { + var attr = reader.GetCustomAttribute (attrHandle); + var blob = reader.GetBlobReader (attr.Value); - // Custom attribute blob: prolog (2 bytes) + SerString value - var prolog = blob.ReadUInt16 (); - Assert.Equal (1, prolog); // ECMA-335 prolog - var value = blob.ReadSerializedString (); - Assert.NotNull (value); - attrValues.Add (value!); - } - - Assert.Equal (2, attrValues.Count); - Assert.Contains ("_App.TypeMap", attrValues); - Assert.Contains ("_Mono.Android.TypeMap", attrValues); - } finally { - CleanUpDir (path); + // Custom attribute blob: prolog (2 bytes) + SerString value + var prolog = blob.ReadUInt16 (); + Assert.Equal (1, prolog); // ECMA-335 prolog + var value = blob.ReadSerializedString (); + Assert.NotNull (value); + attrValues.Add (value!); } + + Assert.Equal (2, attrValues.Count); + Assert.Contains ("_App.TypeMap", attrValues); + Assert.Contains ("_Mono.Android.TypeMap", attrValues); } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index f7b08365470..cceb2e20a62 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -12,10 +12,9 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; public class TypeMapAssemblyGeneratorTests : FixtureTestBase { - static string GenerateAssembly (IReadOnlyList peers, string? assemblyName = null) + static string GenerateAssembly (IReadOnlyList peers, string outputDir, string? assemblyName = null) { - var outputPath = Path.Combine (Path.GetTempPath (), $"typemap-test-{Guid.NewGuid ():N}", - (assemblyName ?? "TestTypeMap") + ".dll"); + var outputPath = Path.Combine (outputDir, (assemblyName ?? "TestTypeMap") + ".dll"); var generator = new TypeMapAssemblyGenerator (new Version (11, 0, 0, 0)); generator.Generate (peers, outputPath, assemblyName); return outputPath; @@ -39,74 +38,68 @@ static List GetTypeRefNames (MetadataReader reader) => .Select (t => reader.GetString (t.Name)) .ToList (); - public class BasicAssemblyStructure + public class BasicAssemblyStructure : IDisposable { + readonly string _outputDir = CreateTempDir (); + public void Dispose () => DeleteTempDir (_outputDir); [Fact] public void Generate_ProducesValidPEAssembly () { var peers = ScanFixtures (); - var path = GenerateAssembly (peers); - try { - Assert.True (File.Exists (path)); - using var pe = new PEReader (File.OpenRead (path)); - Assert.True (pe.HasMetadata); - var reader = pe.GetMetadataReader (); - Assert.NotNull (reader); - } finally { - CleanUpDir (path); - } + 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); } } - public class AssemblyReference + public class AssemblyReference : IDisposable { + readonly string _outputDir = CreateTempDir (); + public void Dispose () => DeleteTempDir (_outputDir); [Fact] public void Generate_HasRequiredAssemblyReferences () { var peers = ScanFixtures (); - var path = GenerateAssembly (peers); - try { - 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); - } - } finally { - CleanUpDir (path); + 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); } } } - public class ProxyType + public class ProxyType : IDisposable { + readonly string _outputDir = CreateTempDir (); + public void Dispose () => DeleteTempDir (_outputDir); [Fact] public void Generate_CreatesProxyTypes () { var peers = ScanFixtures (); - var path = GenerateAssembly (peers); - try { - 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"); - } - } finally { - CleanUpDir (path); + 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"); } } @@ -114,57 +107,53 @@ public void Generate_CreatesProxyTypes () public void Generate_ProxyType_HasCtorAndCreateInstance () { var peers = ScanFixtures (); - var path = GenerateAssembly (peers); - try { - 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); - } - } finally { - CleanUpDir (path); + 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 IgnoresAccessChecksTo + public class IgnoresAccessChecksTo : IDisposable { + readonly string _outputDir = CreateTempDir (); + public void Dispose () => DeleteTempDir (_outputDir); [Fact] public void Generate_HasIgnoresAccessChecksToAttribute () { var peers = ScanFixtures (); - var path = GenerateAssembly (peers); - try { - 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"); - } - } finally { - CleanUpDir (path); + 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"); } } } - public class Alias + public class Alias : IDisposable { + readonly string _outputDir = CreateTempDir (); + public void Dispose () => DeleteTempDir (_outputDir); static List MakeDuplicateAliasPeers () => new List { new JavaPeerInfo { @@ -192,16 +181,11 @@ public class Alias public void Generate_DuplicateJniNames_CreatesAliasEntries () { var peers = MakeDuplicateAliasPeers (); - - var path = GenerateAssembly (peers, "AliasTest"); - try { - var (pe, reader) = OpenAssembly (path); - using (pe) { - var assemblyAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); - Assert.True (assemblyAttrs.Count () >= 3); - } - } finally { - CleanUpDir (path); + var path = GenerateAssembly (peers, _outputDir, "AliasTest"); + var (pe, reader) = OpenAssembly (path); + using (pe) { + var assemblyAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); + Assert.True (assemblyAttrs.Count () >= 3); } } @@ -209,50 +193,45 @@ public void Generate_DuplicateJniNames_CreatesAliasEntries () public void Generate_DuplicateJniNames_EmitsTypeMapAssociationAttribute () { var peers = MakeDuplicateAliasPeers (); - - var path = GenerateAssembly (peers, "AliasAssocTest"); - try { - 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); - } - } finally { - CleanUpDir (path); + 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); } } } - public class EmptyInput + public class EmptyInput : IDisposable { + readonly string _outputDir = CreateTempDir (); + public void Dispose () => DeleteTempDir (_outputDir); [Fact] public void Generate_EmptyPeerList_ProducesValidAssembly () { - var path = GenerateAssembly (Array.Empty (), "EmptyTest"); - try { - 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)); - } - } finally { - CleanUpDir (path); + 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)); } } } - public class CreateInstancePaths + public class CreateInstancePaths : IDisposable { + readonly string _outputDir = CreateTempDir (); + public void Dispose () => DeleteTempDir (_outputDir); [Fact] public void Generate_SimpleActivity_UsesGetUninitializedObject () @@ -262,19 +241,15 @@ public void Generate_SimpleActivity_UsesGetUninitializedObject () Assert.NotNull (simpleActivity.ActivationCtor); Assert.NotEqual (simpleActivity.ManagedTypeName, simpleActivity.ActivationCtor.DeclaringTypeName); - var path = GenerateAssembly (new [] { simpleActivity }, "InheritedCtorTest"); - try { - var (pe, reader) = OpenAssembly (path); - using (pe) { - var typeNames = GetTypeRefNames (reader); - Assert.Contains ("RuntimeHelpers", typeNames); + 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); - } - } finally { - CleanUpDir (path); + var memberNames = GetMemberRefNames (reader); + Assert.DoesNotContain ("CreateManagedPeer", memberNames); + Assert.Contains ("GetUninitializedObject", memberNames); } } @@ -287,21 +262,17 @@ public void Generate_LeafCtor_DoesNotUseCreateManagedPeer () Assert.NotNull (clickableView.ActivationCtor); Assert.Equal (clickableView.ManagedTypeName, clickableView.ActivationCtor.DeclaringTypeName); - var path = GenerateAssembly (new [] { clickableView }, "LeafCtorTest"); - try { - 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"); - } - } finally { - CleanUpDir (path); + 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"); } } @@ -312,22 +283,20 @@ public void Generate_GenericType_ThrowsNotSupportedException () var generic = peers.First (p => p.JavaName == "my/app/GenericHolder"); Assert.True (generic.IsGenericDefinition); - var path = GenerateAssembly (new [] { generic }, "GenericTest"); - try { - var (pe, reader) = OpenAssembly (path); - using (pe) { - var typeNames = GetTypeRefNames (reader); - Assert.Contains ("NotSupportedException", typeNames); - } - } finally { - CleanUpDir (path); + var path = GenerateAssembly (new [] { generic }, _outputDir, "GenericTest"); + var (pe, reader) = OpenAssembly (path); + using (pe) { + var typeNames = GetTypeRefNames (reader); + Assert.Contains ("NotSupportedException", typeNames); } } } - public class IgnoresAccessChecksToForBaseCtor + public class IgnoresAccessChecksToForBaseCtor : IDisposable { + readonly string _outputDir = CreateTempDir (); + public void Dispose () => DeleteTempDir (_outputDir); [Fact] public void Generate_InheritedCtor_IncludesBaseCtorAssembly () @@ -338,28 +307,24 @@ public void Generate_InheritedCtor_IncludesBaseCtorAssembly () var peers = ScanFixtures (); var simpleActivity = peers.First (p => p.JavaName == "my/app/SimpleActivity"); - var path = GenerateAssembly (new [] { simpleActivity }, "IgnoresAccessTest"); - try { - 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")); + 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); } - } finally { - CleanUpDir (path); + // Activity is in TestFixtures, so IgnoresAccessChecksTo must include TestFixtures + Assert.Contains (attrBlobs, b => b.Contains ("TestFixtures")); } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 486b1948449..54d66fc03f7 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -722,14 +722,15 @@ public void Build_SameInput_ProducesDeterministicOutput () static void EmitAndVerify (TypeMapAssemblyData model, string assemblyName, Action verify) { - var outputPath = Path.Combine (Path.GetTempPath (), $"{assemblyName.ToLowerInvariant ()}-{Guid.NewGuid ():N}", $"{assemblyName}.dll"); + 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 { - CleanUpDir (outputPath); + DeleteTempDir (outputDir); } } From 02a21869d82c06ff40bfe9281a3254522b3dcc0f Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 19 Feb 2026 14:25:44 +0100 Subject: [PATCH 05/40] Clean up comments: remove section separators, expand inline summaries - Remove // ---- section separator comments from PEAssemblyBuilder and TypeMapAssemblyEmitter - Expand all single-line /// ... to 3-line format - Remove trivial property doc comments that just restate the name Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/JniSignatureHelper.cs | 12 +- .../Generator/Model/TypeMapAssemblyData.cs | 114 ++++++++++++++---- .../Generator/PEAssemblyBuilder.cs | 35 +++--- .../Generator/TypeMapAssemblyEmitter.cs | 6 - 4 files changed, 120 insertions(+), 47 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs index 336fec59a7f..d07bb062bd3 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs @@ -3,10 +3,14 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; -/// Helpers for parsing JNI method signatures. +/// +/// Helpers for parsing JNI method signatures. +/// static class JniSignatureHelper { - /// Parses the raw JNI type descriptor strings from a JNI method signature. + /// + /// Parses the raw JNI type descriptor strings from a JNI method signature. + /// public static List ParseParameterTypeStrings (string jniSignature) { var result = new List (); @@ -19,7 +23,9 @@ public static List ParseParameterTypeStrings (string jniSignature) return result; } - /// Extracts the return type descriptor from a JNI method signature. + /// + /// Extracts the return type descriptor from a JNI method signature. + /// public static string ParseReturnTypeString (string jniSignature) { int i = jniSignature.IndexOf (')') + 1; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs index 4ab45ecca37..279d3e15519 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -10,22 +10,44 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// sealed class TypeMapAssemblyData { - /// Assembly name (e.g., "_MyApp.TypeMap"). + /// + /// Assembly name (e.g., "_MyApp.TypeMap"). + /// public required string AssemblyName { get; init; } - /// Module file name (e.g., "_MyApp.TypeMap.dll"). + /// + + /// Module file name (e.g., "_MyApp.TypeMap.dll"). + + /// public required string ModuleName { get; init; } - /// TypeMap entries — one per unique JNI name. + /// + + /// TypeMap entries — one per unique JNI name. + + /// public List Entries { get; } = new (); - /// Proxy types to emit in the assembly. + /// + + /// Proxy types to emit in the assembly. + + /// public List ProxyTypes { get; } = new (); - /// TypeMapAssociation entries for alias groups (multiple managed types → same JNI name). + /// + + /// 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. + /// + + /// Assembly names that need [IgnoresAccessChecksTo] for cross-assembly n_* calls. + + /// public List IgnoresAccessChecksTo { get; } = new (); } @@ -38,7 +60,9 @@ sealed class TypeMapAssemblyData /// sealed record TypeMapAttributeData { - /// JNI type name, e.g., "android/app/Activity". + /// + /// JNI type name, e.g., "android/app/Activity". + /// public required string JniName { get; init; } /// @@ -54,7 +78,11 @@ sealed record TypeMapAttributeData /// public string? TargetTypeReference { get; init; } - /// True for 2-arg unconditional entries (ACW types, essential runtime types). + /// + + /// True for 2-arg unconditional entries (ACW types, essential runtime types). + + /// public bool IsUnconditional => TargetTypeReference == null; } @@ -63,19 +91,37 @@ sealed record TypeMapAttributeData /// sealed class JavaPeerProxyData { - /// Simple type name, e.g., "Java_Lang_Object_Proxy". + /// + /// Simple type name, e.g., "Java_Lang_Object_Proxy". + /// public required string TypeName { get; init; } - /// Namespace for all proxy types. + /// + + /// 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). + /// + + /// 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. + /// + + /// 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. + /// + + /// Whether this proxy has a CreateInstance that can actually create instances. + + /// public bool HasActivation => ActivationCtor != null || InvokerType != null; /// @@ -83,7 +129,11 @@ sealed class JavaPeerProxyData /// public ActivationCtorData? ActivationCtor { get; set; } - /// True if this is an open generic type definition. CreateInstance throws NotSupportedException. + /// + + /// True if this is an open generic type definition. CreateInstance throws NotSupportedException. + + /// public bool IsGenericDefinition { get; init; } } @@ -94,10 +144,16 @@ sealed class JavaPeerProxyData /// sealed record TypeRefData { - /// Full managed type name, e.g., "Android.App.Activity" or "MyApp.Outer+Inner". + /// + /// Full managed type name, e.g., "Android.App.Activity" or "MyApp.Outer+Inner". + /// public required string ManagedTypeName { get; init; } - /// Assembly containing the type, e.g., "Mono.Android". + /// + + /// Assembly containing the type, e.g., "Mono.Android". + + /// public required string AssemblyName { get; init; } } @@ -106,13 +162,23 @@ sealed record TypeRefData /// sealed record ActivationCtorData { - /// Type that declares the activation constructor (may be a base type). + /// + /// Type that declares the activation constructor (may be a base type). + /// public required TypeRefData DeclaringType { get; init; } - /// True when the leaf type itself declares the activation ctor. + /// + + /// True when the leaf type itself declares the activation ctor. + + /// public required bool IsOnLeafType { get; init; } - /// The style of activation ctor (XamarinAndroid or JavaInterop). + /// + + /// The style of activation ctor (XamarinAndroid or JavaInterop). + + /// public required ActivationCtorStyle Style { get; init; } } @@ -122,9 +188,15 @@ sealed record ActivationCtorData /// sealed record TypeMapAssociationData { - /// Assembly-qualified source type reference (the managed alias type). + /// + /// Assembly-qualified source type reference (the managed alias type). + /// public required string SourceTypeReference { get; init; } - /// Assembly-qualified proxy type reference (the alias holder proxy). + /// + + /// 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 d0b28c2b638..9cae7720e6a 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs @@ -33,13 +33,10 @@ sealed class PEAssemblyBuilder public MetadataBuilder Metadata { get; } = new MetadataBuilder (); public BlobBuilder ILBuilder { get; } = new BlobBuilder (); - /// Reference to the System.Runtime assembly. public AssemblyReferenceHandle SystemRuntimeRef { get; private set; } - /// Reference to the System.Runtime.InteropServices assembly. public AssemblyReferenceHandle SystemRuntimeInteropServicesRef { get; private set; } - /// Reference to the Mono.Android assembly. public AssemblyReferenceHandle MonoAndroidRef { get; private set; } public PEAssemblyBuilder (Version systemRuntimeVersion) @@ -85,7 +82,9 @@ public void EmitPreamble (string assemblyName, string moduleName) MetadataTokens.MethodDefinitionHandle (1)); } - /// Serialises the metadata + IL into a PE DLL at . + /// + /// Serialises the metadata + IL into a PE DLL at . + /// public void WritePE (string outputPath) { var dir = Path.GetDirectoryName (outputPath); @@ -103,9 +102,9 @@ public void WritePE (string outputPath) peBlob.WriteContentTo (fs); } - // ---- Assembly / type / member reference helpers ---- - - /// Adds (or retrieves from cache) an assembly reference. + /// + /// Adds (or retrieves from cache) an assembly reference. + /// public AssemblyReferenceHandle AddAssemblyRef (string name, Version version, byte []? publicKeyOrToken = null) { if (_asmRefCache.TryGetValue (name, out var existing)) { @@ -118,7 +117,9 @@ public AssemblyReferenceHandle AddAssemblyRef (string name, Version version, byt return handle; } - /// Finds an existing assembly reference or adds one with version 0.0.0.0. + /// + /// 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)) { @@ -127,7 +128,9 @@ public AssemblyReferenceHandle FindOrAddAssemblyRef (string assemblyName) return AddAssemblyRef (assemblyName, new Version (0, 0, 0, 0)); } - /// Adds a member reference using the reusable signature blob builder. + /// + /// Adds a member reference using the reusable signature blob builder. + /// public MemberReferenceHandle AddMemberRef (EntityHandle parent, string name, Action encodeSig) { _sigBlob.Clear (); @@ -135,7 +138,9 @@ public MemberReferenceHandle AddMemberRef (EntityHandle parent, string name, Act return Metadata.AddMemberReference (parent, Metadata.GetOrAddString (name), Metadata.GetOrAddBlob (_sigBlob)); } - /// Resolves a to a TypeReference/TypeSpecification handle, with caching. + /// + /// Resolves a to a TypeReference/TypeSpecification handle, with caching. + /// public EntityHandle ResolveTypeRef (TypeRefData typeRef) { var cacheKey = (typeRef.AssemblyName, typeRef.ManagedTypeName); @@ -161,9 +166,9 @@ TypeReferenceHandle MakeTypeRefForManagedName (EntityHandle scope, string manage return Metadata.AddTypeReference (scope, Metadata.GetOrAddString (ns), Metadata.GetOrAddString (name)); } - // ---- Method body emission ---- - - /// Emits a method body and definition in one call. + /// + /// Emits a method body and definition in one call. + /// public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, Action encodeSig, Action emitIL) { @@ -187,8 +192,6 @@ public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, bodyOffset, default); } - // ---- TypeSpec helpers ---- - /// /// Builds a TypeSpec for a closed generic type with a single type argument. /// For example, MakeGenericTypeSpec(openAttrRef, javaLangObjectRef) produces @@ -206,8 +209,6 @@ public TypeSpecificationHandle MakeGenericTypeSpec (EntityHandle openType, Entit return Metadata.AddTypeSpecification (Metadata.GetOrAddBlob (_sigBlob)); } - // ---- Attribute blob helpers ---- - /// /// Writes a custom attribute blob. Calls to fill in the /// payload between the prolog and NumNamed footer. diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 8bb789239fa..f96d3448647 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -112,8 +112,6 @@ public void Emit (TypeMapAssemblyData model, string outputPath) _pe.WritePE (outputPath); } - // ---- Type / Member references ---- - void EmitTypeReferences () { var metadata = _pe.Metadata; @@ -206,8 +204,6 @@ void EmitTypeMapAssociationAttributeCtorRef () })); } - // ---- Proxy types ---- - void EmitProxyType (JavaPeerProxyData proxy) { var metadata = _pe.Metadata; @@ -352,8 +348,6 @@ void EmitTypeGetter (string methodName, TypeRefData typeRef, MethodAttributes at }); } - // ---- TypeMap attributes ---- - void EmitTypeMapAttribute (TypeMapAttributeData entry) { var ctorRef = entry.IsUnconditional ? _typeMapAttrCtorRef2Arg : _typeMapAttrCtorRef3Arg; From 1a27ee0b60ca4c64059ebe609f0cd11ab598d1d4 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 20 Feb 2026 10:08:11 +0100 Subject: [PATCH 06/40] [TrimmableTypeMap] Normalize constructor native callback names Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Scanner/JavaPeerScanner.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 28941747b30..81c7f8a4c5f 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -268,15 +268,27 @@ static void AddMarshalMethod (List methods, RegisterInfo regi return; } + bool isConstructor = registerInfo.JniName == "" || registerInfo.JniName == ".ctor"; + string nativeCallbackName = $"n_{index.Reader.GetString (methodDef.Name)}"; + if (isConstructor) { + int ctorIndex = 0; + foreach (var method in methods) { + if (method.IsConstructor) { + ctorIndex++; + } + } + nativeCallbackName = ctorIndex == 0 ? "n_ctor" : $"n_ctor_{ctorIndex}"; + } + methods.Add (new MarshalMethodInfo { JniName = registerInfo.JniName, JniSignature = registerInfo.Signature ?? "()V", Connector = registerInfo.Connector, ManagedMethodName = index.Reader.GetString (methodDef.Name), - NativeCallbackName = $"n_{index.Reader.GetString (methodDef.Name)}", + NativeCallbackName = nativeCallbackName, JniReturnType = JniSignatureHelper.ParseReturnTypeString (registerInfo.Signature ?? "()V"), Parameters = ParseJniParameters (registerInfo.Signature ?? "()V"), - IsConstructor = registerInfo.JniName == "" || registerInfo.JniName == ".ctor", + IsConstructor = isConstructor, ThrownNames = exportInfo?.ThrownNames, SuperArgumentsString = exportInfo?.SuperArgumentsString, }); From 2204d91c4c398eb6b882c66a875faf34e6717f1f Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 20 Feb 2026 12:04:05 +0100 Subject: [PATCH 07/40] Trigger CI rerun for Xamarin.Android-PR Retry all PR checks to refresh failing internal status context.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> From 4efc1cdc3a510b16a88ec58d052e89d5301c24dc Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 20 Feb 2026 12:28:32 +0100 Subject: [PATCH 08/40] [CI] Throttle Linux MSBuild node count Add a Linux template parameter for MSBUILD_ARGS and set -m:2 in the public pipeline Linux build to reduce recurrent MSB4166/OOM failures.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- build-tools/automation/azure-pipelines-public.yaml | 1 + build-tools/automation/yaml-templates/build-linux-steps.yaml | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/build-tools/automation/azure-pipelines-public.yaml b/build-tools/automation/azure-pipelines-public.yaml index e3fc3b8a359..8d38b3049f4 100644 --- a/build-tools/automation/azure-pipelines-public.yaml +++ b/build-tools/automation/azure-pipelines-public.yaml @@ -160,6 +160,7 @@ stages: buildResultArtifactName: Build Results - Linux xaSourcePath: $(System.DefaultWorkingDirectory)/android nugetArtifactName: $(LinuxNuGetArtifactName) + makeMSBuildArgs: -m:2 use1ESTemplate: false # Package Tests Stage diff --git a/build-tools/automation/yaml-templates/build-linux-steps.yaml b/build-tools/automation/yaml-templates/build-linux-steps.yaml index 02d34dafda2..bdc9c6a65c0 100644 --- a/build-tools/automation/yaml-templates/build-linux-steps.yaml +++ b/build-tools/automation/yaml-templates/build-linux-steps.yaml @@ -5,6 +5,7 @@ parameters: buildResultArtifactName: Build Results - Linux xaSourcePath: $(System.DefaultWorkingDirectory)/android nugetArtifactName: $(LinuxNuGetArtifactName) + makeMSBuildArgs: '' use1ESTemplate: true steps: @@ -26,7 +27,7 @@ steps: - template: /build-tools/automation/yaml-templates/log-disk-space.yaml -- script: make jenkins PREPARE_CI=1 PREPARE_AUTOPROVISION=1 CONFIGURATION=$(XA.Build.Configuration) +- script: make jenkins PREPARE_CI=1 PREPARE_AUTOPROVISION=1 CONFIGURATION=$(XA.Build.Configuration) MSBUILD_ARGS='${{ parameters.makeMSBuildArgs }}' workingDirectory: ${{ parameters.xaSourcePath }} displayName: make jenkins From 2aca0050a180e3f25725be7992e2b0c39ab45822 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 10 Mar 2026 11:28:09 +0100 Subject: [PATCH 09/40] Address review feedback and slim down PR - Move marshal method scanning infrastructure to follow-up PR - Removes MarshalMethodInfo, JniParameterInfo, JniSignatureHelper - Removes CollectMarshalMethods, ParseExportAttribute, and related scanner code - Removes CompatJniName (only needed by Export support) - Removes related scanner tests for marshal methods - Fix double-buffered Generate(string) in RootTypeMapAssemblyGenerator - Remove dead CleanUpDir method from FixtureTestBase Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/JniSignatureHelper.cs | 53 ---- .../Generator/PEAssemblyBuilder.cs | 12 +- .../Generator/RootTypeMapAssemblyGenerator.cs | 33 ++- .../Generator/TypeMapAssemblyEmitter.cs | 115 +++++---- .../Scanner/JavaPeerInfo.cs | 111 --------- .../Scanner/JavaPeerScanner.cs | 226 +----------------- .../Generator/FixtureTestBase.cs | 39 +-- .../RootTypeMapAssemblyGeneratorTests.cs | 40 ++-- .../Generator/TypeMapModelBuilderTests.cs | 4 +- .../Scanner/JavaPeerScannerTests.Behavior.cs | 70 ------ .../Scanner/JavaPeerScannerTests.EdgeCases.cs | 12 - 11 files changed, 141 insertions(+), 574 deletions(-) delete mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs deleted file mode 100644 index d07bb062bd3..00000000000 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Microsoft.Android.Sdk.TrimmableTypeMap; - -/// -/// Helpers for parsing JNI method signatures. -/// -static class JniSignatureHelper -{ - /// - /// 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; - SkipSingleType (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); - } - - static void SkipSingleType (string sig, ref int i) - { - switch (sig [i]) { - case 'V': case 'Z': case 'B': case 'C': case 'S': - case 'I': case 'J': case 'F': case 'D': - i++; - break; - case 'L': - i = sig.IndexOf (';', i) + 1; - break; - case '[': - i++; - SkipSingleType (sig, ref i); - break; - default: - throw new ArgumentException ($"Unknown JNI type character '{sig [i]}' in '{sig}' at index {i}"); - } - } -} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs index 9cae7720e6a..3ad3da53396 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 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); - using var fs = File.Create (outputPath); - peBlob.WriteContentTo (fs); + peBlob.WriteContentTo (stream); } /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs index ef538272ea5..14b49cfe986 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs @@ -32,22 +32,45 @@ 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 (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 (outputPath is null) { - throw new ArgumentNullException (nameof (outputPath)); + if (stream is null) { + throw new ArgumentNullException (nameof (stream)); } assemblyName ??= DefaultAssemblyName; - var moduleName = Path.GetFileName (outputPath); + moduleName ??= assemblyName + ".dll"; var pe = new PEAssemblyBuilder (_systemRuntimeVersion); pe.EmitPreamble (assemblyName, moduleName); @@ -76,6 +99,6 @@ public void Generate (IReadOnlyList perAssemblyTypeMapNames, string outp pe.Metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, ctorRef, blobHandle); } - pe.WritePE (outputPath); + pe.WritePE (stream); } } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index f96d3448647..13d6952378e 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -44,7 +44,7 @@ sealed class TypeMapAssemblyEmitter { readonly Version _systemRuntimeVersion; - PEAssemblyBuilder _pe = null!; + readonly PEAssemblyBuilder _pe; AssemblyReferenceHandle _javaInteropRef; @@ -74,6 +74,7 @@ sealed class TypeMapAssemblyEmitter public TypeMapAssemblyEmitter (Version systemRuntimeVersion) { _systemRuntimeVersion = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion)); + _pe = new PEAssemblyBuilder (_systemRuntimeVersion); } /// @@ -88,7 +89,6 @@ public void Emit (TypeMapAssemblyData model, string outputPath) throw new ArgumentNullException (nameof (outputPath)); } - _pe = new PEAssemblyBuilder (_systemRuntimeVersion); _pe.EmitPreamble (model.AssemblyName, model.ModuleName); _javaInteropRef = _pe.AddAssemblyRef ("Java.Interop", new Version (0, 0, 0, 0)); @@ -242,34 +242,17 @@ void EmitProxyType (JavaPeerProxyData proxy) void EmitCreateInstance (JavaPeerProxyData proxy) { if (!proxy.HasActivation) { - EmitCreateInstanceBody (encoder => { - encoder.OpCode (ILOpCode.Ldnull); - encoder.OpCode (ILOpCode.Ret); - }); + EmitCreateInstanceNoActivation (); 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); - }); + EmitCreateInstanceGenericDefinition (); return; } - // Interface with invoker: new TInvoker(IntPtr, JniHandleOwnership) if (proxy.InvokerType != null) { - 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); - }); + EmitCreateInstanceInvoker (proxy); return; } @@ -278,34 +261,72 @@ 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); - EmitCreateInstanceBody (encoder => { - encoder.OpCode (ILOpCode.Ldarg_1); - encoder.OpCode (ILOpCode.Ldarg_2); - encoder.OpCode (ILOpCode.Newobj); - encoder.Token (ctorRef); - encoder.OpCode (ILOpCode.Ret); - }); + EmitCreateInstanceLeafCtor (targetTypeRef); } 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); - encoder.Call (_getUninitializedObjectRef); - encoder.OpCode (ILOpCode.Castclass); - encoder.Token (targetTypeRef); + EmitCreateInstanceInheritedCtor (targetTypeRef, activationCtor); + } + } - encoder.OpCode (ILOpCode.Dup); - encoder.OpCode (ILOpCode.Ldarg_1); - encoder.OpCode (ILOpCode.Ldarg_2); - encoder.Call (baseActivationCtorRef); + void EmitCreateInstanceNoActivation () + { + EmitCreateInstanceBody (encoder => { + encoder.OpCode (ILOpCode.Ldnull); + encoder.OpCode (ILOpCode.Ret); + }); + } - 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 EmitCreateInstanceInvoker (JavaPeerProxyData proxy) + { + 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); + }); + } + + void EmitCreateInstanceLeafCtor (EntityHandle targetTypeRef) + { + var ctorRef = AddActivationCtorRef (targetTypeRef); + 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); + }); } void EmitCreateInstanceBody (Action emitIL) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index 2de7a49ead9..df714fdb52d 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; - namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// @@ -16,13 +13,6 @@ sealed record JavaPeerInfo /// public required string JavaName { get; init; } - /// - /// Compat JNI type name, e.g., "myapp.namespace/MyType" for user types (uses raw namespace, not CRC64). - /// For MCW binding types (with [Register]), this equals . - /// Used by acw-map.txt to support legacy custom view name resolution in layout XMLs. - /// - public required string CompatJniName { get; init; } - /// /// Full managed type name, e.g., "Android.App.Activity". /// @@ -59,14 +49,6 @@ sealed record JavaPeerInfo /// public bool IsUnconditional { get; init; } - /// - /// Marshal methods: methods with [Register(name, sig, connector)], [Export], or - /// constructor registrations ([Register(".ctor", sig, "")] / [JniConstructorSignature]). - /// Constructors are identified by . - /// Ordered — the index in this list is the method's ordinal for RegisterNatives. - /// - public IReadOnlyList MarshalMethods { 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. @@ -86,99 +68,6 @@ sealed record JavaPeerInfo public bool IsGenericDefinition { get; init; } } -/// -/// Describes a marshal method (a method with [Register] or [Export]) on a Java peer type. -/// Contains all data needed to generate a UCO wrapper, a JCW native declaration, -/// and a RegisterNatives call. -/// -sealed record MarshalMethodInfo -{ - /// - /// JNI method name, e.g., "onCreate". - /// This is the Java method name (without n_ prefix). - /// - public required string JniName { get; init; } - - /// - /// JNI method signature, e.g., "(Landroid/os/Bundle;)V". - /// Contains both parameter types and return type. - /// - public required string JniSignature { get; init; } - - /// - /// The connector string from [Register], e.g., "GetOnCreate_Landroid_os_Bundle_Handler". - /// Null for [Export] methods. - /// - public string? Connector { get; init; } - - /// - /// Name of the managed method this maps to, e.g., "OnCreate". - /// - 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. - /// - public bool IsConstructor { get; init; } - - /// - /// For [Export] methods: Java exception types that the method declares it can throw. - /// Null for [Register] methods. - /// - public IReadOnlyList? ThrownNames { get; init; } - - /// - /// For [Export] methods: super constructor arguments string. - /// Null for [Register] methods. - /// - 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 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 81c7f8a4c5f..82b13d1cb3f 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Reflection.Metadata; @@ -164,7 +163,6 @@ void ScanAssembly (AssemblyIndex index, Dictionary results // 3. Extends a known Java peer → auto-compute JNI name via CRC64 // 4. None of the above → not a Java peer, skip string? jniName = null; - string? compatJniName = null; bool doNotGenerateAcw = false; index.RegisterInfoByType.TryGetValue (typeHandle, out var registerInfo); @@ -172,17 +170,15 @@ void ScanAssembly (AssemblyIndex index, Dictionary results if (registerInfo is not null && !string.IsNullOrEmpty (registerInfo.JniName)) { jniName = registerInfo.JniName; - compatJniName = jniName; doNotGenerateAcw = registerInfo.DoNotGenerateAcw; } else if (attrInfo?.JniName is not null) { // User type with [Activity(Name = "...")] but no [Register] jniName = attrInfo.JniName; - compatJniName = jniName; } else { // No explicit JNI name — check if this type extends a known Java peer. // If so, auto-compute JNI name from the managed type name via CRC64. if (ExtendsJavaPeer (typeDef, index)) { - (jniName, compatJniName) = ComputeAutoJniNames (typeDef, index); + jniName = ComputeAutoJniName (typeDef, index); } else { continue; } @@ -197,9 +193,6 @@ void ScanAssembly (AssemblyIndex index, Dictionary results var isUnconditional = attrInfo is not null; string? invokerTypeName = null; - // Collect marshal methods (including constructors) in a single pass over methods - var marshalMethods = CollectMarshalMethods (typeDef, index); - // Resolve activation constructor var activationCtor = ResolveActivationCtor (fullName, typeDef, index); @@ -210,7 +203,6 @@ void ScanAssembly (AssemblyIndex index, Dictionary results var peer = new JavaPeerInfo { JavaName = jniName, - CompatJniName = compatJniName, ManagedTypeName = fullName, ManagedTypeNamespace = ExtractNamespace (fullName), ManagedTypeShortName = ExtractShortName (fullName), @@ -219,7 +211,6 @@ void ScanAssembly (AssemblyIndex index, Dictionary results IsAbstract = isAbstract, DoNotGenerateAcw = doNotGenerateAcw, IsUnconditional = isUnconditional, - MarshalMethods = marshalMethods, ActivationCtor = activationCtor, InvokerTypeName = invokerTypeName, IsGenericDefinition = isGenericDefinition, @@ -229,186 +220,6 @@ void ScanAssembly (AssemblyIndex index, Dictionary results } } - List CollectMarshalMethods (TypeDefinition typeDef, AssemblyIndex index) - { - var methods = new List (); - - // Single pass over methods: collect marshal methods (including constructors) - foreach (var methodHandle in typeDef.GetMethods ()) { - var methodDef = index.Reader.GetMethodDefinition (methodHandle); - if (!TryGetMethodRegisterInfo (methodDef, index, out var registerInfo, out var exportInfo) || registerInfo is null) { - continue; - } - - AddMarshalMethod (methods, registerInfo, methodDef, index, exportInfo); - } - - // Collect [Register] from properties (attribute is on the property, not the getter) - foreach (var propHandle in typeDef.GetProperties ()) { - var propDef = index.Reader.GetPropertyDefinition (propHandle); - var propRegister = TryGetPropertyRegisterInfo (propDef, index); - if (propRegister is null) { - continue; - } - - var accessors = propDef.GetAccessors (); - if (!accessors.Getter.IsNil) { - var getterDef = index.Reader.GetMethodDefinition (accessors.Getter); - AddMarshalMethod (methods, propRegister, getterDef, index); - } - } - - return methods; - } - - static void AddMarshalMethod (List methods, RegisterInfo registerInfo, MethodDefinition methodDef, AssemblyIndex index, ExportInfo? exportInfo = null) - { - // Skip methods that are just the JNI name (type-level [Register]) - if (registerInfo.Signature is null && registerInfo.Connector is null) { - return; - } - - bool isConstructor = registerInfo.JniName == "" || registerInfo.JniName == ".ctor"; - string nativeCallbackName = $"n_{index.Reader.GetString (methodDef.Name)}"; - if (isConstructor) { - int ctorIndex = 0; - foreach (var method in methods) { - if (method.IsConstructor) { - ctorIndex++; - } - } - nativeCallbackName = ctorIndex == 0 ? "n_ctor" : $"n_ctor_{ctorIndex}"; - } - - methods.Add (new MarshalMethodInfo { - JniName = registerInfo.JniName, - JniSignature = registerInfo.Signature ?? "()V", - Connector = registerInfo.Connector, - ManagedMethodName = index.Reader.GetString (methodDef.Name), - NativeCallbackName = nativeCallbackName, - JniReturnType = JniSignatureHelper.ParseReturnTypeString (registerInfo.Signature ?? "()V"), - Parameters = ParseJniParameters (registerInfo.Signature ?? "()V"), - IsConstructor = isConstructor, - ThrownNames = exportInfo?.ThrownNames, - SuperArgumentsString = exportInfo?.SuperArgumentsString, - }); - } - - static bool TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex index, out RegisterInfo? registerInfo, out ExportInfo? exportInfo) - { - exportInfo = null; - foreach (var caHandle in methodDef.GetCustomAttributes ()) { - var ca = index.Reader.GetCustomAttribute (caHandle); - var attrName = AssemblyIndex.GetCustomAttributeName (ca, index.Reader); - - if (attrName == "RegisterAttribute") { - registerInfo = index.ParseRegisterAttribute (ca); - return true; - } - - if (attrName == "ExportAttribute") { - (registerInfo, exportInfo) = ParseExportAttribute (ca, methodDef, index); - return true; - } - } - registerInfo = null; - return false; - } - - static RegisterInfo? TryGetPropertyRegisterInfo (PropertyDefinition propDef, AssemblyIndex index) - { - foreach (var caHandle in propDef.GetCustomAttributes ()) { - var ca = index.Reader.GetCustomAttribute (caHandle); - var attrName = AssemblyIndex.GetCustomAttributeName (ca, index.Reader); - - if (attrName == "RegisterAttribute") { - return index.ParseRegisterAttribute (ca); - } - } - return null; - } - - static (RegisterInfo registerInfo, ExportInfo exportInfo) ParseExportAttribute (CustomAttribute ca, MethodDefinition methodDef, AssemblyIndex index) - { - var value = index.DecodeAttribute (ca); - - // [Export("name")] or [Export] (uses method name) - string? exportName = null; - if (value.FixedArguments.Length > 0) { - exportName = (string?)value.FixedArguments [0].Value; - } - - List? thrownNames = null; - string? superArguments = null; - - // Check Named arguments - foreach (var named in value.NamedArguments) { - if (named.Name == "Name" && named.Value is string name) { - exportName = name; - } else if (named.Name == "ThrownNames" && named.Value is ImmutableArray> names) { - thrownNames = new List (names.Length); - foreach (var item in names) { - if (item.Value is string s) { - thrownNames.Add (s); - } - } - } else if (named.Name == "SuperArgumentsString" && named.Value is string superArgs) { - superArguments = superArgs; - } - } - - if (string.IsNullOrEmpty (exportName)) { - exportName = index.Reader.GetString (methodDef.Name); - } - string resolvedExportName = exportName ?? throw new InvalidOperationException ("Export name should not be null at this point."); - - // Build JNI signature from method signature - var sig = methodDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default); - var jniSig = BuildJniSignatureFromManaged (sig); - - return ( - new RegisterInfo { JniName = resolvedExportName, Signature = jniSig, Connector = null, DoNotGenerateAcw = false }, - new ExportInfo { ThrownNames = thrownNames, SuperArgumentsString = superArguments } - ); - } - - static string BuildJniSignatureFromManaged (MethodSignature sig) - { - var sb = new System.Text.StringBuilder (); - sb.Append ('('); - foreach (var param in sig.ParameterTypes) { - sb.Append (ManagedTypeToJniDescriptor (param)); - } - sb.Append (')'); - sb.Append (ManagedTypeToJniDescriptor (sig.ReturnType)); - return sb.ToString (); - } - - static string ManagedTypeToJniDescriptor (string managedType) - { - switch (managedType) { - case "System.Void": return "V"; - case "System.Boolean": return "Z"; - case "System.Byte": - case "System.SByte": return "B"; - case "System.Char": return "C"; - case "System.Int16": - case "System.UInt16": return "S"; - case "System.Int32": - case "System.UInt32": return "I"; - case "System.Int64": - case "System.UInt64": return "J"; - case "System.Single": return "F"; - case "System.Double": return "D"; - case "System.String": return "Ljava/lang/String;"; - default: - if (managedType.EndsWith ("[]")) { - return $"[{ManagedTypeToJniDescriptor (managedType.Substring (0, managedType.Length - 2))}"; - } - return "Ljava/lang/Object;"; - } - } - ActivationCtorInfo? ResolveActivationCtor (string typeName, TypeDefinition typeDef, AssemblyIndex index) { var cacheKey = (typeName, index.AssemblyName); @@ -615,29 +426,21 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) } /// - /// Compute both JNI name and compat JNI name for a type without [Register] or component Name. + /// Compute JNI name for a type without [Register] or component Name. /// JNI name uses CRC64 hash of "namespace:assemblyName" for the package. - /// Compat JNI name uses the raw managed namespace (lowercased). - /// If a declaring type has [Register], its JNI name is used as prefix for both. + /// If a declaring type has [Register], its JNI name is used as prefix. /// Generic backticks are replaced with _. /// - static (string jniName, string compatJniName) ComputeAutoJniNames (TypeDefinition typeDef, AssemblyIndex index) + static string ComputeAutoJniName (TypeDefinition typeDef, AssemblyIndex index) { var (typeName, parentJniName, ns) = ComputeTypeNameParts (typeDef, index); if (parentJniName is not null) { - var name = $"{parentJniName}_{typeName}"; - return (name, name); + return $"{parentJniName}_{typeName}"; } var packageName = GetCrc64PackageName (ns, index.AssemblyName); - var jniName = $"{packageName}/{typeName}"; - - string compatName = ns.Length == 0 - ? typeName - : $"{ns.ToLowerInvariant ().Replace ('.', '/')}/{typeName}"; - - return (jniName, compatName); + return $"{packageName}/{typeName}"; } /// @@ -705,19 +508,10 @@ static string ExtractNamespace (string fullName) 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; - } - - 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; + return (lastPlus >= 0 ? typePart.Slice (lastPlus + 1) : typePart).ToString (); } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs index bb446ff3029..605a8127fba 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs @@ -33,13 +33,6 @@ protected static JavaPeerInfo FindFixtureByJavaName (string javaName) return peer; } - protected static void CleanUpDir (string path) - { - var dir = Path.GetDirectoryName (path); - if (dir != null && Directory.Exists (dir)) - try { Directory.Delete (dir, true); } catch { } - } - protected static JavaPeerInfo MakeMcwPeer (string jniName, string managedName, string asmName) { var ns = managedName.Contains ('.') ? managedName.Substring (0, managedName.LastIndexOf ('.')) : ""; @@ -64,25 +57,13 @@ protected static JavaPeerInfo MakePeerWithActivation (string jniName, string man } protected static JavaPeerInfo MakeAcwPeer (string jniName, string managedName, string asmName) - { - var peer = MakePeerWithActivation (jniName, managedName, asmName); - peer.DoNotGenerateAcw = false; - peer.MarshalMethods = new List { - new MarshalMethodInfo { - JniName = "", - NativeCallbackName = "n_ctor", - JniSignature = "()V", - IsConstructor = true, - }, - }; - return peer; - } + => MakePeerWithActivation (jniName, managedName, asmName); 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 = managedName.Contains ('.') ? managedName.Substring (0, managedName.LastIndexOf ('.')) : ""; var shortName = managedName.Contains ('.') ? managedName.Substring (managedName.LastIndexOf ('.') + 1) : managedName; @@ -96,14 +77,4 @@ protected static JavaPeerInfo MakeInterfacePeer ( InvokerTypeName = invokerName, }; } - - protected static MarshalMethodInfo MakeMarshalMethod (string jniName, string callbackName, string jniSig, bool isConstructor = false) - { - return new MarshalMethodInfo { - JniName = jniName, - NativeCallbackName = callbackName, - JniSignature = jniSig, - IsConstructor = isConstructor, - }; - } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs index b6dfe06c6b2..e61875ea376 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs @@ -8,26 +8,22 @@ 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) + static 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 +32,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 +42,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 +67,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 +78,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 +89,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/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 54d66fc03f7..9e1f167edd4 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -195,7 +195,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); @@ -476,7 +476,7 @@ 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); } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs index 81aabe9ad74..7a654f4693d 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs @@ -1,61 +1,9 @@ -using System.Linq; using Xunit; namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; public partial class JavaPeerScannerTests { - [Theory] - [InlineData ("android/app/Activity", "OnCreate", "onCreate", "(Landroid/os/Bundle;)V")] - [InlineData ("android/app/Activity", "OnStart", "onStart", "()V")] - [InlineData ("my/app/MainActivity", "OnCreate", "onCreate", "(Landroid/os/Bundle;)V")] - [InlineData ("my/app/AbstractBase", "DoWork", "doWork", "()V")] - [InlineData ("java/lang/Throwable", "Message", "getMessage", "()Ljava/lang/String;")] - [InlineData ("my/app/TouchHandler", "OnTouch", "onTouch", "(Landroid/view/View;I)Z")] - [InlineData ("my/app/TouchHandler", "OnFocusChange", "onFocusChange", "(Landroid/view/View;Z)V")] - [InlineData ("my/app/TouchHandler", "OnScroll", "onScroll", "(IFJD)V")] - [InlineData ("my/app/TouchHandler", "SetItems", "setItems", "([Ljava/lang/String;)V")] - public void Scan_MarshalMethod_HasCorrectSignature (string javaName, string managedName, string jniName, string jniSig) - { - var peers = ScanFixtures (); - var method = FindByJavaName (peers, javaName) - .MarshalMethods.FirstOrDefault (m => m.ManagedMethodName == managedName || m.JniName == jniName); - Assert.NotNull (method); - Assert.Equal (jniName, method.JniName); - Assert.Equal (jniSig, method.JniSignature); - } - - [Fact] - public void Scan_MarshalMethod_ConstructorsAndSpecialCases () - { - var peers = ScanFixtures (); - - var ctors = FindByJavaName (peers, "my/app/CustomView") - .MarshalMethods.Where (m => m.IsConstructor).ToList (); - Assert.Equal (2, ctors.Count); - Assert.Equal ("()V", ctors [0].JniSignature); - Assert.Equal ("(Landroid/content/Context;)V", ctors [1].JniSignature); - - Assert.DoesNotContain (FindByJavaName (peers, "my/app/MyHelper").MarshalMethods, m => m.IsConstructor); - - var exportMethod = FindByJavaName (peers, "my/app/ExportExample").MarshalMethods.Single (); - Assert.Equal ("myExportedMethod", exportMethod.JniName); - Assert.Null (exportMethod.Connector); - - var onStart = FindByJavaName (peers, "android/app/Activity") - .MarshalMethods.FirstOrDefault (m => m.JniName == "onStart"); - Assert.NotNull (onStart); - Assert.Equal ("", onStart.Connector); - - var onClick = FindByManagedName (peers, "Android.Views.IOnClickListener") - .MarshalMethods.FirstOrDefault (m => m.JniName == "onClick"); - Assert.NotNull (onClick); - Assert.Equal ("(Landroid/view/View;)V", onClick.JniSignature); - - Assert.Equal ("Android.Views.IOnClickListenerInvoker", - FindByManagedName (peers, "Android.Views.IOnClickListener").InvokerTypeName); - } - [Theory] [InlineData ("android/app/Activity", "Android.App.Activity")] [InlineData ("my/app/SimpleActivity", "Android.App.Activity")] @@ -96,24 +44,6 @@ public void Scan_MultipleInterfaces_AllResolved () Assert.Empty (FindByJavaName (peers, "my/app/MyHelper").ImplementedInterfaceJavaNames); } - [Theory] - [InlineData ("android/app/Activity", "android/app/Activity")] - [InlineData ("my/app/MainActivity", "my/app/MainActivity")] - public void Scan_CompatJniName (string javaName, string expectedCompat) - { - var peers = ScanFixtures (); - Assert.Equal (expectedCompat, FindByJavaName (peers, javaName).CompatJniName); - } - - [Fact] - public void Scan_CompatJniName_UnregisteredType_UsesRawNamespace () - { - var peers = ScanFixtures (); - var unregistered = FindByManagedName (peers, "MyApp.UnregisteredHelper"); - Assert.StartsWith ("crc64", unregistered.JavaName); - Assert.Equal ("myapp/UnregisteredHelper", unregistered.CompatJniName); - } - [Fact] public void Scan_CustomJniNameProviderAttribute_UsesNameFromAttribute () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs index ec368ed6a44..4c7fa70aee1 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs @@ -1,4 +1,3 @@ -using System.Linq; using Xunit; namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; @@ -42,8 +41,6 @@ public void Scan_EmptyNamespace_Handled () { var peers = ScanFixtures (); Assert.Equal ("GlobalType", FindByJavaName (peers, "my/app/GlobalType").ManagedTypeName); - Assert.Equal ("GlobalUnregisteredType", - FindByManagedName (peers, "GlobalUnregisteredType").CompatJniName); } [Theory] @@ -57,13 +54,4 @@ public void Scan_UnregisteredType_DiscoveredWithCrc64Name (string managedName) Assert.StartsWith ("crc64", FindByManagedName (peers, managedName).JavaName); } - [Fact] - public void Scan_ExportOnUnregisteredType_MethodDiscovered () - { - var peers = ScanFixtures (); - var exportMethod = FindByManagedName (peers, "MyApp.UnregisteredExporter") - .MarshalMethods.FirstOrDefault (m => m.JniName == "doExportedWork"); - Assert.NotNull (exportMethod); - Assert.Null (exportMethod.Connector); - } } From 2a7bb4b370956ea612a3c742a38a19b8e3a02856 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 10 Mar 2026 11:56:21 +0100 Subject: [PATCH 10/40] Simplify unit tests: merge Facts into Theories, extract TempDirTestBase - Extract TempDirTestBase in TypeMapAssemblyGeneratorTests to eliminate 8x repeated IDisposable boilerplate (~14 lines saved) - Merge Build_AssemblyNameDerivedFromOutputPath + Build_ExplicitAssemblyName_OverridesOutputPath into a single [Theory] in ModelBuilderTests.BasicStructure - Merge Build_PeerWithActivationCtor_CreatesProxy + Build_ProxyNaming_ReplacesDotAndPlus into a single [Theory] in ModelBuilderTests.ProxyTypes - Merge Generate_EmptyList_ProducesValidAssemblyWithNoTargetAttributes + Generate_MultipleTargets_HasCorrectAttributeCount into a single [Theory] in RootTypeMapAssemblyGeneratorTests - Merge Generate_DuplicateJniNames_CreatesAliasEntries + Generate_DuplicateJniNames_EmitsTypeMapAssociationAttribute into one test in TypeMapAssemblyGeneratorTests.Alias - Add missing CreateTempDir/DeleteTempDir helpers to FixtureTestBase - Fix CS0050 accessibility errors by using private protected on methods returning internal types Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/FixtureTestBase.cs | 26 ++++++-- .../RootTypeMapAssemblyGeneratorTests.cs | 20 ++---- .../TypeMapAssemblyGeneratorTests.cs | 66 ++++--------------- .../Generator/TypeMapModelBuilderTests.cs | 42 ++++-------- 4 files changed, 53 insertions(+), 101 deletions(-) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs index 605a8127fba..39948a5796f 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs @@ -8,6 +8,20 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; public abstract class FixtureTestBase { + protected static string CreateTempDir () + { + var dir = Path.Combine (Path.GetTempPath (), "TypeMapTests_" + Guid.NewGuid ().ToString ("N")); + Directory.CreateDirectory (dir); + return dir; + } + + protected static void DeleteTempDir (string dir) + { + if (Directory.Exists (dir)) { + Directory.Delete (dir, recursive: true); + } + } + static string TestFixtureAssemblyPath { get { var testAssemblyDir = Path.GetDirectoryName (typeof (FixtureTestBase).Assembly.Location)!; @@ -23,9 +37,9 @@ static string TestFixtureAssemblyPath { return scanner.Scan (new [] { TestFixtureAssemblyPath }); }); - protected static List ScanFixtures () => _cachedFixtures.Value; + private protected static List ScanFixtures () => _cachedFixtures.Value; - protected static JavaPeerInfo FindFixtureByJavaName (string javaName) + private protected static JavaPeerInfo FindFixtureByJavaName (string javaName) { var peers = ScanFixtures (); var peer = peers.FirstOrDefault (p => p.JavaName == javaName); @@ -33,7 +47,7 @@ protected static JavaPeerInfo FindFixtureByJavaName (string javaName) return peer; } - protected static JavaPeerInfo MakeMcwPeer (string jniName, string managedName, string asmName) + private protected static JavaPeerInfo MakeMcwPeer (string jniName, string managedName, string asmName) { var ns = managedName.Contains ('.') ? managedName.Substring (0, managedName.LastIndexOf ('.')) : ""; var typePart = managedName.Contains ('.') ? managedName.Substring (managedName.LastIndexOf ('.') + 1) : managedName; @@ -47,7 +61,7 @@ protected static JavaPeerInfo MakeMcwPeer (string jniName, string managedName, s }; } - protected static JavaPeerInfo MakePeerWithActivation (string jniName, string managedName, string asmName) + private protected static JavaPeerInfo MakePeerWithActivation (string jniName, string managedName, string asmName) { var peer = MakeMcwPeer (jniName, managedName, asmName); peer.ActivationCtor = new ActivationCtorInfo { @@ -56,10 +70,10 @@ protected static JavaPeerInfo MakePeerWithActivation (string jniName, string man return peer; } - protected static JavaPeerInfo MakeAcwPeer (string jniName, string managedName, string asmName) + private protected static JavaPeerInfo MakeAcwPeer (string jniName, string managedName, string asmName) => MakePeerWithActivation (jniName, managedName, asmName); - protected static JavaPeerInfo MakeInterfacePeer ( + private protected static JavaPeerInfo MakeInterfacePeer ( string jniName, string managedName, string asmName, diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs index e61875ea376..52a2a894bcd 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs @@ -64,25 +64,17 @@ public void Generate_ReferencesGenericTypeMapAssemblyTargetAttribute () reader.GetString (t.Name).Contains ("TypeMapAssemblyTarget")); } - [Fact] - public void Generate_EmptyList_ProducesValidAssemblyWithNoTargetAttributes () - { - 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); - } - - [Fact] - public void Generate_MultipleTargets_HasCorrectAttributeCount () + [Theory] + [InlineData (0, 0)] + [InlineData (3, 3)] + public void Generate_AttributeCount_MatchesTargetCount (int targetCount, int expectedCount) { - var targets = new [] { "_App.TypeMap", "_Mono.Android.TypeMap", "_Java.Interop.TypeMap" }; + var targets = Enumerable.Range (0, targetCount).Select (i => $"_Target{i}.TypeMap").ToArray (); 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 ()); + Assert.Equal (expectedCount, asmAttrs.Count ()); } [Fact] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index cceb2e20a62..06c311bb599 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -38,11 +38,14 @@ static List GetTypeRefNames (MetadataReader reader) => .Select (t => reader.GetString (t.Name)) .ToList (); - public class BasicAssemblyStructure : IDisposable + public abstract class TempDirTestBase : IDisposable { - readonly string _outputDir = CreateTempDir (); + protected readonly string _outputDir = CreateTempDir (); public void Dispose () => DeleteTempDir (_outputDir); + } + public class BasicAssemblyStructure : TempDirTestBase + { [Fact] public void Generate_ProducesValidPEAssembly () { @@ -54,14 +57,10 @@ public void Generate_ProducesValidPEAssembly () var reader = pe.GetMetadataReader (); Assert.NotNull (reader); } - } - public class AssemblyReference : IDisposable + public class AssemblyReference : TempDirTestBase { - readonly string _outputDir = CreateTempDir (); - public void Dispose () => DeleteTempDir (_outputDir); - [Fact] public void Generate_HasRequiredAssemblyReferences () { @@ -78,14 +77,10 @@ public void Generate_HasRequiredAssemblyReferences () Assert.Contains ("System.Runtime.InteropServices", asmRefs); } } - } - public class ProxyType : IDisposable + public class ProxyType : TempDirTestBase { - readonly string _outputDir = CreateTempDir (); - public void Dispose () => DeleteTempDir (_outputDir); - [Fact] public void Generate_CreatesProxyTypes () { @@ -124,14 +119,10 @@ public void Generate_ProxyType_HasCtorAndCreateInstance () Assert.Contains ("get_TargetType", methods); } } - } - public class IgnoresAccessChecksTo : IDisposable + public class IgnoresAccessChecksTo : TempDirTestBase { - readonly string _outputDir = CreateTempDir (); - public void Dispose () => DeleteTempDir (_outputDir); - [Fact] public void Generate_HasIgnoresAccessChecksToAttribute () { @@ -147,14 +138,10 @@ public void Generate_HasIgnoresAccessChecksToAttribute () reader.GetString (t.Namespace) == "System.Runtime.CompilerServices"); } } - } - public class Alias : IDisposable + public class Alias : TempDirTestBase { - readonly string _outputDir = CreateTempDir (); - public void Dispose () => DeleteTempDir (_outputDir); - static List MakeDuplicateAliasPeers () => new List { new JavaPeerInfo { JavaName = "test/Duplicate", @@ -178,7 +165,7 @@ public class Alias : IDisposable }; [Fact] - public void Generate_DuplicateJniNames_CreatesAliasEntries () + public void Generate_DuplicateJniNames_CreatesAliasEntriesAndAssociationAttribute () { var peers = MakeDuplicateAliasPeers (); var path = GenerateAssembly (peers, _outputDir, "AliasTest"); @@ -186,33 +173,15 @@ public void Generate_DuplicateJniNames_CreatesAliasEntries () using (pe) { var assemblyAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); Assert.True (assemblyAttrs.Count () >= 3); - } - } - - [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); } } - } - public class EmptyInput : IDisposable + public class EmptyInput : TempDirTestBase { - readonly string _outputDir = CreateTempDir (); - public void Dispose () => DeleteTempDir (_outputDir); - [Fact] public void Generate_EmptyPeerList_ProducesValidAssembly () { @@ -225,14 +194,10 @@ public void Generate_EmptyPeerList_ProducesValidAssembly () Assert.Equal ("EmptyTest", reader.GetString (asmDef.Name)); } } - } - public class CreateInstancePaths : IDisposable + public class CreateInstancePaths : TempDirTestBase { - readonly string _outputDir = CreateTempDir (); - public void Dispose () => DeleteTempDir (_outputDir); - [Fact] public void Generate_SimpleActivity_UsesGetUninitializedObject () { @@ -290,14 +255,10 @@ public void Generate_GenericType_ThrowsNotSupportedException () Assert.Contains ("NotSupportedException", typeNames); } } - } - public class IgnoresAccessChecksToForBaseCtor : IDisposable + public class IgnoresAccessChecksToForBaseCtor : TempDirTestBase { - readonly string _outputDir = CreateTempDir (); - public void Dispose () => DeleteTempDir (_outputDir); - [Fact] public void Generate_InheritedCtor_IncludesBaseCtorAssembly () { @@ -327,7 +288,6 @@ public void Generate_InheritedCtor_IncludesBaseCtorAssembly () Assert.Contains (attrBlobs, b => b.Contains ("TestFixtures")); } } - } } \ 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 9e1f167edd4..3989b7083b9 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -30,19 +30,13 @@ public void Build_EmptyPeers_ProducesEmptyModel () Assert.Empty (model.ProxyTypes); } - [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 () + [Theory] + [InlineData ("/some/path/Foo.Bar.dll", null, "Foo.Bar")] + [InlineData ("/some/path/Foo.dll", "MyAssembly", "MyAssembly")] + public void Build_AssemblyName_ResolvedCorrectly (string outputPath, string? explicitName, string expected) { - var model = ModelBuilder.Build (Array.Empty (), "/some/path/Foo.dll", "MyAssembly"); - Assert.Equal ("MyAssembly", model.AssemblyName); + var model = ModelBuilder.Build (Array.Empty (), outputPath, explicitName); + Assert.Equal (expected, model.AssemblyName); } } @@ -177,19 +171,21 @@ public void Build_McwPeerWithoutActivation_NoProxy () 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] @@ -204,16 +200,6 @@ public void Build_PeerWithInvoker_CreatesProxy () 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 FixtureScan From 3de7bc97576a6e1af3c10b374a005ac9e1fc8bf6 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 10 Mar 2026 12:32:36 +0100 Subject: [PATCH 11/40] Cache ScanFixtures() in JavaPeerScannerTests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fixture assembly is static — no need to re-scan from disk on every test invocation. Use Lazy<> to cache the scan result, matching the pattern already used in FixtureTestBase. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Scanner/JavaPeerScannerTests.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs index bc2b6195f22..6bf476dfb7c 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs @@ -18,11 +18,12 @@ static string TestFixtureAssemblyPath { } } - List ScanFixtures () - { + static readonly Lazy> _cachedFixtures = new (() => { using var scanner = new JavaPeerScanner (); return scanner.Scan (new [] { TestFixtureAssemblyPath }); - } + }); + + List ScanFixtures () => _cachedFixtures.Value; JavaPeerInfo FindByJavaName (List peers, string javaName) { From 5b908e8fef56cc66d483cdb16baebb2b4418c7bb Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 10 Mar 2026 12:36:22 +0100 Subject: [PATCH 12/40] Use MemoryStream in tests, add stream overloads to generators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add Stream overloads to TypeMapAssemblyEmitter.Emit() and TypeMapAssemblyGenerator.Generate() — PEAssemblyBuilder.WritePE(Stream) already existed, so this just wires it through. Convert TypeMapAssemblyGeneratorTests to use MemoryStream exclusively, eliminating all temp directory creation/cleanup and IDisposable boilerplate. The nested test classes are flattened into the parent class. Convert TypeMapModelBuilderTests.EmitAndVerify to use MemoryStream, removing its temp directory usage as well. Remove now-unused CreateTempDir/DeleteTempDir from FixtureTestBase. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/TypeMapAssemblyEmitter.cs | 24 +- .../Generator/TypeMapAssemblyGenerator.cs | 14 + .../Generator/FixtureTestBase.cs | 14 - .../TypeMapAssemblyGeneratorTests.cs | 370 ++++++++---------- .../Generator/TypeMapModelBuilderTests.cs | 16 +- 5 files changed, 204 insertions(+), 234 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 13d6952378e..967d605e499 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; @@ -89,6 +90,28 @@ 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); _javaInteropRef = _pe.AddAssemblyRef ("Java.Interop", new Version (0, 0, 0, 0)); @@ -109,7 +132,6 @@ public void Emit (TypeMapAssemblyData model, string outputPath) } _pe.EmitIgnoresAccessChecksToAttribute (model.IgnoresAccessChecksTo); - _pe.WritePE (outputPath); } void EmitTypeReferences () diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs index 927346fbf10..f6586218d6a 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 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/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs index 39948a5796f..d80bedfe18c 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs @@ -8,20 +8,6 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; public abstract class FixtureTestBase { - protected static string CreateTempDir () - { - var dir = Path.Combine (Path.GetTempPath (), "TypeMapTests_" + Guid.NewGuid ().ToString ("N")); - Directory.CreateDirectory (dir); - return dir; - } - - protected static void DeleteTempDir (string dir) - { - if (Directory.Exists (dir)) { - Directory.Delete (dir, recursive: true); - } - } - static string TestFixtureAssemblyPath { get { var testAssemblyDir = Path.GetDirectoryName (typeof (FixtureTestBase).Assembly.Location)!; diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 06c311bb599..120971d5d7b 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -12,18 +12,13 @@ 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 = "TestTypeMap") { - 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; - } - - static (PEReader pe, MetadataReader reader) OpenAssembly (string path) - { - var pe = new PEReader (File.OpenRead (path)); - return (pe, pe.GetMetadataReader ()); + generator.Generate (peers, stream, assemblyName); + stream.Position = 0; + return stream; } static List GetMemberRefNames (MetadataReader reader) => @@ -38,111 +33,89 @@ static List GetTypeRefNames (MetadataReader reader) => .Select (t => reader.GetString (t.Name)) .ToList (); - public abstract class TempDirTestBase : IDisposable + [Fact] + public void Generate_ProducesValidPEAssembly () { - protected readonly string _outputDir = CreateTempDir (); - public void Dispose () => DeleteTempDir (_outputDir); + 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 : TempDirTestBase + [Fact] + public void Generate_HasRequiredAssemblyReferences () { - [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); - } + 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 AssemblyReference : TempDirTestBase + [Fact] + public void Generate_CreatesProxyTypes () { - [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); - } - } + 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 ProxyType : TempDirTestBase + [Fact] + public void Generate_ProxyType_HasCtorAndCreateInstance () { - [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 (); + 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); - } - } + Assert.Contains (".ctor", methods); + Assert.Contains ("CreateInstance", methods); + Assert.Contains ("get_TargetType", methods); } - public class IgnoresAccessChecksTo : TempDirTestBase + [Fact] + public void Generate_HasIgnoresAccessChecksToAttribute () { - [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"); - } - } + 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 : TempDirTestBase + [Fact] + public void Generate_DuplicateJniNames_CreatesAliasEntriesAndAssociationAttribute () { - static List MakeDuplicateAliasPeers () => new List { + var peers = new List { new JavaPeerInfo { JavaName = "test/Duplicate", ManagedTypeName = "Test.Duplicate1", @@ -164,130 +137,109 @@ public class Alias : TempDirTestBase }, }; - [Fact] - public void Generate_DuplicateJniNames_CreatesAliasEntriesAndAssociationAttribute () - { - 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); + 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); - } - } + var typeNames = GetTypeRefNames (reader); + Assert.Contains ("TypeMapAssociationAttribute", typeNames); } - public class EmptyInput : TempDirTestBase + [Fact] + public void Generate_EmptyPeerList_ProducesValidAssembly () { - [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)); - } - } + using var stream = GenerateAssembly (Array.Empty (), "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 CreateInstancePaths : TempDirTestBase + [Fact] + public void Generate_SimpleActivity_UsesGetUninitializedObject () { - [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); - } - } - - [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"); - } - } - - [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); - } - } + 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 IgnoresAccessChecksToForBaseCtor : TempDirTestBase + [Fact] + public void Generate_LeafCtor_DoesNotUseCreateManagedPeer () { - [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 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"); + } - 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"); + [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); + } - 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_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")); } } \ 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 3989b7083b9..f70269ea5b8 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -708,16 +708,12 @@ public void Build_SameInput_ProducesDeterministicOutput () 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 ()); } /// From b872b30173d221716f9be1a041bb641868a1ee01 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 10 Mar 2026 12:51:17 +0100 Subject: [PATCH 13/40] Fix compilation errors in TrimmableTypeMap tests - Add BaseJavaName and ImplementedInterfaceJavaNames properties to JavaPeerInfo, populated by the scanner via base-type and interface resolution (required by existing tests) - Fix init-only property mutations in test helpers: use 'with' expressions on record types instead of post-construction assignment - Fix MakePeerWithActivation to supply required DeclaringTypeName and DeclaringAssemblyName for ActivationCtorInfo Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Scanner/JavaPeerInfo.cs | 12 ++++++++ .../Scanner/JavaPeerScanner.cs | 28 +++++++++++++++++++ .../Generator/FixtureTestBase.cs | 10 ++++--- .../Generator/TypeMapModelBuilderTests.cs | 19 ++++++------- 4 files changed, 54 insertions(+), 15 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index df714fdb52d..df883b647f9 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; + namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// @@ -61,6 +63,16 @@ sealed record JavaPeerInfo /// public string? InvokerTypeName { get; init; } + /// + /// JNI name of the base Java peer type, if any. + /// + public string? BaseJavaName { get; init; } + + /// + /// JNI names of Java interfaces implemented by this type. + /// + public IReadOnlyList ImplementedInterfaceJavaNames { get; init; } = []; + /// /// True if this is an open generic type definition. /// Generic types get TypeMap entries but CreateInstance throws NotSupportedException. diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 82b13d1cb3f..6d6d59821ae 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -201,6 +201,16 @@ void ScanAssembly (AssemblyIndex index, Dictionary results invokerTypeName = TryFindInvokerTypeName (fullName, typeHandle, index); } + // Resolve base type JNI name + string? baseJavaName = null; + var baseTypeInfo = GetBaseTypeInfo (typeDef, index); + if (baseTypeInfo is not null) { + baseJavaName = ResolveRegisterJniName (baseTypeInfo.Value.typeName, baseTypeInfo.Value.assemblyName); + } + + // Resolve implemented interface JNI names + var implementedInterfaces = ResolveImplementedInterfaces (typeDef, index); + var peer = new JavaPeerInfo { JavaName = jniName, ManagedTypeName = fullName, @@ -214,6 +224,8 @@ void ScanAssembly (AssemblyIndex index, Dictionary results ActivationCtor = activationCtor, InvokerTypeName = invokerTypeName, IsGenericDefinition = isGenericDefinition, + BaseJavaName = baseJavaName, + ImplementedInterfaceJavaNames = implementedInterfaces, }; results [fullName] = peer; @@ -371,6 +383,22 @@ void ScanAssembly (AssemblyIndex index, Dictionary results return null; } + List ResolveImplementedInterfaces (TypeDefinition typeDef, AssemblyIndex index) + { + var result = new List (); + foreach (var ifaceImplHandle in typeDef.GetInterfaceImplementations ()) { + var ifaceImpl = index.Reader.GetInterfaceImplementation (ifaceImplHandle); + var resolved = ResolveEntityHandle (ifaceImpl.Interface, index); + if (resolved is not null) { + var ifaceJniName = ResolveRegisterJniName (resolved.Value.typeName, resolved.Value.assemblyName); + if (ifaceJniName is not null) { + result.Add (ifaceJniName); + } + } + } + return result; + } + public void Dispose () { foreach (var index in assemblyCache.Values) { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs index d80bedfe18c..358a94049ba 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs @@ -49,11 +49,13 @@ private protected static JavaPeerInfo MakeMcwPeer (string jniName, string manage private protected static JavaPeerInfo MakePeerWithActivation (string jniName, string managedName, string asmName) { - var peer = MakeMcwPeer (jniName, managedName, asmName); - peer.ActivationCtor = new ActivationCtorInfo { - Style = ActivationCtorStyle.XamarinAndroid, + return MakeMcwPeer (jniName, managedName, asmName) with { + ActivationCtor = new ActivationCtorInfo { + DeclaringTypeName = managedName, + DeclaringAssemblyName = asmName, + Style = ActivationCtorStyle.XamarinAndroid, + }, }; - return peer; } private protected static JavaPeerInfo MakeAcwPeer (string jniName, string managedName, string asmName) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index f70269ea5b8..b14c8e90c56 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -91,8 +91,7 @@ public class ConditionalAttributes [InlineData ("java/lang/Thread")] public void Build_AllEssentialRuntimeTypes_AreUnconditional (string jniName) { - var peer = MakeMcwPeer (jniName, "Java.Lang.SomeType", "Mono.Android"); - peer.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"); } @@ -114,8 +113,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"); - peer.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); @@ -128,9 +126,10 @@ public void Build_McwBinding_IsTrimmable () public void Build_UnconditionalScannedType_IsUnconditional () { // Types with IsUnconditional from scanner (e.g., from [Activity], [Service] attrs) - var peer = MakeMcwPeer ("my/app/MySvc", "MyApp.MyService", "App"); - peer.DoNotGenerateAcw = true; // simulate MCW-like - peer.IsUnconditional = true; // scanner marked it + var peer = MakeMcwPeer ("my/app/MySvc", "MyApp.MyService", "App") with { + DoNotGenerateAcw = true, // simulate MCW-like + IsUnconditional = true, // scanner marked it + }; var model = BuildModel (new [] { peer }); Assert.True (model.Entries [0].IsUnconditional); @@ -409,8 +408,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"); - invokerPeer.DoNotGenerateAcw = true; + var invokerPeer = MakePeerWithActivation ("my/app/IFoo", "MyApp.FooInvoker", "App") with { DoNotGenerateAcw = true }; var model = BuildModel (new [] { ifacePeer, invokerPeer }); @@ -516,8 +514,7 @@ 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"); - invokerPeer.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 }); From ff967d2f48e1b6c0b9cba5c26fea3fec3b436706 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 10 Mar 2026 12:54:32 +0100 Subject: [PATCH 14/40] Deduplicate code in TrimmableTypeMap - Merge identical EmitCreateInstanceInvoker and EmitCreateInstanceLeafCtor into a single EmitCreateInstanceViaNewobj method - Extract AssemblyQualify helper in ModelBuilder to consolidate repeated "$typeName, $assemblyName" formatting - Extract StripAssemblyQualification helper in JavaPeerScanner to deduplicate comma-split-trim logic - Make JavaPeerScannerTests extend FixtureTestBase, removing duplicated fixture loading, ScanFixtures, and Find* helpers - Extract ParseManagedTypeName in FixtureTestBase to deduplicate namespace/short-name parsing between MakeMcwPeer and MakeInterfacePeer - Add FindFixtureByManagedName to FixtureTestBase for shared use Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/ModelBuilder.cs | 13 +++-- .../Generator/TypeMapAssemblyEmitter.cs | 20 ++------ .../Scanner/JavaPeerScanner.cs | 31 +++++++----- .../Generator/FixtureTestBase.cs | 19 ++++++-- .../Scanner/JavaPeerScannerTests.Behavior.cs | 20 +++----- .../Scanner/JavaPeerScannerTests.EdgeCases.cs | 20 +++----- .../Scanner/JavaPeerScannerTests.cs | 48 +++---------------- 7 files changed, 68 insertions(+), 103 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index e64d14849a9..70620b72684 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -119,8 +119,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), }); } } @@ -220,15 +220,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 { @@ -237,4 +237,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/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 967d605e499..e2ccf77dd04 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -274,7 +274,7 @@ void EmitCreateInstance (JavaPeerProxyData proxy) } if (proxy.InvokerType != null) { - EmitCreateInstanceInvoker (proxy); + EmitCreateInstanceViaNewobj (_pe.ResolveTypeRef (proxy.InvokerType)); return; } @@ -283,7 +283,7 @@ void EmitCreateInstance (JavaPeerProxyData proxy) var targetTypeRef = _pe.ResolveTypeRef (proxy.TargetType); if (activationCtor.IsOnLeafType) { - EmitCreateInstanceLeafCtor (targetTypeRef); + EmitCreateInstanceViaNewobj (targetTypeRef); } else { EmitCreateInstanceInheritedCtor (targetTypeRef, activationCtor); } @@ -307,21 +307,9 @@ void EmitCreateInstanceGenericDefinition () }); } - void EmitCreateInstanceInvoker (JavaPeerProxyData proxy) + void EmitCreateInstanceViaNewobj (EntityHandle typeRef) { - 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); - }); - } - - void EmitCreateInstanceLeafCtor (EntityHandle targetTypeRef) - { - var ctorRef = AddActivationCtorRef (targetTypeRef); + var ctorRef = AddActivationCtorRef (typeRef); EmitCreateInstanceBody (encoder => { encoder.OpCode (ILOpCode.Ldarg_1); encoder.OpCode (ILOpCode.Ldarg_2); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 6d6d59821ae..f8591829cfc 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -135,13 +135,8 @@ static void ForceUnconditionalIfPresent (Dictionary result // TryGetTypeProperty may return assembly-qualified names like "Ns.Type, Assembly, ..." // Strip to just the type name for lookup - var commaIndex = managedTypeName.IndexOf (','); - if (commaIndex <= 0) { - return; - } - - var typeName = managedTypeName.Substring (0, commaIndex).Trim (); - if (typeName.Length > 0 && resultsByManagedName.TryGetValue (typeName, out peer)) { + var typeName = StripAssemblyQualification (managedTypeName); + if (typeName is not null && resultsByManagedName.TryGetValue (typeName, out peer)) { resultsByManagedName [typeName] = peer with { IsUnconditional = true }; } } @@ -364,11 +359,9 @@ void ScanAssembly (AssemblyIndex index, Dictionary results // where the connector contains the assembly-qualified invoker type name. if (index.RegisterInfoByType.TryGetValue (typeHandle, out var registerInfo) && registerInfo.Connector is not null) { var connector = registerInfo.Connector; - // The connector may be "TypeName" or "TypeName, Assembly, Version=..., Culture=..., PublicKeyToken=..." - // We want just the type name (before the first comma, if any) - var commaIndex = connector.IndexOf (','); - if (commaIndex > 0) { - return connector.Substring (0, commaIndex).Trim (); + var stripped = StripAssemblyQualification (connector); + if (stripped is not null) { + return stripped; } if (connector.Length > 0) { return connector; @@ -542,4 +535,18 @@ static string ExtractShortName (string fullName) int lastPlus = typePart.LastIndexOf ('+'); return (lastPlus >= 0 ? typePart.Slice (lastPlus + 1) : typePart).ToString (); } + + /// + /// Strips assembly qualification from a type name like "Ns.Type, Assembly, Version=..." + /// returning just the type name, or null if the input has no comma. + /// + static string? StripAssemblyQualification (string assemblyQualifiedName) + { + var commaIndex = assemblyQualifiedName.IndexOf (','); + if (commaIndex <= 0) { + return null; + } + var typeName = assemblyQualifiedName.Substring (0, commaIndex).Trim (); + return typeName.Length > 0 ? typeName : null; + } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs index 358a94049ba..2caa76fa94e 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs @@ -33,11 +33,25 @@ private protected static JavaPeerInfo FindFixtureByJavaName (string javaName) return peer; } - private protected static JavaPeerInfo MakeMcwPeer (string jniName, string managedName, string asmName) + private protected static JavaPeerInfo FindFixtureByManagedName (string managedName) + { + var peers = ScanFixtures (); + var peer = peers.FirstOrDefault (p => p.ManagedTypeName == managedName); + Assert.NotNull (peer); + return peer; + } + + static (string ns, string shortName) ParseManagedTypeName (string managedName) { var ns = managedName.Contains ('.') ? managedName.Substring (0, managedName.LastIndexOf ('.')) : ""; var typePart = managedName.Contains ('.') ? managedName.Substring (managedName.LastIndexOf ('.') + 1) : managedName; var shortName = typePart.Contains ('+') ? typePart.Substring (typePart.LastIndexOf ('+') + 1) : typePart; + return (ns, shortName); + } + + private protected static JavaPeerInfo MakeMcwPeer (string jniName, string managedName, string asmName) + { + var (ns, shortName) = ParseManagedTypeName (managedName); return new JavaPeerInfo { JavaName = jniName, ManagedTypeName = managedName, @@ -67,8 +81,7 @@ private protected static JavaPeerInfo MakeInterfacePeer ( string asmName, string invokerName) { - var ns = managedName.Contains ('.') ? managedName.Substring (0, managedName.LastIndexOf ('.')) : ""; - var shortName = managedName.Contains ('.') ? managedName.Substring (managedName.LastIndexOf ('.') + 1) : managedName; + var (ns, shortName) = ParseManagedTypeName (managedName); return new JavaPeerInfo { JavaName = jniName, ManagedTypeName = managedName, diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs index 7a654f4693d..6e2795c303b 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs @@ -10,8 +10,7 @@ public partial class JavaPeerScannerTests [InlineData ("my/app/MyButton", "MyApp.MyButton")] public void Scan_ActivationCtor_InheritsFromNearestBase (string javaName, string expectedDeclaringType) { - var peers = ScanFixtures (); - var peer = FindByJavaName (peers, javaName); + var peer = FindFixtureByJavaName (javaName); Assert.NotNull (peer.ActivationCtor); Assert.Equal (expectedDeclaringType, peer.ActivationCtor.DeclaringTypeName); } @@ -25,31 +24,27 @@ public void Scan_ActivationCtor_InheritsFromNearestBase (string javaName, string [InlineData ("my/app/MyButton", "android/widget/Button")] public void Scan_BaseJavaName_ResolvesCorrectly (string javaName, string? expectedBase) { - var peers = ScanFixtures (); - Assert.Equal (expectedBase, FindByJavaName (peers, javaName).BaseJavaName); + Assert.Equal (expectedBase, FindFixtureByJavaName (javaName).BaseJavaName); } [Fact] public void Scan_MultipleInterfaces_AllResolved () { - var peers = ScanFixtures (); - - var multi = FindByJavaName (peers, "my/app/MultiInterfaceView"); + var multi = FindFixtureByJavaName ("my/app/MultiInterfaceView"); Assert.Contains ("android/view/View$OnClickListener", multi.ImplementedInterfaceJavaNames); Assert.Contains ("android/view/View$OnLongClickListener", multi.ImplementedInterfaceJavaNames); Assert.Equal (2, multi.ImplementedInterfaceJavaNames.Count); Assert.Contains ("android/view/View$OnClickListener", - FindByJavaName (peers, "my/app/ClickableView").ImplementedInterfaceJavaNames); - Assert.Empty (FindByJavaName (peers, "my/app/MyHelper").ImplementedInterfaceJavaNames); + FindFixtureByJavaName ("my/app/ClickableView").ImplementedInterfaceJavaNames); + Assert.Empty (FindFixtureByJavaName ("my/app/MyHelper").ImplementedInterfaceJavaNames); } [Fact] public void Scan_CustomJniNameProviderAttribute_UsesNameFromAttribute () { - var peers = ScanFixtures (); Assert.Equal ("com/example/CustomWidget", - FindByManagedName (peers, "MyApp.CustomWidget").JavaName); + FindFixtureByManagedName ("MyApp.CustomWidget").JavaName); } [Theory] @@ -57,7 +52,6 @@ public void Scan_CustomJniNameProviderAttribute_UsesNameFromAttribute () [InlineData ("my/app/ICallback$Result", "MyApp.ICallback+Result")] public void Scan_NestedType_IsDiscovered (string javaName, string managedName) { - var peers = ScanFixtures (); - Assert.Equal (managedName, FindByJavaName (peers, javaName).ManagedTypeName); + Assert.Equal (managedName, FindFixtureByJavaName (javaName).ManagedTypeName); } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs index 4c7fa70aee1..6f1ceeed19e 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs @@ -7,23 +7,20 @@ public partial class JavaPeerScannerTests [Fact] public void Scan_GenericTypes_ResolveViaTypeSpecification () { - var peers = ScanFixtures (); Assert.Equal ("my/app/GenericBase", - FindByJavaName (peers, "my/app/ConcreteFromGeneric").BaseJavaName); + FindFixtureByJavaName ("my/app/ConcreteFromGeneric").BaseJavaName); Assert.Contains ("my/app/IGenericCallback", - FindByJavaName (peers, "my/app/GenericCallbackImpl").ImplementedInterfaceJavaNames); + FindFixtureByJavaName ("my/app/GenericCallbackImpl").ImplementedInterfaceJavaNames); } [Fact] public void Scan_ComponentOnlyBase_BothBaseAndDerivedDiscovered () { - var peers = ScanFixtures (); - - var baseType = FindByJavaName (peers, "my/app/BaseActivityNoRegister"); + var baseType = FindFixtureByJavaName ("my/app/BaseActivityNoRegister"); Assert.True (baseType.IsUnconditional); Assert.Equal ("android/app/Activity", baseType.BaseJavaName); - var derived = FindByManagedName (peers, "MyApp.DerivedFromComponentBase"); + var derived = FindFixtureByManagedName ("MyApp.DerivedFromComponentBase"); Assert.StartsWith ("crc64", derived.JavaName); } @@ -32,15 +29,13 @@ public void Scan_ComponentOnlyBase_BothBaseAndDerivedDiscovered () [InlineData ("MyApp.DeepOuter+Middle+DeepInner", "my/app/DeepOuter_Middle_DeepInner")] public void Scan_UnregisteredNestedType_UsesParentJniPrefix (string managedName, string expectedJavaName) { - var peers = ScanFixtures (); - Assert.Equal (expectedJavaName, FindByManagedName (peers, managedName).JavaName); + Assert.Equal (expectedJavaName, FindFixtureByManagedName (managedName).JavaName); } [Fact] public void Scan_EmptyNamespace_Handled () { - var peers = ScanFixtures (); - Assert.Equal ("GlobalType", FindByJavaName (peers, "my/app/GlobalType").ManagedTypeName); + Assert.Equal ("GlobalType", FindFixtureByJavaName ("my/app/GlobalType").ManagedTypeName); } [Theory] @@ -50,8 +45,7 @@ public void Scan_EmptyNamespace_Handled () [InlineData ("MyApp.UnregisteredExporter")] public void Scan_UnregisteredType_DiscoveredWithCrc64Name (string managedName) { - var peers = ScanFixtures (); - Assert.StartsWith ("crc64", FindByManagedName (peers, managedName).JavaName); + Assert.StartsWith ("crc64", FindFixtureByManagedName (managedName).JavaName); } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs index 6bf476dfb7c..555bba3451e 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs @@ -6,39 +6,8 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; -public partial class JavaPeerScannerTests +public partial class JavaPeerScannerTests : FixtureTestBase { - static string TestFixtureAssemblyPath { - get { - var testAssemblyDir = Path.GetDirectoryName (typeof (JavaPeerScannerTests).Assembly.Location)!; - var fixtureAssembly = Path.Combine (testAssemblyDir, "TestFixtures.dll"); - Assert.True (File.Exists (fixtureAssembly), - $"TestFixtures.dll not found at {fixtureAssembly}. Ensure the TestFixtures project builds."); - return fixtureAssembly; - } - } - - static readonly Lazy> _cachedFixtures = new (() => { - using var scanner = new JavaPeerScanner (); - return scanner.Scan (new [] { TestFixtureAssemblyPath }); - }); - - List ScanFixtures () => _cachedFixtures.Value; - - JavaPeerInfo FindByJavaName (List peers, string javaName) - { - var peer = peers.FirstOrDefault (p => p.JavaName == javaName); - Assert.NotNull (peer); - return peer; - } - - JavaPeerInfo FindByManagedName (List peers, string managedName) - { - var peer = peers.FirstOrDefault (p => p.ManagedTypeName == managedName); - Assert.NotNull (peer); - return peer; - } - [Fact] public void Scan_FindsAllJavaPeerTypes () { @@ -55,8 +24,7 @@ public void Scan_FindsAllJavaPeerTypes () [InlineData ("my/app/MainActivity", false)] public void Scan_DoNotGenerateAcw (string javaName, bool expected) { - var peers = ScanFixtures (); - Assert.Equal (expected, FindByJavaName (peers, javaName).DoNotGenerateAcw); + Assert.Equal (expected, FindFixtureByJavaName (javaName).DoNotGenerateAcw); } [Theory] @@ -72,19 +40,17 @@ public void Scan_DoNotGenerateAcw (string javaName, bool expected) [InlineData ("android/app/Activity", false)] public void Scan_IsUnconditional (string javaName, bool expected) { - var peers = ScanFixtures (); - Assert.Equal (expected, FindByJavaName (peers, javaName).IsUnconditional); + Assert.Equal (expected, FindFixtureByJavaName (javaName).IsUnconditional); } [Fact] public void Scan_TypeMetadata_IsCorrect () { - var peers = ScanFixtures (); - Assert.True (FindByJavaName (peers, "my/app/AbstractBase").IsAbstract); - Assert.True (FindByManagedName (peers, "Android.Views.IOnClickListener").IsInterface); - Assert.False (FindByManagedName (peers, "Android.Views.IOnClickListener").DoNotGenerateAcw); + Assert.True (FindFixtureByJavaName ("my/app/AbstractBase").IsAbstract); + Assert.True (FindFixtureByManagedName ("Android.Views.IOnClickListener").IsInterface); + Assert.False (FindFixtureByManagedName ("Android.Views.IOnClickListener").DoNotGenerateAcw); - var generic = FindByJavaName (peers, "my/app/GenericHolder"); + var generic = FindFixtureByJavaName ("my/app/GenericHolder"); Assert.True (generic.IsGenericDefinition); Assert.Equal ("MyApp.Generic.GenericHolder`1", generic.ManagedTypeName); } From 09b65ec18d657f75aded5a90265a2dfe3c564a12 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 10 Mar 2026 12:55:43 +0100 Subject: [PATCH 15/40] Simplify PEAssemblyBuilder and fix doc comments - Simplify FindOrAddAssemblyRef to delegate directly to AddAssemblyRef (which already handles the cache check), removing redundant lookup - Remove 30 spurious blank lines inside XML doc comment blocks in TypeMapAssemblyData.cs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/Model/TypeMapAssemblyData.cs | 30 ------------------- .../Generator/PEAssemblyBuilder.cs | 7 +---- 2 files changed, 1 insertion(+), 36 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs index 279d3e15519..625a4508c45 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -16,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 (); } @@ -79,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; } @@ -97,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; @@ -130,9 +110,7 @@ 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; } @@ -150,9 +128,7 @@ sealed record TypeRefData public required string ManagedTypeName { get; init; } /// - /// Assembly containing the type, e.g., "Mono.Android". - /// public required string AssemblyName { get; init; } } @@ -168,16 +144,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; } } @@ -194,9 +166,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 3ad3da53396..ad8d5244850 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs @@ -129,12 +129,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. From c22e8fd0d535cd8b38266e4dce5f1bd37450afc7 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 10 Mar 2026 12:56:51 +0100 Subject: [PATCH 16/40] Optimize GetCrc64PackageName to avoid intermediate strings Replace BitConverter.ToString().Replace().ToLowerInvariant() (3 intermediate string allocations) with a direct char-array hex conversion that produces the final string in a single allocation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Scanner/JavaPeerScanner.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index f8591829cfc..958a71e1cf3 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -518,9 +518,20 @@ static string GetCrc64PackageName (string ns, string assemblyName) var data = System.Text.Encoding.UTF8.GetBytes ($"{ns}:{assemblyName}"); var hash = System.IO.Hashing.Crc64.Hash (data); - return $"crc64{BitConverter.ToString (hash).Replace ("-", "").ToLowerInvariant ()}"; + + // "crc64" prefix (5 chars) + 16 hex chars = 21 chars total, single allocation + var chars = new char [5 + hash.Length * 2]; + chars [0] = 'c'; chars [1] = 'r'; chars [2] = 'c'; chars [3] = '6'; chars [4] = '4'; + for (int i = 0; i < hash.Length; i++) { + chars [5 + i * 2] = ToHexChar (hash [i] >> 4); + chars [5 + i * 2 + 1] = ToHexChar (hash [i] & 0xF); + } + return new string (chars); } + static char ToHexChar (int nibble) + => (char) (nibble < 10 ? '0' + nibble : 'a' + nibble - 10); + static string ExtractNamespace (string fullName) { int lastDot = fullName.LastIndexOf ('.'); From 3dbc59cb83d4ce47d6c22700131722faefa7999a Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 10 Mar 2026 13:08:26 +0100 Subject: [PATCH 17/40] Add missing Implementor and EventDispatcher test fixtures Add View_IOnClickListenerImplementor and View_ClickEventDispatcher types to TestFixtures so the FixtureImplementorsAndDispatchers tests can verify that these helper types are treated as trimmable (not unconditional) by the ModelBuilder name-based heuristic. Fixes 2 previously failing tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TestFixtures/TestTypes.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs index 35987f36f93..820538a5ce4 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs @@ -96,6 +96,18 @@ public interface IOnLongClickListener [Register ("onLongClick", "(Landroid/view/View;)Z", "GetOnLongClick_Landroid_view_View_Handler:Android.Views.IOnLongClickListenerInvoker")] bool OnLongClick (View v); } + + [Register ("android/view/View_IOnClickListenerImplementor")] + public class View_IOnClickListenerImplementor : Java.Lang.Object + { + public View_IOnClickListenerImplementor (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } + + [Register ("android/view/View_ClickEventDispatcher")] + public class View_ClickEventDispatcher : Java.Lang.Object + { + public View_ClickEventDispatcher (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } } namespace Android.Widget From 9365b530644427095d112d94c5bd773219e798bf Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 10 Mar 2026 13:09:14 +0100 Subject: [PATCH 18/40] Remove unused ExportInfo record ExportInfo was defined but never instantiated or consumed anywhere. The [Export] attribute comment in ParseAttributes confirms it is handled separately at method-level scan time. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Scanner/AssemblyIndex.cs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs index 4da7b3d752d..84fdd5c3ac8 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs @@ -277,15 +277,6 @@ sealed record RegisterInfo public bool DoNotGenerateAcw { get; init; } } -/// -/// Parsed [Export] attribute data for a method. -/// -sealed record ExportInfo -{ - public IReadOnlyList? ThrownNames { get; init; } - public string? SuperArgumentsString { get; init; } -} - class TypeAttributeInfo (string attributeName) { public string AttributeName { get; } = attributeName; From 55460dc1b31a7706d666f1588cf2919348849877 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 10 Mar 2026 13:11:08 +0100 Subject: [PATCH 19/40] Avoid double attribute decode in TryGetNameProperty TryGetNameProperty was calling TryGetTypeProperty (which decodes the attribute blob), then calling DecodeAttribute again to check the first constructor argument. Inline the named-argument check so the attribute is decoded only once. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Scanner/AssemblyIndex.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs index 84fdd5c3ac8..3c3106bc274 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs @@ -233,13 +233,12 @@ RegisterInfo ParseRegisterInfo (CustomAttributeValue value) string? TryGetNameProperty (CustomAttribute ca) { - var name = TryGetTypeProperty (ca, "Name"); - if (!string.IsNullOrEmpty (name)) { + var value = DecodeAttribute (ca); + + if (TryGetNamedArgument (value, "Name", out var name) && !string.IsNullOrEmpty (name)) { return name; } - var value = DecodeAttribute (ca); - // Fall back to first constructor argument (e.g., [CustomJniName("...")]) if (value.FixedArguments.Length > 0 && value.FixedArguments [0].Value is string ctorName && !string.IsNullOrEmpty (ctorName)) { return ctorName; From 628ecf470bd49db177b319f48d28b9b643606763 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 10 Mar 2026 13:12:40 +0100 Subject: [PATCH 20/40] Move PE metadata test helpers to FixtureTestBase Move GetTypeRefNames and GetMemberRefNames from TypeMapAssemblyGeneratorTests to FixtureTestBase so they are available to all test classes that work with generated PE metadata. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/FixtureTestBase.cs | 14 ++++++++++++++ .../Generator/TypeMapAssemblyGeneratorTests.cs | 12 ------------ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs index 2caa76fa94e..552bcb9886d 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; using Xunit; namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; @@ -92,4 +94,16 @@ private protected static JavaPeerInfo MakeInterfacePeer ( InvokerTypeName = invokerName, }; } + + private protected static List GetTypeRefNames (MetadataReader reader) => + reader.TypeReferences + .Select (h => reader.GetTypeReference (h)) + .Select (t => reader.GetString (t.Name)) + .ToList (); + + private protected static List GetMemberRefNames (MetadataReader reader) => + Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) + .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i))) + .Select (m => reader.GetString (m.Name)) + .ToList (); } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 120971d5d7b..1ab65843ebe 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -21,18 +21,6 @@ static MemoryStream GenerateAssembly (IReadOnlyList peers, string return stream; } - static List GetMemberRefNames (MetadataReader reader) => - Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) - .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i))) - .Select (m => reader.GetString (m.Name)) - .ToList (); - - static List GetTypeRefNames (MetadataReader reader) => - reader.TypeReferences - .Select (h => reader.GetTypeReference (h)) - .Select (t => reader.GetString (t.Name)) - .ToList (); - [Fact] public void Generate_ProducesValidPEAssembly () { From 6ba786aaf3e020a9e1143399bf2dc1c68537002f Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 10 Mar 2026 13:13:21 +0100 Subject: [PATCH 21/40] Standardize field naming in JavaPeerScanner Rename assemblyCache, activationCtorCache, extendsJavaPeerCache to use _camelCase prefix convention, consistent with PEAssemblyBuilder and the rest of the codebase. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Scanner/JavaPeerScanner.cs | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 958a71e1cf3..6d876295a71 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -15,8 +15,8 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// sealed class JavaPeerScanner : IDisposable { - readonly Dictionary assemblyCache = new (StringComparer.Ordinal); - readonly Dictionary<(string typeName, string assemblyName), ActivationCtorInfo> activationCtorCache = new (); + readonly Dictionary _assemblyCache = new (StringComparer.Ordinal); + readonly Dictionary<(string typeName, string assemblyName), ActivationCtorInfo> _activationCtorCache = new (); /// /// Resolves a type name + assembly name to a TypeDefinitionHandle + AssemblyIndex. @@ -24,7 +24,7 @@ sealed class JavaPeerScanner : IDisposable /// bool TryResolveType (string typeName, string assemblyName, out TypeDefinitionHandle handle, [NotNullWhen (true)] out AssemblyIndex? resolvedIndex) { - if (assemblyCache.TryGetValue (assemblyName, out resolvedIndex) && + if (_assemblyCache.TryGetValue (assemblyName, out resolvedIndex) && resolvedIndex.TypesByFullName.TryGetValue (typeName, out handle)) { return true; } @@ -83,18 +83,18 @@ public List Scan (IReadOnlyList assemblyPaths) // Phase 1: Build indices for all assemblies foreach (var path in assemblyPaths) { var index = AssemblyIndex.Create (path); - assemblyCache [index.AssemblyName] = index; + _assemblyCache [index.AssemblyName] = index; } // Phase 2: Analyze types using cached indices var resultsByManagedName = new Dictionary (StringComparer.Ordinal); - foreach (var index in assemblyCache.Values) { + foreach (var index in _assemblyCache.Values) { ScanAssembly (index, resultsByManagedName); } // Phase 3: Force unconditional on types referenced by [Application] attributes - ForceUnconditionalCrossReferences (resultsByManagedName, assemblyCache); + ForceUnconditionalCrossReferences (resultsByManagedName, _assemblyCache); return new List (resultsByManagedName.Values); } @@ -104,9 +104,9 @@ public List Scan (IReadOnlyList assemblyPaths) /// [Application(ManageSpaceActivity = typeof(X))] must be unconditional, /// because the manifest will reference them even if nothing else does. /// - static void ForceUnconditionalCrossReferences (Dictionary resultsByManagedName, Dictionary assemblyCache) + static void ForceUnconditionalCrossReferences (Dictionary resultsByManagedName, Dictionary _assemblyCache) { - foreach (var index in assemblyCache.Values) { + foreach (var index in _assemblyCache.Values) { foreach (var attrInfo in index.AttributesByType.Values) { if (attrInfo is ApplicationAttributeInfo applicationAttributeInfo) { ForceUnconditionalIfPresent (resultsByManagedName, applicationAttributeInfo.BackupAgent); @@ -230,7 +230,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary results ActivationCtorInfo? ResolveActivationCtor (string typeName, TypeDefinition typeDef, AssemblyIndex index) { var cacheKey = (typeName, index.AssemblyName); - if (activationCtorCache.TryGetValue (cacheKey, out var cached)) { + if (_activationCtorCache.TryGetValue (cacheKey, out var cached)) { return cached; } @@ -238,7 +238,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary results var ownCtor = FindActivationCtorOnType (typeDef, index); if (ownCtor is not null) { var info = new ActivationCtorInfo { DeclaringTypeName = typeName, DeclaringAssemblyName = index.AssemblyName, Style = ownCtor.Value }; - activationCtorCache [cacheKey] = info; + _activationCtorCache [cacheKey] = info; return info; } @@ -250,7 +250,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary results var baseTypeDef = baseIndex.Reader.GetTypeDefinition (baseHandle); var result = ResolveActivationCtor (baseTypeName, baseTypeDef, baseIndex); if (result is not null) { - activationCtorCache [cacheKey] = result; + _activationCtorCache [cacheKey] = result; } return result; } @@ -394,13 +394,13 @@ List ResolveImplementedInterfaces (TypeDefinition typeDef, AssemblyIndex public void Dispose () { - foreach (var index in assemblyCache.Values) { + foreach (var index in _assemblyCache.Values) { index.Dispose (); } - assemblyCache.Clear (); + _assemblyCache.Clear (); } - readonly Dictionary extendsJavaPeerCache = new (StringComparer.Ordinal); + readonly Dictionary _extendsJavaPeerCache = new (StringComparer.Ordinal); /// /// Check if a type extends a known Java peer (has [Register] or component attribute) @@ -411,12 +411,12 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) var fullName = MetadataTypeNameResolver.GetFullName (typeDef, index.Reader); var key = $"{index.AssemblyName}:{fullName}"; - if (extendsJavaPeerCache.TryGetValue (key, out var cached)) { + if (_extendsJavaPeerCache.TryGetValue (key, out var cached)) { return cached; } // Mark as false to prevent cycles, then compute - extendsJavaPeerCache [key] = false; + _extendsJavaPeerCache [key] = false; var baseInfo = GetBaseTypeInfo (typeDef, index); if (baseInfo is null) { @@ -431,18 +431,18 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) // Direct hit: base has [Register] or component attribute if (baseIndex.RegisterInfoByType.ContainsKey (baseHandle)) { - extendsJavaPeerCache [key] = true; + _extendsJavaPeerCache [key] = true; return true; } if (baseIndex.AttributesByType.ContainsKey (baseHandle)) { - extendsJavaPeerCache [key] = true; + _extendsJavaPeerCache [key] = true; return true; } // Recurse up the hierarchy var baseDef = baseIndex.Reader.GetTypeDefinition (baseHandle); var result = ExtendsJavaPeer (baseDef, baseIndex); - extendsJavaPeerCache [key] = result; + _extendsJavaPeerCache [key] = result; return result; } From 7770dd93ba48eddc65c2a5c4390ad45c896a063d Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 10 Mar 2026 13:20:06 +0100 Subject: [PATCH 22/40] Use Path.GetTempPath() instead of hardcoded "/tmp" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes cross-platform compatibility — "/tmp" does not exist on Windows. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/TypeMapModelBuilderTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index b14c8e90c56..da317586823 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -12,7 +12,7 @@ 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); } From 555a844dd11fcefe523a389667c15cf4d80e4875 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 10 Mar 2026 13:20:54 +0100 Subject: [PATCH 23/40] Remove Unix-specific paths from InlineData test cases Use bare filenames instead of "/some/path/..." which is not a valid path on Windows. The test only exercises Path.GetFileNameWithoutExtension so the directory component is unnecessary. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/TypeMapModelBuilderTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index da317586823..4e7abc72e99 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -31,8 +31,8 @@ public void Build_EmptyPeers_ProducesEmptyModel () } [Theory] - [InlineData ("/some/path/Foo.Bar.dll", null, "Foo.Bar")] - [InlineData ("/some/path/Foo.dll", "MyAssembly", "MyAssembly")] + [InlineData ("Foo.Bar.dll", null, "Foo.Bar")] + [InlineData ("Foo.dll", "MyAssembly", "MyAssembly")] public void Build_AssemblyName_ResolvedCorrectly (string outputPath, string? explicitName, string expected) { var model = ModelBuilder.Build (Array.Empty (), outputPath, explicitName); From 6375e5515862364bd2d413d87fbaed589930cd82 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 10 Mar 2026 13:22:37 +0100 Subject: [PATCH 24/40] Remove blank lines between consecutive closing braces Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/Model/TypeMapAssemblyData.cs | 1 - .../Generator/PEAssemblyBuilder.cs | 2 -- .../Generator/TypeMapAssemblyEmitter.cs | 1 - .../Scanner/JavaPeerInfo.cs | 1 - .../TypeMapAssemblyGeneratorTests.cs | 1 - .../Generator/TypeMapModelBuilderTests.cs | 19 ------------------- .../Scanner/JavaPeerScannerTests.EdgeCases.cs | 1 - .../TestFixtures/TestTypes.cs | 4 ---- 8 files changed, 30 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs index 625a4508c45..732be8f0b0d 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -113,7 +113,6 @@ sealed class JavaPeerProxyData /// True if this is an open generic type definition. CreateInstance throws NotSupportedException. /// public bool IsGenericDefinition { get; init; } - } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs index ad8d5244850..f7d16452d31 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) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index e2ccf77dd04..06b7ced97a1 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -400,5 +400,4 @@ void EmitTypeMapAssociationAttribute (TypeMapAssociationData assoc) }); _pe.Metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, _typeMapAssociationAttrCtorRef, blob); } - } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index df883b647f9..f37c0dbb85a 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -34,7 +34,6 @@ sealed record JavaPeerInfo /// Assembly name the type belongs to, e.g., "Mono.Android". /// public required string AssemblyName { get; init; } - public bool IsInterface { get; init; } public bool IsAbstract { get; init; } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 1ab65843ebe..32ca7f4ffa3 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -229,5 +229,4 @@ public void Generate_InheritedCtor_IncludesBaseCtorAssembly () // Activity is in TestFixtures, so IgnoresAccessChecksTo must include TestFixtures Assert.Contains (attrBlobs, b => b.Contains ("TestFixtures")); } - } \ 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 4e7abc72e99..16553a7e843 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -38,7 +38,6 @@ public void Build_AssemblyName_ResolvedCorrectly (string outputPath, string? exp var model = ModelBuilder.Build (Array.Empty (), outputPath, explicitName); Assert.Equal (expected, model.AssemblyName); } - } public class TypeMapEntries @@ -74,7 +73,6 @@ public void Build_DuplicateJniNames_CreatesAliasEntries () Assert.Equal ("test/Dup[1]", model.Entries [1].JniName); Assert.Contains ("Test.Second", model.Entries [1].ProxyTypeReference); } - } public class ConditionalAttributes @@ -134,7 +132,6 @@ public void Build_UnconditionalScannedType_IsUnconditional () Assert.True (model.Entries [0].IsUnconditional); } - } public class Aliases @@ -164,7 +161,6 @@ public void Build_McwPeerWithoutActivation_NoProxy () Assert.Single (model.Entries); Assert.Contains ("Java.Lang.Object, Mono.Android", model.Entries [0].ProxyTypeReference); } - } public class ProxyTypes @@ -198,7 +194,6 @@ public void Build_PeerWithInvoker_CreatesProxy () Assert.NotNull (proxy.InvokerType); Assert.Equal ("Android.Views.View+IOnClickListenerInvoker", proxy.InvokerType!.ManagedTypeName); } - } public class FixtureScan @@ -229,7 +224,6 @@ public void ScanFixtures_ManagedTypeShortName_IsCorrect (string javaName, string var peer = FindFixtureByJavaName (javaName); Assert.Equal (expectedShortName, peer.ManagedTypeShortName); } - } public class FixtureConditionalAttributes @@ -256,7 +250,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) @@ -315,7 +308,6 @@ public void Fixture_Service_NoActivation_NoProxy () Assert.Empty (model.ProxyTypes); } } - } public class FixtureCustomView @@ -330,7 +322,6 @@ public void Fixture_CustomView_HasTwoConstructors () var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_CustomView_Proxy"); Assert.NotNull (proxy); } - } public class FixtureInterfaces @@ -351,7 +342,6 @@ public void Fixture_IOnClickListener_HasInvokerProxy () Assert.NotNull (proxy!.InvokerType); Assert.Equal ("Android.Views.IOnClickListenerInvoker", proxy.InvokerType!.ManagedTypeName); } - } public class FixtureNestedTypes @@ -374,7 +364,6 @@ public void Fixture_NestedType_ProxyNaming (string javaName, string expectedProx Assert.Equal (expectedManagedName, proxy!.TargetType.ManagedTypeName); } } - } public class FixtureInvokers @@ -426,7 +415,6 @@ public void Build_InvokerType_NoProxyNoEntry () // Interface proxy has activation because it will create the invoker Assert.True (proxy.HasActivation); } - } public class FixtureGenericHolder @@ -442,7 +430,6 @@ public void Fixture_GenericHolder_Entry () var entry = FindEntry (model, "my/app/GenericHolder"); Assert.NotNull (entry); } - } public class FixtureAcwTypeHasProxy @@ -465,7 +452,6 @@ public void Fixture_AcwType_HasProxy (string javaName, string expectedProxyName) Assert.NotNull (proxy); } } - } public class FixtureImplementorsAndDispatchers @@ -487,7 +473,6 @@ public void Fixture_HelperType_IsTrimmable_NotUnconditional (string javaName, st Assert.False (entry!.IsUnconditional, $"{kind} should NOT be unconditional"); Assert.NotNull (entry.TargetTypeReference); } - } public class NameBasedDetection @@ -527,7 +512,6 @@ public void Build_TypeIsInvoker_OnlyWhenReferencedByAnotherPeer () Assert.Single (model2.Entries); Assert.Equal ("MyApp.IMyInterface", model2.ProxyTypes [0].TargetType.ManagedTypeName); } - } public class PipelineTests @@ -608,7 +592,6 @@ public void FullPipeline_GenericHolder_ProducesValidAssembly () Assert.NotEmpty (asmAttrs); }); } - } public class PeBlobValidation @@ -679,7 +662,6 @@ public void FullPipeline_McwBinding_Emits3ArgAttribute () Assert.Contains ("Android.App.Activity", targetRef!); }); } - } public class DeterminismTests @@ -700,7 +682,6 @@ public void Build_SameInput_ProducesDeterministicOutput () Assert.Equal (model1.Entries [i].TargetTypeReference, model2.Entries [i].TargetTypeReference); } } - } static void EmitAndVerify (TypeMapAssemblyData model, string assemblyName, Action verify) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs index 6f1ceeed19e..916431945f5 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs @@ -47,5 +47,4 @@ public void Scan_UnregisteredType_DiscoveredWithCrc64Name (string managedName) { Assert.StartsWith ("crc64", FindFixtureByManagedName (managedName).JavaName); } - } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs index 820538a5ce4..c0cf823db81 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs @@ -251,7 +251,6 @@ public class MyManageSpaceActivity : Android.App.Activity { protected MyManageSpaceActivity (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } } - public class UnregisteredHelper : Java.Lang.Object { } [Register ("my/app/MyButton")] @@ -277,7 +276,6 @@ public class CustomWidget : Java.Lang.Object { } [Activity (Name = "my.app.BaseActivityNoRegister")] public class BaseActivityNoRegister : Android.App.Activity { } - public class DerivedFromComponentBase : BaseActivityNoRegister { } [Register ("my/app/RegisteredParent")] @@ -294,7 +292,6 @@ public class Middle : Java.Lang.Object public class DeepInner : Java.Lang.Object { } } } - public class PlainActivitySubclass : Android.App.Activity { } [Activity (Label = "Unnamed")] @@ -349,5 +346,4 @@ public class GlobalType : Java.Lang.Object { protected GlobalType (IntPtr handle, Android.Runtime.JniHandleOwnership transfer) : base (handle, transfer) { } } - public class GlobalUnregisteredType : Java.Lang.Object { } From 3db165868be43cf177cab294c6f70853ed7b5fda Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 10 Mar 2026 13:23:17 +0100 Subject: [PATCH 25/40] Remove extra blank lines in TrimmableTypeMap files Remove blank lines after opening braces, before closing braces, and double blank lines. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/Model/TypeMapAssemblyData.cs | 1 - .../Scanner/AssemblyIndex.cs | 1 - .../Generator/TypeMapModelBuilderTests.cs | 21 ------------------- 3 files changed, 23 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs index 732be8f0b0d..701f9cd4f2c 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -115,7 +115,6 @@ sealed class JavaPeerProxyData public bool IsGenericDefinition { get; init; } } - /// /// A cross-assembly type reference (assembly name + full managed type name). /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs index 3c3106bc274..548846e659b 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs @@ -194,7 +194,6 @@ internal CustomAttributeValue DecodeAttribute (CustomAttribute ca) RegisterInfo ParseRegisterInfo (CustomAttributeValue value) { - string jniName = ""; string? signature = null; string? connector = null; diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 16553a7e843..2e48ab83069 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -16,10 +16,8 @@ static TypeMapAssemblyData BuildModel (IReadOnlyList peers, string return ModelBuilder.Build (peers, outputPath, assemblyName); } - public class BasicStructure { - [Fact] public void Build_EmptyPeers_ProducesEmptyModel () { @@ -42,7 +40,6 @@ public void Build_AssemblyName_ResolvedCorrectly (string outputPath, string? exp public class TypeMapEntries { - [Fact] public void Build_CreatesOneEntryPerPeer () { @@ -77,7 +74,6 @@ public void Build_DuplicateJniNames_CreatesAliasEntries () public class ConditionalAttributes { - [Theory] [InlineData ("java/lang/Object")] [InlineData ("java/lang/Throwable")] @@ -136,7 +132,6 @@ public void Build_UnconditionalScannedType_IsUnconditional () public class Aliases { - [Fact] public void Build_AliasedPeersWithActivation_GetDistinctProxies () { @@ -165,7 +160,6 @@ public void Build_McwPeerWithoutActivation_NoProxy () 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")] @@ -198,7 +192,6 @@ public void Build_PeerWithInvoker_CreatesProxy () public class FixtureScan { - [Fact] public void Build_FromScannedFixtures_ProducesValidModel () { @@ -228,7 +221,6 @@ public void ScanFixtures_ManagedTypeShortName_IsCorrect (string javaName, string public class FixtureConditionalAttributes { - [Theory] [InlineData ("my/app/MainActivity")] [InlineData ("my/app/TouchHandler")] @@ -262,10 +254,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")] @@ -312,7 +302,6 @@ public void Fixture_Service_NoActivation_NoProxy () public class FixtureCustomView { - [Fact] public void Fixture_CustomView_HasTwoConstructors () { @@ -326,7 +315,6 @@ public void Fixture_CustomView_HasTwoConstructors () public class FixtureInterfaces { - [Fact] public void Fixture_IOnClickListener_HasInvokerProxy () { @@ -346,7 +334,6 @@ public void Fixture_IOnClickListener_HasInvokerProxy () 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")] @@ -368,7 +355,6 @@ public void Fixture_NestedType_ProxyNaming (string javaName, string expectedProx public class FixtureInvokers { - [Fact] public void Fixture_InterfaceAndInvoker_ShareJniName_InvokerSeparated () { @@ -419,7 +405,6 @@ public void Build_InvokerType_NoProxyNoEntry () public class FixtureGenericHolder { - [Fact] public void Fixture_GenericHolder_Entry () { @@ -434,7 +419,6 @@ public void Fixture_GenericHolder_Entry () public class FixtureAcwTypeHasProxy { - [Theory] [InlineData ("my/app/AbstractBase", "MyApp_AbstractBase_Proxy")] [InlineData ("my/app/ClickableView", "MyApp_ClickableView_Proxy")] @@ -456,7 +440,6 @@ public void Fixture_AcwType_HasProxy (string javaName, string expectedProxyName) public class FixtureImplementorsAndDispatchers { - [Theory] [InlineData ("android/view/View_IOnClickListenerImplementor", "Implementor")] [InlineData ("android/view/View_ClickEventDispatcher", "EventDispatcher")] @@ -477,7 +460,6 @@ public void Fixture_HelperType_IsTrimmable_NotUnconditional (string javaName, st public class NameBasedDetection { - [Fact] public void Build_UserTypeNamedImplementor_IsTreatedAsTrimmable () { @@ -516,7 +498,6 @@ public void Build_TypeIsInvoker_OnlyWhenReferencedByAnotherPeer () public class PipelineTests { - [Fact] public void FullPipeline_AllFixtures_ProducesLoadableAssembly () { @@ -596,7 +577,6 @@ public void FullPipeline_GenericHolder_ProducesValidAssembly () public class PeBlobValidation { - [Fact] public void FullPipeline_Mixed2ArgAnd3Arg_BothSurviveRoundTrip () { @@ -666,7 +646,6 @@ public void FullPipeline_McwBinding_Emits3ArgAttribute () public class DeterminismTests { - [Fact] public void Build_SameInput_ProducesDeterministicOutput () { From 46e83f7e861a7737872f21deeb01d8720c35dbd9 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 10 Mar 2026 13:53:08 +0100 Subject: [PATCH 26/40] Handle JavaInterop-style activation ctors in emitter The scanner detects both XamarinAndroid (IntPtr, JniHandleOwnership) and JavaInterop (ref JniObjectReference, JniObjectReferenceOptions) activation constructor styles. The emitter previously hardcoded the XA-style signature, which would produce invalid IL for JI-style types. For now, JI-style types fall back to no-activation (CreateInstance returns null), letting the runtime use reflection-based activation. Full JI-style IL emission requires parameter conversion logic that can be added when needed. Add JI stub types (JniObjectReference, JniObjectReferenceOptions) to test fixtures and a JiStylePeer test type with a test verifying the emitter handles it gracefully. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/TypeMapAssemblyEmitter.cs | 8 ++++++ .../TypeMapAssemblyGeneratorTests.cs | 27 +++++++++++++++++++ .../TestFixtures/StubAttributes.cs | 15 +++++++++++ .../TestFixtures/TestTypes.cs | 7 +++++ 4 files changed, 57 insertions(+) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 06b7ced97a1..576ccef98a6 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -273,6 +273,14 @@ void EmitCreateInstance (JavaPeerProxyData proxy) return; } + // JavaInterop-style activation ctors (ref JniObjectReference, JniObjectReferenceOptions) + // require parameter conversion that is not yet implemented in the emitter. + // Fall back to no-activation (runtime will use reflection-based activation). + if (proxy.ActivationCtor?.Style == ActivationCtorStyle.JavaInterop) { + EmitCreateInstanceNoActivation (); + return; + } + if (proxy.InvokerType != null) { EmitCreateInstanceViaNewobj (_pe.ResolveTypeRef (proxy.InvokerType)); return; diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 32ca7f4ffa3..edeaa38d5c2 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -229,4 +229,31 @@ public void Generate_InheritedCtor_IncludesBaseCtorAssembly () // Activity is in TestFixtures, so IgnoresAccessChecksTo must include TestFixtures Assert.Contains (attrBlobs, b => b.Contains ("TestFixtures")); } + + [Fact] + public void Generate_JiStyleCtor_DoesNotEmitActivation () + { + 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 is not yet supported in the emitter, so + // CreateInstance should be the no-activation variant (ldnull + ret). + // Verify the proxy exists but no JI-specific type refs are emitted. + var typeNames = GetTypeRefNames (reader); + Assert.DoesNotContain ("JniObjectReference", typeNames); + Assert.DoesNotContain ("JniObjectReferenceOptions", typeNames); + + // The proxy still exists (with a TargetType property) — only activation is skipped + var proxyTypes = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .Where (t => reader.GetString (t.Namespace) == "_TypeMap.Proxies") + .ToList (); + Assert.Single (proxyTypes); + } } \ No newline at end of file diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs index 36c7587eb28..4a9ebb1d079 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs @@ -40,6 +40,21 @@ public enum JniHandleOwnership } } +namespace Java.Interop +{ + public struct JniObjectReference + { + public IntPtr Handle; + } + + public enum JniObjectReferenceOptions + { + None = 0, + Copy = 1, + CopyAndDispose = 2, + } +} + namespace Android.App { [AttributeUsage (AttributeTargets.Class)] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs index c0cf823db81..57caee48ceb 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs @@ -339,6 +339,13 @@ public class GenericCallbackImpl : Java.Lang.Object, IGenericCallback { protected GenericCallbackImpl (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } } + + [Register ("my/app/JiStylePeer", DoNotGenerateAcw = true)] + public class JiStylePeer : Java.Lang.Object + { + protected JiStylePeer (ref Java.Interop.JniObjectReference reference, Java.Interop.JniObjectReferenceOptions options) + : base ((IntPtr)0, JniHandleOwnership.DoNotTransfer) { } + } } [Register ("my/app/GlobalType")] From f831588caa3f08d9fc97e59699ae4c78d9f16fea Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 10 Mar 2026 13:56:05 +0100 Subject: [PATCH 27/40] Fix file handle leak in AssemblyIndex.Create Dispose PEReader (which owns the file stream) if GetMetadataReader() or Build() throws an exception during index creation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Scanner/AssemblyIndex.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs index 548846e659b..ca5063ff019 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs @@ -47,11 +47,16 @@ sealed class AssemblyIndex : IDisposable public static AssemblyIndex Create (string filePath) { var peReader = new PEReader (File.OpenRead (filePath)); - var reader = peReader.GetMetadataReader (); - var assemblyName = reader.GetString (reader.GetAssemblyDefinition ().Name); - var index = new AssemblyIndex (peReader, reader, assemblyName, filePath); - index.Build (); - return index; + try { + var reader = peReader.GetMetadataReader (); + var assemblyName = reader.GetString (reader.GetAssemblyDefinition ().Name); + var index = new AssemblyIndex (peReader, reader, assemblyName, filePath); + index.Build (); + return index; + } catch { + peReader.Dispose (); + throw; + } } void Build () From 6ef8dc25e4da2fd47ee43fd75fbb675a867b37ec Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 10 Mar 2026 13:56:15 +0100 Subject: [PATCH 28/40] Improve JavaPeerScanner: tuple cache key, single-use guard - Replace string interpolation cache key in ExtendsJavaPeer with (string, string) tuple to avoid allocation per recursive call - Add single-use guard: throw if Scan() is called more than once, making the stateful contract explicit Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Scanner/JavaPeerScanner.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 6d876295a71..86cb2340a37 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -17,6 +17,7 @@ sealed class JavaPeerScanner : IDisposable { readonly Dictionary _assemblyCache = new (StringComparer.Ordinal); readonly Dictionary<(string typeName, string assemblyName), ActivationCtorInfo> _activationCtorCache = new (); + bool _hasScanned; /// /// Resolves a type name + assembly name to a TypeDefinitionHandle + AssemblyIndex. @@ -80,6 +81,11 @@ bool TryResolveType (string typeName, string assemblyName, out TypeDefinitionHan /// public List Scan (IReadOnlyList assemblyPaths) { + if (_hasScanned) { + throw new InvalidOperationException ("Scan() can only be called once per JavaPeerScanner instance. Create a new instance for each scan."); + } + _hasScanned = true; + // Phase 1: Build indices for all assemblies foreach (var path in assemblyPaths) { var index = AssemblyIndex.Create (path); @@ -400,7 +406,7 @@ public void Dispose () _assemblyCache.Clear (); } - readonly Dictionary _extendsJavaPeerCache = new (StringComparer.Ordinal); + readonly Dictionary<(string assemblyName, string fullName), bool> _extendsJavaPeerCache = new (); /// /// Check if a type extends a known Java peer (has [Register] or component attribute) @@ -409,7 +415,7 @@ public void Dispose () bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) { var fullName = MetadataTypeNameResolver.GetFullName (typeDef, index.Reader); - var key = $"{index.AssemblyName}:{fullName}"; + var key = (index.AssemblyName, fullName); if (_extendsJavaPeerCache.TryGetValue (key, out var cached)) { return cached; From b5e7fefd409ccb8526418e23de7960e1c0017698 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 10 Mar 2026 13:56:23 +0100 Subject: [PATCH 29/40] Guard against proxy type name collisions in ModelBuilder Types like My.Type and My_Type would both produce My_Type_Proxy after character replacement. Track used names and append a numeric suffix if a collision is detected. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/ModelBuilder.cs | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index 70620b72684..63d517e8626 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -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 @@ -93,7 +95,7 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri } 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]", ... @@ -106,7 +108,7 @@ static void EmitPeers (TypeMapAssemblyData model, string jniName, JavaPeerProxyData? proxy = null; if (hasProxy) { - proxy = BuildProxyType (peer); + proxy = BuildProxyType (peer, usedProxyNames); model.ProxyTypes.Add (proxy); } @@ -178,12 +180,23 @@ static void AddIfCrossAssembly (SortedSet set, string? asmName, string o } } - static JavaPeerProxyData BuildProxyType (JavaPeerInfo peer) + static JavaPeerProxyData BuildProxyType (JavaPeerInfo peer, HashSet usedProxyNames) { // 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 { From a5296b129e270961a280f1863c65009c52417370 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 10 Mar 2026 13:56:31 +0100 Subject: [PATCH 30/40] Assert empty associations for no-proxy alias test Document the intentional behavior that TypeMapAssociation entries are not emitted when neither peer in an alias group has a proxy. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/TypeMapModelBuilderTests.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 2e48ab83069..6dd92627fc6 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -69,6 +69,9 @@ 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); } } From 4714c19367967ec5c17eb6618634bf1b34e3e985 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 10 Mar 2026 15:27:20 +0100 Subject: [PATCH 31/40] Fix attribute count assertion and EmitBody scratch blob reentrance - Fix FullPipeline_AllFixtures_TypeMapAttributeCountMatchesEntries to include model.Associations.Count in the expected total. The old formula was accidentally correct (zero associations in fixtures). Add a dedicated alias-group test that exercises the corrected count. - Fix EmitBody to capture the sig blob handle before running the emitIL callback, preventing signature corruption if the callback calls AddMemberRef (which reuses the same scratch BlobBuilder). Add regression test that triggers the reentrance scenario. - Add Emit_CalledTwice_Throws test documenting that MetadataBuilder already prevents double Emit on the same emitter instance. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/PEAssemblyBuilder.cs | 5 +- .../TypeMapAssemblyGeneratorTests.cs | 63 +++++++++++++++++++ .../Generator/TypeMapModelBuilderTests.cs | 21 ++++++- 3 files changed, 87 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs index f7d16452d31..15ce5388e4b 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs @@ -175,6 +175,9 @@ public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, { _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); _codeBlob.Clear (); var encoder = new InstructionEncoder (_codeBlob); @@ -189,7 +192,7 @@ public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, return Metadata.AddMethodDefinition ( attrs, MethodImplAttributes.IL, Metadata.GetOrAddString (name), - Metadata.GetOrAddBlob (_sigBlob), + sigBlobHandle, bodyOffset, default); } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index edeaa38d5c2..70b2a2bbf4b 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -256,4 +256,67 @@ public void Generate_JiStyleCtor_DoesNotEmitActivation () .ToList (); Assert.Single (proxyTypes); } + + [Fact] + public void Emit_CalledTwice_Throws () + { + var model = ModelBuilder.Build (Array.Empty (), "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); + Assert.Equal (1, sig.ParameterTypes.Length); + Assert.Equal ("System.Int32", sig.ParameterTypes [0]); + } } \ 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 6dd92627fc6..0606b61b85d 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -535,7 +535,26 @@ 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_AliasGroup_TypeMapAttributeCountIncludesAssociations () + { + // 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); + + 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); }); } From 192785e6e7361569df5c8d2558e85e73e7ddc60f Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 10 Mar 2026 16:02:21 +0100 Subject: [PATCH 32/40] Fix convention violations: remove null-forgiving operator and Array.Empty() - Replace null-forgiving ! with OfType() in ModelBuilder.cs - Replace null-forgiving ! with explicit null check in TypeMapAssemblyEmitter.cs - Replace null-forgiving ! with ?? throw in FixtureTestBase.cs - Replace Array.Empty() with [] in 5 test file locations Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/ModelBuilder.cs | 2 +- .../Generator/TypeMapAssemblyEmitter.cs | 5 ++++- .../Generator/FixtureTestBase.cs | 3 ++- .../Generator/RootTypeMapAssemblyGeneratorTests.cs | 2 +- .../Generator/TypeMapAssemblyGeneratorTests.cs | 4 ++-- .../Generator/TypeMapModelBuilderTests.cs | 4 ++-- 6 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index 63d517e8626..3b32b3002dc 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). diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 576ccef98a6..ffc29f8f474 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -394,7 +394,10 @@ void EmitTypeMapAttribute (TypeMapAttributeData entry) b.WriteSerializedString (entry.JniName); b.WriteSerializedString (entry.ProxyTypeReference); if (!entry.IsUnconditional) { - b.WriteSerializedString (entry.TargetTypeReference!); + 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); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs index 552bcb9886d..0a29a1b9231 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs @@ -12,7 +12,8 @@ public abstract class FixtureTestBase { static string TestFixtureAssemblyPath { get { - var testAssemblyDir = Path.GetDirectoryName (typeof (FixtureTestBase).Assembly.Location)!; + var testAssemblyDir = Path.GetDirectoryName (typeof (FixtureTestBase).Assembly.Location) + ?? throw new InvalidOperationException ("Cannot determine test assembly directory"); var fixtureAssembly = Path.Combine (testAssemblyDir, "TestFixtures.dll"); Assert.True (File.Exists (fixtureAssembly), $"TestFixtures.dll not found at {fixtureAssembly}. Ensure the TestFixtures project builds."); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs index 52a2a894bcd..088ccdab8d9 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs @@ -32,7 +32,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 (); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 70b2a2bbf4b..9b2090fcf5c 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -138,7 +138,7 @@ public void Generate_DuplicateJniNames_CreatesAliasEntriesAndAssociationAttribut [Fact] public void Generate_EmptyPeerList_ProducesValidAssembly () { - using var stream = GenerateAssembly (Array.Empty (), "EmptyTest"); + using var stream = GenerateAssembly ([], "EmptyTest"); using var pe = new PEReader (stream); var reader = pe.GetMetadataReader (); Assert.NotNull (reader); @@ -260,7 +260,7 @@ public void Generate_JiStyleCtor_DoesNotEmitActivation () [Fact] public void Emit_CalledTwice_Throws () { - var model = ModelBuilder.Build (Array.Empty (), "Double.dll", "Double"); + 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) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 0606b61b85d..9308b6cc41d 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -21,7 +21,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 () [InlineData ("Foo.dll", "MyAssembly", "MyAssembly")] public void Build_AssemblyName_ResolvedCorrectly (string outputPath, string? explicitName, string expected) { - var model = ModelBuilder.Build (Array.Empty (), outputPath, explicitName); + var model = ModelBuilder.Build ([], outputPath, explicitName); Assert.Equal (expected, model.AssemblyName); } } From 9b954d02e2323c56d3c8be8b62ab9c4840db3f6c Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 10 Mar 2026 16:30:10 +0100 Subject: [PATCH 33/40] Fix known limitations: JI-style activation ctors and Implementor detection JavaInterop-style activation constructors: - Add EmitBody overload with local variable support to PEAssemblyBuilder - Add JniObjectReference/JniObjectReferenceOptions type and member refs - Emit IL that converts (IntPtr, JniHandleOwnership) to (ref JniObjectReference, JniObjectReferenceOptions) for both leaf and inherited ctor cases Implementor/EventDispatcher detection: - Move detection from name-based heuristic in ModelBuilder to scanner - Add IsImplementorOrEventDispatcher flag to JavaPeerInfo - Use JNI name prefix 'mono/' + managed name suffix to avoid misclassifying user types ending in 'Implementor' - Update test fixtures to use realistic 'mono/' JNI name prefix Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/ModelBuilder.cs | 16 +-- .../Generator/PEAssemblyBuilder.cs | 24 +++- .../Generator/TypeMapAssemblyEmitter.cs | 126 +++++++++++++++++- .../Scanner/JavaPeerInfo.cs | 8 ++ .../Scanner/JavaPeerScanner.cs | 15 +++ .../TypeMapAssemblyGeneratorTests.cs | 12 +- .../Generator/TypeMapModelBuilderTests.cs | 19 ++- .../TestFixtures/TestTypes.cs | 4 +- 8 files changed, 186 insertions(+), 38 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index 3b32b3002dc..6aea0583c1c 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -141,7 +141,7 @@ static bool IsUnconditionalEntry (JavaPeerInfo peer) // 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)) { + if (peer.IsImplementorOrEventDispatcher) { return false; } @@ -159,20 +159,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/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs index 15ce5388e4b..28fb675c2a3 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs @@ -172,6 +172,19 @@ 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)); @@ -179,6 +192,13 @@ public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, // 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); emitIL (encoder); @@ -187,7 +207,9 @@ 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, diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index ffc29f8f474..62976156dc0 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -52,6 +52,8 @@ sealed class TypeMapAssemblyEmitter TypeReferenceHandle _javaPeerProxyRef; TypeReferenceHandle _iJavaPeerableRef; TypeReferenceHandle _jniHandleOwnershipRef; + TypeReferenceHandle _jniObjectReferenceRef; + TypeReferenceHandle _jniObjectReferenceOptionsRef; TypeReferenceHandle _systemTypeRef; TypeReferenceHandle _runtimeTypeHandleRef; TypeReferenceHandle _notSupportedExceptionRef; @@ -61,6 +63,7 @@ sealed class TypeMapAssemblyEmitter MemberReferenceHandle _getTypeFromHandleRef; MemberReferenceHandle _getUninitializedObjectRef; MemberReferenceHandle _notSupportedExceptionCtorRef; + MemberReferenceHandle _jniObjectReferenceCtorRef; MemberReferenceHandle _typeMapAttrCtorRef2Arg; MemberReferenceHandle _typeMapAttrCtorRef3Arg; MemberReferenceHandle _typeMapAssociationAttrCtorRef; @@ -143,6 +146,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")); _systemTypeRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, metadata.GetOrAddString ("System"), metadata.GetOrAddString ("Type")); _runtimeTypeHandleRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, @@ -173,6 +180,11 @@ 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 ())); + EmitTypeMapAttributeCtorRef (); EmitTypeMapAssociationAttributeCtorRef (); } @@ -274,10 +286,19 @@ void EmitCreateInstance (JavaPeerProxyData proxy) } // JavaInterop-style activation ctors (ref JniObjectReference, JniObjectReferenceOptions) - // require parameter conversion that is not yet implemented in the emitter. - // Fall back to no-activation (runtime will use reflection-based activation). + // require parameter conversion from (IntPtr, JniHandleOwnership). if (proxy.ActivationCtor?.Style == ActivationCtorStyle.JavaInterop) { - EmitCreateInstanceNoActivation (); + 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); + } + } return; } @@ -347,6 +368,91 @@ void EmitCreateInstanceInheritedCtor (EntityHandle targetTypeRef, ActivationCtor }); } + /// + /// Emits CreateInstance for JavaInterop-style activation (leaf type): + /// var jniRef = new JniObjectReference(handle); + /// return new TargetType(ref jniRef, JniObjectReferenceOptions.Copy); + /// + void EmitCreateInstanceViaJavaInteropNewobj (EntityHandle typeRef) + { + var ctorRef = AddJavaInteropActivationCtorRef (typeRef); + EmitCreateInstanceBodyWithLocals ( + EncodeJniObjectReferenceLocal, + encoder => { + // var jniRef = new JniObjectReference(handle); + encoder.LoadLocalAddress (0); + encoder.OpCode (ILOpCode.Ldarg_1); // handle + encoder.Call (_jniObjectReferenceCtorRef); + + // return new TargetType(ref jniRef, JniObjectReferenceOptions.Copy); + encoder.LoadLocalAddress (0); + encoder.LoadConstantI4 (1); // JniObjectReferenceOptions.Copy + encoder.OpCode (ILOpCode.Newobj); + encoder.Token (ctorRef); + 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); + /// 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); + + 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)); + } + + 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 ().Type (_jniObjectReferenceRef, true); + // JniObjectReferenceOptions — encoded as valuetype (enum) + p.AddParameter ().Type ().Type (_jniObjectReferenceOptionsRef, true); + })); + } + void EmitCreateInstanceBody (Action emitIL) { _pe.EmitBody ("CreateInstance", @@ -360,6 +466,20 @@ 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", diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index f37c0dbb85a..6252d73f2c3 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -77,6 +77,14 @@ sealed record JavaPeerInfo /// Generic types get TypeMap entries but CreateInstance throws NotSupportedException. /// public bool IsGenericDefinition { get; init; } + + /// + /// True if this type is a binding-generated Implementor or EventDispatcher. + /// These types are only instantiated from .NET and should be trimmable. + /// Detected by checking for the "mono/" JNI name prefix (used by the binding generator) + /// combined with the "Implementor" or "EventDispatcher" managed name suffix. + /// + public bool IsImplementorOrEventDispatcher { get; init; } } /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 86cb2340a37..4c261adf955 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -227,6 +227,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary results IsGenericDefinition = isGenericDefinition, BaseJavaName = baseJavaName, ImplementedInterfaceJavaNames = implementedInterfaces, + IsImplementorOrEventDispatcher = DetectImplementorOrEventDispatcher (jniName, fullName), }; results [fullName] = peer; @@ -553,6 +554,20 @@ static string ExtractShortName (string fullName) return (lastPlus >= 0 ? typePart.Slice (lastPlus + 1) : typePart).ToString (); } + /// + /// Detects binding-generated Implementor and EventDispatcher types. + /// These are generated by the Java.Interop binding generator and always have: + /// - JNI name starting with "mono/" (binding generator convention) + /// - Managed name ending with "Implementor" or "EventDispatcher" + /// Requiring both conditions avoids misclassifying user types. + /// + static bool DetectImplementorOrEventDispatcher (string jniName, string managedTypeName) + { + return jniName.StartsWith ("mono/", StringComparison.Ordinal) && + (managedTypeName.EndsWith ("Implementor", StringComparison.Ordinal) || + managedTypeName.EndsWith ("EventDispatcher", StringComparison.Ordinal)); + } + /// /// Strips assembly qualification from a type name like "Ns.Type, Assembly, Version=..." /// returning just the type name, or null if the input has no comma. diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 9b2090fcf5c..c6d4bf121c2 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -231,7 +231,7 @@ public void Generate_InheritedCtor_IncludesBaseCtorAssembly () } [Fact] - public void Generate_JiStyleCtor_DoesNotEmitActivation () + public void Generate_JiStyleCtor_EmitsJavaInteropActivation () { var peers = ScanFixtures (); var jiPeer = peers.First (p => p.JavaName == "my/app/JiStylePeer"); @@ -242,14 +242,12 @@ public void Generate_JiStyleCtor_DoesNotEmitActivation () using var pe = new PEReader (stream); var reader = pe.GetMetadataReader (); - // JI-style activation is not yet supported in the emitter, so - // CreateInstance should be the no-activation variant (ldnull + ret). - // Verify the proxy exists but no JI-specific type refs are emitted. + // JI-style activation should emit JniObjectReference and JniObjectReferenceOptions type refs var typeNames = GetTypeRefNames (reader); - Assert.DoesNotContain ("JniObjectReference", typeNames); - Assert.DoesNotContain ("JniObjectReferenceOptions", typeNames); + Assert.Contains ("JniObjectReference", typeNames); + Assert.Contains ("JniObjectReferenceOptions", typeNames); - // The proxy still exists (with a TargetType property) — only activation is skipped + // 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") diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 9308b6cc41d..4caf3029ab3 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -444,13 +444,14 @@ public void Fixture_AcwType_HasProxy (string javaName, string expectedProxyName) public class FixtureImplementorsAndDispatchers { [Theory] - [InlineData ("android/view/View_IOnClickListenerImplementor", "Implementor")] - [InlineData ("android/view/View_ClickEventDispatcher", "EventDispatcher")] + [InlineData ("mono/android/view/View_IOnClickListenerImplementor", "Implementor")] + [InlineData ("mono/android/view/View_ClickEventDispatcher", "EventDispatcher")] public void Fixture_HelperType_IsTrimmable_NotUnconditional (string javaName, string kind) { var peer = FindFixtureByJavaName (javaName); Assert.False (peer.DoNotGenerateAcw); Assert.False (peer.IsInterface); + Assert.True (peer.IsImplementorOrEventDispatcher, $"{kind} should be detected by scanner"); var model = BuildModel (new [] { peer }, "TypeMap"); @@ -461,22 +462,20 @@ public void Fixture_HelperType_IsTrimmable_NotUnconditional (string javaName, st } } - public class NameBasedDetection + public class ImplementorDetection { [Fact] - public void Build_UserTypeNamedImplementor_IsTreatedAsTrimmable () + public void Build_UserTypeNamedImplementor_IsNotMisclassified () { - // 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. + // User ACW types ending in "Implementor" should NOT be treated as trimmable + // because their JNI name doesn't start with "mono/" (binding generator convention). 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"); + Assert.True (entry!.IsUnconditional, + "User types ending in 'Implementor' should be unconditional (not misclassified as trimmable)"); } [Fact] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs index 57caee48ceb..d516be0de5b 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs @@ -97,13 +97,13 @@ public interface IOnLongClickListener bool OnLongClick (View v); } - [Register ("android/view/View_IOnClickListenerImplementor")] + [Register ("mono/android/view/View_IOnClickListenerImplementor")] public class View_IOnClickListenerImplementor : Java.Lang.Object { public View_IOnClickListenerImplementor (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } } - [Register ("android/view/View_ClickEventDispatcher")] + [Register ("mono/android/view/View_ClickEventDispatcher")] public class View_ClickEventDispatcher : Java.Lang.Object { public View_ClickEventDispatcher (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } From e846cc20d664880c486c40c8d879cfeee79b6e42 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 10 Mar 2026 17:24:58 +0100 Subject: [PATCH 34/40] Remove Implementor/EventDispatcher trimming optimization Remove the IsImplementorOrEventDispatcher detection from scanner and ModelBuilder. These types will be treated as unconditional ACW types for now (same behavior as the legacy type map). A dedicated follow-up issue will explore proper trimming of Implementor and EventDispatcher types with appropriate analysis and testing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/ModelBuilder.cs | 6 ----- .../Scanner/JavaPeerInfo.cs | 8 ------- .../Scanner/JavaPeerScanner.cs | 15 ------------ .../Generator/TypeMapModelBuilderTests.cs | 24 ++++--------------- 4 files changed, 5 insertions(+), 48 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index 6aea0583c1c..4181252fd54 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -139,12 +139,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 (peer.IsImplementorOrEventDispatcher) { - 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) { diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index 6252d73f2c3..f37c0dbb85a 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -77,14 +77,6 @@ sealed record JavaPeerInfo /// Generic types get TypeMap entries but CreateInstance throws NotSupportedException. /// public bool IsGenericDefinition { get; init; } - - /// - /// True if this type is a binding-generated Implementor or EventDispatcher. - /// These types are only instantiated from .NET and should be trimmable. - /// Detected by checking for the "mono/" JNI name prefix (used by the binding generator) - /// combined with the "Implementor" or "EventDispatcher" managed name suffix. - /// - public bool IsImplementorOrEventDispatcher { get; init; } } /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 4c261adf955..86cb2340a37 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -227,7 +227,6 @@ void ScanAssembly (AssemblyIndex index, Dictionary results IsGenericDefinition = isGenericDefinition, BaseJavaName = baseJavaName, ImplementedInterfaceJavaNames = implementedInterfaces, - IsImplementorOrEventDispatcher = DetectImplementorOrEventDispatcher (jniName, fullName), }; results [fullName] = peer; @@ -554,20 +553,6 @@ static string ExtractShortName (string fullName) return (lastPlus >= 0 ? typePart.Slice (lastPlus + 1) : typePart).ToString (); } - /// - /// Detects binding-generated Implementor and EventDispatcher types. - /// These are generated by the Java.Interop binding generator and always have: - /// - JNI name starting with "mono/" (binding generator convention) - /// - Managed name ending with "Implementor" or "EventDispatcher" - /// Requiring both conditions avoids misclassifying user types. - /// - static bool DetectImplementorOrEventDispatcher (string jniName, string managedTypeName) - { - return jniName.StartsWith ("mono/", StringComparison.Ordinal) && - (managedTypeName.EndsWith ("Implementor", StringComparison.Ordinal) || - managedTypeName.EndsWith ("EventDispatcher", StringComparison.Ordinal)); - } - /// /// Strips assembly qualification from a type name like "Ns.Type, Assembly, Version=..." /// returning just the type name, or null if the input has no comma. diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 4caf3029ab3..31a06dec2e9 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -446,38 +446,24 @@ 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); Assert.False (peer.IsInterface); - Assert.True (peer.IsImplementorOrEventDispatcher, $"{kind} should be detected by scanner"); var model = BuildModel (new [] { peer }, "TypeMap"); 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 treated as unconditional ACW types. + // Future optimization (see issue tracking Implementor trimming) may make them trimmable. + Assert.True (entry!.IsUnconditional, $"{kind} should be unconditional"); } } - public class ImplementorDetection + public class InvokerDetection { - [Fact] - public void Build_UserTypeNamedImplementor_IsNotMisclassified () - { - // User ACW types ending in "Implementor" should NOT be treated as trimmable - // because their JNI name doesn't start with "mono/" (binding generator convention). - var peer = MakeAcwPeer ("my/app/MyImplementor", "MyApp.MyImplementor", "App"); - var model = BuildModel (new [] { peer }); - - var entry = model.Entries.FirstOrDefault (); - Assert.NotNull (entry); - Assert.True (entry!.IsUnconditional, - "User types ending in 'Implementor' should be unconditional (not misclassified as trimmable)"); - } - [Fact] public void Build_TypeIsInvoker_OnlyWhenReferencedByAnotherPeer () { From 3d4374ea9abce2c5e7d8ea43fe80779445773b22 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 10 Mar 2026 18:00:11 +0100 Subject: [PATCH 35/40] Restore [Export] and marshal method scanning groundwork Restores code that was already present in the scanner PR stack (dev/simonrozsival/trimmable-typemap-02-scanner) but was mistakenly removed during the PR slim-down (commit 2aca0050a). This code is pre-existing groundwork for [Export] support, not new to this PR. Restored: - MarshalMethodInfo record on JavaPeerInfo - CompatJniName property on JavaPeerInfo - ExportInfo record on AssemblyIndex - CollectMarshalMethods, ParseExportAttribute, and related scanner methods - BuildJniSignatureFromManaged / ManagedTypeToJniDescriptor helpers - ComputeAutoJniNames (dual JNI naming for acw-map.txt compat) - Marshal method, Export, and CompatJniName tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Scanner/AssemblyIndex.cs | 9 + .../Scanner/JavaPeerInfo.cs | 64 ++++++ .../Scanner/JavaPeerScanner.cs | 194 +++++++++++++++++- .../Generator/FixtureTestBase.cs | 2 + .../TypeMapAssemblyGeneratorTests.cs | 2 + .../Scanner/JavaPeerScannerTests.Behavior.cs | 65 ++++++ .../Scanner/JavaPeerScannerTests.EdgeCases.cs | 12 ++ 7 files changed, 342 insertions(+), 6 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs index ca5063ff019..c2a1830f9d9 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs @@ -291,3 +291,12 @@ sealed class ApplicationAttributeInfo () : TypeAttributeInfo ("ApplicationAttrib public string? BackupAgent { get; set; } public string? ManageSpaceActivity { get; set; } } + +/// +/// Parsed [Export] attribute data for a method. +/// +sealed record ExportInfo +{ + public IReadOnlyList? ThrownNames { get; init; } + public string? SuperArgumentsString { get; init; } +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index f37c0dbb85a..8e119b712e5 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; namespace Microsoft.Android.Sdk.TrimmableTypeMap; @@ -15,6 +16,13 @@ sealed record JavaPeerInfo /// public required string JavaName { get; init; } + /// + /// Compat JNI type name, e.g., "myapp.namespace/MyType" for user types (uses raw namespace, not CRC64). + /// For MCW binding types (with [Register]), this equals . + /// Used by acw-map.txt to support legacy custom view name resolution in layout XMLs. + /// + public required string CompatJniName { get; init; } + /// /// Full managed type name, e.g., "Android.App.Activity". /// @@ -50,6 +58,14 @@ sealed record JavaPeerInfo /// public bool IsUnconditional { get; init; } + /// + /// Marshal methods: methods with [Register(name, sig, connector)], [Export], or + /// constructor registrations ([Register(".ctor", sig, "")] / [JniConstructorSignature]). + /// Constructors are identified by . + /// Ordered — the index in this list is the method's ordinal for RegisterNatives. + /// + public IReadOnlyList MarshalMethods { get; init; } = []; + /// /// Information about the activation constructor for this type. /// May reference a base type's constructor if the type doesn't define its own. @@ -79,6 +95,54 @@ sealed record JavaPeerInfo public bool IsGenericDefinition { get; init; } } +/// +/// Describes a marshal method (a method with [Register] or [Export]) on a Java peer type. +/// Contains all data needed to generate a UCO wrapper, a JCW native declaration, +/// and a RegisterNatives call. +/// +sealed record MarshalMethodInfo +{ + /// + /// JNI method name, e.g., "onCreate". + /// This is the Java method name (without n_ prefix). + /// + public required string JniName { get; init; } + + /// + /// JNI method signature, e.g., "(Landroid/os/Bundle;)V". + /// Contains both parameter types and return type. + /// + public required string JniSignature { get; init; } + + /// + /// The connector string from [Register], e.g., "GetOnCreate_Landroid_os_Bundle_Handler". + /// Null for [Export] methods. + /// + public string? Connector { get; init; } + + /// + /// Name of the managed method this maps to, e.g., "OnCreate". + /// + public required string ManagedMethodName { get; init; } + + /// + /// True if this is a constructor registration. + /// + public bool IsConstructor { get; init; } + + /// + /// For [Export] methods: Java exception types that the method declares it can throw. + /// Null for [Register] methods. + /// + public IReadOnlyList? ThrownNames { get; init; } + + /// + /// For [Export] constructors: super constructor arguments string. + /// Null for [Register] methods. + /// + 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 86cb2340a37..13619ef6201 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Reflection.Metadata; @@ -164,6 +165,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary results // 3. Extends a known Java peer → auto-compute JNI name via CRC64 // 4. None of the above → not a Java peer, skip string? jniName = null; + string? compatJniName = null; bool doNotGenerateAcw = false; index.RegisterInfoByType.TryGetValue (typeHandle, out var registerInfo); @@ -171,15 +173,17 @@ void ScanAssembly (AssemblyIndex index, Dictionary results if (registerInfo is not null && !string.IsNullOrEmpty (registerInfo.JniName)) { jniName = registerInfo.JniName; + compatJniName = jniName; doNotGenerateAcw = registerInfo.DoNotGenerateAcw; } else if (attrInfo?.JniName is not null) { // User type with [Activity(Name = "...")] but no [Register] jniName = attrInfo.JniName; + compatJniName = jniName; } else { // No explicit JNI name — check if this type extends a known Java peer. // If so, auto-compute JNI name from the managed type name via CRC64. if (ExtendsJavaPeer (typeDef, index)) { - jniName = ComputeAutoJniName (typeDef, index); + (jniName, compatJniName) = ComputeAutoJniNames (typeDef, index); } else { continue; } @@ -194,6 +198,9 @@ void ScanAssembly (AssemblyIndex index, Dictionary results var isUnconditional = attrInfo is not null; string? invokerTypeName = null; + // Collect marshal methods (including constructors) in a single pass over methods + var marshalMethods = CollectMarshalMethods (typeDef, index); + // Resolve activation constructor var activationCtor = ResolveActivationCtor (fullName, typeDef, index); @@ -214,6 +221,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary results var peer = new JavaPeerInfo { JavaName = jniName, + CompatJniName = compatJniName, ManagedTypeName = fullName, ManagedTypeNamespace = ExtractNamespace (fullName), ManagedTypeShortName = ExtractShortName (fullName), @@ -222,6 +230,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary results IsAbstract = isAbstract, DoNotGenerateAcw = doNotGenerateAcw, IsUnconditional = isUnconditional, + MarshalMethods = marshalMethods, ActivationCtor = activationCtor, InvokerTypeName = invokerTypeName, IsGenericDefinition = isGenericDefinition, @@ -233,6 +242,171 @@ void ScanAssembly (AssemblyIndex index, Dictionary results } } + List CollectMarshalMethods (TypeDefinition typeDef, AssemblyIndex index) + { + var methods = new List (); + + // Single pass over methods: collect marshal methods (including constructors) + foreach (var methodHandle in typeDef.GetMethods ()) { + var methodDef = index.Reader.GetMethodDefinition (methodHandle); + if (!TryGetMethodRegisterInfo (methodDef, index, out var registerInfo, out var exportInfo) || registerInfo is null) { + continue; + } + + AddMarshalMethod (methods, registerInfo, methodDef, index, exportInfo); + } + + // Collect [Register] from properties (attribute is on the property, not the getter) + foreach (var propHandle in typeDef.GetProperties ()) { + var propDef = index.Reader.GetPropertyDefinition (propHandle); + var propRegister = TryGetPropertyRegisterInfo (propDef, index); + if (propRegister is null) { + continue; + } + + var accessors = propDef.GetAccessors (); + if (!accessors.Getter.IsNil) { + var getterDef = index.Reader.GetMethodDefinition (accessors.Getter); + AddMarshalMethod (methods, propRegister, getterDef, index); + } + } + + return methods; + } + + static void AddMarshalMethod (List methods, RegisterInfo registerInfo, MethodDefinition methodDef, AssemblyIndex index, ExportInfo? exportInfo = null) + { + // Skip methods that are just the JNI name (type-level [Register]) + if (registerInfo.Signature is null && registerInfo.Connector is null) { + return; + } + + methods.Add (new MarshalMethodInfo { + JniName = registerInfo.JniName, + JniSignature = registerInfo.Signature ?? "()V", + Connector = registerInfo.Connector, + ManagedMethodName = index.Reader.GetString (methodDef.Name), + IsConstructor = registerInfo.JniName == "" || registerInfo.JniName == ".ctor", + ThrownNames = exportInfo?.ThrownNames, + SuperArgumentsString = exportInfo?.SuperArgumentsString, + }); + } + + static bool TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex index, out RegisterInfo? registerInfo, out ExportInfo? exportInfo) + { + exportInfo = null; + foreach (var caHandle in methodDef.GetCustomAttributes ()) { + var ca = index.Reader.GetCustomAttribute (caHandle); + var attrName = AssemblyIndex.GetCustomAttributeName (ca, index.Reader); + + if (attrName == "RegisterAttribute") { + registerInfo = index.ParseRegisterAttribute (ca); + return true; + } + + if (attrName == "ExportAttribute") { + (registerInfo, exportInfo) = ParseExportAttribute (ca, methodDef, index); + return true; + } + } + registerInfo = null; + return false; + } + + static RegisterInfo? TryGetPropertyRegisterInfo (PropertyDefinition propDef, AssemblyIndex index) + { + foreach (var caHandle in propDef.GetCustomAttributes ()) { + var ca = index.Reader.GetCustomAttribute (caHandle); + var attrName = AssemblyIndex.GetCustomAttributeName (ca, index.Reader); + + if (attrName == "RegisterAttribute") { + return index.ParseRegisterAttribute (ca); + } + } + return null; + } + + static (RegisterInfo registerInfo, ExportInfo exportInfo) ParseExportAttribute (CustomAttribute ca, MethodDefinition methodDef, AssemblyIndex index) + { + var value = index.DecodeAttribute (ca); + + // [Export("name")] or [Export] (uses method name) + string? exportName = null; + if (value.FixedArguments.Length > 0) { + exportName = (string?)value.FixedArguments [0].Value; + } + + List? thrownNames = null; + string? superArguments = null; + + // Check Named arguments + foreach (var named in value.NamedArguments) { + if (named.Name == "Name" && named.Value is string name) { + exportName = name; + } else if (named.Name == "ThrownNames" && named.Value is ImmutableArray> names) { + thrownNames = new List (names.Length); + foreach (var item in names) { + if (item.Value is string s) { + thrownNames.Add (s); + } + } + } else if (named.Name == "SuperArgumentsString" && named.Value is string superArgs) { + superArguments = superArgs; + } + } + + if (string.IsNullOrEmpty (exportName)) { + exportName = index.Reader.GetString (methodDef.Name); + } + string resolvedExportName = exportName ?? throw new InvalidOperationException ("Export name should not be null at this point."); + + // Build JNI signature from method signature + var sig = methodDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default); + var jniSig = BuildJniSignatureFromManaged (sig); + + return ( + new RegisterInfo { JniName = resolvedExportName, Signature = jniSig, Connector = null, DoNotGenerateAcw = false }, + new ExportInfo { ThrownNames = thrownNames, SuperArgumentsString = superArguments } + ); + } + + static string BuildJniSignatureFromManaged (MethodSignature sig) + { + var sb = new System.Text.StringBuilder (); + sb.Append ('('); + foreach (var param in sig.ParameterTypes) { + sb.Append (ManagedTypeToJniDescriptor (param)); + } + sb.Append (')'); + sb.Append (ManagedTypeToJniDescriptor (sig.ReturnType)); + return sb.ToString (); + } + + static string ManagedTypeToJniDescriptor (string managedType) + { + switch (managedType) { + case "System.Void": return "V"; + case "System.Boolean": return "Z"; + case "System.Byte": + case "System.SByte": return "B"; + case "System.Char": return "C"; + case "System.Int16": + case "System.UInt16": return "S"; + case "System.Int32": + case "System.UInt32": return "I"; + case "System.Int64": + case "System.UInt64": return "J"; + case "System.Single": return "F"; + case "System.Double": return "D"; + case "System.String": return "Ljava/lang/String;"; + default: + if (managedType.EndsWith ("[]")) { + return $"[{ManagedTypeToJniDescriptor (managedType.Substring (0, managedType.Length - 2))}"; + } + return "Ljava/lang/Object;"; + } + } + ActivationCtorInfo? ResolveActivationCtor (string typeName, TypeDefinition typeDef, AssemblyIndex index) { var cacheKey = (typeName, index.AssemblyName); @@ -453,21 +627,29 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) } /// - /// Compute JNI name for a type without [Register] or component Name. + /// Compute both JNI name and compat JNI name for a type without [Register] or component Name. /// JNI name uses CRC64 hash of "namespace:assemblyName" for the package. - /// If a declaring type has [Register], its JNI name is used as prefix. + /// Compat JNI name uses the raw managed namespace (lowercased). + /// If a declaring type has [Register], its JNI name is used as prefix for both. /// Generic backticks are replaced with _. /// - static string ComputeAutoJniName (TypeDefinition typeDef, AssemblyIndex index) + static (string jniName, string compatJniName) ComputeAutoJniNames (TypeDefinition typeDef, AssemblyIndex index) { var (typeName, parentJniName, ns) = ComputeTypeNameParts (typeDef, index); if (parentJniName is not null) { - return $"{parentJniName}_{typeName}"; + var name = $"{parentJniName}_{typeName}"; + return (name, name); } var packageName = GetCrc64PackageName (ns, index.AssemblyName); - return $"{packageName}/{typeName}"; + var jniName = $"{packageName}/{typeName}"; + + string compatName = ns.Length == 0 + ? typeName + : $"{ns.ToLowerInvariant ().Replace ('.', '/')}/{typeName}"; + + return (jniName, compatName); } /// diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs index 0a29a1b9231..70471f62e13 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs @@ -57,6 +57,7 @@ private protected static JavaPeerInfo MakeMcwPeer (string jniName, string manage var (ns, shortName) = ParseManagedTypeName (managedName); return new JavaPeerInfo { JavaName = jniName, + CompatJniName = jniName, ManagedTypeName = managedName, ManagedTypeNamespace = ns, ManagedTypeShortName = shortName, @@ -87,6 +88,7 @@ private protected static JavaPeerInfo MakeInterfacePeer ( var (ns, shortName) = ParseManagedTypeName (managedName); return new JavaPeerInfo { JavaName = jniName, + CompatJniName = jniName, ManagedTypeName = managedName, ManagedTypeNamespace = ns, ManagedTypeShortName = shortName, diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index c6d4bf121c2..d5dd043d8f4 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -106,6 +106,7 @@ public void Generate_DuplicateJniNames_CreatesAliasEntriesAndAssociationAttribut var peers = new List { new JavaPeerInfo { JavaName = "test/Duplicate", + CompatJniName = "test/Duplicate", ManagedTypeName = "Test.Duplicate1", ManagedTypeNamespace = "Test", ManagedTypeShortName = "Duplicate1", @@ -118,6 +119,7 @@ public void Generate_DuplicateJniNames_CreatesAliasEntriesAndAssociationAttribut }, new JavaPeerInfo { JavaName = "test/Duplicate", + CompatJniName = "test/Duplicate", ManagedTypeName = "Test.Duplicate2", ManagedTypeNamespace = "Test", ManagedTypeShortName = "Duplicate2", diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs index 6e2795c303b..3ed0c175cf6 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs @@ -1,9 +1,58 @@ +using System.Linq; using Xunit; namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; public partial class JavaPeerScannerTests { + [Theory] + [InlineData ("android/app/Activity", "OnCreate", "onCreate", "(Landroid/os/Bundle;)V")] + [InlineData ("android/app/Activity", "OnStart", "onStart", "()V")] + [InlineData ("my/app/MainActivity", "OnCreate", "onCreate", "(Landroid/os/Bundle;)V")] + [InlineData ("my/app/AbstractBase", "DoWork", "doWork", "()V")] + [InlineData ("java/lang/Throwable", "Message", "getMessage", "()Ljava/lang/String;")] + [InlineData ("my/app/TouchHandler", "OnTouch", "onTouch", "(Landroid/view/View;I)Z")] + [InlineData ("my/app/TouchHandler", "OnFocusChange", "onFocusChange", "(Landroid/view/View;Z)V")] + [InlineData ("my/app/TouchHandler", "OnScroll", "onScroll", "(IFJD)V")] + [InlineData ("my/app/TouchHandler", "SetItems", "setItems", "([Ljava/lang/String;)V")] + public void Scan_MarshalMethod_HasCorrectSignature (string javaName, string managedName, string jniName, string jniSig) + { + var method = FindFixtureByJavaName (javaName) + .MarshalMethods.FirstOrDefault (m => m.ManagedMethodName == managedName || m.JniName == jniName); + Assert.NotNull (method); + Assert.Equal (jniName, method.JniName); + Assert.Equal (jniSig, method.JniSignature); + } + + [Fact] + public void Scan_MarshalMethod_ConstructorsAndSpecialCases () + { + var ctors = FindFixtureByJavaName ("my/app/CustomView") + .MarshalMethods.Where (m => m.IsConstructor).ToList (); + Assert.Equal (2, ctors.Count); + Assert.Equal ("()V", ctors [0].JniSignature); + Assert.Equal ("(Landroid/content/Context;)V", ctors [1].JniSignature); + + Assert.DoesNotContain (FindFixtureByJavaName ("my/app/MyHelper").MarshalMethods, m => m.IsConstructor); + + var exportMethod = FindFixtureByJavaName ("my/app/ExportExample").MarshalMethods.Single (); + Assert.Equal ("myExportedMethod", exportMethod.JniName); + Assert.Null (exportMethod.Connector); + + var onStart = FindFixtureByJavaName ("android/app/Activity") + .MarshalMethods.FirstOrDefault (m => m.JniName == "onStart"); + Assert.NotNull (onStart); + Assert.Equal ("", onStart.Connector); + + var onClick = FindFixtureByManagedName ("Android.Views.IOnClickListener") + .MarshalMethods.FirstOrDefault (m => m.JniName == "onClick"); + Assert.NotNull (onClick); + Assert.Equal ("(Landroid/view/View;)V", onClick.JniSignature); + + Assert.Equal ("Android.Views.IOnClickListenerInvoker", + FindFixtureByManagedName ("Android.Views.IOnClickListener").InvokerTypeName); + } + [Theory] [InlineData ("android/app/Activity", "Android.App.Activity")] [InlineData ("my/app/SimpleActivity", "Android.App.Activity")] @@ -40,6 +89,22 @@ public void Scan_MultipleInterfaces_AllResolved () Assert.Empty (FindFixtureByJavaName ("my/app/MyHelper").ImplementedInterfaceJavaNames); } + [Theory] + [InlineData ("android/app/Activity", "android/app/Activity")] + [InlineData ("my/app/MainActivity", "my/app/MainActivity")] + public void Scan_CompatJniName (string javaName, string expectedCompat) + { + Assert.Equal (expectedCompat, FindFixtureByJavaName (javaName).CompatJniName); + } + + [Fact] + public void Scan_CompatJniName_UnregisteredType_UsesRawNamespace () + { + var unregistered = FindFixtureByManagedName ("MyApp.UnregisteredHelper"); + Assert.StartsWith ("crc64", unregistered.JavaName); + Assert.Equal ("myapp/UnregisteredHelper", unregistered.CompatJniName); + } + [Fact] public void Scan_CustomJniNameProviderAttribute_UsesNameFromAttribute () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs index 916431945f5..1e7b0c29f16 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs @@ -1,3 +1,4 @@ +using System.Linq; using Xunit; namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; @@ -36,6 +37,8 @@ public void Scan_UnregisteredNestedType_UsesParentJniPrefix (string managedName, public void Scan_EmptyNamespace_Handled () { Assert.Equal ("GlobalType", FindFixtureByJavaName ("my/app/GlobalType").ManagedTypeName); + Assert.Equal ("GlobalUnregisteredType", + FindFixtureByManagedName ("GlobalUnregisteredType").CompatJniName); } [Theory] @@ -47,4 +50,13 @@ public void Scan_UnregisteredType_DiscoveredWithCrc64Name (string managedName) { Assert.StartsWith ("crc64", FindFixtureByManagedName (managedName).JavaName); } + + [Fact] + public void Scan_ExportOnUnregisteredType_MethodDiscovered () + { + var exportMethod = FindFixtureByManagedName ("MyApp.UnregisteredExporter") + .MarshalMethods.FirstOrDefault (m => m.JniName == "doExportedWork"); + Assert.NotNull (exportMethod); + Assert.Null (exportMethod.Connector); + } } From bfa6d5f974fd0ae44e8a7da8014677b32f4d6c4b Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 10 Mar 2026 18:14:42 +0100 Subject: [PATCH 36/40] Restore scanner branch code, minimize diff to pre-existing files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restore all Scanner/*.cs files to their original scanner branch versions (dev/simonrozsival/trimmable-typemap-02-scanner). This PR should only add new code on top of the scanner, not refactor it. Changes vs scanner branch (additions only): - JavaPeerInfo: add ManagedTypeNamespace, ManagedTypeShortName properties - JavaPeerScanner: populate new properties + ExtractNamespace/ExtractShortName - AssemblyIndex: fix file handle leak in Create (try-catch on error) Reverts all gratuitous style changes (class→record, set→init, required, string interpolation, primary constructors, etc.) that made the diff against the scanner branch unnecessarily large. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Scanner/AssemblyIndex.cs | 334 ++++++++++++------ .../Scanner/CustomAttributeTypeProvider.cs | 120 +++---- .../Scanner/JavaPeerInfo.cs | 80 +++-- .../Scanner/JavaPeerScanner.cs | 310 ++++++++-------- .../Scanner/SignatureTypeProvider.cs | 39 +- .../Generator/FixtureTestBase.cs | 12 +- .../Generator/TypeMapModelBuilderTests.cs | 19 +- 7 files changed, 515 insertions(+), 399 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs index c2a1830f9d9..2def0478585 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.IO; using System.Reflection.Metadata; using System.Reflection.PortableExecutable; @@ -14,7 +13,7 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; sealed class AssemblyIndex : IDisposable { readonly PEReader peReader; - readonly CustomAttributeTypeProvider customAttributeTypeProvider; + internal readonly CustomAttributeTypeProvider customAttributeTypeProvider; public MetadataReader Reader { get; } public string AssemblyName { get; } @@ -35,6 +34,18 @@ sealed class AssemblyIndex : IDisposable /// public Dictionary AttributesByType { get; } = new (); + /// + /// Type names of attributes that implement Java.Interop.IJniNameProviderAttribute + /// in this assembly. Used to detect JNI name providers without hardcoding attribute names. + /// + public HashSet JniNameProviderAttributes { get; } = new (StringComparer.Ordinal); + + /// + /// Merged set of all JNI name provider attribute type names across all loaded assemblies. + /// Set by after all assemblies are indexed. + /// + HashSet? allJniNameProviderAttributes; + AssemblyIndex (PEReader peReader, MetadataReader reader, string assemblyName, string filePath) { this.peReader = peReader; @@ -61,10 +72,12 @@ public static AssemblyIndex Create (string filePath) void Build () { + FindJniNameProviderAttributes (); + foreach (var typeHandle in Reader.TypeDefinitions) { var typeDef = Reader.GetTypeDefinition (typeHandle); - var fullName = MetadataTypeNameResolver.GetFullName (typeDef, Reader); + var fullName = GetFullName (typeDef, Reader); if (fullName.Length == 0) { continue; } @@ -73,16 +86,109 @@ void Build () var (registerInfo, attrInfo) = ParseAttributes (typeDef); - if (attrInfo is not null) { + if (attrInfo != null) { AttributesByType [typeHandle] = attrInfo; } - if (registerInfo is not null) { + if (registerInfo != null) { RegisterInfoByType [typeHandle] = registerInfo; } } } + /// + /// Finds all types in this assembly that implement Java.Interop.IJniNameProviderAttribute. + /// + void FindJniNameProviderAttributes () + { + foreach (var typeHandle in Reader.TypeDefinitions) { + var typeDef = Reader.GetTypeDefinition (typeHandle); + if (ImplementsIJniNameProviderAttribute (typeDef)) { + var name = Reader.GetString (typeDef.Name); + JniNameProviderAttributes.Add (name); + } + } + } + + bool ImplementsIJniNameProviderAttribute (TypeDefinition typeDef) + { + foreach (var implHandle in typeDef.GetInterfaceImplementations ()) { + var impl = Reader.GetInterfaceImplementation (implHandle); + if (impl.Interface.Kind == HandleKind.TypeReference) { + var typeRef = Reader.GetTypeReference ((TypeReferenceHandle)impl.Interface); + var name = Reader.GetString (typeRef.Name); + var ns = Reader.GetString (typeRef.Namespace); + if (name == "IJniNameProviderAttribute" && ns == "Java.Interop") { + return true; + } + } else if (impl.Interface.Kind == HandleKind.TypeDefinition) { + var ifaceTypeDef = Reader.GetTypeDefinition ((TypeDefinitionHandle)impl.Interface); + var name = Reader.GetString (ifaceTypeDef.Name); + var ns = Reader.GetString (ifaceTypeDef.Namespace); + if (name == "IJniNameProviderAttribute" && ns == "Java.Interop") { + return true; + } + } + } + return false; + } + + /// + /// Sets the merged set of JNI name provider attributes from all loaded assemblies + /// and re-classifies any attributes that weren't recognized in the initial pass. + /// + public void ReclassifyAttributes (HashSet mergedJniNameProviders) + { + allJniNameProviderAttributes = mergedJniNameProviders; + + foreach (var typeHandle in Reader.TypeDefinitions) { + var typeDef = Reader.GetTypeDefinition (typeHandle); + + // Skip types that already have component attribute info + if (AttributesByType.TryGetValue (typeHandle, out var existing) && existing.HasComponentAttribute) { + continue; + } + + // Re-check custom attributes with the full set of known providers + foreach (var caHandle in typeDef.GetCustomAttributes ()) { + var ca = Reader.GetCustomAttribute (caHandle); + var attrName = GetCustomAttributeName (ca, Reader); + + if (attrName == null || attrName == "RegisterAttribute" || attrName == "ExportAttribute") { + continue; + } + + if (mergedJniNameProviders.Contains (attrName) && !IsKnownComponentAttribute (attrName)) { + var componentName = TryGetNameProperty (ca); + if (componentName != null) { + var attrInfo = existing ?? new TypeAttributeInfo (); + attrInfo.HasComponentAttribute = true; + attrInfo.ComponentAttributeJniName = componentName.Replace ('.', '/'); + AttributesByType [typeHandle] = attrInfo; + } + } + } + } + } + + internal static string GetFullName (TypeDefinition typeDef, MetadataReader reader) + { + var name = reader.GetString (typeDef.Name); + var ns = reader.GetString (typeDef.Namespace); + + if (typeDef.IsNested) { + var declaringType = reader.GetTypeDefinition (typeDef.GetDeclaringType ()); + var parentName = GetFullName (declaringType, reader); + return parentName + "+" + name; + } + + if (ns.Length == 0) { + return name; + } + + return ns + "." + name; + } + (RegisterInfo? register, TypeAttributeInfo? attrs) ParseAttributes (TypeDefinition typeDef) { RegisterInfo? registerInfo = null; @@ -92,30 +198,24 @@ void Build () var ca = Reader.GetCustomAttribute (caHandle); var attrName = GetCustomAttributeName (ca, Reader); - if (attrName is null) { + if (attrName == null) { continue; } if (attrName == "RegisterAttribute") { - registerInfo = ParseRegisterAttribute (ca); + registerInfo = ParseRegisterAttribute (ca, customAttributeTypeProvider); } else if (attrName == "ExportAttribute") { - // [Export] is a method-level attribute; it is parsed at scan time by JavaPeerScanner - } else if (IsKnownComponentAttribute (attrName)) { - attrInfo ??= CreateTypeAttributeInfo (attrName); - var name = TryGetNameProperty (ca); - if (name is not null) { - attrInfo.JniName = name.Replace ('.', '/'); + // [Export] methods are detected per-method in CollectMarshalMethods + } else if (IsJniNameProviderAttribute (attrName)) { + attrInfo ??= new TypeAttributeInfo (); + attrInfo.HasComponentAttribute = true; + var componentName = TryGetNameProperty (ca); + if (componentName != null) { + attrInfo.ComponentAttributeJniName = componentName.Replace ('.', '/'); } - if (attrInfo is ApplicationAttributeInfo applicationAttributeInfo) { - applicationAttributeInfo.BackupAgent = TryGetTypeProperty (ca, "BackupAgent"); - applicationAttributeInfo.ManageSpaceActivity = TryGetTypeProperty (ca, "ManageSpaceActivity"); - } - } else if (attrInfo is null && ImplementsJniNameProviderAttribute (ca)) { - // Custom attribute implementing IJniNameProviderAttribute (e.g., user-defined [CustomJniName]) - var name = TryGetNameProperty (ca); - if (name is not null) { - attrInfo = new TypeAttributeInfo (attrName); - attrInfo.JniName = name.Replace ('.', '/'); + if (attrName == "ApplicationAttribute") { + attrInfo.ApplicationBackupAgent = TryGetTypeProperty (ca, "BackupAgent"); + attrInfo.ApplicationManageSpaceActivity = TryGetTypeProperty (ca, "ManageSpaceActivity"); } } } @@ -123,52 +223,35 @@ void Build () return (registerInfo, attrInfo); } - static readonly HashSet KnownComponentAttributes = new (StringComparer.Ordinal) { - "ActivityAttribute", - "ServiceAttribute", - "BroadcastReceiverAttribute", - "ContentProviderAttribute", - "ApplicationAttribute", - "InstrumentationAttribute", - }; - - static TypeAttributeInfo CreateTypeAttributeInfo (string attrName) - { - return attrName == "ApplicationAttribute" - ? new ApplicationAttributeInfo () - : new TypeAttributeInfo (attrName); - } - - static bool IsKnownComponentAttribute (string attrName) => KnownComponentAttributes.Contains (attrName); - /// - /// Checks whether a custom attribute's type implements Java.Interop.IJniNameProviderAttribute. - /// Only works for attributes defined in the assembly being scanned (MethodDefinition constructors). + /// Checks if an attribute type name is a known IJniNameProviderAttribute implementor. + /// Uses the local set first (from this assembly), then falls back to the merged set + /// (populated after all assemblies are loaded), then falls back to hardcoded names + /// for the well-known Android component attributes. /// - bool ImplementsJniNameProviderAttribute (CustomAttribute ca) + bool IsJniNameProviderAttribute (string attrName) { - if (ca.Constructor.Kind != HandleKind.MethodDefinition) { - return false; + if (JniNameProviderAttributes.Contains (attrName)) { + return true; } - var methodDef = Reader.GetMethodDefinition ((MethodDefinitionHandle)ca.Constructor); - var typeDef = Reader.GetTypeDefinition (methodDef.GetDeclaringType ()); - foreach (var implHandle in typeDef.GetInterfaceImplementations ()) { - var impl = Reader.GetInterfaceImplementation (implHandle); - if (impl.Interface.Kind == HandleKind.TypeReference) { - var typeRef = Reader.GetTypeReference ((TypeReferenceHandle)impl.Interface); - if (Reader.GetString (typeRef.Name) == "IJniNameProviderAttribute" && - Reader.GetString (typeRef.Namespace) == "Java.Interop") { - return true; - } - } else if (impl.Interface.Kind == HandleKind.TypeDefinition) { - var ifaceDef = Reader.GetTypeDefinition ((TypeDefinitionHandle)impl.Interface); - if (Reader.GetString (ifaceDef.Name) == "IJniNameProviderAttribute" && - Reader.GetString (ifaceDef.Namespace) == "Java.Interop") { - return true; - } - } + + if (allJniNameProviderAttributes != null && allJniNameProviderAttributes.Contains (attrName)) { + return true; } - return false; + + // Fallback for the case where we haven't loaded the assembly defining the attribute yet. + // This covers the common case where user assemblies reference Mono.Android attributes. + return IsKnownComponentAttribute (attrName); + } + + static bool IsKnownComponentAttribute (string attrName) + { + return attrName == "ActivityAttribute" + || attrName == "ServiceAttribute" + || attrName == "BroadcastReceiverAttribute" + || attrName == "ContentProviderAttribute" + || attrName == "ApplicationAttribute" + || attrName == "InstrumentationAttribute"; } internal static string? GetCustomAttributeName (CustomAttribute ca, MetadataReader reader) @@ -187,18 +270,10 @@ bool ImplementsJniNameProviderAttribute (CustomAttribute ca) return null; } - internal RegisterInfo ParseRegisterAttribute (CustomAttribute ca) + internal static RegisterInfo ParseRegisterAttribute (CustomAttribute ca, ICustomAttributeTypeProvider provider) { - return ParseRegisterInfo (DecodeAttribute (ca)); - } - - internal CustomAttributeValue DecodeAttribute (CustomAttribute ca) - { - return ca.DecodeValue (customAttributeTypeProvider); - } + var value = ca.DecodeValue (provider); - RegisterInfo ParseRegisterInfo (CustomAttributeValue value) - { string jniName = ""; string? signature = null; string? connector = null; @@ -214,22 +289,18 @@ RegisterInfo ParseRegisterInfo (CustomAttributeValue value) connector = (string?)value.FixedArguments [2].Value; } - if (TryGetNamedArgument (value, "DoNotGenerateAcw", out var doNotGenerateAcwValue)) { + if (TryGetNamedBooleanArgument (value, "DoNotGenerateAcw", out var doNotGenerateAcwValue)) { doNotGenerateAcw = doNotGenerateAcwValue; } - return new RegisterInfo { - JniName = jniName, - Signature = signature, - Connector = connector, - DoNotGenerateAcw = doNotGenerateAcw, - }; + return new RegisterInfo (jniName, signature, connector, doNotGenerateAcw); } string? TryGetTypeProperty (CustomAttribute ca, string propertyName) { - var value = DecodeAttribute (ca); - if (TryGetNamedArgument (value, propertyName, out var typeName) && !string.IsNullOrEmpty (typeName)) { + var value = ca.DecodeValue (customAttributeTypeProvider); + var typeName = TryGetNamedStringArgument (value, propertyName); + if (!string.IsNullOrEmpty (typeName)) { return typeName; } return null; @@ -237,9 +308,11 @@ RegisterInfo ParseRegisterInfo (CustomAttributeValue value) string? TryGetNameProperty (CustomAttribute ca) { - var value = DecodeAttribute (ca); + var value = ca.DecodeValue (customAttributeTypeProvider); - if (TryGetNamedArgument (value, "Name", out var name) && !string.IsNullOrEmpty (name)) { + // Check named arguments first (e.g., [Activity(Name = "...")]) + var name = TryGetNamedStringArgument (value, "Name"); + if (!string.IsNullOrEmpty (name)) { return name; } @@ -251,18 +324,30 @@ RegisterInfo ParseRegisterInfo (CustomAttributeValue value) return null; } - static bool TryGetNamedArgument (CustomAttributeValue value, string argumentName, [MaybeNullWhen (false)] out T argumentValue) where T : notnull + static bool TryGetNamedBooleanArgument (CustomAttributeValue value, string argumentName, out bool argumentValue) { foreach (var named in value.NamedArguments) { - if (named.Name == argumentName && named.Value is T typedValue) { - argumentValue = typedValue; + if (named.Name == argumentName && named.Value is bool boolValue) { + argumentValue = boolValue; return true; } } - argumentValue = default; + + argumentValue = false; return false; } + static string? TryGetNamedStringArgument (CustomAttributeValue value, string argumentName) + { + foreach (var named in value.NamedArguments) { + if (named.Name == argumentName && named.Value is string stringValue) { + return stringValue; + } + } + + return null; + } + public void Dispose () { peReader.Dispose (); @@ -270,33 +355,64 @@ public void Dispose () } /// -/// Parsed [Register] attribute data for a type or method. +/// Parsed [Register] or [Export] attribute data for a type or method. /// -sealed record RegisterInfo +sealed class RegisterInfo { - public required string JniName { get; init; } - public string? Signature { get; init; } - public string? Connector { get; init; } - public bool DoNotGenerateAcw { get; init; } -} + public string JniName { get; } + public string? Signature { get; } + public string? Connector { get; } + public bool DoNotGenerateAcw { get; } -class TypeAttributeInfo (string attributeName) -{ - public string AttributeName { get; } = attributeName; - public string? JniName { get; set; } -} + /// + /// For [Export] methods: Java exception type names the method declares it can throw. + /// + public IReadOnlyList? ThrownNames { get; } -sealed class ApplicationAttributeInfo () : TypeAttributeInfo ("ApplicationAttribute") -{ - public string? BackupAgent { get; set; } - public string? ManageSpaceActivity { get; set; } + /// + /// For [Export] methods: super constructor arguments string. + /// + public string? SuperArgumentsString { get; } + + public RegisterInfo (string jniName, string? signature, string? connector, bool doNotGenerateAcw, + IReadOnlyList? thrownNames = null, string? superArgumentsString = null) + { + JniName = jniName; + Signature = signature; + Connector = connector; + DoNotGenerateAcw = doNotGenerateAcw; + ThrownNames = thrownNames; + SuperArgumentsString = superArgumentsString; + } } /// -/// Parsed [Export] attribute data for a method. +/// Aggregated attribute information for a type, beyond [Register]. /// -sealed record ExportInfo +sealed class TypeAttributeInfo { - public IReadOnlyList? ThrownNames { get; init; } - public string? SuperArgumentsString { get; init; } + /// + /// Type has [Activity], [Service], [BroadcastReceiver], [ContentProvider], + /// [Application], or [Instrumentation]. + /// + public bool HasComponentAttribute { get; set; } + + /// + /// The JNI name from the Name property of a component attribute + /// (e.g., [Activity(Name = "my.app.MainActivity")] → "my/app/MainActivity"). + /// Null if no Name was specified on the component attribute. + /// + public string? ComponentAttributeJniName { get; set; } + + /// + /// If the type has [Application(BackupAgent = typeof(X))], + /// this is the full name of X. + /// + public string? ApplicationBackupAgent { get; set; } + + /// + /// If the type has [Application(ManageSpaceActivity = typeof(X))], + /// this is the full name of X. + /// + public string? ApplicationManageSpaceActivity { get; set; } } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/CustomAttributeTypeProvider.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/CustomAttributeTypeProvider.cs index f3441d1a475..2152e557bb8 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/CustomAttributeTypeProvider.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/CustomAttributeTypeProvider.cs @@ -1,5 +1,5 @@ using System; -using System.Collections.Generic; +using System.Collections.Immutable; using System.Reflection.Metadata; namespace Microsoft.Android.Sdk.TrimmableTypeMap; @@ -8,95 +8,83 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// Minimal ICustomAttributeTypeProvider implementation for decoding /// custom attribute values via System.Reflection.Metadata. /// -sealed class CustomAttributeTypeProvider (MetadataReader reader) : ICustomAttributeTypeProvider +sealed class CustomAttributeTypeProvider : ICustomAttributeTypeProvider { - Dictionary? enumTypeCache; + readonly MetadataReader reader; + + public CustomAttributeTypeProvider (MetadataReader reader) + { + this.reader = reader; + } public string GetPrimitiveType (PrimitiveTypeCode typeCode) => typeCode.ToString (); public string GetTypeFromDefinition (MetadataReader metadataReader, TypeDefinitionHandle handle, byte rawTypeKind) - => MetadataTypeNameResolver.GetTypeFromDefinition (metadataReader, handle, rawTypeKind); - - public string GetTypeFromReference (MetadataReader metadataReader, TypeReferenceHandle handle, byte rawTypeKind) - => MetadataTypeNameResolver.GetTypeFromReference (metadataReader, handle, rawTypeKind); - - public string GetTypeFromSerializedName (string name) => name; - - public PrimitiveTypeCode GetUnderlyingEnumType (string type) { - if (enumTypeCache == null) { - enumTypeCache = BuildEnumTypeCache (); - } - - if (enumTypeCache.TryGetValue (type, out var code)) { - return code; + var typeDef = metadataReader.GetTypeDefinition (handle); + var name = metadataReader.GetString (typeDef.Name); + if (typeDef.IsNested) { + var parent = GetTypeFromDefinition (metadataReader, typeDef.GetDeclaringType (), rawTypeKind); + return parent + "+" + name; } - - // Default to Int32 for enums defined in other assemblies - return PrimitiveTypeCode.Int32; + var ns = metadataReader.GetString (typeDef.Namespace); + return ns.Length > 0 ? ns + "." + name : name; } - Dictionary BuildEnumTypeCache () + public string GetTypeFromReference (MetadataReader metadataReader, TypeReferenceHandle handle, byte rawTypeKind) { - var cache = new Dictionary (); - - foreach (var typeHandle in reader.TypeDefinitions) { - var typeDef = reader.GetTypeDefinition (typeHandle); - - // Only process enum types - if (!IsEnum (typeDef)) - continue; - - var fullName = GetTypeFromDefinition (reader, typeHandle, rawTypeKind: 0); - cache [fullName] = GetEnumUnderlyingTypeCode (typeDef); + var typeRef = metadataReader.GetTypeReference (handle); + var name = metadataReader.GetString (typeRef.Name); + if (typeRef.ResolutionScope.Kind == HandleKind.TypeReference) { + var parent = GetTypeFromReference (metadataReader, (TypeReferenceHandle)typeRef.ResolutionScope, rawTypeKind); + return parent + "+" + name; } - - return cache; + var ns = metadataReader.GetString (typeRef.Namespace); + return ns.Length > 0 ? ns + "." + name : name; } - bool IsEnum (TypeDefinition typeDef) - { - var baseType = typeDef.BaseType; - if (baseType.IsNil) - return false; - - string? baseFullName = baseType.Kind switch { - HandleKind.TypeReference => GetTypeFromReference (reader, (TypeReferenceHandle)baseType, rawTypeKind: 0), - HandleKind.TypeDefinition => GetTypeFromDefinition (reader, (TypeDefinitionHandle)baseType, rawTypeKind: 0), - _ => null, - }; - - return baseFullName == "System.Enum"; - } + public string GetTypeFromSerializedName (string name) => name; - PrimitiveTypeCode GetEnumUnderlyingTypeCode (TypeDefinition typeDef) + public PrimitiveTypeCode GetUnderlyingEnumType (string type) { - // For enums, the first instance field is the underlying value__ field - foreach (var fieldHandle in typeDef.GetFields ()) { - var field = reader.GetFieldDefinition (fieldHandle); - if ((field.Attributes & System.Reflection.FieldAttributes.Static) != 0) + // Find the enum type in this assembly's metadata and read its value__ field type. + foreach (var typeHandle in reader.TypeDefinitions) { + var typeDef = reader.GetTypeDefinition (typeHandle); + var name = reader.GetString (typeDef.Name); + var ns = reader.GetString (typeDef.Namespace); + var fullName = ns.Length > 0 ? ns + "." + name : name; + + if (fullName != type) continue; - var sig = field.DecodeSignature (SignatureTypeProvider.Instance, genericContext: null); - return sig switch { - "System.Byte" => PrimitiveTypeCode.Byte, - "System.SByte" => PrimitiveTypeCode.SByte, - "System.Int16" => PrimitiveTypeCode.Int16, - "System.UInt16" => PrimitiveTypeCode.UInt16, - "System.Int32" => PrimitiveTypeCode.Int32, - "System.UInt32" => PrimitiveTypeCode.UInt32, - "System.Int64" => PrimitiveTypeCode.Int64, - "System.UInt64" => PrimitiveTypeCode.UInt64, - _ => PrimitiveTypeCode.Int32, - }; + // For enums, the first instance field is the underlying value__ field + foreach (var fieldHandle in typeDef.GetFields ()) { + var field = reader.GetFieldDefinition (fieldHandle); + if ((field.Attributes & System.Reflection.FieldAttributes.Static) != 0) + continue; + + var sig = field.DecodeSignature (SignatureTypeProvider.Instance, genericContext: null); + return sig switch { + "System.Byte" => PrimitiveTypeCode.Byte, + "System.SByte" => PrimitiveTypeCode.SByte, + "System.Int16" => PrimitiveTypeCode.Int16, + "System.UInt16" => PrimitiveTypeCode.UInt16, + "System.Int32" => PrimitiveTypeCode.Int32, + "System.UInt32" => PrimitiveTypeCode.UInt32, + "System.Int64" => PrimitiveTypeCode.Int64, + "System.UInt64" => PrimitiveTypeCode.UInt64, + _ => PrimitiveTypeCode.Int32, + }; + } } + // Default to Int32 for enums defined in other assemblies return PrimitiveTypeCode.Int32; } public string GetSystemType () => "System.Type"; - public string GetSZArrayType (string elementType) => $"{elementType}[]"; + public string GetSZArrayType (string elementType) => elementType + "[]"; public bool IsSystemType (string type) => type == "System.Type" || type == "Type"; } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index 8e119b712e5..67564c61f00 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -8,55 +8,69 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// Contains all data needed by downstream generators (TypeMap IL, UCO wrappers, JCW Java sources). /// Generators consume this data model — they never touch PEReader/MetadataReader. /// -sealed record JavaPeerInfo +sealed class JavaPeerInfo { /// /// JNI type name, e.g., "android/app/Activity". /// Extracted from the [Register] attribute. /// - public required string JavaName { get; init; } + public string JavaName { get; set; } = ""; /// /// Compat JNI type name, e.g., "myapp.namespace/MyType" for user types (uses raw namespace, not CRC64). /// For MCW binding types (with [Register]), this equals . /// Used by acw-map.txt to support legacy custom view name resolution in layout XMLs. /// - public required string CompatJniName { get; init; } + public string CompatJniName { get; set; } = ""; /// /// Full managed type name, e.g., "Android.App.Activity". /// - public required string ManagedTypeName { get; init; } + public string ManagedTypeName { get; set; } = ""; /// /// Managed type namespace, e.g., "Android.App". /// - public required string ManagedTypeNamespace { get; init; } + public string ManagedTypeNamespace { get; set; } = ""; /// /// Managed type short name (without namespace), e.g., "Activity". /// - public required string ManagedTypeShortName { get; init; } + public string ManagedTypeShortName { get; set; } = ""; /// /// Assembly name the type belongs to, e.g., "Mono.Android". /// - public required string AssemblyName { get; init; } - public bool IsInterface { get; init; } - public bool IsAbstract { get; init; } + public string AssemblyName { get; set; } = ""; + + /// + /// JNI name of the base Java type, e.g., "android/app/Activity" for a type + /// that extends Activity. Null for java/lang/Object or types without a Java base. + /// Needed by JCW Java source generation ("extends" clause). + /// + public string? BaseJavaName { get; set; } + + /// + /// JNI names of Java interfaces this type implements, e.g., ["android/view/View$OnClickListener"]. + /// Needed by JCW Java source generation ("implements" clause). + /// + public IReadOnlyList ImplementedInterfaceJavaNames { get; set; } = Array.Empty (); + + public bool IsInterface { get; set; } + public bool IsAbstract { get; set; } /// /// If true, this is a Managed Callable Wrapper (MCW) binding type. /// No JCW or RegisterNatives will be generated for it. /// - public bool DoNotGenerateAcw { get; init; } + public bool DoNotGenerateAcw { get; set; } /// /// Types with component attributes ([Activity], [Service], etc.), /// custom views from layout XML, or manifest-declared components /// are unconditionally preserved (not trimmable). /// - public bool IsUnconditional { get; init; } + public bool IsUnconditional { get; set; } /// /// Marshal methods: methods with [Register(name, sig, connector)], [Export], or @@ -64,35 +78,25 @@ sealed record JavaPeerInfo /// Constructors are identified by . /// Ordered — the index in this list is the method's ordinal for RegisterNatives. /// - public IReadOnlyList MarshalMethods { get; init; } = []; + public IReadOnlyList MarshalMethods { get; set; } = 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. /// - public ActivationCtorInfo? ActivationCtor { get; init; } + public ActivationCtorInfo? ActivationCtor { get; set; } /// /// For interfaces and abstract types, the name of the invoker type /// used to instantiate instances from Java. /// - public string? InvokerTypeName { get; init; } - - /// - /// JNI name of the base Java peer type, if any. - /// - public string? BaseJavaName { get; init; } - - /// - /// JNI names of Java interfaces implemented by this type. - /// - public IReadOnlyList ImplementedInterfaceJavaNames { get; init; } = []; + public string? InvokerTypeName { get; set; } /// /// True if this is an open generic type definition. /// Generic types get TypeMap entries but CreateInstance throws NotSupportedException. /// - public bool IsGenericDefinition { get; init; } + public bool IsGenericDefinition { get; set; } } /// @@ -100,69 +104,69 @@ sealed record JavaPeerInfo /// Contains all data needed to generate a UCO wrapper, a JCW native declaration, /// and a RegisterNatives call. /// -sealed record MarshalMethodInfo +sealed class MarshalMethodInfo { /// /// JNI method name, e.g., "onCreate". /// This is the Java method name (without n_ prefix). /// - public required string JniName { get; init; } + public string JniName { get; set; } = ""; /// /// JNI method signature, e.g., "(Landroid/os/Bundle;)V". /// Contains both parameter types and return type. /// - public required string JniSignature { get; init; } + public string JniSignature { get; set; } = ""; /// /// The connector string from [Register], e.g., "GetOnCreate_Landroid_os_Bundle_Handler". /// Null for [Export] methods. /// - public string? Connector { get; init; } + public string? Connector { get; set; } /// /// Name of the managed method this maps to, e.g., "OnCreate". /// - public required string ManagedMethodName { get; init; } + public string ManagedMethodName { get; set; } = ""; /// /// True if this is a constructor registration. /// - public bool IsConstructor { get; init; } + public bool IsConstructor { get; set; } /// /// For [Export] methods: Java exception types that the method declares it can throw. /// Null for [Register] methods. /// - public IReadOnlyList? ThrownNames { get; init; } + public IReadOnlyList? ThrownNames { get; set; } /// - /// For [Export] constructors: super constructor arguments string. + /// For [Export] methods: super constructor arguments string. /// Null for [Register] methods. /// - public string? SuperArgumentsString { get; init; } + public string? SuperArgumentsString { get; set; } } /// /// Describes how to call the activation constructor for a Java peer type. /// -sealed record ActivationCtorInfo +sealed class ActivationCtorInfo { /// /// The type that declares the activation constructor. /// May be the type itself or a base type. /// - public required string DeclaringTypeName { get; init; } + public string DeclaringTypeName { get; set; } = ""; /// /// The assembly containing the declaring type. /// - public required string DeclaringAssemblyName { get; init; } + public string DeclaringAssemblyName { get; set; } = ""; /// /// The style of activation constructor found. /// - public required ActivationCtorStyle Style { get; init; } + public ActivationCtorStyle Style { get; set; } } enum ActivationCtorStyle diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 13619ef6201..785453ecf87 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; @@ -16,22 +14,21 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// sealed class JavaPeerScanner : IDisposable { - readonly Dictionary _assemblyCache = new (StringComparer.Ordinal); - readonly Dictionary<(string typeName, string assemblyName), ActivationCtorInfo> _activationCtorCache = new (); - bool _hasScanned; + readonly Dictionary assemblyCache = new (StringComparer.Ordinal); + readonly Dictionary activationCtorCache = new (StringComparer.Ordinal); /// /// Resolves a type name + assembly name to a TypeDefinitionHandle + AssemblyIndex. /// Checks the specified assembly (by name) in the assembly cache. /// - bool TryResolveType (string typeName, string assemblyName, out TypeDefinitionHandle handle, [NotNullWhen (true)] out AssemblyIndex? resolvedIndex) + bool TryResolveType (string typeName, string assemblyName, out TypeDefinitionHandle handle, out AssemblyIndex resolvedIndex) { - if (_assemblyCache.TryGetValue (assemblyName, out resolvedIndex) && + if (assemblyCache.TryGetValue (assemblyName, out resolvedIndex!) && resolvedIndex.TypesByFullName.TryGetValue (typeName, out handle)) { return true; } handle = default; - resolvedIndex = null; + resolvedIndex = null!; return false; } @@ -49,16 +46,16 @@ bool TryResolveType (string typeName, string assemblyName, out TypeDefinitionHan switch (scope.Kind) { case HandleKind.AssemblyReference: { var asmRef = index.Reader.GetAssemblyReference ((AssemblyReferenceHandle)scope); - var fullName = MetadataTypeNameResolver.JoinNamespaceAndName (ns, name); + var fullName = ns.Length > 0 ? ns + "." + name : name; return (fullName, index.Reader.GetString (asmRef.Name)); } case HandleKind.TypeReference: { // Nested type: recurse to get the declaring type's full name and assembly var (parentFullName, assemblyName) = ResolveTypeReference ((TypeReferenceHandle)scope, index); - return (MetadataTypeNameResolver.JoinNestedTypeName (parentFullName, name), assemblyName); + return (parentFullName + "+" + name, assemblyName); } default: { - var fullName = MetadataTypeNameResolver.JoinNamespaceAndName (ns, name); + var fullName = ns.Length > 0 ? ns + "." + name : name; return (fullName, index.AssemblyName); } } @@ -82,26 +79,32 @@ bool TryResolveType (string typeName, string assemblyName, out TypeDefinitionHan /// public List Scan (IReadOnlyList assemblyPaths) { - if (_hasScanned) { - throw new InvalidOperationException ("Scan() can only be called once per JavaPeerScanner instance. Create a new instance for each scan."); - } - _hasScanned = true; - // Phase 1: Build indices for all assemblies foreach (var path in assemblyPaths) { var index = AssemblyIndex.Create (path); - _assemblyCache [index.AssemblyName] = index; + assemblyCache [index.AssemblyName] = index; + } + + // Phase 1b: Merge IJniNameProviderAttribute implementor sets from all assemblies + // and re-classify any attributes that weren't recognized in the initial pass + // (e.g., user assembly references ActivityAttribute from Mono.Android.dll). + var mergedJniNameProviders = new HashSet (StringComparer.Ordinal); + foreach (var index in assemblyCache.Values) { + mergedJniNameProviders.UnionWith (index.JniNameProviderAttributes); + } + foreach (var index in assemblyCache.Values) { + index.ReclassifyAttributes (mergedJniNameProviders); } // Phase 2: Analyze types using cached indices var resultsByManagedName = new Dictionary (StringComparer.Ordinal); - foreach (var index in _assemblyCache.Values) { + foreach (var index in assemblyCache.Values) { ScanAssembly (index, resultsByManagedName); } // Phase 3: Force unconditional on types referenced by [Application] attributes - ForceUnconditionalCrossReferences (resultsByManagedName, _assemblyCache); + ForceUnconditionalCrossReferences (resultsByManagedName, assemblyCache); return new List (resultsByManagedName.Values); } @@ -111,40 +114,31 @@ public List Scan (IReadOnlyList assemblyPaths) /// [Application(ManageSpaceActivity = typeof(X))] must be unconditional, /// because the manifest will reference them even if nothing else does. /// - static void ForceUnconditionalCrossReferences (Dictionary resultsByManagedName, Dictionary _assemblyCache) + static void ForceUnconditionalCrossReferences (Dictionary resultsByManagedName, Dictionary assemblyCache) { - foreach (var index in _assemblyCache.Values) { + foreach (var index in assemblyCache.Values) { foreach (var attrInfo in index.AttributesByType.Values) { - if (attrInfo is ApplicationAttributeInfo applicationAttributeInfo) { - ForceUnconditionalIfPresent (resultsByManagedName, applicationAttributeInfo.BackupAgent); - ForceUnconditionalIfPresent (resultsByManagedName, applicationAttributeInfo.ManageSpaceActivity); - } + ForceUnconditionalIfPresent (resultsByManagedName, attrInfo.ApplicationBackupAgent); + ForceUnconditionalIfPresent (resultsByManagedName, attrInfo.ApplicationManageSpaceActivity); } } } static void ForceUnconditionalIfPresent (Dictionary resultsByManagedName, string? managedTypeName) { - if (managedTypeName is null) { + if (managedTypeName == null) { return; } - managedTypeName = managedTypeName.Trim (); - if (managedTypeName.Length == 0) { - return; + // TryGetTypeProperty may return assembly-qualified names like "Ns.Type, Assembly, ..." + // Strip to just the type name for lookup + var commaIndex = managedTypeName.IndexOf (','); + if (commaIndex > 0) { + managedTypeName = managedTypeName.Substring (0, commaIndex).Trim (); } - // Try exact match first (handles both plain and assembly-qualified names) if (resultsByManagedName.TryGetValue (managedTypeName, out var peer)) { - resultsByManagedName [managedTypeName] = peer with { IsUnconditional = true }; - return; - } - - // TryGetTypeProperty may return assembly-qualified names like "Ns.Type, Assembly, ..." - // Strip to just the type name for lookup - var typeName = StripAssemblyQualification (managedTypeName); - if (typeName is not null && resultsByManagedName.TryGetValue (typeName, out peer)) { - resultsByManagedName [typeName] = peer with { IsUnconditional = true }; + peer.IsUnconditional = true; } } @@ -171,13 +165,13 @@ void ScanAssembly (AssemblyIndex index, Dictionary results index.RegisterInfoByType.TryGetValue (typeHandle, out var registerInfo); index.AttributesByType.TryGetValue (typeHandle, out var attrInfo); - if (registerInfo is not null && !string.IsNullOrEmpty (registerInfo.JniName)) { + if (registerInfo != null && !string.IsNullOrEmpty (registerInfo.JniName)) { jniName = registerInfo.JniName; compatJniName = jniName; doNotGenerateAcw = registerInfo.DoNotGenerateAcw; - } else if (attrInfo?.JniName is not null) { + } else if (attrInfo?.ComponentAttributeJniName != null) { // User type with [Activity(Name = "...")] but no [Register] - jniName = attrInfo.JniName; + jniName = attrInfo.ComponentAttributeJniName; compatJniName = jniName; } else { // No explicit JNI name — check if this type extends a known Java peer. @@ -189,15 +183,21 @@ void ScanAssembly (AssemblyIndex index, Dictionary results } } - var fullName = MetadataTypeNameResolver.GetFullName (typeDef, index.Reader); + var fullName = AssemblyIndex.GetFullName (typeDef, index.Reader); var isInterface = (typeDef.Attributes & TypeAttributes.Interface) != 0; var isAbstract = (typeDef.Attributes & TypeAttributes.Abstract) != 0; var isGenericDefinition = typeDef.GetGenericParameters ().Count > 0; - var isUnconditional = attrInfo is not null; + var isUnconditional = attrInfo?.HasComponentAttribute ?? false; string? invokerTypeName = null; + // Resolve base Java type name + var baseJavaName = ResolveBaseJavaName (typeDef, index, results); + + // Resolve implemented Java interface names + var implementedInterfaces = ResolveImplementedInterfaceJavaNames (typeDef, index); + // Collect marshal methods (including constructors) in a single pass over methods var marshalMethods = CollectMarshalMethods (typeDef, index); @@ -209,16 +209,6 @@ void ScanAssembly (AssemblyIndex index, Dictionary results invokerTypeName = TryFindInvokerTypeName (fullName, typeHandle, index); } - // Resolve base type JNI name - string? baseJavaName = null; - var baseTypeInfo = GetBaseTypeInfo (typeDef, index); - if (baseTypeInfo is not null) { - baseJavaName = ResolveRegisterJniName (baseTypeInfo.Value.typeName, baseTypeInfo.Value.assemblyName); - } - - // Resolve implemented interface JNI names - var implementedInterfaces = ResolveImplementedInterfaces (typeDef, index); - var peer = new JavaPeerInfo { JavaName = jniName, CompatJniName = compatJniName, @@ -226,6 +216,8 @@ void ScanAssembly (AssemblyIndex index, Dictionary results ManagedTypeNamespace = ExtractNamespace (fullName), ManagedTypeShortName = ExtractShortName (fullName), AssemblyName = index.AssemblyName, + BaseJavaName = baseJavaName, + ImplementedInterfaceJavaNames = implementedInterfaces, IsInterface = isInterface, IsAbstract = isAbstract, DoNotGenerateAcw = doNotGenerateAcw, @@ -234,8 +226,6 @@ void ScanAssembly (AssemblyIndex index, Dictionary results ActivationCtor = activationCtor, InvokerTypeName = invokerTypeName, IsGenericDefinition = isGenericDefinition, - BaseJavaName = baseJavaName, - ImplementedInterfaceJavaNames = implementedInterfaces, }; results [fullName] = peer; @@ -249,18 +239,19 @@ List CollectMarshalMethods (TypeDefinition typeDef, AssemblyI // Single pass over methods: collect marshal methods (including constructors) foreach (var methodHandle in typeDef.GetMethods ()) { var methodDef = index.Reader.GetMethodDefinition (methodHandle); - if (!TryGetMethodRegisterInfo (methodDef, index, out var registerInfo, out var exportInfo) || registerInfo is null) { + var registerInfo = TryGetMethodRegisterInfo (methodDef, index); + if (registerInfo == null) { continue; } - AddMarshalMethod (methods, registerInfo, methodDef, index, exportInfo); + AddMarshalMethod (methods, registerInfo, methodDef, index); } // Collect [Register] from properties (attribute is on the property, not the getter) foreach (var propHandle in typeDef.GetProperties ()) { var propDef = index.Reader.GetPropertyDefinition (propHandle); var propRegister = TryGetPropertyRegisterInfo (propDef, index); - if (propRegister is null) { + if (propRegister == null) { continue; } @@ -274,10 +265,10 @@ List CollectMarshalMethods (TypeDefinition typeDef, AssemblyI return methods; } - static void AddMarshalMethod (List methods, RegisterInfo registerInfo, MethodDefinition methodDef, AssemblyIndex index, ExportInfo? exportInfo = null) + static void AddMarshalMethod (List methods, RegisterInfo registerInfo, MethodDefinition methodDef, AssemblyIndex index) { // Skip methods that are just the JNI name (type-level [Register]) - if (registerInfo.Signature is null && registerInfo.Connector is null) { + if (registerInfo.Signature == null && registerInfo.Connector == null) { return; } @@ -287,30 +278,71 @@ static void AddMarshalMethod (List methods, RegisterInfo regi Connector = registerInfo.Connector, ManagedMethodName = index.Reader.GetString (methodDef.Name), IsConstructor = registerInfo.JniName == "" || registerInfo.JniName == ".ctor", - ThrownNames = exportInfo?.ThrownNames, - SuperArgumentsString = exportInfo?.SuperArgumentsString, + ThrownNames = registerInfo.ThrownNames, + SuperArgumentsString = registerInfo.SuperArgumentsString, }); } - static bool TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex index, out RegisterInfo? registerInfo, out ExportInfo? exportInfo) + string? ResolveBaseJavaName (TypeDefinition typeDef, AssemblyIndex index, Dictionary results) + { + var baseInfo = GetBaseTypeInfo (typeDef, index); + if (baseInfo == null) { + return null; + } + + var (baseTypeName, baseAssemblyName) = baseInfo.Value; + + // First try [Register] attribute + var registerJniName = ResolveRegisterJniName (baseTypeName, baseAssemblyName); + if (registerJniName != null) { + return registerJniName; + } + + // Fall back to already-scanned results (component-attributed or CRC64-computed peers) + if (results.TryGetValue (baseTypeName, out var basePeer)) { + return basePeer.JavaName; + } + + return null; + } + + List ResolveImplementedInterfaceJavaNames (TypeDefinition typeDef, AssemblyIndex index) + { + var result = new List (); + var interfaceImpls = typeDef.GetInterfaceImplementations (); + + foreach (var implHandle in interfaceImpls) { + var impl = index.Reader.GetInterfaceImplementation (implHandle); + var ifaceJniName = ResolveInterfaceJniName (impl.Interface, index); + if (ifaceJniName != null) { + result.Add (ifaceJniName); + } + } + + return result; + } + + string? ResolveInterfaceJniName (EntityHandle interfaceHandle, AssemblyIndex index) + { + var resolved = ResolveEntityHandle (interfaceHandle, index); + return resolved != null ? ResolveRegisterJniName (resolved.Value.typeName, resolved.Value.assemblyName) : null; + } + + static RegisterInfo? TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex index) { - exportInfo = null; foreach (var caHandle in methodDef.GetCustomAttributes ()) { var ca = index.Reader.GetCustomAttribute (caHandle); var attrName = AssemblyIndex.GetCustomAttributeName (ca, index.Reader); if (attrName == "RegisterAttribute") { - registerInfo = index.ParseRegisterAttribute (ca); - return true; + return AssemblyIndex.ParseRegisterAttribute (ca, index.customAttributeTypeProvider); } if (attrName == "ExportAttribute") { - (registerInfo, exportInfo) = ParseExportAttribute (ca, methodDef, index); - return true; + return ParseExportAttribute (ca, methodDef, index); } } - registerInfo = null; - return false; + return null; } static RegisterInfo? TryGetPropertyRegisterInfo (PropertyDefinition propDef, AssemblyIndex index) @@ -320,15 +352,15 @@ static bool TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex var attrName = AssemblyIndex.GetCustomAttributeName (ca, index.Reader); if (attrName == "RegisterAttribute") { - return index.ParseRegisterAttribute (ca); + return AssemblyIndex.ParseRegisterAttribute (ca, index.customAttributeTypeProvider); } } return null; } - static (RegisterInfo registerInfo, ExportInfo exportInfo) ParseExportAttribute (CustomAttribute ca, MethodDefinition methodDef, AssemblyIndex index) + static RegisterInfo ParseExportAttribute (CustomAttribute ca, MethodDefinition methodDef, AssemblyIndex index) { - var value = index.DecodeAttribute (ca); + var value = ca.DecodeValue (index.customAttributeTypeProvider); // [Export("name")] or [Export] (uses method name) string? exportName = null; @@ -343,31 +375,23 @@ static bool TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex foreach (var named in value.NamedArguments) { if (named.Name == "Name" && named.Value is string name) { exportName = name; - } else if (named.Name == "ThrownNames" && named.Value is ImmutableArray> names) { - thrownNames = new List (names.Length); - foreach (var item in names) { - if (item.Value is string s) { - thrownNames.Add (s); - } - } + } else if (named.Name == "ThrownNames" && named.Value is string[] names) { + thrownNames = new List (names); } else if (named.Name == "SuperArgumentsString" && named.Value is string superArgs) { superArguments = superArgs; } } - if (string.IsNullOrEmpty (exportName)) { + if (exportName == null || exportName.Length == 0) { exportName = index.Reader.GetString (methodDef.Name); } - string resolvedExportName = exportName ?? throw new InvalidOperationException ("Export name should not be null at this point."); // Build JNI signature from method signature var sig = methodDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default); var jniSig = BuildJniSignatureFromManaged (sig); - return ( - new RegisterInfo { JniName = resolvedExportName, Signature = jniSig, Connector = null, DoNotGenerateAcw = false }, - new ExportInfo { ThrownNames = thrownNames, SuperArgumentsString = superArguments } - ); + return new RegisterInfo (exportName, jniSig, null, false, + thrownNames: thrownNames, superArgumentsString: superArguments); } static string BuildJniSignatureFromManaged (MethodSignature sig) @@ -401,7 +425,7 @@ static string ManagedTypeToJniDescriptor (string managedType) case "System.String": return "Ljava/lang/String;"; default: if (managedType.EndsWith ("[]")) { - return $"[{ManagedTypeToJniDescriptor (managedType.Substring (0, managedType.Length - 2))}"; + return "[" + ManagedTypeToJniDescriptor (managedType.Substring (0, managedType.Length - 2)); } return "Ljava/lang/Object;"; } @@ -409,28 +433,27 @@ static string ManagedTypeToJniDescriptor (string managedType) ActivationCtorInfo? ResolveActivationCtor (string typeName, TypeDefinition typeDef, AssemblyIndex index) { - var cacheKey = (typeName, index.AssemblyName); - if (_activationCtorCache.TryGetValue (cacheKey, out var cached)) { + if (activationCtorCache.TryGetValue (typeName, out var cached)) { return cached; } // Check this type's constructors var ownCtor = FindActivationCtorOnType (typeDef, index); - if (ownCtor is not null) { + if (ownCtor != null) { var info = new ActivationCtorInfo { DeclaringTypeName = typeName, DeclaringAssemblyName = index.AssemblyName, Style = ownCtor.Value }; - _activationCtorCache [cacheKey] = info; + activationCtorCache [typeName] = info; return info; } // Walk base type hierarchy var baseInfo = GetBaseTypeInfo (typeDef, index); - if (baseInfo is not null) { + if (baseInfo != null) { var (baseTypeName, baseAssemblyName) = baseInfo.Value; if (TryResolveType (baseTypeName, baseAssemblyName, out var baseHandle, out var baseIndex)) { var baseTypeDef = baseIndex.Reader.GetTypeDefinition (baseHandle); var result = ResolveActivationCtor (baseTypeName, baseTypeDef, baseIndex); - if (result is not null) { - _activationCtorCache [cacheKey] = result; + if (result != null) { + activationCtorCache [typeName] = result; } return result; } @@ -498,7 +521,7 @@ static string ManagedTypeToJniDescriptor (string managedType) case 0: { // TypeDef var handle = MetadataTokens.TypeDefinitionHandle (row); var baseDef = index.Reader.GetTypeDefinition (handle); - return (MetadataTypeNameResolver.GetFullName (baseDef, index.Reader), index.AssemblyName); + return (AssemblyIndex.GetFullName (baseDef, index.Reader), index.AssemblyName); } case 1: // TypeRef return ResolveTypeReference (MetadataTokens.TypeReferenceHandle (row), index); @@ -516,7 +539,7 @@ static string ManagedTypeToJniDescriptor (string managedType) switch (handle.Kind) { case HandleKind.TypeDefinition: { var td = index.Reader.GetTypeDefinition ((TypeDefinitionHandle)handle); - return (MetadataTypeNameResolver.GetFullName (td, index.Reader), index.AssemblyName); + return (AssemblyIndex.GetFullName (td, index.Reader), index.AssemblyName); } case HandleKind.TypeReference: return ResolveTypeReference ((TypeReferenceHandle)handle, index); @@ -537,11 +560,13 @@ static string ManagedTypeToJniDescriptor (string managedType) // First, check the [Register] attribute's connector arg (3rd arg). // In real Mono.Android, interfaces have [Register("jni/name", "", "InvokerTypeName, Assembly")] // where the connector contains the assembly-qualified invoker type name. - if (index.RegisterInfoByType.TryGetValue (typeHandle, out var registerInfo) && registerInfo.Connector is not null) { + if (index.RegisterInfoByType.TryGetValue (typeHandle, out var registerInfo) && registerInfo.Connector != null) { var connector = registerInfo.Connector; - var stripped = StripAssemblyQualification (connector); - if (stripped is not null) { - return stripped; + // The connector may be "TypeName" or "TypeName, Assembly, Version=..., Culture=..., PublicKeyToken=..." + // We want just the type name (before the first comma, if any) + var commaIndex = connector.IndexOf (','); + if (commaIndex > 0) { + return connector.Substring (0, commaIndex).Trim (); } if (connector.Length > 0) { return connector; @@ -549,38 +574,22 @@ static string ManagedTypeToJniDescriptor (string managedType) } // Fallback: convention-based lookup — invoker type is TypeName + "Invoker" - var invokerName = $"{typeName}Invoker"; + var invokerName = typeName + "Invoker"; if (index.TypesByFullName.ContainsKey (invokerName)) { return invokerName; } return null; } - List ResolveImplementedInterfaces (TypeDefinition typeDef, AssemblyIndex index) - { - var result = new List (); - foreach (var ifaceImplHandle in typeDef.GetInterfaceImplementations ()) { - var ifaceImpl = index.Reader.GetInterfaceImplementation (ifaceImplHandle); - var resolved = ResolveEntityHandle (ifaceImpl.Interface, index); - if (resolved is not null) { - var ifaceJniName = ResolveRegisterJniName (resolved.Value.typeName, resolved.Value.assemblyName); - if (ifaceJniName is not null) { - result.Add (ifaceJniName); - } - } - } - return result; - } - public void Dispose () { - foreach (var index in _assemblyCache.Values) { + foreach (var index in assemblyCache.Values) { index.Dispose (); } - _assemblyCache.Clear (); + assemblyCache.Clear (); } - readonly Dictionary<(string assemblyName, string fullName), bool> _extendsJavaPeerCache = new (); + readonly Dictionary extendsJavaPeerCache = new (StringComparer.Ordinal); /// /// Check if a type extends a known Java peer (has [Register] or component attribute) @@ -588,18 +597,18 @@ public void Dispose () /// bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) { - var fullName = MetadataTypeNameResolver.GetFullName (typeDef, index.Reader); - var key = (index.AssemblyName, fullName); + var fullName = AssemblyIndex.GetFullName (typeDef, index.Reader); + var key = index.AssemblyName + ":" + fullName; - if (_extendsJavaPeerCache.TryGetValue (key, out var cached)) { + if (extendsJavaPeerCache.TryGetValue (key, out var cached)) { return cached; } // Mark as false to prevent cycles, then compute - _extendsJavaPeerCache [key] = false; + extendsJavaPeerCache [key] = false; var baseInfo = GetBaseTypeInfo (typeDef, index); - if (baseInfo is null) { + if (baseInfo == null) { return false; } @@ -611,18 +620,18 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) // Direct hit: base has [Register] or component attribute if (baseIndex.RegisterInfoByType.ContainsKey (baseHandle)) { - _extendsJavaPeerCache [key] = true; + extendsJavaPeerCache [key] = true; return true; } - if (baseIndex.AttributesByType.ContainsKey (baseHandle)) { - _extendsJavaPeerCache [key] = true; + if (baseIndex.AttributesByType.TryGetValue (baseHandle, out var attrInfo) && attrInfo.HasComponentAttribute) { + extendsJavaPeerCache [key] = true; return true; } // Recurse up the hierarchy var baseDef = baseIndex.Reader.GetTypeDefinition (baseHandle); var result = ExtendsJavaPeer (baseDef, baseIndex); - _extendsJavaPeerCache [key] = result; + extendsJavaPeerCache [key] = result; return result; } @@ -637,17 +646,17 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) { var (typeName, parentJniName, ns) = ComputeTypeNameParts (typeDef, index); - if (parentJniName is not null) { - var name = $"{parentJniName}_{typeName}"; + if (parentJniName != null) { + var name = parentJniName + "_" + typeName; return (name, name); } var packageName = GetCrc64PackageName (ns, index.AssemblyName); - var jniName = $"{packageName}/{typeName}"; + var jniName = packageName + "/" + typeName; string compatName = ns.Length == 0 ? typeName - : $"{ns.ToLowerInvariant ().Replace ('.', '/')}/{typeName}"; + : ns.ToLowerInvariant ().Replace ('.', '/') + "/" + typeName; return (jniName, compatName); } @@ -682,8 +691,8 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) parentJniName = parentRegister.JniName; break; } - if (index.AttributesByType.TryGetValue (parentHandle, out var parentAttr) && parentAttr.JniName is not null) { - parentJniName = parentAttr.JniName; + if (index.AttributesByType.TryGetValue (parentHandle, out var parentAttr) && parentAttr.ComponentAttributeJniName != null) { + parentJniName = parentAttr.ComponentAttributeJniName; break; } @@ -704,22 +713,11 @@ static string GetCrc64PackageName (string ns, string assemblyName) return ns.ToLowerInvariant ().Replace ('.', '/'); } - var data = System.Text.Encoding.UTF8.GetBytes ($"{ns}:{assemblyName}"); + var data = System.Text.Encoding.UTF8.GetBytes (ns + ":" + assemblyName); var hash = System.IO.Hashing.Crc64.Hash (data); - - // "crc64" prefix (5 chars) + 16 hex chars = 21 chars total, single allocation - var chars = new char [5 + hash.Length * 2]; - chars [0] = 'c'; chars [1] = 'r'; chars [2] = 'c'; chars [3] = '6'; chars [4] = '4'; - for (int i = 0; i < hash.Length; i++) { - chars [5 + i * 2] = ToHexChar (hash [i] >> 4); - chars [5 + i * 2 + 1] = ToHexChar (hash [i] & 0xF); - } - return new string (chars); + return "crc64" + BitConverter.ToString (hash).Replace ("-", "").ToLowerInvariant (); } - static char ToHexChar (int nibble) - => (char) (nibble < 10 ? '0' + nibble : 'a' + nibble - 10); - static string ExtractNamespace (string fullName) { int lastDot = fullName.LastIndexOf ('.'); @@ -734,18 +732,4 @@ static string ExtractShortName (string fullName) int lastPlus = typePart.LastIndexOf ('+'); return (lastPlus >= 0 ? typePart.Slice (lastPlus + 1) : typePart).ToString (); } - - /// - /// Strips assembly qualification from a type name like "Ns.Type, Assembly, Version=..." - /// returning just the type name, or null if the input has no comma. - /// - static string? StripAssemblyQualification (string assemblyQualifiedName) - { - var commaIndex = assemblyQualifiedName.IndexOf (','); - if (commaIndex <= 0) { - return null; - } - var typeName = assemblyQualifiedName.Substring (0, commaIndex).Trim (); - return typeName.Length > 0 ? typeName : null; - } } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/SignatureTypeProvider.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/SignatureTypeProvider.cs index 87ed078adf2..185f10c89bd 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/SignatureTypeProvider.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/SignatureTypeProvider.cs @@ -36,10 +36,31 @@ sealed class SignatureTypeProvider : ISignatureTypeProvider }; public string GetTypeFromDefinition (MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) - => MetadataTypeNameResolver.GetTypeFromDefinition (reader, handle, rawTypeKind); + { + var typeDef = reader.GetTypeDefinition (handle); + var ns = reader.GetString (typeDef.Namespace); + var name = reader.GetString (typeDef.Name); + if (typeDef.IsNested) { + var parent = GetTypeFromDefinition (reader, typeDef.GetDeclaringType (), rawTypeKind); + return parent + "+" + name; + } + return ns.Length > 0 ? ns + "." + name : name; + } public string GetTypeFromReference (MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind) - => MetadataTypeNameResolver.GetTypeFromReference (reader, handle, rawTypeKind); + { + var typeRef = reader.GetTypeReference (handle); + var name = reader.GetString (typeRef.Name); + + // Handle nested types: if the ResolutionScope is another TypeReference, resolve recursively + if (typeRef.ResolutionScope.Kind == HandleKind.TypeReference) { + var parent = GetTypeFromReference (reader, (TypeReferenceHandle)typeRef.ResolutionScope, rawTypeKind); + return parent + "+" + name; + } + + var ns = reader.GetString (typeRef.Namespace); + return ns.Length > 0 ? ns + "." + name : name; + } public string GetTypeFromSpecification (MetadataReader reader, object? genericContext, TypeSpecificationHandle handle, byte rawTypeKind) { @@ -47,20 +68,20 @@ public string GetTypeFromSpecification (MetadataReader reader, object? genericCo return typeSpec.DecodeSignature (this, genericContext); } - public string GetSZArrayType (string elementType) => $"{elementType}[]"; - public string GetArrayType (string elementType, ArrayShape shape) => $"{elementType}[{new string (',', shape.Rank - 1)}]"; - public string GetByReferenceType (string elementType) => $"{elementType}&"; - public string GetPointerType (string elementType) => $"{elementType}*"; + public string GetSZArrayType (string elementType) => elementType + "[]"; + public string GetArrayType (string elementType, ArrayShape shape) => elementType + "[" + new string (',', shape.Rank - 1) + "]"; + public string GetByReferenceType (string elementType) => elementType + "&"; + public string GetPointerType (string elementType) => elementType + "*"; public string GetPinnedType (string elementType) => elementType; public string GetModifiedType (string modifier, string unmodifiedType, bool isRequired) => unmodifiedType; public string GetGenericInstantiation (string genericType, ImmutableArray typeArguments) { - return $"{genericType}<{string.Join (",", typeArguments)}>"; + return genericType + "<" + string.Join (",", typeArguments) + ">"; } - public string GetGenericTypeParameter (object? genericContext, int index) => $"!{index}"; - public string GetGenericMethodParameter (object? genericContext, int index) => $"!!{index}"; + public string GetGenericTypeParameter (object? genericContext, int index) => "!" + index; + public string GetGenericMethodParameter (object? genericContext, int index) => "!!" + index; public string GetFunctionPointerType (MethodSignature signature) => "delegate*"; } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs index 70471f62e13..d34708592d4 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs @@ -67,13 +67,13 @@ private protected static JavaPeerInfo MakeMcwPeer (string jniName, string manage private protected static JavaPeerInfo MakePeerWithActivation (string jniName, string managedName, string asmName) { - return MakeMcwPeer (jniName, managedName, asmName) with { - ActivationCtor = new ActivationCtorInfo { - DeclaringTypeName = managedName, - DeclaringAssemblyName = asmName, - Style = ActivationCtorStyle.XamarinAndroid, - }, + var peer = MakeMcwPeer (jniName, managedName, asmName); + peer.ActivationCtor = new ActivationCtorInfo { + DeclaringTypeName = managedName, + DeclaringAssemblyName = asmName, + Style = ActivationCtorStyle.XamarinAndroid, }; + return peer; } private protected static JavaPeerInfo MakeAcwPeer (string jniName, string managedName, string asmName) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 31a06dec2e9..017917d9fbc 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -88,7 +88,8 @@ 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"); + peer.DoNotGenerateAcw = true; var model = BuildModel (new [] { peer }); Assert.True (model.Entries [0].IsUnconditional, $"{jniName} should be unconditional"); } @@ -110,7 +111,8 @@ 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"); + peer.DoNotGenerateAcw = true; var model = BuildModel (new [] { peer }); Assert.Single (model.Entries); @@ -123,10 +125,9 @@ public void Build_McwBinding_IsTrimmable () public void Build_UnconditionalScannedType_IsUnconditional () { // Types with IsUnconditional from scanner (e.g., from [Activity], [Service] attrs) - var peer = MakeMcwPeer ("my/app/MySvc", "MyApp.MyService", "App") with { - DoNotGenerateAcw = true, // simulate MCW-like - IsUnconditional = true, // scanner marked it - }; + var peer = MakeMcwPeer ("my/app/MySvc", "MyApp.MyService", "App"); + peer.DoNotGenerateAcw = true; // simulate MCW-like + peer.IsUnconditional = true; // scanner marked it var model = BuildModel (new [] { peer }); Assert.True (model.Entries [0].IsUnconditional); @@ -386,7 +387,8 @@ 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"); + invokerPeer.DoNotGenerateAcw = true; var model = BuildModel (new [] { ifacePeer, invokerPeer }); @@ -469,7 +471,8 @@ 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"); + invokerPeer.DoNotGenerateAcw = true; // Without a referencing peer, it gets a normal entry var model1 = BuildModel (new [] { invokerPeer }); From edc5ccb97f66775bf4cf7ad0791ed8a47af4d1ad Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 10 Mar 2026 18:26:54 +0100 Subject: [PATCH 37/40] Restore scanner files from origin/main, not stale branch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The scanner PR was already merged to main with record/required/init style. Previous commit mistakenly restored from the stale local scanner branch, causing regressions (record→class, init→set, etc.) visible in the GitHub PR diff. Now scanner files match origin/main exactly, with only +27 lines added: - JavaPeerInfo: ManagedTypeNamespace, ManagedTypeShortName properties - JavaPeerScanner: populate new properties + ExtractNamespace/ExtractShortName Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Scanner/AssemblyIndex.cs | 351 ++++++------------ .../Scanner/CustomAttributeTypeProvider.cs | 120 +++--- .../Scanner/JavaPeerInfo.cs | 58 +-- .../Scanner/JavaPeerScanner.cs | 171 +++++---- .../Scanner/SignatureTypeProvider.cs | 39 +- .../Generator/FixtureTestBase.cs | 12 +- .../Generator/TypeMapModelBuilderTests.cs | 19 +- 7 files changed, 328 insertions(+), 442 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs index 2def0478585..4da7b3d752d 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Reflection.Metadata; using System.Reflection.PortableExecutable; @@ -13,7 +14,7 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; sealed class AssemblyIndex : IDisposable { readonly PEReader peReader; - internal readonly CustomAttributeTypeProvider customAttributeTypeProvider; + readonly CustomAttributeTypeProvider customAttributeTypeProvider; public MetadataReader Reader { get; } public string AssemblyName { get; } @@ -34,18 +35,6 @@ sealed class AssemblyIndex : IDisposable /// public Dictionary AttributesByType { get; } = new (); - /// - /// Type names of attributes that implement Java.Interop.IJniNameProviderAttribute - /// in this assembly. Used to detect JNI name providers without hardcoding attribute names. - /// - public HashSet JniNameProviderAttributes { get; } = new (StringComparer.Ordinal); - - /// - /// Merged set of all JNI name provider attribute type names across all loaded assemblies. - /// Set by after all assemblies are indexed. - /// - HashSet? allJniNameProviderAttributes; - AssemblyIndex (PEReader peReader, MetadataReader reader, string assemblyName, string filePath) { this.peReader = peReader; @@ -58,26 +47,19 @@ sealed class AssemblyIndex : IDisposable public static AssemblyIndex Create (string filePath) { var peReader = new PEReader (File.OpenRead (filePath)); - try { - var reader = peReader.GetMetadataReader (); - var assemblyName = reader.GetString (reader.GetAssemblyDefinition ().Name); - var index = new AssemblyIndex (peReader, reader, assemblyName, filePath); - index.Build (); - return index; - } catch { - peReader.Dispose (); - throw; - } + var reader = peReader.GetMetadataReader (); + var assemblyName = reader.GetString (reader.GetAssemblyDefinition ().Name); + var index = new AssemblyIndex (peReader, reader, assemblyName, filePath); + index.Build (); + return index; } void Build () { - FindJniNameProviderAttributes (); - foreach (var typeHandle in Reader.TypeDefinitions) { var typeDef = Reader.GetTypeDefinition (typeHandle); - var fullName = GetFullName (typeDef, Reader); + var fullName = MetadataTypeNameResolver.GetFullName (typeDef, Reader); if (fullName.Length == 0) { continue; } @@ -86,109 +68,16 @@ void Build () var (registerInfo, attrInfo) = ParseAttributes (typeDef); - if (attrInfo != null) { + if (attrInfo is not null) { AttributesByType [typeHandle] = attrInfo; } - if (registerInfo != null) { + if (registerInfo is not null) { RegisterInfoByType [typeHandle] = registerInfo; } } } - /// - /// Finds all types in this assembly that implement Java.Interop.IJniNameProviderAttribute. - /// - void FindJniNameProviderAttributes () - { - foreach (var typeHandle in Reader.TypeDefinitions) { - var typeDef = Reader.GetTypeDefinition (typeHandle); - if (ImplementsIJniNameProviderAttribute (typeDef)) { - var name = Reader.GetString (typeDef.Name); - JniNameProviderAttributes.Add (name); - } - } - } - - bool ImplementsIJniNameProviderAttribute (TypeDefinition typeDef) - { - foreach (var implHandle in typeDef.GetInterfaceImplementations ()) { - var impl = Reader.GetInterfaceImplementation (implHandle); - if (impl.Interface.Kind == HandleKind.TypeReference) { - var typeRef = Reader.GetTypeReference ((TypeReferenceHandle)impl.Interface); - var name = Reader.GetString (typeRef.Name); - var ns = Reader.GetString (typeRef.Namespace); - if (name == "IJniNameProviderAttribute" && ns == "Java.Interop") { - return true; - } - } else if (impl.Interface.Kind == HandleKind.TypeDefinition) { - var ifaceTypeDef = Reader.GetTypeDefinition ((TypeDefinitionHandle)impl.Interface); - var name = Reader.GetString (ifaceTypeDef.Name); - var ns = Reader.GetString (ifaceTypeDef.Namespace); - if (name == "IJniNameProviderAttribute" && ns == "Java.Interop") { - return true; - } - } - } - return false; - } - - /// - /// Sets the merged set of JNI name provider attributes from all loaded assemblies - /// and re-classifies any attributes that weren't recognized in the initial pass. - /// - public void ReclassifyAttributes (HashSet mergedJniNameProviders) - { - allJniNameProviderAttributes = mergedJniNameProviders; - - foreach (var typeHandle in Reader.TypeDefinitions) { - var typeDef = Reader.GetTypeDefinition (typeHandle); - - // Skip types that already have component attribute info - if (AttributesByType.TryGetValue (typeHandle, out var existing) && existing.HasComponentAttribute) { - continue; - } - - // Re-check custom attributes with the full set of known providers - foreach (var caHandle in typeDef.GetCustomAttributes ()) { - var ca = Reader.GetCustomAttribute (caHandle); - var attrName = GetCustomAttributeName (ca, Reader); - - if (attrName == null || attrName == "RegisterAttribute" || attrName == "ExportAttribute") { - continue; - } - - if (mergedJniNameProviders.Contains (attrName) && !IsKnownComponentAttribute (attrName)) { - var componentName = TryGetNameProperty (ca); - if (componentName != null) { - var attrInfo = existing ?? new TypeAttributeInfo (); - attrInfo.HasComponentAttribute = true; - attrInfo.ComponentAttributeJniName = componentName.Replace ('.', '/'); - AttributesByType [typeHandle] = attrInfo; - } - } - } - } - } - - internal static string GetFullName (TypeDefinition typeDef, MetadataReader reader) - { - var name = reader.GetString (typeDef.Name); - var ns = reader.GetString (typeDef.Namespace); - - if (typeDef.IsNested) { - var declaringType = reader.GetTypeDefinition (typeDef.GetDeclaringType ()); - var parentName = GetFullName (declaringType, reader); - return parentName + "+" + name; - } - - if (ns.Length == 0) { - return name; - } - - return ns + "." + name; - } - (RegisterInfo? register, TypeAttributeInfo? attrs) ParseAttributes (TypeDefinition typeDef) { RegisterInfo? registerInfo = null; @@ -198,24 +87,30 @@ internal static string GetFullName (TypeDefinition typeDef, MetadataReader reade var ca = Reader.GetCustomAttribute (caHandle); var attrName = GetCustomAttributeName (ca, Reader); - if (attrName == null) { + if (attrName is null) { continue; } if (attrName == "RegisterAttribute") { - registerInfo = ParseRegisterAttribute (ca, customAttributeTypeProvider); + registerInfo = ParseRegisterAttribute (ca); } else if (attrName == "ExportAttribute") { - // [Export] methods are detected per-method in CollectMarshalMethods - } else if (IsJniNameProviderAttribute (attrName)) { - attrInfo ??= new TypeAttributeInfo (); - attrInfo.HasComponentAttribute = true; - var componentName = TryGetNameProperty (ca); - if (componentName != null) { - attrInfo.ComponentAttributeJniName = componentName.Replace ('.', '/'); + // [Export] is a method-level attribute; it is parsed at scan time by JavaPeerScanner + } else if (IsKnownComponentAttribute (attrName)) { + attrInfo ??= CreateTypeAttributeInfo (attrName); + var name = TryGetNameProperty (ca); + if (name is not null) { + attrInfo.JniName = name.Replace ('.', '/'); + } + if (attrInfo is ApplicationAttributeInfo applicationAttributeInfo) { + applicationAttributeInfo.BackupAgent = TryGetTypeProperty (ca, "BackupAgent"); + applicationAttributeInfo.ManageSpaceActivity = TryGetTypeProperty (ca, "ManageSpaceActivity"); } - if (attrName == "ApplicationAttribute") { - attrInfo.ApplicationBackupAgent = TryGetTypeProperty (ca, "BackupAgent"); - attrInfo.ApplicationManageSpaceActivity = TryGetTypeProperty (ca, "ManageSpaceActivity"); + } else if (attrInfo is null && ImplementsJniNameProviderAttribute (ca)) { + // Custom attribute implementing IJniNameProviderAttribute (e.g., user-defined [CustomJniName]) + var name = TryGetNameProperty (ca); + if (name is not null) { + attrInfo = new TypeAttributeInfo (attrName); + attrInfo.JniName = name.Replace ('.', '/'); } } } @@ -223,35 +118,52 @@ internal static string GetFullName (TypeDefinition typeDef, MetadataReader reade return (registerInfo, attrInfo); } + static readonly HashSet KnownComponentAttributes = new (StringComparer.Ordinal) { + "ActivityAttribute", + "ServiceAttribute", + "BroadcastReceiverAttribute", + "ContentProviderAttribute", + "ApplicationAttribute", + "InstrumentationAttribute", + }; + + static TypeAttributeInfo CreateTypeAttributeInfo (string attrName) + { + return attrName == "ApplicationAttribute" + ? new ApplicationAttributeInfo () + : new TypeAttributeInfo (attrName); + } + + static bool IsKnownComponentAttribute (string attrName) => KnownComponentAttributes.Contains (attrName); + /// - /// Checks if an attribute type name is a known IJniNameProviderAttribute implementor. - /// Uses the local set first (from this assembly), then falls back to the merged set - /// (populated after all assemblies are loaded), then falls back to hardcoded names - /// for the well-known Android component attributes. + /// Checks whether a custom attribute's type implements Java.Interop.IJniNameProviderAttribute. + /// Only works for attributes defined in the assembly being scanned (MethodDefinition constructors). /// - bool IsJniNameProviderAttribute (string attrName) + bool ImplementsJniNameProviderAttribute (CustomAttribute ca) { - if (JniNameProviderAttributes.Contains (attrName)) { - return true; + if (ca.Constructor.Kind != HandleKind.MethodDefinition) { + return false; } - - if (allJniNameProviderAttributes != null && allJniNameProviderAttributes.Contains (attrName)) { - return true; + var methodDef = Reader.GetMethodDefinition ((MethodDefinitionHandle)ca.Constructor); + var typeDef = Reader.GetTypeDefinition (methodDef.GetDeclaringType ()); + foreach (var implHandle in typeDef.GetInterfaceImplementations ()) { + var impl = Reader.GetInterfaceImplementation (implHandle); + if (impl.Interface.Kind == HandleKind.TypeReference) { + var typeRef = Reader.GetTypeReference ((TypeReferenceHandle)impl.Interface); + if (Reader.GetString (typeRef.Name) == "IJniNameProviderAttribute" && + Reader.GetString (typeRef.Namespace) == "Java.Interop") { + return true; + } + } else if (impl.Interface.Kind == HandleKind.TypeDefinition) { + var ifaceDef = Reader.GetTypeDefinition ((TypeDefinitionHandle)impl.Interface); + if (Reader.GetString (ifaceDef.Name) == "IJniNameProviderAttribute" && + Reader.GetString (ifaceDef.Namespace) == "Java.Interop") { + return true; + } + } } - - // Fallback for the case where we haven't loaded the assembly defining the attribute yet. - // This covers the common case where user assemblies reference Mono.Android attributes. - return IsKnownComponentAttribute (attrName); - } - - static bool IsKnownComponentAttribute (string attrName) - { - return attrName == "ActivityAttribute" - || attrName == "ServiceAttribute" - || attrName == "BroadcastReceiverAttribute" - || attrName == "ContentProviderAttribute" - || attrName == "ApplicationAttribute" - || attrName == "InstrumentationAttribute"; + return false; } internal static string? GetCustomAttributeName (CustomAttribute ca, MetadataReader reader) @@ -270,9 +182,18 @@ static bool IsKnownComponentAttribute (string attrName) return null; } - internal static RegisterInfo ParseRegisterAttribute (CustomAttribute ca, ICustomAttributeTypeProvider provider) + internal RegisterInfo ParseRegisterAttribute (CustomAttribute ca) + { + return ParseRegisterInfo (DecodeAttribute (ca)); + } + + internal CustomAttributeValue DecodeAttribute (CustomAttribute ca) + { + return ca.DecodeValue (customAttributeTypeProvider); + } + + RegisterInfo ParseRegisterInfo (CustomAttributeValue value) { - var value = ca.DecodeValue (provider); string jniName = ""; string? signature = null; @@ -289,18 +210,22 @@ internal static RegisterInfo ParseRegisterAttribute (CustomAttribute ca, ICustom connector = (string?)value.FixedArguments [2].Value; } - if (TryGetNamedBooleanArgument (value, "DoNotGenerateAcw", out var doNotGenerateAcwValue)) { + if (TryGetNamedArgument (value, "DoNotGenerateAcw", out var doNotGenerateAcwValue)) { doNotGenerateAcw = doNotGenerateAcwValue; } - return new RegisterInfo (jniName, signature, connector, doNotGenerateAcw); + return new RegisterInfo { + JniName = jniName, + Signature = signature, + Connector = connector, + DoNotGenerateAcw = doNotGenerateAcw, + }; } string? TryGetTypeProperty (CustomAttribute ca, string propertyName) { - var value = ca.DecodeValue (customAttributeTypeProvider); - var typeName = TryGetNamedStringArgument (value, propertyName); - if (!string.IsNullOrEmpty (typeName)) { + var value = DecodeAttribute (ca); + if (TryGetNamedArgument (value, propertyName, out var typeName) && !string.IsNullOrEmpty (typeName)) { return typeName; } return null; @@ -308,14 +233,13 @@ internal static RegisterInfo ParseRegisterAttribute (CustomAttribute ca, ICustom string? TryGetNameProperty (CustomAttribute ca) { - var value = ca.DecodeValue (customAttributeTypeProvider); - - // Check named arguments first (e.g., [Activity(Name = "...")]) - var name = TryGetNamedStringArgument (value, "Name"); + var name = TryGetTypeProperty (ca, "Name"); if (!string.IsNullOrEmpty (name)) { return name; } + var value = DecodeAttribute (ca); + // Fall back to first constructor argument (e.g., [CustomJniName("...")]) if (value.FixedArguments.Length > 0 && value.FixedArguments [0].Value is string ctorName && !string.IsNullOrEmpty (ctorName)) { return ctorName; @@ -324,30 +248,18 @@ internal static RegisterInfo ParseRegisterAttribute (CustomAttribute ca, ICustom return null; } - static bool TryGetNamedBooleanArgument (CustomAttributeValue value, string argumentName, out bool argumentValue) + static bool TryGetNamedArgument (CustomAttributeValue value, string argumentName, [MaybeNullWhen (false)] out T argumentValue) where T : notnull { foreach (var named in value.NamedArguments) { - if (named.Name == argumentName && named.Value is bool boolValue) { - argumentValue = boolValue; + if (named.Name == argumentName && named.Value is T typedValue) { + argumentValue = typedValue; return true; } } - - argumentValue = false; + argumentValue = default; return false; } - static string? TryGetNamedStringArgument (CustomAttributeValue value, string argumentName) - { - foreach (var named in value.NamedArguments) { - if (named.Name == argumentName && named.Value is string stringValue) { - return stringValue; - } - } - - return null; - } - public void Dispose () { peReader.Dispose (); @@ -355,64 +267,33 @@ public void Dispose () } /// -/// Parsed [Register] or [Export] attribute data for a type or method. +/// Parsed [Register] attribute data for a type or method. /// -sealed class RegisterInfo +sealed record RegisterInfo { - public string JniName { get; } - public string? Signature { get; } - public string? Connector { get; } - public bool DoNotGenerateAcw { get; } - - /// - /// For [Export] methods: Java exception type names the method declares it can throw. - /// - public IReadOnlyList? ThrownNames { get; } - - /// - /// For [Export] methods: super constructor arguments string. - /// - public string? SuperArgumentsString { get; } - - public RegisterInfo (string jniName, string? signature, string? connector, bool doNotGenerateAcw, - IReadOnlyList? thrownNames = null, string? superArgumentsString = null) - { - JniName = jniName; - Signature = signature; - Connector = connector; - DoNotGenerateAcw = doNotGenerateAcw; - ThrownNames = thrownNames; - SuperArgumentsString = superArgumentsString; - } + public required string JniName { get; init; } + public string? Signature { get; init; } + public string? Connector { get; init; } + public bool DoNotGenerateAcw { get; init; } } /// -/// Aggregated attribute information for a type, beyond [Register]. +/// Parsed [Export] attribute data for a method. /// -sealed class TypeAttributeInfo +sealed record ExportInfo { - /// - /// Type has [Activity], [Service], [BroadcastReceiver], [ContentProvider], - /// [Application], or [Instrumentation]. - /// - public bool HasComponentAttribute { get; set; } - - /// - /// The JNI name from the Name property of a component attribute - /// (e.g., [Activity(Name = "my.app.MainActivity")] → "my/app/MainActivity"). - /// Null if no Name was specified on the component attribute. - /// - public string? ComponentAttributeJniName { get; set; } + public IReadOnlyList? ThrownNames { get; init; } + public string? SuperArgumentsString { get; init; } +} - /// - /// If the type has [Application(BackupAgent = typeof(X))], - /// this is the full name of X. - /// - public string? ApplicationBackupAgent { get; set; } +class TypeAttributeInfo (string attributeName) +{ + public string AttributeName { get; } = attributeName; + public string? JniName { get; set; } +} - /// - /// If the type has [Application(ManageSpaceActivity = typeof(X))], - /// this is the full name of X. - /// - public string? ApplicationManageSpaceActivity { get; set; } +sealed class ApplicationAttributeInfo () : TypeAttributeInfo ("ApplicationAttribute") +{ + public string? BackupAgent { get; set; } + public string? ManageSpaceActivity { get; set; } } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/CustomAttributeTypeProvider.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/CustomAttributeTypeProvider.cs index 2152e557bb8..f3441d1a475 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/CustomAttributeTypeProvider.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/CustomAttributeTypeProvider.cs @@ -1,5 +1,5 @@ using System; -using System.Collections.Immutable; +using System.Collections.Generic; using System.Reflection.Metadata; namespace Microsoft.Android.Sdk.TrimmableTypeMap; @@ -8,83 +8,95 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// Minimal ICustomAttributeTypeProvider implementation for decoding /// custom attribute values via System.Reflection.Metadata. /// -sealed class CustomAttributeTypeProvider : ICustomAttributeTypeProvider +sealed class CustomAttributeTypeProvider (MetadataReader reader) : ICustomAttributeTypeProvider { - readonly MetadataReader reader; - - public CustomAttributeTypeProvider (MetadataReader reader) - { - this.reader = reader; - } + Dictionary? enumTypeCache; public string GetPrimitiveType (PrimitiveTypeCode typeCode) => typeCode.ToString (); public string GetTypeFromDefinition (MetadataReader metadataReader, TypeDefinitionHandle handle, byte rawTypeKind) - { - var typeDef = metadataReader.GetTypeDefinition (handle); - var name = metadataReader.GetString (typeDef.Name); - if (typeDef.IsNested) { - var parent = GetTypeFromDefinition (metadataReader, typeDef.GetDeclaringType (), rawTypeKind); - return parent + "+" + name; - } - var ns = metadataReader.GetString (typeDef.Namespace); - return ns.Length > 0 ? ns + "." + name : name; - } + => MetadataTypeNameResolver.GetTypeFromDefinition (metadataReader, handle, rawTypeKind); public string GetTypeFromReference (MetadataReader metadataReader, TypeReferenceHandle handle, byte rawTypeKind) - { - var typeRef = metadataReader.GetTypeReference (handle); - var name = metadataReader.GetString (typeRef.Name); - if (typeRef.ResolutionScope.Kind == HandleKind.TypeReference) { - var parent = GetTypeFromReference (metadataReader, (TypeReferenceHandle)typeRef.ResolutionScope, rawTypeKind); - return parent + "+" + name; - } - var ns = metadataReader.GetString (typeRef.Namespace); - return ns.Length > 0 ? ns + "." + name : name; - } + => MetadataTypeNameResolver.GetTypeFromReference (metadataReader, handle, rawTypeKind); public string GetTypeFromSerializedName (string name) => name; public PrimitiveTypeCode GetUnderlyingEnumType (string type) { - // Find the enum type in this assembly's metadata and read its value__ field type. + if (enumTypeCache == null) { + enumTypeCache = BuildEnumTypeCache (); + } + + if (enumTypeCache.TryGetValue (type, out var code)) { + return code; + } + + // Default to Int32 for enums defined in other assemblies + return PrimitiveTypeCode.Int32; + } + + Dictionary BuildEnumTypeCache () + { + var cache = new Dictionary (); + foreach (var typeHandle in reader.TypeDefinitions) { var typeDef = reader.GetTypeDefinition (typeHandle); - var name = reader.GetString (typeDef.Name); - var ns = reader.GetString (typeDef.Namespace); - var fullName = ns.Length > 0 ? ns + "." + name : name; - if (fullName != type) + // Only process enum types + if (!IsEnum (typeDef)) continue; - // For enums, the first instance field is the underlying value__ field - foreach (var fieldHandle in typeDef.GetFields ()) { - var field = reader.GetFieldDefinition (fieldHandle); - if ((field.Attributes & System.Reflection.FieldAttributes.Static) != 0) - continue; - - var sig = field.DecodeSignature (SignatureTypeProvider.Instance, genericContext: null); - return sig switch { - "System.Byte" => PrimitiveTypeCode.Byte, - "System.SByte" => PrimitiveTypeCode.SByte, - "System.Int16" => PrimitiveTypeCode.Int16, - "System.UInt16" => PrimitiveTypeCode.UInt16, - "System.Int32" => PrimitiveTypeCode.Int32, - "System.UInt32" => PrimitiveTypeCode.UInt32, - "System.Int64" => PrimitiveTypeCode.Int64, - "System.UInt64" => PrimitiveTypeCode.UInt64, - _ => PrimitiveTypeCode.Int32, - }; - } + var fullName = GetTypeFromDefinition (reader, typeHandle, rawTypeKind: 0); + cache [fullName] = GetEnumUnderlyingTypeCode (typeDef); + } + + return cache; + } + + bool IsEnum (TypeDefinition typeDef) + { + var baseType = typeDef.BaseType; + if (baseType.IsNil) + return false; + + string? baseFullName = baseType.Kind switch { + HandleKind.TypeReference => GetTypeFromReference (reader, (TypeReferenceHandle)baseType, rawTypeKind: 0), + HandleKind.TypeDefinition => GetTypeFromDefinition (reader, (TypeDefinitionHandle)baseType, rawTypeKind: 0), + _ => null, + }; + + return baseFullName == "System.Enum"; + } + + PrimitiveTypeCode GetEnumUnderlyingTypeCode (TypeDefinition typeDef) + { + // For enums, the first instance field is the underlying value__ field + foreach (var fieldHandle in typeDef.GetFields ()) { + var field = reader.GetFieldDefinition (fieldHandle); + if ((field.Attributes & System.Reflection.FieldAttributes.Static) != 0) + continue; + + var sig = field.DecodeSignature (SignatureTypeProvider.Instance, genericContext: null); + return sig switch { + "System.Byte" => PrimitiveTypeCode.Byte, + "System.SByte" => PrimitiveTypeCode.SByte, + "System.Int16" => PrimitiveTypeCode.Int16, + "System.UInt16" => PrimitiveTypeCode.UInt16, + "System.Int32" => PrimitiveTypeCode.Int32, + "System.UInt32" => PrimitiveTypeCode.UInt32, + "System.Int64" => PrimitiveTypeCode.Int64, + "System.UInt64" => PrimitiveTypeCode.UInt64, + _ => PrimitiveTypeCode.Int32, + }; } - // Default to Int32 for enums defined in other assemblies return PrimitiveTypeCode.Int32; } public string GetSystemType () => "System.Type"; - public string GetSZArrayType (string elementType) => elementType + "[]"; + public string GetSZArrayType (string elementType) => $"{elementType}[]"; public bool IsSystemType (string type) => type == "System.Type" || type == "Type"; } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index 67564c61f00..c34d7f2009c 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -8,69 +8,69 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// Contains all data needed by downstream generators (TypeMap IL, UCO wrappers, JCW Java sources). /// Generators consume this data model — they never touch PEReader/MetadataReader. /// -sealed class JavaPeerInfo +sealed record JavaPeerInfo { /// /// JNI type name, e.g., "android/app/Activity". /// Extracted from the [Register] attribute. /// - public string JavaName { get; set; } = ""; + public required string JavaName { get; init; } /// /// Compat JNI type name, e.g., "myapp.namespace/MyType" for user types (uses raw namespace, not CRC64). /// For MCW binding types (with [Register]), this equals . /// Used by acw-map.txt to support legacy custom view name resolution in layout XMLs. /// - public string CompatJniName { get; set; } = ""; + public required string CompatJniName { get; init; } /// /// Full managed type name, e.g., "Android.App.Activity". /// - public string ManagedTypeName { get; set; } = ""; + public required string ManagedTypeName { get; init; } /// /// Managed type namespace, e.g., "Android.App". /// - public string ManagedTypeNamespace { get; set; } = ""; + public string ManagedTypeNamespace { get; init; } = ""; /// /// Managed type short name (without namespace), e.g., "Activity". /// - public string ManagedTypeShortName { get; set; } = ""; + public string ManagedTypeShortName { get; init; } = ""; /// /// Assembly name the type belongs to, e.g., "Mono.Android". /// - public string AssemblyName { get; set; } = ""; + public required string AssemblyName { get; init; } /// /// JNI name of the base Java type, e.g., "android/app/Activity" for a type /// that extends Activity. Null for java/lang/Object or types without a Java base. /// Needed by JCW Java source generation ("extends" clause). /// - public string? BaseJavaName { get; set; } + public string? BaseJavaName { get; init; } /// /// JNI names of Java interfaces this type implements, e.g., ["android/view/View$OnClickListener"]. /// Needed by JCW Java source generation ("implements" clause). /// - public IReadOnlyList ImplementedInterfaceJavaNames { get; set; } = Array.Empty (); + public IReadOnlyList ImplementedInterfaceJavaNames { get; init; } = Array.Empty (); - public bool IsInterface { get; set; } - public bool IsAbstract { get; set; } + public bool IsInterface { get; init; } + public bool IsAbstract { get; init; } /// /// If true, this is a Managed Callable Wrapper (MCW) binding type. /// No JCW or RegisterNatives will be generated for it. /// - public bool DoNotGenerateAcw { get; set; } + public bool DoNotGenerateAcw { get; init; } /// /// Types with component attributes ([Activity], [Service], etc.), /// custom views from layout XML, or manifest-declared components /// are unconditionally preserved (not trimmable). /// - public bool IsUnconditional { get; set; } + public bool IsUnconditional { get; init; } /// /// Marshal methods: methods with [Register(name, sig, connector)], [Export], or @@ -78,25 +78,25 @@ sealed class JavaPeerInfo /// Constructors are identified by . /// Ordered — the index in this list is the method's ordinal for RegisterNatives. /// - public IReadOnlyList MarshalMethods { get; set; } = Array.Empty (); + public IReadOnlyList MarshalMethods { 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. /// - public ActivationCtorInfo? ActivationCtor { get; set; } + public ActivationCtorInfo? ActivationCtor { get; init; } /// /// For interfaces and abstract types, the name of the invoker type /// used to instantiate instances from Java. /// - public string? InvokerTypeName { get; set; } + public string? InvokerTypeName { get; init; } /// /// True if this is an open generic type definition. /// Generic types get TypeMap entries but CreateInstance throws NotSupportedException. /// - public bool IsGenericDefinition { get; set; } + public bool IsGenericDefinition { get; init; } } /// @@ -104,69 +104,69 @@ sealed class JavaPeerInfo /// Contains all data needed to generate a UCO wrapper, a JCW native declaration, /// and a RegisterNatives call. /// -sealed class MarshalMethodInfo +sealed record MarshalMethodInfo { /// /// JNI method name, e.g., "onCreate". /// This is the Java method name (without n_ prefix). /// - public string JniName { get; set; } = ""; + public required string JniName { get; init; } /// /// JNI method signature, e.g., "(Landroid/os/Bundle;)V". /// Contains both parameter types and return type. /// - public string JniSignature { get; set; } = ""; + public required string JniSignature { get; init; } /// /// The connector string from [Register], e.g., "GetOnCreate_Landroid_os_Bundle_Handler". /// Null for [Export] methods. /// - public string? Connector { get; set; } + public string? Connector { get; init; } /// /// Name of the managed method this maps to, e.g., "OnCreate". /// - public string ManagedMethodName { get; set; } = ""; + public required string ManagedMethodName { get; init; } /// /// True if this is a constructor registration. /// - public bool IsConstructor { get; set; } + public bool IsConstructor { get; init; } /// /// For [Export] methods: Java exception types that the method declares it can throw. /// Null for [Register] methods. /// - public IReadOnlyList? ThrownNames { get; set; } + public IReadOnlyList? ThrownNames { get; init; } /// /// For [Export] methods: super constructor arguments string. /// Null for [Register] methods. /// - public string? SuperArgumentsString { get; set; } + public string? SuperArgumentsString { get; init; } } /// /// Describes how to call the activation constructor for a Java peer type. /// -sealed class ActivationCtorInfo +sealed record ActivationCtorInfo { /// /// The type that declares the activation constructor. /// May be the type itself or a base type. /// - public string DeclaringTypeName { get; set; } = ""; + public required string DeclaringTypeName { get; init; } /// /// The assembly containing the declaring type. /// - public string DeclaringAssemblyName { get; set; } = ""; + public required string DeclaringAssemblyName { get; init; } /// /// The style of activation constructor found. /// - public ActivationCtorStyle Style { get; set; } + public required ActivationCtorStyle Style { get; init; } } enum ActivationCtorStyle diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 785453ecf87..921532881fc 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; @@ -15,20 +17,20 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; sealed class JavaPeerScanner : IDisposable { readonly Dictionary assemblyCache = new (StringComparer.Ordinal); - readonly Dictionary activationCtorCache = new (StringComparer.Ordinal); + readonly Dictionary<(string typeName, string assemblyName), ActivationCtorInfo> activationCtorCache = new (); /// /// Resolves a type name + assembly name to a TypeDefinitionHandle + AssemblyIndex. /// Checks the specified assembly (by name) in the assembly cache. /// - bool TryResolveType (string typeName, string assemblyName, out TypeDefinitionHandle handle, out AssemblyIndex resolvedIndex) + bool TryResolveType (string typeName, string assemblyName, out TypeDefinitionHandle handle, [NotNullWhen (true)] out AssemblyIndex? resolvedIndex) { - if (assemblyCache.TryGetValue (assemblyName, out resolvedIndex!) && + if (assemblyCache.TryGetValue (assemblyName, out resolvedIndex) && resolvedIndex.TypesByFullName.TryGetValue (typeName, out handle)) { return true; } handle = default; - resolvedIndex = null!; + resolvedIndex = null; return false; } @@ -46,16 +48,16 @@ bool TryResolveType (string typeName, string assemblyName, out TypeDefinitionHan switch (scope.Kind) { case HandleKind.AssemblyReference: { var asmRef = index.Reader.GetAssemblyReference ((AssemblyReferenceHandle)scope); - var fullName = ns.Length > 0 ? ns + "." + name : name; + var fullName = MetadataTypeNameResolver.JoinNamespaceAndName (ns, name); return (fullName, index.Reader.GetString (asmRef.Name)); } case HandleKind.TypeReference: { // Nested type: recurse to get the declaring type's full name and assembly var (parentFullName, assemblyName) = ResolveTypeReference ((TypeReferenceHandle)scope, index); - return (parentFullName + "+" + name, assemblyName); + return (MetadataTypeNameResolver.JoinNestedTypeName (parentFullName, name), assemblyName); } default: { - var fullName = ns.Length > 0 ? ns + "." + name : name; + var fullName = MetadataTypeNameResolver.JoinNamespaceAndName (ns, name); return (fullName, index.AssemblyName); } } @@ -85,17 +87,6 @@ public List Scan (IReadOnlyList assemblyPaths) assemblyCache [index.AssemblyName] = index; } - // Phase 1b: Merge IJniNameProviderAttribute implementor sets from all assemblies - // and re-classify any attributes that weren't recognized in the initial pass - // (e.g., user assembly references ActivityAttribute from Mono.Android.dll). - var mergedJniNameProviders = new HashSet (StringComparer.Ordinal); - foreach (var index in assemblyCache.Values) { - mergedJniNameProviders.UnionWith (index.JniNameProviderAttributes); - } - foreach (var index in assemblyCache.Values) { - index.ReclassifyAttributes (mergedJniNameProviders); - } - // Phase 2: Analyze types using cached indices var resultsByManagedName = new Dictionary (StringComparer.Ordinal); @@ -118,27 +109,41 @@ static void ForceUnconditionalCrossReferences (Dictionary { foreach (var index in assemblyCache.Values) { foreach (var attrInfo in index.AttributesByType.Values) { - ForceUnconditionalIfPresent (resultsByManagedName, attrInfo.ApplicationBackupAgent); - ForceUnconditionalIfPresent (resultsByManagedName, attrInfo.ApplicationManageSpaceActivity); + if (attrInfo is ApplicationAttributeInfo applicationAttributeInfo) { + ForceUnconditionalIfPresent (resultsByManagedName, applicationAttributeInfo.BackupAgent); + ForceUnconditionalIfPresent (resultsByManagedName, applicationAttributeInfo.ManageSpaceActivity); + } } } } static void ForceUnconditionalIfPresent (Dictionary resultsByManagedName, string? managedTypeName) { - if (managedTypeName == null) { + if (managedTypeName is null) { + return; + } + + managedTypeName = managedTypeName.Trim (); + if (managedTypeName.Length == 0) { + return; + } + + // Try exact match first (handles both plain and assembly-qualified names) + if (resultsByManagedName.TryGetValue (managedTypeName, out var peer)) { + resultsByManagedName [managedTypeName] = peer with { IsUnconditional = true }; return; } // TryGetTypeProperty may return assembly-qualified names like "Ns.Type, Assembly, ..." // Strip to just the type name for lookup var commaIndex = managedTypeName.IndexOf (','); - if (commaIndex > 0) { - managedTypeName = managedTypeName.Substring (0, commaIndex).Trim (); + if (commaIndex <= 0) { + return; } - if (resultsByManagedName.TryGetValue (managedTypeName, out var peer)) { - peer.IsUnconditional = true; + var typeName = managedTypeName.Substring (0, commaIndex).Trim (); + if (typeName.Length > 0 && resultsByManagedName.TryGetValue (typeName, out peer)) { + resultsByManagedName [typeName] = peer with { IsUnconditional = true }; } } @@ -165,13 +170,13 @@ void ScanAssembly (AssemblyIndex index, Dictionary results index.RegisterInfoByType.TryGetValue (typeHandle, out var registerInfo); index.AttributesByType.TryGetValue (typeHandle, out var attrInfo); - if (registerInfo != null && !string.IsNullOrEmpty (registerInfo.JniName)) { + if (registerInfo is not null && !string.IsNullOrEmpty (registerInfo.JniName)) { jniName = registerInfo.JniName; compatJniName = jniName; doNotGenerateAcw = registerInfo.DoNotGenerateAcw; - } else if (attrInfo?.ComponentAttributeJniName != null) { + } else if (attrInfo?.JniName is not null) { // User type with [Activity(Name = "...")] but no [Register] - jniName = attrInfo.ComponentAttributeJniName; + jniName = attrInfo.JniName; compatJniName = jniName; } else { // No explicit JNI name — check if this type extends a known Java peer. @@ -183,13 +188,13 @@ void ScanAssembly (AssemblyIndex index, Dictionary results } } - var fullName = AssemblyIndex.GetFullName (typeDef, index.Reader); + var fullName = MetadataTypeNameResolver.GetFullName (typeDef, index.Reader); var isInterface = (typeDef.Attributes & TypeAttributes.Interface) != 0; var isAbstract = (typeDef.Attributes & TypeAttributes.Abstract) != 0; var isGenericDefinition = typeDef.GetGenericParameters ().Count > 0; - var isUnconditional = attrInfo?.HasComponentAttribute ?? false; + var isUnconditional = attrInfo is not null; string? invokerTypeName = null; // Resolve base Java type name @@ -239,19 +244,18 @@ List CollectMarshalMethods (TypeDefinition typeDef, AssemblyI // Single pass over methods: collect marshal methods (including constructors) foreach (var methodHandle in typeDef.GetMethods ()) { var methodDef = index.Reader.GetMethodDefinition (methodHandle); - var registerInfo = TryGetMethodRegisterInfo (methodDef, index); - if (registerInfo == null) { + if (!TryGetMethodRegisterInfo (methodDef, index, out var registerInfo, out var exportInfo) || registerInfo is null) { continue; } - AddMarshalMethod (methods, registerInfo, methodDef, index); + AddMarshalMethod (methods, registerInfo, methodDef, index, exportInfo); } // Collect [Register] from properties (attribute is on the property, not the getter) foreach (var propHandle in typeDef.GetProperties ()) { var propDef = index.Reader.GetPropertyDefinition (propHandle); var propRegister = TryGetPropertyRegisterInfo (propDef, index); - if (propRegister == null) { + if (propRegister is null) { continue; } @@ -265,10 +269,10 @@ List CollectMarshalMethods (TypeDefinition typeDef, AssemblyI return methods; } - static void AddMarshalMethod (List methods, RegisterInfo registerInfo, MethodDefinition methodDef, AssemblyIndex index) + static void AddMarshalMethod (List methods, RegisterInfo registerInfo, MethodDefinition methodDef, AssemblyIndex index, ExportInfo? exportInfo = null) { // Skip methods that are just the JNI name (type-level [Register]) - if (registerInfo.Signature == null && registerInfo.Connector == null) { + if (registerInfo.Signature is null && registerInfo.Connector is null) { return; } @@ -278,15 +282,15 @@ static void AddMarshalMethod (List methods, RegisterInfo regi Connector = registerInfo.Connector, ManagedMethodName = index.Reader.GetString (methodDef.Name), IsConstructor = registerInfo.JniName == "" || registerInfo.JniName == ".ctor", - ThrownNames = registerInfo.ThrownNames, - SuperArgumentsString = registerInfo.SuperArgumentsString, + ThrownNames = exportInfo?.ThrownNames, + SuperArgumentsString = exportInfo?.SuperArgumentsString, }); } string? ResolveBaseJavaName (TypeDefinition typeDef, AssemblyIndex index, Dictionary results) { var baseInfo = GetBaseTypeInfo (typeDef, index); - if (baseInfo == null) { + if (baseInfo is null) { return null; } @@ -294,7 +298,7 @@ static void AddMarshalMethod (List methods, RegisterInfo regi // First try [Register] attribute var registerJniName = ResolveRegisterJniName (baseTypeName, baseAssemblyName); - if (registerJniName != null) { + if (registerJniName is not null) { return registerJniName; } @@ -314,7 +318,7 @@ List ResolveImplementedInterfaceJavaNames (TypeDefinition typeDef, Assem foreach (var implHandle in interfaceImpls) { var impl = index.Reader.GetInterfaceImplementation (implHandle); var ifaceJniName = ResolveInterfaceJniName (impl.Interface, index); - if (ifaceJniName != null) { + if (ifaceJniName is not null) { result.Add (ifaceJniName); } } @@ -325,24 +329,28 @@ List ResolveImplementedInterfaceJavaNames (TypeDefinition typeDef, Assem string? ResolveInterfaceJniName (EntityHandle interfaceHandle, AssemblyIndex index) { var resolved = ResolveEntityHandle (interfaceHandle, index); - return resolved != null ? ResolveRegisterJniName (resolved.Value.typeName, resolved.Value.assemblyName) : null; + return resolved is not null ? ResolveRegisterJniName (resolved.Value.typeName, resolved.Value.assemblyName) : null; } - static RegisterInfo? TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex index) + static bool TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex index, out RegisterInfo? registerInfo, out ExportInfo? exportInfo) { + exportInfo = null; foreach (var caHandle in methodDef.GetCustomAttributes ()) { var ca = index.Reader.GetCustomAttribute (caHandle); var attrName = AssemblyIndex.GetCustomAttributeName (ca, index.Reader); if (attrName == "RegisterAttribute") { - return AssemblyIndex.ParseRegisterAttribute (ca, index.customAttributeTypeProvider); + registerInfo = index.ParseRegisterAttribute (ca); + return true; } if (attrName == "ExportAttribute") { - return ParseExportAttribute (ca, methodDef, index); + (registerInfo, exportInfo) = ParseExportAttribute (ca, methodDef, index); + return true; } } - return null; + registerInfo = null; + return false; } static RegisterInfo? TryGetPropertyRegisterInfo (PropertyDefinition propDef, AssemblyIndex index) @@ -352,15 +360,15 @@ List ResolveImplementedInterfaceJavaNames (TypeDefinition typeDef, Assem var attrName = AssemblyIndex.GetCustomAttributeName (ca, index.Reader); if (attrName == "RegisterAttribute") { - return AssemblyIndex.ParseRegisterAttribute (ca, index.customAttributeTypeProvider); + return index.ParseRegisterAttribute (ca); } } return null; } - static RegisterInfo ParseExportAttribute (CustomAttribute ca, MethodDefinition methodDef, AssemblyIndex index) + static (RegisterInfo registerInfo, ExportInfo exportInfo) ParseExportAttribute (CustomAttribute ca, MethodDefinition methodDef, AssemblyIndex index) { - var value = ca.DecodeValue (index.customAttributeTypeProvider); + var value = index.DecodeAttribute (ca); // [Export("name")] or [Export] (uses method name) string? exportName = null; @@ -375,23 +383,31 @@ static RegisterInfo ParseExportAttribute (CustomAttribute ca, MethodDefinition m foreach (var named in value.NamedArguments) { if (named.Name == "Name" && named.Value is string name) { exportName = name; - } else if (named.Name == "ThrownNames" && named.Value is string[] names) { - thrownNames = new List (names); + } else if (named.Name == "ThrownNames" && named.Value is ImmutableArray> names) { + thrownNames = new List (names.Length); + foreach (var item in names) { + if (item.Value is string s) { + thrownNames.Add (s); + } + } } else if (named.Name == "SuperArgumentsString" && named.Value is string superArgs) { superArguments = superArgs; } } - if (exportName == null || exportName.Length == 0) { + if (string.IsNullOrEmpty (exportName)) { exportName = index.Reader.GetString (methodDef.Name); } + string resolvedExportName = exportName ?? throw new InvalidOperationException ("Export name should not be null at this point."); // Build JNI signature from method signature var sig = methodDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default); var jniSig = BuildJniSignatureFromManaged (sig); - return new RegisterInfo (exportName, jniSig, null, false, - thrownNames: thrownNames, superArgumentsString: superArguments); + return ( + new RegisterInfo { JniName = resolvedExportName, Signature = jniSig, Connector = null, DoNotGenerateAcw = false }, + new ExportInfo { ThrownNames = thrownNames, SuperArgumentsString = superArguments } + ); } static string BuildJniSignatureFromManaged (MethodSignature sig) @@ -425,7 +441,7 @@ static string ManagedTypeToJniDescriptor (string managedType) case "System.String": return "Ljava/lang/String;"; default: if (managedType.EndsWith ("[]")) { - return "[" + ManagedTypeToJniDescriptor (managedType.Substring (0, managedType.Length - 2)); + return $"[{ManagedTypeToJniDescriptor (managedType.Substring (0, managedType.Length - 2))}"; } return "Ljava/lang/Object;"; } @@ -433,27 +449,28 @@ static string ManagedTypeToJniDescriptor (string managedType) ActivationCtorInfo? ResolveActivationCtor (string typeName, TypeDefinition typeDef, AssemblyIndex index) { - if (activationCtorCache.TryGetValue (typeName, out var cached)) { + var cacheKey = (typeName, index.AssemblyName); + if (activationCtorCache.TryGetValue (cacheKey, out var cached)) { return cached; } // Check this type's constructors var ownCtor = FindActivationCtorOnType (typeDef, index); - if (ownCtor != null) { + if (ownCtor is not null) { var info = new ActivationCtorInfo { DeclaringTypeName = typeName, DeclaringAssemblyName = index.AssemblyName, Style = ownCtor.Value }; - activationCtorCache [typeName] = info; + activationCtorCache [cacheKey] = info; return info; } // Walk base type hierarchy var baseInfo = GetBaseTypeInfo (typeDef, index); - if (baseInfo != null) { + if (baseInfo is not null) { var (baseTypeName, baseAssemblyName) = baseInfo.Value; if (TryResolveType (baseTypeName, baseAssemblyName, out var baseHandle, out var baseIndex)) { var baseTypeDef = baseIndex.Reader.GetTypeDefinition (baseHandle); var result = ResolveActivationCtor (baseTypeName, baseTypeDef, baseIndex); - if (result != null) { - activationCtorCache [typeName] = result; + if (result is not null) { + activationCtorCache [cacheKey] = result; } return result; } @@ -521,7 +538,7 @@ static string ManagedTypeToJniDescriptor (string managedType) case 0: { // TypeDef var handle = MetadataTokens.TypeDefinitionHandle (row); var baseDef = index.Reader.GetTypeDefinition (handle); - return (AssemblyIndex.GetFullName (baseDef, index.Reader), index.AssemblyName); + return (MetadataTypeNameResolver.GetFullName (baseDef, index.Reader), index.AssemblyName); } case 1: // TypeRef return ResolveTypeReference (MetadataTokens.TypeReferenceHandle (row), index); @@ -539,7 +556,7 @@ static string ManagedTypeToJniDescriptor (string managedType) switch (handle.Kind) { case HandleKind.TypeDefinition: { var td = index.Reader.GetTypeDefinition ((TypeDefinitionHandle)handle); - return (AssemblyIndex.GetFullName (td, index.Reader), index.AssemblyName); + return (MetadataTypeNameResolver.GetFullName (td, index.Reader), index.AssemblyName); } case HandleKind.TypeReference: return ResolveTypeReference ((TypeReferenceHandle)handle, index); @@ -560,7 +577,7 @@ static string ManagedTypeToJniDescriptor (string managedType) // First, check the [Register] attribute's connector arg (3rd arg). // In real Mono.Android, interfaces have [Register("jni/name", "", "InvokerTypeName, Assembly")] // where the connector contains the assembly-qualified invoker type name. - if (index.RegisterInfoByType.TryGetValue (typeHandle, out var registerInfo) && registerInfo.Connector != null) { + if (index.RegisterInfoByType.TryGetValue (typeHandle, out var registerInfo) && registerInfo.Connector is not null) { var connector = registerInfo.Connector; // The connector may be "TypeName" or "TypeName, Assembly, Version=..., Culture=..., PublicKeyToken=..." // We want just the type name (before the first comma, if any) @@ -574,7 +591,7 @@ static string ManagedTypeToJniDescriptor (string managedType) } // Fallback: convention-based lookup — invoker type is TypeName + "Invoker" - var invokerName = typeName + "Invoker"; + var invokerName = $"{typeName}Invoker"; if (index.TypesByFullName.ContainsKey (invokerName)) { return invokerName; } @@ -597,8 +614,8 @@ public void Dispose () /// bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) { - var fullName = AssemblyIndex.GetFullName (typeDef, index.Reader); - var key = index.AssemblyName + ":" + fullName; + var fullName = MetadataTypeNameResolver.GetFullName (typeDef, index.Reader); + var key = $"{index.AssemblyName}:{fullName}"; if (extendsJavaPeerCache.TryGetValue (key, out var cached)) { return cached; @@ -608,7 +625,7 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) extendsJavaPeerCache [key] = false; var baseInfo = GetBaseTypeInfo (typeDef, index); - if (baseInfo == null) { + if (baseInfo is null) { return false; } @@ -623,7 +640,7 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) extendsJavaPeerCache [key] = true; return true; } - if (baseIndex.AttributesByType.TryGetValue (baseHandle, out var attrInfo) && attrInfo.HasComponentAttribute) { + if (baseIndex.AttributesByType.ContainsKey (baseHandle)) { extendsJavaPeerCache [key] = true; return true; } @@ -646,17 +663,17 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) { var (typeName, parentJniName, ns) = ComputeTypeNameParts (typeDef, index); - if (parentJniName != null) { - var name = parentJniName + "_" + typeName; + if (parentJniName is not null) { + var name = $"{parentJniName}_{typeName}"; return (name, name); } var packageName = GetCrc64PackageName (ns, index.AssemblyName); - var jniName = packageName + "/" + typeName; + var jniName = $"{packageName}/{typeName}"; string compatName = ns.Length == 0 ? typeName - : ns.ToLowerInvariant ().Replace ('.', '/') + "/" + typeName; + : $"{ns.ToLowerInvariant ().Replace ('.', '/')}/{typeName}"; return (jniName, compatName); } @@ -691,8 +708,8 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) parentJniName = parentRegister.JniName; break; } - if (index.AttributesByType.TryGetValue (parentHandle, out var parentAttr) && parentAttr.ComponentAttributeJniName != null) { - parentJniName = parentAttr.ComponentAttributeJniName; + if (index.AttributesByType.TryGetValue (parentHandle, out var parentAttr) && parentAttr.JniName is not null) { + parentJniName = parentAttr.JniName; break; } @@ -713,9 +730,9 @@ static string GetCrc64PackageName (string ns, string assemblyName) return ns.ToLowerInvariant ().Replace ('.', '/'); } - var data = System.Text.Encoding.UTF8.GetBytes (ns + ":" + assemblyName); + var data = System.Text.Encoding.UTF8.GetBytes ($"{ns}:{assemblyName}"); var hash = System.IO.Hashing.Crc64.Hash (data); - return "crc64" + BitConverter.ToString (hash).Replace ("-", "").ToLowerInvariant (); + return $"crc64{BitConverter.ToString (hash).Replace ("-", "").ToLowerInvariant ()}"; } static string ExtractNamespace (string fullName) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/SignatureTypeProvider.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/SignatureTypeProvider.cs index 185f10c89bd..87ed078adf2 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/SignatureTypeProvider.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/SignatureTypeProvider.cs @@ -36,31 +36,10 @@ sealed class SignatureTypeProvider : ISignatureTypeProvider }; public string GetTypeFromDefinition (MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) - { - var typeDef = reader.GetTypeDefinition (handle); - var ns = reader.GetString (typeDef.Namespace); - var name = reader.GetString (typeDef.Name); - if (typeDef.IsNested) { - var parent = GetTypeFromDefinition (reader, typeDef.GetDeclaringType (), rawTypeKind); - return parent + "+" + name; - } - return ns.Length > 0 ? ns + "." + name : name; - } + => MetadataTypeNameResolver.GetTypeFromDefinition (reader, handle, rawTypeKind); public string GetTypeFromReference (MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind) - { - var typeRef = reader.GetTypeReference (handle); - var name = reader.GetString (typeRef.Name); - - // Handle nested types: if the ResolutionScope is another TypeReference, resolve recursively - if (typeRef.ResolutionScope.Kind == HandleKind.TypeReference) { - var parent = GetTypeFromReference (reader, (TypeReferenceHandle)typeRef.ResolutionScope, rawTypeKind); - return parent + "+" + name; - } - - var ns = reader.GetString (typeRef.Namespace); - return ns.Length > 0 ? ns + "." + name : name; - } + => MetadataTypeNameResolver.GetTypeFromReference (reader, handle, rawTypeKind); public string GetTypeFromSpecification (MetadataReader reader, object? genericContext, TypeSpecificationHandle handle, byte rawTypeKind) { @@ -68,20 +47,20 @@ public string GetTypeFromSpecification (MetadataReader reader, object? genericCo return typeSpec.DecodeSignature (this, genericContext); } - public string GetSZArrayType (string elementType) => elementType + "[]"; - public string GetArrayType (string elementType, ArrayShape shape) => elementType + "[" + new string (',', shape.Rank - 1) + "]"; - public string GetByReferenceType (string elementType) => elementType + "&"; - public string GetPointerType (string elementType) => elementType + "*"; + public string GetSZArrayType (string elementType) => $"{elementType}[]"; + public string GetArrayType (string elementType, ArrayShape shape) => $"{elementType}[{new string (',', shape.Rank - 1)}]"; + public string GetByReferenceType (string elementType) => $"{elementType}&"; + public string GetPointerType (string elementType) => $"{elementType}*"; public string GetPinnedType (string elementType) => elementType; public string GetModifiedType (string modifier, string unmodifiedType, bool isRequired) => unmodifiedType; public string GetGenericInstantiation (string genericType, ImmutableArray typeArguments) { - return genericType + "<" + string.Join (",", typeArguments) + ">"; + return $"{genericType}<{string.Join (",", typeArguments)}>"; } - public string GetGenericTypeParameter (object? genericContext, int index) => "!" + index; - public string GetGenericMethodParameter (object? genericContext, int index) => "!!" + index; + public string GetGenericTypeParameter (object? genericContext, int index) => $"!{index}"; + public string GetGenericMethodParameter (object? genericContext, int index) => $"!!{index}"; public string GetFunctionPointerType (MethodSignature signature) => "delegate*"; } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs index d34708592d4..70471f62e13 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs @@ -67,13 +67,13 @@ private protected static JavaPeerInfo MakeMcwPeer (string jniName, string manage private protected static JavaPeerInfo MakePeerWithActivation (string jniName, string managedName, string asmName) { - var peer = MakeMcwPeer (jniName, managedName, asmName); - peer.ActivationCtor = new ActivationCtorInfo { - DeclaringTypeName = managedName, - DeclaringAssemblyName = asmName, - Style = ActivationCtorStyle.XamarinAndroid, + return MakeMcwPeer (jniName, managedName, asmName) with { + ActivationCtor = new ActivationCtorInfo { + DeclaringTypeName = managedName, + DeclaringAssemblyName = asmName, + Style = ActivationCtorStyle.XamarinAndroid, + }, }; - return peer; } private protected static JavaPeerInfo MakeAcwPeer (string jniName, string managedName, string asmName) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 017917d9fbc..31a06dec2e9 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -88,8 +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"); - peer.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"); } @@ -111,8 +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"); - peer.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); @@ -125,9 +123,10 @@ public void Build_McwBinding_IsTrimmable () public void Build_UnconditionalScannedType_IsUnconditional () { // Types with IsUnconditional from scanner (e.g., from [Activity], [Service] attrs) - var peer = MakeMcwPeer ("my/app/MySvc", "MyApp.MyService", "App"); - peer.DoNotGenerateAcw = true; // simulate MCW-like - peer.IsUnconditional = true; // scanner marked it + var peer = MakeMcwPeer ("my/app/MySvc", "MyApp.MyService", "App") with { + DoNotGenerateAcw = true, // simulate MCW-like + IsUnconditional = true, // scanner marked it + }; var model = BuildModel (new [] { peer }); Assert.True (model.Entries [0].IsUnconditional); @@ -387,8 +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"); - invokerPeer.DoNotGenerateAcw = true; + var invokerPeer = MakePeerWithActivation ("my/app/IFoo", "MyApp.FooInvoker", "App") with { DoNotGenerateAcw = true }; var model = BuildModel (new [] { ifacePeer, invokerPeer }); @@ -471,8 +469,7 @@ 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"); - invokerPeer.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 }); From e825dd743518ec0e2c0ed785b63b3697e4e921f1 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 10 Mar 2026 19:27:02 +0100 Subject: [PATCH 38/40] Fix byref encoding, MVID uniqueness, nested namespace, and JI handle cleanup - Fix missing ELEMENT_TYPE_BYREF for ref JniObjectReference in JI-style activation ctor signature (MissingMethodException at runtime) - Make DeterministicMvid content-aware so different type maps produce different MVIDs even with the same module name - Fix ExtractNamespace for nested types (My.NS.Outer+Inner -> My.NS) - Add JNIEnv.DeleteRef(handle, ownership) call after JI-style ctor to match legacy TypeManager.CreateProxy handle cleanup behavior - Always include Mono.Android in IgnoresAccessChecksTo for DeleteRef access - Add unit tests covering all four fixes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/MetadataHelper.cs | 38 +++++++- .../Generator/ModelBuilder.cs | 5 + .../Generator/PEAssemblyBuilder.cs | 4 +- .../Generator/TypeMapAssemblyEmitter.cs | 53 ++++++++++- .../Scanner/JavaPeerScanner.cs | 7 +- .../TypeMapAssemblyGeneratorTests.cs | 91 +++++++++++++++++++ .../Scanner/JavaPeerScannerTests.EdgeCases.cs | 8 ++ 7 files changed, 194 insertions(+), 12 deletions(-) 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/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index 4181252fd54..949b034571a 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -89,6 +89,11 @@ 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; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs index 28fb675c2a3..b862cc2b29f 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs @@ -46,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 (); @@ -62,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 62976156dc0..f878997f04a 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -54,6 +54,7 @@ sealed class TypeMapAssemblyEmitter TypeReferenceHandle _jniHandleOwnershipRef; TypeReferenceHandle _jniObjectReferenceRef; TypeReferenceHandle _jniObjectReferenceOptionsRef; + TypeReferenceHandle _jniEnvRef; TypeReferenceHandle _systemTypeRef; TypeReferenceHandle _runtimeTypeHandleRef; TypeReferenceHandle _notSupportedExceptionRef; @@ -64,6 +65,7 @@ sealed class TypeMapAssemblyEmitter MemberReferenceHandle _getUninitializedObjectRef; MemberReferenceHandle _notSupportedExceptionCtorRef; MemberReferenceHandle _jniObjectReferenceCtorRef; + MemberReferenceHandle _jniEnvDeleteRefRef; MemberReferenceHandle _typeMapAttrCtorRef2Arg; MemberReferenceHandle _typeMapAttrCtorRef3Arg; MemberReferenceHandle _typeMapAssociationAttrCtorRef; @@ -115,7 +117,7 @@ public void Emit (TypeMapAssemblyData model, Stream stream) 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)); @@ -146,6 +148,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, @@ -185,6 +189,17 @@ void EmitMemberReferences () 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); + })); + EmitTypeMapAttributeCtorRef (); EmitTypeMapAssociationAttributeCtorRef (); } @@ -371,24 +386,34 @@ void EmitCreateInstanceInheritedCtor (EntityHandle targetTypeRef, ActivationCtor /// /// Emits CreateInstance for JavaInterop-style activation (leaf type): /// var jniRef = new JniObjectReference(handle); - /// return new TargetType(ref jniRef, JniObjectReferenceOptions.Copy); + /// var result = new TargetType(ref jniRef, JniObjectReferenceOptions.Copy); + /// JNIEnv.DeleteRef(handle, ownership); + /// return result; /// void EmitCreateInstanceViaJavaInteropNewobj (EntityHandle typeRef) { var ctorRef = AddJavaInteropActivationCtorRef (typeRef); EmitCreateInstanceBodyWithLocals ( - EncodeJniObjectReferenceLocal, + EncodeJniObjectReferenceAndObjectLocals, encoder => { // var jniRef = new JniObjectReference(handle); encoder.LoadLocalAddress (0); encoder.OpCode (ILOpCode.Ldarg_1); // handle encoder.Call (_jniObjectReferenceCtorRef); - // return new TargetType(ref jniRef, JniObjectReferenceOptions.Copy); + // 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); }); } @@ -398,6 +423,7 @@ void EmitCreateInstanceViaJavaInteropNewobj (EntityHandle typeRef) /// 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) @@ -427,6 +453,11 @@ void EmitCreateInstanceInheritedJavaInteropCtor (EntityHandle targetTypeRef, Act 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); }); } @@ -440,6 +471,18 @@ void EncodeJniObjectReferenceLocal (BlobBuilder blob) 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", @@ -447,7 +490,7 @@ MemberReferenceHandle AddJavaInteropActivationCtorRef (EntityHandle declaringTyp rt => rt.Void (), p => { // ref JniObjectReference — encoded as byref valuetype - p.AddParameter ().Type ().Type (_jniObjectReferenceRef, true); + p.AddParameter ().Type (isByRef: true).Type (_jniObjectReferenceRef, true); // JniObjectReferenceOptions — encoded as valuetype (enum) p.AddParameter ().Type ().Type (_jniObjectReferenceOptionsRef, true); })); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 921532881fc..fc3627224f5 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -737,8 +737,11 @@ 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) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index d5dd043d8f4..7623e63dad6 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -319,4 +319,95 @@ public void EmitBody_ILCallbackCallsAddMemberRef_SignatureNotCorrupted () Assert.Equal (1, sig.ParameterTypes.Length); Assert.Equal ("System.Int32", sig.ParameterTypes [0]); } + + [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)); + } + + [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"); + + 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); + } + + [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"); + + 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); + } } \ No newline at end of file diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs index 1e7b0c29f16..b1f96d6d320 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs @@ -33,6 +33,14 @@ public void Scan_UnregisteredNestedType_UsesParentJniPrefix (string managedName, Assert.Equal (expectedJavaName, FindFixtureByManagedName (managedName).JavaName); } + [Theory] + [InlineData ("MyApp.RegisteredParent+UnregisteredChild", "MyApp")] + [InlineData ("MyApp.DeepOuter+Middle+DeepInner", "MyApp")] + public void Scan_NestedType_HasCorrectNamespace (string managedName, string expectedNamespace) + { + Assert.Equal (expectedNamespace, FindFixtureByManagedName (managedName).ManagedTypeNamespace); + } + [Fact] public void Scan_EmptyNamespace_Handled () { From 66fa683ea5e07894e68823d4dbe8ad3f959270b3 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 11 Mar 2026 09:22:16 +0100 Subject: [PATCH 39/40] Fix xUnit2013: use Assert.Single instead of Assert.Equal for collection size Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/TypeMapAssemblyGeneratorTests.cs | 4 ++-- 1 file 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 7623e63dad6..596528b742f 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -316,8 +316,8 @@ public void EmitBody_ILCallbackCallsAddMemberRef_SignatureNotCorrupted () var testMethod = methods.First (m => reader.GetString (m.Name) == "TestMethod"); var sig = testMethod.DecodeSignature (SignatureTypeProvider.Instance, null); - Assert.Equal (1, sig.ParameterTypes.Length); - Assert.Equal ("System.Int32", sig.ParameterTypes [0]); + var paramType = Assert.Single (sig.ParameterTypes); + Assert.Equal ("System.Int32", paramType); } [Fact] From 3827e5aa6ec6d9ceec02dcfebc6b59ddb2c9e0bd Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 11 Mar 2026 15:30:18 +0100 Subject: [PATCH 40/40] Remove leftover makeMSBuildArgs: -m:2 from Linux build The -m:2 flag was added during debugging and is not needed on Linux which doesn't have file locking issues like Windows. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- build-tools/automation/azure-pipelines-public.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/build-tools/automation/azure-pipelines-public.yaml b/build-tools/automation/azure-pipelines-public.yaml index 907e870c432..180cfdc247a 100644 --- a/build-tools/automation/azure-pipelines-public.yaml +++ b/build-tools/automation/azure-pipelines-public.yaml @@ -162,7 +162,6 @@ stages: buildResultArtifactName: Build Results - Linux xaSourcePath: $(System.DefaultWorkingDirectory)/android nugetArtifactName: $(LinuxNuGetArtifactName) - makeMSBuildArgs: -m:2 use1ESTemplate: false # Package Tests Stage