#!/usr/bin/env dub
/+ dub.sdl:
name "io_uring_send_zc"
dependency "during" version="~>0.5.0"
platforms "linux"
targetPath "build"
+/
/**
* `io_uring` — zero-copy send (`IORING_OP_SEND_ZC`, Linux 6.0).
*
* Ordinary `IORING_OP_SEND` copies the payload into the socket buffer before
* the CQE arrives, so the user buffer is free to reuse immediately. Zero-copy
* send instead pins the user pages and lets the NIC/loopback DMA straight from
* them — which means the buffer must stay alive until the *kernel* is done with
* it, not just until the send is "issued". `io_uring` signals this with a
* distinctive **two-CQE pattern** for a single `SEND_ZC` SQE:
*
* 1. a transfer-result CQE carrying the byte count, flagged `CQEFlags.MORE`
* ("more completions for this user_data are coming"); and
* 2. a later notification CQE flagged `CQEFlags.NOTIF` — emitted once the
* kernel has released the buffer, so it is now safe to reuse/free.
*
* Passing `IORING_SEND_ZC_REPORT_USAGE` additionally makes the kernel report,
* in the notification CQE's `res`, whether the path was truly zero-copy or it
* had to fall back to a copy (`IORING_NOTIF_USAGE_ZC_COPIED`). On loopback the
* kernel often copies — that is expected and still a successful demonstration
* of the API and its completion sequence.
*
* This example connects a client socket to a listening server, both on
* 127.0.0.1, sends a payload from the client with `SEND_ZC`, asserts the
* MORE-then-NOTIF CQE sequence, and reads the bytes back on the server end to
* confirm the transfer.
*
* Companion to the io_uring chronology:
* see docs/research/async-io/io-uring/timeline.md
* § "6.0 — Zero-copy send, single-issuer, sync cancel (October 2022)".
*
* Run with: `dub run --single send-zc.d`
*
* Portability: if `io_uring` is unavailable (old kernel / sandbox) or the
* kernel predates `SEND_ZC` (< 6.0), the program prints a `SKIP:` line and
* exits 0 so it stays green in CI regardless of the host kernel.
*/
module (module) io_uring_send_zcio_uring — zero-copy send (IORING_OP_SEND_ZC, Linux 6.0).
Ordinary IORING_OP_SEND copies the payload into the socket buffer before
the CQE arrives, so the user buffer is free to reuse immediately. Zero-copy
send instead pins the user pages and lets the NIC/loopback DMA straight from
them — which means the buffer must stay alive until the kernel is done with
it, not just until the send is "issued". io_uring signals this with a
distinctive two-CQE pattern for a single SEND_ZC SQE:
a transfer-result CQE carrying the byte count, flagged CQEFlags.MORE
("more completions for this user_data are coming"); and
a later notification CQE flagged CQEFlags.NOTIF — emitted once the
kernel has released the buffer, so it is now safe to reuse/free.
Passing IORING_SEND_ZC_REPORT_USAGE additionally makes the kernel report,
in the notification CQE's res, whether the path was truly zero-copy or it
had to fall back to a copy (IORING_NOTIF_USAGE_ZC_COPIED). On loopback the
kernel often copies — that is expected and still a successful demonstration
of the API and its completion sequence.
This example connects a client socket to a listening server, both on
127.0.0.1, sends a payload from the client with SEND_ZC, asserts the
MORE-then-NOTIF CQE sequence, and reads the bytes back on the server end to
confirm the transfer.
Companion to the io_uring chronology:
see docs/research/async-io/io-uring/timeline.md
§ "6.0 — Zero-copy send, single-issuer, sync cancel (October 2022)".
Run with: dub run --single send-zc.d
Portability
if io_uring is unavailable (old kernel / sandbox) or the
kernel predates SEND_ZC (< 6.0), the program prints a SKIP: line and
exits 0 so it stays green in CI regardless of the host kernel.
io_uring_send_zc;
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.errnoD header file for GNU/Linux
errno : (alias constant) io_uring_send_zc.EINVAL = int core.stdc.errno.EINVAL = 22EINVAL, (alias constant) io_uring_send_zc.EOPNOTSUPP = int core.stdc.errno.EOPNOTSUPP = 95EOPNOTSUPP, (alias constant) io_uring_send_zc.ENOSYS = int core.stdc.errno.ENOSYS = 38ENOSYS;
import (package) corecore.(package) core.syssys.(package) core.sys.posixposix.(package) core.sys.posix.arpaarpa.(module) core.sys.posix.arpa.inetD header file for POSIX.
inet : (alias) io_uring_send_zc.htonl = uint core.sys.posix.arpa.inet.htonl(uint) pure nothrow @nogc @trustedhtonl;
import (package) corecore.(package) core.syssys.(package) core.sys.posixposix.(package) core.sys.posix.netinetnetinet.(module) core.sys.posix.netinet.in_D header file for POSIX.
in_;
import (package) corecore.(package) core.syssys.(package) core.sys.posixposix.(package) core.sys.posix.syssys.(module) core.sys.posix.sys.socketD header file for POSIX.
socket;
import (package) corecore.(package) core.syssys.(package) core.sys.posixposix.(module) core.sys.posix.unistdD header file for POSIX.
unistd : (alias) io_uring_send_zc.close = int core.sys.posix.unistd.close(int) nothrow @nogc @trustedclose, (alias) io_uring_send_zc.read = long core.sys.posix.unistd.read(int, void*, ulong) nothrow @nogcread;
import (package) stdstd.(module) std.rangeThis module defines the notion of a range. Ranges generalize the concept of
arrays, lists, or anything that involves sequential access. This abstraction
enables the same set of algorithms (see std.algorithm) to be used
with a vast variety of different concrete types. For example,
a linear search algorithm such as find
works not just for arrays, but for linked-lists, input files,
incoming network data, etc.
Guides
There are many articles available that can bolster understanding ranges:
Ali Çehreli's tutorial on ranges
for the basics of working with and creating range-based code.
Jonathan M. Davis Introduction to Ranges
talk at DConf 2015 a vivid introduction from its core constructs to practical advice.
The DLang Tour's chapter on ranges
for an interactive introduction.
H. S. Teoh's tutorial on
component programming with ranges for a real-world showcase of the influence
of range-based programming on complex algorithms.
Andrei Alexandrescu's article
On Iteration for conceptual aspect of ranges and the motivation
Submodules
This module has two submodules:
The std.range.primitives submodule
provides basic range functionality. It defines several templates for testing
whether a given object is a range, what kind of range it is, and provides
some common range operations.
The std.range.interfaces submodule
provides object-based interfaces for working with ranges via runtime
polymorphism.
The remainder of this module provides a rich set of range creation and
composition templates that let you construct new ranges out of existing ranges:
| chain |
Concatenates several ranges into a single range.
|
| choose |
Chooses one of two ranges at runtime based on a boolean condition.
|
| chooseAmong |
Chooses one of several ranges at runtime based on an index.
|
| chunks |
Creates a range that returns fixed-size chunks of the original
range.
|
| cycle |
Creates an infinite range that repeats the given forward range
indefinitely. Good for implementing circular buffers.
|
| drop |
Creates the range that results from discarding the first n
elements from the given range.
|
| dropBack |
Creates the range that results from discarding the last n
elements from the given range.
|
| dropExactly |
Creates the range that results from discarding exactly n
of the first elements from the given range.
|
| dropBackExactly |
Creates the range that results from discarding exactly n
of the last elements from the given range.
|
| dropOne |
Creates the range that results from discarding
the first element from the given range.
|
| dropBackOne |
Creates the range that results from discarding
the last element from the given range.
|
| enumerate |
Iterates a range with an attached index variable.
|
| evenChunks |
Creates a range that returns a number of chunks of
approximately equal length from the original range.
|
| frontTransversal |
Creates a range that iterates over the first elements of the
given ranges.
|
| generate |
Creates a range by successive calls to a given function. This
allows to create ranges as a single delegate.
|
| indexed |
Creates a range that offers a view of a given range as though
its elements were reordered according to a given range of indices.
|
| iota |
Creates a range consisting of numbers between a starting point
and ending point, spaced apart by a given interval.
|
| lockstep |
Iterates n ranges in lockstep, for use in a foreach
loop. Similar to zip, except that lockstep is designed
especially for foreach loops.
|
| nullSink |
An output range that discards the data it receives.
|
| only |
Creates a range that iterates over the given arguments.
|
| padLeft |
Pads a range to a specified length by adding a given element to
the front of the range. Is lazy if the range has a known length.
|
| padRight |
Lazily pads a range to a specified length by adding a given element to
the back of the range.
|
| radial |
Given a random-access range and a starting point, creates a
range that alternately returns the next left and next right element to
the starting point.
|
| recurrence |
Creates a forward range whose values are defined by a
mathematical recurrence relation.
|
| refRange |
Pass a range by reference. Both the original range and the RefRange
will always have the exact same elements.
Any operation done on one will affect the other.
|
| repeat |
Creates a range that consists of a single element repeated n
times, or an infinite range repeating that element indefinitely.
|
| retro |
Iterates a bidirectional range backwards.
|
| roundRobin |
Given n ranges, creates a new range that return the n
first elements of each range, in turn, then the second element of each
range, and so on, in a round-robin fashion.
|
| sequence |
Similar to recurrence, except that a random-access range is
created.
|
| slide |
Creates a range that returns a fixed-size sliding window
over the original range. Unlike chunks,
it advances a configurable number of items at a time,
not one chunk at a time.
|
| stride |
Iterates a range with stride n.
|
| tail |
Return a range advanced to within n elements of the end of
the given range.
|
| take |
Creates a sub-range consisting of only up to the first n
elements of the given range.
|
| takeExactly |
Like take, but assumes the given range actually has n
elements, and therefore also defines the length property.
|
| takeNone |
Creates a random-access range consisting of zero elements of the
given range.
|
| takeOne |
Creates a random-access range consisting of exactly the first
element of the given range.
|
| tee |
Creates a range that wraps a given range, forwarding along
its elements while also calling a provided function with each element.
|
| transposed |
Transposes a range of ranges.
|
| transversal |
Creates a range that iterates over the n'th elements of the
given random-access ranges.
|
| zip |
Given n ranges, creates a range that successively returns a
tuple of all the first elements, a tuple of all the second elements,
etc.
|
Sortedness
Ranges whose elements are sorted afford better efficiency with certain
operations. For this, the assumeSorted function can be used to
construct a SortedRange from a pre-sorted range. The sort function also conveniently
returns a SortedRange. SortedRange objects provide some additional
range operations that take advantage of the fact that the range is sorted.
Source
std/range/package.d
range : (alias template) io_uring_send_zc.iota = std.range.iota(B, E, S)(B begin, E end, S step) if ((isIntegral!(CommonType!(B, E)) || isPointer!(CommonType!(B, E))) && isIntegral!S)Creates a range of values that span the given starting and stopping
values.
Example
void main()
{
import std.stdio;
// The following groups all produce the same output of:
// 0 1 2 3 4
foreach (i; 0 .. 5)
writef("%s ", i);
writeln();
import std.range : iota;
foreach (i; iota(0, 5))
writef("%s ", i);
writeln();
writefln("%(%s %|%)", iota(0, 5));
import std.algorithm.iteration : map;
import std.algorithm.mutation : copy;
import std.format;
iota(0, 5).map!(i => format("%s ", i)).copy(stdout.lockingTextWriter());
writeln();
}
iota;
import (package) stdstd.(module) std.algorithmThis package implements generic algorithms oriented towards the processing of
sequences. Sequences processed by these functions define range-based
interfaces. See also Reference on ranges and
tutorial on ranges.
Algorithms are categorized into the following submodules:
Submodule Functions
| Searching |
all
any
balancedParens
boyerMooreFinder
canFind
commonPrefix
count
countUntil
endsWith
find
findAdjacent
findAmong
findSkip
findSplit
findSplitAfter
findSplitBefore
minCount
maxCount
minElement
maxElement
minIndex
maxIndex
minPos
maxPos
skipOver
startsWith
until
|
| Comparison |
among
castSwitch
clamp
cmp
either
equal
isPermutation
isSameLength
levenshteinDistance
levenshteinDistanceAndPath
max
min
mismatch
predSwitch
|
| Iteration |
cache
cacheBidirectional
chunkBy
cumulativeFold
each
filter
filterBidirectional
fold
group
joiner
map
mean
permutations
reduce
splitWhen
splitter
substitute
sum
uniq
|
| Sorting |
completeSort
isPartitioned
isSorted
isStrictlyMonotonic
ordered
strictlyOrdered
makeIndex
merge
multiSort
nextEvenPermutation
nextPermutation
nthPermutation
partialSort
partition
partition3
schwartzSort
sort
topN
topNCopy
topNIndex
|
| Set operations (setops) |
cartesianProduct
largestPartialIntersection
largestPartialIntersectionWeighted
multiwayMerge
multiwayUnion
setDifference
setIntersection
setSymmetricDifference
|
| Mutation |
bringToFront
copy
fill
initializeAll
move
moveAll
moveSome
moveEmplace
moveEmplaceAll
moveEmplaceSome
remove
reverse
strip
stripLeft
stripRight
swap
swapRanges
uninitializedFill
|
Many functions in this package are parameterized with a predicate.
The predicate may be any suitable callable type
(a function, a delegate, a functor, or a lambda), or a
compile-time string. The string may consist of any legal D
expression that uses the symbol a (for unary functions) or the
symbols a and b (for binary functions). These names will NOT
interfere with other homonym symbols in user code because they are
evaluated in a different context. The default for all binary
comparison predicates is "a == b" for unordered operations and
"a < b" for ordered operations.
Example
int[] a = ...;
static bool greater(int a, int b)
{
return a > b;
}
sort!greater(a); // predicate as alias
sort!((a, b) => a > b)(a); // predicate as a lambda.
sort!"a > b"(a); // predicate as string
// (no ambiguity with array name)
sort(a); // no predicate, "a < b" is implicit
Source
std/algorithm/package.d
algorithm : (alias template) io_uring_send_zc.copy = std.algorithm.mutation.copy(SourceRange, TargetRange)(SourceRange source, TargetRange target) if (isInputRange!SourceRange && isOutputRange!(TargetRange, ElementType!SourceRange))Copies the content of source into target and returns the
remaining (unfilled) part of target.
Preconditions
target shall have enough room to accommodate
the entirety of source.
copy, (alias template) io_uring_send_zc.equal = std.algorithm.comparison.equal(alias pred = "a == b")Compares two or more ranges for equality, as defined by predicate pred
(which is == by default).
equal, (alias template) io_uring_send_zc.map = std.algorithm.iteration.map(fun...) if (fun.length >= 1)Implements the homonym function (also known as transform) present
in many languages of functional flavor. The call ``map!(fun)(range)
returns a range of which elements are obtained by applying fun(a)
left to right for all elements a in range. The original ranges are
not changed. Evaluation is done lazily.
map;
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_send_zc.writefln = std.stdio.writefln(alias fmt, A...)(A args) if (isSomeString!(typeof(fmt)))Equivalent to writef(fmt, args, '\n').
writefln, stderr;
enum (constant) int io_uring_send_zc.N = 4096N = 4096;
int int D main()main()
{
// --- Loopback TCP pair, all via libc (the ring only does the SEND) ------
int (local variable) int srvsrv = int core.sys.posix.sys.socket.socket(int, int, int) nothrow @nogc @safesocket((enum value) core.sys.posix.sys.socket.AF_INET = 2AF_INET, (enum value) core.sys.posix.sys.socket.SOCK_STREAM = 1SOCK_STREAM, 0);
if ((local variable) int srvsrv < 0) { stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("socket(srv) failed"); return 1; }
scope (exit) int core.sys.posix.unistd.close(int) nothrow @nogc @trustedclose((local variable) int srvsrv);
int (local variable) int oneone = 1;
int core.sys.posix.sys.socket.setsockopt(int, int, int, scope const(void*), uint) nothrow @nogcsetsockopt((local variable) int srvsrv, (enum value) core.sys.posix.sys.socket.SOL_SOCKET = 1SOL_SOCKET, (enum value) core.sys.posix.sys.socket.SO_REUSEADDR = 2SO_REUSEADDR, &(local variable) int oneone, (local variable) int oneone.(constant) ulong int.sizeof = 4LUsizeof);
(struct) core.sys.posix.netinet.in_.sockaddr_insockaddr_in (local variable) core.sys.posix.netinet.in_.sockaddr_in saddrsaddr;
(local variable) core.sys.posix.netinet.in_.sockaddr_in saddrsaddr.(field) ushort core.sys.posix.netinet.in_.sockaddr_in.sin_familysin_family = (enum value) core.sys.posix.sys.socket.AF_INET = 2AF_INET;
(local variable) core.sys.posix.netinet.in_.sockaddr_in saddrsaddr.(field) ushort core.sys.posix.netinet.in_.sockaddr_in.sin_portsin_port = 0; // let the kernel pick a free port
(local variable) core.sys.posix.netinet.in_.sockaddr_in saddrsaddr.(field) core.sys.posix.arpa.inet.in_addr core.sys.posix.netinet.in_.sockaddr_in.sin_addrsin_addr.(field) uint core.sys.posix.arpa.inet.in_addr.s_addrs_addr = uint core.sys.posix.arpa.inet.htonl(uint) pure nothrow @nogc @trustedhtonl(0x7f000001); // 127.0.0.1
if (int core.sys.posix.sys.socket.bind(int, scope const(core.sys.posix.sys.socket.sockaddr*), uint) nothrow @nogcbind((local variable) int srvsrv, cast((struct) core.sys.posix.sys.socket.sockaddrsockaddr*) &(local variable) core.sys.posix.netinet.in_.sockaddr_in saddrsaddr, (local variable) core.sys.posix.netinet.in_.sockaddr_in saddrsaddr.(constant) ulong core.sys.posix.netinet.in_.sockaddr_in.sizeof = 16LUsizeof) != 0) { stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("bind failed"); return 1; }
if (int core.sys.posix.sys.socket.listen(int, int) nothrow @nogc @safelisten((local variable) int srvsrv, 1) != 0) { stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("listen failed"); return 1; }
// Read back the port the kernel assigned so the client can connect to it.
(struct) core.sys.posix.netinet.in_.sockaddr_insockaddr_in (local variable) core.sys.posix.netinet.in_.sockaddr_in actualactual;
(alias) core.sys.posix.sys.socket.socklen_t = uintsocklen_t (local variable) uint alenalen = (local variable) core.sys.posix.netinet.in_.sockaddr_in actualactual.(constant) ulong core.sys.posix.netinet.in_.sockaddr_in.sizeof = 16LUsizeof;
if (int core.sys.posix.sys.socket.getsockname(int, scope core.sys.posix.sys.socket.sockaddr*, scope uint*) nothrow @nogcgetsockname((local variable) int srvsrv, cast((struct) core.sys.posix.sys.socket.sockaddrsockaddr*) &(local variable) core.sys.posix.netinet.in_.sockaddr_in actualactual, &(local variable) uint alenalen) != 0) { stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("getsockname failed"); return 1; }
int (local variable) int clicli = int core.sys.posix.sys.socket.socket(int, int, int) nothrow @nogc @safesocket((enum value) core.sys.posix.sys.socket.AF_INET = 2AF_INET, (enum value) core.sys.posix.sys.socket.SOCK_STREAM = 1SOCK_STREAM, 0);
if ((local variable) int clicli < 0) { stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("socket(cli) failed"); return 1; }
scope (exit) int core.sys.posix.unistd.close(int) nothrow @nogc @trustedclose((local variable) int clicli);
if (int core.sys.posix.sys.socket.connect(int, scope const(core.sys.posix.sys.socket.sockaddr*), uint) nothrow @nogcconnect((local variable) int clicli, cast((struct) core.sys.posix.sys.socket.sockaddrsockaddr*) &(local variable) core.sys.posix.netinet.in_.sockaddr_in actualactual, (local variable) uint alenalen) != 0) { stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("connect failed"); return 1; }
int (local variable) int accacc = int core.sys.posix.sys.socket.accept(int, scope core.sys.posix.sys.socket.sockaddr*, scope uint*) nothrow @nogcaccept((local variable) int srvsrv, null, null);
if ((local variable) int accacc < 0) { stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("accept failed"); return 1; }
scope (exit) int core.sys.posix.unistd.close(int) nothrow @nogc @trustedclose((local variable) int accacc);
// --- io_uring setup -----------------------------------------------------
(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 first: SEND_ZC arrived in 6.0. On older kernels the op is unknown,
// so we SKIP cleanly rather than submitting an SQE the kernel can't decode.
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.SEND_ZC = cast(ubyte)47uIORING_OP_SEND_ZC - zero-copy send
SEND_ZC))
{
void std.stdio.writefln!char(in char[] fmt) @safeEquivalent to writef(fmt, args, '\n').
writefln("SKIP: IORING_OP_SEND_ZC unsupported (kernel < 6.0)");
return 0;
}
// Deterministic payload: bytes 0,1,2,...,255,0,1,... so we can verify it
// round-trips intact on the receiving end.
ubyte[(constant) int io_uring_send_zc.N = 4096N] (local variable) ubyte[4096] payloadpayload;
std.range.iota!(int, int).Result std.range.iota!(int, int)(int begin, int end) pure nothrow @nogc @safeCreates a range of values that span the given starting and stopping
values.
Example
void main()
{
import std.stdio;
// The following groups all produce the same output of:
// 0 1 2 3 4
foreach (i; 0 .. 5)
writef("%s ", i);
writeln();
import std.range : iota;
foreach (i; iota(0, 5))
writef("%s ", i);
writeln();
writefln("%(%s %|%)", iota(0, 5));
import std.algorithm.iteration : map;
import std.algorithm.mutation : copy;
import std.format;
iota(0, 5).map!(i => format("%s ", i)).copy(stdout.lockingTextWriter());
writeln();
}
iota(0, (constant) int io_uring_send_zc.N = 4096N).io_uring_send_zc.main.MapResult!(__lambda_L112_C21, Result) io_uring_send_zc.main.map!(std.range.iota!(int, int).Result)(std.range.iota!(int, int).Result r) pure nothrow @nogc @safeImplements the homonym function (also known as transform) present
in many languages of functional flavor. The call ``map!(fun)(range)
returns a range of which elements are obtained by applying fun(a)
left to right for all elements a in range. The original ranges are
not changed. Evaluation is done lazily.
Examples
import std.algorithm.comparison : equal;
import std.range : chain, only;
auto squares =
chain(only(1, 2, 3, 4), only(5, 6)).map!(a => a * a);
assert(equal(squares, only(1, 4, 9, 16, 25, 36)));
Multiple functions can be passed to map. In that case, the
element type of map is a tuple containing one element for each
function.
auto sums = [2, 4, 6, 8];
auto products = [1, 4, 9, 16];
size_t i = 0;
foreach (result; [ 1, 2, 3, 4 ].map!("a + a", "a * a"))
{
assert(result[0] == sums[i]);
assert(result[1] == products[i]);
++i;
}
You may alias map with some function(s) to a symbol and use
it separately:
import std.algorithm.comparison : equal;
import std.conv : to;
alias stringize = map!(to!string);
assert(equal(stringize([ 1, 2, 3, 4 ]), [ "1", "2", "3", "4" ]));
map!(a => cast(ubyte)(a & 0xff)).ubyte[] std.algorithm.mutation.copy!(io_uring_send_zc.main.MapResult!(__lambda_L112_C21, Result), ubyte[])(io_uring_send_zc.main.MapResult!(__lambda_L112_C21, Result) source, ubyte[] target) pure nothrow @nogc @safeCopies the content of source into target and returns the
remaining (unfilled) part of target.
Preconditions
target shall have enough room to accommodate
the entirety of source.
Examples
int[] a = [ 1, 5 ];
int[] b = [ 9, 8 ];
int[] buf = new int[](a.length + b.length + 10);
auto rem = a.copy(buf); // copy a into buf
rem = b.copy(rem); // copy b into remainder of buf
assert(buf[0 .. a.length + b.length] == [1, 5, 9, 8]);
assert(rem.length == 10); // unused slots in buf
As long as the target range elements support assignment from source
range elements, different types of ranges are accepted:
float[] src = [ 1.0f, 5 ];
double[] dest = new double[src.length];
src.copy(dest);
To copy at most n elements from a range, you may want to use
take:
import std.range;
int[] src = [ 1, 5, 8, 9, 10 ];
auto dest = new int[](3);
src.take(dest.length).copy(dest);
assert(dest == [ 1, 5, 8 ]);
To copy just those elements from a range that satisfy a predicate,
use filter:
import std.algorithm.iteration : filter;
int[] src = [ 1, 5, 8, 9, 10, 1, 2, 0 ];
auto dest = new int[src.length];
auto rem = src
.filter!(a => (a & 1) == 1)
.copy(dest);
assert(dest[0 .. $ - rem.length] == [ 1, 5, 9, 1 ]);
retro can be used to achieve behavior similar to
STL's copy_backward':
import std.algorithm, std.range;
int[] src = [1, 2, 4];
int[] dest = [0, 0, 0, 0, 0];
src.retro.copy(dest.retro);
assert(dest == [0, 0, 1, 2, 4]);
copy((local variable) ubyte[4096] payloadpayload[]);
// Enqueue a single zero-copy send. IORING_SEND_ZC_REPORT_USAGE makes the
// notification CQE report whether the kernel managed a true zero-copy.
(local variable) during.Uring ioio.putWith!((ref SubmissionEntry e, int fd, ubyte[] buf) {
e.prepSendZc(fd, buf, MsgFlags.NONE, IORING_SEND_ZC_REPORT_USAGE);
e.user_data = 1;
})(during.Uring during.Uring.putWith!(function (ref during.io_uring.SubmissionEntry e, int fd, ubyte[] buf) nothrow @nogc @safe
{
prepSendZc(e, fd, cast(const(ubyte)[])buf, MsgFlags.NONE, 8u);
e.user_data = 1LU;
}
, int, ubyte[])(ref int __param_0, ubyte[] __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().
cli, during.Uring during.Uring.putWith!(function (ref during.io_uring.SubmissionEntry e, int fd, ubyte[] buf) nothrow @nogc @safe
{
prepSendZc(e, fd, cast(const(ubyte)[])buf, MsgFlags.NONE, 8u);
e.user_data = 1LU;
}
, int, ubyte[])(ref int __param_0, ubyte[] __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().
payload[]);
// submit(0) just flushes the SQ; we don't ask submit to wait on a count.
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(0);
if ((local variable) const(int) submittedsubmitted != 1) { stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("submit=%d (expected 1)", (local variable) const(int) submittedsubmitted); return 1; }
// --- The two-CQE dance --------------------------------------------------
// A single SEND_ZC produces two completions: the transfer result (with
// CQEFlags.MORE set) and a separate notification (CQEFlags.NOTIF) once the
// buffer is released. Consume exactly two.
int (local variable) int sendRessendRes = int.(constant) int int.min = -2147483648min;
bool (local variable) bool gotMoregotMore, (local variable) bool gotNotifgotNotif, (local variable) bool zcCopiedzcCopied;
foreach ((local variable) int __; 0 .. 2)
{
(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(during.io_uring.CompletionEntry) cqecqe = (local variable) during.Uring ioio.during.io_uring.CompletionEntry during.Uring.front() pure nothrow @nogc return ref @safeGet first CompletionEntry from cq ring
front;
scope (exit) (local variable) during.Uring ioio.void during.Uring.popFront() pure nothrow @nogc @safeMove to next CompletionEntry
popFront();
// Belt-and-suspenders: even if the probe lied, a kernel without SEND_ZC
// fails the op itself — treat that as an unsupported-feature SKIP.
if ((local variable) const(during.io_uring.CompletionEntry) cqecqe.(field) int during.io_uring.CompletionEntry.resresult code for this event
res == -(constant) int core.stdc.errno.EINVAL = 22EINVAL || (local variable) const(during.io_uring.CompletionEntry) cqecqe.(field) int during.io_uring.CompletionEntry.resresult code for this event
res == -(constant) int core.stdc.errno.EOPNOTSUPP = 95EOPNOTSUPP || (local variable) const(during.io_uring.CompletionEntry) cqecqe.(field) int during.io_uring.CompletionEntry.resresult code for this event
res == -(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: SEND_ZC rejected by kernel (res=%d) — unsupported (kernel < 6.0)", (local variable) const(during.io_uring.CompletionEntry) cqecqe.(field) int during.io_uring.CompletionEntry.resresult code for this event
res);
return 0;
}
if ((local variable) const(during.io_uring.CompletionEntry) cqecqe.(field) during.io_uring.CQEFlags during.io_uring.CompletionEntry.flagsflags & (enum) during.io_uring.CQEFlagsFlags used with CompletionEntry
CQEFlags.(enum value) during.io_uring.CQEFlags.NOTIF = 8uIORING_CQE_F_NOTIF (from Linux 6.0)
Set for notification CQEs. Used to distinguish the per-send completion
from the (later) zero-copy notification on IORING_OP_SEND_ZC /
IORING_OP_SENDMSG_ZC.
NOTIF)
{
// Notification CQE: kernel is done with the buffer. With
// REPORT_USAGE, res tells us whether it had to fall back to a copy.
(local variable) bool gotNotifgotNotif = true;
(local variable) bool zcCopiedzcCopied = (cast(uint) (local variable) const(during.io_uring.CompletionEntry) cqecqe.(field) int during.io_uring.CompletionEntry.resresult code for this event
res & (constant) uint during.io_uring.IORING_NOTIF_USAGE_ZC_COPIED = 2147483648uReported in cqe.res of an IORING_CQE_F_NOTIF CQE if IORING_SEND_ZC_REPORT_USAGE was set
on the request and the send had to fall back to a copy (at least partially). If unset, the
transfer was performed without a copy.
IORING_NOTIF_USAGE_ZC_COPIED) != 0;
}
else
{
// Transfer-result CQE: byte count + the MORE flag promising a NOTIF.
if ((local variable) const(during.io_uring.CompletionEntry) cqecqe.(field) int during.io_uring.CompletionEntry.resresult code for this event
res < 0) { stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("send failed: errno %d", -(local variable) const(during.io_uring.CompletionEntry) cqecqe.(field) int during.io_uring.CompletionEntry.resresult code for this event
res); return 1; }
(local variable) int sendRessendRes = (local variable) const(during.io_uring.CompletionEntry) cqecqe.(field) int during.io_uring.CompletionEntry.resresult code for this event
res;
(local variable) bool gotMoregotMore = ((local variable) const(during.io_uring.CompletionEntry) cqecqe.(field) during.io_uring.CQEFlags during.io_uring.CompletionEntry.flagsflags & (enum) during.io_uring.CQEFlagsFlags used with CompletionEntry
CQEFlags.(enum value) during.io_uring.CQEFlags.MORE = 2uIORING_CQE_F_MORE (from Linux 5.13)
If set, parent SQE will generate more CQE entries
MORE) != 0;
}
}
if (!(local variable) bool gotMoregotMore) { stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("transfer CQE lacked CQEFlags.MORE"); return 1; }
if (!(local variable) bool gotNotifgotNotif) { stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("missing CQEFlags.NOTIF notification CQE"); return 1; }
if ((local variable) int sendRessendRes != (constant) int io_uring_send_zc.N = 4096N) { stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("short send: %d of %d bytes", (local variable) int sendRessendRes, (constant) int io_uring_send_zc.N = 4096N); return 1; }
// Confirm the bytes actually arrived intact on the server side.
ubyte[(constant) int io_uring_send_zc.N = 4096N] (local variable) ubyte[4096] rxrx;
const (local variable) const(long) rdrd = long core.sys.posix.unistd.read(int, void*, ulong) nothrow @nogcread((local variable) int accacc, &(local variable) ubyte[4096] rxrx[0], (local variable) ubyte[4096] rxrx.(constant) ulong ubyte[4096].length = 4096LUlength);
if ((local variable) const(long) rdrd != (constant) int io_uring_send_zc.N = 4096N) { stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("short read: %d of %d bytes", cast(int) (local variable) const(long) rdrd, (constant) int io_uring_send_zc.N = 4096N); return 1; }
if (!(local variable) ubyte[4096] rxrx[].bool std.algorithm.comparison.equal!().equal!(ubyte[], ubyte[])(ubyte[] __param_0, ubyte[] __param_1) pure nothrow @nogc @safeCompares two or more ranges for equality, as defined by predicate pred
(which is == by default).
Compares two or more ranges for equality. The ranges may have
different element types, as long as all are comparable by means of
the pred.
Performs O(min(rs0.length, rs1.length, ...)) evaluations of pred. However, if
equal is invoked with the default predicate, the implementation may take the liberty
to use faster implementations that have the theoretical worst-case
O(max(rs0.length, rs1.length, ...)).
At least one of the ranges must be finite. If one range involved is infinite, the result is
(statically known to be) false.
If the ranges have different kinds of UTF code unit (char, wchar, or
dchar), then they are compared using UTF decoding to avoid
accidentally integer-promoting units.
Examples
import std.algorithm.comparison : equal;
import std.math.operations : isClose;
int[4] a = [ 1, 2, 4, 3 ];
assert(!equal(a[], a[1..$]));
assert(equal(a[], a[]));
assert(equal!((a, b) => a == b)(a[], a[]));
// different types
double[4] b = [ 1.0, 2, 4, 3];
assert(!equal(a[], b[1..$]));
assert(equal(a[], b[]));
// predicated: ensure that two vectors are approximately equal
double[4] c = [ 1.0000000005, 2, 4, 3];
assert(equal!isClose(b[], c[]));
Tip
equal can itself be used as a predicate to other functions.
This can be very useful when the element type of a range is itself a
range. In particular, equal can be its own predicate, allowing
range of range (of range...) comparisons.
import std.algorithm.comparison : equal;
import std.range : iota, chunks;
assert(equal!(equal!equal)(
[[[0, 1], [2, 3]], [[4, 5], [6, 7]]],
iota(0, 8).chunks(2).chunks(2)
));
equal((local variable) ubyte[4096] payloadpayload[])) { stderr.std.stdio.File std.stdio.makeGlobal!"core.stdc.stdio.stderr"() nothrow @nogc @property ref @systemwritefln("payload mismatch on receive"); return 1; }
void std.stdio.writefln!(char, int, string)(in char[] fmt, int __param_1, string __param_2) @safeEquivalent to writef(fmt, args, '\n').
writefln("ok: SEND_ZC sent %d bytes (transfer CQE F_MORE, then F_NOTIF; path=%s); payload round-tripped",
(local variable) int sendRessendRes, (local variable) bool zcCopiedzcCopied ? "kernel-copied" : "true zero-copy");
return 0;
}