pipe.dhover×117all
#!/usr/bin/env dub
/+ dub.sdl:
    name "io_uring_pipe"
    dependency "during" version="~>0.5.0"
    platforms "linux"
    targetPath "build"
+/
/**
 * `io_uring` — async pipe creation (`IORING_OP_PIPE`, Linux 6.16).
 *
 * Before 6.16, creating a pipe meant a synchronous `pipe2(2)` syscall outside
 * the ring. `IORING_OP_PIPE` lets the ring itself manufacture the pipe pair:
 * submit one SQE pointing at an `int[2]` buffer and, on completion, the kernel
 * has filled it with the read end (`fds[0]`) and write end (`fds[1]`) — exactly
 * like `pipe2(2)`, but folded into the same submit/complete batch as the rest of
 * your I/O (so a pipe can be created and immediately used in a single linked
 * chain without a syscall round-trip).
 *
 * This example creates a pipe through the ring, then proves it actually works by
 * writing a few bytes into the write end and reading them back out of the read
 * end (plain libc `write`/`read` — the point here is the *creation* op, not the
 * transfer), asserting the bytes round-trip before closing both ends.
 *
 * Companion to the io_uring chronology:
 * see docs/research/async-io/io-uring/timeline.md § "6.16 — Async pipe".
 *
 * Run with: `dub run --single pipe.d`
 *
 * Portability: prints a `SKIP:` line and exits 0 if io_uring is unavailable, or
 * if the `PIPE` op is unsupported (kernel older than the op's introduction —
 * detected via the probe or an `-EINVAL`/`-EOPNOTSUPP` completion), so it stays
 * green on CI hosts running older kernels.
 */
module 
(module) io_uring_pipe

io_uring — async pipe creation (IORING_OP_PIPE, Linux 6.16).

Before 6.16, creating a pipe meant a synchronous pipe2(2) syscall outside the ring. IORING_OP_PIPE lets the ring itself manufacture the pipe pair: submit one SQE pointing at an int[2] buffer and, on completion, the kernel has filled it with the read end (fds[0]) and write end (fds[1]) — exactly like pipe2(2), but folded into the same submit/complete batch as the rest of your I/O (so a pipe can be created and immediately used in a single linked chain without a syscall round-trip).

This example creates a pipe through the ring, then proves it actually works by writing a few bytes into the write end and reading them back out of the read end (plain libc write/read — the point here is the creation op, not the transfer), asserting the bytes round-trip before closing both ends.

Companion to the io_uring chronology: see docs/research/async-io/io-uring/timeline.md § "6.16 — Async pipe".

Run with: dub run --single pipe.d

Portability

prints a SKIP: line and exits 0 if io_uring is unavailable, or if the PIPE op is unsupported (kernel older than the op's introduction — detected via the probe or an -EINVAL/-EOPNOTSUPP completion), so it stays green on CI hosts running older kernels.

io_uring_pipe
;
import
(module) during

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

during
;
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_pipe.writefln = std.stdio.writefln(alias fmt, A...)(A args) if (isSomeString!(typeof(fmt)))

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

writefln
, stderr;
import
(package) core
core
.
(package) core.sys
sys
.
(package) core.sys.linux
linux
.
(module) core.sys.linux.errno

D header file for GNU/Linux

glibc stdlib/errno.h

errno
:
(alias constant) io_uring_pipe.EINVAL = int core.stdc.errno.EINVAL = 22
EINVAL
,
(alias constant) io_uring_pipe.EOPNOTSUPP = int core.stdc.errno.EOPNOTSUPP = 95
EOPNOTSUPP
,
(alias constant) io_uring_pipe.ENOSYS = int core.stdc.errno.ENOSYS = 38
ENOSYS
;
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_pipe.close = int core.sys.posix.unistd.close(int) nothrow @nogc @trusted
close
,
(alias) io_uring_pipe.read = long core.sys.posix.unistd.read(int, void*, ulong) nothrow @nogc
read
,
(alias) io_uring_pipe.write = long core.sys.posix.unistd.write(int, scope const(void*), ulong) nothrow @nogc
write
;
// O_CLOEXEC: ask the kernel to mark both pipe ends close-on-exec, the sane // default for fds we never intend to leak across an exec. Linux value is octal // 02000000, identical across the architectures this example targets. enum int
(constant) int io_uring_pipe.O_CLOEXEC = 524288
O_CLOEXEC
= 0x80000;
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; } // Fast path: if the kernel's op probe answers, trust it. A kernel too old to // know about PIPE simply reports it unsupported. 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.PIPE = cast(ubyte)62u

