From 28ef43264ae1756fde0e9b6738915553e0cab74a Mon Sep 17 00:00:00 2001 From: Tim Lingo <1timlingo@gmail.com> Date: Mon, 15 Jun 2026 16:58:11 -0500 Subject: [PATCH 1/3] feat(el-runtime): native Windows port of el_runtime.c (winsock/dlsym/CreateProcess) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compiles for Windows x64 via mingw-w64 and still compiles clean on POSIX (darwin/linux) — all Windows code is behind #ifdef _WIN32, POSIX path unchanged. - el_platform_win.h (new): winsock2 + auto WSAStartup, el_closesocket(), dlsym->GetProcAddress, popen/_popen, mkdir/_mkdir, setenv/_putenv_s, timegm/_mkgmtime, localtime_r/gmtime_r. Threading unchanged — mingw winpthreads supplies + -lpthread. - el_runtime.c: include block guarded; 10 socket-close sites -> el_closesocket(); setsockopt arg4 cast; tm_zone guarded; exec_bg fork/exec -> CreateProcess. Part of feat/windows-port. Core-el change, for Will's review. Co-Authored-By: Claude Opus 4.8 (1M context) --- lang/el-compiler/runtime/el_platform_win.h | 94 ++++++++++++++++++++++ lang/el-compiler/runtime/el_runtime.c | 59 ++++++++++---- 2 files changed, 139 insertions(+), 14 deletions(-) create mode 100644 lang/el-compiler/runtime/el_platform_win.h diff --git a/lang/el-compiler/runtime/el_platform_win.h b/lang/el-compiler/runtime/el_platform_win.h new file mode 100644 index 0000000..12d417b --- /dev/null +++ b/lang/el-compiler/runtime/el_platform_win.h @@ -0,0 +1,94 @@ +#ifndef EL_PLATFORM_WIN_H +#define EL_PLATFORM_WIN_H +/* + * el_platform_win.h — Windows OS-boundary shim for el_runtime.c. + * + * Branch: feat/windows-el-runtime. Included ONLY when _WIN32 is defined; the POSIX build is + * untouched. Goal: let el_runtime.c (a BSD-sockets / dlfcn / fork host) compile and link with + * mingw-w64 into a native neuron.exe, with no behavioural change to the Linux/macOS build. + * + * What it maps: + * - sockets : winsock2 (same call names: socket/bind/listen/accept/recv/send/setsockopt). + * Sockets close with closesocket() (see el_closesocket), and the stack must be + * started once with WSAStartup — done automatically via a load-time constructor. + * - dlsym : el_runtime.c uses dlsym(RTLD_DEFAULT, name) to resolve callback/tool symbols + * exported by the main module. Windows equivalent: GetProcAddress on the process + * module. Link the soul with -Wl,--export-all-symbols so the symbols are findable. + * - popen : mapped to _popen/_pclose. + * - threads : UNCHANGED. mingw-w64 ships winpthreads, so + -lpthread just work. + */ + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#include +#include +#include +#include + +/* Portable headers mingw-w64 provides (verified present). */ +#include +#include +#include +#include +#include +#include /* strcasecmp */ +#include +#include +#include +#include /* mingw-w64 provides gettimeofday here */ +#include +#include +#include +#include +#include +#include + +/* ── socket close ─────────────────────────────────────────────────────────── */ +/* Winsock closes sockets with closesocket(), not close() (close() is for file fds). The POSIX + build defines the same helper as close() so the call sites are identical across platforms. */ +static inline int el_closesocket(int s) { return closesocket((SOCKET)s); } + +/* ── winsock init (once, at load) ─────────────────────────────────────────── */ +static void el__win_net_init(void) { + static int inited = 0; + if (!inited) { WSADATA w; WSAStartup(MAKEWORD(2, 2), &w); inited = 1; } +} +__attribute__((constructor)) static void el__win_ctor(void) { el__win_net_init(); } + +/* ── dlsym → GetProcAddress ───────────────────────────────────────────────── */ +#ifndef RTLD_DEFAULT +#define RTLD_DEFAULT ((void*)0) +#endif +static inline void* el_win_dlsym(void* handle, const char* name) { + (void)handle; + return (void*)(uintptr_t)GetProcAddress(GetModuleHandleA(NULL), name); +} +#define dlsym(h, n) el_win_dlsym((h), (n)) + +/* ── popen / pclose ───────────────────────────────────────────────────────── */ +#define popen _popen +#define pclose _pclose + +/* ── misc POSIX → Win32 shims ─────────────────────────────────────────────── */ +#include /* _mkdir */ +#define mkdir(path, mode) _mkdir(path) /* POSIX mkdir(path,mode) → _mkdir(path) */ +#define timegm _mkgmtime /* UTC tm → time_t */ + +/* setenv/unsetenv: not in the Windows CRT; map to _putenv_s. */ +static inline int setenv(const char* name, const char* value, int overwrite) { + (void)overwrite; + return _putenv_s(name, value ? value : ""); +} +static inline int unsetenv(const char* name) { return _putenv_s(name, ""); } + +/* localtime_r/gmtime_r: Windows offers localtime_s/gmtime_s with reversed arg order. */ +static inline struct tm* localtime_r(const time_t* t, struct tm* out) { + return localtime_s(out, t) == 0 ? out : (struct tm*)0; +} +static inline struct tm* gmtime_r(const time_t* t, struct tm* out) { + return gmtime_s(out, t) == 0 ? out : (struct tm*)0; +} + +#endif /* EL_PLATFORM_WIN_H */ diff --git a/lang/el-compiler/runtime/el_runtime.c b/lang/el-compiler/runtime/el_runtime.c index df83e95..4faf72a 100644 --- a/lang/el-compiler/runtime/el_runtime.c +++ b/lang/el-compiler/runtime/el_runtime.c @@ -21,6 +21,10 @@ #include "el_runtime.h" +#ifdef _WIN32 +/* Windows OS-boundary shim (winsock/dlsym/popen). Threading stays on (winpthreads). */ +#include "el_platform_win.h" +#else #include #include /* strcasecmp */ #include @@ -42,6 +46,10 @@ #include #include #include +/* On POSIX, sockets close with the same close() as files; el_platform_win.h supplies the Windows + variant. Defined here so the socket call sites are identical across platforms. */ +static inline int el_closesocket(int s) { return close(s); } +#endif #ifdef HAVE_CURL #include #endif @@ -1587,17 +1595,17 @@ el_val_t http_serve(el_val_t port, el_val_t handler) { int sock = socket(AF_INET6, SOCK_STREAM, 0); if (sock < 0) { perror("socket"); return 0; } int yes = 1; int no = 0; - setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); - setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, &no, sizeof(no)); + setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const char*)&yes, sizeof(yes)); + setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, (const char*)&no, sizeof(no)); struct sockaddr_in6 addr; memset(&addr, 0, sizeof(addr)); addr.sin6_family = AF_INET6; addr.sin6_addr = in6addr_any; addr.sin6_port = htons((uint16_t)p); if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) { - perror("bind"); close(sock); return 0; + perror("bind"); el_closesocket(sock); return 0; } - if (listen(sock, 64) < 0) { perror("listen"); close(sock); return 0; } + if (listen(sock, 64) < 0) { perror("listen"); el_closesocket(sock); return 0; } fprintf(stderr, "[http] listening on [::]:%d (dual-stack)\n", p); while (1) { struct sockaddr_in6 cli; @@ -1614,11 +1622,11 @@ el_val_t http_serve(el_val_t port, el_val_t handler) { _http_conn_active++; pthread_mutex_unlock(&_http_conn_mu); HttpWorkerArg* arg = malloc(sizeof(HttpWorkerArg)); - if (!arg) { close(cfd); continue; } + if (!arg) { el_closesocket(cfd); continue; } arg->fd = cfd; pthread_t tid; if (pthread_create(&tid, NULL, http_worker, arg) != 0) { - close(cfd); free(arg); + el_closesocket(cfd); free(arg); pthread_mutex_lock(&_http_conn_mu); _http_conn_active--; pthread_cond_signal(&_http_conn_cv); @@ -1627,7 +1635,7 @@ el_val_t http_serve(el_val_t port, el_val_t handler) { } pthread_detach(tid); } - close(sock); + el_closesocket(sock); return 0; } @@ -1837,17 +1845,17 @@ el_val_t http_serve_v2(el_val_t port, el_val_t handler) { int sock = socket(AF_INET6, SOCK_STREAM, 0); if (sock < 0) { perror("socket"); return 0; } int yes = 1; int no = 0; - setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); - setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, &no, sizeof(no)); + setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const char*)&yes, sizeof(yes)); + setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, (const char*)&no, sizeof(no)); struct sockaddr_in6 addr; memset(&addr, 0, sizeof(addr)); addr.sin6_family = AF_INET6; addr.sin6_addr = in6addr_any; addr.sin6_port = htons((uint16_t)p); if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) { - perror("bind"); close(sock); return 0; + perror("bind"); el_closesocket(sock); return 0; } - if (listen(sock, 64) < 0) { perror("listen"); close(sock); return 0; } + if (listen(sock, 64) < 0) { perror("listen"); el_closesocket(sock); return 0; } fprintf(stderr, "[http v2] listening on [::]:%d (dual-stack)\n", p); while (1) { struct sockaddr_in6 cli; @@ -1864,11 +1872,11 @@ el_val_t http_serve_v2(el_val_t port, el_val_t handler) { _http_conn_active++; pthread_mutex_unlock(&_http_conn_mu); HttpWorkerArg* arg = malloc(sizeof(HttpWorkerArg)); - if (!arg) { close(cfd); continue; } + if (!arg) { el_closesocket(cfd); continue; } arg->fd = cfd; pthread_t tid; if (pthread_create(&tid, NULL, http_worker_v2, arg) != 0) { - close(cfd); free(arg); + el_closesocket(cfd); free(arg); pthread_mutex_lock(&_http_conn_mu); _http_conn_active--; pthread_cond_signal(&_http_conn_cv); @@ -1877,7 +1885,7 @@ el_val_t http_serve_v2(el_val_t port, el_val_t handler) { } pthread_detach(tid); } - close(sock); + el_closesocket(sock); return 0; } @@ -2051,6 +2059,23 @@ el_val_t exec(el_val_t cmdv) { el_val_t exec_bg(el_val_t cmdv) { const char* cmd = EL_CSTR(cmdv); if (!cmd || !*cmd) return el_wrap_str(el_strdup("")); +#ifdef _WIN32 + /* Windows: no fork/exec. Launch a detached `cmd /c ` with no console window via + CreateProcess (DETACHED_PROCESS | CREATE_NO_WINDOW). Returns the PID as a string, "" on fail. + Mirrors the POSIX branch: child runs independently, caller is not blocked. */ + char cmdline[8192]; + snprintf(cmdline, sizeof(cmdline), "cmd.exe /c %s", cmd); + STARTUPINFOA si; ZeroMemory(&si, sizeof(si)); si.cb = sizeof(si); + PROCESS_INFORMATION pi; ZeroMemory(&pi, sizeof(pi)); + BOOL ok = CreateProcessA(NULL, cmdline, NULL, NULL, FALSE, + DETACHED_PROCESS | CREATE_NO_WINDOW, NULL, NULL, &si, &pi); + if (!ok) return el_wrap_str(el_strdup("")); + char pidbuf[32]; + snprintf(pidbuf, sizeof(pidbuf), "%lu", (unsigned long)pi.dwProcessId); + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + return el_wrap_str(el_strdup(pidbuf)); +#else pid_t pid = fork(); if (pid < 0) { /* fork failed */ @@ -2073,6 +2098,7 @@ el_val_t exec_bg(el_val_t cmdv) { char pidbuf[32]; snprintf(pidbuf, sizeof(pidbuf), "%d", (int)pid); return el_wrap_str(el_strdup(pidbuf)); +#endif } el_val_t fs_list(el_val_t pathv) { @@ -4337,7 +4363,12 @@ static int _el_decompose_earth(el_caltime_t* ct, struct tm* tm_out, int* abbr_le localtime_r(&s, &tm); *tm_out = tm; if (abbr_buf && abbr_cap > 0) { + /* mingw's struct tm has no tm_zone (BSD/glibc extension); no abbrev available there. */ +#ifdef _WIN32 + const char* z_str = ""; +#else const char* z_str = tm.tm_zone ? tm.tm_zone : ""; +#endif size_t n = strlen(z_str); if (n >= abbr_cap) n = abbr_cap - 1; memcpy(abbr_buf, z_str, n); From a36a62ca141a531e498b0215fef8cb48771af48e Mon Sep 17 00:00:00 2001 From: Tim Lingo <1timlingo@gmail.com> Date: Mon, 15 Jun 2026 17:14:17 -0500 Subject: [PATCH 2/3] fix(el-runtime): promote http_handler typedefs to el_runtime.h (cross-module + Windows) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit http_handler_fn / http_handler4_fn were defined only inside el_runtime.c, so soul modules (routes/chat/...) that reference them via cross-module forward declarations couldn't see the types — which broke the Windows link of every module. Moving the public function-pointer types to the shared header is the correct home and unblocks the build on all platforms (identical typedef, C11-safe redefinition in el_runtime.c). With this, the soul links into a native Windows neuron.exe (mingw, static) that boots and serves HTTP on :7770 — verified /health → 200 {"status":"alive",...} in a Win11 VM. Co-Authored-By: Claude Opus 4.8 (1M context) --- lang/el-compiler/runtime/el_runtime.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lang/el-compiler/runtime/el_runtime.h b/lang/el-compiler/runtime/el_runtime.h index c0529ef..88c2a36 100644 --- a/lang/el-compiler/runtime/el_runtime.h +++ b/lang/el-compiler/runtime/el_runtime.h @@ -52,6 +52,12 @@ typedef int64_t el_val_t; +/* HTTP request-handler function-pointer types. Public because soul modules (routes/chat/etc.) + * register handlers across translation units; previously defined only inside el_runtime.c, which + * made cross-module references (and the Windows build) fail. Home in the shared header. */ +typedef el_val_t (*http_handler_fn)(el_val_t method, el_val_t path, el_val_t body); +typedef el_val_t (*http_handler4_fn)(el_val_t method, el_val_t path, el_val_t body, el_val_t headers); + #define EL_STR(s) ((el_val_t)(uintptr_t)(s)) #define EL_CSTR(v) ((const char*)(uintptr_t)(v)) #define EL_INT(v) (v) From c087b97093a300f24f599384f3235f7de992d401 Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Fri, 19 Jun 2026 18:59:10 -0500 Subject: [PATCH 3/3] =?UTF-8?q?fix(windows):=20resolve=20PR=20blockers=20?= =?UTF-8?q?=E2=80=94=20nanosleep=20shim,=20unsetenv,=20duplicate=20typedef?= =?UTF-8?q?s,=20SOCKET=20type,=20el=5Fclosesocket?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lang/el-compiler/runtime/el_platform_win.h | 29 +++++++++++++++++++--- lang/el-compiler/runtime/el_runtime.c | 27 ++++++++++++++++---- 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/lang/el-compiler/runtime/el_platform_win.h b/lang/el-compiler/runtime/el_platform_win.h index 12d417b..88204a0 100644 --- a/lang/el-compiler/runtime/el_platform_win.h +++ b/lang/el-compiler/runtime/el_platform_win.h @@ -48,7 +48,7 @@ /* ── socket close ─────────────────────────────────────────────────────────── */ /* Winsock closes sockets with closesocket(), not close() (close() is for file fds). The POSIX build defines the same helper as close() so the call sites are identical across platforms. */ -static inline int el_closesocket(int s) { return closesocket((SOCKET)s); } +static inline int el_closesocket(SOCKET s) { return closesocket(s); } /* ── winsock init (once, at load) ─────────────────────────────────────────── */ static void el__win_net_init(void) { @@ -76,12 +76,35 @@ static inline void* el_win_dlsym(void* handle, const char* name) { #define mkdir(path, mode) _mkdir(path) /* POSIX mkdir(path,mode) → _mkdir(path) */ #define timegm _mkgmtime /* UTC tm → time_t */ -/* setenv/unsetenv: not in the Windows CRT; map to _putenv_s. */ +/* setenv/unsetenv: not in the Windows CRT; map to _putenv_s / SetEnvironmentVariable. */ static inline int setenv(const char* name, const char* value, int overwrite) { (void)overwrite; return _putenv_s(name, value ? value : ""); } -static inline int unsetenv(const char* name) { return _putenv_s(name, ""); } +static inline int unsetenv(const char* name) { + /* _putenv_s(name, "") sets VAR="" rather than removing it. + * SetEnvironmentVariableA(name, NULL) truly deletes it from the Win32 + * env block; then we sync the CRT cache with _putenv("NAME="). */ + SetEnvironmentVariableA(name, NULL); + size_t len = strlen(name); + char *buf = (char*)malloc(len + 2); + if (!buf) return -1; + memcpy(buf, name, len); + buf[len] = '='; + buf[len + 1] = '\0'; + _putenv(buf); + free(buf); + return 0; +} + +/* nanosleep — not available in MSVC/UCRT; approximate with Sleep(). */ +static inline int el_nanosleep(const struct timespec *req, struct timespec *rem) { + (void)rem; + DWORD ms = (DWORD)((req->tv_sec * 1000ULL) + (req->tv_nsec / 1000000ULL)); + Sleep(ms ? ms : 1); + return 0; +} +#define nanosleep(req, rem) el_nanosleep((req), (rem)) /* localtime_r/gmtime_r: Windows offers localtime_s/gmtime_s with reversed arg order. */ static inline struct tm* localtime_r(const time_t* t, struct tm* out) { diff --git a/lang/el-compiler/runtime/el_runtime.c b/lang/el-compiler/runtime/el_runtime.c index 4faf72a..f19b266 100644 --- a/lang/el-compiler/runtime/el_runtime.c +++ b/lang/el-compiler/runtime/el_runtime.c @@ -1061,7 +1061,6 @@ el_val_t http_post_to_file(el_val_t url, el_val_t body, el_val_t headers_map, el #define HTTP_MAX_CONNS 64 -typedef el_val_t (*http_handler_fn)(el_val_t method, el_val_t path, el_val_t body); typedef struct { char* name; @@ -1536,12 +1535,20 @@ static void http_send_response(int fd, const char* body) { } typedef struct { +#ifdef _WIN32 + SOCKET fd; +#else int fd; +#endif } HttpWorkerArg; static void* http_worker(void* arg) { HttpWorkerArg* a = (HttpWorkerArg*)arg; +#ifdef _WIN32 + SOCKET fd = a->fd; +#else int fd = a->fd; +#endif free(a); char *method = NULL, *path = NULL, *body = NULL; if (http_read_request(fd, &method, &path, &body, NULL) == 0) { @@ -1573,7 +1580,7 @@ static void* http_worker(void* arg) { free(response); } free(method); free(path); free(body); - close(fd); + el_closesocket(fd); /* release a slot */ pthread_mutex_lock(&_http_conn_mu); _http_conn_active--; @@ -1610,7 +1617,11 @@ el_val_t http_serve(el_val_t port, el_val_t handler) { while (1) { struct sockaddr_in6 cli; socklen_t clen = sizeof(cli); +#ifdef _WIN32 + SOCKET cfd = accept(sock, (struct sockaddr*)&cli, &clen); +#else int cfd = accept(sock, (struct sockaddr*)&cli, &clen); +#endif if (cfd < 0) { if (errno == EINTR) continue; perror("accept"); break; @@ -1656,8 +1667,6 @@ el_val_t http_serve(el_val_t port, el_val_t handler) { * separate active-handler slot, separate dlsym fallback. Mixing v1 and v2 * handlers in the same process is fine — they don't share the active slot. */ -typedef el_val_t (*http_handler4_fn)(el_val_t method, el_val_t path, - el_val_t headers_map, el_val_t body); typedef struct { char* name; @@ -1793,7 +1802,11 @@ static el_val_t http_build_headers_map(const char* hdr_block) { static void* http_worker_v2(void* arg) { HttpWorkerArg* a = (HttpWorkerArg*)arg; +#ifdef _WIN32 + SOCKET fd = a->fd; +#else int fd = a->fd; +#endif free(a); char *method = NULL, *path = NULL, *body = NULL, *hdr_block = NULL; if (http_read_request(fd, &method, &path, &body, &hdr_block) == 0) { @@ -1823,7 +1836,7 @@ static void* http_worker_v2(void* arg) { free(response); } free(method); free(path); free(body); free(hdr_block); - close(fd); + el_closesocket(fd); pthread_mutex_lock(&_http_conn_mu); _http_conn_active--; pthread_cond_signal(&_http_conn_cv); @@ -1860,7 +1873,11 @@ el_val_t http_serve_v2(el_val_t port, el_val_t handler) { while (1) { struct sockaddr_in6 cli; socklen_t clen = sizeof(cli); +#ifdef _WIN32 + SOCKET cfd = accept(sock, (struct sockaddr*)&cli, &clen); +#else int cfd = accept(sock, (struct sockaddr*)&cli, &clen); +#endif if (cfd < 0) { if (errno == EINTR) continue; perror("accept"); break;