From bfd4ec7ec8b0c7382e0b31c953ff81eb2ab480ed Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Thu, 16 Apr 2026 18:58:27 -0400 Subject: [PATCH] =?UTF-8?q?sockets=20+=20portable=20HTTP=20server=20?= =?UTF-8?q?=E2=80=94=206=20primitives,=20same=20server=20runs=20in=20all?= =?UTF-8?q?=203?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added tcp-listen/accept/connect/recv/send/close to Python, C, and asm. One examples/http-server.lsp runs identically in all three impls and serves HTTP/1.0 with routing, content-type, and content-length headers. asm additions: - SYS_SOCKET/BIND/LISTEN/ACCEPT/CONNECT/SETSOCKOPT syscalls - 6 tcp-* builtins using the existing port encoding (SPECIAL ≥ 1000) - bi_tcp_connect: dotted-quad IPv4 parser, no DNS dependency Defects fixed along the way (surfaced by the HTTP server): - string-append: was 2-arg only; now variadic (walks arg list twice) - number->string: was stubbed to VAL_VOID; now correctly writes digits into a heap-allocated string (incl. negative handling) - String-literal reader: \r and \0 escape sequences now handled (was silently dropping backslash, treating them as literal 'r' / '0') - tcp_accept: sockaddr buffer was 8 bytes, now 16 (was corrupting caller's stack when accept wrote full struct sockaddr_in) Pinocchio benchmark (tests/web-benchmark.sh): At concurrency=20, 1000 requests, serving a 1KB body: uncommonlisp Python 373 req/s uncommonlisp C 370 req/s uncommonlisp asm 370 req/s python3 http.server 381 req/s (stdlib reference) busybox httpd 382 req/s (production reference) All five converge within 3% — the client (curl fork/exec) is the bottleneck, not the server. Our single-threaded blocking servers are indistinguishable from battle-tested ones at this load. Binary sizes: uncommonlisp asm 45 KB (HTTP + everything else) busybox httpd 2.1 MB (multi-call binary) python3 8 MB (interpreter) The asm HTTP server is 46× smaller than busybox and 176× smaller than Python, serves from 7 Linux syscalls, and the entire protocol handler is 70 lines of portable Scheme. Test counts: 132 asm (up 1), rest unchanged. All green. --- asm/test.sh | 5 + asm/uncommonlisp | Bin 43840 -> 45536 bytes asm/uncommonlisp.o | Bin 41064 -> 43952 bytes asm/uncommonlisp.s | 515 ++++++++++++++++++++++++++++++++------- c/builtins.c | 100 ++++++++ examples/http-server.lsp | 90 +++++++ tests/web-benchmark.sh | 104 ++++++++ uncommonlisp.py | 37 +++ 8 files changed, 762 insertions(+), 89 deletions(-) create mode 100644 examples/http-server.lsp create mode 100755 tests/web-benchmark.sh diff --git a/asm/test.sh b/asm/test.sh index a3fed76..03cc01f 100644 --- a/asm/test.sh +++ b/asm/test.sh @@ -263,6 +263,11 @@ check "port-display" "(begin (let ((p (open-output-file \"$LOAD_TMP/p.txt\"))) check "port-newline" "(begin (let ((p (open-output-file \"$LOAD_TMP/p.txt\"))) (display \"a\" p) (newline p) (display \"b\" p) (close-port p)) (string-length (file->string \"$LOAD_TMP/p.txt\")))" "3" check "port-write-num" "(begin (let ((p (open-output-file \"$LOAD_TMP/p.txt\"))) (write 42 p) (close-port p)) (file->string \"$LOAD_TMP/p.txt\"))" '"42"' +echo "[unit] tcp sockets (create/close only)" +check "tcp-listen-close" "(let ((s (tcp-listen 0))) (port? s))" "#t" +# Note: tcp-listen 0 binds to ephemeral port. Can't accept (would block). +# Full connection round-trip tests live in the web-benchmark integration script. + echo "[integration] ports — S-expression portal written via ports" PORTAL_TMP="$LOAD_TMP/port-portal.sexp" check "port-portal" "(begin (define p (open-output-file \"$PORTAL_TMP\")) (display \"(define pp-x \" p) (write 42 p) (display \")\" p) (newline p) (display \"(define pp-y \" p) (write 777 p) (display \")\" p) (newline p) (close-port p) (define pp-x 0) (define pp-y 0) (load \"$PORTAL_TMP\") (+ pp-x pp-y))" "819" diff --git a/asm/uncommonlisp b/asm/uncommonlisp index dacd550c48ee8b893f3b6c9f459eefea949eb53f..7ebf6a24ba2f551b84ae2da3cf896f7920a91535 100755 GIT binary patch literal 45536 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 literal 43840 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 literal 41064 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 KVi?B&;r|~woW5)T diff --git a/asm/uncommonlisp.s b/asm/uncommonlisp.s index c4c0a99..312341a 100644 --- a/asm/uncommonlisp.s +++ b/asm/uncommonlisp.s @@ -28,8 +28,19 @@ .equ SYS_LSEEK, 8 .equ SYS_MMAP, 9 .equ SYS_MUNMAP,11 +.equ SYS_SOCKET,41 +.equ SYS_CONNECT,42 +.equ SYS_ACCEPT,43 +.equ SYS_BIND, 49 +.equ SYS_LISTEN,50 +.equ SYS_SETSOCKOPT,54 .equ SYS_EXIT, 60 +.equ AF_INET, 2 +.equ SOCK_STREAM,1 +.equ SOL_SOCKET,1 +.equ SO_REUSEADDR,2 + .equ O_RDONLY, 0 .equ O_WRONLY, 1 .equ O_CREAT, 64 @@ -143,7 +154,13 @@ .equ BI_PORTP, 76 .equ BI_WRITEFILE, 77 .equ BI_FILETOSTR, 78 -.equ BI_COUNT, 79 +.equ BI_TCPLISTEN, 79 +.equ BI_TCPACCEPT, 80 +.equ BI_TCPCONNECT, 81 +.equ BI_TCPRECV, 82 +.equ BI_TCPSEND, 83 +.equ BI_TCPCLOSE, 84 +.equ BI_COUNT, 85 # ============================================================ .data @@ -248,6 +265,12 @@ bn_closeport: .byte 10; .ascii "close-port" bn_portp: .byte 5; .ascii "port?" bn_writefile: .byte 10; .ascii "write-file" bn_filetostr: .byte 12; .ascii "file->string" +bn_tcplisten: .byte 10; .ascii "tcp-listen" +bn_tcpaccept: .byte 10; .ascii "tcp-accept" +bn_tcpconnect: .byte 11; .ascii "tcp-connect" +bn_tcprecv: .byte 8; .ascii "tcp-recv" +bn_tcpsend: .byte 8; .ascii "tcp-send" +bn_tcpclose: .byte 9; .ascii "tcp-close" portal_magic: .ascii "ULPORTAL" .equ PORTAL_MAGIC_LEN, 8 @@ -275,6 +298,8 @@ bi_names: .quad bn_portalsave, bn_portalresume, bn_load .quad bn_openout, bn_closeport, bn_portp .quad bn_writefile, bn_filetostr + .quad bn_tcplisten, bn_tcpaccept, bn_tcpconnect + .quad bn_tcprecv, bn_tcpsend, bn_tcpclose # Error messages err_unbound: .ascii "Error: unbound variable: " @@ -1026,7 +1051,17 @@ scheme_read: movb $9, (%rsp,%rbx) incq %rbx jmp .sr_str_loop -2: movb %al, (%rsp,%rbx) +2: cmpb $'r', %al + jne 3f + movb $13, (%rsp,%rbx) + incq %rbx + jmp .sr_str_loop +3: cmpb $'0', %al + jne 4f + movb $0, (%rsp,%rbx) + incq %rbx + jmp .sr_str_loop +4: movb %al, (%rsp,%rbx) incq %rbx jmp .sr_str_loop .sr_str_end: @@ -2441,6 +2476,18 @@ eval_list: je bi_write_file cmpq $BI_FILETOSTR, %rax je bi_file_to_string + cmpq $BI_TCPLISTEN, %rax + je bi_tcp_listen + cmpq $BI_TCPACCEPT, %rax + je bi_tcp_accept + cmpq $BI_TCPCONNECT, %rax + je bi_tcp_connect + cmpq $BI_TCPRECV, %rax + je bi_tcp_recv + cmpq $BI_TCPSEND, %rax + je bi_tcp_send + cmpq $BI_TCPCLOSE, %rax + je bi_close_port movq $VAL_VOID, %rax popq %r12 @@ -3326,56 +3373,61 @@ bi_strref: RET_VAL bi_strappend: - # (string-append s1 s2 ...) - # Simple 2-arg version - GETARG %rdi # s1 + # (string-append s1 s2 ...) — variadic + # Save arg list head for the second pass + pushq %r12 # original arg list head + + # Pass 1: total length + xorq %rcx, %rcx # accumulator + movq %r12, %rax +.bsa_len_loop: + cmpq $VAL_NIL, %rax + je .bsa_alloc + movq %rax, %rbx + andq $-8, %rbx + movq (%rbx), %rdi # string value andq $-8, %rdi - GETARG %rsi # s2 - andq $-8, %rsi - movq (%rdi), %rcx # len1 - movq (%rsi), %rdx # len2 - movq %r15, %rax # result - leaq (%rcx,%rdx), %r8 - movq %r8, (%r15) # total length - # Copy s1 bytes - leaq 8(%r15), %r9 # dest - leaq 8(%rdi), %r10 # src1 - pushq %rcx - pushq %rdx - pushq %rsi - pushq %rax -.bsa_cp1: + addq (%rdi), %rcx # += length + movq 8(%rbx), %rax # advance + jmp .bsa_len_loop + +.bsa_alloc: + # Allocate string cell: 8 byte length + rcx bytes + movq %rcx, %rbx # save total length + movq %rcx, %rdi + addq $8, %rdi + call heap_alloc + movq %rax, %rbp # string object base + movq %rbx, (%rbp) # store length + leaq 8(%rbp), %r9 # dest cursor + + # Pass 2: copy each arg's bytes + popq %r12 # restore arg list + pushq %rbp # save string obj + movq %r12, %rax +.bsa_copy_loop: + cmpq $VAL_NIL, %rax + je .bsa_done + movq %rax, %rbx + andq $-8, %rbx + movq (%rbx), %rdi # string value + movq 8(%rbx), %rax # save advance + andq $-8, %rdi + movq (%rdi), %rcx # length + leaq 8(%rdi), %r10 # src bytes +.bsa_byte_loop: testq %rcx, %rcx - jz .bsa_cp2_start - movb (%r10), %bl - movb %bl, (%r9) + jz .bsa_copy_loop + movb (%r10), %r8b + movb %r8b, (%r9) incq %r10 incq %r9 decq %rcx - jmp .bsa_cp1 -.bsa_cp2_start: - popq %rax - popq %rsi - popq %rdx - popq %rcx - leaq 8(%rsi), %r10 # src2 -.bsa_cp2: - testq %rdx, %rdx - jz .bsa_done - movb (%r10), %bl - movb %bl, (%r9) - incq %r10 - incq %r9 - decq %rdx - jmp .bsa_cp2 + jmp .bsa_byte_loop + .bsa_done: - # Align r15 - leaq (%rcx,%rdx), %r8 # was already computed but clobbered - movq (%rax), %r8 # reload total len from result - addq $8, %r8 # +header - addq $7, %r8 - andq $-8, %r8 - addq %r8, %r15 + popq %rbp + movq %rbp, %rax orq $TAG_STRING, %rax RET_VAL @@ -3401,57 +3453,60 @@ bi_streqp: jmp .bseq_loop bi_numtostr: + # (number->string n) → string + # Write digits (reverse) into num_buf[end..], then copy into a heap cell. GETARG %rax - sarq $3, %rax - # Convert int to string using itoa - pushq %rax - movq %r15, %rdx # result string obj - addq $8, %r15 # skip length field - movq %r15, %rdi # buffer - popq %rax - # Handle negative + sarq $3, %rax # untag int + leaq num_buf(%rip), %rdi + addq $63, %rdi # write backward from end + movb $0, (%rdi) # sentinel (not used for length) + movq %rdi, %rsi # save end pos + # Detect negative + xorq %r8, %r8 # negative flag testq %rax, %rax - jns .bn2s_pos + jns .bn2s_absloop + movq $1, %r8 negq %rax - movb $'-', (%rdi) - incq %rdi -.bn2s_pos: - # itoa into buffer - movq %rdi, %r8 # start - movq $10, %rcx -.bn2s_dloop: +.bn2s_absloop: xorq %rdx, %rdx - divq %rcx + movq $10, %rcx + divq %rcx # rax = q, rdx = r addb $'0', %dl + decq %rdi movb %dl, (%rdi) - incq %rdi testq %rax, %rax - jnz .bn2s_dloop - # Reverse the digits - movq %rdi, %rsi # end - movq %r8, %rdi # start - decq %rsi -.bn2s_rev: - cmpq %rdi, %rsi - jle .bn2s_revdone - movb (%rdi), %al - movb (%rsi), %cl - movb %cl, (%rdi) - movb %al, (%rsi) + jnz .bn2s_absloop + testq %r8, %r8 + jz .bn2s_copy + decq %rdi + movb $'-', (%rdi) +.bn2s_copy: + # rdi = start of digit bytes, rsi = end (exclusive) + movq %rsi, %rcx + subq %rdi, %rcx # length + # Allocate string cell: 8 byte length + rcx bytes + pushq %rdi + pushq %rcx + movq %rcx, %rdi + addq $8, %rdi + call heap_alloc + popq %rcx + popq %rdi + movq %rcx, (%rax) # length + movq %rax, %rbx # save obj + leaq 8(%rax), %rdx +.bn2s_copyloop: + testq %rcx, %rcx + jz .bn2s_done + movb (%rdi), %r8b + movb %r8b, (%rdx) incq %rdi - decq %rsi - jmp .bn2s_rev -.bn2s_revdone: - # Compute length - movq %r15, %rax - subq $8, %rax # string obj start - movq %rsi, %rcx # approximate end... need actual end - # Redo: length = current r15 pos + digits - string start - 8 - # Actually this is getting complex. Simpler approach: - # We wrote digits starting at (string_obj + 8), ending at current rdi+1 - # The result string obj is at (r15 - 8 - digit_count - maybe_minus) - # Let me just use the print buffer approach - movq $VAL_VOID, %rax # TODO: fix numtostr properly + incq %rdx + decq %rcx + jmp .bn2s_copyloop +.bn2s_done: + movq %rbx, %rax + orq $TAG_STRING, %rax RET_VAL bi_strtonum: @@ -4344,6 +4399,288 @@ bi_file_to_string: movq $VAL_FALSE, %rax RET_VAL +# ============================================================ +# TCP sockets: fd encoded as port (same as file ports). +# tcp-recv = sys_read, tcp-send = sys_write, tcp-close = close-port. +# ============================================================ + +# encode_port: %rdi = fd → %rax = port value +encode_port: + movq %rdi, %rax + addq $PORT_SPECIAL_BASE, %rax + shlq $3, %rax + orq $TAG_SPECIAL, %rax + ret + +# decode_port: %rdi = port value → %rax = fd (or -1 if not a port) +decode_port: + movq %rdi, %rax + andq $TAG_MASK, %rax + cmpq $TAG_SPECIAL, %rax + jne .dp_bad + movq %rdi, %rax + shrq $3, %rax + cmpq $PORT_SPECIAL_BASE, %rax + jl .dp_bad + subq $PORT_SPECIAL_BASE, %rax + ret +.dp_bad: + movq $-1, %rax + ret + +# bi_tcp_listen: (tcp-listen port) → port or #f +bi_tcp_listen: + GETARG %rax + sarq $3, %rax # untag int → port number + movq %rax, %rbp # save port + + # socket(AF_INET, SOCK_STREAM, 0) + movq $SYS_SOCKET, %rax + movq $AF_INET, %rdi + movq $SOCK_STREAM, %rsi + xorq %rdx, %rdx + syscall + testq %rax, %rax + js .tl_fail + movq %rax, %rbx # fd + + # setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, 4) + subq $16, %rsp + movl $1, (%rsp) + movq $SYS_SETSOCKOPT, %rax + movq %rbx, %rdi + movq $SOL_SOCKET, %rsi + movq $SO_REUSEADDR, %rdx + movq %rsp, %r10 + movq $4, %r8 + syscall + addq $16, %rsp + + # Build sockaddr_in on stack (16 bytes): + # [0:2] sin_family = AF_INET = 2 + # [2:4] sin_port = htons(port) + # [4:8] sin_addr = 0 (INADDR_ANY) + # [8:16] padding = 0 + subq $16, %rsp + movw $AF_INET, (%rsp) + # htons(port): swap bytes of lower 16 bits + movq %rbp, %rax + movw %ax, %cx + rolw $8, %cx + movw %cx, 2(%rsp) + movl $0, 4(%rsp) + movq $0, 8(%rsp) + + # bind(fd, &addr, 16) + movq $SYS_BIND, %rax + movq %rbx, %rdi + movq %rsp, %rsi + movq $16, %rdx + syscall + addq $16, %rsp + testq %rax, %rax + js .tl_close_fail + + # listen(fd, 128) + movq $SYS_LISTEN, %rax + movq %rbx, %rdi + movq $128, %rsi + syscall + testq %rax, %rax + js .tl_close_fail + + movq %rbx, %rdi + call encode_port + RET_VAL +.tl_close_fail: + movq $SYS_CLOSE, %rax + movq %rbx, %rdi + syscall +.tl_fail: + movq $VAL_FALSE, %rax + RET_VAL + +# bi_tcp_accept: (tcp-accept server) → port or #f +bi_tcp_accept: + GETARG %rdi + call decode_port + cmpq $0, %rax + jl .ta_fail + movq %rax, %rbx # server fd + + # accept(fd, &addr, &addrlen); reserve 32 bytes: + # [0..4] addrlen (in: 16, out: 16) + # [8..24] sockaddr_in (16 bytes) + subq $32, %rsp + movl $16, (%rsp) + movq $SYS_ACCEPT, %rax + movq %rbx, %rdi + leaq 8(%rsp), %rsi # sockaddr buffer (16 bytes) + movq %rsp, %rdx # addrlen ptr + syscall + addq $32, %rsp + testq %rax, %rax + js .ta_fail + movq %rax, %rdi + call encode_port + RET_VAL +.ta_fail: + movq $VAL_FALSE, %rax + RET_VAL + +# bi_tcp_connect: (tcp-connect host port) → port or #f +# Only supports dotted-quad IPv4 addresses (no DNS). +bi_tcp_connect: + GETARG %rdi # host string + GETARG %rax # port int + sarq $3, %rax + movq %rax, %rbp # port number + + # Parse dotted quad into 4-byte addr on stack + subq $8, %rsp # buf + movl $0, (%rsp) + andq $-8, %rdi + movq (%rdi), %rcx # length + leaq 8(%rdi), %rdi # bytes + xorq %r8, %r8 # octet value + xorq %r9, %r9 # octet index (0..3) +.tc_parse: + testq %rcx, %rcx + jz .tc_store + movzbq (%rdi), %rax + cmpb $'.', %al + je .tc_dot + subb $'0', %al + cmpb $9, %al + ja .tc_fail_parse + imulq $10, %r8 + addq %rax, %r8 + incq %rdi + decq %rcx + jmp .tc_parse +.tc_dot: + movq %r9, %rax + movb %r8b, (%rsp,%rax) + incq %r9 + cmpq $4, %r9 + jge .tc_fail_parse + xorq %r8, %r8 + incq %rdi + decq %rcx + jmp .tc_parse +.tc_store: + movq %r9, %rax + movb %r8b, (%rsp,%rax) + + # socket() + movq $SYS_SOCKET, %rax + movq $AF_INET, %rdi + movq $SOCK_STREAM, %rsi + xorq %rdx, %rdx + syscall + testq %rax, %rax + js .tc_fail + movq %rax, %rbx # fd + + # Build sockaddr_in + subq $16, %rsp + movw $AF_INET, (%rsp) + movq %rbp, %rax + movw %ax, %cx + rolw $8, %cx + movw %cx, 2(%rsp) + movl 16(%rsp), %eax # the parsed IP (dword at original buf) + movl %eax, 4(%rsp) + movq $0, 8(%rsp) + + movq $SYS_CONNECT, %rax + movq %rbx, %rdi + movq %rsp, %rsi + movq $16, %rdx + syscall + addq $16, %rsp + addq $8, %rsp # discard parse buf + testq %rax, %rax + js .tc_close_fail + + movq %rbx, %rdi + call encode_port + RET_VAL + +.tc_close_fail: + movq $SYS_CLOSE, %rax + movq %rbx, %rdi + syscall + jmp .tc_fail_noparsebuf +.tc_fail_parse: +.tc_fail: + addq $8, %rsp +.tc_fail_noparsebuf: + movq $VAL_FALSE, %rax + RET_VAL + +# bi_tcp_recv: (tcp-recv sock max) → string or #f +bi_tcp_recv: + GETARG %rdi + call decode_port + cmpq $0, %rax + jl .tr_fail + movq %rax, %rbx # fd + GETARG %rax # max bytes + sarq $3, %rax + cmpq $65536, %rax + jle .tr_ok + movq $65536, %rax # cap at 64 KB +.tr_ok: + movq %rax, %rbp # size + + # Allocate string cell: 8-byte length + size bytes + movq %rbp, %rdi + addq $8, %rdi + call heap_alloc + movq %rax, %r12 # string object base + + # read(fd, cell+8, size) + movq $SYS_READ, %rax + movq %rbx, %rdi + leaq 8(%r12), %rsi + movq %rbp, %rdx + syscall + testq %rax, %rax + js .tr_fail + movq %rax, (%r12) # actual length + + movq %r12, %rax + orq $TAG_STRING, %rax + RET_VAL +.tr_fail: + movq $VAL_FALSE, %rax + RET_VAL + +# bi_tcp_send: (tcp-send sock string) → int bytes written or #f +bi_tcp_send: + GETARG %rdi + call decode_port + cmpq $0, %rax + jl .ts_fail + movq %rax, %rbx # fd + + GETARG %rdi # string + andq $-8, %rdi + movq (%rdi), %rdx # length + leaq 8(%rdi), %rsi # bytes + + movq $SYS_WRITE, %rax + movq %rbx, %rdi + syscall + testq %rax, %rax + js .ts_fail + shlq $3, %rax # tag as int + RET_VAL +.ts_fail: + movq $VAL_FALSE, %rax + RET_VAL + # ============================================================ # list_reverse: %rdi = list -> %rax = reversed list # ============================================================ diff --git a/c/builtins.c b/c/builtins.c index de4c5d6..bf2bf46 100644 --- a/c/builtins.c +++ b/c/builtins.c @@ -1110,6 +1110,100 @@ static Value bi_open_input_string(Value *a, int n, Env *e) { ULString *s = AS_STRING(a[0]); return make_string_input_port(s->data, s->len); } + +/* ── TCP sockets: fd wrapped in a FILE* via fdopen, unbuffered ─ */ +#include +#include +#include +#include +#include +#include + +static Value wrap_fd_as_port(int fd, PortDir dir) { + FILE *fp = fdopen(fd, dir == PORT_INPUT ? "rb" : "r+b"); + if (!fp) { close(fd); return VAL_FALSE; } + setvbuf(fp, NULL, _IONBF, 0); + return make_file_port(fp, dir); +} + +static Value bi_tcp_listen(Value *a, int n, Env *e) { + (void)e; CHECK_ARITY("tcp-listen", 1); + int port = (int)as_int(a[0]); + int fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) return VAL_FALSE; + int one = 1; + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); + struct sockaddr_in addr = {0}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_ANY); + addr.sin_port = htons((uint16_t)port); + if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { close(fd); return VAL_FALSE; } + if (listen(fd, 128) < 0) { close(fd); return VAL_FALSE; } + return wrap_fd_as_port(fd, PORT_INPUT); +} + +static Value bi_tcp_accept(Value *a, int n, Env *e) { + (void)e; CHECK_ARITY("tcp-accept", 1); + ULPort *p = AS_PORT(a[0]); + int server_fd = fileno(p->fp); + struct sockaddr_in caddr; + socklen_t clen = sizeof(caddr); + int cfd = accept(server_fd, (struct sockaddr *)&caddr, &clen); + if (cfd < 0) return VAL_FALSE; + return wrap_fd_as_port(cfd, PORT_OUTPUT); +} + +static Value bi_tcp_connect(Value *a, int n, Env *e) { + (void)e; CHECK_ARITY("tcp-connect", 2); check_string(a[0]); + const char *host = AS_STRING(a[0])->data; + int port = (int)as_int(a[1]); + int fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) return VAL_FALSE; + struct sockaddr_in addr = {0}; + addr.sin_family = AF_INET; + addr.sin_port = htons((uint16_t)port); + if (inet_pton(AF_INET, host, &addr.sin_addr) <= 0) { + struct hostent *he = gethostbyname(host); + if (!he) { close(fd); return VAL_FALSE; } + memcpy(&addr.sin_addr, he->h_addr_list[0], he->h_length); + } + if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { close(fd); return VAL_FALSE; } + return wrap_fd_as_port(fd, PORT_OUTPUT); +} + +static Value bi_tcp_recv(Value *a, int n, Env *e) { + (void)e; CHECK_ARITY("tcp-recv", 2); + ULPort *p = AS_PORT(a[0]); + size_t cap = (size_t)as_int(a[1]); + if (cap > (1u << 20)) cap = (1u << 20); + char *buf = ul_malloc(cap + 1); + int fd = fileno(p->fp); + ssize_t got = read(fd, buf, cap); + if (got < 0) { ul_free(buf); return VAL_FALSE; } + buf[got] = '\0'; + Value r = make_string(buf, got, false); + ul_free(buf); + return r; +} + +static Value bi_tcp_send(Value *a, int n, Env *e) { + (void)e; CHECK_ARITY("tcp-send", 2); check_string(a[1]); + ULPort *p = AS_PORT(a[0]); + ULString *s = AS_STRING(a[1]); + int fd = fileno(p->fp); + ssize_t wrote = write(fd, s->data, s->len); + if (wrote < 0) return VAL_FALSE; + return VAL_INT(wrote); +} + +static Value bi_tcp_close(Value *a, int n, Env *e) { + (void)e; CHECK_ARITY("tcp-close", 1); + ULPort *p = AS_PORT(a[0]); + if (p->fp && p->fp != stdin && p->fp != stdout && p->fp != stderr) + fclose(p->fp); + p->closed = true; + return VAL_VOID; +} static Value bi_open_output_string(Value *a, int n, Env *e) { (void)e; return make_string_output_port(); @@ -1736,6 +1830,12 @@ Env *make_global_env(void) { DEF("open-output-file", bi_open_output_file); DEF("write-file", bi_write_file); DEF("file->string", bi_file_to_string); + DEF("tcp-listen", bi_tcp_listen); + DEF("tcp-accept", bi_tcp_accept); + DEF("tcp-connect", bi_tcp_connect); + DEF("tcp-recv", bi_tcp_recv); + DEF("tcp-send", bi_tcp_send); + DEF("tcp-close", bi_tcp_close); DEF("open-input-string", bi_open_input_string); DEF("open-output-string", bi_open_output_string); DEF("get-output-string", bi_get_output_string); diff --git a/examples/http-server.lsp b/examples/http-server.lsp new file mode 100644 index 0000000..8c0aaac --- /dev/null +++ b/examples/http-server.lsp @@ -0,0 +1,90 @@ +;;; http-server.lsp — portable HTTP/1.0 server in pure Scheme +;;; +;;; Runs identically in Python, C, and asm. The only primitives used are +;;; the six socket builtins + display/string-append/substring. +;;; +;;; python3 uncommonlisp.py --fast examples/http-server.lsp +;;; ./c/uncommonlisp examples/http-server.lsp +;;; ./asm/uncommonlisp < examples/http-server.lsp +;;; +;;; Default port 8080. First-line dispatch: GET / → greeting page. +;;; GET /bench → 1 KB body for throughput benchmarks. +;;; Other paths → 404. + +(define *port* 8080) +(define *crlf* "\r\n") +(define *crlf-crlf* "\r\n\r\n") + +;;; ── HTTP helpers ───────────────────────────────────────────── + +(define (http-response status ctype body) + (string-append + "HTTP/1.0 " status *crlf* + "Content-Type: " ctype *crlf* + "Content-Length: " (number->string (string-length body)) *crlf* + "Connection: close" *crlf-crlf* + body)) + +;; Compare via char->integer so we don't need char=? (asm lacks it). +(define SPACE 32) + +(define (char-at s i) (char->integer (string-ref s i))) + +(define (first-token s) + (let ((len (string-length s))) + (let loop ((i 0)) + (cond + ((= i len) s) + ((= (char-at s i) SPACE) (substring s 0 i)) + (else (loop (+ i 1))))))) + +(define (second-token s) + (let ((len (string-length s))) + (let loop1 ((i 0)) + (cond + ((= i len) "") + ((= (char-at s i) SPACE) + (let loop2 ((j (+ i 1))) + (cond + ((= j len) (substring s (+ i 1) len)) + ((= (char-at s j) SPACE) (substring s (+ i 1) j)) + (else (loop2 (+ j 1)))))) + (else (loop1 (+ i 1))))))) + +;;; ── Request handler ───────────────────────────────────────── + +(define *bench-body* + ;; ~1 KB payload so clients have something to measure throughput on. + (let loop ((s "") (i 0)) + (if (= i 32) s + (loop (string-append s "0123456789abcdef0123456789abcdef") (+ i 1))))) + +(define (handle-request req) + (let ((path (second-token req))) + (cond + ((string=? path "/") + (http-response "200 OK" "text/html" + "uncommonlisp

feedback is all you need

portable HTTP in Scheme.

")) + ((string=? path "/bench") + (http-response "200 OK" "text/plain" *bench-body*)) + ((string=? path "/hello") + (http-response "200 OK" "text/plain" "hello world\n")) + (else + (http-response "404 Not Found" "text/plain" + (string-append "not found: " path "\n")))))) + +;;; ── Main loop ─────────────────────────────────────────────── + +(define (serve) + (let ((server (tcp-listen *port*))) + (display "uncommonlisp http server on :") (display *port*) (newline) + (let loop () + (let ((client (tcp-accept server))) + (let ((req (tcp-recv client 4096))) + (if (and req (> (string-length req) 0)) + (tcp-send client (handle-request req)) + #f)) + (tcp-close client)) + (loop)))) + +(serve) diff --git a/tests/web-benchmark.sh b/tests/web-benchmark.sh new file mode 100755 index 0000000..55f8a09 --- /dev/null +++ b/tests/web-benchmark.sh @@ -0,0 +1,104 @@ +#!/bin/bash +# web-benchmark.sh — race the three uncommonlisp HTTP servers +# against Python http.server and busybox httpd. +# +# No wrk/ab/nginx dependency — we use xargs+curl for concurrency. +# +# Fixed 1KB body at /bench for all servers. Wall-clock time over +# N parallel-safe requests = throughput. + +set -u +cd "$(dirname "$0")/.." + +REQUESTS=${REQUESTS:-500} +CONCURRENCY=${CONCURRENCY:-10} +PY="python3 uncommonlisp.py --fast" +C="./c/uncommonlisp" +ASM="./asm/uncommonlisp" + +pad() { printf " %-36s " "$1"; } + +bench_one() { + local label="$1" port="$2" path="$3" + pad "$label" + # Warm up + curl -s "http://localhost:$port$path" > /dev/null + # Measure + local t0 t1 + t0=$(date +%s.%N) + seq 1 "$REQUESTS" | xargs -P "$CONCURRENCY" -I_ \ + curl -s -o /dev/null "http://localhost:$port$path" + t1=$(date +%s.%N) + python3 -c " +t = float('$t1') - float('$t0') +rps = $REQUESTS / t +print(f'{rps:8.0f} req/s ({t:.3f}s total, concurrency $CONCURRENCY)')" +} + +# Setup a 1KB static file for file-serving benchmarks +STATIC_DIR=$(mktemp -d) +printf '%.0s0123456789abcdef0123456789abcdef' {1..32} > "$STATIC_DIR/bench" +trap "rm -rf $STATIC_DIR" EXIT + +echo "═══════════════════════════════════════════════════════════════" +echo "HTTP benchmark — $REQUESTS requests, concurrency $CONCURRENCY" +echo "═══════════════════════════════════════════════════════════════" +echo + +# Helper to start + wait for port ready +start_server() { + local cmd="$1" port="$2" + eval "$cmd" > /dev/null 2>&1 & + local pid=$! + for _ in $(seq 1 20); do + if curl -s -o /dev/null -w "%{http_code}" "http://localhost:$port/" 2>/dev/null | grep -q 200; then + echo "$pid" + return 0 + fi + sleep 0.1 + done + kill "$pid" 2>/dev/null + return 1 +} + +# ── uncommonlisp Python ── +PID=$(start_server "$PY examples/http-server.lsp" 8080) +bench_one "uncommonlisp Python (/bench, 1 KB)" 8080 /bench +kill "$PID" 2>/dev/null; wait "$PID" 2>/dev/null +sleep 0.3 + +# ── uncommonlisp C ── +PID=$(start_server "$C examples/http-server.lsp" 8080) +bench_one "uncommonlisp C (/bench, 1 KB)" 8080 /bench +kill "$PID" 2>/dev/null; wait "$PID" 2>/dev/null +sleep 0.3 + +# ── uncommonlisp asm ── +PID=$(start_server "$ASM < examples/http-server.lsp" 8080) +bench_one "uncommonlisp asm (/bench, 1 KB)" 8080 /bench +kill "$PID" 2>/dev/null; wait "$PID" 2>/dev/null +sleep 0.3 + +# ── Python http.server (stdlib) ── +(cd "$STATIC_DIR" && python3 -m http.server 8080 > /dev/null 2>&1) & +PID=$! +sleep 1 +bench_one "python3 -m http.server (1 KB file)" 8080 /bench +kill "$PID" 2>/dev/null; wait "$PID" 2>/dev/null +sleep 0.3 + +# ── busybox httpd ── +busybox httpd -f -p 127.0.0.1:8080 -h "$STATIC_DIR" > /dev/null 2>&1 & +PID=$! +sleep 0.5 +bench_one "busybox httpd (1 KB file)" 8080 /bench +kill "$PID" 2>/dev/null; wait "$PID" 2>/dev/null + +echo +echo "═══════════════════════════════════════════════════════════════" +echo "Binary sizes" +echo "═══════════════════════════════════════════════════════════════" +printf " uncommonlisp asm: %s\n" "$(du -b asm/uncommonlisp | cut -f1) bytes" +printf " uncommonlisp C: %s\n" "$(du -b c/uncommonlisp | cut -f1) bytes" +printf " busybox httpd: %s\n" "$(du -b /usr/bin/busybox | cut -f1) bytes (multi-call)" +printf " python3: %s bytes (interpreter binary)\n" "$(du -bL $(which python3) | cut -f1)" diff --git a/uncommonlisp.py b/uncommonlisp.py index b001f55..bead283 100644 --- a/uncommonlisp.py +++ b/uncommonlisp.py @@ -2646,6 +2646,37 @@ def _read_file_to_string(path): except OSError: return False +import socket as _sockmod +def _tcp_listen(port): + s = _sockmod.socket(_sockmod.AF_INET, _sockmod.SOCK_STREAM) + s.setsockopt(_sockmod.SOL_SOCKET, _sockmod.SO_REUSEADDR, 1) + s.bind(('0.0.0.0', port)) + s.listen(128) + return s + +def _tcp_accept(server): + client, _addr = server.accept() + return client + +def _tcp_connect(host, port): + s = _sockmod.socket(_sockmod.AF_INET, _sockmod.SOCK_STREAM) + s.connect((host, port)) + return s + +def _tcp_recv(sock, n): + try: + data = sock.recv(n) + except OSError: + return False + return data.decode('utf-8', errors='replace') + +def _tcp_send(sock, s): + data = s.encode('utf-8') if isinstance(s, str) else bytes(s) + try: + return sock.send(data) + except OSError: + return False + def _sym_val(x): if not isinstance(x, Symbol): raise LispErr(f'not a symbol: {show(x)}') return x @@ -3223,6 +3254,12 @@ def make_global_env(): d(S('open-output-file'), lambda a, _: open(_str_val(a[0]), 'w')) d(S('write-file'), lambda a, _: _write_file(_str_val(a[0]), _str_val(a[1]))) d(S('file->string'), lambda a, _: _read_file_to_string(_str_val(a[0]))) + d(S('tcp-listen'), lambda a, _: _tcp_listen(int(a[0]))) + d(S('tcp-accept'), lambda a, _: _tcp_accept(a[0])) + d(S('tcp-connect'), lambda a, _: _tcp_connect(_str_val(a[0]), int(a[1]))) + d(S('tcp-recv'), lambda a, _: _tcp_recv(a[0], int(a[1]))) + d(S('tcp-send'), lambda a, _: _tcp_send(a[0], _str_val(a[1]))) + d(S('tcp-close'), lambda a, _: (a[0].close(), VOID)[-1]) d(S('open-input-string'), lambda a, _: StringInputPort(_str_val(a[0]))) d(S('open-output-string'),lambda a, _: StringOutputPort()) d(S('get-output-string'), lambda a, _: a[0].getvalue() if isinstance(a[0], StringOutputPort) else '')