Overview of the bridge architecture and wire protocol
Architecture
This section describes how PyJavaBridge works under the hood. Start here for the big picture, then dive into specific topics.
Overview
Each Python script runs as a separate OS process spawned by the Java plugin. Communication happens over stdin/stdout using a binary protocol. The Python process's stderr is inherited by the Java process, so print() output appears in the server console.
┌──────────────────────┐ stdin (Java→Python) ┌─────────────────────┐
│ │ ◄──────────────────────── │ │
│ Python Process │ │ Java Plugin │
│ │ ────────────────────────► │ │
│ - asyncio event │ stdout (Python→Java) │ - Bridge thread │
│ loop (main) │ │ (per script) │
│ - reader thread │ stderr (Python→Java) │ - Main thread │
│ (daemon) │ ────────────────────────► │ (server tick) │
│ │ (console output) │ │
└──────────────────────┘ └─────────────────────┘
Per-script threads on Java side:
- Bridge thread — reads from Python's stdout, writes to Python's stdin. Handles thread-safe calls inline.
- Main thread — Bukkit's server thread. Non-thread-safe calls are queued here.
Per-script threads on Python side:
- Event loop (main thread) — handles async operations, event dispatch, user code.
- Reader thread (daemon) — blocks on stdin, dispatches messages to the event loop or sync waiters.
Wire Protocol
All messages use length-prefixed JSON over stdin/stdout:
┌─────────────┬─────────────────────────────┐
│ 4 bytes │ N bytes │
│ uint32 BE │ UTF-8 JSON payload │
│ (length N) │ │
└─────────────┴─────────────────────────────┘
- Header: 4-byte big-endian unsigned integer (payload length)
- Payload: UTF-8 encoded JSON object
- Serialization: Uses
orjsonif available (faster), falls back tojson - Thread safety: Python writes are protected by a
threading.Lockto keep header+payload atomic
Example wire message
Calling player.getName() where the player object has handle 42:
{"type": "call", "id": 1, "method": "getName", "args_list": [], "handle": 42}
Response:
{"type": "return", "id": 1, "result": "Steve"}
Send implementation (Python)
def send(self, message):
data = _json_dumps(message) # orjson or json
header = struct.pack("!I", len(data)) # 4-byte big-endian length
with self._lock:
self._stdout.write(header)
self._stdout.write(data)
self._stdout.flush()
The lock ensures header and payload are written atomically — without it, concurrent sends from different tasks could interleave.
Message Types
Python → Java (P2J)
| Type | Purpose | Key Fields |
|---|---|---|
call |
Invoke a method | id, method, handle or target, args_list |
call_batch |
Batch multiple calls | atomic, messages[] |
subscribe |
Register event listener | event, priority, once_per_tick, throttle_ms |
register_command |
Register a /command |
name, description, usage, permission |
wait |
Wait N server ticks | id, ticks |
release |
Free object handles | handles[] |
ready |
Script finished loading | — |
shutdown_ack |
Confirm shutdown received | — |
event_done |
Event handler finished | id |
event_cancel |
Cancel the event | id |
event_result |
Return a value from event | id, result, result_type |
Java → Python (J2P)
| Type | Purpose | Key Fields |
|---|---|---|
return |
Successful call result | id, result |
error |
Call failed | id, message, code |
event |
Bukkit event fired | event, payload, id |
event_batch |
Multiple events at once | event, payloads[] |
Reader Thread
The Python-side reader thread is a daemon thread that blocks on stdin in a loop:
- Read 4-byte header → decode payload length
- Read N bytes of JSON payload → parse
- Sync responses (
return/errormatching a_pending_syncID) → setthreading.Eventdirectly on the reader thread (fast path, no event loop involvement) - Async responses → dispatch to event loop via
call_soon_threadsafe() - On disconnect → wake all pending waiters with
BridgeError("Connection lost")
This two-tier design means sync property access (player.name) gets resolved on the reader thread without waiting for the event loop, while async calls integrate with asyncio naturally.
Further Reading
- Execution — Call dispatch, threading model, timing, and batching
- Events — Event subscriptions, dispatch flow, cancellation, overrides
- Serialization — Object handles, type serialization, proxy classes
- Lifecycle — Startup, shutdown, hot reload, commands
- Debugging — Debug logging, metrics, error codes, performance tips