From 9449651e8a447cefa2467563a5b1839be23a6c2b Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 18 Feb 2026 18:25:26 +0100 Subject: [PATCH 1/8] [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 2/8] =?UTF-8?q?Remove=20JcwJavaSourceGeneratorTests.cs=20?= =?UTF-8?q?=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 3/8] 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 4/8] 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 5/8] 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 6/8] [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 7/8] 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 8/8] [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