@ai-ecoverse/slicc-kernel 1.55.0 → 1.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -188,7 +188,7 @@ On top of preview1 a WASI program gets:
|
|
|
188
188
|
- **Threads**: `wasi.thread-spawn` (wasm32-wasip1-threads) and WASIX `thread_spawn_v2` start a worker per thread on the process's shared memory, sized from the module's own import (a 2 GiB maximum when the engine cannot reserve more). The threads share one descriptor table; `exit` or a trap in any thread ends the process. At most 64 threads per process; `SLICC_WASM_THREADS` sets another cap, up to 256.
|
|
189
189
|
- **WASIX** (a module importing `wasix_32v1`): fork and setjmp/longjmp through Asyncify (the module is built with `wasm-opt --asyncify`), exec, `posix_spawn`, `waitpid` (with `WNOHANG`, and `WUNTRACED` for a child that stops; WASIX has no `WCONTINUED`), pipes, `dup2`, signal handlers and interval timers, terminal modes, and dynamic linking: a position-independent main module loads side modules with `dlopen` from `LD_LIBRARY_PATH`, its runtime path, `/lib`, `/usr/lib` and `/usr/local/lib`. Each thread has its own copy of the function table, so the kernel hands out its slots (a side module's range, a `dlsym`'d function, a closure): threads loading or looking up at the same time never get the same slot. As in glibc, one `dlopen` at a time runs in a process, constructors included: a `dlopen` from a library's constructor goes ahead in its own thread, and another thread's `dlopen` waits, then finds the library loaded and gets the same handle. A thread waiting there still ends or stops with its process, but a signal handler it has runs once that `dlopen` returns. The lock is let go when a `dlopen` fails (an exception, a trap or `pthread_exit` out of a constructor included) and when its thread ends; a forked child starts with it free. In such a program, `dlopen(NULL)`, and `dlopen` of the program's own file (also by a path that resolves to it through symlinks), give a handle for the program that is already running (no second copy is loaded); a program that isn't position-independent gets an error from `dlopen(NULL)`, as from any `dlopen`; `dlsym` on it searches the program, then the loaded libraries, and `dlclose` of it does nothing. A function already in the table (the program's own, or a library's) keeps its slot, so `dlsym(handle, "strlen") == strlen`.
|
|
190
190
|
- **Foreign calls** (libffi's WASIX backend, so Python's `ctypes`): `call_dynamic` calls a function through the program's indirect function table (a static module's exported `__indirect_function_table`, or the dynamic linker's), its arguments and results packed in memory one after another (`i32`/`f32` 4 bytes, `i64`/`f64` 8). With `strict` set, the buffers must fit the function exactly (else `EINVAL`, before the function is called); without it, missing arguments are zero (one cut short keeps the bytes given, with zeros above them) and results that do not fit are left out. A trap or exception of the callee goes through to the caller. JS cannot ask a function for its type, so the kernel reads the types from the module files (the program's and each side module's, once, when a program first calls through `call_dynamic`): a table slot that is empty, past the end, holds a function of no module the kernel knows of, or takes or returns a `v128`, gives `EINVAL`. For closures (callbacks such as `CFUNCTYPE`), a dynamically linked program gets table slots from `closure_allocate`, and `closure_free` gives them back for reuse and empties the slot in every thread; a static program gets `ENOTSUP`. `closure_prepare` puts a trampoline with the requested signature (any of `i32`, `i64`, `f32`, `f64`, `v128`) into such a slot, in every thread: each call stores its arguments below the stack pointer and calls the backing function as `backing(values, results, user_data)`, then returns what it wrote to `results`. The trampolines are small wasm modules made and compiled at run time, once per signature in each thread (an instance per closure), so a page whose Content Security Policy forbids compiling WebAssembly (no `'wasm-unsafe-eval'`) cannot run them; it cannot run the program either. A `fork` or a `longjmp` (both through Asyncify) inside a function reached through `call_dynamic` cannot unwind through it.
|
|
191
|
-
- **Job control:** `^Z`, `SIGTSTP`, `SIGTTIN` and `SIGTTOU` stop a WASIX program at once, unless it has a handler for them. A stopped program that is busy computing is stopped for job control (`jobs` shows it, `fg`, `bg` and `SIGCONT` work), but its worker keeps a core busy until its next system call, since a worker's running code cannot be frozen from outside. Agents should `kill` such a job rather than leave it stopped. For the same reason, a signal with a handler or a terminating default (`^C`, `SIGTERM`) reaches a program that is busy computing only at its next system call, so a CPU-bound loop may not end on `^C`; `kill -9` always ends it. A thread blocked in the kernel or waiting on a lock (`pthread_cond_wait`, a futex) is woken by a signal for it (a futex wait checks for signals every 50 ms, so each waiting thread wakes briefly that often), and `pthread_kill` runs the handler on the thread it names. A sleep, or a clock in `poll_oneoff`, never ends before the time asked (it can end later, never earlier). A signal that ends a sleep or a `poll`/`select` with a timeout makes the call fail with `EINTR` in a program whose libc says it is wasix-sysroot 2025.9.30-22 or later (its `slicc.libc` custom section). Older libcs turn that answer into `ENOTSUP` from `nanosleep` (up to -18), so for a program without the section the wait ends as if it had timed out, unless it imports `fd_fdflags_set` (then `EINTR`, as before).
|
|
191
|
+
- **Job control:** `^Z`, `SIGTSTP`, `SIGTTIN` and `SIGTTOU` stop a WASIX program at once, unless it has a handler for them. A stopped program that is busy computing is stopped for job control (`jobs` shows it, `fg`, `bg` and `SIGCONT` work), but its worker keeps a core busy until its next system call, since a worker's running code cannot be frozen from outside. Agents should `kill` such a job rather than leave it stopped. For the same reason, a signal with a handler or a terminating default (`^C`, `SIGTERM`) reaches a program that is busy computing only at its next system call, so a CPU-bound loop may not end on `^C`; `kill -9` always ends it. A pending signal is delivered at the program's next WASI or WASIX call, including the calls its worker answers without the kernel (`random_get`, `args_get`, …), except the exempt ones: `clock_time_get` (a tight loop of it measured up to 30% slower), `proc_exit`, `proc_exit2`, `thread_exit`, `thread_spawn_v2` and `wasi.thread-spawn` (wasi-threads), the calls that install handlers (`callback_signal`, `slicc.sigaction_set`), and calls made while Asyncify unwinds or rewinds the stack for `fork` or `longjmp`. A thread blocked in the kernel or waiting on a lock (`pthread_cond_wait`, a futex) is woken by a signal for it (a futex wait checks for signals every 50 ms, so each waiting thread wakes briefly that often), and `pthread_kill` runs the handler on the thread it names. A sleep, or a clock in `poll_oneoff`, never ends before the time asked (it can end later, never earlier). A signal that ends a sleep or a `poll`/`select` with a timeout makes the call fail with `EINTR` in a program whose libc says it is wasix-sysroot 2025.9.30-22 or later (its `slicc.libc` custom section). Older libcs turn that answer into `ENOTSUP` from `nanosleep` (up to -18), so for a program without the section the wait ends as if it had timed out, unless it imports `fd_fdflags_set` (then `EINTR`, as before).
|
|
192
192
|
- **Pipes** have no file type for WASI and WASIX programs' `fstat` (`st_mode` type 0), because WASI has no FIFO file type. A socket type, as kernels 1.47.3 to 1.48.1 gave them, made Ruby treat a `popen` pipe as a socket (`close_write` then never ends the child's input). The kernel's `slicc_fs.fd_mode` answers `S_IFIFO | 0600` (Linux numbering) for a pipe, and wasix-sysroot 2025.9.30-21 and later take the type from it, so programs built on it see a FIFO (`S_ISFIFO`); older libcs keep only the permission bits. Until then Python's asyncio refuses a pipe for a subprocess (`Pipe transport is only for pipes, sockets and character devices`). They are read and written as pipes; socket calls on them fail with `ENOTSOCK`, and `isatty()` is false. Emscripten programs, bash included, see a FIFO. The same holds for any kernel descriptor that is neither seekable nor a terminal or device, such as captured output.
|
|
193
193
|
- **Sockets**: the WASIX socket calls (`sock_open`, `sock_pair` for an `AF_UNIX` stream pair, `bind`, `listen`, `connect`, `accept`, options, local and peer names) are the kernel's loopback sockets, and preview1's `sock_recv` / `sock_send` / `sock_shutdown` work on them and on inherited ones. Writing to a socket whose peer is gone fails with `EPIPE`, through `fd_write` as through `sock_send`, as if `MSG_NOSIGNAL` were set, since a WASI program cannot ignore `SIGPIPE`; a pipe or terminal whose reader is gone still ends the program with status 141. HTTP and HTTPS leave through the realm proxy that `https_proxy` names, as for any other program; `resolve` answers `/etc/hosts` names and literal addresses, and the [uplink](#uplink)'s names.
|
|
194
194
|
- **Diagnostics**: a trap ends the program with 134 and its message on stderr; with `SLICC_WASM_BACKTRACE=1` the wasm frames follow, named from the module's name section, or from a sidecar for a module shipped without one (`<module>.names` beside it, or the same path in a `<package>-names` package). `SLICC_WASI_STATS=1` makes a program print its calls, counted and timed, as it ends.
|
|
@@ -1905,6 +1905,7 @@ var init_wasi_host = __esm({
|
|
|
1905
1905
|
muted;
|
|
1906
1906
|
signals;
|
|
1907
1907
|
interrupted;
|
|
1908
|
+
quietly = (fn) => fn();
|
|
1908
1909
|
listening;
|
|
1909
1910
|
o;
|
|
1910
1911
|
constructor(o) {
|
|
@@ -9268,6 +9269,10 @@ var init_wasix_fork = __esm({
|
|
|
9268
9269
|
get supported() {
|
|
9269
9270
|
return typeof this.exports.asyncify_start_unwind === "function";
|
|
9270
9271
|
}
|
|
9272
|
+
get idle() {
|
|
9273
|
+
const state = this.exports.asyncify_get_state;
|
|
9274
|
+
return !this.supported || typeof state !== "function" || state() === 0;
|
|
9275
|
+
}
|
|
9271
9276
|
resume() {
|
|
9272
9277
|
if (this.exports.asyncify_get_state?.() !== UNWINDING) return false;
|
|
9273
9278
|
this.exports.asyncify_stop_unwind?.();
|
|
@@ -10998,13 +11003,49 @@ var wasi_runtime_exports = {};
|
|
|
10998
11003
|
__export(wasi_runtime_exports, {
|
|
10999
11004
|
FALLBACK_MAXIMUM_PAGES: () => FALLBACK_MAXIMUM_PAGES,
|
|
11000
11005
|
LinkSync: () => LinkSync,
|
|
11006
|
+
UNDELIVERED: () => UNDELIVERED,
|
|
11001
11007
|
captureBacktraces: () => captureBacktraces,
|
|
11008
|
+
catchUpFirst: () => catchUpFirst,
|
|
11002
11009
|
createImportedMemory: () => createImportedMemory,
|
|
11010
|
+
delivering: () => delivering,
|
|
11003
11011
|
runWasiProcess: () => runWasiProcess,
|
|
11004
11012
|
runWasiThread: () => runWasiThread,
|
|
11005
11013
|
trapMessage: () => trapMessage,
|
|
11006
|
-
unsupportedImport: () => unsupportedImport
|
|
11014
|
+
unsupportedImport: () => unsupportedImport,
|
|
11015
|
+
wireSignals: () => wireSignals
|
|
11007
11016
|
});
|
|
11017
|
+
function wireSignals(host, gate) {
|
|
11018
|
+
host.interrupted = () => gate.interrupt();
|
|
11019
|
+
host.quietly = (fn) => gate.quiet(fn);
|
|
11020
|
+
}
|
|
11021
|
+
function catchUpFirst(host, sync) {
|
|
11022
|
+
return sync && (() => host.quietly(() => sync.current()));
|
|
11023
|
+
}
|
|
11024
|
+
function deliverAll(imports, host, driver, before) {
|
|
11025
|
+
for (const ns of DELIVERED) {
|
|
11026
|
+
const table = imports[ns];
|
|
11027
|
+
if (table) imports[ns] = delivering(ns, table, host, driver, before);
|
|
11028
|
+
}
|
|
11029
|
+
}
|
|
11030
|
+
function delivering(ns, table, host, driver, before) {
|
|
11031
|
+
const exempt = new Set(UNDELIVERED[ns]);
|
|
11032
|
+
const out = {};
|
|
11033
|
+
for (const [name2, value2] of Object.entries(table)) {
|
|
11034
|
+
if (exempt.has(name2) || typeof value2 !== "function") {
|
|
11035
|
+
out[name2] = value2;
|
|
11036
|
+
continue;
|
|
11037
|
+
}
|
|
11038
|
+
const fn = value2;
|
|
11039
|
+
out[name2] = (...args) => {
|
|
11040
|
+
if (driver.idle) {
|
|
11041
|
+
before?.();
|
|
11042
|
+
host.interrupted?.();
|
|
11043
|
+
}
|
|
11044
|
+
return fn(...args);
|
|
11045
|
+
};
|
|
11046
|
+
}
|
|
11047
|
+
return out;
|
|
11048
|
+
}
|
|
11008
11049
|
function enosys(module, name2, foreign) {
|
|
11009
11050
|
const type = foreign?.[module]?.[name2];
|
|
11010
11051
|
if (type === "other") throw new Error(noErrno(`${module}.${name2}`));
|
|
@@ -11203,6 +11244,7 @@ async function instantiate(host, module, memory, threads, {
|
|
|
11203
11244
|
sliccFs,
|
|
11204
11245
|
sliccTty2
|
|
11205
11246
|
);
|
|
11247
|
+
deliverAll(hostImports, host, driver, catchUpFirst(host, sync));
|
|
11206
11248
|
const imports = sync ? merge(hostImports, sync.linker.mainImports(module)) : hostImports;
|
|
11207
11249
|
instance = await WebAssembly.instantiate(module, imports);
|
|
11208
11250
|
stats?.phase("instantiate");
|
|
@@ -11330,7 +11372,7 @@ async function runWasiProcess(init, port3) {
|
|
|
11330
11372
|
if (importsSigactionHook(module)) signals.useHook();
|
|
11331
11373
|
if (fork?.signals) signals.restore(fork.signals);
|
|
11332
11374
|
host.signals = signals;
|
|
11333
|
-
host
|
|
11375
|
+
wireSignals(host, gate);
|
|
11334
11376
|
host.onRaise = (sig) => signals.raised(sig);
|
|
11335
11377
|
host.muted = (fd, data) => signals.muted(fd, data);
|
|
11336
11378
|
const exports = instance.exports;
|
|
@@ -11410,7 +11452,7 @@ async function runWasiThread(init, port3) {
|
|
|
11410
11452
|
});
|
|
11411
11453
|
signals.bind(instance.exports);
|
|
11412
11454
|
signals.register(SIGNAL_CALLBACK);
|
|
11413
|
-
host
|
|
11455
|
+
wireSignals(host, gate);
|
|
11414
11456
|
host.onRaise = (sig) => signals.raised(sig);
|
|
11415
11457
|
const start2 = instance.exports.wasi_thread_start;
|
|
11416
11458
|
try {
|
|
@@ -11436,7 +11478,7 @@ async function runWasiThread(init, port3) {
|
|
|
11436
11478
|
throw e;
|
|
11437
11479
|
}
|
|
11438
11480
|
}
|
|
11439
|
-
var TRAPPED, PREVIEW1, WASIX, SLICC, SIGNAL_CALLBACK, takeDefault, SLICC_FS, SLICC_TTY, RESERVED2, importsSigactionHook, noErrno, FALLBACK_MAXIMUM_PAGES, LinkSync, DL_GEN, BACKTRACE_FRAMES;
|
|
11481
|
+
var TRAPPED, PREVIEW1, WASIX, SLICC, SIGNAL_CALLBACK, takeDefault, SLICC_FS, SLICC_TTY, RESERVED2, UNDELIVERED, DELIVERED, importsSigactionHook, noErrno, FALLBACK_MAXIMUM_PAGES, LinkSync, DL_GEN, BACKTRACE_FRAMES;
|
|
11440
11482
|
var init_wasi_runtime = __esm({
|
|
11441
11483
|
"src/process/wasi/wasi-runtime.ts"() {
|
|
11442
11484
|
"use strict";
|
|
@@ -11473,6 +11515,13 @@ var init_wasi_runtime = __esm({
|
|
|
11473
11515
|
SLICC_FS = "slicc_fs";
|
|
11474
11516
|
SLICC_TTY = "slicc_tty";
|
|
11475
11517
|
RESERVED2 = RESERVED_NAMESPACES;
|
|
11518
|
+
UNDELIVERED = {
|
|
11519
|
+
[PREVIEW1]: ["proc_exit", "clock_time_get"],
|
|
11520
|
+
[WASIX]: ["proc_exit2", "thread_exit", "thread_spawn_v2", "callback_signal"],
|
|
11521
|
+
[SLICC]: ["sigaction_set"],
|
|
11522
|
+
wasi: ["thread-spawn"]
|
|
11523
|
+
};
|
|
11524
|
+
DELIVERED = [PREVIEW1, WASIX, SLICC, SLICC_FS, SLICC_TTY];
|
|
11476
11525
|
importsSigactionHook = (module) => WebAssembly.Module.imports(module).some((i) => i.module === SLICC && i.name === "sigaction_set");
|
|
11477
11526
|
noErrno = (key) => `imports ${key}: its result cannot carry ENOSYS, and this host does not provide it`;
|
|
11478
11527
|
FALLBACK_MAXIMUM_PAGES = 32768;
|
|
@@ -11524,6 +11573,9 @@ var init_wasi_runtime = __esm({
|
|
|
11524
11573
|
this.bump();
|
|
11525
11574
|
this.catchUp();
|
|
11526
11575
|
}
|
|
11576
|
+
current() {
|
|
11577
|
+
if (this.ids && Atomics.load(this.ids, DL_GEN) !== this.gen) this.catchUp();
|
|
11578
|
+
}
|
|
11527
11579
|
catchUp() {
|
|
11528
11580
|
for (; ; ) {
|
|
11529
11581
|
const gen = this.ids ? Atomics.load(this.ids, DL_GEN) : 0;
|
|
@@ -11543,9 +11595,9 @@ var init_wasi_runtime = __esm({
|
|
|
11543
11595
|
Object.entries(table).map(([name2, fn]) => [
|
|
11544
11596
|
name2,
|
|
11545
11597
|
((...args) => {
|
|
11546
|
-
|
|
11598
|
+
this.current();
|
|
11547
11599
|
const result = fn(...args);
|
|
11548
|
-
|
|
11600
|
+
this.current();
|
|
11549
11601
|
return result;
|
|
11550
11602
|
})
|
|
11551
11603
|
])
|