libtealet 0.4.2
Loading...
Searching...
No Matches
Getting Started with libtealet

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

make test

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>
#include "tealet.h"
/* The run function executes in the tealet's context */
tealet_t *hello_run(tealet_t *current, void *arg) {
printf("Hello from coroutine!\n");
printf("Argument: %s\n", (char*)arg);
/* Return to the main tealet */
return current->main;
}
int main(void) {
/* Initialize using standard malloc */
tealet_t *main = tealet_initialize(&alloc, 0);
/* Create and run a coroutine */
void *arg = "Hello World";
tealet_t *coro = tealet_new(main, hello_run, &arg, NULL);
/* Clean up */
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 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

tealet_t *my_run(tealet_t *current, void *arg) {
int local_value = 42; /* On the stack! */
void *ptr = &local_value;
/* DANGER: Switching invalidates local_value */
tealet_switch(current->main, &ptr);
/* ptr now points to invalid stack data */
return current->main;
}
TEALET_API int tealet_switch(tealet_t *target, void **parg)
Suspend current tealet and resume target.

✅ Correct - Heap Data

tealet_t *my_run(tealet_t *current, void *arg) {
/* Allocate on heap - use malloc or your own allocator */
int *heap_value = malloc(sizeof(int));
*heap_value = 42;
void *ptr = heap_value;
tealet_switch(current->main, &ptr);
/* ptr still valid */
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:

tealet_t *my_run(tealet_t *current, void *arg) {
printf("Doing work...\n");
/* ✅ Recommended: Explicit exit */
tealet_exit(current->main, NULL, TEALET_EXIT_DELETE);
/* Should not reach here */
return current->main; /* Fallback only */
}
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:

/* ❌ DANGER: Race condition */
tealet_t *worker_run(tealet_t *current, void *arg) {
printf("Quick work\n");
return current->main; /* Auto-deletes this tealet */
}
int main(void) {
tealet_t *main = tealet_initialize(&alloc, 0);
void *arg = NULL;
tealet_t *worker = tealet_new(main, worker_run, &arg, NULL);
/* worker may already be deleted here! */
int status = tealet_status(worker); /* ❌ Dangling pointer! */
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

tealet_t *worker_run(tealet_t *current, void *arg) {
printf("Work done\n");
/* Exit without auto-delete */
tealet_exit(current->main, NULL, TEALET_EXIT_DEFAULT);
return current->main;
}
int main(void) {
tealet_t *main = tealet_initialize(&alloc, 0);
void *arg = NULL;
tealet_t *worker = tealet_new(main, worker_run, &arg, NULL);
/* worker still exists and can be queried */
int status = tealet_status(worker); /* ✅ Safe */
printf("Worker status: %d\n", status);
/* Manual cleanup */
tealet_delete(worker);
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):

tealet_t *fire_and_forget(tealet_t *current, void *arg) {
/* Do work that doesn't need caller intervention */
printf("Task complete\n");
return current->main; /* Auto-deletes */
}

Manual delete:

tealet_t *controlled_worker(tealet_t *current, void *arg) {
while (should_continue()) {
do_work();
tealet_switch(current->main, NULL); /* Yield back */
}
tealet_exit(current->main, NULL, TEALET_EXIT_DEFAULT); /* Don't auto-delete */
return current->main;
}
int main(void) {
/* ... */
tealet_t *worker = tealet_new(main, controlled_worker, &arg, NULL);
/* Can switch back to worker multiple times */
tealet_switch(worker, NULL);
tealet_switch(worker, NULL);
/* Manual cleanup when done */
tealet_delete(worker);
/* ... */
}

Common Patterns

Ping-Pong: Two Coroutines Alternating

#include <stdio.h>
#include "tealet.h"
tealet_t *ping_run(tealet_t *current, void *arg) {
tealet_t *pong = (tealet_t*)arg;
for (int i = 0; i < 3; i++) {
printf("Ping %d\n", i);
tealet_switch(pong, NULL);
}
return current->main;
}
tealet_t *pong_run(tealet_t *current, void *arg) {
tealet_t *ping = (tealet_t*)arg;
for (int i = 0; i < 3; i++) {
printf(" Pong %d\n", i);
tealet_switch(ping, NULL);
}
return current->main;
}
int main(void) {
tealet_t *main = tealet_initialize(&alloc, 0);
/* Create both coroutines */
tealet_t *ping = tealet_create(main, ping_run, NULL);
tealet_t *pong = tealet_create(main, pong_run, NULL);
/* Start ping, passing pong as argument */
void *arg = pong;
tealet_switch(ping, &arg);
/* Start pong, passing ping as argument */
arg = ping;
tealet_switch(pong, &arg);
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>
#include "tealet.h"
tealet_t *range_run(tealet_t *current, void *arg) {
int max = *(int*)arg;
for (int i = 0; i < max; i++) {
/* Yield value back to caller */
void *value = (void*)(intptr_t)i;
tealet_switch(current->main, &value);
}
return current->main;
}
int main(void) {
tealet_t *main = tealet_initialize(&alloc, 0);
/* Create generator for range(5) */
int max = 5;
void *arg = &max;
tealet_t *gen = tealet_new(main, range_run, &arg, NULL);
/* Pull values from generator */
while (tealet_status(gen) == TEALET_STATUS_ACTIVE) {
printf("%d\n", (int)(intptr_t)arg);
tealet_switch(gen, &arg);
}
return 0;
}

Output:

0
1
2
3
4

Producer-Consumer

#include <stdio.h>
#include "tealet.h"
typedef struct {
tealet_t *consumer;
int *buffer;
int count;
} producer_ctx_t;
tealet_t *producer_run(tealet_t *current, void *arg) {
producer_ctx_t *ctx = (producer_ctx_t*)arg;
/* Produce 10 items */
for (int i = 0; i < 10; i++) {
*ctx->buffer = i * i; /* Produce square numbers */
printf("Produced: %d\n", *ctx->buffer);
/* Switch to consumer */
tealet_switch(ctx->consumer, NULL);
}
return current->main;
}
tealet_t *consumer_run(tealet_t *current, void *arg) {
producer_ctx_t *ctx = (producer_ctx_t*)arg;
tealet_t *producer = (tealet_t*)arg; /* Passed on first switch */
while (tealet_status(producer) == TEALET_STATUS_ACTIVE) {
/* Consume item */
printf(" Consumed: %d\n", *ctx->buffer);
ctx->count++;
/* Switch back to producer */
tealet_switch(producer, NULL);
}
return current->main;
}
int main(void) {
tealet_t *main = tealet_initialize(&alloc, 0);
/* Shared buffer on heap */
int *buffer = malloc(sizeof(int));
/* Create consumer first */
tealet_t *consumer = tealet_create(main, consumer_run, NULL);
/* Create producer context */
producer_ctx_t ctx = {consumer, buffer, 0};
void *arg = &ctx;
tealet_t *producer = tealet_new(main, producer_run, &arg, NULL);
printf("Total consumed: %d items\n", ctx.count);
free(buffer);
tealet_delete(consumer);
return 0;
}

Creation Patterns

<tt>tealet_new()</tt> - Create and Start Immediately

void *arg = my_data;
tealet_t *t = tealet_new(main, my_run, &arg, NULL);
/* Returns when my_run switches back */
/* arg now contains return value */

Use when you want to start execution immediately.

<tt>tealet_create()</tt> + <tt>tealet_switch()</tt> - Deferred Start

tealet_t *t = tealet_create(main, my_run, NULL);
/* Tealet created but not yet running */
void *arg = my_data;
tealet_switch(t, &arg); /* Now it starts */

Use when you need to set up multiple coroutines before starting any.

Error Handling

Functions return NULL or negative error codes on failure:

tealet_t *t = tealet_create(main, my_run, NULL);
if (t == NULL) {
fprintf(stderr, "Failed to create tealet (out of memory)\n");
}
int result = tealet_switch(t, &arg);
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) {
/* Tealet is corrupt, don't switch to it */
}

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
/* Thread 1 */
tealet_t *main1 = tealet_initialize(&alloc, 0);
tealet_t *t1 = tealet_create(main1, func1, NULL);
/* Thread 2 */
tealet_t *main2 = tealet_initialize(&alloc, 0);
tealet_t *t2 = tealet_create(main2, func2, NULL);
/* ❌ WRONG: Can't switch between t1 and t2 (different mains) */
/* ✅ OK: Can switch within same thread between t1 and other tealets from main1 */

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