Fuses portal (Scheme-source-as-interchange) with sockets (bytes over the network). Wire protocol: one S-expression per connection. Same server + client .lsp runs byte-identically in Python, C, and asm. New primitives in all three impls: - read-from-string — parse one sexp from a string Asm gets two more: - symbol->string — standard R7RS, was missing - eval — evaluate a Scheme value in the global env (Python + C had it as a special form; asm exposes it as a builtin) examples/rpc-server.lsp (port 9080): - Whitelisted dispatch: ping / add / mul / fib / echo - Never calls eval on client input; safe by construction - Uses heap-snapshot/restore for O(1) memory on asm - ~90 lines, portable examples/rpc-client.lsp: - Sends one request, reads one response, displays both - 45 lines, portable examples/repl-server.lsp (port 9081): - DANGER: full remote eval. Any Scheme form accepted and evaluated in the server's global env. Persistent across connections. - Deliberately does NOT use heap-snapshot — remote (define x ...) lives in the global env above any snapshot point; rewinding would invalidate the new binding. The ulimit -v 512 MB safety net (documented in CLAUDE.md) ensures an escaped process can't crash the machine. - ~70 lines, portable. Demonstrates what "the language IS the interchange format" gets you at the limit: a single socket and a single primitive (eval) carry a full-powered REPL. Verified 3×3 server×client matrix: all 9 combinations green. All 132 asm + 571 py + 189 shared + 83 c tests still pass. One quirk discovered and worked around: in asm, a closure captures its env chain by pointer at define time. Forward-referenced names in mutually-recursive toplevel defines can fail under specific heap-restore patterns — see the leaf-first ordering note in rpc-server.lsp.
53 lines
1.7 KiB
Text
53 lines
1.7 KiB
Text
;;; rpc-client.lsp — send one S-expression, read one back.
|
|
;;;
|
|
;;; Demonstrates the three-way symmetry: the CLIENT is also portable
|
|
;;; across Python, C, and asm. Any client impl talks to any server impl
|
|
;;; because they agree on S-expression framing.
|
|
;;;
|
|
;;; Usage:
|
|
;;; python3 uncommonlisp.py --fast examples/rpc-client.lsp
|
|
;;; ./c/uncommonlisp examples/rpc-client.lsp
|
|
;;; ./asm/uncommonlisp < examples/rpc-client.lsp
|
|
;;;
|
|
;;; Override *target-port* before loading to hit either rpc-server (9080)
|
|
;;; or repl-server (9081).
|
|
|
|
(define *host* "127.0.0.1")
|
|
(define *target-port* 9080)
|
|
|
|
;; Serialize a form to a string — same pattern as rpc-server's
|
|
;; response->string. Avoids open-output-string so this runs in asm.
|
|
(define (form->string v)
|
|
(cond
|
|
((number? v) (number->string v))
|
|
((symbol? v) (symbol->string v))
|
|
((null? v) "()")
|
|
((pair? v) (string-append "(" (list->string v) ")"))
|
|
((string? v) (string-append "\"" v "\""))
|
|
(else "#<unknown>")))
|
|
(define (list->string lst)
|
|
(cond
|
|
((null? lst) "")
|
|
((null? (cdr lst)) (form->string (car lst)))
|
|
(else (string-append (form->string (car lst)) " " (list->string (cdr lst))))))
|
|
|
|
(define (rpc request-form)
|
|
(let ((sock (tcp-connect *host* *target-port*)))
|
|
(tcp-send sock (form->string request-form))
|
|
(let ((resp (tcp-recv sock 8192)))
|
|
(tcp-close sock)
|
|
(if (and resp (> (string-length resp) 0))
|
|
(read-from-string resp)
|
|
'no-response))))
|
|
|
|
(define (demo form)
|
|
(display "→ ") (write form) (newline)
|
|
(display "← ") (write (rpc form)) (newline)
|
|
(newline))
|
|
|
|
(demo '(ping))
|
|
(demo '(add 1 2 3 4 5))
|
|
(demo '(mul 6 7))
|
|
(demo '(fib 30))
|
|
(demo '(echo (hello world)))
|
|
(demo '(nope whatever))
|