Skip to content

Commit da39508

Browse files
authored
Merge pull request GaijinEntertainment#2674 from GaijinEntertainment/bbatkin/live-api-stdio
live_api_stdio: JSON-RPC 2.0 transport for live commands (no dasHV)
2 parents feb65a9 + d7ba730 commit da39508

9 files changed

Lines changed: 864 additions & 17 deletions

File tree

doc/source/reference/utils/daslang_live.rst

Lines changed: 162 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
Edit a ``.das`` file, save it, and the running application picks up
1414
the changes instantly --- preserving windows, GPU state, game entities,
1515
and everything stored in the persistent byte store. No restart, no
16-
lost state. Applications range from games to REST APIs to MCP plugins.
16+
lost state. Applications range from games to REST APIs to stdio
17+
JSON-RPC integrations and MCP plugins.
1718

1819
.. contents::
1920
:local:
@@ -164,6 +165,15 @@ and ``daslang.exe`` (standalone). Under ``daslang.exe`` the ``main()``
164165
function drives the loop; under ``daslang-live.exe`` the host calls
165166
``init()``, ``update()``, and ``shutdown()`` directly.
166167

168+
For a stdin/stdout JSON-RPC transport instead of HTTP, swap one require
169+
line and use ``examples/daslive/hello_stdio/``::
170+
171+
require live/live_api_stdio // instead of live/live_api
172+
173+
User commands (any ``[live_command]``) work identically over both
174+
transports. The two transports differ in wire protocol and in how
175+
built-in lifecycle commands are surfaced; see :ref:`stdio_api` below.
176+
167177

168178
Mode detection
169179
==============
@@ -195,9 +205,9 @@ Lifecycle
195205

196206
**Failed reload:**
197207
The host reverts to the old context, pauses execution, and stores the
198-
compilation error. Retrieve it via ``GET /error`` or
199-
``get_last_error()``. The next successful reload unpauses
200-
automatically.
208+
compilation error. Retrieve it via ``GET /error``, the
209+
``last_error`` stdio command, or ``get_last_error()``. The next
210+
successful reload unpauses automatically.
201211

202212
**Runtime exception:**
203213
The host pauses, clears the persistent store (potentially corrupted),
@@ -271,7 +281,7 @@ Reload annotations
271281
- Called after recompile, before ``init()``. Restore state here.
272282
* - ``[before_update]``
273283
- Called every frame before ``update()``. Used internally by
274-
``live_api``.
284+
``live_api``, ``live_api_stdio``, and other transport agents.
275285

276286
The host discovers annotated functions by name prefix
277287
(``__before_reload_*``, ``__after_reload_*``, ``__before_update_*``).
@@ -351,16 +361,25 @@ Helper modules
351361
* - ``live/decs_live``
352362
- Auto-serialization of DECS entities across reloads.
353363
* - ``live/live_commands``
354-
- ``[live_command]`` annotation for REST-callable functions.
364+
- ``[live_command]`` annotation for transport-callable functions.
355365
* - ``live/live_vars``
356366
- ``@live`` variable macro (auto-persistence).
357367
* - ``live/live_watch``
358368
- File watcher (auto-reload on save).
359369
* - ``live/live_watch_boost``
360370
- File watcher with diagnostic commands (recommended over
361371
``live_watch``).
372+
* - ``live/live_api_builtins``
373+
- Built-in commands (``status``, ``last_error``, ``reload``,
374+
``reload_full``, ``pause``, ``unpause``, ``shutdown``). Required
375+
transitively by ``live_api_stdio`` so stdio clients can drive
376+
lifecycle by name. The HTTP transport exposes the same
377+
operations as REST endpoints (``GET /status``, ``POST /reload``,
378+
…) rather than pulling these built-ins in.
362379
* - ``live/live_api``
363-
- REST API server on port 9090.
380+
- REST API server on port 9090 (requires ``dasHV``).
381+
* - ``live/live_api_stdio``
382+
- JSON-RPC 2.0 over stdin/stdout. No ``dasHV`` dependency.
364383
* - ``live/audio_live``
365384
- Audio state persistence across reloads.
366385

