Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Hello-NativeAOTFromJNI] Add NativeAOT sample #1153

Merged
merged 52 commits into from
Feb 22, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
007317b
[Hello-NativeAOTFromJNI] Add NativeAOT sample
jonpryor Oct 24, 2023
db84cc3
Try to create a JreRuntime
jonpryor Oct 25, 2023
a32e244
icanhaz blittable method registration?
jonpryor Oct 25, 2023
4c30ab5
Linux builds require -Wl,-soname to be set properly.
jonpryor Oct 25, 2023
a08dce8
Fix build break.
jonpryor Oct 25, 2023
da9f188
Use [UnmanagedFunctionPointer]
jonpryor Oct 25, 2023
69c90ba
"Full(er)" sample
jonpryor Nov 1, 2023
1fc0af5
Use JniTransition.
jonpryor Nov 2, 2023
a499a1d
Allow init from JNIEnv*, not just JavaVM*
jonpryor Nov 2, 2023
e1822f0
Make it (mostly) work!
jonpryor Nov 2, 2023
9027591
Fix Java.Interop.Export-Tests
jonpryor Nov 7, 2023
1d422da
Merge remote-tracking branch 'origin/main' into dev/jonp/jonp-hello-f…
jonpryor Nov 7, 2023
902fe28
Remove more `n_`s?!
jonpryor Nov 7, 2023
0392169
Enable activation
jonpryor Nov 7, 2023
93a901a
Allow marking constructors as callable from Java.
jonpryor Nov 7, 2023
a50457b
Partially revert 9027591c41232c82a582f8ab16ddaa783795582c
jonpryor Nov 8, 2023
ce850ca
Rethink `Type.GetType()` alternatives
jonpryor Nov 9, 2023
6920e6c
Remove need for NativeAotValueManager.
jonpryor Nov 9, 2023
4ceaa34
Merge remote-tracking branch 'origin/main' into dev/jonp/jonp-hello-f…
jonpryor Nov 13, 2023
cf73d77
Merge remote-tracking branch 'origin/main' into dev/jonp/jonp-hello-f…
jonpryor Nov 14, 2023
986f0c4
Reduce patch size.
jonpryor Nov 14, 2023
17a4403
Work with .NET 8 GA (…?)
jonpryor Nov 15, 2023
1b86f9a
Use `net.dot.jni` package prefix
jonpryor Nov 16, 2023
f71cadb
Forgot to fix up a header path…
jonpryor Nov 16, 2023
b5fc4cf
Don't add new public API
jonpryor Nov 16, 2023
4741dc9
Reduce patch size.
jonpryor Nov 16, 2023
8eb2850
Remove more unneeded bits.
jonpryor Nov 16, 2023
c3e4870
Merge remote-tracking branch 'origin/main' into dev/jonp/jonp-hello-f…
jonpryor Nov 23, 2023
aeed449
Merge remote-tracking branch 'origin/main' into dev/jonp/jonp-hello-f…
jonpryor Nov 24, 2023
1e71988
Remove GetTypeFromAssemblyQualifiedName()
jonpryor Nov 23, 2023
1b7be6c
Merge remote-tracking branch 'origin/main' into dev/jonp/jonp-hello-f…
jonpryor Dec 2, 2023
14f6713
Cleanup.
jonpryor Dec 3, 2023
508dcde
Merge remote-tracking branch 'origin/main' into dev/jonp/jonp-hello-f…
jonpryor Feb 9, 2024
74248fb
Merge remote-tracking branch 'origin/main' into dev/jonp/jonp-hello-f…
jonpryor Feb 16, 2024
12e187e
Fix spelling of JNI_OnUnload
jonpryor Feb 16, 2024
cfdfeaf
Reduce unnecessary changes.
jonpryor Feb 16, 2024
0803048
Integrate Hello-NativeAOTFromJNI.csproj into Java.Interop.sln
jonpryor Feb 16, 2024
f8bb97f
Cleanup.
jonpryor Feb 16, 2024
12cda04
Run the sample on CI!
jonpryor Feb 16, 2024
3861773
Reduce manual string duplication.
jonpryor Feb 16, 2024
adf5773
Remove unnecessary line.
jonpryor Feb 16, 2024
65f7d90
Set publishWebProjects: false.
jonpryor Feb 16, 2024
429bb4d
Try setting workingDirectory.
jonpryor Feb 16, 2024
b4cddb7
Setting workingDirectory didn't fix things.
jonpryor Feb 16, 2024
42c7251
$CWD isn't what I expect, so use a rooted WorkingDirectory.
jonpryor Feb 16, 2024
f656870
MOAR PRINTFS
jonpryor Feb 16, 2024
1b8b1af
Avoid DotNetCoreCLI@2
jonpryor Feb 16, 2024
59314fc
Cleanup.
jonpryor Feb 16, 2024
2a012c7
Fix some `dotnet publish` linker warnings.
jonpryor Feb 16, 2024
f79d3e1
Quote `$(DotnetToolPath)`, as it will contain spaces on Windows.
jonpryor Feb 16, 2024
213a57b
$(Standalone) by default, and prefer `native-library` loader in JreRu…
jonpryor Feb 16, 2024
cf240f9
Run the Hello-NativeAOT tests on Windows, too
jonpryor Feb 16, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build-tools/jnienv-gen/Generator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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") },
Expand Down
35 changes: 35 additions & 0 deletions samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>Hello_NativeAOTFromJNI</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<NativeLib>Shared</NativeLib>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Java.Interop\Java.Interop.csproj"
AdditionalProperties="Standalone=True"
/>
<ProjectReference Include="..\..\src\Java.Runtime.Environment\Java.Runtime.Environment.csproj"
AdditionalProperties="Standalone=True"
/>
</ItemGroup>

