I will never get tired of vendoring dependencies. ha ha. It is possible
I am insane. I had to do a lot of pruning to get these not to be
ridiculous (especially the unicode data, which had nearly 1 million
lines of... stuff).
This commit is contained in:
2024-08-09 17:32:06 -07:00
commit 7692cb4bc7
155 changed files with 206515 additions and 0 deletions

21
deps/libxev/LICENSE vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Mitchell Hashimoto
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

315
deps/libxev/README.md vendored Normal file
View File

@@ -0,0 +1,315 @@
# libxev
libxev is a cross-platform event loop. libxev provides a unified event loop
abstraction for non-blocking IO, timers, signals, events, and more that
works on macOS, Windows, Linux, and WebAssembly (browser and WASI). It is
written in [Zig](https://ziglang.org/) but exports a C-compatible API (which
further makes it compatible with any language out there that can communicate
with C APIs).
**Project Status: 🐲 Unstable, alpha-ish quality.** The feature list is quite
good across multiple platforms, but there are plenty of missing features.
The project hasn't been well tested in real-world environments and there
are lots of low-hanging fruit for performance optimization. I'm not promising
any API compatibility at this point, either. If you want a production ready,
high quality, generalized event loop implementation check out
[libuv](https://libuv.org/), libev, etc.
**Why a new event loop library?** A few reasons. One, I think Zig lacks
a generalized event loop comparable to libuv in features ("generalized"
being a key word here). Two, I wanted to build a library like this around
the design patterns of [io_uring](https://unixism.net/loti/what_is_io_uring.html),
even mimicking its style on top of other OS primitives (
[credit to this awesome blog post](https://tigerbeetle.com/blog/a-friendly-abstraction-over-iouring-and-kqueue/)).
Three, I wanted an event loop library that could build to WebAssembly
(both WASI and freestanding) and that didn't really fit well
into the goals of API style of existing libraries without bringing in
something super heavy like Emscripten. The motivation for this library
primarily though is scratching my own itch!
## Features
**Cross-platform.** Linux (`io_uring` and `epoll`), macOS (`kqueue`),
WebAssembly + WASI (`poll_oneoff`, threaded and non-threaded runtimes).
(Windows support is planned and coming soon)
**[Proactor API](https://en.wikipedia.org/wiki/Proactor_pattern).** Work
is submitted to the libxev event loop and the caller is notified of
work _completion_, as opposed to work _readiness_.
**Zero runtime allocations.** This helps make runtime performance more
predictable and makes libxev well suited for embedded environments.
**Timers, TCP, UDP, Files, Processes.** High-level platform-agnostic APIs for
interacting with timers, TCP/UDP sockets, files, processes, and more. For
platforms that don't support async IO, the file operations are automatically
scheduled to a thread pool.
**Generic Thread Pool (Optional).** You can create a generic thread pool,
configure its resource utilization, and use this to perform custom background
tasks. The thread pool is used by some backends to do non-blocking tasks that
don't have reliable non-blocking APIs (such as local file operations with
`kqueue`). The thread pool can be shared across multiple threads and event
loops to optimize resource utilization.
**Low-level and High-Level API.** The high-level API is platform-agnostic
but has some opinionated behavior and limited flexibility. The high-level
API is recommended but the low-level API is always an available escape hatch.
The low-level API is platform-specific and provides a mechanism for libxev
users to squeeze out maximum performance. The low-level API is _just enough
abstraction_ above the OS interface to make it easier to use without
sacrificing noticable performance.
**Tree Shaking (Zig).** This is a feature of Zig, but substantially benefits
libraries such as libxev. Zig will only include function calls and features
that you actually use. If you don't use a particular kind of high-level
watcher (such as UDP sockets), then the functionality related to that
abstraction is not compiled into your final binary at all. This lets libxev
support optional "nice-to-have" functionality that may be considered
"bloat" in some cases, but the end user doesn't have to pay for it.
**Dependency-free.** libxev has no dependencies other than the built-in
OS APIs at runtime. The C library depends on libc. This makes it very
easy to cross-compile.
### Roadmap
There are plenty of missing features that I still want to add:
* Pipe high-level API
* Signal handlers
* Filesystem events
* Windows backend
* Freestanding WebAssembly support via an external event loop (i.e. the browser)
And more...
### Performance
There is plenty of room for performance improvements, and I want to be
fully clear that I haven't done a lot of optimization work. Still,
performance is looking good. I've tried to port many of
[libuv benchmarks](https://github.com/libuv/libuv) to use the libxev
API.
I won't post specific benchmark results until I have a better
environment to run them in. As a _very broad generalization_,
you shouldn't notice a slowdown using libxev compared to other
major event loops. This may differ on a feature-by-feature basis, and
if you can show really poor performance in an issue I'm interested
in resolving it!
## Example
The example below shows an identical program written in Zig and in C
that uses libxev to run a single 5s timer. This is almost silly how
simple it is but is meant to just convey the overall feel of the library
rather than a practical use case.
<table>
<tr>
<td> Zig </td> <td> C </td>
</tr>
<tr>
<td>
```zig
const xev = @import("xev");
pub fn main() !void {
var loop = try xev.Loop.init(.{});
defer loop.deinit();
const w = try xev.Timer.init();
defer w.deinit();
// 5s timer
var c: xev.Completion = undefined;
w.run(&loop, &c, 5000, void, null, &timerCallback);
try loop.run(.until_done);
}
fn timerCallback(
userdata: ?*void,
loop: *xev.Loop,
c: *xev.Completion,
result: xev.Timer.RunError!void,
) xev.CallbackAction {
_ = userdata;
_ = loop;
_ = c;
_ = result catch unreachable;
return .disarm;
}
```
</td>
<td>
```zig
#include <stddef.h>
#include <stdio.h>
#include <xev.h>
xev_cb_action timerCallback(xev_loop* loop, xev_completion* c, int result, void *userdata) {
return XEV_DISARM;
}
int main(void) {
xev_loop loop;
if (xev_loop_init(&loop) != 0) {
printf("xev_loop_init failure\n");
return 1;
}
xev_watcher w;
if (xev_timer_init(&w) != 0) {
printf("xev_timer_init failure\n");
return 1;
}
xev_completion c;
xev_timer_run(&w, &loop, &c, 5000, NULL, &timerCallback);
xev_loop_run(&loop, XEV_RUN_UNTIL_DONE);
xev_timer_deinit(&w);
xev_loop_deinit(&loop);
return 0;
}
```
</td>
</tr>
</table>
## Installation (Zig)
**These instructions are for Zig downstream users only.** If you are
using the C API to libxev, see the "Build" section.
This package works with the Zig package manager introduced in Zig 0.11.
Create a `build.zig.zon` file like this:
```zig
.{
.name = "my-project",
.version = "0.0.0",
.dependencies = .{
.libxev = .{
.url = "https://github.com/mitchellh/libxev/archive/<git-ref-here>.tar.gz",
.hash = "12208070233b17de6be05e32af096a6760682b48598323234824def41789e993432c",
},
},
}
```
And in your `build.zig`:
```zig
const xev = b.dependency("libxev", .{ .target = target, .optimize = optimize });
exe.addModule("xev", xev.module("xev"));
```
## Documentation
🚧 Documentation is a work-in-progress. 🚧
Currently, documentation is available in three forms: **man pages**,
**examples**, and **code comments.** In the future, I plan on writing detailed
guides and API documentation in website form, but that isn't currently
available.
### Man Pages
The man pages are relatively detailed! `xev(7)` will
give you a good overview of the entire library. `xev-zig(7)` and
`xev-c(7)` will provide overviews of the Zig and C API, respectively.
From there, API-specifc man pages such as `xev_loop_init(3)` are
available. This is the best documentation currently.
There are multiple ways to browse the man pages. The most immediately friendly
is to just browse the raw man page sources in the `docs/` directory in
your web browser. The man page source is a _markdown-like_ syntax so it
renders _okay_ in your browser via GitHub.
Another approach is to run `zig build -Dman-pages` and the man pages
will be available in `zig-out`. This requires
[scdoc](https://git.sr.ht/~sircmpwn/scdoc)
to be installed (this is available in most package managers).
Once you've built the man pages, you can render them by path:
```
$ man zig-out/share/man/man7/xev.7
```
And the final approach is to install libxev via your favorite package
manager (if and when available), which should hopefully put your man pages
into your man path, so you can just do `man 7 xev`.
### Examples
There are examples available in the `examples/` folder. The examples are
available in both C and Zig, and you can tell which one is which using
the file extension.
To build an example, use the following:
```
$ zig build -Dexample-name=_basic.zig
...
$ zig-out/bin/example-basic
...
```
The `-Dexample-name` value should be the filename including the extension.
### Code Comments
The Zig code is well commented. If you're comfortable reading code comments
you can find a lot of insight within them. The source is in the `src/`
directory.
# Build
Build requires the installation of the latest [Zig nightly](https://ziglang.org/download/).
**libxev has no other build dependencies.**
Once installed, `zig build install` on its own will build the full library and output
a [FHS-compatible](https://en.wikipedia.org/wiki/Filesystem_Hierarchy_Standard)
directory in `zig-out`. You can customize the output directory with the
`--prefix` flag.
## Tests
libxev has a large and growing test suite. To run the tests for the current
platform:
```sh
$ zig build test
...
```
This will run all the tests for all the supported features for the current
host platform. For example, on Linux this will run both the full io_uring
and epoll test suite.
**You can build and run tests for other platforms** by cross-compiling the
test executable, copying it to a target machine and executing it. For example,
the below shows how to cross-compile and build the tests for macOS from Linux:
```sh
$ zig build -Dtarget=aarch64-macos -Dinstall-tests
...
$ file zig-out/bin/xev-test
zig-out/bin/xev-test: Mach-O 64-bit arm64 executable
```
**WASI is a special-case.** You can run tests for WASI if you have
[wasmtime](https://wasmtime.dev/) installed:
```
$ zig build test -Dtarget=wasm32-wasi -Dwasmtime
...
```

348
deps/libxev/build.zig vendored Normal file
View File

@@ -0,0 +1,348 @@
const std = @import("std");
const CompileStep = std.build.Step.Compile;
const ScdocStep = @import("src/build/ScdocStep.zig");
pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
_ = b.addModule("xev", .{ .root_source_file = b.path("src/main.zig") });
const man_pages = b.option(
bool,
"man-pages",
"Set to true to build man pages. Requires scdoc. Defaults to true if scdoc is found.",
) orelse if (b.findProgram(&[_][]const u8{"scdoc"}, &[_][]const u8{})) |_|
true
else |err| switch (err) {
error.FileNotFound => false,
else => return err,
};
const bench_name = b.option(
[]const u8,
"bench-name",
"Build and install a single benchmark",
);
const bench_install = b.option(
bool,
"bench",
"Install the benchmark binaries to zig-out/bench",
) orelse (bench_name != null);
const example_name = b.option(
[]const u8,
"example-name",
"Build and install a single example",
);
const example_install = b.option(
bool,
"example",
"Install the example binaries to zig-out/example",
) orelse (example_name != null);
const test_install = b.option(
bool,
"install-tests",
"Install the test binaries into zig-out",
) orelse false;
// Our tests require libc on Linux and Mac. Note that libxev itself
// does NOT require libc.
const test_libc = switch (target.result.os.tag) {
.linux, .macos => true,
else => false,
};
// We always build our test exe as part of `zig build` so that
// we can easily run it manually without digging through the cache.
const test_exe = b.addTest(.{
.name = "xev-test",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
if (test_libc) test_exe.linkLibC(); // Tests depend on libc, libxev does not
if (test_install) b.installArtifact(test_exe);
// zig build test test binary and runner.
const tests_run = b.addRunArtifact(test_exe);
const test_step = b.step("test", "Run tests");
test_step.dependOn(&tests_run.step);
// Static C lib
const static_c_lib: ?*std.Build.Step.Compile = if (target.result.os.tag != .wasi) lib: {
const static_lib = b.addStaticLibrary(.{
.name = "xev",
.root_source_file = b.path("src/c_api.zig"),
.target = target,
.optimize = optimize,
});
static_lib.linkLibC();
// Link required libraries if targeting Windows
if (target.result.os.tag == .windows) {
static_lib.linkSystemLibrary("ws2_32");
static_lib.linkSystemLibrary("mswsock");
}
b.installArtifact(static_lib);
b.default_step.dependOn(&static_lib.step);
const static_binding_test = b.addExecutable(.{
.name = "static-binding-test",
.target = target,
.optimize = optimize,
});
static_binding_test.linkLibC();
static_binding_test.addIncludePath(b.path("include"));
static_binding_test.addCSourceFile(.{
.file = b.path("examples/_basic.c"),
.flags = &[_][]const u8{ "-Wall", "-Wextra", "-pedantic", "-std=c99", "-D_POSIX_C_SOURCE=199309L" },
});
static_binding_test.linkLibrary(static_lib);
if (test_install) b.installArtifact(static_binding_test);
const static_binding_test_run = b.addRunArtifact(static_binding_test);
test_step.dependOn(&static_binding_test_run.step);
break :lib static_lib;
} else null;
// Dynamic C lib. We only build this if this is the native target so we
// can link to libxml2 on our native system.
if (target.query.isNative()) {
const dynamic_lib_name = "xev";
const dynamic_lib = b.addSharedLibrary(.{
.name = dynamic_lib_name,
.root_source_file = b.path("src/c_api.zig"),
.target = target,
.optimize = optimize,
});
b.installArtifact(dynamic_lib);
b.default_step.dependOn(&dynamic_lib.step);
const dynamic_binding_test = b.addExecutable(.{
.name = "dynamic-binding-test",
.target = target,
.optimize = optimize,
});
dynamic_binding_test.linkLibC();
dynamic_binding_test.addIncludePath(b.path("include"));
dynamic_binding_test.addCSourceFile(.{
.file = b.path("examples/_basic.c"),
.flags = &[_][]const u8{ "-Wall", "-Wextra", "-pedantic", "-std=c99" },
});
dynamic_binding_test.linkLibrary(dynamic_lib);
if (test_install) b.installArtifact(dynamic_binding_test);
const dynamic_binding_test_run = b.addRunArtifact(dynamic_binding_test);
test_step.dependOn(&dynamic_binding_test_run.step);
}
// C Headers
const c_header = b.addInstallFileWithDir(
b.path("include/xev.h"),
.header,
"xev.h",
);
b.getInstallStep().dependOn(&c_header.step);
// pkg-config
{
const file = try b.cache_root.join(b.allocator, &[_][]const u8{"libxev.pc"});
const pkgconfig_file = try std.fs.cwd().createFile(file, .{});
const writer = pkgconfig_file.writer();
try writer.print(
\\prefix={s}
\\includedir=${{prefix}}/include
\\libdir=${{prefix}}/lib
\\
\\Name: libxev
\\URL: https://github.com/mitchellh/libxev
\\Description: High-performance, cross-platform event loop
\\Version: 0.1.0
\\Cflags: -I${{includedir}}
\\Libs: -L${{libdir}} -lxev
, .{b.install_prefix});
defer pkgconfig_file.close();
b.getInstallStep().dependOn(&b.addInstallFileWithDir(
.{ .cwd_relative = file },
.prefix,
"share/pkgconfig/libxev.pc",
).step);
}
// Benchmarks
_ = try benchTargets(b, target, optimize, bench_install, bench_name);
// Examples
_ = try exampleTargets(b, target, optimize, static_c_lib, example_install, example_name);
// Man pages
if (man_pages) {
const scdoc_step = ScdocStep.create(b);
try scdoc_step.install();
}
}
fn benchTargets(
b: *std.Build,
target: std.Build.ResolvedTarget,
mode: std.builtin.OptimizeMode,
install: bool,
install_name: ?[]const u8,
) !std.StringHashMap(*std.Build.Step.Compile) {
_ = mode;
var map = std.StringHashMap(*std.Build.Step.Compile).init(b.allocator);
// Open the directory
const c_dir_path = "src/bench";
var c_dir = try std.fs.cwd().openDir(comptime thisDir() ++ "/" ++ c_dir_path, .{ .iterate = true });
defer c_dir.close();
// Go through and add each as a step
var c_dir_it = c_dir.iterate();
while (try c_dir_it.next()) |entry| {
// Get the index of the last '.' so we can strip the extension.
const index = std.mem.lastIndexOfScalar(u8, entry.name, '.') orelse continue;
if (index == 0) continue;
// Name of the app and full path to the entrypoint.
const name = entry.name[0..index];
const path = try std.fs.path.join(b.allocator, &[_][]const u8{
c_dir_path,
entry.name,
});
// If we have specified a specific name, only install that one.
if (install_name) |n| {
if (!std.mem.eql(u8, n, name)) continue;
}
// Executable builder.
const c_exe = b.addExecutable(.{
.name = name,
.root_source_file = b.path(path),
.target = target,
.optimize = .ReleaseFast, // benchmarks are always release fast
});
c_exe.root_module.addImport("xev", b.modules.get("xev").?);
if (install) {
const install_step = b.addInstallArtifact(c_exe, .{
.dest_dir = .{ .override = .{ .custom = "bench" } },
});
b.getInstallStep().dependOn(&install_step.step);
}
// Store the mapping
try map.put(try b.allocator.dupe(u8, name), c_exe);
}
return map;
}
fn exampleTargets(
b: *std.Build,
target: std.Build.ResolvedTarget,
optimize: std.builtin.OptimizeMode,
c_lib_: ?*std.Build.Step.Compile,
install: bool,
install_name: ?[]const u8,
) !void {
// Ignore if we're not installing
if (!install) return;
// Open the directory
const c_dir_path = (comptime thisDir()) ++ "/examples";
var c_dir = try std.fs.cwd().openDir(c_dir_path, .{ .iterate = true });
defer c_dir.close();
// Go through and add each as a step
var c_dir_it = c_dir.iterate();
while (try c_dir_it.next()) |entry| {
// Get the index of the last '.' so we can strip the extension.
const index = std.mem.lastIndexOfScalar(u8, entry.name, '.') orelse continue;
if (index == 0) continue;
// If we have specified a specific name, only install that one.
if (install_name) |n| {
if (!std.mem.eql(u8, n, entry.name)) continue;
}
// Name of the app and full path to the entrypoint.
const name = entry.name[0..index];
const path = try std.fs.path.join(b.allocator, &[_][]const u8{
c_dir_path,
entry.name,
});
const is_zig = std.mem.eql(u8, entry.name[index + 1 ..], "zig");
if (is_zig) {
const c_exe = b.addExecutable(.{
.name = name,
.root_source_file = .{ .cwd_relative = path },
.target = target,
.optimize = optimize,
});
c_exe.root_module.addImport("xev", b.modules.get("xev").?);
if (install) {
const install_step = b.addInstallArtifact(c_exe, .{
.dest_dir = .{ .override = .{ .custom = "example" } },
});
b.getInstallStep().dependOn(&install_step.step);
}
} else {
const c_lib = c_lib_ orelse return error.UnsupportedPlatform;
const c_exe = b.addExecutable(.{
.name = name,
.target = target,
.optimize = optimize,
});
c_exe.linkLibC();
c_exe.addIncludePath(b.path("include"));
c_exe.addCSourceFile(.{
.file = .{ .cwd_relative = path },
.flags = &[_][]const u8{
"-Wall",
"-Wextra",
"-pedantic",
"-std=c99",
"-D_POSIX_C_SOURCE=199309L",
},
});
c_exe.linkLibrary(c_lib);
if (install) {
const install_step = b.addInstallArtifact(c_exe, .{
.dest_dir = .{ .override = .{ .custom = "example" } },
});
b.getInstallStep().dependOn(&install_step.step);
}
}
// If we have specified a specific name, only install that one.
if (install_name) |_| break;
} else {
if (install_name) |n| {
std.debug.print("No example file named: {s}\n", .{n});
std.debug.print("Choices:\n", .{});
var c_dir_it2 = c_dir.iterate();
while (try c_dir_it2.next()) |entry| {
std.debug.print("\t{s}\n", .{entry.name});
}
return error.InvalidExampleName;
}
}
}
/// Path to the directory with the build.zig.
fn thisDir() []const u8 {
return std.fs.path.dirname(@src().file) orelse unreachable;
}

6
deps/libxev/build.zig.zon vendored Normal file
View File

@@ -0,0 +1,6 @@
.{
.name = "libxev",
.minimum_zig_version = "0.12.0-dev.3191+9cf28d1e9",
.paths = .{""},
.version = "0.0.0",
}

103
deps/libxev/include/xev.h vendored Normal file
View File

@@ -0,0 +1,103 @@
#ifndef XEV_H
#define XEV_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#include <stddef.h>
/* TODO(mitchellh): we should use platform detection to set the correct
* byte sizes here. We choose some overly large values for now so that
* we can retain ABI compatibility. */
const size_t XEV_SIZEOF_LOOP = 512;
const size_t XEV_SIZEOF_COMPLETION = 320;
const size_t XEV_SIZEOF_WATCHER = 256;
const size_t XEV_SIZEOF_THREADPOOL = 64;
const size_t XEV_SIZEOF_THREADPOOL_BATCH = 24;
const size_t XEV_SIZEOF_THREADPOOL_TASK = 24;
const size_t XEV_SIZEOF_THREADPOOL_CONFIG = 64;
#if __STDC_VERSION__ >= 201112L || __cplusplus >= 201103L
typedef max_align_t XEV_ALIGN_T;
#else
// 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.
typedef long double XEV_ALIGN_T;
#endif
/* There's a ton of preprocessor directives missing here for real cross-platform
* compatibility. I'm going to defer to the community or future issues to help
* plug those holes. For now, we get some stuff working we can test! */
/* Opaque types. These types have a size defined so that they can be
* statically allocated but they are not to be accessed. */
// todo: give struct individual alignment, instead of max alignment
typedef struct { XEV_ALIGN_T _pad; uint8_t data[XEV_SIZEOF_LOOP - sizeof(XEV_ALIGN_T)]; } xev_loop;
typedef struct { XEV_ALIGN_T _pad; uint8_t data[XEV_SIZEOF_COMPLETION - sizeof(XEV_ALIGN_T)]; } xev_completion;
typedef struct { XEV_ALIGN_T _pad; uint8_t data[XEV_SIZEOF_WATCHER - sizeof(XEV_ALIGN_T)]; } xev_watcher;
typedef struct { XEV_ALIGN_T _pad; uint8_t data[XEV_SIZEOF_THREADPOOL - sizeof(XEV_ALIGN_T)]; } xev_threadpool;
typedef struct { XEV_ALIGN_T _pad; uint8_t data[XEV_SIZEOF_THREADPOOL_BATCH - sizeof(XEV_ALIGN_T)]; } xev_threadpool_batch;
typedef struct { XEV_ALIGN_T _pad; uint8_t data[XEV_SIZEOF_THREADPOOL_TASK - sizeof(XEV_ALIGN_T)]; } xev_threadpool_task;
typedef struct { XEV_ALIGN_T _pad; uint8_t data[XEV_SIZEOF_THREADPOOL_CONFIG - sizeof(XEV_ALIGN_T)]; } xev_threadpool_config;
/* Callback types. */
typedef enum { XEV_DISARM = 0, XEV_REARM = 1 } xev_cb_action;
typedef void (*xev_task_cb)(xev_threadpool_task* t);
typedef xev_cb_action (*xev_timer_cb)(xev_loop* l, xev_completion* c, int result, void* userdata);
typedef xev_cb_action (*xev_async_cb)(xev_loop* l, xev_completion* c, int result, void* userdata);
typedef enum {
XEV_RUN_NO_WAIT = 0,
XEV_RUN_ONCE = 1,
XEV_RUN_UNTIL_DONE = 2,
} xev_run_mode_t;
typedef enum {
XEV_COMPLETION_DEAD = 0,
XEV_COMPLETION_ACTIVE = 1,
} xev_completion_state_t;
/* Documentation for functions can be found in man pages or online. I
* purposely do not add docs to the header so that you can quickly scan
* all exported functions. */
int xev_loop_init(xev_loop* loop);
void xev_loop_deinit(xev_loop* loop);
int xev_loop_run(xev_loop* loop, xev_run_mode_t mode);
int64_t xev_loop_now(xev_loop* loop);
void xev_loop_update_now(xev_loop* loop);
void xev_completion_zero(xev_completion* c);
xev_completion_state_t xev_completion_state(xev_completion* c);
void xev_threadpool_config_init(xev_threadpool_config* config);
void xev_threadpool_config_set_stack_size(xev_threadpool_config* config, uint32_t v);
void xev_threadpool_config_set_max_threads(xev_threadpool_config* config, uint32_t v);
int xev_threadpool_init(xev_threadpool* pool, xev_threadpool_config* config);
void xev_threadpool_deinit(xev_threadpool* pool);
void xev_threadpool_shutdown(xev_threadpool* pool);
void xev_threadpool_schedule(xev_threadpool* pool, xev_threadpool_batch *batch);
void xev_threadpool_task_init(xev_threadpool_task* t, xev_task_cb cb);
void xev_threadpool_batch_init(xev_threadpool_batch* b);
void xev_threadpool_batch_push_task(xev_threadpool_batch* b, xev_threadpool_task *t);
void xev_threadpool_batch_push_batch(xev_threadpool_batch* b, xev_threadpool_batch *other);
int xev_timer_init(xev_watcher *w);
void xev_timer_deinit(xev_watcher *w);
void xev_timer_run(xev_watcher *w, xev_loop* loop, xev_completion* c, uint64_t next_ms, void* userdata, xev_timer_cb cb);
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);
void xev_timer_cancel(xev_watcher *w, xev_loop* loop, xev_completion* c, xev_completion* c_cancel, void* userdata, xev_timer_cb cb);
int xev_async_init(xev_watcher *w);
void xev_async_deinit(xev_watcher *w);
int xev_async_notify(xev_watcher *w);
void xev_async_wait(xev_watcher *w, xev_loop* loop, xev_completion* c, void* userdata, xev_async_cb cb);
#ifdef __cplusplus
}
#endif
#endif /* XEV_H */

829
deps/libxev/src/ThreadPool.zig vendored Normal file
View File

@@ -0,0 +1,829 @@
//! Thread pool copied almost directly from Zap[1]. In @kprotty's own words:
//! lock-free, allocation-free* (excluding spawning threads), supports batch
//! scheduling, and dynamically spawns threads while handling thread spawn
//! failure. I highly recommend reading @kprotty's incredible blog post[2] on
//! this topic.
//!
//! The original file in Zap is licensed under the MIT license, and the
//! license and copyright is reproduced below. The libxev project is also
//! MIT licensed so the entire project (including this file) are equally
//! licensed. This is just a convenience note for any OSS users, contributors,
//! etc.
//!
//! MIT License
//!
//! Copyright (c) 2021 kprotty
//!
//! Permission is hereby granted, free of charge, to any person obtaining a copy
//! of this software and associated documentation files (the "Software"), to deal
//! in the Software without restriction, including without limitation the rights
//! to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//! copies of the Software, and to permit persons to whom the Software is
//! furnished to do so, subject to the following conditions:
//!
//! The above copyright notice and this permission notice shall be included in all
//! copies or substantial portions of the Software.
//!
//! THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//! IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//! FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//! AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//! LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//! OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
//! SOFTWARE.
//!
//! [1]: https://github.com/kprotty/zap
//! [2]: https://zig.news/kprotty/resource-efficient-thread-pools-with-zig-3291
const ThreadPool = @This();
const std = @import("std");
const assert = std.debug.assert;
const Atomic = std.atomic.Value;
stack_size: u32,
max_threads: u32,
sync: Atomic(u32) = Atomic(u32).init(@bitCast(Sync{})),
idle_event: Event = .{},
join_event: Event = .{},
run_queue: Node.Queue = .{},
threads: Atomic(?*Thread) = Atomic(?*Thread).init(null),
const Sync = packed struct {
/// Tracks the number of threads not searching for Tasks
idle: u14 = 0,
/// Tracks the number of threads spawned
spawned: u14 = 0,
/// What you see is what you get
unused: bool = false,
/// Used to not miss notifications while state = waking
notified: bool = false,
/// The current state of the thread pool
state: enum(u2) {
/// A notification can be issued to wake up a sleeping as the "waking thread".
pending = 0,
/// The state was notifiied with a signal. A thread is woken up.
/// The first thread to transition to `waking` becomes the "waking thread".
signaled,
/// There is a "waking thread" among us.
/// No other thread should be woken up until the waking thread transitions the state.
waking,
/// The thread pool was terminated. Start decremented `spawned` so that it can be joined.
shutdown,
} = .pending,
};
/// Configuration options for the thread pool.
/// TODO: add CPU core affinity?
pub const Config = struct {
stack_size: u32 = (std.Thread.SpawnConfig{}).stack_size,
max_threads: u32 = 0,
};
/// Statically initialize the thread pool using the configuration.
pub fn init(config: Config) ThreadPool {
return .{
.stack_size = @max(1, config.stack_size),
.max_threads = if (config.max_threads > 0)
config.max_threads
else
@intCast(std.Thread.getCpuCount() catch 1),
};
}
/// Wait for a thread to call shutdown() on the thread pool and kill the worker threads.
pub fn deinit(self: *ThreadPool) void {
self.join();
self.* = undefined;
}
/// A Task represents the unit of Work / Job / Execution that the ThreadPool schedules.
/// The user provides a `callback` which is invoked when the *Task can run on a thread.
pub const Task = struct {
node: Node = .{},
callback: *const fn (*Task) void,
};
/// An unordered collection of Tasks which can be submitted for scheduling as a group.
pub const Batch = struct {
len: usize = 0,
head: ?*Task = null,
tail: ?*Task = null,
/// Create a batch from a single task.
pub fn from(task: *Task) Batch {
return Batch{
.len = 1,
.head = task,
.tail = task,
};
}
/// Another batch into this one, taking ownership of its tasks.
pub fn push(self: *Batch, batch: Batch) void {
if (batch.len == 0) return;
if (self.len == 0) {
self.* = batch;
} else {
self.tail.?.node.next = if (batch.head) |h| &h.node else null;
self.tail = batch.tail;
self.len += batch.len;
}
}
};
/// Schedule a batch of tasks to be executed by some thread on the thread pool.
pub fn schedule(self: *ThreadPool, batch: Batch) void {
// Sanity check
if (batch.len == 0) {
return;
}
// Extract out the Node's from the Tasks
var list = Node.List{
.head = &batch.head.?.node,
.tail = &batch.tail.?.node,
};
// Push the task Nodes to the most approriate queue
if (Thread.current) |thread| {
thread.run_buffer.push(&list) catch thread.run_queue.push(list);
} else {
self.run_queue.push(list);
}
// Try to notify a thread
const is_waking = false;
return self.notify(is_waking);
}
inline fn notify(self: *ThreadPool, is_waking: bool) void {
// Fast path to check the Sync state to avoid calling into notifySlow().
// If we're waking, then we need to update the state regardless
if (!is_waking) {
const sync: Sync = @bitCast(self.sync.load(.monotonic));
if (sync.notified) {
return;
}
}
return self.notifySlow(is_waking);
}
noinline fn notifySlow(self: *ThreadPool, is_waking: bool) void {
var sync: Sync = @bitCast(self.sync.load(.monotonic));
while (sync.state != .shutdown) {
const can_wake = is_waking or (sync.state == .pending);
if (is_waking) {
assert(sync.state == .waking);
}
var new_sync = sync;
new_sync.notified = true;
if (can_wake and sync.idle > 0) { // wake up an idle thread
new_sync.state = .signaled;
} else if (can_wake and sync.spawned < self.max_threads) { // spawn a new thread
new_sync.state = .signaled;
new_sync.spawned += 1;
} else if (is_waking) { // no other thread to pass on "waking" status
new_sync.state = .pending;
} else if (sync.notified) { // nothing to update
return;
}
// Release barrier synchronizes with Acquire in wait()
// to ensure pushes to run queues happen before observing a posted notification.
sync = @bitCast(self.sync.cmpxchgWeak(
@bitCast(sync),
@bitCast(new_sync),
.release,
.monotonic,
) orelse {
// We signaled to notify an idle thread
if (can_wake and sync.idle > 0) {
return self.idle_event.notify();
}
// We signaled to spawn a new thread
if (can_wake and sync.spawned < self.max_threads) {
const spawn_config = std.Thread.SpawnConfig{ .stack_size = self.stack_size };
const thread = std.Thread.spawn(spawn_config, Thread.run, .{self}) catch return self.unregister(null);
return thread.detach();
}
return;
});
}
}
noinline fn wait(self: *ThreadPool, _is_waking: bool) error{Shutdown}!bool {
var is_idle = false;
var is_waking = _is_waking;
var sync: Sync = @bitCast(self.sync.load(.monotonic));
while (true) {
if (sync.state == .shutdown) return error.Shutdown;
if (is_waking) assert(sync.state == .waking);
// Consume a notification made by notify().
if (sync.notified) {
var new_sync = sync;
new_sync.notified = false;
if (is_idle)
new_sync.idle -= 1;
if (sync.state == .signaled)
new_sync.state = .waking;
// Acquire barrier synchronizes with notify()
// to ensure that pushes to run queue are observed after wait() returns.
sync = @bitCast(self.sync.cmpxchgWeak(
@bitCast(sync),
@bitCast(new_sync),
.acquire,
.monotonic,
) orelse {
return is_waking or (sync.state == .signaled);
});
// No notification to consume.
// Mark this thread as idle before sleeping on the idle_event.
} else if (!is_idle) {
var new_sync = sync;
new_sync.idle += 1;
if (is_waking)
new_sync.state = .pending;
sync = @bitCast(self.sync.cmpxchgWeak(
@bitCast(sync),
@bitCast(new_sync),
.monotonic,
.monotonic,
) orelse {
is_waking = false;
is_idle = true;
continue;
});
// Wait for a signal by either notify() or shutdown() without wasting cpu cycles.
// TODO: Add I/O polling here.
} else {
self.idle_event.wait();
sync = @bitCast(self.sync.load(.monotonic));
}
}
}
/// Marks the thread pool as shutdown
pub noinline fn shutdown(self: *ThreadPool) void {
var sync: Sync = @bitCast(self.sync.load(.monotonic));
while (sync.state != .shutdown) {
var new_sync = sync;
new_sync.notified = true;
new_sync.state = .shutdown;
new_sync.idle = 0;
// Full barrier to synchronize with both wait() and notify()
sync = @bitCast(self.sync.cmpxchgWeak(
@bitCast(sync),
@bitCast(new_sync),
.acq_rel,
.monotonic,
) orelse {
// Wake up any threads sleeping on the idle_event.
// TODO: I/O polling notification here.
if (sync.idle > 0) self.idle_event.shutdown();
return;
});
}
}
fn register(noalias self: *ThreadPool, noalias thread: *Thread) void {
// Push the thread onto the threads stack in a lock-free manner.
var threads = self.threads.load(.monotonic);
while (true) {
thread.next = threads;
threads = self.threads.cmpxchgWeak(
threads,
thread,
.release,
.monotonic,
) orelse break;
}
}
fn unregister(noalias self: *ThreadPool, noalias maybe_thread: ?*Thread) void {
// Un-spawn one thread, either due to a failed OS thread spawning or the thread is exitting.
const one_spawned: u32 = @bitCast(Sync{ .spawned = 1 });
const sync: Sync = @bitCast(self.sync.fetchSub(one_spawned, .release));
assert(sync.spawned > 0);
// The last thread to exit must wake up the thread pool join()er
// who will start the chain to shutdown all the threads.
if (sync.state == .shutdown and sync.spawned == 1) {
self.join_event.notify();
}
// If this is a thread pool thread, wait for a shutdown signal by the thread pool join()er.
const thread = maybe_thread orelse return;
thread.join_event.wait();
// After receiving the shutdown signal, shutdown the next thread in the pool.
// We have to do that without touching the thread pool itself since it's memory is invalidated by now.
// So just follow our .next link.
const next_thread = thread.next orelse return;
next_thread.join_event.notify();
}
fn join(self: *ThreadPool) void {
// Wait for the thread pool to be shutdown() then for all threads to enter a joinable state
var sync: Sync = @bitCast(self.sync.load(.monotonic));
if (!(sync.state == .shutdown and sync.spawned == 0)) {
self.join_event.wait();
sync = @bitCast(self.sync.load(.monotonic));
}
assert(sync.state == .shutdown);
assert(sync.spawned == 0);
// If there are threads, start off the chain sending it the shutdown signal.
// The thread receives the shutdown signal and sends it to the next thread, and the next..
const thread = self.threads.load(.acquire) orelse return;
thread.join_event.notify();
}
const Thread = struct {
next: ?*Thread = null,
target: ?*Thread = null,
join_event: Event = .{},
run_queue: Node.Queue = .{},
run_buffer: Node.Buffer = .{},
threadlocal var current: ?*Thread = null;
/// Thread entry point which runs a worker for the ThreadPool
fn run(thread_pool: *ThreadPool) void {
var self = Thread{};
current = &self;
thread_pool.register(&self);
defer thread_pool.unregister(&self);
var is_waking = false;
while (true) {
is_waking = thread_pool.wait(is_waking) catch return;
while (self.pop(thread_pool)) |result| {
if (result.pushed or is_waking)
thread_pool.notify(is_waking);
is_waking = false;
const task: *Task = @fieldParentPtr("node", result.node);
(task.callback)(task);
}
}
}
/// Try to dequeue a Node/Task from the ThreadPool.
/// Spurious reports of dequeue() returning empty are allowed.
fn pop(noalias self: *Thread, noalias thread_pool: *ThreadPool) ?Node.Buffer.Stole {
// Check our local buffer first
if (self.run_buffer.pop()) |node| {
return Node.Buffer.Stole{
.node = node,
.pushed = false,
};
}
// Then check our local queue
if (self.run_buffer.consume(&self.run_queue)) |stole| {
return stole;
}
// Then the global queue
if (self.run_buffer.consume(&thread_pool.run_queue)) |stole| {
return stole;
}
// TODO: add optimistic I/O polling here
// Then try work stealing from other threads
var num_threads: u32 = @as(Sync, @bitCast(thread_pool.sync.load(.monotonic))).spawned;
while (num_threads > 0) : (num_threads -= 1) {
// Traverse the stack of registered threads on the thread pool
const target = self.target orelse thread_pool.threads.load(.acquire) orelse unreachable;
self.target = target.next;
// Try to steal from their queue first to avoid contention (the target steal's from queue last).
if (self.run_buffer.consume(&target.run_queue)) |stole| {
return stole;
}
// Skip stealing from the buffer if we're the target.
// We still steal from our own queue above given it may have just been locked the first time we tried.
if (target == self) {
continue;
}
// Steal from the buffer of a remote thread as a last resort
if (self.run_buffer.steal(&target.run_buffer)) |stole| {
return stole;
}
}
return null;
}
};
/// An event which stores 1 semaphore token and is multi-threaded safe.
/// The event can be shutdown(), waking up all wait()ing threads and
/// making subsequent wait()'s return immediately.
const Event = struct {
state: Atomic(u32) = Atomic(u32).init(EMPTY),
const EMPTY = 0;
const WAITING = 1;
const NOTIFIED = 2;
const SHUTDOWN = 3;
/// Wait for and consume a notification
/// or wait for the event to be shutdown entirely
noinline fn wait(self: *Event) void {
var acquire_with: u32 = EMPTY;
var state = self.state.load(.monotonic);
while (true) {
// If we're shutdown then exit early.
// Acquire barrier to ensure operations before the shutdown() are seen after the wait().
// Shutdown is rare so it's better to have an Acquire barrier here instead of on CAS failure + load which are common.
if (state == SHUTDOWN) {
@fence(.acquire);
return;
}
// Consume a notification when it pops up.
// Acquire barrier to ensure operations before the notify() appear after the wait().
if (state == NOTIFIED) {
state = self.state.cmpxchgWeak(
state,
acquire_with,
.acquire,
.monotonic,
) orelse return;
continue;
}
// There is no notification to consume, we should wait on the event by ensuring its WAITING.
if (state != WAITING) blk: {
state = self.state.cmpxchgWeak(
state,
WAITING,
.monotonic,
.monotonic,
) orelse break :blk;
continue;
}
// Wait on the event until a notify() or shutdown().
// If we wake up to a notification, we must acquire it with WAITING instead of EMPTY
// since there may be other threads sleeping on the Futex who haven't been woken up yet.
//
// Acquiring to WAITING will make the next notify() or shutdown() wake a sleeping futex thread
// who will either exit on SHUTDOWN or acquire with WAITING again, ensuring all threads are awoken.
// This unfortunately results in the last notify() or shutdown() doing an extra futex wake but that's fine.
std.Thread.Futex.wait(&self.state, WAITING);
state = self.state.load(.monotonic);
acquire_with = WAITING;
}
}
/// Post a notification to the event if it doesn't have one already
/// then wake up a waiting thread if there is one as well.
fn notify(self: *Event) void {
return self.wake(NOTIFIED, 1);
}
/// Marks the event as shutdown, making all future wait()'s return immediately.
/// Then wakes up any threads currently waiting on the Event.
fn shutdown(self: *Event) void {
return self.wake(SHUTDOWN, std.math.maxInt(u32));
}
fn wake(self: *Event, release_with: u32, wake_threads: u32) void {
// Update the Event to notifty it with the new `release_with` state (either NOTIFIED or SHUTDOWN).
// Release barrier to ensure any operations before this are this to happen before the wait() in the other threads.
const state = self.state.swap(release_with, .release);
// Only wake threads sleeping in futex if the state is WAITING.
// Avoids unnecessary wake ups.
if (state == WAITING) {
std.Thread.Futex.wake(&self.state, wake_threads);
}
}
};
/// Linked list intrusive memory node and lock-free data structures to operate with it
const Node = struct {
next: ?*Node = null,
/// A linked list of Nodes
const List = struct {
head: *Node,
tail: *Node,
};
/// An unbounded multi-producer-(non blocking)-multi-consumer queue of Node pointers.
const Queue = struct {
stack: Atomic(usize) = Atomic(usize).init(0),
cache: ?*Node = null,
const HAS_CACHE: usize = 0b01;
const IS_CONSUMING: usize = 0b10;
const PTR_MASK: usize = ~(HAS_CACHE | IS_CONSUMING);
comptime {
assert(@alignOf(Node) >= ((IS_CONSUMING | HAS_CACHE) + 1));
}
fn push(noalias self: *Queue, list: List) void {
var stack = self.stack.load(.monotonic);
while (true) {
// Attach the list to the stack (pt. 1)
list.tail.next = @ptrFromInt(stack & PTR_MASK);
// Update the stack with the list (pt. 2).
// Don't change the HAS_CACHE and IS_CONSUMING bits of the consumer.
var new_stack = @intFromPtr(list.head);
assert(new_stack & ~PTR_MASK == 0);
new_stack |= (stack & ~PTR_MASK);
// Push to the stack with a release barrier for the consumer to see the proper list links.
stack = self.stack.cmpxchgWeak(
stack,
new_stack,
.release,
.monotonic,
) orelse break;
}
}
fn tryAcquireConsumer(self: *Queue) error{ Empty, Contended }!?*Node {
var stack = self.stack.load(.monotonic);
while (true) {
if (stack & IS_CONSUMING != 0)
return error.Contended; // The queue already has a consumer.
if (stack & (HAS_CACHE | PTR_MASK) == 0)
return error.Empty; // The queue is empty when there's nothing cached and nothing in the stack.
// When we acquire the consumer, also consume the pushed stack if the cache is empty.
var new_stack = stack | HAS_CACHE | IS_CONSUMING;
if (stack & HAS_CACHE == 0) {
assert(stack & PTR_MASK != 0);
new_stack &= ~PTR_MASK;
}
// Acquire barrier on getting the consumer to see cache/Node updates done by previous consumers
// and to ensure our cache/Node updates in pop() happen after that of previous consumers.
stack = self.stack.cmpxchgWeak(
stack,
new_stack,
.acquire,
.monotonic,
) orelse return self.cache orelse @ptrFromInt(stack & PTR_MASK);
}
}
fn releaseConsumer(noalias self: *Queue, noalias consumer: ?*Node) void {
// Stop consuming and remove the HAS_CACHE bit as well if the consumer's cache is empty.
// When HAS_CACHE bit is zeroed, the next consumer will acquire the pushed stack nodes.
var remove = IS_CONSUMING;
if (consumer == null)
remove |= HAS_CACHE;
// Release the consumer with a release barrier to ensure cache/node accesses
// happen before the consumer was released and before the next consumer starts using the cache.
self.cache = consumer;
const stack = self.stack.fetchSub(remove, .release);
assert(stack & remove != 0);
}
fn pop(noalias self: *Queue, noalias consumer_ref: *?*Node) ?*Node {
// Check the consumer cache (fast path)
if (consumer_ref.*) |node| {
consumer_ref.* = node.next;
return node;
}
// Load the stack to see if there was anything pushed that we could grab.
var stack = self.stack.load(.monotonic);
assert(stack & IS_CONSUMING != 0);
if (stack & PTR_MASK == 0) {
return null;
}
// Nodes have been pushed to the stack, grab then with an Acquire barrier to see the Node links.
stack = self.stack.swap(HAS_CACHE | IS_CONSUMING, .acquire);
assert(stack & IS_CONSUMING != 0);
assert(stack & PTR_MASK != 0);
const node: *Node = @ptrFromInt(stack & PTR_MASK);
consumer_ref.* = node.next;
return node;
}
};
/// A bounded single-producer, multi-consumer ring buffer for node pointers.
const Buffer = struct {
head: Atomic(Index) = Atomic(Index).init(0),
tail: Atomic(Index) = Atomic(Index).init(0),
array: [capacity]Atomic(*Node) = undefined,
const Index = u32;
const capacity = 256; // Appears to be a pretty good trade-off in space vs contended throughput
comptime {
assert(std.math.maxInt(Index) >= capacity);
assert(std.math.isPowerOfTwo(capacity));
}
fn push(noalias self: *Buffer, noalias list: *List) error{Overflow}!void {
var head = self.head.load(.monotonic);
var tail = self.tail.raw; // we're the only thread that can change this
while (true) {
var size = tail -% head;
assert(size <= capacity);
// Push nodes from the list to the buffer if it's not empty..
if (size < capacity) {
var nodes: ?*Node = list.head;
while (size < capacity) : (size += 1) {
const node = nodes orelse break;
nodes = node.next;
// Array written atomically with weakest ordering since it could be getting atomically read by steal().
self.array[tail % capacity].store(node, .unordered);
tail +%= 1;
}
// Release barrier synchronizes with Acquire loads for steal()ers to see the array writes.
self.tail.store(tail, .release);
// Update the list with the nodes we pushed to the buffer and try again if there's more.
list.head = nodes orelse return;
std.atomic.spinLoopHint();
head = self.head.load(.monotonic);
continue;
}
// Try to steal/overflow half of the tasks in the buffer to make room for future push()es.
// Migrating half amortizes the cost of stealing while requiring future pops to still use the buffer.
// Acquire barrier to ensure the linked list creation after the steal only happens after we succesfully steal.
var migrate = size / 2;
head = self.head.cmpxchgWeak(
head,
head +% migrate,
.acquire,
.monotonic,
) orelse {
// Link the migrated Nodes together
const first = self.array[head % capacity].raw;
while (migrate > 0) : (migrate -= 1) {
const prev = self.array[head % capacity].raw;
head +%= 1;
prev.next = self.array[head % capacity].raw;
}
// Append the list that was supposed to be pushed to the end of the migrated Nodes
const last = self.array[(head -% 1) % capacity].raw;
last.next = list.head;
list.tail.next = null;
// Return the migrated nodes + the original list as overflowed
list.head = first;
return error.Overflow;
};
}
}
fn pop(self: *Buffer) ?*Node {
var head = self.head.load(.monotonic);
const tail = self.tail.raw; // we're the only thread that can change this
while (true) {
// Quick sanity check and return null when not empty
const size = tail -% head;
assert(size <= capacity);
if (size == 0) {
return null;
}
// Dequeue with an acquire barrier to ensure any writes done to the Node
// only happen after we succesfully claim it from the array.
head = self.head.cmpxchgWeak(
head,
head +% 1,
.acquire,
.monotonic,
) orelse return self.array[head % capacity].raw;
}
}
const Stole = struct {
node: *Node,
pushed: bool,
};
fn consume(noalias self: *Buffer, noalias queue: *Queue) ?Stole {
var consumer = queue.tryAcquireConsumer() catch return null;
defer queue.releaseConsumer(consumer);
const head = self.head.load(.monotonic);
const tail = self.tail.raw; // we're the only thread that can change this
const size = tail -% head;
assert(size <= capacity);
assert(size == 0); // we should only be consuming if our array is empty
// Pop nodes from the queue and push them to our array.
// Atomic stores to the array as steal() threads may be atomically reading from it.
var pushed: Index = 0;
while (pushed < capacity) : (pushed += 1) {
const node = queue.pop(&consumer) orelse break;
self.array[(tail +% pushed) % capacity].store(node, .unordered);
}
// We will be returning one node that we stole from the queue.
// Get an extra, and if that's not possible, take one from our array.
const node = queue.pop(&consumer) orelse blk: {
if (pushed == 0) return null;
pushed -= 1;
break :blk self.array[(tail +% pushed) % capacity].raw;
};
// Update the array tail with the nodes we pushed to it.
// Release barrier to synchronize with Acquire barrier in steal()'s to see the written array Nodes.
if (pushed > 0) self.tail.store(tail +% pushed, .release);
return Stole{
.node = node,
.pushed = pushed > 0,
};
}
fn steal(noalias self: *Buffer, noalias buffer: *Buffer) ?Stole {
const head = self.head.load(.monotonic);
const tail = self.tail.raw; // we're the only thread that can change this
const size = tail -% head;
assert(size <= capacity);
assert(size == 0); // we should only be stealing if our array is empty
while (true) : (std.atomic.spinLoopHint()) {
const buffer_head = buffer.head.load(.acquire);
const buffer_tail = buffer.tail.load(.acquire);
// Overly large size indicates the the tail was updated a lot after the head was loaded.
// Reload both and try again.
const buffer_size = buffer_tail -% buffer_head;
if (buffer_size > capacity) {
continue;
}
// Try to steal half (divCeil) to amortize the cost of stealing from other threads.
const steal_size = buffer_size - (buffer_size / 2);
if (steal_size == 0) {
return null;
}
// Copy the nodes we will steal from the target's array to our own.
// Atomically load from the target buffer array as it may be pushing and atomically storing to it.
// Atomic store to our array as other steal() threads may be atomically loading from it as above.
var i: Index = 0;
while (i < steal_size) : (i += 1) {
const node = buffer.array[(buffer_head +% i) % capacity].load(.unordered);
self.array[(tail +% i) % capacity].store(node, .unordered);
}
// Try to commit the steal from the target buffer using:
// - an Acquire barrier to ensure that we only interact with the stolen Nodes after the steal was committed.
// - a Release barrier to ensure that the Nodes are copied above prior to the committing of the steal
// because if they're copied after the steal, the could be getting rewritten by the target's push().
_ = buffer.head.cmpxchgStrong(
buffer_head,
buffer_head +% steal_size,
.acq_rel,
.monotonic,
) orelse {
// Pop one from the nodes we stole as we'll be returning it
const pushed = steal_size - 1;
const node = self.array[(tail +% pushed) % capacity].raw;
// Update the array tail with the nodes we pushed to it.
// Release barrier to synchronize with Acquire barrier in steal()'s to see the written array Nodes.
if (pushed > 0) self.tail.store(tail +% pushed, .release);
return Stole{
.node = node,
.pushed = pushed > 0,
};
};
}
}
};
};

1971
deps/libxev/src/backend/epoll.zig vendored Normal file

File diff suppressed because it is too large Load Diff

1745
deps/libxev/src/backend/io_uring.zig vendored Normal file

File diff suppressed because it is too large Load Diff

2356
deps/libxev/src/backend/iocp.zig vendored Normal file

File diff suppressed because it is too large Load Diff

2645
deps/libxev/src/backend/kqueue.zig vendored Normal file

File diff suppressed because it is too large Load Diff

1637
deps/libxev/src/backend/wasi_poll.zig vendored Normal file

File diff suppressed because it is too large Load Diff

105
deps/libxev/src/bench/async1.zig vendored Normal file
View File

@@ -0,0 +1,105 @@
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const Instant = std.time.Instant;
const xev = @import("xev");
pub const std_options: std.Options = .{
.log_level = .info,
};
// Tune-ables
pub const NUM_PINGS = 1000 * 1000;
pub fn main() !void {
try run(1);
}
pub fn run(comptime thread_count: comptime_int) !void {
var loop = try xev.Loop.init(.{});
defer loop.deinit();
// Initialize all our threads
var contexts: [thread_count]Thread = undefined;
var threads: [contexts.len]std.Thread = undefined;
var comps: [contexts.len]xev.Completion = undefined;
for (&contexts, 0..) |*ctx, i| {
ctx.* = try Thread.init();
ctx.main_async.wait(&loop, &comps[i], Thread, ctx, mainAsyncCallback);
threads[i] = try std.Thread.spawn(.{}, Thread.threadMain, .{ctx});
}
const start_time = try Instant.now();
try loop.run(.until_done);
for (&threads) |thr| thr.join();
const end_time = try Instant.now();
const elapsed = @as(f64, @floatFromInt(end_time.since(start_time)));
std.log.info("async{d}: {d:.2} seconds ({d:.2}/sec)", .{
thread_count,
elapsed / 1e9,
NUM_PINGS / (elapsed / 1e9),
});
}
fn mainAsyncCallback(
ud: ?*Thread,
_: *xev.Loop,
_: *xev.Completion,
r: xev.Async.WaitError!void,
) xev.CallbackAction {
_ = r catch unreachable;
const self = ud.?;
self.worker_async.notify() catch unreachable;
self.main_sent += 1;
self.main_seen += 1;
return if (self.main_sent >= NUM_PINGS) .disarm else .rearm;
}
/// The thread state
const Thread = struct {
loop: xev.Loop,
worker_async: xev.Async,
main_async: xev.Async,
worker_sent: usize = 0,
worker_seen: usize = 0,
main_sent: usize = 0,
main_seen: usize = 0,
pub fn init() !Thread {
return .{
.loop = try xev.Loop.init(.{}),
.worker_async = try xev.Async.init(),
.main_async = try xev.Async.init(),
};
}
pub fn threadMain(self: *Thread) !void {
// Kick us off
try self.main_async.notify();
// Start our waiter
var c: xev.Completion = undefined;
self.worker_async.wait(&self.loop, &c, Thread, self, asyncCallback);
// Run
try self.loop.run(.until_done);
if (self.worker_sent < NUM_PINGS) @panic("FAIL");
}
fn asyncCallback(
ud: ?*Thread,
_: *xev.Loop,
_: *xev.Completion,
r: xev.Async.WaitError!void,
) xev.CallbackAction {
_ = r catch unreachable;
const self = ud.?;
self.main_async.notify() catch unreachable;
self.worker_sent += 1;
self.worker_seen += 1;
return if (self.worker_sent >= NUM_PINGS) .disarm else .rearm;
}
};

10
deps/libxev/src/bench/async2.zig vendored Normal file
View File

@@ -0,0 +1,10 @@
const std = @import("std");
const run = @import("async1.zig").run;
pub const std_options: std.Options = .{
.log_level = .info,
};
pub fn main() !void {
try run(2);
}

10
deps/libxev/src/bench/async4.zig vendored Normal file
View File

@@ -0,0 +1,10 @@
const std = @import("std");
const run = @import("async1.zig").run;
pub const std_options: std.Options = .{
.log_level = .info,
};
pub fn main() !void {
try run(4);
}

10
deps/libxev/src/bench/async8.zig vendored Normal file
View File

@@ -0,0 +1,10 @@
const std = @import("std");
const run = @import("async1.zig").run;
pub const std_options: std.Options = .{
.log_level = .info,
};
pub fn main() !void {
try run(8);
}

View File

@@ -0,0 +1,81 @@
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const Instant = std.time.Instant;
const xev = @import("xev");
pub const std_options: std.Options = .{
.log_level = .info,
};
// Tune-ables
pub const NUM_PINGS = 1000 * 1000;
pub fn main() !void {
try run(1);
}
pub fn run(comptime thread_count: comptime_int) !void {
var thread_pool = xev.ThreadPool.init(.{});
defer thread_pool.deinit();
defer thread_pool.shutdown();
var loop = try xev.Loop.init(.{
.entries = std.math.pow(u13, 2, 12),
.thread_pool = &thread_pool,
});
defer loop.deinit();
// Create our async
notifier = try xev.Async.init();
defer notifier.deinit();
const userdata: ?*void = null;
var c: xev.Completion = undefined;
notifier.wait(&loop, &c, void, userdata, &asyncCallback);
// Initialize all our threads
var threads: [thread_count]std.Thread = undefined;
for (&threads) |*thr| {
thr.* = try std.Thread.spawn(.{}, threadMain, .{});
}
const start_time = try Instant.now();
try loop.run(.until_done);
for (&threads) |thr| thr.join();
const end_time = try Instant.now();
const elapsed = @as(f64, @floatFromInt(end_time.since(start_time)));
std.log.info("async_pummel_{d}: {d} callbacks in {d:.2} seconds ({d:.2}/sec)", .{
thread_count,
callbacks,
elapsed / 1e9,
@as(f64, @floatFromInt(callbacks)) / (elapsed / 1e9),
});
}
var callbacks: usize = 0;
var notifier: xev.Async = undefined;
var state: enum { running, stop, stopped } = .running;
fn asyncCallback(
_: ?*void,
_: *xev.Loop,
_: *xev.Completion,
r: xev.Async.WaitError!void,
) xev.CallbackAction {
_ = r catch unreachable;
callbacks += 1;
if (callbacks < NUM_PINGS) return .rearm;
// We're done
state = .stop;
while (state != .stopped) std.time.sleep(0);
return .disarm;
}
fn threadMain() !void {
while (state == .running) try notifier.notify();
state = .stopped;
}

View File

@@ -0,0 +1,10 @@
const std = @import("std");
const run = @import("async_pummel_1.zig").run;
pub const std_options: std.Options = .{
.log_level = .info,
};
pub fn main() !void {
try run(2);
}

View File

@@ -0,0 +1,10 @@
const std = @import("std");
const run = @import("async_pummel_1.zig").run;
pub const std_options: std.Options = .{
.log_level = .info,
};
pub fn main() !void {
try run(4);
}

View File

@@ -0,0 +1,10 @@
const std = @import("std");
const run = @import("async_pummel_1.zig").run;
pub const std_options: std.Options = .{
.log_level = .info,
};
pub fn main() !void {
try run(8);
}

View File

@@ -0,0 +1,61 @@
const std = @import("std");
const Instant = std.time.Instant;
const xev = @import("xev");
pub const NUM_TIMERS: usize = 10 * 1000 * 1000;
pub fn main() !void {
var thread_pool = xev.ThreadPool.init(.{});
defer thread_pool.deinit();
defer thread_pool.shutdown();
var loop = try xev.Loop.init(.{
.entries = std.math.pow(u13, 2, 12),
.thread_pool = &thread_pool,
});
defer loop.deinit();
const GPA = std.heap.GeneralPurposeAllocator(.{});
var gpa: GPA = .{};
defer _ = gpa.deinit();
const alloc = gpa.allocator();
var cs = try alloc.alloc(xev.Completion, NUM_TIMERS);
defer alloc.free(cs);
const before_all = try Instant.now();
var i: usize = 0;
var timeout: u64 = 1;
while (i < NUM_TIMERS) : (i += 1) {
if (i % 1000 == 0) timeout += 1;
const timer = try xev.Timer.init();
timer.run(&loop, &cs[i], timeout, void, null, timerCallback);
}
const before_run = try Instant.now();
try loop.run(.until_done);
const after_run = try Instant.now();
const after_all = try Instant.now();
std.log.info("{d:.2} seconds total", .{@as(f64, @floatFromInt(after_all.since(before_all))) / 1e9});
std.log.info("{d:.2} seconds init", .{@as(f64, @floatFromInt(before_run.since(before_all))) / 1e9});
std.log.info("{d:.2} seconds dispatch", .{@as(f64, @floatFromInt(after_run.since(before_run))) / 1e9});
std.log.info("{d:.2} seconds cleanup", .{@as(f64, @floatFromInt(after_all.since(after_run))) / 1e9});
}
pub const std_options: std.Options = .{
.log_level = .info,
};
var timer_callback_count: usize = 0;
fn timerCallback(
_: ?*void,
_: *xev.Loop,
_: *xev.Completion,
result: xev.Timer.RunError!void,
) xev.CallbackAction {
_ = result catch unreachable;
timer_callback_count += 1;
return .disarm;
}

359
deps/libxev/src/bench/ping-pongs.zig vendored Normal file
View File

@@ -0,0 +1,359 @@
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const Instant = std.time.Instant;
const xev = @import("xev");
pub const std_options: std.Options = .{
.log_level = .info,
};
pub fn main() !void {
var thread_pool = xev.ThreadPool.init(.{});
defer thread_pool.deinit();
defer thread_pool.shutdown();
var loop = try xev.Loop.init(.{
.entries = std.math.pow(u13, 2, 12),
.thread_pool = &thread_pool,
});
defer loop.deinit();
const GPA = std.heap.GeneralPurposeAllocator(.{});
var gpa: GPA = .{};
defer _ = gpa.deinit();
const alloc = gpa.allocator();
var server_loop = try xev.Loop.init(.{
.entries = std.math.pow(u13, 2, 12),
.thread_pool = &thread_pool,
});
defer server_loop.deinit();
var server = try Server.init(alloc, &server_loop);
defer server.deinit();
try server.start();
// Start our echo server
const server_thr = try std.Thread.spawn(.{}, Server.threadMain, .{&server});
// Start our client
var client_loop = try xev.Loop.init(.{
.entries = std.math.pow(u13, 2, 12),
.thread_pool = &thread_pool,
});
defer client_loop.deinit();
var client = try Client.init(alloc, &client_loop);
defer client.deinit();
try client.start();
const start_time = try Instant.now();
try client_loop.run(.until_done);
server_thr.join();
const end_time = try Instant.now();
const elapsed = @as(f64, @floatFromInt(end_time.since(start_time)));
std.log.info("{d:.2} roundtrips/s", .{@as(f64, @floatFromInt(client.pongs)) / (elapsed / 1e9)});
std.log.info("{d:.2} seconds total", .{elapsed / 1e9});
}
/// Memory pools for things that need stable pointers
const BufferPool = std.heap.MemoryPool([4096]u8);
const CompletionPool = std.heap.MemoryPool(xev.Completion);
const TCPPool = std.heap.MemoryPool(xev.TCP);
/// The client state
const Client = struct {
loop: *xev.Loop,
completion_pool: CompletionPool,
read_buf: [1024]u8,
pongs: u64,
state: usize = 0,
stop: bool = false,
pub const PING = "PING\n";
pub fn init(alloc: Allocator, loop: *xev.Loop) !Client {
return .{
.loop = loop,
.completion_pool = CompletionPool.init(alloc),
.read_buf = undefined,
.pongs = 0,
.state = 0,
.stop = false,
};
}
pub fn deinit(self: *Client) void {
self.completion_pool.deinit();
}
/// Must be called with stable self pointer.
pub fn start(self: *Client) !void {
const addr = try std.net.Address.parseIp4("127.0.0.1", 3131);
const socket = try xev.TCP.init(addr);
const c = try self.completion_pool.create();
socket.connect(self.loop, c, addr, Client, self, connectCallback);
}
fn connectCallback(
self_: ?*Client,
l: *xev.Loop,
c: *xev.Completion,
socket: xev.TCP,
r: xev.TCP.ConnectError!void,
) xev.CallbackAction {
_ = r catch unreachable;
const self = self_.?;
// Send message
socket.write(l, c, .{ .slice = PING[0..PING.len] }, Client, self, writeCallback);
// Read
const c_read = self.completion_pool.create() catch unreachable;
socket.read(l, c_read, .{ .slice = &self.read_buf }, Client, self, readCallback);
return .disarm;
}
fn writeCallback(
self_: ?*Client,
l: *xev.Loop,
c: *xev.Completion,
s: xev.TCP,
b: xev.WriteBuffer,
r: xev.TCP.WriteError!usize,
) xev.CallbackAction {
_ = r catch unreachable;
_ = l;
_ = s;
_ = b;
// Put back the completion.
self_.?.completion_pool.destroy(c);
return .disarm;
}
fn readCallback(
self_: ?*Client,
l: *xev.Loop,
c: *xev.Completion,
socket: xev.TCP,
buf: xev.ReadBuffer,
r: xev.TCP.ReadError!usize,
) xev.CallbackAction {
const self = self_.?;
const n = r catch unreachable;
const data = buf.slice[0..n];
// Count the number of pings in our message
var i: usize = 0;
while (i < n) : (i += 1) {
assert(data[i] == PING[self.state]);
self.state = (self.state + 1) % (PING.len);
if (self.state == 0) {
self.pongs += 1;
// If we're done then exit
if (self.pongs > 500_000) {
socket.shutdown(l, c, Client, self, shutdownCallback);
return .disarm;
}
// Send another ping
const c_ping = self.completion_pool.create() catch unreachable;
socket.write(l, c_ping, .{ .slice = PING[0..PING.len] }, Client, self, writeCallback);
}
}
// Read again
return .rearm;
}
fn shutdownCallback(
self_: ?*Client,
l: *xev.Loop,
c: *xev.Completion,
socket: xev.TCP,
r: xev.TCP.ShutdownError!void,
) xev.CallbackAction {
_ = r catch {};
const self = self_.?;
socket.close(l, c, Client, self, closeCallback);
return .disarm;
}
fn closeCallback(
self_: ?*Client,
l: *xev.Loop,
c: *xev.Completion,
socket: xev.TCP,
r: xev.TCP.CloseError!void,
) xev.CallbackAction {
_ = l;
_ = socket;
_ = r catch unreachable;
const self = self_.?;
self.stop = true;
self.completion_pool.destroy(c);
return .disarm;
}
};
/// The server state
const Server = struct {
loop: *xev.Loop,
buffer_pool: BufferPool,
completion_pool: CompletionPool,
socket_pool: TCPPool,
stop: bool,
pub fn init(alloc: Allocator, loop: *xev.Loop) !Server {
return .{
.loop = loop,
.buffer_pool = BufferPool.init(alloc),
.completion_pool = CompletionPool.init(alloc),
.socket_pool = TCPPool.init(alloc),
.stop = false,
};
}
pub fn deinit(self: *Server) void {
self.buffer_pool.deinit();
self.completion_pool.deinit();
self.socket_pool.deinit();
}
/// Must be called with stable self pointer.
pub fn start(self: *Server) !void {
const addr = try std.net.Address.parseIp4("127.0.0.1", 3131);
var socket = try xev.TCP.init(addr);
const c = try self.completion_pool.create();
try socket.bind(addr);
try socket.listen(std.os.linux.SOMAXCONN);
socket.accept(self.loop, c, Server, self, acceptCallback);
}
pub fn threadMain(self: *Server) !void {
try self.loop.run(.until_done);
}
fn destroyBuf(self: *Server, buf: []const u8) void {
self.buffer_pool.destroy(
@alignCast(
@as(*[4096]u8, @ptrFromInt(@intFromPtr(buf.ptr))),
),
);
}
fn acceptCallback(
self_: ?*Server,
l: *xev.Loop,
c: *xev.Completion,
r: xev.TCP.AcceptError!xev.TCP,
) xev.CallbackAction {
const self = self_.?;
// Create our socket
const socket = self.socket_pool.create() catch unreachable;
socket.* = r catch unreachable;
// Start reading -- we can reuse c here because its done.
const buf = self.buffer_pool.create() catch unreachable;
socket.read(l, c, .{ .slice = buf }, Server, self, readCallback);
return .disarm;
}
fn readCallback(
self_: ?*Server,
loop: *xev.Loop,
c: *xev.Completion,
socket: xev.TCP,
buf: xev.ReadBuffer,
r: xev.TCP.ReadError!usize,
) xev.CallbackAction {
const self = self_.?;
const n = r catch |err| switch (err) {
error.EOF => {
self.destroyBuf(buf.slice);
socket.shutdown(loop, c, Server, self, shutdownCallback);
return .disarm;
},
else => {
self.destroyBuf(buf.slice);
self.completion_pool.destroy(c);
std.log.warn("server read unexpected err={}", .{err});
return .disarm;
},
};
// Echo it back
const c_echo = self.completion_pool.create() catch unreachable;
const buf_write = self.buffer_pool.create() catch unreachable;
@memcpy(buf_write, buf.slice[0..n]);
socket.write(loop, c_echo, .{ .slice = buf_write[0..n] }, Server, self, writeCallback);
// Read again
return .rearm;
}
fn writeCallback(
self_: ?*Server,
l: *xev.Loop,
c: *xev.Completion,
s: xev.TCP,
buf: xev.WriteBuffer,
r: xev.TCP.WriteError!usize,
) xev.CallbackAction {
_ = l;
_ = s;
_ = r catch unreachable;
// We do nothing for write, just put back objects into the pool.
const self = self_.?;
self.completion_pool.destroy(c);
self.buffer_pool.destroy(
@alignCast(
@as(*[4096]u8, @ptrFromInt(@intFromPtr(buf.slice.ptr))),
),
);
return .disarm;
}
fn shutdownCallback(
self_: ?*Server,
l: *xev.Loop,
c: *xev.Completion,
s: xev.TCP,
r: xev.TCP.ShutdownError!void,
) xev.CallbackAction {
_ = r catch {};
const self = self_.?;
s.close(l, c, Server, self, closeCallback);
return .disarm;
}
fn closeCallback(
self_: ?*Server,
l: *xev.Loop,
c: *xev.Completion,
socket: xev.TCP,
r: xev.TCP.CloseError!void,
) xev.CallbackAction {
_ = l;
_ = r catch unreachable;
_ = socket;
const self = self_.?;
self.stop = true;
self.completion_pool.destroy(c);
return .disarm;
}
};

177
deps/libxev/src/bench/ping-udp1.zig vendored Normal file
View File

@@ -0,0 +1,177 @@
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const Instant = std.time.Instant;
const xev = @import("xev");
pub const std_options: std.Options = .{
.log_level = .info,
};
pub fn main() !void {
try run(1);
}
pub fn run(comptime count: comptime_int) !void {
var thread_pool = xev.ThreadPool.init(.{});
defer thread_pool.deinit();
defer thread_pool.shutdown();
var loop = try xev.Loop.init(.{
.entries = std.math.pow(u13, 2, 12),
.thread_pool = &thread_pool,
});
defer loop.deinit();
const addr = try std.net.Address.parseIp4("127.0.0.1", 3131);
var pingers: [count]Pinger = undefined;
for (&pingers) |*p| {
p.* = try Pinger.init(addr);
try p.start(&loop);
}
const start_time = try Instant.now();
try loop.run(.until_done);
const end_time = try Instant.now();
const total: usize = total: {
var total: usize = 0;
for (&pingers) |p| total += p.pongs;
break :total total;
};
const elapsed = @as(f64, @floatFromInt(end_time.since(start_time)));
std.log.info("ping_pongs: {d} pingers, ~{d:.0} roundtrips/s", .{
count,
@as(f64, @floatFromInt(total)) / (elapsed / 1e9),
});
}
const Pinger = struct {
udp: xev.UDP,
addr: std.net.Address,
state: usize = 0,
pongs: u64 = 0,
read_buf: [1024]u8 = undefined,
c_read: xev.Completion = undefined,
c_write: xev.Completion = undefined,
state_read: xev.UDP.State = undefined,
state_write: xev.UDP.State = undefined,
op_count: u8 = 0,
pub const PING = "PING\n";
pub fn init(addr: std.net.Address) !Pinger {
return .{
.udp = try xev.UDP.init(addr),
.state = 0,
.pongs = 0,
.addr = addr,
};
}
pub fn start(self: *Pinger, loop: *xev.Loop) !void {
try self.udp.bind(self.addr);
self.udp.read(
loop,
&self.c_read,
&self.state_read,
.{ .slice = &self.read_buf },
Pinger,
self,
Pinger.readCallback,
);
self.write(loop);
}
pub fn write(self: *Pinger, loop: *xev.Loop) void {
self.udp.write(
loop,
&self.c_write,
&self.state_write,
self.addr,
.{ .slice = PING[0..PING.len] },
Pinger,
self,
writeCallback,
);
}
pub fn readCallback(
self_: ?*Pinger,
loop: *xev.Loop,
c: *xev.Completion,
_: *xev.UDP.State,
_: std.net.Address,
socket: xev.UDP,
buf: xev.ReadBuffer,
r: xev.UDP.ReadError!usize,
) xev.CallbackAction {
_ = c;
_ = socket;
const self = self_.?;
const n = r catch unreachable;
const data = buf.slice[0..n];
var i: usize = 0;
while (i < n) : (i += 1) {
assert(data[i] == PING[self.state]);
self.state = (self.state + 1) % (PING.len);
if (self.state == 0) {
self.pongs += 1;
// If we're done then exit
if (self.pongs > 500_000) {
self.udp.close(loop, &self.c_read, Pinger, self, closeCallback);
return .disarm;
}
self.op_count += 1;
if (self.op_count == 2) {
self.op_count = 0;
// Send another ping
self.write(loop);
}
}
}
return .rearm;
}
pub fn writeCallback(
self_: ?*Pinger,
loop: *xev.Loop,
_: *xev.Completion,
_: *xev.UDP.State,
_: xev.UDP,
_: xev.WriteBuffer,
r: xev.UDP.WriteError!usize,
) xev.CallbackAction {
const self = self_.?;
self.op_count += 1;
if (self.op_count == 2) {
self.op_count = 0;
// Send another ping
self.write(loop);
}
_ = r catch unreachable;
return .disarm;
}
pub fn closeCallback(
_: ?*Pinger,
_: *xev.Loop,
_: *xev.Completion,
_: xev.UDP,
r: xev.UDP.CloseError!void,
) xev.CallbackAction {
_ = r catch unreachable;
return .disarm;
}
};

147
deps/libxev/src/bench/udp_pummel_1v1.zig vendored Normal file
View File

@@ -0,0 +1,147 @@
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const Instant = std.time.Instant;
const xev = @import("xev");
const EXPECTED = "RANG TANG DING DONG I AM THE JAPANESE SANDMAN";
/// This is a global var decremented for the test without any locks. That's
/// how the original is written and that's how we're going to do it.
var packet_counter: usize = 1e6;
var send_cb_called: usize = 0;
var recv_cb_called: usize = 0;
pub const std_options: std.Options = .{
.log_level = .info,
};
pub fn main() !void {
try run(1, 1);
}
pub fn run(comptime n_senders: comptime_int, comptime n_receivers: comptime_int) !void {
const base_port = 12345;
var thread_pool = xev.ThreadPool.init(.{});
defer thread_pool.deinit();
defer thread_pool.shutdown();
var loop = try xev.Loop.init(.{
.entries = std.math.pow(u13, 2, 12),
.thread_pool = &thread_pool,
});
defer loop.deinit();
var receivers: [n_receivers]Receiver = undefined;
for (&receivers, 0..) |*r, i| {
const addr = try std.net.Address.parseIp4("127.0.0.1", @as(u16, @intCast(base_port + i)));
r.* = .{ .udp = try xev.UDP.init(addr) };
try r.udp.bind(addr);
r.udp.read(
&loop,
&r.c_recv,
&r.udp_state,
.{ .slice = &r.recv_buf },
Receiver,
r,
Receiver.readCallback,
);
}
var senders: [n_senders]Sender = undefined;
for (&senders, 0..) |*s, i| {
const addr = try std.net.Address.parseIp4(
"127.0.0.1",
@as(u16, @intCast(base_port + (i % n_receivers))),
);
s.* = .{ .udp = try xev.UDP.init(addr) };
s.udp.write(
&loop,
&s.c_send,
&s.udp_state,
addr,
.{ .slice = EXPECTED },
Sender,
s,
Sender.writeCallback,
);
}
const start_time = try Instant.now();
try loop.run(.until_done);
const end_time = try Instant.now();
const elapsed = @as(f64, @floatFromInt(end_time.since(start_time)));
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", .{
n_senders,
n_receivers,
@as(f64, @floatFromInt(recv_cb_called)) / (elapsed / std.time.ns_per_s),
@as(f64, @floatFromInt(send_cb_called)) / (elapsed / std.time.ns_per_s),
recv_cb_called,
send_cb_called,
elapsed / std.time.ns_per_s,
});
}
const Sender = struct {
udp: xev.UDP,
udp_state: xev.UDP.State = undefined,
c_send: xev.Completion = undefined,
fn writeCallback(
_: ?*Sender,
l: *xev.Loop,
_: *xev.Completion,
_: *xev.UDP.State,
_: xev.UDP,
_: xev.WriteBuffer,
r: xev.UDP.WriteError!usize,
) xev.CallbackAction {
_ = r catch unreachable;
if (packet_counter == 0) {
l.stop();
return .disarm;
}
packet_counter -|= 1;
send_cb_called += 1;
return .rearm;
}
};
const Receiver = struct {
udp: xev.UDP,
udp_state: xev.UDP.State = undefined,
c_recv: xev.Completion = undefined,
recv_buf: [65536]u8 = undefined,
fn readCallback(
_: ?*Receiver,
_: *xev.Loop,
_: *xev.Completion,
_: *xev.UDP.State,
_: std.net.Address,
_: xev.UDP,
b: xev.ReadBuffer,
r: xev.UDP.ReadError!usize,
) xev.CallbackAction {
const n = r catch |err| {
switch (err) {
error.EOF => {},
else => std.log.warn("err={}", .{err}),
}
return .disarm;
};
if (!std.mem.eql(u8, b.slice[0..n], EXPECTED)) {
@panic("Unexpected data.");
}
recv_cb_called += 1;
return .rearm;
}
};

163
deps/libxev/src/build/ScdocStep.zig vendored Normal file
View File

@@ -0,0 +1,163 @@
const std = @import("std");
const mem = std.mem;
const fs = std.fs;
const Step = std.Build.Step;
const Build = std.Build;
/// ScdocStep generates man pages using scdoc(1).
///
/// It reads all the raw pages from src_path and writes them to out_path.
/// src_path is typically "docs/" relative to the build root and out_path is
/// the build cache.
///
/// The man pages can be installed by calling install() on the step.
const ScdocStep = @This();
step: Step,
builder: *Build,
/// path to read man page sources from, defaults to the "doc/" subdirectory
/// from the build.zig file. This must be an absolute path.
src_path: []const u8,
/// path where the generated man pages will be written (NOT installed). This
/// defaults to build cache root.
out_path: []const u8,
pub fn create(builder: *Build) *ScdocStep {
const self = builder.allocator.create(ScdocStep) catch unreachable;
self.* = init(builder);
return self;
}
pub fn init(builder: *Build) ScdocStep {
return ScdocStep{
.builder = builder,
.step = Step.init(.{
.id = .custom,
.name = "generate man pages",
.owner = builder,
.makeFn = make,
}),
.src_path = builder.pathFromRoot("docs/"),
.out_path = builder.cache_root.join(builder.allocator, &[_][]const u8{
"man",
}) catch unreachable,
};
}
fn make(step: *std.Build.Step, _: std.Progress.Node) !void {
const self: *ScdocStep = @fieldParentPtr("step", step);
// Create our cache path
// TODO(mitchellh): ideally this would be pure zig
{
const command = try std.fmt.allocPrint(
self.builder.allocator,
"rm -f {[path]s}/* && mkdir -p {[path]s}",
.{ .path = self.out_path },
);
_ = self.builder.run(&[_][]const u8{ "sh", "-c", command });
}
// Find all our man pages which are in our src path ending with ".scd".
var dir = try fs.openDirAbsolute(self.src_path, .{ .iterate = true });
defer dir.close();
var iter = dir.iterate();
while (try iter.next()) |*entry| {
// We only want "scd" files to generate.
if (!mem.eql(u8, fs.path.extension(entry.name), ".scd")) {
continue;
}
const src = try fs.path.join(
self.builder.allocator,
&[_][]const u8{ self.src_path, entry.name },
);
const dst = try fs.path.join(
self.builder.allocator,
&[_][]const u8{ self.out_path, entry.name[0..(entry.name.len - 4)] },
);
const command = try std.fmt.allocPrint(
self.builder.allocator,
"scdoc < {s} > {s}",
.{ src, dst },
);
_ = self.builder.run(&[_][]const u8{ "sh", "-c", command });
}
}
pub fn install(self: *ScdocStep) !void {
// Ensure that `zig build install` depends on our generation step first.
self.builder.getInstallStep().dependOn(&self.step);
// Then run our install step which looks at what we made out of our
// generation and moves it to the install prefix.
const install_step = InstallStep.create(self.builder, self);
self.builder.getInstallStep().dependOn(&install_step.step);
}
/// Install man pages, create using install() on ScdocStep.
const InstallStep = struct {
step: Step,
builder: *Build,
scdoc: *ScdocStep,
pub fn create(builder: *Build, scdoc: *ScdocStep) *InstallStep {
const self = builder.allocator.create(InstallStep) catch unreachable;
self.* = InstallStep.init(builder, scdoc);
self.step.dependOn(&scdoc.step);
return self;
}
fn init(builder: *Build, scdoc: *ScdocStep) InstallStep {
return InstallStep{
.builder = builder,
.step = Step.init(.{
.id = .custom,
.name = "install man pages",
.owner = builder,
.makeFn = InstallStep.make,
}),
.scdoc = scdoc,
};
}
fn make(step: *Step, progress: std.Progress.Node) !void {
const self: *InstallStep = @fieldParentPtr("step", step);
// Get our absolute output path
var path = self.scdoc.out_path;
if (!fs.path.isAbsolute(path)) {
path = self.builder.pathFromRoot(path);
}
// Find all our man pages which are in our src path ending with ".scd".
var dir = try fs.openDirAbsolute(path, .{ .iterate = true });
defer dir.close();
var iter = dir.iterate();
while (try iter.next()) |*entry| {
// We expect filenames to be "foo.3" and this gets us "3"
const section = entry.name[(entry.name.len - 1)..];
const src = try fs.path.join(
self.builder.allocator,
&[_][]const u8{ path, entry.name },
);
const output = try std.fmt.allocPrint(
self.builder.allocator,
"share/man/man{s}/{s}",
.{ section, entry.name },
);
const fileStep = self.builder.addInstallFile(
.{ .cwd_relative = src },
output,
);
try fileStep.step.make(progress);
}
}
};

355
deps/libxev/src/c_api.zig vendored Normal file
View File

@@ -0,0 +1,355 @@
// This file contains the C bindings that are exported when building
// the system libraries.
//
// WHERE IS THE DOCUMENTATION? Note that all the documentation for the C
// interface is in the man pages. The header file xev.h purposely has no
// documentation so that its concise and easy to see the list of exported
// functions.
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const xev = @import("main.zig");
export fn xev_loop_init(loop: *xev.Loop) c_int {
// TODO: overflow
loop.* = xev.Loop.init(.{}) catch |err| return errorCode(err);
return 0;
}
export fn xev_loop_deinit(loop: *xev.Loop) void {
loop.deinit();
}
export fn xev_loop_run(loop: *xev.Loop, mode: xev.RunMode) c_int {
loop.run(mode) catch |err| return errorCode(err);
return 0;
}
export fn xev_loop_now(loop: *xev.Loop) i64 {
return loop.now();
}
export fn xev_loop_update_now(loop: *xev.Loop) void {
loop.update_now();
}
export fn xev_completion_zero(c: *xev.Completion) void {
c.* = .{};
}
export fn xev_completion_state(c: *xev.Completion) xev.CompletionState {
return c.state();
}
//-------------------------------------------------------------------
// ThreadPool
export fn xev_threadpool_config_init(cfg: *xev.ThreadPool.Config) void {
cfg.* = .{};
}
export fn xev_threadpool_config_set_stack_size(
cfg: *xev.ThreadPool.Config,
v: u32,
) void {
cfg.stack_size = v;
}
export fn xev_threadpool_config_set_max_threads(
cfg: *xev.ThreadPool.Config,
v: u32,
) void {
cfg.max_threads = v;
}
export fn xev_threadpool_init(
threadpool: *xev.ThreadPool,
cfg_: ?*xev.ThreadPool.Config,
) c_int {
const cfg: xev.ThreadPool.Config = if (cfg_) |v| v.* else .{};
threadpool.* = xev.ThreadPool.init(cfg);
return 0;
}
export fn xev_threadpool_deinit(threadpool: *xev.ThreadPool) void {
threadpool.deinit();
}
export fn xev_threadpool_shutdown(threadpool: *xev.ThreadPool) void {
threadpool.shutdown();
}
export fn xev_threadpool_schedule(
pool: *xev.ThreadPool,
batch: *xev.ThreadPool.Batch,
) void {
pool.schedule(batch.*);
}
export fn xev_threadpool_task_init(
t: *xev.ThreadPool.Task,
cb: *const fn (*xev.ThreadPool.Task) callconv(.C) void,
) void {
const extern_t = @as(*Task, @ptrCast(@alignCast(t)));
extern_t.c_callback = cb;
t.* = .{
.callback = (struct {
fn callback(inner_t: *xev.ThreadPool.Task) void {
const outer_t: *Task = @alignCast(@fieldParentPtr(
"data",
@as(*Task.Data, @ptrCast(inner_t)),
));
outer_t.c_callback(inner_t);
}
}).callback,
};
}
export fn xev_threadpool_batch_init(b: *xev.ThreadPool.Batch) void {
b.* = .{};
}
export fn xev_threadpool_batch_push_task(
b: *xev.ThreadPool.Batch,
t: *xev.ThreadPool.Task,
) void {
b.push(xev.ThreadPool.Batch.from(t));
}
export fn xev_threadpool_batch_push_batch(
b: *xev.ThreadPool.Batch,
other: *xev.ThreadPool.Batch,
) void {
b.push(other.*);
}
//-------------------------------------------------------------------
// Timers
export fn xev_timer_init(v: *xev.Timer) c_int {
v.* = xev.Timer.init() catch |err| return errorCode(err);
return 0;
}
export fn xev_timer_deinit(v: *xev.Timer) void {
v.deinit();
}
export fn xev_timer_run(
v: *xev.Timer,
loop: *xev.Loop,
c: *xev.Completion,
next_ms: u64,
userdata: ?*anyopaque,
cb: *const fn (
*xev.Loop,
*xev.Completion,
c_int,
?*anyopaque,
) callconv(.C) xev.CallbackAction,
) void {
const Callback = @typeInfo(@TypeOf(cb)).Pointer.child;
const extern_c = @as(*Completion, @ptrCast(@alignCast(c)));
extern_c.c_callback = @as(*const anyopaque, @ptrCast(cb));
v.run(loop, c, next_ms, anyopaque, userdata, (struct {
fn callback(
ud: ?*anyopaque,
cb_loop: *xev.Loop,
cb_c: *xev.Completion,
r: xev.Timer.RunError!void,
) xev.CallbackAction {
const cb_extern_c = @as(*Completion, @ptrCast(cb_c));
const cb_c_callback = @as(
*const Callback,
@ptrCast(@alignCast(cb_extern_c.c_callback)),
);
return @call(.auto, cb_c_callback, .{
cb_loop,
cb_c,
if (r) |_| 0 else |err| errorCode(err),
ud,
});
}
}).callback);
}
export fn xev_timer_reset(
v: *xev.Timer,
loop: *xev.Loop,
c: *xev.Completion,
c_cancel: *xev.Completion,
next_ms: u64,
userdata: ?*anyopaque,
cb: *const fn (
*xev.Loop,
*xev.Completion,
c_int,
?*anyopaque,
) callconv(.C) xev.CallbackAction,
) void {
const Callback = @typeInfo(@TypeOf(cb)).Pointer.child;
const extern_c = @as(*Completion, @ptrCast(@alignCast(c)));
extern_c.c_callback = @as(*const anyopaque, @ptrCast(cb));
v.reset(loop, c, c_cancel, next_ms, anyopaque, userdata, (struct {
fn callback(
ud: ?*anyopaque,
cb_loop: *xev.Loop,
cb_c: *xev.Completion,
r: xev.Timer.RunError!void,
) xev.CallbackAction {
const cb_extern_c = @as(*Completion, @ptrCast(cb_c));
const cb_c_callback = @as(
*const Callback,
@ptrCast(@alignCast(cb_extern_c.c_callback)),
);
return @call(.auto, cb_c_callback, .{
cb_loop,
cb_c,
if (r) |_| 0 else |err| errorCode(err),
ud,
});
}
}).callback);
}
export fn xev_timer_cancel(
v: *xev.Timer,
loop: *xev.Loop,
c_timer: *xev.Completion,
c_cancel: *xev.Completion,
userdata: ?*anyopaque,
cb: *const fn (
*xev.Loop,
*xev.Completion,
c_int,
?*anyopaque,
) callconv(.C) xev.CallbackAction,
) void {
const Callback = @typeInfo(@TypeOf(cb)).Pointer.child;
const extern_c = @as(*Completion, @ptrCast(@alignCast(c_cancel)));
extern_c.c_callback = @as(*const anyopaque, @ptrCast(cb));
v.cancel(loop, c_timer, c_cancel, anyopaque, userdata, (struct {
fn callback(
ud: ?*anyopaque,
cb_loop: *xev.Loop,
cb_c: *xev.Completion,
r: xev.Timer.CancelError!void,
) xev.CallbackAction {
const cb_extern_c = @as(*Completion, @ptrCast(cb_c));
const cb_c_callback = @as(
*const Callback,
@ptrCast(@alignCast(cb_extern_c.c_callback)),
);
return @call(.auto, cb_c_callback, .{
cb_loop,
cb_c,
if (r) |_| 0 else |err| errorCode(err),
ud,
});
}
}).callback);
}
//-------------------------------------------------------------------
// Async
export fn xev_async_init(v: *xev.Async) c_int {
v.* = xev.Async.init() catch |err| return errorCode(err);
return 0;
}
export fn xev_async_deinit(v: *xev.Async) void {
v.deinit();
}
export fn xev_async_notify(v: *xev.Async) c_int {
v.notify() catch |err| return errorCode(err);
return 0;
}
export fn xev_async_wait(
v: *xev.Async,
loop: *xev.Loop,
c: *xev.Completion,
userdata: ?*anyopaque,
cb: *const fn (
*xev.Loop,
*xev.Completion,
c_int,
?*anyopaque,
) callconv(.C) xev.CallbackAction,
) void {
const Callback = @typeInfo(@TypeOf(cb)).Pointer.child;
const extern_c = @as(*Completion, @ptrCast(@alignCast(c)));
extern_c.c_callback = @as(*const anyopaque, @ptrCast(cb));
v.wait(loop, c, anyopaque, userdata, (struct {
fn callback(
ud: ?*anyopaque,
cb_loop: *xev.Loop,
cb_c: *xev.Completion,
r: xev.Async.WaitError!void,
) xev.CallbackAction {
const cb_extern_c = @as(*Completion, @ptrCast(cb_c));
const cb_c_callback = @as(
*const Callback,
@ptrCast(@alignCast(cb_extern_c.c_callback)),
);
return @call(.auto, cb_c_callback, .{
cb_loop,
cb_c,
if (r) |_| 0 else |err| errorCode(err),
ud,
});
}
}).callback);
}
//-------------------------------------------------------------------
// Sync with xev.h
/// Since we can't pass the callback at comptime with C, we have to
/// have an additional field on completions to store our callback pointer.
/// We just tack it onto the end of the memory chunk that C programs allocate
/// for completions.
const Completion = extern struct {
const Data = [@sizeOf(xev.Completion)]u8;
data: Data,
c_callback: *const anyopaque,
};
const Task = extern struct {
const Data = [@sizeOf(xev.ThreadPool.Task)]u8;
data: Data,
c_callback: *const fn (*xev.ThreadPool.Task) callconv(.C) void,
};
/// Returns the unique error code for an error.
fn errorCode(err: anyerror) c_int {
// TODO(mitchellh): This is a bad idea because its not stable across
// code changes. For now we just document that error codes are not
// stable but that is not useful at all!
return @intFromError(err);
}
test "c-api sizes" {
// This tests the sizes that are defined in the C API. We must ensure
// that our main structure sizes never exceed these so that the C ABI
// is maintained.
//
// THE MAGIC NUMBERS ARE KEPT IN SYNC WITH "include/xev.h"
const testing = std.testing;
try testing.expect(@sizeOf(xev.Loop) <= 512);
try testing.expect(@sizeOf(Completion) <= 320);
try testing.expect(@sizeOf(xev.Async) <= 256);
try testing.expect(@sizeOf(xev.Timer) <= 256);
try testing.expectEqual(@as(usize, 48), @sizeOf(xev.ThreadPool));
try testing.expectEqual(@as(usize, 24), @sizeOf(xev.ThreadPool.Batch));
try testing.expectEqual(@as(usize, 24), @sizeOf(Task));
try testing.expectEqual(@as(usize, 8), @sizeOf(xev.ThreadPool.Config));
}

58
deps/libxev/src/debug.zig vendored Normal file
View File

@@ -0,0 +1,58 @@
const std = @import("std");
inline fn indent(depth: usize, writer: anytype) !void {
for (0..depth) |_| try writer.writeByte(' ');
}
pub fn describe(comptime T: type, writer: anytype, depth: usize) !void {
const type_info = @typeInfo(T);
switch (type_info) {
.Type,
.Void,
.Bool,
.NoReturn,
.Int,
.Float,
.Pointer,
.Array,
.ComptimeFloat,
.ComptimeInt,
.Undefined,
.Null,
.Optional,
.ErrorUnion,
.ErrorSet,
.Enum,
.Fn,
.Opaque,
.Frame,
.AnyFrame,
.Vector,
.EnumLiteral,
=> {
try writer.print("{s} ({d} bytes)", .{ @typeName(T), @sizeOf(T) });
},
.Union => |s| {
try writer.print("{s} ({d} bytes) {{\n", .{ @typeName(T), @sizeOf(T) });
inline for (s.fields) |f| {
try indent(depth + 4, writer);
try writer.print("{s}: ", .{f.name});
try describe(f.type, writer, depth + 4);
try writer.writeByte('\n');
}
try indent(depth, writer);
try writer.writeByte('}');
},
.Struct => |s| {
try writer.print("{s} ({d} bytes) {{\n", .{ @typeName(T), @sizeOf(T) });
inline for (s.fields) |f| {
try indent(depth + 4, writer);
try writer.print("{s}: ", .{f.name});
try describe(f.type, writer, depth + 4);
try writer.writeByte('\n');
}
try indent(depth, writer);
try writer.writeByte('}');
},
}
}

379
deps/libxev/src/heap.zig vendored Normal file
View File

@@ -0,0 +1,379 @@
const std = @import("std");
const assert = std.debug.assert;
/// An intrusive heap implementation backed by a pairing heap[1] implementation.
///
/// Why? Intrusive data structures require the element type to hold the metadata
/// required for the structure, rather than an additional container structure.
/// There are numerous pros/cons that are documented well by Boost[2]. For Zig,
/// I think the primary benefits are making data structures allocation free
/// (rather, shifting allocation up to the consumer which can choose how they
/// want the memory to be available). There are various costs to this such as
/// the costs of pointer chasing, larger memory overhead, requiring the element
/// type to be aware of its container, etc. But for certain use cases an intrusive
/// data structure can yield much better performance.
///
/// Usage notes:
/// - The element T is expected to have a field "heap" of type InstrusiveHeapField.
/// See the tests for a full example of how to set this.
/// - You can easily make this a min or max heap by inverting the result of
/// "less" below.
///
/// [1]: https://en.wikipedia.org/wiki/Pairing_heap
/// [2]: https://www.boost.org/doc/libs/1_64_0/doc/html/intrusive/intrusive_vs_nontrusive.html
pub fn Intrusive(
comptime T: type,
comptime Context: type,
comptime less: *const fn (ctx: Context, a: *T, b: *T) bool,
) type {
return struct {
const Self = @This();
root: ?*T = null,
context: Context,
/// Insert a new element v into the heap. An element v can only
/// be a member of a single heap at any given time. When compiled
/// with runtime-safety, assertions will help verify this property.
pub fn insert(self: *Self, v: *T) void {
self.root = if (self.root) |root| self.meld(v, root) else v;
}
/// Look at the next minimum value but do not remove it.
pub fn peek(self: *Self) ?*T {
return self.root;
}
/// Delete the minimum value from the heap and return it.
pub fn deleteMin(self: *Self) ?*T {
const root = self.root orelse return null;
self.root = if (root.heap.child) |child|
self.combine_siblings(child)
else
null;
// Clear pointers with runtime safety so we can verify on
// insert that values aren't incorrectly being set multiple times.
root.heap = .{};
return root;
}
/// Remove the value v from the heap.
pub fn remove(self: *Self, v: *T) void {
// If v doesn't have a previous value, this must be the root
// element. If it is NOT the root element, v can't be in this
// heap and we trigger an assertion failure.
const prev = v.heap.prev orelse {
assert(self.root.? == v);
_ = self.deleteMin();
return;
};
// Detach "v" from the tree and clean up any links so it
// is as if this node never nexisted. The previous value
// must point to the proper next value and the pointers
// must all be cleaned up.
if (v.heap.next) |next| next.heap.prev = prev;
if (prev.heap.child == v)
prev.heap.child = v.heap.next
else
prev.heap.next = v.heap.next;
v.heap.prev = null;
v.heap.next = null;
// If we have children, then we need to merge them back in.
const child = v.heap.child orelse return;
v.heap.child = null;
const x = self.combine_siblings(child);
self.root = self.meld(x, self.root.?);
}
/// Meld (union) two heaps together. This isn't a generalized
/// union. It assumes that a.heap.next is null so this is only
/// meant in specific scenarios in the pairing heap where meld
/// is expected.
///
/// For example, when melding a new value "v" with an existing
/// root "root", "v" must always be the first param.
fn meld(self: *Self, a: *T, b: *T) *T {
assert(a.heap.next == null);
if (less(self.context, a, b)) {
// B points back to A
b.heap.prev = a;
// If B has siblings, then A inherits B's siblings
// and B's immediate sibling must point back to A to
// maintain the doubly linked list.
if (b.heap.next) |b_next| {
a.heap.next = b_next;
b_next.heap.prev = a;
b.heap.next = null;
}
// If A has a child, then B becomes the leftmost sibling
// of that child.
if (a.heap.child) |a_child| {
b.heap.next = a_child;
a_child.heap.prev = b;
}
// B becomes the leftmost child of A
a.heap.child = b;
return a;
}
// Replace A with B in the tree. Any of B's children
// become siblings of A. A becomes the leftmost child of B.
// A points back to B
b.heap.prev = a.heap.prev;
a.heap.prev = b;
if (b.heap.child) |b_child| {
a.heap.next = b_child;
b_child.heap.prev = a;
}
b.heap.child = a;
return b;
}
/// Combine the siblings of the leftmost value "left" into a single
/// new rooted with the minimum value.
fn combine_siblings(self: *Self, left: *T) *T {
left.heap.prev = null;
// Merge pairs right
var root: *T = root: {
var a: *T = left;
while (true) {
var b = a.heap.next orelse break :root a;
a.heap.next = null;
b = self.meld(a, b);
a = b.heap.next orelse break :root b;
}
};
// Merge pairs left
while (true) {
var b = root.heap.prev orelse return root;
b.heap.next = null;
root = self.meld(b, root);
}
}
};
}
/// The state that is required for IntrusiveHeap element types. This
/// should be set as the "heap" field in the type T.
pub fn IntrusiveField(comptime T: type) type {
return struct {
child: ?*T = null,
prev: ?*T = null,
next: ?*T = null,
};
}
test "heap" {
const Elem = struct {
const Self = @This();
value: usize = 0,
heap: IntrusiveField(Self) = .{},
};
const Heap = Intrusive(Elem, void, (struct {
fn less(ctx: void, a: *Elem, b: *Elem) bool {
_ = ctx;
return a.value < b.value;
}
}).less);
var a: Elem = .{ .value = 12 };
var b: Elem = .{ .value = 24 };
var c: Elem = .{ .value = 7 };
var d: Elem = .{ .value = 9 };
var h: Heap = .{ .context = {} };
h.insert(&a);
h.insert(&b);
h.insert(&c);
h.insert(&d);
h.remove(&d);
const testing = std.testing;
try testing.expect(h.deleteMin().?.value == 7);
try testing.expect(h.deleteMin().?.value == 12);
try testing.expect(h.deleteMin().?.value == 24);
try testing.expect(h.deleteMin() == null);
}
test "heap remove root" {
const Elem = struct {
const Self = @This();
value: usize = 0,
heap: IntrusiveField(Self) = .{},
};
const Heap = Intrusive(Elem, void, (struct {
fn less(ctx: void, a: *Elem, b: *Elem) bool {
_ = ctx;
return a.value < b.value;
}
}).less);
var a: Elem = .{ .value = 12 };
var b: Elem = .{ .value = 24 };
var h: Heap = .{ .context = {} };
h.insert(&a);
h.insert(&b);
h.remove(&a);
const testing = std.testing;
try testing.expect(h.deleteMin().?.value == 24);
try testing.expect(h.deleteMin() == null);
}
test "heap remove with children" {
const Elem = struct {
const Self = @This();
value: usize = 0,
heap: IntrusiveField(Self) = .{},
};
const Heap = Intrusive(Elem, void, (struct {
fn less(ctx: void, a: *Elem, b: *Elem) bool {
_ = ctx;
return a.value < b.value;
}
}).less);
var a: Elem = .{ .value = 36 };
var b: Elem = .{ .value = 24 };
var c: Elem = .{ .value = 12 };
var h: Heap = .{ .context = {} };
h.insert(&a);
h.insert(&b);
h.insert(&c);
h.remove(&b);
const testing = std.testing;
try testing.expect(h.deleteMin().?.value == 12);
try testing.expect(h.deleteMin().?.value == 36);
try testing.expect(h.deleteMin() == null);
}
test "heap equal values" {
const testing = std.testing;
const Elem = struct {
const Self = @This();
value: usize = 0,
heap: IntrusiveField(Self) = .{},
};
const Heap = Intrusive(Elem, void, (struct {
fn less(ctx: void, a: *Elem, b: *Elem) bool {
_ = ctx;
return a.value < b.value;
}
}).less);
var a: Elem = .{ .value = 1 };
var b: Elem = .{ .value = 2 };
var c: Elem = .{ .value = 3 };
var d: Elem = .{ .value = 4 };
var h: Heap = .{ .context = {} };
h.insert(&a);
h.insert(&b);
h.insert(&c);
h.insert(&d);
try testing.expect(h.deleteMin().?.value == 1);
try testing.expect(h.deleteMin().?.value == 2);
try testing.expect(h.deleteMin().?.value == 3);
try testing.expect(h.deleteMin().?.value == 4);
try testing.expect(h.deleteMin() == null);
}
test "heap: million values" {
const testing = std.testing;
const alloc = testing.allocator;
const Elem = struct {
const Self = @This();
value: usize = 0,
heap: IntrusiveField(Self) = .{},
};
const Heap = Intrusive(Elem, void, (struct {
fn less(ctx: void, a: *Elem, b: *Elem) bool {
_ = ctx;
return a.value < b.value;
}
}).less);
const NUM_TIMERS: usize = 1000 * 1000;
var elems = try alloc.alloc(Elem, NUM_TIMERS);
defer alloc.free(elems);
var i: usize = 0;
var value: usize = 0;
while (i < NUM_TIMERS) : (i += 1) {
if (i % 100 == 0) value += 1;
elems[i] = .{ .value = value };
}
var h: Heap = .{ .context = {} };
for (elems) |*elem| {
h.insert(elem);
}
var count: usize = 0;
var last: usize = 0;
while (h.deleteMin()) |elem| {
count += 1;
try testing.expect(elem.value >= last);
last = elem.value;
}
try testing.expect(h.deleteMin() == null);
try testing.expect(count == NUM_TIMERS);
}
test "heap: dangling next pointer" {
const testing = std.testing;
const Elem = struct {
const Self = @This();
value: usize = 0,
heap: IntrusiveField(Self) = .{},
};
const Heap = Intrusive(Elem, void, (struct {
fn less(ctx: void, a: *Elem, b: *Elem) bool {
_ = ctx;
return a.value < b.value;
}
}).less);
var a: Elem = .{ .value = 2 };
var b: Elem = .{ .value = 4 };
var c: Elem = .{ .value = 5 };
var d: Elem = .{ .value = 1 };
var e: Elem = .{ .value = 3 };
var h: Heap = .{ .context = {} };
h.insert(&a);
h.insert(&b);
h.insert(&c);
h.insert(&d);
h.insert(&e);
try testing.expect(h.deleteMin().?.value == 1);
try testing.expect(h.deleteMin().?.value == 2);
try testing.expect(h.deleteMin().?.value == 3);
try testing.expect(h.deleteMin().?.value == 4);
try testing.expect(h.deleteMin().?.value == 5);
try testing.expect(h.deleteMin() == null);
}

98
deps/libxev/src/linux/timerfd.zig vendored Normal file
View File

@@ -0,0 +1,98 @@
const std = @import("std");
const linux = std.os.linux;
const posix = std.posix;
/// Timerfd is a wrapper around the timerfd system calls. See the
/// timerfd_create man page for information on timerfd and associated
/// system calls.
///
/// This is a small wrapper around timerfd to make it slightly more
/// pleasant to use, but may not expose all available functionality.
/// For maximum control you should use the syscalls directly.
pub const Timerfd = struct {
/// The timerfd file descriptor for use with poll, etc.
fd: i32,
/// timerfd_create
pub fn init(clock: Clock, flags: linux.TFD) !Timerfd {
const res = linux.timerfd_create(@intFromEnum(clock), flags);
return switch (posix.errno(res)) {
.SUCCESS => .{ .fd = @as(i32, @intCast(res)) },
else => error.UnknownError,
};
}
pub fn deinit(self: *const Timerfd) void {
posix.close(self.fd);
}
/// timerfd_settime
pub fn set(
self: *const Timerfd,
flags: linux.TFD.TIMER,
new_value: *const Spec,
old_value: ?*Spec,
) !void {
const res = linux.timerfd_settime(
self.fd,
flags,
@as(*const linux.itimerspec, @ptrCast(new_value)),
@as(?*linux.itimerspec, @ptrCast(old_value)),
);
return switch (posix.errno(res)) {
.SUCCESS => {},
else => error.UnknownError,
};
}
/// timerfd_gettime
pub fn get(self: *const Timerfd) !Spec {
var out: Spec = undefined;
const res = linux.timerfd_gettime(self.fd, @as(*linux.itimerspec, @ptrCast(&out)));
return switch (posix.errno(res)) {
.SUCCESS => out,
else => error.UnknownError,
};
}
/// The clocks available for a Timerfd. This is a non-exhaustive enum
/// so that unsupported values can be attempted to be passed into the
/// system calls.
pub const Clock = enum(i32) {
realtime = 0,
monotonic = 1,
boottime = 7,
realtime_alarm = 8,
boottime_alarm = 9,
_,
};
/// itimerspec
pub const Spec = extern struct {
interval: TimeSpec = .{},
value: TimeSpec = .{},
};
/// timespec
pub const TimeSpec = extern struct {
seconds: isize = 0,
nanoseconds: isize = 0,
};
};
test Timerfd {
const testing = std.testing;
var t = try Timerfd.init(.monotonic, .{});
defer t.deinit();
// Set
try t.set(.{}, &.{ .value = .{ .seconds = 60 } }, null);
try testing.expect((try t.get()).value.seconds > 0);
// Disarm
var old: Timerfd.Spec = undefined;
try t.set(.{}, &.{ .value = .{ .seconds = 0 } }, &old);
try testing.expect(old.value.seconds > 0);
}

77
deps/libxev/src/loop.zig vendored Normal file
View File

@@ -0,0 +1,77 @@
//! Common loop structures. The actual loop implementation is in backend-specific
//! files such as linux/io_uring.zig.
const std = @import("std");
const assert = std.debug.assert;
const xev = @import("main.zig");
/// Common options across backends. Not all options apply to all backends.
/// Read the doc comment for individual fields to learn what backends they
/// apply to.
pub const Options = struct {
/// The number of queued completions that can be in flight before
/// requiring interaction with the kernel.
///
/// Backends: io_uring
entries: u32 = 256,
/// A thread pool to use for blocking operations. If the backend doesn't
/// need to perform any blocking operations then no threads will ever
/// be spawned. If the backend does need to perform blocking operations
/// on a thread and no thread pool is provided, the operations will simply
/// fail. Unless you're trying to really optimize for space, it is
/// recommended you provide a thread pool.
///
/// Backends: epoll, kqueue
thread_pool: ?*xev.ThreadPool = null,
};
/// The loop run mode -- all backends are required to support this in some way.
/// Backends may provide backend-specific APIs that behave slightly differently
/// or in a more configurable way.
pub const RunMode = enum(c_int) {
/// Run the event loop once. If there are no blocking operations ready,
/// return immediately.
no_wait = 0,
/// Run the event loop once, waiting for at least one blocking operation
/// to complete.
once = 1,
/// Run the event loop until it is "done". "Doneness" is defined as
/// there being no more completions that are active.
until_done = 2,
};
/// The result type for callbacks. This should be used by all loop
/// implementations and higher level abstractions in order to control
/// what to do after the loop completes.
pub const CallbackAction = enum(c_int) {
/// The request is complete and is not repeated. For example, a read
/// callback only fires once and is no longer watched for reads. You
/// can always free memory associated with the completion prior to
/// returning this.
disarm = 0,
/// Requeue the same operation request with the same parameters
/// with the event loop. This makes it easy to repeat a read, timer,
/// etc. This rearms the request EXACTLY as-is. For example, the
/// low-level timer interface for io_uring uses an absolute timeout.
/// If you rearm the timer, it will fire immediately because the absolute
/// timeout will be in the past.
///
/// The completion is reused so it is not safe to use the same completion
/// for anything else.
rearm = 1,
};
/// The state that a completion can be in.
pub const CompletionState = enum(c_int) {
/// The completion is not being used and is ready to be configured
/// for new work.
dead = 0,
/// The completion is part of an event loop. This may be already waited
/// on or in the process of being registered.
active = 1,
};

174
deps/libxev/src/main.zig vendored Normal file
View File

@@ -0,0 +1,174 @@
const std = @import("std");
const builtin = @import("builtin");
/// The low-level IO interfaces using the recommended compile-time
/// interface for the target system.
const xev = Backend.default().Api();
pub usingnamespace xev;
//pub usingnamespace Epoll;
/// System-specific interfaces. Note that they are always pub for
/// all systems but if you reference them and force them to be analyzed
/// the proper system APIs must exist. Due to Zig's lazy analysis, if you
/// don't use any interface it will NOT be compiled (yay!).
pub const IO_Uring = Xev(.io_uring, @import("backend/io_uring.zig"));
pub const Epoll = Xev(.epoll, @import("backend/epoll.zig"));
pub const Kqueue = Xev(.kqueue, @import("backend/kqueue.zig"));
pub const WasiPoll = Xev(.wasi_poll, @import("backend/wasi_poll.zig"));
pub const IOCP = Xev(.iocp, @import("backend/iocp.zig"));
/// Generic thread pool implementation.
pub const ThreadPool = @import("ThreadPool.zig");
/// This stream (lowercase s) can be used as a namespace to access
/// Closeable, Writeable, Readable, etc. so that custom streams
/// can be constructed.
pub const stream = @import("watcher/stream.zig");
/// The backend types.
pub const Backend = enum {
io_uring,
epoll,
kqueue,
wasi_poll,
iocp,
/// Returns a recommend default backend from inspecting the system.
pub fn default() Backend {
return @as(?Backend, switch (builtin.os.tag) {
.linux => .io_uring,
.ios, .macos => .kqueue,
.wasi => .wasi_poll,
.windows => .iocp,
else => null,
}) orelse {
@compileLog(builtin.os);
@compileError("no default backend for this target");
};
}
/// Returns the Api (return value of Xev) for the given backend type.
pub fn Api(comptime self: Backend) type {
return switch (self) {
.io_uring => IO_Uring,
.epoll => Epoll,
.kqueue => Kqueue,
.wasi_poll => WasiPoll,
.iocp => IOCP,
};
}
};
/// Creates the Xev API based on a backend type.
///
/// For the default backend type for your system (i.e. io_uring on Linux),
/// this is the main API you interact with. It is `usingnamespaced` into
/// the "xev" package so you'd use types such as `xev.Loop`, `xev.Completion`,
/// etc.
///
/// Unless you're using a custom or specific backend type, you do NOT ever
/// need to call the Xev function itself.
pub fn Xev(comptime be: Backend, comptime T: type) type {
return struct {
const Self = @This();
const loop = @import("loop.zig");
/// The backend that this is. This is supplied at comptime so
/// it is up to the caller to say the right thing. This lets custom
/// implementations also "quack" like an implementation.
pub const backend = be;
/// The core loop APIs.
pub const Loop = T.Loop;
pub const Completion = T.Completion;
pub const Result = T.Result;
pub const ReadBuffer = T.ReadBuffer;
pub const WriteBuffer = T.WriteBuffer;
pub const Options = loop.Options;
pub const RunMode = loop.RunMode;
pub const CallbackAction = loop.CallbackAction;
pub const CompletionState = loop.CompletionState;
/// Error types
pub const AcceptError = T.AcceptError;
pub const CancelError = T.CancelError;
pub const CloseError = T.CloseError;
pub const ConnectError = T.ConnectError;
pub const ShutdownError = T.ShutdownError;
pub const WriteError = T.WriteError;
pub const ReadError = T.ReadError;
/// The high-level helper interfaces that make it easier to perform
/// common tasks. These may not work with all possible Loop implementations.
pub const Async = @import("watcher/async.zig").Async(Self);
pub const File = @import("watcher/file.zig").File(Self);
pub const Process = @import("watcher/process.zig").Process(Self);
pub const Stream = stream.GenericStream(Self);
pub const Timer = @import("watcher/timer.zig").Timer(Self);
pub const TCP = @import("watcher/tcp.zig").TCP(Self);
pub const UDP = @import("watcher/udp.zig").UDP(Self);
/// The callback of the main Loop operations. Higher level interfaces may
/// use a different callback mechanism.
pub const Callback = *const fn (
userdata: ?*anyopaque,
loop: *Loop,
completion: *Completion,
result: Result,
) CallbackAction;
/// A way to access the raw type.
pub const Sys = T;
/// A callback that does nothing and immediately disarms. This
/// implements xev.Callback and is the default value for completions.
pub fn noopCallback(
_: ?*anyopaque,
_: *Loop,
_: *Completion,
_: Result,
) CallbackAction {
return .disarm;
}
test {
@import("std").testing.refAllDecls(@This());
}
test "completion is zero-able" {
const c: Completion = .{};
_ = c;
}
};
}
test {
// Tested on all platforms
_ = @import("heap.zig");
_ = @import("queue.zig");
_ = @import("queue_mpsc.zig");
_ = ThreadPool;
// Test the C API
if (builtin.os.tag != .wasi) _ = @import("c_api.zig");
// OS-specific tests
switch (builtin.os.tag) {
.linux => {
_ = Epoll;
_ = IO_Uring;
_ = @import("linux/timerfd.zig");
},
.wasi => {
//_ = WasiPoll;
_ = @import("backend/wasi_poll.zig");
},
.windows => {
_ = @import("backend/iocp.zig");
},
else => {},
}
}

101
deps/libxev/src/queue.zig vendored Normal file
View File

@@ -0,0 +1,101 @@
const std = @import("std");
const assert = std.debug.assert;
/// An intrusive queue implementation. The type T must have a field
/// "next" of type `?*T`.
///
/// For those unaware, an intrusive variant of a data structure is one in which
/// the data type in the list has the pointer to the next element, rather
/// than a higher level "node" or "container" type. The primary benefit
/// of this (and the reason we implement this) is that it defers all memory
/// management to the caller: the data structure implementation doesn't need
/// to allocate "nodes" to contain each element. Instead, the caller provides
/// the element and how its allocated is up to them.
pub fn Intrusive(comptime T: type) type {
return struct {
const Self = @This();
/// Head is the front of the queue and tail is the back of the queue.
head: ?*T = null,
tail: ?*T = null,
/// Enqueue a new element to the back of the queue.
pub fn push(self: *Self, v: *T) void {
assert(v.next == null);
if (self.tail) |tail| {
// If we have elements in the queue, then we add a new tail.
tail.next = v;
self.tail = v;
} else {
// No elements in the queue we setup the initial state.
self.head = v;
self.tail = v;
}
}
/// Dequeue the next element from the queue.
pub fn pop(self: *Self) ?*T {
// The next element is in "head".
const next = self.head orelse return null;
// If the head and tail are equal this is the last element
// so we also set tail to null so we can now be empty.
if (self.head == self.tail) self.tail = null;
// Head is whatever is next (if we're the last element,
// this will be null);
self.head = next.next;
// We set the "next" field to null so that this element
// can be inserted again.
next.next = null;
return next;
}
/// Returns true if the queue is empty.
pub fn empty(self: *const Self) bool {
return self.head == null;
}
};
}
test Intrusive {
const testing = std.testing;
// Types
const Elem = struct {
const Self = @This();
next: ?*Self = null,
};
const Queue = Intrusive(Elem);
var q: Queue = .{};
try testing.expect(q.empty());
// Elems
var elems: [10]Elem = .{.{}} ** 10;
// One
try testing.expect(q.pop() == null);
q.push(&elems[0]);
try testing.expect(!q.empty());
try testing.expect(q.pop().? == &elems[0]);
try testing.expect(q.pop() == null);
try testing.expect(q.empty());
// Two
try testing.expect(q.pop() == null);
q.push(&elems[0]);
q.push(&elems[1]);
try testing.expect(q.pop().? == &elems[0]);
try testing.expect(q.pop().? == &elems[1]);
try testing.expect(q.pop() == null);
// Interleaved
try testing.expect(q.pop() == null);
q.push(&elems[0]);
try testing.expect(q.pop().? == &elems[0]);
q.push(&elems[1]);
try testing.expect(q.pop().? == &elems[1]);
try testing.expect(q.pop() == null);
}

116
deps/libxev/src/queue_mpsc.zig vendored Normal file
View File

@@ -0,0 +1,116 @@
const std = @import("std");
const assert = std.debug.assert;
/// An intrusive MPSC (multi-provider, single consumer) queue implementation.
/// The type T must have a field "next" of type `?*T`.
///
/// This is an implementatin of a Vyukov Queue[1].
/// TODO(mitchellh): I haven't audited yet if I got all the atomic operations
/// correct. I was short term more focused on getting something that seemed
/// to work; I need to make sure it actually works.
///
/// For those unaware, an intrusive variant of a data structure is one in which
/// the data type in the list has the pointer to the next element, rather
/// than a higher level "node" or "container" type. The primary benefit
/// of this (and the reason we implement this) is that it defers all memory
/// management to the caller: the data structure implementation doesn't need
/// to allocate "nodes" to contain each element. Instead, the caller provides
/// the element and how its allocated is up to them.
///
/// [1]: https://www.1024cores.net/home/lock-free-algorithms/queues/intrusive-mpsc-node-based-queue
pub fn Intrusive(comptime T: type) type {
return struct {
const Self = @This();
/// Head is the front of the queue and tail is the back of the queue.
head: *T,
tail: *T,
stub: T,
/// Initialize the queue. This requires a stable pointer to itself.
/// This must be called before the queue is used concurrently.
pub fn init(self: *Self) void {
self.head = &self.stub;
self.tail = &self.stub;
self.stub.next = null;
}
/// Push an item onto the queue. This can be called by any number
/// of producers.
pub fn push(self: *Self, v: *T) void {
@atomicStore(?*T, &v.next, null, .unordered);
const prev = @atomicRmw(*T, &self.head, .Xchg, v, .acq_rel);
@atomicStore(?*T, &prev.next, v, .release);
}
/// Pop the first in element from the queue. This must be called
/// by only a single consumer at any given time.
pub fn pop(self: *Self) ?*T {
var tail = @atomicLoad(*T, &self.tail, .unordered);
var next_ = @atomicLoad(?*T, &tail.next, .acquire);
if (tail == &self.stub) {
const next = next_ orelse return null;
@atomicStore(*T, &self.tail, next, .unordered);
tail = next;
next_ = @atomicLoad(?*T, &tail.next, .acquire);
}
if (next_) |next| {
@atomicStore(*T, &self.tail, next, .release);
tail.next = null;
return tail;
}
const head = @atomicLoad(*T, &self.head, .unordered);
if (tail != head) return null;
self.push(&self.stub);
next_ = @atomicLoad(?*T, &tail.next, .acquire);
if (next_) |next| {
@atomicStore(*T, &self.tail, next, .unordered);
tail.next = null;
return tail;
}
return null;
}
};
}
test Intrusive {
const testing = std.testing;
// Types
const Elem = struct {
const Self = @This();
next: ?*Self = null,
};
const Queue = Intrusive(Elem);
var q: Queue = undefined;
q.init();
// Elems
var elems: [10]Elem = .{.{}} ** 10;
// One
try testing.expect(q.pop() == null);
q.push(&elems[0]);
try testing.expect(q.pop().? == &elems[0]);
try testing.expect(q.pop() == null);
// Two
try testing.expect(q.pop() == null);
q.push(&elems[0]);
q.push(&elems[1]);
try testing.expect(q.pop().? == &elems[0]);
try testing.expect(q.pop().? == &elems[1]);
try testing.expect(q.pop() == null);
// // Interleaved
try testing.expect(q.pop() == null);
q.push(&elems[0]);
try testing.expect(q.pop().? == &elems[0]);
q.push(&elems[1]);
try testing.expect(q.pop().? == &elems[1]);
try testing.expect(q.pop() == null);
}

630
deps/libxev/src/watcher/async.zig vendored Normal file
View File

@@ -0,0 +1,630 @@
/// "Wake up" an event loop from any thread using an async completion.
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const posix = std.posix;
const common = @import("common.zig");
pub fn Async(comptime xev: type) type {
return switch (xev.backend) {
// Supported, uses eventfd
.io_uring,
.epoll,
=> AsyncEventFd(xev),
// Supported, uses the backend API
.wasi_poll => AsyncLoopState(xev, xev.Loop.threaded),
// Supported, uses mach ports
.kqueue => AsyncMachPort(xev),
.iocp => AsyncIOCP(xev),
};
}
/// Async implementation using eventfd (Linux).
fn AsyncEventFd(comptime xev: type) type {
return struct {
const Self = @This();
/// The error that can come in the wait callback.
pub const WaitError = xev.ReadError;
/// eventfd file descriptor
fd: posix.fd_t,
/// Create a new async. An async can be assigned to exactly one loop
/// to be woken up. The completion must be allocated in advance.
pub fn init() !Self {
return .{
.fd = try std.posix.eventfd(0, 0),
};
}
/// Clean up the async. This will forcibly deinitialize any resources
/// and may result in erroneous wait callbacks to be fired.
pub fn deinit(self: *Self) void {
std.posix.close(self.fd);
}
/// Wait for a message on this async. Note that async messages may be
/// coalesced (or they may not be) so you should not expect a 1:1 mapping
/// between send and wait.
///
/// Just like the rest of libxev, the wait must be re-queued if you want
/// to continue to be notified of async events.
///
/// You should NOT register an async with multiple loops (the same loop
/// is fine -- but unnecessary). The behavior when waiting on multiple
/// loops is undefined.
pub fn wait(
self: Self,
loop: *xev.Loop,
c: *xev.Completion,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
r: WaitError!void,
) xev.CallbackAction,
) void {
c.* = .{
.op = .{
.read = .{
.fd = self.fd,
.buffer = .{ .array = undefined },
},
},
.userdata = userdata,
.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
return @call(.always_inline, cb, .{
common.userdataValue(Userdata, ud),
l_inner,
c_inner,
if (r.read) |v| assert(v > 0) else |err| err,
});
}
}).callback,
};
loop.add(c);
}
/// Notify a loop to wake up synchronously. This should never block forever
/// (it will always EVENTUALLY succeed regardless of if the loop is currently
/// ticking or not).
///
/// The "c" value is the completion associated with the "wait".
///
/// Internal details subject to change but if you're relying on these
/// details then you may want to consider using a lower level interface
/// using the loop directly:
///
/// - linux+io_uring: eventfd is used. If the eventfd write would block
/// (EAGAIN) then we assume success because the eventfd is full.
///
pub fn notify(self: Self) !void {
// We want to just write "1" in the correct byte order as our host.
const val = @as([8]u8, @bitCast(@as(u64, 1)));
_ = posix.write(self.fd, &val) catch |err| switch (err) {
error.WouldBlock => return,
else => return err,
};
}
/// Common tests
pub usingnamespace AsyncTests(xev, Self);
};
}
/// Async implementation using mach ports (Darwin).
///
/// This allocates a mach port per async request and sends to that mach
/// port to wake up the loop and trigger the completion.
fn AsyncMachPort(comptime xev: type) type {
return struct {
const Self = @This();
/// The error that can come in the wait callback.
pub const WaitError = xev.Sys.MachPortError;
/// Missing Mach APIs from Zig stdlib. Data from xnu: osfmk/mach/port.h
const mach_port_flavor_t = c_int;
const mach_port_limits = extern struct { mpl_qlimit: c_uint };
const MACH_PORT_LIMITS_INFO = 1;
extern "c" fn mach_port_set_attributes(
task: posix.system.ipc_space_t,
name: posix.system.mach_port_name_t,
flavor: mach_port_flavor_t,
info: *anyopaque,
count: posix.system.mach_msg_type_number_t,
) posix.system.kern_return_t;
extern "c" fn mach_port_destroy(
task: posix.system.ipc_space_t,
name: posix.system.mach_port_name_t,
) posix.system.kern_return_t;
/// The mach port
port: posix.system.mach_port_name_t,
/// Create a new async. An async can be assigned to exactly one loop
/// to be woken up. The completion must be allocated in advance.
pub fn init() !Self {
const mach_self = posix.system.mach_task_self();
// Allocate the port
var mach_port: posix.system.mach_port_name_t = undefined;
switch (posix.system.getKernError(posix.system.mach_port_allocate(
mach_self,
@intFromEnum(posix.system.MACH_PORT_RIGHT.RECEIVE),
&mach_port,
))) {
.SUCCESS => {}, // Success
else => return error.MachPortAllocFailed,
}
errdefer _ = mach_port_destroy(mach_self, mach_port);
// Insert a send right into the port since we also use this to send
switch (posix.system.getKernError(posix.system.mach_port_insert_right(
mach_self,
mach_port,
mach_port,
@intFromEnum(posix.system.MACH_MSG_TYPE.MAKE_SEND),
))) {
.SUCCESS => {}, // Success
else => return error.MachPortAllocFailed,
}
// Modify the port queue size to be 1 because we are only
// using it for notifications and not for any other purpose.
var limits: mach_port_limits = .{ .mpl_qlimit = 1 };
switch (posix.system.getKernError(mach_port_set_attributes(
mach_self,
mach_port,
MACH_PORT_LIMITS_INFO,
&limits,
@sizeOf(@TypeOf(limits)),
))) {
.SUCCESS => {}, // Success
else => return error.MachPortAllocFailed,
}
return .{
.port = mach_port,
};
}
/// Clean up the async. This will forcibly deinitialize any resources
/// and may result in erroneous wait callbacks to be fired.
pub fn deinit(self: *Self) void {
_ = mach_port_destroy(
posix.system.mach_task_self(),
self.port,
);
}
/// Wait for a message on this async. Note that async messages may be
/// coalesced (or they may not be) so you should not expect a 1:1 mapping
/// between send and wait.
///
/// Just like the rest of libxev, the wait must be re-queued if you want
/// to continue to be notified of async events.
///
/// You should NOT register an async with multiple loops (the same loop
/// is fine -- but unnecessary). The behavior when waiting on multiple
/// loops is undefined.
pub fn wait(
self: Self,
loop: *xev.Loop,
c: *xev.Completion,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
r: WaitError!void,
) xev.CallbackAction,
) void {
c.* = .{
.op = .{
.machport = .{
.port = self.port,
.buffer = .{ .array = undefined },
},
},
.userdata = userdata,
.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
// Drain the mach port so that we only fire one
// notification even if many are queued.
drain(c_inner.op.machport.port);
return @call(.always_inline, cb, .{
common.userdataValue(Userdata, ud),
l_inner,
c_inner,
if (r.machport) |_| {} else |err| err,
});
}
}).callback,
};
loop.add(c);
}
/// Drain the given mach port. All message bodies are discarded.
fn drain(port: posix.system.mach_port_name_t) void {
var message: struct {
header: posix.system.mach_msg_header_t,
} = undefined;
while (true) {
switch (posix.system.getMachMsgError(posix.system.mach_msg(
&message.header,
posix.system.MACH_RCV_MSG | posix.system.MACH_RCV_TIMEOUT,
0,
@sizeOf(@TypeOf(message)),
port,
posix.system.MACH_MSG_TIMEOUT_NONE,
posix.system.MACH_PORT_NULL,
))) {
// This means a read would've blocked, so we drained.
.RCV_TIMED_OUT => return,
// We dequeued, so we want to loop again.
.SUCCESS => {},
// We dequeued but the message had a body. We ignore
// message bodies for async so we are happy to discard
// it and continue.
.RCV_TOO_LARGE => {},
else => |err| {
std.log.warn("mach msg drain err, may duplicate async wakeups err={}", .{err});
return;
},
}
}
}
/// Notify a loop to wake up synchronously. This should never block forever
/// (it will always EVENTUALLY succeed regardless of if the loop is currently
/// ticking or not).
pub fn notify(self: Self) !void {
// This constructs an empty mach message. It has no data.
var msg: posix.system.mach_msg_header_t = .{
// We use COPY_SEND which will not increment any send ref
// counts because it'll reuse the existing send right.
.msgh_bits = @intFromEnum(posix.system.MACH_MSG_TYPE.COPY_SEND),
.msgh_size = @sizeOf(posix.system.mach_msg_header_t),
.msgh_remote_port = self.port,
.msgh_local_port = posix.system.MACH_PORT_NULL,
.msgh_voucher_port = undefined,
.msgh_id = undefined,
};
return switch (posix.system.getMachMsgError(
posix.system.mach_msg(
&msg,
posix.system.MACH_SEND_MSG | posix.system.MACH_SEND_TIMEOUT,
msg.msgh_size,
0,
posix.system.MACH_PORT_NULL,
0, // Fail instantly if the port is full
posix.system.MACH_PORT_NULL,
),
)) {
.SUCCESS => {},
else => |e| {
std.log.warn("mach msg err={}", .{e});
return error.MachMsgFailed;
},
// This is okay because it means that there was no more buffer
// space meaning that the port will wake up.
.SEND_NO_BUFFER => {},
// This means that the send would've blocked because the
// queue is full. We assume success because the port is full.
.SEND_TIMED_OUT => {},
};
}
/// Common tests
pub usingnamespace AsyncTests(xev, Self);
};
}
/// Async implementation that is deferred to the backend implementation
/// loop state. This is kind of a hacky implementation and not recommended
/// but its the only way currently to get asyncs to work on WASI.
fn AsyncLoopState(comptime xev: type, comptime threaded: bool) type {
// TODO: we don't support threaded loop state async. We _can_ it just
// isn't done yet. To support it we need to have some sort of mutex
// to guard waiter below.
if (threaded) return struct {};
return struct {
const Self = @This();
wakeup: bool = false,
waiter: ?struct {
loop: *xev.Loop,
c: *xev.Completion,
} = null,
/// The error that can come in the wait callback.
pub const WaitError = xev.Sys.AsyncError;
pub fn init() !Self {
return .{};
}
pub fn deinit(self: *Self) void {
_ = self;
}
pub fn wait(
self: *Self,
loop: *xev.Loop,
c: *xev.Completion,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
r: WaitError!void,
) xev.CallbackAction,
) void {
c.* = .{
.op = .{
.async_wait = .{},
},
.userdata = userdata,
.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
return @call(.always_inline, cb, .{
common.userdataValue(Userdata, ud),
l_inner,
c_inner,
if (r.async_wait) |_| {} else |err| err,
});
}
}).callback,
};
loop.add(c);
self.waiter = .{
.loop = loop,
.c = c,
};
if (self.wakeup) self.notify() catch {};
}
pub fn notify(self: *Self) !void {
if (self.waiter) |w|
w.loop.async_notify(w.c)
else
self.wakeup = true;
}
/// Common tests
pub usingnamespace AsyncTests(xev, Self);
};
}
/// Async implementation for IOCP.
fn AsyncIOCP(comptime xev: type) type {
return struct {
const Self = @This();
const windows = std.os.windows;
pub const WaitError = xev.Sys.AsyncError;
guard: std.Thread.Mutex = .{},
wakeup: bool = false,
waiter: ?struct {
loop: *xev.Loop,
c: *xev.Completion,
} = null,
pub fn init() !Self {
return Self{};
}
pub fn deinit(self: *Self) void {
_ = self;
}
pub fn wait(
self: *Self,
loop: *xev.Loop,
c: *xev.Completion,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
r: WaitError!void,
) xev.CallbackAction,
) void {
c.* = xev.Completion{
.op = .{ .async_wait = .{} },
.userdata = userdata,
.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
return @call(.always_inline, cb, .{
common.userdataValue(Userdata, ud),
l_inner,
c_inner,
if (r.async_wait) |_| {} else |err| err,
});
}
}).callback,
};
loop.add(c);
self.guard.lock();
defer self.guard.unlock();
self.waiter = .{
.loop = loop,
.c = c,
};
if (self.wakeup) loop.async_notify(c);
}
pub fn notify(self: *Self) !void {
self.guard.lock();
defer self.guard.unlock();
if (self.waiter) |w| {
w.loop.async_notify(w.c);
} else {
self.wakeup = true;
}
}
/// Common tests
pub usingnamespace AsyncTests(xev, Self);
};
}
fn AsyncTests(comptime xev: type, comptime Impl: type) type {
return struct {
test "async" {
const testing = std.testing;
var loop = try xev.Loop.init(.{});
defer loop.deinit();
var notifier = try Impl.init();
defer notifier.deinit();
// Wait
var wake: bool = false;
var c_wait: xev.Completion = undefined;
notifier.wait(&loop, &c_wait, bool, &wake, (struct {
fn callback(
ud: ?*bool,
_: *xev.Loop,
_: *xev.Completion,
r: Impl.WaitError!void,
) xev.CallbackAction {
_ = r catch unreachable;
ud.?.* = true;
return .disarm;
}
}).callback);
// Send a notification
try notifier.notify();
// Wait for wake
try loop.run(.until_done);
try testing.expect(wake);
}
test "async: notify first" {
const testing = std.testing;
var loop = try xev.Loop.init(.{});
defer loop.deinit();
var notifier = try Impl.init();
defer notifier.deinit();
// Send a notification
try notifier.notify();
// Wait
var wake: bool = false;
var c_wait: xev.Completion = undefined;
notifier.wait(&loop, &c_wait, bool, &wake, (struct {
fn callback(
ud: ?*bool,
_: *xev.Loop,
_: *xev.Completion,
r: Impl.WaitError!void,
) xev.CallbackAction {
_ = r catch unreachable;
ud.?.* = true;
return .disarm;
}
}).callback);
// Wait for wake
try loop.run(.until_done);
try testing.expect(wake);
}
test "async batches multiple notifications" {
const testing = std.testing;
var loop = try xev.Loop.init(.{});
defer loop.deinit();
var notifier = try Impl.init();
defer notifier.deinit();
// Send a notification many times
try notifier.notify();
try notifier.notify();
try notifier.notify();
try notifier.notify();
try notifier.notify();
// Wait
var count: u32 = 0;
var c_wait: xev.Completion = undefined;
notifier.wait(&loop, &c_wait, u32, &count, (struct {
fn callback(
ud: ?*u32,
_: *xev.Loop,
_: *xev.Completion,
r: Impl.WaitError!void,
) xev.CallbackAction {
_ = r catch unreachable;
ud.?.* += 1;
return .rearm;
}
}).callback);
// Send a notification
try notifier.notify();
// Wait for wake
try loop.run(.once);
for (0..10) |_| try loop.run(.no_wait);
try testing.expectEqual(@as(u32, 1), count);
}
};
}

