Skip to content

Guide list | Previous: C++ | Next: Node.js

Java Binding Guide (systems.zlink)

Contract-owning document for this chapter — the Java bindings spec covers it. This chapter shows that contract as working sample code.

Explains how to use zlink in Java through working sample code. The deep explanation of messaging concepts is owned by the core guide; this guide focuses on using the Java API.


Installation

Add via Gradle or Maven. The native core is bundled per platform.

Gradle (build.gradle):

dependencies {
    implementation 'systems.zlink:zlink:0.9.0'
}

Maven (pom.xml):

<dependency>
    <groupId>systems.zlink</groupId>
    <artifactId>zlink</artifactId>
    <version>0.9.0</version>
</dependency>
  • Java 25 or later. The library calls Core through the FFM (Foreign Function & Memory) API, so run with --enable-native-access=ALL-UNNAMED (classpath) or --enable-native-access=systems.zlink (module path).
  • No separate native install — the per-RID shared library loads automatically.
import systems.zlink.contracts.core.Zlink;
import systems.zlink.contracts.core.Context;
import systems.zlink.contracts.messaging.Message;
import systems.zlink.contracts.messaging.Received;

5-Minute Example

A minimal example with a Pair socket where one side sends PING and the other replies with ACK. Every resource is managed with try-with-resources.

// Server
try (Context ctx = Zlink.createContext();
     var server = ctx.createPairSocket()) {

    server.bind("tcp://127.0.0.1:5555");

    try (Received received = new Received()) {
        server.recv(received, RecvFlags.NONE);
        String text = received.firstPart().toUtf8String();
        System.out.println(text); // PING

        try (Message reply = Message.from("ACK")) {
            server.send().message(reply).submit_sync();
        }
    }
}
// Client
try (Context ctx = Zlink.createContext();
     var client = ctx.createPairSocket()) {

    client.connect("tcp://127.0.0.1:5555");

    try (Message ping = Message.from("PING")) {
        client.send().message(ping).submit_sync();
    }

    try (Received received = new Received()) {
        client.recv(received, RecvFlags.NONE);
        System.out.println(received.firstPart().toUtf8String()); // ACK
    }
}

Core Types

The 4 fundamental types every feature shares.

1. Context

The runtime entry point for a process. Implements AutoCloseable, so it's managed with try-with-resources. Closing the context interrupts blocking operations on its child sockets/services.

try (Context ctx = Zlink.createContext()) {
    // create sockets and services here
    var socket = ctx.createPairSocket();
    // ...
} // ctx.close() runs automatically → child sockets shut down

Adjust the I/O thread count:

ctx.options().ioThreads(4);

2. Message

Owns a single payload frame. Implements AutoCloseable. Sending transfers ownership, so it doesn't need a separate close. If the send fails, ownership is retained, so retry or close it explicitly.

// build a copy from a string
try (Message msg = Message.from("payload")) {
    socket.send().message(msg).submit_sync();
}
// once submit succeeds msg is already consumed — closing the try block is harmless

// build a copy from a byte array
try (Message msg = Message.from(bytes)) { ... }

// allocate a sized empty frame
try (Message msg = new Message(256)) {
    msg.mutableDataBuffer().put(data);
    socket.send().message(msg).submit_sync();
}

HWM-managed sends provide asynchronous submit() and synchronous submit_sync() terminals. submit_sync() stops the current thread until Core admits the record locally. submit() uses DONTWAIT and returns a CompletionStage<Void> settled from the socket completion queue.

socket.send().message(message).submit_sync(); // synchronous Core admission
CompletionStage<Void> completion = socket.send().message(message).submit(); // asynchronous

Request provides submit_sync() to block until the reply and submit() to return a CompletionStage<List<Message>> settled from the socket completion queue. The reply is that terminal result, not DATA received separately.

Core owns retry after accepting a pre-admission operation; do not create a caller retry queue or resubmit its payload. The shared native ZLINK_OPT_PENDING_MAX_MSGS/BYTES limits cover pending SEND and REQUEST; no send-only pending names exist. Completion confirms local admission, not peer delivery or an application acknowledgement.

Canceling a Java Future/coroutine can stop the language waiter. Before Core submit, abort without calling Core; after Core accepts the payload, admission or request work may continue and the socket owner drains a late completion. Set stream.options().recvMode(StreamRecvMode.RAW) or .PACKET before bind/connect, then use recv or recvPacket respectively.

