Skip to content

Commit

Permalink
Allow marking constructors as callable from Java.
Browse files Browse the repository at this point in the history
Continuing on the "activation constructor" train, we want to allow
Java to pass parameters to a constructor.

In .NET Android parlance:

	class Example : Java.Lang.Object {
	    [Export]
	    public Example (int value) {}
	}

Commit 9027591 updated `JavaCallableAttribute` to allow it to be
placed on constructors, but if we try that with
`samples/Hello-NativeAOTFromJNI`, the Java Callable Wrapper
doesn't build:

	// JCW
	/* partial */ class Example extends java.lang.Object {
	    public Example (int p0) {
	        super (p0);
	        if (getClass () == Example.class) ManagedPeer.construct(…);
	    }
	}

To make this work, we need `ExportAttribute.SuperArgumentsString`.

We *could* add this to `JavaCallableAttribute`, but that means
we'd have a property which can only be used from constructors.
(Yes, `ExportAttribute` has this "wonkiness", but that doens't mean
we should continue it!)

Add a new `JavaCallableConstructorAttribute`, which has a new
`SuperConstructorExpression` property, along with `jcw-gen` support.
This allows things to compile:

	// C#
	class Example : Java.Lang.Object {
	    [JavaCallableConstructor (SuperConstructorExpression="")]
	    public Example (int value) {}
	}

	// JCW
	/* partial */ class Example extends java.lang.Object {
	    public Example (int p0) {
	        super ();
	        if (getClass () == Example.class) ManagedPeer.construct(…);
	    }
	}

…but it's not quite right yet, because the wrong constructor is
invoked!

	ManagedPeer.construct (
	    /* self */      this,
	    /* aqn */       "Namespace.Example, Assembly",
	    /* ctor sig */  "",
	    /* arguments */ new java.lang.Object[] { p0 }
	);

Oops!

Update the `Signature`/`SignatureOptions` creation within
`JavaCallableWrapperGenerator.cs` so that `ManagedParameters`
is set for *all* constructor codepaths.

	ManagedPeer.construct (
	    /* self */      this,
	    /* aqn */       "Namespace.Example, Assembly",
	    /* ctor sig */  "System.Int32, System.Runtime",
	    /* arguments */ new java.lang.Object[] { p0 }
	);

This appears to fix a long-standing bug/"thinko" in JCW generation!
  • Loading branch information