7
deps/libxev/src/watcher/common.zig vendored Normal file
View File

@@ -0,0 +1,7 @@
/// Convert the callback value with an opaque pointer into the userdata type
/// that we can pass to our higher level callback types.
pub fn userdataValue(comptime Userdata: type, v: ?*anyopaque) ?*Userdata {
// Void userdata is always a null pointer.
if (Userdata == void) return null;
return @ptrCast(@alignCast(v));
}

542
deps/libxev/src/watcher/file.zig vendored Normal file
View File

@@ -0,0 +1,542 @@
const std = @import("std");
const builtin = @import("builtin");
const common = @import("common.zig");
const assert = std.debug.assert;
const posix = std.posix;
const main = @import("../main.zig");
const stream = @import("stream.zig");
/// File operations.
///
/// These operations typically run on the event loop thread pool, rather
/// than the core async OS APIs, because most core async OS APIs don't support
/// async operations on regular files (with many caveats attached to that
/// statement). This high-level abstraction will attempt to make the right
/// decision about what to do but this should generally be used by
/// operations that need to run on a thread pool. For operations that you're
/// sure are better supported by core async OS APIs (such as sockets, pipes,
/// TTYs, etc.), use a specific high-level abstraction like xev.TCP or
/// the generic xev.Stream.
///
/// This is a "higher-level abstraction" in libxev. The goal of higher-level
/// abstractions in libxev are to make it easier to use specific functionality
/// with the event loop, but does not promise perfect flexibility or optimal
/// performance. In almost all cases, the abstraction is good enough. But,
/// if you have specific needs or want to push for the most optimal performance,
/// use the platform-specific Loop directly.
pub fn File(comptime xev: type) type {
return struct {
const Self = @This();
const FdType = if (xev.backend == .iocp) std.windows.HANDLE else posix.socket_t;
/// The underlying file
fd: FdType,
pub usingnamespace stream.Stream(xev, Self, .{
.close = true,
.read = .read,
.write = .write,
.threadpool = true,
});
/// Initialize a File from a std.fs.File.
pub fn init(file: std.fs.File) !Self {
return .{
.fd = file.handle,
};
}
/// Initialize a File from a file descriptor.
pub fn initFd(fd: std.fs.File.Handle) Self {
return .{
.fd = fd,
};
}
/// Clean up any watcher resources. This does NOT close the file.
/// If you want to close the file you must call close or do so
/// synchronously.
pub fn deinit(self: *const File) void {
_ = self;
}
pub fn pread(
self: Self,
loop: *xev.Loop,
c: *xev.Completion,
buf: xev.ReadBuffer,
offset: u64,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
s: Self,
b: xev.ReadBuffer,
r: Self.ReadError!usize,
) xev.CallbackAction,
) void {
switch (buf) {
inline .slice, .array => {
c.* = .{
.op = .{
.pread = .{
.fd = self.fd,
.buffer = buf,
.offset = offset,
},
},
.userdata = userdata,
.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
return @call(.always_inline, cb, .{
common.userdataValue(Userdata, ud),
l_inner,
c_inner,
Self.initFd(c_inner.op.pread.fd),
c_inner.op.pread.buffer,
if (r.pread) |v| v else |err| err,
});
}
}).callback,
};
// If we're dup-ing, then we ask the backend to manage the fd.
switch (xev.backend) {
.io_uring,
.wasi_poll,
.iocp,
=> {},
.epoll => {
c.flags.threadpool = true;
},
.kqueue => {
c.flags.threadpool = true;
},
}
loop.add(c);
},
}
}
pub fn queuePWrite(
self: Self,
loop: *xev.Loop,
q: *Self.WriteQueue,
req: *Self.WriteRequest,
buf: xev.WriteBuffer,
offset: u64,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
s: Self,
b: xev.WriteBuffer,
r: Self.WriteError!usize,
) xev.CallbackAction,
) void {
// Initialize our completion
req.* = .{};
self.pwrite_init(&req.completion, buf, offset);
req.completion.userdata = q;
req.completion.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
const q_inner = @as(?*Self.WriteQueue, @ptrCast(@alignCast(ud))).?;
// The queue MUST have a request because a completion
// can only be added if the queue is not empty, and
// nothing else should be popping!.
const req_inner = q_inner.pop().?;
const cb_res = pwrite_result(c_inner, r);
const action = @call(.always_inline, cb, .{
common.userdataValue(Userdata, req_inner.userdata),
l_inner,
c_inner,
cb_res.writer,
cb_res.buf,
cb_res.result,
});
// Rearm requeues this request, it doesn't return rearm
// on the actual callback here...
if (action == .rearm) q_inner.push(req_inner);
// If we have another request, add that completion next.
if (q_inner.head) |req_next| l_inner.add(&req_next.completion);
// We always disarm because the completion in the next
// request will be used if there is more to queue.
return .disarm;
}
}).callback;
// The userdata as to go on the WriteRequest because we need
// our actual completion userdata to be the WriteQueue so that
// we can process the queue.
req.userdata = @as(?*anyopaque, @ptrCast(@alignCast(userdata)));
// If the queue is empty, then we add our completion. Otherwise,
// the previously queued writes will trigger this one.
if (q.empty()) loop.add(&req.completion);
// We always add this item to our queue no matter what
q.push(req);
}
pub fn pwrite(
self: Self,
loop: *xev.Loop,
c: *xev.Completion,
buf: xev.WriteBuffer,
offset: u64,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
s: Self,
b: xev.WriteBuffer,
r: Self.WriteError!usize,
) xev.CallbackAction,
) void {
self.pwrite_init(c, buf, offset);
c.userdata = userdata;
c.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
const cb_res = pwrite_result(c_inner, r);
return @call(.always_inline, cb, .{
common.userdataValue(Userdata, ud),
l_inner,
c_inner,
cb_res.writer,
cb_res.buf,
cb_res.result,
});
}
}).callback;
loop.add(c);
}
inline fn pwrite_result(c: *xev.Completion, r: xev.Result) struct {
writer: Self,
buf: xev.WriteBuffer,
result: Self.WriteError!usize,
} {
return .{
.writer = Self.initFd(c.op.pwrite.fd),
.buf = c.op.pwrite.buffer,
.result = if (r.pwrite) |v| v else |err| err,
};
}
fn pwrite_init(
self: Self,
c: *xev.Completion,
buf: xev.WriteBuffer,
offset: u64,
) void {
switch (buf) {
inline .slice, .array => {
c.* = .{
.op = .{
.pwrite = .{
.fd = self.fd,
.buffer = buf,
.offset = offset,
},
},
};
// If we're dup-ing, then we ask the backend to manage the fd.
switch (xev.backend) {
.io_uring,
.wasi_poll,
.iocp,
=> {},
.epoll => {
c.flags.threadpool = true;
},
.kqueue => {
c.flags.threadpool = true;
},
}
},
}
}
test "read/write" {
// wasi: local files don't work with poll (always ready)
if (builtin.os.tag == .wasi) return error.SkipZigTest;
// windows: std.fs.File is not opened with OVERLAPPED flag.
if (builtin.os.tag == .windows) return error.SkipZigTest;
const testing = std.testing;
var tpool = main.ThreadPool.init(.{});
defer tpool.deinit();
defer tpool.shutdown();
var loop = try xev.Loop.init(.{ .thread_pool = &tpool });
defer loop.deinit();
// Create our file
const path = "test_watcher_file";
const f = try std.fs.cwd().createFile(path, .{
.read = true,
.truncate = true,
});
defer f.close();
defer std.fs.cwd().deleteFile(path) catch {};
const file = try init(f);
// Perform a write and then a read
var write_buf = [_]u8{ 1, 1, 2, 3, 5, 8, 13 };
var c_write: xev.Completion = undefined;
file.write(&loop, &c_write, .{ .slice = &write_buf }, void, null, (struct {
fn callback(
_: ?*void,
_: *xev.Loop,
_: *xev.Completion,
_: Self,
_: xev.WriteBuffer,
r: Self.WriteError!usize,
) xev.CallbackAction {
_ = r catch unreachable;
return .disarm;
}
}).callback);
// Wait for the write
try loop.run(.until_done);
// Make sure the data is on disk
try f.sync();
const f2 = try std.fs.cwd().openFile(path, .{});
defer f2.close();
const file2 = try init(f2);
// Read
var read_buf: [128]u8 = undefined;
var read_len: usize = 0;
file2.read(&loop, &c_write, .{ .slice = &read_buf }, usize, &read_len, (struct {
fn callback(
ud: ?*usize,
_: *xev.Loop,
_: *xev.Completion,
_: Self,
_: xev.ReadBuffer,
r: Self.ReadError!usize,
) xev.CallbackAction {
ud.?.* = r catch unreachable;
return .disarm;
}
}).callback);
try loop.run(.until_done);
try testing.expectEqual(read_len, write_buf.len);
try testing.expectEqualSlices(u8, &write_buf, read_buf[0..read_len]);
}
test "pread/pwrite" {
// wasi: local files don't work with poll (always ready)
if (builtin.os.tag == .wasi) return error.SkipZigTest;
// windows: std.fs.File is not opened with OVERLAPPED flag.
if (builtin.os.tag == .windows) return error.SkipZigTest;
const testing = std.testing;
var tpool = main.ThreadPool.init(.{});
defer tpool.deinit();
defer tpool.shutdown();
var loop = try xev.Loop.init(.{ .thread_pool = &tpool });
defer loop.deinit();
// Create our file
const path = "test_watcher_file";
const f = try std.fs.cwd().createFile(path, .{
.read = true,
.truncate = true,
});
defer f.close();
defer std.fs.cwd().deleteFile(path) catch {};
const file = try init(f);
// Perform a write and then a read
var write_buf = [_]u8{ 1, 1, 2, 3, 5, 8, 13 };
var c_write: xev.Completion = undefined;
file.pwrite(&loop, &c_write, .{ .slice = &write_buf }, 0, void, null, (struct {
fn callback(
_: ?*void,
_: *xev.Loop,
_: *xev.Completion,
_: Self,
_: xev.WriteBuffer,
r: Self.WriteError!usize,
) xev.CallbackAction {
_ = r catch unreachable;
return .disarm;
}
}).callback);
// Wait for the write
try loop.run(.until_done);
// Make sure the data is on disk
try f.sync();
const f2 = try std.fs.cwd().openFile(path, .{});
defer f2.close();
const file2 = try init(f2);
var read_buf: [128]u8 = undefined;
var read_len: usize = 0;
file2.pread(&loop, &c_write, .{ .slice = &read_buf }, 0, usize, &read_len, (struct {
fn callback(
ud: ?*usize,
_: *xev.Loop,
_: *xev.Completion,
_: Self,
_: xev.ReadBuffer,
r: Self.ReadError!usize,
) xev.CallbackAction {
ud.?.* = r catch unreachable;
return .disarm;
}
}).callback);
try loop.run(.until_done);
try testing.expectEqualSlices(u8, &write_buf, read_buf[0..read_len]);
}
test "queued writes" {
// wasi: local files don't work with poll (always ready)
if (builtin.os.tag == .wasi) return error.SkipZigTest;
// windows: std.fs.File is not opened with OVERLAPPED flag.
if (builtin.os.tag == .windows) return error.SkipZigTest;
const testing = std.testing;
var tpool = main.ThreadPool.init(.{});
defer tpool.deinit();
defer tpool.shutdown();
var loop = try xev.Loop.init(.{ .thread_pool = &tpool });
defer loop.deinit();
// Create our file
const path = "test_watcher_file";
const f = try std.fs.cwd().createFile(path, .{
.read = true,
.truncate = true,
});
defer f.close();
defer std.fs.cwd().deleteFile(path) catch {};
const file = try init(f);
var write_queue: Self.WriteQueue = .{};
var write_req: [2]Self.WriteRequest = undefined;
// Perform a write and then a read
file.queueWrite(
&loop,
&write_queue,
&write_req[0],
.{ .slice = "1234" },
void,
null,
(struct {
fn callback(
_: ?*void,
_: *xev.Loop,
_: *xev.Completion,
_: Self,
_: xev.WriteBuffer,
r: Self.WriteError!usize,
) xev.CallbackAction {
_ = r catch unreachable;
return .disarm;
}
}).callback,
);
file.queueWrite(
&loop,
&write_queue,
&write_req[1],
.{ .slice = "5678" },
void,
null,
(struct {
fn callback(
_: ?*void,
_: *xev.Loop,
_: *xev.Completion,
_: Self,
_: xev.WriteBuffer,
r: Self.WriteError!usize,
) xev.CallbackAction {
_ = r catch unreachable;
return .disarm;
}
}).callback,
);
// Wait for the write
try loop.run(.until_done);
// Make sure the data is on disk
try f.sync();
const f2 = try std.fs.cwd().openFile(path, .{});
defer f2.close();
const file2 = try init(f2);
// Read
var read_buf: [128]u8 = undefined;
var read_len: usize = 0;
var c_read: xev.Completion = undefined;
file2.read(&loop, &c_read, .{ .slice = &read_buf }, usize, &read_len, (struct {
fn callback(
ud: ?*usize,
_: *xev.Loop,
_: *xev.Completion,
_: Self,
_: xev.ReadBuffer,
r: Self.ReadError!usize,
) xev.CallbackAction {
ud.?.* = r catch unreachable;
return .disarm;
}
}).callback);
try loop.run(.until_done);
try testing.expectEqualSlices(u8, "12345678", read_buf[0..read_len]);
}
};
}

