diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs index 7e41fc570d7..d629fc83d2c 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs @@ -122,15 +122,28 @@ static void WriteClassDeclaration (JavaPeerInfo type, TextWriter writer) static void WriteStaticInitializer (JavaPeerInfo type, TextWriter writer) { + string className = JniSignatureHelper.GetJavaSimpleName (type.JavaName); + // Application and Instrumentation types cannot call registerNatives in their - // static initializer — the native library isn't loaded yet at that point. - // Their registerNatives call is emitted in the generated - // ApplicationRegistration.registerApplications() method instead. + // static initializer — the runtime isn't ready yet at that point. Emit a + // lazy one-time helper instead so the first managed callback can register + // the class just before invoking its native method. if (type.CannotRegisterInStaticConstructor) { + writer.Write ($$""" + private static boolean __md_natives_registered; + private static synchronized void __md_registerNatives () + { + if (!__md_natives_registered) { + mono.android.Runtime.registerNatives ({{className}}.class); + __md_natives_registered = true; + } + } + + +"""); return; } - string className = JniSignatureHelper.GetJavaSimpleName (type.JavaName); writer.Write ($$""" static { mono.android.Runtime.registerNatives ({{className}}.class); @@ -154,7 +167,17 @@ static void WriteConstructors (JavaPeerInfo type, TextWriter writer) public {{simpleClassName}} ({{parameters}}) { super ({{superArgs}}); + +"""); + + if (!type.CannotRegisterInStaticConstructor) { + writer.Write ($$""" if (getClass () == {{simpleClassName}}.class) nctor_{{ctor.ConstructorIndex}} ({{args}}); + +"""); + } + + writer.Write ($$""" } @@ -197,6 +220,10 @@ static void WriteFields (JavaPeerInfo type, TextWriter writer) static void WriteMethods (JavaPeerInfo type, TextWriter writer) { + string registerNativesLine = type.CannotRegisterInStaticConstructor + ? "\t\t__md_registerNatives ();\n" + : ""; + foreach (var method in type.MarshalMethods) { if (method.IsConstructor) { continue; @@ -222,7 +249,7 @@ static void WriteMethods (JavaPeerInfo type, TextWriter writer) @Override public {{javaReturnType}} {{method.JniName}} ({{parameters}}){{throwsClause}} { - {{returnPrefix}}{{method.NativeCallbackName}} ({{args}}); +{{registerNativesLine}} {{returnPrefix}}{{method.NativeCallbackName}} ({{args}}); } public native {{javaReturnType}} {{method.NativeCallbackName}} ({{parameters}}); @@ -233,7 +260,7 @@ static void WriteMethods (JavaPeerInfo type, TextWriter writer) {{access}} {{javaReturnType}} {{method.JniName}} ({{parameters}}){{throwsClause}} { - {{returnPrefix}}{{method.NativeCallbackName}} ({{args}}); +{{registerNativesLine}} {{returnPrefix}}{{method.NativeCallbackName}} ({{args}}); } {{access}} native {{javaReturnType}} {{method.NativeCallbackName}} ({{parameters}}); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index a8b01c45e28..d61d716cbb3 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text; namespace Microsoft.Android.Sdk.TrimmableTypeMap; @@ -13,6 +14,8 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// static class ModelBuilder { + const string ProxyTypeSuffix = "_Proxy"; + static readonly HashSet EssentialRuntimeTypes = new (StringComparer.Ordinal) { "java/lang/Object", "java/lang/Class", @@ -171,11 +174,25 @@ static void AddIfCrossAssembly (SortedSet set, string? asmName, string o } } + static string ManagedTypeNameToProxyTypeName (string managedTypeName) + { + var builder = new StringBuilder (managedTypeName.Length + ProxyTypeSuffix.Length); + for (int i = 0; i < managedTypeName.Length; i++) { + char c = managedTypeName [i]; + builder.Append (c == '.' || c == '+' || c == '`' ? '_' : c); + } + + builder.Append (ProxyTypeSuffix); + return builder.ToString (); + } + static JavaPeerProxyData BuildProxyType (JavaPeerInfo peer, string jniName, HashSet usedProxyNames, 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"; + // Replace generic arity markers too, because backticks would make the emitted + // proxy type itself look generic even though we don't emit generic parameters. + var proxyTypeName = ManagedTypeNameToProxyTypeName (peer.ManagedTypeName); // Guard against name collisions (e.g., "My.Type" and "My_Type" both map to "My_Type_Proxy") if (!usedProxyNames.Add (proxyTypeName)) { diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 5efa08b058e..08ee7cd337d 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -6,7 +6,6 @@ using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; using System.Reflection.PortableExecutable; - namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// @@ -732,7 +731,7 @@ bool TryResolveBaseType (TypeDefinition typeDef, AssemblyIndex index, (RegisterInfo Info, string DeclaringTypeName, string DeclaringAssemblyName)? FindBaseRegisteredMethodInfo ( TypeDefinition typeDef, AssemblyIndex index, string methodName, MethodDefinition derivedMethod) { - if (!TryResolveBaseType (typeDef, index, out var baseTypeDef, out var baseHandle, out var baseIndex, out var baseTypeName, out var baseAssemblyName)) { + if (!TryResolveBaseType (typeDef, index, out var baseTypeDef, out _, out var baseIndex, out var baseTypeName, out var baseAssemblyName)) { return null; } @@ -760,10 +759,8 @@ bool TryResolveBaseType (TypeDefinition typeDef, AssemblyIndex index, } } - // Recurse up the hierarchy (stop at DoNotGenerateAcw boundary) - if (baseIndex.RegisterInfoByType.TryGetValue (baseHandle, out var baseRegInfo) && baseRegInfo.DoNotGenerateAcw) { - return null; - } + // Keep walking the full base hierarchy so overrides can inherit [Register] + // metadata declared above an intermediate MCW base type. return FindBaseRegisteredMethodInfo (baseTypeDef, baseIndex, methodName, derivedMethod); } @@ -796,7 +793,7 @@ bool TryResolveBaseType (TypeDefinition typeDef, AssemblyIndex index, MarshalMethodInfo? FindBaseRegisteredProperty (TypeDefinition typeDef, AssemblyIndex index, string getterName, MethodDefinition derivedGetter) { - if (!TryResolveBaseType (typeDef, index, out var baseTypeDef, out var baseHandle, out var baseIndex, out var baseTypeName, out var baseAssemblyName)) { + if (!TryResolveBaseType (typeDef, index, out var baseTypeDef, out _, out var baseIndex, out var baseTypeName, out var baseAssemblyName)) { return null; } @@ -835,10 +832,8 @@ bool TryResolveBaseType (TypeDefinition typeDef, AssemblyIndex index, } } - // Recurse up (stop at DoNotGenerateAcw boundary) - if (baseIndex.RegisterInfoByType.TryGetValue (baseHandle, out var baseRegInfo) && baseRegInfo.DoNotGenerateAcw) { - return null; - } + // Keep walking the full base hierarchy so property overrides can inherit + // [Register] metadata declared above an intermediate MCW base type. return FindBaseRegisteredProperty (baseTypeDef, baseIndex, getterName, derivedGetter); } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs index 95afe0971f0..30d81218883 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs @@ -138,16 +138,20 @@ public void Generate_AcwType_HasRegisterNativesStaticBlock () public void Generate_ApplicationType_SkipsRegisterNatives () { var java = GenerateFixture ("my/app/MyApplication"); - Assert.DoesNotContain ("registerNatives", java); Assert.DoesNotContain ("static {", java); + Assert.DoesNotContain ("if (getClass () == MyApplication.class) nctor_0 ();", java); + AssertContainsLine ("private static synchronized void __md_registerNatives ()\n", java); + AssertContainsLine ("mono.android.Runtime.registerNatives (MyApplication.class);\n", java); } [Fact] public void Generate_InstrumentationType_SkipsRegisterNatives () { var java = GenerateFixture ("my/app/MyInstrumentation"); - Assert.DoesNotContain ("registerNatives", java); Assert.DoesNotContain ("static {", java); + Assert.DoesNotContain ("if (getClass () == MyInstrumentation.class) nctor_0 ();", java); + AssertContainsLine ("private static synchronized void __md_registerNatives ()\n", java); + AssertContainsLine ("mono.android.Runtime.registerNatives (MyInstrumentation.class);\n", java); } } @@ -257,6 +261,61 @@ public void Generate_MarshalMethod_HasOverrideAndNativeDeclaration () AssertContainsLine ("public native void n_OnCreate_Landroid_os_Bundle_ (android.os.Bundle p0);\n", java); } + [Fact] + public void Generate_OverrideAcrossIntermediateMcwBase_HasMethodStub () + { + var java = GenerateFixture ("my/app/SelectableList"); + AssertContainsLine ("@Override\n", java); + AssertContainsLine ("public void setSelection (int p0)\n", java); + AssertContainsLine ("n_SetSelection_I (p0);\n", java); + AssertContainsLine ("public native void n_SetSelection_I (int p0);\n", java); + } + + [Fact] + public void Generate_OverrideAcrossGenericIntermediateMcwBase_HasMethodStub () + { + var java = GenerateFixture ("my/app/GenericSelectableList"); + AssertContainsLine ("@Override\n", java); + AssertContainsLine ("public void setSelection (int p0)\n", java); + AssertContainsLine ("n_SetSelection_I (p0);\n", java); + AssertContainsLine ("public native void n_SetSelection_I (int p0);\n", java); + } + + [Fact] + public void Generate_DeferredRegistrationType_LazilyRegistersBeforeNativeCallback () + { + var type = new JavaPeerInfo { + JavaName = "my/app/DeferredInstrumentation", + CompatJniName = "my/app/DeferredInstrumentation", + ManagedTypeName = "MyApp.DeferredInstrumentation", + ManagedTypeNamespace = "MyApp", + ManagedTypeShortName = "DeferredInstrumentation", + AssemblyName = "App", + BaseJavaName = "android/app/Instrumentation", + CannotRegisterInStaticConstructor = true, + MarshalMethods = new List { + new () { + JniName = "onCreate", + JniSignature = "(Landroid/os/Bundle;)V", + ManagedMethodName = "OnCreate", + NativeCallbackName = "n_OnCreate_Landroid_os_Bundle_", + Connector = "GetOnCreate_Landroid_os_Bundle_Handler", + }, + new () { + JniName = "onStart", + JniSignature = "()V", + ManagedMethodName = "OnStart", + NativeCallbackName = "n_OnStart", + Connector = "GetOnStartHandler", + }, + }, + }; + + var java = GenerateToString (type); + AssertContainsLine ("__md_registerNatives ();\n\t\tn_OnCreate_Landroid_os_Bundle_ (p0);\n", java); + AssertContainsLine ("__md_registerNatives ();\n\t\tn_OnStart ();\n", java); + } + } public class NestedType diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index c07340ae981..665b98a94b9 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -68,6 +68,19 @@ public void Execute_WithTestFixtures_ProducesOutputs () Assert.Contains (result.GeneratedAssemblies, a => a.Name == "_TestFixtures.TypeMap"); } + [Fact] + public void Execute_CollectsDeferredRegistrationTypes_ForConcreteApplicationAndInstrumentation () + { + using var peReader = CreateTestFixturePEReader (); + var result = CreateGenerator ().Execute (new List<(string, PEReader)> { ("TestFixtures", peReader) }, new Version (11, 0), new HashSet ()); + + Assert.Contains ("my.app.MyApplication", result.ApplicationRegistrationTypes); + Assert.Contains ("my.app.MyInstrumentation", result.ApplicationRegistrationTypes); + Assert.DoesNotContain ("my.app.BaseApplication", result.ApplicationRegistrationTypes); + Assert.DoesNotContain ("my.app.BaseInstrumentation", result.ApplicationRegistrationTypes); + Assert.DoesNotContain ("my.app.IntermediateInstrumentation", result.ApplicationRegistrationTypes); + } + [Fact] public void Execute_NullAssemblyList_Throws () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 4e7d17f3c6a..a99d95bfca0 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -246,7 +246,13 @@ public void Generate_GenericType_ThrowsNotSupportedException () using var pe = new PEReader (stream); var reader = pe.GetMetadataReader (); var typeNames = GetTypeRefNames (reader); + var generatedTypeNames = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .Select (t => reader.GetString (t.Name)) + .ToList (); Assert.Contains ("NotSupportedException", typeNames); + Assert.Contains ("MyApp_Generic_GenericHolder_1_Proxy", generatedTypeNames); + Assert.DoesNotContain (generatedTypeNames, name => name.Contains ('`')); } [Fact] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 59cbf878229..df3a1256763 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -166,6 +166,7 @@ public class ProxyTypes [Theory] [InlineData ("java/lang/Object", "Java.Lang.Object", "Mono.Android", "Java_Lang_Object_Proxy")] [InlineData ("com/example/Outer$Inner", "Com.Example.Outer.Inner", "App", "Com_Example_Outer_Inner_Proxy")] + [InlineData ("my/app/GenericHolder", "MyApp.Generic.GenericHolder`1", "App", "MyApp_Generic_GenericHolder_1_Proxy")] public void Build_PeerWithActivation_CreatesNamedProxy (string jniName, string managedName, string asmName, string expectedProxyName) { var peer = MakePeerWithActivation (jniName, managedName, asmName); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/OverrideDetectionTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/OverrideDetectionTests.cs index 54f7e0e133a..0213fd3ebb6 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/OverrideDetectionTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/OverrideDetectionTests.cs @@ -102,6 +102,24 @@ public void MultipleOverloads_PicksCorrectOne () Assert.DoesNotContain (nonCtorMethods, m => m.JniName == "process" && m.JniSignature == "()V"); } + [Fact] + public void OverrideAcrossIntermediateMcwBase_Detected () + { + var peer = FindFixtureByJavaName ("my/app/SelectableList"); + var setSelection = Assert.Single (peer.MarshalMethods, m => m.JniName == "setSelection"); + Assert.Equal ("(I)V", setSelection.JniSignature); + Assert.Equal ("GetSetSelection_IHandler", setSelection.Connector); + } + + [Fact] + public void OverrideAcrossGenericIntermediateMcwBase_Detected () + { + var peer = FindFixtureByJavaName ("my/app/GenericSelectableList"); + var setSelection = Assert.Single (peer.MarshalMethods, m => m.JniName == "setSelection"); + Assert.Equal ("(I)V", setSelection.JniSignature); + Assert.Equal ("GetSetSelection_IHandler", setSelection.Connector); + } + [Fact] public void EmptyConnector_OverrideStillDetected () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs index eac273ba746..776ca77c494 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs @@ -49,6 +49,20 @@ public class Service : Java.Lang.Object { protected Service (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } } + + [Register ("android/app/Application", DoNotGenerateAcw = true)] + public class Application : Java.Lang.Object + { + public Application () { } + protected Application (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } + + [Register ("android/app/Instrumentation", DoNotGenerateAcw = true)] + public class Instrumentation : Java.Lang.Object + { + public Instrumentation () { } + protected Instrumentation (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } } namespace Android.App.Backup @@ -317,8 +331,11 @@ public void PublicMethod () { } protected void ProtectedMethod () { } } + [Register ("my/app/BaseApplication")] + public abstract class BaseApplication : Android.App.Application { } + [Application (Name = "my.app.MyApplication", BackupAgent = typeof (MyBackupAgent), ManageSpaceActivity = typeof (MyManageSpaceActivity))] - public class MyApplication : Java.Lang.Object { } + public class MyApplication : BaseApplication { } /// /// Has [ExportField] methods that should produce Java field declarations. @@ -335,8 +352,14 @@ protected ExportFieldExample (IntPtr handle, JniHandleOwnership transfer) : base public string GetValue () => ""; } + [Register ("my/app/BaseInstrumentation")] + public abstract class BaseInstrumentation : Android.App.Instrumentation { } + + [Register ("my/app/IntermediateInstrumentation")] + public abstract class IntermediateInstrumentation : BaseInstrumentation { } + [Instrumentation (Name = "my.app.MyInstrumentation")] - public class MyInstrumentation : Java.Lang.Object { } + public class MyInstrumentation : IntermediateInstrumentation { } [Register ("my/app/MyBackupAgent")] public class MyBackupAgent : Android.App.Backup.BackupAgent @@ -751,6 +774,72 @@ protected OverloadDerived (IntPtr handle, JniHandleOwnership transfer) : base (h public override void Process (int value) { } } + /// + /// Declares a registered abstract method above an intermediate MCW base type. + /// Mirrors AdapterView.SetSelection(int) for AbsListView-derived test fixtures. + /// + [Register ("my/app/SelectionHost", DoNotGenerateAcw = true)] + public abstract class SelectionHost : Java.Lang.Object + { + protected SelectionHost (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + + [Register ("setSelection", "(I)V", "GetSetSelection_IHandler")] + public abstract void SetSelection (int position); + } + + /// + /// Intermediate MCW base that inherits the registered method without redeclaring it. + /// + [Register ("my/app/SelectionContainer", DoNotGenerateAcw = true)] + public abstract class SelectionContainer : SelectionHost + { + protected SelectionContainer (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } + + /// + /// Generic base used to verify override discovery through a generic-instantiated base type. + /// Mirrors AdapterView<T> in the real Mono.Android hierarchy. + /// + [Register ("my/app/GenericSelectionHost", DoNotGenerateAcw = true)] + public abstract class GenericSelectionHost : Java.Lang.Object where T : class + { + protected GenericSelectionHost (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + + [Register ("setSelection", "(I)V", "GetSetSelection_IHandler")] + public abstract void SetSelection (int position); + } + + /// + /// Intermediate MCW base that closes the generic base. + /// + [Register ("my/app/GenericSelectionContainer", DoNotGenerateAcw = true)] + public abstract class GenericSelectionContainer : GenericSelectionHost + { + protected GenericSelectionContainer (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } + + /// + /// Overrides a registered method declared above the first MCW base in the hierarchy. + /// + [Register ("my/app/SelectableList")] + public class SelectableList : SelectionContainer + { + protected SelectableList (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + + public override void SetSelection (int position) { } + } + + /// + /// Overrides a registered method declared above a generic-instantiated MCW base. + /// + [Register ("my/app/GenericSelectableList")] + public class GenericSelectableList : GenericSelectionContainer + { + protected GenericSelectableList (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + + public override void SetSelection (int position) { } + } + /// /// Has a ctor with unsigned primitive params to test JNI mapping. ///