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:

Per-script threads on Python side:


Wire Protocol

All messages use length-prefixed frames over stdin/stdout:

┌─────────────┬─────────────────────────────┐
│  4 bytes    │  N bytes                    │
│  uint32 BE  │  payload (msgpack or JSON)  │
│  (length N) │                             │
└─────────────┴─────────────────────────────┘

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)           # msgpack, orjson, or json
    header = struct.pack("!I", len(data))  # 4-byte big-endian length
    with self._lock:
        self._stdout.write(header + data)
        self._stdout.flush()

The lock ensures header and payload are written atomically — without it, concurrent sends from different tasks could interleave.

Format negotiation

On startup, Python sends a handshake (always as JSON, since Java starts in JSON mode):

{{"type": "handshake", "format": "msgpack"}}

Java processes this and switches its useMsgpack flag. All subsequent messages in both directions use the negotiated format. If Python doesn't have msgpack installed, the handshake says "format": "json" and nothing changes.


Message Types

Python → Java (P2J)

TypePurposeKey Fields
handshakeNegotiate wire formatformat ("msgpack" or "json")
callInvoke a methodid, method, handle or target, args_list, no_response?
call_batchBatch multiple callsatomic, messages[]
subscribeRegister event listenerevent, priority, once_per_tick, throttle_ms
register_commandRegister a /commandname, description, usage, permission
waitWait N server ticksid, ticks
releaseFree object handleshandles[]
readyScript finished loading
shutdown_ackConfirm shutdown received
event_doneEvent handler finishedid
event_cancelCancel the eventid
event_resultReturn a value from eventid, result, result_type

Java → Python (J2P)

TypePurposeKey Fields
returnSuccessful call resultid, result
errorCall failedid, message, code
eventBukkit event firedevent, payload, id
event_batchMultiple events at onceevent, payloads[]

Reader Thread

The Python-side reader thread is a daemon thread that blocks on stdin in a loop:

  1. Read 4-byte header → decode payload length
  2. Read N bytes of payload → deserialize (msgpack or JSON, matching negotiated format)
  3. Sync responses (return/error matching a _pending_sync ID) → set threading.Event directly on the reader thread (fast path, no event loop involvement)
  4. Async responses → dispatch to event loop via call_soon_threadsafe()
  5. 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