C and C++ Interop¶
Dream calls C and C++ directly on native and wasm32 builds, including Dream Task workers. A package puts portable C or C++ sources in native/,
then writes a thin Dream wrapper: one declaration per native function or method, plus whatever
idiomatic Dream API it wants on top. Consumers just import the package.
At a glance
- Declare C functions with
@cand call them like Dream functions. - Package C sources in
native/; they build for native and WebAssembly targets.
- C is bound with
@cexterns. - C++ is bound with
@cppdeclarations. The compiler generates a small C++ shim that implements them, and the C++ compiler checks each declaration against the real header. - The compiler never parses headers. Declarations are ordinary Dream source, so completion, hover, and go-to-definition work on them.
- Native builds can link package sources or host system libraries. WASM builds compile portable
package sources into the same guest module; WASI libc/libm bindings (
@c("c", ...)/@c("m", ...)) also work. Host libraries such as system libsqlite3 cannot be linked into it. Unused bindings are pruned before checking target availability.
For native builds, Zig (installed by dreamer toolchain install cc) compiles the C/C++ and links
it with the Dream object. DREAM_CXX / CXX override the native C++ compiler. WASM package builds
use the pinned clang.
Runnable samples: sample/native_c/ and
sample/native_cpp/.
Explore this topic¶
Package layout¶
ticker/
├── dream.toml # [package] name = "ticker"
├── src/ticker.dream # the thin wrapper
└── native/
├── include/ticker.h # on the include path
├── ticker.c
└── vendor/lz4.c # mixed C and C++ is fine
Everything under native/ compiles as one native set named after the package (kv-store →
kv_store). native/include/ is on the include path. .c files compile as gnu11, and .cpp /
.cc / .cxx files as gnu++20. libc++ is linked only when the set contains C++.
A set is built only when the program calls into it. Native objects live in
target/<profile>/native-c/<set>/ and are rebuilt when a source is newer. WASM package bitcode
is rebuilt beside the module in its .wasm-c/ directory.
dream.toml¶
A [native.<set>] table is optional. It adds build settings to the implicit set, or declares an
extra set with explicit sources:
[package]
name = "kvstore"
links = "kv" # optional: at most one package in the graph may provide "kv"
[native.kvstore]
cflags = ["-fno-exceptions"]
defines = ["KV_THREADSAFE=1"]
[native.kvstore.macos]
frameworks = ["Security"]
[native.kvstore.linux]
libs = ["dl"]
| Key | Meaning |
|---|---|
sources |
Extra source files, relative to the package root |
include |
Extra include directories |
defines |
-D definitions |
cflags |
Extra compiler flags |
frameworks |
macOS frameworks to link |
libs |
System libraries to link (-l) |
Target subtables (macos, linux, windows, wasm) take the same keys and are appended for the build target. A WASM build never inherits the host OS overlay.
A set with no sources for the selected target, or a source that does not exist, is an error. So are an
unknown key, two packages declaring the same set name, and two packages declaring the same
[package] links value. Each of these errors names the manifests involved.
WebAssembly¶
The existing native/ directory and @c / @cpp declarations also work for portable WASM
packages. Add guest-only flags with [native.<set>.wasm]. Use usize / isize for C size_t /
ptrdiff_t; long in Dream is always 64 bits. Scalar signature mismatches between a binding
and its compiled package implementation produce a build diagnostic.
Portable package sources become part of your WebAssembly program. Dream can optimize calls between your code and the package, though a particular speed improvement is not guaranteed. A prebuilt desktop library cannot be used this way.
C++ uses the WASI SDK 33 exception-enabled libc++, libc++abi and libunwind. Generated @cpp
shims preserve exception-to-Result conversion. Global constructors run after Dream heap
initialization; exit hooks run registered destructors and flush stdio after main completes.
The JS loader supports output during constructors in both full and selective runtimes.
C allocation uses the same linear memory and heap as Dream, with C-compatible alignment. Pointers and callbacks remain within that module. Stored callbacks must be invoked and released on the instance that created them. Owned pointers, UTF-8 strings, array data, unmanaged structs, and callbacks use the same bindings as native builds.
Supported libc services include stdout/stderr, realtime/monotonic clocks and an empty process environment. OS-specific
functions or unresolved library symbols fail during the build, instead of becoming broken host
imports. Use @js for browser/Node platform services. In Task / shared-memory modules,
each instance gets initialized, aligned C thread-local storage. A worker pool retains its
thread-local values between dispatches. Global constructors run once after the primary
instance's TLS is ready. Workers come from Dream's Task runtime; WASI pthread_create is
not supplied. Use @native, @node or @web to restrict a binding to specific runtimes.
Development installs need the full WASI sysroot next to the installed build tools; update it with
scripts/fetch-dev-llvm.sh or scripts/fetch-dev-llvm.ps1 on Windows. Release packaging includes
clang, its resource headers, and the wasm32 WASI headers/libraries needed by package builds.
C: @c¶
import system.interop;
module ticker;
import system;
@c
extern fun ticker_new(name: string): CPtr;
@c
extern fun ticker_run(t: CPtr, ticks: int): int;
@c("m", "cbrt")
extern fun cube_root(x: double): double;
- Bare
@cbinds to the declaring package's ownnative/set. The Dream name is the C symbol. @c("lib")names another library, and@c("lib", "symbol")also renames the symbol. A library that is not a native set is linked from the system (-l<lib>); see Linking system libraries.@ccannot be combined with@js,@runtime, or@intrinsic.
Types at the boundary¶
| Dream | C |
|---|---|
int, long, float, double |
int32_t, int64_t, float, double |
isize, usize |
intptr_t, uintptr_t (usize also matches size_t) |
bool, char, byte |
bool, char, uint8_t |
string parameter |
const char*: NUL-terminated UTF-8, valid for the call |
string result |
const char*, copied into a Dream string; C keeps ownership |
CPtr |
void* / T* |
Option<string>, Option<CPtr> |
a pointer where NULL is None |
T[] (numbers, byte, plain-data structs) |
T* to the first element, valid for the call |
| plain-data value struct (see Structs) | the C struct by value, or T* as ref x: T |
ref x: T |
T*: C writes through it (ref p: CPtr is T**) |
fun(...) |
a C function pointer (see Callbacks) |
NativeCallback<F> |
a (fn, void* user_data) pair |
By-value char converts to C’s narrow char, including fields of structs passed by value.
Pointers and ref arguments expose Dream storage: a char occupies four bytes, so use
int32_t* for a character slot and byte[] for a raw C character buffer.
| Option<fun(...)>, Option<NativeCallback<F>> | a nullable function pointer |
| OwnedCPtr result with @owned("free_fn") | void* that Dream frees with free_fn |
Every @c call goes through a small C shim the compiler generates and compiles with the pinned
clang, so the platform C ABI (struct passing and return, bool/char widths, calling
conventions) is clang's, not a hand-written copy.
Classes, unions, and arrays of managed elements are rejected in @c signatures. They are heap
references that C does not understand. Wrap them in a Dream function that passes C what it needs.
A NULL returned where the declaration says string traps with a message naming the function.
Declare the result as Option<string> when C may return NULL.
Arrays pass only the pointer. When C needs the length, declare it as a separate parameter and pass
xs.length:
import system.interop;
@c
extern fun checksum(data: byte[], len: long): int;
public fun fingerprint(data: byte[]): int {
return checksum(data, data.length);
}
CPtr¶
CPtr (in system) is an opaque C pointer. It has CPtr.null(), is_null(), ==,
address(): usize, and offset(bytes: isize). Its single address field is target-sized. Use it for handles, and use ref for C's out-parameters:
import system.interop;
@c("sqlite3")
extern fun sqlite3_open(path: string, ref db: CPtr): int;
let db = CPtr.null();
if sqlite3_open("app.db", ref db) != 0 {
...
}
Ffi.read_ptr, Ffi.read_int, Ffi.read_long, Ffi.read_double, and Ffi.read_cstring read
through pointers C hands to Dream (char** rows and similar).
Owned pointers¶
When C hands back a pointer the caller must free, return OwnedCPtr and name the C function that
frees it with @owned("free_fn"). The object calls free_fn(ptr) when its last reference goes
away. get() reads the pointer while the object keeps owning it; take() hands ownership back to
you and leaves the object holding NULL.
import system.interop;
@c
@owned("ticker_free")
extern fun ticker_new(name: string): OwnedCPtr;
@c
extern fun ticker_run(t: CPtr, ticks: int): int;
let t = ticker_new("demo");
ticker_run(t.get(), 3); // ticker_free runs when `t` goes out of scope
free_fn must be a C identifier with the signature void free_fn(void*). A @c extern returning
OwnedCPtr without @owned("free_fn"), or @owned("free_fn") on any other result, is an error.
Structs¶
A value struct whose fields are numbers, bool/char/byte, or other such structs maps to the C
struct with the same fields in the same order. Pass it by value, return it by value, or pass it by
ref, which C sees as T*. Mark structs @packed when the C header uses #pragma pack(1) /
__attribute__((packed)). Otherwise fields are naturally aligned, as in C.
import system.interop;
public struct Vec2 {
public x: double;
public y: double;
}
@c
extern fun vec2_scale(v: Vec2, k: double): Vec2; // C: Vec2 vec2_scale(Vec2 v, double k)
@c
extern fun vec2_len(ref v: Vec2): double; // C: double vec2_len(const Vec2* v)
@c
extern fun vec2_origin(ref out: Vec2): void;
Strings¶
string parameters are copied into a NUL-terminated UTF-8 buffer that is freed after the call.
@marshal(MarshalKind.LpWStr) passes UTF-16 instead (Windows LPWSTR):
import system.interop;
@c("user32", "MessageBoxW")
@marshal(MarshalKind.LpWStr)
extern fun MessageBoxW(hwnd: CPtr, text: string, caption: string, flags: int): int;
Externs use the platform C calling convention. @c_call(CCallConv.Cdecl) spells that out explicitly.
@c_call(CCallConv.Stdcall) selects __stdcall on 32-bit x86 Windows and is the default convention
everywhere else. Any other convention is a compile error.
Packages¶
- Packages under
dream_packages/bring their ownnative/sets. A consumer only imports the package; its sets are built and linked when the program calls into them. - Shim symbols are namespaced by native set, so two packages can each bind a C++ class called
Store. Dream class names are program-wide, so give the Dream classes distinct names (@cpp("alpha.hpp", "alpha::Store") public class AlphaStore). [package] links = "sqlite3"declares that a package provides a native library, and at most one package in the dependency graph may do so.dreamer packproduces a single executable. Native sets are linked in statically.
Linking system libraries¶
On native targets, @c("lib", ...) with a library that is not a native set links -l<lib>. Search order:
native/<lib>next to the source (vendored shared libraries).- The directory containing the source, then the current working directory.
- Standard system directories:
- macOS:
/opt/homebrew/lib,/usr/local/lib,/opt/local/lib,/usr/lib - Linux:
/usr/local/lib,/usr/lib/x86_64-linux-gnu,/usr/lib/aarch64-linux-gnu,/usr/lib64,/usr/lib,/lib - Windows:
%WINDIR%\System32 - The OS loader's own path (
DYLD_FALLBACK_LIBRARY_PATH/LD_LIBRARY_PATH).
sample/sqlite/ binds the system libsqlite3 this way:
Compared to @js and @runtime¶
| Concern | @js("mod", "field") |
@runtime("name") |
@c / @cpp |
|---|---|---|---|
| Host | JS (Node / browser) | Dream runtime (WASM + native) | Native and portable wasm32 package builds |
| Async | async + Promise bridge |
async + host future |
Not supported; C runs on the caller's thread |
| Out-params | Return a wrapper struct / tuple | Return a wrapper struct / tuple | ref parameters |
| Callbacks into Dream | fun(...) values |
host-defined | fun pointers, NativeCallback, std::function |
| Null | Option |
Option |
Option |