#!/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_waitio_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) duringSimple idiomatic dlang wrapper around linux io_uring
(see: https://kernel.dk/io_uring.pdf) asynchronous API.
during;
import (package) corecore.(package) core.syssys.(package) core.sys.linuxlinux.(module) core.sys.linux.epollD header file to interface with the Linux epoll API (http://man7.org/linux/man-pages/man7/epoll.7.html).
Available since Linux 2.6
epoll : (alias) io_uring_epoll_wait.epoll_create1 = int core.sys.linux.epoll.epoll_create1(int flags) nothrow @nogcCreates an epoll instance.
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 @nogcManipulate an epoll instance
epoll_ctl, (struct) core.sys.linux.epoll.epoll_eventepoll_event, (alias enum value) io_uring_epoll_wait.EPOLL_CTL_ADD = core.sys.linux.epoll.EPOLL_CTL_ADD = 1Add a file descriptor to the interface.
EPOLL_CTL_ADD, (alias enum value) io_uring_epoll_wait.EPOLLIN = core.sys.linux.epoll.EPOLLIN = 1EPOLLIN;
import (package) corecore.(package) core.syssys.(package) core.sys.posixposix.(module) core.sys.posix.unistdD header file for POSIX.
unistd : (alias) io_uring_epoll_wait.close = int core.sys.posix.unistd.close(int) nothrow @nogc @trustedclose, (alias) io_uring_epoll_wait.pipe = int core.sys.posix.unistd.pipe(ref int[2]) nothrow @nogc @trustedpipe, (alias) io_uring_epoll_wait.write = long core.sys.posix.unistd.write(int, scope const(void*), ulong) nothrow @nogcwrite;
import (package) corecore.(package) core.stdcstdc.(module) core.stdc.errnoD header file for C99.
pubs.opengroup.org/onlinepubs/009695399/basedefs/errno.h.html, errno.h
Source
core/stdc/errno.d
errno : (alias constant) io_uring_epoll_wait.EINVAL = int core.stdc.errno.EINVAL = 22EINVAL, (alias constant) io_uring_epoll_wait.EOPNOTSUPP = int core.stdc.errno.EOPNOTSUPP = 95EOPNOTSUPP, (alias constant) io_uring_epoll_wait.ENOSYS = int core.stdc.errno.ENOSYS = 38ENOSYS;
import (package) stdstd.(module) std.stdioCategory 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:
The lowest layer is the operating system layer. The two main schemes are Windows and Posix.
C's stdio.h which unifies the two operating system schemes.
std.stdio, this module, unifies the various stdio.h implementations into
a high level package for D programs.
Source
std/stdio.d
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.UringMain 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 ioio;
const (local variable) const(int) setupRetsetupRet = (local variable) during.Uring ioio.int during.setup(ref during.Uring uring, uint entries = 128u, during.io_uring.SetupFlags flags = SetupFlags.NONE) nothrow @nogc @safeSetup new instance of io_uring into provided Uring structure.
setup(8);
if ((local variable) const(int) setupRetsetupRet < 0)
{
void std.stdio.writefln!(char, const(int))(in char[] fmt, const(int) __param_1) @safeEquivalent to writef(fmt, args, '\n').
writefln("SKIP: io_uring_setup failed (errno %d) — io_uring unavailable on this host", -(local variable) const(int) setupRetsetupRet);
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 probeprobe = (local variable) during.Uring ioio.during.Probe during.Uring.probe() nothrow @nogc @safeProbes supported operations
probe();
if (cast(bool) (local variable) during.Probe probeprobe && !(local variable) during.Probe probeprobe.bool during.Probe.isSupported(during.io_uring.Operation op) const pure nothrow @nogc @safeIs operation supported?
isSupported((enum) during.io_uring.OperationDescribes the operation to be performed
Operation.(enum value) during.io_uring.Operation.EPOLL_WAIT = cast(ubyte)59uIORING_OP_EPOLL_WAIT - async epoll_wait(2)
EPOLL_WAIT))
{
void std.stdio.writefln!char(in char[] fmt) @safeEquivalent 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] pp;
if (int core.sys.posix.unistd.pipe(ref int[2]) nothrow @nogc @trustedpipe((local variable) int[2] pp) != 0)
{
stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("pipe() failed");
return 1;
}
scope (exit) { int core.sys.posix.unistd.close(int) nothrow @nogc @trustedclose((local variable) int[2] pp[0]); int core.sys.posix.unistd.close(int) nothrow @nogc @trustedclose((local variable) int[2] pp[1]); }
// Build a classic epoll set and register the pipe's read end for EPOLLIN.
int (local variable) int epep = int core.sys.linux.epoll.epoll_create1(int flags) nothrow @nogcCreates an epoll instance.
epoll_create1(0);
if ((local variable) int epep < 0)
{
stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("epoll_create1() failed");
return 1;
}
scope (exit) int core.sys.posix.unistd.close(int) nothrow @nogc @trustedclose((local variable) int epep);
(struct) core.sys.linux.epoll.epoll_eventepoll_event (local variable) core.sys.linux.epoll.epoll_event evev;
(local variable) core.sys.linux.epoll.epoll_event evev.(field) uint core.sys.linux.epoll.epoll_event.eventsevents = (enum value) core.sys.linux.epoll.EPOLLIN = 1EPOLLIN;
(local variable) core.sys.linux.epoll.epoll_event evev.(field) core.sys.linux.epoll.epoll_data_t core.sys.linux.epoll.epoll_event.datadata.(field) int core.sys.linux.epoll.epoll_data_t.fdfd = (local variable) int[2] pp[0];
if (int core.sys.linux.epoll.epoll_ctl(int epfd, int op, int fd, core.sys.linux.epoll.epoll_event* event) nothrow @nogcManipulate an epoll instance
epoll_ctl((local variable) int epep, (enum value) core.sys.linux.epoll.EPOLL_CTL_ADD = 1Add a file descriptor to the interface.
EPOLL_CTL_ADD, (local variable) int[2] pp[0], &(local variable) core.sys.linux.epoll.epoll_event evev) != 0)
{
stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("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 oneone = 0xAA;
if (long core.sys.posix.unistd.write(int, scope const(void*), ulong) nothrow @nogcwrite((local variable) int[2] pp[1], &(local variable) ubyte oneone, 1) != 1)
{
stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("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_eventepoll_event[4] (local variable) core.sys.linux.epoll.epoll_event[4] out_out_;
(local variable) during.Uring ioio.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 @safeAdds 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().
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 @safeAdds 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().
out_[]);
const (local variable) const(int) submittedsubmitted = (local variable) during.Uring ioio.int during.Uring.submit(uint want) nothrow @nogc @safeSubmits qued SubmissionEntry to be processed by kernel.
submit(1);
if ((local variable) const(int) submittedsubmitted < 0)
{
stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("submit failed: errno %d", -(local variable) const(int) submittedsubmitted);
return 1;
}
(local variable) during.Uring ioio.int during.Uring.wait(uint want = 1u) nothrow @nogcSimmilar to submit but with this method we just wait for required number
of CompletionEntries.
wait(1);
const (local variable) const(int) resres = (local variable) during.Uring ioio.during.io_uring.CompletionEntry during.Uring.front() pure nothrow @nogc return ref @safeGet first CompletionEntry from cq ring
front.(field) int during.io_uring.CompletionEntry.resresult code for this event
res;
(local variable) during.Uring ioio.void during.Uring.popFront() pure nothrow @nogc @safeMove 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) resres == -(constant) int core.stdc.errno.EINVAL = 22EINVAL || (local variable) const(int) resres == -(constant) int core.stdc.errno.EOPNOTSUPP = 95EOPNOTSUPP || (local variable) const(int) resres == -(constant) int core.stdc.errno.ENOSYS = 38ENOSYS)
{
void std.stdio.writefln!(char, const(int))(in char[] fmt, const(int) __param_1) @safeEquivalent to writef(fmt, args, '\n').
writefln("SKIP: IORING_OP_EPOLL_WAIT rejected by kernel (errno %d) — feature unavailable", -(local variable) const(int) resres);
return 0;
}
if ((local variable) const(int) resres < 0)
{
stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("EPOLL_WAIT completed with error: errno %d", -(local variable) const(int) resres);
return 1;
}
if ((local variable) const(int) resres != 1)
{
stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("expected exactly 1 ready event, got res=%d", (local variable) const(int) resres);
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.datadata.(field) int core.sys.linux.epoll.epoll_data_t.fdfd != (local variable) int[2] pp[0] || ((local variable) core.sys.linux.epoll.epoll_event[4] out_out_[0].(field) uint core.sys.linux.epoll.epoll_event.eventsevents & (enum value) core.sys.linux.epoll.EPOLLIN = 1EPOLLIN) == 0)
{
stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("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) @safeEquivalent 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) resres, (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.datadata.(field) int core.sys.linux.epoll.epoll_data_t.fdfd);
return 0;
}