├── src ├── ecosystem │ ├── http │ │ ├── middleware.rs │ │ ├── client.rs │ │ ├── mime.rs │ │ ├── mod.rs │ │ ├── server │ │ │ ├── fake_client.rs │ │ │ ├── mod.rs │ │ │ └── route.rs │ │ └── payload.rs │ ├── nats │ │ ├── mod.rs │ │ └── proto.rs │ └── mod.rs ├── sync │ ├── mod.rs │ └── channel.rs ├── runtime │ ├── context_switch.rs │ ├── tls.rs │ ├── syscall.rs │ ├── assembly │ │ └── x86_64.s │ ├── stack.rs │ └── mod.rs ├── time.rs ├── lib.rs ├── net │ ├── mod.rs │ └── tcp.rs ├── circular_buffer.rs └── fs.rs ├── .gitignore ├── tests ├── fail_compile.rs └── http │ ├── return_body_then_code.rs │ ├── return_body_then_part.rs │ ├── return_part_then_code.rs │ ├── take_two_requests.rs │ ├── return_two_status_codes.rs │ ├── take_request_then_part.rs │ ├── take_request_then_part.stderr │ ├── take_two_requests.stderr │ ├── return_body_then_code.stderr │ ├── return_body_then_part.stderr │ ├── return_part_then_code.stderr │ └── return_two_status_codes.stderr ├── examples └── hello_world.rs ├── macros ├── Cargo.toml └── src │ └── lib.rs ├── .github └── workflows │ ├── uringy-macros.yaml │ └── uringy.yaml ├── LICENSE ├── Cargo.toml └── README.md /src/ecosystem/http/middleware.rs: -------------------------------------------------------------------------------- 1 | //! ... 2 | -------------------------------------------------------------------------------- /src/sync/mod.rs: -------------------------------------------------------------------------------- 1 | //! ... 2 | 3 | pub mod channel; 4 | -------------------------------------------------------------------------------- /src/ecosystem/nats/mod.rs: -------------------------------------------------------------------------------- 1 | //! ... 2 | 3 | mod proto; 4 | 5 | -------------------------------------------------------------------------------- /src/ecosystem/http/client.rs: -------------------------------------------------------------------------------- 1 | //! ... 2 | 3 | // TODO: 1:1 mirror of fake client 4 | -------------------------------------------------------------------------------- /src/ecosystem/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "http")] 2 | pub mod http; 3 | 4 | #[cfg(feature = "nats")] 5 | pub mod nats; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | 3 | # Libraries should ignore the lock file 4 | Cargo.lock 5 | 6 | # IDE garbage 7 | .idea 8 | -------------------------------------------------------------------------------- /tests/fail_compile.rs: -------------------------------------------------------------------------------- 1 | // run with `cargo test --test '*' --features http` 2 | 3 | #[test] 4 | fn http() { 5 | // let t = trybuild::TestCases::new(); 6 | // t.compile_fail("tests/http/*.rs"); 7 | } 8 | -------------------------------------------------------------------------------- /tests/http/return_body_then_code.rs: -------------------------------------------------------------------------------- 1 | use http::StatusCode; 2 | use uringy::ecosystem::http::server::routing::{get, Router}; 3 | 4 | fn main() { 5 | Router::new().route("/", get(|| ("hello", StatusCode::OK))); 6 | } 7 | -------------------------------------------------------------------------------- /tests/http/return_body_then_part.rs: -------------------------------------------------------------------------------- 1 | use http::HeaderMap; 2 | 3 | use uringy::ecosystem::http::server::routing::{get, Router}; 4 | 5 | fn main() { 6 | Router::new().route("/", get(|| ("hello", HeaderMap::new()))); 7 | } 8 | -------------------------------------------------------------------------------- /tests/http/return_part_then_code.rs: -------------------------------------------------------------------------------- 1 | use http::{HeaderMap, StatusCode}; 2 | use uringy::ecosystem::http::server::routing::{get, Router}; 3 | 4 | fn main() { 5 | Router::new().route("/", get(|| (HeaderMap::new(), StatusCode::OK))); 6 | } 7 | -------------------------------------------------------------------------------- /tests/http/take_two_requests.rs: -------------------------------------------------------------------------------- 1 | use uringy::ecosystem::http::server::routing::{get, Router}; 2 | use uringy::ecosystem::http::Request; 3 | 4 | fn main() { 5 | fn root(_: Request, _: Request) {} 6 | Router::new().route("/", get(root)); 7 | } 8 | -------------------------------------------------------------------------------- /tests/http/return_two_status_codes.rs: -------------------------------------------------------------------------------- 1 | use http::StatusCode; 2 | use uringy::ecosystem::http::server::routing::{get, Router}; 3 | 4 | fn main() { 5 | // this is valid in Axum 6 | Router::new().route("/", get(|| (StatusCode::OK, StatusCode::OK))); 7 | } 8 | -------------------------------------------------------------------------------- /examples/hello_world.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | use std::os::fd::{AsRawFd, FromRawFd}; 3 | 4 | #[uringy::start] 5 | fn main() { 6 | let mut stdout = unsafe { uringy::fs::File::from_raw_fd(std::io::stdout().as_raw_fd()) }; 7 | 8 | stdout.write_all(b"hello world").unwrap(); 9 | } 10 | -------------------------------------------------------------------------------- /tests/http/take_request_then_part.rs: -------------------------------------------------------------------------------- 1 | use uringy::ecosystem::http::server::from_request::Query; 2 | use uringy::ecosystem::http::server::routing::{get, Router}; 3 | use uringy::ecosystem::http::Request; 4 | 5 | fn main() { 6 | fn root(_: Request, _: Query<()>) {} 7 | Router::new().route("/", get(root)); 8 | } 9 | -------------------------------------------------------------------------------- /macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "uringy-macros" 3 | version = "0.2.0" 4 | documentation = "https://docs.rs/uringy" 5 | repository = "https://github.com/Dennis-Krasnov/Uringy" 6 | homepage = "https://github.com/Dennis-Krasnov/Uringy" 7 | description = "Procedural macros for the uringy crate." 8 | license = "MIT" 9 | authors = ["Dennis Krasnov "] 10 | edition = "2021" 11 | 12 | [lib] 13 | proc-macro = true 14 | 15 | [dependencies] 16 | syn = { version = "2.0.29", features = ["full"] } 17 | quote = "1.0" 18 | -------------------------------------------------------------------------------- /.github/workflows/uringy-macros.yaml: -------------------------------------------------------------------------------- 1 | name: Uringy Macros CI/CD 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ master ] 7 | paths: 8 | - macros/** 9 | 10 | jobs: 11 | unit_test: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v3 16 | 17 | - name: Cache Rust 18 | uses: Swatinem/rust-cache@v2 19 | with: 20 | key: macros-unit-test 21 | 22 | - name: Unit test 23 | run: cargo test --package uringy-macros 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2023 Dennis Krasnov 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. 4 | 5 | THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 6 | -------------------------------------------------------------------------------- /macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | 3 | use quote::quote; 4 | use syn::{parse_macro_input, ItemFn}; 5 | 6 | #[proc_macro_attribute] 7 | pub fn start(_attr: TokenStream, item: TokenStream) -> TokenStream { 8 | let item = parse_macro_input!(item as ItemFn); 9 | 10 | let attributes = &item.attrs; 11 | let visibility = &item.vis; 12 | let signature = &item.sig; 13 | let body = &item.block; 14 | 15 | let result = quote! { 16 | #(#attributes)* 17 | #visibility #signature { 18 | ::uringy::runtime::start(move || #body).unwrap(); 19 | } 20 | }; 21 | 22 | result.into() 23 | } 24 | -------------------------------------------------------------------------------- /tests/http/take_request_then_part.stderr: -------------------------------------------------------------------------------- 1 | error[E0277]: the trait bound `fn(uringy::ecosystem::http::Request, Query<()>) {root}: Handler<_>` is not satisfied 2 | --> tests/http/take_request_then_part.rs:7:34 3 | | 4 | 7 | Router::new().route("/", get(root)); 5 | | --- ^^^^ the trait `Handler<_>` is not implemented for fn item `fn(uringy::ecosystem::http::Request, Query<()>) {root}` 6 | | | 7 | | required by a bound introduced by this call 8 | | 9 | note: required by a bound in `uringy::ecosystem::http::server::routing::get` 10 | --> src/ecosystem/http/server/routing.rs 11 | | 12 | | pub fn get(handler: impl Handler + 'static) -> MethodRouter { 13 | | ^^^^^^^^^^^^^ required by this bound in `get` 14 | -------------------------------------------------------------------------------- /tests/http/take_two_requests.stderr: -------------------------------------------------------------------------------- 1 | error[E0277]: the trait bound `fn(uringy::ecosystem::http::Request, uringy::ecosystem::http::Request) {root}: Handler<_>` is not satisfied 2 | --> tests/http/take_two_requests.rs:6:34 3 | | 4 | 6 | Router::new().route("/", get(root)); 5 | | --- ^^^^ the trait `Handler<_>` is not implemented for fn item `fn(uringy::ecosystem::http::Request, uringy::ecosystem::http::Request) {root}` 6 | | | 7 | | required by a bound introduced by this call 8 | | 9 | note: required by a bound in `uringy::ecosystem::http::server::routing::get` 10 | --> src/ecosystem/http/server/routing.rs 11 | | 12 | | pub fn get(handler: impl Handler + 'static) -> MethodRouter { 13 | | ^^^^^^^^^^^^^ required by this bound in `get` 14 | -------------------------------------------------------------------------------- /tests/http/return_body_then_code.stderr: -------------------------------------------------------------------------------- 1 | error[E0277]: the trait bound `[closure@$DIR/tests/http/return_body_then_code.rs:5:34: 5:36]: Handler<_>` is not satisfied 2 | --> tests/http/return_body_then_code.rs:5:34 3 | | 4 | 5 | Router::new().route("/", get(|| ("hello", StatusCode::OK))); 5 | | --- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Handler<_>` is not implemented for closure `[closure@$DIR/tests/http/return_body_then_code.rs:5:34: 5:36]` 6 | | | 7 | | required by a bound introduced by this call 8 | | 9 | note: required by a bound in `uringy::ecosystem::http::server::routing::get` 10 | --> src/ecosystem/http/server/routing.rs 11 | | 12 | | pub fn get(handler: impl Handler + 'static) -> MethodRouter { 13 | | ^^^^^^^^^^^^^ required by this bound in `get` 14 | -------------------------------------------------------------------------------- /tests/http/return_body_then_part.stderr: -------------------------------------------------------------------------------- 1 | error[E0277]: the trait bound `[closure@$DIR/tests/http/return_body_then_part.rs:6:34: 6:36]: Handler<_>` is not satisfied 2 | --> tests/http/return_body_then_part.rs:6:34 3 | | 4 | 6 | Router::new().route("/", get(|| ("hello", HeaderMap::new()))); 5 | | --- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Handler<_>` is not implemented for closure `[closure@$DIR/tests/http/return_body_then_part.rs:6:34: 6:36]` 6 | | | 7 | | required by a bound introduced by this call 8 | | 9 | note: required by a bound in `uringy::ecosystem::http::server::routing::get` 10 | --> src/ecosystem/http/server/routing.rs 11 | | 12 | | pub fn get(handler: impl Handler + 'static) -> MethodRouter { 13 | | ^^^^^^^^^^^^^ required by this bound in `get` 14 | -------------------------------------------------------------------------------- /tests/http/return_part_then_code.stderr: -------------------------------------------------------------------------------- 1 | error[E0277]: the trait bound `[closure@$DIR/tests/http/return_part_then_code.rs:5:34: 5:36]: Handler<_>` is not satisfied 2 | --> tests/http/return_part_then_code.rs:5:34 3 | | 4 | 5 | Router::new().route("/", get(|| (HeaderMap::new(), StatusCode::OK))); 5 | | --- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Handler<_>` is not implemented for closure `[closure@$DIR/tests/http/return_part_then_code.rs:5:34: 5:36]` 6 | | | 7 | | required by a bound introduced by this call 8 | | 9 | note: required by a bound in `uringy::ecosystem::http::server::routing::get` 10 | --> src/ecosystem/http/server/routing.rs 11 | | 12 | | pub fn get(handler: impl Handler + 'static) -> MethodRouter { 13 | | ^^^^^^^^^^^^^ required by this bound in `get` 14 | -------------------------------------------------------------------------------- /tests/http/return_two_status_codes.stderr: -------------------------------------------------------------------------------- 1 | error[E0277]: the trait bound `[closure@$DIR/tests/http/return_two_status_codes.rs:6:34: 6:36]: Handler<_>` is not satisfied 2 | --> tests/http/return_two_status_codes.rs:6:34 3 | | 4 | 6 | Router::new().route("/", get(|| (StatusCode::OK, StatusCode::OK))); 5 | | --- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Handler<_>` is not implemented for closure `[closure@$DIR/tests/http/return_two_status_codes.rs:6:34: 6:36]` 6 | | | 7 | | required by a bound introduced by this call 8 | | 9 | note: required by a bound in `uringy::ecosystem::http::server::routing::get` 10 | --> src/ecosystem/http/server/routing.rs 11 | | 12 | | pub fn get(handler: impl Handler + 'static) -> MethodRouter { 13 | | ^^^^^^^^^^^^^ required by this bound in `get` 14 | -------------------------------------------------------------------------------- /src/runtime/context_switch.rs: -------------------------------------------------------------------------------- 1 | //! Abstraction over userspace multitasking. 2 | //! 3 | //! Provides an implementation for every CPU architecture. 4 | 5 | use std::arch::global_asm; 6 | use std::ffi; 7 | 8 | /// Handle to a stack pointer set up for context switching. 9 | #[repr(transparent)] 10 | #[derive(Debug, Copy, Clone)] 11 | pub(super) struct Continuation(*const ()); 12 | 13 | extern "C" { 14 | /// Initializes a stack for context switching. 15 | pub(super) fn prepare_stack( 16 | stack: *mut ffi::c_void, 17 | func: extern "C" fn() -> !, 18 | ) -> Continuation; 19 | 20 | /// Executes a context switch. 21 | /// 22 | /// Spills registers, sets [from] to updated stack pointer. 23 | /// Sets stack pointer to [to], restores registers. 24 | pub(super) fn jump(from: *mut Continuation, to: *const Continuation); 25 | } 26 | 27 | #[cfg(not(target_arch = "x86_64"))] 28 | compile_error!("Uringy only supports x86_64"); 29 | 30 | #[cfg(target_arch = "x86_64")] 31 | global_asm!(include_str!("assembly/x86_64.s")); 32 | -------------------------------------------------------------------------------- /src/ecosystem/http/mime.rs: -------------------------------------------------------------------------------- 1 | //! ... 2 | 3 | use crate::ecosystem::http::payload::AsBody; 4 | 5 | /// ... 6 | pub struct Html(pub T); 7 | 8 | impl AsBody for Html { 9 | fn contents(&self) -> &[u8] { 10 | self.0.contents() 11 | } 12 | 13 | fn content_type(&self) -> Option<&str> { 14 | Some("text/html; charset=utf-8") 15 | } 16 | } 17 | 18 | /// ... 19 | pub struct JavaScript(pub T); 20 | 21 | impl AsBody for JavaScript { 22 | fn contents(&self) -> &[u8] { 23 | self.0.contents() 24 | } 25 | 26 | fn content_type(&self) -> Option<&str> { 27 | Some("text/javascript") 28 | } 29 | } 30 | 31 | /// ... 32 | pub struct Css(pub T); 33 | 34 | impl AsBody for Css { 35 | fn contents(&self) -> &[u8] { 36 | self.0.contents() 37 | } 38 | 39 | fn content_type(&self) -> Option<&str> { 40 | Some("text/css") 41 | } 42 | } 43 | 44 | /// ... 45 | pub struct Png(pub T); 46 | 47 | impl AsBody for Png { 48 | fn contents(&self) -> &[u8] { 49 | self.0.contents() 50 | } 51 | 52 | fn content_type(&self) -> Option<&str> { 53 | Some("image/png") 54 | } 55 | } 56 | 57 | /// ... 58 | pub struct Woff2(pub T); 59 | 60 | impl AsBody for Woff2 { 61 | fn contents(&self) -> &[u8] { 62 | self.0.contents() 63 | } 64 | 65 | fn content_type(&self) -> Option<&str> { 66 | Some("font/woff2") 67 | } 68 | } 69 | 70 | // https://docs.rs/mime/latest/src/mime/lib.rs.html#746-784 71 | -------------------------------------------------------------------------------- /src/time.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use crate::{runtime, Error}; 4 | 5 | /// Puts the current fiber to sleep for at least [duration]. 6 | pub fn sleep(duration: Duration) -> crate::CancellableResult<()> { 7 | let timespec = io_uring::types::Timespec::from(duration); 8 | let sqe = io_uring::opcode::Timeout::new(×pec).build(); 9 | let result = runtime::syscall(sqe); 10 | 11 | match result { 12 | Ok(_) => unreachable!(), 13 | Err(error) => match error { 14 | Error::Original(e) => assert_eq!(e.raw_os_error().unwrap(), libc::ETIME), 15 | Error::Cancelled => return Err(Error::Cancelled), 16 | }, 17 | } 18 | 19 | Ok(()) 20 | } 21 | 22 | #[cfg(test)] 23 | mod tests { 24 | use std::time::Instant; 25 | 26 | use runtime::start; 27 | 28 | use super::*; 29 | 30 | mod sleep { 31 | use super::*; 32 | 33 | #[test] 34 | fn doesnt_hang_when_sleeping_zero() { 35 | start(|| { 36 | let before = Instant::now(); 37 | 38 | sleep(Duration::from_millis(0)).unwrap(); 39 | 40 | assert!(before.elapsed() < Duration::from_millis(5)); 41 | }) 42 | .unwrap(); 43 | } 44 | 45 | #[test] 46 | fn passes_time() { 47 | start(|| { 48 | let before = Instant::now(); 49 | 50 | sleep(Duration::from_millis(5)).unwrap(); 51 | 52 | assert!(before.elapsed() > Duration::from_millis(5)); 53 | }) 54 | .unwrap(); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /.github/workflows/uringy.yaml: -------------------------------------------------------------------------------- 1 | name: Uringy CI/CD 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | paths-ignore: 7 | - macros/** 8 | 9 | jobs: 10 | unit_test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v3 15 | 16 | - name: Fix rust 17 | run: rustup update stable 18 | 19 | - name: Cache Rust 20 | uses: Swatinem/rust-cache@v2 21 | with: 22 | key: unit-test 23 | 24 | - name: Unit test 25 | run: cargo test --features http 26 | 27 | build_timings: 28 | runs-on: ubuntu-latest 29 | steps: 30 | - name: Checkout repository 31 | uses: actions/checkout@v3 32 | 33 | - name: Fix rust 34 | run: rustup update stable 35 | 36 | - name: Cache Rust 37 | uses: Swatinem/rust-cache@v2 38 | with: 39 | key: unit-timings 40 | 41 | - name: Download Cargo dependencies 42 | run: cargo fetch 43 | 44 | - name: Time cold build 45 | run: cargo build --release --timings 46 | 47 | - name: Upload timings report 48 | uses: actions/upload-artifact@v3 49 | with: 50 | name: build_timings_report 51 | path: target/cargo-timings/cargo-timing.html 52 | 53 | verify_msrv: 54 | runs-on: ubuntu-latest 55 | steps: 56 | - name: Checkout repository 57 | uses: actions/checkout@v3 58 | 59 | - name: Cache Rust 60 | uses: Swatinem/rust-cache@v2 61 | with: 62 | key: verify-msrv 63 | 64 | - name: Verify whether MSRV is satisfiable 65 | run: | 66 | cargo install cargo-msrv 67 | cargo msrv verify -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(feature = "fast_thread_local", feature(thread_local))] 2 | 3 | #[cfg(feature = "macros")] 4 | pub use uringy_macros::start; 5 | 6 | pub mod circular_buffer; 7 | pub mod ecosystem; 8 | pub mod fs; 9 | pub mod net; 10 | pub mod runtime; 11 | pub mod sync; 12 | pub mod time; 13 | 14 | /// ... 15 | #[derive(thiserror::Error, Debug, PartialEq)] 16 | pub enum Error { 17 | #[error("original...")] 18 | Original(#[from] E), 19 | 20 | #[error("cancelled...")] 21 | Cancelled, 22 | } 23 | 24 | impl Error { 25 | /// ... 26 | #[inline] 27 | pub fn map U, U>(self, f: F) -> Error { 28 | match self { 29 | Error::Original(e) => Error::Original(f(e)), 30 | Error::Cancelled => Error::Cancelled, 31 | } 32 | } 33 | 34 | /// ... 35 | #[inline] 36 | pub fn and_then Error, U>(self, f: F) -> Error { 37 | match self { 38 | Error::Original(e) => f(e), 39 | Error::Cancelled => Error::Cancelled, 40 | } 41 | } 42 | } 43 | 44 | impl Error { 45 | /// ... 46 | pub fn from_io_error(error: std::io::Error) -> Self { 47 | match error.raw_os_error().unwrap() { 48 | libc::ECANCELED => Error::Cancelled, 49 | _ => Error::Original(error), 50 | } 51 | } 52 | } 53 | 54 | impl From> for std::io::Error { 55 | fn from(error: Error) -> Self { 56 | match error { 57 | Error::Original(e) => e, 58 | Error::Cancelled => std::io::Error::from_raw_os_error(libc::ECANCELED), 59 | } 60 | } 61 | } 62 | 63 | /// ... 64 | pub type IoResult = Result>; 65 | pub type CancellableResult = Result>; 66 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "uringy" 3 | version = "0.5.0" 4 | documentation = "https://docs.rs/uringy" 5 | repository = "https://github.com/Dennis-Krasnov/Uringy" 6 | homepage = "https://uringy-documentation.fly.dev" 7 | description = "A simple single-threaded concurrency runtime for Rust based on io_uring." 8 | categories = ["asynchronous", "concurrency", "filesystem", "os", "network-programming"] 9 | keywords = ["io", "async", "non-blocking", "linux", "io_uring"] 10 | license = "0BSD" 11 | authors = ["Dennis Krasnov "] 12 | edition = "2021" 13 | rust-version = "1.75.0" # found with cargo msrv 14 | 15 | [workspace] 16 | members = [ 17 | "macros", 18 | ] 19 | 20 | [features] 21 | # core 22 | default = ["macros"] 23 | macros = ["dep:uringy-macros"] 24 | 25 | # optional 26 | http = ["dep:matchit", "dep:serde", "dep:serde_json", "dep:serde_urlencoded", "dep:httpdate", "dep:httparse", "dep:ahash"] 27 | nats = ["dep:nom", "dep:itoa"] 28 | 29 | # experimental 30 | huge_pages = [] # requires OS setup 31 | fast_thread_local = [] # requires nightly toolchain 32 | 33 | [dependencies] 34 | uringy-macros = { version = "0.2.0", optional = true } 35 | 36 | thiserror = "1.0.50" 37 | io-uring = "0.6.0" 38 | libc = "0.2.147" 39 | slab = "0.4.8" 40 | 41 | # HTTP 42 | matchit = { version = "0.7.3", optional = true } 43 | serde = { version = "1.0.190", optional = true } 44 | serde_json = { version = "1.0.108", optional = true } 45 | serde_urlencoded = { version = "0.7.1", optional = true } 46 | httpdate = { version = "1.0.3", optional = true } 47 | httparse = { version = "1.8.0", optional = true } 48 | ahash = { version = "0.8.7", optional = true } 49 | 50 | # NATS 51 | nom = { version = "7.1.3", optional = true } 52 | itoa = { version = "1.0.9", optional = true } 53 | 54 | [dev-dependencies] 55 | uuid = { version = "1.5.0", features = ["v4"] } 56 | serde = { version = "1.0.190", features = ["derive"] } 57 | trybuild = "1.0.85" 58 | -------------------------------------------------------------------------------- /src/runtime/tls.rs: -------------------------------------------------------------------------------- 1 | //! Abstraction over thread local storage. 2 | //! 3 | //! Can transparently switch between using: 4 | //! - `RefCell` and `UnsafeCell`. 5 | //! - `thread_local` declarative and procedural macros. 6 | 7 | use std::cell::RefCell; 8 | 9 | /// Cache padded to avoid potential performance hit due to false sharing. 10 | #[repr(align(128))] 11 | struct Runtime(RefCell>); 12 | 13 | #[cfg(not(feature = "fast_thread_local"))] 14 | thread_local! { 15 | /// Each thread gets its own independent runtime. 16 | static RUNTIME: Runtime = Runtime(RefCell::new(None)); 17 | } 18 | 19 | /// Provides a runtime for the duration of the closure. 20 | #[cfg(not(feature = "fast_thread_local"))] 21 | pub(super) fn exclusive_runtime(f: impl FnOnce() -> T) -> T { 22 | RUNTIME.with(|thread_local| { 23 | let mut cell = thread_local.0.borrow_mut(); 24 | assert!(cell.is_none(), "can't nest runtimes ..."); 25 | *cell = Some(super::RuntimeState::new()); 26 | }); 27 | 28 | let output = f(); 29 | 30 | RUNTIME.with(|thread_local| { 31 | let mut cell = thread_local.0.borrow_mut(); 32 | *cell = None; 33 | }); 34 | 35 | output 36 | } 37 | 38 | /// Runs a closure that's given a reference to the active `RuntimeState`. 39 | #[cfg(not(feature = "fast_thread_local"))] 40 | pub(super) fn runtime(f: impl FnOnce(&mut super::RuntimeState) -> T) -> T { 41 | RUNTIME.with(|thread_local| { 42 | let mut cell = thread_local.0.borrow_mut(); 43 | let runtime = cell.as_mut().expect("no runtime..."); 44 | f(runtime) 45 | }) 46 | } 47 | 48 | #[cfg(feature = "fast_thread_local")] 49 | #[thread_local] 50 | static RUNTIME: Runtime = Runtime(RefCell::new(None)); 51 | 52 | /// Provides a runtime for the duration of the closure. 53 | #[cfg(feature = "fast_thread_local")] 54 | pub(super) fn exclusive_runtime(f: impl FnOnce() -> T) -> T { 55 | { 56 | let mut cell = RUNTIME.0.borrow_mut(); 57 | assert!(cell.is_none(), "..."); 58 | *cell = Some(super::RuntimeState::new()); 59 | } 60 | 61 | let output = f(); 62 | 63 | let mut cell = RUNTIME.0.borrow_mut(); 64 | *cell = None; 65 | 66 | output 67 | } 68 | 69 | /// Runs a closure that's given a reference to the active `RuntimeState`. 70 | #[cfg(feature = "fast_thread_local")] 71 | pub(super) fn runtime(f: impl FnOnce(&mut super::RuntimeState) -> T) -> T { 72 | let mut cell = RUNTIME.0.borrow_mut(); 73 | let runtime = cell.as_mut().expect("no runtime..."); 74 | f(runtime) 75 | } 76 | -------------------------------------------------------------------------------- /src/runtime/syscall.rs: -------------------------------------------------------------------------------- 1 | //! Abstraction over cancellable non-blocking syscalls. 2 | //! 3 | //! Provides an implementation for every OS. 4 | 5 | #[cfg(not(target_os = "linux"))] 6 | compile_error!("Uringy only supports Linux"); 7 | 8 | #[cfg(target_os = "linux")] 9 | pub(super) struct Interface { 10 | io_uring: io_uring::IoUring, 11 | } 12 | 13 | #[cfg(target_os = "linux")] 14 | const ASYNC_CANCELLATION_USER_DATA: u64 = u64::MAX; 15 | 16 | #[cfg(target_os = "linux")] 17 | impl Interface { 18 | // TODO: optionally reuse kernel workers 19 | pub(super) fn new() -> Self { 20 | let mut builder = io_uring::IoUring::builder(); 21 | builder.setup_clamp(); // won't panic if IORING_MAX_ENTRIES is too large 22 | let io_uring = builder.build(1024).unwrap(); 23 | Interface { io_uring } 24 | } 25 | 26 | /// ... 27 | pub(super) fn wait_for_completed(&mut self) { 28 | self.io_uring.submit_and_wait(1).unwrap(); 29 | // TODO: retry on EINTR (interrupted) 30 | } 31 | 32 | /// ... 33 | /// TODO: give this a closure? 34 | pub(super) fn process_completed(&mut self) -> impl Iterator { 35 | let mut results = vec![]; // TODO: return iterator (to avoid allocating) that mutably borrows io_uring by holding cq 36 | 37 | for cqe in self.io_uring.completion() { 38 | if cqe.user_data() == ASYNC_CANCELLATION_USER_DATA { 39 | continue; 40 | } 41 | 42 | let syscall_id = Id(cqe.user_data()); 43 | 44 | // TODO: also process flags in match: 45 | // Storing the selected buffer ID, if one was selected. See BUFFER_SELECT for more info. 46 | // whether oneshot accepts needs to resubscribe (convert to yet another io::error) 47 | 48 | results.push((syscall_id, cqe.result())); 49 | } 50 | 51 | results.into_iter() 52 | } 53 | 54 | /// ... 55 | // TODO: make my own sqe struct (exposed to whole crate) 56 | pub(super) fn issue(&mut self, id: Id, sqe: io_uring::squeue::Entry) { 57 | let sqe = sqe.user_data(id.0); 58 | 59 | let mut sq = self.io_uring.submission(); 60 | while sq.is_full() { 61 | drop(sq); // avoid borrowing io_uring more than once 62 | // TODO: process CQs as well (same syscall) 63 | dbg!(self.io_uring.submit().unwrap()); // TODO: remove debug after ensuring this works 64 | sq = self.io_uring.submission(); 65 | } 66 | unsafe { sq.push(&sqe).unwrap() }; // safety: submission queue isn't full 67 | } 68 | 69 | /// ... 70 | pub(super) fn cancel(&mut self, target: Id) { 71 | let sqe = io_uring::opcode::AsyncCancel::new(target.0).build(); 72 | self.issue(Id(ASYNC_CANCELLATION_USER_DATA), sqe); 73 | } 74 | } 75 | 76 | #[repr(transparent)] 77 | #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] 78 | pub(super) struct Id(pub(super) u64); 79 | -------------------------------------------------------------------------------- /src/runtime/assembly/x86_64.s: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * 0x38 : RIP - return address = &trampoline 3 | * 0x30 : RBP - base pointer = &finish // removed 4 | * 0x28 : RBX - function pointer = &fn() 5 | * 0x20 : R15 6 | * 0x18 : R14 7 | * 0x10 : R13 rbx : used in trampoline, set before call 8 | * 0x08 : R12 9 | * 0x04 : fc_x87_cw - 4B rsp : stack pointer 10 | * 0x00 : fc_mxcsr - 4B rip : instruction pointer (r/o) 11 | ******************************************************************************/ 12 | 13 | .global prepare_stack // fn(rdi: stack, rsi: func) -> rax: continuation 14 | prepare_stack: 15 | mov rax, rdi // The first argument of prepare_stack() == top of context-stack 16 | and rax, -16 // Shift address in RAX to lower 16-byte boundary 17 | lea rax, [rax - 0x40] // Reserve space for context-data on context-stack 18 | 19 | stmxcsr [rax] // Save MMX control-word and status-word 20 | fnstcw [rax + 0x04] // Save x87 control-word 21 | mov [rax + 0x28], rsi // 2-rd arg of prepare_stack() == address of context-fn, store in RBX 22 | 23 | lea rcx, [rip + trampoline] // Compute absolute address of label trampoline 24 | mov [rax + 0x38], rcx // Save the addr of trampoline as a return-address for func will be entered after the context-function returns 25 | ret // Return pointer to context-data 26 | 27 | .global trampoline 28 | trampoline: 29 | push rbp // Store return address on stack, fix stack alignment 30 | jmp rbx // Jump to context-function 31 | 32 | 33 | .global jump // fn (rdi: from, rsi: to) 34 | jump: 35 | lea rsp, [rsp - 0x38] // Prepare stack (RIP is already stored in stack) 36 | 37 | stmxcsr [rsp] // Save MMX control-word and status-word 38 | fnstcw [rsp + 0x04] // Save x87 control-word 39 | mov [rsp + 0x08], r12 40 | mov [rsp + 0x10], r13 41 | mov [rsp + 0x18], r14 42 | mov [rsp + 0x20], r15 43 | mov [rsp + 0x28], rbx 44 | mov [rsp + 0x30], rbp 45 | 46 | mov [rdi], rsp // Save SP (pointing to context-data) to the first arg (RDI) 47 | mov rsp, [rsi] // Restore SP (pointing to context-data) from second arg (RSI) 48 | 49 | ldmxcsr [rsp] // Restore MMX control-word and status-word 50 | fldcw [rsp + 0x04] // Restore x87 control-word 51 | mov r12, [rsp + 0x08] 52 | mov r13, [rsp + 0x10] 53 | mov r14, [rsp + 0x18] 54 | mov r15, [rsp + 0x20] 55 | mov rbx, [rsp + 0x28] 56 | mov rbp, [rsp + 0x30] 57 | lea rsp, [rsp + 0x38] // Clear stack 58 | 59 | ret // Jump to the address at [rsp] 60 | -------------------------------------------------------------------------------- /src/runtime/stack.rs: -------------------------------------------------------------------------------- 1 | //! Userspace stack. 2 | //! 3 | //! Demand paging ensures that physical memory is allocated only as necessary, during a page fault. 4 | //! The stack is protected from overflow using guard pages at the lowest addresses. 5 | 6 | use std::num::NonZeroUsize; 7 | use std::{ffi, io, ptr}; 8 | 9 | #[derive(Debug)] 10 | pub(super) struct Stack { 11 | pub(super) pointer: *mut ffi::c_void, 12 | pub(super) length: usize, 13 | } 14 | 15 | impl Stack { 16 | /// Allocates a new stack. 17 | pub(super) fn new(guard_pages: NonZeroUsize, usable_pages: NonZeroUsize) -> io::Result { 18 | let (guard_pages, usable_pages) = (guard_pages.get(), usable_pages.get()); 19 | 20 | // page aligned sizes 21 | let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) as usize }; 22 | let length = (guard_pages + usable_pages) * page_size; 23 | 24 | // kernel allocates an unused block of virtual memory 25 | let pointer = unsafe { 26 | libc::mmap( 27 | ptr::null_mut(), 28 | length, 29 | libc::PROT_READ | libc::PROT_WRITE, 30 | libc::MAP_PRIVATE | libc::MAP_ANONYMOUS, 31 | -1, 32 | 0, 33 | ) 34 | }; 35 | if pointer == libc::MAP_FAILED { 36 | let error = io::Error::last_os_error(); 37 | return Err(error); 38 | } 39 | 40 | // if guarding memory goes wrong then mmap gets cleaned up in Stack's drop 41 | let stack = Stack { pointer, length }; 42 | 43 | let result = unsafe { libc::mprotect(pointer, guard_pages * page_size, libc::PROT_NONE) }; 44 | if result == -1 { 45 | let error = io::Error::last_os_error(); 46 | return Err(error); 47 | } 48 | 49 | Ok(stack) 50 | } 51 | 52 | /// The highest address, since stacks grow downwards. 53 | pub(super) fn base(&self) -> *mut ffi::c_void { 54 | // safety: part of same allocation, can't overflow 55 | unsafe { self.pointer.byte_add(self.length) } 56 | } 57 | } 58 | 59 | impl Drop for Stack { 60 | fn drop(&mut self) { 61 | let result = unsafe { libc::munmap(self.pointer, self.length) }; 62 | assert_eq!(result, 0); 63 | } 64 | } 65 | 66 | #[cfg(test)] 67 | mod tests { 68 | use super::*; 69 | 70 | #[test] 71 | fn reads_and_writes() { 72 | let stack = Stack::new(NonZeroUsize::MIN, NonZeroUsize::MIN).unwrap(); 73 | unsafe { 74 | let pointer = (stack.base() as *mut u32).sub(1); 75 | pointer.write(123); 76 | assert_eq!(pointer.read(), 123); 77 | } 78 | } 79 | 80 | #[test] 81 | fn cant_execute() { 82 | // TODO 83 | } 84 | 85 | // #[test] 86 | // #[ignore = "aborts process"] // TODO: test with fork() 87 | // fn overflow() { 88 | // let stack = Stack::new(NonZeroUsize::MIN, NonZeroUsize::MIN).unwrap(); 89 | // let pointer = stack.base() as *mut u8; 90 | // unsafe { 91 | // let pointer = pointer.sub(4096 + 1); 92 | // pointer.write(123); 93 | // } 94 | // } 95 | } 96 | -------------------------------------------------------------------------------- /src/ecosystem/http/mod.rs: -------------------------------------------------------------------------------- 1 | //! Opinionated HTTP 1.1 client and server inspired by Axum. 2 | //! 3 | //! Unlike Axum, this library is zero copy (better performance). 4 | //! This means the request/response can reference the stack and the server can reference the request. 5 | //! 6 | //! The major drawback of this design is that streaming isn't supported. 7 | //! This means requests and responses must fit into memory (you can configure the buffer size). 8 | //! There is no support for chunked transfer encoding nor SSE. 9 | //! 10 | //! The justification is that most clients/servers don't need streaming: 11 | //! - Typically there's a streaming reverse proxy in front of your server that buffers requests/responses and sends/receives them at once. 12 | //! - It's pure overhead for small to medium payloads. 13 | //! - It's too naive for big payloads, where you would manually chunk for the ability to resume a partially completed transfer. 14 | //! - Realtime applications outside the web aren't limited by HTTP and can use better suiting protocols like gRPC. 15 | //! - Realtime applications on the web still have the option of long polling, websockets, and eventually WebTransport. 16 | //! - If you still need streaming, you can proxy that endpoint to a server that supports it. 17 | //! 18 | //! There are plans to support websockets and connect tunnels, as they respond like normal then hijack the whole connection. 19 | //! 20 | //! HTTP 2/3 aren't supported since they aren't compatible with the zero copy design. 21 | //! Use a reverse proxy like Nginx to support these newer protocols, remember to enable keepalive to the origin. 22 | 23 | use crate::ecosystem::http::payload::{Request, Response, StatusCode}; 24 | use std::marker::PhantomData; 25 | 26 | pub mod client; 27 | pub mod payload; 28 | pub mod server; 29 | 30 | pub mod middleware; 31 | pub mod mime; 32 | 33 | /// Dynamically dispatched handle to the next step in processing the request. 34 | pub type Handler = Box; 35 | 36 | /// Dynamically dispatched handle to the next step in processing the response. 37 | pub struct Responder<'a, TS = DefaultStatusCode> { 38 | respond: Box, 39 | type_state: PhantomData, 40 | status: StatusCode, 41 | headers: Vec<(&'a str, &'a [u8])>, 42 | } 43 | 44 | /// Type state for `Responder`. 45 | pub struct DefaultStatusCode; 46 | pub struct CustomStatusCode; 47 | 48 | impl<'a> Responder<'a, DefaultStatusCode> { 49 | fn new(respond: impl Respond + 'static) -> Self { 50 | Responder { 51 | respond: Box::new(respond), 52 | type_state: PhantomData, 53 | status: StatusCode::Ok, 54 | headers: vec![], 55 | } 56 | } 57 | 58 | /// ... 59 | #[inline] 60 | pub fn status(self, status: StatusCode) -> Responder<'a, CustomStatusCode> { 61 | Responder { 62 | respond: self.respond, 63 | type_state: PhantomData, 64 | status, 65 | headers: self.headers, 66 | } 67 | } 68 | } 69 | 70 | impl<'a, TS> Responder<'a, TS> { 71 | /// ... 72 | #[inline] 73 | pub fn header(mut self, name: &'a str, value: &'a [u8]) -> Self { 74 | self.headers.push((name, value)); 75 | self 76 | } 77 | 78 | /// ... 79 | #[inline] 80 | pub fn send(self, body: impl payload::AsBody) { 81 | let response = Response { 82 | status: self.status, 83 | headers: self.headers, 84 | body: body.contents(), 85 | content_type: body.content_type(), 86 | }; 87 | self.respond.respond(response); 88 | } 89 | } 90 | 91 | /// A concrete `Responder::send` is exposed instead of this trait because: 92 | /// - You don't need to import the `Respond` trait to send responses. 93 | /// - It allows you to take non-object safe `impl IntoResponse`. 94 | trait Respond { 95 | fn respond(self: Box, response: Response); 96 | } 97 | 98 | /// ... 99 | /// 100 | /// Generic `ARGS` prevent conflicting implementations. 101 | pub trait IntoHandler { 102 | /// ... 103 | fn into_handler(self) -> Handler; 104 | } 105 | 106 | impl IntoHandler<(Responder<'_>,), S> for F { 107 | fn into_handler(self) -> Handler { 108 | Box::new(move |r, _, _| self(r)) 109 | } 110 | } 111 | 112 | impl IntoHandler<(Responder<'_>, (), &S), S> for F { 113 | fn into_handler(self) -> Handler { 114 | Box::new(move |r, _, state| self(r, state)) 115 | } 116 | } 117 | 118 | impl IntoHandler<(Responder<'_>, &Request<'_>, ()), S> 119 | for F 120 | { 121 | fn into_handler(self) -> Handler { 122 | Box::new(move |r, request, _| self(r, request)) 123 | } 124 | } 125 | 126 | impl IntoHandler<(Responder<'_>, &Request<'_>, &S), S> 127 | for F 128 | { 129 | fn into_handler(self) -> Handler { 130 | Box::new(move |r, request, state| self(r, request, state)) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/net/mod.rs: -------------------------------------------------------------------------------- 1 | //! ... 2 | 3 | use crate::IoResult; 4 | use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; 5 | use std::option; 6 | 7 | pub mod tcp; 8 | 9 | /// ... 10 | pub trait ToSocketAddrs { 11 | /// ... 12 | type Iter: Iterator; 13 | 14 | /// ... 15 | fn to_socket_addrs(&self) -> IoResult; 16 | } 17 | 18 | // FIXME: I don't want this... I want to manually resolve. 19 | // impl ToSocketAddrs for T { 20 | // type Iter = T::Iter; 21 | // 22 | // fn to_socket_addrs(&self) -> io::Result> { 23 | // Ok(Some(*self).into_iter()) 24 | // } 25 | // } 26 | 27 | impl ToSocketAddrs for SocketAddr { 28 | type Iter = option::IntoIter; 29 | 30 | fn to_socket_addrs(&self) -> IoResult { 31 | Ok(Some(*self).into_iter()) 32 | } 33 | } 34 | 35 | impl ToSocketAddrs for SocketAddrV4 { 36 | type Iter = option::IntoIter; 37 | 38 | fn to_socket_addrs(&self) -> IoResult { 39 | SocketAddr::V4(*self).to_socket_addrs() 40 | } 41 | } 42 | // 43 | // impl ToSocketAddrs for SocketAddrV6 { 44 | // type Iter = option::IntoIter; 45 | // 46 | // fn to_socket_addrs(&self) -> io::Result> { 47 | // SocketAddr::V6(*self).to_socket_addrs() 48 | // } 49 | // } 50 | // 51 | // impl ToSocketAddrs for (IpAddr, u16) { 52 | // type Iter = option::IntoIter; 53 | // 54 | // fn to_socket_addrs(&self) -> io::Result> { 55 | // let (ip, port) = *self; 56 | // match ip { 57 | // IpAddr::V4(addr) => (addr, port).to_socket_addrs(), 58 | // IpAddr::V6(addr) => (addr, port).to_socket_addrs(), 59 | // } 60 | // } 61 | // } 62 | // 63 | impl ToSocketAddrs for (Ipv4Addr, u16) { 64 | type Iter = option::IntoIter; 65 | 66 | fn to_socket_addrs(&self) -> IoResult { 67 | let (ip, port) = *self; 68 | SocketAddrV4::new(ip, port).to_socket_addrs() 69 | } 70 | } 71 | // 72 | // impl ToSocketAddrs for (Ipv6Addr, u16) { 73 | // type Iter = option::IntoIter; 74 | // 75 | // fn to_socket_addrs(&self) -> io::Result> { 76 | // let (ip, port) = *self; 77 | // SocketAddrV6::new(ip, port, 0, 0).to_socket_addrs() 78 | // } 79 | // } 80 | // 81 | // impl ToSocketAddrs for (&str, u16) { 82 | // // type Iter = vec::IntoIter; 83 | // type Iter = sync::channel::Receiver; 84 | // 85 | // fn to_socket_addrs(&self) -> io::Result> { 86 | // let (host, port) = *self; 87 | // let (tx, rx) = sync::channel::unbounded(); 88 | // 89 | // if let Ok(addr) = host.parse() { 90 | // let addr = SocketAddrV4::new(addr, port); 91 | // tx.send(SocketAddr::V4(addr)).unwrap(); 92 | // return Ok(rx); 93 | // // return Ok(vec![SocketAddr::V4(addr)].into_iter()); 94 | // } 95 | // 96 | // if let Ok(addr) = host.parse() { 97 | // let addr = SocketAddrV6::new(addr, port, 0, 0); 98 | // tx.send(SocketAddr::V6(addr)).unwrap(); 99 | // return Ok(rx); 100 | // // return Ok(vec![SocketAddr::V6(addr)].into_iter()); 101 | // } 102 | // 103 | // spawn(move || { 104 | // drop(tx); 105 | // // TODO: do DNS stuff, send to tx 106 | // }); 107 | // 108 | // Ok(rx) 109 | // 110 | // // // TODO: DNS returns a read channel handle (implements iterator) (continues to do stuff in background) 111 | // // let addresses: Vec<_> = dns::dig_short(host)? 112 | // // .into_iter() 113 | // // .map(|ip| SocketAddr::new(ip, port)) 114 | // // .collect(); 115 | // // Ok(addresses.into_iter()) 116 | // } 117 | // } 118 | // 119 | // impl ToSocketAddrs for (String, u16) { 120 | // // type Iter = vec::IntoIter; 121 | // type Iter = sync::channel::Receiver; 122 | // 123 | // fn to_socket_addrs(&self) -> io::Result> { 124 | // (&*self.0, self.1).to_socket_addrs() 125 | // } 126 | // } 127 | // 128 | // // accepts strings like 'localhost:12345' 129 | // impl ToSocketAddrs for str { 130 | // // type Iter = vec::IntoIter; 131 | // type Iter = sync::channel::Receiver; 132 | // 133 | // fn to_socket_addrs(&self) -> io::Result> { 134 | // if let Ok(addr) = self.parse() { 135 | // let (tx, rx) = sync::channel::unbounded(); 136 | // tx.send(addr).unwrap(); 137 | // return Ok(rx); 138 | // // return Ok(vec![addr].into_iter()); 139 | // } 140 | // 141 | // let Some((host, port)) = self.rsplit_once(':') else { 142 | // return Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid socket address")); 143 | // }; 144 | // let Ok(port) = port.parse() else { 145 | // return Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid port value")); 146 | // }; 147 | // (host, port).to_socket_addrs() 148 | // } 149 | // } 150 | // 151 | // impl<'a> ToSocketAddrs for &'a [SocketAddr] { 152 | // type Iter = iter::Cloned>; 153 | // 154 | // fn to_socket_addrs(&self) -> io::Result { 155 | // Ok(self.iter().cloned()) 156 | // } 157 | // } 158 | // 159 | // impl ToSocketAddrs for &T { 160 | // type Iter = T::Iter; 161 | // 162 | // fn to_socket_addrs(&self) -> io::Result { 163 | // (**self).to_socket_addrs() 164 | // } 165 | // } 166 | // 167 | // impl ToSocketAddrs for String { 168 | // type Iter = sync::channel::Receiver; 169 | // 170 | // fn to_socket_addrs(&self) -> io::Result> { 171 | // (&**self).to_socket_addrs() 172 | // } 173 | // } 174 | -------------------------------------------------------------------------------- /src/ecosystem/http/server/fake_client.rs: -------------------------------------------------------------------------------- 1 | //! ... 2 | 3 | use crate::ecosystem::http::payload::{AsBody, Method, Request, Response, StatusCode}; 4 | use crate::ecosystem::http::server::route::Router; 5 | use crate::ecosystem::http::{Respond, Responder}; 6 | use crate::sync::channel; 7 | 8 | /// ... 9 | pub struct FakeClient { 10 | router: Router, 11 | response: Option, 12 | } 13 | 14 | impl FakeClient { 15 | /// ... 16 | #[inline] 17 | pub fn new(router: Router, state: S) -> Self { 18 | FakeClient { 19 | router: router.with_state(state), 20 | response: None, 21 | } 22 | } 23 | 24 | /// Make a GET request. 25 | #[inline] 26 | pub fn get<'a>(&'a mut self, path: &'a str) -> FakeRequestBuilder<'a, S> { 27 | self.request(Method::Get, path) 28 | } 29 | 30 | /// Make a POST request. 31 | #[inline] 32 | pub fn post<'a>(&'a mut self, path: &'a str) -> FakeRequestBuilder<'a, S> { 33 | self.request(Method::Post, path) 34 | } 35 | 36 | /// Make a HEAD request. 37 | #[inline] 38 | pub fn head<'a>(&'a mut self, path: &'a str) -> FakeRequestBuilder<'a, S> { 39 | self.request(Method::Head, path) 40 | } 41 | 42 | /// Make a PUT request. 43 | #[inline] 44 | pub fn put<'a>(&'a mut self, path: &'a str) -> FakeRequestBuilder<'a, S> { 45 | self.request(Method::Put, path) 46 | } 47 | 48 | /// Make a DELETE request. 49 | #[inline] 50 | pub fn delete<'a>(&'a mut self, path: &'a str) -> FakeRequestBuilder<'a, S> { 51 | self.request(Method::Delete, path) 52 | } 53 | 54 | /// Make a CONNECT request. 55 | #[inline] 56 | pub fn connect<'a>(&'a mut self, path: &'a str) -> FakeRequestBuilder<'a, S> { 57 | self.request(Method::Connect, path) 58 | } 59 | 60 | /// Make a OPTIONS request. 61 | #[inline] 62 | pub fn options<'a>(&'a mut self, path: &'a str) -> FakeRequestBuilder<'a, S> { 63 | self.request(Method::Options, path) 64 | } 65 | 66 | /// Make a TRACE request. 67 | #[inline] 68 | pub fn trace<'a>(&'a mut self, path: &'a str) -> FakeRequestBuilder<'a, S> { 69 | self.request(Method::Trace, path) 70 | } 71 | 72 | /// Make a PATCH request. 73 | #[inline] 74 | pub fn patch<'a>(&'a mut self, path: &'a str) -> FakeRequestBuilder<'a, S> { 75 | self.request(Method::Patch, path) 76 | } 77 | 78 | /// Make a request with the given method. 79 | #[inline] 80 | pub fn request<'a>(&'a mut self, method: Method, path: &'a str) -> FakeRequestBuilder<'a, S> { 81 | FakeRequestBuilder { 82 | client: self, 83 | method, 84 | path, 85 | query: Vec::new(), 86 | headers: Vec::new(), 87 | } 88 | } 89 | } 90 | 91 | /// Can't `impl, ARGS> From for FakeClient` since ARGS are unconstrained. 92 | impl From> for FakeClient<()> { 93 | fn from(router: Router) -> Self { 94 | FakeClient::new(router, ()) 95 | } 96 | } 97 | 98 | /// ... 99 | pub struct FakeRequestBuilder<'a, S> { 100 | client: &'a mut FakeClient, 101 | method: Method, 102 | path: &'a str, 103 | query: Vec<(&'a str, &'a str)>, 104 | headers: Vec<(&'a str, &'a [u8])>, 105 | } 106 | 107 | impl<'a, S> FakeRequestBuilder<'a, S> { 108 | /// ... 109 | #[inline] 110 | pub fn query(mut self, name: &'a str, value: &'a str) -> Self { 111 | self.query.push((name, value)); 112 | self 113 | } 114 | 115 | /// ... 116 | #[inline] 117 | pub fn header(mut self, name: &'a str, value: &'a [u8]) -> Self { 118 | self.headers.push((name, value)); 119 | self 120 | } 121 | 122 | // /// ... 123 | // pub fn headers(self) -> Self { 124 | // self 125 | // } 126 | 127 | /// ... 128 | #[inline] 129 | pub fn send(self, body: impl AsBody) -> Response<'a> { 130 | let (tx, rx) = channel::unbounded(); // TODO: channel::oneshot 131 | let r = Responder::new(FakeResponder(tx)); 132 | let query = serde_urlencoded::to_string(self.query).unwrap(); 133 | let request = Request::new( 134 | self.method, 135 | self.path, 136 | &query, 137 | self.headers, 138 | body.contents(), 139 | ); 140 | self.client.router.handle(r, &request); 141 | self.client.response = Some(rx.recv().expect("must respond...")); 142 | Response::from(self.client.response.as_ref().unwrap()) 143 | } 144 | } 145 | 146 | struct FakeResponder(channel::Sender); 147 | 148 | impl Respond for FakeResponder { 149 | fn respond(self: Box, response: Response) { 150 | // TODO: add content length, content-type, date headers here 151 | self.0.send(OwnedResponse::from(response)).unwrap(); 152 | } 153 | } 154 | 155 | /// Simplifies transfer of the [Response] back to the [FakeClient]. 156 | struct OwnedResponse { 157 | status_code: StatusCode, 158 | headers: Vec<(String, Vec)>, 159 | body: Box<[u8]>, 160 | content_type: Option, 161 | } 162 | 163 | impl From> for OwnedResponse { 164 | fn from(response: Response<'_>) -> Self { 165 | OwnedResponse { 166 | status_code: response.status, 167 | headers: response 168 | .headers 169 | .into_iter() 170 | .map(|(k, v)| (k.to_string(), v.to_vec())) 171 | .collect(), 172 | body: response.body.to_vec().into_boxed_slice(), 173 | content_type: response.content_type.map(String::from), 174 | } 175 | } 176 | } 177 | 178 | impl<'a> From<&'a OwnedResponse> for Response<'a> { 179 | fn from(response: &'a OwnedResponse) -> Self { 180 | Response { 181 | status: response.status_code, 182 | headers: response 183 | .headers 184 | .iter() 185 | .map(|(k, v)| (k.as_str(), v.as_slice())) 186 | .collect(), 187 | body: &response.body, 188 | content_type: response.content_type.as_ref().map(String::as_str), 189 | } 190 | } 191 | } 192 | 193 | #[cfg(test)] 194 | mod tests { 195 | use super::*; 196 | use crate::ecosystem::http::payload::StatusCode; 197 | use crate::ecosystem::http::Responder; 198 | use crate::runtime::start; 199 | 200 | #[test] 201 | fn smoke() { 202 | start(|| { 203 | let routes = Router::new().route( 204 | Method::Get, 205 | "/echo", 206 | |r: Responder, request: &Request, state: &i32| { 207 | assert_eq!(request.raw_query(), "foo=bar&beep=boop"); 208 | assert_eq!(request.query_params()["foo"], "bar"); 209 | assert_eq!(request.query("foo"), Some("bar")); 210 | assert!(!request.raw_headers().is_empty()); 211 | assert_eq!(request.headers()["foo"], "hello".as_bytes()); 212 | assert_eq!(request.header("foo"), Some("hello".as_bytes())); 213 | assert_eq!(state, &123); 214 | r.send(request.body()) 215 | }, 216 | ); 217 | let mut client = FakeClient::new(routes, 123); 218 | 219 | let response = client 220 | .get("/echo") 221 | .query("foo", "bar") 222 | .query("beep", "boop") 223 | .header("foo", b"hello") 224 | .send("hello"); 225 | assert_eq!(response.status, StatusCode::Ok); 226 | assert_eq!(response.body, b"hello"); 227 | }) 228 | .unwrap(); 229 | } 230 | 231 | #[test] 232 | #[should_panic] 233 | fn panics_when_no_response_sent() { 234 | let routes = Router::new().route(Method::Get, "/", |_: Responder| {}); 235 | let mut client = FakeClient::from(routes); 236 | 237 | client.get("/").send(()); 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/ecosystem/http/payload.rs: -------------------------------------------------------------------------------- 1 | //! ... 2 | 3 | use ahash::HashMapExt; 4 | use std::cell::OnceCell; 5 | use std::str::FromStr; 6 | 7 | #[derive(Debug)] 8 | pub struct Request<'a> { 9 | method: Method, 10 | path: &'a str, 11 | query: &'a str, 12 | query_map: OnceCell>, 13 | headers: Vec<(&'a str, &'a [u8])>, 14 | header_map: OnceCell>, 15 | body: &'a [u8], 16 | } 17 | 18 | impl<'a> Request<'a> { 19 | pub(crate) fn new( 20 | method: Method, 21 | path: &'a str, 22 | query: &'a str, 23 | headers: Vec<(&'a str, &'a [u8])>, 24 | body: &'a [u8], 25 | ) -> Self { 26 | Request { 27 | method, 28 | path, 29 | query, 30 | query_map: OnceCell::new(), 31 | headers, 32 | header_map: OnceCell::new(), 33 | body, 34 | } 35 | } 36 | 37 | /// ... 38 | #[inline] 39 | pub fn method(&self) -> Method { 40 | self.method 41 | } 42 | 43 | /// ... 44 | #[inline] 45 | pub fn path(&self) -> &str { 46 | &self.path 47 | } 48 | 49 | /// ... 50 | #[inline] 51 | pub fn path_param(&self, _name: &str) -> &str { 52 | todo!() 53 | } 54 | 55 | /// ... 56 | #[inline] 57 | pub fn raw_query(&self) -> &str { 58 | &self.query 59 | } 60 | 61 | /// ... lazy 62 | #[inline] 63 | pub fn query_params(&self) -> &ahash::HashMap<&str, &str> { 64 | self.query_map 65 | .get_or_init(|| serde_urlencoded::from_str(self.query).unwrap()) 66 | } 67 | 68 | /// ... 69 | #[inline] 70 | pub fn query(&self, name: &str) -> Option<&str> { 71 | self.query_params().get(name).map(|v| *v) 72 | } 73 | 74 | /// ... 75 | #[inline] 76 | pub fn raw_headers(&self) -> &Vec<(&str, &[u8])> { 77 | &self.headers 78 | } 79 | 80 | /// ... lazy 81 | /// ignores duplicates (takes the last) 82 | #[inline] 83 | pub fn headers(&self) -> &ahash::HashMap { 84 | self.header_map.get_or_init(|| { 85 | let mut map = ahash::HashMap::with_capacity(self.headers.len()); 86 | for (name, value) in &self.headers { 87 | map.insert(name.to_ascii_lowercase(), *value); 88 | } 89 | map 90 | }) 91 | } 92 | 93 | /// ... 94 | /// ignores duplicates (takes the last) 95 | #[inline] 96 | pub fn header(&self, name: &str) -> Option<&[u8]> { 97 | self.headers().get(&name.to_ascii_lowercase()).map(|v| *v) 98 | } 99 | 100 | /// ... 101 | #[inline] 102 | pub fn body(&self) -> &[u8] { 103 | self.body 104 | } 105 | } 106 | 107 | /// ... 108 | #[derive(Debug)] 109 | pub struct Response<'a> { 110 | pub status: StatusCode, 111 | pub headers: Vec<(&'a str, &'a [u8])>, 112 | pub body: &'a [u8], 113 | pub content_type: Option<&'a str>, 114 | } 115 | 116 | impl Response<'_> { 117 | /// ... 118 | #[inline] 119 | pub fn header(&self, name: &str) -> Option<&[u8]> { 120 | self.headers 121 | .iter() 122 | .find(|(k, _)| k.eq_ignore_ascii_case(name)) 123 | .map(|(_, v)| *v) 124 | } 125 | } 126 | 127 | /// ... 128 | #[derive(Debug, Copy, Clone)] 129 | pub enum Method { 130 | /// Requests with the GET method: 131 | /// - Retrieve data at the target resource. 132 | /// - Shouldn't mutate. 133 | /// - Shouldn't have a body. 134 | Get, 135 | /// Requests with the POST method: 136 | /// - Submit data to the target resource. 137 | /// - Aren't idempotent. 138 | Post, 139 | /// Requests with the HEAD method: 140 | /// - Are identical to GET requests, but without the response body. 141 | Head, 142 | /// Requests with the PUT method: 143 | /// - Replace the target resource. 144 | Put, 145 | /// Requests with the DELETE method: 146 | /// - Delete the target resource. 147 | /// - Shouldn't have a body. 148 | Delete, 149 | /// Requests with the CONNECT method: 150 | /// - Establish a tunnel to the server identified by the target resource. 151 | /// - Shouldn't have a body. 152 | Connect, 153 | /// Requests with OPTIONS method: 154 | /// - Describe the endpoints the server supports. 155 | /// - Shouldn't have a body. 156 | Options, 157 | /// Requests with the TRACE method: 158 | /// - Perform a message loop-back test along the path to the target resource. 159 | /// - Must not have a body. 160 | Trace, 161 | /// Requests with the PATCH method: 162 | /// - Partially update a resource. 163 | /// - Aren't idempotent. 164 | Patch, 165 | } 166 | 167 | impl FromStr for Method { 168 | type Err = (); 169 | 170 | fn from_str(s: &str) -> Result { 171 | match s { 172 | "GET" => Ok(Method::Get), 173 | "POST" => Ok(Method::Post), 174 | "HEAD" => Ok(Method::Head), 175 | "PUT" => Ok(Method::Put), 176 | "DELETE" => Ok(Method::Delete), 177 | "CONNECT" => Ok(Method::Connect), 178 | "OPTIONS" => Ok(Method::Options), 179 | "TRACE" => Ok(Method::Trace), 180 | "PATCH" => Ok(Method::Patch), 181 | _ => Err(()), 182 | } 183 | } 184 | } 185 | 186 | /// ... 187 | #[derive(Debug, Copy, Clone, PartialEq)] 188 | pub enum StatusCode { 189 | // TODO: rename to Status ??? 190 | Ok, 191 | Accepted, 192 | NotModified, 193 | TemporaryRedirect, 194 | BadRequest, 195 | Unauthorized, 196 | Forbidden, 197 | NotFound, 198 | MethodNotAllowed, 199 | } 200 | 201 | impl From for u16 { 202 | fn from(status: StatusCode) -> Self { 203 | match status { 204 | StatusCode::Ok => 200, 205 | StatusCode::Accepted => 202, 206 | StatusCode::NotModified => 304, 207 | StatusCode::TemporaryRedirect => 307, 208 | StatusCode::BadRequest => 400, 209 | StatusCode::Unauthorized => 401, 210 | StatusCode::Forbidden => 403, 211 | StatusCode::NotFound => 404, 212 | StatusCode::MethodNotAllowed => 405, 213 | } 214 | } 215 | } 216 | 217 | /// ... 218 | pub trait AsBody { 219 | /// ... 220 | fn contents(&self) -> &[u8]; 221 | 222 | /// ... 223 | fn content_type(&self) -> Option<&str>; 224 | } 225 | 226 | impl AsBody for () { 227 | fn contents(&self) -> &[u8] { 228 | &[] 229 | } 230 | 231 | fn content_type(&self) -> Option<&str> { 232 | None 233 | } 234 | } 235 | 236 | impl AsBody for &str { 237 | fn contents(&self) -> &[u8] { 238 | self.as_bytes() 239 | } 240 | 241 | fn content_type(&self) -> Option<&str> { 242 | Some("text/plain") 243 | } 244 | } 245 | 246 | impl AsBody for String { 247 | fn contents(&self) -> &[u8] { 248 | self.as_bytes() 249 | } 250 | 251 | fn content_type(&self) -> Option<&str> { 252 | Some("text/plain") 253 | } 254 | } 255 | 256 | impl AsBody for &[u8] { 257 | fn contents(&self) -> &[u8] { 258 | self 259 | } 260 | 261 | fn content_type(&self) -> Option<&str> { 262 | Some("application/octet-stream") 263 | } 264 | } 265 | 266 | impl AsBody for &[u8; N] { 267 | fn contents(&self) -> &[u8] { 268 | *self 269 | } 270 | 271 | fn content_type(&self) -> Option<&str> { 272 | Some("application/octet-stream") 273 | } 274 | } 275 | 276 | #[cfg(test)] 277 | mod tests { 278 | use super::*; 279 | 280 | #[test] 281 | fn as_body_impls() { 282 | ().contents(); 283 | "".contents(); 284 | "".as_bytes().contents(); 285 | } 286 | 287 | #[test] 288 | fn case_insensitive_request_headers() { 289 | let request = Request::new( 290 | Method::Get, 291 | "/", 292 | "", 293 | vec![("FOO", b"bar"), ("abc", b"xyz")], 294 | b"", 295 | ); 296 | 297 | assert_eq!(request.header("foo"), Some("bar".as_bytes())); 298 | assert_eq!(request.header("ABC"), Some("xyz".as_bytes())); 299 | } 300 | 301 | // TODO: case_insensitive_response_headers 302 | } 303 | -------------------------------------------------------------------------------- /src/sync/channel.rs: -------------------------------------------------------------------------------- 1 | //! ... can't block when cancelled: can read if not empty, can send if not full. 2 | 3 | use std::cell::RefCell; 4 | use std::collections::VecDeque; 5 | use std::rc::Rc; 6 | 7 | use crate::runtime; 8 | use crate::runtime::is_cancelled; 9 | 10 | pub fn unbounded() -> (Sender, Receiver) { 11 | let state = Rc::new(RefCell::new(ChannelState { 12 | no_longer_empty: VecDeque::new(), 13 | queue: VecDeque::new(), 14 | is_closed: false, 15 | })); 16 | 17 | let tx = Sender(Rc::new(SenderState { 18 | state: state.clone(), 19 | })); 20 | 21 | let rx = Receiver(Rc::new(ReceiverState { state })); 22 | 23 | (tx, rx) 24 | } 25 | 26 | /// ... 27 | #[derive(Debug, Clone)] 28 | pub struct Sender(Rc>); 29 | 30 | impl Sender { 31 | /// ... 32 | pub fn send(&self, data: T) -> Result<(), crate::Error> { 33 | let mut state = self.0.state.borrow_mut(); 34 | 35 | if state.is_closed { 36 | println!("recv: closed"); 37 | return Err(crate::Error::Original(ClosedError)); 38 | } 39 | 40 | state.queue.push_back(data); 41 | 42 | if let Some(waker) = state.no_longer_empty.pop_front() { 43 | println!("sender send woke {waker:?}"); 44 | waker.schedule(); 45 | } 46 | 47 | Ok(()) 48 | } 49 | 50 | /// ... 51 | #[inline] 52 | pub fn len(&self) -> usize { 53 | let state = self.0.state.borrow(); 54 | state.queue.len() 55 | } 56 | 57 | /// ... 58 | #[inline] 59 | pub fn is_empty(&self) -> bool { 60 | self.len() == 0 61 | } 62 | 63 | /// ... 64 | #[inline] 65 | pub fn close(&self) { 66 | self.0.close(); 67 | } 68 | 69 | /// ... 70 | #[inline] 71 | pub fn is_closed(&self) -> bool { 72 | let state = self.0.state.borrow(); 73 | state.is_closed 74 | } 75 | } 76 | 77 | #[derive(Debug)] 78 | struct SenderState { 79 | state: Rc>>, 80 | } 81 | 82 | impl SenderState { 83 | fn close(&self) { 84 | let mut state = self.state.borrow_mut(); 85 | state.is_closed = true; 86 | 87 | for waker in state.no_longer_empty.drain(..) { 88 | println!("sender close woke {waker:?}"); 89 | waker.schedule(); 90 | } 91 | } 92 | } 93 | 94 | impl Drop for SenderState { 95 | fn drop(&mut self) { 96 | self.close(); 97 | } 98 | } 99 | 100 | /// ... 101 | #[derive(Debug, Clone)] 102 | pub struct Receiver(Rc>); 103 | 104 | impl Receiver { 105 | /// ... 106 | pub fn recv(&self) -> Result> { 107 | loop { 108 | let mut state = self.0.state.borrow_mut(); 109 | 110 | if let Some(message) = state.queue.pop_front() { 111 | println!("recv: value"); 112 | break Ok(message); 113 | } 114 | 115 | if state.is_closed { 116 | println!("recv: closed"); 117 | break Err(crate::Error::Original(ClosedError)); 118 | } 119 | 120 | if is_cancelled() { 121 | println!("recv: cancelled"); 122 | return Err(crate::Error::Cancelled); 123 | } 124 | 125 | runtime::park(|waker| { 126 | state.no_longer_empty.push_back(waker); 127 | drop(state); 128 | }); // woken up by sender or cancellation 129 | } 130 | } 131 | 132 | /// ... 133 | #[inline] 134 | pub fn len(&self) -> usize { 135 | let state = self.0.state.borrow(); 136 | state.queue.len() 137 | } 138 | 139 | /// ... 140 | #[inline] 141 | pub fn is_empty(&self) -> bool { 142 | self.len() == 0 143 | } 144 | 145 | /// ... 146 | #[inline] 147 | pub fn close(&self) { 148 | let mut state = self.0.state.borrow_mut(); 149 | state.is_closed = true; 150 | } 151 | 152 | /// ... 153 | #[inline] 154 | pub fn is_closed(&self) -> bool { 155 | let state = self.0.state.borrow(); 156 | state.is_closed 157 | } 158 | } 159 | 160 | impl Iterator for Receiver { 161 | type Item = T; 162 | 163 | fn next(&mut self) -> Option { 164 | self.recv().ok() 165 | } 166 | } 167 | 168 | #[derive(Debug)] 169 | struct ReceiverState { 170 | state: Rc>>, 171 | } 172 | 173 | impl Drop for ReceiverState { 174 | fn drop(&mut self) { 175 | let mut state = self.state.borrow_mut(); 176 | state.is_closed = true; 177 | } 178 | } 179 | 180 | #[derive(Debug)] 181 | struct ChannelState { 182 | no_longer_empty: VecDeque, 183 | queue: VecDeque, 184 | is_closed: bool, 185 | } 186 | 187 | /// ... 188 | #[derive(Debug, PartialEq)] 189 | pub struct ClosedError; 190 | 191 | #[cfg(test)] 192 | mod tests { 193 | use runtime::{spawn, start}; 194 | 195 | use crate::runtime::cancel; 196 | 197 | use super::*; 198 | 199 | #[test] 200 | fn send_then_receive() { 201 | start(|| { 202 | let (tx, rx) = unbounded(); 203 | 204 | tx.send(1).unwrap(); 205 | tx.send(2).unwrap(); 206 | tx.send(3).unwrap(); 207 | 208 | assert_eq!(rx.recv(), Ok(1)); 209 | assert_eq!(rx.recv(), Ok(2)); 210 | assert_eq!(rx.recv(), Ok(3)); 211 | }) 212 | .unwrap(); 213 | } 214 | 215 | #[test] 216 | fn receive_then_send() { 217 | start(|| { 218 | let (tx, rx) = unbounded(); 219 | 220 | spawn(move || { 221 | tx.send(1).unwrap(); 222 | }); 223 | 224 | assert_eq!(rx.recv(), Ok(1)); 225 | assert_eq!(rx.recv(), Err(crate::Error::Original(ClosedError))); 226 | }) 227 | .unwrap(); 228 | } 229 | 230 | #[test] 231 | fn sender_close_stops_recv() { 232 | start(|| { 233 | let (tx, rx) = unbounded::<()>(); 234 | let handle = spawn(move || rx.recv()); 235 | 236 | tx.close(); 237 | let result = handle.join().unwrap(); 238 | 239 | assert_eq!(result, Err(crate::Error::Original(ClosedError))); 240 | }) 241 | .unwrap(); 242 | } 243 | 244 | #[test] 245 | fn sender_drop_stops_recv() { 246 | start(|| { 247 | let (tx, rx) = unbounded::<()>(); 248 | let handle = spawn(move || rx.recv()); 249 | 250 | drop(tx); 251 | let result = handle.join().unwrap(); 252 | 253 | assert_eq!(result, Err(crate::Error::Original(ClosedError))); 254 | }) 255 | .unwrap(); 256 | } 257 | 258 | #[test] 259 | fn compatible_with_iterator() { 260 | start(|| { 261 | let (tx, rx) = unbounded(); 262 | tx.send(1).unwrap(); 263 | tx.send(2).unwrap(); 264 | tx.send(3).unwrap(); 265 | drop(tx); 266 | 267 | let collected: Vec<_> = rx.into_iter().collect(); 268 | 269 | assert_eq!(collected, vec![1, 2, 3]); 270 | }) 271 | .unwrap(); 272 | } 273 | 274 | mod cancellation { 275 | use super::*; 276 | 277 | #[test] 278 | fn can_always_send_to_unbounded_channel() { 279 | start(|| { 280 | let (tx, _rx) = unbounded(); 281 | cancel(); 282 | 283 | assert!(tx.send(()).is_ok()); 284 | }) 285 | .unwrap(); 286 | } 287 | 288 | #[test] 289 | fn stops_active_recv() { 290 | start(|| { 291 | let (_tx, rx) = unbounded::<()>(); 292 | let handle = spawn(move || rx.recv()); 293 | 294 | handle.cancel(); 295 | let result = handle.join().unwrap(); 296 | 297 | assert_eq!(result, Err(crate::Error::Cancelled)); 298 | }) 299 | .unwrap(); 300 | } 301 | 302 | #[test] 303 | fn fails_blocking_recv() { 304 | start(|| { 305 | let (tx, rx) = unbounded(); 306 | tx.send(1).unwrap(); 307 | cancel(); 308 | 309 | assert_eq!(rx.recv(), Ok(1)); 310 | assert_eq!(rx.recv(), Err(crate::Error::Cancelled)); 311 | }) 312 | .unwrap(); 313 | } 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /src/net/tcp.rs: -------------------------------------------------------------------------------- 1 | //! ... 2 | 3 | use std::cell::RefCell; 4 | use std::io::{Read, Write}; 5 | use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}; 6 | use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd, RawFd}; 7 | use std::rc::Rc; 8 | use std::{io, mem}; 9 | 10 | use crate::{runtime, IoResult}; 11 | 12 | /// ... 13 | pub fn connect(address: impl super::ToSocketAddrs) -> IoResult<(WriteHalf, ReadHalf)> { 14 | let address = address.to_socket_addrs()?.next().unwrap().to_string(); 15 | 16 | // TODO: ensure runtime exists 17 | // TODO: take std::net::IpAddr (dns -> happy eyes) 18 | // TODO: do this manually: https://www.geeksforgeeks.org/tcp-server-client-implementation-in-c/ 19 | // let sqe = io_uring::opcode::Connect::new().build(); // TODO: benchmark difference! 20 | let stream = std::net::TcpStream::connect(address).unwrap(); 21 | let fd = stream.into_raw_fd(); 22 | 23 | let state = Rc::new(RefCell::new(StreamState { fd })); 24 | 25 | Ok((WriteHalf(state.clone()), ReadHalf(state))) 26 | } 27 | 28 | /// ... 29 | pub struct WriteHalf(Rc>); 30 | 31 | impl Write for WriteHalf { 32 | fn write(&mut self, buffer: &[u8]) -> io::Result { 33 | let fd = io_uring::types::Fd(self.0.borrow().fd); 34 | let sqe = io_uring::opcode::Send::new(fd, buffer.as_ptr(), buffer.len() as u32).build(); 35 | let bytes_wrote = runtime::syscall(sqe)?; 36 | Ok(bytes_wrote as usize) 37 | } 38 | 39 | fn flush(&mut self) -> io::Result<()> { 40 | Ok(()) 41 | } 42 | } 43 | 44 | /// ... 45 | pub struct ReadHalf(Rc>); 46 | 47 | impl Read for ReadHalf { 48 | fn read(&mut self, buffer: &mut [u8]) -> io::Result { 49 | let fd = io_uring::types::Fd(self.0.borrow().fd); 50 | let sqe = io_uring::opcode::Recv::new(fd, buffer.as_mut_ptr(), buffer.len() as u32).build(); 51 | let bytes_read = runtime::syscall(sqe)?; 52 | Ok(bytes_read as usize) 53 | } 54 | } 55 | 56 | #[derive(Debug)] 57 | struct StreamState { 58 | fd: RawFd, 59 | } 60 | 61 | /// ... 62 | #[derive(Debug)] 63 | pub struct Listener(RawFd); 64 | 65 | impl Listener { 66 | /// ... 67 | pub fn bind(address: impl super::ToSocketAddrs) -> crate::IoResult { 68 | // FIXME non-blocking 69 | let address = address.to_socket_addrs()?.next().unwrap().to_string(); 70 | let listener = std::net::TcpListener::bind(address)?; 71 | let fd = listener.as_raw_fd(); 72 | mem::forget(listener); 73 | 74 | Ok(Listener(fd)) 75 | } 76 | 77 | /// ... 78 | pub fn accept(&self) -> crate::IoResult<((WriteHalf, ReadHalf), SocketAddr)> { 79 | let fd = io_uring::types::Fd(self.0); 80 | let mut storage: libc::sockaddr_storage = unsafe { mem::zeroed() }; 81 | let mut length = mem::size_of_val(&storage) as libc::socklen_t; 82 | let sqe = io_uring::opcode::Accept::new(fd, &mut storage as *mut _ as *mut _, &mut length) 83 | .flags(libc::SOCK_CLOEXEC) 84 | .build(); 85 | let fd = runtime::syscall(sqe)?; 86 | 87 | let fd = RawFd::from(fd as i32); 88 | let state = Rc::new(RefCell::new(StreamState { fd })); 89 | let stream = (WriteHalf(state.clone()), ReadHalf(state)); 90 | 91 | let addr = sockaddr_to_addr(&storage, length as usize)?; 92 | 93 | Ok((stream, addr)) 94 | } 95 | 96 | // TODO: incoming, into_incoming 97 | /// not the same as std library! can return None... 98 | pub fn into_incoming(self) -> IntoIncoming { 99 | IntoIncoming(self) 100 | } 101 | 102 | /// ... 103 | pub fn local_addr(&self) -> crate::IoResult { 104 | let listener = unsafe { std::net::TcpListener::from_raw_fd(self.0) }; 105 | let addr = listener.local_addr()?; 106 | mem::forget(listener); 107 | Ok(addr) 108 | } 109 | 110 | /// ... 111 | pub fn set_ttl(&self, ttl: u32) -> crate::IoResult<()> { 112 | let listener = unsafe { std::net::TcpListener::from_raw_fd(self.0) }; 113 | listener.set_ttl(ttl)?; 114 | mem::forget(listener); 115 | Ok(()) 116 | } 117 | 118 | /// ... 119 | pub fn ttl(&self) -> crate::IoResult { 120 | let listener = unsafe { std::net::TcpListener::from_raw_fd(self.0) }; 121 | let ttl = listener.ttl()?; 122 | mem::forget(listener); 123 | Ok(ttl) 124 | } 125 | 126 | // TODO: take_error SO_ERROR 127 | } 128 | 129 | impl Drop for Listener { 130 | fn drop(&mut self) { 131 | let fd = io_uring::types::Fd(self.0); 132 | let sqe = io_uring::opcode::Close::new(fd).build(); 133 | let _ = runtime::syscall(sqe); 134 | } 135 | } 136 | 137 | /// ... 138 | pub struct IntoIncoming(Listener); 139 | 140 | impl Iterator for IntoIncoming { 141 | type Item = (WriteHalf, ReadHalf); 142 | 143 | fn next(&mut self) -> Option { 144 | self.0.accept().map(|(s, _)| s).ok() 145 | } 146 | } 147 | 148 | fn sockaddr_to_addr(storage: &libc::sockaddr_storage, length: usize) -> io::Result { 149 | match storage.ss_family as libc::c_int { 150 | libc::AF_INET => { 151 | assert!(length >= mem::size_of::()); 152 | let addr = unsafe { *(storage as *const _ as *const libc::sockaddr_in) }; 153 | 154 | Ok(SocketAddr::V4(SocketAddrV4::new( 155 | Ipv4Addr::from(addr.sin_addr.s_addr.to_ne_bytes()), 156 | u16::from_be(addr.sin_port), 157 | ))) 158 | } 159 | libc::AF_INET6 => { 160 | assert!(length >= mem::size_of::()); 161 | let addr = unsafe { *(storage as *const _ as *const libc::sockaddr_in6) }; 162 | 163 | Ok(SocketAddr::V6(SocketAddrV6::new( 164 | Ipv6Addr::from(addr.sin6_addr.s6_addr), 165 | u16::from_be(addr.sin6_port), 166 | addr.sin6_flowinfo, 167 | addr.sin6_scope_id, 168 | ))) 169 | } 170 | _ => Err(io::Error::new( 171 | io::ErrorKind::InvalidInput, 172 | "invalid argument", 173 | )), 174 | } 175 | } 176 | 177 | #[cfg(test)] 178 | mod tests { 179 | use std::io::{Read, Write}; 180 | use std::net::Ipv4Addr; 181 | 182 | use crate::runtime::{spawn, start}; 183 | 184 | use super::*; 185 | 186 | #[test] 187 | fn smoke() { 188 | start(|| { 189 | let listener = Listener::bind((Ipv4Addr::UNSPECIFIED, 0)).unwrap(); 190 | let server_addr = listener.local_addr().unwrap(); 191 | 192 | let (client_addr_handle, _client_addr) = crate::sync::channel::unbounded(); 193 | 194 | spawn(move || { 195 | let ((mut w, mut r), address) = listener.accept().unwrap(); 196 | client_addr_handle.send(address).unwrap(); 197 | 198 | let mut buffer = vec![0; 1024]; 199 | let bytes_read = r.read(&mut buffer).unwrap(); 200 | w.write_all(&buffer[..bytes_read]).unwrap(); 201 | }); 202 | 203 | let (mut w, mut r) = connect((Ipv4Addr::LOCALHOST, server_addr.port())).unwrap(); 204 | // assert_eq!(w.addr(), server_addr); 205 | // assert_eq!(r.addr(), client_addr.recv().unwrap()); 206 | 207 | w.write_all(b"hello").unwrap(); 208 | 209 | let mut buffer = vec![0; 1024]; 210 | let bytes_read = r.read(&mut buffer).unwrap(); 211 | assert_eq!(&buffer[..bytes_read], b"hello"); 212 | }) 213 | .unwrap(); 214 | } 215 | 216 | // #[test] 217 | // // #[ignore = "takes 16s to run in release mode"] 218 | // fn cleans_up_after_itself() { 219 | // start(|| { 220 | // // enough to hit OS limits 221 | // for _ in 0..1_000_000 { 222 | // // FIXME: this hits the wrong os limits: called `Result::unwrap()` on an `Err` value: Original(Os { code: 98, kind: AddrInUse, message: "Address already in use" }) 223 | // // need to set flag on tcp stream to prevent wait state 224 | // let listener = Listener::bind((Ipv4Addr::UNSPECIFIED, 0)).unwrap(); 225 | // let port = listener.local_addr().unwrap().port(); 226 | // let server = spawn(move || drop(listener.accept().unwrap().0)); 227 | // drop(connect((Ipv4Addr::LOCALHOST, port)).unwrap()); 228 | // server.join().unwrap(); 229 | // } 230 | // }) 231 | // .unwrap(); 232 | // } 233 | } 234 | -------------------------------------------------------------------------------- /src/circular_buffer.rs: -------------------------------------------------------------------------------- 1 | //! Circular buffer data structure for byte streams. 2 | //! 3 | //! A virtual memory trick is used to efficiently implement the circular buffer. 4 | //! Two consecutive blocks of virtual memory are mapped to the same physical memory. 5 | //! Consumers are able to read the full message despite it being split between the end and start of the buffer. 6 | //! Producers can also transparently write into both the end and start of the buffer. 7 | //! 8 | //! physical memory: D E 0 0 0 0 A B C 9 | //! ^tail ^head 10 | //! 11 | //! virtual memory: D E 0 0 0 0 A B C D E 0 0 0 0 A B C 12 | //! \-------/ continuous 13 | //! 14 | //! 2MB huge pages can be enabled with the [huge_pages] cargo feature. 15 | //! https://www.kernel.org/doc/Documentation/admin-guide/mm/hugetlbpage.rst 16 | //! 17 | //! This design has several downsides: 18 | //! - Buffer size has to be a multiple of the page size (4KB). 19 | //! - It takes several blocking syscalls to set up and tear down (~16μs). 20 | //! - Memory maps occupy several entries in the TLB. 21 | //! - It doesn't work in `no_std` environments. 22 | 23 | use std::cell::RefCell; 24 | use std::os::fd::{AsRawFd, FromRawFd}; 25 | use std::rc::Rc; 26 | use std::{ffi, io, ops, ptr, slice}; 27 | 28 | /// ... 29 | /// minimum [length] in bytes. 30 | pub fn circular_buffer(length: usize) -> io::Result<(Data, Uninit)> { 31 | let length = calculate_length(length)?; 32 | 33 | // setup physical memory 34 | let file = anonymous_file()?; 35 | file.set_len(length as u64)?; 36 | 37 | // setup virtual memory mappings 38 | let pointer = anonymous_mapping(2 * length)?; 39 | file_mapping(&file, pointer, length)?; 40 | file_mapping(&file, unsafe { pointer.byte_add(length) }, length)?; 41 | 42 | let state = Rc::new(RefCell::new(State { 43 | _file: file, 44 | pointer, 45 | head: 0, 46 | tail: 0, 47 | length, 48 | })); 49 | 50 | Ok((Data(state.clone()), Uninit(state))) 51 | } 52 | 53 | fn calculate_length(length: usize) -> io::Result { 54 | let page_size = if cfg!(feature = "huge_pages") { 55 | 2 * 1024 * 1024 56 | } else { 57 | unsafe { libc::sysconf(libc::_SC_PAGESIZE) as usize } 58 | }; 59 | 60 | length 61 | .checked_next_multiple_of(page_size) 62 | .and_then(usize::checked_next_power_of_two) 63 | .ok_or(io::Error::new( 64 | io::ErrorKind::InvalidInput, 65 | "invalid circular buffer size", 66 | )) 67 | } 68 | 69 | /// ... 70 | #[derive(Debug)] 71 | pub struct Data(Rc>); 72 | 73 | impl Data { 74 | /// ... data -> uninit 75 | pub fn consume(&mut self, capacity: usize) { 76 | let mut state = self.0.borrow_mut(); 77 | state.head = state.head.overflowing_add(capacity).0; // safe to overflow due to power of two length 78 | assert!(state.head <= state.tail); 79 | } 80 | 81 | /// ... 82 | pub fn len(&self) -> usize { 83 | let state = self.0.borrow(); 84 | state.data_len() 85 | } 86 | 87 | /// ... 88 | pub fn is_empty(&self) -> bool { 89 | self.len() == 0 90 | } 91 | } 92 | 93 | impl ops::Deref for Data { 94 | type Target = [u8]; 95 | 96 | fn deref(&self) -> &Self::Target { 97 | let state = self.0.borrow(); 98 | unsafe { 99 | slice::from_raw_parts( 100 | state.pointer.byte_add(p2_modulo(state.head, state.length)) as *const u8, 101 | state.data_len(), 102 | ) 103 | } 104 | } 105 | } 106 | 107 | impl ops::DerefMut for Data { 108 | fn deref_mut(&mut self) -> &mut Self::Target { 109 | let state = self.0.borrow_mut(); 110 | unsafe { 111 | slice::from_raw_parts_mut( 112 | state.pointer.byte_add(p2_modulo(state.head, state.length)) as *mut u8, 113 | state.data_len(), 114 | ) 115 | } 116 | } 117 | } 118 | 119 | /// ... 120 | #[derive(Debug)] 121 | pub struct Uninit(Rc>); 122 | 123 | impl Uninit { 124 | /// ... uninit -> data 125 | pub fn commit(&mut self, capacity: usize) { 126 | assert!(capacity <= self.len()); 127 | let mut state = self.0.borrow_mut(); 128 | state.tail = state.tail.overflowing_add(capacity).0; // safe to overflow due to power of two length 129 | } 130 | 131 | /// ... 132 | pub fn len(&self) -> usize { 133 | let state = self.0.borrow(); 134 | state.uninit_len() 135 | } 136 | 137 | /// ... 138 | pub fn is_empty(&self) -> bool { 139 | self.len() == 0 140 | } 141 | } 142 | 143 | impl ops::Deref for Uninit { 144 | type Target = [u8]; 145 | 146 | fn deref(&self) -> &Self::Target { 147 | let state = self.0.borrow(); 148 | unsafe { 149 | slice::from_raw_parts( 150 | state.pointer.byte_add(p2_modulo(state.tail, state.length)) as *const u8, 151 | state.uninit_len(), 152 | ) 153 | } 154 | } 155 | } 156 | 157 | impl ops::DerefMut for Uninit { 158 | fn deref_mut(&mut self) -> &mut Self::Target { 159 | let state = self.0.borrow_mut(); 160 | unsafe { 161 | slice::from_raw_parts_mut( 162 | state.pointer.byte_add(p2_modulo(state.tail, state.length)) as *mut u8, 163 | state.uninit_len(), 164 | ) 165 | } 166 | } 167 | } 168 | 169 | #[derive(Debug)] 170 | struct State { 171 | _file: std::fs::File, 172 | pointer: *mut ffi::c_void, 173 | head: usize, 174 | tail: usize, 175 | length: usize, 176 | } 177 | 178 | impl State { 179 | fn data_len(&self) -> usize { 180 | self.tail - self.head 181 | } 182 | 183 | fn uninit_len(&self) -> usize { 184 | self.length - self.data_len() 185 | } 186 | } 187 | 188 | impl Drop for State { 189 | fn drop(&mut self) { 190 | let _ = remove_mapping(self.pointer, 2 * self.length); 191 | let _ = remove_mapping(self.pointer, self.length); 192 | let _ = remove_mapping(unsafe { self.pointer.byte_add(self.length) }, self.length); 193 | } 194 | } 195 | 196 | /// Bit-hacking optimization for calculating a number mod a power of two. 197 | unsafe fn p2_modulo(n: usize, m: usize) -> usize { 198 | debug_assert!(m.is_power_of_two()); 199 | n & (m - 1) 200 | } 201 | 202 | fn anonymous_file() -> io::Result { 203 | let mut flags = libc::MFD_CLOEXEC; 204 | if cfg!(feature = "huge_pages") { 205 | flags |= libc::MFD_HUGETLB; 206 | } 207 | 208 | let fd = unsafe { libc::memfd_create(b"circular-buffer\0".as_ptr() as _, flags) }; 209 | if fd == -1 { 210 | let error = io::Error::last_os_error(); 211 | return Err(error); 212 | } 213 | 214 | let file = unsafe { std::fs::File::from_raw_fd(fd) }; 215 | 216 | Ok(file) 217 | } 218 | 219 | fn anonymous_mapping(size: usize) -> io::Result<*mut ffi::c_void> { 220 | let mut flags = libc::MAP_PRIVATE | libc::MAP_ANONYMOUS; 221 | if cfg!(feature = "huge_pages") { 222 | flags |= libc::MAP_HUGETLB; 223 | } 224 | 225 | let pointer = unsafe { libc::mmap(ptr::null_mut(), size, libc::PROT_NONE, flags, -1, 0) }; 226 | 227 | if pointer == libc::MAP_FAILED { 228 | let error = io::Error::last_os_error(); 229 | return Err(error); 230 | } 231 | 232 | Ok(pointer) 233 | } 234 | 235 | fn remove_mapping(pointer: *mut ffi::c_void, size: usize) -> io::Result<()> { 236 | let result = unsafe { libc::munmap(pointer, size) }; 237 | if result == -1 { 238 | let error = io::Error::last_os_error(); 239 | return Err(error); 240 | } 241 | 242 | Ok(()) 243 | } 244 | 245 | fn file_mapping(file: &std::fs::File, pointer: *mut ffi::c_void, size: usize) -> io::Result<()> { 246 | let mut flags = libc::MAP_SHARED | libc::MAP_FIXED; 247 | if cfg!(feature = "huge_pages") { 248 | flags |= libc::MAP_HUGETLB; 249 | } 250 | 251 | let pointer = unsafe { 252 | libc::mmap( 253 | pointer, 254 | size, 255 | libc::PROT_READ | libc::PROT_WRITE, 256 | flags, 257 | file.as_raw_fd(), 258 | 0, 259 | ) 260 | }; 261 | 262 | if pointer == libc::MAP_FAILED { 263 | let error = io::Error::last_os_error(); 264 | return Err(error); 265 | } 266 | 267 | Ok(()) 268 | } 269 | 270 | #[cfg(test)] 271 | mod tests { 272 | use super::*; 273 | 274 | #[test] 275 | fn starts_uninitialized() { 276 | let (data, uninit) = circular_buffer(4096).unwrap(); 277 | 278 | assert!(data.is_empty()); 279 | assert_eq!(uninit.len(), 4096); 280 | } 281 | 282 | #[test] 283 | fn rounds_up_length_to_nearest_page_size() { 284 | let (_, uninit) = circular_buffer(1).unwrap(); 285 | 286 | assert!(uninit.len() > 1); 287 | assert!(uninit.len().is_power_of_two()); 288 | } 289 | 290 | #[test] 291 | fn commits_uninit() { 292 | let (data, mut uninit) = circular_buffer(4096).unwrap(); 293 | 294 | uninit[..2].copy_from_slice(b"hi"); 295 | uninit.commit(2); 296 | 297 | assert_eq!(data.as_ref(), b"hi"); 298 | assert_eq!(uninit.len(), 4096 - 2); 299 | } 300 | 301 | #[test] 302 | fn consumes_data() { 303 | let (mut data, mut uninit) = circular_buffer(4096).unwrap(); 304 | uninit[..2].copy_from_slice(b"hi"); 305 | uninit.commit(2); 306 | 307 | assert_eq!(&data[..1], b"h"); 308 | data.consume(1); 309 | 310 | assert_eq!(data.as_ref(), b"i"); 311 | assert_eq!(uninit.len(), 4096 - 1); 312 | } 313 | 314 | #[test] 315 | fn data_spans_across_boundary() { 316 | let (mut data, mut uninit) = circular_buffer(4096).unwrap(); 317 | uninit.commit(uninit.len() - 1); 318 | data.consume(data.len()); 319 | 320 | uninit[..2].copy_from_slice(b"hi"); 321 | uninit.commit(2); 322 | 323 | assert_eq!(data.as_ref(), b"hi"); 324 | } 325 | 326 | #[test] 327 | fn uninit_spans_across_boundary() { 328 | let (mut data, mut uninit) = circular_buffer(4096).unwrap(); 329 | 330 | uninit.commit(42); 331 | data.consume(42); 332 | 333 | assert_eq!(uninit.len(), 4096); 334 | } 335 | 336 | #[test] 337 | #[should_panic] 338 | fn cant_consume_more_than_committed() { 339 | let (mut data, _) = circular_buffer(4096).unwrap(); 340 | 341 | data.consume(data.len() + 1); 342 | } 343 | 344 | #[test] 345 | #[should_panic] 346 | fn cant_commit_more_than_uninitialized() { 347 | let (_, mut uninit) = circular_buffer(4096).unwrap(); 348 | 349 | uninit.commit(uninit.len() + 1); 350 | } 351 | 352 | #[test] 353 | #[ignore = "takes 16s to run in release mode"] 354 | fn cleans_up_after_itself() { 355 | // enough to hit OS limits 356 | for _ in 0..1_000_000 { 357 | drop(circular_buffer(4096).unwrap()); 358 | } 359 | } 360 | } 361 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Uringy 2 | ====== 3 | 4 | [![website]](https://uringy-documentation.fly.dev/) 5 | [![github]](https://github.com/Dennis-Krasnov/Uringy) 6 | [![crates-io]](https://crates.io/crates/uringy) 7 | [![docs-rs]](https://docs.rs/uringy) 8 | [![license]](https://github.com/Dennis-Krasnov/Uringy/blob/master/LICENSE) 9 | 10 | [website]: https://img.shields.io/static/v1?label=website&message=uringy-documentation.fly.dev&style=for-the-badge&labelColor=555555&color=blue&logo=github 11 | [github]: https://img.shields.io/static/v1?label=github&message=Dennis-Krasnov/Uringy&style=for-the-badge&labelColor=555555&color=17c208&logo=github 12 | [crates-io]: https://img.shields.io/crates/v/uringy.svg?style=for-the-badge&logo=image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMjM5LjEgNi4zbC0yMDggNzhjLTE4LjcgNy0zMS4xIDI1LTMxLjEgNDV2MjI1LjFjMCAxOC4yIDEwLjMgMzQuOCAyNi41IDQyLjlsMjA4IDEwNGMxMy41IDYuOCAyOS40IDYuOCA0Mi45IDBsMjA4LTEwNGMxNi4zLTguMSAyNi41LTI0LjggMjYuNS00Mi45VjEyOS4zYzAtMjAtMTIuNC0zNy45LTMxLjEtNDQuOWwtMjA4LTc4QzI2MiAyLjIgMjUwIDIuMiAyMzkuMSA2LjN6TTI1NiA2OC40bDE5MiA3MnYxLjFsLTE5MiA3OC0xOTItNzh2LTEuMWwxOTItNzJ6bTMyIDM1NlYyNzUuNWwxNjAtNjV2MTMzLjlsLTE2MCA4MHoiPjwvcGF0aD48L3N2Zz4= 13 | [docs-rs]: https://img.shields.io/static/v1?label=docs.rs&message=uringy&style=for-the-badge&labelColor=555555&color=red&logo=data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDUxMiA1MTIiPjxwYXRoIGZpbGw9IiNmNWY1ZjUiIGQ9Ik00ODguNiAyNTAuMkwzOTIgMjE0VjEwNS41YzAtMTUtOS4zLTI4LjQtMjMuNC0zMy43bC0xMDAtMzcuNWMtOC4xLTMuMS0xNy4xLTMuMS0yNS4zIDBsLTEwMCAzNy41Yy0xNC4xIDUuMy0yMy40IDE4LjctMjMuNCAzMy43VjIxNGwtOTYuNiAzNi4yQzkuMyAyNTUuNSAwIDI2OC45IDAgMjgzLjlWMzk0YzAgMTMuNiA3LjcgMjYuMSAxOS45IDMyLjJsMTAwIDUwYzEwLjEgNS4xIDIyLjEgNS4xIDMyLjIgMGwxMDMuOS01MiAxMDMuOSA1MmMxMC4xIDUuMSAyMi4xIDUuMSAzMi4yIDBsMTAwLTUwYzEyLjItNi4xIDE5LjktMTguNiAxOS45LTMyLjJWMjgzLjljMC0xNS05LjMtMjguNC0yMy40LTMzLjd6TTM1OCAyMTQuOGwtODUgMzEuOXYtNjguMmw4NS0zN3Y3My4zek0xNTQgMTA0LjFsMTAyLTM4LjIgMTAyIDM4LjJ2LjZsLTEwMiA0MS40LTEwMi00MS40di0uNnptODQgMjkxLjFsLTg1IDQyLjV2LTc5LjFsODUtMzguOHY3NS40em0wLTExMmwtMTAyIDQxLjQtMTAyLTQxLjR2LS42bDEwMi0zOC4yIDEwMiAzOC4ydi42em0yNDAgMTEybC04NSA0Mi41di03OS4xbDg1LTM4Ljh2NzUuNHptMC0xMTJsLTEwMiA0MS40LTEwMi00MS40di0uNmwxMDItMzguMiAxMDIgMzguMnYuNnoiPjwvcGF0aD48L3N2Zz4K 14 | [license]: https://img.shields.io/static/v1?label=license&message=BSD0&style=for-the-badge&labelColor=555555&color=b509a4&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NDAgNTEyIj48cGF0aCBmaWxsPSIjZmZmIiBkPSJNOTYgNDY0djMyYzAgOC44NCA3LjE2IDE2IDE2IDE2aDIyNGM4Ljg0IDAgMTYtNy4xNiAxNi0xNlYxNTMuMjVjNC41Ni0yIDguOTItNC4zNSAxMi45OS03LjEybDE0Mi4wNSA0Ny42M2M4LjM4IDIuODEgMTcuNDUtMS43MSAyMC4yNi0xMC4wOGwxMC4xNy0zMC4zNGMyLjgxLTguMzgtMS43MS0xNy40NS0xMC4wOC0yMC4yNmwtMTI4LjQtNDMuMDVjLjQyLTMuMzIgMS4wMS02LjYgMS4wMS0xMC4wMyAwLTQ0LjE4LTM1LjgyLTgwLTgwLTgwLTI5LjY5IDAtNTUuMyAxNi4zNi02OS4xMSA0MC4zN0wxMzIuOTYuODNjLTguMzgtMi44MS0xNy40NSAxLjcxLTIwLjI2IDEwLjA4bC0xMC4xNyAzMC4zNGMtMi44MSA4LjM4IDEuNzEgMTcuNDUgMTAuMDggMjAuMjZsMTMyIDQ0LjI2YzcuMjggMjEuMjUgMjIuOTYgMzguNTQgNDMuMzggNDcuNDdWNDQ4SDExMmMtOC44NCAwLTE2IDcuMTYtMTYgMTZ6TTAgMzA0YzAgNDQuMTggNTcuMzEgODAgMTI4IDgwczEyOC0zNS44MiAxMjgtODBoLS4wMmMwLTE1LjY3IDIuMDgtNy4yNS04NS4wNS0xODEuNTEtMTcuNjgtMzUuMzYtNjguMjItMzUuMjktODUuODcgMEMtMS4zMiAyOTUuMjcuMDIgMjg3LjgyLjAyIDMwNEgwem01Ni0xNmw3Mi0xNDQgNzIgMTQ0SDU2em0zMjguMDIgMTQ0SDM4NGMwIDQ0LjE4IDU3LjMxIDgwIDEyOCA4MHMxMjgtMzUuODIgMTI4LTgwaC0uMDJjMC0xNS42NyAyLjA4LTcuMjUtODUuMDUtMTgxLjUxLTE3LjY4LTM1LjM2LTY4LjIyLTM1LjI5LTg1Ljg3IDAtODYuMzggMTcyLjc4LTg1LjA0IDE2NS4zMy04NS4wNCAxODEuNTF6TTQ0MCA0MTZsNzItMTQ0IDcyIDE0NEg0NDB6Ij48L3BhdGg+PC9zdmc+ 15 | 16 | Writing concurrent code in Rust doesn't need to be painful. 17 | Uringy is a runtime that combines structured concurrency, a single-threaded design, and Linux's io_uring. 18 | Intended for server applications, from simple single-threaded to highly scalable thread-per-core designs. 19 | 20 | ## Goals 21 | #### Simple API 22 | - Familiar blocking syntax which closely mirrors Rust's standard library 23 | - Avoid `async`/`await`'s limitations and footguns 24 | - Easy to learn with stellar documentation and examples 25 | - Spawn with non-`Send` and non-`'static` types 26 | - Leak-free hierarchy of fibers with first-class cancellation support 27 | 28 | #### Performant 29 | - Issue non-blocking, batched, zero-copy syscalls with io_uring 30 | - Efficient context switching with cooperative multitasking 31 | - Atomic-free scheduler, parallelized manually if required 32 | 33 | #### Quick to compile 34 | - Compile only what you need using [cargo features](#Compile Time Flags) 35 | - Minimal dependencies 36 | - Minimal use of macros 37 | 38 | ## Quick Start 39 | [Install Rust](https://www.rust-lang.org/tools/install) and [create a new cargo project](https://doc.rust-lang.org/book/ch01-03-hello-cargo.html). 40 | 41 | Add uringy as a dependency: `cargo add uringy` 42 | 43 | Then replace `src/main.rs` with: 44 | ```rust 45 | // No need for async main 46 | #[uringy::start] 47 | fn main() { 48 | let handle = uringy::fiber::spawn(|| tcp_echo_server(9000)); // No need for async block 49 | 50 | uringy::signals().filter(Signal::is_terminal).next().unwrap(); 51 | uringy::println!("gracefully shutting down"); 52 | handle.cancel(); // Cancellation propagates throughout the entire fiber hierarchy 53 | 54 | // Automatically waits for all fibers to complete 55 | } 56 | 57 | // No need for async functions 58 | fn tcp_echo_server(port: u16) { 59 | let listener = uringy::net::TcpListener::bind(("0.0.0.0", port)).unwrap(); 60 | uringy::println!("listening for TCP connections on port {port}"); // No need for .await 61 | let mut connections = listener.incoming(); 62 | while let Ok((stream, _)) = connections.next() { 63 | uringy::fiber::spawn(move || handle_connection(stream)); 64 | } 65 | } 66 | 67 | fn handle_connection(tcp: TcpStream) { 68 | let (mut r, mut w) = stream.split(); 69 | let _ = std::io::copy(&mut r, &mut w); // TcpStream implements std::io's Read and Write 70 | } 71 | ``` 72 | 73 | And run your project using: `cargo run --release` 74 | 75 | If you're using macOS, use a [Linux virtual machine](https://orbstack.dev) or a docker container. 76 | If you're using Windows, use [WSL](https://learn.microsoft.com/en-us/windows/wsl/install). 77 | 78 | For more, check out the [examples](examples) directory. 79 | 80 | ## Compile Time Flags 81 | There are currently no cargo flags. 82 | 83 | ## Comparison with Other Runtimes 84 | | | std thread | uringy fiber | tokio task | 85 | |---------------------------------------------------------------------------------------------|--------------------------------------------|----------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------| 86 | | OS support | all | Linux | most | 87 | | IO interface | blocking | [io_uring](https://unixism.net/loti) | epoll + thread pool | 88 | | [function color](https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function) | sync | sync | sync and async | 89 | | start | N/A | 27 μs | 27.5 μs (3.5 μs using [current thread scheduler](https://docs.rs/tokio/latest/tokio/runtime/struct.Runtime.html#current-thread-scheduler)) | 90 | | spawn | 9828 ns | 59 ns | 907 ns (58ns using [current thread scheduler](https://docs.rs/tokio/latest/tokio/runtime/struct.Runtime.html#current-thread-scheduler)) | 91 | | spawn `Send` bound | yes | no | yes, unless using [LocalSet](https://docs.rs/tokio/latest/tokio/task/struct.LocalSet.html) | 92 | | spawn `'static` bound | yes, unless using scope | yes, unless using scope | yes | 93 | | [stack size](https://without.boats/blog/futures-and-segmented-stacks) | virtual 8MB (configurable), 4KB increments | virtual 128KB (configurable), 4KB increments | perfectly sized | 94 | | stack limitations | may overflow | may overflow | can't use recursion | 95 | | context switch | 1405 ns | 60 ns | 1328 ns (308 ns using [current thread scheduler](https://docs.rs/tokio/latest/tokio/runtime/struct.Runtime.html#current-thread-scheduler)) | 96 | | multi-tasking | preemptive | cooperative | mostly cooperative | 97 | | structured concurrency | no guarantees | parent fiber outlives its children | no guarantees | 98 | | runs until | main thread completes | all fibers complete | block_on completes | 99 | | parallelism | automatic | manual | automatic, unless using [current thread scheduler](https://docs.rs/tokio/latest/tokio/runtime/struct.Runtime.html#current-thread-scheduler) | 100 | | userspace scheduler | N/A | minimal | work stealing | 101 | | cancellation | using esoteric unix signals | first class, voluntary | leaks memory, [causes bugs](https://docs.rs/tokio/latest/tokio/macro.select.html#cancellation-safety) | 102 | 103 | ## Supported Rust Versions 104 | The MSRV is 1.75.0 (released in December 2023). 105 | Check your Rust version by running `rustc --version` in a terminal. 106 | 107 | ## Supported Linux Kernel Versions 108 | The minimum kernel version is 6.1 (released in December 2022). 109 | Check your kernel version by running `uname -r` in a terminal. 110 | 111 | ## License 112 | Uringy is licensed under the [MIT license](LICENSE). 113 | It's a permissive license, which basically means you can do whatever you want. 114 | -------------------------------------------------------------------------------- /src/ecosystem/http/server/mod.rs: -------------------------------------------------------------------------------- 1 | //! ... 2 | 3 | use std::cell::RefCell; 4 | use std::io::{BufWriter, Read, Write}; 5 | use std::rc::Rc; 6 | use std::time::SystemTime; 7 | 8 | use crate::circular_buffer; 9 | use crate::circular_buffer::circular_buffer; 10 | use crate::ecosystem::http::payload::{Request, Response}; 11 | use crate::ecosystem::http::server::route::Router; 12 | use crate::ecosystem::http::{Respond, Responder}; 13 | use crate::runtime::{is_cancelled, park, spawn, Waker}; 14 | 15 | pub mod fake_client; 16 | pub mod route; 17 | 18 | /// ... 19 | pub fn serve( 20 | router: Router, 21 | state: S, 22 | connections: impl Iterator, 23 | ) { 24 | // TODO: don't need Rc for router when using scoped spawn 25 | let router = Rc::new(router.with_state(state)); 26 | 27 | for (w, r) in connections { 28 | let router = router.clone(); 29 | // TODO: spawn_contained 30 | spawn(move || { 31 | handle_connection(&router, w, r).unwrap(); 32 | }); 33 | } 34 | } 35 | 36 | fn handle_connection( 37 | router: &Router, 38 | w: impl Write + 'static, 39 | r: impl Read + 'static, 40 | ) -> crate::IoResult<()> { 41 | // TODO: pool to reuse 42 | let (mut data, uninit) = circular_buffer(4096)?; 43 | let waiting_for_data = Rc::new(RefCell::new(None)); 44 | 45 | spawn({ 46 | let waiting_for_data = waiting_for_data.clone(); 47 | move || reader(uninit, r, waiting_for_data) 48 | }); 49 | 50 | while !is_cancelled() { 51 | park(|waker| { 52 | let mut data = waiting_for_data.borrow_mut(); 53 | *data = Some(waker); 54 | }); 55 | 56 | let mut headers = [httparse::EMPTY_HEADER; 64]; 57 | let mut request = httparse::Request::new(&mut headers); 58 | 59 | match request.parse(&data) { 60 | Ok(httparse::Status::Complete(wire_size)) => { 61 | let body_size: usize = request 62 | .headers 63 | .iter() 64 | .find(|h| h.name.to_ascii_lowercase() == "content-length") 65 | .and_then(|h| std::str::from_utf8(h.value).ok()) 66 | .and_then(|v| v.parse().ok()) 67 | .unwrap_or(0); 68 | 69 | if data.len() < wire_size + body_size { 70 | println!("server reading more!"); 71 | continue; 72 | } 73 | 74 | let r = Responder::new(RealResponder(Box::new(w))); 75 | let (path, query) = parse_partial_uri(request.path.unwrap()); 76 | let request = Request::new( 77 | request.method.unwrap().parse().unwrap(), 78 | path, 79 | query, 80 | request.headers.iter().map(|h| (h.name, h.value)).collect(), 81 | &data[wire_size..(wire_size + body_size)], 82 | ); 83 | router.handle(r, &request); 84 | 85 | data.consume(wire_size); 86 | println!("exiting"); 87 | break; // FIXME writer should be reusable 88 | } 89 | Ok(httparse::Status::Partial) => continue, 90 | Err(e) => { 91 | dbg!(e); 92 | break; 93 | } 94 | } 95 | } 96 | 97 | Ok(()) 98 | } 99 | 100 | /// Parses path and query out of partial URI (path, query, and fragment). 101 | /// Inspired by https://github.com/hyperium/http/blob/bda93204b3da1a776cf471ed39e8e374cec652e7/src/uri/path.rs#L21-L106. 102 | fn parse_partial_uri(uri: &str) -> (&str, &str) { 103 | for (i, c) in uri.char_indices() { 104 | match c as u8 { 105 | b'?' => return (&uri[..i], parse_query(&uri[i + 1..])), 106 | b'#' => return (&uri[..i], ""), 107 | // Code points that don't need to be character encoded 108 | 0x21 | 0x24..=0x3B | 0x3D | 0x40..=0x5F | 0x61..=0x7A | 0x7C | 0x7E => {} 109 | // JSON should be percent encoded, but still allowed 110 | b'"' | b'{' | b'}' => {} 111 | _ => panic!("invalid uri char"), // FIXME Err(InvalidUriChar) 112 | } 113 | } 114 | 115 | (uri, "") 116 | } 117 | 118 | fn parse_query(query: &str) -> &str { 119 | for (i, c) in query.char_indices() { 120 | match c as u8 { 121 | b'#' => return &query[..i], 122 | b'?' => {} 123 | // Should be percent-encoded, but most byes are actually allowed 124 | 0x21 | 0x24..=0x3B | 0x3D | 0x3F..=0x7E => {} 125 | _ => panic!("invalid uri char"), // FIXME Err(InvalidUriChar) 126 | } 127 | } 128 | 129 | query 130 | } 131 | 132 | fn reader( 133 | mut uninit: circular_buffer::Uninit, 134 | mut r: impl Read, 135 | waiting_for_data: Rc>>, 136 | ) { 137 | loop { 138 | let Ok(bytes_read) = r.read(&mut uninit) else { 139 | break; 140 | }; 141 | 142 | if bytes_read == 0 { 143 | break; 144 | } 145 | 146 | uninit.commit(bytes_read); 147 | 148 | if let Some(waker) = waiting_for_data.borrow_mut().take() { 149 | waker.schedule(); 150 | } 151 | } 152 | 153 | // TODO: scoped spawn 154 | // cancel_propagating(); 155 | } 156 | 157 | struct RealResponder(Box); 158 | 159 | impl Respond for RealResponder { 160 | fn respond(self: Box, response: Response) { 161 | let mut writer = BufWriter::new(self.0); 162 | serialize(&mut writer, response).unwrap(); 163 | } 164 | } 165 | 166 | fn serialize(mut writer: impl Write, response: Response) -> crate::IoResult<()> { 167 | writer.write_all(b"HTTP/1.1 ")?; 168 | let status: u16 = response.status.into(); 169 | writer.write_all(status.to_string().as_bytes())?; 170 | writer.write_all(b" ")?; 171 | writer.write_all(b"OK")?; 172 | // writer.write_all(response.status.canonical_reason().unwrap().as_bytes())?; 173 | writer.write_all(b"\r\n")?; 174 | 175 | writer.write_all("content-length".as_bytes())?; 176 | writer.write_all(b": ")?; 177 | writer.write_all(response.body.len().to_string().as_bytes())?; 178 | writer.write_all(b"\r\n")?; 179 | 180 | if let Some(content_type) = response.content_type { 181 | writer.write_all("content-type".as_bytes())?; 182 | writer.write_all(b": ")?; 183 | writer.write_all(content_type.as_bytes())?; 184 | writer.write_all(b"\r\n")?; 185 | } 186 | 187 | writer.write_all("date".as_bytes())?; 188 | writer.write_all(b": ")?; 189 | writer.write_all(httpdate::fmt_http_date(SystemTime::now()).as_bytes())?; 190 | writer.write_all(b"\r\n")?; 191 | 192 | writer.write_all(b"connection: close\r\n")?; 193 | 194 | for (name, value) in response.headers { 195 | // TEST (put this in responder?) 196 | assert!(!name.eq_ignore_ascii_case("content-length")); 197 | assert!(!name.eq_ignore_ascii_case("content-type")); 198 | assert!(!name.eq_ignore_ascii_case("date")); 199 | 200 | writer.write_all(name.as_bytes())?; 201 | writer.write_all(b": ")?; 202 | writer.write_all(value)?; 203 | writer.write_all(b"\r\n")?; 204 | } 205 | writer.write_all(b"\r\n")?; 206 | writer.write_all(response.body)?; 207 | 208 | Ok(()) 209 | } 210 | 211 | #[cfg(test)] 212 | mod tests { 213 | use crate::ecosystem::http::payload::Method; 214 | use std::io::{Read, Write}; 215 | use std::net::Ipv4Addr; 216 | 217 | use crate::ecosystem::http::server::route::Router; 218 | use crate::ecosystem::http::Responder; 219 | use crate::net::tcp; 220 | use crate::runtime::{spawn, start}; 221 | 222 | use super::*; 223 | 224 | #[test] 225 | fn end_to_end() { 226 | start(|| { 227 | let listener = tcp::Listener::bind((Ipv4Addr::UNSPECIFIED, 0)).unwrap(); 228 | let server_addr = listener.local_addr().unwrap(); 229 | 230 | let server = spawn(move || { 231 | let routes = Router::new().route(Method::Get, "/", index); 232 | serve(routes, (), listener.into_incoming()); 233 | }); 234 | 235 | let (mut w, mut r) = tcp::connect((Ipv4Addr::LOCALHOST, server_addr.port())).unwrap(); 236 | 237 | // TODO: http client 238 | let request_wire = b"GET / HTTP/1.1\r\ncontent-length: 2\r\n\r\nhi"; 239 | w.write_all(request_wire).unwrap(); 240 | 241 | let mut buffer = vec![0; 1024]; 242 | let bytes_read = r.read(&mut buffer).unwrap(); 243 | let response = String::from_utf8_lossy(&buffer[..bytes_read]); 244 | println!("read:\n{}", response); 245 | assert!(response.contains("200")); 246 | // assert_eq!(&buffer[..bytes_read], b"hello"); 247 | 248 | server.cancel(); 249 | }) 250 | .unwrap(); 251 | } 252 | 253 | fn index(r: Responder) { 254 | r.send("hello"); // TODO: include content-length 255 | } 256 | 257 | #[test] 258 | fn takes_query_params() { 259 | start(|| { 260 | let listener = tcp::Listener::bind((Ipv4Addr::UNSPECIFIED, 0)).unwrap(); 261 | let server_addr = listener.local_addr().unwrap(); 262 | 263 | let server = spawn(move || { 264 | let routes = Router::new().route(Method::Get, "/", index); 265 | serve(routes, (), listener.into_incoming()); 266 | }); 267 | 268 | let (mut w, mut r) = tcp::connect((Ipv4Addr::LOCALHOST, server_addr.port())).unwrap(); 269 | 270 | // TODO: http client 271 | let request_wire = b"GET /?id=123 HTTP/1.1\r\ncontent-length: 2\r\n\r\nhi"; 272 | w.write_all(request_wire).unwrap(); 273 | 274 | let mut buffer = vec![0; 1024]; 275 | let bytes_read = r.read(&mut buffer).unwrap(); 276 | let response = String::from_utf8_lossy(&buffer[..bytes_read]); 277 | println!("read:\n{}", response); 278 | assert!(response.contains("200")); 279 | 280 | server.cancel(); 281 | }) 282 | .unwrap(); 283 | } 284 | 285 | mod partial_uri { 286 | use super::*; 287 | 288 | #[test] 289 | fn root_path() { 290 | let (path, query) = parse_partial_uri("/"); 291 | 292 | assert_eq!(path, "/"); 293 | assert!(query.is_empty()); 294 | } 295 | 296 | #[test] 297 | fn path() { 298 | let (path, query) = parse_partial_uri("/foo/bar"); 299 | 300 | assert_eq!(path, "/foo/bar"); 301 | assert!(query.is_empty()); 302 | } 303 | 304 | #[test] 305 | fn json_path() { 306 | let (path, query) = parse_partial_uri(r#"/{"foo":"bar"}"#); 307 | 308 | assert_eq!(path, r#"/{"foo":"bar"}"#); 309 | assert!(query.is_empty()); 310 | } 311 | 312 | #[test] 313 | fn root_path_with_query() { 314 | let (path, query) = parse_partial_uri("/?id=123&f"); 315 | 316 | assert_eq!(path, "/"); 317 | assert_eq!(query, "id=123&f"); 318 | } 319 | 320 | #[test] 321 | fn path_with_query() { 322 | let (path, query) = parse_partial_uri("/foo/bar?id=123&f"); 323 | 324 | assert_eq!(path, "/foo/bar"); 325 | assert_eq!(query, "id=123&f"); 326 | } 327 | 328 | #[test] 329 | fn path_ignores_fragment() { 330 | let (path, query) = parse_partial_uri("/foo/bar#abc"); 331 | 332 | assert_eq!(path, "/foo/bar"); 333 | assert!(query.is_empty()); 334 | } 335 | 336 | #[test] 337 | fn path_with_query_ignores_fragment() { 338 | let (path, query) = parse_partial_uri("/foo/bar?id=123&f#abc"); 339 | 340 | assert_eq!(path, "/foo/bar"); 341 | assert_eq!(query, "id=123&f"); 342 | } 343 | 344 | #[test] 345 | fn subsequent_question_marks_are_just_characters() { 346 | let (path, query) = parse_partial_uri("/?id=123?f"); 347 | 348 | assert_eq!(path, "/"); 349 | assert_eq!(query, "id=123?f"); // TODO: make sure serde_urlencoded can parse this 350 | } 351 | 352 | // TODO 353 | // #[test] 354 | // #[should_panic] 355 | // fn fails_invalid_path() { 356 | // parse_partial_uri("/"); 357 | // } 358 | // 359 | // #[test] 360 | // #[should_panic] 361 | // fn fails_invalid_query() { 362 | // parse_partial_uri("/?"); 363 | // } 364 | 365 | #[test] 366 | fn ignores_valid_percent_encodings() { 367 | assert_eq!("/a%20b", parse_partial_uri("/a%20b?r=1").0); 368 | assert_eq!("qr=%31", parse_partial_uri("/a/b?qr=%31").1); 369 | } 370 | 371 | #[test] 372 | fn ignores_invalid_percent_encodings() { 373 | assert_eq!("/a%%b", parse_partial_uri("/a%%b?r=1").0); 374 | assert_eq!("/aaa%", parse_partial_uri("/aaa%").0); 375 | assert_eq!("/aaa%", parse_partial_uri("/aaa%?r=1").0); 376 | assert_eq!("/aa%2", parse_partial_uri("/aa%2").0); 377 | assert_eq!("/aa%2", parse_partial_uri("/aa%2?r=1").0); 378 | assert_eq!("qr=%3", parse_partial_uri("/a/b?qr=%3").1); 379 | } 380 | } 381 | } 382 | -------------------------------------------------------------------------------- /src/ecosystem/http/server/route.rs: -------------------------------------------------------------------------------- 1 | //! ... 2 | //! 3 | //! Optimized for reads since routes are typically constructed once at startup. 4 | 5 | use crate::ecosystem::http::payload::{Method, Request, StatusCode}; 6 | use crate::ecosystem::http::{Handler, IntoHandler, Responder}; 7 | 8 | /// Handle for composing endpoint handlers. 9 | // TODO: #[must_use] 10 | pub struct Router { 11 | matcher: matchit::Router>, 12 | /// Work around for `matchit::Router` not exposing an iterator 13 | matcher_paths: Vec, 14 | fallback: Handler, 15 | state: Option, 16 | } 17 | 18 | // TODO: S: Clone + 'static 19 | impl Router { 20 | /// ... 21 | #[inline] 22 | pub fn new() -> Self { 23 | Router { 24 | matcher: matchit::Router::new(), 25 | matcher_paths: vec![], 26 | fallback: (|r: Responder| r.status(StatusCode::NotFound).send(())).into_handler(), 27 | state: None, 28 | } 29 | } 30 | 31 | pub(crate) fn with_state(mut self, state: S) -> Self { 32 | assert!(self.state.is_none()); 33 | self.state = Some(state); 34 | self 35 | } 36 | 37 | /// Adds a route to the router. 38 | #[inline] 39 | pub fn route( 40 | mut self, 41 | method: Method, 42 | path: &str, 43 | handler: impl IntoHandler, 44 | ) -> Self { 45 | match self.matcher.at_mut(path) { 46 | Ok(found) => found.value.set(method, handler.into_handler()), 47 | Err(_) => { 48 | let mut method_router = MethodRouter::new(); 49 | method_router.set(method, handler.into_handler()); 50 | self.matcher.insert(path, method_router).unwrap(); 51 | self.matcher_paths.push(path.to_string()); 52 | } 53 | } 54 | 55 | self 56 | } 57 | 58 | /// Override the the default fallback service that's called if no routes match the request. 59 | #[inline] 60 | pub fn fallback(mut self, handler: impl IntoHandler + 'static) -> Self { 61 | // TODO: decide what to do when merging/nesting two routers. 62 | self.fallback = handler.into_handler(); 63 | self 64 | } 65 | 66 | // /// ... 67 | // #[inline] 68 | // pub fn merge(self, _other: Self) -> Self { 69 | // for path in _other.matcher_paths { 70 | // 71 | // } 72 | // 73 | // unimplemented!(); 74 | // } 75 | 76 | // /// ... 77 | // #[inline] 78 | // pub fn nest>(self, other: Router) -> Self { 79 | // // for path in other.matcher_paths { 80 | // // other.matcher.at() 81 | // // } 82 | // 83 | // self 84 | // } 85 | 86 | /// ... 87 | pub(crate) fn handle(&self, r: Responder, request: &Request) { 88 | // TODO: take ownership of request 89 | let handler = self 90 | .matcher 91 | .at(request.path()) 92 | .ok() 93 | .and_then(|found| found.value.handle(request.method())) // TODO: also return params 94 | .unwrap_or(&self.fallback); 95 | 96 | // TODO: add params to request 97 | let state = self.state.as_ref().unwrap(); 98 | 99 | handler(r, request, state); // TODO: pass reference to request 100 | } 101 | } 102 | 103 | /// ... 104 | pub struct MethodRouter { 105 | // HTTP methods 106 | get: Option>, 107 | post: Option>, 108 | head: Option>, 109 | put: Option>, 110 | delete: Option>, 111 | connect: Option>, 112 | options: Option>, 113 | trace: Option>, 114 | patch: Option>, 115 | // Miscellaneous 116 | head_derived_from_get: Option>, 117 | allowed_methods: String, 118 | other_method_allowed: Option>, 119 | } 120 | 121 | impl MethodRouter { 122 | fn new() -> Self { 123 | MethodRouter { 124 | get: None, 125 | post: None, 126 | head: None, 127 | put: None, 128 | delete: None, 129 | connect: None, 130 | options: None, 131 | trace: None, 132 | patch: None, 133 | head_derived_from_get: None, 134 | allowed_methods: String::new(), 135 | other_method_allowed: None, 136 | } 137 | } 138 | 139 | fn set_get(&mut self, handler: Handler) { 140 | assert!(self.get.is_none()); 141 | self.get = Some(handler); 142 | 143 | if self.head.is_none() { 144 | // TODO: clone handler (need Rc or + Clone) strip response body from middleware 145 | // self.head_derived_from_get = Some((|r: Responder| r.send(())).into_handler()); 146 | } 147 | 148 | self.append_allowed_method("GET, HEAD"); 149 | } 150 | 151 | fn set_post(&mut self, handler: Handler) { 152 | assert!(self.post.is_none()); 153 | self.post = Some(handler); 154 | self.append_allowed_method("POST"); 155 | } 156 | 157 | fn set_head(&mut self, handler: Handler) { 158 | assert!(self.head.is_none()); 159 | self.head = Some(handler); 160 | 161 | self.head_derived_from_get = None; 162 | 163 | self.append_allowed_method("HEAD"); 164 | } 165 | 166 | fn set_put(&mut self, handler: Handler) { 167 | assert!(self.put.is_none()); 168 | self.put = Some(handler); 169 | self.append_allowed_method("PUT"); 170 | } 171 | 172 | fn set_delete(&mut self, handler: Handler) { 173 | assert!(self.delete.is_none()); 174 | self.delete = Some(handler); 175 | self.append_allowed_method("DELETE"); 176 | } 177 | 178 | fn set_connect(&mut self, handler: Handler) { 179 | assert!(self.connect.is_none()); 180 | self.connect = Some(handler); 181 | self.append_allowed_method("CONNECT"); 182 | } 183 | 184 | fn set_options(&mut self, handler: Handler) { 185 | assert!(self.options.is_none()); 186 | self.options = Some(handler); 187 | self.append_allowed_method("OPTIONS"); 188 | } 189 | 190 | fn set_trace(&mut self, handler: Handler) { 191 | assert!(self.trace.is_none()); 192 | self.trace = Some(handler); 193 | self.append_allowed_method("TRACE"); 194 | } 195 | 196 | fn set_patch(&mut self, handler: Handler) { 197 | assert!(self.patch.is_none()); 198 | self.patch = Some(handler); 199 | self.append_allowed_method("PATCH"); 200 | } 201 | 202 | fn append_allowed_method(&mut self, method: &str) { 203 | if !self.allowed_methods.is_empty() { 204 | self.allowed_methods.push_str(", "); 205 | } 206 | self.allowed_methods.push_str(method); 207 | self.allowed_methods.shrink_to_fit(); 208 | 209 | let allowed_methods = self.allowed_methods.clone(); 210 | self.other_method_allowed = Some( 211 | (move |r: Responder| { 212 | r.status(StatusCode::MethodNotAllowed) 213 | .header("allow", allowed_methods.as_bytes()) 214 | .send(()) 215 | }) 216 | .into_handler(), 217 | ); 218 | } 219 | 220 | fn set(&mut self, method: Method, handler: Handler) { 221 | match method { 222 | Method::Get => self.set_get(handler), 223 | Method::Post => self.set_post(handler), 224 | Method::Head => self.set_head(handler), 225 | Method::Put => self.set_put(handler), 226 | Method::Delete => self.set_delete(handler), 227 | Method::Connect => self.set_connect(handler), 228 | Method::Options => self.set_options(handler), 229 | Method::Trace => self.set_trace(handler), 230 | Method::Patch => self.set_patch(handler), 231 | } 232 | } 233 | 234 | // fn merge(&mut self, other: Self) { 235 | // if let Some(handler) = other.get { 236 | // self.set_get(handler); 237 | // } 238 | // if let Some(handler) = other.post { 239 | // self.set_post(handler); 240 | // } 241 | // if let Some(handler) = other.head { 242 | // self.set_head(handler); 243 | // } 244 | // if let Some(handler) = other.put { 245 | // self.set_put(handler); 246 | // } 247 | // if let Some(handler) = other.delete { 248 | // self.set_delete(handler); 249 | // } 250 | // if let Some(handler) = other.connect { 251 | // self.set_connect(handler); 252 | // } 253 | // if let Some(handler) = other.options { 254 | // self.set_options(handler); 255 | // } 256 | // if let Some(handler) = other.trace { 257 | // self.set_trace(handler); 258 | // } 259 | // if let Some(handler) = other.patch { 260 | // self.set_patch(handler); 261 | // } 262 | // } 263 | 264 | fn handle(&self, method: Method) -> Option<&Handler> { 265 | match method { 266 | Method::Get => self.get.as_ref(), 267 | Method::Post => self.post.as_ref(), 268 | Method::Head => self.head.as_ref().or(self.head_derived_from_get.as_ref()), 269 | Method::Put => self.put.as_ref(), 270 | Method::Delete => self.delete.as_ref(), 271 | Method::Connect => self.connect.as_ref(), 272 | Method::Options => self.options.as_ref(), 273 | Method::Trace => self.trace.as_ref(), 274 | Method::Patch => self.patch.as_ref(), 275 | } 276 | .or(self.other_method_allowed.as_ref()) 277 | } 278 | } 279 | 280 | #[cfg(test)] 281 | mod tests { 282 | use crate::ecosystem::http::payload::Method; 283 | use crate::ecosystem::http::payload::StatusCode; 284 | use crate::ecosystem::http::server::fake_client::FakeClient; 285 | use crate::ecosystem::http::Responder; 286 | use crate::runtime::start; 287 | 288 | use super::*; 289 | 290 | #[test] 291 | fn handles_static_route() { 292 | start(|| { 293 | let routes = Router::new().route(Method::Get, "/", |r: Responder| r.send(())); 294 | let mut client = FakeClient::from(routes); 295 | 296 | let response = client.get("/").send(()); 297 | 298 | assert_eq!(response.status, StatusCode::Ok); 299 | }) 300 | .unwrap(); 301 | } 302 | 303 | #[test] 304 | fn prioritizes_static_over_dynamic_route() { 305 | start(|| { 306 | let routes = Router::new() 307 | .route(Method::Get, "/*dyn", |r: Responder| { 308 | r.status(StatusCode::Forbidden).send(()) 309 | }) 310 | .route(Method::Get, "/", |r: Responder| { 311 | r.status(StatusCode::Accepted).send(()) 312 | }); 313 | let mut client = FakeClient::from(routes); 314 | 315 | let response = client.get("/").send(()); 316 | 317 | assert_eq!(response.status, StatusCode::Accepted); 318 | }) 319 | .unwrap(); 320 | } 321 | 322 | #[test] 323 | fn merges_two_routes_with_same_path_but_different_methods() { 324 | start(|| { 325 | let routes = Router::new() 326 | .route(Method::Post, "/", |r: Responder| r.send(())) 327 | .route(Method::Put, "/", |r: Responder| r.send(())); 328 | let mut client = FakeClient::from(routes); 329 | 330 | assert_eq!(client.post("/").send(()).status, StatusCode::Ok); 331 | assert_eq!(client.put("/").send(()).status, StatusCode::Ok); 332 | }) 333 | .unwrap(); 334 | } 335 | 336 | #[test] 337 | #[should_panic] 338 | fn fails_to_create_duplicate_static_route() { 339 | start(|| { 340 | Router::<()>::new() 341 | .route(Method::Get, "/", |r: Responder| r.send(())) 342 | .route(Method::Get, "/", |r: Responder| r.send(())); 343 | }) 344 | .unwrap(); 345 | } 346 | 347 | #[test] 348 | #[should_panic] 349 | fn fails_to_create_duplicate_dynamic_route() { 350 | start(|| { 351 | Router::<()>::new() 352 | .route(Method::Get, "/*dyn1", |r: Responder| r.send(())) 353 | .route(Method::Get, "/*dyn2", |r: Responder| r.send(())); 354 | }) 355 | .unwrap(); 356 | } 357 | 358 | #[test] 359 | fn returns_404_when_unknown_route() { 360 | start(|| { 361 | let routes = Router::new(); 362 | let mut client = FakeClient::from(routes); 363 | 364 | let response = client.get("/").send(()); 365 | 366 | assert_eq!(response.status, StatusCode::NotFound); 367 | }) 368 | .unwrap(); 369 | } 370 | 371 | #[test] 372 | fn handles_unknown_route_with_fallback_handler() { 373 | start(|| { 374 | let routes = 375 | Router::new().fallback(|r: Responder| r.status(StatusCode::Forbidden).send(())); 376 | let mut client = FakeClient::from(routes); 377 | 378 | let response = client.get("/").send(()); 379 | 380 | assert_eq!(response.status, StatusCode::Forbidden); 381 | }) 382 | .unwrap(); 383 | } 384 | 385 | #[test] 386 | fn returns_405_when_wrong_method() { 387 | start(|| { 388 | let routes = Router::new() 389 | .route(Method::Get, "/", |r: Responder| r.send(())) 390 | .route(Method::Options, "/", |r: Responder| r.send(())); 391 | let mut client = FakeClient::from(routes); 392 | 393 | let response = client.post("/").send(()); 394 | 395 | assert_eq!(response.status, StatusCode::MethodNotAllowed); 396 | let (_, allow) = response 397 | .headers 398 | .iter() 399 | .find(|(k, _)| k.eq_ignore_ascii_case("allow")) 400 | .unwrap(); 401 | dbg!(String::from_utf8_lossy(allow)); 402 | assert_eq!(String::from_utf8_lossy(allow), "GET, HEAD, OPTIONS"); 403 | }) 404 | .unwrap(); 405 | } 406 | 407 | #[test] 408 | #[ignore] 409 | fn head_defers_to_get() { 410 | start(|| { 411 | let routes = Router::new().route(Method::Get, "/", |r: Responder| r.send("hello")); 412 | let mut client = FakeClient::from(routes); 413 | 414 | let response = client.head("/").send(()); 415 | 416 | assert_eq!(response.status, StatusCode::Ok); 417 | assert!(response.body.is_empty()); 418 | }) 419 | .unwrap(); 420 | } 421 | 422 | #[test] 423 | fn custom_head_overrides_get() { 424 | start(|| { 425 | let routes = Router::new() 426 | .route(Method::Get, "/", |r: Responder| { 427 | r.status(StatusCode::Forbidden).send(()) 428 | }) 429 | .route(Method::Head, "/", |r: Responder| { 430 | r.status(StatusCode::Accepted).send(()) 431 | }); 432 | let mut client = FakeClient::from(routes); 433 | 434 | let response = client.head("/").send(()); 435 | 436 | assert_eq!(response.status, StatusCode::Accepted); 437 | }) 438 | .unwrap(); 439 | } 440 | 441 | #[test] 442 | fn handles_state() { 443 | start(|| { 444 | let routes = Router::new().route(Method::Get, "/", |r: Responder, state: &String| { 445 | r.send(state.as_str()) 446 | }); 447 | let mut client = FakeClient::new(routes, String::from("hello")); 448 | 449 | let response = client.get("/").send(()); 450 | 451 | assert_eq!(response.status, StatusCode::Ok); 452 | assert_eq!(response.body, b"hello"); 453 | }) 454 | .unwrap(); 455 | } 456 | } 457 | -------------------------------------------------------------------------------- /src/fs.rs: -------------------------------------------------------------------------------- 1 | //! Filesystem operations inspired by the standard library. 2 | 3 | use std::io::{Read, Write}; 4 | use std::os::fd::{AsRawFd, FromRawFd, RawFd}; 5 | use std::os::unix::ffi::OsStrExt; 6 | use std::path::Path; 7 | use std::{cmp, ffi, io, mem}; 8 | 9 | use io_uring::types::FsyncFlags; 10 | 11 | use crate::runtime; 12 | 13 | /// Handle to an open file. 14 | pub struct File(RawFd); 15 | 16 | impl File { 17 | /// Opens a file in read-only mode. 18 | pub fn open>(path: P) -> crate::IoResult { 19 | OpenOptions::new().read(true).open(path.as_ref()) 20 | } 21 | 22 | /// Opens a file in write-only mode. 23 | pub fn create(path: impl AsRef) -> crate::IoResult { 24 | OpenOptions::new() 25 | .write(true) 26 | .create(true) 27 | .truncate(true) 28 | .open(path.as_ref()) 29 | } 30 | 31 | /// Returns an [OpenOptions] builder. 32 | /// Equivalent to [OpenOptions::new()]. 33 | #[must_use] 34 | pub fn options() -> OpenOptions { 35 | OpenOptions::new() 36 | } 37 | 38 | /// Syncs all OS-internal metadata to disk. 39 | /// Catches errors that would otherwise be ignored when dropping the file. 40 | pub fn sync_all(&self) -> crate::IoResult<()> { 41 | let fd = io_uring::types::Fd(self.0); 42 | let sqe = io_uring::opcode::Fsync::new(fd).build(); 43 | let result = runtime::syscall(sqe)?; 44 | assert_eq!(result, 0); 45 | 46 | Ok(()) 47 | } 48 | 49 | /// Syncs content, but maybe not file metadata to disk. 50 | /// Reduces disk operations compared to [sync_all]. 51 | pub fn sync_data(&self) -> crate::IoResult<()> { 52 | let fd = io_uring::types::Fd(self.0); 53 | let sqe = io_uring::opcode::Fsync::new(fd) 54 | .flags(FsyncFlags::DATASYNC) 55 | .build(); 56 | let result = runtime::syscall(sqe)?; 57 | assert_eq!(result, 0); 58 | 59 | Ok(()) 60 | } 61 | 62 | /// Truncates or extends the underlying file. 63 | pub fn set_len(&self, size: u64) -> crate::IoResult<()> { 64 | let file = unsafe { std::fs::File::from_raw_fd(self.0) }; 65 | file.set_len(size)?; 66 | mem::forget(file); 67 | 68 | Ok(()) 69 | } 70 | 71 | /// Queries metadata about the underlying file. 72 | pub fn metadata(&self) -> crate::IoResult { 73 | let file = unsafe { std::fs::File::from_raw_fd(self.0) }; 74 | let metadata = file.metadata()?; 75 | mem::forget(file); 76 | 77 | // TODO io_uring operation 78 | 79 | Ok(metadata) 80 | } 81 | 82 | // /// ... 83 | // pub fn try_clone(&self) -> crate::IoResult { 84 | // 85 | // } 86 | 87 | /// Changes the permissions on the underlying file. 88 | pub fn set_permissions(&self, permissions: std::fs::Permissions) -> crate::IoResult<()> { 89 | let file = unsafe { std::fs::File::from_raw_fd(self.0) }; 90 | file.set_permissions(permissions)?; 91 | mem::forget(file); 92 | 93 | Ok(()) 94 | } 95 | } 96 | 97 | impl Drop for File { 98 | fn drop(&mut self) { 99 | let fd = io_uring::types::Fd(self.0); 100 | let sqe = io_uring::opcode::Close::new(fd).build(); 101 | let _ = runtime::syscall(sqe); 102 | } 103 | } 104 | 105 | // TODO: doesn't work if using fixed fd 106 | impl FromRawFd for File { 107 | unsafe fn from_raw_fd(fd: RawFd) -> Self { 108 | File(fd) 109 | } 110 | } 111 | 112 | impl AsRawFd for File { 113 | fn as_raw_fd(&self) -> RawFd { 114 | self.0 115 | } 116 | } 117 | 118 | // impl IntoRawFd for File { 119 | // fn into_raw_fd(self) -> RawFd { 120 | // FIXME: decide whether to close or not 121 | // self.0 122 | // } 123 | // } 124 | 125 | // The maximum read limit on most POSIX-like systems is `SSIZE_MAX`, 126 | // with the man page quoting that if the count of bytes to read is 127 | // greater than `SSIZE_MAX` the result is "unspecified". 128 | // 129 | // On macOS, however, apparently the 64-bit libc is either buggy or 130 | // intentionally showing odd behavior by rejecting any read with a size 131 | // larger than or equal to INT_MAX. To handle both of these the read 132 | // size is capped on both platforms. 133 | #[cfg(target_os = "macos")] 134 | const READ_LIMIT: u32 = libc::c_int::MAX as u32 - 1; 135 | #[cfg(not(target_os = "macos"))] 136 | const READ_LIMIT: u32 = libc::ssize_t::MAX as u32; 137 | 138 | impl io::Write for File { 139 | fn write(&mut self, buf: &[u8]) -> io::Result { 140 | let fd = io_uring::types::Fd(self.0); 141 | let sqe = 142 | io_uring::opcode::Write::new(fd, buf.as_ptr(), cmp::min(buf.len() as u32, READ_LIMIT)) 143 | .offset(0_u64.wrapping_sub(1)) // use file offset for files that support seeking 144 | .build(); 145 | let bytes_wrote = runtime::syscall(sqe)?; 146 | Ok(bytes_wrote as usize) 147 | } 148 | 149 | fn flush(&mut self) -> io::Result<()> { 150 | Ok(()) 151 | } 152 | } 153 | 154 | impl io::Read for File { 155 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 156 | let fd = io_uring::types::Fd(self.0); 157 | let sqe = io_uring::opcode::Read::new( 158 | fd, 159 | buf.as_mut_ptr(), 160 | cmp::min(buf.len() as u32, READ_LIMIT), 161 | ) 162 | .offset(0_u64.wrapping_sub(1)) // use file offset for files that support seeking 163 | .build(); 164 | let bytes_read = runtime::syscall(sqe)?; 165 | Ok(bytes_read as usize) 166 | } 167 | } 168 | 169 | /// Options and flags for configuring how a file is opened. 170 | #[derive(Clone, Debug)] 171 | pub struct OpenOptions { 172 | // generic 173 | read: bool, 174 | write: bool, 175 | append: bool, 176 | truncate: bool, 177 | create: bool, 178 | create_new: bool, 179 | // system-specific 180 | custom_flags: i32, 181 | mode: libc::mode_t, 182 | } 183 | 184 | impl OpenOptions { 185 | /// Creates a blank new set of options ready for configuration. 186 | /// 187 | /// All options are initially set to false. 188 | pub fn new() -> Self { 189 | OpenOptions { 190 | read: false, 191 | write: false, 192 | append: false, 193 | truncate: false, 194 | create: false, 195 | create_new: false, 196 | custom_flags: 0, 197 | mode: 0o666, // read and write but not execute 198 | } 199 | } 200 | 201 | /// Sets the option for read access. 202 | pub fn read(&mut self, read: bool) -> &mut Self { 203 | self.read = read; 204 | self 205 | } 206 | 207 | /// Sets the option for write access. 208 | /// 209 | /// If the file already exists, any write calls on it will overwrite its contents, without truncating it. 210 | pub fn write(&mut self, write: bool) -> &mut Self { 211 | self.write = write; 212 | self 213 | } 214 | 215 | /// Sets the option for the append mode. 216 | /// 217 | /// Note that setting [.write(true).append(true)] has the same effect as setting only [.append(true)]. 218 | /// 219 | /// ## Note 220 | /// This function doesn't create the file if it doesn't exist. 221 | /// Use the [`OpenOptions::create`] method to do so. 222 | pub fn append(&mut self, append: bool) -> &mut Self { 223 | self.append = append; 224 | self 225 | } 226 | 227 | /// Sets the option for truncating a previous file. 228 | /// 229 | /// The file must be opened with write access for truncate to work. 230 | pub fn truncate(&mut self, truncate: bool) -> &mut Self { 231 | self.truncate = truncate; 232 | self 233 | } 234 | 235 | /// Sets the option to create a new file, or open it if it already exists. 236 | /// 237 | /// In order for the file to be created, [OpenOptions::write] or [OpenOptions::append] access must be used. 238 | /// 239 | /// See also [uringy::fs::write()] for a simple function to create a file with a given data. 240 | pub fn create(&mut self, create: bool) -> &mut Self { 241 | self.create = create; 242 | self 243 | } 244 | 245 | /// Sets the option to create a new file, failing if it already exists. 246 | /// 247 | /// This option is useful because it is atomic. 248 | /// 249 | /// If .create_new(true) is set, .create() and .truncate() are ignored. 250 | /// 251 | /// The file must be opened with write or append access in order to create a new file. 252 | pub fn create_new(&mut self, create_new: bool) -> &mut Self { 253 | self.create_new = create_new; 254 | self 255 | } 256 | 257 | fn get_access_mode(&self) -> io::Result { 258 | match (self.read, self.write, self.append) { 259 | (true, false, false) => Ok(libc::O_RDONLY), 260 | (false, true, false) => Ok(libc::O_WRONLY), 261 | (true, true, false) => Ok(libc::O_RDWR), 262 | (false, _, true) => Ok(libc::O_WRONLY | libc::O_APPEND), 263 | (true, _, true) => Ok(libc::O_RDWR | libc::O_APPEND), 264 | (false, false, false) => Err(io::Error::from_raw_os_error(libc::EINVAL)), 265 | } 266 | } 267 | 268 | fn get_creation_mode(&self) -> io::Result { 269 | match (self.write, self.append) { 270 | (true, false) => {} 271 | (false, false) => { 272 | if self.truncate || self.create || self.create_new { 273 | return Err(io::Error::from_raw_os_error(libc::EINVAL)); 274 | } 275 | } 276 | (_, true) => { 277 | if self.truncate && !self.create_new { 278 | return Err(io::Error::from_raw_os_error(libc::EINVAL)); 279 | } 280 | } 281 | } 282 | 283 | Ok(match (self.create, self.truncate, self.create_new) { 284 | (false, false, false) => 0, 285 | (true, false, false) => libc::O_CREAT, 286 | (false, true, false) => libc::O_TRUNC, 287 | (true, true, false) => libc::O_CREAT | libc::O_TRUNC, 288 | (_, _, true) => libc::O_CREAT | libc::O_EXCL, 289 | }) 290 | } 291 | 292 | /// Opens a file at [path] with the options specified by [self]. 293 | pub fn open(&self, path: impl AsRef) -> crate::IoResult { 294 | let fd = io_uring::types::Fd(libc::AT_FDCWD); // pathname is relative to working directory 295 | let path = ffi::CString::new(path.as_ref().as_os_str().as_bytes()).unwrap(); 296 | let flags = libc::O_CLOEXEC 297 | | self.get_access_mode()? 298 | | self.get_creation_mode()? 299 | | (self.custom_flags as libc::c_int & !libc::O_ACCMODE); 300 | let sqe = io_uring::opcode::OpenAt::new(fd, path.as_ptr()) 301 | .mode(self.mode) 302 | .flags(flags) 303 | .build(); 304 | runtime::syscall(sqe).map(|fd| File(fd as i32)) 305 | } 306 | } 307 | 308 | /// Copies the contents of one file to another. 309 | /// This function will also copy the permission bits of the original file to the destination file. 310 | /// 311 | /// This function will **overwrite** the contents of `to`. 312 | /// 313 | /// Note that if `from` and `to` both point to the same file, then the file will likely get truncated by this operation. 314 | /// 315 | /// On success, the total number of bytes copied is returned and it is equal to the length of the `to` file as reported by `metadata`. 316 | /// 317 | /// If you want to copy the contents of one file to another and you’re working with [`File`]s, see the [`io::copy()`] function. 318 | pub fn copy(from: impl AsRef, to: impl AsRef) -> crate::IoResult { 319 | std::fs::copy(from.as_ref(), to.as_ref()).map_err(crate::Error::from_io_error) 320 | } 321 | 322 | /// Queries metadata about the underlying file. 323 | pub fn metadata(path: impl AsRef) -> crate::IoResult { 324 | let file = File::open(path.as_ref())?; 325 | file.metadata() 326 | } 327 | 328 | /// Read the entire contents of a file into a bytes vector. 329 | pub fn read(path: impl AsRef) -> crate::IoResult> { 330 | let mut file = File::open(path.as_ref())?; 331 | let mut vector = vec![]; 332 | file.read_to_end(&mut vector) 333 | .map_err(crate::Error::from_io_error)?; 334 | 335 | Ok(vector) 336 | } 337 | 338 | /// Read the entire contents of a file into a string. 339 | pub fn read_to_string(path: impl AsRef) -> crate::IoResult { 340 | let file = File::open(path.as_ref())?; 341 | let string = io::read_to_string(file).map_err(crate::Error::from_io_error)?; 342 | 343 | Ok(string) 344 | } 345 | 346 | /// Removes a file from the filesystem. 347 | pub fn remove_file(path: impl AsRef) -> crate::IoResult<()> { 348 | let fd = io_uring::types::Fd(libc::AT_FDCWD); // pathname is relative to working directory 349 | let path = ffi::CString::new(path.as_ref().as_os_str().as_bytes()).unwrap(); 350 | let sqe = io_uring::opcode::UnlinkAt::new(fd, path.as_ptr()).build(); 351 | let result = runtime::syscall(sqe)?; 352 | assert_eq!(result, 0); 353 | 354 | Ok(()) 355 | } 356 | 357 | /// Write a slice as the entire contents of a file. 358 | pub fn write(path: impl AsRef, contents: impl AsRef<[u8]>) -> crate::IoResult<()> { 359 | File::create(path.as_ref())? 360 | .write_all(contents.as_ref()) 361 | .map_err(crate::Error::from_io_error)?; 362 | 363 | Ok(()) 364 | } 365 | 366 | // TODO: O_LARGEFILE open64, otherwise EOVERFLOW 367 | // TODO: https://docs.rs/io-uring/latest/io_uring/opcode/struct.MkDirAt.html 368 | 369 | #[cfg(test)] 370 | mod tests { 371 | use crate::runtime::start; 372 | 373 | use super::*; 374 | 375 | #[test] 376 | fn creates_and_deletes_file() { 377 | start(|| { 378 | let path = format!("/tmp/{}", uuid::Uuid::new_v4()); 379 | 380 | File::create(&path).unwrap(); 381 | assert!(Path::new(&path).exists()); 382 | 383 | remove_file(&path).unwrap(); 384 | assert!(!Path::new(&path).exists()); 385 | }) 386 | .unwrap(); 387 | } 388 | 389 | #[test] 390 | fn copies_file() { 391 | start(|| { 392 | let path = format!("/tmp/{}", uuid::Uuid::new_v4()); 393 | 394 | copy("/etc/hosts", &path).unwrap(); 395 | 396 | assert_eq!(read("/etc/hosts").unwrap(), read(&path).unwrap()); 397 | }) 398 | .unwrap(); 399 | } 400 | 401 | #[test] 402 | fn truncates_file() { 403 | start(|| { 404 | let path = format!("/tmp/{}", uuid::Uuid::new_v4()); 405 | write(&path, b"hi").unwrap(); 406 | 407 | write(&path, b"hello").unwrap(); 408 | 409 | assert_eq!(read(&path).unwrap(), b"hello"); 410 | }) 411 | .unwrap(); 412 | } 413 | 414 | #[test] 415 | fn appends_to_file() { 416 | start(|| { 417 | let path = format!("/tmp/{}", uuid::Uuid::new_v4()); 418 | write(&path, b"hi ").unwrap(); 419 | 420 | let mut file = File::options().append(true).open(&path).unwrap(); 421 | file.write_all(b"hello").unwrap(); 422 | 423 | assert_eq!(read(&path).unwrap(), b"hi hello"); 424 | }) 425 | .unwrap(); 426 | } 427 | 428 | #[test] 429 | fn queries_metadata() { 430 | start(|| { 431 | let uringy = metadata("/etc/hosts").unwrap(); 432 | let std = std::fs::metadata("/etc/hosts").unwrap(); 433 | 434 | // core 435 | assert_eq!(uringy.file_type(), std.file_type()); 436 | assert_eq!(uringy.is_dir(), std.is_dir()); 437 | assert_eq!(uringy.is_file(), std.is_file()); 438 | assert_eq!(uringy.is_symlink(), std.is_symlink()); 439 | assert_eq!(uringy.len(), std.len()); 440 | assert_eq!(uringy.permissions(), std.permissions()); 441 | // assert_eq!(uringy.modified(), std.modified()); 442 | // assert_eq!(uringy.accessed(), std.accessed()); 443 | // assert_eq!(uringy.created(), std.created()); 444 | 445 | { 446 | use std::os::unix::fs::MetadataExt; 447 | 448 | assert_eq!(uringy.dev(), std.dev()); 449 | assert_eq!(uringy.ino(), std.ino()); 450 | assert_eq!(uringy.mode(), std.mode()); 451 | assert_eq!(uringy.nlink(), std.nlink()); 452 | assert_eq!(uringy.uid(), std.uid()); 453 | assert_eq!(uringy.gid(), std.gid()); 454 | assert_eq!(uringy.rdev(), std.rdev()); 455 | assert_eq!(uringy.size(), std.size()); 456 | assert_eq!(uringy.atime(), std.atime()); 457 | assert_eq!(uringy.atime_nsec(), std.atime_nsec()); 458 | assert_eq!(uringy.mtime(), std.mtime()); 459 | assert_eq!(uringy.mtime_nsec(), std.mtime_nsec()); 460 | assert_eq!(uringy.ctime(), std.ctime()); 461 | assert_eq!(uringy.ctime_nsec(), std.ctime_nsec()); 462 | assert_eq!(uringy.blksize(), std.blksize()); 463 | assert_eq!(uringy.blocks(), std.blocks()); 464 | } 465 | 466 | { 467 | use std::os::linux::fs::MetadataExt; 468 | 469 | // assert_eq!(uringy.as_raw_stat(), std.as_raw_stat()); 470 | assert_eq!(uringy.st_dev(), std.st_dev()); 471 | assert_eq!(uringy.st_ino(), std.st_ino()); 472 | assert_eq!(uringy.st_mode(), std.st_mode()); 473 | assert_eq!(uringy.st_nlink(), std.st_nlink()); 474 | assert_eq!(uringy.st_uid(), std.st_uid()); 475 | assert_eq!(uringy.st_gid(), std.st_gid()); 476 | assert_eq!(uringy.st_rdev(), std.st_rdev()); 477 | assert_eq!(uringy.st_size(), std.st_size()); 478 | assert_eq!(uringy.st_atime(), std.st_atime()); 479 | assert_eq!(uringy.st_atime_nsec(), std.st_atime_nsec()); 480 | assert_eq!(uringy.st_mtime(), std.st_mtime()); 481 | assert_eq!(uringy.st_mtime_nsec(), std.st_mtime_nsec()); 482 | assert_eq!(uringy.st_ctime(), std.st_ctime()); 483 | assert_eq!(uringy.st_ctime_nsec(), std.st_ctime_nsec()); 484 | assert_eq!(uringy.st_blksize(), std.st_blksize()); 485 | assert_eq!(uringy.st_blocks(), std.st_blocks()); 486 | } 487 | }) 488 | .unwrap(); 489 | } 490 | } 491 | -------------------------------------------------------------------------------- /src/ecosystem/nats/proto.rs: -------------------------------------------------------------------------------- 1 | //! ... 2 | 3 | use std::io::Write; 4 | use std::{io, str}; 5 | 6 | use nom::bytes::streaming as bytes; 7 | use nom::character::streaming as character; 8 | use nom::combinator::*; 9 | use nom::sequence::*; 10 | 11 | #[derive(Debug, PartialEq)] 12 | pub(super) enum ClientOperation<'a> { 13 | /// Sent to server to specify connection information. 14 | /// Wire protocol: `CONNECT `. 15 | Connect { json: &'a str }, 16 | 17 | /// Publish a message to a subject, with optional reply subject. 18 | /// Wire protocol: `PUB [reply-to] <#bytes>\r\n[payload]\r\n`. 19 | Pub { 20 | subject: &'a str, 21 | reply_to: Option<&'a str>, 22 | payload: &'a [u8], 23 | }, 24 | 25 | /// Publish a message to a subject, with optional reply subject, with headers. 26 | /// Wire protocol: `HPUB [reply-to] <#header_bytes> <#total_bytes>\r\n[headers][payload]\r\n`. 27 | Hpub { 28 | subject: &'a str, 29 | reply_to: Option<&'a str>, 30 | headers: &'a str, 31 | payload: &'a [u8], 32 | }, 33 | 34 | /// Subscribe to a subject, with optional load balancing. 35 | /// Wire protocol: `SUB [queue group] \r\n`. 36 | Sub { 37 | subject: &'a str, 38 | queue_group: Option<&'a str>, 39 | sid: u64, // FIXME: sid is alphanumeric 40 | }, 41 | 42 | /// Unsubscribe from subject, optionally after a number of messages. 43 | /// Wire protocol: `UNSUB [max_msgs]\r\n`. 44 | Unsub { sid: u64, max_messages: Option }, // FIXME: sid is alphanumeric 45 | 46 | /// PING keep-alive message. 47 | /// Wire protocol: `PING\r\n`. 48 | Ping, 49 | 50 | /// PONG keep-alive response. 51 | /// Wire protocol: `PONG\r\n`. 52 | Pong, 53 | } 54 | 55 | impl ClientOperation<'_> { 56 | pub(super) fn encode(&self, writer: &mut impl Write) -> io::Result<()> { 57 | let mut numbers = itoa::Buffer::new(); 58 | 59 | match *self { 60 | ClientOperation::Connect { json } => { 61 | writer.write_all(b"CONNECT ")?; 62 | writer.write_all(json.as_bytes())?; 63 | } 64 | ClientOperation::Pub { 65 | subject, 66 | reply_to, 67 | payload, 68 | } => { 69 | writer.write_all(b"PUB ")?; 70 | writer.write_all(subject.as_bytes())?; 71 | writer.write_all(b" ")?; 72 | if let Some(reply_to) = reply_to { 73 | writer.write_all(reply_to.as_bytes())?; 74 | writer.write_all(b" ")?; 75 | } 76 | writer.write_all(numbers.format(payload.len()).as_bytes())?; 77 | writer.write_all(b"\r\n")?; 78 | writer.write_all(payload)?; 79 | } 80 | ClientOperation::Hpub { .. } => unimplemented!(), 81 | ClientOperation::Sub { 82 | subject, 83 | queue_group, 84 | sid, 85 | } => { 86 | writer.write_all(b"SUB ")?; 87 | writer.write_all(subject.as_bytes())?; 88 | writer.write_all(b" ")?; 89 | if let Some(queue_group) = queue_group { 90 | writer.write_all(queue_group.as_bytes())?; 91 | writer.write_all(b" ")?; 92 | } 93 | writer.write_all(numbers.format(sid).as_bytes())?; 94 | } 95 | ClientOperation::Unsub { sid, max_messages } => { 96 | writer.write_all(b"UNSUB ")?; 97 | writer.write_all(sid.to_string().as_bytes())?; 98 | if let Some(max_messages) = max_messages { 99 | writer.write_all(b" ")?; 100 | writer.write_all(numbers.format(max_messages).as_bytes())?; 101 | } 102 | } 103 | ClientOperation::Ping => writer.write_all(b"PING")?, 104 | ClientOperation::Pong => writer.write_all(b"PONG")?, 105 | } 106 | 107 | writer.write_all(b"\r\n")?; 108 | 109 | Ok(()) 110 | } 111 | } 112 | 113 | #[derive(Debug, PartialEq)] 114 | pub(super) enum ServerOperation<'a> { 115 | /// Sent to client after initial TCP/IP connection. 116 | /// Wire protocol: `INFO \r\n`. 117 | Info { json: &'a str }, 118 | 119 | /// Delivers a message payload to a subscriber, with optional reply subject. 120 | /// Wire protocol: `MSG [reply-to] <#bytes>\r\n[payload]\r\n`. 121 | Msg { 122 | subject: &'a str, 123 | sid: u64, // FIXME: sid is alphanumeric 124 | reply_to: Option<&'a str>, 125 | payload: &'a [u8], 126 | }, 127 | 128 | /// Delivers a message payload to a subscriber, with optional reply subject, with headers. 129 | /// Wire protocol: `HMSG [reply-to] <#header_bytes> <#total_bytes>\r\n[headers][payload]\r\n`. 130 | Hmsg { 131 | subject: &'a str, 132 | sid: u64, // FIXME: sid is alphanumeric 133 | reply_to: Option<&'a str>, 134 | headers: &'a str, 135 | payload: &'a [u8], 136 | }, 137 | 138 | /// Verbose mode acknowledgment. 139 | /// Wire protocol: `+OK\r\n`. 140 | Ok, 141 | 142 | /// Verbose mode protocol error, may cause client disconnection. 143 | /// Wire protocol: `-ERR \r\n`. 144 | Err { message: &'a str }, 145 | 146 | /// PING keep-alive message. 147 | /// Wire protocol: `PING\r\n`. 148 | Ping, 149 | 150 | /// PONG keep-alive response. 151 | /// Wire protocol: `PONG\r\n`. 152 | Pong, 153 | } 154 | 155 | impl ServerOperation<'_> { 156 | pub(super) fn decode(buffer: &[u8]) -> Result<(ServerOperation, usize), NatsProtoError> { 157 | match parse_server_operation(buffer) { 158 | Ok((remaining, operation)) => Ok((operation, buffer.len() - remaining.len())), 159 | Err(err) => match err { 160 | nom::Err::Incomplete(_) => Err(NatsProtoError::BufferTooSmall), 161 | nom::Err::Error(_) => Err(NatsProtoError::InvalidProtocol), 162 | nom::Err::Failure(_) => Err(NatsProtoError::InvalidProtocol), 163 | }, 164 | } 165 | } 166 | } 167 | 168 | // TODO: this_err 169 | #[derive(Debug)] 170 | pub(super) enum NatsProtoError { 171 | /// ... 172 | BufferTooSmall, 173 | 174 | /// ... 175 | InvalidProtocol, 176 | } 177 | 178 | fn parse_server_operation(buffer: &[u8]) -> nom::IResult<&[u8], ServerOperation> { 179 | let (buffer, operation) = nom::branch::alt(( 180 | parse_info, parse_msg, parse_ok, parse_err, parse_ping, parse_pong, 181 | ))(buffer)?; 182 | let (buffer, _) = parse_newline(buffer)?; 183 | Ok((buffer, operation)) 184 | } 185 | 186 | fn parse_info(buffer: &[u8]) -> nom::IResult<&[u8], ServerOperation> { 187 | let (buffer, _) = bytes::tag_no_case("INFO")(buffer)?; 188 | let (buffer, _) = parse_whitespace(buffer)?; 189 | 190 | let (buffer, json) = map_res(bytes::take_till1(|byte| byte == b'\r'), str::from_utf8)(buffer)?; 191 | 192 | Ok((buffer, ServerOperation::Info { json })) 193 | } 194 | 195 | fn parse_msg(buffer: &[u8]) -> nom::IResult<&[u8], ServerOperation> { 196 | let (buffer, _) = bytes::tag_no_case("MSG")(buffer)?; 197 | let (buffer, _) = parse_whitespace(buffer)?; 198 | 199 | let (buffer, subject) = parse_subject(buffer)?; 200 | let (buffer, _) = parse_whitespace(buffer)?; 201 | 202 | let (buffer, sid) = character::u64(buffer)?; 203 | let (buffer, _) = parse_whitespace(buffer)?; 204 | 205 | let (buffer, reply_to) = opt(terminated(parse_subject, parse_whitespace))(buffer)?; 206 | 207 | let (buffer, payload) = parse_payload(buffer)?; 208 | 209 | Ok(( 210 | buffer, 211 | ServerOperation::Msg { 212 | subject, 213 | sid, 214 | reply_to, 215 | payload, 216 | }, 217 | )) 218 | } 219 | 220 | // TODO: parse_hmsg 221 | 222 | fn parse_ok(buffer: &[u8]) -> nom::IResult<&[u8], ServerOperation> { 223 | let (buffer, _) = bytes::tag_no_case("+OK")(buffer)?; 224 | 225 | Ok((buffer, ServerOperation::Ok)) 226 | } 227 | 228 | fn parse_err(buffer: &[u8]) -> nom::IResult<&[u8], ServerOperation> { 229 | let (buffer, _) = bytes::tag_no_case("-ERR ")(buffer)?; 230 | 231 | let (buffer, message) = map_res(bytes::take_till1(|byte| byte == b'\r'), |bytes| { 232 | str::from_utf8(bytes) 233 | })(buffer)?; 234 | 235 | Ok((buffer, ServerOperation::Err { message })) 236 | } 237 | 238 | fn parse_ping(buffer: &[u8]) -> nom::IResult<&[u8], ServerOperation> { 239 | let (buffer, _) = bytes::tag_no_case("PING")(buffer)?; 240 | 241 | Ok((buffer, ServerOperation::Ping)) 242 | } 243 | 244 | fn parse_pong(buffer: &[u8]) -> nom::IResult<&[u8], ServerOperation> { 245 | let (buffer, _) = bytes::tag_no_case("PONG")(buffer)?; 246 | 247 | Ok((buffer, ServerOperation::Pong)) 248 | } 249 | 250 | fn parse_subject(buffer: &[u8]) -> nom::IResult<&[u8], &str> { 251 | // TODO: while(alphanumeric)|wildcard|fullwildcard delimited with . 252 | let (buffer, subject) = map_res( 253 | bytes::take_while1(|byte| { 254 | nom::character::is_alphabetic(byte) || byte == b'.' || byte == b'*' || byte == b'>' 255 | }), 256 | str::from_utf8, 257 | )(buffer)?; 258 | Ok((buffer, subject)) 259 | } 260 | 261 | fn parse_payload(buffer: &[u8]) -> nom::IResult<&[u8], &[u8]> { 262 | let (buffer, payload_size) = character::u64(buffer)?; 263 | let (buffer, _) = parse_newline(buffer)?; 264 | bytes::take(payload_size)(buffer) 265 | } 266 | 267 | fn parse_newline(buffer: &[u8]) -> nom::IResult<&[u8], ()> { 268 | let (buffer, _) = bytes::tag("\r\n")(buffer)?; 269 | 270 | Ok((buffer, ())) 271 | } 272 | 273 | fn parse_whitespace(buffer: &[u8]) -> nom::IResult<&[u8], ()> { 274 | let (buffer, _) = bytes::take_while1(nom::character::is_space)(buffer)?; 275 | 276 | Ok((buffer, ())) 277 | } 278 | 279 | #[cfg(test)] 280 | mod tests { 281 | use super::*; 282 | 283 | mod client_operation { 284 | use std::io::Cursor; 285 | 286 | use super::*; 287 | 288 | #[test] 289 | fn encodes_connect() { 290 | let operation = ClientOperation::Connect { json: "{}" }; 291 | let mut cursor = Cursor::new(vec![]); 292 | 293 | let result = operation.encode(&mut cursor); 294 | 295 | assert!(result.is_ok()); 296 | assert_eq!(cursor.into_inner(), b"CONNECT {}\r\n"); 297 | } 298 | 299 | mod encodes_pub { 300 | use super::*; 301 | 302 | #[test] 303 | fn base() { 304 | let operation = ClientOperation::Pub { 305 | subject: "foo", 306 | reply_to: None, 307 | payload: &[], 308 | }; 309 | let mut cursor = Cursor::new(vec![]); 310 | 311 | let result = operation.encode(&mut cursor); 312 | 313 | assert!(result.is_ok()); 314 | assert_eq!(cursor.into_inner(), b"PUB foo 0\r\n\r\n"); 315 | } 316 | 317 | #[test] 318 | fn with_reply() { 319 | let operation = ClientOperation::Pub { 320 | subject: "foo", 321 | reply_to: Some("bar"), 322 | payload: &[], 323 | }; 324 | let mut cursor = Cursor::new(vec![]); 325 | 326 | let result = operation.encode(&mut cursor); 327 | 328 | assert!(result.is_ok()); 329 | assert_eq!(cursor.into_inner(), b"PUB foo bar 0\r\n\r\n"); 330 | } 331 | 332 | #[test] 333 | fn with_payload() { 334 | let operation = ClientOperation::Pub { 335 | subject: "foo", 336 | reply_to: None, 337 | payload: b"hello", 338 | }; 339 | let mut cursor = Cursor::new(vec![]); 340 | 341 | let result = operation.encode(&mut cursor); 342 | 343 | assert!(result.is_ok()); 344 | assert_eq!(cursor.into_inner(), b"PUB foo 5\r\nhello\r\n"); 345 | } 346 | 347 | #[test] 348 | fn with_payload_and_reply() { 349 | let operation = ClientOperation::Pub { 350 | subject: "foo", 351 | reply_to: Some("bar"), 352 | payload: b"hello", 353 | }; 354 | let mut cursor = Cursor::new(vec![]); 355 | 356 | let result = operation.encode(&mut cursor); 357 | 358 | assert!(result.is_ok()); 359 | assert_eq!(cursor.into_inner(), b"PUB foo bar 5\r\nhello\r\n"); 360 | } 361 | } 362 | 363 | mod encodes_sub { 364 | use super::*; 365 | 366 | #[test] 367 | fn base() { 368 | let operation = ClientOperation::Sub { 369 | subject: "foo", 370 | queue_group: None, 371 | sid: 123, 372 | }; 373 | let mut cursor = Cursor::new(vec![]); 374 | 375 | let result = operation.encode(&mut cursor); 376 | 377 | assert!(result.is_ok()); 378 | assert_eq!(cursor.into_inner(), b"SUB foo 123\r\n"); 379 | } 380 | 381 | #[test] 382 | fn with_queue_group() { 383 | let operation = ClientOperation::Sub { 384 | subject: "foo", 385 | queue_group: Some("biz"), 386 | sid: 123, 387 | }; 388 | let mut cursor = Cursor::new(vec![]); 389 | 390 | let result = operation.encode(&mut cursor); 391 | 392 | assert!(result.is_ok()); 393 | assert_eq!(cursor.into_inner(), b"SUB foo biz 123\r\n"); 394 | } 395 | } 396 | 397 | mod encodes_unsub { 398 | use super::*; 399 | 400 | #[test] 401 | fn base() { 402 | let operation = ClientOperation::Unsub { 403 | sid: 123, 404 | max_messages: None, 405 | }; 406 | let mut cursor = Cursor::new(vec![]); 407 | 408 | let result = operation.encode(&mut cursor); 409 | 410 | assert!(result.is_ok()); 411 | assert_eq!(cursor.into_inner(), b"UNSUB 123\r\n"); 412 | } 413 | 414 | #[test] 415 | fn with_max_messages() { 416 | let operation = ClientOperation::Unsub { 417 | sid: 123, 418 | max_messages: Some(3), 419 | }; 420 | let mut cursor = Cursor::new(vec![]); 421 | 422 | let result = operation.encode(&mut cursor); 423 | 424 | assert!(result.is_ok()); 425 | assert_eq!(cursor.into_inner(), b"UNSUB 123 3\r\n"); 426 | } 427 | } 428 | 429 | #[test] 430 | fn encodes_ping() { 431 | let operation = ClientOperation::Ping; 432 | let mut cursor = Cursor::new(vec![]); 433 | 434 | let result = operation.encode(&mut cursor); 435 | 436 | assert!(result.is_ok()); 437 | assert_eq!(cursor.into_inner(), b"PING\r\n"); 438 | } 439 | 440 | #[test] 441 | fn encodes_pong() { 442 | let operation = ClientOperation::Pong; 443 | let mut cursor = Cursor::new(vec![]); 444 | 445 | let result = operation.encode(&mut cursor); 446 | 447 | assert!(result.is_ok()); 448 | assert_eq!(cursor.into_inner(), b"PONG\r\n"); 449 | } 450 | } 451 | 452 | mod server_operation { 453 | use super::*; 454 | 455 | #[test] 456 | fn decodes_info() { 457 | let wire = b"INFO {}\r\n"; 458 | 459 | let (operation, wire_size) = ServerOperation::decode(wire).unwrap(); 460 | 461 | assert_eq!(operation, ServerOperation::Info { json: "{}" }); 462 | assert_eq!(wire_size, wire.len()); 463 | } 464 | 465 | mod decodes_msg { 466 | use super::*; 467 | 468 | #[test] 469 | fn base() { 470 | let wire = b"MSG foo 123 0\r\n\r\n"; 471 | 472 | let (operation, wire_size) = ServerOperation::decode(wire).unwrap(); 473 | 474 | assert_eq!( 475 | operation, 476 | ServerOperation::Msg { 477 | subject: "foo", 478 | sid: 123, 479 | reply_to: None, 480 | payload: &[], 481 | } 482 | ); 483 | assert_eq!(wire_size, wire.len()); 484 | } 485 | 486 | #[test] 487 | fn with_reply_to() { 488 | let wire = b"MSG foo 123 bar 0\r\n\r\n"; 489 | 490 | let (operation, wire_size) = ServerOperation::decode(wire).unwrap(); 491 | 492 | assert_eq!( 493 | operation, 494 | ServerOperation::Msg { 495 | subject: "foo", 496 | sid: 123, 497 | reply_to: Some("bar"), 498 | payload: &[], 499 | } 500 | ); 501 | assert_eq!(wire_size, wire.len()); 502 | } 503 | 504 | #[test] 505 | fn with_payload() { 506 | let wire = b"MSG foo 123 5\r\nhello\r\n"; 507 | 508 | let (operation, wire_size) = ServerOperation::decode(wire).unwrap(); 509 | 510 | assert_eq!( 511 | operation, 512 | ServerOperation::Msg { 513 | subject: "foo", 514 | sid: 123, 515 | reply_to: None, 516 | payload: b"hello", 517 | } 518 | ); 519 | assert_eq!(wire_size, wire.len()); 520 | } 521 | 522 | #[test] 523 | fn with_payload_and_reply_to() { 524 | let wire = b"MSG foo 123 bar 5\r\nhello\r\n"; 525 | 526 | let (operation, wire_size) = ServerOperation::decode(wire).unwrap(); 527 | 528 | assert_eq!( 529 | operation, 530 | ServerOperation::Msg { 531 | subject: "foo", 532 | sid: 123, 533 | reply_to: Some("bar"), 534 | payload: b"hello", 535 | } 536 | ); 537 | assert_eq!(wire_size, wire.len()); 538 | } 539 | } 540 | 541 | #[test] 542 | fn decodes_ok() { 543 | let wire = b"+OK\r\n"; 544 | 545 | let (operation, wire_size) = ServerOperation::decode(wire).unwrap(); 546 | 547 | assert_eq!(operation, ServerOperation::Ok); 548 | assert_eq!(wire_size, wire.len()); 549 | } 550 | 551 | #[test] 552 | fn decodes_error() { 553 | let wire = b"-ERR oops\r\n"; 554 | 555 | let (operation, wire_size) = ServerOperation::decode(wire).unwrap(); 556 | 557 | assert_eq!(operation, ServerOperation::Err { message: "oops" }); 558 | assert_eq!(wire_size, wire.len()); 559 | } 560 | 561 | #[test] 562 | fn decodes_ping() { 563 | let wire = b"PING\r\n"; 564 | 565 | let (operation, wire_size) = ServerOperation::decode(wire).unwrap(); 566 | 567 | assert_eq!(operation, ServerOperation::Ping); 568 | assert_eq!(wire_size, wire.len()); 569 | } 570 | 571 | #[test] 572 | fn decodes_pong() { 573 | let wire = b"PONG\r\n"; 574 | 575 | let (operation, wire_size) = ServerOperation::decode(wire).unwrap(); 576 | 577 | assert_eq!(operation, ServerOperation::Pong); 578 | assert_eq!(wire_size, wire.len()); 579 | } 580 | } 581 | } 582 | -------------------------------------------------------------------------------- /src/runtime/mod.rs: -------------------------------------------------------------------------------- 1 | //! ... 2 | 3 | use std::any::Any; 4 | use std::collections::{BTreeSet, VecDeque}; 5 | use std::num::NonZeroUsize; 6 | use std::{ffi, hint, io, marker, mem, panic, thread}; 7 | 8 | mod context_switch; 9 | mod stack; 10 | mod syscall; 11 | mod tls; 12 | 13 | /// ... 14 | pub fn start T, T>(f: F) -> thread::Result { 15 | tls::exclusive_runtime(|| { 16 | let (original, root) = tls::runtime(|runtime| { 17 | let root_fiber = runtime.create_fiber(f, start_trampoline::, false); 18 | runtime.running_fiber = Some(root_fiber); 19 | 20 | ( 21 | runtime.original.as_mut_ptr(), 22 | &runtime.running().continuation as *const context_switch::Continuation, 23 | ) 24 | }); 25 | 26 | unsafe { context_switch::jump(original, root) }; 27 | tls::runtime(|rt| unsafe { rt.running().stack.union_ref::>().read() }) 28 | }) 29 | } 30 | 31 | extern "C" fn start_trampoline T, T>() -> ! { 32 | // execute closure 33 | let closure: F = tls::runtime(|runtime| { 34 | let fiber = runtime.running(); 35 | unsafe { fiber.stack.union_ref::().read() } 36 | }); 37 | 38 | let result = panic::catch_unwind(panic::AssertUnwindSafe(|| (closure)())); 39 | hint::black_box(&result); // removing this causes a segfault in release mode 40 | 41 | tls::runtime(|runtime| { 42 | let fiber = runtime.running(); 43 | fiber.is_completed = true; 44 | fiber.is_cancelled = true; // prevent cancel scheduling while waiting for children 45 | unsafe { fiber.stack.union_mut::>().write(result) }; 46 | }); 47 | 48 | // wait for children 49 | if tls::runtime(|rt| !rt.running().children.is_empty()) { 50 | park(|_| {}); // woken up by last child 51 | } 52 | 53 | // deallocate stack 54 | tls::runtime(|runtime| { 55 | let stack = runtime.running().stack; 56 | runtime.stack_pool.push(stack); 57 | }); 58 | 59 | // return to original thread 60 | let mut dummy = mem::MaybeUninit::uninit(); 61 | let original = tls::runtime(|runtime| runtime.original.as_ptr()); 62 | unsafe { context_switch::jump(dummy.as_mut_ptr(), original) }; 63 | unreachable!(); 64 | } 65 | 66 | struct RuntimeState { 67 | kernel: syscall::Interface, 68 | fibers: slab::Slab, 69 | ready_fibers: VecDeque, 70 | running_fiber: Option, 71 | stack_pool: Vec, 72 | original: mem::MaybeUninit, 73 | } 74 | 75 | impl RuntimeState { 76 | fn new() -> Self { 77 | RuntimeState { 78 | kernel: syscall::Interface::new(), 79 | fibers: slab::Slab::new(), 80 | ready_fibers: VecDeque::new(), 81 | running_fiber: None, 82 | stack_pool: Vec::new(), 83 | original: mem::MaybeUninit::uninit(), 84 | } 85 | } 86 | 87 | fn create_fiber T, T>( 88 | &mut self, 89 | f: F, 90 | trampoline: extern "C" fn() -> !, 91 | is_cancelled: bool, 92 | ) -> FiberIndex { 93 | // allocate stack 94 | let mut stack_base = self.stack_pool.pop().unwrap_or_else(|| { 95 | let usable_pages = NonZeroUsize::new(32).unwrap(); 96 | let stack = stack::Stack::new(NonZeroUsize::MIN, usable_pages).unwrap(); 97 | let stack_base = StackBase(stack.base()); 98 | mem::forget(stack); 99 | stack_base 100 | }); 101 | 102 | unsafe { stack_base.union_mut::().write(f) }; 103 | 104 | let index = self.fibers.insert(FiberState { 105 | stack: stack_base, 106 | continuation: unsafe { 107 | context_switch::prepare_stack(stack_base.after_union::(), trampoline) 108 | }, 109 | join_handle: JoinHandleState::Unused, 110 | parent: None, 111 | children: BTreeSet::new(), 112 | syscall_result: None, 113 | is_completed: false, 114 | is_cancelled, 115 | // is_scheduled: false, 116 | }); 117 | 118 | FiberIndex(index) 119 | } 120 | 121 | fn running(&mut self) -> &mut FiberState { 122 | // TODO: #[cfg(not(debug_assertions))]: unwrap_unchecked, get_unchecked. document performance difference. 123 | let fiber_index = self.running_fiber.expect("..."); 124 | &mut self.fibers[fiber_index.0] 125 | } 126 | 127 | fn process_io(&mut self) -> *const context_switch::Continuation { 128 | loop { 129 | for (user_data, result) in self.kernel.process_completed() { 130 | let fiber = FiberIndex(user_data.0 as usize); 131 | self.fibers[fiber.0].syscall_result = Some(result); 132 | Waker(fiber).schedule_with(self); 133 | } 134 | 135 | if let Some(fiber) = self.ready_fibers.pop_front() { 136 | self.running_fiber = Some(fiber); 137 | // self.fibers[fiber.0].is_scheduled = false; 138 | break &self.fibers[fiber.0].continuation as *const context_switch::Continuation; 139 | } 140 | 141 | self.kernel.wait_for_completed(); 142 | } 143 | } 144 | 145 | fn cancel(&mut self, root: FiberIndex) { 146 | // TODO: if is_cancelled { return } (short circuit) 147 | 148 | if !self.fibers[root.0].is_cancelled && root != self.running_fiber.unwrap() { 149 | Waker(root).schedule_with(self); 150 | } 151 | 152 | self.fibers[root.0].is_cancelled = true; 153 | 154 | for child in self.fibers[root.0].children.clone() { 155 | self.cancel(child); 156 | } 157 | } 158 | 159 | // TODO: is_contained flag 160 | fn nearest_contained(&self, _fiber: FiberIndex) -> FiberIndex { 161 | FiberIndex(0) 162 | } 163 | } 164 | 165 | impl Drop for RuntimeState { 166 | fn drop(&mut self) { 167 | let guard_pages = 1; 168 | let usable_pages = 32; 169 | 170 | let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) as usize }; 171 | let length = (guard_pages + usable_pages) * page_size; 172 | 173 | for stack_bottom in self.stack_pool.drain(..) { 174 | let pointer = unsafe { stack_bottom.0.byte_sub(length) }; 175 | drop(stack::Stack { pointer, length }) 176 | } 177 | } 178 | } 179 | 180 | /// ... 181 | /// max 4.3 billion... (u32 takes up less space in FiberState) 182 | #[repr(transparent)] 183 | #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] 184 | struct FiberIndex(usize); 185 | 186 | #[derive(Debug)] 187 | struct FiberState { 188 | stack: StackBase, 189 | continuation: context_switch::Continuation, 190 | join_handle: JoinHandleState, 191 | parent: Option, 192 | children: BTreeSet, 193 | syscall_result: Option, 194 | is_completed: bool, 195 | is_cancelled: bool, 196 | } 197 | 198 | #[derive(Debug)] 199 | enum JoinHandleState { 200 | Unused, 201 | Waiting(Option), // option for taking ownership from mutable reference 202 | Dropped, 203 | } 204 | 205 | /// Upper address of a fiber's stack memory, stack addresses grow downwards. 206 | /// The union of the user's closure and its output is stored at the top of the stack to save space in [FiberState]. 207 | #[derive(Debug, Copy, Clone)] 208 | struct StackBase(*mut ffi::c_void); 209 | 210 | impl StackBase { 211 | unsafe fn union_ref(&self) -> *const U { 212 | (self.0 as *const U).sub(1) 213 | } 214 | 215 | unsafe fn union_mut(&mut self) -> *mut U { 216 | (self.0 as *mut U).sub(1) 217 | } 218 | 219 | unsafe fn after_union(&self) -> *mut ffi::c_void { 220 | let union_size = std::cmp::max(mem::size_of::(), mem::size_of::()); 221 | self.0.byte_sub(union_size) 222 | } 223 | } 224 | 225 | /// Spawns a new fiber, returning a [JoinHandle] for it. 226 | pub fn spawn T + 'static, T: 'static>(f: F) -> JoinHandle { 227 | let child_fiber = tls::runtime(|runtime| { 228 | let is_cancelled = runtime.running().is_cancelled; 229 | let child_fiber = runtime.create_fiber(f, spawn_trampoline::, is_cancelled); 230 | runtime.ready_fibers.push_back(child_fiber); 231 | // runtime.fibers[child_fiber.0].is_scheduled = true; 232 | 233 | // parent child relationship 234 | runtime.running().children.insert(child_fiber); 235 | runtime.fibers[child_fiber.0].parent = Some(runtime.running_fiber.unwrap()); 236 | 237 | child_fiber 238 | }); 239 | 240 | JoinHandle::new(child_fiber) 241 | } 242 | 243 | extern "C" fn spawn_trampoline T, T>() -> ! { 244 | // execute closure 245 | let closure: F = tls::runtime(|rt| unsafe { rt.running().stack.union_ref::().read() }); 246 | let result = panic::catch_unwind(panic::AssertUnwindSafe(|| (closure)())); 247 | hint::black_box(&result); // removing this causes a segfault in release mode 248 | let result_is_error = result.is_err(); 249 | 250 | tls::runtime(|runtime| { 251 | let fiber = runtime.running(); 252 | 253 | fiber.is_completed = true; 254 | fiber.is_cancelled = true; // prevent cancel scheduling while waiting for children 255 | unsafe { fiber.stack.union_mut::>().write(result) }; 256 | }); 257 | 258 | // wait for children 259 | if tls::runtime(|rt| !rt.running().children.is_empty()) { 260 | park(|_| {}); // woken up by last child 261 | } 262 | 263 | // schedule joining fiber 264 | tls::runtime(|runtime| { 265 | if let JoinHandleState::Waiting(waker) = &mut runtime.running().join_handle { 266 | waker.take().unwrap().schedule_with(runtime); 267 | } else if result_is_error { 268 | let nearest_contained = runtime.nearest_contained(runtime.running_fiber.unwrap()); 269 | runtime.cancel(nearest_contained); 270 | } 271 | }); 272 | 273 | // cleanup parent 274 | tls::runtime(|runtime| { 275 | let parent_index = runtime.running().parent.unwrap(); 276 | let parent = &mut runtime.fibers[parent_index.0]; 277 | 278 | parent.children.remove(&runtime.running_fiber.unwrap()); 279 | 280 | if parent.is_completed && parent.children.is_empty() { 281 | Waker(parent_index).schedule_with(runtime); 282 | } 283 | }); 284 | 285 | // deallocate stack 286 | tls::runtime(|runtime| { 287 | if let JoinHandleState::Dropped = runtime.running().join_handle { 288 | let stack = runtime.running().stack; 289 | runtime.stack_pool.push(stack); 290 | runtime.fibers.remove(runtime.running_fiber.unwrap().0); 291 | } 292 | }); 293 | 294 | // continue to next fiber 295 | let mut dummy = mem::MaybeUninit::uninit(); 296 | let next = tls::runtime(|runtime| runtime.process_io()); 297 | unsafe { context_switch::jump(dummy.as_mut_ptr(), next) }; 298 | unreachable!() 299 | } 300 | 301 | /// Handle for joining or cancelling a fiber. 302 | #[derive(Debug)] 303 | pub struct JoinHandle { 304 | fiber: FiberIndex, 305 | output: marker::PhantomData, 306 | } 307 | 308 | impl JoinHandle { 309 | fn new(fiber: FiberIndex) -> Self { 310 | JoinHandle { 311 | fiber, 312 | output: marker::PhantomData, 313 | } 314 | } 315 | 316 | /// ... 317 | /// if cancelled: returns if completes, waits only if waiting on an already cancelled fiber. 318 | pub fn join(self) -> Result>> { 319 | if tls::runtime(|rt| rt.fibers[self.fiber.0].is_completed) { 320 | return self.read_output(); 321 | } 322 | 323 | if is_cancelled() && !tls::runtime(|rt| rt.fibers[self.fiber.0].is_cancelled) { 324 | return Err(crate::Error::Cancelled); 325 | } 326 | 327 | park(|waker| { 328 | tls::runtime(|runtime| { 329 | let fiber = &mut runtime.fibers[self.fiber.0]; 330 | assert!(!fiber.is_completed); 331 | fiber.join_handle = JoinHandleState::Waiting(Some(waker)); 332 | }); 333 | }); // woken up by completion or cancellation 334 | 335 | if tls::runtime(|rt| rt.fibers[self.fiber.0].is_completed) { 336 | return self.read_output(); 337 | } 338 | 339 | assert!(is_cancelled()); 340 | if !tls::runtime(|rt| rt.fibers[self.fiber.0].is_cancelled) { 341 | return Err(crate::Error::Cancelled); 342 | } 343 | park(|_| {}); // woken up by completion 344 | 345 | self.read_output() 346 | } 347 | 348 | fn read_output(self) -> Result>> { 349 | tls::runtime(|runtime| { 350 | let fiber = &mut runtime.fibers[self.fiber.0]; 351 | let result = unsafe { fiber.stack.union_ref::>().read() }; 352 | result.map_err(|e| crate::Error::Original(e)) 353 | }) 354 | } 355 | 356 | /// ... 357 | pub fn cancel(&self) { 358 | tls::runtime(|runtime| { 359 | runtime.cancel(self.fiber); 360 | }) 361 | } 362 | 363 | /// ... 364 | pub fn cancel_propagating(&self) { 365 | tls::runtime(|runtime| { 366 | let nearest_contained = runtime.nearest_contained(self.fiber); 367 | runtime.cancel(nearest_contained); 368 | }) 369 | } 370 | } 371 | 372 | impl Drop for JoinHandle { 373 | fn drop(&mut self) { 374 | // deallocate stack 375 | tls::runtime(|runtime| { 376 | runtime.fibers[self.fiber.0].join_handle = JoinHandleState::Dropped; 377 | 378 | if runtime.fibers[self.fiber.0].is_completed { 379 | let stack = runtime.fibers[self.fiber.0].stack; 380 | runtime.stack_pool.push(stack); 381 | runtime.fibers.remove(self.fiber.0); 382 | } 383 | }); 384 | } 385 | } 386 | 387 | /// ... 388 | pub fn park(schedule: impl FnOnce(Waker)) { 389 | let running = tls::runtime(|runtime| runtime.running_fiber.unwrap()); 390 | 391 | let waker = Waker(running); 392 | schedule(waker); 393 | 394 | // continue to next fiber 395 | let (running, next) = tls::runtime(|runtime| { 396 | ( 397 | &mut runtime.running().continuation as *mut context_switch::Continuation, 398 | runtime.process_io(), 399 | ) 400 | }); 401 | unsafe { context_switch::jump(running, next) }; 402 | } 403 | 404 | /// Handle for scheduling a parked fiber. 405 | #[repr(transparent)] 406 | #[derive(Debug)] 407 | pub struct Waker(FiberIndex); 408 | 409 | impl Waker { 410 | /// Wake up the parked fiber to be run at some point. 411 | pub fn schedule(self) { 412 | tls::runtime(|runtime| { 413 | self.schedule_with(runtime); 414 | }); 415 | } 416 | 417 | fn schedule_with(self, runtime: &mut RuntimeState) { 418 | // if !runtime.fibers[self.0 .0].is_scheduled { 419 | // FIXME: slow 420 | if !runtime.ready_fibers.contains(&self.0) { 421 | runtime.ready_fibers.push_back(self.0); 422 | // runtime.fibers[self.0 .0].is_scheduled = true; 423 | } 424 | } 425 | 426 | // Wake up the parked fiber to be run next. 427 | // pub fn schedule_immediately(self) {} 428 | } 429 | 430 | pub fn yield_now() { 431 | // TODO: schedule_io, fast path, document speedup 432 | 433 | park(|waker| waker.schedule()); 434 | } 435 | 436 | /// ... 437 | pub fn cancel() { 438 | tls::runtime(|runtime| { 439 | runtime.cancel(runtime.running_fiber.unwrap()); 440 | }) 441 | } 442 | 443 | /// ... 444 | pub fn cancel_propagating() { 445 | tls::runtime(|runtime| { 446 | let nearest_contained = runtime.nearest_contained(runtime.running_fiber.unwrap()); 447 | runtime.cancel(nearest_contained); 448 | }) 449 | } 450 | 451 | /// ... 452 | pub fn is_cancelled() -> bool { 453 | tls::runtime(|runtime| { 454 | let fiber = runtime.running(); 455 | fiber.is_cancelled 456 | }) 457 | } 458 | 459 | pub(crate) fn syscall(sqe: io_uring::squeue::Entry) -> crate::IoResult { 460 | if is_cancelled() { 461 | return Err(crate::Error::Cancelled); 462 | } 463 | 464 | let fiber_id = tls::runtime(|rt| rt.running_fiber.unwrap()); 465 | let syscall_id = syscall::Id(fiber_id.0 as u64); 466 | 467 | tls::runtime(|runtime| { 468 | let fiber = runtime.running(); 469 | assert!(fiber.syscall_result.is_none()); 470 | 471 | runtime.kernel.issue(syscall_id, sqe); 472 | }); 473 | 474 | park(|_| {}); // woken up by CQE or cancellation 475 | 476 | if tls::runtime(|rt| rt.running().syscall_result.is_some()) { 477 | return read_syscall_result(); 478 | } 479 | 480 | assert!(is_cancelled()); 481 | tls::runtime(|rt| rt.kernel.cancel(syscall_id)); 482 | park(|_| {}); // woken up by CQE 483 | 484 | read_syscall_result() 485 | } 486 | 487 | fn read_syscall_result() -> crate::IoResult { 488 | let result = tls::runtime(|rt| rt.running().syscall_result.take()).unwrap(); 489 | 490 | if result >= 0 { 491 | Ok(result as u32) 492 | } else { 493 | if -result == libc::ECANCELED { 494 | return Err(crate::Error::Cancelled); 495 | } 496 | 497 | let error = io::Error::from_raw_os_error(-result); 498 | Err(crate::Error::Original(error)) 499 | } 500 | } 501 | 502 | #[cfg(test)] 503 | mod tests { 504 | use std::time::{Duration, Instant}; 505 | 506 | use super::*; 507 | 508 | mod start { 509 | use std::time::Duration; 510 | 511 | use super::*; 512 | 513 | #[test] 514 | fn returns_output() { 515 | let output = start(|| 123); 516 | 517 | assert_eq!(output.unwrap(), 123); 518 | } 519 | 520 | #[test] 521 | fn catches_panic() { 522 | let result = start(|| panic!()); 523 | 524 | assert!(result.is_err()); 525 | } 526 | 527 | #[test] 528 | #[should_panic] 529 | fn cant_nest() { 530 | start(|| { 531 | start(|| {}).unwrap(); 532 | }) 533 | .unwrap(); 534 | } 535 | 536 | #[test] 537 | fn works_several_times() { 538 | start(|| {}).unwrap(); 539 | start(|| {}).unwrap(); 540 | } 541 | 542 | #[test] 543 | fn works_in_parallel() { 544 | let handle = thread::spawn(|| { 545 | start(|| thread::sleep(Duration::from_millis(2))).unwrap(); 546 | }); 547 | 548 | start(|| { 549 | thread::sleep(Duration::from_millis(1)); 550 | }) 551 | .unwrap(); 552 | 553 | handle.join().unwrap(); 554 | } 555 | 556 | #[test] 557 | fn waits_for_dropped_child() { 558 | let before = Instant::now(); 559 | 560 | start(|| { 561 | let handle = spawn(|| crate::time::sleep(Duration::from_millis(5))); 562 | drop(handle); 563 | }) 564 | .unwrap(); 565 | 566 | assert!(before.elapsed() > Duration::from_millis(5)); 567 | } 568 | 569 | #[test] 570 | fn waits_for_forgotten_child() { 571 | let before = Instant::now(); 572 | 573 | start(|| { 574 | let handle = spawn(|| crate::time::sleep(Duration::from_millis(5))); 575 | mem::forget(handle); 576 | }) 577 | .unwrap(); 578 | 579 | assert!(before.elapsed() > Duration::from_millis(5)); 580 | } 581 | 582 | #[test] 583 | #[ignore] 584 | fn cleans_up_after_itself() { 585 | // enough to hit OS limits 586 | for _ in 0..1_000_000 { 587 | start(|| {}).unwrap(); 588 | } 589 | } 590 | 591 | mod cancellation { 592 | use super::*; 593 | 594 | #[test] 595 | fn initially_not_cancelled() { 596 | start(|| { 597 | assert!(!is_cancelled()); 598 | }) 599 | .unwrap(); 600 | } 601 | 602 | #[test] 603 | fn cancelled_after_cancelling_self() { 604 | start(|| { 605 | cancel(); 606 | assert!(is_cancelled()); 607 | }) 608 | .unwrap(); 609 | } 610 | } 611 | } 612 | 613 | mod spawn { 614 | use super::*; 615 | 616 | #[test] 617 | fn returns_child_output() { 618 | start(|| { 619 | let handle = spawn(|| 123); 620 | 621 | let output = handle.join(); 622 | 623 | assert_eq!(output.unwrap(), 123); 624 | }) 625 | .unwrap(); 626 | } 627 | 628 | #[test] 629 | fn returns_non_child_output() { 630 | start(|| { 631 | let other = spawn(|| 123); 632 | let handle = spawn(|| other.join().unwrap()); 633 | 634 | let output = handle.join(); 635 | 636 | assert_eq!(output.unwrap(), 123); 637 | }) 638 | .unwrap(); 639 | } 640 | 641 | #[test] 642 | fn returns_already_completed_output() { 643 | start(|| { 644 | let handle = spawn(|| 123); 645 | 646 | yield_now(); 647 | // TODO: assert!(handle.is_completed()); 648 | let output = handle.join(); 649 | 650 | assert_eq!(output.unwrap(), 123); 651 | }) 652 | .unwrap(); 653 | } 654 | 655 | #[test] 656 | fn catches_panic() { 657 | start(|| { 658 | let result = spawn(|| panic!()).join(); 659 | 660 | assert!(result.is_err()); 661 | }) 662 | .unwrap(); 663 | } 664 | 665 | #[test] 666 | fn waits_for_dropped_child() { 667 | start(|| { 668 | let handle = spawn(|| { 669 | let handle = spawn(|| crate::time::sleep(Duration::from_millis(5))); 670 | drop(handle); 671 | }); 672 | 673 | let before = Instant::now(); 674 | handle.join().unwrap(); 675 | 676 | assert!(before.elapsed() > Duration::from_millis(5)); 677 | }) 678 | .unwrap(); 679 | } 680 | 681 | #[test] 682 | fn waits_for_forgotten_child() { 683 | start(|| { 684 | let handle = spawn(|| { 685 | let handle = spawn(|| crate::time::sleep(Duration::from_millis(5))); 686 | mem::forget(handle); 687 | }); 688 | 689 | let before = Instant::now(); 690 | handle.join().unwrap(); 691 | 692 | assert!(before.elapsed() > Duration::from_millis(5)); 693 | }) 694 | .unwrap(); 695 | } 696 | 697 | #[test] 698 | #[ignore] 699 | fn joined_child_reuses_stack() { 700 | start(|| { 701 | // enough to hit OS limits 702 | for _ in 0..1_000_000 { 703 | spawn(|| {}).join().unwrap(); 704 | } 705 | }) 706 | .unwrap(); 707 | } 708 | 709 | #[test] 710 | #[ignore] 711 | fn dropped_child_reuses_stack() { 712 | start(|| { 713 | // enough to hit OS limits 714 | for _ in 0..1_000_000 { 715 | let handle = spawn(|| {}); 716 | drop(handle); 717 | yield_now(); 718 | } 719 | }) 720 | .unwrap(); 721 | } 722 | 723 | #[test] 724 | #[should_panic] 725 | #[ignore] 726 | fn forgotten_child_cant_reuse_stack() { 727 | start(|| { 728 | // enough to hit OS limits 729 | for _ in 0..1_000_000 { 730 | let handle = spawn(|| {}); 731 | mem::forget(handle); // memory leak 732 | yield_now(); 733 | } 734 | }) 735 | .unwrap(); 736 | } 737 | 738 | #[test] 739 | #[ignore] 740 | fn joined_child_cleans_up_after_itself() { 741 | // enough to hit OS limits 742 | for _ in 0..1_000_000 { 743 | start(|| { 744 | spawn(|| {}).join().unwrap(); 745 | }) 746 | .unwrap(); 747 | } 748 | } 749 | 750 | #[test] 751 | #[ignore] 752 | fn dropped_child_cleans_up_after_itself() { 753 | // enough to hit OS limits 754 | for _ in 0..1_000_000 { 755 | start(|| { 756 | let handle = spawn(|| {}); 757 | drop(handle); 758 | }) 759 | .unwrap(); 760 | } 761 | } 762 | 763 | #[test] 764 | #[should_panic] 765 | #[ignore] 766 | fn forgotten_child_cant_clean_up_after_itself() { 767 | // enough to hit OS limits 768 | for _ in 0..1_000_000 { 769 | start(|| { 770 | let handle = spawn(|| {}); 771 | mem::forget(handle); // memory leak 772 | }) 773 | .unwrap(); 774 | } 775 | } 776 | 777 | mod cancellation { 778 | use super::*; 779 | 780 | #[test] 781 | fn child_initially_not_cancelled() { 782 | start(|| { 783 | let handle = spawn(|| assert!(!is_cancelled())); 784 | 785 | handle.join().unwrap(); 786 | }) 787 | .unwrap(); 788 | } 789 | 790 | #[test] 791 | fn child_starts_cancelled_if_parent_cancelled() { 792 | start(|| { 793 | cancel(); 794 | 795 | let handle = spawn(|| assert!(is_cancelled())); 796 | 797 | handle.join().unwrap(); 798 | }) 799 | .unwrap(); 800 | } 801 | 802 | #[test] 803 | fn child_cancelled_after_cancelling_self() { 804 | start(|| { 805 | let handle = spawn(|| { 806 | cancel(); 807 | assert!(is_cancelled()); 808 | }); 809 | 810 | handle.join().unwrap(); 811 | assert!(!is_cancelled()); 812 | }) 813 | .unwrap(); 814 | } 815 | 816 | #[test] 817 | fn parent_propagates_cancel_to_children() { 818 | start(|| { 819 | let handle = spawn(|| assert!(is_cancelled())); 820 | 821 | cancel(); 822 | 823 | handle.join().unwrap(); 824 | }) 825 | .unwrap(); 826 | } 827 | 828 | #[test] 829 | fn child_cancelled_after_cancelling_handle() { 830 | start(|| { 831 | let handle = spawn(|| assert!(is_cancelled())); 832 | 833 | handle.cancel(); 834 | 835 | handle.join().unwrap(); 836 | assert!(!is_cancelled()); 837 | }) 838 | .unwrap(); 839 | } 840 | 841 | #[test] 842 | fn cancelled_after_child_cancel_propagating_self() { 843 | start(|| { 844 | let handle = spawn(|| { 845 | cancel_propagating(); 846 | assert!(is_cancelled()); 847 | }); 848 | 849 | handle.join().unwrap(); 850 | assert!(is_cancelled()); 851 | }) 852 | .unwrap(); 853 | } 854 | 855 | #[test] 856 | fn cancelled_after_cancel_propagating_handle() { 857 | start(|| { 858 | let handle = spawn(|| assert!(is_cancelled())); 859 | 860 | handle.cancel_propagating(); 861 | 862 | handle.join().unwrap(); 863 | assert!(is_cancelled()); 864 | }) 865 | .unwrap(); 866 | } 867 | 868 | #[test] 869 | fn not_cancelled_after_joined_child_panic() { 870 | start(|| { 871 | let handle = spawn(|| panic!()); 872 | 873 | let _ = handle.join(); 874 | 875 | assert!(!is_cancelled()); 876 | }) 877 | .unwrap(); 878 | } 879 | 880 | #[test] 881 | fn cancelled_after_dropped_child_panic() { 882 | start(|| { 883 | let handle = spawn(|| panic!()); 884 | drop(handle); 885 | 886 | yield_now(); 887 | 888 | assert!(is_cancelled()); 889 | }) 890 | .unwrap(); 891 | } 892 | 893 | #[test] 894 | fn cancelled_after_forgotten_child_panic() { 895 | start(|| { 896 | let handle = spawn(|| panic!()); 897 | mem::forget(handle); 898 | 899 | yield_now(); 900 | 901 | assert!(is_cancelled()); 902 | }) 903 | .unwrap(); 904 | } 905 | } 906 | 907 | mod syscall { 908 | use super::*; 909 | 910 | #[test] 911 | fn executes_syscalls_in_start() { 912 | start(|| { 913 | nop().unwrap(); 914 | nop().unwrap(); 915 | }) 916 | .unwrap(); 917 | } 918 | 919 | #[test] 920 | fn executes_syscalls_in_spawn() { 921 | start(|| { 922 | spawn(|| { 923 | nop().unwrap(); 924 | nop().unwrap(); 925 | }) 926 | .join() 927 | .unwrap(); 928 | }) 929 | .unwrap(); 930 | } 931 | 932 | fn nop() -> crate::CancellableResult<()> { 933 | let sqe = io_uring::opcode::Nop::new().build(); 934 | let result = syscall(sqe).map_err(|cancellable| cancellable.map(|_| ()))?; 935 | assert_eq!(result, 0); 936 | 937 | Ok(()) 938 | } 939 | 940 | mod cancellation { 941 | use super::*; 942 | 943 | #[test] 944 | fn tries_to_stop_active_syscall() { 945 | start(|| { 946 | let handle = spawn(|| crate::time::sleep(Duration::from_millis(5))); 947 | yield_now(); 948 | 949 | handle.cancel(); 950 | let before = Instant::now(); 951 | let result = handle.join().unwrap(); 952 | 953 | assert_eq!(result, Err(crate::Error::Cancelled)); 954 | assert!(before.elapsed() < Duration::from_millis(5)); 955 | }) 956 | .unwrap(); 957 | } 958 | 959 | #[test] 960 | fn immediately_fails_new_syscall() { 961 | start(|| { 962 | cancel(); 963 | 964 | let before = Instant::now(); 965 | let result = crate::time::sleep(Duration::from_millis(5)); 966 | 967 | assert_eq!(result, Err(crate::Error::Cancelled)); 968 | assert!(before.elapsed() < Duration::from_millis(5)); 969 | }) 970 | .unwrap(); 971 | } 972 | } 973 | } 974 | } 975 | 976 | mod yield_now { 977 | use std::cell::RefCell; 978 | use std::rc::Rc; 979 | 980 | use super::*; 981 | 982 | #[test] 983 | fn to_same_fiber() { 984 | start(|| { 985 | yield_now(); 986 | }) 987 | .unwrap(); 988 | } 989 | 990 | #[test] 991 | fn to_other_fiber() { 992 | start(|| { 993 | let changed = Rc::new(RefCell::new(false)); 994 | 995 | spawn({ 996 | let changed = changed.clone(); 997 | move || *changed.borrow_mut() = true 998 | }); 999 | 1000 | assert!(!*changed.borrow()); 1001 | yield_now(); 1002 | 1003 | assert!(*changed.borrow()); 1004 | }) 1005 | .unwrap(); 1006 | } 1007 | } 1008 | } 1009 | --------------------------------------------------------------------------------