Skip to content

Commit

Permalink
Published under GPLv2 per JVMCI license requirements
Browse files Browse the repository at this point in the history
  • Loading branch information
apangin committed Oct 15, 2022
0 parents commit 045ba37
Show file tree
Hide file tree
Showing 22 changed files with 1,461 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/out/
/.idea/
*.iml
339 changes: 339 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions MANIFEST.MF
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Premain-Class: one.nalim.Agent
182 changes: 182 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
# nalim

Nalim is a library for linking Java methods to native functions using
[JVMCI](https://openjdk.org/jeps/243) (JVM compiler interface).

Unlike other Java frameworks for native library access, nalim does not
use JNI and therefore does not incur [JNI related overhead](https://stackoverflow.com/a/24747484/3448419).

When calling a native function with nalim
- a thread does not switch from `in_Java` to `in_native` state and back;
- no memory barrier is involved;
- no JNI handles are created;
- exception checks and safepoint checks are omitted;
- native function can access primitive arrays directly in the heap.

As a result, native calls become faster comparing to JNI, especially when
a target function is short. In this sense, nalim is similar to
[JNI Critical Natives](https://stackoverflow.com/a/36309652/3448419),
but relies on a standard supported interface. JNI Critical Natives
have been [deprecated](https://bugs.openjdk.org/browse/JDK-8233343) in JDK 16
and [obsoleted](https://bugs.openjdk.org/browse/JDK-8258192) since JDK 18,
so nalim can serve as a replacement.

### Examples

#### 1. Basic usage

```java
public class Libc {

@Link
public static native int getuid();

@Link
public static native int getgid();

static {
Linker.linkClass(Libc.class);
}
}
```
```
System.out.println("My user id = " + Libc.getuid());
```

#### 2. Linking by a different name

```java
public class Mem {

@Link(name = "malloc")
public static native long allocate(long size);

@Link(name = "free")
public static native void release(long ptr);

static {
Linker.linkClass(Mem.class);
}
}
```

#### 3. Working with arrays

```java
@Library("ssl")
public class LibSSL {

public static byte[] sha256(byte[] data) {
byte[] digest = new byte[32];
SHA256(data, data.length, digest);
return digest;
}

@Link
private static native void SHA256(byte[] data, int len, byte[] digest);
}
```

#### 4. Inlining raw machine code

```java
public class Cpu {

// rdtsc
// shl $0x20,%rdx
// or %rdx,%rax
// ret
@Code({15, 49, 72, -63, -30, 32, 72, 9, -48, -61})
public static native long rdtsc();

static {
Linker.linkClass(Cpu.class);
}
}
```

### Running

#### 1. As an agent

```
java -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI \
-javaagent:nalim.jar -cp <classpath> MainClass
```

This is the simplest way to add nalim to your application,
as the agent exports all required JDK internal packages for you.

The agent optionally accepts a list of classes whose native methods
will be automatically linked at startup:
```
-javaagent:nalim.jar=com.example.MyLib,com.example.OtherLib
```

#### 2. On the classpath

If not adding nalim as an agent, you'll have to add all required
`--add-exports` manually.

```
java -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI \
--add-exports jdk.internal.vm.ci/jdk.vm.ci.code=ALL-UNNAMED \
--add-exports jdk.internal.vm.ci/jdk.vm.ci.code.site=ALL-UNNAMED \
--add-exports jdk.internal.vm.ci/jdk.vm.ci.hotspot=ALL-UNNAMED \
--add-exports jdk.internal.vm.ci/jdk.vm.ci.meta=ALL-UNNAMED \
--add-exports jdk.internal.vm.ci/jdk.vm.ci.runtime=ALL-UNNAMED \
-cp nalim.jar:app.jar MainClass
```

### Performance

JMH benchmark for comparing regular JNI calls with nalim calls is available
[here](https://github.com/apangin/nalim/blob/master/example/one/nalim/bench).

The following results were obtained on Intel Core i7-1280P CPU with JDK 17.0.4.1.

#### Simple native method

```
static native int add(int a, int b);
```

```
Benchmark Mode Cnt Score Error Units
JniBench.add_jni avgt 10 6,535 ± 0,225 ns/op
JniBench.add_nalim avgt 10 0,862 ± 0,035 ns/op
```

#### Array processing

```
static native long max(long[] array, int length);
```

```
Benchmark (length) Mode Cnt Score Error Units
JniBench.max_jni 10 avgt 10 25,103 ± 0,994 ns/op
JniBench.max_jni 100 avgt 10 55,981 ± 2,930 ns/op
JniBench.max_jni 1000 avgt 10 433,106 ± 1,661 ns/op
JniBench.max_nalim 10 avgt 10 3,477 ± 0,215 ns/op
JniBench.max_nalim 100 avgt 10 38,368 ± 2,348 ns/op
JniBench.max_nalim 1000 avgt 10 420,540 ± 4,049 ns/op
```

### Supported platforms

- **Linux:** amd64 aarch64
- **macOS:** amd64 aarch64
- **Windows:** amd64

### Limitations

A native function called with nalim has certain limitations comparing to a regular
JNI function.

1. It must be `static`.
2. It does not have access to `JNIEnv` and therefore cannot call JNI functions,
in particular, it cannot throw exceptions.
3. Only primitive types and primitive arrays can be passed as arguments.
4. A function must return as soon as possible, since it blocks JVM from reaching
a safepoint.
11 changes: 11 additions & 0 deletions build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/bin/sh
javac --add-modules jdk.internal.vm.ci \
--add-exports jdk.internal.vm.ci/jdk.vm.ci.code=ALL-UNNAMED \
--add-exports jdk.internal.vm.ci/jdk.vm.ci.code.site=ALL-UNNAMED \
--add-exports jdk.internal.vm.ci/jdk.vm.ci.hotspot=ALL-UNNAMED \
--add-exports jdk.internal.vm.ci/jdk.vm.ci.meta=ALL-UNNAMED \
--add-exports jdk.internal.vm.ci/jdk.vm.ci.runtime=ALL-UNNAMED \
--source 11 --target 11 \
-d . src/one/nalim/*.java

jar cfm nalim.jar MANIFEST.MF one
59 changes: 59 additions & 0 deletions example/one/nalim/bench/JniBench.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package one.nalim.bench;

import one.nalim.Link;
import one.nalim.Linker;
import org.openjdk.jmh.annotations.*;

import java.util.concurrent.ThreadLocalRandom;

@State(Scope.Benchmark)
public class JniBench {

int a = 123;
int b = 45678;

@Param({"10", "100", "1000"})
int length;

long[] array;

@Setup
public void setup() {
array = ThreadLocalRandom.current().longs(length).toArray();
}

@Benchmark
public long add_jni() {
return add(a, b);
}

@Benchmark
public long add_nalim() {
return raw_add(a, b);
}

@Benchmark
public long max_jni() {
return max(array, array.length);
}

@Benchmark
public long max_nalim() {
return raw_max(array, array.length);
}

static native int add(int a, int b);

static native long max(long[] array, int length);

@Link
static native int raw_add(int a, int b);

@Link
static native long raw_max(long[] array, int length);

static {
System.loadLibrary("jnibench");
Linker.linkClass(JniBench.class);
}
}
36 changes: 36 additions & 0 deletions example/one/nalim/bench/jnibench.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#include <jni.h>

JNIEXPORT jint JNICALL
Java_bench_JniBench_add(JNIEnv* env, jclass unused, jint a, jint b) {
return a + b;
}

JNIEXPORT jint JNICALL
raw_add(jint a, jint b) {
return a + b;
}

JNIEXPORT jlong JNICALL
Java_bench_JniBench_max(JNIEnv* env, jclass unused, jlongArray array, jint length) {
jboolean isCopy;
jlong* data = (jlong*) (*env)->GetPrimitiveArrayCritical(env, array, &isCopy);

jlong max = 1LL << 63;
jint i;
for (i = 0; i < length; i++) {
if (data[i] > max) max = data[i];
}

(*env)->ReleasePrimitiveArrayCritical(env, array, data, JNI_ABORT);
return max;
}

JNIEXPORT jlong JNICALL
raw_max(jlong* data, jint length) {
jlong max = 1LL << 63;
jint i;
for (i = 0; i < length; i++) {
if (data[i] > max) max = data[i];
}
return max;
}
18 changes: 18 additions & 0 deletions example/one/nalim/example/Cpu.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package one.nalim.example;

import one.nalim.Code;
import one.nalim.Linker;

public class Cpu {

// rdtsc
// shl $0x20,%rdx
// or %rdx,%rax
// ret
@Code({15, 49, 72, -63, -30, 32, 72, 9, -48, -61})
public static native long rdtsc();

static {
Linker.linkClass(Cpu.class);
}
}
17 changes: 17 additions & 0 deletions example/one/nalim/example/LibSSL.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package one.nalim.example;

import one.nalim.Library;
import one.nalim.Link;

@Library("ssl")
public class LibSSL {

public static byte[] sha256(byte[] data) {
byte[] digest = new byte[32];
SHA256(data, data.length, digest);
return digest;
}

@Link
private static native void SHA256(byte[] data, int len, byte[] digest);
}
17 changes: 17 additions & 0 deletions example/one/nalim/example/Libc.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package one.nalim.example;

import one.nalim.Link;
import one.nalim.Linker;

public class Libc {

@Link
public static native int getuid();

@Link
public static native int getgid();

static {
Linker.linkClass(Libc.class);
}
}
17 changes: 17 additions & 0 deletions example/one/nalim/example/Mem.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package one.nalim.example;

import one.nalim.Link;
import one.nalim.Linker;

public class Mem {

@Link(name = "malloc")
public static native long allocate(long size);

@Link(name = "free")
public static native void release(long ptr);

static {
Linker.linkClass(Mem.class);
}
}
20 changes: 20 additions & 0 deletions example/one/nalim/example/Time.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package one.nalim.example;

import one.nalim.Link;
import one.nalim.Linker;

public class Time {

public static long[] current() {
long[] result = new long[2];
clock_gettime(0, result);
return result;
}

@Link
private static native void clock_gettime(int clk_id, long[] tp);

static {
Linker.linkClass(Time.class);
}
}
Loading

0 comments on commit 045ba37

Please sign in to comment.