streamio
nimony/lib/std/streamio.nim
Streams: one Reader, one Writer (nim-lang/nimony#2635).
A network connection is as unremarkable as a file. Every constructor returns a Reader, a Writer or both, and after construction the interface is the same whatever is underneath:
.. code-block:: nim var r = openReader(path"log.txt") echo r.readLine()
let (r, w) = connect("cache.local:6379", "PING\r\n") echo r.readLine() # +PONG
var w = openWriter() w.setAdapter gzipAdapter(fmtGzip) w.write data let gz = extract(w)
Pieces:
ReaderandWriterare concrete, move-only values. A pair returned by
one constructor shares a Core, a ref to the descriptor and the TLS engine if any, which closes the descriptor when the last end is gone.
- Each end has ONE adapter slot,
adapter(compression, HTTP framing, …),
set with setAdapter. Encryption is not an adapter: a TlsEngine lives in the core, because TLS is a property of the connection, not of a direction. So the write path is caller -> adapter -> TLS -> descriptor and the read path the reverse; compression below encryption cannot be written.
readanswering0is the end of the data. Everything that goes wrong
raises an ErrorCode.
- Streams close themselves. A writer's destructor finishes it (adapter
trailer, TLS close_notify, shutdown(SHUT_WR)) and swallows errors; call close to see them. abort ends a writer without the clean finish.
deadlinereads the standing I/O deadline;renewreplaces it, or clears
it with never. A connection's reader and writer share that deadline. Connection constructors take dl for setup only; the returned streams start with never, so setup does not become a connection lifetime limit.
pipeis an in-processReader/Writerpair over a ring; waiting on
it parks the task, it does not block a thread. std/channels builds typed channels on its record form, recordPipe.
Not here yet (see the issue): openReadWrite, startTls, deadlines for waits on a pipe, sendfile/splice/kTLS fast paths in copy, and cross-thread close of a descriptor another lane is parked on.
type TlsEngine = ref TlsEngine.Obj not niltype TlsEngine.Obj = objecttype Adapter = ref Adapter.Obj not niltype Adapter.Obj = objectmethod start(e: ref TlsEngine.Obj not nil; host: string)Begin the handshake for
host(queues the first flight).method recordsIn(e: ref TlsEngine.Obj not nil; src: openArray[char]; used: var int64)Bytes from the network. Reports how many were taken.
method recordsOut(e: ref TlsEngine.Obj not nil; dest: var string)Appends whatever must go to the network now.
method appIn(e: ref TlsEngine.Obj not nil; src: openArray[char]; used: var int64)Plaintext to send; its records come out of
recordsOut.method appOut(e: ref TlsEngine.Obj not nil; dest: var string)Appends the plaintext received so far.
method close(e: ref TlsEngine.Obj not nil)Queue
close_notify.method isConnected(e: ref TlsEngine.Obj not nil): boolThe handshake has completed and application data may flow.
method isPeerClosed(e: ref TlsEngine.Obj not nil): boolThe peer sent
close_notify: whatappOuthas handed out is all of it.method run(a: ref Adapter.Obj not nil; src: openArray[char]; used: var int64; dest: var string; final: bool)Transform a prefix of
src, append the output todestand report inusedhow many bytes ofsrcwere taken.finalmeans no more input will follow: write the trailer, or check that the stream really ended.method attach(a: ref Adapter.Obj not nil; reading: bool)Called by
setAdapterwith the direction the adapter now serves. An adapter that only works one way raises for the other.method hasMore(a: ref Adapter.Obj not nil): boolFalse for a read adapter whose stream has ended on its own — an HTTP body after
Content-Lengthbytes or the last chunk — although the source has not. The reader then returns0, and the bytes below stay where they are.func dollar`.CoreKind(e: CoreKind): stringtype Reader = object core: ref CoreObj buf: string pos: int64 raw: string adapter: ref Adapter.Obj eof: bool step: int64
type Writer = object core: ref CoreObj buf: string adapter: ref Adapter.Obj finished: bool
proc =destroy(r: Reader)proc =wasMoved(r: var Reader)proc =dup(x: Reader): Readerproc =copy(dest: var Reader; src: Reader)proc =destroy(w: Writer)proc =wasMoved(w: var Writer)proc =dup(x: Writer): Writerproc =copy(dest: var Writer; src: Writer)proc deadline(r: Reader): DeadlineThe standing deadline for I/O on the underlying descriptor. Bytes already buffered and in-memory streams do not wait on it. Raises
BadOperationfor a closed or default-constructed reader.proc deadline(w: Writer): DeadlineThe writer's standing I/O deadline, shared with its paired reader. Raises
BadOperationfor a closed or default-constructed writer.proc renew(r: var Reader; deadline: Deadline)Replace the standing I/O deadline;
neverclears it. A paired writer observes the same deadline. RaisesBadOperationif the reader is closed or default-constructed. This does not resume a failed partial write.proc renew(w: var Writer; deadline: Deadline)Replace the shared I/O deadline through the writer. Raises
BadOperationif the writer is closed or default-constructed.proc buffered(r: Reader): int64Decoded bytes not yet consumed.
proc peek(r: Reader): openArray[char]A view of the decoded bytes not yet consumed. No I/O or copying. Reader mutations invalidate the view.
proc consume`body(r`b: var Reader; n`b: int64)Drop the first
nbytes ofpeek, without I/O.proc consume(r: var Reader; n: int64)Drop the first
nbytes ofpeek, without I/O.proc fill(r: var Reader): int64Append more decoded bytes, preserving the unread tail and compacting consumed bytes first. Returns the number added, or
0at source or adapter end even if unread bytes remain. Raises whatreadraises.Unlike
read, this can add bytes while others are still buffered. The caller must consume them or bound how much it allows to accumulate.proc read(r: var Reader; dest: var openArray[char]): int64Up to
dest.lenbytes;0is the end of the data.proc readLine(r: var Reader): stringOne line without its
\nand a trailing\r. At the end of the data the last, unterminated line is returned, then"".proc readAll(r: var Reader): stringEverything up to the end of the data.
proc close(r: var Reader)Optional: the destructor does the same.
proc write(w: var Writer; data: openArray[char])proc write(w: var Writer; data: string)proc flush(w: var Writer)Everything written so far goes down (and, for a connection, out).
proc scratch(w: var Writer; n: int64): var stringThe writer's own buffer, resized to
nbytes for a caller that serializes a head or a frame in place beforeflush. Nothing is copied, and nothing is allocated once the buffer has grown ton.Only for a writer with nothing buffered: the scratch is the buffer, so bytes the caller had written and not flushed would be replaced. Raises
BadOperationwhen it is closed, finished or has pending bytes. The caller has to fill allnbytes.proc grow(w: var Writer; n: int64): var openArray[char]nmore bytes at the end of the writer's buffer, for a caller that serializes a head or a frame in place. Nothing is copied, and nothing is allocated once the buffer has grown; the caller has to fill allnbytes.scratchfor a writer that already has bytes buffered: a run of small frames is spelled one after the other into the same buffer and reaches the kernel with oneflush.proc close(w: var Writer)Optional: finishes the stream — adapter trailer, TLS
close_notify, end of data for the peer — and raises if any of it fails, which the destructor cannot.proc abort(w: var Writer)Ends the writer WITHOUT a clean finish: no adapter trailer, no
close_notify. For error paths, where a clean finish would hand the peer a complete-looking, truncated stream.proc extract(w: sink Writer): stringThe bytes of an in-memory writer (
openWriter(sizeHint)), finished: adapter trailers included.proc setAdapter(r: var Reader; a: ref Adapter.Obj)Fills (or with
nilempties) the reader's adapter slot. Bytes read from below but not yet handed out become the new adapter's input. Replacing a adapter that has not reached its end raises: its data would be lost.proc setAdapter(w: var Writer; a: ref Adapter.Obj)Fills (or with
nilempties) the writer's adapter slot. Bytes written before go out first, through the old adapter, which writes its trailer.type GzipAdapter = ref GzipAdapter.Obj not niltype GzipAdapter.Obj = object fmt: Format reading: bool enc: Encoder dec: Decoder done: bool
proc gzipAdapter(fmt: Format; level: int64): ref GzipAdapter.Obj not nilfmtDetect(gzip or zlib, recognised by header) is for reading only.method attach(a: ref GzipAdapter.Obj not nil; reading: bool)method run(a: ref GzipAdapter.Obj not nil; src: openArray[char]; used: var int64; dest: var string; final: bool)method hasMore(a: ref GzipAdapter.Obj not nil): boolproc openReader(p: Path): ReaderThe file at
p.proc openReader(s: sink string): ReaderThe bytes of
s.proc openWriter(p: Path): WriterThe file at
p, truncated.proc openAppending(p: Path): WriterThe file at
p, written at its end.proc openWriter(sizeHint: int64): WriterAn in-memory writer;
extractgives its bytes back.proc openStreams(fd: int32; dl: Deadline): tuple[Reader, Writer]The streams over an already connected, non-blocking socket, without TLS: the server-side counterpart of
connect, for a caller with a listener of its own (std/httpserver). No I/O, so it can be called outside a passive chain;dlis the standing deadline. The core ownsfdfrom here on.proc connect(address: string; tls: ref TlsEngine.Obj; dl: Deadline): tuple[Reader, Writer]A connection to
address(host:port); TLS when an engine is given. For a protocol where the peer speaks first (SMTP, IMAP): the next step is reading its greeting.dlbounds DNS, TCP and TLS setup together; the returned streams have no standing deadline. RaisesTimeoutErroron expiry.proc connect(address: string; request: string; tls: ref TlsEngine.Obj; dl: Deadline): tuple[Reader, Writer]For a protocol where we speak first (HTTP, Redis):
requestis written and FLUSHED before this returns, so the next step is reading the reply — the forgotten flush that hangs both sides cannot be written. Onedlcovers DNS, TCP, TLS and that flush; the returned streams start withnever. A failed request write aborts rather than cleanly finishing.proc connectReader(address: string; tls: ref TlsEngine.Obj; dl: Deadline): ReaderOnly the receiving end, e.g. a feed. The sending direction is left alone (not shut down) and closes with the reader.
dlbounds construction only; userenewto set a deadline for later I/O.proc connectWriter(address: string; tls: ref TlsEngine.Obj; dl: Deadline): WriterOnly the sending end, e.g. shipping logs.
dlbounds construction only; userenewto set a deadline for later I/O.proc detachWriter(w: var Writer)Drops a writer WITHOUT ending its direction: no adapter trailer, no
close_notify, noshutdown. For a protocol layer that has written its request and keeps only the reader (std/httpclient), where ending the direction would make many servers drop the connection.proc copy(dest: var Writer; src: var Reader): int64Moves everything
srchas left intodestand returns how many bytes;deststays open. Straight from the reader's buffer into the writer.proc pipe(capacity: int64): tuple[Reader, Writer]An in-process byte pipe of
capacitybytes, e.g. a body produced by another task. The writer has no buffer: what is written is readable at once.readparks while the pipe is empty and returns0once the writer is gone;writeparks while it is full and raisesBadDescriptoronce the reader is gone.proc recordPipe(capacity: int64; recSize: int64; drop: proc (p: pointer not nil) {.nimcall.}): tuple[Reader, Writer]A
pipeforcapacityrecords ofrecSizebytes each.dropdestroys a record that is never read; nil for plain bytes.proc hasRecordRing(w: Writer; recSize: int64): boolwis arecordPipewriter forrecSizewith nothing buffered and no adapter, sowriteRecordmay bypass it.proc hasRecordRing(r: Reader; recSize: int64): boolLike the writer's:
readRecordmay bypassr.proc writeRecord(w: Writer; p: pointer not nil): boolOne record from
pstraight into the ring, waiting for room; only afterhasRecordRing. False when the reader is gone. Safe from several threads at once.proc readRecord(r: Reader; p: pointer not nil): boolOne record from the ring into
p, waiting for it; only afterhasRecordRing. False once the writer is gone and the ring is empty. Safe from several threads at once.proc tryReadRecord(r: Reader; p: pointer not nil): boolOne record from the ring into
pif one is already buffered;falsewhen there is none, or the writer is gone. Never waits, so a task can drain what is there and then do something else with it — put a batch of frames on a socket with one flush, for one. Only afterhasRecordRing, and safe from several threads at once.proc isOpen(r: Reader): boolNeither closed nor default-constructed.
proc isOpen(w: Writer): boolNeither closed, finished nor default-constructed.
proc isPipe(w: Writer): boolwwrites into apipeor arecordPipe.