447
deps/libxev/src/watcher/process.zig vendored Normal file
View File

@@ -0,0 +1,447 @@
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const linux = std.os.linux;
const posix = std.posix;
const common = @import("common.zig");
/// Process management, such as waiting for process exit.
pub fn Process(comptime xev: type) type {
return switch (xev.backend) {
// Supported, uses pidfd
.io_uring,
.epoll,
=> ProcessPidFd(xev),
.kqueue => ProcessKqueue(xev),
.iocp => ProcessIocp(xev),
// Unsupported
.wasi_poll => struct {},
};
}
/// Process implementation using pidfd (Linux).
fn ProcessPidFd(comptime xev: type) type {
return struct {
const Self = @This();
/// The error that can come in the wait callback.
pub const WaitError = xev.Sys.PollError || error{
InvalidChild,
};
/// pidfd file descriptor
fd: posix.fd_t,
/// Create a new process watcher for the given pid.
pub fn init(pid: posix.pid_t) !Self {
// Note: SOCK_NONBLOCK == PIDFD_NONBLOCK but we should PR that
// over to Zig.
const res = linux.pidfd_open(pid, posix.SOCK.NONBLOCK);
const fd = switch (posix.errno(res)) {
.SUCCESS => @as(posix.fd_t, @intCast(res)),
.INVAL => return error.InvalidArgument,
.MFILE => return error.ProcessFdQuotaExceeded,
.NFILE => return error.SystemFdQuotaExceeded,
.NODEV => return error.SystemResources,
.NOMEM => return error.SystemResources,
else => |err| return posix.unexpectedErrno(err),
};
return .{
.fd = fd,
};
}
/// Clean up the process watcher.
pub fn deinit(self: *Self) void {
std.posix.close(self.fd);
}
/// Wait for the process to exit. This will automatically call
/// `waitpid` or equivalent and report the exit status.
pub fn wait(
self: Self,
loop: *xev.Loop,
c: *xev.Completion,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
r: WaitError!u32,
) xev.CallbackAction,
) void {
const events: u32 = comptime switch (xev.backend) {
.io_uring => posix.POLL.IN,
.epoll => linux.EPOLL.IN,
else => unreachable,
};
c.* = .{
.op = .{
.poll = .{
.fd = self.fd,
.events = events,
},
},
.userdata = userdata,
.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
const arg: WaitError!u32 = arg: {
// If our poll failed, report that error.
_ = r.poll catch |err| break :arg err;
// We need to wait on the pidfd because it is noted as ready
const fd = c_inner.op.poll.fd;
var info: linux.siginfo_t = undefined;
const res = linux.waitid(.PIDFD, fd, &info, linux.W.EXITED);
break :arg switch (posix.errno(res)) {
.SUCCESS => @as(u32, @intCast(info.fields.common.second.sigchld.status)),
.CHILD => error.InvalidChild,
// The fd isn't ready to read, I guess?
.AGAIN => return .rearm,
else => |err| err: {
std.log.warn("unexpected process wait errno={}", .{err});
break :err error.Unexpected;
},
};
};
return @call(.always_inline, cb, .{
common.userdataValue(Userdata, ud),
l_inner,
c_inner,
arg,
});
}
}).callback,
};
loop.add(c);
}
/// Common tests
pub usingnamespace ProcessTests(xev, Self, &.{ "sh", "-c", "exit 0" }, &.{ "sh", "-c", "exit 42" });
};
}
fn ProcessKqueue(comptime xev: type) type {
return struct {
const Self = @This();
/// The error that can come in the wait callback.
pub const WaitError = xev.Sys.ProcError;
/// The pid to watch.
pid: posix.pid_t,
/// Create a new process watcher for the given pid.
pub fn init(pid: posix.pid_t) !Self {
return .{
.pid = pid,
};
}
/// Does nothing for Kqueue.
pub fn deinit(self: *Self) void {
_ = self;
}
/// Wait for the process to exit. This will automatically call
/// `waitpid` or equivalent and report the exit status.
pub fn wait(
self: Self,
loop: *xev.Loop,
c: *xev.Completion,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
r: WaitError!u32,
) xev.CallbackAction,
) void {
c.* = .{
.op = .{
.proc = .{
.pid = self.pid,
.flags = posix.system.NOTE_EXIT | posix.system.NOTE_EXITSTATUS,
},
},
.userdata = userdata,
.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
return @call(.always_inline, cb, .{
common.userdataValue(Userdata, ud),
l_inner,
c_inner,
if (r.proc) |v| v else |err| err,
});
}
}).callback,
};
loop.add(c);
}
/// Common tests
pub usingnamespace ProcessTests(xev, Self, &.{ "sh", "-c", "exit 0" }, &.{ "sh", "-c", "exit 42" });
};
}
const windows = @import("../windows.zig");
fn ProcessIocp(comptime xev: type) type {
return struct {
const Self = @This();
pub const WaitError = xev.Sys.JobObjectError;
job: windows.HANDLE,
process: windows.HANDLE,
pub fn init(process: posix.pid_t) !Self {
const current_process = windows.kernel32.GetCurrentProcess();
// Duplicate the process handle so we don't rely on the caller keeping it alive
var dup_process: windows.HANDLE = undefined;
const dup_result = windows.kernel32.DuplicateHandle(
current_process,
process,
current_process,
&dup_process,
0,
windows.FALSE,
windows.DUPLICATE_SAME_ACCESS,
);
if (dup_result == 0) return windows.unexpectedError(windows.kernel32.GetLastError());
const job = try windows.exp.CreateJobObject(null, null);
errdefer _ = windows.kernel32.CloseHandle(job);
try windows.exp.AssignProcessToJobObject(job, dup_process);
return .{
.job = job,
.process = dup_process,
};
}
pub fn deinit(self: *Self) void {
_ = windows.kernel32.CloseHandle(self.job);
_ = windows.kernel32.CloseHandle(self.process);
}
pub fn wait(
self: Self,
loop: *xev.Loop,
c: *xev.Completion,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
r: WaitError!u32,
) xev.CallbackAction,
) void {
c.* = .{
.op = .{
.job_object = .{
.job = self.job,
.userdata = self.process,
},
},
.userdata = userdata,
.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
if (r.job_object) |result| {
switch (result) {
.associated => {
// There was a period of time between when the job object was created
// and when it was associated with the completion port. We may have
// missed a notification, so check if it's still alive.
var exit_code: windows.DWORD = undefined;
const process: windows.HANDLE = @ptrCast(c_inner.op.job_object.userdata);
const has_code = windows.kernel32.GetExitCodeProcess(process, &exit_code) != 0;
if (!has_code) std.log.warn("unable to get exit code for process={}", .{windows.kernel32.GetLastError()});
if (exit_code == windows.exp.STILL_ACTIVE) return .rearm;
return @call(.always_inline, cb, .{
common.userdataValue(Userdata, ud),
l_inner,
c_inner,
exit_code,
});
},
.message => |message| {
const result_inner = switch (message.type) {
.JOB_OBJECT_MSG_EXIT_PROCESS,
.JOB_OBJECT_MSG_ABNORMAL_EXIT_PROCESS,
=> b: {
const process: windows.HANDLE = @ptrCast(c_inner.op.job_object.userdata);
const pid = windows.exp.kernel32.GetProcessId(process);
if (pid == 0) break :b WaitError.Unexpected;
if (message.value != pid) return .rearm;
var exit_code: windows.DWORD = undefined;
const has_code = windows.kernel32.GetExitCodeProcess(process, &exit_code) != 0;
if (!has_code) std.log.warn("unable to get exit code for process={}", .{windows.kernel32.GetLastError()});
break :b if (has_code) exit_code else WaitError.Unexpected;
},
else => return .rearm,
};
return @call(.always_inline, cb, .{ common.userdataValue(Userdata, ud), l_inner, c_inner, result_inner });
},
}
} else |err| {
return @call(.always_inline, cb, .{
common.userdataValue(Userdata, ud),
l_inner,
c_inner,
err,
});
}
}
}).callback,
};
loop.add(c);
}
/// Common tests
pub usingnamespace ProcessTests(xev, Self, &.{ "cmd.exe", "/C", "exit 0" }, &.{ "cmd.exe", "/C", "exit 42" });
};
}
fn ProcessTests(
comptime xev: type,
comptime Impl: type,
comptime argv_0: []const []const u8,
comptime argv_42: []const []const u8,
) type {
return struct {
test "process wait" {
const testing = std.testing;
const alloc = testing.allocator;
var child = std.process.Child.init(argv_0, alloc);
try child.spawn();
var loop = try xev.Loop.init(.{});
defer loop.deinit();
var p = try Impl.init(child.id);
defer p.deinit();
// Wait
var code: ?u32 = null;
var c_wait: xev.Completion = undefined;
p.wait(&loop, &c_wait, ?u32, &code, (struct {
fn callback(
ud: ?*?u32,
_: *xev.Loop,
_: *xev.Completion,
r: Impl.WaitError!u32,
) xev.CallbackAction {
ud.?.* = r catch unreachable;
return .disarm;
}
}).callback);
// Wait for wake
try loop.run(.until_done);
try testing.expectEqual(@as(u32, 0), code.?);
}
test "process wait with non-zero exit code" {
const testing = std.testing;
const alloc = testing.allocator;
var child = std.process.Child.init(argv_42, alloc);
try child.spawn();
var loop = try xev.Loop.init(.{});
defer loop.deinit();
var p = try Impl.init(child.id);
defer p.deinit();
// Wait
var code: ?u32 = null;
var c_wait: xev.Completion = undefined;
p.wait(&loop, &c_wait, ?u32, &code, (struct {
fn callback(
ud: ?*?u32,
_: *xev.Loop,
_: *xev.Completion,
r: Impl.WaitError!u32,
) xev.CallbackAction {
ud.?.* = r catch unreachable;
return .disarm;
}
}).callback);
// Wait for wake
try loop.run(.until_done);
try testing.expectEqual(@as(u32, 42), code.?);
}
test "process wait on a process that already exited" {
const testing = std.testing;
const alloc = testing.allocator;
var child = std.process.Child.init(argv_0, alloc);
try child.spawn();
var loop = try xev.Loop.init(.{});
defer loop.deinit();
var p = try Impl.init(child.id);
defer p.deinit();
_ = try child.wait();
// Wait
var code: ?u32 = null;
var c_wait: xev.Completion = undefined;
p.wait(&loop, &c_wait, ?u32, &code, (struct {
fn callback(
ud: ?*?u32,
_: *xev.Loop,
_: *xev.Completion,
r: Impl.WaitError!u32,
) xev.CallbackAction {
ud.?.* = r catch 0;
return .disarm;
}
}).callback);
// Wait for wake
try loop.run(.until_done);
try testing.expectEqual(@as(u32, 0), code.?);
}
};
}

