Recipes¶
Short programs for the things a caller is most likely to want first. Every
block on this page is compiled against this package by check-examples in
CI, so the names and signatures in them are the ones the package has — but
they are built, not run, so a program that compiles is a claim about the
API, not a promise about its output.
Why a call failed¶
C's answer to failure is a special return value — often NULL — plus a
number in errno saying which failure it was. strerror turns the number
into a sentence:
from libc.dirent import opendir
from libc.errno import get_errno
from libc.string import strerror
from libc.utils import char_ptr_to_string, is_null, string_to_char_ptr
def main() raises:
var path = string_to_char_ptr("/does/not/exist")
var dirp = opendir(path)
if is_null(dirp):
print("opendir:", char_ptr_to_string(strerror(get_errno())))
path.unsafe_free()
errno only says what the last call did, so read it before anything else
has the chance to fail.
Reading a file¶
A FILE stream from fopen, a buffer from malloc, and fread to fill
it. fread answers how much it read rather than failing, so there is no
errno to check here:
from libc.file import FILE, fclose, fopen, fread
from libc.stdlib import free, malloc
from libc.utils import is_null, string_to_char_ptr
def main() raises:
var path = string_to_char_ptr("docs/recipes.md")
var mode = string_to_char_ptr("rb")
var stream = fopen(path, mode)
if is_null(stream):
print("could not open the file")
return
var room = malloc(4096)
var got = fread(room, 1, 4096, stream)
print("read", got, "bytes")
_ = fclose(stream)
_ = free(room)
path.unsafe_free()
mode.unsafe_free()
Running a program and waiting for it¶
fork makes a copy of the process, execvp replaces the copy with a
program found by name, and waitpid in the parent collects how it ended.
The argument list is a char **: the strings, then a null to say where
they end:
from std.memory.alloc import unsafe_alloc
from libc.constant import c_ptr
from libc.stdlib import exit
from libc.sys.wait import WEXITSTATUS, WIFEXITED, waitpid
from libc.unistd import execvp, fork
from libc.utils import null_ptr, string_to_char_ptr
def main() raises:
var pid = fork()
if pid == 0:
# The child: become `echo`, found by name along the PATH.
# execvp only comes back when it failed.
var program = string_to_char_ptr("echo")
var argv = unsafe_alloc[c_ptr[UInt8]](3)
argv.unsafe_offset(0)[] = string_to_char_ptr("echo")
argv.unsafe_offset(1)[] = string_to_char_ptr("from a child process")
argv.unsafe_offset(2)[] = null_ptr[UInt8]()
_ = execvp(program, argv)
_ = exit(1)
# The parent: wait, then ask which kind of ending it was.
var status = unsafe_alloc[Int32](1)
_ = waitpid(pid, status, 0)
if WIFEXITED(status.unsafe_offset(0)[]):
print("the child exited with", WEXITSTATUS(status.unsafe_offset(0)[]))
status.unsafe_free()
A child that exits without being waited for stays as a zombie until the
parent collects it, so the waitpid half is not optional.
The time, written out¶
clock_gettime reads a clock into a timespec — whole seconds and then
nanoseconds — localtime_r breaks the seconds down against the machine's
timezone into a tm, and strftime writes that out in whatever shape is
asked for:
from std.memory.alloc import unsafe_alloc
from libc.constant import c_long
from libc.time import CLOCK_REALTIME, clock_gettime, localtime_r, strftime, timespec, tm
from libc.utils import char_ptr_to_string, string_to_char_ptr
def main() raises:
var now = unsafe_alloc[timespec](1)
now[] = timespec()
_ = clock_gettime(CLOCK_REALTIME, now)
var seconds = unsafe_alloc[c_long](1)
seconds[] = now[].tv_sec
var broken = unsafe_alloc[tm](1)
broken[] = tm()
_ = localtime_r(seconds, broken)
var text = unsafe_alloc[UInt8](64)
var format = string_to_char_ptr("%Y-%m-%d %H:%M:%S")
_ = strftime(text, 64, format, broken)
print("now:", char_ptr_to_string(text))
format.unsafe_free()
text.unsafe_free()
broken.unsafe_free()
seconds.unsafe_free()
now.unsafe_free()
localtime_r is the one to reach for: localtime hands every caller the
same single tm, so two calls overwrite each other's answer.
Asking where a symbol is¶
dlsym takes a name and answers an address; dladdr takes the address
and answers which object it is in and the name of the nearest symbol at or
below it. Between them, a name can be shown to be real without anything
being called:
from std.memory.alloc import unsafe_alloc
from std.sys import CompilationTarget
from libc.constant import c_void
from libc.dlfcn import RTLD_NOW, dl_info, dladdr, dlclose, dlopen, dlsym
from libc.utils import char_ptr_to_string, string_to_char_ptr
def main() raises:
# The two platforms spell the C library differently.
comptime LIBRARY = "libc.dylib" if CompilationTarget.is_macos() else "libc.so.6"
var handle = dlopen(string_to_char_ptr(LIBRARY), RTLD_NOW)
var address = dlsym(handle, string_to_char_ptr("getpid"))
var info = unsafe_alloc[dl_info](1)
if dladdr(address.unsafe_bitcast[c_void](), info) != 0:
print(
char_ptr_to_string(info[].dli_sname),
"is in",
char_ptr_to_string(info[].dli_fname),
)
info.unsafe_free()
_ = dlclose(handle)
What the name comes back as can be spelled two ways: libc exports the
same address under the public name and under an underscored one, and the
loader reports whichever its linker settled on — getpid on one build
and __getpid on another. The address is what identifies the symbol; the
name is one of the things it is called.
The addresses of this machine's interfaces¶
getifaddrs answers a linked list of one record per address the machine
has, walked through ifa_next until it runs out. The flags on each record
say what the interface is, and the loopback is the one to skip when what
is wanted is the addresses that reach other machines:
from std.memory.alloc import unsafe_alloc
from libc.constant import c_ptr
from libc.ifaddrs import IFF_LOOPBACK, freeifaddrs, getifaddrs, ifaddrs
from libc.utils import is_null, null_ptr
def main() raises:
var list = unsafe_alloc[c_ptr[ifaddrs]](1)
list.unsafe_offset(0)[] = null_ptr[ifaddrs]()
if getifaddrs(list) != 0:
print("getifaddrs failed")
return
var node = list[]
while not is_null(node):
if (Int(node[].ifa_flags()) & IFF_LOOPBACK) == 0:
print(node[].ifa_name())
node = node[].ifa_next()
freeifaddrs(list[])
list.unsafe_free()
The list is one allocation from the kernel, and freeifaddrs gives the
whole of it back — there is nothing to free per record. One platform
difference worth knowing before reading ifa_dstaddr: on a point-to-point
link that member is the address at the far end, and glibc fills the
loopback's with the loopback's own address, so a destination that equals
the address does not mean the record is wrong.
Looking up an account¶
getpwnam takes a name and answers a row of the account database; the
row lives in a buffer the next call to the same functions will reuse, so
read it before asking again:
from libc.pwd import getpwnam
from libc.utils import is_null, string_to_char_ptr
def main() raises:
var name = string_to_char_ptr("root")
var row = getpwnam(name)
if is_null(row):
print("no such account")
return
print(row[].pw_name(), "is uid", Int(row[].pw_uid()), "with home", row[].pw_dir())
name.unsafe_free()
The password member is a placeholder, not a password: the password left
this row long ago — to shadow on Linux, which keeps x there, and out
of the file on Darwin, which keeps *. Three more members — the time the
password was last changed, the time the account expires, and the login
class — exist on Darwin's struct and not on Linux's; the accessors answer
0 and nothing on Linux rather than reading a slot that holds another
member.
Giving a program a terminal to talk to¶
forkpty is openpty, login_tty and fork in one call: the child
starts life with a pseudo-terminal's slave on descriptors 0, 1 and 2, and
the parent holds the master. The master is the terminal's other end, so
what the child prints arrives there and what is written there is the
child's input.
from std.memory.alloc import unsafe_alloc
from libc.constant import c_char, c_int, c_ptr
from libc.pty import Winsize, forkpty
from libc.stdlib import exit
from libc.sys.wait import waitpid
from libc.termios import Termios
from libc.unistd import close, read, write
from libc.utils import null_ptr, string_to_char_ptr
comptime SAID = "the child has a terminal\n"
def main() raises:
var master = unsafe_alloc[c_int](1)
master.unsafe_offset(0)[] = -1
var child = forkpty(
master, null_ptr[c_char](), null_ptr[Termios](), null_ptr[Winsize]()
)
if child == 0:
# The slave is already on 0, 1 and 2 here: a program that insists
# on a terminal has one, and does not have to be told.
var said = string_to_char_ptr(SAID)
_ = write(1, said, UInt32(SAID.byte_length()))
_ = exit(0)
var m = master.unsafe_load()
var block = unsafe_alloc[c_char](128)
var got = read(m, block, 128)
while got > 0:
print(
String(unsafe_from_utf8=Span[UInt8](unsafe_ptr=block, length=Int(got))),
end="",
)
got = read(m, block, 128)
var status = unsafe_alloc[Int32](1)
status.unsafe_offset(0)[] = 0
_ = waitpid(child, status, 0)
_ = close(m)
block.unsafe_free()
status.unsafe_free()
master.unsafe_free()
Two things happen to those bytes on the way. The terminal translates as
well as carries: at its default settings ONLCR turns each newline the
child writes into a carriage return and a newline, so a child that wrote
twenty-five bytes has a parent reading twenty-six. And there is no end of
file to read while anything still holds the slave open -- the loop above
ends because the child exited, and a pair taken with openpty whose
slave the parent forgot to close has the parent waiting for that zero
forever. forkpty closes the slave in the parent for exactly that
reason, and login_tty makes the child a session leader so that closing
the master hangs the terminal up and sends it SIGHUP -- which is how a
program that will not read what is sent to it is stopped.
One thing not to use to tell the two ends apart: isatty says yes for
both. The line discipline is on both ends, and the master answers
tcgetattr as readily as the slave does.
Two sockets on the loopback¶
A connection is made of an address and two sockets: one bound to the
address and listening, one asking for it. socketaddr is the sixteen
bytes C passes as a struct sockaddr whatever the family is, and it owns
them, so it is filled in place rather than handed back. set_family
writes Darwin's sa_len and the family where the two platforms keep
them, set_inet_port writes the port in the order the wire counts in,
and set_inet_address writes the four bytes as in_addr holds them.
from std.memory.alloc import unsafe_alloc
from libc.constant import AF_INET, SOCK_STREAM, c_char
from libc.network import in_addr, inet_addr, inet_ntoa
from libc.sys.socket import (
accept,
bind,
connect,
getsockname,
listen,
socket,
socketaddr,
)
from libc.unistd import close, read, write
from libc.utils import char_ptr_to_string, string_to_char_ptr
comptime ADDRESS_SIZE: UInt32 = 16
def at_the_loopback(mut address: socketaddr, port: UInt16) -> None:
"""Fills `address` in as the loopback on `port`.
The struct owns its sixteen bytes, so it is filled in place rather
than handed back, and the four bytes of the address come from C's
own parser rather than from arithmetic here -- which is the short
way of saying 127.0.0.1 without also saying what order its bytes are
in.
Args:
address: The sixteen bytes to fill in.
port: The port, or 0 to let the kernel choose one.
"""
address = socketaddr()
address.set_family(UInt16(AF_INET))
address.set_inet_port(port)
var loopback = in_addr()
loopback.s_addr = inet_addr(string_to_char_ptr("127.0.0.1"))
address.set_inet_address(loopback)
def main() raises:
var listener = socket(AF_INET, SOCK_STREAM, 0)
var here = unsafe_alloc[socketaddr](1)
at_the_loopback(here.unsafe_offset(0)[], 0)
_ = bind(listener, here, ADDRESS_SIZE)
_ = listen(listener, 1)
# Which port 0 turned out to be is only knowable by asking.
var chosen = unsafe_alloc[socketaddr](1)
var size = unsafe_alloc[UInt32](1)
size.unsafe_offset(0)[] = ADDRESS_SIZE
_ = getsockname(listener, chosen, size)
var port = chosen.unsafe_offset(0)[].inet_port()
print("listening on", port)
var caller = socket(AF_INET, SOCK_STREAM, 0)
var there = unsafe_alloc[socketaddr](1)
at_the_loopback(there.unsafe_offset(0)[], port)
_ = connect(caller, there, ADDRESS_SIZE)
# `accept` writes whoever asked into a buffer of its own.
var who = unsafe_alloc[socketaddr](1)
var length = unsafe_alloc[UInt32](1)
length.unsafe_offset(0)[] = ADDRESS_SIZE
var taken = accept(listener, who, length)
var caller_text = char_ptr_to_string(
inet_ntoa(who.unsafe_offset(0)[].inet_address())
)
print("connected from", caller_text)
var said = string_to_char_ptr("hello\n")
_ = write(caller, said, 6)
var heard = unsafe_alloc[c_char](64)
var got = read(taken, heard, 64)
print("the other end read", got, "bytes")
_ = close(caller)
_ = close(taken)
_ = close(listener)
heard.unsafe_free()
length.unsafe_free()
who.unsafe_free()
there.unsafe_free()
chosen.unsafe_free()
size.unsafe_free()
here.unsafe_free()
Two things are worth knowing before the code. A port of 0 is not a port:
it asks the kernel to choose one, and getsockname is the only way to
learn which one it picked -- which is why nothing here can collide with
anything else on the machine, and why the second address is built from a
port that was already answered for rather than from a number guessed at.
And accept hands back a socket of its own, because the listener's job
is to keep listening; the address it fills in alongside is the caller's,
not the listener's, which is the one place a connection's two ends are
told apart by something other than which descriptor is which.
A socket with a name instead of a port¶
An AF_UNIX socket does not leave the machine, and its address is a path
rather than a machine and a port. That makes it the easiest way for two
programs on one machine to talk, and it makes its address longer than a
socketaddr has room for: C gives it a struct of its own, 110 bytes here
and 106 on Darwin, where a one-byte length comes first and the path has
104 bytes rather than 108.
sockaddr_un is a view of a block rather than a struct holding one,
because the calls that read the address are handed a pointer into the
block and would not see a copy. So the block is allocated first, and every
address in the conversation is built over one.
from std.memory.alloc import unsafe_alloc
from libc.constant import AF_UNIX, SOCK_STREAM, c_char
from libc.sys.socket import accept, bind, connect, listen, recv, send, socket
from libc.sys.un import SOCKADDR_UN_BYTES, sockaddr_un
from libc.unistd import close, getpid, unlink
from libc.utils import string_to_char_ptr
def main() raises:
var name = "/tmp/libc-recipe-" + String(Int(getpid())) + ".sock"
_ = unlink(string_to_char_ptr(name))
# The address is a view of this block, which has to outlive every call
# that is given the pointer.
var here_block = unsafe_alloc[UInt8](SOCKADDR_UN_BYTES)
var here = sockaddr_un(here_block)
here.set_path(name)
var listener = socket(AF_UNIX, SOCK_STREAM, 0)
_ = bind(listener, here.as_socketaddr(), here.length())
_ = listen(listener, 1)
print("listening on", name)
var caller = socket(AF_UNIX, SOCK_STREAM, 0)
_ = connect(caller, here.as_socketaddr(), here.length())
# `accept` writes the caller's address into a block of its own.
var there_block = unsafe_alloc[UInt8](SOCKADDR_UN_BYTES)
var there = sockaddr_un(there_block)
var size = unsafe_alloc[UInt32](1)
size.unsafe_offset(0)[] = there.length()
var taken = accept(listener, there.as_socketaddr(), size)
print("the caller's name is", there.path().byte_length(), "bytes long")
var said = string_to_char_ptr("hello\n")
_ = send(caller, said, 6, 0)
var heard = unsafe_alloc[c_char](64)
var got = recv(taken, heard, 64, 0)
print("the other end read", got, "bytes")
_ = close(caller)
_ = close(taken)
_ = close(listener)
_ = unlink(string_to_char_ptr(name))
heard.unsafe_free()
size.unsafe_free()
there_block.unsafe_free()
here_block.unsafe_free()
Three things differ from a port and are worth having in mind. bind
creates a file, and the file outlives the socket: closing everything does
not take the name away, and the next bind to it is refused with
EADDRINUSE until it is unlinked -- which is why the name carries the
process id here and why the last line is unlink. The address that
accept fills in is the caller's, and a caller that never bound a name of
its own has none, which is what the zero above is: the kernel is not
refusing to answer, there being nothing to say. And the length handed to
bind and connect is the whole struct, length(), the same number C
passes as sizeof(struct sockaddr_un).