diff --git a/build-tools/automation/azure-pipelines-public.yaml b/build-tools/automation/azure-pipelines-public.yaml
index c3aea6acad1..39e6deccd43 100644
--- a/build-tools/automation/azure-pipelines-public.yaml
+++ b/build-tools/automation/azure-pipelines-public.yaml
@@ -160,6 +160,7 @@ stages:
buildResultArtifactName: Build Results - Linux
xaSourcePath: $(System.DefaultWorkingDirectory)/android
nugetArtifactName: $(LinuxNuGetArtifactName)
+ makeMSBuildArgs: -m:2
use1ESTemplate: false
# Package Tests Stage
diff --git a/build-tools/automation/yaml-templates/build-linux-steps.yaml b/build-tools/automation/yaml-templates/build-linux-steps.yaml
index 02d34dafda2..bdc9c6a65c0 100644
--- a/build-tools/automation/yaml-templates/build-linux-steps.yaml
+++ b/build-tools/automation/yaml-templates/build-linux-steps.yaml
@@ -5,6 +5,7 @@ parameters:
buildResultArtifactName: Build Results - Linux
xaSourcePath: $(System.DefaultWorkingDirectory)/android
nugetArtifactName: $(LinuxNuGetArtifactName)
+ makeMSBuildArgs: ''
use1ESTemplate: true
steps:
@@ -26,7 +27,7 @@ steps:
- template: /build-tools/automation/yaml-templates/log-disk-space.yaml
-- script: make jenkins PREPARE_CI=1 PREPARE_AUTOPROVISION=1 CONFIGURATION=$(XA.Build.Configuration)
+- script: make jenkins PREPARE_CI=1 PREPARE_AUTOPROVISION=1 CONFIGURATION=$(XA.Build.Configuration) MSBUILD_ARGS='${{ parameters.makeMSBuildArgs }}'
workingDirectory: ${{ parameters.xaSourcePath }}
displayName: make jenkins
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs
new file mode 100644
index 00000000000..d07bb062bd3
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs
@@ -0,0 +1,53 @@
+using System;
+using System.Collections.Generic;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+///
+/// Helpers for parsing JNI method signatures.
+///
+static class JniSignatureHelper
+{
+ ///
+ /// Parses the raw JNI type descriptor strings from a JNI method signature.
+ ///
+ public static List ParseParameterTypeStrings (string jniSignature)
+ {
+ var result = new List ();
+ int i = 1; // skip opening '('
+ while (i < jniSignature.Length && jniSignature [i] != ')') {
+ int start = i;
+ SkipSingleType (jniSignature, ref i);
+ result.Add (jniSignature.Substring (start, i - start));
+ }
+ return result;
+ }
+
+ ///
+ /// Extracts the return type descriptor from a JNI method signature.
+ ///
+ public static string ParseReturnTypeString (string jniSignature)
+ {
+ int i = jniSignature.IndexOf (')') + 1;
+ return jniSignature.Substring (i);
+ }
+
+ static void SkipSingleType (string sig, ref int i)
+ {
+ switch (sig [i]) {
+ case 'V': case 'Z': case 'B': case 'C': case 'S':
+ case 'I': case 'J': case 'F': case 'D':
+ i++;
+ break;
+ case 'L':
+ i = sig.IndexOf (';', i) + 1;
+ break;
+ case '[':
+ i++;
+ SkipSingleType (sig, ref i);
+ break;
+ default:
+ throw new ArgumentException ($"Unknown JNI type character '{sig [i]}' in '{sig}' at index {i}");
+ }
+ }
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs
new file mode 100644
index 00000000000..2f62bb468f1
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Security.Cryptography;
+using System.Text;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+static class MetadataHelper
+{
+ ///
+ /// Produces a deterministic MVID from the module name so that identical inputs produce identical assemblies.
+ ///
+ public static Guid DeterministicMvid (string moduleName)
+ {
+ using var sha = SHA256.Create ();
+ byte [] hash = sha.ComputeHash (Encoding.UTF8.GetBytes (moduleName));
+ byte [] guidBytes = new byte [16];
+ Array.Copy (hash, guidBytes, 16);
+ return new Guid (guidBytes);
+ }
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs
new file mode 100644
index 00000000000..279d3e15519
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs
@@ -0,0 +1,202 @@
+using System;
+using System.Collections.Generic;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+///
+/// Data model for a single TypeMap output assembly.
+/// Describes what to emit — the emitter writes this directly into a PE assembly.
+/// Built by , consumed by .
+///
+sealed class TypeMapAssemblyData
+{
+ ///
+ /// Assembly name (e.g., "_MyApp.TypeMap").
+ ///
+ public required string AssemblyName { get; init; }
+
+ ///
+
+ /// Module file name (e.g., "_MyApp.TypeMap.dll").
+
+ ///
+ public required string ModuleName { get; init; }
+
+ ///
+
+ /// TypeMap entries — one per unique JNI name.
+
+ ///
+ public List Entries { get; } = new ();
+
+ ///
+
+ /// Proxy types to emit in the assembly.
+
+ ///
+ public List ProxyTypes { get; } = new ();
+
+ ///
+
+ /// TypeMapAssociation entries for alias groups (multiple managed types → same JNI name).
+
+ ///
+ public List Associations { get; } = new ();
+
+ ///
+
+ /// Assembly names that need [IgnoresAccessChecksTo] for cross-assembly n_* calls.
+
+ ///
+ public List IgnoresAccessChecksTo { get; } = new ();
+}
+
+///
+/// One [assembly: TypeMap("jni/name", typeof(Proxy))] or
+/// [assembly: TypeMap("jni/name", typeof(Proxy), typeof(Target))] entry.
+///
+/// 2-arg (unconditional): proxy is always preserved — used for ACW types and essential runtime types.
+/// 3-arg (trimmable): proxy is preserved only if Target type is referenced by the app.
+///
+sealed record TypeMapAttributeData
+{
+ ///
+ /// JNI type name, e.g., "android/app/Activity".
+ ///
+ public required string JniName { get; init; }
+
+ ///
+ /// Assembly-qualified proxy type reference string.
+ /// Either points to a generated proxy or to the original managed type.
+ ///
+ public required string ProxyTypeReference { get; init; }
+
+ ///
+ /// Assembly-qualified target type reference for the trimmable (3-arg) variant.
+ /// Null for unconditional (2-arg) entries.
+ /// The trimmer preserves the proxy only if this target type is used by the app.
+ ///
+ public string? TargetTypeReference { get; init; }
+
+ ///
+
+ /// True for 2-arg unconditional entries (ACW types, essential runtime types).
+
+ ///
+ public bool IsUnconditional => TargetTypeReference == null;
+}
+
+///
+/// A proxy type to generate in the TypeMap assembly (subclass of JavaPeerProxy).
+///
+sealed class JavaPeerProxyData
+{
+ ///
+ /// Simple type name, e.g., "Java_Lang_Object_Proxy".
+ ///
+ public required string TypeName { get; init; }
+
+ ///
+
+ /// Namespace for all proxy types.
+
+ ///
+ public string Namespace { get; init; } = "_TypeMap.Proxies";
+
+ ///
+
+ /// Reference to the managed type this proxy wraps (for ldtoken in TargetType property).
+
+ ///
+ public required TypeRefData TargetType { get; init; }
+
+ ///
+
+ /// Reference to the invoker type (for interfaces/abstract types). Null if not applicable.
+
+ ///
+ public TypeRefData? InvokerType { get; set; }
+
+ ///
+
+ /// Whether this proxy has a CreateInstance that can actually create instances.
+
+ ///
+ public bool HasActivation => ActivationCtor != null || InvokerType != null;
+
+ ///
+ /// Activation constructor details. Determines how CreateInstance instantiates the managed peer.
+ ///
+ public ActivationCtorData? ActivationCtor { get; set; }
+
+ ///
+
+ /// True if this is an open generic type definition. CreateInstance throws NotSupportedException.
+
+ ///
+ public bool IsGenericDefinition { get; init; }
+
+}
+
+
+///
+/// A cross-assembly type reference (assembly name + full managed type name).
+///
+sealed record TypeRefData
+{
+ ///
+ /// Full managed type name, e.g., "Android.App.Activity" or "MyApp.Outer+Inner".
+ ///
+ public required string ManagedTypeName { get; init; }
+
+ ///
+
+ /// Assembly containing the type, e.g., "Mono.Android".
+
+ ///
+ public required string AssemblyName { get; init; }
+}
+
+///
+/// Describes how the proxy's CreateInstance should construct the managed peer.
+///
+sealed record ActivationCtorData
+{
+ ///
+ /// Type that declares the activation constructor (may be a base type).
+ ///
+ public required TypeRefData DeclaringType { get; init; }
+
+ ///
+
+ /// True when the leaf type itself declares the activation ctor.
+
+ ///
+ public required bool IsOnLeafType { get; init; }
+
+ ///
+
+ /// The style of activation ctor (XamarinAndroid or JavaInterop).
+
+ ///
+ public required ActivationCtorStyle Style { get; init; }
+}
+
+///
+/// One [assembly: TypeMapAssociation(typeof(Source), typeof(AliasProxy))] entry.
+/// Links a managed type to the proxy that holds its alias TypeMap entry.
+///
+sealed record TypeMapAssociationData
+{
+ ///
+ /// Assembly-qualified source type reference (the managed alias type).
+ ///
+ public required string SourceTypeReference { get; init; }
+
+ ///
+
+ /// Assembly-qualified proxy type reference (the alias holder proxy).
+
+ ///
+ public required string AliasProxyTypeReference { get; init; }
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs
new file mode 100644
index 00000000000..e64d14849a9
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs
@@ -0,0 +1,240 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+///
+/// Builds a from scanned records.
+/// All decision logic (deduplication, alias detection, ACW filtering, 2-arg vs 3-arg attribute
+/// selection, callback resolution, proxy naming) lives here.
+/// The output model is a plain data structure that the emitter writes directly into a PE assembly.
+///
+static class ModelBuilder
+{
+ static readonly HashSet EssentialRuntimeTypes = new (StringComparer.Ordinal) {
+ "java/lang/Object",
+ "java/lang/Class",
+ "java/lang/String",
+ "java/lang/Throwable",
+ "java/lang/Exception",
+ "java/lang/RuntimeException",
+ "java/lang/Error",
+ "java/lang/Thread",
+ };
+
+ ///
+ /// Builds a TypeMap assembly model for the given peers.
+ ///
+ /// Scanned Java peer types (typically from a single input assembly).
+ /// Output .dll path — used to derive assembly/module names if not specified.
+ /// Explicit assembly name. If null, derived from .
+ public static TypeMapAssemblyData Build (IReadOnlyList peers, string outputPath, string? assemblyName = null)
+ {
+ if (peers is null) {
+ throw new ArgumentNullException (nameof (peers));
+ }
+ if (outputPath is null) {
+ throw new ArgumentNullException (nameof (outputPath));
+ }
+
+ assemblyName ??= Path.GetFileNameWithoutExtension (outputPath);
+ string moduleName = Path.GetFileName (outputPath);
+
+ var model = new TypeMapAssemblyData {
+ AssemblyName = assemblyName,
+ ModuleName = moduleName,
+ };
+
+ // Invoker types are NOT emitted as separate proxies or TypeMap entries —
+ // they only appear as a TypeRef in the interface proxy's get_InvokerType property.
+ var invokerTypeNames = new HashSet (
+ peers.Where (p => p.InvokerTypeName != null).Select (p => p.InvokerTypeName!),
+ StringComparer.Ordinal);
+
+ // Group non-invoker peers by JNI name to detect aliases (multiple .NET types → same Java class).
+ // Use an ordered dictionary to ensure deterministic output across runs.
+ var groups = new SortedDictionary> (StringComparer.Ordinal);
+ foreach (var peer in peers) {
+ if (invokerTypeNames.Contains (peer.ManagedTypeName)) {
+ continue;
+ }
+ if (!groups.TryGetValue (peer.JavaName, out var list)) {
+ list = new List ();
+ groups [peer.JavaName] = list;
+ }
+ list.Add (peer);
+ }
+
+ foreach (var kvp in groups) {
+ string jniName = kvp.Key;
+ var peersForName = kvp.Value;
+
+ // Sort aliases by managed type name for deterministic proxy naming
+ if (peersForName.Count > 1) {
+ peersForName.Sort ((a, b) => StringComparer.Ordinal.Compare (a.ManagedTypeName, b.ManagedTypeName));
+ }
+
+ EmitPeers (model, jniName, peersForName, assemblyName);
+ }
+
+ // Compute IgnoresAccessChecksTo from cross-assembly references
+ var referencedAssemblies = new SortedSet (StringComparer.Ordinal);
+ foreach (var proxy in model.ProxyTypes) {
+ AddIfCrossAssembly (referencedAssemblies, proxy.TargetType?.AssemblyName, assemblyName);
+ if (proxy.ActivationCtor != null && !proxy.ActivationCtor.IsOnLeafType) {
+ AddIfCrossAssembly (referencedAssemblies, proxy.ActivationCtor.DeclaringType.AssemblyName, assemblyName);
+ }
+ }
+ model.IgnoresAccessChecksTo.AddRange (referencedAssemblies);
+
+ return model;
+ }
+
+ static void EmitPeers (TypeMapAssemblyData model, string jniName,
+ List peersForName, string assemblyName)
+ {
+ // First peer is the "primary" — it gets the base JNI name entry.
+ // Remaining peers get indexed alias entries: "jni/name[1]", "jni/name[2]", ...
+ JavaPeerProxyData? primaryProxy = null;
+ for (int i = 0; i < peersForName.Count; i++) {
+ var peer = peersForName [i];
+ string entryJniName = i == 0 ? jniName : $"{jniName}[{i}]";
+
+ bool hasProxy = peer.ActivationCtor != null || peer.InvokerTypeName != null;
+
+ JavaPeerProxyData? proxy = null;
+ if (hasProxy) {
+ proxy = BuildProxyType (peer);
+ model.ProxyTypes.Add (proxy);
+ }
+
+ if (i == 0) {
+ primaryProxy = proxy;
+ }
+
+ model.Entries.Add (BuildEntry (peer, proxy, assemblyName, entryJniName));
+
+ // Emit TypeMapAssociation linking alias types to the primary proxy
+ if (i > 0 && primaryProxy != null) {
+ model.Associations.Add (new TypeMapAssociationData {
+ SourceTypeReference = $"{peer.ManagedTypeName}, {peer.AssemblyName}",
+ AliasProxyTypeReference = $"{primaryProxy.Namespace}.{primaryProxy.TypeName}, {assemblyName}",
+ });
+ }
+ }
+ }
+
+ ///
+ /// Determines whether a type should use the unconditional (2-arg) TypeMap attribute.
+ /// Unconditional types are always preserved by the trimmer.
+ ///
+ static bool IsUnconditionalEntry (JavaPeerInfo peer)
+ {
+ // Essential runtime types needed by the Java interop runtime
+ if (EssentialRuntimeTypes.Contains (peer.JavaName)) {
+ return true;
+ }
+
+ // Implementor/EventDispatcher types are only created from .NET (e.g., when a C# event
+ // is subscribed). They should NOT be unconditional — they're trimmable.
+ if (IsImplementorOrEventDispatcher (peer)) {
+ return false;
+ }
+
+ // User-defined ACW types (not MCW bindings, not interfaces) are unconditional
+ // because Android can instantiate them from Java at any time.
+ if (!peer.DoNotGenerateAcw && !peer.IsInterface) {
+ return true;
+ }
+
+ // Types marked unconditional by the scanner (component attributes: Activity, Service, etc.)
+ if (peer.IsUnconditional) {
+ return true;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Implementor and EventDispatcher types are generated by the binding generator
+ /// and are only instantiated from .NET. They should be trimmable.
+ /// NOTE: This is a name-based heuristic. Ideally the scanner would provide a dedicated flag.
+ /// User types whose names happen to end in "Implementor" or "EventDispatcher" would be
+ /// misclassified as trimmable. This is acceptable for now since such naming in user code
+ /// is unlikely and would only affect trimming behavior, not correctness.
+ ///
+ static bool IsImplementorOrEventDispatcher (JavaPeerInfo peer)
+ {
+ return peer.ManagedTypeName.EndsWith ("Implementor", StringComparison.Ordinal) ||
+ peer.ManagedTypeName.EndsWith ("EventDispatcher", StringComparison.Ordinal);
+ }
+
+ static void AddIfCrossAssembly (SortedSet set, string? asmName, string outputAssemblyName)
+ {
+ if (asmName != null && !string.Equals (asmName, outputAssemblyName, StringComparison.Ordinal)) {
+ set.Add (asmName);
+ }
+ }
+
+ static JavaPeerProxyData BuildProxyType (JavaPeerInfo peer)
+ {
+ // Use managed type name for proxy naming to guarantee uniqueness across aliases
+ // (two types with the same JNI name will have different managed names).
+ var proxyTypeName = peer.ManagedTypeName.Replace ('.', '_').Replace ('+', '_') + "_Proxy";
+
+ var proxy = new JavaPeerProxyData {
+ TypeName = proxyTypeName,
+ TargetType = new TypeRefData {
+ ManagedTypeName = peer.ManagedTypeName,
+ AssemblyName = peer.AssemblyName,
+ },
+ IsGenericDefinition = peer.IsGenericDefinition,
+ };
+
+ if (peer.InvokerTypeName != null) {
+ proxy.InvokerType = new TypeRefData {
+ ManagedTypeName = peer.InvokerTypeName,
+ AssemblyName = peer.AssemblyName,
+ };
+ }
+
+ if (peer.ActivationCtor != null) {
+ bool isOnLeaf = string.Equals (peer.ActivationCtor.DeclaringTypeName, peer.ManagedTypeName, StringComparison.Ordinal);
+ proxy.ActivationCtor = new ActivationCtorData {
+ DeclaringType = new TypeRefData {
+ ManagedTypeName = peer.ActivationCtor.DeclaringTypeName,
+ AssemblyName = peer.ActivationCtor.DeclaringAssemblyName,
+ },
+ IsOnLeafType = isOnLeaf,
+ Style = peer.ActivationCtor.Style,
+ };
+ }
+
+ return proxy;
+ }
+
+ static TypeMapAttributeData BuildEntry (JavaPeerInfo peer, JavaPeerProxyData? proxy,
+ string outputAssemblyName, string jniName)
+ {
+ string proxyRef;
+ if (proxy != null) {
+ proxyRef = $"{proxy.Namespace}.{proxy.TypeName}, {outputAssemblyName}";
+ } else {
+ proxyRef = $"{peer.ManagedTypeName}, {peer.AssemblyName}";
+ }
+
+ bool isUnconditional = IsUnconditionalEntry (peer);
+ string? targetRef = null;
+ if (!isUnconditional) {
+ targetRef = $"{peer.ManagedTypeName}, {peer.AssemblyName}";
+ }
+
+ return new TypeMapAttributeData {
+ JniName = jniName,
+ ProxyTypeReference = proxyRef,
+ TargetTypeReference = targetRef,
+ };
+ }
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs
new file mode 100644
index 00000000000..9cae7720e6a
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs
@@ -0,0 +1,264 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection;
+using System.Reflection.Metadata;
+using System.Reflection.Metadata.Ecma335;
+using System.Reflection.PortableExecutable;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+///
+/// Shared plumbing for building PE assemblies with System.Reflection.Metadata.
+/// Owns the , common assembly/type references, scratch blob builders,
+/// and the final PE serialisation. Both and
+/// delegate to this instead of duplicating boilerplate.
+///
+sealed class PEAssemblyBuilder
+{
+ // Mono.Android strong name public key token (84e04ff9cfb79065)
+ static readonly byte [] MonoAndroidPublicKeyToken = { 0x84, 0xe0, 0x4f, 0xf9, 0xcf, 0xb7, 0x90, 0x65 };
+
+ readonly Dictionary _asmRefCache = new (StringComparer.OrdinalIgnoreCase);
+ readonly Dictionary<(string Assembly, string Type), EntityHandle> _typeRefCache = new ();
+
+ // Reusable scratch BlobBuilders — avoids allocating a new one per method body / attribute / member ref.
+ // Each is Clear()'d before use. Safe because all emission is single-threaded and non-reentrant.
+ readonly BlobBuilder _sigBlob = new BlobBuilder (64);
+ readonly BlobBuilder _codeBlob = new BlobBuilder (256);
+ readonly BlobBuilder _attrBlob = new BlobBuilder (64);
+
+ readonly Version _systemRuntimeVersion;
+
+ public MetadataBuilder Metadata { get; } = new MetadataBuilder ();
+ public BlobBuilder ILBuilder { get; } = new BlobBuilder ();
+
+ public AssemblyReferenceHandle SystemRuntimeRef { get; private set; }
+
+ public AssemblyReferenceHandle SystemRuntimeInteropServicesRef { get; private set; }
+
+ public AssemblyReferenceHandle MonoAndroidRef { get; private set; }
+
+ public PEAssemblyBuilder (Version systemRuntimeVersion)
+ {
+ _systemRuntimeVersion = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion));
+ }
+
+ ///
+ /// Emits the assembly definition, module definition, common assembly references, and <Module> type.
+ /// Call this first.
+ ///
+ public void EmitPreamble (string assemblyName, string moduleName)
+ {
+ _asmRefCache.Clear ();
+ _typeRefCache.Clear ();
+
+ Metadata.AddAssembly (
+ Metadata.GetOrAddString (assemblyName),
+ new Version (1, 0, 0, 0),
+ culture: default,
+ publicKey: default,
+ flags: 0,
+ hashAlgorithm: AssemblyHashAlgorithm.None);
+
+ Metadata.AddModule (
+ generation: 0,
+ Metadata.GetOrAddString (moduleName),
+ Metadata.GetOrAddGuid (MetadataHelper.DeterministicMvid (moduleName)),
+ encId: default,
+ encBaseId: default);
+
+ // Common assembly references
+ SystemRuntimeRef = AddAssemblyRef ("System.Runtime", _systemRuntimeVersion);
+ SystemRuntimeInteropServicesRef = AddAssemblyRef ("System.Runtime.InteropServices", _systemRuntimeVersion);
+ MonoAndroidRef = AddAssemblyRef ("Mono.Android", new Version (0, 0, 0, 0), MonoAndroidPublicKeyToken);
+
+ // type
+ Metadata.AddTypeDefinition (
+ default, default,
+ Metadata.GetOrAddString (""),
+ default,
+ MetadataTokens.FieldDefinitionHandle (1),
+ MetadataTokens.MethodDefinitionHandle (1));
+ }
+
+ ///
+ /// Serialises the metadata + IL into a PE DLL at .
+ ///
+ public void WritePE (string outputPath)
+ {
+ var dir = Path.GetDirectoryName (outputPath);
+ if (!string.IsNullOrEmpty (dir)) {
+ Directory.CreateDirectory (dir);
+ }
+
+ var peBuilder = new ManagedPEBuilder (
+ new PEHeaderBuilder (imageCharacteristics: Characteristics.Dll),
+ new MetadataRootBuilder (Metadata),
+ ILBuilder);
+ var peBlob = new BlobBuilder ();
+ peBuilder.Serialize (peBlob);
+ using var fs = File.Create (outputPath);
+ peBlob.WriteContentTo (fs);
+ }
+
+ ///
+ /// Adds (or retrieves from cache) an assembly reference.
+ ///
+ public AssemblyReferenceHandle AddAssemblyRef (string name, Version version, byte []? publicKeyOrToken = null)
+ {
+ if (_asmRefCache.TryGetValue (name, out var existing)) {
+ return existing;
+ }
+ var handle = Metadata.AddAssemblyReference (
+ Metadata.GetOrAddString (name), version, default,
+ publicKeyOrToken != null ? Metadata.GetOrAddBlob (publicKeyOrToken) : default, 0, default);
+ _asmRefCache [name] = handle;
+ return handle;
+ }
+
+ ///
+ /// Finds an existing assembly reference or adds one with version 0.0.0.0.
+ ///
+ public AssemblyReferenceHandle FindOrAddAssemblyRef (string assemblyName)
+ {
+ if (_asmRefCache.TryGetValue (assemblyName, out var handle)) {
+ return handle;
+ }
+ return AddAssemblyRef (assemblyName, new Version (0, 0, 0, 0));
+ }
+
+ ///
+ /// Adds a member reference using the reusable signature blob builder.
+ ///
+ public MemberReferenceHandle AddMemberRef (EntityHandle parent, string name, Action encodeSig)
+ {
+ _sigBlob.Clear ();
+ encodeSig (new BlobEncoder (_sigBlob));
+ return Metadata.AddMemberReference (parent, Metadata.GetOrAddString (name), Metadata.GetOrAddBlob (_sigBlob));
+ }
+
+ ///
+ /// Resolves a to a TypeReference/TypeSpecification handle, with caching.
+ ///
+ public EntityHandle ResolveTypeRef (TypeRefData typeRef)
+ {
+ var cacheKey = (typeRef.AssemblyName, typeRef.ManagedTypeName);
+ if (_typeRefCache.TryGetValue (cacheKey, out var cached)) {
+ return cached;
+ }
+ var asmRef = FindOrAddAssemblyRef (typeRef.AssemblyName);
+ var result = MakeTypeRefForManagedName (asmRef, typeRef.ManagedTypeName);
+ _typeRefCache [cacheKey] = result;
+ return result;
+ }
+
+ TypeReferenceHandle MakeTypeRefForManagedName (EntityHandle scope, string managedTypeName)
+ {
+ int plusIndex = managedTypeName.IndexOf ('+');
+ if (plusIndex >= 0) {
+ var outerRef = MakeTypeRefForManagedName (scope, managedTypeName.Substring (0, plusIndex));
+ return MakeTypeRefForManagedName (outerRef, managedTypeName.Substring (plusIndex + 1));
+ }
+ int lastDot = managedTypeName.LastIndexOf ('.');
+ var ns = lastDot >= 0 ? managedTypeName.Substring (0, lastDot) : "";
+ var name = lastDot >= 0 ? managedTypeName.Substring (lastDot + 1) : managedTypeName;
+ return Metadata.AddTypeReference (scope, Metadata.GetOrAddString (ns), Metadata.GetOrAddString (name));
+ }
+
+ ///
+ /// Emits a method body and definition in one call.
+ ///
+ public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs,
+ Action encodeSig, Action emitIL)
+ {
+ _sigBlob.Clear ();
+ encodeSig (new BlobEncoder (_sigBlob));
+
+ _codeBlob.Clear ();
+ var encoder = new InstructionEncoder (_codeBlob);
+ emitIL (encoder);
+
+ while (ILBuilder.Count % 4 != 0) {
+ ILBuilder.WriteByte (0);
+ }
+ var bodyEncoder = new MethodBodyStreamEncoder (ILBuilder);
+ int bodyOffset = bodyEncoder.AddMethodBody (encoder);
+
+ return Metadata.AddMethodDefinition (
+ attrs, MethodImplAttributes.IL,
+ Metadata.GetOrAddString (name),
+ Metadata.GetOrAddBlob (_sigBlob),
+ bodyOffset, default);
+ }
+
+ ///
+ /// Builds a TypeSpec for a closed generic type with a single type argument.
+ /// For example, MakeGenericTypeSpec(openAttrRef, javaLangObjectRef) produces
+ /// TypeMapAttribute<Java.Lang.Object>.
+ ///
+ public TypeSpecificationHandle MakeGenericTypeSpec (EntityHandle openType, EntityHandle typeArg)
+ {
+ _sigBlob.Clear ();
+ _sigBlob.WriteByte (0x15); // ELEMENT_TYPE_GENERICINST
+ _sigBlob.WriteByte (0x12); // ELEMENT_TYPE_CLASS
+ _sigBlob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (openType));
+ _sigBlob.WriteCompressedInteger (1); // generic arity = 1
+ _sigBlob.WriteByte (0x12); // ELEMENT_TYPE_CLASS
+ _sigBlob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (typeArg));
+ return Metadata.AddTypeSpecification (Metadata.GetOrAddBlob (_sigBlob));
+ }
+
+ ///
+ /// Writes a custom attribute blob. Calls to fill in the
+ /// payload between the prolog and NumNamed footer.
+ ///
+ public BlobHandle BuildAttributeBlob (Action writePayload)
+ {
+ _attrBlob.Clear ();
+ _attrBlob.WriteUInt16 (0x0001); // Prolog
+ writePayload (_attrBlob);
+ _attrBlob.WriteUInt16 (0x0000); // NumNamed
+ return Metadata.GetOrAddBlob (_attrBlob);
+ }
+
+ ///
+ /// Emits the IgnoresAccessChecksToAttribute type and applies
+ /// [assembly: IgnoresAccessChecksTo("...")] for each assembly name.
+ ///
+ public void EmitIgnoresAccessChecksToAttribute (List assemblyNames)
+ {
+ var attributeTypeRef = Metadata.AddTypeReference (SystemRuntimeRef,
+ Metadata.GetOrAddString ("System"), Metadata.GetOrAddString ("Attribute"));
+
+ int typeFieldStart = Metadata.GetRowCount (TableIndex.Field) + 1;
+ int typeMethodStart = Metadata.GetRowCount (TableIndex.MethodDef) + 1;
+
+ var baseAttrCtorRef = AddMemberRef (attributeTypeRef, ".ctor",
+ sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { }));
+
+ var ctorDef = EmitBody (".ctor",
+ MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName,
+ sig => sig.MethodSignature (isInstanceMethod: true).Parameters (1,
+ rt => rt.Void (),
+ p => p.AddParameter ().Type ().String ()),
+ encoder => {
+ encoder.LoadArgument (0);
+ encoder.Call (baseAttrCtorRef);
+ encoder.OpCode (ILOpCode.Ret);
+ });
+
+ Metadata.AddTypeDefinition (
+ TypeAttributes.NotPublic | TypeAttributes.Sealed | TypeAttributes.BeforeFieldInit,
+ Metadata.GetOrAddString ("System.Runtime.CompilerServices"),
+ Metadata.GetOrAddString ("IgnoresAccessChecksToAttribute"),
+ attributeTypeRef,
+ MetadataTokens.FieldDefinitionHandle (typeFieldStart),
+ MetadataTokens.MethodDefinitionHandle (typeMethodStart));
+
+ foreach (var asmName in assemblyNames) {
+ var blob = BuildAttributeBlob (b => b.WriteSerializedString (asmName));
+ Metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, ctorDef, blob);
+ }
+ }
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs
new file mode 100644
index 00000000000..ef538272ea5
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs
@@ -0,0 +1,81 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection.Metadata;
+using System.Reflection.Metadata.Ecma335;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+///
+/// Generates the root _Microsoft.Android.TypeMaps.dll assembly that references
+/// all per-assembly typemap assemblies via
+/// [assembly: TypeMapAssemblyTargetAttribute<Java.Lang.Object>("name")].
+///
+///
+/// The generated assembly looks like this (pseudo-C#):
+///
+/// // One attribute per per-assembly typemap assembly — tells the runtime where to find TypeMap entries:
+/// [assembly: TypeMapAssemblyTarget<Java.Lang.Object>("_Mono.Android.TypeMap")]
+/// [assembly: TypeMapAssemblyTarget<Java.Lang.Object>("_MyApp.TypeMap")]
+///
+///
+sealed class RootTypeMapAssemblyGenerator
+{
+ const string DefaultAssemblyName = "_Microsoft.Android.TypeMaps";
+
+ readonly Version _systemRuntimeVersion;
+
+ /// Version for System.Runtime assembly references.
+ public RootTypeMapAssemblyGenerator (Version systemRuntimeVersion)
+ {
+ _systemRuntimeVersion = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion));
+ }
+
+ ///
+ /// Generates the root typemap assembly.
+ ///
+ /// Names of per-assembly typemap assemblies to reference.
+ /// Path to write the output .dll.
+ /// Optional assembly name (defaults to _Microsoft.Android.TypeMaps).
+ public void Generate (IReadOnlyList perAssemblyTypeMapNames, string outputPath, string? assemblyName = null)
+ {
+ if (perAssemblyTypeMapNames is null) {
+ throw new ArgumentNullException (nameof (perAssemblyTypeMapNames));
+ }
+ if (outputPath is null) {
+ throw new ArgumentNullException (nameof (outputPath));
+ }
+
+ assemblyName ??= DefaultAssemblyName;
+ var moduleName = Path.GetFileName (outputPath);
+
+ var pe = new PEAssemblyBuilder (_systemRuntimeVersion);
+ pe.EmitPreamble (assemblyName, moduleName);
+
+ // Reference the open generic TypeMapAssemblyTargetAttribute`1 from System.Runtime.InteropServices
+ var openAttrRef = pe.Metadata.AddTypeReference (pe.SystemRuntimeInteropServicesRef,
+ pe.Metadata.GetOrAddString ("System.Runtime.InteropServices"),
+ pe.Metadata.GetOrAddString ("TypeMapAssemblyTargetAttribute`1"));
+
+ // Reference Java.Lang.Object from Mono.Android (the type universe)
+ var javaLangObjectRef = pe.Metadata.AddTypeReference (pe.MonoAndroidRef,
+ pe.Metadata.GetOrAddString ("Java.Lang"), pe.Metadata.GetOrAddString ("Object"));
+
+ // Build TypeSpec for TypeMapAssemblyTargetAttribute
+ var closedAttrTypeSpec = pe.MakeGenericTypeSpec (openAttrRef, javaLangObjectRef);
+
+ // MemberRef for .ctor(string) on the closed generic type
+ var ctorRef = pe.AddMemberRef (closedAttrTypeSpec, ".ctor",
+ sig => sig.MethodSignature (isInstanceMethod: true).Parameters (1,
+ rt => rt.Void (),
+ p => p.AddParameter ().Type ().String ()));
+
+ // Add [assembly: TypeMapAssemblyTargetAttribute("name")] for each per-assembly typemap
+ foreach (var name in perAssemblyTypeMapNames) {
+ var blobHandle = pe.BuildAttributeBlob (blob => blob.WriteSerializedString (name));
+ pe.Metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, ctorRef, blobHandle);
+ }
+
+ pe.WritePE (outputPath);
+ }
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs
new file mode 100644
index 00000000000..f96d3448647
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs
@@ -0,0 +1,373 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Reflection.Metadata;
+using System.Reflection.Metadata.Ecma335;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+///
+/// Emits a per-assembly TypeMap PE assembly from a .
+/// This is a mechanical translation — all decision logic lives in .
+///
+///
+/// The generated assembly looks like this (pseudo-C#):
+///
+/// // Assembly-level TypeMap attributes — one per Java peer type:
+/// [assembly: TypeMap<Java.Lang.Object>("android/app/Activity", typeof(Activity_Proxy))] // unconditional (ACW)
+/// [assembly: TypeMap<Java.Lang.Object>("android/widget/TextView", typeof(TextView_Proxy), typeof(TextView))] // trimmable (MCW)
+/// [assembly: TypeMapAssociation(typeof(MyTextView), typeof(Android_Widget_TextView_Proxy))] // alias
+///
+/// // One proxy type per Java peer that needs activation:
+/// public sealed class Activity_Proxy : JavaPeerProxy
+/// {
+/// public Activity_Proxy() : base() { }
+///
+/// // Creates the managed peer when Java calls into .NET
+/// public override IJavaPeerable CreateInstance(IntPtr handle, JniHandleOwnership ownership)
+/// => new Activity(handle, ownership); // leaf ctor
+/// // or: (Activity)RuntimeHelpers.GetUninitializedObject(typeof(Activity));
+/// // obj.BaseCtor(handle, ownership); // inherited ctor
+/// // or: new IOnClickListenerInvoker(handle, ownership); // interface invoker
+/// // or: null; // no activation
+/// // or: throw new NotSupportedException(...); // open generic
+///
+/// public override Type TargetType => typeof(Activity);
+/// public Type InvokerType => typeof(IOnClickListenerInvoker); // interfaces only
+/// }
+///
+/// // Emitted so the proxy assembly can access internal members in the target assembly:
+/// [assembly: IgnoresAccessChecksTo("Mono.Android")]
+///
+///
+sealed class TypeMapAssemblyEmitter
+{
+ readonly Version _systemRuntimeVersion;
+
+ PEAssemblyBuilder _pe = null!;
+
+ AssemblyReferenceHandle _javaInteropRef;
+
+ TypeReferenceHandle _javaPeerProxyRef;
+ TypeReferenceHandle _iJavaPeerableRef;
+ TypeReferenceHandle _jniHandleOwnershipRef;
+ TypeReferenceHandle _systemTypeRef;
+ TypeReferenceHandle _runtimeTypeHandleRef;
+ TypeReferenceHandle _notSupportedExceptionRef;
+ TypeReferenceHandle _runtimeHelpersRef;
+
+ MemberReferenceHandle _baseCtorRef;
+ MemberReferenceHandle _getTypeFromHandleRef;
+ MemberReferenceHandle _getUninitializedObjectRef;
+ MemberReferenceHandle _notSupportedExceptionCtorRef;
+ MemberReferenceHandle _typeMapAttrCtorRef2Arg;
+ MemberReferenceHandle _typeMapAttrCtorRef3Arg;
+ MemberReferenceHandle _typeMapAssociationAttrCtorRef;
+
+ ///
+ /// Creates a new emitter.
+ ///
+ ///
+ /// Version for System.Runtime assembly references.
+ /// Will be derived from $(DotNetTargetVersion) MSBuild property in the build task.
+ ///
+ public TypeMapAssemblyEmitter (Version systemRuntimeVersion)
+ {
+ _systemRuntimeVersion = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion));
+ }
+
+ ///
+ /// Emits a PE assembly from the given model and writes it to .
+ ///
+ public void Emit (TypeMapAssemblyData model, string outputPath)
+ {
+ if (model is null) {
+ throw new ArgumentNullException (nameof (model));
+ }
+ if (outputPath is null) {
+ throw new ArgumentNullException (nameof (outputPath));
+ }
+
+ _pe = new PEAssemblyBuilder (_systemRuntimeVersion);
+ _pe.EmitPreamble (model.AssemblyName, model.ModuleName);
+
+ _javaInteropRef = _pe.AddAssemblyRef ("Java.Interop", new Version (0, 0, 0, 0));
+
+ EmitTypeReferences ();
+ EmitMemberReferences ();
+
+ foreach (var proxy in model.ProxyTypes) {
+ EmitProxyType (proxy);
+ }
+
+ foreach (var entry in model.Entries) {
+ EmitTypeMapAttribute (entry);
+ }
+
+ foreach (var assoc in model.Associations) {
+ EmitTypeMapAssociationAttribute (assoc);
+ }
+
+ _pe.EmitIgnoresAccessChecksToAttribute (model.IgnoresAccessChecksTo);
+ _pe.WritePE (outputPath);
+ }
+
+ void EmitTypeReferences ()
+ {
+ var metadata = _pe.Metadata;
+ _javaPeerProxyRef = metadata.AddTypeReference (_pe.MonoAndroidRef,
+ metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JavaPeerProxy"));
+ _iJavaPeerableRef = metadata.AddTypeReference (_javaInteropRef,
+ metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("IJavaPeerable"));
+ _jniHandleOwnershipRef = metadata.AddTypeReference (_pe.MonoAndroidRef,
+ metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("JniHandleOwnership"));
+ _systemTypeRef = metadata.AddTypeReference (_pe.SystemRuntimeRef,
+ metadata.GetOrAddString ("System"), metadata.GetOrAddString ("Type"));
+ _runtimeTypeHandleRef = metadata.AddTypeReference (_pe.SystemRuntimeRef,
+ metadata.GetOrAddString ("System"), metadata.GetOrAddString ("RuntimeTypeHandle"));
+ _notSupportedExceptionRef = metadata.AddTypeReference (_pe.SystemRuntimeRef,
+ metadata.GetOrAddString ("System"), metadata.GetOrAddString ("NotSupportedException"));
+ _runtimeHelpersRef = metadata.AddTypeReference (_pe.SystemRuntimeRef,
+ metadata.GetOrAddString ("System.Runtime.CompilerServices"), metadata.GetOrAddString ("RuntimeHelpers"));
+ }
+
+ void EmitMemberReferences ()
+ {
+ _baseCtorRef = _pe.AddMemberRef (_javaPeerProxyRef, ".ctor",
+ sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { }));
+
+ _getTypeFromHandleRef = _pe.AddMemberRef (_systemTypeRef, "GetTypeFromHandle",
+ sig => sig.MethodSignature ().Parameters (1,
+ rt => rt.Type ().Type (_systemTypeRef, false),
+ p => p.AddParameter ().Type ().Type (_runtimeTypeHandleRef, true)));
+
+ _getUninitializedObjectRef = _pe.AddMemberRef (_runtimeHelpersRef, "GetUninitializedObject",
+ sig => sig.MethodSignature ().Parameters (1,
+ rt => rt.Type ().Object (),
+ p => p.AddParameter ().Type ().Type (_systemTypeRef, false)));
+
+ _notSupportedExceptionCtorRef = _pe.AddMemberRef (_notSupportedExceptionRef, ".ctor",
+ sig => sig.MethodSignature (isInstanceMethod: true).Parameters (1,
+ rt => rt.Void (),
+ p => p.AddParameter ().Type ().String ()));
+
+ EmitTypeMapAttributeCtorRef ();
+ EmitTypeMapAssociationAttributeCtorRef ();
+ }
+
+ void EmitTypeMapAttributeCtorRef ()
+ {
+ var metadata = _pe.Metadata;
+ var typeMapAttrOpenRef = metadata.AddTypeReference (_pe.SystemRuntimeInteropServicesRef,
+ metadata.GetOrAddString ("System.Runtime.InteropServices"),
+ metadata.GetOrAddString ("TypeMapAttribute`1"));
+ var javaLangObjectRef = metadata.AddTypeReference (_pe.MonoAndroidRef,
+ metadata.GetOrAddString ("Java.Lang"), metadata.GetOrAddString ("Object"));
+
+ var closedAttrTypeSpec = _pe.MakeGenericTypeSpec (typeMapAttrOpenRef, javaLangObjectRef);
+
+ // 2-arg: TypeMap(string jniName, Type proxyType) — unconditional
+ _typeMapAttrCtorRef2Arg = _pe.AddMemberRef (closedAttrTypeSpec, ".ctor",
+ sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2,
+ rt => rt.Void (),
+ p => {
+ p.AddParameter ().Type ().String ();
+ p.AddParameter ().Type ().Type (_systemTypeRef, false);
+ }));
+
+ // 3-arg: TypeMap(string jniName, Type proxyType, Type targetType) — trimmable
+ _typeMapAttrCtorRef3Arg = _pe.AddMemberRef (closedAttrTypeSpec, ".ctor",
+ sig => sig.MethodSignature (isInstanceMethod: true).Parameters (3,
+ rt => rt.Void (),
+ p => {
+ p.AddParameter ().Type ().String ();
+ p.AddParameter ().Type ().Type (_systemTypeRef, false);
+ p.AddParameter ().Type ().Type (_systemTypeRef, false);
+ }));
+ }
+
+ void EmitTypeMapAssociationAttributeCtorRef ()
+ {
+ var metadata = _pe.Metadata;
+ // TypeMapAssociationAttribute is in System.Runtime.InteropServices, takes 2 Type args:
+ // TypeMapAssociation(Type sourceType, Type aliasProxyType)
+ var typeMapAssociationAttrRef = metadata.AddTypeReference (_pe.SystemRuntimeInteropServicesRef,
+ metadata.GetOrAddString ("System.Runtime.InteropServices"),
+ metadata.GetOrAddString ("TypeMapAssociationAttribute"));
+
+ _typeMapAssociationAttrCtorRef = _pe.AddMemberRef (typeMapAssociationAttrRef, ".ctor",
+ sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2,
+ rt => rt.Void (),
+ p => {
+ p.AddParameter ().Type ().Type (_systemTypeRef, false);
+ p.AddParameter ().Type ().Type (_systemTypeRef, false);
+ }));
+ }
+
+ void EmitProxyType (JavaPeerProxyData proxy)
+ {
+ var metadata = _pe.Metadata;
+ metadata.AddTypeDefinition (
+ TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.Class,
+ metadata.GetOrAddString (proxy.Namespace),
+ metadata.GetOrAddString (proxy.TypeName),
+ _javaPeerProxyRef,
+ MetadataTokens.FieldDefinitionHandle (metadata.GetRowCount (TableIndex.Field) + 1),
+ MetadataTokens.MethodDefinitionHandle (metadata.GetRowCount (TableIndex.MethodDef) + 1));
+
+ // .ctor
+ _pe.EmitBody (".ctor",
+ MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName,
+ sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { }),
+ encoder => {
+ encoder.OpCode (ILOpCode.Ldarg_0);
+ encoder.Call (_baseCtorRef);
+ encoder.OpCode (ILOpCode.Ret);
+ });
+
+ // CreateInstance
+ EmitCreateInstance (proxy);
+
+ // get_TargetType
+ EmitTypeGetter ("get_TargetType", proxy.TargetType,
+ MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.SpecialName | MethodAttributes.HideBySig);
+
+ // get_InvokerType
+ if (proxy.InvokerType != null) {
+ EmitTypeGetter ("get_InvokerType", proxy.InvokerType,
+ MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig);
+ }
+ }
+
+ void EmitCreateInstance (JavaPeerProxyData proxy)
+ {
+ if (!proxy.HasActivation) {
+ EmitCreateInstanceBody (encoder => {
+ encoder.OpCode (ILOpCode.Ldnull);
+ encoder.OpCode (ILOpCode.Ret);
+ });
+ return;
+ }
+
+ // Generic type definitions cannot be instantiated
+ if (proxy.IsGenericDefinition) {
+ EmitCreateInstanceBody (encoder => {
+ encoder.LoadString (_pe.Metadata.GetOrAddUserString ("Cannot create instance of open generic type."));
+ encoder.OpCode (ILOpCode.Newobj);
+ encoder.Token (_notSupportedExceptionCtorRef);
+ encoder.OpCode (ILOpCode.Throw);
+ });
+ return;
+ }
+
+ // Interface with invoker: new TInvoker(IntPtr, JniHandleOwnership)
+ if (proxy.InvokerType != null) {
+ var invokerCtorRef = AddActivationCtorRef (_pe.ResolveTypeRef (proxy.InvokerType));
+ EmitCreateInstanceBody (encoder => {
+ encoder.OpCode (ILOpCode.Ldarg_1);
+ encoder.OpCode (ILOpCode.Ldarg_2);
+ encoder.OpCode (ILOpCode.Newobj);
+ encoder.Token (invokerCtorRef);
+ encoder.OpCode (ILOpCode.Ret);
+ });
+ return;
+ }
+
+ // At this point, ActivationCtor is guaranteed non-null (HasActivation && InvokerType == null)
+ var activationCtor = proxy.ActivationCtor ?? throw new InvalidOperationException ("ActivationCtor should not be null when HasActivation is true and InvokerType is null");
+ var targetTypeRef = _pe.ResolveTypeRef (proxy.TargetType);
+
+ if (activationCtor.IsOnLeafType) {
+ // Leaf type has its own ctor: new T(IntPtr, JniHandleOwnership)
+ var ctorRef = AddActivationCtorRef (targetTypeRef);
+ EmitCreateInstanceBody (encoder => {
+ encoder.OpCode (ILOpCode.Ldarg_1);
+ encoder.OpCode (ILOpCode.Ldarg_2);
+ encoder.OpCode (ILOpCode.Newobj);
+ encoder.Token (ctorRef);
+ encoder.OpCode (ILOpCode.Ret);
+ });
+ } else {
+ // Inherited ctor: GetUninitializedObject(typeof(T)) + call Base::.ctor(IntPtr, JniHandleOwnership)
+ var baseActivationCtorRef = AddActivationCtorRef (_pe.ResolveTypeRef (activationCtor.DeclaringType));
+ EmitCreateInstanceBody (encoder => {
+ encoder.OpCode (ILOpCode.Ldtoken);
+ encoder.Token (targetTypeRef);
+ encoder.Call (_getTypeFromHandleRef);
+ encoder.Call (_getUninitializedObjectRef);
+ encoder.OpCode (ILOpCode.Castclass);
+ encoder.Token (targetTypeRef);
+
+ encoder.OpCode (ILOpCode.Dup);
+ encoder.OpCode (ILOpCode.Ldarg_1);
+ encoder.OpCode (ILOpCode.Ldarg_2);
+ encoder.Call (baseActivationCtorRef);
+
+ encoder.OpCode (ILOpCode.Ret);
+ });
+ }
+ }
+
+ void EmitCreateInstanceBody (Action emitIL)
+ {
+ _pe.EmitBody ("CreateInstance",
+ MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig,
+ sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2,
+ rt => rt.Type ().Type (_iJavaPeerableRef, false),
+ p => {
+ p.AddParameter ().Type ().IntPtr ();
+ p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true);
+ }),
+ emitIL);
+ }
+
+ MemberReferenceHandle AddActivationCtorRef (EntityHandle declaringTypeRef)
+ {
+ return _pe.AddMemberRef (declaringTypeRef, ".ctor",
+ sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2,
+ rt => rt.Void (),
+ p => {
+ p.AddParameter ().Type ().IntPtr ();
+ p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true);
+ }));
+ }
+
+ void EmitTypeGetter (string methodName, TypeRefData typeRef, MethodAttributes attrs)
+ {
+ var handle = _pe.ResolveTypeRef (typeRef);
+
+ _pe.EmitBody (methodName, attrs,
+ sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0,
+ rt => rt.Type ().Type (_systemTypeRef, false),
+ p => { }),
+ encoder => {
+ encoder.OpCode (ILOpCode.Ldtoken);
+ encoder.Token (handle);
+ encoder.Call (_getTypeFromHandleRef);
+ encoder.OpCode (ILOpCode.Ret);
+ });
+ }
+
+ void EmitTypeMapAttribute (TypeMapAttributeData entry)
+ {
+ var ctorRef = entry.IsUnconditional ? _typeMapAttrCtorRef2Arg : _typeMapAttrCtorRef3Arg;
+ var blob = _pe.BuildAttributeBlob (b => {
+ b.WriteSerializedString (entry.JniName);
+ b.WriteSerializedString (entry.ProxyTypeReference);
+ if (!entry.IsUnconditional) {
+ b.WriteSerializedString (entry.TargetTypeReference!);
+ }
+ });
+ _pe.Metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, ctorRef, blob);
+ }
+
+ void EmitTypeMapAssociationAttribute (TypeMapAssociationData assoc)
+ {
+ var blob = _pe.BuildAttributeBlob (b => {
+ b.WriteSerializedString (assoc.SourceTypeReference);
+ b.WriteSerializedString (assoc.AliasProxyTypeReference);
+ });
+ _pe.Metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, _typeMapAssociationAttrCtorRef, blob);
+ }
+
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs
new file mode 100644
index 00000000000..927346fbf10
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Collections.Generic;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+///
+/// High-level API: builds the model from peers, then emits the PE assembly.
+/// Composes + .
+///
+sealed class TypeMapAssemblyGenerator
+{
+ readonly Version _systemRuntimeVersion;
+
+ /// Version for System.Runtime assembly references.
+ public TypeMapAssemblyGenerator (Version systemRuntimeVersion)
+ {
+ _systemRuntimeVersion = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion));
+ }
+
+ ///
+ /// Generates a TypeMap PE assembly from the given Java peer info records.
+ ///
+ /// Scanned Java peer types.
+ /// Path where the output .dll will be written.
+ /// Optional explicit assembly name. Derived from outputPath if null.
+ public void Generate (IReadOnlyList peers, string outputPath, string? assemblyName = null)
+ {
+ var model = ModelBuilder.Build (peers, outputPath, assemblyName);
+ var emitter = new TypeMapAssemblyEmitter (_systemRuntimeVersion);
+ emitter.Emit (model, outputPath);
+ }
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs
index 85472f1b3ba..2de7a49ead9 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs
@@ -29,22 +29,19 @@ sealed record JavaPeerInfo
public required string ManagedTypeName { get; init; }
///
- /// Assembly name the type belongs to, e.g., "Mono.Android".
+ /// Managed type namespace, e.g., "Android.App".
///
- public required string AssemblyName { get; init; }
+ public required string ManagedTypeNamespace { get; init; }
///
- /// JNI name of the base Java type, e.g., "android/app/Activity" for a type
- /// that extends Activity. Null for java/lang/Object or types without a Java base.
- /// Needed by JCW Java source generation ("extends" clause).
+ /// Managed type short name (without namespace), e.g., "Activity".
///
- public string? BaseJavaName { get; init; }
+ public required string ManagedTypeShortName { get; init; }
///
- /// JNI names of Java interfaces this type implements, e.g., ["android/view/View$OnClickListener"].
- /// Needed by JCW Java source generation ("implements" clause).
+ /// Assembly name the type belongs to, e.g., "Mono.Android".
///
- public IReadOnlyList ImplementedInterfaceJavaNames { get; init; } = Array.Empty ();
+ public required string AssemblyName { get; init; }
public bool IsInterface { get; init; }
public bool IsAbstract { get; init; }
@@ -119,6 +116,35 @@ sealed record MarshalMethodInfo
///
public required string ManagedMethodName { get; init; }
+ ///
+ /// Full name of the type that declares the managed method (may be a base type).
+ /// Empty when the declaring type is the same as the peer type.
+ ///
+ public string DeclaringTypeName { get; init; } = "";
+
+ ///
+ /// Assembly name of the type that declares the managed method.
+ /// Needed for cross-assembly UCO wrapper generation.
+ /// Empty when the declaring type is the same as the peer type.
+ ///
+ public string DeclaringAssemblyName { get; init; } = "";
+
+ ///
+ /// The native callback method name, e.g., "n_onCreate".
+ /// This is the actual method the UCO wrapper delegates to.
+ ///
+ public required string NativeCallbackName { get; init; }
+
+ ///
+ /// JNI parameter types for UCO generation.
+ ///
+ public IReadOnlyList Parameters { get; init; } = Array.Empty ();
+
+ ///
+ /// JNI return type descriptor, e.g., "V", "Landroid/os/Bundle;".
+ ///
+ public required string JniReturnType { get; init; }
+
///
/// True if this is a constructor registration.
///
@@ -137,6 +163,22 @@ sealed record MarshalMethodInfo
public string? SuperArgumentsString { get; init; }
}
+///
+/// Describes a JNI parameter for UCO method generation.
+///
+sealed record JniParameterInfo
+{
+ ///
+ /// JNI type descriptor, e.g., "Landroid/os/Bundle;", "I", "Z".
+ ///
+ public required string JniType { get; init; }
+
+ ///
+ /// Managed parameter type name, e.g., "Android.OS.Bundle", "System.Int32".
+ ///
+ public string ManagedType { get; init; } = "";
+}
+
///
/// Describes how to call the activation constructor for a Java peer type.
///
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
index 469d6345596..81c7f8a4c5f 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
@@ -197,12 +197,6 @@ void ScanAssembly (AssemblyIndex index, Dictionary results
var isUnconditional = attrInfo is not null;
string? invokerTypeName = null;
- // Resolve base Java type name
- var baseJavaName = ResolveBaseJavaName (typeDef, index, results);
-
- // Resolve implemented Java interface names
- var implementedInterfaces = ResolveImplementedInterfaceJavaNames (typeDef, index);
-
// Collect marshal methods (including constructors) in a single pass over methods
var marshalMethods = CollectMarshalMethods (typeDef, index);
@@ -218,9 +212,9 @@ void ScanAssembly (AssemblyIndex index, Dictionary results
JavaName = jniName,
CompatJniName = compatJniName,
ManagedTypeName = fullName,
+ ManagedTypeNamespace = ExtractNamespace (fullName),
+ ManagedTypeShortName = ExtractShortName (fullName),
AssemblyName = index.AssemblyName,
- BaseJavaName = baseJavaName,
- ImplementedInterfaceJavaNames = implementedInterfaces,
IsInterface = isInterface,
IsAbstract = isAbstract,
DoNotGenerateAcw = doNotGenerateAcw,
@@ -274,62 +268,32 @@ static void AddMarshalMethod (List methods, RegisterInfo regi
return;
}
+ bool isConstructor = registerInfo.JniName == "" || registerInfo.JniName == ".ctor";
+ string nativeCallbackName = $"n_{index.Reader.GetString (methodDef.Name)}";
+ if (isConstructor) {
+ int ctorIndex = 0;
+ foreach (var method in methods) {
+ if (method.IsConstructor) {
+ ctorIndex++;
+ }
+ }
+ nativeCallbackName = ctorIndex == 0 ? "n_ctor" : $"n_ctor_{ctorIndex}";
+ }
+
methods.Add (new MarshalMethodInfo {
JniName = registerInfo.JniName,
JniSignature = registerInfo.Signature ?? "()V",
Connector = registerInfo.Connector,
ManagedMethodName = index.Reader.GetString (methodDef.Name),
- IsConstructor = registerInfo.JniName == "" || registerInfo.JniName == ".ctor",
+ NativeCallbackName = nativeCallbackName,
+ JniReturnType = JniSignatureHelper.ParseReturnTypeString (registerInfo.Signature ?? "()V"),
+ Parameters = ParseJniParameters (registerInfo.Signature ?? "()V"),
+ IsConstructor = isConstructor,
ThrownNames = exportInfo?.ThrownNames,
SuperArgumentsString = exportInfo?.SuperArgumentsString,
});
}
- string? ResolveBaseJavaName (TypeDefinition typeDef, AssemblyIndex index, Dictionary results)
- {
- var baseInfo = GetBaseTypeInfo (typeDef, index);
- if (baseInfo is null) {
- return null;
- }
-
- var (baseTypeName, baseAssemblyName) = baseInfo.Value;
-
- // First try [Register] attribute
- var registerJniName = ResolveRegisterJniName (baseTypeName, baseAssemblyName);
- if (registerJniName is not null) {
- return registerJniName;
- }
-
- // Fall back to already-scanned results (component-attributed or CRC64-computed peers)
- if (results.TryGetValue (baseTypeName, out var basePeer)) {
- return basePeer.JavaName;
- }
-
- return null;
- }
-
- List ResolveImplementedInterfaceJavaNames (TypeDefinition typeDef, AssemblyIndex index)
- {
- var result = new List ();
- var interfaceImpls = typeDef.GetInterfaceImplementations ();
-
- foreach (var implHandle in interfaceImpls) {
- var impl = index.Reader.GetInterfaceImplementation (implHandle);
- var ifaceJniName = ResolveInterfaceJniName (impl.Interface, index);
- if (ifaceJniName is not null) {
- result.Add (ifaceJniName);
- }
- }
-
- return result;
- }
-
- string? ResolveInterfaceJniName (EntityHandle interfaceHandle, AssemblyIndex index)
- {
- var resolved = ResolveEntityHandle (interfaceHandle, index);
- return resolved is not null ? ResolveRegisterJniName (resolved.Value.typeName, resolved.Value.assemblyName) : null;
- }
-
static bool TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex index, out RegisterInfo? registerInfo, out ExportInfo? exportInfo)
{
exportInfo = null;
@@ -732,4 +696,28 @@ static string GetCrc64PackageName (string ns, string assemblyName)
var hash = System.IO.Hashing.Crc64.Hash (data);
return $"crc64{BitConverter.ToString (hash).Replace ("-", "").ToLowerInvariant ()}";
}
+
+ static string ExtractNamespace (string fullName)
+ {
+ int lastDot = fullName.LastIndexOf ('.');
+ return lastDot >= 0 ? fullName.Substring (0, lastDot) : "";
+ }
+
+ static string ExtractShortName (string fullName)
+ {
+ int lastDot = fullName.LastIndexOf ('.');
+ string typePart = lastDot >= 0 ? fullName.Substring (lastDot + 1) : fullName;
+ int lastPlus = typePart.LastIndexOf ('+');
+ return lastPlus >= 0 ? typePart.Substring (lastPlus + 1) : typePart;
+ }
+
+ static List ParseJniParameters (string jniSignature)
+ {
+ var typeStrings = JniSignatureHelper.ParseParameterTypeStrings (jniSignature);
+ var result = new List (typeStrings.Count);
+ foreach (var t in typeStrings) {
+ result.Add (new JniParameterInfo { JniType = t });
+ }
+ return result;
+ }
}
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs
new file mode 100644
index 00000000000..bb446ff3029
--- /dev/null
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs
@@ -0,0 +1,109 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Xunit;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests;
+
+public abstract class FixtureTestBase
+{
+ static string TestFixtureAssemblyPath {
+ get {
+ var testAssemblyDir = Path.GetDirectoryName (typeof (FixtureTestBase).Assembly.Location)!;
+ var fixtureAssembly = Path.Combine (testAssemblyDir, "TestFixtures.dll");
+ Assert.True (File.Exists (fixtureAssembly),
+ $"TestFixtures.dll not found at {fixtureAssembly}. Ensure the TestFixtures project builds.");
+ return fixtureAssembly;
+ }
+ }
+
+ static readonly Lazy> _cachedFixtures = new (() => {
+ using var scanner = new JavaPeerScanner ();
+ return scanner.Scan (new [] { TestFixtureAssemblyPath });
+ });
+
+ protected static List ScanFixtures () => _cachedFixtures.Value;
+
+ protected static JavaPeerInfo FindFixtureByJavaName (string javaName)
+ {
+ var peers = ScanFixtures ();
+ var peer = peers.FirstOrDefault (p => p.JavaName == javaName);
+ Assert.NotNull (peer);
+ return peer;
+ }
+
+ protected static void CleanUpDir (string path)
+ {
+ var dir = Path.GetDirectoryName (path);
+ if (dir != null && Directory.Exists (dir))
+ try { Directory.Delete (dir, true); } catch { }
+ }
+
+ protected static JavaPeerInfo MakeMcwPeer (string jniName, string managedName, string asmName)
+ {
+ var ns = managedName.Contains ('.') ? managedName.Substring (0, managedName.LastIndexOf ('.')) : "";
+ var typePart = managedName.Contains ('.') ? managedName.Substring (managedName.LastIndexOf ('.') + 1) : managedName;
+ var shortName = typePart.Contains ('+') ? typePart.Substring (typePart.LastIndexOf ('+') + 1) : typePart;
+ return new JavaPeerInfo {
+ JavaName = jniName,
+ ManagedTypeName = managedName,
+ ManagedTypeNamespace = ns,
+ ManagedTypeShortName = shortName,
+ AssemblyName = asmName,
+ };
+ }
+
+ protected static JavaPeerInfo MakePeerWithActivation (string jniName, string managedName, string asmName)
+ {
+ var peer = MakeMcwPeer (jniName, managedName, asmName);
+ peer.ActivationCtor = new ActivationCtorInfo {
+ Style = ActivationCtorStyle.XamarinAndroid,
+ };
+ return peer;
+ }
+
+ protected static JavaPeerInfo MakeAcwPeer (string jniName, string managedName, string asmName)
+ {
+ var peer = MakePeerWithActivation (jniName, managedName, asmName);
+ peer.DoNotGenerateAcw = false;
+ peer.MarshalMethods = new List {
+ new MarshalMethodInfo {
+ JniName = "",
+ NativeCallbackName = "n_ctor",
+ JniSignature = "()V",
+ IsConstructor = true,
+ },
+ };
+ return peer;
+ }
+
+ protected static JavaPeerInfo MakeInterfacePeer (
+ string jniName = "android/view/View$OnClickListener",
+ string managedName = "Android.Views.View+IOnClickListener",
+ string asmName = "Mono.Android",
+ string invokerName = "Android.Views.View+IOnClickListenerInvoker")
+ {
+ var ns = managedName.Contains ('.') ? managedName.Substring (0, managedName.LastIndexOf ('.')) : "";
+ var shortName = managedName.Contains ('.') ? managedName.Substring (managedName.LastIndexOf ('.') + 1) : managedName;
+ return new JavaPeerInfo {
+ JavaName = jniName,
+ ManagedTypeName = managedName,
+ ManagedTypeNamespace = ns,
+ ManagedTypeShortName = shortName,
+ AssemblyName = asmName,
+ IsInterface = true,
+ InvokerTypeName = invokerName,
+ };
+ }
+
+ protected static MarshalMethodInfo MakeMarshalMethod (string jniName, string callbackName, string jniSig, bool isConstructor = false)
+ {
+ return new MarshalMethodInfo {
+ JniName = jniName,
+ NativeCallbackName = callbackName,
+ JniSignature = jniSig,
+ IsConstructor = isConstructor,
+ };
+ }
+}
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs
new file mode 100644
index 00000000000..b6dfe06c6b2
--- /dev/null
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs
@@ -0,0 +1,117 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection.Metadata;
+using System.Reflection.PortableExecutable;
+using Xunit;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests;
+
+public class RootTypeMapAssemblyGeneratorTests : FixtureTestBase, IDisposable
+{
+ readonly string _outputDir = CreateTempDir ();
+ public void Dispose () => DeleteTempDir (_outputDir);
+
+ string GenerateRootAssembly (IReadOnlyList perAssemblyNames, string? assemblyName = null)
+ {
+ var outputPath = Path.Combine (_outputDir,
+ (assemblyName ?? "_Microsoft.Android.TypeMaps") + ".dll");
+ var generator = new RootTypeMapAssemblyGenerator (new Version (11, 0, 0, 0));
+ generator.Generate (perAssemblyNames, outputPath, assemblyName);
+ return outputPath;
+ }
+
+ [Fact]
+ public void Generate_ProducesValidPEAssembly ()
+ {
+ var path = GenerateRootAssembly (new [] { "_App.TypeMap", "_Mono.Android.TypeMap" });
+ Assert.True (File.Exists (path));
+ using var pe = new PEReader (File.OpenRead (path));
+ Assert.True (pe.HasMetadata);
+ }
+
+ [Theory]
+ [InlineData (null, "_Microsoft.Android.TypeMaps")]
+ [InlineData ("MyRoot", "MyRoot")]
+ public void Generate_AssemblyName_MatchesExpected (string? assemblyName, string expectedName)
+ {
+ var path = GenerateRootAssembly (Array.Empty (), assemblyName);
+ using var pe = new PEReader (File.OpenRead (path));
+ var reader = pe.GetMetadataReader ();
+ var asmDef = reader.GetAssemblyDefinition ();
+ Assert.Equal (expectedName, reader.GetString (asmDef.Name));
+ }
+
+ [Fact]
+ public void Generate_ReferencesGenericTypeMapAssemblyTargetAttribute ()
+ {
+ var path = GenerateRootAssembly (new [] { "_App.TypeMap" });
+ using var pe = new PEReader (File.OpenRead (path));
+ var reader = pe.GetMetadataReader ();
+
+ var typeRefs = reader.TypeReferences
+ .Select (h => reader.GetTypeReference (h))
+ .ToList ();
+ Assert.Contains (typeRefs, t =>
+ reader.GetString (t.Name) == "TypeMapAssemblyTargetAttribute`1" &&
+ reader.GetString (t.Namespace) == "System.Runtime.InteropServices");
+
+ Assert.Contains (typeRefs, t =>
+ reader.GetString (t.Name) == "Object" &&
+ reader.GetString (t.Namespace) == "Java.Lang");
+
+ var typeDefs = reader.TypeDefinitions
+ .Select (h => reader.GetTypeDefinition (h))
+ .ToList ();
+ Assert.DoesNotContain (typeDefs, t =>
+ reader.GetString (t.Name).Contains ("TypeMapAssemblyTarget"));
+ }
+
+ [Fact]
+ public void Generate_EmptyList_ProducesValidAssemblyWithNoTargetAttributes ()
+ {
+ var path = GenerateRootAssembly (Array.Empty ());
+ using var pe = new PEReader (File.OpenRead (path));
+ var reader = pe.GetMetadataReader ();
+ var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition);
+ Assert.Empty (asmAttrs);
+ }
+
+ [Fact]
+ public void Generate_MultipleTargets_HasCorrectAttributeCount ()
+ {
+ var targets = new [] { "_App.TypeMap", "_Mono.Android.TypeMap", "_Java.Interop.TypeMap" };
+ var path = GenerateRootAssembly (targets);
+ using var pe = new PEReader (File.OpenRead (path));
+ var reader = pe.GetMetadataReader ();
+ var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition);
+ Assert.Equal (3, asmAttrs.Count ());
+ }
+
+ [Fact]
+ public void Generate_AttributeBlobValues_MatchTargetNames ()
+ {
+ var targets = new [] { "_App.TypeMap", "_Mono.Android.TypeMap" };
+ var path = GenerateRootAssembly (targets);
+ using var pe = new PEReader (File.OpenRead (path));
+ var reader = pe.GetMetadataReader ();
+
+ var attrValues = new List ();
+ foreach (var attrHandle in reader.GetCustomAttributes (EntityHandle.AssemblyDefinition)) {
+ var attr = reader.GetCustomAttribute (attrHandle);
+ var blob = reader.GetBlobReader (attr.Value);
+
+ // Custom attribute blob: prolog (2 bytes) + SerString value
+ var prolog = blob.ReadUInt16 ();
+ Assert.Equal (1, prolog); // ECMA-335 prolog
+ var value = blob.ReadSerializedString ();
+ Assert.NotNull (value);
+ attrValues.Add (value!);
+ }
+
+ Assert.Equal (2, attrValues.Count);
+ Assert.Contains ("_App.TypeMap", attrValues);
+ Assert.Contains ("_Mono.Android.TypeMap", attrValues);
+ }
+}
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs
new file mode 100644
index 00000000000..cceb2e20a62
--- /dev/null
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs
@@ -0,0 +1,333 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Reflection.Metadata;
+using System.Reflection.Metadata.Ecma335;
+using System.Reflection.PortableExecutable;
+using Xunit;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests;
+
+public class TypeMapAssemblyGeneratorTests : FixtureTestBase
+{
+ static string GenerateAssembly (IReadOnlyList peers, string outputDir, string? assemblyName = null)
+ {
+ var outputPath = Path.Combine (outputDir, (assemblyName ?? "TestTypeMap") + ".dll");
+ var generator = new TypeMapAssemblyGenerator (new Version (11, 0, 0, 0));
+ generator.Generate (peers, outputPath, assemblyName);
+ return outputPath;
+ }
+
+ static (PEReader pe, MetadataReader reader) OpenAssembly (string path)
+ {
+ var pe = new PEReader (File.OpenRead (path));
+ return (pe, pe.GetMetadataReader ());
+ }
+
+ static List GetMemberRefNames (MetadataReader reader) =>
+ Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef))
+ .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i)))
+ .Select (m => reader.GetString (m.Name))
+ .ToList ();
+
+ static List GetTypeRefNames (MetadataReader reader) =>
+ reader.TypeReferences
+ .Select (h => reader.GetTypeReference (h))
+ .Select (t => reader.GetString (t.Name))
+ .ToList ();
+
+ public class BasicAssemblyStructure : IDisposable
+ {
+ readonly string _outputDir = CreateTempDir ();
+ public void Dispose () => DeleteTempDir (_outputDir);
+
+ [Fact]
+ public void Generate_ProducesValidPEAssembly ()
+ {
+ var peers = ScanFixtures ();
+ var path = GenerateAssembly (peers, _outputDir);
+ Assert.True (File.Exists (path));
+ using var pe = new PEReader (File.OpenRead (path));
+ Assert.True (pe.HasMetadata);
+ var reader = pe.GetMetadataReader ();
+ Assert.NotNull (reader);
+ }
+
+ }
+
+ public class AssemblyReference : IDisposable
+ {
+ readonly string _outputDir = CreateTempDir ();
+ public void Dispose () => DeleteTempDir (_outputDir);
+
+ [Fact]
+ public void Generate_HasRequiredAssemblyReferences ()
+ {
+ var peers = ScanFixtures ();
+ var path = GenerateAssembly (peers, _outputDir);
+ var (pe, reader) = OpenAssembly (path);
+ using (pe) {
+ var asmRefs = reader.AssemblyReferences
+ .Select (h => reader.GetString (reader.GetAssemblyReference (h).Name))
+ .ToList ();
+ Assert.Contains ("System.Runtime", asmRefs);
+ Assert.Contains ("Mono.Android", asmRefs);
+ Assert.Contains ("Java.Interop", asmRefs);
+ Assert.Contains ("System.Runtime.InteropServices", asmRefs);
+ }
+ }
+
+ }
+
+ public class ProxyType : IDisposable
+ {
+ readonly string _outputDir = CreateTempDir ();
+ public void Dispose () => DeleteTempDir (_outputDir);
+
+ [Fact]
+ public void Generate_CreatesProxyTypes ()
+ {
+ var peers = ScanFixtures ();
+ var path = GenerateAssembly (peers, _outputDir);
+ var (pe, reader) = OpenAssembly (path);
+ using (pe) {
+ var proxyTypes = reader.TypeDefinitions
+ .Select (h => reader.GetTypeDefinition (h))
+ .Where (t => reader.GetString (t.Namespace) == "_TypeMap.Proxies")
+ .ToList ();
+
+ Assert.NotEmpty (proxyTypes);
+ Assert.Contains (proxyTypes, t => reader.GetString (t.Name) == "Java_Lang_Object_Proxy");
+ }
+ }
+
+ [Fact]
+ public void Generate_ProxyType_HasCtorAndCreateInstance ()
+ {
+ var peers = ScanFixtures ();
+ var path = GenerateAssembly (peers, _outputDir);
+ var (pe, reader) = OpenAssembly (path);
+ using (pe) {
+ var objectProxy = reader.TypeDefinitions
+ .Select (h => reader.GetTypeDefinition (h))
+ .First (t => reader.GetString (t.Name) == "Java_Lang_Object_Proxy");
+
+ var methods = objectProxy.GetMethods ()
+ .Select (h => reader.GetMethodDefinition (h))
+ .Select (m => reader.GetString (m.Name))
+ .ToList ();
+
+ Assert.Contains (".ctor", methods);
+ Assert.Contains ("CreateInstance", methods);
+ Assert.Contains ("get_TargetType", methods);
+ }
+ }
+
+ }
+
+ public class IgnoresAccessChecksTo : IDisposable
+ {
+ readonly string _outputDir = CreateTempDir ();
+ public void Dispose () => DeleteTempDir (_outputDir);
+
+ [Fact]
+ public void Generate_HasIgnoresAccessChecksToAttribute ()
+ {
+ var peers = ScanFixtures ();
+ var path = GenerateAssembly (peers, _outputDir);
+ var (pe, reader) = OpenAssembly (path);
+ using (pe) {
+ var types = reader.TypeDefinitions
+ .Select (h => reader.GetTypeDefinition (h))
+ .ToList ();
+ Assert.Contains (types, t =>
+ reader.GetString (t.Name) == "IgnoresAccessChecksToAttribute" &&
+ reader.GetString (t.Namespace) == "System.Runtime.CompilerServices");
+ }
+ }
+
+ }
+
+ public class Alias : IDisposable
+ {
+ readonly string _outputDir = CreateTempDir ();
+ public void Dispose () => DeleteTempDir (_outputDir);
+
+ static List MakeDuplicateAliasPeers () => new List {
+ new JavaPeerInfo {
+ JavaName = "test/Duplicate",
+ ManagedTypeName = "Test.Duplicate1",
+ ManagedTypeNamespace = "Test",
+ ManagedTypeShortName = "Duplicate1",
+ AssemblyName = "TestAssembly",
+ ActivationCtor = new ActivationCtorInfo {
+ DeclaringTypeName = "Test.Duplicate1",
+ DeclaringAssemblyName = "TestAssembly",
+ Style = ActivationCtorStyle.XamarinAndroid,
+ },
+ },
+ new JavaPeerInfo {
+ JavaName = "test/Duplicate",
+ ManagedTypeName = "Test.Duplicate2",
+ ManagedTypeNamespace = "Test",
+ ManagedTypeShortName = "Duplicate2",
+ AssemblyName = "TestAssembly",
+ },
+ };
+
+ [Fact]
+ public void Generate_DuplicateJniNames_CreatesAliasEntries ()
+ {
+ var peers = MakeDuplicateAliasPeers ();
+ var path = GenerateAssembly (peers, _outputDir, "AliasTest");
+ var (pe, reader) = OpenAssembly (path);
+ using (pe) {
+ var assemblyAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition);
+ Assert.True (assemblyAttrs.Count () >= 3);
+ }
+ }
+
+ [Fact]
+ public void Generate_DuplicateJniNames_EmitsTypeMapAssociationAttribute ()
+ {
+ var peers = MakeDuplicateAliasPeers ();
+ var path = GenerateAssembly (peers, _outputDir, "AliasAssocTest");
+ var (pe, reader) = OpenAssembly (path);
+ using (pe) {
+ var memberRefs = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef))
+ .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i)))
+ .Where (m => reader.GetString (m.Name) == ".ctor")
+ .ToList ();
+
+ var typeNames = GetTypeRefNames (reader);
+ Assert.Contains ("TypeMapAssociationAttribute", typeNames);
+ }
+ }
+
+ }
+
+ public class EmptyInput : IDisposable
+ {
+ readonly string _outputDir = CreateTempDir ();
+ public void Dispose () => DeleteTempDir (_outputDir);
+
+ [Fact]
+ public void Generate_EmptyPeerList_ProducesValidAssembly ()
+ {
+ var path = GenerateAssembly (Array.Empty (), _outputDir, "EmptyTest");
+ Assert.True (File.Exists (path));
+ var (pe, reader) = OpenAssembly (path);
+ using (pe) {
+ Assert.NotNull (reader);
+ var asmDef = reader.GetAssemblyDefinition ();
+ Assert.Equal ("EmptyTest", reader.GetString (asmDef.Name));
+ }
+ }
+
+ }
+
+ public class CreateInstancePaths : IDisposable
+ {
+ readonly string _outputDir = CreateTempDir ();
+ public void Dispose () => DeleteTempDir (_outputDir);
+
+ [Fact]
+ public void Generate_SimpleActivity_UsesGetUninitializedObject ()
+ {
+ var peers = ScanFixtures ();
+ var simpleActivity = peers.First (p => p.JavaName == "my/app/SimpleActivity");
+ Assert.NotNull (simpleActivity.ActivationCtor);
+ Assert.NotEqual (simpleActivity.ManagedTypeName, simpleActivity.ActivationCtor.DeclaringTypeName);
+
+ var path = GenerateAssembly (new [] { simpleActivity }, _outputDir, "InheritedCtorTest");
+ var (pe, reader) = OpenAssembly (path);
+ using (pe) {
+ var typeNames = GetTypeRefNames (reader);
+ Assert.Contains ("RuntimeHelpers", typeNames);
+
+ var memberNames = GetMemberRefNames (reader);
+ Assert.DoesNotContain ("CreateManagedPeer", memberNames);
+ Assert.Contains ("GetUninitializedObject", memberNames);
+ }
+ }
+
+ [Fact]
+ public void Generate_LeafCtor_DoesNotUseCreateManagedPeer ()
+ {
+ var peers = ScanFixtures ();
+ // ClickableView has its own (IntPtr, JniHandleOwnership) ctor
+ var clickableView = peers.First (p => p.JavaName == "my/app/ClickableView");
+ Assert.NotNull (clickableView.ActivationCtor);
+ Assert.Equal (clickableView.ManagedTypeName, clickableView.ActivationCtor.DeclaringTypeName);
+
+ var path = GenerateAssembly (new [] { clickableView }, _outputDir, "LeafCtorTest");
+ var (pe, reader) = OpenAssembly (path);
+ using (pe) {
+ var memberNames = GetMemberRefNames (reader);
+ Assert.DoesNotContain ("CreateManagedPeer", memberNames);
+
+ var ctorRefs = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef))
+ .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i)))
+ .Where (m => reader.GetString (m.Name) == ".ctor")
+ .ToList ();
+ Assert.True (ctorRefs.Count >= 2, "Should have ctor refs for proxy base + target type");
+ }
+ }
+
+ [Fact]
+ public void Generate_GenericType_ThrowsNotSupportedException ()
+ {
+ var peers = ScanFixtures ();
+ var generic = peers.First (p => p.JavaName == "my/app/GenericHolder");
+ Assert.True (generic.IsGenericDefinition);
+
+ var path = GenerateAssembly (new [] { generic }, _outputDir, "GenericTest");
+ var (pe, reader) = OpenAssembly (path);
+ using (pe) {
+ var typeNames = GetTypeRefNames (reader);
+ Assert.Contains ("NotSupportedException", typeNames);
+ }
+ }
+
+ }
+
+ public class IgnoresAccessChecksToForBaseCtor : IDisposable
+ {
+ readonly string _outputDir = CreateTempDir ();
+ public void Dispose () => DeleteTempDir (_outputDir);
+
+ [Fact]
+ public void Generate_InheritedCtor_IncludesBaseCtorAssembly ()
+ {
+ // SimpleActivity inherits activation ctor from Activity — both in TestFixtures
+ // but the generated assembly is "IgnoresAccessTest", so TestFixtures must be
+ // in IgnoresAccessChecksTo
+ var peers = ScanFixtures ();
+ var simpleActivity = peers.First (p => p.JavaName == "my/app/SimpleActivity");
+
+ var path = GenerateAssembly (new [] { simpleActivity }, _outputDir, "IgnoresAccessTest");
+ var (pe, reader) = OpenAssembly (path);
+ using (pe) {
+ var ignoresAttrType = reader.TypeDefinitions
+ .Select (h => reader.GetTypeDefinition (h))
+ .FirstOrDefault (t => reader.GetString (t.Name) == "IgnoresAccessChecksToAttribute");
+ Assert.True (ignoresAttrType.Attributes != 0, "IgnoresAccessChecksToAttribute should be defined");
+
+ var assemblyAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition);
+ var attrBlobs = new List ();
+ foreach (var attrHandle in assemblyAttrs) {
+ var attr = reader.GetCustomAttribute (attrHandle);
+ var blob = reader.GetBlobBytes (attr.Value);
+ var blobStr = System.Text.Encoding.UTF8.GetString (blob);
+ attrBlobs.Add (blobStr);
+ }
+ // Activity is in TestFixtures, so IgnoresAccessChecksTo must include TestFixtures
+ Assert.Contains (attrBlobs, b => b.Contains ("TestFixtures"));
+ }
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs
new file mode 100644
index 00000000000..54d66fc03f7
--- /dev/null
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs
@@ -0,0 +1,793 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection.Metadata;
+using System.Reflection.PortableExecutable;
+using Xunit;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests;
+
+public class ModelBuilderTests : FixtureTestBase
+{
+ static TypeMapAssemblyData BuildModel (IReadOnlyList peers, string? assemblyName = null)
+ {
+ var outputPath = Path.Combine ("/tmp", (assemblyName ?? "TestTypeMap") + ".dll");
+ return ModelBuilder.Build (peers, outputPath, assemblyName);
+ }
+
+
+ public class BasicStructure
+ {
+
+ [Fact]
+ public void Build_EmptyPeers_ProducesEmptyModel ()
+ {
+ var model = BuildModel (Array.Empty (), "Empty");
+ Assert.Equal ("Empty", model.AssemblyName);
+ Assert.Equal ("Empty.dll", model.ModuleName);
+ Assert.Empty (model.Entries);
+ Assert.Empty (model.ProxyTypes);
+ }
+
+ [Fact]
+ public void Build_AssemblyNameDerivedFromOutputPath ()
+ {
+ var model = ModelBuilder.Build (Array.Empty (), "/some/path/Foo.Bar.dll");
+ Assert.Equal ("Foo.Bar", model.AssemblyName);
+ Assert.Equal ("Foo.Bar.dll", model.ModuleName);
+ }
+
+ [Fact]
+ public void Build_ExplicitAssemblyName_OverridesOutputPath ()
+ {
+ var model = ModelBuilder.Build (Array.Empty (), "/some/path/Foo.dll", "MyAssembly");
+ Assert.Equal ("MyAssembly", model.AssemblyName);
+ }
+
+ }
+
+ public class TypeMapEntries
+ {
+
+ [Fact]
+ public void Build_CreatesOneEntryPerPeer ()
+ {
+ var peers = new List {
+ MakeMcwPeer ("java/lang/Object", "Java.Lang.Object", "Mono.Android"),
+ MakeMcwPeer ("android/app/Activity", "Android.App.Activity", "Mono.Android"),
+ };
+
+ var model = BuildModel (peers);
+ Assert.Equal (2, model.Entries.Count);
+ Assert.Equal ("android/app/Activity", model.Entries [0].JniName);
+ Assert.Equal ("java/lang/Object", model.Entries [1].JniName);
+ }
+
+ [Fact]
+ public void Build_DuplicateJniNames_CreatesAliasEntries ()
+ {
+ var peers = new List {
+ MakeMcwPeer ("test/Dup", "Test.First", "A"),
+ MakeMcwPeer ("test/Dup", "Test.Second", "A"),
+ };
+
+ var model = BuildModel (peers);
+ // Two entries: primary "test/Dup" and alias "test/Dup[1]"
+ Assert.Equal (2, model.Entries.Count);
+ Assert.Equal ("test/Dup", model.Entries [0].JniName);
+ Assert.Contains ("Test.First", model.Entries [0].ProxyTypeReference);
+ Assert.Equal ("test/Dup[1]", model.Entries [1].JniName);
+ Assert.Contains ("Test.Second", model.Entries [1].ProxyTypeReference);
+ }
+
+ }
+
+ public class ConditionalAttributes
+ {
+
+ [Theory]
+ [InlineData ("java/lang/Object")]
+ [InlineData ("java/lang/Throwable")]
+ [InlineData ("java/lang/Exception")]
+ [InlineData ("java/lang/RuntimeException")]
+ [InlineData ("java/lang/Error")]
+ [InlineData ("java/lang/Class")]
+ [InlineData ("java/lang/String")]
+ [InlineData ("java/lang/Thread")]
+ public void Build_AllEssentialRuntimeTypes_AreUnconditional (string jniName)
+ {
+ var peer = MakeMcwPeer (jniName, "Java.Lang.SomeType", "Mono.Android");
+ peer.DoNotGenerateAcw = true;
+ var model = BuildModel (new [] { peer });
+ Assert.True (model.Entries [0].IsUnconditional, $"{jniName} should be unconditional");
+ }
+
+ [Fact]
+ public void Build_UserAcwType_IsUnconditional ()
+ {
+ // User-defined ACW types (not MCW, not interface) are unconditional
+ // because Android can instantiate them from Java
+ var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App");
+ var model = BuildModel (new [] { peer });
+
+ var mainEntry = model.Entries.First (e => e.JniName == "my/app/Main");
+ Assert.True (mainEntry.IsUnconditional);
+ Assert.Null (mainEntry.TargetTypeReference);
+ }
+
+ [Fact]
+ public void Build_McwBinding_IsTrimmable ()
+ {
+ // MCW binding types (DoNotGenerateAcw=true) are trimmable unless essential
+ var peer = MakeMcwPeer ("android/app/Activity", "Android.App.Activity", "Mono.Android");
+ peer.DoNotGenerateAcw = true;
+ var model = BuildModel (new [] { peer });
+
+ Assert.Single (model.Entries);
+ Assert.False (model.Entries [0].IsUnconditional);
+ Assert.NotNull (model.Entries [0].TargetTypeReference);
+ Assert.Contains ("Android.App.Activity, Mono.Android", model.Entries [0].TargetTypeReference!);
+ }
+
+ [Fact]
+ public void Build_UnconditionalScannedType_IsUnconditional ()
+ {
+ // Types with IsUnconditional from scanner (e.g., from [Activity], [Service] attrs)
+ var peer = MakeMcwPeer ("my/app/MySvc", "MyApp.MyService", "App");
+ peer.DoNotGenerateAcw = true; // simulate MCW-like
+ peer.IsUnconditional = true; // scanner marked it
+ var model = BuildModel (new [] { peer });
+
+ Assert.True (model.Entries [0].IsUnconditional);
+ }
+
+ }
+
+ public class Aliases
+ {
+
+ [Fact]
+ public void Build_AliasedPeersWithActivation_GetDistinctProxies ()
+ {
+ var peers = new List {
+ MakePeerWithActivation ("test/Dup", "Test.First", "A"),
+ MakePeerWithActivation ("test/Dup", "Test.Second", "A"),
+ };
+
+ var model = BuildModel (peers, "TypeMap");
+ Assert.Equal (2, model.ProxyTypes.Count);
+ Assert.Equal ("Test_First_Proxy", model.ProxyTypes [0].TypeName);
+ Assert.Equal ("Test_Second_Proxy", model.ProxyTypes [1].TypeName);
+ }
+
+ [Fact]
+ public void Build_McwPeerWithoutActivation_NoProxy ()
+ {
+ var peer = MakeMcwPeer ("java/lang/Object", "Java.Lang.Object", "Mono.Android");
+ var model = BuildModel (new [] { peer });
+
+ Assert.Empty (model.ProxyTypes);
+ Assert.Single (model.Entries);
+ Assert.Contains ("Java.Lang.Object, Mono.Android", model.Entries [0].ProxyTypeReference);
+ }
+
+ }
+
+ public class ProxyTypes
+ {
+
+ [Fact]
+ public void Build_PeerWithActivationCtor_CreatesProxy ()
+ {
+ var peer = MakePeerWithActivation ("java/lang/Object", "Java.Lang.Object", "Mono.Android");
+ var model = BuildModel (new [] { peer }, "MyTypeMap");
+
+ Assert.Single (model.ProxyTypes);
+ var proxy = model.ProxyTypes [0];
+ Assert.Equal ("Java_Lang_Object_Proxy", proxy.TypeName);
+ Assert.Equal ("_TypeMap.Proxies", proxy.Namespace);
+ Assert.True (proxy.HasActivation);
+ Assert.Equal ("Java.Lang.Object", proxy.TargetType.ManagedTypeName);
+ Assert.Equal ("Mono.Android", proxy.TargetType.AssemblyName);
+ }
+
+ [Fact]
+ public void Build_PeerWithInvoker_CreatesProxy ()
+ {
+ var peer = MakeInterfacePeer ();
+
+ var model = BuildModel (new [] { peer });
+ Assert.Single (model.ProxyTypes);
+ var proxy = model.ProxyTypes [0];
+ Assert.NotNull (proxy.InvokerType);
+ Assert.Equal ("Android.Views.View+IOnClickListenerInvoker", proxy.InvokerType!.ManagedTypeName);
+ }
+
+ [Fact]
+ public void Build_ProxyNaming_ReplacesDotAndPlus ()
+ {
+ var peer = MakePeerWithActivation ("com/example/Outer$Inner", "Com.Example.Outer.Inner", "App");
+ var model = BuildModel (new [] { peer });
+
+ Assert.Single (model.ProxyTypes);
+ Assert.Equal ("Com_Example_Outer_Inner_Proxy", model.ProxyTypes [0].TypeName);
+ }
+
+ }
+
+ public class FixtureScan
+ {
+
+ [Fact]
+ public void Build_FromScannedFixtures_ProducesValidModel ()
+ {
+ var peers = ScanFixtures ();
+ var model = BuildModel (peers, "TestTypeMap");
+
+ Assert.Equal ("TestTypeMap", model.AssemblyName);
+ Assert.NotEmpty (model.Entries);
+ Assert.NotEmpty (model.ProxyTypes);
+
+ Assert.All (model.Entries, e => Assert.False (string.IsNullOrEmpty (e.JniName)));
+ Assert.All (model.Entries, e => Assert.False (string.IsNullOrEmpty (e.ProxyTypeReference)));
+ }
+
+ [Theory]
+ [InlineData ("my/app/MainActivity", "MainActivity")]
+ [InlineData ("android/app/Activity", "Activity")]
+ [InlineData ("java/lang/Object", "Object")]
+ [InlineData ("my/app/Outer$Inner", "Inner")]
+ [InlineData ("my/app/ICallback$Result", "Result")]
+ public void ScanFixtures_ManagedTypeShortName_IsCorrect (string javaName, string expectedShortName)
+ {
+ var peer = FindFixtureByJavaName (javaName);
+ Assert.Equal (expectedShortName, peer.ManagedTypeShortName);
+ }
+
+ }
+
+ public class FixtureConditionalAttributes
+ {
+
+ [Theory]
+ [InlineData ("my/app/MainActivity")]
+ [InlineData ("my/app/TouchHandler")]
+ public void Fixture_UserAcwType_IsUnconditional (string javaName)
+ {
+ var peer = FindFixtureByJavaName (javaName);
+ Assert.False (peer.DoNotGenerateAcw);
+ var model = BuildModel (new [] { peer });
+ Assert.True (model.Entries [0].IsUnconditional);
+ }
+
+ [Theory]
+ [InlineData ("android/app/Activity")]
+ [InlineData ("android/widget/Button")]
+ public void Fixture_McwBinding_IsTrimmable (string javaName)
+ {
+ var peer = FindFixtureByJavaName (javaName);
+ Assert.True (peer.DoNotGenerateAcw);
+ var model = BuildModel (new [] { peer });
+ Assert.False (model.Entries [0].IsUnconditional);
+ }
+
+ }
+
+ static JavaPeerProxyData? FindProxy (TypeMapAssemblyData model, string proxyTypeName)
+ {
+ return model.ProxyTypes.FirstOrDefault (p => p.TypeName == proxyTypeName);
+ }
+
+ static TypeMapAttributeData? FindEntry (TypeMapAssemblyData model, string jniName)
+ {
+ return model.Entries.FirstOrDefault (e => e.JniName == jniName);
+ }
+
+
+ public class FixtureMcwTypes
+ {
+
+ [Theory]
+ [InlineData ("java/lang/Object", "Java_Lang_Object_Proxy", "Java.Lang.Object")]
+ [InlineData ("android/app/Activity", "Android_App_Activity_Proxy", "Android.App.Activity")]
+ [InlineData ("java/lang/Throwable", "Java_Lang_Throwable_Proxy", "Java.Lang.Throwable")]
+ [InlineData ("java/lang/Exception", "Java_Lang_Exception_Proxy", "Java.Lang.Exception")]
+ public void Fixture_McwType_HasActivation_CreatesProxy (string javaName, string expectedProxyName, string expectedManagedName)
+ {
+ var peer = FindFixtureByJavaName (javaName);
+ var model = BuildModel (new [] { peer }, "TypeMap");
+
+ var proxy = FindProxy (model, expectedProxyName);
+ Assert.NotNull (proxy);
+ Assert.True (proxy!.HasActivation);
+ Assert.Equal (expectedManagedName, proxy.TargetType.ManagedTypeName);
+ }
+
+ [Fact]
+ public void Fixture_Activity_Entry_PointsToProxy ()
+ {
+ var peer = FindFixtureByJavaName ("android/app/Activity");
+ var model = BuildModel (new [] { peer }, "MyTypeMap");
+
+ var entry = FindEntry (model, "android/app/Activity");
+ Assert.NotNull (entry);
+ Assert.Contains ("Android_App_Activity_Proxy", entry!.ProxyTypeReference);
+ Assert.Contains ("MyTypeMap", entry.ProxyTypeReference);
+ }
+
+ [Fact]
+ public void Fixture_Service_NoActivation_NoProxy ()
+ {
+ // Service in fixtures has no activation ctor on its own — it inherits from J.L.Object
+ // but Service itself has `protected Service(IntPtr, JniHandleOwnership)` which IS an activation ctor
+ var peer = FindFixtureByJavaName ("android/app/Service");
+ var model = BuildModel (new [] { peer }, "TypeMap");
+
+ if (peer.ActivationCtor != null) {
+ Assert.Single (model.ProxyTypes);
+ } else {
+ Assert.Empty (model.ProxyTypes);
+ }
+ }
+
+ }
+
+ public class FixtureCustomView
+ {
+
+ [Fact]
+ public void Fixture_CustomView_HasTwoConstructors ()
+ {
+ var peer = FindFixtureByJavaName ("my/app/CustomView");
+
+ var model = BuildModel (new [] { peer }, "TypeMap");
+ var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_CustomView_Proxy");
+ Assert.NotNull (proxy);
+ }
+
+ }
+
+ public class FixtureInterfaces
+ {
+
+ [Fact]
+ public void Fixture_IOnClickListener_HasInvokerProxy ()
+ {
+ var peers = ScanFixtures ();
+ var listener = peers.FirstOrDefault (p => p.ManagedTypeName == "Android.Views.IOnClickListener");
+ Assert.NotNull (listener);
+ Assert.True (listener!.IsInterface);
+ Assert.NotNull (listener.InvokerTypeName);
+
+ var model = BuildModel (new [] { listener }, "TypeMap");
+ var proxy = model.ProxyTypes.FirstOrDefault ();
+ Assert.NotNull (proxy);
+ Assert.NotNull (proxy!.InvokerType);
+ Assert.Equal ("Android.Views.IOnClickListenerInvoker", proxy.InvokerType!.ManagedTypeName);
+ }
+
+ }
+
+ public class FixtureNestedTypes
+ {
+
+ [Theory]
+ [InlineData ("my/app/Outer$Inner", "MyApp_Outer_Inner_Proxy", "MyApp.Outer+Inner")]
+ [InlineData ("my/app/ICallback$Result", "MyApp_ICallback_Result_Proxy", "MyApp.ICallback+Result")]
+ public void Fixture_NestedType_ProxyNaming (string javaName, string expectedProxyName, string expectedManagedName)
+ {
+ var peer = FindFixtureByJavaName (javaName);
+ var model = BuildModel (new [] { peer }, "TypeMap");
+
+ var entry = FindEntry (model, javaName);
+ Assert.NotNull (entry);
+
+ if (peer.ActivationCtor != null) {
+ var proxy = FindProxy (model, expectedProxyName);
+ Assert.NotNull (proxy);
+ Assert.Equal (expectedManagedName, proxy!.TargetType.ManagedTypeName);
+ }
+ }
+
+ }
+
+ public class FixtureInvokers
+ {
+
+ [Fact]
+ public void Fixture_InterfaceAndInvoker_ShareJniName_InvokerSeparated ()
+ {
+ var peers = ScanFixtures ();
+ // IOnClickListener and IOnClickListenerInvoker share "android/view/View$OnClickListener"
+ var clickPeers = peers.Where (p => p.JavaName == "android/view/View$OnClickListener").ToList ();
+ Assert.Equal (2, clickPeers.Count);
+
+ var model = BuildModel (clickPeers, "TypeMap");
+
+ // Invoker is excluded entirely — no TypeMap entry, no proxy.
+ // Only the interface gets a TypeMap entry and a proxy.
+ Assert.Single (model.Entries);
+ Assert.Equal ("android/view/View$OnClickListener", model.Entries [0].JniName);
+
+ // Only the interface proxy exists; the invoker type is referenced
+ // only as a TypeRef in the interface proxy's InvokerType property.
+ Assert.Single (model.ProxyTypes);
+ Assert.NotNull (model.ProxyTypes [0].InvokerType);
+ Assert.Equal ("Android.Views.IOnClickListenerInvoker", model.ProxyTypes [0].InvokerType!.ManagedTypeName);
+ }
+
+ [Fact]
+ public void Build_InvokerType_NoProxyNoEntry ()
+ {
+ // Invoker types should never get their own proxy or TypeMap entry.
+ // They only appear as a TypeRef in the interface proxy's InvokerType/CreateInstance.
+ var ifacePeer = MakeInterfacePeer ("my/app/IFoo", "MyApp.IFoo", "App", "MyApp.FooInvoker");
+ var invokerPeer = MakePeerWithActivation ("my/app/IFoo", "MyApp.FooInvoker", "App");
+ invokerPeer.DoNotGenerateAcw = true;
+
+ var model = BuildModel (new [] { ifacePeer, invokerPeer });
+
+ // Only the interface gets a TypeMap entry — its ProxyTypeReference points to the generated proxy
+ Assert.Single (model.Entries);
+ Assert.Contains ("MyApp_IFoo_Proxy", model.Entries [0].ProxyTypeReference);
+
+ // Only the interface gets a proxy — the invoker is referenced, not proxied
+ Assert.Single (model.ProxyTypes);
+ var proxy = model.ProxyTypes [0];
+ Assert.Equal ("MyApp.IFoo", proxy.TargetType.ManagedTypeName);
+ Assert.NotNull (proxy.InvokerType);
+ Assert.Equal ("MyApp.FooInvoker", proxy.InvokerType!.ManagedTypeName);
+
+ // Interface proxy has activation because it will create the invoker
+ Assert.True (proxy.HasActivation);
+ }
+
+ }
+
+ public class FixtureGenericHolder
+ {
+
+ [Fact]
+ public void Fixture_GenericHolder_Entry ()
+ {
+ var peer = FindFixtureByJavaName ("my/app/GenericHolder");
+ Assert.True (peer.IsGenericDefinition);
+
+ var model = BuildModel (new [] { peer }, "TypeMap");
+ var entry = FindEntry (model, "my/app/GenericHolder");
+ Assert.NotNull (entry);
+ }
+
+ }
+
+ public class FixtureAcwTypeHasProxy
+ {
+
+ [Theory]
+ [InlineData ("my/app/AbstractBase", "MyApp_AbstractBase_Proxy")]
+ [InlineData ("my/app/ClickableView", "MyApp_ClickableView_Proxy")]
+ [InlineData ("my/app/MultiInterfaceView", "MyApp_MultiInterfaceView_Proxy")]
+ [InlineData ("my/app/ExportExample", "MyApp_ExportExample_Proxy")]
+ public void Fixture_AcwType_HasProxy (string javaName, string expectedProxyName)
+ {
+ var peer = FindFixtureByJavaName (javaName);
+ Assert.False (peer.DoNotGenerateAcw);
+
+ var model = BuildModel (new [] { peer }, "TypeMap");
+
+ if (peer.ActivationCtor != null && peer.MarshalMethods.Count > 0) {
+ var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == expectedProxyName);
+ Assert.NotNull (proxy);
+ }
+ }
+
+ }
+
+ public class FixtureImplementorsAndDispatchers
+ {
+
+ [Theory]
+ [InlineData ("android/view/View_IOnClickListenerImplementor", "Implementor")]
+ [InlineData ("android/view/View_ClickEventDispatcher", "EventDispatcher")]
+ public void Fixture_HelperType_IsTrimmable_NotUnconditional (string javaName, string kind)
+ {
+ var peer = FindFixtureByJavaName (javaName);
+ Assert.False (peer.DoNotGenerateAcw);
+ Assert.False (peer.IsInterface);
+
+ var model = BuildModel (new [] { peer }, "TypeMap");
+
+ var entry = model.Entries.FirstOrDefault ();
+ Assert.NotNull (entry);
+ Assert.False (entry!.IsUnconditional, $"{kind} should NOT be unconditional");
+ Assert.NotNull (entry.TargetTypeReference);
+ }
+
+ }
+
+ public class NameBasedDetection
+ {
+
+ [Fact]
+ public void Build_UserTypeNamedImplementor_IsTreatedAsTrimmable ()
+ {
+ // Limitation: name-based heuristic means a user type ending in "Implementor"
+ // will be treated as trimmable even if it's genuinely a user ACW type.
+ // This test documents the known behavior.
+ var peer = MakeAcwPeer ("my/app/MyImplementor", "MyApp.MyImplementor", "App");
+ var model = BuildModel (new [] { peer });
+
+ var entry = model.Entries.FirstOrDefault ();
+ Assert.NotNull (entry);
+ // The heuristic treats this as an Implementor → trimmable (not unconditional)
+ Assert.False (entry!.IsUnconditional,
+ "Name-based heuristic: types ending in 'Implementor' are treated as trimmable");
+ }
+
+ [Fact]
+ public void Build_TypeIsInvoker_OnlyWhenReferencedByAnotherPeer ()
+ {
+ // A type is only treated as an invoker when another peer's InvokerTypeName references it.
+ // A type named "MyInvoker" with DoNotGenerateAcw is NOT automatically an invoker.
+ var invokerPeer = MakePeerWithActivation ("my/app/MyInvoker", "MyApp.MyInvoker", "App");
+ invokerPeer.DoNotGenerateAcw = true;
+
+ // Without a referencing peer, it gets a normal entry
+ var model1 = BuildModel (new [] { invokerPeer });
+ Assert.Single (model1.Entries);
+
+ // When an interface references it as invoker, it is excluded
+ var ifacePeer = MakeInterfacePeer ("my/app/MyInvoker", "MyApp.IMyInterface", "App", "MyApp.MyInvoker");
+ var model2 = BuildModel (new [] { ifacePeer, invokerPeer });
+ // Only the interface gets entries/proxies, the invoker is excluded
+ Assert.Single (model2.Entries);
+ Assert.Equal ("MyApp.IMyInterface", model2.ProxyTypes [0].TargetType.ManagedTypeName);
+ }
+
+ }
+
+ public class PipelineTests
+ {
+
+ [Fact]
+ public void FullPipeline_AllFixtures_ProducesLoadableAssembly ()
+ {
+ var peers = ScanFixtures ();
+ var model = BuildModel (peers, "FullPipeline");
+
+ EmitAndVerify (model, "FullPipeline", (pe, reader) => {
+ Assert.True (pe.HasMetadata);
+
+ var asmDef = reader.GetAssemblyDefinition ();
+ Assert.Equal ("FullPipeline", reader.GetString (asmDef.Name));
+
+ var proxyTypes = reader.TypeDefinitions
+ .Select (h => reader.GetTypeDefinition (h))
+ .Where (t => reader.GetString (t.Namespace) == "_TypeMap.Proxies")
+ .ToList ();
+ Assert.Equal (model.ProxyTypes.Count, proxyTypes.Count);
+
+ var proxyNames = proxyTypes.Select (t => reader.GetString (t.Name)).OrderBy (n => n).ToList ();
+ var modelNames = model.ProxyTypes.Select (p => p.TypeName).OrderBy (n => n).ToList ();
+ Assert.Equal (modelNames, proxyNames);
+ });
+ }
+
+ [Fact]
+ public void FullPipeline_AllFixtures_TypeMapAttributeCountMatchesEntries ()
+ {
+ var peers = ScanFixtures ();
+ var model = BuildModel (peers, "AttrCount");
+
+ EmitAndVerify (model, "AttrCount", (pe, reader) => {
+ var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition);
+ int totalAttrs = asmAttrs.Count ();
+
+ int expected = model.Entries.Count + model.IgnoresAccessChecksTo.Count;
+ Assert.Equal (expected, totalAttrs);
+ });
+ }
+
+ [Fact]
+ public void FullPipeline_CustomView_HasConstructorAndMethodWrappers ()
+ {
+ var peer = FindFixtureByJavaName ("my/app/CustomView");
+ var model = BuildModel (new [] { peer }, "CtorTest");
+
+ EmitAndVerify (model, "CtorTest", (pe, reader) => {
+ var proxy = reader.TypeDefinitions
+ .Select (h => reader.GetTypeDefinition (h))
+ .First (t => reader.GetString (t.Name) == "MyApp_CustomView_Proxy");
+
+ var methodNames = proxy.GetMethods ()
+ .Select (h => reader.GetString (reader.GetMethodDefinition (h).Name))
+ .ToList ();
+
+ Assert.Contains (".ctor", methodNames);
+ Assert.Contains ("CreateInstance", methodNames);
+ Assert.Contains ("get_TargetType", methodNames);
+ });
+ }
+
+ [Fact]
+ public void FullPipeline_GenericHolder_ProducesValidAssembly ()
+ {
+ var peer = FindFixtureByJavaName ("my/app/GenericHolder");
+ var model = BuildModel (new [] { peer }, "GenericTest");
+
+ EmitAndVerify (model, "GenericTest", (pe, reader) => {
+ Assert.True (pe.HasMetadata);
+ var entry = FindEntry (model, "my/app/GenericHolder");
+ Assert.NotNull (entry);
+
+ var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition);
+ Assert.NotEmpty (asmAttrs);
+ });
+ }
+
+ }
+
+ public class PeBlobValidation
+ {
+
+ [Fact]
+ public void FullPipeline_Mixed2ArgAnd3Arg_BothSurviveRoundTrip ()
+ {
+ // java/lang/Object → essential → 2-arg unconditional
+ var objectPeer = FindFixtureByJavaName ("java/lang/Object");
+ // android/app/Activity → MCW → 3-arg trimmable
+ var activityPeer = FindFixtureByJavaName ("android/app/Activity");
+
+ var model = BuildModel (new [] { objectPeer, activityPeer }, "MixedBlob");
+ Assert.Equal (2, model.Entries.Count);
+
+ EmitAndVerify (model, "MixedBlob", (pe, reader) => {
+ var attrs = ReadAllTypeMapAttributeBlobs (reader);
+ Assert.Equal (2, attrs.Count);
+
+ var unconditional = attrs.FirstOrDefault (a => a.jniName == "java/lang/Object");
+ Assert.NotNull (unconditional.jniName);
+ Assert.Null (unconditional.targetRef);
+
+ var trimmable = attrs.FirstOrDefault (a => a.jniName == "android/app/Activity");
+ Assert.NotNull (trimmable.jniName);
+ Assert.NotNull (trimmable.targetRef);
+ Assert.Contains ("Android.App.Activity", trimmable.targetRef!);
+ });
+ }
+
+ [Theory]
+ [InlineData ("java/lang/Object", "Blob2Arg", "Java_Lang_Object_Proxy")]
+ [InlineData ("my/app/MainActivity", "BlobAcw", "MyApp_MainActivity_Proxy")]
+ public void FullPipeline_UnconditionalType_Emits2ArgAttribute (string javaName, string assemblyName, string expectedProxyName)
+ {
+ var peer = FindFixtureByJavaName (javaName);
+ var model = BuildModel (new [] { peer }, assemblyName);
+ Assert.Single (model.Entries);
+ Assert.True (model.Entries [0].IsUnconditional);
+
+ EmitAndVerify (model, assemblyName, (pe, reader) => {
+ var (jniName2, proxyRef, targetRef) = ReadFirstTypeMapAttributeBlob (reader);
+
+ Assert.Equal (javaName, jniName2);
+ Assert.NotNull (proxyRef);
+ Assert.Contains (expectedProxyName, proxyRef!);
+ Assert.Null (targetRef);
+ });
+ }
+
+ [Fact]
+ public void FullPipeline_McwBinding_Emits3ArgAttribute ()
+ {
+ // android/app/Activity is MCW → trimmable 3-arg attribute
+ var peer = FindFixtureByJavaName ("android/app/Activity");
+ var model = BuildModel (new [] { peer }, "Blob3Arg");
+ Assert.Single (model.Entries);
+ Assert.False (model.Entries [0].IsUnconditional);
+
+ EmitAndVerify (model, "Blob3Arg", (pe, reader) => {
+ var (jniName, proxyRef, targetRef) = ReadFirstTypeMapAttributeBlob (reader);
+
+ Assert.Equal ("android/app/Activity", jniName);
+ Assert.NotNull (proxyRef);
+ Assert.Contains ("Android_App_Activity_Proxy", proxyRef!);
+ Assert.NotNull (targetRef);
+ Assert.Contains ("Android.App.Activity", targetRef!);
+ });
+ }
+
+ }
+
+ public class DeterminismTests
+ {
+
+ [Fact]
+ public void Build_SameInput_ProducesDeterministicOutput ()
+ {
+ var peers = ScanFixtures ();
+
+ var model1 = BuildModel (peers, "DetTest");
+ var model2 = BuildModel (peers, "DetTest");
+
+ Assert.Equal (model1.Entries.Count, model2.Entries.Count);
+ for (int i = 0; i < model1.Entries.Count; i++) {
+ Assert.Equal (model1.Entries [i].JniName, model2.Entries [i].JniName);
+ Assert.Equal (model1.Entries [i].ProxyTypeReference, model2.Entries [i].ProxyTypeReference);
+ Assert.Equal (model1.Entries [i].TargetTypeReference, model2.Entries [i].TargetTypeReference);
+ }
+ }
+
+ }
+
+ static void EmitAndVerify (TypeMapAssemblyData model, string assemblyName, Action verify)
+ {
+ var outputDir = CreateTempDir ();
+ try {
+ var outputPath = Path.Combine (outputDir, $"{assemblyName}.dll");
+ var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0));
+ emitter.Emit (model, outputPath);
+ using var pe = new PEReader (File.OpenRead (outputPath));
+ verify (pe, pe.GetMetadataReader ());
+ } finally {
+ DeleteTempDir (outputDir);
+ }
+ }
+
+ ///
+ /// Reads the first TypeMap assembly-level attribute blob and returns (jniName, proxyRef, targetRef).
+ /// targetRef is null for 2-arg attributes.
+ ///
+ static (string? jniName, string? proxyRef, string? targetRef) ReadFirstTypeMapAttributeBlob (MetadataReader reader)
+ {
+ var all = ReadAllTypeMapAttributeBlobs (reader);
+ if (all.Count == 0) {
+ throw new InvalidOperationException ("No TypeMap attribute found on assembly");
+ }
+ return all [0];
+ }
+
+ ///
+ /// Reads TypeMap attribute blobs from a PE assembly's metadata.
+ ///
+ /// NOTE: This is a PE-level integration test helper, not a primary unit test mechanism.
+ /// The model-level tests (which verify TypeMapAssemblyData directly) are the main unit tests.
+ /// These PE round-trip tests exist to catch encoding bugs in the emitter and to verify that
+ /// the full scan→model→emit pipeline produces a valid, loadable assembly.
+ ///
+ /// The distinction between TypeMap and IgnoresAccessChecksTo attributes relies on
+ /// attr.Constructor.Kind: TypeMap attributes reference their ctor via MemberReference
+ /// (because the attribute type is a TypeSpec — generic), while IgnoresAccessChecksTo
+ /// uses MethodDefinition (the attribute type is defined in the same assembly as a TypeDef).
+ /// If this logic breaks, the test will either fail to find TypeMap attributes or
+ /// misidentify IgnoresAccessChecksTo as TypeMap — both cause obvious assertion failures.
+ ///
+ static List<(string? jniName, string? proxyRef, string? targetRef)> ReadAllTypeMapAttributeBlobs (MetadataReader reader)
+ {
+ var result = new List<(string?, string?, string?)> ();
+ var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition);
+ foreach (var attrHandle in asmAttrs) {
+ var attr = reader.GetCustomAttribute (attrHandle);
+ // Skip IgnoresAccessChecksTo attributes (their ctor is a MethodDefinition, not MemberRef)
+ if (attr.Constructor.Kind == HandleKind.MethodDefinition)
+ continue;
+
+ var blobReader = reader.GetBlobReader (attr.Value);
+ ushort prolog = blobReader.ReadUInt16 ();
+ if (prolog != 1)
+ continue;
+
+ string? jniName = blobReader.ReadSerializedString ();
+ string? proxyRef = blobReader.ReadSerializedString ();
+
+ // Try to read third arg (target type) — if remaining bytes are just NumNamed (2 bytes), it's 2-arg
+ string? targetRef = null;
+ if (blobReader.RemainingBytes > 2) {
+ targetRef = blobReader.ReadSerializedString ();
+ }
+
+ result.Add ((jniName, proxyRef, targetRef));
+ }
+ return result;
+ }
+}
\ No newline at end of file