diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 552fae963..dff963739 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -142,19 +142,14 @@ jobs: name: test wasm32-unknown-unknown runs-on: ubuntu-latest steps: - - name: Checkout sources - uses: actions/checkout@v4 - - - name: Install stable toolchain - uses: dtolnay/rust-toolchain@stable - - - name: Add wasm target - run: rustup target add wasm32-unknown-unknown - - - name: Install nodejs v20 - uses: actions/setup-node@v4 + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - run: rustup target add wasm32-unknown-unknown + - uses: actions/setup-node@v4 with: node-version: 20 + - uses: bytecodealliance/actions/wasm-tools/setup@v1 + - uses: cargo-bins/cargo-binstall@main - name: Setup `wasm-tools` uses: bytecodealliance/actions/wasm-tools/setup@v1 @@ -162,20 +157,21 @@ jobs: - name: Install cargo binstall uses: cargo-bins/cargo-binstall@main - - name: build wasm32 tests (quinn-proto) - run: cargo test -p quinn-proto --target wasm32-unknown-unknown --no-run + - run: cargo test -p quinn-proto --target wasm32-unknown-unknown --no-run + - run: cargo check -p quinn-udp --target wasm32-unknown-unknown --no-default-features --features=tracing,log + - run: cargo rustc -p quinn --target wasm32-unknown-unknown --no-default-features --features=rustls --crate-type=cdylib # If the Wasm file contains any 'import "env"' declarations, then # some non-Wasm-compatible code made it into the final code. - - name: Check for 'import "env"' in Wasm + - name: Ensure no 'import "env"' in quinn_proto Wasm run: | ! wasm-tools print --skeleton target/wasm32-unknown-unknown/debug/deps/quinn_proto-*.wasm | grep 'import "env"' + - name: Ensure no 'import "env"' in quinn Wasm + run: | + ! wasm-tools print --skeleton target/wasm32-unknown-unknown/debug/quinn.wasm | grep 'import "env"' - - name: Install wasm-bindgen-test-runner - run: cargo binstall wasm-bindgen-cli --locked --no-confirm - - - name: wasm32 test (quinn-proto) - run: cargo test -p quinn-proto --target wasm32-unknown-unknown + - run: cargo binstall wasm-bindgen-cli --locked --no-confirm + - run: cargo test -p quinn-proto --target wasm32-unknown-unknown msrv: runs-on: ubuntu-latest diff --git a/quinn-udp/Cargo.toml b/quinn-udp/Cargo.toml index 6a1a25e77..c46172d19 100644 --- a/quinn-udp/Cargo.toml +++ b/quinn-udp/Cargo.toml @@ -11,23 +11,28 @@ categories.workspace = true workspace = ".." [features] -default = ["tracing", "log"] +default = ["tracing", "log", "net"] # Configure `tracing` to log events via `log` if no `tracing` subscriber exists. log = ["tracing/log"] direct-log = ["dep:log"] # Use private Apple APIs to send multiple packets in a single syscall. fast-apple-datapath = [] +# Enables dependencies on native sockets. Disabling allows compiling to wasm. +net = ["dep:socket2"] [dependencies] libc = "0.2.158" log = { workspace = true, optional = true } -socket2 = { workspace = true } +socket2 = { workspace = true, optional = true } tracing = { workspace = true, optional = true } [target.'cfg(windows)'.dependencies] once_cell = { workspace = true } windows-sys = { workspace = true } +[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies] +web-time = { workspace = true } + [dev-dependencies] criterion = { version = "0.5", default-features = false, features = ["async_tokio"] } tokio = { workspace = true, features = ["rt", "rt-multi-thread", "net"] } diff --git a/quinn-udp/src/lib.rs b/quinn-udp/src/lib.rs index 606dcc4a7..42c80e2ab 100644 --- a/quinn-udp/src/lib.rs +++ b/quinn-udp/src/lib.rs @@ -27,29 +27,35 @@ #![warn(unreachable_pub)] #![warn(clippy::use_self)] -#[cfg(unix)] +use std::net::{IpAddr, Ipv6Addr, SocketAddr}; +#[cfg(all(feature = "net", unix))] use std::os::unix::io::AsFd; -#[cfg(windows)] +#[cfg(all(feature = "net", windows))] use std::os::windows::io::AsSocket; -use std::{ - net::{IpAddr, Ipv6Addr, SocketAddr}, - sync::Mutex, - time::{Duration, Instant}, -}; - -#[cfg(any(unix, windows))] +#[cfg(feature = "net")] +use std::sync::Mutex; +// Allowing unused, otherwise the cfg condition complexity gets +// out of control only to skip an unused `Duration` import. +#[allow(unused)] +#[cfg(not(all(target_family = "wasm", target_os = "unknown")))] +use std::time::{Duration, Instant}; +#[allow(unused)] +#[cfg(all(target_family = "wasm", target_os = "unknown"))] +use web_time::{Duration, Instant}; + +#[cfg(all(feature = "net", any(unix, windows)))] mod cmsg; -#[cfg(unix)] +#[cfg(all(feature = "net", unix))] #[path = "unix.rs"] mod imp; -#[cfg(windows)] +#[cfg(all(feature = "net", windows))] #[path = "windows.rs"] mod imp; // No ECN support -#[cfg(not(any(unix, windows)))] +#[cfg(all(feature = "net", not(any(unix, windows))))] #[path = "fallback.rs"] mod imp; @@ -76,10 +82,15 @@ mod log { pub(crate) use no_op::*; } +#[cfg(feature = "net")] pub use imp::UdpSocketState; /// Number of UDP packets to send/receive at a time +#[cfg(feature = "net")] pub const BATCH_SIZE: usize = imp::BATCH_SIZE; +/// Number of UDP packets to send/receive at a time +#[cfg(not(feature = "net"))] +pub const BATCH_SIZE: usize = 1; /// Metadata for a single buffer filled with bytes received from the network /// @@ -141,13 +152,14 @@ pub struct Transmit<'a> { } /// Log at most 1 IO error per minute +#[cfg(feature = "net")] const IO_ERROR_LOG_INTERVAL: Duration = std::time::Duration::from_secs(60); /// Logs a warning message when sendmsg fails /// /// Logging will only be performed if at least [`IO_ERROR_LOG_INTERVAL`] /// has elapsed since the last error was logged. -#[cfg(any(feature = "tracing", feature = "direct-log"))] +#[cfg(all(feature = "net", any(feature = "tracing", feature = "direct-log")))] fn log_sendmsg_error( last_send_error: &Mutex, err: impl core::fmt::Debug, @@ -164,7 +176,7 @@ fn log_sendmsg_error( } // No-op -#[cfg(not(any(feature = "tracing", feature = "direct-log")))] +#[cfg(all(feature = "net", not(any(feature = "tracing", feature = "direct-log"))))] fn log_sendmsg_error(_: &Mutex, _: impl core::fmt::Debug, _: &Transmit) {} /// A borrowed UDP socket @@ -172,9 +184,10 @@ fn log_sendmsg_error(_: &Mutex, _: impl core::fmt::Debug, _: &Transmit) /// On Unix, constructible via `From`. On Windows, constructible via `From`. // Wrapper around socket2 to avoid making it a public dependency and incurring stability risk +#[cfg(feature = "net")] pub struct UdpSockRef<'a>(socket2::SockRef<'a>); -#[cfg(unix)] +#[cfg(all(feature = "net", unix))] impl<'s, S> From<&'s S> for UdpSockRef<'s> where S: AsFd, @@ -184,7 +197,7 @@ where } } -#[cfg(windows)] +#[cfg(all(feature = "net", windows))] impl<'s, S> From<&'s S> for UdpSockRef<'s> where S: AsSocket, diff --git a/quinn/Cargo.toml b/quinn/Cargo.toml index a061520d7..e14ab49a5 100644 --- a/quinn/Cargo.toml +++ b/quinn/Cargo.toml @@ -12,7 +12,7 @@ edition.workspace = true rust-version.workspace = true [features] -default = ["log", "platform-verifier", "runtime-tokio", "rustls-ring"] +default = ["log", "platform-verifier", "net", "runtime-tokio", "rustls-ring"] # Enables `Endpoint::client` and `Endpoint::server` conveniences aws-lc-rs = ["proto/aws-lc-rs"] aws-lc-rs-fips = ["proto/aws-lc-rs-fips"] @@ -27,11 +27,15 @@ rustls-aws-lc-rs = ["dep:rustls", "aws-lc-rs", "proto/rustls-aws-lc-rs", "proto/ rustls-aws-lc-rs-fips = ["dep:rustls", "aws-lc-rs-fips", "proto/rustls-aws-lc-rs-fips", "proto/aws-lc-rs-fips"] # Enable rustls with the `ring` crypto provider rustls-ring = ["dep:rustls", "ring", "proto/rustls-ring", "proto/ring"] -# Enables `Endpoint::client` and `Endpoint::server` conveniences +# Enable the `ring` crypto provider. +# Together with the `net` feature, this enables `Endpoint::client` and `Endpoint::server` conveniences. ring = ["proto/ring"] -runtime-tokio = ["tokio/time", "tokio/rt", "tokio/net"] -runtime-async-std = ["async-io", "async-std"] -runtime-smol = ["async-io", "smol"] +runtime-tokio = ["net", "tokio/time", "tokio/rt", "tokio/net"] +runtime-async-std = ["net", "async-io", "async-std"] +runtime-smol = ["net", "async-io", "smol"] +# Enables dependencies on native sockets. Required for `Endpoint::client` and `Endpoint::server` conveniences. +# Disabling allows compiling to wasm. +net = ["dep:socket2", "udp/net"] # Configure `tracing` to log events via `log` if no `tracing` subscriber exists. log = ["tracing/log", "proto/log", "udp/log"] @@ -49,12 +53,15 @@ pin-project-lite = { workspace = true } proto = { package = "quinn-proto", path = "../quinn-proto", version = "0.11.7", default-features = false } rustls = { workspace = true, optional = true } smol = { workspace = true, optional = true } -socket2 = { workspace = true } +socket2 = { workspace = true, optional = true } thiserror = { workspace = true } tracing = { workspace = true } tokio = { workspace = true } udp = { package = "quinn-udp", path = "../quinn-udp", version = "0.5", default-features = false, features = ["tracing"] } +[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies] +web-time = { workspace = true } + [dev-dependencies] anyhow = { workspace = true } crc = { workspace = true } diff --git a/quinn/src/connection.rs b/quinn/src/connection.rs index cf09c6e67..e6bd8230c 100644 --- a/quinn/src/connection.rs +++ b/quinn/src/connection.rs @@ -7,7 +7,6 @@ use std::{ pin::Pin, sync::Arc, task::{Context, Poll, Waker}, - time::{Duration, Instant}, }; use bytes::Bytes; @@ -22,7 +21,7 @@ use crate::{ recv_stream::RecvStream, runtime::{AsyncTimer, AsyncUdpSocket, Runtime, UdpPoller}, send_stream::SendStream, - udp_transmit, ConnectionEvent, VarInt, + udp_transmit, ConnectionEvent, Duration, Instant, VarInt, }; use proto::{ congestion::Controller, ConnectionError, ConnectionHandle, ConnectionStats, Dir, EndpointEvent, diff --git a/quinn/src/endpoint.rs b/quinn/src/endpoint.rs index 2563f3771..ecc7d3770 100644 --- a/quinn/src/endpoint.rs +++ b/quinn/src/endpoint.rs @@ -9,14 +9,13 @@ use std::{ str, sync::{Arc, Mutex}, task::{Context, Poll, Waker}, - time::Instant, }; -#[cfg(any(feature = "aws-lc-rs", feature = "ring"))] +#[cfg(all(feature = "net", any(feature = "aws-lc-rs", feature = "ring")))] use crate::runtime::default_runtime; use crate::{ runtime::{AsyncUdpSocket, Runtime}, - udp_transmit, + udp_transmit, Instant, }; use bytes::{Bytes, BytesMut}; use pin_project_lite::pin_project; @@ -25,7 +24,7 @@ use proto::{ EndpointEvent, ServerConfig, }; use rustc_hash::FxHashMap; -#[cfg(any(feature = "aws-lc-rs", feature = "ring"))] +#[cfg(all(feature = "net", any(feature = "aws-lc-rs", feature = "ring"),))] use socket2::{Domain, Protocol, Socket, Type}; use tokio::sync::{futures::Notified, mpsc, Notify}; use tracing::{Instrument, Span}; @@ -67,7 +66,7 @@ impl Endpoint { /// /// Some environments may not allow creation of dual-stack sockets, in which case an IPv6 /// client will only be able to connect to IPv6 servers. An IPv4 client is never dual-stack. - #[cfg(any(feature = "aws-lc-rs", feature = "ring"))] // `EndpointConfig::default()` is only available with these + #[cfg(all(feature = "net", any(feature = "aws-lc-rs", feature = "ring")))] // `EndpointConfig::default()` is only available with these pub fn client(addr: SocketAddr) -> io::Result { let socket = Socket::new(Domain::for_address(addr), Type::DGRAM, Some(Protocol::UDP))?; if addr.is_ipv6() { @@ -97,7 +96,7 @@ impl Endpoint { /// IPv6 address on Windows will not by default be able to communicate with IPv4 /// addresses. Portable applications should bind an address that matches the family they wish to /// communicate within. - #[cfg(any(feature = "aws-lc-rs", feature = "ring"))] // `EndpointConfig::default()` is only available with these + #[cfg(all(feature = "net", any(feature = "aws-lc-rs", feature = "ring")))] // `EndpointConfig::default()` is only available with these pub fn server(config: ServerConfig, addr: SocketAddr) -> io::Result { let socket = std::net::UdpSocket::bind(addr)?; let runtime = default_runtime() @@ -111,6 +110,7 @@ impl Endpoint { } /// Construct an endpoint with arbitrary configuration and socket + #[cfg(feature = "net")] pub fn new( config: EndpointConfig, server_config: Option, @@ -234,6 +234,7 @@ impl Endpoint { /// Switch to a new UDP socket /// /// See [`Endpoint::rebind_abstract()`] for details. + #[cfg(feature = "net")] pub fn rebind(&self, socket: std::net::UdpSocket) -> io::Result<()> { self.rebind_abstract(self.runtime.wrap_udp_socket(socket)?) } diff --git a/quinn/src/lib.rs b/quinn/src/lib.rs index 84a3821f6..c365fe757 100644 --- a/quinn/src/lib.rs +++ b/quinn/src/lib.rs @@ -41,7 +41,7 @@ #![warn(unreachable_pub)] #![warn(clippy::use_self)] -use std::{sync::Arc, time::Duration}; +use std::sync::Arc; macro_rules! ready { ($e:expr $(,)?) => { @@ -61,6 +61,11 @@ mod runtime; mod send_stream; mod work_limiter; +#[cfg(not(all(target_family = "wasm", target_os = "unknown")))] +pub(crate) use std::time::{Duration, Instant}; +#[cfg(all(target_family = "wasm", target_os = "unknown"))] +pub(crate) use web_time::{Duration, Instant}; + pub use proto::{ congestion, crypto, AckFrequencyConfig, ApplicationClose, Chunk, ClientConfig, ClosedStream, ConfigError, ConnectError, ConnectionClose, ConnectionError, ConnectionId, diff --git a/quinn/src/mutex.rs b/quinn/src/mutex.rs index a2a1fe277..913566922 100644 --- a/quinn/src/mutex.rs +++ b/quinn/src/mutex.rs @@ -6,10 +6,8 @@ use std::{ #[cfg(feature = "lock_tracking")] mod tracking { use super::*; - use std::{ - collections::VecDeque, - time::{Duration, Instant}, - }; + use crate::{Duration, Instant}; + use std::collections::VecDeque; use tracing::warn; #[derive(Debug)] diff --git a/quinn/src/runtime.rs b/quinn/src/runtime.rs index 0bf30b568..eb1a4ff1a 100644 --- a/quinn/src/runtime.rs +++ b/quinn/src/runtime.rs @@ -6,11 +6,12 @@ use std::{ pin::Pin, sync::Arc, task::{Context, Poll}, - time::Instant, }; use udp::{RecvMeta, Transmit}; +use crate::Instant; + /// Abstracts I/O and timer operations for runtime independence pub trait Runtime: Send + Sync + Debug + 'static { /// Construct a timer that will expire at `i` @@ -18,6 +19,7 @@ pub trait Runtime: Send + Sync + Debug + 'static { /// Drive `future` to completion in the background fn spawn(&self, future: Pin + Send>>); /// Convert `t` into the socket type used by this runtime + #[cfg(feature = "net")] fn wrap_udp_socket(&self, t: std::net::UdpSocket) -> io::Result>; /// Look up the current time /// diff --git a/quinn/src/tests.rs b/quinn/src/tests.rs index 8887af1c2..1f15d9e83 100755 --- a/quinn/src/tests.rs +++ b/quinn/src/tests.rs @@ -14,6 +14,7 @@ use std::{ }; use crate::runtime::TokioRuntime; +use crate::{Duration, Instant}; use bytes::Bytes; use proto::{crypto::rustls::QuicClientConfig, RandomConnectionIdGenerator}; use rand::{rngs::StdRng, RngCore, SeedableRng}; @@ -21,10 +22,7 @@ use rustls::{ pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer}, RootCertStore, }; -use tokio::{ - runtime::{Builder, Runtime}, - time::{Duration, Instant}, -}; +use tokio::runtime::{Builder, Runtime}; use tracing::{error_span, info}; use tracing_futures::Instrument as _; use tracing_subscriber::EnvFilter; @@ -160,7 +158,7 @@ fn read_after_close() { .unwrap() .await .expect("connect"); - tokio::time::sleep_until(Instant::now() + Duration::from_millis(100)).await; + tokio::time::sleep(Duration::from_millis(100)).await; let mut stream = new_conn.accept_uni().await.expect("incoming streams"); let msg = stream.read_to_end(usize::MAX).await.expect("read_to_end"); assert_eq!(msg, MSG); diff --git a/quinn/src/work_limiter.rs b/quinn/src/work_limiter.rs index efffd2cca..c3c3d3551 100644 --- a/quinn/src/work_limiter.rs +++ b/quinn/src/work_limiter.rs @@ -1,4 +1,4 @@ -use std::time::{Duration, Instant}; +use crate::{Duration, Instant}; /// Limits the amount of time spent on a certain type of work in a cycle ///