libdill

repository·master·Indexed 24 days ago

https://github.com/sustrik/libdill

A 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.

Tokens
57.7K
Snippets
57
Records
346
Agent score
81%

What's inside libdill

  1. What is structured concurrency in libdill

    master
    Structured concurrency ensures that the lifetimes of concurrent functions are cleanly nested. In libdill, this means if a coroutine foo launches coroutine bar, bar must finish before foo finishes. This creates a tree of coroutines (a "call tree") rooted in the main function, guaranteeing that once a parent function completes, no background tasks are left running.
  2. How libdill's concurrency differs from Go

    master

    While inspired by Go, libdill implements several key differences:

    1. Thread Isolation: There is no interaction between threads; each thread is treated as a separate process.
    2. Unbuffered Channels: Channels in libdill are always unbuffered.
    3. Deterministic choose: Unlike Go's select, libdill's choose is deterministic. If multiple clauses are ready, the clause closest to the beginning of the pollset wins.
    4. Channel Closing: chdone signals the closing of a channel to both senders and receivers.
    5. Cancellable Coroutines: Coroutines can be explicitly canceled, forming the basis for structured concurrency.
  3. Use the iolist structure for gather/scatter I/O

    master

    The iolist structure is used for gather/scatter operations, similar to iovec in the classic BSD socket API. Instead of arrays, iolist structures 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 to NULL.
    • iol_rsvd: MUST always be set to 0 by 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 EINVAL if the list contains a loop, if iol_rsvd is non-zero, or if the first and last pointers do not belong to the same list.
    struct iolist {
        void *iol_base;
        size_t iol_len;
        struct iolist *iol_next;
        int iol_rsvd;
    };
  4. Understand and use iolists for scatter/gather I/O

    master

    In libdill, iolist is a linked list of buffers used as an alternative to POSIX iovec arrays. It allows for efficient scatter/gather I/O operations.

    Each struct iolist contains:

    • iol_base: Pointer to the data buffer.
    • iol_len: Size of the data in the buffer.
    • iol_next: Pointer to the next iolist node (set to NULL for the last item).
    • iol_rsvd: A reserved field that must always be set to zero.

    Note that iolists are not guaranteed to be thread- or coroutine-safe. While you can temporarily modify an iolist during a function call (e.g., to prepend a header), you must revert all changes before the function returns.

  5. Bytestream vs Message-based sockets

    master

    libdill distinguishes between two types of socket communication models:

    1. Bytestream Sockets: These do not preserve message boundaries. Examples include TCP and TLS. Use bsend and brecv to interact with them.
    2. Message-based Sockets: These preserve message boundaries. Examples include UDP and Websockets. Use msend and mrecv to 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.

  6. Perform orderly protocol termination

    master

    Orderly 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 close function (e.g., tcp_close).
    • Overlay Protocols: Use a protocol-specific detach function (e.g., crlf_detach).

    Key Behaviors:

    • If the operation is blocking, the last parameter must be a deadline.
    • Success Return Values:
      • close functions must return 0.
      • detach functions 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 set errno.
  7. Use `hquery` to implement multi-faceted handles

    master

    Libdill 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:

    1. Define a unique ID for your type (typically a static const void * to avoid collisions).
    2. Implement the query function in your hvfs table. It should check if the provided type matches your unique ID.
    3. If it matches, return a pointer to your object; otherwise, set errno = ENOTSUP and return NULL.

    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;
    }
  8. Using libdill in multi-threaded programs

    master

    libdill 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.
  9. Manage coroutine lifetimes with bundles

    master

    To 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 bundle b.
    • 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 mrecv or msend) will immediately return the ECANCELED error code. Coroutines must be written to handle ECANCELED by 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.
  10. How concurrency is implemented in libdill

    master

    libdill implements concurrency using coroutines. To make a function capable of running concurrently, you must annotate it with the coroutine modifier.

    To launch a coroutine, use the go construct. Coroutines are extremely lightweight and are scheduled cooperatively. This means a coroutine must yield control to allow others to run. While blocking functions (like msleep or chrecv) yield control automatically, you can manually relinquish the CPU using the yield function if a coroutine is performing a long-running computation without blocking.

    coroutine void foo(int arg1, const char *arg2);
    
    go(foo(34, "ABC"));