# Input/Output
The sys/io subsystem offers a unified API to read and write any device or
filesystem. All words in this API revolve around a structure that we call the
"IO handle". This handle represent one "place" we read and write to, and if
needed save any state needed to fulfill the API's specification.
I/O operations are byte based.
When "EOF" is mentioned, it means "end of file". In some cases, it's a misnomer,
but the idea is the same: if all possible data from the I/O source have been
consumed, we're in EOF condition.
Position management is opaque to the I/O words.
The structure of these handles will vary depending on the driver or filesystem,
but they all start with the same structure:
putback
A holder of the "putback" value. If nonzero, the next call to :getc will
return this instead of actually reading from the buffer.
:readbuf ( n hdl -- a? read-n )
Try to read "n" bytes from hdl at current position. Unless EOF is reached, at
least 1 byte must be read, but otherwise "read-n" can be lower than n, even
if that doesn't take the handle to EOF. The idea is to take the handle, at
most, to the end of its internal buffer. Return the number of bytes read and,
if it was nonzero, return an address to the beginning of that buffer.
After the read operation, advance current position by "read-n" bytes.
:writebuf ( a n hdl -- written-n )
Try to write "n" bytes from buffer "a" to hdl at current position, growing
the file if needed. The idea is the same as with readbuf: the filesystem can
proceed as is best for its implementation, as long as it writes at least 1
byte. If no data can be written, return 0.
After the write operation, advance current position by "written-n" bytes.
:flush ( hdl -- )
If the hdl's buffer is "dirty" (has been changed compared to the permanent
storage it represents), save that data to permanent storage and flag the
handle as clean.
:close ( hdl -- )
Close hdl and free its resources. When a handle is closed, operations on
it become noops (read return 0, writes/seeks do nothing)
In addition to those device-specific words, all IO structures share these
convenience words:
:write ( a n hdl -- )
Repeatedly call :writebuf until all requested bytes have been written.
:getc ( hdl -- c )
Read 1 byte from hdl an return it. Advance position by 1 byte. Return -1 on
EOF. If putback is nonzero, return this value instead, and reset putback to 0.
:putc ( c hdl -- )
Write 1 byte to hdl. Advance position by 1 byte. Aborts if unable to write.
:puts ( str hdl -- )
Write str to hdl.
:putback ( c hdl -- )
Set the putback value to c.
:readline ( hdl -- str-or-0 )
read stdin for a maximum of STR_MAXSZ-1 characters until LF or EOF is
encountered, then return a string representing that read line. The LF
character is not included. Aborts on LNSZ overflow. The string can be zero in
length if LF is encountered right away. When EOF is encountered in the middle
of the string, return the string. When it's encountered at the beginning,
return 0 to indicate an absence of result.
:spit ( dst hdl -- )
Until EOF is reached, read "hdl" and write its contents to other IO "dst".
## SumIO
The SumIO struct allows you to "spit" another IO into it and extract a result.
It is created using a word with the signature ( res c -- res ) and whenever data
is written to it, it is applied to this function to accumulate a result. It has
the following fields:
fn ( hdl -- 'fn )
Points to a ( res c -- res ) word that is applied on incoming data.
res ( hdl -- res )
Where the result is stored.
:new ( initres 'fn -- hdl )
Create a new SumIO.
If, for example, you want to create a CRC32 calculator, you'd do:
' crc32 SumIO :new const mycrc32io
-1 mycrc32io to SumIO res
mycrc32io otheriohandle IO :spit
mycrc32io SumIO res ^ .x \ prints crc32 of otheriohandle's contents
## SystemIn and SystemOut
SystemIn and SystemOut are structures that wrap around "key" and "emit",
providing an I/O API around them.
## ConsoleIn and ConsoleOut
ConsoleIn and ConsoleOut are values that, by default, point to SystemIn and
SystemOut. When sys/rdln is used, ConsoleIn points to RdlnIn.
Once loaded, the system interpret loop feeds itself from ConsoleIn.
There is a "consoleecho" value, which is initialized to 0. When set to 1, all
characters being read through ConsoleIn are echoed through "emit". This can be
useful for debugging, and it's also used in the test harness.
## StdIn and StdOut
StdIn and StdOut are values at the top of our I/O indirection pyramid. By
default, they point to ConsoleIn and ConsoleOut, but you will often want to
redirect them to somewhere else when running higher level words. At this level,
you can redirect those aliases freely without breaking the interpret loop, but
at the same time, control the flow of most higher level I/Os.
"stdin" is a shortcut for "StdIn IO :getc"
"stdout" is a shortcut for "StdOut IO :putc"
"stdio$" sets StdIn to ConsoleIn and StdOut to ConsoleOut.
## I/O and file loading
When loading a file to be interpreted by the system (with ":fload" or "f<<"),
*both* ConsoleIn and StdIn are set to the handle of the file being read and
those redirections are expected to stay as-is while the file is loaded. For this
reason, unless you know what you are doing, you shouldn't redirect StdIn at the
"interpret" level of a Forth source file.
## printf
An interesting augmentation of the I/O subsystem is implemented in lib/fmt.fs,
IO :printf ( ... fmt hdl -- ). It takes a format string, for example "hello %d"
which contains placeholder elements identified by the character '%' followed by
a letter.
That formatting string must be preceded by a number of arguments equal to the
number of placeholders in the string, in reverse order. Example:
3 2 1 S" hello %d %d %d" StdOut IO :printf --> prints "hello 1 2 3"
The letter following the '%' chars determines how the argument is formatted and
these letters are supported (others produce an error):
d - decimal
x - 4b hexadecimal
w - 2b hexadecimal
b - 1b hexadecimal
s - string