Address review comments; Add documentation; Shared buffering mechanism for input and output streams

This commit is contained in:
Zahary Karadjov 2020-04-29 21:19:18 +03:00
commit b24300bd3f
No known key found for this signature in database
GPG key ID: C8936F8A3073D609
24 changed files with 2396 additions and 1060 deletions

View file

@ -1,18 +1,29 @@
const
faststreams_async_backend {.strdefine.} = "chronos"
when faststreams_async_backend == "chronos":
import chronos # import chronos/[asyncfutures2, asyncmacro2]
export chronos # export asyncfutures2, asyncmacro2
type
CloseBehavior* = enum
waitAsyncClose
dontWaitAsyncClose
template faststreamsAwait*(f: Future): untyped =
when faststreams_async_backend == "chronos":
import
chronos
export
chronos
template fsAwait*(f: Future): untyped =
await f
elif faststreams_async_backend in ["std", "asyncdispatch"]:
import std/[asyncfutures, asyncmacro]
export asyncfutures, asyncmacro
import
std/[asyncfutures, asyncmacro]
export
asyncfutures, asyncmacro
template faststreamsAwait*(awaited: Future[T]): untyped =
template fsAwait*(awaited: Future[T]): untyped =
# TODO revisit after https://github.com/nim-lang/Nim/pull/12085/ is merged
let f = awaited
yield f
@ -23,9 +34,15 @@ elif faststreams_async_backend in ["std", "asyncdispatch"]:
else:
{.fatal: "Unrecognized network backend: " & faststreams_async_backend.}
template raiseFaststreamsError*(errMsg: string, body: untyped) =
template fsTranslateErrors*(errMsg: string, body: untyped) =
try:
body
except CatchableError as err:
raise newException(IOError, errMsg, err)
except Exception as err:
if err[] of Defect:
raise (ref Defect)(err)
else:
raise newException(IOError, errMsg, err)
template noAwait*(expr: untyped): untyped =
expr

226
faststreams/buffers.nim Normal file
View file

@ -0,0 +1,226 @@
import
deques,
stew/[ptrops, ranges/ptr_arith],
async_backend
type
PageKind* = enum
userPage
stringPage
mallocPage
PageSpan* = object
startAddr*, endAddr*: ptr byte
Page* = object
startOffset*: Natural
endOffset*: Natural
case kind*: PageKind
of userPage, mallocPage:
bufferStart, bufferEnd: ptr byte
of stringPage:
data*: ref string
PageRef* = ref Page
PageBuffers* = ref object
pageSize*: Natural
maxWriteSize*: Natural
backPressureLimit*: Natural
queue*: Deque[PageRef]
getters: seq[Future[void]]
putters: seq[Future[void]]
eofReached: bool
totalBytesRead*: Natural
totalBytesWritten*: Natural
const
nimPageSize* = 4096
pageMetadataSize* = offsetof(Page, data)
nimAllocatorMetadataSize* = 32
# TODO: Get this legally from the Nim allocator.
# The goal is to make perfect page-aligned allocations
# that get fast O(0) treatment.
defaultPageSize* = 4096 - (pageMetadataSize + nimAllocatorMetadataSize)
maxStackUsage* = 16384
func pageBaseAddr*(page: PageRef): ptr byte =
if page.kind == stringPage:
cast[ptr byte](addr page.data[][0])
else:
page.bufferStart
func pageStartAddr*(page: PageRef): ptr byte =
if page.kind == stringPage:
offset(cast[ptr byte](addr page.data[][0]), page.startOffset)
else:
offset(page.bufferStart, page.startOffset)
func pageEndAddr*(page: PageRef): ptr byte =
if page.kind == stringPage:
offset(cast[ptr byte](addr page.data[][0]), page.endOffset)
else:
offset(page.bufferStart, page.endOffset)
template pageChars*(page: PageRef): untyped =
let baseAddr = cast[ptr UncheckedArray[char]](pageBaseAddr(page))
toOpenArray(baseAddr, page.startOffset, page.endOffset - 1)
func span*(page: PageRef, writable: static[bool] = false): PageSpan =
if page.kind == stringPage:
let baseAddr = cast[ptr byte](addr page.data[][0])
PageSpan(startAddr: offset(baseAddr, page.startOffset),
endAddr: offset(baseAddr, when writable: page.data[].len
else: page.endOffset))
else:
PageSpan(startAddr: offset(page.bufferStart, page.startOffset),
endAddr: when writable: page.bufferEnd
else: offset(page.bufferStart, page.endOffset))
template writableSpan*(page: PageRef): PageSpan =
span(page, writable = true)
func initPageBuffers*(pageSize: Natural,
maxWriteSize = high(int)): PageBuffers =
if pageSize > 0:
return PageBuffers(pageSize: pageSize,
maxWriteSize: maxWriteSize)
template allocRef[T: not ref](x: T): ref T =
let res = new type(x)
res[] = x
res
func getWritablePage*(buffers: PageBuffers): PageRef =
# TODO: The semantics of this func are quite unusual
# I should find a more appropriate name
if buffers.queue.len == 0:
result = PageRef(kind: stringPage,
data: allocRef newString(buffers.pageSize),
endOffset: buffers.pageSize)
buffers.queue.addLast result
else:
result = buffers.queue[0]
func addWritablePage*(buffers: PageBuffers, pageSize: Natural): PageRef =
result = PageRef(kind: stringPage,
data: allocRef newString(pageSize),
endOffset: pageSize)
buffers.queue.addLast result
func addWritablePage*(buffers: PageBuffers): PageRef =
buffers.addWritablePage(buffers.pageSize)
template getWritableSpan*(buffers: PageBuffers): PageSpan =
getWritablePage(buffers).span(writable = true)
func ensureRunway*(buffers: PageBuffers, neededRunway: Natural): PageSpan =
doAssert buffers.queue.len == 0
buffers.pageSize = neededRunway
getWritableSpan(buffers)
template len*(buffers: PageBuffers): int =
buffers.queue.len
template popFirst*(buffers: PageBuffers): PageRef =
buffers.queue.popFirst
template `[]`*(buffers: PageBuffers, idx: Natural): PageRef =
buffers.queue[idx]
func splitLastPageAt*(buffers: PageBuffers, address: ptr byte) =
var
topPage = buffers.queue.peekLast
newPage = PageRef()
splitPosition = distance(topPage.pageBaseAddr, address)
newPage[] = topPage[]
topPage.endOffset = splitPosition
newPage.startOffset = splitPosition
buffers.queue.addLast newPage
func endLastPageAt*(buffers: PageBuffers, address: ptr byte) =
if buffers != nil and buffers.queue.len > 0:
var topPage = buffers.queue.peekLast
topPage.endOffset = distance(topPage.pageBaseAddr, address)
func trackPageWrite*(page: PageRef, bytesWritten: Natural) {.inline.} =
page.endOffset = page.startOffset + bytesWritten
template writeToSpan*(buffersParam: PageBuffers,
spanVarName, writeExpr: untyped) =
var
buffers = buffersParam
page = buffers.getWritablePage
spanVarName = page.writableSpan
# TODO: what if we exit with an exception here?
# Are the side-effects of `getWritablePage` above OK to keep?
let bytesWritten = writeExpr
trackPageWrite(page, bytesWritten)
if bytesWritten == 0:
buffers.eofReached = true
func nextAlignedSize*(minSize, pageSize: Natural): Natural =
# TODO: This is not perfectly accurate. Revisit later
((minSize div pageSize) + 1) * pageSize
template consumeAllPages*(buffersParam: PageBuffers,
pageAddrVar, pageLenVar, body: untyped) =
let buffers = buffersParam
doAssert buffers != nil
var recycledPage: PageRef
for page in buffers.queue:
let
pageAddrVar = page.pageStartAddr
pageLenVar = page.endOffset - page.startOffset
if page.kind == stringPage and page.data[].len == buffers.pageSize:
recycledPage = page
# TODO: what if the body throws an exception?
# Should we do anything with the remaining pages?
body
buffers.queue.clear()
if recycledPage != nil:
recycledPage.startOffset = 0
recycledPage.endOffset = 0
buffers.queue.addLast recycledPage
template wasEofReached*(buffers: PageBuffers): bool =
buffers.eofReached
# BEWARE! These templates violate the double evaluation
# safety measures in order to produce better inlined
# code. We are using a `var` type to make it harder
# to accidentally misuse them.
template len*(span: var PageSpan): Natural =
distance(span.startAddr, span.endAddr)
template atEnd*(span: var PageSpan): bool =
span.startAddr == span.endAddr
template hasRunway*(span: var PageSpan): bool =
span.startAddr != span.endAddr
template bumpPointer*(span: var PageSpan, numberOfBytes: Natural = 1) =
span.startAddr = offset(span.startAddr, numberOfBytes)
template writeByte*(span: var PageSpan, val: byte) =
span.startAddr[] = val
span.startAddr = offset(span.startAddr, 1)
template charsToBytes*(chars: openArray[char]): untyped =
bind makeOpenArray
var charsStart = unsafeAddr chars[0]
makeOpenArray(cast[ptr byte](charsStart), chars.len)

View file

