Skip to content
This repository was archived by the owner on Jul 6, 2026. It is now read-only.

Commit 6bc87e8

Browse files
authored
[jcw-gen] Use + for nested types, not / (#1304)
Context: dotnet/android#9747 Context: https://discord.com/channels/732297728826277939/732297837953679412/1336353039031734352 Context: https://discord.com/channels/732297728826277939/732297837953679412/1336358257769316372 Context: #1302 Context: dotnet/android#9750 The `[Register]` attribute provides "connector method" names, and for interface methods this will also include the name of the type which declares the method, which itself may be in a nested type: namespace Android.App { public partial class Application { public partial interface IActivityLifecycleCallbacks : IJavaObject, IJavaPeerable { [Register ( name: "onActivityCreated", signature: "(Landroid/app/Activity;Landroid/os/Bundle;)V", connector: "GetOnActivityCreated_Landroid_app_Activity_Landroid_os_Bundle_Handler:Android.App.Application/IActivityLifecycleCallbacksInvoker, Mono.Android, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null")] void OnActivityCreated (Android.App.Activity activity, Android.OS.Bundle? savedInstanceState); // … } } } The `connector` value is copied as-is into Java Callable Wrappers, as part of the `__md_methods` value and `Runtime.register()` call. Given the C# type: partial class MauiApplication : Application { partial class ActivityLifecycleCallbacks : Java.Lang.Object, Application.IActivityLifecycleCallbacks { public void OnActivityCreated (Activity activity, Bundle? savedInstanceState) => … } } then `Java.Interop.Tools.JavaCallableWrappers` will produce: // Java Callable Wrapper /* partial */ class MauiApplication_ActivityLifecycleCallbacks { public static final String __md_methods; static { __md_methods = // … "n_onActivityCreated:(Landroid/app/Activity;Landroid/os/Bundle;)V:GetOnActivityCreated_Landroid_app_Activity_Landroid_os_Bundle_Handler:Android.App.Application/IActivityLifecycleCallbacksInvoker, Mono.Android, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null\n" + // … ""; mono.android.Runtime.register ("Microsoft.Maui.MauiApplication+ActivityLifecycleCallbacks, Microsoft.Maui", MauiApplication_ActivityLifecycleCallbacks.class, __md_methods); } } The `signature` and `connector` values from the `[Register(…)]` on the method declaration are copied as-is into `__md_methods`. As the `connector` value contains a `/`, `__md_methods` does as well. This has worked fine for nearly 15+ years…on Mono/MonoVM. This *fails* on NativeAOT and CoreCLR, as: Type.GetType ("Android.App.Application/IActivityLifecycleCallbacksInvoker, Mono.Android, …", throwOnError:true) fails with: TypeLoadException: Could not resolve type 'Android.App.Application/IActivityLifecycleCallbacksInvoker' in assembly 'Mono.Android, …'. The reason for the failure is that when using Reflection APIs such as `Type.GetType()`, the [`Type.AssemblyQualifiedName` grammar][0] must be followed, and that grammar uses `+` to "Precede a nested class", *not* `/`. (`/` isn't even in the Reflection grammar!) (Aside: where does `/` come from then? It's the *IL* separator for nested types!) For eventual CoreCLR and NativeAOT support, then, we need to replace `/` with `+` *somewhere* before it hits `Type.GetType()`. There are (at least?) three places to do so: 1. Within `JniRuntime.JniTypeManager.RegisterNativeMembers()` or equivalent override. 2. Within `generator`, updating the contents of `[Register]`. 3. Within `Java.Interop.Tools.JavaCallableWrappers`. (1) is rejected out of hand as it would be additional work done on- device at runtime. Why do that if we don't need to? (2) was attempted in #1302 & dotnet/android#9750. It turned into a bit of a boondoggle, because there are lots of linker steps which interpret the `connector` value on `[Register]`, and it "just worked" that the `connector` value contained IL names, as the linker steps deal in IL! Trying to update `connector` to instead contain Reflection syntax required finding all the places in the linker that used `connector` values, which was tedious & brittle. "Just" (2) is also inadequate, as it would require new binding assemblies to take advantage of, so (2) *also* needed (3). Which brings us to (3), the current approach: *don't* alter the semantics of the `connect` value within `[Register]`, and instead require that `Java.Interop.Tools.JavaCallableWrappers` replace all instances of `/` with `+` within `__md_methods`. This is needed *anyway*, for compatibility with existing bindings, and also avoids lots of pain that (2) encountered. With this approach, `generator` output is unchanged, and `jcw-gen` output for `MauiApplication.ActivityLifecycleCallbacks` becomes: // Java Callable Wrapper /* partial */ class MauiApplication_ActivityLifecycleCallbacks { public static final String __md_methods; static { __md_methods = // … "n_onActivityCreated:(Landroid/app/Activity;Landroid/os/Bundle;)V:GetOnActivityCreated_Landroid_app_Activity_Landroid_os_Bundle_Handler:Android.App.Application+IActivityLifecycleCallbacksInvoker, Mono.Android, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null\n" + // … ""; mono.android.Runtime.register ("Microsoft.Maui.MauiApplication+ActivityLifecycleCallbacks, Microsoft.Maui", MauiApplication_ActivityLifecycleCallbacks.class, __md_methods); } } [0]: https://learn.microsoft.com/en-us/dotnet/api/system.type.assemblyqualifiedname?view=net-9.0#remarks
1 parent dd3c1d0 commit 6bc87e8

7 files changed

Lines changed: 96 additions & 6 deletions

File tree

‎src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers.Adapters/CecilImporter.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ static CallableWrapperMethod CreateMethod (MethodDefinition methodDefinition, Ca
269269
staticCallableWrapperMethodCreateMethod(stringname,CallableWrapperTypedeclaringType,string?signature,string?connector,string?managedParameters,string?outerType,string?superCall)
270270
{
271271
signature=signature??thrownewArgumentNullException("`connector` cannot be null.",nameof(connector));
272-
varmethod_name="n_"+name+":"+signature+":"+connector;
272+
varmethod_name="n_"+name+":"+signature+":"+connector?.Replace('/','+');
273273

274274
varmethod=newCallableWrapperMethod(declaringType,name,method_name,signature);
275275

‎src/Java.Interop.Tools.TypeNameMappings/Java.Interop.Tools.TypeNameMappings/JavaNativeTypeManager.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ static string ToCliTypePart (string part)
150150
for(inti=0;i<parts.Length;++i){
151151
parts[i]=ToPascalCase(parts[i],1);
152152
}
153-
returnstring.Join("/",parts);
153+
returnstring.Join("+",parts);
154154
}
155155

156156
staticstringToPascalCase(stringvalue,intminLength)

‎tests/Java.Interop.Export-Tests/Java.Interop.Export-Tests.csproj‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@
2626
<ProjectReferenceInclude="..\..\src\Java.Interop.Export\Java.Interop.Export.csproj" />
2727
<ProjectReferenceInclude="..\..\src\Java.Runtime.Environment\Java.Runtime.Environment.csproj" />
2828
<ProjectReferenceInclude="..\TestJVM\TestJVM.csproj" />
29+
<ProjectReference
30+
Include="..\..\tools\jcw-gen\jcw-gen.csproj"
31+
ReferenceOutputAssembly="false"
32+
/>
2933
</ItemGroup>
3034

3135
<ItemGroup>

‎tests/Java.Interop.Tools.JavaCallableWrappers-Tests/Java.Interop.Tools.JavaCallableWrappers/JavaCallableWrapperGeneratorTests.cs‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,55 @@ public void monodroidClearReferences ()
104104
Assert.AreEqual(expected,actual);
105105
}
106106

107+
[Test]
108+
publicvoidGenerateTypeMentioningNestedInvoker()
109+
{
110+
varactual=Generate(typeof(ApplicationName.ActivityLifecycleCallbacks));
111+
varexpected="""
112+
package application;
113+
114+
115+
public class Name_ActivityLifecycleCallbacks
116+
extends java.lang.Object
117+
implements
118+
mono.android.IGCUserPeer,
119+
java.lang.Object
120+
{
121+
/** @hide */
122+
public static final String __md_methods;
123+
static {
124+
__md_methods =
125+
"n_onActivityCreated:(Landroid/app/Activity;Landroid/os/Bundle;)V:GetOnActivityCreated_Landroid_app_Activity_Landroid_os_Bundle_Handler:Android.App.Application+IActivityLifecycleCallbacksInvoker, Mono.Android\n" +
126+
"";
127+
mono.android.Runtime.register ("Xamarin.Android.ToolsTests.ApplicationName+ActivityLifecycleCallbacks, Java.Interop.Tools.JavaCallableWrappers-Tests", Name_ActivityLifecycleCallbacks.class, __md_methods);
128+
}
129+
130+
public void onActivityCreated (android.app.Activity p0, android.os.Bundle p1)
131+
{
132+
n_onActivityCreated (p0, p1);
133+
}
134+
135+
private native void n_onActivityCreated (android.app.Activity p0, android.os.Bundle p1);
136+
137+
private java.util.ArrayList refList;
138+
public void monodroidAddReference (java.lang.Object obj)
139+
{
140+
if (refList == null)
141+
refList = new java.util.ArrayList ();
142+
refList.add (obj);
143+
}
144+
145+
public void monodroidClearReferences ()
146+
{
147+
if (refList != null)
148+
refList.clear ();
149+
}
150+
}
151+
152+
""";
153+
Assert.AreEqual(expected,actual);
154+
}
155+
107156
staticstringGenerate(Typetype,stringapplicationJavaClass=null,stringmonoRuntimeInit=null,JavaPeerStylestyle=JavaPeerStyle.XAJavaInterop1)
108157
{
109158
varreader_options=newCallableWrapperReaderOptions{

‎tests/Java.Interop.Tools.JavaCallableWrappers-Tests/Java.Interop.Tools.JavaCallableWrappers/SupportDeclarations.cs‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,26 @@ class Application : Java.Lang.Object
2222
protectedvirtualvoidOnCreate()
2323
{
2424
}
25+
26+
[Register("android/app/Application$ActivityLifecycleCallbacks",DoNotGenerateAcw=true)]
27+
publicpartialinterfaceIActivityLifecycleCallbacks{
28+
[Register(
29+
name:"onActivityCreated",
30+
signature:"(Landroid/app/Activity;Landroid/os/Bundle;)V",
31+
connector:"GetOnActivityCreated_Landroid_app_Activity_Landroid_os_Bundle_Handler:Android.App.Application/IActivityLifecycleCallbacksInvoker, Mono.Android")]
32+
voidOnActivityCreated(Android.App.Activityactivity,global::Android.OS.BundlesavedInstanceState);
33+
}
34+
35+
internalclassIActivityLifecycleCallbacksInvoker:Java.Lang.Object,IActivityLifecycleCallbacks{
36+
staticDelegateGetOnActivityCreated_Landroid_app_Activity_Landroid_os_Bundle_Handler()
37+
{
38+
returnnull;
39+
}
40+
41+
publicvoidOnActivityCreated(Android.App.Activityactivity,global::Android.OS.BundlesavedInstanceState)
42+
{
43+
}
44+
}
2545
}
2646

2747
[Register("android/app/Activity",DoNotGenerateAcw=true)]
@@ -43,6 +63,14 @@ public virtual void OnCreate (Java.Lang.Object arguments)
4363
}
4464
}
4565

