libdill
repository·master·Indexed 24 days ago
https://github.com/sustrik/libdillA C library that implements structured concurrency, providing tools to manage lightweight concurrent execution units called fibers. It includes support for coroutine management via functions like bundle_go and bundle_go_mem, as well as blocking bytestream socket I/O operations such as bsend, bsendl, brecv, and brecvl using iolist for fragmented data.
What's inside libdill
- libdill is a C library designed to provide structured concurrency. It allows developers to manage concurrent tasks (fibers) with a focus on safety and structured lifecycles.
What is structured concurrency in libdill
masterStructured concurrency ensures that the lifetimes of concurrent functions are cleanly nested. In libdill, this means if a coroutinefoolaunches coroutinebar,barmust finish beforefoofinishes. This creates a tree of coroutines (a "call tree") rooted in themainfunction, guaranteeing that once a parent function completes, no background tasks are left running.How libdill's concurrency differs from Go
masterWhile inspired by Go, libdill implements several key differences:
- Thread Isolation: There is no interaction between threads; each thread is treated as a separate process.
- Unbuffered Channels: Channels in libdill are always unbuffered.
- Deterministic
choose: Unlike Go'sselect, libdill'schooseis deterministic. If multiple clauses are ready, the clause closest to the beginning of the pollset wins. - Channel Closing:
chdonesignals the closing of a channel to both senders and receivers. - Cancellable Coroutines: Coroutines can be explicitly canceled, forming the basis for structured concurrency.
Use the iolist structure for gather/scatter I/O
masterThe
ioliststructure is used for gather/scatter operations, similar toiovecin the classic BSD socket API. Instead of arrays,ioliststructures are chained together as a singly-linked list.Rules for using
iolist:iol_base: Points to the data buffer.iol_len: The size of the buffer.iol_next: Pointer to the next element. The last element in the list MUST have this set toNULL.iol_rsvd: MUST always be set to0by the caller.- Thread Safety: Gather/scatter lists are not thread-safe. Functions may modify the list but must restore it to its original state before returning, even on failure.
- Validation: Functions will return
EINVALif the list contains a loop, ifiol_rsvdis non-zero, or if thefirstandlastpointers do not belong to the same list.
struct iolist { void *iol_base; size_t iol_len; struct iolist *iol_next; int iol_rsvd; };Understand and use iolists for scatter/gather I/O
masterIn libdill,
iolistis a linked list of buffers used as an alternative to POSIXiovecarrays. It allows for efficient scatter/gather I/O operations.Each
struct iolistcontains:iol_base: Pointer to the data buffer.iol_len: Size of the data in the buffer.iol_next: Pointer to the nextiolistnode (set toNULLfor the last item).iol_rsvd: A reserved field that must always be set to zero.
Note that
iolistsare not guaranteed to be thread- or coroutine-safe. While you can temporarily modify aniolistduring a function call (e.g., to prepend a header), you must revert all changes before the function returns.Use Coroutine Bundles to manage groups of coroutines
masterIntroduced in version 2.1, Coroutine Bundles allow you to group multiple coroutines together. You can usehdone()on a bundle to wait for all coroutines within that bundle to finish. In version 2.5,bundle_wait()was also added for this purpose.Bytestream vs Message-based sockets
masterlibdill distinguishes between two types of socket communication models:
- Bytestream Sockets: These do not preserve message boundaries. Examples include TCP and TLS. Use
bsendandbrecvto interact with them. - Message-based Sockets: These preserve message boundaries. Examples include UDP and Websockets. Use
msendandmrecvto interact with them.
Protocol Layering Constraint: When stacking protocols, a protocol requires a specific underlying type. For example, TLS is a bytestream protocol and must be layered on top of a bytestream-based protocol like TCP. Attempting to attach TLS to a message-based protocol like UDP will fail.
- Bytestream Sockets: These do not preserve message boundaries. Examples include TCP and TLS. Use
Perform orderly protocol termination
masterOrderly termination performs a terminal handshake with the peer, ensuring both sides have a consistent view of the connection. This allows the underlying protocol to potentially continue being used after the overlay protocol has been detached.
Depending on the protocol type, use the following:
- Base Protocols: Use a protocol-specific
closefunction (e.g.,tcp_close). - Overlay Protocols: Use a protocol-specific
detachfunction (e.g.,crlf_detach).
Key Behaviors:
- If the operation is blocking, the last parameter must be a deadline.
- Success Return Values:
closefunctions must return0.detachfunctions must return the handle of the underlying protocol.
- Error Behavior: If an error occurs, both functions must forcefully close the entire socket stack, return
-1, and seterrno.
- Base Protocols: Use a protocol-specific
Use `hquery` to implement multi-faceted handles
masterLibdill handles can represent multiple interfaces (e.g., a socket that is both a handle, a message socket, and a UDP socket). The
hquery()function allows you to retrieve specific interfaces from a handle using an opaque ID.To implement this for your own type:
- Define a unique ID for your type (typically a
static const void *to avoid collisions). - Implement the
queryfunction in yourhvfstable. It should check if the providedtypematches your unique ID. - If it matches, return a pointer to your object; otherwise, set
errno = ENOTSUPand returnNULL.
Users can then call
hquery(handle, type_id)to get a pointer to your object and perform type-specific operations.static const int quux_type_placeholder = 0; static const void *quux_type = &quux_type_placeholder; static void *quux_hquery(struct hvfs *hvfs, const void *type) { struct quux *self = (struct quux*)hvfs; if(type == quux_type) return self; errno = ENOTSUP; return NULL; } // User-facing function using the queried object int quux_frobnicate(int h) { struct quux *self = hquery(h, quux_type); if(!self) return -1; // ... perform operation ... return 0; }- Define a unique ID for your type (typically a
Using libdill in multi-threaded programs
masterlibdill can be used within multi-threaded applications, but it operates under a strict isolation model. Each thread acts as a separate environment, similar to a separate process.
Key constraints to observe:
- Coroutine Locality: A coroutine created in a specific thread is bound to that thread and will never migrate to another thread.
- Handle Locality: Handles created in one thread (such as channels or coroutine handles) are not thread-safe across boundaries and cannot be used in a different thread.
Manage coroutine lifetimes with bundles
masterTo avoid memory leaks from unclosed coroutine handles and to enable clean shutdowns, use bundles.
A bundle is a set of zero or more coroutines referred to by a single handle.
bundle(): Creates a new empty bundle.bundle_go(b, coroutine_function()): Launches a coroutine within the specified bundleb.hclose(b): Closes the bundle. This cancels all coroutines currently running within that bundle.
Important: When a coroutine is canceled via a bundle closure, all its blocking operations (like
mrecvormsend) will immediately return theECANCELEDerror code. Coroutines must be written to handleECANCELEDby cleaning up their own resources and exiting.int b = bundle(); assert(b >= 0); for(int i = 0; i < 3; i++) { int s = tcp_accept(ls, NULL, -1); s = suffix_attach(s, "\r\n", 2); rc = bundle_go(b, dialogue(s)); assert(rc == 0); } hclose(b); // All coroutines in bundle b are now canceled.How concurrency is implemented in libdill
masterlibdill implements concurrency using coroutines. To make a function capable of running concurrently, you must annotate it with the
coroutinemodifier.To launch a coroutine, use the
goconstruct. Coroutines are extremely lightweight and are scheduled cooperatively. This means a coroutine must yield control to allow others to run. While blocking functions (likemsleeporchrecv) yield control automatically, you can manually relinquish the CPU using theyieldfunction if a coroutine is performing a long-running computation without blocking.coroutine void foo(int arg1, const char *arg2); go(foo(34, "ABC"));