diff --git a/build-tools/automation/azure-pipelines-public.yaml b/build-tools/automation/azure-pipelines-public.yaml index c3aea6acad1..39e6deccd43 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 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..d07bb062bd3 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Helpers for parsing JNI method signatures. +/// +static class JniSignatureHelper +{ + /// + /// Parses the raw JNI type descriptor strings from a JNI method signature. + /// + public static List ParseParameterTypeStrings (string jniSignature) + { + var result = new List (); + int i = 1; // skip opening '(' + while (i < jniSignature.Length && jniSignature [i] != ')') { + int start = i; + SkipSingleType (jniSignature, ref i); + result.Add (jniSignature.Substring (start, i - start)); + } + return result; + } + + /// + /// Extracts the return type descriptor from a JNI method signature. + /// + public static string ParseReturnTypeString (string jniSignature) + { + int i = jniSignature.IndexOf (')') + 1; + return jniSignature.Substring (i); + } + + static void SkipSingleType (string sig, ref int i) + { + switch (sig [i]) { + case 'V': case 'Z': case 'B': case 'C': case 'S': + case 'I': case 'J': case 'F': case 'D': + i++; + break; + case 'L': + i = sig.IndexOf (';', i) + 1; + break; + case '[': + i++; + SkipSingleType (sig, ref i); + break; + default: + throw new ArgumentException ($"Unknown JNI type character '{sig [i]}' in '{sig}' at index {i}"); + } + } +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/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..279d3e15519 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -0,0 +1,202 @@ +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..9cae7720e6a --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs @@ -0,0 +1,264 @@ +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 (); + + public AssemblyReferenceHandle SystemRuntimeRef { get; private set; } + + public AssemblyReferenceHandle SystemRuntimeInteropServicesRef { get; private set; } + + 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); + } + + /// + /// 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)); + } + + /// + /// 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); + } + + /// + /// 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)); + } + + /// + /// 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); + } + + /// + /// 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 new file mode 100644 index 00000000000..ef538272ea5 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs @@ -0,0 +1,81 @@ +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 closedAttrTypeSpec = pe.MakeGenericTypeSpec (openAttrRef, javaLangObjectRef); + + // 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..f96d3448647 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -0,0 +1,373 @@ +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); + } + + _pe.EmitIgnoresAccessChecksToAttribute (model.IgnoresAccessChecksTo); + _pe.WritePE (outputPath); + } + + 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 closedAttrTypeSpec = _pe.MakeGenericTypeSpec (typeMapAttrOpenRef, javaLangObjectRef); + + // 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); + })); + } + + 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); + }); + } + + 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); + } + +} 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..2de7a49ead9 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -29,22 +29,19 @@ sealed record JavaPeerInfo public required string ManagedTypeName { get; init; } /// - /// Assembly name the type belongs to, e.g., "Mono.Android". + /// Managed type namespace, e.g., "Android.App". /// - public required string AssemblyName { get; init; } + public required string ManagedTypeNamespace { 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). + /// Managed type short name (without namespace), e.g., "Activity". /// - public string? BaseJavaName { get; init; } + public required string ManagedTypeShortName { get; init; } /// - /// JNI names of Java interfaces this type implements, e.g., ["android/view/View$OnClickListener"]. - /// Needed by JCW Java source generation ("implements" clause). + /// Assembly name the type belongs to, e.g., "Mono.Android". /// - public IReadOnlyList ImplementedInterfaceJavaNames { get; init; } = Array.Empty (); + public required string AssemblyName { get; init; } public bool IsInterface { get; init; } public bool IsAbstract { get; init; } @@ -119,6 +116,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 +163,22 @@ 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 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..81c7f8a4c5f 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); @@ -218,9 +212,9 @@ 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, IsInterface = isInterface, IsAbstract = isAbstract, DoNotGenerateAcw = doNotGenerateAcw, @@ -274,62 +268,32 @@ 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), - IsConstructor = registerInfo.JniName == "" || registerInfo.JniName == ".ctor", + NativeCallbackName = nativeCallbackName, + JniReturnType = JniSignatureHelper.ParseReturnTypeString (registerInfo.Signature ?? "()V"), + Parameters = ParseJniParameters (registerInfo.Signature ?? "()V"), + IsConstructor = isConstructor, ThrownNames = exportInfo?.ThrownNames, SuperArgumentsString = exportInfo?.SuperArgumentsString, }); } - 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; @@ -732,4 +696,28 @@ 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; + } } 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..bb446ff3029 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs @@ -0,0 +1,109 @@ +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.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/RootTypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs new file mode 100644 index 00000000000..b6dfe06c6b2 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs @@ -0,0 +1,117 @@ +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, IDisposable +{ + readonly string _outputDir = CreateTempDir (); + public void Dispose () => DeleteTempDir (_outputDir); + + string GenerateRootAssembly (IReadOnlyList perAssemblyNames, string? assemblyName = null) + { + 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); + return outputPath; + } + + [Fact] + public void Generate_ProducesValidPEAssembly () + { + var path = GenerateRootAssembly (new [] { "_App.TypeMap", "_Mono.Android.TypeMap" }); + Assert.True (File.Exists (path)); + using var pe = new PEReader (File.OpenRead (path)); + Assert.True (pe.HasMetadata); + } + + [Theory] + [InlineData (null, "_Microsoft.Android.TypeMaps")] + [InlineData ("MyRoot", "MyRoot")] + public void Generate_AssemblyName_MatchesExpected (string? assemblyName, string expectedName) + { + var path = GenerateRootAssembly (Array.Empty (), assemblyName); + using var pe = new PEReader (File.OpenRead (path)); + 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" }); + 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")); + } + + [Fact] + public void Generate_EmptyList_ProducesValidAssemblyWithNoTargetAttributes () + { + var path = GenerateRootAssembly (Array.Empty ()); + using var pe = new PEReader (File.OpenRead (path)); + var reader = pe.GetMetadataReader (); + var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); + Assert.Empty (asmAttrs); + } + + [Fact] + public void Generate_MultipleTargets_HasCorrectAttributeCount () + { + var targets = new [] { "_App.TypeMap", "_Mono.Android.TypeMap", "_Java.Interop.TypeMap" }; + var path = GenerateRootAssembly (targets); + using var pe = new PEReader (File.OpenRead (path)); + var reader = pe.GetMetadataReader (); + var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); + Assert.Equal (3, asmAttrs.Count ()); + } + + [Fact] + public void Generate_AttributeBlobValues_MatchTargetNames () + { + var targets = new [] { "_App.TypeMap", "_Mono.Android.TypeMap" }; + var path = GenerateRootAssembly (targets); + using var pe = new PEReader (File.OpenRead (path)); + 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); + } +} 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..cceb2e20a62 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -0,0 +1,333 @@ +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 outputDir, string? assemblyName = null) + { + 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; + } + + 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 : IDisposable + { + readonly string _outputDir = CreateTempDir (); + public void Dispose () => DeleteTempDir (_outputDir); + + [Fact] + public void Generate_ProducesValidPEAssembly () + { + var peers = ScanFixtures (); + var path = GenerateAssembly (peers, _outputDir); + Assert.True (File.Exists (path)); + using var pe = new PEReader (File.OpenRead (path)); + Assert.True (pe.HasMetadata); + var reader = pe.GetMetadataReader (); + Assert.NotNull (reader); + } + + } + + 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, _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 : IDisposable + { + readonly string _outputDir = CreateTempDir (); + public void Dispose () => DeleteTempDir (_outputDir); + + [Fact] + public void Generate_CreatesProxyTypes () + { + var peers = ScanFixtures (); + var path = GenerateAssembly (peers, _outputDir); + var (pe, reader) = OpenAssembly (path); + using (pe) { + var proxyTypes = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .Where (t => reader.GetString (t.Namespace) == "_TypeMap.Proxies") + .ToList (); + + Assert.NotEmpty (proxyTypes); + Assert.Contains (proxyTypes, t => reader.GetString (t.Name) == "Java_Lang_Object_Proxy"); + } + } + + [Fact] + public void Generate_ProxyType_HasCtorAndCreateInstance () + { + var peers = ScanFixtures (); + var path = GenerateAssembly (peers, _outputDir); + var (pe, reader) = OpenAssembly (path); + using (pe) { + var objectProxy = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .First (t => reader.GetString (t.Name) == "Java_Lang_Object_Proxy"); + + var methods = objectProxy.GetMethods () + .Select (h => reader.GetMethodDefinition (h)) + .Select (m => reader.GetString (m.Name)) + .ToList (); + + Assert.Contains (".ctor", methods); + Assert.Contains ("CreateInstance", methods); + Assert.Contains ("get_TargetType", methods); + } + } + + } + + public class IgnoresAccessChecksTo : IDisposable + { + readonly string _outputDir = CreateTempDir (); + public void Dispose () => DeleteTempDir (_outputDir); + + [Fact] + public void Generate_HasIgnoresAccessChecksToAttribute () + { + var peers = ScanFixtures (); + var path = GenerateAssembly (peers, _outputDir); + var (pe, reader) = OpenAssembly (path); + using (pe) { + var types = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .ToList (); + Assert.Contains (types, t => + reader.GetString (t.Name) == "IgnoresAccessChecksToAttribute" && + reader.GetString (t.Namespace) == "System.Runtime.CompilerServices"); + } + } + + } + + public class Alias : IDisposable + { + readonly string _outputDir = CreateTempDir (); + public void Dispose () => DeleteTempDir (_outputDir); + + 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, _outputDir, "AliasTest"); + var (pe, reader) = OpenAssembly (path); + using (pe) { + var assemblyAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); + Assert.True (assemblyAttrs.Count () >= 3); + } + } + + [Fact] + public void Generate_DuplicateJniNames_EmitsTypeMapAssociationAttribute () + { + var peers = MakeDuplicateAliasPeers (); + var path = GenerateAssembly (peers, _outputDir, "AliasAssocTest"); + var (pe, reader) = OpenAssembly (path); + using (pe) { + var memberRefs = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) + .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i))) + .Where (m => reader.GetString (m.Name) == ".ctor") + .ToList (); + + var typeNames = GetTypeRefNames (reader); + Assert.Contains ("TypeMapAssociationAttribute", typeNames); + } + } + + } + + public class EmptyInput : IDisposable + { + readonly string _outputDir = CreateTempDir (); + public void Dispose () => DeleteTempDir (_outputDir); + + [Fact] + public void Generate_EmptyPeerList_ProducesValidAssembly () + { + var path = GenerateAssembly (Array.Empty (), _outputDir, "EmptyTest"); + Assert.True (File.Exists (path)); + var (pe, reader) = OpenAssembly (path); + using (pe) { + Assert.NotNull (reader); + var asmDef = reader.GetAssemblyDefinition (); + Assert.Equal ("EmptyTest", reader.GetString (asmDef.Name)); + } + } + + } + + public class CreateInstancePaths : IDisposable + { + readonly string _outputDir = CreateTempDir (); + public void Dispose () => DeleteTempDir (_outputDir); + + [Fact] + public void Generate_SimpleActivity_UsesGetUninitializedObject () + { + var peers = ScanFixtures (); + var simpleActivity = peers.First (p => p.JavaName == "my/app/SimpleActivity"); + Assert.NotNull (simpleActivity.ActivationCtor); + Assert.NotEqual (simpleActivity.ManagedTypeName, simpleActivity.ActivationCtor.DeclaringTypeName); + + var path = GenerateAssembly (new [] { simpleActivity }, _outputDir, "InheritedCtorTest"); + var (pe, reader) = OpenAssembly (path); + using (pe) { + var typeNames = GetTypeRefNames (reader); + Assert.Contains ("RuntimeHelpers", typeNames); + + var memberNames = GetMemberRefNames (reader); + Assert.DoesNotContain ("CreateManagedPeer", memberNames); + Assert.Contains ("GetUninitializedObject", memberNames); + } + } + + [Fact] + public void Generate_LeafCtor_DoesNotUseCreateManagedPeer () + { + var peers = ScanFixtures (); + // ClickableView has its own (IntPtr, JniHandleOwnership) ctor + var clickableView = peers.First (p => p.JavaName == "my/app/ClickableView"); + Assert.NotNull (clickableView.ActivationCtor); + Assert.Equal (clickableView.ManagedTypeName, clickableView.ActivationCtor.DeclaringTypeName); + + var path = GenerateAssembly (new [] { clickableView }, _outputDir, "LeafCtorTest"); + var (pe, reader) = OpenAssembly (path); + using (pe) { + var memberNames = GetMemberRefNames (reader); + Assert.DoesNotContain ("CreateManagedPeer", memberNames); + + var ctorRefs = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) + .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i))) + .Where (m => reader.GetString (m.Name) == ".ctor") + .ToList (); + Assert.True (ctorRefs.Count >= 2, "Should have ctor refs for proxy base + target type"); + } + } + + [Fact] + public void Generate_GenericType_ThrowsNotSupportedException () + { + var peers = ScanFixtures (); + var generic = peers.First (p => p.JavaName == "my/app/GenericHolder"); + Assert.True (generic.IsGenericDefinition); + + var path = GenerateAssembly (new [] { generic }, _outputDir, "GenericTest"); + var (pe, reader) = OpenAssembly (path); + using (pe) { + var typeNames = GetTypeRefNames (reader); + Assert.Contains ("NotSupportedException", typeNames); + } + } + + } + + public class IgnoresAccessChecksToForBaseCtor : IDisposable + { + readonly string _outputDir = CreateTempDir (); + public void Dispose () => DeleteTempDir (_outputDir); + + [Fact] + public void Generate_InheritedCtor_IncludesBaseCtorAssembly () + { + // SimpleActivity inherits activation ctor from Activity — both in TestFixtures + // but the generated assembly is "IgnoresAccessTest", so TestFixtures must be + // in IgnoresAccessChecksTo + var peers = ScanFixtures (); + var simpleActivity = peers.First (p => p.JavaName == "my/app/SimpleActivity"); + + var path = GenerateAssembly (new [] { simpleActivity }, _outputDir, "IgnoresAccessTest"); + var (pe, reader) = OpenAssembly (path); + using (pe) { + var ignoresAttrType = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .FirstOrDefault (t => reader.GetString (t.Name) == "IgnoresAccessChecksToAttribute"); + Assert.True (ignoresAttrType.Attributes != 0, "IgnoresAccessChecksToAttribute should be defined"); + + var assemblyAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); + var attrBlobs = new List (); + foreach (var attrHandle in assemblyAttrs) { + var attr = reader.GetCustomAttribute (attrHandle); + var blob = reader.GetBlobBytes (attr.Value); + var blobStr = System.Text.Encoding.UTF8.GetString (blob); + attrBlobs.Add (blobStr); + } + // Activity is in TestFixtures, so IgnoresAccessChecksTo must include TestFixtures + Assert.Contains (attrBlobs, b => b.Contains ("TestFixtures")); + } + } + + } + +} \ 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..54d66fc03f7 --- /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"); + + 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 outputDir = CreateTempDir (); + try { + var outputPath = Path.Combine (outputDir, $"{assemblyName}.dll"); + var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); + emitter.Emit (model, outputPath); + using var pe = new PEReader (File.OpenRead (outputPath)); + verify (pe, pe.GetMetadataReader ()); + } finally { + DeleteTempDir (outputDir); + } + } + + /// + /// 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