Skip to content

Commit

Permalink
Make it (mostly) work!
Browse files Browse the repository at this point in the history
Commit 69c90ba expanded into a "Full(er)" sample, with just one issue:
it didn't fully work:

	Exception in thread "main" com.xamarin.java_interop.internal.JavaProxyThrowable: System.IO.FileNotFoundException: Could not resolve assembly 'Hello-NativeAOTFromJNI'.
	   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.RegisterNativeMembers(IntPtr jnienv, IntPtr klass, IntPtr n_nativeClass, IntPtr n_assemblyQualifiedName, IntPtr n_methods) + 0x103
	        at com.xamarin.java_interop.ManagedPeer.registerNativeMembers(Native Method)
	        at example.ManagedType.<clinit>(ManagedType.java:15)
	        at com.microsoft.hello_from_jni.App.main(App.java:13)

The problem is that `ManagedPeer.RegisterNativeMembers()` calls
`Type.GetType("Example.ManagedType, Hello-NativeAOTFromJNI")`, which
throws `FileNotFoundException`.

Let's attempt to fix that:

Update `MangedPeer.RegisterNativeMembers()` to call the (new!) method:

	partial class JniRuntime {
	  partial class JniTypeManager {
	    public virtual void RegisterNativeMembers (
	        JniType nativeClass,
	        ReadOnlySpan<char> assmblyQualifiedTypeName,
	        ReadOnlySpan<char> methods);
	  }
	}

which allows a subclass to *avoid* the `Type.GetType()` call.

Add a `NativeAotTypeManager` class which subclasses
`JniRuntime.JniTypeManager`, overriding `RegisterNativeMembers()`
so as to avoid the `Type.GetType()` call.  (It also "fakes" its own
"typemap" implementation…)

Add `Hello-NativeAOTFromJNI.xml`, a Linker Descriptor, to preserve
the `JavaException` constructors, which are needed when things
break horrifically.

TODO: figure out the appropriate `DynamicDependencyAttribute`
incantations to replace `Hello-NativeAOTFromJNI.xml`.

Update the `_AddMarshalMethods` build task to *also* update
`$(IntermediateOutputPath)$(TargetFileName)`, as the copy in
`$(IntermediateOutputPath)` is used by the `IlcCompile` target.
*Not* updating the copy in `$(IntermediateOutputPath)` means that
we don't get *any* marshal methods, and things break.

Rename `ExpressionAssemblyBuilder.CreateRegistrationMethod()` to
Rename `ExpressionAssemblyBuilder.AddRegistrationMethod()`, so that
the `EmitConsoleWriteLine()` invocation can provide the *full*
type name of the `__RegisterNativeMembers()` method, which helps
when there is more than one such method running around…

Various changes to `JniRuntime.JniTypeManager.cs`, to increase
logging verbosity, and to make the optimization effort in
`TryLoadJniMarshalMethods()` actually work; `Type.GetRuntimeMethod()`
*will not find* non-public methods, and `__RegisterNativeMembers()`
is rarely/never public.  Thus, it was basically dead code, and we'd
always fallback to the "look at all methods and see which ones have
`[JniAddNativeMethodRegistration]`" codepath, which is by definition
slower.  Use `Type.GetMethod()` instead.

Update `jnimarshalmethod-gen` & co so that they're consistent with
the output of `jcw-gen`.  Without these changes, the JCW would declare
`n_GetString()`, while `jnimarshalmethod-gen` would try to register
`getString`, and Java would thrown an exception because there is no
`getString` member to register.  Doh!

Finally, and the one thing that keeps this from being "perfect",
add an "activation constructor"
`Example.ManagedType(ref JniObjectReference, JniObjectReferenceOptions)`.

This constructor is currently needed because "JavaInterop1"-style
Java Callable Wrappers don't contain constructors (?!), so no
`Example.ManagedType` instance is created *until* the
`ManagedType.n_GetString()` marshal method is executed and attempts
to invoke the `ManagedType.GetString()` method.

We'll need to update `jcw-gen` & related to address this.

Current output:

	% (cd bin/Release/osx-x64/publish ; java -cp hello-from-java.jar:java-interop.jar com/microsoft/hello_from_jni/App)
	Hello from Java!
	C# init()
	Hello from .NET NativeAOT!
	String returned to Java: Hello from .NET NativeAOT!
	C# RegisterNativeMembers(JniType(Name='example/ManagedType' PeerReference=0x7fe545812ed8/G), "Example.ManagedType, Hello-NativeAOTFromJNI", "n_GetString:()Ljava/lang/String;:__export__
	")
	# jonp: called `Example.ManagedType/__<$>_jni_marshal_methods.__RegisterNativeMembers()` w/ 1 methods to register.
	mt.getString()=Hello from C#, via Java.Interop!
  • Loading branch information
