diff --git a/Xamarin.Android.sln b/Xamarin.Android.sln
index d1edbe95c97..48ee13d6a66 100644
--- a/Xamarin.Android.sln
+++ b/Xamarin.Android.sln
@@ -59,6 +59,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Xamarin.ProjectTools", "src
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Xamarin.Android.Build.Tests", "src\Xamarin.Android.Build.Tasks\Tests\Xamarin.Android.Build.Tests\Xamarin.Android.Build.Tests.csproj", "{53E4ABF0-1085-45F9-B964-DCAE4B819998}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Android.Sdk.TrimmableTypeMap", "src\Microsoft.Android.Sdk.TrimmableTypeMap\Microsoft.Android.Sdk.TrimmableTypeMap.csproj", "{507759AE-93DF-411B-8645-31F680319F5C}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Android.Sdk.TrimmableTypeMap.Tests", "tests\Microsoft.Android.Sdk.TrimmableTypeMap.Tests\Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj", "{F9CD012E-67AC-4A4E-B2A7-252387F91256}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestFixtures", "tests\Microsoft.Android.Sdk.TrimmableTypeMap.Tests\TestFixtures\TestFixtures.csproj", "{C5A44686-3469-45A7-B6AB-2798BA0625BC}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests", "tests\Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests\Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests.csproj", "{A14CB0A1-7A05-4F27-88B2-383798CE1DEE}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UserTypesFixture", "tests\Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests\UserTypesFixture\UserTypesFixture.csproj", "{2498F8A0-AA04-40EF-8691-59BBD2396B4D}"
+EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "class-parse", "external\Java.Interop\tools\class-parse\class-parse.csproj", "{38C762AB-8FD1-44DE-9855-26AAE7129DC3}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "logcat-parse", "external\Java.Interop\tools\logcat-parse\logcat-parse.csproj", "{7387E151-48E3-4885-B2CA-A74434A34045}"
@@ -231,6 +241,26 @@ Global
{53E4ABF0-1085-45F9-B964-DCAE4B819998}.Debug|AnyCPU.Build.0 = Debug|Any CPU
{53E4ABF0-1085-45F9-B964-DCAE4B819998}.Release|AnyCPU.ActiveCfg = Release|Any CPU
{53E4ABF0-1085-45F9-B964-DCAE4B819998}.Release|AnyCPU.Build.0 = Release|Any CPU
+ {507759AE-93DF-411B-8645-31F680319F5C}.Debug|AnyCPU.ActiveCfg = Debug|Any CPU
+ {507759AE-93DF-411B-8645-31F680319F5C}.Debug|AnyCPU.Build.0 = Debug|Any CPU
+ {507759AE-93DF-411B-8645-31F680319F5C}.Release|AnyCPU.ActiveCfg = Release|Any CPU
+ {507759AE-93DF-411B-8645-31F680319F5C}.Release|AnyCPU.Build.0 = Release|Any CPU
+ {F9CD012E-67AC-4A4E-B2A7-252387F91256}.Debug|AnyCPU.ActiveCfg = Debug|Any CPU
+ {F9CD012E-67AC-4A4E-B2A7-252387F91256}.Debug|AnyCPU.Build.0 = Debug|Any CPU
+ {F9CD012E-67AC-4A4E-B2A7-252387F91256}.Release|AnyCPU.ActiveCfg = Release|Any CPU
+ {F9CD012E-67AC-4A4E-B2A7-252387F91256}.Release|AnyCPU.Build.0 = Release|Any CPU
+ {C5A44686-3469-45A7-B6AB-2798BA0625BC}.Debug|AnyCPU.ActiveCfg = Debug|Any CPU
+ {C5A44686-3469-45A7-B6AB-2798BA0625BC}.Debug|AnyCPU.Build.0 = Debug|Any CPU
+ {C5A44686-3469-45A7-B6AB-2798BA0625BC}.Release|AnyCPU.ActiveCfg = Release|Any CPU
+ {C5A44686-3469-45A7-B6AB-2798BA0625BC}.Release|AnyCPU.Build.0 = Release|Any CPU
+ {A14CB0A1-7A05-4F27-88B2-383798CE1DEE}.Debug|AnyCPU.ActiveCfg = Debug|Any CPU
+ {A14CB0A1-7A05-4F27-88B2-383798CE1DEE}.Debug|AnyCPU.Build.0 = Debug|Any CPU
+ {A14CB0A1-7A05-4F27-88B2-383798CE1DEE}.Release|AnyCPU.ActiveCfg = Release|Any CPU
+ {A14CB0A1-7A05-4F27-88B2-383798CE1DEE}.Release|AnyCPU.Build.0 = Release|Any CPU
+ {2498F8A0-AA04-40EF-8691-59BBD2396B4D}.Debug|AnyCPU.ActiveCfg = Debug|Any CPU
+ {2498F8A0-AA04-40EF-8691-59BBD2396B4D}.Debug|AnyCPU.Build.0 = Debug|Any CPU
+ {2498F8A0-AA04-40EF-8691-59BBD2396B4D}.Release|AnyCPU.ActiveCfg = Release|Any CPU
+ {2498F8A0-AA04-40EF-8691-59BBD2396B4D}.Release|AnyCPU.Build.0 = Release|Any CPU
{38C762AB-8FD1-44DE-9855-26AAE7129DC3}.Debug|AnyCPU.ActiveCfg = Debug|Any CPU
{38C762AB-8FD1-44DE-9855-26AAE7129DC3}.Debug|AnyCPU.Build.0 = Debug|Any CPU
{38C762AB-8FD1-44DE-9855-26AAE7129DC3}.Release|AnyCPU.ActiveCfg = Release|Any CPU
@@ -398,6 +428,10 @@ Global
{645E1718-C8C4-4C23-8A49-5A37E4ECF7ED} = {04E3E11E-B47D-4599-8AFC-50515A95E715}
{2DD1EE75-6D8D-4653-A800-0A24367F7F38} = {CAB438D8-B0F5-4AF0-BEBD-9E2ADBD7B483}
{53E4ABF0-1085-45F9-B964-DCAE4B819998} = {CAB438D8-B0F5-4AF0-BEBD-9E2ADBD7B483}
+ {F9CD012E-67AC-4A4E-B2A7-252387F91256} = {CAB438D8-B0F5-4AF0-BEBD-9E2ADBD7B483}
+ {C5A44686-3469-45A7-B6AB-2798BA0625BC} = {CAB438D8-B0F5-4AF0-BEBD-9E2ADBD7B483}
+ {A14CB0A1-7A05-4F27-88B2-383798CE1DEE} = {CAB438D8-B0F5-4AF0-BEBD-9E2ADBD7B483}
+ {2498F8A0-AA04-40EF-8691-59BBD2396B4D} = {CAB438D8-B0F5-4AF0-BEBD-9E2ADBD7B483}
{38C762AB-8FD1-44DE-9855-26AAE7129DC3} = {864062D3-A415-4A6F-9324-5820237BA058}
{7387E151-48E3-4885-B2CA-A74434A34045} = {864062D3-A415-4A6F-9324-5820237BA058}
{8A6CB07C-E493-4A4F-AB94-038645A27118} = {E351F97D-EA4F-4E7F-AAA0-8EBB1F2A4A62}
diff --git a/build-tools/automation/yaml-templates/build-windows-steps.yaml b/build-tools/automation/yaml-templates/build-windows-steps.yaml
index 6d3d6738ad2..f7cd09ce3b3 100644
--- a/build-tools/automation/yaml-templates/build-windows-steps.yaml
+++ b/build-tools/automation/yaml-templates/build-windows-steps.yaml
@@ -77,6 +77,36 @@ steps:
testRunTitle: Microsoft.Android.Sdk.Analysis.Tests
continueOnError: true
+- template: /build-tools/automation/yaml-templates/run-dotnet-preview.yaml@self
+ parameters:
+ command: test
+ project: tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj
+ arguments: -c $(XA.Build.Configuration) --logger trx --results-directory $(Agent.TempDirectory)/trimmable-typemap-tests
+ displayName: Test Microsoft.Android.Sdk.TrimmableTypeMap.Tests $(XA.Build.Configuration)
+
+- task: PublishTestResults@2
+ displayName: publish Microsoft.Android.Sdk.TrimmableTypeMap.Tests results
+ condition: always()
+ inputs:
+ testResultsFormat: VSTest
+ testResultsFiles: "$(Agent.TempDirectory)/trimmable-typemap-tests/*.trx"
+ testRunTitle: Microsoft.Android.Sdk.TrimmableTypeMap.Tests
+
+- template: /build-tools/automation/yaml-templates/run-dotnet-preview.yaml@self
+ parameters:
+ command: test
+ project: bin/Test$(XA.Build.Configuration)/$(DotNetTargetFramework)/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests.dll
+ arguments: --logger trx --results-directory $(Agent.TempDirectory)/trimmable-typemap-integration-tests
+ displayName: Test Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests $(XA.Build.Configuration)
+
+- task: PublishTestResults@2
+ displayName: publish Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests results
+ condition: always()
+ inputs:
+ testResultsFormat: VSTest
+ testResultsFiles: "$(Agent.TempDirectory)/trimmable-typemap-integration-tests/*.trx"
+ testRunTitle: Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests
+
- task: BatchScript@1
displayName: Test dotnet-local.cmd - create template
inputs:
diff --git a/eng/Versions.props b/eng/Versions.props
index d87b6a2fe34..bf1e2b58feb 100644
--- a/eng/Versions.props
+++ b/eng/Versions.props
@@ -16,6 +16,7 @@
11.0.100-preview.1.26076.102
0.11.5-preview.26076.102
9.0.4
+ 11.0.0-preview.1.26104.118
36.1.30
$(MicrosoftNETSdkAndroidManifest100100PackageVersion)
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/AcwMapWriter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/AcwMapWriter.cs
new file mode 100644
index 00000000000..d8f563df272
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/AcwMapWriter.cs
@@ -0,0 +1,151 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+///
+/// Generates acw-map.txt from JavaPeerInfo scanner results.
+/// The acw-map is consumed by _ConvertCustomView to fix up custom view names in layout XML.
+///
+/// Each type produces 3 lines: PartialAssemblyQualifiedName;JavaKey, ManagedKey;JavaKey, CompatJniName;JavaKey.
+/// Types with DoNotGenerateAcw = true are excluded (MCW binding types).
+///
+static class AcwMapWriter
+{
+ ///
+ /// Creates per-assembly acw-map entries for a single assembly's scan results.
+ ///
+ public static List CreateEntries (IReadOnlyList peers, string assemblyName)
+ {
+ var entries = new List ();
+
+ foreach (var peer in peers) {
+ if (peer.DoNotGenerateAcw)
+ continue;
+
+ if (peer.AssemblyName != assemblyName)
+ continue;
+
+ var javaKey = peer.JavaName.Replace ('/', '.');
+ var managedKey = peer.ManagedTypeName;
+ var partialAssemblyQualifiedName = $"{peer.ManagedTypeName}, {peer.AssemblyName}";
+ // Compat JNI name uses the same format for now
+ var compatJniName = javaKey;
+
+ entries.Add (new AcwMapEntry {
+ JavaKey = javaKey,
+ ManagedKey = managedKey,
+ PartialAssemblyQualifiedName = partialAssemblyQualifiedName,
+ CompatJniName = compatJniName,
+ AssemblyName = peer.AssemblyName,
+ });
+ }
+
+ return entries;
+ }
+
+ ///
+ /// Writes acw-map.txt, detecting XA4214 (duplicate managed key) and XA4215 (duplicate Java key).
+ ///
+ public static AcwMapResult WriteMap (IReadOnlyList entries, TextWriter writer)
+ {
+ var managed = new Dictionary (entries.Count, StringComparer.Ordinal);
+ var java = new Dictionary (entries.Count, StringComparer.Ordinal);
+ var managedConflicts = new Dictionary> (0, StringComparer.Ordinal);
+ var javaConflicts = new Dictionary> (0, StringComparer.Ordinal);
+
+ foreach (var entry in entries.OrderBy (e => e.ManagedKey, StringComparer.Ordinal)) {
+ writer.Write (entry.PartialAssemblyQualifiedName);
+ writer.Write (';');
+ writer.WriteLine (entry.JavaKey);
+
+ bool hasConflict = false;
+
+ if (managed.TryGetValue (entry.ManagedKey, out var managedConflict)) {
+ if (!managedConflict.AssemblyName.Equals (entry.AssemblyName, StringComparison.Ordinal)) {
+ if (!managedConflicts.TryGetValue (entry.ManagedKey, out var list))
+ managedConflicts.Add (entry.ManagedKey, list = new List { managedConflict.AssemblyName });
+ list.Add (entry.AssemblyName);
+ }
+ hasConflict = true;
+ }
+
+ if (java.TryGetValue (entry.JavaKey, out var javaConflict)) {
+ if (!javaConflict.AssemblyName.Equals (entry.AssemblyName, StringComparison.Ordinal)) {
+ if (!javaConflicts.TryGetValue (entry.JavaKey, out var list))
+ javaConflicts.Add (entry.JavaKey, list = new List { javaConflict.PartialAssemblyQualifiedName });
+ list.Add (entry.PartialAssemblyQualifiedName);
+ }
+ hasConflict = true;
+ }
+
+ if (!hasConflict) {
+ managed.Add (entry.ManagedKey, entry);
+ java.Add (entry.JavaKey, entry);
+
+ writer.Write (entry.ManagedKey);
+ writer.Write (';');
+ writer.WriteLine (entry.JavaKey);
+
+ writer.Write (entry.CompatJniName);
+ writer.Write (';');
+ writer.WriteLine (entry.JavaKey);
+ }
+ }
+
+ return new AcwMapResult {
+ ManagedConflicts = managedConflicts,
+ JavaConflicts = javaConflicts,
+ };
+ }
+
+ ///
+ /// Writes acw-map.txt to a file, only if content changed.
+ ///
+ public static AcwMapResult WriteMapToFile (IReadOnlyList entries, string outputPath)
+ {
+ using var sw = new StringWriter ();
+ var result = WriteMap (entries, sw);
+
+ if (result.JavaConflicts.Count > 0)
+ return result;
+
+ var content = sw.ToString ();
+ WriteIfChanged (outputPath, content);
+ return result;
+ }
+
+ static void WriteIfChanged (string path, string content)
+ {
+ if (File.Exists (path)) {
+ var existing = File.ReadAllText (path);
+ if (string.Equals (existing, content, StringComparison.Ordinal))
+ return;
+ }
+
+ var dir = Path.GetDirectoryName (path);
+ if (!string.IsNullOrEmpty (dir) && !Directory.Exists (dir))
+ Directory.CreateDirectory (dir);
+
+ File.WriteAllText (path, content);
+ }
+}
+
+sealed class AcwMapEntry
+{
+ public string JavaKey { get; set; } = "";
+ public string ManagedKey { get; set; } = "";
+ public string PartialAssemblyQualifiedName { get; set; } = "";
+ public string CompatJniName { get; set; } = "";
+ public string AssemblyName { get; set; } = "";
+}
+
+sealed class AcwMapResult
+{
+ public Dictionary> ManagedConflicts { get; set; } = new ();
+ public Dictionary> JavaConflicts { get; set; } = new ();
+ public bool HasErrors => JavaConflicts.Count > 0;
+ public bool HasWarnings => ManagedConflicts.Count > 0;
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/CompilerFeaturePolyfills.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/CompilerFeaturePolyfills.cs
new file mode 100644
index 00000000000..c33ab5025c2
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/CompilerFeaturePolyfills.cs
@@ -0,0 +1,23 @@
+// Polyfills for C# language features on netstandard2.0
+
+// Required for init-only setters
+namespace System.Runtime.CompilerServices
+{
+ static class IsExternalInit { }
+
+ [AttributeUsage (AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
+ sealed class RequiredMemberAttribute : Attribute { }
+
+ [AttributeUsage (AttributeTargets.All, AllowMultiple = true, Inherited = false)]
+ sealed class CompilerFeatureRequiredAttribute (string featureName) : Attribute
+ {
+ public string FeatureName { get; } = featureName;
+ public bool IsOptional { get; init; }
+ }
+}
+
+namespace System.Diagnostics.CodeAnalysis
+{
+ [AttributeUsage (AttributeTargets.Constructor, AllowMultiple = false, Inherited = false)]
+ sealed class SetsRequiredMembersAttribute : Attribute { }
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs
new file mode 100644
index 00000000000..61e18460a82
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs
@@ -0,0 +1,370 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+///
+/// Generates JCW (Java Callable Wrapper) .java source files from scanned records.
+/// Only processes ACW types (where is false).
+///
+sealed class JcwJavaSourceGenerator
+{
+ ///
+ /// Generates .java source files for all ACW types and writes them to the output directory.
+ /// Returns the list of generated file paths.
+ ///
+ public IReadOnlyList Generate (IReadOnlyList types, string outputDirectory)
+ {
+ if (types is null) {
+ throw new ArgumentNullException (nameof (types));
+ }
+ if (outputDirectory is null) {
+ throw new ArgumentNullException (nameof (outputDirectory));
+ }
+
+ var generatedFiles = new List ();
+
+ foreach (var type in types) {
+ if (type.DoNotGenerateAcw) {
+ continue;
+ }
+
+ string filePath = GetOutputFilePath (type, outputDirectory);
+ string? dir = Path.GetDirectoryName (filePath);
+ if (dir != null) {
+ Directory.CreateDirectory (dir);
+ }
+
+ using var writer = new StreamWriter (filePath);
+ Generate (type, writer);
+ generatedFiles.Add (filePath);
+ }
+
+ return generatedFiles;
+ }
+
+ ///
+ /// Generates a single .java source file for the given type.
+ ///
+ internal void Generate (JavaPeerInfo type, TextWriter writer)
+ {
+ WritePackageDeclaration (type, writer);
+ WriteClassDeclaration (type, writer);
+ WriteStaticInitializer (type, writer);
+ WriteExportFields (type, writer);
+ WriteConstructors (type, writer);
+ WriteMethods (type, writer);
+ WriteClassClose (writer);
+ }
+
+ static string GetOutputFilePath (JavaPeerInfo type, string outputDirectory)
+ {
+ // JNI name uses '/' as separator and '$' for nested types
+ // e.g., "com/example/MainActivity" → "com/example/MainActivity.java"
+ // Nested types: "com/example/Outer$Inner" → "com/example/Outer$Inner.java" (same file convention)
+ string relativePath = type.JavaName + ".java";
+ return Path.Combine (outputDirectory, relativePath);
+ }
+
+ static void WritePackageDeclaration (JavaPeerInfo type, TextWriter writer)
+ {
+ string? package = GetJavaPackageName (type.JavaName);
+ if (package != null) {
+ writer.Write ("package ");
+ writer.Write (package);
+ writer.WriteLine (';');
+ writer.WriteLine ();
+ }
+ }
+
+ static void WriteClassDeclaration (JavaPeerInfo type, TextWriter writer)
+ {
+ writer.Write ("public ");
+ if (type.IsAbstract && !type.IsInterface) {
+ writer.Write ("abstract ");
+ }
+ writer.Write ("class ");
+ writer.WriteLine (GetJavaSimpleName (type.JavaName));
+
+ // extends clause
+ string? baseJavaType = type.BaseJavaName != null ? JniNameToJavaName (type.BaseJavaName) : null;
+ if (baseJavaType != null) {
+ writer.Write ("\textends ");
+ writer.WriteLine (baseJavaType);
+ }
+
+ // implements clause — always includes IGCUserPeer, plus any implemented interfaces
+ writer.Write ("\timplements");
+ writer.Write ("\n\t\tmono.android.IGCUserPeer");
+
+ foreach (var iface in type.ImplementedInterfaceJavaNames) {
+ writer.Write (",\n\t\t");
+ writer.Write (JniNameToJavaName (iface));
+ }
+
+ writer.WriteLine ();
+ writer.WriteLine ('{');
+ }
+
+ static void WriteStaticInitializer (JavaPeerInfo type, TextWriter writer)
+ {
+ writer.Write ("\tstatic {\n");
+ writer.Write ("\t\tmono.android.Runtime.registerNatives (");
+ writer.Write (GetJavaSimpleName (type.JavaName));
+ writer.Write (".class);\n");
+ writer.Write ("\t}\n");
+ writer.WriteLine ();
+ }
+
+ static void WriteExportFields (JavaPeerInfo type, TextWriter writer)
+ {
+ foreach (var field in type.ExportFields) {
+ string javaType = JniTypeToJava (field.JniReturnType);
+
+ writer.Write ("\tpublic ");
+ if (field.IsStatic) {
+ writer.Write ("static ");
+ }
+ writer.Write (javaType);
+ writer.Write (' ');
+ writer.Write (field.FieldName);
+ writer.Write (" = ");
+ writer.Write (field.MethodName);
+ writer.WriteLine (" ();");
+ }
+
+ if (type.ExportFields.Count > 0) {
+ writer.WriteLine ();
+ }
+ }
+
+ static void WriteConstructors (JavaPeerInfo type, TextWriter writer)
+ {
+ string simpleClassName = GetJavaSimpleName (type.JavaName);
+
+ foreach (var ctor in type.JavaConstructors) {
+ // Constructor signature
+ writer.Write ("\tpublic ");
+ writer.Write (simpleClassName);
+ writer.Write (" (");
+ WriteParameterList (ctor.Parameters, writer);
+ writer.Write (")\n");
+
+ WriteThrowsClause (ctor.IsExport ? ctor.ThrownNames : null, writer);
+
+ writer.WriteLine ("\t{");
+
+ // super() call — use SuperArgumentsString if provided ([Export] constructors),
+ // otherwise forward all constructor parameters.
+ writer.Write ("\t\tsuper (");
+ if (ctor.SuperArgumentsString != null) {
+ writer.Write (ctor.SuperArgumentsString);
+ } else {
+ WriteArgumentList (ctor.Parameters, writer);
+ }
+ writer.WriteLine (");");
+
+ // Activation guard: only activate if this is the exact class
+ writer.Write ("\t\tif (getClass () == ");
+ writer.Write (simpleClassName);
+ writer.Write (".class) ");
+
+ // Both [Register] and [Export] constructors use native nctor_N methods.
+ // The .NET side generates a UCO wrapper with the full marshal body.
+ writer.Write ("nctor_");
+ writer.Write (ctor.ConstructorIndex);
+ writer.Write (" (");
+ WriteArgumentList (ctor.Parameters, writer);
+ writer.Write (')');
+ writer.WriteLine (";");
+
+ writer.WriteLine ("\t}");
+ writer.WriteLine ();
+ }
+
+ // Write native constructor declarations
+ foreach (var ctor in type.JavaConstructors) {
+ writer.Write ("\tprivate native void nctor_");
+ writer.Write (ctor.ConstructorIndex);
+ writer.Write (" (");
+ WriteParameterList (ctor.Parameters, writer);
+ writer.WriteLine (");");
+ }
+
+ if (type.JavaConstructors.Count > 0) {
+ writer.WriteLine ();
+ }
+ }
+
+ static void WriteMethods (JavaPeerInfo type, TextWriter writer)
+ {
+ foreach (var method in type.MarshalMethods) {
+ if (method.IsConstructor) {
+ continue;
+ }
+
+ string javaReturnType = JniTypeToJava (method.JniReturnType);
+ bool isVoid = method.JniReturnType == "V";
+ bool isExport = method.Connector == null;
+
+ // Public wrapper method
+ if (!isExport) {
+ writer.Write ("\t@Override\n");
+ }
+ writer.Write ("\tpublic ");
+ if (method.IsStatic) {
+ writer.Write ("static ");
+ }
+ writer.Write (javaReturnType);
+ writer.Write (' ');
+ writer.Write (method.JniName);
+ writer.Write (" (");
+ WriteParameterList (method.Parameters, writer);
+ writer.Write (")\n");
+
+ WriteThrowsClause (method.ThrownNames, writer);
+
+ writer.Write ("\t{\n");
+
+ // Delegate to native method
+ writer.Write ("\t\t");
+ if (!isVoid) {
+ writer.Write ("return ");
+ }
+ writer.Write (method.NativeCallbackName);
+ writer.Write (" (");
+ WriteArgumentList (method.Parameters, writer);
+ writer.Write (");\n");
+
+ writer.Write ("\t}\n");
+
+ // Native method declaration
+ writer.Write ("\tprivate ");
+ if (method.IsStatic) {
+ writer.Write ("static ");
+ }
+ writer.Write ("native ");
+ writer.Write (javaReturnType);
+ writer.Write (' ');
+ writer.Write (method.NativeCallbackName);
+ writer.Write (" (");
+ WriteParameterList (method.Parameters, writer);
+ writer.Write (");\n");
+
+ writer.WriteLine ();
+ }
+ }
+
+ static void WriteClassClose (TextWriter writer)
+ {
+ writer.WriteLine ('}');
+ }
+
+ static void WriteParameterList (IReadOnlyList parameters, TextWriter writer)
+ {
+ for (int i = 0; i < parameters.Count; i++) {
+ if (i > 0) {
+ writer.Write (", ");
+ }
+ writer.Write (JniTypeToJava (parameters [i].JniType));
+ writer.Write (" p");
+ writer.Write (i);
+ }
+ }
+
+ static void WriteArgumentList (IReadOnlyList parameters, TextWriter writer)
+ {
+ for (int i = 0; i < parameters.Count; i++) {
+ if (i > 0) {
+ writer.Write (", ");
+ }
+ writer.Write ('p');
+ writer.Write (i);
+ }
+ }
+
+ static void WriteThrowsClause (IReadOnlyList? thrownNames, TextWriter writer)
+ {
+ if (thrownNames == null || thrownNames.Count == 0) {
+ return;
+ }
+
+ writer.Write ("\t\tthrows ");
+ for (int i = 0; i < thrownNames.Count; i++) {
+ if (i > 0) {
+ writer.Write (", ");
+ }
+ writer.Write (thrownNames [i]);
+ }
+ writer.Write ('\n');
+ }
+
+ ///
+ /// Converts a JNI type name to a Java source type name.
+ /// e.g., "android/app/Activity" → "android.app.Activity"
+ ///
+ internal static string JniNameToJavaName (string jniName)
+ {
+ return jniName.Replace ('/', '.');
+ }
+
+ ///
+ /// Extracts the Java package name from a JNI type name.
+ /// e.g., "com/example/MainActivity" → "com.example"
+ /// Returns null for types without a package.
+ ///
+ internal static string? GetJavaPackageName (string jniName)
+ {
+ int lastSlash = jniName.LastIndexOf ('/');
+ if (lastSlash < 0) {
+ return null;
+ }
+ return jniName.Substring (0, lastSlash).Replace ('/', '.');
+ }
+
+ ///
+ /// Extracts the simple Java class name from a JNI type name.
+ /// e.g., "com/example/MainActivity" → "MainActivity"
+ /// e.g., "com/example/Outer$Inner" → "Outer$Inner" (preserves nesting separator)
+ ///
+ internal static string GetJavaSimpleName (string jniName)
+ {
+ int lastSlash = jniName.LastIndexOf ('/');
+ return lastSlash >= 0 ? jniName.Substring (lastSlash + 1) : jniName;
+ }
+
+ ///
+ /// Converts a JNI type descriptor to a Java source type.
+ /// e.g., "V" → "void", "I" → "int", "Landroid/os/Bundle;" → "android.os.Bundle"
+ ///
+ internal static string JniTypeToJava (string jniType)
+ {
+ if (jniType.Length == 1) {
+ return jniType [0] switch {
+ 'V' => "void",
+ 'Z' => "boolean",
+ 'B' => "byte",
+ 'C' => "char",
+ 'S' => "short",
+ 'I' => "int",
+ 'J' => "long",
+ 'F' => "float",
+ 'D' => "double",
+ _ => throw new ArgumentException ($"Unknown JNI primitive type: {jniType}"),
+ };
+ }
+
+ // Array types: "[I" → "int[]", "[Ljava/lang/String;" → "java.lang.String[]"
+ if (jniType [0] == '[') {
+ return JniTypeToJava (jniType.Substring (1)) + "[]";
+ }
+
+ // Object types: "Landroid/os/Bundle;" → "android.os.Bundle"
+ if (jniType [0] == 'L' && jniType [jniType.Length - 1] == ';') {
+ return JniNameToJavaName (jniType.Substring (1, jniType.Length - 2));
+ }
+
+ throw new ArgumentException ($"Unknown JNI type descriptor: {jniType}");
+ }
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs
new file mode 100644
index 00000000000..df672795274
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs
@@ -0,0 +1,130 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection.Metadata;
+using System.Reflection.Metadata.Ecma335;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+/// JNI primitive type kinds used for mapping JNI signatures → CLR types.
+enum JniParamKind
+{
+ Void, // V
+ Boolean, // Z → byte (JNI's jboolean is unsigned 8-bit)
+ Byte, // B → sbyte (JNI's jbyte is signed 8-bit)
+ Char, // C → char
+ Short, // S → short
+ Int, // I → int
+ Long, // J → long
+ Float, // F → float
+ Double, // D → double
+ Object, // L...; or [ → IntPtr
+}
+
+/// Helpers for parsing JNI method signatures.
+static class JniSignatureHelper
+{
+ /// Parses the parameter types from a JNI method signature like "(Landroid/os/Bundle;)V".
+ public static List ParseParameterTypes (string jniSignature)
+ {
+ var result = new List ();
+ int i = 1; // skip opening '('
+ while (i < jniSignature.Length && jniSignature [i] != ')') {
+ result.Add (ParseSingleType (jniSignature, ref i));
+ }
+ return result;
+ }
+
+ /// Parses the raw JNI type descriptor strings from a JNI method signature.
+ public static List ParseParameterTypeStrings (string jniSignature)
+ {
+ var result = new List ();
+ int i = 1; // skip opening '('
+ while (i < jniSignature.Length && jniSignature [i] != ')') {
+ int start = i;
+ SkipSingleType (jniSignature, ref i);
+ result.Add (jniSignature.Substring (start, i - start));
+ }
+ return result;
+ }
+
+ /// Extracts the return type descriptor from a JNI method signature.
+ public static string ParseReturnTypeString (string jniSignature)
+ {
+ int i = jniSignature.IndexOf (')') + 1;
+ return jniSignature.Substring (i);
+ }
+
+ /// Parses the return type from a JNI method signature.
+ public static JniParamKind ParseReturnType (string jniSignature)
+ {
+ int i = jniSignature.IndexOf (')') + 1;
+ return ParseSingleType (jniSignature, ref i);
+ }
+
+ static JniParamKind ParseSingleType (string sig, ref int i)
+ {
+ switch (sig [i]) {
+ case 'V': i++; return JniParamKind.Void;
+ case 'Z': i++; return JniParamKind.Boolean;
+ case 'B': i++; return JniParamKind.Byte;
+ case 'C': i++; return JniParamKind.Char;
+ case 'S': i++; return JniParamKind.Short;
+ case 'I': i++; return JniParamKind.Int;
+ case 'J': i++; return JniParamKind.Long;
+ case 'F': i++; return JniParamKind.Float;
+ case 'D': i++; return JniParamKind.Double;
+ case 'L':
+ i = sig.IndexOf (';', i) + 1;
+ return JniParamKind.Object;
+ case '[':
+ i++;
+ ParseSingleType (sig, ref i); // skip element type
+ return JniParamKind.Object;
+ default:
+ throw new ArgumentException ($"Unknown JNI type character '{sig [i]}' in '{sig}' at index {i}");
+ }
+ }
+
+ /// Parses a standalone JNI type descriptor like "I", "Ljava/lang/String;", "[B".
+ public static JniParamKind ParseSingleTypeFromDescriptor (string descriptor)
+ {
+ int i = 0;
+ return ParseSingleType (descriptor, ref i);
+ }
+
+ static void SkipSingleType (string sig, ref int i)
+ {
+ switch (sig [i]) {
+ case 'V': case 'Z': case 'B': case 'C': case 'S':
+ case 'I': case 'J': case 'F': case 'D':
+ i++;
+ break;
+ case 'L':
+ i = sig.IndexOf (';', i) + 1;
+ break;
+ case '[':
+ i++;
+ SkipSingleType (sig, ref i);
+ break;
+ default:
+ throw new ArgumentException ($"Unknown JNI type character '{sig [i]}' in '{sig}' at index {i}");
+ }
+ }
+
+ /// Encodes the CLR type for a JNI parameter kind into a signature type encoder.
+ public static void EncodeClrType (SignatureTypeEncoder encoder, JniParamKind kind)
+ {
+ switch (kind) {
+ case JniParamKind.Boolean: encoder.Byte (); break;
+ case JniParamKind.Byte: encoder.SByte (); break;
+ case JniParamKind.Char: encoder.Char (); break;
+ case JniParamKind.Short: encoder.Int16 (); break;
+ case JniParamKind.Int: encoder.Int32 (); break;
+ case JniParamKind.Long: encoder.Int64 (); break;
+ case JniParamKind.Float: encoder.Single (); break;
+ case JniParamKind.Double: encoder.Double (); break;
+ case JniParamKind.Object: encoder.IntPtr (); break;
+ default: throw new ArgumentException ($"Cannot encode JNI param kind {kind} as CLR type");
+ }
+ }
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs
new file mode 100644
index 00000000000..bc9ca36efd3
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs
@@ -0,0 +1,231 @@
+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 string AssemblyName { get; set; } = "";
+
+ /// Module file name (e.g., "_MyApp.TypeMap.dll").
+ public string ModuleName { get; set; } = "";
+
+ /// 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 class TypeMapAttributeData
+{
+ /// JNI type name, e.g., "android/app/Activity".
+ public string JniName { get; set; } = "";
+
+ ///
+ /// Assembly-qualified proxy type reference string.
+ /// Either points to a generated proxy or to the original managed type.
+ ///
+ public string ProxyTypeReference { get; set; } = "";
+
+ ///
+ /// 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; set; }
+
+ /// 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 string TypeName { get; set; } = "";
+
+ /// Namespace for all proxy types.
+ public string Namespace { get; set; } = "_TypeMap.Proxies";
+
+ /// Reference to the managed type this proxy wraps (for ldtoken in TargetType property).
+ public TypeRefData TargetType { get; set; } = new ();
+
+ /// 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; set; }
+
+ /// Whether this proxy needs ACW support (RegisterNatives + UCO wrappers + IAndroidCallableWrapper).
+ public bool IsAcw { get; set; }
+
+ /// UCO method wrappers for [Register] methods and constructors.
+ public List UcoMethods { get; } = new ();
+
+ /// Export marshal method wrappers — full marshal body for [Export] methods and constructors.
+ public List ExportMarshalMethods { get; } = new ();
+
+ /// RegisterNatives registrations (method name, JNI signature, wrapper name).
+ public List NativeRegistrations { get; } = new ();
+}
+
+///
+/// A cross-assembly type reference (assembly name + full managed type name).
+///
+sealed class TypeRefData
+{
+ /// Full managed type name, e.g., "Android.App.Activity" or "MyApp.Outer+Inner".
+ public string ManagedTypeName { get; set; } = "";
+
+ /// Assembly containing the type, e.g., "Mono.Android".
+ public string AssemblyName { get; set; } = "";
+}
+
+///
+/// An [UnmanagedCallersOnly] static wrapper for a marshal method.
+/// Body: load all args → call n_* callback → ret.
+///
+sealed class UcoMethodData
+{
+ /// Name of the generated wrapper method, e.g., "n_onCreate_uco_0".
+ public string WrapperName { get; set; } = "";
+
+ /// Name of the n_* callback to call, e.g., "n_OnCreate".
+ public string CallbackMethodName { get; set; } = "";
+
+ /// Type containing the callback method.
+ public TypeRefData CallbackType { get; set; } = new ();
+
+ /// JNI method signature, e.g., "(Landroid/os/Bundle;)V". Used to determine CLR parameter types.
+ public string JniSignature { get; set; } = "";
+}
+
+///
+/// An [UnmanagedCallersOnly] static wrapper for an [Export] method or constructor.
+/// Unlike which just forwards to an existing n_* callback,
+/// this generates the full marshal method body: BeginMarshalMethod, GetObject, param
+/// unmarshaling, managed method call, return marshaling, exception handling, EndMarshalMethod.
+///
+sealed class ExportMarshalMethodData
+{
+ /// Name of the generated wrapper method, e.g., "n_myMethod_uco_0" or "nctor_0_uco".
+ public string WrapperName { get; set; } = "";
+
+ ///
+ /// JNI method name for RegisterNatives, e.g., "n_DoWork" or "nctor_0".
+ /// Must match the native method declaration in the Java JCW.
+ ///
+ public string NativeCallbackName { get; set; } = "";
+
+ /// Name of the managed method to call, e.g., "MyMethod" or ".ctor".
+ public string ManagedMethodName { get; set; } = "";
+
+ /// Type containing the managed method (the user's type).
+ public TypeRefData DeclaringType { get; set; } = new ();
+
+ /// JNI method signature, e.g., "(Ljava/lang/String;I)V".
+ public string JniSignature { get; set; } = "";
+
+ /// True if this is a constructor.
+ public bool IsConstructor { get; set; }
+
+ /// True if this is a static method.
+ public bool IsStatic { get; set; }
+
+ ///
+ /// Managed parameter types for the managed method call.
+ /// Each entry is the assembly-qualified managed type name.
+ ///
+ public List ManagedParameters { get; } = new ();
+
+ /// Managed return type (assembly-qualified). Null/empty for void or constructors.
+ public string? ManagedReturnType { get; set; }
+}
+
+///
+/// Describes a parameter for an [Export] marshal method, with both JNI and managed type info.
+///
+sealed class ExportParamData
+{
+ /// JNI type descriptor, e.g., "Ljava/lang/String;", "I".
+ public string JniType { get; set; } = "";
+
+ /// Managed type name (assembly-qualified), e.g., "System.String, System.Private.CoreLib".
+ public string ManagedTypeName { get; set; } = "";
+
+ /// Assembly containing the managed type.
+ public string AssemblyName { get; set; } = "";
+}
+
+///
+/// One JNI native method registration in RegisterNatives.
+///
+sealed class NativeRegistrationData
+{
+ /// JNI method name to register, e.g., "n_onCreate" or "nctor_0".
+ public string JniMethodName { get; set; } = "";
+
+ /// JNI method signature, e.g., "(Landroid/os/Bundle;)V".
+ public string JniSignature { get; set; } = "";
+
+ /// Name of the UCO wrapper method whose function pointer to register.
+ public string WrapperMethodName { get; set; } = "";
+}
+
+///
+/// Describes how the proxy's CreateInstance should construct the managed peer.
+///
+sealed class ActivationCtorData
+{
+ /// Type that declares the activation constructor (may be a base type).
+ public TypeRefData DeclaringType { get; set; } = new ();
+
+ /// True when the leaf type itself declares the activation ctor.
+ public bool IsOnLeafType { get; set; }
+
+ /// The style of activation ctor (XamarinAndroid or JavaInterop).
+ public ActivationCtorStyle Style { get; set; }
+}
+
+///
+/// One [assembly: TypeMapAssociation(typeof(Source), typeof(AliasProxy))] entry.
+/// Links a managed type to the proxy that holds its alias TypeMap entry.
+///
+sealed class TypeMapAssociationData
+{
+ /// Assembly-qualified source type reference (the managed alias type).
+ public string SourceTypeReference { get; set; } = "";
+
+ /// Assembly-qualified proxy type reference (the alias holder proxy).
+ public string AliasProxyTypeReference { get; set; } = "";
+}
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..2264f7ec756
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs
@@ -0,0 +1,380 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+///
+/// Builds a from scanned records.
+/// All decision logic (deduplication, alias detection, ACW filtering, 2-arg vs 3-arg attribute
+/// selection, callback resolution, proxy naming) lives here.
+/// The output model is a plain data structure that the emitter writes directly into a PE assembly.
+///
+static class ModelBuilder
+{
+ static readonly HashSet EssentialRuntimeTypes = new (StringComparer.Ordinal) {
+ "java/lang/Object",
+ "java/lang/Class",
+ "java/lang/String",
+ "java/lang/Throwable",
+ "java/lang/Exception",
+ "java/lang/RuntimeException",
+ "java/lang/Error",
+ "java/lang/Thread",
+ };
+
+ ///
+ /// Builds a TypeMap assembly model for the given peers.
+ ///
+ /// Scanned Java peer types (typically from a single input assembly).
+ /// Output .dll path — used to derive assembly/module names if not specified.
+ /// Explicit assembly name. If null, derived from .
+ public static TypeMapAssemblyData Build (IReadOnlyList peers, string outputPath, string? assemblyName = null)
+ {
+ if (peers is null) {
+ throw new ArgumentNullException (nameof (peers));
+ }
+ if (outputPath is null) {
+ throw new ArgumentNullException (nameof (outputPath));
+ }
+
+ assemblyName ??= Path.GetFileNameWithoutExtension (outputPath);
+ string moduleName = Path.GetFileName (outputPath);
+
+ var model = new TypeMapAssemblyData {
+ AssemblyName = assemblyName,
+ ModuleName = moduleName,
+ };
+
+ // Invoker types are NOT emitted as separate proxies or TypeMap entries —
+ // they only appear as a TypeRef in the interface proxy's get_InvokerType property.
+ var invokerTypeNames = new HashSet (
+ peers.Where (p => p.InvokerTypeName != null).Select (p => p.InvokerTypeName!),
+ StringComparer.Ordinal);
+
+ // Group non-invoker peers by JNI name to detect aliases (multiple .NET types → same Java class).
+ // Use an ordered dictionary to ensure deterministic output across runs.
+ var groups = new SortedDictionary> (StringComparer.Ordinal);
+ foreach (var peer in peers) {
+ if (invokerTypeNames.Contains (peer.ManagedTypeName)) {
+ continue;
+ }
+ if (!groups.TryGetValue (peer.JavaName, out var list)) {
+ list = new List ();
+ groups [peer.JavaName] = list;
+ }
+ list.Add (peer);
+ }
+
+ foreach (var kvp in groups) {
+ string jniName = kvp.Key;
+ var peersForName = kvp.Value;
+
+ // Sort aliases by managed type name for deterministic proxy naming
+ if (peersForName.Count > 1) {
+ peersForName.Sort ((a, b) => StringComparer.Ordinal.Compare (a.ManagedTypeName, b.ManagedTypeName));
+ }
+
+ EmitPeers (model, jniName, peersForName, assemblyName);
+ }
+
+ // Compute IgnoresAccessChecksTo from cross-assembly references
+ var referencedAssemblies = new SortedSet (StringComparer.Ordinal);
+ foreach (var proxy in model.ProxyTypes) {
+ AddIfCrossAssembly (referencedAssemblies, proxy.TargetType?.AssemblyName, assemblyName);
+ foreach (var uco in proxy.UcoMethods) {
+ AddIfCrossAssembly (referencedAssemblies, uco.CallbackType.AssemblyName, assemblyName);
+ }
+ foreach (var export in proxy.ExportMarshalMethods) {
+ AddIfCrossAssembly (referencedAssemblies, export.DeclaringType.AssemblyName, assemblyName);
+ }
+ // SetHandle is protected on Java.Lang.Object (Mono.Android) — needed for
+ // [Export] constructor marshal methods (nctor_N_uco) that call SetHandle directly.
+ // Inherited activation ctors call .ctor(IntPtr, JniHandleOwnership) which is public,
+ // but the base ctor's declaring assembly still needs IgnoresAccessChecksTo.
+ bool usesSetHandle = proxy.ExportMarshalMethods.Any (e => e.IsConstructor);
+ bool usesInheritedCtor = proxy.ActivationCtor != null && !proxy.ActivationCtor.IsOnLeafType;
+ if (usesSetHandle) {
+ AddIfCrossAssembly (referencedAssemblies, "Mono.Android", assemblyName);
+ }
+ if (usesInheritedCtor) {
+ AddIfCrossAssembly (referencedAssemblies, proxy.ActivationCtor!.DeclaringType.AssemblyName, assemblyName);
+ }
+ }
+ model.IgnoresAccessChecksTo.AddRange (referencedAssemblies);
+
+ return model;
+ }
+
+ static void EmitPeers (TypeMapAssemblyData model, string jniName,
+ List peersForName, string assemblyName)
+ {
+ // First peer is the "primary" — it gets the base JNI name entry.
+ // Remaining peers get indexed alias entries: "jni/name[1]", "jni/name[2]", ...
+ JavaPeerProxyData? primaryProxy = null;
+ for (int i = 0; i < peersForName.Count; i++) {
+ var peer = peersForName [i];
+ string entryJniName = i == 0 ? jniName : $"{jniName}[{i}]";
+
+ bool isAcw = !peer.DoNotGenerateAcw && !peer.IsInterface && peer.MarshalMethods.Count > 0;
+ bool hasProxy = peer.ActivationCtor != null || peer.InvokerTypeName != null || isAcw;
+
+ JavaPeerProxyData? proxy = null;
+ if (hasProxy) {
+ proxy = BuildProxyType (peer, isAcw);
+ model.ProxyTypes.Add (proxy);
+ }
+
+ if (i == 0) {
+ primaryProxy = proxy;
+ }
+
+ model.Entries.Add (BuildEntry (peer, proxy, assemblyName, entryJniName));
+
+ // Emit TypeMapAssociation linking alias types to the primary proxy
+ if (i > 0 && primaryProxy != null) {
+ model.Associations.Add (new TypeMapAssociationData {
+ SourceTypeReference = $"{peer.ManagedTypeName}, {peer.AssemblyName}",
+ AliasProxyTypeReference = $"{primaryProxy.Namespace}.{primaryProxy.TypeName}, {assemblyName}",
+ });
+ }
+ }
+ }
+
+ ///
+ /// Determines whether a type should use the unconditional (2-arg) TypeMap attribute.
+ /// Unconditional types are always preserved by the trimmer.
+ ///
+ static bool IsUnconditionalEntry (JavaPeerInfo peer)
+ {
+ // Essential runtime types needed by the Java interop runtime
+ if (EssentialRuntimeTypes.Contains (peer.JavaName)) {
+ return true;
+ }
+
+ // Implementor/EventDispatcher types are only created from .NET (e.g., when a C# event
+ // is subscribed). They should NOT be unconditional — they're trimmable.
+ if (IsImplementorOrEventDispatcher (peer)) {
+ return false;
+ }
+
+ // User-defined ACW types (not MCW bindings, not interfaces) are unconditional
+ // because Android can instantiate them from Java at any time.
+ if (!peer.DoNotGenerateAcw && !peer.IsInterface) {
+ return true;
+ }
+
+ // Types marked unconditional by the scanner (component attributes: Activity, Service, etc.)
+ if (peer.IsUnconditional) {
+ return true;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Implementor and EventDispatcher types are generated by the binding generator
+ /// and are only instantiated from .NET. They should be trimmable.
+ /// NOTE: This is a name-based heuristic. Ideally the scanner would provide a dedicated flag.
+ /// User types whose names happen to end in "Implementor" or "EventDispatcher" would be
+ /// misclassified as trimmable. This is acceptable for now since such naming in user code
+ /// is unlikely and would only affect trimming behavior, not correctness.
+ ///
+ static bool IsImplementorOrEventDispatcher (JavaPeerInfo peer)
+ {
+ return peer.ManagedTypeName.EndsWith ("Implementor", StringComparison.Ordinal) ||
+ peer.ManagedTypeName.EndsWith ("EventDispatcher", StringComparison.Ordinal);
+ }
+
+ static void AddIfCrossAssembly (SortedSet set, string? asmName, string outputAssemblyName)
+ {
+ if (asmName != null && !string.Equals (asmName, outputAssemblyName, StringComparison.Ordinal)) {
+ set.Add (asmName);
+ }
+ }
+
+ static JavaPeerProxyData BuildProxyType (JavaPeerInfo peer, bool isAcw)
+ {
+ // Use managed type name for proxy naming to guarantee uniqueness across aliases
+ // (two types with the same JNI name will have different managed names).
+ var proxyTypeName = peer.ManagedTypeName.Replace ('.', '_').Replace ('+', '_') + "_Proxy";
+
+ var proxy = new JavaPeerProxyData {
+ TypeName = proxyTypeName,
+ TargetType = new TypeRefData {
+ ManagedTypeName = peer.ManagedTypeName,
+ AssemblyName = peer.AssemblyName,
+ },
+ IsAcw = isAcw,
+ 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,
+ };
+ }
+
+ if (isAcw) {
+ BuildUcoMethods (peer, proxy);
+ BuildUcoConstructors (peer, proxy);
+ BuildNativeRegistrations (proxy);
+ }
+
+ return proxy;
+ }
+
+ static void BuildUcoMethods (JavaPeerInfo peer, JavaPeerProxyData proxy)
+ {
+ int ucoIndex = 0;
+ for (int i = 0; i < peer.MarshalMethods.Count; i++) {
+ var mm = peer.MarshalMethods [i];
+ if (mm.IsConstructor) {
+ continue;
+ }
+
+ string wrapperName = $"n_{mm.JniName}_uco_{ucoIndex}";
+
+ if (mm.Connector == null) {
+ // [Export] method — generate full marshal body
+ var exportData = BuildExportMarshalMethod (mm, peer, wrapperName, mm.NativeCallbackName, isConstructor: false);
+ proxy.ExportMarshalMethods.Add (exportData);
+ } else {
+ // [Register] method — forward to existing n_* callback
+ proxy.UcoMethods.Add (new UcoMethodData {
+ WrapperName = wrapperName,
+ CallbackMethodName = mm.NativeCallbackName,
+ CallbackType = new TypeRefData {
+ ManagedTypeName = !string.IsNullOrEmpty (mm.DeclaringTypeName) ? mm.DeclaringTypeName : peer.ManagedTypeName,
+ AssemblyName = !string.IsNullOrEmpty (mm.DeclaringAssemblyName) ? mm.DeclaringAssemblyName : peer.AssemblyName,
+ },
+ JniSignature = mm.JniSignature,
+ });
+ }
+ ucoIndex++;
+ }
+ }
+
+ static void BuildUcoConstructors (JavaPeerInfo peer, JavaPeerProxyData proxy)
+ {
+ if (peer.ActivationCtor == null || peer.JavaConstructors.Count == 0) {
+ return;
+ }
+
+ // Index marshal methods by JNI signature for lookup
+ var marshalMethodsBySignature = new Dictionary (StringComparer.Ordinal);
+ foreach (var mm in peer.MarshalMethods) {
+ if (mm.IsConstructor) {
+ marshalMethodsBySignature [mm.JniSignature] = mm;
+ }
+ }
+
+ foreach (var ctor in peer.JavaConstructors) {
+ if (!marshalMethodsBySignature.TryGetValue (ctor.JniSignature, out var mm)) {
+ continue;
+ }
+
+ string wrapperName = $"nctor_{ctor.ConstructorIndex}_uco";
+ string nativeCallbackName = $"nctor_{ctor.ConstructorIndex}";
+
+ // ALL constructors need full marshal body generation — there are no
+ // pre-existing n_* callbacks for constructors (unlike [Register] methods).
+ // The [Register] connector for constructors is always "" (empty string).
+ var exportData = BuildExportMarshalMethod (mm, peer, wrapperName, nativeCallbackName, isConstructor: true);
+ proxy.ExportMarshalMethods.Add (exportData);
+ }
+ }
+
+ static void BuildNativeRegistrations (JavaPeerProxyData proxy)
+ {
+ foreach (var uco in proxy.UcoMethods) {
+ proxy.NativeRegistrations.Add (new NativeRegistrationData {
+ JniMethodName = uco.CallbackMethodName,
+ JniSignature = uco.JniSignature,
+ WrapperMethodName = uco.WrapperName,
+ });
+ }
+
+ foreach (var export in proxy.ExportMarshalMethods) {
+ proxy.NativeRegistrations.Add (new NativeRegistrationData {
+ JniMethodName = export.NativeCallbackName,
+ JniSignature = export.JniSignature,
+ WrapperMethodName = export.WrapperName,
+ });
+ }
+ }
+
+ static ExportMarshalMethodData BuildExportMarshalMethod (MarshalMethodInfo mm, JavaPeerInfo peer,
+ string wrapperName, string nativeCallbackName, bool isConstructor)
+ {
+ var data = new ExportMarshalMethodData {
+ WrapperName = wrapperName,
+ NativeCallbackName = nativeCallbackName,
+ ManagedMethodName = mm.ManagedMethodName,
+ DeclaringType = new TypeRefData {
+ ManagedTypeName = peer.ManagedTypeName,
+ AssemblyName = peer.AssemblyName,
+ },
+ JniSignature = mm.JniSignature,
+ IsConstructor = isConstructor,
+ IsStatic = mm.IsStatic,
+ ManagedReturnType = mm.ManagedReturnType,
+ };
+
+ foreach (var param in mm.Parameters) {
+ // Parse assembly name from assembly-qualified name "TypeName, AssemblyName"
+ string managedTypeName = param.ManagedType;
+ string assemblyName = peer.AssemblyName;
+ int commaIndex = managedTypeName.IndexOf (", ", StringComparison.Ordinal);
+ if (commaIndex >= 0) {
+ assemblyName = managedTypeName.Substring (commaIndex + 2);
+ managedTypeName = managedTypeName.Substring (0, commaIndex);
+ }
+
+ data.ManagedParameters.Add (new ExportParamData {
+ JniType = param.JniType,
+ ManagedTypeName = managedTypeName,
+ AssemblyName = assemblyName,
+ });
+ }
+
+ return data;
+ }
+
+ static TypeMapAttributeData BuildEntry (JavaPeerInfo peer, JavaPeerProxyData? proxy,
+ string outputAssemblyName, string jniName)
+ {
+ string proxyRef;
+ if (proxy != null) {
+ proxyRef = $"{proxy.Namespace}.{proxy.TypeName}, {outputAssemblyName}";
+ } else {
+ proxyRef = $"{peer.ManagedTypeName}, {peer.AssemblyName}";
+ }
+
+ bool isUnconditional = IsUnconditionalEntry (peer);
+ string? targetRef = null;
+ if (!isUnconditional) {
+ targetRef = $"{peer.ManagedTypeName}, {peer.AssemblyName}";
+ }
+
+ return new TypeMapAttributeData {
+ JniName = jniName,
+ ProxyTypeReference = proxyRef,
+ TargetTypeReference = targetRef,
+ };
+ }
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs
new file mode 100644
index 00000000000..7d74767aa50
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs
@@ -0,0 +1,149 @@
+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;
+
+///
+/// Generates the root _Microsoft.Android.TypeMaps.dll assembly that references
+/// all per-assembly typemap assemblies via
+/// [assembly: TypeMapAssemblyTargetAttribute<Java.Lang.Object>("name")].
+///
+sealed class RootTypeMapAssemblyGenerator
+{
+ const string DefaultAssemblyName = "_Microsoft.Android.TypeMaps";
+
+ // Mono.Android strong name public key token (84e04ff9cfb79065)
+ static readonly byte [] MonoAndroidPublicKeyToken = { 0x84, 0xe0, 0x4f, 0xf9, 0xcf, 0xb7, 0x90, 0x65 };
+
+ readonly Version _systemRuntimeVersion;
+
+ /// Version for System.Runtime assembly references.
+ public RootTypeMapAssemblyGenerator (Version systemRuntimeVersion)
+ {
+ _systemRuntimeVersion = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion));
+ }
+
+ ///
+ /// Generates the root typemap assembly.
+ ///
+ /// Names of per-assembly typemap assemblies to reference.
+ /// Path to write the output .dll.
+ /// Optional assembly name (defaults to _Microsoft.Android.TypeMaps).
+ public void Generate (IReadOnlyList perAssemblyTypeMapNames, string outputPath, string? assemblyName = null)
+ {
+ if (perAssemblyTypeMapNames is null) {
+ throw new ArgumentNullException (nameof (perAssemblyTypeMapNames));
+ }
+ if (outputPath is null) {
+ throw new ArgumentNullException (nameof (outputPath));
+ }
+
+ assemblyName ??= DefaultAssemblyName;
+ var moduleName = Path.GetFileName (outputPath);
+
+ var dir = Path.GetDirectoryName (outputPath);
+ if (!string.IsNullOrEmpty (dir)) {
+ Directory.CreateDirectory (dir);
+ }
+
+ var metadata = new MetadataBuilder ();
+ var ilBuilder = new BlobBuilder ();
+
+ // Assembly definition
+ metadata.AddAssembly (
+ metadata.GetOrAddString (assemblyName),
+ new Version (1, 0, 0, 0),
+ culture: default,
+ publicKey: default,
+ flags: 0,
+ hashAlgorithm: AssemblyHashAlgorithm.None);
+
+ // Module definition
+ metadata.AddModule (
+ generation: 0,
+ metadata.GetOrAddString (moduleName),
+ metadata.GetOrAddGuid (Guid.NewGuid ()),
+ encId: default,
+ encBaseId: default);
+
+ // Assembly references
+ var systemRuntimeRef = metadata.AddAssemblyReference (
+ metadata.GetOrAddString ("System.Runtime"),
+ _systemRuntimeVersion, default, default, 0, default);
+
+ var systemRuntimeInteropServicesRef = metadata.AddAssemblyReference (
+ metadata.GetOrAddString ("System.Runtime.InteropServices"),
+ _systemRuntimeVersion, default, default, 0, default);
+
+ var monoAndroidRef = metadata.AddAssemblyReference (
+ metadata.GetOrAddString ("Mono.Android"),
+ new Version (0, 0, 0, 0), default,
+ metadata.GetOrAddBlob (MonoAndroidPublicKeyToken), 0, default);
+
+ // type
+ metadata.AddTypeDefinition (
+ default, default,
+ metadata.GetOrAddString (""),
+ default,
+ MetadataTokens.FieldDefinitionHandle (1),
+ MetadataTokens.MethodDefinitionHandle (1));
+
+ // Reference the open generic TypeMapAssemblyTargetAttribute`1 from System.Runtime.InteropServices
+ var openAttrRef = metadata.AddTypeReference (systemRuntimeInteropServicesRef,
+ metadata.GetOrAddString ("System.Runtime.InteropServices"),
+ metadata.GetOrAddString ("TypeMapAssemblyTargetAttribute`1"));
+
+ // Reference Java.Lang.Object from Mono.Android (the type universe)
+ var javaLangObjectRef = metadata.AddTypeReference (monoAndroidRef,
+ metadata.GetOrAddString ("Java.Lang"), metadata.GetOrAddString ("Object"));
+
+ // Build TypeSpec for TypeMapAssemblyTargetAttribute
+ var genericInstBlob = new BlobBuilder ();
+ genericInstBlob.WriteByte (0x15); // ELEMENT_TYPE_GENERICINST
+ genericInstBlob.WriteByte (0x12); // ELEMENT_TYPE_CLASS
+ genericInstBlob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (openAttrRef));
+ genericInstBlob.WriteCompressedInteger (1); // generic arity = 1
+ genericInstBlob.WriteByte (0x12); // ELEMENT_TYPE_CLASS
+ genericInstBlob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (javaLangObjectRef));
+ var closedAttrTypeSpec = metadata.AddTypeSpecification (metadata.GetOrAddBlob (genericInstBlob));
+
+ // MemberRef for .ctor(string) on the closed generic type
+ var ctorRef = AddMemberRef (metadata, 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 attrBlob = new BlobBuilder ();
+ attrBlob.WriteUInt16 (1); // Prolog
+ attrBlob.WriteSerializedString (name);
+ attrBlob.WriteUInt16 (0); // NumNamed
+ metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, ctorRef,
+ metadata.GetOrAddBlob (attrBlob));
+ }
+
+ // Write PE
+ var peBuilder = new ManagedPEBuilder (
+ new PEHeaderBuilder (imageCharacteristics: Characteristics.Dll),
+ new MetadataRootBuilder (metadata),
+ ilBuilder);
+ var peBlob = new BlobBuilder ();
+ peBuilder.Serialize (peBlob);
+ using var fs = File.Create (outputPath);
+ peBlob.WriteContentTo (fs);
+ }
+
+ static MemberReferenceHandle AddMemberRef (MetadataBuilder metadata, EntityHandle parent, string name,
+ Action encodeSig)
+ {
+ var blob = new BlobBuilder ();
+ encodeSig (new BlobEncoder (blob));
+ return metadata.AddMemberReference (parent, metadata.GetOrAddString (name), metadata.GetOrAddBlob (blob));
+ }
+}
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..c6f1b8b0748
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs
@@ -0,0 +1,1446 @@
+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;
+
+///
+/// Emits a TypeMap PE assembly from a .
+/// This is a mechanical translation — all decision logic lives in .
+///
+sealed class TypeMapAssemblyEmitter
+{
+ 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;
+
+ AssemblyReferenceHandle _systemRuntimeRef;
+ AssemblyReferenceHandle _monoAndroidRef;
+ AssemblyReferenceHandle _javaInteropRef;
+ AssemblyReferenceHandle _systemRuntimeInteropServicesRef;
+
+ TypeReferenceHandle _javaPeerProxyRef;
+ TypeReferenceHandle _iJavaPeerableRef;
+ TypeReferenceHandle _jniHandleOwnershipRef;
+ TypeReferenceHandle _iAndroidCallableWrapperRef;
+ TypeReferenceHandle _systemTypeRef;
+ TypeReferenceHandle _runtimeTypeHandleRef;
+ TypeReferenceHandle _jniTypeRef;
+ TypeReferenceHandle _trimmableNativeRegistrationRef;
+ TypeReferenceHandle _notSupportedExceptionRef;
+ TypeReferenceHandle _runtimeHelpersRef;
+ TypeReferenceHandle _jniEnvironmentRef;
+ TypeReferenceHandle _jniTransitionRef;
+ TypeReferenceHandle _jniRuntimeRef;
+ TypeReferenceHandle _javaLangObjectRef;
+ TypeReferenceHandle _jniEnvRef;
+ TypeReferenceHandle _charSequenceRef;
+ TypeReferenceHandle _systemExceptionRef;
+ TypeReferenceHandle _iJavaObjectRef;
+
+ MemberReferenceHandle _baseCtorRef;
+ MemberReferenceHandle _getTypeFromHandleRef;
+ MemberReferenceHandle _getUninitializedObjectRef;
+ MemberReferenceHandle _notSupportedExceptionCtorRef;
+ MemberReferenceHandle _registerMethodRef;
+ MemberReferenceHandle _ucoAttrCtorRef;
+ BlobHandle _ucoAttrBlobHandle;
+ MemberReferenceHandle _typeMapAttrCtorRef2Arg;
+ MemberReferenceHandle _typeMapAttrCtorRef3Arg;
+ MemberReferenceHandle _typeMapAssociationAttrCtorRef;
+ MemberReferenceHandle _beginMarshalMethodRef;
+ MemberReferenceHandle _endMarshalMethodRef;
+ MemberReferenceHandle _onUserUnhandledExceptionRef;
+ MemberReferenceHandle _jniEnvGetStringRef;
+ MemberReferenceHandle _jniEnvGetCharSequenceRef;
+ MemberReferenceHandle _jniEnvNewStringRef;
+ MemberReferenceHandle _jniEnvToLocalJniHandleRef;
+ MemberReferenceHandle _charSequenceToLocalJniHandleStringRef;
+ MemberReferenceHandle _jniEnvGetArrayOpenRef;
+ MemberReferenceHandle _jniEnvNewArrayOpenRef;
+ MemberReferenceHandle _jniEnvCopyArrayOpenRef;
+ MemberReferenceHandle _setHandleRef;
+
+ ///
+ /// Creates a new emitter.
+ ///
+ ///
+ /// Version for System.Runtime assembly references.
+ /// Will be derived from $(DotNetTargetVersion) MSBuild property in the build task.
+ ///
+ public TypeMapAssemblyEmitter (Version systemRuntimeVersion)
+ {
+ _systemRuntimeVersion = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion));
+ }
+
+ ///
+ /// Emits a PE assembly from the given model and writes it to .
+ ///
+ public void Emit (TypeMapAssemblyData model, string outputPath)
+ {
+ if (model is null) {
+ throw new ArgumentNullException (nameof (model));
+ }
+ if (outputPath is null) {
+ throw new ArgumentNullException (nameof (outputPath));
+ }
+
+ _asmRefCache.Clear ();
+ _typeRefCache.Clear ();
+
+ var dir = Path.GetDirectoryName (outputPath);
+ if (!string.IsNullOrEmpty (dir)) {
+ Directory.CreateDirectory (dir);
+ }
+
+ var metadata = new MetadataBuilder ();
+ var ilBuilder = new BlobBuilder ();
+
+ EmitAssemblyAndModule (metadata, model);
+ EmitAssemblyReferences (metadata);
+ EmitTypeReferences (metadata);
+ EmitMemberReferences (metadata);
+ EmitModuleType (metadata);
+
+ // Track wrapper method names → handles for RegisterNatives
+ var wrapperHandles = new Dictionary ();
+
+ foreach (var proxy in model.ProxyTypes) {
+ EmitProxyType (metadata, ilBuilder, proxy, wrapperHandles);
+ }
+
+ foreach (var entry in model.Entries) {
+ EmitTypeMapAttribute (metadata, entry);
+ }
+
+ foreach (var assoc in model.Associations) {
+ EmitTypeMapAssociationAttribute (metadata, assoc);
+ }
+
+ EmitIgnoresAccessChecksToAttribute (metadata, ilBuilder, model.IgnoresAccessChecksTo);
+ WritePE (metadata, ilBuilder, outputPath);
+ }
+
+ // ---- Assembly / Module ----
+
+ void EmitAssemblyAndModule (MetadataBuilder metadata, TypeMapAssemblyData model)
+ {
+ metadata.AddAssembly (
+ metadata.GetOrAddString (model.AssemblyName),
+ new Version (1, 0, 0, 0),
+ culture: default,
+ publicKey: default,
+ flags: 0,
+ hashAlgorithm: AssemblyHashAlgorithm.None);
+
+ metadata.AddModule (
+ generation: 0,
+ metadata.GetOrAddString (model.ModuleName),
+ metadata.GetOrAddGuid (Guid.NewGuid ()),
+ encId: default,
+ encBaseId: default);
+ }
+
+ // Mono.Android strong name public key token (84e04ff9cfb79065)
+ static readonly byte [] MonoAndroidPublicKeyToken = { 0x84, 0xe0, 0x4f, 0xf9, 0xcf, 0xb7, 0x90, 0x65 };
+
+ void EmitAssemblyReferences (MetadataBuilder metadata)
+ {
+ _systemRuntimeRef = AddAssemblyRef (metadata, "System.Runtime", _systemRuntimeVersion);
+ _monoAndroidRef = AddAssemblyRef (metadata, "Mono.Android", new Version (0, 0, 0, 0),
+ publicKeyOrToken: MonoAndroidPublicKeyToken);
+ _javaInteropRef = AddAssemblyRef (metadata, "Java.Interop", new Version (0, 0, 0, 0));
+ _systemRuntimeInteropServicesRef = AddAssemblyRef (metadata, "System.Runtime.InteropServices", _systemRuntimeVersion);
+ }
+
+ void EmitTypeReferences (MetadataBuilder metadata)
+ {
+ _javaPeerProxyRef = metadata.AddTypeReference (_monoAndroidRef,
+ metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JavaPeerProxy"));
+ _iJavaPeerableRef = metadata.AddTypeReference (_javaInteropRef,
+ metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("IJavaPeerable"));
+ _jniHandleOwnershipRef = metadata.AddTypeReference (_monoAndroidRef,
+ metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("JniHandleOwnership"));
+ _iAndroidCallableWrapperRef = metadata.AddTypeReference (_monoAndroidRef,
+ metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("IAndroidCallableWrapper"));
+ _systemTypeRef = metadata.AddTypeReference (_systemRuntimeRef,
+ metadata.GetOrAddString ("System"), metadata.GetOrAddString ("Type"));
+ _runtimeTypeHandleRef = metadata.AddTypeReference (_systemRuntimeRef,
+ metadata.GetOrAddString ("System"), metadata.GetOrAddString ("RuntimeTypeHandle"));
+ _jniTypeRef = metadata.AddTypeReference (_javaInteropRef,
+ metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniType"));
+ _trimmableNativeRegistrationRef = metadata.AddTypeReference (_monoAndroidRef,
+ metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("TrimmableNativeRegistration"));
+ _notSupportedExceptionRef = metadata.AddTypeReference (_systemRuntimeRef,
+ metadata.GetOrAddString ("System"), metadata.GetOrAddString ("NotSupportedException"));
+ _runtimeHelpersRef = metadata.AddTypeReference (_systemRuntimeRef,
+ metadata.GetOrAddString ("System.Runtime.CompilerServices"), metadata.GetOrAddString ("RuntimeHelpers"));
+ _jniEnvironmentRef = metadata.AddTypeReference (_javaInteropRef,
+ metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniEnvironment"));
+ _jniTransitionRef = metadata.AddTypeReference (_javaInteropRef,
+ metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniTransition"));
+ _jniRuntimeRef = metadata.AddTypeReference (_javaInteropRef,
+ metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniRuntime"));
+ _javaLangObjectRef = metadata.AddTypeReference (_monoAndroidRef,
+ metadata.GetOrAddString ("Java.Lang"), metadata.GetOrAddString ("Object"));
+ _jniEnvRef = metadata.AddTypeReference (_monoAndroidRef,
+ metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("JNIEnv"));
+ _charSequenceRef = metadata.AddTypeReference (_monoAndroidRef,
+ metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("CharSequence"));
+ _systemExceptionRef = metadata.AddTypeReference (_systemRuntimeRef,
+ metadata.GetOrAddString ("System"), metadata.GetOrAddString ("Exception"));
+ _iJavaObjectRef = metadata.AddTypeReference (_javaInteropRef,
+ metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("IJavaObject"));
+ }
+
+ void EmitMemberReferences (MetadataBuilder metadata)
+ {
+ _baseCtorRef = AddMemberRef (metadata, _javaPeerProxyRef, ".ctor",
+ sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { }));
+
+ _getTypeFromHandleRef = AddMemberRef (metadata, _systemTypeRef, "GetTypeFromHandle",
+ sig => sig.MethodSignature ().Parameters (1,
+ rt => rt.Type ().Type (_systemTypeRef, false),
+ p => p.AddParameter ().Type ().Type (_runtimeTypeHandleRef, true)));
+
+ _getUninitializedObjectRef = AddMemberRef (metadata, _runtimeHelpersRef, "GetUninitializedObject",
+ sig => sig.MethodSignature ().Parameters (1,
+ rt => rt.Type ().Object (),
+ p => p.AddParameter ().Type ().Type (_systemTypeRef, false)));
+
+ _notSupportedExceptionCtorRef = AddMemberRef (metadata, _notSupportedExceptionRef, ".ctor",
+ sig => sig.MethodSignature (isInstanceMethod: true).Parameters (1,
+ rt => rt.Void (),
+ p => p.AddParameter ().Type ().String ()));
+
+ _registerMethodRef = AddMemberRef (metadata, _trimmableNativeRegistrationRef, "RegisterMethod",
+ sig => sig.MethodSignature ().Parameters (4,
+ rt => rt.Void (),
+ p => {
+ p.AddParameter ().Type ().Type (_jniTypeRef, false);
+ p.AddParameter ().Type ().String ();
+ p.AddParameter ().Type ().String ();
+ p.AddParameter ().Type ().IntPtr ();
+ }));
+
+ var ucoAttrTypeRef = metadata.AddTypeReference (_systemRuntimeInteropServicesRef,
+ metadata.GetOrAddString ("System.Runtime.InteropServices"),
+ metadata.GetOrAddString ("UnmanagedCallersOnlyAttribute"));
+ _ucoAttrCtorRef = AddMemberRef (metadata, ucoAttrTypeRef, ".ctor",
+ sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { }));
+
+ // Pre-compute the UCO attribute blob — it's always the same 4 bytes (prolog + no named args)
+ _attrBlob.Clear ();
+ _attrBlob.WriteUInt16 (1);
+ _attrBlob.WriteUInt16 (0);
+ _ucoAttrBlobHandle = metadata.GetOrAddBlob (_attrBlob);
+
+ // Marshal method support: BeginMarshalMethod, EndMarshalMethod, GetObject, GetString, etc.
+ // BeginMarshalMethod(IntPtr jnienv, out JniTransition, out JniRuntime) : bool
+ _beginMarshalMethodRef = AddMemberRef (metadata, _jniEnvironmentRef, "BeginMarshalMethod",
+ sig => sig.MethodSignature ().Parameters (3,
+ rt => rt.Type ().Boolean (),
+ p => {
+ p.AddParameter ().Type ().IntPtr ();
+ p.AddParameter ().Type (isByRef: true).Type (_jniTransitionRef, true);
+ p.AddParameter ().Type (isByRef: true).Type (_jniRuntimeRef, false);
+ }));
+
+ // EndMarshalMethod(ref JniTransition)
+ _endMarshalMethodRef = AddMemberRef (metadata, _jniEnvironmentRef, "EndMarshalMethod",
+ sig => sig.MethodSignature ().Parameters (1,
+ rt => rt.Void (),
+ p => p.AddParameter ().Type (isByRef: true).Type (_jniTransitionRef, true)));
+
+ // JniRuntime.OnUserUnhandledException(ref JniTransition, Exception) — virtual instance method
+ _onUserUnhandledExceptionRef = AddMemberRef (metadata, _jniRuntimeRef, "OnUserUnhandledException",
+ sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2,
+ rt => rt.Void (),
+ p => {
+ p.AddParameter ().Type (isByRef: true).Type (_jniTransitionRef, true);
+ p.AddParameter ().Type ().Type (_systemExceptionRef, false);
+ }));
+
+ // JNIEnv.GetString(IntPtr, JniHandleOwnership) : string
+ _jniEnvGetStringRef = AddMemberRef (metadata, _jniEnvRef, "GetString",
+ sig => sig.MethodSignature ().Parameters (2,
+ rt => rt.Type ().String (),
+ p => {
+ p.AddParameter ().Type ().IntPtr ();
+ p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true);
+ }));
+
+ // JNIEnv.GetCharSequence(IntPtr, JniHandleOwnership) : string
+ _jniEnvGetCharSequenceRef = AddMemberRef (metadata, _jniEnvRef, "GetCharSequence",
+ sig => sig.MethodSignature ().Parameters (2,
+ rt => rt.Type ().String (),
+ p => {
+ p.AddParameter ().Type ().IntPtr ();
+ p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true);
+ }));
+
+ // JNIEnv.NewString(string) : IntPtr
+ _jniEnvNewStringRef = AddMemberRef (metadata, _jniEnvRef, "NewString",
+ sig => sig.MethodSignature ().Parameters (1,
+ rt => rt.Type ().IntPtr (),
+ p => p.AddParameter ().Type ().String ()));
+
+ // CharSequence.ToLocalJniHandle(string) : IntPtr
+ _charSequenceToLocalJniHandleStringRef = AddMemberRef (metadata, _charSequenceRef, "ToLocalJniHandle",
+ sig => sig.MethodSignature ().Parameters (1,
+ rt => rt.Type ().IntPtr (),
+ p => p.AddParameter ().Type ().String ()));
+
+ // JNIEnv.ToLocalJniHandle(IJavaObject) : IntPtr
+ _jniEnvToLocalJniHandleRef = AddMemberRef (metadata, _jniEnvRef, "ToLocalJniHandle",
+ sig => sig.MethodSignature ().Parameters (1,
+ rt => rt.Type ().IntPtr (),
+ p => p.AddParameter ().Type ().Type (_iJavaObjectRef, false)));
+
+ // JNIEnv.GetArray(IntPtr) : T[]
+ _jniEnvGetArrayOpenRef = AddMemberRef (metadata, _jniEnvRef, "GetArray",
+ sig => {
+ var methodSig = sig.MethodSignature (genericParameterCount: 1);
+ methodSig.Parameters (1,
+ rt => rt.Type ().SZArray ().GenericMethodTypeParameter (0),
+ p => p.AddParameter ().Type ().IntPtr ());
+ });
+
+ // JNIEnv.NewArray(T[]) : IntPtr
+ _jniEnvNewArrayOpenRef = AddMemberRef (metadata, _jniEnvRef, "NewArray",
+ sig => {
+ var methodSig = sig.MethodSignature (genericParameterCount: 1);
+ methodSig.Parameters (1,
+ rt => rt.Type ().IntPtr (),
+ p => p.AddParameter ().Type ().SZArray ().GenericMethodTypeParameter (0));
+ });
+
+ // JNIEnv.CopyArray(T[], IntPtr) : void
+ _jniEnvCopyArrayOpenRef = AddMemberRef (metadata, _jniEnvRef, "CopyArray",
+ sig => {
+ var methodSig = sig.MethodSignature (genericParameterCount: 1);
+ methodSig.Parameters (2,
+ rt => rt.Void (),
+ p => {
+ p.AddParameter ().Type ().SZArray ().GenericMethodTypeParameter (0);
+ p.AddParameter ().Type ().IntPtr ();
+ });
+ });
+
+ // Java.Lang.Object.SetHandle(IntPtr, JniHandleOwnership) : void — protected instance method
+ _setHandleRef = AddMemberRef (metadata, _javaLangObjectRef, "SetHandle",
+ sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2,
+ rt => rt.Void (),
+ p => {
+ p.AddParameter ().Type ().IntPtr ();
+ p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true);
+ }));
+
+ EmitTypeMapAttributeCtorRef (metadata);
+ EmitTypeMapAssociationAttributeCtorRef (metadata);
+ }
+
+ void EmitTypeMapAttributeCtorRef (MetadataBuilder metadata)
+ {
+ var typeMapAttrOpenRef = metadata.AddTypeReference (_systemRuntimeInteropServicesRef,
+ metadata.GetOrAddString ("System.Runtime.InteropServices"),
+ metadata.GetOrAddString ("TypeMapAttribute`1"));
+ var javaLangObjectRef = metadata.AddTypeReference (_monoAndroidRef,
+ metadata.GetOrAddString ("Java.Lang"), metadata.GetOrAddString ("Object"));
+
+ var genericInstBlob = new BlobBuilder ();
+ genericInstBlob.WriteByte (0x15); // ELEMENT_TYPE_GENERICINST
+ genericInstBlob.WriteByte (0x12); // ELEMENT_TYPE_CLASS
+ genericInstBlob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (typeMapAttrOpenRef));
+ genericInstBlob.WriteCompressedInteger (1);
+ genericInstBlob.WriteByte (0x12); // ELEMENT_TYPE_CLASS
+ genericInstBlob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (javaLangObjectRef));
+ var closedAttrTypeSpec = metadata.AddTypeSpecification (metadata.GetOrAddBlob (genericInstBlob));
+
+ // 2-arg: TypeMap(string jniName, Type proxyType) — unconditional
+ _typeMapAttrCtorRef2Arg = AddMemberRef (metadata, 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 = AddMemberRef (metadata, 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 (MetadataBuilder metadata)
+ {
+ // TypeMapAssociationAttribute is in System.Runtime.InteropServices, takes 2 Type args:
+ // TypeMapAssociation(Type sourceType, Type aliasProxyType)
+ var typeMapAssociationAttrRef = metadata.AddTypeReference (_systemRuntimeInteropServicesRef,
+ metadata.GetOrAddString ("System.Runtime.InteropServices"),
+ metadata.GetOrAddString ("TypeMapAssociationAttribute"));
+
+ _typeMapAssociationAttrCtorRef = AddMemberRef (metadata, 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 EmitModuleType (MetadataBuilder metadata)
+ {
+ metadata.AddTypeDefinition (
+ default, default,
+ metadata.GetOrAddString (""),
+ default,
+ MetadataTokens.FieldDefinitionHandle (1),
+ MetadataTokens.MethodDefinitionHandle (1));
+ }
+
+ // ---- Proxy types ----
+
+ void EmitProxyType (MetadataBuilder metadata, BlobBuilder ilBuilder, JavaPeerProxyData proxy,
+ Dictionary wrapperHandles)
+ {
+ var typeDefHandle = 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));
+
+ if (proxy.IsAcw) {
+ metadata.AddInterfaceImplementation (typeDefHandle, _iAndroidCallableWrapperRef);
+ }
+
+ // .ctor
+ EmitBody (metadata, ilBuilder, ".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 (metadata, ilBuilder, proxy);
+
+ // get_TargetType
+ EmitTypeGetter (metadata, ilBuilder, "get_TargetType", proxy.TargetType,
+ MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.SpecialName | MethodAttributes.HideBySig);
+
+ // get_InvokerType
+ if (proxy.InvokerType != null) {
+ EmitTypeGetter (metadata, ilBuilder, "get_InvokerType", proxy.InvokerType,
+ MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig);
+ }
+
+ // UCO wrappers (methods and constructors with [Register] connectors)
+ foreach (var uco in proxy.UcoMethods) {
+ var handle = EmitUcoMethod (metadata, ilBuilder, uco);
+ wrapperHandles [uco.WrapperName] = handle;
+ }
+
+ // Export marshal method wrappers (full marshal body)
+ foreach (var export in proxy.ExportMarshalMethods) {
+ var handle = EmitExportMarshalMethod (metadata, ilBuilder, export);
+ wrapperHandles [export.WrapperName] = handle;
+ }
+
+ // RegisterNatives
+ if (proxy.IsAcw) {
+ EmitRegisterNatives (metadata, ilBuilder, proxy.NativeRegistrations, wrapperHandles);
+ }
+ }
+
+ void EmitCreateInstance (MetadataBuilder metadata, BlobBuilder ilBuilder, JavaPeerProxyData proxy)
+ {
+ if (!proxy.HasActivation) {
+ EmitCreateInstanceBody (metadata, ilBuilder, encoder => {
+ encoder.OpCode (ILOpCode.Ldnull);
+ encoder.OpCode (ILOpCode.Ret);
+ });
+ return;
+ }
+
+ // Generic type definitions cannot be instantiated
+ if (proxy.IsGenericDefinition) {
+ EmitCreateInstanceBody (metadata, ilBuilder, encoder => {
+ encoder.LoadString (metadata.GetOrAddUserString ("Cannot create instance of open generic type."));
+ encoder.OpCode (ILOpCode.Newobj);
+ encoder.Token (_notSupportedExceptionCtorRef);
+ encoder.OpCode (ILOpCode.Throw);
+ });
+ return;
+ }
+
+ // Interface with invoker: new TInvoker(IntPtr, JniHandleOwnership)
+ if (proxy.InvokerType != null) {
+ var invokerCtorRef = AddActivationCtorRef (metadata, ResolveTypeRef (metadata, proxy.InvokerType));
+ EmitCreateInstanceBody (metadata, ilBuilder, encoder => {
+ encoder.OpCode (ILOpCode.Ldarg_1);
+ encoder.OpCode (ILOpCode.Ldarg_2);
+ encoder.OpCode (ILOpCode.Newobj);
+ encoder.Token (invokerCtorRef);
+ encoder.OpCode (ILOpCode.Ret);
+ });
+ return;
+ }
+
+ // At this point, ActivationCtor is guaranteed non-null (HasActivation && InvokerType == null)
+ var activationCtor = proxy.ActivationCtor ?? throw new InvalidOperationException ("ActivationCtor should not be null when HasActivation is true and InvokerType is null");
+ var targetTypeRef = ResolveTypeRef (metadata, proxy.TargetType);
+
+ if (activationCtor.IsOnLeafType) {
+ // Leaf type has its own ctor: new T(IntPtr, JniHandleOwnership)
+ var ctorRef = AddActivationCtorRef (metadata, targetTypeRef);
+ EmitCreateInstanceBody (metadata, ilBuilder, encoder => {
+ encoder.OpCode (ILOpCode.Ldarg_1);
+ encoder.OpCode (ILOpCode.Ldarg_2);
+ encoder.OpCode (ILOpCode.Newobj);
+ encoder.Token (ctorRef);
+ encoder.OpCode (ILOpCode.Ret);
+ });
+ } else {
+ // Inherited ctor: GetUninitializedObject(typeof(T)) then call BaseType::.ctor(IntPtr, JniHandleOwnership)
+ // The base ctor does SetHandle + any other initialization the base class needs.
+ var baseTypeRef = ResolveTypeRef (metadata, activationCtor.DeclaringType);
+ var baseCtorRef = AddActivationCtorRef (metadata, baseTypeRef);
+ EmitCreateInstanceBody (metadata, ilBuilder, 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 (baseCtorRef);
+
+ encoder.OpCode (ILOpCode.Ret);
+ });
+ }
+ }
+
+ void EmitCreateInstanceBody (MetadataBuilder metadata, BlobBuilder ilBuilder, Action emitIL)
+ {
+ EmitBody (metadata, ilBuilder, "CreateInstance",
+ MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig,
+ sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2,
+ rt => rt.Type ().Type (_iJavaPeerableRef, false),
+ p => {
+ p.AddParameter ().Type ().IntPtr ();
+ p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true);
+ }),
+ emitIL);
+ }
+
+ MemberReferenceHandle AddActivationCtorRef (MetadataBuilder metadata, EntityHandle declaringTypeRef)
+ {
+ return AddMemberRef (metadata, 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 (MetadataBuilder metadata, BlobBuilder ilBuilder, string methodName,
+ TypeRefData typeRef, MethodAttributes attrs)
+ {
+ var handle = ResolveTypeRef (metadata, typeRef);
+
+ EmitBody (metadata, ilBuilder, 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);
+ });
+ }
+
+ // ---- UCO wrappers ----
+
+ MethodDefinitionHandle EmitUcoMethod (MetadataBuilder metadata, BlobBuilder ilBuilder, UcoMethodData uco)
+ {
+ var jniParams = JniSignatureHelper.ParseParameterTypes (uco.JniSignature);
+ var returnKind = JniSignatureHelper.ParseReturnType (uco.JniSignature);
+ int paramCount = 2 + jniParams.Count;
+ bool isVoid = returnKind == JniParamKind.Void;
+
+ Action encodeSig = sig => sig.MethodSignature ().Parameters (paramCount,
+ rt => { if (isVoid) rt.Void (); else JniSignatureHelper.EncodeClrType (rt.Type (), returnKind); },
+ p => {
+ p.AddParameter ().Type ().IntPtr ();
+ p.AddParameter ().Type ().IntPtr ();
+ for (int j = 0; j < jniParams.Count; j++)
+ JniSignatureHelper.EncodeClrType (p.AddParameter ().Type (), jniParams [j]);
+ });
+
+ var callbackTypeHandle = ResolveTypeRef (metadata, uco.CallbackType);
+ var callbackRef = AddMemberRef (metadata, callbackTypeHandle, uco.CallbackMethodName, encodeSig);
+
+ var handle = EmitBody (metadata, ilBuilder, uco.WrapperName,
+ MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig,
+ encodeSig,
+ encoder => {
+ for (int p = 0; p < paramCount; p++)
+ encoder.LoadArgument (p);
+ encoder.Call (callbackRef);
+ encoder.OpCode (ILOpCode.Ret);
+ });
+
+ AddUnmanagedCallersOnlyAttribute (metadata, handle);
+ return handle;
+ }
+
+ // ---- Export marshal method wrappers ----
+
+ ///
+ /// Emits a full marshal method body for an [Export] method or constructor.
+ /// Pattern for methods:
+ /// static RetType n_Method(IntPtr jnienv, IntPtr native__this, <JNI params...>) {
+ /// if (!JniEnvironment.BeginMarshalMethod(jnienv, out var __envp, out var __r)) return default;
+ /// try {
+ /// var __this = Object.GetObject<T>(jnienv, native__this, DoNotTransfer);
+ /// // unmarshal params, call managed method, marshal return
+ /// } catch / finally ...
+ /// }
+ /// Pattern for constructors:
+ /// static void nctor_N_uco(IntPtr jnienv, IntPtr native__this, <ctor params...>) {
+ /// if (!JniEnvironment.BeginMarshalMethod(jnienv, out var __envp, out var __r)) return;
+ /// try {
+ /// var __this = (T)RuntimeHelpers.GetUninitializedObject(typeof(T));
+ /// __this.SetHandle(native__this, DoNotTransfer); // registers peer with runtime
+ /// __this..ctor(params...); // user constructor
+ /// } catch / finally ...
+ /// }
+ ///
+ MethodDefinitionHandle EmitExportMarshalMethod (MetadataBuilder metadata, BlobBuilder ilBuilder, ExportMarshalMethodData export)
+ {
+ var jniParams = JniSignatureHelper.ParseParameterTypes (export.JniSignature);
+ var jniParamTypes = JniSignatureHelper.ParseParameterTypeStrings (export.JniSignature);
+ var jniReturnType = export.IsConstructor ? "V" : JniSignatureHelper.ParseReturnTypeString (export.JniSignature);
+ var returnKind = export.IsConstructor ? JniParamKind.Void : JniSignatureHelper.ParseReturnType (export.JniSignature);
+ bool isVoid = returnKind == JniParamKind.Void;
+ int jniParamCount = 2 + jniParams.Count; // jnienv + self + method params
+
+ // Build the method signature
+ Action encodeSig = sig => sig.MethodSignature ().Parameters (jniParamCount,
+ rt => { if (isVoid) rt.Void (); else JniSignatureHelper.EncodeClrType (rt.Type (), returnKind); },
+ p => {
+ p.AddParameter ().Type ().IntPtr (); // jnienv
+ p.AddParameter ().Type ().IntPtr (); // native__this
+ for (int j = 0; j < jniParams.Count; j++)
+ JniSignatureHelper.EncodeClrType (p.AddParameter ().Type (), jniParams [j]);
+ });
+
+ // Resolve managed type references (needed early for locals signature in constructor case)
+ var declaringTypeRef = ResolveTypeRef (metadata, export.DeclaringType);
+
+ // Build the locals signature:
+ // local 0: JniTransition __envp
+ // local 1: JniRuntime __r
+ // local 2: Exception __e
+ // local 3: __ret (only for non-void methods)
+ // local 3 (ctor): T __this (for constructors — holds the uninitialized object)
+ int fixedLocalCount = export.IsConstructor || !isVoid ? 4 : 3;
+ int parameterLocalStart = fixedLocalCount;
+ int[] parameterLocals = new int [export.ManagedParameters.Count];
+ int localCount = fixedLocalCount + export.ManagedParameters.Count;
+ var localsBlob = new BlobBuilder (32);
+ var localsEncoder = new BlobEncoder (localsBlob).LocalVariableSignature (localCount);
+ localsEncoder.AddVariable ().Type ().Type (_jniTransitionRef, true); // local 0
+ localsEncoder.AddVariable ().Type ().Type (_jniRuntimeRef, false); // local 1
+ localsEncoder.AddVariable ().Type ().Type (_systemExceptionRef, false); // local 2
+ if (export.IsConstructor) {
+ localsEncoder.AddVariable ().Type ().Type (declaringTypeRef, false); // local 3: T __this
+ } else if (!isVoid) {
+ JniSignatureHelper.EncodeClrType (localsEncoder.AddVariable ().Type (), returnKind); // local 3: __ret
+ }
+ for (int i = 0; i < export.ManagedParameters.Count; i++) {
+ parameterLocals [i] = parameterLocalStart + i;
+ EncodeManagedTypeForExportCall (localsEncoder.AddVariable ().Type (), metadata,
+ export.ManagedParameters [i].ManagedTypeName, export.ManagedParameters [i].AssemblyName, jniParams [i], export.ManagedParameters [i].JniType);
+ }
+ var localsSigHandle = metadata.AddStandaloneSignature (metadata.GetOrAddBlob (localsBlob));
+
+ // Build GetObject method spec — generic instantiation of Object.GetObject
+ // Not needed for static methods or constructors
+ EntityHandle getObjectRef = default;
+ if (!export.IsStatic && !export.IsConstructor) {
+ getObjectRef = BuildGetObjectMethodSpec (metadata, declaringTypeRef);
+ }
+
+ // Resolve managed method to call
+ MemberReferenceHandle managedMethodRef;
+ if (export.IsConstructor) {
+ managedMethodRef = BuildExportCtorRef (metadata, export, declaringTypeRef);
+ } else {
+ managedMethodRef = BuildExportMethodRef (metadata, export, declaringTypeRef);
+ }
+
+ // Build the IL with ControlFlowBuilder for try/catch/finally
+ var cfBuilder = new ControlFlowBuilder ();
+ _codeBlob.Clear ();
+ var encoder = new InstructionEncoder (_codeBlob, cfBuilder);
+
+ // Define labels
+ var tryStartLabel = encoder.DefineLabel ();
+ var tryEndLabel = encoder.DefineLabel ();
+ var catchStartLabel = encoder.DefineLabel ();
+ var catchEndLabel = encoder.DefineLabel ();
+ var finallyStartLabel = encoder.DefineLabel ();
+ var finallyEndLabel = encoder.DefineLabel ();
+ var returnLabel = encoder.DefineLabel ();
+
+ // --- if (!BeginMarshalMethod(jnienv, out __envp, out __r)) return default; ---
+ encoder.LoadArgument (0); // jnienv
+ encoder.OpCode (ILOpCode.Ldloca_s); encoder.CodeBuilder.WriteByte (0); // out __envp
+ encoder.OpCode (ILOpCode.Ldloca_s); encoder.CodeBuilder.WriteByte (1); // out __r
+ encoder.Call (_beginMarshalMethodRef);
+ encoder.Branch (ILOpCode.Brtrue_s, tryStartLabel);
+ // return default
+ if (!isVoid) {
+ EmitDefaultReturnValue (encoder, returnKind);
+ }
+ encoder.OpCode (ILOpCode.Ret);
+
+ // --- try { ---
+ encoder.MarkLabel (tryStartLabel);
+
+ if (export.IsConstructor) {
+ // Constructor: create uninitialized object, call activation ctor, then user ctor
+ // var __this = (T)RuntimeHelpers.GetUninitializedObject(typeof(T));
+ encoder.OpCode (ILOpCode.Ldtoken);
+ encoder.Token (declaringTypeRef);
+ encoder.Call (_getTypeFromHandleRef);
+ encoder.Call (_getUninitializedObjectRef);
+ encoder.OpCode (ILOpCode.Castclass);
+ encoder.Token (declaringTypeRef);
+ encoder.OpCode (ILOpCode.Stloc_3); // store in local 3: __this
+
+ // __this.SetHandle(native__this, JniHandleOwnership.DoNotTransfer)
+ // — registers the peer with the runtime and sets up the JNI handle association
+ encoder.OpCode (ILOpCode.Ldloc_3); // __this
+ encoder.LoadArgument (1); // native__this
+ encoder.OpCode (ILOpCode.Ldc_i4_0); // JniHandleOwnership.DoNotTransfer = 0
+ encoder.Call (_setHandleRef);
+ } else if (!export.IsStatic) {
+ // Instance method: get managed object from JNI handle
+ encoder.LoadArgument (0); // jnienv
+ encoder.LoadArgument (1); // native__this
+ encoder.OpCode (ILOpCode.Ldc_i4_0); // JniHandleOwnership.DoNotTransfer = 0
+ encoder.Call (getObjectRef);
+ }
+
+ // Unmarshal each parameter into locals
+ for (int i = 0; i < export.ManagedParameters.Count; i++) {
+ EmitParameterUnmarshal (encoder, metadata, export.ManagedParameters [i], jniParams [i], jniParamTypes [i], i + 2);
+ StoreLocal (encoder, parameterLocals [i]);
+ }
+
+ // Load target + managed parameters for the managed call
+ if (export.IsConstructor) {
+ encoder.OpCode (ILOpCode.Ldloc_3);
+ }
+ for (int i = 0; i < export.ManagedParameters.Count; i++) {
+ LoadLocal (encoder, parameterLocals [i]);
+ }
+
+ // Call managed method: static → call, instance ctor → call, instance method → callvirt
+ if (export.IsStatic || export.IsConstructor) {
+ encoder.Call (managedMethodRef);
+ } else {
+ encoder.OpCode (ILOpCode.Callvirt);
+ encoder.Token (managedMethodRef);
+ }
+
+ // Marshal return value and store in local 3
+ if (!isVoid) {
+ EmitReturnMarshal (encoder, metadata, returnKind, jniReturnType, export.ManagedReturnType);
+ encoder.OpCode (ILOpCode.Stloc_3);
+ }
+
+ // Copy back array parameter changes to JNI arrays
+ for (int i = 0; i < export.ManagedParameters.Count; i++) {
+ if (IsManagedArrayType (export.ManagedParameters [i].ManagedTypeName)) {
+ EmitArrayParameterCopyBack (encoder, metadata, export.ManagedParameters [i], parameterLocals [i], i + 2);
+ }
+ }
+
+ // leave to after the handler
+ encoder.Branch (ILOpCode.Leave_s, returnLabel);
+ encoder.MarkLabel (tryEndLabel);
+
+ // --- } catch (Exception __e) { ---
+ encoder.MarkLabel (catchStartLabel);
+ encoder.OpCode (ILOpCode.Stloc_2); // store exception in local 2
+ encoder.OpCode (ILOpCode.Ldloc_1); // __r
+ encoder.OpCode (ILOpCode.Ldloca_s); encoder.CodeBuilder.WriteByte (0); // ref __envp
+ encoder.OpCode (ILOpCode.Ldloc_2); // __e
+ encoder.OpCode (ILOpCode.Callvirt);
+ encoder.Token (_onUserUnhandledExceptionRef);
+ if (!isVoid) {
+ EmitDefaultReturnValue (encoder, returnKind);
+ encoder.OpCode (ILOpCode.Stloc_3);
+ }
+ encoder.Branch (ILOpCode.Leave_s, returnLabel);
+ encoder.MarkLabel (catchEndLabel);
+
+ // --- } finally { ---
+ encoder.MarkLabel (finallyStartLabel);
+ encoder.OpCode (ILOpCode.Ldloca_s); encoder.CodeBuilder.WriteByte (0); // ref __envp
+ encoder.Call (_endMarshalMethodRef);
+ encoder.OpCode (ILOpCode.Endfinally);
+ encoder.MarkLabel (finallyEndLabel);
+
+ // --- return ---
+ encoder.MarkLabel (returnLabel);
+ if (!isVoid) {
+ encoder.OpCode (ILOpCode.Ldloc_3);
+ }
+ encoder.OpCode (ILOpCode.Ret);
+
+ // Add exception regions
+ cfBuilder.AddCatchRegion (tryStartLabel, tryEndLabel, catchStartLabel, catchEndLabel, _systemExceptionRef);
+ cfBuilder.AddFinallyRegion (tryStartLabel, catchEndLabel, finallyStartLabel, finallyEndLabel);
+
+ // Emit the method with fat body (locals + exception handlers)
+ _sigBlob.Clear ();
+ encodeSig (new BlobEncoder (_sigBlob));
+
+ while (ilBuilder.Count % 4 != 0) {
+ ilBuilder.WriteByte (0);
+ }
+ var bodyEncoder = new MethodBodyStreamEncoder (ilBuilder);
+ int bodyOffset = bodyEncoder.AddMethodBody (encoder, maxStack: 8, localsSigHandle, MethodBodyAttributes.InitLocals);
+
+ var handle = metadata.AddMethodDefinition (
+ MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig,
+ MethodImplAttributes.IL,
+ metadata.GetOrAddString (export.WrapperName),
+ metadata.GetOrAddBlob (_sigBlob),
+ bodyOffset, default);
+
+ AddUnmanagedCallersOnlyAttribute (metadata, handle);
+ return handle;
+ }
+
+ ///
+ /// Builds a MethodSpec for Object.GetObject<T>(IntPtr, IntPtr, JniHandleOwnership).
+ ///
+ EntityHandle BuildGetObjectMethodSpec (MetadataBuilder metadata, EntityHandle managedTypeRef)
+ {
+ // Object.GetObject(IntPtr jnienv, IntPtr handle, JniHandleOwnership transfer) : T
+ var openGetObjectRef = AddMemberRef (metadata, _javaLangObjectRef, "GetObject",
+ sig => {
+ var methodSig = sig.MethodSignature (genericParameterCount: 1);
+ methodSig.Parameters (3,
+ rt => rt.Type ().GenericMethodTypeParameter (0),
+ p => {
+ p.AddParameter ().Type ().IntPtr ();
+ p.AddParameter ().Type ().IntPtr ();
+ p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true);
+ });
+ });
+
+ // Build generic instantiation blob: GetObject
+ var instBlob = new BlobBuilder (16);
+ instBlob.WriteByte (0x0A); // ELEMENT_TYPE_GENERICINST (for method)
+ instBlob.WriteCompressedInteger (1); // 1 type argument
+ instBlob.WriteByte (0x12); // ELEMENT_TYPE_CLASS
+ instBlob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (managedTypeRef));
+
+ return metadata.AddMethodSpecification (openGetObjectRef, metadata.GetOrAddBlob (instBlob));
+ }
+
+ MemberReferenceHandle BuildExportCtorRef (MetadataBuilder metadata, ExportMarshalMethodData export, EntityHandle declaringTypeRef)
+ {
+ int paramCount = export.ManagedParameters.Count;
+ return AddMemberRef (metadata, declaringTypeRef, ".ctor",
+ sig => sig.MethodSignature (isInstanceMethod: true).Parameters (paramCount,
+ rt => rt.Void (),
+ p => {
+ foreach (var param in export.ManagedParameters)
+ EncodeExportParamType (p, metadata, param);
+ }));
+ }
+
+ MemberReferenceHandle BuildExportMethodRef (MetadataBuilder metadata, ExportMarshalMethodData export, EntityHandle declaringTypeRef)
+ {
+ int paramCount = export.ManagedParameters.Count;
+ var returnKind = JniSignatureHelper.ParseReturnType (export.JniSignature);
+ bool isVoid = returnKind == JniParamKind.Void;
+
+ return AddMemberRef (metadata, declaringTypeRef, export.ManagedMethodName,
+ sig => sig.MethodSignature (isInstanceMethod: !export.IsStatic).Parameters (paramCount,
+ rt => {
+ if (isVoid) {
+ rt.Void ();
+ } else {
+ EncodeExportReturnType (rt, metadata, export.ManagedReturnType, returnKind, JniSignatureHelper.ParseReturnTypeString (export.JniSignature));
+ }
+ },
+ p => {
+ foreach (var param in export.ManagedParameters)
+ EncodeExportParamType (p, metadata, param);
+ }));
+ }
+
+ void EncodeExportParamType (ParametersEncoder p, MetadataBuilder metadata, ExportParamData param)
+ {
+ var jniKind = JniSignatureHelper.ParseSingleTypeFromDescriptor (param.JniType);
+ if (!string.IsNullOrEmpty (param.ManagedTypeName)) {
+ EncodeManagedTypeForExportCall (p.AddParameter ().Type (), metadata, param.ManagedTypeName, param.AssemblyName, jniKind, param.JniType);
+ } else if (jniKind != JniParamKind.Object) {
+ JniSignatureHelper.EncodeClrType (p.AddParameter ().Type (), jniKind);
+ } else {
+ p.AddParameter ().Type ().IntPtr ();
+ }
+ }
+
+ void EncodeExportReturnType (ReturnTypeEncoder rt, MetadataBuilder metadata, string? managedReturnType, JniParamKind returnKind, string jniReturnType)
+ {
+ if (!string.IsNullOrEmpty (managedReturnType)) {
+ string typeName = managedReturnType!;
+ string assemblyName = "";
+ int commaIndex = typeName.IndexOf (", ", StringComparison.Ordinal);
+ if (commaIndex >= 0) {
+ assemblyName = typeName.Substring (commaIndex + 2);
+ typeName = typeName.Substring (0, commaIndex);
+ }
+ EncodeManagedTypeForExportCall (rt.Type (), metadata, typeName, assemblyName, returnKind, jniReturnType);
+ } else if (returnKind != JniParamKind.Object) {
+ JniSignatureHelper.EncodeClrType (rt.Type (), returnKind);
+ } else {
+ rt.Type ().IntPtr ();
+ }
+ }
+
+ void EncodeManagedTypeForExportCall (SignatureTypeEncoder encoder, MetadataBuilder metadata,
+ string managedTypeName, string assemblyName, JniParamKind jniKind, string jniType)
+ {
+ if (TryEncodeManagedPrimitiveType (encoder, managedTypeName)) {
+ return;
+ }
+
+ if (managedTypeName == "System.String") {
+ encoder.String ();
+ return;
+ }
+
+ if (IsManagedArrayType (managedTypeName)) {
+ EncodeManagedArrayType (encoder, metadata, managedTypeName, assemblyName, jniType);
+ return;
+ }
+
+ var typeRef = ResolveTypeRef (metadata, new TypeRefData {
+ ManagedTypeName = managedTypeName,
+ AssemblyName = assemblyName,
+ });
+ encoder.Type (typeRef, IsEnumManagedType (managedTypeName, jniKind));
+ }
+
+ void EncodeManagedArrayType (SignatureTypeEncoder encoder, MetadataBuilder metadata, string managedArrayTypeName, string assemblyName, string jniType)
+ {
+ string elementType = managedArrayTypeName.Substring (0, managedArrayTypeName.Length - 2);
+ var arrayEncoder = encoder.SZArray ();
+ if (TryEncodeManagedPrimitiveType (arrayEncoder, elementType)) {
+ return;
+ }
+ if (elementType == "System.String") {
+ arrayEncoder.String ();
+ return;
+ }
+ var elementRef = ResolveTypeRef (metadata, new TypeRefData {
+ ManagedTypeName = elementType,
+ AssemblyName = assemblyName,
+ });
+ var elementJniKind = jniType.StartsWith ("[", StringComparison.Ordinal)
+ ? JniSignatureHelper.ParseSingleTypeFromDescriptor (jniType.Substring (1))
+ : JniParamKind.Object;
+ arrayEncoder.Type (elementRef, IsEnumManagedType (elementType, elementJniKind));
+ }
+
+ static bool TryEncodeManagedPrimitiveType (SignatureTypeEncoder encoder, string managedTypeName)
+ {
+ switch (managedTypeName) {
+ case "System.Boolean": encoder.Boolean (); return true;
+ case "System.SByte": encoder.SByte (); return true;
+ case "System.Byte": encoder.Byte (); return true;
+ case "System.Char": encoder.Char (); return true;
+ case "System.Int16": encoder.Int16 (); return true;
+ case "System.UInt16": encoder.UInt16 (); return true;
+ case "System.Int32": encoder.Int32 (); return true;
+ case "System.UInt32": encoder.UInt32 (); return true;
+ case "System.Int64": encoder.Int64 (); return true;
+ case "System.UInt64": encoder.UInt64 (); return true;
+ case "System.Single": encoder.Single (); return true;
+ case "System.Double": encoder.Double (); return true;
+ case "System.IntPtr": encoder.IntPtr (); return true;
+ case "System.UIntPtr": encoder.UIntPtr (); return true;
+ default: return false;
+ }
+ }
+
+ static bool IsManagedArrayType (string managedTypeName)
+ => managedTypeName.EndsWith ("[]", StringComparison.Ordinal);
+
+ static bool IsEnumManagedType (string managedTypeName, JniParamKind jniKind)
+ {
+ if (jniKind == JniParamKind.Object || IsManagedArrayType (managedTypeName)) {
+ return false;
+ }
+ return !string.Equals (managedTypeName, "System.Boolean", StringComparison.Ordinal) &&
+ !string.Equals (managedTypeName, "System.SByte", StringComparison.Ordinal) &&
+ !string.Equals (managedTypeName, "System.Byte", StringComparison.Ordinal) &&
+ !string.Equals (managedTypeName, "System.Char", StringComparison.Ordinal) &&
+ !string.Equals (managedTypeName, "System.Int16", StringComparison.Ordinal) &&
+ !string.Equals (managedTypeName, "System.UInt16", StringComparison.Ordinal) &&
+ !string.Equals (managedTypeName, "System.Int32", StringComparison.Ordinal) &&
+ !string.Equals (managedTypeName, "System.UInt32", StringComparison.Ordinal) &&
+ !string.Equals (managedTypeName, "System.Int64", StringComparison.Ordinal) &&
+ !string.Equals (managedTypeName, "System.UInt64", StringComparison.Ordinal) &&
+ !string.Equals (managedTypeName, "System.Single", StringComparison.Ordinal) &&
+ !string.Equals (managedTypeName, "System.Double", StringComparison.Ordinal) &&
+ !string.Equals (managedTypeName, "System.IntPtr", StringComparison.Ordinal) &&
+ !string.Equals (managedTypeName, "System.UIntPtr", StringComparison.Ordinal);
+ }
+
+ void EmitParameterUnmarshal (InstructionEncoder encoder, MetadataBuilder metadata, ExportParamData param, JniParamKind jniKind, string jniType, int argIndex)
+ {
+ if (IsManagedArrayType (param.ManagedTypeName)) {
+ // Arrays: JNIEnv.GetArray(handle)
+ var getArraySpec = BuildArrayMethodSpec (metadata, _jniEnvGetArrayOpenRef, param.ManagedTypeName, param.AssemblyName, param.JniType);
+ encoder.LoadArgument (argIndex);
+ encoder.Call (getArraySpec);
+ return;
+ }
+
+ if (jniKind != JniParamKind.Object) {
+ encoder.LoadArgument (argIndex);
+ if (jniKind == JniParamKind.Boolean && param.ManagedTypeName == "System.Boolean") {
+ // JNI jboolean is byte; managed bool expects 0/1 semantics.
+ encoder.OpCode (ILOpCode.Ldc_i4_0);
+ encoder.OpCode (ILOpCode.Cgt_un);
+ }
+ return;
+ }
+
+ if (param.ManagedTypeName == "System.String") {
+ // String: GetString or GetCharSequence depending on JNI descriptor.
+ encoder.LoadArgument (argIndex);
+ encoder.OpCode (ILOpCode.Ldc_i4_0); // DoNotTransfer
+ encoder.Call (jniType == "Ljava/lang/CharSequence;" ? _jniEnvGetCharSequenceRef : _jniEnvGetStringRef);
+ return;
+ }
+
+ // Java object: Object.GetObject(handle, DoNotTransfer)
+ // Use the 2-arg overload (without jnienv)
+ var typeRef = ResolveTypeRef (metadata, new TypeRefData {
+ ManagedTypeName = param.ManagedTypeName,
+ AssemblyName = param.AssemblyName,
+ });
+ var getObjectRef2 = AddMemberRef (metadata, _javaLangObjectRef, "GetObject",
+ sig => {
+ var methodSig = sig.MethodSignature (genericParameterCount: 1);
+ methodSig.Parameters (2,
+ rt => rt.Type ().GenericMethodTypeParameter (0),
+ p => {
+ p.AddParameter ().Type ().IntPtr ();
+ p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true);
+ });
+ });
+ var instBlob = new BlobBuilder (16);
+ instBlob.WriteByte (0x0A);
+ instBlob.WriteCompressedInteger (1);
+ instBlob.WriteByte (0x12);
+ instBlob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (typeRef));
+ var methodSpec = metadata.AddMethodSpecification (getObjectRef2, metadata.GetOrAddBlob (instBlob));
+
+ encoder.LoadArgument (argIndex);
+ encoder.OpCode (ILOpCode.Ldc_i4_0); // DoNotTransfer
+ encoder.Call (methodSpec);
+ }
+
+ void EmitReturnMarshal (InstructionEncoder encoder, MetadataBuilder metadata, JniParamKind returnKind, string jniReturnType, string? managedReturnType)
+ {
+ string? managedTypeName = null;
+ string managedAssemblyName = "";
+ if (!string.IsNullOrEmpty (managedReturnType)) {
+ (managedTypeName, managedAssemblyName) = SplitManagedTypeNameAndAssembly (managedReturnType!);
+ }
+
+ if (!string.IsNullOrEmpty (managedTypeName) && IsManagedArrayType (managedTypeName!)) {
+ // Managed array -> JNI array
+ var newArraySpec = BuildArrayMethodSpec (metadata, _jniEnvNewArrayOpenRef, managedTypeName!, managedAssemblyName, jniReturnType);
+ encoder.Call (newArraySpec);
+ return;
+ }
+
+ if (returnKind != JniParamKind.Object) {
+ // Enum return values are marshaled as int (legacy behavior).
+ if (!string.IsNullOrEmpty (managedTypeName) && IsEnumManagedType (managedTypeName!, returnKind)) {
+ encoder.OpCode (ILOpCode.Conv_i4);
+ }
+ return;
+ }
+
+ if (managedTypeName == "System.String") {
+ encoder.Call (jniReturnType == "Ljava/lang/CharSequence;" ? _charSequenceToLocalJniHandleStringRef : _jniEnvNewStringRef);
+ return;
+ }
+
+ // Java object: JNIEnv.ToLocalJniHandle(result)
+ encoder.Call (_jniEnvToLocalJniHandleRef);
+ }
+
+ void EmitArrayParameterCopyBack (InstructionEncoder encoder, MetadataBuilder metadata, ExportParamData param, int localIndex, int argIndex)
+ {
+ var skipLabel = encoder.DefineLabel ();
+ LoadLocal (encoder, localIndex);
+ encoder.Branch (ILOpCode.Brfalse_s, skipLabel);
+ LoadLocal (encoder, localIndex);
+ encoder.LoadArgument (argIndex);
+ var copyArraySpec = BuildArrayMethodSpec (metadata, _jniEnvCopyArrayOpenRef, param.ManagedTypeName, param.AssemblyName, param.JniType);
+ encoder.Call (copyArraySpec);
+ encoder.MarkLabel (skipLabel);
+ }
+
+ EntityHandle BuildArrayMethodSpec (MetadataBuilder metadata, MemberReferenceHandle openMethodRef, string managedArrayTypeName, string assemblyName, string jniType)
+ {
+ string elementTypeName = managedArrayTypeName.EndsWith ("[]", StringComparison.Ordinal)
+ ? managedArrayTypeName.Substring (0, managedArrayTypeName.Length - 2)
+ : managedArrayTypeName;
+
+ var instBlob = new BlobBuilder (16);
+ instBlob.WriteByte (0x0A); // ELEMENT_TYPE_GENERICINST (method)
+ instBlob.WriteCompressedInteger (1); // one type argument
+ WriteGenericTypeArgument (instBlob, metadata, elementTypeName, assemblyName,
+ IsEnumManagedType (elementTypeName, JniSignatureHelper.ParseSingleTypeFromDescriptor (jniType.StartsWith ("[", StringComparison.Ordinal) ? jniType.Substring (1) : jniType)));
+ return metadata.AddMethodSpecification (openMethodRef, metadata.GetOrAddBlob (instBlob));
+ }
+
+ void WriteGenericTypeArgument (BlobBuilder blob, MetadataBuilder metadata, string managedTypeName, string assemblyName, bool isValueType)
+ {
+ if (TryGetPrimitiveElementTypeCode (managedTypeName, out byte primitiveCode)) {
+ blob.WriteByte (primitiveCode);
+ return;
+ }
+
+ if (managedTypeName == "System.String") {
+ blob.WriteByte (0x0E); // ELEMENT_TYPE_STRING
+ return;
+ }
+
+ var typeRef = ResolveTypeRef (metadata, new TypeRefData {
+ ManagedTypeName = managedTypeName,
+ AssemblyName = assemblyName,
+ });
+ blob.WriteByte (isValueType ? (byte) 0x11 : (byte) 0x12); // VALUETYPE | CLASS
+ blob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (typeRef));
+ }
+
+ static bool TryGetPrimitiveElementTypeCode (string managedTypeName, out byte typeCode)
+ {
+ switch (managedTypeName) {
+ case "System.Boolean": typeCode = 0x02; return true;
+ case "System.Char": typeCode = 0x03; return true;
+ case "System.SByte": typeCode = 0x04; return true;
+ case "System.Byte": typeCode = 0x05; return true;
+ case "System.Int16": typeCode = 0x06; return true;
+ case "System.UInt16": typeCode = 0x07; return true;
+ case "System.Int32": typeCode = 0x08; return true;
+ case "System.UInt32": typeCode = 0x09; return true;
+ case "System.Int64": typeCode = 0x0A; return true;
+ case "System.UInt64": typeCode = 0x0B; return true;
+ case "System.Single": typeCode = 0x0C; return true;
+ case "System.Double": typeCode = 0x0D; return true;
+ default:
+ typeCode = 0;
+ return false;
+ }
+ }
+
+ static (string managedTypeName, string assemblyName) SplitManagedTypeNameAndAssembly (string managedType)
+ {
+ int commaIndex = managedType.IndexOf (", ", StringComparison.Ordinal);
+ if (commaIndex < 0) {
+ return (managedType, "");
+ }
+ return (managedType.Substring (0, commaIndex), managedType.Substring (commaIndex + 2));
+ }
+
+ static void LoadLocal (InstructionEncoder encoder, int localIndex)
+ {
+ switch (localIndex) {
+ case 0: encoder.OpCode (ILOpCode.Ldloc_0); return;
+ case 1: encoder.OpCode (ILOpCode.Ldloc_1); return;
+ case 2: encoder.OpCode (ILOpCode.Ldloc_2); return;
+ case 3: encoder.OpCode (ILOpCode.Ldloc_3); return;
+ default:
+ encoder.OpCode (ILOpCode.Ldloc_s);
+ encoder.CodeBuilder.WriteByte ((byte) localIndex);
+ return;
+ }
+ }
+
+ static void StoreLocal (InstructionEncoder encoder, int localIndex)
+ {
+ switch (localIndex) {
+ case 0: encoder.OpCode (ILOpCode.Stloc_0); return;
+ case 1: encoder.OpCode (ILOpCode.Stloc_1); return;
+ case 2: encoder.OpCode (ILOpCode.Stloc_2); return;
+ case 3: encoder.OpCode (ILOpCode.Stloc_3); return;
+ default:
+ encoder.OpCode (ILOpCode.Stloc_s);
+ encoder.CodeBuilder.WriteByte ((byte) localIndex);
+ return;
+ }
+ }
+
+ static void EmitDefaultReturnValue (InstructionEncoder encoder, JniParamKind kind)
+ {
+ switch (kind) {
+ case JniParamKind.Boolean:
+ case JniParamKind.Byte:
+ case JniParamKind.Char:
+ case JniParamKind.Short:
+ case JniParamKind.Int:
+ encoder.OpCode (ILOpCode.Ldc_i4_0);
+ break;
+ case JniParamKind.Long:
+ encoder.OpCode (ILOpCode.Ldc_i8);
+ encoder.CodeBuilder.WriteInt64 (0);
+ break;
+ case JniParamKind.Float:
+ encoder.OpCode (ILOpCode.Ldc_r4);
+ encoder.CodeBuilder.WriteSingle (0);
+ break;
+ case JniParamKind.Double:
+ encoder.OpCode (ILOpCode.Ldc_r8);
+ encoder.CodeBuilder.WriteDouble (0);
+ break;
+ case JniParamKind.Object:
+ encoder.OpCode (ILOpCode.Ldc_i4_0);
+ encoder.OpCode (ILOpCode.Conv_i); // IntPtr.Zero
+ break;
+ }
+ }
+
+ void EmitRegisterNatives (MetadataBuilder metadata, BlobBuilder ilBuilder,
+ List registrations, Dictionary wrapperHandles)
+ {
+ EmitBody (metadata, ilBuilder, "RegisterNatives",
+ MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig |
+ MethodAttributes.NewSlot | MethodAttributes.Final,
+ sig => sig.MethodSignature (isInstanceMethod: true).Parameters (1,
+ rt => rt.Void (),
+ p => p.AddParameter ().Type ().Type (_jniTypeRef, false)),
+ encoder => {
+ foreach (var reg in registrations) {
+ if (!wrapperHandles.TryGetValue (reg.WrapperMethodName, out var wrapperHandle)) {
+ continue;
+ }
+ encoder.LoadArgument (1);
+ encoder.LoadString (metadata.GetOrAddUserString (reg.JniMethodName));
+ encoder.LoadString (metadata.GetOrAddUserString (reg.JniSignature));
+ encoder.OpCode (ILOpCode.Ldftn);
+ encoder.Token (wrapperHandle);
+ encoder.Call (_registerMethodRef);
+ }
+ encoder.OpCode (ILOpCode.Ret);
+ });
+ }
+
+ // ---- TypeMap attributes ----
+
+ void EmitTypeMapAttribute (MetadataBuilder metadata, TypeMapAttributeData entry)
+ {
+ _attrBlob.Clear ();
+ _attrBlob.WriteUInt16 (0x0001); // Prolog
+ _attrBlob.WriteSerializedString (entry.JniName);
+ _attrBlob.WriteSerializedString (entry.ProxyTypeReference);
+ if (!entry.IsUnconditional) {
+ _attrBlob.WriteSerializedString (entry.TargetTypeReference!);
+ }
+ _attrBlob.WriteUInt16 (0x0000); // NumNamed
+
+ var ctorRef = entry.IsUnconditional ? _typeMapAttrCtorRef2Arg : _typeMapAttrCtorRef3Arg;
+ metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, ctorRef, metadata.GetOrAddBlob (_attrBlob));
+ }
+
+ void EmitTypeMapAssociationAttribute (MetadataBuilder metadata, TypeMapAssociationData assoc)
+ {
+ _attrBlob.Clear ();
+ _attrBlob.WriteUInt16 (0x0001); // Prolog
+ _attrBlob.WriteSerializedString (assoc.SourceTypeReference);
+ _attrBlob.WriteSerializedString (assoc.AliasProxyTypeReference);
+ _attrBlob.WriteUInt16 (0x0000); // NumNamed
+ metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, _typeMapAssociationAttrCtorRef,
+ metadata.GetOrAddBlob (_attrBlob));
+ }
+
+ // ---- IgnoresAccessChecksTo ----
+
+ void EmitIgnoresAccessChecksToAttribute (MetadataBuilder metadata, BlobBuilder ilBuilder, 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 (metadata, attributeTypeRef, ".ctor",
+ sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { }));
+
+ var ctorDef = EmitBody (metadata, ilBuilder, ".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) {
+ _attrBlob.Clear ();
+ _attrBlob.WriteUInt16 (1);
+ _attrBlob.WriteSerializedString (asmName);
+ _attrBlob.WriteUInt16 (0);
+ metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, ctorDef, metadata.GetOrAddBlob (_attrBlob));
+ }
+ }
+
+ // ---- Plumbing helpers ----
+
+ AssemblyReferenceHandle AddAssemblyRef (MetadataBuilder metadata, string name, Version version,
+ byte []? publicKeyOrToken = null)
+ {
+ var handle = metadata.AddAssemblyReference (
+ metadata.GetOrAddString (name), version, default,
+ publicKeyOrToken != null ? metadata.GetOrAddBlob (publicKeyOrToken) : default, 0, default);
+ _asmRefCache [name] = handle;
+ return handle;
+ }
+
+ AssemblyReferenceHandle FindOrAddAssemblyReference (MetadataBuilder metadata, string assemblyName)
+ {
+ if (_asmRefCache.TryGetValue (assemblyName, out var handle)) {
+ return handle;
+ }
+ return AddAssemblyRef (metadata, assemblyName, new Version (0, 0, 0, 0));
+ }
+
+ MemberReferenceHandle AddMemberRef (MetadataBuilder metadata, EntityHandle parent, string name,
+ Action encodeSig)
+ {
+ _sigBlob.Clear ();
+ encodeSig (new BlobEncoder (_sigBlob));
+ return metadata.AddMemberReference (parent, metadata.GetOrAddString (name), metadata.GetOrAddBlob (_sigBlob));
+ }
+
+ EntityHandle ResolveTypeRef (MetadataBuilder metadata, TypeRefData typeRef)
+ {
+ var cacheKey = (typeRef.AssemblyName, typeRef.ManagedTypeName);
+ if (_typeRefCache.TryGetValue (cacheKey, out var cached)) {
+ return cached;
+ }
+ var asmRef = FindOrAddAssemblyReference (metadata, typeRef.AssemblyName);
+ var result = MakeTypeRefForManagedName (metadata, asmRef, typeRef.ManagedTypeName);
+ _typeRefCache [cacheKey] = result;
+ return result;
+ }
+
+ TypeReferenceHandle MakeTypeRefForManagedName (MetadataBuilder metadata, EntityHandle scope, string managedTypeName)
+ {
+ int plusIndex = managedTypeName.IndexOf ('+');
+ if (plusIndex >= 0) {
+ var outerRef = MakeTypeRefForManagedName (metadata, scope, managedTypeName.Substring (0, plusIndex));
+ return MakeTypeRefForManagedName (metadata, 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));
+ }
+
+ void AddUnmanagedCallersOnlyAttribute (MetadataBuilder metadata, MethodDefinitionHandle handle)
+ {
+ metadata.AddCustomAttribute (handle, _ucoAttrCtorRef, _ucoAttrBlobHandle);
+ }
+
+ /// Emits a method body and definition in one call.
+ MethodDefinitionHandle EmitBody (MetadataBuilder metadata, BlobBuilder ilBuilder,
+ string name, MethodAttributes attrs,
+ Action encodeSig, Action emitIL)
+ {
+ _sigBlob.Clear ();
+ encodeSig (new BlobEncoder (_sigBlob));
+
+ _codeBlob.Clear ();
+ var encoder = new InstructionEncoder (_codeBlob);
+ emitIL (encoder);
+
+ while (ilBuilder.Count % 4 != 0) {
+ ilBuilder.WriteByte (0);
+ }
+ var bodyEncoder = new MethodBodyStreamEncoder (ilBuilder);
+ int bodyOffset = bodyEncoder.AddMethodBody (encoder);
+
+ return metadata.AddMethodDefinition (
+ attrs, MethodImplAttributes.IL,
+ metadata.GetOrAddString (name),
+ metadata.GetOrAddBlob (_sigBlob),
+ bodyOffset, default);
+ }
+
+ static void WritePE (MetadataBuilder metadata, BlobBuilder ilBuilder, string outputPath)
+ {
+ var peBuilder = new ManagedPEBuilder (
+ new PEHeaderBuilder (imageCharacteristics: Characteristics.Dll),
+ new MetadataRootBuilder (metadata),
+ ilBuilder);
+ var peBlob = new BlobBuilder ();
+ peBuilder.Serialize (peBlob);
+ using var fs = File.Create (outputPath);
+ peBlob.WriteContentTo (fs);
+ }
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs
new file mode 100644
index 00000000000..927346fbf10
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Collections.Generic;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+///
+/// High-level API: builds the model from peers, then emits the PE assembly.
+/// Composes + .
+///
+sealed class TypeMapAssemblyGenerator
+{
+ readonly Version _systemRuntimeVersion;
+
+ /// Version for System.Runtime assembly references.
+ public TypeMapAssemblyGenerator (Version systemRuntimeVersion)
+ {
+ _systemRuntimeVersion = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion));
+ }
+
+ ///
+ /// Generates a TypeMap PE assembly from the given Java peer info records.
+ ///
+ /// Scanned Java peer types.
+ /// Path where the output .dll will be written.
+ /// Optional explicit assembly name. Derived from outputPath if null.
+ public void Generate (IReadOnlyList peers, string outputPath, string? assemblyName = null)
+ {
+ var model = ModelBuilder.Build (peers, outputPath, assemblyName);
+ var emitter = new TypeMapAssemblyEmitter (_systemRuntimeVersion);
+ emitter.Emit (model, outputPath);
+ }
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Manifest/IManifestTypeInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Manifest/IManifestTypeInfo.cs
new file mode 100644
index 00000000000..ad2c602f959
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Manifest/IManifestTypeInfo.cs
@@ -0,0 +1,72 @@
+using System.Collections.Generic;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+///
+/// Component kind for manifest generation.
+///
+enum ManifestComponentKind
+{
+ None,
+ Activity,
+ Service,
+ BroadcastReceiver,
+ ContentProvider,
+ Application,
+ Instrumentation,
+}
+
+///
+/// Holds raw attribute property data extracted from either Cecil or SRM.
+///
+sealed class ComponentAttributeInfo
+{
+ ///
+ /// Full attribute type name, e.g., "Android.App.ActivityAttribute".
+ ///
+ public string AttributeType { get; set; } = "";
+
+ ///
+ /// Named property values, e.g., { "MainLauncher": true, "Label": "My App" }.
+ /// Values are decoded .NET types (string, bool, int, Type name as string, enum as int).
+ ///
+ public IReadOnlyDictionary Properties { get; set; } = new Dictionary ();
+
+ ///
+ /// Constructor arguments (positional), e.g., ContentProviderAttribute(string[] authorities).
+ ///
+ public IReadOnlyList