libtealet 0.4.2
Loading...
Searching...
No Matches
libtealet

build and test docs Powered by Stackman

libtealet

Version 0.4.3

LibTealet is a lightweight co-routine library for C. It is based on the technique of stack-slicing, where the execution stack is saved and restored in order to maintain separate execution context. It uses the Stackman library library for low level stack operations, providing implementation for common modern desktop platforms. For ease of integration, this repository bundles Stackman v1.2.0 in stackman/ including pre-built platform libraries used by the default build. The official Stackman repository is: https://github.com/stackless-dev/stackman There are no other run-time dependencies, ecxcept for memcpy(), and the default use of malloc() can be replaced with a custom memory allocator, and assert() which is used by debug builds.

Coroutines

In today's common parlance, using coroutines refers to co-operative multitasking within a single operating system thread, where execution in a function stack is halted in order to be resumed later. This is commonly achieved using the coroutine programming construct, as available in Python 3.5 and later, and C++20] although these are technically asymmetric coroutines since they can only be suspended and resumed by their immediate caller. To be able to suspend a stack of functions, all of the functions involved must therefore be coroutines, often decorated with a special keyword such as async.

Libtealet does not require special functions. Its approach is more akin to threading in that a whole execution stack is suspended, and control passed to another stack. Perhaps it is prudent to talk of co-stacks in this regard. No special compiler or language support is required.

Stack-slicing

The approach used here employs stack-slicing, a term coined by Christian Tismer to desctibe the technique employed by Stackless-Python.
Instead of each coroutine (or co-stack) having its own stack in virtual memory like an operating system thread does, parts of the C stack that belong to different execution contexts are stored on the heap and restored to the system stack as required.

For each platform, a small piece of assembly code is required. This code stores cpu registers on the stack, then calls functions to save/restore the stack to or from the heap, and adjusts the stack pointer as required. Then the cpu state is restored from the restored stack and a new co-routine is running. This support is provided by the Stackman library.

Similar work

Stackless Python and Gevent use a similar mechanism, and this code is based on that work.

For C, there also exist some other approaches. For an overview, see Coroutines for C

Stack-chaining Optimization

Unlike traditional coroutine implementations that save entire stacks, libtealet uses an incremental stack-saving technique with chunk chaining. When switching contexts, it saves only the portion needed for the current target stack boundary (that is, the overlapping stack region required for that switch). Stack chunks are linked via a g_prev chain, enabling:

  • On-demand overlap saves: Initial and subsequent saves are boundary-driven, and additional bytes are copied only when a later switch requires a deeper overlapping region
  • Shared stack snapshots (duplicate/clone cases): Tealets created via stack duplication can share unchanged saved stack segments via reference counting
  • Memory efficiency: Typical coroutine overhead is a few KB, not the 1-8MB of OS thread stacks

This makes libtealet suitable for applications with thousands of concurrent coroutines.

Quick Start

#include "tealet.h"
tealet_t *worker(tealet_t *current, void *arg) {
printf("Hello from coroutine!\n");
return current->main; /* Return to caller */
}
int main(void) {
tealet_t *main = tealet_initialize(&alloc, 0);
void *arg = NULL;
tealet_t *coro = tealet_new(main, worker, &arg, NULL);
return 0;
}
Definition tealet.h:40
Definition tealet.h:61
Public core API for libtealet.
TEALET_API void tealet_finalize(tealet_t *tealet)
Destroy a previously initialized main tealet.
TEALET_API tealet_t * tealet_new(tealet_t *tealet, tealet_run_t run, void **parg, void *stack_far)
Create and immediately start a new tealet.
#define TEALET_ALLOC_INIT_MALLOC
Definition tealet.h:50
TEALET_API tealet_t * tealet_initialize(tealet_alloc_t *alloc, size_t extrasize)
Initialize libtealet and create the main tealet for the current thread.

Compile: gcc -o example example.c -Isrc -Lbin -ltealet

Development requirements

For contributor workflows, clang-format is required.

  • CI runs make check-format in .github/workflows/build-test.yml.
  • Local formatting commands:
    • make check-format (verify formatting)
    • make format (apply formatting)

Install clang-format:

  • Linux (Debian/Ubuntu): sudo apt install clang-format
  • macOS (Homebrew): brew install clang-format
  • Windows:
    • winget install LLVM.LLVM
    • or choco install llvm

