diff --git a/build.zig b/build.zig index bd81b33..e63b572 100644 --- a/build.zig +++ b/build.zig @@ -23,6 +23,8 @@ fn findGccSanitizerLibDir(b: *std.Build) ?[]const u8 { pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); + const target_info = target.result; + const is_wasi = target_info.os.tag == .wasi; // Sanitizer options for runtime C code const sanitize = b.option(bool, "sanitize", "Enable ASan+UBSan for runtime C code") orelse false; @@ -37,18 +39,29 @@ pub fn build(b: *std.Build) void { const build_options = b.addOptions(); build_options.addOption([]const u8, "version", version); - // Main compiler executable - const exe = b.addExecutable(.{ - .name = "run", - .root_module = b.createModule(.{ - .root_source_file = b.path("src/main.zig"), - .target = target, - .optimize = optimize, - .link_libc = true, - }), - }); - exe.root_module.addOptions("build_options", build_options); - b.installArtifact(exe); + if (!is_wasi) { + // Main compiler executable + const exe = b.addExecutable(.{ + .name = "run", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + .link_libc = true, + }), + }); + exe.root_module.addOptions("build_options", build_options); + b.installArtifact(exe); + + // Run command: `zig build run -- ` + const run_cmd = b.addRunArtifact(exe); + run_cmd.step.dependOn(b.getInstallStep()); + if (b.args) |args| { + run_cmd.addArgs(args); + } + const run_step = b.step("run", "Compile and run the Run compiler"); + run_step.dependOn(&run_cmd.step); + } // libxev dependency (cross-platform event loop for async I/O) const libxev_dep = b.dependency("libxev", .{ @@ -77,7 +90,7 @@ pub fn build(b: *std.Build) void { "src/runtime/run_string.c", "src/runtime/run_slice.c", "src/runtime/run_fmt.c", - "src/runtime/run_scheduler.c", + if (is_wasi) "src/runtime/run_scheduler_wasi.c" else "src/runtime/run_scheduler.c", "src/runtime/run_chan.c", "src/runtime/run_vmem.c", "src/runtime/run_map.c", @@ -137,7 +150,6 @@ pub fn build(b: *std.Build) void { }); // Add platform-specific assembly for context switching - const target_info = target.result; if (target_info.cpu.arch == .x86_64) { runtime_lib.root_module.addAssemblyFile(b.path("src/runtime/run_context_amd64.S")); runtime_lib.root_module.addAssemblyFile(b.path("src/runtime/run_async_preempt_amd64.S")); @@ -171,7 +183,7 @@ pub fn build(b: *std.Build) void { } else null; if (xev_bridge_obj) |obj| { runtime_lib.root_module.addObject(obj); - if (target_info.os.tag == .windows) { + if (target_info.os.tag == .windows or is_wasi) { const xev_bridge_lib = b.addLibrary(.{ .name = "runxev", .linkage = .static, @@ -197,7 +209,9 @@ pub fn build(b: *std.Build) void { b.getInstallStep().dependOn(&install_xev_lib.step); } } - runtime_lib.root_module.linkSystemLibrary("pthread", .{}); + if (!is_wasi) { + runtime_lib.root_module.linkSystemLibrary("pthread", .{}); + } // Link libunwind for stack traces with DWARF unwinding. // On macOS, libunwind is part of the system (linked automatically). // On Linux, it requires the libunwind-dev package. @@ -216,15 +230,6 @@ pub fn build(b: *std.Build) void { .include_extensions = &.{".h"}, }); - // Run command: `zig build run -- ` - const run_cmd = b.addRunArtifact(exe); - run_cmd.step.dependOn(b.getInstallStep()); - if (b.args) |args| { - run_cmd.addArgs(args); - } - const run_step = b.step("run", "Compile and run the Run compiler"); - run_step.dependOn(&run_cmd.step); - // Tests (via root.zig which re-exports all modules) const tests = b.addTest(.{ .root_module = b.createModule(.{ @@ -238,175 +243,206 @@ pub fn build(b: *std.Build) void { const test_step = b.step("test", "Run unit tests"); test_step.dependOn(&run_tests.step); - // Runtime C tests - const runtime_test_exe = b.addExecutable(.{ - .name = "runtime-tests", - .root_module = b.createModule(.{ - .target = target, - .optimize = optimize, - .link_libc = true, - }), - }); - - const runtime_test_sources = .{ - "src/runtime/tests/test_main.c", - "src/runtime/tests/test_vmem.c", - "src/runtime/tests/test_scheduler.c", - "src/runtime/tests/test_chan.c", - "src/runtime/tests/test_map.c", - "src/runtime/tests/test_fmt.c", - "src/runtime/tests/test_simd.c", - "src/runtime/tests/test_numa.c", - "src/runtime/tests/test_runtime_api.c", - "src/runtime/tests/test_debug_api.c", - "src/runtime/tests/test_poller.c", - "src/runtime/tests/test_stress.c", - }; - inline for (runtime_test_sources) |src| { - runtime_test_exe.root_module.addCSourceFile(.{ - .file = b.path(src), - .flags = sanitizer_flags, + if (!is_wasi) { + // Runtime C tests + const runtime_test_exe = b.addExecutable(.{ + .name = "runtime-tests", + .root_module = b.createModule(.{ + .target = target, + .optimize = optimize, + .link_libc = true, + }), }); - } - inline for (runtime_c_sources) |src| { + + const runtime_test_sources = .{ + "src/runtime/tests/test_main.c", + "src/runtime/tests/test_vmem.c", + "src/runtime/tests/test_scheduler.c", + "src/runtime/tests/test_chan.c", + "src/runtime/tests/test_map.c", + "src/runtime/tests/test_fmt.c", + "src/runtime/tests/test_simd.c", + "src/runtime/tests/test_numa.c", + "src/runtime/tests/test_runtime_api.c", + "src/runtime/tests/test_debug_api.c", + "src/runtime/tests/test_poller.c", + "src/runtime/tests/test_stress.c", + }; + inline for (runtime_test_sources) |src| { + runtime_test_exe.root_module.addCSourceFile(.{ + .file = b.path(src), + .flags = sanitizer_flags, + }); + } + inline for (runtime_c_sources) |src| { + runtime_test_exe.root_module.addCSourceFile(.{ + .file = b.path(src), + .flags = sanitizer_flags, + }); + } + // Add selected poller implementation for tests runtime_test_exe.root_module.addCSourceFile(.{ - .file = b.path(src), + .file = b.path(poller_source), .flags = sanitizer_flags, }); - } - // Add selected poller implementation for tests - runtime_test_exe.root_module.addCSourceFile(.{ - .file = b.path(poller_source), - .flags = sanitizer_flags, - }); - // Add assembly for runtime tests too - if (target_info.cpu.arch == .x86_64) { - runtime_test_exe.root_module.addAssemblyFile(b.path("src/runtime/run_context_amd64.S")); - runtime_test_exe.root_module.addAssemblyFile(b.path("src/runtime/run_async_preempt_amd64.S")); - } else if (target_info.cpu.arch == .aarch64) { - runtime_test_exe.root_module.addAssemblyFile(b.path("src/runtime/run_context_arm64.S")); - runtime_test_exe.root_module.addAssemblyFile(b.path("src/runtime/run_async_preempt_arm64.S")); - } + // Add assembly for runtime tests too + if (target_info.cpu.arch == .x86_64) { + runtime_test_exe.root_module.addAssemblyFile(b.path("src/runtime/run_context_amd64.S")); + runtime_test_exe.root_module.addAssemblyFile(b.path("src/runtime/run_async_preempt_amd64.S")); + } else if (target_info.cpu.arch == .aarch64) { + runtime_test_exe.root_module.addAssemblyFile(b.path("src/runtime/run_context_arm64.S")); + runtime_test_exe.root_module.addAssemblyFile(b.path("src/runtime/run_async_preempt_arm64.S")); + } - runtime_test_exe.root_module.addIncludePath(b.path("src/runtime")); - runtime_test_exe.root_module.addIncludePath(b.path("src/runtime/tests")); - // Reuse the same object file produced for runtime_lib (see note above). - if (xev_bridge_obj) |obj| { - runtime_test_exe.root_module.addObject(obj); - } - runtime_test_exe.root_module.linkSystemLibrary("pthread", .{}); - // Link libunwind for stack trace tests (matches runtime_lib linking). - if (target_info.os.tag == .linux) { - runtime_test_exe.root_module.linkSystemLibrary("unwind", .{}); - // On Linux, dladdr only resolves symbols exposed through the dynamic - // symbol table. Without --export-dynamic, stack-trace tests that match - // on function names (e.g. strstr(trace, "test_runtime_stack")) will - // fail because the static test functions aren't visible to dladdr. - runtime_test_exe.rdynamic = true; - } + runtime_test_exe.root_module.addIncludePath(b.path("src/runtime")); + runtime_test_exe.root_module.addIncludePath(b.path("src/runtime/tests")); + // Reuse the same object file produced for runtime_lib (see note above). + if (xev_bridge_obj) |obj| { + runtime_test_exe.root_module.addObject(obj); + } + runtime_test_exe.root_module.linkSystemLibrary("pthread", .{}); + // Link libunwind for stack trace tests (matches runtime_lib linking). + if (target_info.os.tag == .linux) { + runtime_test_exe.root_module.linkSystemLibrary("unwind", .{}); + // On Linux, dladdr only resolves symbols exposed through the dynamic + // symbol table. Without --export-dynamic, stack-trace tests that match + // on function names (e.g. strstr(trace, "test_runtime_stack")) will + // fail because the static test functions aren't visible to dladdr. + runtime_test_exe.rdynamic = true; + } - // Link sanitizer runtime libraries for the test executable. - // On Ubuntu/Debian, these live in GCC's versioned lib directory - // (e.g. /usr/lib/gcc/x86_64-linux-gnu/13/) which isn't in the - // standard search path. Probe for it at configure time. - if (sanitize or tsan) { - if (findGccSanitizerLibDir(b)) |gcc_dir| { - runtime_test_exe.root_module.addLibraryPath(.{ .cwd_relative = gcc_dir }); + // Link sanitizer runtime libraries for the test executable. + // On Ubuntu/Debian, these live in GCC's versioned lib directory + // (e.g. /usr/lib/gcc/x86_64-linux-gnu/13/) which isn't in the + // standard search path. Probe for it at configure time. + if (sanitize or tsan) { + if (findGccSanitizerLibDir(b)) |gcc_dir| { + runtime_test_exe.root_module.addLibraryPath(.{ .cwd_relative = gcc_dir }); + } } - } - if (sanitize) { - runtime_test_exe.root_module.linkSystemLibrary("asan", .{}); - runtime_test_exe.root_module.linkSystemLibrary("ubsan", .{}); - } - if (tsan) { - runtime_test_exe.root_module.linkSystemLibrary("tsan", .{}); - } - b.installArtifact(runtime_test_exe); - - const run_runtime_tests = b.addRunArtifact(runtime_test_exe); - run_runtime_tests.step.dependOn(&runtime_test_exe.step); - if (target_info.os.tag == .macos) { - const runtime_tests_dsym = b.addSystemCommand(&.{"dsymutil"}); - runtime_tests_dsym.addArtifactArg(runtime_test_exe); - run_runtime_tests.step.dependOn(&runtime_tests_dsym.step); - } - const runtime_test_step = b.step("test-runtime", "Run runtime C tests"); - runtime_test_step.dependOn(&run_runtime_tests.step); + if (sanitize) { + runtime_test_exe.root_module.linkSystemLibrary("asan", .{}); + runtime_test_exe.root_module.linkSystemLibrary("ubsan", .{}); + } + if (tsan) { + runtime_test_exe.root_module.linkSystemLibrary("tsan", .{}); + } + b.installArtifact(runtime_test_exe); - // Runtime benchmarks - const runtime_bench_exe = b.addExecutable(.{ - .name = "runtime-bench", - .root_module = b.createModule(.{ - .target = target, - .optimize = .ReleaseFast, - .link_libc = true, - }), - }); - const runtime_bench_sources = .{ - "benchmarks/runtime/bench_main.c", - "benchmarks/runtime/bench_spawn.c", - "benchmarks/runtime/bench_context_switch.c", - "benchmarks/runtime/bench_channel.c", - "benchmarks/runtime/bench_steal.c", - "benchmarks/runtime/bench_poll.c", - "benchmarks/runtime/bench_scheduler.c", - }; - inline for (runtime_bench_sources) |src| { - runtime_bench_exe.root_module.addCSourceFile(.{ - .file = b.path(src), - .flags = &.{"-D_GNU_SOURCE"}, + const run_runtime_tests = b.addRunArtifact(runtime_test_exe); + run_runtime_tests.step.dependOn(&runtime_test_exe.step); + if (target_info.os.tag == .macos) { + const runtime_tests_dsym = b.addSystemCommand(&.{"dsymutil"}); + runtime_tests_dsym.addArtifactArg(runtime_test_exe); + run_runtime_tests.step.dependOn(&runtime_tests_dsym.step); + } + const runtime_test_step = b.step("test-runtime", "Run runtime C tests"); + runtime_test_step.dependOn(&run_runtime_tests.step); + + // Runtime benchmarks + const runtime_bench_exe = b.addExecutable(.{ + .name = "runtime-bench", + .root_module = b.createModule(.{ + .target = target, + .optimize = .ReleaseFast, + .link_libc = true, + }), }); - } - inline for (runtime_c_sources) |src| { + const runtime_bench_sources = .{ + "benchmarks/runtime/bench_main.c", + "benchmarks/runtime/bench_spawn.c", + "benchmarks/runtime/bench_context_switch.c", + "benchmarks/runtime/bench_channel.c", + "benchmarks/runtime/bench_steal.c", + "benchmarks/runtime/bench_poll.c", + "benchmarks/runtime/bench_scheduler.c", + }; + inline for (runtime_bench_sources) |src| { + runtime_bench_exe.root_module.addCSourceFile(.{ + .file = b.path(src), + .flags = &.{"-D_GNU_SOURCE"}, + }); + } + inline for (runtime_c_sources) |src| { + runtime_bench_exe.root_module.addCSourceFile(.{ + .file = b.path(src), + .flags = &.{"-D_GNU_SOURCE"}, + }); + } runtime_bench_exe.root_module.addCSourceFile(.{ - .file = b.path(src), + .file = b.path(poller_source), .flags = &.{"-D_GNU_SOURCE"}, }); - } - runtime_bench_exe.root_module.addCSourceFile(.{ - .file = b.path(poller_source), - .flags = &.{"-D_GNU_SOURCE"}, - }); - // Note: run_main.c is NOT included in benchmarks — bench_main.c provides main() - if (target_info.cpu.arch == .x86_64) { - if (target_info.os.tag == .windows) { - runtime_bench_exe.root_module.addAssemblyFile(b.path("src/runtime/run_context_win64.S")); - } else { - runtime_bench_exe.root_module.addAssemblyFile(b.path("src/runtime/run_context_amd64.S")); - runtime_bench_exe.root_module.addAssemblyFile(b.path("src/runtime/run_async_preempt_amd64.S")); + // Note: run_main.c is NOT included in benchmarks — bench_main.c provides main() + if (target_info.cpu.arch == .x86_64) { + if (target_info.os.tag == .windows) { + runtime_bench_exe.root_module.addAssemblyFile(b.path("src/runtime/run_context_win64.S")); + } else { + runtime_bench_exe.root_module.addAssemblyFile(b.path("src/runtime/run_context_amd64.S")); + runtime_bench_exe.root_module.addAssemblyFile(b.path("src/runtime/run_async_preempt_amd64.S")); + } + } else if (target_info.cpu.arch == .aarch64) { + runtime_bench_exe.root_module.addAssemblyFile(b.path("src/runtime/run_context_arm64.S")); + runtime_bench_exe.root_module.addAssemblyFile(b.path("src/runtime/run_async_preempt_arm64.S")); } - } else if (target_info.cpu.arch == .aarch64) { - runtime_bench_exe.root_module.addAssemblyFile(b.path("src/runtime/run_context_arm64.S")); - runtime_bench_exe.root_module.addAssemblyFile(b.path("src/runtime/run_async_preempt_arm64.S")); - } - runtime_bench_exe.root_module.addIncludePath(b.path("src/runtime")); - runtime_bench_exe.root_module.addIncludePath(b.path("benchmarks/runtime")); - if (!legacy_poller) { - const xev_bench_bridge = b.addLibrary(.{ - .name = "runxev-bench", - .linkage = .static, + runtime_bench_exe.root_module.addIncludePath(b.path("src/runtime")); + runtime_bench_exe.root_module.addIncludePath(b.path("benchmarks/runtime")); + if (!legacy_poller) { + const xev_bench_bridge = b.addLibrary(.{ + .name = "runxev-bench", + .linkage = .static, + .root_module = b.createModule(.{ + .root_source_file = b.path("src/runtime/run_xev_bridge.zig"), + .target = target, + .optimize = .ReleaseFast, + .link_libc = true, + }), + }); + xev_bench_bridge.root_module.addImport("xev", libxev_dep.module("xev")); + runtime_bench_exe.root_module.linkLibrary(xev_bench_bridge); + } + runtime_bench_exe.root_module.linkSystemLibrary("pthread", .{}); + // Link libunwind for stack trace support (matches runtime_lib linking). + if (target_info.os.tag == .linux) { + runtime_bench_exe.root_module.linkSystemLibrary("unwind", .{}); + } + b.installArtifact(runtime_bench_exe); + + const run_runtime_bench = b.addRunArtifact(runtime_bench_exe); + run_runtime_bench.step.dependOn(&runtime_bench_exe.step); + const runtime_bench_step = b.step("bench-runtime", "Run runtime benchmarks"); + runtime_bench_step.dependOn(&run_runtime_bench.step); + } else { + _ = b.step("test-runtime", "Runtime C tests are not available for WASI"); + _ = b.step("bench-runtime", "Runtime benchmarks are not available for WASI"); + + const wasi_smoke_exe = b.addExecutable(.{ + .name = "runtime-wasi-smoke", .root_module = b.createModule(.{ - .root_source_file = b.path("src/runtime/run_xev_bridge.zig"), .target = target, - .optimize = .ReleaseFast, + .optimize = optimize, .link_libc = true, }), }); - xev_bench_bridge.root_module.addImport("xev", libxev_dep.module("xev")); - runtime_bench_exe.root_module.linkLibrary(xev_bench_bridge); - } - runtime_bench_exe.root_module.linkSystemLibrary("pthread", .{}); - // Link libunwind for stack trace support (matches runtime_lib linking). - if (target_info.os.tag == .linux) { - runtime_bench_exe.root_module.linkSystemLibrary("unwind", .{}); - } - b.installArtifact(runtime_bench_exe); + wasi_smoke_exe.root_module.addCSourceFile(.{ + .file = b.path("src/runtime/tests/wasi_smoke.c"), + .flags = sanitizer_flags, + }); + wasi_smoke_exe.root_module.addIncludePath(b.path("src/runtime")); + wasi_smoke_exe.root_module.linkLibrary(runtime_lib); + b.installArtifact(wasi_smoke_exe); - const run_runtime_bench = b.addRunArtifact(runtime_bench_exe); - run_runtime_bench.step.dependOn(&runtime_bench_exe.step); - const runtime_bench_step = b.step("bench-runtime", "Run runtime benchmarks"); - runtime_bench_step.dependOn(&run_runtime_bench.step); + const wasi_smoke_step = b.step("test-wasi-runtime", "Run WASI runtime smoke test with wasmtime"); + if (b.findProgram(&.{"wasmtime"}, &.{})) |wasmtime| { + const run_wasi_smoke = b.addSystemCommand(&.{wasmtime}); + run_wasi_smoke.addArtifactArg(wasi_smoke_exe); + wasi_smoke_step.dependOn(&run_wasi_smoke.step); + } else |_| { + const missing_wasmtime = b.addFail("wasmtime not found; install wasmtime to run test-wasi-runtime"); + wasi_smoke_step.dependOn(&missing_wasmtime.step); + } + } // Example build tests const examples_test_exe = b.addExecutable(.{ diff --git a/src/runtime/run_debug_api.c b/src/runtime/run_debug_api.c index 2c33b0f..8123d11 100644 --- a/src/runtime/run_debug_api.c +++ b/src/runtime/run_debug_api.c @@ -5,7 +5,6 @@ #include "run_stacktrace.h" #include "run_string.h" -#include #include #include #include diff --git a/src/runtime/run_numa.c b/src/runtime/run_numa.c index d4da97a..89518f1 100644 --- a/src/runtime/run_numa.c +++ b/src/runtime/run_numa.c @@ -400,6 +400,47 @@ int run_numa_set_memory_policy(uint32_t policy, uint32_t node_id) { return 0; /* Windows does not expose per-thread memory policy */ } +#elif defined(__wasi__) + +void run_numa_init(void) { + if (topology.initialized) + return; + + memset(&topology, 0, sizeof(topology)); + topology.node_count = 1; + topology.total_cpus = 1; + topology.nodes[0].node_id = 0; + topology.nodes[0].cpu_count = 1; + topology.nodes[0].cpu_ids[0] = 0; + topology.cpu_to_node[0] = 0; + topology.distances[0][0] = 10; + topology.initialized = true; +} + +uint32_t run_numa_current_node(void) { + return 0; +} + +void *run_numa_alloc_on_node(size_t size, uint32_t node_id) { + (void)node_id; + return run_vmem_alloc(size); +} + +void run_numa_free(void *ptr, size_t size) { + run_vmem_free(ptr, size); +} + +int run_numa_bind_thread(uint32_t node_id) { + (void)node_id; + return 0; +} + +int run_numa_set_memory_policy(uint32_t policy, uint32_t node_id) { + (void)policy; + (void)node_id; + return 0; +} + #endif /* ======================================================================== diff --git a/src/runtime/run_scheduler.h b/src/runtime/run_scheduler.h index d3c4a6b..a81999e 100644 --- a/src/runtime/run_scheduler.h +++ b/src/runtime/run_scheduler.h @@ -13,7 +13,11 @@ typedef struct run_m run_m_t; typedef struct run_p run_p_t; /* ---------- Context (platform-specific) ---------- */ -#if defined(__aarch64__) || defined(__arm64__) +#if defined(__wasi__) +typedef struct { + void *unused; +} run_context_t; +#elif defined(__aarch64__) || defined(__arm64__) typedef struct { void *sp; void *lr; @@ -74,8 +78,10 @@ typedef struct { } run_context_t; #endif +#if !defined(__wasi__) extern void run_context_switch(run_context_t *from, run_context_t *to); extern void run_context_init(run_context_t *ctx, void *stack_top, void (*entry)(void *), void *arg); +#endif /* ---------- G — Green Thread ---------- */ typedef enum { G_IDLE, G_RUNNABLE, G_RUNNING, G_WAITING, G_DEAD } run_g_status_t; diff --git a/src/runtime/run_scheduler_wasi.c b/src/runtime/run_scheduler_wasi.c new file mode 100644 index 0000000..aad5f51 --- /dev/null +++ b/src/runtime/run_scheduler_wasi.c @@ -0,0 +1,285 @@ +#include "run_numa.h" +#include "run_poller.h" +#include "run_scheduler.h" + +#include +#include +#include +#include + +/* WASI MVP has no native threads, signals, or stack-switching ABI. This file + * provides a single-P cooperative scheduler that can run queued tasks to + * completion and keeps the public runtime API linkable for WASI smoke tests. */ + +static run_p_t p0; +static run_m_t m0; +static run_g_queue_t global_queue; +static _Atomic uint64_t next_g_id = 1; +static _Atomic int64_t live_g_count = 0; +static _Atomic int64_t goroutine_count = 0; +static bool scheduler_initialized = false; +static run_metrics_t scheduler_metrics = {0}; + +run_g_t *run_current_g(void) { + return m0.current_g; +} + +run_m_t *run_current_m(void) { + return scheduler_initialized ? &m0 : NULL; +} + +void run_g_queue_init(run_g_queue_t *q) { + q->head = NULL; + q->tail = NULL; + q->len = 0; +} + +void run_g_queue_push(run_g_queue_t *q, run_g_t *g) { + g->sched_next = NULL; + if (q->tail) { + q->tail->sched_next = g; + } else { + q->head = g; + } + q->tail = g; + q->len++; +} + +run_g_t *run_g_queue_pop(run_g_queue_t *q) { + run_g_t *g = q->head; + if (!g) + return NULL; + q->head = g->sched_next; + if (!q->head) + q->tail = NULL; + g->sched_next = NULL; + q->len--; + return g; +} + +bool run_g_queue_remove(run_g_queue_t *q, run_g_t *g) { + if (!q->head) + return false; + if (q->head == g) { + q->head = g->sched_next; + if (!q->head) + q->tail = NULL; + g->sched_next = NULL; + q->len--; + return true; + } + + run_g_t *prev = q->head; + while (prev->sched_next) { + if (prev->sched_next == g) { + prev->sched_next = g->sched_next; + if (q->tail == g) + q->tail = prev; + g->sched_next = NULL; + q->len--; + return true; + } + prev = prev->sched_next; + } + return false; +} + +void run_local_queue_init(run_local_queue_t *q) { + atomic_store_explicit(&q->head, 0, memory_order_relaxed); + atomic_store_explicit(&q->tail, 0, memory_order_relaxed); + memset((void *)q->buf, 0, sizeof(q->buf)); +} + +bool run_local_queue_push(run_local_queue_t *q, run_g_t *g) { + uint32_t tail = atomic_load_explicit(&q->tail, memory_order_relaxed); + uint32_t head = atomic_load_explicit(&q->head, memory_order_acquire); + if (tail - head >= RUN_LOCAL_QUEUE_SIZE) + return false; + q->buf[tail % RUN_LOCAL_QUEUE_SIZE] = g; + atomic_store_explicit(&q->tail, tail + 1, memory_order_release); + return true; +} + +run_g_t *run_local_queue_pop(run_local_queue_t *q) { + uint32_t head = atomic_load_explicit(&q->head, memory_order_relaxed); + uint32_t tail = atomic_load_explicit(&q->tail, memory_order_acquire); + if (head == tail) + return NULL; + run_g_t *g = q->buf[head % RUN_LOCAL_QUEUE_SIZE]; + atomic_store_explicit(&q->head, head + 1, memory_order_release); + return g; +} + +run_g_t *run_local_queue_steal(run_local_queue_t *src, run_local_queue_t *dst) { + (void)dst; + return run_local_queue_pop(src); +} + +uint32_t run_local_queue_len(run_local_queue_t *q) { + uint32_t head = atomic_load_explicit(&q->head, memory_order_relaxed); + uint32_t tail = atomic_load_explicit(&q->tail, memory_order_relaxed); + return tail >= head ? tail - head : 0; +} + +void run_scheduler_init(void) { + if (scheduler_initialized) + return; + + memset(&p0, 0, sizeof(p0)); + memset(&m0, 0, sizeof(m0)); + run_g_queue_init(&global_queue); + run_local_queue_init(&p0.local_queue); + + p0.id = 0; + p0.status = P_RUNNING; + p0.bound_m = &m0; + p0.numa_node = 0; + + m0.id = 1; + m0.current_p = &p0; + + run_numa_init(); + run_poller_init(); + scheduler_initialized = true; +} + +static run_g_t *run_g_new(void (*fn)(void *), void *arg, int32_t preferred_node) { + run_g_t *g = (run_g_t *)calloc(1, sizeof(run_g_t)); + if (!g) { + fprintf(stderr, "run: failed to allocate goroutine\n"); + abort(); + } + + g->id = atomic_fetch_add_explicit(&next_g_id, 1, memory_order_relaxed); + g->status = G_RUNNABLE; + g->entry_fn = fn; + g->entry_arg = arg; + g->preferred_node = preferred_node; + g->preempt_safe = true; + return g; +} + +void run_spawn(void (*fn)(void *), void *arg) { + run_spawn_on_node(fn, arg, -1); +} + +void run_spawn_on_node(void (*fn)(void *), void *arg, int32_t node_id) { + if (!scheduler_initialized) + run_scheduler_init(); + run_g_t *g = run_g_new(fn, arg, node_id); + atomic_fetch_add_explicit(&live_g_count, 1, memory_order_relaxed); + atomic_fetch_add_explicit(&goroutine_count, 1, memory_order_relaxed); + atomic_fetch_add_explicit(&scheduler_metrics.spawn_count, 1, memory_order_relaxed); + run_g_queue_push(&global_queue, g); +} + +static void run_g_finish(run_g_t *g) { + g->status = G_DEAD; + atomic_fetch_sub_explicit(&live_g_count, 1, memory_order_relaxed); + atomic_fetch_sub_explicit(&goroutine_count, 1, memory_order_relaxed); + atomic_fetch_add_explicit(&scheduler_metrics.complete_count, 1, memory_order_relaxed); + free(g); +} + +void run_scheduler_run(void) { + if (!scheduler_initialized) + run_scheduler_init(); + + while (atomic_load_explicit(&live_g_count, memory_order_relaxed) > 0) { + run_g_t *g = run_g_queue_pop(&global_queue); + if (!g) { + if (run_poller_has_waiters()) { + run_poller_poll_blocking(-1); + continue; + } + break; + } + + m0.current_g = g; + g->status = G_RUNNING; + atomic_fetch_add_explicit(&scheduler_metrics.context_switches, 1, memory_order_relaxed); + g->entry_fn(g->entry_arg); + m0.current_g = NULL; + run_g_finish(g); + } +} + +void run_yield(void) { + atomic_fetch_add_explicit(&scheduler_metrics.context_switches, 1, memory_order_relaxed); +} + +void run_schedule(void) { + run_yield(); +} + +void run_g_exit(void) { + run_g_t *g = run_current_g(); + if (g) + g->status = G_DEAD; +} + +void run_g_ready(run_g_t *g) { + if (!g || g->status == G_DEAD) + return; + g->status = G_RUNNABLE; + run_g_queue_push(&global_queue, g); +} + +void run_numa_pin(uint32_t node_id) { + (void)node_id; +} + +void run_preemption_start(void) {} +void run_preemption_stop(void) {} +void run_signal_preemption_start(void) {} +void run_signal_preemption_stop(void) {} +void run_entersyscall(void) {} +void run_exitsyscall(void) {} +void run_wake_m(void) {} + +void run_global_queue_push(run_g_t *g) { + run_g_queue_push(&global_queue, g); +} + +run_g_t *run_global_queue_pop(void) { + return run_g_queue_pop(&global_queue); +} + +uint32_t run_global_queue_len(void) { + return global_queue.len; +} + +size_t run_stack_max_size(void) { + return 0; +} + +void run_stack_growth_init(void) {} + +void run_morestack(void) { + fprintf(stderr, "run: stack growth is unavailable on WASI\n"); + abort(); +} + +void run_debug_dump_goroutines(char *buf, size_t buf_size) { + if (!buf || buf_size == 0) + return; + snprintf(buf, buf_size, "wasi scheduler: %lld live goroutine(s)\n", + (long long)atomic_load_explicit(&live_g_count, memory_order_relaxed)); +} + +int64_t run_scheduler_goroutine_count(void) { + return atomic_load_explicit(&goroutine_count, memory_order_relaxed); +} + +uint32_t run_scheduler_get_maxprocs(void) { + return 1; +} + +uint32_t run_scheduler_set_maxprocs(uint32_t n) { + (void)n; + return 1; +} + +run_metrics_t run_runtime_metrics(void) { + return scheduler_metrics; +} diff --git a/src/runtime/run_vmem.c b/src/runtime/run_vmem.c index db7ecc5..848b18f 100644 --- a/src/runtime/run_vmem.c +++ b/src/runtime/run_vmem.c @@ -79,4 +79,34 @@ size_t run_vmem_page_size(void) { return (size_t)si.dwPageSize; } +#elif defined(__wasi__) + +void *run_vmem_alloc(size_t size) { + return malloc(size); +} + +void *run_vmem_reserve(size_t size) { + return malloc(size); +} + +void run_vmem_free(void *ptr, size_t size) { + (void)size; + free(ptr); +} + +void run_vmem_protect(void *ptr, size_t size, int prot) { + (void)ptr; + (void)size; + (void)prot; +} + +void run_vmem_release(void *ptr, size_t size) { + (void)ptr; + (void)size; +} + +size_t run_vmem_page_size(void) { + return 64 * 1024; +} + #endif diff --git a/src/runtime/run_xev.c b/src/runtime/run_xev.c index ccac6a9..2a6acb2 100644 --- a/src/runtime/run_xev.c +++ b/src/runtime/run_xev.c @@ -2,12 +2,21 @@ #include "run_poller.h" -#include #include #include #include #include +#if defined(__wasi__) +#define RUN_XEV_LOCK() ((void)0) +#define RUN_XEV_UNLOCK() ((void)0) +#else +#include +static pthread_mutex_t xev_lock = PTHREAD_MUTEX_INITIALIZER; +#define RUN_XEV_LOCK() pthread_mutex_lock(&xev_lock) +#define RUN_XEV_UNLOCK() pthread_mutex_unlock(&xev_lock) +#endif + /* ======================================================================== * libxev-backed Network Poller * @@ -25,7 +34,6 @@ /* ---------- Internal state ---------- */ -static pthread_mutex_t xev_lock = PTHREAD_MUTEX_INITIALIZER; static volatile int32_t woken_count = 0; /* ---------- Readiness callback ---------- */ @@ -64,7 +72,7 @@ int run_poll_open(run_poll_desc_t *pd) { } void run_poll_close(run_poll_desc_t *pd) { - pthread_mutex_lock(&xev_lock); + RUN_XEV_LOCK(); pd->closing = true; /* Wake any Gs still waiting */ @@ -80,7 +88,7 @@ void run_poll_close(run_poll_desc_t *pd) { } run_xev_close_fd(pd->fd); - pthread_mutex_unlock(&xev_lock); + RUN_XEV_UNLOCK(); } void run_poll_wait(run_poll_desc_t *pd, run_poll_event_t events) { @@ -88,7 +96,7 @@ void run_poll_wait(run_poll_desc_t *pd, run_poll_event_t events) { if (!g) return; - pthread_mutex_lock(&xev_lock); + RUN_XEV_LOCK(); if (events & RUN_POLL_READ) { pd->read_g = g; @@ -101,28 +109,28 @@ void run_poll_wait(run_poll_desc_t *pd, run_poll_event_t events) { /* Park the G */ g->status = G_WAITING; - pthread_mutex_unlock(&xev_lock); + RUN_XEV_UNLOCK(); run_schedule(); - pthread_mutex_lock(&xev_lock); + RUN_XEV_LOCK(); if ((events & RUN_POLL_READ) && pd->read_g == g) { pd->read_g = NULL; } if ((events & RUN_POLL_WRITE) && pd->write_g == g) { pd->write_g = NULL; } - pthread_mutex_unlock(&xev_lock); + RUN_XEV_UNLOCK(); } int run_poller_poll(void) { if (!run_xev_has_waiters()) return 0; - pthread_mutex_lock(&xev_lock); + RUN_XEV_LOCK(); __atomic_store_n(&woken_count, 0, __ATOMIC_SEQ_CST); run_xev_tick(); int woken = __atomic_load_n(&woken_count, __ATOMIC_SEQ_CST); - pthread_mutex_unlock(&xev_lock); + RUN_XEV_UNLOCK(); return woken; } @@ -130,7 +138,7 @@ int run_poller_poll_blocking(int64_t timeout_ns) { if (!run_xev_has_waiters()) return 0; - pthread_mutex_lock(&xev_lock); + RUN_XEV_LOCK(); __atomic_store_n(&woken_count, 0, __ATOMIC_SEQ_CST); /* Convert nanoseconds to milliseconds for libxev */ @@ -147,7 +155,7 @@ int run_poller_poll_blocking(int64_t timeout_ns) { run_xev_tick_blocking(timeout_ms); int woken = __atomic_load_n(&woken_count, __ATOMIC_SEQ_CST); - pthread_mutex_unlock(&xev_lock); + RUN_XEV_UNLOCK(); return woken; } diff --git a/src/runtime/run_xev_bridge.zig b/src/runtime/run_xev_bridge.zig index 1c1c79e..5a36e5d 100644 --- a/src/runtime/run_xev_bridge.zig +++ b/src/runtime/run_xev_bridge.zig @@ -71,7 +71,7 @@ fn fileHandleFromFd(fd: c_int) ?std.Io.File.Handle { if (handle == -1) return null; return @ptrFromInt(@as(usize, @intCast(handle))); } - return fd; + return @intCast(fd); } // ── C exports ────────────────────────────────────────────────────── @@ -201,7 +201,7 @@ export fn run_xev_poll_read(fd: c_int, g: GPtr) void { .generation = slot.generation, }; slot.read_armed = true; - if (builtin.os.tag == .windows) { + if (builtin.os.tag == .windows or builtin.os.tag == .wasi) { slot.read_completion = .{ .op = .{ .read = .{ diff --git a/src/runtime/tests/wasi_smoke.c b/src/runtime/tests/wasi_smoke.c new file mode 100644 index 0000000..9915988 --- /dev/null +++ b/src/runtime/tests/wasi_smoke.c @@ -0,0 +1,20 @@ +#include "../run_runtime.h" + +#include + +static int spawned_task_ran = 0; + +static void spawned_task(void *arg) { + (void)arg; + spawned_task_ran = 1; + printf("hello from WASI runtime task\n"); +} + +void run_main__main(void) { + printf("hello from WASI runtime main\n"); + run_spawn(spawned_task, NULL); +} + +int run_wasi_smoke_task_ran(void) { + return spawned_task_ran; +} diff --git a/zig-pkg/libxev-0.0.0-86vtcwIRFACVrx54GaHsMFFlyC4dTi0tcVh10V7btRUc/build.zig b/zig-pkg/libxev-0.0.0-86vtcwIRFACVrx54GaHsMFFlyC4dTi0tcVh10V7btRUc/build.zig index ec2da42..219dfbb 100644 --- a/zig-pkg/libxev-0.0.0-86vtcwIRFACVrx54GaHsMFFlyC4dTi0tcVh10V7btRUc/build.zig +++ b/zig-pkg/libxev-0.0.0-86vtcwIRFACVrx54GaHsMFFlyC4dTi0tcVh10V7btRUc/build.zig @@ -110,8 +110,8 @@ pub fn build(b: *std.Build) !void { const man = try manPages(b); // Benchmarks and examples - const benchmarks = try buildBenchmarks(b, target); - const examples = try buildExamples(b, target, optimize, static_lib); + const benchmarks = if (emit_bench) try buildBenchmarks(b, target) else &.{}; + const examples = if (emit_examples) try buildExamples(b, target, optimize, static_lib) else &.{}; // Test Executable const test_exe: *Step.Compile = test_exe: { diff --git a/zig-pkg/libxev-0.0.0-86vtcwIRFACVrx54GaHsMFFlyC4dTi0tcVh10V7btRUc/src/backend/wasi_poll.zig b/zig-pkg/libxev-0.0.0-86vtcwIRFACVrx54GaHsMFFlyC4dTi0tcVh10V7btRUc/src/backend/wasi_poll.zig index 5d979f7..5f9db61 100644 --- a/zig-pkg/libxev-0.0.0-86vtcwIRFACVrx54GaHsMFFlyC4dTi0tcVh10V7btRUc/src/backend/wasi_poll.zig +++ b/zig-pkg/libxev-0.0.0-86vtcwIRFACVrx54GaHsMFFlyC4dTi0tcVh10V7btRUc/src/backend/wasi_poll.zig @@ -256,10 +256,10 @@ pub const Loop = struct { self.batch.array[0] = .{ .userdata = 0, .u = .{ - .tag = wasi.EVENTTYPE_CLOCK, + .tag = wasi.eventtype_t.CLOCK, .u = .{ .clock = .{ - .id = @as(u32, @bitCast(posix.CLOCK.MONOTONIC)), + .id = wasi.clockid_t.MONOTONIC, .timeout = timeout, .precision = 1 * std.time.ns_per_ms, .flags = wasi.SUBSCRIPTION_CLOCK_ABSTIME, @@ -378,9 +378,9 @@ pub const Loop = struct { .shutdown => |v| res: { const how: wasi.sdflags_t = switch (v.how) { - .both => wasi.SHUT.WR | wasi.SHUT.RD, - .recv => wasi.SHUT.RD, - .send => wasi.SHUT.WR, + .both => .{ .RD = true, .WR = true }, + .recv => .{ .RD = true }, + .send => .{ .WR = true }, }; break :res .{ @@ -392,8 +392,12 @@ pub const Loop = struct { }, .close => |v| res: { - posix.close(v.fd); - break :res .{ .close = {} }; + break :res .{ + .close = switch (wasi.fd_close(v.fd)) { + .SUCCESS => {}, + else => error.Unknown, + }, + }; }, .async_wait => res: { @@ -584,7 +588,7 @@ pub const Loop = struct { fn timer_next(next_ms: u64) wasi.timestamp_t { // Get the absolute time we'll execute this timer next. var now_ts: wasi.timestamp_t = undefined; - switch (wasi.clock_time_get(@as(u32, @bitCast(posix.CLOCK.MONOTONIC)), 1, &now_ts)) { + switch (wasi.clock_time_get(wasi.clockid_t.MONOTONIC, 1, &now_ts)) { .SUCCESS => {}, .INVAL => unreachable, else => unreachable, @@ -597,7 +601,7 @@ pub const Loop = struct { fn get_now() !wasi.timestamp_t { var ts: wasi.timestamp_t = undefined; - return switch (wasi.clock_time_get(posix.CLOCK.MONOTONIC, 1, &ts)) { + return switch (wasi.clock_time_get(wasi.clockid_t.MONOTONIC, 1, &ts)) { .SUCCESS => ts, .INVAL => error.UnsupportedClock, else => |err| posix.unexpectedErrno(err), @@ -605,6 +609,82 @@ pub const Loop = struct { } }; +fn wasiRead(fd: posix.fd_t, buf: []u8) ReadError!usize { + if (buf.len == 0) return 0; + var iov = wasi.iovec_t{ .base = buf.ptr, .len = buf.len }; + var nread: usize = 0; + return switch (wasi.fd_read(fd, @ptrCast(&iov), 1, &nread)) { + .SUCCESS => nread, + else => |err| mapWasiReadError(err), + }; +} + +fn wasiPRead(fd: posix.fd_t, buf: []u8, offset: u64) ReadError!usize { + if (buf.len == 0) return 0; + var iov = wasi.iovec_t{ .base = buf.ptr, .len = buf.len }; + var nread: usize = 0; + return switch (wasi.fd_pread(fd, @ptrCast(&iov), 1, offset, &nread)) { + .SUCCESS => nread, + else => |err| mapWasiReadError(err), + }; +} + +fn wasiWrite(fd: posix.fd_t, bytes: []const u8) WriteError!usize { + if (bytes.len == 0) return 0; + var iov = wasi.ciovec_t{ .base = bytes.ptr, .len = bytes.len }; + var nwritten: usize = 0; + return switch (wasi.fd_write(fd, @ptrCast(&iov), 1, &nwritten)) { + .SUCCESS => nwritten, + else => |err| mapWasiWriteError(err), + }; +} + +fn wasiPWrite(fd: posix.fd_t, bytes: []const u8, offset: u64) WriteError!usize { + if (bytes.len == 0) return 0; + var iov = wasi.ciovec_t{ .base = bytes.ptr, .len = bytes.len }; + var nwritten: usize = 0; + return switch (wasi.fd_pwrite(fd, @ptrCast(&iov), 1, offset, &nwritten)) { + .SUCCESS => nwritten, + else => |err| mapWasiWriteError(err), + }; +} + +fn mapWasiReadError(err: wasi.errno_t) ReadError { + return switch (err) { + .ACCES, .NOTCAPABLE => error.AccessDenied, + .AGAIN => error.WouldBlock, + .BADF => error.NotOpenForReading, + .CONNRESET => error.ConnectionResetByPeer, + .IO => error.InputOutput, + .ISDIR => error.IsDir, + .NOMEM, .NOBUFS => error.SystemResources, + .NOTCONN => error.SocketNotConnected, + .SRCH => error.ProcessNotFound, + .TIMEDOUT => error.ConnectionTimedOut, + else => error.Unexpected, + }; +} + +fn mapWasiWriteError(err: wasi.errno_t) WriteError { + return switch (err) { + .ACCES, .NOTCAPABLE => error.AccessDenied, + .AGAIN => error.WouldBlock, + .BADF => error.NotOpenForWriting, + .CONNRESET => error.ConnectionResetByPeer, + .DQUOT => error.DiskQuota, + .FBIG => error.FileTooBig, + .IO => error.InputOutput, + .INVAL => error.InvalidArgument, + .MSGSIZE => error.MessageTooBig, + .NOMEM, .NOBUFS => error.SystemResources, + .NOSPC => error.NoSpaceLeft, + .PERM => error.PermissionDenied, + .PIPE => error.BrokenPipe, + .SRCH => error.ProcessNotFound, + else => error.Unexpected, + }; +} + pub const Completion = struct { /// Operation to execute. This is only safe to read BEFORE the completion /// is queued. After being queued (with "add"), the operation may change. @@ -669,7 +749,7 @@ pub const Completion = struct { .read => |v| .{ .userdata = @intFromPtr(self), .u = .{ - .tag = wasi.EVENTTYPE_FD_READ, + .tag = wasi.eventtype_t.FD_READ, .u = .{ .fd_read = .{ .fd = v.fd, @@ -681,7 +761,7 @@ pub const Completion = struct { .pread => |v| .{ .userdata = @intFromPtr(self), .u = .{ - .tag = wasi.EVENTTYPE_FD_READ, + .tag = wasi.eventtype_t.FD_READ, .u = .{ .fd_read = .{ .fd = v.fd, @@ -693,7 +773,7 @@ pub const Completion = struct { .write => |v| .{ .userdata = @intFromPtr(self), .u = .{ - .tag = wasi.EVENTTYPE_FD_WRITE, + .tag = wasi.eventtype_t.FD_WRITE, .u = .{ .fd_write = .{ .fd = v.fd, @@ -705,7 +785,7 @@ pub const Completion = struct { .pwrite => |v| .{ .userdata = @intFromPtr(self), .u = .{ - .tag = wasi.EVENTTYPE_FD_WRITE, + .tag = wasi.eventtype_t.FD_WRITE, .u = .{ .fd_write = .{ .fd = v.fd, @@ -717,7 +797,7 @@ pub const Completion = struct { .accept => |v| .{ .userdata = @intFromPtr(self), .u = .{ - .tag = wasi.EVENTTYPE_FD_READ, + .tag = wasi.eventtype_t.FD_READ, .u = .{ .fd_read = .{ .fd = v.socket, @@ -729,7 +809,7 @@ pub const Completion = struct { .recv => |v| .{ .userdata = @intFromPtr(self), .u = .{ - .tag = wasi.EVENTTYPE_FD_READ, + .tag = wasi.eventtype_t.FD_READ, .u = .{ .fd_read = .{ .fd = v.fd, @@ -741,7 +821,7 @@ pub const Completion = struct { .send => |v| .{ .userdata = @intFromPtr(self), .u = .{ - .tag = wasi.EVENTTYPE_FD_WRITE, + .tag = wasi.eventtype_t.FD_WRITE, .u = .{ .fd_write = .{ .fd = v.fd, @@ -777,7 +857,7 @@ pub const Completion = struct { .accept => |*op| res: { var out_fd: posix.fd_t = undefined; break :res .{ - .accept = switch (wasi.sock_accept(op.socket, 0, &out_fd)) { + .accept = switch (wasi.sock_accept(op.socket, .{}, &out_fd)) { .SUCCESS => out_fd, else => |err| posix.unexpectedErrno(err), }, @@ -786,8 +866,8 @@ pub const Completion = struct { .read => |*op| res: { const n_ = switch (op.buffer) { - .slice => |v| posix.read(op.fd, v), - .array => |*v| posix.read(op.fd, v), + .slice => |v| wasiRead(op.fd, v), + .array => |*v| wasiRead(op.fd, v), }; break :res .{ @@ -800,8 +880,8 @@ pub const Completion = struct { .pread => |*op| res: { const n_ = switch (op.buffer) { - .slice => |v| posix.pread(op.fd, v, op.offset), - .array => |*v| posix.pread(op.fd, v, op.offset), + .slice => |v| wasiPRead(op.fd, v, op.offset), + .array => |*v| wasiPRead(op.fd, v, op.offset), }; break :res .{ @@ -814,8 +894,8 @@ pub const Completion = struct { .write => |*op| res: { const n_ = switch (op.buffer) { - .slice => |v| posix.write(op.fd, v), - .array => |*v| posix.write(op.fd, v.array[0..v.len]), + .slice => |v| wasiWrite(op.fd, v), + .array => |*v| wasiWrite(op.fd, v.array[0..v.len]), }; break :res .{ @@ -825,8 +905,8 @@ pub const Completion = struct { .pwrite => |*op| res: { const n_ = switch (op.buffer) { - .slice => |v| posix.pwrite(op.fd, v, op.offset), - .array => |*v| posix.pwrite(op.fd, v.array[0..v.len], op.offset), + .slice => |v| wasiPWrite(op.fd, v, op.offset), + .array => |*v| wasiPWrite(op.fd, v.array[0..v.len], op.offset), }; break :res .{