The Serene runtime is a static library linked into code produced by a Serene compiler. It provides the runtime facilities that a program needs, such as the in-memory object model (values, symbols, namespaces), the persistent data structures, the cooperative fiber scheduler, a memory manager, the FFI subsystem, and a JIT engine among other things.
While the runtime is meant to be used with Serene, nothing stops you from using it with other compilers or even generic programs (for example, I used the runtime with my wayland compositor).
How to build the runtime
The runtime builds with Meson and a C23 compiler (Clang or GCC). A Nix flake pins every dependency, so the easiest path is:
# Run these from the repository root.
nix build '.#runtime' # build the standalone runtime as a Nix package
# or, to hack on it:
nix develop # a shell with the toolchain + deps
meson setup build runtime # build dir 'build', source dir 'runtime'
meson compile -C build
Without Nix you need a C23 toolchain with the following dependencies:
- pthreads (on POSIX)
- LLVM 22 or newer only for the full build; the standalone runtime don't need it
Serene build vs Standalone build
The with-serene feature (enabled by default) controls the Serene language layer: the value model, ABI, namespaces and keywords, and the JIT (and with it the LLVM dependency).
# full runtime (default): generic layer + Serene layer + LLVM
meson setup build runtime
# substrate only: memory manager, fibers, data structures. No value model, no LLVM.
meson setup build runtime -Dwith-serene=disabled
Install
meson setup build runtime --prefix=/usr/local # add -Dwith-serene=disabled for generic layer only
meson install -C build # sudo for a system prefix
This installs the static library, the public headers under $prefix/include/serene/, and a serene.runtime.pc pkg-config file.
Using the runtime in your project
Find the runtime through pkg-config. The serene.runtime.pc file carries the include path, the transitive libraries, and the right -DSRN_WITH_SERENE define so your code sees the same struct layout the library was built with. The public headers use C23 attributes, so compile your code as C23.
A minimal program that runs one fiber (works against a substrate-only build):
#include <stdio.h>
(void)ctx;
(void)arg;
printf("hello from a fiber\n");
}
}
static srn_fiber_result_t say_hello(srn_context_t *ctx, void *arg)
srn_context_t * srn_context_make(srn_engine_t *engine)
Make an empty context, by allocating a new memory block.
int srn_context_release(srn_context_t *ctx)
void srn_mm_shutdown(srn_mm_t *mm)
Shut down the memory manager and release the resources.
srn_mm_t * srn_mm_init(const srn_configuration_t *config)
Initialize the memory manager, this function will panic on error.
void srn_engine_shutdown(srn_engine_t *engine)
srn_engine_t * srn_engine_make(srn_mm_t *mm, const srn_configuration_t *config)
Create the engine over mm, copying config (the runtime's knobs) into it.
srn_fiber_t * srn_fiber_make(srn_context_t *ctx, srn_scheduler_t *sched, const char *name, srn_fiber_entry_t entry, void *arg, size_t stack_size)
Create a fiber that will run entry(ctx, arg), registered with sched but NOT scheduled.
void * srn_fiber_result_t
What a fiber's entry produces, type-erased.
void srn_sched_run(srn_scheduler_t *sched, size_t nworkers)
Run the scheduler with nworkers os threads draining it, returning once the pool goes quiescent (every...
Engine is a structure to own the long living and main pieces of the compiler.
srn_scheduler_t * scheduler
The fiber scheduler, that is the entry point of the fiber subsystem.
Main memory manager structure that will own all the allocated blocks and data.
Build it with whatever compiler you use:
cc -std=c23 $(pkg-config --cflags serene.runtime) hello.c \
$(pkg-config --libs serene.runtime) -o hello
Ready-to-copy templates for Make, Meson, and CMake live in examples/usage/, and runnable fiber demos in examples/fibers/ (built by default; configure with -Dwith-examples=disabled to skip them).
Browse the API
- Functions — a flat, alphabetical index of every runtime function. Most of the API is free functions (srn_*), so this is usually the fastest way in.
- Data Structures — every struct, union, enum, and typedef.
- Files — browse by header. Public headers live under serene/rt/; the serene, rt, and jit folders expand to the full tree.
Core object model
- engine.h — srn_engine_t, the long-lived compiler state: object-id counter, memory manager, scheduler, namespaces, and interned keywords.
- context.h — srn_context_t, an allocation and lookup scope (srn_context_make, srn_allocate).
- core.h — srn_value_t and the metadata, syntax, and error variants that describe every Serene value.
- namespaces.h — srn_namespace_t, the unit of code organisation.
- symbols.h, keywords.h — interned symbols and keywords.
- strings.h — srn_string_t, length-prefixed strings.
- protocols.h — value-level equality and hashing (srn_value_eq, srn_value_hash).
Persistent data structures
Concurrency: fibers
- Overview: the fiber subsystem implementation overview — the connective architecture doc (the fiber object, scheduling, work stealing, and the reactor bridge). Pairs with the "Fibers" chapter of the language report.
- fiber.h — stackful, cooperative fibers that yield, suspend, or finish (srn_fiber_make, srn_fiber_yield, srn_fiber_suspend).
- The M:N, work-stealing scheduler lives in rt/fiber/scheduler.c, with the OS-thread primitives (spawn, mutex, cond) in fiber/thread.h and the context switch under rt/fiber/arch/.
Asynchronous IO: the reactor
- Overview: the reactor implementation overview — the architecture, the data flow, and the backend-author contract. Pairs with the "The Reactor" and "Fiber I/O" sections of the language report.
- reactor.h — the IO reactor: one completion-based engine (an io_uring-style submit/complete contract) running on its own dedicated OS thread. Each worker talks to it over a channel – a submission/completion SPSC ring pair – and the reactor owns the rings. Platform backends realise the contract: epoll on Linux today, with io_uring/kqueue/IOCP planned.
- fiber/io.h — the fiber-facing IO surface: suspending calls (srn_fiber_read, srn_fiber_write, srn_fiber_sleep (family), srn_fiber_accept, srn_fiber_connect, and the send/recv family) plus non-suspending helpers (srn_fiber_socket, bind, listen, open, ...). A suspending call reads like a blocking one but suspends only the calling fiber; its worker keeps running others, and the fiber resumes with a per-op result – a fallible value carrying either the outcome or an error – once the reactor finishes. A pollable fd (socket, pipe, ...) goes through the reactor; a regular file, which a readiness backend cannot poll, is served by a synchronous syscall on the worker. Suspending calls must run on a fiber that is on a worker.
- Internals: the backend vtable in rt/reactor/backend.h and the epoll backend in rt/reactor/backend/epoll.c; the reactor core (channels, the reactor thread, the dispatch loop, the timer heap) in rt/reactor/reactor.c; and the fiber-to-reactor bridge (submit-on-park, completion -> wake) in rt/fiber/io.c.
- Deferred: a thread-pool offload for blocking work. Regular-file IO is currently served by a blocking syscall on the worker; a dedicated pool that decouples that from the worker count is designed but not yet built.
Memory, FFI, and JIT
TODOs
- Fix the comments to be in markdown
- Make the compiler warn/errors more strict
License
The runtime library — the sources compiled into libserene.runtime and the public headers under serene/ — is licensed under the GNU LGPL v3, so you can link it into programs under other licenses. The runtime's tests, examples, and build tooling, and the rest of the Serene project, are under the GNU GPL v3. Each file states its license in its header; the full texts are in LICENSE.