Giter Site home page Giter Site logo

mitchellh / libxev Goto Github PK

View Code? Open in Web Editor NEW
1.6K 20.0 42.0 1.06 MB

libxev is a cross-platform, high-performance event loop that provides abstractions for non-blocking IO, timers, events, and more and works on Linux (io_uring or epoll), macOS (kqueue), and Wasm + WASI. Available as both a Zig and C API.

License: MIT License

Zig 97.39% Nix 0.45% C 0.75% JavaScript 0.14% TypeScript 0.11% CSS 1.11% MDX 0.04%
async c epoll io-uring kqueue wasi webassembly zig

libxev's Introduction

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 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, 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, even mimicking its style on top of other OS primitives ( credit to this awesome blog post). 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. 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 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.

Zig C
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;
}
#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;
}

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:

.{
    .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:

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 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. libxev has no other build dependencies.

Once installed, zig build install on its own will build the full library and output a FHS-compatible 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:

$ 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:

$ 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 installed:

$ zig build test -Dtarget=wasm32-wasi -Dwasmtime
...

libxev's People

Contributors

almmiko avatar cdolan avatar corendos avatar dependabot[bot] avatar der-teufel-programming avatar iacore avatar ianic avatar kcbanner avatar kyleamathews avatar linuxy avatar mitchellh avatar moderation avatar penberg avatar recursivegecko avatar rosscomputerguy avatar rsepassi avatar vulfox avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

libxev's Issues

Stream HLA

For epoll/kqueue compat file descriptors

Make `Stream` watcher directly accessible

This would allow users to mixin Writable, Readable and Closeable to their own watchers. By that, file descriptors that are not supported directly by libxev can be implemented with ease.

Export "old"-style `Pkg` for build.zig if Zig version is older

From #18:

I think we should do a semver check on builtin.zig_version (stdlib has all the functions) to continue exported a Pkg type so that downstream can continue to use older Zig versions. I'm interested in staying on nightly but the build system in particular is SO unstable right now that I think keeping downstream consumers pkg-aware is fine for a few weeks/months.

Selfish need because I have a downstream use of this lib and it has a bunch of other deps that aren't updated yet so I can't update myself...

This is all comptime so it makes our build.zig compatible with multiple Zig versions.

Not sure what Git sha to go back to yet. I think 1557+03cdb4fb5 is probably safe.

Windows - thread panic: integer overflow

This is probably user error since I'm not fully sure what I'm doing, my apologies in advance.

I have multiple threads trying to accept TCP requests. Based on the documentation it seems this should be possible as long as each thread has their own loop. However, when I do this things go wrong when multiple TCP connections come in at the same time:

C:\Users\Jeroen\Desktop\libxev\src\backend\iocp.zig:346:29: 0x7ff7974f825c in tick (http.exe.obj)
                self.active -= 1;
                            ^
C:\Users\Jeroen\Desktop\libxev\src\backend\iocp.zig:171:62: 0x7ff7974f9932 in run (http.exe.obj)
            .until_done => while (!self.done()) try self.tick(1),
                                                             ^
C:\Users\Jeroen\Desktop\libxev\http.zig:92:30: 0x7ff7974f9abe in start (http.exe.obj)
            try self.loop.run(.until_done);
                             ^
C:\Users\Jeroen\zig\lib\std\Thread.zig:432:13: 0x7ff7974e1288 in callFn__anon_10500 (http.exe.obj)
            @call(.auto, f, args) catch |err| {
            ^
C:\Users\Jeroen\zig\lib\std\Thread.zig:523:30: 0x7ff7974a807e in entryFn (http.exe.obj)
                return callFn(f, self.fn_args);
                             ^
???:?:?: 0x7ff906dc7343 in ??? (KERNEL32.DLL)
???:?:?: 0x7ff9089e26b0 in ??? (ntdll.dll)

It seems like the completions are somehow added to the same loop even though I init them per thread. Am I doing something wrong? Or is this usecase currently not supported with the IOCP backend?

My code:

const xev = @import("xev");
const std = @import("std");
const Allocator = std.mem.Allocator;
const Thread = std.Thread;
const Mutex = std.Thread.Mutex;
const log = std.log;

const TCPPool = std.heap.MemoryPool(xev.TCP);
const CompletionPool = std.heap.MemoryPool(xev.Completion);
const BufferPool = std.heap.MemoryPool([4096]u8);

pub fn main() !void {
    const GPA = std.heap.GeneralPurposeAllocator(.{});
    var gpa: GPA = .{};
    defer _ = gpa.deinit();
    const alloc = gpa.allocator();

    var server = try Server.init(alloc);

    try server.start();
}

const Server = struct {
    allocator: Allocator,

    pub fn init(alloc: Allocator) !Server {
        return .{ .allocator = alloc };
    }

    pub fn start(self: *Server) !void {
        const addr = try std.net.Address.parseIp4("0.0.0.0", 8088);
        var socket = try xev.TCP.init(addr);

        try socket.bind(addr);
        try socket.listen(std.os.linux.SOMAXCONN);

        const worker_count = 2;
        log.info("(server) init {d} workers...", .{worker_count});

        const workers = try self.allocator.alloc(Worker, worker_count);
        const threads = try self.allocator.alloc(Thread, worker_count);

        var started: usize = 0;

        defer {
            for (0..started) |i| {
                threads[i].join();
                workers[i].deinit();
            }
            self.allocator.free(workers);
            self.allocator.free(threads);
        }

        for (0..workers.len) |i| {
            workers[i] = try Worker.init(self.allocator, self, &socket, i);
            threads[i] = try Thread.spawn(.{}, Worker.start, .{&workers[i]});
            started += 1;
        }
        var wait: Thread.Condition = .{};

        var mutex = Thread.Mutex{};
        mutex.lock();
        wait.wait(&mutex);
        mutex.unlock();
    }
};

const Worker = struct {
    loop: xev.Loop,
    server: *Server,
    alloc: Allocator,

    socket: *xev.TCP,

    socket_pool: TCPPool,
    completion_pool: CompletionPool,
    buffer_pool: BufferPool,

    worker_id: usize,

    pub fn init(alloc: Allocator, server: *Server, socket: *xev.TCP, worker_id: usize) !Worker {
        return .{ .loop = try xev.Loop.init(.{}), .server = server, .alloc = alloc, .socket = socket, .socket_pool = TCPPool.init(alloc), .buffer_pool = BufferPool.init(alloc), .completion_pool = CompletionPool.init(alloc), .worker_id = worker_id };
    }

    pub fn start(self: *Worker) !void {
        while (true) {
            const c = try self.completion_pool.create();

            log.info("(worker {d}) Wait for accept...", .{self.worker_id});
            self.socket.accept(&self.loop, c, Worker, self, acceptCallback);

            try self.loop.run(.until_done);
        }
    }

    fn acceptCallback(
        self_: ?*Worker,
        l: *xev.Loop,
        c: *xev.Completion,
        r: xev.TCP.AcceptError!xev.TCP,
    ) xev.CallbackAction {
        const self = self_.?;

        const socket = self.socket_pool.create() catch unreachable;
        socket.* = r catch unreachable;

        log.info("(worker {d}) Accept callback [loop: {*}] [completion: {*}]", .{ self.worker_id, l, c });

        const buf = self.buffer_pool.create() catch unreachable;
        _ = buf;

        // socket.read(l, c, .{ .slice = buf }, Worker, self, readCallback);
        return .disarm;
    }

    pub fn deinit(self: *Worker) void {
        self.buffer_pool.deinit();
        self.completion_pool.deinit();
        self.socket_pool.deinit();
        self.loop.deinit();
    }
};

When I make a concurrent request, e.g. curl http://localhost:8088 & curl http://localhost:8088 & curl http://localhost:8088 & curl http://localhost:8088 the crash occurs.

Embedding libxev with freestanding targets

I was wondering if it would be possible to add freestanding support (so libxev could be used in MCU's and kernels). A possible way would be to check the root module with @hasDecl() and then load in the backend. I have a use for this which is a cross platform UI toolkit which can run in UEFI.

Add FreeBSD support

Please add FreeBSD support. Currently it doesn't build under FreeBSD 13.2-RELEASE and zig-0.11:

$ zig build
zig build-lib xev Debug native: error: the following command failed with 1 compilation errors:
/usr/local/bin/zig build-lib /tmp/libxev/src/c_api.zig -lc --cache-dir /tmp/libxev/zig-cache --global-cache-dir /home/iron/.cache/zig --name xev -static --listen=- 
zig build-lib xev Debug native: error: the following command failed with 1 compilation errors:
/usr/local/bin/zig build-lib /tmp/libxev/src/c_api.zig --cache-dir /tmp/libxev/zig-cache --global-cache-dir /home/iron/.cache/zig --name xev -dynamic --listen=- 
Build Summary: 2/7 steps succeeded; 2 failed (disable with --summary none)
install transitive failure
├─ install xev transitive failure
│  └─ zig build-lib xev Debug native 1 errors
├─ zig build-lib xev Debug native (reused)
├─ install xev transitive failure
│  └─ zig build-lib xev Debug native 1 errors
└─ zig build-lib xev Debug native (reused)
src/main.zig:43:13: error: no default backend for this target
            @compileError("no default backend for this target");
            ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/main.zig:6:28: note: called from here
const xev = Backend.default().Api();
            ~~~~~~~~~~~~~~~^~

Compile Log Output:
@as(target.Target.Os, .{.tag = .freebsd, .version_range = .{ .semver = .{.min = .{ ... }, .max = .{ ... }} }})
src/main.zig:43:13: error: no default backend for this target
            @compileError("no default backend for this target");
            ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/main.zig:6:28: note: called from here
const xev = Backend.default().Api();
            ~~~~~~~~~~~~~~~^~

Compile Log Output:
@as(target.Target.Os, .{.tag = .freebsd, .version_range = .{ .semver = .{.min = .{ ... }, .max = .{ ... }} }})

$ zig version
0.11.0

Thank you!

Windows bug?

Don't have a clean repro at the moment, but initial check of the Windows support indicated that 2 timers were running sequentially instead of in parallel; could very well have been holding it wrong, but the same code worked fine on Linux and Mac. cc @Corendos

Non-clean repro is the zigcoro test. https://github.com/rsepassi/zigcoro windows branch

Potential issue in UDP ping-pong benchmark

What's the issue

In the ping-udp1.zig benchmark, there is a potential issue that can arise depending on the order of execution of read/write callbacks.

If everything is fine, the write callback will run first, thus setting the associated Completion to a .dead state and ready to be used again.

However, if the read callback runs first, here is what happens:

  • During the read callback, we call the Pinger.write(...) method and enqueue a write operation using the Pinger.c_write field. This has the effect to put the completion in the list of completion to submit on the next tick
  • Then, when the write completion is executed, we set the state to the Completion to .dead
  • When the time has come to submit the awaiting completion, the c_write completion is discarded because its state is set to .dead
  • There is sort of a livelock, because the read operation is waiting for a write operation that will never happen

Context

I noticed that while working on the IOCP backend. Somehow, the read completion was handled before the write operation and it lead to the described behavior.

Potential solutions

Solution 1 - Simple but kind of misses the point of the bench

Instead of queuing the read operation at the same time as the write operation, we can wait for the write to be completed. However, this introduce artificial latency and despite making the behavior correct, it doesn't really bench anything as operation are serialized.

Solution 2

Another approach would be to wait for both callbacks to have been executed before enqueuing new operations. That would solve the issue of the c_write completion to be reused before its callback has been called.

TTY streams

We want to be able to block on read/writes of TTY fds. This may just work with generic streams but we may also need to inspect and set some flags (i.e. nonblock) on the TTY. Do some investigation.

More advanced examples of using the API?

I am using libxev to write a toy database library that's looks something like RocksDB or SQLite where you embed the thing into your application. However, the examples in the tree only show stuff like timers that arm or rearm them. I would love to know how you @mitchellh use libxev in a more complex application.

For example, when my toy library reads the database file, it first needs to parse the header before anything else can happen. What would be the recommended way to do this? Submit the read and return a completion to it? How do you structure the application to wait for the completion?

I tried to make an API like read_header(callback) that takes a callback that libxev invokes on I/O completion, but that's proving to be bit awkward with the file watcher API. (It's also possible that I just have no idea what I am doing.) Another option I explored was to return the completion from read_header(callback), but now it's unclear to me how I would even wait for that without busy-polling.

Feature Request: async dns

Hi Mitchell,

Nice library, can we add async dns to the roadmap, because it's really helpful for network programming.

Thanks

Process (pid) monitoring

Add the ability to monitor for the exit of a process.

  • Epoll: pidfd
  • io_uring: pidfd
  • kqueue: EVFILT_PROC
  • wasi: unsupported

Wasm freestanding (i.e. browser) via extern loop

Real goal: I want libxev to "work" in the browser so that applications using the event loop can be ported more easily (I have a real world example personal project I want to make work).

The idea is to support a "WasmExtern" backend that defers the actual event loop to the host environment (i.e. the browser). The host environment will call back into the module environment in order to trigger a "tick" and have callbacks be called.

This will make the consumer API slightly different since we can't just block on loop.run(.until_done). But consumers can check something like if (xev.Loop.is_async) or something and work differently.

How to embed this in wasi

Real goal: I want to implement event loop in wasi and use webassembly as serverless runtime (handling multiple http calls in the same invocation, aggregate / transform the response)

Is there a way I can use this? What would you suggest the approach would be, to port this to a wasi runtime? Like porting to Spin (fermyon) for instance

Handle EAGAIN errno's properly

When working on a project, I wanted to see if setting NONBLOCK would allow for the thread to shutdown if the loop is stopped. I enabled it and got errno 11 which is EAGAIN. I think libxev should handle this error and just continue to monitor until the next event. This would make it so developers using libxev won't have to catch this error on their own.

neutron.runtime.ipc.base.Ipc{ .server = neutron.runtime.ipc.base.server{ .type = neutron.elemental.type.Type(neutron.runtime.ipc.base.server,neutron.runtime.ipc.base.server.Params,neutron.runtime.ipc.base.server.Impl){ .allocated = false, .allocator = mem.Allocator{ ... }, .parent = anyopaque@7ffc7e50dc80, .ref = neutron.elemental.ref{ ... } }, .base = neutron.runtime.ipc.base.base{ .type = neutron.elemental.type.Type(neutron.runtime.ipc.base.base,neutron.runtime.ipc.base.base.Params,neutron.runtime.ipc.base.base.Impl){ ... }, .vtable = neutron.runtime.ipc.base.base.VTableunexpected errno: { ... }, .runtime11 =
neutron.runtime.runtime{ ... }, .loop = backend.io_uring.Loop{ ... }, .running = false, .thread = null } } }/nix/store/vxhdcs2nc56ynlkm89f6f0qndsk5rs36-zig-0.11.0-dev.2247+38ee46dda/lib/std/debug.zig:561:19: 0x2939a7 in writeCurrentStackTrace__anon_10812 (neutron-runner)
    while (it.next()) |return_address| {
                  ^
/nix/store/vxhdcs2nc56ynlkm89f6f0qndsk5rs36-zig-0.11.0-dev.2247+38ee46dda/lib/std/debug.zig:157:80: 0x257b8d in dumpCurrentStackTrace (neutron-runner)
        writeCurrentStackTrace(stderr, debug_info, detectTTYConfig(io.getStdErr()), start_addr) catch |err| {
                                                                               ^
/nix/store/vxhdcs2nc56ynlkm89f6f0qndsk5rs36-zig-0.11.0-dev.2247+38ee46dda/lib/std/os.zig:5559:40: 0x23c438 in unexpectedErrno (neutron-runner)
        std.debug.dumpCurrentStackTrace(null);
                                       ^
/home/ross/ExpidusOS/neutron/vendor/third-party/zig/libxev/src/backend/io_uring.zig:557:59: 0x25bb33 in invoke (neutron-runner)
                    else => |errno| std.os.unexpectedErrno(errno),
                                                          ^
/home/ross/ExpidusOS/neutron/vendor/third-party/zig/libxev/src/backend/io_uring.zig:147:43: 0x25d408 in tick___anon_8624 (neutron-runner)
                switch (c.invoke(self, cqe.res)) {
                                          ^
/home/ross/ExpidusOS/neutron/vendor/third-party/zig/libxev/src/backend/io_uring.zig:60:42: 0x25d5b0 in run (neutron-runner)
            .until_done => try self.tick_(.until_done),
                                         ^
/home/ross/ExpidusOS/neutron/src/neutron/runtime/ipc/base/base.zig:75:24: 0x25d618 in callback (neutron-runner)
      try base.loop.run(.until_done);
                       ^
/nix/store/vxhdcs2nc56ynlkm89f6f0qndsk5rs36-zig-0.11.0-dev.2247+38ee46dda/lib/std/Thread.zig:427:13: 0x296166 in callFn__anon_10930 (neutron-runner)
            @call(.auto, f, args) catch |err| {
            ^
/nix/store/vxhdcs2nc56ynlkm89f6f0qndsk5rs36-zig-0.11.0-dev.2247+38ee46dda/lib/std/Thread.zig:679:30: 0x25de5c in entryFn (neutron-runner)
                return callFn(f, args_ptr.*);
                             ^
???:?:?: 0x7f9e6c40be85 in ??? (???)
thread 2996945 panic: attempt to unwrap error: Unexpected
/home/ross/ExpidusOS/neutron/src/neutron/runtime/ipc/socket/server.zig:93:37: 0x241d23 in callback (neutron-runner)
        const fd = res.accept catch unreachable;
                                    ^
/home/ross/ExpidusOS/neutron/vendor/third-party/zig/libxev/src/backend/io_uring.zig:671:34: 0x25c7ac in invoke (neutron-runner)
        return self.callback(self.userdata, loop, self, result);
                                 ^
/home/ross/ExpidusOS/neutron/vendor/third-party/zig/libxev/src/backend/io_uring.zig:147:43: 0x25d408 in tick___anon_8624 (neutron-runner)
                switch (c.invoke(self, cqe.res)) {
                                          ^
/home/ross/ExpidusOS/neutron/vendor/third-party/zig/libxev/src/backend/io_uring.zig:60:42: 0x25d5b0 in run (neutron-runner)
            .until_done => try self.tick_(.until_done),
                                         ^
/home/ross/ExpidusOS/neutron/src/neutron/runtime/ipc/base/base.zig:75:24: 0x25d618 in callback (neutron-runner)
      try base.loop.run(.until_done);
                       ^
/nix/store/vxhdcs2nc56ynlkm89f6f0qndsk5rs36-zig-0.11.0-dev.2247+38ee46dda/lib/std/Thread.zig:427:13: 0x296166 in callFn__anon_10930 (neutron-runner)
            @call(.auto, f, args) catch |err| {
            ^
/nix/store/vxhdcs2nc56ynlkm89f6f0qndsk5rs36-zig-0.11.0-dev.2247+38ee46dda/lib/std/Thread.zig:679:30: 0x25de5c in entryFn (neutron-runner)
                return callFn(f, args_ptr.*)

Socket example using Antiphony

I'm currently trying to use libxev to perform RPC calls in the background along with Antiphony. I tried looking at how libxev works and I cannot figure out how to create an event loop and have it handle Antiphony clients without blocking the main process.

Feature request: profile benchmarking "mode"

I'm looking for a way to effectively benchmark my libxev event loops per iteration of the loop. It may be possible to write libxev to have a comptime enabled "profiling mode" which measures the speed of each completion and each iteration of the loop. The benefit of doing this in libxev would be that it's standard and doesn't rely on some trickery. The only real issue I see here is determining what measurement should be used. Maybe it's possible to set different ones through an enum, some options could possibly be real_time, cpu_time, etc. It should also be possible to hook into some sort of method to retrieve the data so it may be logged by the developer.

Reading exactly N bytes from a file?

I have the following example code that attempts to read 100 bytes from a file:

const f = try xev.File.init(file);
var read_buf: [100]u8 = undefined;
var read_len: ?usize = null;
var c_read: xev.Completion = undefined;
f.read(loop, &c_read, .{ .slice = &read_buf }, ?usize, &read_len, (struct {
    fn callback(
        ud: ?*?usize,
        _: *xev.Loop,
        _: *xev.Completion,
        _: xev.File,
        _: xev.ReadBuffer,
        r: Self.ReadError!usize,
    ) xev.CallbackAction {
        ud.?.* = r catch unreachable;
        return .disarm;
    }
}).callback);
try loop.run(.until_done);
assert(read_len == 100);

However, the assertion fails because read_len is 1. This is, of course, allowed by the read() system call (I am on mac) so perhaps a new read_exact() API is needed for this use case?

Is this based on callbacks? Is it possible to be `async` based?

I've looked at the readme example, and it seems to be very similar to the C libev library, which is based on callbacks.

When there's a lot of business, this can lead to callback hell, and that's really cumbersome, is it possible to avoid callback hell by using zig's async functionality?

i.e. writing code in a "synchronous" coding style, but the underlying code is still "asynchronous and non-blocking".

Serialized writes

RIght now, if multiple writes are submitted, all writes happen in any possible order (i.e. io_uring makes no guarantee about write order). Therefore, program authors must be careful to only submit the next write when the previous write callback succeeds. This may or may not be easy to do for program authors.

The recommendation for now is that program authors maintain their own queue, and use the write callback as a way to automatically rearm with the next queued item.

We may want to consider introducing a higher level abstraction concept of serialized writes, where the completion and write queue is managed by this abstraction. I think it makes sense for the lower level loop to remain unaware of the write queue.

Windows

Maybe in the future ?

Bun also can’t fully work on windows

website content + deploy

I bought libxev.{com,org}. Nothing fancy, just a place where there are some more user friendly docs. We need to setup the docs scaffold.

Need more examples: C Server

Hi @mitchellh,

Nice hard work (for you and all your collaborators) to make this library.

I have been playing with the xev api for C code on linux today.
However, some trouble was encountered when running the same code with or without xev.

Client: curl 127.0.0.1:8000.

Without xev

C code - server
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define PORT 8000 // The port number for the server to listen on

int main() {
    int server_socket, client_socket;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    
    // Create a socket
    server_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (server_socket == -1) {
        perror("Socket creation failed");
        exit(1);
    }
    
    // Bind the socket to an IP address and port
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY; // Listen on all available network interfaces
    server_addr.sin_port = htons(PORT);
    
    if (bind(server_socket, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("Socket binding failed");
        close(server_socket);
        exit(1);
    }
    
    // Listen for incoming connections
    if (listen(server_socket, 10) == -1) {
        perror("Listen failed");
        close(server_socket);
        exit(1);
    }
    
    printf("Server listening on port %d...\n", PORT);
    
    while (1) {
        // Accept a client connection
        client_socket = accept(server_socket, (struct sockaddr*)&client_addr, &client_addr_len);
        if (client_socket == -1) {
            perror("Accept failed");
            continue; // Continue listening for other connections
        }
        
        printf("Connection accepted from %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
        
        // Handle the client connection (in this example, we simply echo back received data)
        char buffer[1024];
        ssize_t bytes_received;
        
        while ((bytes_received = recv(client_socket, buffer, sizeof(buffer), 0)) > 0) {
            buffer[bytes_received] = '\0'; // Null-terminate the received data
            printf("Received: %s", buffer);
            
            // Echo back to the client
            send(client_socket, buffer, bytes_received, 0);
        }
        
        // Close the client socket
        close(client_socket);
        printf("Connection closed by %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
    }
    
    // Close the server socket (this part will not be reached in this example)
    close(server_socket);
    
    return 0;
}

Output

$> ./server_example 
Server listening on port 8000...
Connection accepted from 127.0.0.1:42220
Received: GET / HTTP/1.1
Host: 127.0.0.1:8000
User-Agent: curl/8.2.1
Accept: */*

Connection closed by 127.0.0.1:42220

With xev

C code - server
#include "xev.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 8000 // The port number for the server to listen on

void on_connection(xev_loop* loop, xev_completion* c, int result, void* userdata) {
    int server_socket = *(int*)userdata;

    int client_socket = accept(server_socket, NULL, NULL);
    if (client_socket == -1) {
        perror("Accept failed");
        return;
    }

    char buffer[1024];
    ssize_t bytes_received;

    while ((bytes_received = recv(client_socket, buffer, sizeof(buffer), 0)) > 0) {
        buffer[bytes_received] = '\0'; // Null-terminate the received data
        printf("Received: %s", buffer);

        // Echo back to the client
        send(client_socket, buffer, bytes_received, 0);
    }

    close(client_socket);
}

int main() {
    xev_loop loop;
    if (xev_loop_init(&loop) != 0) {
        fprintf(stderr, "Failed to initialize event loop\n");
        return 1;
    }

    int server_socket, client_socket;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addr_len = sizeof(client_addr);

    // Create a socket
    server_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (server_socket == -1) {
        perror("Socket creation failed");
        exit(1);
    }

    // Bind the socket to an IP address and port
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY; // Listen on all available network interfaces
    server_addr.sin_port = htons(PORT);

    if (bind(server_socket, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("Socket binding failed");
        close(server_socket);
        exit(1);
    }

    // Listen for incoming connections
    if (listen(server_socket, 10) == -1) {
        perror("Listen failed");
        close(server_socket);
        exit(1);
    }

    printf("Server listening on port %d...\n", PORT);

    // Pass the server_socket as user data to the connection handler
    int user_data = server_socket;
    xev_completion completion;
    xev_completion_zero(&completion);
    xev_threadpool_task task;
    xev_threadpool_task_init(&task, (xev_task_cb)on_connection);

    xev_threadpool_batch batch;
    xev_threadpool_batch_init(&batch);
    xev_threadpool_batch_push_task(&batch, &task);

    xev_threadpool threadpool;
    xev_threadpool_init(&threadpool, NULL);
    xev_threadpool_schedule(&threadpool, &batch);

    xev_threadpool_deinit(&threadpool);

    xev_loop_run(&loop, XEV_RUN_UNTIL_DONE);

    xev_loop_deinit(&loop);
    close(server_socket);

    return 0;
}

Output

$> ./server_example 
Server listening on port 8000...
Accept failed: Bad file descriptor

hold client, needed Ctrl+C to stopping.


Note: gcc get errors on data array on header.

include/xev.h:36:44: error: variably modified ‘data’ at file scope
   36 | typedef struct { XEV_ALIGN_T _pad; uint8_t data[XEV_SIZEOF_LOOP - sizeof(XEV_ALIGN_T)]; } xev_loop;
      |                                            ^~~~
include/xev.h:37:44: error: variably modified ‘data’ at file scope
   37 | typedef struct { XEV_ALIGN_T _pad; uint8_t data[XEV_SIZEOF_COMPLETION - sizeof(XEV_ALIGN_T)]; } xev_completion;
      |                                            ^~~~
include/xev.h:38:44: error: variably modified ‘data’ at file scope
   38 | typedef struct { XEV_ALIGN_T _pad; uint8_t data[XEV_SIZEOF_WATCHER - sizeof(XEV_ALIGN_T)]; } xev_watcher;
      |                                            ^~~~
include/xev.h:39:44: error: variably modified ‘data’ at file scope
   39 | typedef struct { XEV_ALIGN_T _pad; uint8_t data[XEV_SIZEOF_THREADPOOL - sizeof(XEV_ALIGN_T)]; } xev_threadpool;
      |                                            ^~~~
include/xev.h:40:44: error: variably modified ‘data’ at file scope
   40 | typedef struct { XEV_ALIGN_T _pad; uint8_t data[XEV_SIZEOF_THREADPOOL_BATCH - sizeof(XEV_ALIGN_T)]; } xev_threadpool_batch;
      |                                            ^~~~
include/xev.h:41:44: error: variably modified ‘data’ at file scope
   41 | typedef struct { XEV_ALIGN_T _pad; uint8_t data[XEV_SIZEOF_THREADPOOL_TASK - sizeof(XEV_ALIGN_T)]; } xev_threadpool_task;
      |                                            ^~~~
include/xev.h:42:44: error: variably modified ‘data’ at file scope
   42 | typedef struct { XEV_ALIGN_T _pad; uint8_t data[XEV_SIZEOF_THREADPOOL_CONFIG - sizeof(XEV_ALIGN_T)]; } xev_threadpool_config;

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.