822
deps/libxev/src/watcher/stream.zig vendored Normal file
View File

@@ -0,0 +1,822 @@
const std = @import("std");
const assert = std.debug.assert;
const builtin = @import("builtin");
const common = @import("common.zig");
const queue = @import("../queue.zig");
/// Options for creating a stream type. Each of the options makes the
/// functionality available for the stream.
pub const Options = struct {
read: ReadMethod,
write: WriteMethod,
close: bool,
/// True to schedule the read/write on the threadpool.
threadpool: bool = false,
pub const ReadMethod = enum { none, read, recv };
pub const WriteMethod = enum { none, write, send };
};
/// Creates a stream type that is meant to be embedded within other
/// types using "usingnamespace". A stream is something that supports read,
/// write, close, etc. The exact operations supported are defined by the
/// "options" struct.
///
/// T requirements:
/// - field named "fd" of type fd_t or socket_t
/// - decl named "initFd" to initialize a new T from a fd
///
pub fn Stream(comptime xev: type, comptime T: type, comptime options: Options) type {
return struct {
pub usingnamespace if (options.close) Closeable(xev, T, options) else struct {};
pub usingnamespace if (options.read != .none) Readable(xev, T, options) else struct {};
pub usingnamespace if (options.write != .none) Writeable(xev, T, options) else struct {};
};
}
pub fn Closeable(comptime xev: type, comptime T: type, comptime options: Options) type {
_ = options;
return struct {
const Self = T;
pub const CloseError = xev.CloseError;
/// Close the socket.
pub fn close(
self: Self,
loop: *xev.Loop,
c: *xev.Completion,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
s: Self,
r: CloseError!void,
) xev.CallbackAction,
) void {
c.* = .{
.op = .{ .close = .{ .fd = self.fd } },
.userdata = userdata,
.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
const fd = T.initFd(c_inner.op.close.fd);
return @call(.always_inline, cb, .{
common.userdataValue(Userdata, ud),
l_inner,
c_inner,
fd,
if (r.close) |_| {} else |err| err,
});
}
}).callback,
};
loop.add(c);
}
};
}
pub fn Readable(comptime xev: type, comptime T: type, comptime options: Options) type {
return struct {
const Self = T;
pub const ReadError = xev.ReadError;
/// Read from the socket. This performs a single read. The callback must
/// requeue the read if additional reads want to be performed. Additional
/// reads simultaneously can be queued by calling this multiple times. Note
/// that depending on the backend, the reads can happen out of order.
pub fn read(
self: Self,
loop: *xev.Loop,
c: *xev.Completion,
buf: xev.ReadBuffer,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
s: Self,
b: xev.ReadBuffer,
r: ReadError!usize,
) xev.CallbackAction,
) void {
switch (buf) {
inline .slice, .array => {
c.* = .{
.op = switch (options.read) {
.none => unreachable,
.read => .{
.read = .{
.fd = self.fd,
.buffer = buf,
},
},
.recv => .{
.recv = .{
.fd = self.fd,
.buffer = buf,
},
},
},
.userdata = userdata,
.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
return switch (options.read) {
.none => unreachable,
.recv => @call(.always_inline, cb, .{
common.userdataValue(Userdata, ud),
l_inner,
c_inner,
T.initFd(c_inner.op.recv.fd),
c_inner.op.recv.buffer,
if (r.recv) |v| v else |err| err,
}),
.read => @call(.always_inline, cb, .{
common.userdataValue(Userdata, ud),
l_inner,
c_inner,
T.initFd(c_inner.op.read.fd),
c_inner.op.read.buffer,
if (r.read) |v| v else |err| err,
}),
};
}
}).callback,
};
// If we're dup-ing, then we ask the backend to manage the fd.
switch (xev.backend) {
.io_uring,
.wasi_poll,
.iocp,
=> {},
.epoll => {
if (options.threadpool)
c.flags.threadpool = true
else
c.flags.dup = true;
},
.kqueue => {
if (options.threadpool) c.flags.threadpool = true;
},
}
loop.add(c);
},
}
}
};
}
pub fn Writeable(comptime xev: type, comptime T: type, comptime options: Options) type {
return struct {
const Self = T;
pub const WriteError = xev.WriteError;
/// WriteQueue is the queue of write requests for ordered writes.
/// This can be copied around.
pub const WriteQueue = queue.Intrusive(WriteRequest);
/// WriteRequest is a single request for a write. It wraps a
/// completion so that it can be inserted into the WriteQueue.
pub const WriteRequest = struct {
completion: xev.Completion = .{},
userdata: ?*anyopaque = null,
/// This is the original buffer passed to queueWrite. We have
/// to keep track of this because we may be forced to split
/// the write or rearm the write due to partial writes, but when
/// we call the final callback we want to pass the original
/// complete buffer.
full_write_buffer: xev.WriteBuffer,
next: ?*@This() = null,
/// This can be used to convert a completion pointer back to
/// a WriteRequest. This is only safe of course if the completion
/// originally is from a write request. This is useful for getting
/// the WriteRequest back in a callback from queuedWrite.
pub fn from(c: *xev.Completion) *WriteRequest {
return @fieldParentPtr("completion", c);
}
};
/// Write to the stream. This queues the writes to ensure they
/// remain in order. Queueing has a small overhead: you must
/// maintain a WriteQueue and WriteRequests instead of just
/// Completions.
///
/// If ordering isn't important, or you can maintain ordering
/// naturally in your program, consider using write since it
/// has a slightly smaller overhead.
///
/// The "CallbackAction" return value of this callback behaves slightly
/// different. The "rearm" return value will re-queue the same write
/// at the end of the queue.
///
/// It is safe to call this at anytime from the main thread.
pub fn queueWrite(
self: Self,
loop: *xev.Loop,
q: *WriteQueue,
req: *WriteRequest,
buf: xev.WriteBuffer,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
s: Self,
b: xev.WriteBuffer,
r: WriteError!usize,
) xev.CallbackAction,
) void {
// Initialize our completion
req.* = .{ .full_write_buffer = buf };
// Must be kept in sync with partial write logic inside the callback
self.write_init(&req.completion, buf);
req.completion.userdata = q;
req.completion.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
const q_inner = @as(?*WriteQueue, @ptrCast(@alignCast(ud))).?;
// The queue MUST have a request because a completion
// can only be added if the queue is not empty, and
// nothing else should be popping!.
//
// We only peek the request here (not pop) because we may
// need to rearm this write if the write was partial.
const req_inner: *WriteRequest = q_inner.head.?;
const cb_res = write_result(c_inner, r);
var result: WriteError!usize = cb_res.result;
// Checks whether the entire buffer was written, this is
// necessary to guarantee correct ordering of writes.
// If the write was partial, it re-submits the remainder of
// the buffer.
const queued_len = writeBufferLength(cb_res.buf);
if (cb_res.result) |written_len| {
if (written_len < queued_len) {
// Write remainder of the buffer, reusing the same completion
const rem_buf = writeBufferRemainder(cb_res.buf, written_len);
cb_res.writer.write_init(&req_inner.completion, rem_buf);
req_inner.completion.userdata = q_inner;
req_inner.completion.callback = callback;
l_inner.add(&req_inner.completion);
return .disarm;
}
// We wrote the entire buffer, modify the result to indicate
// to the caller that all bytes have been written.
result = writeBufferLength(req_inner.full_write_buffer);
} else |_| {}
// We can pop previously peeked request.
_ = q_inner.pop().?;
const action = @call(.always_inline, cb, .{
common.userdataValue(Userdata, req_inner.userdata),
l_inner,
c_inner,
cb_res.writer,
req_inner.full_write_buffer,
result,
});
// Rearm requeues this request, it doesn't return rearm
// on the actual callback here...
if (action == .rearm) q_inner.push(req_inner);
// If we have another request, add that completion next.
if (q_inner.head) |req_next| l_inner.add(&req_next.completion);
// We always disarm because the completion in the next
// request will be used if there is more to queue.
return .disarm;
}
}).callback;
// The userdata as to go on the WriteRequest because we need
// our actual completion userdata to be the WriteQueue so that
// we can process the queue.
req.userdata = @as(?*anyopaque, @ptrCast(@alignCast(userdata)));
// If the queue is empty, then we add our completion. Otherwise,
// the previously queued writes will trigger this one.
if (q.empty()) loop.add(&req.completion);
// We always add this item to our queue no matter what
q.push(req);
}
/// Write to the stream. This performs a single write. Additional
/// writes can be requested by calling this multiple times.
///
/// IMPORTANT: writes are NOT queued. There is no order guarantee
/// if this is called multiple times. If ordered writes are important
/// (they usually are!) then you should only call write again once
/// the previous write callback is called.
///
/// If ordering is important, use queueWrite instead.
pub fn write(
self: Self,
loop: *xev.Loop,
c: *xev.Completion,
buf: xev.WriteBuffer,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
s: Self,
b: xev.WriteBuffer,
r: WriteError!usize,
) xev.CallbackAction,
) void {
self.write_init(c, buf);
c.userdata = userdata;
c.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
const cb_res = write_result(c_inner, r);
return @call(.always_inline, cb, .{
common.userdataValue(Userdata, ud),
l_inner,
c_inner,
cb_res.writer,
cb_res.buf,
cb_res.result,
});
}
}).callback;
loop.add(c);
}
/// Extracts the result from a completion for a write callback.
inline fn write_result(c: *xev.Completion, r: xev.Result) struct {
writer: Self,
buf: xev.WriteBuffer,
result: WriteError!usize,
} {
return switch (options.write) {
.none => unreachable,
.send => .{
.writer = T.initFd(c.op.send.fd),
.buf = c.op.send.buffer,
.result = if (r.send) |v| v else |err| err,
},
.write => .{
.writer = T.initFd(c.op.write.fd),
.buf = c.op.write.buffer,
.result = if (r.write) |v| v else |err| err,
},
};
}
/// Initialize the completion c for a write. This does NOT set
/// userdata or a callback.
fn write_init(
self: Self,
c: *xev.Completion,
buf: xev.WriteBuffer,
) void {
switch (buf) {
inline .slice, .array => {
c.* = .{
.op = switch (options.write) {
.none => unreachable,
.write => .{
.write = .{
.fd = self.fd,
.buffer = buf,
},
},
.send => .{
.send = .{
.fd = self.fd,
.buffer = buf,
},
},
},
};
// If we're dup-ing, then we ask the backend to manage the fd.
switch (xev.backend) {
.io_uring,
.wasi_poll,
.iocp,
=> {},
.epoll => {
if (options.threadpool) {
c.flags.threadpool = true;
} else {
c.flags.dup = true;
}
},
.kqueue => {
if (options.threadpool) c.flags.threadpool = true;
},
}
},
}
}
/// Returns the length of the write buffer
fn writeBufferLength(buf: xev.WriteBuffer) usize {
return switch (buf) {
.slice => |slice| slice.len,
.array => |array| array.len,
};
}
/// Given a `WriteBuffer` and number of bytes written during the previous
/// write operation, returns a new `WriteBuffer` with remaining data.
fn writeBufferRemainder(buf: xev.WriteBuffer, offset: usize) xev.WriteBuffer {
switch (buf) {
.slice => |slice| {
assert(offset <= slice.len);
return .{ .slice = slice[offset..] };
},
.array => |array| {
assert(offset <= array.len);
const rem_len = array.len - offset;
var wb = xev.WriteBuffer{ .array = .{
.array = undefined,
.len = rem_len,
} };
@memcpy(
wb.array.array[0..rem_len],
array.array[offset..][0..rem_len],
);
return wb;
},
}
}
};
}
/// Creates a generic stream type that supports read, write, close. This
/// can be used for any file descriptor that would exhibit normal blocking
/// behavior on read/write. This should NOT be used for local files because
/// local files have some special properties; you should use xev.File for that.
pub fn GenericStream(comptime xev: type) type {
return struct {
const Self = @This();
/// The underlying file
fd: std.posix.fd_t,
pub usingnamespace Stream(xev, Self, .{
.close = true,
.read = .read,
.write = .write,
});
/// Initialize a generic stream from a file descriptor.
pub fn initFd(fd: std.posix.fd_t) Self {
return .{
.fd = fd,
};
}
/// Clean up any watcher resources. This does NOT close the file.
/// If you want to close the file you must call close or do so
/// synchronously.
pub fn deinit(self: *const Self) void {
_ = self;
}
test "pty: child to parent" {
const testing = std.testing;
switch (builtin.os.tag) {
.linux, .macos => {},
else => return error.SkipZigTest,
}
// Create the pty parent/child side.
var pty = try Pty.init();
defer pty.deinit();
var loop = try xev.Loop.init(.{});
defer loop.deinit();
const parent = initFd(pty.parent);
const child = initFd(pty.child);
// Read
var read_buf: [128]u8 = undefined;
var read_len: ?usize = null;
var c_read: xev.Completion = undefined;
parent.read(&loop, &c_read, .{ .slice = &read_buf }, ?usize, &read_len, (struct {
fn callback(
ud: ?*?usize,
_: *xev.Loop,
_: *xev.Completion,
_: Self,
_: xev.ReadBuffer,
r: Self.ReadError!usize,
) xev.CallbackAction {
ud.?.* = r catch unreachable;
return .disarm;
}
}).callback);
// This should not block!
try loop.run(.no_wait);
try testing.expect(read_len == null);
// Send
const send_buf = "hello, world!";
var c_write: xev.Completion = undefined;
child.write(&loop, &c_write, .{ .slice = send_buf }, void, null, (struct {
fn callback(
_: ?*void,
_: *xev.Loop,
c: *xev.Completion,
_: Self,
_: xev.WriteBuffer,
r: Self.WriteError!usize,
) xev.CallbackAction {
_ = c;
_ = r catch unreachable;
return .disarm;
}
}).callback);
// The write and read should trigger
try loop.run(.until_done);
try testing.expect(read_len != null);
try testing.expectEqualSlices(u8, send_buf, read_buf[0..read_len.?]);
}
test "pty: parent to child" {
const testing = std.testing;
switch (builtin.os.tag) {
.linux, .macos => {},
else => return error.SkipZigTest,
}
// Create the pty parent/child side.
var pty = try Pty.init();
defer pty.deinit();
var loop = try xev.Loop.init(.{});
defer loop.deinit();
const parent = initFd(pty.parent);
const child = initFd(pty.child);
// Read
var read_buf: [128]u8 = undefined;
var read_len: ?usize = null;
var c_read: xev.Completion = undefined;
child.read(&loop, &c_read, .{ .slice = &read_buf }, ?usize, &read_len, (struct {
fn callback(
ud: ?*?usize,
_: *xev.Loop,
_: *xev.Completion,
_: Self,
_: xev.ReadBuffer,
r: Self.ReadError!usize,
) xev.CallbackAction {
ud.?.* = r catch unreachable;
return .disarm;
}
}).callback);
// This should not block!
try loop.run(.no_wait);
try testing.expect(read_len == null);
// Send (note the newline at the end of the buf is important
// since we're in cooked mode)
const send_buf = "hello, world!\n";
var c_write: xev.Completion = undefined;
parent.write(&loop, &c_write, .{ .slice = send_buf }, void, null, (struct {
fn callback(
_: ?*void,
_: *xev.Loop,
c: *xev.Completion,
_: Self,
_: xev.WriteBuffer,
r: Self.WriteError!usize,
) xev.CallbackAction {
_ = c;
_ = r catch unreachable;
return .disarm;
}
}).callback);
// The write and read should trigger
try loop.run(.until_done);
try testing.expect(read_len != null);
try testing.expectEqualSlices(u8, send_buf, read_buf[0..read_len.?]);
}
test "pty: queued writes" {
const testing = std.testing;
switch (builtin.os.tag) {
.linux, .macos => {},
else => return error.SkipZigTest,
}
// Create the pty parent/child side.
var pty = try Pty.init();
defer pty.deinit();
var loop = try xev.Loop.init(.{});
defer loop.deinit();
const parent = initFd(pty.parent);
const child = initFd(pty.child);
// Read
var read_buf: [128]u8 = undefined;
var read_len: ?usize = null;
var c_read: xev.Completion = undefined;
child.read(&loop, &c_read, .{ .slice = &read_buf }, ?usize, &read_len, (struct {
fn callback(
ud: ?*?usize,
_: *xev.Loop,
_: *xev.Completion,
_: Self,
_: xev.ReadBuffer,
r: Self.ReadError!usize,
) xev.CallbackAction {
ud.?.* = r catch unreachable;
return .disarm;
}
}).callback);
// This should not block!
try loop.run(.no_wait);
try testing.expect(read_len == null);
var write_queue: Self.WriteQueue = .{};
var write_req: [2]Self.WriteRequest = undefined;
// Send (note the newline at the end of the buf is important
// since we're in cooked mode)
parent.queueWrite(
&loop,
&write_queue,
&write_req[0],
.{ .slice = "hello, " },
void,
null,
(struct {
fn callback(
_: ?*void,
_: *xev.Loop,
c: *xev.Completion,
_: Self,
_: xev.WriteBuffer,
r: Self.WriteError!usize,
) xev.CallbackAction {
_ = c;
_ = r catch unreachable;
return .disarm;
}
}).callback,
);
var c_result: ?*xev.Completion = null;
parent.queueWrite(
&loop,
&write_queue,
&write_req[1],
.{ .slice = "world!\n" },
?*xev.Completion,
&c_result,
(struct {
fn callback(
ud: ?*?*xev.Completion,
_: *xev.Loop,
c: *xev.Completion,
_: Self,
_: xev.WriteBuffer,
r: Self.WriteError!usize,
) xev.CallbackAction {
_ = r catch unreachable;
ud.?.* = c;
return .disarm;
}
}).callback,
);
// The write and read should trigger
try loop.run(.until_done);
try testing.expect(read_len != null);
try testing.expectEqualSlices(u8, "hello, world!\n", read_buf[0..read_len.?]);
// Verify our completion is equal to our request
try testing.expect(Self.WriteRequest.from(c_result.?) == &write_req[1]);
}
};
}
/// Helper to open a pty. This isn't exposed as a public API this is only
/// used for tests.
const Pty = struct {
/// The file descriptors for the parent/child side of the pty. This refers
/// to the master/slave side respectively, and while that terminology is
/// the officially used terminology of the syscall, I will use parent/child
/// here.
parent: std.posix.fd_t,
child: std.posix.fd_t,
/// Redeclare this winsize struct so we can just use a Zig struct. This
/// layout should be correct on all tested platforms.
const Winsize = extern struct {
ws_row: u16,
ws_col: u16,
ws_xpixel: u16,
ws_ypixel: u16,
};
// libc pty.h
extern "c" fn openpty(
parent: *std.posix.fd_t,
child: *std.posix.fd_t,
name: ?[*]u8,
termios: ?*const anyopaque, // termios but we don't use it
winsize: ?*const Winsize,
) c_int;
pub fn init() !Pty {
// Reasonable size
var size: Winsize = .{
.ws_row = 80,
.ws_col = 80,
.ws_xpixel = 800,
.ws_ypixel = 600,
};
var parent_fd: std.posix.fd_t = undefined;
var child_fd: std.posix.fd_t = undefined;
if (openpty(
&parent_fd,
&child_fd,
null,
null,
&size,
) < 0)
return error.OpenptyFailed;
errdefer {
_ = std.posix.system.close(parent_fd);
_ = std.posix.system.close(child_fd);
}
return .{
.parent = parent_fd,
.child = child_fd,
};
}
pub fn deinit(self: *Pty) void {
std.posix.close(self.parent);
std.posix.close(self.child);
}
};

