The FFM APIs mentioned by @LouisWasserman are not stable yet. But I did more research and found that the VarHandle API lets us perform atomic store/loads/ops with any memory order of our choice on any Java value: fields, array elements, bytebuffer elements and more.
Note: it's extremely hard to test the correctness of concurrent code, I'm not 100% sure that my answer is memory-safe.
For the sake of simplicity, I'll focus on a release-acquire scenario, but I don't see any reason why atomic_fetch_add
wouldn't work. My idea is to share a ByteBuffer between C and Java, since they're made specifically for that. Then you can write all the data you want in the ByteBuffer, and in my specific case about Java-to-C transfer, you can do an atomic release-store to make sure that all data written prior to the atomic store will be visible to anyone acquire-loading the changed "ready" flag. For some reason, using a byte for the flag rather than an int throws an UnsupportedOperationException
. The C code can treat the ByteBuffer's backing memory as whatever it wants (such as volatile fields in a struct) and load them using usual atomic functions.
I'm assuming that a good JVM should easily be able to optimise hot ByteBuffer read/stores into simple instructions (not involving method calls), so this approach should definitely be faster than doing JNI calls on AtomicIntegers from the C side. As a final note, atomics are hard to do right, and you should definitely use them only if the performance gain is measurable.
I don't think StackOverflow supports collapsible sections, sorry for the visual noise.
This example uses a memory map to have shared memory between Java and C, but JNI should work just as well. If using JNI, you should use env->GetDirectBufferAddress
to obtain the void*
address of a direct ByteBuffer instance's internal buffer.
How to use: Run the Java program first. When it tells you to, run the C program. Go back to the Java console, enter some text and press enter. The C code will print it and exit.
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Scanner;
public class Main {
private static final int MMAP_SIZE = 256;
private static final VarHandle BYTE_BUFFER_INT_HANDLE = MethodHandles.byteBufferViewVarHandle(int[].class, ByteOrder.BIG_ENDIAN);
public static void main(String[] args) throws IOException {
try (var mmapFile = FileChannel.open(Path.of("mmap"), StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.READ, StandardOpenOption.TRUNCATE_EXISTING)) {
assert mmapFile.write(ByteBuffer.wrap(new byte[0]), MMAP_SIZE) == MMAP_SIZE;
var bb = mmapFile.map(FileChannel.MapMode.READ_WRITE, 0, MMAP_SIZE);
// Fill the byte buffer with zeros
for (int i = 0; i < MMAP_SIZE; i++) {
bb.put((byte) 0);
}
bb.force();
System.out.println("You can start the C program now");
// Write the user-inputted string after the first int (which corresponds to the "ready" flag)
System.out.print("> ");
String input = new Scanner(System.in).nextLine();
bb.position(4);
bb.put(StandardCharsets.UTF_8.encode(input));
// When the text has been written to the buffer, release the text by setting the "ready" flag to 1
BYTE_BUFFER_INT_HANDLE.setRelease(bb, 0, 1);
}
}
}
#include <sys/mman.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdatomic.h>
#define MMAP_SIZE 256
#define PAYLOAD_MAX_SIZE (MMAP_SIZE - 4)
typedef struct {
volatile int32_t ready;
char payload[PAYLOAD_MAX_SIZE];
} shared_memory;
int main() {
int mapFile = open("mmap", O_RDONLY);
if (mapFile == -1) {
perror("Error opening mmap file, the Java program should be running right now");
return 1;
}
shared_memory* map = (shared_memory*) mmap(NULL, MMAP_SIZE, PROT_READ, MAP_SHARED, mapFile, 0);
if (map == MAP_FAILED) {
perror("mmap failed");
close(mapFile);
return 1;
}
int ready;
while (!(ready = atomic_load_explicit(&map->ready, memory_order_acquire))) {
sleep(1);
}
printf("Received: %.*s", PAYLOAD_MAX_SIZE, map->payload);
}