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):
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:
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 — useclone()for that. (sharedCopyOf/moveInto/moveTowere internal, not public API, so the only public-surface change is addingcopy/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:
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 withuse { }instead oftry/finally. - Send completion: in a coroutine, await the
CompletionStagereturned by Java's asynchronoussubmit()assubmit().await(). Do not call the blockingsubmit_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-samplessubproject.
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