C tier: spawn-process-stdio + flush-port for bend cross-tier
Two new primitives in builtins.c, paralleling the Python tier shipped in
the previous commit. gpu-worker.lsp now runs on the C tier byte-identically
to the Python tier.
(spawn-process-stdio path args) → (stdin-port . stdout-port)
fork + pipe + execvp; child's stdin & stdout wired back to parent
as line-buffered FILE* ports. Accepts both strings and symbols in
the args list (matches Python tier's permissive conversion).
(flush-port port)
fflush() on the port's FILE*. No-op when fp is null.
End-to-end on 3090-ai with C-tier lumbda everywhere:
shell A: ./lumbda /tmp/launch-c.lsp
→ gpu-worker: ready cuda-shake-fanout ← ./shake256-fanout
→ gpu-worker listening on port 9091
shell B: ./lumbda smoke-bend.lsp # run 3×
=== smoke-bend ===
1. cost estimator picks local for 3 inputs: OK
2. worker available? #t
3. bend! (cuda-shake-fanout '("00" "01" "deadbeef") 32):
(b8d01df855… 94da6280b2… fa094fa86e…)
Three runs identical bytes. Same hashes as Python tier. Same hashes as
hashlib.shake_256 host reference.
Cross-tier matrix (proves wire protocol is tier-agnostic):
client tier worker tier status
─────────────────────────────────────
C tier C tier PASS — 2 sequential runs, byte-identical
Python tier C tier PASS — same hashes
C tier Python tier implicit by symmetry (same wire bytes
both directions; Python-server tested
against Python-client in prior commit)
Per-tier status after this commit:
Python tier ✓ end-to-end
C tier ✓ end-to-end + cross-tier byte-identical to Python tier
asm tier → still needs spawn-process-stdio via raw fork+pipe+
execve syscalls. Scheme files unchanged.
This commit is contained in:
parent
494ae3193c
commit
b890e3641f
1 changed files with 82 additions and 0 deletions
82
c/builtins.c
82
c/builtins.c
|
|
@ -1223,6 +1223,86 @@ static Value bi_tcp_close(Value *a, int n, Env *e) {
|
|||
return VAL_VOID;
|
||||
}
|
||||
|
||||
#include <sys/wait.h>
|
||||
/* spawn-process-stdio: fork+exec a child with its stdin & stdout piped
|
||||
* back to us; return (stdin-port . stdout-port). Lets gpu-worker.lsp
|
||||
* (and any other lumbda code) hold a long-lived subprocess across many
|
||||
* request cycles without spawning per request.
|
||||
*
|
||||
* (spawn-process-stdio "path/to/binary" '("arg1" "arg2"))
|
||||
* ⇒ (#<port> . #<port>)
|
||||
*/
|
||||
static Value bi_spawn_process_stdio(Value *a, int n, Env *e) {
|
||||
(void)e; CHECK_ARITY("spawn-process-stdio", 2); check_string(a[0]);
|
||||
const char *path = AS_STRING(a[0])->data;
|
||||
|
||||
/* count args */
|
||||
int argc = 1;
|
||||
Value lst = a[1];
|
||||
while (IS_PAIR(lst)) { argc++; lst = CDR(lst); }
|
||||
char **argv = (char**)ul_malloc(sizeof(char*) * (argc + 1));
|
||||
argv[0] = (char*)path;
|
||||
int i = 1; lst = a[1];
|
||||
while (IS_PAIR(lst)) {
|
||||
Value v = CAR(lst);
|
||||
if (IS_STRING(v)) {
|
||||
argv[i++] = AS_STRING(v)->data;
|
||||
} else if (IS_SYM(v)) {
|
||||
argv[i++] = (char*)sym_name(v);
|
||||
} else {
|
||||
ul_free(argv);
|
||||
lisp_error("spawn-process-stdio: arg must be string or symbol");
|
||||
}
|
||||
lst = CDR(lst);
|
||||
}
|
||||
argv[argc] = NULL;
|
||||
|
||||
int in_pipe[2]; /* parent writes → child reads (child's stdin) */
|
||||
int out_pipe[2]; /* child writes → parent reads (child's stdout)*/
|
||||
if (pipe(in_pipe) < 0 || pipe(out_pipe) < 0) {
|
||||
ul_free(argv); return VAL_FALSE;
|
||||
}
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
close(in_pipe[0]); close(in_pipe[1]);
|
||||
close(out_pipe[0]); close(out_pipe[1]);
|
||||
ul_free(argv); return VAL_FALSE;
|
||||
}
|
||||
if (pid == 0) {
|
||||
/* child */
|
||||
dup2(in_pipe[0], STDIN_FILENO);
|
||||
dup2(out_pipe[1], STDOUT_FILENO);
|
||||
close(in_pipe[0]); close(in_pipe[1]);
|
||||
close(out_pipe[0]); close(out_pipe[1]);
|
||||
execvp(path, argv);
|
||||
_exit(127);
|
||||
}
|
||||
/* parent */
|
||||
close(in_pipe[0]);
|
||||
close(out_pipe[1]);
|
||||
ul_free(argv);
|
||||
|
||||
/* line-buffer the parent's write end so daemon sees newlines promptly */
|
||||
FILE *win = fdopen(in_pipe[1], "w");
|
||||
FILE *rout = fdopen(out_pipe[0], "r");
|
||||
if (!win || !rout) {
|
||||
if (win) fclose(win); else close(in_pipe[1]);
|
||||
if (rout) fclose(rout); else close(out_pipe[0]);
|
||||
return VAL_FALSE;
|
||||
}
|
||||
setvbuf(win, NULL, _IOLBF, 0);
|
||||
Value pin = make_file_port(win, PORT_OUTPUT);
|
||||
Value pout = make_file_port(rout, PORT_INPUT);
|
||||
return cons(pin, pout);
|
||||
}
|
||||
|
||||
static Value bi_flush_port(Value *a, int n, Env *e) {
|
||||
(void)e; CHECK_ARITY("flush-port", 1);
|
||||
ULPort *p = AS_PORT(a[0]);
|
||||
if (p->fp) fflush(p->fp);
|
||||
return VAL_VOID;
|
||||
}
|
||||
|
||||
/* heap-snapshot / heap-restore: asm-only arena primitives. The asm impl
|
||||
* has no GC; these let a server rewind its bump allocator between
|
||||
* requests. Python + C have real GCs — no-ops here so portable .lsp
|
||||
|
|
@ -1906,6 +1986,8 @@ Env *make_global_env(void) {
|
|||
DEF("tcp-recv", bi_tcp_recv);
|
||||
DEF("tcp-send", bi_tcp_send);
|
||||
DEF("tcp-close", bi_tcp_close);
|
||||
DEF("spawn-process-stdio", bi_spawn_process_stdio);
|
||||
DEF("flush-port", bi_flush_port);
|
||||
DEF("heap-snapshot", bi_heap_snapshot);
|
||||
DEF("heap-restore", bi_heap_restore);
|
||||
DEF("current-time-ms", bi_current_time_ms);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue