Search docsSearch this language
Using C2Go
Release candidatev0.20260809.0-rc.5

These docs describe the current coordinated release candidate.

Using C2Go

Managed libc (mlib)

Use libc stateful APIs with direct GC-visible pointers and explicit managed ownership.

The root c2go_libc package preserves ordinary C-style unmanaged carriers. When one of those carriers needs to retain Go state, it usually stores an integer handle that resolves through a Go-owned table. mlib is the alternate surface for code already using C2Go managed memory: its carriers store direct, GC-visible Go pointers.

Use mlib when a libc-owned object must live inside a managed object graph. It is not a faster spelling of every libc function, and an mlib carrier must never be passed to the matching root-libc function.

One implementation model, two storage worlds

mlib does not compile the complete musl library twice:

  • stateless algorithms are shared;
  • Go-owned behavior cores are shared, with a root handle wrapper and a direct-pointer mlib wrapper;
  • C code is instantiated again only when record layout, allocation, pointer copying, or the returned object graph requires managed storage.

Semaphore and pthread synchronization, for example, share their Go behavior core. Managed regex needs a separate TRE instance because its allocator and ownership graph differ. This selective boundary keeps the root libc ABI stable without hiding managed pointers in unscanned C memory.

Include only the family you use

There is deliberately no mlib.h umbrella header. Include the focused headers that own the API:

#include <c2go.h>
#include <c2go/mlib/semaphore.h>
#include <c2go/mlib/pthread.h>
#include <c2go/mlib/dirent.h>
#include <c2go/mlib/glob.h>
#include <c2go/mlib/regex.h>
#include <c2go/mlib/search.h>
#include <c2go/mlib/stdio.h>
#include <c2go/mlib/string.h>
#include <c2go/mlib/stdlib.h>
#include <c2go/mlib/unistd.h>
#include <c2go/mlib/wchar.h>

The default names are explicitly namespaced, such as mlib_sem_t, mlib_sem_init, and mlib_fopen. This mode can coexist with the corresponding ordinary libc header.

Allocate typed carriers and retire both owners

A pointer-bearing carrier allocated on the Go heap needs its real type information:

#include <c2go.h>
#include <c2go/mlib/semaphore.h>

#pragma c2go managed push

struct managed Owner {
    mlib_sem_t *sem;
};

static struct Owner *new_owner(void) {
    struct Owner *owner = gc_malloc(
        c2go_typeinfo(struct Owner), sizeof(*owner));
    if (owner == NULL) return NULL;

    owner->sem = gc_malloc(
        c2go_typeinfo(mlib_sem_t), sizeof(*owner->sem));
    if (owner->sem == NULL || mlib_sem_init(owner->sem, 0, 1) != 0) {
        owner->sem = NULL;
        return owner;
    }
    return owner;
}

static void release_owner(struct Owner *owner) {
    if (owner != NULL && owner->sem != NULL) {
        mlib_sem_destroy(owner->sem); /* Clears the carrier state root. */
        owner->sem = NULL;            /* Clears the caller-owned carrier root. */
    }
}

#pragma c2go pop

GC eventually reclaims unreachable storage, but logical release is still explicit. destroy, close, delete, and free-shaped mlib operations clear every library-owned managed root they can reach. If the POSIX signature receives the descriptor by value, it cannot rewrite the caller’s variable; assign that final owner to NULL yourself.

Never call ordinary free on mlib storage. mlib does not expose or use malloc, realloc, or free. Use typed gc_malloc for pointer-bearing records and gc_malloc(NULL, size) only for storage that can never contain a managed pointer.

Optional standard source names

Define C2GO_MLIB_UNPREFIXED before the first mlib header when the whole LTO package should use standard source spellings:

#define C2GO_MLIB_UNPREFIXED 1
#include <c2go/mlib/semaphore.h>

#pragma c2go managed push
static void example(void) {
    sem_t sem;
    sem_init(&sem, 0, 1);
    sem_destroy(&sem);
}
#pragma c2go pop

This changes public names only; it does not change ownership. The mlib header must precede the corresponding ordinary libc header, and one LTO package must not mix managed and unmanaged definitions of the same standard API. Namespaced mode is the safer default for incremental adoption.

Current managed families

Family Managed ownership provided now
Synchronization unnamed semaphores; pthread lifecycle, keys, mutexes, condition variables, and rwlocks
Filesystem graphs DIR, scandir, Unix nftw/ftw, and glob
Streams managed FILE, standard streams, formatted and wide I/O, memory/cookie streams, and popen/pclose
Search containers AVL trees, hash tables, and linked queues with typed pointer stores
POSIX regex regcomp, regexec, and regfree with per-object GC arenas
Allocating helpers strdup, strndup, wcsdup, asprintf, vasprintf, realpath, and getcwd

This remains a selective surface. iconv is root-only because exact POSIX iconv_t error semantics do not fit a precise managed pointer slot. lsearch and lfind cannot infer a pointer bitmap from void * plus an element width, so they remain root functions for pointer-free elements.

Managed allocation does not remove C2Go’s stack-escape limitation. A library or callback must not retain the address of a C stack local after its frame returns; use a typed heap carrier and run the stack escape audit for the complete package.

Continue with multi-file projects and LTO once every translation unit uses one consistent ownership mode.