alef 0.61.1

Opinionated polyglot binding generator for Rust libraries
Documentation
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);
        }
    }