diff --git a/.cargo/config.toml b/.cargo/config.toml index 4e54d1d3b..79a36b717 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -10,9 +10,12 @@ rustflags = ["--cfg=getrandom_backend=\"wasm_js\""] # Enforce a 1 MB stack limit for WASI targets. # (Runaway heap growth is fixed by the smaller DataStack chunk # and the size‑optimized wasm‑release profile.) + [target.wasm32-wasip1] rustflags = [ "-C", "link-arg=-zstack-size=1048576", + "-L", "crates/host_env/vendor/wasm32-wasip1", + "-l", "wasi-emulated-signal", ] [target.wasm32-wasip2] diff --git a/crates/host_env/src/lib.rs b/crates/host_env/src/lib.rs index ee9c5238e..33b5ad60b 100644 --- a/crates/host_env/src/lib.rs +++ b/crates/host_env/src/lib.rs @@ -74,7 +74,7 @@ pub mod pwd; pub mod resource; #[cfg(all(unix, not(target_os = "redox"), not(target_os = "android")))] pub mod shm; -#[cfg(any(unix, windows))] +#[cfg(any(unix, windows, target_os = "wasi"))] pub mod signal; pub mod time; diff --git a/crates/host_env/src/signal.rs b/crates/host_env/src/signal.rs index 090269123..70d32a9fd 100644 --- a/crates/host_env/src/signal.rs +++ b/crates/host_env/src/signal.rs @@ -1,3 +1,4 @@ +#[cfg(any(unix, windows))] use std::io; #[cfg(windows)] use std::sync::Once; @@ -13,13 +14,83 @@ pub use libc::sighandler_t; #[cfg(unix)] pub use libc::{SIG_DFL, SIG_ERR, SIG_IGN}; -#[cfg(not(unix))] +#[cfg(not(any(unix, target_os = "wasi")))] pub const SIG_DFL: libc::sighandler_t = 0; -#[cfg(not(unix))] +#[cfg(not(any(unix, target_os = "wasi")))] pub const SIG_IGN: libc::sighandler_t = 1; -#[cfg(not(unix))] +#[cfg(not(any(unix, target_os = "wasi")))] pub const SIG_ERR: libc::sighandler_t = -1 as _; +/// wasi-libc's userspace emulation of `signal()`/`raise()`, linked from +/// `libwasi-emulated-signal` (see `crates/host_env/vendor/wasm32-wasip1`). +/// WebAssembly has no asynchronous signal delivery, so this is a +/// synchronous, in-process handler table: a handler only runs when this +/// process's own code calls `raise()`, never from an outside interrupt. +/// Numbering matches wasi-libc's `bits/signal.h`, the same values CPython's +/// WASI build gets by linking the same library. +#[cfg(target_os = "wasi")] +mod wasm { + use std::io; + + #[allow(non_camel_case_types)] + pub type sighandler_t = usize; + + pub const SIG_DFL: sighandler_t = 0; + pub const SIG_IGN: sighandler_t = 1; + pub const SIG_ERR: sighandler_t = -1isize as usize; + + pub const SIGINT: i32 = 2; + pub const SIGILL: i32 = 4; + pub const SIGABRT: i32 = 6; + pub const SIGFPE: i32 = 8; + pub const SIGSEGV: i32 = 11; + pub const SIGTERM: i32 = 15; + + unsafe extern "C" { + fn signal(signum: i32, handler: sighandler_t) -> sighandler_t; + fn raise(signum: i32) -> i32; + } + + /// # Safety + /// + /// The caller must ensure `signalnum` is a valid platform signal number. + pub unsafe fn probe_handler(signalnum: i32) -> Option { + let handler = unsafe { signal(signalnum, SIG_IGN) }; + if handler == SIG_ERR { + None + } else { + unsafe { signal(signalnum, handler) }; + Some(handler) + } + } + + /// # Safety + /// + /// The caller must ensure `signalnum` is a valid platform signal number and + /// `handler` is accepted by the platform signal ABI. + pub unsafe fn install_handler( + signalnum: i32, + handler: sighandler_t, + ) -> io::Result { + let old = unsafe { signal(signalnum, handler) }; + if old == SIG_ERR { + return Err(io::Error::last_os_error()); + } + Ok(old) + } + + pub fn raise_signal(signalnum: i32) -> io::Result<()> { + if unsafe { raise(signalnum) } == 0 { + Ok(()) + } else { + Err(io::Error::last_os_error()) + } + } +} + +#[cfg(target_os = "wasi")] +pub use wasm::*; + #[cfg(unix)] pub use libc::{SIG_BLOCK, SIG_SETMASK, SIG_UNBLOCK}; @@ -320,7 +391,7 @@ pub fn notify_signal( } } -#[cfg(unix)] +#[cfg(any(unix, target_os = "wasi"))] pub fn notify_signal(signum: i32, wakeup_fd: i32) { if wakeup_fd == -1 { return; diff --git a/crates/host_env/vendor/wasm32-wasip1/README.md b/crates/host_env/vendor/wasm32-wasip1/README.md new file mode 100644 index 000000000..b8d201c91 --- /dev/null +++ b/crates/host_env/vendor/wasm32-wasip1/README.md @@ -0,0 +1,16 @@ +# Vendored wasi-libc emulation library + +`libwasi-emulated-signal.a` is a prebuilt static library from +[wasi-sdk](https://github.com/WebAssembly/wasi-sdk), providing wasi-libc's +userspace emulation of `signal()`/`raise()` for `wasm32-wasip1` (WebAssembly +has no asynchronous signal delivery, so this is a synchronous, in-process +handler table, not a real OS signal mechanism). + +CPython's own WASI build links the same library +(`configure.ac`'s `_WASI_EMULATED_SIGNAL` case). + +- Source: https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-34/wasi-sysroot-34.0.tar.gz + (path: `lib/wasm32-wasip1/libwasi-emulated-signal.a`) +- License: Apache-2.0 WITH LLVM-exception (same as wasi-libc) +- sha256: see below +becc35b856608fb9282d94c70166796e4d7b21e4e1057e625725306e765eb035 crates/host_env/vendor/wasm32-wasip1/libwasi-emulated-signal.a diff --git a/crates/host_env/vendor/wasm32-wasip1/libwasi-emulated-signal.a b/crates/host_env/vendor/wasm32-wasip1/libwasi-emulated-signal.a new file mode 100644 index 000000000..25ba3e051 Binary files /dev/null and b/crates/host_env/vendor/wasm32-wasip1/libwasi-emulated-signal.a differ diff --git a/crates/vm/src/signal.rs b/crates/vm/src/signal.rs index ddd4d58e5..eda531bbc 100644 --- a/crates/vm/src/signal.rs +++ b/crates/vm/src/signal.rs @@ -233,6 +233,10 @@ impl SignalNum { #[allow(dead_code, reason = "Not used on all platforms")] pub(crate) const SIGINT: Self = Self(libc::SIGINT); + #[cfg(all(target_os = "wasi", feature = "host_env"))] + #[allow(dead_code, reason = "Not used on all platforms")] + pub(crate) const SIGINT: Self = Self(rustpython_host_env::signal::SIGINT); + /// Construct [`Self`] without any validation on the signalnum value. /// /// # Safety diff --git a/crates/vm/src/stdlib/_signal.rs b/crates/vm/src/stdlib/_signal.rs index edbff6d5d..67a765a62 100644 --- a/crates/vm/src/stdlib/_signal.rs +++ b/crates/vm/src/stdlib/_signal.rs @@ -13,7 +13,7 @@ pub(crate) mod _signal { use core::sync::atomic::{self, Ordering}; cfg_select! { - any(unix, windows) => { + any(unix, windows, target_os = "wasi") => { use crate::convert::{IntoPyException, TryFromBorrowedObject}; use rustpython_host_env::signal as host_signal; } @@ -73,11 +73,11 @@ pub(crate) mod _signal { } } - #[cfg(any(unix, windows))] + #[cfg(any(unix, windows, target_os = "wasi"))] #[allow(unused_imports)] pub use host_signal::SIG_ERR; - #[cfg(any(unix, windows))] + #[cfg(any(unix, windows, target_os = "wasi"))] #[pyattr] pub use host_signal::{SIG_DFL, SIG_IGN}; @@ -86,22 +86,22 @@ pub(crate) mod _signal { #[pyattr] use host_signal::{SIG_BLOCK, SIG_SETMASK, SIG_UNBLOCK}; - #[cfg(not(any(unix, windows)))] + #[cfg(not(any(unix, windows, target_os = "wasi")))] #[pyattr] pub const SIG_DFL: sighandler_t = 0; - #[cfg(not(any(unix, windows)))] + #[cfg(not(any(unix, windows, target_os = "wasi")))] #[pyattr] pub const SIG_IGN: sighandler_t = 1; - #[cfg(not(any(unix, windows)))] + #[cfg(not(any(unix, windows, target_os = "wasi")))] #[allow(dead_code)] pub const SIG_ERR: sighandler_t = -1 as _; #[pyattr] use crate::signal::NSIG; - #[cfg(any(unix, windows))] + #[cfg(any(unix, windows, target_os = "wasi"))] #[pyattr] pub use host_signal::{SIGABRT, SIGFPE, SIGILL, SIGINT, SIGSEGV, SIGTERM}; @@ -160,7 +160,7 @@ pub(crate) mod _signal { const _: () = assert!(SignalNum::VALID_RANGE.start.is_positive()); const _: () = assert!(SignalNum::VALID_RANGE.end.is_positive()); - #[cfg(any(unix, windows))] + #[cfg(any(unix, windows, target_os = "wasi"))] pub(super) fn init_signal_handlers( module: &Py, vm: &VirtualMachine, @@ -199,7 +199,7 @@ pub(crate) mod _signal { } } - #[cfg(not(any(unix, windows)))] + #[cfg(not(any(unix, windows, target_os = "wasi")))] #[pyfunction] pub fn signal( _signalnum: i32, @@ -209,7 +209,7 @@ pub(crate) mod _signal { Err(vm.new_not_implemented_error("signal is not implemented on this platform")) } - #[cfg(any(unix, windows))] + #[cfg(any(unix, windows, target_os = "wasi"))] #[pyfunction] pub fn signal( signalnum: SignalNum, @@ -401,7 +401,7 @@ pub(crate) mod _signal { host_signal::siginterrupt(signalnum.into(), flag).map_err(|_| vm.new_last_errno_error()) } - #[cfg(any(unix, windows))] + #[cfg(any(unix, windows, target_os = "wasi"))] #[pyfunction] fn raise_signal(signalnum: i32, vm: &VirtualMachine) -> PyResult<()> { let signalnum = SignalNum::try_from(signalnum).map_err(cfg_select! { @@ -506,7 +506,7 @@ pub(crate) mod _signal { sigset_to_pyset(old_mask, vm) } - #[cfg(any(unix, windows))] + #[cfg(any(unix, windows, target_os = "wasi"))] pub extern "C" fn run_signal(signum: i32) { signal::TRIGGERS[signum as usize].store(true, Ordering::Relaxed); signal::set_triggered(); @@ -535,7 +535,7 @@ pub(crate) mod _signal { ) -> PyResult<()> { __module_exec(vm, module); - #[cfg(any(unix, windows))] + #[cfg(any(unix, windows, target_os = "wasi"))] init_signal_handlers(module, vm); Ok(()) diff --git a/extra_tests/snippets/stdlib_signal.py b/extra_tests/snippets/stdlib_signal.py index 50a025a06..8e00b5d88 100644 --- a/extra_tests/snippets/stdlib_signal.py +++ b/extra_tests/snippets/stdlib_signal.py @@ -21,8 +21,8 @@ assert old_signal is signal.SIG_IGN assert signal.getsignal(signal.SIGILL) is signal.SIG_DFL -# unix -if "win" not in sys.platform: +# unix, and not WASI +if "win" not in sys.platform and sys.platform != "wasi": signal.signal(signal.SIGALRM, handler) assert signal.getsignal(signal.SIGALRM) is handler