624
deps/libxev/src/watcher/tcp.zig vendored Normal file
View File

@@ -0,0 +1,624 @@
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const posix = std.posix;
const stream = @import("stream.zig");
const common = @import("common.zig");
/// TCP client and server.
///
/// This is a "higher-level abstraction" in libxev. The goal of higher-level
/// abstractions in libxev are to make it easier to use specific functionality
/// with the event loop, but does not promise perfect flexibility or optimal
/// performance. In almost all cases, the abstraction is good enough. But,
/// if you have specific needs or want to push for the most optimal performance,
/// use the platform-specific Loop directly.
pub fn TCP(comptime xev: type) type {
return struct {
const Self = @This();
const FdType = if (xev.backend == .iocp) std.os.windows.HANDLE else posix.socket_t;
fd: FdType,
pub usingnamespace stream.Stream(xev, Self, .{
.close = true,
.read = .recv,
.write = .send,
});
/// Initialize a new TCP with the family from the given address. Only
/// the family is used, the actual address has no impact on the created
/// resource.
pub fn init(addr: std.net.Address) !Self {
if (xev.backend == .wasi_poll) @compileError("unsupported in WASI");
const fd = if (xev.backend == .iocp)
try std.os.windows.WSASocketW(addr.any.family, posix.SOCK.STREAM, 0, null, 0, std.os.windows.ws2_32.WSA_FLAG_OVERLAPPED)
else fd: {
// On io_uring we don't use non-blocking sockets because we may
// just get EAGAIN over and over from completions.
const flags = flags: {
var flags: u32 = posix.SOCK.STREAM | posix.SOCK.CLOEXEC;
if (xev.backend != .io_uring) flags |= posix.SOCK.NONBLOCK;
break :flags flags;
};
break :fd try posix.socket(addr.any.family, flags, 0);
};
return .{
.fd = fd,
};
}
/// Initialize a TCP socket from a file descriptor.
pub fn initFd(fd: FdType) Self {
return .{
.fd = fd,
};
}
/// Bind the address to the socket.
pub fn bind(self: Self, addr: std.net.Address) !void {
if (xev.backend == .wasi_poll) @compileError("unsupported in WASI");
const fd = if (xev.backend == .iocp) @as(std.os.windows.ws2_32.SOCKET, @ptrCast(self.fd)) else self.fd;
try posix.setsockopt(fd, posix.SOL.SOCKET, posix.SO.REUSEADDR, &std.mem.toBytes(@as(c_int, 1)));
try posix.bind(fd, &addr.any, addr.getOsSockLen());
}
/// Listen for connections on the socket. This puts the socket into passive
/// listening mode. Connections must still be accepted one at a time.
pub fn listen(self: Self, backlog: u31) !void {
if (xev.backend == .wasi_poll) @compileError("unsupported in WASI");
const fd = if (xev.backend == .iocp) @as(std.os.windows.ws2_32.SOCKET, @ptrCast(self.fd)) else self.fd;
try posix.listen(fd, backlog);
}
/// Accept a single connection.
pub fn accept(
self: Self,
loop: *xev.Loop,
c: *xev.Completion,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
r: AcceptError!Self,
) xev.CallbackAction,
) void {
c.* = .{
.op = .{
.accept = .{
.socket = self.fd,
},
},
.userdata = userdata,
.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
return @call(.always_inline, cb, .{
common.userdataValue(Userdata, ud),
l_inner,
c_inner,
if (r.accept) |fd| initFd(fd) else |err| err,
});
}
}).callback,
};
// If we're dup-ing, then we ask the backend to manage the fd.
switch (xev.backend) {
.io_uring,
.kqueue,
.wasi_poll,
.iocp,
=> {},
.epoll => c.flags.dup = true,
}
loop.add(c);
}
/// Establish a connection as a client.
pub fn connect(
self: Self,
loop: *xev.Loop,
c: *xev.Completion,
addr: std.net.Address,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
s: Self,
r: ConnectError!void,
) xev.CallbackAction,
) void {
if (xev.backend == .wasi_poll) @compileError("unsupported in WASI");
c.* = .{
.op = .{
.connect = .{
.socket = self.fd,
.addr = addr,
},
},
.userdata = userdata,
.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
return @call(.always_inline, cb, .{
common.userdataValue(Userdata, ud),
l_inner,
c_inner,
initFd(c_inner.op.connect.socket),
if (r.connect) |_| {} else |err| err,
});
}
}).callback,
};
loop.add(c);
}
/// Shutdown the socket. This always only shuts down the writer side. You
/// can use the lower level interface directly to control this if the
/// platform supports it.
pub fn shutdown(
self: Self,
loop: *xev.Loop,
c: *xev.Completion,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
s: Self,
r: ShutdownError!void,
) xev.CallbackAction,
) void {
c.* = .{
.op = .{
.shutdown = .{
.socket = self.fd,
.how = .send,
},
},
.userdata = userdata,
.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
return @call(.always_inline, cb, .{
common.userdataValue(Userdata, ud),
l_inner,
c_inner,
initFd(c_inner.op.shutdown.socket),
if (r.shutdown) |_| {} else |err| err,
});
}
}).callback,
};
loop.add(c);
}
pub const AcceptError = xev.AcceptError;
pub const ConnectError = xev.ConnectError;
pub const ShutdownError = xev.ShutdownError;
test "TCP: accept/connect/send/recv/close" {
// We have no way to get a socket in WASI from a WASI context.
if (xev.backend == .wasi_poll) return error.SkipZigTest;
const testing = std.testing;
var loop = try xev.Loop.init(.{});
defer loop.deinit();
// Choose random available port (Zig #14907)
var address = try std.net.Address.parseIp4("127.0.0.1", 0);
const server = try Self.init(address);
// Bind and listen
try server.bind(address);
try server.listen(1);
// Retrieve bound port and initialize client
var sock_len = address.getOsSockLen();
const fd = if (xev.backend == .iocp) @as(std.os.windows.ws2_32.SOCKET, @ptrCast(server.fd)) else server.fd;
try posix.getsockname(fd, &address.any, &sock_len);
const client = try Self.init(address);
//const address = try std.net.Address.parseIp4("127.0.0.1", 3132);
//var server = try Self.init(address);
//var client = try Self.init(address);
// Completions we need
var c_accept: xev.Completion = undefined;
var c_connect: xev.Completion = undefined;
// Accept
var server_conn: ?Self = null;
server.accept(&loop, &c_accept, ?Self, &server_conn, (struct {
fn callback(
ud: ?*?Self,
_: *xev.Loop,
_: *xev.Completion,
r: AcceptError!Self,
) xev.CallbackAction {
ud.?.* = r catch unreachable;
return .disarm;
}
}).callback);
// Connect
var connected: bool = false;
client.connect(&loop, &c_connect, address, bool, &connected, (struct {
fn callback(
ud: ?*bool,
_: *xev.Loop,
_: *xev.Completion,
_: Self,
r: ConnectError!void,
) xev.CallbackAction {
_ = r catch unreachable;
ud.?.* = true;
return .disarm;
}
}).callback);
// Wait for the connection to be established
try loop.run(.until_done);
try testing.expect(server_conn != null);
try testing.expect(connected);
// Close the server
var server_closed = false;
server.close(&loop, &c_accept, bool, &server_closed, (struct {
fn callback(
ud: ?*bool,
_: *xev.Loop,
_: *xev.Completion,
_: Self,
r: Self.CloseError!void,
) xev.CallbackAction {
_ = r catch unreachable;
ud.?.* = true;
return .disarm;
}
}).callback);
try loop.run(.until_done);
try testing.expect(server_closed);
// Send
var send_buf = [_]u8{ 1, 1, 2, 3, 5, 8, 13 };
client.write(&loop, &c_connect, .{ .slice = &send_buf }, void, null, (struct {
fn callback(
_: ?*void,
_: *xev.Loop,
c: *xev.Completion,
_: Self,
_: xev.WriteBuffer,
r: Self.WriteError!usize,
) xev.CallbackAction {
_ = c;
_ = r catch unreachable;
return .disarm;
}
}).callback);
// Receive
var recv_buf: [128]u8 = undefined;
var recv_len: usize = 0;
server_conn.?.read(&loop, &c_accept, .{ .slice = &recv_buf }, usize, &recv_len, (struct {
fn callback(
ud: ?*usize,
_: *xev.Loop,
_: *xev.Completion,
_: Self,
_: xev.ReadBuffer,
r: Self.ReadError!usize,
) xev.CallbackAction {
ud.?.* = r catch unreachable;
return .disarm;
}
}).callback);
// Wait for the send/receive
try loop.run(.until_done);
try testing.expectEqualSlices(u8, &send_buf, recv_buf[0..recv_len]);
// Close
server_conn.?.close(&loop, &c_accept, ?Self, &server_conn, (struct {
fn callback(
ud: ?*?Self,
_: *xev.Loop,
_: *xev.Completion,
_: Self,
r: Self.CloseError!void,
) xev.CallbackAction {
_ = r catch unreachable;
ud.?.* = null;
return .disarm;
}
}).callback);
client.close(&loop, &c_connect, bool, &connected, (struct {
fn callback(
ud: ?*bool,
_: *xev.Loop,
_: *xev.Completion,
_: Self,
r: Self.CloseError!void,
) xev.CallbackAction {
_ = r catch unreachable;
ud.?.* = false;
return .disarm;
}
}).callback);
try loop.run(.until_done);
try testing.expect(server_conn == null);
try testing.expect(!connected);
try testing.expect(server_closed);
}
// Potentially flaky - this test could hang if the sender is unable to
// write everything to the socket for whatever reason
// (e.g. incorrectly sized buffer on the receiver side), or if the
// receiver is trying to receive while sender has nothing left to send.
//
// Overview:
// 1. Set up server and client sockets
// 2. connect & accept, set SO_SNDBUF to 8kB on the client
// 3. Try to send 1MB buffer from client to server without queuing, this _should_ fail
// and theoretically send <= 8kB, but in practice, it seems to write ~32kB.
// Asserts that <= 100kB was written
// 4. Set up a queued write with the remaining buffer, shutdown() the socket afterwards
// 5. Set up a receiver that loops until it receives the entire buffer
// 6. Assert send_buf == recv_buf
test "TCP: Queued writes" {
// We have no way to get a socket in WASI from a WASI context.
if (xev.backend == .wasi_poll) return error.SkipZigTest;
// Windows doesn't seem to respect the SNDBUF socket option.
if (builtin.os.tag == .windows) return error.SkipZigTest;
const testing = std.testing;
var loop = try xev.Loop.init(.{});
defer loop.deinit();
// Choose random available port (Zig #14907)
var address = try std.net.Address.parseIp4("127.0.0.1", 0);
const server = try Self.init(address);
// Bind and listen
try server.bind(address);
try server.listen(1);
// Retrieve bound port and initialize client
var sock_len = address.getOsSockLen();
try posix.getsockname(server.fd, &address.any, &sock_len);
const client = try Self.init(address);
// Completions we need
var c_accept: xev.Completion = undefined;
var c_connect: xev.Completion = undefined;
// Accept
var server_conn: ?Self = null;
server.accept(&loop, &c_accept, ?Self, &server_conn, (struct {
fn callback(
ud: ?*?Self,
_: *xev.Loop,
_: *xev.Completion,
r: AcceptError!Self,
) xev.CallbackAction {
ud.?.* = r catch unreachable;
return .disarm;
}
}).callback);
// Connect
var connected: bool = false;
client.connect(&loop, &c_connect, address, bool, &connected, (struct {
fn callback(
ud: ?*bool,
_: *xev.Loop,
_: *xev.Completion,
_: Self,
r: ConnectError!void,
) xev.CallbackAction {
_ = r catch unreachable;
ud.?.* = true;
return .disarm;
}
}).callback);
// Wait for the connection to be established
try loop.run(.until_done);
try testing.expect(server_conn != null);
try testing.expect(connected);
// Close the server
var server_closed = false;
server.close(&loop, &c_accept, bool, &server_closed, (struct {
fn callback(
ud: ?*bool,
_: *xev.Loop,
_: *xev.Completion,
_: Self,
r: Self.CloseError!void,
) xev.CallbackAction {
_ = r catch unreachable;
ud.?.* = true;
return .disarm;
}
}).callback);
try loop.run(.until_done);
try testing.expect(server_closed);
// Unqueued send - Limit send buffer to 8kB, this should force partial writes.
try posix.setsockopt(client.fd, posix.SOL.SOCKET, posix.SO.SNDBUF, &std.mem.toBytes(@as(c_int, 8192)));
const send_buf = [_]u8{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 } ** 100_000;
var sent_unqueued: usize = 0;
// First we try to send the whole 1MB buffer in one write operation, this _should_ result
// in a partial write.
client.write(&loop, &c_connect, .{ .slice = &send_buf }, usize, &sent_unqueued, (struct {
fn callback(
sent_unqueued_inner: ?*usize,
_: *xev.Loop,
_: *xev.Completion,
_: Self,
_: xev.WriteBuffer,
r: Self.WriteError!usize,
) xev.CallbackAction {
sent_unqueued_inner.?.* = r catch unreachable;
return .disarm;
}
}).callback);
// Make sure that we sent a small fraction of the buffer
try loop.run(.until_done);
// SO_SNDBUF doesn't seem to be respected exactly, sent_unqueued will often be ~32kB
// even though SO_SNDBUF was set to 8kB
try testing.expect(sent_unqueued < (send_buf.len / 10));
// Set up queued write
var w_queue = Self.WriteQueue{};
var wr_send: xev.TCP.WriteRequest = undefined;
var sent_queued: usize = 0;
const queued_slice = send_buf[sent_unqueued..];
client.queueWrite(&loop, &w_queue, &wr_send, .{ .slice = queued_slice }, usize, &sent_queued, (struct {
fn callback(
sent_queued_inner: ?*usize,
l: *xev.Loop,
c: *xev.Completion,
tcp: Self,
_: xev.WriteBuffer,
r: Self.WriteError!usize,
) xev.CallbackAction {
sent_queued_inner.?.* = r catch unreachable;
tcp.shutdown(l, c, void, null, (struct {
fn callback(
_: ?*void,
_: *xev.Loop,
_: *xev.Completion,
_: Self,
_: Self.ShutdownError!void,
) xev.CallbackAction {
return .disarm;
}
}).callback);
return .disarm;
}
}).callback);
// Set up receiver which is going to keep reading until it reads the full
// send buffer
const Receiver = struct {
loop: *xev.Loop,
conn: Self,
completion: xev.Completion = .{},
buf: [send_buf.len]u8 = undefined,
bytes_read: usize = 0,
pub fn read(receiver: *@This()) void {
if (receiver.bytes_read == receiver.buf.len) return;
const read_buf = xev.ReadBuffer{
.slice = receiver.buf[receiver.bytes_read..],
};
receiver.conn.read(receiver.loop, &receiver.completion, read_buf, @This(), receiver, readCb);
}
pub fn readCb(
receiver_opt: ?*@This(),
_: *xev.Loop,
_: *xev.Completion,
_: Self,
_: xev.ReadBuffer,
r: Self.ReadError!usize,
) xev.CallbackAction {
var receiver = receiver_opt.?;
const n_bytes = r catch unreachable;
receiver.bytes_read += n_bytes;
if (receiver.bytes_read < send_buf.len) {
receiver.read();
}
return .disarm;
}
};
var receiver = Receiver{
.loop = &loop,
.conn = server_conn.?,
};
receiver.read();
// Wait for the send/receive
try loop.run(.until_done);
try testing.expectEqualSlices(u8, &send_buf, receiver.buf[0..receiver.bytes_read]);
try testing.expect(send_buf.len == sent_unqueued + sent_queued);
// Close
server_conn.?.close(&loop, &c_accept, ?Self, &server_conn, (struct {
fn callback(
ud: ?*?Self,
_: *xev.Loop,
_: *xev.Completion,
_: Self,
r: Self.CloseError!void,
) xev.CallbackAction {
_ = r catch unreachable;
ud.?.* = null;
return .disarm;
}
}).callback);
client.close(&loop, &c_connect, bool, &connected, (struct {
fn callback(
ud: ?*bool,
_: *xev.Loop,
_: *xev.Completion,
_: Self,
r: Self.CloseError!void,
) xev.CallbackAction {
_ = r catch unreachable;
ud.?.* = false;
return .disarm;
}
}).callback);
try loop.run(.until_done);
try testing.expect(server_conn == null);
try testing.expect(!connected);
try testing.expect(server_closed);
}
};
}

