├── .envrc ├── .github ├── dependabot.yml └── workflows │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.zig ├── build.zig.zon ├── docs ├── xev-c.7.scd ├── xev-faq.7.scd ├── xev-zig.7.scd ├── xev.7.scd ├── xev_completion_state.3.scd ├── xev_completion_zero.3.scd └── xev_threadpool.3.scd ├── examples ├── _basic.c ├── _basic.zig ├── async.c ├── million-timers.c └── threadpool.c ├── flake.lock ├── flake.nix ├── include └── xev.h ├── nix └── package.nix ├── shell.nix ├── src ├── ThreadPool.zig ├── backend │ ├── epoll.zig │ ├── io_uring.zig │ ├── iocp.zig │ ├── kqueue.zig │ └── wasi_poll.zig ├── bench │ ├── async1.zig │ ├── async2.zig │ ├── async4.zig │ ├── async8.zig │ ├── async_pummel_1.zig │ ├── async_pummel_2.zig │ ├── async_pummel_4.zig │ ├── async_pummel_8.zig │ ├── million-timers.zig │ ├── ping-pongs.zig │ ├── ping-udp1.zig │ └── udp_pummel_1v1.zig ├── c_api.zig ├── darwin.zig ├── debug.zig ├── dynamic.zig ├── heap.zig ├── linux │ └── timerfd.zig ├── loop.zig ├── main.zig ├── queue.zig ├── queue_mpsc.zig ├── watcher │ ├── async.zig │ ├── common.zig │ ├── file.zig │ ├── process.zig │ ├── stream.zig │ ├── tcp.zig │ ├── timer.zig │ └── udp.zig └── windows.zig └── website ├── .eslintrc.json ├── .gitignore ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.tsx ├── _document.tsx ├── _meta.json ├── api-docs │ ├── _meta.json │ └── concepts.mdx ├── api │ └── hello.ts ├── getting-started.mdx ├── getting-started │ ├── _meta.json │ └── concepts.mdx └── index.mdx ├── public ├── favicon.ico ├── next.svg ├── thirteen.svg └── vercel.svg ├── styles ├── Home.module.css └── globals.css ├── theme.config.jsx └── tsconfig.json /.envrc: -------------------------------------------------------------------------------- 1 | # If we are a computer with nix-shell available, then use that to setup 2 | # the build environment with exactly what we need. 3 | if has nix; then 4 | use nix 5 | fi 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for GitHub Actions 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | # Check for updates to GitHub Actions every weekday 8 | interval: "daily" 9 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Test 3 | jobs: 4 | build: 5 | strategy: 6 | matrix: 7 | os: [namespace-profile-mitchellh-sm] 8 | 9 | target: [ 10 | aarch64-linux-gnu, 11 | aarch64-linux-musl, 12 | x86_64-linux-gnu, 13 | x86_64-linux-musl, 14 | aarch64-macos, 15 | x86_64-macos, 16 | # wasm32-wasi, - regressed in Zig 0.13 17 | x86_64-windows-gnu 18 | 19 | # Broken but not in any obvious way: 20 | # x86-linux-gnu, 21 | # x86-linux-musl, 22 | # x86-windows, 23 | ] 24 | runs-on: ${{ matrix.os }} 25 | needs: [test-x86_64-linux, test-x86_64-windows] 26 | env: 27 | ZIG_LOCAL_CACHE_DIR: /zig/local-cache 28 | ZIG_GLOBAL_CACHE_DIR: /zig/global-cache 29 | steps: 30 | - name: Checkout code 31 | uses: actions/checkout@v4 32 | 33 | - name: Setup Cache 34 | uses: namespacelabs/nscloud-cache-action@v1.2.7 35 | with: 36 | path: | 37 | /nix 38 | /zig 39 | 40 | # Install Nix and use that to run our tests so our environment matches exactly. 41 | - uses: cachix/install-nix-action@v31 42 | with: 43 | nix_path: nixpkgs=channel:nixos-unstable 44 | 45 | # Run our checks to catch quick issues 46 | - run: nix flake check 47 | 48 | # Run our go tests within the context of the dev shell from the flake. This 49 | # will ensure we have all our dependencies. 50 | - name: test 51 | run: nix develop -c zig build --summary all -Dtarget=${{ matrix.target }} 52 | 53 | test-x86_64-linux: 54 | strategy: 55 | matrix: 56 | os: [namespace-profile-mitchellh-sm] 57 | runs-on: ${{ matrix.os }} 58 | env: 59 | ZIG_LOCAL_CACHE_DIR: /zig/local-cache 60 | ZIG_GLOBAL_CACHE_DIR: /zig/global-cache 61 | steps: 62 | - name: Checkout code 63 | uses: actions/checkout@v4 64 | 65 | - name: Setup Cache 66 | uses: namespacelabs/nscloud-cache-action@v1.2.7 67 | with: 68 | path: | 69 | /nix 70 | /zig 71 | 72 | - uses: cachix/install-nix-action@v31 73 | with: 74 | nix_path: nixpkgs=channel:nixos-unstable 75 | 76 | # Run our checks to catch quick issues 77 | - run: nix flake check 78 | 79 | # Run our go tests within the context of the dev shell from the flake. This 80 | # will ensure we have all our dependencies. 81 | - name: test 82 | run: nix develop -c zig build test --summary all 83 | # WASI has regressed since Zig 0.13, we should fix it. 84 | # - name: test wasi 85 | # run: nix develop -c zig build test -Dtarget=wasm32-wasi -fwasmtime --summary all 86 | 87 | - name: build all benchmarks and examples 88 | run: nix develop -c zig build -Demit-example -Demit-bench --summary all 89 | 90 | # Run a full build to ensure that works 91 | - run: nix build 92 | 93 | test-x86_64-windows: 94 | strategy: 95 | matrix: 96 | os: [windows-latest] 97 | runs-on: ${{ matrix.os }} 98 | steps: 99 | - name: Checkout code 100 | uses: actions/checkout@v4 101 | 102 | - name: Install zig 103 | uses: goto-bus-stop/setup-zig@v2 104 | with: 105 | version: 0.14.0 106 | 107 | - name: test 108 | run: zig build test --summary all 109 | 110 | - name: build all benchmarks and examples 111 | run: zig build -Demit-example -Demit-bench --summary all 112 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .zig-cache 2 | zig-out 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Mitchell Hashimoto 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # libxev 2 | 3 | libxev is a cross-platform event loop. libxev provides a unified event loop 4 | abstraction for non-blocking IO, timers, signals, events, and more that 5 | works on macOS, Windows, Linux, and WebAssembly (browser and WASI). It is 6 | written in [Zig](https://ziglang.org/) but exports a C-compatible API (which 7 | further makes it compatible with any language out there that can communicate 8 | with C APIs). 9 | 10 | **Project Status: Stable for most use cases.** libxev is in daily use by 11 | large projects such as [Ghostty](https://ghostty.org), 12 | [zml](https://github.com/zml/zml), and more. For most use cases, libxev 13 | has been shown to be stable at scale. libxev has a broad featureset and 14 | there are likely less well-used corners of the library, but for most 15 | use cases libxev is already heavily used in production environments. 16 | 17 | **Why a new event loop library?** A few reasons. One, I think Zig lacks 18 | a generalized event loop comparable to libuv in features ("generalized" 19 | being a key word here). Two, I wanted to build a library like this around 20 | the design patterns of [io_uring](https://unixism.net/loti/what_is_io_uring.html), 21 | even mimicking its style on top of other OS primitives ( 22 | [credit to this awesome blog post](https://tigerbeetle.com/blog/a-friendly-abstraction-over-iouring-and-kqueue/)). 23 | Three, I wanted an event loop library that could build to WebAssembly 24 | (both WASI and freestanding) and that didn't really fit well 25 | into the goals of API style of existing libraries without bringing in 26 | something super heavy like Emscripten. The motivation for this library 27 | primarily though is scratching my own itch! 28 | 29 | ## Features 30 | 31 | **Cross-platform.** Linux (`io_uring` and `epoll`), macOS (`kqueue`), 32 | WebAssembly + WASI (`poll_oneoff`, threaded and non-threaded runtimes). 33 | (Windows support is planned and coming soon) 34 | 35 | **[Proactor API](https://en.wikipedia.org/wiki/Proactor_pattern).** Work 36 | is submitted to the libxev event loop and the caller is notified of 37 | work _completion_, as opposed to work _readiness_. 38 | 39 | **Zero runtime allocations.** This helps make runtime performance more 40 | predictable and makes libxev well suited for embedded environments. 41 | 42 | **Timers, TCP, UDP, Files, Processes.** High-level platform-agnostic APIs for 43 | interacting with timers, TCP/UDP sockets, files, processes, and more. For 44 | platforms that don't support async IO, the file operations are automatically 45 | scheduled to a thread pool. 46 | 47 | **Generic Thread Pool (Optional).** You can create a generic thread pool, 48 | configure its resource utilization, and use this to perform custom background 49 | tasks. The thread pool is used by some backends to do non-blocking tasks that 50 | don't have reliable non-blocking APIs (such as local file operations with 51 | `kqueue`). The thread pool can be shared across multiple threads and event 52 | loops to optimize resource utilization. 53 | 54 | **Low-level and High-Level API.** The high-level API is platform-agnostic 55 | but has some opinionated behavior and limited flexibility. The high-level 56 | API is recommended but the low-level API is always an available escape hatch. 57 | The low-level API is platform-specific and provides a mechanism for libxev 58 | users to squeeze out maximum performance. The low-level API is _just enough 59 | abstraction_ above the OS interface to make it easier to use without 60 | sacrificing noticable performance. 61 | 62 | **Tree Shaking (Zig).** This is a feature of Zig, but substantially benefits 63 | libraries such as libxev. Zig will only include function calls and features 64 | that you actually use. If you don't use a particular kind of high-level 65 | watcher (such as UDP sockets), then the functionality related to that 66 | abstraction is not compiled into your final binary at all. This lets libxev 67 | support optional "nice-to-have" functionality that may be considered 68 | "bloat" in some cases, but the end user doesn't have to pay for it. 69 | 70 | **Dependency-free.** libxev has no dependencies other than the built-in 71 | OS APIs at runtime. The C library depends on libc. This makes it very 72 | easy to cross-compile. 73 | 74 | ### Roadmap 75 | 76 | There are plenty of missing features that I still want to add: 77 | 78 | * Pipe high-level API 79 | * Signal handlers 80 | * Filesystem events 81 | * Windows backend 82 | * Freestanding WebAssembly support via an external event loop (i.e. the browser) 83 | 84 | And more... 85 | 86 | ### Performance 87 | 88 | There is plenty of room for performance improvements, and I want to be 89 | fully clear that I haven't done a lot of optimization work. Still, 90 | performance is looking good. I've tried to port many of 91 | [libuv benchmarks](https://github.com/libuv/libuv) to use the libxev 92 | API. 93 | 94 | I won't post specific benchmark results until I have a better 95 | environment to run them in. As a _very broad generalization_, 96 | you shouldn't notice a slowdown using libxev compared to other 97 | major event loops. This may differ on a feature-by-feature basis, and 98 | if you can show really poor performance in an issue I'm interested 99 | in resolving it! 100 | 101 | ## Example 102 | 103 | The example below shows an identical program written in Zig and in C 104 | that uses libxev to run a single 5s timer. This is almost silly how 105 | simple it is but is meant to just convey the overall feel of the library 106 | rather than a practical use case. 107 | 108 | 109 | 110 | 111 | 112 | 113 | 147 | 182 | 183 |
Zig C
114 | 115 | ```zig 116 | const xev = @import("xev"); 117 | 118 | pub fn main() !void { 119 | var loop = try xev.Loop.init(.{}); 120 | defer loop.deinit(); 121 | 122 | const w = try xev.Timer.init(); 123 | defer w.deinit(); 124 | 125 | // 5s timer 126 | var c: xev.Completion = undefined; 127 | w.run(&loop, &c, 5000, void, null, &timerCallback); 128 | 129 | try loop.run(.until_done); 130 | } 131 | 132 | fn timerCallback( 133 | userdata: ?*void, 134 | loop: *xev.Loop, 135 | c: *xev.Completion, 136 | result: xev.Timer.RunError!void, 137 | ) xev.CallbackAction { 138 | _ = userdata; 139 | _ = loop; 140 | _ = c; 141 | _ = result catch unreachable; 142 | return .disarm; 143 | } 144 | ``` 145 | 146 | 148 | 149 | ```zig 150 | #include 151 | #include 152 | #include 153 | 154 | xev_cb_action timerCallback(xev_loop* loop, xev_completion* c, int result, void *userdata) { 155 | return XEV_DISARM; 156 | } 157 | 158 | int main(void) { 159 | xev_loop loop; 160 | if (xev_loop_init(&loop) != 0) { 161 | printf("xev_loop_init failure\n"); 162 | return 1; 163 | } 164 | 165 | xev_watcher w; 166 | if (xev_timer_init(&w) != 0) { 167 | printf("xev_timer_init failure\n"); 168 | return 1; 169 | } 170 | 171 | xev_completion c; 172 | xev_timer_run(&w, &loop, &c, 5000, NULL, &timerCallback); 173 | 174 | xev_loop_run(&loop, XEV_RUN_UNTIL_DONE); 175 | 176 | xev_timer_deinit(&w); 177 | xev_loop_deinit(&loop); 178 | return 0; 179 | } 180 | ``` 181 |
184 | 185 | ## Installation (Zig) 186 | 187 | **These instructions are for Zig downstream users only.** If you are 188 | using the C API to libxev, see the "Build" section. 189 | 190 | This package works with the Zig package manager introduced in Zig 0.11. 191 | Create a `build.zig.zon` file like this: 192 | 193 | ```zig 194 | .{ 195 | .name = "my-project", 196 | .version = "0.0.0", 197 | .dependencies = .{ 198 | .libxev = .{ 199 | .url = "https://github.com/mitchellh/libxev/archive/.tar.gz", 200 | .hash = "12208070233b17de6be05e32af096a6760682b48598323234824def41789e993432c", 201 | }, 202 | }, 203 | } 204 | ``` 205 | 206 | And in your `build.zig`: 207 | 208 | ```zig 209 | const xev = b.dependency("libxev", .{ .target = target, .optimize = optimize }); 210 | exe.addModule("xev", xev.module("xev")); 211 | ``` 212 | 213 | ## Documentation 214 | 215 | 🚧 Documentation is a work-in-progress. 🚧 216 | 217 | Currently, documentation is available in three forms: **man pages**, 218 | **examples**, and **code comments.** In the future, I plan on writing detailed 219 | guides and API documentation in website form, but that isn't currently 220 | available. 221 | 222 | ### Man Pages 223 | 224 | The man pages are relatively detailed! `xev(7)` will 225 | give you a good overview of the entire library. `xev-zig(7)` and 226 | `xev-c(7)` will provide overviews of the Zig and C API, respectively. 227 | From there, API-specifc man pages such as `xev_loop_init(3)` are 228 | available. This is the best documentation currently. 229 | 230 | There are multiple ways to browse the man pages. The most immediately friendly 231 | is to just browse the raw man page sources in the `docs/` directory in 232 | your web browser. The man page source is a _markdown-like_ syntax so it 233 | renders _okay_ in your browser via GitHub. 234 | 235 | Another approach is to run `zig build -Dman-pages` and the man pages 236 | will be available in `zig-out`. This requires 237 | [scdoc](https://git.sr.ht/~sircmpwn/scdoc) 238 | to be installed (this is available in most package managers). 239 | Once you've built the man pages, you can render them by path: 240 | 241 | ``` 242 | $ man zig-out/share/man/man7/xev.7 243 | ``` 244 | 245 | And the final approach is to install libxev via your favorite package 246 | manager (if and when available), which should hopefully put your man pages 247 | into your man path, so you can just do `man 7 xev`. 248 | 249 | ### Examples 250 | 251 | There are examples available in the `examples/` folder. The examples are 252 | available in both C and Zig, and you can tell which one is which using 253 | the file extension. 254 | 255 | To build an example, use the following: 256 | 257 | ``` 258 | $ zig build -Dexample-name=_basic.zig 259 | ... 260 | $ zig-out/bin/example-basic 261 | ... 262 | ``` 263 | 264 | The `-Dexample-name` value should be the filename including the extension. 265 | 266 | ### Code Comments 267 | 268 | The Zig code is well commented. If you're comfortable reading code comments 269 | you can find a lot of insight within them. The source is in the `src/` 270 | directory. 271 | 272 | # Build 273 | 274 | Build requires the installation of the Zig 0.14.0. libxev follows stable 275 | Zig releases and generally does not support nightly builds. When a stable 276 | release is imminent we may have a branch that supports it. 277 | **libxev has no other build dependencies.** 278 | 279 | Once installed, `zig build install` on its own will build the full library and output 280 | a [FHS-compatible](https://en.wikipedia.org/wiki/Filesystem_Hierarchy_Standard) 281 | directory in `zig-out`. You can customize the output directory with the 282 | `--prefix` flag. 283 | 284 | ## Tests 285 | 286 | libxev has a large and growing test suite. To run the tests for the current 287 | platform: 288 | 289 | ```sh 290 | $ zig build test 291 | ... 292 | ``` 293 | 294 | This will run all the tests for all the supported features for the current 295 | host platform. For example, on Linux this will run both the full io_uring 296 | and epoll test suite. 297 | 298 | **You can build and run tests for other platforms** by cross-compiling the 299 | test executable, copying it to a target machine and executing it. For example, 300 | the below shows how to cross-compile and build the tests for macOS from Linux: 301 | 302 | ```sh 303 | $ zig build -Dtarget=aarch64-macos -Dinstall-tests 304 | ... 305 | 306 | $ file zig-out/bin/xev-test 307 | zig-out/bin/xev-test: Mach-O 64-bit arm64 executable 308 | ``` 309 | 310 | **WASI is a special-case.** You can run tests for WASI if you have 311 | [wasmtime](https://wasmtime.dev/) installed: 312 | 313 | ``` 314 | $ zig build test -Dtarget=wasm32-wasi -Dwasmtime 315 | ... 316 | ``` 317 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Step = std.Build.Step; 3 | 4 | /// A note on my build.zig style: I try to create all the artifacts first, 5 | /// unattached to any steps. At the end of the build() function, I create 6 | /// steps or attach unattached artifacts to predefined steps such as 7 | /// install. This means the only thing affecting the `zig build` user 8 | /// interaction is at the end of the build() file and makes it easier 9 | /// to reason about the structure. 10 | pub fn build(b: *std.Build) !void { 11 | const target = b.standardTargetOptions(.{}); 12 | const optimize = b.standardOptimizeOption(.{}); 13 | 14 | _ = b.addModule("xev", .{ .root_source_file = b.path("src/main.zig") }); 15 | 16 | const emit_man = b.option( 17 | bool, 18 | "emit-man-pages", 19 | "Set to true to build man pages. Requires scdoc. Defaults to true if scdoc is found.", 20 | ) orelse if (b.findProgram( 21 | &[_][]const u8{"scdoc"}, 22 | &[_][]const u8{}, 23 | )) |_| 24 | true 25 | else |err| switch (err) { 26 | error.FileNotFound => false, 27 | else => return err, 28 | }; 29 | 30 | const emit_bench = b.option( 31 | bool, 32 | "emit-bench", 33 | "Install the benchmark binaries to zig-out", 34 | ) orelse false; 35 | 36 | const emit_examples = b.option( 37 | bool, 38 | "emit-example", 39 | "Install the example binaries to zig-out", 40 | ) orelse false; 41 | 42 | // Static C lib 43 | const static_lib: ?*Step.Compile = lib: { 44 | if (target.result.os.tag == .wasi) break :lib null; 45 | 46 | const static_lib = b.addStaticLibrary(.{ 47 | .name = "xev", 48 | .root_source_file = b.path("src/c_api.zig"), 49 | .target = target, 50 | .optimize = optimize, 51 | }); 52 | static_lib.linkLibC(); 53 | if (target.result.os.tag == .windows) { 54 | static_lib.linkSystemLibrary("ws2_32"); 55 | static_lib.linkSystemLibrary("mswsock"); 56 | } 57 | break :lib static_lib; 58 | }; 59 | 60 | // Dynamic C lib 61 | const dynamic_lib: ?*Step.Compile = lib: { 62 | // We require native so we can link to libxml2 63 | if (!target.query.isNative()) break :lib null; 64 | 65 | const dynamic_lib = b.addSharedLibrary(.{ 66 | .name = "xev", 67 | .root_source_file = b.path("src/c_api.zig"), 68 | .target = target, 69 | .optimize = optimize, 70 | }); 71 | break :lib dynamic_lib; 72 | }; 73 | 74 | // C Headers 75 | const c_header = b.addInstallFileWithDir( 76 | b.path("include/xev.h"), 77 | .header, 78 | "xev.h", 79 | ); 80 | 81 | // pkg-config 82 | const pc: *Step.InstallFile = pc: { 83 | const file = b.addWriteFile("libxev.pc", b.fmt( 84 | \\prefix={s} 85 | \\includedir=${{prefix}}/include 86 | \\libdir=${{prefix}}/lib 87 | \\ 88 | \\Name: libxev 89 | \\URL: https://github.com/mitchellh/libxev 90 | \\Description: High-performance, cross-platform event loop 91 | \\Version: 0.1.0 92 | \\Cflags: -I${{includedir}} 93 | \\Libs: -L${{libdir}} -lxev 94 | , .{b.install_prefix})); 95 | break :pc b.addInstallFileWithDir( 96 | file.getDirectory().path(b, "libxev.pc"), 97 | .prefix, 98 | "share/pkgconfig/libxev.pc", 99 | ); 100 | }; 101 | 102 | // Man pages 103 | const man = try manPages(b); 104 | 105 | // Benchmarks and examples 106 | const benchmarks = try buildBenchmarks(b, target); 107 | const examples = try buildExamples(b, target, optimize, static_lib); 108 | 109 | // Test Executable 110 | const test_exe: *Step.Compile = test_exe: { 111 | const test_filter = b.option( 112 | []const u8, 113 | "test-filter", 114 | "Filter for test", 115 | ); 116 | const test_exe = b.addTest(.{ 117 | .name = "xev-test", 118 | .root_source_file = b.path("src/main.zig"), 119 | .target = target, 120 | .optimize = optimize, 121 | .filter = test_filter, 122 | }); 123 | switch (target.result.os.tag) { 124 | .linux, .macos => test_exe.linkLibC(), 125 | else => {}, 126 | } 127 | break :test_exe test_exe; 128 | }; 129 | 130 | // "test" Step 131 | { 132 | const tests_run = b.addRunArtifact(test_exe); 133 | const test_step = b.step("test", "Run tests"); 134 | test_step.dependOn(&tests_run.step); 135 | } 136 | 137 | if (static_lib) |v| b.installArtifact(v); 138 | if (dynamic_lib) |v| b.installArtifact(v); 139 | b.getInstallStep().dependOn(&c_header.step); 140 | b.getInstallStep().dependOn(&pc.step); 141 | b.installArtifact(test_exe); 142 | if (emit_man) { 143 | for (man) |step| b.getInstallStep().dependOn(step); 144 | } 145 | if (emit_bench) for (benchmarks) |exe| { 146 | b.getInstallStep().dependOn(&b.addInstallArtifact( 147 | exe, 148 | .{ .dest_dir = .{ .override = .{ 149 | .custom = "bin/bench", 150 | } } }, 151 | ).step); 152 | }; 153 | if (emit_examples) for (examples) |exe| { 154 | b.getInstallStep().dependOn(&b.addInstallArtifact( 155 | exe, 156 | .{ .dest_dir = .{ .override = .{ 157 | .custom = "bin/example", 158 | } } }, 159 | ).step); 160 | }; 161 | } 162 | 163 | fn buildBenchmarks( 164 | b: *std.Build, 165 | target: std.Build.ResolvedTarget, 166 | ) ![]const *Step.Compile { 167 | var steps = std.ArrayList(*Step.Compile).init(b.allocator); 168 | defer steps.deinit(); 169 | 170 | var dir = try std.fs.cwd().openDir(try b.build_root.join( 171 | b.allocator, 172 | &.{ "src", "bench" }, 173 | ), .{ .iterate = true }); 174 | defer dir.close(); 175 | 176 | // Go through and add each as a step 177 | var it = dir.iterate(); 178 | while (try it.next()) |entry| { 179 | // Get the index of the last '.' so we can strip the extension. 180 | const index = std.mem.lastIndexOfScalar( 181 | u8, 182 | entry.name, 183 | '.', 184 | ) orelse continue; 185 | if (index == 0) continue; 186 | 187 | // Name of the app and full path to the entrypoint. 188 | const name = entry.name[0..index]; 189 | 190 | // Executable builder. 191 | const exe = b.addExecutable(.{ 192 | .name = name, 193 | .root_source_file = b.path(b.fmt( 194 | "src/bench/{s}", 195 | .{entry.name}, 196 | )), 197 | .target = target, 198 | .optimize = .ReleaseFast, // benchmarks are always release fast 199 | }); 200 | exe.root_module.addImport("xev", b.modules.get("xev").?); 201 | 202 | // Store the mapping 203 | try steps.append(exe); 204 | } 205 | 206 | return try steps.toOwnedSlice(); 207 | } 208 | 209 | fn buildExamples( 210 | b: *std.Build, 211 | target: std.Build.ResolvedTarget, 212 | optimize: std.builtin.OptimizeMode, 213 | c_lib_: ?*Step.Compile, 214 | ) ![]const *Step.Compile { 215 | var steps = std.ArrayList(*Step.Compile).init(b.allocator); 216 | defer steps.deinit(); 217 | 218 | var dir = try std.fs.cwd().openDir(try b.build_root.join( 219 | b.allocator, 220 | &.{"examples"}, 221 | ), .{ .iterate = true }); 222 | defer dir.close(); 223 | 224 | // Go through and add each as a step 225 | var it = dir.iterate(); 226 | while (try it.next()) |entry| { 227 | // Get the index of the last '.' so we can strip the extension. 228 | const index = std.mem.lastIndexOfScalar( 229 | u8, 230 | entry.name, 231 | '.', 232 | ) orelse continue; 233 | if (index == 0) continue; 234 | 235 | // Name of the app and full path to the entrypoint. 236 | const name = entry.name[0..index]; 237 | 238 | const is_zig = std.mem.eql(u8, entry.name[index + 1 ..], "zig"); 239 | const exe: *Step.Compile = if (is_zig) exe: { 240 | const exe = b.addExecutable(.{ 241 | .name = name, 242 | .root_source_file = b.path(b.fmt( 243 | "examples/{s}", 244 | .{entry.name}, 245 | )), 246 | .target = target, 247 | .optimize = optimize, 248 | }); 249 | exe.root_module.addImport("xev", b.modules.get("xev").?); 250 | break :exe exe; 251 | } else exe: { 252 | const c_lib = c_lib_ orelse return error.UnsupportedPlatform; 253 | const exe = b.addExecutable(.{ 254 | .name = name, 255 | .target = target, 256 | .optimize = optimize, 257 | }); 258 | exe.linkLibC(); 259 | exe.addIncludePath(b.path("include")); 260 | exe.addCSourceFile(.{ 261 | .file = b.path(b.fmt( 262 | "examples/{s}", 263 | .{entry.name}, 264 | )), 265 | .flags = &[_][]const u8{ 266 | "-Wall", 267 | "-Wextra", 268 | "-pedantic", 269 | "-std=c99", 270 | "-D_POSIX_C_SOURCE=199309L", 271 | }, 272 | }); 273 | exe.linkLibrary(c_lib); 274 | break :exe exe; 275 | }; 276 | 277 | // Store the mapping 278 | try steps.append(exe); 279 | } 280 | 281 | return try steps.toOwnedSlice(); 282 | } 283 | 284 | fn manPages(b: *std.Build) ![]const *Step { 285 | var steps = std.ArrayList(*Step).init(b.allocator); 286 | defer steps.deinit(); 287 | 288 | var dir = try std.fs.cwd().openDir(try b.build_root.join( 289 | b.allocator, 290 | &.{"docs"}, 291 | ), .{ .iterate = true }); 292 | defer dir.close(); 293 | 294 | var it = dir.iterate(); 295 | while (try it.next()) |*entry| { 296 | // Filenames must end in "{section}.scd" and sections are 297 | // single numerals. 298 | const base = entry.name[0 .. entry.name.len - 4]; 299 | const section = base[base.len - 1 ..]; 300 | 301 | const cmd = b.addSystemCommand(&.{"scdoc"}); 302 | cmd.setStdIn(.{ .lazy_path = b.path( 303 | b.fmt("docs/{s}", .{entry.name}), 304 | ) }); 305 | 306 | try steps.append(&b.addInstallFile( 307 | cmd.captureStdOut(), 308 | b.fmt("share/man/man{s}/{s}", .{ section, base }), 309 | ).step); 310 | } 311 | 312 | return try steps.toOwnedSlice(); 313 | } 314 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = .libxev, 3 | .minimum_zig_version = "0.14.0", 4 | .version = "0.0.0", 5 | .fingerprint = 0x30f7363573edabf3, 6 | .paths = .{""}, 7 | } 8 | -------------------------------------------------------------------------------- /docs/xev-c.7.scd: -------------------------------------------------------------------------------- 1 | xev-c(7) "github.com/mitchellh/libxev" "Miscellaneous Information Manual" 2 | 3 | # NAME 4 | 5 | libxev C API 6 | 7 | # DESCRIPTION 8 | 9 | See xev(7) for a general libxev overview. This man page will give a more 10 | specific overview of the C API for libxev. 11 | 12 | TODO -- This isn't written yet, sorry. 13 | -------------------------------------------------------------------------------- /docs/xev-faq.7.scd: -------------------------------------------------------------------------------- 1 | xev-faq(7) "github.com/mitchellh/libxev" "Miscellaneous Information Manual" 2 | 3 | # NAME 4 | 5 | libxev Frequently Asked Questions (FAQ) 6 | 7 | # DESCRIPTION 8 | 9 | This manual page contains frequently asked questions around the design 10 | and usage of libxev. The goal of this page is to collect various tips or 11 | varios common challenges people run into. The information in this page 12 | may duplicate documentation that is available for a specific API or 13 | concept. 14 | 15 | # FAQ 16 | 17 | ## HOW CAN I CHECK IF A COMPLETION, TIMER, SOCKET, ETC. IS ACTIVE? 18 | 19 | Some other event loops (libuv, libev) have a function that can check if 20 | a given _thing_ (timer, socket, etc.) is "active." "Active" means that the 21 | thing in question is part of an event loop and _running_ (the definition of 22 | running is dependent on the type of the watcher). 23 | 24 | For libxev, the correct question to ask is: is this _completion_ active? 25 | A completion represents a single operation and can be used to determine 26 | if a corresponding watcher is active if the complation was used with 27 | the watcher. 28 | 29 | Completion state can be checked using the `c.state()` function (Zig) or 30 | the xev_completion_state(3) function (C). This will return an enum value 31 | of "dead" or "alive". More enum values may be added in the future if 32 | it is determined that finer grained state is useful. 33 | 34 | # SUGGEST A TOPIC 35 | 36 | There is still a lot of improvement to be made to the documentation. Please 37 | suggest any topics to cover at . 38 | 39 | # SEE ALSO 40 | 41 | xev(7) 42 | 43 | 44 | 45 | # AUTHORS 46 | 47 | Mitchell Hashimoto (xmitchx@gmail.com) and any open source contributors. 48 | See . 49 | -------------------------------------------------------------------------------- /docs/xev-zig.7.scd: -------------------------------------------------------------------------------- 1 | xev-zig(7) "github.com/mitchellh/libxev" "Miscellaneous Information Manual" 2 | 3 | # NAME 4 | 5 | libxev Zig API 6 | 7 | # DESCRIPTION 8 | 9 | See xev(7) for a general libxev overview. This man page will give a more 10 | specific overview of the Zig API for libxev. 11 | 12 | libxev is written in Zig and exports a native Zig API. The Zig API 13 | takes advantage of first-class Zig concepts such as comptime parameters, 14 | error sets, etc. in order to provide an idiomatic Zig experience. Beyond 15 | basic idioms, these Zig features usually result in improved performance 16 | over the C API. For example, all callbacks in the Zig API must be available 17 | at comptime because the callback call is always inlined -- this results in 18 | a noticable performance improvement over equivalent C consumption of libxev. 19 | 20 | The primary Zig API is visible in `src/main.zig` in the libxev source. The main file uses a somewhat convoluted `usingnamespace` to setup the 21 | API so it isn't immediately obvious, but the API is provided by the `Xev` 22 | function. For example, available consts are: `xev.Loop`, `xev.Completion`, 23 | etc. 24 | 25 | # INSTALLATION 26 | 27 | libxev has no dependencies, making it easy to install into your Zig project 28 | using any available Zig package manager or Git submodules. The `build.zig` 29 | file exports a `module` constant that you can use with `addAnonymousModule`: 30 | 31 | ``` 32 | // build.zig 33 | 34 | const libxev = @import("submodules/libxev/build.zig"); 35 | 36 | pub fn build(b: *std.build.Builder) !void { 37 | // Your other build options... 38 | 39 | my_exe.addAnonymousModule("xev", libxev.module); 40 | } 41 | ``` 42 | 43 | The package is then available in your source code as "xev": 44 | 45 | ``` 46 | // Your main.zig 47 | 48 | const xev = @import("xev"); 49 | ``` 50 | 51 | # QUICK START 52 | 53 | ## INITIALIZING A LOOP 54 | 55 | After importing xev, the first thing you'll need is an event loop: 56 | 57 | ``` 58 | var loop = try xev.Loop.init(.{}); 59 | defer loop.deinit(); // or wherever your cleanup is 60 | ``` 61 | 62 | This initializes the resources associated with an event loop. An event loop 63 | _can be copied_ until the first `run` is called. Once `run` is called, 64 | the loop *must be at a stable memory location* (pointer won't change). 65 | 66 | ## ADDING A COMPLETION 67 | 68 | An empty event loop does nothing. You must add one or more _completions_ 69 | to request work to be done asynchronously. A completion is represented with 70 | the `xev.Completion` structure. A completion *must have a stable memory 71 | location when it is added to the loop* until the completion is finished. 72 | xev(7) talks more about completion lifetimes and memory allocation requirements. 73 | 74 | The example below adds a timer to the previously initialized event loop: 75 | 76 | ``` 77 | var c_timer: xev.Completion = undefined; 78 | const timer = try xev.Timer.init(); 79 | timer.run(&loop, &c_timer, 5_000, void, null, timerCallback); 80 | ``` 81 | 82 | This uses the `xev.Timer` _high-level abstraction_. This is an abstraction 83 | that provides a common API on top of multiple operating system async APIs. 84 | You can also use the low-level API to manage completions with the loop directly, 85 | but these are not portable. 86 | 87 | **Important concept to notice:** The completion allocation is up to the 88 | program author. It can be stack allocated (such as in this case) or heap 89 | allocated. The pointer must remain stable until it is complete. This gives 90 | program authors substantial power over optimizing the performance of libxev. 91 | In fact, libxev doesn't perform _any_ dynamic memory allocation. 92 | 93 | ## RUNNING THE LOOP 94 | 95 | An added completion is a request for future work. The work does not start 96 | until the completion is submitted. A completion is submitted only during 97 | an event loop tick. To tick the event loop, use the `run` method: 98 | 99 | ``` 100 | try loop.run(.until_done); 101 | ``` 102 | 103 | The enum value is an `xev.RunMode` described in xev(7). This call will run 104 | until the loop has no more active completions (`until_done`). At 105 | some point (in about 5 seconds), this will call the registered `timerCallback`. 106 | 107 | # MEMORY ALLOCATIONS 108 | 109 | You'll notice that none of the Zig APIs take an allocator as an argument. 110 | libxev does not perform any dynamic memory allocations. All memory must be 111 | provided by the caller. This section will outline specific memory management 112 | rules you should be aware of when using libxev. 113 | 114 | *xev.Loop.* This can be copied and moved until `submit`, `tick`, or `run` 115 | is called. Once any of those loop functions are called, the loop memory 116 | must remain at a stable address. It is not safe to copy, move, or reuse the 117 | memory that loop is occupying until `deinit` completes. 118 | 119 | *xev.Completion.* This can be copied and moved until the completion is 120 | added to a loop. A completion is often added to a loop with any function 121 | call on a high-level abstraction that takes a completion pointer as a function 122 | argument such as `xev.Timer.run`. The completion memory can be reclaimed 123 | only when the callback associated with it is fired or the loop is deinitialized. 124 | 125 | # SUGGEST A TOPIC 126 | 127 | There is still a lot of improvement to be made to the documentation. Please 128 | suggest any topics to cover at . 129 | 130 | # SEE ALSO 131 | 132 | xev(7) 133 | 134 | 135 | 136 | # AUTHORS 137 | 138 | Mitchell Hashimoto (xmitchx@gmail.com) and any open source contributors. 139 | See . 140 | -------------------------------------------------------------------------------- /docs/xev.7.scd: -------------------------------------------------------------------------------- 1 | xev(7) "github.com/mitchellh/libxev" "Miscellaneous Information Manual" 2 | 3 | # NAME 4 | 5 | libxev - high-performance, cross-platform event loop 6 | 7 | # DESCRIPTION 8 | 9 | *libxev* is a high-performance, cross-platform event loop. libxev provides a 10 | unified event loop abstraction for non-blocking IO, timers, signals, events, 11 | and more that works on macOS, Windows, Linux, and WebAssembly (browser and WASI). 12 | It is written in Zig but exports a C-compatible API (which makes it compatible 13 | with any language out there that can communicate with C APIs). 14 | 15 | This manual will focus on general libxev concepts. For details specific 16 | to the C API see xev-c(7) and for details specific to the Zig API see 17 | xev-zig(7). 18 | 19 | # FEATURES 20 | 21 | *Cross-platform.* Linux (io_uring(7) and epoll(7)), macOS (kqueue(2)), 22 | WebAssembly + WASI (poll_oneoff(2), threaded and non-threaded runtimes). 23 | (Windows support is planned and coming soon) 24 | 25 | *Proactor API.* Work is submitted to the libxev event loop and the caller 26 | is notified of work _completion_, as opposed to work _readiness_. 27 | 28 | *Zero runtime allocations.* This helps make runtime performance more 29 | predictable and makes libxev well suited for embedded environments. 30 | 31 | *Timers, TCP, UDP.* High-level platform-agnostic APIs for interacting 32 | with timers, TCP/UDP sockets, and more. 33 | 34 | *Generic Thread Pool (Optional).* You can create a generic thread pool, 35 | configure its resource utilization, and use this to perform custom background 36 | tasks. The thread pool is used by some backends to do non-blocking tasks that 37 | don't have reliable non-blocking APIs (such as local file operations with 38 | kqueue(7)). The thread pool can be shared across multiple threads and event 39 | loops to optimize resource utilization. 40 | 41 | *Low-level and High-Level API.* The high-level API is platform-agnostic 42 | but has some opinionated behavior and limited flexibility. The high-level 43 | API is recommended but the low-level API is always an available escape hatch. 44 | The low-level API is platform-specific and provides a mechanism for libxev 45 | users to squeeze out maximum performance. The low-level API is _just enough 46 | abstraction_ above the OS interface to make it easier to use without 47 | sacrificing noticable performance. 48 | 49 | *Tree Shaking (Zig).* This is a feature of Zig, but substantially benefits 50 | libraries such as libxev. Zig will only include function calls and features 51 | that you actually use. If you don't use a particular kind of high-level 52 | watcher (such as UDP sockets), then the functionality related to that 53 | abstraction is not compiled into your final binary at all. This lets libxev 54 | support optional "nice-to-have" functionality that may be considered 55 | "bloat" in some cases, but the end user doesn't have to pay for it. 56 | 57 | *Dependency-free.* libxev has no dependencies other than the built-in 58 | OS APIs at runtime. The C library depends on libc. This makes it very 59 | easy to cross-compile. 60 | 61 | # EVENT LOOP PROGRAMMING 62 | 63 | Event loop programming is a programming design pattern where a program 64 | registers multiple events and is notified when those events occur. 65 | Concretely, event loop programming is typically used as a foundational 66 | component of asynchronous programs. It is the core mechanism used to 67 | for asynchronous network IO, disk IO, timers, signals, and more. 68 | 69 | There are two popular event loop styles: _proactor_ and _reactor_. 70 | The reactor pattern notifies the event loop user of task _readiness_, 71 | whereas the proactor pattern notifies the event loop user of task _completion_. 72 | Examples of reactor APIs: POSIX poll(2), BSD kqueue(2). Examples of proactor 73 | APIs: Linux io_uring(7), Windows IOCP, and JavaScript IO callbacks. 74 | *libxev is a proactor event loop.* 75 | 76 | # LIBXEV GENERAL CONCEPTS 77 | 78 | ## TERMINOLOGY 79 | 80 | - *Loop*: An instance of an event loop. 81 | 82 | - *Completion*: A request to perform some work. A completion is _queued_ 83 | in an event loop, and an associated callback is invoked when the work 84 | is completed. 85 | 86 | - *Watcher*: A slightly higher level abstraction to make it easier to 87 | work with common capabilities in a cross-platform way. For example, 88 | files, sockets, async/notify patterns, etc. These are just opinionated 89 | logic and sugar on top of completions. 90 | 91 | - *Disarm/Rearm*: A completion that is actively being worked on by the 92 | event loop is considered _armed_. When a completion is complete, the 93 | program can choose to _disarm_ or _rearm_ the completion. If a completion 94 | is disarmed, it is no longer in use. If a completion is rearmed, it will 95 | repeat its work and fire the associated callback again. 96 | 97 | ## MEMORY ALLOCATION 98 | 99 | libxev doesn't do any runtime memory allocation. The caller is expected 100 | to allocate memory and provide libxev with pointers. The caller is then 101 | free to allocate on the stack or heap in the way that is best suited for 102 | their program and lifecycle. 103 | 104 | The lifecycles of various resources in libxev are documented in their 105 | resource-specific documentation sections. libxev _never_ takes ownership 106 | of a programmer-provided memory location. 107 | 108 | ## LOOPS 109 | 110 | The `xev.Loop` (Zig) and `xev_loop` (C) types represent a single event 111 | loop. A program may have multiple event loops, but most typically there 112 | is at most one per thread. If you are just getting started, just use a 113 | single event loop per thread until you're more comfortable with the API. 114 | 115 | Completions are _added_ to the event loop. Completions are explained 116 | in more detail in the next section but at a high level represent a request 117 | to perform some work. The event loop does NOT begin performing any work 118 | until the loop is run. This is important, so to repeat it in another way: 119 | an _added_ completion does nothing until the next time a loop is run. 120 | 121 | An event loop is _run_ with `loop.run` (Zig) or xev_loop_run(3) (C). 122 | The first thing the event loop does is _submit_ any _added_ completions. 123 | Submitted completions begin their requested work in the background. If a 124 | completion doesn't complete and the loop run returns (such as with a "no wait" 125 | run mode -- covered later), the work will continue in the background and 126 | may be checked for completion with another loop tick. 127 | 128 | A loop can be run in multiple _run modes_: 129 | 130 | - *No Wait.* This runs through the loop without blocking on any completions. 131 | If a completion is ready, the callbacks will be fired, but otherwise 132 | the loop will return. 133 | 134 | - *Once.* This runs the loop and waits for at least one completion to become 135 | ready before returning. 136 | 137 | - *Until Done.* This runs the loop and waits until there are no more 138 | completions in the event loop. This can potentially run forever as 139 | completion callbacks rearm or register new completions. This is the 140 | most common run mode and is usually used to start up the "main" loop 141 | of a program. The loop can be stopped from the main thread using the 142 | `stop` API call. 143 | 144 | An event loop has to be allocated to a stable memory address (stable 145 | pointer) _once you have called `run`_ once. Prior to calling run, you 146 | can copy the loop value. However, once the loop has run any times 147 | (even a no wait run once), the loop pointer must remain stable. 148 | 149 | ## COMPLETIONS 150 | 151 | The `xev.Completion` (Zig) and `xev_completion` (C) types represent a 152 | single request to do some work, such as read or write a file, accept 153 | a network connection, sleep on a timer, etc. 154 | 155 | Completions do nothing until they are _added_ to an event loop (and 156 | even then, do nothing until the next event loop tick). Completions must 157 | only be added to one event loop at a time. After a completion is dead 158 | (see states below), it can be used with another event loop or the memory 159 | can be reclaimed. 160 | 161 | Completions have multiple states that are managed by libxev: 162 | 163 | - *Dead.* The completion can be configured for new work and added to 164 | an event loop. The completion is not actively being used by the loop 165 | so you can also free memory associated with it. This is its initial state. 166 | 167 | - *Added.* The completion is queued to be submitted to an event loop. 168 | The completion must no longer be modified. 169 | 170 | - *Active.* The completion is submitted as part of an event loop and actively 171 | performing some work. The completion must no longer be modified. 172 | 173 | The memory associated with a completion is always owned by the program 174 | author. libxev never takes ownership of memory and never dynamically 175 | allocates or free memory on its own. The memory associated with a completion 176 | cannot be freed unless the the completion is dead. 177 | 178 | A completion is dead only in the following scenarios: 179 | 180 | - The completion has never been added to an event loop. 181 | 182 | - The completion callback has been fired and the callback return action 183 | was "disarm." The completion memory is safe to free _during the callback_, 184 | in this case, too. 185 | 186 | - The event loop that a completion was added to has been deinitialized. 187 | Even if the completion never fired, all system resources registered by 188 | libxev for the completion are no longer in use. 189 | 190 | ## CALLBACKS 191 | 192 | When the work associated with a completion has been completed, libxev 193 | will invoke the registered callback. At a low-level, all callbacks have 194 | the same function signature. The Zig signature is shown below. 195 | 196 | ``` 197 | pub const xev.Callback = *const fn ( 198 | userdata: ?*anyopaque, 199 | loop: *xev.Loop, 200 | completion: *xev.Completion, 201 | result: xev.Result, 202 | ) xev.CallbackAction; 203 | ``` 204 | 205 | *NOTE:* The "low-level" word above is important. In general, program authors 206 | will be using the _high-level_ APIs which have varied and more 207 | programmer-friendly callback signatures depending on the feature. For example, 208 | TCP connection accept will provide the new connection in the callback. 209 | Underneath these abstractions, however, this regular completion callback 210 | signature is used. 211 | 212 | Every callback gets access to some optional programmer-provided userdata, 213 | the loop where the completion was added, the completion itself, and a 214 | result union. 215 | The result of the callback is the action to take and is either "disarm" 216 | or "rearm" (covered in TERMINOLOGY above). 217 | 218 | Some callback tips for common behaviors: 219 | 220 | - You can reuse the completion for a different operation now as long as 221 | you return the disarm action. For example, after a TCP connection callback, 222 | you can reuse the same completion now to begin writing data. The "loop" 223 | parameter is specifically provided to make this easy. 224 | 225 | - You can free the memory associated with a completion from the callback 226 | if you no longer intend to use it. In fact, its unsafe to free memory 227 | for an active completion except after the callback is fired (or the 228 | event loop is deinitialized). 229 | 230 | # EXAMPLE (C) 231 | 232 | The example below shows how the C API can be used to initialize an event 233 | loop and run a 5 second timer. To learn more about the C API, see 234 | xev-c(7). 235 | 236 | ``` 237 | #include 238 | #include 239 | #include 240 | 241 | xev_cb_action timerCallback(xev_loop* loop, xev_completion* c, int result, void *userdata) { 242 | return XEV_DISARM; 243 | } 244 | 245 | int main(void) { 246 | // Initialize the loop state. Notice we can use a stack-allocated 247 | // value here. We can even pass around the loop by value! The loop 248 | // will contain all of our "completions" (watches). 249 | xev_loop loop; 250 | if (xev_loop_init(&loop, 128) != 0) { 251 | printf("xev_loop_init failure\n"); 252 | return 1; 253 | } 254 | 255 | // Initialize a completion and a watcher. A completion is the actual 256 | // thing a loop does for us, and a watcher is a high-level structure 257 | // to help make it easier to use completions. 258 | xev_completion c; 259 | xev_watcher w; 260 | 261 | // In this case, we initialize a timer watcher. 262 | if (xev_timer_init(&w) != 0) { 263 | printf("xev_timer_init failure\n"); 264 | return 1; 265 | } 266 | 267 | // Configure the timer to run in 5s. This requires the completion that 268 | // we actually configure to become a timer, and the loop that the 269 | // completion should be registered with. 270 | xev_timer_run(&w, &loop, &c, 5000, NULL, &timerCallback); 271 | 272 | // Run the loop until there are no more completions. 273 | xev_loop_run(&loop, XEV_RUN_UNTIL_DONE); 274 | 275 | xev_timer_deinit(&w); 276 | xev_loop_deinit(&loop); 277 | return 0; 278 | } 279 | ``` 280 | 281 | # EXAMPLE (ZIG) 282 | 283 | The example below shows how the Zig API can be used to initialize an event 284 | loop and run a 5 second timer. To learn more about the Zig API, see 285 | xev-zig(7). 286 | 287 | ``` 288 | const xev = @import("xev"); 289 | 290 | pub fn main() !void { 291 | // Initialize the loop state. Notice we can use a stack-allocated 292 | // value here. We can even pass around the loop by value! The loop 293 | // will contain all of our "completions" (watches). 294 | var loop = try xev.Loop.init(.{ .entries = 128 }); 295 | defer loop.deinit(); 296 | 297 | // Initialize a completion and a watcher. A completion is the actual 298 | // thing a loop does for us, and a watcher is a high-level structure 299 | // to help make it easier to use completions. 300 | var c: xev.Completion = undefined; 301 | 302 | // In this case, we initialize a timer watcher. 303 | const w = try xev.Timer.init(); 304 | defer w.deinit(); 305 | 306 | // Configure the timer to run in 5s. This requires the completion that 307 | // we actually configure to become a timer, and the loop that the 308 | // completion should be registered with. 309 | w.run(&loop, &c, 5000, void, null, &timerCallback); 310 | 311 | // Run the loop until there are no more completions. 312 | try loop.run(.until_done); 313 | } 314 | 315 | fn timerCallback( 316 | userdata: ?*void, 317 | loop: *xev.Loop, 318 | c: *xev.Completion, 319 | result: xev.Timer.RunError!void, 320 | ) xev.CallbackAction { 321 | _ = userdata; 322 | _ = loop; 323 | _ = c; 324 | _ = result catch unreachable; 325 | return .disarm; 326 | } 327 | ``` 328 | 329 | # SEE ALSO 330 | 331 | xev-c(7), xev-zig(7) 332 | 333 | 334 | 335 | # AUTHORS 336 | 337 | Mitchell Hashimoto (xmitchx@gmail.com) and any open source contributors. 338 | See . 339 | -------------------------------------------------------------------------------- /docs/xev_completion_state.3.scd: -------------------------------------------------------------------------------- 1 | xev_completion_state(3) "github.com/mitchellh/libxev" "Library Functions Manual" 2 | 3 | # NAME 4 | 5 | xev_completion_state - check the state of a completion 6 | 7 | # LIBRARY 8 | 9 | libxev (_-lxev_) 10 | 11 | # SYNOPSIS 12 | 13 | ``` 14 | #include ; 15 | 16 | xev_completion_state_t state = xev_completion_state(&c); 17 | ``` 18 | 19 | # DESCRIPTION 20 | 21 | *xev_completion_state* returns the current state of a completion: dead 22 | if the completion is not currently used or active if the completion is 23 | part of an event loop. 24 | 25 | The state is sometimes useful to determine if an operation has already 26 | started or not. For example, the completion state can be used to check if 27 | a timer is running or not. 28 | 29 | The completion must be initialized if there is any chance the program author 30 | may call this function prior to the completion being used. To initialize 31 | a completion use xev_completion_zero(3). 32 | 33 | This function can only be called from the main thread. 34 | 35 | # RETURN VALUES 36 | 37 | The return value is always a valid xev_completion_state_t enum value. 38 | 39 | # SEE ALSO 40 | 41 | xev_completion_state(3), xev(7), xev-c(7) 42 | 43 | 44 | 45 | # AUTHORS 46 | 47 | Mitchell Hashimoto (xmitchx@gmail.com) and any open source contributors. 48 | See . 49 | -------------------------------------------------------------------------------- /docs/xev_completion_zero.3.scd: -------------------------------------------------------------------------------- 1 | xev_completion_zero(3) "github.com/mitchellh/libxev" "Library Functions Manual" 2 | 3 | # NAME 4 | 5 | xev_completion_zero - set a completion to the zero value 6 | 7 | # LIBRARY 8 | 9 | libxev (_-lxev_) 10 | 11 | # SYNOPSIS 12 | 13 | ``` 14 | #include ; 15 | 16 | xev_completion_zero(&c); 17 | ``` 18 | 19 | # DESCRIPTION 20 | 21 | *xev_completion_zero* sets default values for the given completion, rather 22 | than uninitialized memory state. This is particularly useful if you're using 23 | xev_completion_state(3). Otherwise, it isn't necessary. 24 | 25 | _You typically do NOT need to call this!_ You are allowed to pass uninitialized 26 | completions to almost all functions that start an operation. However, you 27 | _do need to call this_ if you are inspecting completion state (i.e. 28 | xev_completion_state(3)) before the completion is started. 29 | 30 | You do NOT need to call this after you are done using a completion. Once 31 | a completion has been used, the value of the completion is always initialized. 32 | The ONLY time you need to call this is if you're inspecting completion state 33 | prior to the completion ever being used. 34 | 35 | # SEE ALSO 36 | 37 | xev_completion_state(3), xev(7), xev-c(7) 38 | 39 | 40 | 41 | # AUTHORS 42 | 43 | Mitchell Hashimoto (xmitchx@gmail.com) and any open source contributors. 44 | See . 45 | -------------------------------------------------------------------------------- /docs/xev_threadpool.3.scd: -------------------------------------------------------------------------------- 1 | xev_threadpool(3) "github.com/mitchellh/libxev" "Library Functions Manual" 2 | 3 | # NAME 4 | 5 | xev_threadpool - generic thread pool for scheduling work 6 | 7 | # LIBRARY 8 | 9 | libxev (_-lxev_) 10 | 11 | # SYNOPSIS 12 | 13 | ``` 14 | #include ; 15 | 16 | xev_threadpool pool; 17 | ``` 18 | 19 | # DESCRIPTION 20 | 21 | *xev_threadpool* is a generic thread pool that is lock-free, allocation-free 22 | (except spawning threads), supports batch scheduling, dynamically spawns 23 | threads, and handles thread spawn failure. It can be used to queue work that 24 | should be executed on other threads as resources permit. 25 | 26 | This man page focuses on the C API for the thread pool. The Zig API 27 | can be discovered by reading the `src/ThreadPool.zig` file. This page will 28 | provide a broad overview of using the thread pool API but won't cover 29 | specific details such as an exhaustive list of error return codes. 30 | 31 | There are multiple types that are important when working with thread pools. 32 | 33 | - *xev_threadpool* is the pool itself. 34 | - *xev_threadpool_config* is used to configure a new pool. 35 | - *xev_threadpool_task* is a single task to execute. 36 | - *xev_threadpool_batch* is a batch of zero or more tasks. 37 | 38 | All the types are expected to be allocated by the program author. They 39 | can be allocated on the stack or heap, but their pointer lifetimes must 40 | remain valid as documented throughout this manual. 41 | 42 | # POOL CREATION AND MANAGEMENT 43 | 44 | To start, a pool must be initialized. The easiest way to initialize a pool 45 | is with xev_threadpool_init(3) and no configuration. The default configuration 46 | will choose reasonable defaults. 47 | 48 | ``` 49 | xev_threadpool pool; 50 | assert(xev_threadpool_init(&pool, null)); 51 | ``` 52 | 53 | The pool is now ready to have task batches scheduled to it. To shutdown 54 | a pool, you must call xev_shutdown(3) and xev_deinit(3). 55 | 56 | ``` 57 | xev_shutdown(&pool); 58 | xev_deinit(&pool); 59 | ``` 60 | 61 | The xev_shutdown(3) call notifies all threads that the pool is in a shutdown 62 | state. They complete their most recent tasks, shut down, and accept no new 63 | work. This function returns _immediately_. The xev_deinit(3) call waits for 64 | all the threads in the pool to exit, then cleans up any additional state. 65 | xev_deinit(3) and xev_shutdown(3) can be called in any order. 66 | 67 | The xev_threadpool(3) value must be pointer-stable until after 68 | xev_deinit(3) returns. After this point, no other xev_threadpool API 69 | calls can be called using the pointer. You may reinitialize and reuse the 70 | value. 71 | 72 | # TASK SCHEDULING 73 | 74 | A task is a single unit of work. A task is inserted into a _batch_. 75 | A batch is scheduled with a _pool_. The first step is to define a task: 76 | 77 | ``` 78 | void do_expensive_work(xev_threadpool_task* t) {} 79 | 80 | xev_threadpool_task t; 81 | xev_threadpool_task_init(&t, &do_expensive_work); 82 | ``` 83 | 84 | The task then must be added to a batch. The code below creates a batch 85 | with a single item "t". You can use xev_threadpool_batch_push_batch(3) to merge 86 | multiple batches. 87 | 88 | ``` 89 | xev_threadpool_batch b; 90 | xev_threadpool_batch_init(&b); 91 | xev_threadpool_batch_push_task(&b, &t); 92 | ``` 93 | 94 | Finally, the batch must be scheduled with the pool with 95 | xev_threadpool_schedule(3): 96 | 97 | ``` 98 | xev_threadpool_schedule(&pool, &b); 99 | ``` 100 | 101 | The scheduled work can be picked up immediately. The work is executed 102 | on a separate thread so if resources are available, the work may begin 103 | immediately. Otherwise, it is queued for execution later. 104 | 105 | You can call xev_threadpool_schedule(3) from multiple threads against 106 | the same pool to schedule work concurrently. You MUST NOT read or write 107 | batches concurrently; for concurrent scheduling each thread should build 108 | up its own batch. 109 | 110 | ## MEMORY LIFETIMES 111 | 112 | - The task "t" must be pointer-stable until the task has completed execution. 113 | 114 | - The task "t" can only be scheduled in one pool exactly one time until 115 | it has completed execution. You CAN NOT initialize a task and add it to 116 | multiple batches. 117 | 118 | - The batch "b" can be copied and doesn't need to be pointer-stable. The 119 | batch can be freed at anytime. 120 | 121 | ## TASK STATE 122 | 123 | The callback type only gives access to the `xev_threadpool_task` pointer. 124 | To associate state or userdata with a task, make the task a member of 125 | a struct and use the `container_of` macro (shown below) to access the 126 | parent container. 127 | 128 | An example is shown below: 129 | 130 | ``` 131 | typedef struct { 132 | xev_threadpool_task task; 133 | bool state; 134 | 135 | // other state can be here, too. 136 | } work_t; 137 | 138 | 139 | #define container_of(ptr, type, member) \ 140 | ((type *) ((char *) (ptr) - offsetof(type, member))) 141 | 142 | void do_expensive_work(xev_threadpool_task* t) { 143 | work_t *work = container_of(t, work_t, task); 144 | work->state = true; 145 | } 146 | 147 | work_t work; 148 | xev_threadpool_task_init(&work.task, &do_expensive_work); 149 | ``` 150 | 151 | *IMPORTANT:* The `xev_threadpool_task` must be aligned to a power-of-2 152 | memory address. When using it in a struct, be careful that it is properly 153 | aligned. 154 | 155 | # SEE ALSO 156 | 157 | xev(7), xev-c(7) 158 | 159 | 160 | 161 | 162 | 163 | # AUTHORS 164 | 165 | King Protty (https://github.com/kprotty) is the author of the thread pool. 166 | Mitchell Hashimoto (xmitchx@gmail.com) is the author of the C API and 167 | documentation. Plus any open source contributors. See . 168 | -------------------------------------------------------------------------------- /examples/_basic.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | xev_cb_action timerCallback(xev_loop* loop, xev_completion* c, int result, void *userdata) { 6 | return XEV_DISARM; 7 | } 8 | 9 | int main(void) { 10 | // Initialize the loop state. Notice we can use a stack-allocated 11 | // value here. We can even pass around the loop by value! The loop 12 | // will contain all of our "completions" (watches). 13 | xev_loop loop; 14 | if (xev_loop_init(&loop) != 0) { 15 | printf("xev_loop_init failure\n"); 16 | return 1; 17 | } 18 | 19 | // Initialize a completion and a watcher. A completion is the actual 20 | // thing a loop does for us, and a watcher is a high-level structure 21 | // to help make it easier to use completions. 22 | xev_completion c; 23 | xev_watcher w; 24 | 25 | // In this case, we initialize a timer watcher. 26 | if (xev_timer_init(&w) != 0) { 27 | printf("xev_timer_init failure\n"); 28 | return 1; 29 | } 30 | 31 | // Configure the timer to run in 1ms. This requires the completion that 32 | // we actually configure to become a timer, and the loop that the 33 | // completion should be registered with. 34 | xev_timer_run(&w, &loop, &c, 1, NULL, &timerCallback); 35 | 36 | // Run the loop until there are no more completions. 37 | xev_loop_run(&loop, XEV_RUN_UNTIL_DONE); 38 | 39 | xev_timer_deinit(&w); 40 | xev_loop_deinit(&loop); 41 | return 0; 42 | } 43 | -------------------------------------------------------------------------------- /examples/_basic.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Instant = std.time.Instant; 3 | const xev = @import("xev"); 4 | 5 | pub fn main() !void { 6 | // Initialize the loop state. Notice we can use a stack-allocated 7 | // value here. We can even pass around the loop by value! The loop 8 | // will contain all of our "completions" (watches). 9 | var loop = try xev.Loop.init(.{}); 10 | defer loop.deinit(); 11 | 12 | // Initialize a completion and a watcher. A completion is the actual 13 | // thing a loop does for us, and a watcher is a high-level structure 14 | // to help make it easier to use completions. 15 | var c: xev.Completion = undefined; 16 | const timer = try xev.Timer.init(); 17 | timer.run(&loop, &c, 1, void, null, timerCallback); 18 | 19 | // Run the loop until there are no more completions. 20 | try loop.run(.until_done); 21 | } 22 | 23 | fn timerCallback( 24 | _: ?*void, 25 | _: *xev.Loop, 26 | _: *xev.Completion, 27 | result: xev.Timer.RunError!void, 28 | ) xev.CallbackAction { 29 | _ = result catch unreachable; 30 | return .disarm; 31 | } 32 | -------------------------------------------------------------------------------- /examples/async.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #define UNUSED(v) ((void)v); 7 | 8 | xev_cb_action timer_callback(xev_loop* loop, xev_completion* c, int result, void *userdata) { 9 | UNUSED(loop); UNUSED(c); UNUSED(result); 10 | 11 | // Send the notification to our async which will wake up the loop and 12 | // call the waiter callback. 13 | xev_async_notify((xev_watcher *)userdata); 14 | return XEV_DISARM; 15 | } 16 | 17 | xev_cb_action async_callback(xev_loop* loop, xev_completion* c, int result, void *userdata) { 18 | UNUSED(loop); UNUSED(c); UNUSED(result); 19 | 20 | bool *notified = (bool *)userdata; 21 | *notified = true; 22 | return XEV_DISARM; 23 | } 24 | 25 | int main(void) { 26 | xev_loop loop; 27 | if (xev_loop_init(&loop) != 0) { 28 | printf("xev_loop_init failure\n"); 29 | return 1; 30 | } 31 | 32 | // Initialize an async watcher. An async watcher can be used to wake up 33 | // the event loop from any thread. 34 | xev_completion async_c; 35 | xev_watcher async; 36 | if (xev_async_init(&async) != 0) { 37 | printf("xev_async_init failure\n"); 38 | return 1; 39 | } 40 | 41 | // We start a "waiter" for the async watcher. Only one waiter can 42 | // ever be set at a time. This callback will be called when the async 43 | // is notified (via xev_async_notify). 44 | bool notified = false; 45 | xev_async_wait(&async, &loop, &async_c, ¬ified, &async_callback); 46 | 47 | // Initialize a timer. The timer will fire our async. 48 | xev_completion timer_c; 49 | xev_watcher timer; 50 | if (xev_timer_init(&timer) != 0) { 51 | printf("xev_timer_init failure\n"); 52 | return 1; 53 | } 54 | xev_timer_run(&timer, &loop, &timer_c, 1, &async, &timer_callback); 55 | 56 | // Run the loop until there are no more completions. This means 57 | // that both the async watcher AND the timer have to complete. 58 | // Notice that if you comment out `xev_async_notify` in the timer 59 | // callback this blocks forever (because the async watcher is waiting). 60 | xev_loop_run(&loop, XEV_RUN_UNTIL_DONE); 61 | 62 | if (!notified) { 63 | printf("FAIL! async should've been notified!"); 64 | return 1; 65 | } 66 | 67 | xev_timer_deinit(&timer); 68 | xev_async_deinit(&async); 69 | xev_loop_deinit(&loop); 70 | return 0; 71 | } 72 | -------------------------------------------------------------------------------- /examples/million-timers.c: -------------------------------------------------------------------------------- 1 | // libuv million-timers benchmark ported to libxev 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #define NUM_TIMERS (10 * 1000 * 1000) 9 | 10 | static int timer_cb_called; 11 | 12 | xev_cb_action timer_cb(xev_loop* loop, xev_completion* c, int result, void *userdata) { 13 | timer_cb_called++; 14 | return XEV_DISARM; 15 | } 16 | 17 | #ifdef _WIN32 18 | #include 19 | uint64_t hrtime(void) { 20 | static int initialized = 0; 21 | static LARGE_INTEGER start_timestamp; 22 | static uint64_t qpc_tick_duration; 23 | 24 | if (!initialized) { 25 | initialized = 1; 26 | 27 | LARGE_INTEGER qpc_freq; 28 | QueryPerformanceFrequency(&qpc_freq); 29 | qpc_tick_duration = 1e9 / qpc_freq.QuadPart; 30 | 31 | QueryPerformanceCounter(&start_timestamp); 32 | } 33 | 34 | LARGE_INTEGER t; 35 | QueryPerformanceCounter(&t); 36 | t.QuadPart -= start_timestamp.QuadPart; 37 | 38 | return (uint64_t)t.QuadPart * qpc_tick_duration; 39 | } 40 | #else 41 | uint64_t hrtime(void) { 42 | struct timespec ts; 43 | clock_gettime(CLOCK_MONOTONIC, &ts); 44 | return ts.tv_nsec + (ts.tv_sec * 1e9); 45 | } 46 | #endif 47 | 48 | int main(void) { 49 | xev_watcher* timers; 50 | xev_completion* completions; 51 | xev_loop loop; 52 | uint64_t before_all; 53 | uint64_t before_run; 54 | uint64_t after_run; 55 | uint64_t after_all; 56 | int timeout; 57 | int i; 58 | int err; 59 | 60 | timers = malloc(NUM_TIMERS * sizeof(timers[0])); 61 | completions = malloc(NUM_TIMERS * sizeof(completions[0])); 62 | 63 | if ((err = xev_loop_init(&loop)) != 0) { 64 | fprintf(stderr, "xev_loop_init failure\n"); 65 | return 1; 66 | } 67 | timeout = 1; 68 | 69 | before_all = hrtime(); 70 | for (i = 0; i < NUM_TIMERS; i++) { 71 | if (i % 1000 == 0) timeout++; 72 | xev_timer_init(timers + i); 73 | xev_timer_run(timers + i, &loop, completions + i, timeout, NULL, &timer_cb); 74 | } 75 | 76 | before_run = hrtime(); 77 | xev_loop_run(&loop, XEV_RUN_UNTIL_DONE); 78 | after_run = hrtime(); 79 | after_all = hrtime(); 80 | 81 | if (timer_cb_called != NUM_TIMERS) return 1; 82 | free(timers); 83 | free(completions); 84 | 85 | fprintf(stderr, "%.2f seconds total\n", (after_all - before_all) / 1e9); 86 | fprintf(stderr, "%.2f seconds init\n", (before_run - before_all) / 1e9); 87 | fprintf(stderr, "%.2f seconds dispatch\n", (after_run - before_run) / 1e9); 88 | fprintf(stderr, "%.2f seconds cleanup\n", (after_all - after_run) / 1e9); 89 | fflush(stderr); 90 | return 0; 91 | } 92 | -------------------------------------------------------------------------------- /examples/threadpool.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | // We create a job struct that can contain additional state that we use 8 | // for our threadpool tasks. In this case, we only have a boolean to note 9 | // we're done but you can imagine any sort of user data here! 10 | typedef struct { 11 | xev_threadpool_task pool_task; 12 | 13 | bool done; 14 | } job_t; 15 | 16 | // We use the container_of trick to access our job_t from a threadpool task. 17 | // See task_callback. 18 | #define container_of(ptr, type, member) \ 19 | ((type *) ((char *) (ptr) - offsetof(type, member))) 20 | 21 | // This is the callback that is invoked when the task is being worked on. 22 | void task_callback(xev_threadpool_task* t) { 23 | job_t *job = container_of(t, job_t, pool_task); 24 | job->done = true; 25 | } 26 | 27 | int main(void) { 28 | xev_threadpool pool; 29 | if (xev_threadpool_init(&pool, NULL) != 0) { 30 | printf("xev_threadpool_init failure\n"); 31 | return 1; 32 | } 33 | 34 | // A "batch" is used to group together multiple tasks that we schedule 35 | // atomically into the thread pool. We initialize an empty batch that 36 | // we'll add our tasks to. 37 | xev_threadpool_batch batch; 38 | xev_threadpool_batch_init(&batch); 39 | 40 | // Create all our tasks we want to submit. The number here can be changed 41 | // to anything! 42 | const int TASK_COUNT = 128; 43 | job_t jobs[TASK_COUNT]; 44 | for (int i = 0; i < TASK_COUNT; i++) { 45 | jobs[i].done = false; 46 | xev_threadpool_task_init(&jobs[i].pool_task, &task_callback); 47 | xev_threadpool_batch_push_task(&batch, &jobs[i].pool_task); 48 | } 49 | 50 | // Schedule our batch. This will immediately queue and start the tasks 51 | // if there are available threads. This will also automatically start 52 | // threads as needed. After this, you can reclaim the memory associated 53 | // with "batch". 54 | xev_threadpool_schedule(&pool, &batch); 55 | 56 | // We need a way to detect that our work is done. Normally here you'd 57 | // use some sort of waitgroup or signal the libxev loop or something. 58 | // Since this example is showing ONLY the threadpool, we just do a 59 | // somewhat unsafe thing and just race on done booleans... 60 | while (true) { 61 | bool done = true; 62 | for (int i = 0; i < TASK_COUNT; i++) { 63 | if (!jobs[i].done) { 64 | done = false; 65 | break; 66 | } 67 | } 68 | if (done) break; 69 | } 70 | 71 | // Shutdown notifies the threadpool to notify the threads it has launched 72 | // to start shutting down. This MUST be called. 73 | xev_threadpool_shutdown(&pool); 74 | 75 | // Deinit reclaims memory. 76 | xev_threadpool_deinit(&pool); 77 | 78 | printf("%d tasks completed!\n", TASK_COUNT); 79 | return 0; 80 | } 81 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-compat": { 4 | "flake": false, 5 | "locked": { 6 | "lastModified": 1673956053, 7 | "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", 8 | "owner": "edolstra", 9 | "repo": "flake-compat", 10 | "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", 11 | "type": "github" 12 | }, 13 | "original": { 14 | "owner": "edolstra", 15 | "repo": "flake-compat", 16 | "type": "github" 17 | } 18 | }, 19 | "flake-compat_2": { 20 | "flake": false, 21 | "locked": { 22 | "lastModified": 1696426674, 23 | "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", 24 | "owner": "edolstra", 25 | "repo": "flake-compat", 26 | "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", 27 | "type": "github" 28 | }, 29 | "original": { 30 | "owner": "edolstra", 31 | "repo": "flake-compat", 32 | "type": "github" 33 | } 34 | }, 35 | "flake-utils": { 36 | "locked": { 37 | "lastModified": 1667395993, 38 | "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=", 39 | "owner": "numtide", 40 | "repo": "flake-utils", 41 | "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f", 42 | "type": "github" 43 | }, 44 | "original": { 45 | "owner": "numtide", 46 | "repo": "flake-utils", 47 | "type": "github" 48 | } 49 | }, 50 | "flake-utils_2": { 51 | "inputs": { 52 | "systems": "systems" 53 | }, 54 | "locked": { 55 | "lastModified": 1705309234, 56 | "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", 57 | "owner": "numtide", 58 | "repo": "flake-utils", 59 | "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", 60 | "type": "github" 61 | }, 62 | "original": { 63 | "owner": "numtide", 64 | "repo": "flake-utils", 65 | "type": "github" 66 | } 67 | }, 68 | "nixpkgs": { 69 | "locked": { 70 | "lastModified": 1717895720, 71 | "narHash": "sha256-Dl6JKx1rIDEuv4q9rtlt9QwyerSQbrk1bUtNHzx9bIY=", 72 | "owner": "nixos", 73 | "repo": "nixpkgs", 74 | "rev": "0e0826ec06d2b3db8e28e280d68179f022b1d160", 75 | "type": "github" 76 | }, 77 | "original": { 78 | "owner": "nixos", 79 | "ref": "release-24.05", 80 | "repo": "nixpkgs", 81 | "type": "github" 82 | } 83 | }, 84 | "nixpkgs-unstable": { 85 | "locked": { 86 | "lastModified": 1675584158, 87 | "narHash": "sha256-SBkchaDzCHxnPNRDdtZ5ko5caHio9iS0Mbyn/xXbXxs=", 88 | "owner": "nixos", 89 | "repo": "nixpkgs", 90 | "rev": "d840126a0890621e7b220894d749132dd4bde6a0", 91 | "type": "github" 92 | }, 93 | "original": { 94 | "owner": "nixos", 95 | "ref": "nixpkgs-unstable", 96 | "repo": "nixpkgs", 97 | "type": "github" 98 | } 99 | }, 100 | "nixpkgs_2": { 101 | "locked": { 102 | "lastModified": 1708161998, 103 | "narHash": "sha256-6KnemmUorCvlcAvGziFosAVkrlWZGIc6UNT9GUYr0jQ=", 104 | "owner": "NixOS", 105 | "repo": "nixpkgs", 106 | "rev": "84d981bae8b5e783b3b548de505b22880559515f", 107 | "type": "github" 108 | }, 109 | "original": { 110 | "owner": "NixOS", 111 | "ref": "nixos-23.11", 112 | "repo": "nixpkgs", 113 | "type": "github" 114 | } 115 | }, 116 | "root": { 117 | "inputs": { 118 | "flake-compat": "flake-compat", 119 | "flake-utils": "flake-utils", 120 | "nixpkgs": "nixpkgs", 121 | "nixpkgs-unstable": "nixpkgs-unstable", 122 | "zig": "zig" 123 | } 124 | }, 125 | "systems": { 126 | "locked": { 127 | "lastModified": 1681028828, 128 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 129 | "owner": "nix-systems", 130 | "repo": "default", 131 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 132 | "type": "github" 133 | }, 134 | "original": { 135 | "owner": "nix-systems", 136 | "repo": "default", 137 | "type": "github" 138 | } 139 | }, 140 | "zig": { 141 | "inputs": { 142 | "flake-compat": "flake-compat_2", 143 | "flake-utils": "flake-utils_2", 144 | "nixpkgs": "nixpkgs_2" 145 | }, 146 | "locked": { 147 | "lastModified": 1741566430, 148 | "narHash": "sha256-BEKycplUAd9A0KBKIKrV2tw11+JmXEbVU8zMpe4AJ38=", 149 | "owner": "mitchellh", 150 | "repo": "zig-overlay", 151 | "rev": "b2897fe1ff741b627cbb8c6e41531fe9d5a4ed47", 152 | "type": "github" 153 | }, 154 | "original": { 155 | "owner": "mitchellh", 156 | "repo": "zig-overlay", 157 | "type": "github" 158 | } 159 | } 160 | }, 161 | "root": "root", 162 | "version": 7 163 | } 164 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "libxev is a high performance, cross-platform event loop."; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:nixos/nixpkgs/release-24.05"; 6 | nixpkgs-unstable.url = "github:nixos/nixpkgs/nixpkgs-unstable"; 7 | flake-utils.url = "github:numtide/flake-utils"; 8 | zig.url = "github:mitchellh/zig-overlay"; 9 | 10 | # Used for shell.nix 11 | flake-compat = { 12 | url = github:edolstra/flake-compat; 13 | flake = false; 14 | }; 15 | }; 16 | 17 | outputs = { 18 | self, 19 | nixpkgs, 20 | flake-utils, 21 | ... 22 | } @ inputs: let 23 | overlays = [ 24 | # Other overlays 25 | (final: prev: rec { 26 | zigpkgs = inputs.zig.packages.${prev.system}; 27 | zig = inputs.zig.packages.${prev.system}."0.14.0"; 28 | 29 | # Latest versions 30 | wasmtime = inputs.nixpkgs-unstable.legacyPackages.${prev.system}.wasmtime; 31 | wasmer = inputs.nixpkgs-unstable.legacyPackages.${prev.system}.wasmer; 32 | 33 | # Our package 34 | libxev = prev.callPackage ./nix/package.nix {}; 35 | }) 36 | ]; 37 | 38 | # Our supported systems are the same supported systems as the Zig binaries 39 | systems = builtins.attrNames inputs.zig.packages; 40 | in 41 | flake-utils.lib.eachSystem systems ( 42 | system: let 43 | pkgs = import nixpkgs {inherit overlays system;}; 44 | in rec { 45 | devShells.default = pkgs.mkShell { 46 | nativeBuildInputs = with pkgs; [ 47 | mandoc 48 | scdoc 49 | zig 50 | 51 | # Wasm 52 | wabt 53 | wasmtime 54 | wasmer 55 | 56 | # Website 57 | nodejs 58 | ]; 59 | }; 60 | 61 | # For compatibility with older versions of the `nix` binary 62 | devShell = self.devShells.${system}.default; 63 | 64 | # Our package 65 | packages.libxev = pkgs.libxev; 66 | defaultPackage = packages.libxev; 67 | } 68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /include/xev.h: -------------------------------------------------------------------------------- 1 | #ifndef XEV_H 2 | #define XEV_H 3 | 4 | #ifdef __cplusplus 5 | extern "C" { 6 | #endif 7 | 8 | #include 9 | #include 10 | 11 | /* TODO(mitchellh): we should use platform detection to set the correct 12 | * byte sizes here. We choose some overly large values for now so that 13 | * we can retain ABI compatibility. */ 14 | const size_t XEV_SIZEOF_LOOP = 512; 15 | const size_t XEV_SIZEOF_COMPLETION = 320; 16 | const size_t XEV_SIZEOF_WATCHER = 256; 17 | const size_t XEV_SIZEOF_THREADPOOL = 64; 18 | const size_t XEV_SIZEOF_THREADPOOL_BATCH = 24; 19 | const size_t XEV_SIZEOF_THREADPOOL_TASK = 24; 20 | const size_t XEV_SIZEOF_THREADPOOL_CONFIG = 64; 21 | 22 | #if __STDC_VERSION__ >= 201112L || __cplusplus >= 201103L 23 | typedef max_align_t XEV_ALIGN_T; 24 | #else 25 | // max_align_t is usually synonymous with the largest scalar type, which is long double on most platforms, and its alignment requirement is either 8 or 16. 26 | typedef long double XEV_ALIGN_T; 27 | #endif 28 | 29 | /* There's a ton of preprocessor directives missing here for real cross-platform 30 | * compatibility. I'm going to defer to the community or future issues to help 31 | * plug those holes. For now, we get some stuff working we can test! */ 32 | 33 | /* Opaque types. These types have a size defined so that they can be 34 | * statically allocated but they are not to be accessed. */ 35 | // todo: give struct individual alignment, instead of max alignment 36 | typedef struct { XEV_ALIGN_T _pad; uint8_t data[XEV_SIZEOF_LOOP - sizeof(XEV_ALIGN_T)]; } xev_loop; 37 | typedef struct { XEV_ALIGN_T _pad; uint8_t data[XEV_SIZEOF_COMPLETION - sizeof(XEV_ALIGN_T)]; } xev_completion; 38 | typedef struct { XEV_ALIGN_T _pad; uint8_t data[XEV_SIZEOF_WATCHER - sizeof(XEV_ALIGN_T)]; } xev_watcher; 39 | typedef struct { XEV_ALIGN_T _pad; uint8_t data[XEV_SIZEOF_THREADPOOL - sizeof(XEV_ALIGN_T)]; } xev_threadpool; 40 | typedef struct { XEV_ALIGN_T _pad; uint8_t data[XEV_SIZEOF_THREADPOOL_BATCH - sizeof(XEV_ALIGN_T)]; } xev_threadpool_batch; 41 | typedef struct { XEV_ALIGN_T _pad; uint8_t data[XEV_SIZEOF_THREADPOOL_TASK - sizeof(XEV_ALIGN_T)]; } xev_threadpool_task; 42 | typedef struct { XEV_ALIGN_T _pad; uint8_t data[XEV_SIZEOF_THREADPOOL_CONFIG - sizeof(XEV_ALIGN_T)]; } xev_threadpool_config; 43 | 44 | /* Callback types. */ 45 | typedef enum { XEV_DISARM = 0, XEV_REARM = 1 } xev_cb_action; 46 | typedef void (*xev_task_cb)(xev_threadpool_task* t); 47 | typedef xev_cb_action (*xev_timer_cb)(xev_loop* l, xev_completion* c, int result, void* userdata); 48 | typedef xev_cb_action (*xev_async_cb)(xev_loop* l, xev_completion* c, int result, void* userdata); 49 | 50 | typedef enum { 51 | XEV_RUN_NO_WAIT = 0, 52 | XEV_RUN_ONCE = 1, 53 | XEV_RUN_UNTIL_DONE = 2, 54 | } xev_run_mode_t; 55 | 56 | typedef enum { 57 | XEV_COMPLETION_DEAD = 0, 58 | XEV_COMPLETION_ACTIVE = 1, 59 | } xev_completion_state_t; 60 | 61 | 62 | /* Documentation for functions can be found in man pages or online. I 63 | * purposely do not add docs to the header so that you can quickly scan 64 | * all exported functions. */ 65 | int xev_loop_init(xev_loop* loop); 66 | void xev_loop_deinit(xev_loop* loop); 67 | int xev_loop_run(xev_loop* loop, xev_run_mode_t mode); 68 | int64_t xev_loop_now(xev_loop* loop); 69 | void xev_loop_update_now(xev_loop* loop); 70 | 71 | void xev_completion_zero(xev_completion* c); 72 | xev_completion_state_t xev_completion_state(xev_completion* c); 73 | 74 | void xev_threadpool_config_init(xev_threadpool_config* config); 75 | void xev_threadpool_config_set_stack_size(xev_threadpool_config* config, uint32_t v); 76 | void xev_threadpool_config_set_max_threads(xev_threadpool_config* config, uint32_t v); 77 | 78 | int xev_threadpool_init(xev_threadpool* pool, xev_threadpool_config* config); 79 | void xev_threadpool_deinit(xev_threadpool* pool); 80 | void xev_threadpool_shutdown(xev_threadpool* pool); 81 | void xev_threadpool_schedule(xev_threadpool* pool, xev_threadpool_batch *batch); 82 | 83 | void xev_threadpool_task_init(xev_threadpool_task* t, xev_task_cb cb); 84 | void xev_threadpool_batch_init(xev_threadpool_batch* b); 85 | void xev_threadpool_batch_push_task(xev_threadpool_batch* b, xev_threadpool_task *t); 86 | void xev_threadpool_batch_push_batch(xev_threadpool_batch* b, xev_threadpool_batch *other); 87 | 88 | int xev_timer_init(xev_watcher *w); 89 | void xev_timer_deinit(xev_watcher *w); 90 | void xev_timer_run(xev_watcher *w, xev_loop* loop, xev_completion* c, uint64_t next_ms, void* userdata, xev_timer_cb cb); 91 | void xev_timer_reset(xev_watcher *w, xev_loop* loop, xev_completion* c, xev_completion *c_cancel, uint64_t next_ms, void* userdata, xev_timer_cb cb); 92 | void xev_timer_cancel(xev_watcher *w, xev_loop* loop, xev_completion* c, xev_completion* c_cancel, void* userdata, xev_timer_cb cb); 93 | 94 | int xev_async_init(xev_watcher *w); 95 | void xev_async_deinit(xev_watcher *w); 96 | int xev_async_notify(xev_watcher *w); 97 | void xev_async_wait(xev_watcher *w, xev_loop* loop, xev_completion* c, void* userdata, xev_async_cb cb); 98 | 99 | #ifdef __cplusplus 100 | } 101 | #endif 102 | 103 | #endif /* XEV_H */ 104 | -------------------------------------------------------------------------------- /nix/package.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | , lib 3 | , zig 4 | , pkg-config 5 | , scdoc 6 | }: 7 | 8 | stdenv.mkDerivation rec { 9 | pname = "libxev"; 10 | version = "0.1.0"; 11 | 12 | src = ./..; 13 | 14 | nativeBuildInputs = [ zig scdoc pkg-config ]; 15 | 16 | buildInputs = []; 17 | 18 | dontConfigure = true; 19 | 20 | preBuild = '' 21 | # Necessary for zig cache to work 22 | export HOME=$TMPDIR 23 | ''; 24 | 25 | installPhase = '' 26 | runHook preInstall 27 | zig build -Doptimize=ReleaseFast -Demit-man-pages --prefix $out install 28 | runHook postInstall 29 | ''; 30 | 31 | outputs = [ "out" "dev" "man" ]; 32 | 33 | meta = with lib; { 34 | description = "A high performance, cross-platform event loop."; 35 | homepage = "https://github.com/mitchellh/libxev"; 36 | license = licenses.mit; 37 | platforms = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | (import 2 | ( 3 | let 4 | flake-compat = (builtins.fromJSON (builtins.readFile ./flake.lock)).nodes.flake-compat; 5 | in 6 | fetchTarball { 7 | url = "https://github.com/edolstra/flake-compat/archive/${flake-compat.locked.rev}.tar.gz"; 8 | sha256 = flake-compat.locked.narHash; 9 | } 10 | ) 11 | {src = ./.;}) 12 | .shellNix 13 | -------------------------------------------------------------------------------- /src/bench/async1.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const assert = std.debug.assert; 3 | const Allocator = std.mem.Allocator; 4 | const Instant = std.time.Instant; 5 | const xev = @import("xev"); 6 | //const xev = @import("xev").Dynamic; 7 | 8 | pub const std_options: std.Options = .{ 9 | .log_level = .info, 10 | }; 11 | 12 | // Tune-ables 13 | pub const NUM_PINGS = 1000 * 1000; 14 | 15 | pub fn main() !void { 16 | try run(1); 17 | } 18 | 19 | pub fn run(comptime thread_count: comptime_int) !void { 20 | if (comptime xev.dynamic) try xev.detect(); 21 | var loop = try xev.Loop.init(.{}); 22 | defer loop.deinit(); 23 | 24 | // Initialize all our threads 25 | var contexts: [thread_count]Thread = undefined; 26 | var threads: [contexts.len]std.Thread = undefined; 27 | var comps: [contexts.len]xev.Completion = undefined; 28 | for (&contexts, 0..) |*ctx, i| { 29 | ctx.* = try Thread.init(); 30 | ctx.main_async.wait(&loop, &comps[i], Thread, ctx, mainAsyncCallback); 31 | threads[i] = try std.Thread.spawn(.{}, Thread.threadMain, .{ctx}); 32 | } 33 | 34 | const start_time = try Instant.now(); 35 | try loop.run(.until_done); 36 | for (&threads) |thr| thr.join(); 37 | const end_time = try Instant.now(); 38 | 39 | const elapsed = @as(f64, @floatFromInt(end_time.since(start_time))); 40 | std.log.info("async{d}: {d:.2} seconds ({d:.2}/sec)", .{ 41 | thread_count, 42 | elapsed / 1e9, 43 | NUM_PINGS / (elapsed / 1e9), 44 | }); 45 | } 46 | 47 | fn mainAsyncCallback( 48 | ud: ?*Thread, 49 | _: *xev.Loop, 50 | _: *xev.Completion, 51 | r: xev.Async.WaitError!void, 52 | ) xev.CallbackAction { 53 | _ = r catch unreachable; 54 | 55 | const self = ud.?; 56 | self.worker_async.notify() catch unreachable; 57 | self.main_sent += 1; 58 | self.main_seen += 1; 59 | 60 | return if (self.main_sent >= NUM_PINGS) .disarm else .rearm; 61 | } 62 | 63 | /// The thread state 64 | const Thread = struct { 65 | loop: xev.Loop, 66 | worker_async: xev.Async, 67 | main_async: xev.Async, 68 | worker_sent: usize = 0, 69 | worker_seen: usize = 0, 70 | main_sent: usize = 0, 71 | main_seen: usize = 0, 72 | 73 | pub fn init() !Thread { 74 | return .{ 75 | .loop = try xev.Loop.init(.{}), 76 | .worker_async = try xev.Async.init(), 77 | .main_async = try xev.Async.init(), 78 | }; 79 | } 80 | 81 | pub fn threadMain(self: *Thread) !void { 82 | // Kick us off 83 | try self.main_async.notify(); 84 | 85 | // Start our waiter 86 | var c: xev.Completion = undefined; 87 | self.worker_async.wait(&self.loop, &c, Thread, self, asyncCallback); 88 | 89 | // Run 90 | try self.loop.run(.until_done); 91 | if (self.worker_sent < NUM_PINGS) @panic("FAIL"); 92 | } 93 | 94 | fn asyncCallback( 95 | ud: ?*Thread, 96 | _: *xev.Loop, 97 | _: *xev.Completion, 98 | r: xev.Async.WaitError!void, 99 | ) xev.CallbackAction { 100 | _ = r catch unreachable; 101 | const self = ud.?; 102 | self.main_async.notify() catch unreachable; 103 | self.worker_sent += 1; 104 | self.worker_seen += 1; 105 | return if (self.worker_sent >= NUM_PINGS) .disarm else .rearm; 106 | } 107 | }; 108 | -------------------------------------------------------------------------------- /src/bench/async2.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const run = @import("async1.zig").run; 3 | 4 | pub const std_options: std.Options = .{ 5 | .log_level = .info, 6 | }; 7 | 8 | pub fn main() !void { 9 | try run(2); 10 | } 11 | -------------------------------------------------------------------------------- /src/bench/async4.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const run = @import("async1.zig").run; 3 | 4 | pub const std_options: std.Options = .{ 5 | .log_level = .info, 6 | }; 7 | 8 | pub fn main() !void { 9 | try run(4); 10 | } 11 | -------------------------------------------------------------------------------- /src/bench/async8.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const run = @import("async1.zig").run; 3 | 4 | pub const std_options: std.Options = .{ 5 | .log_level = .info, 6 | }; 7 | 8 | pub fn main() !void { 9 | try run(8); 10 | } 11 | -------------------------------------------------------------------------------- /src/bench/async_pummel_1.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const assert = std.debug.assert; 3 | const Allocator = std.mem.Allocator; 4 | const Instant = std.time.Instant; 5 | const xev = @import("xev"); 6 | //const xev = @import("xev").Dynamic; 7 | 8 | pub const std_options: std.Options = .{ 9 | .log_level = .info, 10 | }; 11 | 12 | // Tune-ables 13 | pub const NUM_PINGS = 1000 * 1000; 14 | 15 | pub fn main() !void { 16 | try run(1); 17 | } 18 | 19 | pub fn run(comptime thread_count: comptime_int) !void { 20 | var thread_pool = xev.ThreadPool.init(.{}); 21 | defer thread_pool.deinit(); 22 | defer thread_pool.shutdown(); 23 | 24 | if (xev.dynamic) try xev.detect(); 25 | var loop = try xev.Loop.init(.{ 26 | .entries = std.math.pow(u13, 2, 12), 27 | .thread_pool = &thread_pool, 28 | }); 29 | defer loop.deinit(); 30 | 31 | // Create our async 32 | notifier = try xev.Async.init(); 33 | defer notifier.deinit(); 34 | 35 | const userdata: ?*void = null; 36 | var c: xev.Completion = undefined; 37 | notifier.wait(&loop, &c, void, userdata, &asyncCallback); 38 | 39 | // Initialize all our threads 40 | var threads: [thread_count]std.Thread = undefined; 41 | for (&threads) |*thr| { 42 | thr.* = try std.Thread.spawn(.{}, threadMain, .{}); 43 | } 44 | 45 | const start_time = try Instant.now(); 46 | try loop.run(.until_done); 47 | for (&threads) |thr| thr.join(); 48 | const end_time = try Instant.now(); 49 | 50 | const elapsed = @as(f64, @floatFromInt(end_time.since(start_time))); 51 | std.log.info("async_pummel_{d}: {d} callbacks in {d:.2} seconds ({d:.2}/sec)", .{ 52 | thread_count, 53 | callbacks, 54 | elapsed / 1e9, 55 | @as(f64, @floatFromInt(callbacks)) / (elapsed / 1e9), 56 | }); 57 | } 58 | 59 | var callbacks: usize = 0; 60 | var notifier: xev.Async = undefined; 61 | var state: enum { running, stop, stopped } = .running; 62 | 63 | fn asyncCallback( 64 | _: ?*void, 65 | _: *xev.Loop, 66 | _: *xev.Completion, 67 | r: xev.Async.WaitError!void, 68 | ) xev.CallbackAction { 69 | _ = r catch unreachable; 70 | 71 | callbacks += 1; 72 | if (callbacks < NUM_PINGS) return .rearm; 73 | 74 | // We're done 75 | state = .stop; 76 | while (state != .stopped) std.time.sleep(0); 77 | return .disarm; 78 | } 79 | 80 | fn threadMain() !void { 81 | while (state == .running) try notifier.notify(); 82 | state = .stopped; 83 | } 84 | -------------------------------------------------------------------------------- /src/bench/async_pummel_2.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const run = @import("async_pummel_1.zig").run; 3 | 4 | pub const std_options: std.Options = .{ 5 | .log_level = .info, 6 | }; 7 | 8 | pub fn main() !void { 9 | try run(2); 10 | } 11 | -------------------------------------------------------------------------------- /src/bench/async_pummel_4.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const run = @import("async_pummel_1.zig").run; 3 | 4 | pub const std_options: std.Options = .{ 5 | .log_level = .info, 6 | }; 7 | 8 | pub fn main() !void { 9 | try run(4); 10 | } 11 | -------------------------------------------------------------------------------- /src/bench/async_pummel_8.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const run = @import("async_pummel_1.zig").run; 3 | 4 | pub const std_options: std.Options = .{ 5 | .log_level = .info, 6 | }; 7 | 8 | pub fn main() !void { 9 | try run(8); 10 | } 11 | -------------------------------------------------------------------------------- /src/bench/million-timers.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Instant = std.time.Instant; 3 | const xev = @import("xev"); 4 | //const xev = @import("xev").Dynamic; 5 | 6 | pub const NUM_TIMERS: usize = 10 * 1000 * 1000; 7 | 8 | pub fn main() !void { 9 | var thread_pool = xev.ThreadPool.init(.{}); 10 | defer thread_pool.deinit(); 11 | defer thread_pool.shutdown(); 12 | 13 | if (xev.dynamic) try xev.detect(); 14 | var loop = try xev.Loop.init(.{ 15 | .entries = std.math.pow(u13, 2, 12), 16 | .thread_pool = &thread_pool, 17 | }); 18 | defer loop.deinit(); 19 | 20 | const GPA = std.heap.GeneralPurposeAllocator(.{}); 21 | var gpa: GPA = .{}; 22 | defer _ = gpa.deinit(); 23 | const alloc = gpa.allocator(); 24 | 25 | var cs = try alloc.alloc(xev.Completion, NUM_TIMERS); 26 | defer alloc.free(cs); 27 | 28 | const before_all = try Instant.now(); 29 | var i: usize = 0; 30 | var timeout: u64 = 1; 31 | while (i < NUM_TIMERS) : (i += 1) { 32 | if (i % 1000 == 0) timeout += 1; 33 | const timer = try xev.Timer.init(); 34 | timer.run(&loop, &cs[i], timeout, void, null, timerCallback); 35 | } 36 | 37 | const before_run = try Instant.now(); 38 | try loop.run(.until_done); 39 | const after_run = try Instant.now(); 40 | const after_all = try Instant.now(); 41 | 42 | std.log.info("{d:.2} seconds total", .{@as(f64, @floatFromInt(after_all.since(before_all))) / 1e9}); 43 | std.log.info("{d:.2} seconds init", .{@as(f64, @floatFromInt(before_run.since(before_all))) / 1e9}); 44 | std.log.info("{d:.2} seconds dispatch", .{@as(f64, @floatFromInt(after_run.since(before_run))) / 1e9}); 45 | std.log.info("{d:.2} seconds cleanup", .{@as(f64, @floatFromInt(after_all.since(after_run))) / 1e9}); 46 | } 47 | 48 | pub const std_options: std.Options = .{ 49 | .log_level = .info, 50 | }; 51 | 52 | var timer_callback_count: usize = 0; 53 | 54 | fn timerCallback( 55 | _: ?*void, 56 | _: *xev.Loop, 57 | _: *xev.Completion, 58 | result: xev.Timer.RunError!void, 59 | ) xev.CallbackAction { 60 | _ = result catch unreachable; 61 | timer_callback_count += 1; 62 | return .disarm; 63 | } 64 | -------------------------------------------------------------------------------- /src/bench/ping-pongs.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const assert = std.debug.assert; 3 | const Allocator = std.mem.Allocator; 4 | const Instant = std.time.Instant; 5 | const xev = @import("xev"); 6 | //const xev = @import("xev").Dynamic; 7 | 8 | pub const std_options: std.Options = .{ 9 | .log_level = .info, 10 | }; 11 | 12 | pub fn main() !void { 13 | var thread_pool = xev.ThreadPool.init(.{}); 14 | defer thread_pool.deinit(); 15 | defer thread_pool.shutdown(); 16 | 17 | if (xev.dynamic) try xev.detect(); 18 | var loop = try xev.Loop.init(.{ 19 | .entries = std.math.pow(u13, 2, 12), 20 | .thread_pool = &thread_pool, 21 | }); 22 | defer loop.deinit(); 23 | 24 | const GPA = std.heap.GeneralPurposeAllocator(.{}); 25 | var gpa: GPA = .{}; 26 | defer _ = gpa.deinit(); 27 | const alloc = gpa.allocator(); 28 | 29 | var server_loop = try xev.Loop.init(.{ 30 | .entries = std.math.pow(u13, 2, 12), 31 | .thread_pool = &thread_pool, 32 | }); 33 | defer server_loop.deinit(); 34 | 35 | var server = try Server.init(alloc, &server_loop); 36 | defer server.deinit(); 37 | try server.start(); 38 | 39 | // Start our echo server 40 | const server_thr = try std.Thread.spawn(.{}, Server.threadMain, .{&server}); 41 | 42 | // Start our client 43 | var client_loop = try xev.Loop.init(.{ 44 | .entries = std.math.pow(u13, 2, 12), 45 | .thread_pool = &thread_pool, 46 | }); 47 | defer client_loop.deinit(); 48 | 49 | var client = try Client.init(alloc, &client_loop); 50 | defer client.deinit(); 51 | try client.start(); 52 | 53 | const start_time = try Instant.now(); 54 | try client_loop.run(.until_done); 55 | server_thr.join(); 56 | const end_time = try Instant.now(); 57 | 58 | const elapsed = @as(f64, @floatFromInt(end_time.since(start_time))); 59 | std.log.info("{d:.2} roundtrips/s", .{@as(f64, @floatFromInt(client.pongs)) / (elapsed / 1e9)}); 60 | std.log.info("{d:.2} seconds total", .{elapsed / 1e9}); 61 | } 62 | 63 | /// Memory pools for things that need stable pointers 64 | const BufferPool = std.heap.MemoryPool([4096]u8); 65 | const CompletionPool = std.heap.MemoryPool(xev.Completion); 66 | const TCPPool = std.heap.MemoryPool(xev.TCP); 67 | 68 | /// The client state 69 | const Client = struct { 70 | loop: *xev.Loop, 71 | completion_pool: CompletionPool, 72 | read_buf: [1024]u8, 73 | pongs: u64, 74 | state: usize = 0, 75 | stop: bool = false, 76 | 77 | pub const PING = "PING\n"; 78 | 79 | pub fn init(alloc: Allocator, loop: *xev.Loop) !Client { 80 | return .{ 81 | .loop = loop, 82 | .completion_pool = CompletionPool.init(alloc), 83 | .read_buf = undefined, 84 | .pongs = 0, 85 | .state = 0, 86 | .stop = false, 87 | }; 88 | } 89 | 90 | pub fn deinit(self: *Client) void { 91 | self.completion_pool.deinit(); 92 | } 93 | 94 | /// Must be called with stable self pointer. 95 | pub fn start(self: *Client) !void { 96 | const addr = try std.net.Address.parseIp4("127.0.0.1", 3131); 97 | const socket = try xev.TCP.init(addr); 98 | 99 | const c = try self.completion_pool.create(); 100 | socket.connect(self.loop, c, addr, Client, self, connectCallback); 101 | } 102 | 103 | fn connectCallback( 104 | self_: ?*Client, 105 | l: *xev.Loop, 106 | c: *xev.Completion, 107 | socket: xev.TCP, 108 | r: xev.ConnectError!void, 109 | ) xev.CallbackAction { 110 | _ = r catch unreachable; 111 | 112 | const self = self_.?; 113 | 114 | // Send message 115 | socket.write(l, c, .{ .slice = PING[0..PING.len] }, Client, self, writeCallback); 116 | 117 | // Read 118 | const c_read = self.completion_pool.create() catch unreachable; 119 | socket.read(l, c_read, .{ .slice = &self.read_buf }, Client, self, readCallback); 120 | return .disarm; 121 | } 122 | 123 | fn writeCallback( 124 | self_: ?*Client, 125 | l: *xev.Loop, 126 | c: *xev.Completion, 127 | s: xev.TCP, 128 | b: xev.WriteBuffer, 129 | r: xev.WriteError!usize, 130 | ) xev.CallbackAction { 131 | _ = r catch unreachable; 132 | _ = l; 133 | _ = s; 134 | _ = b; 135 | 136 | // Put back the completion. 137 | self_.?.completion_pool.destroy(c); 138 | return .disarm; 139 | } 140 | 141 | fn readCallback( 142 | self_: ?*Client, 143 | l: *xev.Loop, 144 | c: *xev.Completion, 145 | socket: xev.TCP, 146 | buf: xev.ReadBuffer, 147 | r: xev.ReadError!usize, 148 | ) xev.CallbackAction { 149 | const self = self_.?; 150 | const n = r catch unreachable; 151 | const data = buf.slice[0..n]; 152 | 153 | // Count the number of pings in our message 154 | var i: usize = 0; 155 | while (i < n) : (i += 1) { 156 | assert(data[i] == PING[self.state]); 157 | self.state = (self.state + 1) % (PING.len); 158 | if (self.state == 0) { 159 | self.pongs += 1; 160 | 161 | // If we're done then exit 162 | if (self.pongs > 500_000) { 163 | socket.shutdown(l, c, Client, self, shutdownCallback); 164 | return .disarm; 165 | } 166 | 167 | // Send another ping 168 | const c_ping = self.completion_pool.create() catch unreachable; 169 | socket.write(l, c_ping, .{ .slice = PING[0..PING.len] }, Client, self, writeCallback); 170 | } 171 | } 172 | 173 | // Read again 174 | return .rearm; 175 | } 176 | 177 | fn shutdownCallback( 178 | self_: ?*Client, 179 | l: *xev.Loop, 180 | c: *xev.Completion, 181 | socket: xev.TCP, 182 | r: xev.ShutdownError!void, 183 | ) xev.CallbackAction { 184 | _ = r catch {}; 185 | 186 | const self = self_.?; 187 | socket.close(l, c, Client, self, closeCallback); 188 | return .disarm; 189 | } 190 | 191 | fn closeCallback( 192 | self_: ?*Client, 193 | l: *xev.Loop, 194 | c: *xev.Completion, 195 | socket: xev.TCP, 196 | r: xev.CloseError!void, 197 | ) xev.CallbackAction { 198 | _ = l; 199 | _ = socket; 200 | _ = r catch unreachable; 201 | 202 | const self = self_.?; 203 | self.stop = true; 204 | self.completion_pool.destroy(c); 205 | return .disarm; 206 | } 207 | }; 208 | 209 | /// The server state 210 | const Server = struct { 211 | loop: *xev.Loop, 212 | buffer_pool: BufferPool, 213 | completion_pool: CompletionPool, 214 | socket_pool: TCPPool, 215 | stop: bool, 216 | 217 | pub fn init(alloc: Allocator, loop: *xev.Loop) !Server { 218 | return .{ 219 | .loop = loop, 220 | .buffer_pool = BufferPool.init(alloc), 221 | .completion_pool = CompletionPool.init(alloc), 222 | .socket_pool = TCPPool.init(alloc), 223 | .stop = false, 224 | }; 225 | } 226 | 227 | pub fn deinit(self: *Server) void { 228 | self.buffer_pool.deinit(); 229 | self.completion_pool.deinit(); 230 | self.socket_pool.deinit(); 231 | } 232 | 233 | /// Must be called with stable self pointer. 234 | pub fn start(self: *Server) !void { 235 | const addr = try std.net.Address.parseIp4("127.0.0.1", 3131); 236 | var socket = try xev.TCP.init(addr); 237 | 238 | const c = try self.completion_pool.create(); 239 | try socket.bind(addr); 240 | try socket.listen(std.os.linux.SOMAXCONN); 241 | socket.accept(self.loop, c, Server, self, acceptCallback); 242 | } 243 | 244 | pub fn threadMain(self: *Server) !void { 245 | try self.loop.run(.until_done); 246 | } 247 | 248 | fn destroyBuf(self: *Server, buf: []const u8) void { 249 | self.buffer_pool.destroy( 250 | @alignCast( 251 | @as(*[4096]u8, @ptrFromInt(@intFromPtr(buf.ptr))), 252 | ), 253 | ); 254 | } 255 | 256 | fn acceptCallback( 257 | self_: ?*Server, 258 | l: *xev.Loop, 259 | c: *xev.Completion, 260 | r: xev.AcceptError!xev.TCP, 261 | ) xev.CallbackAction { 262 | const self = self_.?; 263 | 264 | // Create our socket 265 | const socket = self.socket_pool.create() catch unreachable; 266 | socket.* = r catch unreachable; 267 | 268 | // Start reading -- we can reuse c here because its done. 269 | const buf = self.buffer_pool.create() catch unreachable; 270 | socket.read(l, c, .{ .slice = buf }, Server, self, readCallback); 271 | return .disarm; 272 | } 273 | 274 | fn readCallback( 275 | self_: ?*Server, 276 | loop: *xev.Loop, 277 | c: *xev.Completion, 278 | socket: xev.TCP, 279 | buf: xev.ReadBuffer, 280 | r: xev.ReadError!usize, 281 | ) xev.CallbackAction { 282 | const self = self_.?; 283 | const n = r catch |err| switch (err) { 284 | error.EOF => { 285 | self.destroyBuf(buf.slice); 286 | socket.shutdown(loop, c, Server, self, shutdownCallback); 287 | return .disarm; 288 | }, 289 | 290 | else => { 291 | self.destroyBuf(buf.slice); 292 | self.completion_pool.destroy(c); 293 | std.log.warn("server read unexpected err={}", .{err}); 294 | return .disarm; 295 | }, 296 | }; 297 | 298 | // Echo it back 299 | const c_echo = self.completion_pool.create() catch unreachable; 300 | const buf_write = self.buffer_pool.create() catch unreachable; 301 | @memcpy(buf_write, buf.slice[0..n]); 302 | socket.write(loop, c_echo, .{ .slice = buf_write[0..n] }, Server, self, writeCallback); 303 | 304 | // Read again 305 | return .rearm; 306 | } 307 | 308 | fn writeCallback( 309 | self_: ?*Server, 310 | l: *xev.Loop, 311 | c: *xev.Completion, 312 | s: xev.TCP, 313 | buf: xev.WriteBuffer, 314 | r: xev.WriteError!usize, 315 | ) xev.CallbackAction { 316 | _ = l; 317 | _ = s; 318 | _ = r catch unreachable; 319 | 320 | // We do nothing for write, just put back objects into the pool. 321 | const self = self_.?; 322 | self.completion_pool.destroy(c); 323 | self.buffer_pool.destroy( 324 | @alignCast( 325 | @as(*[4096]u8, @ptrFromInt(@intFromPtr(buf.slice.ptr))), 326 | ), 327 | ); 328 | return .disarm; 329 | } 330 | 331 | fn shutdownCallback( 332 | self_: ?*Server, 333 | l: *xev.Loop, 334 | c: *xev.Completion, 335 | s: xev.TCP, 336 | r: xev.ShutdownError!void, 337 | ) xev.CallbackAction { 338 | _ = r catch {}; 339 | 340 | const self = self_.?; 341 | s.close(l, c, Server, self, closeCallback); 342 | return .disarm; 343 | } 344 | 345 | fn closeCallback( 346 | self_: ?*Server, 347 | l: *xev.Loop, 348 | c: *xev.Completion, 349 | socket: xev.TCP, 350 | r: xev.CloseError!void, 351 | ) xev.CallbackAction { 352 | _ = l; 353 | _ = r catch unreachable; 354 | _ = socket; 355 | 356 | const self = self_.?; 357 | self.stop = true; 358 | self.completion_pool.destroy(c); 359 | return .disarm; 360 | } 361 | }; 362 | -------------------------------------------------------------------------------- /src/bench/ping-udp1.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | const assert = std.debug.assert; 4 | const Allocator = std.mem.Allocator; 5 | const Instant = std.time.Instant; 6 | const xev = @import("xev"); 7 | //const xev = @import("xev").Dynamic; 8 | 9 | pub const std_options: std.Options = .{ 10 | .log_level = .info, 11 | }; 12 | 13 | pub fn main() !void { 14 | try run(1); 15 | } 16 | 17 | pub fn run(comptime count: comptime_int) !void { 18 | var thread_pool = xev.ThreadPool.init(.{}); 19 | defer thread_pool.deinit(); 20 | defer thread_pool.shutdown(); 21 | 22 | if (xev.dynamic) try xev.detect(); 23 | var loop = try xev.Loop.init(.{ 24 | .entries = std.math.pow(u13, 2, 12), 25 | .thread_pool = &thread_pool, 26 | }); 27 | defer loop.deinit(); 28 | 29 | const addr = try std.net.Address.parseIp4("127.0.0.1", 3131); 30 | 31 | var pingers: [count]Pinger = undefined; 32 | for (&pingers) |*p| { 33 | p.* = try Pinger.init(addr); 34 | try p.start(&loop); 35 | } 36 | 37 | const start_time = try Instant.now(); 38 | try loop.run(.until_done); 39 | const end_time = try Instant.now(); 40 | 41 | const total: usize = total: { 42 | var total: usize = 0; 43 | for (&pingers) |p| total += p.pongs; 44 | break :total total; 45 | }; 46 | 47 | const elapsed = @as(f64, @floatFromInt(end_time.since(start_time))); 48 | std.log.info("ping_pongs: {d} pingers, ~{d:.0} roundtrips/s", .{ 49 | count, 50 | @as(f64, @floatFromInt(total)) / (elapsed / 1e9), 51 | }); 52 | } 53 | 54 | const Pinger = struct { 55 | udp: xev.UDP, 56 | addr: std.net.Address, 57 | state: usize = 0, 58 | pongs: u64 = 0, 59 | read_buf: [1024]u8 = undefined, 60 | c_read: xev.Completion = undefined, 61 | c_write: xev.Completion = undefined, 62 | state_read: xev.UDP.State = undefined, 63 | state_write: xev.UDP.State = undefined, 64 | op_count: u8 = 0, 65 | 66 | pub const PING = "PING\n"; 67 | 68 | pub fn init(addr: std.net.Address) !Pinger { 69 | return .{ 70 | .udp = try xev.UDP.init(addr), 71 | .state = 0, 72 | .pongs = 0, 73 | .addr = addr, 74 | }; 75 | } 76 | 77 | pub fn start(self: *Pinger, loop: *xev.Loop) !void { 78 | try self.udp.bind(self.addr); 79 | 80 | self.udp.read( 81 | loop, 82 | &self.c_read, 83 | &self.state_read, 84 | .{ .slice = &self.read_buf }, 85 | Pinger, 86 | self, 87 | Pinger.readCallback, 88 | ); 89 | 90 | self.write(loop); 91 | } 92 | 93 | pub fn write(self: *Pinger, loop: *xev.Loop) void { 94 | self.udp.write( 95 | loop, 96 | &self.c_write, 97 | &self.state_write, 98 | self.addr, 99 | .{ .slice = PING[0..PING.len] }, 100 | Pinger, 101 | self, 102 | writeCallback, 103 | ); 104 | } 105 | 106 | pub fn readCallback( 107 | self_: ?*Pinger, 108 | loop: *xev.Loop, 109 | c: *xev.Completion, 110 | _: *xev.UDP.State, 111 | _: std.net.Address, 112 | socket: xev.UDP, 113 | buf: xev.ReadBuffer, 114 | r: xev.ReadError!usize, 115 | ) xev.CallbackAction { 116 | _ = c; 117 | _ = socket; 118 | const self = self_.?; 119 | const n = r catch unreachable; 120 | const data = buf.slice[0..n]; 121 | 122 | var i: usize = 0; 123 | while (i < n) : (i += 1) { 124 | assert(data[i] == PING[self.state]); 125 | self.state = (self.state + 1) % (PING.len); 126 | if (self.state == 0) { 127 | self.pongs += 1; 128 | 129 | // If we're done then exit 130 | if (self.pongs > 500_000) { 131 | self.udp.close(loop, &self.c_read, Pinger, self, closeCallback); 132 | return .disarm; 133 | } 134 | 135 | self.op_count += 1; 136 | if (self.op_count == 2) { 137 | self.op_count = 0; 138 | // Send another ping 139 | self.write(loop); 140 | } 141 | } 142 | } 143 | 144 | return .rearm; 145 | } 146 | 147 | pub fn writeCallback( 148 | self_: ?*Pinger, 149 | loop: *xev.Loop, 150 | _: *xev.Completion, 151 | _: *xev.UDP.State, 152 | _: xev.UDP, 153 | _: xev.WriteBuffer, 154 | r: xev.WriteError!usize, 155 | ) xev.CallbackAction { 156 | const self = self_.?; 157 | 158 | self.op_count += 1; 159 | if (self.op_count == 2) { 160 | self.op_count = 0; 161 | // Send another ping 162 | self.write(loop); 163 | } 164 | 165 | _ = r catch unreachable; 166 | return .disarm; 167 | } 168 | 169 | pub fn closeCallback( 170 | _: ?*Pinger, 171 | _: *xev.Loop, 172 | _: *xev.Completion, 173 | _: xev.UDP, 174 | r: xev.CloseError!void, 175 | ) xev.CallbackAction { 176 | _ = r catch unreachable; 177 | return .disarm; 178 | } 179 | }; 180 | -------------------------------------------------------------------------------- /src/bench/udp_pummel_1v1.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const assert = std.debug.assert; 3 | const Allocator = std.mem.Allocator; 4 | const Instant = std.time.Instant; 5 | const xev = @import("xev"); 6 | //const xev = @import("xev").Dynamic; 7 | 8 | const EXPECTED = "RANG TANG DING DONG I AM THE JAPANESE SANDMAN"; 9 | 10 | /// This is a global var decremented for the test without any locks. That's 11 | /// how the original is written and that's how we're going to do it. 12 | var packet_counter: usize = 1e6; 13 | var send_cb_called: usize = 0; 14 | var recv_cb_called: usize = 0; 15 | 16 | pub const std_options: std.Options = .{ 17 | .log_level = .info, 18 | }; 19 | 20 | pub fn main() !void { 21 | try run(1, 1); 22 | } 23 | 24 | pub fn run(comptime n_senders: comptime_int, comptime n_receivers: comptime_int) !void { 25 | const base_port = 12345; 26 | 27 | var thread_pool = xev.ThreadPool.init(.{}); 28 | defer thread_pool.deinit(); 29 | defer thread_pool.shutdown(); 30 | 31 | if (xev.dynamic) try xev.detect(); 32 | var loop = try xev.Loop.init(.{ 33 | .entries = std.math.pow(u13, 2, 12), 34 | .thread_pool = &thread_pool, 35 | }); 36 | defer loop.deinit(); 37 | 38 | var receivers: [n_receivers]Receiver = undefined; 39 | for (&receivers, 0..) |*r, i| { 40 | const addr = try std.net.Address.parseIp4("127.0.0.1", @as(u16, @intCast(base_port + i))); 41 | r.* = .{ .udp = try xev.UDP.init(addr) }; 42 | try r.udp.bind(addr); 43 | r.udp.read( 44 | &loop, 45 | &r.c_recv, 46 | &r.udp_state, 47 | .{ .slice = &r.recv_buf }, 48 | Receiver, 49 | r, 50 | Receiver.readCallback, 51 | ); 52 | } 53 | 54 | var senders: [n_senders]Sender = undefined; 55 | for (&senders, 0..) |*s, i| { 56 | const addr = try std.net.Address.parseIp4( 57 | "127.0.0.1", 58 | @as(u16, @intCast(base_port + (i % n_receivers))), 59 | ); 60 | s.* = .{ .udp = try xev.UDP.init(addr) }; 61 | s.udp.write( 62 | &loop, 63 | &s.c_send, 64 | &s.udp_state, 65 | addr, 66 | .{ .slice = EXPECTED }, 67 | Sender, 68 | s, 69 | Sender.writeCallback, 70 | ); 71 | } 72 | 73 | const start_time = try Instant.now(); 74 | try loop.run(.until_done); 75 | const end_time = try Instant.now(); 76 | 77 | const elapsed = @as(f64, @floatFromInt(end_time.since(start_time))); 78 | std.log.info("udp_pummel_{d}v{d}: {d:.0}f/s received, {d:.0}f/s sent, {d} received, {d} sent in {d:.1} seconds", .{ 79 | n_senders, 80 | n_receivers, 81 | @as(f64, @floatFromInt(recv_cb_called)) / (elapsed / std.time.ns_per_s), 82 | @as(f64, @floatFromInt(send_cb_called)) / (elapsed / std.time.ns_per_s), 83 | recv_cb_called, 84 | send_cb_called, 85 | elapsed / std.time.ns_per_s, 86 | }); 87 | } 88 | 89 | const Sender = struct { 90 | udp: xev.UDP, 91 | udp_state: xev.UDP.State = undefined, 92 | c_send: xev.Completion = undefined, 93 | 94 | fn writeCallback( 95 | _: ?*Sender, 96 | l: *xev.Loop, 97 | _: *xev.Completion, 98 | _: *xev.UDP.State, 99 | _: xev.UDP, 100 | _: xev.WriteBuffer, 101 | r: xev.WriteError!usize, 102 | ) xev.CallbackAction { 103 | _ = r catch unreachable; 104 | 105 | if (packet_counter == 0) { 106 | l.stop(); 107 | return .disarm; 108 | } 109 | 110 | packet_counter -|= 1; 111 | send_cb_called += 1; 112 | 113 | return .rearm; 114 | } 115 | }; 116 | 117 | const Receiver = struct { 118 | udp: xev.UDP, 119 | udp_state: xev.UDP.State = undefined, 120 | c_recv: xev.Completion = undefined, 121 | recv_buf: [65536]u8 = undefined, 122 | 123 | fn readCallback( 124 | _: ?*Receiver, 125 | _: *xev.Loop, 126 | _: *xev.Completion, 127 | _: *xev.UDP.State, 128 | _: std.net.Address, 129 | _: xev.UDP, 130 | b: xev.ReadBuffer, 131 | r: xev.ReadError!usize, 132 | ) xev.CallbackAction { 133 | const n = r catch |err| { 134 | switch (err) { 135 | error.EOF => {}, 136 | else => std.log.warn("err={}", .{err}), 137 | } 138 | 139 | return .disarm; 140 | }; 141 | 142 | if (!std.mem.eql(u8, b.slice[0..n], EXPECTED)) { 143 | @panic("Unexpected data."); 144 | } 145 | 146 | recv_cb_called += 1; 147 | return .rearm; 148 | } 149 | }; 150 | -------------------------------------------------------------------------------- /src/c_api.zig: -------------------------------------------------------------------------------- 1 | // This file contains the C bindings that are exported when building 2 | // the system libraries. 3 | // 4 | // WHERE IS THE DOCUMENTATION? Note that all the documentation for the C 5 | // interface is in the man pages. The header file xev.h purposely has no 6 | // documentation so that its concise and easy to see the list of exported 7 | // functions. 8 | 9 | const std = @import("std"); 10 | const builtin = @import("builtin"); 11 | const assert = std.debug.assert; 12 | const xev = @import("main.zig"); 13 | 14 | export fn xev_loop_init(loop: *xev.Loop) c_int { 15 | // TODO: overflow 16 | loop.* = xev.Loop.init(.{}) catch |err| return errorCode(err); 17 | return 0; 18 | } 19 | 20 | export fn xev_loop_deinit(loop: *xev.Loop) void { 21 | loop.deinit(); 22 | } 23 | 24 | export fn xev_loop_run(loop: *xev.Loop, mode: xev.RunMode) c_int { 25 | loop.run(mode) catch |err| return errorCode(err); 26 | return 0; 27 | } 28 | 29 | export fn xev_loop_now(loop: *xev.Loop) i64 { 30 | return loop.now(); 31 | } 32 | 33 | export fn xev_loop_update_now(loop: *xev.Loop) void { 34 | loop.update_now(); 35 | } 36 | 37 | export fn xev_completion_zero(c: *xev.Completion) void { 38 | c.* = .{}; 39 | } 40 | 41 | export fn xev_completion_state(c: *xev.Completion) xev.CompletionState { 42 | return c.state(); 43 | } 44 | 45 | //------------------------------------------------------------------- 46 | // ThreadPool 47 | 48 | export fn xev_threadpool_config_init(cfg: *xev.ThreadPool.Config) void { 49 | cfg.* = .{}; 50 | } 51 | 52 | export fn xev_threadpool_config_set_stack_size( 53 | cfg: *xev.ThreadPool.Config, 54 | v: u32, 55 | ) void { 56 | cfg.stack_size = v; 57 | } 58 | 59 | export fn xev_threadpool_config_set_max_threads( 60 | cfg: *xev.ThreadPool.Config, 61 | v: u32, 62 | ) void { 63 | cfg.max_threads = v; 64 | } 65 | 66 | export fn xev_threadpool_init( 67 | threadpool: *xev.ThreadPool, 68 | cfg_: ?*xev.ThreadPool.Config, 69 | ) c_int { 70 | const cfg: xev.ThreadPool.Config = if (cfg_) |v| v.* else .{}; 71 | threadpool.* = xev.ThreadPool.init(cfg); 72 | return 0; 73 | } 74 | 75 | export fn xev_threadpool_deinit(threadpool: *xev.ThreadPool) void { 76 | threadpool.deinit(); 77 | } 78 | 79 | export fn xev_threadpool_shutdown(threadpool: *xev.ThreadPool) void { 80 | threadpool.shutdown(); 81 | } 82 | 83 | export fn xev_threadpool_schedule( 84 | pool: *xev.ThreadPool, 85 | batch: *xev.ThreadPool.Batch, 86 | ) void { 87 | pool.schedule(batch.*); 88 | } 89 | 90 | export fn xev_threadpool_task_init( 91 | t: *xev.ThreadPool.Task, 92 | cb: *const fn (*xev.ThreadPool.Task) callconv(.C) void, 93 | ) void { 94 | const extern_t = @as(*Task, @ptrCast(@alignCast(t))); 95 | extern_t.c_callback = cb; 96 | 97 | t.* = .{ 98 | .callback = (struct { 99 | fn callback(inner_t: *xev.ThreadPool.Task) void { 100 | const outer_t: *Task = @alignCast(@fieldParentPtr( 101 | "data", 102 | @as(*Task.Data, @ptrCast(inner_t)), 103 | )); 104 | outer_t.c_callback(inner_t); 105 | } 106 | }).callback, 107 | }; 108 | } 109 | 110 | export fn xev_threadpool_batch_init(b: *xev.ThreadPool.Batch) void { 111 | b.* = .{}; 112 | } 113 | 114 | export fn xev_threadpool_batch_push_task( 115 | b: *xev.ThreadPool.Batch, 116 | t: *xev.ThreadPool.Task, 117 | ) void { 118 | b.push(xev.ThreadPool.Batch.from(t)); 119 | } 120 | 121 | export fn xev_threadpool_batch_push_batch( 122 | b: *xev.ThreadPool.Batch, 123 | other: *xev.ThreadPool.Batch, 124 | ) void { 125 | b.push(other.*); 126 | } 127 | 128 | //------------------------------------------------------------------- 129 | // Timers 130 | 131 | export fn xev_timer_init(v: *xev.Timer) c_int { 132 | v.* = xev.Timer.init() catch |err| return errorCode(err); 133 | return 0; 134 | } 135 | 136 | export fn xev_timer_deinit(v: *xev.Timer) void { 137 | v.deinit(); 138 | } 139 | 140 | export fn xev_timer_run( 141 | v: *xev.Timer, 142 | loop: *xev.Loop, 143 | c: *xev.Completion, 144 | next_ms: u64, 145 | userdata: ?*anyopaque, 146 | cb: *const fn ( 147 | *xev.Loop, 148 | *xev.Completion, 149 | c_int, 150 | ?*anyopaque, 151 | ) callconv(.C) xev.CallbackAction, 152 | ) void { 153 | const Callback = @typeInfo(@TypeOf(cb)).pointer.child; 154 | const extern_c = @as(*Completion, @ptrCast(@alignCast(c))); 155 | extern_c.c_callback = @as(*const anyopaque, @ptrCast(cb)); 156 | 157 | v.run(loop, c, next_ms, anyopaque, userdata, (struct { 158 | fn callback( 159 | ud: ?*anyopaque, 160 | cb_loop: *xev.Loop, 161 | cb_c: *xev.Completion, 162 | r: xev.Timer.RunError!void, 163 | ) xev.CallbackAction { 164 | const cb_extern_c = @as(*Completion, @ptrCast(cb_c)); 165 | const cb_c_callback = @as( 166 | *const Callback, 167 | @ptrCast(@alignCast(cb_extern_c.c_callback)), 168 | ); 169 | return @call(.auto, cb_c_callback, .{ 170 | cb_loop, 171 | cb_c, 172 | if (r) |_| 0 else |err| errorCode(err), 173 | ud, 174 | }); 175 | } 176 | }).callback); 177 | } 178 | 179 | export fn xev_timer_reset( 180 | v: *xev.Timer, 181 | loop: *xev.Loop, 182 | c: *xev.Completion, 183 | c_cancel: *xev.Completion, 184 | next_ms: u64, 185 | userdata: ?*anyopaque, 186 | cb: *const fn ( 187 | *xev.Loop, 188 | *xev.Completion, 189 | c_int, 190 | ?*anyopaque, 191 | ) callconv(.C) xev.CallbackAction, 192 | ) void { 193 | const Callback = @typeInfo(@TypeOf(cb)).pointer.child; 194 | const extern_c = @as(*Completion, @ptrCast(@alignCast(c))); 195 | extern_c.c_callback = @as(*const anyopaque, @ptrCast(cb)); 196 | 197 | v.reset(loop, c, c_cancel, next_ms, anyopaque, userdata, (struct { 198 | fn callback( 199 | ud: ?*anyopaque, 200 | cb_loop: *xev.Loop, 201 | cb_c: *xev.Completion, 202 | r: xev.Timer.RunError!void, 203 | ) xev.CallbackAction { 204 | const cb_extern_c = @as(*Completion, @ptrCast(cb_c)); 205 | const cb_c_callback = @as( 206 | *const Callback, 207 | @ptrCast(@alignCast(cb_extern_c.c_callback)), 208 | ); 209 | return @call(.auto, cb_c_callback, .{ 210 | cb_loop, 211 | cb_c, 212 | if (r) |_| 0 else |err| errorCode(err), 213 | ud, 214 | }); 215 | } 216 | }).callback); 217 | } 218 | 219 | export fn xev_timer_cancel( 220 | v: *xev.Timer, 221 | loop: *xev.Loop, 222 | c_timer: *xev.Completion, 223 | c_cancel: *xev.Completion, 224 | userdata: ?*anyopaque, 225 | cb: *const fn ( 226 | *xev.Loop, 227 | *xev.Completion, 228 | c_int, 229 | ?*anyopaque, 230 | ) callconv(.C) xev.CallbackAction, 231 | ) void { 232 | const Callback = @typeInfo(@TypeOf(cb)).pointer.child; 233 | const extern_c = @as(*Completion, @ptrCast(@alignCast(c_cancel))); 234 | extern_c.c_callback = @as(*const anyopaque, @ptrCast(cb)); 235 | 236 | v.cancel(loop, c_timer, c_cancel, anyopaque, userdata, (struct { 237 | fn callback( 238 | ud: ?*anyopaque, 239 | cb_loop: *xev.Loop, 240 | cb_c: *xev.Completion, 241 | r: xev.Timer.CancelError!void, 242 | ) xev.CallbackAction { 243 | const cb_extern_c = @as(*Completion, @ptrCast(cb_c)); 244 | const cb_c_callback = @as( 245 | *const Callback, 246 | @ptrCast(@alignCast(cb_extern_c.c_callback)), 247 | ); 248 | return @call(.auto, cb_c_callback, .{ 249 | cb_loop, 250 | cb_c, 251 | if (r) |_| 0 else |err| errorCode(err), 252 | ud, 253 | }); 254 | } 255 | }).callback); 256 | } 257 | 258 | //------------------------------------------------------------------- 259 | // Async 260 | 261 | export fn xev_async_init(v: *xev.Async) c_int { 262 | v.* = xev.Async.init() catch |err| return errorCode(err); 263 | return 0; 264 | } 265 | 266 | export fn xev_async_deinit(v: *xev.Async) void { 267 | v.deinit(); 268 | } 269 | 270 | export fn xev_async_notify(v: *xev.Async) c_int { 271 | v.notify() catch |err| return errorCode(err); 272 | return 0; 273 | } 274 | 275 | export fn xev_async_wait( 276 | v: *xev.Async, 277 | loop: *xev.Loop, 278 | c: *xev.Completion, 279 | userdata: ?*anyopaque, 280 | cb: *const fn ( 281 | *xev.Loop, 282 | *xev.Completion, 283 | c_int, 284 | ?*anyopaque, 285 | ) callconv(.C) xev.CallbackAction, 286 | ) void { 287 | const Callback = @typeInfo(@TypeOf(cb)).pointer.child; 288 | const extern_c = @as(*Completion, @ptrCast(@alignCast(c))); 289 | extern_c.c_callback = @as(*const anyopaque, @ptrCast(cb)); 290 | 291 | v.wait(loop, c, anyopaque, userdata, (struct { 292 | fn callback( 293 | ud: ?*anyopaque, 294 | cb_loop: *xev.Loop, 295 | cb_c: *xev.Completion, 296 | r: xev.Async.WaitError!void, 297 | ) xev.CallbackAction { 298 | const cb_extern_c = @as(*Completion, @ptrCast(cb_c)); 299 | const cb_c_callback = @as( 300 | *const Callback, 301 | @ptrCast(@alignCast(cb_extern_c.c_callback)), 302 | ); 303 | return @call(.auto, cb_c_callback, .{ 304 | cb_loop, 305 | cb_c, 306 | if (r) |_| 0 else |err| errorCode(err), 307 | ud, 308 | }); 309 | } 310 | }).callback); 311 | } 312 | 313 | //------------------------------------------------------------------- 314 | // Sync with xev.h 315 | 316 | /// Since we can't pass the callback at comptime with C, we have to 317 | /// have an additional field on completions to store our callback pointer. 318 | /// We just tack it onto the end of the memory chunk that C programs allocate 319 | /// for completions. 320 | const Completion = extern struct { 321 | const Data = [@sizeOf(xev.Completion)]u8; 322 | data: Data, 323 | c_callback: *const anyopaque, 324 | }; 325 | 326 | const Task = extern struct { 327 | const Data = [@sizeOf(xev.ThreadPool.Task)]u8; 328 | data: Data, 329 | c_callback: *const fn (*xev.ThreadPool.Task) callconv(.C) void, 330 | }; 331 | 332 | /// Returns the unique error code for an error. 333 | fn errorCode(err: anyerror) c_int { 334 | // TODO(mitchellh): This is a bad idea because its not stable across 335 | // code changes. For now we just document that error codes are not 336 | // stable but that is not useful at all! 337 | return @intFromError(err); 338 | } 339 | 340 | test "c-api sizes" { 341 | // This tests the sizes that are defined in the C API. We must ensure 342 | // that our main structure sizes never exceed these so that the C ABI 343 | // is maintained. 344 | // 345 | // THE MAGIC NUMBERS ARE KEPT IN SYNC WITH "include/xev.h" 346 | const testing = std.testing; 347 | try testing.expect(@sizeOf(xev.Loop) <= 512); 348 | try testing.expect(@sizeOf(Completion) <= 320); 349 | try testing.expect(@sizeOf(xev.Async) <= 256); 350 | try testing.expect(@sizeOf(xev.Timer) <= 256); 351 | try testing.expectEqual(@as(usize, 48), @sizeOf(xev.ThreadPool)); 352 | try testing.expectEqual(@as(usize, 24), @sizeOf(xev.ThreadPool.Batch)); 353 | try testing.expectEqual(@as(usize, 24), @sizeOf(Task)); 354 | try testing.expectEqual(@as(usize, 8), @sizeOf(xev.ThreadPool.Config)); 355 | } 356 | -------------------------------------------------------------------------------- /src/darwin.zig: -------------------------------------------------------------------------------- 1 | //! These are copied from Zig's stdlib as of Zig 0.14 because 2 | //! they are no longer exported. This is probably going to be 3 | //! fixed in the future: https://github.com/ziglang/zig/pull/21218 4 | const std = @import("std"); 5 | 6 | pub const MACH_SEND_MSG = 0x00000001; 7 | pub const MACH_RCV_MSG = 0x00000002; 8 | 9 | pub const MACH_SEND_TIMEOUT = 0x00000010; 10 | pub const MACH_SEND_OVERRIDE = 0x00000020; 11 | pub const MACH_SEND_INTERRUPT = 0x00000040; 12 | pub const MACH_SEND_NOTIFY = 0x00000080; 13 | pub const MACH_SEND_ALWAYS = 0x00010000; 14 | pub const MACH_SEND_FILTER_NONFATAL = 0x00010000; 15 | pub const MACH_SEND_TRAILER = 0x00020000; 16 | pub const MACH_SEND_NOIMPORTANCE = 0x00040000; 17 | pub const MACH_SEND_NODENAP = MACH_SEND_NOIMPORTANCE; 18 | pub const MACH_SEND_IMPORTANCE = 0x00080000; 19 | pub const MACH_SEND_SYNC_OVERRIDE = 0x00100000; 20 | pub const MACH_SEND_PROPAGATE_QOS = 0x00200000; 21 | pub const MACH_SEND_SYNC_USE_THRPRI = MACH_SEND_PROPAGATE_QOS; 22 | pub const MACH_SEND_KERNEL = 0x00400000; 23 | pub const MACH_SEND_SYNC_BOOTSTRAP_CHECKIN = 0x00800000; 24 | 25 | pub const MACH_RCV_TIMEOUT = 0x00000100; 26 | pub const MACH_RCV_NOTIFY = 0x00000000; 27 | pub const MACH_RCV_INTERRUPT = 0x00000400; 28 | pub const MACH_RCV_VOUCHER = 0x00000800; 29 | pub const MACH_RCV_OVERWRITE = 0x00000000; 30 | pub const MACH_RCV_GUARDED_DESC = 0x00001000; 31 | pub const MACH_RCV_SYNC_WAIT = 0x00004000; 32 | pub const MACH_RCV_SYNC_PEEK = 0x00008000; 33 | 34 | pub const MACH_PORT_NULL: mach_port_t = 0; 35 | pub const MACH_MSG_TIMEOUT_NONE: mach_msg_timeout_t = 0; 36 | 37 | pub const natural_t = c_uint; 38 | pub const integer_t = c_int; 39 | pub const mach_port_t = c_uint; 40 | pub const mach_port_name_t = natural_t; 41 | pub const mach_msg_bits_t = c_uint; 42 | pub const mach_msg_id_t = integer_t; 43 | pub const mach_msg_type_number_t = natural_t; 44 | pub const mach_msg_type_name_t = c_uint; 45 | pub const mach_msg_option_t = integer_t; 46 | pub const mach_msg_size_t = natural_t; 47 | pub const mach_msg_timeout_t = natural_t; 48 | pub const mach_msg_return_t = std.c.kern_return_t; 49 | 50 | pub const mach_msg_header_t = extern struct { 51 | msgh_bits: mach_msg_bits_t, 52 | msgh_size: mach_msg_size_t, 53 | msgh_remote_port: mach_port_t, 54 | msgh_local_port: mach_port_t, 55 | msgh_voucher_port: mach_port_name_t, 56 | msgh_id: mach_msg_id_t, 57 | }; 58 | 59 | pub extern "c" fn mach_msg( 60 | msg: ?*mach_msg_header_t, 61 | option: mach_msg_option_t, 62 | send_size: mach_msg_size_t, 63 | rcv_size: mach_msg_size_t, 64 | rcv_name: mach_port_name_t, 65 | timeout: mach_msg_timeout_t, 66 | notify: mach_port_name_t, 67 | ) std.c.kern_return_t; 68 | 69 | pub fn getKernError(err: std.c.kern_return_t) KernE { 70 | return @as(KernE, @enumFromInt(@as(u32, @truncate(@as(usize, @intCast(err)))))); 71 | } 72 | 73 | /// Kernel return values 74 | pub const KernE = enum(u32) { 75 | SUCCESS = 0, 76 | 77 | /// Specified address is not currently valid 78 | INVALID_ADDRESS = 1, 79 | 80 | /// Specified memory is valid, but does not permit the 81 | /// required forms of access. 82 | PROTECTION_FAILURE = 2, 83 | 84 | /// The address range specified is already in use, or 85 | /// no address range of the size specified could be 86 | /// found. 87 | NO_SPACE = 3, 88 | 89 | /// The function requested was not applicable to this 90 | /// type of argument, or an argument is invalid 91 | INVALID_ARGUMENT = 4, 92 | 93 | /// The function could not be performed. A catch-all. 94 | FAILURE = 5, 95 | 96 | /// A system resource could not be allocated to fulfill 97 | /// this request. This failure may not be permanent. 98 | RESOURCE_SHORTAGE = 6, 99 | 100 | /// The task in question does not hold receive rights 101 | /// for the port argument. 102 | NOT_RECEIVER = 7, 103 | 104 | /// Bogus access restriction. 105 | NO_ACCESS = 8, 106 | 107 | /// During a page fault, the target address refers to a 108 | /// memory object that has been destroyed. This 109 | /// failure is permanent. 110 | MEMORY_FAILURE = 9, 111 | 112 | /// During a page fault, the memory object indicated 113 | /// that the data could not be returned. This failure 114 | /// may be temporary; future attempts to access this 115 | /// same data may succeed, as defined by the memory 116 | /// object. 117 | MEMORY_ERROR = 10, 118 | 119 | /// The receive right is already a member of the portset. 120 | ALREADY_IN_SET = 11, 121 | 122 | /// The receive right is not a member of a port set. 123 | NOT_IN_SET = 12, 124 | 125 | /// The name already denotes a right in the task. 126 | NAME_EXISTS = 13, 127 | 128 | /// The operation was aborted. Ipc code will 129 | /// catch this and reflect it as a message error. 130 | ABORTED = 14, 131 | 132 | /// The name doesn't denote a right in the task. 133 | INVALID_NAME = 15, 134 | 135 | /// Target task isn't an active task. 136 | INVALID_TASK = 16, 137 | 138 | /// The name denotes a right, but not an appropriate right. 139 | INVALID_RIGHT = 17, 140 | 141 | /// A blatant range error. 142 | INVALID_VALUE = 18, 143 | 144 | /// Operation would overflow limit on user-references. 145 | UREFS_OVERFLOW = 19, 146 | 147 | /// The supplied (port) capability is improper. 148 | INVALID_CAPABILITY = 20, 149 | 150 | /// The task already has send or receive rights 151 | /// for the port under another name. 152 | RIGHT_EXISTS = 21, 153 | 154 | /// Target host isn't actually a host. 155 | INVALID_HOST = 22, 156 | 157 | /// An attempt was made to supply "precious" data 158 | /// for memory that is already present in a 159 | /// memory object. 160 | MEMORY_PRESENT = 23, 161 | 162 | /// A page was requested of a memory manager via 163 | /// memory_object_data_request for an object using 164 | /// a MEMORY_OBJECT_COPY_CALL strategy, with the 165 | /// VM_PROT_WANTS_COPY flag being used to specify 166 | /// that the page desired is for a copy of the 167 | /// object, and the memory manager has detected 168 | /// the page was pushed into a copy of the object 169 | /// while the kernel was walking the shadow chain 170 | /// from the copy to the object. This error code 171 | /// is delivered via memory_object_data_error 172 | /// and is handled by the kernel (it forces the 173 | /// kernel to restart the fault). It will not be 174 | /// seen by users. 175 | MEMORY_DATA_MOVED = 24, 176 | 177 | /// A strategic copy was attempted of an object 178 | /// upon which a quicker copy is now possible. 179 | /// The caller should retry the copy using 180 | /// vm_object_copy_quickly. This error code 181 | /// is seen only by the kernel. 182 | MEMORY_RESTART_COPY = 25, 183 | 184 | /// An argument applied to assert processor set privilege 185 | /// was not a processor set control port. 186 | INVALID_PROCESSOR_SET = 26, 187 | 188 | /// The specified scheduling attributes exceed the thread's 189 | /// limits. 190 | POLICY_LIMIT = 27, 191 | 192 | /// The specified scheduling policy is not currently 193 | /// enabled for the processor set. 194 | INVALID_POLICY = 28, 195 | 196 | /// The external memory manager failed to initialize the 197 | /// memory object. 198 | INVALID_OBJECT = 29, 199 | 200 | /// A thread is attempting to wait for an event for which 201 | /// there is already a waiting thread. 202 | ALREADY_WAITING = 30, 203 | 204 | /// An attempt was made to destroy the default processor 205 | /// set. 206 | DEFAULT_SET = 31, 207 | 208 | /// An attempt was made to fetch an exception port that is 209 | /// protected, or to abort a thread while processing a 210 | /// protected exception. 211 | EXCEPTION_PROTECTED = 32, 212 | 213 | /// A ledger was required but not supplied. 214 | INVALID_LEDGER = 33, 215 | 216 | /// The port was not a memory cache control port. 217 | INVALID_MEMORY_CONTROL = 34, 218 | 219 | /// An argument supplied to assert security privilege 220 | /// was not a host security port. 221 | INVALID_SECURITY = 35, 222 | 223 | /// thread_depress_abort was called on a thread which 224 | /// was not currently depressed. 225 | NOT_DEPRESSED = 36, 226 | 227 | /// Object has been terminated and is no longer available 228 | TERMINATED = 37, 229 | 230 | /// Lock set has been destroyed and is no longer available. 231 | LOCK_SET_DESTROYED = 38, 232 | 233 | /// The thread holding the lock terminated before releasing 234 | /// the lock 235 | LOCK_UNSTABLE = 39, 236 | 237 | /// The lock is already owned by another thread 238 | LOCK_OWNED = 40, 239 | 240 | /// The lock is already owned by the calling thread 241 | LOCK_OWNED_SELF = 41, 242 | 243 | /// Semaphore has been destroyed and is no longer available. 244 | SEMAPHORE_DESTROYED = 42, 245 | 246 | /// Return from RPC indicating the target server was 247 | /// terminated before it successfully replied 248 | RPC_SERVER_TERMINATED = 43, 249 | 250 | /// Terminate an orphaned activation. 251 | RPC_TERMINATE_ORPHAN = 44, 252 | 253 | /// Allow an orphaned activation to continue executing. 254 | RPC_CONTINUE_ORPHAN = 45, 255 | 256 | /// Empty thread activation (No thread linked to it) 257 | NOT_SUPPORTED = 46, 258 | 259 | /// Remote node down or inaccessible. 260 | NODE_DOWN = 47, 261 | 262 | /// A signalled thread was not actually waiting. 263 | NOT_WAITING = 48, 264 | 265 | /// Some thread-oriented operation (semaphore_wait) timed out 266 | OPERATION_TIMED_OUT = 49, 267 | 268 | /// During a page fault, indicates that the page was rejected 269 | /// as a result of a signature check. 270 | CODESIGN_ERROR = 50, 271 | 272 | /// The requested property cannot be changed at this time. 273 | POLICY_STATIC = 51, 274 | 275 | /// The provided buffer is of insufficient size for the requested data. 276 | INSUFFICIENT_BUFFER_SIZE = 52, 277 | 278 | /// Denied by security policy 279 | DENIED = 53, 280 | 281 | /// The KC on which the function is operating is missing 282 | MISSING_KC = 54, 283 | 284 | /// The KC on which the function is operating is invalid 285 | INVALID_KC = 55, 286 | 287 | /// A search or query operation did not return a result 288 | NOT_FOUND = 56, 289 | 290 | _, 291 | }; 292 | 293 | pub fn getMachMsgError(err: mach_msg_return_t) MachMsgE { 294 | return @as(MachMsgE, @enumFromInt(@as(u32, @truncate(@as(usize, @intCast(err)))))); 295 | } 296 | 297 | /// Mach msg return values 298 | pub const MachMsgE = enum(u32) { 299 | SUCCESS = 0x00000000, 300 | 301 | /// Thread is waiting to send. (Internal use only.) 302 | SEND_IN_PROGRESS = 0x10000001, 303 | /// Bogus in-line data. 304 | SEND_INVALID_DATA = 0x10000002, 305 | /// Bogus destination port. 306 | SEND_INVALID_DEST = 0x10000003, 307 | /// Message not sent before timeout expired. 308 | SEND_TIMED_OUT = 0x10000004, 309 | /// Bogus voucher port. 310 | SEND_INVALID_VOUCHER = 0x10000005, 311 | /// Software interrupt. 312 | SEND_INTERRUPTED = 0x10000007, 313 | /// Data doesn't contain a complete message. 314 | SEND_MSG_TOO_SMALL = 0x10000008, 315 | /// Bogus reply port. 316 | SEND_INVALID_REPLY = 0x10000009, 317 | /// Bogus port rights in the message body. 318 | SEND_INVALID_RIGHT = 0x1000000a, 319 | /// Bogus notify port argument. 320 | SEND_INVALID_NOTIFY = 0x1000000b, 321 | /// Invalid out-of-line memory pointer. 322 | SEND_INVALID_MEMORY = 0x1000000c, 323 | /// No message buffer is available. 324 | SEND_NO_BUFFER = 0x1000000d, 325 | /// Send is too large for port 326 | SEND_TOO_LARGE = 0x1000000e, 327 | /// Invalid msg-type specification. 328 | SEND_INVALID_TYPE = 0x1000000f, 329 | /// A field in the header had a bad value. 330 | SEND_INVALID_HEADER = 0x10000010, 331 | /// The trailer to be sent does not match kernel format. 332 | SEND_INVALID_TRAILER = 0x10000011, 333 | /// The sending thread context did not match the context on the dest port 334 | SEND_INVALID_CONTEXT = 0x10000012, 335 | /// compatibility: no longer a returned error 336 | SEND_INVALID_RT_OOL_SIZE = 0x10000015, 337 | /// The destination port doesn't accept ports in body 338 | SEND_NO_GRANT_DEST = 0x10000016, 339 | /// Message send was rejected by message filter 340 | SEND_MSG_FILTERED = 0x10000017, 341 | 342 | /// Thread is waiting for receive. (Internal use only.) 343 | RCV_IN_PROGRESS = 0x10004001, 344 | /// Bogus name for receive port/port-set. 345 | RCV_INVALID_NAME = 0x10004002, 346 | /// Didn't get a message within the timeout value. 347 | RCV_TIMED_OUT = 0x10004003, 348 | /// Message buffer is not large enough for inline data. 349 | RCV_TOO_LARGE = 0x10004004, 350 | /// Software interrupt. 351 | RCV_INTERRUPTED = 0x10004005, 352 | /// compatibility: no longer a returned error 353 | RCV_PORT_CHANGED = 0x10004006, 354 | /// Bogus notify port argument. 355 | RCV_INVALID_NOTIFY = 0x10004007, 356 | /// Bogus message buffer for inline data. 357 | RCV_INVALID_DATA = 0x10004008, 358 | /// Port/set was sent away/died during receive. 359 | RCV_PORT_DIED = 0x10004009, 360 | /// compatibility: no longer a returned error 361 | RCV_IN_SET = 0x1000400a, 362 | /// Error receiving message header. See special bits. 363 | RCV_HEADER_ERROR = 0x1000400b, 364 | /// Error receiving message body. See special bits. 365 | RCV_BODY_ERROR = 0x1000400c, 366 | /// Invalid msg-type specification in scatter list. 367 | RCV_INVALID_TYPE = 0x1000400d, 368 | /// Out-of-line overwrite region is not large enough 369 | RCV_SCATTER_SMALL = 0x1000400e, 370 | /// trailer type or number of trailer elements not supported 371 | RCV_INVALID_TRAILER = 0x1000400f, 372 | /// Waiting for receive with timeout. (Internal use only.) 373 | RCV_IN_PROGRESS_TIMED = 0x10004011, 374 | /// invalid reply port used in a STRICT_REPLY message 375 | RCV_INVALID_REPLY = 0x10004012, 376 | }; 377 | -------------------------------------------------------------------------------- /src/debug.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | inline fn indent(depth: usize, writer: anytype) !void { 4 | for (0..depth) |_| try writer.writeByte(' '); 5 | } 6 | 7 | pub fn describe(comptime T: type, writer: anytype, depth: usize) !void { 8 | const type_info = @typeInfo(T); 9 | switch (type_info) { 10 | .Type, 11 | .Void, 12 | .Bool, 13 | .NoReturn, 14 | .Int, 15 | .Float, 16 | .Pointer, 17 | .Array, 18 | .ComptimeFloat, 19 | .ComptimeInt, 20 | .Undefined, 21 | .Null, 22 | .Optional, 23 | .ErrorUnion, 24 | .ErrorSet, 25 | .Enum, 26 | .Fn, 27 | .Opaque, 28 | .Frame, 29 | .AnyFrame, 30 | .Vector, 31 | .EnumLiteral, 32 | => { 33 | try writer.print("{s} ({d} bytes)", .{ @typeName(T), @sizeOf(T) }); 34 | }, 35 | .Union => |s| { 36 | try writer.print("{s} ({d} bytes) {{\n", .{ @typeName(T), @sizeOf(T) }); 37 | inline for (s.fields) |f| { 38 | try indent(depth + 4, writer); 39 | try writer.print("{s}: ", .{f.name}); 40 | try describe(f.type, writer, depth + 4); 41 | try writer.writeByte('\n'); 42 | } 43 | try indent(depth, writer); 44 | try writer.writeByte('}'); 45 | }, 46 | .Struct => |s| { 47 | try writer.print("{s} ({d} bytes) {{\n", .{ @typeName(T), @sizeOf(T) }); 48 | inline for (s.fields) |f| { 49 | try indent(depth + 4, writer); 50 | try writer.print("{s}: ", .{f.name}); 51 | try describe(f.type, writer, depth + 4); 52 | try writer.writeByte('\n'); 53 | } 54 | try indent(depth, writer); 55 | try writer.writeByte('}'); 56 | }, 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/dynamic.zig: -------------------------------------------------------------------------------- 1 | const dynamicpkg = @This(); 2 | const std = @import("std"); 3 | const builtin = @import("builtin"); 4 | const posix = std.posix; 5 | const main = @import("main.zig"); 6 | const AllBackend = main.Backend; 7 | const looppkg = @import("loop.zig"); 8 | 9 | /// Creates the Xev API based on a set of backend types that allows 10 | /// for dynamic choice between the various candidate backends (assuming 11 | /// they're available). For example, on Linux, this allows a consumer to 12 | /// choose between io_uring and epoll, or for automatic fallback to epoll 13 | /// to occur. 14 | /// 15 | /// If only one backend is given, the returned type is equivalent to 16 | /// the static API with no runtime conditional logic, so there is no downside 17 | /// to always using the dynamic choice variant if you wish to support 18 | /// dynamic choice. 19 | /// 20 | /// The goal of this API is to match the static Xev() (in main.zig) 21 | /// API as closely as possible. It can't be exact since this is an abstraction 22 | /// and therefore must be limited to the least common denominator of all 23 | /// backends. 24 | /// 25 | /// Since the static Xev API exposes types that can be initialized on their 26 | /// own (i.e. xev.Async.init()), this API does the same. Since we need to 27 | /// know the backend to use, this uses a global variable. The backend to use 28 | /// defaults to the first candidate backend and DOES NOT test that it is 29 | /// available. The API user should call `detect()` to set the backend to 30 | /// the first available backend. 31 | /// 32 | /// Additionally, a caller can manually set the backend to use, but this 33 | /// must be done before initializing any other types. 34 | pub fn Xev(comptime bes: []const AllBackend) type { 35 | if (bes.len == 0) @compileError("no backends provided"); 36 | 37 | // Ensure that if we only have one candidate that we have no 38 | // overhead to use the dynamic API. 39 | if (bes.len == 1) return bes[0].Api(); 40 | 41 | return struct { 42 | const Dynamic = @This(); 43 | 44 | /// This is a flag that can be used to detect that you're using 45 | /// a dynamic API and not the static API via `hasDecl`. 46 | pub const dynamic = true; 47 | 48 | pub const candidates = bes; 49 | 50 | /// Backend becomes the subset of the full xev.Backend that 51 | /// is available to this dynamic API. 52 | pub const Backend = EnumSubset(AllBackend, bes); 53 | 54 | /// Forward some global types so that users can replace 55 | /// @import("xev") with the dynamic package and everything 56 | /// works. 57 | pub const ThreadPool = main.ThreadPool; 58 | 59 | /// The shared structures 60 | pub const Options = looppkg.Options; 61 | pub const RunMode = looppkg.RunMode; 62 | pub const CallbackAction = looppkg.CallbackAction; 63 | pub const CompletionState = looppkg.CompletionState; 64 | pub const Completion = DynamicCompletion(Dynamic); 65 | pub const PollEvent = DynamicPollEvent(Dynamic); 66 | pub const ReadBuffer = DynamicReadBuffer(Dynamic); 67 | pub const WriteBuffer = DynamicWriteBuffer(Dynamic); 68 | pub const WriteQueue = DynamicWriteQueue(Dynamic); 69 | pub const WriteRequest = Dynamic.Union(&.{"WriteRequest"}); 70 | 71 | /// Error types 72 | pub const AcceptError = Dynamic.ErrorSet(&.{"AcceptError"}); 73 | pub const CancelError = Dynamic.ErrorSet(&.{"CancelError"}); 74 | pub const CloseError = Dynamic.ErrorSet(&.{"CloseError"}); 75 | pub const ConnectError = Dynamic.ErrorSet(&.{"ConnectError"}); 76 | pub const PollError = Dynamic.ErrorSet(&.{"PollError"}); 77 | pub const ShutdownError = Dynamic.ErrorSet(&.{"ShutdownError"}); 78 | pub const WriteError = Dynamic.ErrorSet(&.{"WriteError"}); 79 | pub const ReadError = Dynamic.ErrorSet(&.{"ReadError"}); 80 | 81 | /// Core types 82 | pub const Async = @import("watcher/async.zig").Async(Dynamic); 83 | pub const File = @import("watcher/file.zig").File(Dynamic); 84 | pub const Process = @import("watcher/process.zig").Process(Dynamic); 85 | pub const Stream = @import("watcher/stream.zig").GenericStream(Dynamic); 86 | pub const Timer = @import("watcher/timer.zig").Timer(Dynamic); 87 | pub const TCP = @import("watcher/tcp.zig").TCP(Dynamic); 88 | pub const UDP = @import("watcher/udp.zig").UDP(Dynamic); 89 | 90 | /// The backend that is in use. 91 | pub var backend: Backend = subset(bes[bes.len - 1]); 92 | 93 | /// Detect the preferred backend based on the system and set it 94 | /// for use. "Preferred" is the first available (API exists) candidate 95 | /// backend in the list. 96 | pub fn detect() error{NoAvailableBackends}!void { 97 | inline for (bes) |be| { 98 | if (be.Api().available()) { 99 | backend = subset(be); 100 | return; 101 | } 102 | } 103 | 104 | return error.NoAvailableBackends; 105 | } 106 | 107 | /// Manually set the backend to use, but if the backend is not 108 | /// available, this will not change the backend in use. 109 | pub fn prefer(be: AllBackend) bool { 110 | inline for (bes) |candidate| { 111 | if (candidate == be) { 112 | const api = candidate.Api(); 113 | if (!api.available()) return false; 114 | backend = subset(candidate); 115 | return true; 116 | } 117 | } 118 | 119 | return false; 120 | } 121 | 122 | pub const Loop = struct { 123 | backend: Loop.Union, 124 | 125 | pub const Union = Dynamic.Union(&.{"Loop"}); 126 | 127 | pub fn init(opts: Options) !Loop { 128 | return .{ .backend = switch (backend) { 129 | inline else => |tag| backend: { 130 | const api = (comptime superset(tag)).Api(); 131 | break :backend @unionInit( 132 | Loop.Union, 133 | @tagName(tag), 134 | try api.Loop.init(opts), 135 | ); 136 | }, 137 | } }; 138 | } 139 | 140 | pub fn deinit(self: *Loop) void { 141 | switch (backend) { 142 | inline else => |tag| @field( 143 | self.backend, 144 | @tagName(tag), 145 | ).deinit(), 146 | } 147 | } 148 | 149 | pub fn stop(self: *Loop) void { 150 | switch (backend) { 151 | inline else => |tag| @field( 152 | self.backend, 153 | @tagName(tag), 154 | ).stop(), 155 | } 156 | } 157 | 158 | pub fn stopped(self: *Loop) bool { 159 | return switch (backend) { 160 | inline else => |tag| @field( 161 | self.backend, 162 | @tagName(tag), 163 | ).stopped(), 164 | }; 165 | } 166 | 167 | pub fn run(self: *Loop, mode: RunMode) !void { 168 | switch (backend) { 169 | inline else => |tag| try @field( 170 | self.backend, 171 | @tagName(tag), 172 | ).run(mode), 173 | } 174 | } 175 | }; 176 | 177 | /// Helpers to convert between the subset/superset of backends. 178 | pub fn subset(comptime be: AllBackend) Backend { 179 | return @enumFromInt(@intFromEnum(be)); 180 | } 181 | 182 | pub fn superset(comptime be: Backend) AllBackend { 183 | return @enumFromInt(@intFromEnum(be)); 184 | } 185 | 186 | pub fn Union(comptime field: []const []const u8) type { 187 | return dynamicpkg.Union(bes, field, false); 188 | } 189 | 190 | pub fn TaggedUnion(comptime field: []const []const u8) type { 191 | return dynamicpkg.Union(bes, field, true); 192 | } 193 | 194 | pub fn ErrorSet(comptime field: []const []const u8) type { 195 | return dynamicpkg.ErrorSet(bes, field); 196 | } 197 | 198 | test { 199 | @import("std").testing.refAllDecls(@This()); 200 | } 201 | 202 | test "completion is zero-able" { 203 | const c: Completion = .{}; 204 | _ = c; 205 | } 206 | 207 | test "detect" { 208 | const testing = std.testing; 209 | try detect(); 210 | inline for (bes) |be| { 211 | if (@intFromEnum(be) == @intFromEnum(backend)) { 212 | try testing.expect(be.Api().available()); 213 | break; 214 | } 215 | } else try testing.expect(false); 216 | } 217 | 218 | test "prefer" { 219 | const testing = std.testing; 220 | try testing.expect(prefer(bes[0])); 221 | inline for (bes) |be| { 222 | if (@intFromEnum(be) == @intFromEnum(backend)) { 223 | try testing.expect(be.Api().available()); 224 | break; 225 | } 226 | } else try testing.expect(false); 227 | } 228 | 229 | test "loop basics" { 230 | try detect(); 231 | var l = try Loop.init(.{}); 232 | defer l.deinit(); 233 | try l.run(.until_done); 234 | l.stop(); 235 | try std.testing.expect(l.stopped()); 236 | } 237 | }; 238 | } 239 | 240 | fn DynamicCompletion(comptime dynamic: type) type { 241 | return struct { 242 | const Self = @This(); 243 | 244 | // Completions, unlike almost any other dynamic type in this 245 | // file, are tagged unions. This adds a minimal overhead to 246 | // the completion but is necessary to ensure that we have 247 | // zero-initialized the correct type for where it matters 248 | // (timers). 249 | pub const Union = dynamic.TaggedUnion(&.{"Completion"}); 250 | 251 | value: Self.Union = @unionInit( 252 | Self.Union, 253 | @tagName(dynamic.candidates[dynamic.candidates.len - 1]), 254 | .{}, 255 | ), 256 | 257 | pub fn init() Self { 258 | return .{ .value = switch (dynamic.backend) { 259 | inline else => |tag| value: { 260 | const api = (comptime dynamic.superset(tag)).Api(); 261 | break :value @unionInit( 262 | Self.Union, 263 | @tagName(tag), 264 | api.Completion.init(), 265 | ); 266 | }, 267 | } }; 268 | } 269 | 270 | pub fn state(self: *Self) dynamic.CompletionState { 271 | return switch (self.value) { 272 | inline else => |*v| v.state(), 273 | }; 274 | } 275 | 276 | /// This ensures that the tag is currently set for this 277 | /// completion. If it is not, it will be set to the given 278 | /// tag with a zero-initialized value. 279 | /// 280 | /// This lets users zero-intialize the completion and then 281 | /// call any watcher API on it without having to worry about 282 | /// the correct detected backend tag being set. 283 | pub fn ensureTag(self: *Self, comptime tag: dynamic.Backend) void { 284 | if (self.value == tag) return; 285 | self.value = @unionInit( 286 | Self.Union, 287 | @tagName(tag), 288 | .{}, 289 | ); 290 | } 291 | }; 292 | } 293 | 294 | fn DynamicReadBuffer(comptime dynamic: type) type { 295 | // Our read buffer supports a least common denominator of 296 | // read targets available by all backends. The direct backend 297 | // may support more read targets but if you need that you should 298 | // use the backend directly and not through the dynamic API. 299 | return union(enum) { 300 | const Self = @This(); 301 | 302 | slice: []u8, 303 | array: [32]u8, 304 | 305 | /// Convert a backend-specific read buffer to this. 306 | pub fn fromBackend( 307 | comptime be: dynamic.Backend, 308 | buf: dynamic.superset(be).Api().ReadBuffer, 309 | ) Self { 310 | return switch (buf) { 311 | inline else => |data, tag| @unionInit( 312 | Self, 313 | @tagName(tag), 314 | data, 315 | ), 316 | }; 317 | } 318 | 319 | /// Convert this read buffer to the backend-specific 320 | /// buffer. 321 | pub fn toBackend( 322 | self: Self, 323 | comptime be: dynamic.Backend, 324 | ) dynamic.superset(be).Api().ReadBuffer { 325 | return switch (self) { 326 | inline else => |data, tag| @unionInit( 327 | dynamic.superset(be).Api().ReadBuffer, 328 | @tagName(tag), 329 | data, 330 | ), 331 | }; 332 | } 333 | }; 334 | } 335 | 336 | fn DynamicWriteBuffer(comptime dynamic: type) type { 337 | // Our write buffer supports a least common denominator of 338 | // write targets available by all backends. The direct backend 339 | // may support more write targets but if you need that you should 340 | // use the backend directly and not through the dynamic API. 341 | return union(enum) { 342 | const Self = @This(); 343 | 344 | slice: []const u8, 345 | array: struct { array: [32]u8, len: usize }, 346 | 347 | /// Convert a backend-specific write buffer to this. 348 | pub fn fromBackend( 349 | comptime be: dynamic.Backend, 350 | buf: dynamic.superset(be).Api().WriteBuffer, 351 | ) Self { 352 | return switch (buf) { 353 | .slice => |v| .{ .slice = v }, 354 | .array => |v| .{ .array = .{ .array = v.array, .len = v.len } }, 355 | }; 356 | } 357 | 358 | /// Convert this write buffer to the backend-specific 359 | /// buffer. 360 | pub fn toBackend( 361 | self: Self, 362 | comptime be: dynamic.Backend, 363 | ) dynamic.superset(be).Api().WriteBuffer { 364 | return switch (self) { 365 | .slice => |v| .{ .slice = v }, 366 | .array => |v| .{ .array = .{ .array = v.array, .len = v.len } }, 367 | }; 368 | } 369 | }; 370 | } 371 | 372 | fn DynamicWriteQueue(comptime xev: type) type { 373 | return struct { 374 | const Self = @This(); 375 | pub const Union = xev.TaggedUnion(&.{"WriteQueue"}); 376 | 377 | value: Self.Union = @unionInit( 378 | Self.Union, 379 | @tagName(xev.candidates[xev.candidates.len - 1]), 380 | .{}, 381 | ), 382 | 383 | pub fn ensureTag(self: *Self, comptime tag: xev.Backend) void { 384 | if (self.value == tag) return; 385 | self.value = @unionInit( 386 | Self.Union, 387 | @tagName(tag), 388 | .{}, 389 | ); 390 | } 391 | }; 392 | } 393 | 394 | fn DynamicPollEvent(comptime xev: type) type { 395 | return enum { 396 | const Self = @This(); 397 | 398 | read, 399 | 400 | pub fn fromBackend( 401 | comptime tag: xev.Backend, 402 | event: xev.superset(tag).Api().PollEvent, 403 | ) Self { 404 | return switch (event) { 405 | inline else => |event_tag| @field( 406 | Self, 407 | @tagName(event_tag), 408 | ), 409 | }; 410 | } 411 | 412 | pub fn toBackend( 413 | self: Self, 414 | comptime tag: xev.Backend, 415 | ) xev.superset(tag).Api().PollEvent { 416 | return switch (self) { 417 | inline else => |event_tag| @field( 418 | (comptime xev.superset(tag)).Api().PollEvent, 419 | @tagName(event_tag), 420 | ), 421 | }; 422 | } 423 | }; 424 | } 425 | 426 | /// Create an exhaustive enum that is a subset of another enum. 427 | /// Preserves the same backing type and integer values for the 428 | /// subset making it easy to convert between the two. 429 | fn EnumSubset(comptime T: type, comptime values: []const T) type { 430 | var fields: [values.len]std.builtin.Type.EnumField = undefined; 431 | for (values, 0..) |value, i| fields[i] = .{ 432 | .name = @tagName(value), 433 | .value = @intFromEnum(value), 434 | }; 435 | 436 | return @Type(.{ .@"enum" = .{ 437 | .tag_type = @typeInfo(T).@"enum".tag_type, 438 | .fields = &fields, 439 | .decls = &.{}, 440 | .is_exhaustive = true, 441 | } }); 442 | } 443 | 444 | /// Creates a union type that can hold the implementation of a given 445 | /// backend by common field name. Example, for Async: Union(bes, "Async") 446 | /// produces: 447 | /// 448 | /// union { 449 | /// io_uring: IO_Uring.Async, 450 | /// epoll: Epoll.Async, 451 | /// ... 452 | /// } 453 | /// 454 | /// The union is untagged to save an extra bit of memory per 455 | /// instance since we have the active backend from the outer struct. 456 | fn Union( 457 | comptime bes: []const AllBackend, 458 | comptime field: []const []const u8, 459 | comptime tagged: bool, 460 | ) type { 461 | // Keep track of the largest field size, see the conditional 462 | // below using this variable for more info. 463 | var largest: usize = 0; 464 | 465 | var fields: [bes.len + 1]std.builtin.Type.UnionField = undefined; 466 | for (bes, 0..) |be, i| { 467 | var T: type = be.Api(); 468 | for (field) |f| T = @field(T, f); 469 | largest = @max(largest, @sizeOf(T)); 470 | fields[i] = .{ 471 | .name = @tagName(be), 472 | .type = T, 473 | .alignment = @alignOf(T), 474 | }; 475 | } 476 | 477 | // If our union only has zero-sized types, we need to add some 478 | // non-zero sized padding entry. This avoids a Zig 0.13 compiler 479 | // crash when trying to create a zero-sized union using @unionInit 480 | // from a switch of a comptime-generated enum. I wasn't able to 481 | // minimize this. In future Zig versions we can remove this and if 482 | // our examples can build with Dynamic then we're good. 483 | var count: usize = bes.len; 484 | if (largest == 0) { 485 | fields[count] = .{ 486 | .name = "_zig_bug_padding", 487 | .type = u8, 488 | .alignment = @alignOf(u8), 489 | }; 490 | count += 1; 491 | } 492 | 493 | return @Type(.{ 494 | .@"union" = .{ 495 | .layout = .auto, 496 | .tag_type = if (tagged) EnumSubset( 497 | AllBackend, 498 | bes, 499 | ) else null, 500 | .fields = fields[0..count], 501 | .decls = &.{}, 502 | }, 503 | }); 504 | } 505 | 506 | /// Create a new error set from a list of error sets within 507 | /// the given backends at the given field name. For example, 508 | /// to merge all xev.Async.WaitErrors: 509 | /// 510 | /// ErrorSet(bes, &.{"Async", "WaitError"}); 511 | /// 512 | fn ErrorSet( 513 | comptime bes: []const AllBackend, 514 | comptime field: []const []const u8, 515 | ) type { 516 | var Set: type = error{}; 517 | for (bes) |be| { 518 | var NextSet: type = be.Api(); 519 | for (field) |f| NextSet = @field(NextSet, f); 520 | Set = Set || NextSet; 521 | } 522 | 523 | return Set; 524 | } 525 | -------------------------------------------------------------------------------- /src/heap.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const assert = std.debug.assert; 3 | 4 | /// An intrusive heap implementation backed by a pairing heap[1] implementation. 5 | /// 6 | /// Why? Intrusive data structures require the element type to hold the metadata 7 | /// required for the structure, rather than an additional container structure. 8 | /// There are numerous pros/cons that are documented well by Boost[2]. For Zig, 9 | /// I think the primary benefits are making data structures allocation free 10 | /// (rather, shifting allocation up to the consumer which can choose how they 11 | /// want the memory to be available). There are various costs to this such as 12 | /// the costs of pointer chasing, larger memory overhead, requiring the element 13 | /// type to be aware of its container, etc. But for certain use cases an intrusive 14 | /// data structure can yield much better performance. 15 | /// 16 | /// Usage notes: 17 | /// - The element T is expected to have a field "heap" of type InstrusiveHeapField. 18 | /// See the tests for a full example of how to set this. 19 | /// - You can easily make this a min or max heap by inverting the result of 20 | /// "less" below. 21 | /// 22 | /// [1]: https://en.wikipedia.org/wiki/Pairing_heap 23 | /// [2]: https://www.boost.org/doc/libs/1_64_0/doc/html/intrusive/intrusive_vs_nontrusive.html 24 | pub fn Intrusive( 25 | comptime T: type, 26 | comptime Context: type, 27 | comptime less: *const fn (ctx: Context, a: *T, b: *T) bool, 28 | ) type { 29 | return struct { 30 | const Self = @This(); 31 | 32 | root: ?*T = null, 33 | context: Context, 34 | 35 | /// Insert a new element v into the heap. An element v can only 36 | /// be a member of a single heap at any given time. When compiled 37 | /// with runtime-safety, assertions will help verify this property. 38 | pub fn insert(self: *Self, v: *T) void { 39 | self.root = if (self.root) |root| self.meld(v, root) else v; 40 | } 41 | 42 | /// Look at the next minimum value but do not remove it. 43 | pub fn peek(self: *Self) ?*T { 44 | return self.root; 45 | } 46 | 47 | /// Delete the minimum value from the heap and return it. 48 | pub fn deleteMin(self: *Self) ?*T { 49 | const root = self.root orelse return null; 50 | self.root = if (root.heap.child) |child| 51 | self.combine_siblings(child) 52 | else 53 | null; 54 | 55 | // Clear pointers with runtime safety so we can verify on 56 | // insert that values aren't incorrectly being set multiple times. 57 | root.heap = .{}; 58 | 59 | return root; 60 | } 61 | 62 | /// Remove the value v from the heap. 63 | pub fn remove(self: *Self, v: *T) void { 64 | // If v doesn't have a previous value, this must be the root 65 | // element. If it is NOT the root element, v can't be in this 66 | // heap and we trigger an assertion failure. 67 | const prev = v.heap.prev orelse { 68 | assert(self.root.? == v); 69 | _ = self.deleteMin(); 70 | return; 71 | }; 72 | 73 | // Detach "v" from the tree and clean up any links so it 74 | // is as if this node never nexisted. The previous value 75 | // must point to the proper next value and the pointers 76 | // must all be cleaned up. 77 | if (v.heap.next) |next| next.heap.prev = prev; 78 | if (prev.heap.child == v) 79 | prev.heap.child = v.heap.next 80 | else 81 | prev.heap.next = v.heap.next; 82 | v.heap.prev = null; 83 | v.heap.next = null; 84 | 85 | // If we have children, then we need to merge them back in. 86 | const child = v.heap.child orelse return; 87 | v.heap.child = null; 88 | const x = self.combine_siblings(child); 89 | self.root = self.meld(x, self.root.?); 90 | } 91 | 92 | /// Meld (union) two heaps together. This isn't a generalized 93 | /// union. It assumes that a.heap.next is null so this is only 94 | /// meant in specific scenarios in the pairing heap where meld 95 | /// is expected. 96 | /// 97 | /// For example, when melding a new value "v" with an existing 98 | /// root "root", "v" must always be the first param. 99 | fn meld(self: *Self, a: *T, b: *T) *T { 100 | assert(a.heap.next == null); 101 | 102 | if (less(self.context, a, b)) { 103 | // B points back to A 104 | b.heap.prev = a; 105 | 106 | // If B has siblings, then A inherits B's siblings 107 | // and B's immediate sibling must point back to A to 108 | // maintain the doubly linked list. 109 | if (b.heap.next) |b_next| { 110 | a.heap.next = b_next; 111 | b_next.heap.prev = a; 112 | b.heap.next = null; 113 | } 114 | 115 | // If A has a child, then B becomes the leftmost sibling 116 | // of that child. 117 | if (a.heap.child) |a_child| { 118 | b.heap.next = a_child; 119 | a_child.heap.prev = b; 120 | } 121 | 122 | // B becomes the leftmost child of A 123 | a.heap.child = b; 124 | 125 | return a; 126 | } 127 | 128 | // Replace A with B in the tree. Any of B's children 129 | // become siblings of A. A becomes the leftmost child of B. 130 | // A points back to B 131 | b.heap.prev = a.heap.prev; 132 | a.heap.prev = b; 133 | if (b.heap.child) |b_child| { 134 | a.heap.next = b_child; 135 | b_child.heap.prev = a; 136 | } 137 | b.heap.child = a; 138 | return b; 139 | } 140 | 141 | /// Combine the siblings of the leftmost value "left" into a single 142 | /// new rooted with the minimum value. 143 | fn combine_siblings(self: *Self, left: *T) *T { 144 | left.heap.prev = null; 145 | 146 | // Merge pairs right 147 | var root: *T = root: { 148 | var a: *T = left; 149 | while (true) { 150 | var b = a.heap.next orelse break :root a; 151 | a.heap.next = null; 152 | b = self.meld(a, b); 153 | a = b.heap.next orelse break :root b; 154 | } 155 | }; 156 | 157 | // Merge pairs left 158 | while (true) { 159 | var b = root.heap.prev orelse return root; 160 | b.heap.next = null; 161 | root = self.meld(b, root); 162 | } 163 | } 164 | }; 165 | } 166 | 167 | /// The state that is required for IntrusiveHeap element types. This 168 | /// should be set as the "heap" field in the type T. 169 | pub fn IntrusiveField(comptime T: type) type { 170 | return struct { 171 | child: ?*T = null, 172 | prev: ?*T = null, 173 | next: ?*T = null, 174 | }; 175 | } 176 | 177 | test "heap" { 178 | const Elem = struct { 179 | const Self = @This(); 180 | value: usize = 0, 181 | heap: IntrusiveField(Self) = .{}, 182 | }; 183 | 184 | const Heap = Intrusive(Elem, void, (struct { 185 | fn less(ctx: void, a: *Elem, b: *Elem) bool { 186 | _ = ctx; 187 | return a.value < b.value; 188 | } 189 | }).less); 190 | 191 | var a: Elem = .{ .value = 12 }; 192 | var b: Elem = .{ .value = 24 }; 193 | var c: Elem = .{ .value = 7 }; 194 | var d: Elem = .{ .value = 9 }; 195 | 196 | var h: Heap = .{ .context = {} }; 197 | h.insert(&a); 198 | h.insert(&b); 199 | h.insert(&c); 200 | h.insert(&d); 201 | h.remove(&d); 202 | 203 | const testing = std.testing; 204 | try testing.expect(h.deleteMin().?.value == 7); 205 | try testing.expect(h.deleteMin().?.value == 12); 206 | try testing.expect(h.deleteMin().?.value == 24); 207 | try testing.expect(h.deleteMin() == null); 208 | } 209 | 210 | test "heap remove root" { 211 | const Elem = struct { 212 | const Self = @This(); 213 | value: usize = 0, 214 | heap: IntrusiveField(Self) = .{}, 215 | }; 216 | 217 | const Heap = Intrusive(Elem, void, (struct { 218 | fn less(ctx: void, a: *Elem, b: *Elem) bool { 219 | _ = ctx; 220 | return a.value < b.value; 221 | } 222 | }).less); 223 | 224 | var a: Elem = .{ .value = 12 }; 225 | var b: Elem = .{ .value = 24 }; 226 | 227 | var h: Heap = .{ .context = {} }; 228 | h.insert(&a); 229 | h.insert(&b); 230 | h.remove(&a); 231 | 232 | const testing = std.testing; 233 | try testing.expect(h.deleteMin().?.value == 24); 234 | try testing.expect(h.deleteMin() == null); 235 | } 236 | 237 | test "heap remove with children" { 238 | const Elem = struct { 239 | const Self = @This(); 240 | value: usize = 0, 241 | heap: IntrusiveField(Self) = .{}, 242 | }; 243 | 244 | const Heap = Intrusive(Elem, void, (struct { 245 | fn less(ctx: void, a: *Elem, b: *Elem) bool { 246 | _ = ctx; 247 | return a.value < b.value; 248 | } 249 | }).less); 250 | 251 | var a: Elem = .{ .value = 36 }; 252 | var b: Elem = .{ .value = 24 }; 253 | var c: Elem = .{ .value = 12 }; 254 | 255 | var h: Heap = .{ .context = {} }; 256 | h.insert(&a); 257 | h.insert(&b); 258 | h.insert(&c); 259 | h.remove(&b); 260 | 261 | const testing = std.testing; 262 | try testing.expect(h.deleteMin().?.value == 12); 263 | try testing.expect(h.deleteMin().?.value == 36); 264 | try testing.expect(h.deleteMin() == null); 265 | } 266 | 267 | test "heap equal values" { 268 | const testing = std.testing; 269 | 270 | const Elem = struct { 271 | const Self = @This(); 272 | value: usize = 0, 273 | heap: IntrusiveField(Self) = .{}, 274 | }; 275 | 276 | const Heap = Intrusive(Elem, void, (struct { 277 | fn less(ctx: void, a: *Elem, b: *Elem) bool { 278 | _ = ctx; 279 | return a.value < b.value; 280 | } 281 | }).less); 282 | 283 | var a: Elem = .{ .value = 1 }; 284 | var b: Elem = .{ .value = 2 }; 285 | var c: Elem = .{ .value = 3 }; 286 | var d: Elem = .{ .value = 4 }; 287 | 288 | var h: Heap = .{ .context = {} }; 289 | h.insert(&a); 290 | h.insert(&b); 291 | h.insert(&c); 292 | h.insert(&d); 293 | 294 | try testing.expect(h.deleteMin().?.value == 1); 295 | try testing.expect(h.deleteMin().?.value == 2); 296 | try testing.expect(h.deleteMin().?.value == 3); 297 | try testing.expect(h.deleteMin().?.value == 4); 298 | try testing.expect(h.deleteMin() == null); 299 | } 300 | 301 | test "heap: million values" { 302 | const testing = std.testing; 303 | const alloc = testing.allocator; 304 | 305 | const Elem = struct { 306 | const Self = @This(); 307 | value: usize = 0, 308 | heap: IntrusiveField(Self) = .{}, 309 | }; 310 | 311 | const Heap = Intrusive(Elem, void, (struct { 312 | fn less(ctx: void, a: *Elem, b: *Elem) bool { 313 | _ = ctx; 314 | return a.value < b.value; 315 | } 316 | }).less); 317 | 318 | const NUM_TIMERS: usize = 1000 * 1000; 319 | var elems = try alloc.alloc(Elem, NUM_TIMERS); 320 | defer alloc.free(elems); 321 | 322 | var i: usize = 0; 323 | var value: usize = 0; 324 | while (i < NUM_TIMERS) : (i += 1) { 325 | if (i % 100 == 0) value += 1; 326 | elems[i] = .{ .value = value }; 327 | } 328 | 329 | var h: Heap = .{ .context = {} }; 330 | for (elems) |*elem| { 331 | h.insert(elem); 332 | } 333 | 334 | var count: usize = 0; 335 | var last: usize = 0; 336 | while (h.deleteMin()) |elem| { 337 | count += 1; 338 | try testing.expect(elem.value >= last); 339 | last = elem.value; 340 | } 341 | try testing.expect(h.deleteMin() == null); 342 | try testing.expect(count == NUM_TIMERS); 343 | } 344 | 345 | test "heap: dangling next pointer" { 346 | const testing = std.testing; 347 | const Elem = struct { 348 | const Self = @This(); 349 | value: usize = 0, 350 | heap: IntrusiveField(Self) = .{}, 351 | }; 352 | 353 | const Heap = Intrusive(Elem, void, (struct { 354 | fn less(ctx: void, a: *Elem, b: *Elem) bool { 355 | _ = ctx; 356 | return a.value < b.value; 357 | } 358 | }).less); 359 | 360 | var a: Elem = .{ .value = 2 }; 361 | var b: Elem = .{ .value = 4 }; 362 | var c: Elem = .{ .value = 5 }; 363 | var d: Elem = .{ .value = 1 }; 364 | var e: Elem = .{ .value = 3 }; 365 | 366 | var h: Heap = .{ .context = {} }; 367 | h.insert(&a); 368 | h.insert(&b); 369 | h.insert(&c); 370 | h.insert(&d); 371 | h.insert(&e); 372 | 373 | try testing.expect(h.deleteMin().?.value == 1); 374 | try testing.expect(h.deleteMin().?.value == 2); 375 | try testing.expect(h.deleteMin().?.value == 3); 376 | try testing.expect(h.deleteMin().?.value == 4); 377 | try testing.expect(h.deleteMin().?.value == 5); 378 | try testing.expect(h.deleteMin() == null); 379 | } 380 | -------------------------------------------------------------------------------- /src/linux/timerfd.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const linux = std.os.linux; 3 | const posix = std.posix; 4 | 5 | /// Timerfd is a wrapper around the timerfd system calls. See the 6 | /// timerfd_create man page for information on timerfd and associated 7 | /// system calls. 8 | /// 9 | /// This is a small wrapper around timerfd to make it slightly more 10 | /// pleasant to use, but may not expose all available functionality. 11 | /// For maximum control you should use the syscalls directly. 12 | pub const Timerfd = struct { 13 | /// The timerfd file descriptor for use with poll, etc. 14 | fd: i32, 15 | 16 | /// timerfd_create 17 | pub fn init( 18 | clock: linux.timerfd_clockid_t, 19 | flags: linux.TFD, 20 | ) !Timerfd { 21 | const res = linux.timerfd_create(clock, flags); 22 | return switch (posix.errno(res)) { 23 | .SUCCESS => .{ .fd = @as(i32, @intCast(res)) }, 24 | else => error.UnknownError, 25 | }; 26 | } 27 | 28 | pub fn deinit(self: *const Timerfd) void { 29 | posix.close(self.fd); 30 | } 31 | 32 | /// timerfd_settime 33 | pub fn set( 34 | self: *const Timerfd, 35 | flags: linux.TFD.TIMER, 36 | new_value: *const Spec, 37 | old_value: ?*Spec, 38 | ) !void { 39 | const res = linux.timerfd_settime( 40 | self.fd, 41 | flags, 42 | @as(*const linux.itimerspec, @ptrCast(new_value)), 43 | @as(?*linux.itimerspec, @ptrCast(old_value)), 44 | ); 45 | 46 | return switch (posix.errno(res)) { 47 | .SUCCESS => {}, 48 | else => error.UnknownError, 49 | }; 50 | } 51 | 52 | /// timerfd_gettime 53 | pub fn get(self: *const Timerfd) !Spec { 54 | var out: Spec = undefined; 55 | const res = linux.timerfd_gettime(self.fd, @as(*linux.itimerspec, @ptrCast(&out))); 56 | return switch (posix.errno(res)) { 57 | .SUCCESS => out, 58 | else => error.UnknownError, 59 | }; 60 | } 61 | 62 | /// itimerspec 63 | pub const Spec = extern struct { 64 | interval: TimeSpec = .{}, 65 | value: TimeSpec = .{}, 66 | }; 67 | 68 | /// timespec 69 | pub const TimeSpec = extern struct { 70 | seconds: isize = 0, 71 | nanoseconds: isize = 0, 72 | }; 73 | }; 74 | 75 | test Timerfd { 76 | const testing = std.testing; 77 | 78 | var t = try Timerfd.init(.MONOTONIC, .{}); 79 | defer t.deinit(); 80 | 81 | // Set 82 | try t.set(.{}, &.{ .value = .{ .seconds = 60 } }, null); 83 | try testing.expect((try t.get()).value.seconds > 0); 84 | 85 | // Disarm 86 | var old: Timerfd.Spec = undefined; 87 | try t.set(.{}, &.{ .value = .{ .seconds = 0 } }, &old); 88 | try testing.expect(old.value.seconds > 0); 89 | } 90 | -------------------------------------------------------------------------------- /src/loop.zig: -------------------------------------------------------------------------------- 1 | //! Common loop structures. The actual loop implementation is in backend-specific 2 | //! files such as linux/io_uring.zig. 3 | 4 | const std = @import("std"); 5 | const assert = std.debug.assert; 6 | const xev = @import("main.zig"); 7 | 8 | /// Common options across backends. Not all options apply to all backends. 9 | /// Read the doc comment for individual fields to learn what backends they 10 | /// apply to. 11 | pub const Options = struct { 12 | /// The number of queued completions that can be in flight before 13 | /// requiring interaction with the kernel. 14 | /// 15 | /// Backends: io_uring 16 | entries: u32 = 256, 17 | 18 | /// A thread pool to use for blocking operations. If the backend doesn't 19 | /// need to perform any blocking operations then no threads will ever 20 | /// be spawned. If the backend does need to perform blocking operations 21 | /// on a thread and no thread pool is provided, the operations will simply 22 | /// fail. Unless you're trying to really optimize for space, it is 23 | /// recommended you provide a thread pool. 24 | /// 25 | /// Backends: epoll, kqueue 26 | thread_pool: ?*xev.ThreadPool = null, 27 | }; 28 | 29 | /// The loop run mode -- all backends are required to support this in some way. 30 | /// Backends may provide backend-specific APIs that behave slightly differently 31 | /// or in a more configurable way. 32 | pub const RunMode = enum(c_int) { 33 | /// Run the event loop once. If there are no blocking operations ready, 34 | /// return immediately. 35 | no_wait = 0, 36 | 37 | /// Run the event loop once, waiting for at least one blocking operation 38 | /// to complete. 39 | once = 1, 40 | 41 | /// Run the event loop until it is "done". "Doneness" is defined as 42 | /// there being no more completions that are active. 43 | until_done = 2, 44 | }; 45 | 46 | /// The result type for callbacks. This should be used by all loop 47 | /// implementations and higher level abstractions in order to control 48 | /// what to do after the loop completes. 49 | pub const CallbackAction = enum(c_int) { 50 | /// The request is complete and is not repeated. For example, a read 51 | /// callback only fires once and is no longer watched for reads. You 52 | /// can always free memory associated with the completion prior to 53 | /// returning this. 54 | disarm = 0, 55 | 56 | /// Requeue the same operation request with the same parameters 57 | /// with the event loop. This makes it easy to repeat a read, timer, 58 | /// etc. This rearms the request EXACTLY as-is. For example, the 59 | /// low-level timer interface for io_uring uses an absolute timeout. 60 | /// If you rearm the timer, it will fire immediately because the absolute 61 | /// timeout will be in the past. 62 | /// 63 | /// The completion is reused so it is not safe to use the same completion 64 | /// for anything else. 65 | rearm = 1, 66 | }; 67 | 68 | /// The state that a completion can be in. 69 | pub const CompletionState = enum(c_int) { 70 | /// The completion is not being used and is ready to be configured 71 | /// for new work. 72 | dead = 0, 73 | 74 | /// The completion is part of an event loop. This may be already waited 75 | /// on or in the process of being registered. 76 | active = 1, 77 | }; 78 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | 4 | /// The low-level IO interfaces using the recommended compile-time 5 | /// interface for the target system. 6 | const xev = Backend.default().Api(); 7 | pub usingnamespace xev; 8 | //pub usingnamespace Epoll; 9 | 10 | /// The dynamic interface that allows for runtime selection of the 11 | /// backend to use. This is useful if you want to support multiple 12 | /// backends and have a fallback mechanism. 13 | /// 14 | /// There is a very small overhead to using this API compared to the 15 | /// static API, but it is generally negligible. 16 | /// 17 | /// The API for this isn't _exactly_ the same as the static API 18 | /// since it requires initialization of the main struct to detect 19 | /// a backend and then this needs to be passed to every high-level 20 | /// type such as Async, File, etc. so that they can use the correct 21 | /// backend that is coherent with the loop. 22 | pub const Dynamic = DynamicXev(Backend.candidates()); 23 | pub const DynamicXev = @import("dynamic.zig").Xev; 24 | 25 | /// System-specific interfaces. Note that they are always pub for 26 | /// all systems but if you reference them and force them to be analyzed 27 | /// the proper system APIs must exist. Due to Zig's lazy analysis, if you 28 | /// don't use any interface it will NOT be compiled (yay!). 29 | pub const IO_Uring = Xev(.io_uring, @import("backend/io_uring.zig")); 30 | pub const Epoll = Xev(.epoll, @import("backend/epoll.zig")); 31 | pub const Kqueue = Xev(.kqueue, @import("backend/kqueue.zig")); 32 | pub const WasiPoll = Xev(.wasi_poll, @import("backend/wasi_poll.zig")); 33 | pub const IOCP = Xev(.iocp, @import("backend/iocp.zig")); 34 | 35 | /// Generic thread pool implementation. 36 | pub const ThreadPool = @import("ThreadPool.zig"); 37 | 38 | /// This stream (lowercase s) can be used as a namespace to access 39 | /// Closeable, Writeable, Readable, etc. so that custom streams 40 | /// can be constructed. 41 | pub const stream = @import("watcher/stream.zig"); 42 | 43 | /// The backend types. 44 | pub const Backend = enum { 45 | io_uring, 46 | epoll, 47 | kqueue, 48 | wasi_poll, 49 | iocp, 50 | 51 | /// Returns a recommend default backend from inspecting the system. 52 | pub fn default() Backend { 53 | return switch (builtin.os.tag) { 54 | .linux => .io_uring, 55 | .ios, .macos => .kqueue, 56 | .wasi => .wasi_poll, 57 | .windows => .iocp, 58 | else => { 59 | @compileLog(builtin.os); 60 | @compileError("no default backend for this target"); 61 | }, 62 | }; 63 | } 64 | 65 | /// Candidate backends for this platform in priority order. 66 | pub fn candidates() []const Backend { 67 | return switch (builtin.os.tag) { 68 | .linux => &.{ .io_uring, .epoll }, 69 | .ios, .macos => &.{.kqueue}, 70 | .wasi => &.{.wasi_poll}, 71 | .windows => &.{.iocp}, 72 | else => { 73 | @compileLog(builtin.os); 74 | @compileError("no candidate backends for this target"); 75 | }, 76 | }; 77 | } 78 | 79 | /// Returns the Api (return value of Xev) for the given backend type. 80 | pub fn Api(comptime self: Backend) type { 81 | return switch (self) { 82 | .io_uring => IO_Uring, 83 | .epoll => Epoll, 84 | .kqueue => Kqueue, 85 | .wasi_poll => WasiPoll, 86 | .iocp => IOCP, 87 | }; 88 | } 89 | }; 90 | 91 | /// Creates the Xev API based on a backend type. 92 | /// 93 | /// For the default backend type for your system (i.e. io_uring on Linux), 94 | /// this is the main API you interact with. It is `usingnamespaced` into 95 | /// the "xev" package so you'd use types such as `xev.Loop`, `xev.Completion`, 96 | /// etc. 97 | /// 98 | /// Unless you're using a custom or specific backend type, you do NOT ever 99 | /// need to call the Xev function itself. 100 | pub fn Xev(comptime be: Backend, comptime T: type) type { 101 | return struct { 102 | const Self = @This(); 103 | const loop = @import("loop.zig"); 104 | 105 | /// This is used to detect a static vs dynamic API at comptime. 106 | pub const dynamic = false; 107 | 108 | /// The backend that this is. This is supplied at comptime so 109 | /// it is up to the caller to say the right thing. This lets custom 110 | /// implementations also "quack" like an implementation. 111 | pub const backend = be; 112 | 113 | /// A function to test if this API is available on the 114 | /// current system. 115 | pub const available = T.available; 116 | 117 | /// The core loop APIs. 118 | pub const Loop = T.Loop; 119 | pub const Completion = T.Completion; 120 | pub const Result = T.Result; 121 | pub const ReadBuffer = T.ReadBuffer; 122 | pub const WriteBuffer = T.WriteBuffer; 123 | pub const Options = loop.Options; 124 | pub const RunMode = loop.RunMode; 125 | pub const CallbackAction = loop.CallbackAction; 126 | pub const CompletionState = loop.CompletionState; 127 | 128 | /// Error types 129 | pub const AcceptError = T.AcceptError; 130 | pub const CancelError = T.CancelError; 131 | pub const CloseError = T.CloseError; 132 | pub const ConnectError = T.ConnectError; 133 | pub const ShutdownError = T.ShutdownError; 134 | pub const WriteError = T.WriteError; 135 | pub const ReadError = T.ReadError; 136 | 137 | /// Shared stream types 138 | const SharedStream = stream.Shared(Self); 139 | pub const PollError = SharedStream.PollError; 140 | pub const PollEvent = SharedStream.PollEvent; 141 | pub const WriteQueue = SharedStream.WriteQueue; 142 | pub const WriteRequest = SharedStream.WriteRequest; 143 | 144 | /// The high-level helper interfaces that make it easier to perform 145 | /// common tasks. These may not work with all possible Loop implementations. 146 | pub const Async = @import("watcher/async.zig").Async(Self); 147 | pub const File = @import("watcher/file.zig").File(Self); 148 | pub const Process = @import("watcher/process.zig").Process(Self); 149 | pub const Stream = stream.GenericStream(Self); 150 | pub const Timer = @import("watcher/timer.zig").Timer(Self); 151 | pub const TCP = @import("watcher/tcp.zig").TCP(Self); 152 | pub const UDP = @import("watcher/udp.zig").UDP(Self); 153 | 154 | /// The callback of the main Loop operations. Higher level interfaces may 155 | /// use a different callback mechanism. 156 | pub const Callback = *const fn ( 157 | userdata: ?*anyopaque, 158 | loop: *Loop, 159 | completion: *Completion, 160 | result: Result, 161 | ) CallbackAction; 162 | 163 | /// A way to access the raw type. 164 | pub const Sys = T; 165 | 166 | /// A callback that does nothing and immediately disarms. This 167 | /// implements xev.Callback and is the default value for completions. 168 | pub fn noopCallback( 169 | _: ?*anyopaque, 170 | _: *Loop, 171 | _: *Completion, 172 | _: Result, 173 | ) CallbackAction { 174 | return .disarm; 175 | } 176 | 177 | test { 178 | @import("std").testing.refAllDecls(@This()); 179 | } 180 | 181 | test "completion is zero-able" { 182 | const c: Completion = .{}; 183 | _ = c; 184 | } 185 | }; 186 | } 187 | 188 | test { 189 | // Tested on all platforms 190 | _ = @import("heap.zig"); 191 | _ = @import("queue.zig"); 192 | _ = @import("queue_mpsc.zig"); 193 | _ = ThreadPool; 194 | _ = Dynamic; 195 | 196 | // Test the C API 197 | if (builtin.os.tag != .wasi) _ = @import("c_api.zig"); 198 | 199 | // OS-specific tests 200 | switch (builtin.os.tag) { 201 | .linux => { 202 | _ = Epoll; 203 | _ = IO_Uring; 204 | _ = @import("linux/timerfd.zig"); 205 | }, 206 | 207 | .wasi => { 208 | //_ = WasiPoll; 209 | _ = @import("backend/wasi_poll.zig"); 210 | }, 211 | 212 | .windows => { 213 | _ = @import("backend/iocp.zig"); 214 | }, 215 | 216 | else => {}, 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/queue.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const assert = std.debug.assert; 3 | 4 | /// An intrusive queue implementation. The type T must have a field 5 | /// "next" of type `?*T`. 6 | /// 7 | /// For those unaware, an intrusive variant of a data structure is one in which 8 | /// the data type in the list has the pointer to the next element, rather 9 | /// than a higher level "node" or "container" type. The primary benefit 10 | /// of this (and the reason we implement this) is that it defers all memory 11 | /// management to the caller: the data structure implementation doesn't need 12 | /// to allocate "nodes" to contain each element. Instead, the caller provides 13 | /// the element and how its allocated is up to them. 14 | pub fn Intrusive(comptime T: type) type { 15 | return struct { 16 | const Self = @This(); 17 | 18 | /// Head is the front of the queue and tail is the back of the queue. 19 | head: ?*T = null, 20 | tail: ?*T = null, 21 | 22 | /// Enqueue a new element to the back of the queue. 23 | pub fn push(self: *Self, v: *T) void { 24 | assert(v.next == null); 25 | 26 | if (self.tail) |tail| { 27 | // If we have elements in the queue, then we add a new tail. 28 | tail.next = v; 29 | self.tail = v; 30 | } else { 31 | // No elements in the queue we setup the initial state. 32 | self.head = v; 33 | self.tail = v; 34 | } 35 | } 36 | 37 | /// Dequeue the next element from the queue. 38 | pub fn pop(self: *Self) ?*T { 39 | // The next element is in "head". 40 | const next = self.head orelse return null; 41 | 42 | // If the head and tail are equal this is the last element 43 | // so we also set tail to null so we can now be empty. 44 | if (self.head == self.tail) self.tail = null; 45 | 46 | // Head is whatever is next (if we're the last element, 47 | // this will be null); 48 | self.head = next.next; 49 | 50 | // We set the "next" field to null so that this element 51 | // can be inserted again. 52 | next.next = null; 53 | return next; 54 | } 55 | 56 | /// Returns true if the queue is empty. 57 | pub fn empty(self: *const Self) bool { 58 | return self.head == null; 59 | } 60 | }; 61 | } 62 | 63 | test Intrusive { 64 | const testing = std.testing; 65 | 66 | // Types 67 | const Elem = struct { 68 | const Self = @This(); 69 | next: ?*Self = null, 70 | }; 71 | const Queue = Intrusive(Elem); 72 | var q: Queue = .{}; 73 | try testing.expect(q.empty()); 74 | 75 | // Elems 76 | var elems: [10]Elem = .{Elem{}} ** 10; 77 | 78 | // One 79 | try testing.expect(q.pop() == null); 80 | q.push(&elems[0]); 81 | try testing.expect(!q.empty()); 82 | try testing.expect(q.pop().? == &elems[0]); 83 | try testing.expect(q.pop() == null); 84 | try testing.expect(q.empty()); 85 | 86 | // Two 87 | try testing.expect(q.pop() == null); 88 | q.push(&elems[0]); 89 | q.push(&elems[1]); 90 | try testing.expect(q.pop().? == &elems[0]); 91 | try testing.expect(q.pop().? == &elems[1]); 92 | try testing.expect(q.pop() == null); 93 | 94 | // Interleaved 95 | try testing.expect(q.pop() == null); 96 | q.push(&elems[0]); 97 | try testing.expect(q.pop().? == &elems[0]); 98 | q.push(&elems[1]); 99 | try testing.expect(q.pop().? == &elems[1]); 100 | try testing.expect(q.pop() == null); 101 | } 102 | -------------------------------------------------------------------------------- /src/queue_mpsc.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const assert = std.debug.assert; 3 | 4 | /// An intrusive MPSC (multi-provider, single consumer) queue implementation. 5 | /// The type T must have a field "next" of type `?*T`. 6 | /// 7 | /// This is an implementatin of a Vyukov Queue[1]. 8 | /// TODO(mitchellh): I haven't audited yet if I got all the atomic operations 9 | /// correct. I was short term more focused on getting something that seemed 10 | /// to work; I need to make sure it actually works. 11 | /// 12 | /// For those unaware, an intrusive variant of a data structure is one in which 13 | /// the data type in the list has the pointer to the next element, rather 14 | /// than a higher level "node" or "container" type. The primary benefit 15 | /// of this (and the reason we implement this) is that it defers all memory 16 | /// management to the caller: the data structure implementation doesn't need 17 | /// to allocate "nodes" to contain each element. Instead, the caller provides 18 | /// the element and how its allocated is up to them. 19 | /// 20 | /// [1]: https://www.1024cores.net/home/lock-free-algorithms/queues/intrusive-mpsc-node-based-queue 21 | pub fn Intrusive(comptime T: type) type { 22 | return struct { 23 | const Self = @This(); 24 | 25 | /// Head is the front of the queue and tail is the back of the queue. 26 | head: *T, 27 | tail: *T, 28 | stub: T, 29 | 30 | /// Initialize the queue. This requires a stable pointer to itself. 31 | /// This must be called before the queue is used concurrently. 32 | pub fn init(self: *Self) void { 33 | self.head = &self.stub; 34 | self.tail = &self.stub; 35 | self.stub.next = null; 36 | } 37 | 38 | /// Push an item onto the queue. This can be called by any number 39 | /// of producers. 40 | pub fn push(self: *Self, v: *T) void { 41 | @atomicStore(?*T, &v.next, null, .unordered); 42 | const prev = @atomicRmw(*T, &self.head, .Xchg, v, .acq_rel); 43 | @atomicStore(?*T, &prev.next, v, .release); 44 | } 45 | 46 | /// Pop the first in element from the queue. This must be called 47 | /// by only a single consumer at any given time. 48 | pub fn pop(self: *Self) ?*T { 49 | var tail = @atomicLoad(*T, &self.tail, .unordered); 50 | var next_ = @atomicLoad(?*T, &tail.next, .acquire); 51 | if (tail == &self.stub) { 52 | const next = next_ orelse return null; 53 | @atomicStore(*T, &self.tail, next, .unordered); 54 | tail = next; 55 | next_ = @atomicLoad(?*T, &tail.next, .acquire); 56 | } 57 | 58 | if (next_) |next| { 59 | @atomicStore(*T, &self.tail, next, .release); 60 | tail.next = null; 61 | return tail; 62 | } 63 | 64 | const head = @atomicLoad(*T, &self.head, .unordered); 65 | if (tail != head) return null; 66 | self.push(&self.stub); 67 | 68 | next_ = @atomicLoad(?*T, &tail.next, .acquire); 69 | if (next_) |next| { 70 | @atomicStore(*T, &self.tail, next, .unordered); 71 | tail.next = null; 72 | return tail; 73 | } 74 | 75 | return null; 76 | } 77 | }; 78 | } 79 | 80 | test Intrusive { 81 | const testing = std.testing; 82 | 83 | // Types 84 | const Elem = struct { 85 | const Self = @This(); 86 | next: ?*Self = null, 87 | }; 88 | const Queue = Intrusive(Elem); 89 | var q: Queue = undefined; 90 | q.init(); 91 | 92 | // Elems 93 | var elems: [10]Elem = .{Elem{}} ** 10; 94 | 95 | // One 96 | try testing.expect(q.pop() == null); 97 | q.push(&elems[0]); 98 | try testing.expect(q.pop().? == &elems[0]); 99 | try testing.expect(q.pop() == null); 100 | 101 | // Two 102 | try testing.expect(q.pop() == null); 103 | q.push(&elems[0]); 104 | q.push(&elems[1]); 105 | try testing.expect(q.pop().? == &elems[0]); 106 | try testing.expect(q.pop().? == &elems[1]); 107 | try testing.expect(q.pop() == null); 108 | 109 | // // Interleaved 110 | try testing.expect(q.pop() == null); 111 | q.push(&elems[0]); 112 | try testing.expect(q.pop().? == &elems[0]); 113 | q.push(&elems[1]); 114 | try testing.expect(q.pop().? == &elems[1]); 115 | try testing.expect(q.pop() == null); 116 | } 117 | -------------------------------------------------------------------------------- /src/watcher/common.zig: -------------------------------------------------------------------------------- 1 | /// Convert the callback value with an opaque pointer into the userdata type 2 | /// that we can pass to our higher level callback types. 3 | pub fn userdataValue(comptime Userdata: type, v: ?*anyopaque) ?*Userdata { 4 | // Void userdata is always a null pointer. 5 | if (Userdata == void) return null; 6 | return @ptrCast(@alignCast(v)); 7 | } 8 | -------------------------------------------------------------------------------- /src/windows.zig: -------------------------------------------------------------------------------- 1 | //! Namespace containing missing utils from std 2 | 3 | const std = @import("std"); 4 | const windows = std.os.windows; 5 | const posix = std.posix; 6 | 7 | pub usingnamespace std.os.windows; 8 | 9 | pub extern "kernel32" fn DeleteFileW(lpFileName: [*:0]const u16) callconv(windows.WINAPI) windows.BOOL; 10 | 11 | pub const exp = struct { 12 | pub const STATUS_PENDING = 0x00000103; 13 | pub const STILL_ACTIVE = STATUS_PENDING; 14 | 15 | pub const JOBOBJECT_ASSOCIATE_COMPLETION_PORT = extern struct { 16 | CompletionKey: windows.ULONG_PTR, 17 | CompletionPort: windows.HANDLE, 18 | }; 19 | 20 | pub const JOBOBJECT_BASIC_LIMIT_INFORMATION = extern struct { 21 | PerProcessUserTimeLimit: windows.LARGE_INTEGER, 22 | PerJobUserTimeLimit: windows.LARGE_INTEGER, 23 | LimitFlags: windows.DWORD, 24 | MinimumWorkingSetSize: windows.SIZE_T, 25 | MaximumWorkingSetSize: windows.SIZE_T, 26 | ActiveProcessLimit: windows.DWORD, 27 | Affinity: windows.ULONG_PTR, 28 | PriorityClass: windows.DWORD, 29 | SchedulingClass: windows.DWORD, 30 | }; 31 | 32 | pub const IO_COUNTERS = extern struct { 33 | ReadOperationCount: windows.ULONGLONG, 34 | WriteOperationCount: windows.ULONGLONG, 35 | OtherOperationCount: windows.ULONGLONG, 36 | ReadTransferCount: windows.ULONGLONG, 37 | WriteTransferCount: windows.ULONGLONG, 38 | OtherTransferCount: windows.ULONGLONG, 39 | }; 40 | 41 | pub const JOBOBJECT_EXTENDED_LIMIT_INFORMATION = extern struct { 42 | BasicLimitInformation: JOBOBJECT_BASIC_LIMIT_INFORMATION, 43 | IoInfo: IO_COUNTERS, 44 | ProcessMemoryLimit: windows.SIZE_T, 45 | JobMemoryLimit: windows.SIZE_T, 46 | PeakProcessMemoryUsed: windows.SIZE_T, 47 | PeakJobMemoryUsed: windows.SIZE_T, 48 | }; 49 | 50 | pub const JOB_OBJECT_LIMIT_ACTIVE_PROCESS = 0x00000008; 51 | pub const JOB_OBJECT_LIMIT_AFFINITY = 0x00000010; 52 | pub const JOB_OBJECT_LIMIT_BREAKAWAY_OK = 0x00000800; 53 | pub const JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION = 0x00000400; 54 | pub const JOB_OBJECT_LIMIT_JOB_MEMORY = 0x00000200; 55 | pub const JOB_OBJECT_LIMIT_JOB_TIME = 0x00000004; 56 | pub const JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000; 57 | pub const JOB_OBJECT_LIMIT_PRESERVE_JOB_TIME = 0x00000004; 58 | pub const JOB_OBJECT_LIMIT_PRIORITY_CLASS = 0x00000020; 59 | pub const JOB_OBJECT_LIMIT_PROCESS_MEMORY = 0x00000100; 60 | pub const JOB_OBJECT_LIMIT_PROCESS_TIME = 0x00000002; 61 | pub const JOB_OBJECT_LIMIT_SCHEDULING_CLASS = 0x00000080; 62 | pub const JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK = 0x00001000; 63 | pub const JOB_OBJECT_LIMIT_SUBSET_AFFINITY = 0x00004000; 64 | pub const JOB_OBJECT_LIMIT_WORKINGSET = 0x00000001; 65 | 66 | pub const JOBOBJECT_INFORMATION_CLASS = enum(c_int) { 67 | JobObjectAssociateCompletionPortInformation = 7, 68 | JobObjectBasicLimitInformation = 2, 69 | JobObjectBasicUIRestrictions = 4, 70 | JobObjectCpuRateControlInformation = 15, 71 | JobObjectEndOfJobTimeInformation = 6, 72 | JobObjectExtendedLimitInformation = 9, 73 | JobObjectGroupInformation = 11, 74 | JobObjectGroupInformationEx = 14, 75 | JobObjectLimitViolationInformation2 = 34, 76 | JobObjectNetRateControlInformation = 32, 77 | JobObjectNotificationLimitInformation = 12, 78 | JobObjectNotificationLimitInformation2 = 33, 79 | JobObjectSecurityLimitInformation = 5, 80 | }; 81 | 82 | pub const JOB_OBJECT_MSG_TYPE = enum(windows.DWORD) { 83 | JOB_OBJECT_MSG_END_OF_JOB_TIME = 1, 84 | JOB_OBJECT_MSG_END_OF_PROCESS_TIME = 2, 85 | JOB_OBJECT_MSG_ACTIVE_PROCESS_LIMIT = 3, 86 | JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO = 4, 87 | JOB_OBJECT_MSG_NEW_PROCESS = 6, 88 | JOB_OBJECT_MSG_EXIT_PROCESS = 7, 89 | JOB_OBJECT_MSG_ABNORMAL_EXIT_PROCESS = 8, 90 | JOB_OBJECT_MSG_PROCESS_MEMORY_LIMIT = 9, 91 | JOB_OBJECT_MSG_JOB_MEMORY_LIMIT = 10, 92 | JOB_OBJECT_MSG_NOTIFICATION_LIMIT = 11, 93 | JOB_OBJECT_MSG_JOB_CYCLE_TIME_LIMIT = 12, 94 | JOB_OBJECT_MSG_SILO_TERMINATED = 13, 95 | _, 96 | }; 97 | 98 | pub const kernel32 = struct { 99 | pub extern "kernel32" fn GetProcessId(Process: windows.HANDLE) callconv(windows.WINAPI) windows.DWORD; 100 | pub extern "kernel32" fn CreateJobObjectA(lpSecurityAttributes: ?*windows.SECURITY_ATTRIBUTES, lpName: ?windows.LPCSTR) callconv(windows.WINAPI) windows.HANDLE; 101 | pub extern "kernel32" fn AssignProcessToJobObject(hJob: windows.HANDLE, hProcess: windows.HANDLE) callconv(windows.WINAPI) windows.BOOL; 102 | pub extern "kernel32" fn SetInformationJobObject( 103 | hJob: windows.HANDLE, 104 | JobObjectInformationClass: JOBOBJECT_INFORMATION_CLASS, 105 | lpJobObjectInformation: windows.LPVOID, 106 | cbJobObjectInformationLength: windows.DWORD, 107 | ) callconv(windows.WINAPI) windows.BOOL; 108 | }; 109 | 110 | pub const CreateFileError = error{} || posix.UnexpectedError; 111 | 112 | pub fn CreateFile( 113 | lpFileName: [*:0]const u16, 114 | dwDesiredAccess: windows.DWORD, 115 | dwShareMode: windows.DWORD, 116 | lpSecurityAttributes: ?*windows.SECURITY_ATTRIBUTES, 117 | dwCreationDisposition: windows.DWORD, 118 | dwFlagsAndAttributes: windows.DWORD, 119 | hTemplateFile: ?windows.HANDLE, 120 | ) CreateFileError!windows.HANDLE { 121 | const handle = windows.kernel32.CreateFileW(lpFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile); 122 | if (handle == windows.INVALID_HANDLE_VALUE) { 123 | const err = windows.kernel32.GetLastError(); 124 | return switch (err) { 125 | else => windows.unexpectedError(err), 126 | }; 127 | } 128 | 129 | return handle; 130 | } 131 | 132 | pub fn ReadFile( 133 | handle: windows.HANDLE, 134 | buffer: []u8, 135 | overlapped: ?*windows.OVERLAPPED, 136 | ) windows.ReadFileError!?usize { 137 | var read: windows.DWORD = 0; 138 | const result: windows.BOOL = windows.kernel32.ReadFile(handle, buffer.ptr, @as(windows.DWORD, @intCast(buffer.len)), &read, overlapped); 139 | if (result == windows.FALSE) { 140 | const err = windows.kernel32.GetLastError(); 141 | return switch (err) { 142 | windows.Win32Error.IO_PENDING => null, 143 | else => windows.unexpectedError(err), 144 | }; 145 | } 146 | 147 | return @as(usize, @intCast(read)); 148 | } 149 | 150 | pub fn WriteFile( 151 | handle: windows.HANDLE, 152 | buffer: []const u8, 153 | overlapped: ?*windows.OVERLAPPED, 154 | ) windows.WriteFileError!?usize { 155 | var written: windows.DWORD = 0; 156 | const result: windows.BOOL = windows.kernel32.WriteFile(handle, buffer.ptr, @as(windows.DWORD, @intCast(buffer.len)), &written, overlapped); 157 | if (result == windows.FALSE) { 158 | const err = windows.kernel32.GetLastError(); 159 | return switch (err) { 160 | windows.Win32Error.IO_PENDING => null, 161 | else => windows.unexpectedError(err), 162 | }; 163 | } 164 | 165 | return @as(usize, @intCast(written)); 166 | } 167 | 168 | pub const DeleteFileError = error{} || posix.UnexpectedError; 169 | 170 | pub fn DeleteFile(name: [*:0]const u16) DeleteFileError!void { 171 | const result: windows.BOOL = DeleteFileW(name); 172 | if (result == windows.FALSE) { 173 | const err = windows.kernel32.GetLastError(); 174 | return switch (err) { 175 | else => windows.unexpectedError(err), 176 | }; 177 | } 178 | } 179 | 180 | pub const CreateJobObjectError = error{AlreadyExists} || posix.UnexpectedError; 181 | pub fn CreateJobObject( 182 | lpSecurityAttributes: ?*windows.SECURITY_ATTRIBUTES, 183 | lpName: ?windows.LPCSTR, 184 | ) !windows.HANDLE { 185 | const handle = kernel32.CreateJobObjectA(lpSecurityAttributes, lpName); 186 | return switch (windows.kernel32.GetLastError()) { 187 | .SUCCESS => handle, 188 | .ALREADY_EXISTS => CreateJobObjectError.AlreadyExists, 189 | else => |err| windows.unexpectedError(err), 190 | }; 191 | } 192 | 193 | pub fn AssignProcessToJobObject(hJob: windows.HANDLE, hProcess: windows.HANDLE) posix.UnexpectedError!void { 194 | const result: windows.BOOL = kernel32.AssignProcessToJobObject(hJob, hProcess); 195 | if (result == windows.FALSE) { 196 | const err = windows.kernel32.GetLastError(); 197 | return switch (err) { 198 | else => windows.unexpectedError(err), 199 | }; 200 | } 201 | } 202 | 203 | pub fn SetInformationJobObject( 204 | hJob: windows.HANDLE, 205 | JobObjectInformationClass: JOBOBJECT_INFORMATION_CLASS, 206 | lpJobObjectInformation: windows.LPVOID, 207 | cbJobObjectInformationLength: windows.DWORD, 208 | ) posix.UnexpectedError!void { 209 | const result: windows.BOOL = kernel32.SetInformationJobObject( 210 | hJob, 211 | JobObjectInformationClass, 212 | lpJobObjectInformation, 213 | cbJobObjectInformationLength, 214 | ); 215 | 216 | if (result == windows.FALSE) { 217 | const err = windows.kernel32.GetLastError(); 218 | return switch (err) { 219 | else => windows.unexpectedError(err), 220 | }; 221 | } 222 | } 223 | }; 224 | -------------------------------------------------------------------------------- /website/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /website/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | } 5 | 6 | const withNextra = require('nextra')({ 7 | theme: 'nextra-theme-docs', 8 | themeConfig: './theme.config.jsx', 9 | }) 10 | 11 | module.exports = withNextra(nextConfig) 12 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "website", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@next/font": "13.1.2", 13 | "@types/node": "18.11.18", 14 | "@types/react": "18.0.26", 15 | "@types/react-dom": "18.0.10", 16 | "eslint": "8.32.0", 17 | "eslint-config-next": "13.1.2", 18 | "next": "13.1.2", 19 | "nextra": "^2.2.5", 20 | "nextra-theme-docs": "^2.2.5", 21 | "react": "18.2.0", 22 | "react-dom": "18.2.0", 23 | "typescript": "4.9.4" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /website/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '@/styles/globals.css' 2 | import type { AppProps } from 'next/app' 3 | 4 | export default function App({ Component, pageProps }: AppProps) { 5 | return 6 | } 7 | -------------------------------------------------------------------------------- /website/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from 'next/document' 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /website/pages/_meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "index": "Home", 3 | "getting-started": "Getting Started", 4 | "api-docs": "API" 5 | } 6 | -------------------------------------------------------------------------------- /website/pages/api-docs/_meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "concepts": "Concepts" 3 | } 4 | -------------------------------------------------------------------------------- /website/pages/api-docs/concepts.mdx: -------------------------------------------------------------------------------- 1 | # General API Concepts 2 | 3 | -------------------------------------------------------------------------------- /website/pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | type Data = { 5 | name: string 6 | } 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ) { 12 | res.status(200).json({ name: 'John Doe' }) 13 | } 14 | -------------------------------------------------------------------------------- /website/pages/getting-started.mdx: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | TODO 4 | -------------------------------------------------------------------------------- /website/pages/getting-started/_meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "concepts": "Concepts" 3 | } 4 | -------------------------------------------------------------------------------- /website/pages/getting-started/concepts.mdx: -------------------------------------------------------------------------------- 1 | # General Concepts 2 | 3 | TODO 4 | -------------------------------------------------------------------------------- /website/pages/index.mdx: -------------------------------------------------------------------------------- 1 | # libxev 2 | 3 | import { Callout } from 'nextra-theme-docs' 4 | 5 | TODO! See the GitHub repo for now. 6 | 7 | 8 | **Coming soon.** I've just got some scaffolding down. 9 | 10 | -------------------------------------------------------------------------------- /website/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchellh/libxev/58507577fc87b89471809a1a23415bee1d81814d/website/public/favicon.ico -------------------------------------------------------------------------------- /website/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /website/public/thirteen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /website/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /website/styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .main { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: space-between; 5 | align-items: center; 6 | padding: 6rem; 7 | min-height: 100vh; 8 | } 9 | 10 | .description { 11 | display: inherit; 12 | justify-content: inherit; 13 | align-items: inherit; 14 | font-size: 0.85rem; 15 | max-width: var(--max-width); 16 | width: 100%; 17 | z-index: 2; 18 | font-family: var(--font-mono); 19 | } 20 | 21 | .description a { 22 | display: flex; 23 | justify-content: center; 24 | align-items: center; 25 | gap: 0.5rem; 26 | } 27 | 28 | .description p { 29 | position: relative; 30 | margin: 0; 31 | padding: 1rem; 32 | background-color: rgba(var(--callout-rgb), 0.5); 33 | border: 1px solid rgba(var(--callout-border-rgb), 0.3); 34 | border-radius: var(--border-radius); 35 | } 36 | 37 | .code { 38 | font-weight: 700; 39 | font-family: var(--font-mono); 40 | } 41 | 42 | .grid { 43 | display: grid; 44 | grid-template-columns: repeat(4, minmax(25%, auto)); 45 | width: var(--max-width); 46 | max-width: 100%; 47 | } 48 | 49 | .card { 50 | padding: 1rem 1.2rem; 51 | border-radius: var(--border-radius); 52 | background: rgba(var(--card-rgb), 0); 53 | border: 1px solid rgba(var(--card-border-rgb), 0); 54 | transition: background 200ms, border 200ms; 55 | } 56 | 57 | .card span { 58 | display: inline-block; 59 | transition: transform 200ms; 60 | } 61 | 62 | .card h2 { 63 | font-weight: 600; 64 | margin-bottom: 0.7rem; 65 | } 66 | 67 | .card p { 68 | margin: 0; 69 | opacity: 0.6; 70 | font-size: 0.9rem; 71 | line-height: 1.5; 72 | max-width: 30ch; 73 | } 74 | 75 | .center { 76 | display: flex; 77 | justify-content: center; 78 | align-items: center; 79 | position: relative; 80 | padding: 4rem 0; 81 | } 82 | 83 | .center::before { 84 | background: var(--secondary-glow); 85 | border-radius: 50%; 86 | width: 480px; 87 | height: 360px; 88 | margin-left: -400px; 89 | } 90 | 91 | .center::after { 92 | background: var(--primary-glow); 93 | width: 240px; 94 | height: 180px; 95 | z-index: -1; 96 | } 97 | 98 | .center::before, 99 | .center::after { 100 | content: ''; 101 | left: 50%; 102 | position: absolute; 103 | filter: blur(45px); 104 | transform: translateZ(0); 105 | } 106 | 107 | .logo, 108 | .thirteen { 109 | position: relative; 110 | } 111 | 112 | .thirteen { 113 | display: flex; 114 | justify-content: center; 115 | align-items: center; 116 | width: 75px; 117 | height: 75px; 118 | padding: 25px 10px; 119 | margin-left: 16px; 120 | transform: translateZ(0); 121 | border-radius: var(--border-radius); 122 | overflow: hidden; 123 | box-shadow: 0px 2px 8px -1px #0000001a; 124 | } 125 | 126 | .thirteen::before, 127 | .thirteen::after { 128 | content: ''; 129 | position: absolute; 130 | z-index: -1; 131 | } 132 | 133 | /* Conic Gradient Animation */ 134 | .thirteen::before { 135 | animation: 6s rotate linear infinite; 136 | width: 200%; 137 | height: 200%; 138 | background: var(--tile-border); 139 | } 140 | 141 | /* Inner Square */ 142 | .thirteen::after { 143 | inset: 0; 144 | padding: 1px; 145 | border-radius: var(--border-radius); 146 | background: linear-gradient( 147 | to bottom right, 148 | rgba(var(--tile-start-rgb), 1), 149 | rgba(var(--tile-end-rgb), 1) 150 | ); 151 | background-clip: content-box; 152 | } 153 | 154 | /* Enable hover only on non-touch devices */ 155 | @media (hover: hover) and (pointer: fine) { 156 | .card:hover { 157 | background: rgba(var(--card-rgb), 0.1); 158 | border: 1px solid rgba(var(--card-border-rgb), 0.15); 159 | } 160 | 161 | .card:hover span { 162 | transform: translateX(4px); 163 | } 164 | } 165 | 166 | @media (prefers-reduced-motion) { 167 | .thirteen::before { 168 | animation: none; 169 | } 170 | 171 | .card:hover span { 172 | transform: none; 173 | } 174 | } 175 | 176 | /* Mobile */ 177 | @media (max-width: 700px) { 178 | .content { 179 | padding: 4rem; 180 | } 181 | 182 | .grid { 183 | grid-template-columns: 1fr; 184 | margin-bottom: 120px; 185 | max-width: 320px; 186 | text-align: center; 187 | } 188 | 189 | .card { 190 | padding: 1rem 2.5rem; 191 | } 192 | 193 | .card h2 { 194 | margin-bottom: 0.5rem; 195 | } 196 | 197 | .center { 198 | padding: 8rem 0 6rem; 199 | } 200 | 201 | .center::before { 202 | transform: none; 203 | height: 300px; 204 | } 205 | 206 | .description { 207 | font-size: 0.8rem; 208 | } 209 | 210 | .description a { 211 | padding: 1rem; 212 | } 213 | 214 | .description p, 215 | .description div { 216 | display: flex; 217 | justify-content: center; 218 | position: fixed; 219 | width: 100%; 220 | } 221 | 222 | .description p { 223 | align-items: center; 224 | inset: 0 0 auto; 225 | padding: 2rem 1rem 1.4rem; 226 | border-radius: 0; 227 | border: none; 228 | border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25); 229 | background: linear-gradient( 230 | to bottom, 231 | rgba(var(--background-start-rgb), 1), 232 | rgba(var(--callout-rgb), 0.5) 233 | ); 234 | background-clip: padding-box; 235 | backdrop-filter: blur(24px); 236 | } 237 | 238 | .description div { 239 | align-items: flex-end; 240 | pointer-events: none; 241 | inset: auto 0 0; 242 | padding: 2rem; 243 | height: 200px; 244 | background: linear-gradient( 245 | to bottom, 246 | transparent 0%, 247 | rgb(var(--background-end-rgb)) 40% 248 | ); 249 | z-index: 1; 250 | } 251 | } 252 | 253 | /* Tablet and Smaller Desktop */ 254 | @media (min-width: 701px) and (max-width: 1120px) { 255 | .grid { 256 | grid-template-columns: repeat(2, 50%); 257 | } 258 | } 259 | 260 | @media (prefers-color-scheme: dark) { 261 | .vercelLogo { 262 | filter: invert(1); 263 | } 264 | 265 | .logo, 266 | .thirteen img { 267 | filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70); 268 | } 269 | } 270 | 271 | @keyframes rotate { 272 | from { 273 | transform: rotate(360deg); 274 | } 275 | to { 276 | transform: rotate(0deg); 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /website/styles/globals.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --max-width: 1100px; 3 | --border-radius: 12px; 4 | --font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono', 5 | 'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro', 6 | 'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace; 7 | 8 | --foreground-rgb: 0, 0, 0; 9 | --background-start-rgb: 214, 219, 220; 10 | --background-end-rgb: 255, 255, 255; 11 | 12 | --primary-glow: conic-gradient( 13 | from 180deg at 50% 50%, 14 | #16abff33 0deg, 15 | #0885ff33 55deg, 16 | #54d6ff33 120deg, 17 | #0071ff33 160deg, 18 | transparent 360deg 19 | ); 20 | --secondary-glow: radial-gradient( 21 | rgba(255, 255, 255, 1), 22 | rgba(255, 255, 255, 0) 23 | ); 24 | 25 | --tile-start-rgb: 239, 245, 249; 26 | --tile-end-rgb: 228, 232, 233; 27 | --tile-border: conic-gradient( 28 | #00000080, 29 | #00000040, 30 | #00000030, 31 | #00000020, 32 | #00000010, 33 | #00000010, 34 | #00000080 35 | ); 36 | 37 | --callout-rgb: 238, 240, 241; 38 | --callout-border-rgb: 172, 175, 176; 39 | --card-rgb: 180, 185, 188; 40 | --card-border-rgb: 131, 134, 135; 41 | } 42 | 43 | @media (prefers-color-scheme: dark) { 44 | :root { 45 | --foreground-rgb: 255, 255, 255; 46 | --background-start-rgb: 0, 0, 0; 47 | --background-end-rgb: 0, 0, 0; 48 | 49 | --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0)); 50 | --secondary-glow: linear-gradient( 51 | to bottom right, 52 | rgba(1, 65, 255, 0), 53 | rgba(1, 65, 255, 0), 54 | rgba(1, 65, 255, 0.3) 55 | ); 56 | 57 | --tile-start-rgb: 2, 13, 46; 58 | --tile-end-rgb: 2, 5, 19; 59 | --tile-border: conic-gradient( 60 | #ffffff80, 61 | #ffffff40, 62 | #ffffff30, 63 | #ffffff20, 64 | #ffffff10, 65 | #ffffff10, 66 | #ffffff80 67 | ); 68 | 69 | --callout-rgb: 20, 20, 20; 70 | --callout-border-rgb: 108, 108, 108; 71 | --card-rgb: 100, 100, 100; 72 | --card-border-rgb: 200, 200, 200; 73 | } 74 | } 75 | 76 | * { 77 | box-sizing: border-box; 78 | padding: 0; 79 | margin: 0; 80 | } 81 | 82 | html, 83 | body { 84 | max-width: 100vw; 85 | overflow-x: hidden; 86 | } 87 | 88 | body { 89 | color: rgb(var(--foreground-rgb)); 90 | background: linear-gradient( 91 | to bottom, 92 | transparent, 93 | rgb(var(--background-end-rgb)) 94 | ) 95 | rgb(var(--background-start-rgb)); 96 | } 97 | 98 | a { 99 | color: inherit; 100 | text-decoration: none; 101 | } 102 | 103 | @media (prefers-color-scheme: dark) { 104 | html { 105 | color-scheme: dark; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /website/theme.config.jsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router' 2 | 3 | export default { 4 | logo: libxev, 5 | docsRepositoryBase: 'https://github.com/mitchellh/libxev/blob/website/pages', 6 | 7 | project: { 8 | link: 'https://github.com/mitchellh/libxev', 9 | }, 10 | 11 | feedback: { 12 | content: null, 13 | }, 14 | 15 | footer: { 16 | text: 17 | © {new Date().getFullYear()} 18 | libxev 19 | , 20 | }, 21 | 22 | useNextSeoProps() { 23 | const { route } = useRouter() 24 | if (route !== '/') { 25 | return { 26 | titleTemplate: '%s – libxev' 27 | } 28 | } 29 | }, 30 | 31 | // Hide the last updated date. 32 | gitTimestamp: , 33 | } 34 | -------------------------------------------------------------------------------- /website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "baseUrl": ".", 18 | "paths": { 19 | "@/*": ["./*"] 20 | } 21 | }, 22 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 23 | "exclude": ["node_modules"] 24 | } 25 | --------------------------------------------------------------------------------