26Vendoring C Libraries
cryo vendor makes a C library importable as a Cryo module. You point it at the library's source tree once, and it reads the library's headers through libclang and writes Cryo bindings for them: the structs, enums, constants and an extern "C" block for the functions. It also records how the library is linked. After that, any project on the machine can write import vendor::Name; and call into the library with no extern declarations and no [link] entries of its own.
import std::fmt;
import vendor::RayLib;
function main() -> int {
RayLib::InitWindow(800, 450, "Cryo + raylib");
const bg = RayLib::Color { r: 245, g: 245, b: 245, a: 255 };
RayLib::ClearBackground(bg);
RayLib::BeginDrawing();
RayLib::DrawText(
"Hello, Cryo + raylib!",
190, 200, 20,
RayLib::Color { r: 0, g: 0, b: 0, a: 255 }
);
RayLib::EndDrawing();
RayLib::CloseWindow();
return 0;
}
Vendoring is the automated form of the hand-written bindings in section 18. The generated file is ordinary Cryo, and everything that section says about extern "C", ![repr(C)] and pointers applies to it.
cryo vendordoes not compile the library. It generates bindings and records link flags. The native library itself — the.a,.libor shared object the linker needs — has to be built or installed by its own build system first, exactly as it would be for a C project.
The manifest
A library is vendored from a directory containing a cryo-vendor.json manifest, usually the root of the library's checkout:
{
"name": "RayLib",
"binding_source": "headers",
"headers": ["raylib.h"],
"include_dirs": ["src"],
"defines": ["PLATFORM_DESKTOP"],
"link": {
"system": ["raylib"],
"system_unix": ["GL", "m", "pthread", "dl", "rt", "X11"],
"system_windows": ["opengl32", "gdi32", "winmm"]
}
}
| Key | Meaning |
|---|---|
name | The module name, as in import vendor::RayLib. When omitted it is derived from the directory name, capitalised. |
binding_source | Where the bindings come from: "headers" generates them from C headers; "manual" (the default) uses a hand-written bindings_file; "cxx-headers" is the experimental C++ generator. |
headers | The headers to bind, resolved against include_dirs and then the manifest's directory. Whatever they #include is followed. |
include_dirs | -I directories for libclang, relative to the manifest's directory. |
defines | -D preprocessor definitions, for headers that need a configuration macro to expose their API. |
language | "c" (the default) or "c++". std sets the C++ standard, c++17 by default. |
link | How a consumer links the library: system (-l<name>), search (-L<dir>), and static (an archive path passed verbatim), each with _unix and _windows variants. These are the same roles as a project's [link] table. |
bindings_file | A pre-written .cryo bindings file, relative to the manifest's directory, copied in as-is instead of generating one. |
static and search paths are not rebased onto the library's directory, so give them as absolute paths. _comment is ignored, which leaves room to leave a note for whoever edits the manifest next.
Registering a library
cryo vendor /path/to/libgreet
Generating bindings for 'Greet' from 1 header(s) via libclang...
Translation report for 'Greet': 0 not bound, 0 approximated (+1 ignored: header guards / markers / function-like macros).
Registered vendor library 'libgreet' -> import vendor::Greet
source: /path/to/libgreet
bindings: ~/.cache/cryo/vendor/libgreet/host/Greet.cryo
cached for triple 'host'
The registry is per machine, not per project: a library registered once is importable from every project. It lives in the Cryo cache, at $CRYO_HOME/cache when that is set, and otherwise at $XDG_CACHE_HOME/cryo or ~/.cache/cryo on Linux and %LOCALAPPDATA%\cryo on Windows:
<cache>/vendor/
+-- registry.json # every registered library and its manifest
+-- <key>/<triple>/<Name>.cryo # generated bindings, one per target triple
The translation report lists every declaration the generator could not bind ([skip]) or could bind only approximately ([approx]), so nothing goes missing silently. Function-like macros and compound-literal macros, such as raylib's RAYWHITE, have no Cryo equivalent and are dropped. Header guards and other marker macros are counted as ignored rather than reported.
A C header like this one:
#define GREET_VERSION 3
typedef struct GreetSize {
int width;
int height;
} GreetSize;
int greet_add(int a, int b);
becomes this:
/// Auto-generated by `cryo vendor` from C headers. DO NOT EDIT.
/// Regenerate with: cryo vendor rebuild Greet
namespace Greet;
![repr(C)]
type struct GreetSize {
width: i32;
height: i32;
}
const GREET_VERSION: i32 = 3 as i32;
extern "C" {
function greet_add(a: i32, b: i32) -> i32;
}
Generation is cached. The cache key covers every header the library's headers reached, along with the manifest's include directories, defines and language, the target triple, and the libclang and generator versions. Registering an unchanged library again reports Bindings for 'Greet' are up to date and does no work.
Using a vendored library
Import it by the manifest's name under vendor::, and name its items through that module:
import std::fmt;
import vendor::Greet;
function main() -> int {
const size: Greet::GreetSize = Greet::GreetSize { width: 6, height: 7 };
fmt::printf("%d\n", Greet::greet_add(Greet::GREET_VERSION, size.width));
return 0;
}
The project's cryoconfig needs no vendor section and no [link] entries for the library. When a build reaches a vendor:: import, the library's link block is merged into the project's link line: the common entries first, then the _unix or _windows overlay for the target. A vendored library the build never imports contributes nothing.
vendor:: imports are resolved by project builds (cryo build, run and test in a directory with a cryoconfig). A single file compiled on its own does not see the registry. An import of a library that is not registered is the ordinary E0500, "cannot find module".
Pinning the bindings: cryoconfig.lock
A build that imports a vendored library records a hash of the bindings it compiled against in the project's cryoconfig.lock, next to cryoconfig. The file is shared with the git dependencies of [dependencies]:
{
"schema_version": 2,
"packages": [],
"vendor": [
{
"name": "Greet",
"triple": "host",
"binding_sha": "8eee1b64c33942be581eb13ebbe9a4cf"
}
]
}
The hash is of the generated .cryo file's contents, not of the library's version or sources. A new header or toolchain that produces the same bindings is therefore not a change, and one that alters them is. A normal build updates the lockfile when the bindings change and says so (note: vendor 'Greet' (triple 'host') bindings changed; updating cryoconfig.lock).
Commit the lockfile, and build with --locked in CI. A --locked build only checks the lockfile, never writes it, and fails if any vendored library's bindings differ from the pin or have no entry:
error: --locked: vendor 'Greet' (triple 'host') bindings drifted from cryoconfig.lock.
locked 1cfc7b9786ea07aee35e5bdf97f2b421, built 8eee1b64c33942be581eb13ebbe9a4cf. Run `cryo vendor rebuild Greet` and re-lock, or restore the toolchain.
Cross-compiling
Bindings are cached per target triple, because a header can declare different things on different targets. cryo vendor caches for the host unless told otherwise:
cryo vendor --target=x86_64-pc-windows-gnu /path/to/libgreet
A build for a triple the library has no bindings for stops with a note naming the command that generates them. A project can also pin one library to a triple in its cryoconfig. If the library generates its bindings from headers, the build then creates the missing ones itself:
[vendor.Greet]
triple = "x86_64-pc-windows-gnu"
Managing the registry
| Command | What it does |
|---|---|
cryo vendor <path> | Register the library at <path>, or re-register it after its manifest changed. cryo vendor add <path> is the same command. |
cryo vendor list | Every registered library: its import name, where its bindings come from, the triples it has bindings for, and its source directory, flagged when the directory is gone. |
cryo vendor rebuild [name] | Regenerate a library's bindings from its recorded source directory, re-reading the manifest and ignoring the cache. With no name, every library is rebuilt. Honours --target. |
cryo vendor remove <name> | Unregister a library, by import name or key. Its cached bindings stay on disk. |
cryo vendor clean | Unregister every library whose source directory no longer exists. |
Registered vendor libraries (1)
Bindings cache: ~/.cache/cryo/vendor
+- libgreet ---------------------------------------------------------------+
| import as vendor::Greet |
| bindings generated from C headers |
| targets host |
| source /path/to/libgreet |
| |
+--------------------------------------------------------------------------+
C++ libraries
"binding_source": "cxx-headers" (or "language": "c++") runs the generator over C++ headers, binding functions by their mangled symbols. It is experimental: a project that imports such a library has to opt in, or the import is refused with E0505:
[experimental]
cxx_ffi = true
A C++ library is linked against the C++ standard library automatically. Overloads, and leaf names that collide, keep only their first declaration and report the rest. "cxx-shim", a binding mode that would wrap a C++ API behind a generated C shim, is reserved and not implemented.
import a::b;
import a::b::{ X, Y };
import a::b as c;
import a::b::*;Brings a module, or selected items from it, into scope. One path per declaration: use the brace form for several items, as to alias, and * for everything public (prefer the brace form; wildcards invite name collisions).
function name<T>(param: Type) -> Ret { ... }Declares a function. Parameters are written name: Type and are never inferred; the return type follows -> and defaults to void. Declaration order does not matter: every signature is collected before any body is checked, so functions may call each other freely and recurse.
function main() -> intconst name: Type = value;An immutable binding: it cannot be reassigned after initialisation. The annotation may be omitted when an initialiser is present - the binding takes the initialiser's concrete type, inferred locally. Mutability is opt-in through mut.
const bg: Colorconst bg: Colorreturn value;Leaves the current function with the given value. A function whose return type is not void must return one; a bare return; is for void. In an async function it completes the future with the value.
namespace app::module;Every source file opens with one: its position in the module hierarchy. The compiler uses it to resolve imports and to mangle symbol names, so two parse functions in different modules never collide at link time.
type struct Name { ... }
type enum Name { ... }
type trait Name { ... }
type class Name { ... }Begins a type declaration; the keyword that follows says which kind. On its own, type Name = Existing; declares an alias.
type struct Name {
field: Type;
method(&this) -> Ret { ... }
}A value type with named fields and optional methods. Structs live on the stack and are passed by value. Fields are public by default and may carry = default values; methods take their receiver as &this, mut &this, or this.
type struct GreetSizeGreetSize.width: i32GreetSize.height: i32const NAME: Type = value;At module level, a true compile-time constant, conventionally SCREAMING_SNAKE_CASE. (mut at module level declares mutable global state instead.)
const GREET_VERSION: i32value as TypeExplicit conversion: between numeric types, or reinterpreting one pointer type as another. Cryo never converts implicitly, and as inserts no range checks - a narrowing cast is the programmer's responsibility.
extern function exit(code: i32) -> void;
extern "C" {
function puts(s: string) -> i32;
}Declares a function whose body the linker supplies - typically a C library symbol. The compiler trusts the Cryo signature; keeping it in step with the real C signature is on you.
language referencefunction greet_add(a: i32, b: i32) -> i32(parameter) a: i32(parameter) b: i32const size: Greet::GreetSizefunction printf(fmt: string, args...) -> i32Write a printf-style formatted string to stdout. Returns the number of bytes written, or a negative value on error (libc printf convention). Use this for genuinely variadic %s/%d output; for value formatting prefer an f-string through println (Display-based) below.
const size: Greet::GreetSizeGreetSize.width: i32static name(param: Type) -> Ret { ... }A method that belongs to the type rather than an instance, called as Type::name(..). For structs, static new(..) returning a struct literal is the idiomatic constructor.
for (mut i: i32 = 0; i < n; i++) { ... }
for (item in iterable) { ... }Two forms. The C-style for (init; condition; step) scopes its loop variable to the body. for (x in expr) iterates anything with next() -> Option<T>: an iterator, a range such as 0..n, an Array or Slice (their iter() is inserted for you), or a fixed-size array.