<ItemGroup>
<Content Include="$(OutputPath)hello-from-java.jar" CopyToPublishDirectory="PreserveNewest" />
</ItemGroup>

<ItemGroup>
<HelloNativeAOTFromJNIJar Include="$(MSBuildThisFileDirectory)java\**\*.java" />
</ItemGroup>

<ItemGroup Condition=" $([MSBuild]::IsOSPlatform('linux')) Or $([MSBuild]::IsOSPlatform('FreeBSD')) Or $([MSBuild]::IsOSPlatform('Android')) ">
<CustomLinkerArg Include="-Wl,-soname,lib$(NativeBinary)$(NativeBinaryExt)" />
</ItemGroup>

<Import Project="Hello-NativeAOTFromJNI.targets" />
</Project>
53 changes: 53 additions & 0 deletions samples/Hello-NativeAOTFromJNI/Hello-NativeAOTFromJNI.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<Project>

<Target Name="BuildNativeAOTFromJNIJar"
BeforeTargets="Build"
Inputs="@(HelloNativeAOTFromJNIJar)"
Outputs="$(OutputPath)hello-from-java.jar">
<MakeDir Directories="$(IntermediateOutputPath)h-classes" />
<ItemGroup>
<_Source Include="@(HelloNativeAOTFromJNIJar->Replace('%5c', '/'))" />
</ItemGroup>
<WriteLinesToFile
File="$(IntermediateOutputPath)_java_sources.txt"
Lines="@(_Source)"
Overwrite="True"
/>
<ItemGroup>
<_JavacOpt Include="$(_JavacSourceOptions)" />
<_JavacOpt Include="-d &quot;$(IntermediateOutputPath)h-classes&quot; " />
<_JavacOpt Include="-classpath &quot;$(OutputPath)java-interop.jar&quot; " />
<_JavacOpt Include="&quot;@$(IntermediateOutputPath)_java_sources.txt&quot;" />
<_JavacOpt Include="-h &quot;$(IntermediateOutputPath)h-classes&quot; " />
</ItemGroup>
<Exec Command="&quot;$(JavaCPath)&quot; @(_JavacOpt, ' ')" />
<Delete Files="$(IntermediateOutputPath)_java_sources.txt" />
<Exec Command="&quot;$(JarPath)&quot; cf &quot;$(OutputPath)hello-from-java.jar&quot; -C &quot;$(IntermediateOutputPath)h-classes&quot; ." />
</Target>

<Target Name="_NativeLibRequiresLibPrefix"
AfterTargets="Publish">
<Copy
Condition=" '$(OS)' != 'Windows_NT' "
SourceFiles="$(PublishDir)$(AssemblyName).dylib"
DestinationFiles="$(PublishDir)lib$(AssemblyName).dylib"
/>
<Copy SourceFiles="$(OutputPath)hello-from-java.jar" DestinationFolder="$(PublishDir)" />
</Target>

