Go-style CSP concurrency for plain C: fibers, channels, select, async I/O, and GC in one runtime 🚀
libgoc gives C programs:
- ✅ stackful coroutines (
fibers) - ✅ Go-like channels +
goc_altsselect - ✅ timeout channels + thread-pool scheduling
- ⚡ async I/O and HTTP support
- ♻️ GC-managed memory via Boehm GC
Use libgoc when you want Go-style concurrency without leaving C:
- 👩💻 C programmers building concurrent systems
- 🛠 language implementors targeting C/C++
- 🌐 runtime authors who need async I/O + channel-style coordination
Dependencies:
minicoro |
fiber suspend/resume (vendored MIT) |
libuv |
event loop, threads, timers, cross-thread wakeup |
| Boehm GC | garbage collection |
picohttpparser |
HTTP/1.1 request parser (vendored MIT); used by goc_http; disable with -DLIBGOC_SERVER=OFF |
| musl/TRE regex | POSIX ERE regex (vendored BSD-2-Clause); used by goc_schema |
yyjson |
JSON reader/writer (vendored MIT); used by goc_json |
🚧 Pre-built static libraries
Available on the Releases page
- Linux (x86-64)
- macOS (arm64)
- Windows (x86-64)
📚 API reference
🔗 Also see
Table of Contents
Examples
1. Ping-pong
Two fibers exchange a message back and forth over a pair of unbuffered channels. This is the canonical CSP "ping-pong" pattern — each fiber blocks on a take, then immediately puts to wake the other side.
#include "goc.h" #include "goc_dict.h" #include <stdio.h> #define N_ROUNDS 5 static void player_fiber(void* arg) { goc_dict* d = arg; goc_chan* recv = goc_dict_get(d, "recv", NULL); goc_chan* send = goc_dict_get(d, "send", NULL); const char* name = goc_dict_get(d, "name", NULL); goc_val_t* v; while ((v = goc_take(recv))->ok == GOC_OK) { int count = goc_unbox(int, v->val); printf("%s %d\n", name, count); if (count >= N_ROUNDS) { goc_close(send); return; } goc_put_boxed(int, send, count + 1); } } static void main_fiber(void* _) { goc_chan* a_to_b = goc_chan_make(0); goc_chan* b_to_a = goc_chan_make(0); goc_chan* done_ping = goc_go(player_fiber, goc_dict_of( {"recv", b_to_a}, {"send", a_to_b}, {"name", "ping"} )); goc_chan* done_pong = goc_go(player_fiber, goc_dict_of( {"recv", a_to_b}, {"send", b_to_a}, {"name", "pong"} )); /* Kick off the exchange with the first message. */ goc_put_boxed(int, a_to_b, 1); /* Wait for both fibers to finish. */ goc_take(done_ping); goc_take(done_pong); } int main(void) { goc_init(); goc_go(main_fiber, NULL); goc_shutdown(); return 0; }
What this example demonstrates:
goc_chan_make(0)— unbuffered channels enforce a synchronous rendezvous: eachgoc_putblocks until the other fiber callsgoc_take, and vice versa.goc_go— spawns both player fibers on the current pool (or default pool when called outside fiber context) and returns a join channel that is closed automatically when the fiber returns.goc_close— when the round limit is reached the active fiber closes the forward channel, causing the partner's nextgoc_taketo returnGOC_CLOSEDand exit its loop cleanly.goc_dict_of(...)— constructs a GC-managed key-value dict used here to pass multiple named arguments to a fiber in a singlevoid*.goc_put_boxed(T, ch, val)/goc_unbox(T, ptr)— channels carryvoid*; boxing heap-allocates a scalar so it can be sent, unboxing dereferences it back to the original type on the receiving end.
2. JSON greeting over HTTP
A minimal HTTP example that demonstrates P11-style JSON request parsing and response serialisation. The client sends { "name": "Arjun" }, and the server responds with { "response": "Hi, Arjun!" }.
#include "goc.h" #include "goc_dict.h" #include "goc_http.h" #include "goc_json.h" #include "goc_schema.h" #include <stdio.h> static goc_schema* request_schema; static goc_schema* response_schema; /* HTTP request handler */ static void greet_handler(goc_http_ctx_t* ctx) { goc_json_result req_r = goc_json_parse(goc_http_server_body_str(ctx)); goc_dict* req = req_r.res; const char* name = goc_dict_get(req, "name", NULL); goc_dict* resp = goc_dict_of( {"response", goc_sprintf("Hi, %s!", name)} ); goc_json_result out_r = goc_json_stringify(response_schema, resp); goc_http_server_respond(ctx, 200, "application/json", out_r.res); } static void main_fiber(void* _) { /* define schemas */ request_schema = goc_schema_dict_of( {"name", goc_schema_str()} ); response_schema = goc_schema_dict_of( {"response", goc_schema_str()} ); /* make server */ goc_http_server_opts_t* opts = goc_http_server_opts(); goc_http_server* srv = goc_http_server_make(opts); goc_http_server_route(srv, "POST", "/greet", greet_handler); goc_chan* ready = goc_http_server_listen(srv, "127.0.0.1", 8080); goc_take(ready); /* send request */ goc_dict* req_obj = goc_dict_of( {"name", "Arjun"} ); goc_json_result req_json_r = goc_json_stringify(request_schema, req_obj); goc_chan* resp_ch = goc_http_post( "http://127.0.0.1:8080/greet", "application/json", req_json_r.res, goc_http_request_opts() ); /* handle response */ goc_http_response_t* resp = goc_take(resp_ch)->val; printf("server replied: %s\n", resp->body); /* shutdown server */ goc_chan* close_ch = goc_http_server_close(srv); goc_take(close_ch); } int main(void) { goc_init(); goc_go(main_fiber, NULL); goc_shutdown(); return 0; }
What this example demonstrates:
goc_json_parse— parse an inbound JSON request body into agoc_dict*.goc_json_stringify— serialize a response object using a schema.goc_http_server_routeandgoc_http_post— wire a request/response roundtrip over HTTP.goc_http_server_respond(..., "application/json", ...)— send a JSON response with the correct MIME type.
Best Practices
Used the right way, libgoc provides a runtime environment very similar to Go's.
The blocking versions of take/put/alts are intended only for the initial setup in the main function, and should not be used otherwise.
A typical program's main function should be like this:
static void main_fiber(void* _) { /* * User code comes here. * Since this is a fiber context, * async channel ops work here * and in all code reachable from here. */ } int main(void) { goc_init(); /* reify main thread as main fiber */ goc_go(main_fiber, NULL); goc_shutdown(); return 0; }
Building and Testing
expand / collapse
Pre-built static libraries are available on the Releases page.
libgoc ships with a comprehensive, phased test suite covering the full public API. See the Testing section in the Design Doc for a breakdown of the test phases and what each one covers.
test.sh — Full build + test runner with optional watch mode:
./test.sh # build and run all tests WATCH=1 ./test.sh # rebuild and rerun on any src/include/tests change ./test.sh -dbg 1 # enable verbose [GOC_DBG] output
Options: -dbg <0|1>, -rp <0|1> (SO_REUSEPORT for HTTP tests), -vmem <0|1>. Output is streamed to console and test.log. In watch mode, only previously-failing tests are rerun on the next change.
run_test_loop.sh — Stress a single test for flakiness detection:
./run_test_loop.sh tests/test_p06_thread_pool.c # run up to 20 times
./run_test_loop.sh tests/test_p06_thread_pool.c -max-tries 100 -trace 1Builds only the named target, runs it in a loop, and exits on the first failure. Each run is timestamped; log path is printed on exit.
Prerequisites
| Dependency | macOS | Linux (Debian/Ubuntu) | Linux (Fedora/RHEL) | Windows |
|---|---|---|---|---|
| CMake ≥ 3.20 | brew install cmake |
apt install cmake |
dnf install cmake |
MSYS2 UCRT64 (bundled) |
| libuv | brew install libuv |
apt install libuv1-dev |
dnf install libuv-devel |
MSYS2 UCRT64 — see Windows |
| Boehm GC | brew install bdw-gc |
source build (see below) | dnf install gc-devel |
MSYS2 UCRT64 — see Windows |
| pkg-config | brew install pkg-config |
apt install pkg-config |
dnf install pkgconfig |
MSYS2 UCRT64 (bundled) |
| minicoro | vendored (vendor/minicoro/); instantiated via src/minicoro.c |
A C11 compiler is required: GCC or Clang on Linux/macOS; MinGW-w64 GCC via MSYS2 UCRT64 on Windows.
libgoc is built to link statically against libuv and Boehm GC. Ensure static versions of those dependencies are available to pkg-config before configuring.
macOS
# 1. Install dependencies (Homebrew) brew install cmake libuv bdw-gc pkg-config # Homebrew's bdw-gc does not ship a bdw-gc-threaded.pc pkg-config alias. # Create it once in the global Homebrew pkgconfig directory: PKGDIR="$(brew --prefix)/lib/pkgconfig" [ -f "$PKGDIR/bdw-gc-threaded.pc" ] || cp "$PKGDIR/bdw-gc.pc" "$PKGDIR/bdw-gc-threaded.pc" # 2. Configure export PKG_CONFIG_ALL_STATIC=1 cmake -B build -DLIBGOC_STATIC_DEPENDENCIES=ON # 3. Build cmake --build build # 4. Run tests ctest --test-dir build --output-on-failure # Or run a single phase directly for full output ./build/test_p01_foundation
Linux
# 1. Install dependencies (Debian/Ubuntu shown; see table above for RPM) sudo apt update sudo apt install cmake libuv1-dev libatomic-ops-dev pkg-config build-essential # Ubuntu's libgc-dev is NOT compiled with --enable-threads, which libgoc requires. # GC_allow_register_threads is required for libgoc's goc_thread_create/ # goc_thread_join wrappers; the system package can crash at runtime. # Build Boehm GC from source instead: wget https://github.com/ivmai/bdwgc/releases/download/v8.2.6/gc-8.2.6.tar.gz tar xf gc-8.2.6.tar.gz && cd gc-8.2.6 ./configure --enable-threads=posix --enable-thread-local-alloc --disable-shared --enable-static --prefix=/usr/local make -j$(nproc) && sudo make install && sudo ldconfig && cd .. # The source build does not always generate a bdw-gc-threaded.pc pkg-config alias. # Create it manually if it is missing: if [ ! -f /usr/local/lib/pkgconfig/bdw-gc-threaded.pc ]; then sudo ln -s /usr/local/lib/pkgconfig/bdw-gc.pc /usr/local/lib/pkgconfig/bdw-gc-threaded.pc fi # Ensure pkg-config searches /usr/local (not on the default path on all distros): export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:$PKG_CONFIG_PATH # To make this permanent: # echo 'export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:$PKG_CONFIG_PATH' >> ~/.bashrc # 2. Configure export PKG_CONFIG_ALL_STATIC=1 cmake -B build -DLIBGOC_STATIC_DEPENDENCIES=ON # 3. Build cmake --build build # 4. Run tests ctest --test-dir build --output-on-failure # Or run a single phase directly ./build/test_p01_foundation
Windows
libgoc uses libuv thread primitives (uv_thread_t, etc.) and C11 atomics via <stdatomic.h> (_Atomic, atomic_*). MSVC builds are still not supported (notably due to bdwgc/toolchain constraints, including vcpkg's Win32-threads build), so the recommended Windows setup remains MSYS2/MinGW-w64 (UCRT64).
# 1. Install MSYS2 from https://www.msys2.org/, then in a UCRT64 shell: pacman -S mingw-w64-ucrt-x86_64-gcc \ mingw-w64-ucrt-x86_64-cmake \ mingw-w64-ucrt-x86_64-libuv \ mingw-w64-ucrt-x86_64-gc \ mingw-w64-ucrt-x86_64-pkg-config # 2. Create the bdw-gc-threaded pkg-config alias if it is missing PKGDIR="/ucrt64/lib/pkgconfig" [ -f "$PKGDIR/bdw-gc-threaded.pc" ] || cp "$PKGDIR/bdw-gc.pc" "$PKGDIR/bdw-gc-threaded.pc" # 3. Configure and build everything (library + tests) export PKG_CONFIG_ALL_STATIC=1 cmake -B build -DLIBGOC_STATIC_DEPENDENCIES=ON cmake --build build --parallel $(nproc) # 4. Run tests ctest --test-dir build --output-on-failure
Tests: Phases P1–P7 and P9 run normally on Windows. Phase 8 (safety tests) requires
fork()/waitpid()to isolate processes that callabort()— these POSIX APIs are not available in MinGW. The P8 test binary builds successfully but all 11 tests reportskipat runtime.
Build types
# Debug (no optimisation, debug symbols) cmake -B build -DCMAKE_BUILD_TYPE=Debug # Release cmake -B build -DCMAKE_BUILD_TYPE=Release # RelWithDebInfo (default) cmake -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo
Stack allocator
# Default: canary-protected stacks (recommended, portable) cmake -B build # Enable virtual memory allocator (dynamic stack growth) cmake -B build -DLIBGOC_VMEM=ON
The default fiber stack size can be set at build time:
cmake -B build -DLIBGOC_STACK_SIZE=131072 # 128 KBInstallation and pkg-config
libgoc is installed as a static archive plus headers. The install step writes a libgoc.pc pkg-config file to <prefix>/lib/pkgconfig/, so downstream projects can locate and link libgoc without knowing its install prefix.
cmake -B build
cmake --build build
sudo cmake --install build # installs goc.h, goc_io.h, goc_array.h, libgoc.a, and libgoc.pc# Compile and link a consumer with pkg-config cc $(pkg-config --cflags libgoc) my_app.c $(pkg-config --libs libgoc) -o my_app
In a CMake-based consumer, use pkg_check_modules in the same way as libgoc itself uses it for libuv:
find_package(PkgConfig REQUIRED) pkg_check_modules(LIBGOC REQUIRED IMPORTED_TARGET libgoc) target_link_libraries(my_target PRIVATE PkgConfig::LIBGOC)
Code coverage
Code coverage instrumentation is opt-in via -DLIBGOC_COVERAGE=ON. It requires GCC or Clang and uses gcov-compatible .gcda/.gcno files. If lcov and genhtml are found, a coverage build target is also registered that runs the test suite and produces a self-contained HTML report.
Install lcov
| Platform | Command |
|---|---|
| macOS | brew install lcov |
| Debian/Ubuntu | apt install lcov |
| Fedora/RHEL | dnf install lcov |
Configure and build
# Coverage builds should use Debug to avoid optimisation hiding branches
cmake -B build-cov \
-DCMAKE_BUILD_TYPE=Debug \
-DLIBGOC_COVERAGE=ON
cmake --build build-covGenerate the HTML report
cmake --build build-cov --target coverage # Report written to: build-cov/coverage_html/index.html open build-cov/coverage_html/index.html # macOS xdg-open build-cov/coverage_html/index.html # Linux
The coverage target runs ctest internally, so there is no need to invoke the test binary separately. The final report includes branch coverage and filters out system headers and build-system generated files.
Note: Coverage and sanitizer builds are mutually exclusive — configure them in separate build directories. Coverage is also incompatible with
-DCMAKE_BUILD_TYPE=Releaseoptimisation levels that inline or eliminate branches.
Sanitizers
AddressSanitizer and ThreadSanitizer builds are available as opt-in targets.
# AddressSanitizer cmake -B build-asan -DLIBGOC_ASAN=ON -DCMAKE_BUILD_TYPE=Debug cmake --build build-asan ctest --test-dir build-asan --output-on-failure # ThreadSanitizer cmake -B build-tsan -DLIBGOC_TSAN=ON -DCMAKE_BUILD_TYPE=Debug cmake --build build-tsan ctest --test-dir build-tsan --output-on-failure
Note: ASAN and TSAN are mutually exclusive — configure them in separate build directories.
Copyright (c) Divyansh Prakash | MIT License
