Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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 ($$"""
}


Expand Down Expand Up @@ -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;
Expand All @@ -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}});

Expand All @@ -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}});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;

namespace Microsoft.Android.Sdk.TrimmableTypeMap;

Expand All @@ -13,6 +14,8 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap;
/// </summary>
static class ModelBuilder
{
const string ProxyTypeSuffix = "_Proxy";

static readonly HashSet<string> EssentialRuntimeTypes = new (StringComparer.Ordinal) {
"java/lang/Object",
"java/lang/Class",
Expand Down Expand Up @@ -171,11 +174,25 @@ static void AddIfCrossAssembly (SortedSet<string> 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<string> 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)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
using System.Reflection.PortableExecutable;

namespace Microsoft.Android.Sdk.TrimmableTypeMap;

/// <summary>
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

}
Expand Down Expand Up @@ -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<MarshalMethodInfo> {
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);
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 💡 Testing — This nicely locks in the per-callback __md_registerNatives() fallback. Since CannotRegisterInStaticConstructor still also depends on ApplicationRegistration.registerApplications() for the startup path, consider adding one regression test for result.ApplicationRegistrationTypes or the generated ApplicationRegistration.java as well. That would keep both halves of the deferred-registration contract covered if this generator gets refactored again.

Rule: Add regression coverage for the full bug-fix path.

AssertContainsLine ("__md_registerNatives ();\n\t\tn_OnStart ();\n", java);
}

}

public class NestedType
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> ());

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 ()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 ()
{
Expand Down
Loading