diff --git a/.gitignore b/.gitignore index 43f4569..5ae34c8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,51 @@ -/target -.build +# Rust build output +/target/ +**/target/ + +# Cargo +# keep Cargo.lock for binaries (you are building executables, so keep it) +# If this were a library, you might ignore it. +# /Cargo.lock + +# Backup / temp files +*~ +*.swp +*.swo +*.tmp + +# macOS / Linux junk +.DS_Store +Thumbs.db + +# VSCode / editors +.vscode/ +.idea/ + +# Logs +*.log + +# Binary outputs / test artifacts +*.bin +*.out +out.bin +[out.bin] + +# Your specific generated files +/latency/out* +/throughput/out* + +# Python cache (for your bench script) +__pycache__/ +*.pyc + +# Coverage / profiling +*.prof +*.data + +# OS stuff +*.pid +*.seed + +# Misc +*.bak + diff --git a/Cargo.lock b/Cargo.lock index 8f973f0..8db4a02 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,363 +3,29 @@ version = 4 [[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - -[[package]] -name = "anstream" -version = "0.6.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" - -[[package]] -name = "anstyle-parse" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys 0.61.2", -] - -[[package]] -name = "anyhow" -version = "1.0.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" - -[[package]] -name = "ar_archive_writer" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c269894b6fe5e9d7ada0cf69b5bf847ff35bc25fc271f08e1d080fce80339a" -dependencies = [ - "object", -] - -[[package]] -name = "arca" +name = "arca-control" version = "0.1.0" dependencies = [ - "log", - "serde", + "arca-pipe", ] [[package]] -name = "arcane" +name = "arca-monitor" version = "0.1.0" dependencies = [ - "autotools", - "bindgen", - "derive_more", -] - -[[package]] -name = "async-channel" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" -dependencies = [ - "concurrent-queue", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-executor" -version = "1.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" -dependencies = [ - "async-task", - "concurrent-queue", - "fastrand", - "futures-lite", - "pin-project-lite", - "slab", -] - -[[package]] -name = "async-fs" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" -dependencies = [ - "async-lock", - "blocking", - "futures-lite", -] - -[[package]] -name = "async-io" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" -dependencies = [ - "autocfg", - "cfg-if", - "concurrent-queue", - "futures-io", - "futures-lite", - "parking", - "polling", - "rustix", - "slab", - "windows-sys 0.61.2", -] - -[[package]] -name = "async-lock" -version = "3.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" -dependencies = [ - "event-listener", - "event-listener-strategy", - "pin-project-lite", -] - -[[package]] -name = "async-net" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" -dependencies = [ - "async-io", - "blocking", - "futures-lite", -] - -[[package]] -name = "async-process" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" -dependencies = [ - "async-channel", - "async-io", - "async-lock", - "async-signal", - "async-task", - "blocking", - "cfg-if", - "event-listener", - "futures-lite", - "rustix", -] - -[[package]] -name = "async-signal" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" -dependencies = [ - "async-io", - "async-lock", - "atomic-waker", - "cfg-if", - "futures-core", - "futures-io", - "rustix", - "signal-hook-registry", - "slab", - "windows-sys 0.61.2", -] - -[[package]] -name = "async-task" -version = "4.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" - -[[package]] -name = "atomic-polyfill" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" -dependencies = [ - "critical-section", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "autotools" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef941527c41b0fc0dd48511a8154cd5fc7e29200a0ff8b7203c5d777dbc795cf" -dependencies = [ - "cc", -] - -[[package]] -name = "bindgen" -version = "0.72.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" -dependencies = [ - "bitflags 2.10.0", - "cexpr", - "clang-sys", - "itertools", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn", -] - -[[package]] -name = "bitfield-struct" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c2ce686adbebce0ee484a502c440b4657739adbad65eadf06d64f5816ee9765" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "bitfield-struct" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3ca019570363e800b05ad4fd890734f28ac7b72f563ad8a35079efb793616f8" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "arca-control", + "arca-pipe", ] [[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +name = "arca-pipe" +version = "0.1.0" [[package]] name = "bitflags" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" - -[[package]] -name = "block2" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" -dependencies = [ - "objc2", -] - -[[package]] -name = "blocking" -version = "1.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" -dependencies = [ - "async-channel", - "async-task", - "futures-io", - "futures-lite", - "piper", -] - -[[package]] -name = "bytemuck" -version = "1.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" - -[[package]] -name = "cc" -version = "1.2.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cexpr" -version = "0.6.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "cfg-if" @@ -374,1906 +40,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] -name = "chrono" -version = "0.4.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" -dependencies = [ - "num-traits", - "serde", -] - -[[package]] -name = "chumsky" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14377e276b2c8300513dff55ba4cc4142b44e5d6de6d00eb5b2307d650bb4ec1" -dependencies = [ - "hashbrown 0.15.5", - "regex-automata 0.3.9", - "serde", - "stacker", - "unicode-ident", - "unicode-segmentation", -] - -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - -[[package]] -name = "clap" -version = "4.5.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.5.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.5.49" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "clap_lex" -version = "0.7.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" - -[[package]] -name = "cmake" -version = "0.1.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" -dependencies = [ - "cc", -] - -[[package]] -name = "cobs" -version = "0.3.0" +name = "libc" +version = "0.2.185" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" -dependencies = [ - "thiserror", -] +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" [[package]] -name = "colorchoice" -version = "1.0.4" +name = "memmap2" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" - -[[package]] -name = "common" -version = "0.1.0" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" dependencies = [ - "arca", - "async-channel", - "async-lock", - "derive_more", - "elf", - "hashbrown 0.15.5", "libc", - "log", - "macros", - "nix", - "rand", - "snafu", -] - -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", ] [[package]] -name = "convert_case" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "core_affinity" -version = "0.8.3" +name = "nix" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a034b3a7b624016c6e13f5df875747cc25f884156aad2abd12b6c46797971342" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", "libc", - "num_cpus", - "winapi", ] [[package]] -name = "critical-section" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "ctrlc" -version = "3.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73736a89c4aff73035ba2ed2e565061954da00d4970fc9ac25dcc85a2a20d790" +name = "pipe-test" +version = "0.1.0" dependencies = [ - "dispatch2", + "arca-pipe", + "memmap2", "nix", - "windows-sys 0.61.2", -] - -[[package]] -name = "deranged" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" -dependencies = [ - "powerfmt", -] - -[[package]] -name = "derive_more" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn", - "unicode-xid", -] - -[[package]] -name = "dispatch2" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" -dependencies = [ - "bitflags 2.10.0", - "block2", - "libc", - "objc2", -] - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "elevate" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8b9e2c51eb43c3abc3a7408dc7274d0e09a778eb6c11053fed2841bcff547c4" -dependencies = [ - "libc", - "log", -] - -[[package]] -name = "elf" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4445909572dbd556c457c849c4ca58623d84b27c8fff1e74b0b4227d8b90d17b" - -[[package]] -name = "embedded-io" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" - -[[package]] -name = "embedded-io" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" - -[[package]] -name = "env_filter" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" -dependencies = [ - "log", - "regex", -] - -[[package]] -name = "env_logger" -version = "0.11.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" -dependencies = [ - "anstream", - "anstyle", - "env_filter", - "jiff", - "log", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "event-listener" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" -dependencies = [ - "event-listener", - "pin-project-lite", -] - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "fix" -version = "0.1.0" -dependencies = [ - "anyhow", - "arca", - "arcane", - "async-lock", - "autotools", - "bindgen", - "bitfield-struct 0.11.0", - "bytemuck", - "cc", - "chrono", - "cmake", - "common", - "derive_more", - "fixhandle", - "fixruntime", - "fixshell", - "futures", - "include_directory", - "kernel", - "log", - "macros", - "postcard", - "serde", - "serde_bytes", - "trait-variant", - "user", -] - -[[package]] -name = "fixhandle" -version = "0.1.0" -dependencies = [ - "common", - "derive_more", -] - -[[package]] -name = "fixruntime" -version = "0.1.0" -dependencies = [ - "arca", - "arcane", - "async-lock", - "bitfield-struct 0.11.0", - "bytemuck", - "chrono", - "chumsky", - "common", - "derive_more", - "fixhandle", - "futures", - "kernel", - "log", - "macros", - "postcard", - "serde", - "serde_bytes", - "trait-variant", - "user", -] - -[[package]] -name = "fixshell" -version = "0.1.0" -dependencies = [ - "anyhow", - "arca", - "arcane", - "bindgen", - "cc", - "fixhandle", - "user", -] - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-lite" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", -] - -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - -[[package]] -name = "hash32" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" -dependencies = [ - "byteorder", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash", - "rustc-std-workspace-alloc", - "rustc-std-workspace-core", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" - -[[package]] -name = "heapless" -version = "0.7.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" -dependencies = [ - "atomic-polyfill", - "hash32", - "rustc_version", - "serde", - "spin 0.9.8", - "stable_deref_trait", -] - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - -[[package]] -name = "include_directory" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc51bf21d9c8c76d0d55b3926add7fde9b595719ee0d5710d46f8ee66131cca9" -dependencies = [ - "include_directory_macros", - "mime", -] - -[[package]] -name = "include_directory_macros" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35011b5de7391d94ea631aa584d09a88b2e877505a614e4952970214f2fd1b90" -dependencies = [ - "mime", - "new_mime_guess", - "proc-macro2", - "quote", -] - -[[package]] -name = "indexmap" -version = "2.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" -dependencies = [ - "equivalent", - "hashbrown 0.16.1", -] - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - -[[package]] -name = "jiff" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67e8da4c49d6d9909fe03361f9b620f58898859f5c7aded68351e85e71ecf50" -dependencies = [ - "jiff-static", - "log", - "portable-atomic", - "portable-atomic-util", - "serde_core", -] - -[[package]] -name = "jiff-static" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c84ee7f197eca9a86c6fd6cb771e55eb991632f15f2bc3ca6ec838929e6e78" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "kernel" -version = "0.1.0" -dependencies = [ - "anyhow", - "arca", - "arcane", - "async-lock", - "bitfield-struct 0.7.0", - "cc", - "cfg-if", - "common", - "glob", - "hashbrown 0.15.5", - "log", - "macros", - "postcard", - "serde", - "spin 0.10.0", - "talc", - "time", -] - -[[package]] -name = "kvm-bindings" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf3432d9f609fbede9f624d1dbefcce77985a9322de1d0e6d460ec05502b7fd0" -dependencies = [ - "vmm-sys-util", -] - -[[package]] -name = "kvm-ioctls" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e00243d27a20feb05cf001ae52ddc79831ac70c020f215ba1153ff9270b650a" -dependencies = [ - "bitflags 2.10.0", - "kvm-bindings", - "libc", - "vmm-sys-util", -] - -[[package]] -name = "libc" -version = "0.2.180" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" - -[[package]] -name = "libloading" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" -dependencies = [ - "cfg-if", - "windows-link 0.2.1", -] - -[[package]] -name = "linux-raw-sys" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "macros" -version = "0.1.0" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "memchr" -version = "2.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" - -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "mio" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.61.2", -] - -[[package]] -name = "new_mime_guess" -version = "4.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02a2dfb3559d53e90b709376af1c379462f7fb3085a0177deb73e6ea0d99eff4" -dependencies = [ - "mime", - "unicase", -] - -[[package]] -name = "nix" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" -dependencies = [ - "bitflags 2.10.0", - "cfg-if", - "cfg_aliases", - "libc", - "memoffset", -] - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_cpus" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" -dependencies = [ - "hermit-abi", - "libc", -] - -[[package]] -name = "numtoa" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a6da8d50bd4f4b2e9788c44714a3fa4e465d33fd6a6ad70991db6eb30807dca" - -[[package]] -name = "objc2" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" -dependencies = [ - "objc2-encode", -] - -[[package]] -name = "objc2-encode" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" - -[[package]] -name = "object" -version = "0.32.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - -[[package]] -name = "parking" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "piper" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" -dependencies = [ - "atomic-waker", - "fastrand", - "futures-io", -] - -[[package]] -name = "polling" -version = "3.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" -dependencies = [ - "cfg-if", - "concurrent-queue", - "hermit-abi", - "pin-project-lite", - "rustix", - "windows-sys 0.61.2", -] - -[[package]] -name = "portable-atomic" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" - -[[package]] -name = "portable-atomic-util" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" -dependencies = [ - "portable-atomic", -] - -[[package]] -name = "postcard" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" -dependencies = [ - "cobs", - "embedded-io 0.4.0", - "embedded-io 0.6.1", - "heapless", - "serde", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn", -] - -[[package]] -name = "proc-macro-crate" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" -dependencies = [ - "toml_edit", -] - -[[package]] -name = "proc-macro2" -version = "1.0.105" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "psm" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d11f2fedc3b7dafdc2851bc52f277377c5473d378859be234bc7ebb593144d01" -dependencies = [ - "ar_archive_writer", - "cc", -] - -[[package]] -name = "quote" -version = "1.0.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "rand" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom", -] - -[[package]] -name = "regex" -version = "1.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata 0.4.13", - "regex-syntax 0.8.8", -] - -[[package]] -name = "regex-automata" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax 0.7.5", -] - -[[package]] -name = "regex-automata" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax 0.8.8", -] - -[[package]] -name = "regex-syntax" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" - -[[package]] -name = "regex-syntax" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" - -[[package]] -name = "rlimit" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7043b63bd0cd1aaa628e476b80e6d4023a3b50eb32789f2728908107bd0c793a" -dependencies = [ - "libc", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" - -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - -[[package]] -name = "rustc-std-workspace-alloc" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d441c3b2ebf55cebf796bfdc265d67fa09db17b7bb6bd4be75c509e1e8fec3" - -[[package]] -name = "rustc-std-workspace-core" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa9c45b374136f52f2d6311062c7146bff20fec063c3f5d46a410bd937746955" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" -dependencies = [ - "bitflags 2.10.0", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_bytes" -version = "0.11.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" -dependencies = [ - "serde", - "serde_core", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "signal-hook-registry" -version = "1.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" -dependencies = [ - "errno", - "libc", -] - -[[package]] -name = "slab" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" - -[[package]] -name = "smol" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a33bd3e260892199c3ccfc487c88b2da2265080acb316cd920da72fdfd7c599f" -dependencies = [ - "async-channel", - "async-executor", - "async-fs", - "async-io", - "async-lock", - "async-net", - "async-process", - "blocking", - "futures-lite", -] - -[[package]] -name = "snafu" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e84b3f4eacbf3a1ce05eac6763b4d629d60cbc94d632e4092c54ade71f1e1a2" -dependencies = [ - "snafu-derive", -] - -[[package]] -name = "snafu-derive" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1c97747dbf44bb1ca44a561ece23508e99cb592e862f22222dcf42f51d1e451" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "socket2" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" -dependencies = [ - "libc", - "windows-sys 0.60.2", -] - -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" -dependencies = [ - "lock_api", -] - -[[package]] -name = "spin" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" -dependencies = [ - "lock_api", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "stacker" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1f8b29fb42aafcea4edeeb6b2f2d7ecd0d969c48b4cf0d2e64aafc471dd6e59" -dependencies = [ - "cc", - "cfg-if", - "libc", - "psm", - "windows-sys 0.59.0", -] - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "syn" -version = "2.0.114" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "talc" -version = "4.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3ae828aa394de34c7de08f522d1b86bd1c182c668d27da69caadda00590f26d" -dependencies = [ - "lock_api", -] - -[[package]] -name = "thiserror" -version = "2.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thread-priority" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2210811179577da3d54eb69ab0b50490ee40491a25d95b8c6011ba40771cb721" -dependencies = [ - "bitflags 2.10.0", - "cfg-if", - "libc", - "log", - "rustversion", - "windows", -] - -[[package]] -name = "time" -version = "0.3.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" -dependencies = [ - "deranged", - "num-conv", - "powerfmt", - "serde_core", - "time-core", -] - -[[package]] -name = "time-core" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca" - -[[package]] -name = "tokio" -version = "1.49.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" -dependencies = [ - "bytes", - "libc", - "mio", - "pin-project-lite", - "signal-hook-registry", - "socket2", - "windows-sys 0.61.2", -] - -[[package]] -name = "tokio-stream" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", - "tokio-util", -] - -[[package]] -name = "tokio-util" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "toml_datetime" -version = "0.7.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.23.10+spec-1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" -dependencies = [ - "indexmap", - "toml_datetime", - "toml_parser", - "winnow", -] - -[[package]] -name = "toml_parser" -version = "1.0.6+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" -dependencies = [ - "winnow", -] - -[[package]] -name = "trait-variant" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70977707304198400eb4835a78f6a9f928bf41bba420deb8fdb175cd965d77a7" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "unicase" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" - -[[package]] -name = "unicode-ident" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" - -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "user" -version = "0.1.0" -dependencies = [ - "arca", - "arcane", - "autotools", - "cc", - "numtoa", - "spin 0.10.0", - "talc", -] - -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "vmm" -version = "0.1.0" -dependencies = [ - "anyhow", - "arca", - "bindgen", - "chumsky", - "clap", - "common", - "core_affinity", - "ctrlc", - "either", - "elevate", - "elf", - "env_logger", - "futures", - "kvm-bindings", - "kvm-ioctls", - "libc", - "log", - "nix", - "regex", - "rlimit", - "rustc-demangle", - "smol", - "thread-priority", - "tokio-stream", - "vmm-sys-util", - "vsock", -] - -[[package]] -name = "vmm-sys-util" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d21f366bf22bfba3e868349978766a965cbe628c323d58e026be80b8357ab789" -dependencies = [ - "bitflags 1.3.2", - "libc", -] - -[[package]] -name = "vsock" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2da6e4ac76cd19635dce0f98985378bb62f8044ee2ff80abd2a7334b920ed63" -dependencies = [ - "libc", - "nix", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.2+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows" -version = "0.61.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" -dependencies = [ - "windows-collections", - "windows-core", - "windows-future", - "windows-link 0.1.3", - "windows-numerics", -] - -[[package]] -name = "windows-collections" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" -dependencies = [ - "windows-core", -] - -[[package]] -name = "windows-core" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link 0.1.3", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-future" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" -dependencies = [ - "windows-core", - "windows-link 0.1.3", - "windows-threading", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-numerics" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" -dependencies = [ - "windows-core", - "windows-link 0.1.3", -] - -[[package]] -name = "windows-result" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-strings" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link 0.2.1", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - -[[package]] -name = "windows-threading" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - -[[package]] -name = "winnow" -version = "0.7.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" -dependencies = [ - "memchr", -] - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" - -[[package]] -name = "zerocopy" -version = "0.8.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71ddd76bcebeed25db614f82bf31a9f4222d3fbba300e6fb6c00afa26cbd4d9d" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8187381b52e32220d50b255276aa16a084ec0a9017a0ca2152a1f55c539758d" -dependencies = [ - "proc-macro2", - "quote", - "syn", ] diff --git a/Cargo.toml b/Cargo.toml index 993a3df..5a3aa77 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,9 @@ [workspace] -members = [ "common", "vmm", "kernel", "macros" , "user", "arca" , "arcane", "fix", "fix/runtime", "fix/handle", "fix/shell" ] -default-members = [ "common", "vmm", "macros", "arca", "fix/handle" ] - resolver = "2" - -[profile.release] -debug = true +members = [ + "pipe", + "control", + "monitor", + "data-pipe", + "test", +] diff --git a/DATAPIPE.md b/DATAPIPE.md new file mode 100644 index 0000000..f83e86f --- /dev/null +++ b/DATAPIPE.md @@ -0,0 +1,168 @@ +# Arca Data Pipe Protocol + +This doc specifies the **data pipe**: how bytes move between Arca and +the Linux monitor for an established connection. The control protocol +(`PROTOCOL.md`) handles session setup and hands off a `DataPipeInfo` +(SHM handle + ring size); this doc covers everything after that handoff. + +--- + +## 1. Overview + +Each connection gets its own **bidirectional pipe**, a pair of +single-producer/single-consumer (SPSC) ring buffers in a shared-memory +region. One ring carries bytes Arca→Linux (outgoing), the other Linux→Arca +(incoming). The monitor relays bytes between the ring and the kernel +`TcpStream`; Arca reads and writes through `SyncStream` / `AsyncStream`. + +``` + Arca shared memory Monitor + ──── ───────────── ─────── + SyncStream ──write──► Ring A (A→B) ──read──► pipe_to_tcp → TcpStream + SyncStream ◄──read── Ring B (B→A) ◄──write── tcp_to_pipe ← TcpStream +``` + +--- + +## 2. Ring layer (`arca-pipe`) + +### 2.1 Ring buffer + +Each ring is a fixed-capacity byte buffer in shared memory with a header +and a data region: + +``` +[RingHeader: 24 bytes][Data: ring_size bytes] +``` + +`RingHeader` (stored in shared memory, all fields atomic): + +``` +read_cursor: AtomicU64 — monotonically increasing logical read position +write_cursor: AtomicU64 — monotonically increasing logical write position +writer_closed: AtomicBool — set by the producer when it will write no more +reader_closed: AtomicBool — set by the consumer when it will read no more +``` + +Physical position is `cursor % ring_size`. Cursors never reset; wrapping +arithmetic handles overflow. + +### 2.2 Read and write + +Both operations are **non-blocking**, return immediately if the ring +is full or empty. + +**`RingProducer::write(buf)`** +- Writes `min(buf.len(), free_space)` bytes into the ring. +- Returns the number of bytes actually written OR returns `WouldBlock` + if the ring is full (`free_space == 0`). + +**`RingConsumer::read(buf)`** +- Reads `min(buf.len(), used_space)` bytes from the ring. +- Returns the number of bytes actually read OR returns `WouldBlock` if the + ring is empty (`used_space == 0`). + +Callers that need blocking behavior loop on `WouldBlock` (see §3). + +### 2.3 Close flags + +Each ring has two close flags in its header, set independently by each end: + +| Flag | Set by | Meaning | +| --------------- | -------- | -------------------------------------------- | +| `writer_closed` | Producer | No more bytes will be written to this ring. | +| `reader_closed` | Consumer | No more bytes will be read from this ring. | + +A ring is **closed** when both flags are set. A `BidirectionalPipe` is +**fully closed** when both of its rings are closed (all four flags set). + +### 2.4 `BidirectionalPipe` layout and API + +Total shared-memory size for one pipe: + +``` +required_size(ring_size) = 2 × (24 + ring_size) bytes +``` + +Layout: `[HeaderA][DataA: ring_size][HeaderB][DataB: ring_size]` + +Side A's producer writes to Ring A; Side B's consumer reads from Ring A, +and vice versa for Ring B. + +**Close API on `BidirectionalPipe`:** + +| Method | What it does | +| ------------------------ | ---------------------------------------------------------- | +| `close_write()` | Sets `writer_closed` on the outgoing ring. | +| `close_read()` | Sets `reader_closed` on the incoming ring. | +| `is_peer_write_closed()` | Reads `writer_closed` on the incoming ring (peer set it). | +| `is_peer_read_closed()` | Reads `reader_closed` on the outgoing ring (peer set it). | +| `is_closed()` | True when all four flags across both rings are set. | + +--- + +## 3. Stream layer (`data-pipe`) + +`SyncStream` (blocking) and `AsyncStream` (async/await) wrap a +`BidirectionalPipe` and provide `send` / `recv` with automatic close +propagation. + +### 3.1 Internal helpers + +**`Write::write_all(src)`** — default method on the `Write` trait (`pipe/src/traits.rs`) +Writes every byte of `src` into the pipe. Loops on `WouldBlock` (ring full) +with `spin_loop()` until all bytes are written. Used by `SyncStream::send`. + +**`async write_all(pipe, src)`** — internal to `async_stream.rs` +Same contract as above but calls `yield_now().await` on `WouldBlock` instead +of spinning, so the async executor can make progress on other tasks. + +**`read_exact(pipe, buf)`** +Fills `buf` completely by looping on `WouldBlock` (ring empty). Breaks +early only when the ring is empty **and** `is_peer_write_closed()` is true (EOF). Returns the number of bytes read; `n < buf.len()` means EOF was reached before the buffer was full. + +### 3.2 `send(buf) → Result` + +1. Check `is_peer_read_closed()`, if true, call `close_write()` and return + `Err(WriteClosed)`. The peer stopped reading; writing is futile. +2. Call `write_all` to write every byte of `buf`. +3. Return `Ok(buf.len())`. + +### 3.3 `recv(buf) → Result` + +1. Call `read_exact` to fill `buf`. +2. If `n < buf.len()` (EOF), call `close_read()`. +3. Return `Ok(n)`. Caller detects EOF when `n < buf.len()` or `n == 0`. + +### 3.4 Close methods + +| Method | What it does | +| --------------- | ---------------------------------------- | +| `close_write()` | Sets `writer_closed` on outgoing ring. | +| `close_read()` | Sets `reader_closed` on incoming ring. | +| `is_closed()` | Delegates to `BidirectionalPipe::is_closed()`. | + +### 3.5 Close propagation (involuntary) + +- **During `send`:** if `is_peer_read_closed()` → close writer automatically. +- **During `recv`:** if ring empty and `is_peer_write_closed()` → close reader automatically. + +--- + +## 4. Where the code lives + +``` +arca-networking/ +├── pipe/ # ring buffers, BidirectionalPipe +│ └── src/ +│ ├── traits.rs # Read, Write traits; Write::write_all default +│ ├── ring.rs # RingHeader (cursors + close flags), RingData +│ ├── ring_producer.rs# RingProducer — write, close_writer, is_reader_closed +│ ├── ring_consumer.rs# RingConsumer — read, close_reader, is_writer_closed +│ └── bidirectional_pipe.rs # BidirectionalPipe — close_write/read, is_closed +├── data-pipe/ # SyncStream, AsyncStream +│ └── src/ +│ ├── sync_stream.rs # SyncStream — send, recv, close_write, close_read +│ └── async_stream.rs # AsyncStream — async equivalents +└── DATAPIPE.md +``` diff --git a/PROTOCOL.md b/PROTOCOL.md new file mode 100644 index 0000000..42af673 --- /dev/null +++ b/PROTOCOL.md @@ -0,0 +1,536 @@ +# Arca ↔ Linux Control Protocol + +This doc specifies the **control protocol**: framed messages on one +**control pipe** between Arca and a **monitor** in Linux so Arca can call +`connect`, `listen`, and `accept` without implementing TCP. It defines the +wire format, message types, identifiers, data-pipe lifecycle at session +setup, and how `arca-control` (Arca) and `arca-monitor` (Linux) implement +the exchange. + +All Linux→Arca control traffic is **replies**: each frame echoes Arca’s +`request_id`. Bind/listen uses `ListenRequest` and `ListenOk` or `ListenErr`. +Outbound connect uses `ConnectRequest` and `ConnectOk` or `ConnectErr`; the +monitor waits on `TcpStream::connect` until the handshake completes. +Inbound connections use `AcceptRequest` (message type 8): Arca waits for +`IncomingConnection` (success) or `AcceptErr` (unknown listener or kernel +`accept` failed), each sent **only** in reply to that request with the +same `request_id`. The monitor keeps a pending-accept queue per +listener, calls kernel `accept` only when a wait exists, and drives the pipe +with `poll_accepts`, `pump_once`, `serve_one`, and `FrameReadBuf` when the +transport is non-blocking. `ArcaSession` matches replies by `request_id` +only (no secondary stash for out-of-order events). + +Related pieces in the stack: + +| Piece | Crate | Role | +| ------------------- | ------------------------------------------- | -------------------------------------------------------------------- | +| Bidirectional pipe | `arca-pipe` | Shared-memory rings; `Read` + `Write` byte streams. | +| Control protocol | `arca-control`, `arca-monitor` *(this doc)* | Framed messages on one control pipe for listen, connect, accept. | +| Data path | *(not this doc)* | Per-session byte stream on a bidirectional pipe; monitor relays I/O. | + +--- + +## 1. Mental model + +``` + ┌────────────────────────────────────┐ + │ Linux user-space │ + ┌────────────┐ │ │ + │ │control │ Monitor (arca-monitor) │ + │ ├───────►│ owns listeners / TCP streams │ + │ Arca │◄──────┤ │ + │ (no_std) │ reply │ ↕ │ + │ │ data │ Linux kernel networking │ + └────────────┘ └────────────────────────────────────┘ +``` + +- **Arca never speaks TCP.** It calls `bind` / `connect` / `accept` on the + control pipe, and Linux runs the real socket calls. +- **Linux owns sockets.** Whatever leaves the NIC is plain kernel TCP. +- **One control pipe, many data pipes.** The control pipe is statically + allocated (one per Arca instance). Each accepted/connected session gets + its **own** bidirectional data pipe, allocated dynamically by the monitor. + +--- + +## 2. Pipes and shared memory + +Both control and data pipes are instances of `arca_pipe::BidirectionalPipe`, +which is two single-producer/single-consumer rings packed into one shared +memory region. The pipe layer is a raw byte stream — **no framing**. Higher +layers add their own framing. + +| Pipe | Lifetime | Created by | Carries | +| -------------- | ------------------------------ | ------------------------ | ----------------------------------------- | +| Control pipe | Static (1 per Arca instance) | Bootstrap (out of scope) | Framed control messages (this doc). | +| Data pipe | Dynamic (1 per session) | Monitor, on demand | Raw application bytes for one TCP session. | + +The control pipe is assumed to exist before any frame on it is sent — both +Arca and the monitor know about its SHM region at boot. *Data* pipes are +the ones the protocol actually creates and tears down at runtime, so they +deserve a precise lifecycle. + +### Data-pipe lifecycle + +A new data pipe is needed at exactly **two moments** — and these are +exactly the two moments the monitor is about to send a `ConnectionReady` +payload: + +- *Outbound*: a `ConnectRequest` succeeded. The kernel handshake is done + and the monitor is about to reply with `ConnectOk`. +- *Inbound*: Arca has sent `AcceptRequest` for a listener, the monitor has + queued that wait, and a non-blocking kernel `accept` on that listener + returned a fresh socket. The monitor is about to reply with + `IncomingConnection` carrying the same `request_id` as that + `AcceptRequest`. + +In both cases the lifecycle is the same five steps: + +``` +1. Trigger Monitor decides a new session needs a pipe (one of the two + moments above). + +2. Allocate Monitor: + • picks a connection_id (monotonic, §6) + • allocates an SHM region of + BidirectionalPipe::required_size(ring_size) bytes + via Box::new_zeroed_slice_in(BuddyAllocator) — zero- + initialised so both sides start with empty rings + • computes shm_offset = BuddyAllocator.to_offset(ptr) + No application bytes are written into the rings yet. + +3. Inform Monitor encodes the just-allocated handles into a + ConnectionReady{ listener_id, connection_id, pipe } + and ships it inside ConnectOk (outbound) or + IncomingConnection (inbound), always echoing Arca's `request_id` + on those replies. + +4. Attach Arca decodes ConnectionReady and reconstructs the SHM pointer: + ptr = BuddyAllocator.from_offset(shm_offset) + Both sides share the same physical memory; BuddyAllocator + on each side has a different virtual base but the same offset + maps to the same pages. Arca then constructs its half: + BidirectionalPipe::new(region, ring_size, Side::A) + The monitor already holds Side::B. + +5. Pump Per-session data path: move bytes between the rings and the + kernel `TcpStream`. The control protocol's job for this session + is done until teardown. +``` + +**Ordering matters.** The SHM region must be allocated **before** the +`ConnectionReady` frame is written — allocate → encode → write on one +stack. Arca cannot see `shm_offset` until it reads the frame, so it will +always call `from_offset` on a region that is already initialised. +A future multi-threaded monitor must preserve this happens-before edge. + +**How Arca resolves `shm_offset` to a pointer:** both sides call +`BuddyAllocator.from_offset(shm_offset)`. The monitor's host virtual base +and Arca's guest virtual base differ, but `from_offset` adds the caller's +own base, so both arrive at a pointer into the same physical memory. No +external registry or SHM name table is needed. + +### Teardown + +Half-close is propagated by the monitor's relay functions: + +- `tcp_to_pipe`: on TCP EOF → `pipe.close_write()` (no more data for Arca). +- `tcp_to_pipe`: if Arca closed its read side (`is_peer_read_closed()`) → + `pipe.close_write()` + `tcp.shutdown(Read)`. +- `pipe_to_tcp`: on pipe `WouldBlock` when Arca closed its write side + (`is_peer_write_closed()`) → `pipe.close_read()` + `tcp.shutdown(Write)`. + +When the pipe is fully closed (`pipe.is_closed()`), `event_loop` sets +`Connection.shm = None`, dropping the `Box<[u8], BuddyAllocator>` and +returning the SHM region to the buddy allocator. The `Connection` (and +its TCP socket) stays in the map until OS-level teardown completes. +Full `Connection` removal and `connection_id` reuse require a future +`CloseRequest` / `PeerClosed` message pair (see §9). + +### Status of the current implementation + +Steps 2, 3, and 5 are fully implemented on the monitor side. +`Monitor::dispatch_request` and `Monitor::poll_accepts` allocate a +zero-initialised SHM region via `Box::new_zeroed_slice_in(BuddyAllocator)`, +compute `shm_offset = BuddyAllocator.to_offset(ptr)`, and embed it in +`DataPipeInfo` (Steps 2–3). The monitor’s `event_loop` runs +`tcp_to_pipe` / `pipe_to_tcp` on every live connection each iteration +(Step 5). Step 4 (Arca-side attach: calling `BuddyAllocator.from_offset` +and constructing `Side::A`) is part of the Arca kernel networking layer +and is not yet implemented. + +--- + +## 3. Wire format + +A control message is one **frame**. All multi-byte integers are +**little-endian**. + +``` +offset size field +------ ---- ------------------------------------------ + 0 1 message_type (u8, see catalog below) + 1 2 payload_len (u16, bytes after header) + 3 4 request_id (u32, correlation token) + 7 .. payload (payload_len bytes, fixed layout per message_type) +``` + +- **`message_type`** — single byte, one of the variants in the catalog. +- **`payload_len`** — caps at `MAX_FRAME_PAYLOAD` (currently 256). Any frame + with a larger length is rejected as malformed. +- **`request_id`** — Arca-assigned for **every** Arca→Linux request + (including `AcceptRequest`); Linux echoes the same value on the matching + reply. There are no Linux-initiated control frames: everything the monitor + writes on the Arca→Linux ring is a **response** to something Arca asked. + +The framing is intentionally tiny so a hex dump on the pipe is human +readable. We don't have a magic byte or version field yet — when we add +the first backwards-incompatible change we'll bump the protocol with a +new message type or an extra header byte. + +--- + +## 4. Message catalog + +| Code | Name | Direction | Payload | Notes | +| ---- | -------------------- | ----------- | ----------------------- | ----- | +| 1 | `ListenRequest` | Arca → Linux | `Endpoint` (6 B) | Bind and listen. | +| 2 | `ListenOk` | Linux → Arca | `ListenerReady` (4 B) | Reply to `ListenRequest`. | +| 3 | `ConnectRequest` | Arca → Linux | `Endpoint` (6 B) | Outbound connect. | +| 4 | `ConnectOk` | Linux → Arca | `ConnectionReady` (24 B) | Reply to `ConnectRequest`. `listener_id == 0`. | +| 5 | `IncomingConnection` | Linux → Arca | `ConnectionReady` (24 B) | Reply to `AcceptRequest`. `listener_id != 0`. Same `request_id` as the wait. | +| 6 | `ListenErr` | Linux → Arca | `ErrPayload` (4 B) | Reply to `ListenRequest`. | +| 7 | `ConnectErr` | Linux → Arca | `ErrPayload` (4 B) | Reply to `ConnectRequest`. | +| 8 | `AcceptRequest` | Arca → Linux | `AcceptListenerId` (4 B) | Wait for next inbound on this `listener_id` (see §5). | +| 9 | `AcceptErr` | Linux → Arca | `ErrPayload` (4 B) | Reply to `AcceptRequest` when the monitor can't fulfil it (unknown listener, kernel `accept` failed). `code` is errno-like; `9` (EBADF) for unknown listener. | + +### Payload layouts + +All fields little-endian, fixed offsets, no padding. + +**`Endpoint` (6 B)** — IPv4 only for now. +``` +0..4 host (4 bytes, network-order octets) +4..6 port (u16) +``` + +**`ListenerReady` (4 B)** +``` +0..4 listener_id (u32, allocated by the monitor) +``` + +**`AcceptListenerId` (4 B)** +``` +0..4 listener_id (u32, must be a live listener from `ListenOk`) +``` + +**`ErrPayload` (4 B)** +``` +0..4 code (u32, Linux errno or 1 for "unknown") +``` + +**`DataPipeInfo` (16 B)** — shared by both `ConnectOk` and `IncomingConnection`. +``` +0..8 shm_offset (u64, BuddyAllocator.to_offset(ptr) — pass to from_offset() to get a pointer) +8..16 ring_size (u64, per-direction ring capacity in bytes) +``` +The total shared-memory size for this pipe is +`BidirectionalPipe::required_size(ring_size)` — derived, not transmitted. + +**`ConnectionReady` (24 B)** +``` +0..4 listener_id (u32, 0 == outbound connection) +4..8 connection_id (u32, monitor-allocated) +8..24 data_pipe_info (16 B, layout above) +``` + +--- + +## 5. Sequence diagrams + +### Outbound connect + +``` + Arca Monitor + │ ConnectRequest{rid=N, ep} │ + ├──────────────────────────────────────►│ TcpStream::connect(ep) + │ │ (kernel handshake) + │ │ allocate connection_id + │ │ Box::new_zeroed_slice_in(BuddyAllocator) ← data pipe born here (§2) + │ │ shm_offset = BuddyAllocator.to_offset(ptr) + │ ConnectOk{rid=N, ConnectionReady} │ + │◄──────────────────────────────────────┤ + │ BuddyAllocator.from_offset(shm_offset)│ + │ → BidirectionalPipe(Side::A) │ (monitor already holds Side::B) + │ │ + ▼ ▼ + ArcaTcpStream monitor.connection(id) is live +``` + +If `connect` fails the monitor replies with `ConnectErr{rid=N, errno}` and +no connection or SHM region is allocated. + +### Listen + accept (inbound) + +``` + Arca Monitor + │ ListenRequest{rid=N, ep} │ + ├──────────────────────────────────────►│ TcpListener::bind(ep) + │ │ set_nonblocking(true) + │ ListenOk{rid=N, listener_id=L} │ (no data pipe yet — listeners + │◄──────────────────────────────────────┤ don't carry application bytes) + │ │ + │ AcceptRequest{rid=M, listener L} │ enqueue (L, M); accept waits + ├──────────────────────────────────────►│ until a kernel accept pairs it + │ │ + │ ───────── time passes ───────── │ poll_accepts(): non-blocking + │ │ accept(); on success → reply + │ │ allocate connection_id ← data pipe born here (§2) + │ │ Box::new_zeroed_slice_in(BuddyAllocator) + │ │ shm_offset = BuddyAllocator.to_offset(ptr) + │ IncomingConnection{rid=M, │ + │ listener_id=L, conn_id=C, pipe} │ + │◄──────────────────────────────────────┤ + │ BuddyAllocator.from_offset(shm_offset)│ + │ → BidirectionalPipe(Side::A) │ + │ │ + ▼ ▼ + accept(&listener) returns ArcaTcpStream +``` + +Subtleties: + +- **Listen** returns immediately (`ListenOk` / `ListenErr`). +- **Connect** (`ConnectRequest`) blocks in the monitor until the kernel + connect completes — *connect waits*. +- **Accept** (`AcceptRequest`) blocks on the Arca side until the matching + `IncomingConnection` reply arrives; the monitor does **not** perform a + kernel `accept` unless there is a pending `request_id` for that listener + (so stray inbound TCP connections are not turned into sessions with no + Arca wait). +- The monitor services the control pipe with **`poll_accepts`** + (try kernel `accept` for each listener that has a FIFO of pending Arca + waits) plus **`pump_once` / `serve_one`** (read Arca→Linux frames with a + small incremental decoder on **non-blocking** transports). `serve_one` + yields the CPU while waiting for a full frame. + +--- + +## 6. Identifiers + +There are **four** numbers in a typical frame, and they're easy to mix up +because three of them look like little integers and they often appear in +the same payload. Each one answers a different question. + +| Number | Lives in | Meaning | Allocated by | +| --------------- | ---------------- | -------------------------------------------- | ------------------------------------- | +| `message_type` | header byte 0 | What kind of frame (see catalog). | Protocol (`1`–`8`). | +| `request_id` | header bytes 3–7 | Which request–reply pair (Arca sets). | Arca; echoed on replies. | +| `listener_id` | payload | Which `TcpListener`. | Monitor on bind. | +| `connection_id` | payload | Which live TCP session. | Monitor on connect or accept. | + +The first one says **what** we're doing. The other three say **which thing** +we're doing it to / about. `message_type` is the same byte for every +`ListenRequest` Arca ever sends (always `1`); the others are fresh per +listener / per connection / per conversation. + +### Correlation + +Every Linux→Arca control frame is a **reply**: its `request_id` copies the +Arca-issued token from the matching request (`ListenRequest`, +`ConnectRequest`, or `AcceptRequest`). There is no parallel “event” channel +and **no stash** on the Arca library side — `IncomingConnection` is not +delivered ahead of an `AcceptRequest`. + +### Worked example: full lifecycle of one listener with one inbound peer + +Inbound: + +```text +Arca → Linux: ListenRequest rid=42 payload: 0.0.0.0:8080 +Linux → Arca: ListenOk rid=42 payload: listener_id=1 + +Arca → Linux: AcceptRequest rid=50 payload: listener_id=1 + + ... time passes, someone opens a TCP socket to port 8080 ... + +Linux → Arca: IncomingConnection rid=50 payload: listener_id=1, connection_id=7, pipe… + (rid matches AcceptRequest) +``` + +Outbound: + +```text +Arca → Linux: ConnectRequest rid=43 payload: 8.8.8.8:443 +Linux → Arca: ConnectOk rid=43 payload: listener_id=0, connection_id=8, pipe… + listener_id 0 = outbound connect (no listener) +``` + +### Reserved values (partial) + +| Field | Reserved | Meaning | +| --------------- | -------- | ------- | +| `listener_id` | `0` | No listener — used in `ConnectOk` for outbound connects. | +| `connection_id` | `0` | Reserved for future “no connection” / error payloads. | + +`request_id == 0` is not used by the protocol today (Arca allocates monotonically from `1`). It remains available as a sentinel if we add monitor-pushed exceptions later. + +`shm_offset` (inside `DataPipeInfo`) is the BuddyAllocator byte offset to +the SHM region backing the pipe — independent of `connection_id`. Both +sides call `BuddyAllocator.from_offset(shm_offset)` to get a pointer into +the same physical memory. + +All allocators wrap on `u32::MAX` and skip back to `1` so the reserved +`0` is preserved. Real production code should reuse IDs of closed +listeners/connections (out of scope for the current iteration — see §9). + +--- + +## 7. Multiple Arca threads, ordering, and waiting on the ring + +The Linux→Arca direction of the control pipe is still a **FIFO** byte +stream: frames arrive in the order the monitor writes them. **Every** +frame is a *reply*, so its `request_id` tells you which outstanding Arca +request it completes. There is **no** secondary stash queue for +out-of-order arrivals. + +### Accept before kernel `accept` + +Because `IncomingConnection` is only emitted after an `AcceptRequest`, +the monitor cannot push inbound sessions “ahead of” unrelated replies. +For example, while Arca waits on `ConnectOk(rid=43)`, there is no longer a +scenario where two `IncomingConnection` events sit in front of that reply in +the ring without matching `AcceptRequest`s. + +### Pipelined control ops + +Several Arca threads may each block in `bind`, `connect`, or `accept` with +distinct `request_id`s. The **completion order** is whatever the monitor +produces; the Arca library reads frames strictly in FIFO order. If thread A +is waiting for `rid=2` but the next frame on the wire is `ConnectOk` for +`rid=7`, that is a **protocol / scheduling bug** (you need a single +reader/demux, or you must guarantee the monitor completes requests in the +same order Arca expects). The reference `ArcaSession` implementation +therefore **errors** on a mismatched `request_id` while waiting for a +specific reply. + +### “Peek” and CPU yield + +On **non-blocking** transports the codecs spin when a read or write returns +`WouldBlock`; they call `core::hint::spin_loop` so other hardware threads can +make progress. The monitor’s `serve_one` similarly calls `std::thread::yield_now` +while waiting for the incremental decoder to fill a complete frame. That is +the cooperative wait for the control pipe driver, without a separate buffer +of undelivered frames on Arca. + +--- + +## 8. Linux-side I/O thread + +The monitor is currently single-threaded. The main drive loop is +`Monitor::event_loop`, which combines control-frame dispatch with +per-connection byte relay: + +```rust +loop { + monitor.pump_once(&mut control_pipe)?; // control frames + for each live connection: + tcp_to_pipe / pipe_to_tcp // data relay + if pipe.is_closed(): free SHM +} +``` + +`pump_once` is **non-blocking** on the transport: it runs `poll_accepts` +(a non-blocking kernel `accept` for listeners that have pending Arca +`AcceptRequest` IDs) and then drains every fully received Arca→Linux frame +from an internal reassembly buffer. + +`Monitor::serve_one` spins (with `yield_now`) until one complete Arca→Linux +frame is available — useful when the caller prefers a blocking API. + +`dispatch_request` handles only `ListenRequest` and `ConnectRequest`; the +latter **blocks** on `TcpStream::connect` until the kernel handshake +finishes (connect waits). + +The monitor explicitly **does not** force connection streams non-blocking. +That's a policy decision for the byte-pump layer — the control +protocol just hands off the `TcpStream`. + +### Comparison with `io_uring` + +`io_uring`'s submission queue + completion queue model is exactly what we +have here at a tiny scale: the **control pipe = SQ+CQ for session +management**, with `request_id` as the user-data field. The data pipes are +the bulk-transfer side, like `io_uring` shared rings. We don't currently +support multi-shot or chained ops — when the monitor goes async, the +shape will likely converge a bit further toward `io_uring`. + +--- + +## 9. Out of scope (future work) + +In rough priority order, things this iteration intentionally doesn't do: + +1. **Full connection teardown.** Half-close is implemented (TCP EOF and + pipe close flags propagate via relay; SHM is freed when `pipe.is_closed()`). + What's missing is removing the `Connection` from the monitor's map and + reusing the `connection_id`. Needs a `CloseRequest{conn_id}` / + `PeerClosed{conn_id}` message pair. +2. **Listener teardown.** Same idea: `CloseListenerRequest{listener_id}`. +3. **IPv6.** `Endpoint` is fixed at 4 octets. When IPv6 lands, either + add a sibling type or change `Endpoint` to a length-prefixed form + (which would be the first wire-incompatible change). +4. **Backpressure on the control pipe.** The codec spins on `WouldBlock`. + That's fine when traffic is low; under load we want a proper readiness + mechanism (epoll-style on Linux side, signaling primitive on Arca). +5. **ID reuse / cleanup.** Listener and connection IDs leak monotonically + today. +6. **Arca-side data-pipe attach.** The monitor allocates SHM and sends + `shm_offset`; the Arca kernel networking layer needs to call + `BuddyAllocator.from_offset(shm_offset)` and construct `Side::A`. +7. **Shared file mapping** (notes file): "ask Linux to open `path`, + return a pointer/length into shared memory." Same control/data split + as TCP, different verb. Easy follow-on once the existing path is + solid. + +--- + +## 10. Where the code lives + +``` +arca-networking/ +├── pipe/ # arca-pipe — bidirectional pipe +├── control/ # arca-control — wire types, codec, ArcaSession +│ └── src/ +│ ├── protocol.rs # wire types + payload encodings +│ ├── message.rs # ControlRequest / ControlReply enums + to_frame / try_from +│ ├── codec.rs # read_frame / write_frame / FrameReadBuf +│ ├── arca_side.rs # ArcaSession, ArcaTcpListener, ArcaTcpStream +│ └── lib.rs +├── monitor/ # arca-monitor — Linux-side driver +│ ├── src/ +│ │ ├── lib.rs # Monitor, dispatch_request, pump_once, serve_one, poll_accepts +│ │ └── relay.rs # tcp_to_pipe / pipe_to_tcp helpers +│ └── tests/end_to_end.rs +└── PROTOCOL.md # this file +``` + +The Arca-facing public surface is intentionally small: + +```rust +use arca_control::{ArcaSession, Endpoint}; + +let mut sess = ArcaSession::new(&mut control_pipe); + +let listener = sess.bind(Endpoint::new([0, 0, 0, 0], 8080))?; +let inbound = sess.accept(&listener)?; // ArcaTcpStream + +let outbound = sess.connect(Endpoint::new([8, 8, 8, 8], 443))?; // ArcaTcpStream + +// inbound.pipe() ──► DataPipeInfo { shm_offset, ring_size } +// then attach your per-session pipe / byte layer for read/write. +``` + +Everything beyond returning the `ArcaTcpStream` handle (i.e., actually +moving bytes through the per-session data pipe) is the data-protocol +layer's job. diff --git a/common/Cargo.lock b/common/Cargo.lock new file mode 100644 index 0000000..4b3b687 --- /dev/null +++ b/common/Cargo.lock @@ -0,0 +1,16 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "throughput" +version = "0.1.0" +dependencies = [ + "libc", +] diff --git a/common/Cargo.toml b/common/Cargo.toml index 6c9e85c..f1e9dc2 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -1,29 +1,8 @@ [package] -name = "common" +name = "throughput" version = "0.1.0" edition = "2021" -[features] -default = ["std"] -std = ["alloc", "snafu/std", "libc", "nix"] -alloc = [] -thread_local_cache = ["cache"] -core_local_cache = ["cache"] -cache = [] -nix = ["dep:nix"] - [dependencies] -log = "0.4.22" -snafu = { version="0.8.5", default-features=false } -macros = { path = "../macros" } -arca = { path = "../arca" } -libc = { version="0.2.164", optional=true } -elf = { version = "0.7.4", default-features = false } -hashbrown = { version = "0.15.4", features = ["alloc", "core"] } -derive_more = { version = "2.0.1", default-features = false, features = ["full"] } -nix = { version = "0.30.1", features = ["mman"], optional = true } -async-channel = { version = "2.5.0", default-features = false } -async-lock = { version = "3.4.1", default-features = false } - -[target.'cfg(target_os="linux")'.dev-dependencies] -rand = "0.9.2" +libc = "0.2" +# rand = "0.8" diff --git a/common/plotting/1024mb.png b/common/plotting/1024mb.png new file mode 100644 index 0000000..b2860f9 Binary files /dev/null and b/common/plotting/1024mb.png differ diff --git a/common/plotting/128mb.png b/common/plotting/128mb.png new file mode 100644 index 0000000..58d0a69 Binary files /dev/null and b/common/plotting/128mb.png differ diff --git a/common/plotting/2048mb.png b/common/plotting/2048mb.png new file mode 100644 index 0000000..5200f4e Binary files /dev/null and b/common/plotting/2048mb.png differ diff --git a/common/plotting/256mb.png b/common/plotting/256mb.png new file mode 100644 index 0000000..68c713e Binary files /dev/null and b/common/plotting/256mb.png differ diff --git a/common/plotting/4096mb.png b/common/plotting/4096mb.png new file mode 100644 index 0000000..4147844 Binary files /dev/null and b/common/plotting/4096mb.png differ diff --git a/common/plotting/512mb.png b/common/plotting/512mb.png new file mode 100644 index 0000000..a3ff4f1 Binary files /dev/null and b/common/plotting/512mb.png differ diff --git a/common/plotting/64mb.png b/common/plotting/64mb.png new file mode 100644 index 0000000..d842f57 Binary files /dev/null and b/common/plotting/64mb.png differ diff --git a/common/plotting/plot_tsc.py b/common/plotting/plot_tsc.py new file mode 100644 index 0000000..339c54c --- /dev/null +++ b/common/plotting/plot_tsc.py @@ -0,0 +1,107 @@ +import matplotlib.pyplot as plt + +writer_runs = [ + [ + 2388750332103860, 2388750600415580, 2388750899833260, 2388751169017140, + 2388751468441760, 2388751752172060, 2388752036281320, 2388752335692860, + 2388752603189520, 2388752902553900, 2388753171139520 + ], + [ + 2388762320920060, 2388762637222060, 2388762945178360, 2388763253899920, + 2388763530539040, 2388763839253460, 2388764146715280, 2388764455396280, + 2388764764089980, 2388765054394460, 2388765348976240 + ], + [ + 2388776316613540, 2388776576396820, 2388776834006180, 2388777088938060, + 2388777344139300, 2388777599634180, 2388777853815340, 2388778107957660, + 2388778362673000, 2388778618001180, 2388778872369060 + ], + [ + 2388788373366020, 2388788633611040, 2388788892467160, 2388789150505640, + 2388789406778680, 2388789662678160, 2388789918417620, 2388790174163100, + 2388790432638520, 2388790688217420, 2388790943995100 + ], + [ + 2388798568896120, 2388798833304540, 2388799092775560, 2388799349636680, + 2388799606009300, 2388799866392160, 2388800123231240, 2388800385448840, + 2388800647825720, 2388800905686200, 2388801163495760 + ], +] + +reader_runs = [ + [ + 2388750332050460, 2388750600416640, 2388750899834160, 2388751169018120, + 2388751468443180, 2388751752173660, 2388752036282760, 2388752335694300, + 2388752603191020, 2388752902555520, 2388753171168140 + ], + [ + 2388762320892360, 2388762637223540, 2388762945179860, 2388763253900940, + 2388763530540400, 2388763839254900, 2388764146716900, 2388764455397380, + 2388764764091840, 2388765054395580, 2388765349005080 + ], + [ + 2388776316590700, 2388776576398300, 2388776834008020, 2388777088939580, + 2388777344140660, 2388777599635500, 2388777853816720, 2388778107958880, + 2388778362673920, 2388778618002540, 2388778872394620 + ], + [ + 2388788373338800, 2388788633612120, 2388788892468280, 2388789150507480, + 2388789406780200, 2388789662679940, 2388789918418620, 2388790174164260, + 2388790432639540, 2388790688218420, 2388790944029120 + ], + [ + 2388798568891040, 2388798833305540, 2388799092776800, 2388799349638200, + 2388799606010700, 2388799866393480, 2388800123232680, 2388800385450620, + 2388800647826920, 2388800905687640, 2388801163522360 + ], +] + +def deltas(tsc): + return [tsc[i+1] - tsc[i] for i in range(len(tsc)-1)] + +def average_runs(runs): + all_d = [deltas(r) for r in runs] + n = len(all_d) + m = len(all_d[0]) + return [sum(run[i] for run in all_d) / n for i in range(m)] + +def total_diff(tsc): + return tsc[-1] - tsc[0] + +# ========================= +# COMPUTE +# ========================= +writer_avg = average_runs(writer_runs) +reader_avg = average_runs(reader_runs) + +# compute total diff per run, then average +writer_total_diffs = [total_diff(r) for r in writer_runs] +reader_total_diffs = [total_diff(r) for r in reader_runs] + +# ========================= +# PRINT +# ========================= +print("WRITER_AVG") +for v in writer_avg: + print(v) + +print("\nREADER_AVG") +for v in reader_avg: + print(v) + +# ========================= +# PLOT +# ========================= +x = list(range(1, len(writer_avg) + 1)) + +plt.figure() +plt.plot(x, writer_avg, marker='o', label='Writer') +plt.plot(x, reader_avg, marker='x', label='Reader') + +plt.xlabel("Checkpoint Interval") +plt.ylabel("Cycles (TSC delta)") +plt.title("4 GB Transfer: Avg Cycles per Interval") +plt.legend() +plt.grid() + +plt.show() diff --git a/common/src/bin/reader.rs b/common/src/bin/reader.rs new file mode 100644 index 0000000..a37d546 --- /dev/null +++ b/common/src/bin/reader.rs @@ -0,0 +1,178 @@ +// reader.rs +use std::env; +use std::ffi::CString; +use std::sync::atomic::{fence, Ordering}; +use std::ptr; +use std::mem::size_of; +use throughput::{read_tsc, ShmHeader}; + +const MB: u64 = 1024 * 1024; + +fn main() { + let args: Vec = env::args().collect(); + + if args.len() < 5 { + eprintln!( + "Usage: {} ", + args[0] + ); + std::process::exit(1); + } + + let shm_name = &args[1]; + let shm_size: u64 = args[2].parse() + .expect("share_mem_size must be a valid number (bytes)"); + let transfer_size_mb: u64 = args[3].parse() + .expect("transfer_size_mb must be a valid number (MB)"); + let transfer_size: u64 = transfer_size_mb.saturating_mul(MB); + let chunk_size: u32 = args[4].parse() + .expect("chunk_size must be a valid number (bytes)"); + + // Add '/' prefix if needed + let shm_name = if shm_name.starts_with('/') { + shm_name.to_string() + } else { + format!("/{}", shm_name) + }; + + // Convert to C string + let c_name = CString::new(shm_name.as_bytes()).unwrap(); + + println!("Reader: Waiting for writer to create shared memory..."); + + // Open existing shared memory (no O_CREAT flag) + let fd = loop { + let fd = unsafe { libc::shm_open(c_name.as_ptr(), libc::O_RDWR, 0o666) }; + if fd >= 0 { + break fd; + } + std::thread::sleep(std::time::Duration::from_millis(100)); + }; + + println!("Reader: Shared memory found!"); + + let total_size = size_of::() as u64 + shm_size; + println!("Writer: ShmHeader size: {}", size_of::()); + + let ptr = unsafe { + libc::mmap( + std::ptr::null_mut(), + total_size as usize, + libc::PROT_READ | libc::PROT_WRITE, + libc::MAP_SHARED, + fd, + 0, + ) + }; + + if ptr == libc::MAP_FAILED { + panic!("Failed to map shared memory"); + } + + // Get pointers to shared variables + let header = unsafe { &*(ptr as *mut ShmHeader) }; + let data_start = unsafe { (ptr as *mut u8).add(size_of::()) }; + + // Prepare buffer for reading + let mut dst = vec![0u8; transfer_size as usize]; + + // Touch pages so allocation/fault cost doesn't hit the timed path (reader side) + let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) as usize }; + for i in (0..dst.len()).step_by(page_size) { + dst[i] = 1; + } + + let mut total_read = 0u64; + + #[cfg(debug_assertions)] + let mut xor_checksum: u8 = 0; + + // tsc + let ckpt_total_interval = 10; + let ckpt_interval_sz = (transfer_size + ckpt_total_interval - 1) / ckpt_total_interval; + let mut ckpt_next = ckpt_interval_sz; + + // Change transfer_started to 1 (signal writer to start) + header.transfer_started.store(1, Ordering::Release); + println!("Reader: Signaled writer to start, waiting for data..."); + + eprintln!("--- Reader checkpoint 0/{} tsc: {}", ckpt_total_interval, read_tsc()); + + while total_read < transfer_size { + let end_idx = header.end_index.load(Ordering::Acquire); + let start_idx = header.start_index.load(Ordering::Acquire); + + let avail_len = end_idx - start_idx; + + if avail_len > 0 { + let len = (chunk_size as u64) + .min(transfer_size - total_read) + .min(avail_len); + + // Calculate read position with wrap-around + let read_start = (start_idx % shm_size) as usize; + let l = std::cmp::min(len, shm_size - read_start as u64) as usize; + + unsafe { + // First part (until wrap or end of chunk) + ptr::copy_nonoverlapping( + data_start.add(read_start), + dst.as_mut_ptr().add(total_read as usize), + l, + ); + + // Second part (wrapped around to beginning) + if l < len as usize { + ptr::copy_nonoverlapping( + data_start, + dst.as_mut_ptr().add(total_read as usize + l), + len as usize - l, + ); + } + } + + // Barrier: smp_wmb() + fence(Ordering::Release); + + header.start_index.store(start_idx + len, Ordering::Relaxed); + total_read += len; + + if total_read > ckpt_next { + eprintln!( + "--- Reader checkpoint {}/{} tsc: {}", + ckpt_next / ckpt_interval_sz, + ckpt_total_interval, + read_tsc() + ); + ckpt_next += ckpt_interval_sz; + } + } else { + std::hint::spin_loop(); + } + } + + eprintln!( + "--- Reader checkpoint {}/{} tsc: {}", + ckpt_next / ckpt_interval_sz, + ckpt_total_interval, + read_tsc() + ); + println!("Reader: Finished reading {} bytes", total_read); + + header.transfer_started.store(0, Ordering::Relaxed); + + #[cfg(debug_assertions)] + { + for i in 0..total_read as usize { + xor_checksum ^= dst[i]; + } + println!("Reader XOR checksum: 0x{:02X}", xor_checksum); + } + + // Cleanup + unsafe { + libc::munmap(ptr, total_size as usize); + libc::close(fd); + libc::shm_unlink(c_name.as_ptr()); + } +} \ No newline at end of file diff --git a/common/src/bin/writer.rs b/common/src/bin/writer.rs new file mode 100644 index 0000000..960fafa --- /dev/null +++ b/common/src/bin/writer.rs @@ -0,0 +1,193 @@ +// writer.rs +use std::env; +use std::ffi::CString; +use std::sync::atomic::{fence, Ordering}; +use std::time::Instant; +use std::ptr; +use std::mem::size_of; +use throughput::{read_tsc, ShmHeader}; + +const MB: u64 = 1024 * 1024; + +fn main() { + let args: Vec = env::args().collect(); + + if args.len() < 5 { + eprintln!( + "Usage: {} ", + args[0] + ); + std::process::exit(1); + } + + let shm_name = &args[1]; + let shm_size: u64 = args[2].parse() + .expect("share_mem_size must be a valid number (bytes)"); + let transfer_size_mb: u64 = args[3].parse() + .expect("transfer_size_mb must be a valid number (MB)"); + let transfer_size: u64 = transfer_size_mb.saturating_mul(MB); + let chunk_size: u32 = args[4].parse() + .expect("chunk_size must be a valid number (bytes)"); + + // Add '/' prefix if needed + let shm_name = if shm_name.starts_with('/') { + shm_name.to_string() + } else { + format!("/{}", shm_name) + }; + + // Convert to C string + let c_name = CString::new(shm_name.as_bytes()).unwrap(); + + // Open/create shared memory + let fd = unsafe { libc::shm_open(c_name.as_ptr(), libc::O_CREAT | libc::O_RDWR, 0o666) }; + + if fd < 0 { + panic!("Failed to create shared memory"); + } + + let total_size = size_of::() as u64 + shm_size; + println!("Writer: ShmHeader size: {}", size_of::()); + + unsafe { + libc::ftruncate(fd, total_size as i64); + } + + let ptr = unsafe { + libc::mmap( + std::ptr::null_mut(), + total_size as usize, + libc::PROT_READ | libc::PROT_WRITE, + libc::MAP_SHARED, + fd, + 0, + ) + }; + + if ptr == libc::MAP_FAILED { + panic!("Failed to map shared memory"); + } + + // Get pointers to shared variables + let header = unsafe { &*(ptr as *mut ShmHeader) }; + let data_start = unsafe { (ptr as *mut u8).add(size_of::()) }; + + // Fill with pattern: 1, 2, 3, ..., 255, 1, 2, 3, ... + let mut src = vec![0u8; chunk_size as usize]; + for i in 0..chunk_size as usize { + src[i] = ((i % 255) + 1) as u8; + } + + let mut total_written = 0u64; + + #[cfg(debug_assertions)] + let mut xor_checksum: u8 = 0; + + // tsc + let ckpt_total_interval = 10; + let ckpt_interval_sz = (transfer_size + ckpt_total_interval - 1) / ckpt_total_interval; + let mut ckpt_next = ckpt_interval_sz; + + println!("Writer: Waiting for reader to start (transfer_started=1)..."); + + // Wait till reader changes transfer_started to 1 + while header.transfer_started.load(Ordering::Acquire) == 0 { + std::hint::spin_loop(); + } + + println!("Writer: Reader ready, starting write..."); + let start_time = Instant::now(); + eprintln!("--- Writer checkpoint 0/{} tsc: {}", ckpt_total_interval, read_tsc()); + + while total_written < transfer_size { + let end_idx = header.end_index.load(Ordering::Acquire); + let start_idx = header.start_index.load(Ordering::Acquire); + + let unused_len = shm_size - (end_idx - start_idx); + + if unused_len > 0 { + let len = (chunk_size as u64) + .min(transfer_size - total_written) + .min(unused_len); + + // Calculate write position with wrap-around + let write_start = (end_idx % shm_size) as usize; + let l = std::cmp::min(len, shm_size - write_start as u64) as usize; + + unsafe { + // First part (until wrap or end of chunk) + ptr::copy_nonoverlapping(src.as_ptr(), data_start.add(write_start), l); + + // Second part (wrapped around to beginning) + if l < len as usize { + ptr::copy_nonoverlapping( + src.as_ptr().add(l), + data_start, + len as usize - l, + ); + } + } + + fence(Ordering::Release); + + header.end_index.store(end_idx + len, Ordering::Release); + total_written += len; + + #[cfg(debug_assertions)] + { + for i in 0..len as usize { + xor_checksum ^= src[i]; + } + } + + if total_written > ckpt_next { + eprintln!( + "--- Writer checkpoint {}/{} tsc: {}", + ckpt_next / ckpt_interval_sz, + ckpt_total_interval, + read_tsc() + ); + ckpt_next += ckpt_interval_sz; + } + } else { + std::hint::spin_loop(); + } + } + + eprintln!( + "--- Writer checkpoint {}/{} tsc: {}", + ckpt_next / ckpt_interval_sz, + ckpt_total_interval, + read_tsc() + ); + println!("Writer: Finished writing {} bytes", total_written); + + #[cfg(debug_assertions)] + println!("Writer XOR checksum: 0x{:02X}", xor_checksum); + + println!("Writer: Waiting for reader to finish ..."); + + // Wait till reader changes transfer_started to 0 + while header.transfer_started.load(Ordering::Relaxed) != 0 { + std::hint::spin_loop(); + } + + let elapsed = start_time.elapsed(); + + println!("========================================"); + println!("WRITER STATS"); + println!("========================================"); + println!("Total time: {} µs, {} s", elapsed.as_micros(), elapsed.as_secs_f64()); + println!("Data written: {} bytes", total_written); + println!( + "Throughput: {:.4} GB / s", + total_written as f64 / (1024.0 * 1024.0 * 1024.0 * elapsed.as_secs_f64()) + ); + println!("========================================"); + + // Cleanup + unsafe { + libc::munmap(ptr, total_size as usize); + libc::close(fd); + } +} \ No newline at end of file diff --git a/common/src/lib.rs b/common/src/lib.rs index 78ea10b..e03d4e6 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -1,58 +1,20 @@ -#![cfg_attr(not(feature = "std"), no_std)] -#![allow(stable_features, unused_features)] -#![feature(allocator_api)] -#![feature(fn_traits)] -#![cfg_attr(feature = "std", feature(layout_for_ptr))] -#![feature(negative_impls)] -#![feature(ptr_metadata)] -#![cfg_attr(test, feature(test))] -#![feature(unboxed_closures)] -#![cfg_attr(feature = "thread_local_cache", feature(thread_local))] - -pub mod buddy; -pub mod refcnt; -pub use buddy::BuddyAllocator; -pub mod arrayvec; -pub mod bitpack; -pub mod controlreg; -pub mod elfloader; -pub mod ipaddr; -pub mod sendable; -pub mod util; -pub mod vhost; - -#[cfg(feature = "std")] -pub mod mmap; - -#[repr(C)] -#[derive(Debug)] -pub struct LogRecord { - pub level: u8, - pub target: (usize, usize), - pub file: Option<(usize, usize)>, - pub line: Option, - pub module_path: Option<(usize, usize)>, - pub message: (usize, usize), -} +use std::arch::x86_64::{_mm_lfence, _mm_mfence, _rdtsc}; +use std::sync::atomic::{AtomicU64, AtomicU32}; #[repr(C)] -#[derive(Debug, Default)] -pub struct SymtabRecord { - pub addr: usize, - pub offset: usize, - pub found: bool, - pub file_buffer: (usize, usize), - pub file_len: usize, +pub struct ShmHeader { + pub start_index: AtomicU64, + pub end_index: AtomicU64, + pub transfer_started: AtomicU32, } -pub mod hypercall { - pub const EXIT: u64 = 0; - pub const LOG: u64 = 1; - pub const SYMNAME: u64 = 2; - pub const MEMSET: u64 = 3; - pub const MEMCLR: u64 = 4; - pub const SERVERREAD: u64 = 5; - pub const SERVERWRITE: u64 = 6; - pub const CLIENTREAD: u64 = 7; - pub const CLIENTWRITE: u64 = 8; +#[inline] +pub fn read_tsc() -> u64 { + unsafe { + _mm_mfence(); + _mm_lfence(); + let tsc = _rdtsc(); + _mm_lfence(); + tsc + } } diff --git a/common/src/plot.py b/common/src/plot.py new file mode 100644 index 0000000..1b66bf1 --- /dev/null +++ b/common/src/plot.py @@ -0,0 +1,52 @@ +import matplotlib.pyplot as plt + +# Writer TSC values +writer_tsc = [ + 2376748235825360, + 2376748240457500, + 2376748245022620, + 2376748249579840, + 2376748254105020, + 2376748258725820, + 2376748263314320, + 2376748267876480, + 2376748272403660, + 2376748276957160, + 2376748281879180, +] + +# Reader TSC values +reader_tsc = [ + 2376748235817640, + 2376748240458340, + 2376748245023720, + 2376748249580600, + 2376748254106440, + 2376748258727040, + 2376748263315080, + 2376748267877260, + 2376748272404400, + 2376748276957880, + 2376748281899400, +] + +# Compute deltas (cycles between checkpoints) +writer_deltas = [writer_tsc[i] - writer_tsc[i-1] for i in range(1, len(writer_tsc))] +reader_deltas = [reader_tsc[i] - reader_tsc[i-1] for i in range(1, len(reader_tsc))] + +# X axis (checkpoint index) +x = list(range(1, len(writer_tsc))) + +# Plot +plt.figure() + +plt.plot(x, writer_deltas, marker='o', label='Writer') +plt.plot(x, reader_deltas, marker='o', label='Reader') + +plt.xlabel("Checkpoint") +plt.ylabel("Cycles (delta TSC)") +plt.title("TSC Delta per Checkpoint (Reader vs Writer)") +plt.legend() +plt.grid(True) + +plt.show() \ No newline at end of file diff --git a/control/Cargo.lock b/control/Cargo.lock new file mode 100644 index 0000000..58584e6 --- /dev/null +++ b/control/Cargo.lock @@ -0,0 +1,14 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "arca-control" +version = "0.1.0" +dependencies = [ + "arca-pipe", +] + +[[package]] +name = "arca-pipe" +version = "0.1.0" diff --git a/control/Cargo.toml b/control/Cargo.toml new file mode 100644 index 0000000..7c3e448 --- /dev/null +++ b/control/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "arca-control" +version = "0.1.0" +edition = "2021" + +[dependencies] +arca-pipe = { path = "../pipe" } diff --git a/control/src/arca_side.rs b/control/src/arca_side.rs new file mode 100644 index 0000000..9072e44 --- /dev/null +++ b/control/src/arca_side.rs @@ -0,0 +1,419 @@ +//! Arca-side wrappers over the control pipe. +//! +//! Designed to *feel* like `std::net` for callers: +//! +//! ```ignore +//! let listener = session.bind(Endpoint::new([0, 0, 0, 0], 8080))?; +//! let stream = session.connect(Endpoint::new([8, 8, 8, 8], 443))?; +//! let inbound = session.accept(&listener)?; +//! ``` +//! +//! The objects we hand back ([`ArcaTcpListener`], [`ArcaTcpStream`]) are +//! lightweight handles — just IDs and a [`DataPipeInfo`]. They deliberately +//! don't implement `Read`/`Write` themselves; the per-connection bytestream +//! is wired up by the data-pipe layer (the rings live in their own SHM +//! region, separate from the control pipe). +//! +//! **Correlation:** every Linux→Arca frame is tagged with the same +//! `request_id` as the Arca→Linux request it answers (including inbound +//! connections via [`MessageType::AcceptRequest`]). There is no separate +//! event stash — if several Arca threads share one control pipe, they must +//! coordinate so only one thread reads at a time (or a single demux task +//! routes by `request_id`). + +use arca_pipe::{Read, Write}; + +use crate::{ + read_frame, write_frame, CodecError, ConnectionReady, ControlReply, ControlRequest, + DataPipeInfo, Endpoint, MessageType, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ArcaError { + Codec(CodecError), + /// Linux returned `ListenErr` with the given errno-like code. + ListenFailed { + code: u32, + }, + /// Linux returned `ConnectErr` with the given errno-like code. + ConnectFailed { + code: u32, + }, + /// Linux returned `AcceptErr` with the given errno-like code (unknown + /// listener, or kernel `accept` failed on the monitor side). + AcceptFailed { + code: u32, + }, + /// Got a frame we weren't expecting — protocol bug or out-of-sync state. + UnexpectedReply(MessageType), + /// Reply came back with a `request_id` we didn't issue (wrong order on + /// this transport, or another thread's reply). + UnexpectedRequestId { + expected: u32, + got: u32, + }, +} + +impl From for ArcaError { + fn from(value: CodecError) -> Self { + Self::Codec(value) + } +} + +/// Handle to a listener Linux is holding open for us. POD on purpose — +/// pass it to `accept` to wait for new connections on this listener. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ArcaTcpListener { + listener_id: u32, +} + +impl ArcaTcpListener { + pub fn id(&self) -> u32 { + self.listener_id + } +} + +/// Handle to one accepted/connected TCP session. +/// +/// `listener_id == 0` means "outbound connection, no listener". The actual +/// per-direction bytestream is in the data pipe described by `pipe`; this +/// struct is just the metadata Arca needs to attach to it. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ArcaTcpStream { + listener_id: u32, + connection_id: u32, + pipe: DataPipeInfo, +} + +impl ArcaTcpStream { + pub fn connection_id(&self) -> u32 { + self.connection_id + } + + /// `0` for outbound `connect`, otherwise the listener that produced this + /// stream via `accept`. + pub fn listener_id(&self) -> u32 { + self.listener_id + } + + /// Where the per-connection data pipe lives in shared memory. + pub fn pipe(&self) -> DataPipeInfo { + self.pipe + } + + pub fn is_inbound(&self) -> bool { + self.listener_id != 0 + } +} + +/// Owner of the **single** control pipe on the Arca side. +pub struct ArcaSession<'a, T: Read + Write> { + transport: &'a mut T, + next_request_id: u32, +} + +impl<'a, T: Read + Write> ArcaSession<'a, T> { + pub fn new(transport: &'a mut T) -> Self { + Self { + transport, + next_request_id: 1, + } + } + + fn alloc_request_id(&mut self) -> u32 { + let id = self.next_request_id; + self.next_request_id = self.next_request_id.wrapping_add(1); + id + } + + /// Ask Linux to bind+listen on `ep`. Returns a handle on success. + pub fn bind(&mut self, ep: Endpoint) -> Result { + let request_id = self.alloc_request_id(); + write_frame( + self.transport, + &ControlRequest::Listen { + request_id, + endpoint: ep, + } + .to_frame(), + )?; + + match self.read_reply_for(request_id)? { + ControlReply::ListenOk { listener_id, .. } => Ok(ArcaTcpListener { listener_id }), + ControlReply::ListenErr { code, .. } => Err(ArcaError::ListenFailed { code }), + other => Err(ArcaError::UnexpectedReply(other.message_type())), + } + } + + /// Ask Linux to connect outbound to `ep`. Returns a handle on success. + pub fn connect(&mut self, ep: Endpoint) -> Result { + let request_id = self.alloc_request_id(); + write_frame( + self.transport, + &ControlRequest::Connect { + request_id, + endpoint: ep, + } + .to_frame(), + )?; + + match self.read_reply_for(request_id)? { + ControlReply::ConnectOk { ready, .. } => Ok(stream_from_ready(ready)), + ControlReply::ConnectErr { code, .. } => Err(ArcaError::ConnectFailed { code }), + other => Err(ArcaError::UnexpectedReply(other.message_type())), + } + } + + /// Wait for the next inbound connection on `listener`. + /// + /// Sends [`ControlRequest::Accept`] and blocks until Linux replies with a + /// [`ControlReply::AcceptOk`] for the same `request_id`. + pub fn accept(&mut self, listener: &ArcaTcpListener) -> Result { + let request_id = self.alloc_request_id(); + write_frame( + self.transport, + &ControlRequest::Accept { + request_id, + listener_id: listener.listener_id, + } + .to_frame(), + )?; + + match self.read_reply_for(request_id)? { + ControlReply::AcceptOk { ready, .. } => Ok(stream_from_ready(ready)), + ControlReply::AcceptErr { code, .. } => Err(ArcaError::AcceptFailed { code }), + other => Err(ArcaError::UnexpectedReply(other.message_type())), + } + } + + fn read_reply_for(&mut self, expected_rid: u32) -> Result { + let reply = ControlReply::try_from(&read_frame(self.transport)?)?; + if reply.request_id() != expected_rid { + return Err(ArcaError::UnexpectedRequestId { + expected: expected_rid, + got: reply.request_id(), + }); + } + Ok(reply) + } +} + +fn stream_from_ready(ready: ConnectionReady) -> ArcaTcpStream { + ArcaTcpStream { + listener_id: ready.listener_id, + connection_id: ready.connection_id, + pipe: ready.pipe, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ControlFrame, DataPipeInfo, MessageType}; + use arca_pipe::PipeError; + + /// In-memory transport for tests: writes append, reads pop from front. + struct MemTransport { + outbox: [u8; 1024], + outbox_len: usize, + inbox: [u8; 1024], + inbox_len: usize, + inbox_pos: usize, + } + + impl MemTransport { + fn new() -> Self { + Self { + outbox: [0u8; 1024], + outbox_len: 0, + inbox: [0u8; 1024], + inbox_len: 0, + inbox_pos: 0, + } + } + + fn push_inbound(&mut self, frame: &ControlFrame) { + struct InboxWriter<'a>(&'a mut MemTransport); + impl Write for InboxWriter<'_> { + fn write(&mut self, src: &[u8]) -> Result { + let end = self.0.inbox_len + src.len(); + assert!(end <= self.0.inbox.len(), "inbox overflow"); + self.0.inbox[self.0.inbox_len..end].copy_from_slice(src); + self.0.inbox_len = end; + Ok(src.len()) + } + } + let mut w = InboxWriter(self); + write_frame(&mut w, frame).unwrap(); + } + + fn outbox_slice(&self) -> &[u8] { + &self.outbox[..self.outbox_len] + } + } + + impl Write for MemTransport { + fn write(&mut self, src: &[u8]) -> Result { + let end = self.outbox_len + src.len(); + assert!(end <= self.outbox.len(), "outbox overflow"); + self.outbox[self.outbox_len..end].copy_from_slice(src); + self.outbox_len = end; + Ok(src.len()) + } + } + + impl Read for MemTransport { + fn read(&mut self, buf: &mut [u8]) -> Result { + if self.inbox_pos >= self.inbox_len { + return Err(PipeError::WouldBlock); + } + let n = core::cmp::min(buf.len(), self.inbox_len - self.inbox_pos); + buf[..n].copy_from_slice(&self.inbox[self.inbox_pos..self.inbox_pos + n]); + self.inbox_pos += n; + Ok(n) + } + } + + fn listen_ok(rid: u32, listener_id: u32) -> ControlFrame { + ControlReply::ListenOk { + request_id: rid, + listener_id, + } + .to_frame() + } + + fn connect_ok(rid: u32, ready: ConnectionReady) -> ControlFrame { + ControlReply::ConnectOk { + request_id: rid, + ready, + } + .to_frame() + } + + fn incoming(rid: u32, ready: ConnectionReady) -> ControlFrame { + ControlReply::AcceptOk { + request_id: rid, + ready, + } + .to_frame() + } + + #[test] + fn bind_returns_listener_handle() { + let mut t = MemTransport::new(); + t.push_inbound(&listen_ok(1, 7)); + + let listener = { + let mut s = ArcaSession::new(&mut t); + s.bind(Endpoint::new([127, 0, 0, 1], 8080)).unwrap() + }; + assert_eq!(listener.id(), 7); + + let mut reader = SliceReader { + data: t.outbox_slice(), + pos: 0, + }; + let req = read_frame(&mut reader).unwrap(); + assert_eq!(req.message_type, MessageType::ListenRequest); + assert_eq!(req.request_id, 1); + } + + #[test] + fn connect_returns_stream_handle() { + let ready = ConnectionReady { + listener_id: 0, + connection_id: 17, + pipe: DataPipeInfo::new(17, 256), + }; + let mut t = MemTransport::new(); + t.push_inbound(&connect_ok(1, ready)); + + let stream = { + let mut s = ArcaSession::new(&mut t); + s.connect(Endpoint::new([8, 8, 8, 8], 443)).unwrap() + }; + assert_eq!(stream.connection_id(), 17); + assert!(!stream.is_inbound()); + assert_eq!(stream.pipe(), DataPipeInfo::new(17, 256)); + } + + #[test] + fn accept_sends_accept_request_and_parses_reply() { + let ready = ConnectionReady { + listener_id: 5, + connection_id: 99, + pipe: DataPipeInfo::new(99, 64), + }; + let mut t = MemTransport::new(); + t.push_inbound(&incoming(1, ready)); + + let mut s = ArcaSession::new(&mut t); + let listener = ArcaTcpListener { listener_id: 5 }; + let inbound = s.accept(&listener).unwrap(); + assert_eq!(inbound.connection_id(), 99); + assert!(inbound.is_inbound()); + + let mut reader = SliceReader { + data: t.outbox_slice(), + pos: 0, + }; + let req = read_frame(&mut reader).unwrap(); + assert_eq!( + ControlRequest::try_from(&req).unwrap(), + ControlRequest::Accept { + request_id: 1, + listener_id: 5 + } + ); + } + + #[test] + fn listen_failure_propagates_errno() { + let mut t = MemTransport::new(); + t.push_inbound( + &ControlReply::ListenErr { + request_id: 1, + code: 98, + } + .to_frame(), + ); + + let mut s = ArcaSession::new(&mut t); + let err = s.bind(Endpoint::new([0, 0, 0, 0], 1)).unwrap_err(); + assert_eq!(err, ArcaError::ListenFailed { code: 98 }); + } + + #[test] + fn accept_failure_propagates_errno() { + let mut t = MemTransport::new(); + t.push_inbound( + &ControlReply::AcceptErr { + request_id: 1, + code: 9, + } + .to_frame(), + ); + + let mut s = ArcaSession::new(&mut t); + let listener = ArcaTcpListener { listener_id: 99 }; + let err = s.accept(&listener).unwrap_err(); + assert_eq!(err, ArcaError::AcceptFailed { code: 9 }); + } + + struct SliceReader<'a> { + data: &'a [u8], + pos: usize, + } + + impl<'a> Read for SliceReader<'a> { + fn read(&mut self, buf: &mut [u8]) -> Result { + if self.pos >= self.data.len() { + return Err(PipeError::WouldBlock); + } + let n = core::cmp::min(buf.len(), self.data.len() - self.pos); + buf[..n].copy_from_slice(&self.data[self.pos..self.pos + n]); + self.pos += n; + Ok(n) + } + } +} diff --git a/control/src/codec.rs b/control/src/codec.rs new file mode 100644 index 0000000..5b0a1cb --- /dev/null +++ b/control/src/codec.rs @@ -0,0 +1,340 @@ +//! Read and write [`ControlFrame`] values on an `arca-pipe` byte stream. +//! +//! Header layout matches [`crate::protocol`]. + +use arca_pipe::{PipeError, Read, Write}; + +use crate::{CodecError, ControlFrame, MessageType, MAX_FRAME_PAYLOAD}; + +/// `message_type` (1) + `payload_len` (2) + `request_id` (4). +pub const HEADER_LEN: usize = 7; + +/// Largest on-wire frame: header + full payload. +pub const MAX_WIRE_FRAME_LEN: usize = HEADER_LEN + MAX_FRAME_PAYLOAD; + +#[inline] +fn relax_wait() { + // Avoid busy-spinning on WouldBlock when multiple threads / the monitor + // is waiting on the control ring. + core::hint::spin_loop(); +} + +pub fn write_frame(transport: &mut T, frame: &ControlFrame) -> Result<(), CodecError> { + let mut header = [0u8; HEADER_LEN]; + header[0] = frame.message_type as u8; + header[1..3].copy_from_slice(&frame.payload_len.to_le_bytes()); + header[3..7].copy_from_slice(&frame.request_id.to_le_bytes()); + + write_all(transport, &header)?; + write_all(transport, frame.payload())?; + Ok(()) +} + +pub fn read_frame(transport: &mut T) -> Result { + let mut buf = [0u8; MAX_WIRE_FRAME_LEN]; + read_exact(transport, &mut buf[..HEADER_LEN])?; + let payload_len = u16::from_le_bytes([buf[1], buf[2]]) as usize; + if payload_len > MAX_FRAME_PAYLOAD { + return Err(CodecError::PayloadTooLarge { len: payload_len }); + } + let total = HEADER_LEN + payload_len; + read_exact(transport, &mut buf[HEADER_LEN..total])?; + decode_frame_from_prefix(&buf[..total]) +} + +fn write_all(transport: &mut T, mut src: &[u8]) -> Result<(), CodecError> { + while !src.is_empty() { + match transport.write(src) { + Ok(0) => return Err(CodecError::Closed), + Ok(n) => src = &src[n..], + Err(PipeError::WouldBlock) => relax_wait(), + } + } + Ok(()) +} + +/// Incremental decoder for non-blocking transports. +/// +/// Keeps partial bytes until a full frame is available; supports multiple +/// frames read in one chunk. +#[derive(Debug, Clone)] +pub struct FrameReadBuf { + storage: [u8; MAX_WIRE_FRAME_LEN], + len: usize, +} + +impl Default for FrameReadBuf { + fn default() -> Self { + Self::new() + } +} + +impl FrameReadBuf { + pub const fn new() -> Self { + Self { + storage: [0u8; MAX_WIRE_FRAME_LEN], + len: 0, + } + } + + /// Append bytes from `transport` and return the next full frame, if any. + /// + /// Returns `Ok(None)` when starved by `WouldBlock` or a partial frame. + pub fn try_read_frame( + &mut self, + transport: &mut T, + ) -> Result, CodecError> { + loop { + if self.len >= HEADER_LEN { + let payload_len = u16::from_le_bytes([self.storage[1], self.storage[2]]) as usize; + if payload_len > MAX_FRAME_PAYLOAD { + return Err(CodecError::PayloadTooLarge { len: payload_len }); + } + let total = HEADER_LEN + payload_len; + if total > MAX_WIRE_FRAME_LEN { + return Err(CodecError::PayloadTooLarge { len: payload_len }); + } + if self.len >= total { + let frame = decode_frame_from_prefix(&self.storage[..total])?; + self.consume_prefix(total); + return Ok(Some(frame)); + } + } + if self.len >= self.storage.len() { + // Malformed / oversized; avoid growing past the buffer. + return Err(CodecError::PayloadTooLarge { + len: MAX_FRAME_PAYLOAD + 1, + }); + } + match transport.read(&mut self.storage[self.len..]) { + Ok(0) => return Err(CodecError::Closed), + Ok(n) => self.len += n, + Err(PipeError::WouldBlock) => return Ok(None), + } + } + } + + fn consume_prefix(&mut self, n: usize) { + debug_assert!(n <= self.len); + let remain = self.len - n; + if remain > 0 { + self.storage.copy_within(n..self.len, 0); + } + self.len = remain; + } +} + +fn decode_frame_from_prefix(buf: &[u8]) -> Result { + if buf.len() < HEADER_LEN { + return Err(CodecError::PayloadTooLarge { len: 0 }); + } + let message_type = + MessageType::from_u8(buf[0]).ok_or(CodecError::UnknownMessageType(buf[0]))?; + let payload_len = u16::from_le_bytes([buf[1], buf[2]]) as usize; + if payload_len > MAX_FRAME_PAYLOAD { + return Err(CodecError::PayloadTooLarge { len: payload_len }); + } + let total = HEADER_LEN + payload_len; + if buf.len() < total { + return Err(CodecError::PayloadTooLarge { len: payload_len }); + } + let request_id = u32::from_le_bytes([buf[3], buf[4], buf[5], buf[6]]); + + let mut frame = ControlFrame { + message_type, + request_id, + payload_len: payload_len as u16, + payload: [0u8; MAX_FRAME_PAYLOAD], + }; + frame.payload[..payload_len].copy_from_slice(&buf[HEADER_LEN..total]); + Ok(frame) +} + +fn read_exact(transport: &mut T, mut dst: &mut [u8]) -> Result<(), CodecError> { + while !dst.is_empty() { + match transport.read(dst) { + Ok(0) => return Err(CodecError::Closed), + Ok(n) => dst = &mut dst[n..], + Err(PipeError::WouldBlock) => relax_wait(), + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{MessageType, MAX_FRAME_PAYLOAD}; + + struct MemPipe { + buf: [u8; N], + len: usize, + pos: usize, + } + + impl MemPipe { + fn new() -> Self { + Self { + buf: [0u8; N], + len: 0, + pos: 0, + } + } + } + + impl Write for MemPipe { + fn write(&mut self, src: &[u8]) -> Result { + let n = src.len(); + assert!(self.len + n <= N, "MemPipe full"); + self.buf[self.len..self.len + n].copy_from_slice(src); + self.len += n; + Ok(n) + } + } + + impl Read for MemPipe { + fn read(&mut self, dst: &mut [u8]) -> Result { + if self.pos >= self.len { + return Err(PipeError::WouldBlock); + } + let n = core::cmp::min(dst.len(), self.len - self.pos); + dst[..n].copy_from_slice(&self.buf[self.pos..self.pos + n]); + self.pos += n; + Ok(n) + } + } + + #[test] + fn write_read_round_trip_empty_payload() { + let mut pipe = MemPipe::<256>::new(); + let frame = ControlFrame::new(MessageType::ListenRequest, 7, &[]); + write_frame(&mut pipe, &frame).unwrap(); + + let got = read_frame(&mut pipe).unwrap(); + assert_eq!(got.message_type, MessageType::ListenRequest); + assert_eq!(got.request_id, 7); + assert_eq!(got.payload_len, 0); + } + + #[test] + fn write_read_round_trip_with_payload() { + // The codec is payload-agnostic; verify framing identity with raw bytes. + let pl: [u8; 6] = [127, 0, 0, 1, 0x90, 0x1f]; + let frame = ControlFrame::new(MessageType::ConnectRequest, 99, &pl); + + let mut pipe = MemPipe::<256>::new(); + write_frame(&mut pipe, &frame).unwrap(); + let got = read_frame(&mut pipe).unwrap(); + + assert_eq!(got.message_type, MessageType::ConnectRequest); + assert_eq!(got.request_id, 99); + assert_eq!(got.payload(), &pl); + } + + #[test] + fn write_read_two_frames_in_order() { + let mut pipe = MemPipe::<1024>::new(); + let a = ControlFrame::new(MessageType::ListenRequest, 1, &[1, 2, 3]); + let b = ControlFrame::new(MessageType::ConnectRequest, 2, &[]); + write_frame(&mut pipe, &a).unwrap(); + write_frame(&mut pipe, &b).unwrap(); + + let got_a = read_frame(&mut pipe).unwrap(); + assert_eq!(got_a.request_id, 1); + assert_eq!(got_a.payload(), &[1, 2, 3]); + + let got_b = read_frame(&mut pipe).unwrap(); + assert_eq!(got_b.message_type, MessageType::ConnectRequest); + assert_eq!(got_b.request_id, 2); + assert_eq!(got_b.payload_len, 0); + } + + #[test] + fn write_read_max_sized_payload() { + let payload: [u8; MAX_FRAME_PAYLOAD] = core::array::from_fn(|i| i as u8); + let frame = ControlFrame::new(MessageType::IncomingConnection, 0x1234_5678, &payload); + + let mut pipe = MemPipe::<{ HEADER_LEN + MAX_FRAME_PAYLOAD + 32 }>::new(); + write_frame(&mut pipe, &frame).unwrap(); + let got = read_frame(&mut pipe).unwrap(); + assert_eq!(got.payload_len as usize, MAX_FRAME_PAYLOAD); + assert_eq!(got.payload(), payload.as_slice()); + } + + #[test] + fn read_unknown_message_type() { + let mut pipe = MemPipe::<32>::new(); + let mut header = [0u8; HEADER_LEN]; + header[0] = 99; + header[1..3].copy_from_slice(&0u16.to_le_bytes()); + header[3..7].copy_from_slice(&1u32.to_le_bytes()); + pipe.write(&header).unwrap(); + let err = read_frame(&mut pipe).unwrap_err(); + assert_eq!(err, CodecError::UnknownMessageType(99)); + } + + #[test] + fn frame_read_buf_decodes_back_to_back_frames() { + let pay = 7u32.to_le_bytes(); + let a = ControlFrame::new(MessageType::AcceptRequest, 10, &pay); + let b = ControlFrame::new(MessageType::ListenRequest, 20, &[]); + let mut pipe = MemPipe::<1024>::new(); + write_frame(&mut pipe, &a).unwrap(); + write_frame(&mut pipe, &b).unwrap(); + pipe.pos = 0; + + let mut dec = FrameReadBuf::new(); + let f1 = dec.try_read_frame(&mut pipe).unwrap().unwrap(); + assert_eq!(f1.message_type, MessageType::AcceptRequest); + assert_eq!(f1.request_id, 10); + assert_eq!(f1.payload(), &pay); + + let f2 = dec.try_read_frame(&mut pipe).unwrap().unwrap(); + assert_eq!(f2.message_type, MessageType::ListenRequest); + assert_eq!(f2.request_id, 20); + + assert!(dec.try_read_frame(&mut pipe).unwrap().is_none()); + } + + #[test] + fn frame_read_buf_one_byte_at_a_time() { + let pay = 3u32.to_le_bytes(); + let frame = ControlFrame::new(MessageType::AcceptRequest, 99, &pay); + let mut wire = MemPipe::<256>::new(); + write_frame(&mut wire, &frame).unwrap(); + let encoded_len = wire.len; + + struct OneByte<'a> { + buf: &'a [u8], + pos: usize, + } + + impl<'a> Read for OneByte<'a> { + fn read(&mut self, dst: &mut [u8]) -> Result { + if self.pos >= self.buf.len() { + return Err(PipeError::WouldBlock); + } + dst[0] = self.buf[self.pos]; + self.pos += 1; + Ok(1) + } + } + + let mut reader = OneByte { + buf: &wire.buf[..encoded_len], + pos: 0, + }; + let mut dec = FrameReadBuf::new(); + let mut decoded = None; + for _ in 0..4096 { + if let Some(f) = dec.try_read_frame(&mut reader).unwrap() { + decoded = Some(f); + break; + } + } + let got = decoded.expect("should decode after enough bytes"); + assert_eq!(got.message_type, MessageType::AcceptRequest); + assert_eq!(got.request_id, 99); + assert_eq!(got.payload(), &pay); + } +} diff --git a/control/src/lib.rs b/control/src/lib.rs new file mode 100644 index 0000000..055be52 --- /dev/null +++ b/control/src/lib.rs @@ -0,0 +1,27 @@ +//! Arca ↔ Linux **control protocol**. +//! +//! Layout of this crate: +//! +//! - [`protocol`]: the on-wire types — frame header, message catalog, payload +//! structs ([`ControlFrame`], [`MessageType`], [`Endpoint`], …). +//! - Codec: [`read_frame`] / [`write_frame`] move frames over any +//! `arca_pipe::Read`/`Write` byte transport (typically the dedicated +//! control pipe, an `arca_pipe::BidirectionalPipe`). +//! - Arca side: [`ArcaSession`] owns the control pipe and exposes +//! [`ArcaSession::bind`] / [`ArcaSession::connect`] / [`ArcaSession::accept`] +//! — `accept` sends [`MessageType::AcceptRequest`] and waits for +//! [`MessageType::IncomingConnection`]. +//! +//! `no_std` throughout. The Linux-side counterpart lives in `arca-monitor`. + +#![no_std] + +mod arca_side; +mod codec; +mod message; +pub mod protocol; + +pub use arca_side::{ArcaError, ArcaSession, ArcaTcpListener, ArcaTcpStream}; +pub use codec::{read_frame, write_frame, FrameReadBuf, HEADER_LEN, MAX_WIRE_FRAME_LEN}; +pub use message::{ControlReply, ControlRequest}; +pub use protocol::*; diff --git a/control/src/message.rs b/control/src/message.rs new file mode 100644 index 0000000..6a2ae45 --- /dev/null +++ b/control/src/message.rs @@ -0,0 +1,366 @@ +//! Semantic control messages: the layer above [`crate::codec`]. +//! +//! [`crate::codec`] turns the wire bytestream into [`ControlFrame`]s +//! (header + raw payload); this module turns a frame into a parsed, +//! direction-typed message and back. +//! +//! - [`ControlRequest`]: Arca -> Linux monitor. +//! - [`ControlReply`]: monitor -> Arca. +//! +//! Parsing is fallible (`ControlRequest::try_from(&frame)?`); encoding is +//! infallible (`req.to_frame()`). All payload encode/decode lives in this +//! file. Payload structs in [`crate::protocol`] are pure data. +//! +//! The split keeps framing (how many bytes is one message) independent +//! from semantics (what the message means), so the incremental decoder +//! in [`crate::codec`] never needs to understand payloads. + +use crate::{ + CodecError, ConnectionReady, ControlFrame, DataPipeInfo, Endpoint, MessageType, + MAX_FRAME_PAYLOAD, +}; + +/// A request flowing Arca -> Linux. `request_id` correlates each request +/// with the [`ControlReply`] that answers it. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ControlRequest { + /// `bind`+`listen` on `endpoint`. + Listen { request_id: u32, endpoint: Endpoint }, + /// `connect` outbound to `endpoint`. + Connect { request_id: u32, endpoint: Endpoint }, + /// Wait for the next inbound connection on `listener_id`. + Accept { request_id: u32, listener_id: u32 }, +} + +impl ControlRequest { + /// Correlation id shared with the matching [`ControlReply`]. + pub fn request_id(&self) -> u32 { + match self { + Self::Listen { request_id, .. } + | Self::Connect { request_id, .. } + | Self::Accept { request_id, .. } => *request_id, + } + } + + /// Encode into a ready-to-write [`ControlFrame`]. + pub fn to_frame(&self) -> ControlFrame { + let mut pl = [0u8; MAX_FRAME_PAYLOAD]; + let (mt, n) = match self { + Self::Listen { endpoint, .. } => ( + MessageType::ListenRequest, + write_endpoint(&mut pl, endpoint), + ), + Self::Connect { endpoint, .. } => ( + MessageType::ConnectRequest, + write_endpoint(&mut pl, endpoint), + ), + Self::Accept { listener_id, .. } => { + (MessageType::AcceptRequest, write_u32(&mut pl, *listener_id)) + } + }; + ControlFrame::new(mt, self.request_id(), &pl[..n]) + } +} + +impl TryFrom<&ControlFrame> for ControlRequest { + type Error = CodecError; + + fn try_from(f: &ControlFrame) -> Result { + let request_id = f.request_id; + Ok(match f.message_type { + MessageType::ListenRequest => Self::Listen { + request_id, + endpoint: read_endpoint(f.payload())?, + }, + MessageType::ConnectRequest => Self::Connect { + request_id, + endpoint: read_endpoint(f.payload())?, + }, + MessageType::AcceptRequest => Self::Accept { + request_id, + listener_id: read_u32(f.payload())?, + }, + other => return Err(CodecError::UnexpectedMessage(other)), + }) + } +} + +/// A reply flowing Linux -> Arca, tagged with the originating `request_id`. +/// +/// [`Self::AcceptOk`] is carried on the wire by [`MessageType::IncomingConnection`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ControlReply { + ListenOk { + request_id: u32, + listener_id: u32, + }, + ListenErr { + request_id: u32, + code: u32, + }, + ConnectOk { + request_id: u32, + ready: ConnectionReady, + }, + ConnectErr { + request_id: u32, + code: u32, + }, + AcceptOk { + request_id: u32, + ready: ConnectionReady, + }, + /// Monitor couldn't fulfil an `AcceptRequest` (unknown listener, kernel + /// accept failed). `code` is an errno-like value. + AcceptErr { + request_id: u32, + code: u32, + }, +} + +impl ControlReply { + /// Correlation id of the request this reply answers. + pub fn request_id(&self) -> u32 { + match self { + Self::ListenOk { request_id, .. } + | Self::ListenErr { request_id, .. } + | Self::ConnectOk { request_id, .. } + | Self::ConnectErr { request_id, .. } + | Self::AcceptOk { request_id, .. } + | Self::AcceptErr { request_id, .. } => *request_id, + } + } + + /// The wire message type this reply encodes to. + pub fn message_type(&self) -> MessageType { + match self { + Self::ListenOk { .. } => MessageType::ListenOk, + Self::ListenErr { .. } => MessageType::ListenErr, + Self::ConnectOk { .. } => MessageType::ConnectOk, + Self::ConnectErr { .. } => MessageType::ConnectErr, + Self::AcceptOk { .. } => MessageType::IncomingConnection, + Self::AcceptErr { .. } => MessageType::AcceptErr, + } + } + + /// Encode into a ready-to-write [`ControlFrame`]. + pub fn to_frame(&self) -> ControlFrame { + let mut pl = [0u8; MAX_FRAME_PAYLOAD]; + let n = match self { + Self::ListenOk { listener_id, .. } => write_u32(&mut pl, *listener_id), + Self::ListenErr { code, .. } + | Self::ConnectErr { code, .. } + | Self::AcceptErr { code, .. } => write_u32(&mut pl, *code), + Self::ConnectOk { ready, .. } | Self::AcceptOk { ready, .. } => { + write_ready(&mut pl, ready) + } + }; + ControlFrame::new(self.message_type(), self.request_id(), &pl[..n]) + } +} + +impl TryFrom<&ControlFrame> for ControlReply { + type Error = CodecError; + + fn try_from(f: &ControlFrame) -> Result { + let request_id = f.request_id; + Ok(match f.message_type { + MessageType::ListenOk => Self::ListenOk { + request_id, + listener_id: read_u32(f.payload())?, + }, + MessageType::ListenErr => Self::ListenErr { + request_id, + code: read_u32(f.payload())?, + }, + MessageType::ConnectOk => Self::ConnectOk { + request_id, + ready: read_ready(f.payload())?, + }, + MessageType::ConnectErr => Self::ConnectErr { + request_id, + code: read_u32(f.payload())?, + }, + MessageType::IncomingConnection => Self::AcceptOk { + request_id, + ready: read_ready(f.payload())?, + }, + MessageType::AcceptErr => Self::AcceptErr { + request_id, + code: read_u32(f.payload())?, + }, + other => return Err(CodecError::UnexpectedMessage(other)), + }) + } +} + +// --- payload codec helpers (the only place bytes meet semantic types) --- + +fn take(p: &[u8], n: usize) -> Result<&[u8], CodecError> { + p.get(..n).ok_or(CodecError::ShortPayload { + expected: n, + got: p.len(), + }) +} + +fn read_u32(p: &[u8]) -> Result { + Ok(u32::from_le_bytes(take(p, 4)?.try_into().unwrap())) +} + +fn read_endpoint(p: &[u8]) -> Result { + let p = take(p, 6)?; + Ok(Endpoint::new( + [p[0], p[1], p[2], p[3]], + u16::from_le_bytes([p[4], p[5]]), + )) +} + +fn read_ready(p: &[u8]) -> Result { + let p = take(p, 24)?; + Ok(ConnectionReady { + listener_id: u32::from_le_bytes(p[0..4].try_into().unwrap()), + connection_id: u32::from_le_bytes(p[4..8].try_into().unwrap()), + pipe: DataPipeInfo::new( + u64::from_le_bytes(p[8..16].try_into().unwrap()), + u64::from_le_bytes(p[16..24].try_into().unwrap()), + ), + }) +} + +fn write_u32(out: &mut [u8], v: u32) -> usize { + out[..4].copy_from_slice(&v.to_le_bytes()); + 4 +} + +fn write_endpoint(out: &mut [u8], ep: &Endpoint) -> usize { + out[..4].copy_from_slice(&ep.host); + out[4..6].copy_from_slice(&ep.port.to_le_bytes()); + 6 +} + +fn write_ready(out: &mut [u8], r: &ConnectionReady) -> usize { + out[..4].copy_from_slice(&r.listener_id.to_le_bytes()); + out[4..8].copy_from_slice(&r.connection_id.to_le_bytes()); + out[8..16].copy_from_slice(&r.pipe.shm_offset.to_le_bytes()); + out[16..24].copy_from_slice(&r.pipe.ring_size.to_le_bytes()); + 24 +} + +#[cfg(test)] +mod tests { + use super::*; + + fn sample_ready() -> ConnectionReady { + ConnectionReady { + listener_id: 3, + connection_id: 9, + pipe: DataPipeInfo::new(9, 128), + } + } + + #[test] + fn request_round_trip_through_frame() { + for req in [ + ControlRequest::Listen { + request_id: 1, + endpoint: Endpoint::new([127, 0, 0, 1], 8080), + }, + ControlRequest::Connect { + request_id: 2, + endpoint: Endpoint::new([8, 8, 8, 8], 443), + }, + ControlRequest::Accept { + request_id: 3, + listener_id: 77, + }, + ] { + let frame = req.to_frame(); + assert_eq!(frame.request_id, req.request_id()); + assert_eq!(ControlRequest::try_from(&frame).unwrap(), req); + } + } + + #[test] + fn reply_round_trip_through_frame() { + for reply in [ + ControlReply::ListenOk { + request_id: 1, + listener_id: 5, + }, + ControlReply::ListenErr { + request_id: 2, + code: 98, + }, + ControlReply::ConnectOk { + request_id: 3, + ready: sample_ready(), + }, + ControlReply::ConnectErr { + request_id: 4, + code: 111, + }, + ControlReply::AcceptOk { + request_id: 5, + ready: sample_ready(), + }, + ControlReply::AcceptErr { + request_id: 6, + code: 9, + }, + ] { + let frame = reply.to_frame(); + assert_eq!(frame.request_id, reply.request_id()); + assert_eq!(frame.message_type, reply.message_type()); + assert_eq!(ControlReply::try_from(&frame).unwrap(), reply); + } + } + + #[test] + fn accept_ok_uses_incoming_connection_wire_type() { + let f = ControlReply::AcceptOk { + request_id: 7, + ready: sample_ready(), + } + .to_frame(); + assert_eq!(f.message_type, MessageType::IncomingConnection); + } + + #[test] + fn rejects_wrong_direction_message_type() { + let req_frame = ControlRequest::Accept { + request_id: 1, + listener_id: 2, + } + .to_frame(); + assert_eq!( + ControlReply::try_from(&req_frame), + Err(CodecError::UnexpectedMessage(MessageType::AcceptRequest)) + ); + let reply_frame = ControlReply::ListenOk { + request_id: 1, + listener_id: 5, + } + .to_frame(); + assert_eq!( + ControlRequest::try_from(&reply_frame), + Err(CodecError::UnexpectedMessage(MessageType::ListenOk)) + ); + } + + #[test] + fn try_from_short_payload_errors() { + let mut frame = ControlRequest::Listen { + request_id: 1, + endpoint: Endpoint::new([0, 0, 0, 0], 0), + } + .to_frame(); + frame.payload_len = 3; + assert_eq!( + ControlRequest::try_from(&frame), + Err(CodecError::ShortPayload { + expected: 6, + got: 3 + }) + ); + } +} diff --git a/control/src/protocol.rs b/control/src/protocol.rs new file mode 100644 index 0000000..ecbe2a8 --- /dev/null +++ b/control/src/protocol.rs @@ -0,0 +1,215 @@ +//! Wire-protocol data types for the **single control pipe** between Arca +//! and the Linux monitor. +//! +//! This module is **pure data**: the framing/encoding live in +//! [`crate::codec`] and [`crate::message`]. The frame is the only thing +//! that flows across the control pipe. +//! +//! ```text +//! offset size field +//! ------ ---- ----------------------------------------- +//! 0 1 message_type (u8, see MessageType) +//! 1 2 payload_len (u16 little-endian, bytes) +//! 3 4 request_id (u32 little-endian) +//! 7 .. payload (payload_len bytes; fixed-layout little-endian) +//! ``` +//! +//! Per-connection bytestreams flow on **separate** shared-memory data pipes; +//! the receiver finds them via [`DataPipeInfo`] carried in the +//! [`crate::ControlReply::ConnectOk`] / [`crate::ControlReply::AcceptOk`] +//! payloads. + +use arca_pipe::BidirectionalPipe; + +/// Maximum payload bytes after the 7-byte header. +/// +/// Sized comfortably above today's largest payload (`ConnectionReady`, 24 B) +/// so we have headroom to add fields without bumping a version byte. +pub const MAX_FRAME_PAYLOAD: usize = 256; + +/// Errors from moving frames over a transport and from parsing them into +/// semantically meaningful messages. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CodecError { + /// Frame header named a message-type byte we don't recognize. + UnknownMessageType(u8), + /// Declared payload length exceeds [`MAX_FRAME_PAYLOAD`]. + PayloadTooLarge { len: usize }, + /// Transport returned `Ok(0)` — the peer hung up. + Closed, + /// Payload was shorter than the fixed layout its message requires. + ShortPayload { expected: usize, got: usize }, + /// Message type is valid on the wire but illegal in this direction + /// (e.g. a reply parsed as a request, or vice versa). + UnexpectedMessage(MessageType), +} + +/// Catalog of message kinds carried on the control pipe. +/// +/// Keep this list **small** and add new variants only when there's a real +/// need. Each variant is a single byte on the wire. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum MessageType { + ListenRequest = 1, + ListenOk = 2, + ConnectRequest = 3, + ConnectOk = 4, + /// Reply to [`MessageType::AcceptRequest`]; payload is `ConnectionReady`. + IncomingConnection = 5, + ListenErr = 6, + ConnectErr = 7, + AcceptRequest = 8, + /// Linux → Arca: reply to [`MessageType::AcceptRequest`] when the + /// monitor couldn't fulfil it (unknown listener, kernel accept failed). + AcceptErr = 9, +} + +impl MessageType { + /// Reverse of `as u8`, returning `None` for unknown bytes so the codec + /// can reject garbage instead of silently misinterpreting it. + pub fn from_u8(v: u8) -> Option { + match v { + 1 => Some(Self::ListenRequest), + 2 => Some(Self::ListenOk), + 3 => Some(Self::ConnectRequest), + 4 => Some(Self::ConnectOk), + 5 => Some(Self::IncomingConnection), + 6 => Some(Self::ListenErr), + 7 => Some(Self::ConnectErr), + 8 => Some(Self::AcceptRequest), + 9 => Some(Self::AcceptErr), + _ => None, + } + } +} + +/// One control message in memory (header + inline payload buffer). +/// +/// The `payload` array is fixed-size so `ControlFrame` is `Copy` and lives +/// happily in `no_std`. Only the first `payload_len` bytes are valid; use +/// [`ControlFrame::payload`] to get the live slice. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ControlFrame { + pub message_type: MessageType, + pub request_id: u32, + pub payload_len: u16, + pub payload: [u8; MAX_FRAME_PAYLOAD], +} + +impl ControlFrame { + /// Build a frame, copying `payload` into the inline buffer. + /// + /// Panics if `payload.len() > MAX_FRAME_PAYLOAD` — callers always own + /// the payload, so this is a programming bug, not runtime input. + pub fn new(message_type: MessageType, request_id: u32, payload: &[u8]) -> Self { + assert!(payload.len() <= MAX_FRAME_PAYLOAD, "payload too large"); + let mut out = Self { + message_type, + request_id, + payload_len: payload.len() as u16, + payload: [0u8; MAX_FRAME_PAYLOAD], + }; + out.payload[..payload.len()].copy_from_slice(payload); + out + } + + /// The valid prefix of `self.payload`. + pub fn payload(&self) -> &[u8] { + &self.payload[..self.payload_len as usize] + } +} + +/// IPv4 host + port. Six bytes on the wire: 4 octets then `port` LE. +/// +/// IPv6 isn't modeled yet; when we need it we'll add a sibling type or a +/// length-prefixed address field. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Endpoint { + pub host: [u8; 4], + pub port: u16, +} + +impl Endpoint { + pub fn new(host: [u8; 4], port: u16) -> Self { + Self { host, port } + } +} + +/// How Arca finds the per-connection data pipe. +/// +/// Layout on the wire (16 bytes): `shm_offset` (u64 LE) then `ring_size` (u64 LE). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct DataPipeInfo { + /// BuddyAllocator offset from the allocator base to the SHM region backing + /// this pipe. Pass to `BuddyAllocator.from_offset()` to get a pointer. + pub shm_offset: u64, + /// Per-direction ring capacity in bytes (same value passed to + /// [`BidirectionalPipe::new`]). + pub ring_size: u64, +} + +impl DataPipeInfo { + pub fn new(shm_offset: u64, ring_size: u64) -> Self { + Self { shm_offset, ring_size } + } + + /// Total shared-memory bytes the receiver must map for this pipe. + pub fn shm_len(self) -> u64 { + BidirectionalPipe::required_size(self.ring_size) + } +} + +/// Payload of [`crate::ControlReply::ConnectOk`] (outbound) and +/// [`crate::ControlReply::AcceptOk`] (inbound). Same fields, same layout — +/// the message kind says which is which. +/// +/// `listener_id == 0` means "outbound connection, no listener was involved". +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ConnectionReady { + pub listener_id: u32, + pub connection_id: u32, + pub pipe: DataPipeInfo, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn message_type_from_u8_round_trip() { + for mt in [ + MessageType::ListenRequest, + MessageType::ListenOk, + MessageType::ConnectRequest, + MessageType::ConnectOk, + MessageType::IncomingConnection, + MessageType::ListenErr, + MessageType::ConnectErr, + MessageType::AcceptRequest, + MessageType::AcceptErr, + ] { + assert_eq!(MessageType::from_u8(mt as u8), Some(mt)); + } + } + + #[test] + fn message_type_from_u8_unknown() { + assert_eq!(MessageType::from_u8(0), None); + assert_eq!(MessageType::from_u8(10), None); + assert_eq!(MessageType::from_u8(255), None); + } + + #[test] + fn control_frame_payload_slice() { + let frame = ControlFrame::new(MessageType::ListenRequest, 0, &[10, 20]); + assert_eq!(frame.payload(), &[10, 20]); + } + + #[test] + fn data_pipe_info_shm_len_matches_bidirectional_pipe() { + let ring_size = 64u64; + let info = DataPipeInfo::new(1, ring_size); + assert_eq!(info.shm_len(), BidirectionalPipe::required_size(ring_size)); + } +} diff --git a/data-pipe/Cargo.lock b/data-pipe/Cargo.lock new file mode 100644 index 0000000..94f34f9 --- /dev/null +++ b/data-pipe/Cargo.lock @@ -0,0 +1,14 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "arca-pipe" +version = "0.1.0" + +[[package]] +name = "tcp" +version = "0.1.0" +dependencies = [ + "arca-pipe", +] diff --git a/data-pipe/Cargo.toml b/data-pipe/Cargo.toml new file mode 100644 index 0000000..f5e22f2 --- /dev/null +++ b/data-pipe/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "tcp" +version = "0.1.0" +edition = "2021" + +[dependencies] +arca-pipe = { path = "../pipe" } diff --git a/data-pipe/src/async_stream.rs b/data-pipe/src/async_stream.rs new file mode 100644 index 0000000..9336b7b --- /dev/null +++ b/data-pipe/src/async_stream.rs @@ -0,0 +1,90 @@ +use arca_pipe::{BidirectionalPipe, PipeError}; +use arca_pipe::Read as PipeRead; +use arca_pipe::Write as PipeWrite; + +#[derive(Debug)] +pub enum StreamError { + WriteClosed, +} + +pub struct AsyncStream<'a> { + /// BuddyAllocator offset of the SHM region backing this pipe. + pub shm_offset: u64, + pipe: BidirectionalPipe<'a>, +} + +impl<'a> AsyncStream<'a> { + pub fn from_pipe(shm_offset: u64, pipe: BidirectionalPipe<'a>) -> Self { + Self { shm_offset, pipe } + } + + /// Write all of `buf` into the pipe, yielding if the ring is full; returns `Err(WriteClosed)` if the peer closed their read side. + pub async fn send(&mut self, buf: &[u8]) -> Result { + if self.pipe.is_peer_read_closed() { + self.pipe.close_write(); + return Err(StreamError::WriteClosed); + } + if buf.is_empty() { + return Ok(0); + } + write_all(&mut self.pipe, buf).await; + Ok(buf.len()) + } + + /// Read exactly `buf.len()` bytes, yielding until full; returns `Ok(n < buf.len())` only on EOF when the peer closed their write side. + pub async fn recv(&mut self, buf: &mut [u8]) -> Result { + let n = read_exact(&mut self.pipe, buf).await; + if n < buf.len() { + self.pipe.close_read(); + } + Ok(n) + } + + pub fn close_write(&mut self) { + self.pipe.close_write(); + } + + pub fn close_read(&mut self) { + self.pipe.close_read(); + } + + pub fn is_closed(&self) -> bool { + self.pipe.is_closed() + } +} + +async fn read_exact(pipe: &mut arca_pipe::BidirectionalPipe<'_>, buf: &mut [u8]) -> usize { + let mut filled = 0; + while filled < buf.len() { + match pipe.read(&mut buf[filled..]) { + Ok(n) => filled += n, + Err(PipeError::WouldBlock) => { + if pipe.is_peer_write_closed() { + break; + } + yield_now().await; + } + } + } + filled +} + +async fn write_all(pipe: &mut W, buf: &[u8]) { + let mut remaining = buf; + while !remaining.is_empty() { + match pipe.write(remaining) { + Ok(n) => remaining = &remaining[n..], + Err(PipeError::WouldBlock) => yield_now().await, + } + } +} + +async fn yield_now() { + let mut yielded = false; + core::future::poll_fn(|cx| { + if yielded { return core::task::Poll::Ready(()); } + yielded = true; + cx.waker().wake_by_ref(); + core::task::Poll::Pending + }).await +} diff --git a/data-pipe/src/lib.rs b/data-pipe/src/lib.rs new file mode 100644 index 0000000..6376728 --- /dev/null +++ b/data-pipe/src/lib.rs @@ -0,0 +1,4 @@ +pub mod sync_stream; +pub mod async_stream; +pub use sync_stream::SyncStream; +pub use async_stream::AsyncStream; diff --git a/data-pipe/src/sync_stream.rs b/data-pipe/src/sync_stream.rs new file mode 100644 index 0000000..a3bd398 --- /dev/null +++ b/data-pipe/src/sync_stream.rs @@ -0,0 +1,157 @@ +use arca_pipe::{BidirectionalPipe, PipeError, Read, Write}; + +#[derive(Debug)] +pub enum StreamError { + WriteClosed, +} + +pub struct SyncStream<'a> { + /// BuddyAllocator offset of the SHM region backing this pipe. + pub shm_offset: u64, + pipe: BidirectionalPipe<'a>, +} + +impl<'a> SyncStream<'a> { + pub fn from_pipe(shm_offset: u64, pipe: BidirectionalPipe<'a>) -> Self { + Self { shm_offset, pipe } + } + + /// Write all of `buf` into the pipe, spinning if the ring is full; returns `Err(WriteClosed)` if the peer closed their read side. + pub fn send(&mut self, buf: &[u8]) -> Result { + if self.pipe.is_peer_read_closed() { + self.pipe.close_write(); + return Err(StreamError::WriteClosed); + } + if buf.is_empty() { + return Ok(0); + } + self.pipe.write_all(buf); + Ok(buf.len()) + } + + /// Read exactly `buf.len()` bytes, spinning until full; returns `Ok(n < buf.len())` only on EOF when the peer closed their write side. + pub fn recv(&mut self, buf: &mut [u8]) -> Result { + let n = read_exact(&mut self.pipe, buf); + if n < buf.len() { + self.pipe.close_read(); + } + Ok(n) + } + + pub fn close_write(&mut self) { + self.pipe.close_write(); + } + + pub fn close_read(&mut self) { + self.pipe.close_read(); + } + + pub fn is_closed(&self) -> bool { + self.pipe.is_closed() + } +} + +fn read_exact(pipe: &mut arca_pipe::BidirectionalPipe, buf: &mut [u8]) -> usize { + let mut filled = 0; + while filled < buf.len() { + match pipe.read(&mut buf[filled..]) { + Ok(n) => filled += n, + Err(PipeError::WouldBlock) => { + if pipe.is_peer_write_closed() { + break; + } + } + } + } + filled +} + + +#[cfg(test)] +mod tests { + use super::*; + use arca_pipe::{BidirectionalPipe, SharedMemoryRegion, Side}; + + #[repr(align(8))] + struct Aligned([u8; N]); + + macro_rules! stream_pair { + ($ring:expr, $mem:ident, $a:ident, $b:ident) => { + let mut $mem = Aligned([0u8; BidirectionalPipe::required_size($ring as u64) as usize]); + let region = unsafe { + SharedMemoryRegion::from_raw($mem.0.as_mut_ptr(), $mem.0.len() as u64) + }; + let pipe_a = BidirectionalPipe::new(®ion, $ring, Side::A); + let pipe_b = BidirectionalPipe::new(®ion, $ring, Side::B); + let mut $a = SyncStream::from_pipe(0,pipe_a); + let mut $b = SyncStream::from_pipe(0,pipe_b); + }; + } + + #[test] + fn send_recv_data() { + stream_pair!(128, mem, a, b); + assert_eq!(a.send(b"hello").unwrap(), 5); + let mut buf = [0u8; 5]; + assert_eq!(b.recv(&mut buf).unwrap(), 5); + assert_eq!(&buf, b"hello"); + } + + #[test] + fn close_write_signals_eof_to_peer() { + stream_pair!(64, mem, a, b); + a.close_write(); + let mut buf = [0u8; 8]; + assert_eq!(b.recv(&mut buf).unwrap(), 0); + assert!(!b.is_closed()); + } + + #[test] + fn close_both_sides_blocks_peer_ops() { + stream_pair!(64, mem, a, b); + b.close_write(); + b.close_read(); + // b has closed its own ends but a hasn't yet — pipe not fully closed + assert!(!b.is_closed()); + let mut buf = [0u8; 8]; + // a sees EOF because b closed write, and WriteClosed because b closed read + assert_eq!(a.recv(&mut buf).unwrap(), 0); + assert!(matches!(a.send(b"x"), Err(StreamError::WriteClosed))); + } + + #[test] + fn send_after_peer_closes_read_errors() { + stream_pair!(64, mem, a, b); + b.close_read(); + assert!(matches!(a.send(b"x"), Err(StreamError::WriteClosed))); + } + + #[test] + fn recv_after_eof_returns_zero() { + stream_pair!(64, mem, a, b); + a.close_write(); + let mut buf = [0u8; 8]; + b.recv(&mut buf).unwrap(); + assert_eq!(b.recv(&mut buf).unwrap(), 0); + } + + #[test] + fn recv_fills_exact_buffer_size() { + stream_pair!(128, mem, a, b); + assert_eq!(a.send(b"hello").unwrap(), 5); + let mut buf = [0u8; 5]; + assert_eq!(b.recv(&mut buf).unwrap(), 5); + assert_eq!(&buf, b"hello"); + } + + #[test] + fn pipe_closed_after_both_sides_close() { + stream_pair!(128, mem, a, b); + a.close_write(); + let mut buf = [0u8; 8]; + b.recv(&mut buf).unwrap(); + b.close_write(); + a.recv(&mut buf).unwrap(); + assert!(a.is_closed()); + } +} diff --git a/monitor/Cargo.toml b/monitor/Cargo.toml new file mode 100644 index 0000000..40cc342 --- /dev/null +++ b/monitor/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "arca-monitor" +version = "0.1.0" +edition = "2021" + +[dependencies] +arca-control = { path = "../control" } +arca-pipe = { path = "../pipe" } diff --git a/monitor/src/lib.rs b/monitor/src/lib.rs new file mode 100644 index 0000000..76916c2 --- /dev/null +++ b/monitor/src/lib.rs @@ -0,0 +1,669 @@ +#![feature(allocator_api)] +//! Linux-side **monitor**: holds the kernel sockets and translates Arca's +//! control-protocol requests into real `TcpListener` / `TcpStream` actions. +//! +//! Architecture in one paragraph: +//! +//! - The monitor owns one [`TcpListener`] per `ControlReply::ListenOk` it +//! has handed out, and one [`TcpStream`] per live connection. +//! - Listeners are kept **non-blocking** so [`Monitor::poll_accepts`] can probe +//! the kernel when Arca has queued an accept wait, without wedging the I/O +//! thread when no TCP peer is ready yet. +//! - Connection streams are left in their default mode — the *byte pump* +//! (the data-pipe layer) decides blocking vs non-blocking based on its own +//! scheduling needs. +//! +//! Driving the protocol is one of: +//! - [`Monitor::dispatch_request`] — `Listen` / `Connect` only (for tests and +//! custom drivers); [`MessageType::AcceptRequest`] is handled in +//! [`Monitor::pump_once`] / [`Monitor::serve_one`]. +//! - [`Monitor::pump_once`] — non-blocking kernel accepts + try read every +//! fully received Arca→Linux frame on `transport`. +//! - [`Monitor::serve_one`] — spins until a full Arca→Linux frame exists, then +//! dispatches it (uses [`std::thread::yield_now`] while waiting). + +mod relay; + +pub use relay::{pipe_to_tcp, tcp_to_pipe}; + +use std::collections::{HashMap, VecDeque}; +use std::io; +use std::net::{Ipv4Addr, SocketAddr, TcpListener, TcpStream}; + +use arca_control::{ + write_frame, CodecError, ConnectionReady, ControlFrame, ControlReply, ControlRequest, + DataPipeInfo, FrameReadBuf, MessageType, +}; +use arca_pipe::{BidirectionalPipe, Read, SharedMemoryRegion, Side, Write}; +use common::BuddyAllocator; + +/// Errno-like code we send to the guest in [`ControlReply::AcceptErr`] when +/// the request referenced a `listener_id` we don't know about (closest match +/// to Linux's `EBADF`). +const ERR_UNKNOWN_LISTENER: u32 = 9; + +#[derive(Debug)] +pub enum MonitorError { + Io(io::Error), + Codec(CodecError), + UnexpectedRequest(MessageType), +} + +impl From for MonitorError { + fn from(value: io::Error) -> Self { + Self::Io(value) + } +} + +impl From for MonitorError { + fn from(value: CodecError) -> Self { + Self::Codec(value) + } +} + +fn io_err_code(e: &io::Error) -> u32 { + // Linux `errno` when available, else `1` to mean "unknown error". + e.raw_os_error().map(|x| x as u32).unwrap_or(1) +} + +/// One live TCP connection plus its data pipe on the monitor side. +pub struct Connection { + pub tcp: TcpStream, + /// Owns the SHM allocation backing the BidirectionalPipe. + /// Set to None when the pipe is fully closed to free the region; + /// the TCP socket stays alive until the OS-level connection closes. + pub shm: Option>, +} + +/// Linux-side state machine. +pub struct Monitor { + next_listener_id: u32, + next_connection_id: u32, + default_ring_size: u64, + listeners: HashMap, + connections: HashMap, + /// For each listener, FIFO of Arca `request_id`s waiting for a kernel `accept`. + pending_accepts: HashMap>, + control_rx: FrameReadBuf, +} + +impl Monitor { + pub fn new(default_ring_size: u64) -> Self { + Self { + // 0 is reserved as "no listener" sentinel for outbound connects. + next_listener_id: 1, + next_connection_id: 1, + default_ring_size, + listeners: HashMap::new(), + connections: HashMap::new(), + pending_accepts: HashMap::new(), + control_rx: FrameReadBuf::new(), + } + } + + /// Translate one Arca → Linux request frame into the reply we owe Arca. + /// + /// Handles only [`MessageType::ListenRequest`] and + /// [`MessageType::ConnectRequest`]. [`MessageType::AcceptRequest`] is + /// handled in [`Monitor::handle_control_frame`]. + pub fn dispatch_request(&mut self, frame: ControlFrame) -> Result { + let request = ControlRequest::try_from(&frame)?; + let rid = request.request_id(); + match request { + ControlRequest::Listen { endpoint, .. } => { + let addr = SocketAddr::from((Ipv4Addr::from(endpoint.host), endpoint.port)); + let reply = match TcpListener::bind(addr) { + Ok(listener) => { + listener.set_nonblocking(true)?; + let id = self.alloc_listener_id(); + self.listeners.insert(id, listener); + ControlReply::ListenOk { + request_id: rid, + listener_id: id, + } + } + Err(e) => ControlReply::ListenErr { + request_id: rid, + code: io_err_code(&e), + }, + }; + Ok(reply.to_frame()) + } + ControlRequest::Connect { endpoint, .. } => { + let addr = SocketAddr::from((Ipv4Addr::from(endpoint.host), endpoint.port)); + // Blocks until the kernel handshake completes — “connect waits”. + let reply = match TcpStream::connect(addr) { + Ok(stream) => { + let id = self.alloc_connection_id(); + let shm: Box<[u8], BuddyAllocator> = unsafe { + Box::new_zeroed_slice_in( + BidirectionalPipe::required_size(self.default_ring_size) as usize, + BuddyAllocator, + ) + .assume_init() + }; + let shm_offset = + BuddyAllocator.to_offset(shm.as_ptr() as *const ()) as u64; + self.connections.insert(id, Connection { tcp: stream, shm: Some(shm) }); + ControlReply::ConnectOk { + request_id: rid, + ready: ConnectionReady { + listener_id: 0, + connection_id: id, + pipe: DataPipeInfo::new(shm_offset, self.default_ring_size), + }, + } + } + Err(e) => ControlReply::ConnectErr { + request_id: rid, + code: io_err_code(&e), + }, + }; + Ok(reply.to_frame()) + } + ControlRequest::Accept { .. } => { + Err(MonitorError::UnexpectedRequest(MessageType::AcceptRequest)) + } + } + } + + /// Try pairing pending Arca `AcceptRequest`s with kernel `accept` results, + /// writing one [`MessageType::IncomingConnection`] per successful accept + /// (each carrying the Arca-issued `request_id`). + pub fn poll_accepts(&mut self, transport: &mut T) -> Result { + use std::io::ErrorKind; + let mut written = 0usize; + let lids: Vec = self + .pending_accepts + .iter() + .filter(|(_, q)| !q.is_empty()) + .map(|(k, _)| *k) + .collect(); + for lid in lids { + let Some(listener) = self.listeners.get_mut(&lid) else { + continue; + }; + match listener.accept() { + Ok((stream, _)) => { + let Some(rid) = self + .pending_accepts + .get_mut(&lid) + .and_then(|q| q.pop_front()) + else { + continue; + }; + let cid = self.alloc_connection_id(); + let shm: Box<[u8], BuddyAllocator> = unsafe { + Box::new_zeroed_slice_in( + BidirectionalPipe::required_size(self.default_ring_size) as usize, + BuddyAllocator, + ) + .assume_init() + }; + let shm_offset = + BuddyAllocator.to_offset(shm.as_ptr() as *const ()) as u64; + self.connections.insert(cid, Connection { tcp: stream, shm: Some(shm) }); + let reply = ControlReply::AcceptOk { + request_id: rid, + ready: ConnectionReady { + listener_id: lid, + connection_id: cid, + pipe: DataPipeInfo::new(shm_offset, self.default_ring_size), + }, + }; + write_frame(transport, &reply.to_frame())?; + written += 1; + } + Err(e) if e.kind() == ErrorKind::WouldBlock => {} + Err(e) => { + // Kernel `accept` failed for a reason other than + // `WouldBlock`. Tell the next waiter on this listener + // instead of dropping the error on the floor. + if let Some(rid) = self + .pending_accepts + .get_mut(&lid) + .and_then(|q| q.pop_front()) + { + let reply = ControlReply::AcceptErr { + request_id: rid, + code: io_err_code(&e), + }; + write_frame(transport, &reply.to_frame())?; + written += 1; + } + } + } + } + Ok(written) + } + + /// Non-blocking progress: poll kernel accepts, then read and handle every + /// fully received Arca→Linux frame currently available on `transport`. + pub fn pump_once(&mut self, transport: &mut T) -> Result<(), MonitorError> { + self.poll_accepts(transport)?; + while let Some(frame) = self.control_rx.try_read_frame(transport)? { + self.handle_control_frame(transport, frame)?; + } + Ok(()) + } + + fn handle_control_frame( + &mut self, + transport: &mut T, + frame: ControlFrame, + ) -> Result<(), MonitorError> { + match ControlRequest::try_from(&frame)? { + ControlRequest::Accept { + request_id, + listener_id, + } => { + if !self.listeners.contains_key(&listener_id) { + let reply = ControlReply::AcceptErr { + request_id, + code: ERR_UNKNOWN_LISTENER, + }; + write_frame(transport, &reply.to_frame())?; + return Ok(()); + } + self.pending_accepts + .entry(listener_id) + .or_default() + .push_back(request_id); + self.poll_accepts(transport)?; + Ok(()) + } + _ => { + let reply = self.dispatch_request(frame)?; + write_frame(transport, &reply)?; + self.poll_accepts(transport)?; + Ok(()) + } + } + } + + /// Read and dispatch one Arca→Linux frame. Spins with + /// [`std::thread::yield_now`] until the incremental decoder can produce a + /// full frame (transport keeps returning [`arca_pipe::PipeError::WouldBlock`]). + pub fn serve_one(&mut self, transport: &mut T) -> Result<(), MonitorError> { + loop { + self.poll_accepts(transport)?; + if let Some(frame) = self.control_rx.try_read_frame(transport)? { + return self.handle_control_frame(transport, frame); + } + std::thread::yield_now(); + } + } + + /// Borrow a live connection's `TcpStream` for the byte pump. + pub fn connection(&mut self, id: u32) -> Option<&mut TcpStream> { + self.connections.get_mut(&id).map(|c| &mut c.tcp) + } + + /// Main event loop: pumps control frames and relays bytes between each + /// live TcpStream and its data pipe until an error occurs. + pub fn event_loop(&mut self, control_pipe: &mut T) -> Result<(), MonitorError> { + loop { + self.pump_once(control_pipe)?; + for (_, conn) in &mut self.connections { + let Some(shm) = &conn.shm else { continue }; + let region = unsafe { + SharedMemoryRegion::from_raw(shm.as_ptr() as *mut u8, shm.len() as u64) + }; + let mut pipe = BidirectionalPipe::new(®ion, self.default_ring_size, Side::B); + tcp_to_pipe(&mut conn.tcp, &mut pipe).ok(); + pipe_to_tcp(&mut conn.tcp, &mut pipe).ok(); + if pipe.is_closed() { + // Drop the Box — BuddyAllocator::deallocate runs automatically, + // returning the SHM region to the buddy allocator. + conn.shm = None; + } + } + std::thread::yield_now(); + } + } + + /// Borrow a live listener (mostly for tests / introspection). + pub fn listener(&self, id: u32) -> Option<&TcpListener> { + self.listeners.get(&id) + } + + fn alloc_listener_id(&mut self) -> u32 { + let id = self.next_listener_id; + self.next_listener_id = self.next_listener_id.wrapping_add(1); + if self.next_listener_id == 0 { + self.next_listener_id = 1; + } + id + } + + fn alloc_connection_id(&mut self) -> u32 { + let id = self.next_connection_id; + self.next_connection_id = self.next_connection_id.wrapping_add(1); + if self.next_connection_id == 0 { + self.next_connection_id = 1; + } + id + } +} + +#[cfg(test)] +impl Monitor { + pub(crate) fn test_enqueue_accept(&mut self, listener_id: u32, rid: u32) { + self.pending_accepts + .entry(listener_id) + .or_default() + .push_back(rid); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use arca_control::{read_frame, Endpoint}; + use arca_pipe::{PipeError, Read as PipeRead, Write as PipeWrite}; + use std::io::{Read as IoRead, Write as IoWrite}; + use std::sync::mpsc; + use std::thread; + use std::time::Duration; + + struct VecWriter(Vec); + + impl PipeWrite for VecWriter { + fn write(&mut self, buf: &[u8]) -> Result { + self.0.extend_from_slice(buf); + Ok(buf.len()) + } + } + + /// In-memory control pipe: pops inbound bytes, collects outbound writes. + struct QueuePipe { + inbound: std::collections::VecDeque, + outbound: Vec, + } + + impl PipeRead for QueuePipe { + fn read(&mut self, buf: &mut [u8]) -> Result { + if self.inbound.is_empty() { + return Err(PipeError::WouldBlock); + } + let n = std::cmp::min(buf.len(), self.inbound.len()); + for i in 0..n { + buf[i] = self.inbound.pop_front().unwrap(); + } + Ok(n) + } + } + + impl PipeWrite for QueuePipe { + fn write(&mut self, buf: &[u8]) -> Result { + self.outbound.extend_from_slice(buf); + Ok(buf.len()) + } + } + + #[test] + fn accept_for_unknown_listener_replies_with_accept_err() { + use arca_control::write_frame; + let mut m = Monitor::new(64); + + // Accept on a listener_id we never handed out. + let acc = ControlRequest::Accept { + request_id: 55, + listener_id: 999, + } + .to_frame(); + let mut enc = VecWriter(Vec::new()); + write_frame(&mut enc, &acc).unwrap(); + + let mut transport = QueuePipe { + inbound: std::collections::VecDeque::from(enc.0), + outbound: Vec::new(), + }; + m.pump_once(&mut transport).unwrap(); + + // Monitor should have written an AcceptErr frame back to the guest. + struct SliceReader<'a> { + data: &'a [u8], + pos: usize, + } + impl PipeRead for SliceReader<'_> { + fn read(&mut self, buf: &mut [u8]) -> Result { + if self.pos >= self.data.len() { + return Err(PipeError::WouldBlock); + } + let n = std::cmp::min(buf.len(), self.data.len() - self.pos); + buf[..n].copy_from_slice(&self.data[self.pos..self.pos + n]); + self.pos += n; + Ok(n) + } + } + let mut r = SliceReader { + data: &transport.outbound, + pos: 0, + }; + let fr = read_frame(&mut r).unwrap(); + let ControlReply::AcceptErr { + request_id: 55, + code, + } = ControlReply::try_from(&fr).unwrap() + else { + panic!("expected AcceptErr(rid=55)"); + }; + assert_eq!(code, ERR_UNKNOWN_LISTENER); + } + + #[test] + fn pump_once_reads_accept_request_then_tcp_pairs_request_id() { + use arca_control::write_frame; + let mut m = Monitor::new(64); + let listen = ControlRequest::Listen { + request_id: 1, + endpoint: Endpoint::new([127, 0, 0, 1], 0), + }; + let reply = m.dispatch_request(listen.to_frame()).unwrap(); + let ControlReply::ListenOk { + listener_id: lid, .. + } = ControlReply::try_from(&reply).unwrap() + else { + panic!("expected ListenOk"); + }; + + let acc = ControlRequest::Accept { + request_id: 77, + listener_id: lid, + } + .to_frame(); + let mut enc = VecWriter(Vec::new()); + write_frame(&mut enc, &acc).unwrap(); + + let mut transport = QueuePipe { + inbound: std::collections::VecDeque::from(enc.0), + outbound: Vec::new(), + }; + m.pump_once(&mut transport).unwrap(); + + let port = m + .listener(lid) + .expect("listener") + .local_addr() + .unwrap() + .port(); + thread::spawn(move || { + thread::sleep(Duration::from_millis(30)); + let _ = TcpStream::connect(("127.0.0.1", port)); + }); + thread::sleep(Duration::from_millis(60)); + + let mut w = VecWriter(Vec::new()); + assert_eq!(m.poll_accepts(&mut w).unwrap(), 1); + + struct FrameSliceReader<'a> { + data: &'a [u8], + pos: usize, + } + impl PipeRead for FrameSliceReader<'_> { + fn read(&mut self, buf: &mut [u8]) -> Result { + if self.pos >= self.data.len() { + return Err(PipeError::WouldBlock); + } + let n = std::cmp::min(buf.len(), self.data.len() - self.pos); + buf[..n].copy_from_slice(&self.data[self.pos..self.pos + n]); + self.pos += n; + Ok(n) + } + } + let mut r = FrameSliceReader { data: &w.0, pos: 0 }; + let fr = read_frame(&mut r).unwrap(); + let ControlReply::AcceptOk { + request_id: 77, + ready, + } = ControlReply::try_from(&fr).unwrap() + else { + panic!("expected AcceptOk(rid=77)"); + }; + assert_eq!(ready.listener_id, lid); + } + + #[test] + fn listen_dispatch_binds() { + let mut m = Monitor::new(64); + let req = ControlRequest::Listen { + request_id: 7, + endpoint: Endpoint::new([127, 0, 0, 1], 0), + } + .to_frame(); + let reply = m.dispatch_request(req).unwrap(); + let ControlReply::ListenOk { + request_id: 7, + listener_id, + } = ControlReply::try_from(&reply).unwrap() + else { + panic!("expected ListenOk(rid=7)"); + }; + assert!(m.listeners.contains_key(&listener_id)); + } + + #[test] + fn connect_dispatch_reaches_listener() { + let (tx, rx) = mpsc::channel::(); + let server = thread::spawn(move || { + let l = TcpListener::bind("127.0.0.1:0").unwrap(); + l.set_nonblocking(false).unwrap(); + let port = l.local_addr().unwrap().port(); + tx.send(port).unwrap(); + let (mut s, _) = l.accept().unwrap(); + let mut buf = [0u8; 4]; + s.read_exact(&mut buf).unwrap(); + assert_eq!(&buf, b"ping"); + s.write_all(b"pong").unwrap(); + }); + + let port = rx.recv_timeout(Duration::from_secs(2)).unwrap(); + let mut m = Monitor::new(64); + let req = ControlRequest::Connect { + request_id: 1, + endpoint: Endpoint::new([127, 0, 0, 1], port), + } + .to_frame(); + let reply = m.dispatch_request(req).unwrap(); + let ControlReply::ConnectOk { ready, .. } = ControlReply::try_from(&reply).unwrap() else { + panic!("expected ConnectOk"); + }; + let stream = m.connection(ready.connection_id).unwrap(); + let mut owned = stream.try_clone().unwrap(); + owned.write_all(b"ping").unwrap(); + let mut out = [0u8; 4]; + owned.read_exact(&mut out).unwrap(); + assert_eq!(&out, b"pong"); + + server.join().unwrap(); + } + + #[test] + fn incoming_after_client_connects() { + let mut m = Monitor::new(64); + let req = ControlRequest::Listen { + request_id: 1, + endpoint: Endpoint::new([127, 0, 0, 1], 0), + } + .to_frame(); + let reply = m.dispatch_request(req).unwrap(); + let ControlReply::ListenOk { + listener_id: lid, .. + } = ControlReply::try_from(&reply).unwrap() + else { + panic!("expected ListenOk"); + }; + let port = m.listeners.get(&lid).unwrap().local_addr().unwrap().port(); + m.test_enqueue_accept(lid, 42); + + thread::spawn(move || { + thread::sleep(Duration::from_millis(20)); + let _ = TcpStream::connect(("127.0.0.1", port)); + }); + + thread::sleep(Duration::from_millis(50)); + let mut w = VecWriter(Vec::new()); + let written = m.poll_accepts(&mut w).unwrap(); + assert_eq!(written, 1); + + struct SliceReader<'a> { + data: &'a [u8], + pos: usize, + } + + impl PipeRead for SliceReader<'_> { + fn read(&mut self, buf: &mut [u8]) -> Result { + if self.pos >= self.data.len() { + return Err(PipeError::WouldBlock); + } + let n = std::cmp::min(buf.len(), self.data.len() - self.pos); + buf[..n].copy_from_slice(&self.data[self.pos..self.pos + n]); + self.pos += n; + Ok(n) + } + } + + let mut r = SliceReader { data: &w.0, pos: 0 }; + let ev = read_frame(&mut r).unwrap(); + let ControlReply::AcceptOk { + request_id: 42, + ready, + } = ControlReply::try_from(&ev).unwrap() + else { + panic!("expected AcceptOk(rid=42)"); + }; + assert_eq!(ready.listener_id, lid); + } + + #[test] + fn tcp_to_pipe_reads_from_tcp() { + const RING: u64 = 64; + #[repr(align(8))] + struct Mem([u8; BidirectionalPipe::required_size(RING) as usize]); + + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let port = listener.local_addr().unwrap().port(); + let server = thread::spawn(move || { + let (mut s, _) = listener.accept().unwrap(); + s.write_all(b"hi").unwrap(); + }); + + let mut tcp = TcpStream::connect(("127.0.0.1", port)).unwrap(); + thread::sleep(Duration::from_millis(20)); + + let mut mem = Mem([0u8; BidirectionalPipe::required_size(RING) as usize]); + let region = unsafe { SharedMemoryRegion::from_raw(mem.0.as_mut_ptr(), mem.0.len() as u64) }; + let mut pipe_b = BidirectionalPipe::new(®ion, RING, Side::B); + let n = relay::tcp_to_pipe(&mut tcp, &mut pipe_b).unwrap(); + assert_eq!(n, 2); + + // Side::A is what Arca would use; read back to verify bytes arrived in the ring. + let mut pipe_a = BidirectionalPipe::new(®ion, RING, Side::A); + let mut out = [0u8; 2]; + pipe_a.read(&mut out).unwrap(); + assert_eq!(&out, b"hi"); + + server.join().unwrap(); + } +} diff --git a/monitor/src/relay.rs b/monitor/src/relay.rs new file mode 100644 index 0000000..3c954da --- /dev/null +++ b/monitor/src/relay.rs @@ -0,0 +1,60 @@ +use arca_pipe::{BidirectionalPipe, PipeError, Read, Write}; +use std::io::{Read as IoRead, Write as IoWrite}; +use std::net::{Shutdown, TcpStream}; + +/// Read from TCP and write into the pipe. +/// +/// - If the pipe's peer read side is closed (Arca stopped reading), closes the +/// pipe write side and shuts down the TCP receive direction. +/// - On TCP EOF (`Ok(0)`), closes the pipe write side to signal Arca. +/// - On TCP error, closes the pipe write side and returns the error. +/// - Spins on pipe `WouldBlock` until all bytes from the TCP read are delivered. +pub fn tcp_to_pipe(tcp: &mut TcpStream, pipe: &mut BidirectionalPipe) -> std::io::Result { + if pipe.is_peer_read_closed() { + pipe.close_write(); + let _ = tcp.shutdown(Shutdown::Read); + return Ok(0); + } + + let mut buf = [0u8; 4096]; + match tcp.read(&mut buf) { + Ok(0) => { + pipe.close_write(); + Ok(0) + } + Ok(n) => { + pipe.write_all(&buf[..n]); + Ok(n) + } + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => Ok(0), + Err(e) => { + pipe.close_write(); + Err(e) + } + } +} + +/// Read from the pipe and write to TCP. +/// +/// - On pipe `WouldBlock` with peer write closed (Arca done sending), closes +/// the pipe read side and shuts down the TCP send direction. +/// - On TCP write error, closes the pipe read side to signal Arca. +pub fn pipe_to_tcp(tcp: &mut TcpStream, pipe: &mut BidirectionalPipe) -> std::io::Result { + let mut buf = [0u8; 4096]; + match pipe.read(&mut buf) { + Ok(n) => { + if let Err(e) = tcp.write_all(&buf[..n]) { + pipe.close_read(); + return Err(e); + } + Ok(n) + } + Err(PipeError::WouldBlock) => { + if pipe.is_peer_write_closed() { + pipe.close_read(); + let _ = tcp.shutdown(Shutdown::Write); + } + Ok(0) + } + } +} diff --git a/monitor/tests/end_to_end.rs b/monitor/tests/end_to_end.rs new file mode 100644 index 0000000..44f9f57 --- /dev/null +++ b/monitor/tests/end_to_end.rs @@ -0,0 +1,216 @@ +//! End-to-end tests: run an [`ArcaSession`] against a real [`Monitor`] over a +//! shared transport, with real Linux TCP sockets at the far end. +//! +//! What's *not* exercised here: +//! - The `arca_pipe::BidirectionalPipe` itself. That has its own unit tests +//! in `arca-pipe`, and using it cross-thread requires `unsafe impl Sync` +//! on `SharedMemoryRegion`, which is owned by another crate. The protocol +//! is transport-agnostic — anything that implements `arca_pipe::Read + +//! Write` slots in. +//! +//! What *is* exercised: +//! - The full request/reply round trip across two threads. +//! - Real outbound `connect` against a stdlib `TcpListener` peer. +//! - Real inbound `accept` flow: Arca `bind`, Arca `accept` (posts +//! `AcceptRequest`), monitor pairs the kernel `accept` with that wait and +//! replies with `IncomingConnection`. + +use std::collections::VecDeque; +use std::net::{TcpListener, TcpStream}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::mpsc; +use std::sync::{Arc, Mutex}; +use std::thread; +use std::time::{Duration, Instant}; + +use arca_control::{ArcaSession, Endpoint}; +use arca_monitor::Monitor; +use arca_pipe::{PipeError, Read, Write}; + +/// One end of a thread-safe bidirectional in-memory transport. +/// +/// Reads come from `inbox`; writes append to the *other* side's inbox. +/// `read` is non-blocking (returns `WouldBlock` on empty), which is exactly +/// what `arca_pipe::Read` requires. +#[derive(Clone)] +struct ChannelEnd { + inbox: Arc>>, + outbox: Arc>>, +} + +fn channel_pair() -> (ChannelEnd, ChannelEnd) { + let a_to_b = Arc::new(Mutex::new(VecDeque::::new())); + let b_to_a = Arc::new(Mutex::new(VecDeque::::new())); + let a = ChannelEnd { + inbox: b_to_a.clone(), + outbox: a_to_b.clone(), + }; + let b = ChannelEnd { + inbox: a_to_b, + outbox: b_to_a, + }; + (a, b) +} + +impl Read for ChannelEnd { + fn read(&mut self, buf: &mut [u8]) -> Result { + let mut q = self.inbox.lock().unwrap(); + if q.is_empty() { + return Err(PipeError::WouldBlock); + } + let n = buf.len().min(q.len()); + for slot in buf.iter_mut().take(n) { + *slot = q.pop_front().unwrap(); + } + Ok(n) + } +} + +impl Write for ChannelEnd { + fn write(&mut self, src: &[u8]) -> Result { + let mut q = self.outbox.lock().unwrap(); + q.extend(src.iter().copied()); + Ok(src.len()) + } +} + +#[test] +fn arca_connect_round_trip_against_real_tcp_listener() { + // The Linux kernel-side "remote peer" — accepts one connection, echoes 4 bytes. + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let port = listener.local_addr().unwrap().port(); + let server = thread::spawn(move || { + use std::io::{Read as IoRead, Write as IoWrite}; + let (mut s, _) = listener.accept().unwrap(); + let mut buf = [0u8; 4]; + s.read_exact(&mut buf).unwrap(); + assert_eq!(&buf, b"ping"); + s.write_all(b"pong").unwrap(); + }); + + let (mut arca_end, mut mon_end) = channel_pair(); + + // Monitor thread: handle exactly one ConnectRequest. + let monitor_thread = thread::spawn(move || { + let mut m = Monitor::new(64); + m.serve_one(&mut mon_end).unwrap(); + m + }); + + // Arca side: connect to the listener. + let mut arca = ArcaSession::new(&mut arca_end); + let stream = arca + .connect(Endpoint::new([127, 0, 0, 1], port)) + .expect("connect should succeed"); + + assert!(!stream.is_inbound(), "outbound connect has listener_id 0"); + assert_eq!(stream.connection_id(), 1, "first connection gets id 1"); + assert_eq!(stream.pipe().ring_size, 64, "ring_size from default config"); + assert!(stream.pipe().shm_offset > 0, "shm_offset should be a real BuddyAllocator offset"); + + // Monitor returns; verify it actually owns a live socket for that id. + let mut m = monitor_thread.join().unwrap(); + let mut owned = m + .connection(stream.connection_id()) + .unwrap() + .try_clone() + .unwrap(); + + // Drive the byte exchange to make sure the kernel socket really works. + use std::io::{Read as IoRead, Write as IoWrite}; + owned.write_all(b"ping").unwrap(); + let mut got = [0u8; 4]; + owned.read_exact(&mut got).unwrap(); + assert_eq!(&got, b"pong"); + + server.join().unwrap(); +} + +#[test] +fn arca_bind_then_accept_after_external_connect() { + let (mut arca_end, mut mon_end) = channel_pair(); + + // Lets the monitor thread tell the test what port it bound to and lets + // the test signal it to shut down. + let (port_tx, port_rx) = mpsc::channel::(); + let shutdown = Arc::new(AtomicBool::new(false)); + let shutdown_for_thread = shutdown.clone(); + + let monitor_thread = thread::spawn(move || { + let mut m = Monitor::new(64); + + // 1. Read the bind request, dispatch, peek the bound port, write reply. + use arca_control::{read_frame, write_frame, ControlReply, MessageType}; + let req = read_frame(&mut mon_end).unwrap(); + assert_eq!(req.message_type, MessageType::ListenRequest); + let reply = m.dispatch_request(req).unwrap(); + let ControlReply::ListenOk { + listener_id: lid, .. + } = ControlReply::try_from(&reply).unwrap() + else { + panic!("expected ListenOk"); + }; + let port = m + .listener(lid) + .expect("listener should exist after dispatch") + .local_addr() + .unwrap() + .port(); + port_tx.send(port).unwrap(); + write_frame(&mut mon_end, &reply).unwrap(); + + // 2. Poll: kernel accepts + any Arca→Linux frames (AcceptRequest, …). + while !shutdown_for_thread.load(Ordering::Relaxed) { + m.pump_once(&mut mon_end).unwrap(); + thread::sleep(Duration::from_millis(2)); + } + // Last pass so a frame just before shutdown is not left stranded. + m.pump_once(&mut mon_end).unwrap(); + m + }); + + // Arca: bind on an ephemeral port. + let mut arca = ArcaSession::new(&mut arca_end); + let listener = arca + .bind(Endpoint::new([127, 0, 0, 1], 0)) + .expect("bind should succeed"); + assert_eq!(listener.id(), 1); + + // External peer connects to the bound port. + let port = port_rx.recv_timeout(Duration::from_secs(2)).unwrap(); + let _peer = thread::spawn(move || { + // Tiny delay so the monitor's pump loop is already running. + thread::sleep(Duration::from_millis(20)); + let _stream = TcpStream::connect(("127.0.0.1", port)).unwrap(); + // Hold the connection open until the test ends, otherwise the peer + // might close before the monitor can `accept`. + thread::sleep(Duration::from_millis(500)); + }); + + // Arca: accept the inbound connection. Bound the wait so a regression + // doesn't hang CI. + let started = Instant::now(); + let stream = loop { + if started.elapsed() > Duration::from_secs(3) { + panic!("accept timed out"); + } + match arca.accept(&listener) { + Ok(s) => break s, + Err(arca_control::ArcaError::Codec(_)) => { + // Codec spinning on WouldBlock — read_frame's read_exact + // already loops, so this branch is mostly dead, but keep + // it for safety. + thread::sleep(Duration::from_millis(2)); + } + Err(e) => panic!("unexpected accept error: {:?}", e), + } + }; + + assert!(stream.is_inbound()); + assert_eq!(stream.listener_id(), listener.id()); + + // Tear down monitor cleanly. + shutdown.store(true, Ordering::Relaxed); + let mut m = monitor_thread.join().unwrap(); + assert!(m.connection(stream.connection_id()).is_some()); +} diff --git a/pipe/Cargo.lock b/pipe/Cargo.lock new file mode 100644 index 0000000..a68ee29 --- /dev/null +++ b/pipe/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "arca-pipe" +version = "0.1.0" diff --git a/pipe/Cargo.toml b/pipe/Cargo.toml new file mode 100644 index 0000000..8f63ee2 --- /dev/null +++ b/pipe/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "arca-pipe" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/pipe/src/bidirectional_pipe.rs b/pipe/src/bidirectional_pipe.rs new file mode 100644 index 0000000..032b145 --- /dev/null +++ b/pipe/src/bidirectional_pipe.rs @@ -0,0 +1,203 @@ +use crate::error::PipeError; +use crate::ring::{RingData, RingHeader}; +use crate::ring_consumer::RingConsumer; +use crate::ring_producer::RingProducer; +use crate::shared_memory_region::SharedMemoryRegion; +use crate::traits; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Side { + A, + B, +} + +/// One endpoint of a bidirectional pipe. +/// +/// Memory layout: `[HeaderA][Ring A->B data][HeaderB][Ring B->A data]`. +pub struct BidirectionalPipe<'a> { + writer: RingProducer<'a>, + reader: RingConsumer<'a>, +} + +const HEADER_SIZE: u64 = core::mem::size_of::() as u64; + +impl<'a> BidirectionalPipe<'a> { + /// Total bytes of shared memory needed for a given `ring_size`. + pub const fn required_size(ring_size: u64) -> u64 { + 2 * (HEADER_SIZE + ring_size) + } + + /// Create a pipe endpoint over a shared memory region. + /// + /// Caller must ensure the region is zero-initialized before the first side + /// is constructed, and that exactly one `Side::A` and one `Side::B` are + /// created per region. + pub fn new(region: &'a SharedMemoryRegion, ring_size: u64, side: Side) -> Self { + assert!(region.len() >= Self::required_size(ring_size)); + assert!(ring_size % core::mem::align_of::() as u64 == 0, + "ring_size must be a multiple of 8 for header alignment"); + let base = region.as_ptr(); + assert!(base.align_offset(core::mem::align_of::()) == 0, + "shared memory region must be 8-byte aligned"); + + // Layout: [HeaderA (16)] [DataA (ring_size)] [HeaderB (16)] [DataB (ring_size)] + // Interleaved so each header is adjacent to its data (cache locality) + // and headers are separated by ring_size (avoids false sharing). + let header_a = unsafe { &*(base as *const RingHeader) }; + let data_a = unsafe { base.add(HEADER_SIZE as usize) }; + let header_b = unsafe { &*(data_a.add(ring_size as usize) as *const RingHeader) }; + let data_b = unsafe { data_a.add(ring_size as usize + HEADER_SIZE as usize) }; + + let (writer_header, writer_data, reader_header, reader_data) = match side { + Side::A => (header_a, data_a, header_b, data_b), + Side::B => (header_b, data_b, header_a, data_a), + }; + + let writer = RingProducer::new(writer_header, unsafe { + RingData::new(writer_data, ring_size) + }); + let reader = RingConsumer::new(reader_header, unsafe { + RingData::new(reader_data, ring_size) + }); + Self { writer, reader } + } + + /// Split into independent read and write halves (like `TcpStream::split`). + pub fn split(&mut self) -> (&mut RingConsumer<'a>, &mut RingProducer<'a>) { + (&mut self.reader, &mut self.writer) + } + + /// Close this side's outgoing (write) direction. + pub fn close_write(&self) { + self.writer.close_writer(); + } + + /// Close this side's incoming (read) direction. + pub fn close_read(&self) { + self.reader.close_reader(); + } + + /// True if the peer has closed their write side (no more data incoming). + pub fn is_peer_write_closed(&self) -> bool { + self.reader.is_writer_closed() + } + + /// True if the peer has closed their read side (they will not read more data we send). + pub fn is_peer_read_closed(&self) -> bool { + self.writer.is_reader_closed() + } + + /// True when both unidirectional rings are fully closed (all four flags set). + pub fn is_closed(&self) -> bool { + self.writer.is_closed() && self.reader.is_closed() + } +} + +impl<'a> traits::Read for BidirectionalPipe<'a> { + fn read(&mut self, buf: &mut [u8]) -> Result { + self.reader.read(buf) + } +} + +impl<'a> traits::Write for BidirectionalPipe<'a> { + fn write(&mut self, buf: &[u8]) -> Result { + self.writer.write(buf) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::traits::{Read, Write}; + + #[repr(align(8))] + struct Aligned([u8; N]); + + macro_rules! pipe_pair { + ($ring:expr, $mem:ident, $a:ident, $b:ident) => { + let mut $mem = Aligned([0u8; BidirectionalPipe::required_size($ring as u64) as usize]); + let region = unsafe { + SharedMemoryRegion::from_raw($mem.0.as_mut_ptr(), $mem.0.len() as u64) + }; + let mut $a = BidirectionalPipe::new(®ion, $ring, Side::A); + let mut $b = BidirectionalPipe::new(®ion, $ring, Side::B); + }; + } + + #[test] + fn required_size_matches_layout() { + assert_eq!(BidirectionalPipe::required_size(64), 2 * (24 + 64)); + } + + #[test] + fn round_trip_a_to_b() { + pipe_pair!(64, mem, a, b); + assert_eq!(a.write(b"ping").unwrap(), 4); + let mut out = [0u8; 4]; + assert_eq!(b.read(&mut out).unwrap(), 4); + assert_eq!(&out, b"ping"); + } + + #[test] + fn round_trip_b_to_a() { + pipe_pair!(32, mem, a, b); + assert_eq!(b.write(b"pong!!").unwrap(), 6); + let mut out = [0u8; 6]; + assert_eq!(a.read(&mut out).unwrap(), 6); + assert_eq!(&out, b"pong!!"); + } + + #[test] + fn both_directions_independent() { + pipe_pair!(32, mem, a, b); + a.write(b"hello").unwrap(); + b.write(b"world").unwrap(); + + let mut from_a = [0u8; 5]; + let mut from_b = [0u8; 5]; + b.read(&mut from_a).unwrap(); + a.read(&mut from_b).unwrap(); + assert_eq!(&from_a, b"hello"); + assert_eq!(&from_b, b"world"); + } + + #[test] + fn multi_lap() { + pipe_pair!(8, mem, a, b); + for i in 0u8..20 { + assert_eq!(a.write(&[i]).unwrap(), 1); + let mut out = [0u8; 1]; + assert_eq!(b.read(&mut out).unwrap(), 1); + assert_eq!(out[0], i); + } + } + + #[test] + fn fill_drain_refill() { + pipe_pair!(8, mem, a, b); + assert_eq!(a.write(b"12345678").unwrap(), 8); + let mut out = [0u8; 8]; + assert_eq!(b.read(&mut out).unwrap(), 8); + assert_eq!(&out, b"12345678"); + + assert_eq!(a.write(b"abcdefgh").unwrap(), 8); + assert_eq!(b.read(&mut out).unwrap(), 8); + assert_eq!(&out, b"abcdefgh"); + } + + #[test] + fn interleaved_both_directions() { + pipe_pair!(16, mem, a, b); + a.write(b"aa").unwrap(); + b.write(b"bb").unwrap(); + a.write(b"cc").unwrap(); + b.write(b"dd").unwrap(); + + let mut out = [0u8; 4]; + b.read(&mut out).unwrap(); + assert_eq!(&out, b"aacc"); + a.read(&mut out).unwrap(); + assert_eq!(&out, b"bbdd"); + } + +} diff --git a/pipe/src/error.rs b/pipe/src/error.rs new file mode 100644 index 0000000..82a381c --- /dev/null +++ b/pipe/src/error.rs @@ -0,0 +1,16 @@ +use core::fmt; + +/// Errors returned by pipe operations. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PipeError { + /// Ring buffer is empty (read) or full (write). Try again later. + WouldBlock, +} + +impl fmt::Display for PipeError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + PipeError::WouldBlock => write!(f, "operation would block"), + } + } +} diff --git a/pipe/src/lib.rs b/pipe/src/lib.rs new file mode 100644 index 0000000..a6d5694 --- /dev/null +++ b/pipe/src/lib.rs @@ -0,0 +1,28 @@ +//! # arca-pipe +//! +//! A `no_std`-compatible, lock-free bidirectional byte pipe built from two +//! single-producer, single-consumer (SPSC) ring buffers over shared memory. +//! +//! This is the lowest-level transport primitive in arca-networking — both the +//! control protocol and per-connection data streams are built on top of it. +//! +//! The pipe is a raw byte stream with no message framing. Higher layers +//! (control protocol, data protocol) add their own framing on top. + +#![no_std] + +mod error; +mod traits; +mod ring; +mod ring_producer; +mod ring_consumer; +mod shared_memory_region; +mod bidirectional_pipe; + +pub use error::PipeError; +pub use traits::{Read, Write}; +pub use ring::{RingData, RingHeader}; +pub use ring_producer::RingProducer; +pub use ring_consumer::RingConsumer; +pub use shared_memory_region::SharedMemoryRegion; +pub use bidirectional_pipe::{BidirectionalPipe, Side}; diff --git a/pipe/src/ring.rs b/pipe/src/ring.rs new file mode 100644 index 0000000..2c3ded5 --- /dev/null +++ b/pipe/src/ring.rs @@ -0,0 +1,157 @@ +use core::sync::atomic::{AtomicBool, AtomicU64, Ordering}; + +/// Header for a single SPSC ring buffer, stored in shared memory. +/// +/// Cursors are monotonically increasing logical offsets. Physical positions +/// are `cursor % ring_size`. The close flags signal orderly shutdown: the +/// producer sets `writer_closed`; the consumer sets `reader_closed`. +#[repr(C)] +pub struct RingHeader { + pub read_cursor: AtomicU64, + pub write_cursor: AtomicU64, + pub writer_closed: AtomicBool, + pub reader_closed: AtomicBool, +} + +impl RingHeader { + /// Bytes available to read. Called by the consumer. + pub fn used_space(&self) -> u64 { + let write = self.write_cursor.load(Ordering::Acquire); + let read = self.read_cursor.load(Ordering::Relaxed); + + // Cursors are monotonically increasing and never reset, but they can + // overflow u64. wrapping_sub gives the correct delta regardless, + // also avoids panic on debug-mode subtraction overflow + write.wrapping_sub(read) + } + + /// Bytes available to write. Called by the producer. + pub fn free_space(&self, capacity: u64) -> u64 { + let write = self.write_cursor.load(Ordering::Relaxed); + let read = self.read_cursor.load(Ordering::Acquire); + + // See used_space — wrapping_sub handles cursor overflow correctly + capacity - write.wrapping_sub(read) + } +} + +/// Raw data region of a single SPSC ring buffer. +/// +/// Owns `(ptr, size)` together so call sites don't juggle them. +/// Wrap-around is handled inside `write_at` / `read_at`. +pub struct RingData { + ptr: *mut u8, + size: u64, +} + +impl RingData { + /// # Safety + /// - `ptr` must point to a valid region of `size` bytes. + /// - Caller must guarantee SPSC discipline on top of this region. + pub unsafe fn new(ptr: *mut u8, size: u64) -> Self { + Self { ptr, size } + } + + pub fn size(&self) -> u64 { + self.size + } + + /// Write `buf` starting at physical offset `cursor % size`, wrapping if needed. + /// Caller must ensure `buf.len() <= free space`. + pub fn write_at(&mut self, cursor: u64, buf: &[u8]) { + let size = self.size as usize; + let offset = (cursor % self.size) as usize; + let first = core::cmp::min(buf.len(), size - offset); + unsafe { + core::ptr::copy_nonoverlapping(buf.as_ptr(), self.ptr.add(offset), first); + if buf.len() > first { + core::ptr::copy_nonoverlapping( + buf.as_ptr().add(first), + self.ptr, + buf.len() - first, + ); + } + } + } + + /// Read into `buf` starting at physical offset `cursor % size`, wrapping if needed. + /// Caller must ensure `buf.len() <= used space`. + pub fn read_at(&self, cursor: u64, buf: &mut [u8]) { + let size = self.size as usize; + let offset = (cursor % self.size) as usize; + let first = core::cmp::min(buf.len(), size - offset); + unsafe { + core::ptr::copy_nonoverlapping(self.ptr.add(offset), buf.as_mut_ptr(), first); + if buf.len() > first { + core::ptr::copy_nonoverlapping( + self.ptr, + buf.as_mut_ptr().add(first), + buf.len() - first, + ); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn header_with(read: u64, write: u64) -> RingHeader { + RingHeader { + read_cursor: AtomicU64::new(read), + write_cursor: AtomicU64::new(write), + writer_closed: AtomicBool::new(false), + reader_closed: AtomicBool::new(false), + } + } + + #[test] + fn empty_ring() { + let h = header_with(0, 0); + assert_eq!(h.used_space(), 0); + assert_eq!(h.free_space(64), 64); + } + + #[test] + fn partial_fill() { + let h = header_with(10, 40); + assert_eq!(h.used_space(), 30); + assert_eq!(h.free_space(64), 34); + } + + #[test] + fn full_ring() { + let h = header_with(100, 164); + assert_eq!(h.used_space(), 64); + assert_eq!(h.free_space(64), 0); + } + + #[test] + fn data_write_then_read_no_wrap() { + let mut mem = [0u8; 8]; + let mut rd = unsafe { RingData::new(mem.as_mut_ptr(), 8) }; + rd.write_at(0, b"abcd"); + let mut out = [0u8; 4]; + rd.read_at(0, &mut out); + assert_eq!(&out, b"abcd"); + } + + #[test] + fn data_write_wraps() { + let mut mem = [0u8; 8]; + let mut rd = unsafe { RingData::new(mem.as_mut_ptr(), 8) }; + rd.write_at(6, b"XYZW"); + assert_eq!(&mem[6..8], b"XY"); + assert_eq!(&mem[..2], b"ZW"); + } + + #[test] + fn data_read_wraps() { + let mut mem = *b"cdEFabXY"; + let rd = unsafe { RingData::new(mem.as_mut_ptr(), 8) }; + let mut out = [0u8; 6]; + rd.read_at(6, &mut out); + assert_eq!(&out, b"XYcdEF"); + } +} diff --git a/pipe/src/ring_consumer.rs b/pipe/src/ring_consumer.rs new file mode 100644 index 0000000..52a787a --- /dev/null +++ b/pipe/src/ring_consumer.rs @@ -0,0 +1,135 @@ +use crate::error::PipeError; +use crate::ring::{RingData, RingHeader}; +use crate::traits; +use core::sync::atomic::Ordering; + +/// Consumer (read) end of a single SPSC ring buffer. +pub struct RingConsumer<'a> { + header: &'a RingHeader, + data: RingData, +} + +impl<'a> RingConsumer<'a> { + pub fn new(header: &'a RingHeader, data: RingData) -> Self { + Self { header, data } + } + + /// Signal that this consumer will read no more bytes. + pub fn close_reader(&self) { + self.header.reader_closed.store(true, Ordering::Release); + } + + /// True if the producer has closed its write end. + pub fn is_writer_closed(&self) -> bool { + self.header.writer_closed.load(Ordering::Acquire) + } + + /// True when both ends of this ring are closed. + pub fn is_closed(&self) -> bool { + self.header.writer_closed.load(Ordering::Acquire) + && self.header.reader_closed.load(Ordering::Acquire) + } +} + +impl<'a> traits::Read for RingConsumer<'a> { + fn read(&mut self, buf: &mut [u8]) -> Result { + if buf.is_empty() { return Ok(0); } + let used = self.header.used_space(); + if used == 0 { + return Err(PipeError::WouldBlock); + } + + let n = core::cmp::min(buf.len() as u64, used) as usize; + let cursor = self.header.read_cursor.load(Ordering::Relaxed); + self.data.read_at(cursor, &mut buf[..n]); + + // No standalone fence needed, release on the store guarantees the + // preceding read_at is visible before the cursor update + self.header.read_cursor.store(cursor + n as u64, Ordering::Release); + Ok(n) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::traits::Read; + use core::sync::atomic::AtomicU64; + + fn header(read: u64, write: u64) -> RingHeader { + use core::sync::atomic::AtomicBool; + RingHeader { + read_cursor: AtomicU64::new(read), + write_cursor: AtomicU64::new(write), + writer_closed: AtomicBool::new(false), + reader_closed: AtomicBool::new(false), + } + } + + #[test] + fn simple_read() { + let mut mem = *b"hello..."; + let h = header(0, 5); + let data = unsafe { RingData::new(mem.as_mut_ptr(), 8) }; + let mut c = RingConsumer::new(&h, data); + let mut out = [0u8; 8]; + assert_eq!(c.read(&mut out).unwrap(), 5); + assert_eq!(&out[..5], b"hello"); + assert_eq!(h.read_cursor.load(Ordering::Relaxed), 5); + } + + #[test] + fn partial_read() { + let mut mem = *b"abcdefgh"; + let h = header(0, 8); + let data = unsafe { RingData::new(mem.as_mut_ptr(), 8) }; + let mut c = RingConsumer::new(&h, data); + let mut out = [0u8; 3]; + assert_eq!(c.read(&mut out).unwrap(), 3); + assert_eq!(&out, b"abc"); + } + + #[test] + fn wrap_around() { + let mut mem = *b"WXYZabcd"; + let h = header(5, 12); + let data = unsafe { RingData::new(mem.as_mut_ptr(), 8) }; + let mut c = RingConsumer::new(&h, data); + let mut out = [0u8; 8]; + assert_eq!(c.read(&mut out).unwrap(), 7); + assert_eq!(&out[..7], b"bcdWXYZ"); + } + + #[test] + fn empty_ring_blocks() { + let mut mem = [0u8; 4]; + let h = header(4, 4); + let data = unsafe { RingData::new(mem.as_mut_ptr(), 4) }; + let mut c = RingConsumer::new(&h, data); + let mut out = [0u8; 4]; + assert!(matches!(c.read(&mut out), Err(PipeError::WouldBlock))); + } + + #[test] + fn zero_length_read_non_empty() { + let mut mem = *b"data"; + let h = header(0, 4); + let data = unsafe { RingData::new(mem.as_mut_ptr(), 4) }; + let mut c = RingConsumer::new(&h, data); + let mut out = [0u8; 0]; + assert_eq!(c.read(&mut out).unwrap(), 0); + assert_eq!(h.read_cursor.load(Ordering::Relaxed), 0); + } + + #[test] + fn zero_length_read_empty() { + let mut mem = [0u8; 4]; + let h = header(0, 0); + let data = unsafe { RingData::new(mem.as_mut_ptr(), 4) }; + let mut c = RingConsumer::new(&h, data); + let mut out = [0u8; 0]; + assert_eq!(c.read(&mut out).unwrap(), 0); + } + + +} diff --git a/pipe/src/ring_producer.rs b/pipe/src/ring_producer.rs new file mode 100644 index 0000000..5e2a477 --- /dev/null +++ b/pipe/src/ring_producer.rs @@ -0,0 +1,170 @@ +use crate::error::PipeError; +use crate::ring::{RingData, RingHeader}; +use crate::traits; +use core::sync::atomic::Ordering; + +/// Producer (write) end of a single SPSC ring buffer. +pub struct RingProducer<'a> { + header: &'a RingHeader, + data: RingData, +} + +impl<'a> RingProducer<'a> { + pub fn new(header: &'a RingHeader, data: RingData) -> Self { + Self { header, data } + } + + /// Bytes written by this producer that the consumer has not yet read. + /// Uses Acquire on read_cursor so this can be called cross-thread safely. + pub fn bytes_pending(&self) -> u64 { + let write = self.header.write_cursor.load(Ordering::Relaxed); + let read = self.header.read_cursor.load(Ordering::Acquire); + write.wrapping_sub(read) + } + + /// Signal that this producer will write no more bytes. + pub fn close_writer(&self) { + self.header.writer_closed.store(true, Ordering::Release); + } + + /// True if the consumer has closed its read end. + pub fn is_reader_closed(&self) -> bool { + self.header.reader_closed.load(Ordering::Acquire) + } + + /// True when both ends of this ring are closed. + pub fn is_closed(&self) -> bool { + self.header.writer_closed.load(Ordering::Acquire) + && self.header.reader_closed.load(Ordering::Acquire) + } +} + +impl<'a> traits::Write for RingProducer<'a> { + fn write(&mut self, buf: &[u8]) -> Result { + if buf.is_empty() { return Ok(0); } + let free = self.header.free_space(self.data.size()); + if free == 0 { + return Err(PipeError::WouldBlock); + } + + let n = core::cmp::min(buf.len() as u64, free) as usize; + let cursor = self.header.write_cursor.load(Ordering::Relaxed); + self.data.write_at(cursor, &buf[..n]); + + // No standalone fence needed, release on the store guarantees the + // preceding write_at is visible before the cursor update + self.header.write_cursor.store(cursor + n as u64, Ordering::Release); + Ok(n) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::traits::Write; + use core::sync::atomic::AtomicU64; + + fn header() -> RingHeader { + use core::sync::atomic::AtomicBool; + RingHeader { + read_cursor: AtomicU64::new(0), + write_cursor: AtomicU64::new(0), + writer_closed: AtomicBool::new(false), + reader_closed: AtomicBool::new(false), + } + } + + #[test] + fn simple_write() { + let h = header(); + let mut mem = [0u8; 16]; + let data = unsafe { RingData::new(mem.as_mut_ptr(), 16) }; + let mut p = RingProducer::new(&h, data); + assert_eq!(p.write(b"hello").unwrap(), 5); + assert_eq!(&mem[..5], b"hello"); + assert_eq!(h.write_cursor.load(Ordering::Relaxed), 5); + } + + #[test] + fn fill_to_full() { + let h = header(); + let mut mem = [0u8; 8]; + let data = unsafe { RingData::new(mem.as_mut_ptr(), 8) }; + let mut p = RingProducer::new(&h, data); + assert_eq!(p.write(b"abcdefghij").unwrap(), 8); + assert_eq!(&mem, b"abcdefgh"); + } + + #[test] + fn wrap_around() { + let h = header(); + h.read_cursor.store(5, Ordering::Relaxed); + h.write_cursor.store(5, Ordering::Relaxed); + let mut mem = [0u8; 8]; + let data = unsafe { RingData::new(mem.as_mut_ptr(), 8) }; + let mut p = RingProducer::new(&h, data); + assert_eq!(p.write(b"XYZW").unwrap(), 4); + assert_eq!(&mem[5..8], b"XYZ"); + assert_eq!(&mem[..1], b"W"); + } + + #[test] + fn full_ring_blocks() { + let h = header(); + h.write_cursor.store(4, Ordering::Relaxed); + let mut mem = [0u8; 4]; + let data = unsafe { RingData::new(mem.as_mut_ptr(), 4) }; + let mut p = RingProducer::new(&h, data); + assert!(matches!(p.write(b"x"), Err(PipeError::WouldBlock))); + } + + #[test] + fn zero_length_write_non_full() { + let h = header(); + let mut mem = [0u8; 8]; + let data = unsafe { RingData::new(mem.as_mut_ptr(), 8) }; + let mut p = RingProducer::new(&h, data); + assert_eq!(p.write(b"").unwrap(), 0); + assert_eq!(h.write_cursor.load(Ordering::Relaxed), 0); + } + + #[test] + fn zero_length_write_full() { + let h = header(); + h.write_cursor.store(8, Ordering::Relaxed); + let mut mem = [0u8; 8]; + let data = unsafe { RingData::new(mem.as_mut_ptr(), 8) }; + let mut p = RingProducer::new(&h, data); + assert_eq!(p.write(b"").unwrap(), 0); + } + + #[test] + fn bytes_pending_empty() { + let h = header(); + let mut mem = [0u8; 8]; + let data = unsafe { RingData::new(mem.as_mut_ptr(), 8) }; + let p = RingProducer::new(&h, data); + assert_eq!(p.bytes_pending(), 0); + } + + #[test] + fn bytes_pending_after_write() { + let h = header(); + let mut mem = [0u8; 8]; + let data = unsafe { RingData::new(mem.as_mut_ptr(), 8) }; + let mut p = RingProducer::new(&h, data); + p.write(b"hello").unwrap(); + assert_eq!(p.bytes_pending(), 5); + } + + #[test] + fn bytes_pending_zero_after_full_read() { + let h = header(); + let mut mem = [0u8; 8]; + let data = unsafe { RingData::new(mem.as_mut_ptr(), 8) }; + let mut p = RingProducer::new(&h, data); + p.write(b"hello").unwrap(); + h.read_cursor.store(5, Ordering::Release); + assert_eq!(p.bytes_pending(), 0); + } +} diff --git a/pipe/src/shared_memory_region.rs b/pipe/src/shared_memory_region.rs new file mode 100644 index 0000000..4b9ec64 --- /dev/null +++ b/pipe/src/shared_memory_region.rs @@ -0,0 +1,66 @@ +/// Owns a reference to a shared memory region. +/// +/// Instead of exposing raw pointers and `unsafe` at every call site, we wrap +/// the shared memory region in a type that guarantees validity — pushing the +/// `unsafe` into a single place. After constructing a `SharedMemoryRegion`, +/// all pipe construction and usage is safe. +/// +/// How the shared memory pointer is obtained (hypervisor mapping, POSIX shm, +/// etc.) is outside this type's scope — we assume both sides have a way to +/// get it. +pub struct SharedMemoryRegion { + ptr: *mut u8, + len: u64, +} + +impl SharedMemoryRegion { + /// Create a new shared memory region from a raw pointer. + /// + /// This is the one and only unsafe entry point for the pipe library. + /// + /// # Safety + /// - `ptr` must point to a valid, read-write memory region of at least `len` bytes. + /// - The memory must remain valid for the lifetime of this `SharedMemoryRegion`. + /// - The memory must be shared between both sides of the pipe (e.g. via + /// hypervisor page mapping or POSIX shared memory). + /// - The memory must be zero-initialized before the first pipe is created from it. + pub unsafe fn from_raw(ptr: *mut u8, len: u64) -> Self { + Self { ptr, len } + } + + /// Returns a raw pointer to the start of the shared memory region. + pub fn as_ptr(&self) -> *mut u8 { + self.ptr + } + + /// Returns the length of the shared memory region in bytes. + pub fn len(&self) -> u64 { + self.len + } + + /// Returns true if the shared memory region has zero length. + pub fn is_empty(&self) -> bool { + self.len == 0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn from_raw_stores_ptr_and_len() { + let mut buf = [0u8; 16]; + let region = unsafe { SharedMemoryRegion::from_raw(buf.as_mut_ptr(), buf.len() as u64) }; + assert_eq!(region.as_ptr(), buf.as_mut_ptr()); + assert_eq!(region.len(), 16); + assert!(!region.is_empty()); + } + + #[test] + fn zero_length_is_empty() { + let region = unsafe { SharedMemoryRegion::from_raw(core::ptr::null_mut(), 0) }; + assert_eq!(region.len(), 0); + assert!(region.is_empty()); + } +} diff --git a/pipe/src/traits.rs b/pipe/src/traits.rs new file mode 100644 index 0000000..a9b435c --- /dev/null +++ b/pipe/src/traits.rs @@ -0,0 +1,35 @@ +use crate::error::PipeError; + +/// Read bytes from a pipe. Analogous to std::io::Read. +/// +/// Partial reads are normal — `read` may return fewer bytes than `buf.len()`. +/// The caller loops if it needs more. This matches `std::io` semantics. +pub trait Read { + /// Try to read bytes into `buf`. + /// + /// Returns `Ok(n)` where `n > 0` is the number of bytes read, + /// or `Err(WouldBlock)` if no data is currently available. + fn read(&mut self, buf: &mut [u8]) -> Result; +} + +/// Write bytes to a pipe. Analogous to std::io::Write. +/// +/// Partial writes are normal — `write` may accept fewer bytes than `buf.len()`. +/// The caller loops if it needs to write more. This matches `std::io` semantics. +pub trait Write { + /// Try to write bytes from `buf`. + /// + /// Returns `Ok(n)` where `n > 0` is the number of bytes written, + /// or `Err(WouldBlock)` if the ring is currently full. + fn write(&mut self, buf: &[u8]) -> Result; + + /// Write all bytes in `src`, spinning on `WouldBlock` until every byte is written. + fn write_all(&mut self, mut src: &[u8]) { + while !src.is_empty() { + match self.write(src) { + Ok(n) => src = &src[n..], + Err(PipeError::WouldBlock) => core::hint::spin_loop(), + } + } + } +} diff --git a/test/Cargo.lock b/test/Cargo.lock new file mode 100644 index 0000000..a6c6281 --- /dev/null +++ b/test/Cargo.lock @@ -0,0 +1,61 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "arca-pipe" +version = "0.1.0" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "libc" +version = "0.2.185" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" + +[[package]] +name = "memmap2" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +dependencies = [ + "libc", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "pipe-test" +version = "0.1.0" +dependencies = [ + "arca-pipe", + "memmap2", + "nix", +] diff --git a/test/Cargo.toml b/test/Cargo.toml new file mode 100644 index 0000000..ec2be2b --- /dev/null +++ b/test/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "pipe-test" +version = "0.1.0" +edition = "2021" + +[dependencies] +arca-pipe = { path = "../pipe" } +memmap2 = "0.9" +nix = { version = "0.29", features = ["fs", "mman", "feature"] } diff --git a/test/src/bin/ring_reader_correctness.rs b/test/src/bin/ring_reader_correctness.rs new file mode 100644 index 0000000..2d7156c --- /dev/null +++ b/test/src/bin/ring_reader_correctness.rs @@ -0,0 +1,70 @@ +/// Reader side of the ring-buffer correctness test. +/// +/// Polls for a POSIX shared memory region created by ring_writer_correctness, +/// maps it, and reads `transfer_bytes` from the B-side of the BidirectionalPipe. +/// Prints an XOR checksum of all bytes received. +/// +/// Usage: ring_reader_correctness [chunk_bytes] +use memmap2::MmapMut; +use nix::fcntl::OFlag; +use nix::sys::mman::{shm_open, shm_unlink}; +use nix::sys::stat::Mode; +use arca_pipe::{BidirectionalPipe, SharedMemoryRegion, Side}; +use arca_pipe::Read as PipeRead; + +const RING_SIZE: u64 = 64 * 1024; + +fn main() { + let args: Vec = std::env::args().collect(); + if args.len() < 3 { + eprintln!("Usage: {} [chunk_bytes]", args[0]); + std::process::exit(1); + } + + let shm_name = format!("/{}", args[1].trim_start_matches('/')); + let transfer_size: u64 = args[2].parse().expect("transfer_bytes must be a number"); + let chunk_size: usize = args + .get(3) + .map(|s| s.parse().expect("chunk_bytes must be a number")) + .unwrap_or(4096); + + let required = BidirectionalPipe::required_size(RING_SIZE) as usize; + + // Poll until the writer has created the shared memory object. + println!("Reader: waiting for writer to create shared memory '{}'...", shm_name); + let fd = loop { + match shm_open(shm_name.as_str(), OFlag::O_RDWR, Mode::empty()) { + Ok(fd) => break fd, + Err(_) => std::thread::sleep(std::time::Duration::from_millis(10)), + } + }; + println!("Reader: shared memory found, mapping {} bytes", required); + + // Safety: fd is valid and points to `required` bytes written by the writer. + let mut mmap = unsafe { MmapMut::map_mut(&fd).expect("mmap failed") }; + + let region = unsafe { SharedMemoryRegion::from_raw(mmap.as_mut_ptr(), required as u64) }; + let mut pipe = BidirectionalPipe::new(®ion, RING_SIZE, Side::B); + + let mut buf = vec![0u8; chunk_size]; + let mut pos: u64 = 0; + let mut xor: u8 = 0; + + while pos < transfer_size { + let remaining = (transfer_size - pos) as usize; + let to_read = remaining.min(chunk_size); + + match pipe.read(&mut buf[..to_read]) { + Ok(n) => { + for i in 0..n { xor ^= buf[i]; } + pos += n as u64; + } + Err(_) => std::hint::spin_loop(), + } + } + + println!("Reader: received {} bytes XOR=0x{:04X}", pos, xor); + + drop(mmap); + shm_unlink(shm_name.as_str()).expect("shm_unlink failed"); +} diff --git a/test/src/bin/ring_reader_perf.rs b/test/src/bin/ring_reader_perf.rs new file mode 100644 index 0000000..f481410 --- /dev/null +++ b/test/src/bin/ring_reader_perf.rs @@ -0,0 +1,114 @@ +/// Reader side of the ring-buffer throughput test. +/// +/// Polls for the POSIX shared memory region created by ring_writer_perf, maps +/// it, signals the writer to start, then reads `transfer_bytes` and prints +/// GB/s throughput. +/// +/// Usage: ring_reader_perf [chunk_bytes] +use std::hint::spin_loop; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::Instant; +use memmap2::MmapMut; +use nix::fcntl::OFlag; +use nix::sys::mman::{shm_open, shm_unlink}; +use nix::sys::stat::Mode; +use nix::unistd::{sysconf, SysconfVar}; +use arca_pipe::{BidirectionalPipe, SharedMemoryRegion, Side}; +use arca_pipe::Read as PipeRead; + +const RING_SIZE: u64 = 1024 * 1024; +const CTRL_BYTES: usize = 64; + +fn main() { + let args: Vec = std::env::args().collect(); + if args.len() < 3 { + eprintln!("Usage: {} [chunk_bytes]", args[0]); + std::process::exit(1); + } + + let shm_name = format!("/{}", args[1].trim_start_matches('/')); + let transfer_size: u64 = args[2].parse().expect("transfer_bytes must be a number"); + let chunk_size: usize = args + .get(3) + .map(|s| s.parse().expect("chunk_bytes must be a number")) + .unwrap_or(4096); + + let pipe_size = BidirectionalPipe::required_size(RING_SIZE) as usize; + let total_size = CTRL_BYTES + pipe_size; + + println!("Reader: waiting for writer to create shared memory '{}'...", shm_name); + let fd = loop { + match shm_open(shm_name.as_str(), OFlag::O_RDWR, Mode::empty()) { + Ok(fd) => break fd, + Err(_) => std::thread::sleep(std::time::Duration::from_millis(10)), + } + }; + println!("Reader: shared memory found, mapping {} bytes", total_size); + + let mut mmap = unsafe { MmapMut::map_mut(&fd).expect("mmap failed") }; + + // store CTRL_BYTES on shared mem + let ready: &AtomicU64 = unsafe { &*(mmap.as_ptr() as *const AtomicU64) }; + let region = unsafe { + SharedMemoryRegion::from_raw(mmap.as_mut_ptr().add(CTRL_BYTES), pipe_size as u64) + }; + let mut pipe = BidirectionalPipe::new(®ion, RING_SIZE, Side::B); + + let mut dst = vec![0u8; transfer_size as usize]; + + // Pre-fault all pages so allocation cost doesn't hit the timed path. + let page_size = sysconf(SysconfVar::PAGE_SIZE).unwrap().unwrap() as usize; + for i in (0..dst.len()).step_by(page_size) { + dst[i] = 1; + } + + let ckpt_total: u64 = 10; + let ckpt_sz = (transfer_size + ckpt_total - 1) / ckpt_total; + let mut ckpt_next = ckpt_sz; + + // Signal writer to start. + ready.store(1, Ordering::Release); + println!("Reader: signaled writer, waiting for data..."); + + let start = Instant::now(); + eprintln!("--- Reader checkpoint 0/{}", ckpt_total); + + let mut read: u64 = 0; + while read < transfer_size { + let remaining = (transfer_size - read) as usize; + let to_read = remaining.min(chunk_size); + match pipe.read(&mut dst[read as usize..read as usize + to_read]) { + Ok(n) => { + read += n as u64; + if read >= ckpt_next { + eprintln!( + "--- Reader checkpoint {}/{} elapsed: {:.3}s", + ckpt_next / ckpt_sz, + ckpt_total, + start.elapsed().as_secs_f64() + ); + ckpt_next += ckpt_sz; + } + } + Err(_) => spin_loop(), + } + } + + let elapsed = start.elapsed(); + + println!("========================================"); + println!("READER STATS"); + println!("========================================"); + println!("Total time: {} µs {:.6} s", elapsed.as_micros(), elapsed.as_secs_f64()); + println!( + "Throughput: {:.4} GB/s", + read as f64 / (1024.0 * 1024.0 * 1024.0 * elapsed.as_secs_f64()) + ); + println!("========================================"); + + // Signal writer we are done so it can unmap cleanly. + ready.store(0, Ordering::Relaxed); + + drop(mmap); + shm_unlink(shm_name.as_str()).expect("shm_unlink failed"); +} diff --git a/test/src/bin/ring_writer_correctness.rs b/test/src/bin/ring_writer_correctness.rs new file mode 100644 index 0000000..05ff12c --- /dev/null +++ b/test/src/bin/ring_writer_correctness.rs @@ -0,0 +1,75 @@ +/// Writer side of the ring-buffer correctness test. +/// +/// Creates a POSIX shared memory region, lays out a BidirectionalPipe over it +/// (Side::A), and streams `transfer_bytes` of a fixed pattern into the A→B +/// channel. Prints an XOR checksum of all bytes sent. +/// +/// Usage: ring_writer_correctness [chunk_bytes] +use memmap2::MmapMut; +use nix::fcntl::OFlag; +use nix::sys::mman::shm_open; +use nix::sys::stat::Mode; +use nix::unistd::ftruncate; +use arca_pipe::{BidirectionalPipe, SharedMemoryRegion, Side}; +use arca_pipe::Write as PipeWrite; + +const RING_SIZE: u64 = 64 * 1024; + +fn main() { + let args: Vec = std::env::args().collect(); + if args.len() < 3 { + eprintln!("Usage: {} [chunk_bytes]", args[0]); + std::process::exit(1); + } + + let shm_name = format!("/{}", args[1].trim_start_matches('/')); + let transfer_size: u64 = args[2].parse().expect("transfer_bytes must be a number"); + let chunk_size: usize = args + .get(3) + .map(|s| s.parse().expect("chunk_bytes must be a number")) + .unwrap_or(4096); + + let required = BidirectionalPipe::required_size(RING_SIZE) as usize; + + let fd = shm_open( + shm_name.as_str(), + OFlag::O_CREAT | OFlag::O_RDWR, + Mode::from_bits_truncate(0o666), + ) + .expect("shm_open failed"); + + ftruncate(&fd, required as i64).expect("ftruncate failed"); + + // Safety: fd is valid and points to `required` zero-initialised bytes. + let mut mmap = unsafe { MmapMut::map_mut(&fd).expect("mmap failed") }; + + // Zero out to clear any stale cursor state if shm name was reused. + mmap.fill(0); + + let region = unsafe { SharedMemoryRegion::from_raw(mmap.as_mut_ptr(), required as u64) }; + let mut pipe = BidirectionalPipe::new(®ion, RING_SIZE, Side::A); + + let buf: Vec = (0..chunk_size).map(|i| ((i % 255) + 1) as u8).collect(); + let mut pos: u64 = 0; + let mut xor: u8 = 0; + + println!("Writer: shared memory created ({}), starting write of {} bytes", shm_name, transfer_size); + + while pos < transfer_size { + let remaining = (transfer_size - pos) as usize; + let to_write = remaining.min(chunk_size); + + match pipe.write(&buf[..to_write]) { + Ok(n) => { + for i in 0..n { xor ^= buf[i]; } + pos += n as u64; + } + Err(_) => std::hint::spin_loop(), + } + } + + println!("Writer: sent {} bytes XOR=0x{:04X}", pos, xor); + + // Reader is responsible for shm_unlink. + drop(mmap); +} diff --git a/test/src/bin/ring_writer_perf.rs b/test/src/bin/ring_writer_perf.rs new file mode 100644 index 0000000..8cea025 --- /dev/null +++ b/test/src/bin/ring_writer_perf.rs @@ -0,0 +1,117 @@ +/// Writer side of the ring-buffer throughput test. +/// +/// Creates a POSIX shared memory region with a ready flag followed by +/// a BidirectionalPipe (Side::A). Waits for the reader to signal ready, then +/// streams `transfer_bytes` and prints GB/s throughput. +/// +/// Usage: ring_writer_perf [chunk_bytes] +use std::hint::spin_loop; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::Instant; +use memmap2::MmapMut; +use nix::fcntl::OFlag; +use nix::sys::mman::shm_open; +use nix::sys::stat::Mode; +use nix::unistd::ftruncate; +use arca_pipe::{BidirectionalPipe, SharedMemoryRegion, Side}; +use arca_pipe::Write as PipeWrite; + +const RING_SIZE: u64 = 1024 * 1024; +// One cache line of control space before the ring region to avoid false sharing. +const CTRL_BYTES: usize = 64; + +fn main() { + let args: Vec = std::env::args().collect(); + if args.len() < 3 { + eprintln!("Usage: {} [chunk_bytes]", args[0]); + std::process::exit(1); + } + + let shm_name = format!("/{}", args[1].trim_start_matches('/')); + let transfer_size: u64 = args[2].parse().expect("transfer_bytes must be a number"); + let chunk_size: usize = args + .get(3) + .map(|s| s.parse().expect("chunk_bytes must be a number")) + .unwrap_or(4096); + + let pipe_size = BidirectionalPipe::required_size(RING_SIZE) as usize; + let total_size = CTRL_BYTES + pipe_size; + + let fd = shm_open( + shm_name.as_str(), + OFlag::O_CREAT | OFlag::O_RDWR, + Mode::from_bits_truncate(0o666), + ) + .expect("shm_open failed"); + + ftruncate(&fd, total_size as i64).expect("ftruncate failed"); + + let mut mmap = unsafe { MmapMut::map_mut(&fd).expect("mmap failed") }; + + // Zero out the entire region before use + mmap.fill(0); + + let ready: &AtomicU64 = unsafe { &*(mmap.as_ptr() as *const AtomicU64) }; + let region = unsafe { + SharedMemoryRegion::from_raw(mmap.as_mut_ptr().add(CTRL_BYTES), pipe_size as u64) + }; + let mut pipe = BidirectionalPipe::new(®ion, RING_SIZE, Side::A); + + // hard-coded buffer to transfer + let buf: Vec = (0..chunk_size).map(|i| ((i % 255) + 1) as u8).collect(); + + let ckpt_total: u64 = 10; + let ckpt_sz = (transfer_size + ckpt_total - 1) / ckpt_total; + let mut ckpt_next = ckpt_sz; + + println!("Writer: waiting for reader to signal ready..."); + while ready.load(Ordering::Acquire) == 0 { + spin_loop(); + } + + println!("Writer: reader ready, starting write of {} bytes", transfer_size); + let start = Instant::now(); + eprintln!("--- Writer checkpoint 0/{}", ckpt_total); + + let mut written: u64 = 0; + while written < transfer_size { + let remaining = (transfer_size - written) as usize; + let to_write = remaining.min(chunk_size); + match pipe.write(&buf[..to_write]) { + Ok(n) => { + written += n as u64; + if written >= ckpt_next { + eprintln!( + "--- Writer checkpoint {}/{} elapsed: {:.3}s", + ckpt_next / ckpt_sz, + ckpt_total, + start.elapsed().as_secs_f64() + ); + ckpt_next += ckpt_sz; + } + } + Err(_) => spin_loop(), + } + } + + let elapsed = start.elapsed(); + + // Wait for reader to finish draining before unmapping. + while ready.load(Ordering::Relaxed) != 0 { + spin_loop(); + } + + println!("========================================"); + println!("WRITER STATS"); + println!("========================================"); + println!("Total time: {} µs {:.6} s", elapsed.as_micros(), elapsed.as_secs_f64()); + println!("Data written: {} bytes", written); + println!( + "Throughput: {:.4} GB/s", + written as f64 / (1024.0 * 1024.0 * 1024.0 * elapsed.as_secs_f64()) + ); + println!("========================================"); + + // Reader is responsible for shm_unlink. + drop(mmap); +}