epoll-wait.dhover×125all
#!/usr/bin/env dub
/+ dub.sdl:
    name "io_uring_epoll_wait"
    dependency "during" version="~>0.5.0"
    platforms "linux"
    targetPath "build"
+/
/**
 * `io_uring` — folding a legacy `epoll` set into the ring (`IORING_OP_EPOLL_WAIT`, Linux 6.15).
 *
 * Before 6.15 you bridged `io_uring` and `epoll` by adding the epoll fd as a
 * pollable fd (`IORING_OP_POLL_ADD` / `EPOLL_CTL`) and then calling `epoll_wait(2)`
 * synchronously once the ring told you the epoll set was readable. 6.15 added
 * `IORING_OP_EPOLL_WAIT`, which performs the `epoll_wait` *inside* the ring: you
 * submit an SQE pointing at an `epoll_event[]` buffer, and the matching CQE's
 * `res` reports how many ready events were written into it. This lets an existing
 * epoll-based event loop be migrated to `io_uring` one step at a time.
 *
 * Demonstrated here: build an epoll set watching the read end of a pipe for
 * `EPOLLIN`, write a byte to make it readable, then submit a single
 * `EPOLL_WAIT` SQE and verify it reports exactly one ready event for our pipe fd.
 * Because the pipe is already readable when we submit, the kernel can complete
 * the SQE immediately — no second thread, fully deterministic.
 *
 * Companion to the io_uring chronology:
 * see docs/research/async-io/io-uring/timeline.md
 *   § "6.15 — Zero-copy receive, epoll-wait, vectored fixed, query".
 *
 * Run with: `dub run --single epoll-wait.d`
 *
 * Portability: prints a `SKIP:` line and exits 0 when `io_uring` is unavailable
 * (old kernel / sandbox) or when this specific op is missing (kernel < 6.15,
 * surfaced as a probe miss or an `-EINVAL`/`-EOPNOTSUPP` CQE), so it stays green
 * in CI regardless of the host kernel.
 */
module 
(module) io_uring_epoll_wait

io_uring — folding a legacy epoll set into the ring (IORING_OP_EPOLL_WAIT, Linux 6.15).

Before 6.15 you bridged io_uring and epoll by adding the epoll fd as a pollable fd (IORING_OP_POLL_ADD / EPOLL_CTL) and then calling epoll_wait(2) synchronously once the ring told you the epoll set was readable. 6.15 added IORING_OP_EPOLL_WAIT, which performs the epoll_wait inside the ring: you submit an SQE pointing at an epoll_event[] buffer, and the matching CQE's res reports how many ready events were written into it. This lets an existing epoll-based event loop be migrated to io_uring one step at a time.

Demonstrated here: build an epoll set watching the read end of a pipe for EPOLLIN, write a byte to make it readable, then submit a single EPOLL_WAIT SQE and verify it reports exactly one ready event for our pipe fd. Because the pipe is already readable when we submit, the kernel can complete the SQE immediately — no second thread, fully deterministic.

Companion to the io_uring chronology: see docs/research/async-io/io-uring/timeline.md § "6.15 — Zero-copy receive, epoll-wait, vectored fixed, query".

Run with: dub run --single epoll-wait.d

Portability

prints a SKIP: line and exits 0 when io_uring is unavailable (old kernel / sandbox) or when this specific op is missing (kernel < 6.15, surfaced as a probe miss or an -EINVAL/-EOPNOTSUPP CQE), so it stays green in CI regardless of the host kernel.

io_uring_epoll_wait
;
import
(module) during

