libtealet enables symmetric coroutines (co-stacks) in C without requiring compiler support. Unlike async/await patterns, libtealet can suspend and resume entire call stacks at any point.
Installation
Using Pre-built Libraries
Download the latest source distribution from the releases page or clone the repository:
git clone https://github.com/kristjanvalur/libtealet.git
cd libtealet
make
The library will be built in bin/:
bin/libtealet.so - Shared library
bin/libtealet.a - Static library
Running Tests
Formatting checks (development)
clang-format is required for local contributor formatting checks and for CI parity.
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
Then run:
make check-format # verify formatting
make format # apply formatting
API docs generation (development)
doxygen is used to generate local API documentation from header comments and Markdown guides.
Install tools:
- Linux (Debian/Ubuntu):
sudo apt install doxygen graphviz
- macOS (Homebrew):
brew install doxygen graphviz
- Windows:
winget install DimitriVanHeesch.Doxygen
winget install Graphviz.Graphviz
- or
choco install doxygen.install graphviz
Then run:
make docs # generate docs/_build/doxygen/html/index.html
make docs-check # fail on Doxygen warnings
make docs-clean # remove generated docs
See docs/DOXYGEN.md for the hybrid authored+generated documentation workflow.
Hello Coroutine
The simplest example demonstrates creating and switching between coroutines:
#include <stdio.h>
printf("Hello from coroutine!\n");
printf("Argument: %s\n", (char*)arg);
return current->main;
}
int main(void) {
void *arg = "Hello World";
return 0;
}
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 and run:
gcc -o hello hello.c -Isrc -Lbin -ltealet
./hello
Output:
Hello from coroutine!
Argument: Hello World
Memory Safety Rules
⚠️ Critical: Stack-allocated data becomes invalid when switching contexts.
❌ Wrong - Stack Data
int local_value = 42;
void *ptr = &local_value;
return current->main;
}
TEALET_API int tealet_switch(tealet_t *target, void **parg)
Suspend current tealet and resume target.
✅ Correct - Heap Data
int *heap_value = malloc(sizeof(int));
*heap_value = 42;
void *ptr = heap_value;
free(heap_value);
return current->main;
}
Rule of thumb: If data needs to survive a tealet_switch(), allocate it on the heap. Exception: You can intentionally include additional creator-frame stack data in a new tealet's initial snapshot by passing stack_far to tealet_new() or tealet_create(). This exists to make integration practical for existing code paths that already rely on stack-based structures.
Tealet Lifecycle and Exiting
Best Practice: Always Use <tt>tealet_exit()</tt>
While a tealet run function can simply return to exit, it's strongly recommended to use tealet_exit() explicitly:
printf("Doing work...\n");
return current->main;
}
TEALET_API int tealet_exit(tealet_t *target, void *arg, int flags)
Exit current tealet and transfer control to target.
Why use tealet_exit()?
- More explicit about where execution goes
- Controls auto-deletion with flags
- Required for main-lineage fork flows (see Advanced section)
- Clearer intent in complex switching scenarios
⚠️ Auto-Delete Danger
When a run function returns (or calls tealet_exit() with TEALET_EXIT_DELETE), the tealet is automatically deleted. This can cause dangling pointer issues:
printf("Quick work\n");
return current->main;
}
int main(void) {
void *arg = NULL;
int status = tealet_status(worker);
return 0;
}
What happened? tealet_new() creates the worker and immediately runs it. The worker completes and deletes itself before tealet_new() returns. Now worker is a dangling pointer.
✅ Safe Pattern: Prevent Auto-Delete
printf("Work done\n");
return current->main;
}
int main(void) {
void *arg = NULL;
int status = tealet_status(worker);
printf("Worker status: %d\n", status);
return 0;
}
TEALET_API void tealet_delete(tealet_t *target)
Deallocate a non-main tealet.
Exit Flags
TEALET_EXIT_DEFAULT (0): Prevent auto-delete; tealet must be manually deleted with tealet_delete()
TEALET_EXIT_DELETE: Auto-delete on exit; same as returning from the run function (the default behavior)
TEALET_EXIT_DEFER: Defer deletion to run function return (advanced; see API docs)
Note: The old TEALET_FLAG_* names are still available for backwards compatibility.
Shutdown Order Requirement
At shutdown, always delete non-main tealets before calling tealet_finalize(main).
There is no supported way to decouple this order.
When to Use Each
Auto-delete (default):
printf("Task complete\n");
return current->main;
}
Manual delete:
while (should_continue()) {
do_work();
}
return current->main;
}
int main(void) {
}
Common Patterns
Ping-Pong: Two Coroutines Alternating
#include <stdio.h>
for (int i = 0; i < 3; i++) {
printf("Ping %d\n", i);
}
return current->main;
}
for (int i = 0; i < 3; i++) {
printf(" Pong %d\n", i);
}
return current->main;
}
int main(void) {
void *arg = pong;
arg = ping;
return 0;
}
TEALET_API tealet_t * tealet_create(tealet_t *tealet, tealet_run_t run, void *stack_far)
Create a new tealet without starting it.
Output:
Ping 0
Pong 0
Ping 1
Pong 1
Ping 2
Pong 2
Generator: Yielding Values
#include <stdio.h>
int max = *(int*)arg;
for (int i = 0; i < max; i++) {
void *value = (void*)(intptr_t)i;
}
return current->main;
}
int main(void) {
int max = 5;
void *arg = &max;
while (tealet_status(gen) == TEALET_STATUS_ACTIVE) {
printf("%d\n", (int)(intptr_t)arg);
}
return 0;
}
Output:
Producer-Consumer
#include <stdio.h>
typedef struct {
int *buffer;
int count;
} producer_ctx_t;
producer_ctx_t *ctx = (producer_ctx_t*)arg;
for (int i = 0; i < 10; i++) {
*ctx->buffer = i * i;
printf("Produced: %d\n", *ctx->buffer);
}
return current->main;
}
producer_ctx_t *ctx = (producer_ctx_t*)arg;
while (tealet_status(producer) == TEALET_STATUS_ACTIVE) {
printf(" Consumed: %d\n", *ctx->buffer);
ctx->count++;
}
return current->main;
}
int main(void) {
int *buffer = malloc(sizeof(int));
producer_ctx_t ctx = {consumer, buffer, 0};
void *arg = &ctx;
printf("Total consumed: %d items\n", ctx.count);
free(buffer);
return 0;
}
Creation Patterns
<tt>tealet_new()</tt> - Create and Start Immediately
Use when you want to start execution immediately.
<tt>tealet_create()</tt> + <tt>tealet_switch()</tt> - Deferred Start
Use when you need to set up multiple coroutines before starting any.
Error Handling
Functions return NULL or negative error codes on failure:
if (t == NULL) {
fprintf(stderr, "Failed to create tealet (out of memory)\n");
}
if (result == TEALET_ERR_DEFUNCT) {
fprintf(stderr, "Target tealet is corrupt\n");
return -1;
}
#define TEALET_ERR_MEM
Definition tealet.h:81
Error codes:
TEALET_ERR_MEM (-1): Memory allocation failed
TEALET_ERR_DEFUNCT (-2): Target tealet is corrupt
Check status before switching:
if (tealet_status(t) == TEALET_STATUS_DEFUNCT) {
}
Thread Safety
⚠️ Tealets are not thread-safe across threads.
Rules:
- Each thread must have its own main tealet
- Never switch between tealets from different threads
- Tealets can only switch if they share the same main tealet
Next Steps
- Read API.md for complete function reference
- See ARCHITECTURE.md to understand internals
- Check
tests/setcontext.c for a complete working example
- Explore
tests/tests.c for advanced usage patterns
Performance Characteristics
- Context switch: ~100-500 CPU cycles (platform-dependent)
- Memory overhead: Proportional to saved stack size
- Stack saving: Incremental (only saves needed portions)
- No system calls: Pure user-space operation
Compared to OS threads:
- ✅ Much faster context switching (no kernel involvement)
- ✅ Lower memory overhead per coroutine
- ❌ No true parallelism (runs on single OS thread)
- ❌ Manual scheduling required