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/MetadataHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs new file mode 100644 index 00000000000..9c0867c0875 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs @@ -0,0 +1,52 @@ +using System; +using System.Security.Cryptography; +using System.Text; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +static class MetadataHelper +{ + /// + /// Produces a deterministic MVID by hashing the module name together with content-dependent data. + /// Assemblies with the same name but different content will have different MVIDs. + /// + public static Guid DeterministicMvid (string moduleName, ReadOnlySpan contentBytes = default) + { + using var sha = SHA256.Create (); + byte [] nameBytes = Encoding.UTF8.GetBytes (moduleName); + byte [] input = new byte [nameBytes.Length + contentBytes.Length]; + nameBytes.CopyTo (input, 0); + contentBytes.CopyTo (input.AsSpan (nameBytes.Length)); + byte [] hash = sha.ComputeHash (input); + byte [] guidBytes = new byte [16]; + Array.Copy (hash, guidBytes, 16); + return new Guid (guidBytes); + } + + /// + /// Computes a content fingerprint for the given . + /// + public static byte [] ComputeContentFingerprint (TypeMapAssemblyData data) + { + using var sha = SHA256.Create (); + using var stream = new System.IO.MemoryStream (); + using var writer = new System.IO.BinaryWriter (stream, Encoding.UTF8); + foreach (var entry in data.Entries) { + writer.Write (entry.JniName); + writer.Write (entry.ProxyTypeReference); + writer.Write (entry.TargetTypeReference ?? ""); + } + foreach (var proxy in data.ProxyTypes) { + writer.Write (proxy.TypeName); + writer.Write (proxy.TargetType.ManagedTypeName); + writer.Write (proxy.TargetType.AssemblyName); + writer.Write ((byte)(proxy.ActivationCtor?.Style ?? 0)); + } + foreach (var assoc in data.Associations) { + writer.Write (assoc.SourceTypeReference); + writer.Write (assoc.AliasProxyTypeReference); + } + writer.Flush (); + return sha.ComputeHash (stream.ToArray ()); + } +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs new file mode 100644 index 00000000000..701f9cd4f2c --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -0,0 +1,170 @@ +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..949b034571a --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -0,0 +1,241 @@ +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.Select (p => p.InvokerTypeName).OfType (), + 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); + } + + var usedProxyNames = new HashSet (StringComparer.Ordinal); + + 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, usedProxyNames); + } + + // 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); + } + } + + // Always include Mono.Android — the emitter calls internal JNIEnv.DeleteRef + // for JI-style activation cleanup (matching legacy TypeManager.CreateProxy behavior). + referencedAssemblies.Add ("Mono.Android"); + + model.IgnoresAccessChecksTo.AddRange (referencedAssemblies); + + return model; + } + + static void EmitPeers (TypeMapAssemblyData model, string jniName, + List peersForName, string assemblyName, HashSet usedProxyNames) + { + // 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, usedProxyNames); + 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 = AssemblyQualify (peer.ManagedTypeName, peer.AssemblyName), + AliasProxyTypeReference = AssemblyQualify ($"{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; + } + + // 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; + } + + 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, HashSet usedProxyNames) + { + // Use managed type name for proxy naming to guarantee uniqueness across aliases + // (two types with the same JNI name will have different managed names). + var proxyTypeName = peer.ManagedTypeName.Replace ('.', '_').Replace ('+', '_') + "_Proxy"; + + // Guard against name collisions (e.g., "My.Type" and "My_Type" both map to "My_Type_Proxy") + if (!usedProxyNames.Add (proxyTypeName)) { + int suffix = 2; + string candidate; + do { + candidate = $"{proxyTypeName}_{suffix}"; + suffix++; + } while (!usedProxyNames.Add (candidate)); + proxyTypeName = candidate; + } + + var proxy = new JavaPeerProxyData { + TypeName = proxyTypeName, + TargetType = new TypeRefData { + 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 = AssemblyQualify ($"{proxy.Namespace}.{proxy.TypeName}", outputAssemblyName); + } else { + proxyRef = AssemblyQualify (peer.ManagedTypeName, peer.AssemblyName); + } + + bool isUnconditional = IsUnconditionalEntry (peer); + string? targetRef = null; + if (!isUnconditional) { + targetRef = AssemblyQualify (peer.ManagedTypeName, peer.AssemblyName); + } + + return new TypeMapAttributeData { + JniName = jniName, + ProxyTypeReference = proxyRef, + TargetTypeReference = targetRef, + }; + } + + static string AssemblyQualify (string typeName, string assemblyName) + => $"{typeName}, {assemblyName}"; +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs new file mode 100644 index 00000000000..b862cc2b29f --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs @@ -0,0 +1,290 @@ +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, ReadOnlySpan contentFingerprint = default) + { + _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, contentFingerprint)), + 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); + } + + using var fs = File.Create (outputPath); + WritePE (fs); + } + + /// + /// Serialises the metadata + IL into a PE DLL and writes it to the given . + /// + public void WritePE (Stream stream) + { + var peBuilder = new ManagedPEBuilder ( + new PEHeaderBuilder (imageCharacteristics: Characteristics.Dll), + new MetadataRootBuilder (Metadata), + ILBuilder); + var peBlob = new BlobBuilder (); + peBuilder.Serialize (peBlob); + peBlob.WriteContentTo (stream); + } + + /// + /// 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) + => 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) + => EmitBody (name, attrs, encodeSig, emitIL, encodeLocals: null); + + /// + /// Emits a method body and definition with optional local variable declarations. + /// + /// + /// If non-null, writes the local variable signature blob. The callback receives a fresh + /// and must write the full LOCAL_SIG blob (header 0x07, + /// compressed count, then each variable type). + /// + public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, + Action encodeSig, Action emitIL, + Action? encodeLocals) + { + _sigBlob.Clear (); + encodeSig (new BlobEncoder (_sigBlob)); + // Capture the sig blob handle before emitIL, because emitIL callbacks + // may call AddMemberRef which clears and repopulates _sigBlob. + var sigBlobHandle = Metadata.GetOrAddBlob (_sigBlob); + + StandaloneSignatureHandle localSigHandle = default; + if (encodeLocals != null) { + var localSigBlob = new BlobBuilder (32); + encodeLocals (localSigBlob); + localSigHandle = Metadata.AddStandaloneSignature (Metadata.GetOrAddBlob (localSigBlob)); + } + + _codeBlob.Clear (); + var encoder = new InstructionEncoder (_codeBlob); + emitIL (encoder); + + while (ILBuilder.Count % 4 != 0) { + ILBuilder.WriteByte (0); + } + var bodyEncoder = new MethodBodyStreamEncoder (ILBuilder); + int bodyOffset = localSigHandle.IsNil + ? bodyEncoder.AddMethodBody (encoder) + : bodyEncoder.AddMethodBody (encoder, maxStack: 8, localSigHandle, MethodBodyAttributes.InitLocals); + + return Metadata.AddMethodDefinition ( + attrs, MethodImplAttributes.IL, + Metadata.GetOrAddString (name), + sigBlobHandle, + 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..14b49cfe986 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs @@ -0,0 +1,104 @@ +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 and writes it to a file. + /// + /// Names of per-assembly typemap assemblies to reference. + /// Path to write the output .dll. + /// Optional assembly name (defaults to _Microsoft.Android.TypeMaps). + public void Generate (IReadOnlyList perAssemblyTypeMapNames, string outputPath, string? assemblyName = null) + { + if (outputPath is null) { + throw new ArgumentNullException (nameof (outputPath)); + } + + var dir = Path.GetDirectoryName (outputPath); + if (!string.IsNullOrEmpty (dir)) { + Directory.CreateDirectory (dir); + } + + var moduleName = Path.GetFileName (outputPath); + using var fs = File.Create (outputPath); + Generate (perAssemblyTypeMapNames, fs, assemblyName, moduleName); + } + + /// + /// Generates the root typemap assembly and writes it to the given stream. + /// + /// Names of per-assembly typemap assemblies to reference. + /// Stream to write the output PE to. + /// Optional assembly name (defaults to _Microsoft.Android.TypeMaps). + /// Optional module name for the PE metadata. + public void Generate (IReadOnlyList perAssemblyTypeMapNames, Stream stream, string? assemblyName = null, string? moduleName = null) + { + if (perAssemblyTypeMapNames is null) { + throw new ArgumentNullException (nameof (perAssemblyTypeMapNames)); + } + if (stream is null) { + throw new ArgumentNullException (nameof (stream)); + } + + assemblyName ??= DefaultAssemblyName; + moduleName ??= assemblyName + ".dll"; + + 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 (stream); + } +} 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..f878997f04a --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -0,0 +1,577 @@ +using System; +using System.Collections.Generic; +using System.IO; +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; + + readonly PEAssemblyBuilder _pe; + + AssemblyReferenceHandle _javaInteropRef; + + TypeReferenceHandle _javaPeerProxyRef; + TypeReferenceHandle _iJavaPeerableRef; + TypeReferenceHandle _jniHandleOwnershipRef; + TypeReferenceHandle _jniObjectReferenceRef; + TypeReferenceHandle _jniObjectReferenceOptionsRef; + TypeReferenceHandle _jniEnvRef; + TypeReferenceHandle _systemTypeRef; + TypeReferenceHandle _runtimeTypeHandleRef; + TypeReferenceHandle _notSupportedExceptionRef; + TypeReferenceHandle _runtimeHelpersRef; + + MemberReferenceHandle _baseCtorRef; + MemberReferenceHandle _getTypeFromHandleRef; + MemberReferenceHandle _getUninitializedObjectRef; + MemberReferenceHandle _notSupportedExceptionCtorRef; + MemberReferenceHandle _jniObjectReferenceCtorRef; + MemberReferenceHandle _jniEnvDeleteRefRef; + 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)); + _pe = new PEAssemblyBuilder (_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)); + } + + EmitCore (model); + _pe.WritePE (outputPath); + } + + /// + /// Emits a PE assembly from the given model and writes it to . + /// + public void Emit (TypeMapAssemblyData model, Stream stream) + { + if (model is null) { + throw new ArgumentNullException (nameof (model)); + } + if (stream is null) { + throw new ArgumentNullException (nameof (stream)); + } + + EmitCore (model); + _pe.WritePE (stream); + } + + void EmitCore (TypeMapAssemblyData model) + { + _pe.EmitPreamble (model.AssemblyName, model.ModuleName, MetadataHelper.ComputeContentFingerprint (model)); + + _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); + } + + 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")); + _jniEnvRef = metadata.AddTypeReference (_pe.MonoAndroidRef, + metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("JNIEnv")); + _jniObjectReferenceRef = metadata.AddTypeReference (_javaInteropRef, + metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniObjectReference")); + _jniObjectReferenceOptionsRef = metadata.AddTypeReference (_javaInteropRef, + metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniObjectReferenceOptions")); + _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 ())); + + _jniObjectReferenceCtorRef = _pe.AddMemberRef (_jniObjectReferenceRef, ".ctor", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (1, + rt => rt.Void (), + p => p.AddParameter ().Type ().IntPtr ())); + + // JNIEnv.DeleteRef(IntPtr, JniHandleOwnership) — static, internal + // Used by JI-style activation to clean up the original handle after constructing the peer. + // Matches the legacy TypeManager.CreateProxy behavior. + _jniEnvDeleteRefRef = _pe.AddMemberRef (_jniEnvRef, "DeleteRef", + sig => sig.MethodSignature ().Parameters (2, + rt => rt.Void (), + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); + })); + + 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) { + EmitCreateInstanceNoActivation (); + return; + } + + if (proxy.IsGenericDefinition) { + EmitCreateInstanceGenericDefinition (); + return; + } + + // JavaInterop-style activation ctors (ref JniObjectReference, JniObjectReferenceOptions) + // require parameter conversion from (IntPtr, JniHandleOwnership). + if (proxy.ActivationCtor?.Style == ActivationCtorStyle.JavaInterop) { + if (proxy.InvokerType != null) { + EmitCreateInstanceViaJavaInteropNewobj (_pe.ResolveTypeRef (proxy.InvokerType)); + } else { + var targetRef = _pe.ResolveTypeRef (proxy.TargetType); + var jiCtor = proxy.ActivationCtor ?? throw new InvalidOperationException ("ActivationCtor should not be null"); + if (jiCtor.IsOnLeafType) { + EmitCreateInstanceViaJavaInteropNewobj (targetRef); + } else { + EmitCreateInstanceInheritedJavaInteropCtor (targetRef, jiCtor); + } + } + return; + } + + if (proxy.InvokerType != null) { + EmitCreateInstanceViaNewobj (_pe.ResolveTypeRef (proxy.InvokerType)); + 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) { + EmitCreateInstanceViaNewobj (targetTypeRef); + } else { + EmitCreateInstanceInheritedCtor (targetTypeRef, activationCtor); + } + } + + void EmitCreateInstanceNoActivation () + { + EmitCreateInstanceBody (encoder => { + encoder.OpCode (ILOpCode.Ldnull); + encoder.OpCode (ILOpCode.Ret); + }); + } + + void EmitCreateInstanceGenericDefinition () + { + EmitCreateInstanceBody (encoder => { + encoder.LoadString (_pe.Metadata.GetOrAddUserString ("Cannot create instance of open generic type.")); + encoder.OpCode (ILOpCode.Newobj); + encoder.Token (_notSupportedExceptionCtorRef); + encoder.OpCode (ILOpCode.Throw); + }); + } + + void EmitCreateInstanceViaNewobj (EntityHandle typeRef) + { + var ctorRef = AddActivationCtorRef (typeRef); + EmitCreateInstanceBody (encoder => { + encoder.OpCode (ILOpCode.Ldarg_1); + encoder.OpCode (ILOpCode.Ldarg_2); + encoder.OpCode (ILOpCode.Newobj); + encoder.Token (ctorRef); + encoder.OpCode (ILOpCode.Ret); + }); + } + + void EmitCreateInstanceInheritedCtor (EntityHandle targetTypeRef, ActivationCtorData activationCtor) + { + var baseActivationCtorRef = AddActivationCtorRef (_pe.ResolveTypeRef (activationCtor.DeclaringType)); + EmitCreateInstanceBody (encoder => { + encoder.OpCode (ILOpCode.Ldtoken); + encoder.Token (targetTypeRef); + encoder.Call (_getTypeFromHandleRef); + encoder.Call (_getUninitializedObjectRef); + encoder.OpCode (ILOpCode.Castclass); + encoder.Token (targetTypeRef); + + encoder.OpCode (ILOpCode.Dup); + encoder.OpCode (ILOpCode.Ldarg_1); + encoder.OpCode (ILOpCode.Ldarg_2); + encoder.Call (baseActivationCtorRef); + + encoder.OpCode (ILOpCode.Ret); + }); + } + + /// + /// Emits CreateInstance for JavaInterop-style activation (leaf type): + /// var jniRef = new JniObjectReference(handle); + /// var result = new TargetType(ref jniRef, JniObjectReferenceOptions.Copy); + /// JNIEnv.DeleteRef(handle, ownership); + /// return result; + /// + void EmitCreateInstanceViaJavaInteropNewobj (EntityHandle typeRef) + { + var ctorRef = AddJavaInteropActivationCtorRef (typeRef); + EmitCreateInstanceBodyWithLocals ( + EncodeJniObjectReferenceAndObjectLocals, + encoder => { + // var jniRef = new JniObjectReference(handle); + encoder.LoadLocalAddress (0); + encoder.OpCode (ILOpCode.Ldarg_1); // handle + encoder.Call (_jniObjectReferenceCtorRef); + + // var result = new TargetType(ref jniRef, JniObjectReferenceOptions.Copy); + encoder.LoadLocalAddress (0); + encoder.LoadConstantI4 (1); // JniObjectReferenceOptions.Copy + encoder.OpCode (ILOpCode.Newobj); + encoder.Token (ctorRef); + encoder.StoreLocal (1); // save result + + // JNIEnv.DeleteRef(handle, ownership); + encoder.OpCode (ILOpCode.Ldarg_1); // handle + encoder.OpCode (ILOpCode.Ldarg_2); // ownership + encoder.Call (_jniEnvDeleteRefRef); + + encoder.LoadLocal (1); // load result + encoder.OpCode (ILOpCode.Ret); + }); + } + + /// + /// Emits CreateInstance for JavaInterop-style activation (inherited ctor): + /// var obj = (TargetType)RuntimeHelpers.GetUninitializedObject(typeof(TargetType)); + /// var jniRef = new JniObjectReference(handle); + /// obj.BaseCtor(ref jniRef, JniObjectReferenceOptions.Copy); + /// JNIEnv.DeleteRef(handle, ownership); + /// return obj; + /// + void EmitCreateInstanceInheritedJavaInteropCtor (EntityHandle targetTypeRef, ActivationCtorData activationCtor) + { + var baseCtorRef = AddJavaInteropActivationCtorRef (_pe.ResolveTypeRef (activationCtor.DeclaringType)); + EmitCreateInstanceBodyWithLocals ( + EncodeJniObjectReferenceLocal, + encoder => { + // var obj = (TargetType)RuntimeHelpers.GetUninitializedObject(typeof(TargetType)); + encoder.OpCode (ILOpCode.Ldtoken); + encoder.Token (targetTypeRef); + encoder.Call (_getTypeFromHandleRef); + encoder.Call (_getUninitializedObjectRef); + encoder.OpCode (ILOpCode.Castclass); + encoder.Token (targetTypeRef); + + // dup obj (one copy for the call, one for the return) + encoder.OpCode (ILOpCode.Dup); + + // var jniRef = new JniObjectReference(handle); + encoder.LoadLocalAddress (0); + encoder.OpCode (ILOpCode.Ldarg_1); // handle + encoder.Call (_jniObjectReferenceCtorRef); + + // obj.BaseCtor(ref jniRef, JniObjectReferenceOptions.Copy); + encoder.LoadLocalAddress (0); + encoder.LoadConstantI4 (1); // JniObjectReferenceOptions.Copy + encoder.Call (baseCtorRef); + + // JNIEnv.DeleteRef(handle, ownership); + encoder.OpCode (ILOpCode.Ldarg_1); // handle + encoder.OpCode (ILOpCode.Ldarg_2); // ownership + encoder.Call (_jniEnvDeleteRefRef); + + encoder.OpCode (ILOpCode.Ret); + }); + } + + void EncodeJniObjectReferenceLocal (BlobBuilder blob) + { + // LOCAL_SIG header (0x07), count = 1, ELEMENT_TYPE_VALUETYPE + compressed token + blob.WriteByte (0x07); // LOCAL_SIG + blob.WriteCompressedInteger (1); // 1 local variable + blob.WriteByte (0x11); // ELEMENT_TYPE_VALUETYPE + blob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (_jniObjectReferenceRef)); + } + + void EncodeJniObjectReferenceAndObjectLocals (BlobBuilder blob) + { + // LOCAL_SIG header (0x07), count = 2: + // local 0: JniObjectReference (valuetype) + // local 1: object (for storing the newobj result across the DeleteRef call) + blob.WriteByte (0x07); // LOCAL_SIG + blob.WriteCompressedInteger (2); // 2 local variables + blob.WriteByte (0x11); // ELEMENT_TYPE_VALUETYPE + blob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (_jniObjectReferenceRef)); + blob.WriteByte (0x1c); // ELEMENT_TYPE_OBJECT + } + + MemberReferenceHandle AddJavaInteropActivationCtorRef (EntityHandle declaringTypeRef) + { + return _pe.AddMemberRef (declaringTypeRef, ".ctor", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, + rt => rt.Void (), + p => { + // ref JniObjectReference — encoded as byref valuetype + p.AddParameter ().Type (isByRef: true).Type (_jniObjectReferenceRef, true); + // JniObjectReferenceOptions — encoded as valuetype (enum) + p.AddParameter ().Type ().Type (_jniObjectReferenceOptionsRef, true); + })); + } + + void EmitCreateInstanceBody (Action emitIL) + { + _pe.EmitBody ("CreateInstance", + 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); + } + + void EmitCreateInstanceBodyWithLocals (Action encodeLocals, Action emitIL) + { + _pe.EmitBody ("CreateInstance", + MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig, + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, + rt => rt.Type ().Type (_iJavaPeerableRef, false), + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); + }), + emitIL, + encodeLocals); + } + + MemberReferenceHandle AddActivationCtorRef (EntityHandle declaringTypeRef) + { + return _pe.AddMemberRef (declaringTypeRef, ".ctor", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, + rt => rt.Void (), + p => { + 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) { + if (entry.TargetTypeReference is null) { + throw new InvalidOperationException ($"TargetTypeReference must not be null for conditional entry '{entry.JniName}'"); + } + b.WriteSerializedString (entry.TargetTypeReference); + } + }); + _pe.Metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, ctorRef, blob); + } + + 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..f6586218d6a --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.IO; + +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); + } + + /// + /// Generates a TypeMap PE assembly from the given Java peer info records and writes it to . + /// + /// Scanned Java peer types. + /// Stream to write the output PE assembly to. + /// Assembly name for the generated assembly. + public void Generate (IReadOnlyList peers, Stream stream, string assemblyName) + { + var model = ModelBuilder.Build (peers, assemblyName + ".dll", assemblyName); + var emitter = new TypeMapAssemblyEmitter (_systemRuntimeVersion); + emitter.Emit (model, stream); + } +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index 85472f1b3ba..c34d7f2009c 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -28,6 +28,16 @@ sealed record JavaPeerInfo /// public required string ManagedTypeName { get; init; } + /// + /// Managed type namespace, e.g., "Android.App". + /// + public string ManagedTypeNamespace { get; init; } = ""; + + /// + /// Managed type short name (without namespace), e.g., "Activity". + /// + public string ManagedTypeShortName { get; init; } = ""; + /// /// Assembly name the type belongs to, e.g., "Mono.Android". /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 469d6345596..fc3627224f5 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -218,6 +218,8 @@ void ScanAssembly (AssemblyIndex index, Dictionary results JavaName = jniName, CompatJniName = compatJniName, ManagedTypeName = fullName, + ManagedTypeNamespace = ExtractNamespace (fullName), + ManagedTypeShortName = ExtractShortName (fullName), AssemblyName = index.AssemblyName, BaseJavaName = baseJavaName, ImplementedInterfaceJavaNames = implementedInterfaces, @@ -732,4 +734,22 @@ 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) + { + // Strip nested type suffix (e.g., "My.Namespace.Outer+Inner" → "My.Namespace.Outer") + int plusIndex = fullName.IndexOf ('+'); + var nameForNamespace = plusIndex >= 0 ? fullName.Substring (0, plusIndex) : fullName; + int lastDot = nameForNamespace.LastIndexOf ('.'); + return lastDot >= 0 ? nameForNamespace.Substring (0, lastDot) : ""; + } + + static string ExtractShortName (string fullName) + { + var span = fullName.AsSpan (); + int lastDot = span.LastIndexOf ('.'); + var typePart = lastDot >= 0 ? span.Slice (lastDot + 1) : span; + int lastPlus = typePart.LastIndexOf ('+'); + return (lastPlus >= 0 ? typePart.Slice (lastPlus + 1) : typePart).ToString (); + } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs new file mode 100644 index 00000000000..70471f62e13 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using Xunit; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; + +public abstract class FixtureTestBase +{ + static string TestFixtureAssemblyPath { + get { + var testAssemblyDir = Path.GetDirectoryName (typeof (FixtureTestBase).Assembly.Location) + ?? throw new InvalidOperationException ("Cannot determine test assembly directory"); + var fixtureAssembly = Path.Combine (testAssemblyDir, "TestFixtures.dll"); + Assert.True (File.Exists (fixtureAssembly), + $"TestFixtures.dll not found at {fixtureAssembly}. Ensure the TestFixtures project builds."); + return fixtureAssembly; + } + } + + static readonly Lazy> _cachedFixtures = new (() => { + using var scanner = new JavaPeerScanner (); + return scanner.Scan (new [] { TestFixtureAssemblyPath }); + }); + + private protected static List ScanFixtures () => _cachedFixtures.Value; + + private protected static JavaPeerInfo FindFixtureByJavaName (string javaName) + { + var peers = ScanFixtures (); + var peer = peers.FirstOrDefault (p => p.JavaName == javaName); + Assert.NotNull (peer); + return peer; + } + + private protected static JavaPeerInfo FindFixtureByManagedName (string managedName) + { + var peers = ScanFixtures (); + var peer = peers.FirstOrDefault (p => p.ManagedTypeName == managedName); + Assert.NotNull (peer); + return peer; + } + + static (string ns, string shortName) ParseManagedTypeName (string managedName) + { + var ns = managedName.Contains ('.') ? managedName.Substring (0, managedName.LastIndexOf ('.')) : ""; + var typePart = managedName.Contains ('.') ? managedName.Substring (managedName.LastIndexOf ('.') + 1) : managedName; + var shortName = typePart.Contains ('+') ? typePart.Substring (typePart.LastIndexOf ('+') + 1) : typePart; + return (ns, shortName); + } + + private protected static JavaPeerInfo MakeMcwPeer (string jniName, string managedName, string asmName) + { + var (ns, shortName) = ParseManagedTypeName (managedName); + return new JavaPeerInfo { + JavaName = jniName, + CompatJniName = jniName, + ManagedTypeName = managedName, + ManagedTypeNamespace = ns, + ManagedTypeShortName = shortName, + AssemblyName = asmName, + }; + } + + private protected static JavaPeerInfo MakePeerWithActivation (string jniName, string managedName, string asmName) + { + return MakeMcwPeer (jniName, managedName, asmName) with { + ActivationCtor = new ActivationCtorInfo { + DeclaringTypeName = managedName, + DeclaringAssemblyName = asmName, + Style = ActivationCtorStyle.XamarinAndroid, + }, + }; + } + + private protected static JavaPeerInfo MakeAcwPeer (string jniName, string managedName, string asmName) + => MakePeerWithActivation (jniName, managedName, asmName); + + private protected static JavaPeerInfo MakeInterfacePeer ( + string jniName, + string managedName, + string asmName, + string invokerName) + { + var (ns, shortName) = ParseManagedTypeName (managedName); + return new JavaPeerInfo { + JavaName = jniName, + CompatJniName = jniName, + ManagedTypeName = managedName, + ManagedTypeNamespace = ns, + ManagedTypeShortName = shortName, + AssemblyName = asmName, + IsInterface = true, + InvokerTypeName = invokerName, + }; + } + + private protected static List GetTypeRefNames (MetadataReader reader) => + reader.TypeReferences + .Select (h => reader.GetTypeReference (h)) + .Select (t => reader.GetString (t.Name)) + .ToList (); + + private protected static List GetMemberRefNames (MetadataReader reader) => + Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) + .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i))) + .Select (m => reader.GetString (m.Name)) + .ToList (); +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs new file mode 100644 index 00000000000..088ccdab8d9 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs @@ -0,0 +1,105 @@ +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 +{ + static MemoryStream GenerateRootAssembly (IReadOnlyList perAssemblyNames, string? assemblyName = null) + { + var stream = new MemoryStream (); + var generator = new RootTypeMapAssemblyGenerator (new Version (11, 0, 0, 0)); + generator.Generate (perAssemblyNames, stream, assemblyName); + stream.Position = 0; + return stream; + } + + [Fact] + public void Generate_ProducesValidPEAssembly () + { + using var stream = GenerateRootAssembly (new [] { "_App.TypeMap", "_Mono.Android.TypeMap" }); + using var pe = new PEReader (stream); + Assert.True (pe.HasMetadata); + } + + [Theory] + [InlineData (null, "_Microsoft.Android.TypeMaps")] + [InlineData ("MyRoot", "MyRoot")] + public void Generate_AssemblyName_MatchesExpected (string? assemblyName, string expectedName) + { + using var stream = GenerateRootAssembly ([], assemblyName); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + var asmDef = reader.GetAssemblyDefinition (); + Assert.Equal (expectedName, reader.GetString (asmDef.Name)); + } + + [Fact] + public void Generate_ReferencesGenericTypeMapAssemblyTargetAttribute () + { + using var stream = GenerateRootAssembly (new [] { "_App.TypeMap" }); + using var pe = new PEReader (stream); + 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")); + } + + [Theory] + [InlineData (0, 0)] + [InlineData (3, 3)] + public void Generate_AttributeCount_MatchesTargetCount (int targetCount, int expectedCount) + { + var targets = Enumerable.Range (0, targetCount).Select (i => $"_Target{i}.TypeMap").ToArray (); + using var stream = GenerateRootAssembly (targets); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); + Assert.Equal (expectedCount, asmAttrs.Count ()); + } + + [Fact] + public void Generate_AttributeBlobValues_MatchTargetNames () + { + var targets = new [] { "_App.TypeMap", "_Mono.Android.TypeMap" }; + using var stream = GenerateRootAssembly (targets); + using var pe = new PEReader (stream); + var 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..596528b742f --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -0,0 +1,413 @@ +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 MemoryStream GenerateAssembly (IReadOnlyList peers, string assemblyName = "TestTypeMap") + { + var stream = new MemoryStream (); + var generator = new TypeMapAssemblyGenerator (new Version (11, 0, 0, 0)); + generator.Generate (peers, stream, assemblyName); + stream.Position = 0; + return stream; + } + + [Fact] + public void Generate_ProducesValidPEAssembly () + { + var peers = ScanFixtures (); + using var stream = GenerateAssembly (peers); + using var pe = new PEReader (stream); + Assert.True (pe.HasMetadata); + var reader = pe.GetMetadataReader (); + Assert.NotNull (reader); + } + + [Fact] + public void Generate_HasRequiredAssemblyReferences () + { + var peers = ScanFixtures (); + using var stream = GenerateAssembly (peers); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + var asmRefs = reader.AssemblyReferences + .Select (h => reader.GetString (reader.GetAssemblyReference (h).Name)) + .ToList (); + Assert.Contains ("System.Runtime", asmRefs); + Assert.Contains ("Mono.Android", asmRefs); + Assert.Contains ("Java.Interop", asmRefs); + Assert.Contains ("System.Runtime.InteropServices", asmRefs); + } + + [Fact] + public void Generate_CreatesProxyTypes () + { + var peers = ScanFixtures (); + using var stream = GenerateAssembly (peers); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + var proxyTypes = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .Where (t => reader.GetString (t.Namespace) == "_TypeMap.Proxies") + .ToList (); + + Assert.NotEmpty (proxyTypes); + Assert.Contains (proxyTypes, t => reader.GetString (t.Name) == "Java_Lang_Object_Proxy"); + } + + [Fact] + public void Generate_ProxyType_HasCtorAndCreateInstance () + { + var peers = ScanFixtures (); + using var stream = GenerateAssembly (peers); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + var objectProxy = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .First (t => reader.GetString (t.Name) == "Java_Lang_Object_Proxy"); + + var methods = objectProxy.GetMethods () + .Select (h => reader.GetMethodDefinition (h)) + .Select (m => reader.GetString (m.Name)) + .ToList (); + + Assert.Contains (".ctor", methods); + Assert.Contains ("CreateInstance", methods); + Assert.Contains ("get_TargetType", methods); + } + + [Fact] + public void Generate_HasIgnoresAccessChecksToAttribute () + { + var peers = ScanFixtures (); + using var stream = GenerateAssembly (peers); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + var types = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .ToList (); + Assert.Contains (types, t => + reader.GetString (t.Name) == "IgnoresAccessChecksToAttribute" && + reader.GetString (t.Namespace) == "System.Runtime.CompilerServices"); + } + + [Fact] + public void Generate_DuplicateJniNames_CreatesAliasEntriesAndAssociationAttribute () + { + var peers = new List { + new JavaPeerInfo { + JavaName = "test/Duplicate", + CompatJniName = "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", + CompatJniName = "test/Duplicate", + ManagedTypeName = "Test.Duplicate2", + ManagedTypeNamespace = "Test", + ManagedTypeShortName = "Duplicate2", + AssemblyName = "TestAssembly", + }, + }; + + using var stream = GenerateAssembly (peers, "AliasTest"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + var assemblyAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); + Assert.True (assemblyAttrs.Count () >= 3); + + var typeNames = GetTypeRefNames (reader); + Assert.Contains ("TypeMapAssociationAttribute", typeNames); + } + + [Fact] + public void Generate_EmptyPeerList_ProducesValidAssembly () + { + using var stream = GenerateAssembly ([], "EmptyTest"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + Assert.NotNull (reader); + var asmDef = reader.GetAssemblyDefinition (); + Assert.Equal ("EmptyTest", reader.GetString (asmDef.Name)); + } + + [Fact] + public void Generate_SimpleActivity_UsesGetUninitializedObject () + { + var peers = ScanFixtures (); + var simpleActivity = peers.First (p => p.JavaName == "my/app/SimpleActivity"); + Assert.NotNull (simpleActivity.ActivationCtor); + Assert.NotEqual (simpleActivity.ManagedTypeName, simpleActivity.ActivationCtor.DeclaringTypeName); + + using var stream = GenerateAssembly (new [] { simpleActivity }, "InheritedCtorTest"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + var typeNames = GetTypeRefNames (reader); + Assert.Contains ("RuntimeHelpers", typeNames); + + var memberNames = GetMemberRefNames (reader); + Assert.DoesNotContain ("CreateManagedPeer", memberNames); + Assert.Contains ("GetUninitializedObject", memberNames); + } + + [Fact] + public void Generate_LeafCtor_DoesNotUseCreateManagedPeer () + { + var peers = ScanFixtures (); + // ClickableView has its own (IntPtr, JniHandleOwnership) ctor + var clickableView = peers.First (p => p.JavaName == "my/app/ClickableView"); + Assert.NotNull (clickableView.ActivationCtor); + Assert.Equal (clickableView.ManagedTypeName, clickableView.ActivationCtor.DeclaringTypeName); + + using var stream = GenerateAssembly (new [] { clickableView }, "LeafCtorTest"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + var memberNames = GetMemberRefNames (reader); + Assert.DoesNotContain ("CreateManagedPeer", memberNames); + + var ctorRefs = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) + .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i))) + .Where (m => reader.GetString (m.Name) == ".ctor") + .ToList (); + Assert.True (ctorRefs.Count >= 2, "Should have ctor refs for proxy base + target type"); + } + + [Fact] + public void Generate_GenericType_ThrowsNotSupportedException () + { + var peers = ScanFixtures (); + var generic = peers.First (p => p.JavaName == "my/app/GenericHolder"); + Assert.True (generic.IsGenericDefinition); + + using var stream = GenerateAssembly (new [] { generic }, "GenericTest"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + var typeNames = GetTypeRefNames (reader); + Assert.Contains ("NotSupportedException", typeNames); + } + + [Fact] + public void Generate_InheritedCtor_IncludesBaseCtorAssembly () + { + // SimpleActivity inherits activation ctor from Activity — both in TestFixtures + // but the generated assembly is "IgnoresAccessTest", so TestFixtures must be + // in IgnoresAccessChecksTo + var peers = ScanFixtures (); + var simpleActivity = peers.First (p => p.JavaName == "my/app/SimpleActivity"); + + using var stream = GenerateAssembly (new [] { simpleActivity }, "IgnoresAccessTest"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + var ignoresAttrType = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .FirstOrDefault (t => reader.GetString (t.Name) == "IgnoresAccessChecksToAttribute"); + Assert.True (ignoresAttrType.Attributes != 0, "IgnoresAccessChecksToAttribute should be defined"); + + var assemblyAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); + var attrBlobs = new List (); + foreach (var attrHandle in assemblyAttrs) { + var attr = reader.GetCustomAttribute (attrHandle); + var blob = reader.GetBlobBytes (attr.Value); + var blobStr = System.Text.Encoding.UTF8.GetString (blob); + attrBlobs.Add (blobStr); + } + // Activity is in TestFixtures, so IgnoresAccessChecksTo must include TestFixtures + Assert.Contains (attrBlobs, b => b.Contains ("TestFixtures")); + } + + [Fact] + public void Generate_JiStyleCtor_EmitsJavaInteropActivation () + { + var peers = ScanFixtures (); + var jiPeer = peers.First (p => p.JavaName == "my/app/JiStylePeer"); + Assert.NotNull (jiPeer.ActivationCtor); + Assert.Equal (ActivationCtorStyle.JavaInterop, jiPeer.ActivationCtor.Style); + + using var stream = GenerateAssembly (new [] { jiPeer }, "JiStyleTest"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + // JI-style activation should emit JniObjectReference and JniObjectReferenceOptions type refs + var typeNames = GetTypeRefNames (reader); + Assert.Contains ("JniObjectReference", typeNames); + Assert.Contains ("JniObjectReferenceOptions", typeNames); + + // The proxy still exists (with a TargetType property) + var proxyTypes = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .Where (t => reader.GetString (t.Namespace) == "_TypeMap.Proxies") + .ToList (); + Assert.Single (proxyTypes); + } + + [Fact] + public void Emit_CalledTwice_Throws () + { + var model = ModelBuilder.Build ([], "Double.dll", "Double"); + var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); + emitter.Emit (model, new MemoryStream ()); + // MetadataBuilder.AddAssembly throws on second call (only one assembly definition per PE) + Assert.ThrowsAny (() => emitter.Emit (model, new MemoryStream ())); + } + + [Fact] + public void EmitBody_ILCallbackCallsAddMemberRef_SignatureNotCorrupted () + { + // Regression test: EmitBody uses shared _sigBlob for the method signature. + // If the emitIL callback calls AddMemberRef (which also uses _sigBlob), + // the method signature must not be corrupted. + var pe = new PEAssemblyBuilder (new Version (11, 0, 0, 0)); + pe.EmitPreamble ("SigTest", "SigTest.dll"); + + var objectRef = pe.Metadata.AddTypeReference (pe.SystemRuntimeRef, + pe.Metadata.GetOrAddString ("System"), pe.Metadata.GetOrAddString ("Object")); + + // already defined; add a type to host the method + pe.Metadata.AddTypeDefinition ( + System.Reflection.TypeAttributes.Public | System.Reflection.TypeAttributes.Class, + pe.Metadata.GetOrAddString ("Test"), + pe.Metadata.GetOrAddString ("MyType"), + objectRef, + MetadataTokens.FieldDefinitionHandle (pe.Metadata.GetRowCount (TableIndex.Field) + 1), + MetadataTokens.MethodDefinitionHandle (pe.Metadata.GetRowCount (TableIndex.MethodDef) + 1)); + + // EmitBody with an IL callback that calls AddMemberRef (clearing _sigBlob) + pe.EmitBody ("TestMethod", + MethodAttributes.Public | MethodAttributes.Static, + sig => sig.MethodSignature ().Parameters (1, + rt => rt.Void (), + p => p.AddParameter ().Type ().Int32 ()), + encoder => { + // This AddMemberRef call clears and repopulates _sigBlob + pe.AddMemberRef (objectRef, ".ctor", + s => s.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { })); + encoder.OpCode (ILOpCode.Ret); + }); + + // If the sig blob was corrupted, the PE metadata will have a wrong signature. + // Write and read back to verify. + var stream = new MemoryStream (); + pe.WritePE (stream); + stream.Position = 0; + + using var peReader = new PEReader (stream); + var reader = peReader.GetMetadataReader (); + var methods = reader.TypeDefinitions + .SelectMany (h => reader.GetTypeDefinition (h).GetMethods ()) + .Select (h => reader.GetMethodDefinition (h)) + .ToList (); + + var testMethod = methods.First (m => reader.GetString (m.Name) == "TestMethod"); + var sig = testMethod.DecodeSignature (SignatureTypeProvider.Instance, null); + var paramType = Assert.Single (sig.ParameterTypes); + Assert.Equal ("System.Int32", paramType); + } + + [Fact] + public void Generate_JiStyleCtor_FirstParamIsByRef () + { + var peers = ScanFixtures (); + var jiPeer = peers.First (p => p.JavaName == "my/app/JiStylePeer"); + Assert.Equal (ActivationCtorStyle.JavaInterop, jiPeer.ActivationCtor!.Style); + + using var stream = GenerateAssembly (new [] { jiPeer }, "JiByRefTest"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + // Find the .ctor member reference whose parent type is the JI peer's declaring type + var ctorRefs = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) + .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i))) + .Where (m => reader.GetString (m.Name) == ".ctor") + .ToList (); + + // Decode each .ctor signature and find the JI-style one (2 params, first is byref JniObjectReference) + bool foundByRefCtor = false; + foreach (var ctor in ctorRefs) { + var sig = ctor.DecodeMethodSignature (SignatureTypeProvider.Instance, null); + if (sig.ParameterTypes.Length == 2 && + sig.ParameterTypes [0].Contains ("JniObjectReference")) { + // The byref encoding should produce "Java.Interop.JniObjectReference&" + Assert.True (sig.ParameterTypes [0].EndsWith ("&"), + $"JI-style .ctor first param must be byref, got: {sig.ParameterTypes [0]}"); + foundByRefCtor = true; + } + } + Assert.True (foundByRefCtor, "Expected to find a .ctor with byref JniObjectReference parameter"); + } + + [Fact] + public void Generate_JiStyleCtor_EmitsDeleteRefCall () + { + var peers = ScanFixtures (); + var jiPeer = peers.First (p => p.JavaName == "my/app/JiStylePeer"); + + using var stream = GenerateAssembly (new [] { jiPeer }, "JiDeleteRefTest"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + // The JI-style activation path must emit a call to JNIEnv.DeleteRef(IntPtr, JniHandleOwnership) + // to match the legacy TypeManager.CreateProxy behavior. + var memberRefs = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) + .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i))) + .ToList (); + + var deleteRefRef = memberRefs.FirstOrDefault (m => reader.GetString (m.Name) == "DeleteRef"); + Assert.True (!deleteRefRef.Equals (default (MemberReference)), + "JI-style activation must emit a DeleteRef member reference for JNI handle cleanup"); + + // Verify it's on the JNIEnv type + var parentTypeRef = reader.GetTypeReference ((TypeReferenceHandle)deleteRefRef.Parent); + Assert.Equal ("JNIEnv", reader.GetString (parentTypeRef.Name)); + Assert.Equal ("Android.Runtime", reader.GetString (parentTypeRef.Namespace)); + } + + [Fact] + public void Generate_DifferentContent_ProducesDifferentMVIDs () + { + var peer1 = MakePeerWithActivation ("test/TypeA", "Test.TypeA", "TestAsm"); + var peer2 = MakePeerWithActivation ("test/TypeB", "Test.TypeB", "TestAsm"); + + using var stream1 = GenerateAssembly (new [] { peer1 }, "SameName"); + using var stream2 = GenerateAssembly (new [] { peer2 }, "SameName"); + + using var pe1 = new PEReader (stream1); + using var pe2 = new PEReader (stream2); + var mvid1 = pe1.GetMetadataReader ().GetGuid (pe1.GetMetadataReader ().GetModuleDefinition ().Mvid); + var mvid2 = pe2.GetMetadataReader ().GetGuid (pe2.GetMetadataReader ().GetModuleDefinition ().Mvid); + + Assert.NotEqual (mvid1, mvid2); + } + + [Fact] + public void Generate_IdenticalContent_ProducesIdenticalMVIDs () + { + var peer = MakePeerWithActivation ("test/TypeA", "Test.TypeA", "TestAsm"); + + using var stream1 = GenerateAssembly (new [] { peer }, "SameName"); + using var stream2 = GenerateAssembly (new [] { peer }, "SameName"); + + using var pe1 = new PEReader (stream1); + using var pe2 = new PEReader (stream2); + var mvid1 = pe1.GetMetadataReader ().GetGuid (pe1.GetMetadataReader ().GetModuleDefinition ().Mvid); + var mvid2 = pe2.GetMetadataReader ().GetGuid (pe2.GetMetadataReader ().GetModuleDefinition ().Mvid); + + Assert.Equal (mvid1, mvid2); + } +} \ No newline at end of file diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs new file mode 100644 index 00000000000..31a06dec2e9 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -0,0 +1,739 @@ +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 (Path.GetTempPath (), (assemblyName ?? "TestTypeMap") + ".dll"); + return ModelBuilder.Build (peers, outputPath, assemblyName); + } + + public class BasicStructure + { + [Fact] + public void Build_EmptyPeers_ProducesEmptyModel () + { + var model = BuildModel ([], "Empty"); + Assert.Equal ("Empty", model.AssemblyName); + Assert.Equal ("Empty.dll", model.ModuleName); + Assert.Empty (model.Entries); + Assert.Empty (model.ProxyTypes); + } + + [Theory] + [InlineData ("Foo.Bar.dll", null, "Foo.Bar")] + [InlineData ("Foo.dll", "MyAssembly", "MyAssembly")] + public void Build_AssemblyName_ResolvedCorrectly (string outputPath, string? explicitName, string expected) + { + var model = ModelBuilder.Build ([], outputPath, explicitName); + Assert.Equal (expected, 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); + + // No associations when neither peer has a proxy (no activation ctor or invoker) + Assert.Empty (model.Associations); + } + } + + public class ConditionalAttributes + { + [Theory] + [InlineData ("java/lang/Object")] + [InlineData ("java/lang/Throwable")] + [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") with { 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") with { 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") with { + DoNotGenerateAcw = true, // simulate MCW-like + 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 + { + [Theory] + [InlineData ("java/lang/Object", "Java.Lang.Object", "Mono.Android", "Java_Lang_Object_Proxy")] + [InlineData ("com/example/Outer$Inner", "Com.Example.Outer.Inner", "App", "Com_Example_Outer_Inner_Proxy")] + public void Build_PeerWithActivation_CreatesNamedProxy (string jniName, string managedName, string asmName, string expectedProxyName) + { + var peer = MakePeerWithActivation (jniName, managedName, asmName); + var model = BuildModel (new [] { peer }, "MyTypeMap"); + + Assert.Single (model.ProxyTypes); + var proxy = model.ProxyTypes [0]; + Assert.Equal (expectedProxyName, proxy.TypeName); + Assert.Equal ("_TypeMap.Proxies", proxy.Namespace); + Assert.True (proxy.HasActivation); + Assert.Equal (managedName, proxy.TargetType.ManagedTypeName); + Assert.Equal (asmName, proxy.TargetType.AssemblyName); + } + + [Fact] + public void Build_PeerWithInvoker_CreatesProxy () + { + var peer = MakeInterfacePeer ("android/view/View$OnClickListener", "Android.Views.View+IOnClickListener", "Mono.Android", "Android.Views.View+IOnClickListenerInvoker"); + + var 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); + } + } + + 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") with { 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) { + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == expectedProxyName); + Assert.NotNull (proxy); + } + } + } + + public class FixtureImplementorsAndDispatchers + { + [Theory] + [InlineData ("mono/android/view/View_IOnClickListenerImplementor", "Implementor")] + [InlineData ("mono/android/view/View_ClickEventDispatcher", "EventDispatcher")] + public void Fixture_HelperType_IsUnconditional (string javaName, string kind) + { + 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); + // Implementor/EventDispatcher types are treated as unconditional ACW types. + // Future optimization (see issue tracking Implementor trimming) may make them trimmable. + Assert.True (entry!.IsUnconditional, $"{kind} should be unconditional"); + } + } + + public class InvokerDetection + { + [Fact] + public void Build_TypeIsInvoker_OnlyWhenReferencedByAnotherPeer () + { + // A type is only treated as an invoker when another peer's InvokerTypeName references it. + // A type named "MyInvoker" with DoNotGenerateAcw is NOT automatically an invoker. + var invokerPeer = MakePeerWithActivation ("my/app/MyInvoker", "MyApp.MyInvoker", "App") with { DoNotGenerateAcw = true }; + + // 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.Associations.Count + model.IgnoresAccessChecksTo.Count; + Assert.Equal (expected, totalAttrs); + }); + } + + [Fact] + public void FullPipeline_AliasGroup_TypeMapAttributeCountIncludesAssociations () + { + // Two peers with the same JNI name, both with activation → generates an association + var peers = new List { + MakePeerWithActivation ("test/Alias", "Test.Primary", "Asm"), + MakePeerWithActivation ("test/Alias", "Test.Secondary", "Asm"), + }; + var model = BuildModel (peers, "AliasAttrCount"); + Assert.NotEmpty (model.Associations); + + EmitAndVerify (model, "AliasAttrCount", (pe, reader) => { + var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); + int totalAttrs = asmAttrs.Count (); + int expected = model.Entries.Count + model.Associations.Count + model.IgnoresAccessChecksTo.Count; + Assert.Equal (expected, totalAttrs); + }); + } + + [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 stream = new MemoryStream (); + var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); + emitter.Emit (model, stream); + stream.Position = 0; + using var pe = new PEReader (stream); + verify (pe, pe.GetMetadataReader ()); + } + + /// + /// 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 diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs index 81aabe9ad74..3ed0c175cf6 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs @@ -17,8 +17,7 @@ public partial class JavaPeerScannerTests [InlineData ("my/app/TouchHandler", "SetItems", "setItems", "([Ljava/lang/String;)V")] public void Scan_MarshalMethod_HasCorrectSignature (string javaName, string managedName, string jniName, string jniSig) { - var peers = ScanFixtures (); - var method = FindByJavaName (peers, javaName) + var method = FindFixtureByJavaName (javaName) .MarshalMethods.FirstOrDefault (m => m.ManagedMethodName == managedName || m.JniName == jniName); Assert.NotNull (method); Assert.Equal (jniName, method.JniName); @@ -28,32 +27,30 @@ public void Scan_MarshalMethod_HasCorrectSignature (string javaName, string mana [Fact] public void Scan_MarshalMethod_ConstructorsAndSpecialCases () { - var peers = ScanFixtures (); - - var ctors = FindByJavaName (peers, "my/app/CustomView") + var ctors = FindFixtureByJavaName ("my/app/CustomView") .MarshalMethods.Where (m => m.IsConstructor).ToList (); Assert.Equal (2, ctors.Count); Assert.Equal ("()V", ctors [0].JniSignature); Assert.Equal ("(Landroid/content/Context;)V", ctors [1].JniSignature); - Assert.DoesNotContain (FindByJavaName (peers, "my/app/MyHelper").MarshalMethods, m => m.IsConstructor); + Assert.DoesNotContain (FindFixtureByJavaName ("my/app/MyHelper").MarshalMethods, m => m.IsConstructor); - var exportMethod = FindByJavaName (peers, "my/app/ExportExample").MarshalMethods.Single (); + var exportMethod = FindFixtureByJavaName ("my/app/ExportExample").MarshalMethods.Single (); Assert.Equal ("myExportedMethod", exportMethod.JniName); Assert.Null (exportMethod.Connector); - var onStart = FindByJavaName (peers, "android/app/Activity") + var onStart = FindFixtureByJavaName ("android/app/Activity") .MarshalMethods.FirstOrDefault (m => m.JniName == "onStart"); Assert.NotNull (onStart); Assert.Equal ("", onStart.Connector); - var onClick = FindByManagedName (peers, "Android.Views.IOnClickListener") + var onClick = FindFixtureByManagedName ("Android.Views.IOnClickListener") .MarshalMethods.FirstOrDefault (m => m.JniName == "onClick"); Assert.NotNull (onClick); Assert.Equal ("(Landroid/view/View;)V", onClick.JniSignature); Assert.Equal ("Android.Views.IOnClickListenerInvoker", - FindByManagedName (peers, "Android.Views.IOnClickListener").InvokerTypeName); + FindFixtureByManagedName ("Android.Views.IOnClickListener").InvokerTypeName); } [Theory] @@ -62,8 +59,7 @@ public void Scan_MarshalMethod_ConstructorsAndSpecialCases () [InlineData ("my/app/MyButton", "MyApp.MyButton")] public void Scan_ActivationCtor_InheritsFromNearestBase (string javaName, string expectedDeclaringType) { - var peers = ScanFixtures (); - var peer = FindByJavaName (peers, javaName); + var peer = FindFixtureByJavaName (javaName); Assert.NotNull (peer.ActivationCtor); Assert.Equal (expectedDeclaringType, peer.ActivationCtor.DeclaringTypeName); } @@ -77,23 +73,20 @@ public void Scan_ActivationCtor_InheritsFromNearestBase (string javaName, string [InlineData ("my/app/MyButton", "android/widget/Button")] public void Scan_BaseJavaName_ResolvesCorrectly (string javaName, string? expectedBase) { - var peers = ScanFixtures (); - Assert.Equal (expectedBase, FindByJavaName (peers, javaName).BaseJavaName); + Assert.Equal (expectedBase, FindFixtureByJavaName (javaName).BaseJavaName); } [Fact] public void Scan_MultipleInterfaces_AllResolved () { - var peers = ScanFixtures (); - - var multi = FindByJavaName (peers, "my/app/MultiInterfaceView"); + var multi = FindFixtureByJavaName ("my/app/MultiInterfaceView"); Assert.Contains ("android/view/View$OnClickListener", multi.ImplementedInterfaceJavaNames); Assert.Contains ("android/view/View$OnLongClickListener", multi.ImplementedInterfaceJavaNames); Assert.Equal (2, multi.ImplementedInterfaceJavaNames.Count); Assert.Contains ("android/view/View$OnClickListener", - FindByJavaName (peers, "my/app/ClickableView").ImplementedInterfaceJavaNames); - Assert.Empty (FindByJavaName (peers, "my/app/MyHelper").ImplementedInterfaceJavaNames); + FindFixtureByJavaName ("my/app/ClickableView").ImplementedInterfaceJavaNames); + Assert.Empty (FindFixtureByJavaName ("my/app/MyHelper").ImplementedInterfaceJavaNames); } [Theory] @@ -101,15 +94,13 @@ public void Scan_MultipleInterfaces_AllResolved () [InlineData ("my/app/MainActivity", "my/app/MainActivity")] public void Scan_CompatJniName (string javaName, string expectedCompat) { - var peers = ScanFixtures (); - Assert.Equal (expectedCompat, FindByJavaName (peers, javaName).CompatJniName); + Assert.Equal (expectedCompat, FindFixtureByJavaName (javaName).CompatJniName); } [Fact] public void Scan_CompatJniName_UnregisteredType_UsesRawNamespace () { - var peers = ScanFixtures (); - var unregistered = FindByManagedName (peers, "MyApp.UnregisteredHelper"); + var unregistered = FindFixtureByManagedName ("MyApp.UnregisteredHelper"); Assert.StartsWith ("crc64", unregistered.JavaName); Assert.Equal ("myapp/UnregisteredHelper", unregistered.CompatJniName); } @@ -117,9 +108,8 @@ public void Scan_CompatJniName_UnregisteredType_UsesRawNamespace () [Fact] public void Scan_CustomJniNameProviderAttribute_UsesNameFromAttribute () { - var peers = ScanFixtures (); Assert.Equal ("com/example/CustomWidget", - FindByManagedName (peers, "MyApp.CustomWidget").JavaName); + FindFixtureByManagedName ("MyApp.CustomWidget").JavaName); } [Theory] @@ -127,7 +117,6 @@ public void Scan_CustomJniNameProviderAttribute_UsesNameFromAttribute () [InlineData ("my/app/ICallback$Result", "MyApp.ICallback+Result")] public void Scan_NestedType_IsDiscovered (string javaName, string managedName) { - var peers = ScanFixtures (); - Assert.Equal (managedName, FindByJavaName (peers, javaName).ManagedTypeName); + Assert.Equal (managedName, FindFixtureByJavaName (javaName).ManagedTypeName); } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs index ec368ed6a44..b1f96d6d320 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs @@ -8,23 +8,20 @@ public partial class JavaPeerScannerTests [Fact] public void Scan_GenericTypes_ResolveViaTypeSpecification () { - var peers = ScanFixtures (); Assert.Equal ("my/app/GenericBase", - FindByJavaName (peers, "my/app/ConcreteFromGeneric").BaseJavaName); + FindFixtureByJavaName ("my/app/ConcreteFromGeneric").BaseJavaName); Assert.Contains ("my/app/IGenericCallback", - FindByJavaName (peers, "my/app/GenericCallbackImpl").ImplementedInterfaceJavaNames); + FindFixtureByJavaName ("my/app/GenericCallbackImpl").ImplementedInterfaceJavaNames); } [Fact] public void Scan_ComponentOnlyBase_BothBaseAndDerivedDiscovered () { - var peers = ScanFixtures (); - - var baseType = FindByJavaName (peers, "my/app/BaseActivityNoRegister"); + var baseType = FindFixtureByJavaName ("my/app/BaseActivityNoRegister"); Assert.True (baseType.IsUnconditional); Assert.Equal ("android/app/Activity", baseType.BaseJavaName); - var derived = FindByManagedName (peers, "MyApp.DerivedFromComponentBase"); + var derived = FindFixtureByManagedName ("MyApp.DerivedFromComponentBase"); Assert.StartsWith ("crc64", derived.JavaName); } @@ -33,17 +30,23 @@ public void Scan_ComponentOnlyBase_BothBaseAndDerivedDiscovered () [InlineData ("MyApp.DeepOuter+Middle+DeepInner", "my/app/DeepOuter_Middle_DeepInner")] public void Scan_UnregisteredNestedType_UsesParentJniPrefix (string managedName, string expectedJavaName) { - var peers = ScanFixtures (); - Assert.Equal (expectedJavaName, FindByManagedName (peers, managedName).JavaName); + Assert.Equal (expectedJavaName, FindFixtureByManagedName (managedName).JavaName); + } + + [Theory] + [InlineData ("MyApp.RegisteredParent+UnregisteredChild", "MyApp")] + [InlineData ("MyApp.DeepOuter+Middle+DeepInner", "MyApp")] + public void Scan_NestedType_HasCorrectNamespace (string managedName, string expectedNamespace) + { + Assert.Equal (expectedNamespace, FindFixtureByManagedName (managedName).ManagedTypeNamespace); } [Fact] public void Scan_EmptyNamespace_Handled () { - var peers = ScanFixtures (); - Assert.Equal ("GlobalType", FindByJavaName (peers, "my/app/GlobalType").ManagedTypeName); + Assert.Equal ("GlobalType", FindFixtureByJavaName ("my/app/GlobalType").ManagedTypeName); Assert.Equal ("GlobalUnregisteredType", - FindByManagedName (peers, "GlobalUnregisteredType").CompatJniName); + FindFixtureByManagedName ("GlobalUnregisteredType").CompatJniName); } [Theory] @@ -53,15 +56,13 @@ public void Scan_EmptyNamespace_Handled () [InlineData ("MyApp.UnregisteredExporter")] public void Scan_UnregisteredType_DiscoveredWithCrc64Name (string managedName) { - var peers = ScanFixtures (); - Assert.StartsWith ("crc64", FindByManagedName (peers, managedName).JavaName); + Assert.StartsWith ("crc64", FindFixtureByManagedName (managedName).JavaName); } [Fact] public void Scan_ExportOnUnregisteredType_MethodDiscovered () { - var peers = ScanFixtures (); - var exportMethod = FindByManagedName (peers, "MyApp.UnregisteredExporter") + var exportMethod = FindFixtureByManagedName ("MyApp.UnregisteredExporter") .MarshalMethods.FirstOrDefault (m => m.JniName == "doExportedWork"); Assert.NotNull (exportMethod); Assert.Null (exportMethod.Connector); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs index bc2b6195f22..555bba3451e 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs @@ -6,38 +6,8 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; -public partial class JavaPeerScannerTests +public partial class JavaPeerScannerTests : FixtureTestBase { - static string TestFixtureAssemblyPath { - get { - var testAssemblyDir = Path.GetDirectoryName (typeof (JavaPeerScannerTests).Assembly.Location)!; - var fixtureAssembly = Path.Combine (testAssemblyDir, "TestFixtures.dll"); - Assert.True (File.Exists (fixtureAssembly), - $"TestFixtures.dll not found at {fixtureAssembly}. Ensure the TestFixtures project builds."); - return fixtureAssembly; - } - } - - List ScanFixtures () - { - using var scanner = new JavaPeerScanner (); - return scanner.Scan (new [] { TestFixtureAssemblyPath }); - } - - JavaPeerInfo FindByJavaName (List peers, string javaName) - { - var peer = peers.FirstOrDefault (p => p.JavaName == javaName); - Assert.NotNull (peer); - return peer; - } - - JavaPeerInfo FindByManagedName (List peers, string managedName) - { - var peer = peers.FirstOrDefault (p => p.ManagedTypeName == managedName); - Assert.NotNull (peer); - return peer; - } - [Fact] public void Scan_FindsAllJavaPeerTypes () { @@ -54,8 +24,7 @@ public void Scan_FindsAllJavaPeerTypes () [InlineData ("my/app/MainActivity", false)] public void Scan_DoNotGenerateAcw (string javaName, bool expected) { - var peers = ScanFixtures (); - Assert.Equal (expected, FindByJavaName (peers, javaName).DoNotGenerateAcw); + Assert.Equal (expected, FindFixtureByJavaName (javaName).DoNotGenerateAcw); } [Theory] @@ -71,19 +40,17 @@ public void Scan_DoNotGenerateAcw (string javaName, bool expected) [InlineData ("android/app/Activity", false)] public void Scan_IsUnconditional (string javaName, bool expected) { - var peers = ScanFixtures (); - Assert.Equal (expected, FindByJavaName (peers, javaName).IsUnconditional); + Assert.Equal (expected, FindFixtureByJavaName (javaName).IsUnconditional); } [Fact] public void Scan_TypeMetadata_IsCorrect () { - var peers = ScanFixtures (); - Assert.True (FindByJavaName (peers, "my/app/AbstractBase").IsAbstract); - Assert.True (FindByManagedName (peers, "Android.Views.IOnClickListener").IsInterface); - Assert.False (FindByManagedName (peers, "Android.Views.IOnClickListener").DoNotGenerateAcw); + Assert.True (FindFixtureByJavaName ("my/app/AbstractBase").IsAbstract); + Assert.True (FindFixtureByManagedName ("Android.Views.IOnClickListener").IsInterface); + Assert.False (FindFixtureByManagedName ("Android.Views.IOnClickListener").DoNotGenerateAcw); - var generic = FindByJavaName (peers, "my/app/GenericHolder"); + var generic = FindFixtureByJavaName ("my/app/GenericHolder"); Assert.True (generic.IsGenericDefinition); Assert.Equal ("MyApp.Generic.GenericHolder`1", generic.ManagedTypeName); } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs index 36c7587eb28..4a9ebb1d079 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs @@ -40,6 +40,21 @@ public enum JniHandleOwnership } } +namespace Java.Interop +{ + public struct JniObjectReference + { + public IntPtr Handle; + } + + public enum JniObjectReferenceOptions + { + None = 0, + Copy = 1, + CopyAndDispose = 2, + } +} + namespace Android.App { [AttributeUsage (AttributeTargets.Class)] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs index 35987f36f93..d516be0de5b 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs @@ -96,6 +96,18 @@ public interface IOnLongClickListener [Register ("onLongClick", "(Landroid/view/View;)Z", "GetOnLongClick_Landroid_view_View_Handler:Android.Views.IOnLongClickListenerInvoker")] bool OnLongClick (View v); } + + [Register ("mono/android/view/View_IOnClickListenerImplementor")] + public class View_IOnClickListenerImplementor : Java.Lang.Object + { + public View_IOnClickListenerImplementor (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } + + [Register ("mono/android/view/View_ClickEventDispatcher")] + public class View_ClickEventDispatcher : Java.Lang.Object + { + public View_ClickEventDispatcher (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } } namespace Android.Widget @@ -239,7 +251,6 @@ public class MyManageSpaceActivity : Android.App.Activity { protected MyManageSpaceActivity (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } } - public class UnregisteredHelper : Java.Lang.Object { } [Register ("my/app/MyButton")] @@ -265,7 +276,6 @@ public class CustomWidget : Java.Lang.Object { } [Activity (Name = "my.app.BaseActivityNoRegister")] public class BaseActivityNoRegister : Android.App.Activity { } - public class DerivedFromComponentBase : BaseActivityNoRegister { } [Register ("my/app/RegisteredParent")] @@ -282,7 +292,6 @@ public class Middle : Java.Lang.Object public class DeepInner : Java.Lang.Object { } } } - public class PlainActivitySubclass : Android.App.Activity { } [Activity (Label = "Unnamed")] @@ -330,6 +339,13 @@ public class GenericCallbackImpl : Java.Lang.Object, IGenericCallback { protected GenericCallbackImpl (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } } + + [Register ("my/app/JiStylePeer", DoNotGenerateAcw = true)] + public class JiStylePeer : Java.Lang.Object + { + protected JiStylePeer (ref Java.Interop.JniObjectReference reference, Java.Interop.JniObjectReferenceOptions options) + : base ((IntPtr)0, JniHandleOwnership.DoNotTransfer) { } + } } [Register ("my/app/GlobalType")] @@ -337,5 +353,4 @@ public class GlobalType : Java.Lang.Object { protected GlobalType (IntPtr handle, Android.Runtime.JniHandleOwnership transfer) : base (handle, transfer) { } } - public class GlobalUnregisteredType : Java.Lang.Object { }