Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Hello-NativeAOTFromJNI] Add NativeAOT sample (#1153)
Context: 28849ec Context: bc5bcf4 Context: 25850ba Context: 56955d9 Context: #1157 Context: c6c487b Context: dotnet/android@180dd52 Commit 28849ec noted: > [JNI][0] supports *two* modes of operation: > > 1. Native code creates the JVM, e.g. via [`JNI_CreateJavaVM()`][1] > > 2. The JVM already exists, and when Java code calls > [`System.loadLibrary()`][3], the JVM calls the > [`JNI_OnLoad()`][2] function on the specified library. > > [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/en/java/javase/11/docs/api/java.base/java/lang/Runtime.html#loadLibrary(java.lang.String) > [3]: https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/invocation.html#JNJI_OnLoad Our existing unit tests, e.g. `tests/Java.Interop-Tests`, exercise the first mode of operation. The second mode of operation is how .NET Android executes. Add `samples/Hello-NativeAOTFromJNI`, a new sample which exercises the second mode of operation: 1. `Hello-NativeAOTFromJNI` contains C# methods which use [`UnmanagedCallersOnlyAttribute`][4] to export `JNI_OnLoad()` and various other native symbols for JNI to execute. 2. `dotnet publish Hello-NativeAOTFromJNI.csproj` will use [NativeAOT][5] to produce a native library which can be loaded and used by a desktop JVM. 3. `Hello-NativeAOTFromJNI` references `Java.Base.dll` (bc5bcf4), and contains a C# `Java.Lang.Object` subclass, which contains a `[JavaCallable]` method. 4. `jcw-gen` is called during the build process to generate Java Callable Wrappers for (3), containing the `[JavaCallable]` method. 5. `jnimarshalmethod-gen` (25850ba) is called during the build process to generate marshal methods for (3), as `generator` output for JavaInterop1 doesn't contain marshal methods (unlike XAJavaInterop1, used by .NET Android), and NativeAOT doesn't support System.Reflection.Emit. 6. The type in (3) is instantiated *from Java*, and Java invokes the `[JavaCallable]` method. 7. `Hello-NativeAOTFromJNI` also contains Java code including a `main()` method (to start execution from Java) Conceptually straightforward! See `samples/Hello-NativeAOTFromJNI/README.md` for more details. ~~ `$(Standalone)`=true is now the default build config ~~ The `$(Standalone)` build config (c6c487b) is now enabled by default. This means that neither `Java.Interop.dll` nor `Java.Runtime.Environment.dll` *require* a `java-interop` native library, which (1) was already the case for xamarin-android as of dotnet/android@180dd520, and (2) simplifies NativeAOT support immensely. ~~ Avoid Adding Public API ~~ In order to avoid adding new public API to `Java.Interop.dll`, update `Java.Interop.dll` to have `[InternalsVisibleTo]` for `Java.Runtime.Environment.dll`. This allows `JreTypeManager` to use `JniRuntime.UseMarshalMemberBuilder`, without needing to worry about `try`/`catch` blocks. This in turn requires renaming `NativeMethods` within `Java.Runtime.Environment.dll` to `JreNativeMethods`, as `Java.Interop.dll` *also* has a `NativeMethods` type. ~~ NativeAOT Integration ~~ Conceptually straightforward, but not without lots of pitfalls. In order for Java code to call into C# code, there must be a Java Callable Wrapper, which contains Java `native` methods, and those Java `native` methods must be *registered with Java* before they are executed. Method registration involves lots of delegates, which need to be stored in the `JniNativeMethodRegistration.Delegate` field, of type `System.Delegate`. NativeAOT doesn't like this by default. In order to convince NativeAOT to support this, every delegate instance provided to `JniNativeMethodRegistration.Delegate` must be of a delegate type which has [`UnmanagedFunctionPointerAttribute`][6]. This coerces NativeAOT to emit "stubs" for the referenced method, allowing things to work with fewer changes. Linux builds require `-Wl,-soname` to be set manually in order for the resulting `.so` to work properly. ~~ Trimmer Warnings ~~ Fix additional trimmer warnings, a'la #1157. `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.<GetInvokerType>g__MakeGenericType|31_1(Type,Type[]): 'Java.Interop.JniRuntime.JniValueManager.<GetInvokerType>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.<GetInvokerType>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.<MarshalerType>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'. ~~ Future Work ~~ Make `JniRuntime.UseMarshalMemberBuilder` public for .NET 9, and remove the `InternalsVisibleTo` from `Java.Interop.dll` to `Java.Runtime.Environment.dll`. `generator` should be updated to apply `UnmanagedFunctionPointerAttribute` onto all `_JniMarshal_*` delegate type declarations (56955d9). [4]: https://learn.microsoft.com/dotnet/api/system.runtime.interopservices.unmanagedcallersonlyattribute?view=net-8.0 [5]: https://learn.microsoft.com/dotnet/core/deploying/native-aot [6]: https://learn.microsoft.com/dotnet/api/system.runtime.interopservices.unmanagedfunctionpointerattribute?view=net-8.0
- Loading branch information