jonpryor committed Nov 7, 2023
1 parent 0392169 commit 93a901a
Show file tree
Hide file tree
Showing 9 changed files with 232 additions and 12 deletions.
2 changes: 1 addition & 1 deletion samples/Hello-NativeAOTFromJNI/ManagedType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace Example;
[JniTypeSignature ("example/ManagedType")]
class ManagedType : Java.Lang.Object {

[JavaCallable]
[JavaCallableConstructor]
public ManagedType ()
{
}
Expand Down
2 changes: 1 addition & 1 deletion samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ class NativeAotTypeManager : JniRuntime.JniTypeManager {

#pragma warning disable IL2026
Dictionary<string, Type> typeMappings = new () {
["com/xamarin/java_interop/internal/JavaProxyThrowable"] = typeof (JniEnvironment).Assembly.GetType ("Java.Interop.JavaProxyThrowable", throwOnError: true)!,
["com/xamarin/java_interop/internal/JavaProxyThrowable"] = Type.GetType ("Java.Interop.JavaProxyThrowable, Java.Interop", throwOnError: true)!,
["example/ManagedType"] = typeof (Example.ManagedType),
};
#pragma warning restore IL2026
Expand Down
188 changes: 188 additions & 0 deletions samples/Hello-NativeAOTFromJNI/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,194 @@ mt.getString()=Hello from C#, via Java.Interop!
Note the use of `(cd …; java …)` so that `libHello-NativeAOTFromJNI.dylib` is
in the current working directory, so that it can be found.

# Known Unknowns

With this sample "done" (-ish), there are several "future research directions" to
make NativeAOT + Java *viable*.

## GC

Firstly, there's the open GC question: NativeAOT doesn't provide a "GC Bridge"
like MonoVM does, so how do we support cross-VM object references?

* [Collecting Cyclic Garbage across Foreign Function Interfaces: Who Takes the Last Piece of Cake?](https://pldi23.sigplan.org/details/pldi-2023-pldi/25/Collecting-Cyclic-Garbage-across-Foreign-Function-Interfaces-Who-Takes-the-Last-Piec)
* [`JavaScope`?](https://github.com/jonpryor/java.interop/commits/jonp-registration-scope)
(Less a "solution" and more a "Glorious Workaround".)

## `Type.GetType()`

Next, Java.Interop and .NET Android make *extensive* use of `Type.GetType()`,
which doesn't quite work "the same" in NativeAOT. It works when using a string
constant:

```csharp
var type = Type.GetType ("System.Int32, System.Runtime");
```

It fails if the string comes from "elsewhere", even if it's a type that exists.

Unfortunately, we do this *everywhere* in Java.Interop. Consider this more
complete Java Callable Wrapper fragment:

```java
public class ManagedType
extends java.lang.Object
implements
com.xamarin.java_interop.GCUserPeerable
{
/** @hide */
public static final String __md_methods;
static {
__md_methods =
"getString:()Ljava/lang/String;:__export__\n" +
"";
com.xamarin.java_interop.ManagedPeer.registerNativeMembers (
ManagedType.class,
"Example.ManagedType, Hello-NativeAOTFromJNI",
__md_methods);
}


public ManagedType (int p0)
{
super ();
if (getClass () == ManagedType.class) {
com.xamarin.java_interop.ManagedPeer.construct (
this,
"Example.ManagedType, Hello-NativeAOTFromJNI",
"System.Int32, System.Runtime",
new java.lang.Object[] { p0 });
}
}


public native java.lang.String getString ();
}
```

There are *two* places that assembly-qualified names are used, both of which
currently wind up at `Type.GetType()`:

* `ManagedPeer.RegisterNativeMembers()` is given an assembly-qualified name
to register the `native` methods.
* `ManagedPeer.Construct()` is given a `:`-separated list of assembly-qualified
names for each parameter type. This is done to lookup a `ConstructorInfo`.

This sample "fixes" `ManagedPeer.RegisterNativeMembers()` by adding a new
`JniRuntime.JniTypeManager.RegisterNativeMembers()` overload which *avoids* the
`Type.GetType()` call, which allows `NativeAotTypeManager` to "do something else".

This sample "avoids" `ManagedPeer.Construct()` by not using any parameter types
in the constructor! If we add any, e.g. via this patch:

```diff
diff --git a/samples/Hello-NativeAOTFromJNI/JavaInteropRuntime.cs b/samples/Hello-NativeAOTFromJNI/JavaInteropRuntime.cs
index 607bd73f..7ed83c59 100644
--- a/samples/Hello-NativeAOTFromJNI/JavaInteropRuntime.cs
+++ b/samples/Hello-NativeAOTFromJNI/JavaInteropRuntime.cs
@@ -31,9 +31,18 @@ static class JavaInteropRuntime
ValueManager = new NativeAotValueManager (),
};
runtime = options.CreateJreVM ();
+#pragma warning disable IL2057
+ var t = Type.GetType (CreateTypeName (), throwOnError: true);
+#pragma warning restore IL2057
+ Console.WriteLine ($"# jonp: found System.Int32: {t}");
}
catch (Exception e) {
Console.Error.WriteLine ($"JavaInteropRuntime.init: error: {e}");
}
}
+
+ static string CreateTypeName () =>
+ new System.Text.StringBuilder ().Append ("System").Append (".").Append ("Int32")
+ .Append (", ").Append ("System").Append (".").Append ("Runtime")
+ .ToString ();
}
diff --git a/samples/Hello-NativeAOTFromJNI/ManagedType.cs b/samples/Hello-NativeAOTFromJNI/ManagedType.cs
index c5224a40..5db7af84 100644
--- a/samples/Hello-NativeAOTFromJNI/ManagedType.cs
+++ b/samples/Hello-NativeAOTFromJNI/ManagedType.cs
@@ -5,14 +5,17 @@ using Java.Interop;
[JniTypeSignature ("example/ManagedType")]
class ManagedType : Java.Lang.Object {

- [JavaCallableConstructor]
- public ManagedType ()
+ [JavaCallableConstructor(SuperConstructorExpression="")]
+ public ManagedType (int value)
{
+ this.value = value;
}

+ int value;
+
[JavaCallable ("getString")]
public Java.Lang.String GetString ()
{
- return new Java.Lang.String ("Hello from C#, via Java.Interop!");
+ return new Java.Lang.String ($"Hello from C#, via Java.Interop! Value={value}.");
}
}
diff --git a/samples/Hello-NativeAOTFromJNI/java/com/microsoft/hello_from_jni/App.java b/samples/Hello-NativeAOTFromJNI/java/com/microsoft/hello_from_jni/App.java
index f6d6fff2..f4764cf1 100644
--- a/samples/Hello-NativeAOTFromJNI/java/com/microsoft/hello_from_jni/App.java
+++ b/samples/Hello-NativeAOTFromJNI/java/com/microsoft/hello_from_jni/App.java
@@ -10,7 +10,7 @@ class App {
JavaInteropRuntime.init();
String s = sayHello();
System.out.println("String returned to Java: " + s);
- ManagedType mt = new ManagedType();
+ ManagedType mt = new ManagedType(42);
System.out.println("mt.getString()=" + mt.getString());
}

```

this will fail at runtime:

```
Exception in thread "main" com.xamarin.java_interop.internal.JavaProxyThrowable: System.IO.FileNotFoundException: Could not resolve assembly 'System.Runtime'.
at System.Reflection.TypeNameParser.ResolveAssembly(String) + 0x97
at System.Reflection.TypeNameParser.GetType(String, ReadOnlySpan`1, String) + 0x32
at System.Reflection.TypeNameParser.NamespaceTypeName.ResolveType(TypeNameParser&, String) + 0x17
at System.Reflection.TypeNameParser.GetType(String, Func`2, Func`4, Boolean, Boolean, Boolean, String) + 0x99
at Java.Interop.ManagedPeer.GetParameterTypes(String) + 0xc1
at Java.Interop.ManagedPeer.Construct(IntPtr jnienv, IntPtr klass, IntPtr n_self, IntPtr n_assemblyQualifiedName, IntPtr n_constructorSignature, IntPtr n_constructorArguments) + 0x293
at com.xamarin.java_interop.ManagedPeer.construct(Native Method)
at example.ManagedType.<init>(ManagedType.java:23)
at com.microsoft.hello_from_jni.App.main(App.java:13)
```

This isn't impossible -- a straightforward fix would be to declare `native`
methods for each constructor overload -- but fixing this gets increasingly difficult.

(Possible "quick hack": replace `Type.GetType()` use with calls to something
on `JniRuntime.JniTypeManager`, allowing a subclass to provide its own
mapping? This feels "duplicative" of dotnet/runtime, though.)

## Type Maps

A "derivative" of the `Type.GetType()` problem is that Java.Interop needs a way
to associate a Java type to a .NET `System.Type` instance, for all manner of
reasons. (One such reason: `JniRuntime.JniValueManager.GetValue()` needs to
know the associated type so that it can create a "peer wrapper", if needed.)

Java.Interop unit tests "hack" around this by using a dictionary in TestJVM,
and `Hello-NativeAOTFromJNI` follows suite. This isn't a "real" answer, though.

.NET Android has a very complicated typemap mechanism that involves a table
between the Java JNI name and an { assembly name, type token } pair, along with
copious use of MonoVM embedding API such as `mono_class_get()`. ***A Lot***
of effort has gone into making type maps performant.

How do we "do" type maps in NativeAOT? We may need to consider some equivalent
to the iOS "static registrar", and this also needs to support getting `Type`
instances for non-`public` types. There are also concerns about initialization
overhead; a `Dictionary<string, Type>` will require loading and resolving
*all* the `Type` instances as part of startup, which *can't* be good for
reducing startup time. What other data structure could be used?

[0]: https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/jniTOC.html
[1]: https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/invocation.html#creating_the_vm
[2]: https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/invocation.html#JNJI_OnLoad
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace Java.Interop {

[AttributeUsage (AttributeTargets.Constructor | AttributeTargets.Method, AllowMultiple=false)]
[AttributeUsage (AttributeTargets.Method, AllowMultiple=false)]
public sealed class JavaCallableAttribute : Attribute {

public JavaCallableAttribute ()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#nullable enable
using System;

namespace Java.Interop {

[AttributeUsage (AttributeTargets.Constructor, AllowMultiple=false)]
public sealed class JavaCallableConstructorAttribute : Attribute {

public JavaCallableConstructorAttribute ()
{
}

public string? SuperConstructorExpression {get; set;}
public string? Signature {get; set;}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,9 @@ void Initialize ()
else if (minfo.AnyCustomAttributes ("Java.Interop.JavaCallableAttribute")) {
AddMethod (null, minfo);
HasExport = true;
} else if (minfo.AnyCustomAttributes ("Java.Interop.JavaCallableConstructorAttribute")) {
AddMethod (null, minfo);
HasExport = true;
} else if (minfo.AnyCustomAttributes (typeof(ExportFieldAttribute))) {
AddMethod (null, minfo);
HasExport = true;
Expand Down Expand Up @@ -310,7 +313,9 @@ void AddConstructor (MethodDefinition ctor, TypeDefinition type, string? outerTy
if (!string.IsNullOrEmpty (eattr.Name)) {
// Diagnostic.Warning (log, "Use of ExportAttribute.Name property is invalid on constructors");
}
ctors.Add (new Signature (new (cache, CodeGenerationTarget, ctor, eattr)));
ctors.Add (new Signature (new (cache, CodeGenerationTarget, ctor, eattr) {
ManagedParameters = managedParameters,
}));
curCtors.Add (ctor);
return;
}
Expand Down Expand Up @@ -450,6 +455,15 @@ ExportAttribute ToExportAttributeFromJavaCallableAttribute (CustomAttribute attr
return new ExportAttribute (name);
}

ExportAttribute ToExportAttributeFromJavaCallableConstructorAttribute (CustomAttribute attr, IMemberDefinition declaringMember)
{
var name = attr.ConstructorArguments.Count > 0 ? (string) attr.ConstructorArguments [0].Value : declaringMember.Name;
var superArgs = (string) attr.Properties.FirstOrDefault (p => p.Name == "SuperConstructorExpression").Argument.Value;
return new ExportAttribute (".ctor") {
SuperArgumentsString = superArgs,
};
}

internal static ExportFieldAttribute ToExportFieldAttribute (CustomAttribute attr)
{
return new ExportFieldAttribute ((string) attr.ConstructorArguments [0].Value);
Expand Down Expand Up @@ -486,7 +500,8 @@ static IEnumerable<RegisterAttribute> GetMethodRegistrationAttributes (Mono.Ceci
IEnumerable<ExportAttribute> GetExportAttributes (IMemberDefinition p)
{
return GetAttributes<ExportAttribute> (p, a => ToExportAttribute (a, p))
.Concat (GetAttributes<ExportAttribute> (p, "Java.Interop.JavaCallableAttribute", a => ToExportAttributeFromJavaCallableAttribute (a, p)));
.Concat (GetAttributes<ExportAttribute> (p, "Java.Interop.JavaCallableAttribute", a => ToExportAttributeFromJavaCallableAttribute (a, p)))
.Concat (GetAttributes<ExportAttribute> (p, "Java.Interop.JavaCallableConstructorAttribute", a => ToExportAttributeFromJavaCallableConstructorAttribute (a, p)));
}

static IEnumerable<ExportFieldAttribute> GetExportFieldAttributes (Mono.Cecil.ICustomAttributeProvider p)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
<ItemGroup>
<Compile Include="..\..\src\Java.Interop\Java.Interop\JniTypeSignatureAttribute.cs" />
<Compile Include="..\..\src\Java.Interop.Export\Java.Interop\JavaCallableAttribute.cs" />
<Compile Include="..\..\src\Java.Interop.Export\Java.Interop\JavaCallableConstructorAttribute.cs" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,7 @@ public ExportsConstructors (int p0)
{
super (p0);
if (getClass () == ExportsConstructors.class) {
mono.android.TypeManager.Activate (""Xamarin.Android.ToolsTests.ExportsConstructors, Java.Interop.Tools.JavaCallableWrappers-Tests"", """", this, new java.lang.Object[] { p0 });
mono.android.TypeManager.Activate (""Xamarin.Android.ToolsTests.ExportsConstructors, Java.Interop.Tools.JavaCallableWrappers-Tests"", ""System.Int32, System.Private.CoreLib"", this, new java.lang.Object[] { p0 });
}
}
Expand Down Expand Up @@ -508,7 +508,7 @@ public ExportsThrowsConstructors (int p0) throws java.lang.Throwable
{
super (p0);
if (getClass () == ExportsThrowsConstructors.class) {
mono.android.TypeManager.Activate (""Xamarin.Android.ToolsTests.ExportsThrowsConstructors, Java.Interop.Tools.JavaCallableWrappers-Tests"", """", this, new java.lang.Object[] { p0 });
mono.android.TypeManager.Activate (""Xamarin.Android.ToolsTests.ExportsThrowsConstructors, Java.Interop.Tools.JavaCallableWrappers-Tests"", ""System.Int32, System.Private.CoreLib"", this, new java.lang.Object[] { p0 });
}
}
Expand All @@ -517,7 +517,7 @@ public ExportsThrowsConstructors (java.lang.String p0)
{
super (p0);
if (getClass () == ExportsThrowsConstructors.class) {
mono.android.TypeManager.Activate (""Xamarin.Android.ToolsTests.ExportsThrowsConstructors, Java.Interop.Tools.JavaCallableWrappers-Tests"", """", this, new java.lang.Object[] { p0 });
mono.android.TypeManager.Activate (""Xamarin.Android.ToolsTests.ExportsThrowsConstructors, Java.Interop.Tools.JavaCallableWrappers-Tests"", ""System.String, System.Private.CoreLib"", this, new java.lang.Object[] { p0 });
}
}
Expand Down Expand Up @@ -647,11 +647,11 @@ extends java.lang.Object
}
public JavaInteropExample ()
public JavaInteropExample (int p0, int p1)
{
super ();
if (getClass () == JavaInteropExample.class) {
com.xamarin.java_interop.ManagedPeer.construct (this, ""Xamarin.Android.ToolsTests.JavaInteropExample, Java.Interop.Tools.JavaCallableWrappers-Tests"", """", new java.lang.Object[] { });
com.xamarin.java_interop.ManagedPeer.construct (this, ""Xamarin.Android.ToolsTests.JavaInteropExample, Java.Interop.Tools.JavaCallableWrappers-Tests"", ""System.Int32, System.Private.CoreLib:System.Int32, System.Private.CoreLib"", new java.lang.Object[] { p0, p1 });
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -328,8 +328,8 @@ public ExportsThrowsConstructors (string value) { }
class JavaInteropExample : Java.Lang.Object {


[JavaCallable]
public JavaInteropExample () {}
[JavaCallableConstructor(SuperConstructorExpression="")]
public JavaInteropExample (int a, int b) {}

[JavaCallable ("example")]
public void Example () {}
Expand Down

0 comments on commit 93a901a

Please sign in to comment.