llama_cpp package
C+ bindings for upstream llama.cpp.
The package name is llama_cpp because C+ dependency names are lowercase
identifiers. Use it like this:
[dependencies]
llama_cpp = "*"
import "llama_cpp/llama_cpp" as llama;
llama::backend_init();
guard let result::Result[llama::Session, llama::LlamaError]::Ok(loaded) =
llama::load("model.gguf", context_size: 4096 as u32) else { return 1; };
var session: llama::Session = loaded;
guard let result::Result[text::Text, llama::LlamaError]::Ok(answer) =
session.generate("The capital of France is", max_new_tokens: 32) else { return 1; };
io::println(answer.view());
Architecture
This package binds upstream's C APIs, not its C++ internals:
llama.cpp include/llama.h --cpc-bindgen--> src/raw.cplus
llama.cpp tools/mtmd/mtmd.h --cpc-bindgen--> src/mtmd_raw.cplus
src/llama_cpp.cplus (the facade)
src/raw.cplus and src/mtmd_raw.cplus are generated — do not hand-edit
them. Struct layouts come from clang's own record layout of the real header,
so they are correct by construction, and opaque upstream handles arrive as
*u8 with no ggml types to model.
There is deliberately no curated copy of the headers in this package. There
used to be one (upstream/llama_cplus.h), and it is the reason this package
needed regenerating: it still declared llama_model_params::use_mmap,
use_direct_io and use_mlock long after upstream deleted them, and had never
heard of load_mode, lazy_mode or load_mtp. Every field past
n_gpu_layers was reading the wrong bytes. A hand-written ABI mirror is a
second copy of a layout that nothing checks; regenerating from the real header
removes the whole class of bug.
The facade surface:
backend_init/backend_freeload— a GGUF into a ready-to-runSession, with labeled defaults for context size, GPU layers, threads, and the sampler chainSession::generate— prompt to an ownedText, greedy or sampledSession::tokens_for/tokenize/token_piece/decode/sample— the lower-level loop, for callers that want their own control flowSession::describe,context_size,train_context_size,model_size,parameter_count,embedding_size,vocabulary_size,resetmtmd_caps_from_file,mtmd_init,MtmdContext,bitmap_rgb,bitmap_audio— the multimodal C API
Build
First build upstream llama.cpp as shared libraries:
git clone https://github.com/ggml-org/llama.cpp.git
cmake -S llama.cpp -B llama.cpp/build \
-DBUILD_SHARED_LIBS=ON \
-DLLAMA_BUILD_EXAMPLES=OFF \
-DLLAMA_BUILD_TESTS=OFF \
-DLLAMA_BUILD_SERVER=OFF \
-DGGML_METAL=ON
cmake --build llama.cpp/build -j
LLAMA_BUILD_TOOLS must stay ON (its default) — libmtmd is built under
tools/mtmd, and this package links it.
Then point cpc at the directory holding libllama.dylib and libmtmd.dylib:
export LLAMA_CPP_LIB="$HOME/Workspace/llama.cpp/build/bin"
That directory is used as both -L and -rpath, which is how libllama finds
its own @rpath/libggml*.dylib siblings at run time. The ggml libraries are
therefore not listed in [link].libs.
Regenerating after a llama.cpp update
cargo build --release -p cpc-bindgen
cd vendor/llama_cpp
LLAMA_CPP_SRC=/path/to/llama.cpp ./build.sh
LLAMA_CPP_SRC defaults to $HOME/Workspace/llama.cpp. The output records the
upstream revision it was generated against in a header comment, and
regeneration is byte-stable: the same checkout produces the same file.
Then run the suite — an upstream API rename shows up as a compile error in the facade, and an upstream layout change shows up only in the e2e tests below.
Testing
cd vendor/llama_cpp
export LLAMA_CPP_LIB=/path/to/llama.cpp/build/bin
export LLAMA_CPP_TEST_MODEL=/path/to/some-model.gguf # any small GGUF
export LLAMA_CPP_TEST_MMPROJ=/path/to/mmproj-....gguf # optional, for mtmd
cpc test
The suite has two halves:
src/llama_cpp.cplus— unit and negative tests. Every one runs with null handles and never reaches allama_*symbol, so they prove the guard logic and nothing about the linked library.src/test_main.cplus— the e2e half: loads a real GGUF and drives real inference. This is the only half that can catch an ABI drift between this package and the llama.cpp it was generated against, because a wrong struct layout compiles, links, and then hands libllama garbage. (Verified: putting the old stalellama_model_paramsback kills the test binary with SIGSEGV.)
Without LLAMA_CPP_TEST_MODEL the e2e half prints a SKIPPED <test>: ... line
per test and passes. A green run with SKIPPED lines is not coverage of the
linked library — set the variable before believing it.
XCFramework note
Upstream releases publish an Apple XCFramework. C+ manifests support -L
library search paths but not -F framework search paths, so the simplest route
for C+ is still to extract or build normal libllama / libmtmd libraries and
point [link].search-paths at them. A future compiler/linker polish could add
framework search paths and make the XCFramework flow direct.