diff --git a/build-tools/automation/yaml-templates/build-linux-steps.yaml b/build-tools/automation/yaml-templates/build-linux-steps.yaml
index 02d34dafda2..bdc9c6a65c0 100644
--- a/build-tools/automation/yaml-templates/build-linux-steps.yaml
+++ b/build-tools/automation/yaml-templates/build-linux-steps.yaml
@@ -5,6 +5,7 @@ parameters:
buildResultArtifactName: Build Results - Linux
xaSourcePath: $(System.DefaultWorkingDirectory)/android
nugetArtifactName: $(LinuxNuGetArtifactName)
+ makeMSBuildArgs: ''
use1ESTemplate: true
steps:
@@ -26,7 +27,7 @@ steps:
- template: /build-tools/automation/yaml-templates/log-disk-space.yaml
-- script: make jenkins PREPARE_CI=1 PREPARE_AUTOPROVISION=1 CONFIGURATION=$(XA.Build.Configuration)
+- script: make jenkins PREPARE_CI=1 PREPARE_AUTOPROVISION=1 CONFIGURATION=$(XA.Build.Configuration) MSBUILD_ARGS='${{ parameters.makeMSBuildArgs }}'
workingDirectory: ${{ parameters.xaSourcePath }}
displayName: make jenkins
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs
new file mode 100644
index 00000000000..9c0867c0875
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs
@@ -0,0 +1,52 @@
+using System;
+using System.Security.Cryptography;
+using System.Text;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+static class MetadataHelper
+{
+ ///
+ /// Produces a deterministic MVID by hashing the module name together with content-dependent data.
+ /// Assemblies with the same name but different content will have different MVIDs.
+ ///
+ public static Guid DeterministicMvid (string moduleName, ReadOnlySpan contentBytes = default)
+ {
+ using var sha = SHA256.Create ();
+ byte [] nameBytes = Encoding.UTF8.GetBytes (moduleName);
+ byte [] input = new byte [nameBytes.Length + contentBytes.Length];
+ nameBytes.CopyTo (input, 0);
+ contentBytes.CopyTo (input.AsSpan (nameBytes.Length));
+ byte [] hash = sha.ComputeHash (input);
+ byte [] guidBytes = new byte [16];
+ Array.Copy (hash, guidBytes, 16);
+ return new Guid (guidBytes);
+ }
+
+ ///
+ /// Computes a content fingerprint for the given .
+ ///
+ public static byte [] ComputeContentFingerprint (TypeMapAssemblyData data)
+ {
+ using var sha = SHA256.Create ();
+ using var stream = new System.IO.MemoryStream ();
+ using var writer = new System.IO.BinaryWriter (stream, Encoding.UTF8);
+ foreach (var entry in data.Entries) {
+ writer.Write (entry.JniName);
+ writer.Write (entry.ProxyTypeReference);
+ writer.Write (entry.TargetTypeReference ?? "");
+ }
+ foreach (var proxy in data.ProxyTypes) {
+ writer.Write (proxy.TypeName);
+ writer.Write (proxy.TargetType.ManagedTypeName);
+ writer.Write (proxy.TargetType.AssemblyName);
+ writer.Write ((byte)(proxy.ActivationCtor?.Style ?? 0));
+ }
+ foreach (var assoc in data.Associations) {
+ writer.Write (assoc.SourceTypeReference);
+ writer.Write (assoc.AliasProxyTypeReference);
+ }
+ writer.Flush ();
+ return sha.ComputeHash (stream.ToArray ());
+ }
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs
new file mode 100644
index 00000000000..701f9cd4f2c
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs
@@ -0,0 +1,170 @@
+using System;
+using System.Collections.Generic;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+///
+/// Data model for a single TypeMap output assembly.
+/// Describes what to emit — the emitter writes this directly into a PE assembly.
+/// Built by , consumed by .
+///
+sealed class TypeMapAssemblyData
+{
+ ///
+ /// Assembly name (e.g., "_MyApp.TypeMap").
+ ///
+ public required string AssemblyName { get; init; }
+
+ ///
+ /// Module file name (e.g., "_MyApp.TypeMap.dll").
+ ///
+ public required string ModuleName { get; init; }
+
+ ///
+ /// TypeMap entries — one per unique JNI name.
+ ///
+ public List Entries { get; } = new ();
+
+ ///
+ /// Proxy types to emit in the assembly.
+ ///
+ public List ProxyTypes { get; } = new ();
+
+ ///
+ /// TypeMapAssociation entries for alias groups (multiple managed types → same JNI name).
+ ///
+ public List Associations { get; } = new ();
+
+ ///
+ /// Assembly names that need [IgnoresAccessChecksTo] for cross-assembly n_* calls.
+ ///
+ public List IgnoresAccessChecksTo { get; } = new ();
+}
+
+///
+/// One [assembly: TypeMap("jni/name", typeof(Proxy))] or
+/// [assembly: TypeMap("jni/name", typeof(Proxy), typeof(Target))] entry.
+///
+/// 2-arg (unconditional): proxy is always preserved — used for ACW types and essential runtime types.
+/// 3-arg (trimmable): proxy is preserved only if Target type is referenced by the app.
+///
+sealed record TypeMapAttributeData
+{
+ ///
+ /// JNI type name, e.g., "android/app/Activity".
+ ///
+ public required string JniName { get; init; }
+
+ ///
+ /// Assembly-qualified proxy type reference string.
+ /// Either points to a generated proxy or to the original managed type.
+ ///
+ public required string ProxyTypeReference { get; init; }
+
+ ///
+ /// Assembly-qualified target type reference for the trimmable (3-arg) variant.
+ /// Null for unconditional (2-arg) entries.
+ /// The trimmer preserves the proxy only if this target type is used by the app.
+ ///
+ public string? TargetTypeReference { get; init; }
+
+ ///
+ /// True for 2-arg unconditional entries (ACW types, essential runtime types).
+ ///
+ public bool IsUnconditional => TargetTypeReference == null;
+}
+
+///
+/// A proxy type to generate in the TypeMap assembly (subclass of JavaPeerProxy).
+///
+sealed class JavaPeerProxyData
+{
+ ///
+ /// Simple type name, e.g., "Java_Lang_Object_Proxy".
+ ///
+ public required string TypeName { get; init; }
+
+ ///
+ /// Namespace for all proxy types.
+ ///
+ public string Namespace { get; init; } = "_TypeMap.Proxies";
+
+ ///
+ /// Reference to the managed type this proxy wraps (for ldtoken in TargetType property).
+ ///
+ public required TypeRefData TargetType { get; init; }
+
+ ///
+ /// Reference to the invoker type (for interfaces/abstract types). Null if not applicable.
+ ///
+ public TypeRefData? InvokerType { get; set; }
+
+ ///
+ /// Whether this proxy has a CreateInstance that can actually create instances.
+ ///
+ public bool HasActivation => ActivationCtor != null || InvokerType != null;
+
+ ///
+ /// Activation constructor details. Determines how CreateInstance instantiates the managed peer.
+ ///
+ public ActivationCtorData? ActivationCtor { get; set; }
+
+ ///
+ /// True if this is an open generic type definition. CreateInstance throws NotSupportedException.
+ ///
+ public bool IsGenericDefinition { get; init; }
+}
+
+///
+/// A cross-assembly type reference (assembly name + full managed type name).
+///
+sealed record TypeRefData
+{
+ ///
+ /// Full managed type name, e.g., "Android.App.Activity" or "MyApp.Outer+Inner".
+ ///
+ public required string ManagedTypeName { get; init; }
+
+ ///
+ /// Assembly containing the type, e.g., "Mono.Android".
+ ///
+ public required string AssemblyName { get; init; }
+}
+
+///
+/// Describes how the proxy's CreateInstance should construct the managed peer.
+///
+sealed record ActivationCtorData
+{
+ ///
+ /// Type that declares the activation constructor (may be a base type).
+ ///
+ public required TypeRefData DeclaringType { get; init; }
+
+ ///
+ /// True when the leaf type itself declares the activation ctor.
+ ///
+ public required bool IsOnLeafType { get; init; }
+
+ ///
+ /// The style of activation ctor (XamarinAndroid or JavaInterop).
+ ///
+ public required ActivationCtorStyle Style { get; init; }
+}
+
+///
+/// One [assembly: TypeMapAssociation(typeof(Source), typeof(AliasProxy))] entry.
+/// Links a managed type to the proxy that holds its alias TypeMap entry.
+///
+sealed record TypeMapAssociationData
+{
+ ///
+ /// Assembly-qualified source type reference (the managed alias type).
+ ///
+ public required string SourceTypeReference { get; init; }
+
+ ///
+ /// Assembly-qualified proxy type reference (the alias holder proxy).
+ ///
+ public required string AliasProxyTypeReference { get; init; }
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs
new file mode 100644
index 00000000000..949b034571a
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs
@@ -0,0 +1,241 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+///
+/// Builds a from scanned records.
+/// All decision logic (deduplication, alias detection, ACW filtering, 2-arg vs 3-arg attribute
+/// selection, callback resolution, proxy naming) lives here.
+/// The output model is a plain data structure that the emitter writes directly into a PE assembly.
+///
+static class ModelBuilder
+{
+ static readonly HashSet EssentialRuntimeTypes = new (StringComparer.Ordinal) {
+ "java/lang/Object",
+ "java/lang/Class",
+ "java/lang/String",
+ "java/lang/Throwable",
+ "java/lang/Exception",
+ "java/lang/RuntimeException",
+ "java/lang/Error",
+ "java/lang/Thread",
+ };
+
+ ///
+ /// Builds a TypeMap assembly model for the given peers.
+ ///
+ /// Scanned Java peer types (typically from a single input assembly).
+ /// Output .dll path — used to derive assembly/module names if not specified.
+ /// Explicit assembly name. If null, derived from .
+ public static TypeMapAssemblyData Build (IReadOnlyList peers, string outputPath, string? assemblyName = null)
+ {
+ if (peers is null) {
+ throw new ArgumentNullException (nameof (peers));
+ }
+ if (outputPath is null) {
+ throw new ArgumentNullException (nameof (outputPath));
+ }
+
+ assemblyName ??= Path.GetFileNameWithoutExtension (outputPath);
+ string moduleName = Path.GetFileName (outputPath);
+
+ var model = new TypeMapAssemblyData {
+ AssemblyName = assemblyName,
+ ModuleName = moduleName,
+ };
+
+ // Invoker types are NOT emitted as separate proxies or TypeMap entries —
+ // they only appear as a TypeRef in the interface proxy's get_InvokerType property.
+ var invokerTypeNames = new HashSet (
+ peers.Select (p => p.InvokerTypeName).OfType (),
+ StringComparer.Ordinal);
+
+ // Group non-invoker peers by JNI name to detect aliases (multiple .NET types → same Java class).
+ // Use an ordered dictionary to ensure deterministic output across runs.
+ var groups = new SortedDictionary> (StringComparer.Ordinal);
+ foreach (var peer in peers) {
+ if (invokerTypeNames.Contains (peer.ManagedTypeName)) {
+ continue;
+ }
+ if (!groups.TryGetValue (peer.JavaName, out var list)) {
+ list = new List ();
+ groups [peer.JavaName] = list;
+ }
+ list.Add (peer);
+ }
+
+ var usedProxyNames = new HashSet (StringComparer.Ordinal);
+
+ foreach (var kvp in groups) {
+ string jniName = kvp.Key;
+ var peersForName = kvp.Value;
+
+ // Sort aliases by managed type name for deterministic proxy naming
+ if (peersForName.Count > 1) {
+ peersForName.Sort ((a, b) => StringComparer.Ordinal.Compare (a.ManagedTypeName, b.ManagedTypeName));
+ }
+
+ EmitPeers (model, jniName, peersForName, assemblyName, usedProxyNames);
+ }
+
+ // Compute IgnoresAccessChecksTo from cross-assembly references
+ var referencedAssemblies = new SortedSet (StringComparer.Ordinal);
+ foreach (var proxy in model.ProxyTypes) {
+ AddIfCrossAssembly (referencedAssemblies, proxy.TargetType?.AssemblyName, assemblyName);
+ if (proxy.ActivationCtor != null && !proxy.ActivationCtor.IsOnLeafType) {
+ AddIfCrossAssembly (referencedAssemblies, proxy.ActivationCtor.DeclaringType.AssemblyName, assemblyName);
+ }
+ }
+
+ // Always include Mono.Android — the emitter calls internal JNIEnv.DeleteRef
+ // for JI-style activation cleanup (matching legacy TypeManager.CreateProxy behavior).
+ referencedAssemblies.Add ("Mono.Android");
+
+ model.IgnoresAccessChecksTo.AddRange (referencedAssemblies);
+
+ return model;
+ }
+
+ static void EmitPeers (TypeMapAssemblyData model, string jniName,
+ List peersForName, string assemblyName, HashSet usedProxyNames)
+ {
+ // First peer is the "primary" — it gets the base JNI name entry.
+ // Remaining peers get indexed alias entries: "jni/name[1]", "jni/name[2]", ...
+ JavaPeerProxyData? primaryProxy = null;
+ for (int i = 0; i < peersForName.Count; i++) {
+ var peer = peersForName [i];
+ string entryJniName = i == 0 ? jniName : $"{jniName}[{i}]";
+
+ bool hasProxy = peer.ActivationCtor != null || peer.InvokerTypeName != null;
+
+ JavaPeerProxyData? proxy = null;
+ if (hasProxy) {
+ proxy = BuildProxyType (peer, usedProxyNames);
+ model.ProxyTypes.Add (proxy);
+ }
+
+ if (i == 0) {
+ primaryProxy = proxy;
+ }
+
+ model.Entries.Add (BuildEntry (peer, proxy, assemblyName, entryJniName));
+
+ // Emit TypeMapAssociation linking alias types to the primary proxy
+ if (i > 0 && primaryProxy != null) {
+ model.Associations.Add (new TypeMapAssociationData {
+ SourceTypeReference = AssemblyQualify (peer.ManagedTypeName, peer.AssemblyName),
+ AliasProxyTypeReference = AssemblyQualify ($"{primaryProxy.Namespace}.{primaryProxy.TypeName}", assemblyName),
+ });
+ }
+ }
+ }
+
+ ///
+ /// Determines whether a type should use the unconditional (2-arg) TypeMap attribute.
+ /// Unconditional types are always preserved by the trimmer.
+ ///
+ static bool IsUnconditionalEntry (JavaPeerInfo peer)
+ {
+ // Essential runtime types needed by the Java interop runtime
+ if (EssentialRuntimeTypes.Contains (peer.JavaName)) {
+ return true;
+ }
+
+ // User-defined ACW types (not MCW bindings, not interfaces) are unconditional
+ // because Android can instantiate them from Java at any time.
+ if (!peer.DoNotGenerateAcw && !peer.IsInterface) {
+ return true;
+ }
+
+ // Types marked unconditional by the scanner (component attributes: Activity, Service, etc.)
+ if (peer.IsUnconditional) {
+ return true;
+ }
+
+ return false;
+ }
+
+ static void AddIfCrossAssembly (SortedSet set, string? asmName, string outputAssemblyName)
+ {
+ if (asmName != null && !string.Equals (asmName, outputAssemblyName, StringComparison.Ordinal)) {
+ set.Add (asmName);
+ }
+ }
+
+ static JavaPeerProxyData BuildProxyType (JavaPeerInfo peer, HashSet usedProxyNames)
+ {
+ // Use managed type name for proxy naming to guarantee uniqueness across aliases
+ // (two types with the same JNI name will have different managed names).
+ var proxyTypeName = peer.ManagedTypeName.Replace ('.', '_').Replace ('+', '_') + "_Proxy";
+
+ // Guard against name collisions (e.g., "My.Type" and "My_Type" both map to "My_Type_Proxy")
+ if (!usedProxyNames.Add (proxyTypeName)) {
+ int suffix = 2;
+ string candidate;
+ do {
+ candidate = $"{proxyTypeName}_{suffix}";
+ suffix++;
+ } while (!usedProxyNames.Add (candidate));
+ proxyTypeName = candidate;
+ }
+
+ var proxy = new JavaPeerProxyData {
+ TypeName = proxyTypeName,
+ TargetType = new TypeRefData {
+ ManagedTypeName = peer.ManagedTypeName,
+ AssemblyName = peer.AssemblyName,
+ },
+ IsGenericDefinition = peer.IsGenericDefinition,
+ };
+
+ if (peer.InvokerTypeName != null) {
+ proxy.InvokerType = new TypeRefData {
+ ManagedTypeName = peer.InvokerTypeName,
+ AssemblyName = peer.AssemblyName,
+ };
+ }
+
+ if (peer.ActivationCtor != null) {
+ bool isOnLeaf = string.Equals (peer.ActivationCtor.DeclaringTypeName, peer.ManagedTypeName, StringComparison.Ordinal);
+ proxy.ActivationCtor = new ActivationCtorData {
+ DeclaringType = new TypeRefData {
+ ManagedTypeName = peer.ActivationCtor.DeclaringTypeName,
+ AssemblyName = peer.ActivationCtor.DeclaringAssemblyName,
+ },
+ IsOnLeafType = isOnLeaf,
+ Style = peer.ActivationCtor.Style,
+ };
+ }
+
+ return proxy;
+ }
+
+ static TypeMapAttributeData BuildEntry (JavaPeerInfo peer, JavaPeerProxyData? proxy,
+ string outputAssemblyName, string jniName)
+ {
+ string proxyRef;
+ if (proxy != null) {
+ proxyRef = AssemblyQualify ($"{proxy.Namespace}.{proxy.TypeName}", outputAssemblyName);
+ } else {
+ proxyRef = AssemblyQualify (peer.ManagedTypeName, peer.AssemblyName);
+ }
+
+ bool isUnconditional = IsUnconditionalEntry (peer);
+ string? targetRef = null;
+ if (!isUnconditional) {
+ targetRef = AssemblyQualify (peer.ManagedTypeName, peer.AssemblyName);
+ }
+
+ return new TypeMapAttributeData {
+ JniName = jniName,
+ ProxyTypeReference = proxyRef,
+ TargetTypeReference = targetRef,
+ };
+ }
+
+ static string AssemblyQualify (string typeName, string assemblyName)
+ => $"{typeName}, {assemblyName}";
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs
new file mode 100644
index 00000000000..b862cc2b29f
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs
@@ -0,0 +1,290 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection;
+using System.Reflection.Metadata;
+using System.Reflection.Metadata.Ecma335;
+using System.Reflection.PortableExecutable;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+///
+/// Shared plumbing for building PE assemblies with System.Reflection.Metadata.
+/// Owns the , common assembly/type references, scratch blob builders,
+/// and the final PE serialisation. Both and
+/// delegate to this instead of duplicating boilerplate.
+///
+sealed class PEAssemblyBuilder
+{
+ // Mono.Android strong name public key token (84e04ff9cfb79065)
+ static readonly byte [] MonoAndroidPublicKeyToken = { 0x84, 0xe0, 0x4f, 0xf9, 0xcf, 0xb7, 0x90, 0x65 };
+
+ readonly Dictionary _asmRefCache = new (StringComparer.OrdinalIgnoreCase);
+ readonly Dictionary<(string Assembly, string Type), EntityHandle> _typeRefCache = new ();
+
+ // Reusable scratch BlobBuilders — avoids allocating a new one per method body / attribute / member ref.
+ // Each is Clear()'d before use. Safe because all emission is single-threaded and non-reentrant.
+ readonly BlobBuilder _sigBlob = new BlobBuilder (64);
+ readonly BlobBuilder _codeBlob = new BlobBuilder (256);
+ readonly BlobBuilder _attrBlob = new BlobBuilder (64);
+
+ readonly Version _systemRuntimeVersion;
+
+ public MetadataBuilder Metadata { get; } = new MetadataBuilder ();
+ public BlobBuilder ILBuilder { get; } = new BlobBuilder ();
+
+ public AssemblyReferenceHandle SystemRuntimeRef { get; private set; }
+ public AssemblyReferenceHandle SystemRuntimeInteropServicesRef { get; private set; }
+ public AssemblyReferenceHandle MonoAndroidRef { get; private set; }
+
+ public PEAssemblyBuilder (Version systemRuntimeVersion)
+ {
+ _systemRuntimeVersion = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion));
+ }
+
+ ///
+ /// Emits the assembly definition, module definition, common assembly references, and <Module> type.
+ /// Call this first.
+ ///
+ public void EmitPreamble (string assemblyName, string moduleName, ReadOnlySpan contentFingerprint = default)
+ {
+ _asmRefCache.Clear ();
+ _typeRefCache.Clear ();
+
+ Metadata.AddAssembly (
+ Metadata.GetOrAddString (assemblyName),
+ new Version (1, 0, 0, 0),
+ culture: default,
+ publicKey: default,
+ flags: 0,
+ hashAlgorithm: AssemblyHashAlgorithm.None);
+
+ Metadata.AddModule (
+ generation: 0,
+ Metadata.GetOrAddString (moduleName),
+ Metadata.GetOrAddGuid (MetadataHelper.DeterministicMvid (moduleName, contentFingerprint)),
+ encId: default,
+ encBaseId: default);
+
+ // Common assembly references
+ SystemRuntimeRef = AddAssemblyRef ("System.Runtime", _systemRuntimeVersion);
+ SystemRuntimeInteropServicesRef = AddAssemblyRef ("System.Runtime.InteropServices", _systemRuntimeVersion);
+ MonoAndroidRef = AddAssemblyRef ("Mono.Android", new Version (0, 0, 0, 0), MonoAndroidPublicKeyToken);
+
+ // type
+ Metadata.AddTypeDefinition (
+ default, default,
+ Metadata.GetOrAddString (""),
+ default,
+ MetadataTokens.FieldDefinitionHandle (1),
+ MetadataTokens.MethodDefinitionHandle (1));
+ }
+
+ ///
+ /// Serialises the metadata + IL into a PE DLL at .
+ ///
+ public void WritePE (string outputPath)
+ {
+ var dir = Path.GetDirectoryName (outputPath);
+ if (!string.IsNullOrEmpty (dir)) {
+ Directory.CreateDirectory (dir);
+ }
+
+ using var fs = File.Create (outputPath);
+ WritePE (fs);
+ }
+
+ ///
+ /// Serialises the metadata + IL into a PE DLL and writes it to the given .
+ ///
+ public void WritePE (Stream stream)
+ {
+ var peBuilder = new ManagedPEBuilder (
+ new PEHeaderBuilder (imageCharacteristics: Characteristics.Dll),
+ new MetadataRootBuilder (Metadata),
+ ILBuilder);
+ var peBlob = new BlobBuilder ();
+ peBuilder.Serialize (peBlob);
+ peBlob.WriteContentTo (stream);
+ }
+
+ ///
+ /// Adds (or retrieves from cache) an assembly reference.
+ ///
+ public AssemblyReferenceHandle AddAssemblyRef (string name, Version version, byte []? publicKeyOrToken = null)
+ {
+ if (_asmRefCache.TryGetValue (name, out var existing)) {
+ return existing;
+ }
+ var handle = Metadata.AddAssemblyReference (
+ Metadata.GetOrAddString (name), version, default,
+ publicKeyOrToken != null ? Metadata.GetOrAddBlob (publicKeyOrToken) : default, 0, default);
+ _asmRefCache [name] = handle;
+ return handle;
+ }
+
+ ///
+ /// Finds an existing assembly reference or adds one with version 0.0.0.0.
+ ///
+ public AssemblyReferenceHandle FindOrAddAssemblyRef (string assemblyName)
+ => AddAssemblyRef (assemblyName, new Version (0, 0, 0, 0));
+
+ ///
+ /// Adds a member reference using the reusable signature blob builder.
+ ///
+ public MemberReferenceHandle AddMemberRef (EntityHandle parent, string name, Action encodeSig)
+ {
+ _sigBlob.Clear ();
+ encodeSig (new BlobEncoder (_sigBlob));
+ return Metadata.AddMemberReference (parent, Metadata.GetOrAddString (name), Metadata.GetOrAddBlob (_sigBlob));
+ }
+
+ ///
+ /// Resolves a to a TypeReference/TypeSpecification handle, with caching.
+ ///
+ public EntityHandle ResolveTypeRef (TypeRefData typeRef)
+ {
+ var cacheKey = (typeRef.AssemblyName, typeRef.ManagedTypeName);
+ if (_typeRefCache.TryGetValue (cacheKey, out var cached)) {
+ return cached;
+ }
+ var asmRef = FindOrAddAssemblyRef (typeRef.AssemblyName);
+ var result = MakeTypeRefForManagedName (asmRef, typeRef.ManagedTypeName);
+ _typeRefCache [cacheKey] = result;
+ return result;
+ }
+
+ TypeReferenceHandle MakeTypeRefForManagedName (EntityHandle scope, string managedTypeName)
+ {
+ int plusIndex = managedTypeName.IndexOf ('+');
+ if (plusIndex >= 0) {
+ var outerRef = MakeTypeRefForManagedName (scope, managedTypeName.Substring (0, plusIndex));
+ return MakeTypeRefForManagedName (outerRef, managedTypeName.Substring (plusIndex + 1));
+ }
+ int lastDot = managedTypeName.LastIndexOf ('.');
+ var ns = lastDot >= 0 ? managedTypeName.Substring (0, lastDot) : "";
+ var name = lastDot >= 0 ? managedTypeName.Substring (lastDot + 1) : managedTypeName;
+ return Metadata.AddTypeReference (scope, Metadata.GetOrAddString (ns), Metadata.GetOrAddString (name));
+ }
+
+ ///
+ /// Emits a method body and definition in one call.
+ ///
+ public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs,
+ Action encodeSig, Action emitIL)
+ => EmitBody (name, attrs, encodeSig, emitIL, encodeLocals: null);
+
+ ///
+ /// Emits a method body and definition with optional local variable declarations.
+ ///
+ ///
+ /// If non-null, writes the local variable signature blob. The callback receives a fresh
+ /// and must write the full LOCAL_SIG blob (header 0x07,
+ /// compressed count, then each variable type).
+ ///
+ public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs,
+ Action encodeSig, Action emitIL,
+ Action? encodeLocals)
+ {
+ _sigBlob.Clear ();
+ encodeSig (new BlobEncoder (_sigBlob));
+ // Capture the sig blob handle before emitIL, because emitIL callbacks
+ // may call AddMemberRef which clears and repopulates _sigBlob.
+ var sigBlobHandle = Metadata.GetOrAddBlob (_sigBlob);
+
+ StandaloneSignatureHandle localSigHandle = default;
+ if (encodeLocals != null) {
+ var localSigBlob = new BlobBuilder (32);
+ encodeLocals (localSigBlob);
+ localSigHandle = Metadata.AddStandaloneSignature (Metadata.GetOrAddBlob (localSigBlob));
+ }
+
+ _codeBlob.Clear ();
+ var encoder = new InstructionEncoder (_codeBlob);
+ emitIL (encoder);
+
+ while (ILBuilder.Count % 4 != 0) {
+ ILBuilder.WriteByte (0);
+ }
+ var bodyEncoder = new MethodBodyStreamEncoder (ILBuilder);
+ int bodyOffset = localSigHandle.IsNil
+ ? bodyEncoder.AddMethodBody (encoder)
+ : bodyEncoder.AddMethodBody (encoder, maxStack: 8, localSigHandle, MethodBodyAttributes.InitLocals);
+
+ return Metadata.AddMethodDefinition (
+ attrs, MethodImplAttributes.IL,
+ Metadata.GetOrAddString (name),
+ sigBlobHandle,
+ bodyOffset, default);
+ }
+
+ ///
+ /// Builds a TypeSpec for a closed generic type with a single type argument.
+ /// For example, MakeGenericTypeSpec(openAttrRef, javaLangObjectRef) produces
+ /// TypeMapAttribute<Java.Lang.Object>.
+ ///
+ public TypeSpecificationHandle MakeGenericTypeSpec (EntityHandle openType, EntityHandle typeArg)
+ {
+ _sigBlob.Clear ();
+ _sigBlob.WriteByte (0x15); // ELEMENT_TYPE_GENERICINST
+ _sigBlob.WriteByte (0x12); // ELEMENT_TYPE_CLASS
+ _sigBlob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (openType));
+ _sigBlob.WriteCompressedInteger (1); // generic arity = 1
+ _sigBlob.WriteByte (0x12); // ELEMENT_TYPE_CLASS
+ _sigBlob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (typeArg));
+ return Metadata.AddTypeSpecification (Metadata.GetOrAddBlob (_sigBlob));
+ }
+
+ ///
+ /// Writes a custom attribute blob. Calls to fill in the
+ /// payload between the prolog and NumNamed footer.
+ ///
+ public BlobHandle BuildAttributeBlob (Action writePayload)
+ {
+ _attrBlob.Clear ();
+ _attrBlob.WriteUInt16 (0x0001); // Prolog
+ writePayload (_attrBlob);
+ _attrBlob.WriteUInt16 (0x0000); // NumNamed
+ return Metadata.GetOrAddBlob (_attrBlob);
+ }
+
+ ///
+ /// Emits the IgnoresAccessChecksToAttribute type and applies
+ /// [assembly: IgnoresAccessChecksTo("...")] for each assembly name.
+ ///
+ public void EmitIgnoresAccessChecksToAttribute (List assemblyNames)
+ {
+ var attributeTypeRef = Metadata.AddTypeReference (SystemRuntimeRef,
+ Metadata.GetOrAddString ("System"), Metadata.GetOrAddString ("Attribute"));
+
+ int typeFieldStart = Metadata.GetRowCount (TableIndex.Field) + 1;
+ int typeMethodStart = Metadata.GetRowCount (TableIndex.MethodDef) + 1;
+
+ var baseAttrCtorRef = AddMemberRef (attributeTypeRef, ".ctor",
+ sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { }));
+
+ var ctorDef = EmitBody (".ctor",
+ MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName,
+ sig => sig.MethodSignature (isInstanceMethod: true).Parameters (1,
+ rt => rt.Void (),
+ p => p.AddParameter ().Type ().String ()),
+ encoder => {
+ encoder.LoadArgument (0);
+ encoder.Call (baseAttrCtorRef);
+ encoder.OpCode (ILOpCode.Ret);
+ });
+
+ Metadata.AddTypeDefinition (
+ TypeAttributes.NotPublic | TypeAttributes.Sealed | TypeAttributes.BeforeFieldInit,
+ Metadata.GetOrAddString ("System.Runtime.CompilerServices"),
+ Metadata.GetOrAddString ("IgnoresAccessChecksToAttribute"),
+ attributeTypeRef,
+ MetadataTokens.FieldDefinitionHandle (typeFieldStart),
+ MetadataTokens.MethodDefinitionHandle (typeMethodStart));
+
+ foreach (var asmName in assemblyNames) {
+ var blob = BuildAttributeBlob (b => b.WriteSerializedString (asmName));
+ Metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, ctorDef, blob);
+ }
+ }
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs
new file mode 100644
index 00000000000..14b49cfe986
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs
@@ -0,0 +1,104 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection.Metadata;
+using System.Reflection.Metadata.Ecma335;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+///
+/// Generates the root _Microsoft.Android.TypeMaps.dll assembly that references
+/// all per-assembly typemap assemblies via
+/// [assembly: TypeMapAssemblyTargetAttribute<Java.Lang.Object>("name")].
+///
+///
+/// The generated assembly looks like this (pseudo-C#):
+///
+/// // One attribute per per-assembly typemap assembly — tells the runtime where to find TypeMap entries:
+/// [assembly: TypeMapAssemblyTarget<Java.Lang.Object>("_Mono.Android.TypeMap")]
+/// [assembly: TypeMapAssemblyTarget<Java.Lang.Object>("_MyApp.TypeMap")]
+///
+///
+sealed class RootTypeMapAssemblyGenerator
+{
+ const string DefaultAssemblyName = "_Microsoft.Android.TypeMaps";
+
+ readonly Version _systemRuntimeVersion;
+
+ /// Version for System.Runtime assembly references.
+ public RootTypeMapAssemblyGenerator (Version systemRuntimeVersion)
+ {
+ _systemRuntimeVersion = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion));
+ }
+
+ ///
+ /// Generates the root typemap assembly and writes it to a file.
+ ///
+ /// Names of per-assembly typemap assemblies to reference.
+ /// Path to write the output .dll.
+ /// Optional assembly name (defaults to _Microsoft.Android.TypeMaps).
+ public void Generate (IReadOnlyList perAssemblyTypeMapNames, string outputPath, string? assemblyName = null)
+ {
+ if (outputPath is null) {
+ throw new ArgumentNullException (nameof (outputPath));
+ }
+
+ var dir = Path.GetDirectoryName (outputPath);
+ if (!string.IsNullOrEmpty (dir)) {
+ Directory.CreateDirectory (dir);
+ }
+
+ var moduleName = Path.GetFileName (outputPath);
+ using var fs = File.Create (outputPath);
+ Generate (perAssemblyTypeMapNames, fs, assemblyName, moduleName);
+ }
+
+ ///
+ /// Generates the root typemap assembly and writes it to the given stream.
+ ///
+ /// Names of per-assembly typemap assemblies to reference.
+ /// Stream to write the output PE to.
+ /// Optional assembly name (defaults to _Microsoft.Android.TypeMaps).
+ /// Optional module name for the PE metadata.
+ public void Generate (IReadOnlyList perAssemblyTypeMapNames, Stream stream, string? assemblyName = null, string? moduleName = null)
+ {
+ if (perAssemblyTypeMapNames is null) {
+ throw new ArgumentNullException (nameof (perAssemblyTypeMapNames));
+ }
+ if (stream is null) {
+ throw new ArgumentNullException (nameof (stream));
+ }
+
+ assemblyName ??= DefaultAssemblyName;
+ moduleName ??= assemblyName + ".dll";
+
+ var pe = new PEAssemblyBuilder (_systemRuntimeVersion);
+ pe.EmitPreamble (assemblyName, moduleName);
+
+ // Reference the open generic TypeMapAssemblyTargetAttribute`1 from System.Runtime.InteropServices
+ var openAttrRef = pe.Metadata.AddTypeReference (pe.SystemRuntimeInteropServicesRef,
+ pe.Metadata.GetOrAddString ("System.Runtime.InteropServices"),
+ pe.Metadata.GetOrAddString ("TypeMapAssemblyTargetAttribute`1"));
+
+ // Reference Java.Lang.Object from Mono.Android (the type universe)
+ var javaLangObjectRef = pe.Metadata.AddTypeReference (pe.MonoAndroidRef,
+ pe.Metadata.GetOrAddString ("Java.Lang"), pe.Metadata.GetOrAddString ("Object"));
+
+ // Build TypeSpec for TypeMapAssemblyTargetAttribute
+ var closedAttrTypeSpec = pe.MakeGenericTypeSpec (openAttrRef, javaLangObjectRef);
+
+ // MemberRef for .ctor(string) on the closed generic type
+ var ctorRef = pe.AddMemberRef (closedAttrTypeSpec, ".ctor",
+ sig => sig.MethodSignature (isInstanceMethod: true).Parameters (1,
+ rt => rt.Void (),
+ p => p.AddParameter ().Type ().String ()));
+
+ // Add [assembly: TypeMapAssemblyTargetAttribute("name")] for each per-assembly typemap
+ foreach (var name in perAssemblyTypeMapNames) {
+ var blobHandle = pe.BuildAttributeBlob (blob => blob.WriteSerializedString (name));
+ pe.Metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, ctorRef, blobHandle);
+ }
+
+ pe.WritePE (stream);
+ }
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs
new file mode 100644
index 00000000000..f878997f04a
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs
@@ -0,0 +1,577 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection;
+using System.Reflection.Metadata;
+using System.Reflection.Metadata.Ecma335;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+///
+/// Emits a per-assembly TypeMap PE assembly from a .
+/// This is a mechanical translation — all decision logic lives in .
+///
+///
+/// The generated assembly looks like this (pseudo-C#):
+///
+/// // Assembly-level TypeMap attributes — one per Java peer type:
+/// [assembly: TypeMap<Java.Lang.Object>("android/app/Activity", typeof(Activity_Proxy))] // unconditional (ACW)
+/// [assembly: TypeMap<Java.Lang.Object>("android/widget/TextView", typeof(TextView_Proxy), typeof(TextView))] // trimmable (MCW)
+/// [assembly: TypeMapAssociation(typeof(MyTextView), typeof(Android_Widget_TextView_Proxy))] // alias
+///
+/// // One proxy type per Java peer that needs activation:
+/// public sealed class Activity_Proxy : JavaPeerProxy
+/// {
+/// public Activity_Proxy() : base() { }
+///
+/// // Creates the managed peer when Java calls into .NET
+/// public override IJavaPeerable CreateInstance(IntPtr handle, JniHandleOwnership ownership)
+/// => new Activity(handle, ownership); // leaf ctor
+/// // or: (Activity)RuntimeHelpers.GetUninitializedObject(typeof(Activity));
+/// // obj.BaseCtor(handle, ownership); // inherited ctor
+/// // or: new IOnClickListenerInvoker(handle, ownership); // interface invoker
+/// // or: null; // no activation
+/// // or: throw new NotSupportedException(...); // open generic
+///
+/// public override Type TargetType => typeof(Activity);
+/// public Type InvokerType => typeof(IOnClickListenerInvoker); // interfaces only
+/// }
+///
+/// // Emitted so the proxy assembly can access internal members in the target assembly:
+/// [assembly: IgnoresAccessChecksTo("Mono.Android")]
+///
+///
+sealed class TypeMapAssemblyEmitter
+{
+ readonly Version _systemRuntimeVersion;
+
+ readonly PEAssemblyBuilder _pe;
+
+ AssemblyReferenceHandle _javaInteropRef;
+
+ TypeReferenceHandle _javaPeerProxyRef;
+ TypeReferenceHandle _iJavaPeerableRef;
+ TypeReferenceHandle _jniHandleOwnershipRef;
+ TypeReferenceHandle _jniObjectReferenceRef;
+ TypeReferenceHandle _jniObjectReferenceOptionsRef;
+ TypeReferenceHandle _jniEnvRef;
+ TypeReferenceHandle _systemTypeRef;
+ TypeReferenceHandle _runtimeTypeHandleRef;
+ TypeReferenceHandle _notSupportedExceptionRef;
+ TypeReferenceHandle _runtimeHelpersRef;
+
+ MemberReferenceHandle _baseCtorRef;
+ MemberReferenceHandle _getTypeFromHandleRef;
+ MemberReferenceHandle _getUninitializedObjectRef;
+ MemberReferenceHandle _notSupportedExceptionCtorRef;
+ MemberReferenceHandle _jniObjectReferenceCtorRef;
+ MemberReferenceHandle _jniEnvDeleteRefRef;
+ MemberReferenceHandle _typeMapAttrCtorRef2Arg;
+ MemberReferenceHandle _typeMapAttrCtorRef3Arg;
+ MemberReferenceHandle _typeMapAssociationAttrCtorRef;
+
+ ///
+ /// Creates a new emitter.
+ ///
+ ///
+ /// Version for System.Runtime assembly references.
+ /// Will be derived from $(DotNetTargetVersion) MSBuild property in the build task.
+ ///
+ public TypeMapAssemblyEmitter (Version systemRuntimeVersion)
+ {
+ _systemRuntimeVersion = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion));
+ _pe = new PEAssemblyBuilder (_systemRuntimeVersion);
+ }
+
+ ///
+ /// Emits a PE assembly from the given model and writes it to .
+ ///
+ public void Emit (TypeMapAssemblyData model, string outputPath)
+ {
+ if (model is null) {
+ throw new ArgumentNullException (nameof (model));
+ }
+ if (outputPath is null) {
+ throw new ArgumentNullException (nameof (outputPath));
+ }
+
+ EmitCore (model);
+ _pe.WritePE (outputPath);
+ }
+
+ ///
+ /// Emits a PE assembly from the given model and writes it to .
+ ///
+ public void Emit (TypeMapAssemblyData model, Stream stream)
+ {
+ if (model is null) {
+ throw new ArgumentNullException (nameof (model));
+ }
+ if (stream is null) {
+ throw new ArgumentNullException (nameof (stream));
+ }
+
+ EmitCore (model);
+ _pe.WritePE (stream);
+ }
+
+ void EmitCore (TypeMapAssemblyData model)
+ {
+ _pe.EmitPreamble (model.AssemblyName, model.ModuleName, MetadataHelper.ComputeContentFingerprint (model));
+
+ _javaInteropRef = _pe.AddAssemblyRef ("Java.Interop", new Version (0, 0, 0, 0));
+
+ EmitTypeReferences ();
+ EmitMemberReferences ();
+
+ foreach (var proxy in model.ProxyTypes) {
+ EmitProxyType (proxy);
+ }
+
+ foreach (var entry in model.Entries) {
+ EmitTypeMapAttribute (entry);
+ }
+
+ foreach (var assoc in model.Associations) {
+ EmitTypeMapAssociationAttribute (assoc);
+ }
+
+ _pe.EmitIgnoresAccessChecksToAttribute (model.IgnoresAccessChecksTo);
+ }
+
+ void EmitTypeReferences ()
+ {
+ var metadata = _pe.Metadata;
+ _javaPeerProxyRef = metadata.AddTypeReference (_pe.MonoAndroidRef,
+ metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JavaPeerProxy"));
+ _iJavaPeerableRef = metadata.AddTypeReference (_javaInteropRef,
+ metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("IJavaPeerable"));
+ _jniHandleOwnershipRef = metadata.AddTypeReference (_pe.MonoAndroidRef,
+ metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("JniHandleOwnership"));
+ _jniEnvRef = metadata.AddTypeReference (_pe.MonoAndroidRef,
+ metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("JNIEnv"));
+ _jniObjectReferenceRef = metadata.AddTypeReference (_javaInteropRef,
+ metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniObjectReference"));
+ _jniObjectReferenceOptionsRef = metadata.AddTypeReference (_javaInteropRef,
+ metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniObjectReferenceOptions"));
+ _systemTypeRef = metadata.AddTypeReference (_pe.SystemRuntimeRef,
+ metadata.GetOrAddString ("System"), metadata.GetOrAddString ("Type"));
+ _runtimeTypeHandleRef = metadata.AddTypeReference (_pe.SystemRuntimeRef,
+ metadata.GetOrAddString ("System"), metadata.GetOrAddString ("RuntimeTypeHandle"));
+ _notSupportedExceptionRef = metadata.AddTypeReference (_pe.SystemRuntimeRef,
+ metadata.GetOrAddString ("System"), metadata.GetOrAddString ("NotSupportedException"));
+ _runtimeHelpersRef = metadata.AddTypeReference (_pe.SystemRuntimeRef,
+ metadata.GetOrAddString ("System.Runtime.CompilerServices"), metadata.GetOrAddString ("RuntimeHelpers"));
+ }
+
+ void EmitMemberReferences ()
+ {
+ _baseCtorRef = _pe.AddMemberRef (_javaPeerProxyRef, ".ctor",
+ sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { }));
+
+ _getTypeFromHandleRef = _pe.AddMemberRef (_systemTypeRef, "GetTypeFromHandle",
+ sig => sig.MethodSignature ().Parameters (1,
+ rt => rt.Type ().Type (_systemTypeRef, false),
+ p => p.AddParameter ().Type ().Type (_runtimeTypeHandleRef, true)));
+
+ _getUninitializedObjectRef = _pe.AddMemberRef (_runtimeHelpersRef, "GetUninitializedObject",
+ sig => sig.MethodSignature ().Parameters (1,
+ rt => rt.Type ().Object (),
+ p => p.AddParameter ().Type ().Type (_systemTypeRef, false)));
+
+ _notSupportedExceptionCtorRef = _pe.AddMemberRef (_notSupportedExceptionRef, ".ctor",
+ sig => sig.MethodSignature (isInstanceMethod: true).Parameters (1,
+ rt => rt.Void (),
+ p => p.AddParameter ().Type ().String ()));
+
+ _jniObjectReferenceCtorRef = _pe.AddMemberRef (_jniObjectReferenceRef, ".ctor",
+ sig => sig.MethodSignature (isInstanceMethod: true).Parameters (1,
+ rt => rt.Void (),
+ p => p.AddParameter ().Type ().IntPtr ()));
+
+ // JNIEnv.DeleteRef(IntPtr, JniHandleOwnership) — static, internal
+ // Used by JI-style activation to clean up the original handle after constructing the peer.
+ // Matches the legacy TypeManager.CreateProxy behavior.
+ _jniEnvDeleteRefRef = _pe.AddMemberRef (_jniEnvRef, "DeleteRef",
+ sig => sig.MethodSignature ().Parameters (2,
+ rt => rt.Void (),
+ p => {
+ p.AddParameter ().Type ().IntPtr ();
+ p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true);
+ }));
+
+ EmitTypeMapAttributeCtorRef ();
+ EmitTypeMapAssociationAttributeCtorRef ();
+ }
+
+ void EmitTypeMapAttributeCtorRef ()
+ {
+ var metadata = _pe.Metadata;
+ var typeMapAttrOpenRef = metadata.AddTypeReference (_pe.SystemRuntimeInteropServicesRef,
+ metadata.GetOrAddString ("System.Runtime.InteropServices"),
+ metadata.GetOrAddString ("TypeMapAttribute`1"));
+ var javaLangObjectRef = metadata.AddTypeReference (_pe.MonoAndroidRef,
+ metadata.GetOrAddString ("Java.Lang"), metadata.GetOrAddString ("Object"));
+
+ var closedAttrTypeSpec = _pe.MakeGenericTypeSpec (typeMapAttrOpenRef, javaLangObjectRef);
+
+ // 2-arg: TypeMap(string jniName, Type proxyType) — unconditional
+ _typeMapAttrCtorRef2Arg = _pe.AddMemberRef (closedAttrTypeSpec, ".ctor",
+ sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2,
+ rt => rt.Void (),
+ p => {
+ p.AddParameter ().Type ().String ();
+ p.AddParameter ().Type ().Type (_systemTypeRef, false);
+ }));
+
+ // 3-arg: TypeMap(string jniName, Type proxyType, Type targetType) — trimmable
+ _typeMapAttrCtorRef3Arg = _pe.AddMemberRef (closedAttrTypeSpec, ".ctor",
+ sig => sig.MethodSignature (isInstanceMethod: true).Parameters (3,
+ rt => rt.Void (),
+ p => {
+ p.AddParameter ().Type ().String ();
+ p.AddParameter ().Type ().Type (_systemTypeRef, false);
+ p.AddParameter ().Type ().Type (_systemTypeRef, false);
+ }));
+ }
+
+ void EmitTypeMapAssociationAttributeCtorRef ()
+ {
+ var metadata = _pe.Metadata;
+ // TypeMapAssociationAttribute is in System.Runtime.InteropServices, takes 2 Type args:
+ // TypeMapAssociation(Type sourceType, Type aliasProxyType)
+ var typeMapAssociationAttrRef = metadata.AddTypeReference (_pe.SystemRuntimeInteropServicesRef,
+ metadata.GetOrAddString ("System.Runtime.InteropServices"),
+ metadata.GetOrAddString ("TypeMapAssociationAttribute"));
+
+ _typeMapAssociationAttrCtorRef = _pe.AddMemberRef (typeMapAssociationAttrRef, ".ctor",
+ sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2,
+ rt => rt.Void (),
+ p => {
+ p.AddParameter ().Type ().Type (_systemTypeRef, false);
+ p.AddParameter ().Type ().Type (_systemTypeRef, false);
+ }));
+ }
+
+ void EmitProxyType (JavaPeerProxyData proxy)
+ {
+ var metadata = _pe.Metadata;
+ metadata.AddTypeDefinition (
+ TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.Class,
+ metadata.GetOrAddString (proxy.Namespace),
+ metadata.GetOrAddString (proxy.TypeName),
+ _javaPeerProxyRef,
+ MetadataTokens.FieldDefinitionHandle (metadata.GetRowCount (TableIndex.Field) + 1),
+ MetadataTokens.MethodDefinitionHandle (metadata.GetRowCount (TableIndex.MethodDef) + 1));
+
+ // .ctor
+ _pe.EmitBody (".ctor",
+ MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName,
+ sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { }),
+ encoder => {
+ encoder.OpCode (ILOpCode.Ldarg_0);
+ encoder.Call (_baseCtorRef);
+ encoder.OpCode (ILOpCode.Ret);
+ });
+
+ // CreateInstance
+ EmitCreateInstance (proxy);
+
+ // get_TargetType
+ EmitTypeGetter ("get_TargetType", proxy.TargetType,
+ MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.SpecialName | MethodAttributes.HideBySig);
+
+ // get_InvokerType
+ if (proxy.InvokerType != null) {
+ EmitTypeGetter ("get_InvokerType", proxy.InvokerType,
+ MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig);
+ }
+ }
+
+ void EmitCreateInstance (JavaPeerProxyData proxy)
+ {
+ if (!proxy.HasActivation) {
+ EmitCreateInstanceNoActivation ();
+ return;
+ }
+
+ if (proxy.IsGenericDefinition) {
+ EmitCreateInstanceGenericDefinition ();
+ return;
+ }
+
+ // JavaInterop-style activation ctors (ref JniObjectReference, JniObjectReferenceOptions)
+ // require parameter conversion from (IntPtr, JniHandleOwnership).
+ if (proxy.ActivationCtor?.Style == ActivationCtorStyle.JavaInterop) {
+ if (proxy.InvokerType != null) {
+ EmitCreateInstanceViaJavaInteropNewobj (_pe.ResolveTypeRef (proxy.InvokerType));
+ } else {
+ var targetRef = _pe.ResolveTypeRef (proxy.TargetType);
+ var jiCtor = proxy.ActivationCtor ?? throw new InvalidOperationException ("ActivationCtor should not be null");
+ if (jiCtor.IsOnLeafType) {
+ EmitCreateInstanceViaJavaInteropNewobj (targetRef);
+ } else {
+ EmitCreateInstanceInheritedJavaInteropCtor (targetRef, jiCtor);
+ }
+ }
+ return;
+ }
+
+ if (proxy.InvokerType != null) {
+ EmitCreateInstanceViaNewobj (_pe.ResolveTypeRef (proxy.InvokerType));
+ return;
+ }
+
+ // At this point, ActivationCtor is guaranteed non-null (HasActivation && InvokerType == null)
+ var activationCtor = proxy.ActivationCtor ?? throw new InvalidOperationException ("ActivationCtor should not be null when HasActivation is true and InvokerType is null");
+ var targetTypeRef = _pe.ResolveTypeRef (proxy.TargetType);
+
+ if (activationCtor.IsOnLeafType) {
+ EmitCreateInstanceViaNewobj (targetTypeRef);
+ } else {
+ EmitCreateInstanceInheritedCtor (targetTypeRef, activationCtor);
+ }
+ }
+
+ void EmitCreateInstanceNoActivation ()
+ {
+ EmitCreateInstanceBody (encoder => {
+ encoder.OpCode (ILOpCode.Ldnull);
+ encoder.OpCode (ILOpCode.Ret);
+ });
+ }
+
+ void EmitCreateInstanceGenericDefinition ()
+ {
+ EmitCreateInstanceBody (encoder => {
+ encoder.LoadString (_pe.Metadata.GetOrAddUserString ("Cannot create instance of open generic type."));
+ encoder.OpCode (ILOpCode.Newobj);
+ encoder.Token (_notSupportedExceptionCtorRef);
+ encoder.OpCode (ILOpCode.Throw);
+ });
+ }
+
+ void EmitCreateInstanceViaNewobj (EntityHandle typeRef)
+ {
+ var ctorRef = AddActivationCtorRef (typeRef);
+ EmitCreateInstanceBody (encoder => {
+ encoder.OpCode (ILOpCode.Ldarg_1);
+ encoder.OpCode (ILOpCode.Ldarg_2);
+ encoder.OpCode (ILOpCode.Newobj);
+ encoder.Token (ctorRef);
+ encoder.OpCode (ILOpCode.Ret);
+ });
+ }
+
+ void EmitCreateInstanceInheritedCtor (EntityHandle targetTypeRef, ActivationCtorData activationCtor)
+ {
+ var baseActivationCtorRef = AddActivationCtorRef (_pe.ResolveTypeRef (activationCtor.DeclaringType));
+ EmitCreateInstanceBody (encoder => {
+ encoder.OpCode (ILOpCode.Ldtoken);
+ encoder.Token (targetTypeRef);
+ encoder.Call (_getTypeFromHandleRef);
+ encoder.Call (_getUninitializedObjectRef);
+ encoder.OpCode (ILOpCode.Castclass);
+ encoder.Token (targetTypeRef);
+
+ encoder.OpCode (ILOpCode.Dup);
+ encoder.OpCode (ILOpCode.Ldarg_1);
+ encoder.OpCode (ILOpCode.Ldarg_2);
+ encoder.Call (baseActivationCtorRef);
+
+ encoder.OpCode (ILOpCode.Ret);
+ });
+ }
+
+ ///
+ /// Emits CreateInstance for JavaInterop-style activation (leaf type):
+ /// var jniRef = new JniObjectReference(handle);
+ /// var result = new TargetType(ref jniRef, JniObjectReferenceOptions.Copy);
+ /// JNIEnv.DeleteRef(handle, ownership);
+ /// return result;
+ ///
+ void EmitCreateInstanceViaJavaInteropNewobj (EntityHandle typeRef)
+ {
+ var ctorRef = AddJavaInteropActivationCtorRef (typeRef);
+ EmitCreateInstanceBodyWithLocals (
+ EncodeJniObjectReferenceAndObjectLocals,
+ encoder => {
+ // var jniRef = new JniObjectReference(handle);
+ encoder.LoadLocalAddress (0);
+ encoder.OpCode (ILOpCode.Ldarg_1); // handle
+ encoder.Call (_jniObjectReferenceCtorRef);
+
+ // var result = new TargetType(ref jniRef, JniObjectReferenceOptions.Copy);
+ encoder.LoadLocalAddress (0);
+ encoder.LoadConstantI4 (1); // JniObjectReferenceOptions.Copy
+ encoder.OpCode (ILOpCode.Newobj);
+ encoder.Token (ctorRef);
+ encoder.StoreLocal (1); // save result
+
+ // JNIEnv.DeleteRef(handle, ownership);
+ encoder.OpCode (ILOpCode.Ldarg_1); // handle
+ encoder.OpCode (ILOpCode.Ldarg_2); // ownership
+ encoder.Call (_jniEnvDeleteRefRef);
+
+ encoder.LoadLocal (1); // load result
+ encoder.OpCode (ILOpCode.Ret);
+ });
+ }
+
+ ///
+ /// Emits CreateInstance for JavaInterop-style activation (inherited ctor):
+ /// var obj = (TargetType)RuntimeHelpers.GetUninitializedObject(typeof(TargetType));
+ /// var jniRef = new JniObjectReference(handle);
+ /// obj.BaseCtor(ref jniRef, JniObjectReferenceOptions.Copy);
+ /// JNIEnv.DeleteRef(handle, ownership);
+ /// return obj;
+ ///
+ void EmitCreateInstanceInheritedJavaInteropCtor (EntityHandle targetTypeRef, ActivationCtorData activationCtor)
+ {
+ var baseCtorRef = AddJavaInteropActivationCtorRef (_pe.ResolveTypeRef (activationCtor.DeclaringType));
+ EmitCreateInstanceBodyWithLocals (
+ EncodeJniObjectReferenceLocal,
+ encoder => {
+ // var obj = (TargetType)RuntimeHelpers.GetUninitializedObject(typeof(TargetType));
+ encoder.OpCode (ILOpCode.Ldtoken);
+ encoder.Token (targetTypeRef);
+ encoder.Call (_getTypeFromHandleRef);
+ encoder.Call (_getUninitializedObjectRef);
+ encoder.OpCode (ILOpCode.Castclass);
+ encoder.Token (targetTypeRef);
+
+ // dup obj (one copy for the call, one for the return)
+ encoder.OpCode (ILOpCode.Dup);
+
+ // var jniRef = new JniObjectReference(handle);
+ encoder.LoadLocalAddress (0);
+ encoder.OpCode (ILOpCode.Ldarg_1); // handle
+ encoder.Call (_jniObjectReferenceCtorRef);
+
+ // obj.BaseCtor(ref jniRef, JniObjectReferenceOptions.Copy);
+ encoder.LoadLocalAddress (0);
+ encoder.LoadConstantI4 (1); // JniObjectReferenceOptions.Copy
+ encoder.Call (baseCtorRef);
+
+ // JNIEnv.DeleteRef(handle, ownership);
+ encoder.OpCode (ILOpCode.Ldarg_1); // handle
+ encoder.OpCode (ILOpCode.Ldarg_2); // ownership
+ encoder.Call (_jniEnvDeleteRefRef);
+
+ encoder.OpCode (ILOpCode.Ret);
+ });
+ }
+
+ void EncodeJniObjectReferenceLocal (BlobBuilder blob)
+ {
+ // LOCAL_SIG header (0x07), count = 1, ELEMENT_TYPE_VALUETYPE + compressed token
+ blob.WriteByte (0x07); // LOCAL_SIG
+ blob.WriteCompressedInteger (1); // 1 local variable
+ blob.WriteByte (0x11); // ELEMENT_TYPE_VALUETYPE
+ blob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (_jniObjectReferenceRef));
+ }
+
+ void EncodeJniObjectReferenceAndObjectLocals (BlobBuilder blob)
+ {
+ // LOCAL_SIG header (0x07), count = 2:
+ // local 0: JniObjectReference (valuetype)
+ // local 1: object (for storing the newobj result across the DeleteRef call)
+ blob.WriteByte (0x07); // LOCAL_SIG
+ blob.WriteCompressedInteger (2); // 2 local variables
+ blob.WriteByte (0x11); // ELEMENT_TYPE_VALUETYPE
+ blob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (_jniObjectReferenceRef));
+ blob.WriteByte (0x1c); // ELEMENT_TYPE_OBJECT
+ }
+
+ MemberReferenceHandle AddJavaInteropActivationCtorRef (EntityHandle declaringTypeRef)
+ {
+ return _pe.AddMemberRef (declaringTypeRef, ".ctor",
+ sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2,
+ rt => rt.Void (),
+ p => {
+ // ref JniObjectReference — encoded as byref valuetype
+ p.AddParameter ().Type (isByRef: true).Type (_jniObjectReferenceRef, true);
+ // JniObjectReferenceOptions — encoded as valuetype (enum)
+ p.AddParameter ().Type ().Type (_jniObjectReferenceOptionsRef, true);
+ }));
+ }
+
+ void EmitCreateInstanceBody (Action emitIL)
+ {
+ _pe.EmitBody ("CreateInstance",
+ MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig,
+ sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2,
+ rt => rt.Type ().Type (_iJavaPeerableRef, false),
+ p => {
+ p.AddParameter ().Type ().IntPtr ();
+ p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true);
+ }),
+ emitIL);
+ }
+
+ void EmitCreateInstanceBodyWithLocals (Action encodeLocals, Action emitIL)
+ {
+ _pe.EmitBody ("CreateInstance",
+ MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig,
+ sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2,
+ rt => rt.Type ().Type (_iJavaPeerableRef, false),
+ p => {
+ p.AddParameter ().Type ().IntPtr ();
+ p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true);
+ }),
+ emitIL,
+ encodeLocals);
+ }
+
+ MemberReferenceHandle AddActivationCtorRef (EntityHandle declaringTypeRef)
+ {
+ return _pe.AddMemberRef (declaringTypeRef, ".ctor",
+ sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2,
+ rt => rt.Void (),
+ p => {
+ p.AddParameter ().Type ().IntPtr ();
+ p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true);
+ }));
+ }
+
+ void EmitTypeGetter (string methodName, TypeRefData typeRef, MethodAttributes attrs)
+ {
+ var handle = _pe.ResolveTypeRef (typeRef);
+
+ _pe.EmitBody (methodName, attrs,
+ sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0,
+ rt => rt.Type ().Type (_systemTypeRef, false),
+ p => { }),
+ encoder => {
+ encoder.OpCode (ILOpCode.Ldtoken);
+ encoder.Token (handle);
+ encoder.Call (_getTypeFromHandleRef);
+ encoder.OpCode (ILOpCode.Ret);
+ });
+ }
+
+ void EmitTypeMapAttribute (TypeMapAttributeData entry)
+ {
+ var ctorRef = entry.IsUnconditional ? _typeMapAttrCtorRef2Arg : _typeMapAttrCtorRef3Arg;
+ var blob = _pe.BuildAttributeBlob (b => {
+ b.WriteSerializedString (entry.JniName);
+ b.WriteSerializedString (entry.ProxyTypeReference);
+ if (!entry.IsUnconditional) {
+ if (entry.TargetTypeReference is null) {
+ throw new InvalidOperationException ($"TargetTypeReference must not be null for conditional entry '{entry.JniName}'");
+ }
+ b.WriteSerializedString (entry.TargetTypeReference);
+ }
+ });
+ _pe.Metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, ctorRef, blob);
+ }
+
+ void EmitTypeMapAssociationAttribute (TypeMapAssociationData assoc)
+ {
+ var blob = _pe.BuildAttributeBlob (b => {
+ b.WriteSerializedString (assoc.SourceTypeReference);
+ b.WriteSerializedString (assoc.AliasProxyTypeReference);
+ });
+ _pe.Metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, _typeMapAssociationAttrCtorRef, blob);
+ }
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs
new file mode 100644
index 00000000000..f6586218d6a
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+///
+/// High-level API: builds the model from peers, then emits the PE assembly.
+/// Composes + .
+///
+sealed class TypeMapAssemblyGenerator
+{
+ readonly Version _systemRuntimeVersion;
+
+ /// Version for System.Runtime assembly references.
+ public TypeMapAssemblyGenerator (Version systemRuntimeVersion)
+ {
+ _systemRuntimeVersion = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion));
+ }
+
+ ///
+ /// Generates a TypeMap PE assembly from the given Java peer info records.
+ ///
+ /// Scanned Java peer types.
+ /// Path where the output .dll will be written.
+ /// Optional explicit assembly name. Derived from outputPath if null.
+ public void Generate (IReadOnlyList peers, string outputPath, string? assemblyName = null)
+ {
+ var model = ModelBuilder.Build (peers, outputPath, assemblyName);
+ var emitter = new TypeMapAssemblyEmitter (_systemRuntimeVersion);
+ emitter.Emit (model, outputPath);
+ }
+
+ ///
+ /// Generates a TypeMap PE assembly from the given Java peer info records and writes it to .
+ ///
+ /// Scanned Java peer types.
+ /// Stream to write the output PE assembly to.
+ /// Assembly name for the generated assembly.
+ public void Generate (IReadOnlyList peers, Stream stream, string assemblyName)
+ {
+ var model = ModelBuilder.Build (peers, assemblyName + ".dll", assemblyName);
+ var emitter = new TypeMapAssemblyEmitter (_systemRuntimeVersion);
+ emitter.Emit (model, stream);
+ }
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs
index 85472f1b3ba..c34d7f2009c 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs
@@ -28,6 +28,16 @@ sealed record JavaPeerInfo
///
public required string ManagedTypeName { get; init; }
+ ///
+ /// Managed type namespace, e.g., "Android.App".
+ ///
+ public string ManagedTypeNamespace { get; init; } = "";
+
+ ///
+ /// Managed type short name (without namespace), e.g., "Activity".
+ ///
+ public string ManagedTypeShortName { get; init; } = "";
+
///
/// Assembly name the type belongs to, e.g., "Mono.Android".
///
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
index 469d6345596..fc3627224f5 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
@@ -218,6 +218,8 @@ void ScanAssembly (AssemblyIndex index, Dictionary results
JavaName = jniName,
CompatJniName = compatJniName,
ManagedTypeName = fullName,
+ ManagedTypeNamespace = ExtractNamespace (fullName),
+ ManagedTypeShortName = ExtractShortName (fullName),
AssemblyName = index.AssemblyName,
BaseJavaName = baseJavaName,
ImplementedInterfaceJavaNames = implementedInterfaces,
@@ -732,4 +734,22 @@ static string GetCrc64PackageName (string ns, string assemblyName)
var hash = System.IO.Hashing.Crc64.Hash (data);
return $"crc64{BitConverter.ToString (hash).Replace ("-", "").ToLowerInvariant ()}";
}
+
+ static string ExtractNamespace (string fullName)
+ {
+ // Strip nested type suffix (e.g., "My.Namespace.Outer+Inner" → "My.Namespace.Outer")
+ int plusIndex = fullName.IndexOf ('+');
+ var nameForNamespace = plusIndex >= 0 ? fullName.Substring (0, plusIndex) : fullName;
+ int lastDot = nameForNamespace.LastIndexOf ('.');
+ return lastDot >= 0 ? nameForNamespace.Substring (0, lastDot) : "";
+ }
+
+ static string ExtractShortName (string fullName)
+ {
+ var span = fullName.AsSpan ();
+ int lastDot = span.LastIndexOf ('.');
+ var typePart = lastDot >= 0 ? span.Slice (lastDot + 1) : span;
+ int lastPlus = typePart.LastIndexOf ('+');
+ return (lastPlus >= 0 ? typePart.Slice (lastPlus + 1) : typePart).ToString ();
+ }
}
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs
new file mode 100644
index 00000000000..70471f62e13
--- /dev/null
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs
@@ -0,0 +1,112 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection.Metadata;
+using System.Reflection.Metadata.Ecma335;
+using Xunit;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests;
+
+public abstract class FixtureTestBase
+{
+ static string TestFixtureAssemblyPath {
+ get {
+ var testAssemblyDir = Path.GetDirectoryName (typeof (FixtureTestBase).Assembly.Location)
+ ?? throw new InvalidOperationException ("Cannot determine test assembly directory");
+ var fixtureAssembly = Path.Combine (testAssemblyDir, "TestFixtures.dll");
+ Assert.True (File.Exists (fixtureAssembly),
+ $"TestFixtures.dll not found at {fixtureAssembly}. Ensure the TestFixtures project builds.");
+ return fixtureAssembly;
+ }
+ }
+
+ static readonly Lazy> _cachedFixtures = new (() => {
+ using var scanner = new JavaPeerScanner ();
+ return scanner.Scan (new [] { TestFixtureAssemblyPath });
+ });
+
+ private protected static List ScanFixtures () => _cachedFixtures.Value;
+
+ private protected static JavaPeerInfo FindFixtureByJavaName (string javaName)
+ {
+ var peers = ScanFixtures ();
+ var peer = peers.FirstOrDefault (p => p.JavaName == javaName);
+ Assert.NotNull (peer);
+ return peer;
+ }
+
+ private protected static JavaPeerInfo FindFixtureByManagedName (string managedName)
+ {
+ var peers = ScanFixtures ();
+ var peer = peers.FirstOrDefault (p => p.ManagedTypeName == managedName);
+ Assert.NotNull (peer);
+ return peer;
+ }
+
+ static (string ns, string shortName) ParseManagedTypeName (string managedName)
+ {
+ var ns = managedName.Contains ('.') ? managedName.Substring (0, managedName.LastIndexOf ('.')) : "";
+ var typePart = managedName.Contains ('.') ? managedName.Substring (managedName.LastIndexOf ('.') + 1) : managedName;
+ var shortName = typePart.Contains ('+') ? typePart.Substring (typePart.LastIndexOf ('+') + 1) : typePart;
+ return (ns, shortName);
+ }
+
+ private protected static JavaPeerInfo MakeMcwPeer (string jniName, string managedName, string asmName)
+ {
+ var (ns, shortName) = ParseManagedTypeName (managedName);
+ return new JavaPeerInfo {
+ JavaName = jniName,
+ CompatJniName = jniName,
+ ManagedTypeName = managedName,
+ ManagedTypeNamespace = ns,
+ ManagedTypeShortName = shortName,
+ AssemblyName = asmName,
+ };
+ }
+
+ private protected static JavaPeerInfo MakePeerWithActivation (string jniName, string managedName, string asmName)
+ {
+ return MakeMcwPeer (jniName, managedName, asmName) with {
+ ActivationCtor = new ActivationCtorInfo {
+ DeclaringTypeName = managedName,
+ DeclaringAssemblyName = asmName,
+ Style = ActivationCtorStyle.XamarinAndroid,
+ },
+ };
+ }
+
+ private protected static JavaPeerInfo MakeAcwPeer (string jniName, string managedName, string asmName)
+ => MakePeerWithActivation (jniName, managedName, asmName);
+
+ private protected static JavaPeerInfo MakeInterfacePeer (
+ string jniName,
+ string managedName,
+ string asmName,
+ string invokerName)
+ {
+ var (ns, shortName) = ParseManagedTypeName (managedName);
+ return new JavaPeerInfo {
+ JavaName = jniName,
+ CompatJniName = jniName,
+ ManagedTypeName = managedName,
+ ManagedTypeNamespace = ns,
+ ManagedTypeShortName = shortName,
+ AssemblyName = asmName,
+ IsInterface = true,
+ InvokerTypeName = invokerName,
+ };
+ }
+
+ private protected static List GetTypeRefNames (MetadataReader reader) =>
+ reader.TypeReferences
+ .Select (h => reader.GetTypeReference (h))
+ .Select (t => reader.GetString (t.Name))
+ .ToList ();
+
+ private protected static List GetMemberRefNames (MetadataReader reader) =>
+ Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef))
+ .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i)))
+ .Select (m => reader.GetString (m.Name))
+ .ToList ();
+}
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs
new file mode 100644
index 00000000000..088ccdab8d9
--- /dev/null
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs
@@ -0,0 +1,105 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection.Metadata;
+using System.Reflection.PortableExecutable;
+using Xunit;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests;
+
+public class RootTypeMapAssemblyGeneratorTests : FixtureTestBase
+{
+ static MemoryStream GenerateRootAssembly (IReadOnlyList perAssemblyNames, string? assemblyName = null)
+ {
+ var stream = new MemoryStream ();
+ var generator = new RootTypeMapAssemblyGenerator (new Version (11, 0, 0, 0));
+ generator.Generate (perAssemblyNames, stream, assemblyName);
+ stream.Position = 0;
+ return stream;
+ }
+
+ [Fact]
+ public void Generate_ProducesValidPEAssembly ()
+ {
+ using var stream = GenerateRootAssembly (new [] { "_App.TypeMap", "_Mono.Android.TypeMap" });
+ using var pe = new PEReader (stream);
+ Assert.True (pe.HasMetadata);
+ }
+
+ [Theory]
+ [InlineData (null, "_Microsoft.Android.TypeMaps")]
+ [InlineData ("MyRoot", "MyRoot")]
+ public void Generate_AssemblyName_MatchesExpected (string? assemblyName, string expectedName)
+ {
+ using var stream = GenerateRootAssembly ([], assemblyName);
+ using var pe = new PEReader (stream);
+ var reader = pe.GetMetadataReader ();
+ var asmDef = reader.GetAssemblyDefinition ();
+ Assert.Equal (expectedName, reader.GetString (asmDef.Name));
+ }
+
+ [Fact]
+ public void Generate_ReferencesGenericTypeMapAssemblyTargetAttribute ()
+ {
+ using var stream = GenerateRootAssembly (new [] { "_App.TypeMap" });
+ using var pe = new PEReader (stream);
+ var reader = pe.GetMetadataReader ();
+
+ var typeRefs = reader.TypeReferences
+ .Select (h => reader.GetTypeReference (h))
+ .ToList ();
+ Assert.Contains (typeRefs, t =>
+ reader.GetString (t.Name) == "TypeMapAssemblyTargetAttribute`1" &&
+ reader.GetString (t.Namespace) == "System.Runtime.InteropServices");
+
+ Assert.Contains (typeRefs, t =>
+ reader.GetString (t.Name) == "Object" &&
+ reader.GetString (t.Namespace) == "Java.Lang");
+
+ var typeDefs = reader.TypeDefinitions
+ .Select (h => reader.GetTypeDefinition (h))
+ .ToList ();
+ Assert.DoesNotContain (typeDefs, t =>
+ reader.GetString (t.Name).Contains ("TypeMapAssemblyTarget"));
+ }
+
+ [Theory]
+ [InlineData (0, 0)]
+ [InlineData (3, 3)]
+ public void Generate_AttributeCount_MatchesTargetCount (int targetCount, int expectedCount)
+ {
+ var targets = Enumerable.Range (0, targetCount).Select (i => $"_Target{i}.TypeMap").ToArray ();
+ using var stream = GenerateRootAssembly (targets);
+ using var pe = new PEReader (stream);
+ var reader = pe.GetMetadataReader ();
+ var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition);
+ Assert.Equal (expectedCount, asmAttrs.Count ());
+ }
+
+ [Fact]
+ public void Generate_AttributeBlobValues_MatchTargetNames ()
+ {
+ var targets = new [] { "_App.TypeMap", "_Mono.Android.TypeMap" };
+ using var stream = GenerateRootAssembly (targets);
+ using var pe = new PEReader (stream);
+ var reader = pe.GetMetadataReader ();
+
+ var attrValues = new List ();
+ foreach (var attrHandle in reader.GetCustomAttributes (EntityHandle.AssemblyDefinition)) {
+ var attr = reader.GetCustomAttribute (attrHandle);
+ var blob = reader.GetBlobReader (attr.Value);
+
+ // Custom attribute blob: prolog (2 bytes) + SerString value
+ var prolog = blob.ReadUInt16 ();
+ Assert.Equal (1, prolog); // ECMA-335 prolog
+ var value = blob.ReadSerializedString ();
+ Assert.NotNull (value);
+ attrValues.Add (value!);
+ }
+
+ Assert.Equal (2, attrValues.Count);
+ Assert.Contains ("_App.TypeMap", attrValues);
+ Assert.Contains ("_Mono.Android.TypeMap", attrValues);
+ }
+}
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs
new file mode 100644
index 00000000000..596528b742f
--- /dev/null
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs
@@ -0,0 +1,413 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Reflection.Metadata;
+using System.Reflection.Metadata.Ecma335;
+using System.Reflection.PortableExecutable;
+using Xunit;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests;
+
+public class TypeMapAssemblyGeneratorTests : FixtureTestBase
+{
+ static MemoryStream GenerateAssembly (IReadOnlyList peers, string assemblyName = "TestTypeMap")
+ {
+ var stream = new MemoryStream ();
+ var generator = new TypeMapAssemblyGenerator (new Version (11, 0, 0, 0));
+ generator.Generate (peers, stream, assemblyName);
+ stream.Position = 0;
+ return stream;
+ }
+
+ [Fact]
+ public void Generate_ProducesValidPEAssembly ()
+ {
+ var peers = ScanFixtures ();
+ using var stream = GenerateAssembly (peers);
+ using var pe = new PEReader (stream);
+ Assert.True (pe.HasMetadata);
+ var reader = pe.GetMetadataReader ();
+ Assert.NotNull (reader);
+ }
+
+ [Fact]
+ public void Generate_HasRequiredAssemblyReferences ()
+ {
+ var peers = ScanFixtures ();
+ using var stream = GenerateAssembly (peers);
+ using var pe = new PEReader (stream);
+ var reader = pe.GetMetadataReader ();
+ var asmRefs = reader.AssemblyReferences
+ .Select (h => reader.GetString (reader.GetAssemblyReference (h).Name))
+ .ToList ();
+ Assert.Contains ("System.Runtime", asmRefs);
+ Assert.Contains ("Mono.Android", asmRefs);
+ Assert.Contains ("Java.Interop", asmRefs);
+ Assert.Contains ("System.Runtime.InteropServices", asmRefs);
+ }
+
+ [Fact]
+ public void Generate_CreatesProxyTypes ()
+ {
+ var peers = ScanFixtures ();
+ using var stream = GenerateAssembly (peers);
+ using var pe = new PEReader (stream);
+ var reader = pe.GetMetadataReader ();
+ var proxyTypes = reader.TypeDefinitions
+ .Select (h => reader.GetTypeDefinition (h))
+ .Where (t => reader.GetString (t.Namespace) == "_TypeMap.Proxies")
+ .ToList ();
+
+ Assert.NotEmpty (proxyTypes);
+ Assert.Contains (proxyTypes, t => reader.GetString (t.Name) == "Java_Lang_Object_Proxy");
+ }
+
+ [Fact]
+ public void Generate_ProxyType_HasCtorAndCreateInstance ()
+ {
+ var peers = ScanFixtures ();
+ using var stream = GenerateAssembly (peers);
+ using var pe = new PEReader (stream);
+ var reader = pe.GetMetadataReader ();
+ var objectProxy = reader.TypeDefinitions
+ .Select (h => reader.GetTypeDefinition (h))
+ .First (t => reader.GetString (t.Name) == "Java_Lang_Object_Proxy");
+
+ var methods = objectProxy.GetMethods ()
+ .Select (h => reader.GetMethodDefinition (h))
+ .Select (m => reader.GetString (m.Name))
+ .ToList ();
+
+ Assert.Contains (".ctor", methods);
+ Assert.Contains ("CreateInstance", methods);
+ Assert.Contains ("get_TargetType", methods);
+ }
+
+ [Fact]
+ public void Generate_HasIgnoresAccessChecksToAttribute ()
+ {
+ var peers = ScanFixtures ();
+ using var stream = GenerateAssembly (peers);
+ using var pe = new PEReader (stream);
+ var reader = pe.GetMetadataReader ();
+ var types = reader.TypeDefinitions
+ .Select (h => reader.GetTypeDefinition (h))
+ .ToList ();
+ Assert.Contains (types, t =>
+ reader.GetString (t.Name) == "IgnoresAccessChecksToAttribute" &&
+ reader.GetString (t.Namespace) == "System.Runtime.CompilerServices");
+ }
+
+ [Fact]
+ public void Generate_DuplicateJniNames_CreatesAliasEntriesAndAssociationAttribute ()
+ {
+ var peers = new List {
+ new JavaPeerInfo {
+ JavaName = "test/Duplicate",
+ CompatJniName = "test/Duplicate",
+ ManagedTypeName = "Test.Duplicate1",
+ ManagedTypeNamespace = "Test",
+ ManagedTypeShortName = "Duplicate1",
+ AssemblyName = "TestAssembly",
+ ActivationCtor = new ActivationCtorInfo {
+ DeclaringTypeName = "Test.Duplicate1",
+ DeclaringAssemblyName = "TestAssembly",
+ Style = ActivationCtorStyle.XamarinAndroid,
+ },
+ },
+ new JavaPeerInfo {
+ JavaName = "test/Duplicate",
+ CompatJniName = "test/Duplicate",
+ ManagedTypeName = "Test.Duplicate2",
+ ManagedTypeNamespace = "Test",
+ ManagedTypeShortName = "Duplicate2",
+ AssemblyName = "TestAssembly",
+ },
+ };
+
+ using var stream = GenerateAssembly (peers, "AliasTest");
+ using var pe = new PEReader (stream);
+ var reader = pe.GetMetadataReader ();
+ var assemblyAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition);
+ Assert.True (assemblyAttrs.Count () >= 3);
+
+ var typeNames = GetTypeRefNames (reader);
+ Assert.Contains ("TypeMapAssociationAttribute", typeNames);
+ }
+
+ [Fact]
+ public void Generate_EmptyPeerList_ProducesValidAssembly ()
+ {
+ using var stream = GenerateAssembly ([], "EmptyTest");
+ using var pe = new PEReader (stream);
+ var reader = pe.GetMetadataReader ();
+ Assert.NotNull (reader);
+ var asmDef = reader.GetAssemblyDefinition ();
+ Assert.Equal ("EmptyTest", reader.GetString (asmDef.Name));
+ }
+
+ [Fact]
+ public void Generate_SimpleActivity_UsesGetUninitializedObject ()
+ {
+ var peers = ScanFixtures ();
+ var simpleActivity = peers.First (p => p.JavaName == "my/app/SimpleActivity");
+ Assert.NotNull (simpleActivity.ActivationCtor);
+ Assert.NotEqual (simpleActivity.ManagedTypeName, simpleActivity.ActivationCtor.DeclaringTypeName);
+
+ using var stream = GenerateAssembly (new [] { simpleActivity }, "InheritedCtorTest");
+ using var pe = new PEReader (stream);
+ var reader = pe.GetMetadataReader ();
+ var typeNames = GetTypeRefNames (reader);
+ Assert.Contains ("RuntimeHelpers", typeNames);
+
+ var memberNames = GetMemberRefNames (reader);
+ Assert.DoesNotContain ("CreateManagedPeer", memberNames);
+ Assert.Contains ("GetUninitializedObject", memberNames);
+ }
+
+ [Fact]
+ public void Generate_LeafCtor_DoesNotUseCreateManagedPeer ()
+ {
+ var peers = ScanFixtures ();
+ // ClickableView has its own (IntPtr, JniHandleOwnership) ctor
+ var clickableView = peers.First (p => p.JavaName == "my/app/ClickableView");
+ Assert.NotNull (clickableView.ActivationCtor);
+ Assert.Equal (clickableView.ManagedTypeName, clickableView.ActivationCtor.DeclaringTypeName);
+
+ using var stream = GenerateAssembly (new [] { clickableView }, "LeafCtorTest");
+ using var pe = new PEReader (stream);
+ var reader = pe.GetMetadataReader ();
+ var memberNames = GetMemberRefNames (reader);
+ Assert.DoesNotContain ("CreateManagedPeer", memberNames);
+
+ var ctorRefs = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef))
+ .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i)))
+ .Where (m => reader.GetString (m.Name) == ".ctor")
+ .ToList ();
+ Assert.True (ctorRefs.Count >= 2, "Should have ctor refs for proxy base + target type");
+ }
+
+ [Fact]
+ public void Generate_GenericType_ThrowsNotSupportedException ()
+ {
+ var peers = ScanFixtures ();
+ var generic = peers.First (p => p.JavaName == "my/app/GenericHolder");
+ Assert.True (generic.IsGenericDefinition);
+
+ using var stream = GenerateAssembly (new [] { generic }, "GenericTest");
+ using var pe = new PEReader (stream);
+ var reader = pe.GetMetadataReader ();
+ var typeNames = GetTypeRefNames (reader);
+ Assert.Contains ("NotSupportedException", typeNames);
+ }
+
+ [Fact]
+ public void Generate_InheritedCtor_IncludesBaseCtorAssembly ()
+ {
+ // SimpleActivity inherits activation ctor from Activity — both in TestFixtures
+ // but the generated assembly is "IgnoresAccessTest", so TestFixtures must be
+ // in IgnoresAccessChecksTo
+ var peers = ScanFixtures ();
+ var simpleActivity = peers.First (p => p.JavaName == "my/app/SimpleActivity");
+
+ using var stream = GenerateAssembly (new [] { simpleActivity }, "IgnoresAccessTest");
+ using var pe = new PEReader (stream);
+ var reader = pe.GetMetadataReader ();
+ var ignoresAttrType = reader.TypeDefinitions
+ .Select (h => reader.GetTypeDefinition (h))
+ .FirstOrDefault (t => reader.GetString (t.Name) == "IgnoresAccessChecksToAttribute");
+ Assert.True (ignoresAttrType.Attributes != 0, "IgnoresAccessChecksToAttribute should be defined");
+
+ var assemblyAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition);
+ var attrBlobs = new List ();
+ foreach (var attrHandle in assemblyAttrs) {
+ var attr = reader.GetCustomAttribute (attrHandle);
+ var blob = reader.GetBlobBytes (attr.Value);
+ var blobStr = System.Text.Encoding.UTF8.GetString (blob);
+ attrBlobs.Add (blobStr);
+ }
+ // Activity is in TestFixtures, so IgnoresAccessChecksTo must include TestFixtures
+ Assert.Contains (attrBlobs, b => b.Contains ("TestFixtures"));
+ }
+
+ [Fact]
+ public void Generate_JiStyleCtor_EmitsJavaInteropActivation ()
+ {
+ var peers = ScanFixtures ();
+ var jiPeer = peers.First (p => p.JavaName == "my/app/JiStylePeer");
+ Assert.NotNull (jiPeer.ActivationCtor);
+ Assert.Equal (ActivationCtorStyle.JavaInterop, jiPeer.ActivationCtor.Style);
+
+ using var stream = GenerateAssembly (new [] { jiPeer }, "JiStyleTest");
+ using var pe = new PEReader (stream);
+ var reader = pe.GetMetadataReader ();
+
+ // JI-style activation should emit JniObjectReference and JniObjectReferenceOptions type refs
+ var typeNames = GetTypeRefNames (reader);
+ Assert.Contains ("JniObjectReference", typeNames);
+ Assert.Contains ("JniObjectReferenceOptions", typeNames);
+
+ // The proxy still exists (with a TargetType property)
+ var proxyTypes = reader.TypeDefinitions
+ .Select (h => reader.GetTypeDefinition (h))
+ .Where (t => reader.GetString (t.Namespace) == "_TypeMap.Proxies")
+ .ToList ();
+ Assert.Single (proxyTypes);
+ }
+
+ [Fact]
+ public void Emit_CalledTwice_Throws ()
+ {
+ var model = ModelBuilder.Build ([], "Double.dll", "Double");
+ var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0));
+ emitter.Emit (model, new MemoryStream ());
+ // MetadataBuilder.AddAssembly throws on second call (only one assembly definition per PE)
+ Assert.ThrowsAny (() => emitter.Emit (model, new MemoryStream ()));
+ }
+
+ [Fact]
+ public void EmitBody_ILCallbackCallsAddMemberRef_SignatureNotCorrupted ()
+ {
+ // Regression test: EmitBody uses shared _sigBlob for the method signature.
+ // If the emitIL callback calls AddMemberRef (which also uses _sigBlob),
+ // the method signature must not be corrupted.
+ var pe = new PEAssemblyBuilder (new Version (11, 0, 0, 0));
+ pe.EmitPreamble ("SigTest", "SigTest.dll");
+
+ var objectRef = pe.Metadata.AddTypeReference (pe.SystemRuntimeRef,
+ pe.Metadata.GetOrAddString ("System"), pe.Metadata.GetOrAddString ("Object"));
+
+ // already defined; add a type to host the method
+ pe.Metadata.AddTypeDefinition (
+ System.Reflection.TypeAttributes.Public | System.Reflection.TypeAttributes.Class,
+ pe.Metadata.GetOrAddString ("Test"),
+ pe.Metadata.GetOrAddString ("MyType"),
+ objectRef,
+ MetadataTokens.FieldDefinitionHandle (pe.Metadata.GetRowCount (TableIndex.Field) + 1),
+ MetadataTokens.MethodDefinitionHandle (pe.Metadata.GetRowCount (TableIndex.MethodDef) + 1));
+
+ // EmitBody with an IL callback that calls AddMemberRef (clearing _sigBlob)
+ pe.EmitBody ("TestMethod",
+ MethodAttributes.Public | MethodAttributes.Static,
+ sig => sig.MethodSignature ().Parameters (1,
+ rt => rt.Void (),
+ p => p.AddParameter ().Type ().Int32 ()),
+ encoder => {
+ // This AddMemberRef call clears and repopulates _sigBlob
+ pe.AddMemberRef (objectRef, ".ctor",
+ s => s.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { }));
+ encoder.OpCode (ILOpCode.Ret);
+ });
+
+ // If the sig blob was corrupted, the PE metadata will have a wrong signature.
+ // Write and read back to verify.
+ var stream = new MemoryStream ();
+ pe.WritePE (stream);
+ stream.Position = 0;
+
+ using var peReader = new PEReader (stream);
+ var reader = peReader.GetMetadataReader ();
+ var methods = reader.TypeDefinitions
+ .SelectMany (h => reader.GetTypeDefinition (h).GetMethods ())
+ .Select (h => reader.GetMethodDefinition (h))
+ .ToList ();
+
+ var testMethod = methods.First (m => reader.GetString (m.Name) == "TestMethod");
+ var sig = testMethod.DecodeSignature (SignatureTypeProvider.Instance, null);
+ var paramType = Assert.Single (sig.ParameterTypes);
+ Assert.Equal ("System.Int32", paramType);
+ }
+
+ [Fact]
+ public void Generate_JiStyleCtor_FirstParamIsByRef ()
+ {
+ var peers = ScanFixtures ();
+ var jiPeer = peers.First (p => p.JavaName == "my/app/JiStylePeer");
+ Assert.Equal (ActivationCtorStyle.JavaInterop, jiPeer.ActivationCtor!.Style);
+
+ using var stream = GenerateAssembly (new [] { jiPeer }, "JiByRefTest");
+ using var pe = new PEReader (stream);
+ var reader = pe.GetMetadataReader ();
+
+ // Find the .ctor member reference whose parent type is the JI peer's declaring type
+ var ctorRefs = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef))
+ .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i)))
+ .Where (m => reader.GetString (m.Name) == ".ctor")
+ .ToList ();
+
+ // Decode each .ctor signature and find the JI-style one (2 params, first is byref JniObjectReference)
+ bool foundByRefCtor = false;
+ foreach (var ctor in ctorRefs) {
+ var sig = ctor.DecodeMethodSignature (SignatureTypeProvider.Instance, null);
+ if (sig.ParameterTypes.Length == 2 &&
+ sig.ParameterTypes [0].Contains ("JniObjectReference")) {
+ // The byref encoding should produce "Java.Interop.JniObjectReference&"
+ Assert.True (sig.ParameterTypes [0].EndsWith ("&"),
+ $"JI-style .ctor first param must be byref, got: {sig.ParameterTypes [0]}");
+ foundByRefCtor = true;
+ }
+ }
+ Assert.True (foundByRefCtor, "Expected to find a .ctor with byref JniObjectReference parameter");
+ }
+
+ [Fact]
+ public void Generate_JiStyleCtor_EmitsDeleteRefCall ()
+ {
+ var peers = ScanFixtures ();
+ var jiPeer = peers.First (p => p.JavaName == "my/app/JiStylePeer");
+
+ using var stream = GenerateAssembly (new [] { jiPeer }, "JiDeleteRefTest");
+ using var pe = new PEReader (stream);
+ var reader = pe.GetMetadataReader ();
+
+ // The JI-style activation path must emit a call to JNIEnv.DeleteRef(IntPtr, JniHandleOwnership)
+ // to match the legacy TypeManager.CreateProxy behavior.
+ var memberRefs = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef))
+ .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i)))
+ .ToList ();
+
+ var deleteRefRef = memberRefs.FirstOrDefault (m => reader.GetString (m.Name) == "DeleteRef");
+ Assert.True (!deleteRefRef.Equals (default (MemberReference)),
+ "JI-style activation must emit a DeleteRef member reference for JNI handle cleanup");
+
+ // Verify it's on the JNIEnv type
+ var parentTypeRef = reader.GetTypeReference ((TypeReferenceHandle)deleteRefRef.Parent);
+ Assert.Equal ("JNIEnv", reader.GetString (parentTypeRef.Name));
+ Assert.Equal ("Android.Runtime", reader.GetString (parentTypeRef.Namespace));
+ }
+
+ [Fact]
+ public void Generate_DifferentContent_ProducesDifferentMVIDs ()
+ {
+ var peer1 = MakePeerWithActivation ("test/TypeA", "Test.TypeA", "TestAsm");
+ var peer2 = MakePeerWithActivation ("test/TypeB", "Test.TypeB", "TestAsm");
+
+ using var stream1 = GenerateAssembly (new [] { peer1 }, "SameName");
+ using var stream2 = GenerateAssembly (new [] { peer2 }, "SameName");
+
+ using var pe1 = new PEReader (stream1);
+ using var pe2 = new PEReader (stream2);
+ var mvid1 = pe1.GetMetadataReader ().GetGuid (pe1.GetMetadataReader ().GetModuleDefinition ().Mvid);
+ var mvid2 = pe2.GetMetadataReader ().GetGuid (pe2.GetMetadataReader ().GetModuleDefinition ().Mvid);
+
+ Assert.NotEqual (mvid1, mvid2);
+ }
+
+ [Fact]
+ public void Generate_IdenticalContent_ProducesIdenticalMVIDs ()
+ {
+ var peer = MakePeerWithActivation ("test/TypeA", "Test.TypeA", "TestAsm");
+
+ using var stream1 = GenerateAssembly (new [] { peer }, "SameName");
+ using var stream2 = GenerateAssembly (new [] { peer }, "SameName");
+
+ using var pe1 = new PEReader (stream1);
+ using var pe2 = new PEReader (stream2);
+ var mvid1 = pe1.GetMetadataReader ().GetGuid (pe1.GetMetadataReader ().GetModuleDefinition ().Mvid);
+ var mvid2 = pe2.GetMetadataReader ().GetGuid (pe2.GetMetadataReader ().GetModuleDefinition ().Mvid);
+
+ Assert.Equal (mvid1, mvid2);
+ }
+}
\ No newline at end of file
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs
new file mode 100644
index 00000000000..31a06dec2e9
--- /dev/null
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs
@@ -0,0 +1,739 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection.Metadata;
+using System.Reflection.PortableExecutable;
+using Xunit;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests;
+
+public class ModelBuilderTests : FixtureTestBase
+{
+ static TypeMapAssemblyData BuildModel (IReadOnlyList peers, string? assemblyName = null)
+ {
+ var outputPath = Path.Combine (Path.GetTempPath (), (assemblyName ?? "TestTypeMap") + ".dll");
+ return ModelBuilder.Build (peers, outputPath, assemblyName);
+ }
+
+ public class BasicStructure
+ {
+ [Fact]
+ public void Build_EmptyPeers_ProducesEmptyModel ()
+ {
+ var model = BuildModel ([], "Empty");
+ Assert.Equal ("Empty", model.AssemblyName);
+ Assert.Equal ("Empty.dll", model.ModuleName);
+ Assert.Empty (model.Entries);
+ Assert.Empty (model.ProxyTypes);
+ }
+
+ [Theory]
+ [InlineData ("Foo.Bar.dll", null, "Foo.Bar")]
+ [InlineData ("Foo.dll", "MyAssembly", "MyAssembly")]
+ public void Build_AssemblyName_ResolvedCorrectly (string outputPath, string? explicitName, string expected)
+ {
+ var model = ModelBuilder.Build ([], outputPath, explicitName);
+ Assert.Equal (expected, model.AssemblyName);
+ }
+ }
+
+ public class TypeMapEntries
+ {
+ [Fact]
+ public void Build_CreatesOneEntryPerPeer ()
+ {
+ var peers = new List {
+ MakeMcwPeer ("java/lang/Object", "Java.Lang.Object", "Mono.Android"),
+ MakeMcwPeer ("android/app/Activity", "Android.App.Activity", "Mono.Android"),
+ };
+
+ var model = BuildModel (peers);
+ Assert.Equal (2, model.Entries.Count);
+ Assert.Equal ("android/app/Activity", model.Entries [0].JniName);
+ Assert.Equal ("java/lang/Object", model.Entries [1].JniName);
+ }
+
+ [Fact]
+ public void Build_DuplicateJniNames_CreatesAliasEntries ()
+ {
+ var peers = new List {
+ MakeMcwPeer ("test/Dup", "Test.First", "A"),
+ MakeMcwPeer ("test/Dup", "Test.Second", "A"),
+ };
+
+ var model = BuildModel (peers);
+ // Two entries: primary "test/Dup" and alias "test/Dup[1]"
+ Assert.Equal (2, model.Entries.Count);
+ Assert.Equal ("test/Dup", model.Entries [0].JniName);
+ Assert.Contains ("Test.First", model.Entries [0].ProxyTypeReference);
+ Assert.Equal ("test/Dup[1]", model.Entries [1].JniName);
+ Assert.Contains ("Test.Second", model.Entries [1].ProxyTypeReference);
+
+ // No associations when neither peer has a proxy (no activation ctor or invoker)
+ Assert.Empty (model.Associations);
+ }
+ }
+
+ public class ConditionalAttributes
+ {
+ [Theory]
+ [InlineData ("java/lang/Object")]
+ [InlineData ("java/lang/Throwable")]
+ [InlineData ("java/lang/Exception")]
+ [InlineData ("java/lang/RuntimeException")]
+ [InlineData ("java/lang/Error")]
+ [InlineData ("java/lang/Class")]
+ [InlineData ("java/lang/String")]
+ [InlineData ("java/lang/Thread")]
+ public void Build_AllEssentialRuntimeTypes_AreUnconditional (string jniName)
+ {
+ var peer = MakeMcwPeer (jniName, "Java.Lang.SomeType", "Mono.Android") with { DoNotGenerateAcw = true };
+ var model = BuildModel (new [] { peer });
+ Assert.True (model.Entries [0].IsUnconditional, $"{jniName} should be unconditional");
+ }
+
+ [Fact]
+ public void Build_UserAcwType_IsUnconditional ()
+ {
+ // User-defined ACW types (not MCW, not interface) are unconditional
+ // because Android can instantiate them from Java
+ var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App");
+ var model = BuildModel (new [] { peer });
+
+ var mainEntry = model.Entries.First (e => e.JniName == "my/app/Main");
+ Assert.True (mainEntry.IsUnconditional);
+ Assert.Null (mainEntry.TargetTypeReference);
+ }
+
+ [Fact]
+ public void Build_McwBinding_IsTrimmable ()
+ {
+ // MCW binding types (DoNotGenerateAcw=true) are trimmable unless essential
+ var peer = MakeMcwPeer ("android/app/Activity", "Android.App.Activity", "Mono.Android") with { DoNotGenerateAcw = true };
+ var model = BuildModel (new [] { peer });
+
+ Assert.Single (model.Entries);
+ Assert.False (model.Entries [0].IsUnconditional);
+ Assert.NotNull (model.Entries [0].TargetTypeReference);
+ Assert.Contains ("Android.App.Activity, Mono.Android", model.Entries [0].TargetTypeReference!);
+ }
+
+ [Fact]
+ public void Build_UnconditionalScannedType_IsUnconditional ()
+ {
+ // Types with IsUnconditional from scanner (e.g., from [Activity], [Service] attrs)
+ var peer = MakeMcwPeer ("my/app/MySvc", "MyApp.MyService", "App") with {
+ DoNotGenerateAcw = true, // simulate MCW-like
+ IsUnconditional = true, // scanner marked it
+ };
+ var model = BuildModel (new [] { peer });
+
+ Assert.True (model.Entries [0].IsUnconditional);
+ }
+ }
+
+ public class Aliases
+ {
+ [Fact]
+ public void Build_AliasedPeersWithActivation_GetDistinctProxies ()
+ {
+ var peers = new List {
+ MakePeerWithActivation ("test/Dup", "Test.First", "A"),
+ MakePeerWithActivation ("test/Dup", "Test.Second", "A"),
+ };
+
+ var model = BuildModel (peers, "TypeMap");
+ Assert.Equal (2, model.ProxyTypes.Count);
+ Assert.Equal ("Test_First_Proxy", model.ProxyTypes [0].TypeName);
+ Assert.Equal ("Test_Second_Proxy", model.ProxyTypes [1].TypeName);
+ }
+
+ [Fact]
+ public void Build_McwPeerWithoutActivation_NoProxy ()
+ {
+ var peer = MakeMcwPeer ("java/lang/Object", "Java.Lang.Object", "Mono.Android");
+ var model = BuildModel (new [] { peer });
+
+ Assert.Empty (model.ProxyTypes);
+ Assert.Single (model.Entries);
+ Assert.Contains ("Java.Lang.Object, Mono.Android", model.Entries [0].ProxyTypeReference);
+ }
+ }
+
+ public class ProxyTypes
+ {
+ [Theory]
+ [InlineData ("java/lang/Object", "Java.Lang.Object", "Mono.Android", "Java_Lang_Object_Proxy")]
+ [InlineData ("com/example/Outer$Inner", "Com.Example.Outer.Inner", "App", "Com_Example_Outer_Inner_Proxy")]
+ public void Build_PeerWithActivation_CreatesNamedProxy (string jniName, string managedName, string asmName, string expectedProxyName)
+ {
+ var peer = MakePeerWithActivation (jniName, managedName, asmName);
+ var model = BuildModel (new [] { peer }, "MyTypeMap");
+
+ Assert.Single (model.ProxyTypes);
+ var proxy = model.ProxyTypes [0];
+ Assert.Equal (expectedProxyName, proxy.TypeName);
+ Assert.Equal ("_TypeMap.Proxies", proxy.Namespace);
+ Assert.True (proxy.HasActivation);
+ Assert.Equal (managedName, proxy.TargetType.ManagedTypeName);
+ Assert.Equal (asmName, proxy.TargetType.AssemblyName);
+ }
+
+ [Fact]
+ public void Build_PeerWithInvoker_CreatesProxy ()
+ {
+ var peer = MakeInterfacePeer ("android/view/View$OnClickListener", "Android.Views.View+IOnClickListener", "Mono.Android", "Android.Views.View+IOnClickListenerInvoker");
+
+ var model = BuildModel (new [] { peer });
+ Assert.Single (model.ProxyTypes);
+ var proxy = model.ProxyTypes [0];
+ Assert.NotNull (proxy.InvokerType);
+ Assert.Equal ("Android.Views.View+IOnClickListenerInvoker", proxy.InvokerType!.ManagedTypeName);
+ }
+ }
+
+ public class FixtureScan
+ {
+ [Fact]
+ public void Build_FromScannedFixtures_ProducesValidModel ()
+ {
+ var peers = ScanFixtures ();
+ var model = BuildModel (peers, "TestTypeMap");
+
+ Assert.Equal ("TestTypeMap", model.AssemblyName);
+ Assert.NotEmpty (model.Entries);
+ Assert.NotEmpty (model.ProxyTypes);
+
+ Assert.All (model.Entries, e => Assert.False (string.IsNullOrEmpty (e.JniName)));
+ Assert.All (model.Entries, e => Assert.False (string.IsNullOrEmpty (e.ProxyTypeReference)));
+ }
+
+ [Theory]
+ [InlineData ("my/app/MainActivity", "MainActivity")]
+ [InlineData ("android/app/Activity", "Activity")]
+ [InlineData ("java/lang/Object", "Object")]
+ [InlineData ("my/app/Outer$Inner", "Inner")]
+ [InlineData ("my/app/ICallback$Result", "Result")]
+ public void ScanFixtures_ManagedTypeShortName_IsCorrect (string javaName, string expectedShortName)
+ {
+ var peer = FindFixtureByJavaName (javaName);
+ Assert.Equal (expectedShortName, peer.ManagedTypeShortName);
+ }
+ }
+
+ public class FixtureConditionalAttributes
+ {
+ [Theory]
+ [InlineData ("my/app/MainActivity")]
+ [InlineData ("my/app/TouchHandler")]
+ public void Fixture_UserAcwType_IsUnconditional (string javaName)
+ {
+ var peer = FindFixtureByJavaName (javaName);
+ Assert.False (peer.DoNotGenerateAcw);
+ var model = BuildModel (new [] { peer });
+ Assert.True (model.Entries [0].IsUnconditional);
+ }
+
+ [Theory]
+ [InlineData ("android/app/Activity")]
+ [InlineData ("android/widget/Button")]
+ public void Fixture_McwBinding_IsTrimmable (string javaName)
+ {
+ var peer = FindFixtureByJavaName (javaName);
+ Assert.True (peer.DoNotGenerateAcw);
+ var model = BuildModel (new [] { peer });
+ Assert.False (model.Entries [0].IsUnconditional);
+ }
+ }
+
+ static JavaPeerProxyData? FindProxy (TypeMapAssemblyData model, string proxyTypeName)
+ {
+ return model.ProxyTypes.FirstOrDefault (p => p.TypeName == proxyTypeName);
+ }
+
+ static TypeMapAttributeData? FindEntry (TypeMapAssemblyData model, string jniName)
+ {
+ return model.Entries.FirstOrDefault (e => e.JniName == jniName);
+ }
+
+ public class FixtureMcwTypes
+ {
+ [Theory]
+ [InlineData ("java/lang/Object", "Java_Lang_Object_Proxy", "Java.Lang.Object")]
+ [InlineData ("android/app/Activity", "Android_App_Activity_Proxy", "Android.App.Activity")]
+ [InlineData ("java/lang/Throwable", "Java_Lang_Throwable_Proxy", "Java.Lang.Throwable")]
+ [InlineData ("java/lang/Exception", "Java_Lang_Exception_Proxy", "Java.Lang.Exception")]
+ public void Fixture_McwType_HasActivation_CreatesProxy (string javaName, string expectedProxyName, string expectedManagedName)
+ {
+ var peer = FindFixtureByJavaName (javaName);
+ var model = BuildModel (new [] { peer }, "TypeMap");
+
+ var proxy = FindProxy (model, expectedProxyName);
+ Assert.NotNull (proxy);
+ Assert.True (proxy!.HasActivation);
+ Assert.Equal (expectedManagedName, proxy.TargetType.ManagedTypeName);
+ }
+
+ [Fact]
+ public void Fixture_Activity_Entry_PointsToProxy ()
+ {
+ var peer = FindFixtureByJavaName ("android/app/Activity");
+ var model = BuildModel (new [] { peer }, "MyTypeMap");
+
+ var entry = FindEntry (model, "android/app/Activity");
+ Assert.NotNull (entry);
+ Assert.Contains ("Android_App_Activity_Proxy", entry!.ProxyTypeReference);
+ Assert.Contains ("MyTypeMap", entry.ProxyTypeReference);
+ }
+
+ [Fact]
+ public void Fixture_Service_NoActivation_NoProxy ()
+ {
+ // Service in fixtures has no activation ctor on its own — it inherits from J.L.Object
+ // but Service itself has `protected Service(IntPtr, JniHandleOwnership)` which IS an activation ctor
+ var peer = FindFixtureByJavaName ("android/app/Service");
+ var model = BuildModel (new [] { peer }, "TypeMap");
+
+ if (peer.ActivationCtor != null) {
+ Assert.Single (model.ProxyTypes);
+ } else {
+ Assert.Empty (model.ProxyTypes);
+ }
+ }
+ }
+
+ public class FixtureCustomView
+ {
+ [Fact]
+ public void Fixture_CustomView_HasTwoConstructors ()
+ {
+ var peer = FindFixtureByJavaName ("my/app/CustomView");
+
+ var model = BuildModel (new [] { peer }, "TypeMap");
+ var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_CustomView_Proxy");
+ Assert.NotNull (proxy);
+ }
+ }
+
+ public class FixtureInterfaces
+ {
+ [Fact]
+ public void Fixture_IOnClickListener_HasInvokerProxy ()
+ {
+ var peers = ScanFixtures ();
+ var listener = peers.FirstOrDefault (p => p.ManagedTypeName == "Android.Views.IOnClickListener");
+ Assert.NotNull (listener);
+ Assert.True (listener!.IsInterface);
+ Assert.NotNull (listener.InvokerTypeName);
+
+ var model = BuildModel (new [] { listener }, "TypeMap");
+ var proxy = model.ProxyTypes.FirstOrDefault ();
+ Assert.NotNull (proxy);
+ Assert.NotNull (proxy!.InvokerType);
+ Assert.Equal ("Android.Views.IOnClickListenerInvoker", proxy.InvokerType!.ManagedTypeName);
+ }
+ }
+
+ public class FixtureNestedTypes
+ {
+ [Theory]
+ [InlineData ("my/app/Outer$Inner", "MyApp_Outer_Inner_Proxy", "MyApp.Outer+Inner")]
+ [InlineData ("my/app/ICallback$Result", "MyApp_ICallback_Result_Proxy", "MyApp.ICallback+Result")]
+ public void Fixture_NestedType_ProxyNaming (string javaName, string expectedProxyName, string expectedManagedName)
+ {
+ var peer = FindFixtureByJavaName (javaName);
+ var model = BuildModel (new [] { peer }, "TypeMap");
+
+ var entry = FindEntry (model, javaName);
+ Assert.NotNull (entry);
+
+ if (peer.ActivationCtor != null) {
+ var proxy = FindProxy (model, expectedProxyName);
+ Assert.NotNull (proxy);
+ Assert.Equal (expectedManagedName, proxy!.TargetType.ManagedTypeName);
+ }
+ }
+ }
+
+ public class FixtureInvokers
+ {
+ [Fact]
+ public void Fixture_InterfaceAndInvoker_ShareJniName_InvokerSeparated ()
+ {
+ var peers = ScanFixtures ();
+ // IOnClickListener and IOnClickListenerInvoker share "android/view/View$OnClickListener"
+ var clickPeers = peers.Where (p => p.JavaName == "android/view/View$OnClickListener").ToList ();
+ Assert.Equal (2, clickPeers.Count);
+
+ var model = BuildModel (clickPeers, "TypeMap");
+
+ // Invoker is excluded entirely — no TypeMap entry, no proxy.
+ // Only the interface gets a TypeMap entry and a proxy.
+ Assert.Single (model.Entries);
+ Assert.Equal ("android/view/View$OnClickListener", model.Entries [0].JniName);
+
+ // Only the interface proxy exists; the invoker type is referenced
+ // only as a TypeRef in the interface proxy's InvokerType property.
+ Assert.Single (model.ProxyTypes);
+ Assert.NotNull (model.ProxyTypes [0].InvokerType);
+ Assert.Equal ("Android.Views.IOnClickListenerInvoker", model.ProxyTypes [0].InvokerType!.ManagedTypeName);
+ }
+
+ [Fact]
+ public void Build_InvokerType_NoProxyNoEntry ()
+ {
+ // Invoker types should never get their own proxy or TypeMap entry.
+ // They only appear as a TypeRef in the interface proxy's InvokerType/CreateInstance.
+ var ifacePeer = MakeInterfacePeer ("my/app/IFoo", "MyApp.IFoo", "App", "MyApp.FooInvoker");
+ var invokerPeer = MakePeerWithActivation ("my/app/IFoo", "MyApp.FooInvoker", "App") with { DoNotGenerateAcw = true };
+
+ var model = BuildModel (new [] { ifacePeer, invokerPeer });
+
+ // Only the interface gets a TypeMap entry — its ProxyTypeReference points to the generated proxy
+ Assert.Single (model.Entries);
+ Assert.Contains ("MyApp_IFoo_Proxy", model.Entries [0].ProxyTypeReference);
+
+ // Only the interface gets a proxy — the invoker is referenced, not proxied
+ Assert.Single (model.ProxyTypes);
+ var proxy = model.ProxyTypes [0];
+ Assert.Equal ("MyApp.IFoo", proxy.TargetType.ManagedTypeName);
+ Assert.NotNull (proxy.InvokerType);
+ Assert.Equal ("MyApp.FooInvoker", proxy.InvokerType!.ManagedTypeName);
+
+ // Interface proxy has activation because it will create the invoker
+ Assert.True (proxy.HasActivation);
+ }
+ }
+
+ public class FixtureGenericHolder
+ {
+ [Fact]
+ public void Fixture_GenericHolder_Entry ()
+ {
+ var peer = FindFixtureByJavaName ("my/app/GenericHolder");
+ Assert.True (peer.IsGenericDefinition);
+
+ var model = BuildModel (new [] { peer }, "TypeMap");
+ var entry = FindEntry (model, "my/app/GenericHolder");
+ Assert.NotNull (entry);
+ }
+ }
+
+ public class FixtureAcwTypeHasProxy
+ {
+ [Theory]
+ [InlineData ("my/app/AbstractBase", "MyApp_AbstractBase_Proxy")]
+ [InlineData ("my/app/ClickableView", "MyApp_ClickableView_Proxy")]
+ [InlineData ("my/app/MultiInterfaceView", "MyApp_MultiInterfaceView_Proxy")]
+ [InlineData ("my/app/ExportExample", "MyApp_ExportExample_Proxy")]
+ public void Fixture_AcwType_HasProxy (string javaName, string expectedProxyName)
+ {
+ var peer = FindFixtureByJavaName (javaName);
+ Assert.False (peer.DoNotGenerateAcw);
+
+ var model = BuildModel (new [] { peer }, "TypeMap");
+
+ if (peer.ActivationCtor != null) {
+ var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == expectedProxyName);
+ Assert.NotNull (proxy);
+ }
+ }
+ }
+
+ public class FixtureImplementorsAndDispatchers
+ {
+ [Theory]
+ [InlineData ("mono/android/view/View_IOnClickListenerImplementor", "Implementor")]
+ [InlineData ("mono/android/view/View_ClickEventDispatcher", "EventDispatcher")]
+ public void Fixture_HelperType_IsUnconditional (string javaName, string kind)
+ {
+ var peer = FindFixtureByJavaName (javaName);
+ Assert.False (peer.DoNotGenerateAcw);
+ Assert.False (peer.IsInterface);
+
+ var model = BuildModel (new [] { peer }, "TypeMap");
+
+ var entry = model.Entries.FirstOrDefault ();
+ Assert.NotNull (entry);
+ // Implementor/EventDispatcher types are treated as unconditional ACW types.
+ // Future optimization (see issue tracking Implementor trimming) may make them trimmable.
+ Assert.True (entry!.IsUnconditional, $"{kind} should be unconditional");
+ }
+ }
+
+ public class InvokerDetection
+ {
+ [Fact]
+ public void Build_TypeIsInvoker_OnlyWhenReferencedByAnotherPeer ()
+ {
+ // A type is only treated as an invoker when another peer's InvokerTypeName references it.
+ // A type named "MyInvoker" with DoNotGenerateAcw is NOT automatically an invoker.
+ var invokerPeer = MakePeerWithActivation ("my/app/MyInvoker", "MyApp.MyInvoker", "App") with { DoNotGenerateAcw = true };
+
+ // Without a referencing peer, it gets a normal entry
+ var model1 = BuildModel (new [] { invokerPeer });
+ Assert.Single (model1.Entries);
+
+ // When an interface references it as invoker, it is excluded
+ var ifacePeer = MakeInterfacePeer ("my/app/MyInvoker", "MyApp.IMyInterface", "App", "MyApp.MyInvoker");
+ var model2 = BuildModel (new [] { ifacePeer, invokerPeer });
+ // Only the interface gets entries/proxies, the invoker is excluded
+ Assert.Single (model2.Entries);
+ Assert.Equal ("MyApp.IMyInterface", model2.ProxyTypes [0].TargetType.ManagedTypeName);
+ }
+ }
+
+ public class PipelineTests
+ {
+ [Fact]
+ public void FullPipeline_AllFixtures_ProducesLoadableAssembly ()
+ {
+ var peers = ScanFixtures ();
+ var model = BuildModel (peers, "FullPipeline");
+
+ EmitAndVerify (model, "FullPipeline", (pe, reader) => {
+ Assert.True (pe.HasMetadata);
+
+ var asmDef = reader.GetAssemblyDefinition ();
+ Assert.Equal ("FullPipeline", reader.GetString (asmDef.Name));
+
+ var proxyTypes = reader.TypeDefinitions
+ .Select (h => reader.GetTypeDefinition (h))
+ .Where (t => reader.GetString (t.Namespace) == "_TypeMap.Proxies")
+ .ToList ();
+ Assert.Equal (model.ProxyTypes.Count, proxyTypes.Count);
+
+ var proxyNames = proxyTypes.Select (t => reader.GetString (t.Name)).OrderBy (n => n).ToList ();
+ var modelNames = model.ProxyTypes.Select (p => p.TypeName).OrderBy (n => n).ToList ();
+ Assert.Equal (modelNames, proxyNames);
+ });
+ }
+
+ [Fact]
+ public void FullPipeline_AllFixtures_TypeMapAttributeCountMatchesEntries ()
+ {
+ var peers = ScanFixtures ();
+ var model = BuildModel (peers, "AttrCount");
+
+ EmitAndVerify (model, "AttrCount", (pe, reader) => {
+ var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition);
+ int totalAttrs = asmAttrs.Count ();
+
+ int expected = model.Entries.Count + model.Associations.Count + model.IgnoresAccessChecksTo.Count;
+ Assert.Equal (expected, totalAttrs);
+ });
+ }
+
+ [Fact]
+ public void FullPipeline_AliasGroup_TypeMapAttributeCountIncludesAssociations ()
+ {
+ // Two peers with the same JNI name, both with activation → generates an association
+ var peers = new List {
+ MakePeerWithActivation ("test/Alias", "Test.Primary", "Asm"),
+ MakePeerWithActivation ("test/Alias", "Test.Secondary", "Asm"),
+ };
+ var model = BuildModel (peers, "AliasAttrCount");
+ Assert.NotEmpty (model.Associations);
+
+ EmitAndVerify (model, "AliasAttrCount", (pe, reader) => {
+ var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition);
+ int totalAttrs = asmAttrs.Count ();
+ int expected = model.Entries.Count + model.Associations.Count + model.IgnoresAccessChecksTo.Count;
+ Assert.Equal (expected, totalAttrs);
+ });
+ }
+
+ [Fact]
+ public void FullPipeline_CustomView_HasConstructorAndMethodWrappers ()
+ {
+ var peer = FindFixtureByJavaName ("my/app/CustomView");
+ var model = BuildModel (new [] { peer }, "CtorTest");
+
+ EmitAndVerify (model, "CtorTest", (pe, reader) => {
+ var proxy = reader.TypeDefinitions
+ .Select (h => reader.GetTypeDefinition (h))
+ .First (t => reader.GetString (t.Name) == "MyApp_CustomView_Proxy");
+
+ var methodNames = proxy.GetMethods ()
+ .Select (h => reader.GetString (reader.GetMethodDefinition (h).Name))
+ .ToList ();
+
+ Assert.Contains (".ctor", methodNames);
+ Assert.Contains ("CreateInstance", methodNames);
+ Assert.Contains ("get_TargetType", methodNames);
+ });
+ }
+
+ [Fact]
+ public void FullPipeline_GenericHolder_ProducesValidAssembly ()
+ {
+ var peer = FindFixtureByJavaName ("my/app/GenericHolder");
+ var model = BuildModel (new [] { peer }, "GenericTest");
+
+ EmitAndVerify (model, "GenericTest", (pe, reader) => {
+ Assert.True (pe.HasMetadata);
+ var entry = FindEntry (model, "my/app/GenericHolder");
+ Assert.NotNull (entry);
+
+ var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition);
+ Assert.NotEmpty (asmAttrs);
+ });
+ }
+ }
+
+ public class PeBlobValidation
+ {
+ [Fact]
+ public void FullPipeline_Mixed2ArgAnd3Arg_BothSurviveRoundTrip ()
+ {
+ // java/lang/Object → essential → 2-arg unconditional
+ var objectPeer = FindFixtureByJavaName ("java/lang/Object");
+ // android/app/Activity → MCW → 3-arg trimmable
+ var activityPeer = FindFixtureByJavaName ("android/app/Activity");
+
+ var model = BuildModel (new [] { objectPeer, activityPeer }, "MixedBlob");
+ Assert.Equal (2, model.Entries.Count);
+
+ EmitAndVerify (model, "MixedBlob", (pe, reader) => {
+ var attrs = ReadAllTypeMapAttributeBlobs (reader);
+ Assert.Equal (2, attrs.Count);
+
+ var unconditional = attrs.FirstOrDefault (a => a.jniName == "java/lang/Object");
+ Assert.NotNull (unconditional.jniName);
+ Assert.Null (unconditional.targetRef);
+
+ var trimmable = attrs.FirstOrDefault (a => a.jniName == "android/app/Activity");
+ Assert.NotNull (trimmable.jniName);
+ Assert.NotNull (trimmable.targetRef);
+ Assert.Contains ("Android.App.Activity", trimmable.targetRef!);
+ });
+ }
+
+ [Theory]
+ [InlineData ("java/lang/Object", "Blob2Arg", "Java_Lang_Object_Proxy")]
+ [InlineData ("my/app/MainActivity", "BlobAcw", "MyApp_MainActivity_Proxy")]
+ public void FullPipeline_UnconditionalType_Emits2ArgAttribute (string javaName, string assemblyName, string expectedProxyName)
+ {
+ var peer = FindFixtureByJavaName (javaName);
+ var model = BuildModel (new [] { peer }, assemblyName);
+ Assert.Single (model.Entries);
+ Assert.True (model.Entries [0].IsUnconditional);
+
+ EmitAndVerify (model, assemblyName, (pe, reader) => {
+ var (jniName2, proxyRef, targetRef) = ReadFirstTypeMapAttributeBlob (reader);
+
+ Assert.Equal (javaName, jniName2);
+ Assert.NotNull (proxyRef);
+ Assert.Contains (expectedProxyName, proxyRef!);
+ Assert.Null (targetRef);
+ });
+ }
+
+ [Fact]
+ public void FullPipeline_McwBinding_Emits3ArgAttribute ()
+ {
+ // android/app/Activity is MCW → trimmable 3-arg attribute
+ var peer = FindFixtureByJavaName ("android/app/Activity");
+ var model = BuildModel (new [] { peer }, "Blob3Arg");
+ Assert.Single (model.Entries);
+ Assert.False (model.Entries [0].IsUnconditional);
+
+ EmitAndVerify (model, "Blob3Arg", (pe, reader) => {
+ var (jniName, proxyRef, targetRef) = ReadFirstTypeMapAttributeBlob (reader);
+
+ Assert.Equal ("android/app/Activity", jniName);
+ Assert.NotNull (proxyRef);
+ Assert.Contains ("Android_App_Activity_Proxy", proxyRef!);
+ Assert.NotNull (targetRef);
+ Assert.Contains ("Android.App.Activity", targetRef!);
+ });
+ }
+ }
+
+ public class DeterminismTests
+ {
+ [Fact]
+ public void Build_SameInput_ProducesDeterministicOutput ()
+ {
+ var peers = ScanFixtures ();
+
+ var model1 = BuildModel (peers, "DetTest");
+ var model2 = BuildModel (peers, "DetTest");
+
+ Assert.Equal (model1.Entries.Count, model2.Entries.Count);
+ for (int i = 0; i < model1.Entries.Count; i++) {
+ Assert.Equal (model1.Entries [i].JniName, model2.Entries [i].JniName);
+ Assert.Equal (model1.Entries [i].ProxyTypeReference, model2.Entries [i].ProxyTypeReference);
+ Assert.Equal (model1.Entries [i].TargetTypeReference, model2.Entries [i].TargetTypeReference);
+ }
+ }
+ }
+
+ static void EmitAndVerify (TypeMapAssemblyData model, string assemblyName, Action verify)
+ {
+ var stream = new MemoryStream ();
+ var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0));
+ emitter.Emit (model, stream);
+ stream.Position = 0;
+ using var pe = new PEReader (stream);
+ verify (pe, pe.GetMetadataReader ());
+ }
+
+ ///
+ /// Reads the first TypeMap assembly-level attribute blob and returns (jniName, proxyRef, targetRef).
+ /// targetRef is null for 2-arg attributes.
+ ///
+ static (string? jniName, string? proxyRef, string? targetRef) ReadFirstTypeMapAttributeBlob (MetadataReader reader)
+ {
+ var all = ReadAllTypeMapAttributeBlobs (reader);
+ if (all.Count == 0) {
+ throw new InvalidOperationException ("No TypeMap attribute found on assembly");
+ }
+ return all [0];
+ }
+
+ ///
+ /// Reads TypeMap attribute blobs from a PE assembly's metadata.
+ ///
+ /// NOTE: This is a PE-level integration test helper, not a primary unit test mechanism.
+ /// The model-level tests (which verify TypeMapAssemblyData directly) are the main unit tests.
+ /// These PE round-trip tests exist to catch encoding bugs in the emitter and to verify that
+ /// the full scan→model→emit pipeline produces a valid, loadable assembly.
+ ///
+ /// The distinction between TypeMap and IgnoresAccessChecksTo attributes relies on
+ /// attr.Constructor.Kind: TypeMap attributes reference their ctor via MemberReference
+ /// (because the attribute type is a TypeSpec — generic), while IgnoresAccessChecksTo
+ /// uses MethodDefinition (the attribute type is defined in the same assembly as a TypeDef).
+ /// If this logic breaks, the test will either fail to find TypeMap attributes or
+ /// misidentify IgnoresAccessChecksTo as TypeMap — both cause obvious assertion failures.
+ ///
+ static List<(string? jniName, string? proxyRef, string? targetRef)> ReadAllTypeMapAttributeBlobs (MetadataReader reader)
+ {
+ var result = new List<(string?, string?, string?)> ();
+ var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition);
+ foreach (var attrHandle in asmAttrs) {
+ var attr = reader.GetCustomAttribute (attrHandle);
+ // Skip IgnoresAccessChecksTo attributes (their ctor is a MethodDefinition, not MemberRef)
+ if (attr.Constructor.Kind == HandleKind.MethodDefinition)
+ continue;
+
+ var blobReader = reader.GetBlobReader (attr.Value);
+ ushort prolog = blobReader.ReadUInt16 ();
+ if (prolog != 1)
+ continue;
+
+ string? jniName = blobReader.ReadSerializedString ();
+ string? proxyRef = blobReader.ReadSerializedString ();
+
+ // Try to read third arg (target type) — if remaining bytes are just NumNamed (2 bytes), it's 2-arg
+ string? targetRef = null;
+ if (blobReader.RemainingBytes > 2) {
+ targetRef = blobReader.ReadSerializedString ();
+ }
+
+ result.Add ((jniName, proxyRef, targetRef));
+ }
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs
index 81aabe9ad74..3ed0c175cf6 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs
@@ -17,8 +17,7 @@ public partial class JavaPeerScannerTests
[InlineData ("my/app/TouchHandler", "SetItems", "setItems", "([Ljava/lang/String;)V")]
public void Scan_MarshalMethod_HasCorrectSignature (string javaName, string managedName, string jniName, string jniSig)
{
- var peers = ScanFixtures ();
- var method = FindByJavaName (peers, javaName)
+ var method = FindFixtureByJavaName (javaName)
.MarshalMethods.FirstOrDefault (m => m.ManagedMethodName == managedName || m.JniName == jniName);
Assert.NotNull (method);
Assert.Equal (jniName, method.JniName);
@@ -28,32 +27,30 @@ public void Scan_MarshalMethod_HasCorrectSignature (string javaName, string mana
[Fact]
public void Scan_MarshalMethod_ConstructorsAndSpecialCases ()
{
- var peers = ScanFixtures ();
-
- var ctors = FindByJavaName (peers, "my/app/CustomView")
+ var ctors = FindFixtureByJavaName ("my/app/CustomView")
.MarshalMethods.Where (m => m.IsConstructor).ToList ();
Assert.Equal (2, ctors.Count);
Assert.Equal ("()V", ctors [0].JniSignature);
Assert.Equal ("(Landroid/content/Context;)V", ctors [1].JniSignature);
- Assert.DoesNotContain (FindByJavaName (peers, "my/app/MyHelper").MarshalMethods, m => m.IsConstructor);
+ Assert.DoesNotContain (FindFixtureByJavaName ("my/app/MyHelper").MarshalMethods, m => m.IsConstructor);
- var exportMethod = FindByJavaName (peers, "my/app/ExportExample").MarshalMethods.Single ();
+ var exportMethod = FindFixtureByJavaName ("my/app/ExportExample").MarshalMethods.Single ();
Assert.Equal ("myExportedMethod", exportMethod.JniName);
Assert.Null (exportMethod.Connector);
- var onStart = FindByJavaName (peers, "android/app/Activity")
+ var onStart = FindFixtureByJavaName ("android/app/Activity")
.MarshalMethods.FirstOrDefault (m => m.JniName == "onStart");
Assert.NotNull (onStart);
Assert.Equal ("", onStart.Connector);
- var onClick = FindByManagedName (peers, "Android.Views.IOnClickListener")
+ var onClick = FindFixtureByManagedName ("Android.Views.IOnClickListener")
.MarshalMethods.FirstOrDefault (m => m.JniName == "onClick");
Assert.NotNull (onClick);
Assert.Equal ("(Landroid/view/View;)V", onClick.JniSignature);
Assert.Equal ("Android.Views.IOnClickListenerInvoker",
- FindByManagedName (peers, "Android.Views.IOnClickListener").InvokerTypeName);
+ FindFixtureByManagedName ("Android.Views.IOnClickListener").InvokerTypeName);
}
[Theory]
@@ -62,8 +59,7 @@ public void Scan_MarshalMethod_ConstructorsAndSpecialCases ()
[InlineData ("my/app/MyButton", "MyApp.MyButton")]
public void Scan_ActivationCtor_InheritsFromNearestBase (string javaName, string expectedDeclaringType)
{
- var peers = ScanFixtures ();
- var peer = FindByJavaName (peers, javaName);
+ var peer = FindFixtureByJavaName (javaName);
Assert.NotNull (peer.ActivationCtor);
Assert.Equal (expectedDeclaringType, peer.ActivationCtor.DeclaringTypeName);
}
@@ -77,23 +73,20 @@ public void Scan_ActivationCtor_InheritsFromNearestBase (string javaName, string
[InlineData ("my/app/MyButton", "android/widget/Button")]
public void Scan_BaseJavaName_ResolvesCorrectly (string javaName, string? expectedBase)
{
- var peers = ScanFixtures ();
- Assert.Equal (expectedBase, FindByJavaName (peers, javaName).BaseJavaName);
+ Assert.Equal (expectedBase, FindFixtureByJavaName (javaName).BaseJavaName);
}
[Fact]
public void Scan_MultipleInterfaces_AllResolved ()
{
- var peers = ScanFixtures ();
-
- var multi = FindByJavaName (peers, "my/app/MultiInterfaceView");
+ var multi = FindFixtureByJavaName ("my/app/MultiInterfaceView");
Assert.Contains ("android/view/View$OnClickListener", multi.ImplementedInterfaceJavaNames);
Assert.Contains ("android/view/View$OnLongClickListener", multi.ImplementedInterfaceJavaNames);
Assert.Equal (2, multi.ImplementedInterfaceJavaNames.Count);
Assert.Contains ("android/view/View$OnClickListener",
- FindByJavaName (peers, "my/app/ClickableView").ImplementedInterfaceJavaNames);
- Assert.Empty (FindByJavaName (peers, "my/app/MyHelper").ImplementedInterfaceJavaNames);
+ FindFixtureByJavaName ("my/app/ClickableView").ImplementedInterfaceJavaNames);
+ Assert.Empty (FindFixtureByJavaName ("my/app/MyHelper").ImplementedInterfaceJavaNames);
}
[Theory]
@@ -101,15 +94,13 @@ public void Scan_MultipleInterfaces_AllResolved ()
[InlineData ("my/app/MainActivity", "my/app/MainActivity")]
public void Scan_CompatJniName (string javaName, string expectedCompat)
{
- var peers = ScanFixtures ();
- Assert.Equal (expectedCompat, FindByJavaName (peers, javaName).CompatJniName);
+ Assert.Equal (expectedCompat, FindFixtureByJavaName (javaName).CompatJniName);
}
[Fact]
public void Scan_CompatJniName_UnregisteredType_UsesRawNamespace ()
{
- var peers = ScanFixtures ();
- var unregistered = FindByManagedName (peers, "MyApp.UnregisteredHelper");
+ var unregistered = FindFixtureByManagedName ("MyApp.UnregisteredHelper");
Assert.StartsWith ("crc64", unregistered.JavaName);
Assert.Equal ("myapp/UnregisteredHelper", unregistered.CompatJniName);
}
@@ -117,9 +108,8 @@ public void Scan_CompatJniName_UnregisteredType_UsesRawNamespace ()
[Fact]
public void Scan_CustomJniNameProviderAttribute_UsesNameFromAttribute ()
{
- var peers = ScanFixtures ();
Assert.Equal ("com/example/CustomWidget",
- FindByManagedName (peers, "MyApp.CustomWidget").JavaName);
+ FindFixtureByManagedName ("MyApp.CustomWidget").JavaName);
}
[Theory]
@@ -127,7 +117,6 @@ public void Scan_CustomJniNameProviderAttribute_UsesNameFromAttribute ()
[InlineData ("my/app/ICallback$Result", "MyApp.ICallback+Result")]
public void Scan_NestedType_IsDiscovered (string javaName, string managedName)
{
- var peers = ScanFixtures ();
- Assert.Equal (managedName, FindByJavaName (peers, javaName).ManagedTypeName);
+ Assert.Equal (managedName, FindFixtureByJavaName (javaName).ManagedTypeName);
}
}
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs
index ec368ed6a44..b1f96d6d320 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs
@@ -8,23 +8,20 @@ public partial class JavaPeerScannerTests
[Fact]
public void Scan_GenericTypes_ResolveViaTypeSpecification ()
{
- var peers = ScanFixtures ();
Assert.Equal ("my/app/GenericBase",
- FindByJavaName (peers, "my/app/ConcreteFromGeneric").BaseJavaName);
+ FindFixtureByJavaName ("my/app/ConcreteFromGeneric").BaseJavaName);
Assert.Contains ("my/app/IGenericCallback",
- FindByJavaName (peers, "my/app/GenericCallbackImpl").ImplementedInterfaceJavaNames);
+ FindFixtureByJavaName ("my/app/GenericCallbackImpl").ImplementedInterfaceJavaNames);
}
[Fact]
public void Scan_ComponentOnlyBase_BothBaseAndDerivedDiscovered ()
{
- var peers = ScanFixtures ();
-
- var baseType = FindByJavaName (peers, "my/app/BaseActivityNoRegister");
+ var baseType = FindFixtureByJavaName ("my/app/BaseActivityNoRegister");
Assert.True (baseType.IsUnconditional);
Assert.Equal ("android/app/Activity", baseType.BaseJavaName);
- var derived = FindByManagedName (peers, "MyApp.DerivedFromComponentBase");
+ var derived = FindFixtureByManagedName ("MyApp.DerivedFromComponentBase");
Assert.StartsWith ("crc64", derived.JavaName);
}
@@ -33,17 +30,23 @@ public void Scan_ComponentOnlyBase_BothBaseAndDerivedDiscovered ()
[InlineData ("MyApp.DeepOuter+Middle+DeepInner", "my/app/DeepOuter_Middle_DeepInner")]
public void Scan_UnregisteredNestedType_UsesParentJniPrefix (string managedName, string expectedJavaName)
{
- var peers = ScanFixtures ();
- Assert.Equal (expectedJavaName, FindByManagedName (peers, managedName).JavaName);
+ Assert.Equal (expectedJavaName, FindFixtureByManagedName (managedName).JavaName);
+ }
+
+ [Theory]
+ [InlineData ("MyApp.RegisteredParent+UnregisteredChild", "MyApp")]
+ [InlineData ("MyApp.DeepOuter+Middle+DeepInner", "MyApp")]
+ public void Scan_NestedType_HasCorrectNamespace (string managedName, string expectedNamespace)
+ {
+ Assert.Equal (expectedNamespace, FindFixtureByManagedName (managedName).ManagedTypeNamespace);
}
[Fact]
public void Scan_EmptyNamespace_Handled ()
{
- var peers = ScanFixtures ();
- Assert.Equal ("GlobalType", FindByJavaName (peers, "my/app/GlobalType").ManagedTypeName);
+ Assert.Equal ("GlobalType", FindFixtureByJavaName ("my/app/GlobalType").ManagedTypeName);
Assert.Equal ("GlobalUnregisteredType",
- FindByManagedName (peers, "GlobalUnregisteredType").CompatJniName);
+ FindFixtureByManagedName ("GlobalUnregisteredType").CompatJniName);
}
[Theory]
@@ -53,15 +56,13 @@ public void Scan_EmptyNamespace_Handled ()
[InlineData ("MyApp.UnregisteredExporter")]
public void Scan_UnregisteredType_DiscoveredWithCrc64Name (string managedName)
{
- var peers = ScanFixtures ();
- Assert.StartsWith ("crc64", FindByManagedName (peers, managedName).JavaName);
+ Assert.StartsWith ("crc64", FindFixtureByManagedName (managedName).JavaName);
}
[Fact]
public void Scan_ExportOnUnregisteredType_MethodDiscovered ()
{
- var peers = ScanFixtures ();
- var exportMethod = FindByManagedName (peers, "MyApp.UnregisteredExporter")
+ var exportMethod = FindFixtureByManagedName ("MyApp.UnregisteredExporter")
.MarshalMethods.FirstOrDefault (m => m.JniName == "doExportedWork");
Assert.NotNull (exportMethod);
Assert.Null (exportMethod.Connector);
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs
index bc2b6195f22..555bba3451e 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs
@@ -6,38 +6,8 @@
namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests;
-public partial class JavaPeerScannerTests
+public partial class JavaPeerScannerTests : FixtureTestBase
{
- static string TestFixtureAssemblyPath {
- get {
- var testAssemblyDir = Path.GetDirectoryName (typeof (JavaPeerScannerTests).Assembly.Location)!;
- var fixtureAssembly = Path.Combine (testAssemblyDir, "TestFixtures.dll");
- Assert.True (File.Exists (fixtureAssembly),
- $"TestFixtures.dll not found at {fixtureAssembly}. Ensure the TestFixtures project builds.");
- return fixtureAssembly;
- }
- }
-
- List ScanFixtures ()
- {
- using var scanner = new JavaPeerScanner ();
- return scanner.Scan (new [] { TestFixtureAssemblyPath });
- }
-
- JavaPeerInfo FindByJavaName (List peers, string javaName)
- {
- var peer = peers.FirstOrDefault (p => p.JavaName == javaName);
- Assert.NotNull (peer);
- return peer;
- }
-
- JavaPeerInfo FindByManagedName (List peers, string managedName)
- {
- var peer = peers.FirstOrDefault (p => p.ManagedTypeName == managedName);
- Assert.NotNull (peer);
- return peer;
- }
-
[Fact]
public void Scan_FindsAllJavaPeerTypes ()
{
@@ -54,8 +24,7 @@ public void Scan_FindsAllJavaPeerTypes ()
[InlineData ("my/app/MainActivity", false)]
public void Scan_DoNotGenerateAcw (string javaName, bool expected)
{
- var peers = ScanFixtures ();
- Assert.Equal (expected, FindByJavaName (peers, javaName).DoNotGenerateAcw);
+ Assert.Equal (expected, FindFixtureByJavaName (javaName).DoNotGenerateAcw);
}
[Theory]
@@ -71,19 +40,17 @@ public void Scan_DoNotGenerateAcw (string javaName, bool expected)
[InlineData ("android/app/Activity", false)]
public void Scan_IsUnconditional (string javaName, bool expected)
{
- var peers = ScanFixtures ();
- Assert.Equal (expected, FindByJavaName (peers, javaName).IsUnconditional);
+ Assert.Equal (expected, FindFixtureByJavaName (javaName).IsUnconditional);
}
[Fact]
public void Scan_TypeMetadata_IsCorrect ()
{
- var peers = ScanFixtures ();
- Assert.True (FindByJavaName (peers, "my/app/AbstractBase").IsAbstract);
- Assert.True (FindByManagedName (peers, "Android.Views.IOnClickListener").IsInterface);
- Assert.False (FindByManagedName (peers, "Android.Views.IOnClickListener").DoNotGenerateAcw);
+ Assert.True (FindFixtureByJavaName ("my/app/AbstractBase").IsAbstract);
+ Assert.True (FindFixtureByManagedName ("Android.Views.IOnClickListener").IsInterface);
+ Assert.False (FindFixtureByManagedName ("Android.Views.IOnClickListener").DoNotGenerateAcw);
- var generic = FindByJavaName (peers, "my/app/GenericHolder");
+ var generic = FindFixtureByJavaName ("my/app/GenericHolder");
Assert.True (generic.IsGenericDefinition);
Assert.Equal ("MyApp.Generic.GenericHolder`1", generic.ManagedTypeName);
}
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs
index 36c7587eb28..4a9ebb1d079 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs
@@ -40,6 +40,21 @@ public enum JniHandleOwnership
}
}
+namespace Java.Interop
+{
+ public struct JniObjectReference
+ {
+ public IntPtr Handle;
+ }
+
+ public enum JniObjectReferenceOptions
+ {
+ None = 0,
+ Copy = 1,
+ CopyAndDispose = 2,
+ }
+}
+
namespace Android.App
{
[AttributeUsage (AttributeTargets.Class)]
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs
index 35987f36f93..d516be0de5b 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs
@@ -96,6 +96,18 @@ public interface IOnLongClickListener
[Register ("onLongClick", "(Landroid/view/View;)Z", "GetOnLongClick_Landroid_view_View_Handler:Android.Views.IOnLongClickListenerInvoker")]
bool OnLongClick (View v);
}
+
+ [Register ("mono/android/view/View_IOnClickListenerImplementor")]
+ public class View_IOnClickListenerImplementor : Java.Lang.Object
+ {
+ public View_IOnClickListenerImplementor (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { }
+ }
+
+ [Register ("mono/android/view/View_ClickEventDispatcher")]
+ public class View_ClickEventDispatcher : Java.Lang.Object
+ {
+ public View_ClickEventDispatcher (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { }
+ }
}
namespace Android.Widget
@@ -239,7 +251,6 @@ public class MyManageSpaceActivity : Android.App.Activity
{
protected MyManageSpaceActivity (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { }
}
-
public class UnregisteredHelper : Java.Lang.Object { }
[Register ("my/app/MyButton")]
@@ -265,7 +276,6 @@ public class CustomWidget : Java.Lang.Object { }
[Activity (Name = "my.app.BaseActivityNoRegister")]
public class BaseActivityNoRegister : Android.App.Activity { }
-
public class DerivedFromComponentBase : BaseActivityNoRegister { }
[Register ("my/app/RegisteredParent")]
@@ -282,7 +292,6 @@ public class Middle : Java.Lang.Object
public class DeepInner : Java.Lang.Object { }
}
}
-
public class PlainActivitySubclass : Android.App.Activity { }
[Activity (Label = "Unnamed")]
@@ -330,6 +339,13 @@ public class GenericCallbackImpl : Java.Lang.Object, IGenericCallback
{
protected GenericCallbackImpl (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { }
}
+
+ [Register ("my/app/JiStylePeer", DoNotGenerateAcw = true)]
+ public class JiStylePeer : Java.Lang.Object
+ {
+ protected JiStylePeer (ref Java.Interop.JniObjectReference reference, Java.Interop.JniObjectReferenceOptions options)
+ : base ((IntPtr)0, JniHandleOwnership.DoNotTransfer) { }
+ }
}
[Register ("my/app/GlobalType")]
@@ -337,5 +353,4 @@ public class GlobalType : Java.Lang.Object
{
protected GlobalType (IntPtr handle, Android.Runtime.JniHandleOwnership transfer) : base (handle, transfer) { }
}
-
public class GlobalUnregisteredType : Java.Lang.Object { }