JNEXT
Skip to content

3.10.5 The ZRCP server (ZEsarUX's protocol)

--zrcp-port N makes JNEXT serve ZRCP, the line-based text protocol ZEsarUX offers on its --remoteprotocol-port, speaking as ZEsarUX 12.0 does. Its main client is DeZog's zrcp remote (the one DeZog uses for ZEsarUX); because every command is a line of text, telnet is a client too. The server is src/remote/zrcp/, one adapter over the shared socket transport (3.10.1), reaching the machine only through jnext::dbg::Debugger. The design, with the ZEsarUX transcripts and DeZog source lines every rule below comes from, is doc/design/debug-subsystem/zrcp-frontend.md (§11 records what was built and every deviation; §10 lists the deliberate divergences from ZEsarUX).

The pieces

File What it holds
src/remote/zrcp/zrcp_server.{h,cpp} ZrcpServer, a remote::Protocol and a dbg::Listener: the line reader, the command table — every one of the 125 names ZEsarUX 12.0's own ls prints: 67 served, 1 declined (exit-emulator), 57 answered Error. Unsupported command in jnext: — the session state, the run state machine, the breakpoint slots and the memory-breakpoint map
src/remote/zrcp/zrcp_format.{h,cpp} the reply formatters: the register line at the widths DeZog's decodezesaruxdata.ts reads, MMU=, disassembly at column 7, hexdump, the cpu-history line, quoted-argument splitting
src/remote/zrcp/zrcp_condition.{h,cpp} ZEsarUX's breakpoint-condition dialect: its tokeniser and its operator grouping, translated into a fully bracketed DSL expression that script::compile_expr (3.10.6) compiles; SEGn / ROM / RAM are evaluated here, since the DSL cannot read whether a slot is ROM
src/platform/debug_servers.{h,cpp} DebugServers opens it beside the DZRP and GDB servers — its own listener, its own backend client — in all three loop owners

The session and run

The client is attached on connect and detached on disconnect or quit, which removes everything it subscribed and releases a pause that is its own, or hands it to another arming client if one is attached (the backend's rule, 3.9.3). cpu-step mode, the prompt, set-cr, the debug-settings byte and the partial T-state base are adapter state; the backend learns none of them.

run, run n and cpu-step-over do not answer at once: the Running until … line goes out first, and the stop reply is written the first time the adapter is asked after the machine stops — at the top of a pump or after its drain — so it is level-triggered on state().paused, not on the Paused edge. While one is in flight no line is executed: any byte received stops the machine and the line it belongs to is discarded, which is how ZEsarUX's clients interrupt a run. run n is a loop of step_into in time slices, so a large n never holds one pump. hard-reset-cpu is a cold boot completed inside the reply, through the loop owner's LoopDriver (the reconstruct contract of 3.9.3): the session and its subscriptions survive it, and a stopped machine stays stopped.

get-memory-pages

On the Next memory_pages() names each slot from SlotInfo::now, with ZEsarUX's spellings: O<2*image + half> for ROM, A<page>, SR<NR 0x04> (the full value) in config mode, RO for the boot ROM, DIV for either DivMMC memory (also over a RAM-mapped slot: the VHDL overlays it too). The Multiface, Layer 2, alt ROM and a floating slot have no ZEsarUX spelling and print the slot's own mapping (O<n> / A<nr_page>). On the legacy machines segment 0 is DIV while slot 0 reads DivMMC. MMU= stays the DeZog projection (ROM half >= 0x8000) and does not use now. cpu-step-over reads the opcode at the PC through memory_reader(), the instruction the CPU will fetch.

CVAR(path): jnext's extension of the dialect

evaluate and a breakpoint condition both go through translate_condition(), ZEsarUX's tokeniser and grouping, and only the translation reaches the DSL: cvar(...) typed into evaluate was Error parsing. The tokeniser therefore takes one new operand, CVAR( and the raw path up to the first ) (Tok::CVar, Token::raw), before its function branch; a (, a ", a missing ) and an empty path make tokenise() fail, which is ZEsarUX's Error parsing. The path is one token, so first_top() never splits inside it (CVAR(p->x)+1 splits at the + only) and its case is kept (canonical_of() prints CVAR(path), which get-breakpoints lists and Breakpoint fired: echoes). Translator::operand() turns it into the DSL text cvar(path), so ZRCP reaches c_int32_scalar() only through the evaluator: one rule for scripts and ZRCP (3.10.6). No ZEsarUX exchange changes meaning, because ZEsarUX answers this text Error parsing, and the recorded fixture is untouched. A read that fails answers Error evaluating parsed string: CVAR(path), with no reason (the reply is ZEsarUX's; scripts and GDB's monitor print show the reason). A condition with no PC= first term is checked at every instruction, so one with a CVAR builds the C frames each time: help set-breakpoint says to put a PC= term first.

The write is the same extension on the one assignment verb: set-register CVAR(path)=value (cmd_set_register(), zrcp_server.cpp:1190) writes a C variable of frame 0 through write_c_value() (7.10). The table's census (ZRCP-TAB-01) pins the command names to ZEsarUX's ls, so no set-cvar could be added. Success answers the register line, and every failure (no ) or =, an empty path or value, a refusal of the facade) is ZEsarUX's Error changing register, with no reason, as a failed CVAR() read has none.

Breakpoints, history, loading

The 100 condition slots of set-breakpoint are one backend subscription each, owned by the client. A PC= first term is the fast path (an Execute subscription at that address); any other condition fires on its false-to-true edge, as in ZEsarUX, keyed to the master cycle. Memory breakpoints are kept as ZEsarUX's per-address type map and turned into one Mem range subscription per run of equal type. The history (cpu-history) is a view over the machine's trace log: clear and set-max-size move the session's view, never the trace jnext's own Step Back reads. Turning the trace, coverage or call tracking on is owned: a client turns off only what it turned on. smartload goes through the loop owner's LoopDriver::load, the same path as --load; snapshot-save / snapshot-load are the backend's bookmarks, kept for the session.

Tests

  • zrcp_adapter_test (Qt-free, both configurations) runs the production Server over the transport's in-memory fake, on a real machine through pump(): the framing, the command census, every formatter against ZEsarUX's bytes, the run state machine (a stop is proved by the machine being stopped where it should be), the condition translator on both sides of every grouping rule, the slots, the memory map, history, coverage and loading.
  • test/fixtures/zrcp/zesarux-12.0-exchanges.txt holds scenes recorded from the real ZEsarUX 12.0 server, one fresh server per scene: the exchanges whose replies depend on nothing but the session. zrcp_adapter_test (rows ZRCP-FIX-*) and the zrcp-func regression row both replay every scene and compare the bytes. A new deliberate divergence from ZEsarUX therefore shows up as a fixture failure, and belongs in the design's §10, not in the fixture.
  • zrcp-func, zrcp-bp-func, zrcp-hist-func, zrcp-sdl-func and zrcp-qt-func drive the real binary over a socket with an independent client (test/00regression/zrcp-peer.py), which knows ZRCP only from the fixture, the transcripts and DeZog's parser.