From 007317bb0cabfa41235dda286ba038329bc28d1c Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Tue, 24 Oct 2023 15:28:14 -0400 Subject: [PATCH 01/44] [Hello-NativeAOTFromJNI] Add NativeAOT sample Instead of a managed app creating a Java VM and calling into Java, do the opposite: have a Java app call into Native Library built using NativeAOT, using `[UnmanagedCallersOnly]` to provide the entry points. --- .../Hello-NativeAOTFromJNI.csproj | 31 ++++++++++ .../Hello-NativeAOTFromJNI.targets | 53 ++++++++++++++++ samples/Hello-NativeAOTFromJNI/JNIEnvInit.cs | 37 ++++++++++++ samples/Hello-NativeAOTFromJNI/README.md | 60 +++++++++++++++++++ .../com/microsoft/hello_from_jni/App.java | 9 +++ .../hello_from_jni/NativeAOTInit.java | 9 +++ 6 files changed, 199 insertions(+) create mode 100644 samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj create mode 100644 samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets create mode 100644 samples/Hello-NativeAOTFromJNI/JNIEnvInit.cs create mode 100644 samples/Hello-NativeAOTFromJNI/README.md create mode 100644 samples/Hello-NativeAOTFromJNI/java/com/microsoft/hello_from_jni/App.java create mode 100644 samples/Hello-NativeAOTFromJNI/java/com/microsoft/hello_from_jni/NativeAOTInit.java diff --git a/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj b/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj new file mode 100644 index 000000000..d5a55f216 --- /dev/null +++ b/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj @@ -0,0 +1,31 @@ + + + + net8.0 + Hello_NativeAOTFromJNI + enable + enable + true + true + Shared + + + + + + + + + + + + + + + + + diff --git a/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets b/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets new file mode 100644 index 000000000..5e5cf7811 --- /dev/null +++ b/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets @@ -0,0 +1,53 @@ + + + + + + <_Source Include="@(HelloNativeAOTFromJNIJar->Replace('%5c', '/'))" /> + + + + <_JavacOpt Include="$(_JavacSourceOptions)" /> + <_JavacOpt Include="-d "$(IntermediateOutputPath)h-classes" " /> + <_JavacOpt Include="-classpath "$(OutputPath)java-interop.jar" " /> + <_JavacOpt Include=""@$(IntermediateOutputPath)_java_sources.txt"" /> + <_JavacOpt Include="-h "$(IntermediateOutputPath)h-classes" " /> + + + + + + + + + + + + + + <_Classpath Include="hello-from-java.jar" /> + <_Classpath Include="java-interop.jar" /> + + + <_CPSep Condition=" '$(OS)' == 'Windows_NT' ">; + <_CPSep Condition=" '$(_CPSep)' == '' ">: + <_CP>@(_Classpath, '$(_CPSep)') + + + + diff --git a/samples/Hello-NativeAOTFromJNI/JNIEnvInit.cs b/samples/Hello-NativeAOTFromJNI/JNIEnvInit.cs new file mode 100644 index 000000000..b86436adf --- /dev/null +++ b/samples/Hello-NativeAOTFromJNI/JNIEnvInit.cs @@ -0,0 +1,37 @@ +using System.Runtime.InteropServices; + +using Java.Interop; + +namespace Hello_NativeAOTFromJNI; + +static class JNIEnvInit +{ + // static JniRuntime? runtime; + + [UnmanagedCallersOnly (EntryPoint="JNI_OnLoad")] + static int JNI_OnLoad (IntPtr vm, IntPtr reserved) + { + try { + // runtime = new JniRuntime (null); + // return runtime.JniVersion; + return (int) JniVersion.v1_2; + } + catch (Exception e) { + Console.Error.WriteLine ($"JNI_OnLoad: {e}"); + return 0; + } + } + + [UnmanagedCallersOnly (EntryPoint="JNI_OnUnload")] + static void JNI_Onload (IntPtr vm, IntPtr reserved) + { + // runtime?.Dispose (); + } + + // symbol name from `$(IntermediateOutputPath)/h-classes/com_microsoft_hello_from_jni_NativeAOTInit.h` + [UnmanagedCallersOnly (EntryPoint="Java_com_microsoft_hello_1from_1jni_NativeAOTInit_sayHello")] + static void sayHello (IntPtr jnienv, IntPtr klass) + { + Console.WriteLine ($"Hello from .NET NativeAOT!"); + } +} diff --git a/samples/Hello-NativeAOTFromJNI/README.md b/samples/Hello-NativeAOTFromJNI/README.md new file mode 100644 index 000000000..e0a85b909 --- /dev/null +++ b/samples/Hello-NativeAOTFromJNI/README.md @@ -0,0 +1,60 @@ +# Hello From JNI + +[JNI][0] supports *two* modes of operation: + + 1. Native code creates the JVM, e.g. via [`JNI_CreateJavaVM()`][1], or + 2. The JVM already exists, and calls [`JNI_OnLoad()`][2] when loading a native library. + +Java.Interop samples and unit tests rely on the first approach. + +.NET Android / neé Xamarin.Android is the second approach. + +Bring an example of the latter into a Java.Interop sample, using [NativeAOT][3]. + +## Building + +Building a native library with NativeAOT requires a Release configuration build. +For in-repo use, that means that xamarin/Java.Interop itself needs to be built in +Release configuration: + +```sh +% dotnet build -c Release -t:Prepare +% dotnet build -c Release +``` + +Once Java.Interop itself is built, you can build the sample: + +```sh +% dotnet build -c Release +% dotnet publish -r osx-x64 +``` + +The resulting native library contains the desired symbols: + +```sh +% nm bin/Release/osx-x64/publish/Hello-NativeAOTFromJNI.dylib | grep ' S ' +00000000000cb710 S _JNI_OnLoad +00000000000cb820 S _JNI_OnUnload +00000000000cb840 S _Java_com_microsoft_hello_1from_1jni_NativeAOTInit_sayHello +``` + +Use the `RunJavaSample` target to run Java, which will run +`System.loadLibrary("Hello-NativeAOTFromJNI")`, which will cause the +NativeAOT-generated `libHello-NativeAOTFromJNI.dylib` to be run: + +```sh +% dotnet build -c Release -r osx-x64 -t:RunJavaSample -v m --nologo --no-restore + Hello from Java! + Hello from .NET NativeAOT! + +Build succeeded. + 0 Warning(s) + 0 Error(s) + +Time Elapsed 00:00:00.83 +``` + +[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 +[3]: https://github.com/dotnet/samples/blob/main/core/nativeaot/NativeLibrary/README.md \ No newline at end of file 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 new file mode 100644 index 000000000..b9a63ddb7 --- /dev/null +++ b/samples/Hello-NativeAOTFromJNI/java/com/microsoft/hello_from_jni/App.java @@ -0,0 +1,9 @@ +package com.microsoft.hello_from_jni; + +class App { + + public static void main(String[] args) { + System.out.println("Hello from Java!"); + NativeAOTInit.sayHello(); + } +} diff --git a/samples/Hello-NativeAOTFromJNI/java/com/microsoft/hello_from_jni/NativeAOTInit.java b/samples/Hello-NativeAOTFromJNI/java/com/microsoft/hello_from_jni/NativeAOTInit.java new file mode 100644 index 000000000..0f8c8cccc --- /dev/null +++ b/samples/Hello-NativeAOTFromJNI/java/com/microsoft/hello_from_jni/NativeAOTInit.java @@ -0,0 +1,9 @@ +package com.microsoft.hello_from_jni; + +class NativeAOTInit { + static { + System.loadLibrary("Hello-NativeAOTFromJNI"); + } + + public static native void sayHello(); +} From db84cc3d89ed906ff4b4c2e8c9a4b566c54d8514 Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Tue, 24 Oct 2023 21:23:21 -0400 Subject: [PATCH 02/44] Try to create a JreRuntime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BROKEN BROKEN BROKEN BROKEN What do we want? To create a `JreRuntime` instance! …which is shockingly difficult. But first, *why* do we want to create a `JreRuntime` instance? (It's not like we've needed it so far!) Force the issue by changing `NativeAOTInit.sayHello()` to return a `String` instance, thus requiring that for `NativeAOTInit.sayHello()` to return a non-`null` value, it needs a `JreRuntime` instance! This is where everything falls apart. `JreRuntime` and `JreRuntimeOptions` were not designed with this scenario in mind! Update `JreRuntime` & co so that it doesn't require `JvmLibraryPath` when `InvocationPointer` isn't null, and don't require that `JreRuntimeOptions.ClassPath` contain the path to `java-interop.jar` when `InvocationPointer` isn't null. This allows us to create the `JreRuntimeOptions` instance. Yay! The next problem is that setting `%(ProjectReference.AdditionalProperties)` to include `Standalone=true` doesn't appear to work, in that the `Java.Interop.dll` that is included still contains P/Invokes instead of the `JniNativeMethods` class. Update `Java.Interop.csproj` so that `$(Standalone)`=true is now the default. (This *shouldn't* break anybody, but… it now means the P/Invoke backend is getting NO testing. ¯\_(ツ)_/¯ ) The next part is where it blows up GOOD: `options.CreateJreVM()` throws, which aborts everything: % (cd bin/Release/osx-x64/publish ; java -cp hello-from-java.jar:java-interop.jar com/microsoft/hello_from_jni/App) Hello from Java! # jonp: JNI_OnLoad: vm=10a9c0b00 # jonp: JNI_OnLoad: created options… # jonp: builder.InvocationPointer=10a9c0b00 JNI_OnLoad: error: System.TypeInitializationException: A type initializer threw an exception. To determine which type, inspect the InnerException's StackTrace property. ---> System.NotSupportedException: 'Java.Interop.ManagedPeer+ConstructMarshalMethod' is missing delegate marshalling data. This can happen for code that is not compatible with AOT. Inspect and fix AOT related warnings that were generated when the app was published. For more information see https://aka.ms/nativeaot-compatibility at Internal.Runtime.CompilerHelpers.RuntimeInteropData.GetDelegateMarshallingStub(RuntimeTypeHandle, Boolean) + 0x78 at System.Runtime.InteropServices.PInvokeMarshal.AllocateThunk(Delegate del) + 0x6b at System.Runtime.CompilerServices.ConditionalWeakTable`2.GetValueLocked(TKey, ConditionalWeakTable`2.CreateValueCallback) + 0x27 at System.Runtime.CompilerServices.ConditionalWeakTable`2.GetValue(TKey, ConditionalWeakTable`2.CreateValueCallback) + 0x41 at System.Runtime.InteropServices.PInvokeMarshal.GetFunctionPointerForDelegate(Delegate) + 0xd5 at libHello-NativeAOTFromJNI!+0x8adeb at Java.Interop.JniEnvironment.Types._RegisterNatives(JniObjectReference, JniNativeMethodRegistration[], Int32) + 0x90 at Java.Interop.JniEnvironment.Types.RegisterNatives(JniObjectReference, JniNativeMethodRegistration[], Int32) + 0x65 at Java.Interop.JniType.RegisterNativeMethods(JniNativeMethodRegistration[]) + 0x30 at Java.Interop.ManagedPeer..cctor() + 0x175 at System.Runtime.CompilerServices.ClassConstructorRunner.EnsureClassConstructorRun(StaticClassConstructionContext*) + 0xb4 --- End of inner exception stack trace --- at System.Runtime.CompilerServices.ClassConstructorRunner.EnsureClassConstructorRun(StaticClassConstructionContext*) + 0x147 at System.Runtime.CompilerServices.ClassConstructorRunner.CheckStaticClassConstructionReturnGCStaticBase(StaticClassConstructionContext*, Object) + 0x9 at Java.Interop.JniRuntime..ctor(JniRuntime.CreationOptions) + 0x5a2 at Java.Interop.JreRuntime..ctor(JreRuntimeOptions) + 0x22 at Hello_NativeAOTFromJNI.JNIEnvInit.JNI_OnLoad(IntPtr, IntPtr) + 0x9e Exception in thread "main" java.lang.UnsatisfiedLinkError: unsupported JNI version 0x00000000 required by /Volumes/Xamarin-Work/src/xamarin/Java.Interop/samples/Hello-NativeAOTFromJNI/bin/Release/osx-x64/publish/libHello-NativeAOTFromJNI.dylib at java.base/jdk.internal.loader.NativeLibraries.load(Native Method) at java.base/jdk.internal.loader.NativeLibraries$NativeLibraryImpl.open(NativeLibraries.java:388) at java.base/jdk.internal.loader.NativeLibraries.loadLibrary(NativeLibraries.java:232) at java.base/jdk.internal.loader.NativeLibraries.loadLibrary(NativeLibraries.java:174) at java.base/jdk.internal.loader.NativeLibraries.findFromPaths(NativeLibraries.java:315) at java.base/jdk.internal.loader.NativeLibraries.loadLibrary(NativeLibraries.java:287) at java.base/java.lang.ClassLoader.loadLibrary(ClassLoader.java:2427) at java.base/java.lang.Runtime.loadLibrary0(Runtime.java:818) at java.base/java.lang.System.loadLibrary(System.java:1989) at com.microsoft.hello_from_jni.NativeAOTInit.(NativeAOTInit.java:5) at com.microsoft.hello_from_jni.App.main(App.java:7) Within the `JniRuntime` constructor, we're hitting: #if !XA_JI_EXCLUDE ManagedPeer.Init (); #endif // !XA_JI_EXCLUDE This invokes the `ManagedPeer` static constructor, which attempts to call `JniType.RegisterNativeMethods()`, which attempts to call `JNIEnv::RegisterNatives()`, but before it can get that far it needs to marshal things: 1. The `JniNativeMethodRegistration` struct, which in turn requires 2. The `JniNativeMethodRegistration.Marshaler` delegate field. This apparently isn't supported by NativeAOT, at least not without additional work that I am not currently privy to. Given that `JniNativeMethodRegistration` structure marshaling is how *all* JNI method registration is done in .NET Android, this is a bit of a blocker for this sample. TODO? Figure out how to allow NativeAOT to marshal `JniNativeMethodRegistration` with a minimum of effort? TODO? *Require* `jnimarshalmethod-gen`, and update it so that it emits `[UnmanagedCallersOnlyAttribute]` on all generated marshal methods *and* emits `UnmanagedCallersOnlyAttribute.EntryPoint` to a `Java_…` symbol name so that we *don't* hit the `JniNativeMethodRegistration` struct marshaling codepath. (See also 77800dda). [0]:https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#RegisterNatives --- samples/Hello-NativeAOTFromJNI/JNIEnvInit.cs | 24 +++++++++++++------ samples/Hello-NativeAOTFromJNI/README.md | 7 ++++++ .../com/microsoft/hello_from_jni/App.java | 3 ++- .../hello_from_jni/NativeAOTInit.java | 2 +- src/Java.Interop/Java.Interop.csproj | 1 + .../Java.Interop/JreRuntime.cs | 20 +++++++++++----- 6 files changed, 42 insertions(+), 15 deletions(-) diff --git a/samples/Hello-NativeAOTFromJNI/JNIEnvInit.cs b/samples/Hello-NativeAOTFromJNI/JNIEnvInit.cs index b86436adf..b549afde8 100644 --- a/samples/Hello-NativeAOTFromJNI/JNIEnvInit.cs +++ b/samples/Hello-NativeAOTFromJNI/JNIEnvInit.cs @@ -6,18 +6,23 @@ namespace Hello_NativeAOTFromJNI; static class JNIEnvInit { - // static JniRuntime? runtime; + static JniRuntime? runtime; [UnmanagedCallersOnly (EntryPoint="JNI_OnLoad")] static int JNI_OnLoad (IntPtr vm, IntPtr reserved) { + Console.WriteLine ($"# jonp: JNI_OnLoad: vm={vm.ToString("x2")}"); try { - // runtime = new JniRuntime (null); - // return runtime.JniVersion; - return (int) JniVersion.v1_2; + var options = new JreRuntimeOptions { + InvocationPointer = vm, + }; + Console.WriteLine ($"# jonp: JNI_OnLoad: created options…"); + runtime = options.CreateJreVM (); + Console.WriteLine ($"# jonp: JNI_OnLoad: created runtime…"); + return (int) runtime.JniVersion; } catch (Exception e) { - Console.Error.WriteLine ($"JNI_OnLoad: {e}"); + Console.Error.WriteLine ($"JNI_OnLoad: error: {e}"); return 0; } } @@ -30,8 +35,13 @@ static void JNI_Onload (IntPtr vm, IntPtr reserved) // symbol name from `$(IntermediateOutputPath)/h-classes/com_microsoft_hello_from_jni_NativeAOTInit.h` [UnmanagedCallersOnly (EntryPoint="Java_com_microsoft_hello_1from_1jni_NativeAOTInit_sayHello")] - static void sayHello (IntPtr jnienv, IntPtr klass) + static IntPtr sayHello (IntPtr jnienv, IntPtr klass) { - Console.WriteLine ($"Hello from .NET NativeAOT!"); + var s = $"Hello from .NET NativeAOT!"; + Console.WriteLine (s); + var h = JniEnvironment.Strings.NewString (s); + var r = JniEnvironment.References.NewReturnToJniRef (h); + JniObjectReference.Dispose (ref h); + return r; } } diff --git a/samples/Hello-NativeAOTFromJNI/README.md b/samples/Hello-NativeAOTFromJNI/README.md index e0a85b909..abc74f49c 100644 --- a/samples/Hello-NativeAOTFromJNI/README.md +++ b/samples/Hello-NativeAOTFromJNI/README.md @@ -52,8 +52,15 @@ Build succeeded. 0 Error(s) Time Elapsed 00:00:00.83 + +% (cd bin/Release/osx-x64/publish ; java -cp hello-from-java.jar:java-interop.jar com/microsoft/hello_from_jni/App) +Hello from Java! +Hello from .NET NativeAOT! ``` +Note the use of `(cd …; java …)` so that `libHello-NativeAOTFromJNI.dylib` is +in the current working directory, so that it can be found. + [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 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 b9a63ddb7..a63d9c7e6 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 @@ -4,6 +4,7 @@ class App { public static void main(String[] args) { System.out.println("Hello from Java!"); - NativeAOTInit.sayHello(); + String s = NativeAOTInit.sayHello(); + System.out.println("String returned to Java: " + s); } } diff --git a/samples/Hello-NativeAOTFromJNI/java/com/microsoft/hello_from_jni/NativeAOTInit.java b/samples/Hello-NativeAOTFromJNI/java/com/microsoft/hello_from_jni/NativeAOTInit.java index 0f8c8cccc..cef013881 100644 --- a/samples/Hello-NativeAOTFromJNI/java/com/microsoft/hello_from_jni/NativeAOTInit.java +++ b/samples/Hello-NativeAOTFromJNI/java/com/microsoft/hello_from_jni/NativeAOTInit.java @@ -5,5 +5,5 @@ class NativeAOTInit { System.loadLibrary("Hello-NativeAOTFromJNI"); } - public static native void sayHello(); + public static native String sayHello(); } diff --git a/src/Java.Interop/Java.Interop.csproj b/src/Java.Interop/Java.Interop.csproj index 7132b01e8..43d5937e3 100644 --- a/src/Java.Interop/Java.Interop.csproj +++ b/src/Java.Interop/Java.Interop.csproj @@ -29,6 +29,7 @@ 9.0 8.0 $(JICoreLibVersion) + true FEATURE_JNIENVIRONMENT_JI_FUNCTION_POINTERS;$(DefineConstants) diff --git a/src/Java.Runtime.Environment/Java.Interop/JreRuntime.cs b/src/Java.Runtime.Environment/Java.Interop/JreRuntime.cs index aace91706..8e778b41a 100644 --- a/src/Java.Runtime.Environment/Java.Interop/JreRuntime.cs +++ b/src/Java.Runtime.Environment/Java.Interop/JreRuntime.cs @@ -39,11 +39,7 @@ public class JreRuntimeOptions : JniRuntime.CreationOptions { public JreRuntimeOptions () { JniVersion = JniVersion.v1_2; - ClassPath = new Collection () { - Path.Combine ( - Path.GetDirectoryName (typeof (JreRuntimeOptions).Assembly.Location) ?? throw new NotSupportedException (), - "java-interop.jar"), - }; + ClassPath = new Collection (); } public JreRuntimeOptions AddOption (string option) @@ -80,7 +76,7 @@ static unsafe JreRuntimeOptions CreateJreVM (JreRuntimeOptions builder) { if (builder == null) throw new ArgumentNullException ("builder"); - if (string.IsNullOrEmpty (builder.JvmLibraryPath)) + if (builder.InvocationPointer == IntPtr.Zero && string.IsNullOrEmpty (builder.JvmLibraryPath)) throw new InvalidOperationException ($"Member `{nameof (JreRuntimeOptions)}.{nameof (JreRuntimeOptions.JvmLibraryPath)}` must be set."); builder.LibraryHandler = JvmLibraryHandler.Create (); @@ -99,11 +95,23 @@ static unsafe JreRuntimeOptions CreateJreVM (JreRuntimeOptions builder) builder.ObjectReferenceManager = builder.ObjectReferenceManager ?? new ManagedObjectReferenceManager (builder.JniGlobalReferenceLogWriter, builder.JniLocalReferenceLogWriter); } + Console.WriteLine($"# jonp: builder.InvocationPointer={builder.InvocationPointer.ToString("x2")}"); if (builder.InvocationPointer != IntPtr.Zero) return builder; + Console.WriteLine($"# jonp: loading {builder.JvmLibraryPath}..."); builder.LibraryHandler.LoadJvmLibrary (builder.JvmLibraryPath!); + if (!builder.ClassPath.Any (p => p.EndsWith ("java-interop.jar", StringComparison.OrdinalIgnoreCase))) { + var loc = typeof (JreRuntimeOptions).Assembly.Location; + var dir = string.IsNullOrEmpty (loc) ? null : Path.GetDirectoryName (loc); + var jij = string.IsNullOrEmpty (dir) ? null : Path.Combine (dir, "java-interop.jar"); + if (!File.Exists (jij)) { + throw new InvalidOperationException ($"`java-interop.jar` is required. Please add to `JreRuntimeOptions.ClassPath`. Tried to find it in `{jij}`."); + } + builder.ClassPath.Add (jij); + } + var args = new JavaVMInitArgs () { version = builder.JniVersion, nOptions = builder.Options.Count + 1, From a32e24453558898539dfeb23f85086e0f3bc2180 Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Wed, 25 Oct 2023 14:07:37 -0400 Subject: [PATCH 03/44] icanhaz blittable method registration? Commit db84cc3d "didn't work" because the "traditional" form of method registration through `JNIEnv::RegisterNatives()` used a `System.Delegate` instance to refer to a C# method to invoke, but this fails under NativeAOT because the NativeAOT infrastructure needs to be able to produce "stubs" for the methods based on compile-time scans, and it doesn't represent our code pattern and/or our pattern is not supportable with how NativeAOT works. Oops. "Ecosystem-wise", I'm not sure how to solve this, other than handy-wavy ideas such as "rely on `jnimarshalmethod-gen`!". Short-term, we can at least resolve `ManagedPeer`. Create a new `JniBlittableNativeMethodRegistration` struct which contains only blittable types, vs. `JniNativeMethodRegistration` which contains `string` fields and `Delegate` fields and is not easily supportable by NativeAOT. public partial struct JniBlittableNativeMethodRegistration { public JniBlittableNativeMethodRegistration (IntPtr name, IntPtr signature, IntPtr marshaler); // To support `new JniBlittableNativeMethodRegistration("name"u8, "signature"u8, marshaler)` public JniBlittableNativeMethodRegistration (ReadOnlySpan name, ReadOnlySpan signature, IntPtr marshaler); } "Plumb through" use of `JniBlittableNativeMethodRegistration` to `JniEnvironment.Types.RegisterNatives()` overloads: partial class JniEnvironment { partial class Types { public static void RegisterNatives(JniObjectReference type, ReadOnlySpan methods); } } This in turn requires updating `jnienv-gen` so that `RegisterNatives()` is bound with the `const JNINativeMethod *methods` parameter bound as an `IntPtr` instead of a `JniNativeMethodRegistration[]`. Update the `ManagedPeer` static constructor to use `[UnmanagedCallersOnly]` methods and native function pointers to register the marshal methods that `ManagedPeer` requires. --- build-tools/jnienv-gen/Generator.cs | 2 +- samples/Hello-NativeAOTFromJNI/JNIEnvInit.cs | 5 +- src/Java.Interop/Java.Interop.csproj | 2 +- .../Java.Interop/JniEnvironment.Types.cs | 39 ++++++++++- .../JniNativeMethodRegistration.cs | 66 +++++++++++++++++++ src/Java.Interop/Java.Interop/JniType.cs | 7 ++ src/Java.Interop/Java.Interop/ManagedPeer.cs | 49 +++++++------- .../Java.Interop/JreRuntime.cs | 2 - 8 files changed, 137 insertions(+), 35 deletions(-) diff --git a/build-tools/jnienv-gen/Generator.cs b/build-tools/jnienv-gen/Generator.cs index a3c4c2bf6..2b1c876cc 100644 --- a/build-tools/jnienv-gen/Generator.cs +++ b/build-tools/jnienv-gen/Generator.cs @@ -758,7 +758,7 @@ abstract class TypeInfo { "void*", new BuiltinTypeInfo ("void*", "IntPtr") }, { "const jchar*", new StringTypeInfo ("const jchar*") }, { "const char*", new StringTypeInfo ("const char*") }, - { "const JNINativeMethod*", new BuiltinTypeInfo ("const JNINativeMethod*", "JniNativeMethodRegistration []") }, + { "const JNINativeMethod*", new BuiltinTypeInfo ("const JNINativeMethod*", "IntPtr") }, { "jobjectRefType", new BuiltinTypeInfo ("jobjectRefType", "JniObjectReferenceType") }, { "jfieldID", new InstanceFieldTypeInfo ("jfieldID") }, { "jstaticfieldID", new StaticFieldTypeInfo ("jstaticfieldID") }, diff --git a/samples/Hello-NativeAOTFromJNI/JNIEnvInit.cs b/samples/Hello-NativeAOTFromJNI/JNIEnvInit.cs index b549afde8..5aecdd43b 100644 --- a/samples/Hello-NativeAOTFromJNI/JNIEnvInit.cs +++ b/samples/Hello-NativeAOTFromJNI/JNIEnvInit.cs @@ -11,14 +11,11 @@ static class JNIEnvInit [UnmanagedCallersOnly (EntryPoint="JNI_OnLoad")] static int JNI_OnLoad (IntPtr vm, IntPtr reserved) { - Console.WriteLine ($"# jonp: JNI_OnLoad: vm={vm.ToString("x2")}"); try { var options = new JreRuntimeOptions { InvocationPointer = vm, }; - Console.WriteLine ($"# jonp: JNI_OnLoad: created options…"); runtime = options.CreateJreVM (); - Console.WriteLine ($"# jonp: JNI_OnLoad: created runtime…"); return (int) runtime.JniVersion; } catch (Exception e) { @@ -30,7 +27,7 @@ static int JNI_OnLoad (IntPtr vm, IntPtr reserved) [UnmanagedCallersOnly (EntryPoint="JNI_OnUnload")] static void JNI_Onload (IntPtr vm, IntPtr reserved) { - // runtime?.Dispose (); + runtime?.Dispose (); } // symbol name from `$(IntermediateOutputPath)/h-classes/com_microsoft_hello_from_jni_NativeAOTInit.h` diff --git a/src/Java.Interop/Java.Interop.csproj b/src/Java.Interop/Java.Interop.csproj index 43d5937e3..df955955b 100644 --- a/src/Java.Interop/Java.Interop.csproj +++ b/src/Java.Interop/Java.Interop.csproj @@ -26,7 +26,7 @@ $(ToolOutputFullPath) $(ToolOutputFullPath)Java.Interop.xml $(BuildToolOutputFullPath) - 9.0 + 11.0 8.0 $(JICoreLibVersion) true diff --git a/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs b/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs index da4d41b99..93adc3caa 100644 --- a/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs +++ b/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs @@ -250,6 +250,9 @@ public static void RegisterNatives (JniObjectReference type, JniNativeMethodRegi throw new ArgumentOutOfRangeException (nameof (numMethods), numMethods, $"`numMethods` must be between 0 and `methods.Length` ({methods?.Length ?? 0})!"); } + if (methods == null || numMethods == 0) { + return; + } #if DEBUG && NETCOREAPP for (int i = 0; methods != null && i < numMethods; ++i) { var m = methods [i]; @@ -262,8 +265,42 @@ public static void RegisterNatives (JniObjectReference type, JniNativeMethodRegi } #endif // DEBUG && NETCOREAPP - int r = _RegisterNatives (type, methods, numMethods); + Span blittableMethods + = stackalloc JniBlittableNativeMethodRegistration [numMethods]; + Span freeStrings + = stackalloc IntPtr [numMethods * 2]; + try { + for (int i = 0; i < numMethods; ++i) { + var name = methods [i].Name == null ? IntPtr.Zero : Marshal.StringToCoTaskMemUTF8 (methods [i].Name); + var sig = methods [i].Signature == null ? IntPtr.Zero : Marshal.StringToCoTaskMemUTF8 (methods [i].Signature); + + freeStrings [(i*2)+0] = name; + freeStrings [(i*2)+1] = sig; + + blittableMethods [i] = new JniBlittableNativeMethodRegistration ( + name, + sig, + Marshal.GetFunctionPointerForDelegate (methods [i].Marshaler) + ); + } + RegisterNatives (type, blittableMethods); + } + finally { + for (int i = 0; i < freeStrings.Length; ++i) { + Marshal.ZeroFreeCoTaskMemUTF8 (freeStrings [i]); + } + } + } + public static unsafe void RegisterNatives (JniObjectReference type, ReadOnlySpan methods) + { + if (methods.Length == 0) { + return; + } + int r; + fixed (JniBlittableNativeMethodRegistration* pMethods = methods) { + r = _RegisterNatives (type, (IntPtr) pMethods, methods.Length); + } if (r != 0) { throw new InvalidOperationException ( string.Format ("Could not register native methods for class '{0}'; JNIEnv::RegisterNatives() returned {1}.", GetJniTypeNameFromClass (type), r)); diff --git a/src/Java.Interop/Java.Interop/JniNativeMethodRegistration.cs b/src/Java.Interop/Java.Interop/JniNativeMethodRegistration.cs index 20f99f947..c638d1e16 100644 --- a/src/Java.Interop/Java.Interop/JniNativeMethodRegistration.cs +++ b/src/Java.Interop/Java.Interop/JniNativeMethodRegistration.cs @@ -20,4 +20,70 @@ public JniNativeMethodRegistration (string name, string signature, Delegate mars Marshaler = marshaler ?? throw new ArgumentNullException (nameof (marshaler)); } } + + public struct JniBlittableNativeMethodRegistration : IEquatable { + + IntPtr name, signature, marshaler; + + public JniBlittableNativeMethodRegistration (IntPtr name, IntPtr signature, IntPtr marshaler) + { + if (name == IntPtr.Zero) + throw new ArgumentNullException (nameof (name)); + if (signature == IntPtr.Zero) + throw new ArgumentNullException (nameof (signature)); + if (marshaler == IntPtr.Zero) + throw new ArgumentNullException (nameof (marshaler)); + + this.name = name; + this.signature = signature; + this.marshaler = marshaler; + } + + public JniBlittableNativeMethodRegistration (ReadOnlySpan name, ReadOnlySpan signature, IntPtr marshaler) + { + if (name.Length == 0) + throw new ArgumentException ("must not be empty", nameof (name)); + if (signature.Length == 0) + throw new ArgumentException ("must not be empty", nameof (signature)); + if (marshaler == IntPtr.Zero) + throw new ArgumentNullException (nameof (marshaler)); + + this.name = FromReadOnlySpan (name); + this.signature = FromReadOnlySpan (signature); + this.marshaler = marshaler; + } + + // Dodgy AF, but as the C# compiler guarantees no allocations for u8 strings, + // the "address" of `value` will never move, so this is "fine"… + // *so long as* it's *only* used for "string"u8 values. + // https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-11.0/utf8-string-literals#detailed-design + // > Since the literals would be allocated as global constants, the lifetime of the + // > resulting ReadOnlySpan would not prevent it from being returned or passed around to elsewhere. + static unsafe IntPtr FromReadOnlySpan (ReadOnlySpan value) + { + fixed (byte* p = value) + return (IntPtr) p; + } + + public override bool Equals (object? obj) + { + if (obj is JniBlittableNativeMethodRegistration other) { + return Equals (other); + } + return false; + } + + public override int GetHashCode () => + HashCode.Combine (name, signature, marshaler); + + public bool Equals (JniBlittableNativeMethodRegistration other) => + name == other.name && + signature == other.signature && + marshaler == other.marshaler; + + public static bool operator == (JniBlittableNativeMethodRegistration lhs, JniBlittableNativeMethodRegistration rhs) => + lhs.Equals (rhs); + public static bool operator != (JniBlittableNativeMethodRegistration lhs, JniBlittableNativeMethodRegistration rhs) => + !lhs.Equals (rhs); + } } diff --git a/src/Java.Interop/Java.Interop/JniType.cs b/src/Java.Interop/Java.Interop/JniType.cs index 396f5067b..f3fa29bb7 100644 --- a/src/Java.Interop/Java.Interop/JniType.cs +++ b/src/Java.Interop/Java.Interop/JniType.cs @@ -168,6 +168,13 @@ public void RegisterNativeMethods (params JniNativeMethodRegistration[] methods) RegisterWithRuntime (); } + public void RegisterNativeMethods (ReadOnlySpan methods) + { + AssertValid (); + JniEnvironment.Types.RegisterNatives (PeerReference, methods); + RegisterWithRuntime (); + } + public void UnregisterNativeMethods () { AssertValid (); diff --git a/src/Java.Interop/Java.Interop/ManagedPeer.cs b/src/Java.Interop/Java.Interop/ManagedPeer.cs index eb43a9e85..56ca02f2f 100644 --- a/src/Java.Interop/Java.Interop/ManagedPeer.cs +++ b/src/Java.Interop/Java.Interop/ManagedPeer.cs @@ -6,6 +6,7 @@ using System.Linq.Expressions; using System.Reflection; using System.Reflection.Emit; +using System.Runtime.InteropServices; using System.Runtime.Serialization; using System.Text; @@ -19,18 +20,27 @@ namespace Java.Interop { static readonly JniPeerMembers _members = new JniPeerMembers (JniTypeName, typeof (ManagedPeer)); - static ManagedPeer () + static unsafe ManagedPeer () { - _members.JniPeerType.RegisterNativeMethods ( - new JniNativeMethodRegistration ( - "construct", - ConstructSignature, - new ConstructMarshalMethod (Construct)), - new JniNativeMethodRegistration ( - "registerNativeMembers", - RegisterNativeMembersSignature, - new RegisterMarshalMethod (RegisterNativeMembers)) - ); + Span registrations = stackalloc JniBlittableNativeMethodRegistration [2]; + + delegate* unmanaged construct = &Construct; + registrations [0] = new JniBlittableNativeMethodRegistration ( + "construct"u8, + "(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;[Ljava/lang/Object;)V"u8, + (IntPtr) construct); + + delegate* unmanaged registerNativeMembers = &RegisterNativeMembers; + registrations [1] = new JniBlittableNativeMethodRegistration ( + "registerNativeMembers"u8, + "(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;)V"u8, + (IntPtr) registerNativeMembers); + + _members.JniPeerType.RegisterNativeMethods (registrations); } ManagedPeer () @@ -47,15 +57,8 @@ public override JniPeerMembers JniPeerMembers { get {return _members;} } - const string ConstructSignature = "(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;[Ljava/lang/Object;)V"; - // TODO: Keep in sync with the code generated by ExportedMemberBuilder - delegate void ConstructMarshalMethod (IntPtr jnienv, - IntPtr klass, - IntPtr n_self, - IntPtr n_assemblyQualifiedName, - IntPtr n_constructorSignature, - IntPtr n_constructorArguments); + [UnmanagedCallersOnly] static void Construct ( IntPtr jnienv, IntPtr klass, @@ -175,13 +178,7 @@ static Type[] GetParameterTypes (string? signature) return pvalues; } - const string RegisterNativeMembersSignature = "(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;)V"; - - delegate void RegisterMarshalMethod (IntPtr jnienv, - IntPtr klass, - IntPtr n_nativeClass, - IntPtr n_assemblyQualifiedName, - IntPtr n_methods); + [UnmanagedCallersOnly] static unsafe void RegisterNativeMembers ( IntPtr jnienv, IntPtr klass, diff --git a/src/Java.Runtime.Environment/Java.Interop/JreRuntime.cs b/src/Java.Runtime.Environment/Java.Interop/JreRuntime.cs index 8e778b41a..3c49d68b0 100644 --- a/src/Java.Runtime.Environment/Java.Interop/JreRuntime.cs +++ b/src/Java.Runtime.Environment/Java.Interop/JreRuntime.cs @@ -95,10 +95,8 @@ static unsafe JreRuntimeOptions CreateJreVM (JreRuntimeOptions builder) builder.ObjectReferenceManager = builder.ObjectReferenceManager ?? new ManagedObjectReferenceManager (builder.JniGlobalReferenceLogWriter, builder.JniLocalReferenceLogWriter); } - Console.WriteLine($"# jonp: builder.InvocationPointer={builder.InvocationPointer.ToString("x2")}"); if (builder.InvocationPointer != IntPtr.Zero) return builder; - Console.WriteLine($"# jonp: loading {builder.JvmLibraryPath}..."); builder.LibraryHandler.LoadJvmLibrary (builder.JvmLibraryPath!); From 4c30ab53a997d664a2bc41dd696fa066040751d5 Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Wed, 25 Oct 2023 14:25:40 -0400 Subject: [PATCH 04/44] Linux builds require -Wl,-soname to be set properly. --- samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj b/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj index d5a55f216..d5721aa07 100644 --- a/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj +++ b/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj @@ -27,5 +27,9 @@ + + + + From a08dce8d2511e6323d1107f707026ecb678f4a2e Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Wed, 25 Oct 2023 14:34:53 -0400 Subject: [PATCH 05/44] Fix build break. --- tests/Java.Interop-Tests/Java.Interop/JniTypeTest.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/Java.Interop-Tests/Java.Interop/JniTypeTest.cs b/tests/Java.Interop-Tests/Java.Interop/JniTypeTest.cs index 9d3d8c294..92835b372 100644 --- a/tests/Java.Interop-Tests/Java.Interop/JniTypeTest.cs +++ b/tests/Java.Interop-Tests/Java.Interop/JniTypeTest.cs @@ -56,7 +56,8 @@ public unsafe void Dispose_Exceptions () Assert.Throws (() => t.IsAssignableFrom (null)); Assert.Throws (() => t.IsInstanceOfType (new JniObjectReference ())); Assert.Throws (() => t.RegisterWithRuntime ()); - Assert.Throws (() => t.RegisterNativeMethods (null)); + Assert.Throws (() => t.RegisterNativeMethods ((JniNativeMethodRegistration[])null)); + Assert.Throws (() => t.RegisterNativeMethods (new ReadOnlySpan ())); Assert.Throws (() => t.UnregisterNativeMethods ()); JniFieldInfo jif = null; From da9f188d8ffc19cc15a5dd90810151c378d73c56 Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Wed, 25 Oct 2023 15:20:16 -0400 Subject: [PATCH 06/44] Use [UnmanagedFunctionPointer] Commit db84cc3d noted that the sample failed to run because NativeAOT didn't know how to produce the required "stubs" to allow a `System.Delegate` field to be marshaled. Commit a32e2445 was an attempt to "fix" this by introducing a `JniBlittableNativeMethodRegistration` and using C#9 function pointers to register `ManagedPeer` marshal methods instead of delegates. While this works, this is an extensive change, and leaves various "corner cases" in how things fit together, such as the `JniNativeMethodRegistrationArguments` struct used as part of `jnimarshalmethod-gen`-based method registration. @lambdageek found an easier solution: place `[UnmanagedFunctionPointer]` onto the delegate declaration. This appears to tell NativeAOT to generate the required stubs, with significantly fewer changes. This also could fit nicely into a future `generator` change to place `[UnmanagedFunctionPointer]` on all the `_JniMarshal_*` declarations (56955d9a). --- build-tools/jnienv-gen/Generator.cs | 2 +- samples/Hello-NativeAOTFromJNI/README.md | 1 + src/Java.Interop/Java.Interop.csproj | 2 +- .../Java.Interop/JniEnvironment.Types.cs | 39 +---------- .../JniNativeMethodRegistration.cs | 66 ------------------- src/Java.Interop/Java.Interop/JniType.cs | 7 -- src/Java.Interop/Java.Interop/ManagedPeer.cs | 50 +++++++------- .../Java.Interop/JniTypeTest.cs | 3 +- 8 files changed, 33 insertions(+), 137 deletions(-) diff --git a/build-tools/jnienv-gen/Generator.cs b/build-tools/jnienv-gen/Generator.cs index 2b1c876cc..a3c4c2bf6 100644 --- a/build-tools/jnienv-gen/Generator.cs +++ b/build-tools/jnienv-gen/Generator.cs @@ -758,7 +758,7 @@ abstract class TypeInfo { "void*", new BuiltinTypeInfo ("void*", "IntPtr") }, { "const jchar*", new StringTypeInfo ("const jchar*") }, { "const char*", new StringTypeInfo ("const char*") }, - { "const JNINativeMethod*", new BuiltinTypeInfo ("const JNINativeMethod*", "IntPtr") }, + { "const JNINativeMethod*", new BuiltinTypeInfo ("const JNINativeMethod*", "JniNativeMethodRegistration []") }, { "jobjectRefType", new BuiltinTypeInfo ("jobjectRefType", "JniObjectReferenceType") }, { "jfieldID", new InstanceFieldTypeInfo ("jfieldID") }, { "jstaticfieldID", new StaticFieldTypeInfo ("jstaticfieldID") }, diff --git a/samples/Hello-NativeAOTFromJNI/README.md b/samples/Hello-NativeAOTFromJNI/README.md index abc74f49c..86ebe6e53 100644 --- a/samples/Hello-NativeAOTFromJNI/README.md +++ b/samples/Hello-NativeAOTFromJNI/README.md @@ -56,6 +56,7 @@ Time Elapsed 00:00:00.83 % (cd bin/Release/osx-x64/publish ; java -cp hello-from-java.jar:java-interop.jar com/microsoft/hello_from_jni/App) Hello from Java! Hello from .NET NativeAOT! +String returned to Java: Hello from .NET NativeAOT! ``` Note the use of `(cd …; java …)` so that `libHello-NativeAOTFromJNI.dylib` is diff --git a/src/Java.Interop/Java.Interop.csproj b/src/Java.Interop/Java.Interop.csproj index df955955b..43d5937e3 100644 --- a/src/Java.Interop/Java.Interop.csproj +++ b/src/Java.Interop/Java.Interop.csproj @@ -26,7 +26,7 @@ $(ToolOutputFullPath) $(ToolOutputFullPath)Java.Interop.xml $(BuildToolOutputFullPath) - 11.0 + 9.0 8.0 $(JICoreLibVersion) true diff --git a/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs b/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs index 93adc3caa..da4d41b99 100644 --- a/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs +++ b/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs @@ -250,9 +250,6 @@ public static void RegisterNatives (JniObjectReference type, JniNativeMethodRegi throw new ArgumentOutOfRangeException (nameof (numMethods), numMethods, $"`numMethods` must be between 0 and `methods.Length` ({methods?.Length ?? 0})!"); } - if (methods == null || numMethods == 0) { - return; - } #if DEBUG && NETCOREAPP for (int i = 0; methods != null && i < numMethods; ++i) { var m = methods [i]; @@ -265,42 +262,8 @@ public static void RegisterNatives (JniObjectReference type, JniNativeMethodRegi } #endif // DEBUG && NETCOREAPP - Span blittableMethods - = stackalloc JniBlittableNativeMethodRegistration [numMethods]; - Span freeStrings - = stackalloc IntPtr [numMethods * 2]; - try { - for (int i = 0; i < numMethods; ++i) { - var name = methods [i].Name == null ? IntPtr.Zero : Marshal.StringToCoTaskMemUTF8 (methods [i].Name); - var sig = methods [i].Signature == null ? IntPtr.Zero : Marshal.StringToCoTaskMemUTF8 (methods [i].Signature); - - freeStrings [(i*2)+0] = name; - freeStrings [(i*2)+1] = sig; - - blittableMethods [i] = new JniBlittableNativeMethodRegistration ( - name, - sig, - Marshal.GetFunctionPointerForDelegate (methods [i].Marshaler) - ); - } - RegisterNatives (type, blittableMethods); - } - finally { - for (int i = 0; i < freeStrings.Length; ++i) { - Marshal.ZeroFreeCoTaskMemUTF8 (freeStrings [i]); - } - } - } + int r = _RegisterNatives (type, methods, numMethods); - public static unsafe void RegisterNatives (JniObjectReference type, ReadOnlySpan methods) - { - if (methods.Length == 0) { - return; - } - int r; - fixed (JniBlittableNativeMethodRegistration* pMethods = methods) { - r = _RegisterNatives (type, (IntPtr) pMethods, methods.Length); - } if (r != 0) { throw new InvalidOperationException ( string.Format ("Could not register native methods for class '{0}'; JNIEnv::RegisterNatives() returned {1}.", GetJniTypeNameFromClass (type), r)); diff --git a/src/Java.Interop/Java.Interop/JniNativeMethodRegistration.cs b/src/Java.Interop/Java.Interop/JniNativeMethodRegistration.cs index c638d1e16..20f99f947 100644 --- a/src/Java.Interop/Java.Interop/JniNativeMethodRegistration.cs +++ b/src/Java.Interop/Java.Interop/JniNativeMethodRegistration.cs @@ -20,70 +20,4 @@ public JniNativeMethodRegistration (string name, string signature, Delegate mars Marshaler = marshaler ?? throw new ArgumentNullException (nameof (marshaler)); } } - - public struct JniBlittableNativeMethodRegistration : IEquatable { - - IntPtr name, signature, marshaler; - - public JniBlittableNativeMethodRegistration (IntPtr name, IntPtr signature, IntPtr marshaler) - { - if (name == IntPtr.Zero) - throw new ArgumentNullException (nameof (name)); - if (signature == IntPtr.Zero) - throw new ArgumentNullException (nameof (signature)); - if (marshaler == IntPtr.Zero) - throw new ArgumentNullException (nameof (marshaler)); - - this.name = name; - this.signature = signature; - this.marshaler = marshaler; - } - - public JniBlittableNativeMethodRegistration (ReadOnlySpan name, ReadOnlySpan signature, IntPtr marshaler) - { - if (name.Length == 0) - throw new ArgumentException ("must not be empty", nameof (name)); - if (signature.Length == 0) - throw new ArgumentException ("must not be empty", nameof (signature)); - if (marshaler == IntPtr.Zero) - throw new ArgumentNullException (nameof (marshaler)); - - this.name = FromReadOnlySpan (name); - this.signature = FromReadOnlySpan (signature); - this.marshaler = marshaler; - } - - // Dodgy AF, but as the C# compiler guarantees no allocations for u8 strings, - // the "address" of `value` will never move, so this is "fine"… - // *so long as* it's *only* used for "string"u8 values. - // https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-11.0/utf8-string-literals#detailed-design - // > Since the literals would be allocated as global constants, the lifetime of the - // > resulting ReadOnlySpan would not prevent it from being returned or passed around to elsewhere. - static unsafe IntPtr FromReadOnlySpan (ReadOnlySpan value) - { - fixed (byte* p = value) - return (IntPtr) p; - } - - public override bool Equals (object? obj) - { - if (obj is JniBlittableNativeMethodRegistration other) { - return Equals (other); - } - return false; - } - - public override int GetHashCode () => - HashCode.Combine (name, signature, marshaler); - - public bool Equals (JniBlittableNativeMethodRegistration other) => - name == other.name && - signature == other.signature && - marshaler == other.marshaler; - - public static bool operator == (JniBlittableNativeMethodRegistration lhs, JniBlittableNativeMethodRegistration rhs) => - lhs.Equals (rhs); - public static bool operator != (JniBlittableNativeMethodRegistration lhs, JniBlittableNativeMethodRegistration rhs) => - !lhs.Equals (rhs); - } } diff --git a/src/Java.Interop/Java.Interop/JniType.cs b/src/Java.Interop/Java.Interop/JniType.cs index f3fa29bb7..396f5067b 100644 --- a/src/Java.Interop/Java.Interop/JniType.cs +++ b/src/Java.Interop/Java.Interop/JniType.cs @@ -168,13 +168,6 @@ public void RegisterNativeMethods (params JniNativeMethodRegistration[] methods) RegisterWithRuntime (); } - public void RegisterNativeMethods (ReadOnlySpan methods) - { - AssertValid (); - JniEnvironment.Types.RegisterNatives (PeerReference, methods); - RegisterWithRuntime (); - } - public void UnregisterNativeMethods () { AssertValid (); diff --git a/src/Java.Interop/Java.Interop/ManagedPeer.cs b/src/Java.Interop/Java.Interop/ManagedPeer.cs index 56ca02f2f..044b94247 100644 --- a/src/Java.Interop/Java.Interop/ManagedPeer.cs +++ b/src/Java.Interop/Java.Interop/ManagedPeer.cs @@ -20,27 +20,18 @@ namespace Java.Interop { static readonly JniPeerMembers _members = new JniPeerMembers (JniTypeName, typeof (ManagedPeer)); - static unsafe ManagedPeer () + static ManagedPeer () { - Span registrations = stackalloc JniBlittableNativeMethodRegistration [2]; - - delegate* unmanaged construct = &Construct; - registrations [0] = new JniBlittableNativeMethodRegistration ( - "construct"u8, - "(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;[Ljava/lang/Object;)V"u8, - (IntPtr) construct); - - delegate* unmanaged registerNativeMembers = &RegisterNativeMembers; - registrations [1] = new JniBlittableNativeMethodRegistration ( - "registerNativeMembers"u8, - "(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;)V"u8, - (IntPtr) registerNativeMembers); - - _members.JniPeerType.RegisterNativeMethods (registrations); + _members.JniPeerType.RegisterNativeMethods ( + new JniNativeMethodRegistration ( + "construct", + ConstructSignature, + new ConstructMarshalMethod (Construct)), + new JniNativeMethodRegistration ( + "registerNativeMembers", + RegisterNativeMembersSignature, + new RegisterMarshalMethod (RegisterNativeMembers)) + ); } ManagedPeer () @@ -57,8 +48,16 @@ public override JniPeerMembers JniPeerMembers { get {return _members;} } + const string ConstructSignature = "(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;[Ljava/lang/Object;)V"; + // TODO: Keep in sync with the code generated by ExportedMemberBuilder - [UnmanagedCallersOnly] + [UnmanagedFunctionPointer (CallingConvention.Winapi)] + delegate void ConstructMarshalMethod (IntPtr jnienv, + IntPtr klass, + IntPtr n_self, + IntPtr n_assemblyQualifiedName, + IntPtr n_constructorSignature, + IntPtr n_constructorArguments); static void Construct ( IntPtr jnienv, IntPtr klass, @@ -178,7 +177,14 @@ static Type[] GetParameterTypes (string? signature) return pvalues; } - [UnmanagedCallersOnly] + const string RegisterNativeMembersSignature = "(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;)V"; + + [UnmanagedFunctionPointer (CallingConvention.Winapi)] + delegate void RegisterMarshalMethod (IntPtr jnienv, + IntPtr klass, + IntPtr n_nativeClass, + IntPtr n_assemblyQualifiedName, + IntPtr n_methods); static unsafe void RegisterNativeMembers ( IntPtr jnienv, IntPtr klass, diff --git a/tests/Java.Interop-Tests/Java.Interop/JniTypeTest.cs b/tests/Java.Interop-Tests/Java.Interop/JniTypeTest.cs index 92835b372..9d3d8c294 100644 --- a/tests/Java.Interop-Tests/Java.Interop/JniTypeTest.cs +++ b/tests/Java.Interop-Tests/Java.Interop/JniTypeTest.cs @@ -56,8 +56,7 @@ public unsafe void Dispose_Exceptions () Assert.Throws (() => t.IsAssignableFrom (null)); Assert.Throws (() => t.IsInstanceOfType (new JniObjectReference ())); Assert.Throws (() => t.RegisterWithRuntime ()); - Assert.Throws (() => t.RegisterNativeMethods ((JniNativeMethodRegistration[])null)); - Assert.Throws (() => t.RegisterNativeMethods (new ReadOnlySpan ())); + Assert.Throws (() => t.RegisterNativeMethods (null)); Assert.Throws (() => t.UnregisterNativeMethods ()); JniFieldInfo jif = null; From 69c90baf8d795cd18e36f8b68d7a250b9562037e Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Wed, 1 Nov 2023 13:19:18 -0400 Subject: [PATCH 07/44] "Full(er)" sample MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What `Hello-NativeAOTFromJNI` previously did was quite minimal: 1. Use `[UnmanagedCallersOnly]` to provide a `JNI_OnLoad()` method which 2. Initialized the Java.Interop runtime, allowing 3. An `[UnmanagedCallersOnly]`-provided `Java_…` method which is called from Java. All quite low level, all miles away from .NET Android. Expand the sample to: 1. Contain a `Java.Lang.Object` subclass, which contains a `[JavaCallable]` method. 2. Call `jcw-gen` to generate Java Callable Wrappers for (1), containing the `[JavaCallable]` method. 3. Call `jnimarshalmethod-gen` to generate marshal methods for (1), as NativeAOT doesn't support System.Reflection.Emit. 4. Instantiate (1) *from Java*, and invoke the `[JavaCallable]` method. *Now* we're (kinda) getting something that looks like .NET Android. But first, we need to make that *work*: Update `Java.Interop.Tools.JavaCallableWrappers` so that it will emit `native` method declarations for `[JavaCallable]` methods, not just method overrides and `[Export]` methods. Update `Java.Interop.Tools.Expressions` so that the `_JniMarshal_*` delegate types have `[UnmanagedFunctionPointer(CallingConvention.Winapi)]`, as this is what allows NativeAOT to emit appropriate "stubs"; see also da9f188d8ffc19cc15a5dd90810151c378d73c56. Update `Java.Interop.Tools.Expressions.ExpressionAssemblyBuilder` to no longer attempt to "remove and fixup" `System.Private.CoreLib`. So long as `ExpressionAssemblyBuilder` output is *only* used in "completed" apps (not distributed in NuGet packages in some "intermediate" form), referencing `System.Private.CoreLib` is "fine". Additionally, trying to remove `System.Private.CoreLib` broke things when adding `[UnmanagedFunctionPointer]`, as `CallingConvention` could not be resolved, resulting in `jnimarshalmethod-gen` erroring out with: error JM4006: jnimarshalmethod-gen: Unable to process assembly '/Volumes/Xamarin-Work/src/xamarin/Java.Interop/samples/Hello-NativeAOTFromJNI/bin/Debug/Hello-NativeAOTFromJNI.dll' Failed to resolve System.Runtime.InteropServices.CallingConvention Mono.Cecil.ResolutionException: Failed to resolve System.Runtime.InteropServices.CallingConvention at Mono.Cecil.Mixin.CheckedResolve(TypeReference self) at Mono.Cecil.SignatureWriter.WriteCustomAttributeEnumValue(TypeReference enum_type, Object value) … (This is because `CallingConvention` is in `System.Runtime.InteropServices.dll`, which isn't referenced.) We could "fix" this by explicitly adding a reference to `System.Runtime.InteropServices.dll`, but this is just one of an unknown number of corner cases. Give up for now. Update `jnimarshalmethod-gen` assembly location probing: it was given the *full assembly name* of `Java.Base`: # jonp: resolving assembly: Java.Base, Version=7.0.0.0, Culture=neutral, PublicKeyToken=null …and failing to find `Java.Base.dll`, because it was looking for `Java.Base, Version=7.0.0.0, Culture=neutral, PublicKeyToken=null.dll`. Oops. Use `AssemblyName` to parse the string and extract out the assembly name., so that `Java.Base.dll` is probed for and found. With all that…it still fails: % (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! 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.(ManagedType.java:15) at com.microsoft.hello_from_jni.App.main(App.java:13) `App.main()` has `new example.ManagedType()`, which hits the `ManagedType` static constructor of: public /* partial */ class ManagedType extends java.lang.Object implements com.xamarin.java_interop.GCUserPeerable { /** @hide */ public static final String __md_methods; static { __md_methods = "n_GetString:()Ljava/lang/String;:__export__\n" + ""; com.xamarin.java_interop.ManagedPeer.registerNativeMembers (ManagedType.class, "Example.ManagedType, Hello-NativeAOTFromJNI", __md_methods); } } The `ManagedPeer.registerNativeMembers()` call is what is needed to register the native `ManagedPeer.getString()` method, so that it can be called. This is good. (Though `__md_methods` containing *anything* is not desired, but that's a different problem.) `ManagedPeer.RegisterNativeMembers()` is given the assembly-qualified name `Example.ManagedType, Hello-NativeAOTFromJNI`, and tries to: Type.GetType ("Example.ManagedType, Hello-NativeAOTFromJNI", throwOnError: true); …which then proceeds to throw, because in NativeAOT *there are no assemblies*, and thus `Type.GetType()` *cannot work*. Oops. Thus, the only way to make something remotely like .NET Android infrastructure work is to *require* the use of `Java_…` native method names and `[UnmanagedCallersOnly]` on marshal methods. (In .NET Android parlance, the experimental `$(AndroidEnableMarshalMethods)`=True is required.) --- samples/Hello-NativeAOTFromJNI/App.cs | 20 +++++++ .../Hello-NativeAOTFromJNI.csproj | 2 + .../Hello-NativeAOTFromJNI.targets | 53 ++++++++++++++++++- .../{JNIEnvInit.cs => JavaInteropRuntime.cs} | 14 ++--- samples/Hello-NativeAOTFromJNI/ManagedType.cs | 17 ++++++ .../com/microsoft/hello_from_jni/App.java | 10 +++- .../hello_from_jni/NativeAOTInit.java | 9 ---- .../java_interop/JavaInteropRuntime.java | 12 +++++ .../ExpressionAssemblyBuilder.cs | 17 +++++- .../JavaCallableWrapperGenerator.cs | 22 ++++++-- tools/jnimarshalmethod-gen/App.cs | 5 +- 11 files changed, 155 insertions(+), 26 deletions(-) create mode 100644 samples/Hello-NativeAOTFromJNI/App.cs rename samples/Hello-NativeAOTFromJNI/{JNIEnvInit.cs => JavaInteropRuntime.cs} (55%) create mode 100644 samples/Hello-NativeAOTFromJNI/ManagedType.cs delete mode 100644 samples/Hello-NativeAOTFromJNI/java/com/microsoft/hello_from_jni/NativeAOTInit.java create mode 100644 samples/Hello-NativeAOTFromJNI/java/com/microsoft/java_interop/JavaInteropRuntime.java diff --git a/samples/Hello-NativeAOTFromJNI/App.cs b/samples/Hello-NativeAOTFromJNI/App.cs new file mode 100644 index 000000000..0cb0ac50d --- /dev/null +++ b/samples/Hello-NativeAOTFromJNI/App.cs @@ -0,0 +1,20 @@ +using System.Runtime.InteropServices; + +using Java.Interop; + +namespace Hello_NativeAOTFromJNI; + +static class App { + + // symbol name from `$(IntermediateOutputPath)/h-classes/com_microsoft_hello_from_jni_NativeAOTInit.h` + [UnmanagedCallersOnly (EntryPoint="Java_com_microsoft_hello_1from_1jni_App_sayHello")] + static IntPtr sayHello (IntPtr jnienv, IntPtr klass) + { + var s = $"Hello from .NET NativeAOT!"; + Console.WriteLine (s); + var h = JniEnvironment.Strings.NewString (s); + var r = JniEnvironment.References.NewReturnToJniRef (h); + JniObjectReference.Dispose (ref h); + return r; + } +} \ No newline at end of file diff --git a/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj b/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj index d5721aa07..b062480fe 100644 --- a/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj +++ b/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj @@ -17,6 +17,8 @@ + + diff --git a/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets b/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets index 5e5cf7811..4b70acf9f 100644 --- a/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets +++ b/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets @@ -1,11 +1,62 @@ + + $(DOTNET_HOST_PATH) + $(MSBuildThisFileDirectory)..\..\bin\Debug-net7.0 + + + + + + + + <_RefAsmDirs Include="@(ReferencePathWithRefAssemblies->'%(RootDir)%(Directory).'->Distinct())" /> + + + <_JcwGen>"$(UtilityOutputFullPath)/jcw-gen.dll" + <_Target>--codegen-target JavaInterop1 + <_Output>-o "$(IntermediateOutputPath)/java" + <_Libpath>@(_RefAsmDirs->'-L "%(Identity)"', ' ') + + + + + + + + + <_RefAsmDirs Include="@(ReferencePathWithRefAssemblies->'%(RootDir)%(Directory).'->Distinct())" /> + + + <_JnimarshalmethodGen>"$(UtilityOutputFullPath)/jnimarshalmethod-gen.dll" + <_Verbosity>-v -v --keeptemp + <_Libpath>-L "$(TargetDir)" @(_RefAsmDirs->'-L "%(Identity)"', ' ') + + + + + + + + + <_JcwSource Include="$(IntermediateOutputPath)java\**\*.java" /> + + + <_Source Include="@(_JcwSource->Replace('%5c', '/'))" /> <_Source Include="@(HelloNativeAOTFromJNIJar->Replace('%5c', '/'))" /> 0 ? (string) attr.ConstructorArguments [0].Value : declaringMember.Name; + return new ExportAttribute (name); + } + internal static ExportFieldAttribute ToExportFieldAttribute (CustomAttribute attr) { return new ExportFieldAttribute ((string) attr.ConstructorArguments [0].Value); @@ -447,7 +456,8 @@ static IEnumerable GetMethodRegistrationAttributes (Mono.Ceci IEnumerable GetExportAttributes (IMemberDefinition p) { - return GetAttributes (p, a => ToExportAttribute (a, p)); + return GetAttributes (p, a => ToExportAttribute (a, p)) + .Concat (GetAttributes (p, "Java.Interop.JavaCallableAttribute", a => ToExportAttributeFromJavaCallableAttribute (a, p))); } static IEnumerable GetExportFieldAttributes (Mono.Cecil.ICustomAttributeProvider p) @@ -458,7 +468,13 @@ static IEnumerable GetExportFieldAttributes (Mono.Cecil.IC static IEnumerable GetAttributes (Mono.Cecil.ICustomAttributeProvider p, Func selector) where TAttribute : class { - return p.GetCustomAttributes (typeof (TAttribute)) + return GetAttributes (p, typeof (TAttribute).FullName, selector); + } + + static IEnumerable GetAttributes (Mono.Cecil.ICustomAttributeProvider p, string attributeName, Func selector) + where TAttribute : class + { + return p.GetCustomAttributes (attributeName) .Select (selector) .Where (v => v != null) .Select (v => v!); diff --git a/tools/jnimarshalmethod-gen/App.cs b/tools/jnimarshalmethod-gen/App.cs index adf938648..946ef4a6b 100644 --- a/tools/jnimarshalmethod-gen/App.cs +++ b/tools/jnimarshalmethod-gen/App.cs @@ -228,9 +228,11 @@ void ProcessAssemblies (List assemblies) // loadContext = CreateLoadContext (); AppDomain.CurrentDomain.AssemblyResolve += (o, e) => { Log (TraceLevel.Verbose, $"# jonp: resolving assembly: {e.Name}"); + var name = new AssemblyName (e.Name); foreach (var d in resolver.SearchDirectories) { - var a = Path.Combine (d, e.Name); + var a = Path.Combine (d, name.Name); var f = a + ".dll"; + Log (TraceLevel.Verbose, $"# jonp: checking for: {f} ({File.Exists (f)})"); if (File.Exists (f)) { return Assembly.LoadFile (Path.GetFullPath (f)); } @@ -239,6 +241,7 @@ void ProcessAssemblies (List assemblies) return Assembly.LoadFile (Path.GetFullPath (f)); } } + Log (TraceLevel.Verbose, $"# jonp: could not resolve assembly: {e.Name}"); return null; }; From 1fc0af5213af94e910dec7b1d2ac2ee8ab6e1ab5 Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Thu, 2 Nov 2023 11:03:07 -0400 Subject: [PATCH 08/44] Use JniTransition. --- samples/Hello-NativeAOTFromJNI/App.cs | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/samples/Hello-NativeAOTFromJNI/App.cs b/samples/Hello-NativeAOTFromJNI/App.cs index 0cb0ac50d..b8729218d 100644 --- a/samples/Hello-NativeAOTFromJNI/App.cs +++ b/samples/Hello-NativeAOTFromJNI/App.cs @@ -6,15 +6,26 @@ namespace Hello_NativeAOTFromJNI; static class App { - // symbol name from `$(IntermediateOutputPath)/h-classes/com_microsoft_hello_from_jni_NativeAOTInit.h` + // symbol name from `$(IntermediateOutputPath)h-classes/com_microsoft_hello_from_jni_App.h` [UnmanagedCallersOnly (EntryPoint="Java_com_microsoft_hello_1from_1jni_App_sayHello")] static IntPtr sayHello (IntPtr jnienv, IntPtr klass) { - var s = $"Hello from .NET NativeAOT!"; - Console.WriteLine (s); - var h = JniEnvironment.Strings.NewString (s); - var r = JniEnvironment.References.NewReturnToJniRef (h); - JniObjectReference.Dispose (ref h); - return r; + var envp = new JniTransition (jnienv); + try { + var s = $"Hello from .NET NativeAOT!"; + Console.WriteLine (s); + var h = JniEnvironment.Strings.NewString (s); + var r = JniEnvironment.References.NewReturnToJniRef (h); + JniObjectReference.Dispose (ref h); + return r; + } + catch (Exception e) { + Console.Error.WriteLine ($"Error in App.sayHello(): {e.ToString ()}"); + envp.SetPendingException (e); + } + finally { + envp.Dispose (); + } + return nint.Zero; } } \ No newline at end of file From a499a1df8841006c90e66553da1f39ccb1861d0b Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Thu, 2 Nov 2023 13:01:09 -0400 Subject: [PATCH 09/44] Allow init from JNIEnv*, not just JavaVM* --- .../JavaInteropRuntime.cs | 25 +++++++-------- src/Java.Interop/Java.Interop/JniRuntime.cs | 31 +++++++++++++++++-- .../Java.Interop/JreRuntime.cs | 6 ++-- 3 files changed, 44 insertions(+), 18 deletions(-) diff --git a/samples/Hello-NativeAOTFromJNI/JavaInteropRuntime.cs b/samples/Hello-NativeAOTFromJNI/JavaInteropRuntime.cs index 33472c9a2..4a0466426 100644 --- a/samples/Hello-NativeAOTFromJNI/JavaInteropRuntime.cs +++ b/samples/Hello-NativeAOTFromJNI/JavaInteropRuntime.cs @@ -11,17 +11,7 @@ static class JavaInteropRuntime [UnmanagedCallersOnly (EntryPoint="JNI_OnLoad")] static int JNI_OnLoad (IntPtr vm, IntPtr reserved) { - try { - var options = new JreRuntimeOptions { - InvocationPointer = vm, - }; - runtime = options.CreateJreVM (); - return (int) runtime.JniVersion; - } - catch (Exception e) { - Console.Error.WriteLine ($"JNI_OnLoad: error: {e}"); - return 0; - } + return (int) JniVersion.v1_6; } [UnmanagedCallersOnly (EntryPoint="JNI_OnUnload")] @@ -31,8 +21,17 @@ static void JNI_Onload (IntPtr vm, IntPtr reserved) } [UnmanagedCallersOnly (EntryPoint="Java_com_microsoft_java_1interop_JavaInteropRuntime_init")] - static void init () + static void init (IntPtr jnienv, IntPtr klass) { - Console.Error.WriteLine ($"C# init()"); + Console.WriteLine ($"C# init()"); + try { + var options = new JreRuntimeOptions { + EnvironmentPointer = jnienv, + }; + runtime = options.CreateJreVM (); + } + catch (Exception e) { + Console.Error.WriteLine ($"JavaInteropRuntime.init: error: {e}"); + } } } diff --git a/src/Java.Interop/Java.Interop/JniRuntime.cs b/src/Java.Interop/Java.Interop/JniRuntime.cs index cd377f3f2..75b1b4485 100644 --- a/src/Java.Interop/Java.Interop/JniRuntime.cs +++ b/src/Java.Interop/Java.Interop/JniRuntime.cs @@ -165,8 +165,8 @@ protected JniRuntime (CreationOptions options) { if (options == null) throw new ArgumentNullException (nameof (options)); - if (options.InvocationPointer == IntPtr.Zero) - throw new ArgumentException ("options.InvocationPointer is null", nameof (options)); + if (options.InvocationPointer == IntPtr.Zero && options.EnvironmentPointer == IntPtr.Zero) + throw new ArgumentException ("Need either options.InvocationPointer or options.EnvironmentPointer!", nameof (options)); TrackIDs = options.TrackIDs; DestroyRuntimeOnDispose = options.DestroyRuntimeOnDispose; @@ -175,7 +175,12 @@ protected JniRuntime (CreationOptions options) NewObjectRequired = options.NewObjectRequired; JniVersion = options.JniVersion; - InvocationPointer = options.InvocationPointer; + + if (options.InvocationPointer == IntPtr.Zero && options.EnvironmentPointer != IntPtr.Zero) { + InvocationPointer = GetInvocationPointerFromEnvironmentPointer (options.EnvironmentPointer); + } else { + InvocationPointer = options.InvocationPointer; + } Invoker = CreateInvoker (InvocationPointer); SetValueManager (options); @@ -230,6 +235,26 @@ protected JniRuntime (CreationOptions options) #endif // !XA_JI_EXCLUDE } + static unsafe IntPtr GetInvocationPointerFromEnvironmentPointer (IntPtr envp) + { + IntPtr vm = IntPtr.Zero; +#if FEATURE_JNIENVIRONMENT_JI_FUNCTION_POINTERS + if (JniNativeMethods.GetJavaVM (envp, &vm) is int r && + r != JNI_OK) { + throw new InvalidOperationException ($"Could not obtain JavaVM* from JNIEnv*; JNIEnv::GetJavaVM() returned {r}!"); + } +#elif FEATURE_JNIENVIRONMENT_JI_PINVOKES + if (NativeMethods.java_interop_jnienv_get_java_vm (envp, out vm) is int r && + r != JNI_OK) { + throw new InvalidOperationException ($"Could not obtain JavaVM* from JNIEnv*; JNIEnv::GetJavaVM() returned {r}!"); + } +#else // !FEATURE_JNIENVIRONMENT_JI_FUNCTION_POINTERS && !FEATURE_JNIENVIRONMENT_JI_PINVOKES + throw new NotSupportedException ("Cannot obtain JavaVM* from JNIEnv*! " + + "Rebuild with FEATURE_JNIENVIRONMENT_JI_FUNCTION_POINTERS or FEATURE_JNIENVIRONMENT_JI_PINVOKES set!"); +#endif // !FEATURE_JNIENVIRONMENT_JI_FUNCTION_POINTERS && !FEATURE_JNIENVIRONMENT_JI_PINVOKES + return vm; + } + T SetRuntime (T value) where T : class, ISetRuntime { diff --git a/src/Java.Runtime.Environment/Java.Interop/JreRuntime.cs b/src/Java.Runtime.Environment/Java.Interop/JreRuntime.cs index 3c49d68b0..4d0d27a03 100644 --- a/src/Java.Runtime.Environment/Java.Interop/JreRuntime.cs +++ b/src/Java.Runtime.Environment/Java.Interop/JreRuntime.cs @@ -76,7 +76,9 @@ static unsafe JreRuntimeOptions CreateJreVM (JreRuntimeOptions builder) { if (builder == null) throw new ArgumentNullException ("builder"); - if (builder.InvocationPointer == IntPtr.Zero && string.IsNullOrEmpty (builder.JvmLibraryPath)) + if (builder.InvocationPointer == IntPtr.Zero && + builder.EnvironmentPointer == IntPtr.Zero && + string.IsNullOrEmpty (builder.JvmLibraryPath)) throw new InvalidOperationException ($"Member `{nameof (JreRuntimeOptions)}.{nameof (JreRuntimeOptions.JvmLibraryPath)}` must be set."); builder.LibraryHandler = JvmLibraryHandler.Create (); @@ -95,7 +97,7 @@ static unsafe JreRuntimeOptions CreateJreVM (JreRuntimeOptions builder) builder.ObjectReferenceManager = builder.ObjectReferenceManager ?? new ManagedObjectReferenceManager (builder.JniGlobalReferenceLogWriter, builder.JniLocalReferenceLogWriter); } - if (builder.InvocationPointer != IntPtr.Zero) + if (builder.InvocationPointer != IntPtr.Zero || builder.EnvironmentPointer != IntPtr.Zero) return builder; builder.LibraryHandler.LoadJvmLibrary (builder.JvmLibraryPath!); From e1822f05ff735aa77f7d4458216081765b03cdb7 Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Thu, 2 Nov 2023 17:11:20 -0400 Subject: [PATCH 10/44] Make it (mostly) work! MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Commit 69c90baf 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.(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 assmblyQualifiedTypeName, ReadOnlySpan 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! --- .../Hello-NativeAOTFromJNI.csproj | 5 ++ .../Hello-NativeAOTFromJNI.targets | 3 ++ .../Hello-NativeAOTFromJNI.xml | 7 +++ .../JavaInteropRuntime.cs | 1 + samples/Hello-NativeAOTFromJNI/ManagedType.cs | 6 +++ .../NativeAotTypeManager.cs | 48 +++++++++++++++++++ .../ExpressionAssemblyBuilder.cs | 9 ++-- .../Java.Interop/JniEnvironment.Types.cs | 2 + .../Java.Interop/JniRuntime.JniTypeManager.cs | 23 +++++++-- src/Java.Interop/Java.Interop/ManagedPeer.cs | 13 +++-- tools/jnimarshalmethod-gen/App.cs | 19 +++----- 11 files changed, 112 insertions(+), 24 deletions(-) create mode 100644 samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.xml create mode 100644 samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs diff --git a/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj b/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj index b062480fe..0c42785d4 100644 --- a/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj +++ b/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj @@ -21,6 +21,11 @@ + + + + + diff --git a/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets b/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets index 4b70acf9f..2913a492b 100644 --- a/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets +++ b/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets @@ -44,6 +44,9 @@ + + + diff --git a/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.xml b/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.xml new file mode 100644 index 000000000..5fc989ecc --- /dev/null +++ b/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/samples/Hello-NativeAOTFromJNI/JavaInteropRuntime.cs b/samples/Hello-NativeAOTFromJNI/JavaInteropRuntime.cs index 4a0466426..a59fa0a77 100644 --- a/samples/Hello-NativeAOTFromJNI/JavaInteropRuntime.cs +++ b/samples/Hello-NativeAOTFromJNI/JavaInteropRuntime.cs @@ -27,6 +27,7 @@ static void init (IntPtr jnienv, IntPtr klass) try { var options = new JreRuntimeOptions { EnvironmentPointer = jnienv, + TypeManager = new NativeAotTypeManager (), }; runtime = options.CreateJreVM (); } diff --git a/samples/Hello-NativeAOTFromJNI/ManagedType.cs b/samples/Hello-NativeAOTFromJNI/ManagedType.cs index 55a86745e..374c03fdc 100644 --- a/samples/Hello-NativeAOTFromJNI/ManagedType.cs +++ b/samples/Hello-NativeAOTFromJNI/ManagedType.cs @@ -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 () { diff --git a/samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs b/samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs new file mode 100644 index 000000000..2f2dda3de --- /dev/null +++ b/samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs @@ -0,0 +1,48 @@ +using Java.Interop; + +namespace Hello_NativeAOTFromJNI; + +class NativeAotTypeManager : JniRuntime.JniTypeManager { + +#pragma warning disable IL2026 + Dictionary 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 assemblyQualifiedTypeName, ReadOnlySpan 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 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 GetSimpleReferences (Type type) + { + return base.GetSimpleReferences (type) + .Concat (CreateSimpleReferencesEnumerator (type)); + } + + IEnumerable CreateSimpleReferencesEnumerator (Type type) + { + if (typeMappings == null) + yield break; + foreach (var e in typeMappings) { + if (e.Value == type) + yield return e.Key; + } + } +} diff --git a/src/Java.Interop.Tools.Expressions/Java.Interop.Tools.Expressions/ExpressionAssemblyBuilder.cs b/src/Java.Interop.Tools.Expressions/Java.Interop.Tools.Expressions/ExpressionAssemblyBuilder.cs index d4f2db1f6..1e48ee170 100644 --- a/src/Java.Interop.Tools.Expressions/Java.Interop.Tools.Expressions/ExpressionAssemblyBuilder.cs +++ b/src/Java.Interop.Tools.Expressions/Java.Interop.Tools.Expressions/ExpressionAssemblyBuilder.cs @@ -59,7 +59,7 @@ static MethodDefinition CreateMethodDefinition (AssemblyDefinition declaringAsse return mmDef; } - public MethodDefinition CreateRegistrationMethod (IList methods) + public void AddRegistrationMethod (TypeDefinition declaringType, IList methods) { var registrations = new MethodDefinition ( name: "__RegisterNativeMembers", @@ -71,6 +71,8 @@ public MethodDefinition CreateRegistrationMethod (IList CreateGetTypesForSimpleReferenceEnumerator (string jniSimpleRe protected virtual ReplacementMethodInfo? GetReplacementMethodInfoCore (string jniSimpleReference, string jniMethodName, string jniMethodSignature) => null; + public virtual void RegisterNativeMembers (JniType nativeClass, ReadOnlySpan assmblyQualifiedTypeName, ReadOnlySpan methods) => + RegisterNativeMembers (nativeClass, Type.GetType (new string (assmblyQualifiedTypeName), throwOnError: true)!, methods); + public virtual void RegisterNativeMembers (JniType nativeClass, Type type, ReadOnlySpan methods) { TryRegisterNativeMembers (nativeClass, type, methods); @@ -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 ?? "")}.__<$>_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); } @@ -466,11 +480,14 @@ bool FindAndCallRegisterMethod (Type marshalType, JniNativeMethodRegistrationArg continue; } + var declaringTypeName = methodInfo.DeclaringType?.FullName ?? ""; + 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)methodInfo.CreateDelegate (typeof (Action)); + Console.WriteLine ($"# jonp: FindAndCallRegisterMethod: calling `{declaringTypeName}.{methodInfo}`…"); register (arguments); found = true; diff --git a/src/Java.Interop/Java.Interop/ManagedPeer.cs b/src/Java.Interop/Java.Interop/ManagedPeer.cs index 044b94247..1371692ff 100644 --- a/src/Java.Interop/Java.Interop/ManagedPeer.cs +++ b/src/Java.Interop/Java.Interop/ManagedPeer.cs @@ -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(aqnChars, aqnLength); + int methodsLength = JniEnvironment.Strings.GetStringLength (methodsRef); var methodsChars = JniEnvironment.Strings.GetStringChars (methodsRef, null); var methods = new ReadOnlySpan(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 diff --git a/tools/jnimarshalmethod-gen/App.cs b/tools/jnimarshalmethod-gen/App.cs index 946ef4a6b..c6e9bed91 100644 --- a/tools/jnimarshalmethod-gen/App.cs +++ b/tools/jnimarshalmethod-gen/App.cs @@ -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); } } From 9027591c41232c82a582f8ab16ddaa783795582c Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Mon, 6 Nov 2023 20:45:46 -0500 Subject: [PATCH 11/44] Fix Java.Interop.Export-Tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A funny thing happened on the way through CI: `Java.Interop.Export-Tests` *with* `jnimarshalmethod-gen` failed! Failed AddExportMethods [169 ms] Error Message: Java.Interop.JavaException : Could not initialize class com.xamarin.interop.export.ExportType Stack Trace: at Java.Interop.JniEnvironment.StaticMethods.GetStaticMethodID(JniObjectReference type, String name, String signature) in /Users/runner/work/1/s/src/Java.Interop/obj/Release/net7.0/JniEnvironment.g.cs:line 21407 at Java.Interop.JniType.GetStaticMethod(String name, String signature) in /Users/runner/work/1/s/src/Java.Interop/Java.Interop/JniType.cs:line 315 at Java.InteropTests.MarshalMemberBuilderTest.AddExportMethods() at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor) at System.Reflection.MethodInvoker.Invoke(Object obj, IntPtr* args, BindingFlags invokeAttr) --- End of managed Java.Interop.JavaException stack trace --- java.lang.NoClassDefFoundError: Could not initialize class com.xamarin.interop.export.ExportType The cause? e1822f05 updated `jnimarshalmethod-gen`: - 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)); That is, instead of attempting to register e.g. `ExportType.action()V` (via `[JavaCallable("action")]`), it was instead attempting to register `ExportType.n_action()V`, because of the introduced `"n_" + method.Name` change. The "problem" is that `jnimarshalmethod-gen` was written in the context of Java.Interop, *not* .NET Android, and the existing `Java.Interop.Export-Tests` unit tests assume that there is no `n_`-prefix on native method declarations. There are two plausible solutions: update the unit tests to conform to existing .NET Android convention, and use an `n_` prefix on `native` method declarations. Or update `jcw-gen` so that when using `jcw-gen --codegen-target=JavaInterop1`, the `JavaCallableAttribute.Name` value is used for the `native` method declaration. Because @jonpryor is a glutton for punishment and "cleanliness", let's try the latter. `JavaCallableWrapperGenerator` already has a `.CodeGenerationTarget` property, How Hard Could It Be™? Turns out, harder than expected: `JavaCallableWrapperGenerator` was doing *lots* of work in its constructor, including the all important bit of populating `Signature` values, and the constructor runs *before* the `.CodeGenerationTarget` property is set. This is a somewhat straightforward fix: turn most of the `JavaCallableWrapperGenerator` constructor into a `Initialize()` method, and call `Initialize()` from the public methods. This creates a semantic change: some exceptions which were thrown by the constructor are now thrown from the public methods. We deem this as acceptable because all known uses of `JavaCallableWrapperGenerator` ~immediately call `.Generate()` after creating the instance, so the exception will still be thrown from a site near where it previously would have been thrown. The more annoying aspect is `Signature` initialization: we need to pass in `.CodeGenerationTarget`, which is Yet Another Constructor Parameter, and we already have A Lot™. Attempt to bring sanity to this mess by introducing a new `SignatureOptions` type to hold the `Signature` parameters. Update `jnimarshalmethod-gen` to undo the change which broke `Java.Interop.Export-Tests`. Update `tests/Java.Interop.Tools.JavaCallableWrappers-Tests` to add a test for `.CodeGenerationTarget==JavaInterop1`. Add `$(NoWarn)` to `Java.Interop.Tools.JavaCallableWrappers-Tests.csproj` in order to "work around" the warnings-as-errors: …/src/Java.Interop.NamingCustomAttributes/Java.Interop/ExportFieldAttribute.cs(19,63): error CA1019: Remove the property setter from Name or reduce its accessibility because it corresponds to positional argument name …/src/Java.Interop.NamingCustomAttributes/Android.Runtime/RegisterAttribute.cs(53,4): error CA1019: Remove the property setter from Name or reduce its accessibility because it corresponds to positional argument name …/src/Java.Interop.NamingCustomAttributes/Java.Interop/ExportFieldAttribute.cs(12,16): error CA1813: Avoid unsealed attributes … This is "weird"; the warnings/errors appear to come in because `Java.Interop.Tools.JavaCallableWrappers-Tests.csproj` now has: which appears to pull in `src/Java.Interop/.editorconfig`, which makes CA1019 and CA1813 errors. (insert massively confused face here. Like, wat?) --- .../Java.Interop/JavaCallableAttribute.cs | 3 +- ....Interop.Tools.JavaCallableWrappers.csproj | 2 +- .../JavaCallableWrapperGenerator.cs | 177 ++++++++++++++---- .../Java.Interop/JniTypeSignatureAttribute.cs | 2 + ...op.Tools.JavaCallableWrappers-Tests.csproj | 6 + .../JavaCallableWrapperGeneratorTests.cs | 60 +++++- .../SupportDeclarations.cs | 11 ++ tools/jnimarshalmethod-gen/App.cs | 3 +- 8 files changed, 218 insertions(+), 46 deletions(-) diff --git a/src/Java.Interop.Export/Java.Interop/JavaCallableAttribute.cs b/src/Java.Interop.Export/Java.Interop/JavaCallableAttribute.cs index ed3f44a2e..0e4d0a415 100644 --- a/src/Java.Interop.Export/Java.Interop/JavaCallableAttribute.cs +++ b/src/Java.Interop.Export/Java.Interop/JavaCallableAttribute.cs @@ -1,8 +1,9 @@ +#nullable enable using System; namespace Java.Interop { - [AttributeUsage (AttributeTargets.Method, AllowMultiple=false)] + [AttributeUsage (AttributeTargets.Constructor | AttributeTargets.Method, AllowMultiple=false)] public sealed class JavaCallableAttribute : Attribute { public JavaCallableAttribute () diff --git a/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers.csproj b/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers.csproj index 7e4704293..05f576b4d 100644 --- a/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers.csproj +++ b/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers.csproj @@ -3,7 +3,7 @@ netstandard2.0 false - 8.0 + 11.0 enable true true diff --git a/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers/JavaCallableWrapperGenerator.cs b/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers/JavaCallableWrapperGenerator.cs index 048596246..6d653fa72 100644 --- a/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers/JavaCallableWrapperGenerator.cs +++ b/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers/JavaCallableWrapperGenerator.cs @@ -160,6 +160,18 @@ void AddNestedTypes (TypeDefinition type) type.IsSubclassOf ("Android.Content.ContentProvider", cache))) Diagnostic.Error (4203, LookupSource (type), Localization.Resources.JavaCallableWrappers_XA4203, jniName); + this.outerType = outerType; + } + + bool initialized; + string? outerType; + + void Initialize () + { + if (initialized) + return; + initialized = true; + foreach (MethodDefinition minfo in type.Methods.Where (m => !m.IsConstructor)) { var baseRegisteredMethod = GetBaseRegisteredMethod (minfo); if (baseRegisteredMethod != null) @@ -298,7 +310,7 @@ 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 (ctor, eattr, cache)); + ctors.Add (new Signature (new (cache, CodeGenerationTarget, ctor, eattr))); curCtors.Add (ctor); return; } @@ -307,7 +319,10 @@ void AddConstructor (MethodDefinition ctor, TypeDefinition type, string? outerTy if (rattr != null) { if (ctors.Any (c => c.JniSignature == rattr.Signature)) return; - ctors.Add (new Signature (ctor, rattr, managedParameters, outerType, cache)); + ctors.Add (new Signature (new (cache, CodeGenerationTarget, ctor, rattr) { + ManagedParameters = managedParameters, + OuterType = outerType, + })); curCtors.Add (ctor); return; } @@ -328,12 +343,26 @@ void AddConstructor (MethodDefinition ctor, TypeDefinition type, string? outerTy } if (baseCtors.Any (m => m.Parameters.AreParametersCompatibleWith (ctor.Parameters, cache))) { - ctors.Add (new Signature (".ctor", jniSignature, "", managedParameters, outerType, null)); + ctors.Add (new Signature (new (cache, CodeGenerationTarget) { + RegisterName = ".ctor", + RegisterSignature = jniSignature, + RegisterConnector = "", + ManagedParameters = managedParameters, + OuterType = outerType, + IsDynamicallyRegistered = true, + })); curCtors.Add (ctor); return; } if (baseCtors.Any (m => !m.HasParameters)) { - ctors.Add (new Signature (".ctor", jniSignature, "", managedParameters, outerType, "")); + ctors.Add (new Signature (new (cache, CodeGenerationTarget) { + RegisterName = ".ctor", + RegisterSignature = jniSignature, + RegisterConnector = "", + ManagedParameters = managedParameters, + OuterType = outerType, + IsDynamicallyRegistered = true, + })); curCtors.Add (ctor); return; } @@ -489,7 +518,9 @@ void AddMethod (MethodDefinition? registeredMethod, MethodDefinition implemented Diagnostic.Error (4217, LookupSource (implementedMethod), Localization.Resources.JavaCallableWrappers_XA4217, attr.Name); bool shouldBeDynamicallyRegistered = methodClassifier?.ShouldBeDynamicallyRegistered (type, registeredMethod, implementedMethod, attr.OriginAttribute) ?? true; - var msig = new Signature (implementedMethod, attr, cache, shouldBeDynamicallyRegistered); + var msig = new Signature (new (cache, CodeGenerationTarget, implementedMethod, attr) { + IsDynamicallyRegistered = shouldBeDynamicallyRegistered, + }); if (!registeredMethod.IsConstructor && !methods.Any (m => m.Name == msig.Name && m.Params == msig.Params)) methods.Add (msig); } @@ -497,7 +528,7 @@ void AddMethod (MethodDefinition? registeredMethod, MethodDefinition implemented if (type.HasGenericParameters) Diagnostic.Error (4206, LookupSource (implementedMethod), Localization.Resources.JavaCallableWrappers_XA4206); - var msig = new Signature (implementedMethod, attr, cache); + var msig = new Signature (new (cache, CodeGenerationTarget, implementedMethod, attr)); if (!string.IsNullOrEmpty (attr.SuperArgumentsString)) { // Diagnostic.Warning (log, "Use of ExportAttribute.SuperArgumentsString property is invalid on methods"); } @@ -508,7 +539,7 @@ void AddMethod (MethodDefinition? registeredMethod, MethodDefinition implemented if (type.HasGenericParameters) Diagnostic.Error (4207, LookupSource (implementedMethod), Localization.Resources.JavaCallableWrappers_XA4207); - var msig = new Signature (implementedMethod, attr, cache); + var msig = new Signature (new (cache, CodeGenerationTarget, implementedMethod, attr)); if (!implementedMethod.IsConstructor && !methods.Any (m => m.Name == msig.Name && m.Params == msig.Params)) { methods.Add (msig); exported_fields.Add (new JavaFieldInfo (implementedMethod, attr.Name, cache)); @@ -557,6 +588,8 @@ string GetManagedParameters (MethodDefinition ctor, string? outerType) public void Generate (TextWriter writer) { + Initialize (); + if (!string.IsNullOrEmpty (package)) { writer.WriteLine ("package " + package + ";"); writer.WriteLine (); @@ -773,7 +806,7 @@ void GenerateRegisterType (TextWriter sw, JavaCallableWrapperGenerator self, str foreach (Signature method in self.methods) { if (method.IsDynamicallyRegistered) { - sw.Write ("\t\t\t\"", method.Method); + sw.Write ("\t\t\t\""); sw.Write (method.Method); sw.WriteLine ("\\n\" +"); } @@ -839,55 +872,111 @@ bool CannotRegisterInStaticConstructor (TypeDefinition type) return JavaNativeTypeManager.IsApplication (type, cache) || JavaNativeTypeManager.IsInstrumentation (type, cache); } - class Signature { + record struct SignatureOptions (IMetadataResolver Cache, JavaPeerStyle Style) { - public Signature (MethodDefinition method, RegisterAttribute register, IMetadataResolver cache, bool shouldBeDynamicallyRegistered = true) : this (method, register, null, null, cache, shouldBeDynamicallyRegistered) {} + public SignatureOptions (IMetadataResolver cache, JavaPeerStyle style, MethodDefinition method) + : this (cache, style) + { + Method = method; + IsStatic = method.IsStatic; + JavaAccess = JavaCallableWrapperGenerator.GetJavaAccess (method.Attributes & MethodAttributes.MemberAccessMask); + Annotations = JavaCallableWrapperGenerator.GetAnnotationsString ("\t", method.CustomAttributes, cache); + IsDynamicallyRegistered = true; + } - public Signature (MethodDefinition method, RegisterAttribute register, string? managedParameters, string? outerType, IMetadataResolver cache, bool shouldBeDynamicallyRegistered = true) - : this (register.Name, register.Signature, register.Connector, managedParameters, outerType, null) + public SignatureOptions (IMetadataResolver cache, JavaPeerStyle style, MethodDefinition method, RegisterAttribute register) + : this (cache, style, method) { - Annotations = JavaCallableWrapperGenerator.GetAnnotationsString ("\t", method.CustomAttributes, cache); - IsDynamicallyRegistered = shouldBeDynamicallyRegistered; + Register = register; } - public Signature (MethodDefinition method, ExportAttribute export, IMetadataResolver cache) - : this (method.Name, GetJniSignature (method, cache), "__export__", null, null, export.SuperArgumentsString) + public SignatureOptions (IMetadataResolver cache, JavaPeerStyle style, MethodDefinition method, ExportAttribute export) + : this (cache, style, method) { - IsExport = true; - IsStatic = method.IsStatic; - JavaAccess = JavaCallableWrapperGenerator.GetJavaAccess (method.Attributes & MethodAttributes.MemberAccessMask); - ThrownTypeNames = export.ThrownNames; - JavaNameOverride = export.Name; - Annotations = JavaCallableWrapperGenerator.GetAnnotationsString ("\t", method.CustomAttributes, cache); + IsExport = true; + SuperCall = export.SuperArgumentsString; + ThrownTypeNames = export.ThrownNames; + JavaNameOverride = export.Name; + if (style == JavaPeerStyle.JavaInterop1) { + RegisterName = export.Name; + } } - public Signature (MethodDefinition method, ExportFieldAttribute exportField, IMetadataResolver cache) - : this (method.Name, GetJniSignature (method, cache), "__export__", null, null, null) + public SignatureOptions (IMetadataResolver cache, JavaPeerStyle style, MethodDefinition method, ExportFieldAttribute exportField) + : this (cache, style, method) { + IsExport = true; + Annotations = null; + if (method.HasParameters) Diagnostic.Error (4205, JavaCallableWrapperGenerator.LookupSource (method), Localization.Resources.JavaCallableWrappers_XA4205); if (method.ReturnType.MetadataType == MetadataType.Void) Diagnostic.Error (4208, JavaCallableWrapperGenerator.LookupSource (method), Localization.Resources.JavaCallableWrappers_XA4208); - IsExport = true; - IsStatic = method.IsStatic; - JavaAccess = JavaCallableWrapperGenerator.GetJavaAccess (method.Attributes & MethodAttributes.MemberAccessMask); + } + - // annotations are processed within JavaFieldInfo, not the initializer method. So we don't generate them here. + public string? RegisterName; + public string? RegisterSignature; + public string? RegisterConnector; + public RegisterAttribute Register { + set { + RegisterName = value.Name; + RegisterSignature = value.Signature; + RegisterConnector = value.Connector; + } + } + public MethodDefinition Method { + set { + RegisterName = value.Name; + RegisterSignature = GetJniSignature (value, Cache); + RegisterConnector = "__export__"; + Annotations = JavaCallableWrapperGenerator.GetAnnotationsString ("\t", value.CustomAttributes, Cache); + } } + public bool IsDynamicallyRegistered; + public string? ManagedParameters; + public string? OuterType; + public string? SuperCall; + public string? Annotations; + public string? JavaAccess; + public string? JavaNameOverride; + public bool IsExport; + public bool IsStatic; + public string[]? ThrownTypeNames; + } - public Signature (string name, string? signature, string? connector, string? managedParameters, string? outerType, string? superCall) + class Signature { + + public Signature (SignatureOptions options) { - ManagedParameters = managedParameters; - JniSignature = signature ?? throw new ArgumentNullException ("`connector` cannot be null.", nameof (connector)); - Method = "n_" + name + ":" + JniSignature + ":" + connector; - Name = name; + if (options.RegisterName == null) { + throw new ArgumentNullException ("`RegisterName` cannot be null.", nameof (options.RegisterName)); + } + if (options.RegisterSignature == null) { + throw new ArgumentNullException ("`RegisterSignature` cannot be null.", nameof (options.RegisterSignature)); + } + Annotations = options.Annotations; + IsDynamicallyRegistered = options.IsDynamicallyRegistered; + IsExport = options.IsExport; + IsStatic = options.IsStatic; + JavaAccess = options.JavaAccess; + JavaNameOverride = options.JavaNameOverride; + JniSignature = options.RegisterSignature; + ManagedParameters = options.ManagedParameters; + Method = options.RegisterName + ":" + JniSignature + ":" + options.RegisterConnector; + Name = options.RegisterName; + ThrownTypeNames = options.ThrownTypeNames; + + if (options.Style == JavaPeerStyle.XAJavaInterop1) { + Method = "n_" + Method; + } var jnisig = JniSignature; int closer = jnisig.IndexOf (')'); string ret = jnisig.Substring (closer + 1); retval = JavaNativeTypeManager.Parse (ret)?.Type; string jniparms = jnisig.Substring (1, closer - 1); - if (string.IsNullOrEmpty (jniparms) && string.IsNullOrEmpty (superCall)) + if (string.IsNullOrEmpty (jniparms) && string.IsNullOrEmpty (options.SuperCall)) return; var parms = new StringBuilder (); var scall = new StringBuilder (); @@ -895,9 +984,9 @@ public Signature (string name, string? signature, string? connector, string? man bool first = true; int i = 0; foreach (JniTypeName jti in JavaNativeTypeManager.FromSignature (jniparms)) { - if (outerType != null) { - acall.Append (outerType).Append (".this"); - outerType = null; + if (options.OuterType != null) { + acall.Append (options.OuterType).Append (".this"); + options.OuterType = null; continue; } string? parmType = jti.Type; @@ -913,7 +1002,7 @@ public Signature (string name, string? signature, string? connector, string? man ++i; } this.parms = parms.ToString (); - this.call = superCall != null ? superCall : scall.ToString (); + this.call = options.SuperCall != null ? options.SuperCall : scall.ToString (); this.ActivateCall = acall.ToString (); } @@ -1050,13 +1139,21 @@ void GenerateMethod (Signature method, TextWriter sw) sw.Write (' '); if (method.IsStatic) sw.Write ("static "); + if (CodeGenerationTarget == JavaPeerStyle.JavaInterop1) { + sw.Write ("native "); + } sw.Write (method.Retval); sw.Write (' '); sw.Write (method.JavaName); sw.Write (" ("); sw.Write (method.Params); sw.Write (')'); - sw.WriteLine (method.ThrowsDeclaration); + sw.Write (method.ThrowsDeclaration); + if (CodeGenerationTarget == JavaPeerStyle.JavaInterop1) { + sw.WriteLine (";"); + return; + } + sw.WriteLine (); sw.WriteLine ("\t{"); #if MONODROID_TIMING sw.WriteLine ("\t\tandroid.util.Log.i(\"MonoDroid-Timing\", \"{0}.{1}: time: \"+java.lang.System.currentTimeMillis());", name, method.Name); @@ -1127,6 +1224,8 @@ StreamWriter OpenStream (string outputPath) /// public string GetDestinationPath (string outputPath) { + Initialize (); + var dir = package.Replace ('.', Path.DirectorySeparatorChar); return Path.Combine (outputPath, dir, name + ".java"); } diff --git a/src/Java.Interop/Java.Interop/JniTypeSignatureAttribute.cs b/src/Java.Interop/Java.Interop/JniTypeSignatureAttribute.cs index 4b5b6d8d9..a44b677e5 100644 --- a/src/Java.Interop/Java.Interop/JniTypeSignatureAttribute.cs +++ b/src/Java.Interop/Java.Interop/JniTypeSignatureAttribute.cs @@ -11,7 +11,9 @@ public sealed class JniTypeSignatureAttribute : Attribute { public JniTypeSignatureAttribute (string simpleReference) { +#if !JCW_ONLY_TYPE_NAMES JniRuntime.JniTypeManager.AssertSimpleReference (simpleReference, nameof (simpleReference)); +#endif // !JCW_ONLY_TYPE_NAMES SimpleReference = simpleReference; } diff --git a/tests/Java.Interop.Tools.JavaCallableWrappers-Tests/Java.Interop.Tools.JavaCallableWrappers-Tests.csproj b/tests/Java.Interop.Tools.JavaCallableWrappers-Tests/Java.Interop.Tools.JavaCallableWrappers-Tests.csproj index 1a4cea112..faaffa5ea 100644 --- a/tests/Java.Interop.Tools.JavaCallableWrappers-Tests/Java.Interop.Tools.JavaCallableWrappers-Tests.csproj +++ b/tests/Java.Interop.Tools.JavaCallableWrappers-Tests/Java.Interop.Tools.JavaCallableWrappers-Tests.csproj @@ -4,6 +4,7 @@ $(DotNetTargetFramework) false $(DefineConstants);HAVE_CECIL;JCW_ONLY_TYPE_NAMES + $(NoWarn);CA1019;CA1813 @@ -29,4 +30,9 @@ + + + + + diff --git a/tests/Java.Interop.Tools.JavaCallableWrappers-Tests/Java.Interop.Tools.JavaCallableWrappers/JavaCallableWrapperGeneratorTests.cs b/tests/Java.Interop.Tools.JavaCallableWrappers-Tests/Java.Interop.Tools.JavaCallableWrappers/JavaCallableWrapperGeneratorTests.cs index fa846a723..a7c23e746 100644 --- a/tests/Java.Interop.Tools.JavaCallableWrappers-Tests/Java.Interop.Tools.JavaCallableWrappers/JavaCallableWrapperGeneratorTests.cs +++ b/tests/Java.Interop.Tools.JavaCallableWrappers-Tests/Java.Interop.Tools.JavaCallableWrappers/JavaCallableWrapperGeneratorTests.cs @@ -37,7 +37,8 @@ public void KotlinInvalidImplRegisterName () // Contains invalid [Register] name of "foo-impl" var td = SupportDeclarations.GetTypeDefinition (typeof (KotlinInvalidImplRegisterName)); - var e = Assert.Throws (() => new JavaCallableWrapperGenerator (td, logger, cache: null)); + var g = new JavaCallableWrapperGenerator (td, logger, cache: null); + var e = Assert.Throws (() => g.Generate (TextWriter.Null)); Assert.AreEqual (4217, e.Code); } @@ -48,7 +49,8 @@ public void KotlinInvalidHashRegisterName () // Contains invalid [Register] name of "foo-f8k2a13" var td = SupportDeclarations.GetTypeDefinition (typeof (KotlinInvalidHashRegisterName)); - var e = Assert.Throws (() => new JavaCallableWrapperGenerator (td, logger, cache: null)); + var g = new JavaCallableWrapperGenerator (td, logger, cache: null); + var e = Assert.Throws (() => g.Generate (TextWriter.Null)); Assert.AreEqual (4217, e.Code); } @@ -102,13 +104,14 @@ public void monodroidClearReferences () Assert.AreEqual (expected, actual); } - static string Generate (Type type, string applicationJavaClass = null, string monoRuntimeInit = null) + static string Generate (Type type, string applicationJavaClass = null, string monoRuntimeInit = null, JavaPeerStyle style = JavaPeerStyle.XAJavaInterop1) { var td = SupportDeclarations.GetTypeDefinition (type); var g = new JavaCallableWrapperGenerator (td, log: null, cache: null) { ApplicationJavaClass = applicationJavaClass, GenerateOnCreateOverrides = true, MonoRuntimeInitialization = monoRuntimeInit, + CodeGenerationTarget = style, }; var o = new StringWriter (); var dir = Path.GetDirectoryName (typeof (JavaCallableWrapperGeneratorTests).Assembly.Location); @@ -618,6 +621,57 @@ public void monodroidClearReferences () refList.clear (); }} }} +"; + Assert.AreEqual (expected, actual); + } + + [Test] + public void GenerateJavaInteropExample () + { + var actual = Generate (typeof (JavaInteropExample), style: JavaPeerStyle.JavaInterop1); + var expected = @"package register; + + +public class JavaInteropExample + extends java.lang.Object + implements + com.xamarin.java_interop.GCUserPeerable +{ +/** @hide */ + public static final String __md_methods; + static { + __md_methods = + ""example:()V:__export__\n"" + + """"; + com.xamarin.java_interop.ManagedPeer.registerNativeMembers (JavaInteropExample.class, ""Xamarin.Android.ToolsTests.JavaInteropExample, Java.Interop.Tools.JavaCallableWrappers-Tests"", __md_methods); + } + + + public JavaInteropExample () + { + 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[] { }); + } + } + + + public native void example (); + + private java.util.ArrayList refList; + public void jiAddManagedReference (java.lang.Object obj) + { + if (refList == null) + refList = new java.util.ArrayList (); + refList.add (obj); + } + + public void jiClearManagedReferences () + { + if (refList != null) + refList.clear (); + } +} "; Assert.AreEqual (expected, actual); } diff --git a/tests/Java.Interop.Tools.JavaCallableWrappers-Tests/Java.Interop.Tools.JavaCallableWrappers/SupportDeclarations.cs b/tests/Java.Interop.Tools.JavaCallableWrappers-Tests/Java.Interop.Tools.JavaCallableWrappers/SupportDeclarations.cs index de99b77ec..63d671c20 100644 --- a/tests/Java.Interop.Tools.JavaCallableWrappers-Tests/Java.Interop.Tools.JavaCallableWrappers/SupportDeclarations.cs +++ b/tests/Java.Interop.Tools.JavaCallableWrappers-Tests/Java.Interop.Tools.JavaCallableWrappers/SupportDeclarations.cs @@ -323,4 +323,15 @@ public ExportsThrowsConstructors (int value) { } [Export (Throws = new Type [0])] public ExportsThrowsConstructors (string value) { } } + + [JniTypeSignature ("register/JavaInteropExample")] + class JavaInteropExample : Java.Lang.Object { + + + [JavaCallable] + public JavaInteropExample () {} + + [JavaCallable ("example")] + public void Example () {} + } } diff --git a/tools/jnimarshalmethod-gen/App.cs b/tools/jnimarshalmethod-gen/App.cs index c6e9bed91..3ce12a778 100644 --- a/tools/jnimarshalmethod-gen/App.cs +++ b/tools/jnimarshalmethod-gen/App.cs @@ -560,8 +560,7 @@ void CreateMarshalMethodAssembly (string path) signature = export?.Signature ?? builder.GetJniMethodSignature (method); - // 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)); + registrations.Add (new ExpressionMethodRegistration (name, signature, mmDef)); addedMethods.Add (methodName); } From 902fe2875311b3d5ba863c976d9130b8c6c03e12 Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Mon, 6 Nov 2023 22:47:15 -0500 Subject: [PATCH 12/44] Remove more `n_`s?! 9027591c41232c82a582f8ab16ddaa783795582c tried to fix Java.Interop.Export-Tests, and in the process broke Java.Base-Tests! A total of 1 test files matched the specified pattern. Failed InterfaceInvokerMethod [100 ms] Error Message: Java.Interop.JavaException : 'void example.MyIntConsumer.accept(int)' Stack Trace: at Java.Interop.JniEnvironment.InstanceMethods.CallVoidMethod(JniObjectReference instance, JniMethodInfo method, JniArgumentValue* args) in /Users/runner/work/1/s/src/Java.Interop/obj/Release/net7.0/JniEnvironment.g.cs:line 20370 at Java.Interop.JniPeerMembers.JniInstanceMethods.InvokeAbstractVoidMethod(String encodedMember, IJavaPeerable self, JniArgumentValue* parameters) in /Users/runner/work/1/s/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods_Invoke.cs:line 47 at Java.Lang.IRunnableInvoker.Run() in /Users/runner/work/1/s/src/Java.Base/obj/Release-net7.0/mcw/Java.Lang.IRunnable.cs:line 34 at Java.BaseTests.JavaToManagedTests.InterfaceInvokerMethod() in /Users/runner/work/1/s/tests/Java.Base-Tests/Java.Base/JavaToManagedTests.cs:line 28 at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor) at System.Reflection.MethodInvoker.Invoke(Object obj, IntPtr* args, BindingFlags invokeAttr) --- End of managed Java.Interop.JavaException stack trace --- java.lang.UnsatisfiedLinkError: 'void example.MyIntConsumer.accept(int)' at example.MyIntConsumer.accept(Native Method) at com.microsoft.java_base_tests.Invoker$1.run(Invoker.java:15) The problem? That pesky `n_`! Or rather, the lack thereof this time. The issue is that `JreTypeManager`/etc. and `jcw-gen` need to be consistent. 9027591c41232c82a582f8ab16ddaa783795582c altered the consistency, breaking Java.Base tests. Commit this to verify that things work on CI, not just locally. Assuming it works on CI, next step will be to largely revert 9027591c41232c82a582f8ab16ddaa783795582c, and then fix `Java.Interop.Export-Tests` to use an `n_` prefix on all `native` method declarations. That bit is hand-written; it can change. Keeping the tooling consistent is more important. --- src/Java.Runtime.Environment/Java.Interop/JreTypeManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Java.Runtime.Environment/Java.Interop/JreTypeManager.cs b/src/Java.Runtime.Environment/Java.Interop/JreTypeManager.cs index 7f3e0763c..dc4f7b9ec 100644 --- a/src/Java.Runtime.Environment/Java.Interop/JreTypeManager.cs +++ b/src/Java.Runtime.Environment/Java.Interop/JreTypeManager.cs @@ -75,7 +75,7 @@ static void AddJniMethod (JniMethodMap toRegister, MethodInfo declaringMethod, M if (signature == null || string.IsNullOrEmpty (signature.MemberName)) { return; } - toRegister [("n_" + signature.MemberName, signature.MemberSignature)] = targetMethod ?? declaringMethod; + toRegister [(signature.MemberName, signature.MemberSignature)] = targetMethod ?? declaringMethod; } static void AddClassMethods (JniMethodMap toRegister, Type type) From 03921691a014168005fe82881d527c1ba3de5280 Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Tue, 7 Nov 2023 08:06:30 -0500 Subject: [PATCH 13/44] Enable activation Invoking the Java-side constructor should invoke the C#-side constructor too! And now it does! Hello from Java! C# init() Hello from .NET NativeAOT! String returned to Java: Hello from .NET NativeAOT! C# RegisterNativeMembers(JniType(Name='example/ManagedType' PeerReference=0x7f9392713098/G), "Example.ManagedType, Hello-NativeAOTFromJNI", "getString:()Ljava/lang/String;:__export__ ") # jonp: called `Example.ManagedType/__<$>_jni_marshal_methods.__RegisterNativeMembers()` w/ 1 methods to register. # jonp: ActivatePeer( False, 0x700001bc6a70/I, Void .ctor(), System.Object[]) mt.getString()=Hello from C#, via Java.Interop! --- .../Hello-NativeAOTFromJNI.csproj | 4 +++ .../JavaInteropRuntime.cs | 1 + samples/Hello-NativeAOTFromJNI/ManagedType.cs | 7 +---- .../NativeAotValueManager.cs | 26 +++++++++++++++++++ samples/Hello-NativeAOTFromJNI/README.md | 23 +++++++++++----- .../Java.Interop/ManagedValueManager.cs | 10 +++---- 6 files changed, 54 insertions(+), 17 deletions(-) create mode 100644 samples/Hello-NativeAOTFromJNI/NativeAotValueManager.cs diff --git a/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj b/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj index 0c42785d4..1909cd272 100644 --- a/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj +++ b/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj @@ -26,6 +26,10 @@ + + + + diff --git a/samples/Hello-NativeAOTFromJNI/JavaInteropRuntime.cs b/samples/Hello-NativeAOTFromJNI/JavaInteropRuntime.cs index a59fa0a77..607bd73fb 100644 --- a/samples/Hello-NativeAOTFromJNI/JavaInteropRuntime.cs +++ b/samples/Hello-NativeAOTFromJNI/JavaInteropRuntime.cs @@ -28,6 +28,7 @@ static void init (IntPtr jnienv, IntPtr klass) var options = new JreRuntimeOptions { EnvironmentPointer = jnienv, TypeManager = new NativeAotTypeManager (), + ValueManager = new NativeAotValueManager (), }; runtime = options.CreateJreVM (); } diff --git a/samples/Hello-NativeAOTFromJNI/ManagedType.cs b/samples/Hello-NativeAOTFromJNI/ManagedType.cs index 374c03fdc..c5224a40f 100644 --- a/samples/Hello-NativeAOTFromJNI/ManagedType.cs +++ b/samples/Hello-NativeAOTFromJNI/ManagedType.cs @@ -5,16 +5,11 @@ namespace Example; [JniTypeSignature ("example/ManagedType")] class ManagedType : Java.Lang.Object { + [JavaCallable] public ManagedType () { } - // TODO: remove this - public ManagedType (ref JniObjectReference reference, JniObjectReferenceOptions options) - : base (ref reference, options) - { - } - [JavaCallable ("getString")] public Java.Lang.String GetString () { diff --git a/samples/Hello-NativeAOTFromJNI/NativeAotValueManager.cs b/samples/Hello-NativeAOTFromJNI/NativeAotValueManager.cs new file mode 100644 index 000000000..0478a1890 --- /dev/null +++ b/samples/Hello-NativeAOTFromJNI/NativeAotValueManager.cs @@ -0,0 +1,26 @@ +using System.Reflection; + +using Java.Interop; + +namespace Hello_NativeAOTFromJNI; + +class NativeAotValueManager : ManagedValueManager { + + // Can't use default version, because: + // System.ArgumentException: Instance property 'DeclaringType' is not defined for type 'System.Reflection.MemberInfo' (Parameter 'propertyName') + // at System.Linq.Expressions.Expression.Property(Expression, String) + 0xaa + // at Java.Interop.MarshalMemberBuilder.CreateConstructActivationPeerExpression(ConstructorInfo) + 0x209 + // at Java.Interop.JniRuntime.JniMarshalMemberBuilder.CreateConstructActivationPeerFunc(ConstructorInfo) + 0x13 + // Do it "manually" + public override void ActivatePeer (IJavaPeerable? self, JniObjectReference reference, ConstructorInfo cinfo, object?[]? argumentValues) + { + var declType = cinfo.DeclaringType ?? throw new NotSupportedException ("Do not know the type to create!"); + +#pragma warning disable IL2072 + self = (IJavaPeerable) System.Runtime.CompilerServices.RuntimeHelpers.GetUninitializedObject (declType); +#pragma warning restore IL2072 + self.SetPeerReference (reference); + + cinfo.Invoke (self, argumentValues); + } +} diff --git a/samples/Hello-NativeAOTFromJNI/README.md b/samples/Hello-NativeAOTFromJNI/README.md index 86ebe6e53..9aeea293d 100644 --- a/samples/Hello-NativeAOTFromJNI/README.md +++ b/samples/Hello-NativeAOTFromJNI/README.md @@ -25,17 +25,17 @@ Release configuration: Once Java.Interop itself is built, you can build the sample: ```sh -% dotnet build -c Release -% dotnet publish -r osx-x64 +% dotnet publish -c Release -r osx-x64 ``` The resulting native library contains the desired symbols: ```sh % nm bin/Release/osx-x64/publish/Hello-NativeAOTFromJNI.dylib | grep ' S ' -00000000000cb710 S _JNI_OnLoad -00000000000cb820 S _JNI_OnUnload -00000000000cb840 S _Java_com_microsoft_hello_1from_1jni_NativeAOTInit_sayHello +000000000016f5a0 S _JNI_OnLoad +000000000016f5d0 S _JNI_OnUnload +000000000016f2f0 S _Java_com_microsoft_hello_1from_1jni_App_sayHello +000000000016f620 S _Java_com_microsoft_java_1interop_JavaInteropRuntime_init ``` Use the `RunJavaSample` target to run Java, which will run @@ -45,18 +45,29 @@ NativeAOT-generated `libHello-NativeAOTFromJNI.dylib` to be run: ```sh % dotnet build -c Release -r osx-x64 -t:RunJavaSample -v m --nologo --no-restore Hello from Java! + C# init() Hello from .NET NativeAOT! + String returned to Java: Hello from .NET NativeAOT! + C# RegisterNativeMembers(JniType(Name='example/ManagedType' PeerReference=0x7fd0a00072d8/G), "Example.ManagedType, Hello-NativeAOTFromJNI", "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! Build succeeded. 0 Warning(s) 0 Error(s) -Time Elapsed 00:00:00.83 +Time Elapsed 00:00:00.73 % (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=0x7fa822114598/G), "Example.ManagedType, Hello-NativeAOTFromJNI", "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! ``` Note the use of `(cd …; java …)` so that `libHello-NativeAOTFromJNI.dylib` is diff --git a/src/Java.Runtime.Environment/Java.Interop/ManagedValueManager.cs b/src/Java.Runtime.Environment/Java.Interop/ManagedValueManager.cs index 7ff510a7a..2a60914c8 100644 --- a/src/Java.Runtime.Environment/Java.Interop/ManagedValueManager.cs +++ b/src/Java.Runtime.Environment/Java.Interop/ManagedValueManager.cs @@ -22,7 +22,7 @@ public override void WaitForGCBridgeProcessing () public override void CollectPeers () { if (RegisteredInstances == null) - throw new ObjectDisposedException (nameof (MonoRuntimeValueManager)); + throw new ObjectDisposedException (nameof (ManagedValueManager)); var peers = new List (); @@ -51,7 +51,7 @@ public override void CollectPeers () public override void AddPeer (IJavaPeerable value) { if (RegisteredInstances == null) - throw new ObjectDisposedException (nameof (MonoRuntimeValueManager)); + throw new ObjectDisposedException (nameof (ManagedValueManager)); var r = value.PeerReference; if (!r.IsValid) @@ -116,7 +116,7 @@ void WarnNotReplacing (int key, IJavaPeerable ignoreValue, IJavaPeerable keepVal public override IJavaPeerable? PeekPeer (JniObjectReference reference) { if (RegisteredInstances == null) - throw new ObjectDisposedException (nameof (MonoRuntimeValueManager)); + throw new ObjectDisposedException (nameof (ManagedValueManager)); if (!reference.IsValid) return null; @@ -142,7 +142,7 @@ void WarnNotReplacing (int key, IJavaPeerable ignoreValue, IJavaPeerable keepVal public override void RemovePeer (IJavaPeerable value) { if (RegisteredInstances == null) - throw new ObjectDisposedException (nameof (MonoRuntimeValueManager)); + throw new ObjectDisposedException (nameof (ManagedValueManager)); if (value == null) throw new ArgumentNullException (nameof (value)); @@ -221,7 +221,7 @@ public override void ActivatePeer (IJavaPeerable? self, JniObjectReference refer public override List GetSurfacedPeers () { if (RegisteredInstances == null) - throw new ObjectDisposedException (nameof (MonoRuntimeValueManager)); + throw new ObjectDisposedException (nameof (ManagedValueManager)); lock (RegisteredInstances) { var peers = new List (RegisteredInstances.Count); From 93a901a8207ded8b8ec124c64ad76d7a7a2d3779 Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Tue, 7 Nov 2023 16:15:13 -0500 Subject: [PATCH 14/44] Allow marking constructors as callable from Java. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 9027591c 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! --- samples/Hello-NativeAOTFromJNI/ManagedType.cs | 2 +- .../NativeAotTypeManager.cs | 2 +- samples/Hello-NativeAOTFromJNI/README.md | 188 ++++++++++++++++++ .../Java.Interop/JavaCallableAttribute.cs | 2 +- .../JavaCallableConstructorAttribute.cs | 16 ++ .../JavaCallableWrapperGenerator.cs | 19 +- ...op.Tools.JavaCallableWrappers-Tests.csproj | 1 + .../JavaCallableWrapperGeneratorTests.cs | 10 +- .../SupportDeclarations.cs | 4 +- 9 files changed, 232 insertions(+), 12 deletions(-) create mode 100644 src/Java.Interop.Export/Java.Interop/JavaCallableConstructorAttribute.cs diff --git a/samples/Hello-NativeAOTFromJNI/ManagedType.cs b/samples/Hello-NativeAOTFromJNI/ManagedType.cs index c5224a40f..58e175cb5 100644 --- a/samples/Hello-NativeAOTFromJNI/ManagedType.cs +++ b/samples/Hello-NativeAOTFromJNI/ManagedType.cs @@ -5,7 +5,7 @@ namespace Example; [JniTypeSignature ("example/ManagedType")] class ManagedType : Java.Lang.Object { - [JavaCallable] + [JavaCallableConstructor] public ManagedType () { } diff --git a/samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs b/samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs index 2f2dda3de..4719809e6 100644 --- a/samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs +++ b/samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs @@ -6,7 +6,7 @@ class NativeAotTypeManager : JniRuntime.JniTypeManager { #pragma warning disable IL2026 Dictionary 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 diff --git a/samples/Hello-NativeAOTFromJNI/README.md b/samples/Hello-NativeAOTFromJNI/README.md index 9aeea293d..480e90b88 100644 --- a/samples/Hello-NativeAOTFromJNI/README.md +++ b/samples/Hello-NativeAOTFromJNI/README.md @@ -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.(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` 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 diff --git a/src/Java.Interop.Export/Java.Interop/JavaCallableAttribute.cs b/src/Java.Interop.Export/Java.Interop/JavaCallableAttribute.cs index 0e4d0a415..8b8c7d0f6 100644 --- a/src/Java.Interop.Export/Java.Interop/JavaCallableAttribute.cs +++ b/src/Java.Interop.Export/Java.Interop/JavaCallableAttribute.cs @@ -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 () diff --git a/src/Java.Interop.Export/Java.Interop/JavaCallableConstructorAttribute.cs b/src/Java.Interop.Export/Java.Interop/JavaCallableConstructorAttribute.cs new file mode 100644 index 000000000..23370d4cb --- /dev/null +++ b/src/Java.Interop.Export/Java.Interop/JavaCallableConstructorAttribute.cs @@ -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;} + } +} diff --git a/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers/JavaCallableWrapperGenerator.cs b/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers/JavaCallableWrapperGenerator.cs index 6d653fa72..4102f4ef1 100644 --- a/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers/JavaCallableWrapperGenerator.cs +++ b/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers/JavaCallableWrapperGenerator.cs @@ -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; @@ -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; } @@ -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); @@ -486,7 +500,8 @@ static IEnumerable GetMethodRegistrationAttributes (Mono.Ceci IEnumerable GetExportAttributes (IMemberDefinition p) { return GetAttributes (p, a => ToExportAttribute (a, p)) - .Concat (GetAttributes (p, "Java.Interop.JavaCallableAttribute", a => ToExportAttributeFromJavaCallableAttribute (a, p))); + .Concat (GetAttributes (p, "Java.Interop.JavaCallableAttribute", a => ToExportAttributeFromJavaCallableAttribute (a, p))) + .Concat (GetAttributes (p, "Java.Interop.JavaCallableConstructorAttribute", a => ToExportAttributeFromJavaCallableConstructorAttribute (a, p))); } static IEnumerable GetExportFieldAttributes (Mono.Cecil.ICustomAttributeProvider p) diff --git a/tests/Java.Interop.Tools.JavaCallableWrappers-Tests/Java.Interop.Tools.JavaCallableWrappers-Tests.csproj b/tests/Java.Interop.Tools.JavaCallableWrappers-Tests/Java.Interop.Tools.JavaCallableWrappers-Tests.csproj index faaffa5ea..fc245d4d6 100644 --- a/tests/Java.Interop.Tools.JavaCallableWrappers-Tests/Java.Interop.Tools.JavaCallableWrappers-Tests.csproj +++ b/tests/Java.Interop.Tools.JavaCallableWrappers-Tests/Java.Interop.Tools.JavaCallableWrappers-Tests.csproj @@ -33,6 +33,7 @@ + diff --git a/tests/Java.Interop.Tools.JavaCallableWrappers-Tests/Java.Interop.Tools.JavaCallableWrappers/JavaCallableWrapperGeneratorTests.cs b/tests/Java.Interop.Tools.JavaCallableWrappers-Tests/Java.Interop.Tools.JavaCallableWrappers/JavaCallableWrapperGeneratorTests.cs index a7c23e746..9858698e3 100644 --- a/tests/Java.Interop.Tools.JavaCallableWrappers-Tests/Java.Interop.Tools.JavaCallableWrappers/JavaCallableWrapperGeneratorTests.cs +++ b/tests/Java.Interop.Tools.JavaCallableWrappers-Tests/Java.Interop.Tools.JavaCallableWrappers/JavaCallableWrapperGeneratorTests.cs @@ -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 }); } } @@ -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 }); } } @@ -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 }); } } @@ -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 }); } } diff --git a/tests/Java.Interop.Tools.JavaCallableWrappers-Tests/Java.Interop.Tools.JavaCallableWrappers/SupportDeclarations.cs b/tests/Java.Interop.Tools.JavaCallableWrappers-Tests/Java.Interop.Tools.JavaCallableWrappers/SupportDeclarations.cs index 63d671c20..0a56b225c 100644 --- a/tests/Java.Interop.Tools.JavaCallableWrappers-Tests/Java.Interop.Tools.JavaCallableWrappers/SupportDeclarations.cs +++ b/tests/Java.Interop.Tools.JavaCallableWrappers-Tests/Java.Interop.Tools.JavaCallableWrappers/SupportDeclarations.cs @@ -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 () {} From a50457be6517921f77f4cf3307d09c25588803a1 Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Wed, 8 Nov 2023 17:25:49 -0500 Subject: [PATCH 15/44] Partially revert 9027591c41232c82a582f8ab16ddaa783795582c Remember 902fe2875311b3d5ba863c976d9130b8c6c03e12? > Assuming it works on CI, next step will be to largely revert > 9027591c41232c82a582f8ab16ddaa783795582c, and then fix > `Java.Interop.Export-Tests` to use an `n_` prefix on all `native` > method declarations. That bit is hand-written; it can change. > Keeping the tooling consistent is more important. Let's Do It! Revert most of the `JavaCallableWrapperGenerator` bits of 9027591c41232c82a582f8ab16ddaa783795582c. Update the `JavaInterop1` JCW output to conform, using `n_`-prefixed native method declarations. *Retain the constructor signature fix* in 93a901a8.. Revert the `JreTypeManager` change in 902fe287. Update `jnimarshalmethod-gen` to register `n_`-prefixed names. Update `Java.Interop.Export-Tests` so that native method declarations now have `n_` prefixes, so that all is in sync. --- .../Java.Interop/MarshalMemberBuilder.cs | 2 +- .../JavaCallableWrapperGenerator.cs | 180 ++++-------------- .../Java.Interop/JreTypeManager.cs | 2 +- .../Java.Interop/MarshalMemberBuilderTest.cs | 6 +- .../xamarin/interop/export/ExportType.java | 75 ++++++-- .../JavaCallableWrapperGeneratorTests.cs | 15 +- tools/jnimarshalmethod-gen/App.cs | 2 +- 7 files changed, 119 insertions(+), 163 deletions(-) diff --git a/src/Java.Interop.Export/Java.Interop/MarshalMemberBuilder.cs b/src/Java.Interop.Export/Java.Interop/MarshalMemberBuilder.cs index b4a97c94d..6b3e4458b 100644 --- a/src/Java.Interop.Export/Java.Interop/MarshalMemberBuilder.cs +++ b/src/Java.Interop.Export/Java.Interop/MarshalMemberBuilder.cs @@ -68,7 +68,7 @@ public JniNativeMethodRegistration CreateMarshalToManagedMethodRegistration (Jav string GetJniMethodName (JavaCallableAttribute export, MethodInfo method) { - return export.Name ?? "n_" + method.Name; + return "n_" + method.Name; } public string GetJniMethodSignature (JavaCallableAttribute export, MethodInfo method) diff --git a/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers/JavaCallableWrapperGenerator.cs b/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers/JavaCallableWrapperGenerator.cs index 4102f4ef1..178fc9d9e 100644 --- a/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers/JavaCallableWrapperGenerator.cs +++ b/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers/JavaCallableWrapperGenerator.cs @@ -160,18 +160,6 @@ void AddNestedTypes (TypeDefinition type) type.IsSubclassOf ("Android.Content.ContentProvider", cache))) Diagnostic.Error (4203, LookupSource (type), Localization.Resources.JavaCallableWrappers_XA4203, jniName); - this.outerType = outerType; - } - - bool initialized; - string? outerType; - - void Initialize () - { - if (initialized) - return; - initialized = true; - foreach (MethodDefinition minfo in type.Methods.Where (m => !m.IsConstructor)) { var baseRegisteredMethod = GetBaseRegisteredMethod (minfo); if (baseRegisteredMethod != null) @@ -313,9 +301,7 @@ 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) { - ManagedParameters = managedParameters, - })); + ctors.Add (new Signature (ctor, eattr, managedParameters, cache)); curCtors.Add (ctor); return; } @@ -324,10 +310,7 @@ void AddConstructor (MethodDefinition ctor, TypeDefinition type, string? outerTy if (rattr != null) { if (ctors.Any (c => c.JniSignature == rattr.Signature)) return; - ctors.Add (new Signature (new (cache, CodeGenerationTarget, ctor, rattr) { - ManagedParameters = managedParameters, - OuterType = outerType, - })); + ctors.Add (new Signature (ctor, rattr, managedParameters, outerType, cache)); curCtors.Add (ctor); return; } @@ -348,26 +331,12 @@ void AddConstructor (MethodDefinition ctor, TypeDefinition type, string? outerTy } if (baseCtors.Any (m => m.Parameters.AreParametersCompatibleWith (ctor.Parameters, cache))) { - ctors.Add (new Signature (new (cache, CodeGenerationTarget) { - RegisterName = ".ctor", - RegisterSignature = jniSignature, - RegisterConnector = "", - ManagedParameters = managedParameters, - OuterType = outerType, - IsDynamicallyRegistered = true, - })); + ctors.Add (new Signature (".ctor", jniSignature, "", managedParameters, outerType, null)); curCtors.Add (ctor); return; } if (baseCtors.Any (m => !m.HasParameters)) { - ctors.Add (new Signature (new (cache, CodeGenerationTarget) { - RegisterName = ".ctor", - RegisterSignature = jniSignature, - RegisterConnector = "", - ManagedParameters = managedParameters, - OuterType = outerType, - IsDynamicallyRegistered = true, - })); + ctors.Add (new Signature (".ctor", jniSignature, "", managedParameters, outerType, "")); curCtors.Add (ctor); return; } @@ -533,9 +502,7 @@ void AddMethod (MethodDefinition? registeredMethod, MethodDefinition implemented Diagnostic.Error (4217, LookupSource (implementedMethod), Localization.Resources.JavaCallableWrappers_XA4217, attr.Name); bool shouldBeDynamicallyRegistered = methodClassifier?.ShouldBeDynamicallyRegistered (type, registeredMethod, implementedMethod, attr.OriginAttribute) ?? true; - var msig = new Signature (new (cache, CodeGenerationTarget, implementedMethod, attr) { - IsDynamicallyRegistered = shouldBeDynamicallyRegistered, - }); + var msig = new Signature (implementedMethod, attr, cache, shouldBeDynamicallyRegistered); if (!registeredMethod.IsConstructor && !methods.Any (m => m.Name == msig.Name && m.Params == msig.Params)) methods.Add (msig); } @@ -543,7 +510,7 @@ void AddMethod (MethodDefinition? registeredMethod, MethodDefinition implemented if (type.HasGenericParameters) Diagnostic.Error (4206, LookupSource (implementedMethod), Localization.Resources.JavaCallableWrappers_XA4206); - var msig = new Signature (new (cache, CodeGenerationTarget, implementedMethod, attr)); + var msig = new Signature (implementedMethod, attr, null, cache); if (!string.IsNullOrEmpty (attr.SuperArgumentsString)) { // Diagnostic.Warning (log, "Use of ExportAttribute.SuperArgumentsString property is invalid on methods"); } @@ -554,7 +521,7 @@ void AddMethod (MethodDefinition? registeredMethod, MethodDefinition implemented if (type.HasGenericParameters) Diagnostic.Error (4207, LookupSource (implementedMethod), Localization.Resources.JavaCallableWrappers_XA4207); - var msig = new Signature (new (cache, CodeGenerationTarget, implementedMethod, attr)); + var msig = new Signature (implementedMethod, attr, cache); if (!implementedMethod.IsConstructor && !methods.Any (m => m.Name == msig.Name && m.Params == msig.Params)) { methods.Add (msig); exported_fields.Add (new JavaFieldInfo (implementedMethod, attr.Name, cache)); @@ -603,8 +570,6 @@ string GetManagedParameters (MethodDefinition ctor, string? outerType) public void Generate (TextWriter writer) { - Initialize (); - if (!string.IsNullOrEmpty (package)) { writer.WriteLine ("package " + package + ";"); writer.WriteLine (); @@ -821,7 +786,7 @@ void GenerateRegisterType (TextWriter sw, JavaCallableWrapperGenerator self, str foreach (Signature method in self.methods) { if (method.IsDynamicallyRegistered) { - sw.Write ("\t\t\t\""); + sw.Write ("\t\t\t\"", method.Method); sw.Write (method.Method); sw.WriteLine ("\\n\" +"); } @@ -887,111 +852,56 @@ bool CannotRegisterInStaticConstructor (TypeDefinition type) return JavaNativeTypeManager.IsApplication (type, cache) || JavaNativeTypeManager.IsInstrumentation (type, cache); } - record struct SignatureOptions (IMetadataResolver Cache, JavaPeerStyle Style) { + class Signature { - public SignatureOptions (IMetadataResolver cache, JavaPeerStyle style, MethodDefinition method) - : this (cache, style) - { - Method = method; - IsStatic = method.IsStatic; - JavaAccess = JavaCallableWrapperGenerator.GetJavaAccess (method.Attributes & MethodAttributes.MemberAccessMask); - Annotations = JavaCallableWrapperGenerator.GetAnnotationsString ("\t", method.CustomAttributes, cache); - IsDynamicallyRegistered = true; - } + public Signature (MethodDefinition method, RegisterAttribute register, IMetadataResolver cache, bool shouldBeDynamicallyRegistered = true) : this (method, register, null, null, cache, shouldBeDynamicallyRegistered) {} - public SignatureOptions (IMetadataResolver cache, JavaPeerStyle style, MethodDefinition method, RegisterAttribute register) - : this (cache, style, method) + public Signature (MethodDefinition method, RegisterAttribute register, string? managedParameters, string? outerType, IMetadataResolver cache, bool shouldBeDynamicallyRegistered = true) + : this (register.Name, register.Signature, register.Connector, managedParameters, outerType, null) { - Register = register; + Annotations = JavaCallableWrapperGenerator.GetAnnotationsString ("\t", method.CustomAttributes, cache); + IsDynamicallyRegistered = shouldBeDynamicallyRegistered; } - public SignatureOptions (IMetadataResolver cache, JavaPeerStyle style, MethodDefinition method, ExportAttribute export) - : this (cache, style, method) + public Signature (MethodDefinition method, ExportAttribute export, string? managedParameters, IMetadataResolver cache) + : this (method.Name, GetJniSignature (method, cache), "__export__", null, null, export.SuperArgumentsString) { - IsExport = true; - SuperCall = export.SuperArgumentsString; - ThrownTypeNames = export.ThrownNames; - JavaNameOverride = export.Name; - if (style == JavaPeerStyle.JavaInterop1) { - RegisterName = export.Name; - } + IsExport = true; + IsStatic = method.IsStatic; + JavaAccess = JavaCallableWrapperGenerator.GetJavaAccess (method.Attributes & MethodAttributes.MemberAccessMask); + ThrownTypeNames = export.ThrownNames; + JavaNameOverride = export.Name; + ManagedParameters = managedParameters; + Annotations = JavaCallableWrapperGenerator.GetAnnotationsString ("\t", method.CustomAttributes, cache); } - public SignatureOptions (IMetadataResolver cache, JavaPeerStyle style, MethodDefinition method, ExportFieldAttribute exportField) - : this (cache, style, method) + public Signature (MethodDefinition method, ExportFieldAttribute exportField, IMetadataResolver cache) + : this (method.Name, GetJniSignature (method, cache), "__export__", null, null, null) { - IsExport = true; - Annotations = null; - if (method.HasParameters) Diagnostic.Error (4205, JavaCallableWrapperGenerator.LookupSource (method), Localization.Resources.JavaCallableWrappers_XA4205); if (method.ReturnType.MetadataType == MetadataType.Void) Diagnostic.Error (4208, JavaCallableWrapperGenerator.LookupSource (method), Localization.Resources.JavaCallableWrappers_XA4208); - } - + IsExport = true; + IsStatic = method.IsStatic; + JavaAccess = JavaCallableWrapperGenerator.GetJavaAccess (method.Attributes & MethodAttributes.MemberAccessMask); - public string? RegisterName; - public string? RegisterSignature; - public string? RegisterConnector; - public RegisterAttribute Register { - set { - RegisterName = value.Name; - RegisterSignature = value.Signature; - RegisterConnector = value.Connector; - } - } - public MethodDefinition Method { - set { - RegisterName = value.Name; - RegisterSignature = GetJniSignature (value, Cache); - RegisterConnector = "__export__"; - Annotations = JavaCallableWrapperGenerator.GetAnnotationsString ("\t", value.CustomAttributes, Cache); - } + // annotations are processed within JavaFieldInfo, not the initializer method. So we don't generate them here. } - public bool IsDynamicallyRegistered; - public string? ManagedParameters; - public string? OuterType; - public string? SuperCall; - public string? Annotations; - public string? JavaAccess; - public string? JavaNameOverride; - public bool IsExport; - public bool IsStatic; - public string[]? ThrownTypeNames; - } - class Signature { - - public Signature (SignatureOptions options) + public Signature (string name, string? signature, string? connector, string? managedParameters, string? outerType, string? superCall) { - if (options.RegisterName == null) { - throw new ArgumentNullException ("`RegisterName` cannot be null.", nameof (options.RegisterName)); - } - if (options.RegisterSignature == null) { - throw new ArgumentNullException ("`RegisterSignature` cannot be null.", nameof (options.RegisterSignature)); - } - Annotations = options.Annotations; - IsDynamicallyRegistered = options.IsDynamicallyRegistered; - IsExport = options.IsExport; - IsStatic = options.IsStatic; - JavaAccess = options.JavaAccess; - JavaNameOverride = options.JavaNameOverride; - JniSignature = options.RegisterSignature; - ManagedParameters = options.ManagedParameters; - Method = options.RegisterName + ":" + JniSignature + ":" + options.RegisterConnector; - Name = options.RegisterName; - ThrownTypeNames = options.ThrownTypeNames; - - if (options.Style == JavaPeerStyle.XAJavaInterop1) { - Method = "n_" + Method; - } + ManagedParameters = managedParameters; + JniSignature = signature ?? throw new ArgumentNullException ("`connector` cannot be null.", nameof (connector)); + Method = "n_" + name + ":" + JniSignature + ":" + connector; + Name = name; var jnisig = JniSignature; int closer = jnisig.IndexOf (')'); string ret = jnisig.Substring (closer + 1); retval = JavaNativeTypeManager.Parse (ret)?.Type; string jniparms = jnisig.Substring (1, closer - 1); - if (string.IsNullOrEmpty (jniparms) && string.IsNullOrEmpty (options.SuperCall)) + if (string.IsNullOrEmpty (jniparms) && string.IsNullOrEmpty (superCall)) return; var parms = new StringBuilder (); var scall = new StringBuilder (); @@ -999,9 +909,9 @@ public Signature (SignatureOptions options) bool first = true; int i = 0; foreach (JniTypeName jti in JavaNativeTypeManager.FromSignature (jniparms)) { - if (options.OuterType != null) { - acall.Append (options.OuterType).Append (".this"); - options.OuterType = null; + if (outerType != null) { + acall.Append (outerType).Append (".this"); + outerType = null; continue; } string? parmType = jti.Type; @@ -1017,7 +927,7 @@ public Signature (SignatureOptions options) ++i; } this.parms = parms.ToString (); - this.call = options.SuperCall != null ? options.SuperCall : scall.ToString (); + this.call = superCall != null ? superCall : scall.ToString (); this.ActivateCall = acall.ToString (); } @@ -1154,21 +1064,13 @@ void GenerateMethod (Signature method, TextWriter sw) sw.Write (' '); if (method.IsStatic) sw.Write ("static "); - if (CodeGenerationTarget == JavaPeerStyle.JavaInterop1) { - sw.Write ("native "); - } sw.Write (method.Retval); sw.Write (' '); sw.Write (method.JavaName); sw.Write (" ("); sw.Write (method.Params); sw.Write (')'); - sw.Write (method.ThrowsDeclaration); - if (CodeGenerationTarget == JavaPeerStyle.JavaInterop1) { - sw.WriteLine (";"); - return; - } - sw.WriteLine (); + sw.WriteLine (method.ThrowsDeclaration); sw.WriteLine ("\t{"); #if MONODROID_TIMING sw.WriteLine ("\t\tandroid.util.Log.i(\"MonoDroid-Timing\", \"{0}.{1}: time: \"+java.lang.System.currentTimeMillis());", name, method.Name); @@ -1239,8 +1141,6 @@ StreamWriter OpenStream (string outputPath) /// public string GetDestinationPath (string outputPath) { - Initialize (); - var dir = package.Replace ('.', Path.DirectorySeparatorChar); return Path.Combine (outputPath, dir, name + ".java"); } diff --git a/src/Java.Runtime.Environment/Java.Interop/JreTypeManager.cs b/src/Java.Runtime.Environment/Java.Interop/JreTypeManager.cs index dc4f7b9ec..7f3e0763c 100644 --- a/src/Java.Runtime.Environment/Java.Interop/JreTypeManager.cs +++ b/src/Java.Runtime.Environment/Java.Interop/JreTypeManager.cs @@ -75,7 +75,7 @@ static void AddJniMethod (JniMethodMap toRegister, MethodInfo declaringMethod, M if (signature == null || string.IsNullOrEmpty (signature.MemberName)) { return; } - toRegister [(signature.MemberName, signature.MemberSignature)] = targetMethod ?? declaringMethod; + toRegister [("n_" + signature.MemberName, signature.MemberSignature)] = targetMethod ?? declaringMethod; } static void AddClassMethods (JniMethodMap toRegister, Type type) diff --git a/tests/Java.Interop.Export-Tests/Java.Interop/MarshalMemberBuilderTest.cs b/tests/Java.Interop.Export-Tests/Java.Interop/MarshalMemberBuilderTest.cs index f28d82268..2fe03fc0d 100644 --- a/tests/Java.Interop.Export-Tests/Java.Interop/MarshalMemberBuilderTest.cs +++ b/tests/Java.Interop.Export-Tests/Java.Interop/MarshalMemberBuilderTest.cs @@ -24,10 +24,10 @@ public void AddExportMethods () .ToList (); Assert.AreEqual (11, methods.Count); - Assert.AreEqual ("action", methods [0].Name); - Assert.AreEqual ("()V", methods [0].Signature); + Assert.AreEqual ("n_InstanceAction", methods [0].Name); + Assert.AreEqual ("()V", methods [0].Signature); - Assert.AreEqual ("staticAction", methods [1].Name); + Assert.AreEqual ("n_StaticAction", methods [1].Name); Assert.AreEqual ("()V", methods [1].Signature); #if NET diff --git a/tests/Java.Interop.Export-Tests/java/com/xamarin/interop/export/ExportType.java b/tests/Java.Interop.Export-Tests/java/com/xamarin/interop/export/ExportType.java index 8e549580c..609be683e 100644 --- a/tests/Java.Interop.Export-Tests/java/com/xamarin/interop/export/ExportType.java +++ b/tests/Java.Interop.Export-Tests/java/com/xamarin/interop/export/ExportType.java @@ -25,12 +25,54 @@ public static void testStaticMethods () { throw new Error ("staticFuncMyEnum_MyEnum should return 42!"); } - public static native void staticAction (); - public static native void staticActionIJavaObject (Object test); - public static native void staticActionInt32String (int i, String s); - public static native int staticFuncMyLegacyColorMyColor_MyColor (int color1, int color2); + public static void staticAction () {n_StaticAction ();} + private static native void n_StaticAction (); - public static native boolean staticFuncThisMethodTakesLotsOfParameters ( + public static void staticActionIJavaObject (Object test) {n_StaticActionIJavaObject(test);} + private static native void n_StaticActionIJavaObject (Object test); + + public static void staticActionInt32String (int i, String s) {n_StaticActionInt32String (i, s);} + private static native void n_StaticActionInt32String (int i, String s); + + public static int staticFuncMyLegacyColorMyColor_MyColor (int color1, int color2) {return n_StaticFuncMyLegacyColorMyColor_MyColor (color1, color2);} + private static native int n_StaticFuncMyLegacyColorMyColor_MyColor (int color1, int color2); + + public static boolean staticFuncThisMethodTakesLotsOfParameters ( + boolean a, + byte b, + char c, + short d, + int e, + long f, + float g, + double h, + Object i, + String j, + ArrayList k, + String l, + Object m, + double n, + float o, + long p) { + return n_StaticFuncThisMethodTakesLotsOfParameters ( + a, + b, + c, + d, + e, + f, + g, + h, + i, + j, + k, + l, + m, + n, + o, + p); + } + private static native boolean n_StaticFuncThisMethodTakesLotsOfParameters ( boolean a, byte b, char c, @@ -86,12 +128,23 @@ public void testMethods () { throw new Error ("staticFuncThisMethodTakesLotsOfParameters should return true!"); } - public native void action (); - public native void actionIJavaObject (Object test); - public native long funcInt64 (); - public native Object funcIJavaObject (); - public native void staticActionInt (int i); - public native void staticActionFloat (float f); + public void action () {n_InstanceAction ();} + private native void n_InstanceAction (); + + public void actionIJavaObject (Object test) {n_InstanceActionIJavaObject (test);} + private native void n_InstanceActionIJavaObject (Object test); + + public long funcInt64 () {return n_FuncInt64 ();} + private native long n_FuncInt64 (); + + public Object funcIJavaObject () {return n_FuncIJavaObject ();} + private native Object n_FuncIJavaObject (); + + public void staticActionInt (int i) {n_StaticActionInt (i);} + private native void n_StaticActionInt (int i); + + public void staticActionFloat (float f) {n_StaticActionFloat (f);} + private native void n_StaticActionFloat (float f); ArrayList managedReferences = new ArrayList(); diff --git a/tests/Java.Interop.Tools.JavaCallableWrappers-Tests/Java.Interop.Tools.JavaCallableWrappers/JavaCallableWrapperGeneratorTests.cs b/tests/Java.Interop.Tools.JavaCallableWrappers-Tests/Java.Interop.Tools.JavaCallableWrappers/JavaCallableWrapperGeneratorTests.cs index 9858698e3..45adf68e5 100644 --- a/tests/Java.Interop.Tools.JavaCallableWrappers-Tests/Java.Interop.Tools.JavaCallableWrappers/JavaCallableWrapperGeneratorTests.cs +++ b/tests/Java.Interop.Tools.JavaCallableWrappers-Tests/Java.Interop.Tools.JavaCallableWrappers/JavaCallableWrapperGeneratorTests.cs @@ -37,8 +37,7 @@ public void KotlinInvalidImplRegisterName () // Contains invalid [Register] name of "foo-impl" var td = SupportDeclarations.GetTypeDefinition (typeof (KotlinInvalidImplRegisterName)); - var g = new JavaCallableWrapperGenerator (td, logger, cache: null); - var e = Assert.Throws (() => g.Generate (TextWriter.Null)); + var e = Assert.Throws (() => new JavaCallableWrapperGenerator (td, logger, cache: null)); Assert.AreEqual (4217, e.Code); } @@ -49,8 +48,7 @@ public void KotlinInvalidHashRegisterName () // Contains invalid [Register] name of "foo-f8k2a13" var td = SupportDeclarations.GetTypeDefinition (typeof (KotlinInvalidHashRegisterName)); - var g = new JavaCallableWrapperGenerator (td, logger, cache: null); - var e = Assert.Throws (() => g.Generate (TextWriter.Null)); + var e = Assert.Throws (() => new JavaCallableWrapperGenerator (td, logger, cache: null)); Assert.AreEqual (4217, e.Code); } @@ -641,7 +639,7 @@ extends java.lang.Object public static final String __md_methods; static { __md_methods = - ""example:()V:__export__\n"" + + ""n_Example:()V:__export__\n"" + """"; com.xamarin.java_interop.ManagedPeer.registerNativeMembers (JavaInteropExample.class, ""Xamarin.Android.ToolsTests.JavaInteropExample, Java.Interop.Tools.JavaCallableWrappers-Tests"", __md_methods); } @@ -656,7 +654,12 @@ public JavaInteropExample (int p0, int p1) } - public native void example (); + public void example () + { + n_Example (); + } + + private native void n_Example (); private java.util.ArrayList refList; public void jiAddManagedReference (java.lang.Object obj) diff --git a/tools/jnimarshalmethod-gen/App.cs b/tools/jnimarshalmethod-gen/App.cs index 3ce12a778..8ee0daf0c 100644 --- a/tools/jnimarshalmethod-gen/App.cs +++ b/tools/jnimarshalmethod-gen/App.cs @@ -560,7 +560,7 @@ void CreateMarshalMethodAssembly (string path) signature = export?.Signature ?? builder.GetJniMethodSignature (method); - registrations.Add (new ExpressionMethodRegistration (name, signature, mmDef)); + registrations.Add (new ExpressionMethodRegistration ("n_" + method.Name, signature, mmDef)); addedMethods.Add (methodName); } From ce850caeaee8ed71832dcdebfa3ab564ed1935a4 Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Wed, 8 Nov 2023 19:04:35 -0500 Subject: [PATCH 16/44] Rethink `Type.GetType()` alternatives Remember the one-off comment in 93a901a8207ded8b8ec124c64ad76d7a7a2d3779? > (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.) The more I think about it, the saner this feels. Add `JniRuntime.JniTypeManager.GetTypeFromAssemblyQualifiedName()`, and use that instead of `Type.GetType()`. This allows `NativeAotTypeManager` to provide its equivalent functionality. This in turn allows `ManagedType` to provide a constructor which takes parameters, which previously broke because that codepath hit `Type.GetType()`! --- samples/Hello-NativeAOTFromJNI/ManagedType.cs | 9 +- .../NativeAotTypeManager.cs | 17 +-- samples/Hello-NativeAOTFromJNI/README.md | 113 ++---------------- .../com/microsoft/hello_from_jni/App.java | 2 +- .../Java.Interop/JniRuntime.JniTypeManager.cs | 8 +- src/Java.Interop/Java.Interop/ManagedPeer.cs | 20 ++-- 6 files changed, 44 insertions(+), 125 deletions(-) diff --git a/samples/Hello-NativeAOTFromJNI/ManagedType.cs b/samples/Hello-NativeAOTFromJNI/ManagedType.cs index 58e175cb5..aebe6df29 100644 --- a/samples/Hello-NativeAOTFromJNI/ManagedType.cs +++ b/samples/Hello-NativeAOTFromJNI/ManagedType.cs @@ -5,14 +5,17 @@ namespace Example; [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/NativeAotTypeManager.cs b/samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs index 4719809e6..1b08f9a23 100644 --- a/samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs +++ b/samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs @@ -8,18 +8,19 @@ class NativeAotTypeManager : JniRuntime.JniTypeManager { Dictionary typeMappings = new () { ["com/xamarin/java_interop/internal/JavaProxyThrowable"] = Type.GetType ("Java.Interop.JavaProxyThrowable, Java.Interop", throwOnError: true)!, ["example/ManagedType"] = typeof (Example.ManagedType), + ["System.Int32, System.Runtime"] = typeof (int), + ["System.Int32, System.Private.CoreLib"] = typeof (int), + ["Java.Interop.JavaProxyThrowable, Java.Interop"] = Type.GetType ("Java.Interop.JavaProxyThrowable, Java.Interop", throwOnError: true)!, + ["Example.ManagedType, Hello-NativeAOTFromJNI"] = typeof (Example.ManagedType), }; #pragma warning restore IL2026 - public override void RegisterNativeMembers (JniType nativeClass, ReadOnlySpan assemblyQualifiedTypeName, ReadOnlySpan methods) + + public override Type GetTypeFromAssemblyQualifiedName (string assemblyQualifiedTypeName) { - 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}\"!"); - } + if (typeMappings.TryGetValue (assemblyQualifiedTypeName, out var type)) + return type; + throw new NotSupportedException ($"Unsupported type: \"{assemblyQualifiedTypeName}\"!"); } protected override IEnumerable GetTypesForSimpleReference (string jniSimpleReference) diff --git a/samples/Hello-NativeAOTFromJNI/README.md b/samples/Hello-NativeAOTFromJNI/README.md index 480e90b88..e6f8b4ff7 100644 --- a/samples/Hello-NativeAOTFromJNI/README.md +++ b/samples/Hello-NativeAOTFromJNI/README.md @@ -48,26 +48,22 @@ NativeAOT-generated `libHello-NativeAOTFromJNI.dylib` to be run: C# init() Hello from .NET NativeAOT! String returned to Java: Hello from .NET NativeAOT! - C# RegisterNativeMembers(JniType(Name='example/ManagedType' PeerReference=0x7fd0a00072d8/G), "Example.ManagedType, Hello-NativeAOTFromJNI", "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! + mt.getString()=Hello from C#, via Java.Interop! Value=42 Build succeeded. 0 Warning(s) 0 Error(s) -Time Elapsed 00:00:00.73 +Time Elapsed 00:00:01.04 % (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=0x7fa822114598/G), "Example.ManagedType, Hello-NativeAOTFromJNI", "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! +mt.getString()=Hello from C#, via Java.Interop! Value=42 ``` Note the use of `(cd …; java …)` so that `libHello-NativeAOTFromJNI.dylib` is @@ -99,8 +95,8 @@ 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: +Unfortunately, we do this in key places within Java.Interop. Consider this +more complete Java Callable Wrapper fragment: ```java public class ManagedType @@ -139,105 +135,22 @@ public class ManagedType ``` There are *two* places that assembly-qualified names are used, both of which -currently wind up at `Type.GetType()`: +normally 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.(ManagedType.java:23) - at com.microsoft.hello_from_jni.App.main(App.java:13) -``` +This sample "fixes" things by adding +`JniRuntime.JniTypeManager.GetTypeFromAssemblyQualifiedName()`, which allows +`NativeAotTypeManager` to override it and support the various assembly-qualified +name values which the sample requires. -This isn't impossible -- a straightforward fix would be to declare `native` -methods for each constructor overload -- but fixing this gets increasingly difficult. +An alternate idea to avoid some of the new `GetTypeFromAssemblyQualifiedName()` +invocations 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 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 f6d6fff23..f4764cf1c 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 @@ public static void main(String[] args) { 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()); } diff --git a/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs b/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs index 8b36469e5..ab9710bb0 100644 --- a/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs +++ b/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs @@ -372,8 +372,12 @@ IEnumerable CreateGetTypesForSimpleReferenceEnumerator (string jniSimpleRe protected virtual ReplacementMethodInfo? GetReplacementMethodInfoCore (string jniSimpleReference, string jniMethodName, string jniMethodSignature) => null; - public virtual void RegisterNativeMembers (JniType nativeClass, ReadOnlySpan assmblyQualifiedTypeName, ReadOnlySpan methods) => - RegisterNativeMembers (nativeClass, Type.GetType (new string (assmblyQualifiedTypeName), throwOnError: true)!, methods); + public virtual Type GetTypeFromAssemblyQualifiedName (string assemblyQualifiedTypeName) + { + AssertValid (); + + return Type.GetType (assemblyQualifiedTypeName, throwOnError: true)!; + } public virtual void RegisterNativeMembers (JniType nativeClass, Type type, ReadOnlySpan methods) { diff --git a/src/Java.Interop/Java.Interop/ManagedPeer.cs b/src/Java.Interop/Java.Interop/ManagedPeer.cs index 1371692ff..ac60c1c06 100644 --- a/src/Java.Interop/Java.Interop/ManagedPeer.cs +++ b/src/Java.Interop/Java.Interop/ManagedPeer.cs @@ -69,6 +69,7 @@ static void Construct ( var envp = new JniTransition (jnienv); try { var runtime = JniEnvironment.Runtime; + var typeMgr = runtime.TypeManager; var r_self = new JniObjectReference (n_self); var self = runtime.ValueManager.PeekPeer (r_self); if (self != null) { @@ -92,14 +93,14 @@ static void Construct ( return; } - var type = Type.GetType (JniEnvironment.Strings.ToString (n_assemblyQualifiedName)!, throwOnError: true)!; + var type = typeMgr.GetTypeFromAssemblyQualifiedName (JniEnvironment.Strings.ToString (n_assemblyQualifiedName)!); if (type.IsGenericTypeDefinition) { throw new NotSupportedException ( "Constructing instances of generic types from Java is not supported, as the type parameters cannot be determined.", CreateJniLocationException ()); } - var ptypes = GetParameterTypes (JniEnvironment.Strings.ToString (n_constructorSignature)); + var ptypes = GetParameterTypes (typeMgr, JniEnvironment.Strings.ToString (n_constructorSignature)); var pvalues = GetValues (runtime, new JniObjectReference (n_constructorArguments), ptypes); var cinfo = type.GetConstructor (ptypes); if (cinfo == null) { @@ -148,14 +149,14 @@ static Exception CreateJniLocationException () } } - static Type[] GetParameterTypes (string? signature) + static Type[] GetParameterTypes (JniRuntime.JniTypeManager typeMgr, string? signature) { if (string.IsNullOrEmpty (signature)) return Array.Empty (); var typeNames = signature!.Split (':'); var ptypes = new Type [typeNames.Length]; for (int i = 0; i < typeNames.Length; i++) - ptypes [i] = Type.GetType (typeNames [i], throwOnError:true)!; + ptypes [i] = typeMgr.GetTypeFromAssemblyQualifiedName (typeNames [i]); return ptypes; } @@ -197,26 +198,23 @@ 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 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(aqnChars, aqnLength); + var type = JniEnvironment.Runtime.TypeManager.GetTypeFromAssemblyQualifiedName (assemblyQualifiedName!); int methodsLength = JniEnvironment.Strings.GetStringLength (methodsRef); var methodsChars = JniEnvironment.Strings.GetStringChars (methodsRef, null); var methods = new ReadOnlySpan(methodsChars, methodsLength); try { - JniEnvironment.Runtime.TypeManager.RegisterNativeMembers (nativeClass, aqn, methods); + JniEnvironment.Runtime.TypeManager.RegisterNativeMembers (nativeClass, type, 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); From 6920e6c236eecb746d8eb79a231ac6532b13e30d Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Thu, 9 Nov 2023 08:00:52 -0500 Subject: [PATCH 17/44] Remove need for NativeAotValueManager. --- .../JavaInteropRuntime.cs | 4 +-- .../NativeAotValueManager.cs | 26 ------------------- .../JniRuntime.JniMarshalMemberBuilder.cs | 1 + .../Java.Interop/JreTypeManager.cs | 4 +++ .../Java.Interop/ManagedValueManager.cs | 25 ++++++++++++++++-- 5 files changed, 30 insertions(+), 30 deletions(-) delete mode 100644 samples/Hello-NativeAOTFromJNI/NativeAotValueManager.cs diff --git a/samples/Hello-NativeAOTFromJNI/JavaInteropRuntime.cs b/samples/Hello-NativeAOTFromJNI/JavaInteropRuntime.cs index 607bd73fb..3cfd551f4 100644 --- a/samples/Hello-NativeAOTFromJNI/JavaInteropRuntime.cs +++ b/samples/Hello-NativeAOTFromJNI/JavaInteropRuntime.cs @@ -27,8 +27,8 @@ static void init (IntPtr jnienv, IntPtr klass) try { var options = new JreRuntimeOptions { EnvironmentPointer = jnienv, - TypeManager = new NativeAotTypeManager (), - ValueManager = new NativeAotValueManager (), + TypeManager = new NativeAotTypeManager (), + UseMarshalMemberBuilder = false, }; runtime = options.CreateJreVM (); } diff --git a/samples/Hello-NativeAOTFromJNI/NativeAotValueManager.cs b/samples/Hello-NativeAOTFromJNI/NativeAotValueManager.cs deleted file mode 100644 index 0478a1890..000000000 --- a/samples/Hello-NativeAOTFromJNI/NativeAotValueManager.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Reflection; - -using Java.Interop; - -namespace Hello_NativeAOTFromJNI; - -class NativeAotValueManager : ManagedValueManager { - - // Can't use default version, because: - // System.ArgumentException: Instance property 'DeclaringType' is not defined for type 'System.Reflection.MemberInfo' (Parameter 'propertyName') - // at System.Linq.Expressions.Expression.Property(Expression, String) + 0xaa - // at Java.Interop.MarshalMemberBuilder.CreateConstructActivationPeerExpression(ConstructorInfo) + 0x209 - // at Java.Interop.JniRuntime.JniMarshalMemberBuilder.CreateConstructActivationPeerFunc(ConstructorInfo) + 0x13 - // Do it "manually" - public override void ActivatePeer (IJavaPeerable? self, JniObjectReference reference, ConstructorInfo cinfo, object?[]? argumentValues) - { - var declType = cinfo.DeclaringType ?? throw new NotSupportedException ("Do not know the type to create!"); - -#pragma warning disable IL2072 - self = (IJavaPeerable) System.Runtime.CompilerServices.RuntimeHelpers.GetUninitializedObject (declType); -#pragma warning restore IL2072 - self.SetPeerReference (reference); - - cinfo.Invoke (self, argumentValues); - } -} diff --git a/src/Java.Interop/Java.Interop/JniRuntime.JniMarshalMemberBuilder.cs b/src/Java.Interop/Java.Interop/JniRuntime.JniMarshalMemberBuilder.cs index 11fabae92..1471ee402 100644 --- a/src/Java.Interop/Java.Interop/JniRuntime.JniMarshalMemberBuilder.cs +++ b/src/Java.Interop/Java.Interop/JniRuntime.JniMarshalMemberBuilder.cs @@ -26,6 +26,7 @@ public JniMarshalMemberBuilder MarshalMemberBuilder { return marshalMemberBuilder; } } + public bool UseMarshalMemberBuilder => marshalMemberBuilder != null; [System.Diagnostics.CodeAnalysis.SuppressMessage ("Design", "CA1031:Do not catch general exception types", Justification = "the *.Export assemblies are optional, so we don't care when they cannot be loaded (they are presumably missing)")] #if NET diff --git a/src/Java.Runtime.Environment/Java.Interop/JreTypeManager.cs b/src/Java.Runtime.Environment/Java.Interop/JreTypeManager.cs index 7f3e0763c..6b01ef950 100644 --- a/src/Java.Runtime.Environment/Java.Interop/JreTypeManager.cs +++ b/src/Java.Runtime.Environment/Java.Interop/JreTypeManager.cs @@ -23,6 +23,10 @@ public override void RegisterNativeMembers (JniType nativeClass, Type type, Read return; } + if (!Runtime.UseMarshalMemberBuilder) { + throw new NotSupportedException ("JniRuntime.MarshalMemberBuilder is required and not present."); + } + var toRegister = new JniMethodMap (); AddInterfaceMethods (toRegister, type); diff --git a/src/Java.Runtime.Environment/Java.Interop/ManagedValueManager.cs b/src/Java.Runtime.Environment/Java.Interop/ManagedValueManager.cs index 2a60914c8..4016909b2 100644 --- a/src/Java.Runtime.Environment/Java.Interop/ManagedValueManager.cs +++ b/src/Java.Runtime.Environment/Java.Interop/ManagedValueManager.cs @@ -204,8 +204,11 @@ public override void ActivatePeer (IJavaPeerable? self, JniObjectReference refer var runtime = JniEnvironment.Runtime; try { - var f = runtime.MarshalMemberBuilder.CreateConstructActivationPeerFunc (cinfo); - f (cinfo, reference, argumentValues); + if (runtime.UseMarshalMemberBuilder) { + ActivateViaMarshalMemberBuilder (runtime.MarshalMemberBuilder, reference, cinfo, argumentValues); + return; + } + ActivateViaReflection (reference, cinfo, argumentValues); } catch (Exception e) { var m = string.Format ("Could not activate {{ PeerReference={0} IdentityHashCode=0x{1} Java.Type={2} }} for managed type '{3}'.", reference, @@ -218,6 +221,24 @@ public override void ActivatePeer (IJavaPeerable? self, JniObjectReference refer } } + void ActivateViaMarshalMemberBuilder (JniRuntime.JniMarshalMemberBuilder builder, JniObjectReference reference, ConstructorInfo cinfo, object?[]? argumentValues) + { + var f = builder.CreateConstructActivationPeerFunc (cinfo); + f (cinfo, reference, argumentValues); + } + + void ActivateViaReflection (JniObjectReference reference, ConstructorInfo cinfo, object?[]? argumentValues) + { + var declType = cinfo.DeclaringType ?? throw new NotSupportedException ("Do not know the type to create!"); + +#pragma warning disable IL2072 + var self = (IJavaPeerable) System.Runtime.CompilerServices.RuntimeHelpers.GetUninitializedObject (declType); +#pragma warning restore IL2072 + self.SetPeerReference (reference); + + cinfo.Invoke (self, argumentValues); + } + public override List GetSurfacedPeers () { if (RegisteredInstances == null) From 986f0c488d906b5aef15b14249587cc25ed9471f Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Tue, 14 Nov 2023 12:52:45 -0800 Subject: [PATCH 18/44] Reduce patch size. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some things were split into other PRs… --- .../ExpressionAssemblyBuilder.cs | 7 +------ .../Java.Interop.Tools.JavaCallableWrappers.csproj | 2 +- src/Java.Interop/Java.Interop/JniEnvironment.Types.cs | 2 -- src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs | 1 - tools/jnimarshalmethod-gen/App.cs | 2 -- 5 files changed, 2 insertions(+), 12 deletions(-) diff --git a/src/Java.Interop.Tools.Expressions/Java.Interop.Tools.Expressions/ExpressionAssemblyBuilder.cs b/src/Java.Interop.Tools.Expressions/Java.Interop.Tools.Expressions/ExpressionAssemblyBuilder.cs index 4c14f7fd2..e66289184 100644 --- a/src/Java.Interop.Tools.Expressions/Java.Interop.Tools.Expressions/ExpressionAssemblyBuilder.cs +++ b/src/Java.Interop.Tools.Expressions/Java.Interop.Tools.Expressions/ExpressionAssemblyBuilder.cs @@ -227,7 +227,7 @@ public void Write (string path) var module = DeclaringAssemblyDefinition.MainModule; var c = new MemoryStream (); - DeclaringAssemblyDefinition.Write (path); + DeclaringAssemblyDefinition.Write (c); c.Position = 0; if (KeepTemporaryFiles) { @@ -236,10 +236,6 @@ public void Write (string path) c.Position = 0; } - -#if false - // `Failed to resolve System.Runtime.InteropServices.CallingConvention` - // because `System.Runtime.InteropServices` is not referenced. Logger (TraceLevel.Verbose, $"# jonp: ---"); var rp = new ReaderParameters { @@ -272,7 +268,6 @@ public void Write (string path) module.AssemblyReferences.Remove (selfRef); } newAsm.Write (path); -#endif // false } static AssemblyNameReference GetSystemRuntimeReference () diff --git a/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers.csproj b/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers.csproj index 05f576b4d..7e4704293 100644 --- a/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers.csproj +++ b/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers.csproj @@ -3,7 +3,7 @@ netstandard2.0 false - 11.0 + 8.0 enable true true diff --git a/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs b/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs index 863cb172b..da4d41b99 100644 --- a/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs +++ b/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs @@ -265,8 +265,6 @@ 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)); } diff --git a/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs b/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs index 02dad3d83..8a976b0a1 100644 --- a/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs +++ b/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs @@ -486,7 +486,6 @@ bool FindAndCallRegisterMethod (Type marshalType, JniNativeMethodRegistrationArg } var register = (Action)methodInfo.CreateDelegate (typeof (Action)); - Console.WriteLine ($"# jonp: FindAndCallRegisterMethod: calling `{declaringTypeName}.{methodInfo}`…"); register (arguments); found = true; diff --git a/tools/jnimarshalmethod-gen/App.cs b/tools/jnimarshalmethod-gen/App.cs index 8ee0daf0c..fc43233c8 100644 --- a/tools/jnimarshalmethod-gen/App.cs +++ b/tools/jnimarshalmethod-gen/App.cs @@ -232,7 +232,6 @@ void ProcessAssemblies (List assemblies) foreach (var d in resolver.SearchDirectories) { var a = Path.Combine (d, name.Name); var f = a + ".dll"; - Log (TraceLevel.Verbose, $"# jonp: checking for: {f} ({File.Exists (f)})"); if (File.Exists (f)) { return Assembly.LoadFile (Path.GetFullPath (f)); } @@ -241,7 +240,6 @@ void ProcessAssemblies (List assemblies) return Assembly.LoadFile (Path.GetFullPath (f)); } } - Log (TraceLevel.Verbose, $"# jonp: could not resolve assembly: {e.Name}"); return null; }; From 17a44038a0bc5988ea4c59c8d0f75043f32a902c Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Wed, 15 Nov 2023 11:29:55 -0800 Subject: [PATCH 19/44] =?UTF-8?q?Work=20with=20.NET=208=20GA=20(=E2=80=A6?= =?UTF-8?q?=3F)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Hello-NativeAOTFromJNI sample previously built and worked on .NET 8 RC2 on macOS+x64, but when I tried things on .NET 8 GA on macOS+arm64, things failed "inexplicably" % dotnet publish -c Release -r osx-x64 EXEC : error JM4006: jnimarshalmethod-gen: Unable to process assembly '/Users/jon/Developer/src/xamarin/java.interop/samples/Hello-NativeAOTFromJNI/bin/Release/osx-x64/Hello-NativeAOTFromJNI.dll' [/Users/jon/Developer/src/xamarin/java.interop/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj] Could not load file or assembly 'Hello-NativeAOTFromJNI, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'. System.IO.FileLoadException: Could not load file or assembly 'Hello-NativeAOTFromJNI, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'. at System.Runtime.Loader.AssemblyLoadContext.InternalLoad(ReadOnlySpan`1 arrAssembly, ReadOnlySpan`1 arrSymbols) at Xamarin.Android.Tools.JniMarshalMethodGenerator.App.CreateMarshalMethodAssembly(String path) in /Users/jon/Developer/src/xamarin/java.interop/tools/jnimarshalmethod-gen/App.cs:line 445 at Xamarin.Android.Tools.JniMarshalMethodGenerator.App.ProcessAssemblies(List`1 assemblies) in /Users/jon/Developer/src/xamarin/java.interop/tools/jnimarshalmethod-gen/App.cs:line 272 After a few days of pulling my hair out, @lambdageek suggested: > so maybe coreclr on arm64 refuses to load a PE32+ image that is x86-64? This turns out to be the case, presenting two "workarounds": 1. Build with an rid that matches the host platform, e.g. this worked: dotnet publish -c Release -r osx-arm64 2. Setting `$(PlatformTarget)`=AnyCPU also works, as the resulting assembly isn't tied to a specific rid. Update `Hello-NativeAOTFromJNI.csproj` to set `$(PlatformTarget)`=AnyCPU, so that I can at least *build* osx-x64 from osx-arm64. (I can't *run* it, but I can build it!) Also update `jnimarshalmethod-gen` to not load assemblies from directories containing `/ref/`, as reference assemblies cannot be loaded for execution: System.ArgumentException: A BadImageFormatException has been thrown while parsing the signature. This is likely due to lack of a generic context. Ensure genericTypeArguments and genericMethodArguments are provided and contain enough context. ---> System.BadImageFormatException: Could not load file or assembly 'System.Runtime, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'. Reference assemblies cannot be loaded for execution. (0x80131058) File name: 'System.Runtime, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' ---> System.BadImageFormatException: Could not load file or assembly 'System.Runtime, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL'. Reference assemblies cannot be loaded for execution. (0x80131058) File name: 'System.Runtime, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL' ---> System.BadImageFormatException: Cannot load a reference assembly for execution. at System.Runtime.Loader.AssemblyLoadContext.g____PInvoke|5_0(IntPtr ptrNativeAssemblyBinder, UInt16* ilPath, UInt16* niPath, ObjectHandleOnStack retAssembly) at System.Runtime.Loader.AssemblyLoadContext.LoadFromAssemblyPath(String assemblyPath) at System.Reflection.Assembly.LoadFile(String path) --- .../Hello-NativeAOTFromJNI.csproj | 2 + .../Hello-NativeAOTFromJNI.targets | 8 ++-- samples/Hello-NativeAOTFromJNI/README.md | 5 +++ tools/jnimarshalmethod-gen/App.cs | 38 ++++++++++++------- 4 files changed, 35 insertions(+), 18 deletions(-) diff --git a/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj b/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj index 1909cd272..d46e468d9 100644 --- a/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj +++ b/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj @@ -8,6 +8,8 @@ true true Shared + + AnyCPU diff --git a/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets b/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets index 2913a492b..f7300054f 100644 --- a/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets +++ b/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets @@ -14,13 +14,13 @@ - <_RefAsmDirs Include="@(ReferencePathWithRefAssemblies->'%(RootDir)%(Directory).'->Distinct())" /> + <_JcwGenRefAsmDirs Include="@(ReferencePathWithRefAssemblies->'%(RootDir)%(Directory).'->Distinct())" /> <_JcwGen>"$(UtilityOutputFullPath)/jcw-gen.dll" <_Target>--codegen-target JavaInterop1 <_Output>-o "$(IntermediateOutputPath)/java" - <_Libpath>@(_RefAsmDirs->'-L "%(Identity)"', ' ') + <_Libpath>@(_JcwGenRefAsmDirs->'-L "%(Identity)"', ' ') @@ -33,12 +33,12 @@ AfterTargets="_CreateJavaCallableWrappers"> - <_RefAsmDirs Include="@(ReferencePathWithRefAssemblies->'%(RootDir)%(Directory).'->Distinct())" /> + <_JnimmRefAsmDirs Include="@(RuntimePackAsset->'%(RootDir)%(Directory).'->Distinct())" /> <_JnimarshalmethodGen>"$(UtilityOutputFullPath)/jnimarshalmethod-gen.dll" <_Verbosity>-v -v --keeptemp - <_Libpath>-L "$(TargetDir)" @(_RefAsmDirs->'-L "%(Identity)"', ' ') + <_Libpath>-L "$(TargetDir)" @(_JnimmRefAsmDirs->'-L "%(Identity)"', ' ') diff --git a/samples/Hello-NativeAOTFromJNI/README.md b/samples/Hello-NativeAOTFromJNI/README.md index e6f8b4ff7..17d66871b 100644 --- a/samples/Hello-NativeAOTFromJNI/README.md +++ b/samples/Hello-NativeAOTFromJNI/README.md @@ -69,6 +69,11 @@ mt.getString()=Hello from C#, via Java.Interop! Value=42 Note the use of `(cd …; java …)` so that `libHello-NativeAOTFromJNI.dylib` is in the current working directory, so that it can be found. +# Notes + +To support cross-compilation, the project should set +`$(PlatformTarget)`=AnyCPU. + # Known Unknowns With this sample "done" (-ish), there are several "future research directions" to diff --git a/tools/jnimarshalmethod-gen/App.cs b/tools/jnimarshalmethod-gen/App.cs index fc43233c8..b75a8f416 100644 --- a/tools/jnimarshalmethod-gen/App.cs +++ b/tools/jnimarshalmethod-gen/App.cs @@ -227,20 +227,7 @@ void ProcessAssemblies (List assemblies) // loadContext = CreateLoadContext (); AppDomain.CurrentDomain.AssemblyResolve += (o, e) => { - Log (TraceLevel.Verbose, $"# jonp: resolving assembly: {e.Name}"); - var name = new AssemblyName (e.Name); - foreach (var d in resolver.SearchDirectories) { - var a = Path.Combine (d, name.Name); - var f = a + ".dll"; - if (File.Exists (f)) { - return Assembly.LoadFile (Path.GetFullPath (f)); - } - f = a + ".exe"; - if (File.Exists (f)) { - return Assembly.LoadFile (Path.GetFullPath (f)); - } - } - return null; + return TryResolveAssembly (e.Name, e.RequestingAssembly); }; foreach (var r in references) { @@ -341,6 +328,29 @@ static string ReadJavaSdkDirectoryFromJdkInfoProps () return jdkJvmPath.Value; } + readonly string RefPathPart = $"{Path.DirectorySeparatorChar}ref{Path.DirectorySeparatorChar}"; + + Assembly TryResolveAssembly (string assemblyFullName, Assembly requestingAssembly) + { + Log (TraceLevel.Verbose, $"# jonp: resolving assembly: {assemblyFullName}"); + var name = new AssemblyName (assemblyFullName); + foreach (var d in resolver.SearchDirectories) { + if (d.Contains (RefPathPart, StringComparison.OrdinalIgnoreCase)) { + continue; + } + var a = Path.Combine (d, name.Name); + var f = a + ".dll"; + if (File.Exists (f)) { + return Assembly.LoadFile (Path.GetFullPath (f)); + } + f = a + ".exe"; + if (File.Exists (f)) { + return Assembly.LoadFile (Path.GetFullPath (f)); + } + } + return null; + } + AssemblyLoadContext CreateLoadContext () { var c = new AssemblyLoadContext ("jnimarshalmethod-gen", isCollectible: true); From 1b86f9a76c87a2e491433c1ab5ee9e2858178a24 Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Wed, 15 Nov 2023 17:27:56 -0800 Subject: [PATCH 20/44] Use `net.dot.jni` package prefix It's shorter! It avoids `_1` in Java method names! --- samples/Hello-NativeAOTFromJNI/App.cs | 2 +- .../Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets | 2 +- samples/Hello-NativeAOTFromJNI/JavaInteropRuntime.cs | 2 +- samples/Hello-NativeAOTFromJNI/README.md | 6 +++--- .../microsoft/hello_from_jni => net/dot/jni/hello}/App.java | 4 ++-- .../dot/jni/hello}/JavaInteropRuntime.java | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) rename samples/Hello-NativeAOTFromJNI/java/{com/microsoft/hello_from_jni => net/dot/jni/hello}/App.java (81%) rename samples/Hello-NativeAOTFromJNI/java/{com/microsoft/java_interop => net/dot/jni/hello}/JavaInteropRuntime.java (84%) diff --git a/samples/Hello-NativeAOTFromJNI/App.cs b/samples/Hello-NativeAOTFromJNI/App.cs index b8729218d..10bb10ae7 100644 --- a/samples/Hello-NativeAOTFromJNI/App.cs +++ b/samples/Hello-NativeAOTFromJNI/App.cs @@ -7,7 +7,7 @@ namespace Hello_NativeAOTFromJNI; static class App { // symbol name from `$(IntermediateOutputPath)h-classes/com_microsoft_hello_from_jni_App.h` - [UnmanagedCallersOnly (EntryPoint="Java_com_microsoft_hello_1from_1jni_App_sayHello")] + [UnmanagedCallersOnly (EntryPoint="Java_net_dot_jni_hello_App_sayHello")] static IntPtr sayHello (IntPtr jnienv, IntPtr klass) { var envp = new JniTransition (jnienv); diff --git a/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets b/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets index f7300054f..db9707708 100644 --- a/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets +++ b/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets @@ -100,7 +100,7 @@ <_CP>@(_Classpath, '$(_CPSep)') diff --git a/samples/Hello-NativeAOTFromJNI/JavaInteropRuntime.cs b/samples/Hello-NativeAOTFromJNI/JavaInteropRuntime.cs index 3cfd551f4..acca2b4e9 100644 --- a/samples/Hello-NativeAOTFromJNI/JavaInteropRuntime.cs +++ b/samples/Hello-NativeAOTFromJNI/JavaInteropRuntime.cs @@ -20,7 +20,7 @@ static void JNI_Onload (IntPtr vm, IntPtr reserved) runtime?.Dispose (); } - [UnmanagedCallersOnly (EntryPoint="Java_com_microsoft_java_1interop_JavaInteropRuntime_init")] + [UnmanagedCallersOnly (EntryPoint="Java_net_dot_jni_hello_JavaInteropRuntime_init")] static void init (IntPtr jnienv, IntPtr klass) { Console.WriteLine ($"C# init()"); diff --git a/samples/Hello-NativeAOTFromJNI/README.md b/samples/Hello-NativeAOTFromJNI/README.md index 17d66871b..4d1ac0760 100644 --- a/samples/Hello-NativeAOTFromJNI/README.md +++ b/samples/Hello-NativeAOTFromJNI/README.md @@ -34,8 +34,8 @@ The resulting native library contains the desired symbols: % nm bin/Release/osx-x64/publish/Hello-NativeAOTFromJNI.dylib | grep ' S ' 000000000016f5a0 S _JNI_OnLoad 000000000016f5d0 S _JNI_OnUnload -000000000016f2f0 S _Java_com_microsoft_hello_1from_1jni_App_sayHello -000000000016f620 S _Java_com_microsoft_java_1interop_JavaInteropRuntime_init +000000000016f2f0 S _Java_net_dot_jni_hello_App_sayHello +000000000016f620 S _Java_net_dot_jni_hello_JavaInteropRuntime_init ``` Use the `RunJavaSample` target to run Java, which will run @@ -57,7 +57,7 @@ Build succeeded. Time Elapsed 00:00:01.04 -% (cd bin/Release/osx-x64/publish ; java -cp hello-from-java.jar:java-interop.jar com/microsoft/hello_from_jni/App) +% (cd bin/Release/osx-x64/publish ; java -cp hello-from-java.jar:java-interop.jar net/dot/jni/hello/App) Hello from Java! C# init() Hello from .NET NativeAOT! diff --git a/samples/Hello-NativeAOTFromJNI/java/com/microsoft/hello_from_jni/App.java b/samples/Hello-NativeAOTFromJNI/java/net/dot/jni/hello/App.java similarity index 81% rename from samples/Hello-NativeAOTFromJNI/java/com/microsoft/hello_from_jni/App.java rename to samples/Hello-NativeAOTFromJNI/java/net/dot/jni/hello/App.java index f4764cf1c..427016263 100644 --- a/samples/Hello-NativeAOTFromJNI/java/com/microsoft/hello_from_jni/App.java +++ b/samples/Hello-NativeAOTFromJNI/java/net/dot/jni/hello/App.java @@ -1,6 +1,6 @@ -package com.microsoft.hello_from_jni; +package net.dot.jni.hello; -import com.microsoft.java_interop.JavaInteropRuntime; +import net.dot.jni.hello.JavaInteropRuntime; import example.ManagedType; class App { diff --git a/samples/Hello-NativeAOTFromJNI/java/com/microsoft/java_interop/JavaInteropRuntime.java b/samples/Hello-NativeAOTFromJNI/java/net/dot/jni/hello/JavaInteropRuntime.java similarity index 84% rename from samples/Hello-NativeAOTFromJNI/java/com/microsoft/java_interop/JavaInteropRuntime.java rename to samples/Hello-NativeAOTFromJNI/java/net/dot/jni/hello/JavaInteropRuntime.java index 9f11855f9..aa7f1b0d7 100644 --- a/samples/Hello-NativeAOTFromJNI/java/com/microsoft/java_interop/JavaInteropRuntime.java +++ b/samples/Hello-NativeAOTFromJNI/java/net/dot/jni/hello/JavaInteropRuntime.java @@ -1,4 +1,4 @@ -package com.microsoft.java_interop; +package net.dot.jni.hello; public class JavaInteropRuntime { static { From f71cadb24b4571e089470e80e2579f9412e95c69 Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Wed, 15 Nov 2023 17:35:42 -0800 Subject: [PATCH 21/44] =?UTF-8?q?Forgot=20to=20fix=20up=20a=20header=20pat?= =?UTF-8?q?h=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- samples/Hello-NativeAOTFromJNI/App.cs | 2 +- samples/Hello-NativeAOTFromJNI/JavaInteropRuntime.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/samples/Hello-NativeAOTFromJNI/App.cs b/samples/Hello-NativeAOTFromJNI/App.cs index 10bb10ae7..94ce115eb 100644 --- a/samples/Hello-NativeAOTFromJNI/App.cs +++ b/samples/Hello-NativeAOTFromJNI/App.cs @@ -6,7 +6,7 @@ namespace Hello_NativeAOTFromJNI; static class App { - // symbol name from `$(IntermediateOutputPath)h-classes/com_microsoft_hello_from_jni_App.h` + // symbol name from `$(IntermediateOutputPath)obj/Release/osx-arm64/h-classes/net_dot_jni_hello_App.h` [UnmanagedCallersOnly (EntryPoint="Java_net_dot_jni_hello_App_sayHello")] static IntPtr sayHello (IntPtr jnienv, IntPtr klass) { diff --git a/samples/Hello-NativeAOTFromJNI/JavaInteropRuntime.cs b/samples/Hello-NativeAOTFromJNI/JavaInteropRuntime.cs index acca2b4e9..a7b7b4b2e 100644 --- a/samples/Hello-NativeAOTFromJNI/JavaInteropRuntime.cs +++ b/samples/Hello-NativeAOTFromJNI/JavaInteropRuntime.cs @@ -20,6 +20,7 @@ static void JNI_Onload (IntPtr vm, IntPtr reserved) runtime?.Dispose (); } + // symbol name from `$(IntermediateOutputPath)obj/Release/osx-arm64/h-classes/net_dot_jni_hello_JavaInteropRuntime.h` [UnmanagedCallersOnly (EntryPoint="Java_net_dot_jni_hello_JavaInteropRuntime_init")] static void init (IntPtr jnienv, IntPtr klass) { From b5fc4cf223e9f2c34a632d06f13a3b293ac47f4b Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Thu, 16 Nov 2023 10:15:07 -0800 Subject: [PATCH 22/44] Don't add new public API I'm not sure that `GetTypeFromAssemblyQualifiedName()` is actually a good idea or not, so I'd rather not add it as public API, yet it's required in order to make the sample work? How do we square this circle? With build configs (redux!). Commit c6c487b6 added the "Standalone" build config, while commit db84cc3d noted that it didn't quite work right, because "just" using `AdditionalProperties="Standalone=true"` didn't result in a `Java.Interop.dll` which had the right build config. Figure this out. The magic voodoo is that the output directories need to differ for each build config, because reusing the same output path leads to "madness" (parallel builds overwrite files, resulting in possible build breaks, etc., etc.). The key properties that need to differ based on "build config" are: * `$(IntermediateOutputPath)`, which contains intermediate files like AssemblyInfo! * `$(OutputPath)`, which is where the final assemblies are written. Update `Java.Interop.csproj` to treat `$(UseNativeAot)` as a "distinct" build config, which also sets `FEATURE_NATIVE_AOT`. This allows `GetTypeFromAssemblyQualifiedName()` to be `internal` by default, and only public when used by/built from the Hello-NativeAOTFromJNI sample. The next new public API is `JniRuntime.UseMarshalMemberBuilder`. We can make that `internal` for now by making `Java.Runtime.Environment.dll` a "friend" assembly to `Java.Interop.dll` via `[InternalsVisibleTo]`, then fix up the resulting build errors (as both assemblies had `NativeMethods` types). Remove the inclusion of `ManagedValueManager.cs` from `Hello-NativeAOTFromJNI.csproj`. It's no longer needed as of commit 6920e6c2. --- .../Hello-NativeAOTFromJNI.csproj | 6 +--- src/Java.Interop/Java.Interop.csproj | 12 ++++--- .../JniRuntime.JniMarshalMemberBuilder.cs | 3 +- .../Java.Interop/JniRuntime.JniTypeManager.cs | 7 +++- src/Java.Interop/Properties/AssemblyInfo.cs | 7 ++++ .../Java.Interop/JreRuntime.cs | 16 ++++----- .../MonoRuntimeObjectReferenceManager.cs | 36 +++++++++---------- .../Java.Interop/MonoRuntimeValueManager.cs | 22 ++++++------ 8 files changed, 61 insertions(+), 48 deletions(-) diff --git a/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj b/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj index d46e468d9..248347c73 100644 --- a/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj +++ b/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj @@ -14,7 +14,7 @@ - - - - diff --git a/src/Java.Interop/Java.Interop.csproj b/src/Java.Interop/Java.Interop.csproj index 43d5937e3..f93e38e6a 100644 --- a/src/Java.Interop/Java.Interop.csproj +++ b/src/Java.Interop/Java.Interop.csproj @@ -22,14 +22,15 @@ INTEROP;FEATURE_JNIOBJECTREFERENCE_INTPTRS;INTERNAL_NULLABLE_ATTRIBUTES;$(JavaInteropDefineConstants) - $(BaseIntermediateOutputPath)$(Configuration)\$(TargetFramework.ToLowerInvariant())\ - $(ToolOutputFullPath) - $(ToolOutputFullPath)Java.Interop.xml + $(BaseIntermediateOutputPath)$(Configuration)+NativeAOT\$(TargetFramework.ToLowerInvariant())\ + $(BaseIntermediateOutputPath)$(Configuration)\$(TargetFramework.ToLowerInvariant())\ + bin\$(Configuration)+NativeAOT-$(DotNetTargetFramework.ToLowerInvariant())\ + $(ToolOutputFullPath) + $(OutputPath)Java.Interop.xml $(BuildToolOutputFullPath) 9.0 8.0 $(JICoreLibVersion) - true FEATURE_JNIENVIRONMENT_JI_FUNCTION_POINTERS;$(DefineConstants) @@ -38,6 +39,9 @@ FEATURE_JNIENVIRONMENT_JI_PINVOKES;$(DefineConstants) + + FEATURE_NATIVE_AOT;$(DefineConstants) + diff --git a/src/Java.Interop/Java.Interop/JniRuntime.JniMarshalMemberBuilder.cs b/src/Java.Interop/Java.Interop/JniRuntime.JniMarshalMemberBuilder.cs index 1471ee402..58898be5b 100644 --- a/src/Java.Interop/Java.Interop/JniRuntime.JniMarshalMemberBuilder.cs +++ b/src/Java.Interop/Java.Interop/JniRuntime.JniMarshalMemberBuilder.cs @@ -26,7 +26,8 @@ public JniMarshalMemberBuilder MarshalMemberBuilder { return marshalMemberBuilder; } } - public bool UseMarshalMemberBuilder => marshalMemberBuilder != null; + + internal bool UseMarshalMemberBuilder => marshalMemberBuilder != null; [System.Diagnostics.CodeAnalysis.SuppressMessage ("Design", "CA1031:Do not catch general exception types", Justification = "the *.Export assemblies are optional, so we don't care when they cannot be loaded (they are presumably missing)")] #if NET diff --git a/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs b/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs index 8a976b0a1..24788a541 100644 --- a/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs +++ b/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs @@ -372,7 +372,12 @@ IEnumerable CreateGetTypesForSimpleReferenceEnumerator (string jniSimpleRe protected virtual ReplacementMethodInfo? GetReplacementMethodInfoCore (string jniSimpleReference, string jniMethodName, string jniMethodSignature) => null; - public virtual Type GetTypeFromAssemblyQualifiedName (string assemblyQualifiedTypeName) +#if FEATURE_NATIVE_AOT + public virtual +#else // FEATURE_NATIVE_AOT + internal +#endif // FEATURE_NATIVE_AOT + Type GetTypeFromAssemblyQualifiedName (string assemblyQualifiedTypeName) { AssertValid (); diff --git a/src/Java.Interop/Properties/AssemblyInfo.cs b/src/Java.Interop/Properties/AssemblyInfo.cs index b32e4cf93..e394f102e 100644 --- a/src/Java.Interop/Properties/AssemblyInfo.cs +++ b/src/Java.Interop/Properties/AssemblyInfo.cs @@ -15,6 +15,13 @@ "814f144e5d817efc4c6502cc012df310783348304e3ae38573c6d658c234025821fda87a0be8a0" + "d504df564e2c93b2b878925f42503e9d54dfef9f9586d9e6f38a305769587b1de01f6c0410328b" + "2c9733db")] +[assembly: InternalsVisibleTo ( + "Java.Runtime.Environment, PublicKey=" + + "0024000004800000940000000602000000240000525341310004000011000000438ac2a5acfbf1" + + "6cbd2b2b47a62762f273df9cb2795ceccdf77d10bf508e69e7a362ea7a45455bbf3ac955e1f2e2" + + "814f144e5d817efc4c6502cc012df310783348304e3ae38573c6d658c234025821fda87a0be8a0" + + "d504df564e2c93b2b878925f42503e9d54dfef9f9586d9e6f38a305769587b1de01f6c0410328b" + + "2c9733db")] [assembly: InternalsVisibleTo ( "Java.Interop-Tests, PublicKey=" + "0024000004800000940000000602000000240000525341310004000011000000438ac2a5acfbf1" + diff --git a/src/Java.Runtime.Environment/Java.Interop/JreRuntime.cs b/src/Java.Runtime.Environment/Java.Interop/JreRuntime.cs index d7cc0f648..4bce97019 100644 --- a/src/Java.Runtime.Environment/Java.Interop/JreRuntime.cs +++ b/src/Java.Runtime.Environment/Java.Interop/JreRuntime.cs @@ -281,10 +281,10 @@ static JavaInteropLibJvmLibraryHandler () public override void LoadJvmLibrary (string path) { IntPtr errorPtr = IntPtr.Zero; - int r = NativeMethods.java_interop_jvm_load_with_error_message (path, out errorPtr); + int r = JreNativeMethods.java_interop_jvm_load_with_error_message (path, out errorPtr); if (r != 0) { string? error = Marshal.PtrToStringAnsi (errorPtr); - NativeMethods.java_interop_free (errorPtr); + JreNativeMethods.java_interop_free (errorPtr); if (r == JAVA_INTEROP_JVM_FAILED_ALREADY_LOADED) { return; } @@ -294,17 +294,17 @@ public override void LoadJvmLibrary (string path) public override int CreateJavaVM (out IntPtr javavm, out IntPtr jnienv, ref JavaVMInitArgs args) { - return NativeMethods.java_interop_jvm_create (out javavm, out jnienv, ref args); + return JreNativeMethods.java_interop_jvm_create (out javavm, out jnienv, ref args); } public override IEnumerable GetAvailableInvocationPointers () { int nVMs; - int r = NativeMethods.java_interop_jvm_list (null, 0, out nVMs); + int r = JreNativeMethods.java_interop_jvm_list (null, 0, out nVMs); if (r != 0) throw new NotSupportedException ("JNI_GetCreatedJavaVMs() returned: " + r.ToString ()); var handles = new IntPtr [nVMs]; - r = NativeMethods.java_interop_jvm_list (handles, handles.Length, out nVMs); + r = JreNativeMethods.java_interop_jvm_list (handles, handles.Length, out nVMs); if (r != 0) throw new InvalidOperationException ("JNI_GetCreatedJavaVMs() [take 2!] returned: " + r.ToString ()); return handles; @@ -315,14 +315,14 @@ public override void Dispose () } } - partial class NativeMethods { + partial class JreNativeMethods { - static NativeMethods () + static JreNativeMethods () { if (Environment.OSVersion.Platform == PlatformID.Win32NT) { var baseDir = Path.GetDirectoryName (typeof (JreRuntime).Assembly.Location) ?? throw new NotSupportedException (); var newDir = Path.Combine (baseDir, Environment.Is64BitProcess ? "win-x64" : "win-x86"); - NativeMethods.AddDllDirectory (newDir); + JreNativeMethods.AddDllDirectory (newDir); } } diff --git a/src/Java.Runtime.Environment/Java.Interop/MonoRuntimeObjectReferenceManager.cs b/src/Java.Runtime.Environment/Java.Interop/MonoRuntimeObjectReferenceManager.cs index 82b4961ae..47812f237 100644 --- a/src/Java.Runtime.Environment/Java.Interop/MonoRuntimeObjectReferenceManager.cs +++ b/src/Java.Runtime.Environment/Java.Interop/MonoRuntimeObjectReferenceManager.cs @@ -13,19 +13,19 @@ class MonoRuntimeObjectReferenceManager : JniRuntime.JniObjectReferenceManager { public override void OnSetRuntime (JniRuntime runtime) { base.OnSetRuntime (runtime); - bridge = NativeMethods.java_interop_gc_bridge_get_current (); + bridge = JreNativeMethods.java_interop_gc_bridge_get_current (); if (bridge != IntPtr.Zero) { - logLocalRefs = NativeMethods.java_interop_gc_bridge_lref_get_log_file (bridge) != IntPtr.Zero; - logGlobalRefs = NativeMethods.java_interop_gc_bridge_gref_get_log_file (bridge) != IntPtr.Zero; + logLocalRefs = JreNativeMethods.java_interop_gc_bridge_lref_get_log_file (bridge) != IntPtr.Zero; + logGlobalRefs = JreNativeMethods.java_interop_gc_bridge_gref_get_log_file (bridge) != IntPtr.Zero; } } public override int GlobalReferenceCount { - get {return NativeMethods.java_interop_gc_bridge_get_gref_count (bridge);} + get {return JreNativeMethods.java_interop_gc_bridge_get_gref_count (bridge);} } public override int WeakGlobalReferenceCount { - get {return NativeMethods.java_interop_gc_bridge_get_weak_gref_count (bridge);} + get {return JreNativeMethods.java_interop_gc_bridge_get_weak_gref_count (bridge);} } public override bool LogLocalReferenceMessages { @@ -36,8 +36,8 @@ public override void WriteLocalReferenceLine (string format, params object[] arg { if (!LogLocalReferenceMessages) return; - NativeMethods.java_interop_gc_bridge_lref_log_message (bridge, 0, string.Format (format, args)); - NativeMethods.java_interop_gc_bridge_lref_log_message (bridge, 0, "\n"); + JreNativeMethods.java_interop_gc_bridge_lref_log_message (bridge, 0, string.Format (format, args)); + JreNativeMethods.java_interop_gc_bridge_lref_log_message (bridge, 0, "\n"); } public override JniObjectReference CreateLocalReference (JniObjectReference reference, ref int localReferenceCount) @@ -46,7 +46,7 @@ public override JniObjectReference CreateLocalReference (JniObjectReference refe return reference; var r = base.CreateLocalReference (reference, ref localReferenceCount); - NativeMethods.java_interop_gc_bridge_lref_log_new (bridge, + JreNativeMethods.java_interop_gc_bridge_lref_log_new (bridge, localReferenceCount, reference.Handle, ToByte (reference.Type), @@ -76,7 +76,7 @@ public override void DeleteLocalReference (ref JniObjectReference reference, ref { if (!reference.IsValid) return; - NativeMethods.java_interop_gc_bridge_lref_log_delete (bridge, + JreNativeMethods.java_interop_gc_bridge_lref_log_delete (bridge, localReferenceCount, reference.Handle, ToByte (reference.Type), @@ -91,7 +91,7 @@ public override void CreatedLocalReference (JniObjectReference reference, ref in if (!reference.IsValid) return; base.CreatedLocalReference (reference, ref localReferenceCount); - NativeMethods.java_interop_gc_bridge_lref_log_new (bridge, + JreNativeMethods.java_interop_gc_bridge_lref_log_new (bridge, localReferenceCount, reference.Handle, ToByte (reference.Type), @@ -106,7 +106,7 @@ public override IntPtr ReleaseLocalReference (ref JniObjectReference reference, { if (!reference.IsValid) return IntPtr.Zero; - NativeMethods.java_interop_gc_bridge_lref_log_delete (bridge, + JreNativeMethods.java_interop_gc_bridge_lref_log_delete (bridge, localReferenceCount, reference.Handle, ToByte (reference.Type), @@ -124,8 +124,8 @@ public override void WriteGlobalReferenceLine (string format, params object?[]? { if (!LogGlobalReferenceMessages) return; - NativeMethods.java_interop_gc_bridge_gref_log_message (bridge, 0, string.Format (format, args!)); - NativeMethods.java_interop_gc_bridge_gref_log_message (bridge, 0, "\n"); + JreNativeMethods.java_interop_gc_bridge_gref_log_message (bridge, 0, string.Format (format, args!)); + JreNativeMethods.java_interop_gc_bridge_gref_log_message (bridge, 0, "\n"); } public override JniObjectReference CreateGlobalReference (JniObjectReference reference) @@ -133,7 +133,7 @@ public override JniObjectReference CreateGlobalReference (JniObjectReference ref if (!reference.IsValid) return reference; var n = base.CreateGlobalReference (reference); - NativeMethods.java_interop_gc_bridge_gref_log_new (bridge, + JreNativeMethods.java_interop_gc_bridge_gref_log_new (bridge, reference.Handle, ToByte (reference.Type), n.Handle, @@ -148,7 +148,7 @@ public override void DeleteGlobalReference (ref JniObjectReference reference) { if (!reference.IsValid) return; - NativeMethods.java_interop_gc_bridge_gref_log_delete (bridge, + JreNativeMethods.java_interop_gc_bridge_gref_log_delete (bridge, reference.Handle, ToByte (reference.Type), GetCurrentManagedThreadName (LogGlobalReferenceMessages), @@ -162,7 +162,7 @@ public override JniObjectReference CreateWeakGlobalReference (JniObjectReference if (!reference.IsValid) return reference; var n = base.CreateWeakGlobalReference (reference); - NativeMethods.java_interop_gc_bridge_weak_gref_log_new (bridge, + JreNativeMethods.java_interop_gc_bridge_weak_gref_log_new (bridge, reference.Handle, ToByte (reference.Type), n.Handle, @@ -177,7 +177,7 @@ public override void DeleteWeakGlobalReference (ref JniObjectReference reference { if (!reference.IsValid) return; - NativeMethods.java_interop_gc_bridge_weak_gref_log_delete (bridge, + JreNativeMethods.java_interop_gc_bridge_weak_gref_log_delete (bridge, reference.Handle, ToByte (reference.Type), GetCurrentManagedThreadName (LogGlobalReferenceMessages), @@ -202,7 +202,7 @@ static byte ToByte (JniObjectReferenceType type) } } - partial class NativeMethods { + partial class JreNativeMethods { [DllImport (JavaInteropLib, CallingConvention=CallingConvention.Cdecl)] internal static extern int java_interop_gc_bridge_get_gref_count (IntPtr bridge); diff --git a/src/Java.Runtime.Environment/Java.Interop/MonoRuntimeValueManager.cs b/src/Java.Runtime.Environment/Java.Interop/MonoRuntimeValueManager.cs index 223d03f4b..f0edf42d3 100644 --- a/src/Java.Runtime.Environment/Java.Interop/MonoRuntimeValueManager.cs +++ b/src/Java.Runtime.Environment/Java.Interop/MonoRuntimeValueManager.cs @@ -27,32 +27,32 @@ public override void OnSetRuntime (JniRuntime runtime) { base.OnSetRuntime (runtime); - bridge = NativeMethods.java_interop_gc_bridge_get_current (); + bridge = JreNativeMethods.java_interop_gc_bridge_get_current (); if (bridge != IntPtr.Zero) return; - bridge = NativeMethods.java_interop_gc_bridge_new (runtime.InvocationPointer); + bridge = JreNativeMethods.java_interop_gc_bridge_new (runtime.InvocationPointer); if (bridge == IntPtr.Zero) throw new NotSupportedException ("Could not initialize JNI::Mono GC Bridge!"); try { - if (NativeMethods.java_interop_gc_bridge_set_bridge_processing_field (bridge, typeof (MonoRuntimeValueManager).TypeHandle, nameof (GCBridgeProcessingIsActive)) < 0) + if (JreNativeMethods.java_interop_gc_bridge_set_bridge_processing_field (bridge, typeof (MonoRuntimeValueManager).TypeHandle, nameof (GCBridgeProcessingIsActive)) < 0) throw new NotSupportedException ("Could not set bridge processing field!"); foreach (var t in new[]{typeof (JavaObject), typeof (JavaException)}) { - if (NativeMethods.java_interop_gc_bridge_register_bridgeable_type (bridge, t.TypeHandle) < 0) + if (JreNativeMethods.java_interop_gc_bridge_register_bridgeable_type (bridge, t.TypeHandle) < 0) throw new NotSupportedException ("Could not register type " + t.FullName + "!"); } - if (NativeMethods.java_interop_gc_bridge_add_current_app_domain (bridge) < 0) + if (JreNativeMethods.java_interop_gc_bridge_add_current_app_domain (bridge) < 0) throw new NotSupportedException ("Could not register current AppDomain!"); - if (NativeMethods.java_interop_gc_bridge_set_current_once (bridge) < 0) + if (JreNativeMethods.java_interop_gc_bridge_set_current_once (bridge) < 0) throw new NotSupportedException ("Could not set GC Bridge instance!"); } catch (Exception) { - NativeMethods.java_interop_gc_bridge_free (bridge); + JreNativeMethods.java_interop_gc_bridge_free (bridge); bridge = IntPtr.Zero; throw; } - if (NativeMethods.java_interop_gc_bridge_register_hooks (bridge, GCBridgeUseWeakReferenceKind.Jni) < 0) + if (JreNativeMethods.java_interop_gc_bridge_register_hooks (bridge, GCBridgeUseWeakReferenceKind.Jni) < 0) throw new NotSupportedException ("Could not register GC Bridge with Mono!"); } @@ -60,7 +60,7 @@ public override void WaitForGCBridgeProcessing () { if (!GCBridgeProcessingIsActive) return; - NativeMethods.java_interop_gc_bridge_wait_for_bridge_processing (bridge); + JreNativeMethods.java_interop_gc_bridge_wait_for_bridge_processing (bridge); } public override void CollectPeers () @@ -92,7 +92,7 @@ protected override void Dispose (bool disposing) } if (bridge != IntPtr.Zero) { - NativeMethods.java_interop_gc_bridge_remove_current_app_domain (bridge); + JreNativeMethods.java_interop_gc_bridge_remove_current_app_domain (bridge); bridge = IntPtr.Zero; } } @@ -382,7 +382,7 @@ internal static void Collect () } } - partial class NativeMethods { + partial class JreNativeMethods { const string JavaInteropLib = "java-interop"; From 4741dc9e2b3957ad8f1ebadda8ccaafff5b96e8a Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Thu, 16 Nov 2023 14:33:11 -0800 Subject: [PATCH 23/44] Reduce patch size. --- tools/jnimarshalmethod-gen/App.cs | 36 ++++++++++++------------------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/tools/jnimarshalmethod-gen/App.cs b/tools/jnimarshalmethod-gen/App.cs index b75a8f416..5c0303675 100644 --- a/tools/jnimarshalmethod-gen/App.cs +++ b/tools/jnimarshalmethod-gen/App.cs @@ -227,7 +227,20 @@ void ProcessAssemblies (List assemblies) // loadContext = CreateLoadContext (); AppDomain.CurrentDomain.AssemblyResolve += (o, e) => { - return TryResolveAssembly (e.Name, e.RequestingAssembly); + Log (TraceLevel.Verbose, $"# jonp: resolving assembly: {e.Name}"); + var name = new AssemblyName (e.Name); + foreach (var d in resolver.SearchDirectories) { + var a = Path.Combine (d, name.Name); + var f = a + ".dll"; + if (File.Exists (f)) { + return Assembly.LoadFile (Path.GetFullPath (f)); + } + f = a + ".exe"; + if (File.Exists (f)) { + return Assembly.LoadFile (Path.GetFullPath (f)); + } + } + return null; }; foreach (var r in references) { @@ -330,27 +343,6 @@ static string ReadJavaSdkDirectoryFromJdkInfoProps () readonly string RefPathPart = $"{Path.DirectorySeparatorChar}ref{Path.DirectorySeparatorChar}"; - Assembly TryResolveAssembly (string assemblyFullName, Assembly requestingAssembly) - { - Log (TraceLevel.Verbose, $"# jonp: resolving assembly: {assemblyFullName}"); - var name = new AssemblyName (assemblyFullName); - foreach (var d in resolver.SearchDirectories) { - if (d.Contains (RefPathPart, StringComparison.OrdinalIgnoreCase)) { - continue; - } - var a = Path.Combine (d, name.Name); - var f = a + ".dll"; - if (File.Exists (f)) { - return Assembly.LoadFile (Path.GetFullPath (f)); - } - f = a + ".exe"; - if (File.Exists (f)) { - return Assembly.LoadFile (Path.GetFullPath (f)); - } - } - return null; - } - AssemblyLoadContext CreateLoadContext () { var c = new AssemblyLoadContext ("jnimarshalmethod-gen", isCollectible: true); From 8eb2850f28c2279d7d08dd4f72149d0f99e97f78 Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Thu, 16 Nov 2023 14:34:57 -0800 Subject: [PATCH 24/44] Remove more unneeded bits. --- tools/jnimarshalmethod-gen/App.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/tools/jnimarshalmethod-gen/App.cs b/tools/jnimarshalmethod-gen/App.cs index 5c0303675..fc43233c8 100644 --- a/tools/jnimarshalmethod-gen/App.cs +++ b/tools/jnimarshalmethod-gen/App.cs @@ -341,8 +341,6 @@ static string ReadJavaSdkDirectoryFromJdkInfoProps () return jdkJvmPath.Value; } - readonly string RefPathPart = $"{Path.DirectorySeparatorChar}ref{Path.DirectorySeparatorChar}"; - AssemblyLoadContext CreateLoadContext () { var c = new AssemblyLoadContext ("jnimarshalmethod-gen", isCollectible: true); From 1e71988e2164728e388386f362be65348725b019 Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Thu, 23 Nov 2023 16:35:39 -0500 Subject: [PATCH 25/44] Remove GetTypeFromAssemblyQualifiedName() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes: https://github.com/xamarin/java.interop/issues/1165 Context: https://github.com/xamarin/java.interop/issues/1157 If we more strongly rely on JNI signatures, we can remove the need for Java Callable Wrappers to contain assembly-qualified type names, thus removing the need for `ManagedPeer` to use `Type.GetType()` entirely, removing the [IL2057][0] warnings. Furthermore, if we add `[DynamicallyAccessedMembers]` to `JniRuntime.JniTypeManager.GetType()`, we can fix some [IL2075][1] warnings which appeared after fixing the IL2057 warnings. Aside: Excising assembly-qualified type names from Java Callable Wrappers had some "interesting" knock-on effects in the unit tests, requiring that more typemap information be explicitly provided. (This same information was *implicitly* provided before, via the provision of assembly-qualified type names everywhere…) [0]: https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/trim-warnings/IL2057 [1]: https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/trim-warnings/il2075 --- .../NativeAotTypeManager.cs | 12 ------ .../JavaCallableWrapperGenerator.cs | 8 +--- .../Java.Interop/JavaProxyObject.cs | 4 ++ .../Java.Interop/JniBuiltinMarshalers.cs | 4 ++ .../Java.Interop/JniBuiltinMarshalers.tt | 4 ++ .../Java.Interop/JniMemberSignature.cs | 16 ++++++++ .../JniPeerMembers.JniInstanceMethods.cs | 39 +++++++++++++++++-- .../Java.Interop/JniRuntime.JniTypeManager.cs | 13 +------ src/Java.Interop/Java.Interop/ManagedPeer.cs | 37 +++++++++++------- .../java/net/dot/jni/ManagedPeer.java | 2 - .../net/dot/jni/internal/JavaProxyObject.java | 2 - .../dot/jni/internal/JavaProxyThrowable.java | 2 - .../Java.Base/JavaVMFixture.cs | 1 + .../Java.Interop/JavaVMFixture.cs | 3 ++ .../CallVirtualFromConstructorDerived.java | 5 +-- .../java/net/dot/jni/test/GetThis.java | 5 +-- .../java/net/dot/jni/test/TestType.java | 8 +--- .../Java.Interop/ExportTest.cs | 4 +- .../Java.Interop/JavaVMFixture.cs | 6 ++- .../java/net/dot/jni/test/ExportType.java | 2 - .../JavaCallableWrapperGeneratorTests.cs | 4 +- 21 files changed, 107 insertions(+), 74 deletions(-) diff --git a/samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs b/samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs index 1b08f9a23..c80922dfa 100644 --- a/samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs +++ b/samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs @@ -6,23 +6,11 @@ class NativeAotTypeManager : JniRuntime.JniTypeManager { #pragma warning disable IL2026 Dictionary typeMappings = new () { - ["com/xamarin/java_interop/internal/JavaProxyThrowable"] = Type.GetType ("Java.Interop.JavaProxyThrowable, Java.Interop", throwOnError: true)!, ["example/ManagedType"] = typeof (Example.ManagedType), - ["System.Int32, System.Runtime"] = typeof (int), - ["System.Int32, System.Private.CoreLib"] = typeof (int), - ["Java.Interop.JavaProxyThrowable, Java.Interop"] = Type.GetType ("Java.Interop.JavaProxyThrowable, Java.Interop", throwOnError: true)!, - ["Example.ManagedType, Hello-NativeAOTFromJNI"] = typeof (Example.ManagedType), }; #pragma warning restore IL2026 - public override Type GetTypeFromAssemblyQualifiedName (string assemblyQualifiedTypeName) - { - if (typeMappings.TryGetValue (assemblyQualifiedTypeName, out var type)) - return type; - throw new NotSupportedException ($"Unsupported type: \"{assemblyQualifiedTypeName}\"!"); - } - protected override IEnumerable GetTypesForSimpleReference (string jniSimpleReference) { if (typeMappings.TryGetValue (jniSimpleReference, out var target)) diff --git a/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers/JavaCallableWrapperGenerator.cs b/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers/JavaCallableWrapperGenerator.cs index 59d434ed0..e103e9958 100644 --- a/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers/JavaCallableWrapperGenerator.cs +++ b/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers/JavaCallableWrapperGenerator.cs @@ -825,9 +825,7 @@ void GenerateRegisterType (TextWriter sw, JavaCallableWrapperGenerator self, str case JavaPeerStyle.JavaInterop1: sw.Write ("net.dot.jni.ManagedPeer.registerNativeMembers ("); sw.Write (self.name); - sw.Write (".class, \""); - sw.Write (managedTypeName); - sw.Write ("\", "); + sw.Write (".class, "); sw.Write (field); sw.WriteLine (");"); break; @@ -1025,9 +1023,7 @@ void GenerateConstructor (Signature ctor, TextWriter sw) switch (CodeGenerationTarget) { case JavaPeerStyle.JavaInterop1: sw.Write ("net.dot.jni.ManagedPeer.construct (this, \""); - sw.Write (type.GetPartialAssemblyQualifiedName (cache)); - sw.Write ("\", \""); - sw.Write (ctor.ManagedParameters); + sw.Write (ctor.JniSignature); sw.Write ("\", new java.lang.Object[] { "); sw.Write (ctor.ActivateCall); sw.WriteLine (" });"); diff --git a/src/Java.Interop/Java.Interop/JavaProxyObject.cs b/src/Java.Interop/Java.Interop/JavaProxyObject.cs index 00f0ff86d..28f48ef7a 100644 --- a/src/Java.Interop/Java.Interop/JavaProxyObject.cs +++ b/src/Java.Interop/Java.Interop/JavaProxyObject.cs @@ -3,6 +3,7 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace Java.Interop { @@ -72,6 +73,7 @@ public override bool Equals (object? obj) } // TODO: Keep in sync with the code generated by ExportedMemberBuilder + [UnmanagedFunctionPointer (CallingConvention.Winapi)] delegate bool EqualsMarshalMethod (IntPtr jnienv, IntPtr n_self, IntPtr n_value); static bool Equals (IntPtr jnienv, IntPtr n_self, IntPtr n_value) { @@ -92,6 +94,7 @@ static bool Equals (IntPtr jnienv, IntPtr n_self, IntPtr n_value) } // TODO: Keep in sync with the code generated by ExportedMemberBuilder + [UnmanagedFunctionPointer (CallingConvention.Winapi)] delegate int GetHashCodeMarshalMethod (IntPtr jnienv, IntPtr n_self); static int GetHashCode (IntPtr jnienv, IntPtr n_self) { @@ -109,6 +112,7 @@ static int GetHashCode (IntPtr jnienv, IntPtr n_self) } } + [UnmanagedFunctionPointer (CallingConvention.Winapi)] delegate IntPtr ToStringMarshalMethod (IntPtr jnienv, IntPtr n_self); static IntPtr ToString (IntPtr jnienv, IntPtr n_self) { diff --git a/src/Java.Interop/Java.Interop/JniBuiltinMarshalers.cs b/src/Java.Interop/Java.Interop/JniBuiltinMarshalers.cs index fd6b78712..b75e46b6d 100644 --- a/src/Java.Interop/Java.Interop/JniBuiltinMarshalers.cs +++ b/src/Java.Interop/Java.Interop/JniBuiltinMarshalers.cs @@ -130,6 +130,9 @@ static Dictionary InitJniBuiltinSimpleReferenceToType () { return new Dictionary (StringComparer.Ordinal) { {"java/lang/String", typeof (string)}, + {"net/dot/jni/internal/JavaProxyObject", typeof (JavaProxyObject)}, + {"net/dot/jni/internal/JavaProxyThrowable", typeof (JavaProxyThrowable)}, + {"net/dot/jni/ManagedPeer", typeof (ManagedPeer)}, {"V", typeof (void)}, {"Z", typeof (Boolean)}, {"java/lang/Boolean", typeof (Boolean?)}, @@ -156,6 +159,7 @@ static KeyValuePair[] InitJniBuiltinMarshalers () { return new []{ new KeyValuePair(typeof (string), JniStringValueMarshaler.Instance), + new KeyValuePair(typeof (JavaProxyObject), ProxyValueMarshaler.Instance), new KeyValuePair(typeof (Boolean), JniBooleanValueMarshaler.Instance), new KeyValuePair(typeof (Boolean?), JniNullableBooleanValueMarshaler.Instance), new KeyValuePair(typeof (SByte), JniSByteValueMarshaler.Instance), diff --git a/src/Java.Interop/Java.Interop/JniBuiltinMarshalers.tt b/src/Java.Interop/Java.Interop/JniBuiltinMarshalers.tt index a3cf69968..d5910cf62 100644 --- a/src/Java.Interop/Java.Interop/JniBuiltinMarshalers.tt +++ b/src/Java.Interop/Java.Interop/JniBuiltinMarshalers.tt @@ -101,6 +101,9 @@ namespace Java.Interop { { return new Dictionary (StringComparer.Ordinal) { {"java/lang/String", typeof (string)}, + {"net/dot/jni/internal/JavaProxyObject", typeof (JavaProxyObject)}, + {"net/dot/jni/internal/JavaProxyThrowable", typeof (JavaProxyThrowable)}, + {"net/dot/jni/ManagedPeer", typeof (ManagedPeer)}, {"V", typeof (void)}, <# foreach (var type in types) { @@ -119,6 +122,7 @@ namespace Java.Interop { { return new []{ new KeyValuePair(typeof (string), JniStringValueMarshaler.Instance), + new KeyValuePair(typeof (JavaProxyObject), ProxyValueMarshaler.Instance), <# foreach (var type in types) { #> diff --git a/src/Java.Interop/Java.Interop/JniMemberSignature.cs b/src/Java.Interop/Java.Interop/JniMemberSignature.cs index 946cf7bb5..30ba2b4c5 100644 --- a/src/Java.Interop/Java.Interop/JniMemberSignature.cs +++ b/src/Java.Interop/Java.Interop/JniMemberSignature.cs @@ -34,6 +34,22 @@ public JniMemberSignature (string memberName, string memberSignature) this.memberSignature = memberSignature; } + internal static IEnumerable GetParameterTypesFromMethodSignature (string jniMethodSignature) + { + if (jniMethodSignature.Length < "()V".Length || jniMethodSignature [0] != '(' ) { + throw new ArgumentException ( + $"Member signature `{jniMethodSignature}` is not a method signature. Method signatures must start with `(`.", + nameof (jniMethodSignature)); + } + int index = 1; + while (index < jniMethodSignature.Length && + jniMethodSignature [index] != ')') { + var (start, length) = ExtractType (jniMethodSignature, ref index); + var jniType = jniMethodSignature.Substring (start, length); + yield return JniTypeSignature.Parse (jniType); + } + } + public static int GetParameterCountFromMethodSignature (string jniMethodSignature) { if (jniMethodSignature.Length < "()V".Length || jniMethodSignature [0] != '(' ) { diff --git a/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs b/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs index cee7af696..04d96870e 100644 --- a/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs +++ b/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs @@ -72,11 +72,42 @@ internal JniInstanceMethods GetConstructorsForType (Type declaringType) if (declaringType == DeclaringType) return this; + JniInstanceMethods? methods; + lock (SubclassConstructors) { - if (!SubclassConstructors.TryGetValue (declaringType, out var methods)) { - methods = new JniInstanceMethods (declaringType); - SubclassConstructors.Add (declaringType, methods); - } + if (SubclassConstructors.TryGetValue (declaringType, out methods)) + return methods; + } + // Init outside of `lock` in case we have recursive access: + // System.ArgumentException: An item with the same key has already been added. Key: Java.Interop.JavaProxyThrowable + // at System.Collections.Generic.Dictionary`2.TryInsert(TKey key, TValue value, InsertionBehavior behavior) + // at System.Collections.Generic.Dictionary`2.Add(TKey key, TValue value) + // at Java.Interop.JniPeerMembers.JniInstanceMethods.GetConstructorsForType(Type declaringType) in /Users/jon/Developer/src/xamarin/java.interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs:line 80 + // at Java.Interop.JniPeerMembers.JniInstanceMethods.GetConstructorsForType(Type declaringType) in /Users/jon/Developer/src/xamarin/java.interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs:line 80 + // at Java.Interop.JniPeerMembers.JniInstanceMethods.StartCreateInstance(String constructorSignature, Type declaringType, JniArgumentValue* parameters) in /Users/jon/Developer/src/xamarin/java.interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs:line 146 + // at Java.Interop.JavaException..ctor(String message) in /Users/jon/Developer/src/xamarin/java.interop/src/Java.Interop/Java.Interop/JavaException.cs:line 52 + // at Java.Interop.JavaProxyThrowable..ctor(Exception exception) in /Users/jon/Developer/src/xamarin/java.interop/src/Java.Interop/Java.Interop/JavaProxyThrowable.cs:line 15 + // at Java.Interop.JniEnvironment.Exceptions.Throw(Exception e) in /Users/jon/Developer/src/xamarin/java.interop/src/Java.Interop/Java.Interop/JniEnvironment.Errors.cs:line 39 + // at Java.Interop.JniRuntime.RaisePendingException(Exception pendingException) in /Users/jon/Developer/src/xamarin/java.interop/src/Java.Interop/Java.Interop/JniRuntime.cs:line 444 + // at Java.Interop.JniTransition.Dispose() in /Users/jon/Developer/src/xamarin/java.interop/src/Java.Interop/Java.Interop/JniTransition.cs:line 39 + // at Java.Interop.ManagedPeer.RegisterNativeMembers(IntPtr jnienv, IntPtr klass, IntPtr n_nativeClass, IntPtr n_methods) in /Users/jon/Developer/src/xamarin/java.interop/src/Java.Interop/Java.Interop/ManagedPeer.cs:line 195 + // at Java.Interop.NativeMethods.java_interop_jnienv_find_class(IntPtr jnienv, IntPtr& thrown, String classname) + // at Java.Interop.NativeMethods.java_interop_jnienv_find_class(IntPtr jnienv, IntPtr& thrown, String classname) + // at Java.Interop.JniEnvironment.Types.TryRawFindClass(IntPtr env, String classname, IntPtr& klass, IntPtr& thrown) in /Users/jon/Developer/src/xamarin/java.interop/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs:line 135 + // at Java.Interop.JniEnvironment.Types.TryFindClass(String classname, Boolean throwOnError) in /Users/jon/Developer/src/xamarin/java.interop/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs:line 49 + // at Java.Interop.JniEnvironment.Types.FindClass(String classname) in /Users/jon/Developer/src/xamarin/java.interop/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs:line 37 + // at Java.Interop.JniType..ctor(String classname) in /Users/jon/Developer/src/xamarin/java.interop/src/Java.Interop/Java.Interop/JniType.cs:line 51 + // at Java.Interop.JniPeerMembers.JniInstanceMethods..ctor(Type declaringType) in /Users/jon/Developer/src/xamarin/java.interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs:line 27 + // at Java.Interop.JniPeerMembers.JniInstanceMethods.GetConstructorsForType(Type declaringType) in /Users/jon/Developer/src/xamarin/java.interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs:line 77 + // at Java.Interop.JniPeerMembers.JniInstanceMethods.StartCreateInstance(String constructorSignature, Type declaringType, JniArgumentValue* parameters) in /Users/jon/Developer/src/xamarin/java.interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs:line 146 + // at Java.Lang.Object..ctor() in /Users/jon/Developer/src/xamarin/java.interop/src/Java.Base/obj/Debug-net7.0/mcw/Java.Lang.Object.cs:line 32 + // at Java.BaseTests.MyIntConsumer..ctor(Action`1 action) in /Users/jon/Developer/src/xamarin/java.interop/tests/Java.Base-Tests/Java.Base/JavaToManagedTests.cs:line 77 + // at Java.BaseTests.JavaToManagedTests.InterfaceInvokerMethod() in /Users/jon/Developer/src/xamarin/java.interop/tests/Java.Base-Tests/Java.Base/JavaToManagedTests.cs:line 26 + methods = new JniInstanceMethods (declaringType); + lock (SubclassConstructors) { + if (SubclassConstructors.TryGetValue (declaringType, out var m)) + return m; + SubclassConstructors.Add (declaringType, methods); return methods; } } diff --git a/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs b/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs index 24788a541..616f9ed67 100644 --- a/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs +++ b/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs @@ -267,6 +267,7 @@ protected virtual IEnumerable GetSimpleReferences (Type type) static readonly Type[] EmptyTypeArray = Array.Empty (); + [return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] public Type? GetType (JniTypeSignature typeSignature) { AssertValid (); @@ -372,18 +373,6 @@ IEnumerable CreateGetTypesForSimpleReferenceEnumerator (string jniSimpleRe protected virtual ReplacementMethodInfo? GetReplacementMethodInfoCore (string jniSimpleReference, string jniMethodName, string jniMethodSignature) => null; -#if FEATURE_NATIVE_AOT - public virtual -#else // FEATURE_NATIVE_AOT - internal -#endif // FEATURE_NATIVE_AOT - Type GetTypeFromAssemblyQualifiedName (string assemblyQualifiedTypeName) - { - AssertValid (); - - return Type.GetType (assemblyQualifiedTypeName, throwOnError: true)!; - } - public virtual void RegisterNativeMembers (JniType nativeClass, Type type, ReadOnlySpan methods) { TryRegisterNativeMembers (nativeClass, type, methods); diff --git a/src/Java.Interop/Java.Interop/ManagedPeer.cs b/src/Java.Interop/Java.Interop/ManagedPeer.cs index d01fd9092..79fb63ba2 100644 --- a/src/Java.Interop/Java.Interop/ManagedPeer.cs +++ b/src/Java.Interop/Java.Interop/ManagedPeer.cs @@ -2,6 +2,7 @@ using System; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -48,21 +49,19 @@ public override JniPeerMembers JniPeerMembers { get {return _members;} } - const string ConstructSignature = "(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;[Ljava/lang/Object;)V"; + const string ConstructSignature = "(Ljava/lang/Object;Ljava/lang/String;[Ljava/lang/Object;)V"; // TODO: Keep in sync with the code generated by ExportedMemberBuilder [UnmanagedFunctionPointer (CallingConvention.Winapi)] delegate void ConstructMarshalMethod (IntPtr jnienv, IntPtr klass, IntPtr n_self, - IntPtr n_assemblyQualifiedName, IntPtr n_constructorSignature, IntPtr n_constructorArguments); static void Construct ( IntPtr jnienv, IntPtr klass, IntPtr n_self, - IntPtr n_assemblyQualifiedName, IntPtr n_constructorSignature, IntPtr n_constructorArguments) { @@ -93,7 +92,9 @@ static void Construct ( return; } - var type = typeMgr.GetTypeFromAssemblyQualifiedName (JniEnvironment.Strings.ToString (n_assemblyQualifiedName)!); + var typeSig = new JniTypeSignature (JniEnvironment.Types.GetJniTypeNameFromInstance (r_self)); + var type = GetTypeFromSignature (typeMgr, typeSig); + if (type.IsGenericTypeDefinition) { throw new NotSupportedException ( "Constructing instances of generic types from Java is not supported, as the type parameters cannot be determined.", @@ -153,10 +154,11 @@ static Type[] GetParameterTypes (JniRuntime.JniTypeManager typeMgr, string? sign { if (string.IsNullOrEmpty (signature)) return Array.Empty (); - var typeNames = signature!.Split (':'); - var ptypes = new Type [typeNames.Length]; - for (int i = 0; i < typeNames.Length; i++) - ptypes [i] = typeMgr.GetTypeFromAssemblyQualifiedName (typeNames [i]); + var ptypes = new Type [JniMemberSignature.GetParameterCountFromMethodSignature (signature)]; + int i = 0; + foreach (var jniType in JniMemberSignature.GetParameterTypesFromMethodSignature (signature)) { + ptypes [i++] = GetTypeFromSignature (typeMgr, jniType, signature); + } return ptypes; } @@ -178,32 +180,33 @@ static Type[] GetParameterTypes (JniRuntime.JniTypeManager typeMgr, string? sign return pvalues; } - const string RegisterNativeMembersSignature = "(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;)V"; + const string RegisterNativeMembersSignature = "(Ljava/lang/Class;Ljava/lang/String;)V"; [UnmanagedFunctionPointer (CallingConvention.Winapi)] delegate void RegisterMarshalMethod (IntPtr jnienv, IntPtr klass, IntPtr n_nativeClass, - IntPtr n_assemblyQualifiedName, IntPtr n_methods); static unsafe void RegisterNativeMembers ( IntPtr jnienv, IntPtr klass, IntPtr n_nativeClass, - IntPtr n_assemblyQualifiedName, IntPtr n_methods) { var envp = new JniTransition (jnienv); try { var r_nativeClass = new JniObjectReference (n_nativeClass); +#pragma warning disable CA2000 var nativeClass = new JniType (ref r_nativeClass, JniObjectReferenceOptions.Copy); +#pragma warning restore CA2000 - var assemblyQualifiedName = JniEnvironment.Strings.ToString (new JniObjectReference (n_assemblyQualifiedName)); var methodsRef = new JniObjectReference (n_methods); + var typeSig = new JniTypeSignature (nativeClass.Name); + var type = GetTypeFromSignature (JniEnvironment.Runtime.TypeManager, typeSig); + #if NET - var type = JniEnvironment.Runtime.TypeManager.GetTypeFromAssemblyQualifiedName (assemblyQualifiedName!); int methodsLength = JniEnvironment.Strings.GetStringLength (methodsRef); var methodsChars = JniEnvironment.Strings.GetStringChars (methodsRef, null); @@ -215,7 +218,6 @@ static unsafe void RegisterNativeMembers ( JniEnvironment.Strings.ReleaseStringChars (methodsRef, methodsChars); } #else // NET - var type = Type.GetType (assemblyQualifiedName!, throwOnError: true)!; var methods = JniEnvironment.Strings.ToString (methodsRef); JniEnvironment.Runtime.TypeManager.RegisterNativeMembers (nativeClass, type, methods); #endif // NET @@ -229,6 +231,13 @@ static unsafe void RegisterNativeMembers ( envp.Dispose (); } } + + [return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] + static Type GetTypeFromSignature (JniRuntime.JniTypeManager typeManager, JniTypeSignature typeSignature, string? context = null) + { + return typeManager.GetType (typeSignature) ?? + throw new NotSupportedException ($"Could not find System.Type corresponding to Java type {typeSignature} {(context == null ? "" : "within `" + context + "`")}."); + } } sealed class JniLocationException : Exception { diff --git a/src/Java.Interop/java/net/dot/jni/ManagedPeer.java b/src/Java.Interop/java/net/dot/jni/ManagedPeer.java index 50ff8aae6..dc66d3122 100644 --- a/src/Java.Interop/java/net/dot/jni/ManagedPeer.java +++ b/src/Java.Interop/java/net/dot/jni/ManagedPeer.java @@ -6,12 +6,10 @@ private ManagedPeer () { public static native void registerNativeMembers ( java.lang.Class nativeClass, - String assemblyQualifiedName, String methods); public static native void construct ( Object self, - String assemblyQualifiedName, String constructorSignature, Object... arguments ); diff --git a/src/Java.Interop/java/net/dot/jni/internal/JavaProxyObject.java b/src/Java.Interop/java/net/dot/jni/internal/JavaProxyObject.java index 5e7122789..013cb0708 100644 --- a/src/Java.Interop/java/net/dot/jni/internal/JavaProxyObject.java +++ b/src/Java.Interop/java/net/dot/jni/internal/JavaProxyObject.java @@ -8,11 +8,9 @@ extends java.lang.Object implements GCUserPeerable { - static final String assemblyQualifiedName = "Java.Interop.JavaProxyObject, Java.Interop"; static { net.dot.jni.ManagedPeer.registerNativeMembers ( JavaProxyObject.class, - assemblyQualifiedName, ""); } diff --git a/src/Java.Interop/java/net/dot/jni/internal/JavaProxyThrowable.java b/src/Java.Interop/java/net/dot/jni/internal/JavaProxyThrowable.java index 56abfc00a..066025ac5 100644 --- a/src/Java.Interop/java/net/dot/jni/internal/JavaProxyThrowable.java +++ b/src/Java.Interop/java/net/dot/jni/internal/JavaProxyThrowable.java @@ -8,11 +8,9 @@ extends java.lang.Error implements GCUserPeerable { - static final String assemblyQualifiedName = "Java.Interop.JavaProxyThrowable, Java.Interop"; static { net.dot.jni.ManagedPeer.registerNativeMembers ( JavaProxyThrowable.class, - assemblyQualifiedName, ""); } diff --git a/tests/Java.Base-Tests/Java.Base/JavaVMFixture.cs b/tests/Java.Base-Tests/Java.Base/JavaVMFixture.cs index f1729d6cb..63d9a5ced 100644 --- a/tests/Java.Base-Tests/Java.Base/JavaVMFixture.cs +++ b/tests/Java.Base-Tests/Java.Base/JavaVMFixture.cs @@ -17,6 +17,7 @@ static partial void CreateJavaVM () var c = new TestJVM ( jars: new[]{ "java.base-tests.jar" }, typeMappings: new Dictionary () { + ["example/MyIntConsumer"] = typeof (MyIntConsumer), ["example/MyRunnable"] = typeof (MyRunnable), [JavaInvoker.JniTypeName] = typeof (JavaInvoker), } diff --git a/tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.cs b/tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.cs index 2aea0aac7..ba74d62c8 100644 --- a/tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.cs +++ b/tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.cs @@ -37,6 +37,9 @@ class JavaVMFixtureTypeManager : JniRuntime.JniTypeManager { [GenericHolder.JniTypeName] = typeof (GenericHolder<>), [RenameClassBase.JniTypeName] = typeof (RenameClassBase), [RenameClassDerived.JniTypeName] = typeof (RenameClassDerived), + [CallVirtualFromConstructorBase.JniTypeName] = typeof (CallVirtualFromConstructorBase), + [CallVirtualFromConstructorDerived.JniTypeName] = typeof (CallVirtualFromConstructorDerived), + [GetThis.JniTypeName] = typeof (GetThis), }; public JavaVMFixtureTypeManager () diff --git a/tests/Java.Interop-Tests/java/net/dot/jni/test/CallVirtualFromConstructorDerived.java b/tests/Java.Interop-Tests/java/net/dot/jni/test/CallVirtualFromConstructorDerived.java index 44bab4560..5ae143daf 100644 --- a/tests/Java.Interop-Tests/java/net/dot/jni/test/CallVirtualFromConstructorDerived.java +++ b/tests/Java.Interop-Tests/java/net/dot/jni/test/CallVirtualFromConstructorDerived.java @@ -8,11 +8,9 @@ public class CallVirtualFromConstructorDerived extends CallVirtualFromConstructorBase implements GCUserPeerable { - static final String assemblyQualifiedName = "Java.InteropTests.CallVirtualFromConstructorDerived, Java.Interop-Tests"; static { net.dot.jni.ManagedPeer.registerNativeMembers ( CallVirtualFromConstructorDerived.class, - assemblyQualifiedName, ""); } @@ -23,8 +21,7 @@ public CallVirtualFromConstructorDerived (int value) { if (CallVirtualFromConstructorDerived.class == getClass ()) { net.dot.jni.ManagedPeer.construct ( this, - assemblyQualifiedName, - "System.Int32", + "(I)V", value ); } diff --git a/tests/Java.Interop-Tests/java/net/dot/jni/test/GetThis.java b/tests/Java.Interop-Tests/java/net/dot/jni/test/GetThis.java index 3013ebb75..f3a27d025 100644 --- a/tests/Java.Interop-Tests/java/net/dot/jni/test/GetThis.java +++ b/tests/Java.Interop-Tests/java/net/dot/jni/test/GetThis.java @@ -6,11 +6,9 @@ public class GetThis implements GCUserPeerable { - static final String assemblyQualifiedName = "Java.InteropTests.GetThis, Java.Interop-Tests"; static { net.dot.jni.ManagedPeer.registerNativeMembers ( GetThis.class, - assemblyQualifiedName, ""); } @@ -20,8 +18,7 @@ public GetThis () { if (GetThis.class == getClass ()) { net.dot.jni.ManagedPeer.construct ( this, - assemblyQualifiedName, - "" + "()V" ); } } diff --git a/tests/Java.Interop-Tests/java/net/dot/jni/test/TestType.java b/tests/Java.Interop-Tests/java/net/dot/jni/test/TestType.java index adf4eebc7..456bef7b5 100644 --- a/tests/Java.Interop-Tests/java/net/dot/jni/test/TestType.java +++ b/tests/Java.Interop-Tests/java/net/dot/jni/test/TestType.java @@ -6,11 +6,9 @@ public class TestType implements GCUserPeerable { - static final String assemblyQualifiedName = "Java.InteropTests.TestType, Java.Interop-Tests"; static { net.dot.jni.ManagedPeer.registerNativeMembers ( TestType.class, - assemblyQualifiedName, ""); } @@ -20,8 +18,7 @@ public TestType () { if (TestType.class == getClass ()) { net.dot.jni.ManagedPeer.construct ( this, - assemblyQualifiedName, - "" + "()V" ); } } @@ -31,8 +28,7 @@ public TestType (TestType a, int b) { if (TestType.class == getClass ()) { net.dot.jni.ManagedPeer.construct ( this, - assemblyQualifiedName, - assemblyQualifiedName + ":System.Int32", + "(Lnet/dot/jni/test/TestType;I)V", a, b ); } diff --git a/tests/Java.Interop.Export-Tests/Java.Interop/ExportTest.cs b/tests/Java.Interop.Export-Tests/Java.Interop/ExportTest.cs index 69c5ca490..9c709a38b 100644 --- a/tests/Java.Interop.Export-Tests/Java.Interop/ExportTest.cs +++ b/tests/Java.Interop.Export-Tests/Java.Interop/ExportTest.cs @@ -30,9 +30,11 @@ long p namespace Java.InteropTests { - [JniTypeSignature ("net/dot/jni/test/ExportType")] + [JniTypeSignature (JniTypeName)] public class ExportTest : JavaObject { + internal const string JniTypeName = "net/dot/jni/test/ExportType"; + [JniAddNativeMethodRegistrationAttribute] static void RegisterNativeMembers (JniNativeMethodRegistrationArguments args) { diff --git a/tests/Java.Interop.Export-Tests/Java.Interop/JavaVMFixture.cs b/tests/Java.Interop.Export-Tests/Java.Interop/JavaVMFixture.cs index 0f252b2b1..c22d6dae1 100644 --- a/tests/Java.Interop.Export-Tests/Java.Interop/JavaVMFixture.cs +++ b/tests/Java.Interop.Export-Tests/Java.Interop/JavaVMFixture.cs @@ -12,7 +12,11 @@ public abstract partial class JavaVMFixture { static JavaVMFixture () { var c = new TestJVM ( - jars: new[]{ "export-test.jar" } + jars: new[]{ "export-test.jar" }, + typeMappings: new() { + [ExportTest.JniTypeName] = typeof (ExportTest), + [JavaCallableExample.TypeSignature] = typeof (JavaCallableExample), + } ); JniRuntime.SetCurrent (c); } diff --git a/tests/Java.Interop.Export-Tests/java/net/dot/jni/test/ExportType.java b/tests/Java.Interop.Export-Tests/java/net/dot/jni/test/ExportType.java index 1d12e7092..28e1f3b55 100644 --- a/tests/Java.Interop.Export-Tests/java/net/dot/jni/test/ExportType.java +++ b/tests/Java.Interop.Export-Tests/java/net/dot/jni/test/ExportType.java @@ -8,11 +8,9 @@ public class ExportType implements GCUserPeerable { - static final String assemblyQualifiedName = "Java.InteropTests.ExportTest, Java.Interop.Export-Tests"; static { net.dot.jni.ManagedPeer.registerNativeMembers ( ExportType.class, - assemblyQualifiedName, ""); } diff --git a/tests/Java.Interop.Tools.JavaCallableWrappers-Tests/Java.Interop.Tools.JavaCallableWrappers/JavaCallableWrapperGeneratorTests.cs b/tests/Java.Interop.Tools.JavaCallableWrappers-Tests/Java.Interop.Tools.JavaCallableWrappers/JavaCallableWrapperGeneratorTests.cs index 0a8702c76..e64033543 100644 --- a/tests/Java.Interop.Tools.JavaCallableWrappers-Tests/Java.Interop.Tools.JavaCallableWrappers/JavaCallableWrapperGeneratorTests.cs +++ b/tests/Java.Interop.Tools.JavaCallableWrappers-Tests/Java.Interop.Tools.JavaCallableWrappers/JavaCallableWrapperGeneratorTests.cs @@ -641,7 +641,7 @@ extends java.lang.Object __md_methods = ""n_Example:()V:__export__\n"" + """"; - net.dot.jni.ManagedPeer.registerNativeMembers (JavaInteropExample.class, ""Xamarin.Android.ToolsTests.JavaInteropExample, Java.Interop.Tools.JavaCallableWrappers-Tests"", __md_methods); + net.dot.jni.ManagedPeer.registerNativeMembers (JavaInteropExample.class, __md_methods); } @@ -649,7 +649,7 @@ public JavaInteropExample (int p0, int p1) { super (); if (getClass () == JavaInteropExample.class) { - net.dot.jni.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 }); + net.dot.jni.ManagedPeer.construct (this, ""(II)V"", new java.lang.Object[] { p0, p1 }); } } From 14f67137a14bcc5b2bf16e3e0a175e0def4c631c Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Sat, 2 Dec 2023 18:20:35 -0600 Subject: [PATCH 26/44] Cleanup. --- .../Hello-NativeAOTFromJNI.targets | 2 +- samples/Hello-NativeAOTFromJNI/README.md | 114 +++++++----------- 2 files changed, 47 insertions(+), 69 deletions(-) diff --git a/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets b/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets index db9707708..28b22bc80 100644 --- a/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets +++ b/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets @@ -2,7 +2,7 @@ $(DOTNET_HOST_PATH) - $(MSBuildThisFileDirectory)..\..\bin\Debug-net7.0 + $(MSBuildThisFileDirectory)..\..\bin\$(Configuration)-net7.0 Date: Thu, 15 Feb 2024 19:21:14 -0500 Subject: [PATCH 27/44] Fix spelling of JNI_OnUnload --- samples/Hello-NativeAOTFromJNI/JavaInteropRuntime.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/Hello-NativeAOTFromJNI/JavaInteropRuntime.cs b/samples/Hello-NativeAOTFromJNI/JavaInteropRuntime.cs index a7b7b4b2e..c956536df 100644 --- a/samples/Hello-NativeAOTFromJNI/JavaInteropRuntime.cs +++ b/samples/Hello-NativeAOTFromJNI/JavaInteropRuntime.cs @@ -15,7 +15,7 @@ static int JNI_OnLoad (IntPtr vm, IntPtr reserved) } [UnmanagedCallersOnly (EntryPoint="JNI_OnUnload")] - static void JNI_Onload (IntPtr vm, IntPtr reserved) + static void JNI_OnUnload (IntPtr vm, IntPtr reserved) { runtime?.Dispose (); } From cfdfeaf405a01b9003fa939b63d36b8d26a14129 Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Thu, 15 Feb 2024 19:37:35 -0500 Subject: [PATCH 28/44] Reduce unnecessary changes. --- src/Java.Interop/Java.Interop.csproj | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/Java.Interop/Java.Interop.csproj b/src/Java.Interop/Java.Interop.csproj index 1cb9d33eb..80eb91aee 100644 --- a/src/Java.Interop/Java.Interop.csproj +++ b/src/Java.Interop/Java.Interop.csproj @@ -25,11 +25,9 @@ INTEROP;FEATURE_JNIOBJECTREFERENCE_INTPTRS;INTERNAL_NULLABLE_ATTRIBUTES;$(JavaInteropDefineConstants) - $(BaseIntermediateOutputPath)$(Configuration)+NativeAOT\$(TargetFramework.ToLowerInvariant())\ - $(BaseIntermediateOutputPath)$(Configuration)\$(TargetFramework.ToLowerInvariant())\ - bin\$(Configuration)+NativeAOT-$(DotNetTargetFramework.ToLowerInvariant())\ - $(ToolOutputFullPath) - $(OutputPath)Java.Interop.xml + $(BaseIntermediateOutputPath)$(Configuration)\$(TargetFramework.ToLowerInvariant())\ + $(ToolOutputFullPath) + $(ToolOutputFullPath)Java.Interop.xml $(BuildToolOutputFullPath) 9.0 8.0 @@ -42,9 +40,6 @@ FEATURE_JNIENVIRONMENT_JI_PINVOKES;$(DefineConstants) - - FEATURE_NATIVE_AOT;$(DefineConstants) - From 08030485d6ae8268cc169066e68b71264fefdea3 Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Thu, 15 Feb 2024 19:48:45 -0500 Subject: [PATCH 29/44] Integrate Hello-NativeAOTFromJNI.csproj into Java.Interop.sln --- Java.Interop.sln | 7 ++++++ .../Hello-NativeAOTFromJNI.csproj | 23 +++++++++++++------ .../Hello-NativeAOTFromJNI.targets | 1 - 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/Java.Interop.sln b/Java.Interop.sln index d662692ca..3af4b28fb 100644 --- a/Java.Interop.sln +++ b/Java.Interop.sln @@ -113,6 +113,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Java.Interop.Tools.Expressi EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Java.Interop.Tools.Expressions-Tests", "tests\Java.Interop.Tools.Expressions-Tests\Java.Interop.Tools.Expressions-Tests.csproj", "{211BAA88-66B1-41B2-88B2-530DBD8DF702}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hello-NativeAOTFromJNI", "samples\Hello-NativeAOTFromJNI\Hello-NativeAOTFromJNI.csproj", "{8DB3842B-73D7-491C-96F9-EBC863E2C917}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Java.Interop.NamingCustomAttributes\Java.Interop.NamingCustomAttributes.projitems*{58b564a1-570d-4da2-b02d-25bddb1a9f4f}*SharedItemsImports = 5 @@ -320,6 +322,10 @@ Global {211BAA88-66B1-41B2-88B2-530DBD8DF702}.Debug|Any CPU.Build.0 = Debug|Any CPU {211BAA88-66B1-41B2-88B2-530DBD8DF702}.Release|Any CPU.ActiveCfg = Release|Any CPU {211BAA88-66B1-41B2-88B2-530DBD8DF702}.Release|Any CPU.Build.0 = Release|Any CPU + {8DB3842B-73D7-491C-96F9-EBC863E2C917}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8DB3842B-73D7-491C-96F9-EBC863E2C917}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8DB3842B-73D7-491C-96F9-EBC863E2C917}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8DB3842B-73D7-491C-96F9-EBC863E2C917}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -374,6 +380,7 @@ Global {CB05E11B-B96F-4179-A4E9-5D6BDE29A8FC} = {271C9F30-F679-4793-942B-0D9527CB3E2F} {1A0262FE-3CDB-4AF2-AAD8-65C59524FE8A} = {0998E45F-8BCE-4791-A944-962CD54E2D80} {211BAA88-66B1-41B2-88B2-530DBD8DF702} = {271C9F30-F679-4793-942B-0D9527CB3E2F} + {8DB3842B-73D7-491C-96F9-EBC863E2C917} = {D5A93398-AEB1-49F3-89DC-3904A47DB0C7} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {29204E0C-382A-49A0-A814-AD7FBF9774A5} diff --git a/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj b/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj index 248347c73..fc20e0c33 100644 --- a/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj +++ b/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj @@ -1,7 +1,12 @@  - net8.0 + $(DotNetTargetFramework) + + + + + Hello_NativeAOTFromJNI enable enable @@ -13,14 +18,18 @@ - - + + + + diff --git a/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets b/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets index 28b22bc80..35738c9f3 100644 --- a/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets +++ b/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets @@ -2,7 +2,6 @@ $(DOTNET_HOST_PATH) - $(MSBuildThisFileDirectory)..\..\bin\$(Configuration)-net7.0 Date: Thu, 15 Feb 2024 20:00:05 -0500 Subject: [PATCH 30/44] Cleanup. --- .../Hello-NativeAOTFromJNI.csproj | 1 - .../Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.xml | 7 ------- samples/Hello-NativeAOTFromJNI/README.md | 11 ++++++----- 3 files changed, 6 insertions(+), 13 deletions(-) delete mode 100644 samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.xml diff --git a/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj b/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj index fc20e0c33..53822ff34 100644 --- a/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj +++ b/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj @@ -34,7 +34,6 @@ - diff --git a/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.xml b/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.xml deleted file mode 100644 index 5fc989ecc..000000000 --- a/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/samples/Hello-NativeAOTFromJNI/README.md b/samples/Hello-NativeAOTFromJNI/README.md index de4d7bc7c..3494607bf 100644 --- a/samples/Hello-NativeAOTFromJNI/README.md +++ b/samples/Hello-NativeAOTFromJNI/README.md @@ -22,9 +22,10 @@ Release configuration: % dotnet build -c Release ``` -Once Java.Interop itself is built, you can build the sample: +Once Java.Interop itself is built, you can *publish* the sample: ```sh +% cd samples/Hello-NativeAOTFromJNI % dotnet publish -c Release -r osx-x64 ``` @@ -32,10 +33,10 @@ The resulting native library contains the desired symbols: ```sh % nm bin/Release/osx-x64/publish/Hello-NativeAOTFromJNI.dylib | grep ' S ' -000000000016f5a0 S _JNI_OnLoad -000000000016f5d0 S _JNI_OnUnload -000000000016f2f0 S _Java_net_dot_jni_hello_App_sayHello -000000000016f620 S _Java_net_dot_jni_hello_JavaInteropRuntime_init +00000000000ef880 S _JNI_OnLoad +00000000000ef8b0 S _JNI_OnUnload +00000000000ef5d0 S _Java_net_dot_jni_hello_App_sayHello +00000000000ef900 S _Java_net_dot_jni_hello_JavaInteropRuntime_init ``` Use the `RunJavaSample` target to run Java, which will run From 12cda046126a87d8f2ea253c30dc8cdc3ebefccc Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Thu, 15 Feb 2024 20:06:58 -0500 Subject: [PATCH 31/44] Run the sample on CI! --- build-tools/automation/templates/core-tests.yaml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/build-tools/automation/templates/core-tests.yaml b/build-tools/automation/templates/core-tests.yaml index 1f11013ed..ab6bedf97 100644 --- a/build-tools/automation/templates/core-tests.yaml +++ b/build-tools/automation/templates/core-tests.yaml @@ -173,6 +173,22 @@ steps: arguments: -c $(Build.Configuration) tools/java-source-utils/java-source-utils.csproj -t:RunTests continueOnError: true +- task: DotNetCoreCLI@2 + displayName: 'Tests: publish Hello-NativeAOTFromJNI' + condition: eq('${{ parameters.platformName }}', '.NET - MacOS') + inputs: + command: publish + arguments: -c $(Build.Configuration) samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj -r osx-x64 + continueOnError: true + +- task: DotNetCoreCLI@2 + displayName: 'Tests: run Hello-NativeAOTFromJNI' + condition: eq('${{ parameters.platformName }}', '.NET - MacOS') + inputs: + command: build + arguments: -c $(Build.Configuration) samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj -r osx-x64 -t:RunJavaSample + continueOnError: true + - task: PublishTestResults@2 displayName: Publish JUnit Test Results inputs: From 386177341beadbb1beb28089309314c006dbd28f Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Thu, 15 Feb 2024 20:09:41 -0500 Subject: [PATCH 32/44] Reduce manual string duplication. --- samples/Hello-NativeAOTFromJNI/ManagedType.cs | 3 ++- samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/samples/Hello-NativeAOTFromJNI/ManagedType.cs b/samples/Hello-NativeAOTFromJNI/ManagedType.cs index aebe6df29..3d21f7163 100644 --- a/samples/Hello-NativeAOTFromJNI/ManagedType.cs +++ b/samples/Hello-NativeAOTFromJNI/ManagedType.cs @@ -2,8 +2,9 @@ namespace Example; using Java.Interop; -[JniTypeSignature ("example/ManagedType")] +[JniTypeSignature (JniTypeName)] class ManagedType : Java.Lang.Object { + internal const string JniTypeName = "example/ManagedType"; [JavaCallableConstructor(SuperConstructorExpression="")] public ManagedType (int value) diff --git a/samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs b/samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs index c80922dfa..7d21d95e1 100644 --- a/samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs +++ b/samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs @@ -6,7 +6,7 @@ class NativeAotTypeManager : JniRuntime.JniTypeManager { #pragma warning disable IL2026 Dictionary typeMappings = new () { - ["example/ManagedType"] = typeof (Example.ManagedType), + [Example.ManagedType.JniTypeName] = typeof (Example.ManagedType), }; #pragma warning restore IL2026 From adf5773913ef022afabd74640539b67326e33649 Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Thu, 15 Feb 2024 20:10:53 -0500 Subject: [PATCH 33/44] Remove unnecessary line. --- src/Java.Interop/Java.Interop/ManagedPeer.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Java.Interop/Java.Interop/ManagedPeer.cs b/src/Java.Interop/Java.Interop/ManagedPeer.cs index d0a40722d..6a9834954 100644 --- a/src/Java.Interop/Java.Interop/ManagedPeer.cs +++ b/src/Java.Interop/Java.Interop/ManagedPeer.cs @@ -71,7 +71,6 @@ static void Construct ( var envp = new JniTransition (jnienv); try { var runtime = JniEnvironment.Runtime; - var typeMgr = runtime.TypeManager; var r_self = new JniObjectReference (n_self); var self = runtime.ValueManager.PeekPeer (r_self); if (self != null) { From 65f7d90d314be3a0c0188ccacc93fc879d76bcee Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Thu, 15 Feb 2024 21:17:28 -0500 Subject: [PATCH 34/44] Set publishWebProjects: false. `Hello-NativeAOTFromJNI.csproj` isn't an ASP.NET project, so it isn't a web project, and shouldn't be treated as such. We want to invoke the "real" `dotnet publish` command. Also use `projects: path/to/Hello-NativeAOTFromJNI.csproj`, just in case. --- build-tools/automation/templates/core-tests.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/build-tools/automation/templates/core-tests.yaml b/build-tools/automation/templates/core-tests.yaml index ab6bedf97..0ddb2b309 100644 --- a/build-tools/automation/templates/core-tests.yaml +++ b/build-tools/automation/templates/core-tests.yaml @@ -178,7 +178,9 @@ steps: condition: eq('${{ parameters.platformName }}', '.NET - MacOS') inputs: command: publish - arguments: -c $(Build.Configuration) samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj -r osx-x64 + publishWebProjects: false + projects: 'samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj' + arguments: -c $(Build.Configuration) -r osx-x64 continueOnError: true - task: DotNetCoreCLI@2 From 429bb4d8931d525bf79dd189bf2adf14b1c4fe5c Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Thu, 15 Feb 2024 21:34:38 -0500 Subject: [PATCH 35/44] Try setting workingDirectory. Currently fails with: /Users/runner/work/1/s/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets(101,5): error MSB6003: The specified task executable "sh" could not be run. System.IO.DirectoryNotFoundException: The working directory "bin/Release/osx-x64/publish/" does not exist. [/Users/runner/work/1/s/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj] Maybe if we set workingDirectory to `samples/Hello-NativeAOTFromJNI` it will work? --- build-tools/automation/templates/core-tests.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build-tools/automation/templates/core-tests.yaml b/build-tools/automation/templates/core-tests.yaml index 0ddb2b309..40f3aee7e 100644 --- a/build-tools/automation/templates/core-tests.yaml +++ b/build-tools/automation/templates/core-tests.yaml @@ -188,7 +188,8 @@ steps: condition: eq('${{ parameters.platformName }}', '.NET - MacOS') inputs: command: build - arguments: -c $(Build.Configuration) samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj -r osx-x64 -t:RunJavaSample + arguments: -c $(Build.Configuration) Hello-NativeAOTFromJNI.csproj -r osx-x64 -t:RunJavaSample + workingDirectory: samples/Hello-NativeAOTFromJNI continueOnError: true - task: PublishTestResults@2 From b4cddb779bcd92cd24bc4e43eb9d8b9b2f852b10 Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Fri, 16 Feb 2024 09:01:47 -0500 Subject: [PATCH 36/44] Setting workingDirectory didn't fix things. Printf debug this; what is `$(PublishDir)` when running `RunJavaSample`? What files exist? --- .../Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets b/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets index 35738c9f3..3734e5509 100644 --- a/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets +++ b/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets @@ -89,10 +89,15 @@ + <_Classpath Include="hello-from-java.jar" /> <_Classpath Include="java-interop.jar" /> + <_PD Include="$(PublishDir)**\*" /> + + <_CPSep Condition=" '$(OS)' == 'Windows_NT' ">; <_CPSep Condition=" '$(_CPSep)' == '' ">: From 42c72514f3377d54a00a63cbba9f65b40f544512 Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Fri, 16 Feb 2024 09:37:09 -0500 Subject: [PATCH 37/44] $CWD isn't what I expect, so use a rooted WorkingDirectory. --- samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets b/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets index 3734e5509..dbe298c01 100644 --- a/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets +++ b/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets @@ -105,7 +105,7 @@ From f656870233a87a08025eb078fedea1c71f975645 Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Fri, 16 Feb 2024 10:57:12 -0500 Subject: [PATCH 38/44] MOAR PRINTFS This is quite confusing; as per CI logs, during `dotnet publish`: Hello-NativeAOTFromJNI -> /Users/runner/work/1/s/samples/Hello-NativeAOTFromJNI/bin/Release/osx-x64/publish/ which implies to me that `/Users/runner/work/1/s/samples/Hello-NativeAOTFromJNI/bin/Release/osx-x64/publish` *exists*. However, when we get to `dotnet build -t:RunJavaSample`: ##[error]samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets(106,5): Error MSB6003: The specified task executable "sh" could not be run. System.IO.DirectoryNotFoundException: The working directory "/Users/runner/work/1/s/samples/Hello-NativeAOTFromJNI/bin/Release/osx-x64/publish/" does not exist. Which looks the same to me! Dump out dir contents. --- .../Hello-NativeAOTFromJNI.targets | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets b/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets index dbe298c01..16f433593 100644 --- a/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets +++ b/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets @@ -88,15 +88,32 @@ + + + <_TD Include="$(MSBuildThisFileDirectory)**\*" /> + + + + + - + + <_HavePD Condition=" Exists('$(MSBuildThisFileDirectory)$(PublishDir)') ">true + + <_Classpath Include="hello-from-java.jar" /> <_Classpath Include="java-interop.jar" /> - <_PD Include="$(PublishDir)**\*" /> + <_TD Include="$(MSBuildThisFileDirectory)**\*" /> + <_PD Include="$(MSBuildThisFileDirectory)$(PublishDir)**\*" /> - + + + <_CPSep Condition=" '$(OS)' == 'Windows_NT' ">; From 1b8b1af2aeaeb1e33584ad6dcaf02e7d7b9365af Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Fri, 16 Feb 2024 11:25:44 -0500 Subject: [PATCH 39/44] Avoid DotNetCoreCLI@2 Through the glory of printing the contents of the directory after `dotnet publish` and as part of `dotnet build -t:RunJavaSample`, we see that the `$(PublishDir)` `bin/Release/osx-x64/publish` exists after `dotnet publish`, but is *gone* when `dotnet build` runs (?!). In its place is a `publish.zip` file, which doesn't do us any good! We thus infer and assume that the DotNetCoreCLI@2 task is creating this `publish.zip` file and removing the directory, which prevents the execution step from actually executing. Replace DotNetCoreCLI@2 with a powershell script which directly invokes `dotnet publish` & co. Hopefully this will avoid the problem. --- .../automation/templates/core-tests.yaml | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/build-tools/automation/templates/core-tests.yaml b/build-tools/automation/templates/core-tests.yaml index 40f3aee7e..b2b6a7c93 100644 --- a/build-tools/automation/templates/core-tests.yaml +++ b/build-tools/automation/templates/core-tests.yaml @@ -173,23 +173,19 @@ steps: arguments: -c $(Build.Configuration) tools/java-source-utils/java-source-utils.csproj -t:RunTests continueOnError: true -- task: DotNetCoreCLI@2 +- powershell: > + dotnet publish -c $(Build.Configuration) -r osx-x64 + samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj displayName: 'Tests: publish Hello-NativeAOTFromJNI' condition: eq('${{ parameters.platformName }}', '.NET - MacOS') - inputs: - command: publish - publishWebProjects: false - projects: 'samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj' - arguments: -c $(Build.Configuration) -r osx-x64 continueOnError: true -- task: DotNetCoreCLI@2 +- powershell: > + dotnet build -c $(Build.Configuration) -r osx-x64 + -t:RunJavaSample + samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj displayName: 'Tests: run Hello-NativeAOTFromJNI' condition: eq('${{ parameters.platformName }}', '.NET - MacOS') - inputs: - command: build - arguments: -c $(Build.Configuration) Hello-NativeAOTFromJNI.csproj -r osx-x64 -t:RunJavaSample - workingDirectory: samples/Hello-NativeAOTFromJNI continueOnError: true - task: PublishTestResults@2 From 59314fc389c607efd8cb2358c24083e3a396f9d0 Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Fri, 16 Feb 2024 11:42:01 -0500 Subject: [PATCH 40/44] Cleanup. --- samples/Hello-NativeAOTFromJNI/App.cs | 4 ++-- .../Hello-NativeAOTFromJNI.targets | 22 ------------------- 2 files changed, 2 insertions(+), 24 deletions(-) diff --git a/samples/Hello-NativeAOTFromJNI/App.cs b/samples/Hello-NativeAOTFromJNI/App.cs index 94ce115eb..c2e326921 100644 --- a/samples/Hello-NativeAOTFromJNI/App.cs +++ b/samples/Hello-NativeAOTFromJNI/App.cs @@ -6,7 +6,7 @@ namespace Hello_NativeAOTFromJNI; static class App { - // symbol name from `$(IntermediateOutputPath)obj/Release/osx-arm64/h-classes/net_dot_jni_hello_App.h` + // symbol name from `$(IntermediateOutputPath)obj/Release/osx-x64/h-classes/net_dot_jni_hello_App.h` [UnmanagedCallersOnly (EntryPoint="Java_net_dot_jni_hello_App_sayHello")] static IntPtr sayHello (IntPtr jnienv, IntPtr klass) { @@ -28,4 +28,4 @@ static IntPtr sayHello (IntPtr jnienv, IntPtr klass) } return nint.Zero; } -} \ No newline at end of file +} diff --git a/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets b/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets index 16f433593..cf0b4a679 100644 --- a/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets +++ b/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets @@ -88,33 +88,11 @@ - - - <_TD Include="$(MSBuildThisFileDirectory)**\*" /> - - - - - - - <_HavePD Condition=" Exists('$(MSBuildThisFileDirectory)$(PublishDir)') ">true - - <_Classpath Include="hello-from-java.jar" /> <_Classpath Include="java-interop.jar" /> - <_TD Include="$(MSBuildThisFileDirectory)**\*" /> - <_PD Include="$(MSBuildThisFileDirectory)$(PublishDir)**\*" /> - - - - <_CPSep Condition=" '$(OS)' == 'Windows_NT' ">; <_CPSep Condition=" '$(_CPSep)' == '' ">: From 2a012c78570c2c7e7a051ebea1bdc63b38c1488b Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Fri, 16 Feb 2024 14:18:20 -0500 Subject: [PATCH 41/44] Fix some `dotnet publish` linker warnings. Warnings fixed: `Type.MakeGenericType()` needs to ensure constructors are preserved: src/Java.Interop/Java.Interop/JniRuntime.JniValueManager.cs(378): Trim analysis warning IL2068: Java.Interop.JniRuntime.JniValueManager.g__MakeGenericType|31_1(Type,Type[]): 'Java.Interop.JniRuntime.JniValueManager.g__MakeGenericType|31_1(Type,Type[])' method return value does not satisfy 'DynamicallyAccessedMemberTypes.PublicConstructors', 'DynamicallyAccessedMemberTypes.NonPublicConstructors' requirements. The parameter 'type' of method 'Java.Interop.JniRuntime.JniValueManager.g__MakeGenericType|31_1(Type,Type[])' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. `Expression.New()` requires the constructor be kept, so "fake it" by wrapping `Type.GetType()` w/ `[DynamicallyAccessedMembers]`: src/Java.Interop/Java.Interop/JniValueMarshaler.cs(175): Trim analysis warning IL2072: Java.Interop.JniValueMarshaler.CreateSelf(JniValueMarshalerContext,ParameterExpression): 'type' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicParameterlessConstructor' in call to 'System.Linq.Expressions.Expression.New(Type)'. The return value of method 'System.Object.GetType()' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. `[DynamicallyAccessedMembers]` should be on properties, not getters: src/Java.Interop/Java.Interop/JniValueMarshalerAttribute.cs(33): Trim analysis warning IL2078: Java.Interop.JniValueMarshalerAttribute.MarshalerType.get: 'Java.Interop.JniValueMarshalerAttribute.MarshalerType.get' method return value does not satisfy 'DynamicallyAccessedMemberTypes.PublicParameterlessConstructor', 'DynamicallyAccessedMemberTypes.Interfaces' requirements. The field 'Java.Interop.JniValueMarshalerAttribute.k__BackingField' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. Silence use of `Assembly.Location`, when it's optional: src/Java.Runtime.Environment/Java.Interop/JreRuntime.cs(106): warning IL3000: Java.Interop.JreRuntime.CreateJreVM(JreRuntimeOptions): 'System.Reflection.Assembly.Location.get' always returns an empty string for assemblies embedded in a single-file app. If the path to the app directory is needed, consider calling 'System.AppContext.BaseDirectory'. src/Java.Runtime.Environment/Java.Interop/JreRuntime.cs(323): warning IL3000: Java.Interop.JreNativeMethods..cctor(): 'System.Reflection.Assembly.Location.get' always returns an empty string for assemblies embedded in a single-file app. If the path to the app directory is needed, consider calling 'System.AppContext.BaseDirectory'. --- .../Java.Interop/JniRuntime.JniValueManager.cs | 5 ++++- .../Java.Interop/JniValueMarshaler.cs | 5 ++++- .../Java.Interop/JniValueMarshalerAttribute.cs | 2 +- .../Java.Interop/JreRuntime.cs | 15 +++++++++++++-- 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/Java.Interop/Java.Interop/JniRuntime.JniValueManager.cs b/src/Java.Interop/Java.Interop/JniRuntime.JniValueManager.cs index 181469f6d..b26e3b614 100644 --- a/src/Java.Interop/Java.Interop/JniRuntime.JniValueManager.cs +++ b/src/Java.Interop/Java.Interop/JniRuntime.JniValueManager.cs @@ -374,7 +374,10 @@ static Type GetPeerType ([DynamicallyAccessedMembers (Constructors)] Type type) [UnconditionalSuppressMessage ("Trimming", "IL2055", Justification = makeGenericTypeMessage)] [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = makeGenericTypeMessage)] [return: DynamicallyAccessedMembers (Constructors)] - static Type MakeGenericType (Type type, Type [] arguments) => + static Type MakeGenericType ( + [DynamicallyAccessedMembers (Constructors)] + Type type, + Type [] arguments) => type.MakeGenericType (arguments); Type[] arguments = type.GetGenericArguments (); diff --git a/src/Java.Interop/Java.Interop/JniValueMarshaler.cs b/src/Java.Interop/Java.Interop/JniValueMarshaler.cs index ae8e096c2..8b5b08cb5 100644 --- a/src/Java.Interop/Java.Interop/JniValueMarshaler.cs +++ b/src/Java.Interop/Java.Interop/JniValueMarshaler.cs @@ -172,10 +172,13 @@ Expression CreateSelf (JniValueMarshalerContext context, ParameterExpression sou { var self = Expression.Variable (GetType (), sourceValue.Name + "_marshaler"); context.LocalVariables.Add (self); - context.CreationStatements.Add (Expression.Assign (self, Expression.New (GetType ()))); + context.CreationStatements.Add (Expression.Assign (self, Expression.New (_GetType ()))); return self; } + [return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type _GetType () => GetType (); + [RequiresUnreferencedCode (ExpressionRequiresUnreferencedCode)] public virtual Expression CreateReturnValueFromManagedExpression (JniValueMarshalerContext context, ParameterExpression sourceValue) { diff --git a/src/Java.Interop/Java.Interop/JniValueMarshalerAttribute.cs b/src/Java.Interop/Java.Interop/JniValueMarshalerAttribute.cs index b674094be..03bb442b5 100644 --- a/src/Java.Interop/Java.Interop/JniValueMarshalerAttribute.cs +++ b/src/Java.Interop/Java.Interop/JniValueMarshalerAttribute.cs @@ -29,8 +29,8 @@ public JniValueMarshalerAttribute ( MarshalerType = marshalerType; } + [DynamicallyAccessedMembers (ParameterlessConstructorsInterfaces)] public Type MarshalerType { - [return: DynamicallyAccessedMembers (ParameterlessConstructorsInterfaces)] get; } } diff --git a/src/Java.Runtime.Environment/Java.Interop/JreRuntime.cs b/src/Java.Runtime.Environment/Java.Interop/JreRuntime.cs index 4bce97019..eba0c0c5a 100644 --- a/src/Java.Runtime.Environment/Java.Interop/JreRuntime.cs +++ b/src/Java.Runtime.Environment/Java.Interop/JreRuntime.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; @@ -103,7 +104,7 @@ static unsafe JreRuntimeOptions CreateJreVM (JreRuntimeOptions builder) builder.LibraryHandler.LoadJvmLibrary (builder.JvmLibraryPath!); if (!builder.ClassPath.Any (p => p.EndsWith ("java-interop.jar", StringComparison.OrdinalIgnoreCase))) { - var loc = typeof (JreRuntimeOptions).Assembly.Location; + var loc = GetAssemblyLocation (typeof (JreRuntimeOptions).Assembly); var dir = string.IsNullOrEmpty (loc) ? null : Path.GetDirectoryName (loc); var jij = string.IsNullOrEmpty (dir) ? null : Path.Combine (dir, "java-interop.jar"); if (!File.Exists (jij)) { @@ -146,6 +147,15 @@ static unsafe JreRuntimeOptions CreateJreVM (JreRuntimeOptions builder) } } + [UnconditionalSuppressMessage ("Trimming", "IL3000", Justification = "We check for a null Assembly.Location value!")] + internal static string? GetAssemblyLocation (Assembly assembly) + { + var location = assembly.Location; + if (!string.IsNullOrEmpty (location)) + return location; + return null; + } + JvmLibraryHandler LibraryHandler; internal protected JreRuntime (JreRuntimeOptions builder) @@ -320,7 +330,8 @@ partial class JreNativeMethods { static JreNativeMethods () { if (Environment.OSVersion.Platform == PlatformID.Win32NT) { - var baseDir = Path.GetDirectoryName (typeof (JreRuntime).Assembly.Location) ?? throw new NotSupportedException (); + var loc = JreRuntime.GetAssemblyLocation (typeof (JreRuntime).Assembly) ?? throw new NotSupportedException (); + var baseDir = Path.GetDirectoryName (loc) ?? throw new NotSupportedException (); var newDir = Path.Combine (baseDir, Environment.Is64BitProcess ? "win-x64" : "win-x86"); JreNativeMethods.AddDllDirectory (newDir); } From f79d3e131898738fa53be663c66b06cbfc7c9547 Mon Sep 17 00:00:00 2001 From: "Jonathan Pryor (HE/HIM)" Date: Fri, 16 Feb 2024 16:14:29 -0500 Subject: [PATCH 42/44] Quote `$(DotnetToolPath)`, as it will contain spaces on Windows. --- samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets b/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets index cf0b4a679..695c22c18 100644 --- a/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets +++ b/samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets @@ -1,7 +1,7 @@ - $(DOTNET_HOST_PATH) + "$(DOTNET_HOST_PATH)" Date: Fri, 16 Feb 2024 16:16:45 -0500 Subject: [PATCH 43/44] $(Standalone) by default, and prefer `native-library` loader in JreRuntime. These changes mean we don't need `java-interop.dll` on Windows, which vastly simplifies things. --- src/Java.Interop/Java.Interop.csproj | 1 + src/Java.Runtime.Environment/Java.Interop/JreRuntime.cs | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/src/Java.Interop/Java.Interop.csproj b/src/Java.Interop/Java.Interop.csproj index 80eb91aee..3a7dd4325 100644 --- a/src/Java.Interop/Java.Interop.csproj +++ b/src/Java.Interop/Java.Interop.csproj @@ -32,6 +32,7 @@ 9.0 8.0 $(JICoreLibVersion) + true FEATURE_JNIENVIRONMENT_JI_FUNCTION_POINTERS;$(DefineConstants) diff --git a/src/Java.Runtime.Environment/Java.Interop/JreRuntime.cs b/src/Java.Runtime.Environment/Java.Interop/JreRuntime.cs index eba0c0c5a..811523b35 100644 --- a/src/Java.Runtime.Environment/Java.Interop/JreRuntime.cs +++ b/src/Java.Runtime.Environment/Java.Interop/JreRuntime.cs @@ -199,11 +199,15 @@ public static JvmLibraryHandler Create () { var handler = Environment.GetEnvironmentVariable ("JI_LOADER_TYPE"); switch (handler?.ToLowerInvariant ()) { +#if !NET case "": case null: +#endif // NET case "java-interop": return new JavaInteropLibJvmLibraryHandler (); #if NET + case "": + case null: case "native-library": return new NativeLibraryJvmLibraryHandler (); #endif // NET From cf240f9082b7fc1fa4f3ced225eaab39f41fe5b4 Mon Sep 17 00:00:00 2001 From: "Jonathan Pryor (HE/HIM)" Date: Fri, 16 Feb 2024 16:21:31 -0500 Subject: [PATCH 44/44] Run the Hello-NativeAOT tests on Windows, too --- build-tools/automation/azure-pipelines.yaml | 2 ++ build-tools/automation/templates/core-tests.yaml | 7 +++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/build-tools/automation/azure-pipelines.yaml b/build-tools/automation/azure-pipelines.yaml index 654db943e..df6f1bf06 100644 --- a/build-tools/automation/azure-pipelines.yaml +++ b/build-tools/automation/azure-pipelines.yaml @@ -57,6 +57,7 @@ jobs: - template: templates\core-tests.yaml parameters: runNativeDotnetTests: true + nativeAotRid: win-x64 platformName: .NET - Windows - template: templates\fail-on-issue.yaml @@ -87,6 +88,7 @@ jobs: - template: templates\core-tests.yaml parameters: runNativeTests: true + nativeAotRid: osx-x64 platformName: .NET - MacOS - template: templates\fail-on-issue.yaml diff --git a/build-tools/automation/templates/core-tests.yaml b/build-tools/automation/templates/core-tests.yaml index b2b6a7c93..5c886152b 100644 --- a/build-tools/automation/templates/core-tests.yaml +++ b/build-tools/automation/templates/core-tests.yaml @@ -2,6 +2,7 @@ parameters: condition: succeeded() runNativeTests: false platformName: + nativeAotRid: steps: - task: DotNetCoreCLI@2 @@ -174,18 +175,16 @@ steps: continueOnError: true - powershell: > - dotnet publish -c $(Build.Configuration) -r osx-x64 + dotnet publish -c $(Build.Configuration) -r ${{ parameters.nativeAotRid }} samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj displayName: 'Tests: publish Hello-NativeAOTFromJNI' - condition: eq('${{ parameters.platformName }}', '.NET - MacOS') continueOnError: true - powershell: > - dotnet build -c $(Build.Configuration) -r osx-x64 + dotnet build -c $(Build.Configuration) -r ${{ parameters.nativeAotRid }} -t:RunJavaSample samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj displayName: 'Tests: run Hello-NativeAOTFromJNI' - condition: eq('${{ parameters.platformName }}', '.NET - MacOS') continueOnError: true - task: PublishTestResults@2