From e91a3704fecf27ca0bb398d8b292d965290c31c5 Mon Sep 17 00:00:00 2001 From: "shoney.arickathil" Date: Mon, 7 Sep 2026 18:57:50 +0200 Subject: [PATCH] feat(net): net.connect outbound TCP client (id 110) - new builtin net.connect(host, port) -> Int: the outbound-socket gap language 38 named and jarvis surfaced; the client half of the net verbs - getaddrinfo for DNS (v4/v6, numeric or hostname), blocking connect with the same EINTR/stop handling as net.connect_unix, then O_NONBLOCK for the park plane; returns the same fd-scalar accept yields - wob.h enum + WO_B_MAX 110; types.ml registration; loader.c arity; builtin.c sysio dispatch range extended to WO_B_NET_CONNECT; sysio.c impl - verified: numeric IP + hostname (DNS) connect to a local listener, closed port traps cleanly; ASan-clean; runtime battery 0 fail - deferred (next slice): net.connect_dl deadline/park variant (no shard stall during handshake), on the accept_dl pattern (cherry picked from commit 13c6f124428243b4956fbb4eceb1e7d0206d45f2) --- compiler/src/types.ml | 1 + runtime/src/builtin.c | 2 +- runtime/src/loader.c | 2 +- runtime/src/sysio.c | 54 +++++++++++++++++++++++++++++++++++++++++++ runtime/src/wob.h | 7 +++++- 5 files changed, 63 insertions(+), 3 deletions(-) diff --git a/compiler/src/types.ml b/compiler/src/types.ml index b7caf8c..621834a 100644 --- a/compiler/src/types.ml +++ b/compiler/src/types.ml @@ -364,6 +364,7 @@ let stdlib_members : stdlib_member list = m "net" "send_fd" 2 105 (Some (TScalar "Bool")) None; m "net" "recv_fd" 1 106 (Some (TNullable (TScalar "Int"))) None; m "net" "connect_unix" 1 107 (Some (TScalar "Int")) None; + m "net" "connect" 2 110 (Some (TScalar "Int")) None; (* runtime-v2 6: resize's read twin (nil = not a tty), and a codepoint's terminal cell width (libc wcwidth under C.UTF-8) *) m "term" "size" 1 108 (Some (TNullable (TScalar termsize_record_name))) (Some termsize_record_name); diff --git a/runtime/src/builtin.c b/runtime/src/builtin.c index ed6fbc3..04ced17 100644 --- a/runtime/src/builtin.c +++ b/runtime/src/builtin.c @@ -172,7 +172,7 @@ int wo_builtin(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg) { if (C == WO_B_JSON_ENCODE || C == WO_B_JSON_DECODE) return wo_builtin_json(vm, R, ins, msg); if ((C >= WO_B_SYS_FIRST && C <= WO_B_PROC_RUN) || C == WO_B_TIME_TICKS - || (C >= WO_B_NET_READ_DL && C <= WO_B_TERM_WIDTH)) + || (C >= WO_B_NET_READ_DL && C <= WO_B_NET_CONNECT)) return wo_builtin_sys(vm, R, ins, msg); if (C >= WO_B_SHA1 && C <= WO_B_HMAC_SHA256) return wo_builtin_crypto(vm, R, ins, msg); diff --git a/runtime/src/loader.c b/runtime/src/loader.c index 28b2605..6a6c88a 100644 --- a/runtime/src/loader.c +++ b/runtime/src/loader.c @@ -78,7 +78,7 @@ static const uint8_t b_arity[WO_B_MAX + 1] = { [WO_B_PROC_SPAWN_PTY] = 5, [WO_B_PROC_RESIZE] = 3, [WO_B_SIGNAL_ON] = 3, [WO_B_TERM_RAW] = 1, [WO_B_TERM_RESTORE] = 1, [WO_B_NET_SEND_FD] = 2, [WO_B_NET_RECV_FD] = 1, [WO_B_NET_CONNECT_UNIX] = 1, - [WO_B_TERM_SIZE] = 2, [WO_B_TERM_WIDTH] = 1, + [WO_B_TERM_SIZE] = 2, [WO_B_TERM_WIDTH] = 1, [WO_B_NET_CONNECT] = 2, /* json (json.c): encode takes the value's static kind, decode the class id to build */ [WO_B_JSON_ENCODE] = 2, [WO_B_JSON_DECODE] = 2, [WO_B_MAP_GET_OPT] = 2, diff --git a/runtime/src/sysio.c b/runtime/src/sysio.c index fd581e7..1673fb6 100644 --- a/runtime/src/sysio.c +++ b/runtime/src/sysio.c @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -1552,6 +1553,59 @@ int wo_builtin_sys(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg) { R[A] = (uint64_t)fd; return 0; } + case WO_B_NET_CONNECT: { /* (host, port) -> Int: outbound TCP client fd. + * DNS via getaddrinfo (v4 or v6), blocking + * connect with the same EINTR/stop handling as + * WO_B_NET_CONNECT_UNIX, then nonblocking for the + * plane. A _dl deadline/park variant is the next + * slice; this one can stall the shard during the + * handshake, tolerable while connect is rare. */ + if (cstr_of(R[B], path, sizeof path, msg)) return WO_T_BOUNDS; + int64_t port = (int64_t)R[B + 1]; + if (port < 0 || port > 65535) { + *msg = "port out of range"; + return WO_T_BOUNDS; + } + char portstr[8]; + snprintf(portstr, sizeof portstr, "%u", (unsigned)port); + struct addrinfo hints; + struct addrinfo *res = NULL; + struct addrinfo *ai; + memset(&hints, 0, sizeof hints); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + int grc = getaddrinfo(path, portstr, &hints, &res); + if (grc != 0) { + *msg = gai_strerror(grc); + return WO_T_IO; + } + int fd = -1; + for (ai = res; ai; ai = ai->ai_next) { + fd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); + if (fd < 0) continue; + int rc; + for (;;) { + rc = connect(fd, ai->ai_addr, ai->ai_addrlen); + if (rc == 0 || errno != EINTR) break; + if (stop_pending()) { + close(fd); + freeaddrinfo(res); + return WO_SYS_STOPPED; + } + } + if (rc == 0) break; + close(fd); + fd = -1; + } + freeaddrinfo(res); + if (fd < 0) { + *msg = strerror(errno); + return WO_T_IO; + } + fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK); + R[A] = (uint64_t)fd; + return 0; + } case WO_B_NET_SEND_FD: { /* (conn, fd) -> Bool: SCM_RIGHTS, one fd */ int conn = (int)(int64_t)R[B]; int pass = (int)(int64_t)R[B + 1]; diff --git a/runtime/src/wob.h b/runtime/src/wob.h index 8325c15..3d6f176 100644 --- a/runtime/src/wob.h +++ b/runtime/src/wob.h @@ -547,9 +547,14 @@ enum { WO_B_TERM_WIDTH = 109, /* (codepoint) -> Int cell width via libc * wcwidth under C.UTF-8: -1 control, * 0 combining, 1, or 2 */ + /* ---- language 38 / jarvis: the outbound-socket client side. TCP + * connect with DNS via getaddrinfo; blocking connect then nonblocking + * for the plane, mirroring WO_B_NET_CONNECT_UNIX. A _dl deadline/park + * variant is the next slice. Underneath runtime-v2 9's outbound TLS. */ + WO_B_NET_CONNECT = 110, /* (host, port) -> Int: outbound TCP client fd */ }; -#define WO_B_MAX 109u +#define WO_B_MAX 110u /* ids at or above this one live in sysio.c, not builtin.c */ #define WO_B_SYS_FIRST WO_B_FS_EXISTS