@ -1,6 +1,6 @@
import
chronos,
input_stream, output_stream, multisync
inputs, outputs, buffers, multisync
export
chronos, fsMultiSync
@ -21,39 +21,43 @@ const
writeIncompleteErrMsg = "Failed to write all bytes to Chronos transport"
proc fsCloseWait(t: StreamTransport) {.async, raises: [Defect, IOError].} =
raiseFaststreamsError closingErrMsg:
fsTranslateErrors closingErrMsg:
await t.closeWait()
proc fsReadOnce(t: StreamTransport,
buffer: ptr byte, bufSize: int): Future[int] {.async, raises: [Defect, IOError].} =
raiseFaststreamsError readingErrMsg:
return t.readOnce(pointer(buffer), bufSize)
buffer: ptr byte, bufSize: int)
{.raises: [Defect, IOError], async.} =
fsTranslateErrors readingErrMsg:
buffers.writeToSpan(span):
await t.readOnce(span.startAddr, span.len)
# TODO: Use the Raising type here
let ChronosInputStreamVTable = InputStreamVTable(
readSync: proc (s: InputStream, buffer: ptr byte, bufSize: int): int
readSync: proc (s: InputStream, buffers: PageBuffers)
{.nimcall, gcsafe, raises: [IOError, Defect].} =
var cs = ChronosInputStream(s)
doAssert cs.allowWaitFor
raiseFaststreamsError readingErrMsg:
return waitFor cs.transport.readOnce(pointer(buffer), bufSize)
fsTranslateErrors readingErrMsg:
buffers.writeToSpan(span):
waitFor cs.transport.readOnce(span.startAddr, span.len)
,
readAsync: proc (s: InputStream, buffer: ptr byte, bufSize: int): Future[int]
readAsync: proc (s: InputStream, buffers: PageBuffers): Future[Natural]
{.nimcall, gcsafe, raises: [IOError, Defect].} =
ChronosInputStream(s).transport.fsReadOnce(buffer, bufSize)
ChronosInputStream(s).transport.fsReadOnce(buffers)
,
closeSync: proc (s: InputStream)
{.nimcall, gcsafe, raises: [IOError, Defect].} =
raiseFaststreamsError closingErrMsg:
fsTranslateErrors closingErrMsg:
ChronosInputStream(s).transport.close()
,
closeAsync: proc (s: InputStream, cb: CloseAsyncCallback): Future[void]
closeAsync: proc (s: InputStream): Future[void]
{.nimcall, gcsafe, raises: [IOError, Defect].} =
ChronosInputStream(s).transport.fsCloseWait()
)
func chronosInput*(s: StreamTransport,
pageSize = output_stream.defaultPageSize,
pageSize = buffers.defaultPageSize,
allowWaitFor = false): InputStreamHandle =
InputStreamHandle(s: ChronosInputStream(
vtable: vtableAddr ChronosInputStreamVTable,
@ -65,7 +69,7 @@ let ChronosOutputStreamVTable = OutputStreamVTable(
{.nimcall, gcsafe, raises: [IOError, Defect].} =
var cs = ChronosOutputStream(s)
doAssert cs.allowWaitFor
let bytesWritten = raiseFaststreamsError writingErrMsg:
let bytesWritten = fsTranslateErrors writingErrMsg:
waitFor cs.transport.write(unsafeAddr page[0], page.len)
if bytesWritten != page.len:
raise newException(IOError, writeIncompleteErrMsg)
@ -107,7 +111,7 @@ let ChronosOutputStreamVTable = OutputStreamVTable(
)
func chronosOutput*(s: StreamTransport,
pageSize = output_stream.defaultPageSize,
pageSize = buffers.defaultPageSize,
allowWaitFor = false): OutputStreamHandle =
var stream = ChronosOutputStream(
vtable: vtableAddr(SnappyStreamVTable),

View file

@ -1,305 +0,0 @@
import
memfiles, options,
stew/[ptrops, ranges/ptr_arith],
async_backend
type
InputStream* = ref object of RootObj
vtable*: ptr InputStreamVTable
head*: ptr byte
pageSize*: int
bufferSize: int
bufferStart, bufferEnd: ptr byte
bufferEndPos: int
LayeredInputStream* = ref object of InputStream
subStream*: InputStream
InputStreamHandle* = object
s*: InputStream
AsyncInputStream* {.borrow: `.`.} = distinct InputStream
ReadSyncProc* = proc (s: InputStream, buffer: ptr byte, bufSize: int): int
{.nimcall, gcsafe, raises: [IOError, Defect].}
ReadAsyncProc* = proc (s: InputStream, buffer: ptr byte, bufSize: int): Future[int]
{.nimcall, gcsafe, raises: [IOError, Defect].}
CloseSyncProc* = proc (s: InputStream)
{.nimcall, gcsafe, raises: [IOError, Defect].}
CloseAsyncProc* = proc (s: InputStream): Future[void]
{.nimcall, gcsafe, raises: [IOError, Defect].}
GetLenSyncProc* = proc (s: InputStream): int
{.nimcall, gcsafe, raises: [IOError, Defect].}
InputStreamVTable* = object
readSync*: ReadSyncProc
readAsync*: ReadAsyncProc
closeSync*: CloseSyncProc
closeAsync*: CloseAsyncProc
getLenSync*: GetLenSyncProc
FileInputStream = ref object of InputStream
file: MemFile
const
lengthUnknown* = -1
debugHelpers = false
nimAllocatorMetadataSize* = 0
# TODO: Get this from Nim's allocator.
# The goal is to make perfect page-aligned allocations
# defaultPageSize = 4096 - nimAllocatorMetadataSize
proc preventFurtherReading(s: InputStream) =
s.vtable = nil
s.head = nil
s.bufferEnd = nil
proc close*(s: InputStream) {.raises: [IOError, Defect].} =
if s != nil:
if s.vtable != nil and s.vtable.closeSync != nil:
s.vtable.closeSync(s)
s.preventFurtherReading()
# TODO
# The destructors are currently disabled because they seem to cause
# mysterious segmentation faults related to corrupted GC internal
# data structures.
#[
proc `=destroy`*(h: var InputStreamHandle) {.raises: [Defect].} =
if h.s != nil:
if h.s.vtable != nil and h.s.vtable.closeSync != nil:
try:
h.s.vtable.closeSync(h.s)
except IOError:
# Since this is a destructor, there is not much we can do here.
# If the user wanted to handle the error, they would have called
# `close` manually.
discard # TODO
# TODO ATTENTION!
# Uncommenting the following line will lead to a GC heap corruption.
# Most likely this leads to Nim collecting some object prematurely.
# h.s = nil
# We work-around the problem through more indirect incapacitatation
# of the stream object:
h.s.preventFurtherReading()
]#
converter implicitDeref*(h: InputStreamHandle): InputStream =
h.s
let FileStreamVTable = InputStreamVTable(
closeSync: proc (s: InputStream)
{.nimcall, gcsafe, raises: [IOError, Defect].} =
try:
close FileInputStream(s).file
except OSError as err:
raise newException(IOError, "Failed to close file", err)
,
getLenSync: proc (s: InputStream): int
{.nimcall, gcsafe, raises: [IOError, Defect].} =
distance(s.head, s.bufferEnd)
)
template vtableAddr*(vtable: InputStreamVTable): ptr InputStreamVTable =
## This is a simple work-around for the somewhat broken side
## effects analysis of Nim - reading from global let variables
## is considered a side-effect.
{.noSideEffect.}:
unsafeAddr vtable
proc fileInput*(filename: string): InputStreamHandle =
let
memFile = memfiles.open(filename)
head = cast[ptr byte](memFile.mem)
fileSize = memFile.size
var stream = FileInputStream(
vtable: vtableAddr FileStreamVTable,
head: head,
bufferEnd: offset(head, fileSize),
bufferEndPos: fileSize,
file: memFile)
when debugHelpers:
stream.bufferStart = head
InputStreamHandle(s: stream)
proc memoryInput*(mem: openarray[byte]): InputStreamHandle =
let head = unsafeAddr mem[0]
InputStreamHandle(s: InputStream(
head: head,
bufferEnd: offset(head, mem.len),
bufferEndPos: mem.len))
proc memoryInput*(str: string): InputStreamHandle =
memoryInput str.toOpenArrayByte(0, str.len - 1)
# TODO: Is this used, should we deprecate it?
proc endPos*(s: InputStream): int =
doAssert s.vtable == nil or s.vtable.getLenSync != nil
return s.bufferEndPos
# TODO The return type here could be Option[Natural] if Nim had
# the Option[range] optimisation that will make it equvalent to `int`.
proc len*(s: InputStream): int {.raises: [Defect, IOError].} =
if s.vtable == nil:
distance(s.head, s.bufferEnd)
elif s.vtable.getLenSync != nil:
s.vtable.getLenSync(s)
else:
lengthUnknown
template len*(s: AsyncInputStream): int =
len InputStream(s)
proc bufferMoreDataSync(s: InputStream): bool =
# Returns true if more data was successfully buffered
if s.vtable == nil or s.vtable.readSync == nil:
return false
let bytesRead = s.vtable.readSync(s, s.bufferStart, s.bufferSize)
if bytesRead == 0:
# TODO close the input device
s.vtable = nil
return false
else:
s.bufferEnd = offset(s.bufferStart, bytesRead)
s.bufferEndPos += bytesRead
return true
proc bufferMoreDataAsync(s: AsyncInputStream): Future[bool] {.async.} =
# Returns true if more data was successfully buffered
return false
proc readable*(s: InputStream): bool =
if s.head != s.bufferEnd:
true
else:
s.bufferMoreDataSync()
template readable*(sp: AsyncInputStream): bool =
let s = sp
if s.head != s.bufferEnd:
true
else:
faststreamsAwait bufferMoreDataAsync(s)
proc readable*(s: InputStream, n: int): bool =
if distance(s.head, s.bufferEnd) >= n:
return true
if s.vtable == nil or s.vtable.readSync == nil:
return false
# TODO
doAssert false, "Multi-buffer reading will be implemented later"
template readable*(sp: AsyncInputStream, n: int): bool =
let s = sp
if distance(s.head, s.bufferEnd) >= n:
return true
if s.vtable == nil:
return false
# TODO
doAssert false, "Multi-buffer reading will be implemented later"
template close*(s: AsyncInputStream) =
close InputStream(s)
proc peek*(s: InputStream): byte {.inline.} =
doAssert s.head != s.bufferEnd
return s.head[]
template peek*(s: AsyncInputStream): byte =
peek InputStream(s)
proc peekAt*(s: InputStream, pos: int): byte {.inline.} =
# TODO implement page flipping
let peekHead = offset(s.head, pos)
doAssert cast[uint](peekHead) < cast[uint](s.bufferEnd)
return peekHead[]
template peekAt*(s: AsyncInputStream, pos: int): byte =
peekAt InputStream(s)
when debugHelpers:
proc showPosition*(s: InputStream) =
echo "head at ", distance(s.bufferStart, s.head), "/",
distance(s.bufferStart, s.bufferEnd)
proc advance*(s: InputStream) =
if s.head != s.bufferEnd:
s.head = offset(s.head, 1)
else:
discard s.bufferMoreDataSync()
template advance*(sp: AsyncInputStream) =
let s = sp
if s.head != s.bufferEnd:
s.head = offset(s.head, 1)
else:
discard faststreamsAwait(bufferMoreDataAsync(s))
proc read*(s: InputStream): byte =
result = s.peek()
advance s
template read*(sp: AsyncInputStream): byte =
let s = sp
let res = s.peek()
advance(s)
res
proc checkReadAhead(s: InputStream, n: int): ptr byte =
result = s.head
doAssert distance(s.head, s.bufferEnd) >= n
s.head = offset(s.head, n)
template read*(s: InputStream, n: int): auto =
makeOpenArray(checkReadAhead(s, n), n)
proc next*(s: InputStream): Option[byte] =
if readable(s):
result = some read(s)
template next*(sp: AsyncInputStream): Option[byte] =
let s = sp
if readable(s):
some read(s)
else:
none byte
proc bufferPos(s: InputStream, pos: int): ptr byte =
let offsetFromEnd = pos - s.bufferEndPos
doAssert offsetFromEnd < 0
result = offset(s.bufferEnd, offsetFromEnd)
doAssert result >= s.bufferStart
proc pos*(s: InputStream): int {.inline.} =
s.bufferEndPos - distance(s.head, s.bufferEnd)
template pos*(s: AsyncInputStream): int =
pos InputStream(s)
proc firstAccessiblePos*(s: InputStream): int {.inline.} =
s.bufferEndPos - distance(s.bufferStart, s.bufferEnd)
proc `[]`*(s: InputStream, pos: int): byte {.inline.} =
s.bufferPos(pos)[]
proc rewind*(s: InputStream, delta: int) =
s.head = offset(s.head, -delta)
doAssert s.head >= s.bufferStart
proc rewindTo*(s: InputStream, pos: int) {.inline.} =
s.head = s.bufferPos(pos)

577
faststreams/inputs.nim Normal file
View file

@ -0,0 +1,577 @@
import
os, memfiles, options,
stew/[ptrops, ranges/ptr_arith],
async_backend, buffers
export
options, CloseBehavior
type
InputStream* = ref object of RootObj
vtable: ptr InputStreamVTable # This is nil for unsafe memory inputs
buffers: PageBuffers # This is nil for unsafe memory inputs
span: PageSpan
spanEndPos: Natural
closeFut: Future[void] # This is nil before `close` is called
LayeredInputStream* = ref object of InputStream
subStream*: InputStream
InputStreamHandle* = object
s*: InputStream
AsyncInputStream* {.borrow: `.`.} = distinct InputStream
ReadSyncProc* = proc (s: InputStream)
{.nimcall, gcsafe, raises: [IOError, Defect].}
ReadAsyncProc* = proc (s: InputStream): Future[void]
{.nimcall, gcsafe, raises: [IOError, Defect].}
CloseSyncProc* = proc (s: InputStream)
{.nimcall, gcsafe, raises: [IOError, Defect].}
CloseAsyncProc* = proc (s: InputStream): Future[void]
{.nimcall, gcsafe, raises: [IOError, Defect].}
GetLenSyncProc* = proc (s: InputStream): Natural
{.nimcall, gcsafe, raises: [IOError, Defect].}
InputStreamVTable* = object
readSync*: ReadSyncProc
readAsync*: ReadAsyncProc
closeSync*: CloseSyncProc
closeAsync*: CloseAsyncProc
getLenSync*: GetLenSyncProc
MemFileInputStream = ref object of InputStream
file: MemFile
FileInputStream = ref object of InputStream
file: File
proc disconnectInputDevice(s: InputStream) =
# TODO
# Document the behavior that closeAsync is preferred
if s.vtable != nil:
if s.vtable.closeAsync != nil:
s.closeFut = s.vtable.closeAsync(s)
elif s.vtable.closeSync != nil:
s.vtable.closeSync(s)
s.vtable = nil
template disconnectInputDevice(s: AsyncInputStream) =
disconnectInputDevice InputStream(s)
proc preventFurtherReading(s: InputStream) =
s.vtable = nil
s.span = default(PageSpan)
template preventFurtherReading(s: AsyncInputStream) =
preventFurtherReading InputStream(s)
template makeHandle*(sp: InputStream): InputStreamHandle =
let s = sp
InputStreamHandle(s: s)
proc close*(s: InputStream,
behavior = dontWaitAsyncClose)
{.raises: [IOError, Defect].} =
## Closes the stream. Any resources associated with the stream
## will be released and no further reading will be possible.
##
## If the underlying input device requires asynchronous closing
## and `behavior` is set to `waitAsyncClose`, this proc will use
## `waitFor` to block until the async operation completes.
s.disconnectInputDevice()
s.preventFurtherReading()
if s.closeFut != nil:
fsTranslateErrors "Stream closing failed":
if behavior == waitAsyncClose:
waitFor s.closeFut
else:
asyncCheck s.closeFut
proc close*(s: AsyncInputStream): Future[void]
{.raises: [IOError, Defect].} =
## Starts the asychronous closing of the stream and returns a future that
## tracks the closing operation.
s.disconnectInputDevice()
s.preventFurtherReading()
result = InputStream(s).closeFut
doAssert result != nil
template closeNoWait*(sp: AsyncInputStream|InputStream) =
## Close the stream without waiting even if's async.
## This operation will use `asyncCheck` internally to detect unhandled
## errors from the closing operation.
close(InputStream(s), dontWaitAsyncClose)
# TODO
# The destructors are currently disabled because they seem to cause
# mysterious segmentation faults related to corrupted GC internal
# data structures.
#[
proc `=destroy`*(h: var InputStreamHandle) {.raises: [Defect].} =
if h.s != nil:
if h.s.vtable != nil and h.s.vtable.closeSync != nil:
try:
h.s.vtable.closeSync(h.s)
except IOError:
# Since this is a destructor, there is not much we can do here.
# If the user wanted to handle the error, they would have called
# `close` manually.
discard # TODO
# TODO ATTENTION!
# Uncommenting the following line will lead to a GC heap corruption.
# Most likely this leads to Nim collecting some object prematurely.
# h.s = nil
# We work-around the problem through more indirect incapacitatation
# of the stream object:
h.s.preventFurtherReading()
]#
converter implicitDeref*(h: InputStreamHandle): InputStream =
## Any `InputStreamHandle` value can be implicitly converted to an
## `InputStream` or an `AsyncInputStream` value.
h.s
template vtableAddr*(vtable: InputStreamVTable): ptr InputStreamVTable =
# This is a simple work-around for the somewhat broken side
# effects analysis of Nim - reading from global let variables
# is considered a side-effect.
{.noSideEffect.}:
unsafeAddr vtable
let MemFileInputVTable = InputStreamVTable(
closeSync: proc (s: InputStream)
{.nimcall, gcsafe, raises: [IOError, Defect].} =
try:
close MemFileInputStream(s).file
except OSError as err:
raise newException(IOError, "Failed to close file", err)
,
getLenSync: proc (s: InputStream): Natural
{.nimcall, gcsafe, raises: [IOError, Defect].} =
s.span.len
)
proc memFileInput*(filename: string, mappedSize = -1, offset = 0): InputStreamHandle
{.raises: [IOError, OSError].} =
## Creates an input stream for reading the contents of a memory-mapped file.
##
## Using this API will provide better performance than `fileInput`,
## but this comes at a cost of higher address space usage which may
## be problematic when working with extremely large files.
##
## All parameters are forwarded to Nim's memfiles.open function:
##
## ``filename``
## The name of the file to read.
##
## ``mappedSize`` and ``offset``
## can be used to map only a slice of the file.
##
## ``offset`` must be multiples of the PAGE SIZE of your OS
## (usually 4K or 8K, but is unique to your OS)
# Nim's memfiles module will fail to map an empty file,
# but we don't consider this a problem. The stream will
# be in non-readable state from the start.
let fileSize = getFileSize(filename)
if fileSize == 0:
return makeHandle InputStream()
let
memFile = memfiles.open(filename,
mode = fmRead,
mappedSize = mappedSize,
offset = offset)
head = cast[ptr byte](memFile.mem)
mappedSize = memFile.size
makeHandle MemFileInputStream(
vtable: vtableAddr MemFileInputVTable,
span: PageSpan(
startAddr: head,
endAddr: offset(head, mappedSize)),
file: memFile)
proc readableNow*(s: InputStream): bool =
(not s.span.atEnd) or (s.buffers != nil and s.buffers.len > 1)
template readableNow*(s: AsyncInputStream): bool =
readableNow InputStream(s)
func totalUnconsumedBytes*(s: InputStream): Natural =
## Returns the number of bytes that are currently sitting within the stream
## buffers and that can be consumed with `read` or `advance`.
result = s.span.len
if s.buffers != nil:
result += s.buffers.totalBytesRead - s.spanEndPos
template totalUnconsumedBytes*(s: AsyncInputStream): Natural =
## Alias for InputStream.totalUnconsumedBytes
totalUnconsumedBytes InputStream(s)
let FileInputVTable = InputStreamVTable(
readSync: proc (s: InputStream)
{.nimcall, gcsafe, raises: [IOError, Defect].} =
let file = FileInputStream(s).file
s.buffers.writeToSpan(span):
file.readBuffer(span.startAddr, span.len)
,
getLenSync: proc (s: InputStream): Natural
{.nimcall, gcsafe, raises: [IOError, Defect].} =
let
s = FileInputStream(s)
runway = s.totalUnconsumedBytes
let preservedPos = getFilePos(s.file)
setFilePos(s.file, 0, fspEnd)
let endPos = getFilePos(s.file)
setFilePos(s.file, preservedPos)
endPos - preservedPos + runway
,
closeSync: proc (s: InputStream)
{.nimcall, gcsafe, raises: [IOError, Defect].} =
try:
close FileInputStream(s).file
except OSError as err:
raise newException(IOError, "Failed to close file", err)
)
proc fileInput*(filename: string,
offset = 0,
pageSize = defaultPageSize): InputStreamHandle
{.raises: [IOError, OSError].} =
## Creates an input stream for reading the contents of a file
## through Nim's `io` module.
##
## Parameters:
##
## ``filename``
## The name of the file to read.
##
## ``offset``
## Initial position in the file where reading should start.
##
let file = system.open(filename, fmRead)
if offset != 0:
setFilePos(file, offset)
makeHandle FileInputStream(
vtable: vtableAddr FileInputVTable,
buffers: initPageBuffers(pageSize),
file: file)
proc unsafeMemoryInput*(mem: openarray[byte]): InputStreamHandle =
let head = unsafeAddr mem[0]
makeHandle InputStream(
span: PageSpan(
startAddr: head,
endAddr: offset(head, mem.len)),
spanEndPos: mem.len)
proc unsafeMemoryInput*(str: string): InputStreamHandle =
unsafeMemoryInput str.toOpenArrayByte(0, str.len - 1)
proc len*(s: InputStream): Option[Natural] {.raises: [Defect, IOError].} =
if s.vtable == nil:
some s.span.len
elif s.vtable.getLenSync != nil:
some s.vtable.getLenSync(s)
else:
none Natural
template len*(s: AsyncInputStream): int =
len InputStream(s)
proc flipPage(s: InputStream) =
doAssert s.buffers.len > 1
discard s.buffers.popFirst
s.span = s.buffers[0].span
s.spanEndPos += s.span.len
proc continueAfterRead(s: InputStream): bool =
# Please note that this is extracted into a proc only to reduce the code
# that ends up inlined into async procs by `bufferMoreDataImpl`.
# The inlining itself is required to support the await-free operation of
# the `readable` APIs.
let firstReadPage = s.buffers[0]
s.span = firstReadPage.span
let bytesRead = s.span.len
s.spanEndPos += bytesRead
# The read might have been incomplete which signals the EOF of the stream.
# If this is the case, we disconnect the input device which prevents any
# further attempts to read from it:
if wasEofReached(s.buffers):
s.disconnectInputDevice()
# If we read some bytes anyway, we tell the user code that our buffers
# contain some unconsumed data:
bytesRead > 0
template bufferMoreDataImpl(s, awaiter, readOp: untyped): bool =
# This template is always called when the current page has been
# completely exhausted. It should produce `true` if more data was
# successfully buffered, so reading can continue.
#
# The vtable will be `nil` for a memory stream and `vtable.readOp`
# will be `nil` for a memFile. If we've reached here, this is the
# end of the memory buffer, so we can signal EOF:
if s.buffers == nil or s.vtable == nil or s.vtable.readOp == nil:
false
else:
# There might be additional pages in our buffer queue. If so, we
# just jump to the next one:
if s.buffers.len > 1:
flipPage s
true
else:
# We ask our input device to populate our page queue with newly
# read pages. The state of the queue afterwards will tell us if
# the read was successful. In `continueAfterRead`, we examine if
# EOF was reached, but please note that some data might have been
# read anyway:
awaiter s.vtable.readOp(s)
continueAfterRead(s)
proc bufferMoreDataSync(s: InputStream): bool =
# This proc exists only to avoid inlining of the code of
# `bufferMoreDataImpl` into `readable` (which in turn is
# a template inlined in the user code).
bufferMoreDataImpl(s, noAwait, readSync)
template readable*(sp: InputStream): bool =
## Checks whether reading more data from the stream is possible.
##
## If there is any unconsumed data in the stream buffers, the
## operation returns `true` immediately. You can call `read`
## or `peek` afterwards to consume or examine the next byte
## in the stream.
##
## If the stream buffers are empty, the operation may block
## until more data becomes available. The end of the stream
## may be reached at this point, which will be indicated by
## a `false` return value. Any attempt to call `read` or
## `peek` afterwards is considered a `Defect`.
##
## Please note that this API is intended for stream consumers
## who need to consume the data one byte at a time. A typical
## usage will be the following:
##
## ```nim
## while stream.readable:
## case stream.peek.char
## of '"':
## parseString(stream)
## of '0'..'9':
## parseNumber(stream)
## of '\':
## discard stream.read # skip the slash
## let escapedChar = stream.read
## ```
##
## Even though the user code consumes the data one byte at a time,
## in the majority of cases this consist of simply incrementing a
## pointer within the stream buffers. Only when the stream buffers
## are exhausted, a new read operation will be executed throught
## the stream input device which may repopulate the buffers with
## fresh data. See `Stream Pages` for futher discussion of this.
# This is a template, because we want the pointer check to be
# inlined at the call sites. Only if it fails, we call into the
# larger non-inlined proc:
let s = sp
hasRunway(s.span) or bufferMoreDataSync(s)
template readable*(sp: AsyncInputStream): bool =
## Async version of `readable`.
## The intended API usage is the same. Instead of blocking, an async
## stream will use `await` while waiting for more data.
let s = sp
if hasRunway(s.span):
true
else:
bufferMoreDataImpl(s, fsAsync, readAsync)
template readableNImpl(s, n, awaiter, readOp: untyped): bool =
let runway = s.totalUnconsumedBytes
if runway >= n:
true
elif s.buffers == nil or s.vtable == nil or s.vtable.readOp == nil:
false
else:
var
bytesDeficit = n - runway
targetBytesRead = s.buffers.totalBytesRead + bytesDeficit
res = false
while true:
awaiter s.vtable.readOp(s)
if wasEofReached(s.buffers):
s.disconnectInputDevice()
res = s.buffers.totalBytesRead >= targetBytesRead
break
if s.buffers.totalBytesRead >= targetBytesRead:
res = true
break
res
proc readable*(s: InputStream, n: int): bool =
## Checks whether reading `n` bytes from the input stream is possible.
##
## If there is enough unconsumed data in the stream buffers, the
## operation will return `true` immediately. You can use `read`,
## `peek`, `read(n)` or `peek(n)` afterwards to consume up to the
## number of verified bytes. Please note that consuming more bytes
## will be considered a `Defect`.
##
## If the stream buffers do not contain enough data, the operation
## may block until more data becomes available. The end of the stream
## may be reached at this point, which will be indicated by a `false`
## return value. Please note that the stream might still contain some
## unconsumed bytes after `readable(n)` returned false. You can use
## `totalUnconsumedBytes` or a combination of `readable` and `read`
## to consume the remaining bytes if desired.
##
## If possible, prefer consuming the data one byte at a time. This
## ensures the most optimal usage of the stream buffers. Even after
## calling `readable(n)`, it's still preferrable to continue with
## `read` instead of `read(n)` because the later may require the
## resulting bytes to be copied to a freshly allocated sequence.
##
## In the situation where the consumed bytes need to be copied to
## an existing external buffer, `readInto` will provide the best
## performance instead.
##
## Just like `readable`, this operation will invoke reads on the
## stream input device only when necessary. See `Stream Pages`
## for futher discussion of this.
readableNImpl(s, n, noAwait, readSync)
template readable*(sp: AsyncInputStream, np: int): bool =
## Async version of `readable(n)`.
## The intended API usage is the same. Instead of blocking, an async
## stream will use `await` while waiting for more data.
let
s = sp
n = np
readableNImpl(s, n, fsAwait, readAsync)
proc peek*(s: InputStream): byte {.inline.} =
doAssert hasRunway(s.span)
return s.span.startAddr[]
template peek*(s: AsyncInputStream): byte =
peek InputStream(s)
proc peekAt*(s: InputStream, pos: int): byte {.inline.} =
# TODO implement page flipping
let peekHead = offset(s.span.startAddr, pos)
doAssert cast[uint](peekHead) < cast[uint](s.span.endAddr)
return peekHead[]
template peekAt*(s: AsyncInputStream, pos: int): byte =
peekAt InputStream(s)
proc advance*(s: InputStream) =
if hasRunway(s.span):
bumpPointer s.span
elif s.buffers != nil and s.buffers.len > 1:
flipPage s
template advance*(s: AsyncInputStream) =
advance InputStream(s)
proc read*(s: InputStream): byte =
result = s.peek()
advance s
template read*(s: AsyncInputStream): byte =
read InputStream(s)
proc readIntoEx*(s: InputStream, target: var openarray[byte]): int =
## Read data into the destination buffer.
##
## Returns the number of bytes that were successfully
## written to the buffer. The function will return a
## number smaller than the buffer length only if EOF
## was reached before the buffer was fully populated.
discard
proc readInto*(s: InputStream, target: var openarray[byte]): bool =
## Read data into the destination buffer.
##
## Returns `false` if EOF was reached before the buffer
## was fully populated. if you need precise information
## regarding the number of bytes read, see `readIntoEx`.
s.readIntoEx(target) == target.len
template readInto*(s: AsyncInputStream, target: var openarray[byte]): bool =
## Asynchronously read data into the destination buffer.
##
## Returns `false` if EOF was reached before the buffer
## was fully populated. if you need precise information
## regarding the number of bytes read, see `readIntoEx`.
##
## If there are enough bytes already buffered by the stream,
## the expression will complete immediately.
## Otherwise, it will await more bytes to become available.
discard
proc checkReadAhead(s: InputStream, n: Natural): ptr byte =
# TODO: handle multi-page
result = s.span.startAddr
doAssert s.span.len >= n
bumpPointer s.span, n
template read*(s: InputStream, n: Natural): auto =
makeOpenArray(checkReadAhead(s, n), n)
proc next*(s: InputStream): Option[byte] =
if readable(s):
result = some read(s)
template next*(sp: AsyncInputStream): Option[byte] =
let s = sp
if readable(s):
some read(s)
else:
none byte
proc pos*(s: InputStream): int {.inline.} =
s.spanEndPos - s.span.len
template pos*(s: AsyncInputStream): int =
pos InputStream(s)
when false:
# Obsolete APIs for removal
proc bufferPos(s: InputStream, pos: int): ptr byte =
let offsetFromEnd = pos - s.spanEndPos
doAssert offsetFromEnd < 0
result = offset(s.span.endAddr, offsetFromEnd)
doAssert result >= s.bufferStart
proc `[]`*(s: InputStream, pos: int): byte {.inline.} =
s.bufferPos(pos)[]
proc rewind*(s: InputStream, delta: int) =
s.head = offset(s.head, -delta)
doAssert s.head >= s.bufferStart
proc rewindTo*(s: InputStream, pos: int) {.inline.} =
s.head = s.bufferPos(pos)

View file

@ -1,6 +1,6 @@
import
stew/shims/macros,
async_backend, input_stream, output_stream
async_backend, inputs, outputs
macro fsMultiSync*(body: untyped) =
# We will produce an identical copy of the annotated proc,

View file

@ -1,540 +0,0 @@
import
deques, typetraits,
stew/[ptrops, strings, ranges/ptr_arith],
async_backend
type
OutputPage = object
buffer: string
startOffset: int
OutputStream* = ref object of RootObj
vtable*: ptr OutputStreamVTable
cursor*: WriteCursor
pages: Deque[OutputPage]
endPos: int
extCursorsCount: int
pageSize*: int
maxWriteSize*: int
minWriteSize*: int
LayeredOutputStream* = ref object of OutputStream
subStream*: OutputStream
OutputStreamHandle* = object
s*: OutputStream
AsyncOutputStream* {.borrow: `.`.} = distinct OutputStream
WritePageSyncProc* = proc (s: OutputStream, page: openarray[byte])
{.nimcall, gcsafe, raises: [IOError, Defect].}
WritePageAsyncProc* = proc (s: OutputStream, buf: pointer, bufLen: int): Future[void]
{.nimcall, gcsafe, raises: [IOError, Defect].}
FlushSyncProc* = proc (s: OutputStream)
{.nimcall, gcsafe, raises: [IOError, Defect].}
FlushAsyncProc* = proc (s: OutputStream): Future[void]
{.nimcall, gcsafe, raises: [IOError, Defect].}
CloseSyncProc* = proc (s: OutputStream)
{.nimcall, gcsafe, raises: [IOError, Defect].}
CloseAsyncProc* = proc (s: OutputStream): Future[void]
{.nimcall, gcsafe, raises: [IOError, Defect].}
OutputStreamVTable* = object
writePageSync*: WritePageSyncProc
writePageAsync*: WritePageAsyncProc
flushSync*: FlushSyncProc
flushAsync*: FlushAsyncProc
closeSync*: CloseSyncProc
closeAsyncProc*: CloseAsyncProc
WriteCursor* = object
head, bufferEnd: ptr byte
stream: OutputStream
VarSizeWriteCursor* = distinct WriteCursor
FileOutputStream = ref object of OutputStream
file: File
const
nimAllocatorMetadataSize* = 0
# TODO: Get this from Nim's allocator.
# The goal is to make perfect page-aligned allocations
defaultPageSize = 4096 - nimAllocatorMetadataSize - 1 # 1 byte for the null terminator
proc close*(s: OutputStream) {.raises: [IOError, Defect].} =
if s != nil:
if s.vtable != nil and s.vtable.closeSync != nil:
s.vtable.closeSync(s)
# TODO
# The destructors are currently disabled because they seem to cause
# mysterious segmentation faults related to corrupted GC internal
# data structures.
#[
proc `=destroy`*(h: var OutputStreamHandle) {.raises: [Defect].} =
if h.s != nil:
if h.s.vtable != nil and h.s.vtable.closeSync != nil:
try:
h.s.vtable.closeSync(h.s)
except IOError:
# Since this is a destructor, there is not much we can do here.
# If the user wanted to handle the error, they would have called
# `close` manually.
discard # TODO
# h.s = nil
]#
converter implicitDeref*(h: OutputStreamHandle): OutputStream =
h.s
template canExtendOutput(s: OutputStream): bool =
# Streams writing to pre-allocated existing buffers cannot be grown
s != nil and s.pageSize > 0
template isExternalCursor(c: var WriteCursor): bool =
# Is this the original stream cursor or is it one created by a "delayed write"
addr(c) != addr(c.stream.cursor)
func runway*(c: var WriteCursor): int {.inline.} =
distance(c.head, c.bufferEnd)
proc prepareRunway*(s: OutputStream, length: int) =
# TODO implement this
discard
template prepareRunway*(s: AsyncOutputStream, length: int) =
prepareRunway OutputStream(s)
proc flipPage(s: OutputStream) =
s.cursor.head = cast[ptr byte](addr s.pages[s.pages.len - 1].buffer[0])
# TODO: There is an assumption here and elsewhere that `s.pages[^1]` has
# a length equal to `s.pageSize`
s.cursor.bufferEnd = cast[ptr byte](offset(s.cursor.head, s.pageSize))
s.endPos += s.pageSize
proc addPage(s: OutputStream) =
doAssert s.pageSize > 0
s.pages.addLast OutputPage(buffer: newString(s.pageSize),
startOffset: 0)
s.flipPage
proc initWithSinglePage*(s: OutputStream) =
s.pages = initDeque[OutputPage]()
s.addPage()
s.cursor.stream = s
proc memoryOutput*(pageSize = defaultPageSize): OutputStreamHandle =
var stream = OutputStream(
pageSize: pageSize,
minWriteSize: 1,
maxWriteSize: high(int))
stream.initWithSinglePage()
OutputStreamHandle(s: stream)
proc memoryOutput*(buffer: pointer, len: int): OutputStreamHandle =
let buffer = cast[ptr byte](buffer)
var stream = OutputStream()
stream.cursor.head = buffer
stream.cursor.bufferEnd = offset(buffer, len)
stream.cursor.stream = stream
stream.endPos = len
OutputStreamHandle(s: stream)
let FileStreamVTable = OutputStreamVTable(
writePageSync: proc (s: OutputStream, data: openarray[byte])
{.nimcall, gcsafe, raises: [IOError, Defect].} =
var file = FileOutputStream(s).file
var written = file.writeBuffer(unsafeAddr data[0], data.len)
if written != data.len:
raise newException(IOError, "Failed to write OutputStream page.")
,
flushSync: proc (s: OutputStream)
{.nimcall, gcsafe, raises: [IOError, Defect].} =
flushFile FileOutputStream(s).file
,
closeSync: proc (s: OutputStream)
{.nimcall, gcsafe, raises: [IOError, Defect].} =
close FileOutputStream(s).file
)
template vtableAddr*(vtable: OutputStreamVTable): ptr OutputStreamVTable =
## This is a simple work-around for the somewhat broken side
## effects analysis of Nim - reading from global let variables
## is considered a side-effect.
{.noSideEffect.}:
unsafeAddr vtable
proc fileOutput*(filename: string,
fileMode: FileMode = fmWrite,
pageSize = defaultPageSize): OutputStreamHandle {.
raises: [IOError, Defect]
.} =
let f = open(filename, fileMode)
var stream = FileOutputStream(
vtable: vtableAddr FileStreamVTable,
pageSize: pageSize,
minWriteSize: 1,
maxWriteSize: high(int),
file: f)
stream.initWithSinglePage()
OutputStreamHandle(s: stream)
proc pos*(s: OutputStream): int =
s.endPos - s.cursor.runway
proc safeWritePage(s: OutputStream, data: openarray[byte]) {.inline.} =
if data.len > 0: s.vtable.writePageSync(s, data)
proc writePages(s: OutputStream, skipLast = 0) =
assert s.vtable != nil
for i in 0 ..< s.pages.len - skipLast:
s.safeWritePage s.pages[i].buffer.toOpenArrayByte(0, s.pages[i].buffer.len - 1)
proc writePartialPage(s: OutputStream, page: var OutputPage) =
assert s.vtable != nil
let
unwrittenBytes = s.cursor.runway
pageEndPos = s.pageSize - unwrittenBytes - 1
pageStartPos = page.startOffset
s.safeWritePage page.buffer.toOpenArrayByte(pageStartPos, pageEndPos)
s.endPos -= unwrittenBytes
page.startOffset = 0
s.flipPage
proc flush*(s: OutputStream) =
doAssert s.extCursorsCount == 0
if s.vtable != nil:
# We write all pages except the last one
s.writePages(skipLast = 1)
# Then we erase them from the list
s.pages.shrink(fromFirst = s.pages.len - 1)
# Then we write the current page, which is probably incomplete
s.writePartialPage s.pages[0]
# Finally, we flush
s.vtable.flushSync(s)
proc writePendingPagesAndLeaveOne(s: OutputStream) {.inline.} =
s.writePages
s.pages.shrink(fromFirst = s.pages.len - 1)
s.pages[0].startOffset = 0
s.flipPage
proc tryFlushing(s: OutputStream) {.inline.} =
# Pre-conditions:
# * The cursor has reached the current buffer end
#
# Post-conditions:
# * All completed pages are written
# * There is a fresh page ready for writing at the top
# (we can reuse a previously existing page for this)
# * The head and bufferEnd pointers point to the new top page
if s.vtable != nil and s.extCursorsCount == 0:
s.writePendingPagesAndLeaveOne
else:
s.addPage
func endAddr(s: string): ptr byte {.inline.} =
let a = unsafeAddr s[0]
offset(cast[ptr byte](a), s.len)
template startAddr(s: string): ptr byte =
cast[ptr byte](unsafeAddr s[0])
func boundingAddrs(s: string): (ptr byte, ptr byte) {.inline.} =
(startAddr s, endAddr s)
proc findNextPage(c: var WriteCursor): int =
let cursorBufferEnd = c.bufferEnd
for i in 0 .. c.stream.pages.len - 2:
let pageEnd = endAddr c.stream.pages[i].buffer
if cursorBufferEnd == pageEnd:
return i + 1
doAssert false # There is no next page the cursor can move to
proc moveToPage(c: var WriteCursor, p: var OutputPage) =
doAssert p.startOffset > 0
c.head = cast[ptr byte](unsafeAddr p.buffer[0])
c.bufferEnd = offset(c.head, p.startOffset)
p.startOffset = 0
proc moveToNextPage(c: var WriteCursor) =
c.moveToPage c.stream.pages[c.findNextPage()]
proc append*(c: var WriteCursor, b: byte) =
if c.head == c.bufferEnd:
doAssert c.stream.canExtendOutput
if c.isExternalCursor:
c.moveToNextPage()
else:
c.stream.tryFlushing()
c.head[] = b
c.head = offset(c.head, 1)
template append*(c: var WriteCursor, x: char) =
bind append
c.append byte(x)
proc writeDataAsPages(s: OutputStream, data: ptr byte, dataLen: int) =
var
data = data
dataLen = dataLen
if dataLen > s.pageSize:
if dataLen < s.maxWriteSize:
s.vtable.writePageSync(s, makeOpenArray(data, dataLen))
s.endPos += dataLen
return
while dataLen > s.pageSize:
s.vtable.writePageSync(s, makeOpenArray(data, s.pageSize))
data = offset(data, s.pageSize)
dec dataLen, s.pageSize
s.endPos += s.pageSize
copyMem(s.cursor.head, data, dataLen)
s.cursor.head = offset(s.cursor.head, dataLen)
proc newStringFromBytes(input: ptr byte, inputLen: int): string =
assert inputLen > 0
result = newString(inputLen)
copyMem(addr result[0], input, inputLen)
proc handleLongAppend*(c: var WriteCursor, bytes: openarray[byte]) =
var
pageRemaining = c.runway
inputPos = unsafeAddr bytes[0]
inputLen = bytes.len
stream = c.stream
template reduceInput(delta: int) =
inputPos = offset(inputPos, delta)
inputLen -= delta
# Since the input is longer, we first make sure that the top-most
# page is filled to the top:
doAssert c.stream.canExtendOutput
copyMem(c.head, inputPos, pageRemaining)
reduceInput pageRemaining
if c.isExternalCursor:
var
totalPages = stream.pages.len
nextPageIdx = c.findNextPage
while nextPageIdx < totalPages:
let
pageStart = startAddr stream.pages[nextPageIdx].buffer
pageRunway = stream.pages[nextPageIdx].startOffset
pageLen = stream.pageSize
doAssert pageRunway > 0
stream.pages[nextPageIdx].startOffset = 0
if pageRunway < pageLen:
doAssert inputLen <= pageRunway
copyMem(pageStart, inputPos, inputLen)
c.head = offset(pageStart, inputLen)
c.bufferEnd = offset(pageStart, pageRunway)
return
else:
if inputLen <= pageLen:
copyMem(pageStart, inputPos, inputLen)
c.head = offset(pageStart, inputLen)
c.bufferEnd = offset(pageStart, pageLen)
return
else:
copyMem(pageStart, inputPos, pageLen)
reduceInput pageLen
inc nextPageIdx
doAssert false # If we reached here, this means that we've ran out
# of pages, so this is a write past the end of the
# pre-allocated space for the delayed write.
elif c.stream.vtable != nil and c.stream.extCursorsCount == 0:
# This stream has an output device and we are ready to flush
# all the pending pages. One fresh page will be left on top.
# The input is yet to be written:
stream.writePendingPagesAndLeaveOne
# This will directly send our input to the output device.
# Since the output device has a preference for pageSize and
# maxWriteSize, we'll send some full pages to it and then
# some bytes will be written to the fresh page created above:
stream.writeDataAsPages(inputPos, inputLen)
else:
# We are not ready to flush, so we must create pending pages.
# We'll try to create them as large as possible:
let maxPageSize = c.stream.maxWriteSize
# We know how much the endPos will advance, but please note that
# it may be corrected later if we end up writing a portion of the
# input to an incomplete page:
stream.endPos += inputLen
# Try to create big pages until we have more data:
while inputLen > maxPageSize:
stream.pages.addLast OutputPage(
buffer: newStringFromBytes(inputPos, maxPageSize),
startOffset: 0)
reduceInput maxPageSize
# Here the remaining input is smaller than a max page, but it may be
# still larger than a regular page. If this is the case, we just create
# one final oversized page and then we leave one empty fresh page where
# the writing will continue:
if inputLen > c.stream.pageSize:
stream.pages.addLast OutputPage(
buffer: newStringFromBytes(inputPos, inputLen),
startOffset: 0)
stream.addPage
else:
# We don't have enough remaining bytes for a full page, so we'll just
# allocate a new empty page and we'll write the remaining input there.
# This will also reset the cursor to the start of the page:
stream.addPage
copyMem(c.head, inputPos, inputLen)
c.head = offset(c.head, inputLen)
# We must correct endPos, because it must mark the end of the top-most
# page. Since `addPage` advances the endPos as well and our remaining
# input was written to the newly created page, our initial increase of
# endPos was overestimated:
stream.endPos -= inputLen
proc append*(c: var WriteCursor, bytes: openarray[byte]) {.inline.} =
# We have a short inlinable function handling the case when the input is
# short enough to fit in the current page. We'll keep buffering until the
# page is full:
let
pageRemaining = c.runway
inputLen = bytes.len
if inputLen == 0: return
if inputLen <= pageRemaining:
copyMem(c.head, unsafeAddr bytes[0], inputLen)
c.head = offset(c.head, inputLen)
else:
handleLongAppend(c, bytes)
proc append*(c: var WriteCursor, chars: openarray[char]) {.inline.} =
var charsStart = unsafeAddr chars[0]
c.append makeOpenArray(cast[ptr byte](charsStart), chars.len)
template appendMemCopy*[T](c: var WriteCursor, value: T) =
bind append
static:
type TT = T # TODO This deals with a Nim bug
assert supportsCopyMem(TT)
let valueAddr = unsafeAddr value
c.append makeOpenArray(cast[ptr byte](valueAddr), sizeof(value))
template append*(c: var WriteCursor, str: string) =
bind append
append c, str.toOpenArrayByte(0, str.len - 1)
template append*(s: OutputStream, value: auto) =
bind append
append s.cursor, value
template appendMemCopy*(s: OutputStream, value: auto) =
bind append
append s.cursor, value
proc getOutput*(s: OutputStream, T: type string): string =
doAssert s.vtable == nil and s.extCursorsCount == 0 and s.pageSize > 0
s.pages[s.pages.len - 1].buffer.setLen(s.pageSize - s.cursor.runway)
if s.pages.len == 1 and s.pages[0].startOffset == 0:
result.swap s.pages[0].buffer
else:
result = newStringOfCap(s.pos)
for page in items(s.pages):
result.add page.buffer.toOpenArray(page.startOffset.int,
page.buffer.len - 1)
template getOutput*(s: OutputStream, T: type seq[byte]): seq[byte] =
cast[seq[byte]](s.getOutput(string))
template getOutput*(s: OutputStream): seq[byte] =
cast[seq[byte]](s.getOutput(string))
proc finishPageEarly(s: OutputStream, unwrittenBytes: int) {.inline.} =
s.pages[s.pages.len - 1].buffer.setLen(s.pageSize - unwrittenBytes)
s.endPos -= unwrittenBytes
s.tryFlushing()
proc createCursor(s: OutputStream, size: int): WriteCursor =
inc s.extCursorsCount
result = WriteCursor(head: s.cursor.head,
bufferEnd: offset(s.cursor.head, size),
stream: s)
s.cursor.head = result.bufferEnd
proc delayFixedSizeWrite*(s: OutputStream, size: Natural): WriteCursor =
let remainingBytesInPage = s.cursor.runway
if size <= remainingBytesInPage:
result = createCursor(s, size)
else:
result = createCursor(s, remainingBytesInPage)
var size = size - remainingBytesInPage
s.endPos += size
while size > s.pageSize:
s.pages.addLast OutputPage(buffer: newString(s.pageSize),
startOffset: s.pageSize)
size -= s.pageSize
s.pages.addLast OutputPage(buffer: newString(s.pageSize),
startOffset: size)
let (pageStart, pageEnd) = boundingAddrs s.pages[s.pages.len - 1].buffer
s.cursor.head = offset(pageStart, size)
s.cursor.bufferEnd = pageEnd
s.endPos += (s.pageSize - size)
proc delayVarSizeWrite*(s: OutputStream, maxSize: Natural): VarSizeWriteCursor =
doAssert maxSize < s.pageSize
s.finishPageEarly s.cursor.runway
VarSizeWriteCursor createCursor(s, maxSize)
proc finalize*(cursor: var WriteCursor) =
doAssert cursor.stream.extCursorsCount > 0
dec cursor.stream.extCursorsCount
proc writeAndFinalize*(cursor: var WriteCursor, data: openarray[byte]) =
doAssert data.len == cursor.runway
copyMem(cursor.head, unsafeAddr data[0], data.len)
finalize cursor
proc writeAndFinalize*(c: var VarSizeWriteCursor, data: openarray[byte]) =
template cursor: auto = WriteCursor(c)
for page in mitems(cursor.stream.pages):
if unsafeAddr(page.buffer[0]) == cursor.head:
let overestimatedBytes = cursor.runway - data.len
doAssert overestimatedBytes >= 0
page.startOffset = overestimatedBytes
copyMem(offset(cursor.head, overestimatedBytes), unsafeAddr data[0], data.len)
finalize cursor
return
doAssert false

685
faststreams/outputs.nim Normal file
View file

@ -0,0 +1,685 @@
## Please note that the use of unbuffered streams comes with a number
## of restrictions:
##
## * Delayed writes are not supported.
## * Output consuming operations such as `getOutput`, `consumeOutputs` and
## `consumeContiguousOutput` should not be used with them.
## * They cannot participate as intermediate steps in pipelines.
import
deques, typetraits,
stew/[ptrops, strings, ranges/ptr_arith],
buffers, async_backend
export
CloseBehavior
type
OutputStream* = ref object of RootObj
vtable*: ptr OutputStreamVTable # This is nil for any memory output
buffers*: PageBuffers # This is nil for unsafe memory outputs
span: PageSpan
spanEndPos: Natural
extCursorsCount: int
closeFut: Future[void]
WriteCursor* = object
span: PageSpan
stream: OutputStream
LayeredOutputStream* = ref object of OutputStream
subStream*: OutputStream
OutputStreamHandle* = object
s*: OutputStream
AsyncOutputStream* {.borrow: `.`.} = distinct OutputStream
WriteSyncProc* = proc (s: OutputStream, buf: pointer, bufLen: Natural)
{.nimcall, gcsafe, raises: [IOError, Defect].}
WriteAsyncProc* = proc (s: OutputStream, buf: pointer, bufLen: Natural): Future[void]
{.nimcall, gcsafe, raises: [IOError, Defect].}
FlushSyncProc* = proc (s: OutputStream)
{.nimcall, gcsafe, raises: [IOError, Defect].}
FlushAsyncProc* = proc (s: OutputStream): Future[void]
{.nimcall, gcsafe, raises: [IOError, Defect].}
CloseSyncProc* = proc (s: OutputStream)
{.nimcall, gcsafe, raises: [IOError, Defect].}
CloseAsyncProc* = proc (s: OutputStream): Future[void]
{.nimcall, gcsafe, raises: [IOError, Defect].}
OutputStreamVTable* = object
writeSync*: WriteSyncProc
writeAsync*: WriteAsyncProc
flushSync*: FlushSyncProc
flushAsync*: FlushAsyncProc
closeSync*: CloseSyncProc
closeAsync*: CloseAsyncProc
VarSizeWriteCursor* = distinct WriteCursor
FileOutputStream = ref object of OutputStream
file: File
const
nimAllocatorMetadataSize* = 0
# TODO: Get this from Nim's allocator.
# The goal is to make perfect page-aligned allocations
proc disconnectOutputDevice(s: OutputStream) =
if s.vtable != nil:
if s.vtable.closeAsync != nil:
s.closeFut = s.vtable.closeAsync(s)
elif s.vtable.closeSync != nil:
s.vtable.closeSync(s)
s.vtable = nil
template disconnectOutputDevice(s: AsyncOutputStream) =
disconnectOutputDevice OutputStream(s)
proc close*(s: OutputStream,
behavior = dontWaitAsyncClose)
{.raises: [IOError, Defect].} =
disconnectOutputDevice(s)
if s.closeFut != nil:
fsTranslateErrors "Stream closing failed":
if behavior == waitAsyncClose:
waitFor s.closeFut
else:
asyncCheck s.closeFut
proc close*(s: AsyncOutputStream): Future[void]
{.raises: [IOError, Defect].} =
disconnectOutputDevice(s)
result = OutputStream(s).closeFut
doAssert result != nil
template closeNoWait*(sp: AsyncOutputStream|OutputStream) =
## Close the stream without waiting even if's async.
## This operation will use `asyncCheck` internally to detect unhandled
## errors from the closing operation.
close(InputStream(s), dontWaitAsyncClose)
# TODO
# The destructors are currently disabled because they seem to cause
# mysterious segmentation faults related to corrupted GC internal
# data structures.
#[
proc `=destroy`*(h: var OutputStreamHandle) {.raises: [Defect].} =
if h.s != nil:
if h.s.vtable != nil and h.s.vtable.closeSync != nil:
try:
h.s.vtable.closeSync(h.s)
except IOError:
# Since this is a destructor, there is not much we can do here.
# If the user wanted to handle the error, they would have called
# `close` manually.
discard # TODO
# h.s = nil
]#
converter implicitDeref*(h: OutputStreamHandle): OutputStream =
h.s
template canExtendOutput(s: OutputStream): bool =
# Streams writing to pre-allocated existing buffers cannot be grown
s != nil and s.buffers != nil
template isExternalCursor(c: var WriteCursor): bool =
# Is this the original stream cursor or is it one created by a "delayed write"
addr(c) != addr(c.stream.cursor)
proc addPage(s: OutputStream) =
s.span = s.buffers.addWritablePage().writableSpan
s.spanEndPos += s.span.len
template makeHandle*(sp: OutputStream): OutputStreamHandle =
let s = sp
OutputStreamHandle(s: s)
proc memoryOutput*(pageSize = defaultPageSize): OutputStreamHandle =
doAssert pageSize > 0
makeHandle OutputStream(buffers: initPageBuffers(pageSize))
proc unsafeMemoryOutput*(buffer: pointer, len: Natural): OutputStreamHandle =
let buffer = cast[ptr byte](buffer)
makeHandle OutputStream(
span: PageSpan(startAddr: buffer, endAddr: offset(buffer, len)),
spanEndPos: len)
proc ensureRunway*(s: OutputStream, neededRunway: Natural) =
## The hint provided in `ensureRunway` overrides any previous
## hint specified at stream creation with `pageSize`.
let runway = s.span.len
# This is a temporary requirement.
# ensureRunway should be called immediately after creating the OutputStream
# In the future, we'll relax this by implementing more logic in buffers.nim
doAssert runway == 0, "call ensureRunway immediately after stream creation"
if neededRunway > runway:
# If you use an unsafe memory output, you must ensure that
# it will have a large enough size to hold the data you are
# feeding to it.
doAssert s.buffers != nil, "Unsafe memory output of insufficient size"
s.span = s.buffers.ensureRunway(neededRunway - runway)
template ensureRunway*(s: AsyncOutputStream, neededRunway: Natural) =
ensureRunway OutputStream(s, neededRunway)
let FileOutputVTable = OutputStreamVTable(
writeSync: proc (s: OutputStream, buf: pointer, bufLen: Natural)
{.nimcall, gcsafe, raises: [IOError, Defect].} =
var file = FileOutputStream(s).file
template fail =
raise newException(IOError, "Failed to write OutputStream page.")
if s.buffers != nil:
s.buffers.consumeAllPages(pageAddr, pageLen):
let written = file.writeBuffer(pageAddr, pageLen)
if written != pageLen: fail()
if bufLen > 0:
doAssert buf != nil
var written = file.writeBuffer(buf, bufLen)
if written != bufLen: fail()
,
flushSync: proc (s: OutputStream)
{.nimcall, gcsafe, raises: [IOError, Defect].} =
flushFile FileOutputStream(s).file
,
closeSync: proc (s: OutputStream)
{.nimcall, gcsafe, raises: [IOError, Defect].} =
close FileOutputStream(s).file
)
template vtableAddr*(vtable: OutputStreamVTable): ptr OutputStreamVTable =
## This is a simple work-around for the somewhat broken side
## effects analysis of Nim - reading from global let variables
## is considered a side-effect.
{.noSideEffect.}:
unsafeAddr vtable
proc fileOutput*(filename: string,
fileMode: FileMode = fmWrite,
pageSize = defaultPageSize): OutputStreamHandle
{.raises: [IOError, Defect].} =
let f = open(filename, fileMode)
makeHandle FileOutputStream(
vtable: vtableAddr FileOutputVTable,
buffers: initPageBuffers(pageSize),
file: f)
proc pos*(s: OutputStream): int =
s.spanEndPos - s.span.len
template pos*(s: AsyncOutputStream): int =
pos OutputStream(s)
#
# Pre-conditions for `drainAllBuffers(Sync/Async)`
# * The cursor has reached the current span end
# * We are working with a vtable-enabled stream
#
# Post-conditions:
# * All completed pages are written
# * There is a fresh page ready for writing at the top
# (we can reuse a previously existing page for this)
# * The stream cursor is re-initialized at the start of the top page
#
proc drainAllBuffersSync(s: OutputStream, buf: pointer, bufSize: Natural) =
s.vtable.writeSync(s, buf, bufSize)
if s.buffers != nil:
s.span = s.buffers.getWritableSpan()
s.spanEndPos += s.span.len
proc drainAllBuffersAsync(s: OutputStream, buf: pointer, bufSize: Natural) {.async.} =
await s.vtable.writeAsync(s, buf, bufSize)
s.span = s.buffers.getWritableSpan()
s.spanEndPos += s.span.len
proc createCursor(s: OutputStream, size: int): WriteCursor =
inc s.extCursorsCount
let
# The start address matches the current stream main cursor location
startAddr = s.span.startAddr
endAddr = offset(startAddr, size)
result = WriteCursor(
stream: s,
span: PageSpan(startAddr: startAddr, endAddr: endAddr))
# Adjust the stream main cursor to point past the end
# of the newly created cursor:
s.span.startAddr = endAddr
proc delayFixedSizeWrite*(s: OutputStream, size: Natural): WriteCursor =
let runway = s.span.len
if size <= runway:
result = createCursor(s, size)
else:
result = createCursor(s, runway)
let
runwayDeficit = size - runway
nextPageSize = nextAlignedSize(runwayDeficit, s.buffers.pageSize)
nextPage = s.buffers.addWritablePage(nextPageSize)
nextPageSpan = nextPage.writableSpan
s.span = PageSpan(startAddr: offset(nextPageSpan.startAddr, runwayDeficit),
endAddr: nextPageSpan.endAddr)
# See the explanation about split cursors above
nextPage.startOffset = -runwayDeficit
s.spanEndPos += nextPageSize
proc delayVarSizeWrite*(s: OutputStream, maxSize: Natural): VarSizeWriteCursor =
## Please note that using variable sized writes are not supported
## for unbuffered streams and unsafe memory inputs.
doAssert s.buffers != nil
let runway = s.span.len
if maxSize <= runway:
let
startAddr = s.span.startAddr
endAddr = offset(startAddr, maxSize)
result = VarSizeWriteCursor WriteCursor(
stream: s,
span: PageSpan(startAddr: startAddr, endAddr: endAddr))
s.buffers.splitLastPageAt(endAddr)
s.span.startAddr = endAddr
else:
s.buffers.endLastPageAt(s.span.startAddr)
let
nextPageSize = nextAlignedSize(maxSize, s.buffers.pageSize)
nextPageSpan = s.buffers.addWritablePage(nextPageSize).writableSpan
cursorEndAddr = offset(nextPageSpan.startAddr, maxSize)
result = VarSizeWriteCursor WriteCursor(
stream: s,
span: PageSpan(startAddr: nextPageSpan.startAddr,
endAddr: cursorEndAddr))
s.span = PageSpan(startAddr: cursorEndAddr,
endAddr: nextPageSpan.endAddr)
s.spanEndPos += nextPageSize
proc finalize*(cursor: var WriteCursor) =
doAssert cursor.stream.extCursorsCount > 0
dec cursor.stream.extCursorsCount
proc finalWrite*(cursor: var WriteCursor, data: openArray[byte]) =
doAssert data.len == cursor.span.len
copyMem(cursor.span.startAddr, unsafeAddr data[0], data.len)
finalize cursor
proc finalWrite*(c: var VarSizeWriteCursor, data: openArray[byte]) =
template cursor: auto = WriteCursor(c)
let overestimatedBytes = cursor.span.len - data.len
doAssert overestimatedBytes >= 0
for page in items(cursor.stream.buffers.queue):
let baseAddr = page.pageBaseAddr
if page.pageEndAddr == cursor.span.endAddr:
# This is a page ending cursor
page.endOffset = distance(baseAddr, cursor.span.startAddr) + data.len
copyMem(cursor.span.startAddr, unsafeAddr data[0], data.len)
finalize cursor
return
if cursor.span.startAddr == baseAddr:
# This is page starting cursor
page.startOffset = overestimatedBytes
copyMem(offset(baseAddr, overestimatedBytes), unsafeAddr data[0], data.len)
finalize cursor
return
doAssert false
proc tryMovingToNextPage(c: var WriteCursor) =
# A split cursor is a fixed-size cursor that ended up on page boundary.
#
# Part of the cursor used the last few bytes of the first page and we've
# left some empty space at the beginning of the second page.
#
# Even if the cursor size was very large, we've made sure the next
# page is big enough to hold all the data. When we created the cursor,
# we've taken a note regarding the number of bytes on the second page
# that are reserved by writing them as a negative value for the page
# `startOffset`.
#
# All we need to do here is update the cursor span to point to the next
# page and set the now final `endAddr`. The page `startOffset` is updated
# to 0 to indicate that the cursor has made the flip.
#
# If you are wondering, var-sized cursors cannot be split, because our
# strategy is to always place them at the beggining or end of pages.
#
# When we try to create a var-sized cursor, we check if there are enough
# bytes on the current page to contain the worst case scenario (the var
# sized cursor has an upper size limit). If there are enough bytes, we
# end the page prematurely (it will end up with an `endOffset`). We can
# then recycle the same memory for the next page that will use an adjusted
# `startOffset`. The `endOffset` of the first page will be written when
# the cursor is finalized and its final size becomes known.
#
# If there weren't enough bytes (a much more rare event), we allocate a
# new page. We adjust the `endOffset` of the current page to mark it's
# premature end and we mark the cursor as special by writing a
# The split cursor is definetely not on the last page, so we can iterate
# only over the preceeding pages to find where it was:
var prevPage = c.stream.buffers.queue[0]
for i in 1 ..< c.stream.buffers.queue.len:
let page = c.stream.buffers.queue[i]
if c.span.endAddr == prevPage.pageEndAddr and page.startOffset < 0:
# We found what we need, so let's get to business:
c.span.startAddr = page.pageBaseAddr
c.span.endAddr = offset(c.span.startAddr, -page.startOffset)
page.startOffset = 0
return
prevPage = page
# We didn't find any page that this cursor was ending, so this is not
# a split cursor. This means that the user just tried to write past the
# pre-allocated cursor span, which is considered a Defect (a range error)
doAssert false, "Attempt to write past the end of a cursor"
template flushImpl(s: OutputStream, awaiter, writeOp, flushOp: untyped) =
doAssert s.extCursorsCount == 0
if s.vtable != nil:
if s.buffers != nil:
s.buffers.endLastPageAt s.span.startAddr
awaiter s.vtable.writeOp(s, nil, 0)
s.span = s.buffers.getWritableSpan()
s.spanEndPos += s.span.len
if s.vtable.flushOp != nil:
awaiter s.vtable.flushOp(s)
proc flush*(s: OutputStream) =
flushImpl(s, noAwait, writeSync, flushSync)
template flush*(s: AsyncOutputStream) =
let s = sp
flushImpl(s, fsAwait, writeAsync, flushAsync)
template writeByteImpl(s: OutputStream, b: byte, awaiter, writeOp, drainOp: untyped) =
if s.span.atEnd:
# Unsafe memory outputs don't use pages at all, so if our cursor
# reached here, this is a range violation defect:
doAssert canExtendOutput(s)
if s.vtable == nil or s.extCursorsCount > 0:
# This is the main cursor of a stream, but we are either not
# ready to flush due to outstanding delayed writes or this is
# just a memory output stream. In both cases, we just need to
# allocate more memory and continue writing:
addPage(s)
elif s.buffers == nil:
awaiter s.vtable.writeOp(nil, unsafeAddr b, 1)
else:
awaiter drainOp(s, nil, 0)
writeByte(s.span, b)
proc write*(c: var WriteCursor, b: byte) =
if c.span.atEnd:
# The cursor has reached the end of its buffer, but it may be a
# split cursor. If that's the case, the following function will
# succeed. If that's not a split cursor, we'll raise a Defect.
tryMovingToNextPage(c)
writeByte(c.span, b)
proc write*(s: OutputStream, b: byte) =
writeByteImpl(s, b, noAwait, writeSync, drainAllBuffersSync)
template write*(s: AsyncOutputStream, b: byte) =
# TODO: I should do something with the write async Futures
bind write
write OutputStream(s)
template writeAndWait*(sp: AsyncOutputStream, b: byte) =
let s = sp
writeByteImpl(s, b, fsAwait, writeAsync, drainAllBuffersAsync)
template write*(s: OutputStream|AsyncOutputStream|var WriteCursor, x: char) =
bind write
write s, byte(x)
proc writeToANewPage(s: OutputStream, bytes: openArray[byte]) =
var
runway = s.span.len
inputPos = unsafeAddr bytes[0]
inputLen = bytes.len
template reduceInput(delta: int) =
inputPos = offset(inputPos, delta)
inputLen -= delta
if runway > 0:
copyMem(s.span.startAddr, inputPos, runway)
reduceInput runway
doAssert s.buffers != nil
let nextPageSize = nextAlignedSize(inputLen, s.buffers.pageSize)
let nextPage = s.buffers.addWritablePage(nextPageSize)
s.span = nextPage.writableSpan
s.spanEndPos += s.span.len
copyMem(s.span.startAddr, inputPos, inputLen)
s.span.startAddr = offset(s.span.startAddr, inputLen)
template writeBytesImpl(s: OutputStream,
bytes: openArray[byte],
drainOp: untyped) =
let inputLen = bytes.len
if inputLen == 0: return
# We have a short inlinable function handling the case when the input is
# short enough to fit in the current page. We'll keep buffering until the
# page is full:
let runway = s.span.len
if inputLen <= runway:
copyMem(s.span.startAddr, unsafeAddr bytes[0], inputLen)
s.span.startAddr = offset(s.span.startAddr, inputLen)
elif s.vtable == nil or s.extCursorsCount > 0:
# We are not ready to flush, so we must create pending pages.
# We'll try to create them as large as possible:
s.writeToANewPage(bytes)
else:
s.buffers.endLastPageAt(s.span.startAddr)
drainOp
proc write*(s: OutputStream, bytes: openArray[byte]) =
writeBytesImpl(s, bytes):
drainAllBuffersSync(s, unsafeAddr bytes[0], bytes.len)
proc write*(s: OutputStream, chars: openArray[char]) =
write s, charsToBytes(chars)
proc write*(s: OutputStream, value: string) {.inline.} =
write s, value.toOpenArrayByte(0, value.len - 1)
template memCopyToBytes(value: auto): untyped =
type T = type(value)
static: assert supportsCopyMem(T)
let valueAddr = unsafeAddr value
makeOpenArray(cast[ptr byte](valueAddr), sizeof(T))
proc writeMemCopy*(s: OutputStream, value: auto) =
bind write
write s, memCopyToBytes(value)
proc writeBytesAsyncImpl(sp: AsyncOutputStream,
bytes: openarray[byte]): Future[void] =
let s = OutputStream(sp)
writeBytesImpl(s, bytes):
return s.vtable.writeAsync(s, unsafeAddr bytes[0], bytes.len)
proc writeBytesAsyncImpl(s: AsyncOutputStream,
chars: openarray[char]): Future[void] =
writeBytesAsyncImpl s, charsToBytes(chars)
proc writeBytesAsyncImpl(s: AsyncOutputStream,
str: string): Future[void] =
writeBytesAsyncImpl s, toOpenArray(str, 0, str.len - 1)
template writeAndWait*(sp: AsyncOutputStream, value: auto) =
bind writeBytesAsyncImpl
let
s = sp
f = writeBytesAsyncImpl(s, value)
if f != nil:
fsAwait(f)
s.span = s.buffers.getWritableSpan()
s.spanEndPos += s.span.len
template writeMemCopyAndWait*(sp: AsyncOutputStream, value: auto) =
writeAndWait(sp, memCopyToBytes(value))
proc writeBytesToCursor(c: var WriteCursor, bytes: openarray[byte]) =
var
runway = c.span.len
inputPos = unsafeAddr bytes[0]
inputLen = bytes.len
template reduceInput(delta: int) =
inputPos = offset(inputPos, delta)
inputLen -= delta
if inputLen <= runway:
copyMem(c.span.startAddr, inputPos, inputLen)
c.span.startAddr = offset(c.span.startAddr, inputLen)
else:
# This must be a split cursor. We need to complete its first page first,
# then switch to the second and continue the write there.
copyMem(c.span.startAddr, unsafeAddr bytes[0], runway)
reduceInput runway
# If this really is a split cursor, the following operation will succeed.
# Otherwise, it will Defect and the conclusion is that this was a write
# past the cursor end.
c.tryMovingToNextPage()
# On the next page, we have a new runway
runway = c.span.len
# The write shouldn't go past the end of the new runway
doAssert inputLen <= runway
copyMem(c.span.startAddr, inputPos, inputLen)
c.span.startAddr = offset(c.span.startAddr, inputLen)
template write*(c: var WriteCursor, bytes: openarray[byte]) =
bind writeBytesToCursor
writeBytesToCursor(c, bytes)
proc write*(c: var WriteCursor, chars: openarray[char]) {.inline.} =
var charsStart = unsafeAddr chars[0]
writeBytesToCursor(c, makeOpenArray(cast[ptr byte](charsStart), chars.len))
proc writeMemCopy*[T](c: var WriteCursor, value: T) =
bind writeBytesToCursor
writeBytesToCursor(c, memCopyToBytes(value))
proc write*(c: var WriteCursor, str: string) =
writeBytesToCursor(c, str.toOpenArrayByte(0, str.len - 1))
template consumeOutputs*(sp: OutputStream, bytesVar, body: untyped) =
## Please note that calling `consumeOutputs` on an unbuffered stream
## or an unsafe memory stream is considered a Defect.
##
## Before consuming the outputs, all outstanding delayed writes must be finalized.
let s = sp
doAssert s.extCursorsCount == 0 and s.buffers != nil
consumeAllPages(s.buffers, pageStartAddr, pageLen):
template bytesVar: untyped =
makeOpenArray(pageStartAddr, pageLen)
body
template consumeContiguousOutput*(sp: OutputStream, bytesVar, body: untyped) =
## Please note that calling `consumeContiguousOutput` on an unbuffered stream
## or an unsafe memory stream is considered a Defect.
##
## Before consuming the output, all outstanding delayed writes must be finalized.
##
# TODO: This code is a bit too much to be inlined. Maybe this should be a proc
# with a callback, but this will restrict the types of variables it can write to.
# OTOH, perhaps only `consumeAllPages` is the offending part.
var
s = sp
contigiousBytes: string # this may remain null
bytesPtr: ptr byte
bytesLen: int
doAssert s.extCursorsCount == 0 and s.buffers != nil
if s.buffers.queue.len == 1:
let page = s.buffers.queue[0]
bytesPtr = page.pageStartAddr
bytesLen = page.endOffset - pageStartOffset
# We need to reset the page to an empty state, so it can be reused
page.startOffset = 0
page.endOffset = 0
else:
contigiousBytes = newStringOfCap(s.pos)
consumeAllPages(s.buffers, pageStartAddr, pageLen):
contigiousBytes.add makeOpenArray(cast[ptr char](pageStartAddr), pageLen)
bytesPtr = addr contigiousBytes[0]
bytesLen = contigiousBytes.len
template bytesVar: untyped =
makeOpenArray(bytesPtr, bytesLen)
body
proc getOutput*(s: OutputStream, T: type string): string =
## Please note that calling `getOutput` on an unbuffered stream
## or an unsafe memory stream is considered a Defect.
##
## Before consuming the output, all outstanding delayed writes must be finalized.
##
doAssert s.extCursorsCount == 0 and s.buffers != nil
s.buffers.endLastPageAt s.span.startAddr
if s.buffers.queue.len == 1:
let page = s.buffers.queue[0]
if page.kind == stringPage and page.startOffset == 0:
result.swap page.data[]
result.setLen page.endOffset
# We clear the buffers, so the stream will be in pristine state.
# The next write is going to create a fresh new starting page.
s.buffers.queue.clear()
return
result = newStringOfCap(s.pos)
for page in items(s.buffers.queue):
result.add page.pageChars
template getOutput*(s: OutputStream, T: type seq[byte]): seq[byte] =
cast[seq[byte]](s.getOutput(string))
template getOutput*(s: OutputStream): seq[byte] =
cast[seq[byte]](s.getOutput(string))