411
deps/libxev/src/watcher/timer.zig vendored Normal file
View File

@@ -0,0 +1,411 @@
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const os = std.os;
/// A timer fires a callback after a specified amount of time. A timer can
/// repeat by returning "rearm" in the callback or by rescheduling the
/// start within the callback.
pub fn Timer(comptime xev: type) type {
return struct {
const Self = @This();
/// Create a new timer.
pub fn init() !Self {
return .{};
}
pub fn deinit(self: *const Self) void {
// Nothing for now.
_ = self;
}
/// Start the timer. The timer will execute in next_ms milliseconds from
/// now.
///
/// This will use the monotonic clock on your system if available so
/// this is immune to system clock changes or drift. The callback is
/// guaranteed to fire NO EARLIER THAN "next_ms" milliseconds. We can't
/// make any guarantees about exactness or time bounds because its possible
/// for your OS to just... pause.. the process for an indefinite period of
/// time.
///
/// Like everything else in libxev, if you want something to repeat, you
/// must then requeue the completion manually. This punts off one of the
/// "hard" aspects of timers: it is up to you to determine what the semantic
/// meaning of intervals are. For example, if you want a timer to repeat every
/// 10 seconds, is it every 10th second of a wall clock? every 10th second
/// after an invocation? every 10th second after the work time from the
/// invocation? You have the power to answer these questions, manually.
pub fn run(
self: Self,
loop: *xev.Loop,
c: *xev.Completion,
next_ms: u64,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
r: RunError!void,
) xev.CallbackAction,
) void {
_ = self;
loop.timer(c, next_ms, userdata, (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
return @call(.always_inline, cb, .{
@as(?*Userdata, if (Userdata == void) null else @ptrCast(@alignCast(ud))),
l_inner,
c_inner,
if (r.timer) |trigger| @as(RunError!void, switch (trigger) {
.request, .expiration => {},
.cancel => error.Canceled,
}) else |err| err,
});
}
}).callback);
}
/// Reset a timer to execute in next_ms milliseconds. If the timer
/// is already started, this will stop it and restart it. If the
/// timer has never been started, this is equivalent to running "run".
/// In every case, the timer callback is updated to the given userdata
/// and callback.
///
/// This requires an additional completion c_cancel to represent
/// the need to possibly cancel the previous timer. You can check
/// if c_cancel was used by checking the state() after the call.
///
/// VERY IMPORTANT: both c and c_cancel MUST NOT be undefined. They
/// must be initialized to ".{}" if being used for the first time.
pub fn reset(
self: Self,
loop: *xev.Loop,
c: *xev.Completion,
c_cancel: *xev.Completion,
next_ms: u64,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
r: RunError!void,
) xev.CallbackAction,
) void {
_ = self;
loop.timer_reset(c, c_cancel, next_ms, userdata, (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
return @call(.always_inline, cb, .{
@as(?*Userdata, if (Userdata == void) null else @ptrCast(@alignCast(ud))),
l_inner,
c_inner,
if (r.timer) |trigger| @as(RunError!void, switch (trigger) {
.request, .expiration => {},
.cancel => error.Canceled,
}) else |err| err,
});
}
}).callback);
}
/// Cancel a previously started timer. The timer to cancel used the completion
/// "c_cancel". A new completion "c" must be specified which will be called
/// with the callback once cancellation is complete.
///
/// The original timer will still have its callback fired but with the
/// error "error.Canceled".
pub fn cancel(
self: Self,
loop: *xev.Loop,
c_timer: *xev.Completion,
c_cancel: *xev.Completion,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
r: CancelError!void,
) xev.CallbackAction,
) void {
_ = self;
c_cancel.* = switch (xev.backend) {
.io_uring => .{
.op = .{
.timer_remove = .{
.timer = c_timer,
},
},
.userdata = userdata,
.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
return @call(.always_inline, cb, .{
@as(?*Userdata, if (Userdata == void) null else @ptrCast(@alignCast(ud))),
l_inner,
c_inner,
if (r.timer_remove) |_| {} else |err| err,
});
}
}).callback,
},
.epoll,
.kqueue,
.wasi_poll,
.iocp,
=> .{
.op = .{
.cancel = .{
.c = c_timer,
},
},
.userdata = userdata,
.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
return @call(.always_inline, cb, .{
@as(?*Userdata, if (Userdata == void) null else @ptrCast(@alignCast(ud))),
l_inner,
c_inner,
if (r.cancel) |_| {} else |err| err,
});
}
}).callback,
},
};
loop.add(c_cancel);
}
/// Error that could happen while running a timer.
pub const RunError = error{
/// The timer was canceled before it could expire
Canceled,
/// Some unexpected error.
Unexpected,
};
pub const CancelError = xev.CancelError;
test "timer" {
const testing = std.testing;
var loop = try xev.Loop.init(.{});
defer loop.deinit();
var timer = try init();
defer timer.deinit();
// Add the timer
var called = false;
var c1: xev.Completion = undefined;
timer.run(&loop, &c1, 1, bool, &called, (struct {
fn callback(
ud: ?*bool,
_: *xev.Loop,
_: *xev.Completion,
r: RunError!void,
) xev.CallbackAction {
_ = r catch unreachable;
ud.?.* = true;
return .disarm;
}
}).callback);
// Wait
try loop.run(.until_done);
try testing.expect(called);
}
test "timer reset" {
const testing = std.testing;
var loop = try xev.Loop.init(.{});
defer loop.deinit();
var timer = try init();
defer timer.deinit();
var c_timer: xev.Completion = .{};
var c_cancel: xev.Completion = .{};
const cb = (struct {
fn callback(
ud: ?*bool,
_: *xev.Loop,
_: *xev.Completion,
r: RunError!void,
) xev.CallbackAction {
_ = r catch unreachable;
ud.?.* = true;
return .disarm;
}
}).callback;
// Add the timer
var canceled = false;
timer.run(&loop, &c_timer, 100_000, bool, &canceled, cb);
// Wait
try loop.run(.no_wait);
try testing.expect(!canceled);
// Reset it
timer.reset(&loop, &c_timer, &c_cancel, 1, bool, &canceled, cb);
try loop.run(.until_done);
try testing.expect(canceled);
try testing.expect(c_timer.state() == .dead);
try testing.expect(c_cancel.state() == .dead);
}
test "timer reset before tick" {
const testing = std.testing;
var loop = try xev.Loop.init(.{});
defer loop.deinit();
var timer = try init();
defer timer.deinit();
var c_timer: xev.Completion = .{};
var c_cancel: xev.Completion = .{};
const cb = (struct {
fn callback(
ud: ?*bool,
_: *xev.Loop,
_: *xev.Completion,
r: RunError!void,
) xev.CallbackAction {
_ = r catch unreachable;
ud.?.* = true;
return .disarm;
}
}).callback;
// Add the timer
var canceled = false;
timer.run(&loop, &c_timer, 100_000, bool, &canceled, cb);
// Reset it
timer.reset(&loop, &c_timer, &c_cancel, 1, bool, &canceled, cb);
try loop.run(.until_done);
try testing.expect(canceled);
try testing.expect(c_timer.state() == .dead);
try testing.expect(c_cancel.state() == .dead);
}
test "timer reset after trigger" {
const testing = std.testing;
var loop = try xev.Loop.init(.{});
defer loop.deinit();
var timer = try init();
defer timer.deinit();
var c_timer: xev.Completion = .{};
var c_cancel: xev.Completion = .{};
const cb = (struct {
fn callback(
ud: ?*bool,
_: *xev.Loop,
_: *xev.Completion,
r: RunError!void,
) xev.CallbackAction {
_ = r catch unreachable;
ud.?.* = true;
return .disarm;
}
}).callback;
// Add the timer
var canceled = false;
timer.run(&loop, &c_timer, 1, bool, &canceled, cb);
try loop.run(.until_done);
try testing.expect(canceled);
canceled = false;
// Reset it
timer.reset(&loop, &c_timer, &c_cancel, 1, bool, &canceled, cb);
try loop.run(.until_done);
try testing.expect(canceled);
try testing.expect(c_timer.state() == .dead);
try testing.expect(c_cancel.state() == .dead);
}
test "timer cancel" {
const testing = std.testing;
var loop = try xev.Loop.init(.{});
defer loop.deinit();
var timer = try init();
defer timer.deinit();
// Add the timer
var canceled = false;
var c1: xev.Completion = undefined;
timer.run(&loop, &c1, 100_000, bool, &canceled, (struct {
fn callback(
ud: ?*bool,
_: *xev.Loop,
_: *xev.Completion,
r: RunError!void,
) xev.CallbackAction {
ud.?.* = if (r) false else |err| err == error.Canceled;
return .disarm;
}
}).callback);
// Cancel
var cancel_confirm = false;
var c2: xev.Completion = undefined;
timer.cancel(&loop, &c1, &c2, bool, &cancel_confirm, (struct {
fn callback(
ud: ?*bool,
_: *xev.Loop,
_: *xev.Completion,
r: CancelError!void,
) xev.CallbackAction {
_ = r catch unreachable;
ud.?.* = true;
return .disarm;
}
}).callback);
// Wait
try loop.run(.until_done);
try testing.expect(canceled);
try testing.expect(cancel_confirm);
}
};
}

