Skip to content
CryoCryo home
LanguageToolchain

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 vendor does not compile the library. It generates bindings and records link flags. The native library itself — the .a, .lib or 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"]
  }
}
KeyMeaning
nameThe module name, as in import vendor::RayLib. When omitted it is derived from the directory name, capitalised.
binding_sourceWhere 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.
headersThe 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.
linkHow 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_fileA 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

CommandWhat 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 listEvery 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 cleanUnregister 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.