package {{ package }};
import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
/**
* Service wrapper for {{ service_name }} using Panama FFM.
*
* Binds to C FFI symbols:
* - {{ ffi_prefix }}_{{ service_snake }}_new() -> opaque handle
* - {{ ffi_prefix }}_{{ service_snake }}_free(opaque)
{{ bindings_doc }} */
@SuppressWarnings("PMD")
public class {{ class_name }} implements AutoCloseable {
private long ownerHandle;
private int activeOwnerBorrows;
private boolean ownerUnavailable;
private boolean ownerTransferPending;
private boolean serviceArenaClosed;
private final Arena arena = Arena.ofShared();
private final Object ownerMutationLock = new Object();
@FunctionalInterface
private interface ServiceReleaser {
void release(MemorySegment handle) throws Throwable;
}
private static final class ServiceResources implements AutoCloseable {
private record Entry(MemorySegment handle, ServiceReleaser releaser) {}
private final java.util.ArrayDeque<Entry> entries = new java.util.ArrayDeque<>();
private MemorySegment register(MemorySegment handle, ServiceReleaser releaser) {
if (handle != null && !handle.equals(MemorySegment.NULL)) entries.push(new Entry(handle, releaser));
return handle;
}
@Override
public void close() {
RuntimeException aggregate = null;
while (!entries.isEmpty()) {
Entry entry = entries.pop();
try {
entry.releaser().release(entry.handle());
} catch (Throwable failure) {
if (aggregate == null) aggregate = new RuntimeException("Service resource cleanup failed");
aggregate.addSuppressed(failure);
}
}
if (aggregate != null) throw aggregate;
}
}
private final class OwnerHandleLease implements AutoCloseable {
private final long borrowedHandle;
private boolean released;
private OwnerHandleLease(long borrowedHandle) {
this.borrowedHandle = borrowedHandle;
}
private long handle() {
return borrowedHandle;
}
@Override
public void close() {
synchronized ({{ class_name }}.this) {
if (released) return;
released = true;
activeOwnerBorrows--;
if (activeOwnerBorrows == 0) {{ class_name }}.this.notifyAll();
}
}
}
private final class OwnerHandleTransfer implements AutoCloseable {
private long transferredHandle;
private OwnerHandleTransfer(long transferredHandle) {
this.transferredHandle = transferredHandle;
}
private synchronized long handle() {
if (transferredHandle == 0) {
throw new IllegalStateException("Service owner transfer is no longer active");
}
return transferredHandle;
}
private synchronized void commit() {
handle();
transferredHandle = 0;
commitOwnerTransfer();
}
@Override
public synchronized void close() {
if (transferredHandle == 0) return;
rollbackOwnerTransfer(transferredHandle);
transferredHandle = 0;
}
}
private synchronized OwnerHandleLease borrowOwnerHandle() {
if (ownerUnavailable || ownerHandle == 0) {
throw new IllegalStateException("{{ class_name }} is closed");
}
activeOwnerBorrows++;
return new OwnerHandleLease(ownerHandle);
}
private synchronized long takeOwnerHandleForClose() {
if (ownerTransferPending) awaitOwnerTransfer();
if (ownerUnavailable) return 0;
ownerUnavailable = true;
awaitBorrowedCalls();
long detached = ownerHandle;
ownerHandle = 0;
return detached;
}
private void awaitBorrowedCalls() {
boolean interrupted = false;
while (activeOwnerBorrows != 0) {
try {
wait();
} catch (InterruptedException ignored) {
interrupted = true;
}
}
if (interrupted) Thread.currentThread().interrupt();
}
private synchronized OwnerHandleTransfer takeOwnerHandle() {
if (ownerUnavailable || ownerHandle == 0) {
throw new IllegalStateException("{{ class_name }} is closed");
}
OwnerHandleTransfer transfer = new OwnerHandleTransfer(ownerHandle);
ownerUnavailable = true;
awaitBorrowedCalls();
ownerHandle = 0;
ownerTransferPending = true;
return transfer;
}
private synchronized void rollbackOwnerTransfer(long transferredHandle) {
ownerHandle = transferredHandle;
ownerTransferPending = false;
ownerUnavailable = false;
notifyAll();
}
private synchronized void commitOwnerTransfer() {
ownerTransferPending = false;
notifyAll();
}
private void awaitOwnerTransfer() {
boolean interrupted = false;
while (ownerTransferPending) {
try {
wait();
} catch (InterruptedException ignored) {
interrupted = true;
}
}
if (interrupted) Thread.currentThread().interrupt();
}
private synchronized boolean markServiceArenaClosed() {
if (serviceArenaClosed) return false;
serviceArenaClosed = true;
return true;
}
private synchronized void replaceOwnerHandle(long expected, long replacement) {
if (ownerUnavailable || ownerHandle != expected) {
throw new IllegalStateException("{{ class_name }} owner changed while configuring");
}
if (replacement == 0) {
throw new IllegalStateException("{{ class_name }} configuration returned null");
}
ownerHandle = replacement;
}
private static final Linker LINKER = Linker.nativeLinker();
private static final SymbolLookup LOOKUP = SymbolLookup.loaderLookup();
static {
// Force NativeLib static initialization to load the native library
// This ensures all FFI symbols are available before we try to look them up.
// NativeLib is a package-private class, but accessing its class forces
// its static initializer to run and load the native library.
try {
Class.forName("{{ package }}.NativeLib");
} catch (ClassNotFoundException ignored) {
// NativeLib not available; native library may be pre-loaded
}
}
// Adapter for handler upcalls: marshals C pointers <-> Java strings
private static MemorySegment invokeHandlerWithMarshal(
MemorySegment contextPtr,
MemorySegment requestPtr,
Callable handler,
Arena arena) throws Throwable {
try {
// Upcall pointer args arrive as zero-length native segments (the linker has
// no length for a raw `*const c_char`). Reinterpret to an unbounded segment
// before reading, otherwise getString() throws and the uncaught exception
// aborts the VM.
String requestStr = requestPtr.reinterpret(Long.MAX_VALUE).getString(0);
String responseStr = handler.handle(requestStr);
// Allocate response string using malloc (not Arena) so the pointer remains
// valid after this upcall returns. Rust invokes freeHandlerResponse for it.
// Use a confined arena for the temporary scratch allocation: it is
// closeable (unlike Arena.ofAuto(), whose close() throws
// "non-closeable session"), and its memory is freed deterministically at
// block exit — safe because the bytes are copied into malloc'd memory first.
try (Arena scratchArena = Arena.ofConfined()) {
MemorySegment responseSegment = scratchArena.allocateFrom(responseStr);
long responseLen = responseSegment.byteSize();
// Call malloc to allocate memory that persists beyond this upcall.
MethodHandle mallocHandle = LINKER.downcallHandle(
LOOKUP.find("malloc").orElseThrow(),
FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.JAVA_LONG)
);
MemorySegment mallocAddr = (MemorySegment) mallocHandle.invoke(responseLen);
// Reinterpret the malloc'd segment to its actual size before copying.
// The FFI linker returns a zero-sized segment for bare ADDRESS returns;
// reinterpret(size) gives it the proper bounds for MemorySegment.copy().
MemorySegment mallocSegment = mallocAddr.reinterpret(responseLen);
// Copy the response bytes to malloc'd memory.
MemorySegment.copy(responseSegment, 0, mallocSegment, 0, responseLen);
return mallocAddr;
}
} catch (Throwable failure) {
return MemorySegment.NULL;
}
}
private static void freeHandlerResponse(MemorySegment responsePtr) {
try {
if (responsePtr.address() != 0) {
// Resolved lazily (not at class-init time): "free" only needs to be
// reachable through LOOKUP once a handler response is actually
// released, so a service that never frees a response never pays for
// (or can fail on) this lookup during class initialization.
MethodHandle freeHandle = LINKER.downcallHandle(
LOOKUP.find("free").orElseThrow(),
FunctionDescriptor.ofVoid(ValueLayout.ADDRESS)
);
freeHandle.invokeExact(responsePtr);
}
} catch (Throwable error) {
throw new IllegalStateException("Failed to free handler response", error);
}
}