From ff577391f2185949228de7b3f21617d621acb8fc Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Wed, 22 Jul 2026 15:56:08 -0500 Subject: [PATCH] reconcile(release-runtime): Windows-port + complete v1.0.0 release runtime so the desktop soul cross-compiles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The desktop soul (neuron/dist) compiles against the v1.0.0-20260501 release runtime. After #66 landed the engram natives (tokenized/ranked search, engram_prune_telemetry) and #79 the durable truncation fix into this runtime, two gaps remained before it could cross-compile the Windows brain: 1. Windows OS boundary: the release runtime had no Win32 path. Ported the same _WIN32-guarded shim the mainline runtime carries (#69): #ifdef _WIN32 -> el_platform_win.h (winsock/dlsym/popen + WSAStartup ctor), SOCKET fd guards and el_closesocket() at every socket site, CreateProcessA for exec_bg, the tm_zone/mingw guard, an el_setsockopt optval wrapper (GCC14), and curl-less libcurl stubs. Every change is _WIN32/HAVE_CURL-gated — the POSIX build is byte-identical (gcc -fsyntax-only clean; native behaviour unchanged). 2. Header exports: the release el_runtime.h omitted symbols the soul dist calls that are defined in this runtime's .c — the http_handler_fn/http_handler4_fn typedefs and el_arena_push/pop, engram_prune_telemetry, engram_get_node_by_label. Declaration-only, POSIX-neutral; fixes implicit-declaration/unknown-type errors under the C11 mingw build. Result: x86_64-w64-mingw32-gcc compiles el_runtime.c + all 48 soul modules clean; POSIX gcc -fsyntax-only clean. This is the Windows-port PR the runtime needed on main (the release-runtime counterpart to #69), landed via stage. --- .../v1.0.0-20260501/el_platform_win.h | 186 ++++++++++++++++++ lang/releases/v1.0.0-20260501/el_runtime.c | 80 ++++++-- lang/releases/v1.0.0-20260501/el_runtime.h | 12 ++ 3 files changed, 266 insertions(+), 12 deletions(-) create mode 100644 lang/releases/v1.0.0-20260501/el_platform_win.h diff --git a/lang/releases/v1.0.0-20260501/el_platform_win.h b/lang/releases/v1.0.0-20260501/el_platform_win.h new file mode 100644 index 0000000..1042f44 --- /dev/null +++ b/lang/releases/v1.0.0-20260501/el_platform_win.h @@ -0,0 +1,186 @@ +#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(SOCKET s) { return closesocket(s); } + +/* ── setsockopt optval type ───────────────────────────────────────────────── */ +/* Winsock's setsockopt takes optval as (const char*); POSIX takes (const void*), so el_runtime.c + passes &int directly. GCC 14+ makes that an error under -Wincompatible-pointer-types. Wrap it so + the runtime's POSIX-style call sites compile unchanged (defined before the macro so the wrapper + itself resolves to the real winsock setsockopt). */ +static inline int el_setsockopt(SOCKET s, int level, int optname, const void* optval, int optlen) { + return setsockopt(s, level, optname, (const char*)optval, optlen); +} +#define setsockopt(s, l, o, v, n) el_setsockopt((s), (l), (o), (v), (int)(n)) + +/* ── 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 / 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) { + /* _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) { + 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; +} + +/* ── libcurl: degradable stubs for the curl-less Windows build ─────────────── */ +/* The curl-less validation build (WITH_CURL=0) links no libcurl. el_runtime.c uses libcurl + * unconditionally for its HTTP client / LLM layer; these stubs let it compile and link so the + * runtime, HTTP *server*, graph and memory work natively on Windows. Live outbound HTTP/LLM calls + * degrade to a runtime error (curl_easy_perform returns an error) — matching the documented + * curl-less contract. When HAVE_CURL is defined (WITH_CURL=1) the real is used and + * this whole block is compiled out. POSIX never sees this header, so the POSIX build is untouched. */ +#ifndef HAVE_CURL + +typedef void CURL; +typedef int CURLcode; + +#define CURLE_OK 0 +#define CURLE_HTTP_RETURNED_ERROR 22 +#define CURL_ERROR_SIZE 256 + +/* Option ids: values are irrelevant to the no-op setopt below; kept distinct for readability. */ +#define CURLOPT_URL 10002 +#define CURLOPT_WRITEFUNCTION 20011 +#define CURLOPT_WRITEDATA 10001 +#define CURLOPT_POSTFIELDS 10015 +#define CURLOPT_POSTFIELDSIZE 120 +#define CURLOPT_POST 47 +#define CURLOPT_HTTPHEADER 10023 +#define CURLOPT_TIMEOUT_MS 155 +#define CURLOPT_NOSIGNAL 99 +#define CURLOPT_USERAGENT 10018 +#define CURLOPT_FOLLOWLOCATION 52 +#define CURLOPT_ERRORBUFFER 10010 +#define CURLOPT_CUSTOMREQUEST 10036 +#define CURLOPT_FAILONERROR 45 + +struct curl_slist { char* data; struct curl_slist* next; }; + +static inline struct curl_slist* curl_slist_append(struct curl_slist* list, const char* s) { + struct curl_slist* node = (struct curl_slist*)malloc(sizeof(struct curl_slist)); + if (!node) return list; + node->data = s ? strdup(s) : NULL; + node->next = NULL; + if (!list) return node; + struct curl_slist* p = list; + while (p->next) p = p->next; + p->next = node; + return list; +} +static inline void curl_slist_free_all(struct curl_slist* list) { + while (list) { struct curl_slist* n = list->next; free(list->data); free(list); list = n; } +} + +static inline CURL* curl_easy_init(void) { return (CURL*)malloc(1); } +static inline CURLcode curl_easy_setopt(CURL* h, int opt, ...) { (void)h; (void)opt; return CURLE_OK; } +static inline CURLcode curl_easy_perform(CURL* h) { (void)h; return 7 /* CURLE_COULDNT_CONNECT */; } +static inline void curl_easy_cleanup(CURL* h) { free(h); } +static inline const char* curl_easy_strerror(CURLcode c) { + (void)c; return "libcurl not built in (curl-less build)"; +} + +#endif /* !HAVE_CURL */ + +#endif /* EL_PLATFORM_WIN_H */ diff --git a/lang/releases/v1.0.0-20260501/el_runtime.c b/lang/releases/v1.0.0-20260501/el_runtime.c index d05edfe..4629cc2 100644 --- a/lang/releases/v1.0.0-20260501/el_runtime.c +++ b/lang/releases/v1.0.0-20260501/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,7 +46,16 @@ #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 +/* libcurl: present on POSIX and on the WITH_CURL Windows build; absent on the curl-less Windows + validation build, where el_platform_win.h supplies degradable stubs. On POSIX (_WIN32 undefined) + this is always taken, so the POSIX build is unchanged. */ +#if !defined(_WIN32) || defined(HAVE_CURL) #include +#endif /* ── Internal allocators ─────────────────────────────────────────────────── */ @@ -1468,12 +1481,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) { @@ -1516,7 +1537,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--; @@ -1546,14 +1567,18 @@ void http_serve(el_val_t port, el_val_t handler) { 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; + perror("bind"); el_closesocket(sock); return; } - if (listen(sock, 64) < 0) { perror("listen"); close(sock); return; } + if (listen(sock, 64) < 0) { perror("listen"); el_closesocket(sock); return; } fprintf(stderr, "[http] listening on [::]:%d (dual-stack)\n", p); 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; @@ -1565,11 +1590,11 @@ void 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); @@ -1578,7 +1603,7 @@ void http_serve(el_val_t port, el_val_t handler) { } pthread_detach(tid); } - close(sock); + el_closesocket(sock); } /* ── http_serve_async — non-blocking HTTP server ─────────────────────────── */ @@ -1814,7 +1839,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) { @@ -1854,7 +1883,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); @@ -1884,14 +1913,18 @@ void http_serve_v2(el_val_t port, el_val_t handler) { 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; + perror("bind"); el_closesocket(sock); return; } - if (listen(sock, 64) < 0) { perror("listen"); close(sock); return; } + if (listen(sock, 64) < 0) { perror("listen"); el_closesocket(sock); return; } fprintf(stderr, "[http v2] listening on [::]:%d (dual-stack)\n", p); 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; @@ -1903,11 +1936,11 @@ void 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); @@ -1916,7 +1949,7 @@ void http_serve_v2(el_val_t port, el_val_t handler) { } pthread_detach(tid); } - close(sock); + el_closesocket(sock); } /* Build the response envelope a 4-arg handler can return. We hand-write @@ -2063,6 +2096,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 */ @@ -2085,6 +2135,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) { @@ -4214,7 +4265,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); diff --git a/lang/releases/v1.0.0-20260501/el_runtime.h b/lang/releases/v1.0.0-20260501/el_runtime.h index a3dd683..c3dad8e 100644 --- a/lang/releases/v1.0.0-20260501/el_runtime.h +++ b/lang/releases/v1.0.0-20260501/el_runtime.h @@ -758,6 +758,18 @@ el_val_t trace_span_start(el_val_t name); el_val_t trace_span_end(el_val_t span_handle); el_val_t emit_event(el_val_t name, el_val_t duration_ms); +/* ── Runtime symbols required by the soul modules ──────────────────────────── */ +/* All implemented in el_runtime.c but omitted from this release header; the soul dist modules + * reference them directly, so the public header must export them. Declarations only — mirrors the + * mainline el_runtime.h and is platform-independent (no behavioural change to the POSIX build). */ +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); +el_val_t el_arena_push(void); +el_val_t el_arena_pop(el_val_t mark); +void http_serve_async(el_val_t port, el_val_t handler); +el_val_t engram_get_node_by_label(el_val_t label); +el_val_t engram_prune_telemetry(el_val_t older_than_ms); + #ifdef __cplusplus } #endif