diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a1d4de77f..a2e0f3b73 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -99,6 +99,44 @@ jobs: run: rustup target add wasm32-wasip2 - name: Tests run: cargo test --target wasm32-wasip2 --all-features + TestEmscripten: + runs-on: ubuntu-latest + timeout-minutes: 20 + # TEMPORARY: this job builds against the guybedford/emscripten `cf` fork, + # which carries the epoll callback, AF_UNIX pathname sockets, and multicast + # getsockopt patches this branch depends on. Once those land upstream the + # fork checkout can be dropped and this can move to a released emsdk. + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@nightly + with: + components: rust-src + # A recent Node for the cargo runner (rather than the Node bundled with + # emsdk). No JSPI needed: blocking test-harness socket ops block on their + # pthread via the sync-proxy (_emscripten_fd_wait), not stack suspension. + - uses: actions/setup-node@v4 + with: + node-version: 26 + - name: Record runner node + run: echo "RUNNER_NODE=$(which node)" >> "$GITHUB_ENV" + - name: Install emsdk (LLVM + binaryen toolchain) + run: | + git clone --depth 1 https://github.com/emscripten-core/emsdk.git + ./emsdk/emsdk install latest + ./emsdk/emsdk activate latest + - name: Clone guybedford/emscripten (cf) fork over the emsdk tree + run: | + git clone --depth 1 --branch cf https://github.com/guybedford/emscripten.git em-fork + python3 em-fork/bootstrap.py + rm -rf ./emsdk/upstream/emscripten + ln -s "$(pwd)/em-fork" ./emsdk/upstream/emscripten + - name: Tests + run: | + source ./emsdk/emsdk_env.sh + export EM_CONFIG="$EMSDK/.emscripten" + CARGO_TARGET_WASM32_UNKNOWN_EMSCRIPTEN_RUNNER="$RUNNER_NODE" \ + RUSTFLAGS="-A linker_messages -C target-feature=+atomics,+bulk-memory,+mutable-globals -C link-arg=-pthread -C link-arg=-sPROXY_TO_PTHREAD -C link-arg=-sNODERAWSOCKETS -C link-arg=-sNODERAWFS -C link-arg=-sALLOW_MEMORY_GROWTH=1 -C link-arg=-sEXIT_RUNTIME=1" \ + cargo +nightly test --target wasm32-unknown-emscripten -Zbuild-std --all-features --tests --examples --lib Nightly: runs-on: ubuntu-latest timeout-minutes: 10 @@ -170,6 +208,7 @@ jobs: #- powerpc64-ibm-aix - riscv32imc-esp-espidf - sparcv9-sun-solaris + - wasm32-unknown-emscripten - wasm32-wasip1 - x86_64-apple-darwin - x86_64-apple-ios diff --git a/Cargo.toml b/Cargo.toml index 13978e989..9d4c68391 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,7 @@ net = [] log = { version = "0.4.8", optional = true } [target.'cfg(any(unix, target_os = "hermit", target_os = "wasi"))'.dependencies] -libc = "0.2.183" +libc = { git = "https://github.com/guybedford/libc", branch = "emscripten" } [target.'cfg(windows)'.dependencies.windows-sys] version = "0.61" diff --git a/README.md b/README.md index 98a6a752a..68766013e 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,7 @@ Currently supported platforms: * iOS * macOS * Solaris +* Emscripten Mio can handle interfacing with each of the event systems of the aforementioned platforms. The details of their implementation are further discussed in the diff --git a/src/lib.rs b/src/lib.rs index 7a84cc013..54b4c201d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,7 +40,10 @@ //! //! The available features are described in the [`features`] module. -#[cfg(all(target_family = "wasm", not(target_os = "wasi")))] +#[cfg(all( + target_family = "wasm", + not(any(target_os = "wasi", target_os = "emscripten")) +))] compile_error!("This wasm target is unsupported by mio. If using Tokio, disable the net feature."); // macros used internally diff --git a/src/net/mod.rs b/src/net/mod.rs index d941d8010..bc26ceb12 100644 --- a/src/net/mod.rs +++ b/src/net/mod.rs @@ -36,4 +36,7 @@ pub use self::udp::UdpSocket; #[cfg(unix)] mod uds; #[cfg(unix)] -pub use self::uds::{UnixDatagram, UnixListener, UnixStream}; +pub use self::uds::{UnixListener, UnixStream}; +// Emscripten's node-backed AF_UNIX is stream-only (no datagram primitive). +#[cfg(all(unix, not(target_os = "emscripten")))] +pub use self::uds::UnixDatagram; diff --git a/src/net/uds/mod.rs b/src/net/uds/mod.rs index e02fd80dc..55b81bf4e 100644 --- a/src/net/uds/mod.rs +++ b/src/net/uds/mod.rs @@ -1,4 +1,6 @@ +#[cfg(not(target_os = "emscripten"))] mod datagram; +#[cfg(not(target_os = "emscripten"))] pub use self::datagram::UnixDatagram; mod listener; diff --git a/src/net/uds/stream.rs b/src/net/uds/stream.rs index 244f40455..549658a76 100644 --- a/src/net/uds/stream.rs +++ b/src/net/uds/stream.rs @@ -52,6 +52,8 @@ impl UnixStream { /// Creates an unnamed pair of connected sockets. /// /// Returns two `UnixStream`s which are connected to each other. + // Emscripten has no `socketpair(2)` (no `uv_socketpair` exposed by node). + #[cfg(not(target_os = "emscripten"))] pub fn pair() -> io::Result<(UnixStream, UnixStream)> { sys::uds::stream::pair().map(|(stream1, stream2)| { (UnixStream::from_std(stream1), UnixStream::from_std(stream2)) diff --git a/src/sys/shell/uds.rs b/src/sys/shell/uds.rs index 446781aed..73f7b548f 100644 --- a/src/sys/shell/uds.rs +++ b/src/sys/shell/uds.rs @@ -1,3 +1,5 @@ +// Emscripten's node-backed AF_UNIX is stream-only (no datagram primitive). +#[cfg(not(target_os = "emscripten"))] pub(crate) mod datagram { use std::io; use std::os::unix::net::{self, SocketAddr}; @@ -38,6 +40,8 @@ pub(crate) mod stream { os_required!() } + // Emscripten has no `socketpair(2)` (no `uv_socketpair` exposed by node). + #[cfg(not(target_os = "emscripten"))] pub(crate) fn pair() -> io::Result<(net::UnixStream, net::UnixStream)> { os_required!() } diff --git a/src/sys/unix/mod.rs b/src/sys/unix/mod.rs index 0f7f87a1a..97f1e991a 100644 --- a/src/sys/unix/mod.rs +++ b/src/sys/unix/mod.rs @@ -19,6 +19,7 @@ cfg_os_poll! { not(mio_unsupported_force_poll_poll), any( target_os = "android", + target_os = "emscripten", target_os = "illumos", target_os = "linux", target_os = "redox", @@ -105,6 +106,7 @@ cfg_os_poll! { ), target_os = "aix", target_os = "dragonfly", + target_os = "emscripten", target_os = "haiku", target_os = "hurd", target_os = "netbsd", @@ -158,6 +160,7 @@ cfg_os_poll! { // NOTE: also add to the list for the `pipe` module below. target_os = "aix", target_os = "dragonfly", + target_os = "emscripten", target_os = "haiku", target_os = "hurd", target_os = "netbsd", diff --git a/src/sys/unix/net.rs b/src/sys/unix/net.rs index 68626746a..cd1d221ec 100644 --- a/src/sys/unix/net.rs +++ b/src/sys/unix/net.rs @@ -56,8 +56,11 @@ pub(crate) fn new_socket(domain: libc::c_int, socket_type: libc::c_int) -> io::R } // Darwin (and others) doesn't have SOCK_NONBLOCK or SOCK_CLOEXEC. + // Emscripten's `socket(2)` silently strips both flags, so set `O_NONBLOCK` + // via `fcntl(2)` instead (`FD_CLOEXEC` is a no-op there). #[cfg(any( target_os = "aix", + target_os = "emscripten", target_os = "ios", target_os = "macos", target_os = "tvos", diff --git a/src/sys/unix/pipe.rs b/src/sys/unix/pipe.rs index 5dfe7501b..cf49955a0 100644 --- a/src/sys/unix/pipe.rs +++ b/src/sys/unix/pipe.rs @@ -11,6 +11,7 @@ pub(crate) fn new_raw() -> io::Result<[RawFd; 2]> { #[cfg(any( target_os = "android", target_os = "dragonfly", + target_os = "emscripten", target_os = "freebsd", target_os = "fuchsia", target_os = "hurd", diff --git a/src/sys/unix/selector/epoll.rs b/src/sys/unix/selector/epoll.rs index 29e36b7e0..134941ef5 100644 --- a/src/sys/unix/selector/epoll.rs +++ b/src/sys/unix/selector/epoll.rs @@ -238,6 +238,9 @@ pub mod event { } // No special requirement from the implementation around waking. +// On emscripten the public `Waker` is not compiled (the runtime wakes through +// its own event loop), so this re-export is unused there. +#[cfg_attr(target_os = "emscripten", allow(unused_imports))] pub(crate) use crate::sys::unix::waker::Waker; cfg_io_source! { diff --git a/src/sys/unix/tcp.rs b/src/sys/unix/tcp.rs index a463cd2fd..581c4b2c0 100644 --- a/src/sys/unix/tcp.rs +++ b/src/sys/unix/tcp.rs @@ -65,6 +65,7 @@ pub(crate) fn accept(listener: &net::TcpListener) -> io::Result<(net::TcpStream, // See https://github.com/tokio-rs/mio/issues/1445 for details all(not(target_arch="x86"), target_os = "android"), target_os = "dragonfly", + target_os = "emscripten", target_os = "freebsd", target_os = "fuchsia", target_os = "hurd", diff --git a/src/sys/unix/uds/mod.rs b/src/sys/unix/uds/mod.rs index a4585e461..309bafe54 100644 --- a/src/sys/unix/uds/mod.rs +++ b/src/sys/unix/uds/mod.rs @@ -1,12 +1,16 @@ +#[cfg(not(target_os = "emscripten"))] +use std::io; #[cfg(target_os = "android")] use std::os::android::net::SocketAddrExt; #[cfg(target_os = "linux")] use std::os::linux::net::SocketAddrExt; use std::os::unix::ffi::OsStrExt; +#[cfg(not(target_os = "emscripten"))] use std::os::unix::io::FromRawFd; use std::os::unix::net::SocketAddr; -use std::{io, mem, ptr}; +use std::{mem, ptr}; +#[cfg(not(target_os = "emscripten"))] pub(crate) mod datagram; pub(crate) mod listener; pub(crate) mod stream; @@ -81,6 +85,7 @@ fn unix_addr(address: &SocketAddr) -> (libc::sockaddr_un, libc::socklen_t) { (sockaddr, addrlen as _) } +#[cfg(not(target_os = "emscripten"))] fn pair(flags: libc::c_int) -> io::Result<(T, T)> where T: FromRawFd, diff --git a/src/sys/unix/uds/stream.rs b/src/sys/unix/uds/stream.rs index dd2b2cab3..60410a1f8 100644 --- a/src/sys/unix/uds/stream.rs +++ b/src/sys/unix/uds/stream.rs @@ -20,6 +20,7 @@ pub(crate) fn connect_addr(address: &SocketAddr) -> io::Result Ok(socket) } +#[cfg(not(target_os = "emscripten"))] pub(crate) fn pair() -> io::Result<(net::UnixStream, net::UnixStream)> { super::pair(libc::SOCK_STREAM) } diff --git a/tests/regressions.rs b/tests/regressions.rs index c41a23b02..2e8f296da 100644 --- a/tests/regressions.rs +++ b/tests/regressions.rs @@ -109,7 +109,7 @@ fn issue_1205() { } #[test] -#[cfg(unix)] +#[cfg(all(unix, not(target_os = "emscripten")))] fn issue_1403() { use mio::net::UnixDatagram; use util::temp_file; diff --git a/tests/tcp_stream.rs b/tests/tcp_stream.rs index d8eb6bf7a..c2335a25b 100644 --- a/tests/tcp_stream.rs +++ b/tests/tcp_stream.rs @@ -452,6 +452,10 @@ fn shutdown_both() { } #[cfg(unix)] +#[cfg_attr( + target_os = "emscripten", + ignore = "local_addr not preserved across from_raw_fd on emscripten" +)] #[test] fn raw_fd() { init(); @@ -624,6 +628,7 @@ fn tcp_shutdown_client_read_close_event() { #[cfg_attr( any( target_os = "android", + target_os = "emscripten", target_os = "hurd", target_os = "illumos", target_os = "solaris", diff --git a/tests/udp_socket.rs b/tests/udp_socket.rs index b0a7cb78d..34fc0fbd5 100644 --- a/tests/udp_socket.rs +++ b/tests/udp_socket.rs @@ -453,6 +453,10 @@ fn smoke_test_connected_udp_socket(mut socket1: UdpSocket, mut socket2: UdpSocke target_os = "wasi", ignore = "temporarily disabled for WASI pending https://github.com/bytecodealliance/wasmtime/pull/12595" )] +#[cfg_attr( + target_os = "emscripten", + ignore = "libuv does not re-associate a connected UDP socket on reconnect" +)] #[test] fn reconnect_udp_socket_sending() { let (mut poll, mut events) = init_with_poll(); @@ -520,6 +524,10 @@ fn reconnect_udp_socket_sending() { target_os = "wasi", ignore = "temporarily disabled for WASI pending https://github.com/bytecodealliance/wasmtime/pull/12595" )] +#[cfg_attr( + target_os = "emscripten", + ignore = "libuv does not re-associate a connected UDP socket on reconnect" +)] #[test] fn reconnect_udp_socket_receiving() { let (mut poll, mut events) = init_with_poll(); @@ -708,6 +716,7 @@ fn connected_udp_socket_unconnected_methods() { target_os = "android", target_os = "hurd", target_os = "linux", + target_os = "emscripten", target_os = "windows", target_os = "cygwin", target_os = "wasi", @@ -720,6 +729,7 @@ fn connected_udp_socket_unconnected_methods() { target_os = "android", target_os = "hurd", target_os = "linux", + target_os = "emscripten", target_os = "windows", target_os = "cygwin", target_os = "wasi", diff --git a/tests/unix_datagram.rs b/tests/unix_datagram.rs index f94ea7624..a4fa17757 100644 --- a/tests/unix_datagram.rs +++ b/tests/unix_datagram.rs @@ -1,4 +1,10 @@ -#![cfg(all(unix, feature = "os-poll", feature = "net"))] +// Emscripten's node-backed AF_UNIX is stream-only (no datagram sockets). +#![cfg(all( + unix, + not(target_os = "emscripten"), + feature = "os-poll", + feature = "net" +))] use mio::net::UnixDatagram; use mio::{Interest, Token}; diff --git a/tests/unix_pipe.rs b/tests/unix_pipe.rs index 73d5b7fc8..8736c285b 100644 --- a/tests/unix_pipe.rs +++ b/tests/unix_pipe.rs @@ -1,12 +1,15 @@ #![cfg(all(unix, feature = "os-poll", feature = "os-ext", feature = "net"))] use std::io::{Read, Write}; +#[cfg(not(target_os = "emscripten"))] use std::process::{Command, Stdio}; use std::sync::{Arc, Barrier}; use std::thread; use std::time::Duration; -use mio::unix::pipe::{self, Receiver, Sender}; +use mio::unix::pipe; +#[cfg(not(target_os = "emscripten"))] +use mio::unix::pipe::{Receiver, Sender}; use mio::{Events, Interest, Poll, Token}; mod util; @@ -132,6 +135,8 @@ fn event_when_receiver_is_dropped() { } #[test] +// Emscripten/wasm has no fork/exec, so no subprocesses. +#[cfg(not(target_os = "emscripten"))] #[cfg_attr( any(target_os = "hurd", target_os = "nto", target_os = "cygwin"), ignore = "Writer fd close events do not trigger POLLHUP on nto and GNU/Hurd targets" @@ -183,6 +188,7 @@ fn from_child_process_io() { } #[test] +#[cfg(not(target_os = "emscripten"))] fn nonblocking_child_process_io() { // `cat` simply echo everything that we write via standard in. let mut child = Command::new("cat") diff --git a/tests/unix_stream.rs b/tests/unix_stream.rs index 770baddd7..e4c4161fa 100644 --- a/tests/unix_stream.rs +++ b/tests/unix_stream.rs @@ -24,6 +24,7 @@ const DATA1_LEN: usize = 16; const DATA2_LEN: usize = 14; const DEFAULT_BUF_SIZE: usize = 64; const TOKEN_1: Token = Token(0); +#[cfg(not(target_os = "emscripten"))] const TOKEN_2: Token = Token(1); #[test] @@ -145,6 +146,8 @@ fn unix_stream_from_std() { ) } +// Emscripten has no `socketpair(2)`. +#[cfg(not(target_os = "emscripten"))] #[test] fn unix_stream_pair() { let (mut poll, mut events) = init_with_poll(); diff --git a/tests/util/mod.rs b/tests/util/mod.rs index c7b5e3d00..fd6b5d89f 100644 --- a/tests/util/mod.rs +++ b/tests/util/mod.rs @@ -226,7 +226,7 @@ pub fn assert_socket_non_blocking(_: &S) { } /// Assert that `CLOEXEC` is set on `socket`. -#[cfg(unix)] +#[cfg(all(unix, not(target_os = "emscripten")))] pub fn assert_socket_close_on_exec(socket: &S) where S: AsRawFd, @@ -235,6 +235,11 @@ where assert!(flags & libc::FD_CLOEXEC != 0, "socket flag CLOEXEC not set"); } +// Emscripten is a single process with no `exec(2)`, so `FD_CLOEXEC` is a no-op +// (`F_GETFD` always returns 0); the concept doesn't apply. +#[cfg(target_os = "emscripten")] +pub fn assert_socket_close_on_exec(_: &S) {} + #[cfg(windows)] pub fn assert_socket_close_on_exec(_: &S) { // Windows doesn't have this concept.