jonpryor committed Nov 2, 2023
1 parent a499a1d commit e1822f0
Show file tree
Hide file tree
Showing 11 changed files with 112 additions and 24 deletions.
5 changes: 5 additions & 0 deletions samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@
<ProjectReference Include="..\..\src\Java.Interop.Export\Java.Interop.Export.csproj" />
</ItemGroup>

<ItemGroup>
<TrimmerRootAssembly Include="Hello-NativeAOTFromJNI" />
<TrimmerRootDescriptor Include="Hello-NativeAOTFromJNI.xml" />
</ItemGroup>

<ItemGroup>
<Content Include="$(OutputPath)hello-from-java.jar" CopyToPublishDirectory="PreserveNewest" />
</ItemGroup>
Expand Down
3 changes: 3 additions & 0 deletions samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@

<Exec Command="$(DotnetToolPath) $(_JnimarshalmethodGen) &quot;$(TargetPath)&quot; $(_Verbosity) $(_Libpath)" />

<!-- the IlcCompile target uses files from `$(IntermediateOutputPath)`, not `$(TargetPath)`, so… update both? -->
<Copy SourceFiles="$(TargetPath)" DestinationFolder="$(IntermediateOutputPath)" />

<Touch Files="$(IntermediateOutputPath).added-marshal-methods" AlwaysCreate="True" />
</Target>

Expand Down
7 changes: 7 additions & 0 deletions samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<linker>
<assembly fullname="Java.Interop">
<type fullname="Java.Interop.JavaException">
<method name=".ctor" />
</type>
</assembly>
</linker>
1 change: 1 addition & 0 deletions samples/Hello-NativeAOTFromJNI/JavaInteropRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ static void init (IntPtr jnienv, IntPtr klass)
try {
var options = new JreRuntimeOptions {
EnvironmentPointer = jnienv,
TypeManager = new NativeAotTypeManager (),
};
runtime = options.CreateJreVM ();
}
Expand Down
6 changes: 6 additions & 0 deletions samples/Hello-NativeAOTFromJNI/ManagedType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ public ManagedType ()
{
}

// TODO: remove this
public ManagedType (ref JniObjectReference reference, JniObjectReferenceOptions options)
: base (ref reference, options)
{
}

[JavaCallable ("getString")]
public Java.Lang.String GetString ()
{
Expand Down
48 changes: 48 additions & 0 deletions samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using Java.Interop;

namespace Hello_NativeAOTFromJNI;

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)!,
["example/ManagedType"] = typeof (Example.ManagedType),
};
#pragma warning restore IL2026

public override void RegisterNativeMembers (JniType nativeClass, ReadOnlySpan<char> assemblyQualifiedTypeName, ReadOnlySpan<char> methods)
{
Console.WriteLine ($"C# RegisterNativeMembers({nativeClass}, \"{assemblyQualifiedTypeName}\", \"{methods}\")");
if (typeMappings.TryGetValue (nativeClass.Name, out var type)) {
base.TryRegisterNativeMembers (nativeClass, type, methods);
}
else {
throw new NotSupportedException ($"Unsupported registration type: \"{nativeClass.Name}\" <=> \"{assemblyQualifiedTypeName}\"!");
}
}

protected override IEnumerable<Type> GetTypesForSimpleReference (string jniSimpleReference)
{
if (typeMappings.TryGetValue (jniSimpleReference, out var target))
yield return target;
foreach (var t in base.GetTypesForSimpleReference (jniSimpleReference))
yield return t;
}

protected override IEnumerable<string> GetSimpleReferences (Type type)
{
return base.GetSimpleReferences (type)
.Concat (CreateSimpleReferencesEnumerator (type));
}