@@ -446,21 +465,43 @@ restored entities:
446465
``live/live_commands``
447466
----------------------
448467

449-
Functions annotated ``[live_command]`` are callable via
450-
``POST /command``. Signature:
451-
``def cmd_name(input : JsonValue?) : JsonValue?``.
452-
Convention: prefix with ``cmd_``. The ``set_color`` command in the
453-
hello example above demonstrates the pattern.
468+
Functions annotated ``[live_command]`` are callable via any installed
469+
transport — HTTP ``POST /command`` or stdio
470+
``{"method":"name", ...}``. Signature:
471+
``def cmd_name(input : JsonValue?) : JsonValue?``. Convention: prefix
472+
with ``cmd_``. The ``set_color`` command in the hello example above
473+
demonstrates the pattern.
454474

475+
The built-in lifecycle commands (``status``, ``last_error``,
476+
``reload``, ``reload_full``, ``pause``, ``unpause``, ``shutdown``)
477+
live in ``live/live_api_builtins``. They are pulled in by
478+
``live/live_api_stdio`` so stdio clients can invoke lifecycle
479+
operations by name. The HTTP transport exposes the same operations
480+
as REST endpoints (``GET /status``, ``POST /reload``, …) — see below.
455481

456-
REST API
457-
========
482+
483+
Transports
484+
==========
485+
486+
Two transport modules ship in-tree. Pick one — they coexist but the
487+
typical script requires only one:
488+
489+
* ``live/live_api`` — HTTP REST API on a TCP port. Requires
490+
``dasHV``. Lifecycle operations are surfaced as REST endpoints;
491+
user ``[live_command]`` functions are reached via ``POST /command``.
492+
* ``live/live_api_stdio`` — JSON-RPC 2.0 over stdin/stdout. No
493+
``dasHV`` dependency; suitable for embedding in host processes that
494+
drive the script via pipes. Lifecycle and user commands are both
495+
invoked by name in the ``method`` field.
496+
497+
REST API (``live/live_api``)
498+
----------------------------
458499

459500
``require live/live_api`` starts an HTTP server on port 9090.
460501
Configure the port with ``live_api_set_port()`` before ``init()``.
461502

462503
Endpoints
463-
---------
504+
^^^^^^^^^
464505

465506
.. list-table::
466507
:header-rows: 1
@@ -499,7 +540,7 @@ Endpoints
499540
- JSON help with all endpoints and curl examples.
500541

501542
curl examples
502-
-------------
543+
^^^^^^^^^^^^^
503544

504545
Check status::
505546

@@ -518,6 +559,108 @@ When using the daslang MCP server, prefer ``live_*`` MCP tools over
518559
curl (see :ref:`utils_mcp`).
519560

520561

