C and C++ interop
C and C++ headers import directly with no bindings to write, and cx functions can be exported for C and C++ callers to link against.
Using C libraries
For libraries the compiler vendors modules for, import the module instead of the C header: it compiles faster and carries precise nullability. See Vendored C libraries.
Other C headers can be imported directly from cx code. To import a header, write an import declaration with the header file name:
import "stdio.h";
void main() {
printf("hello, world\n");
}Imported functions are called like ordinary cx functions.
Imported type name collisions
An imported struct, union, or enum shares its name with any cx type of the same spelling, so defining both is a redefinition error; use the imported type from all files instead of defining a second one. An empty struct or union is exempt as an opaque placeholder for the imported type. A generic cx type is a different spelling, so it coexists with a same-named import: argument count picks the template when arguments are present.
Memory allocation functions work the same way:
import "stdlib.h";
void main() {
var numbers = malloc(sizeof(int) * 3);
free(numbers);
}Numbers, characters, and strings are imported as constants, as are simple expressions over numbers and characters:
import "stdio.h";
void main() {
println(EOF); // prints -1
}Compound literals of an imported struct are imported as constants when every element is constant; the elements may be numbers, characters, earlier scalar macros, or nested compound literals:
typedef struct Color { unsigned char r, g, b, a; } Color;
#define RAYWHITE (Color){ 245, 245, 245, 255 }import "raylib.h";
void main() {
println(RAYWHITE.g); // prints 245
}Macro import limits
Partial initializers, designators, unions, and structs with anonymous members are not imported.
Function-like macros and casts are not imported.
Individual functions can also be declared with extern,
without importing a header. The standard library uses
extern declarations in std/system/libc.cx to
give common C functions more precise cx types. Declare parameters and
return types with the c_ prefixed types
(c_int, c_uint, c_long,
c_double, and the rest: see Builtin
types) so the signature matches the C headers on every target:
extern c_int putchar(c_int ch);
void main() {
putchar('f'); // prints "f"
}C struct fields are accessed with ., just like cx struct
fields. A cx function can be passed where a C function pointer is
expected:
import "stdlib.h";
void onExit() {
println("exiting");
}
void main() {
atexit(onExit);
println("running");
}Additional include directories, preprocessor definitions, and
libraries are passed on the command line with -I,
-D, -L, and -l. See Modules and imports for how imports are resolved.
Const in C APIs
cx has no const-qualified pointees: const never appears
in types. C and C++ const is dropped when declarations are
imported, so a C const char* becomes plain
char*. Passing a mutable pointer where C expects a const
one is safe (C only reads through it); writing through a pointer that C
considers const is undefined behavior, and the compiler cannot check it.
Named constants still use the const declarator
(const x = ...).
Calling cx from C
An extern function with a body is exported with C
linkage under its plain name, so C callers link it like any C function.
It stays callable from cx too:
extern c_int c_add(c_int a, c_int b) {
return a + b;
}
void main() {
println(c_add(40, 2)); // prints 42
}Compile the cx side to an object file, then link it into the C program:
cx cxlib.cx -c -o cxlib.o
cc main.c cxlib.o -o c-callerOn Windows, link the object file with link.exe, adding
the C runtime libraries the driver links automatically, plus your own
libraries as .lib files:
cx main.cx -c -o main.obj
link main.obj legacy_stdio_definitions.lib ucrt.lib msvcrt.lib /SUBSYSTEM:CONSOLE /OUT:demo.exeSignatures crossing the boundary must be C-compatible: plain numbers,
pointers, and C structs. Generic functions cannot be
extern.
extern C value-crossing limits
Structs cross by value following the platform C ABI, including small float-containing ones, which cross in registers on x86-64 and AArch64 (on other targets those are rejected). Aggregates holding 80-bit floats are rejected; pass them behind a pointer or reference instead, and return them through an out-parameter.
An extern function must be called directly; referring to
one as a value is rejected because a call through a function pointer
cannot use the callee's C calling convention. See the c-interop
example for a complete project calling in both directions.
Using C++ libraries
C++ headers can be imported the same way as C headers. A header with
a C++ extension (.hpp, .hh, .hxx,
.h++, or .H) is parsed as C++, and its
global-scope functions and plain structs become available, including
overloads (from the cxx-interop example):
import "veclib.hpp";
void main() {
Accum a;
a.count = 3;
a.total = 14;
println(accum_add(a, 1)); // prints 15
}C++ header import boundaries
Only declarations written in the imported header itself are imported. Headers it includes are parsed for context, so their types work in signatures, but their declarations are not imported; import each header you use directly. Unsupported declarations (namespaces, templates, globals, and non-POD types) are skipped with a warning naming how many were skipped.
Signatures crossing the boundary must be C++-compatible: plain numbers, pointers, references, function pointers, and plain structs.
C++ value-crossing limits
Structs cross by value only when they are small (up to 16 bytes),
normally aligned, and hold no floats; anything else must cross behind a
pointer or reference. The same rules apply to extra arguments passed
through .... Structs are never returned by value; return
through an out-parameter instead.
A cx function can be passed where a C++ function pointer is expected.
extern "C" functions in C++ headers keep their plain names
and work as usual. Classes with methods and exceptions are not
supported.
Individual functions can also be declared with
extern "C++", without importing a header. The compiler
mangles the name with the Itanium C++ ABI, so it links against the C++
definition. Declare parameters and return types with the c_
prefixed types so the signature matches on every target:
extern "C++" c_int cpp_sum(c_int* values, c_int count);C++ const on parameters is dropped, so cx cannot
distinguish overloads that differ only in constness. A hand-written
declaration links against the mutable overload when one exists. To reach
a const-only overload, qualify the pointee instead:
extern "C++" c_int cpp_find(const c_int* values, c_int count);const T* and const T& parameters mangle
with const while staying mutable in cx; callers pass pointers and
borrows as usual.
Const qualifier restrictions
The qualifier is mangle-only: it is rejected on by-value parameters, on definitions, and on multi-level pointees, and declaring both the mutable and the const overload collides as a redefinition. When both overloads come from an imported header instead, the import succeeds and calls resolve to whichever overload is declared first, mangled with its original constness.
Calling cx from C++
An extern "C++" function with a body is exported under
its Itanium-mangled name, so C++ callers declare and link it like any
C++ function. It stays callable from cx too. The Itanium ABI is not
supported on Windows/MSVC targets, so this does not work there:
extern "C++" c_int cx_is_even(c_int n) {
return n % 2 == 0 ? 1 : 0;
}
void main() {
println(cx_is_even(42)); // prints 1
}int cx_is_even(int n);cx cxlib.cx -c -o cxlib.o
c++ main.cpp cxlib.o -o cxx-callerThe same signature rules apply in both directions. Only free
functions can be extern "C++"; generic functions cannot be
extern. C++ interop uses the Itanium ABI and is not
supported on MSVC targets. See the cxx-interop
example for a complete project calling in both directions.