write file to stdout
Just for fun let's try and write a low level program using mmap to read a file and write it to stdout.
requires: 'libc'.
[main: arguments]
[filename := arguments[0] else| panic.
// Open file or die
fd := (libc open: filename flags: O_RDONLY) else| panic.
--- [libc close: fd].
// Get file size or die
file_size := libc stat: filename else| panic | [st_size].
// Map file or die
data := (libc mmap: NULL length: file_size prot: PROT_READ flags: MAP_PRIVATE fd: fd offset: 0) else| panic.
--- [libc munmap: data length: file_size].
// Output to stdout or die
(libc fwrite: data size: 1 count: file_size stream: libc stdout) else| panic]
Abstractions can be built up around this but it's important to me that STZ can hang with the low level programming crowd and not get in its own way. In this version I changed ... defer statements to use — so that ... is only used for array and map combining.
else| panic is used to write out the failure side of the Maybe to stderr and quit the program as there is nothing sensible that can be done from that moment on. For friendlier error messages we might make some kind of assert| that will print out the error to stderr or the current logger.
requires: 'libc'.
[main: arguments]
[filename := arguments[0]
assert| [e| `Provide a file to operate on as the first parameter of the command line: ~[e]`]
else| panic.
// Open file or die
fd := (libc open: filename flags: O_RDONLY)
assert| [e| `File not found: ~[e]`]
else| panic.
--- [libc close: fd].
// Get file size or die
file_stat := libc stat: filename
assert| [e| `Access denied: ~[e]`]
else| panic.
file_size := file_stat st_size.
// Map file or die
data := (libc mmap: NULL length: file_size prot: PROT_READ flags: MAP_PRIVATE fd: fd offset: 0) else| panic.
--- [libc munmap: data length: file_size].
// Output to stdout or die
(libc fwrite: data size: 1 count: file_size stream: libc stdout) else| panic]
The nice thing about else| and assert| is that if the receiver isn't in failure state you resolve to the success value automatically unwrapping it.
If we lean more heavily on abstractions this program becomes very simple. Trivial even. It does introduce the notion of a 'leaky abstraction' in that we aren't customising our failure responses. But no actual failures are lost or ignored in this code. It is extremely bullet proof.
[main: arguments]
[arguments[0]
| [as-filename]
| [memory-map: ReadOnly] --- [close]
| stdout
else| panic]
At any point in this chain one of these operations could fail which would skip the remaining links in the chain until the else| which would raise a panic. Otherwise the data will flow neatly from file to stdout without any interruption.
There is an implied data structure in the middle there of a MemoryMappedFileDescriptor which has both an fd and a data inside of it that it will release on close.
We can make this more user friendly again by adding in at the very least an assert on the first arguments[0] call. We should also return 0 if all is well. We can do this by either returning the integer ourselves or saying there is no return value in the method:
[main: arguments] [arguments -> ø |
arguments[0]
assert| "Provide an argument on the command line"
else| panic
| [as-filename]
| [memory-map: ReadOnly] --- [close]
| stdout
else| panic]
And while we're at it let's allow it to fail gracefully after it has successfully opened the file, just in case the file happens to be a network resource at some later point in the programs evolution:
[main: arguments] [arguments -> return: Integer |
arguments[0]
assert| "Provide an argument on the command line"
else| [return -1]
| [as-filename]
| [memory-map: ReadOnly] --- [close]
else| panic
| stdout
else| stderr
else| [return -2]
return 0]