562+
.. _stdio_api:
563+
564+
Stdio API (``live/live_api_stdio``)
565+
-----------------------------------
566+
567+
``require live/live_api_stdio`` installs a debug agent that reads
568+
newline-delimited JSON-RPC 2.0 messages from ``stdin`` and writes
569+
responses to ``stdout``. No HTTP server is started; no port is opened;
570+
no ``dasHV`` dependency.
571+
572+
The ``method`` field is the live command name — any
573+
``[live_command]`` function plus the built-ins listed below.
574+
``params`` is passed verbatim to the command as its input
575+
``JsonValue?``.
576+
577+
Request / response shape::
578+
579+
→ {"jsonrpc":"2.0","id":1,"method":"status"}
580+
← {"jsonrpc":"2.0","id":1,"result":{"fps":60.0,"uptime":3.2,"paused":false,"dt":0.016,"has_error":false}}
581+
582+
→ {"jsonrpc":"2.0","id":2,"method":"set_color","params":{"r":1.0,"g":0.0,"b":0.0}}
583+
← {"jsonrpc":"2.0","id":2,"result":"{\"r\": 1.0, \"g\": 0.0, \"b\": 0.0}"}
584+
585+
→ {"method":"shutdown"} ← (no response: notification)
586+
587+
The ``result`` field embeds whatever JSON value the command returned —
588+
an object when the command returned an object (``status``), a string
589+
when the command returned a string (``set_color`` in the demo returns a
590+
``JV(string)`` so the result is a quoted JSON string), and so on.
591+
592+
**Framing guarantee.** The transport always writes exactly one
593+
response per line on stdout, with no embedded newlines in the envelope.
594+
``write_json`` pretty-prints by default, so the dispatch result is
595+
post-processed by ``compact_json_whitespace`` before being embedded in
596+
the envelope — whitespace outside string literals is stripped;
597+
whitespace inside ``"..."`` is preserved verbatim.
598+
599+
**Notifications.** Per JSON-RPC 2.0 §4.1, a request that omits the
600+
``id`` field is a *notification*: the server MUST NOT respond.
601+
``live_api_stdio`` honors this — fire-and-forget commands like
602+
``{"method":"shutdown"}`` produce no output, useful for clients that
603+
don't want to bookkeep a response. An explicit ``"id":null`` is *not*
604+
a notification; the server responds with ``"id":null``. Parse errors
605+
and invalid requests are *not* treated as notifications either — they
606+
emit ``-32700`` / ``-32600`` responses with ``"id":null`` so a buggy
607+
client doesn't hang waiting for a reply.
608+
609+
**Permissive JSON-RPC subset.** Two deviations from strict JSON-RPC 2.0
610+
to ease integration with real-world clients:
611+
612+
* The ``jsonrpc`` member is **optional**. Strict compliance would
613+
reject ``{"id":1,"method":"status"}`` because the ``"jsonrpc":"2.0"``
614+
field is missing — this transport accepts it. Requests with a
615+
different version (``"jsonrpc":"1.0"``) are also accepted.
616+
* All other JSON-RPC 2.0 rules are enforced: ``method`` is required and
617+
must be a string, ``id`` must be string / number / null when present
618+
(objects / arrays / booleans are rejected with ``-32600``), and
619+
notifications produce no response.
620+
621+
Error envelopes follow JSON-RPC 2.0 codes: ``-32700`` parse error,
622+
``-32600`` invalid request (missing/non-string ``method``).
623+
624+
Available methods (built-ins from ``live/live_api_builtins``):
625+
626+
.. list-table::
627+
:header-rows: 1
628+
:widths: 25 75
629+
630+
* - method
631+
- Description
632+
* - ``status``
633+
- JSON: ``fps``, ``uptime``, ``paused``, ``dt``, ``has_error``.
634+
* - ``last_error``
635+
- Last compilation error string (or JSON ``null`` if none).
636+
* - ``reload``
637+
- Incremental reload.
638+
* - ``reload_full``
639+
- Full recompile (clears ``@live`` vars).
640+
* - ``pause``
641+
- Pause execution.
642+
* - ``unpause``
643+
- Resume execution.
644+
* - ``shutdown``
645+
- Graceful shutdown.
646+
647+
Any user-defined ``[live_command]`` is also callable by name.
648+
649+
.. warning::
650+
651+
``stdout`` is the response channel. Scripts that use this transport
652+
must redirect ``print()`` to ``stderr`` or a log file — calling
653+
``print()`` from the script's main loop will interleave application
654+
output with JSON-RPC responses and break clients that parse stdout
655+
line-by-line. ``daslang-live`` itself logs lifecycle messages to
656+
``stdout``; either silence them or have the client tolerate
657+
non-JSON lines.
658+
659+
The example at ``examples/daslive/hello_stdio/`` is the HTTP hello
660+
example with the single ``require`` line swapped — see the same
661+
``set_color`` command driven over stdio instead of ``POST /command``.
662+
663+
521664
CLI reference
522665
=============
523666

