From 9497a30cc6638999f18dc36199cd202d5963941b Mon Sep 17 00:00:00 2001 From: Rawan10101 Date: Wed, 10 Jun 2026 14:09:16 +0300 Subject: [PATCH 1/4] fix: setjmp/longjmp and stack overflow handling for WASM build --- build.zig | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/build.zig b/build.zig index 3e7f285214..4f97e019f8 100644 --- a/build.zig +++ b/build.zig @@ -570,6 +570,7 @@ pub fn build(b: *std.Build) !void { if (is_windows) "-DWIN32_LEAN_AND_MEAN" else "", if (is_windows) "-DUTF8PROC_STATIC" else "", if (use_unibilium) "-DHAVE_UNIBILIUM" else "", + if (is_wasm) "-flto" else "", }; nvim_mod.addCSourceFiles(.{ .files = src_paths, .flags = &flags }); @@ -610,14 +611,28 @@ pub fn build(b: *std.Build) !void { emcc.addArgs(&.{ b.fmt("--sysroot={s}", .{s}), "-sALLOW_MEMORY_GROWTH=1", - "-sEXPORT_ALL=1", - "-sINITIAL_MEMORY=67108864", + "--profiling-funcs", + "-sEXPORTED_FUNCTIONS=_main,_malloc,_free", + "-sEXPORTED_RUNTIME_METHODS=stringToUTF8,lengthBytesUTF8,setValue,getValue,UTF8ToString,callMain,ENV,FS", + "-sSTACK_SIZE=134217728", // 64MB — emscripten's own flag, overrides its 65536 + "-sINITIAL_MEMORY=536870912", // 256MB — stack(64MB) + data(12MB) + heap room + "-sMAXIMUM_MEMORY=2147483648", "-sFORCE_FILESYSTEM=1", + "-sNODERAWFS=0", "-sERROR_ON_UNDEFINED_SYMBOLS=0", + "-sEXIT_RUNTIME=0", + "-sINVOKE_RUN=0", + "-sGLOBAL_BASE=134217728", + "-sMODULARIZE=1", + "-sEXPORT_NAME=createNvim", + "-Wl,--unresolved-symbols=ignore-all", "--no-entry", - "-Wl,--export-all", - "-Wl,--no-gc-sections", + "-fexceptions", + "-flto", + "-sSTACK_OVERFLOW_CHECK=2", + "-sASSERTIONS=1", }); + emcc.addArg("-o"); const nvim_js = emcc.addOutputFileArg("nvim.js"); nvim_exe_step.dependOn(&b.addInstallFileWithDir(nvim_js, .bin, "nvim.js").step); From 4932862d9a87c7faaa0bedab64ace07f46e1f9a8 Mon Sep 17 00:00:00 2001 From: rawan10101 Date: Thu, 18 Jun 2026 17:35:38 +0300 Subject: [PATCH 2/4] build(wasm): add wasm_stubs for Emscripten build --- build.zig | 11 +++-- src/wasm_stubs.c | 126 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+), 5 deletions(-) create mode 100644 src/wasm_stubs.c diff --git a/build.zig b/build.zig index 4f97e019f8..99946f0a38 100644 --- a/build.zig +++ b/build.zig @@ -613,16 +613,15 @@ pub fn build(b: *std.Build) !void { "-sALLOW_MEMORY_GROWTH=1", "--profiling-funcs", "-sEXPORTED_FUNCTIONS=_main,_malloc,_free", - "-sEXPORTED_RUNTIME_METHODS=stringToUTF8,lengthBytesUTF8,setValue,getValue,UTF8ToString,callMain,ENV,FS", - "-sSTACK_SIZE=134217728", // 64MB — emscripten's own flag, overrides its 65536 - "-sINITIAL_MEMORY=536870912", // 256MB — stack(64MB) + data(12MB) + heap room + "-sEXPORTED_RUNTIME_METHODS=stringToUTF8,lengthBytesUTF8,setValue,getValue,UTF8ToString,callMain,ENV,FS,HEAPU8", + "-sSTACK_SIZE=8388608", + "-sINITIAL_MEMORY=268435456", "-sMAXIMUM_MEMORY=2147483648", "-sFORCE_FILESYSTEM=1", "-sNODERAWFS=0", - "-sERROR_ON_UNDEFINED_SYMBOLS=0", + "-sERROR_ON_UNDEFINED_SYMBOLS=1", "-sEXIT_RUNTIME=0", "-sINVOKE_RUN=0", - "-sGLOBAL_BASE=134217728", "-sMODULARIZE=1", "-sEXPORT_NAME=createNvim", "-Wl,--unresolved-symbols=ignore-all", @@ -631,6 +630,8 @@ pub fn build(b: *std.Build) !void { "-flto", "-sSTACK_OVERFLOW_CHECK=2", "-sASSERTIONS=1", + "-sASYNCIFY=1", + "-sASYNCIFY_STACK_SIZE=65536", }); emcc.addArg("-o"); diff --git a/src/wasm_stubs.c b/src/wasm_stubs.c new file mode 100644 index 0000000000..fce80d85fd --- /dev/null +++ b/src/wasm_stubs.c @@ -0,0 +1,126 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +uint64_t uv_get_free_memory(void) { return 8 * 1024 * 1024; } +uint64_t uv_get_total_memory(void) { return 256 * 1024 * 1024; } +uint64_t uv_get_available_memory(void) { return uv_get_free_memory(); } +uint64_t uv_get_constrained_memory(void) { return uv_get_total_memory(); } + +// Report an idle virtual system +void uv_loadavg(double avg[3]) +{ + avg[0] = 0.0; avg[1] = 0.0; avg[2] = 0.0; +} + +int uv_uptime(double* uptime) +{ + if (!uptime) return UV_EINVAL; + *uptime = emscripten_get_now() / 1000.0; + return 0; +} + +int uv_resident_set_memory(size_t* rss) +{ + if (!rss) return UV_EINVAL; + *rss = uv_get_total_memory() / 2; + return 0; +} + +int uv_exepath(char* buffer, size_t* size) +{ + if (!buffer || !size) return UV_EINVAL; + const char* exepath = "/nvim.wasm"; + size_t len = strlen(exepath); + if (*size <= len) { + *size = len + 1; + return UV_ENOBUFS; + } + memcpy(buffer, exepath, len); + buffer[len] = '\0'; + *size = len; + return 0; +} + +int uv__io_fork(uv_loop_t* loop) { + (void)loop; + return 0; +} + +int uv_cpu_info(uv_cpu_info_t** cpu_infos, int* count) +{ + if (!cpu_infos || !count) return UV_EINVAL; + *cpu_infos = (uv_cpu_info_t*) malloc(sizeof(uv_cpu_info_t)); + if (!*cpu_infos) return UV_ENOMEM; + + uv_cpu_info_t* cpu = *cpu_infos; + cpu->model = "WebAssembly Virtual CPU"; + cpu->speed = 0; + cpu->cpu_times.user = 0; cpu->cpu_times.nice = 0; cpu->cpu_times.sys = 0; + cpu->cpu_times.idle = 0; cpu->cpu_times.irq = 0; + *count = 1; + return 0; +} + +int uv_interface_addresses(uv_interface_address_t** addresses, int* count) +{ + if (!addresses || !count) return UV_EINVAL; + *addresses = (uv_interface_address_t*) malloc(sizeof(uv_interface_address_t)); + if (!*addresses) return UV_ENOMEM; + + uv_interface_address_t* addr = *addresses; + memset(addr, 0, sizeof(uv_interface_address_t)); + addr->name = strdup("lo"); + if (!addr->name) { free(addr); return UV_ENOMEM; } + addr->is_internal = 1; + uv_ip4_addr("127.0.0.1", 0, (struct sockaddr_in*)&addr->address); + uv_ip4_addr("255.0.0.0", 0, (struct sockaddr_in*)&addr->netmask); + *count = 1; + return 0; +} + +void uv__platform_invalidate_fd(uv_loop_t* loop, int fd) { (void)loop; (void)fd; } +int uv__io_check_fd(uv_loop_t* loop, void* w) { (void)loop; (void)w; return 0; } + +// Replaces the kernel poll() call +void uv__io_poll(uv_loop_t* loop, int timeout) { + if (timeout > 0) { + emscripten_sleep(timeout); + uv_update_time(loop); + emscripten_sleep(10); + uv_update_time(loop); + } else if (timeout == 0) { + emscripten_sleep(0); + } +} + +int uv__platform_loop_init(uv_loop_t* loop) +{ + if (!loop) return UV_EINVAL; + loop->backend_fd = -1; + return 0; +} + +void uv__platform_loop_delete(uv_loop_t* loop) { (void)loop; } + +// Fakes thread names when nvim tracks or debugs its internal processes +int pthread_getname_np(pthread_t thread, char* name, size_t len) +{ + (void)thread; + if (!name || len < 1) return EINVAL; + strncpy(name, "nvim-main", len - 1); + name[len - 1] = '\0'; + return 0; +} + +int pthread_setname_np(pthread_t thread, const char* name) { (void)thread; (void)name; return 0; } +int pthread_setschedparam(pthread_t thread, int policy, const struct sched_param* param) { (void)thread; (void)policy; (void)param; return 0; } +int sched_get_priority_max(int policy) { (void)policy; return 1; } +int sched_get_priority_min(int policy) { (void)policy; return 1; } From 7cbea86f4966d87534a3517b1e88e422c2b8eadd Mon Sep 17 00:00:00 2001 From: rawan10101 Date: Thu, 25 Jun 2026 14:37:40 +0300 Subject: [PATCH 3/4] build(wasm): add smoke test --- build.zig | 15 ++- contrib/test_wasm.html | 68 ++++++++++++ src/nvim/CMakeLists.txt | 1 + src/wasm_stubs.c | 229 +++++++++++++++++++++++++--------------- 4 files changed, 221 insertions(+), 92 deletions(-) create mode 100644 contrib/test_wasm.html diff --git a/build.zig b/build.zig index 99946f0a38..8421dfbc85 100644 --- a/build.zig +++ b/build.zig @@ -552,8 +552,6 @@ pub fn build(b: *std.Build) !void { var filtered_sources = try std.ArrayList([]u8).initCapacity(b.allocator, nvim_sources.items.len); for (nvim_sources.items) |s| { - // wasm_stubs.c defines libuv symbols & excludes it when real libuv is linked - if (!is_wasm and std.mem.eql(u8, s.name, "wasm_stubs.c")) continue; try filtered_sources.append(b.allocator, b.fmt("src/nvim/{s}", .{s.name})); } const src_paths = try b.allocator.alloc([]u8, filtered_sources.items.len + unit_test_sources.items.len); @@ -571,6 +569,7 @@ pub fn build(b: *std.Build) !void { if (is_windows) "-DUTF8PROC_STATIC" else "", if (use_unibilium) "-DHAVE_UNIBILIUM" else "", if (is_wasm) "-flto" else "", + if (is_wasm) "-DMAKE_LIB" else "", }; nvim_mod.addCSourceFiles(.{ .files = src_paths, .flags = &flags }); @@ -586,6 +585,12 @@ pub fn build(b: *std.Build) !void { "src/cjson/strbuf.c", }, .flags = &flags }); + if (is_wasm) { + nvim_mod.addCSourceFiles(.{ .files = &.{ + "src/wasm_stubs.c", + }, .flags = &flags }); + } + if (is_windows) { nvim_mod.addWin32ResourceFile(.{ .file = b.path("src/nvim/os/nvim.rc") }); } @@ -612,8 +617,9 @@ pub fn build(b: *std.Build) !void { b.fmt("--sysroot={s}", .{s}), "-sALLOW_MEMORY_GROWTH=1", "--profiling-funcs", - "-sEXPORTED_FUNCTIONS=_main,_malloc,_free", - "-sEXPORTED_RUNTIME_METHODS=stringToUTF8,lengthBytesUTF8,setValue,getValue,UTF8ToString,callMain,ENV,FS,HEAPU8", + "-sEXPORTED_FUNCTIONS=_nvim_main,_malloc,_free", + "-Wno-undefined", + "-sEXPORTED_RUNTIME_METHODS=stringToUTF8,lengthBytesUTF8,setValue,getValue,UTF8ToString,ENV,FS,HEAPU8", "-sSTACK_SIZE=8388608", "-sINITIAL_MEMORY=268435456", "-sMAXIMUM_MEMORY=2147483648", @@ -624,7 +630,6 @@ pub fn build(b: *std.Build) !void { "-sINVOKE_RUN=0", "-sMODULARIZE=1", "-sEXPORT_NAME=createNvim", - "-Wl,--unresolved-symbols=ignore-all", "--no-entry", "-fexceptions", "-flto", diff --git a/contrib/test_wasm.html b/contrib/test_wasm.html new file mode 100644 index 0000000000..a33964166d --- /dev/null +++ b/contrib/test_wasm.html @@ -0,0 +1,68 @@ + + + + +nvim.wasm test + + +

nvim.wasm load test

+ + + + + + + diff --git a/src/nvim/CMakeLists.txt b/src/nvim/CMakeLists.txt index 48f1657ec0..2e2d35c52e 100644 --- a/src/nvim/CMakeLists.txt +++ b/src/nvim/CMakeLists.txt @@ -443,6 +443,7 @@ list(APPEND UNCRUSTIFY_NVIM_SOURCES ${NVIM_SOURCES} ${NVIM_HEADERS}) list(APPEND UNCRUSTIFY_NVIM_SOURCES ${PROJECT_SOURCE_DIR}/src/tee/tee.c ${PROJECT_SOURCE_DIR}/src/xxd/xxd.c + ${PROJECT_SOURCE_DIR}/src/wasm_stubs.c ) foreach(sfile ${NVIM_SOURCES}) diff --git a/src/wasm_stubs.c b/src/wasm_stubs.c index fce80d85fd..94c121be96 100644 --- a/src/wasm_stubs.c +++ b/src/wasm_stubs.c @@ -1,126 +1,181 @@ +#include +#include #include #include +#include #include #include -#include -#include #include +#include #include -#include -#include -uint64_t uv_get_free_memory(void) { return 8 * 1024 * 1024; } -uint64_t uv_get_total_memory(void) { return 256 * 1024 * 1024; } -uint64_t uv_get_available_memory(void) { return uv_get_free_memory(); } -uint64_t uv_get_constrained_memory(void) { return uv_get_total_memory(); } +uint64_t uv_get_free_memory(void) +{ + return 8 * 1024 * 1024; +} +uint64_t uv_get_total_memory(void) +{ + return 256 * 1024 * 1024; +} +uint64_t uv_get_available_memory(void) +{ + return uv_get_free_memory(); +} +uint64_t uv_get_constrained_memory(void) +{ + return uv_get_total_memory(); +} // Report an idle virtual system void uv_loadavg(double avg[3]) { - avg[0] = 0.0; avg[1] = 0.0; avg[2] = 0.0; + avg[0] = 0.0; avg[1] = 0.0; avg[2] = 0.0; } -int uv_uptime(double* uptime) +int uv_uptime(double *uptime) { - if (!uptime) return UV_EINVAL; - *uptime = emscripten_get_now() / 1000.0; - return 0; + if (!uptime) { + return UV_EINVAL; + } + *uptime = emscripten_get_now() / 1000.0; + return 0; } -int uv_resident_set_memory(size_t* rss) +int uv_resident_set_memory(size_t *rss) { - if (!rss) return UV_EINVAL; - *rss = uv_get_total_memory() / 2; - return 0; + if (!rss) { + return UV_EINVAL; + } + *rss = uv_get_total_memory() / 2; + return 0; } -int uv_exepath(char* buffer, size_t* size) +int uv_exepath(char *buffer, size_t *size) { - if (!buffer || !size) return UV_EINVAL; - const char* exepath = "/nvim.wasm"; - size_t len = strlen(exepath); - if (*size <= len) { - *size = len + 1; - return UV_ENOBUFS; - } - memcpy(buffer, exepath, len); - buffer[len] = '\0'; - *size = len; - return 0; + if (!buffer || !size) { + return UV_EINVAL; + } + const char *exepath = "/nvim.wasm"; + size_t len = strlen(exepath); + if (*size <= len) { + *size = len + 1; + return UV_ENOBUFS; + } + memcpy(buffer, exepath, len); + buffer[len] = '\0'; + *size = len; + return 0; } -int uv__io_fork(uv_loop_t* loop) { - (void)loop; - return 0; -} - -int uv_cpu_info(uv_cpu_info_t** cpu_infos, int* count) +int uv__io_fork(uv_loop_t *loop) { - if (!cpu_infos || !count) return UV_EINVAL; - *cpu_infos = (uv_cpu_info_t*) malloc(sizeof(uv_cpu_info_t)); - if (!*cpu_infos) return UV_ENOMEM; - - uv_cpu_info_t* cpu = *cpu_infos; - cpu->model = "WebAssembly Virtual CPU"; - cpu->speed = 0; - cpu->cpu_times.user = 0; cpu->cpu_times.nice = 0; cpu->cpu_times.sys = 0; - cpu->cpu_times.idle = 0; cpu->cpu_times.irq = 0; - *count = 1; - return 0; + (void)loop; + return 0; } -int uv_interface_addresses(uv_interface_address_t** addresses, int* count) +int uv_cpu_info(uv_cpu_info_t * *cpu_infos, int *count) { - if (!addresses || !count) return UV_EINVAL; - *addresses = (uv_interface_address_t*) malloc(sizeof(uv_interface_address_t)); - if (!*addresses) return UV_ENOMEM; + if (!cpu_infos || !count) { + return UV_EINVAL; + } + *cpu_infos = (uv_cpu_info_t *)malloc(sizeof(uv_cpu_info_t)); + if (!*cpu_infos) { + return UV_ENOMEM; + } - uv_interface_address_t* addr = *addresses; - memset(addr, 0, sizeof(uv_interface_address_t)); - addr->name = strdup("lo"); - if (!addr->name) { free(addr); return UV_ENOMEM; } - addr->is_internal = 1; - uv_ip4_addr("127.0.0.1", 0, (struct sockaddr_in*)&addr->address); - uv_ip4_addr("255.0.0.0", 0, (struct sockaddr_in*)&addr->netmask); - *count = 1; - return 0; + uv_cpu_info_t *cpu = *cpu_infos; + cpu->model = "WebAssembly Virtual CPU"; + cpu->speed = 0; + cpu->cpu_times.user = 0; cpu->cpu_times.nice = 0; cpu->cpu_times.sys = 0; + cpu->cpu_times.idle = 0; cpu->cpu_times.irq = 0; + *count = 1; + return 0; } -void uv__platform_invalidate_fd(uv_loop_t* loop, int fd) { (void)loop; (void)fd; } -int uv__io_check_fd(uv_loop_t* loop, void* w) { (void)loop; (void)w; return 0; } +int uv_interface_addresses(uv_interface_address_t * *addresses, int *count) +{ + if (!addresses || !count) { + return UV_EINVAL; + } + *addresses = (uv_interface_address_t *)malloc(sizeof(uv_interface_address_t)); + if (!*addresses) { + return UV_ENOMEM; + } + + uv_interface_address_t *addr = *addresses; + memset(addr, 0, sizeof(uv_interface_address_t)); + addr->name = strdup("lo"); + if (!addr->name) { + free(addr); return UV_ENOMEM; + } + addr->is_internal = 1; + uv_ip4_addr("127.0.0.1", 0, (struct sockaddr_in *)&addr->address); + uv_ip4_addr("255.0.0.0", 0, (struct sockaddr_in *)&addr->netmask); + *count = 1; + return 0; +} + +void uv__platform_invalidate_fd(uv_loop_t *loop, int fd) +{ + (void)loop; (void)fd; +} +int uv__io_check_fd(uv_loop_t *loop, void *w) +{ + (void)loop; (void)w; return 0; +} // Replaces the kernel poll() call -void uv__io_poll(uv_loop_t* loop, int timeout) { - if (timeout > 0) { - emscripten_sleep(timeout); - uv_update_time(loop); - emscripten_sleep(10); - uv_update_time(loop); - } else if (timeout == 0) { - emscripten_sleep(0); - } -} - -int uv__platform_loop_init(uv_loop_t* loop) +void uv__io_poll(uv_loop_t *loop, int timeout) { - if (!loop) return UV_EINVAL; - loop->backend_fd = -1; - return 0; + if (timeout > 0) { + emscripten_sleep(timeout); + uv_update_time(loop); + emscripten_sleep(10); + uv_update_time(loop); + } else if (timeout == 0) { + emscripten_sleep(0); + } } -void uv__platform_loop_delete(uv_loop_t* loop) { (void)loop; } +int uv__platform_loop_init(uv_loop_t *loop) +{ + if (!loop) { + return UV_EINVAL; + } + loop->backend_fd = -1; + return 0; +} + +void uv__platform_loop_delete(uv_loop_t *loop) +{ + (void)loop; +} // Fakes thread names when nvim tracks or debugs its internal processes -int pthread_getname_np(pthread_t thread, char* name, size_t len) +int pthread_getname_np(pthread_t thread, char *name, size_t len) { - (void)thread; - if (!name || len < 1) return EINVAL; - strncpy(name, "nvim-main", len - 1); - name[len - 1] = '\0'; - return 0; + (void)thread; + if (!name || len < 1) { + return EINVAL; + } + strncpy(name, "nvim-main", len - 1); + name[len - 1] = '\0'; + return 0; } -int pthread_setname_np(pthread_t thread, const char* name) { (void)thread; (void)name; return 0; } -int pthread_setschedparam(pthread_t thread, int policy, const struct sched_param* param) { (void)thread; (void)policy; (void)param; return 0; } -int sched_get_priority_max(int policy) { (void)policy; return 1; } -int sched_get_priority_min(int policy) { (void)policy; return 1; } +int pthread_setname_np(pthread_t thread, const char *name) +{ + (void)thread; (void)name; return 0; +} +int pthread_setschedparam(pthread_t thread, int policy, const struct sched_param *param) +{ + (void)thread; (void)policy; (void)param; return 0; +} +int sched_get_priority_max(int policy) +{ + (void)policy; return 1; +} +int sched_get_priority_min(int policy) +{ + (void)policy; return 1; +} From be1b5bdfcac1d03c3ce4d200bc40ad222e9f2990 Mon Sep 17 00:00:00 2001 From: rawan10101 Date: Tue, 30 Jun 2026 14:34:50 +0300 Subject: [PATCH 4/4] build(deps): bump libuv to c1ccce24e1 and zlua to 2f0f668a9a --- build.zig.zon | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index ef78a5ec23..94ecabf92a 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -6,8 +6,8 @@ .dependencies = .{ .zlua = .{ - .url = "git+https://github.com/natecraddock/ziglua#1dcb9f2b7b466a1609cc11e746c2efc4f17685da", - .hash = "zlua-0.1.0-hGRpC22ABQA4y12Uz7QaHuCkKrpaa_66xidApjtFuKFg", + .url = "git+https://github.com/natecraddock/ziglua#2f0f668a9a7e7d4ab5d90853958837bae5bc3ca2", + .hash = "zlua-0.1.0-hGRpC8aUBQD4jNxDkqeKXAk_5HInJKze1SWVYbYkLuxO", }, .lpeg = .{ .url = "https://github.com/neovim/deps/raw/d495ee6f79e7962a53ad79670cb92488abe0b9b4/opt/lpeg-1.1.0.tar.gz", @@ -30,8 +30,8 @@ .lazy = true, }, .libuv = .{ - .url = "git+https://github.com/allyourcodebase/libuv.git#b465d9ae435fadb4c30c3abf1fd3177dc0c8a5c5", - .hash = "libuv-1.52.0-htqqv0xqAAAccfnkBz9DgFM9I9FSzP05_44lKS63OrZr", + .url = "git+https://github.com/allyourcodebase/libuv.git#c1ccce24e1077ae7780857c89fb4b3aeb9080315", + .hash = "libuv-1.52.0-htqqvzNqAAAB6cHIx-wIffe_mqMW5GOKjHeDBJYHGwls", .lazy = true, }, .utf8proc = .{ .path = "./deps/utf8proc/", .lazy = true },