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/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/Microsoft.Android.Sdk.TrimmableTypeMap.csproj b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj
new file mode 100644
index 00000000000..48a5f75728d
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj
@@ -0,0 +1,17 @@
+
+
+
+
+ $(TargetFrameworkNETStandard)
+ enable
+ Nullable
+ Microsoft.Android.Sdk.TrimmableTypeMap
+
+
+
+
+
+
+
+
+
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs
new file mode 100644
index 00000000000..8ef2217cb8e
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs
@@ -0,0 +1,265 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection.Metadata;
+using System.Reflection.PortableExecutable;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+///
+/// Phase 1 index for a single assembly. Built in one pass over TypeDefinitions,
+/// all subsequent lookups are O(1) dictionary lookups.
+///
+sealed class AssemblyIndex : IDisposable
+{
+ readonly PEReader peReader;
+ internal readonly CustomAttributeTypeProvider customAttributeTypeProvider;
+
+ public MetadataReader Reader { get; }
+ public string AssemblyName { get; }
+ public string FilePath { get; }
+
+ ///
+ /// Maps full managed type name (e.g., "Android.App.Activity") to its TypeDefinitionHandle.
+ ///
+ public Dictionary TypesByFullName { get; } = new (StringComparer.Ordinal);
+
+ ///
+ /// Cached [Register] attribute data per type.
+ ///
+ public Dictionary RegisterInfoByType { get; } = new ();
+
+ ///
+ /// All custom attribute data per type, pre-parsed for the attributes we care about.
+ ///
+ public Dictionary AttributesByType { get; } = new ();
+
+ AssemblyIndex (PEReader peReader, MetadataReader reader, string assemblyName, string filePath)
+ {
+ this.peReader = peReader;
+ this.customAttributeTypeProvider = new CustomAttributeTypeProvider (reader);
+ Reader = reader;
+ AssemblyName = assemblyName;
+ FilePath = filePath;
+ }
+
+ public static AssemblyIndex Create (string filePath)
+ {
+ var peReader = new PEReader (File.OpenRead (filePath));
+ var reader = peReader.GetMetadataReader ();
+ var assemblyName = reader.GetString (reader.GetAssemblyDefinition ().Name);
+ var index = new AssemblyIndex (peReader, reader, assemblyName, filePath);
+ index.Build ();
+ return index;
+ }
+
+ void Build ()
+ {
+ foreach (var typeHandle in Reader.TypeDefinitions) {
+ var typeDef = Reader.GetTypeDefinition (typeHandle);
+
+ var fullName = MetadataTypeNameResolver.GetFullName (typeDef, Reader);
+ if (fullName.Length == 0) {
+ continue;
+ }
+
+ TypesByFullName [fullName] = typeHandle;
+
+ var (registerInfo, attrInfo) = ParseAttributes (typeDef);
+
+ if (attrInfo is not null) {
+ AttributesByType [typeHandle] = attrInfo;
+ }
+
+ if (registerInfo is not null) {
+ RegisterInfoByType [typeHandle] = registerInfo;
+ }
+ }
+ }
+
+ (RegisterInfo? register, TypeAttributeInfo? attrs) ParseAttributes (TypeDefinition typeDef)
+ {
+ RegisterInfo? registerInfo = null;
+ TypeAttributeInfo? attrInfo = null;
+
+ foreach (var caHandle in typeDef.GetCustomAttributes ()) {
+ var ca = Reader.GetCustomAttribute (caHandle);
+ var attrName = GetCustomAttributeName (ca, Reader);
+
+ if (attrName is null) {
+ continue;
+ }
+
+ if (attrName == "RegisterAttribute") {
+ registerInfo = ParseRegisterAttribute (ca, customAttributeTypeProvider);
+ } else if (attrName == "ExportAttribute") {
+ // [Export] is a method-level attribute; it is parsed at scan time by JavaPeerScanner
+ } else if (IsKnownComponentAttribute (attrName)) {
+ attrInfo ??= CreateTypeAttributeInfo (attrName);
+ var componentName = TryGetNameProperty (ca);
+ if (componentName is not null) {
+ attrInfo.JniName = componentName.Replace ('.', '/');
+ }
+ if (attrInfo is ApplicationAttributeInfo applicationAttributeInfo) {
+ applicationAttributeInfo.BackupAgent = TryGetTypeProperty (ca, "BackupAgent");
+ applicationAttributeInfo.ManageSpaceActivity = TryGetTypeProperty (ca, "ManageSpaceActivity");
+ }
+ }
+ }
+
+ return (registerInfo, attrInfo);
+ }
+
+ static readonly HashSet KnownComponentAttributes = new (StringComparer.Ordinal) {
+ "ActivityAttribute",
+ "ServiceAttribute",
+ "BroadcastReceiverAttribute",
+ "ContentProviderAttribute",
+ "ApplicationAttribute",
+ "InstrumentationAttribute",
+ };
+
+ static TypeAttributeInfo CreateTypeAttributeInfo (string attrName)
+ {
+ return attrName == "ApplicationAttribute"
+ ? new ApplicationAttributeInfo ()
+ : new TypeAttributeInfo (attrName);
+ }
+
+ static bool IsKnownComponentAttribute (string attrName) => KnownComponentAttributes.Contains (attrName);
+
+ internal static string? GetCustomAttributeName (CustomAttribute ca, MetadataReader reader)
+ {
+ if (ca.Constructor.Kind == HandleKind.MemberReference) {
+ var memberRef = reader.GetMemberReference ((MemberReferenceHandle)ca.Constructor);
+ if (memberRef.Parent.Kind == HandleKind.TypeReference) {
+ var typeRef = reader.GetTypeReference ((TypeReferenceHandle)memberRef.Parent);
+ return reader.GetString (typeRef.Name);
+ }
+ } else if (ca.Constructor.Kind == HandleKind.MethodDefinition) {
+ var methodDef = reader.GetMethodDefinition ((MethodDefinitionHandle)ca.Constructor);
+ var declaringType = reader.GetTypeDefinition (methodDef.GetDeclaringType ());
+ return reader.GetString (declaringType.Name);
+ }
+ return null;
+ }
+
+ internal static RegisterInfo ParseRegisterAttribute (CustomAttribute ca, ICustomAttributeTypeProvider provider)
+ {
+ var value = ca.DecodeValue (provider);
+
+ string jniName = "";
+ string? signature = null;
+ string? connector = null;
+ bool doNotGenerateAcw = false;
+
+ if (value.FixedArguments.Length > 0) {
+ jniName = (string?)value.FixedArguments [0].Value ?? "";
+ }
+ if (value.FixedArguments.Length > 1) {
+ signature = (string?)value.FixedArguments [1].Value;
+ }
+ if (value.FixedArguments.Length > 2) {
+ connector = (string?)value.FixedArguments [2].Value;
+ }
+
+ if (TryGetNamedBooleanArgument (value, "DoNotGenerateAcw", out var doNotGenerateAcwValue)) {
+ doNotGenerateAcw = doNotGenerateAcwValue;
+ }
+
+ return new RegisterInfo {
+ JniName = jniName,
+ Signature = signature,
+ Connector = connector,
+ DoNotGenerateAcw = doNotGenerateAcw,
+ };
+ }
+
+ string? TryGetTypeProperty (CustomAttribute ca, string propertyName)
+ {
+ var value = ca.DecodeValue (customAttributeTypeProvider);
+ var typeName = TryGetNamedArgument (value, propertyName);
+ if (!string.IsNullOrEmpty (typeName)) {
+ return typeName;
+ }
+ return null;
+ }
+
+ string? TryGetNameProperty (CustomAttribute ca)
+ {
+ var value = ca.DecodeValue (customAttributeTypeProvider);
+
+ // Check named arguments first (e.g., [Activity(Name = "...")])
+ var name = TryGetNamedArgument (value, "Name");
+ if (!string.IsNullOrEmpty (name)) {
+ return name;
+ }
+
+ // Fall back to first constructor argument (e.g., [CustomJniName("...")])
+ if (value.FixedArguments.Length > 0 && value.FixedArguments [0].Value is string ctorName && !string.IsNullOrEmpty (ctorName)) {
+ return ctorName;
+ }
+
+ return null;
+ }
+
+ static T? TryGetNamedArgument (CustomAttributeValue value, string argumentName) where T : class
+ {
+ foreach (var named in value.NamedArguments) {
+ if (named.Name == argumentName && named.Value is T typedValue) {
+ return typedValue;
+ }
+ }
+ return null;
+ }
+
+ static bool TryGetNamedBooleanArgument (CustomAttributeValue value, string argumentName, out bool argumentValue)
+ {
+ foreach (var named in value.NamedArguments) {
+ if (named.Name == argumentName && named.Value is bool boolValue) {
+ argumentValue = boolValue;
+ return true;
+ }
+ }
+
+ argumentValue = false;
+ return false;
+ }
+
+ public void Dispose ()
+ {
+ peReader.Dispose ();
+ }
+}
+
+///
+/// Parsed [Register] attribute data for a type or method.
+///
+sealed record RegisterInfo
+{
+ public required string JniName { get; init; }
+ public string? Signature { get; init; }
+ public string? Connector { get; init; }
+ public bool DoNotGenerateAcw { get; init; }
+}
+
+///
+/// Parsed [Export] attribute data for a method.
+///
+sealed record ExportInfo
+{
+ public IReadOnlyList? ThrownNames { get; init; }
+ public string? SuperArgumentsString { get; init; }
+}
+
+class TypeAttributeInfo (string attributeName)
+{
+ public string AttributeName { get; } = attributeName;
+ public string? JniName { get; set; }
+}
+
+sealed class ApplicationAttributeInfo () : TypeAttributeInfo ("ApplicationAttribute")
+{
+ public string? BackupAgent { get; set; }
+ public string? ManageSpaceActivity { get; set; }
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/CustomAttributeTypeProvider.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/CustomAttributeTypeProvider.cs
new file mode 100644
index 00000000000..246d13f3760
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/CustomAttributeTypeProvider.cs
@@ -0,0 +1,103 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection.Metadata;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+///
+/// Minimal ICustomAttributeTypeProvider implementation for decoding
+/// custom attribute values via System.Reflection.Metadata.
+///
+sealed class CustomAttributeTypeProvider (MetadataReader reader) : ICustomAttributeTypeProvider
+{
+ Dictionary? enumTypeCache;
+
+ public string GetPrimitiveType (PrimitiveTypeCode typeCode) => typeCode.ToString ();
+
+ public string GetTypeFromDefinition (MetadataReader metadataReader, TypeDefinitionHandle handle, byte rawTypeKind)
+ => MetadataTypeNameResolver.GetTypeFromDefinition (metadataReader, handle, rawTypeKind);
+
+ public string GetTypeFromReference (MetadataReader metadataReader, TypeReferenceHandle handle, byte rawTypeKind)
+ => MetadataTypeNameResolver.GetTypeFromReference (metadataReader, handle, rawTypeKind);
+
+ public string GetTypeFromSerializedName (string name) => name;
+
+ public PrimitiveTypeCode GetUnderlyingEnumType (string type)
+ {
+ if (enumTypeCache == null) {
+ enumTypeCache = BuildEnumTypeCache ();
+ }
+
+ if (enumTypeCache.TryGetValue (type, out var code)) {
+ return code;
+ }
+
+ // Default to Int32 for enums defined in other assemblies
+ return PrimitiveTypeCode.Int32;
+ }
+
+ Dictionary BuildEnumTypeCache ()
+ {
+ var cache = new Dictionary ();
+
+ foreach (var typeHandle in reader.TypeDefinitions) {
+ var typeDef = reader.GetTypeDefinition (typeHandle);
+
+ // Only process enum types
+ if (!IsEnum (typeDef))
+ continue;
+
+ var fullName = GetTypeFromDefinition (reader, typeHandle, rawTypeKind: 0);
+ var code = GetEnumUnderlyingTypeCode (typeDef);
+ cache [fullName] = code;
+ }
+
+ return cache;
+ }
+
+ bool IsEnum (TypeDefinition typeDef)
+ {
+ var baseType = typeDef.BaseType;
+ if (baseType.IsNil)
+ return false;
+
+ string? baseFullName = baseType.Kind switch {
+ HandleKind.TypeReference => GetTypeFromReference (reader, (TypeReferenceHandle)baseType, rawTypeKind: 0),
+ HandleKind.TypeDefinition => GetTypeFromDefinition (reader, (TypeDefinitionHandle)baseType, rawTypeKind: 0),
+ _ => null,
+ };
+
+ return baseFullName == "System.Enum";
+ }
+
+ PrimitiveTypeCode GetEnumUnderlyingTypeCode (TypeDefinition typeDef)
+ {
+ // For enums, the first instance field is the underlying value__ field
+ foreach (var fieldHandle in typeDef.GetFields ()) {
+ var field = reader.GetFieldDefinition (fieldHandle);
+ if ((field.Attributes & System.Reflection.FieldAttributes.Static) != 0)
+ continue;
+
+ var sig = field.DecodeSignature (SignatureTypeProvider.Instance, genericContext: null);
+ return sig switch {
+ "System.Byte" => PrimitiveTypeCode.Byte,
+ "System.SByte" => PrimitiveTypeCode.SByte,
+ "System.Int16" => PrimitiveTypeCode.Int16,
+ "System.UInt16" => PrimitiveTypeCode.UInt16,
+ "System.Int32" => PrimitiveTypeCode.Int32,
+ "System.UInt32" => PrimitiveTypeCode.UInt32,
+ "System.Int64" => PrimitiveTypeCode.Int64,
+ "System.UInt64" => PrimitiveTypeCode.UInt64,
+ _ => PrimitiveTypeCode.Int32,
+ };
+ }
+
+ return PrimitiveTypeCode.Int32;
+ }
+
+ public string GetSystemType () => "System.Type";
+
+ public string GetSZArrayType (string elementType) => $"{elementType}[]";
+
+ public bool IsSystemType (string type) => type == "System.Type" || type == "Type";
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs
new file mode 100644
index 00000000000..e3b0617ed66
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs
@@ -0,0 +1,317 @@
+using System;
+using System.Collections.Generic;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+///
+/// Represents a Java peer type discovered during assembly scanning.
+/// Contains all data needed by downstream generators (TypeMap IL, UCO wrappers, JCW Java sources).
+/// Generators consume this data model — they never touch PEReader/MetadataReader.
+///
+sealed class JavaPeerInfo
+{
+ ///
+ /// JNI type name, e.g., "android/app/Activity".
+ /// Extracted from the [Register] attribute.
+ ///
+ public string JavaName { get; set; } = "";
+
+ ///
+ /// Compat JNI type name, e.g., "myapp.namespace/MyType" for user types (uses raw namespace, not CRC64).
+ /// For MCW binding types (with [Register]), this equals .
+ /// Used by acw-map.txt to support legacy custom view name resolution in layout XMLs.
+ ///
+ public string CompatJniName { get; set; } = "";
+
+ ///
+ /// Full managed type name, e.g., "Android.App.Activity".
+ ///
+ public string ManagedTypeName { get; set; } = "";
+
+ ///
+ /// Managed type namespace, e.g., "Android.App".
+ ///
+ public string ManagedTypeNamespace { get; set; } = "";
+
+ ///
+ /// Managed type short name (without namespace), e.g., "Activity".
+ ///
+ public string ManagedTypeShortName { get; set; } = "";
+
+ ///
+ /// Assembly name the type belongs to, e.g., "Mono.Android".
+ ///
+ public string AssemblyName { get; set; } = "";
+
+ ///
+ /// JNI name of the base Java type, e.g., "android/app/Activity" for a type
+ /// that extends Activity. Null for java/lang/Object or types without a Java base.
+ /// Needed by JCW Java source generation ("extends" clause).
+ ///
+ public string? BaseJavaName { get; set; }
+
+ ///
+ /// JNI names of Java interfaces this type implements, e.g., ["android/view/View$OnClickListener"].
+ /// Needed by JCW Java source generation ("implements" clause).
+ ///
+ public IReadOnlyList ImplementedInterfaceJavaNames { get; set; } = Array.Empty ();
+
+ public bool IsInterface { get; set; }
+ public bool IsAbstract { get; set; }
+
+ ///
+ /// If true, this is a Managed Callable Wrapper (MCW) binding type.
+ /// No JCW or RegisterNatives will be generated for it.
+ ///
+ public bool DoNotGenerateAcw { get; set; }
+
+ ///
+ /// Types with component attributes ([Activity], [Service], etc.),
+ /// custom views from layout XML, or manifest-declared components
+ /// are unconditionally preserved (not trimmable).
+ ///
+ public bool IsUnconditional { get; set; }
+
+ ///
+ /// Marshal methods: methods with [Register(name, sig, connector)], [Export], or
+ /// constructor registrations ([Register(".ctor", sig, "")] / [JniConstructorSignature]).
+ /// Constructors are identified by .
+ /// Ordered — the index in this list is the method's ordinal for RegisterNatives.
+ ///
+ public IReadOnlyList MarshalMethods { get; set; } = Array.Empty ();
+
+ ///
+ /// Java constructors to emit in the JCW .java file.
+ /// Each has a JNI signature and an ordinal index for the nctor_N native method.
+ ///
+ public IReadOnlyList JavaConstructors { get; set; } = Array.Empty ();
+
+ ///
+ /// Information about the activation constructor for this type.
+ /// May reference a base type's constructor if the type doesn't define its own.
+ ///
+ public ActivationCtorInfo? ActivationCtor { get; set; }
+
+ ///
+ /// Java fields generated from [ExportField] attributes.
+ /// Each field is initialized by calling the associated managed method.
+ ///
+ public IReadOnlyList ExportFields { get; set; } = Array.Empty ();
+
+ ///
+ /// For interfaces and abstract types, the name of the invoker type
+ /// used to instantiate instances from Java.
+ ///
+ public string? InvokerTypeName { get; set; }
+
+ ///
+ /// True if this is an open generic type definition.
+ /// Generic types get TypeMap entries but CreateInstance throws NotSupportedException.
+ ///
+ public bool IsGenericDefinition { get; set; }
+}
+
+///
+/// Describes a marshal method (a method with [Register] or [Export]) on a Java peer type.
+/// Contains all data needed to generate a UCO wrapper, a JCW native declaration,
+/// and a RegisterNatives call.
+///
+sealed class MarshalMethodInfo
+{
+ ///
+ /// JNI method name, e.g., "onCreate".
+ /// This is the Java method name (without n_ prefix).
+ ///
+ public string JniName { get; set; } = "";
+
+ ///
+ /// JNI method signature, e.g., "(Landroid/os/Bundle;)V".
+ /// Contains both parameter types and return type.
+ ///
+ public string JniSignature { get; set; } = "";
+
+ ///
+ /// The connector string from [Register], e.g., "GetOnCreate_Landroid_os_Bundle_Handler".
+ /// Null for [Export] methods.
+ ///
+ public string? Connector { get; set; }
+
+ ///
+ /// Name of the managed method this maps to, e.g., "OnCreate".
+ ///
+ public string ManagedMethodName { get; set; } = "";
+
+ ///
+ /// Full name of the type that declares the managed method (may be a base type).
+ ///
+ public string DeclaringTypeName { get; set; } = "";
+
+ ///
+ /// Assembly name of the type that declares the managed method.
+ /// Needed for cross-assembly UCO wrapper generation.
+ ///
+ public string DeclaringAssemblyName { get; set; } = "";
+
+ ///
+ /// The native callback method name, e.g., "n_onCreate".
+ /// This is the actual method the UCO wrapper delegates to.
+ ///
+ public string NativeCallbackName { get; set; } = "";
+
+ ///
+ /// JNI parameter types for UCO generation.
+ ///
+ public IReadOnlyList Parameters { get; set; } = Array.Empty ();
+
+ ///
+ /// JNI return type descriptor, e.g., "V", "Landroid/os/Bundle;".
+ ///
+ public string JniReturnType { get; set; } = "";
+
+ ///
+ /// True if this is a constructor registration.
+ ///
+ public bool IsConstructor { get; set; }
+
+ ///
+ /// For [Export] methods: Java exception types that the method declares it can throw.
+ /// Null for [Register] methods.
+ ///
+ public IReadOnlyList? ThrownNames { get; set; }
+
+ ///
+ /// For [Export] methods: super constructor arguments string.
+ /// Null for [Register] methods.
+ ///
+ public string? SuperArgumentsString { get; set; }
+
+ ///
+ /// For [Export] methods: managed return type name, e.g., "System.String".
+ /// Null for [Register] methods and constructors.
+ ///
+ public string? ManagedReturnType { get; set; }
+
+ ///
+ /// True if the method is static. Relevant for [Export] static methods.
+ ///
+ public bool IsStatic { get; set; }
+}
+
+///
+/// Describes a Java field generated from a method annotated with [ExportField].
+/// The Java side declares a field initialized by calling the method.
+///
+sealed class ExportFieldInfo
+{
+ ///
+ /// The Java field name, e.g., "STATIC_INSTANCE".
+ ///
+ public string FieldName { get; set; } = "";
+
+ ///
+ /// Name of the managed method that initializes the field.
+ /// Used both as the Java initializer method name and the native callback method name.
+ ///
+ public string MethodName { get; set; } = "";
+
+ ///
+ /// JNI return type descriptor, e.g., "Ljava/lang/String;".
+ /// Determines the Java field type.
+ ///
+ public string JniReturnType { get; set; } = "";
+
+ ///
+ /// Whether the method (and thus the field) is static.
+ ///
+ public bool IsStatic { get; set; }
+}
+
+///
+/// Describes a JNI parameter for UCO method generation.
+///
+sealed class JniParameterInfo
+{
+ ///
+ /// JNI type descriptor, e.g., "Landroid/os/Bundle;", "I", "Z".
+ ///
+ public string JniType { get; set; } = "";
+
+ ///
+ /// Managed parameter type name, e.g., "Android.OS.Bundle", "System.Int32".
+ ///
+ public string ManagedType { get; set; } = "";
+}
+
+///
+/// Describes a Java constructor to emit in the JCW .java source file.
+///
+sealed class JavaConstructorInfo
+{
+ ///
+ /// JNI constructor signature, e.g., "(Landroid/content/Context;)V".
+ ///
+ public string JniSignature { get; set; } = "";
+
+ ///
+ /// Ordinal index for the native constructor method (nctor_0, nctor_1, ...).
+ ///
+ public int ConstructorIndex { get; set; }
+
+ ///
+ /// JNI parameter types parsed from the signature.
+ /// Used to generate the Java constructor parameter list.
+ ///
+ public IReadOnlyList Parameters { get; set; } = Array.Empty ();
+
+ ///
+ /// For [Export] constructors: super constructor arguments string.
+ /// Null for [Register] constructors.
+ ///
+ public string? SuperArgumentsString { get; set; }
+
+ ///
+ /// Whether this constructor is from [Export] attribute.
+ ///
+ public bool IsExport { get; set; }
+
+ ///
+ /// For [Export] constructors: Java exception types that the constructor declares it can throw.
+ /// Null for [Register] constructors.
+ ///
+ public IReadOnlyList? ThrownNames { get; set; }
+}
+
+///
+/// Describes how to call the activation constructor for a Java peer type.
+///
+sealed class ActivationCtorInfo
+{
+ ///
+ /// The type that declares the activation constructor.
+ /// May be the type itself or a base type.
+ ///
+ public string DeclaringTypeName { get; set; } = "";
+
+ ///
+ /// The assembly containing the declaring type.
+ ///
+ public string DeclaringAssemblyName { get; set; } = "";
+
+ ///
+ /// The style of activation constructor found.
+ ///
+ public ActivationCtorStyle Style { get; set; }
+}
+
+enum ActivationCtorStyle
+{
+ ///
+ /// Xamarin.Android style: (IntPtr handle, JniHandleOwnership transfer)
+ ///
+ XamarinAndroid,
+
+ ///
+ /// Java.Interop style: (ref JniObjectReference reference, JniObjectReferenceOptions options)
+ ///
+ JavaInterop,
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
new file mode 100644
index 00000000000..28d9ccbf2ef
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
@@ -0,0 +1,1027 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Reflection;
+using System.Reflection.Metadata;
+using System.Reflection.Metadata.Ecma335;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+///
+/// Scans assemblies for Java peer types using System.Reflection.Metadata.
+/// Two-phase architecture:
+/// Phase 1: Build per-assembly indices (fast, O(1) lookups)
+/// Phase 2: Analyze types using cached indices
+///
+sealed class JavaPeerScanner : IDisposable
+{
+ readonly Dictionary assemblyCache = new (StringComparer.Ordinal);
+ readonly Dictionary activationCtorCache = new (StringComparer.Ordinal);
+
+ ///
+ /// Resolves a type name + assembly name to a TypeDefinitionHandle + AssemblyIndex.
+ /// Checks the specified assembly (by name) in the assembly cache.
+ ///
+ bool TryResolveType (string typeName, string assemblyName, out TypeDefinitionHandle handle, out AssemblyIndex resolvedIndex)
+ {
+ if (assemblyCache.TryGetValue (assemblyName, out resolvedIndex!) &&
+ resolvedIndex.TypesByFullName.TryGetValue (typeName, out handle)) {
+ return true;
+ }
+ handle = default;
+ resolvedIndex = null!;
+ return false;
+ }
+
+ ///
+ /// Resolves a TypeReferenceHandle to (fullName, assemblyName), correctly handling
+ /// nested types whose ResolutionScope is another TypeReference.
+ ///
+ static (string fullName, string assemblyName) ResolveTypeReference (TypeReferenceHandle handle, AssemblyIndex index)
+ {
+ var typeRef = index.Reader.GetTypeReference (handle);
+ var name = index.Reader.GetString (typeRef.Name);
+ var ns = index.Reader.GetString (typeRef.Namespace);
+
+ var scope = typeRef.ResolutionScope;
+ switch (scope.Kind) {
+ case HandleKind.AssemblyReference: {
+ var asmRef = index.Reader.GetAssemblyReference ((AssemblyReferenceHandle)scope);
+ var fullName = ns.Length > 0 ? ns + "." + name : name;
+ return (fullName, index.Reader.GetString (asmRef.Name));
+ }
+ case HandleKind.TypeReference: {
+ // Nested type: recurse to get the declaring type's full name and assembly
+ var (parentFullName, assemblyName) = ResolveTypeReference ((TypeReferenceHandle)scope, index);
+ return (parentFullName + "+" + name, assemblyName);
+ }
+ default: {
+ var fullName = ns.Length > 0 ? ns + "." + name : name;
+ return (fullName, index.AssemblyName);
+ }
+ }
+ }
+
+ ///
+ /// Looks up the [Register] JNI name for a type identified by name + assembly.
+ ///
+ string? ResolveRegisterJniName (string typeName, string assemblyName)
+ {
+ if (TryResolveType (typeName, assemblyName, out var handle, out var resolvedIndex) &&
+ resolvedIndex.RegisterInfoByType.TryGetValue (handle, out var regInfo)) {
+ return regInfo.JniName;
+ }
+ return null;
+ }
+
+ ///
+ /// Phase 1: Build indices for all assemblies.
+ /// Phase 2: Scan all types and produce JavaPeerInfo records.
+ ///
+ public List Scan (IReadOnlyList assemblyPaths)
+ {
+ // Phase 1: Build indices for all assemblies
+ foreach (var path in assemblyPaths) {
+ var index = AssemblyIndex.Create (path);
+ assemblyCache [index.AssemblyName] = index;
+ }
+
+ // Phase 1b: Merge IJniNameProviderAttribute implementor sets from all assemblies
+ // and re-classify any attributes that weren't recognized in the initial pass
+ // (e.g., user assembly references ActivityAttribute from Mono.Android.dll).
+ var mergedJniNameProviders = new HashSet (StringComparer.Ordinal);
+ foreach (var index in assemblyCache.Values) {
+ mergedJniNameProviders.UnionWith (index.JniNameProviderAttributes);
+ }
+ foreach (var index in assemblyCache.Values) {
+ index.ReclassifyAttributes (mergedJniNameProviders);
+ }
+
+ // Phase 2: Analyze types using cached indices
+ var resultsByManagedName = new Dictionary (StringComparer.Ordinal);
+
+ foreach (var index in assemblyCache.Values) {
+ ScanAssembly (index, resultsByManagedName);
+ }
+
+ // Phase 3: Force unconditional on types referenced by [Application] attributes
+ ForceUnconditionalCrossReferences (resultsByManagedName, assemblyCache);
+
+ return new List (resultsByManagedName.Values);
+ }
+
+ ///
+ /// Types referenced by [Application(BackupAgent = typeof(X))] or
+ /// [Application(ManageSpaceActivity = typeof(X))] must be unconditional,
+ /// because the manifest will reference them even if nothing else does.
+ ///
+ static void ForceUnconditionalCrossReferences (Dictionary resultsByManagedName, Dictionary assemblyCache)
+ {
+ foreach (var index in assemblyCache.Values) {
+ foreach (var attrInfo in index.AttributesByType.Values) {
+ ForceUnconditionalIfPresent (resultsByManagedName, attrInfo.ApplicationBackupAgent);
+ ForceUnconditionalIfPresent (resultsByManagedName, attrInfo.ApplicationManageSpaceActivity);
+ }
+ }
+ }
+
+ static void ForceUnconditionalIfPresent (Dictionary resultsByManagedName, string? managedTypeName)
+ {
+ if (managedTypeName == null) {
+ return;
+ }
+
+ // TryGetTypeProperty may return assembly-qualified names like "Ns.Type, Assembly, ..."
+ // Strip to just the type name for lookup
+ var commaIndex = managedTypeName.IndexOf (',');
+ if (commaIndex > 0) {
+ managedTypeName = managedTypeName.Substring (0, commaIndex).Trim ();
+ }
+
+ if (resultsByManagedName.TryGetValue (managedTypeName, out var peer)) {
+ peer.IsUnconditional = true;
+ }
+ }
+
+ void ScanAssembly (AssemblyIndex index, Dictionary results)
+ {
+ foreach (var typeHandle in index.Reader.TypeDefinitions) {
+ var typeDef = index.Reader.GetTypeDefinition (typeHandle);
+
+ // Skip module-level types
+ if (index.Reader.GetString (typeDef.Name) == "") {
+ continue;
+ }
+
+ // Determine the JNI name and whether this is a known Java peer.
+ // Priority:
+ // 1. [Register] attribute → use JNI name from attribute
+ // 2. Component attribute Name property → convert dots to slashes
+ // 3. Extends a known Java peer → auto-compute JNI name via CRC64
+ // 4. None of the above → not a Java peer, skip
+ string? jniName = null;
+ string? compatJniName = null;
+ bool doNotGenerateAcw = false;
+
+ index.RegisterInfoByType.TryGetValue (typeHandle, out var registerInfo);
+ index.AttributesByType.TryGetValue (typeHandle, out var attrInfo);
+
+ if (registerInfo != null && !string.IsNullOrEmpty (registerInfo.JniName)) {
+ jniName = registerInfo.JniName;
+ compatJniName = jniName;
+ doNotGenerateAcw = registerInfo.DoNotGenerateAcw;
+ } else if (attrInfo?.ComponentAttributeJniName != null) {
+ // User type with [Activity(Name = "...")] but no [Register]
+ jniName = attrInfo.ComponentAttributeJniName;
+ compatJniName = jniName;
+ } else {
+ // No explicit JNI name — check if this type extends a known Java peer.
+ // If so, auto-compute JNI name from the managed type name via CRC64.
+ if (ExtendsJavaPeer (typeDef, index)) {
+ (jniName, compatJniName) = ComputeAutoJniNames (typeDef, index);
+ } else {
+ continue;
+ }
+ }
+
+ var fullName = AssemblyIndex.GetFullName (typeDef, index.Reader);
+
+ var isInterface = (typeDef.Attributes & TypeAttributes.Interface) != 0;
+ var isAbstract = (typeDef.Attributes & TypeAttributes.Abstract) != 0;
+ var isGenericDefinition = typeDef.GetGenericParameters ().Count > 0;
+
+ var isUnconditional = attrInfo?.HasComponentAttribute ?? false;
+ string? invokerTypeName = null;
+
+ // Resolve base Java type name
+ var baseJavaName = ResolveBaseJavaName (typeDef, index, results);
+
+ // Resolve implemented Java interface names
+ var implementedInterfaces = ResolveImplementedInterfaceJavaNames (typeDef, index);
+
+ // Collect marshal methods (including constructors) and [ExportField] declarations
+ var (marshalMethods, exportFields) = CollectMarshalMethodsAndExportFields (typeDef, index);
+
+ // Resolve activation constructor
+ var activationCtor = ResolveActivationCtor (fullName, typeDef, index);
+
+ // For interfaces/abstract types, try to find invoker type name
+ if (isInterface || isAbstract) {
+ invokerTypeName = TryFindInvokerTypeName (fullName, typeHandle, index);
+ }
+
+ var peer = new JavaPeerInfo {
+ JavaName = jniName,
+ CompatJniName = compatJniName,
+ ManagedTypeName = fullName,
+ ManagedTypeNamespace = ExtractNamespace (fullName),
+ ManagedTypeShortName = ExtractShortName (fullName),
+ AssemblyName = index.AssemblyName,
+ BaseJavaName = baseJavaName,
+ ImplementedInterfaceJavaNames = implementedInterfaces,
+ IsInterface = isInterface,
+ IsAbstract = isAbstract,
+ DoNotGenerateAcw = doNotGenerateAcw,
+ IsUnconditional = isUnconditional,
+ MarshalMethods = marshalMethods,
+ JavaConstructors = BuildJavaConstructors (marshalMethods),
+ ActivationCtor = activationCtor,
+ ExportFields = exportFields,
+ InvokerTypeName = invokerTypeName,
+ IsGenericDefinition = isGenericDefinition,
+ };
+
+ results [fullName] = peer;
+ }
+ }
+
+ (List, List) CollectMarshalMethodsAndExportFields (TypeDefinition typeDef, AssemblyIndex index)
+ {
+ var methods = new List ();
+ var exportFields = new List ();
+
+ // Single pass over methods: collect marshal methods, constructors, and export fields
+ foreach (var methodHandle in typeDef.GetMethods ()) {
+ var methodDef = index.Reader.GetMethodDefinition (methodHandle);
+
+ string? exportFieldName = null;
+ RegisterInfo? registerInfo = null;
+
+ foreach (var caHandle in methodDef.GetCustomAttributes ()) {
+ var ca = index.Reader.GetCustomAttribute (caHandle);
+ var attrName = AssemblyIndex.GetCustomAttributeName (ca, index.Reader);
+
+ if (attrName == "RegisterAttribute") {
+ registerInfo = AssemblyIndex.ParseRegisterAttribute (ca, index.customAttributeTypeProvider);
+ break;
+ }
+
+ if (attrName == "ExportAttribute") {
+ registerInfo = ParseExportAttribute (ca, methodDef, index);
+ break;
+ }
+
+ if (attrName == "ExportFieldAttribute") {
+ registerInfo = ParseExportFieldAsRegisterInfo (methodDef, index);
+ exportFieldName = ParseExportFieldName (ca, index);
+ break;
+ }
+ }
+
+ if (registerInfo != null) {
+ AddMarshalMethod (methods, registerInfo, methodDef, index);
+ }
+
+ if (exportFieldName != null) {
+ var methodName = index.Reader.GetString (methodDef.Name);
+ var jniSig = registerInfo!.Signature ?? "()V";
+ bool isStatic = (methodDef.Attributes & MethodAttributes.Static) != 0;
+
+ exportFields.Add (new ExportFieldInfo {
+ FieldName = exportFieldName,
+ MethodName = methodName,
+ JniReturnType = JniSignatureHelper.ParseReturnTypeString (jniSig),
+ IsStatic = isStatic,
+ });
+ }
+ }
+
+ // Collect [Register] from properties (attribute is on the property, not the getter)
+ foreach (var propHandle in typeDef.GetProperties ()) {
+ var propDef = index.Reader.GetPropertyDefinition (propHandle);
+ var propRegister = TryGetPropertyRegisterInfo (propDef, index);
+ if (propRegister == null) {
+ continue;
+ }
+
+ var accessors = propDef.GetAccessors ();
+ if (!accessors.Getter.IsNil) {
+ var getterDef = index.Reader.GetMethodDefinition (accessors.Getter);
+ AddMarshalMethod (methods, propRegister, getterDef, index);
+ }
+ }
+
+ return (methods, exportFields);
+ }
+
+ void AddMarshalMethod (List methods, RegisterInfo registerInfo, MethodDefinition methodDef, AssemblyIndex index)
+ {
+ // Skip methods that are just the JNI name (type-level [Register])
+ if (registerInfo.Signature == null && registerInfo.Connector == null) {
+ return;
+ }
+
+ var methodName = index.Reader.GetString (methodDef.Name);
+ var jniSignature = registerInfo.Signature ?? "()V";
+ var parameters = ParseJniParameters (jniSignature);
+ bool isStatic = (methodDef.Attributes & MethodAttributes.Static) != 0;
+
+ // For [Export] methods and constructors, populate ManagedType from the actual
+ // method signature (needed for the generated marshal method body).
+ // Constructors always need this because there are no pre-existing n_* callbacks.
+ bool isConstructor = registerInfo.JniName == "" || registerInfo.JniName == ".ctor";
+ string? managedReturnType = null;
+ if (registerInfo.Connector == null || isConstructor) {
+ var sig = methodDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default);
+ for (int i = 0; i < parameters.Count && i < sig.ParameterTypes.Length; i++) {
+ parameters [i].ManagedType = ManagedTypeToAssemblyQualifiedName (sig.ParameterTypes [i], index);
+ }
+ if (sig.ReturnType != "System.Void") {
+ managedReturnType = ManagedTypeToAssemblyQualifiedName (sig.ReturnType, index);
+ }
+ }
+ methods.Add (new MarshalMethodInfo {
+ JniName = registerInfo.JniName,
+ JniSignature = registerInfo.Signature ?? "()V",
+ Connector = registerInfo.Connector,
+ ManagedMethodName = methodName,
+ NativeCallbackName = string.Concat ("n_", methodName),
+ JniReturnType = JniSignatureHelper.ParseReturnTypeString (jniSignature),
+ Parameters = parameters,
+ IsConstructor = isConstructor,
+ IsStatic = isStatic,
+ ThrownNames = registerInfo.ThrownNames,
+ SuperArgumentsString = registerInfo.SuperArgumentsString,
+ ManagedReturnType = managedReturnType,
+ });
+ }
+
+ string? ResolveBaseJavaName (TypeDefinition typeDef, AssemblyIndex index, Dictionary results)
+ {
+ var baseInfo = GetBaseTypeInfo (typeDef, index);
+ if (baseInfo == null) {
+ return null;
+ }
+
+ var (baseTypeName, baseAssemblyName) = baseInfo.Value;
+
+ // First try [Register] attribute
+ var registerJniName = ResolveRegisterJniName (baseTypeName, baseAssemblyName);
+ if (registerJniName != null) {
+ return registerJniName;
+ }
+
+ // Fall back to already-scanned results (component-attributed or CRC64-computed peers)
+ if (results.TryGetValue (baseTypeName, out var basePeer)) {
+ return basePeer.JavaName;
+ }
+
+ return null;
+ }
+
+ List ResolveImplementedInterfaceJavaNames (TypeDefinition typeDef, AssemblyIndex index)
+ {
+ var result = new List ();
+ var interfaceImpls = typeDef.GetInterfaceImplementations ();
+
+ foreach (var implHandle in interfaceImpls) {
+ var impl = index.Reader.GetInterfaceImplementation (implHandle);
+ var ifaceJniName = ResolveInterfaceJniName (impl.Interface, index);
+ if (ifaceJniName != null) {
+ result.Add (ifaceJniName);
+ }
+ }
+
+ return result;
+ }
+
+ string? ResolveInterfaceJniName (EntityHandle interfaceHandle, AssemblyIndex index)
+ {
+ var resolved = ResolveEntityHandle (interfaceHandle, index);
+ return resolved != null ? ResolveRegisterJniName (resolved.Value.typeName, resolved.Value.assemblyName) : null;
+ }
+
+ static RegisterInfo? TryGetPropertyRegisterInfo (PropertyDefinition propDef, AssemblyIndex index)
+ {
+ foreach (var caHandle in propDef.GetCustomAttributes ()) {
+ var ca = index.Reader.GetCustomAttribute (caHandle);
+ var attrName = AssemblyIndex.GetCustomAttributeName (ca, index.Reader);
+
+ if (attrName == "RegisterAttribute") {
+ return AssemblyIndex.ParseRegisterAttribute (ca, index.customAttributeTypeProvider);
+ }
+ }
+ return null;
+ }
+
+ RegisterInfo ParseExportAttribute (CustomAttribute ca, MethodDefinition methodDef, AssemblyIndex index)
+ {
+ var value = ca.DecodeValue (index.customAttributeTypeProvider);
+
+ // [Export("name")] or [Export] (uses method name)
+ string? exportName = null;
+ if (value.FixedArguments.Length > 0) {
+ exportName = (string?)value.FixedArguments [0].Value;
+ }
+
+ List? thrownNames = null;
+ string? superArguments = null;
+
+ // Check Named arguments
+ foreach (var named in value.NamedArguments) {
+ if (named.Name == "Name" && named.Value is string name) {
+ exportName = name;
+ } else if (named.Name == "ThrownNames") {
+ thrownNames = ExtractStringArray (named.Value);
+ } else if (named.Name == "SuperArgumentsString" && named.Value is string superArgs) {
+ superArguments = superArgs;
+ }
+ }
+
+ if (exportName == null || exportName.Length == 0) {
+ exportName = index.Reader.GetString (methodDef.Name);
+ }
+
+ // Build JNI signature from method signature
+ var sig = methodDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default);
+ var jniSig = BuildJniSignatureFromManaged (sig, index);
+
+ return new RegisterInfo (exportName, jniSig, null, false,
+ thrownNames: thrownNames, superArgumentsString: superArguments);
+ }
+
+ ///
+ /// Creates a RegisterInfo for an [ExportField] method.
+ /// The method is registered like [Export] (Connector = null) so it gets a full marshal body.
+ ///
+ RegisterInfo ParseExportFieldAsRegisterInfo (MethodDefinition methodDef, AssemblyIndex index)
+ {
+ var methodName = index.Reader.GetString (methodDef.Name);
+ var sig = methodDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default);
+ var jniSig = BuildJniSignatureFromManaged (sig, index);
+ return new RegisterInfo (methodName, jniSig, null, false);
+ }
+
+ ///
+ /// Extracts the field name from an [ExportField("FIELD_NAME")] attribute.
+ /// Returns null if the field name is empty or missing.
+ ///
+ static string? ParseExportFieldName (CustomAttribute ca, AssemblyIndex index)
+ {
+ var value = ca.DecodeValue (index.customAttributeTypeProvider);
+ if (value.FixedArguments.Length == 0) {
+ return null;
+ }
+
+ var fieldName = (string?)value.FixedArguments [0].Value;
+ return fieldName != null && fieldName.Length > 0 ? fieldName : null;
+ }
+
+ ///
+ /// Extracts a string array from a decoded custom attribute value.
+ /// SRM decodes string[] as ImmutableArray<CustomAttributeTypedArgument<string>>.
+ ///
+ static List? ExtractStringArray (object? value)
+ {
+ if (value is string[] directArray) {
+ return new List (directArray);
+ }
+
+ if (value is ImmutableArray> typedArray) {
+ var result = new List (typedArray.Length);
+ foreach (var item in typedArray) {
+ if (item.Value is string s) {
+ result.Add (s);
+ }
+ }
+ if (result.Count > 0) {
+ return result;
+ }
+ }
+
+ return null;
+ }
+
+ string BuildJniSignatureFromManaged (MethodSignature sig, AssemblyIndex index)
+ {
+ var sb = new System.Text.StringBuilder ();
+ sb.Append ('(');
+ foreach (var param in sig.ParameterTypes) {
+ sb.Append (ManagedTypeToJniDescriptor (param, index));
+ }
+ sb.Append (')');
+ sb.Append (ManagedTypeToJniDescriptor (sig.ReturnType, index));
+ return sb.ToString ();
+ }
+
+ string ManagedTypeToJniDescriptor (string managedType, AssemblyIndex index)
+ {
+ switch (managedType) {
+ case "System.Void": return "V";
+ case "System.Boolean": return "Z";
+ case "System.Byte":
+ case "System.SByte": return "B";
+ case "System.Char": return "C";
+ case "System.Int16":
+ case "System.UInt16": return "S";
+ case "System.Int32":
+ case "System.UInt32": return "I";
+ case "System.Int64":
+ case "System.UInt64": return "J";
+ case "System.Single": return "F";
+ case "System.Double": return "D";
+ case "System.String": return "Ljava/lang/String;";
+ case "Java.Lang.ICharSequence": return "Ljava/lang/CharSequence;";
+ default:
+ if (managedType.EndsWith ("[]")) {
+ return "[" + ManagedTypeToJniDescriptor (managedType.Substring (0, managedType.Length - 2), index);
+ }
+ if (IsEnumManagedType (managedType, index)) {
+ return "I";
+ }
+ var jniName = ResolveManagedTypeJniName (managedType, index);
+ if (!string.IsNullOrEmpty (jniName)) {
+ return "L" + jniName + ";";
+ }
+ return "Ljava/lang/Object;";
+ }
+ }
+
+ ///
+ /// Maps a managed type name (from SignatureTypeProvider) to an assembly-qualified name
+ /// like "System.Int32, System.Private.CoreLib" used in TypeManager.Activate calls.
+ ///
+ string ManagedTypeToAssemblyQualifiedName (string managedType, AssemblyIndex index)
+ {
+ if (managedType.IndexOf (", ", StringComparison.Ordinal) >= 0) {
+ return managedType;
+ }
+
+ // BCL types all live in System.Private.CoreLib
+ switch (managedType) {
+ case "System.Void":
+ case "System.Boolean":
+ case "System.Byte":
+ case "System.SByte":
+ case "System.Char":
+ case "System.Int16":
+ case "System.UInt16":
+ case "System.Int32":
+ case "System.UInt32":
+ case "System.Int64":
+ case "System.UInt64":
+ case "System.Single":
+ case "System.Double":
+ case "System.String":
+ case "System.Object":
+ case "System.IntPtr":
+ case "System.UIntPtr":
+ return managedType + ", System.Private.CoreLib";
+ default:
+ // Best-effort assembly resolution across loaded assemblies.
+ var assemblyName = ResolveManagedTypeAssemblyName (managedType, index);
+ return assemblyName != null ? managedType + ", " + assemblyName : managedType;
+ }
+ }
+
+ string? ResolveManagedTypeAssemblyName (string managedType, AssemblyIndex index)
+ {
+ string typeName = StripManagedTypeDecorations (managedType);
+ if (typeName.Length == 0 || typeName [0] == '!') {
+ return null; // generic method/type parameter
+ }
+
+ if (IsBclTypeName (typeName)) {
+ return "System.Private.CoreLib";
+ }
+
+ if (TryResolveManagedTypeDefinition (typeName, index, out _, out var resolvedIndex)) {
+ return resolvedIndex.AssemblyName;
+ }
+
+ return index.AssemblyName;
+ }
+
+ bool IsEnumManagedType (string managedType, AssemblyIndex index)
+ {
+ string typeName = StripManagedTypeDecorations (managedType);
+ if (typeName.Length == 0 || typeName [0] == '!') {
+ return false;
+ }
+
+ if (!TryResolveManagedTypeDefinition (typeName, index, out var handle, out var resolvedIndex)) {
+ return false;
+ }
+
+ var typeDef = resolvedIndex.Reader.GetTypeDefinition (handle);
+ if (typeDef.BaseType.Kind == HandleKind.TypeReference) {
+ var (baseTypeName, _) = ResolveTypeReference ((TypeReferenceHandle) typeDef.BaseType, resolvedIndex);
+ return baseTypeName == "System.Enum";
+ }
+ if (typeDef.BaseType.Kind == HandleKind.TypeDefinition) {
+ var baseTypeDef = resolvedIndex.Reader.GetTypeDefinition ((TypeDefinitionHandle) typeDef.BaseType);
+ return AssemblyIndex.GetFullName (baseTypeDef, resolvedIndex.Reader) == "System.Enum";
+ }
+ return false;
+ }
+
+ string? ResolveManagedTypeJniName (string managedType, AssemblyIndex index)
+ {
+ string typeName = StripManagedTypeDecorations (managedType);
+ if (typeName.Length == 0 || typeName [0] == '!') {
+ return null;
+ }
+
+ if (!TryResolveManagedTypeDefinition (typeName, index, out var handle, out var resolvedIndex)) {
+ return null;
+ }
+
+ if (resolvedIndex.RegisterInfoByType.TryGetValue (handle, out var regInfo) &&
+ !string.IsNullOrEmpty (regInfo.JniName)) {
+ return regInfo.JniName;
+ }
+
+ return null;
+ }
+
+ bool TryResolveManagedTypeDefinition (string managedTypeName, AssemblyIndex index, out TypeDefinitionHandle handle, out AssemblyIndex resolvedIndex)
+ {
+ if (TryResolveType (managedTypeName, index.AssemblyName, out handle, out resolvedIndex)) {
+ return true;
+ }
+
+ foreach (var candidate in assemblyCache.Values) {
+ if (candidate.TypesByFullName.TryGetValue (managedTypeName, out handle)) {
+ resolvedIndex = candidate;
+ return true;
+ }
+ }
+
+ handle = default;
+ resolvedIndex = null!;
+ return false;
+ }
+
+ static bool IsBclTypeName (string managedTypeName)
+ {
+ switch (managedTypeName) {
+ case "System.Void":
+ case "System.Boolean":
+ case "System.Byte":
+ case "System.SByte":
+ case "System.Char":
+ case "System.Int16":
+ case "System.UInt16":
+ case "System.Int32":
+ case "System.UInt32":
+ case "System.Int64":
+ case "System.UInt64":
+ case "System.Single":
+ case "System.Double":
+ case "System.String":
+ case "System.Object":
+ case "System.IntPtr":
+ case "System.UIntPtr":
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ static string StripManagedTypeDecorations (string managedType)
+ {
+ string result = managedType;
+ while (result.EndsWith ("[]", StringComparison.Ordinal) ||
+ result.EndsWith ("&", StringComparison.Ordinal) ||
+ result.EndsWith ("*", StringComparison.Ordinal)) {
+ result = result.Substring (0, result.Length - (result.EndsWith ("[]", StringComparison.Ordinal) ? 2 : 1));
+ }
+
+ int genericStart = result.IndexOf ('<');
+ if (genericStart >= 0) {
+ result = result.Substring (0, genericStart);
+ }
+
+ return result;
+ }
+
+ ActivationCtorInfo? ResolveActivationCtor (string typeName, TypeDefinition typeDef, AssemblyIndex index)
+ {
+ if (activationCtorCache.TryGetValue (typeName, out var cached)) {
+ return cached;
+ }
+
+ // Check this type's constructors
+ var ownCtor = FindActivationCtorOnType (typeDef, index);
+ if (ownCtor != null) {
+ var info = new ActivationCtorInfo { DeclaringTypeName = typeName, DeclaringAssemblyName = index.AssemblyName, Style = ownCtor.Value };
+ activationCtorCache [typeName] = info;
+ return info;
+ }
+
+ // Walk base type hierarchy
+ var baseInfo = GetBaseTypeInfo (typeDef, index);
+ if (baseInfo != null) {
+ var (baseTypeName, baseAssemblyName) = baseInfo.Value;
+ if (TryResolveType (baseTypeName, baseAssemblyName, out var baseHandle, out var baseIndex)) {
+ var baseTypeDef = baseIndex.Reader.GetTypeDefinition (baseHandle);
+ var result = ResolveActivationCtor (baseTypeName, baseTypeDef, baseIndex);
+ if (result != null) {
+ activationCtorCache [typeName] = result;
+ }
+ return result;
+ }
+ }
+
+ return null;
+ }
+
+ static ActivationCtorStyle? FindActivationCtorOnType (TypeDefinition typeDef, AssemblyIndex index)
+ {
+ foreach (var methodHandle in typeDef.GetMethods ()) {
+ var method = index.Reader.GetMethodDefinition (methodHandle);
+ var name = index.Reader.GetString (method.Name);
+
+ if (name != ".ctor") {
+ continue;
+ }
+
+ var sig = method.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default);
+
+ // XI style: (IntPtr, JniHandleOwnership)
+ if (sig.ParameterTypes.Length == 2 &&
+ sig.ParameterTypes [0] == "System.IntPtr" &&
+ sig.ParameterTypes [1] == "Android.Runtime.JniHandleOwnership") {
+ return ActivationCtorStyle.XamarinAndroid;
+ }
+
+ // JI style: (ref JniObjectReference, JniObjectReferenceOptions)
+ if (sig.ParameterTypes.Length == 2 &&
+ (sig.ParameterTypes [0] == "Java.Interop.JniObjectReference&" || sig.ParameterTypes [0] == "Java.Interop.JniObjectReference") &&
+ sig.ParameterTypes [1] == "Java.Interop.JniObjectReferenceOptions") {
+ return ActivationCtorStyle.JavaInterop;
+ }
+ }
+
+ return null;
+ }
+
+ ///
+ /// Resolves a TypeSpecificationHandle (generic instantiation) to the underlying
+ /// type's (fullName, assemblyName) by reading the raw signature blob.
+ ///
+ static (string fullName, string assemblyName)? ResolveTypeSpecification (TypeSpecificationHandle specHandle, AssemblyIndex index)
+ {
+ var typeSpec = index.Reader.GetTypeSpecification (specHandle);
+ var blobReader = index.Reader.GetBlobReader (typeSpec.Signature);
+
+ // Generic instantiation blob: GENERICINST (CLASS|VALUETYPE) coded-token count args...
+ var elementType = blobReader.ReadByte ();
+ if (elementType != 0x15) { // ELEMENT_TYPE_GENERICINST
+ return null;
+ }
+
+ var classOrValueType = blobReader.ReadByte ();
+ if (classOrValueType != 0x12 && classOrValueType != 0x11) { // CLASS or VALUETYPE
+ return null;
+ }
+
+ // TypeDefOrRefOrSpec coded index: 2 tag bits (0=TypeDef, 1=TypeRef, 2=TypeSpec)
+ var codedToken = blobReader.ReadCompressedInteger ();
+ var tag = codedToken & 0x3;
+ var row = codedToken >> 2;
+
+ switch (tag) {
+ case 0: { // TypeDef
+ var handle = MetadataTokens.TypeDefinitionHandle (row);
+ var baseDef = index.Reader.GetTypeDefinition (handle);
+ return (AssemblyIndex.GetFullName (baseDef, index.Reader), index.AssemblyName);
+ }
+ case 1: // TypeRef
+ return ResolveTypeReference (MetadataTokens.TypeReferenceHandle (row), index);
+ default:
+ return null;
+ }
+ }
+
+ ///
+ /// Resolves an EntityHandle (TypeDef, TypeRef, or TypeSpec) to (typeName, assemblyName).
+ /// Shared by base type resolution, interface resolution, and any handle-to-name lookup.
+ ///
+ (string typeName, string assemblyName)? ResolveEntityHandle (EntityHandle handle, AssemblyIndex index)
+ {
+ switch (handle.Kind) {
+ case HandleKind.TypeDefinition: {
+ var td = index.Reader.GetTypeDefinition ((TypeDefinitionHandle)handle);
+ return (AssemblyIndex.GetFullName (td, index.Reader), index.AssemblyName);
+ }
+ case HandleKind.TypeReference:
+ return ResolveTypeReference ((TypeReferenceHandle)handle, index);
+ case HandleKind.TypeSpecification:
+ return ResolveTypeSpecification ((TypeSpecificationHandle)handle, index);
+ default:
+ return null;
+ }
+ }
+
+ (string typeName, string assemblyName)? GetBaseTypeInfo (TypeDefinition typeDef, AssemblyIndex index)
+ {
+ return typeDef.BaseType.IsNil ? null : ResolveEntityHandle (typeDef.BaseType, index);
+ }
+
+ string? TryFindInvokerTypeName (string typeName, TypeDefinitionHandle typeHandle, AssemblyIndex index)
+ {
+ // First, check the [Register] attribute's connector arg (3rd arg).
+ // In real Mono.Android, interfaces have [Register("jni/name", "", "InvokerTypeName, Assembly")]
+ // where the connector contains the assembly-qualified invoker type name.
+ if (index.RegisterInfoByType.TryGetValue (typeHandle, out var registerInfo) && registerInfo.Connector != null) {
+ var connector = registerInfo.Connector;
+ // The connector may be "TypeName" or "TypeName, Assembly, Version=..., Culture=..., PublicKeyToken=..."
+ // We want just the type name (before the first comma, if any)
+ var commaIndex = connector.IndexOf (',');
+ if (commaIndex > 0) {
+ return connector.Substring (0, commaIndex).Trim ();
+ }
+ if (connector.Length > 0) {
+ return connector;
+ }
+ }
+
+ // Fallback: convention-based lookup — invoker type is TypeName + "Invoker"
+ var invokerName = typeName + "Invoker";
+ if (index.TypesByFullName.ContainsKey (invokerName)) {
+ return invokerName;
+ }
+ return null;
+ }
+
+ public void Dispose ()
+ {
+ foreach (var index in assemblyCache.Values) {
+ index.Dispose ();
+ }
+ assemblyCache.Clear ();
+ }
+
+ readonly Dictionary extendsJavaPeerCache = new (StringComparer.Ordinal);
+
+ ///
+ /// Check if a type extends a known Java peer (has [Register] or component attribute)
+ /// by walking the base type chain. Results are cached; false-before-recurse prevents cycles.
+ ///
+ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index)
+ {
+ var fullName = AssemblyIndex.GetFullName (typeDef, index.Reader);
+ var key = index.AssemblyName + ":" + fullName;
+
+ if (extendsJavaPeerCache.TryGetValue (key, out var cached)) {
+ return cached;
+ }
+
+ // Mark as false to prevent cycles, then compute
+ extendsJavaPeerCache [key] = false;
+
+ var baseInfo = GetBaseTypeInfo (typeDef, index);
+ if (baseInfo == null) {
+ return false;
+ }
+
+ var (baseTypeName, baseAssemblyName) = baseInfo.Value;
+
+ if (!TryResolveType (baseTypeName, baseAssemblyName, out var baseHandle, out var baseIndex)) {
+ return false;
+ }
+
+ // Direct hit: base has [Register] or component attribute
+ if (baseIndex.RegisterInfoByType.ContainsKey (baseHandle)) {
+ extendsJavaPeerCache [key] = true;
+ return true;
+ }
+ if (baseIndex.AttributesByType.TryGetValue (baseHandle, out var attrInfo) && attrInfo.HasComponentAttribute) {
+ extendsJavaPeerCache [key] = true;
+ return true;
+ }
+
+ // Recurse up the hierarchy
+ var baseDef = baseIndex.Reader.GetTypeDefinition (baseHandle);
+ var result = ExtendsJavaPeer (baseDef, baseIndex);
+ extendsJavaPeerCache [key] = result;
+ return result;
+ }
+
+ ///
+ /// Compute both JNI name and compat JNI name for a type without [Register] or component Name.
+ /// JNI name uses CRC64 hash of "namespace:assemblyName" for the package.
+ /// Compat JNI name uses the raw managed namespace (lowercased).
+ /// If a declaring type has [Register], its JNI name is used as prefix for both.
+ /// Generic backticks are replaced with _.
+ ///
+ static (string jniName, string compatJniName) ComputeAutoJniNames (TypeDefinition typeDef, AssemblyIndex index)
+ {
+ var (typeName, parentJniName, ns) = ComputeTypeNameParts (typeDef, index);
+
+ if (parentJniName != null) {
+ var name = parentJniName + "_" + typeName;
+ return (name, name);
+ }
+
+ var packageName = GetCrc64PackageName (ns, index.AssemblyName);
+ var jniName = packageName + "/" + typeName;
+
+ string compatName = ns.Length == 0
+ ? typeName
+ : ns.ToLowerInvariant ().Replace ('.', '/') + "/" + typeName;
+
+ return (jniName, compatName);
+ }
+
+ ///
+ /// Builds the type name part (handling nesting) and returns either a parent's
+ /// registered JNI name or the outermost namespace.
+ /// Matches JavaNativeTypeManager.ToJniName behavior: walks up declaring types
+ /// and if a parent has [Register] or a component attribute JNI name, uses that
+ /// as prefix instead of computing CRC64 from the namespace.
+ ///
+ static (string typeName, string? parentJniName, string ns) ComputeTypeNameParts (TypeDefinition typeDef, AssemblyIndex index)
+ {
+ var firstName = index.Reader.GetString (typeDef.Name).Replace ('`', '_');
+
+ // Fast path: non-nested types (the vast majority)
+ if (!typeDef.IsNested) {
+ return (firstName, null, index.Reader.GetString (typeDef.Namespace));
+ }
+
+ // Nested type: walk up declaring types, collecting name parts
+ var nameParts = new List (4) { firstName };
+ var current = typeDef;
+ string? parentJniName = null;
+
+ do {
+ var parentHandle = current.GetDeclaringType ();
+ current = index.Reader.GetTypeDefinition (parentHandle);
+
+ // Check if the parent has a registered JNI name
+ if (index.RegisterInfoByType.TryGetValue (parentHandle, out var parentRegister) && !string.IsNullOrEmpty (parentRegister.JniName)) {
+ parentJniName = parentRegister.JniName;
+ break;
+ }
+ if (index.AttributesByType.TryGetValue (parentHandle, out var parentAttr) && parentAttr.ComponentAttributeJniName != null) {
+ parentJniName = parentAttr.ComponentAttributeJniName;
+ break;
+ }
+
+ nameParts.Add (index.Reader.GetString (current.Name).Replace ('`', '_'));
+ } while (current.IsNested);
+
+ nameParts.Reverse ();
+ var typeName = string.Join ("_", nameParts);
+ var ns = index.Reader.GetString (current.Namespace);
+
+ return (typeName, parentJniName, ns);
+ }
+
+ static string GetCrc64PackageName (string ns, string assemblyName)
+ {
+ // Only Mono.Android preserves the namespace directly
+ if (assemblyName == "Mono.Android") {
+ return ns.ToLowerInvariant ().Replace ('.', '/');
+ }
+
+ var data = System.Text.Encoding.UTF8.GetBytes (ns + ":" + assemblyName);
+ var hash = System.IO.Hashing.Crc64.Hash (data);
+ return "crc64" + BitConverter.ToString (hash).Replace ("-", "").ToLowerInvariant ();
+ }
+
+ static string ExtractNamespace (string fullName)
+ {
+ int lastDot = fullName.LastIndexOf ('.');
+ return lastDot >= 0 ? fullName.Substring (0, lastDot) : "";
+ }
+
+ static string ExtractShortName (string fullName)
+ {
+ int lastDot = fullName.LastIndexOf ('.');
+ return lastDot >= 0 ? fullName.Substring (lastDot + 1) : fullName;
+ }
+
+ static List ParseJniParameters (string jniSignature)
+ {
+ var typeStrings = JniSignatureHelper.ParseParameterTypeStrings (jniSignature);
+ var result = new List (typeStrings.Count);
+ foreach (var t in typeStrings) {
+ result.Add (new JniParameterInfo { JniType = t });
+ }
+ return result;
+ }
+
+ static List BuildJavaConstructors (List marshalMethods)
+ {
+ var ctors = new List ();
+ int ctorIndex = 0;
+ foreach (var mm in marshalMethods) {
+ if (!mm.IsConstructor) {
+ continue;
+ }
+ ctors.Add (new JavaConstructorInfo {
+ JniSignature = mm.JniSignature,
+ ConstructorIndex = ctorIndex,
+ Parameters = mm.Parameters,
+ SuperArgumentsString = mm.SuperArgumentsString,
+ IsExport = mm.Connector == null,
+ ThrownNames = mm.ThrownNames,
+ });
+ ctorIndex++;
+ }
+ return ctors;
+ }
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/MetadataTypeNameResolver.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/MetadataTypeNameResolver.cs
new file mode 100644
index 00000000000..dcc3d5b7db9
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/MetadataTypeNameResolver.cs
@@ -0,0 +1,39 @@
+using System.Reflection.Metadata;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+///
+/// Shared logic for resolving fully qualified type names from metadata handles.
+/// Used by both and .
+///
+static class MetadataTypeNameResolver
+{
+ public static string GetFullName (TypeDefinition typeDef, MetadataReader reader)
+ {
+ var name = reader.GetString (typeDef.Name);
+ var ns = reader.GetString (typeDef.Namespace);
+ if (typeDef.IsNested) {
+ var declaringType = reader.GetTypeDefinition (typeDef.GetDeclaringType ());
+ var parentName = GetFullName (declaringType, reader);
+ return $"{parentName}+{name}";
+ }
+ return ns.Length > 0 ? $"{ns}.{name}" : name;
+ }
+
+ public static string GetTypeFromDefinition (MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind)
+ {
+ return GetFullName (reader.GetTypeDefinition (handle), reader);
+ }
+
+ public static string GetTypeFromReference (MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind)
+ {
+ var typeRef = reader.GetTypeReference (handle);
+ var name = reader.GetString (typeRef.Name);
+ if (typeRef.ResolutionScope.Kind == HandleKind.TypeReference) {
+ var parent = GetTypeFromReference (reader, (TypeReferenceHandle)typeRef.ResolutionScope, rawTypeKind);
+ return $"{parent}+{name}";
+ }
+ var ns = reader.GetString (typeRef.Namespace);
+ return ns.Length > 0 ? $"{ns}.{name}" : name;
+ }
+}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/SignatureTypeProvider.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/SignatureTypeProvider.cs
new file mode 100644
index 00000000000..87ed078adf2
--- /dev/null
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/SignatureTypeProvider.cs
@@ -0,0 +1,66 @@
+using System;
+using System.Collections.Immutable;
+using System.Reflection.Metadata;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap;
+
+///
+/// Minimal ISignatureTypeProvider implementation for decoding method
+/// signatures via System.Reflection.Metadata.
+/// Returns fully qualified type name strings.
+///
+sealed class SignatureTypeProvider : ISignatureTypeProvider
+{
+ public static readonly SignatureTypeProvider Instance = new ();
+
+ public string GetPrimitiveType (PrimitiveTypeCode typeCode) => typeCode switch {
+ PrimitiveTypeCode.Void => "System.Void",
+ PrimitiveTypeCode.Boolean => "System.Boolean",
+ PrimitiveTypeCode.Char => "System.Char",
+ PrimitiveTypeCode.SByte => "System.SByte",
+ PrimitiveTypeCode.Byte => "System.Byte",
+ PrimitiveTypeCode.Int16 => "System.Int16",
+ PrimitiveTypeCode.UInt16 => "System.UInt16",
+ PrimitiveTypeCode.Int32 => "System.Int32",
+ PrimitiveTypeCode.UInt32 => "System.UInt32",
+ PrimitiveTypeCode.Int64 => "System.Int64",
+ PrimitiveTypeCode.UInt64 => "System.UInt64",
+ PrimitiveTypeCode.Single => "System.Single",
+ PrimitiveTypeCode.Double => "System.Double",
+ PrimitiveTypeCode.String => "System.String",
+ PrimitiveTypeCode.Object => "System.Object",
+ PrimitiveTypeCode.IntPtr => "System.IntPtr",
+ PrimitiveTypeCode.UIntPtr => "System.UIntPtr",
+ PrimitiveTypeCode.TypedReference => "System.TypedReference",
+ _ => typeCode.ToString (),
+ };
+
+ public string GetTypeFromDefinition (MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind)
+ => MetadataTypeNameResolver.GetTypeFromDefinition (reader, handle, rawTypeKind);
+
+ public string GetTypeFromReference (MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind)
+ => MetadataTypeNameResolver.GetTypeFromReference (reader, handle, rawTypeKind);
+
+ public string GetTypeFromSpecification (MetadataReader reader, object? genericContext, TypeSpecificationHandle handle, byte rawTypeKind)
+ {
+ var typeSpec = reader.GetTypeSpecification (handle);
+ return typeSpec.DecodeSignature (this, genericContext);
+ }
+
+ public string GetSZArrayType (string elementType) => $"{elementType}[]";
+ public string GetArrayType (string elementType, ArrayShape shape) => $"{elementType}[{new string (',', shape.Rank - 1)}]";
+ public string GetByReferenceType (string elementType) => $"{elementType}&";
+ public string GetPointerType (string elementType) => $"{elementType}*";
+ public string GetPinnedType (string elementType) => elementType;
+ public string GetModifiedType (string modifier, string unmodifiedType, bool isRequired) => unmodifiedType;
+
+ public string GetGenericInstantiation (string genericType, ImmutableArray typeArguments)
+ {
+ return $"{genericType}<{string.Join (",", typeArguments)}>";
+ }
+
+ public string GetGenericTypeParameter (object? genericContext, int index) => $"!{index}";
+ public string GetGenericMethodParameter (object? genericContext, int index) => $"!!{index}";
+
+ public string GetFunctionPointerType (MethodSignature signature) => "delegate*";
+}
diff --git a/src/Xamarin.Android.Build.Tasks/Properties/AssemblyInfo.cs b/src/Xamarin.Android.Build.Tasks/Properties/AssemblyInfo.cs
index 0bd860a35e2..e66435ccc40 100644
--- a/src/Xamarin.Android.Build.Tasks/Properties/AssemblyInfo.cs
+++ b/src/Xamarin.Android.Build.Tasks/Properties/AssemblyInfo.cs
@@ -20,3 +20,4 @@
[assembly: InternalsVisibleTo ("Xamarin.Android.Build.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000011000000438ac2a5acfbf16cbd2b2b47a62762f273df9cb2795ceccdf77d10bf508e69e7a362ea7a45455bbf3ac955e1f2e2814f144e5d817efc4c6502cc012df310783348304e3ae38573c6d658c234025821fda87a0be8a0d504df564e2c93b2b878925f42503e9d54dfef9f9586d9e6f38a305769587b1de01f6c0410328b2c9733db")]
[assembly: InternalsVisibleTo ("MSBuildDeviceIntegration, PublicKey=0024000004800000940000000602000000240000525341310004000011000000438ac2a5acfbf16cbd2b2b47a62762f273df9cb2795ceccdf77d10bf508e69e7a362ea7a45455bbf3ac955e1f2e2814f144e5d817efc4c6502cc012df310783348304e3ae38573c6d658c234025821fda87a0be8a0d504df564e2c93b2b878925f42503e9d54dfef9f9586d9e6f38a305769587b1de01f6c0410328b2c9733db")]
+[assembly: InternalsVisibleTo ("Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests, PublicKey=0024000004800000940000000602000000240000525341310004000011000000438ac2a5acfbf16cbd2b2b47a62762f273df9cb2795ceccdf77d10bf508e69e7a362ea7a45455bbf3ac955e1f2e2814f144e5d817efc4c6502cc012df310783348304e3ae38573c6d658c234025821fda87a0be8a0d504df564e2c93b2b878925f42503e9d54dfef9f9586d9e6f38a305769587b1de01f6c0410328b2c9733db")]
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests.csproj b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests.csproj
new file mode 100644
index 00000000000..35c76b21bbc
--- /dev/null
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests.csproj
@@ -0,0 +1,56 @@
+
+
+
+ $(DotNetTargetFramework)
+ latest
+ enable
+ false
+ Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests
+ true
+ ..\..\product.snk
+ ..\..\bin\Test$(Configuration)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <_MonoAndroidRefCandidate Include="$(MSBuildThisFileDirectory)..\..\bin\$(Configuration)\lib\packs\Microsoft.Android.Ref.*\*\ref\net*\Mono.Android.dll" />
+
+
+ <_MonoAndroidRefAssembly>@(_MonoAndroidRefCandidate, ';')
+ <_MonoAndroidRefAssembly>$(_MonoAndroidRefAssembly.Split(';')[0])
+
+
+
+ $(_MonoAndroidRefAssembly)
+
+
+
+
+
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/MockBuildEngine.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/MockBuildEngine.cs
new file mode 100644
index 00000000000..d6c8c19d1eb
--- /dev/null
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/MockBuildEngine.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Collections;
+using Microsoft.Build.Framework;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests;
+
+///
+/// Minimal IBuildEngine implementation for use with TaskLoggingHelper in tests.
+///
+sealed class MockBuildEngine : IBuildEngine
+{
+ public bool ContinueOnError => false;
+ public int LineNumberOfTaskNode => 0;
+ public int ColumnNumberOfTaskNode => 0;
+ public string ProjectFileOfTaskNode => "";
+
+ public bool BuildProjectFile (string projectFileName, string [] targetNames, IDictionary globalProperties, IDictionary targetOutputs) => true;
+ public void LogCustomEvent (CustomBuildEventArgs e) { }
+ public void LogErrorEvent (BuildErrorEventArgs e) { }
+ public void LogMessageEvent (BuildMessageEventArgs e) { }
+ public void LogWarningEvent (BuildWarningEventArgs e) { }
+}
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs
new file mode 100644
index 00000000000..8ee29061de0
--- /dev/null
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerComparisonTests.cs
@@ -0,0 +1,1086 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Java.Interop.Tools.Cecil;
+using Java.Interop.Tools.TypeNameMappings;
+using Microsoft.Build.Utilities;
+using Mono.Cecil;
+using Xamarin.Android.Tasks;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests;
+
+public class ScannerComparisonTests
+{
+ readonly ITestOutputHelper output;
+
+ public ScannerComparisonTests (ITestOutputHelper output)
+ {
+ this.output = output;
+ }
+
+ record TypeMapEntry (string JavaName, string ManagedName, bool SkipInJavaToManaged);
+
+ record MethodEntry (string JniName, string JniSignature, string? Connector);
+
+ record TypeMethodGroup (string ManagedName, List Methods);
+
+ static (List entries, Dictionary> methodsByJavaName) RunLegacyScanner (string assemblyPath)
+ {
+ var cache = new TypeDefinitionCache ();
+ var resolver = new DefaultAssemblyResolver ();
+ resolver.AddSearchDirectory (Path.GetDirectoryName (assemblyPath)!);
+
+ var runtimeDir = Path.GetDirectoryName (typeof (object).Assembly.Location);
+ if (runtimeDir != null) {
+ resolver.AddSearchDirectory (runtimeDir);
+ }
+
+ var readerParams = new ReaderParameters { AssemblyResolver = resolver };
+ using var assembly = AssemblyDefinition.ReadAssembly (assemblyPath, readerParams);
+
+ var scanner = new Xamarin.Android.Tasks.XAJavaTypeScanner (
+ Xamarin.Android.Tools.AndroidTargetArch.Arm64,
+ new TaskLoggingHelper (new MockBuildEngine (), "test"),
+ cache
+ );
+
+ var javaTypes = scanner.GetJavaTypes (assembly);
+ var (dataSets, _) = Xamarin.Android.Tasks.TypeMapCecilAdapter.GetDebugNativeEntries (
+ javaTypes, cache, needUniqueAssemblies: false
+ );
+
+ var entries = dataSets.JavaToManaged
+ .Select (e => new TypeMapEntry (e.JavaName, e.ManagedName, e.SkipInJavaToManaged))
+ .OrderBy (e => e.JavaName, StringComparer.Ordinal)
+ .ThenBy (e => e.ManagedName, StringComparer.Ordinal)
+ .ToList ();
+
+ // Extract method-level [Register] attributes from each TypeDefinition.
+ // Use the raw javaTypes list to get ALL types — multiple managed types
+ // can map to the same JNI name (aliases).
+ var methodsByJavaName = new Dictionary> ();
+ foreach (var typeDef in javaTypes) {
+ var javaName = GetCecilJavaName (typeDef);
+ if (javaName == null) {
+ continue;
+ }
+
+ // Cecil uses '/' for nested types, SRM uses '+' (CLR format) — normalize
+ var managedName = $"{typeDef.FullName.Replace ('/', '+')}, {typeDef.Module.Assembly.Name.Name}";
+ var methods = ExtractMethodRegistrations (typeDef);
+
+ if (!methodsByJavaName.TryGetValue (javaName, out var groups)) {
+ groups = new List ();
+ methodsByJavaName [javaName] = groups;
+ }
+
+ groups.Add (new TypeMethodGroup (
+ managedName,
+ methods.OrderBy (m => m.JniName, StringComparer.Ordinal)
+ .ThenBy (m => m.JniSignature, StringComparer.Ordinal)
+ .ToList ()
+ ));
+ }
+
+ // Some types appear in dataSets.JavaToManaged (the typemap) but not in
+ // javaTypes (the raw list). Include them with empty method lists so the
+ // comparison covers all types known to the legacy scanner.
+ foreach (var entry in dataSets.JavaToManaged) {
+ if (methodsByJavaName.ContainsKey (entry.JavaName)) {
+ continue;
+ }
+
+ methodsByJavaName [entry.JavaName] = new List {
+ new TypeMethodGroup (entry.ManagedName, new List ())
+ };
+ }
+
+ return (entries, methodsByJavaName);
+ }
+
+ static string? GetCecilJavaName (TypeDefinition typeDef)
+ {
+ if (!typeDef.HasCustomAttributes) {
+ return null;
+ }
+
+ foreach (var attr in typeDef.CustomAttributes) {
+ if (attr.AttributeType.FullName != "Android.Runtime.RegisterAttribute") {
+ continue;
+ }
+
+ if (attr.ConstructorArguments.Count > 0) {
+ return ((string) attr.ConstructorArguments [0].Value).Replace ('.', '/');
+ }
+ }
+
+ return null;
+ }
+
+ static List ExtractMethodRegistrations (TypeDefinition typeDef)
+ {
+ var methods = new List ();
+
+ // Collect [Register] from methods directly
+ foreach (var method in typeDef.Methods) {
+ if (!method.HasCustomAttributes) {
+ continue;
+ }
+
+ foreach (var attr in method.CustomAttributes) {
+ if (attr.AttributeType.FullName != "Android.Runtime.RegisterAttribute") {
+ continue;
+ }
+
+ if (attr.ConstructorArguments.Count < 2) {
+ continue;
+ }
+
+ var jniMethodName = (string) attr.ConstructorArguments [0].Value;
+ var jniSignature = (string) attr.ConstructorArguments [1].Value;
+ var connector = attr.ConstructorArguments.Count > 2
+ ? (string) attr.ConstructorArguments [2].Value
+ : null;
+
+ methods.Add (new MethodEntry (jniMethodName, jniSignature, connector));
+ }
+ }
+
+ // Collect [Register] from properties (attribute is on the property, not the getter/setter)
+ if (typeDef.HasProperties) {
+ foreach (var prop in typeDef.Properties) {
+ if (!prop.HasCustomAttributes) {
+ continue;
+ }
+
+ foreach (var attr in prop.CustomAttributes) {
+ if (attr.AttributeType.FullName != "Android.Runtime.RegisterAttribute") {
+ continue;
+ }
+
+ if (attr.ConstructorArguments.Count < 2) {
+ continue;
+ }
+
+ var jniMethodName = (string) attr.ConstructorArguments [0].Value;
+ var jniSignature = (string) attr.ConstructorArguments [1].Value;
+ var connector = attr.ConstructorArguments.Count > 2
+ ? (string) attr.ConstructorArguments [2].Value
+ : null;
+
+ methods.Add (new MethodEntry (jniMethodName, jniSignature, connector));
+ }
+ }
+ }
+
+ return methods;
+ }
+
+ static (List entries, Dictionary> methodsByJavaName) RunNewScanner (string[] assemblyPaths)
+ {
+ var primaryAssemblyName = Path.GetFileNameWithoutExtension (assemblyPaths [0]);
+ using var scanner = new JavaPeerScanner ();
+ var allPeers = scanner.Scan (assemblyPaths);
+ var peers = allPeers.Where (p => p.AssemblyName == primaryAssemblyName).ToList ();
+
+ var entries = peers
+ .Select (p => new TypeMapEntry (
+ p.JavaName,
+ $"{p.ManagedTypeName}, {p.AssemblyName}",
+ p.IsInterface || p.IsGenericDefinition
+ ))
+ .OrderBy (e => e.JavaName, StringComparer.Ordinal)
+ .ThenBy (e => e.ManagedName, StringComparer.Ordinal)
+ .ToList ();
+
+ var methodsByJavaName = new Dictionary> ();
+ foreach (var peer in peers) {
+ var managedName = $"{peer.ManagedTypeName}, {peer.AssemblyName}";
+
+ if (!methodsByJavaName.TryGetValue (peer.JavaName, out var groups)) {
+ groups = new List ();
+ methodsByJavaName [peer.JavaName] = groups;
+ }
+
+ groups.Add (new TypeMethodGroup (
+ managedName,
+ peer.MarshalMethods
+ .Select (m => new MethodEntry (m.JniName, m.JniSignature, m.Connector))
+ .OrderBy (m => m.JniName, StringComparer.Ordinal)
+ .ThenBy (m => m.JniSignature, StringComparer.Ordinal)
+ .ToList ()
+ ));
+ }
+
+ return (entries, methodsByJavaName);
+ }
+
+ [Fact]
+ public void ExactTypeMap_MonoAndroid ()
+ {
+ var (legacy, _) = RunLegacyScanner (MonoAndroidAssemblyPath);
+ var (newEntries, _) = RunNewScanner (AllAssemblyPaths);
+ output.WriteLine ($"Legacy: {legacy.Count} entries, New: {newEntries.Count} entries");
+ AssertTypeMapMatch (legacy, newEntries);
+ }
+
+ [Fact]
+ public void ExactMarshalMethods_MonoAndroid ()
+ {
+ var assemblyPath = MonoAndroidAssemblyPath;
+
+ var (_, legacyMethods) = RunLegacyScanner (assemblyPath);
+ var (_, newMethods) = RunNewScanner (AllAssemblyPaths);
+
+ var legacyTypeCount = legacyMethods.Values.Sum (g => g.Count);
+ var newTypeCount = newMethods.Values.Sum (g => g.Count);
+ var legacyMethodCount = legacyMethods.Values.Sum (g => g.Sum (t => t.Methods.Count));
+ var newMethodCount = newMethods.Values.Sum (g => g.Sum (t => t.Methods.Count));
+ output.WriteLine ($"Legacy: {legacyTypeCount} type groups across {legacyMethods.Count} JNI names, {legacyMethodCount} total methods");
+ output.WriteLine ($"New: {newTypeCount} type groups across {newMethods.Count} JNI names, {newMethodCount} total methods");
+
+ var allJavaNames = new HashSet (legacyMethods.Keys);
+ allJavaNames.UnionWith (newMethods.Keys);
+
+ var missingTypes = new List ();
+ var extraTypes = new List ();
+ var missingMethods = new List ();
+ var extraMethods = new List ();
+ var connectorMismatches = new List ();
+
+ foreach (var javaName in allJavaNames.OrderBy (n => n)) {
+ var inLegacy = legacyMethods.TryGetValue (javaName, out var legacyGroups);
+ var inNew = newMethods.TryGetValue (javaName, out var newGroups);
+
+ if (inLegacy && !inNew) {
+ foreach (var g in legacyGroups!) {
+ missingTypes.Add ($"{javaName} → {g.ManagedName} ({g.Methods.Count} methods)");
+ }
+ continue;
+ }
+
+ if (!inLegacy && inNew) {
+ foreach (var g in newGroups!) {
+ extraTypes.Add ($"{javaName} → {g.ManagedName} ({g.Methods.Count} methods)");
+ }
+ continue;
+ }
+
+ // Both scanners found this JNI name — compare managed types within it
+ var legacyByManaged = legacyGroups!.ToDictionary (g => g.ManagedName, g => g.Methods);
+ var newByManaged = newGroups!.ToDictionary (g => g.ManagedName, g => g.Methods);
+
+ foreach (var managedName in legacyByManaged.Keys.Except (newByManaged.Keys)) {
+ missingTypes.Add ($"{javaName} → {managedName} ({legacyByManaged [managedName].Count} methods)");
+ }
+
+ foreach (var managedName in newByManaged.Keys.Except (legacyByManaged.Keys)) {
+ extraTypes.Add ($"{javaName} → {managedName} ({newByManaged [managedName].Count} methods)");
+ }
+
+ // For managed types present in both, compare their method sets
+ foreach (var managedName in legacyByManaged.Keys.Intersect (newByManaged.Keys)) {
+ var legacyMethodList = legacyByManaged [managedName];
+ var newMethodList = newByManaged [managedName];
+
+ var legacySet = new HashSet<(string name, string sig)> (
+ legacyMethodList.Select (m => (m.JniName, m.JniSignature))
+ );
+ var newSet = new HashSet<(string name, string sig)> (
+ newMethodList.Select (m => (m.JniName, m.JniSignature))
+ );
+
+ foreach (var m in legacySet.Except (newSet)) {
+ missingMethods.Add ($"{javaName} [{managedName}]: {m.name}{m.sig}");
+ }
+
+ foreach (var m in newSet.Except (legacySet)) {
+ extraMethods.Add ($"{javaName} [{managedName}]: {m.name}{m.sig}");
+ }
+
+ // For methods in both, compare connector strings
+ var legacyByKey = legacyMethodList
+ .GroupBy (m => (m.JniName, m.JniSignature))
+ .ToDictionary (g => g.Key, g => g.First ());
+ var newByKey = newMethodList
+ .GroupBy (m => (m.JniName, m.JniSignature))
+ .ToDictionary (g => g.Key, g => g.First ());
+
+ foreach (var key in legacyByKey.Keys.Intersect (newByKey.Keys)) {
+ var lc = legacyByKey [key].Connector ?? "";
+ var nc = newByKey [key].Connector ?? "";
+ if (lc != nc) {
+ connectorMismatches.Add ($"{javaName} [{managedName}]: {key.JniName}{key.JniSignature} legacy='{lc}' new='{nc}'");
+ }
+ }
+ }
+ }
+
+ LogDiffs ("MANAGED TYPES MISSING from new scanner", missingTypes);
+ LogDiffs ("MANAGED TYPES EXTRA in new scanner", extraTypes);
+ LogDiffs ("METHODS MISSING from new scanner", missingMethods);
+ LogDiffs ("METHODS EXTRA in new scanner", extraMethods);
+ LogDiffs ("CONNECTOR MISMATCHES", connectorMismatches);
+
+ Assert.Empty (missingTypes);
+ Assert.Empty (extraTypes);
+ Assert.Empty (missingMethods);
+ Assert.Empty (extraMethods);
+ Assert.Empty (connectorMismatches);
+ }
+
+ [Fact]
+ public void ScannerDiagnostics_MonoAndroid ()
+ {
+ var assemblyPath = MonoAndroidAssemblyPath;
+
+ using var scanner = new JavaPeerScanner ();
+ var peers = scanner.Scan (new [] { assemblyPath });
+
+ var interfaces = peers.Count (p => p.IsInterface);
+ var abstracts = peers.Count (p => p.IsAbstract);
+ var generics = peers.Count (p => p.IsGenericDefinition);
+ var withMethods = peers.Count (p => p.MarshalMethods.Count > 0);
+ var totalMethods = peers.Sum (p => p.MarshalMethods.Count);
+ var withConstructors = peers.Count (p => p.MarshalMethods.Any (m => m.IsConstructor));
+ var withBase = peers.Count (p => p.BaseJavaName != null);
+ var withInterfaces = peers.Count (p => p.ImplementedInterfaceJavaNames.Count > 0);
+
+ output.WriteLine ($"Total types: {peers.Count}");
+ output.WriteLine ($"Interfaces: {interfaces}");
+ output.WriteLine ($"Abstract classes: {abstracts}");
+ output.WriteLine ($"Generic defs: {generics}");
+ output.WriteLine ($"With marshal methods: {withMethods} ({totalMethods} total methods)");
+ output.WriteLine ($"With constructors: {withConstructors}");
+ output.WriteLine ($"With base Java: {withBase}");
+ output.WriteLine ($"With interfaces: {withInterfaces}");
+
+ // Mono.Android.dll should have thousands of types
+ Assert.True (peers.Count > 3000, $"Expected >3000 types, got {peers.Count}");
+ Assert.True (interfaces > 500, $"Expected >500 interfaces, got {interfaces}");
+ Assert.True (totalMethods > 10000, $"Expected >10000 marshal methods, got {totalMethods}");
+ }
+
+ [Fact]
+ public void ExactBaseJavaNames_MonoAndroid ()
+ {
+ var assemblyPath = MonoAndroidAssemblyPath;
+
+ var (legacyData, _) = BuildLegacyTypeData (assemblyPath);
+ var newData = BuildNewTypeData (AllAssemblyPaths);
+
+ var allManagedNames = new HashSet (legacyData.Keys);
+ allManagedNames.IntersectWith (newData.Keys);
+
+ var mismatches = new List ();
+ int compared = 0;
+
+ foreach (var managedName in allManagedNames.OrderBy (n => n)) {
+ var legacy = legacyData [managedName];
+ var newInfo = newData [managedName];
+
+ compared++;
+
+ if (legacy.BaseJavaName != newInfo.BaseJavaName) {
+ // Legacy ToJniName can't resolve bases for open generic types (returns null).
+ // Our scanner resolves them correctly. Accept this known difference.
+ if (legacy.BaseJavaName == null && newInfo.BaseJavaName != null && managedName.Contains ('`')) {
+ continue;
+ }
+
+ // Invokers share JNI names with their base class. Legacy ToJniName
+ // self-reference filter discards the base (baseJni == javaName), but
+ // our scanner correctly resolves it. Accept legacy=null, new=valid
+ // for DoNotGenerateAcw types.
+ if (legacy.BaseJavaName == null && newInfo.BaseJavaName != null && newInfo.DoNotGenerateAcw) {
+ continue;
+ }
+
+ // Legacy ToJniName(System.Object) returns "java/lang/Object" as a fallback,
+ // making Java.Lang.Object/Throwable appear to have themselves as base.
+ // Our scanner correctly returns null. Accept legacy=self, new=null.
+ if (legacy.BaseJavaName != null && newInfo.BaseJavaName == null &&
+ legacy.BaseJavaName == legacy.JavaName) {
+ continue;
+ }
+
+ mismatches.Add ($"{managedName}: legacy='{legacy.BaseJavaName ?? "(null)"}' new='{newInfo.BaseJavaName ?? "(null)"}'");
+ }
+ }
+
+ output.WriteLine ($"Compared BaseJavaName for {compared} types");
+
+ LogDiffs ("BASE JAVA NAME MISMATCHES", mismatches);
+
+ Assert.Empty (mismatches);
+ }
+
+ [Fact]
+ public void ExactImplementedInterfaces_MonoAndroid ()
+ {
+ var assemblyPath = MonoAndroidAssemblyPath;
+
+ var (legacyData, _) = BuildLegacyTypeData (assemblyPath);
+ var newData = BuildNewTypeData (AllAssemblyPaths);
+
+ var allManagedNames = new HashSet (legacyData.Keys);
+ allManagedNames.IntersectWith (newData.Keys);
+
+ var missingInterfaces = new List ();
+ var extraInterfaces = new List ();
+ int compared = 0;
+
+ foreach (var managedName in allManagedNames.OrderBy (n => n)) {
+ var legacy = legacyData [managedName];
+ var newInfo = newData [managedName];
+
+ compared++;
+
+ var legacySet = new HashSet (legacy.ImplementedInterfaces, StringComparer.Ordinal);
+ var newSet = new HashSet (newInfo.ImplementedInterfaces, StringComparer.Ordinal);
+
+ foreach (var iface in legacySet.Except (newSet)) {
+ missingInterfaces.Add ($"{managedName}: missing '{iface}'");
+ }
+
+ foreach (var iface in newSet.Except (legacySet)) {
+ extraInterfaces.Add ($"{managedName}: extra '{iface}'");
+ }
+ }
+
+ output.WriteLine ($"Compared ImplementedInterfaces for {compared} types");
+
+ LogDiffs ("INTERFACES MISSING from new scanner", missingInterfaces);
+ LogDiffs ("INTERFACES EXTRA in new scanner", extraInterfaces);
+
+ Assert.Empty (missingInterfaces);
+ Assert.Empty (extraInterfaces);
+ }
+
+ [Fact]
+ public void ExactActivationCtors_MonoAndroid ()
+ {
+ var assemblyPath = MonoAndroidAssemblyPath;
+
+ var (legacyData, _) = BuildLegacyTypeData (assemblyPath);
+ var newData = BuildNewTypeData (AllAssemblyPaths);
+
+ var allManagedNames = new HashSet (legacyData.Keys);
+ allManagedNames.IntersectWith (newData.Keys);
+
+ var presenceMismatches = new List ();
+ var declaringTypeMismatches = new List ();
+ var styleMismatches = new List ();
+ int compared = 0;
+ int withActivationCtor = 0;
+
+ foreach (var managedName in allManagedNames.OrderBy (n => n)) {
+ var legacy = legacyData [managedName];
+ var newInfo = newData [managedName];
+
+ compared++;
+
+ if (legacy.HasActivationCtor != newInfo.HasActivationCtor) {
+ presenceMismatches.Add ($"{managedName}: legacy.has={legacy.HasActivationCtor} new.has={newInfo.HasActivationCtor}");
+ continue;
+ }
+
+ if (!legacy.HasActivationCtor) {
+ continue;
+ }
+
+ withActivationCtor++;
+
+ if (legacy.ActivationCtorDeclaringType != newInfo.ActivationCtorDeclaringType) {
+ declaringTypeMismatches.Add ($"{managedName}: legacy='{legacy.ActivationCtorDeclaringType}' new='{newInfo.ActivationCtorDeclaringType}'");
+ }
+
+ if (legacy.ActivationCtorStyle != newInfo.ActivationCtorStyle) {
+ styleMismatches.Add ($"{managedName}: legacy='{legacy.ActivationCtorStyle}' new='{newInfo.ActivationCtorStyle}'");
+ }
+ }
+
+ output.WriteLine ($"Compared ActivationCtor for {compared} types ({withActivationCtor} have activation ctors)");
+
+ LogDiffs ("ACTIVATION CTOR PRESENCE MISMATCHES", presenceMismatches);
+ LogDiffs ("ACTIVATION CTOR DECLARING TYPE MISMATCHES", declaringTypeMismatches);
+ LogDiffs ("ACTIVATION CTOR STYLE MISMATCHES", styleMismatches);
+
+ Assert.Empty (presenceMismatches);
+ Assert.Empty (declaringTypeMismatches);
+ Assert.Empty (styleMismatches);
+ }
+
+ [Fact]
+ public void ExactJavaConstructors_MonoAndroid ()
+ {
+ var assemblyPath = MonoAndroidAssemblyPath;
+
+ var (legacyData, _) = BuildLegacyTypeData (assemblyPath);
+ var newData = BuildNewTypeData (AllAssemblyPaths);
+
+ var allManagedNames = new HashSet (legacyData.Keys);
+ allManagedNames.IntersectWith (newData.Keys);
+
+ var missingCtors = new List ();
+ var extraCtors = new List ();
+ int compared = 0;
+ int totalCtors = 0;
+
+ foreach (var managedName in allManagedNames.OrderBy (n => n)) {
+ var legacy = legacyData [managedName];
+ var newInfo = newData [managedName];
+
+ compared++;
+
+ var legacySet = new HashSet (legacy.JavaConstructorSignatures, StringComparer.Ordinal);
+ var newSet = new HashSet (newInfo.JavaConstructorSignatures, StringComparer.Ordinal);
+ totalCtors += newSet.Count;
+
+ foreach (var sig in legacySet.Except (newSet)) {
+ missingCtors.Add ($"{managedName}: missing '{sig}'");
+ }
+
+ foreach (var sig in newSet.Except (legacySet)) {
+ extraCtors.Add ($"{managedName}: extra '{sig}'");
+ }
+ }
+
+ output.WriteLine ($"Compared JavaConstructors for {compared} types ({totalCtors} total constructors)");
+
+ LogDiffs ("JAVA CONSTRUCTORS MISSING from new scanner", missingCtors);
+ LogDiffs ("JAVA CONSTRUCTORS EXTRA in new scanner", extraCtors);
+
+ Assert.Empty (missingCtors);
+ Assert.Empty (extraCtors);
+ }
+
+ [Fact]
+ public void ExactTypeFlags_MonoAndroid ()
+ {
+ var assemblyPath = MonoAndroidAssemblyPath;
+
+ var (legacyData, _) = BuildLegacyTypeData (assemblyPath);
+ var newData = BuildNewTypeData (AllAssemblyPaths);
+
+ var allManagedNames = new HashSet (legacyData.Keys);
+ allManagedNames.IntersectWith (newData.Keys);
+
+ var interfaceMismatches = new List ();
+ var abstractMismatches = new List ();
+ var genericMismatches = new List ();
+ var acwMismatches = new List ();
+ int compared = 0;
+
+ foreach (var managedName in allManagedNames.OrderBy (n => n)) {
+ var legacy = legacyData [managedName];
+ var newInfo = newData [managedName];
+
+ compared++;
+
+ if (legacy.IsInterface != newInfo.IsInterface) {
+ interfaceMismatches.Add ($"{managedName}: legacy={legacy.IsInterface} new={newInfo.IsInterface}");
+ }
+
+ if (legacy.IsAbstract != newInfo.IsAbstract) {
+ abstractMismatches.Add ($"{managedName}: legacy={legacy.IsAbstract} new={newInfo.IsAbstract}");
+ }
+
+ if (legacy.IsGenericDefinition != newInfo.IsGenericDefinition) {
+ genericMismatches.Add ($"{managedName}: legacy={legacy.IsGenericDefinition} new={newInfo.IsGenericDefinition}");
+ }
+
+ if (legacy.DoNotGenerateAcw != newInfo.DoNotGenerateAcw) {
+ acwMismatches.Add ($"{managedName}: legacy={legacy.DoNotGenerateAcw} new={newInfo.DoNotGenerateAcw}");
+ }
+ }
+
+ output.WriteLine ($"Compared type flags for {compared} types");
+
+ LogDiffs ("IsInterface MISMATCHES", interfaceMismatches);
+ LogDiffs ("IsAbstract MISMATCHES", abstractMismatches);
+ LogDiffs ("IsGenericDefinition MISMATCHES", genericMismatches);
+ LogDiffs ("DoNotGenerateAcw MISMATCHES", acwMismatches);
+
+ Assert.Empty (interfaceMismatches);
+ Assert.Empty (abstractMismatches);
+ Assert.Empty (genericMismatches);
+ Assert.Empty (acwMismatches);
+ }
+
+
+ record TypeComparisonData (
+ string ManagedName,
+ string JavaName,
+ string? BaseJavaName,
+ IReadOnlyList ImplementedInterfaces,
+ bool HasActivationCtor,
+ string? ActivationCtorDeclaringType,
+ string? ActivationCtorStyle,
+ IReadOnlyList JavaConstructorSignatures,
+ bool IsInterface,
+ bool IsAbstract,
+ bool IsGenericDefinition,
+ bool DoNotGenerateAcw
+ );
+
+ static (Dictionary perType, List entries) BuildLegacyTypeData (string assemblyPath)
+ {
+ var cache = new TypeDefinitionCache ();
+ var resolver = new DefaultAssemblyResolver ();
+ resolver.AddSearchDirectory (Path.GetDirectoryName (assemblyPath)!);
+
+ var runtimeDir = Path.GetDirectoryName (typeof (object).Assembly.Location);
+ if (runtimeDir != null) {
+ resolver.AddSearchDirectory (runtimeDir);
+ }
+
+ var readerParams = new ReaderParameters { AssemblyResolver = resolver };
+ using var assembly = AssemblyDefinition.ReadAssembly (assemblyPath, readerParams);
+
+ var scanner = new Xamarin.Android.Tasks.XAJavaTypeScanner (
+ Xamarin.Android.Tools.AndroidTargetArch.Arm64,
+ new TaskLoggingHelper (new MockBuildEngine (), "test"),
+ cache
+ );
+
+ var javaTypes = scanner.GetJavaTypes (assembly);
+ var (dataSets, _) = Xamarin.Android.Tasks.TypeMapCecilAdapter.GetDebugNativeEntries (
+ javaTypes, cache, needUniqueAssemblies: false
+ );
+
+ var entries = dataSets.JavaToManaged
+ .Select (e => new TypeMapEntry (e.JavaName, e.ManagedName, e.SkipInJavaToManaged))
+ .OrderBy (e => e.JavaName, StringComparer.Ordinal)
+ .ThenBy (e => e.ManagedName, StringComparer.Ordinal)
+ .ToList ();
+
+ var perType = new Dictionary (StringComparer.Ordinal);
+
+ foreach (var typeDef in javaTypes) {
+ var javaName = GetCecilJavaName (typeDef);
+ if (javaName == null) {
+ continue;
+ }
+
+ // Cecil uses '/' for nested types, SRM uses '+' — normalize
+ var managedName = $"{typeDef.FullName.Replace ('/', '+')}, {typeDef.Module.Assembly.Name.Name}";
+
+ // Base Java name
+ string? baseJavaName = null;
+ var baseType = typeDef.GetBaseType (cache);
+ if (baseType != null) {
+ var baseJni = JavaNativeTypeManager.ToJniName (baseType, cache);
+ // Filter self-references: ToJniName can return the type's own JNI name
+ // (e.g., Java.Lang.Object → System.Object → "java/lang/Object").
+ if (baseJni != null && baseJni != javaName) {
+ baseJavaName = baseJni;
+ }
+ }
+
+ // Implemented interfaces (only Java peer interfaces with [Register])
+ var implementedInterfaces = new List ();
+ if (typeDef.HasInterfaces) {
+ foreach (var ifaceImpl in typeDef.Interfaces) {
+ var ifaceDef = cache.Resolve (ifaceImpl.InterfaceType);
+ if (ifaceDef == null) {
+ continue;
+ }
+ var ifaceRegs = CecilExtensions.GetTypeRegistrationAttributes (ifaceDef);
+ var ifaceReg = ifaceRegs.FirstOrDefault ();
+ if (ifaceReg != null) {
+ implementedInterfaces.Add (ifaceReg.Name.Replace ('.', '/'));
+ }
+ }
+ }
+ implementedInterfaces.Sort (StringComparer.Ordinal);
+
+ // Activation constructor
+ bool hasActivationCtor = false;
+ string? activationCtorDeclaringType = null;
+ string? activationCtorStyle = null;
+ FindLegacyActivationCtor (typeDef, cache, out hasActivationCtor, out activationCtorDeclaringType, out activationCtorStyle);
+
+ // Java constructors: [Register("", sig, ...)] on .ctor methods
+ var javaCtorSignatures = new List ();
+ foreach (var method in typeDef.Methods) {
+ if (!method.IsConstructor || method.IsStatic || !method.HasCustomAttributes) {
+ continue;
+ }
+ foreach (var attr in method.CustomAttributes) {
+ if (attr.AttributeType.FullName != "Android.Runtime.RegisterAttribute") {
+ continue;
+ }
+ if (attr.ConstructorArguments.Count >= 2) {
+ var regName = (string) attr.ConstructorArguments [0].Value;
+ if (regName == "" || regName == ".ctor") {
+ javaCtorSignatures.Add ((string) attr.ConstructorArguments [1].Value);
+ }
+ }
+ }
+ }
+ javaCtorSignatures.Sort (StringComparer.Ordinal);
+
+ // Type flags
+ var isInterface = typeDef.IsInterface;
+ var isAbstract = typeDef.IsAbstract && !typeDef.IsInterface;
+ var isGenericDefinition = typeDef.HasGenericParameters;
+ var doNotGenerateAcw = GetCecilDoNotGenerateAcw (typeDef);
+
+ perType [managedName] = new TypeComparisonData (
+ managedName,
+ javaName,
+ baseJavaName,
+ implementedInterfaces,
+ hasActivationCtor,
+ activationCtorDeclaringType,
+ activationCtorStyle,
+ javaCtorSignatures,
+ isInterface,
+ isAbstract,
+ isGenericDefinition,
+ doNotGenerateAcw
+ );
+ }
+
+ return (perType, entries);
+ }
+
+ static void FindLegacyActivationCtor (TypeDefinition typeDef, TypeDefinitionCache cache,
+ out bool found, out string? declaringType, out string? style)
+ {
+ found = false;
+ declaringType = null;
+ style = null;
+
+ // Walk from current type up through base types
+ TypeDefinition? current = typeDef;
+ while (current != null) {
+ foreach (var method in current.Methods) {
+ if (!method.IsConstructor || method.IsStatic || method.Parameters.Count != 2) {
+ continue;
+ }
+
+ var p0 = method.Parameters [0].ParameterType.FullName;
+ var p1 = method.Parameters [1].ParameterType.FullName;
+
+ if (p0 == "System.IntPtr" && p1 == "Android.Runtime.JniHandleOwnership") {
+ found = true;
+ declaringType = $"{current.FullName.Replace ('/', '+')}, {current.Module.Assembly.Name.Name}";
+ style = "XamarinAndroid";
+ return;
+ }
+
+ if ((p0 == "Java.Interop.JniObjectReference&" || p0 == "Java.Interop.JniObjectReference") &&
+ p1 == "Java.Interop.JniObjectReferenceOptions") {
+ found = true;
+ declaringType = $"{current.FullName.Replace ('/', '+')}, {current.Module.Assembly.Name.Name}";
+ style = "JavaInterop";
+ return;
+ }
+ }
+
+ current = current.GetBaseType (cache);
+ }
+ }
+
+ static bool GetCecilDoNotGenerateAcw (TypeDefinition typeDef)
+ {
+ if (!typeDef.HasCustomAttributes) {
+ return false;
+ }
+
+ foreach (var attr in typeDef.CustomAttributes) {
+ if (attr.AttributeType.FullName != "Android.Runtime.RegisterAttribute") {
+ continue;
+ }
+ if (attr.HasProperties) {
+ foreach (var prop in attr.Properties.Where (p => p.Name == "DoNotGenerateAcw")) {
+ if (prop.Argument.Value is bool val) {
+ return val;
+ }
+ }
+ }
+ // [Register] found but DoNotGenerateAcw not set — defaults to false
+ return false;
+ }
+
+ return false;
+ }
+
+ static Dictionary BuildNewTypeData (string[] assemblyPaths)
+ {
+ var primaryAssemblyName = Path.GetFileNameWithoutExtension (assemblyPaths [0]);
+ using var scanner = new JavaPeerScanner ();
+ var peers = scanner.Scan (assemblyPaths);
+
+ var perType = new Dictionary (StringComparer.Ordinal);
+
+ foreach (var peer in peers) {
+ // Only include types from the primary assembly
+ if (peer.AssemblyName != primaryAssemblyName) {
+ continue;
+ }
+
+ var managedName = $"{peer.ManagedTypeName}, {peer.AssemblyName}";
+
+ // Map ActivationCtor
+ bool hasActivationCtor = peer.ActivationCtor != null;
+ string? activationCtorDeclaringType = null;
+ string? activationCtorStyle = null;
+ if (peer.ActivationCtor != null) {
+ activationCtorDeclaringType = $"{peer.ActivationCtor.DeclaringTypeName}, {peer.ActivationCtor.DeclaringAssemblyName}";
+ activationCtorStyle = peer.ActivationCtor.Style.ToString ();
+ }
+
+ // Java constructor signatures (sorted) — derived from constructor marshal methods
+ var javaCtorSignatures = peer.MarshalMethods
+ .Where (m => m.IsConstructor)
+ .Select (m => m.JniSignature)
+ .OrderBy (s => s, StringComparer.Ordinal)
+ .ToList ();
+
+ // Implemented interfaces (sorted)
+ var implementedInterfaces = peer.ImplementedInterfaceJavaNames
+ .OrderBy (i => i, StringComparer.Ordinal)
+ .ToList ();
+
+ perType [managedName] = new TypeComparisonData (
+ managedName,
+ peer.JavaName,
+ peer.BaseJavaName,
+ implementedInterfaces,
+ hasActivationCtor,
+ activationCtorDeclaringType,
+ activationCtorStyle,
+ javaCtorSignatures,
+ peer.IsInterface,
+ peer.IsAbstract && !peer.IsInterface, // Match legacy: isAbstract excludes interfaces
+ peer.IsGenericDefinition,
+ peer.DoNotGenerateAcw
+ );
+ }
+
+ return perType;
+ }
+
+ static string MonoAndroidAssemblyPath {
+ get {
+ // Compile-time check: this ensures the Mono.Android reference is properly configured.
+ // It's never actually evaluated at runtime — it just validates the build setup.
+ _ = nameof (Java.Lang.Object);
+
+ // At runtime, find the Mono.Android.dll copy in the test output directory.
+ var testDir = Path.GetDirectoryName (typeof (ScannerComparisonTests).Assembly.Location)!;
+ var path = Path.Combine (testDir, "Mono.Android.dll");
+
+ if (!File.Exists (path)) {
+ throw new InvalidOperationException (
+ $"Mono.Android.dll not found at '{path}'. " +
+ "Ensure Mono.Android is built (bin/Debug/lib/packs/Microsoft.Android.Ref.*).");
+ }
+
+ return path;
+ }
+ }
+
+ static string[] AllAssemblyPaths {
+ get {
+ var monoAndroidPath = MonoAndroidAssemblyPath;
+ var dir = Path.GetDirectoryName (monoAndroidPath)!;
+ var javaInteropPath = Path.Combine (dir, "Java.Interop.dll");
+
+ if (!File.Exists (javaInteropPath)) {
+ return new [] { monoAndroidPath };
+ }
+
+ return new [] { monoAndroidPath, javaInteropPath };
+ }
+ }
+
+ static string NormalizeCrc64 (string javaName)
+ {
+ if (javaName.StartsWith ("crc64", StringComparison.Ordinal)) {
+ int slash = javaName.IndexOf ('/');
+ if (slash > 0) {
+ return "crc64.../" + javaName.Substring (slash + 1);
+ }
+ }
+ return javaName;
+ }
+
+ static string? UserTypesFixturePath {
+ get {
+ var testDir = Path.GetDirectoryName (typeof (ScannerComparisonTests).Assembly.Location)!;
+ var path = Path.Combine (testDir, "UserTypesFixture.dll");
+ return File.Exists (path) ? path : null;
+ }
+ }
+
+ static string[]? AllUserTypesAssemblyPaths {
+ get {
+ var fixturePath = UserTypesFixturePath;
+ if (fixturePath == null) {
+ return null;
+ }
+
+ var dir = Path.GetDirectoryName (fixturePath)!;
+ var monoAndroidPath = Path.Combine (dir, "Mono.Android.dll");
+ var javaInteropPath = Path.Combine (dir, "Java.Interop.dll");
+
+ var paths = new List { fixturePath };
+ if (File.Exists (monoAndroidPath)) {
+ paths.Add (monoAndroidPath);
+ }
+ if (File.Exists (javaInteropPath)) {
+ paths.Add (javaInteropPath);
+ }
+ return paths.ToArray ();
+ }
+ }
+
+ [Fact]
+ public void ExactTypeMap_UserTypesFixture ()
+ {
+ var paths = AllUserTypesAssemblyPaths;
+ Assert.NotNull (paths);
+
+ var fixturePath = paths! [0];
+ var (legacy, _) = RunLegacyScanner (fixturePath);
+ var (newEntries, _) = RunNewScanner (paths);
+
+ output.WriteLine ($"UserTypesFixture: Legacy={legacy.Count} entries, New={newEntries.Count} entries");
+
+ // Normalize CRC64 hashes — the two scanners use different polynomials
+ var legacyNormalized = legacy.Select (e => e with { JavaName = NormalizeCrc64 (e.JavaName) }).ToList ();
+ var newNormalized = newEntries.Select (e => e with { JavaName = NormalizeCrc64 (e.JavaName) }).ToList ();
+
+ AssertTypeMapMatch (legacyNormalized, newNormalized);
+ }
+
+ [Fact]
+ public void ExactMarshalMethods_UserTypesFixture ()
+ {
+ var paths = AllUserTypesAssemblyPaths;
+ Assert.NotNull (paths);
+
+ var fixturePath = paths! [0];
+ var (_, legacyMethods) = RunLegacyScanner (fixturePath);
+ var (_, newMethods) = RunNewScanner (paths);
+
+ // Normalize CRC64 hashes in method group keys
+ var legacyNormalized = legacyMethods
+ .ToDictionary (kvp => NormalizeCrc64 (kvp.Key), kvp => kvp.Value);
+ var newNormalized = newMethods
+ .ToDictionary (kvp => NormalizeCrc64 (kvp.Key), kvp => kvp.Value);
+
+ output.WriteLine ($"UserTypesFixture: Legacy={legacyNormalized.Count} types with methods, New={newNormalized.Count}");
+
+ // Only compare types that the legacy scanner found (it skips user types without [Register])
+ var missing = new List ();
+ var methodMismatches = new List ();
+
+ foreach (var javaName in legacyNormalized.Keys.OrderBy (n => n)) {
+ if (!newNormalized.TryGetValue (javaName, out var newGroups)) {
+ missing.Add (javaName);
+ continue;
+ }
+
+ var legacyGroups = legacyNormalized [javaName];
+
+ foreach (var legacyGroup in legacyGroups) {
+ var newGroup = newGroups.FirstOrDefault (g => g.ManagedName == legacyGroup.ManagedName);
+ if (newGroup == null) {
+ missing.Add ($"{javaName} → {legacyGroup.ManagedName}");
+ continue;
+ }
+
+ // Legacy test helper only extracts [Register] methods, not [Export] methods.
+ // When legacy has 0 methods (from the typemap fallback path) but new has some,
+ // the new scanner is correct — it handles [Export] too. Skip comparison.
+ if (legacyGroup.Methods.Count == 0) {
+ continue;
+ }
+
+ if (legacyGroup.Methods.Count != newGroup.Methods.Count) {
+ methodMismatches.Add ($"{javaName}/{legacyGroup.ManagedName}: legacy={legacyGroup.Methods.Count} methods, new={newGroup.Methods.Count}");
+ continue;
+ }
+
+ for (int i = 0; i < legacyGroup.Methods.Count; i++) {
+ var lm = legacyGroup.Methods [i];
+ var nm = newGroup.Methods [i];
+ if (lm.JniName != nm.JniName || lm.JniSignature != nm.JniSignature) {
+ methodMismatches.Add ($"{javaName}: [{i}] legacy=({lm.JniName}, {lm.JniSignature}) new=({nm.JniName}, {nm.JniSignature})");
+ }
+ }
+ }
+ }
+
+ LogDiffs ("MISSING from new scanner", missing);
+ LogDiffs ("METHOD MISMATCHES", methodMismatches);
+
+ Assert.Empty (missing);
+ Assert.Empty (methodMismatches);
+ }
+
+ void AssertTypeMapMatch (List legacy, List newEntries)
+ {
+ var legacyMap = legacy.GroupBy (e => e.JavaName).ToDictionary (g => g.Key, g => g.ToList ());
+ var newMap = newEntries.GroupBy (e => e.JavaName).ToDictionary (g => g.Key, g => g.ToList ());
+
+ var allJavaNames = new HashSet (legacyMap.Keys);
+ allJavaNames.UnionWith (newMap.Keys);
+
+ var missing = new List ();
+ var extra = new List ();
+ var managedNameMismatches = new List ();
+ var skipMismatches = new List ();
+
+ foreach (var javaName in allJavaNames.OrderBy (n => n)) {
+ var inLegacy = legacyMap.TryGetValue (javaName, out var legacyEntries);
+ var inNew = newMap.TryGetValue (javaName, out var newEntriesForName);
+
+ if (inLegacy && !inNew) {
+ foreach (var e in legacyEntries!)
+ missing.Add ($"{e.JavaName} → {e.ManagedName} (skip={e.SkipInJavaToManaged})");
+ continue;
+ }
+
+ if (!inLegacy && inNew) {
+ foreach (var e in newEntriesForName!)
+ extra.Add ($"{e.JavaName} → {e.ManagedName} (skip={e.SkipInJavaToManaged})");
+ continue;
+ }
+
+ var le = legacyEntries!.OrderBy (e => e.ManagedName).First ();
+ var ne = newEntriesForName!.OrderBy (e => e.ManagedName).First ();
+
+ if (le.ManagedName != ne.ManagedName)
+ managedNameMismatches.Add ($"{javaName}: legacy='{le.ManagedName}' new='{ne.ManagedName}'");
+
+ if (le.SkipInJavaToManaged != ne.SkipInJavaToManaged)
+ skipMismatches.Add ($"{javaName}: legacy.skip={le.SkipInJavaToManaged} new.skip={ne.SkipInJavaToManaged}");
+ }
+
+ LogDiffs ("MISSING", missing);
+ LogDiffs ("EXTRA", extra);
+ LogDiffs ("MANAGED NAME MISMATCHES", managedNameMismatches);
+ LogDiffs ("SKIP FLAG MISMATCHES", skipMismatches);
+
+ Assert.Empty (missing);
+ Assert.Empty (extra);
+ Assert.Empty (managedNameMismatches);
+ Assert.Empty (skipMismatches);
+ }
+
+ void LogDiffs (string label, List items)
+ {
+ if (items.Count == 0) return;
+ output.WriteLine ($"\n--- {label} ({items.Count}) ---");
+ foreach (var item in items) output.WriteLine ($" {item}");
+ }
+}
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypes.cs
new file mode 100644
index 00000000000..291137278cb
--- /dev/null
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypes.cs
@@ -0,0 +1,176 @@
+// User-type test fixture assembly that references REAL Mono.Android.
+// Exercises edge cases that MCW binding assemblies don't have:
+// - User types extending Java peers without [Register]
+// - Component attributes ([Activity], [Service], etc.)
+// - [Export] methods
+// - Nested user types
+// - Generic user types
+
+using System;
+using System.Runtime.Versioning;
+using Android.App;
+using Android.Content;
+using Android.Runtime;
+using Java.Interop;
+
+[assembly: SupportedOSPlatform ("android21.0")]
+
+// --- User Activity with explicit Name ---
+
+namespace UserApp
+{
+ [Activity (Name = "com.example.userapp.MainActivity", MainLauncher = true, Label = "User App")]
+ public class MainActivity : Activity
+ {
+ protected override void OnCreate (Android.OS.Bundle? savedInstanceState)
+ {
+ base.OnCreate (savedInstanceState);
+ }
+ }
+
+ // Activity WITHOUT explicit Name — should get CRC64-based JNI name
+ [Activity (Label = "Settings")]
+ public class SettingsActivity : Activity
+ {
+ }
+
+ // Simple Activity subclass — no attributes at all, just extends a Java peer
+ public class PlainActivity : Activity
+ {
+ }
+}
+
+// --- Services ---
+
+namespace UserApp.Services
+{
+ [Service (Name = "com.example.userapp.MyBackgroundService")]
+ public class MyBackgroundService : Android.App.Service
+ {
+ public override Android.OS.IBinder? OnBind (Android.Content.Intent? intent) => null;
+ }
+
+ // Service without explicit Name
+ [Service]
+ public class UnnamedService : Android.App.Service
+ {
+ public override Android.OS.IBinder? OnBind (Android.Content.Intent? intent) => null;
+ }
+}
+
+// --- BroadcastReceiver ---
+
+namespace UserApp.Receivers
+{
+ [BroadcastReceiver (Name = "com.example.userapp.BootReceiver", Exported = false)]
+ public class BootReceiver : BroadcastReceiver
+ {
+ public override void OnReceive (Context? context, Intent? intent)
+ {
+ }
+ }
+}
+
+// --- Application with BackupAgent ---
+
+namespace UserApp
+{
+ public class MyBackupAgent : Android.App.Backup.BackupAgent
+ {
+ public override void OnBackup (Android.OS.ParcelFileDescriptor? oldState,
+ Android.App.Backup.BackupDataOutput? data,
+ Android.OS.ParcelFileDescriptor? newState)
+ {
+ }
+
+ public override void OnRestore (Android.App.Backup.BackupDataInput? data,
+ int appVersionCode,
+ Android.OS.ParcelFileDescriptor? newState)
+ {
+ }
+ }
+
+ [Application (Name = "com.example.userapp.MyApp", BackupAgent = typeof (MyBackupAgent))]
+ public class MyApp : Application
+ {
+ public MyApp (IntPtr handle, JniHandleOwnership transfer)
+ : base (handle, transfer)
+ {
+ }
+ }
+}
+
+// --- Nested types ---
+
+namespace UserApp.Nested
+{
+ [Register ("com/example/userapp/OuterClass")]
+ public class OuterClass : Java.Lang.Object
+ {
+ // Nested class inheriting from Java peer — no [Register]
+ public class InnerHelper : Java.Lang.Object
+ {
+ }
+
+ // Deeply nested
+ public class MiddleClass : Java.Lang.Object
+ {
+ public class DeepHelper : Java.Lang.Object
+ {
+ }
+ }
+ }
+}
+
+// --- Plain Java.Lang.Object subclasses (no attributes) ---
+
+namespace UserApp.Models
+{
+ // These should all get CRC64-based JNI names
+ public class UserModel : Java.Lang.Object
+ {
+ }
+
+ public class DataManager : Java.Lang.Object
+ {
+ }
+}
+
+// --- Explicit [Register] on user type ---
+
+namespace UserApp
+{
+ [Register ("com/example/userapp/CustomView")]
+ public class CustomView : Android.Views.View
+ {
+ protected CustomView (IntPtr handle, JniHandleOwnership transfer)
+ : base (handle, transfer)
+ {
+ }
+ }
+}
+
+// --- Interface implementation ---
+
+namespace UserApp.Listeners
+{
+ public class MyClickListener : Java.Lang.Object, Android.Views.View.IOnClickListener
+ {
+ public void OnClick (Android.Views.View? v)
+ {
+ }
+ }
+}
+
+// --- [Export] method ---
+
+namespace UserApp
+{
+ public class ExportedMethodHolder : Java.Lang.Object
+ {
+ [Export ("doWork")]
+ public void DoWork ()
+ {
+ }
+ }
+}
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypesFixture.csproj b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypesFixture.csproj
new file mode 100644
index 00000000000..bba3496f276
--- /dev/null
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/UserTypesFixture/UserTypesFixture.csproj
@@ -0,0 +1,45 @@
+
+
+
+
+ $(DotNetTargetFramework)
+ latest
+ enable
+ false
+ Library
+ true
+ ..\..\..\product.snk
+
+ ..\..\..\bin\Test$(Configuration)\
+
+
+
+
+
+
+
+
+
+ <_MonoAndroidRefCandidate Include="$(MSBuildThisFileDirectory)..\..\..\bin\$(Configuration)\lib\packs\Microsoft.Android.Ref.*\*\ref\net*\Mono.Android.dll" />
+ <_JavaInteropRefCandidate Include="$(MSBuildThisFileDirectory)..\..\..\bin\$(Configuration)\lib\packs\Microsoft.Android.Ref.*\*\ref\net*\Java.Interop.dll" />
+
+
+ <_MonoAndroidRefAssembly>@(_MonoAndroidRefCandidate, ';')
+ <_MonoAndroidRefAssembly>$(_MonoAndroidRefAssembly.Split(';')[0])
+ <_JavaInteropRefAssembly>@(_JavaInteropRefCandidate, ';')
+ <_JavaInteropRefAssembly>$(_JavaInteropRefAssembly.Split(';')[0])
+
+
+
+ $(_MonoAndroidRefAssembly)
+
+
+
+
+ $(_JavaInteropRefAssembly)
+
+
+
+
+
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs
new file mode 100644
index 00000000000..6a0fd89e5d5
--- /dev/null
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs
@@ -0,0 +1,773 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Xunit;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests;
+
+public class JcwJavaSourceGeneratorTests
+{
+ static string TestFixtureAssemblyPath {
+ get {
+ var testAssemblyDir = Path.GetDirectoryName (typeof (JcwJavaSourceGeneratorTests).Assembly.Location)!;
+ var fixtureAssembly = Path.Combine (testAssemblyDir, "TestFixtures.dll");
+ Assert.True (File.Exists (fixtureAssembly),
+ $"TestFixtures.dll not found at {fixtureAssembly}. Ensure the TestFixtures project builds.");
+ return fixtureAssembly;
+ }
+ }
+
+ static readonly Lazy> _cachedFixtures = new (() => {
+ using var scanner = new JavaPeerScanner ();
+ return scanner.Scan (new [] { TestFixtureAssemblyPath });
+ });
+
+ static List ScanFixtures () => _cachedFixtures.Value;
+
+ static JavaPeerInfo FindByJavaName (List peers, string javaName)
+ {
+ var peer = peers.FirstOrDefault (p => p.JavaName == javaName);
+ Assert.NotNull (peer);
+ return peer;
+ }
+
+ static string GenerateToString (JavaPeerInfo type)
+ {
+ var generator = new JcwJavaSourceGenerator ();
+ using var writer = new StringWriter ();
+ generator.Generate (type, writer);
+ return writer.ToString ();
+ }
+
+
+ public class JniNameConversion
+ {
+
+ [Theory]
+ [InlineData ("android/app/Activity", "android.app.Activity")]
+ [InlineData ("java/lang/Object", "java.lang.Object")]
+ [InlineData ("android/view/View$OnClickListener", "android.view.View$OnClickListener")]
+ public void JniNameToJavaName_ConvertsCorrectly (string jniName, string expected)
+ {
+ Assert.Equal (expected, JcwJavaSourceGenerator.JniNameToJavaName (jniName));
+ }
+
+ [Theory]
+ [InlineData ("com/example/MainActivity", "com.example")]
+ [InlineData ("java/lang/Object", "java.lang")]
+ [InlineData ("TopLevelClass", null)]
+ public void GetJavaPackageName_ExtractsCorrectly (string jniName, string? expected)
+ {
+ Assert.Equal (expected, JcwJavaSourceGenerator.GetJavaPackageName (jniName));
+ }
+
+ [Theory]
+ [InlineData ("com/example/MainActivity", "MainActivity")]
+ [InlineData ("com/example/Outer$Inner", "Outer$Inner")]
+ [InlineData ("TopLevelClass", "TopLevelClass")]
+ public void GetJavaSimpleName_ExtractsCorrectly (string jniName, string expected)
+ {
+ Assert.Equal (expected, JcwJavaSourceGenerator.GetJavaSimpleName (jniName));
+ }
+
+ [Theory]
+ [InlineData ("V", "void")]
+ [InlineData ("Z", "boolean")]
+ [InlineData ("B", "byte")]
+ [InlineData ("I", "int")]
+ [InlineData ("J", "long")]
+ [InlineData ("F", "float")]
+ [InlineData ("D", "double")]
+ [InlineData ("Landroid/os/Bundle;", "android.os.Bundle")]
+ [InlineData ("[I", "int[]")]
+ [InlineData ("[Ljava/lang/String;", "java.lang.String[]")]
+ public void JniTypeToJava_ConvertsCorrectly (string jniType, string expected)
+ {
+ Assert.Equal (expected, JcwJavaSourceGenerator.JniTypeToJava (jniType));
+ }
+
+ }
+
+ public class Filtering
+ {
+
+ [Fact]
+ public void Generate_SkipsMcwTypes ()
+ {
+ var peers = ScanFixtures ();
+ var generator = new JcwJavaSourceGenerator ();
+ var outputDir = Path.Combine (Path.GetTempPath (), $"jcw-test-{Guid.NewGuid ():N}");
+ try {
+ var files = generator.Generate (peers, outputDir);
+ // MCW types like java/lang/Object, android/app/Activity should NOT be generated
+ Assert.DoesNotContain (files, f => f.EndsWith ("java/lang/Object.java"));
+ Assert.DoesNotContain (files, f => f.EndsWith ("android/app/Activity.java"));
+ // User ACW types should be generated
+ Assert.Contains (files, f => f.Replace ('\\', '/').Contains ("my/app/MainActivity.java"));
+ } finally {
+ if (Directory.Exists (outputDir)) {
+ Directory.Delete (outputDir, true);
+ }
+ }
+ }
+
+ }
+
+ public class PackageDeclaration
+ {
+
+ [Fact]
+ public void Generate_MainActivity_HasPackageDeclaration ()
+ {
+ var peers = ScanFixtures ();
+ var mainActivity = FindByJavaName (peers, "my/app/MainActivity");
+ var java = GenerateToString (mainActivity);
+ Assert.StartsWith ("package my.app;\n", java);
+ }
+
+ }
+
+ public class ClassDeclaration
+ {
+
+ [Fact]
+ public void Generate_MainActivity_HasClassDeclaration ()
+ {
+ var peers = ScanFixtures ();
+ var mainActivity = FindByJavaName (peers, "my/app/MainActivity");
+ var java = GenerateToString (mainActivity);
+ Assert.Contains ("public class MainActivity\n", java);
+ Assert.Contains ("\textends android.app.Activity\n", java);
+ Assert.Contains ("\t\tmono.android.IGCUserPeer\n", java);
+ }
+
+ [Fact]
+ public void Generate_AbstractType_HasAbstractModifier ()
+ {
+ var peers = ScanFixtures ();
+ var abstractBase = FindByJavaName (peers, "my/app/AbstractBase");
+ var java = GenerateToString (abstractBase);
+ Assert.Contains ("public abstract class AbstractBase\n", java);
+ }
+
+ [Fact]
+ public void Generate_TypeWithInterfaces_HasImplementsClause ()
+ {
+ var peers = ScanFixtures ();
+ var multiView = FindByJavaName (peers, "my/app/MultiInterfaceView");
+ var java = GenerateToString (multiView);
+ Assert.Contains ("\timplements\n", java);
+ Assert.Contains ("\t\tmono.android.IGCUserPeer", java);
+ Assert.Contains ("android.view.View$OnClickListener", java);
+ Assert.Contains ("android.view.View$OnLongClickListener", java);
+ }
+
+ }
+
+ public class StaticInitializer
+ {
+
+ [Fact]
+ public void Generate_AcwType_HasRegisterNativesStaticBlock ()
+ {
+ var peers = ScanFixtures ();
+ var mainActivity = FindByJavaName (peers, "my/app/MainActivity");
+ var java = GenerateToString (mainActivity);
+ Assert.Contains ("static {\n", java);
+ Assert.Contains ("mono.android.Runtime.registerNatives (MainActivity.class);\n", java);
+ }
+
+ }
+
+ public class Constructor
+ {
+
+ [Fact]
+ public void Generate_CustomView_HasConstructors ()
+ {
+ var peers = ScanFixtures ();
+ var customView = FindByJavaName (peers, "my/app/CustomView");
+ var java = GenerateToString (customView);
+
+ // Default constructor
+ Assert.Contains ("public CustomView ()\n", java);
+ Assert.Contains ("super ();\n", java);
+ Assert.Contains ("nctor_0 ();\n", java);
+
+ // Context constructor
+ Assert.Contains ("public CustomView (android.content.Context p0)\n", java);
+ Assert.Contains ("super (p0);\n", java);
+ Assert.Contains ("nctor_1 (p0);\n", java);
+ }
+
+ [Fact]
+ public void Generate_CustomView_HasNativeConstructorDeclarations ()
+ {
+ var peers = ScanFixtures ();
+ var customView = FindByJavaName (peers, "my/app/CustomView");
+ var java = GenerateToString (customView);
+ Assert.Contains ("private native void nctor_0 ();\n", java);
+ Assert.Contains ("private native void nctor_1 (android.content.Context p0);\n", java);
+ }
+
+ [Fact]
+ public void Generate_Constructor_HasActivationGuard ()
+ {
+ var peers = ScanFixtures ();
+ var customView = FindByJavaName (peers, "my/app/CustomView");
+ var java = GenerateToString (customView);
+ Assert.Contains ("if (getClass () == CustomView.class) nctor_0 ();\n", java);
+ }
+
+ [Fact]
+ public void Generate_Constructor_WithSuperArgumentsString_UsesCustomSuperArgs ()
+ {
+ // [Export] constructors with SuperArgumentsString should use it in super() call
+ var type = new JavaPeerInfo {
+ JavaName = "my/app/CustomService",
+ ManagedTypeName = "MyApp.CustomService",
+ ManagedTypeNamespace = "MyApp",
+ ManagedTypeShortName = "CustomService",
+ AssemblyName = "App",
+ BaseJavaName = "android/app/Service",
+ JavaConstructors = new List {
+ new JavaConstructorInfo {
+ JniSignature = "(Landroid/content/Context;I)V",
+ ConstructorIndex = 0,
+ Parameters = new List {
+ new JniParameterInfo { JniType = "Landroid/content/Context;" },
+ new JniParameterInfo { JniType = "I" },
+ },
+ SuperArgumentsString = "p0",
+ },
+ },
+ };
+
+ var java = GenerateToString (type);
+ // super() should use the custom args, not all parameters
+ Assert.Contains ("super (p0);", java);
+ Assert.DoesNotContain ("super (p0, p1);", java);
+ }
+
+ [Fact]
+ public void Generate_Constructor_WithEmptySuperArgumentsString_EmptySuper ()
+ {
+ // Empty string means super() with no arguments
+ var type = new JavaPeerInfo {
+ JavaName = "my/app/MyWidget",
+ ManagedTypeName = "MyApp.MyWidget",
+ ManagedTypeNamespace = "MyApp",
+ ManagedTypeShortName = "MyWidget",
+ AssemblyName = "App",
+ BaseJavaName = "android/appwidget/AppWidgetProvider",
+ JavaConstructors = new List {
+ new JavaConstructorInfo {
+ JniSignature = "(Landroid/content/Context;)V",
+ ConstructorIndex = 0,
+ Parameters = new List {
+ new JniParameterInfo { JniType = "Landroid/content/Context;" },
+ },
+ SuperArgumentsString = "",
+ },
+ },
+ };
+
+ var java = GenerateToString (type);
+ Assert.Contains ("super ();", java);
+ Assert.DoesNotContain ("super (p0);", java);
+ }
+
+ [Fact]
+ public void Generate_Constructor_WithoutSuperArgumentsString_ForwardsAllParams ()
+ {
+ // null SuperArgumentsString means forward all params (default behavior)
+ var type = new JavaPeerInfo {
+ JavaName = "my/app/MyView",
+ ManagedTypeName = "MyApp.MyView",
+ ManagedTypeNamespace = "MyApp",
+ ManagedTypeShortName = "MyView",
+ AssemblyName = "App",
+ BaseJavaName = "android/view/View",
+ JavaConstructors = new List {
+ new JavaConstructorInfo {
+ JniSignature = "(Landroid/content/Context;Landroid/util/AttributeSet;)V",
+ ConstructorIndex = 0,
+ Parameters = new List {
+ new JniParameterInfo { JniType = "Landroid/content/Context;" },
+ new JniParameterInfo { JniType = "Landroid/util/AttributeSet;" },
+ },
+ },
+ },
+ };
+
+ var java = GenerateToString (type);
+ Assert.Contains ("super (p0, p1);", java);
+ }
+
+ }
+
+ public class Method
+ {
+
+ [Fact]
+ public void Generate_MarshalMethod_HasOverrideAndNativeDeclaration ()
+ {
+ var peers = ScanFixtures ();
+ var mainActivity = FindByJavaName (peers, "my/app/MainActivity");
+ var java = GenerateToString (mainActivity);
+ Assert.Contains ("@Override\n", java);
+ Assert.Contains ("public void onCreate (android.os.Bundle p0)\n", java);
+ Assert.Contains ("n_OnCreate (p0);\n", java);
+ Assert.Contains ("private native void n_OnCreate (android.os.Bundle p0);\n", java);
+ }
+
+ [Fact]
+ public void Generate_MethodWithReturnValue_HasReturnStatement ()
+ {
+ var peers = ScanFixtures ();
+ var touchHandler = FindByJavaName (peers, "my/app/TouchHandler");
+ var java = GenerateToString (touchHandler);
+ Assert.Contains ("public boolean onTouch (android.view.View p0, int p1)\n", java);
+ Assert.Contains ("return n_OnTouch (p0, p1);\n", java);
+ }
+
+ [Fact]
+ public void Generate_MethodWithMultipleParams_HasAllParameters ()
+ {
+ var peers = ScanFixtures ();
+ var touchHandler = FindByJavaName (peers, "my/app/TouchHandler");
+ var java = GenerateToString (touchHandler);
+ Assert.Contains ("public void onScroll (int p0, float p1, long p2, double p3)\n", java);
+ }
+
+ [Fact]
+ public void Generate_MethodWithObjectReturnType_HasCorrectType ()
+ {
+ var peers = ScanFixtures ();
+ var touchHandler = FindByJavaName (peers, "my/app/TouchHandler");
+ var java = GenerateToString (touchHandler);
+ Assert.Contains ("public java.lang.String getText ()\n", java);
+ Assert.Contains ("return n_GetText ();\n", java);
+ }
+
+ [Fact]
+ public void Generate_MethodWithArrayParam_HasCorrectType ()
+ {
+ var peers = ScanFixtures ();
+ var touchHandler = FindByJavaName (peers, "my/app/TouchHandler");
+ var java = GenerateToString (touchHandler);
+ Assert.Contains ("public void setItems (java.lang.String[] p0)\n", java);
+ }
+
+ }
+
+ public class NestedType
+ {
+
+ [Fact]
+ public void Generate_NestedType_HasCorrectPackageAndClassName ()
+ {
+ var peers = ScanFixtures ();
+ var inner = FindByJavaName (peers, "my/app/Outer$Inner");
+ var java = GenerateToString (inner);
+ Assert.Contains ("package my.app;\n", java);
+ Assert.Contains ("public class Outer$Inner\n", java);
+ }
+
+ }
+
+ public class OutputFilePath
+ {
+
+ [Fact]
+ public void Generate_CreatesCorrectFileStructure ()
+ {
+ var peers = ScanFixtures ();
+ var generator = new JcwJavaSourceGenerator ();
+ var outputDir = Path.Combine (Path.GetTempPath (), $"jcw-test-{Guid.NewGuid ():N}");
+ try {
+ var files = generator.Generate (peers, outputDir);
+ Assert.NotEmpty (files);
+
+ // All files should be under the output directory
+ foreach (var file in files) {
+ Assert.StartsWith (outputDir, file);
+ Assert.True (File.Exists (file), $"Generated file should exist: {file}");
+ Assert.EndsWith (".java", file);
+ }
+ } finally {
+ if (Directory.Exists (outputDir)) {
+ Directory.Delete (outputDir, true);
+ }
+ }
+ }
+
+ }
+
+ public class ExportWithThrowsClause
+ {
+
+ [Fact]
+ public void Generate_ExportWithThrows_HasThrowsClause ()
+ {
+ var peers = ScanFixtures ();
+ var peer = FindByJavaName (peers, "my/app/ExportWithThrows");
+ var java = GenerateToString (peer);
+ Assert.Contains ("throws java.io.IOException, java.lang.IllegalStateException\n", java);
+ }
+
+ [Fact]
+ public void Generate_ExportWithoutThrows_HasNoThrowsClause ()
+ {
+ var peers = ScanFixtures ();
+ var peer = FindByJavaName (peers, "my/app/ExportExample");
+ var java = GenerateToString (peer);
+ Assert.DoesNotContain ("throws", java);
+ }
+ }
+
+ public class ExportConstructor
+ {
+
+ [Fact]
+ public void Generate_ExportConstructors_UsesNativeCtorMethods ()
+ {
+ var peers = ScanFixtures ();
+ var peer = FindByJavaName (peers, "my/app/ExportsConstructors");
+ var java = GenerateToString (peer);
+
+ // [Export] constructors should use nctor_N native methods (same as [Register])
+ Assert.Contains ("nctor_0 ()", java);
+ Assert.Contains ("nctor_1 (int p0)", java);
+ Assert.Contains ("private native void nctor_0 ()", java);
+ Assert.Contains ("private native void nctor_1 (int p0)", java);
+
+ // Should NOT use TypeManager.Activate
+ Assert.DoesNotContain ("TypeManager.Activate", java);
+ }
+
+ [Fact]
+ public void Generate_ExportConstructors_FullOutput ()
+ {
+ var peers = ScanFixtures ();
+ var peer = FindByJavaName (peers, "my/app/ExportsConstructors");
+ var java = GenerateToString (peer);
+
+ // Parameterless ctor: super(), then nctor_0
+ Assert.Contains ("\tpublic ExportsConstructors ()\n\t{\n\t\tsuper ();\n\t\tif (getClass () == ExportsConstructors.class) nctor_0 ();\n\t}\n", java);
+
+ // int ctor: super(p0), then nctor_1
+ Assert.Contains ("\tpublic ExportsConstructors (int p0)\n\t{\n\t\tsuper (p0);\n\t\tif (getClass () == ExportsConstructors.class) nctor_1 (p0);\n\t}\n", java);
+ }
+
+ ///
+ /// Full output comparison — ported from legacy GenerateConstructors_WithThrows.
+ /// Verifies throws clauses appear on ctors that have ThrownNames.
+ ///
+ [Fact]
+ public void Generate_ExportThrowsConstructors_FullOutput ()
+ {
+ var peers = ScanFixtures ();
+ var peer = FindByJavaName (peers, "my/app/ExportsThrowsConstructors");
+ var java = GenerateToString (peer);
+
+ // Parameterless ctor with throws
+ Assert.Contains ("\tpublic ExportsThrowsConstructors ()\n\t\tthrows java.lang.Throwable\n\t{\n\t\tsuper ();\n", java);
+
+ // int ctor with throws
+ Assert.Contains ("\tpublic ExportsThrowsConstructors (int p0)\n\t\tthrows java.lang.Throwable\n\t{\n\t\tsuper (p0);\n", java);
+
+ // string ctor WITHOUT throws (empty ThrownNames in legacy means [Export] with no Throws)
+ Assert.Contains ("\tpublic ExportsThrowsConstructors (java.lang.String p0)\n\t{\n\t\tsuper (p0);\n", java);
+
+ // All ctors should use nctor_N, not TypeManager.Activate
+ Assert.Contains ("nctor_0 ()", java);
+ Assert.Contains ("nctor_1 (int p0)", java);
+ Assert.Contains ("nctor_2 (java.lang.String p0)", java);
+ Assert.DoesNotContain ("TypeManager.Activate", java);
+ }
+
+ [Fact]
+ public void Generate_ExportThrowsConstructors_HasThrowsClause ()
+ {
+ var peers = ScanFixtures ();
+ var peer = FindByJavaName (peers, "my/app/ExportsThrowsConstructors");
+ var java = GenerateToString (peer);
+
+ // [Export] constructors with ThrownNames should have throws clause
+ Assert.Contains ("throws java.lang.Throwable", java);
+ }
+
+ [Fact]
+ public void Generate_MixedRegisterAndExportConstructors_HandledCorrectly ()
+ {
+ // A type with both [Register] and [Export] constructors
+ var type = new JavaPeerInfo {
+ JavaName = "my/app/MixedCtors",
+ ManagedTypeName = "MyApp.MixedCtors",
+ ManagedTypeNamespace = "MyApp",
+ ManagedTypeShortName = "MixedCtors",
+ AssemblyName = "App",
+ BaseJavaName = "java/lang/Object",
+ JavaConstructors = new List {
+ new JavaConstructorInfo {
+ JniSignature = "()V",
+ ConstructorIndex = 0,
+ Parameters = new List (),
+ IsExport = false, // [Register]
+ },
+ new JavaConstructorInfo {
+ JniSignature = "(I)V",
+ ConstructorIndex = 1,
+ Parameters = new List {
+ new JniParameterInfo { JniType = "I", ManagedType = "System.Int32, System.Private.CoreLib" },
+ },
+ IsExport = true, // [Export]
+ },
+ },
+ };
+
+ var java = GenerateToString (type);
+
+ // Both [Register] and [Export] ctors should use nctor_N
+ Assert.Contains ("nctor_0 ()", java);
+ Assert.Contains ("nctor_1 (int p0)", java);
+ Assert.Contains ("private native void nctor_0 ()", java);
+ Assert.Contains ("private native void nctor_1 (int p0)", java);
+
+ // No TypeManager.Activate
+ Assert.DoesNotContain ("TypeManager.Activate", java);
+ }
+
+ [Fact]
+ public void Generate_ExportCtorWithSuperArgs_UsesCustomSuperArgs ()
+ {
+ var peers = ScanFixtures ();
+ var peer = FindByJavaName (peers, "my/app/ExportCtorWithSuperArgs");
+ var java = GenerateToString (peer);
+
+ // SuperArgumentsString = "" means super() with no args, not super(p0)
+ Assert.Contains ("super ();", java);
+ Assert.DoesNotContain ("super (p0);", java);
+
+ // Should use nctor_N, not TypeManager.Activate
+ Assert.Contains ("nctor_0 (int p0)", java);
+ Assert.DoesNotContain ("TypeManager.Activate", java);
+ }
+
+ }
+
+ public class ExportMethodJcw
+ {
+
+ ///
+ /// Ported from legacy GenerateExportedMembers — [Export] with name override.
+ /// The Java method name should be the export name, not the C# method name.
+ ///
+ [Fact]
+ public void Generate_ExportWithNameOverride_UsesExportName ()
+ {
+ var peers = ScanFixtures ();
+ var peer = FindByJavaName (peers, "my/app/ExportMembersComprehensive");
+ var java = GenerateToString (peer);
+
+ // [Export("attributeOverridesNames")] on CompletelyDifferentName
+ // Java method uses export name, native callback uses n_ + C# method name
+ Assert.Contains ("public java.lang.String attributeOverridesNames (java.lang.String p0, int p1)", java);
+ Assert.Contains ("n_CompletelyDifferentName (p0, p1)", java);
+ }
+
+ ///
+ /// Ported from legacy GenerateExportedMembers — [Export] method keeps C# name.
+ ///
+ [Fact]
+ public void Generate_ExportWithoutNameOverride_UsesMethodName ()
+ {
+ var peers = ScanFixtures ();
+ var peer = FindByJavaName (peers, "my/app/ExportMembersComprehensive");
+ var java = GenerateToString (peer);
+
+ // [Export] without name arg uses the C# method name as-is
+ Assert.Contains ("public void methodNamesNotMangled ()", java);
+ Assert.Contains ("n_methodNamesNotMangled ()", java);
+ }
+
+ ///
+ /// Ported from legacy GenerateExportedMembers — [Export] with throws.
+ ///
+ [Fact]
+ public void Generate_ExportMethodWithThrows_HasThrowsClause ()
+ {
+ var peers = ScanFixtures ();
+ var peer = FindByJavaName (peers, "my/app/ExportMembersComprehensive");
+ var java = GenerateToString (peer);
+
+ // throws clause appears on the line after the method signature
+ Assert.Contains ("methodThatThrows ()\n\t\tthrows java.lang.Throwable\n", java);
+ }
+
+ ///
+ /// Ported from legacy GenerateExportedMembers — [Export] with empty throws array.
+ /// Should NOT generate a throws clause.
+ ///
+ [Fact]
+ public void Generate_ExportMethodWithEmptyThrows_NoThrowsClause ()
+ {
+ var peers = ScanFixtures ();
+ var peer = FindByJavaName (peers, "my/app/ExportMembersComprehensive");
+ var java = GenerateToString (peer);
+
+ // methodThatThrowsEmptyArray should NOT have throws clause
+ // It should appear as a plain method declaration
+ Assert.Contains ("public void methodThatThrowsEmptyArray ()", java);
+
+ // Make sure the throws clause is NOT on this specific method
+ // (it might be on methodThatThrows, but not on methodThatThrowsEmptyArray)
+ var lines = java.Split ('\n');
+ for (int i = 0; i < lines.Length; i++) {
+ if (lines [i].Contains ("methodThatThrowsEmptyArray")) {
+ // The line with the method should not have throws,
+ // and neither should the next line
+ Assert.DoesNotContain ("throws", lines [i]);
+ if (i + 1 < lines.Length) {
+ Assert.DoesNotContain ("throws", lines [i + 1]);
+ }
+ break;
+ }
+ }
+ }
+
+ [Fact]
+ public void Generate_ExportMethod_NoOverrideAnnotation ()
+ {
+ var peers = ScanFixtures ();
+ var peer = FindByJavaName (peers, "my/app/ExportMembersComprehensive");
+ var java = GenerateToString (peer);
+
+ // [Export] methods should NOT have @Override — they are new declarations, not overrides
+ var lines = java.Split ('\n');
+ for (int i = 0; i < lines.Length; i++) {
+ if (lines [i].Contains ("methodNamesNotMangled ()")) {
+ // The line before should NOT be @Override
+ Assert.True (i > 0);
+ Assert.DoesNotContain ("@Override", lines [i - 1]);
+ break;
+ }
+ }
+ }
+
+ [Fact]
+ public void Generate_ExportMethod_NativeDeclarationIsPrivate ()
+ {
+ var peers = ScanFixtures ();
+ var peer = FindByJavaName (peers, "my/app/ExportMembersComprehensive");
+ var java = GenerateToString (peer);
+
+ Assert.Contains ("private native void n_methodNamesNotMangled ()", java);
+ Assert.Contains ("private native java.lang.String n_CompletelyDifferentName (java.lang.String p0, int p1)", java);
+ }
+
+ }
+
+ public class StaticExportAndExportField : JcwJavaSourceGeneratorTests
+ {
+ [Fact]
+ public void Generate_StaticExportMethod_HasStaticKeyword ()
+ {
+ var peers = ScanFixtures ();
+ var peer = FindByJavaName (peers, "my/app/ExportStaticAndFields");
+ var java = GenerateToString (peer);
+
+ Assert.Contains ("public static void staticMethodNotMangled ()", java);
+ Assert.Contains ("private static native void n_staticMethodNotMangled ()", java);
+ }
+
+ [Fact]
+ public void Generate_InstanceExportMethod_NoStaticKeyword ()
+ {
+ var peers = ScanFixtures ();
+ var peer = FindByJavaName (peers, "my/app/ExportStaticAndFields");
+ var java = GenerateToString (peer);
+
+ // Instance method should NOT have static
+ Assert.Contains ("public void instanceMethod ()", java);
+ Assert.Contains ("private native void n_instanceMethod ()", java);
+ }
+
+ [Fact]
+ public void Generate_ExportField_StaticFieldDeclaration ()
+ {
+ var peers = ScanFixtures ();
+ var peer = FindByJavaName (peers, "my/app/ExportStaticAndFields");
+ var java = GenerateToString (peer);
+
+ // Static [ExportField] → "public static FIELD_NAME = MethodName ();"
+ Assert.Contains ("public static", java);
+ Assert.Contains ("STATIC_INSTANCE = GetInstance ();", java);
+ }
+
+ [Fact]
+ public void Generate_ExportField_InstanceFieldDeclaration ()
+ {
+ var peers = ScanFixtures ();
+ var peer = FindByJavaName (peers, "my/app/ExportStaticAndFields");
+ var java = GenerateToString (peer);
+
+ // Instance [ExportField] → "public FIELD_NAME = MethodName ();"
+ // Should NOT contain "static" for the VALUE field
+ var lines = java.Split ('\n');
+ bool foundValue = false;
+ foreach (var line in lines) {
+ if (line.Contains ("VALUE = GetValue ()")) {
+ foundValue = true;
+ Assert.DoesNotContain ("static", line);
+ break;
+ }
+ }
+ Assert.True (foundValue, "VALUE field declaration not found");
+ }
+
+ [Fact]
+ public void Generate_ExportField_MethodHasNativeCallback ()
+ {
+ var peers = ScanFixtures ();
+ var peer = FindByJavaName (peers, "my/app/ExportStaticAndFields");
+ var java = GenerateToString (peer);
+
+ // [ExportField] methods should have corresponding wrapper + native method
+ Assert.Contains ("n_GetInstance ()", java);
+ Assert.Contains ("n_GetValue ()", java);
+ }
+
+ [Fact]
+ public void Generate_ExportField_StaticMethodIsStatic ()
+ {
+ var peers = ScanFixtures ();
+ var peer = FindByJavaName (peers, "my/app/ExportStaticAndFields");
+ var java = GenerateToString (peer);
+
+ // Static [ExportField] method: both wrapper and native should be static
+ Assert.Contains ("public static", java);
+ Assert.Contains ("private static native", java);
+ }
+
+ [Fact]
+ public void Generate_ExportField_FieldsAppearBeforeConstructors ()
+ {
+ var peers = ScanFixtures ();
+ var peer = FindByJavaName (peers, "my/app/ExportStaticAndFields");
+ var java = GenerateToString (peer);
+
+ // Fields should appear before constructors (after static initializer)
+ int fieldsPos = java.IndexOf ("STATIC_INSTANCE");
+ int ctorPos = java.IndexOf ("ExportStaticAndFields (");
+ Assert.True (fieldsPos > 0, "STATIC_INSTANCE field not found");
+
+ // Constructors may or may not be present, but if so fields come first
+ if (ctorPos > 0) {
+ Assert.True (fieldsPos < ctorPos, "Fields should appear before constructors");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs
new file mode 100644
index 00000000000..c63801bc8e8
--- /dev/null
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs
@@ -0,0 +1,198 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection.Metadata;
+using System.Reflection.PortableExecutable;
+using Xunit;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests;
+
+public class RootTypeMapAssemblyGeneratorTests
+{
+ string GenerateRootAssembly (IReadOnlyList perAssemblyNames, string? assemblyName = null)
+ {
+ var outputPath = Path.Combine (Path.GetTempPath (), $"root-typemap-{Guid.NewGuid ():N}",
+ (assemblyName ?? "_Microsoft.Android.TypeMaps") + ".dll");
+ var generator = new RootTypeMapAssemblyGenerator (new Version (11, 0, 0, 0));
+ generator.Generate (perAssemblyNames, outputPath, assemblyName);
+ return outputPath;
+ }
+
+ static void CleanUp (string path)
+ {
+ var dir = Path.GetDirectoryName (path);
+ if (dir != null && Directory.Exists (dir))
+ try { Directory.Delete (dir, true); } catch { }
+ }
+
+ [Fact]
+ public void Generate_ProducesValidPEAssembly ()
+ {
+ var path = GenerateRootAssembly (new [] { "_App.TypeMap", "_Mono.Android.TypeMap" });
+ try {
+ Assert.True (File.Exists (path));
+ using var pe = new PEReader (File.OpenRead (path));
+ Assert.True (pe.HasMetadata);
+ } finally {
+ CleanUp (path);
+ }
+ }
+
+ [Fact]
+ public void Generate_DefaultAssemblyName ()
+ {
+ var path = GenerateRootAssembly (Array.Empty ());
+ try {
+ using var pe = new PEReader (File.OpenRead (path));
+ var reader = pe.GetMetadataReader ();
+ var asmDef = reader.GetAssemblyDefinition ();
+ Assert.Equal ("_Microsoft.Android.TypeMaps", reader.GetString (asmDef.Name));
+ } finally {
+ CleanUp (path);
+ }
+ }
+
+ [Fact]
+ public void Generate_CustomAssemblyName ()
+ {
+ var path = GenerateRootAssembly (Array.Empty (), "MyRoot");
+ try {
+ using var pe = new PEReader (File.OpenRead (path));
+ var reader = pe.GetMetadataReader ();
+ var asmDef = reader.GetAssemblyDefinition ();
+ Assert.Equal ("MyRoot", reader.GetString (asmDef.Name));
+ } finally {
+ CleanUp (path);
+ }
+ }
+
+ [Fact]
+ public void Generate_ReferencesGenericTypeMapAssemblyTargetAttribute ()
+ {
+ var path = GenerateRootAssembly (new [] { "_App.TypeMap" });
+ try {
+ using var pe = new PEReader (File.OpenRead (path));
+ var reader = pe.GetMetadataReader ();
+
+ // The attribute type is referenced (not defined) — look for TypeRef
+ var typeRefs = reader.TypeReferences
+ .Select (h => reader.GetTypeReference (h))
+ .ToList ();
+ Assert.Contains (typeRefs, t =>
+ reader.GetString (t.Name) == "TypeMapAssemblyTargetAttribute`1" &&
+ reader.GetString (t.Namespace) == "System.Runtime.InteropServices");
+
+ // Java.Lang.Object must also be referenced (generic type argument)
+ Assert.Contains (typeRefs, t =>
+ reader.GetString (t.Name) == "Object" &&
+ reader.GetString (t.Namespace) == "Java.Lang");
+
+ // No TypeDefinition for the attribute (it's external)
+ var typeDefs = reader.TypeDefinitions
+ .Select (h => reader.GetTypeDefinition (h))
+ .ToList ();
+ Assert.DoesNotContain (typeDefs, t =>
+ reader.GetString (t.Name).Contains ("TypeMapAssemblyTarget"));
+ } finally {
+ CleanUp (path);
+ }
+ }
+
+ [Fact]
+ public void Generate_AttributeCtorIsOnGenericTypeSpec ()
+ {
+ var path = GenerateRootAssembly (new [] { "_App.TypeMap" });
+ try {
+ using var pe = new PEReader (File.OpenRead (path));
+ var reader = pe.GetMetadataReader ();
+
+ var attr = reader.GetCustomAttribute (
+ reader.GetCustomAttributes (EntityHandle.AssemblyDefinition).First ());
+
+ // The ctor should be a MemberReference (on a TypeSpec), not a MethodDefinition
+ Assert.Equal (HandleKind.MemberReference, attr.Constructor.Kind);
+
+ var memberRef = reader.GetMemberReference ((MemberReferenceHandle) attr.Constructor);
+ // Parent should be a TypeSpec (closed generic)
+ Assert.Equal (HandleKind.TypeSpecification, memberRef.Parent.Kind);
+ } finally {
+ CleanUp (path);
+ }
+ }
+
+ [Fact]
+ public void Generate_EmptyList_ProducesValidAssemblyWithNoTargetAttributes ()
+ {
+ var path = GenerateRootAssembly (Array.Empty ());
+ try {
+ using var pe = new PEReader (File.OpenRead (path));
+ var reader = pe.GetMetadataReader ();
+ var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition);
+ Assert.Empty (asmAttrs);
+ } finally {
+ CleanUp (path);
+ }
+ }
+
+ [Fact]
+ public void Generate_MultipleTargets_HasCorrectAttributeCount ()
+ {
+ var targets = new [] { "_App.TypeMap", "_Mono.Android.TypeMap", "_Java.Interop.TypeMap" };
+ var path = GenerateRootAssembly (targets);
+ try {
+ using var pe = new PEReader (File.OpenRead (path));
+ var reader = pe.GetMetadataReader ();
+ var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition);
+ Assert.Equal (3, asmAttrs.Count ());
+ } finally {
+ CleanUp (path);
+ }
+ }
+
+ [Fact]
+ public void Generate_HasModuleType ()
+ {
+ var path = GenerateRootAssembly (Array.Empty ());
+ try {
+ using var pe = new PEReader (File.OpenRead (path));
+ var reader = pe.GetMetadataReader ();
+ var types = reader.TypeDefinitions
+ .Select (h => reader.GetTypeDefinition (h))
+ .ToList ();
+ Assert.Contains (types, t => reader.GetString (t.Name) == "");
+ } finally {
+ CleanUp (path);
+ }
+ }
+
+ [Fact]
+ public void Generate_AttributeBlobValues_MatchTargetNames ()
+ {
+ var targets = new [] { "_App.TypeMap", "_Mono.Android.TypeMap" };
+ var path = GenerateRootAssembly (targets);
+ try {
+ using var pe = new PEReader (File.OpenRead (path));
+ var reader = pe.GetMetadataReader ();
+
+ var attrValues = new List ();
+ foreach (var attrHandle in reader.GetCustomAttributes (EntityHandle.AssemblyDefinition)) {
+ var attr = reader.GetCustomAttribute (attrHandle);
+ var blob = reader.GetBlobReader (attr.Value);
+
+ // Custom attribute blob: prolog (2 bytes) + SerString value
+ var prolog = blob.ReadUInt16 ();
+ Assert.Equal (1, prolog); // ECMA-335 prolog
+ var value = blob.ReadSerializedString ();
+ Assert.NotNull (value);
+ attrValues.Add (value!);
+ }
+
+ Assert.Equal (2, attrValues.Count);
+ Assert.Contains ("_App.TypeMap", attrValues);
+ Assert.Contains ("_Mono.Android.TypeMap", attrValues);
+ } finally {
+ CleanUp (path);
+ }
+ }
+}
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs
new file mode 100644
index 00000000000..52e42e9db45
--- /dev/null
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs
@@ -0,0 +1,1150 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Reflection.Metadata;
+using System.Reflection.Metadata.Ecma335;
+using System.Reflection.PortableExecutable;
+using Xunit;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests;
+
+public class TypeMapAssemblyGeneratorTests
+{
+ static string TestFixtureAssemblyPath {
+ get {
+ var testAssemblyDir = Path.GetDirectoryName (typeof (TypeMapAssemblyGeneratorTests).Assembly.Location)!;
+ var fixtureAssembly = Path.Combine (testAssemblyDir, "TestFixtures.dll");
+ Assert.True (File.Exists (fixtureAssembly),
+ $"TestFixtures.dll not found at {fixtureAssembly}. Ensure the TestFixtures project builds.");
+ return fixtureAssembly;
+ }
+ }
+
+ static readonly Lazy> _cachedFixtures = new (() => {
+ using var scanner = new JavaPeerScanner ();
+ return scanner.Scan (new [] { TestFixtureAssemblyPath });
+ });
+
+ static List ScanFixtures () => _cachedFixtures.Value;
+
+ static string GenerateAssembly (IReadOnlyList peers, string? assemblyName = null)
+ {
+ var outputPath = Path.Combine (Path.GetTempPath (), $"typemap-test-{Guid.NewGuid ():N}",
+ (assemblyName ?? "TestTypeMap") + ".dll");
+ var generator = new TypeMapAssemblyGenerator (new Version (11, 0, 0, 0));
+ generator.Generate (peers, outputPath, assemblyName);
+ return outputPath;
+ }
+
+ static (PEReader pe, MetadataReader reader) OpenAssembly (string path)
+ {
+ var pe = new PEReader (File.OpenRead (path));
+ return (pe, pe.GetMetadataReader ());
+ }
+
+ ///
+ /// Reads the RegisterNatives method's IL body and extracts all (jniMethodName, jniSignature) pairs
+ /// from the ldstr instructions. RegisterNatives emits repeating sequences of:
+ /// ldarg.1; ldstr jniMethodName; ldstr jniSignature; ldftn wrapper; call RegisterMethod
+ ///
+ static List<(string jniMethodName, string jniSignature)> ReadRegisterNativesEntries (
+ PEReader pe, MetadataReader reader, TypeDefinition proxy)
+ {
+ var registerNatives = proxy.GetMethods ()
+ .Select (h => reader.GetMethodDefinition (h))
+ .First (m => reader.GetString (m.Name) == "RegisterNatives");
+
+ var body = pe.GetMethodBody (registerNatives.RelativeVirtualAddress);
+ var il = body.GetILBytes ()!;
+ var result = new List<(string, string)> ();
+ var strings = new List ();
+
+ for (int i = 0; i < il.Length;) {
+ byte op = il [i++];
+ if (op == 0x72) { // ldstr
+ int token = il [i] | (il [i + 1] << 8) | (il [i + 2] << 16) | (il [i + 3] << 24);
+ i += 4;
+ var handle = MetadataTokens.UserStringHandle (token & 0x00FFFFFF);
+ strings.Add (reader.GetUserString (handle));
+ } else if (op == 0x28) { // call — marks end of a registration sequence
+ i += 4;
+ if (strings.Count >= 2) {
+ result.Add ((strings [strings.Count - 2], strings [strings.Count - 1]));
+ }
+ strings.Clear ();
+ } else if (op == 0xFE) { // two-byte opcode prefix (ldftn = 0xFE 0x06)
+ i++; // skip second opcode byte
+ if (i < il.Length && il [i - 1] == 0x06) { // ldftn has 4-byte token
+ i += 4;
+ }
+ } else if (op >= 0x02 && op <= 0x05) { // ldarg.0-3 — no operand
+ // skip
+ } else if (op == 0x0E) { // ldarg.s — 1-byte operand
+ i++;
+ } else if (op == 0x00 || op == 0x01 || op == 0x2A) { // nop, break, ret — no operand
+ // skip
+ }
+ }
+
+ return result;
+ }
+
+
+ public class BasicAssemblyStructure
+ {
+
+ [Fact]
+ public void Generate_ProducesValidPEAssembly ()
+ {
+ var peers = ScanFixtures ();
+ var path = GenerateAssembly (peers);
+ try {
+ Assert.True (File.Exists (path));
+ using var pe = new PEReader (File.OpenRead (path));
+ Assert.True (pe.HasMetadata);
+ var reader = pe.GetMetadataReader ();
+ Assert.NotNull (reader);
+ } finally {
+ CleanUp (path);
+ }
+ }
+
+ [Fact]
+ public void Generate_AssemblyHasCorrectName ()
+ {
+ var peers = ScanFixtures ();
+ var path = GenerateAssembly (peers, "MyTestTypeMap");
+ try {
+ var (pe, reader) = OpenAssembly (path);
+ using (pe) {
+ var asmDef = reader.GetAssemblyDefinition ();
+ Assert.Equal ("MyTestTypeMap", reader.GetString (asmDef.Name));
+ }
+ } finally {
+ CleanUp (path);
+ }
+ }
+
+ [Fact]
+ public void Generate_HasModuleType ()
+ {
+ var peers = ScanFixtures ();
+ var path = GenerateAssembly (peers);
+ try {
+ var (pe, reader) = OpenAssembly (path);
+ using (pe) {
+ var types = reader.TypeDefinitions.Select (h => reader.GetTypeDefinition (h)).ToList ();
+ Assert.Contains (types, t => reader.GetString (t.Name) == "");
+ }
+ } finally {
+ CleanUp (path);
+ }
+ }
+
+ }
+
+ public class AssemblyReference
+ {
+
+ [Fact]
+ public void Generate_HasRequiredAssemblyReferences ()
+ {
+ var peers = ScanFixtures ();
+ var path = GenerateAssembly (peers);
+ try {
+ var (pe, reader) = OpenAssembly (path);
+ using (pe) {
+ var asmRefs = reader.AssemblyReferences
+ .Select (h => reader.GetString (reader.GetAssemblyReference (h).Name))
+ .ToList ();
+ Assert.Contains ("System.Runtime", asmRefs);
+ Assert.Contains ("Mono.Android", asmRefs);
+ Assert.Contains ("Java.Interop", asmRefs);
+ Assert.Contains ("System.Runtime.InteropServices", asmRefs);
+ }
+ } finally {
+ CleanUp (path);
+ }
+ }
+
+ }
+
+ public class TypemapAttribute
+ {
+
+ [Fact]
+ public void Generate_HasTypeMapAttributes ()
+ {
+ var peers = ScanFixtures ();
+ var path = GenerateAssembly (peers);
+ try {
+ var (pe, reader) = OpenAssembly (path);
+ using (pe) {
+ var assemblyCustomAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition);
+ Assert.NotEmpty (assemblyCustomAttrs);
+ // We should have at least as many attributes as non-duplicate peers
+ // (TypeMap attrs + IgnoresAccessChecksTo attrs)
+ Assert.True (assemblyCustomAttrs.Count () >= 2);
+ }
+ } finally {
+ CleanUp (path);
+ }
+ }
+
+ }
+
+ public class ProxyType
+ {
+
+ [Fact]
+ public void Generate_CreatesProxyTypes ()
+ {
+ var peers = ScanFixtures ();
+ var path = GenerateAssembly (peers);
+ try {
+ var (pe, reader) = OpenAssembly (path);
+ using (pe) {
+ var proxyTypes = reader.TypeDefinitions
+ .Select (h => reader.GetTypeDefinition (h))
+ .Where (t => reader.GetString (t.Namespace) == "_TypeMap.Proxies")
+ .ToList ();
+
+ // At least some proxy types should be generated
+ Assert.NotEmpty (proxyTypes);
+
+ // Check that a proxy exists for java/lang/Object → Java_Lang_Object_Proxy
+ Assert.Contains (proxyTypes, t => reader.GetString (t.Name) == "Java_Lang_Object_Proxy");
+ }
+ } finally {
+ CleanUp (path);
+ }
+ }
+
+ [Fact]
+ public void Generate_ProxyTypesAreSealedClasses ()
+ {
+ var peers = ScanFixtures ();
+ var path = GenerateAssembly (peers);
+ try {
+ var (pe, reader) = OpenAssembly (path);
+ using (pe) {
+ var proxyTypes = reader.TypeDefinitions
+ .Select (h => reader.GetTypeDefinition (h))
+ .Where (t => reader.GetString (t.Namespace) == "_TypeMap.Proxies")
+ .ToList ();
+
+ foreach (var proxy in proxyTypes) {
+ Assert.True ((proxy.Attributes & TypeAttributes.Sealed) != 0,
+ $"Proxy {reader.GetString (proxy.Name)} should be sealed");
+ Assert.True ((proxy.Attributes & TypeAttributes.Public) != 0,
+ $"Proxy {reader.GetString (proxy.Name)} should be public");
+ }
+ }
+ } finally {
+ CleanUp (path);
+ }
+ }
+
+ [Fact]
+ public void Generate_ProxyType_HasCtorAndCreateInstance ()
+ {
+ var peers = ScanFixtures ();
+ var path = GenerateAssembly (peers);
+ try {
+ var (pe, reader) = OpenAssembly (path);
+ using (pe) {
+ var objectProxy = reader.TypeDefinitions
+ .Select (h => reader.GetTypeDefinition (h))
+ .First (t => reader.GetString (t.Name) == "Java_Lang_Object_Proxy");
+
+ var methods = objectProxy.GetMethods ()
+ .Select (h => reader.GetMethodDefinition (h))
+ .Select (m => reader.GetString (m.Name))
+ .ToList ();
+
+ Assert.Contains (".ctor", methods);
+ Assert.Contains ("CreateInstance", methods);
+ Assert.Contains ("get_TargetType", methods);
+ }
+ } finally {
+ CleanUp (path);
+ }
+ }
+
+ }
+
+ public class AcwProxy
+ {
+
+ [Fact]
+ public void Generate_AcwProxy_HasRegisterNativesAndUcoMethods ()
+ {
+ var peers = ScanFixtures ();
+ // Find a non-MCW type with marshal methods (e.g., my/app/CustomView has constructors)
+ var acwPeer = peers.First (p => p.JavaName == "my/app/TouchHandler");
+ var path = GenerateAssembly (new [] { acwPeer }, "AcwTest");
+ try {
+ var (pe, reader) = OpenAssembly (path);
+ using (pe) {
+ var proxy = reader.TypeDefinitions
+ .Select (h => reader.GetTypeDefinition (h))
+ .First (t => reader.GetString (t.Name) == "MyApp_TouchHandler_Proxy");
+
+ var methods = proxy.GetMethods ()
+ .Select (h => reader.GetMethodDefinition (h))
+ .Select (m => reader.GetString (m.Name))
+ .ToList ();
+
+ Assert.Contains ("RegisterNatives", methods);
+ // UCO wrappers for each marshal method
+ Assert.Contains (methods, m => m.StartsWith ("n_") && m.EndsWith ("_uco_0"));
+ }
+ } finally {
+ CleanUp (path);
+ }
+ }
+
+ [Fact]
+ public void Generate_AcwProxy_HasUnmanagedCallersOnlyAttribute ()
+ {
+ var peers = ScanFixtures ();
+ var acwPeer = peers.First (p => p.JavaName == "my/app/TouchHandler");
+ var path = GenerateAssembly (new [] { acwPeer }, "UcoTest");
+ try {
+ var (pe, reader) = OpenAssembly (path);
+ using (pe) {
+ var proxy = reader.TypeDefinitions
+ .Select (h => reader.GetTypeDefinition (h))
+ .First (t => reader.GetString (t.Name) == "MyApp_TouchHandler_Proxy");
+
+ // Find a UCO method
+ var ucoMethod = proxy.GetMethods ()
+ .Select (h => reader.GetMethodDefinition (h))
+ .First (m => reader.GetString (m.Name).Contains ("_uco_"));
+
+ // Verify it has [UnmanagedCallersOnly] attribute
+ var attrs = ucoMethod.GetCustomAttributes ()
+ .Select (h => reader.GetCustomAttribute (h))
+ .ToList ();
+ Assert.NotEmpty (attrs);
+ }
+ } finally {
+ CleanUp (path);
+ }
+ }
+
+ }
+
+ public class ExportMarshalMethods
+ {
+
+ [Fact]
+ public void Generate_ExportMethod_ProducesValidAssembly ()
+ {
+ var peers = ScanFixtures ();
+ var peer = peers.First (p => p.JavaName == "my/app/ExportMethodWithParams");
+ var path = GenerateAssembly (new [] { peer }, "ExportMethodTest");
+ try {
+ var (pe, reader) = OpenAssembly (path);
+ using (pe) {
+ Assert.True (reader.TypeDefinitions.Count > 0);
+ }
+ } finally {
+ CleanUp (path);
+ }
+ }
+
+ [Fact]
+ public void Generate_ExportMethod_HasWrapperMethods ()
+ {
+ var peers = ScanFixtures ();
+ var peer = peers.First (p => p.JavaName == "my/app/ExportMethodWithParams");
+ var path = GenerateAssembly (new [] { peer }, "ExportMethodWrappers");
+ try {
+ var (pe, reader) = OpenAssembly (path);
+ using (pe) {
+ var proxy = reader.TypeDefinitions
+ .Select (h => reader.GetTypeDefinition (h))
+ .First (t => reader.GetString (t.Name) == "MyApp_ExportMethodWithParams_Proxy");
+
+ var methods = proxy.GetMethods ()
+ .Select (h => reader.GetMethodDefinition (h))
+ .Select (m => reader.GetString (m.Name))
+ .ToList ();
+
+ // Export methods should produce UCO wrappers
+ Assert.Contains (methods, m => m.StartsWith ("n_") && m.Contains ("_uco"));
+ Assert.Contains ("RegisterNatives", methods);
+ }
+ } finally {
+ CleanUp (path);
+ }
+ }
+
+ [Fact]
+ public void Generate_ExportMarshalComplex_UsesArrayMarshalHelpers ()
+ {
+ var peers = ScanFixtures ();
+ var peer = peers.First (p => p.JavaName == "my/app/ExportMarshalComplex");
+ var path = GenerateAssembly (new [] { peer }, "ExportMarshalComplexHelpers");
+ try {
+ var (pe, reader) = OpenAssembly (path);
+ using (pe) {
+ var memberNames = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef))
+ .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i)))
+ .Select (m => reader.GetString (m.Name))
+ .ToList ();
+ Assert.Contains ("GetArray", memberNames);
+ Assert.Contains ("NewArray", memberNames);
+ Assert.Contains ("CopyArray", memberNames);
+ Assert.Contains ("GetCharSequence", memberNames);
+ }
+ } finally {
+ CleanUp (path);
+ }
+ }
+
+ [Fact]
+ public void Generate_ExportConstructor_HasWrapperMethods ()
+ {
+ var peers = ScanFixtures ();
+ var peer = peers.First (p => p.JavaName == "my/app/ExportsConstructors");
+ var path = GenerateAssembly (new [] { peer }, "ExportCtorWrappers");
+ try {
+ var (pe, reader) = OpenAssembly (path);
+ using (pe) {
+ var proxy = reader.TypeDefinitions
+ .Select (h => reader.GetTypeDefinition (h))
+ .First (t => reader.GetString (t.Name) == "MyApp_ExportsConstructors_Proxy");
+
+ var methods = proxy.GetMethods ()
+ .Select (h => reader.GetMethodDefinition (h))
+ .Select (m => reader.GetString (m.Name))
+ .ToList ();
+
+ // Export constructors should produce nctor_N_uco wrappers
+ Assert.Contains (methods, m => m.StartsWith ("nctor_") && m.EndsWith ("_uco"));
+ Assert.Contains ("RegisterNatives", methods);
+ }
+ } finally {
+ CleanUp (path);
+ }
+ }
+
+ [Fact]
+ public void Generate_ExportOnlyType_HasProxyAndRegistration ()
+ {
+ var peers = ScanFixtures ();
+ var peer = peers.First (p => p.ManagedTypeName == "MyApp.UnregisteredExporter");
+ var path = GenerateAssembly (new [] { peer }, "ExportOnlyType");
+ try {
+ var (pe, reader) = OpenAssembly (path);
+ using (pe) {
+ var proxy = reader.TypeDefinitions
+ .Select (h => reader.GetTypeDefinition (h))
+ .First (t => reader.GetString (t.Name) == "MyApp_UnregisteredExporter_Proxy");
+
+ var methods = proxy.GetMethods ()
+ .Select (h => reader.GetMethodDefinition (h))
+ .Select (m => reader.GetString (m.Name))
+ .ToList ();
+ Assert.Contains ("n_doExportedWork_uco_0", methods);
+ Assert.Contains ("RegisterNatives", methods);
+
+ var entries = ReadRegisterNativesEntries (pe, reader, proxy);
+ Assert.Contains (entries, e => e.jniMethodName == "n_DoExportedWork" && e.jniSignature == "()V");
+ }
+ } finally {
+ CleanUp (path);
+ }
+ }
+
+ [Fact]
+ public void Generate_ExportMethod_HasMethodBody ()
+ {
+ var peers = ScanFixtures ();
+ var peer = peers.First (p => p.JavaName == "my/app/ExportMethodWithParams");
+ var path = GenerateAssembly (new [] { peer }, "ExportMethodBody");
+ try {
+ var (pe, reader) = OpenAssembly (path);
+ using (pe) {
+ var proxy = reader.TypeDefinitions
+ .Select (h => reader.GetTypeDefinition (h))
+ .First (t => reader.GetString (t.Name) == "MyApp_ExportMethodWithParams_Proxy");
+
+ // Export marshal methods should have non-zero RVA (they have a real body)
+ var exportWrappers = proxy.GetMethods ()
+ .Select (h => reader.GetMethodDefinition (h))
+ .Where (m => {
+ var name = reader.GetString (m.Name);
+ return name.StartsWith ("n_") && name.Contains ("_uco");
+ })
+ .ToList ();
+
+ Assert.NotEmpty (exportWrappers);
+ foreach (var wrapper in exportWrappers) {
+ Assert.True (wrapper.RelativeVirtualAddress > 0,
+ $"Export marshal method '{reader.GetString (wrapper.Name)}' should have a method body (non-zero RVA)");
+ }
+ }
+ } finally {
+ CleanUp (path);
+ }
+ }
+
+ [Fact]
+ public void Generate_ExportMembersComprehensive_ProducesValidAssembly ()
+ {
+ var peers = ScanFixtures ();
+ var peer = peers.First (p => p.JavaName == "my/app/ExportMembersComprehensive");
+ var path = GenerateAssembly (new [] { peer }, "ExportComprehensive");
+ try {
+ var (pe, reader) = OpenAssembly (path);
+ using (pe) {
+ var proxy = reader.TypeDefinitions
+ .Select (h => reader.GetTypeDefinition (h))
+ .First (t => reader.GetString (t.Name) == "MyApp_ExportMembersComprehensive_Proxy");
+
+ var methods = proxy.GetMethods ()
+ .Select (h => reader.GetMethodDefinition (h))
+ .Select (m => reader.GetString (m.Name))
+ .ToList ();
+
+ // Should have multiple export wrappers
+ var exportWrappers = methods.Where (m => m.StartsWith ("n_") && m.Contains ("_uco")).ToList ();
+ Assert.True (exportWrappers.Count >= 2,
+ $"Expected at least 2 export wrappers, got {exportWrappers.Count}: [{string.Join (", ", exportWrappers)}]");
+ }
+ } finally {
+ CleanUp (path);
+ }
+ }
+
+ [Fact]
+ public void Generate_StaticExportMethod_ProducesValidAssembly ()
+ {
+ var peers = ScanFixtures ();
+ var peer = peers.First (p => p.JavaName == "my/app/ExportStaticAndFields");
+ var path = GenerateAssembly (new [] { peer }, "StaticExportTest");
+ try {
+ var (pe, reader) = OpenAssembly (path);
+ using (pe) {
+ var proxy = reader.TypeDefinitions
+ .Select (h => reader.GetTypeDefinition (h))
+ .First (t => reader.GetString (t.Name) == "MyApp_ExportStaticAndFields_Proxy");
+
+ var methods = proxy.GetMethods ()
+ .Select (h => reader.GetMethodDefinition (h))
+ .Select (m => reader.GetString (m.Name))
+ .ToList ();
+
+ // Should have export wrappers for static methods, instance methods, and ExportField methods
+ var exportWrappers = methods.Where (m => m.StartsWith ("n_") && m.Contains ("_uco")).ToList ();
+ Assert.True (exportWrappers.Count >= 3,
+ $"Expected at least 3 export wrappers (static, instance, ExportField), got {exportWrappers.Count}: [{string.Join (", ", exportWrappers)}]");
+
+ Assert.Contains ("RegisterNatives", methods);
+ }
+ } finally {
+ CleanUp (path);
+ }
+ }
+
+ [Fact]
+ public void Generate_StaticExportMethod_HasMethodBodyWithCorrectPattern ()
+ {
+ var peers = ScanFixtures ();
+ var peer = peers.First (p => p.JavaName == "my/app/ExportStaticAndFields");
+ var path = GenerateAssembly (new [] { peer }, "StaticExportBody");
+ try {
+ var (pe, reader) = OpenAssembly (path);
+ using (pe) {
+ var proxy = reader.TypeDefinitions
+ .Select (h => reader.GetTypeDefinition (h))
+ .First (t => reader.GetString (t.Name) == "MyApp_ExportStaticAndFields_Proxy");
+
+ // All export wrappers should have non-zero RVA
+ var exportWrappers = proxy.GetMethods ()
+ .Select (h => reader.GetMethodDefinition (h))
+ .Where (m => {
+ var name = reader.GetString (m.Name);
+ return name.StartsWith ("n_") && name.Contains ("_uco");
+ })
+ .ToList ();
+
+ Assert.NotEmpty (exportWrappers);
+ foreach (var wrapper in exportWrappers) {
+ Assert.True (wrapper.RelativeVirtualAddress > 0,
+ $"Export marshal method '{reader.GetString (wrapper.Name)}' should have a method body");
+ }
+ }
+ } finally {
+ CleanUp (path);
+ }
+ }
+
+ }
+
+ public class ExportNativeRegistration
+ {
+
+ [Fact]
+ public void Generate_ExportMethods_RegisteredInRegisterNatives ()
+ {
+ var peers = ScanFixtures ();
+ var peer = peers.First (p => p.JavaName == "my/app/ExportMembersComprehensive");
+ var path = GenerateAssembly (new [] { peer }, "ExportRegistration");
+ try {
+ var (pe, reader) = OpenAssembly (path);
+ using (pe) {
+ var proxy = reader.TypeDefinitions
+ .Select (h => reader.GetTypeDefinition (h))
+ .First (t => reader.GetString (t.Name) == "MyApp_ExportMembersComprehensive_Proxy");
+
+ var entries = ReadRegisterNativesEntries (pe, reader, proxy);
+ var jniNames = entries.Select (e => e.jniMethodName).ToList ();
+
+ Assert.Contains ("n_methodNamesNotMangled", jniNames);
+ Assert.Contains ("n_CompletelyDifferentName", jniNames);
+ Assert.Contains ("n_methodThatThrows", jniNames);
+ Assert.Contains ("n_methodThatThrowsEmptyArray", jniNames);
+ }
+ } finally {
+ CleanUp (path);
+ }
+ }
+
+ [Fact]
+ public void Generate_ExportConstructors_RegisteredInRegisterNatives ()
+ {
+ var peers = ScanFixtures ();
+ var peer = peers.First (p => p.JavaName == "my/app/ExportsConstructors");
+ var path = GenerateAssembly (new [] { peer }, "ExportCtorRegistration");
+ try {
+ var (pe, reader) = OpenAssembly (path);
+ using (pe) {
+ var proxy = reader.TypeDefinitions
+ .Select (h => reader.GetTypeDefinition (h))
+ .First (t => reader.GetString (t.Name) == "MyApp_ExportsConstructors_Proxy");
+
+ var entries = ReadRegisterNativesEntries (pe, reader, proxy);
+ var jniNames = entries.Select (e => e.jniMethodName).ToList ();
+
+ // Two [Export] constructors → nctor_0, nctor_1
+ Assert.Contains ("nctor_0", jniNames);
+ Assert.Contains ("nctor_1", jniNames);
+ }
+ } finally {
+ CleanUp (path);
+ }
+ }
+
+ [Fact]
+ public void Generate_StaticExportAndExportField_RegisteredInRegisterNatives ()
+ {
+ var peers = ScanFixtures ();
+ var peer = peers.First (p => p.JavaName == "my/app/ExportStaticAndFields");
+ var path = GenerateAssembly (new [] { peer }, "StaticExportRegistration");
+ try {
+ var (pe, reader) = OpenAssembly (path);
+ using (pe) {
+ var proxy = reader.TypeDefinitions
+ .Select (h => reader.GetTypeDefinition (h))
+ .First (t => reader.GetString (t.Name) == "MyApp_ExportStaticAndFields_Proxy");
+
+ var entries = ReadRegisterNativesEntries (pe, reader, proxy);
+ var jniNames = entries.Select (e => e.jniMethodName).ToList ();
+
+ // Static [Export] method
+ Assert.Contains ("n_staticMethodNotMangled", jniNames);
+ // Instance [Export] method
+ Assert.Contains ("n_instanceMethod", jniNames);
+ // [ExportField] backing methods
+ Assert.Contains ("n_GetInstance", jniNames);
+ Assert.Contains ("n_GetValue", jniNames);
+ }
+ } finally {
+ CleanUp (path);
+ }
+ }
+
+ [Fact]
+ public void Generate_ExportMarshalComplex_RegisteredInRegisterNatives ()
+ {
+ var peers = ScanFixtures ();
+ var peer = peers.First (p => p.JavaName == "my/app/ExportMarshalComplex");
+ var path = GenerateAssembly (new [] { peer }, "ExportMarshalComplexRegistration");
+ try {
+ var (pe, reader) = OpenAssembly (path);
+ using (pe) {
+ var proxy = reader.TypeDefinitions
+ .Select (h => reader.GetTypeDefinition (h))
+ .First (t => reader.GetString (t.Name) == "MyApp_ExportMarshalComplex_Proxy");
+
+ var entries = ReadRegisterNativesEntries (pe, reader, proxy);
+ Assert.Contains (entries, e => e.jniMethodName == "n_MutateInts" && e.jniSignature == "([I)V");
+ Assert.Contains (entries, e => e.jniMethodName == "n_RoundTripEnum" && e.jniSignature == "(I)I");
+ Assert.Contains (entries, e => e.jniMethodName == "n_EchoCharSequence" && e.jniSignature == "(Ljava/lang/CharSequence;)Ljava/lang/CharSequence;");
+ Assert.Contains (entries, e => e.jniMethodName == "n_EchoViews" && e.jniSignature == "([Landroid/view/View;)[Landroid/view/View;");
+ Assert.Contains (entries, e => e.jniMethodName == "n_EchoStrings" && e.jniSignature == "([Ljava/lang/String;)[Ljava/lang/String;");
+ }
+ } finally {
+ CleanUp (path);
+ }
+ }
+
+ [Fact]
+ public void Generate_ExportRegistration_HasCorrectJniSignatures ()
+ {
+ var peers = ScanFixtures ();
+ var peer = peers.First (p => p.JavaName == "my/app/ExportMembersComprehensive");
+ var path = GenerateAssembly (new [] { peer }, "ExportSignatures");
+ try {
+ var (pe, reader) = OpenAssembly (path);
+ using (pe) {
+ var proxy = reader.TypeDefinitions
+ .Select (h => reader.GetTypeDefinition (h))
+ .First (t => reader.GetString (t.Name) == "MyApp_ExportMembersComprehensive_Proxy");
+
+ var entries = ReadRegisterNativesEntries (pe, reader, proxy);
+
+ // CompletelyDifferentName(String, int) → String = (Ljava/lang/String;I)Ljava/lang/String;
+ var nameOverride = entries.First (e => e.jniMethodName == "n_CompletelyDifferentName");
+ Assert.Equal ("(Ljava/lang/String;I)Ljava/lang/String;", nameOverride.jniSignature);
+
+ // void methodNamesNotMangled() = ()V
+ var simple = entries.First (e => e.jniMethodName == "n_methodNamesNotMangled");
+ Assert.Equal ("()V", simple.jniSignature);
+ }
+ } finally {
+ CleanUp (path);
+ }
+ }
+
+ }
+
+ public class Ignoresaccesschecksto
+ {
+
+ [Fact]
+ public void Generate_HasIgnoresAccessChecksToAttribute ()
+ {
+ var peers = ScanFixtures ();
+ var path = GenerateAssembly (peers);
+ try {
+ var (pe, reader) = OpenAssembly (path);
+ using (pe) {
+ // The IgnoresAccessChecksToAttribute type should be defined
+ var types = reader.TypeDefinitions
+ .Select (h => reader.GetTypeDefinition (h))
+ .ToList ();
+ Assert.Contains (types, t =>
+ reader.GetString (t.Name) == "IgnoresAccessChecksToAttribute" &&
+ reader.GetString (t.Namespace) == "System.Runtime.CompilerServices");
+ }
+ } finally {
+ CleanUp (path);
+ }
+ }
+
+ }
+
+ public class Alias
+ {
+
+ [Fact]
+ public void Generate_DuplicateJniNames_CreatesAliasEntries ()
+ {
+ // Create two peers with the same JNI name — these become aliases
+ var peers = new List {
+ new JavaPeerInfo {
+ JavaName = "test/Duplicate",
+ ManagedTypeName = "Test.Duplicate1",
+ ManagedTypeNamespace = "Test",
+ ManagedTypeShortName = "Duplicate1",
+ AssemblyName = "TestAssembly",
+ ActivationCtor = new ActivationCtorInfo {
+ DeclaringTypeName = "Test.Duplicate1",
+ DeclaringAssemblyName = "TestAssembly",
+ Style = ActivationCtorStyle.XamarinAndroid,
+ },
+ },
+ new JavaPeerInfo {
+ JavaName = "test/Duplicate",
+ ManagedTypeName = "Test.Duplicate2",
+ ManagedTypeNamespace = "Test",
+ ManagedTypeShortName = "Duplicate2",
+ AssemblyName = "TestAssembly",
+ },
+ };
+
+ var path = GenerateAssembly (peers, "AliasTest");
+ try {
+ var (pe, reader) = OpenAssembly (path);
+ using (pe) {
+ var assemblyAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition);
+ // Should have 2 TypeMap entries + TypeMapAssociation + IgnoresAccessChecksTo entries
+ Assert.True (assemblyAttrs.Count () >= 3);
+ }
+ } finally {
+ CleanUp (path);
+ }
+ }
+
+ [Fact]
+ public void Generate_DuplicateJniNames_EmitsTypeMapAssociationAttribute ()
+ {
+ var peers = new List {
+ new JavaPeerInfo {
+ JavaName = "test/Duplicate",
+ ManagedTypeName = "Test.Duplicate1",
+ ManagedTypeNamespace = "Test",
+ ManagedTypeShortName = "Duplicate1",
+ AssemblyName = "TestAssembly",
+ ActivationCtor = new ActivationCtorInfo {
+ DeclaringTypeName = "Test.Duplicate1",
+ DeclaringAssemblyName = "TestAssembly",
+ Style = ActivationCtorStyle.XamarinAndroid,
+ },
+ },
+ new JavaPeerInfo {
+ JavaName = "test/Duplicate",
+ ManagedTypeName = "Test.Duplicate2",
+ ManagedTypeNamespace = "Test",
+ ManagedTypeShortName = "Duplicate2",
+ AssemblyName = "TestAssembly",
+ },
+ };
+
+ var path = GenerateAssembly (peers, "AliasAssocTest");
+ try {
+ var (pe, reader) = OpenAssembly (path);
+ using (pe) {
+ // Look for TypeMapAssociationAttribute references
+ var memberRefs = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef))
+ .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i)))
+ .Where (m => reader.GetString (m.Name) == ".ctor")
+ .ToList ();
+
+ // There should be a TypeMapAssociationAttribute ctor reference
+ var typeNames = reader.TypeReferences
+ .Select (h => reader.GetTypeReference (h))
+ .Select (t => reader.GetString (t.Name))
+ .ToList ();
+ Assert.Contains ("TypeMapAssociationAttribute", typeNames);
+ }
+ } finally {
+ CleanUp (path);
+ }
+ }
+
+ }
+
+ public class EmptyInput
+ {
+
+ [Fact]
+ public void Generate_EmptyPeerList_ProducesValidAssembly ()
+ {
+ var path = GenerateAssembly (Array.Empty (), "EmptyTest");
+ try {
+ Assert.True (File.Exists (path));
+ var (pe, reader) = OpenAssembly (path);
+ using (pe) {
+ Assert.NotNull (reader);
+ var asmDef = reader.GetAssemblyDefinition ();
+ Assert.Equal ("EmptyTest", reader.GetString (asmDef.Name));
+ }
+ } finally {
+ CleanUp (path);
+ }
+ }
+
+ }
+
+ public class PerassemblyModel
+ {
+
+ [Fact]
+ public void Generate_SingleAssemblyInput_Works ()
+ {
+ var allPeers = ScanFixtures ();
+ // Filter to just one assembly's peers
+ var testFixturePeers = allPeers.Where (p => p.AssemblyName == "TestFixtures").ToList ();
+
+ var path = GenerateAssembly (testFixturePeers, "_TestFixtures.TypeMap");
+ try {
+ var (pe, reader) = OpenAssembly (path);
+ using (pe) {
+ var asmDef = reader.GetAssemblyDefinition ();
+ Assert.Equal ("_TestFixtures.TypeMap", reader.GetString (asmDef.Name));
+
+ // Should still have proxy types
+ var proxyTypes = reader.TypeDefinitions
+ .Select (h => reader.GetTypeDefinition (h))
+ .Where (t => reader.GetString (t.Namespace) == "_TypeMap.Proxies")
+ .ToList ();
+ Assert.NotEmpty (proxyTypes);
+ }
+ } finally {
+ CleanUp (path);
+ }
+ }
+
+ }
+
+ public class JniSignatureHelperTests
+ {
+
+ [Theory]
+ [InlineData ("()V", 0)]
+ [InlineData ("(I)V", 1)]
+ [InlineData ("(Landroid/os/Bundle;)V", 1)]
+ [InlineData ("(IFJ)V", 3)]
+ [InlineData ("(ZLandroid/view/View;I)Z", 3)]
+ [InlineData ("([Ljava/lang/String;)V", 1)]
+ public void ParseParameterTypes_ParsesCorrectCount (string signature, int expectedCount)
+ {
+ var actual = JniSignatureHelper.ParseParameterTypes (signature);
+ Assert.Equal (expectedCount, actual.Count);
+ }
+
+ [Fact]
+ public void ParseParameterTypes_BooleanMapsToBoolean ()
+ {
+ var types = JniSignatureHelper.ParseParameterTypes ("(Z)V");
+ Assert.Single (types);
+ Assert.Equal (JniParamKind.Boolean, types [0]);
+ }
+
+ [Fact]
+ public void ParseParameterTypes_ObjectMapsToObject ()
+ {
+ var types = JniSignatureHelper.ParseParameterTypes ("(Ljava/lang/String;)V");
+ Assert.Single (types);
+ Assert.Equal (JniParamKind.Object, types [0]);
+ }
+
+ [Fact]
+ public void ParseReturnType_Void ()
+ {
+ Assert.Equal (JniParamKind.Void, JniSignatureHelper.ParseReturnType ("()V"));
+ }
+
+ [Fact]
+ public void ParseReturnType_Int ()
+ {
+ Assert.Equal (JniParamKind.Int, JniSignatureHelper.ParseReturnType ("()I"));
+ }
+
+ [Fact]
+ public void ParseReturnType_Boolean ()
+ {
+ Assert.Equal (JniParamKind.Boolean, JniSignatureHelper.ParseReturnType ("()Z"));
+ }
+
+ [Fact]
+ public void ParseReturnType_Object ()
+ {
+ Assert.Equal (JniParamKind.Object, JniSignatureHelper.ParseReturnType ("()Ljava/lang/String;"));
+ }
+
+ }
+
+ public class NegativeEdgecase
+ {
+
+ [Theory]
+ [InlineData ("")]
+ [InlineData ("not-a-sig")]
+ [InlineData ("(")]
+ public void ParseParameterTypes_InvalidSignature_ThrowsOrReturnsEmpty (string signature)
+ {
+ // Should not crash — either returns empty or throws ArgumentException
+ try {
+ var result = JniSignatureHelper.ParseParameterTypes (signature);
+ // If it doesn't throw, empty is acceptable
+ Assert.NotNull (result);
+ } catch (Exception ex) when (ex is ArgumentException || ex is IndexOutOfRangeException || ex is FormatException) {
+ // Any of these are acceptable for malformed input
+ }
+ }
+
+ [Fact]
+ public void Generate_NullPeers_ThrowsArgumentNull ()
+ {
+ var gen = new TypeMapAssemblyGenerator (new Version (11, 0, 0, 0));
+ var tmpPath = Path.Combine (Path.GetTempPath (), Guid.NewGuid ().ToString ("N"), "test.dll");
+ Assert.Throws (() => gen.Generate (null!, tmpPath));
+ }
+
+ [Fact]
+ public void Generate_NullOutputPath_ThrowsArgumentNull ()
+ {
+ var gen = new TypeMapAssemblyGenerator (new Version (11, 0, 0, 0));
+ Assert.Throws (() => gen.Generate (Array.Empty (), null!));
+ }
+
+ }
+
+ public class CreateInstancePaths
+ {
+
+ [Fact]
+ public void Generate_SimpleActivity_UsesGetUninitializedObject ()
+ {
+ // SimpleActivity has no own activation ctor — inherits from Activity
+ var peers = ScanFixtures ();
+ var simpleActivity = peers.First (p => p.JavaName == "my/app/SimpleActivity");
+ Assert.NotNull (simpleActivity.ActivationCtor);
+ Assert.NotEqual (simpleActivity.ManagedTypeName, simpleActivity.ActivationCtor.DeclaringTypeName);
+
+ var path = GenerateAssembly (new [] { simpleActivity }, "InheritedCtorTest");
+ try {
+ var (pe, reader) = OpenAssembly (path);
+ using (pe) {
+ // Verify RuntimeHelpers type is referenced (for GetUninitializedObject)
+ var typeNames = reader.TypeReferences
+ .Select (h => reader.GetTypeReference (h))
+ .Select (t => reader.GetString (t.Name))
+ .ToList ();
+ Assert.Contains ("RuntimeHelpers", typeNames);
+
+ // Verify no CreateManagedPeer reference exists
+ var memberNames = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef))
+ .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i)))
+ .Select (m => reader.GetString (m.Name))
+ .ToList ();
+ Assert.DoesNotContain ("CreateManagedPeer", memberNames);
+ Assert.Contains ("GetUninitializedObject", memberNames);
+
+ // The .ctor MemberRef must target the base type that declares the activation ctor
+ var baseTypeName = simpleActivity.ActivationCtor.DeclaringTypeName;
+ var baseSimpleName = baseTypeName.Contains ('.') ? baseTypeName.Substring (baseTypeName.LastIndexOf ('.') + 1) : baseTypeName;
+ var ctorMemberRefs = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef))
+ .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i)))
+ .Where (m => reader.GetString (m.Name) == ".ctor")
+ .ToList ();
+ // One of the .ctor refs must be on the base type
+ bool hasBaseCtorRef = ctorMemberRefs.Any (m => {
+ if (m.Parent.Kind == HandleKind.TypeReference) {
+ var tr = reader.GetTypeReference ((TypeReferenceHandle)m.Parent);
+ return reader.GetString (tr.Name) == baseSimpleName;
+ }
+ return false;
+ });
+ Assert.True (hasBaseCtorRef, $"Should have .ctor MemberRef on base type {baseSimpleName}");
+ }
+ } finally {
+ CleanUp (path);
+ }
+ }
+
+ [Fact]
+ public void Generate_LeafCtor_DoesNotUseCreateManagedPeer ()
+ {
+ var peers = ScanFixtures ();
+ // ClickableView has its own (IntPtr, JniHandleOwnership) ctor
+ var clickableView = peers.First (p => p.JavaName == "my/app/ClickableView");
+ Assert.NotNull (clickableView.ActivationCtor);
+ Assert.Equal (clickableView.ManagedTypeName, clickableView.ActivationCtor.DeclaringTypeName);
+
+ var path = GenerateAssembly (new [] { clickableView }, "LeafCtorTest");
+ try {
+ var (pe, reader) = OpenAssembly (path);
+ using (pe) {
+ // Should NOT have CreateManagedPeer — leaf ctor uses direct newobj
+ var memberNames = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef))
+ .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i)))
+ .Select (m => reader.GetString (m.Name))
+ .ToList ();
+ Assert.DoesNotContain ("CreateManagedPeer", memberNames);
+
+ // Should have a .ctor MemberRef for the target type (direct newobj)
+ var ctorRefs = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef))
+ .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i)))
+ .Where (m => reader.GetString (m.Name) == ".ctor")
+ .ToList ();
+ Assert.True (ctorRefs.Count >= 2, "Should have ctor refs for proxy base + target type");
+ }
+ } finally {
+ CleanUp (path);
+ }
+ }
+
+ [Fact]
+ public void Generate_GenericType_ThrowsNotSupportedException ()
+ {
+ var peers = ScanFixtures ();
+ var generic = peers.First (p => p.JavaName == "my/app/GenericHolder");
+ Assert.True (generic.IsGenericDefinition);
+
+ var path = GenerateAssembly (new [] { generic }, "GenericTest");
+ try {
+ var (pe, reader) = OpenAssembly (path);
+ using (pe) {
+ // NotSupportedException should be referenced
+ var typeNames = reader.TypeReferences
+ .Select (h => reader.GetTypeReference (h))
+ .Select (t => reader.GetString (t.Name))
+ .ToList ();
+ Assert.Contains ("NotSupportedException", typeNames);
+ }
+ } finally {
+ CleanUp (path);
+ }
+ }
+
+ }
+
+ public class IgnoresAccessChecksToForBaseCtor
+ {
+
+ [Fact]
+ public void Generate_InheritedCtor_IncludesBaseCtorAssembly ()
+ {
+ // SimpleActivity inherits activation ctor from Activity — both in TestFixtures
+ // but the generated assembly is "IgnoresAccessTest", so TestFixtures must be
+ // in IgnoresAccessChecksTo
+ var peers = ScanFixtures ();
+ var simpleActivity = peers.First (p => p.JavaName == "my/app/SimpleActivity");
+
+ var path = GenerateAssembly (new [] { simpleActivity }, "IgnoresAccessTest");
+ try {
+ var (pe, reader) = OpenAssembly (path);
+ using (pe) {
+ // Find the IgnoresAccessChecksToAttribute type
+ var ignoresAttrType = reader.TypeDefinitions
+ .Select (h => reader.GetTypeDefinition (h))
+ .FirstOrDefault (t => reader.GetString (t.Name) == "IgnoresAccessChecksToAttribute");
+ Assert.True (ignoresAttrType.Attributes != 0, "IgnoresAccessChecksToAttribute should be defined");
+
+ // Check assembly-level custom attributes include the base ctor's assembly
+ var assemblyAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition);
+ var attrBlobs = new List ();
+ foreach (var attrHandle in assemblyAttrs) {
+ var attr = reader.GetCustomAttribute (attrHandle);
+ var blob = reader.GetBlobBytes (attr.Value);
+ var blobStr = System.Text.Encoding.UTF8.GetString (blob);
+ attrBlobs.Add (blobStr);
+ }
+ // Activity is in TestFixtures, so IgnoresAccessChecksTo must include TestFixtures
+ Assert.Contains (attrBlobs, b => b.Contains ("TestFixtures"));
+ }
+ } finally {
+ CleanUp (path);
+ }
+ }
+
+ }
+
+ static void CleanUp (string path)
+ {
+ var dir = Path.GetDirectoryName (path);
+ if (dir != null && Directory.Exists (dir)) {
+ try { Directory.Delete (dir, true); } catch { }
+ }
+ }
+}
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs
new file mode 100644
index 00000000000..4231f6af4ee
--- /dev/null
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs
@@ -0,0 +1,2117 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection.Metadata;
+using System.Reflection.PortableExecutable;
+using Xunit;
+
+namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests;
+
+public class ModelBuilderTests
+{
+ static string TestFixtureAssemblyPath {
+ get {
+ var testAssemblyDir = Path.GetDirectoryName (typeof (ModelBuilderTests).Assembly.Location)!;
+ var fixtureAssembly = Path.Combine (testAssemblyDir, "TestFixtures.dll");
+ Assert.True (File.Exists (fixtureAssembly),
+ $"TestFixtures.dll not found at {fixtureAssembly}. Ensure the TestFixtures project builds.");
+ return fixtureAssembly;
+ }
+ }
+
+ static readonly Lazy> _cachedFixtures = new (() => {
+ using var scanner = new JavaPeerScanner ();
+ return scanner.Scan (new [] { TestFixtureAssemblyPath });
+ });
+
+ static List ScanFixtures () => _cachedFixtures.Value;
+
+ static TypeMapAssemblyData BuildModel (IReadOnlyList peers, string? assemblyName = null)
+ {
+ var outputPath = Path.Combine ("/tmp", (assemblyName ?? "TestTypeMap") + ".dll");
+ return ModelBuilder.Build (peers, outputPath, assemblyName);
+ }
+
+
+ public class BasicStructure
+ {
+
+ [Fact]
+ public void Build_EmptyPeers_ProducesEmptyModel ()
+ {
+ var model = BuildModel (Array.Empty (), "Empty");
+ Assert.Equal ("Empty", model.AssemblyName);
+ Assert.Equal ("Empty.dll", model.ModuleName);
+ Assert.Empty (model.Entries);
+ Assert.Empty (model.ProxyTypes);
+ }
+
+ [Fact]
+ public void Build_AssemblyNameDerivedFromOutputPath ()
+ {
+ var model = ModelBuilder.Build (Array.Empty (), "/some/path/Foo.Bar.dll");
+ Assert.Equal ("Foo.Bar", model.AssemblyName);
+ Assert.Equal ("Foo.Bar.dll", model.ModuleName);
+ }
+
+ [Fact]
+ public void Build_ExplicitAssemblyName_OverridesOutputPath ()
+ {
+ var model = ModelBuilder.Build (Array.Empty (), "/some/path/Foo.dll", "MyAssembly");
+ Assert.Equal ("MyAssembly", model.AssemblyName);
+ }
+
+ [Fact]
+ public void Build_EmptyInput_HasEmptyIgnoresAccessChecksTo ()
+ {
+ var model = BuildModel (Array.Empty ());
+ Assert.Empty (model.IgnoresAccessChecksTo);
+ }
+
+ [Fact]
+ public void Build_ComputesIgnoresAccessChecksToFromCrossAssemblyCallbacks ()
+ {
+ var peer = MakeAcwPeer ("my/app/MainActivity", "MyApp.MainActivity", "MyApp");
+ ((List) peer.MarshalMethods).Add (new MarshalMethodInfo {
+ JniName = "onCreate",
+ NativeCallbackName = "n_OnCreate",
+ JniSignature = "(Landroid/os/Bundle;)V",
+ IsConstructor = false,
+ Connector = "n_OnCreate_handler",
+ DeclaringTypeName = "Android.App.Activity",
+ DeclaringAssemblyName = "Mono.Android",
+ });
+ var model = BuildModel (new [] { peer });
+ // The UCO callback type references Mono.Android, which is cross-assembly
+ Assert.Contains ("Mono.Android", model.IgnoresAccessChecksTo);
+ // The output assembly itself should not appear
+ Assert.DoesNotContain (model.AssemblyName, model.IgnoresAccessChecksTo);
+ }
+
+ }
+
+ public class TypeMapEntries
+ {
+
+ [Fact]
+ public void Build_CreatesOneEntryPerPeer ()
+ {
+ var peers = new List {
+ MakeMcwPeer ("java/lang/Object", "Java.Lang.Object", "Mono.Android"),
+ MakeMcwPeer ("android/app/Activity", "Android.App.Activity", "Mono.Android"),
+ };
+
+ var model = BuildModel (peers);
+ Assert.Equal (2, model.Entries.Count);
+ // Entries are ordered by JNI name (alphabetical)
+ Assert.Equal ("android/app/Activity", model.Entries [0].JniName);
+ Assert.Equal ("java/lang/Object", model.Entries [1].JniName);
+ }
+
+ [Fact]
+ public void Build_DuplicateJniNames_CreatesAliasEntries ()
+ {
+ var peers = new List {
+ MakeMcwPeer ("test/Dup", "Test.First", "A"),
+ MakeMcwPeer ("test/Dup", "Test.Second", "A"),
+ };
+
+ var model = BuildModel (peers);
+ // Two entries: primary "test/Dup" and alias "test/Dup[1]"
+ Assert.Equal (2, model.Entries.Count);
+ Assert.Equal ("test/Dup", model.Entries [0].JniName);
+ Assert.Contains ("Test.First", model.Entries [0].ProxyTypeReference);
+ Assert.Equal ("test/Dup[1]", model.Entries [1].JniName);
+ Assert.Contains ("Test.Second", model.Entries [1].ProxyTypeReference);
+ }
+
+ }
+
+ public class ConditionalAttributes
+ {
+
+ [Fact]
+ public void Build_EssentialRuntimeType_IsUnconditional ()
+ {
+ var peer = MakeMcwPeer ("java/lang/Object", "Java.Lang.Object", "Mono.Android");
+ peer.DoNotGenerateAcw = true;
+ var model = BuildModel (new [] { peer });
+
+ Assert.Single (model.Entries);
+ Assert.True (model.Entries [0].IsUnconditional);
+ Assert.Null (model.Entries [0].TargetTypeReference);
+ }
+
+ [Theory]
+ [InlineData ("java/lang/Object")]
+ [InlineData ("java/lang/Throwable")]
+ [InlineData ("java/lang/Exception")]
+ [InlineData ("java/lang/RuntimeException")]
+ [InlineData ("java/lang/Error")]
+ [InlineData ("java/lang/Class")]
+ [InlineData ("java/lang/String")]
+ [InlineData ("java/lang/Thread")]
+ public void Build_AllEssentialRuntimeTypes_AreUnconditional (string jniName)
+ {
+ var peer = MakeMcwPeer (jniName, "Java.Lang.SomeType", "Mono.Android");
+ peer.DoNotGenerateAcw = true;
+ var model = BuildModel (new [] { peer });
+ Assert.True (model.Entries [0].IsUnconditional, $"{jniName} should be unconditional");
+ }
+
+ [Fact]
+ public void Build_UserAcwType_IsUnconditional ()
+ {
+ // User-defined ACW types (not MCW, not interface) are unconditional
+ // because Android can instantiate them from Java
+ var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App");
+ var model = BuildModel (new [] { peer });
+
+ var mainEntry = model.Entries.First (e => e.JniName == "my/app/Main");
+ Assert.True (mainEntry.IsUnconditional);
+ Assert.Null (mainEntry.TargetTypeReference);
+ }
+
+ [Fact]
+ public void Build_McwBinding_IsTrimmable ()
+ {
+ // MCW binding types (DoNotGenerateAcw=true) are trimmable unless essential
+ var peer = MakeMcwPeer ("android/app/Activity", "Android.App.Activity", "Mono.Android");
+ peer.DoNotGenerateAcw = true;
+ var model = BuildModel (new [] { peer });
+
+ Assert.Single (model.Entries);
+ Assert.False (model.Entries [0].IsUnconditional);
+ Assert.NotNull (model.Entries [0].TargetTypeReference);
+ Assert.Contains ("Android.App.Activity, Mono.Android", model.Entries [0].TargetTypeReference!);
+ }
+
+ [Fact]
+ public void Build_Interface_IsTrimmable ()
+ {
+ var peer = new JavaPeerInfo {
+ JavaName = "android/view/View$OnClickListener",
+ ManagedTypeName = "Android.Views.View+IOnClickListener",
+ ManagedTypeNamespace = "Android.Views",
+ ManagedTypeShortName = "IOnClickListener",
+ AssemblyName = "Mono.Android",
+ IsInterface = true,
+ InvokerTypeName = "Android.Views.View+IOnClickListenerInvoker",
+ };
+
+ var model = BuildModel (new [] { peer });
+ Assert.Single (model.Entries);
+ Assert.False (model.Entries [0].IsUnconditional);
+ Assert.NotNull (model.Entries [0].TargetTypeReference);
+ }
+
+ [Fact]
+ public void Build_UnconditionalScannedType_IsUnconditional ()
+ {
+ // Types with IsUnconditional from scanner (e.g., from [Activity], [Service] attrs)
+ var peer = MakeMcwPeer ("my/app/MySvc", "MyApp.MyService", "App");
+ peer.DoNotGenerateAcw = true; // simulate MCW-like
+ peer.IsUnconditional = true; // scanner marked it
+ var model = BuildModel (new [] { peer });
+
+ Assert.True (model.Entries [0].IsUnconditional);
+ }
+
+ }
+
+ public class Aliases
+ {
+
+ [Fact]
+ public void Build_AliasedPeers_GetIndexedJniNames ()
+ {
+ var peers = new List {
+ MakeMcwPeer ("test/Dup", "Test.First", "A"),
+ MakeMcwPeer ("test/Dup", "Test.Second", "A"),
+ MakeMcwPeer ("test/Dup", "Test.Third", "A"),
+ };
+
+ var model = BuildModel (peers);
+ Assert.Equal (3, model.Entries.Count);
+ Assert.Equal ("test/Dup", model.Entries [0].JniName);
+ Assert.Equal ("test/Dup[1]", model.Entries [1].JniName);
+ Assert.Equal ("test/Dup[2]", model.Entries [2].JniName);
+ }
+
+ [Fact]
+ public void Build_AliasedPeersWithActivation_GetDistinctProxies ()
+ {
+ var peers = new List {
+ MakePeerWithActivation ("test/Dup", "Test.First", "A"),
+ MakePeerWithActivation ("test/Dup", "Test.Second", "A"),
+ };
+
+ var model = BuildModel (peers, "TypeMap");
+ Assert.Equal (2, model.ProxyTypes.Count);
+ // Distinct proxy names based on managed type names
+ Assert.Equal ("Test_First_Proxy", model.ProxyTypes [0].TypeName);
+ Assert.Equal ("Test_Second_Proxy", model.ProxyTypes [1].TypeName);
+ }
+
+ [Fact]
+ public void Build_McwPeerWithoutActivation_NoProxy ()
+ {
+ var peer = MakeMcwPeer ("java/lang/Object", "Java.Lang.Object", "Mono.Android");
+ // No activation ctor, no invoker → no proxy
+ var model = BuildModel (new [] { peer });
+
+ Assert.Empty (model.ProxyTypes);
+ Assert.Single (model.Entries);
+ Assert.Contains ("Java.Lang.Object, Mono.Android", model.Entries [0].ProxyTypeReference);
+ }
+
+ }
+
+ public class ProxyTypes
+ {
+
+ [Fact]
+ public void Build_PeerWithActivationCtor_CreatesProxy ()
+ {
+ var peer = MakePeerWithActivation ("java/lang/Object", "Java.Lang.Object", "Mono.Android");
+ var model = BuildModel (new [] { peer }, "MyTypeMap");
+
+ Assert.Single (model.ProxyTypes);
+ var proxy = model.ProxyTypes [0];
+ Assert.Equal ("Java_Lang_Object_Proxy", proxy.TypeName);
+ Assert.Equal ("_TypeMap.Proxies", proxy.Namespace);
+ Assert.True (proxy.HasActivation);
+ Assert.Equal ("Java.Lang.Object", proxy.TargetType.ManagedTypeName);
+ Assert.Equal ("Mono.Android", proxy.TargetType.AssemblyName);
+ }
+
+ [Fact]
+ public void Build_LeafActivationCtor_IsOnLeafTypeTrue ()
+ {
+ var peer = MakePeerWithActivation ("my/app/Foo", "MyApp.Foo", "App");
+ // Leaf: the type itself declares the activation ctor
+ peer.ActivationCtor!.DeclaringTypeName = "MyApp.Foo";
+ peer.ActivationCtor!.DeclaringAssemblyName = "App";
+
+ var model = BuildModel (new [] { peer });
+ var proxy = model.ProxyTypes [0];
+
+ Assert.NotNull (proxy.ActivationCtor);
+ Assert.True (proxy.ActivationCtor!.IsOnLeafType);
+ Assert.Equal ("MyApp.Foo", proxy.ActivationCtor.DeclaringType.ManagedTypeName);
+ }
+
+ [Fact]
+ public void Build_InheritedActivationCtor_IsOnLeafTypeFalse ()
+ {
+ var peer = MakePeerWithActivation ("my/app/Bar", "MyApp.Bar", "App");
+ // Inherited: a base type declares the activation ctor
+ peer.ActivationCtor!.DeclaringTypeName = "MyApp.BaseBar";
+ peer.ActivationCtor!.DeclaringAssemblyName = "App";
+
+ var model = BuildModel (new [] { peer });
+ var proxy = model.ProxyTypes [0];
+
+ Assert.NotNull (proxy.ActivationCtor);
+ Assert.False (proxy.ActivationCtor!.IsOnLeafType);
+ Assert.Equal ("MyApp.BaseBar", proxy.ActivationCtor.DeclaringType.ManagedTypeName);
+ Assert.Equal ("App", proxy.ActivationCtor.DeclaringType.AssemblyName);
+ }
+
+ [Fact]
+ public void Build_InheritedActivationCtor_CrossAssembly_AddsIgnoresAccessChecksTo ()
+ {
+ var peer = MakePeerWithActivation ("my/app/Baz", "MyApp.Baz", "App");
+ peer.ActivationCtor!.DeclaringTypeName = "Base.Activity";
+ peer.ActivationCtor!.DeclaringAssemblyName = "Mono.Android";
+
+ var model = BuildModel (new [] { peer }, "TypeMapAsm");
+
+ Assert.Contains ("Mono.Android", model.IgnoresAccessChecksTo);
+ }
+
+ [Fact]
+ public void Build_PeerWithInvoker_CreatesProxy ()
+ {
+ var peer = new JavaPeerInfo {
+ JavaName = "android/view/View$OnClickListener",
+ ManagedTypeName = "Android.Views.View+IOnClickListener",
+ ManagedTypeNamespace = "Android.Views",
+ ManagedTypeShortName = "IOnClickListener",
+ AssemblyName = "Mono.Android",
+ IsInterface = true,
+ InvokerTypeName = "Android.Views.View+IOnClickListenerInvoker",
+ };
+
+ var model = BuildModel (new [] { peer });
+ Assert.Single (model.ProxyTypes);
+ var proxy = model.ProxyTypes [0];
+ Assert.NotNull (proxy.InvokerType);
+ Assert.Equal ("Android.Views.View+IOnClickListenerInvoker", proxy.InvokerType!.ManagedTypeName);
+ }
+
+ [Fact]
+ public void Build_PeerWithInvokerButNoActivationCtor_ProxyHasActivationTrue ()
+ {
+ // An interface with an invoker type has HasActivation = true because
+ // CreateInstance will instantiate the invoker type.
+ var peer = new JavaPeerInfo {
+ JavaName = "android/view/View$OnClickListener",
+ ManagedTypeName = "Android.Views.View+IOnClickListener",
+ ManagedTypeNamespace = "Android.Views",
+ ManagedTypeShortName = "IOnClickListener",
+ AssemblyName = "Mono.Android",
+ IsInterface = true,
+ InvokerTypeName = "Android.Views.View+IOnClickListenerInvoker",
+ };
+
+ var model = BuildModel (new [] { peer });
+ Assert.Single (model.ProxyTypes);
+ var proxy = model.ProxyTypes [0];
+ Assert.True (proxy.HasActivation);
+ Assert.NotNull (proxy.InvokerType);
+ }
+
+ [Fact]
+ public void Build_ProxyNaming_ReplacesDotAndPlus ()
+ {
+ var peer = MakePeerWithActivation ("com/example/Outer$Inner", "Com.Example.Outer.Inner", "App");
+ var model = BuildModel (new [] { peer });
+
+ Assert.Single (model.ProxyTypes);
+ Assert.Equal ("Com_Example_Outer_Inner_Proxy", model.ProxyTypes [0].TypeName);
+ }
+
+ [Fact]
+ public void Build_EntryPointsToProxy_WhenProxyExists ()
+ {
+ var peer = MakePeerWithActivation ("java/lang/Object", "Java.Lang.Object", "Mono.Android");
+ var model = BuildModel (new [] { peer }, "MyTypeMap");
+
+ var entry = model.Entries [0];
+ Assert.Contains ("Java_Lang_Object_Proxy", entry.ProxyTypeReference);
+ Assert.Contains ("MyTypeMap", entry.ProxyTypeReference);
+ }
+
+ }
+
+ public class AcwDetection
+ {
+
+ [Fact]
+ public void Build_AcwType_IsAcwTrue ()
+ {
+ var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App");
+ var model = BuildModel (new [] { peer });
+
+ Assert.Single (model.ProxyTypes);
+ Assert.True (model.ProxyTypes [0].IsAcw);
+ }
+
+ [Fact]
+ public void Build_McwType_IsAcwFalse ()
+ {
+ var peer = MakePeerWithActivation ("java/lang/Object", "Java.Lang.Object", "Mono.Android");
+ var model = BuildModel (new [] { peer });
+
+ Assert.Single (model.ProxyTypes);
+ Assert.False (model.ProxyTypes [0].IsAcw);
+ }
+
+ [Fact]
+ public void Build_InterfaceWithMarshalMethods_IsNotAcw ()
+ {
+ var peer = new JavaPeerInfo {
+ JavaName = "android/view/View$OnClickListener",
+ ManagedTypeName = "Android.Views.View+IOnClickListener",
+ ManagedTypeNamespace = "Android.Views",
+ ManagedTypeShortName = "IOnClickListener",
+ AssemblyName = "Mono.Android",
+ IsInterface = true,
+ InvokerTypeName = "Android.Views.View+IOnClickListenerInvoker",
+ MarshalMethods = new List {
+ MakeMarshalMethod ("onClick", "n_OnClick", "(Landroid/view/View;)V"),
+ },
+ };
+
+ var model = BuildModel (new [] { peer });
+ Assert.Single (model.ProxyTypes);
+ // Interface is NOT an ACW even with marshal methods
+ Assert.False (model.ProxyTypes [0].IsAcw);
+ }
+
+ [Fact]
+ public void Build_DoNotGenerateAcw_IsNotAcw ()
+ {
+ var peer = MakePeerWithActivation ("java/lang/Object", "Java.Lang.Object", "Mono.Android");
+ peer.DoNotGenerateAcw = true;
+ peer.MarshalMethods = new List {
+ MakeMarshalMethod ("toString", "n_ToString", "()Ljava/lang/String;"),
+ };
+
+ var model = BuildModel (new [] { peer });
+ Assert.Single (model.ProxyTypes);
+ Assert.False (model.ProxyTypes [0].IsAcw);
+ }
+
+ }
+
+ public class UcoMethods
+ {
+
+ [Fact]
+ public void Build_AcwWithMarshalMethods_CreatesUcoMethods ()
+ {
+ var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App");
+ peer.MarshalMethods = new List {
+ MakeMarshalMethod ("", "n_ctor", "()V", isConstructor: true),
+ MakeMarshalMethod ("onCreate", "n_OnCreate", "(Landroid/os/Bundle;)V"),
+ MakeMarshalMethod ("onResume", "n_OnResume", "()V"),
+ };
+
+ var model = BuildModel (new [] { peer });
+ var proxy = model.ProxyTypes [0];
+
+ Assert.Equal (2, proxy.UcoMethods.Count);
+
+ // Method UCOs (only non-constructor methods)
+ Assert.Equal ("n_onCreate_uco_0", proxy.UcoMethods [0].WrapperName);
+ Assert.Equal ("n_OnCreate", proxy.UcoMethods [0].CallbackMethodName);
+ Assert.Equal ("(Landroid/os/Bundle;)V", proxy.UcoMethods [0].JniSignature);
+
+ Assert.Equal ("n_onResume_uco_1", proxy.UcoMethods [1].WrapperName);
+ Assert.Equal ("n_OnResume", proxy.UcoMethods [1].CallbackMethodName);
+
+ // Constructor goes into ExportMarshalMethods (full marshal body)
+ var ctorExport = proxy.ExportMarshalMethods.FirstOrDefault (e => e.IsConstructor);
+ Assert.NotNull (ctorExport);
+ Assert.Equal ("nctor_0_uco", ctorExport.WrapperName);
+ }
+
+ [Fact]
+ public void Build_UcoMethod_CallbackTypeIsDeclaringType ()
+ {
+ var mm = MakeMarshalMethod ("toString", "n_ToString", "()Ljava/lang/String;");
+ mm.DeclaringTypeName = "Java.Lang.Object";
+ mm.DeclaringAssemblyName = "Mono.Android";
+
+ var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App");
+ peer.MarshalMethods = new List {
+ MakeMarshalMethod ("", "n_ctor", "()V", isConstructor: true),
+ mm,
+ };
+
+ var model = BuildModel (new [] { peer });
+ var uco = model.ProxyTypes [0].UcoMethods [0];
+ Assert.Equal ("Java.Lang.Object", uco.CallbackType.ManagedTypeName);
+ Assert.Equal ("Mono.Android", uco.CallbackType.AssemblyName);
+ }
+
+ [Fact]
+ public void Build_UcoMethod_FallsBackToPeerType_WhenDeclaringTypeEmpty ()
+ {
+ var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App");
+ peer.MarshalMethods = new List {
+ MakeMarshalMethod ("", "n_ctor", "()V", isConstructor: true),
+ MakeMarshalMethod ("onPause", "n_OnPause", "()V"),
+ };
+
+ var model = BuildModel (new [] { peer });
+ var uco = model.ProxyTypes [0].UcoMethods [0];
+ Assert.Equal ("MyApp.MainActivity", uco.CallbackType.ManagedTypeName);
+ Assert.Equal ("App", uco.CallbackType.AssemblyName);
+ }
+
+ [Fact]
+ public void Build_ConstructorsInMarshalMethods_SkippedFromUcoMethods ()
+ {
+ var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App");
+ peer.MarshalMethods = new List {
+ MakeMarshalMethod ("", "n_ctor", "()V", isConstructor: true),
+ MakeMarshalMethod ("", "n_ctor2", "()V", isConstructor: true),
+ MakeMarshalMethod ("onStart", "n_OnStart", "()V"),
+ };
+
+ var model = BuildModel (new [] { peer });
+ var proxy = model.ProxyTypes [0];
+
+ // Constructors go to ExportMarshalMethods, not UcoMethods
+ Assert.Single (proxy.UcoMethods);
+ Assert.Equal ("n_onStart_uco_0", proxy.UcoMethods [0].WrapperName);
+ Assert.NotEmpty (proxy.ExportMarshalMethods.Where (e => e.IsConstructor));
+ }
+
+ }
+
+ public class ConstructorMarshalMethods
+ {
+
+ [Fact]
+ public void Build_AcwWithConstructors_CreatesExportMarshalMethod ()
+ {
+ var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App");
+
+ var model = BuildModel (new [] { peer });
+ var proxy = model.ProxyTypes [0];
+
+ // All constructors generate full marshal bodies in ExportMarshalMethods
+ var ctorExport = proxy.ExportMarshalMethods.FirstOrDefault (e => e.IsConstructor);
+ Assert.NotNull (ctorExport);
+ Assert.Equal ("nctor_0_uco", ctorExport.WrapperName);
+ Assert.Equal ("nctor_0", ctorExport.NativeCallbackName);
+ }
+
+ [Fact]
+ public void Build_PeerWithoutActivationCtor_NoConstructorMarshalMethods ()
+ {
+ // Peer with marshal methods but no activation ctor
+ var peer = new JavaPeerInfo {
+ JavaName = "my/app/Foo",
+ ManagedTypeName = "MyApp.Foo",
+ ManagedTypeNamespace = "MyApp",
+ ManagedTypeShortName = "Foo",
+ AssemblyName = "App",
+ InvokerTypeName = "MyApp.FooInvoker", // has invoker → will create proxy
+ MarshalMethods = new List {
+ MakeMarshalMethod ("bar", "n_Bar", "()V"),
+ },
+ JavaConstructors = new List {
+ new JavaConstructorInfo { ConstructorIndex = 0, JniSignature = "()V" },
+ },
+ };
+
+ var model = BuildModel (new [] { peer });
+ var proxy = model.ProxyTypes [0];
+
+ // No activation ctor → no constructor marshal methods
+ Assert.DoesNotContain (proxy.ExportMarshalMethods, e => e.IsConstructor);
+ }
+
+ }
+
+ public class NativeRegistrations
+ {
+
+ [Fact]
+ public void Build_NativeRegistrations_MatchUcoMethods ()
+ {
+ var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App");
+ peer.MarshalMethods = new List {
+ MakeMarshalMethod ("", "n_ctor", "()V", isConstructor: true),
+ MakeMarshalMethod ("onCreate", "n_OnCreate", "(Landroid/os/Bundle;)V"),
+ };
+
+ var model = BuildModel (new [] { peer });
+ var proxy = model.ProxyTypes [0];
+
+ // 1 registration for method + 1 for constructor
+ Assert.Equal (2, proxy.NativeRegistrations.Count);
+
+ var methodReg = proxy.NativeRegistrations [0];
+ Assert.Equal ("n_OnCreate", methodReg.JniMethodName);
+ Assert.Equal ("(Landroid/os/Bundle;)V", methodReg.JniSignature);
+ Assert.Equal ("n_onCreate_uco_0", methodReg.WrapperMethodName);
+
+ var ctorReg = proxy.NativeRegistrations [1];
+ Assert.Equal ("nctor_0", ctorReg.JniMethodName);
+ Assert.Equal ("()V", ctorReg.JniSignature);
+ Assert.Equal ("nctor_0_uco", ctorReg.WrapperMethodName);
+ }
+
+ [Fact]
+ public void Build_NativeRegistrations_ParameterizedConstructor_HasCorrectJniSignature ()
+ {
+ var peer = MakeAcwPeer ("my/app/MyView", "MyApp.MyView", "App");
+ peer.JavaConstructors = new List {
+ new JavaConstructorInfo { ConstructorIndex = 0, JniSignature = "()V" },
+ new JavaConstructorInfo { ConstructorIndex = 1, JniSignature = "(Landroid/content/Context;)V",
+ Parameters = new List {
+ new JniParameterInfo { JniType = "Landroid/content/Context;" },
+ }
+ },
+ };
+ peer.MarshalMethods = new List {
+ MakeMarshalMethod ("", "n_ctor", "()V", isConstructor: true),
+ MakeMarshalMethod ("", "n_ctor", "(Landroid/content/Context;)V", isConstructor: true),
+ };
+
+ var model = BuildModel (new [] { peer });
+ var proxy = model.ProxyTypes [0];
+
+ var ctorRegs = proxy.NativeRegistrations.Where (r => r.JniMethodName.StartsWith ("nctor_")).ToList ();
+ Assert.Equal (2, ctorRegs.Count);
+
+ Assert.Equal ("()V", ctorRegs [0].JniSignature);
+ Assert.Equal ("(Landroid/content/Context;)V", ctorRegs [1].JniSignature);
+ }
+
+ [Fact]
+ public void Build_NonAcwProxy_NoNativeRegistrations ()
+ {
+ var peer = MakePeerWithActivation ("java/lang/Object", "Java.Lang.Object", "Mono.Android");
+ var model = BuildModel (new [] { peer });
+
+ Assert.Single (model.ProxyTypes);
+ Assert.Empty (model.ProxyTypes [0].NativeRegistrations);
+ }
+
+ }
+
+ public class FixtureScan
+ {
+
+ [Fact]
+ public void Build_FromScannedFixtures_ProducesValidModel ()
+ {
+ var peers = ScanFixtures ();
+ var model = BuildModel (peers, "TestTypeMap");
+
+ Assert.Equal ("TestTypeMap", model.AssemblyName);
+ Assert.NotEmpty (model.Entries);
+ Assert.NotEmpty (model.ProxyTypes);
+
+ // All entries have non-empty JNI names
+ Assert.All (model.Entries, e => Assert.False (string.IsNullOrEmpty (e.JniName)));
+ Assert.All (model.Entries, e => Assert.False (string.IsNullOrEmpty (e.ProxyTypeReference)));
+ }
+
+ [Fact]
+ public void Build_FromScannedFixtures_NoProxiesForMcwWithoutActivation ()
+ {
+ var peers = ScanFixtures ();
+ var model = BuildModel (peers);
+
+ // Proxy type names should all end with _Proxy
+ Assert.All (model.ProxyTypes, p => Assert.EndsWith ("_Proxy", p.TypeName));
+ }
+
+ [Fact]
+ public void Build_FromScannedFixtures_AcwTypesHaveUcoMethods ()
+ {
+ var peers = ScanFixtures ();
+ var model = BuildModel (peers);
+
+ var acwProxies = model.ProxyTypes.Where (p => p.IsAcw).ToList ();
+ Assert.NotEmpty (acwProxies);
+
+ // ACW proxies should have native registrations for [Register] and [Export] members.
+ var proxiesWithRegistrations = acwProxies.Where (p => p.NativeRegistrations.Count > 0).ToList ();
+ Assert.NotEmpty (proxiesWithRegistrations);
+
+ // [Export]-only unregistered type should still get a proxy + registration.
+ var exportOnlyProxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_UnregisteredExporter_Proxy");
+ Assert.NotNull (exportOnlyProxy);
+ Assert.Contains (exportOnlyProxy!.NativeRegistrations, r => r.JniMethodName == "n_DoExportedWork");
+ }
+
+ }
+
+ public class FixtureConditionalAttributes
+ {
+
+ [Fact]
+ public void Fixture_JavaLangObject_IsUnconditional ()
+ {
+ var peer = FindFixtureByJavaName ("java/lang/Object");
+ var model = BuildModel (new [] { peer });
+ Assert.True (model.Entries [0].IsUnconditional);
+ }
+
+ [Fact]
+ public void Fixture_Throwable_IsUnconditional ()
+ {
+ var peer = FindFixtureByJavaName ("java/lang/Throwable");
+ var model = BuildModel (new [] { peer });
+ Assert.True (model.Entries [0].IsUnconditional);
+ }
+
+ [Fact]
+ public void Fixture_Exception_IsUnconditional ()
+ {
+ var peer = FindFixtureByJavaName ("java/lang/Exception");
+ var model = BuildModel (new [] { peer });
+ Assert.True (model.Entries [0].IsUnconditional);
+ }
+
+ [Fact]
+ public void Fixture_Activity_McwBinding_IsTrimmable ()
+ {
+ var peer = FindFixtureByJavaName ("android/app/Activity");
+ Assert.True (peer.DoNotGenerateAcw);
+ var model = BuildModel (new [] { peer });
+ // Activity is MCW and not an essential runtime type → trimmable
+ Assert.False (model.Entries [0].IsUnconditional);
+ Assert.Contains ("Android.App.Activity", model.Entries [0].TargetTypeReference!);
+ }
+
+ [Fact]
+ public void Fixture_MainActivity_UserAcw_IsUnconditional ()
+ {
+ var peer = FindFixtureByJavaName ("my/app/MainActivity");
+ Assert.False (peer.DoNotGenerateAcw);
+ Assert.False (peer.IsInterface);
+ var model = BuildModel (new [] { peer });
+ Assert.True (model.Entries [0].IsUnconditional);
+ }
+
+ [Fact]
+ public void Fixture_IOnClickListener_Interface_IsTrimmable ()
+ {
+ var peers = ScanFixtures ();
+ var listener = peers.First (p => p.ManagedTypeName == "Android.Views.IOnClickListener");
+ var model = BuildModel (new [] { listener });
+ Assert.False (model.Entries [0].IsUnconditional);
+ }
+
+ [Fact]
+ public void Fixture_TouchHandler_UserType_IsUnconditional ()
+ {
+ var peer = FindFixtureByJavaName ("my/app/TouchHandler");
+ Assert.False (peer.DoNotGenerateAcw);
+ var model = BuildModel (new [] { peer });
+ Assert.True (model.Entries [0].IsUnconditional);
+ }
+
+ [Fact]
+ public void Fixture_Button_McwBinding_IsTrimmable ()
+ {
+ var peer = FindFixtureByJavaName ("android/widget/Button");
+ Assert.True (peer.DoNotGenerateAcw);
+ var model = BuildModel (new [] { peer });
+ Assert.False (model.Entries [0].IsUnconditional);
+ }
+
+ }
+
+ static JavaPeerInfo MakeMcwPeer (string jniName, string managedName, string asmName)
+ {
+ var ns = managedName.Contains ('.') ? managedName.Substring (0, managedName.LastIndexOf ('.')) : "";
+ var shortName = managedName.Contains ('.') ? managedName.Substring (managedName.LastIndexOf ('.') + 1) : managedName;
+ return new JavaPeerInfo {
+ JavaName = jniName,
+ ManagedTypeName = managedName,
+ ManagedTypeNamespace = ns,
+ ManagedTypeShortName = shortName,
+ AssemblyName = asmName,
+ };
+ }
+
+ static JavaPeerInfo MakePeerWithActivation (string jniName, string managedName, string asmName)
+ {
+ var peer = MakeMcwPeer (jniName, managedName, asmName);
+ peer.ActivationCtor = new ActivationCtorInfo {
+ Style = ActivationCtorStyle.XamarinAndroid,
+ };
+ return peer;
+ }
+
+ static JavaPeerInfo MakeAcwPeer (string jniName, string managedName, string asmName)
+ {
+ var peer = MakePeerWithActivation (jniName, managedName, asmName);
+ peer.DoNotGenerateAcw = false;
+ // Add a constructor so it qualifies as ACW
+ peer.JavaConstructors = new List {
+ new JavaConstructorInfo { ConstructorIndex = 0, JniSignature = "()V" },
+ };
+ // Need at least 1 marshal method to be ACW
+ peer.MarshalMethods = new List {
+ new MarshalMethodInfo {
+ JniName = "