diff --git a/Xamarin.Android.sln b/Xamarin.Android.sln index d1edbe95c97..48ee13d6a66 100644 --- a/Xamarin.Android.sln +++ b/Xamarin.Android.sln @@ -59,6 +59,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Xamarin.ProjectTools", "src EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Xamarin.Android.Build.Tests", "src\Xamarin.Android.Build.Tasks\Tests\Xamarin.Android.Build.Tests\Xamarin.Android.Build.Tests.csproj", "{53E4ABF0-1085-45F9-B964-DCAE4B819998}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Android.Sdk.TrimmableTypeMap", "src\Microsoft.Android.Sdk.TrimmableTypeMap\Microsoft.Android.Sdk.TrimmableTypeMap.csproj", "{507759AE-93DF-411B-8645-31F680319F5C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Android.Sdk.TrimmableTypeMap.Tests", "tests\Microsoft.Android.Sdk.TrimmableTypeMap.Tests\Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj", "{F9CD012E-67AC-4A4E-B2A7-252387F91256}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestFixtures", "tests\Microsoft.Android.Sdk.TrimmableTypeMap.Tests\TestFixtures\TestFixtures.csproj", "{C5A44686-3469-45A7-B6AB-2798BA0625BC}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests", "tests\Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests\Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests.csproj", "{A14CB0A1-7A05-4F27-88B2-383798CE1DEE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UserTypesFixture", "tests\Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests\UserTypesFixture\UserTypesFixture.csproj", "{2498F8A0-AA04-40EF-8691-59BBD2396B4D}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "class-parse", "external\Java.Interop\tools\class-parse\class-parse.csproj", "{38C762AB-8FD1-44DE-9855-26AAE7129DC3}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "logcat-parse", "external\Java.Interop\tools\logcat-parse\logcat-parse.csproj", "{7387E151-48E3-4885-B2CA-A74434A34045}" @@ -231,6 +241,26 @@ Global {53E4ABF0-1085-45F9-B964-DCAE4B819998}.Debug|AnyCPU.Build.0 = Debug|Any CPU {53E4ABF0-1085-45F9-B964-DCAE4B819998}.Release|AnyCPU.ActiveCfg = Release|Any CPU {53E4ABF0-1085-45F9-B964-DCAE4B819998}.Release|AnyCPU.Build.0 = Release|Any CPU + {507759AE-93DF-411B-8645-31F680319F5C}.Debug|AnyCPU.ActiveCfg = Debug|Any CPU + {507759AE-93DF-411B-8645-31F680319F5C}.Debug|AnyCPU.Build.0 = Debug|Any CPU + {507759AE-93DF-411B-8645-31F680319F5C}.Release|AnyCPU.ActiveCfg = Release|Any CPU + {507759AE-93DF-411B-8645-31F680319F5C}.Release|AnyCPU.Build.0 = Release|Any CPU + {F9CD012E-67AC-4A4E-B2A7-252387F91256}.Debug|AnyCPU.ActiveCfg = Debug|Any CPU + {F9CD012E-67AC-4A4E-B2A7-252387F91256}.Debug|AnyCPU.Build.0 = Debug|Any CPU + {F9CD012E-67AC-4A4E-B2A7-252387F91256}.Release|AnyCPU.ActiveCfg = Release|Any CPU + {F9CD012E-67AC-4A4E-B2A7-252387F91256}.Release|AnyCPU.Build.0 = Release|Any CPU + {C5A44686-3469-45A7-B6AB-2798BA0625BC}.Debug|AnyCPU.ActiveCfg = Debug|Any CPU + {C5A44686-3469-45A7-B6AB-2798BA0625BC}.Debug|AnyCPU.Build.0 = Debug|Any CPU + {C5A44686-3469-45A7-B6AB-2798BA0625BC}.Release|AnyCPU.ActiveCfg = Release|Any CPU + {C5A44686-3469-45A7-B6AB-2798BA0625BC}.Release|AnyCPU.Build.0 = Release|Any CPU + {A14CB0A1-7A05-4F27-88B2-383798CE1DEE}.Debug|AnyCPU.ActiveCfg = Debug|Any CPU + {A14CB0A1-7A05-4F27-88B2-383798CE1DEE}.Debug|AnyCPU.Build.0 = Debug|Any CPU + {A14CB0A1-7A05-4F27-88B2-383798CE1DEE}.Release|AnyCPU.ActiveCfg = Release|Any CPU + {A14CB0A1-7A05-4F27-88B2-383798CE1DEE}.Release|AnyCPU.Build.0 = Release|Any CPU + {2498F8A0-AA04-40EF-8691-59BBD2396B4D}.Debug|AnyCPU.ActiveCfg = Debug|Any CPU + {2498F8A0-AA04-40EF-8691-59BBD2396B4D}.Debug|AnyCPU.Build.0 = Debug|Any CPU + {2498F8A0-AA04-40EF-8691-59BBD2396B4D}.Release|AnyCPU.ActiveCfg = Release|Any CPU + {2498F8A0-AA04-40EF-8691-59BBD2396B4D}.Release|AnyCPU.Build.0 = Release|Any CPU {38C762AB-8FD1-44DE-9855-26AAE7129DC3}.Debug|AnyCPU.ActiveCfg = Debug|Any CPU {38C762AB-8FD1-44DE-9855-26AAE7129DC3}.Debug|AnyCPU.Build.0 = Debug|Any CPU {38C762AB-8FD1-44DE-9855-26AAE7129DC3}.Release|AnyCPU.ActiveCfg = Release|Any CPU @@ -398,6 +428,10 @@ Global {645E1718-C8C4-4C23-8A49-5A37E4ECF7ED} = {04E3E11E-B47D-4599-8AFC-50515A95E715} {2DD1EE75-6D8D-4653-A800-0A24367F7F38} = {CAB438D8-B0F5-4AF0-BEBD-9E2ADBD7B483} {53E4ABF0-1085-45F9-B964-DCAE4B819998} = {CAB438D8-B0F5-4AF0-BEBD-9E2ADBD7B483} + {F9CD012E-67AC-4A4E-B2A7-252387F91256} = {CAB438D8-B0F5-4AF0-BEBD-9E2ADBD7B483} + {C5A44686-3469-45A7-B6AB-2798BA0625BC} = {CAB438D8-B0F5-4AF0-BEBD-9E2ADBD7B483} + {A14CB0A1-7A05-4F27-88B2-383798CE1DEE} = {CAB438D8-B0F5-4AF0-BEBD-9E2ADBD7B483} + {2498F8A0-AA04-40EF-8691-59BBD2396B4D} = {CAB438D8-B0F5-4AF0-BEBD-9E2ADBD7B483} {38C762AB-8FD1-44DE-9855-26AAE7129DC3} = {864062D3-A415-4A6F-9324-5820237BA058} {7387E151-48E3-4885-B2CA-A74434A34045} = {864062D3-A415-4A6F-9324-5820237BA058} {8A6CB07C-E493-4A4F-AB94-038645A27118} = {E351F97D-EA4F-4E7F-AAA0-8EBB1F2A4A62} diff --git a/build-tools/automation/yaml-templates/build-windows-steps.yaml b/build-tools/automation/yaml-templates/build-windows-steps.yaml index 6d3d6738ad2..f7cd09ce3b3 100644 --- a/build-tools/automation/yaml-templates/build-windows-steps.yaml +++ b/build-tools/automation/yaml-templates/build-windows-steps.yaml @@ -77,6 +77,36 @@ steps: testRunTitle: Microsoft.Android.Sdk.Analysis.Tests continueOnError: true +- template: /build-tools/automation/yaml-templates/run-dotnet-preview.yaml@self + parameters: + command: test + project: tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj + arguments: -c $(XA.Build.Configuration) --logger trx --results-directory $(Agent.TempDirectory)/trimmable-typemap-tests + displayName: Test Microsoft.Android.Sdk.TrimmableTypeMap.Tests $(XA.Build.Configuration) + +- task: PublishTestResults@2 + displayName: publish Microsoft.Android.Sdk.TrimmableTypeMap.Tests results + condition: always() + inputs: + testResultsFormat: VSTest + testResultsFiles: "$(Agent.TempDirectory)/trimmable-typemap-tests/*.trx" + testRunTitle: Microsoft.Android.Sdk.TrimmableTypeMap.Tests + +- template: /build-tools/automation/yaml-templates/run-dotnet-preview.yaml@self + parameters: + command: test + project: bin/Test$(XA.Build.Configuration)/$(DotNetTargetFramework)/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests.dll + arguments: --logger trx --results-directory $(Agent.TempDirectory)/trimmable-typemap-integration-tests + displayName: Test Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests $(XA.Build.Configuration) + +- task: PublishTestResults@2 + displayName: publish Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests results + condition: always() + inputs: + testResultsFormat: VSTest + testResultsFiles: "$(Agent.TempDirectory)/trimmable-typemap-integration-tests/*.trx" + testRunTitle: Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests + - task: BatchScript@1 displayName: Test dotnet-local.cmd - create template inputs: diff --git a/eng/Versions.props b/eng/Versions.props index d87b6a2fe34..bf1e2b58feb 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -16,6 +16,7 @@ 11.0.100-preview.1.26076.102 0.11.5-preview.26076.102 9.0.4 + 11.0.0-preview.1.26104.118 36.1.30 $(MicrosoftNETSdkAndroidManifest100100PackageVersion) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/AcwMapWriter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/AcwMapWriter.cs new file mode 100644 index 00000000000..d8f563df272 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/AcwMapWriter.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Generates acw-map.txt from JavaPeerInfo scanner results. +/// The acw-map is consumed by _ConvertCustomView to fix up custom view names in layout XML. +/// +/// Each type produces 3 lines: PartialAssemblyQualifiedName;JavaKey, ManagedKey;JavaKey, CompatJniName;JavaKey. +/// Types with DoNotGenerateAcw = true are excluded (MCW binding types). +/// +static class AcwMapWriter +{ + /// + /// Creates per-assembly acw-map entries for a single assembly's scan results. + /// + public static List CreateEntries (IReadOnlyList peers, string assemblyName) + { + var entries = new List (); + + foreach (var peer in peers) { + if (peer.DoNotGenerateAcw) + continue; + + if (peer.AssemblyName != assemblyName) + continue; + + var javaKey = peer.JavaName.Replace ('/', '.'); + var managedKey = peer.ManagedTypeName; + var partialAssemblyQualifiedName = $"{peer.ManagedTypeName}, {peer.AssemblyName}"; + // Compat JNI name uses the same format for now + var compatJniName = javaKey; + + entries.Add (new AcwMapEntry { + JavaKey = javaKey, + ManagedKey = managedKey, + PartialAssemblyQualifiedName = partialAssemblyQualifiedName, + CompatJniName = compatJniName, + AssemblyName = peer.AssemblyName, + }); + } + + return entries; + } + + /// + /// Writes acw-map.txt, detecting XA4214 (duplicate managed key) and XA4215 (duplicate Java key). + /// + public static AcwMapResult WriteMap (IReadOnlyList entries, TextWriter writer) + { + var managed = new Dictionary (entries.Count, StringComparer.Ordinal); + var java = new Dictionary (entries.Count, StringComparer.Ordinal); + var managedConflicts = new Dictionary> (0, StringComparer.Ordinal); + var javaConflicts = new Dictionary> (0, StringComparer.Ordinal); + + foreach (var entry in entries.OrderBy (e => e.ManagedKey, StringComparer.Ordinal)) { + writer.Write (entry.PartialAssemblyQualifiedName); + writer.Write (';'); + writer.WriteLine (entry.JavaKey); + + bool hasConflict = false; + + if (managed.TryGetValue (entry.ManagedKey, out var managedConflict)) { + if (!managedConflict.AssemblyName.Equals (entry.AssemblyName, StringComparison.Ordinal)) { + if (!managedConflicts.TryGetValue (entry.ManagedKey, out var list)) + managedConflicts.Add (entry.ManagedKey, list = new List { managedConflict.AssemblyName }); + list.Add (entry.AssemblyName); + } + hasConflict = true; + } + + if (java.TryGetValue (entry.JavaKey, out var javaConflict)) { + if (!javaConflict.AssemblyName.Equals (entry.AssemblyName, StringComparison.Ordinal)) { + if (!javaConflicts.TryGetValue (entry.JavaKey, out var list)) + javaConflicts.Add (entry.JavaKey, list = new List { javaConflict.PartialAssemblyQualifiedName }); + list.Add (entry.PartialAssemblyQualifiedName); + } + hasConflict = true; + } + + if (!hasConflict) { + managed.Add (entry.ManagedKey, entry); + java.Add (entry.JavaKey, entry); + + writer.Write (entry.ManagedKey); + writer.Write (';'); + writer.WriteLine (entry.JavaKey); + + writer.Write (entry.CompatJniName); + writer.Write (';'); + writer.WriteLine (entry.JavaKey); + } + } + + return new AcwMapResult { + ManagedConflicts = managedConflicts, + JavaConflicts = javaConflicts, + }; + } + + /// + /// Writes acw-map.txt to a file, only if content changed. + /// + public static AcwMapResult WriteMapToFile (IReadOnlyList entries, string outputPath) + { + using var sw = new StringWriter (); + var result = WriteMap (entries, sw); + + if (result.JavaConflicts.Count > 0) + return result; + + var content = sw.ToString (); + WriteIfChanged (outputPath, content); + return result; + } + + static void WriteIfChanged (string path, string content) + { + if (File.Exists (path)) { + var existing = File.ReadAllText (path); + if (string.Equals (existing, content, StringComparison.Ordinal)) + return; + } + + var dir = Path.GetDirectoryName (path); + if (!string.IsNullOrEmpty (dir) && !Directory.Exists (dir)) + Directory.CreateDirectory (dir); + + File.WriteAllText (path, content); + } +} + +sealed class AcwMapEntry +{ + public string JavaKey { get; set; } = ""; + public string ManagedKey { get; set; } = ""; + public string PartialAssemblyQualifiedName { get; set; } = ""; + public string CompatJniName { get; set; } = ""; + public string AssemblyName { get; set; } = ""; +} + +sealed class AcwMapResult +{ + public Dictionary> ManagedConflicts { get; set; } = new (); + public Dictionary> JavaConflicts { get; set; } = new (); + public bool HasErrors => JavaConflicts.Count > 0; + public bool HasWarnings => ManagedConflicts.Count > 0; +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/CompilerFeaturePolyfills.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/CompilerFeaturePolyfills.cs new file mode 100644 index 00000000000..c33ab5025c2 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/CompilerFeaturePolyfills.cs @@ -0,0 +1,23 @@ +// Polyfills for C# language features on netstandard2.0 + +// Required for init-only setters +namespace System.Runtime.CompilerServices +{ + static class IsExternalInit { } + + [AttributeUsage (AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] + sealed class RequiredMemberAttribute : Attribute { } + + [AttributeUsage (AttributeTargets.All, AllowMultiple = true, Inherited = false)] + sealed class CompilerFeatureRequiredAttribute (string featureName) : Attribute + { + public string FeatureName { get; } = featureName; + public bool IsOptional { get; init; } + } +} + +namespace System.Diagnostics.CodeAnalysis +{ + [AttributeUsage (AttributeTargets.Constructor, AllowMultiple = false, Inherited = false)] + sealed class SetsRequiredMembersAttribute : Attribute { } +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs new file mode 100644 index 00000000000..61e18460a82 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs @@ -0,0 +1,370 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Generates JCW (Java Callable Wrapper) .java source files from scanned records. +/// Only processes ACW types (where is false). +/// +sealed class JcwJavaSourceGenerator +{ + /// + /// Generates .java source files for all ACW types and writes them to the output directory. + /// Returns the list of generated file paths. + /// + public IReadOnlyList Generate (IReadOnlyList types, string outputDirectory) + { + if (types is null) { + throw new ArgumentNullException (nameof (types)); + } + if (outputDirectory is null) { + throw new ArgumentNullException (nameof (outputDirectory)); + } + + var generatedFiles = new List (); + + foreach (var type in types) { + if (type.DoNotGenerateAcw) { + continue; + } + + string filePath = GetOutputFilePath (type, outputDirectory); + string? dir = Path.GetDirectoryName (filePath); + if (dir != null) { + Directory.CreateDirectory (dir); + } + + using var writer = new StreamWriter (filePath); + Generate (type, writer); + generatedFiles.Add (filePath); + } + + return generatedFiles; + } + + /// + /// Generates a single .java source file for the given type. + /// + internal void Generate (JavaPeerInfo type, TextWriter writer) + { + WritePackageDeclaration (type, writer); + WriteClassDeclaration (type, writer); + WriteStaticInitializer (type, writer); + WriteExportFields (type, writer); + WriteConstructors (type, writer); + WriteMethods (type, writer); + WriteClassClose (writer); + } + + static string GetOutputFilePath (JavaPeerInfo type, string outputDirectory) + { + // JNI name uses '/' as separator and '$' for nested types + // e.g., "com/example/MainActivity" → "com/example/MainActivity.java" + // Nested types: "com/example/Outer$Inner" → "com/example/Outer$Inner.java" (same file convention) + string relativePath = type.JavaName + ".java"; + return Path.Combine (outputDirectory, relativePath); + } + + static void WritePackageDeclaration (JavaPeerInfo type, TextWriter writer) + { + string? package = GetJavaPackageName (type.JavaName); + if (package != null) { + writer.Write ("package "); + writer.Write (package); + writer.WriteLine (';'); + writer.WriteLine (); + } + } + + static void WriteClassDeclaration (JavaPeerInfo type, TextWriter writer) + { + writer.Write ("public "); + if (type.IsAbstract && !type.IsInterface) { + writer.Write ("abstract "); + } + writer.Write ("class "); + writer.WriteLine (GetJavaSimpleName (type.JavaName)); + + // extends clause + string? baseJavaType = type.BaseJavaName != null ? JniNameToJavaName (type.BaseJavaName) : null; + if (baseJavaType != null) { + writer.Write ("\textends "); + writer.WriteLine (baseJavaType); + } + + // implements clause — always includes IGCUserPeer, plus any implemented interfaces + writer.Write ("\timplements"); + writer.Write ("\n\t\tmono.android.IGCUserPeer"); + + foreach (var iface in type.ImplementedInterfaceJavaNames) { + writer.Write (",\n\t\t"); + writer.Write (JniNameToJavaName (iface)); + } + + writer.WriteLine (); + writer.WriteLine ('{'); + } + + static void WriteStaticInitializer (JavaPeerInfo type, TextWriter writer) + { + writer.Write ("\tstatic {\n"); + writer.Write ("\t\tmono.android.Runtime.registerNatives ("); + writer.Write (GetJavaSimpleName (type.JavaName)); + writer.Write (".class);\n"); + writer.Write ("\t}\n"); + writer.WriteLine (); + } + + static void WriteExportFields (JavaPeerInfo type, TextWriter writer) + { + foreach (var field in type.ExportFields) { + string javaType = JniTypeToJava (field.JniReturnType); + + writer.Write ("\tpublic "); + if (field.IsStatic) { + writer.Write ("static "); + } + writer.Write (javaType); + writer.Write (' '); + writer.Write (field.FieldName); + writer.Write (" = "); + writer.Write (field.MethodName); + writer.WriteLine (" ();"); + } + + if (type.ExportFields.Count > 0) { + writer.WriteLine (); + } + } + + static void WriteConstructors (JavaPeerInfo type, TextWriter writer) + { + string simpleClassName = GetJavaSimpleName (type.JavaName); + + foreach (var ctor in type.JavaConstructors) { + // Constructor signature + writer.Write ("\tpublic "); + writer.Write (simpleClassName); + writer.Write (" ("); + WriteParameterList (ctor.Parameters, writer); + writer.Write (")\n"); + + WriteThrowsClause (ctor.IsExport ? ctor.ThrownNames : null, writer); + + writer.WriteLine ("\t{"); + + // super() call — use SuperArgumentsString if provided ([Export] constructors), + // otherwise forward all constructor parameters. + writer.Write ("\t\tsuper ("); + if (ctor.SuperArgumentsString != null) { + writer.Write (ctor.SuperArgumentsString); + } else { + WriteArgumentList (ctor.Parameters, writer); + } + writer.WriteLine (");"); + + // Activation guard: only activate if this is the exact class + writer.Write ("\t\tif (getClass () == "); + writer.Write (simpleClassName); + writer.Write (".class) "); + + // Both [Register] and [Export] constructors use native nctor_N methods. + // The .NET side generates a UCO wrapper with the full marshal body. + writer.Write ("nctor_"); + writer.Write (ctor.ConstructorIndex); + writer.Write (" ("); + WriteArgumentList (ctor.Parameters, writer); + writer.Write (')'); + writer.WriteLine (";"); + + writer.WriteLine ("\t}"); + writer.WriteLine (); + } + + // Write native constructor declarations + foreach (var ctor in type.JavaConstructors) { + writer.Write ("\tprivate native void nctor_"); + writer.Write (ctor.ConstructorIndex); + writer.Write (" ("); + WriteParameterList (ctor.Parameters, writer); + writer.WriteLine (");"); + } + + if (type.JavaConstructors.Count > 0) { + writer.WriteLine (); + } + } + + static void WriteMethods (JavaPeerInfo type, TextWriter writer) + { + foreach (var method in type.MarshalMethods) { + if (method.IsConstructor) { + continue; + } + + string javaReturnType = JniTypeToJava (method.JniReturnType); + bool isVoid = method.JniReturnType == "V"; + bool isExport = method.Connector == null; + + // Public wrapper method + if (!isExport) { + writer.Write ("\t@Override\n"); + } + writer.Write ("\tpublic "); + if (method.IsStatic) { + writer.Write ("static "); + } + writer.Write (javaReturnType); + writer.Write (' '); + writer.Write (method.JniName); + writer.Write (" ("); + WriteParameterList (method.Parameters, writer); + writer.Write (")\n"); + + WriteThrowsClause (method.ThrownNames, writer); + + writer.Write ("\t{\n"); + + // Delegate to native method + writer.Write ("\t\t"); + if (!isVoid) { + writer.Write ("return "); + } + writer.Write (method.NativeCallbackName); + writer.Write (" ("); + WriteArgumentList (method.Parameters, writer); + writer.Write (");\n"); + + writer.Write ("\t}\n"); + + // Native method declaration + writer.Write ("\tprivate "); + if (method.IsStatic) { + writer.Write ("static "); + } + writer.Write ("native "); + writer.Write (javaReturnType); + writer.Write (' '); + writer.Write (method.NativeCallbackName); + writer.Write (" ("); + WriteParameterList (method.Parameters, writer); + writer.Write (");\n"); + + writer.WriteLine (); + } + } + + static void WriteClassClose (TextWriter writer) + { + writer.WriteLine ('}'); + } + + static void WriteParameterList (IReadOnlyList parameters, TextWriter writer) + { + for (int i = 0; i < parameters.Count; i++) { + if (i > 0) { + writer.Write (", "); + } + writer.Write (JniTypeToJava (parameters [i].JniType)); + writer.Write (" p"); + writer.Write (i); + } + } + + static void WriteArgumentList (IReadOnlyList parameters, TextWriter writer) + { + for (int i = 0; i < parameters.Count; i++) { + if (i > 0) { + writer.Write (", "); + } + writer.Write ('p'); + writer.Write (i); + } + } + + static void WriteThrowsClause (IReadOnlyList? thrownNames, TextWriter writer) + { + if (thrownNames == null || thrownNames.Count == 0) { + return; + } + + writer.Write ("\t\tthrows "); + for (int i = 0; i < thrownNames.Count; i++) { + if (i > 0) { + writer.Write (", "); + } + writer.Write (thrownNames [i]); + } + writer.Write ('\n'); + } + + /// + /// Converts a JNI type name to a Java source type name. + /// e.g., "android/app/Activity" → "android.app.Activity" + /// + internal static string JniNameToJavaName (string jniName) + { + return jniName.Replace ('/', '.'); + } + + /// + /// Extracts the Java package name from a JNI type name. + /// e.g., "com/example/MainActivity" → "com.example" + /// Returns null for types without a package. + /// + internal static string? GetJavaPackageName (string jniName) + { + int lastSlash = jniName.LastIndexOf ('/'); + if (lastSlash < 0) { + return null; + } + return jniName.Substring (0, lastSlash).Replace ('/', '.'); + } + + /// + /// Extracts the simple Java class name from a JNI type name. + /// e.g., "com/example/MainActivity" → "MainActivity" + /// e.g., "com/example/Outer$Inner" → "Outer$Inner" (preserves nesting separator) + /// + internal static string GetJavaSimpleName (string jniName) + { + int lastSlash = jniName.LastIndexOf ('/'); + return lastSlash >= 0 ? jniName.Substring (lastSlash + 1) : jniName; + } + + /// + /// Converts a JNI type descriptor to a Java source type. + /// e.g., "V" → "void", "I" → "int", "Landroid/os/Bundle;" → "android.os.Bundle" + /// + internal static string JniTypeToJava (string jniType) + { + if (jniType.Length == 1) { + return jniType [0] switch { + 'V' => "void", + 'Z' => "boolean", + 'B' => "byte", + 'C' => "char", + 'S' => "short", + 'I' => "int", + 'J' => "long", + 'F' => "float", + 'D' => "double", + _ => throw new ArgumentException ($"Unknown JNI primitive type: {jniType}"), + }; + } + + // Array types: "[I" → "int[]", "[Ljava/lang/String;" → "java.lang.String[]" + if (jniType [0] == '[') { + return JniTypeToJava (jniType.Substring (1)) + "[]"; + } + + // Object types: "Landroid/os/Bundle;" → "android.os.Bundle" + if (jniType [0] == 'L' && jniType [jniType.Length - 1] == ';') { + return JniNameToJavaName (jniType.Substring (1, jniType.Length - 2)); + } + + throw new ArgumentException ($"Unknown JNI type descriptor: {jniType}"); + } +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs new file mode 100644 index 00000000000..df672795274 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// JNI primitive type kinds used for mapping JNI signatures → CLR types. +enum JniParamKind +{ + Void, // V + Boolean, // Z → byte (JNI's jboolean is unsigned 8-bit) + Byte, // B → sbyte (JNI's jbyte is signed 8-bit) + Char, // C → char + Short, // S → short + Int, // I → int + Long, // J → long + Float, // F → float + Double, // D → double + Object, // L...; or [ → IntPtr +} + +/// Helpers for parsing JNI method signatures. +static class JniSignatureHelper +{ + /// Parses the parameter types from a JNI method signature like "(Landroid/os/Bundle;)V". + public static List ParseParameterTypes (string jniSignature) + { + var result = new List (); + int i = 1; // skip opening '(' + while (i < jniSignature.Length && jniSignature [i] != ')') { + result.Add (ParseSingleType (jniSignature, ref i)); + } + return result; + } + + /// Parses the raw JNI type descriptor strings from a JNI method signature. + public static List ParseParameterTypeStrings (string jniSignature) + { + var result = new List (); + int i = 1; // skip opening '(' + while (i < jniSignature.Length && jniSignature [i] != ')') { + int start = i; + SkipSingleType (jniSignature, ref i); + result.Add (jniSignature.Substring (start, i - start)); + } + return result; + } + + /// Extracts the return type descriptor from a JNI method signature. + public static string ParseReturnTypeString (string jniSignature) + { + int i = jniSignature.IndexOf (')') + 1; + return jniSignature.Substring (i); + } + + /// Parses the return type from a JNI method signature. + public static JniParamKind ParseReturnType (string jniSignature) + { + int i = jniSignature.IndexOf (')') + 1; + return ParseSingleType (jniSignature, ref i); + } + + static JniParamKind ParseSingleType (string sig, ref int i) + { + switch (sig [i]) { + case 'V': i++; return JniParamKind.Void; + case 'Z': i++; return JniParamKind.Boolean; + case 'B': i++; return JniParamKind.Byte; + case 'C': i++; return JniParamKind.Char; + case 'S': i++; return JniParamKind.Short; + case 'I': i++; return JniParamKind.Int; + case 'J': i++; return JniParamKind.Long; + case 'F': i++; return JniParamKind.Float; + case 'D': i++; return JniParamKind.Double; + case 'L': + i = sig.IndexOf (';', i) + 1; + return JniParamKind.Object; + case '[': + i++; + ParseSingleType (sig, ref i); // skip element type + return JniParamKind.Object; + default: + throw new ArgumentException ($"Unknown JNI type character '{sig [i]}' in '{sig}' at index {i}"); + } + } + + /// Parses a standalone JNI type descriptor like "I", "Ljava/lang/String;", "[B". + public static JniParamKind ParseSingleTypeFromDescriptor (string descriptor) + { + int i = 0; + return ParseSingleType (descriptor, ref i); + } + + static void SkipSingleType (string sig, ref int i) + { + switch (sig [i]) { + case 'V': case 'Z': case 'B': case 'C': case 'S': + case 'I': case 'J': case 'F': case 'D': + i++; + break; + case 'L': + i = sig.IndexOf (';', i) + 1; + break; + case '[': + i++; + SkipSingleType (sig, ref i); + break; + default: + throw new ArgumentException ($"Unknown JNI type character '{sig [i]}' in '{sig}' at index {i}"); + } + } + + /// Encodes the CLR type for a JNI parameter kind into a signature type encoder. + public static void EncodeClrType (SignatureTypeEncoder encoder, JniParamKind kind) + { + switch (kind) { + case JniParamKind.Boolean: encoder.Byte (); break; + case JniParamKind.Byte: encoder.SByte (); break; + case JniParamKind.Char: encoder.Char (); break; + case JniParamKind.Short: encoder.Int16 (); break; + case JniParamKind.Int: encoder.Int32 (); break; + case JniParamKind.Long: encoder.Int64 (); break; + case JniParamKind.Float: encoder.Single (); break; + case JniParamKind.Double: encoder.Double (); break; + case JniParamKind.Object: encoder.IntPtr (); break; + default: throw new ArgumentException ($"Cannot encode JNI param kind {kind} as CLR type"); + } + } +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs new file mode 100644 index 00000000000..bc9ca36efd3 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -0,0 +1,231 @@ +using System; +using System.Collections.Generic; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Data model for a single TypeMap output assembly. +/// Describes what to emit — the emitter writes this directly into a PE assembly. +/// Built by , consumed by . +/// +sealed class TypeMapAssemblyData +{ + /// Assembly name (e.g., "_MyApp.TypeMap"). + public string AssemblyName { get; set; } = ""; + + /// Module file name (e.g., "_MyApp.TypeMap.dll"). + public string ModuleName { get; set; } = ""; + + /// TypeMap entries — one per unique JNI name. + public List Entries { get; } = new (); + + /// Proxy types to emit in the assembly. + public List ProxyTypes { get; } = new (); + + /// TypeMapAssociation entries for alias groups (multiple managed types → same JNI name). + public List Associations { get; } = new (); + + /// Assembly names that need [IgnoresAccessChecksTo] for cross-assembly n_* calls. + public List IgnoresAccessChecksTo { get; } = new (); +} + +/// +/// One [assembly: TypeMap("jni/name", typeof(Proxy))] or +/// [assembly: TypeMap("jni/name", typeof(Proxy), typeof(Target))] entry. +/// +/// 2-arg (unconditional): proxy is always preserved — used for ACW types and essential runtime types. +/// 3-arg (trimmable): proxy is preserved only if Target type is referenced by the app. +/// +sealed class TypeMapAttributeData +{ + /// JNI type name, e.g., "android/app/Activity". + public string JniName { get; set; } = ""; + + /// + /// Assembly-qualified proxy type reference string. + /// Either points to a generated proxy or to the original managed type. + /// + public string ProxyTypeReference { get; set; } = ""; + + /// + /// Assembly-qualified target type reference for the trimmable (3-arg) variant. + /// Null for unconditional (2-arg) entries. + /// The trimmer preserves the proxy only if this target type is used by the app. + /// + public string? TargetTypeReference { get; set; } + + /// True for 2-arg unconditional entries (ACW types, essential runtime types). + public bool IsUnconditional => TargetTypeReference == null; +} + +/// +/// A proxy type to generate in the TypeMap assembly (subclass of JavaPeerProxy). +/// +sealed class JavaPeerProxyData +{ + /// Simple type name, e.g., "Java_Lang_Object_Proxy". + public string TypeName { get; set; } = ""; + + /// Namespace for all proxy types. + public string Namespace { get; set; } = "_TypeMap.Proxies"; + + /// Reference to the managed type this proxy wraps (for ldtoken in TargetType property). + public TypeRefData TargetType { get; set; } = new (); + + /// Reference to the invoker type (for interfaces/abstract types). Null if not applicable. + public TypeRefData? InvokerType { get; set; } + + /// Whether this proxy has a CreateInstance that can actually create instances. + public bool HasActivation => ActivationCtor != null || InvokerType != null; + + /// + /// Activation constructor details. Determines how CreateInstance instantiates the managed peer. + /// + public ActivationCtorData? ActivationCtor { get; set; } + + /// True if this is an open generic type definition. CreateInstance throws NotSupportedException. + public bool IsGenericDefinition { get; set; } + + /// Whether this proxy needs ACW support (RegisterNatives + UCO wrappers + IAndroidCallableWrapper). + public bool IsAcw { get; set; } + + /// UCO method wrappers for [Register] methods and constructors. + public List UcoMethods { get; } = new (); + + /// Export marshal method wrappers — full marshal body for [Export] methods and constructors. + public List ExportMarshalMethods { get; } = new (); + + /// RegisterNatives registrations (method name, JNI signature, wrapper name). + public List NativeRegistrations { get; } = new (); +} + +/// +/// A cross-assembly type reference (assembly name + full managed type name). +/// +sealed class TypeRefData +{ + /// Full managed type name, e.g., "Android.App.Activity" or "MyApp.Outer+Inner". + public string ManagedTypeName { get; set; } = ""; + + /// Assembly containing the type, e.g., "Mono.Android". + public string AssemblyName { get; set; } = ""; +} + +/// +/// An [UnmanagedCallersOnly] static wrapper for a marshal method. +/// Body: load all args → call n_* callback → ret. +/// +sealed class UcoMethodData +{ + /// Name of the generated wrapper method, e.g., "n_onCreate_uco_0". + public string WrapperName { get; set; } = ""; + + /// Name of the n_* callback to call, e.g., "n_OnCreate". + public string CallbackMethodName { get; set; } = ""; + + /// Type containing the callback method. + public TypeRefData CallbackType { get; set; } = new (); + + /// JNI method signature, e.g., "(Landroid/os/Bundle;)V". Used to determine CLR parameter types. + public string JniSignature { get; set; } = ""; +} + +/// +/// An [UnmanagedCallersOnly] static wrapper for an [Export] method or constructor. +/// Unlike which just forwards to an existing n_* callback, +/// this generates the full marshal method body: BeginMarshalMethod, GetObject, param +/// unmarshaling, managed method call, return marshaling, exception handling, EndMarshalMethod. +/// +sealed class ExportMarshalMethodData +{ + /// Name of the generated wrapper method, e.g., "n_myMethod_uco_0" or "nctor_0_uco". + public string WrapperName { get; set; } = ""; + + /// + /// JNI method name for RegisterNatives, e.g., "n_DoWork" or "nctor_0". + /// Must match the native method declaration in the Java JCW. + /// + public string NativeCallbackName { get; set; } = ""; + + /// Name of the managed method to call, e.g., "MyMethod" or ".ctor". + public string ManagedMethodName { get; set; } = ""; + + /// Type containing the managed method (the user's type). + public TypeRefData DeclaringType { get; set; } = new (); + + /// JNI method signature, e.g., "(Ljava/lang/String;I)V". + public string JniSignature { get; set; } = ""; + + /// True if this is a constructor. + public bool IsConstructor { get; set; } + + /// True if this is a static method. + public bool IsStatic { get; set; } + + /// + /// Managed parameter types for the managed method call. + /// Each entry is the assembly-qualified managed type name. + /// + public List ManagedParameters { get; } = new (); + + /// Managed return type (assembly-qualified). Null/empty for void or constructors. + public string? ManagedReturnType { get; set; } +} + +/// +/// Describes a parameter for an [Export] marshal method, with both JNI and managed type info. +/// +sealed class ExportParamData +{ + /// JNI type descriptor, e.g., "Ljava/lang/String;", "I". + public string JniType { get; set; } = ""; + + /// Managed type name (assembly-qualified), e.g., "System.String, System.Private.CoreLib". + public string ManagedTypeName { get; set; } = ""; + + /// Assembly containing the managed type. + public string AssemblyName { get; set; } = ""; +} + +/// +/// One JNI native method registration in RegisterNatives. +/// +sealed class NativeRegistrationData +{ + /// JNI method name to register, e.g., "n_onCreate" or "nctor_0". + public string JniMethodName { get; set; } = ""; + + /// JNI method signature, e.g., "(Landroid/os/Bundle;)V". + public string JniSignature { get; set; } = ""; + + /// Name of the UCO wrapper method whose function pointer to register. + public string WrapperMethodName { get; set; } = ""; +} + +/// +/// Describes how the proxy's CreateInstance should construct the managed peer. +/// +sealed class ActivationCtorData +{ + /// Type that declares the activation constructor (may be a base type). + public TypeRefData DeclaringType { get; set; } = new (); + + /// True when the leaf type itself declares the activation ctor. + public bool IsOnLeafType { get; set; } + + /// The style of activation ctor (XamarinAndroid or JavaInterop). + public ActivationCtorStyle Style { get; set; } +} + +/// +/// One [assembly: TypeMapAssociation(typeof(Source), typeof(AliasProxy))] entry. +/// Links a managed type to the proxy that holds its alias TypeMap entry. +/// +sealed class TypeMapAssociationData +{ + /// Assembly-qualified source type reference (the managed alias type). + public string SourceTypeReference { get; set; } = ""; + + /// Assembly-qualified proxy type reference (the alias holder proxy). + public string AliasProxyTypeReference { get; set; } = ""; +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs new file mode 100644 index 00000000000..2264f7ec756 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -0,0 +1,380 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Builds a from scanned records. +/// All decision logic (deduplication, alias detection, ACW filtering, 2-arg vs 3-arg attribute +/// selection, callback resolution, proxy naming) lives here. +/// The output model is a plain data structure that the emitter writes directly into a PE assembly. +/// +static class ModelBuilder +{ + static readonly HashSet EssentialRuntimeTypes = new (StringComparer.Ordinal) { + "java/lang/Object", + "java/lang/Class", + "java/lang/String", + "java/lang/Throwable", + "java/lang/Exception", + "java/lang/RuntimeException", + "java/lang/Error", + "java/lang/Thread", + }; + + /// + /// Builds a TypeMap assembly model for the given peers. + /// + /// Scanned Java peer types (typically from a single input assembly). + /// Output .dll path — used to derive assembly/module names if not specified. + /// Explicit assembly name. If null, derived from . + public static TypeMapAssemblyData Build (IReadOnlyList peers, string outputPath, string? assemblyName = null) + { + if (peers is null) { + throw new ArgumentNullException (nameof (peers)); + } + if (outputPath is null) { + throw new ArgumentNullException (nameof (outputPath)); + } + + assemblyName ??= Path.GetFileNameWithoutExtension (outputPath); + string moduleName = Path.GetFileName (outputPath); + + var model = new TypeMapAssemblyData { + AssemblyName = assemblyName, + ModuleName = moduleName, + }; + + // Invoker types are NOT emitted as separate proxies or TypeMap entries — + // they only appear as a TypeRef in the interface proxy's get_InvokerType property. + var invokerTypeNames = new HashSet ( + peers.Where (p => p.InvokerTypeName != null).Select (p => p.InvokerTypeName!), + StringComparer.Ordinal); + + // Group non-invoker peers by JNI name to detect aliases (multiple .NET types → same Java class). + // Use an ordered dictionary to ensure deterministic output across runs. + var groups = new SortedDictionary> (StringComparer.Ordinal); + foreach (var peer in peers) { + if (invokerTypeNames.Contains (peer.ManagedTypeName)) { + continue; + } + if (!groups.TryGetValue (peer.JavaName, out var list)) { + list = new List (); + groups [peer.JavaName] = list; + } + list.Add (peer); + } + + foreach (var kvp in groups) { + string jniName = kvp.Key; + var peersForName = kvp.Value; + + // Sort aliases by managed type name for deterministic proxy naming + if (peersForName.Count > 1) { + peersForName.Sort ((a, b) => StringComparer.Ordinal.Compare (a.ManagedTypeName, b.ManagedTypeName)); + } + + EmitPeers (model, jniName, peersForName, assemblyName); + } + + // Compute IgnoresAccessChecksTo from cross-assembly references + var referencedAssemblies = new SortedSet (StringComparer.Ordinal); + foreach (var proxy in model.ProxyTypes) { + AddIfCrossAssembly (referencedAssemblies, proxy.TargetType?.AssemblyName, assemblyName); + foreach (var uco in proxy.UcoMethods) { + AddIfCrossAssembly (referencedAssemblies, uco.CallbackType.AssemblyName, assemblyName); + } + foreach (var export in proxy.ExportMarshalMethods) { + AddIfCrossAssembly (referencedAssemblies, export.DeclaringType.AssemblyName, assemblyName); + } + // SetHandle is protected on Java.Lang.Object (Mono.Android) — needed for + // [Export] constructor marshal methods (nctor_N_uco) that call SetHandle directly. + // Inherited activation ctors call .ctor(IntPtr, JniHandleOwnership) which is public, + // but the base ctor's declaring assembly still needs IgnoresAccessChecksTo. + bool usesSetHandle = proxy.ExportMarshalMethods.Any (e => e.IsConstructor); + bool usesInheritedCtor = proxy.ActivationCtor != null && !proxy.ActivationCtor.IsOnLeafType; + if (usesSetHandle) { + AddIfCrossAssembly (referencedAssemblies, "Mono.Android", assemblyName); + } + if (usesInheritedCtor) { + AddIfCrossAssembly (referencedAssemblies, proxy.ActivationCtor!.DeclaringType.AssemblyName, assemblyName); + } + } + model.IgnoresAccessChecksTo.AddRange (referencedAssemblies); + + return model; + } + + static void EmitPeers (TypeMapAssemblyData model, string jniName, + List peersForName, string assemblyName) + { + // First peer is the "primary" — it gets the base JNI name entry. + // Remaining peers get indexed alias entries: "jni/name[1]", "jni/name[2]", ... + JavaPeerProxyData? primaryProxy = null; + for (int i = 0; i < peersForName.Count; i++) { + var peer = peersForName [i]; + string entryJniName = i == 0 ? jniName : $"{jniName}[{i}]"; + + bool isAcw = !peer.DoNotGenerateAcw && !peer.IsInterface && peer.MarshalMethods.Count > 0; + bool hasProxy = peer.ActivationCtor != null || peer.InvokerTypeName != null || isAcw; + + JavaPeerProxyData? proxy = null; + if (hasProxy) { + proxy = BuildProxyType (peer, isAcw); + model.ProxyTypes.Add (proxy); + } + + if (i == 0) { + primaryProxy = proxy; + } + + model.Entries.Add (BuildEntry (peer, proxy, assemblyName, entryJniName)); + + // Emit TypeMapAssociation linking alias types to the primary proxy + if (i > 0 && primaryProxy != null) { + model.Associations.Add (new TypeMapAssociationData { + SourceTypeReference = $"{peer.ManagedTypeName}, {peer.AssemblyName}", + AliasProxyTypeReference = $"{primaryProxy.Namespace}.{primaryProxy.TypeName}, {assemblyName}", + }); + } + } + } + + /// + /// Determines whether a type should use the unconditional (2-arg) TypeMap attribute. + /// Unconditional types are always preserved by the trimmer. + /// + static bool IsUnconditionalEntry (JavaPeerInfo peer) + { + // Essential runtime types needed by the Java interop runtime + if (EssentialRuntimeTypes.Contains (peer.JavaName)) { + return true; + } + + // Implementor/EventDispatcher types are only created from .NET (e.g., when a C# event + // is subscribed). They should NOT be unconditional — they're trimmable. + if (IsImplementorOrEventDispatcher (peer)) { + return false; + } + + // User-defined ACW types (not MCW bindings, not interfaces) are unconditional + // because Android can instantiate them from Java at any time. + if (!peer.DoNotGenerateAcw && !peer.IsInterface) { + return true; + } + + // Types marked unconditional by the scanner (component attributes: Activity, Service, etc.) + if (peer.IsUnconditional) { + return true; + } + + return false; + } + + /// + /// Implementor and EventDispatcher types are generated by the binding generator + /// and are only instantiated from .NET. They should be trimmable. + /// NOTE: This is a name-based heuristic. Ideally the scanner would provide a dedicated flag. + /// User types whose names happen to end in "Implementor" or "EventDispatcher" would be + /// misclassified as trimmable. This is acceptable for now since such naming in user code + /// is unlikely and would only affect trimming behavior, not correctness. + /// + static bool IsImplementorOrEventDispatcher (JavaPeerInfo peer) + { + return peer.ManagedTypeName.EndsWith ("Implementor", StringComparison.Ordinal) || + peer.ManagedTypeName.EndsWith ("EventDispatcher", StringComparison.Ordinal); + } + + static void AddIfCrossAssembly (SortedSet set, string? asmName, string outputAssemblyName) + { + if (asmName != null && !string.Equals (asmName, outputAssemblyName, StringComparison.Ordinal)) { + set.Add (asmName); + } + } + + static JavaPeerProxyData BuildProxyType (JavaPeerInfo peer, bool isAcw) + { + // Use managed type name for proxy naming to guarantee uniqueness across aliases + // (two types with the same JNI name will have different managed names). + var proxyTypeName = peer.ManagedTypeName.Replace ('.', '_').Replace ('+', '_') + "_Proxy"; + + var proxy = new JavaPeerProxyData { + TypeName = proxyTypeName, + TargetType = new TypeRefData { + ManagedTypeName = peer.ManagedTypeName, + AssemblyName = peer.AssemblyName, + }, + IsAcw = isAcw, + IsGenericDefinition = peer.IsGenericDefinition, + }; + + if (peer.InvokerTypeName != null) { + proxy.InvokerType = new TypeRefData { + ManagedTypeName = peer.InvokerTypeName, + AssemblyName = peer.AssemblyName, + }; + } + + if (peer.ActivationCtor != null) { + bool isOnLeaf = string.Equals (peer.ActivationCtor.DeclaringTypeName, peer.ManagedTypeName, StringComparison.Ordinal); + proxy.ActivationCtor = new ActivationCtorData { + DeclaringType = new TypeRefData { + ManagedTypeName = peer.ActivationCtor.DeclaringTypeName, + AssemblyName = peer.ActivationCtor.DeclaringAssemblyName, + }, + IsOnLeafType = isOnLeaf, + Style = peer.ActivationCtor.Style, + }; + } + + if (isAcw) { + BuildUcoMethods (peer, proxy); + BuildUcoConstructors (peer, proxy); + BuildNativeRegistrations (proxy); + } + + return proxy; + } + + static void BuildUcoMethods (JavaPeerInfo peer, JavaPeerProxyData proxy) + { + int ucoIndex = 0; + for (int i = 0; i < peer.MarshalMethods.Count; i++) { + var mm = peer.MarshalMethods [i]; + if (mm.IsConstructor) { + continue; + } + + string wrapperName = $"n_{mm.JniName}_uco_{ucoIndex}"; + + if (mm.Connector == null) { + // [Export] method — generate full marshal body + var exportData = BuildExportMarshalMethod (mm, peer, wrapperName, mm.NativeCallbackName, isConstructor: false); + proxy.ExportMarshalMethods.Add (exportData); + } else { + // [Register] method — forward to existing n_* callback + proxy.UcoMethods.Add (new UcoMethodData { + WrapperName = wrapperName, + CallbackMethodName = mm.NativeCallbackName, + CallbackType = new TypeRefData { + ManagedTypeName = !string.IsNullOrEmpty (mm.DeclaringTypeName) ? mm.DeclaringTypeName : peer.ManagedTypeName, + AssemblyName = !string.IsNullOrEmpty (mm.DeclaringAssemblyName) ? mm.DeclaringAssemblyName : peer.AssemblyName, + }, + JniSignature = mm.JniSignature, + }); + } + ucoIndex++; + } + } + + static void BuildUcoConstructors (JavaPeerInfo peer, JavaPeerProxyData proxy) + { + if (peer.ActivationCtor == null || peer.JavaConstructors.Count == 0) { + return; + } + + // Index marshal methods by JNI signature for lookup + var marshalMethodsBySignature = new Dictionary (StringComparer.Ordinal); + foreach (var mm in peer.MarshalMethods) { + if (mm.IsConstructor) { + marshalMethodsBySignature [mm.JniSignature] = mm; + } + } + + foreach (var ctor in peer.JavaConstructors) { + if (!marshalMethodsBySignature.TryGetValue (ctor.JniSignature, out var mm)) { + continue; + } + + string wrapperName = $"nctor_{ctor.ConstructorIndex}_uco"; + string nativeCallbackName = $"nctor_{ctor.ConstructorIndex}"; + + // ALL constructors need full marshal body generation — there are no + // pre-existing n_* callbacks for constructors (unlike [Register] methods). + // The [Register] connector for constructors is always "" (empty string). + var exportData = BuildExportMarshalMethod (mm, peer, wrapperName, nativeCallbackName, isConstructor: true); + proxy.ExportMarshalMethods.Add (exportData); + } + } + + static void BuildNativeRegistrations (JavaPeerProxyData proxy) + { + foreach (var uco in proxy.UcoMethods) { + proxy.NativeRegistrations.Add (new NativeRegistrationData { + JniMethodName = uco.CallbackMethodName, + JniSignature = uco.JniSignature, + WrapperMethodName = uco.WrapperName, + }); + } + + foreach (var export in proxy.ExportMarshalMethods) { + proxy.NativeRegistrations.Add (new NativeRegistrationData { + JniMethodName = export.NativeCallbackName, + JniSignature = export.JniSignature, + WrapperMethodName = export.WrapperName, + }); + } + } + + static ExportMarshalMethodData BuildExportMarshalMethod (MarshalMethodInfo mm, JavaPeerInfo peer, + string wrapperName, string nativeCallbackName, bool isConstructor) + { + var data = new ExportMarshalMethodData { + WrapperName = wrapperName, + NativeCallbackName = nativeCallbackName, + ManagedMethodName = mm.ManagedMethodName, + DeclaringType = new TypeRefData { + ManagedTypeName = peer.ManagedTypeName, + AssemblyName = peer.AssemblyName, + }, + JniSignature = mm.JniSignature, + IsConstructor = isConstructor, + IsStatic = mm.IsStatic, + ManagedReturnType = mm.ManagedReturnType, + }; + + foreach (var param in mm.Parameters) { + // Parse assembly name from assembly-qualified name "TypeName, AssemblyName" + string managedTypeName = param.ManagedType; + string assemblyName = peer.AssemblyName; + int commaIndex = managedTypeName.IndexOf (", ", StringComparison.Ordinal); + if (commaIndex >= 0) { + assemblyName = managedTypeName.Substring (commaIndex + 2); + managedTypeName = managedTypeName.Substring (0, commaIndex); + } + + data.ManagedParameters.Add (new ExportParamData { + JniType = param.JniType, + ManagedTypeName = managedTypeName, + AssemblyName = assemblyName, + }); + } + + return data; + } + + static TypeMapAttributeData BuildEntry (JavaPeerInfo peer, JavaPeerProxyData? proxy, + string outputAssemblyName, string jniName) + { + string proxyRef; + if (proxy != null) { + proxyRef = $"{proxy.Namespace}.{proxy.TypeName}, {outputAssemblyName}"; + } else { + proxyRef = $"{peer.ManagedTypeName}, {peer.AssemblyName}"; + } + + bool isUnconditional = IsUnconditionalEntry (peer); + string? targetRef = null; + if (!isUnconditional) { + targetRef = $"{peer.ManagedTypeName}, {peer.AssemblyName}"; + } + + return new TypeMapAttributeData { + JniName = jniName, + ProxyTypeReference = proxyRef, + TargetTypeReference = targetRef, + }; + } +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs new file mode 100644 index 00000000000..7d74767aa50 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using System.Reflection.PortableExecutable; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Generates the root _Microsoft.Android.TypeMaps.dll assembly that references +/// all per-assembly typemap assemblies via +/// [assembly: TypeMapAssemblyTargetAttribute<Java.Lang.Object>("name")]. +/// +sealed class RootTypeMapAssemblyGenerator +{ + const string DefaultAssemblyName = "_Microsoft.Android.TypeMaps"; + + // Mono.Android strong name public key token (84e04ff9cfb79065) + static readonly byte [] MonoAndroidPublicKeyToken = { 0x84, 0xe0, 0x4f, 0xf9, 0xcf, 0xb7, 0x90, 0x65 }; + + readonly Version _systemRuntimeVersion; + + /// Version for System.Runtime assembly references. + public RootTypeMapAssemblyGenerator (Version systemRuntimeVersion) + { + _systemRuntimeVersion = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion)); + } + + /// + /// Generates the root typemap assembly. + /// + /// Names of per-assembly typemap assemblies to reference. + /// Path to write the output .dll. + /// Optional assembly name (defaults to _Microsoft.Android.TypeMaps). + public void Generate (IReadOnlyList perAssemblyTypeMapNames, string outputPath, string? assemblyName = null) + { + if (perAssemblyTypeMapNames is null) { + throw new ArgumentNullException (nameof (perAssemblyTypeMapNames)); + } + if (outputPath is null) { + throw new ArgumentNullException (nameof (outputPath)); + } + + assemblyName ??= DefaultAssemblyName; + var moduleName = Path.GetFileName (outputPath); + + var dir = Path.GetDirectoryName (outputPath); + if (!string.IsNullOrEmpty (dir)) { + Directory.CreateDirectory (dir); + } + + var metadata = new MetadataBuilder (); + var ilBuilder = new BlobBuilder (); + + // Assembly definition + metadata.AddAssembly ( + metadata.GetOrAddString (assemblyName), + new Version (1, 0, 0, 0), + culture: default, + publicKey: default, + flags: 0, + hashAlgorithm: AssemblyHashAlgorithm.None); + + // Module definition + metadata.AddModule ( + generation: 0, + metadata.GetOrAddString (moduleName), + metadata.GetOrAddGuid (Guid.NewGuid ()), + encId: default, + encBaseId: default); + + // Assembly references + var systemRuntimeRef = metadata.AddAssemblyReference ( + metadata.GetOrAddString ("System.Runtime"), + _systemRuntimeVersion, default, default, 0, default); + + var systemRuntimeInteropServicesRef = metadata.AddAssemblyReference ( + metadata.GetOrAddString ("System.Runtime.InteropServices"), + _systemRuntimeVersion, default, default, 0, default); + + var monoAndroidRef = metadata.AddAssemblyReference ( + metadata.GetOrAddString ("Mono.Android"), + new Version (0, 0, 0, 0), default, + metadata.GetOrAddBlob (MonoAndroidPublicKeyToken), 0, default); + + // type + metadata.AddTypeDefinition ( + default, default, + metadata.GetOrAddString (""), + default, + MetadataTokens.FieldDefinitionHandle (1), + MetadataTokens.MethodDefinitionHandle (1)); + + // Reference the open generic TypeMapAssemblyTargetAttribute`1 from System.Runtime.InteropServices + var openAttrRef = metadata.AddTypeReference (systemRuntimeInteropServicesRef, + metadata.GetOrAddString ("System.Runtime.InteropServices"), + metadata.GetOrAddString ("TypeMapAssemblyTargetAttribute`1")); + + // Reference Java.Lang.Object from Mono.Android (the type universe) + var javaLangObjectRef = metadata.AddTypeReference (monoAndroidRef, + metadata.GetOrAddString ("Java.Lang"), metadata.GetOrAddString ("Object")); + + // Build TypeSpec for TypeMapAssemblyTargetAttribute + var genericInstBlob = new BlobBuilder (); + genericInstBlob.WriteByte (0x15); // ELEMENT_TYPE_GENERICINST + genericInstBlob.WriteByte (0x12); // ELEMENT_TYPE_CLASS + genericInstBlob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (openAttrRef)); + genericInstBlob.WriteCompressedInteger (1); // generic arity = 1 + genericInstBlob.WriteByte (0x12); // ELEMENT_TYPE_CLASS + genericInstBlob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (javaLangObjectRef)); + var closedAttrTypeSpec = metadata.AddTypeSpecification (metadata.GetOrAddBlob (genericInstBlob)); + + // MemberRef for .ctor(string) on the closed generic type + var ctorRef = AddMemberRef (metadata, closedAttrTypeSpec, ".ctor", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (1, + rt => rt.Void (), + p => p.AddParameter ().Type ().String ())); + + // Add [assembly: TypeMapAssemblyTargetAttribute("name")] for each per-assembly typemap + foreach (var name in perAssemblyTypeMapNames) { + var attrBlob = new BlobBuilder (); + attrBlob.WriteUInt16 (1); // Prolog + attrBlob.WriteSerializedString (name); + attrBlob.WriteUInt16 (0); // NumNamed + metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, ctorRef, + metadata.GetOrAddBlob (attrBlob)); + } + + // Write PE + var peBuilder = new ManagedPEBuilder ( + new PEHeaderBuilder (imageCharacteristics: Characteristics.Dll), + new MetadataRootBuilder (metadata), + ilBuilder); + var peBlob = new BlobBuilder (); + peBuilder.Serialize (peBlob); + using var fs = File.Create (outputPath); + peBlob.WriteContentTo (fs); + } + + static MemberReferenceHandle AddMemberRef (MetadataBuilder metadata, EntityHandle parent, string name, + Action encodeSig) + { + var blob = new BlobBuilder (); + encodeSig (new BlobEncoder (blob)); + return metadata.AddMemberReference (parent, metadata.GetOrAddString (name), metadata.GetOrAddBlob (blob)); + } +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs new file mode 100644 index 00000000000..c6f1b8b0748 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -0,0 +1,1446 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using System.Reflection.PortableExecutable; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Emits a TypeMap PE assembly from a . +/// This is a mechanical translation — all decision logic lives in . +/// +sealed class TypeMapAssemblyEmitter +{ + readonly Dictionary _asmRefCache = new (StringComparer.OrdinalIgnoreCase); + readonly Dictionary<(string Assembly, string Type), EntityHandle> _typeRefCache = new (); + + // Reusable scratch BlobBuilders — avoids allocating a new one per method body / attribute / member ref. + // Each is Clear()'d before use. Safe because all emission is single-threaded and non-reentrant. + readonly BlobBuilder _sigBlob = new BlobBuilder (64); + readonly BlobBuilder _codeBlob = new BlobBuilder (256); + readonly BlobBuilder _attrBlob = new BlobBuilder (64); + + readonly Version _systemRuntimeVersion; + + AssemblyReferenceHandle _systemRuntimeRef; + AssemblyReferenceHandle _monoAndroidRef; + AssemblyReferenceHandle _javaInteropRef; + AssemblyReferenceHandle _systemRuntimeInteropServicesRef; + + TypeReferenceHandle _javaPeerProxyRef; + TypeReferenceHandle _iJavaPeerableRef; + TypeReferenceHandle _jniHandleOwnershipRef; + TypeReferenceHandle _iAndroidCallableWrapperRef; + TypeReferenceHandle _systemTypeRef; + TypeReferenceHandle _runtimeTypeHandleRef; + TypeReferenceHandle _jniTypeRef; + TypeReferenceHandle _trimmableNativeRegistrationRef; + TypeReferenceHandle _notSupportedExceptionRef; + TypeReferenceHandle _runtimeHelpersRef; + TypeReferenceHandle _jniEnvironmentRef; + TypeReferenceHandle _jniTransitionRef; + TypeReferenceHandle _jniRuntimeRef; + TypeReferenceHandle _javaLangObjectRef; + TypeReferenceHandle _jniEnvRef; + TypeReferenceHandle _charSequenceRef; + TypeReferenceHandle _systemExceptionRef; + TypeReferenceHandle _iJavaObjectRef; + + MemberReferenceHandle _baseCtorRef; + MemberReferenceHandle _getTypeFromHandleRef; + MemberReferenceHandle _getUninitializedObjectRef; + MemberReferenceHandle _notSupportedExceptionCtorRef; + MemberReferenceHandle _registerMethodRef; + MemberReferenceHandle _ucoAttrCtorRef; + BlobHandle _ucoAttrBlobHandle; + MemberReferenceHandle _typeMapAttrCtorRef2Arg; + MemberReferenceHandle _typeMapAttrCtorRef3Arg; + MemberReferenceHandle _typeMapAssociationAttrCtorRef; + MemberReferenceHandle _beginMarshalMethodRef; + MemberReferenceHandle _endMarshalMethodRef; + MemberReferenceHandle _onUserUnhandledExceptionRef; + MemberReferenceHandle _jniEnvGetStringRef; + MemberReferenceHandle _jniEnvGetCharSequenceRef; + MemberReferenceHandle _jniEnvNewStringRef; + MemberReferenceHandle _jniEnvToLocalJniHandleRef; + MemberReferenceHandle _charSequenceToLocalJniHandleStringRef; + MemberReferenceHandle _jniEnvGetArrayOpenRef; + MemberReferenceHandle _jniEnvNewArrayOpenRef; + MemberReferenceHandle _jniEnvCopyArrayOpenRef; + MemberReferenceHandle _setHandleRef; + + /// + /// Creates a new emitter. + /// + /// + /// Version for System.Runtime assembly references. + /// Will be derived from $(DotNetTargetVersion) MSBuild property in the build task. + /// + public TypeMapAssemblyEmitter (Version systemRuntimeVersion) + { + _systemRuntimeVersion = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion)); + } + + /// + /// Emits a PE assembly from the given model and writes it to . + /// + public void Emit (TypeMapAssemblyData model, string outputPath) + { + if (model is null) { + throw new ArgumentNullException (nameof (model)); + } + if (outputPath is null) { + throw new ArgumentNullException (nameof (outputPath)); + } + + _asmRefCache.Clear (); + _typeRefCache.Clear (); + + var dir = Path.GetDirectoryName (outputPath); + if (!string.IsNullOrEmpty (dir)) { + Directory.CreateDirectory (dir); + } + + var metadata = new MetadataBuilder (); + var ilBuilder = new BlobBuilder (); + + EmitAssemblyAndModule (metadata, model); + EmitAssemblyReferences (metadata); + EmitTypeReferences (metadata); + EmitMemberReferences (metadata); + EmitModuleType (metadata); + + // Track wrapper method names → handles for RegisterNatives + var wrapperHandles = new Dictionary (); + + foreach (var proxy in model.ProxyTypes) { + EmitProxyType (metadata, ilBuilder, proxy, wrapperHandles); + } + + foreach (var entry in model.Entries) { + EmitTypeMapAttribute (metadata, entry); + } + + foreach (var assoc in model.Associations) { + EmitTypeMapAssociationAttribute (metadata, assoc); + } + + EmitIgnoresAccessChecksToAttribute (metadata, ilBuilder, model.IgnoresAccessChecksTo); + WritePE (metadata, ilBuilder, outputPath); + } + + // ---- Assembly / Module ---- + + void EmitAssemblyAndModule (MetadataBuilder metadata, TypeMapAssemblyData model) + { + metadata.AddAssembly ( + metadata.GetOrAddString (model.AssemblyName), + new Version (1, 0, 0, 0), + culture: default, + publicKey: default, + flags: 0, + hashAlgorithm: AssemblyHashAlgorithm.None); + + metadata.AddModule ( + generation: 0, + metadata.GetOrAddString (model.ModuleName), + metadata.GetOrAddGuid (Guid.NewGuid ()), + encId: default, + encBaseId: default); + } + + // Mono.Android strong name public key token (84e04ff9cfb79065) + static readonly byte [] MonoAndroidPublicKeyToken = { 0x84, 0xe0, 0x4f, 0xf9, 0xcf, 0xb7, 0x90, 0x65 }; + + void EmitAssemblyReferences (MetadataBuilder metadata) + { + _systemRuntimeRef = AddAssemblyRef (metadata, "System.Runtime", _systemRuntimeVersion); + _monoAndroidRef = AddAssemblyRef (metadata, "Mono.Android", new Version (0, 0, 0, 0), + publicKeyOrToken: MonoAndroidPublicKeyToken); + _javaInteropRef = AddAssemblyRef (metadata, "Java.Interop", new Version (0, 0, 0, 0)); + _systemRuntimeInteropServicesRef = AddAssemblyRef (metadata, "System.Runtime.InteropServices", _systemRuntimeVersion); + } + + void EmitTypeReferences (MetadataBuilder metadata) + { + _javaPeerProxyRef = metadata.AddTypeReference (_monoAndroidRef, + metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JavaPeerProxy")); + _iJavaPeerableRef = metadata.AddTypeReference (_javaInteropRef, + metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("IJavaPeerable")); + _jniHandleOwnershipRef = metadata.AddTypeReference (_monoAndroidRef, + metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("JniHandleOwnership")); + _iAndroidCallableWrapperRef = metadata.AddTypeReference (_monoAndroidRef, + metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("IAndroidCallableWrapper")); + _systemTypeRef = metadata.AddTypeReference (_systemRuntimeRef, + metadata.GetOrAddString ("System"), metadata.GetOrAddString ("Type")); + _runtimeTypeHandleRef = metadata.AddTypeReference (_systemRuntimeRef, + metadata.GetOrAddString ("System"), metadata.GetOrAddString ("RuntimeTypeHandle")); + _jniTypeRef = metadata.AddTypeReference (_javaInteropRef, + metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniType")); + _trimmableNativeRegistrationRef = metadata.AddTypeReference (_monoAndroidRef, + metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("TrimmableNativeRegistration")); + _notSupportedExceptionRef = metadata.AddTypeReference (_systemRuntimeRef, + metadata.GetOrAddString ("System"), metadata.GetOrAddString ("NotSupportedException")); + _runtimeHelpersRef = metadata.AddTypeReference (_systemRuntimeRef, + metadata.GetOrAddString ("System.Runtime.CompilerServices"), metadata.GetOrAddString ("RuntimeHelpers")); + _jniEnvironmentRef = metadata.AddTypeReference (_javaInteropRef, + metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniEnvironment")); + _jniTransitionRef = metadata.AddTypeReference (_javaInteropRef, + metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniTransition")); + _jniRuntimeRef = metadata.AddTypeReference (_javaInteropRef, + metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniRuntime")); + _javaLangObjectRef = metadata.AddTypeReference (_monoAndroidRef, + metadata.GetOrAddString ("Java.Lang"), metadata.GetOrAddString ("Object")); + _jniEnvRef = metadata.AddTypeReference (_monoAndroidRef, + metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("JNIEnv")); + _charSequenceRef = metadata.AddTypeReference (_monoAndroidRef, + metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("CharSequence")); + _systemExceptionRef = metadata.AddTypeReference (_systemRuntimeRef, + metadata.GetOrAddString ("System"), metadata.GetOrAddString ("Exception")); + _iJavaObjectRef = metadata.AddTypeReference (_javaInteropRef, + metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("IJavaObject")); + } + + void EmitMemberReferences (MetadataBuilder metadata) + { + _baseCtorRef = AddMemberRef (metadata, _javaPeerProxyRef, ".ctor", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { })); + + _getTypeFromHandleRef = AddMemberRef (metadata, _systemTypeRef, "GetTypeFromHandle", + sig => sig.MethodSignature ().Parameters (1, + rt => rt.Type ().Type (_systemTypeRef, false), + p => p.AddParameter ().Type ().Type (_runtimeTypeHandleRef, true))); + + _getUninitializedObjectRef = AddMemberRef (metadata, _runtimeHelpersRef, "GetUninitializedObject", + sig => sig.MethodSignature ().Parameters (1, + rt => rt.Type ().Object (), + p => p.AddParameter ().Type ().Type (_systemTypeRef, false))); + + _notSupportedExceptionCtorRef = AddMemberRef (metadata, _notSupportedExceptionRef, ".ctor", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (1, + rt => rt.Void (), + p => p.AddParameter ().Type ().String ())); + + _registerMethodRef = AddMemberRef (metadata, _trimmableNativeRegistrationRef, "RegisterMethod", + sig => sig.MethodSignature ().Parameters (4, + rt => rt.Void (), + p => { + p.AddParameter ().Type ().Type (_jniTypeRef, false); + p.AddParameter ().Type ().String (); + p.AddParameter ().Type ().String (); + p.AddParameter ().Type ().IntPtr (); + })); + + var ucoAttrTypeRef = metadata.AddTypeReference (_systemRuntimeInteropServicesRef, + metadata.GetOrAddString ("System.Runtime.InteropServices"), + metadata.GetOrAddString ("UnmanagedCallersOnlyAttribute")); + _ucoAttrCtorRef = AddMemberRef (metadata, ucoAttrTypeRef, ".ctor", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { })); + + // Pre-compute the UCO attribute blob — it's always the same 4 bytes (prolog + no named args) + _attrBlob.Clear (); + _attrBlob.WriteUInt16 (1); + _attrBlob.WriteUInt16 (0); + _ucoAttrBlobHandle = metadata.GetOrAddBlob (_attrBlob); + + // Marshal method support: BeginMarshalMethod, EndMarshalMethod, GetObject, GetString, etc. + // BeginMarshalMethod(IntPtr jnienv, out JniTransition, out JniRuntime) : bool + _beginMarshalMethodRef = AddMemberRef (metadata, _jniEnvironmentRef, "BeginMarshalMethod", + sig => sig.MethodSignature ().Parameters (3, + rt => rt.Type ().Boolean (), + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type (isByRef: true).Type (_jniTransitionRef, true); + p.AddParameter ().Type (isByRef: true).Type (_jniRuntimeRef, false); + })); + + // EndMarshalMethod(ref JniTransition) + _endMarshalMethodRef = AddMemberRef (metadata, _jniEnvironmentRef, "EndMarshalMethod", + sig => sig.MethodSignature ().Parameters (1, + rt => rt.Void (), + p => p.AddParameter ().Type (isByRef: true).Type (_jniTransitionRef, true))); + + // JniRuntime.OnUserUnhandledException(ref JniTransition, Exception) — virtual instance method + _onUserUnhandledExceptionRef = AddMemberRef (metadata, _jniRuntimeRef, "OnUserUnhandledException", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, + rt => rt.Void (), + p => { + p.AddParameter ().Type (isByRef: true).Type (_jniTransitionRef, true); + p.AddParameter ().Type ().Type (_systemExceptionRef, false); + })); + + // JNIEnv.GetString(IntPtr, JniHandleOwnership) : string + _jniEnvGetStringRef = AddMemberRef (metadata, _jniEnvRef, "GetString", + sig => sig.MethodSignature ().Parameters (2, + rt => rt.Type ().String (), + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); + })); + + // JNIEnv.GetCharSequence(IntPtr, JniHandleOwnership) : string + _jniEnvGetCharSequenceRef = AddMemberRef (metadata, _jniEnvRef, "GetCharSequence", + sig => sig.MethodSignature ().Parameters (2, + rt => rt.Type ().String (), + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); + })); + + // JNIEnv.NewString(string) : IntPtr + _jniEnvNewStringRef = AddMemberRef (metadata, _jniEnvRef, "NewString", + sig => sig.MethodSignature ().Parameters (1, + rt => rt.Type ().IntPtr (), + p => p.AddParameter ().Type ().String ())); + + // CharSequence.ToLocalJniHandle(string) : IntPtr + _charSequenceToLocalJniHandleStringRef = AddMemberRef (metadata, _charSequenceRef, "ToLocalJniHandle", + sig => sig.MethodSignature ().Parameters (1, + rt => rt.Type ().IntPtr (), + p => p.AddParameter ().Type ().String ())); + + // JNIEnv.ToLocalJniHandle(IJavaObject) : IntPtr + _jniEnvToLocalJniHandleRef = AddMemberRef (metadata, _jniEnvRef, "ToLocalJniHandle", + sig => sig.MethodSignature ().Parameters (1, + rt => rt.Type ().IntPtr (), + p => p.AddParameter ().Type ().Type (_iJavaObjectRef, false))); + + // JNIEnv.GetArray(IntPtr) : T[] + _jniEnvGetArrayOpenRef = AddMemberRef (metadata, _jniEnvRef, "GetArray", + sig => { + var methodSig = sig.MethodSignature (genericParameterCount: 1); + methodSig.Parameters (1, + rt => rt.Type ().SZArray ().GenericMethodTypeParameter (0), + p => p.AddParameter ().Type ().IntPtr ()); + }); + + // JNIEnv.NewArray(T[]) : IntPtr + _jniEnvNewArrayOpenRef = AddMemberRef (metadata, _jniEnvRef, "NewArray", + sig => { + var methodSig = sig.MethodSignature (genericParameterCount: 1); + methodSig.Parameters (1, + rt => rt.Type ().IntPtr (), + p => p.AddParameter ().Type ().SZArray ().GenericMethodTypeParameter (0)); + }); + + // JNIEnv.CopyArray(T[], IntPtr) : void + _jniEnvCopyArrayOpenRef = AddMemberRef (metadata, _jniEnvRef, "CopyArray", + sig => { + var methodSig = sig.MethodSignature (genericParameterCount: 1); + methodSig.Parameters (2, + rt => rt.Void (), + p => { + p.AddParameter ().Type ().SZArray ().GenericMethodTypeParameter (0); + p.AddParameter ().Type ().IntPtr (); + }); + }); + + // Java.Lang.Object.SetHandle(IntPtr, JniHandleOwnership) : void — protected instance method + _setHandleRef = AddMemberRef (metadata, _javaLangObjectRef, "SetHandle", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, + rt => rt.Void (), + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); + })); + + EmitTypeMapAttributeCtorRef (metadata); + EmitTypeMapAssociationAttributeCtorRef (metadata); + } + + void EmitTypeMapAttributeCtorRef (MetadataBuilder metadata) + { + var typeMapAttrOpenRef = metadata.AddTypeReference (_systemRuntimeInteropServicesRef, + metadata.GetOrAddString ("System.Runtime.InteropServices"), + metadata.GetOrAddString ("TypeMapAttribute`1")); + var javaLangObjectRef = metadata.AddTypeReference (_monoAndroidRef, + metadata.GetOrAddString ("Java.Lang"), metadata.GetOrAddString ("Object")); + + var genericInstBlob = new BlobBuilder (); + genericInstBlob.WriteByte (0x15); // ELEMENT_TYPE_GENERICINST + genericInstBlob.WriteByte (0x12); // ELEMENT_TYPE_CLASS + genericInstBlob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (typeMapAttrOpenRef)); + genericInstBlob.WriteCompressedInteger (1); + genericInstBlob.WriteByte (0x12); // ELEMENT_TYPE_CLASS + genericInstBlob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (javaLangObjectRef)); + var closedAttrTypeSpec = metadata.AddTypeSpecification (metadata.GetOrAddBlob (genericInstBlob)); + + // 2-arg: TypeMap(string jniName, Type proxyType) — unconditional + _typeMapAttrCtorRef2Arg = AddMemberRef (metadata, closedAttrTypeSpec, ".ctor", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, + rt => rt.Void (), + p => { + p.AddParameter ().Type ().String (); + p.AddParameter ().Type ().Type (_systemTypeRef, false); + })); + + // 3-arg: TypeMap(string jniName, Type proxyType, Type targetType) — trimmable + _typeMapAttrCtorRef3Arg = AddMemberRef (metadata, closedAttrTypeSpec, ".ctor", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (3, + rt => rt.Void (), + p => { + p.AddParameter ().Type ().String (); + p.AddParameter ().Type ().Type (_systemTypeRef, false); + p.AddParameter ().Type ().Type (_systemTypeRef, false); + })); + } + + void EmitTypeMapAssociationAttributeCtorRef (MetadataBuilder metadata) + { + // TypeMapAssociationAttribute is in System.Runtime.InteropServices, takes 2 Type args: + // TypeMapAssociation(Type sourceType, Type aliasProxyType) + var typeMapAssociationAttrRef = metadata.AddTypeReference (_systemRuntimeInteropServicesRef, + metadata.GetOrAddString ("System.Runtime.InteropServices"), + metadata.GetOrAddString ("TypeMapAssociationAttribute")); + + _typeMapAssociationAttrCtorRef = AddMemberRef (metadata, typeMapAssociationAttrRef, ".ctor", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, + rt => rt.Void (), + p => { + p.AddParameter ().Type ().Type (_systemTypeRef, false); + p.AddParameter ().Type ().Type (_systemTypeRef, false); + })); + } + + void EmitModuleType (MetadataBuilder metadata) + { + metadata.AddTypeDefinition ( + default, default, + metadata.GetOrAddString (""), + default, + MetadataTokens.FieldDefinitionHandle (1), + MetadataTokens.MethodDefinitionHandle (1)); + } + + // ---- Proxy types ---- + + void EmitProxyType (MetadataBuilder metadata, BlobBuilder ilBuilder, JavaPeerProxyData proxy, + Dictionary wrapperHandles) + { + var typeDefHandle = metadata.AddTypeDefinition ( + TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.Class, + metadata.GetOrAddString (proxy.Namespace), + metadata.GetOrAddString (proxy.TypeName), + _javaPeerProxyRef, + MetadataTokens.FieldDefinitionHandle (metadata.GetRowCount (TableIndex.Field) + 1), + MetadataTokens.MethodDefinitionHandle (metadata.GetRowCount (TableIndex.MethodDef) + 1)); + + if (proxy.IsAcw) { + metadata.AddInterfaceImplementation (typeDefHandle, _iAndroidCallableWrapperRef); + } + + // .ctor + EmitBody (metadata, ilBuilder, ".ctor", + MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName, + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { }), + encoder => { + encoder.OpCode (ILOpCode.Ldarg_0); + encoder.Call (_baseCtorRef); + encoder.OpCode (ILOpCode.Ret); + }); + + // CreateInstance + EmitCreateInstance (metadata, ilBuilder, proxy); + + // get_TargetType + EmitTypeGetter (metadata, ilBuilder, "get_TargetType", proxy.TargetType, + MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.SpecialName | MethodAttributes.HideBySig); + + // get_InvokerType + if (proxy.InvokerType != null) { + EmitTypeGetter (metadata, ilBuilder, "get_InvokerType", proxy.InvokerType, + MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig); + } + + // UCO wrappers (methods and constructors with [Register] connectors) + foreach (var uco in proxy.UcoMethods) { + var handle = EmitUcoMethod (metadata, ilBuilder, uco); + wrapperHandles [uco.WrapperName] = handle; + } + + // Export marshal method wrappers (full marshal body) + foreach (var export in proxy.ExportMarshalMethods) { + var handle = EmitExportMarshalMethod (metadata, ilBuilder, export); + wrapperHandles [export.WrapperName] = handle; + } + + // RegisterNatives + if (proxy.IsAcw) { + EmitRegisterNatives (metadata, ilBuilder, proxy.NativeRegistrations, wrapperHandles); + } + } + + void EmitCreateInstance (MetadataBuilder metadata, BlobBuilder ilBuilder, JavaPeerProxyData proxy) + { + if (!proxy.HasActivation) { + EmitCreateInstanceBody (metadata, ilBuilder, encoder => { + encoder.OpCode (ILOpCode.Ldnull); + encoder.OpCode (ILOpCode.Ret); + }); + return; + } + + // Generic type definitions cannot be instantiated + if (proxy.IsGenericDefinition) { + EmitCreateInstanceBody (metadata, ilBuilder, encoder => { + encoder.LoadString (metadata.GetOrAddUserString ("Cannot create instance of open generic type.")); + encoder.OpCode (ILOpCode.Newobj); + encoder.Token (_notSupportedExceptionCtorRef); + encoder.OpCode (ILOpCode.Throw); + }); + return; + } + + // Interface with invoker: new TInvoker(IntPtr, JniHandleOwnership) + if (proxy.InvokerType != null) { + var invokerCtorRef = AddActivationCtorRef (metadata, ResolveTypeRef (metadata, proxy.InvokerType)); + EmitCreateInstanceBody (metadata, ilBuilder, encoder => { + encoder.OpCode (ILOpCode.Ldarg_1); + encoder.OpCode (ILOpCode.Ldarg_2); + encoder.OpCode (ILOpCode.Newobj); + encoder.Token (invokerCtorRef); + encoder.OpCode (ILOpCode.Ret); + }); + return; + } + + // At this point, ActivationCtor is guaranteed non-null (HasActivation && InvokerType == null) + var activationCtor = proxy.ActivationCtor ?? throw new InvalidOperationException ("ActivationCtor should not be null when HasActivation is true and InvokerType is null"); + var targetTypeRef = ResolveTypeRef (metadata, proxy.TargetType); + + if (activationCtor.IsOnLeafType) { + // Leaf type has its own ctor: new T(IntPtr, JniHandleOwnership) + var ctorRef = AddActivationCtorRef (metadata, targetTypeRef); + EmitCreateInstanceBody (metadata, ilBuilder, encoder => { + encoder.OpCode (ILOpCode.Ldarg_1); + encoder.OpCode (ILOpCode.Ldarg_2); + encoder.OpCode (ILOpCode.Newobj); + encoder.Token (ctorRef); + encoder.OpCode (ILOpCode.Ret); + }); + } else { + // Inherited ctor: GetUninitializedObject(typeof(T)) then call BaseType::.ctor(IntPtr, JniHandleOwnership) + // The base ctor does SetHandle + any other initialization the base class needs. + var baseTypeRef = ResolveTypeRef (metadata, activationCtor.DeclaringType); + var baseCtorRef = AddActivationCtorRef (metadata, baseTypeRef); + EmitCreateInstanceBody (metadata, ilBuilder, encoder => { + encoder.OpCode (ILOpCode.Ldtoken); + encoder.Token (targetTypeRef); + encoder.Call (_getTypeFromHandleRef); + encoder.Call (_getUninitializedObjectRef); + encoder.OpCode (ILOpCode.Castclass); + encoder.Token (targetTypeRef); + + encoder.OpCode (ILOpCode.Dup); + encoder.OpCode (ILOpCode.Ldarg_1); + encoder.OpCode (ILOpCode.Ldarg_2); + encoder.Call (baseCtorRef); + + encoder.OpCode (ILOpCode.Ret); + }); + } + } + + void EmitCreateInstanceBody (MetadataBuilder metadata, BlobBuilder ilBuilder, Action emitIL) + { + EmitBody (metadata, ilBuilder, "CreateInstance", + MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig, + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, + rt => rt.Type ().Type (_iJavaPeerableRef, false), + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); + }), + emitIL); + } + + MemberReferenceHandle AddActivationCtorRef (MetadataBuilder metadata, EntityHandle declaringTypeRef) + { + return AddMemberRef (metadata, declaringTypeRef, ".ctor", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, + rt => rt.Void (), + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); + })); + } + + void EmitTypeGetter (MetadataBuilder metadata, BlobBuilder ilBuilder, string methodName, + TypeRefData typeRef, MethodAttributes attrs) + { + var handle = ResolveTypeRef (metadata, typeRef); + + EmitBody (metadata, ilBuilder, methodName, attrs, + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, + rt => rt.Type ().Type (_systemTypeRef, false), + p => { }), + encoder => { + encoder.OpCode (ILOpCode.Ldtoken); + encoder.Token (handle); + encoder.Call (_getTypeFromHandleRef); + encoder.OpCode (ILOpCode.Ret); + }); + } + + // ---- UCO wrappers ---- + + MethodDefinitionHandle EmitUcoMethod (MetadataBuilder metadata, BlobBuilder ilBuilder, UcoMethodData uco) + { + var jniParams = JniSignatureHelper.ParseParameterTypes (uco.JniSignature); + var returnKind = JniSignatureHelper.ParseReturnType (uco.JniSignature); + int paramCount = 2 + jniParams.Count; + bool isVoid = returnKind == JniParamKind.Void; + + Action encodeSig = sig => sig.MethodSignature ().Parameters (paramCount, + rt => { if (isVoid) rt.Void (); else JniSignatureHelper.EncodeClrType (rt.Type (), returnKind); }, + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().IntPtr (); + for (int j = 0; j < jniParams.Count; j++) + JniSignatureHelper.EncodeClrType (p.AddParameter ().Type (), jniParams [j]); + }); + + var callbackTypeHandle = ResolveTypeRef (metadata, uco.CallbackType); + var callbackRef = AddMemberRef (metadata, callbackTypeHandle, uco.CallbackMethodName, encodeSig); + + var handle = EmitBody (metadata, ilBuilder, uco.WrapperName, + MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, + encodeSig, + encoder => { + for (int p = 0; p < paramCount; p++) + encoder.LoadArgument (p); + encoder.Call (callbackRef); + encoder.OpCode (ILOpCode.Ret); + }); + + AddUnmanagedCallersOnlyAttribute (metadata, handle); + return handle; + } + + // ---- Export marshal method wrappers ---- + + /// + /// Emits a full marshal method body for an [Export] method or constructor. + /// Pattern for methods: + /// static RetType n_Method(IntPtr jnienv, IntPtr native__this, <JNI params...>) { + /// if (!JniEnvironment.BeginMarshalMethod(jnienv, out var __envp, out var __r)) return default; + /// try { + /// var __this = Object.GetObject<T>(jnienv, native__this, DoNotTransfer); + /// // unmarshal params, call managed method, marshal return + /// } catch / finally ... + /// } + /// Pattern for constructors: + /// static void nctor_N_uco(IntPtr jnienv, IntPtr native__this, <ctor params...>) { + /// if (!JniEnvironment.BeginMarshalMethod(jnienv, out var __envp, out var __r)) return; + /// try { + /// var __this = (T)RuntimeHelpers.GetUninitializedObject(typeof(T)); + /// __this.SetHandle(native__this, DoNotTransfer); // registers peer with runtime + /// __this..ctor(params...); // user constructor + /// } catch / finally ... + /// } + /// + MethodDefinitionHandle EmitExportMarshalMethod (MetadataBuilder metadata, BlobBuilder ilBuilder, ExportMarshalMethodData export) + { + var jniParams = JniSignatureHelper.ParseParameterTypes (export.JniSignature); + var jniParamTypes = JniSignatureHelper.ParseParameterTypeStrings (export.JniSignature); + var jniReturnType = export.IsConstructor ? "V" : JniSignatureHelper.ParseReturnTypeString (export.JniSignature); + var returnKind = export.IsConstructor ? JniParamKind.Void : JniSignatureHelper.ParseReturnType (export.JniSignature); + bool isVoid = returnKind == JniParamKind.Void; + int jniParamCount = 2 + jniParams.Count; // jnienv + self + method params + + // Build the method signature + Action encodeSig = sig => sig.MethodSignature ().Parameters (jniParamCount, + rt => { if (isVoid) rt.Void (); else JniSignatureHelper.EncodeClrType (rt.Type (), returnKind); }, + p => { + p.AddParameter ().Type ().IntPtr (); // jnienv + p.AddParameter ().Type ().IntPtr (); // native__this + for (int j = 0; j < jniParams.Count; j++) + JniSignatureHelper.EncodeClrType (p.AddParameter ().Type (), jniParams [j]); + }); + + // Resolve managed type references (needed early for locals signature in constructor case) + var declaringTypeRef = ResolveTypeRef (metadata, export.DeclaringType); + + // Build the locals signature: + // local 0: JniTransition __envp + // local 1: JniRuntime __r + // local 2: Exception __e + // local 3: __ret (only for non-void methods) + // local 3 (ctor): T __this (for constructors — holds the uninitialized object) + int fixedLocalCount = export.IsConstructor || !isVoid ? 4 : 3; + int parameterLocalStart = fixedLocalCount; + int[] parameterLocals = new int [export.ManagedParameters.Count]; + int localCount = fixedLocalCount + export.ManagedParameters.Count; + var localsBlob = new BlobBuilder (32); + var localsEncoder = new BlobEncoder (localsBlob).LocalVariableSignature (localCount); + localsEncoder.AddVariable ().Type ().Type (_jniTransitionRef, true); // local 0 + localsEncoder.AddVariable ().Type ().Type (_jniRuntimeRef, false); // local 1 + localsEncoder.AddVariable ().Type ().Type (_systemExceptionRef, false); // local 2 + if (export.IsConstructor) { + localsEncoder.AddVariable ().Type ().Type (declaringTypeRef, false); // local 3: T __this + } else if (!isVoid) { + JniSignatureHelper.EncodeClrType (localsEncoder.AddVariable ().Type (), returnKind); // local 3: __ret + } + for (int i = 0; i < export.ManagedParameters.Count; i++) { + parameterLocals [i] = parameterLocalStart + i; + EncodeManagedTypeForExportCall (localsEncoder.AddVariable ().Type (), metadata, + export.ManagedParameters [i].ManagedTypeName, export.ManagedParameters [i].AssemblyName, jniParams [i], export.ManagedParameters [i].JniType); + } + var localsSigHandle = metadata.AddStandaloneSignature (metadata.GetOrAddBlob (localsBlob)); + + // Build GetObject method spec — generic instantiation of Object.GetObject + // Not needed for static methods or constructors + EntityHandle getObjectRef = default; + if (!export.IsStatic && !export.IsConstructor) { + getObjectRef = BuildGetObjectMethodSpec (metadata, declaringTypeRef); + } + + // Resolve managed method to call + MemberReferenceHandle managedMethodRef; + if (export.IsConstructor) { + managedMethodRef = BuildExportCtorRef (metadata, export, declaringTypeRef); + } else { + managedMethodRef = BuildExportMethodRef (metadata, export, declaringTypeRef); + } + + // Build the IL with ControlFlowBuilder for try/catch/finally + var cfBuilder = new ControlFlowBuilder (); + _codeBlob.Clear (); + var encoder = new InstructionEncoder (_codeBlob, cfBuilder); + + // Define labels + var tryStartLabel = encoder.DefineLabel (); + var tryEndLabel = encoder.DefineLabel (); + var catchStartLabel = encoder.DefineLabel (); + var catchEndLabel = encoder.DefineLabel (); + var finallyStartLabel = encoder.DefineLabel (); + var finallyEndLabel = encoder.DefineLabel (); + var returnLabel = encoder.DefineLabel (); + + // --- if (!BeginMarshalMethod(jnienv, out __envp, out __r)) return default; --- + encoder.LoadArgument (0); // jnienv + encoder.OpCode (ILOpCode.Ldloca_s); encoder.CodeBuilder.WriteByte (0); // out __envp + encoder.OpCode (ILOpCode.Ldloca_s); encoder.CodeBuilder.WriteByte (1); // out __r + encoder.Call (_beginMarshalMethodRef); + encoder.Branch (ILOpCode.Brtrue_s, tryStartLabel); + // return default + if (!isVoid) { + EmitDefaultReturnValue (encoder, returnKind); + } + encoder.OpCode (ILOpCode.Ret); + + // --- try { --- + encoder.MarkLabel (tryStartLabel); + + if (export.IsConstructor) { + // Constructor: create uninitialized object, call activation ctor, then user ctor + // var __this = (T)RuntimeHelpers.GetUninitializedObject(typeof(T)); + encoder.OpCode (ILOpCode.Ldtoken); + encoder.Token (declaringTypeRef); + encoder.Call (_getTypeFromHandleRef); + encoder.Call (_getUninitializedObjectRef); + encoder.OpCode (ILOpCode.Castclass); + encoder.Token (declaringTypeRef); + encoder.OpCode (ILOpCode.Stloc_3); // store in local 3: __this + + // __this.SetHandle(native__this, JniHandleOwnership.DoNotTransfer) + // — registers the peer with the runtime and sets up the JNI handle association + encoder.OpCode (ILOpCode.Ldloc_3); // __this + encoder.LoadArgument (1); // native__this + encoder.OpCode (ILOpCode.Ldc_i4_0); // JniHandleOwnership.DoNotTransfer = 0 + encoder.Call (_setHandleRef); + } else if (!export.IsStatic) { + // Instance method: get managed object from JNI handle + encoder.LoadArgument (0); // jnienv + encoder.LoadArgument (1); // native__this + encoder.OpCode (ILOpCode.Ldc_i4_0); // JniHandleOwnership.DoNotTransfer = 0 + encoder.Call (getObjectRef); + } + + // Unmarshal each parameter into locals + for (int i = 0; i < export.ManagedParameters.Count; i++) { + EmitParameterUnmarshal (encoder, metadata, export.ManagedParameters [i], jniParams [i], jniParamTypes [i], i + 2); + StoreLocal (encoder, parameterLocals [i]); + } + + // Load target + managed parameters for the managed call + if (export.IsConstructor) { + encoder.OpCode (ILOpCode.Ldloc_3); + } + for (int i = 0; i < export.ManagedParameters.Count; i++) { + LoadLocal (encoder, parameterLocals [i]); + } + + // Call managed method: static → call, instance ctor → call, instance method → callvirt + if (export.IsStatic || export.IsConstructor) { + encoder.Call (managedMethodRef); + } else { + encoder.OpCode (ILOpCode.Callvirt); + encoder.Token (managedMethodRef); + } + + // Marshal return value and store in local 3 + if (!isVoid) { + EmitReturnMarshal (encoder, metadata, returnKind, jniReturnType, export.ManagedReturnType); + encoder.OpCode (ILOpCode.Stloc_3); + } + + // Copy back array parameter changes to JNI arrays + for (int i = 0; i < export.ManagedParameters.Count; i++) { + if (IsManagedArrayType (export.ManagedParameters [i].ManagedTypeName)) { + EmitArrayParameterCopyBack (encoder, metadata, export.ManagedParameters [i], parameterLocals [i], i + 2); + } + } + + // leave to after the handler + encoder.Branch (ILOpCode.Leave_s, returnLabel); + encoder.MarkLabel (tryEndLabel); + + // --- } catch (Exception __e) { --- + encoder.MarkLabel (catchStartLabel); + encoder.OpCode (ILOpCode.Stloc_2); // store exception in local 2 + encoder.OpCode (ILOpCode.Ldloc_1); // __r + encoder.OpCode (ILOpCode.Ldloca_s); encoder.CodeBuilder.WriteByte (0); // ref __envp + encoder.OpCode (ILOpCode.Ldloc_2); // __e + encoder.OpCode (ILOpCode.Callvirt); + encoder.Token (_onUserUnhandledExceptionRef); + if (!isVoid) { + EmitDefaultReturnValue (encoder, returnKind); + encoder.OpCode (ILOpCode.Stloc_3); + } + encoder.Branch (ILOpCode.Leave_s, returnLabel); + encoder.MarkLabel (catchEndLabel); + + // --- } finally { --- + encoder.MarkLabel (finallyStartLabel); + encoder.OpCode (ILOpCode.Ldloca_s); encoder.CodeBuilder.WriteByte (0); // ref __envp + encoder.Call (_endMarshalMethodRef); + encoder.OpCode (ILOpCode.Endfinally); + encoder.MarkLabel (finallyEndLabel); + + // --- return --- + encoder.MarkLabel (returnLabel); + if (!isVoid) { + encoder.OpCode (ILOpCode.Ldloc_3); + } + encoder.OpCode (ILOpCode.Ret); + + // Add exception regions + cfBuilder.AddCatchRegion (tryStartLabel, tryEndLabel, catchStartLabel, catchEndLabel, _systemExceptionRef); + cfBuilder.AddFinallyRegion (tryStartLabel, catchEndLabel, finallyStartLabel, finallyEndLabel); + + // Emit the method with fat body (locals + exception handlers) + _sigBlob.Clear (); + encodeSig (new BlobEncoder (_sigBlob)); + + while (ilBuilder.Count % 4 != 0) { + ilBuilder.WriteByte (0); + } + var bodyEncoder = new MethodBodyStreamEncoder (ilBuilder); + int bodyOffset = bodyEncoder.AddMethodBody (encoder, maxStack: 8, localsSigHandle, MethodBodyAttributes.InitLocals); + + var handle = metadata.AddMethodDefinition ( + MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, + MethodImplAttributes.IL, + metadata.GetOrAddString (export.WrapperName), + metadata.GetOrAddBlob (_sigBlob), + bodyOffset, default); + + AddUnmanagedCallersOnlyAttribute (metadata, handle); + return handle; + } + + /// + /// Builds a MethodSpec for Object.GetObject<T>(IntPtr, IntPtr, JniHandleOwnership). + /// + EntityHandle BuildGetObjectMethodSpec (MetadataBuilder metadata, EntityHandle managedTypeRef) + { + // Object.GetObject(IntPtr jnienv, IntPtr handle, JniHandleOwnership transfer) : T + var openGetObjectRef = AddMemberRef (metadata, _javaLangObjectRef, "GetObject", + sig => { + var methodSig = sig.MethodSignature (genericParameterCount: 1); + methodSig.Parameters (3, + rt => rt.Type ().GenericMethodTypeParameter (0), + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); + }); + }); + + // Build generic instantiation blob: GetObject + var instBlob = new BlobBuilder (16); + instBlob.WriteByte (0x0A); // ELEMENT_TYPE_GENERICINST (for method) + instBlob.WriteCompressedInteger (1); // 1 type argument + instBlob.WriteByte (0x12); // ELEMENT_TYPE_CLASS + instBlob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (managedTypeRef)); + + return metadata.AddMethodSpecification (openGetObjectRef, metadata.GetOrAddBlob (instBlob)); + } + + MemberReferenceHandle BuildExportCtorRef (MetadataBuilder metadata, ExportMarshalMethodData export, EntityHandle declaringTypeRef) + { + int paramCount = export.ManagedParameters.Count; + return AddMemberRef (metadata, declaringTypeRef, ".ctor", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (paramCount, + rt => rt.Void (), + p => { + foreach (var param in export.ManagedParameters) + EncodeExportParamType (p, metadata, param); + })); + } + + MemberReferenceHandle BuildExportMethodRef (MetadataBuilder metadata, ExportMarshalMethodData export, EntityHandle declaringTypeRef) + { + int paramCount = export.ManagedParameters.Count; + var returnKind = JniSignatureHelper.ParseReturnType (export.JniSignature); + bool isVoid = returnKind == JniParamKind.Void; + + return AddMemberRef (metadata, declaringTypeRef, export.ManagedMethodName, + sig => sig.MethodSignature (isInstanceMethod: !export.IsStatic).Parameters (paramCount, + rt => { + if (isVoid) { + rt.Void (); + } else { + EncodeExportReturnType (rt, metadata, export.ManagedReturnType, returnKind, JniSignatureHelper.ParseReturnTypeString (export.JniSignature)); + } + }, + p => { + foreach (var param in export.ManagedParameters) + EncodeExportParamType (p, metadata, param); + })); + } + + void EncodeExportParamType (ParametersEncoder p, MetadataBuilder metadata, ExportParamData param) + { + var jniKind = JniSignatureHelper.ParseSingleTypeFromDescriptor (param.JniType); + if (!string.IsNullOrEmpty (param.ManagedTypeName)) { + EncodeManagedTypeForExportCall (p.AddParameter ().Type (), metadata, param.ManagedTypeName, param.AssemblyName, jniKind, param.JniType); + } else if (jniKind != JniParamKind.Object) { + JniSignatureHelper.EncodeClrType (p.AddParameter ().Type (), jniKind); + } else { + p.AddParameter ().Type ().IntPtr (); + } + } + + void EncodeExportReturnType (ReturnTypeEncoder rt, MetadataBuilder metadata, string? managedReturnType, JniParamKind returnKind, string jniReturnType) + { + if (!string.IsNullOrEmpty (managedReturnType)) { + string typeName = managedReturnType!; + string assemblyName = ""; + int commaIndex = typeName.IndexOf (", ", StringComparison.Ordinal); + if (commaIndex >= 0) { + assemblyName = typeName.Substring (commaIndex + 2); + typeName = typeName.Substring (0, commaIndex); + } + EncodeManagedTypeForExportCall (rt.Type (), metadata, typeName, assemblyName, returnKind, jniReturnType); + } else if (returnKind != JniParamKind.Object) { + JniSignatureHelper.EncodeClrType (rt.Type (), returnKind); + } else { + rt.Type ().IntPtr (); + } + } + + void EncodeManagedTypeForExportCall (SignatureTypeEncoder encoder, MetadataBuilder metadata, + string managedTypeName, string assemblyName, JniParamKind jniKind, string jniType) + { + if (TryEncodeManagedPrimitiveType (encoder, managedTypeName)) { + return; + } + + if (managedTypeName == "System.String") { + encoder.String (); + return; + } + + if (IsManagedArrayType (managedTypeName)) { + EncodeManagedArrayType (encoder, metadata, managedTypeName, assemblyName, jniType); + return; + } + + var typeRef = ResolveTypeRef (metadata, new TypeRefData { + ManagedTypeName = managedTypeName, + AssemblyName = assemblyName, + }); + encoder.Type (typeRef, IsEnumManagedType (managedTypeName, jniKind)); + } + + void EncodeManagedArrayType (SignatureTypeEncoder encoder, MetadataBuilder metadata, string managedArrayTypeName, string assemblyName, string jniType) + { + string elementType = managedArrayTypeName.Substring (0, managedArrayTypeName.Length - 2); + var arrayEncoder = encoder.SZArray (); + if (TryEncodeManagedPrimitiveType (arrayEncoder, elementType)) { + return; + } + if (elementType == "System.String") { + arrayEncoder.String (); + return; + } + var elementRef = ResolveTypeRef (metadata, new TypeRefData { + ManagedTypeName = elementType, + AssemblyName = assemblyName, + }); + var elementJniKind = jniType.StartsWith ("[", StringComparison.Ordinal) + ? JniSignatureHelper.ParseSingleTypeFromDescriptor (jniType.Substring (1)) + : JniParamKind.Object; + arrayEncoder.Type (elementRef, IsEnumManagedType (elementType, elementJniKind)); + } + + static bool TryEncodeManagedPrimitiveType (SignatureTypeEncoder encoder, string managedTypeName) + { + switch (managedTypeName) { + case "System.Boolean": encoder.Boolean (); return true; + case "System.SByte": encoder.SByte (); return true; + case "System.Byte": encoder.Byte (); return true; + case "System.Char": encoder.Char (); return true; + case "System.Int16": encoder.Int16 (); return true; + case "System.UInt16": encoder.UInt16 (); return true; + case "System.Int32": encoder.Int32 (); return true; + case "System.UInt32": encoder.UInt32 (); return true; + case "System.Int64": encoder.Int64 (); return true; + case "System.UInt64": encoder.UInt64 (); return true; + case "System.Single": encoder.Single (); return true; + case "System.Double": encoder.Double (); return true; + case "System.IntPtr": encoder.IntPtr (); return true; + case "System.UIntPtr": encoder.UIntPtr (); return true; + default: return false; + } + } + + static bool IsManagedArrayType (string managedTypeName) + => managedTypeName.EndsWith ("[]", StringComparison.Ordinal); + + static bool IsEnumManagedType (string managedTypeName, JniParamKind jniKind) + { + if (jniKind == JniParamKind.Object || IsManagedArrayType (managedTypeName)) { + return false; + } + return !string.Equals (managedTypeName, "System.Boolean", StringComparison.Ordinal) && + !string.Equals (managedTypeName, "System.SByte", StringComparison.Ordinal) && + !string.Equals (managedTypeName, "System.Byte", StringComparison.Ordinal) && + !string.Equals (managedTypeName, "System.Char", StringComparison.Ordinal) && + !string.Equals (managedTypeName, "System.Int16", StringComparison.Ordinal) && + !string.Equals (managedTypeName, "System.UInt16", StringComparison.Ordinal) && + !string.Equals (managedTypeName, "System.Int32", StringComparison.Ordinal) && + !string.Equals (managedTypeName, "System.UInt32", StringComparison.Ordinal) && + !string.Equals (managedTypeName, "System.Int64", StringComparison.Ordinal) && + !string.Equals (managedTypeName, "System.UInt64", StringComparison.Ordinal) && + !string.Equals (managedTypeName, "System.Single", StringComparison.Ordinal) && + !string.Equals (managedTypeName, "System.Double", StringComparison.Ordinal) && + !string.Equals (managedTypeName, "System.IntPtr", StringComparison.Ordinal) && + !string.Equals (managedTypeName, "System.UIntPtr", StringComparison.Ordinal); + } + + void EmitParameterUnmarshal (InstructionEncoder encoder, MetadataBuilder metadata, ExportParamData param, JniParamKind jniKind, string jniType, int argIndex) + { + if (IsManagedArrayType (param.ManagedTypeName)) { + // Arrays: JNIEnv.GetArray(handle) + var getArraySpec = BuildArrayMethodSpec (metadata, _jniEnvGetArrayOpenRef, param.ManagedTypeName, param.AssemblyName, param.JniType); + encoder.LoadArgument (argIndex); + encoder.Call (getArraySpec); + return; + } + + if (jniKind != JniParamKind.Object) { + encoder.LoadArgument (argIndex); + if (jniKind == JniParamKind.Boolean && param.ManagedTypeName == "System.Boolean") { + // JNI jboolean is byte; managed bool expects 0/1 semantics. + encoder.OpCode (ILOpCode.Ldc_i4_0); + encoder.OpCode (ILOpCode.Cgt_un); + } + return; + } + + if (param.ManagedTypeName == "System.String") { + // String: GetString or GetCharSequence depending on JNI descriptor. + encoder.LoadArgument (argIndex); + encoder.OpCode (ILOpCode.Ldc_i4_0); // DoNotTransfer + encoder.Call (jniType == "Ljava/lang/CharSequence;" ? _jniEnvGetCharSequenceRef : _jniEnvGetStringRef); + return; + } + + // Java object: Object.GetObject(handle, DoNotTransfer) + // Use the 2-arg overload (without jnienv) + var typeRef = ResolveTypeRef (metadata, new TypeRefData { + ManagedTypeName = param.ManagedTypeName, + AssemblyName = param.AssemblyName, + }); + var getObjectRef2 = AddMemberRef (metadata, _javaLangObjectRef, "GetObject", + sig => { + var methodSig = sig.MethodSignature (genericParameterCount: 1); + methodSig.Parameters (2, + rt => rt.Type ().GenericMethodTypeParameter (0), + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); + }); + }); + var instBlob = new BlobBuilder (16); + instBlob.WriteByte (0x0A); + instBlob.WriteCompressedInteger (1); + instBlob.WriteByte (0x12); + instBlob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (typeRef)); + var methodSpec = metadata.AddMethodSpecification (getObjectRef2, metadata.GetOrAddBlob (instBlob)); + + encoder.LoadArgument (argIndex); + encoder.OpCode (ILOpCode.Ldc_i4_0); // DoNotTransfer + encoder.Call (methodSpec); + } + + void EmitReturnMarshal (InstructionEncoder encoder, MetadataBuilder metadata, JniParamKind returnKind, string jniReturnType, string? managedReturnType) + { + string? managedTypeName = null; + string managedAssemblyName = ""; + if (!string.IsNullOrEmpty (managedReturnType)) { + (managedTypeName, managedAssemblyName) = SplitManagedTypeNameAndAssembly (managedReturnType!); + } + + if (!string.IsNullOrEmpty (managedTypeName) && IsManagedArrayType (managedTypeName!)) { + // Managed array -> JNI array + var newArraySpec = BuildArrayMethodSpec (metadata, _jniEnvNewArrayOpenRef, managedTypeName!, managedAssemblyName, jniReturnType); + encoder.Call (newArraySpec); + return; + } + + if (returnKind != JniParamKind.Object) { + // Enum return values are marshaled as int (legacy behavior). + if (!string.IsNullOrEmpty (managedTypeName) && IsEnumManagedType (managedTypeName!, returnKind)) { + encoder.OpCode (ILOpCode.Conv_i4); + } + return; + } + + if (managedTypeName == "System.String") { + encoder.Call (jniReturnType == "Ljava/lang/CharSequence;" ? _charSequenceToLocalJniHandleStringRef : _jniEnvNewStringRef); + return; + } + + // Java object: JNIEnv.ToLocalJniHandle(result) + encoder.Call (_jniEnvToLocalJniHandleRef); + } + + void EmitArrayParameterCopyBack (InstructionEncoder encoder, MetadataBuilder metadata, ExportParamData param, int localIndex, int argIndex) + { + var skipLabel = encoder.DefineLabel (); + LoadLocal (encoder, localIndex); + encoder.Branch (ILOpCode.Brfalse_s, skipLabel); + LoadLocal (encoder, localIndex); + encoder.LoadArgument (argIndex); + var copyArraySpec = BuildArrayMethodSpec (metadata, _jniEnvCopyArrayOpenRef, param.ManagedTypeName, param.AssemblyName, param.JniType); + encoder.Call (copyArraySpec); + encoder.MarkLabel (skipLabel); + } + + EntityHandle BuildArrayMethodSpec (MetadataBuilder metadata, MemberReferenceHandle openMethodRef, string managedArrayTypeName, string assemblyName, string jniType) + { + string elementTypeName = managedArrayTypeName.EndsWith ("[]", StringComparison.Ordinal) + ? managedArrayTypeName.Substring (0, managedArrayTypeName.Length - 2) + : managedArrayTypeName; + + var instBlob = new BlobBuilder (16); + instBlob.WriteByte (0x0A); // ELEMENT_TYPE_GENERICINST (method) + instBlob.WriteCompressedInteger (1); // one type argument + WriteGenericTypeArgument (instBlob, metadata, elementTypeName, assemblyName, + IsEnumManagedType (elementTypeName, JniSignatureHelper.ParseSingleTypeFromDescriptor (jniType.StartsWith ("[", StringComparison.Ordinal) ? jniType.Substring (1) : jniType))); + return metadata.AddMethodSpecification (openMethodRef, metadata.GetOrAddBlob (instBlob)); + } + + void WriteGenericTypeArgument (BlobBuilder blob, MetadataBuilder metadata, string managedTypeName, string assemblyName, bool isValueType) + { + if (TryGetPrimitiveElementTypeCode (managedTypeName, out byte primitiveCode)) { + blob.WriteByte (primitiveCode); + return; + } + + if (managedTypeName == "System.String") { + blob.WriteByte (0x0E); // ELEMENT_TYPE_STRING + return; + } + + var typeRef = ResolveTypeRef (metadata, new TypeRefData { + ManagedTypeName = managedTypeName, + AssemblyName = assemblyName, + }); + blob.WriteByte (isValueType ? (byte) 0x11 : (byte) 0x12); // VALUETYPE | CLASS + blob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (typeRef)); + } + + static bool TryGetPrimitiveElementTypeCode (string managedTypeName, out byte typeCode) + { + switch (managedTypeName) { + case "System.Boolean": typeCode = 0x02; return true; + case "System.Char": typeCode = 0x03; return true; + case "System.SByte": typeCode = 0x04; return true; + case "System.Byte": typeCode = 0x05; return true; + case "System.Int16": typeCode = 0x06; return true; + case "System.UInt16": typeCode = 0x07; return true; + case "System.Int32": typeCode = 0x08; return true; + case "System.UInt32": typeCode = 0x09; return true; + case "System.Int64": typeCode = 0x0A; return true; + case "System.UInt64": typeCode = 0x0B; return true; + case "System.Single": typeCode = 0x0C; return true; + case "System.Double": typeCode = 0x0D; return true; + default: + typeCode = 0; + return false; + } + } + + static (string managedTypeName, string assemblyName) SplitManagedTypeNameAndAssembly (string managedType) + { + int commaIndex = managedType.IndexOf (", ", StringComparison.Ordinal); + if (commaIndex < 0) { + return (managedType, ""); + } + return (managedType.Substring (0, commaIndex), managedType.Substring (commaIndex + 2)); + } + + static void LoadLocal (InstructionEncoder encoder, int localIndex) + { + switch (localIndex) { + case 0: encoder.OpCode (ILOpCode.Ldloc_0); return; + case 1: encoder.OpCode (ILOpCode.Ldloc_1); return; + case 2: encoder.OpCode (ILOpCode.Ldloc_2); return; + case 3: encoder.OpCode (ILOpCode.Ldloc_3); return; + default: + encoder.OpCode (ILOpCode.Ldloc_s); + encoder.CodeBuilder.WriteByte ((byte) localIndex); + return; + } + } + + static void StoreLocal (InstructionEncoder encoder, int localIndex) + { + switch (localIndex) { + case 0: encoder.OpCode (ILOpCode.Stloc_0); return; + case 1: encoder.OpCode (ILOpCode.Stloc_1); return; + case 2: encoder.OpCode (ILOpCode.Stloc_2); return; + case 3: encoder.OpCode (ILOpCode.Stloc_3); return; + default: + encoder.OpCode (ILOpCode.Stloc_s); + encoder.CodeBuilder.WriteByte ((byte) localIndex); + return; + } + } + + static void EmitDefaultReturnValue (InstructionEncoder encoder, JniParamKind kind) + { + switch (kind) { + case JniParamKind.Boolean: + case JniParamKind.Byte: + case JniParamKind.Char: + case JniParamKind.Short: + case JniParamKind.Int: + encoder.OpCode (ILOpCode.Ldc_i4_0); + break; + case JniParamKind.Long: + encoder.OpCode (ILOpCode.Ldc_i8); + encoder.CodeBuilder.WriteInt64 (0); + break; + case JniParamKind.Float: + encoder.OpCode (ILOpCode.Ldc_r4); + encoder.CodeBuilder.WriteSingle (0); + break; + case JniParamKind.Double: + encoder.OpCode (ILOpCode.Ldc_r8); + encoder.CodeBuilder.WriteDouble (0); + break; + case JniParamKind.Object: + encoder.OpCode (ILOpCode.Ldc_i4_0); + encoder.OpCode (ILOpCode.Conv_i); // IntPtr.Zero + break; + } + } + + void EmitRegisterNatives (MetadataBuilder metadata, BlobBuilder ilBuilder, + List registrations, Dictionary wrapperHandles) + { + EmitBody (metadata, ilBuilder, "RegisterNatives", + MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig | + MethodAttributes.NewSlot | MethodAttributes.Final, + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (1, + rt => rt.Void (), + p => p.AddParameter ().Type ().Type (_jniTypeRef, false)), + encoder => { + foreach (var reg in registrations) { + if (!wrapperHandles.TryGetValue (reg.WrapperMethodName, out var wrapperHandle)) { + continue; + } + encoder.LoadArgument (1); + encoder.LoadString (metadata.GetOrAddUserString (reg.JniMethodName)); + encoder.LoadString (metadata.GetOrAddUserString (reg.JniSignature)); + encoder.OpCode (ILOpCode.Ldftn); + encoder.Token (wrapperHandle); + encoder.Call (_registerMethodRef); + } + encoder.OpCode (ILOpCode.Ret); + }); + } + + // ---- TypeMap attributes ---- + + void EmitTypeMapAttribute (MetadataBuilder metadata, TypeMapAttributeData entry) + { + _attrBlob.Clear (); + _attrBlob.WriteUInt16 (0x0001); // Prolog + _attrBlob.WriteSerializedString (entry.JniName); + _attrBlob.WriteSerializedString (entry.ProxyTypeReference); + if (!entry.IsUnconditional) { + _attrBlob.WriteSerializedString (entry.TargetTypeReference!); + } + _attrBlob.WriteUInt16 (0x0000); // NumNamed + + var ctorRef = entry.IsUnconditional ? _typeMapAttrCtorRef2Arg : _typeMapAttrCtorRef3Arg; + metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, ctorRef, metadata.GetOrAddBlob (_attrBlob)); + } + + void EmitTypeMapAssociationAttribute (MetadataBuilder metadata, TypeMapAssociationData assoc) + { + _attrBlob.Clear (); + _attrBlob.WriteUInt16 (0x0001); // Prolog + _attrBlob.WriteSerializedString (assoc.SourceTypeReference); + _attrBlob.WriteSerializedString (assoc.AliasProxyTypeReference); + _attrBlob.WriteUInt16 (0x0000); // NumNamed + metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, _typeMapAssociationAttrCtorRef, + metadata.GetOrAddBlob (_attrBlob)); + } + + // ---- IgnoresAccessChecksTo ---- + + void EmitIgnoresAccessChecksToAttribute (MetadataBuilder metadata, BlobBuilder ilBuilder, List assemblyNames) + { + var attributeTypeRef = metadata.AddTypeReference (_systemRuntimeRef, + metadata.GetOrAddString ("System"), metadata.GetOrAddString ("Attribute")); + + int typeFieldStart = metadata.GetRowCount (TableIndex.Field) + 1; + int typeMethodStart = metadata.GetRowCount (TableIndex.MethodDef) + 1; + + var baseAttrCtorRef = AddMemberRef (metadata, attributeTypeRef, ".ctor", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { })); + + var ctorDef = EmitBody (metadata, ilBuilder, ".ctor", + MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName, + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (1, + rt => rt.Void (), + p => p.AddParameter ().Type ().String ()), + encoder => { + encoder.LoadArgument (0); + encoder.Call (baseAttrCtorRef); + encoder.OpCode (ILOpCode.Ret); + }); + + metadata.AddTypeDefinition ( + TypeAttributes.NotPublic | TypeAttributes.Sealed | TypeAttributes.BeforeFieldInit, + metadata.GetOrAddString ("System.Runtime.CompilerServices"), + metadata.GetOrAddString ("IgnoresAccessChecksToAttribute"), + attributeTypeRef, + MetadataTokens.FieldDefinitionHandle (typeFieldStart), + MetadataTokens.MethodDefinitionHandle (typeMethodStart)); + + foreach (var asmName in assemblyNames) { + _attrBlob.Clear (); + _attrBlob.WriteUInt16 (1); + _attrBlob.WriteSerializedString (asmName); + _attrBlob.WriteUInt16 (0); + metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, ctorDef, metadata.GetOrAddBlob (_attrBlob)); + } + } + + // ---- Plumbing helpers ---- + + AssemblyReferenceHandle AddAssemblyRef (MetadataBuilder metadata, string name, Version version, + byte []? publicKeyOrToken = null) + { + var handle = metadata.AddAssemblyReference ( + metadata.GetOrAddString (name), version, default, + publicKeyOrToken != null ? metadata.GetOrAddBlob (publicKeyOrToken) : default, 0, default); + _asmRefCache [name] = handle; + return handle; + } + + AssemblyReferenceHandle FindOrAddAssemblyReference (MetadataBuilder metadata, string assemblyName) + { + if (_asmRefCache.TryGetValue (assemblyName, out var handle)) { + return handle; + } + return AddAssemblyRef (metadata, assemblyName, new Version (0, 0, 0, 0)); + } + + MemberReferenceHandle AddMemberRef (MetadataBuilder metadata, EntityHandle parent, string name, + Action encodeSig) + { + _sigBlob.Clear (); + encodeSig (new BlobEncoder (_sigBlob)); + return metadata.AddMemberReference (parent, metadata.GetOrAddString (name), metadata.GetOrAddBlob (_sigBlob)); + } + + EntityHandle ResolveTypeRef (MetadataBuilder metadata, TypeRefData typeRef) + { + var cacheKey = (typeRef.AssemblyName, typeRef.ManagedTypeName); + if (_typeRefCache.TryGetValue (cacheKey, out var cached)) { + return cached; + } + var asmRef = FindOrAddAssemblyReference (metadata, typeRef.AssemblyName); + var result = MakeTypeRefForManagedName (metadata, asmRef, typeRef.ManagedTypeName); + _typeRefCache [cacheKey] = result; + return result; + } + + TypeReferenceHandle MakeTypeRefForManagedName (MetadataBuilder metadata, EntityHandle scope, string managedTypeName) + { + int plusIndex = managedTypeName.IndexOf ('+'); + if (plusIndex >= 0) { + var outerRef = MakeTypeRefForManagedName (metadata, scope, managedTypeName.Substring (0, plusIndex)); + return MakeTypeRefForManagedName (metadata, outerRef, managedTypeName.Substring (plusIndex + 1)); + } + int lastDot = managedTypeName.LastIndexOf ('.'); + var ns = lastDot >= 0 ? managedTypeName.Substring (0, lastDot) : ""; + var name = lastDot >= 0 ? managedTypeName.Substring (lastDot + 1) : managedTypeName; + return metadata.AddTypeReference (scope, metadata.GetOrAddString (ns), metadata.GetOrAddString (name)); + } + + void AddUnmanagedCallersOnlyAttribute (MetadataBuilder metadata, MethodDefinitionHandle handle) + { + metadata.AddCustomAttribute (handle, _ucoAttrCtorRef, _ucoAttrBlobHandle); + } + + /// Emits a method body and definition in one call. + MethodDefinitionHandle EmitBody (MetadataBuilder metadata, BlobBuilder ilBuilder, + string name, MethodAttributes attrs, + Action encodeSig, Action emitIL) + { + _sigBlob.Clear (); + encodeSig (new BlobEncoder (_sigBlob)); + + _codeBlob.Clear (); + var encoder = new InstructionEncoder (_codeBlob); + emitIL (encoder); + + while (ilBuilder.Count % 4 != 0) { + ilBuilder.WriteByte (0); + } + var bodyEncoder = new MethodBodyStreamEncoder (ilBuilder); + int bodyOffset = bodyEncoder.AddMethodBody (encoder); + + return metadata.AddMethodDefinition ( + attrs, MethodImplAttributes.IL, + metadata.GetOrAddString (name), + metadata.GetOrAddBlob (_sigBlob), + bodyOffset, default); + } + + static void WritePE (MetadataBuilder metadata, BlobBuilder ilBuilder, string outputPath) + { + var peBuilder = new ManagedPEBuilder ( + new PEHeaderBuilder (imageCharacteristics: Characteristics.Dll), + new MetadataRootBuilder (metadata), + ilBuilder); + var peBlob = new BlobBuilder (); + peBuilder.Serialize (peBlob); + using var fs = File.Create (outputPath); + peBlob.WriteContentTo (fs); + } +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs new file mode 100644 index 00000000000..927346fbf10 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// High-level API: builds the model from peers, then emits the PE assembly. +/// Composes + . +/// +sealed class TypeMapAssemblyGenerator +{ + readonly Version _systemRuntimeVersion; + + /// Version for System.Runtime assembly references. + public TypeMapAssemblyGenerator (Version systemRuntimeVersion) + { + _systemRuntimeVersion = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion)); + } + + /// + /// Generates a TypeMap PE assembly from the given Java peer info records. + /// + /// Scanned Java peer types. + /// Path where the output .dll will be written. + /// Optional explicit assembly name. Derived from outputPath if null. + public void Generate (IReadOnlyList peers, string outputPath, string? assemblyName = null) + { + var model = ModelBuilder.Build (peers, outputPath, assemblyName); + var emitter = new TypeMapAssemblyEmitter (_systemRuntimeVersion); + emitter.Emit (model, outputPath); + } +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Manifest/IManifestTypeInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Manifest/IManifestTypeInfo.cs new file mode 100644 index 00000000000..ad2c602f959 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Manifest/IManifestTypeInfo.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Component kind for manifest generation. +/// +enum ManifestComponentKind +{ + None, + Activity, + Service, + BroadcastReceiver, + ContentProvider, + Application, + Instrumentation, +} + +/// +/// Holds raw attribute property data extracted from either Cecil or SRM. +/// +sealed class ComponentAttributeInfo +{ + /// + /// Full attribute type name, e.g., "Android.App.ActivityAttribute". + /// + public string AttributeType { get; set; } = ""; + + /// + /// Named property values, e.g., { "MainLauncher": true, "Label": "My App" }. + /// Values are decoded .NET types (string, bool, int, Type name as string, enum as int). + /// + public IReadOnlyDictionary Properties { get; set; } = new Dictionary (); + + /// + /// Constructor arguments (positional), e.g., ContentProviderAttribute(string[] authorities). + /// + public IReadOnlyList ConstructorArguments { get; set; } = []; +} + +/// +/// Abstraction over a Java peer type for manifest generation. +/// Implemented by both Cecil (legacy) and SRM (trimmable) adapters. +/// Contains all data ManifestDocument.Merge() needs without Cecil/SRM dependency. +/// +interface IManifestTypeInfo +{ + string FullName { get; } + string Namespace { get; } + + /// Java type name in dotted format, e.g., "my.app.MainActivity". + string JavaName { get; } + + /// Compat Java name (e.g., with md5 hash for auto-generated names). + string CompatJavaName { get; } + + bool IsAbstract { get; } + + /// Required for component types — XA4213 if missing. + bool HasPublicParameterlessConstructor { get; } + + ManifestComponentKind ComponentKind { get; } + + /// Raw data for the primary component attribute. Null if ComponentKind is None. + ComponentAttributeInfo? ComponentAttribute { get; } + + IReadOnlyList IntentFilters { get; } + IReadOnlyList MetaDataEntries { get; } + IReadOnlyList PropertyAttributes { get; } + ComponentAttributeInfo? LayoutAttribute { get; } + IReadOnlyList GrantUriPermissions { get; } +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Manifest/ManifestTypeInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Manifest/ManifestTypeInfo.cs new file mode 100644 index 00000000000..be9e5f0627c --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Manifest/ManifestTypeInfo.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Default mutable implementation of . +/// +sealed class ManifestTypeInfo : IManifestTypeInfo +{ + public string FullName { get; set; } = ""; + public string Namespace { get; set; } = ""; + public string JavaName { get; set; } = ""; + public string CompatJavaName { get; set; } = ""; + public bool IsAbstract { get; set; } + public bool HasPublicParameterlessConstructor { get; set; } + public ManifestComponentKind ComponentKind { get; set; } + public ComponentAttributeInfo? ComponentAttribute { get; set; } + public IReadOnlyList IntentFilters { get; set; } = []; + public IReadOnlyList MetaDataEntries { get; set; } = []; + public IReadOnlyList PropertyAttributes { get; set; } = []; + public ComponentAttributeInfo? LayoutAttribute { get; set; } + public IReadOnlyList GrantUriPermissions { get; set; } = []; +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Manifest/ScannerManifestTypeInfoAdapter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Manifest/ScannerManifestTypeInfoAdapter.cs new file mode 100644 index 00000000000..c3a77a23cd3 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Manifest/ScannerManifestTypeInfoAdapter.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Converts JavaPeerInfo (from the SRM-based scanner) into IManifestTypeInfo +/// for consumption by ManifestDocument.Merge(). No Cecil dependency. +/// +static class ScannerManifestTypeInfoAdapter +{ + static readonly IReadOnlyList EmptyAttributes = new ComponentAttributeInfo [0]; + + public static List Convert (IReadOnlyList peers) + { + var result = new List (peers.Count); + + foreach (var peer in peers) { + if (peer.DoNotGenerateAcw) + continue; + + var cd = peer.ComponentData; + var javaName = peer.JavaName.Replace ('/', '.'); + + result.Add (new ManifestTypeInfo { + FullName = peer.ManagedTypeName, + Namespace = peer.ManagedTypeNamespace, + JavaName = javaName, + CompatJavaName = javaName, + IsAbstract = peer.IsAbstract, + HasPublicParameterlessConstructor = HasPublicDefaultCtor (peer), + ComponentKind = cd?.ComponentKind ?? ManifestComponentKind.None, + ComponentAttribute = cd?.ComponentAttribute, + IntentFilters = cd?.IntentFilters ?? EmptyAttributes, + MetaDataEntries = cd?.MetaDataEntries ?? EmptyAttributes, + PropertyAttributes = cd?.PropertyAttributes ?? EmptyAttributes, + LayoutAttribute = cd?.LayoutAttribute, + GrantUriPermissions = cd?.GrantUriPermissions ?? EmptyAttributes, + }); + } + + return result; + } + + public static List ScanAndConvert (IReadOnlyList assemblyPaths) + { + using var scanner = new JavaPeerScanner (); + return Convert (scanner.Scan (assemblyPaths)); + } + + static bool HasPublicDefaultCtor (JavaPeerInfo peer) + { + foreach (var ctor in peer.JavaConstructors) { + if (ctor.JniSignature == "()V") + return true; + } + return false; + } +} 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/ComponentAttributeExtractor.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ComponentAttributeExtractor.cs new file mode 100644 index 00000000000..dc7c9c5877b --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/ComponentAttributeExtractor.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections.Generic; +using System.Reflection.Metadata; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Extracts component attribute data ([Activity], [Service], etc.) and sub-attributes +/// ([IntentFilter], [MetaData], [Layout], [Property], [GrantUriPermission]) +/// from SRM CustomAttribute blobs into . +/// +static class ComponentAttributeExtractor +{ + // Component attribute type names (without "Attribute" suffix for matching) + static readonly HashSet ComponentAttributeNames = new (StringComparer.Ordinal) { + "ActivityAttribute", + "ServiceAttribute", + "BroadcastReceiverAttribute", + "ContentProviderAttribute", + "ApplicationAttribute", + "InstrumentationAttribute", + }; + + static readonly HashSet SubAttributeNames = new (StringComparer.Ordinal) { + "IntentFilterAttribute", + "MetaDataAttribute", + "PropertyAttribute", + "LayoutAttribute", + "GrantUriPermissionAttribute", + }; + + /// + /// Extracts all component and sub-attribute data from a type's custom attributes. + /// + public static ComponentData Extract (MetadataReader reader, TypeDefinitionHandle typeHandle) + { + var typeDef = reader.GetTypeDefinition (typeHandle); + var provider = new CustomAttributeTypeProvider (reader); + var result = new ComponentData (); + + foreach (var caHandle in typeDef.GetCustomAttributes ()) { + var ca = reader.GetCustomAttribute (caHandle); + var attrName = GetAttributeName (reader, ca); + if (attrName == null) + continue; + + if (ComponentAttributeNames.Contains (attrName)) { + result.ComponentKind = GetComponentKind (attrName); + result.ComponentAttribute = DecodeAttribute (reader, ca, provider, attrName); + } else if (SubAttributeNames.Contains (attrName)) { + var info = DecodeAttribute (reader, ca, provider, attrName); + switch (attrName) { + case "IntentFilterAttribute": + result.IntentFilters.Add (info); + break; + case "MetaDataAttribute": + result.MetaDataEntries.Add (info); + break; + case "PropertyAttribute": + result.PropertyAttributes.Add (info); + break; + case "LayoutAttribute": + result.LayoutAttribute = info; + break; + case "GrantUriPermissionAttribute": + result.GrantUriPermissions.Add (info); + break; + } + } + } + + return result; + } + + static ComponentAttributeInfo DecodeAttribute (MetadataReader reader, CustomAttribute ca, CustomAttributeTypeProvider provider, string attrName) + { + var decoded = ca.DecodeValue (provider); + var properties = new Dictionary (StringComparer.Ordinal); + var ctorArgs = new List (); + + foreach (var fixedArg in decoded.FixedArguments) { + ctorArgs.Add (NormalizeValue (fixedArg.Value)); + } + + foreach (var named in decoded.NamedArguments) { + if (named.Name != null) + properties [named.Name] = NormalizeValue (named.Value); + } + + var fullAttrName = GetFullAttributeTypeName (attrName); + + return new ComponentAttributeInfo { + AttributeType = fullAttrName, + Properties = properties, + ConstructorArguments = ctorArgs, + }; + } + + /// + /// Normalizes SRM decoded values (boxed primitives, ImmutableArray) to consistent types. + /// + static object NormalizeValue (object? value) + { + if (value == null) + return ""; + + // ImmutableArray> for array-typed args + if (value is System.Collections.Immutable.ImmutableArray> typedArray) { + var result = new string [typedArray.Length]; + for (int i = 0; i < typedArray.Length; i++) { + result [i] = typedArray [i].Value?.ToString () ?? ""; + } + return result; + } + + return value; + } + + static ManifestComponentKind GetComponentKind (string attrName) + { + return attrName switch { + "ActivityAttribute" => ManifestComponentKind.Activity, + "ServiceAttribute" => ManifestComponentKind.Service, + "BroadcastReceiverAttribute" => ManifestComponentKind.BroadcastReceiver, + "ContentProviderAttribute" => ManifestComponentKind.ContentProvider, + "ApplicationAttribute" => ManifestComponentKind.Application, + "InstrumentationAttribute" => ManifestComponentKind.Instrumentation, + _ => ManifestComponentKind.None, + }; + } + + static string GetFullAttributeTypeName (string shortName) + { + return shortName switch { + "ActivityAttribute" => "Android.App.ActivityAttribute", + "ServiceAttribute" => "Android.App.ServiceAttribute", + "InstrumentationAttribute" => "Android.App.InstrumentationAttribute", + "ApplicationAttribute" => "Android.App.ApplicationAttribute", + "BroadcastReceiverAttribute" => "Android.Content.BroadcastReceiverAttribute", + "ContentProviderAttribute" => "Android.Content.ContentProviderAttribute", + "IntentFilterAttribute" => "Android.App.IntentFilterAttribute", + "MetaDataAttribute" => "Android.App.MetaDataAttribute", + "PropertyAttribute" => "Android.App.PropertyAttribute", + "LayoutAttribute" => "Android.App.LayoutAttribute", + "GrantUriPermissionAttribute" => "Android.Content.GrantUriPermissionAttribute", + _ => shortName, + }; + } + + static string? GetAttributeName (MetadataReader reader, CustomAttribute ca) + { + 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; + } +} + +/// +/// Result of extracting component attributes from a type. +/// +sealed class ComponentData +{ + public ManifestComponentKind ComponentKind { get; set; } + public ComponentAttributeInfo? ComponentAttribute { get; set; } + public List IntentFilters { get; } = new (); + public List MetaDataEntries { get; } = new (); + public List PropertyAttributes { get; } = new (); + public ComponentAttributeInfo? LayoutAttribute { get; set; } + public List GrantUriPermissions { get; } = new (); +} 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..783fbef948f --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -0,0 +1,322 @@ +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; } + + /// + /// Component attribute data ([Activity], [Service], etc.). Null if no component attributes. + /// + public ComponentData? ComponentData { 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..b405edb874e --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -0,0 +1,1028 @@ +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, + ComponentData = ComponentAttributeExtractor.Extract (index.Reader, typeHandle), + }; + + 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/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 30e7b9d3813..65e774bab00 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -385,6 +385,11 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved. + + + @@ -2852,10 +2857,4 @@ because xbuild doesn't support framework reference assemblies. DependsOnTargets="_ResolveAssemblies;_CreatePackageWorkspace;$(_BeforeLinkAssemblies);_GenerateJniMarshalMethods;_LinkAssembliesNoShrink" /> - - - - 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/AcwMapWriterTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/AcwMapWriterTests.cs new file mode 100644 index 00000000000..f9f15a44190 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/AcwMapWriterTests.cs @@ -0,0 +1,213 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Xunit; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; + +public class AcwMapWriterTests +{ + static string TestFixtureAssemblyPath { + get { + var testAssemblyDir = Path.GetDirectoryName (typeof (AcwMapWriterTests).Assembly.Location)!; + var fixtureAssembly = Path.Combine (testAssemblyDir, "TestFixtures.dll"); + Assert.True (File.Exists (fixtureAssembly), + $"TestFixtures.dll not found at {fixtureAssembly}. Ensure the TestFixtures project builds."); + return fixtureAssembly; + } + } + + List ScanFixtures () + { + using var scanner = new JavaPeerScanner (); + return scanner.Scan (new [] { TestFixtureAssemblyPath }); + } + + [Fact] + public void CreateEntries_ExcludesDoNotGenerateAcwTypes () + { + var peers = ScanFixtures (); + var assemblyName = "TestFixtures"; + var entries = AcwMapWriter.CreateEntries (peers, assemblyName); + + // MCW types (DoNotGenerateAcw = true) should not appear + Assert.DoesNotContain (entries, e => e.JavaKey == "java.lang.Object"); + Assert.DoesNotContain (entries, e => e.JavaKey == "android.app.Activity"); + Assert.DoesNotContain (entries, e => e.JavaKey == "android.app.Service"); + } + + [Fact] + public void CreateEntries_IncludesUserTypes () + { + var peers = ScanFixtures (); + var assemblyName = "TestFixtures"; + var entries = AcwMapWriter.CreateEntries (peers, assemblyName); + + // User-defined types without DoNotGenerateAcw should appear + Assert.Contains (entries, e => e.ManagedKey == "MyApp.MainActivity"); + Assert.Contains (entries, e => e.ManagedKey == "MyApp.MyHelper"); + } + + [Fact] + public void CreateEntries_FormatsJavaKeyWithDots () + { + var peers = ScanFixtures (); + var assemblyName = "TestFixtures"; + var entries = AcwMapWriter.CreateEntries (peers, assemblyName); + + var mainActivity = entries.First (e => e.ManagedKey == "MyApp.MainActivity"); + Assert.Equal ("my.app.MainActivity", mainActivity.JavaKey); + } + + [Fact] + public void CreateEntries_FormatsPartialAssemblyQualifiedName () + { + var peers = ScanFixtures (); + var assemblyName = "TestFixtures"; + var entries = AcwMapWriter.CreateEntries (peers, assemblyName); + + var mainActivity = entries.First (e => e.ManagedKey == "MyApp.MainActivity"); + Assert.Equal ("MyApp.MainActivity, TestFixtures", mainActivity.PartialAssemblyQualifiedName); + } + + [Fact] + public void CreateEntries_FiltersToSpecifiedAssembly () + { + var peers = ScanFixtures (); + var entries = AcwMapWriter.CreateEntries (peers, "NonExistentAssembly"); + + Assert.Empty (entries); + } + + [Fact] + public void WriteMap_SortsByManagedKey () + { + var entries = new List { + new AcwMapEntry { JavaKey = "z.Z", ManagedKey = "Z.Z", PartialAssemblyQualifiedName = "Z.Z, A", CompatJniName = "z.Z", AssemblyName = "A" }, + new AcwMapEntry { JavaKey = "a.A", ManagedKey = "A.A", PartialAssemblyQualifiedName = "A.A, A", CompatJniName = "a.A", AssemblyName = "A" }, + new AcwMapEntry { JavaKey = "m.M", ManagedKey = "M.M", PartialAssemblyQualifiedName = "M.M, A", CompatJniName = "m.M", AssemblyName = "A" }, + }; + + using var sw = new StringWriter (); + AcwMapWriter.WriteMap (entries, sw); + var lines = sw.ToString ().Split ('\n', System.StringSplitOptions.RemoveEmptyEntries); + + // First entry (sorted) should be A.A + Assert.StartsWith ("A.A, A;", lines [0]); + } + + [Fact] + public void WriteMap_ProducesThreeLinesPerEntry () + { + var entries = new List { + new AcwMapEntry { JavaKey = "my.Type", ManagedKey = "My.Type", PartialAssemblyQualifiedName = "My.Type, Asm", CompatJniName = "my.Type", AssemblyName = "Asm" }, + }; + + using var sw = new StringWriter (); + AcwMapWriter.WriteMap (entries, sw); + var lines = sw.ToString ().Split ('\n', System.StringSplitOptions.RemoveEmptyEntries); + + Assert.Equal (3, lines.Length); + Assert.Equal ("My.Type, Asm;my.Type", lines [0]); + Assert.Equal ("My.Type;my.Type", lines [1]); + Assert.Equal ("my.Type;my.Type", lines [2]); + } + + [Fact] + public void WriteMap_HandlesEmptyInput () + { + using var sw = new StringWriter (); + var result = AcwMapWriter.WriteMap (new List (), sw); + Assert.Empty (sw.ToString ()); + Assert.False (result.HasErrors); + Assert.False (result.HasWarnings); + } + + [Fact] + public void WriteMap_DetectsXA4215_DuplicateJavaKey () + { + var entries = new List { + new AcwMapEntry { JavaKey = "dup.Type", ManagedKey = "A.Type", PartialAssemblyQualifiedName = "A.Type, AsmA", CompatJniName = "dup.Type", AssemblyName = "AsmA" }, + new AcwMapEntry { JavaKey = "dup.Type", ManagedKey = "B.Type", PartialAssemblyQualifiedName = "B.Type, AsmB", CompatJniName = "dup.Type", AssemblyName = "AsmB" }, + }; + + using var sw = new StringWriter (); + var result = AcwMapWriter.WriteMap (entries, sw); + + Assert.True (result.HasErrors); + Assert.True (result.JavaConflicts.ContainsKey ("dup.Type")); + } + + [Fact] + public void WriteMap_DetectsXA4214_DuplicateManagedKey () + { + var entries = new List { + new AcwMapEntry { JavaKey = "a.Type", ManagedKey = "Dup.Type", PartialAssemblyQualifiedName = "Dup.Type, AsmA", CompatJniName = "a.Type", AssemblyName = "AsmA" }, + new AcwMapEntry { JavaKey = "b.Type", ManagedKey = "Dup.Type", PartialAssemblyQualifiedName = "Dup.Type, AsmB", CompatJniName = "b.Type", AssemblyName = "AsmB" }, + }; + + using var sw = new StringWriter (); + var result = AcwMapWriter.WriteMap (entries, sw); + + Assert.True (result.HasWarnings); + Assert.True (result.ManagedConflicts.ContainsKey ("Dup.Type")); + } + + [Fact] + public void WriteMap_DuplicateWithinSameAssembly_NoConflict () + { + var entries = new List { + new AcwMapEntry { JavaKey = "dup.Type", ManagedKey = "A.Type", PartialAssemblyQualifiedName = "A.Type, Asm", CompatJniName = "dup.Type", AssemblyName = "Asm" }, + new AcwMapEntry { JavaKey = "dup.Type", ManagedKey = "B.Type", PartialAssemblyQualifiedName = "B.Type, Asm", CompatJniName = "dup.Type", AssemblyName = "Asm" }, + }; + + using var sw = new StringWriter (); + var result = AcwMapWriter.WriteMap (entries, sw); + + // Same assembly duplicates are not cross-assembly conflicts + Assert.False (result.HasErrors); + } + + [Fact] + public void WriteMapToFile_WritesOnlyWhenChanged () + { + var entries = new List { + new AcwMapEntry { JavaKey = "my.Type", ManagedKey = "My.Type", PartialAssemblyQualifiedName = "My.Type, Asm", CompatJniName = "my.Type", AssemblyName = "Asm" }, + }; + + var tempFile = Path.GetTempFileName (); + try { + AcwMapWriter.WriteMapToFile (entries, tempFile); + var firstWriteTime = File.GetLastWriteTimeUtc (tempFile); + + // Wait a bit and write again with same content + System.Threading.Thread.Sleep (50); + AcwMapWriter.WriteMapToFile (entries, tempFile); + var secondWriteTime = File.GetLastWriteTimeUtc (tempFile); + + // File should not have been rewritten + Assert.Equal (firstWriteTime, secondWriteTime); + } finally { + File.Delete (tempFile); + } + } + + [Fact] + public void WriteMapToFile_NoOutputOnXA4215 () + { + var entries = new List { + new AcwMapEntry { JavaKey = "dup.Type", ManagedKey = "A.Type", PartialAssemblyQualifiedName = "A.Type, AsmA", CompatJniName = "dup.Type", AssemblyName = "AsmA" }, + new AcwMapEntry { JavaKey = "dup.Type", ManagedKey = "B.Type", PartialAssemblyQualifiedName = "B.Type, AsmB", CompatJniName = "dup.Type", AssemblyName = "AsmB" }, + }; + + var tempFile = Path.Combine (Path.GetTempPath (), "acw-map-test-" + System.Guid.NewGuid () + ".txt"); + try { + var result = AcwMapWriter.WriteMapToFile (entries, tempFile); + Assert.True (result.HasErrors); + Assert.False (File.Exists (tempFile)); + } finally { + if (File.Exists (tempFile)) + File.Delete (tempFile); + } + } +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/EndToEndTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/EndToEndTests.cs new file mode 100644 index 00000000000..82106cad5e0 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/EndToEndTests.cs @@ -0,0 +1,205 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Xunit; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; + +/// +/// End-to-end tests that run the full pipeline: +/// Scanner → ComponentExtractor → AcwMapWriter → ManifestTypeInfo +/// Verifies that all pieces work together correctly on the TestFixtures assembly. +/// +public class EndToEndTests +{ + static string TestFixtureAssemblyPath { + get { + var testAssemblyDir = Path.GetDirectoryName (typeof (EndToEndTests).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; + } + } + + [Fact] + public void AcwMapContainsAllNonMcwTypes () + { + using var scanner = new JavaPeerScanner (); + var peers = scanner.Scan (new [] { TestFixtureAssemblyPath }); + var entries = AcwMapWriter.CreateEntries (peers, "TestFixtures"); + + // User types should be present + var managedKeys = entries.Select (e => e.ManagedKey).ToHashSet (); + Assert.Contains ("MyApp.MainActivity", managedKeys); + Assert.Contains ("MyApp.MyHelper", managedKeys); + Assert.Contains ("MyApp.MyService", managedKeys); + Assert.Contains ("MyApp.MyReceiver", managedKeys); + Assert.Contains ("MyApp.MyProvider", managedKeys); + Assert.Contains ("MyApp.DeepLinkActivity", managedKeys); + Assert.Contains ("MyApp.MyApplication", managedKeys); + Assert.Contains ("MyApp.MyInstrumentation", managedKeys); + + // MCW types should NOT be present + Assert.DoesNotContain ("Java.Lang.Object", managedKeys); + Assert.DoesNotContain ("Android.App.Activity", managedKeys); + Assert.DoesNotContain ("Android.App.Service", managedKeys); + Assert.DoesNotContain ("Android.Views.View", managedKeys); + } + + [Fact] + public void AcwMapWritesToFile () + { + using var scanner = new JavaPeerScanner (); + var peers = scanner.Scan (new [] { TestFixtureAssemblyPath }); + var entries = AcwMapWriter.CreateEntries (peers, "TestFixtures"); + + var tempFile = Path.GetTempFileName (); + try { + var result = AcwMapWriter.WriteMapToFile (entries, tempFile); + Assert.False (result.HasErrors); + + var content = File.ReadAllText (tempFile); + Assert.Contains ("MyApp.MainActivity", content); + Assert.Contains ("my.app.MainActivity", content); + + // Verify semicolon-separated format + var lines = content.Split ('\n', System.StringSplitOptions.RemoveEmptyEntries); + foreach (var line in lines) { + Assert.Contains (';', line); + } + } finally { + File.Delete (tempFile); + } + } + + [Fact] + public void AllComponentTypesDetected () + { + var infos = ScannerManifestTypeInfoAdapter.ScanAndConvert (new [] { TestFixtureAssemblyPath }); + + var componentTypes = infos.Where (i => i.ComponentKind != ManifestComponentKind.None).ToList (); + + // Verify we find all expected component kinds + Assert.Contains (componentTypes, i => i.ComponentKind == ManifestComponentKind.Activity); + Assert.Contains (componentTypes, i => i.ComponentKind == ManifestComponentKind.Service); + Assert.Contains (componentTypes, i => i.ComponentKind == ManifestComponentKind.BroadcastReceiver); + Assert.Contains (componentTypes, i => i.ComponentKind == ManifestComponentKind.ContentProvider); + Assert.Contains (componentTypes, i => i.ComponentKind == ManifestComponentKind.Application); + Assert.Contains (componentTypes, i => i.ComponentKind == ManifestComponentKind.Instrumentation); + } + + [Fact] + public void NonComponentTypes_HaveKindNone () + { + var infos = ScannerManifestTypeInfoAdapter.ScanAndConvert (new [] { TestFixtureAssemblyPath }); + + // Types without component attributes should have ComponentKind.None + var helper = infos.First (i => i.FullName == "MyApp.MyHelper"); + Assert.Equal (ManifestComponentKind.None, helper.ComponentKind); + + var touchHandler = infos.First (i => i.FullName == "MyApp.TouchHandler"); + Assert.Equal (ManifestComponentKind.None, touchHandler.ComponentKind); + } + + [Fact] + public void ComponentInfoRoundTrip () + { + var infos = ScannerManifestTypeInfoAdapter.ScanAndConvert (new [] { TestFixtureAssemblyPath }); + + // Verify the deep link activity has all its sub-attributes intact + var deepLink = infos.First (i => i.FullName == "MyApp.DeepLinkActivity"); + + // Component attribute + Assert.Equal (ManifestComponentKind.Activity, deepLink.ComponentKind); + Assert.NotNull (deepLink.ComponentAttribute); + Assert.Equal ("my.app.DeepLinkActivity", deepLink.ComponentAttribute!.Properties ["Name"]); + + // Intent filters + Assert.Equal (2, deepLink.IntentFilters.Count); + var viewFilter = deepLink.IntentFilters [0]; + var actions = (string [])viewFilter.ConstructorArguments [0]; + Assert.Contains ("android.intent.action.VIEW", actions); + + // MetaData + Assert.Equal (2, deepLink.MetaDataEntries.Count); + + // Layout + Assert.NotNull (deepLink.LayoutAttribute); + + // Property + Assert.Single (deepLink.PropertyAttributes); + } + + [Fact] + public void ApplicationAttribute_PreservesTypeReferences () + { + var infos = ScannerManifestTypeInfoAdapter.ScanAndConvert (new [] { TestFixtureAssemblyPath }); + var app = infos.First (i => i.FullName == "MyApp.MyApplication"); + + Assert.NotNull (app.ComponentAttribute); + // BackupAgent and ManageSpaceActivity are Type-valued props stored as type name strings + Assert.True (app.ComponentAttribute!.Properties.ContainsKey ("BackupAgent")); + Assert.True (app.ComponentAttribute.Properties.ContainsKey ("ManageSpaceActivity")); + } + + [Fact] + public void UnconditionalMarking_ComponentTypes () + { + using var scanner = new JavaPeerScanner (); + var peers = scanner.Scan (new [] { TestFixtureAssemblyPath }); + + // Component types should be unconditional + var mainActivity = peers.First (p => p.ManagedTypeName == "MyApp.MainActivity"); + Assert.True (mainActivity.IsUnconditional, "Activity type should be unconditional"); + + var service = peers.First (p => p.ManagedTypeName == "MyApp.MyService"); + Assert.True (service.IsUnconditional, "Service type should be unconditional"); + + var receiver = peers.First (p => p.ManagedTypeName == "MyApp.MyReceiver"); + Assert.True (receiver.IsUnconditional, "BroadcastReceiver type should be unconditional"); + + var provider = peers.First (p => p.ManagedTypeName == "MyApp.MyProvider"); + Assert.True (provider.IsUnconditional, "ContentProvider type should be unconditional"); + + var app = peers.First (p => p.ManagedTypeName == "MyApp.MyApplication"); + Assert.True (app.IsUnconditional, "Application type should be unconditional"); + + var inst = peers.First (p => p.ManagedTypeName == "MyApp.MyInstrumentation"); + Assert.True (inst.IsUnconditional, "Instrumentation type should be unconditional"); + } + + [Fact] + public void BackupAgentCrossReference_IsUnconditional () + { + using var scanner = new JavaPeerScanner (); + var peers = scanner.Scan (new [] { TestFixtureAssemblyPath }); + + // MyBackupAgent is referenced by [Application(BackupAgent=typeof(MyBackupAgent))] + var backupAgent = peers.First (p => p.ManagedTypeName == "MyApp.MyBackupAgent"); + Assert.True (backupAgent.IsUnconditional, + "BackupAgent type referenced by [Application] should be unconditional"); + } + + [Fact] + public void ManageSpaceActivityCrossReference_IsUnconditional () + { + using var scanner = new JavaPeerScanner (); + var peers = scanner.Scan (new [] { TestFixtureAssemblyPath }); + + // MyManageSpaceActivity is referenced by [Application(ManageSpaceActivity=typeof(MyManageSpaceActivity))] + var manageSpace = peers.First (p => p.ManagedTypeName == "MyApp.MyManageSpaceActivity"); + Assert.True (manageSpace.IsUnconditional, + "ManageSpaceActivity type referenced by [Application] should be unconditional"); + } + + [Fact] + public void HelperType_NotUnconditional () + { + using var scanner = new JavaPeerScanner (); + var peers = scanner.Scan (new [] { TestFixtureAssemblyPath }); + + var helper = peers.First (p => p.ManagedTypeName == "MyApp.MyHelper"); + Assert.False (helper.IsUnconditional, "Helper type without component attr should be trimmable"); + } +} 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 = "", + NativeCallbackName = "n_ctor", + JniSignature = "()V", + IsConstructor = true, + Connector = "", + }, + }; + return peer; + } + + static MarshalMethodInfo MakeMarshalMethod (string jniName, string callbackName, string jniSig, bool isConstructor = false) + { + return new MarshalMethodInfo { + JniName = jniName, + NativeCallbackName = callbackName, + JniSignature = jniSig, + IsConstructor = isConstructor, + // [Register] methods always have a non-null Connector (handler name or "" for ctors). + // [Export] methods have null Connector. + Connector = isConstructor ? "" : callbackName + "_handler", + }; + } + + // Fixture-based tests: scan the real TestFixtures.dll and verify model output + + static JavaPeerInfo FindFixtureByJavaName (string javaName) + { + var peers = ScanFixtures (); + var peer = peers.FirstOrDefault (p => p.JavaName == javaName); + Assert.NotNull (peer); + return peer; + } + + static JavaPeerProxyData? FindProxy (TypeMapAssemblyData model, string proxyTypeName) + { + return model.ProxyTypes.FirstOrDefault (p => p.TypeName == proxyTypeName); + } + + static TypeMapAttributeData? FindEntry (TypeMapAssemblyData model, string jniName) + { + return model.Entries.FirstOrDefault (e => e.JniName == jniName); + } + + + public class FixtureMcwTypes + { + + [Fact] + public void Fixture_JavaLangObject_HasActivation_CreatesProxy () + { + var peer = FindFixtureByJavaName ("java/lang/Object"); + var model = BuildModel (new [] { peer }, "TypeMap"); + + var proxy = FindProxy (model, "Java_Lang_Object_Proxy"); + Assert.NotNull (proxy); + Assert.True (proxy!.HasActivation); + Assert.Equal ("Java.Lang.Object", proxy.TargetType.ManagedTypeName); + Assert.Equal ("TestFixtures", proxy.TargetType.AssemblyName); + // MCW with DoNotGenerateAcw → not ACW + Assert.False (proxy.IsAcw); + Assert.Empty (proxy.UcoMethods); + Assert.Empty (proxy.NativeRegistrations); + } + + [Fact] + public void Fixture_Activity_HasActivation_CreatesProxy () + { + var peer = FindFixtureByJavaName ("android/app/Activity"); + var model = BuildModel (new [] { peer }, "TypeMap"); + + var proxy = FindProxy (model, "Android_App_Activity_Proxy"); + Assert.NotNull (proxy); + Assert.True (proxy!.HasActivation); + Assert.Equal ("Android.App.Activity", proxy.TargetType.ManagedTypeName); + // MCW: DoNotGenerateAcw=true → not ACW (even though it has marshal methods) + Assert.False (proxy.IsAcw); + } + + [Fact] + public void Fixture_Activity_Entry_PointsToProxy () + { + var peer = FindFixtureByJavaName ("android/app/Activity"); + var model = BuildModel (new [] { peer }, "MyTypeMap"); + + var entry = FindEntry (model, "android/app/Activity"); + Assert.NotNull (entry); + Assert.Contains ("Android_App_Activity_Proxy", entry!.ProxyTypeReference); + Assert.Contains ("MyTypeMap", entry.ProxyTypeReference); + } + + [Fact] + public void Fixture_Throwable_HasActivation () + { + var peer = FindFixtureByJavaName ("java/lang/Throwable"); + var model = BuildModel (new [] { peer }, "TypeMap"); + + var proxy = FindProxy (model, "Java_Lang_Throwable_Proxy"); + Assert.NotNull (proxy); + Assert.True (proxy!.HasActivation); + Assert.False (proxy.IsAcw); + } + + [Fact] + public void Fixture_Exception_HasActivation () + { + var peer = FindFixtureByJavaName ("java/lang/Exception"); + var model = BuildModel (new [] { peer }, "TypeMap"); + + var proxy = FindProxy (model, "Java_Lang_Exception_Proxy"); + Assert.NotNull (proxy); + Assert.True (proxy!.HasActivation); + } + + [Fact] + public void Fixture_Service_NoActivation_NoProxy () + { + // Service in fixtures has no activation ctor on its own — it inherits from J.L.Object + // but Service itself has `protected Service(IntPtr, JniHandleOwnership)` which IS an activation ctor + var peer = FindFixtureByJavaName ("android/app/Service"); + var model = BuildModel (new [] { peer }, "TypeMap"); + + if (peer.ActivationCtor != null) { + Assert.Single (model.ProxyTypes); + } else { + Assert.Empty (model.ProxyTypes); + } + } + + [Fact] + public void Fixture_Context_HasActivation () + { + var peer = FindFixtureByJavaName ("android/content/Context"); + var model = BuildModel (new [] { peer }, "TypeMap"); + + // Context has (IntPtr, JniHandleOwnership) ctor + if (peer.ActivationCtor != null) { + var proxy = FindProxy (model, "Android_Content_Context_Proxy"); + Assert.NotNull (proxy); + Assert.False (proxy!.IsAcw); + } + } + + [Fact] + public void Fixture_View_HasActivation () + { + var peer = FindFixtureByJavaName ("android/view/View"); + var model = BuildModel (new [] { peer }, "TypeMap"); + + if (peer.ActivationCtor != null) { + var proxy = FindProxy (model, "Android_Views_View_Proxy"); + Assert.NotNull (proxy); + } + } + + [Fact] + public void Fixture_Button_HasActivation () + { + var peer = FindFixtureByJavaName ("android/widget/Button"); + var model = BuildModel (new [] { peer }, "TypeMap"); + + if (peer.ActivationCtor != null) { + var proxy = FindProxy (model, "Android_Widget_Button_Proxy"); + Assert.NotNull (proxy); + } + } + + } + + public class FixtureAcwTypes + { + + [Fact] + public void Fixture_MainActivity_IsAcw () + { + var peer = FindFixtureByJavaName ("my/app/MainActivity"); + Assert.False (peer.DoNotGenerateAcw); + Assert.NotEmpty (peer.MarshalMethods); + Assert.NotNull (peer.ActivationCtor); + + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = FindProxy (model, "MyApp_MainActivity_Proxy"); + Assert.NotNull (proxy); + Assert.True (proxy!.IsAcw); + Assert.True (proxy.HasActivation); + } + + [Fact] + public void Fixture_MainActivity_UcoMethods () + { + var peer = FindFixtureByJavaName ("my/app/MainActivity"); + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = FindProxy (model, "MyApp_MainActivity_Proxy")!; + + // Should have UCO wrappers for non-constructor marshal methods + var nonCtorMethods = peer.MarshalMethods.Where (m => !m.IsConstructor).ToList (); + Assert.Equal (nonCtorMethods.Count, proxy.UcoMethods.Count); + + // Verify the onCreate wrapper + var onCreateUco = proxy.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_OnCreate"); + Assert.NotNull (onCreateUco); + Assert.Equal ("(Landroid/os/Bundle;)V", onCreateUco!.JniSignature); + Assert.StartsWith ("n_onCreate_uco_", onCreateUco.WrapperName); + } + + [Fact] + public void Fixture_MainActivity_NativeRegistrations () + { + var peer = FindFixtureByJavaName ("my/app/MainActivity"); + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = FindProxy (model, "MyApp_MainActivity_Proxy")!; + + Assert.NotEmpty (proxy.NativeRegistrations); + + // Should register n_OnCreate + var onCreateReg = proxy.NativeRegistrations.FirstOrDefault (r => r.JniMethodName == "n_OnCreate"); + Assert.NotNull (onCreateReg); + Assert.Equal ("(Landroid/os/Bundle;)V", onCreateReg!.JniSignature); + } + + [Fact] + public void Fixture_MyHelper_IsAcw () + { + var peer = FindFixtureByJavaName ("my/app/MyHelper"); + Assert.False (peer.DoNotGenerateAcw); + + var model = BuildModel (new [] { peer }, "TypeMap"); + + // MyHelper has marshal methods and is not DoNotGenerateAcw + // Whether it's ACW depends on: not interface, has marshal methods, not DoNotGenerateAcw + if (peer.MarshalMethods.Count > 0 && peer.ActivationCtor != null) { + var proxy = FindProxy (model, "MyApp_MyHelper_Proxy"); + Assert.NotNull (proxy); + } + } + + } + + public class FixtureTouchHandler + { + + [Fact] + public void Fixture_TouchHandler_AllUcoMethods () + { + var peer = FindFixtureByJavaName ("my/app/TouchHandler"); + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_TouchHandler_Proxy"); + Assert.NotNull (proxy); + + var nonCtorMethods = peer.MarshalMethods.Where (m => !m.IsConstructor).ToList (); + Assert.Equal (nonCtorMethods.Count, proxy!.UcoMethods.Count); + + // onTouch: (Landroid/view/View;I)Z + var onTouchUco = proxy.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_OnTouch"); + Assert.NotNull (onTouchUco); + Assert.Equal ("(Landroid/view/View;I)Z", onTouchUco!.JniSignature); + + // onFocusChange: (Landroid/view/View;Z)V + var onFocusUco = proxy.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_OnFocusChange"); + Assert.NotNull (onFocusUco); + Assert.Equal ("(Landroid/view/View;Z)V", onFocusUco!.JniSignature); + + // onScroll: (IFJD)V + var onScrollUco = proxy.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_OnScroll"); + Assert.NotNull (onScrollUco); + Assert.Equal ("(IFJD)V", onScrollUco!.JniSignature); + + // getText: ()Ljava/lang/String; + var getTextUco = proxy.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_GetText"); + Assert.NotNull (getTextUco); + Assert.Equal ("()Ljava/lang/String;", getTextUco!.JniSignature); + + // setItems: ([Ljava/lang/String;)V + var setItemsUco = proxy.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_SetItems"); + Assert.NotNull (setItemsUco); + Assert.Equal ("([Ljava/lang/String;)V", setItemsUco!.JniSignature); + } + + [Fact] + public void Fixture_TouchHandler_NativeRegistrationsMatchUcoMethods () + { + var peer = FindFixtureByJavaName ("my/app/TouchHandler"); + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_TouchHandler_Proxy")!; + + // Every UCO method should have a matching registration + foreach (var uco in proxy.UcoMethods) { + var reg = proxy.NativeRegistrations.FirstOrDefault (r => r.WrapperMethodName == uco.WrapperName); + Assert.NotNull (reg); + Assert.Equal (uco.JniSignature, reg!.JniSignature); + } + } + + } + + public class FixtureCustomView + { + + [Fact] + public void Fixture_CustomView_HasTwoConstructorWrappers () + { + var peer = FindFixtureByJavaName ("my/app/CustomView"); + Assert.Equal (2, peer.JavaConstructors.Count); + + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_CustomView_Proxy"); + Assert.NotNull (proxy); + + if (proxy!.IsAcw) { + // Constructors go into ExportMarshalMethods (full marshal body) + var ctorExports = proxy.ExportMarshalMethods.Where (e => e.IsConstructor).ToList (); + Assert.Equal (2, ctorExports.Count); + Assert.Equal ("nctor_0_uco", ctorExports [0].WrapperName); + Assert.Equal ("nctor_1_uco", ctorExports [1].WrapperName); + + // Constructor JNI signatures should be propagated + Assert.Equal ("()V", ctorExports [0].JniSignature); + Assert.Equal ("(Landroid/content/Context;)V", ctorExports [1].JniSignature); + + // Constructor registrations must use the actual JNI signatures + 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); + } + } + + } + + public class FixtureInterfaces + { + + [Fact] + public void Fixture_IOnClickListener_HasInvokerProxy () + { + var peers = ScanFixtures (); + var listener = peers.FirstOrDefault (p => p.ManagedTypeName == "Android.Views.IOnClickListener"); + Assert.NotNull (listener); + Assert.True (listener!.IsInterface); + Assert.NotNull (listener.InvokerTypeName); + + var model = BuildModel (new [] { listener }, "TypeMap"); + var proxy = model.ProxyTypes.FirstOrDefault (); + Assert.NotNull (proxy); + Assert.NotNull (proxy!.InvokerType); + Assert.Equal ("Android.Views.IOnClickListenerInvoker", proxy.InvokerType!.ManagedTypeName); + } + + [Fact] + public void Fixture_IOnClickListener_IsNotAcw () + { + var peers = ScanFixtures (); + var listener = peers.FirstOrDefault (p => p.ManagedTypeName == "Android.Views.IOnClickListener"); + Assert.NotNull (listener); + + var model = BuildModel (new [] { listener! }, "TypeMap"); + + // Interface → not ACW even though it has marshal methods + foreach (var proxy in model.ProxyTypes) { + Assert.False (proxy.IsAcw); + } + } + + } + + public class FixtureNestedTypes + { + + [Fact] + public void Fixture_OuterInner_ProxyNaming () + { + var peer = FindFixtureByJavaName ("my/app/Outer$Inner"); + var model = BuildModel (new [] { peer }, "TypeMap"); + + // . and + get replaced with _ + var entry = FindEntry (model, "my/app/Outer$Inner"); + Assert.NotNull (entry); + + if (peer.ActivationCtor != null) { + var proxy = FindProxy (model, "MyApp_Outer_Inner_Proxy"); + Assert.NotNull (proxy); + Assert.Equal ("MyApp.Outer+Inner", proxy!.TargetType.ManagedTypeName); + } + } + + [Fact] + public void Fixture_ICallbackResult_ProxyNaming () + { + var peer = FindFixtureByJavaName ("my/app/ICallback$Result"); + var model = BuildModel (new [] { peer }, "TypeMap"); + + var entry = FindEntry (model, "my/app/ICallback$Result"); + Assert.NotNull (entry); + + if (peer.ActivationCtor != null) { + var proxy = FindProxy (model, "MyApp_ICallback_Result_Proxy"); + Assert.NotNull (proxy); + Assert.Equal ("MyApp.ICallback+Result", proxy!.TargetType.ManagedTypeName); + } + } + + } + + public class FixtureInvokers + { + + [Fact] + public void Fixture_InterfaceAndInvoker_ShareJniName_InvokerSeparated () + { + var peers = ScanFixtures (); + // IOnClickListener and IOnClickListenerInvoker share "android/view/View$OnClickListener" + var clickPeers = peers.Where (p => p.JavaName == "android/view/View$OnClickListener").ToList (); + Assert.Equal (2, clickPeers.Count); + + var model = BuildModel (clickPeers, "TypeMap"); + + // Invoker is excluded entirely — no TypeMap entry, no proxy. + // Only the interface gets a TypeMap entry and a proxy. + Assert.Single (model.Entries); + Assert.Equal ("android/view/View$OnClickListener", model.Entries [0].JniName); + + // Only the interface proxy exists; the invoker type is referenced + // only as a TypeRef in the interface proxy's InvokerType property. + Assert.Single (model.ProxyTypes); + Assert.NotNull (model.ProxyTypes [0].InvokerType); + Assert.Equal ("Android.Views.IOnClickListenerInvoker", model.ProxyTypes [0].InvokerType!.ManagedTypeName); + } + + [Fact] + public void Build_InvokerType_NoProxyNoEntry () + { + // Invoker types should never get their own proxy or TypeMap entry. + // They only appear as a TypeRef in the interface proxy's InvokerType/CreateInstance. + var ifacePeer = new JavaPeerInfo { + JavaName = "my/app/IFoo", + ManagedTypeName = "MyApp.IFoo", + AssemblyName = "App", + IsInterface = true, + InvokerTypeName = "MyApp.FooInvoker", + }; + var invokerPeer = MakePeerWithActivation ("my/app/IFoo", "MyApp.FooInvoker", "App"); + invokerPeer.DoNotGenerateAcw = true; + + var model = BuildModel (new [] { ifacePeer, invokerPeer }); + + // Only the interface gets a TypeMap entry — its ProxyTypeReference points to the generated proxy + Assert.Single (model.Entries); + Assert.Contains ("MyApp_IFoo_Proxy", model.Entries [0].ProxyTypeReference); + + // Only the interface gets a proxy — the invoker is referenced, not proxied + Assert.Single (model.ProxyTypes); + var proxy = model.ProxyTypes [0]; + Assert.Equal ("MyApp.IFoo", proxy.TargetType.ManagedTypeName); + Assert.NotNull (proxy.InvokerType); + Assert.Equal ("MyApp.FooInvoker", proxy.InvokerType!.ManagedTypeName); + + // Interface proxy has activation because it will create the invoker + Assert.True (proxy.HasActivation); + } + + } + + public class FixtureGenericHolder + { + + [Fact] + public void Fixture_GenericHolder_Entry () + { + var peer = FindFixtureByJavaName ("my/app/GenericHolder"); + Assert.True (peer.IsGenericDefinition); + + var model = BuildModel (new [] { peer }, "TypeMap"); + var entry = FindEntry (model, "my/app/GenericHolder"); + Assert.NotNull (entry); + } + + } + + public class FixtureAbstractBase + { + + [Fact] + public void Fixture_AbstractBase_IsAcw () + { + var peer = FindFixtureByJavaName ("my/app/AbstractBase"); + Assert.True (peer.IsAbstract); + Assert.False (peer.DoNotGenerateAcw); + + var model = BuildModel (new [] { peer }, "TypeMap"); + + // AbstractBase has marshal methods (doWork) and activation ctor + if (peer.ActivationCtor != null && peer.MarshalMethods.Count > 0) { + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_AbstractBase_Proxy"); + Assert.NotNull (proxy); + Assert.True (proxy!.IsAcw); + } + } + + } + + public class FixtureClickableView + { + + [Fact] + public void Fixture_ClickableView_IsAcw () + { + var peer = FindFixtureByJavaName ("my/app/ClickableView"); + Assert.False (peer.DoNotGenerateAcw); + + var model = BuildModel (new [] { peer }, "TypeMap"); + + if (peer.ActivationCtor != null && peer.MarshalMethods.Count > 0) { + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_ClickableView_Proxy"); + Assert.NotNull (proxy); + Assert.True (proxy!.IsAcw); + // Should have onClick UCO wrapper + var onClick = proxy.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_OnClick"); + Assert.NotNull (onClick); + Assert.Equal ("(Landroid/view/View;)V", onClick!.JniSignature); + } + } + + } + + public class FixtureMultiInterfaceView + { + + [Fact] + public void Fixture_MultiInterfaceView_HasAllUcoMethods () + { + var peer = FindFixtureByJavaName ("my/app/MultiInterfaceView"); + Assert.False (peer.DoNotGenerateAcw); + + var model = BuildModel (new [] { peer }, "TypeMap"); + + if (peer.ActivationCtor != null && peer.MarshalMethods.Count > 0) { + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_MultiInterfaceView_Proxy"); + Assert.NotNull (proxy); + + // Should have onClick and onLongClick UCO wrappers + Assert.NotNull (proxy!.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_OnClick")); + Assert.NotNull (proxy.UcoMethods.FirstOrDefault (u => u.CallbackMethodName == "n_OnLongClick")); + } + } + + } + + public class FixtureExportExample + { + + [Fact] + public void Fixture_ExportExample_IsAcw () + { + var peer = FindFixtureByJavaName ("my/app/ExportExample"); + Assert.False (peer.DoNotGenerateAcw); + Assert.Single (peer.MarshalMethods); + + var model = BuildModel (new [] { peer }, "TypeMap"); + + if (peer.ActivationCtor != null) { + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_ExportExample_Proxy"); + Assert.NotNull (proxy); + } + } + + [Fact] + public void Fixture_ExportExample_ExportMethodInExportMarshalMethods () + { + var peer = FindFixtureByJavaName ("my/app/ExportExample"); + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_ExportExample_Proxy"); + Assert.NotNull (proxy); + + // [Export] methods go into ExportMarshalMethods, not UcoMethods + Assert.Empty (proxy!.UcoMethods); + Assert.NotEmpty (proxy.ExportMarshalMethods); + } + + [Fact] + public void Fixture_ExportMethodWithParams_ExportMethodsInExportMarshalMethods () + { + var peer = FindFixtureByJavaName ("my/app/ExportMethodWithParams"); + Assert.Equal (2, peer.MarshalMethods.Count); + // Both methods should be [Export] (Connector == null) + Assert.All (peer.MarshalMethods, mm => Assert.Null (mm.Connector)); + + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_ExportMethodWithParams_Proxy"); + Assert.NotNull (proxy); + Assert.Empty (proxy!.UcoMethods); + Assert.Equal (2, proxy.ExportMarshalMethods.Count); + } + + [Fact] + public void Fixture_ExportsConstructors_ExportConstructorsInExportMarshalMethods () + { + var peer = FindFixtureByJavaName ("my/app/ExportsConstructors"); + // Should have [Export] constructors (Connector == null) + var exportCtors = peer.MarshalMethods.Where (m => m.IsConstructor && m.Connector == null).ToList (); + Assert.NotEmpty (exportCtors); + + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_ExportsConstructors_Proxy"); + Assert.NotNull (proxy); + + // [Export] constructors go into ExportMarshalMethods, not UcoMethods + Assert.DoesNotContain (proxy!.UcoMethods, u => u.WrapperName.StartsWith ("nctor_")); + Assert.Equal (exportCtors.Count, proxy.ExportMarshalMethods.Count (e => e.IsConstructor)); + } + + [Fact] + public void Fixture_ExportsThrowsConstructors_ExportConstructorsInExportMarshalMethods () + { + var peer = FindFixtureByJavaName ("my/app/ExportsThrowsConstructors"); + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_ExportsThrowsConstructors_Proxy"); + Assert.NotNull (proxy); + + // All constructors are [Export] → in ExportMarshalMethods, not UcoMethods + Assert.DoesNotContain (proxy!.UcoMethods, u => u.WrapperName.StartsWith ("nctor_")); + Assert.NotEmpty (proxy.ExportMarshalMethods); + Assert.All (proxy.ExportMarshalMethods, e => Assert.True (e.IsConstructor)); + } + + [Fact] + public void Fixture_ExportMethodWithParams_HasNativeRegistrations () + { + var peer = FindFixtureByJavaName ("my/app/ExportMethodWithParams"); + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_ExportMethodWithParams_Proxy"); + Assert.NotNull (proxy); + + // [Export] methods should generate NativeRegistrations entries + Assert.Equal (2, proxy!.NativeRegistrations.Count); + + // Registration names must match the JCW native method declarations (n_) + Assert.Contains (proxy.NativeRegistrations, r => r.JniMethodName == "n_DoWork"); + Assert.Contains (proxy.NativeRegistrations, r => r.JniMethodName == "n_ComputeName"); + } + + [Fact] + public void Fixture_ExportWithNameOverride_NativeRegistrationUsesCallbackName () + { + // [Export("attributeOverridesNames")] public string CompletelyDifferentName(...) + // JCW declares: native String n_CompletelyDifferentName(...) + // RegisterNatives must use "n_CompletelyDifferentName", NOT "n_attributeOverridesNames" + var peer = FindFixtureByJavaName ("my/app/ExportMembersComprehensive"); + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_ExportMembersComprehensive_Proxy"); + Assert.NotNull (proxy); + + Assert.Contains (proxy!.NativeRegistrations, + r => r.JniMethodName == "n_CompletelyDifferentName"); + Assert.DoesNotContain (proxy.NativeRegistrations, + r => r.JniMethodName == "n_attributeOverridesNames"); + } + + [Fact] + public void Fixture_ExportMarshalMethod_HasCorrectManagedParameters () + { + var peer = FindFixtureByJavaName ("my/app/ExportMethodWithParams"); + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_ExportMethodWithParams_Proxy"); + Assert.NotNull (proxy); + + // The method with (String, int) params should have correct managed types + var exportMethod = proxy!.ExportMarshalMethods.FirstOrDefault (e => e.ManagedParameters.Count == 2); + Assert.NotNull (exportMethod); + Assert.False (exportMethod!.IsConstructor); + + // Verify the string return type is assembly-qualified + Assert.Equal ("System.String, System.Private.CoreLib", exportMethod.ManagedReturnType); + } + + [Fact] + public void Fixture_ExportMarshalComplex_HasArrayEnumAndCharSequenceMetadata () + { + var peer = FindFixtureByJavaName ("my/app/ExportMarshalComplex"); + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_ExportMarshalComplex_Proxy"); + Assert.NotNull (proxy); + + var roundTripEnum = proxy!.ExportMarshalMethods.First (e => e.ManagedMethodName == "RoundTripEnum"); + Assert.Equal ("(I)I", roundTripEnum.JniSignature); + Assert.Equal ("MyApp.ExportSampleEnum", roundTripEnum.ManagedParameters [0].ManagedTypeName); + Assert.Equal ("TestFixtures", roundTripEnum.ManagedParameters [0].AssemblyName); + Assert.Equal ("MyApp.ExportSampleEnum, TestFixtures", roundTripEnum.ManagedReturnType); + + var echoCharSequence = proxy.ExportMarshalMethods.First (e => e.ManagedMethodName == "EchoCharSequence"); + Assert.Equal ("(Ljava/lang/CharSequence;)Ljava/lang/CharSequence;", echoCharSequence.JniSignature); + Assert.Equal ("Java.Lang.ICharSequence", echoCharSequence.ManagedParameters [0].ManagedTypeName); + + var mutateInts = proxy.ExportMarshalMethods.First (e => e.ManagedMethodName == "MutateInts"); + Assert.Equal ("([I)V", mutateInts.JniSignature); + Assert.Equal ("System.Int32[]", mutateInts.ManagedParameters [0].ManagedTypeName); + + var echoViews = proxy.ExportMarshalMethods.First (e => e.ManagedMethodName == "EchoViews"); + Assert.Equal ("([Landroid/view/View;)[Landroid/view/View;", echoViews.JniSignature); + Assert.Equal ("Android.Views.View[]", echoViews.ManagedParameters [0].ManagedTypeName); + + var echoStrings = proxy.ExportMarshalMethods.First (e => e.ManagedMethodName == "EchoStrings"); + Assert.Equal ("([Ljava/lang/String;)[Ljava/lang/String;", echoStrings.JniSignature); + Assert.Equal ("System.String[]", echoStrings.ManagedParameters [0].ManagedTypeName); + } + + [Fact] + public void Fixture_UnregisteredExporter_ExportOnlyTypeGetsProxy () + { + var peers = ScanFixtures (); + var peer = peers.First (p => p.ManagedTypeName == "MyApp.UnregisteredExporter"); + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_UnregisteredExporter_Proxy"); + Assert.NotNull (proxy); + Assert.True (proxy!.IsAcw); + Assert.Contains (proxy.NativeRegistrations, r => r.JniMethodName == "n_DoExportedWork"); + } + + } + + public class FixtureStaticExportAndExportField + { + + [Fact] + public void Fixture_StaticExport_IsStaticInModel () + { + var peer = FindFixtureByJavaName ("my/app/ExportStaticAndFields"); + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_ExportStaticAndFields_Proxy"); + Assert.NotNull (proxy); + + var staticExport = proxy!.ExportMarshalMethods.FirstOrDefault (e => e.ManagedMethodName == "staticMethodNotMangled"); + Assert.NotNull (staticExport); + Assert.True (staticExport!.IsStatic); + } + + [Fact] + public void Fixture_InstanceExport_IsNotStaticInModel () + { + var peer = FindFixtureByJavaName ("my/app/ExportStaticAndFields"); + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_ExportStaticAndFields_Proxy"); + Assert.NotNull (proxy); + + var instanceExport = proxy!.ExportMarshalMethods.FirstOrDefault (e => e.ManagedMethodName == "instanceMethod"); + Assert.NotNull (instanceExport); + Assert.False (instanceExport!.IsStatic); + } + + [Fact] + public void Fixture_ExportField_MethodInExportMarshalMethods () + { + var peer = FindFixtureByJavaName ("my/app/ExportStaticAndFields"); + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_ExportStaticAndFields_Proxy"); + Assert.NotNull (proxy); + + // [ExportField] methods are [Export]-style and should be in ExportMarshalMethods + var getInstance = proxy!.ExportMarshalMethods.FirstOrDefault (e => e.ManagedMethodName == "GetInstance"); + Assert.NotNull (getInstance); + Assert.True (getInstance!.IsStatic); + Assert.False (getInstance.IsConstructor); + + var getValue = proxy.ExportMarshalMethods.FirstOrDefault (e => e.ManagedMethodName == "GetValue"); + Assert.NotNull (getValue); + Assert.False (getValue!.IsStatic); + } + + [Fact] + public void Fixture_ExportField_NativeRegistrations () + { + var peer = FindFixtureByJavaName ("my/app/ExportStaticAndFields"); + var model = BuildModel (new [] { peer }, "TypeMap"); + var proxy = model.ProxyTypes.FirstOrDefault (p => p.TypeName == "MyApp_ExportStaticAndFields_Proxy"); + Assert.NotNull (proxy); + + // All export methods should have native registrations + var getInstanceReg = proxy!.NativeRegistrations.FirstOrDefault (r => r.JniMethodName == "n_GetInstance"); + Assert.NotNull (getInstanceReg); + + var staticReg = proxy.NativeRegistrations.FirstOrDefault (r => r.JniMethodName == "n_staticMethodNotMangled"); + Assert.NotNull (staticReg); + } + } + + public class FixtureImplementors + { + + [Fact] + public void Fixture_Implementor_IsTrimmable_NotUnconditional () + { + var peer = FindFixtureByJavaName ("android/view/View_IOnClickListenerImplementor"); + Assert.False (peer.DoNotGenerateAcw); + Assert.False (peer.IsInterface); + + var model = BuildModel (new [] { peer }, "TypeMap"); + + // Implementor types should be trimmable (3-arg), NOT unconditional + var entry = model.Entries.FirstOrDefault (); + Assert.NotNull (entry); + Assert.False (entry!.IsUnconditional, "Implementor should NOT be unconditional"); + Assert.NotNull (entry.TargetTypeReference); + } + + } + + public class FixtureEventDispatchers + { + + [Fact] + public void Fixture_EventDispatcher_IsTrimmable_NotUnconditional () + { + var peer = FindFixtureByJavaName ("android/view/View_ClickEventDispatcher"); + Assert.False (peer.DoNotGenerateAcw); + Assert.False (peer.IsInterface); + + var model = BuildModel (new [] { peer }, "TypeMap"); + + // EventDispatcher types should be trimmable (3-arg), NOT unconditional + var entry = model.Entries.FirstOrDefault (); + Assert.NotNull (entry); + Assert.False (entry!.IsUnconditional, "EventDispatcher should NOT be unconditional"); + Assert.NotNull (entry.TargetTypeReference); + } + + } + + public class NameBasedDetection + { + + [Fact] + public void Build_UserTypeNamedImplementor_IsTreatedAsTrimmable () + { + // Limitation: name-based heuristic means a user type ending in "Implementor" + // will be treated as trimmable even if it's genuinely a user ACW type. + // This test documents the known behavior. + var peer = MakeAcwPeer ("my/app/MyImplementor", "MyApp.MyImplementor", "App"); + var model = BuildModel (new [] { peer }); + + var entry = model.Entries.FirstOrDefault (); + Assert.NotNull (entry); + // The heuristic treats this as an Implementor → trimmable (not unconditional) + Assert.False (entry!.IsUnconditional, + "Name-based heuristic: types ending in 'Implementor' are treated as trimmable"); + } + + [Fact] + public void Build_TypeIsInvoker_OnlyWhenReferencedByAnotherPeer () + { + // A type is only treated as an invoker when another peer's InvokerTypeName references it. + // A type named "MyInvoker" with DoNotGenerateAcw is NOT automatically an invoker. + var invokerPeer = MakePeerWithActivation ("my/app/MyInvoker", "MyApp.MyInvoker", "App"); + invokerPeer.DoNotGenerateAcw = true; + + // Without a referencing peer, it gets a normal entry + var model1 = BuildModel (new [] { invokerPeer }); + Assert.Single (model1.Entries); + + // When an interface references it as invoker, it is excluded + var ifacePeer = new JavaPeerInfo { + JavaName = "my/app/MyInvoker", + ManagedTypeName = "MyApp.IMyInterface", + AssemblyName = "App", + IsInterface = true, + InvokerTypeName = "MyApp.MyInvoker", + }; + var model2 = BuildModel (new [] { ifacePeer, invokerPeer }); + // Only the interface gets entries/proxies, the invoker is excluded + Assert.Single (model2.Entries); + Assert.Equal ("MyApp.IMyInterface", model2.ProxyTypes [0].TargetType.ManagedTypeName); + } + + } + + public class PipelineTests + { + + [Fact] + public void FullPipeline_AllFixtures_ProducesLoadableAssembly () + { + var peers = ScanFixtures (); + var model = BuildModel (peers, "FullPipeline"); + + var outputPath = Path.Combine (Path.GetTempPath (), $"fullpipeline-{Guid.NewGuid ():N}", "FullPipeline.dll"); + try { + var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); + emitter.Emit (model, outputPath); + + Assert.True (File.Exists (outputPath)); + using var pe = new PEReader (File.OpenRead (outputPath)); + Assert.True (pe.HasMetadata); + + var reader = pe.GetMetadataReader (); + var asmDef = reader.GetAssemblyDefinition (); + Assert.Equal ("FullPipeline", reader.GetString (asmDef.Name)); + + // Verify proxy types are present + var proxyTypes = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .Where (t => reader.GetString (t.Namespace) == "_TypeMap.Proxies") + .ToList (); + Assert.Equal (model.ProxyTypes.Count, proxyTypes.Count); + + // Verify all proxy type names match + var proxyNames = proxyTypes.Select (t => reader.GetString (t.Name)).OrderBy (n => n).ToList (); + var modelNames = model.ProxyTypes.Select (p => p.TypeName).OrderBy (n => n).ToList (); + Assert.Equal (modelNames, proxyNames); + } finally { + var dir = Path.GetDirectoryName (outputPath); + if (dir != null && Directory.Exists (dir)) + try { Directory.Delete (dir, true); } catch { } + } + } + + [Fact] + public void FullPipeline_AllFixtures_TypeMapAttributeCountMatchesEntries () + { + var peers = ScanFixtures (); + var model = BuildModel (peers, "AttrCount"); + + var outputPath = Path.Combine (Path.GetTempPath (), $"attrcount-{Guid.NewGuid ():N}", "AttrCount.dll"); + try { + var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); + emitter.Emit (model, outputPath); + + using var pe = new PEReader (File.OpenRead (outputPath)); + var reader = pe.GetMetadataReader (); + + var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); + int totalAttrs = asmAttrs.Count (); + + // Assembly attrs = TypeMap entries + IgnoresAccessChecksTo entries + int expected = model.Entries.Count + model.IgnoresAccessChecksTo.Count; + Assert.Equal (expected, totalAttrs); + } finally { + var dir = Path.GetDirectoryName (outputPath); + if (dir != null && Directory.Exists (dir)) + try { Directory.Delete (dir, true); } catch { } + } + } + + [Fact] + public void FullPipeline_TouchHandler_AcwProxyHasUcoAttributes () + { + var peer = FindFixtureByJavaName ("my/app/TouchHandler"); + var model = BuildModel (new [] { peer }, "UcoAttrTest"); + + var outputPath = Path.Combine (Path.GetTempPath (), $"ucoattr-{Guid.NewGuid ():N}", "UcoAttrTest.dll"); + try { + var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); + emitter.Emit (model, outputPath); + + using var pe = new PEReader (File.OpenRead (outputPath)); + var reader = pe.GetMetadataReader (); + + 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)) + .ToList (); + + var ucoMethods = methods.Where (m => reader.GetString (m.Name).Contains ("_uco_")).ToList (); + Assert.NotEmpty (ucoMethods); + + // Each UCO method should have [UnmanagedCallersOnly] + foreach (var uco in ucoMethods) { + var attrs = uco.GetCustomAttributes ().Select (h => reader.GetCustomAttribute (h)).ToList (); + Assert.NotEmpty (attrs); + } + } finally { + var dir = Path.GetDirectoryName (outputPath); + if (dir != null && Directory.Exists (dir)) + try { Directory.Delete (dir, true); } catch { } + } + } + + [Fact] + public void FullPipeline_CustomView_HasConstructorAndMethodWrappers () + { + var peer = FindFixtureByJavaName ("my/app/CustomView"); + var model = BuildModel (new [] { peer }, "CtorTest"); + + var outputPath = Path.Combine (Path.GetTempPath (), $"ctor-{Guid.NewGuid ():N}", "CtorTest.dll"); + try { + var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); + emitter.Emit (model, outputPath); + + using var pe = new PEReader (File.OpenRead (outputPath)); + var reader = pe.GetMetadataReader (); + + var proxy = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .First (t => reader.GetString (t.Name) == "MyApp_CustomView_Proxy"); + + var methodNames = proxy.GetMethods () + .Select (h => reader.GetString (reader.GetMethodDefinition (h).Name)) + .ToList (); + + Assert.Contains (".ctor", methodNames); + Assert.Contains ("CreateInstance", methodNames); + Assert.Contains ("get_TargetType", methodNames); + + if (model.ProxyTypes [0].IsAcw) { + Assert.Contains ("RegisterNatives", methodNames); + Assert.Contains (methodNames, m => m.StartsWith ("nctor_") && m.EndsWith ("_uco")); + } + } finally { + var dir = Path.GetDirectoryName (outputPath); + if (dir != null && Directory.Exists (dir)) + try { Directory.Delete (dir, true); } catch { } + } + } + + [Fact] + public void FullPipeline_CustomView_UcoConstructorMatchesJniSignature () + { + var peer = FindFixtureByJavaName ("my/app/CustomView"); + var model = BuildModel (new [] { peer }, "CtorSigTest"); + + var outputPath = Path.Combine (Path.GetTempPath (), $"ctorsig-{Guid.NewGuid ():N}", "CtorSigTest.dll"); + try { + var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); + emitter.Emit (model, outputPath); + + using var pe = new PEReader (File.OpenRead (outputPath)); + var reader = pe.GetMetadataReader (); + + var proxy = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .First (t => reader.GetString (t.Name) == "MyApp_CustomView_Proxy"); + + // Find UCO constructor wrappers (nctor_*_uco) + var ucoCtors = proxy.GetMethods () + .Select (h => reader.GetMethodDefinition (h)) + .Where (m => reader.GetString (m.Name).StartsWith ("nctor_") && reader.GetString (m.Name).EndsWith ("_uco")) + .ToList (); + + Assert.NotEmpty (ucoCtors); + + // Match each constructor to its model data to verify param count + foreach (var uco in ucoCtors) { + var name = reader.GetString (uco.Name); + var modelExport = model.ProxyTypes + .SelectMany (p => p.ExportMarshalMethods) + .First (e => e.WrapperName == name); + + // Constructor signature must include jnienv + self + JNI params + int expectedJniParams = JniSignatureHelper.ParseParameterTypes (modelExport.JniSignature).Count; + int expectedTotal = 2 + expectedJniParams; + + var sig = reader.GetBlobReader (uco.Signature); + var header = sig.ReadSignatureHeader (); + int paramCount = sig.ReadCompressedInteger (); + Assert.Equal (expectedTotal, paramCount); + } + } finally { + CleanUpDir (outputPath); + } + } + + [Fact] + public void FullPipeline_GenericHolder_ProducesValidAssembly () + { + var peer = FindFixtureByJavaName ("my/app/GenericHolder"); + var model = BuildModel (new [] { peer }, "GenericTest"); + + var outputPath = Path.Combine (Path.GetTempPath (), $"generic-{Guid.NewGuid ():N}", "GenericTest.dll"); + try { + var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); + emitter.Emit (model, outputPath); + + using var pe = new PEReader (File.OpenRead (outputPath)); + var reader = pe.GetMetadataReader (); + + // Verify the assembly is loadable and has entries + Assert.True (pe.HasMetadata); + var entry = FindEntry (model, "my/app/GenericHolder"); + Assert.NotNull (entry); + + // Verify assembly attributes were emitted + var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); + Assert.NotEmpty (asmAttrs); + } finally { + CleanUpDir (outputPath); + } + } + + } + + public class PeBlobValidation + { + + [Fact] + public void FullPipeline_Mixed2ArgAnd3Arg_BothSurviveRoundTrip () + { + // java/lang/Object → essential → 2-arg unconditional + var objectPeer = FindFixtureByJavaName ("java/lang/Object"); + // android/app/Activity → MCW → 3-arg trimmable + var activityPeer = FindFixtureByJavaName ("android/app/Activity"); + + var model = BuildModel (new [] { objectPeer, activityPeer }, "MixedBlob"); + Assert.Equal (2, model.Entries.Count); + + var outputPath = Path.Combine (Path.GetTempPath (), $"mixedblob-{Guid.NewGuid ():N}", "MixedBlob.dll"); + try { + var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); + emitter.Emit (model, outputPath); + + using var pe = new PEReader (File.OpenRead (outputPath)); + var reader = pe.GetMetadataReader (); + + var attrs = ReadAllTypeMapAttributeBlobs (reader); + Assert.Equal (2, attrs.Count); + + // Find the 2-arg (unconditional) entry + var unconditional = attrs.FirstOrDefault (a => a.jniName == "java/lang/Object"); + Assert.NotNull (unconditional.jniName); + Assert.Null (unconditional.targetRef); // 2-arg: no target + + // Find the 3-arg (trimmable) entry + var trimmable = attrs.FirstOrDefault (a => a.jniName == "android/app/Activity"); + Assert.NotNull (trimmable.jniName); + Assert.NotNull (trimmable.targetRef); // 3-arg: has target + Assert.Contains ("Android.App.Activity", trimmable.targetRef!); + } finally { + CleanUpDir (outputPath); + } + } + + [Fact] + public void FullPipeline_EssentialType_Emits2ArgAttribute () + { + // java/lang/Object is essential → unconditional 2-arg attribute + var peer = FindFixtureByJavaName ("java/lang/Object"); + var model = BuildModel (new [] { peer }, "Blob2Arg"); + Assert.Single (model.Entries); + Assert.True (model.Entries [0].IsUnconditional); + + var outputPath = Path.Combine (Path.GetTempPath (), $"blob2arg-{Guid.NewGuid ():N}", "Blob2Arg.dll"); + try { + var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); + emitter.Emit (model, outputPath); + + using var pe = new PEReader (File.OpenRead (outputPath)); + var reader = pe.GetMetadataReader (); + var (jniName, proxyRef, targetRef) = ReadFirstTypeMapAttributeBlob (reader); + + Assert.Equal ("java/lang/Object", jniName); + Assert.NotNull (proxyRef); + Assert.Contains ("Java_Lang_Object_Proxy", proxyRef!); + // 2-arg: no target type + Assert.Null (targetRef); + } finally { + CleanUpDir (outputPath); + } + } + + [Fact] + public void FullPipeline_McwBinding_Emits3ArgAttribute () + { + // android/app/Activity is MCW → trimmable 3-arg attribute + var peer = FindFixtureByJavaName ("android/app/Activity"); + var model = BuildModel (new [] { peer }, "Blob3Arg"); + Assert.Single (model.Entries); + Assert.False (model.Entries [0].IsUnconditional); + + var outputPath = Path.Combine (Path.GetTempPath (), $"blob3arg-{Guid.NewGuid ():N}", "Blob3Arg.dll"); + try { + var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); + emitter.Emit (model, outputPath); + + using var pe = new PEReader (File.OpenRead (outputPath)); + var reader = pe.GetMetadataReader (); + var (jniName, proxyRef, targetRef) = ReadFirstTypeMapAttributeBlob (reader); + + Assert.Equal ("android/app/Activity", jniName); + Assert.NotNull (proxyRef); + Assert.Contains ("Android_App_Activity_Proxy", proxyRef!); + // 3-arg: has target type + Assert.NotNull (targetRef); + Assert.Contains ("Android.App.Activity", targetRef!); + } finally { + CleanUpDir (outputPath); + } + } + + [Fact] + public void FullPipeline_UserAcw_Emits2ArgAttribute () + { + // my/app/MainActivity is user ACW → unconditional 2-arg + var peer = FindFixtureByJavaName ("my/app/MainActivity"); + var model = BuildModel (new [] { peer }, "BlobAcw"); + Assert.Single (model.Entries); + Assert.True (model.Entries [0].IsUnconditional); + + var outputPath = Path.Combine (Path.GetTempPath (), $"blobacw-{Guid.NewGuid ():N}", "BlobAcw.dll"); + try { + var emitter = new TypeMapAssemblyEmitter (new Version (11, 0, 0, 0)); + emitter.Emit (model, outputPath); + + using var pe = new PEReader (File.OpenRead (outputPath)); + var reader = pe.GetMetadataReader (); + var (jniName, proxyRef, targetRef) = ReadFirstTypeMapAttributeBlob (reader); + + Assert.Equal ("my/app/MainActivity", jniName); + Assert.Null (targetRef); // unconditional → no target + } finally { + CleanUpDir (outputPath); + } + } + + } + + public class DeterminismTests + { + + [Fact] + public void Build_SameInput_ProducesDeterministicOutput () + { + var peers = ScanFixtures (); + + var model1 = BuildModel (peers, "DetTest"); + var model2 = BuildModel (peers, "DetTest"); + + Assert.Equal (model1.Entries.Count, model2.Entries.Count); + for (int i = 0; i < model1.Entries.Count; i++) { + Assert.Equal (model1.Entries [i].JniName, model2.Entries [i].JniName); + Assert.Equal (model1.Entries [i].ProxyTypeReference, model2.Entries [i].ProxyTypeReference); + Assert.Equal (model1.Entries [i].TargetTypeReference, model2.Entries [i].TargetTypeReference); + } + } + + } + + /// + /// Reads the first TypeMap assembly-level attribute blob and returns (jniName, proxyRef, targetRef). + /// targetRef is null for 2-arg attributes. + /// + static (string? jniName, string? proxyRef, string? targetRef) ReadFirstTypeMapAttributeBlob (MetadataReader reader) + { + var all = ReadAllTypeMapAttributeBlobs (reader); + if (all.Count == 0) { + throw new InvalidOperationException ("No TypeMap attribute found on assembly"); + } + return all [0]; + } + + /// + /// Reads TypeMap attribute blobs from a PE assembly's metadata. + /// + /// NOTE: This is a PE-level integration test helper, not a primary unit test mechanism. + /// The model-level tests (which verify TypeMapAssemblyData directly) are the main unit tests. + /// These PE round-trip tests exist to catch encoding bugs in the emitter and to verify that + /// the full scan→model→emit pipeline produces a valid, loadable assembly. + /// + /// The distinction between TypeMap and IgnoresAccessChecksTo attributes relies on + /// attr.Constructor.Kind: TypeMap attributes reference their ctor via MemberReference + /// (because the attribute type is a TypeSpec — generic), while IgnoresAccessChecksTo + /// uses MethodDefinition (the attribute type is defined in the same assembly as a TypeDef). + /// If this logic breaks, the test will either fail to find TypeMap attributes or + /// misidentify IgnoresAccessChecksTo as TypeMap — both cause obvious assertion failures. + /// + static List<(string? jniName, string? proxyRef, string? targetRef)> ReadAllTypeMapAttributeBlobs (MetadataReader reader) + { + var result = new List<(string?, string?, string?)> (); + var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); + foreach (var attrHandle in asmAttrs) { + var attr = reader.GetCustomAttribute (attrHandle); + // Skip IgnoresAccessChecksTo attributes (their ctor is a MethodDefinition, not MemberRef) + if (attr.Constructor.Kind == HandleKind.MethodDefinition) + continue; + + var blobReader = reader.GetBlobReader (attr.Value); + ushort prolog = blobReader.ReadUInt16 (); + if (prolog != 1) + continue; + + string? jniName = blobReader.ReadSerializedString (); + string? proxyRef = blobReader.ReadSerializedString (); + + // Try to read third arg (target type) — if remaining bytes are just NumNamed (2 bytes), it's 2-arg + string? targetRef = null; + if (blobReader.RemainingBytes > 2) { + targetRef = blobReader.ReadSerializedString (); + } + + result.Add ((jniName, proxyRef, targetRef)); + } + return result; + } + + static void CleanUpDir (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/Manifest/ManifestTypeInfoTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Manifest/ManifestTypeInfoTests.cs new file mode 100644 index 00000000000..e898200bddb --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Manifest/ManifestTypeInfoTests.cs @@ -0,0 +1,155 @@ +using System.Collections.Generic; +using Xunit; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; + +public class ManifestTypeInfoTests +{ + [Theory] + [InlineData (0)] // None + [InlineData (1)] // Activity + [InlineData (2)] // Service + [InlineData (3)] // BroadcastReceiver + [InlineData (4)] // ContentProvider + [InlineData (5)] // Application + [InlineData (6)] // Instrumentation + public void ComponentKind_AllValuesValid (int kindInt) + { + var kind = (ManifestComponentKind)kindInt; + var info = new ManifestTypeInfo { ComponentKind = kind }; + Assert.Equal (kind, info.ComponentKind); + } + + [Fact] + public void DefaultValues_AreCorrect () + { + var info = new ManifestTypeInfo (); + + Assert.Equal ("", info.FullName); + Assert.Equal ("", info.Namespace); + Assert.Equal ("", info.JavaName); + Assert.Equal ("", info.CompatJavaName); + Assert.False (info.IsAbstract); + Assert.False (info.HasPublicParameterlessConstructor); + Assert.Equal (ManifestComponentKind.None, info.ComponentKind); + Assert.Null (info.ComponentAttribute); + Assert.Empty (info.IntentFilters); + Assert.Empty (info.MetaDataEntries); + Assert.Empty (info.PropertyAttributes); + Assert.Null (info.LayoutAttribute); + Assert.Empty (info.GrantUriPermissions); + } + + [Fact] + public void AllProperties_CanBeSet () + { + var componentAttr = new ComponentAttributeInfo { + AttributeType = "Android.App.ActivityAttribute", + Properties = new Dictionary { { "MainLauncher", true } }, + }; + + var intentFilter = new ComponentAttributeInfo { + AttributeType = "Android.App.IntentFilterAttribute", + ConstructorArguments = new object [] { new string [] { "android.intent.action.MAIN" } }, + }; + + var metaData = new ComponentAttributeInfo { + AttributeType = "Android.App.MetaDataAttribute", + ConstructorArguments = new object [] { "key" }, + Properties = new Dictionary { { "Value", "val" } }, + }; + + var layout = new ComponentAttributeInfo { + AttributeType = "Android.App.LayoutAttribute", + Properties = new Dictionary { { "DefaultWidth", "500dp" } }, + }; + + var property = new ComponentAttributeInfo { + AttributeType = "Android.App.PropertyAttribute", + ConstructorArguments = new object [] { "prop.name" }, + Properties = new Dictionary { { "Value", "prop.val" } }, + }; + + var grantUri = new ComponentAttributeInfo { + AttributeType = "Android.Content.GrantUriPermissionAttribute", + Properties = new Dictionary { { "Path", "/data" } }, + }; + + var info = new ManifestTypeInfo { + FullName = "MyApp.MainActivity", + Namespace = "MyApp", + JavaName = "my.app.MainActivity", + CompatJavaName = "md5hash.MainActivity", + IsAbstract = false, + HasPublicParameterlessConstructor = true, + ComponentKind = ManifestComponentKind.Activity, + ComponentAttribute = componentAttr, + IntentFilters = new [] { intentFilter }, + MetaDataEntries = new [] { metaData }, + PropertyAttributes = new [] { property }, + LayoutAttribute = layout, + GrantUriPermissions = new [] { grantUri }, + }; + + Assert.Equal ("MyApp.MainActivity", info.FullName); + Assert.Equal ("MyApp", info.Namespace); + Assert.Equal ("my.app.MainActivity", info.JavaName); + Assert.Equal ("md5hash.MainActivity", info.CompatJavaName); + Assert.False (info.IsAbstract); + Assert.True (info.HasPublicParameterlessConstructor); + Assert.Equal (ManifestComponentKind.Activity, info.ComponentKind); + Assert.NotNull (info.ComponentAttribute); + Assert.Single (info.IntentFilters); + Assert.Single (info.MetaDataEntries); + Assert.Single (info.PropertyAttributes); + Assert.NotNull (info.LayoutAttribute); + Assert.Single (info.GrantUriPermissions); + } + + [Fact] + public void ComponentAttributeInfo_DefaultValues () + { + var attr = new ComponentAttributeInfo (); + + Assert.Equal ("", attr.AttributeType); + Assert.NotNull (attr.Properties); + Assert.Empty (attr.Properties); + Assert.NotNull (attr.ConstructorArguments); + Assert.Empty (attr.ConstructorArguments); + } + + [Fact] + public void ComponentAttributeInfo_CanStoreVariousPropertyTypes () + { + var props = new Dictionary { + { "StringProp", "hello" }, + { "BoolProp", true }, + { "IntProp", 42 }, + { "ArrayProp", new string [] { "a", "b" } }, + { "TypeProp", "MyApp.SomeType" }, // Type-valued props stored as type name strings + }; + + var attr = new ComponentAttributeInfo { + AttributeType = "Test.Attribute", + Properties = props, + }; + + Assert.Equal ("hello", attr.Properties ["StringProp"]); + Assert.Equal (true, attr.Properties ["BoolProp"]); + Assert.Equal (42, attr.Properties ["IntProp"]); + Assert.IsType (attr.Properties ["ArrayProp"]); + Assert.Equal ("MyApp.SomeType", attr.Properties ["TypeProp"]); + } + + [Fact] + public void IManifestTypeInfo_ImplementedByManifestTypeInfo () + { + IManifestTypeInfo info = new ManifestTypeInfo { + FullName = "Test.Type", + ComponentKind = ManifestComponentKind.Service, + }; + + Assert.Equal ("Test.Type", info.FullName); + Assert.Equal (ManifestComponentKind.Service, info.ComponentKind); + } +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Manifest/ScannerManifestTypeInfoAdapterTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Manifest/ScannerManifestTypeInfoAdapterTests.cs new file mode 100644 index 00000000000..c62a54518b0 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Manifest/ScannerManifestTypeInfoAdapterTests.cs @@ -0,0 +1,182 @@ +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 ScannerManifestTypeInfoAdapterTests +{ + static string TestFixtureAssemblyPath { + get { + var testAssemblyDir = Path.GetDirectoryName (typeof (ScannerManifestTypeInfoAdapterTests).Assembly.Location)!; + var fixtureAssembly = Path.Combine (testAssemblyDir, "TestFixtures.dll"); + Assert.True (File.Exists (fixtureAssembly), + $"TestFixtures.dll not found at {fixtureAssembly}. Ensure the TestFixtures project builds."); + return fixtureAssembly; + } + } + + List ScanAndConvert () + { + return ScannerManifestTypeInfoAdapter.ScanAndConvert (new [] { TestFixtureAssemblyPath }); + } + + IManifestTypeInfo FindByFullName (List infos, string fullName) + { + var info = infos.FirstOrDefault (i => i.FullName == fullName); + Assert.NotNull (info); + return info; + } + + [Fact] + public void ConvertsActivityType () + { + var infos = ScanAndConvert (); + var activity = FindByFullName (infos, "MyApp.MainActivity"); + + Assert.Equal (ManifestComponentKind.Activity, activity.ComponentKind); + Assert.Equal ("my.app.MainActivity", activity.JavaName); + Assert.False (activity.IsAbstract); + Assert.NotNull (activity.ComponentAttribute); + Assert.True ((bool)activity.ComponentAttribute!.Properties ["MainLauncher"]); + } + + [Fact] + public void ConvertsServiceType () + { + var infos = ScanAndConvert (); + var service = FindByFullName (infos, "MyApp.MyService"); + + Assert.Equal (ManifestComponentKind.Service, service.ComponentKind); + Assert.NotNull (service.ComponentAttribute); + Assert.Equal ("my.app.MyService", service.ComponentAttribute!.Properties ["Name"]); + } + + [Fact] + public void ConvertsBroadcastReceiverType () + { + var infos = ScanAndConvert (); + var receiver = FindByFullName (infos, "MyApp.MyReceiver"); + + Assert.Equal (ManifestComponentKind.BroadcastReceiver, receiver.ComponentKind); + } + + [Fact] + public void ConvertsContentProviderType () + { + var infos = ScanAndConvert (); + var provider = FindByFullName (infos, "MyApp.MyProvider"); + + Assert.Equal (ManifestComponentKind.ContentProvider, provider.ComponentKind); + Assert.NotEmpty (provider.GrantUriPermissions); + } + + [Fact] + public void ConvertsApplicationType () + { + var infos = ScanAndConvert (); + var app = FindByFullName (infos, "MyApp.MyApplication"); + + Assert.Equal (ManifestComponentKind.Application, app.ComponentKind); + } + + [Fact] + public void ConvertsInstrumentationType () + { + var infos = ScanAndConvert (); + var inst = FindByFullName (infos, "MyApp.MyInstrumentation"); + + Assert.Equal (ManifestComponentKind.Instrumentation, inst.ComponentKind); + } + + [Fact] + public void NonComponentType_HasKindNone () + { + var infos = ScanAndConvert (); + var helper = FindByFullName (infos, "MyApp.MyHelper"); + + Assert.Equal (ManifestComponentKind.None, helper.ComponentKind); + Assert.Null (helper.ComponentAttribute); + } + + [Fact] + public void ExcludesDoNotGenerateAcwTypes () + { + var infos = ScanAndConvert (); + + // MCW types with DoNotGenerateAcw should not be in the result + Assert.DoesNotContain (infos, i => i.FullName == "Java.Lang.Object"); + Assert.DoesNotContain (infos, i => i.FullName == "Android.App.Activity"); + } + + [Fact] + public void PreservesIntentFilters () + { + var infos = ScanAndConvert (); + var deepLink = FindByFullName (infos, "MyApp.DeepLinkActivity"); + + Assert.Equal (2, deepLink.IntentFilters.Count); + } + + [Fact] + public void PreservesMetaData () + { + var infos = ScanAndConvert (); + var deepLink = FindByFullName (infos, "MyApp.DeepLinkActivity"); + + Assert.Equal (2, deepLink.MetaDataEntries.Count); + } + + [Fact] + public void PreservesLayoutAttribute () + { + var infos = ScanAndConvert (); + var deepLink = FindByFullName (infos, "MyApp.DeepLinkActivity"); + + Assert.NotNull (deepLink.LayoutAttribute); + Assert.Equal ("500dp", deepLink.LayoutAttribute!.Properties ["DefaultWidth"]); + } + + [Fact] + public void PreservesPropertyAttributes () + { + var infos = ScanAndConvert (); + var deepLink = FindByFullName (infos, "MyApp.DeepLinkActivity"); + + Assert.Single (deepLink.PropertyAttributes); + } + + [Fact] + public void PreservesGrantUriPermissions () + { + var infos = ScanAndConvert (); + var provider = FindByFullName (infos, "MyApp.MyProvider"); + + Assert.Equal (2, provider.GrantUriPermissions.Count); + } + + [Fact] + public void AbstractType_IsAbstractTrue () + { + var infos = ScanAndConvert (); + var baseActivity = FindByFullName (infos, "MyApp.BaseActivity"); + + Assert.True (baseActivity.IsAbstract); + Assert.Equal (ManifestComponentKind.Activity, baseActivity.ComponentKind); + } + + [Fact] + public void AllActivityProperties_Preserved () + { + var infos = ScanAndConvert (); + var deepLink = FindByFullName (infos, "MyApp.DeepLinkActivity"); + + Assert.NotNull (deepLink.ComponentAttribute); + Assert.Equal ("my.app.DeepLinkActivity", deepLink.ComponentAttribute!.Properties ["Name"]); + Assert.Equal ("@style/AppTheme", deepLink.ComponentAttribute.Properties ["Theme"]); + Assert.True ((bool)deepLink.ComponentAttribute.Properties ["Exported"]); + } +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj new file mode 100644 index 00000000000..6370a77e680 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj @@ -0,0 +1,38 @@ + + + + $(DotNetStableTargetFramework) + latest + enable + false + Microsoft.Android.Sdk.TrimmableTypeMap.Tests + + + + + + + + + + + + + + + + + + false + + + + + + + <_TestFixtureFiles Include="TestFixtures\bin\$(Configuration)\$(DotNetStableTargetFramework)\TestFixtures.dll" /> + + + + + diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ComponentAttributeExtractorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ComponentAttributeExtractorTests.cs new file mode 100644 index 00000000000..92974290ab5 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/ComponentAttributeExtractorTests.cs @@ -0,0 +1,350 @@ +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 ComponentAttributeExtractorTests +{ + static string TestFixtureAssemblyPath { + get { + var testAssemblyDir = Path.GetDirectoryName (typeof (ComponentAttributeExtractorTests).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; + } + } + + (MetadataReader reader, PEReader peReader) OpenFixtureAssembly () + { + var peReader = new PEReader (File.OpenRead (TestFixtureAssemblyPath)); + var reader = peReader.GetMetadataReader (); + return (reader, peReader); + } + + TypeDefinitionHandle FindType (MetadataReader reader, string fullName) + { + foreach (var typeHandle in reader.TypeDefinitions) { + var typeDef = reader.GetTypeDefinition (typeHandle); + var ns = reader.GetString (typeDef.Namespace); + var name = reader.GetString (typeDef.Name); + var fn = ns.Length > 0 ? ns + "." + name : name; + if (fn == fullName) + return typeHandle; + } + Assert.Fail ($"Type '{fullName}' not found in TestFixtures.dll"); + return default; + } + + [Fact] + public void ExtractsActivityAttribute () + { + var (reader, pe) = OpenFixtureAssembly (); + using (pe) { + var typeHandle = FindType (reader, "MyApp.MainActivity"); + var data = ComponentAttributeExtractor.Extract (reader, typeHandle); + + Assert.Equal (ManifestComponentKind.Activity, data.ComponentKind); + Assert.NotNull (data.ComponentAttribute); + Assert.Equal ("Android.App.ActivityAttribute", data.ComponentAttribute!.AttributeType); + + var props = data.ComponentAttribute.Properties; + Assert.True ((bool)props ["MainLauncher"]); + Assert.Equal ("My App", props ["Label"]); + Assert.Equal ("my.app.MainActivity", props ["Name"]); + } + } + + [Fact] + public void ExtractsActivityWithIntentFilters () + { + var (reader, pe) = OpenFixtureAssembly (); + using (pe) { + var typeHandle = FindType (reader, "MyApp.DeepLinkActivity"); + var data = ComponentAttributeExtractor.Extract (reader, typeHandle); + + Assert.Equal (ManifestComponentKind.Activity, data.ComponentKind); + Assert.Equal (2, data.IntentFilters.Count); + + // First intent filter: VIEW action with deep link data + var viewFilter = data.IntentFilters [0]; + Assert.Equal ("Android.App.IntentFilterAttribute", viewFilter.AttributeType); + Assert.Single (viewFilter.ConstructorArguments); + var actions = (string [])viewFilter.ConstructorArguments [0]; + Assert.Contains ("android.intent.action.VIEW", actions); + var categories = (string [])viewFilter.Properties ["Categories"]; + Assert.Contains ("android.intent.category.BROWSABLE", categories); + Assert.Equal ("https", viewFilter.Properties ["DataScheme"]); + Assert.Equal ("example.com", viewFilter.Properties ["DataHost"]); + Assert.True ((bool)viewFilter.Properties ["AutoVerify"]); + + // Second intent filter: custom action + var customFilter = data.IntentFilters [1]; + var customActions = (string [])customFilter.ConstructorArguments [0]; + Assert.Contains ("my.app.CUSTOM_ACTION", customActions); + } + } + + [Fact] + public void ExtractsActivityWithMetaData () + { + var (reader, pe) = OpenFixtureAssembly (); + using (pe) { + var typeHandle = FindType (reader, "MyApp.DeepLinkActivity"); + var data = ComponentAttributeExtractor.Extract (reader, typeHandle); + + Assert.Equal (2, data.MetaDataEntries.Count); + + var apiKey = data.MetaDataEntries.First (m => + m.ConstructorArguments.Count > 0 && + m.ConstructorArguments [0].ToString () == "com.google.android.geo.API_KEY"); + Assert.Equal ("test-api-key", apiKey.Properties ["Value"]); + + var gmsVersion = data.MetaDataEntries.First (m => + m.ConstructorArguments.Count > 0 && + m.ConstructorArguments [0].ToString () == "com.google.android.gms.version"); + Assert.Equal ("@integer/google_play_services_version", gmsVersion.Properties ["Resource"]); + } + } + + [Fact] + public void ExtractsActivityWithLayout () + { + var (reader, pe) = OpenFixtureAssembly (); + using (pe) { + var typeHandle = FindType (reader, "MyApp.DeepLinkActivity"); + var data = ComponentAttributeExtractor.Extract (reader, typeHandle); + + Assert.NotNull (data.LayoutAttribute); + Assert.Equal ("500dp", data.LayoutAttribute!.Properties ["DefaultWidth"]); + Assert.Equal ("600dp", data.LayoutAttribute.Properties ["DefaultHeight"]); + Assert.Equal ("center", data.LayoutAttribute.Properties ["Gravity"]); + Assert.Equal ("300dp", data.LayoutAttribute.Properties ["MinWidth"]); + Assert.Equal ("400dp", data.LayoutAttribute.Properties ["MinHeight"]); + } + } + + [Fact] + public void ExtractsActivityWithProperty () + { + var (reader, pe) = OpenFixtureAssembly (); + using (pe) { + var typeHandle = FindType (reader, "MyApp.DeepLinkActivity"); + var data = ComponentAttributeExtractor.Extract (reader, typeHandle); + + Assert.Single (data.PropertyAttributes); + var prop = data.PropertyAttributes [0]; + Assert.Equal ("custom.prop", prop.ConstructorArguments [0]); + Assert.Equal ("custom-value", prop.Properties ["Value"]); + } + } + + [Fact] + public void ExtractsServiceAttribute () + { + var (reader, pe) = OpenFixtureAssembly (); + using (pe) { + var typeHandle = FindType (reader, "MyApp.MyService"); + var data = ComponentAttributeExtractor.Extract (reader, typeHandle); + + Assert.Equal (ManifestComponentKind.Service, data.ComponentKind); + Assert.NotNull (data.ComponentAttribute); + Assert.Equal ("my.app.MyService", data.ComponentAttribute!.Properties ["Name"]); + Assert.True ((bool)data.ComponentAttribute.Properties ["Exported"]); + Assert.Equal ("my.app.BIND_SERVICE", data.ComponentAttribute.Properties ["Permission"]); + Assert.True ((bool)data.ComponentAttribute.Properties ["IsolatedProcess"]); + } + } + + [Fact] + public void ExtractsServiceWithIntentFilter () + { + var (reader, pe) = OpenFixtureAssembly (); + using (pe) { + var typeHandle = FindType (reader, "MyApp.MyService"); + var data = ComponentAttributeExtractor.Extract (reader, typeHandle); + + Assert.Single (data.IntentFilters); + var actions = (string [])data.IntentFilters [0].ConstructorArguments [0]; + Assert.Contains ("my.app.START_SERVICE", actions); + } + } + + [Fact] + public void ExtractsBroadcastReceiverAttribute () + { + var (reader, pe) = OpenFixtureAssembly (); + using (pe) { + var typeHandle = FindType (reader, "MyApp.MyReceiver"); + var data = ComponentAttributeExtractor.Extract (reader, typeHandle); + + Assert.Equal (ManifestComponentKind.BroadcastReceiver, data.ComponentKind); + Assert.NotNull (data.ComponentAttribute); + Assert.Equal ("my.app.MyReceiver", data.ComponentAttribute!.Properties ["Name"]); + Assert.True ((bool)data.ComponentAttribute.Properties ["Exported"]); + Assert.Equal ("my.app.RECEIVE_BROADCAST", data.ComponentAttribute.Properties ["Permission"]); + } + } + + [Fact] + public void ExtractsContentProviderAttribute () + { + var (reader, pe) = OpenFixtureAssembly (); + using (pe) { + var typeHandle = FindType (reader, "MyApp.MyProvider"); + var data = ComponentAttributeExtractor.Extract (reader, typeHandle); + + Assert.Equal (ManifestComponentKind.ContentProvider, data.ComponentKind); + Assert.NotNull (data.ComponentAttribute); + Assert.Equal ("my.app.MyProvider", data.ComponentAttribute!.Properties ["Name"]); + + // Constructor argument: authorities string[] + Assert.Single (data.ComponentAttribute.ConstructorArguments); + var authorities = (string [])data.ComponentAttribute.ConstructorArguments [0]; + Assert.Contains ("my.app.provider", authorities); + + Assert.True ((bool)data.ComponentAttribute.Properties ["Exported"]); + Assert.True ((bool)data.ComponentAttribute.Properties ["GrantUriPermissions"]); + } + } + + [Fact] + public void ExtractsContentProviderWithGrantUri () + { + var (reader, pe) = OpenFixtureAssembly (); + using (pe) { + var typeHandle = FindType (reader, "MyApp.MyProvider"); + var data = ComponentAttributeExtractor.Extract (reader, typeHandle); + + Assert.Equal (2, data.GrantUriPermissions.Count); + Assert.Contains (data.GrantUriPermissions, g => g.Properties.ContainsKey ("Path") && (string)g.Properties ["Path"] == "/data"); + Assert.Contains (data.GrantUriPermissions, g => g.Properties.ContainsKey ("PathPrefix") && (string)g.Properties ["PathPrefix"] == "/files"); + } + } + + [Fact] + public void ExtractsApplicationAttribute () + { + var (reader, pe) = OpenFixtureAssembly (); + using (pe) { + var typeHandle = FindType (reader, "MyApp.MyApplication"); + var data = ComponentAttributeExtractor.Extract (reader, typeHandle); + + Assert.Equal (ManifestComponentKind.Application, data.ComponentKind); + Assert.NotNull (data.ComponentAttribute); + Assert.Equal ("my.app.MyApplication", data.ComponentAttribute!.Properties ["Name"]); + Assert.True ((bool)data.ComponentAttribute.Properties ["Debuggable"]); + Assert.True ((bool)data.ComponentAttribute.Properties ["AllowBackup"]); + Assert.True ((bool)data.ComponentAttribute.Properties ["SupportsRtl"]); + Assert.Equal ("@style/AppTheme", data.ComponentAttribute.Properties ["Theme"]); + Assert.Equal ("My Application", data.ComponentAttribute.Properties ["Label"]); + Assert.Equal ("@mipmap/ic_launcher", data.ComponentAttribute.Properties ["Icon"]); + } + } + + [Fact] + public void ExtractsApplicationAttribute_TypeProperties () + { + var (reader, pe) = OpenFixtureAssembly (); + using (pe) { + var typeHandle = FindType (reader, "MyApp.MyApplication"); + var data = ComponentAttributeExtractor.Extract (reader, typeHandle); + + // Type-valued properties are decoded as type name strings by CustomAttributeTypeProvider + Assert.True (data.ComponentAttribute!.Properties.ContainsKey ("BackupAgent")); + Assert.True (data.ComponentAttribute.Properties.ContainsKey ("ManageSpaceActivity")); + } + } + + [Fact] + public void ExtractsApplicationWithMetaData () + { + var (reader, pe) = OpenFixtureAssembly (); + using (pe) { + var typeHandle = FindType (reader, "MyApp.MyApplication"); + var data = ComponentAttributeExtractor.Extract (reader, typeHandle); + + Assert.Single (data.MetaDataEntries); + Assert.Equal ("app.version", data.MetaDataEntries [0].ConstructorArguments [0]); + Assert.Equal ("2.0", data.MetaDataEntries [0].Properties ["Value"]); + } + } + + [Fact] + public void ExtractsInstrumentationAttribute () + { + var (reader, pe) = OpenFixtureAssembly (); + using (pe) { + var typeHandle = FindType (reader, "MyApp.MyInstrumentation"); + var data = ComponentAttributeExtractor.Extract (reader, typeHandle); + + Assert.Equal (ManifestComponentKind.Instrumentation, data.ComponentKind); + Assert.NotNull (data.ComponentAttribute); + Assert.Equal ("my.app.MyInstrumentation", data.ComponentAttribute!.Properties ["Name"]); + Assert.Equal ("my.app", data.ComponentAttribute.Properties ["TargetPackage"]); + Assert.True ((bool)data.ComponentAttribute.Properties ["FunctionalTest"]); + Assert.True ((bool)data.ComponentAttribute.Properties ["HandleProfiling"]); + Assert.Equal ("Test Runner", data.ComponentAttribute.Properties ["Label"]); + } + } + + [Fact] + public void NoComponentAttribute_ReturnsNone () + { + var (reader, pe) = OpenFixtureAssembly (); + using (pe) { + var typeHandle = FindType (reader, "MyApp.MyHelper"); + var data = ComponentAttributeExtractor.Extract (reader, typeHandle); + + Assert.Equal (ManifestComponentKind.None, data.ComponentKind); + Assert.Null (data.ComponentAttribute); + Assert.Empty (data.IntentFilters); + Assert.Empty (data.MetaDataEntries); + } + } + + [Fact] + public void McwBindingType_NoComponentAttribute () + { + var (reader, pe) = OpenFixtureAssembly (); + using (pe) { + var typeHandle = FindType (reader, "Android.App.Activity"); + var data = ComponentAttributeExtractor.Extract (reader, typeHandle); + + Assert.Equal (ManifestComponentKind.None, data.ComponentKind); + Assert.Null (data.ComponentAttribute); + } + } + + [Fact] + public void BooleanProperties_DecodedCorrectly () + { + var (reader, pe) = OpenFixtureAssembly (); + using (pe) { + var typeHandle = FindType (reader, "MyApp.DeepLinkActivity"); + var data = ComponentAttributeExtractor.Extract (reader, typeHandle); + + // Exported is a boolean property + Assert.True ((bool)data.ComponentAttribute!.Properties ["Exported"]); + } + } + + [Fact] + public void StringArrayProperties_DecodedCorrectly () + { + var (reader, pe) = OpenFixtureAssembly (); + using (pe) { + var typeHandle = FindType (reader, "MyApp.MyProvider"); + var data = ComponentAttributeExtractor.Extract (reader, typeHandle); + + // ContentProvider constructor takes string[] authorities + var authorities = (string [])data.ComponentAttribute!.ConstructorArguments [0]; + Assert.Single (authorities); + Assert.Equal ("my.app.provider", authorities [0]); + } + } +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs new file mode 100644 index 00000000000..81aabe9ad74 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs @@ -0,0 +1,133 @@ +using System.Linq; +using Xunit; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; + +public partial class JavaPeerScannerTests +{ + [Theory] + [InlineData ("android/app/Activity", "OnCreate", "onCreate", "(Landroid/os/Bundle;)V")] + [InlineData ("android/app/Activity", "OnStart", "onStart", "()V")] + [InlineData ("my/app/MainActivity", "OnCreate", "onCreate", "(Landroid/os/Bundle;)V")] + [InlineData ("my/app/AbstractBase", "DoWork", "doWork", "()V")] + [InlineData ("java/lang/Throwable", "Message", "getMessage", "()Ljava/lang/String;")] + [InlineData ("my/app/TouchHandler", "OnTouch", "onTouch", "(Landroid/view/View;I)Z")] + [InlineData ("my/app/TouchHandler", "OnFocusChange", "onFocusChange", "(Landroid/view/View;Z)V")] + [InlineData ("my/app/TouchHandler", "OnScroll", "onScroll", "(IFJD)V")] + [InlineData ("my/app/TouchHandler", "SetItems", "setItems", "([Ljava/lang/String;)V")] + public void Scan_MarshalMethod_HasCorrectSignature (string javaName, string managedName, string jniName, string jniSig) + { + var peers = ScanFixtures (); + var method = FindByJavaName (peers, javaName) + .MarshalMethods.FirstOrDefault (m => m.ManagedMethodName == managedName || m.JniName == jniName); + Assert.NotNull (method); + Assert.Equal (jniName, method.JniName); + Assert.Equal (jniSig, method.JniSignature); + } + + [Fact] + public void Scan_MarshalMethod_ConstructorsAndSpecialCases () + { + var peers = ScanFixtures (); + + var ctors = FindByJavaName (peers, "my/app/CustomView") + .MarshalMethods.Where (m => m.IsConstructor).ToList (); + Assert.Equal (2, ctors.Count); + Assert.Equal ("()V", ctors [0].JniSignature); + Assert.Equal ("(Landroid/content/Context;)V", ctors [1].JniSignature); + + Assert.DoesNotContain (FindByJavaName (peers, "my/app/MyHelper").MarshalMethods, m => m.IsConstructor); + + var exportMethod = FindByJavaName (peers, "my/app/ExportExample").MarshalMethods.Single (); + Assert.Equal ("myExportedMethod", exportMethod.JniName); + Assert.Null (exportMethod.Connector); + + var onStart = FindByJavaName (peers, "android/app/Activity") + .MarshalMethods.FirstOrDefault (m => m.JniName == "onStart"); + Assert.NotNull (onStart); + Assert.Equal ("", onStart.Connector); + + var onClick = FindByManagedName (peers, "Android.Views.IOnClickListener") + .MarshalMethods.FirstOrDefault (m => m.JniName == "onClick"); + Assert.NotNull (onClick); + Assert.Equal ("(Landroid/view/View;)V", onClick.JniSignature); + + Assert.Equal ("Android.Views.IOnClickListenerInvoker", + FindByManagedName (peers, "Android.Views.IOnClickListener").InvokerTypeName); + } + + [Theory] + [InlineData ("android/app/Activity", "Android.App.Activity")] + [InlineData ("my/app/SimpleActivity", "Android.App.Activity")] + [InlineData ("my/app/MyButton", "MyApp.MyButton")] + public void Scan_ActivationCtor_InheritsFromNearestBase (string javaName, string expectedDeclaringType) + { + var peers = ScanFixtures (); + var peer = FindByJavaName (peers, javaName); + Assert.NotNull (peer.ActivationCtor); + Assert.Equal (expectedDeclaringType, peer.ActivationCtor.DeclaringTypeName); + } + + [Theory] + [InlineData ("java/lang/Object", null)] + [InlineData ("android/app/Activity", "java/lang/Object")] + [InlineData ("my/app/MainActivity", "android/app/Activity")] + [InlineData ("java/lang/Throwable", "java/lang/Object")] + [InlineData ("java/lang/Exception", "java/lang/Throwable")] + [InlineData ("my/app/MyButton", "android/widget/Button")] + public void Scan_BaseJavaName_ResolvesCorrectly (string javaName, string? expectedBase) + { + var peers = ScanFixtures (); + Assert.Equal (expectedBase, FindByJavaName (peers, javaName).BaseJavaName); + } + + [Fact] + public void Scan_MultipleInterfaces_AllResolved () + { + var peers = ScanFixtures (); + + var multi = FindByJavaName (peers, "my/app/MultiInterfaceView"); + Assert.Contains ("android/view/View$OnClickListener", multi.ImplementedInterfaceJavaNames); + Assert.Contains ("android/view/View$OnLongClickListener", multi.ImplementedInterfaceJavaNames); + Assert.Equal (2, multi.ImplementedInterfaceJavaNames.Count); + + Assert.Contains ("android/view/View$OnClickListener", + FindByJavaName (peers, "my/app/ClickableView").ImplementedInterfaceJavaNames); + Assert.Empty (FindByJavaName (peers, "my/app/MyHelper").ImplementedInterfaceJavaNames); + } + + [Theory] + [InlineData ("android/app/Activity", "android/app/Activity")] + [InlineData ("my/app/MainActivity", "my/app/MainActivity")] + public void Scan_CompatJniName (string javaName, string expectedCompat) + { + var peers = ScanFixtures (); + Assert.Equal (expectedCompat, FindByJavaName (peers, javaName).CompatJniName); + } + + [Fact] + public void Scan_CompatJniName_UnregisteredType_UsesRawNamespace () + { + var peers = ScanFixtures (); + var unregistered = FindByManagedName (peers, "MyApp.UnregisteredHelper"); + Assert.StartsWith ("crc64", unregistered.JavaName); + Assert.Equal ("myapp/UnregisteredHelper", unregistered.CompatJniName); + } + + [Fact] + public void Scan_CustomJniNameProviderAttribute_UsesNameFromAttribute () + { + var peers = ScanFixtures (); + Assert.Equal ("com/example/CustomWidget", + FindByManagedName (peers, "MyApp.CustomWidget").JavaName); + } + + [Theory] + [InlineData ("my/app/Outer$Inner", "MyApp.Outer+Inner")] + [InlineData ("my/app/ICallback$Result", "MyApp.ICallback+Result")] + public void Scan_NestedType_IsDiscovered (string javaName, string managedName) + { + var peers = ScanFixtures (); + Assert.Equal (managedName, FindByJavaName (peers, javaName).ManagedTypeName); + } +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs new file mode 100644 index 00000000000..ec368ed6a44 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs @@ -0,0 +1,69 @@ +using System.Linq; +using Xunit; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; + +public partial class JavaPeerScannerTests +{ + [Fact] + public void Scan_GenericTypes_ResolveViaTypeSpecification () + { + var peers = ScanFixtures (); + Assert.Equal ("my/app/GenericBase", + FindByJavaName (peers, "my/app/ConcreteFromGeneric").BaseJavaName); + Assert.Contains ("my/app/IGenericCallback", + FindByJavaName (peers, "my/app/GenericCallbackImpl").ImplementedInterfaceJavaNames); + } + + [Fact] + public void Scan_ComponentOnlyBase_BothBaseAndDerivedDiscovered () + { + var peers = ScanFixtures (); + + var baseType = FindByJavaName (peers, "my/app/BaseActivityNoRegister"); + Assert.True (baseType.IsUnconditional); + Assert.Equal ("android/app/Activity", baseType.BaseJavaName); + + var derived = FindByManagedName (peers, "MyApp.DerivedFromComponentBase"); + Assert.StartsWith ("crc64", derived.JavaName); + } + + [Theory] + [InlineData ("MyApp.RegisteredParent+UnregisteredChild", "my/app/RegisteredParent_UnregisteredChild")] + [InlineData ("MyApp.DeepOuter+Middle+DeepInner", "my/app/DeepOuter_Middle_DeepInner")] + public void Scan_UnregisteredNestedType_UsesParentJniPrefix (string managedName, string expectedJavaName) + { + var peers = ScanFixtures (); + Assert.Equal (expectedJavaName, FindByManagedName (peers, managedName).JavaName); + } + + [Fact] + public void Scan_EmptyNamespace_Handled () + { + var peers = ScanFixtures (); + Assert.Equal ("GlobalType", FindByJavaName (peers, "my/app/GlobalType").ManagedTypeName); + Assert.Equal ("GlobalUnregisteredType", + FindByManagedName (peers, "GlobalUnregisteredType").CompatJniName); + } + + [Theory] + [InlineData ("MyApp.PlainActivitySubclass")] + [InlineData ("MyApp.UnnamedActivity")] + [InlineData ("MyApp.UnregisteredClickListener")] + [InlineData ("MyApp.UnregisteredExporter")] + public void Scan_UnregisteredType_DiscoveredWithCrc64Name (string managedName) + { + var peers = ScanFixtures (); + Assert.StartsWith ("crc64", FindByManagedName (peers, managedName).JavaName); + } + + [Fact] + public void Scan_ExportOnUnregisteredType_MethodDiscovered () + { + var peers = ScanFixtures (); + var exportMethod = FindByManagedName (peers, "MyApp.UnregisteredExporter") + .MarshalMethods.FirstOrDefault (m => m.JniName == "doExportedWork"); + Assert.NotNull (exportMethod); + Assert.Null (exportMethod.Connector); + } +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs new file mode 100644 index 00000000000..bb368215353 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs @@ -0,0 +1,919 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using Xunit; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; + +public class JavaPeerScannerTests +{ + static string TestFixtureAssemblyPath { + get { + var testAssemblyDir = Path.GetDirectoryName (typeof (JavaPeerScannerTests).Assembly.Location)!; + var fixtureAssembly = Path.Combine (testAssemblyDir, "TestFixtures.dll"); + Assert.True (File.Exists (fixtureAssembly), + $"TestFixtures.dll not found at {fixtureAssembly}. Ensure the TestFixtures project builds."); + return fixtureAssembly; + } + } + + List ScanFixtures () + { + using var scanner = new JavaPeerScanner (); + return scanner.Scan (new [] { TestFixtureAssemblyPath }); + } + + JavaPeerInfo FindByJavaName (List peers, string javaName) + { + var peer = peers.FirstOrDefault (p => p.JavaName == javaName); + Assert.NotNull (peer); + return peer; + } + + JavaPeerInfo FindByManagedName (List peers, string managedName) + { + var peer = peers.FirstOrDefault (p => p.ManagedTypeName == managedName); + Assert.NotNull (peer); + return peer; + } + + [Fact] + public void Scan_FindsAllJavaPeerTypes () + { + var peers = ScanFixtures (); + Assert.NotEmpty (peers); + // MCW types with [Register] + Assert.Contains (peers, p => p.JavaName == "java/lang/Object"); + Assert.Contains (peers, p => p.JavaName == "android/app/Activity"); + // User type with JNI name from [Activity(Name="...")] + Assert.Contains (peers, p => p.JavaName == "my/app/MainActivity"); + // Exception/Throwable hierarchy + Assert.Contains (peers, p => p.JavaName == "java/lang/Throwable"); + Assert.Contains (peers, p => p.JavaName == "java/lang/Exception"); + } + + [Fact] + public void Scan_McwTypes_HaveDoNotGenerateAcw () + { + var peers = ScanFixtures (); + + var activity = FindByJavaName (peers, "android/app/Activity"); + Assert.True (activity.DoNotGenerateAcw, "Activity should be MCW (DoNotGenerateAcw=true)"); + + var button = FindByJavaName (peers, "android/widget/Button"); + Assert.True (button.DoNotGenerateAcw, "Button should be MCW"); + } + + [Fact] + public void Scan_UserTypes_DoNotGenerateAcwIsFalse () + { + var peers = ScanFixtures (); + + var mainActivity = FindByJavaName (peers, "my/app/MainActivity"); + Assert.False (mainActivity.DoNotGenerateAcw, "MainActivity should not have DoNotGenerateAcw"); + } + + [Fact] + public void Scan_ActivityType_IsUnconditional () + { + var peers = ScanFixtures (); + var mainActivity = FindByJavaName (peers, "my/app/MainActivity"); + Assert.True (mainActivity.IsUnconditional, "MainActivity with [Activity] should be unconditional"); + } + + [Fact] + public void Scan_ServiceType_IsUnconditional () + { + var peers = ScanFixtures (); + var service = FindByJavaName (peers, "my/app/MyService"); + Assert.True (service.IsUnconditional, "MyService with [Service] should be unconditional"); + } + + [Fact] + public void Scan_BroadcastReceiverType_IsUnconditional () + { + var peers = ScanFixtures (); + var receiver = FindByJavaName (peers, "my/app/MyReceiver"); + Assert.True (receiver.IsUnconditional, "MyReceiver with [BroadcastReceiver] should be unconditional"); + } + + [Fact] + public void Scan_ContentProviderType_IsUnconditional () + { + var peers = ScanFixtures (); + var provider = FindByJavaName (peers, "my/app/MyProvider"); + Assert.True (provider.IsUnconditional, "MyProvider with [ContentProvider] should be unconditional"); + } + + [Fact] + public void Scan_TypeWithoutComponentAttribute_IsTrimmable () + { + var peers = ScanFixtures (); + var helper = FindByJavaName (peers, "my/app/MyHelper"); + Assert.False (helper.IsUnconditional, "MyHelper without component attr should be trimmable"); + } + + [Fact] + public void Scan_McwBinding_IsTrimmable () + { + var peers = ScanFixtures (); + var activity = FindByJavaName (peers, "android/app/Activity"); + Assert.False (activity.IsUnconditional, "MCW Activity should be trimmable (no component attr on MCW type)"); + } + + [Fact] + public void Scan_InterfaceType_IsMarkedAsInterface () + { + var peers = ScanFixtures (); + var listener = FindByManagedName (peers, "Android.Views.IOnClickListener"); + Assert.True (listener.IsInterface, "IOnClickListener should be marked as interface"); + } + + [Fact] + public void Scan_InvokerTypes_AreIncluded () + { + var peers = ScanFixtures (); + var invoker = peers.FirstOrDefault (p => p.ManagedTypeName == "Android.Views.IOnClickListenerInvoker"); + Assert.NotNull (invoker); + Assert.True (invoker.DoNotGenerateAcw, "Invoker should have DoNotGenerateAcw=true"); + Assert.Equal ("android/view/View$OnClickListener", invoker.JavaName); + } + + [Fact] + public void Scan_GenericType_IsGenericDefinition () + { + var peers = ScanFixtures (); + var generic = FindByJavaName (peers, "my/app/GenericHolder"); + Assert.True (generic.IsGenericDefinition, "GenericHolder should be marked as generic definition"); + } + + [Fact] + public void Scan_AbstractType_IsMarkedAbstract () + { + var peers = ScanFixtures (); + var abstractBase = FindByJavaName (peers, "my/app/AbstractBase"); + Assert.True (abstractBase.IsAbstract, "AbstractBase should be marked as abstract"); + } + + [Fact] + public void Scan_MarshalMethods_Collected () + { + var peers = ScanFixtures (); + var activity = FindByJavaName (peers, "android/app/Activity"); + Assert.NotEmpty (activity.MarshalMethods); + } + + [Fact] + public void Scan_UserTypeOverride_CollectsMarshalMethods () + { + var peers = ScanFixtures (); + var mainActivity = FindByJavaName (peers, "my/app/MainActivity"); + Assert.NotEmpty (mainActivity.MarshalMethods); + + var onCreate = mainActivity.MarshalMethods.FirstOrDefault (m => m.ManagedMethodName == "OnCreate"); + Assert.NotNull (onCreate); + Assert.Equal ("(Landroid/os/Bundle;)V", onCreate.JniSignature); + } + + [Fact] + public void Scan_TypeWithOwnActivationCtor_ResolvesToSelf () + { + var peers = ScanFixtures (); + var activity = FindByJavaName (peers, "android/app/Activity"); + Assert.NotNull (activity.ActivationCtor); + Assert.Equal ("Android.App.Activity", activity.ActivationCtor.DeclaringTypeName); + Assert.Equal (ActivationCtorStyle.XamarinAndroid, activity.ActivationCtor.Style); + } + + [Fact] + public void Scan_TypeWithoutOwnActivationCtor_InheritsFromBase () + { + var peers = ScanFixtures (); + var simpleActivity = FindByJavaName (peers, "my/app/SimpleActivity"); + Assert.NotNull (simpleActivity.ActivationCtor); + Assert.Equal ("Android.App.Activity", simpleActivity.ActivationCtor.DeclaringTypeName); + Assert.Equal (ActivationCtorStyle.XamarinAndroid, simpleActivity.ActivationCtor.Style); + } + + [Fact] + public void Scan_TypeWithOwnActivationCtor_DoesNotLookAtBase () + { + var peers = ScanFixtures (); + var mainActivity = FindByJavaName (peers, "my/app/MainActivity"); + Assert.NotNull (mainActivity.ActivationCtor); + Assert.Equal ("Android.App.Activity", mainActivity.ActivationCtor.DeclaringTypeName); + } + + [Fact] + public void Scan_AllTypes_HaveAssemblyName () + { + var peers = ScanFixtures (); + Assert.All (peers, peer => + Assert.False (string.IsNullOrEmpty (peer.AssemblyName), + $"Type {peer.ManagedTypeName} should have assembly name")); + } + + [Fact] + public void Scan_InvokerSharesJavaNameWithInterface () + { + var peers = ScanFixtures (); + var clickListenerPeers = peers.Where (p => p.JavaName == "android/view/View$OnClickListener").ToList (); + // Interface + Invoker share the same JNI name (this is expected — they're aliases) + Assert.Equal (2, clickListenerPeers.Count); + Assert.Contains (clickListenerPeers, p => p.IsInterface); + Assert.Contains (clickListenerPeers, p => p.DoNotGenerateAcw); + } + + [Fact] + public void Scan_ActivityBaseJavaName_IsJavaLangObject () + { + var peers = ScanFixtures (); + var activity = FindByJavaName (peers, "android/app/Activity"); + Assert.Equal ("java/lang/Object", activity.BaseJavaName); + } + + [Fact] + public void Scan_MainActivityBaseJavaName_IsActivity () + { + var peers = ScanFixtures (); + var mainActivity = FindByJavaName (peers, "my/app/MainActivity"); + Assert.Equal ("android/app/Activity", mainActivity.BaseJavaName); + } + + [Fact] + public void Scan_JavaLangObjectBaseJavaName_IsNull () + { + var peers = ScanFixtures (); + var jlo = FindByJavaName (peers, "java/lang/Object"); + Assert.Null (jlo.BaseJavaName); + } + + [Fact] + public void Scan_TypeImplementingInterface_HasInterfaceJavaNames () + { + var peers = ScanFixtures (); + var clickable = FindByJavaName (peers, "my/app/ClickableView"); + Assert.Contains ("android/view/View$OnClickListener", clickable.ImplementedInterfaceJavaNames); + } + + [Fact] + public void Scan_TypeNotImplementingInterface_HasEmptyList () + { + var peers = ScanFixtures (); + var helper = FindByJavaName (peers, "my/app/MyHelper"); + Assert.Empty (helper.ImplementedInterfaceJavaNames); + } + + [Fact] + public void Scan_TypeWithRegisteredCtors_HasConstructorMarshalMethods () + { + var peers = ScanFixtures (); + var customView = FindByJavaName (peers, "my/app/CustomView"); + var ctors = customView.MarshalMethods.Where (m => m.IsConstructor).ToList (); + Assert.Equal (2, ctors.Count); + Assert.Equal ("()V", ctors [0].JniSignature); + Assert.Equal ("(Landroid/content/Context;)V", ctors [1].JniSignature); + } + + [Fact] + public void Scan_TypeWithoutRegisteredCtors_HasNoConstructorMarshalMethods () + { + var peers = ScanFixtures (); + var helper = FindByJavaName (peers, "my/app/MyHelper"); + Assert.DoesNotContain (helper.MarshalMethods, m => m.IsConstructor); + } + + [Fact] + public void Scan_MarshalMethod_JniNameIsJavaMethodName () + { + var peers = ScanFixtures (); + var activity = FindByJavaName (peers, "android/app/Activity"); + var onCreate = activity.MarshalMethods.FirstOrDefault (m => m.ManagedMethodName == "OnCreate"); + Assert.NotNull (onCreate); + Assert.Equal ("onCreate", onCreate.JniName); + } + + [Fact] + public void Scan_UserTypeWithActivityName_DiscoveredWithoutRegister () + { + var peers = ScanFixtures (); + var mainActivity = FindByJavaName (peers, "my/app/MainActivity"); + Assert.False (mainActivity.DoNotGenerateAcw); + Assert.True (mainActivity.IsUnconditional, "Types with [Activity] are unconditional"); + } + + [Fact] + public void Scan_UserTypeWithServiceName_DiscoveredWithoutRegister () + { + var peers = ScanFixtures (); + var service = FindByJavaName (peers, "my/app/MyService"); + Assert.False (service.DoNotGenerateAcw); + Assert.True (service.IsUnconditional); + } + + [Fact] + public void Scan_UserTypeWithBroadcastReceiverName_DiscoveredWithoutRegister () + { + var peers = ScanFixtures (); + var receiver = FindByJavaName (peers, "my/app/MyReceiver"); + Assert.False (receiver.DoNotGenerateAcw); + Assert.True (receiver.IsUnconditional); + } + + [Fact] + public void Scan_UserTypeWithContentProviderName_DiscoveredWithoutRegister () + { + var peers = ScanFixtures (); + var provider = FindByJavaName (peers, "my/app/MyProvider"); + Assert.False (provider.DoNotGenerateAcw); + Assert.True (provider.IsUnconditional); + } + + [Fact] + public void Scan_Throwable_IsMcwType () + { + var peers = ScanFixtures (); + var throwable = FindByJavaName (peers, "java/lang/Throwable"); + Assert.True (throwable.DoNotGenerateAcw); + Assert.Equal ("java/lang/Object", throwable.BaseJavaName); + } + + [Fact] + public void Scan_Exception_ExtendsThrowable () + { + var peers = ScanFixtures (); + var exception = FindByJavaName (peers, "java/lang/Exception"); + Assert.True (exception.DoNotGenerateAcw); + Assert.Equal ("java/lang/Throwable", exception.BaseJavaName); + } + + [Fact] + public void Scan_NestedType_IsDiscovered () + { + var peers = ScanFixtures (); + var inner = FindByJavaName (peers, "my/app/Outer$Inner"); + Assert.Equal ("MyApp.Outer+Inner", inner.ManagedTypeName); + } + + [Fact] + public void Scan_NestedTypeInInterface_IsDiscovered () + { + var peers = ScanFixtures (); + var result = FindByJavaName (peers, "my/app/ICallback$Result"); + Assert.Equal ("MyApp.ICallback+Result", result.ManagedTypeName); + } + + [Fact] + public void Scan_MarshalMethod_BoolReturn_HasCorrectJniSignature () + { + var peers = ScanFixtures (); + var handler = FindByJavaName (peers, "my/app/TouchHandler"); + var onTouch = handler.MarshalMethods.FirstOrDefault (m => m.JniName == "onTouch"); + Assert.NotNull (onTouch); + Assert.Equal ("(Landroid/view/View;I)Z", onTouch.JniSignature); + } + + [Fact] + public void Scan_MarshalMethod_BoolParam_HasCorrectJniSignature () + { + var peers = ScanFixtures (); + var handler = FindByJavaName (peers, "my/app/TouchHandler"); + var onFocus = handler.MarshalMethods.FirstOrDefault (m => m.JniName == "onFocusChange"); + Assert.NotNull (onFocus); + Assert.Equal ("(Landroid/view/View;Z)V", onFocus.JniSignature); + } + + [Fact] + public void Scan_MarshalMethod_MultiplePrimitiveParams () + { + var peers = ScanFixtures (); + var handler = FindByJavaName (peers, "my/app/TouchHandler"); + var onScroll = handler.MarshalMethods.FirstOrDefault (m => m.JniName == "onScroll"); + Assert.NotNull (onScroll); + Assert.Equal ("(IFJD)V", onScroll.JniSignature); + } + + [Fact] + public void Scan_MarshalMethod_ArrayParam () + { + var peers = ScanFixtures (); + var handler = FindByJavaName (peers, "my/app/TouchHandler"); + var setItems = handler.MarshalMethods.FirstOrDefault (m => m.JniName == "setItems"); + Assert.NotNull (setItems); + Assert.Equal ("([Ljava/lang/String;)V", setItems.JniSignature); + } + + [Fact] + public void Scan_ExportMethod_CollectedAsMarshalMethod () + { + var peers = ScanFixtures (); + var export = FindByJavaName (peers, "my/app/ExportExample"); + Assert.Single (export.MarshalMethods); + var method = export.MarshalMethods [0]; + Assert.Equal ("myExportedMethod", method.JniName); + Assert.Null (method.Connector); + } + + [Fact] + public void Scan_ExportConstructor_CollectedWithNullConnector () + { + var peers = ScanFixtures (); + var peer = FindByJavaName (peers, "my/app/ExportsConstructors"); + // [Export] on constructors: 2 exported ctors + 0 Register methods + var exportCtors = peer.MarshalMethods.Where (m => m.IsConstructor && m.Connector == null).ToList (); + Assert.Equal (2, exportCtors.Count); + // Verify one is parameterless, one takes int + Assert.Contains (exportCtors, c => c.JniSignature == "()V"); + Assert.Contains (exportCtors, c => c.JniSignature == "(I)V"); + } + + [Fact] + public void Scan_ExportMethodWithParams_HasCorrectJniSignature () + { + var peers = ScanFixtures (); + var peer = FindByJavaName (peers, "my/app/ExportMethodWithParams"); + Assert.Equal (2, peer.MarshalMethods.Count); + // All should be [Export] (null connector) + Assert.All (peer.MarshalMethods, m => Assert.Null (m.Connector)); + // doWork(int) → (I)V + var doWork = peer.MarshalMethods.First (m => m.JniName == "doWork"); + Assert.Equal ("(I)V", doWork.JniSignature); + // computeName(String, int) → (Ljava/lang/String;I)Ljava/lang/String; + var computeName = peer.MarshalMethods.First (m => m.JniName == "computeName"); + Assert.Equal ("(Ljava/lang/String;I)Ljava/lang/String;", computeName.JniSignature); + } + + [Fact] + public void Scan_ExportMembersComprehensive_NameOverrideAndThrows () + { + var peers = ScanFixtures (); + var peer = FindByJavaName (peers, "my/app/ExportMembersComprehensive"); + Assert.Equal (4, peer.MarshalMethods.Count); + + // [Export("attributeOverridesNames")] on CompletelyDifferentName + var renamed = peer.MarshalMethods.First (m => m.JniName == "attributeOverridesNames"); + Assert.Null (renamed.Connector); + Assert.Equal ("n_CompletelyDifferentName", renamed.NativeCallbackName); + + // [Export(ThrownNames = new [] { "java.lang.Throwable" })] + var throwing = peer.MarshalMethods.First (m => m.JniName == "methodThatThrows"); + Assert.NotNull (throwing.ThrownNames); + Assert.Single (throwing.ThrownNames!); + Assert.Equal ("java.lang.Throwable", throwing.ThrownNames! [0]); + + // [Export(ThrownNames = new string [0])] + var emptyThrows = peer.MarshalMethods.First (m => m.JniName == "methodThatThrowsEmptyArray"); + // Empty array should result in null or empty ThrownNames + Assert.True (emptyThrows.ThrownNames == null || emptyThrows.ThrownNames.Count == 0); + } + + [Fact] + public void Scan_ExportMarshalComplex_HasArrayEnumAndCharSequenceSignatures () + { + var peers = ScanFixtures (); + var peer = FindByJavaName (peers, "my/app/ExportMarshalComplex"); + + var mutateInts = peer.MarshalMethods.First (m => m.JniName == "mutateInts"); + Assert.Equal ("([I)V", mutateInts.JniSignature); + Assert.Equal ("System.Int32[], System.Private.CoreLib", mutateInts.Parameters [0].ManagedType); + + var roundTripEnum = peer.MarshalMethods.First (m => m.JniName == "roundTripEnum"); + Assert.Equal ("(I)I", roundTripEnum.JniSignature); + Assert.Equal ("MyApp.ExportSampleEnum, TestFixtures", roundTripEnum.Parameters [0].ManagedType); + Assert.Equal ("MyApp.ExportSampleEnum, TestFixtures", roundTripEnum.ManagedReturnType); + + var echoCharSequence = peer.MarshalMethods.First (m => m.JniName == "echoCharSequence"); + Assert.Equal ("(Ljava/lang/CharSequence;)Ljava/lang/CharSequence;", echoCharSequence.JniSignature); + Assert.Equal ("Java.Lang.ICharSequence, TestFixtures", echoCharSequence.Parameters [0].ManagedType); + + var echoViews = peer.MarshalMethods.First (m => m.JniName == "echoViews"); + Assert.Equal ("([Landroid/view/View;)[Landroid/view/View;", echoViews.JniSignature); + Assert.Equal ("Android.Views.View[], TestFixtures", echoViews.Parameters [0].ManagedType); + + var echoStrings = peer.MarshalMethods.First (m => m.JniName == "echoStrings"); + Assert.Equal ("([Ljava/lang/String;)[Ljava/lang/String;", echoStrings.JniSignature); + Assert.Equal ("System.String[], System.Private.CoreLib", echoStrings.Parameters [0].ManagedType); + } + + [Fact] + public void Scan_ExportCtorWithSuperArgs_HasSuperArgumentsString () + { + var peers = ScanFixtures (); + var peer = FindByJavaName (peers, "my/app/ExportCtorWithSuperArgs"); + var ctor = peer.MarshalMethods.FirstOrDefault (m => m.IsConstructor); + Assert.NotNull (ctor); + Assert.Equal ("", ctor!.SuperArgumentsString); + } + + [Fact] + public void Scan_CustomView_DiscoveredAsRegularType () + { + var peers = ScanFixtures (); + var customView = FindByJavaName (peers, "my/app/CustomView"); + Assert.False (customView.IsUnconditional, "Custom views are not unconditional by attribute alone"); + Assert.Equal ("android/view/View", customView.BaseJavaName); + } + + [Fact] + public void Scan_ApplicationType_IsUnconditional () + { + var peers = ScanFixtures (); + var app = FindByJavaName (peers, "my/app/MyApplication"); + Assert.True (app.IsUnconditional, "[Application] should be unconditional"); + } + + [Fact] + public void Scan_InstrumentationType_IsUnconditional () + { + var peers = ScanFixtures (); + var instr = FindByJavaName (peers, "my/app/MyInstrumentation"); + Assert.True (instr.IsUnconditional, "[Instrumentation] should be unconditional"); + } + + [Fact] + public void Scan_BackupAgent_ForcedUnconditional () + { + var peers = ScanFixtures (); + var backupAgent = FindByJavaName (peers, "my/app/MyBackupAgent"); + Assert.True (backupAgent.IsUnconditional, "BackupAgent referenced from [Application] should be forced unconditional"); + } + + [Fact] + public void Scan_ManageSpaceActivity_ForcedUnconditional () + { + var peers = ScanFixtures (); + var activity = FindByJavaName (peers, "my/app/MyManageSpaceActivity"); + Assert.True (activity.IsUnconditional, "ManageSpaceActivity referenced from [Application] should be forced unconditional"); + } + + [Fact] + public void Scan_BackupAgent_NotUnconditional_WhenNotReferenced () + { + // A type extending BackupAgent that is NOT referenced from [Application] + // should remain trimmable (not unconditional). + var peers = ScanFixtures (); + var helper = FindByJavaName (peers, "my/app/MyHelper"); + Assert.False (helper.IsUnconditional, "Unreferenced type should remain trimmable"); + } + + [Fact] + public void Scan_McwBinding_CompatJniNameEqualsJavaName () + { + var peers = ScanFixtures (); + var activity = FindByJavaName (peers, "android/app/Activity"); + Assert.Equal (activity.JavaName, activity.CompatJniName); + } + + [Fact] + public void Scan_UserTypeWithRegister_CompatJniNameEqualsJavaName () + { + var peers = ScanFixtures (); + var mainActivity = FindByJavaName (peers, "my/app/MainActivity"); + Assert.Equal (mainActivity.JavaName, mainActivity.CompatJniName); + } + + [Fact] + public void Scan_UserTypeWithoutRegister_CompatJniNameUsesRawNamespace () + { + var peers = ScanFixtures (); + var unregistered = peers.FirstOrDefault (p => p.ManagedTypeName == "MyApp.UnregisteredHelper"); + Assert.NotNull (unregistered); + + // JavaName should use CRC64 package + Assert.StartsWith ("crc64", unregistered.JavaName); + + // CompatJniName should use the raw namespace + Assert.Equal ("myapp/UnregisteredHelper", unregistered.CompatJniName); + } + + [Fact] + public void Scan_CustomJniNameProviderAttribute_UsesNameFromAttribute () + { + var peers = ScanFixtures (); + var widget = peers.FirstOrDefault (p => p.ManagedTypeName == "MyApp.CustomWidget"); + Assert.NotNull (widget); + + // The custom attribute provides the JNI name via IJniNameProviderAttribute + Assert.Equal ("com/example/CustomWidget", widget.JavaName); + Assert.Equal ("com/example/CustomWidget", widget.CompatJniName); + } + + [Fact] + public void Scan_DeepHierarchy_ResolvesBaseJavaName () + { + var peers = ScanFixtures (); + var myButton = FindByJavaName (peers, "my/app/MyButton"); + Assert.Equal ("android/widget/Button", myButton.BaseJavaName); + } + + [Fact] + public void Scan_DeepHierarchy_InheritsActivationCtor () + { + var peers = ScanFixtures (); + var myButton = FindByJavaName (peers, "my/app/MyButton"); + Assert.NotNull (myButton.ActivationCtor); + // MyButton → Button → View → Java.Lang.Object — should find XI ctor from View or Object + Assert.Equal (ActivationCtorStyle.XamarinAndroid, myButton.ActivationCtor.Style); + } + + [Fact] + public void Scan_MultipleInterfaces_AllResolved () + { + var peers = ScanFixtures (); + var multi = FindByJavaName (peers, "my/app/MultiInterfaceView"); + Assert.Contains ("android/view/View$OnClickListener", multi.ImplementedInterfaceJavaNames); + Assert.Contains ("android/view/View$OnLongClickListener", multi.ImplementedInterfaceJavaNames); + Assert.Equal (2, multi.ImplementedInterfaceJavaNames.Count); + } + + [Fact] + public void Scan_AbstractMethod_CollectedAsMarshalMethod () + { + var peers = ScanFixtures (); + var abstractBase = FindByJavaName (peers, "my/app/AbstractBase"); + var doWork = abstractBase.MarshalMethods.FirstOrDefault (m => m.JniName == "doWork"); + Assert.NotNull (doWork); + Assert.Equal ("()V", doWork.JniSignature); + } + + [Fact] + public void Scan_PropertyRegister_CollectedAsMarshalMethod () + { + var peers = ScanFixtures (); + var throwable = FindByJavaName (peers, "java/lang/Throwable"); + var getMessage = throwable.MarshalMethods.FirstOrDefault (m => m.JniName == "getMessage"); + Assert.NotNull (getMessage); + Assert.Equal ("()Ljava/lang/String;", getMessage.JniSignature); + } + + [Fact] + public void Scan_MethodWithEmptyConnector_Collected () + { + var peers = ScanFixtures (); + var activity = FindByJavaName (peers, "android/app/Activity"); + var onStart = activity.MarshalMethods.FirstOrDefault (m => m.JniName == "onStart"); + Assert.NotNull (onStart); + Assert.Equal ("", onStart.Connector); + } + + [Fact] + public void Scan_InvokerWithRegisterAndDoNotGenerateAcw_IsIncluded () + { + var peers = ScanFixtures (); + // IOnClickListenerInvoker has [Register("android/view/View$OnClickListener", DoNotGenerateAcw=true)] + // It should be included in the scanner output — generators will filter it later + var invoker = peers.FirstOrDefault (p => p.ManagedTypeName == "Android.Views.IOnClickListenerInvoker"); + Assert.NotNull (invoker); + Assert.True (invoker.DoNotGenerateAcw); + Assert.Equal ("android/view/View$OnClickListener", invoker.JavaName); + } + + [Fact] + public void Scan_Interface_HasInvokerTypeNameFromRegisterConnector () + { + var peers = ScanFixtures (); + var listener = FindByManagedName (peers, "Android.Views.IOnClickListener"); + Assert.NotNull (listener.InvokerTypeName); + Assert.Equal ("Android.Views.IOnClickListenerInvoker", listener.InvokerTypeName); + } + + [Fact] + public void Scan_Interface_IsNotMarkedDoNotGenerateAcw () + { + var peers = ScanFixtures (); + var listener = FindByManagedName (peers, "Android.Views.IOnClickListener"); + // Interfaces have [Register("name", "", "connector")] — the 3-arg form doesn't set DoNotGenerateAcw + Assert.False (listener.DoNotGenerateAcw, "Interfaces should not have DoNotGenerateAcw"); + } + + [Fact] + public void Scan_InterfaceMethod_CollectedAsMarshalMethod () + { + var peers = ScanFixtures (); + var listener = FindByManagedName (peers, "Android.Views.IOnClickListener"); + var onClick = listener.MarshalMethods.FirstOrDefault (m => m.JniName == "onClick"); + Assert.NotNull (onClick); + Assert.Equal ("(Landroid/view/View;)V", onClick.JniSignature); + } + + [Fact] + public void Scan_GenericType_HasCorrectManagedTypeName () + { + var peers = ScanFixtures (); + var generic = FindByJavaName (peers, "my/app/GenericHolder"); + Assert.Equal ("MyApp.Generic.GenericHolder`1", generic.ManagedTypeName); + } + + [Fact] + public void Scan_Context_IsDiscovered () + { + var peers = ScanFixtures (); + var context = FindByJavaName (peers, "android/content/Context"); + Assert.True (context.DoNotGenerateAcw, "Context is MCW"); + Assert.Equal ("java/lang/Object", context.BaseJavaName); + } + + // ================================================================ + // Edge case tests — discovered during side-by-side testing + // ================================================================ + + [Fact] + public void Scan_GenericBaseType_ResolvesViaTypeSpecification () + { + // ConcreteFromGeneric extends GenericBase. The base type + // is a TypeSpecification (generic instantiation). The scanner must + // decode the blob to resolve the underlying TypeDef/TypeRef. + var peers = ScanFixtures (); + var concrete = FindByJavaName (peers, "my/app/ConcreteFromGeneric"); + Assert.Equal ("my/app/GenericBase", concrete.BaseJavaName); + } + + [Fact] + public void Scan_GenericInterface_ResolvesViaTypeSpecification () + { + // GenericCallbackImpl implements IGenericCallback. The interface + // is a TypeSpecification. The scanner must decode the blob to resolve it. + var peers = ScanFixtures (); + var impl = FindByJavaName (peers, "my/app/GenericCallbackImpl"); + Assert.Contains ("my/app/IGenericCallback", impl.ImplementedInterfaceJavaNames); + } + + [Fact] + public void Scan_ComponentOnlyBase_DerivedTypeIsDiscovered () + { + // BaseActivityNoRegister has [Activity] but no [Register]. + // DerivedFromComponentBase extends it. ExtendsJavaPeerCore must + // detect the component attribute on the base to include the derived type. + var peers = ScanFixtures (); + var derived = peers.FirstOrDefault (p => p.ManagedTypeName == "MyApp.DerivedFromComponentBase"); + Assert.NotNull (derived); + // Should get a CRC64-computed JNI name + Assert.StartsWith ("crc64", derived.JavaName); + } + + [Fact] + public void Scan_ComponentOnlyBase_BaseTypeIsDiscovered () + { + // BaseActivityNoRegister has [Activity(Name = "...")] — should be discovered + // even without [Register]. + var peers = ScanFixtures (); + var baseType = FindByJavaName (peers, "my/app/BaseActivityNoRegister"); + Assert.True (baseType.IsUnconditional, "[Activity] makes it unconditional"); + Assert.Equal ("android/app/Activity", baseType.BaseJavaName); + } + + [Fact] + public void Scan_UnregisteredNestedType_UsesParentJniPrefix () + { + // UnregisteredChild has no [Register] but its parent RegisteredParent does. + // ComputeTypeNameParts should use parent's JNI name as prefix. + var peers = ScanFixtures (); + var child = peers.FirstOrDefault (p => p.ManagedTypeName == "MyApp.RegisteredParent+UnregisteredChild"); + Assert.NotNull (child); + Assert.Equal ("my/app/RegisteredParent_UnregisteredChild", child.JavaName); + } + + [Fact] + public void Scan_EmptyNamespace_RegisteredType_Discovered () + { + // GlobalType has [Register] and no namespace — should work normally. + var peers = ScanFixtures (); + var global = FindByJavaName (peers, "my/app/GlobalType"); + Assert.Equal ("GlobalType", global.ManagedTypeName); + } + + [Fact] + public void Scan_EmptyNamespace_UnregisteredType_CompatJniHasNoSlash () + { + // GlobalUnregisteredType has no namespace and no [Register]. + // CompatJniName should just be the type name (no leading slash). + var peers = ScanFixtures (); + var global = peers.FirstOrDefault (p => p.ManagedTypeName == "GlobalUnregisteredType"); + Assert.NotNull (global); + Assert.Equal ("GlobalUnregisteredType", global.CompatJniName); + Assert.DoesNotContain ("/", global.CompatJniName); + } + + [Fact] + public void Scan_DeepNestedType_ThreeLevelNesting () + { + // DeepOuter.Middle.DeepInner — 3-level nesting. + // ComputeTypeNameParts walks multiple declaring-type levels. + var peers = ScanFixtures (); + var deep = peers.FirstOrDefault (p => p.ManagedTypeName == "MyApp.DeepOuter+Middle+DeepInner"); + Assert.NotNull (deep); + Assert.Equal ("my/app/DeepOuter_Middle_DeepInner", deep.JavaName); + } + + [Fact] + public void Scan_PlainActivitySubclass_DiscoveredWithCrc64Name () + { + // PlainActivitySubclass extends Activity with no [Register], no [Activity]. + // ExtendsJavaPeer detects it via the base type chain, gets CRC64 name. + var peers = ScanFixtures (); + var plain = peers.FirstOrDefault (p => p.ManagedTypeName == "MyApp.PlainActivitySubclass"); + Assert.NotNull (plain); + Assert.StartsWith ("crc64", plain.JavaName); + Assert.Equal ("android/app/Activity", plain.BaseJavaName); + } + + [Fact] + public void Scan_ComponentAttributeWithoutName_DiscoveredWithCrc64Name () + { + // UnnamedActivity has [Activity(Label="Unnamed")] but no Name property. + // HasComponentAttribute = true, ComponentAttributeJniName should be null, + // and the type should still get a CRC64-based JNI name. + var peers = ScanFixtures (); + var unnamed = peers.FirstOrDefault (p => p.ManagedTypeName == "MyApp.UnnamedActivity"); + Assert.NotNull (unnamed); + Assert.StartsWith ("crc64", unnamed.JavaName); + Assert.True (unnamed.IsUnconditional, "[Activity] makes it unconditional"); + } + + [Fact] + public void Scan_InterfaceOnUnregisteredType_InterfacesResolved () + { + // UnregisteredClickListener has no [Register] but implements IOnClickListener. + // Type gets CRC64 name, interfaces still resolved. + var peers = ScanFixtures (); + var listener = peers.FirstOrDefault (p => p.ManagedTypeName == "MyApp.UnregisteredClickListener"); + Assert.NotNull (listener); + Assert.StartsWith ("crc64", listener.JavaName); + Assert.Contains ("android/view/View$OnClickListener", listener.ImplementedInterfaceJavaNames); + } + + [Fact] + public void Scan_ExportOnUnregisteredType_MethodDiscovered () + { + // UnregisteredExporter has [Export("doExportedWork")] on a type without [Register]. + // Type gets CRC64 name, [Export] method is in MarshalMethods. + var peers = ScanFixtures (); + var exporter = peers.FirstOrDefault (p => p.ManagedTypeName == "MyApp.UnregisteredExporter"); + Assert.NotNull (exporter); + Assert.StartsWith ("crc64", exporter.JavaName); + var exportMethod = exporter.MarshalMethods.FirstOrDefault (m => m.JniName == "doExportedWork"); + Assert.NotNull (exportMethod); + Assert.Null (exportMethod.Connector); + } + + [Fact] + public void Scan_StaticExportMethod_IsStaticSetTrue () + { + var peers = ScanFixtures (); + var peer = FindByJavaName (peers, "my/app/ExportStaticAndFields"); + var staticMethod = peer.MarshalMethods.FirstOrDefault (m => m.JniName == "staticMethodNotMangled"); + Assert.NotNull (staticMethod); + Assert.True (staticMethod.IsStatic); + Assert.Null (staticMethod.Connector); + } + + [Fact] + public void Scan_InstanceExportMethod_IsStaticSetFalse () + { + var peers = ScanFixtures (); + var peer = FindByJavaName (peers, "my/app/ExportStaticAndFields"); + var instanceMethod = peer.MarshalMethods.FirstOrDefault (m => m.JniName == "instanceMethod"); + Assert.NotNull (instanceMethod); + Assert.False (instanceMethod.IsStatic); + } + + [Fact] + public void Scan_ExportField_CollectedAsExportFieldInfo () + { + var peers = ScanFixtures (); + var peer = FindByJavaName (peers, "my/app/ExportStaticAndFields"); + + Assert.NotNull (peer.ExportFields); + Assert.Equal (2, peer.ExportFields.Count); + + var staticField = peer.ExportFields.FirstOrDefault (f => f.FieldName == "STATIC_INSTANCE"); + Assert.NotNull (staticField); + Assert.Equal ("GetInstance", staticField.MethodName); + Assert.True (staticField.IsStatic); + + var instanceField = peer.ExportFields.FirstOrDefault (f => f.FieldName == "VALUE"); + Assert.NotNull (instanceField); + Assert.Equal ("GetValue", instanceField.MethodName); + Assert.False (instanceField.IsStatic); + } + + [Fact] + public void Scan_ExportField_MethodAlsoInMarshalMethods () + { + var peers = ScanFixtures (); + var peer = FindByJavaName (peers, "my/app/ExportStaticAndFields"); + + // [ExportField] methods should also be in MarshalMethods as export methods + var getInstance = peer.MarshalMethods.FirstOrDefault (m => m.ManagedMethodName == "GetInstance"); + Assert.NotNull (getInstance); + Assert.Null (getInstance.Connector); // Export, not Register + Assert.True (getInstance.IsStatic); + + var getValue = peer.MarshalMethods.FirstOrDefault (m => m.ManagedMethodName == "GetValue"); + Assert.NotNull (getValue); + Assert.Null (getValue.Connector); + Assert.False (getValue.IsStatic); + } +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs new file mode 100644 index 00000000000..0cb21fb40c6 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs @@ -0,0 +1,218 @@ +// Minimal stub attributes mirroring the real Mono.Android attributes. +// These exist solely so the test fixture assembly can have types +// with the same attribute shapes the scanner expects. + +using System; + +namespace Java.Interop +{ + public interface IJniNameProviderAttribute + { + string Name { get; } + } +} + +namespace Android.Runtime +{ + [AttributeUsage ( + AttributeTargets.Class | AttributeTargets.Constructor | AttributeTargets.Field | + AttributeTargets.Interface | AttributeTargets.Method | AttributeTargets.Property, + AllowMultiple = false)] + public sealed class RegisterAttribute : Attribute, Java.Interop.IJniNameProviderAttribute + { + public string Name { get; } + public string? Signature { get; set; } + public string? Connector { get; set; } + public bool DoNotGenerateAcw { get; set; } + public int ApiSince { get; set; } + + public RegisterAttribute (string name) + { + Name = name; + } + + public RegisterAttribute (string name, string signature, string connector) + { + Name = name; + Signature = signature; + Connector = connector; + } + } + + public enum JniHandleOwnership + { + DoNotTransfer = 0, + TransferLocalRef = 1, + TransferGlobalRef = 2, + } +} + +namespace Android.App +{ + [AttributeUsage (AttributeTargets.Class)] + public sealed class ActivityAttribute : Attribute + { + public bool MainLauncher { get; set; } + public string? Label { get; set; } + public string? Icon { get; set; } + public string? Name { get; set; } + public string? Theme { get; set; } + public string? ParentActivity { get; set; } + public bool Exported { get; set; } + public string? Permission { get; set; } + public string? Process { get; set; } + public bool Enabled { get; set; } = true; + public string? ConfigurationChanges { get; set; } + public string? LaunchMode { get; set; } + public string? ScreenOrientation { get; set; } + public string? WindowSoftInputMode { get; set; } + } + + [AttributeUsage (AttributeTargets.Class)] + public sealed class ServiceAttribute : Attribute + { + public string? Name { get; set; } + public bool Exported { get; set; } + public bool Enabled { get; set; } = true; + public string? Permission { get; set; } + public string? Process { get; set; } + public bool IsolatedProcess { get; set; } + public string? ForegroundServiceType { get; set; } + } + + [AttributeUsage (AttributeTargets.Class)] + public sealed class InstrumentationAttribute : Attribute + { + public string? Name { get; set; } + public string? TargetPackage { get; set; } + public bool FunctionalTest { get; set; } + public bool HandleProfiling { get; set; } + public string? Label { get; set; } + } + + [AttributeUsage (AttributeTargets.Class)] + public sealed class ApplicationAttribute : Attribute + { + public Type? BackupAgent { get; set; } + public Type? ManageSpaceActivity { get; set; } + public string? Name { get; set; } + public string? Theme { get; set; } + public string? Label { get; set; } + public string? Icon { get; set; } + public bool Debuggable { get; set; } + public bool AllowBackup { get; set; } + public bool SupportsRtl { get; set; } + } + + [AttributeUsage (AttributeTargets.Class, AllowMultiple = true)] + public sealed class IntentFilterAttribute : Attribute + { + public string []? Actions { get; } + public string []? Categories { get; set; } + public string? DataScheme { get; set; } + public string? DataHost { get; set; } + public string? DataPathPrefix { get; set; } + public int Priority { get; set; } + public bool AutoVerify { get; set; } + + public IntentFilterAttribute (string [] actions) + { + Actions = actions; + } + } + + [AttributeUsage (AttributeTargets.Class, AllowMultiple = true)] + public sealed class MetaDataAttribute : Attribute + { + public string Name { get; } + public string? Value { get; set; } + public string? Resource { get; set; } + + public MetaDataAttribute (string name) + { + Name = name; + } + } + + [AttributeUsage (AttributeTargets.Class)] + public sealed class LayoutAttribute : Attribute + { + public string? DefaultWidth { get; set; } + public string? DefaultHeight { get; set; } + public string? Gravity { get; set; } + public string? MinWidth { get; set; } + public string? MinHeight { get; set; } + } + + [AttributeUsage (AttributeTargets.Class, AllowMultiple = true)] + public sealed class PropertyAttribute : Attribute + { + public string Name { get; } + public string? Value { get; set; } + + public PropertyAttribute (string name) + { + Name = name; + } + } +} + +namespace Android.Content +{ + [AttributeUsage (AttributeTargets.Class)] + public sealed class BroadcastReceiverAttribute : Attribute + { + public string? Name { get; set; } + public bool Exported { get; set; } + public bool Enabled { get; set; } = true; + public string? Permission { get; set; } + public string? Process { get; set; } + } + + [AttributeUsage (AttributeTargets.Class)] + public sealed class ContentProviderAttribute : Attribute + { + public string []? Authorities { get; set; } + public string? Name { get; set; } + public bool Exported { get; set; } + public bool Enabled { get; set; } = true; + public string? Permission { get; set; } + public bool GrantUriPermissions { get; set; } + public int InitOrder { get; set; } + + public ContentProviderAttribute (string [] authorities) + { + Authorities = authorities; + } + } + + [AttributeUsage (AttributeTargets.Class, AllowMultiple = true)] + public sealed class GrantUriPermissionAttribute : Attribute + { + public string? Path { get; set; } + public string? PathPattern { get; set; } + public string? PathPrefix { get; set; } + } +} + +namespace Java.Interop +{ + [AttributeUsage (AttributeTargets.Method, AllowMultiple = false)] + public sealed class ExportAttribute : Attribute + { + public string? Name { get; set; } + + public string[]? ThrownNames { get; set; } + + public string? SuperArgumentsString { get; set; } + + public ExportAttribute () + { + } + + public ExportAttribute (string name) + { + Name = name; + } + } +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestFixtures.csproj b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestFixtures.csproj new file mode 100644 index 00000000000..f7f4c72139b --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestFixtures.csproj @@ -0,0 +1,13 @@ + + + + $(DotNetStableTargetFramework) + latest + enable + Microsoft.Android.Sdk.TrimmableTypeMap.Tests.TestFixtures + + false + true + + + diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs new file mode 100644 index 00000000000..c597d3360e4 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs @@ -0,0 +1,905 @@ +// Test fixture types that exercise all scanner code paths. +// Each type is annotated with comments explaining which classification +// and behavior the scanner should produce. + +using System; +using Android.App; +using Android.Content; +using Android.Runtime; + +namespace Java.Lang +{ + [Register ("java/lang/Object", DoNotGenerateAcw = true)] + public class Object + { + public Object () + { + } + + protected Object (IntPtr handle, JniHandleOwnership transfer) + { + } + } + + [Register ("java/lang/CharSequence", DoNotGenerateAcw = true)] + public interface ICharSequence + { + } +} + +namespace Java.Lang +{ + [Register ("java/lang/Throwable", DoNotGenerateAcw = true)] + public class Throwable : Java.Lang.Object + { + protected Throwable (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + + [Register ("getMessage", "()Ljava/lang/String;", "GetGetMessageHandler")] + public virtual string? Message { get; } + } + + [Register ("java/lang/Exception", DoNotGenerateAcw = true)] + public class Exception : Throwable + { + protected Exception (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + } +} + +namespace Android.App +{ + [Register ("android/app/Activity", DoNotGenerateAcw = true)] + public class Activity : Java.Lang.Object + { + public Activity () + { + } + + protected Activity (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + + [Register ("onCreate", "(Landroid/os/Bundle;)V", "GetOnCreate_Landroid_os_Bundle_Handler")] + protected virtual void OnCreate (/* Bundle? */ object? savedInstanceState) + { + } + + [Register ("onStart", "()V", "")] + protected virtual void OnStart () + { + } + } + + [Register ("android/app/Service", DoNotGenerateAcw = true)] + public class Service : Java.Lang.Object + { + protected Service (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + } +} + +namespace Android.Content +{ + [Register ("android/content/Context", DoNotGenerateAcw = true)] + public class Context : Java.Lang.Object + { + protected Context (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + } +} + +namespace Android.Views +{ + [Register ("android/view/View", DoNotGenerateAcw = true)] + public class View : Java.Lang.Object + { + protected View (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + } +} + +namespace Android.Widget +{ + [Register ("android/widget/Button", DoNotGenerateAcw = true)] + public class Button : Android.Views.View + { + protected Button (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + } + + [Register ("android/widget/TextView", DoNotGenerateAcw = true)] + public class TextView : Android.Views.View + { + protected TextView (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + } +} + +namespace Android.Views +{ + [Register ("android/view/View$OnClickListener", "", "Android.Views.IOnClickListenerInvoker")] + public interface IOnClickListener + { + [Register ("onClick", "(Landroid/view/View;)V", "GetOnClick_Landroid_view_View_Handler:Android.Views.IOnClickListenerInvoker")] + void OnClick (View v); + } + + // Invoker types ARE internal implementation details. + // In real Mono.Android.dll, invokers DO have [Register] with DoNotGenerateAcw=true + // and the SAME JNI name as their interface. + // The scanner includes them — generators filter them later. + [Register ("android/view/View$OnClickListener", DoNotGenerateAcw = true)] + internal sealed class IOnClickListenerInvoker : Java.Lang.Object, IOnClickListener + { + public IOnClickListenerInvoker (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + + public void OnClick (View v) + { + } + } +} + +namespace MyApp +{ + // User types get their JNI name from [Activity(Name = "...")] + // NOT from [Register] — that's only on MCW binding types. + [Activity (MainLauncher = true, Label = "My App", Name = "my.app.MainActivity")] + public class MainActivity : Android.App.Activity + { + public MainActivity () + { + } + + [Register ("onCreate", "(Landroid/os/Bundle;)V", "GetOnCreate_Landroid_os_Bundle_Handler")] + protected override void OnCreate (object? savedInstanceState) + { + base.OnCreate (savedInstanceState); + } + } + + // Activity with intent filters, metadata, layout, and property attributes + [Activity (Name = "my.app.DeepLinkActivity", Theme = "@style/AppTheme", Exported = true)] + [IntentFilter ( + new [] { "android.intent.action.VIEW" }, + Categories = new [] { "android.intent.category.DEFAULT", "android.intent.category.BROWSABLE" }, + DataScheme = "https", + DataHost = "example.com", + DataPathPrefix = "/deep", + AutoVerify = true)] + [IntentFilter ( + new [] { "my.app.CUSTOM_ACTION" }, + Categories = new [] { "android.intent.category.DEFAULT" })] + [MetaData ("com.google.android.geo.API_KEY", Value = "test-api-key")] + [MetaData ("com.google.android.gms.version", Resource = "@integer/google_play_services_version")] + [Layout (DefaultWidth = "500dp", DefaultHeight = "600dp", Gravity = "center", MinWidth = "300dp", MinHeight = "400dp")] + [Property ("custom.prop", Value = "custom-value")] + public class DeepLinkActivity : Android.App.Activity + { + public DeepLinkActivity () + { + } + } + + // Abstract activity — should be skipped by manifest generation + [Activity (Name = "my.app.BaseActivity")] + public abstract class BaseActivity : Android.App.Activity + { + protected BaseActivity () + { + } + } + + // Activity without public parameterless constructor — should trigger XA4213 + [Activity (Name = "my.app.NoDefaultCtorActivity")] + public class NoDefaultCtorActivity : Android.App.Activity + { + readonly string _arg; + + public NoDefaultCtorActivity (string arg) + { + _arg = arg; + } + } + + // User type without component attribute: TRIMMABLE + [Register ("my/app/MyHelper")] + public class MyHelper : Java.Lang.Object + { + [Register ("doSomething", "()V", "GetDoSomethingHandler")] + public virtual void DoSomething () + { + } + } + + // User service with rich attributes + [Service (Name = "my.app.MyService", Exported = true, Permission = "my.app.BIND_SERVICE", IsolatedProcess = true)] + [IntentFilter (new [] { "my.app.START_SERVICE" })] + [MetaData ("service.version", Value = "1")] + public class MyService : Android.App.Service + { + protected MyService (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + } + + // User broadcast receiver with attributes + [BroadcastReceiver (Name = "my.app.MyReceiver", Exported = true, Permission = "my.app.RECEIVE_BROADCAST")] + [IntentFilter (new [] { "android.intent.action.BOOT_COMPLETED" })] + public class MyReceiver : Java.Lang.Object + { + } + + // User content provider with grant URI permissions + [ContentProvider (new [] { "my.app.provider" }, Name = "my.app.MyProvider", Exported = true, GrantUriPermissions = true)] + [GrantUriPermission (Path = "/data")] + [GrantUriPermission (PathPrefix = "/files")] + [MetaData ("provider.meta", Value = "meta-value")] + public class MyProvider : Java.Lang.Object + { + } +} + +namespace MyApp.Generic +{ + [Register ("my/app/GenericHolder")] + public class GenericHolder : Java.Lang.Object where T : Java.Lang.Object + { + [Register ("getItem", "()Ljava/lang/Object;", "GetGetItemHandler")] + public virtual T? GetItem () + { + return default; + } + } +} + +namespace MyApp +{ + [Register ("my/app/AbstractBase")] + public abstract class AbstractBase : Java.Lang.Object + { + protected AbstractBase (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + + [Register ("doWork", "()V", "")] + public abstract void DoWork (); + } +} + +namespace MyApp +{ + [Register ("my/app/SimpleActivity")] + public class SimpleActivity : Android.App.Activity + { + // No (IntPtr, JniHandleOwnership) ctor — scanner should + // resolve to Activity's activation ctor + } +} + +namespace MyApp +{ + [Register ("my/app/ClickableView")] + public class ClickableView : Android.Views.View, Android.Views.IOnClickListener + { + protected ClickableView (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + + [Register ("onClick", "(Landroid/view/View;)V", "")] + public void OnClick (Android.Views.View v) + { + } + } +} + +namespace MyApp +{ + [Register ("my/app/CustomView")] + public class CustomView : Android.Views.View + { + protected CustomView (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + + [Register ("", "()V", "")] + public CustomView () + : base (default!, default) + { + } + + [Register ("", "(Landroid/content/Context;)V", "")] + public CustomView (Context context) + : base (default!, default) + { + } + } +} + +namespace MyApp +{ + [Register ("my/app/Outer")] + public class Outer : Java.Lang.Object + { + [Register ("my/app/Outer$Inner")] + public class Inner : Java.Lang.Object + { + protected Inner (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + } + } +} + +namespace MyApp +{ + [Register ("my/app/ICallback", "", "MyApp.ICallbackInvoker")] + public interface ICallback + { + [Register ("my/app/ICallback$Result")] + public class Result : Java.Lang.Object + { + protected Result (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + } + } +} + +namespace MyApp +{ + [Register ("my/app/TouchHandler")] + public class TouchHandler : Java.Lang.Object + { + // bool return type (non-blittable, needs byte wrapper in UCO) + [Register ("onTouch", "(Landroid/view/View;I)Z", "GetOnTouchHandler")] + public virtual bool OnTouch (Android.Views.View v, int action) + { + return false; + } + + // bool parameter (non-blittable) + [Register ("onFocusChange", "(Landroid/view/View;Z)V", "GetOnFocusChangeHandler")] + public virtual void OnFocusChange (Android.Views.View v, bool hasFocus) + { + } + + // Multiple params of different JNI types + [Register ("onScroll", "(IFJD)V", "GetOnScrollHandler")] + public virtual void OnScroll (int x, float y, long timestamp, double velocity) + { + } + + // Object return type + [Register ("getText", "()Ljava/lang/String;", "GetGetTextHandler")] + public virtual string? GetText () + { + return null; + } + + // Array parameter + [Register ("setItems", "([Ljava/lang/String;)V", "GetSetItemsHandler")] + public virtual void SetItems (string[]? items) + { + } + } +} + +namespace MyApp +{ + [Register ("my/app/ExportExample")] + public class ExportExample : Java.Lang.Object + { + [Java.Interop.Export ("myExportedMethod")] + public void MyExportedMethod () + { + } + } + + [Register ("my/app/ExportWithThrows")] + public class ExportWithThrows : Java.Lang.Object + { + [Java.Interop.Export ("riskyMethod", ThrownNames = new [] { "java.io.IOException", "java.lang.IllegalStateException" })] + public void RiskyMethod () + { + } + } +} + +namespace Android.App.Backup +{ + [Register ("android/app/backup/BackupAgent", DoNotGenerateAcw = true)] + public class BackupAgent : Java.Lang.Object + { + protected BackupAgent (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + } +} + +namespace MyApp +{ + [Application (Name = "my.app.MyApplication", BackupAgent = typeof (MyBackupAgent), ManageSpaceActivity = typeof (MyManageSpaceActivity), Theme = "@style/AppTheme", Debuggable = true, AllowBackup = true, SupportsRtl = true, Label = "My Application", Icon = "@mipmap/ic_launcher")] + [MetaData ("app.version", Value = "2.0")] + public class MyApplication : Java.Lang.Object + { + } + + [Instrumentation (Name = "my.app.MyInstrumentation", TargetPackage = "my.app", FunctionalTest = true, HandleProfiling = true, Label = "Test Runner")] + public class MyInstrumentation : Java.Lang.Object + { + } + + // BackupAgent without a component attribute — would normally be trimmable, + // but [Application(BackupAgent = typeof(...))] should force it unconditional. + [Register ("my/app/MyBackupAgent")] + public class MyBackupAgent : Android.App.Backup.BackupAgent + { + protected MyBackupAgent (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + } + + // Activity without [Activity] attribute — would normally be trimmable, + // but [Application(ManageSpaceActivity = typeof(...))] should force it unconditional. + [Register ("my/app/MyManageSpaceActivity")] + public class MyManageSpaceActivity : Android.App.Activity + { + protected MyManageSpaceActivity (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + } + + // User type WITHOUT [Register] — gets CRC64-computed JNI name. + // CompatJniName should use raw namespace instead of CRC64. + public class UnregisteredHelper : Java.Lang.Object + { + } +} + +namespace MyApp +{ + [Register ("my/app/MyButton")] + public class MyButton : Android.Widget.Button + { + protected MyButton (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + } +} + +namespace Android.Views +{ + [Register ("android/view/View$OnLongClickListener", "", "Android.Views.IOnLongClickListenerInvoker")] + public interface IOnLongClickListener + { + [Register ("onLongClick", "(Landroid/view/View;)Z", "GetOnLongClick_Landroid_view_View_Handler:Android.Views.IOnLongClickListenerInvoker")] + bool OnLongClick (View v); + } + + // Implementor types are generated by the binding generator. + // They are NOT DoNotGenerateAcw because they ARE ACW types, but they should still + // be trimmable because they are only instantiated from .NET (e.g., when subscribing to an event). + [Register ("android/view/View_IOnClickListenerImplementor")] + public class IOnClickListenerImplementor : Java.Lang.Object, IOnClickListener + { + public IOnClickListenerImplementor () + { + } + + protected IOnClickListenerImplementor (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + + [Register ("onClick", "(Landroid/view/View;)V", "")] + public void OnClick (View v) + { + } + } + + // EventDispatcher types are used for the event-based pattern in Android bindings. + // Like Implementor types, they should be trimmable. + [Register ("android/view/View_ClickEventDispatcher")] + public class ClickEventDispatcher : Java.Lang.Object + { + public ClickEventDispatcher () + { + } + + protected ClickEventDispatcher (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + + [Register ("dispatch", "()V", "")] + public void Dispatch () + { + } + } +} + +namespace MyApp +{ + [Register ("my/app/MultiInterfaceView")] + public class MultiInterfaceView : Android.Views.View, Android.Views.IOnClickListener, Android.Views.IOnLongClickListener + { + protected MultiInterfaceView (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + + [Register ("onClick", "(Landroid/view/View;)V", "")] + public void OnClick (Android.Views.View v) { } + + [Register ("onLongClick", "(Landroid/view/View;)Z", "")] + public bool OnLongClick (Android.Views.View v) { return false; } + } + + // User type with a custom IJniNameProviderAttribute — the scanner + // should detect this via interface resolution, not hardcoded attribute names. + [CustomJniName ("com.example.CustomWidget")] + public class CustomWidget : Java.Lang.Object + { + } +} + +// ================================================================ +// Edge case: generic base type (TypeSpecification resolution) +// ================================================================ +namespace MyApp.Generic +{ + [Register ("my/app/GenericBase", DoNotGenerateAcw = true)] + public class GenericBase : Java.Lang.Object where T : class + { + protected GenericBase (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + } + + [Register ("my/app/ConcreteFromGeneric")] + public class ConcreteFromGeneric : GenericBase + { + protected ConcreteFromGeneric (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + } +} + +// ================================================================ +// Edge case: generic interface (TypeSpecification resolution) +// ================================================================ +namespace MyApp.Generic +{ + [Register ("my/app/IGenericCallback", "", "")] + public interface IGenericCallback + { + } + + [Register ("my/app/GenericCallbackImpl")] + public class GenericCallbackImpl : Java.Lang.Object, IGenericCallback + { + protected GenericCallbackImpl (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + } +} + +// ================================================================ +// Edge case: component-only base detection +// ================================================================ +namespace MyApp +{ + [Activity (Name = "my.app.BaseActivityNoRegister")] + public class BaseActivityNoRegister : Android.App.Activity + { + } + + public class DerivedFromComponentBase : BaseActivityNoRegister + { + } +} + +// ================================================================ +// Edge case: unregistered nested type inside [Register] parent +// ================================================================ +namespace MyApp +{ + [Register ("my/app/RegisteredParent")] + public class RegisteredParent : Java.Lang.Object + { + public class UnregisteredChild : Java.Lang.Object + { + } + } +} + +// ================================================================ +// Edge case: 3-level deep nesting +// ComputeTypeNameParts must walk multiple levels, collecting names. +// ================================================================ +namespace MyApp +{ + [Register ("my/app/DeepOuter")] + public class DeepOuter : Java.Lang.Object + { + public class Middle : Java.Lang.Object + { + public class DeepInner : Java.Lang.Object + { + } + } + } +} + +// ================================================================ +// Edge case: plain Java peer subclass — no [Register], no component attribute +// ExtendsJavaPeer must detect it via base type chain, gets CRC64 name. +// ================================================================ +namespace MyApp +{ + public class PlainActivitySubclass : Android.App.Activity + { + } +} + +// ================================================================ +// Edge case: component attribute WITHOUT Name property +// HasComponentAttribute = true but ComponentAttributeJniName = null. +// Type should still get a CRC64 JNI name (not null). +// ================================================================ +namespace MyApp +{ + [Activity (Label = "Unnamed")] + public class UnnamedActivity : Android.App.Activity + { + } +} + +// ================================================================ +// Edge case: interface implementation on unregistered type +// Type gets CRC64 JNI name but still resolves interface names. +// ================================================================ +namespace MyApp +{ + public class UnregisteredClickListener : Java.Lang.Object, Android.Views.IOnClickListener + { + [Register ("onClick", "(Landroid/view/View;)V", "")] + public void OnClick (Android.Views.View v) + { + } + } +} + +// ================================================================ +// Edge case: [Export] method on unregistered type +// ParseExportAttribute runs on a type that gets CRC64 JNI name. +// ================================================================ +namespace MyApp +{ + public class UnregisteredExporter : Java.Lang.Object + { + [Java.Interop.Export ("doExportedWork")] + public void DoExportedWork () + { + } + } +} + +// ================================================================ +// Edge case: type in empty namespace +// ================================================================ +[Register ("my/app/GlobalType")] +public class GlobalType : Java.Lang.Object +{ + protected GlobalType (IntPtr handle, Android.Runtime.JniHandleOwnership transfer) + : base (handle, transfer) + { + } +} + +public class GlobalUnregisteredType : Java.Lang.Object +{ +} + +// ================================================================ +// [Export] constructor scenarios — ported from legacy SupportDeclarations.cs +// ================================================================ +namespace MyApp +{ + public enum ExportSampleEnum + { + None, + One, + } + + /// + /// Type with [Export] constructors (no [Register] on ctors). + /// Legacy JCW: TypeManager.Activate pattern, not nctor_N. + /// + [Register ("my/app/ExportsConstructors")] + public class ExportsConstructors : Java.Lang.Object + { + protected ExportsConstructors (IntPtr handle, Android.Runtime.JniHandleOwnership transfer) + : base (handle, transfer) + { + } + + [Java.Interop.Export] + public ExportsConstructors () { } + + [Java.Interop.Export] + public ExportsConstructors (int value) { } + } + + /// + /// Type with [Export] constructors that throw. + /// + [Register ("my/app/ExportsThrowsConstructors")] + public class ExportsThrowsConstructors : Java.Lang.Object + { + protected ExportsThrowsConstructors (IntPtr handle, Android.Runtime.JniHandleOwnership transfer) + : base (handle, transfer) + { + } + + [Java.Interop.Export (ThrownNames = new [] { "java.lang.Throwable" })] + public ExportsThrowsConstructors () { } + + [Java.Interop.Export (ThrownNames = new [] { "java.lang.Throwable" })] + public ExportsThrowsConstructors (int value) { } + + [Java.Interop.Export] + public ExportsThrowsConstructors (string value) { } + } + + /// + /// Type with [Export] methods with parameters (not just parameterless). + /// Ported from legacy ExportsMembers. + /// + [Register ("my/app/ExportMethodWithParams")] + public class ExportMethodWithParams : Java.Lang.Object + { + protected ExportMethodWithParams (IntPtr handle, Android.Runtime.JniHandleOwnership transfer) + : base (handle, transfer) + { + } + + [Java.Interop.Export ("doWork")] + public void DoWork (int count) { } + + [Java.Interop.Export ("computeName")] + public string ComputeName (string prefix, int index) { return ""; } + } + + /// + /// Complex [Export] marshal scenarios: arrays, enums, and CharSequence. + /// + [Register ("my/app/ExportMarshalComplex")] + public class ExportMarshalComplex : Java.Lang.Object + { + protected ExportMarshalComplex (IntPtr handle, Android.Runtime.JniHandleOwnership transfer) + : base (handle, transfer) + { + } + + [Java.Interop.Export ("mutateInts")] + public void MutateInts (int[] values) { } + + [Java.Interop.Export ("roundTripEnum")] + public ExportSampleEnum RoundTripEnum (ExportSampleEnum value) { return value; } + + [Java.Interop.Export ("echoCharSequence")] + public Java.Lang.ICharSequence EchoCharSequence (Java.Lang.ICharSequence value) { return value; } + + [Java.Interop.Export ("echoViews")] + public Android.Views.View[] EchoViews (Android.Views.View[] values) { return values; } + + [Java.Interop.Export ("echoStrings")] + public string[] EchoStrings (string[] values) { return values; } + } + + /// + /// Comprehensive [Export] member scenarios ported from legacy ExportsMembers. + /// Tests: name override, throws, empty throws, static methods. + /// + [Register ("my/app/ExportMembersComprehensive")] + public class ExportMembersComprehensive : Java.Lang.Object + { + protected ExportMembersComprehensive (IntPtr handle, Android.Runtime.JniHandleOwnership transfer) + : base (handle, transfer) + { + } + + [Java.Interop.Export] + public void methodNamesNotMangled () { } + + [Java.Interop.Export ("attributeOverridesNames")] + public string CompletelyDifferentName (string value, int count) { return value; } + + [Java.Interop.Export (ThrownNames = new [] { "java.lang.Throwable" })] + public void methodThatThrows () { } + + [Java.Interop.Export (ThrownNames = new string [0])] + public void methodThatThrowsEmptyArray () { } + } + + /// + /// [Export] constructor with SuperArgumentsString. + /// The super() call should use the custom args, not forward all params. + /// + [Register ("my/app/ExportCtorWithSuperArgs")] + public class ExportCtorWithSuperArgs : Java.Lang.Object + { + protected ExportCtorWithSuperArgs (IntPtr handle, Android.Runtime.JniHandleOwnership transfer) + : base (handle, transfer) + { + } + + [Java.Interop.Export (SuperArgumentsString = "")] + public ExportCtorWithSuperArgs (int value) { } + } + + /// + /// Static [Export] method and [ExportField] declarations. + /// + [Register ("my/app/ExportStaticAndFields")] + public class ExportStaticAndFields : Java.Lang.Object + { + protected ExportStaticAndFields (IntPtr handle, Android.Runtime.JniHandleOwnership transfer) + : base (handle, transfer) + { + } + + [Java.Interop.ExportField ("STATIC_INSTANCE")] + public static ExportStaticAndFields GetInstance () + { + return null!; + } + + [Java.Interop.ExportField ("VALUE")] + public string GetValue () + { + return "value"; + } + + [Java.Interop.Export] + public static void staticMethodNotMangled () + { + } + + [Java.Interop.Export] + public void instanceMethod () { } + } +}