fix: signal.SIGINT and signal() raise NotImplementedError on wasi (#8986)

* fix: signal.SIGINT and signal() raise NotImplementedError on wasi

- Problem: signal.SIGINT and friends don't exist on wasi, and signal.signal()
  is hard-stubbed to raise NotImplementedError there
- Cause: wasi-libc emulates signal()/raise() in a separate opt-in library
  that Rust's own wasm32-wasip1 target distribution doesn't ship
- Fix: vendor wasi-sdk's prebuilt libwasi-emulated-signal.a, link it for
  wasm32-wasip1, and wire SIGINT/SIGABRT/SIGFPE/SIGILL/SIGSEGV/SIGTERM,
  signal()/getsignal()/raise_signal() through it. CPython's own WASI build
  links the same library

Assisted-by: Claude Code:claude-sonnet-5
Signed-off-by: Jiwoo Ahn <ikwydls1314@gmail.com>

* Group WASI signal definitions in mod wasm

Move WASI-only types, constants, FFI, and handlers into
`mod wasm` and re-export them with `#[cfg(target_os = "wasi")]`.

Assisted-by: Grok 4.6

---------

Signed-off-by: Jiwoo Ahn <ikwydls1314@gmail.com>
Co-authored-by: Jeong YunWon <jeong@youknowone.org>
This commit is contained in:
Jiwoo AhnandJeong YunWon authored and GitHub committed 2026-10-07 04:57:13 +00:00
1 parent a403341791
commit ef5c651958
8 files changed
+114 -20

No files matched your search

+3
View File
@@ -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]
+1 -1
View File
@@ -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;
+75 -4
View File
@@ -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<sighandler_t> {
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<sighandler_t> {
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;
+16
View File
@@ -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
Binary file not shown.
+4
View File
@@ -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
+13 -13
View File
@@ -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<crate::builtins::PyModule>,
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(())
+2 -2
View File
@@ -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