770
deps/libxev/src/watcher/udp.zig vendored Normal file
View File

@@ -0,0 +1,770 @@
const std = @import("std");
const assert = std.debug.assert;
const posix = std.posix;
const stream = @import("stream.zig");
const common = @import("common.zig");
/// UDP client and server.
///
/// This is a "higher-level abstraction" in libxev. The goal of higher-level
/// abstractions in libxev are to make it easier to use specific functionality
/// with the event loop, but does not promise perfect flexibility or optimal
/// performance. In almost all cases, the abstraction is good enough. But,
/// if you have specific needs or want to push for the most optimal performance,
/// use the platform-specific Loop directly.
pub fn UDP(comptime xev: type) type {
return switch (xev.backend) {
// Supported, uses sendmsg/recvmsg exclusively
.io_uring,
.epoll,
=> UDPSendMsg(xev),
// Supported, uses sendto/recvfrom
.kqueue => UDPSendto(xev),
// Supported with tweaks
.iocp => UDPSendtoIOCP(xev),
// Noop
.wasi_poll => struct {},
};
}
/// UDP implementation that uses sendto/recvfrom.
fn UDPSendto(comptime xev: type) type {
return struct {
const Self = @This();
fd: posix.socket_t,
/// See UDPSendMsg.State
pub const State = struct {
userdata: ?*anyopaque,
};
pub usingnamespace stream.Stream(xev, Self, .{
.close = true,
.read = .none,
.write = .none,
});
/// Initialize a new UDP with the family from the given address. Only
/// the family is used, the actual address has no impact on the created
/// resource.
pub fn init(addr: std.net.Address) !Self {
return .{
.fd = try posix.socket(
addr.any.family,
posix.SOCK.NONBLOCK | posix.SOCK.DGRAM | posix.SOCK.CLOEXEC,
0,
),
};
}
/// Initialize a UDP socket from a file descriptor.
pub fn initFd(fd: posix.socket_t) Self {
return .{
.fd = fd,
};
}
/// Bind the address to the socket.
pub fn bind(self: Self, addr: std.net.Address) !void {
try posix.setsockopt(self.fd, posix.SOL.SOCKET, posix.SO.REUSEPORT, &std.mem.toBytes(@as(c_int, 1)));
try posix.setsockopt(self.fd, posix.SOL.SOCKET, posix.SO.REUSEADDR, &std.mem.toBytes(@as(c_int, 1)));
try posix.bind(self.fd, &addr.any, addr.getOsSockLen());
}
/// Read from the socket. This performs a single read. The callback must
/// requeue the read if additional reads want to be performed. Additional
/// reads simultaneously can be queued by calling this multiple times. Note
/// that depending on the backend, the reads can happen out of order.
pub fn read(
self: Self,
loop: *xev.Loop,
c: *xev.Completion,
s: *State,
buf: xev.ReadBuffer,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
s: *State,
addr: std.net.Address,
s: Self,
b: xev.ReadBuffer,
r: ReadError!usize,
) xev.CallbackAction,
) void {
s.* = .{
.userdata = userdata,
};
switch (buf) {
inline .slice, .array => {
c.* = .{
.op = .{
.recvfrom = .{
.fd = self.fd,
.buffer = buf,
},
},
.userdata = s,
.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
const s_inner = @as(?*State, @ptrCast(@alignCast(ud))).?;
return @call(.always_inline, cb, .{
common.userdataValue(Userdata, s_inner.userdata),
l_inner,
c_inner,
s_inner,
std.net.Address.initPosix(@alignCast(&c_inner.op.recvfrom.addr)),
initFd(c_inner.op.recvfrom.fd),
c_inner.op.recvfrom.buffer,
r.recvfrom,
});
}
}).callback,
};
loop.add(c);
},
}
}
/// Write to the socket. This performs a single write. Additional writes
/// can be queued by calling this multiple times. Note that depending on the
/// backend, writes can happen out of order.
pub fn write(
self: Self,
loop: *xev.Loop,
c: *xev.Completion,
s: *State,
addr: std.net.Address,
buf: xev.WriteBuffer,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
s: *State,
s: Self,
b: xev.WriteBuffer,
r: WriteError!usize,
) xev.CallbackAction,
) void {
s.* = .{
.userdata = userdata,
};
switch (buf) {
inline .slice, .array => {
c.* = .{
.op = .{
.sendto = .{
.fd = self.fd,
.buffer = buf,
.addr = addr,
},
},
.userdata = s,
.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
const s_inner = @as(?*State, @ptrCast(@alignCast(ud))).?;
return @call(.always_inline, cb, .{
common.userdataValue(Userdata, s_inner.userdata),
l_inner,
c_inner,
s_inner,
initFd(c_inner.op.sendto.fd),
c_inner.op.sendto.buffer,
r.sendto,
});
}
}).callback,
};
loop.add(c);
},
}
}
pub const ReadError = xev.ReadError;
pub const WriteError = xev.WriteError;
/// Common tests
pub usingnamespace UDPTests(xev, Self);
};
}
/// UDP implementation that uses sendto/recvfrom.
fn UDPSendtoIOCP(comptime xev: type) type {
return struct {
const Self = @This();
const windows = std.os.windows;
fd: windows.HANDLE,
/// See UDPSendMsg.State
pub const State = struct {
userdata: ?*anyopaque,
};
pub usingnamespace stream.Stream(xev, Self, .{
.close = true,
.read = .none,
.write = .none,
});
/// Initialize a new UDP with the family from the given address. Only
/// the family is used, the actual address has no impact on the created
/// resource.
pub fn init(addr: std.net.Address) !Self {
const socket = try windows.WSASocketW(addr.any.family, posix.SOCK.DGRAM, 0, null, 0, windows.ws2_32.WSA_FLAG_OVERLAPPED);
return .{
.fd = socket,
};
}
/// Initialize a UDP socket from a file descriptor.
pub fn initFd(fd: windows.HANDLE) Self {
return .{
.fd = fd,
};
}
/// Bind the address to the socket.
pub fn bind(self: Self, addr: std.net.Address) !void {
const socket = @as(windows.ws2_32.SOCKET, @ptrCast(self.fd));
try posix.setsockopt(socket, posix.SOL.SOCKET, posix.SO.REUSEADDR, &std.mem.toBytes(@as(c_int, 1)));
try posix.bind(socket, &addr.any, addr.getOsSockLen());
}
/// Read from the socket. This performs a single read. The callback must
/// requeue the read if additional reads want to be performed. Additional
/// reads simultaneously can be queued by calling this multiple times. Note
/// that depending on the backend, the reads can happen out of order.
///
/// TODO(mitchellh): a way to receive the remote addr
pub fn read(
self: Self,
loop: *xev.Loop,
c: *xev.Completion,
s: *State,
buf: xev.ReadBuffer,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
s: *State,
addr: std.net.Address,
s: Self,
b: xev.ReadBuffer,
r: ReadError!usize,
) xev.CallbackAction,
) void {
s.* = .{
.userdata = userdata,
};
switch (buf) {
inline .slice, .array => {
c.* = .{
.op = .{
.recvfrom = .{
.fd = self.fd,
.buffer = buf,
},
},
.userdata = s,
.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
const s_inner: *State = @ptrCast(@alignCast(ud.?));
return @call(.always_inline, cb, .{
common.userdataValue(Userdata, s_inner.userdata),
l_inner,
c_inner,
s_inner,
std.net.Address.initPosix(@alignCast(&c_inner.op.recvfrom.addr)),
initFd(c_inner.op.recvfrom.fd),
c_inner.op.recvfrom.buffer,
r.recvfrom,
});
}
}).callback,
};
loop.add(c);
},
}
}
/// Write to the socket. This performs a single write. Additional writes
/// can be queued by calling this multiple times. Note that depending on the
/// backend, writes can happen out of order.
pub fn write(
self: Self,
loop: *xev.Loop,
c: *xev.Completion,
s: *State,
addr: std.net.Address,
buf: xev.WriteBuffer,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
s: *State,
s: Self,
b: xev.WriteBuffer,
r: WriteError!usize,
) xev.CallbackAction,
) void {
s.* = .{
.userdata = userdata,
};
switch (buf) {
inline .slice, .array => {
c.* = .{
.op = .{
.sendto = .{
.fd = self.fd,
.buffer = buf,
.addr = addr,
},
},
.userdata = s,
.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
const s_inner: *State = @ptrCast(@alignCast(ud.?));
return @call(.always_inline, cb, .{
common.userdataValue(Userdata, s_inner.userdata),
l_inner,
c_inner,
s_inner,
initFd(c_inner.op.sendto.fd),
c_inner.op.sendto.buffer,
r.sendto,
});
}
}).callback,
};
loop.add(c);
},
}
}
pub const ReadError = xev.ReadError;
pub const WriteError = xev.WriteError;
/// Common tests
pub usingnamespace UDPTests(xev, Self);
};
}
/// UDP implementation that uses sendmsg/recvmsg
fn UDPSendMsg(comptime xev: type) type {
return struct {
const Self = @This();
fd: posix.socket_t,
/// UDP requires some extra state to perform operations. The state is
/// opaque. This isn't part of xev.Completion because it is relatively
/// large and would force ALL operations (not just UDP) to have a relatively
/// large structure size and we didn't want to pay that cost.
pub const State = struct {
userdata: ?*anyopaque = null,
op: union {
recv: struct {
buf: xev.ReadBuffer,
addr_buffer: std.posix.sockaddr.storage = undefined,
msghdr: std.posix.msghdr,
iov: [1]std.posix.iovec,
},
send: struct {
buf: xev.WriteBuffer,
addr: std.net.Address,
msghdr: std.posix.msghdr_const,
iov: [1]std.posix.iovec_const,
},
},
};
pub usingnamespace stream.Stream(xev, Self, .{
.close = true,
.read = .none,
.write = .none,
});
/// Initialize a new UDP with the family from the given address. Only
/// the family is used, the actual address has no impact on the created
/// resource.
pub fn init(addr: std.net.Address) !Self {
// On io_uring we don't use non-blocking sockets because we may
// just get EAGAIN over and over from completions.
const flags = flags: {
var flags: u32 = posix.SOCK.DGRAM | posix.SOCK.CLOEXEC;
if (xev.backend != .io_uring) flags |= posix.SOCK.NONBLOCK;
break :flags flags;
};
return .{
.fd = try posix.socket(addr.any.family, flags, 0),
};
}
/// Initialize a UDP socket from a file descriptor.
pub fn initFd(fd: posix.socket_t) Self {
return .{
.fd = fd,
};
}
/// Bind the address to the socket.
pub fn bind(self: Self, addr: std.net.Address) !void {
try posix.setsockopt(self.fd, posix.SOL.SOCKET, posix.SO.REUSEPORT, &std.mem.toBytes(@as(c_int, 1)));
try posix.setsockopt(self.fd, posix.SOL.SOCKET, posix.SO.REUSEADDR, &std.mem.toBytes(@as(c_int, 1)));
try posix.bind(self.fd, &addr.any, addr.getOsSockLen());
}
/// Read from the socket. This performs a single read. The callback must
/// requeue the read if additional reads want to be performed. Additional
/// reads simultaneously can be queued by calling this multiple times. Note
/// that depending on the backend, the reads can happen out of order.
pub fn read(
self: Self,
loop: *xev.Loop,
c: *xev.Completion,
s: *State,
buf: xev.ReadBuffer,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
s: *State,
addr: std.net.Address,
s: Self,
b: xev.ReadBuffer,
r: ReadError!usize,
) xev.CallbackAction,
) void {
s.op = .{ .recv = undefined };
s.* = .{
.userdata = userdata,
.op = .{
.recv = .{
.buf = buf,
.msghdr = .{
.name = @ptrCast(&s.op.recv.addr_buffer),
.namelen = @sizeOf(@TypeOf(s.op.recv.addr_buffer)),
.iov = &s.op.recv.iov,
.iovlen = 1,
.control = null,
.controllen = 0,
.flags = 0,
},
.iov = undefined,
},
},
};
switch (s.op.recv.buf) {
.slice => |v| {
s.op.recv.iov[0] = .{
.base = v.ptr,
.len = v.len,
};
},
.array => |*arr| {
s.op.recv.iov[0] = .{
.base = arr,
.len = arr.len,
};
},
}
c.* = .{
.op = .{
.recvmsg = .{
.fd = self.fd,
.msghdr = &s.op.recv.msghdr,
},
},
.userdata = s,
.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
const s_inner = @as(?*State, @ptrCast(@alignCast(ud))).?;
return @call(.always_inline, cb, .{
common.userdataValue(Userdata, s_inner.userdata),
l_inner,
c_inner,
s_inner,
std.net.Address.initPosix(@ptrCast(&s_inner.op.recv.addr_buffer)),
initFd(c_inner.op.recvmsg.fd),
s_inner.op.recv.buf,
if (r.recvmsg) |v| v else |err| err,
});
}
}).callback,
};
// If we're dup-ing, then we ask the backend to manage the fd.
switch (xev.backend) {
.io_uring,
.kqueue,
.wasi_poll,
.iocp,
=> {},
.epoll => c.flags.dup = true,
}
loop.add(c);
}
/// Write to the socket. This performs a single write. Additional writes
/// can be queued by calling this multiple times. Note that depending on the
/// backend, writes can happen out of order.
pub fn write(
self: Self,
loop: *xev.Loop,
c: *xev.Completion,
s: *State,
addr: std.net.Address,
buf: xev.WriteBuffer,
comptime Userdata: type,
userdata: ?*Userdata,
comptime cb: *const fn (
ud: ?*Userdata,
l: *xev.Loop,
c: *xev.Completion,
s: *State,
s: Self,
b: xev.WriteBuffer,
r: WriteError!usize,
) xev.CallbackAction,
) void {
// Set the active field for runtime safety
s.op = .{ .send = undefined };
s.* = .{
.userdata = userdata,
.op = .{
.send = .{
.addr = addr,
.buf = buf,
.msghdr = .{
.name = &s.op.send.addr.any,
.namelen = addr.getOsSockLen(),
.iov = &s.op.send.iov,
.iovlen = 1,
.control = null,
.controllen = 0,
.flags = 0,
},
.iov = undefined,
},
},
};
switch (s.op.send.buf) {
.slice => |v| {
s.op.send.iov[0] = .{
.base = v.ptr,
.len = v.len,
};
},
.array => |*arr| {
s.op.send.iov[0] = .{
.base = &arr.array,
.len = arr.len,
};
},
}
// On backends like epoll, you watch file descriptors for
// specific events. Our implementation doesn't merge multiple
// completions for a single fd, so we have to dup the fd. This
// means we use more fds than we could optimally. This isn't a
// problem with io_uring.
c.* = .{
.op = .{
.sendmsg = .{
.fd = self.fd,
.msghdr = &s.op.send.msghdr,
},
},
.userdata = s,
.callback = (struct {
fn callback(
ud: ?*anyopaque,
l_inner: *xev.Loop,
c_inner: *xev.Completion,
r: xev.Result,
) xev.CallbackAction {
const s_inner = @as(?*State, @ptrCast(@alignCast(ud))).?;
return @call(.always_inline, cb, .{
common.userdataValue(Userdata, s_inner.userdata),
l_inner,
c_inner,
s_inner,
initFd(c_inner.op.sendmsg.fd),
s_inner.op.send.buf,
if (r.sendmsg) |v| v else |err| err,
});
}
}).callback,
};
// If we're dup-ing, then we ask the backend to manage the fd.
switch (xev.backend) {
.io_uring,
.kqueue,
.wasi_poll,
.iocp,
=> {},
.epoll => c.flags.dup = true,
}
loop.add(c);
}
pub const ReadError = xev.ReadError;
pub const WriteError = xev.WriteError;
/// Common tests
pub usingnamespace UDPTests(xev, Self);
};
}
fn UDPTests(comptime xev: type, comptime Impl: type) type {
return struct {
test "UDP: read/write" {
const testing = std.testing;
var loop = try xev.Loop.init(.{});
defer loop.deinit();
const address = try std.net.Address.parseIp4("127.0.0.1", 3132);
const server = try Impl.init(address);
const client = try Impl.init(address);
// Bind / Recv
try server.bind(address);
var c_read: xev.Completion = undefined;
var s_read: Impl.State = undefined;
var recv_buf: [128]u8 = undefined;
var recv_len: usize = 0;
server.read(&loop, &c_read, &s_read, .{ .slice = &recv_buf }, usize, &recv_len, (struct {
fn callback(
ud: ?*usize,
_: *xev.Loop,
_: *xev.Completion,
_: *Impl.State,
_: std.net.Address,
_: Impl,
_: xev.ReadBuffer,
r: Impl.ReadError!usize,
) xev.CallbackAction {
ud.?.* = r catch unreachable;
return .disarm;
}
}).callback);
// Send
var send_buf = [_]u8{ 1, 1, 2, 3, 5, 8, 13 };
var c_write: xev.Completion = undefined;
var s_write: Impl.State = undefined;
client.write(&loop, &c_write, &s_write, address, .{ .slice = &send_buf }, void, null, (struct {
fn callback(
_: ?*void,
_: *xev.Loop,
_: *xev.Completion,
_: *Impl.State,
_: Impl,
_: xev.WriteBuffer,
r: Impl.WriteError!usize,
) xev.CallbackAction {
_ = r catch unreachable;
return .disarm;
}
}).callback);
// Wait for the send/receive
try loop.run(.until_done);
try testing.expect(recv_len > 0);
try testing.expectEqualSlices(u8, &send_buf, recv_buf[0..recv_len]);
// Close
server.close(&loop, &c_read, void, null, (struct {
fn callback(
_: ?*void,
_: *xev.Loop,
_: *xev.Completion,
_: Impl,
r: Impl.CloseError!void,
) xev.CallbackAction {
_ = r catch unreachable;
return .disarm;
}
}).callback);
client.close(&loop, &c_write, void, null, (struct {
fn callback(
_: ?*void,
_: *xev.Loop,
_: *xev.Completion,
_: Impl,
r: Impl.CloseError!void,
) xev.CallbackAction {
_ = r catch unreachable;
return .disarm;
}
}).callback);
try loop.run(.until_done);
}
};
}