<Target Name="RunJavaSample">
<ItemGroup>
<_Classpath Include="hello-from-java.jar" />
<_Classpath Include="java-interop.jar" />
</ItemGroup>
<PropertyGroup>
<_CPSep Condition=" '$(OS)' == 'Windows_NT' ">;</_CPSep>
<_CPSep Condition=" '$(_CPSep)' == '' ">:</_CPSep>
<_CP>@(_Classpath, '$(_CPSep)')</_CP>
</PropertyGroup>
<Exec
Command="&quot;$(JavaPath)&quot; -classpath &quot;$(_CP)&quot; com/microsoft/hello_from_jni/App"
WorkingDirectory="$(PublishDir)"
akoeplinger marked this conversation as resolved.
Show resolved Hide resolved
/>
</Target>
</Project>
44 changes: 44 additions & 0 deletions samples/Hello-NativeAOTFromJNI/JNIEnvInit.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
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 {
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;
}
}

[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 IntPtr sayHello (IntPtr jnienv, IntPtr klass)
{
var s = $"Hello from .NET NativeAOT!";
jonpryor marked this conversation as resolved.
Show resolved Hide resolved
Console.WriteLine (s);
var h = JniEnvironment.Strings.NewString (s);
var r = JniEnvironment.References.NewReturnToJniRef (h);
JniObjectReference.Dispose (ref h);
return r;
}
}
67 changes: 67 additions & 0 deletions samples/Hello-NativeAOTFromJNI/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# 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

% (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
[3]: https://github.com/dotnet/samples/blob/main/core/nativeaot/NativeLibrary/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.microsoft.hello_from_jni;

class App {

public static void main(String[] args) {
System.out.println("Hello from Java!");
String s = NativeAOTInit.sayHello();
System.out.println("String returned to Java: " + s);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.microsoft.hello_from_jni;

class NativeAOTInit {
static {
System.loadLibrary("Hello-NativeAOTFromJNI");
}

public static native String sayHello();
}
3 changes: 2 additions & 1 deletion src/Java.Interop/Java.Interop.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@
<OutputPath>$(ToolOutputFullPath)</OutputPath>
<DocumentationFile>$(ToolOutputFullPath)Java.Interop.xml</DocumentationFile>
<JNIEnvGenPath>$(BuildToolOutputFullPath)</JNIEnvGenPath>
<LangVersion Condition=" '$(JIBuildingForNetCoreApp)' == 'True' ">9.0</LangVersion>
<LangVersion Condition=" '$(JIBuildingForNetCoreApp)' == 'True' ">11.0</LangVersion>
<LangVersion Condition=" '$(LangVersion)' == '' ">8.0</LangVersion>
<Version>$(JICoreLibVersion)</Version>
<Standalone Condition=" '$(Standalone)' == '' ">true</Standalone>
</PropertyGroup>
<PropertyGroup Condition=" '$(Standalone)' == 'True' ">
<DefineConstants Condition=" '$(JIBuildingForNetCoreApp)' == 'True' ">FEATURE_JNIENVIRONMENT_JI_FUNCTION_POINTERS;$(DefineConstants)</DefineConstants>
Expand Down
39 changes: 38 additions & 1 deletion src/Java.Interop/Java.Interop/JniEnvironment.Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand All @@ -262,8 +265,42 @@ public static void RegisterNatives (JniObjectReference type, JniNativeMethodRegi
}
#endif // DEBUG && NETCOREAPP

int r = _RegisterNatives (type, methods, numMethods);
Span<JniBlittableNativeMethodRegistration> blittableMethods
= stackalloc JniBlittableNativeMethodRegistration [numMethods];
Span<IntPtr> 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<JniBlittableNativeMethodRegistration> 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));
Expand Down
66 changes: 66 additions & 0 deletions src/Java.Interop/Java.Interop/JniNativeMethodRegistration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,70 @@ public JniNativeMethodRegistration (string name, string signature, Delegate mars
Marshaler = marshaler ?? throw new ArgumentNullException (nameof (marshaler));
}
}

public struct JniBlittableNativeMethodRegistration : IEquatable<JniBlittableNativeMethodRegistration> {

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<byte> name, ReadOnlySpan<byte> 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<byte> would not prevent it from being returned or passed around to elsewhere.
static unsafe IntPtr FromReadOnlySpan (ReadOnlySpan<byte> 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);
}
}
7 changes: 7 additions & 0 deletions src/Java.Interop/Java.Interop/JniType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,13 @@ public void RegisterNativeMethods (params JniNativeMethodRegistration[] methods)
RegisterWithRuntime ();
}

public void RegisterNativeMethods (ReadOnlySpan<JniBlittableNativeMethodRegistration> methods)
{
AssertValid ();
JniEnvironment.Types.RegisterNatives (PeerReference, methods);
RegisterWithRuntime ();
}

public void UnregisterNativeMethods ()
{
AssertValid ();
Expand Down
Loading