@@ -552,6 +695,9 @@ Examples
552695
- Description
553696
* - ``examples/daslive/hello/``
554697
- Minimal GLFW window with background color tuning.
698+
* - ``examples/daslive/hello_stdio/``
699+
- Minimal stdio (JSON-RPC) variant of the hello example.
700+
Same ``set_color`` command, no ``dasHV`` dependency.
555701
* - ``examples/daslive/triangle/``
556702
- DECS + OpenGL shaders, rotating triangle.
557703
* - ``examples/games/arcanoid/``
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
options gen2
2+
3+
require live/glfw_live
4+
require opengl/opengl_boost
5+
require live/live_commands
6+
require live/live_api_stdio
7+
require daslib/json
8+
require daslib/json_boost
9+
require live_host
10+
11+
// --- State ---
12+
13+
var bg_r = 0.2f
14+
var bg_g = 0.3f
15+
var bg_b = 0.5f
16+
var frame_count : int = 0
17+
18+
// --- Live commands ---
19+
20+
[live_command]
21+
def set_color(input : JsonValue?) : JsonValue? {
22+
if (input != null && input.value is _object) {
23+
let tab & = unsafe(input.value as _object)
24+
var rv = tab?["r"] ?? null
25+
if (rv != null && rv.value is _number) {
26+
bg_r = float(rv.value as _number)
27+
}
28+
var gv = tab?["g"] ?? null
29+
if (gv != null && gv.value is _number) {
30+
bg_g = float(gv.value as _number)
31+
}
32+
var bv = tab?["b"] ?? null
33+
if (bv != null && bv.value is _number) {
34+
bg_b = float(bv.value as _number)
35+
}
36+
}
37+
return JV("\{\"r\": {bg_r}, \"g\": {bg_g}, \"b\": {bg_b}}")
38+
}
39+
40+
// --- Lifecycle ---
41+
42+
[export]
43+
def init() {
44+
live_create_window("Hello daslive (stdio)", 640, 480)
45+
if (!is_reload()) {
46+
frame_count = 0
47+
}
48+
}
49+
50+
[export]
51+
def update() {
52+
if (!live_begin_frame()) return
53+
frame_count++
54+
var w, h : int
55+
live_get_framebuffer_size(w, h)
56+
glViewport(0, 0, w, h)
57+
glClearColor(bg_r, bg_g, bg_b, 1.0)
58+
glClear(GL_COLOR_BUFFER_BIT)
59+
live_end_frame()
60+
}
61+
62+
[export]
63+
def shutdown() {
64+
live_destroy_window()
65+
}
66+
67+
// Dual-mode: also works with regular daslang.exe (stdio agent is inactive
68+
// when is_live_mode() is false, so this just runs the GL loop).
69+
[export]
70+
def main() {
71+
init()
72+
while (!exit_requested()) {
73+
update()
74+
}
75+
shutdown()
76+
}

modules/dasLiveHost/.das_module

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ def initialize(project_path : string) {
66
if (das_is_dll_build()) {
77
register_dynamic_module("{project_path}/dasModuleLiveHost.shared_module", "Module_LiveHost")
88
}
9-
let live_paths = ["live_commands", "live_api", "live_watch", "live_watch_boost", "decs_live", "live_vars"]
9+
let live_paths = ["live_commands", "live_api", "live_api_builtins", "live_api_stdio", "live_watch", "live_watch_boost", "decs_live", "live_vars"]
1010
for (path in live_paths) {
1111
register_native_path("live", "{path}", "{project_path}/live/{path}.das")
1212
}

modules/dasLiveHost/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ IF ((NOT DAS_LIVE_HOST_INCLUDED) AND ((NOT ${DAS_LIVE_HOST_DISABLED}) OR (NOT DE
1313

1414
ADD_MODULE_CPP(LiveHost)
1515
ADD_MODULE_DAS(live live live_commands)
16+
ADD_MODULE_DAS(live live live_api_builtins)
1617
ADD_MODULE_DAS(live live live_api)
18+
ADD_MODULE_DAS(live live live_api_stdio)
1719
ADD_MODULE_DAS(live live live_watch)
1820
ADD_MODULE_DAS(live live decs_live)
1921
ADD_MODULE_LIB(libDasModuleLiveHost dasModuleLiveHost ${DAS_LIVE_HOST_MODULE_SRC})

0 commit comments

Comments
 (0)