IORING_OP_PIPE - async pipe(2)/pipe2(2)

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

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

writefln
("SKIP: IORING_OP_PIPE unsupported on this kernel (needs Linux 6.16+)");
return 0; } // The kernel writes the read end into fds[0] and the write end into fds[1]. // We pass the buffer by pointer through `putWith` and dereference inside the // lambda so `prepPipe` binds it by reference. int[2]
(local variable) int[2] fds
fds
= [-1, -1];
(local variable) during.Uring io
io
.putWith!(
(ref SubmissionEntry e, int[2]* out_) { e.prepPipe(*out_, O_CLOEXEC); e.user_data = 1; })(&
during.Uring during.Uring.putWith!(function (ref during.io_uring.SubmissionEntry e, int[2]* out_) nothrow @nogc @safe { prepPipe(e, *out_, 524288); e.user_data = 1LU; } , int[2]*)(int[2]* __param_0) 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.
fds
);
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
;
const
(local variable) const(ulong) cookie
cookie
=
(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) ulong during.io_uring.CompletionEntry.user_data

sqe->data submission passed back

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

Move to next CompletionEntry

popFront
();
// Runtime fallback: on a kernel without the op, the CQE itself reports the // failure. Treat those as "unsupported", anything else as a real error. 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_PIPE rejected (errno %d) — kernel predates the op", -
(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
("PIPE completed with error: errno %d", -
(local variable) const(int) res
res
);
return 1; } if (
(local variable) const(ulong) cookie
cookie
!= 1)
{ stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("user_data mismatch: expected 1, got %d",
(local variable) const(ulong) cookie
cookie
);
return 1; } if (
(local variable) int[2] fds
fds
[0] < 0 ||
(local variable) int[2] fds
fds
[1] < 0)
{ stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("PIPE did not fill the fd pair: fds=[%d, %d]",
(local variable) int[2] fds
fds
[0],
(local variable) int[2] fds
fds
[1]);
return 1; } // Prove the freshly minted pipe works: a short round-trip through it. scope (exit) {
int core.sys.posix.unistd.close(int) nothrow @nogc @trusted
close
(
(local variable) int[2] fds
fds
[0]);
int core.sys.posix.unistd.close(int) nothrow @nogc @trusted
close
(
(local variable) int[2] fds
fds
[1]); }
ubyte[8]
(local variable) ubyte[8] tx
tx
= [0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03, 0x04];
ubyte[8]
(local variable) ubyte[8] rx
rx
= 0;
const
(local variable) const(long) wrote
wrote
=
long core.sys.posix.unistd.write(int, scope const(void*), ulong) nothrow @nogc
write
(
(local variable) int[2] fds
fds
[1], &
(local variable) ubyte[8] tx
tx
[0],
(local variable) ubyte[8] tx
tx
.
(constant) ulong ubyte[8].length = 8LU
length
);
if (
(local variable) const(long) wrote
wrote
!=
(local variable) ubyte[8] tx
tx
.
(constant) ulong ubyte[8].length = 8LU
length
)
{ stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("write to pipe failed: returned %d (errno-side)",
(local variable) const(long) wrote
wrote
);
return 1; } const
(local variable) const(long) got
got
=
long core.sys.posix.unistd.read(int, void*, ulong) nothrow @nogc
read
(
(local variable) int[2] fds
fds
[0], &
(local variable) ubyte[8] rx
rx
[0],
(local variable) ubyte[8] rx
rx
.
(constant) ulong ubyte[8].length = 8LU
length
);
if (
(local variable) const(long) got
got
!=
(local variable) ubyte[8] tx
tx
.
(constant) ulong ubyte[8].length = 8LU
length
)
{ stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("read from pipe short: returned %d, expected %d",
(local variable) const(long) got
got
,
(local variable) ubyte[8] tx
tx
.
(constant) ulong ubyte[8].length = 8LU
length
);
return 1; } if (
(local variable) ubyte[8] rx
rx
[] !=
(local variable) ubyte[8] tx
tx
[])
{ stderr.
std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @system
writefln
("pipe round-trip mismatch: tx=%(%02X %), rx=%(%02X %)",
(local variable) ubyte[8] tx
tx
[],
(local variable) ubyte[8] rx
rx
[]);
return 1; }
void std.stdio.writefln!(char, int, int, const(long))(in char[] fmt, int __param_1, int __param_2, const(long) __param_3) @safe

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

writefln
("ok: PIPE created fds=[%d, %d] through the ring; %d bytes round-tripped",
(local variable) int[2] fds
fds
[0],
(local variable) int[2] fds
fds
[1],
(local variable) const(long) got
got
);
return 0; }