Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

From C to x2c

Start with the C representation you would normally choose. Add an x2c feature where it simplifies repeated declarations, traversal, cleanup, allocation lifetimes, or dynamic data. You can use each feature without converting the whole program to a new object model.

If the C program needs…Use…
one of several runtime value kindsVar
canonical text with interpolationString
persistent heterogeneous sequence dataList
a growable indexed collectionArray
a growable collection of key/value pairsMap
an already-linked native callFunc
code evaluated at run timeLisp and $lisp.bind
structural List casesmatch
traversal without exposing representationforeach or Iter
a grouped allocation lifetimeScope
bounded temporary work with isolated runtime stateContext
a native worker with isolated x2c stateThread and Mutex
cleanup on every exit pathdefer
structured failure across framesraise / try / filtered catch
an interface shared by concrete typesprotocol and adoption
a named type with default construction, Var conversion, comparison, and outputclass with representation-dependent defaults (methods)
methods forwarded to a contained valuea delegate field
repeated declarations or expressionsa compile-time macro or Decorator

What x2c adds

Native C types, layout, preprocessing, calls, and libraries keep their ordinary meaning. The language additions fall into three groups:

  1. Runtime values. Var, the standard collections, and embedded Lisp provide dynamic data and evaluation without requiring every value to be boxed.
  2. Control and lifetime. foreach, match, Scope, Context, Thread, defer, and Error replace common code for traversal, pattern matching, cleanup, worker state, and error propagation.
  3. Compile-time extension. protocol defines interfaces that concrete types explicitly adopt. macro and Decorator generate checked source before C emission.

The source tells you when these features apply. Assigning to Var boxes a native value. Protocol adoption works at compile time without creating a runtime interface object. Macros also run at translation time.

Keep C where C is already clear

Native declarations, expressions, structs, unions, enums, pointers, headers, and libraries remain available:

typedef struct Point {
  double x;
  double y;
} Point;

double length_squared(Point point) {
  return point.x * point.x + point.y * point.y;
}

Nothing is boxed and no runtime collection is involved.

Bring existing C files

Rename a C source file from .c to .x and it compiles as x2c. Headers stay .h files and are reached with #include. x2c collects the declarations a header provides and keeps the directive in the generated C, where the native compiler reads the header itself. An .x file is a translation unit, for which x2c writes its own C file and header.

x2c parses the source as written, before preprocessing, so every branch of an #ifdef that C could compile must parse. x2c output is compiled as C by a GNU-style compiler, so x2c skips three branches that never reach C: the branch under #ifdef __cplusplus or #if defined(__cplusplus), the branch under #ifdef _MSC_VER, and #if 0. The usual guard for C++ callers, a C++ template, or MSVC inline assembly in its own branch needs no change:

#ifdef __cplusplus
extern "C" {
#endif

int add(int left, int right);

#ifdef __cplusplus
}
#endif

A function defined once in each branch of one #ifdef is one definition.

The prefix macros that C libraries write before declarations parse as what they expand to. A macro defined to nothing, to an attribute, or to any mix of storage classes, inline, qualifiers, and builtin type words reads as that text: JSMN_API void jsmn_init(...), STBIDEF stbi_uc *stbi_load(...), int32 count, static z_const char *msg, and API twice(int x) with API defined to static int work as in C. A function-like macro that wraps its parameter in attributes, CJSON_PUBLIC(const char *) cJSON_Version(void), reads as the type inside. An attribute or _Noreturn before the type, __attribute__((unused)) static int f(void), is written after the storage class and before the type in the generated C, and is no part of the type that returns, conversions, and prototypes use. An attribute after a declarator or parameter, int a __attribute__((cleanup(release))) = 1, b = 2;, is written after that declarator. The GNU spellings __inline, __inline__, __restrict, and __restrict__ mean the standard keywords.