66+
namespaceAndroid.OS{
67+
68+
[Register("android/os/Bundle",DoNotGenerateAcw=true)]
69+
classBundle:Java.Lang.Object
70+
{
71+
}
72+
}
73+
4674
namespaceAndroid.Runtime{
4775

4876
interfaceIJavaObject
@@ -72,6 +100,7 @@ static class SupportDeclarations
72100
typeof(AbstractClass),
73101
typeof(ActivityName),
74102
typeof(ApplicationName),
103+
typeof(ApplicationName.ActivityLifecycleCallbacks),
75104
typeof(DefaultName),
76105
typeof(DefaultName.A),
77106
typeof(DefaultName.A.B),
@@ -139,6 +168,12 @@ class ActivityName : Java.Lang.Object
139168
[Application(Name="application.Name")]
140169
classApplicationName:Application
141170
{
171+
publicclassActivityLifecycleCallbacks:Java.Lang.Object,Application.IActivityLifecycleCallbacks
172+
{
173+
publicvoidOnActivityCreated(Activityactivity,global::Android.OS.BundlesavedInstanceState)
174+
{
175+
}
176+
}
142177
}
143178

144179
classIndirectApplication:ApplicationName

‎tests/Java.Interop.Tools.JavaCallableWrappers-Tests/Java.Interop.Tools.JavaCallableWrappers/TypeNameMapGeneratorTests.cs‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public void WriteJavaToManaged ()
5050
v.WriteJavaToManaged(o);
5151
vara=ToArray(o);
5252
Save(a,"__j2m");
53-
varlength=190;
53+
varlength=193;
5454
varoffset=76;
5555
vare=
5656
"version=1\u0000"+
@@ -59,6 +59,7 @@ public void WriteJavaToManaged ()
5959
"value-offset="+offset+"\u0000"+
6060
GetJ2MEntryLine(typeof(ActivityName),"activity/Name",offset,length)+
6161
GetJ2MEntryLine(typeof(ApplicationName),"application/Name",offset,length)+
62+
GetJ2MEntryLine(typeof(ApplicationName.ActivityLifecycleCallbacks),"application/Name_ActivityLifecycleCallbacks",offset,length)+
6263
GetJ2MEntryLine(typeof(DefaultName),"crc64197ae30a36756915/DefaultName",offset,length)+
6364
GetJ2MEntryLine(typeof(DefaultName.A),"crc64197ae30a36756915/DefaultName_A",offset,length)+
6465
GetJ2MEntryLine(typeof(DefaultName.A.B),"crc64197ae30a36756915/DefaultName_A_B",offset,length)+
@@ -128,8 +129,8 @@ public void WriteManagedToJava ()
128129
v.WriteManagedToJava(o);
129130
vara=ToArray(o);
130131
Save(a,"__m2j");
131-
varlength=190;
132-
varoffset=114;
132+
varlength=193;
133+
varoffset=117;
133134
vare=
134135
"version=1\u0000"+
135136
$"entry-count={types.Count}\u0000"+
@@ -138,6 +139,7 @@ public void WriteManagedToJava ()
138139
GetM2JEntryLine(typeof(AbstractClass),"my/AbstractClass",offset,length)+
139140
GetM2JEntryLine(typeof(AbstractClassInvoker),"my/AbstractClass",offset,length)+
140141
GetM2JEntryLine(typeof(ActivityName),"activity/Name",offset,length)+
142+
GetM2JEntryLine(typeof(ApplicationName.ActivityLifecycleCallbacks),"application/Name_ActivityLifecycleCallbacks",offset,length)+
141143
GetM2JEntryLine(typeof(ApplicationName),"application/Name",offset,length)+
142144
GetM2JEntryLine(typeof(DefaultName.A.B),"crc64197ae30a36756915/DefaultName_A_B",offset,length)+
143145
GetM2JEntryLine(typeof(DefaultName.A),"crc64197ae30a36756915/DefaultName_A",offset,length)+

‎tools/generator/Java.Interop.Tools.Generator.ObjectModel/ClassGen.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ public static void GenerateTypeRegistrations (CodeGenerationOptions opt, Generat
181181
intls=reg.Key.LastIndexOf('/');
182182
stringpackage=ls>=0?reg.Key.Substring(0,ls):"";
183183

184-
if(JavaNativeTypeManager.ToCliType(reg.Key)==reg.Value)
184+
if(JavaNativeTypeManager.ToCliType(reg.Key)==reg.Value.Replace('/','+'))
185185
continue;
186186
if(!mapping.TryGetValue(package,outvarv))
187187
mapping.Add(package,v=newList<KeyValuePair<string,string>>());

0 commit comments

Comments
 (0)