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 kinds | Var |
| canonical text with interpolation | String |
| persistent heterogeneous sequence data | List |
| a growable indexed collection | Array |
| a growable collection of key/value pairs | Map |
| an already-linked native call | Func |
| code evaluated at run time | Lisp and $lisp.bind |
structural List cases | match |
| traversal without exposing representation | foreach or Iter |
| a grouped allocation lifetime | Scope |
| bounded temporary work with isolated runtime state | Context |
| a native worker with isolated x2c state | Thread and Mutex |
| cleanup on every exit path | defer |
| structured failure across frames | raise / try / filtered catch |
| an interface shared by concrete types | protocol and adoption |
| a named type with default construction, Var conversion, comparison, and output | class with representation-dependent defaults (methods) |
| methods forwarded to a contained value | a delegate field |
| repeated declarations or expressions | a 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:
- Runtime values.
Var, the standard collections, and embedded Lisp provide dynamic data and evaluation without requiring every value to be boxed. - Control and lifetime.
foreach,match,Scope,Context,Thread,defer, andErrorreplace common code for traversal, pattern matching, cleanup, worker state, and error propagation. - Compile-time extension.
protocoldefines interfaces that concrete types explicitly adopt.macroandDecoratorgenerate 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
ListandStringare canonical values.ArrayandMapare mutable identity-bearing objects.- Empty
ListandStringuse null native representations but remain typed data. - Empty
ArrayandMapare fresh allocated objects. voidrepresents 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
- Values and Var: dynamic values, boxing, conversion, arithmetic.
- Symbols and Atoms: compact tags versus exact names.
- Collections: text and collection construction/mutation.
- Pattern Matching: structural
Listdecisions. - Iteration:
foreachand lazy pipelines. - Scopes and Lifetime: grouped allocation ownership.
- Contexts and Threads: bounded work, native workers, result export, and shared-memory rules.
- Errors and Cleanup: transfer and guaranteed cleanup.
- Protocols: explicit adoption, adapters, punctuation, boxed behavior.
- Compile-time Macros: typed templates, imports, decorators, and when to use them.
- Idioms: putting those facilities together.