IEnumerable<string> CreateSimpleReferencesEnumerator (Type type)
{
if (typeMappings == null)
yield break;
foreach (var e in typeMappings) {
if (e.Value == type)
yield return e.Key;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ static MethodDefinition CreateMethodDefinition (AssemblyDefinition declaringAsse
return mmDef;
}

public MethodDefinition CreateRegistrationMethod (IList<ExpressionMethodRegistration> methods)
public void AddRegistrationMethod (TypeDefinition declaringType, IList<ExpressionMethodRegistration> methods)
{
var registrations = new MethodDefinition (
name: "__RegisterNativeMembers",
Expand All @@ -71,6 +71,8 @@ public MethodDefinition CreateRegistrationMethod (IList<ExpressionMethodRegistra
},
};

declaringType.Methods.Add (registrations);

var ctor = typeof (JniAddNativeMethodRegistrationAttribute).GetConstructor (Type.EmptyTypes);
var attr = new CustomAttribute (DeclaringAssemblyDefinition.MainModule.ImportReference (ctor));
registrations.CustomAttributes.Add (attr);
Expand All @@ -84,7 +86,7 @@ public MethodDefinition CreateRegistrationMethod (IList<ExpressionMethodRegistra
registrations.Body.Variables.Add (array);

var il = registrations.Body.GetILProcessor ();
EmitConsoleWriteLine (il, $"# jonp: called __RegisterNativeMembers w/ {methods.Count} methods to register.");
EmitConsoleWriteLine (il, $"# jonp: called `{declaringType.FullName}.__RegisterNativeMembers()` w/ {methods.Count} methods to register.");
il.Emit (OpCodes.Ldc_I4, methods.Count);
il.Emit (OpCodes.Newarr, DeclaringAssemblyDefinition.MainModule.ImportReference (arrayType.GetElementType ()));
// il.Emit (OpCodes.Stloc_0);
Expand Down Expand Up @@ -117,9 +119,6 @@ public MethodDefinition CreateRegistrationMethod (IList<ExpressionMethodRegistra
il.Emit (OpCodes.Ldloc_0);
il.Emit (OpCodes.Call, DeclaringAssemblyDefinition.MainModule.ImportReference (addRegistrations.Method));
il.Emit (OpCodes.Ret);


return registrations;
}

void EmitConsoleWriteLine (ILProcessor il, string message)
Expand Down
2 changes: 2 additions & 0 deletions src/Java.Interop/Java.Interop/JniEnvironment.Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,8 @@ public static void RegisterNatives (JniObjectReference type, JniNativeMethodRegi
int r = _RegisterNatives (type, methods, numMethods);

if (r != 0) {
Console.WriteLine ($"# jonp: _RegisterNatives returned {r}");
JniNativeMethods.ExceptionDescribe (JniEnvironment.CurrentInfo.EnvironmentPointer);
throw new InvalidOperationException (
string.Format ("Could not register native methods for class '{0}'; JNIEnv::RegisterNatives() returned {1}.", GetJniTypeNameFromClass (type), r));
}
Expand Down
23 changes: 20 additions & 3 deletions src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,9 @@ IEnumerable<Type> CreateGetTypesForSimpleReferenceEnumerator (string jniSimpleRe

protected virtual ReplacementMethodInfo? GetReplacementMethodInfoCore (string jniSimpleReference, string jniMethodName, string jniMethodSignature) => null;

public virtual void RegisterNativeMembers (JniType nativeClass, ReadOnlySpan<char> assmblyQualifiedTypeName, ReadOnlySpan<char> methods) =>
RegisterNativeMembers (nativeClass, Type.GetType (new string (assmblyQualifiedTypeName), throwOnError: true)!, methods);

public virtual void RegisterNativeMembers (JniType nativeClass, Type type, ReadOnlySpan<char> methods)
{
TryRegisterNativeMembers (nativeClass, type, methods);
Expand Down Expand Up @@ -412,10 +415,21 @@ protected bool TryRegisterNativeMembers (JniType nativeClass, Type type, string?
bool TryLoadJniMarshalMethods (JniType nativeClass, Type type, string? methods)
{
var marshalType = type?.GetNestedType ("__<$>_jni_marshal_methods", BindingFlags.NonPublic);
if (marshalType == null)
if (marshalType == null) {
Console.WriteLine ($"# jonp: could not find type `{(type?.FullName ?? "<no-type>")}.__<$>_jni_marshal_methods`");
return false;
}

var registerMethod = marshalType.GetRuntimeMethod ("__RegisterNativeMembers", registerMethodParameters);
var registerMethod = marshalType.GetMethod (
"__RegisterNativeMembers",
BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public,
binder: null,
callConvention: default,
types: registerMethodParameters,
modifiers: null);
if (registerMethod == null) {
Console.WriteLine ($"# jonp: could not find method `{marshalType.FullName}.__<$>_jni_marshal_methods.__RegisterNativeMembers`; will look for methods with `[JniAddNativeMethodRegistration]`…]");
}

return TryRegisterNativeMembers (nativeClass, marshalType, methods, registerMethod);
}
Expand Down Expand Up @@ -466,11 +480,14 @@ bool FindAndCallRegisterMethod (Type marshalType, JniNativeMethodRegistrationArg
continue;
}

var declaringTypeName = methodInfo.DeclaringType?.FullName ?? "<no-decl-type>";

if ((methodInfo.Attributes & MethodAttributes.Static) != MethodAttributes.Static) {
throw new InvalidOperationException ($"The method {methodInfo} marked with {nameof (JniAddNativeMethodRegistrationAttribute)} must be static");
throw new InvalidOperationException ($"The method `{declaringTypeName}.{methodInfo}` marked with [{nameof (JniAddNativeMethodRegistrationAttribute)}] must be static!");
}

var register = (Action<JniNativeMethodRegistrationArguments>)methodInfo.CreateDelegate (typeof (Action<JniNativeMethodRegistrationArguments>));
Console.WriteLine ($"# jonp: FindAndCallRegisterMethod: calling `{declaringTypeName}.{methodInfo}`…");
register (arguments);

found = true;
Expand Down
13 changes: 9 additions & 4 deletions src/Java.Interop/Java.Interop/ManagedPeer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -197,22 +197,27 @@ static unsafe void RegisterNativeMembers (
var r_nativeClass = new JniObjectReference (n_nativeClass);
var nativeClass = new JniType (ref r_nativeClass, JniObjectReferenceOptions.Copy);

var assemblyQualifiedName = JniEnvironment.Strings.ToString (new JniObjectReference (n_assemblyQualifiedName));
var type = Type.GetType (assemblyQualifiedName!, throwOnError: true)!;
var methodsRef = new JniObjectReference (n_methods);

#if NET

var aqnRef = new JniObjectReference (n_assemblyQualifiedName);
int aqnLength = JniEnvironment.Strings.GetStringLength (aqnRef);
var aqnChars = JniEnvironment.Strings.GetStringChars (aqnRef, null);
var aqn = new ReadOnlySpan<char>(aqnChars, aqnLength);

int methodsLength = JniEnvironment.Strings.GetStringLength (methodsRef);
var methodsChars = JniEnvironment.Strings.GetStringChars (methodsRef, null);
var methods = new ReadOnlySpan<char>(methodsChars, methodsLength);
try {
JniEnvironment.Runtime.TypeManager.RegisterNativeMembers (nativeClass, type, methods);
JniEnvironment.Runtime.TypeManager.RegisterNativeMembers (nativeClass, aqn, methods);
}
finally {
JniEnvironment.Strings.ReleaseStringChars (aqnRef, aqnChars);
JniEnvironment.Strings.ReleaseStringChars (methodsRef, methodsChars);
}
#else // NET
var assemblyQualifiedName = JniEnvironment.Strings.ToString (new JniObjectReference (n_assemblyQualifiedName));
var type = Type.GetType (assemblyQualifiedName!, throwOnError: true)!;
var methods = JniEnvironment.Strings.ToString (methodsRef);
JniEnvironment.Runtime.TypeManager.RegisterNativeMembers (nativeClass, type, methods);
#endif // NET
Expand Down
19 changes: 7 additions & 12 deletions tools/jnimarshalmethod-gen/App.cs
Original file line number Diff line number Diff line change
Expand Up @@ -552,27 +552,22 @@ void CreateMarshalMethodAssembly (string path)
Log (TraceLevel.Verbose, $"## Dumping contents of marshal method for `{td.FullName}::{method.Name}({string.Join (", ", method.GetParameters ().Select (p => p.ParameterType))})`:");
Console.WriteLine (lambda.ToCSharpCode ());
#endif // _DUMP_REGISTER_NATIVE_MEMBERS
name = export?.Name ?? method.Name;

var mmDef = assemblyBuilder.Compile (lambda);
mmDef.Name = export?.Name ?? ("n_TODO" + lambda.GetHashCode ());
mmDef.Name = name;
mmTypeDef.Methods.Add (mmDef);

if (export != null) {
name = export.Name;
signature = export.Signature;
}

if (signature == null) {
signature = builder.GetJniMethodSignature (method);
}
signature = export?.Signature ?? builder.GetJniMethodSignature (method);

registrations.Add (new ExpressionMethodRegistration (name, signature, mmDef));
// Assume that `JavaCallableAttribute.Name` is "public" JCW name, and JCW's declare `n_`-prefixed `native` methods…
registrations.Add (new ExpressionMethodRegistration ("n_" + method.Name, signature, mmDef));

addedMethods.Add (methodName);
}
if (registrations.Count > 0) {
var m = assemblyBuilder.CreateRegistrationMethod (registrations);
mmTypeDef.Methods.Add (m);
td.NestedTypes.Add (mmTypeDef);
assemblyBuilder.AddRegistrationMethod (mmTypeDef, registrations);
}
}

Expand Down

0 comments on commit e1822f0

Please sign in to comment.