If a public poller owns PollEventFlags.POLLCOMPLETION for a socket, another thread must keep wait() looping while a blocking request or CompletionStage is pending. wait() drains native completions and settles or cleans Java state; calling a blocking terminal between waits on the same thread can stall it.

Calling recv(..., RecvFlags.NONE) directly blocks the current Java thread in native recv. This surface is a low-level socket API. On a framework path handling many sessions/handlers, don't put this call directly on a handler thread — wait for readiness with a Poller, then do a RecvFlags.DONT_WAIT recv on the ready socket. Application handlers run behind the handler executor the framework configures.

try (Poller poller = Zlink.createPoller()) {
    poller.add(socket, 1L, PollEventFlags.POLLIN);
    PollEvents events = new PollEvents(16);

    int count = poller.wait(events, Duration.ofMillis(10));
    for (int i = 0; i < count; i++) {
        while (true) {
            Received received = new Received();
            if (!socket.recv(received, RecvFlags.DONT_WAIT)) {
                received.close();
                break;
            }
            handlerExecutor.execute(() -> {
                try (received) {
                    handle(received);
                }
            });
        }
    }
}

Reading a received message:

int size = msg.size();
String text = msg.toUtf8String();    // UTF-8 conversion
byte[] data = msg.data();            // copy into a byte array
ByteBuffer buf = msg.dataBuffer();   // read-only view

3. Received — the receive envelope

Holds a received message envelope. Carries a routing ID, part list, and an optional reply context. Reusable. Implements AutoCloseable.

try (Received received = new Received()) {
    socket.recv(received, RecvFlags.NONE);

    // single-part access
    Message part = received.firstPart();         // first part
    Message part = received.singlePartOrThrow();  // must be exactly one part

    // multipart access
    List<Message> parts = received.parts();

    // routing ID (present on ROUTER/SPOT receive)
    Optional<RoutingId> rid = received.getRoutingId();
}

4. RoutingId

An immutable value of 1-255 bytes identifying a peer or spot.

RoutingId rid = RoutingId.from("server-01".getBytes(StandardCharsets.UTF_8));
RoutingId rid = RoutingId.from("server-01");

Ownership And Lifetime

The Java binding's ownership rules. try-with-resources is the default pattern.

Situation Rule
A send terminal succeeds ownership of the added Message transfers to the send stack. No separate close() needed
A send terminal fails (exception or failed stage) ownership is retained by the caller. try-with-resources handles it automatically
recv() succeeds the caller owns the Received. try-with-resources required
Request submit() completes the reply List<Message> is caller-owned. Needs Message.closeAll(reply)
Context.close() interrupts every blocking operation under the context
// pattern: safe via try-with-resources
try (Message msg = Message.from("data")) {
    socket.send().message(msg).submit_sync();
    // returning means msg was consumed; back-pressure is a ZlinkSubmitException
} // if submit throws, try-with-resources closes msg

Share / Move / Clone (copy / move / clone)

Three explicit Message payload operations, with the same name and meaning across every binding, mapping 1:1 to the Core C API (zlink_msg_copy/zlink_msg_move).

Operation Signature Meaning When
copy() Message copy() ref-count share — new Message on the same buffer, original stays valid keep the same payload while still using the original
move(dest) void move(Message dest) ownership transfer — hands off to dest, caller left empty re-send a received message with no copy (relay/echo)
clone() Message clone() deep copy — independent buffer mutate the duplicate independently
try (Message shared = msg.copy()) {
    socket.send().message(shared).submit_sync();   // shared is consumed
}
// msg is still valid

Message out = new Message();
receivedPart.move(out);                             // receivedPart becomes empty
socket.send(routingId).message(out).submit_sync();

try (Message dup = msg.clone()) { /* ... */ }

copy() is a ref-share and does not guarantee mutation isolation — use clone() for that. (sharedCopyOf/moveInto/moveTo were internal, not public API, so the only public-surface change is adding copy/move/clone.)


Error Handling

The Java binding throws exceptions from the ZlinkException hierarchy.

try (Message msg = Message.from("data")) {
    socket.send().message(msg).submit_sync();
} catch (ZlinkSubmitException e) {
    switch (e.getResult()) {
        case BACKPRESSURED -> { /* retry shortly */ }
        case NOT_CONNECTED -> { /* no connected peer */ }
        default -> throw e;
    }
}

Exception types:

