From 0da83139a423131928e9e7f4525673afde3ba2f8 Mon Sep 17 00:00:00 2001 From: lstocchi Date: Tue, 24 Mar 2026 16:26:09 +0100 Subject: [PATCH 1/2] utils: add Windows epoll implementation backed by I/O Completion Ports This introduces an epoll-compatible polling abstraction for Windows by leveraging I/O Completion Ports (IOCP) as a central event multiplexer. By bypassing the standard `WaitForMultipleObjects` 64-handle limit, this architecture gives us true O(1) wake-ups with no handle-count limitations. When an event is added to the epoll, we heap-allocate a `Watch` struct and attach a Wait Completion Packet (WCP) to it. The raw memory pointer of this struct is passed to the kernel as the completion key. When the event signals, the kernel pushes the WCP to the IOCP queue. The waiting thread pops the packet and reads the pointer, allowing us to process events with zero heap allocations and a completely lock-free hot path. When deleting an event, the WCP is closed and the `Watch` memory is moved to a "zombie" list with a 5-second garbage collection delay. Because the IOCP queue is managed asynchronously by the Windows kernel, a deleted event might still have a completion packet in-flight to a worker thread. This GC window safely drains those ghost packets before freeing the memory, preventing Use-After-Free segfaults. One calculated tradeoff is the reliance on the undocumented Windows NT native API (`NtAssociateWaitCompletionPacket`). While unofficial, it has been stable in the Windows kernel for over a decade and is currently the only way to achieve VMM-grade, O(1) polling performance for arbitrary handles. Additional details: - EventSet bit values mirror the macOS implementation for cross-platform portability. - Adds `windows-sys` as a Windows-only dependency for OS APIs. Signed-off-by: lstocchi --- Cargo.lock | 1 + examples/Cargo.lock | 454 +++++++++---------- src/utils/Cargo.toml | 10 +- src/utils/src/lib.rs | 4 + src/utils/src/windows/bindings.rs | 58 +++ src/utils/src/windows/epoll.rs | 700 ++++++++++++++++++++++++++++++ src/utils/src/windows/mod.rs | 13 + 7 files changed, 996 insertions(+), 244 deletions(-) create mode 100644 src/utils/src/windows/bindings.rs create mode 100644 src/utils/src/windows/epoll.rs create mode 100644 src/utils/src/windows/mod.rs diff --git a/Cargo.lock b/Cargo.lock index e606319fd..4da224080 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1757,6 +1757,7 @@ dependencies = [ "log", "nix 0.30.1", "vmm-sys-util 0.14.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/examples/Cargo.lock b/examples/Cargo.lock index 5f46ca0f2..2052db649 100644 --- a/examples/Cargo.lock +++ b/examples/Cargo.lock @@ -4,18 +4,18 @@ version = 4 [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] [[package]] name = "anstream" -version = "0.6.19" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -28,33 +28,33 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.3" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ "windows-sys", ] [[package]] name = "anstyle-wincon" -version = "3.0.9" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", @@ -63,9 +63,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.98" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "atty" @@ -90,7 +90,7 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.0", "cexpr", "clang-sys", "itertools", @@ -110,17 +110,17 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.1" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "cairo-rs" -version = "0.21.0" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6466a563dea2e99f59f6ffbb749fd0bdf75764f5e6e93976b5e7bd73c4c9efb" +checksum = "b01fe135c0bd16afe262b6dea349bd5ea30e6de50708cec639aae7c5c14cc7e4" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.0", "cairo-sys-rs", "glib", "libc", @@ -128,9 +128,9 @@ dependencies = [ [[package]] name = "cairo-sys-rs" -version = "0.21.0" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab7e9f13c802625aad1ad2b4ae3989f4ce9339ff388f335a6f109f9338705e2" +checksum = "06c28280c6b12055b5e39e4554271ae4e6630b27c0da9148c4cf6485fc6d245c" dependencies = [ "glib-sys", "libc", @@ -148,9 +148,9 @@ dependencies = [ [[package]] name = "cfg-expr" -version = "0.20.1" +version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d0390889d58f934f01cd49736275b4c2da15bcfc328c78ff2349907e6cabf22" +checksum = "3c6b04e07d8080154ed4ac03546d9a2b303cc2fe1901ba0b35b301516e289368" dependencies = [ "smallvec", "target-lexicon", @@ -158,9 +158,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" @@ -180,18 +180,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.41" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.41" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -201,9 +201,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.41" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" dependencies = [ "heck", "proc-macro2", @@ -213,15 +213,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.5" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "crossbeam-channel" @@ -275,24 +275,24 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", ] [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -301,15 +301,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", @@ -318,29 +318,28 @@ dependencies = [ [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", "futures-macro", "futures-task", "pin-project-lite", - "pin-utils", "slab", ] [[package]] name = "gdk-pixbuf" -version = "0.21.0" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "688dc7eaf551dbac1f5b11d000d089c3db29feb25562455f47c1a2080cc60bda" +checksum = "debb0d39e3cdd84626edfd54d6e4a6ba2da9a0ef2e796e691c4e9f8646fda00c" dependencies = [ "gdk-pixbuf-sys", "gio", @@ -350,9 +349,9 @@ dependencies = [ [[package]] name = "gdk-pixbuf-sys" -version = "0.21.0" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5af1823d3d1cb72616873ba0a593bd440eb92da700fdfb047505a21ee3ec3e10" +checksum = "bd95ad50b9a3d2551e25dd4f6892aff0b772fe5372d84514e9d0583af60a0ce7" dependencies = [ "gio-sys", "glib-sys", @@ -363,9 +362,9 @@ dependencies = [ [[package]] name = "gdk4" -version = "0.10.0" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a67b064d2f35e649232455c7724f56f977555d2608c43300eabc530eaa4e359" +checksum = "756564212bbe4a4ce05d88ffbd2582581ac6003832d0d32822d0825cca84bfbf" dependencies = [ "cairo-rs", "gdk-pixbuf", @@ -378,9 +377,9 @@ dependencies = [ [[package]] name = "gdk4-sys" -version = "0.10.0" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2edbda0d879eb85317bdb49a3da591ed70a804a10776e358ef416be38c6db2c5" +checksum = "a6d4e5b3ccf591826a4adcc83f5f57b4e59d1925cb4bf620b0d645f79498b034" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", @@ -395,9 +394,9 @@ dependencies = [ [[package]] name = "gio" -version = "0.21.0" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "273d64c833fbbf7cd86c4cdced893c5d3f2f5d6aeb30fd0c30d172456ce8be2e" +checksum = "c5ff48bf600c68b476e61dc6b7c762f2f4eb91deef66583ba8bb815c30b5811a" dependencies = [ "futures-channel", "futures-core", @@ -412,9 +411,9 @@ dependencies = [ [[package]] name = "gio-sys" -version = "0.21.0" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c8130f5810a839d74afc3a929c34a700bf194972bb034f2ecfe639682dd13cc" +checksum = "0071fe88dba8e40086c8ff9bbb62622999f49628344b1d1bf490a48a29d80f22" dependencies = [ "glib-sys", "gobject-sys", @@ -425,11 +424,11 @@ dependencies = [ [[package]] name = "glib" -version = "0.21.0" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "690e8bcf8a819b5911d6ae79879226191d01253a4f602748072603defd5b9553" +checksum = "16de123c2e6c90ce3b573b7330de19be649080ec612033d397d72da265f1bd8b" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.0", "futures-channel", "futures-core", "futures-executor", @@ -446,9 +445,9 @@ dependencies = [ [[package]] name = "glib-macros" -version = "0.21.0" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e772291ebea14c28eb11bb75741f62f4a4894f25e60ce80100797b6b010ef0f9" +checksum = "cf59b675301228a696fe01c3073974643365080a76cc3ed5bc2cbc466ad87f17" dependencies = [ "heck", "proc-macro-crate", @@ -459,9 +458,9 @@ dependencies = [ [[package]] name = "glib-sys" -version = "0.21.0" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b2be4c74454fb4a6bd3328320737d0fa3d6939e2d570f5d846da00cb222f6a0" +checksum = "2d95e1a3a19ae464a7286e14af9a90683c64d70c02532d88d87ce95056af3e6c" dependencies = [ "libc", "system-deps", @@ -469,15 +468,15 @@ dependencies = [ [[package]] name = "glob" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "gobject-sys" -version = "0.21.0" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab318a786f9abd49d388013b9161fa0ef8218ea6118ee7111c95e62186f7d31f" +checksum = "2dca35da0d19a18f4575f3cb99fe1c9e029a2941af5662f326f738a21edaf294" dependencies = [ "glib-sys", "libc", @@ -486,9 +485,9 @@ dependencies = [ [[package]] name = "graphene-rs" -version = "0.21.0" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0487f78e8a772ec89020458fbabadd1332bc1e3236ca1c63ef1d61afd4e5f2cc" +checksum = "2730030ac9db663fd8bfe1e7093742c1cafb92db9c315c9417c29032341fe2f9" dependencies = [ "glib", "graphene-sys", @@ -497,9 +496,9 @@ dependencies = [ [[package]] name = "graphene-sys" -version = "0.21.0" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "270cefb6b270fcb2ef9708c3a35c0e25c2e831dac28d75c4f87e5ad3540c9543" +checksum = "915e32091ea9ad241e4b044af62b7351c2d68aeb24f489a0d7f37a0fc484fd93" dependencies = [ "glib-sys", "libc", @@ -509,9 +508,9 @@ dependencies = [ [[package]] name = "gsk4" -version = "0.10.0" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5dbe33ceed6fc20def67c03d36e532f5a4a569ae437ae015a7146094f31e10c" +checksum = "e755de9d8c5896c5beaa028b89e1969d067f1b9bf1511384ede971f5983aa153" dependencies = [ "cairo-rs", "gdk4", @@ -524,9 +523,9 @@ dependencies = [ [[package]] name = "gsk4-sys" -version = "0.10.0" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d76011d55dd19fde16ffdedee08877ae6ec942818cfa7bc08a91259bc0b9fc9" +checksum = "7ce91472391146f482065f1041876d8f869057b195b95399414caa163d72f4f7" dependencies = [ "cairo-sys-rs", "gdk4-sys", @@ -540,9 +539,9 @@ dependencies = [ [[package]] name = "gtk4" -version = "0.10.0" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "938d68ad43080ad5ee710c30d467c1bc022ee5947856f593855691d726305b3e" +checksum = "acb21d53cfc6f7bfaf43549731c43b67ca47d87348d81c8cfc4dcdd44828e1a4" dependencies = [ "cairo-rs", "field-offset", @@ -561,9 +560,9 @@ dependencies = [ [[package]] name = "gtk4-macros" -version = "0.10.0" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0912d2068695633002b92c5966edc108b2e4f54b58c509d1eeddd4cbceb7315c" +checksum = "3ccfb5a14a3d941244815d5f8101fa12d4577b59cc47245778d8d907b0003e42" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -573,9 +572,9 @@ dependencies = [ [[package]] name = "gtk4-sys" -version = "0.10.0" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a923bdcf00e46723801162de24432cbce38a6810e0178a2d0b6dd4ecc26a1c74" +checksum = "842577fe5a1ee15d166cd3afe804ce0cab6173bc789ca32e21308834f20088dd" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", @@ -621,9 +620,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.4" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "heck" @@ -642,15 +641,15 @@ dependencies = [ [[package]] name = "humantime" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" [[package]] name = "indexmap" -version = "2.10.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown", @@ -658,9 +657,9 @@ dependencies = [ [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" @@ -684,7 +683,7 @@ name = "krun_display" version = "0.1.0" dependencies = [ "bindgen", - "bitflags 2.9.1", + "bitflags 2.11.0", "log", "static_assertions", "thiserror", @@ -695,7 +694,7 @@ name = "krun_input" version = "0.1.0" dependencies = [ "bindgen", - "bitflags 2.9.1", + "bitflags 2.11.0", "libc", "log", "static_assertions", @@ -704,30 +703,30 @@ dependencies = [ [[package]] name = "kvm-bindings" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf3432d9f609fbede9f624d1dbefcce77985a9322de1d0e6d460ec05502b7fd0" +checksum = "4b3c06ff73c7ce03e780887ec2389d62d2a2a9ddf471ab05c2ff69207cd3f3b4" dependencies = [ "vmm-sys-util", ] [[package]] name = "libc" -version = "0.2.174" +version = "0.2.184" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" [[package]] name = "log" -version = "0.4.27" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "memchr" -version = "2.7.5" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memoffset" @@ -750,7 +749,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.11.0", "cfg-if", "cfg_aliases", "libc", @@ -768,15 +767,15 @@ dependencies = [ [[package]] name = "once_cell_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "pango" -version = "0.21.0" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d4803f086c4f49163c31ac14db162112a22401c116435080e4be8678c507d61" +checksum = "52d1d85e2078077a065bb7fc072783d5bcd4e51b379f22d67107d0a16937eb69" dependencies = [ "gio", "glib", @@ -786,9 +785,9 @@ dependencies = [ [[package]] name = "pango-sys" -version = "0.21.0" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66872b3cfd328ad6d1a4f89ebd5357119bd4c592a4ddbb8f6bc2386f8ce7b898" +checksum = "b4f06627d36ed5ff303d2df65211fc2e52ba5b17bf18dd80ff3d9628d6e06cfd" dependencies = [ "glib-sys", "gobject-sys", @@ -798,15 +797,9 @@ dependencies = [ [[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" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pkg-config" @@ -816,36 +809,36 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "proc-macro-crate" -version = "3.3.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ "toml_edit", ] [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] [[package]] name = "regex" -version = "1.11.1" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -855,9 +848,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -866,15 +859,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rustc_version" @@ -887,24 +880,24 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] -name = "serde" -version = "1.0.219" +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -913,11 +906,11 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.9" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -928,9 +921,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "slab" -version = "0.4.10" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -952,9 +945,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.104" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -963,9 +956,9 @@ dependencies = [ [[package]] name = "system-deps" -version = "7.0.5" +version = "7.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4be53aa0cba896d2dc615bd42bbc130acdcffa239e0a2d965ea5b3b2a86ffdb" +checksum = "48c8f33736f986f16d69b6cb8b03f55ddcad5c41acc4ccc39dd88e84aa805e7f" dependencies = [ "cfg-expr", "heck", @@ -976,9 +969,9 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" +checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" [[package]] name = "termcolor" @@ -991,18 +984,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -1011,43 +1004,69 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.23" +version = "0.9.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ - "serde", + "indexmap", + "serde_core", "serde_spanned", - "toml_datetime", - "toml_edit", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[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_datetime" -version = "0.6.11" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ - "serde", + "serde_core", ] [[package]] name = "toml_edit" -version = "0.22.27" +version = "0.25.9+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +checksum = "da053d28fe57e2c9d21b48261e14e7b4c8b670b54d2c684847b91feaf4c7dac5" dependencies = [ "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "winnow", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.1", +] + +[[package]] +name = "toml_parser" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ca317ebc49f06bd748bfba29533eac9485569dc9bf80b849024b025e814fb9" +dependencies = [ + "winnow 1.0.1", ] +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "utf8parse" @@ -1066,19 +1085,20 @@ dependencies = [ "log", "nix", "vmm-sys-util", + "windows-sys", ] [[package]] name = "version-compare" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" [[package]] name = "vmm-sys-util" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d21f366bf22bfba3e868349978766a965cbe628c323d58e026be80b8357ab789" +checksum = "506c62fdf617a5176827c2f9afbcf1be155b03a9b4bf9617a60dbc07e3a1642f" dependencies = [ "bitflags 1.3.2", "libc", @@ -1102,9 +1122,9 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ "windows-sys", ] @@ -1116,83 +1136,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows-sys" -version = "0.59.0" +name = "windows-link" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets", -] +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] -name = "windows-targets" -version = "0.52.6" +name = "windows-sys" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows-link", ] [[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_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[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_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[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_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" +name = "winnow" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" [[package]] name = "winnow" -version = "0.7.12" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" dependencies = [ "memchr", ] diff --git a/src/utils/Cargo.toml b/src/utils/Cargo.toml index 482eb62ae..7621d25c0 100644 --- a/src/utils/Cargo.toml +++ b/src/utils/Cargo.toml @@ -16,4 +16,12 @@ crossbeam-channel = ">=0.5.15" kvm-bindings = { version = ">=0.11", features = ["fam-wrappers"] } [target.'cfg(target_os = "macos")'.dependencies] -nix = { version = "0.30.1", features = ["fs"] } \ No newline at end of file +nix = { version = "0.30.1", features = ["fs"] } + +[target.'cfg(target_os = "windows")'.dependencies] +windows-sys = { version = "0.61.2", features = [ + "Win32_Foundation", + "Win32_Security", + "Win32_System_IO", + "Win32_System_Threading", +] } \ No newline at end of file diff --git a/src/utils/src/lib.rs b/src/utils/src/lib.rs index f3b22a37b..75ced007e 100644 --- a/src/utils/src/lib.rs +++ b/src/utils/src/lib.rs @@ -16,6 +16,10 @@ pub mod macos; pub use macos::epoll; #[cfg(target_os = "macos")] pub use macos::eventfd; +#[cfg(target_os = "windows")] +pub mod windows; +#[cfg(target_os = "windows")] +pub use windows::epoll; pub mod pollable_channel; #[cfg(target_arch = "x86_64")] pub mod rand; diff --git a/src/utils/src/windows/bindings.rs b/src/utils/src/windows/bindings.rs new file mode 100644 index 000000000..0c4fa47e3 --- /dev/null +++ b/src/utils/src/windows/bindings.rs @@ -0,0 +1,58 @@ +//! Windows FFI bindings used by the epoll and eventfd implementations. +//! +//! Documented Win32 APIs come from the [`windows_sys`] crate. The +//! undocumented NT native APIs (`NtCreateWaitCompletionPacket`, etc.) are +//! declared manually since they are not included in any official bindings +//! crate. + +use std::io; + +use windows_sys::Win32::Foundation::HANDLE; + +#[allow(non_camel_case_types)] +pub type NTSTATUS = i32; + +#[link(name = "ntdll")] +extern "system" { + pub fn NtCreateWaitCompletionPacket( + WaitCompletionPacketHandle: *mut HANDLE, + DesiredAccess: u32, + ObjectAttributes: *const std::ffi::c_void, + ) -> NTSTATUS; + + pub fn NtAssociateWaitCompletionPacket( + WaitCompletionPacketHandle: HANDLE, + IoCompletionHandle: HANDLE, + TargetObjectHandle: HANDLE, + KeyContext: *mut std::ffi::c_void, + ApcContext: *mut std::ffi::c_void, + IoStatus: NTSTATUS, + IoStatusInformation: usize, + AlreadySignaled: *mut u8, + ) -> NTSTATUS; + + pub fn NtCancelWaitCompletionPacket( + WaitCompletionPacketHandle: HANDLE, + RemoveSignaledPacket: u8, + ) -> NTSTATUS; + + pub fn RtlNtStatusToDosError(Status: NTSTATUS) -> u32; +} + +/// Equivalent of the `NT_SUCCESS` macro: returns `true` when `status` is in +/// the success (0x0000_0000–0x3FFF_FFFF) or informational +/// (0x4000_0000–0x7FFF_FFFF) range. +/// https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/using-ntstatus-values +#[inline] +pub fn nt_success(status: NTSTATUS) -> bool { + status >= 0 +} + +/// Convert a failing `NTSTATUS` to an [`io::Error`] via the corresponding +/// Win32 error code. +/// A mapping of NTSTATUS values to Win32 error codes can be found at +/// https://www.osr.com/blog/2020/04/23/ntstatus-to-win32-error-code-mappings/ +pub fn nt_status_err(status: NTSTATUS) -> io::Error { + let win_err = unsafe { RtlNtStatusToDosError(status) }; + io::Error::from_raw_os_error(win_err as i32) +} diff --git a/src/utils/src/windows/epoll.rs b/src/utils/src/windows/epoll.rs new file mode 100644 index 000000000..c245022db --- /dev/null +++ b/src/utils/src/windows/epoll.rs @@ -0,0 +1,700 @@ +//! Epoll-like I/O event polling for Windows, backed by I/O Completion Ports. +//! +//! Uses an IOCP as the central event multiplexer with +//! `NtAssociateWaitCompletionPacket` to bridge waitable kernel handles (like +//! Windows Event objects used by `EventFd`) into the completion port. This +//! gives true O(1) wake-up with no handle-count limitations. +//! +//! ## How it works +//! +//! 1. [`Epoll::new`] creates an I/O Completion Port. +//! 2. [`Epoll::ctl`] with [`ControlOperation::Add`] heap-allocates a [`Watch`] +//! struct, creates a *Wait Completion Packet* (WCP) via the NT native API, +//! and associates the caller's waitable handle with the IOCP. The raw +//! `Watch` pointer is used as the completion key. +//! 3. [`Epoll::wait`] calls `GetQueuedCompletionStatusEx` which blocks until +//! one or more packets arrive (or timeout). The returned completion key is +//! cast back to a `Watch` pointer, and event metadata is read through +//! atomics -- **no locks are acquired on this path**. +//! 4. After delivering each event, `wait` **re-associates** (if level-triggered) +//! the WCP so the next signal produces another packet. +//! 5. [`Epoll::ctl`] with [`ControlOperation::Delete`] marks the watch as +//! inactive, closes the WCP handle, and moves the `Watch` allocation to a +//! zombie list. A time-based GC sweep in the same method frees zombies +//! older than 5 seconds, ensuring any in-flight packets referencing the +//! `Watch` memory have been drained. + +use log::debug; +use std::collections::HashMap; +use std::io; +use std::mem::MaybeUninit; +use std::ptr; +use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering}; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, Instant}; + +use bitflags::bitflags; + +use super::bindings::*; +use super::{AsRawFd, RawFd}; + +use windows_sys::Win32::Foundation::{ + CloseHandle, HANDLE, INVALID_HANDLE_VALUE, WAIT_TIMEOUT as WAIT_TIMEOUT_CODE, +}; +use windows_sys::Win32::System::Threading::INFINITE; +use windows_sys::Win32::System::IO::{ + CreateIoCompletionPort, GetQueuedCompletionStatusEx, OVERLAPPED_ENTRY, +}; + +// Generic access mask requesting all permissions the caller is allowed. +// https://learn.microsoft.com/en-us/windows/win32/secauthz/access-mask +const MAXIMUM_ALLOWED: u32 = 0x0200_0000; +const GC_THRESHOLD: Duration = Duration::from_secs(5); + +#[repr(i32)] +pub enum ControlOperation { + Add, + Modify, + Delete, +} + +bitflags! { + /// Bitmask of I/O readiness event types. + /// + /// Bit values are intentionally identical to the macOS implementation so + /// that device code using these constants is portable across all three + /// supported platforms. + pub struct EventSet: u32 { + /// The handle is ready for reading. + const IN = 0b00000001; + /// The handle is ready for writing. + const OUT = 0b00000010; + /// Hang-up (peer closed its end). + const HANG_UP = 0b00000100; + /// Read hang-up (peer shut down its write side). + const READ_HANG_UP = 0b00001000; + /// Request edge-triggered notification. The WCP is not re-armed + /// after delivery; use [`ControlOperation::Modify`] to re-register. + const EDGE_TRIGGERED = 0b00010000; + } +} + +/// Carrier for a readiness event, mirroring `libc::epoll_event` on Linux. +#[derive(Clone, Copy, Default)] +pub struct EpollEvent { + pub events: u32, + u64: u64, +} + +impl std::fmt::Debug for EpollEvent { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{{ events: {}, data: {} }}", self.events(), self.data()) + } +} + +impl EpollEvent { + /// Create a new event with the given readiness mask and user data. + pub fn new(events: EventSet, data: u64) -> Self { + EpollEvent { + events: events.bits(), + u64: data, + } + } + + pub fn events(&self) -> u32 { + self.events + } + + pub fn event_set(&self) -> EventSet { + // This unwrap is safe because `epoll_events` can only be user created or + // initialized by the kernel. We trust the kernel to only send us valid + // events. The user can only initialize `epoll_events` using valid events. + EventSet::from_bits(self.events()).unwrap() + } + + pub fn data(&self) -> u64 { + debug!("EpollEvent data: {}", self.u64); + self.u64 + } + + pub fn fd(&self) -> RawFd { + self.u64 as RawFd + } +} + +/// A watched handle and its associated metadata. +/// +/// Heap-allocated so its address can serve as the IOCP completion key. +/// This lets [`Epoll::wait`] access `Watch` fields through raw +/// pointers and atomics with no locking. +struct Watch { + fd: HANDLE, + wcp: HANDLE, + events: AtomicU32, + data: AtomicU64, + /// Cleared by [`ControlOperation::Delete`] so that in-flight completion + /// packets already queued by the kernel are silently ignored. + is_active: AtomicBool, +} + +/// The I/O Completion Port and the set of handles it is watching. +struct CompletionPort { + handle: HANDLE, + watches: Mutex>, + /// When you call ctl(Delete), we tell the kernel to cancel the WCP. + /// However, there is a possible race condition where + /// the kernel already signaled the handle and put the completion packet in the IOCP queue, + /// but the vCPU thread hasn't popped it out. + /// If we drop Watch immediately during ctl(Delete), + /// the VCPU will have a pointer to a freed resource resulting in a segfault. + /// Instead, we use this vector to maintain a list of Watch pointers and when + /// they were added so we can drop the Watch safely. + zombies: Mutex>, +} + +// All raw `*mut Watch` pointers are either behind a `Mutex` (in +// `watches` / `zombies`) or read-only in the lock-free `wait` path where +// the pointed-to fields are atomics. Windows `HANDLE` values are valid +// across threads. +unsafe impl Send for CompletionPort {} +unsafe impl Sync for CompletionPort {} + +impl Drop for CompletionPort { + fn drop(&mut self) { + for (_, ptr) in self.watches.get_mut().unwrap().drain() { + unsafe { + let w = Box::from_raw(ptr); + let _ = NtCancelWaitCompletionPacket(w.wcp, 1); + CloseHandle(w.wcp); + } + } + for (_, ptr) in self.zombies.get_mut().unwrap().drain(..) { + unsafe { + let _ = Box::from_raw(ptr); + } + } + unsafe { + CloseHandle(self.handle); + } + } +} + +/// Associate a Wait Completion Packet with the given IOCP and target handle. +/// +/// When `fd` becomes signaled, the kernel pushes a completion packet to +/// `iocp` carrying `key` as the completion key (a raw pointer to the +/// corresponding [`Watch`]). +fn associate_wcp( + wcp: HANDLE, + iocp: HANDLE, + fd: HANDLE, + key: *mut std::ffi::c_void, +) -> io::Result<()> { + let mut already_signaled: u8 = 0; + let status = unsafe { + NtAssociateWaitCompletionPacket( + wcp, + iocp, + fd, + key, + ptr::null_mut(), + 0, + 0, + &mut already_signaled, + ) + }; + if !nt_success(status) { + return Err(nt_status_err(status)); + } + Ok(()) +} + +/// Epoll-compatible polling abstraction backed by an I/O Completion Port. +#[derive(Clone)] +pub struct Epoll { + iocp: Arc, +} + +impl Epoll { + /// Create a new polling instance backed by a fresh I/O Completion Port. + pub fn new() -> io::Result { + let handle = + unsafe { CreateIoCompletionPort(INVALID_HANDLE_VALUE, std::ptr::null_mut(), 0, 0) }; + if handle == std::ptr::null_mut() { + return Err(io::Error::last_os_error()); + } + Ok(Epoll { + iocp: Arc::new(CompletionPort { + handle, + watches: Mutex::new(HashMap::new()), + zombies: Mutex::new(Vec::new()), + }), + }) + } + + /// Add, modify, or remove a handle in the interest set. + /// + /// * `fd` – the waitable handle (as [`RawFd`] / `HANDLE`). + /// * `event` – carries the desired [`EventSet`] mask and a `u64` data + /// payload that will be returned by [`wait`](Self::wait) when this + /// handle becomes ready. + pub fn ctl( + &self, + operation: ControlOperation, + fd: RawFd, + event: &EpollEvent, + ) -> io::Result<()> { + let mut watches = self.iocp.watches.lock().unwrap(); + match operation { + ControlOperation::Add => { + if watches.contains_key(&fd) { + return Err(io::Error::new( + io::ErrorKind::AlreadyExists, + "handle already registered", + )); + } + + let mut wcp: HANDLE = ptr::null_mut(); + let status = + unsafe { NtCreateWaitCompletionPacket(&mut wcp, MAXIMUM_ALLOWED, ptr::null()) }; + if !nt_success(status) { + return Err(nt_status_err(status)); + } + + let watch_ptr = Box::into_raw(Box::new(Watch { + fd, + wcp, + events: AtomicU32::new(event.events()), + data: AtomicU64::new(event.data()), + is_active: AtomicBool::new(true), + })); + + if let Err(e) = associate_wcp(wcp, self.iocp.handle, fd, watch_ptr as *mut _) { + unsafe { + CloseHandle(wcp); + let _ = Box::from_raw(watch_ptr); + } + return Err(e); + } + + watches.insert(fd, watch_ptr); + } + ControlOperation::Modify => { + let &watch_ptr = watches.get(&fd).ok_or_else(|| { + io::Error::new(io::ErrorKind::NotFound, "handle not registered") + })?; + + let watch = unsafe { &*watch_ptr }; + watch.events.store(event.events(), Ordering::Release); + watch.data.store(event.data(), Ordering::Release); + + unsafe { + let _ = NtCancelWaitCompletionPacket(watch.wcp, 1); + } + associate_wcp(watch.wcp, self.iocp.handle, fd, watch_ptr as *mut _)?; + } + ControlOperation::Delete => { + let watch_ptr = watches.remove(&fd).ok_or_else(|| { + io::Error::new(io::ErrorKind::NotFound, "handle not registered") + })?; + + let watch = unsafe { &*watch_ptr }; + watch.is_active.store(false, Ordering::Release); + unsafe { + let _ = NtCancelWaitCompletionPacket(watch.wcp, 1); + CloseHandle(watch.wcp); + } + + // Add the Watch to the zombies list with the current time + // so we can drop it safely after some delay. + let mut zombies = self.iocp.zombies.lock().unwrap(); + zombies.push((Instant::now(), watch_ptr)); + + zombies.retain(|(deleted_at, ptr)| { + if deleted_at.elapsed() > GC_THRESHOLD { + // Free the Watch from the heap + unsafe { + let _ = Box::from_raw(*ptr); + } + false // Remove from the vector + } else { + true // Keep in the vector + } + }); + } + } + Ok(()) + } + + /// Block until at least one registered handle is signaled, or until + /// `timeout` milliseconds have elapsed. Pass `-1` to wait indefinitely. + /// + /// This is the lock-free hot path: no `Mutex` is acquired and no heap + /// allocation is performed. The completion key returned by the kernel + /// is the raw `Watch` pointer set during [`ctl`](Self::ctl), so we can + /// read event metadata through atomics without any table lookup. + pub fn wait( + &self, + max_events: usize, + timeout: i32, + events: &mut [EpollEvent], + ) -> io::Result { + let iocp_handle = self.iocp.handle; + + let capacity = max_events.min(events.len()); + if capacity == 0 { + return Ok(0); + } + + const BATCH_SIZE: usize = 256; + let batch_limit = capacity.min(BATCH_SIZE); + let mut entries = [MaybeUninit::::uninit(); BATCH_SIZE]; + let mut count: u32 = 0; + let win_timeout: u32 = if timeout < 0 { + INFINITE + } else { + timeout as u32 + }; + + let ok = unsafe { + GetQueuedCompletionStatusEx( + iocp_handle, + entries[..batch_limit].as_mut_ptr() as *mut _, + batch_limit as u32, + &mut count, + win_timeout, + 0, + ) + }; + + if ok == 0 { + let err = io::Error::last_os_error(); + if err.raw_os_error() == Some(WAIT_TIMEOUT_CODE as i32) { + return Ok(0); + } + return Err(err); + } + + let entries_slice = unsafe { + std::slice::from_raw_parts(entries.as_ptr().cast::(), count as usize) + }; + let mut result_count = 0; + + for entry in entries_slice { + let watch_ptr = entry.lpCompletionKey as *const Watch; + if watch_ptr.is_null() { + continue; + } + + let watch = unsafe { &*watch_ptr }; + + if !watch.is_active.load(Ordering::Acquire) { + continue; + } + + let current_events = watch.events.load(Ordering::Acquire); + let event_set = EventSet::from_bits_truncate(current_events); + + events[result_count] = EpollEvent { + events: (event_set & (EventSet::IN | EventSet::OUT)).bits(), + u64: watch.data.load(Ordering::Acquire), + }; + result_count += 1; + + if !event_set.contains(EventSet::EDGE_TRIGGERED) { + // Level-triggered: re-associate the WCP so the next signal + // on this handle produces another completion packet. + // + // KNOWN RACE: there is a race between this call and + // `CloseHandle(watch.wcp)` in `Epoll::ctl(Delete)`. Another + // thread may delete the watch (marking it inactive and closing + // the WCP handle) between our `is_active` check above and the + // `associate_wcp` call here. In that case: + // + // 1. The WCP handle is already closed and + // `NtAssociateWaitCompletionPacket` returns + // `STATUS_INVALID_HANDLE` -- harmless, we ignore the error. + // 2. Windows recycles the handle value for a non-WCP object -- + // `NtAssociateWaitCompletionPacket` returns + // `STATUS_OBJECT_TYPE_MISMATCH` -- also harmless. + // 3. Windows recycles the handle value for a *new* WCP created + // by a third thread. The associate succeeds on the wrong + // WCP. When that third thread later tries to associate its + // own WCP it will fail and delete its handle, leaving the + // kernel to drop the WCP once its refcount reaches zero + // (the original delete already closed the userspace handle + // and the event was never queued to the IOCP). The only + // consequence is a lost event for the third thread, which + // should be re-queued on the next iteration. + // + // We should try to remove the GC mechanism but for now + // this is acceptable. + let _ = associate_wcp(watch.wcp, iocp_handle, watch.fd, watch_ptr as *mut _); + } + } + + Ok(result_count) + } +} + +impl AsRawFd for Epoll { + fn as_raw_fd(&self) -> RawFd { + self.iocp.handle + } +} + +#[cfg(test)] +mod tests { + use super::*; + use windows_sys::Win32::System::Threading::{CreateEventW, ResetEvent, SetEvent}; + + /// Create a manual-reset, initially non-signaled Windows Event object. + fn create_event() -> HANDLE { + let h = unsafe { CreateEventW(ptr::null(), 1, 0, ptr::null()) }; + assert!(h != std::ptr::null_mut(), "CreateEventW failed"); + h + } + + fn signal(handle: HANDLE) { + assert_ne!(unsafe { SetEvent(handle) }, 0, "SetEvent failed"); + } + + fn reset(handle: HANDLE) { + assert_ne!(unsafe { ResetEvent(handle) }, 0, "ResetEvent failed"); + } + + fn close(handle: HANDLE) { + unsafe { + CloseHandle(handle); + } + } + + #[test] + fn test_event_ops() { + let mut event = EpollEvent::default(); + assert_eq!(event.events(), 0); + assert_eq!(event.data(), 0); + + event = EpollEvent::new(EventSet::IN, 42); + assert_eq!(event.events(), EventSet::IN.bits()); + assert_eq!(event.event_set(), EventSet::IN); + assert_eq!(event.data(), 42); + assert_eq!(event.fd(), 42 as RawFd); + } + + #[test] + fn test_events_debug() { + let event = EpollEvent::new(EventSet::IN, 42); + assert_eq!(format!("{:?}", event), "{ events: 1, data: 42 }"); + } + + #[test] + fn test_ctl_add_modify_delete() { + let epoll = Epoll::new().unwrap(); + let ev = create_event(); + let event = EpollEvent::new(EventSet::IN, ev as u64); + + epoll.ctl(ControlOperation::Add, ev, &event).unwrap(); + assert!(epoll.ctl(ControlOperation::Add, ev, &event).is_err()); + + let event2 = EpollEvent::new(EventSet::OUT, ev as u64); + epoll.ctl(ControlOperation::Modify, ev, &event2).unwrap(); + + epoll + .ctl(ControlOperation::Delete, ev, &EpollEvent::default()) + .unwrap(); + assert!(epoll + .ctl(ControlOperation::Delete, ev, &EpollEvent::default()) + .is_err()); + assert!(epoll + .ctl(ControlOperation::Modify, ev, &EpollEvent::default()) + .is_err()); + + close(ev); + } + + #[test] + fn test_clone_shares_state() { + let epoll = Epoll::new().unwrap(); + let epoll2 = epoll.clone(); + let ev = create_event(); + let event = EpollEvent::new(EventSet::IN, ev as u64); + + epoll.ctl(ControlOperation::Add, ev, &event).unwrap(); + assert!(epoll2.ctl(ControlOperation::Add, ev, &event).is_err()); + + epoll2 + .ctl(ControlOperation::Delete, ev, &EpollEvent::default()) + .unwrap(); + assert!(epoll + .ctl(ControlOperation::Delete, ev, &EpollEvent::default()) + .is_err()); + + close(ev); + } + + #[test] + fn test_wait_returns_signaled_event() { + let epoll = Epoll::new().unwrap(); + let ev = create_event(); + let event = EpollEvent::new(EventSet::IN, ev as u64); + epoll.ctl(ControlOperation::Add, ev, &event).unwrap(); + + signal(ev); + + let mut ready = vec![EpollEvent::default(); 8]; + let n = epoll.wait(8, 1000, &mut ready).unwrap(); + assert_eq!(n, 1); + assert_eq!(ready[0].fd(), ev); + assert_eq!(ready[0].event_set(), EventSet::IN); + + epoll + .ctl(ControlOperation::Delete, ev, &EpollEvent::default()) + .unwrap(); + close(ev); + } + + #[test] + fn test_wait_timeout_no_signal() { + let epoll = Epoll::new().unwrap(); + let ev = create_event(); + let event = EpollEvent::new(EventSet::IN, ev as u64); + epoll.ctl(ControlOperation::Add, ev, &event).unwrap(); + + let mut ready = vec![EpollEvent::default(); 8]; + let n = epoll.wait(8, 50, &mut ready).unwrap(); + assert_eq!(n, 0); + + epoll + .ctl(ControlOperation::Delete, ev, &EpollEvent::default()) + .unwrap(); + close(ev); + } + + #[test] + fn test_wait_multiple_handles() { + let epoll = Epoll::new().unwrap(); + let ev1 = create_event(); + let ev2 = create_event(); + + epoll + .ctl( + ControlOperation::Add, + ev1, + &EpollEvent::new(EventSet::IN, ev1 as u64), + ) + .unwrap(); + epoll + .ctl( + ControlOperation::Add, + ev2, + &EpollEvent::new(EventSet::IN, ev2 as u64), + ) + .unwrap(); + + signal(ev1); + signal(ev2); + + let mut ready = vec![EpollEvent::default(); 8]; + let n = epoll.wait(8, 1000, &mut ready).unwrap(); + assert_eq!(n, 2); + + let handles: Vec = ready[..n].iter().map(|e| e.fd()).collect(); + assert!(handles.contains(&ev1)); + assert!(handles.contains(&ev2)); + + epoll + .ctl(ControlOperation::Delete, ev1, &EpollEvent::default()) + .unwrap(); + epoll + .ctl(ControlOperation::Delete, ev2, &EpollEvent::default()) + .unwrap(); + close(ev1); + close(ev2); + } + + #[test] + fn test_level_triggered_redelivers() { + let epoll = Epoll::new().unwrap(); + let ev = create_event(); + let event = EpollEvent::new(EventSet::IN, ev as u64); + epoll.ctl(ControlOperation::Add, ev, &event).unwrap(); + + signal(ev); + + let mut ready = vec![EpollEvent::default(); 8]; + let n = epoll.wait(8, 1000, &mut ready).unwrap(); + assert_eq!(n, 1); + + // Handle is still signaled (manual-reset event), so a second wait + // should deliver it again (level-triggered semantics). + let n = epoll.wait(8, 1000, &mut ready).unwrap(); + assert_eq!(n, 1); + assert_eq!(ready[0].fd(), ev); + + epoll + .ctl(ControlOperation::Delete, ev, &EpollEvent::default()) + .unwrap(); + close(ev); + } + + // -- Edge-triggered tests ----------------------------------------------- + + #[test] + fn test_edge_triggered_no_redelivery() { + let epoll = Epoll::new().unwrap(); + let ev = create_event(); + let event = EpollEvent::new(EventSet::IN | EventSet::EDGE_TRIGGERED, ev as u64); + epoll.ctl(ControlOperation::Add, ev, &event).unwrap(); + + signal(ev); + + let mut ready = vec![EpollEvent::default(); 8]; + let n = epoll.wait(8, 1000, &mut ready).unwrap(); + assert_eq!(n, 1); + assert_eq!(ready[0].fd(), ev); + + // Even though the handle is still signaled, edge-triggered mode + // should NOT re-deliver because the WCP was not re-armed. + let n = epoll.wait(8, 100, &mut ready).unwrap(); + assert_eq!(n, 0); + + epoll + .ctl(ControlOperation::Delete, ev, &EpollEvent::default()) + .unwrap(); + close(ev); + } + + #[test] + fn test_edge_triggered_rearm_via_modify() { + let epoll = Epoll::new().unwrap(); + let ev = create_event(); + let event = EpollEvent::new(EventSet::IN | EventSet::EDGE_TRIGGERED, ev as u64); + epoll.ctl(ControlOperation::Add, ev, &event).unwrap(); + + signal(ev); + + let mut ready = vec![EpollEvent::default(); 8]; + let n = epoll.wait(8, 1000, &mut ready).unwrap(); + assert_eq!(n, 1); + + // Simulate the caller draining data, then re-registering interest. + reset(ev); + epoll.ctl(ControlOperation::Modify, ev, &event).unwrap(); + + // No signal yet — should time out. + let n = epoll.wait(8, 100, &mut ready).unwrap(); + assert_eq!(n, 0); + + // Signal again — now we should get the event. + signal(ev); + let n = epoll.wait(8, 1000, &mut ready).unwrap(); + assert_eq!(n, 1); + assert_eq!(ready[0].fd(), ev); + + epoll + .ctl(ControlOperation::Delete, ev, &EpollEvent::default()) + .unwrap(); + close(ev); + } +} diff --git a/src/utils/src/windows/mod.rs b/src/utils/src/windows/mod.rs new file mode 100644 index 000000000..fe233c06d --- /dev/null +++ b/src/utils/src/windows/mod.rs @@ -0,0 +1,13 @@ +use windows_sys::Win32::Foundation::HANDLE; + +pub(crate) mod bindings; +pub mod epoll; + +/// Cross-platform alias used by the rest of the codebase. On Windows this +/// is just [`HANDLE`] — the two names are interchangeable. +pub type RawFd = HANDLE; + +/// Windows equivalent of [`std::os::unix::io::AsRawFd`]. +pub trait AsRawFd { + fn as_raw_fd(&self) -> RawFd; +} From 9486f652f43e4e903e41f7876b9f1001c9a95cb0 Mon Sep 17 00:00:00 2001 From: lstocchi Date: Tue, 24 Mar 2026 16:47:04 +0100 Subject: [PATCH 2/2] utils: add Windows eventfd implementation backed by manual-reset Event Emulate eventfd on Windows using a manual-reset kernel Event object paired with a Mutex-protected counter. To maximize VMM throughput, the `write` path only trigger the event when the counter transitions from `0 -> non-zero`. If a virtual device rapid-fires multiple interrupts before the vCPU wakes up, we accumulate the data in user-space RAM and skip the redundant kernel syscalls entirely. `read` and `wait_timeout` maintain strict level-triggered synchronization. The kernel event is only reset (`ResetEvent`) when the internal counter is fully drained, preventing the IOCP epoll loop from entering an infinite busy-wait cycle. Signed-off-by: lstocchi --- src/utils/src/lib.rs | 2 + src/utils/src/windows/eventfd.rs | 270 +++++++++++++++++++++++++++++++ src/utils/src/windows/mod.rs | 1 + 3 files changed, 273 insertions(+) create mode 100644 src/utils/src/windows/eventfd.rs diff --git a/src/utils/src/lib.rs b/src/utils/src/lib.rs index 75ced007e..c921eba0e 100644 --- a/src/utils/src/lib.rs +++ b/src/utils/src/lib.rs @@ -20,6 +20,8 @@ pub use macos::eventfd; pub mod windows; #[cfg(target_os = "windows")] pub use windows::epoll; +#[cfg(target_os = "windows")] +pub use windows::eventfd; pub mod pollable_channel; #[cfg(target_arch = "x86_64")] pub mod rand; diff --git a/src/utils/src/windows/eventfd.rs b/src/utils/src/windows/eventfd.rs new file mode 100644 index 000000000..8d69d385f --- /dev/null +++ b/src/utils/src/windows/eventfd.rs @@ -0,0 +1,270 @@ +//! Structure and wrapper functions emulating eventfd using a Windows manual-reset Event object. + +use std::sync::{Arc, Mutex}; +use std::{io, result}; + +use windows_sys::Win32::Foundation::{CloseHandle, HANDLE, WAIT_OBJECT_0}; +use windows_sys::Win32::System::Threading::{ + CreateEventW, ResetEvent, SetEvent, WaitForSingleObject, INFINITE, +}; + +use super::{AsRawFd, RawFd}; + +pub const EFD_NONBLOCK: i32 = 1; +pub const EFD_SEMAPHORE: i32 = 2; + +#[derive(Debug)] +struct Inner { + event: HANDLE, + counter: Mutex, + nonblock: bool, + semaphore: bool, +} + +// The HANDLE is a Windows kernel object usable from any thread. +unsafe impl Send for Inner {} +unsafe impl Sync for Inner {} + +impl Drop for Inner { + fn drop(&mut self) { + unsafe { + CloseHandle(self.event); + } + } +} + +#[derive(Clone, Debug)] +pub struct EventFd { + inner: Arc, +} + +impl EventFd { + pub fn new(flag: i32) -> result::Result { + let event = unsafe { + CreateEventW( + std::ptr::null(), + 1, // bManualReset = TRUE + 0, // bInitialState = FALSE (non-signaled) + std::ptr::null(), + ) + }; + if event.is_null() { + return Err(io::Error::last_os_error()); + } + + Ok(EventFd { + inner: Arc::new(Inner { + event, + counter: Mutex::new(0), + nonblock: (flag & EFD_NONBLOCK) != 0, + semaphore: (flag & EFD_SEMAPHORE) != 0, + }), + }) + } + + pub fn write(&self, v: u64) -> result::Result<(), io::Error> { + let mut counter = self.inner.counter.lock().unwrap(); + + let was_zero = *counter == 0; + *counter = counter.saturating_add(v); + + // Only signal the event if it was not already signaled. + if was_zero { + if unsafe { SetEvent(self.inner.event) } == 0 { + return Err(io::Error::last_os_error()); + } + } + Ok(()) + } + + pub fn read(&self) -> result::Result { + loop { + { + let mut counter = self.inner.counter.lock().unwrap(); + if *counter > 0 { + let result = if self.inner.semaphore { + // Semaphore mode: Decrement by 1 + *counter -= 1; + 1 + } else { + // Standard mode: Drain the whole counter + let val = *counter; + *counter = 0; + val + }; + + if *counter == 0 { + unsafe { + ResetEvent(self.inner.event); + } + } + return Ok(result); + } + if self.inner.nonblock { + return Err(io::ErrorKind::WouldBlock.into()); + } + } // Lock is dropped here before blocking so writers can make progress! + + let ret = unsafe { WaitForSingleObject(self.inner.event, INFINITE) }; + if ret != WAIT_OBJECT_0 { + return Err(io::Error::last_os_error()); + } + } + } + + pub fn try_clone(&self) -> result::Result { + Ok(EventFd { + inner: Arc::clone(&self.inner), + }) + } + + /// Waits up to `ms` milliseconds for the event to be signaled. + /// + /// Returns `true` if the event was signaled, `false` on timeout. + /// On signal, consumes one unit (semaphore mode) or drains the counter + /// (standard mode). The kernel event is only reset when the counter + /// reaches zero. + pub fn wait_timeout(&self, ms: u32) -> bool { + let result = unsafe { WaitForSingleObject(self.inner.event, ms) }; + if result == WAIT_OBJECT_0 { + let mut counter = self.inner.counter.lock().unwrap(); + if *counter > 0 { + if self.inner.semaphore { + *counter -= 1; + } else { + *counter = 0; + } + if *counter == 0 { + unsafe { + ResetEvent(self.inner.event); + } + } + } + true + } else { + false + } + } +} + +impl AsRawFd for EventFd { + fn as_raw_fd(&self) -> RawFd { + self.inner.event + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_read_write() { + let evt = EventFd::new(EFD_NONBLOCK).unwrap(); + evt.write(55).unwrap(); + assert_eq!(evt.read().unwrap(), 55); + } + + #[test] + fn test_read_nothing_nonblock() { + let evt = EventFd::new(EFD_NONBLOCK).unwrap(); + let res = evt.read(); + assert!(matches!(res, Err(err) if err.kind() == io::ErrorKind::WouldBlock)); + } + + #[test] + fn test_multiple_writes_accumulate() { + let evt = EventFd::new(EFD_NONBLOCK).unwrap(); + evt.write(3).unwrap(); + evt.write(5).unwrap(); + assert_eq!(evt.read().unwrap(), 8); + } + + /// After read() drains the counter to 0, the kernel event must be + /// unsignaled. If ResetEvent is missing, wait_timeout(0) would + /// return true forever — the "infinite wakeup" bug. + #[test] + fn test_event_reset_after_read() { + let evt = EventFd::new(EFD_NONBLOCK).unwrap(); + evt.write(1).unwrap(); + assert_eq!(evt.read().unwrap(), 1); + assert!( + !evt.wait_timeout(0), + "kernel event should be unsignaled after drain" + ); + } + + /// Verify that writing after a full drain re-signals the event. + #[test] + fn test_write_read_cycle() { + let evt = EventFd::new(EFD_NONBLOCK).unwrap(); + + evt.write(10).unwrap(); + assert_eq!(evt.read().unwrap(), 10); + assert!(!evt.wait_timeout(0)); + + evt.write(20).unwrap(); + assert_eq!(evt.read().unwrap(), 20); + assert!(!evt.wait_timeout(0)); + } + + #[test] + fn test_semaphore_mode() { + let evt = EventFd::new(EFD_NONBLOCK | EFD_SEMAPHORE).unwrap(); + evt.write(3).unwrap(); + + assert_eq!(evt.read().unwrap(), 1); + assert_eq!(evt.read().unwrap(), 1); + assert_eq!(evt.read().unwrap(), 1); + + let res = evt.read(); + assert!(matches!(res, Err(err) if err.kind() == io::ErrorKind::WouldBlock)); + } + + /// In semaphore mode, the kernel event must stay signaled as long as + /// the counter is > 0, and only unsignal on the final decrement. + #[test] + fn test_semaphore_event_stays_signaled() { + let evt = EventFd::new(EFD_NONBLOCK | EFD_SEMAPHORE).unwrap(); + evt.write(3).unwrap(); + + assert_eq!(evt.read().unwrap(), 1); // counter: 3 -> 2 + assert!( + evt.wait_timeout(0), + "event should still be signaled with counter=2" + ); + + // wait_timeout consumed one (counter: 2 -> 1) + assert!( + evt.wait_timeout(0), + "event should still be signaled with counter=1" + ); + + // wait_timeout consumed one (counter: 1 -> 0, ResetEvent) + assert!( + !evt.wait_timeout(0), + "event should be unsignaled after full drain" + ); + } + + #[test] + fn test_wait_timeout_not_signaled() { + let evt = EventFd::new(EFD_NONBLOCK).unwrap(); + assert!(!evt.wait_timeout(0)); + } + + #[test] + fn test_wait_timeout_signaled() { + let evt = EventFd::new(EFD_NONBLOCK).unwrap(); + evt.write(42).unwrap(); + assert!(evt.wait_timeout(0)); + } + + #[test] + fn test_clone() { + let evt = EventFd::new(EFD_NONBLOCK).unwrap(); + let evt_clone = evt.try_clone().unwrap(); + + evt.write(923).unwrap(); + assert_eq!(evt_clone.read().unwrap(), 923); + } +} diff --git a/src/utils/src/windows/mod.rs b/src/utils/src/windows/mod.rs index fe233c06d..90a8544b4 100644 --- a/src/utils/src/windows/mod.rs +++ b/src/utils/src/windows/mod.rs @@ -2,6 +2,7 @@ use windows_sys::Win32::Foundation::HANDLE; pub(crate) mod bindings; pub mod epoll; +pub mod eventfd; /// Cross-platform alias used by the rest of the codebase. On Windows this /// is just [`HANDLE`] — the two names are interchangeable.