220
deps/libxev/src/windows.zig vendored Normal file
View File

@@ -0,0 +1,220 @@
const std = @import("std");
const windows = std.os.windows;
const posix = std.posix;
pub usingnamespace std.os.windows;
/// Namespace containing missing utils from std
pub const exp = struct {
pub const STATUS_PENDING = 0x00000103;
pub const STILL_ACTIVE = STATUS_PENDING;
pub const JOBOBJECT_ASSOCIATE_COMPLETION_PORT = extern struct {
CompletionKey: windows.ULONG_PTR,
CompletionPort: windows.HANDLE,
};
pub const JOBOBJECT_BASIC_LIMIT_INFORMATION = extern struct {
PerProcessUserTimeLimit: windows.LARGE_INTEGER,
PerJobUserTimeLimit: windows.LARGE_INTEGER,
LimitFlags: windows.DWORD,
MinimumWorkingSetSize: windows.SIZE_T,
MaximumWorkingSetSize: windows.SIZE_T,
ActiveProcessLimit: windows.DWORD,
Affinity: windows.ULONG_PTR,
PriorityClass: windows.DWORD,
SchedulingClass: windows.DWORD,
};
pub const IO_COUNTERS = extern struct {
ReadOperationCount: windows.ULONGLONG,
WriteOperationCount: windows.ULONGLONG,
OtherOperationCount: windows.ULONGLONG,
ReadTransferCount: windows.ULONGLONG,
WriteTransferCount: windows.ULONGLONG,
OtherTransferCount: windows.ULONGLONG,
};
pub const JOBOBJECT_EXTENDED_LIMIT_INFORMATION = extern struct {
BasicLimitInformation: JOBOBJECT_BASIC_LIMIT_INFORMATION,
IoInfo: IO_COUNTERS,
ProcessMemoryLimit: windows.SIZE_T,
JobMemoryLimit: windows.SIZE_T,
PeakProcessMemoryUsed: windows.SIZE_T,
PeakJobMemoryUsed: windows.SIZE_T,
};
pub const JOB_OBJECT_LIMIT_ACTIVE_PROCESS = 0x00000008;
pub const JOB_OBJECT_LIMIT_AFFINITY = 0x00000010;
pub const JOB_OBJECT_LIMIT_BREAKAWAY_OK = 0x00000800;
pub const JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION = 0x00000400;
pub const JOB_OBJECT_LIMIT_JOB_MEMORY = 0x00000200;
pub const JOB_OBJECT_LIMIT_JOB_TIME = 0x00000004;
pub const JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000;
pub const JOB_OBJECT_LIMIT_PRESERVE_JOB_TIME = 0x00000004;
pub const JOB_OBJECT_LIMIT_PRIORITY_CLASS = 0x00000020;
pub const JOB_OBJECT_LIMIT_PROCESS_MEMORY = 0x00000100;
pub const JOB_OBJECT_LIMIT_PROCESS_TIME = 0x00000002;
pub const JOB_OBJECT_LIMIT_SCHEDULING_CLASS = 0x00000080;
pub const JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK = 0x00001000;
pub const JOB_OBJECT_LIMIT_SUBSET_AFFINITY = 0x00004000;
pub const JOB_OBJECT_LIMIT_WORKINGSET = 0x00000001;
pub const JOBOBJECT_INFORMATION_CLASS = enum(c_int) {
JobObjectAssociateCompletionPortInformation = 7,
JobObjectBasicLimitInformation = 2,
JobObjectBasicUIRestrictions = 4,
JobObjectCpuRateControlInformation = 15,
JobObjectEndOfJobTimeInformation = 6,
JobObjectExtendedLimitInformation = 9,
JobObjectGroupInformation = 11,
JobObjectGroupInformationEx = 14,
JobObjectLimitViolationInformation2 = 34,
JobObjectNetRateControlInformation = 32,
JobObjectNotificationLimitInformation = 12,
JobObjectNotificationLimitInformation2 = 33,
JobObjectSecurityLimitInformation = 5,
};
pub const JOB_OBJECT_MSG_TYPE = enum(windows.DWORD) {
JOB_OBJECT_MSG_END_OF_JOB_TIME = 1,
JOB_OBJECT_MSG_END_OF_PROCESS_TIME = 2,
JOB_OBJECT_MSG_ACTIVE_PROCESS_LIMIT = 3,
JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO = 4,
JOB_OBJECT_MSG_NEW_PROCESS = 6,
JOB_OBJECT_MSG_EXIT_PROCESS = 7,
JOB_OBJECT_MSG_ABNORMAL_EXIT_PROCESS = 8,
JOB_OBJECT_MSG_PROCESS_MEMORY_LIMIT = 9,
JOB_OBJECT_MSG_JOB_MEMORY_LIMIT = 10,
JOB_OBJECT_MSG_NOTIFICATION_LIMIT = 11,
JOB_OBJECT_MSG_JOB_CYCLE_TIME_LIMIT = 12,
JOB_OBJECT_MSG_SILO_TERMINATED = 13,
_,
};
pub const kernel32 = struct {
pub extern "kernel32" fn GetProcessId(Process: windows.HANDLE) callconv(windows.WINAPI) windows.DWORD;
pub extern "kernel32" fn CreateJobObjectA(lpSecurityAttributes: ?*windows.SECURITY_ATTRIBUTES, lpName: ?windows.LPCSTR) callconv(windows.WINAPI) windows.HANDLE;
pub extern "kernel32" fn AssignProcessToJobObject(hJob: windows.HANDLE, hProcess: windows.HANDLE) callconv(windows.WINAPI) windows.BOOL;
pub extern "kernel32" fn SetInformationJobObject(
hJob: windows.HANDLE,
JobObjectInformationClass: JOBOBJECT_INFORMATION_CLASS,
lpJobObjectInformation: windows.LPVOID,
cbJobObjectInformationLength: windows.DWORD,
) callconv(windows.WINAPI) windows.BOOL;
};
pub const CreateFileError = error{} || posix.UnexpectedError;
pub fn CreateFile(
lpFileName: [*:0]const u16,
dwDesiredAccess: windows.DWORD,
dwShareMode: windows.DWORD,
lpSecurityAttributes: ?*windows.SECURITY_ATTRIBUTES,
dwCreationDisposition: windows.DWORD,
dwFlagsAndAttributes: windows.DWORD,
hTemplateFile: ?windows.HANDLE,
) CreateFileError!windows.HANDLE {
const handle = windows.kernel32.CreateFileW(lpFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile);
if (handle == windows.INVALID_HANDLE_VALUE) {
const err = windows.kernel32.GetLastError();
return switch (err) {
else => windows.unexpectedError(err),
};
}
return handle;
}
pub fn ReadFile(
handle: windows.HANDLE,
buffer: []u8,
overlapped: ?*windows.OVERLAPPED,
) windows.ReadFileError!?usize {
var read: windows.DWORD = 0;
const result: windows.BOOL = windows.kernel32.ReadFile(handle, buffer.ptr, @as(windows.DWORD, @intCast(buffer.len)), &read, overlapped);
if (result == windows.FALSE) {
const err = windows.kernel32.GetLastError();
return switch (err) {
windows.Win32Error.IO_PENDING => null,
else => windows.unexpectedError(err),
};
}
return @as(usize, @intCast(read));
}
pub fn WriteFile(
handle: windows.HANDLE,
buffer: []const u8,
overlapped: ?*windows.OVERLAPPED,
) windows.WriteFileError!?usize {
var written: windows.DWORD = 0;
const result: windows.BOOL = windows.kernel32.WriteFile(handle, buffer.ptr, @as(windows.DWORD, @intCast(buffer.len)), &written, overlapped);
if (result == windows.FALSE) {
const err = windows.kernel32.GetLastError();
return switch (err) {
windows.Win32Error.IO_PENDING => null,
else => windows.unexpectedError(err),
};
}
return @as(usize, @intCast(written));
}
pub const DeleteFileError = error{} || posix.UnexpectedError;
pub fn DeleteFile(name: [*:0]const u16) DeleteFileError!void {
const result: windows.BOOL = windows.kernel32.DeleteFileW(name);
if (result == windows.FALSE) {
const err = windows.kernel32.GetLastError();
return switch (err) {
else => windows.unexpectedError(err),
};
}
}
pub const CreateJobObjectError = error{AlreadyExists} || posix.UnexpectedError;
pub fn CreateJobObject(
lpSecurityAttributes: ?*windows.SECURITY_ATTRIBUTES,
lpName: ?windows.LPCSTR,
) !windows.HANDLE {
const handle = kernel32.CreateJobObjectA(lpSecurityAttributes, lpName);
return switch (windows.kernel32.GetLastError()) {
.SUCCESS => handle,
.ALREADY_EXISTS => CreateJobObjectError.AlreadyExists,
else => |err| windows.unexpectedError(err),
};
}
pub fn AssignProcessToJobObject(hJob: windows.HANDLE, hProcess: windows.HANDLE) posix.UnexpectedError!void {
const result: windows.BOOL = kernel32.AssignProcessToJobObject(hJob, hProcess);
if (result == windows.FALSE) {
const err = windows.kernel32.GetLastError();
return switch (err) {
else => windows.unexpectedError(err),
};
}
}
pub fn SetInformationJobObject(
hJob: windows.HANDLE,
JobObjectInformationClass: JOBOBJECT_INFORMATION_CLASS,
lpJobObjectInformation: windows.LPVOID,
cbJobObjectInformationLength: windows.DWORD,
) posix.UnexpectedError!void {
const result: windows.BOOL = kernel32.SetInformationJobObject(
hJob,
JobObjectInformationClass,
lpJobObjectInformation,
cbJobObjectInformationLength,
);
if (result == windows.FALSE) {
const err = windows.kernel32.GetLastError();
return switch (err) {
else => windows.unexpectedError(err),
};
}
}
};