Simple idiomatic dlang wrapper around linux io_uring (see: https://kernel.dk/io_uring.pdf) asynchronous API.

during
;
import
(package) core
core
.
(package) core.sys
sys
.
(package) core.sys.linux
linux
.
(module) core.sys.linux.epoll

D header file to interface with the Linux epoll API (http://man7.org/linux/man-pages/man7/epoll.7.html). Available since Linux 2.6

@copyrightCopyright Adil Baig 2012. License : Boost License 1.0 Authors : Adil Baig (github.com/adilbaig)
epoll
:
(alias) io_uring_epoll_wait.epoll_create1 = int core.sys.linux.epoll.epoll_create1(int flags) nothrow @nogc

Creates an epoll instance.

@paramflags a specified flag. If flags is 0, then, other than the fact that the obsolete size argument is dropped, epoll_create1() is the same as epoll_create().@returnsan fd for the new instance. The fd returned by epoll_create() should be closed with close().@seeepoll_create (int size)
epoll_create1
,
(alias) io_uring_epoll_wait.epoll_ctl = int core.sys.linux.epoll.epoll_ctl(int epfd, int op, int fd, core.sys.linux.epoll.epoll_event* event) nothrow @nogc

Manipulate an epoll instance

@paramepfd an epoll file descriptor instance@paramop one of the EPOLL_CTL_* constants@paramfd target file descriptor of the operation@paramevent describes which events the caller is interested in and any associated user dat@returns0 in case of success, -1 in case of error ( the "errno" variable will contain the specific error code )
epoll_ctl
,
(struct) core.sys.linux.epoll.epoll_event
epoll_event
,
(alias enum value) io_uring_epoll_wait.EPOLL_CTL_ADD = core.sys.linux.epoll.EPOLL_CTL_ADD = 1

Add a file descriptor to the interface.

EPOLL_CTL_ADD
,
(alias enum value) io_uring_epoll_wait.EPOLLIN = core.sys.linux.epoll.EPOLLIN = 1
EPOLLIN
;
import
(package) core
core
.
(package) core.sys
sys
.
(package) core.sys.posix
posix
.
(module) core.sys.posix.unistd

D header file for POSIX.

@copyrightCopyright Sean Kelly 2005 - 2009.@licenseBoost License 1.0.@authorsSean Kelly@standardsThe Open Group Base Specifications Issue 8, IEEE Std 1003.1, 2024 Edition
unistd
:
(alias) io_uring_epoll_wait.close = int core.sys.posix.unistd.close(int) nothrow @nogc @trusted
close
,
(alias) io_uring_epoll_wait.pipe = int core.sys.posix.unistd.pipe(ref int[2]) nothrow @nogc @trusted
pipe
,
(alias) io_uring_epoll_wait.write = long core.sys.posix.unistd.write(int, scope const(void*), ulong) nothrow @nogc
write
;
import
(package) core
core
.
(package) core.stdc
stdc
.
(module) core.stdc.errno

D header file for C99.

pubs.opengroup.org/onlinepubs/009695399/basedefs/errno.h.html, errno.h

Source

core/stdc/errno.d

@copyrightCopyright Sean Kelly 2005 - 2009.@licenseDistributed under the Boost Software License 1.0. (See accompanying file LICENSE)@authorsSean Kelly, Alex Rønne Petersen@standardsISO/IEC 9899:1999 (E)
errno
:
(alias constant) io_uring_epoll_wait.EINVAL = int core.stdc.errno.EINVAL = 22
EINVAL
,
(alias constant) io_uring_epoll_wait.EOPNOTSUPP = int core.stdc.errno.EOPNOTSUPP = 95
EOPNOTSUPP
,
(alias constant) io_uring_epoll_wait.ENOSYS = int core.stdc.errno.ENOSYS = 38
ENOSYS
;
import
(package) std
std
.
(module) std.stdio
Category Symbols
File handles _popen File isFileHandle openNetwork stderr stdin stdout
Reading chunks lines readf readfln readln
Writing toFile write writef writefln writeln
Misc KeepTerminator LockType StdioException

Standard I/O functions that extend core.stdc.stdio. core.stdc.stdio is publically imported when importing std.stdio.

There are three layers of I/O:

  1. The lowest layer is the operating system layer. The two main schemes are Windows and Posix.

  2. C's stdio.h which unifies the two operating system schemes.

  3. std.stdio, this module, unifies the various stdio.h implementations into a high level package for D programs.

Source

std/stdio.d

@copyrightCopyright The D Language Foundation 2007-.@licenseBoost License 1.0.@authorsWalter Bright, Andrei Alexandrescu, Alex Rønne Petersen
stdio
:
(alias template) io_uring_epoll_wait.writefln = std.stdio.writefln(alias fmt, A...)(A args) if (isSomeString!(typeof(fmt)))

Equivalent to writef(fmt, args, '\n').

writefln
, stderr;
int
int D main()
main
()
{
(struct) during.Uring

Main entry point to work with io_uring.

It hides SubmissionQueue and CompletionQueue behind standard range interface. We put in SubmissionEntry entries and take out CompletionEntry entries.

Use predefined prepXX methods to fill required fields of SubmissionEntry before put or during putWith.

Note

prepXX functions doesn't touch previous entry state, just fills in operation properties. This is because for less error prone interface it is cleared automatically when prepared using putWith. So when using on own SubmissionEntry (outside submission queue), that would be added to the submission queue using put, be sure its cleared if it's reused for multiple operations.

Uring
(local variable) during.Uring io
io
;
const
(local variable) const(int) setupRet
setupRet
=
(local variable) during.Uring io
io
.
int during.setup(ref during.Uring uring, uint entries = 128u, during.io_uring.SetupFlags flags = SetupFlags.NONE) nothrow @nogc @safe

Setup new instance of io_uring into provided Uring structure.

@paramuring Uring structure to be initialized (must not be already initialized)@paramentries Number of entries to initialize uring with@paramflags SetupFlags to use to initialize uring.@returnsOn succes it returns 0, -errno otherwise.
setup
(8);
if (
(local variable) const(int) setupRet
setupRet
< 0)
{
void std.stdio.writefln!(char, const(int))(in char[] fmt, const(int) __param_1) @safe

Equivalent to writef(fmt, args, '\n').

writefln
("SKIP: io_uring_setup failed (errno %d) — io_uring unavailable on this host", -
(local variable) const(int) setupRet
setupRet
);
return 0; } // Probe whether the running kernel advertises IORING_OP_EPOLL_WAIT (Linux 6.15). // A miss here means the op simply doesn't exist on this kernel — skip cleanly. auto
(local variable) during.Probe probe
probe
=
(local variable) during.Uring io
io
.
during.Probe during.Uring.probe() nothrow @nogc @safe

Probes supported operations

probe
();
if (cast(bool)
(local variable) during.Probe probe
probe
&& !
(local variable) during.Probe probe
probe
.
bool during.Probe.isSupported(during.io_uring.Operation op) const pure nothrow @nogc @safe

Is operation supported?

isSupported
(
(enum) during.io_uring.Operation

Describes the operation to be performed

@seeio_uring_enter(2)
Operation
.
(enum value) during.io_uring.Operation.EPOLL_WAIT = cast(ubyte)59u

IORING_OP_EPOLL_WAIT - async epoll_wait(2)

EPOLL_WAIT
))
{
void std.stdio.writefln!char(in char[] fmt) @safe

Equivalent to writef(fmt, args, '\n').

writefln
("SKIP: IORING_OP_EPOLL_WAIT not supported (kernel < 6.15)");
return 0; } // A pipe gives us a cheap, loopback-only fd to watch for readability. int[2]
(local variable) int[2] p
p
;
if (
int core.sys.posix.unistd.pipe(ref int[2]) nothrow @nogc @trusted
pipe
(
(local variable) int[2] p
p
) != 0)
{ stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("pipe() failed");
return 1; } scope (exit) {
int core.sys.posix.unistd.close(int) nothrow @nogc @trusted
close
(
(local variable) int[2] p
p
[0]);
int core.sys.posix.unistd.close(int) nothrow @nogc @trusted
close
(
(local variable) int[2] p
p
[1]); }
// Build a classic epoll set and register the pipe's read end for EPOLLIN. int
(local variable) int ep
ep
=
int core.sys.linux.epoll.epoll_create1(int flags) nothrow @nogc

Creates an epoll instance.

@paramflags a specified flag. If flags is 0, then, other than the fact that the obsolete size argument is dropped, epoll_create1() is the same as epoll_create().@returnsan fd for the new instance. The fd returned by epoll_create() should be closed with close().@seeepoll_create (int size)
epoll_create1
(0);
if (
(local variable) int ep
ep
< 0)
{ stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("epoll_create1() failed");
return 1; } scope (exit)
int core.sys.posix.unistd.close(int) nothrow @nogc @trusted
close
(
(local variable) int ep
ep
);
(struct) core.sys.linux.epoll.epoll_event
epoll_event
(local variable) core.sys.linux.epoll.epoll_event ev
ev
;
(local variable) core.sys.linux.epoll.epoll_event ev
ev
.
(field) uint core.sys.linux.epoll.epoll_event.events
events
=
(enum value) core.sys.linux.epoll.EPOLLIN = 1
EPOLLIN
;
(local variable) core.sys.linux.epoll.epoll_event ev
ev
.
(field) core.sys.linux.epoll.epoll_data_t core.sys.linux.epoll.epoll_event.data
data
.
(field) int core.sys.linux.epoll.epoll_data_t.fd
fd
=
(local variable) int[2] p
p
[0];
if (
int core.sys.linux.epoll.epoll_ctl(int epfd, int op, int fd, core.sys.linux.epoll.epoll_event* event) nothrow @nogc

Manipulate an epoll instance

@paramepfd an epoll file descriptor instance@paramop one of the EPOLL_CTL_* constants@paramfd target file descriptor of the operation@paramevent describes which events the caller is interested in and any associated user dat@returns0 in case of success, -1 in case of error ( the "errno" variable will contain the specific error code )
epoll_ctl
(
(local variable) int ep
ep
,
(enum value) core.sys.linux.epoll.EPOLL_CTL_ADD = 1

Add a file descriptor to the interface.

EPOLL_CTL_ADD
,
(local variable) int[2] p
p
[0], &
(local variable) core.sys.linux.epoll.epoll_event ev
ev
) != 0)
{ stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("epoll_ctl(ADD) failed");
return 1; } // Make the read end readable *before* submitting, so the EPOLL_WAIT SQE can // complete immediately — keeps the example deterministic and single-threaded. ubyte
(local variable) ubyte one
one
= 0xAA;
if (
long core.sys.posix.unistd.write(int, scope const(void*), ulong) nothrow @nogc
write
(
(local variable) int[2] p
p
[1], &
(local variable) ubyte one
one
, 1) != 1)
{ stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("write() to pipe failed");
return 1; } // Submit IORING_OP_EPOLL_WAIT: the kernel runs epoll_wait against `ep` and // fills `out_` with up to its length ready events; the CQE's `res` is the count.
(struct) core.sys.linux.epoll.epoll_event
epoll_event
[4]
(local variable) core.sys.linux.epoll.epoll_event[4] out_
out_
;
(local variable) during.Uring io
io
.putWith!(
(ref SubmissionEntry e, int epfd, epoll_event[] dst) { e.prepEpollWait(epfd, dst, 0); e.user_data = 1; })(
during.Uring during.Uring.putWith!(function (ref during.io_uring.SubmissionEntry e, int epfd, core.sys.linux.epoll.epoll_event[] dst) nothrow @nogc @safe { prepEpollWait(e, epfd, dst, 0u); e.user_data = 1LU; } , int, core.sys.linux.epoll.epoll_event[])(ref int __param_0, core.sys.linux.epoll.epoll_event[] __param_1) nothrow @nogc return ref @safe

Adds new entry to the SubmissionQueue.

Note that this just adds entry to the queue and doesn't advance the tail marker kernel sees. For that finishSq() is needed to be called next.

Also note that to actually enter new entries to kernel, it's needed to call submit().

@paramFN Function to fill next entry in queue by ref (should be faster). It is expected to be in a form of void function(ARGS)(ref SubmissionEntry, auto ref ARGS). Note that in this case queue entry is cleaned first before function is called.@paramentry Custom built SubmissionEntry to be posted as is. Note that in this case it is copied whole over one in the SubmissionQueue.@paramargs Optional arguments passed to the function@returnsreference to Uring structure so it's possible to chain multiple commands.
ep
,
during.Uring during.Uring.putWith!(function (ref during.io_uring.SubmissionEntry e, int epfd, core.sys.linux.epoll.epoll_event[] dst) nothrow @nogc @safe { prepEpollWait(e, epfd, dst, 0u); e.user_data = 1LU; } , int, core.sys.linux.epoll.epoll_event[])(ref int __param_0, core.sys.linux.epoll.epoll_event[] __param_1) nothrow @nogc return ref @safe

Adds new entry to the SubmissionQueue.

Note that this just adds entry to the queue and doesn't advance the tail marker kernel sees. For that finishSq() is needed to be called next.

Also note that to actually enter new entries to kernel, it's needed to call submit().

@paramFN Function to fill next entry in queue by ref (should be faster). It is expected to be in a form of void function(ARGS)(ref SubmissionEntry, auto ref ARGS). Note that in this case queue entry is cleaned first before function is called.@paramentry Custom built SubmissionEntry to be posted as is. Note that in this case it is copied whole over one in the SubmissionQueue.@paramargs Optional arguments passed to the function@returnsreference to Uring structure so it's possible to chain multiple commands.
out_
[]);
const
(local variable) const(int) submitted
submitted
=
(local variable) during.Uring io
io
.
int during.Uring.submit(uint want) nothrow @nogc @safe

Submits qued SubmissionEntry to be processed by kernel.

@paramwant number of CompletionEntries to wait for. If 0, this just submits queued entries and returns. If > 0, it blocks until at least wanted number of entries were completed.@paramsig See io_uring_enter(2) man page@returnsNumber of submitted entries on success, -errno on error
submit
(1);
if (
(local variable) const(int) submitted
submitted
< 0)
{ stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("submit failed: errno %d", -
(local variable) const(int) submitted
submitted
);
return 1; }
(local variable) during.Uring io
io
.
int during.Uring.wait(uint want = 1u) nothrow @nogc

Simmilar to submit but with this method we just wait for required number of CompletionEntries.

@returns0 on success, -errno on error
wait
(1);
const
(local variable) const(int) res
res
=
(local variable) during.Uring io
io
.
during.io_uring.CompletionEntry during.Uring.front() pure nothrow @nogc return ref @safe

Get first CompletionEntry from cq ring

front
.
(field) int during.io_uring.CompletionEntry.res

result code for this event

res
;
(local variable) during.Uring io
io
.
void during.Uring.popFront() pure nothrow @nogc @safe

Move to next CompletionEntry

popFront
();
// Some kernels accept setup/submit but reject the op at completion time — treat // -EINVAL / -EOPNOTSUPP / -ENOSYS as "feature absent" rather than a hard failure. if (
(local variable) const(int) res
res
== -
(constant) int core.stdc.errno.EINVAL = 22
EINVAL
||
(local variable) const(int) res
res
== -
(constant) int core.stdc.errno.EOPNOTSUPP = 95
EOPNOTSUPP
||
(local variable) const(int) res
res
== -
(constant) int core.stdc.errno.ENOSYS = 38
ENOSYS
)
{
void std.stdio.writefln!(char, const(int))(in char[] fmt, const(int) __param_1) @safe

Equivalent to writef(fmt, args, '\n').

writefln
("SKIP: IORING_OP_EPOLL_WAIT rejected by kernel (errno %d) — feature unavailable", -
(local variable) const(int) res
res
);
return 0; } if (
(local variable) const(int) res
res
< 0)
{ stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("EPOLL_WAIT completed with error: errno %d", -
(local variable) const(int) res
res
);
return 1; } if (
(local variable) const(int) res
res
!= 1)
{ stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("expected exactly 1 ready event, got res=%d",
(local variable) const(int) res
res
);
return 1; } if (
(local variable) core.sys.linux.epoll.epoll_event[4] out_
out_
[0].
(field) core.sys.linux.epoll.epoll_data_t core.sys.linux.epoll.epoll_event.data
data
.
(field) int core.sys.linux.epoll.epoll_data_t.fd
fd
!=
(local variable) int[2] p
p
[0] || (
(local variable) core.sys.linux.epoll.epoll_event[4] out_
out_
[0].
(field) uint core.sys.linux.epoll.epoll_event.events
events
&
(enum value) core.sys.linux.epoll.EPOLLIN = 1
EPOLLIN
) == 0)
{ stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("ready event did not match the pipe fd / EPOLLIN");
return 1; }
void std.stdio.writefln!(char, const(int), int)(in char[] fmt, const(int) __param_1, int __param_2) @safe

Equivalent to writef(fmt, args, '\n').

writefln
("ok: IORING_OP_EPOLL_WAIT reported %d ready event for pipe fd %d (EPOLLIN), epoll folded into the ring",
(local variable) const(int) res
res
,
(local variable) core.sys.linux.epoll.epoll_event[4] out_
out_
[0].
(field) core.sys.linux.epoll.epoll_data_t core.sys.linux.epoll.epoll_event.data
data
.
(field) int core.sys.linux.epoll.epoll_data_t.fd
fd
);
return 0; }