After installing on Windows, ensure clang-format.exe is on your PATH.

Functionality

The library provides the basic mechanism to create coroutine and to switch between them. It takes care of allocating and saving the stack for dormant coroutines. A way to pass simple values between coroutines is provided but the user must be careful to pass any more complicated data on the heap, and not via stack-local variables.

No form of scheduler is implemented.

Optional stack checks

libtealet includes optional runtime stack-integrity checks that combine page protection and snapshot verification. These checks help verify the promise that each tealet does not access stack areas outside its allowed bounds. They are especially useful during application development and integration to catch boundary-violation bugs early. They come with runtime overhead and are generally intended for development/testing builds, not production deployments.

  • Stack guard (TEALET_CONFIGF_STACK_GUARD) protects full pages (typically via mprotect() where available).
  • Stack snapshot (TEALET_CONFIGF_STACK_SNAPSHOT) verifies byte-level regions, including sub-page areas guard mode cannot represent exactly.
  • Together they are used to catch out-of-bounds stack access across both page-aligned and non-page-aligned regions.
  • Use tealet_configure_check_stack() to enable a sensible default check profile (ideally from a program top-level function, since it derives stack_guard_limit from its own stack frame).
  • Use tealet_configure_get() / tealet_configure_set() for explicit control.

By default, the project build enables both backends:

  • TEALET_WITH_STACK_GUARD=1
  • TEALET_WITH_STACK_SNAPSHOT=1

You can compile them out when needed, for example:

make TEALET_WITH_STACK_GUARD=0 TEALET_WITH_STACK_SNAPSHOT=0

When compiled out, configuration calls are canonicalized to the supported subset on the current build/platform.

Advanced: Fork-like Semantics

In addition to the traditional approach where each tealet exists within the execution scope of a function (created via tealet_new() or tealet_create()), libtealet now supports Unix-like fork semantics through tealet_fork().

This functionality was available in Stackless Python but has historically been omitted from this library to maintain the clean discipline of function-scoped coroutines. tealet_fork() breaks out of this restriction, enabling more dynamic coroutine creation patterns.

Important responsibilities when using fork:

  • When forking main-lineage execution (main tealet or a clone of main): call tealet_set_far() first to bound the stack; otherwise tealet_fork() can fail with TEALET_ERR_UNFORKABLE.
  • Boundary inheritance: A forked tealet inherits the parent's current far boundary at fork time. For main-lineage forks, this means the configured main boundary is carried into the child clone.
  • When forking main-lineage execution: exit explicitly using tealet_exit() (without TEALET_EXIT_DEFER).
  • When forking a regular function-scoped tealet: no extra far-boundary setup is needed for the fork itself, and the forked tealet can generally return through the same run-function path as the original.
  • Scope discipline: For boundary-based fork flows, all switching must occur within the bounded stack region defined by the far boundary

This feature enables advanced use cases like coroutine cloning and continuation capture, but requires careful management of stack boundaries and explicit lifetime control.

If your code needs to branch behavior at runtime, use tealet_get_origin() (or TEALET_IS_MAIN_LINEAGE() / TEALET_IS_FORK()) to detect whether a tealet is main-lineage and/or fork-originated.

Documentation

Performance Characteristics

Metric libtealet OS Threads
Context switch ~100-500 cycles ~1000-10000 cycles
Memory per coroutine ~2-16 KB (incremental) 1-8 MB (fixed)
Creation overhead ~1-2 µs ~50-200 µs
Parallelism No (single thread) Yes (multi-core)
Scheduling Manual OS kernel
System calls None (pure user-space) Required

Trade-offs:

  • ✅ Much faster context switching (no kernel involvement)
  • ✅ Lower memory overhead enables thousands of coroutines
  • ✅ Deterministic scheduling (you control when switches happen)
  • ❌ No true parallelism (runs on single OS thread)
  • ❌ Manual scheduling required (no preemption)

Example

For an example on how to implement longjmp() like functionality, as with the setcontext() library see the file tests/setcontext.c

History

The tealet code was originally extracted from the Python Greenlet project by Armin Rigo and the original version was written by him. Armin had previously created the Greenlet project by extracting the stack slicing code from from Stackless Python.

Changelog

See CHANGELOG.md for release history and version information.