Exception class Raised when Result field
ZlinkSubmitException send/publish failure getResult(): SubmitResult
ZlinkRequestException request failure getResult(): RequestResult
ZlinkRecvException receive failure getResult(): RecvResult
ZlinkBindException bind failure getResult(): BindResult
ZlinkConnectException connect failure getResult(): ConnectResult
ZlinkConfigException option-set failure getResult(): ConfigResult
ZlinkCloseException close failure getResult(): CloseResult
ZlinkHandlerException handler registration failure getResult(): HandlerResult

Every exception inherits from ZlinkException and exposes getCode() and getInternalErrno() to check the native code.


C API Mapping

C API Java API
zlink_ctx_new() Zlink.createContext()
zlink_ctx_term() ctx.close()
zlink_socket(ctx, type) ctx.createPairSocket(), etc.
zlink_close(socket) socket.close()
zlink_bind(socket, ep) socket.bind(ep)
zlink_connect(socket, ep) socket.connect(ep)
zlink_send(..., parts, count, ...) / zlink_send_rid(..., parts, count, ...) + NONE socket.send().message(m).submit_sync()
DONTWAIT send + completion pull socket.send().message(m).submit() (CompletionStage)
zlink_recv(..., parts_out, capacity, count_out, ...) socket.recv(received, flags)
zlink_msg_data(msg) msg.data()
zlink_msg_size(msg) msg.size()
zlink_msg_close(msg) msg.close()
zlink_routing_id_t RoutingId
zlink_socket_monitor_open(...) socket.monitorOpen(...)
zlink_poller_new() Zlink.createPoller()
zlink_timer_new() Zlink.createTimer()

Native Library / Deployment

The Java binding embeds a per-platform shared library. No separate install — just add it via Gradle/Maven.

Checking the native version in use:

int[] version = Zlink.version();
System.out.printf("zlink %d.%d.%d%n", version[0], version[1], version[2]);

Checking whether a specific feature is supported:

if (Zlink.has("draft")) {
    System.out.println("draft API supported");
}

Threading: Context can be shared across threads, but sockets must be used from a single thread only. Dispatch handlers are invoked on zlink's internal worker threads, so avoid blocking for long inside a handler. submit_sync() stops the current platform thread or virtual thread while waiting for HWM admission. Other threads and virtual threads continue to run, so this is safe in those execution environments. Use asynchronous submit() to keep the caller available. See thread safety for details.


Samples

Verified sample code lives at bindings/java/samples/Zlink.Samples/src/main/java/systems/zlink/samples/.

Sample class Description
PairRecvSample PAIR socket send/receive
DealerRouterRecvSample DEALER/ROUTER send/receive
RequestReplyAsyncSample Async request/reply
PubSubRecvSample PUB/SUB publish/subscribe
StreamRecvSample STREAM raw TCP
StreamPacketCallbackSample STREAM PACKET pull (legacy class name)
MonitorRecvSample Monitor event receive

SPOT/Actor examples are covered by the framework samples, not the core binding — see the Spot/Actor links under See Also below.

Building and running the samples:

cd bindings/java
./gradlew :samples:build
./gradlew :samples:run -PmainClass=systems.zlink.samples.PairRecvSample

Kotlin

Kotlin uses the Java binding (systems.zlink.*) as-is, with no separate native binding. The installation, core types, ownership, errors, and mapping table above all apply identically — only the idiom differs for Kotlin.

  • Dependency: systems.zlink:zlink (same as above). Use Kotlin plugin 2.1.0 or later.
  • Ownership: since it's AutoCloseable, clean up with use { } instead of try/finally.
  • Send completion: in a coroutine, await the CompletionStage returned by Java's asynchronous submit() as submit().await(). Do not call the blocking submit_sync() terminal inside a coroutine.
Zlink.createContext().use { ctx ->
    ctx.createPairSocket().use { socket ->
        socket.bind("tcp://127.0.0.1:5555")
        // ...
    }
}
  • Pull delivery: timer, monitor, STREAM packet, send completion, and request reply paths are consumed through their receive or awaitable terminals; Kotlin does not add callback-only terminals.
  • Samples: bindings/kotlin/samples/ (.kt) has the same canonical set as the Java samples. Build/run through the Java gradle :kotlin-samples subproject.
cd bindings/java
./gradlew :kotlin-samples:runPairRecvSample --no-daemon

The core guide's language tabs have a dedicated Kotlin column, so you can see messaging/service usage directly in Kotlin code.


See Also

Socket patterns - Socket pattern overview - PAIR - PUB/SUB - DEALER - ROUTER - STREAM - Proxy

Services - Framework service overview - Spot - Actor

Operations - Socket options - TLS security - Monitoring - Thread safety - Message API - Routing ID