Getting started with C

The same pxx compiler that builds Pascal also compiles C. It picks the C frontend from the .c extension; there is no separate tool. Everything on this page was run with pin v424 (compiler sha256 93a336a7ba85…) on 2026-09-25, from the root of a checkout set up as in Install.

Hello, world

hello.c:

#include <stdio.h>

int main(void)
{
    printf("Hello, world!\n");
    return 0;
}
$ ./pxx hello.c hello
ok: hello  [code=61841B  data=12928B  bss=72456B  procs=887  codeseg=65248B]
$ ./hello
Hello, world!

As with Pascal, the figures on the ok: line vary between builds; the line to check is ok: and the file name.

What you get: a static binary

hello is 78 KB and has no dynamic loader and no C library dependency:

$ readelf -d hello
There is no dynamic section in this file.

<stdio.h> did not come from your system. PXX ships its own C runtime, lib/crtl: its own headers and its own implementation, compiled into your program alongside your code, with anything unused removed. That is the default, and it is why the binary runs on any x86-64 Linux machine as it is.

What the built-in C runtime covers

The built-in runtime implements stdio.h, stdlib.h, string.h, math.h, ctype.h, time.h, signal.h, setjmp.h, unistd.h, fcntl.h, locale.h, assert.h, the socket headers, part of wchar.h and wctype.h, and a subset of pthread.h. A program using only these stays static.

Threads are part of it: a pthread_create program builds with --threadsafe and is still static. Without that flag the compiler refuses and tells you to add it.

./pxx --threadsafe threads.c threads

Full details, and the gaps, are on the C frontend page.

Using the host's C library instead

--system-libs makes the standard headers bind to your system's shared libraries instead of the built-in runtime:

Command What the binary depends on
./pxx prog.c prog (the default) nothing: static
./pxx --system-libs=m prog.c prog libm.so.6 only; everything else built in
./pxx --system-libs prog.c prog libc.so.6 and libm.so.6

Fixed after v424: with --system-libs (all of libc), v424 loses whatever the program printed with printf, because the program does not exit through libc and libc never flushes its buffer. The development tree fixes this. On v424, call fflush(stdout) before main returns.

Fixed after v424: other system libraries, such as zlib, SQLite or GTK, link from C. A function declared in a system header binds to that header's shared library when the library on your machine exports it, so #include <zlib.h> records libz.so.1 and the program runs. On v424 the program compiles, records only libc.so.6, and fails when started with undefined symbol: zlibVersion. There are two ways around that on v424:

The compiler still warns that the header came from /usr/include. A GTK program also needs --threadsafe, because GTK's headers include <pthread.h>.

A project with several files

pxx takes one source file per command. A project of several .c files is built in one of two ways.

greet.h:

#ifndef GREET_H
#define GREET_H

int add(int a, int b);
void greet(const char *name);

#endif

greet.c:

#include <stdio.h>
#include "greet.h"

int add(int a, int b)
{
    return a + b;
}

void greet(const char *name)
{
    printf("Hello, %s!\n", name);
}

main.c:

#include <stdio.h>
#include "greet.h"

int main(void)
{
    greet("pxx");
    printf("6 * 7 = %d\n", add(6 * 6, 6));
    return 0;
}

Separate objects, linked by PXX itself. Compile each file to an object, then link the objects. --link is PXX's own linker, so no ld or gcc is involved:

./pxx --emit-obj --function-sections greet.c greet.o
./pxx --emit-obj --function-sections main.c main.o
./pxx --link main.o greet.o prog
./prog
Hello, pxx!
6 * 7 = 42

--function-sections matters for size: each object carries its own copy of the parts of the runtime it uses, and this flag lets the linker drop the duplicates. Here it gives 113 KB instead of 537 KB. --link is x86-64 only today, and it prints its section statistics on standard error.

One translation unit. A file that includes the others builds as a single program, and the result is smallest (78 KB here):

/* all.c */
#include "main.c"
#include "greet.c"
./pxx all.c prog

This is how the large programs below are built. It needs the files not to define two static things with the same name, since they now share one scope.

Other CPUs

Add --target=. This program uses qsort, snprintf with a double, sqrt and strlen:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>

static int cmp(const void *a, const void *b)
{
    return *(const int *)a - *(const int *)b;
}

int main(void)
{
    int v[] = { 42, 7, 19, 3 };
    char buf[32];

    qsort(v, 4, sizeof v[0], cmp);
    snprintf(buf, sizeof buf, "%.4f", sqrt(2.0));
    printf("%d %d %d %d | %s | %zu | %d-bit\n", v[0], v[1], v[2], v[3], buf,
           strlen(buf), (int)(8 * sizeof(void *)));
    return 0;
}
./pxx --target=aarch64 cross.c cross.a64
tools/run_target.sh aarch64 cross.a64

Built with v424 and run under QEMU user mode, each output matches GCC's build of the same file on x86-64:

Target Output
x86-64 3 7 19 42 | 1.4142 | 6 | 64-bit
i386 3 7 19 42 | 1.4142 | 6 | 32-bit
aarch64 3 7 19 42 | 1.4142 | 6 | 64-bit
arm32 3 7 19 42 | 1.4142 | 6 | 32-bit
riscv32 3 7 19 42 | 1.4142 | 6 | 32-bit
wasm32 v424 refuses it: wasm: var-name pool full; the development tree prints 3 7 19 42 | 1.4142 | 6 | 32-bit

On v424, any wasm32 C program that includes math.h is refused with that message. The development tree builds and runs them under wasmtime.

C also runs on the ESP32 chips; see ESP32.

Real programs

Two larger programs show what the C frontend handles. Both are compiled from their unmodified release sources, and both are in the examples showcase with the exact commands:

Lua, zlib, cJSON and BusyBox are in the same table.

Next