View file

@ -1,9 +1,9 @@
import
macros,
input_stream, output_stream
inputs, outputs
export
input_stream, output_stream
inputs, outputs
macro executePipeline*(start: InputStream, steps: varargs[untyped]) =
var input = start
@ -21,7 +21,7 @@ macro executePipeline*(start: InputStream, steps: varargs[untyped]) =
`step`(`input`, `outputVar`)
input = quote do:
memoryInput(getOutput(`outputVar`))
unsafeMemoryInput(getOutput(`outputVar`))
if defined(debugMacros) or defined(debugPipelines):
echo result.repr

View file

@ -0,0 +1,2 @@
import
async_backend

5
faststreams/stdin.nim Normal file
View file

@ -0,0 +1,5 @@
import
inputs
let fsStdIn* {.threadvar.} = fileInput(system.stdin)

5
faststreams/stdout.nim Normal file
View file

@ -0,0 +1,5 @@
import
outputs
let fsStdOut* {.threadvar.} = fileOutput(system.stdout)

92
faststreams/textio.nim Normal file
View file

@ -0,0 +1,92 @@
import
stew/ptrops,
inputs, outputs, buffers
# The following code implements writing numbers to a stream without going
# through Nim's `$` operator which will allocate memory.
# It's based on some speed comparisons of different methods presented here:
# http://www.zverovich.net/2013/09/07/integer-to-string-conversion-in-cplusplus.html
# TODO Maybe the `writeText` proc shouldn't be instantiated for every integer
# type, but only for the largest "native" one. We can promote the rest with
# a template.
const
digitsTable = block:
var s = ""
for i in 0..99:
if i < 10: s.add '0'
s.add $i
s
maxLen = ($BiggestInt.high).len + 4 # null terminator, sign
proc writeText*(s: OutputStream, x: SomeUnsignedInt) =
var
num: array[maxLen, char]
pos = num.len
template writeByteInReverse(c: char) =
dec pos
num[pos] = c
var val = x
while val > 99:
# Integer division is slow so do it for a group of two digits instead
# of for every digit. The idea comes from the talk by Alexandrescu
# "Three Optimization Tips for C++".
let base100digitIdx = (val mod 100) * 2
val = val div 100
writeByteInReverse digitsTable[base100digitIdx + 1]
writeByteInReverse digitsTable[base100digitIdx]
when true:
if val < 10:
writeByteInReverse char(ord('0') + val)
else:
let base100digitIdx = val * 2
writeByteInReverse digitsTable[base100digitIdx + 1]
writeByteInReverse digitsTable[base100digitIdx]
else:
# Alternative idea:
# We now know enough to write digits directly to the stream.
if val < 10:
write s, byte(ord('\0') + val)
else:
let base100digitIdx = val * 2
write s, digitsTable[base100digitIdx]
write s, digitsTable[base100digitIdx + 1]
write s, num.toOpenArray(pos, static(num.len - 1))
proc writeText*(s: OutputStream, x: SomeSignedInt) =
# TODO: Determine this accurately
type MatchingUInt = BiggestUInt
if x < 0:
s.write '-'
# The `0 - x` trick below takes care of one corner case:
# How do we get the abs value of low(int)?
# The naive `-x` triggers an overflow, because low(int8)
# is -128, while high(int8) is 127.
writeText(s, MatchingUInt(0) - MatchingUInt(x))
else:
writeText(s, MatchingUInt(x))
template writeText*(s: OutputStream, str: string) =
write s, str
template writeText*(s: OutputStream, val: auto) =
write s, $val
proc writeHex*(s: OutputStream, bytes: openarray[byte]) =
const hexChars = "0123456789abcdef"
for b in bytes:
s.write hexChars[int b shr 4 and 0xF]
s.write hexChars[int b and 0xF]
proc writeHex*(s: OutputStream, chars: openarray[char]) =
writeHex s, charsToBytes(chars)