in and match are x2c keywords, and they are C identifiers as well. in is the x2c operator only between two operands, and match is the statement only as match (...) followed by case or {; anywhere else they are ordinary names, so struct buffer *in and int match = 0 compile.

A public object at file scope, int counter = 0; or struct tag { ... } name;, publishes an extern declaration in the generated header and its one definition in the C file, so every including unit shares that object. A struct or union body publishes the type as well. An initializer that has to run, such as Map registry = {};, runs in the unit’s initialization instead of standing as a C initializer, and a const object with such an initializer keeps the qualifier in x2c and loses it in the generated C. A public function whose prototype uses a struct that the file defines privately gets a forward declaration of the tag in the header first.

A macro invocation that supplies grammar, such as a for-loop macro or a call without a trailing semicolon, still needs adjustment.

A C11 generic selection reaches the generated C unchanged, and the native compiler chooses its association. The selection has no x2c type, so it works where C accepts the selected value directly:


int main(void) {
const char *text = "hello";
int kind = _Generic(text, char *: 0, const char *: 1);
  return kind == 1 ? 0 : 1;
}

Cross into Var deliberately

Box when a value is dynamic:

Var value = 42;
value = "forty-two";

if (value is String)
  printf("%s\n", value.string());

Heterogeneous Lists, Arrays, and Maps box their elements automatically. Conversions back to native types depend on the source and target types; not every pair can be converted.

Choose collection identity, not just syntax

  • List and String are canonical values.
  • Array and Map are mutable identity-bearing objects.
  • Empty List and String use null native representations but remain typed data.
  • Empty Array and Map are fresh allocated objects.
  • void represents missing or exhaustion and is not collection data.

These differences determine equality, lifetime, mutation, and the correct missing-value API.

Check whether the operation succeeded

When failure or absence is expected, use the operation that reports it separately:

Map settings = {theme: "dark"};
Var found;
if (settings.try_get(<theme>, &found))
  printf("theme=%s\n", found);

long number;
if (!"not a number".try_long(&number))
  printf("invalid integer\n");

Use a convenience adapter when you know its precondition holds and do not need the information it omits.

Choose who owns the native build

Four of the driver’s commands decide who owns the native build; the CLI reference lists the rest:

x2c translate --out-dir generated src/main.x
x2c build --output build/app src/main.x
x2c run src/main.x -- argument
x2c.com bootstrap --prefix /usr/local

Use translate when Make, Ninja, CMake, or another native build owns compilation and linking. Use build or run when the x2c driver can do that itself. Describe reusable projects with several targets in x2c.toml. The driver builds them the same way as targets named on the command line. The Cosmopolitan bootstrap command is an experiment for fun only; use the full repository for normal development. See Bootstrap a native installation for its limited contents.

Extend types through explicit contracts

A protocol declares the relationship once, and a separate adoption says which concrete type participates:


typedef struct Reading {
  int value;
} *Reading;

typedef struct Gauge {
  Reading reading;
} *Gauge;

protocol Reading(T) {
  int T.value(T);
}

Reading Gauge.reading(Gauge gauge) {
  return gauge.reading;
}

int Reading.value(Reading reading) {
  return reading.value;
}

protocol Reading(Gauge);

int main(void) {
  Reading reading = Scope.malloc(sizeof(struct Reading));
  reading.value = 42;
  Gauge gauge = Scope.malloc(sizeof(struct Gauge));
  gauge.reading = reading;
  return gauge.value() == 42 ? 0 : 1;
}

The compiler resolves each adopted member and generates any required adapters. A method with the same name but an incompatible signature is an error. Matching names alone do not make a type participate.

Use a protocol when several types should satisfy one contract. Do not introduce one to rename a single helper or hide a C call.

Private records can also adopt a protocol without exposing the type or its adapters:

#pragma private

typedef struct LocalJob {
  int id;
} *LocalJob;

static inline Var LocalJob.var(LocalJob job) {
  return Var.new(<localjob>, job);
}

static inline LocalJob Var.localjob(Var value) {
  return (LocalJob) value.pointer();
}

protocol Var(LocalJob);

int main(void) {
  LocalJob job = Scope.malloc(sizeof(struct LocalJob));
  job.id = 7;
  Var stored = job;
  LocalJob restored = stored;
  return restored.id == 7 ? 0 : 1;
}

Because the participant is private, its generated adapters stay in its C file. The public Var(T) protocol does not make LocalJob or its converters appear in the generated header.

Forward through composition

To forward a type’s method calls to one of its fields, mark that field delegate:

typedef struct Reading {
  int value;
} Reading;

typedef struct Gauge {
  delegate Reading reading;
} Gauge;

int Reading.value(Reading reading) {
  return reading.value;
}

int gauge_value(Gauge gauge) {
  return gauge.value();
}

The final call is a direct Reading_value(gauge.reading) call. Gauge does not become a Reading and does not adopt any protocol. Use a delegate field for this contained-value forwarding; use a protocol when Gauge must explicitly participate in a shared contract, as in the Reading(Gauge) example above.

Remove repeated source with typed macros

Macros match parsed, typed source positions. Their hole declarations make the bindings visible:


macro Expression $minutes(Expr $value) => ($value * 60)

int main(void) {
  int seconds = $minutes(2);
  return seconds == 120 ? 0 : 1;
}

Here $minutes is the qualified macro name, Expr $value declares one expression hole, and $value inserts the caller’s expression into the replacement. Other result kinds cover statements, fields, enum members, translation-unit items, and decorators.

Use a macro when its invocation is smaller and clearer than the source it replaces, and its meaning is obvious. Keep a function when runtime evaluation and a function signature already express the job. See Compile-time Macros for the grammar.

Guide chapters