C
hyperlark exposes a small, carefully-documented C ABI (a cdylib/staticlib).
The v1 surface is deliberately minimal: LALR parsing, an opaque tree cursor,
the interactive parser, two fold APIs (post-parse and during-parse), and a
structured error channel. Earley, postlex, and custom lexers are not on the C
surface (see the feature matrix).
The hand-written header hyperlark.h is the ABI source of truth, with a
“THE THREE CONTRACTS” preamble (lifetime / panics / threading). It is
rendered here verbatim from the shipped file.
cargo build -p hyperlark-c # debug -> target/debug/cargo build -p hyperlark-c --release # release -> target/release/Each build produces both libraries:
| artifact | file |
|---|---|
| static lib | libhyperlark_c.a |
| shared lib | libhyperlark_c.so (.dylib on macOS; on Windows hyperlark_c.dll — no lib prefix — plus the import library hyperlark_c.dll.lib (MSVC) / libhyperlark_c.dll.a (MinGW), which is what you link against) |
A Rust static lib pulls in the symbols the Rust std runtime needs, so you must link the system libraries it depends on:
| platform | required link flags |
|---|---|
| Linux | -lpthread -ldl -lm |
| macOS | -framework CoreFoundation -framework Security (usually automatic) |
Against the static lib, from the repo root:
cc -std=c11 -I crates/hyperlark-c/include \ my_app.c target/debug/libhyperlark_c.a -lpthread -ldl -lm -o my_appOr against the shared lib:
cc -std=c11 -I crates/hyperlark-c/include my_app.c \ -L target/debug -lhyperlark_c -o my_appLD_LIBRARY_PATH=target/debug ./my_appInstall (pkg-config / CMake)
Section titled “Install (pkg-config / CMake)”For real integration you don’t want to hardcode build-tree paths or memorize the
-lpthread -ldl -lm list — install once and let pkg-config or CMake carry it:
make -C bindings/c install PREFIX=/usr/local # header + both libs + .pc + CMake config# stage into a packaging root instead:make -C bindings/c install PREFIX=/usr/local DESTDIR=/tmp/stagemake install builds with cargo build --release, then installs the header, the
static + shared libs, a hyperlark.pc, and a CMake package config. The private
system libs (-lpthread -ldl -lm on Linux, the CoreFoundation/Security frameworks
on macOS) are baked into the .pc’s Libs.private and the CMake target, so a
static link just works.
A custom LIBDIR/INCLUDEDIR (e.g. lib64 on Fedora, or lib/<triplet> on
Debian multiarch) is honored: both the .pc and the CMake config record the
actual configured directories, not ${prefix}/lib.
pkg-config
Section titled “pkg-config”Shared link:
cc -std=c11 $(pkg-config --cflags hyperlark) my_app.c \ $(pkg-config --libs hyperlark) -o my_appTo embed the static archive, force static resolution of this lib: the static
and shared libs share a directory, so a bare -lhyperlark_c resolves to the .so,
and pkg-config --static only adds the private system libs — it does not pass
-static. Either toggle static for this lib (GNU ld) or link the archive by path
(portable).
The private deps are --libs-only-l (-lpthread -ldl -lm on Linux) and
--libs-only-other (the -framework CoreFoundation -framework Security on macOS) —
include both, or a macOS static link fails to resolve the frameworks.
# GNU ld: static hyperlark, dynamic everything elsecc -std=c11 $(pkg-config --cflags hyperlark) my_app.c \ -Wl,-Bstatic $(pkg-config --libs-only-L hyperlark) -lhyperlark_c -Wl,-Bdynamic \ $(pkg-config --libs-only-l --static hyperlark | sed 's/-lhyperlark_c//') \ $(pkg-config --libs-only-other --static hyperlark) -o my_app
# portable: name the .a directlycc -std=c11 $(pkg-config --cflags hyperlark) my_app.c \ $(pkg-config --variable=libdir hyperlark)/libhyperlark_c.a \ $(pkg-config --libs-only-l --static hyperlark | sed 's/-lhyperlark_c//') \ $(pkg-config --libs-only-other --static hyperlark) -o my_appThree targets are exported (whichever artifacts are installed):
find_package(Hyperlark 0.1 REQUIRED)target_link_libraries(my_app PRIVATE hyperlark::hyperlark) # default: shared# or pick a link mode explicitly:target_link_libraries(my_app PRIVATE hyperlark::shared) # the .so/.dylibtarget_link_libraries(my_app PRIVATE hyperlark::static) # the .a (no extra flags)hyperlark::static carries the private system libs, so a static link “just works”
through the target — no -Wl,-Bstatic dance. The imported targets carry the
include dir. Point CMake at the install with -DCMAKE_PREFIX_PATH=<prefix>.
You can also consume a cargo build tree without installing, via
-DHyperlark_DIR=bindings/c/cmake -DHYPERLARK_ROOT=<repo> — but that path ships
no generated version file, so call find_package(Hyperlark REQUIRED) there
without a version. The installed tree supports the versioned form above.
Construct → parse → walk → free
Section titled “Construct → parse → walk → free”#include <string.h> /* strlen */#include "hyperlark.h"
Lark *lark = NULL;const char *g = "start: \"hello\" NAME\nNAME: /\\w+/\n%ignore \" \"\n";if (lark_from_source(g, strlen(g), LARK_OPT_NONE, &lark) != LARK_OK) { /* lark_last_error() */ }
LarkParseResult *res = NULL;if (lark_parse(lark, "hello world", 11, NULL, &res) == LARK_OK) { LarkNode root; lark_result_root(res, &root); /* walk with lark_node_child / lark_cursor_* ... */ lark_result_free(res);}lark_free(lark);Construction options
Section titled “Construction options”Options are a uint64_t flag word — no struct to fill. Pass LARK_OPT_NONE
(0) for the defaults, which match Lark; the bit polarity is chosen so 0 is
never wrong. OR the flags you want:
lark_from_source(grammar, len, LARK_OPT_KEEP_ALL_TOKENS, &lark);lark_from_source(grammar, len, LARK_OPT_NO_PLACEHOLDERS | LARK_OPT_PROPAGATE_POSITIONS, &lark);LARK_OPT_NO_PLACEHOLDERS— disablemaybe_placeholders(default: ON, so this is an opt-out)LARK_OPT_KEEP_ALL_TOKENS— retain filtered/punctuation terminals (default: off)LARK_OPT_PROPAGATE_POSITIONS— attach position meta to rule nodes (default: off)
A bit outside LARK_OPT_ALL is LARK_INVALID_ARGUMENT — a newer option requested
of an older library is rejected, never silently dropped.
Errors
Section titled “Errors”Every fallible call returns a LarkStatus (LARK_OK == 0). On a non-OK status,
read the thread-local last-error buffer (errno-style; valid until the next failing
call on the same thread):
lark_last_error()— the message string;lark_last_error_position(&line, &col)— the 1-based location (returnsfalsewhen the error carries none);lark_last_error_context()— Lark’sget_contextcaret view;lark_last_error_expected_count()/lark_last_error_expected(i)— the accepted-terminal set for an unexpected-token/character error;lark_status_name(status)— a stable name for logging.
What the status codes do and do not promise. They promise panic containment:
no Rust panic becomes an unwind across the boundary. They do not promise the
process survives everything, because two failures are not panics and
catch_unwind cannot intercept either — both abort.
- Out of memory, generally. Rust aborts on allocation failure, and most of this
API allocates, so there is no OOM-total subset of it.
lark_result_prettyand grammar compilation amplify the risk far beyond input size. - Stack overflow, which unlike OOM is localized: grammar compilation recurses
and is not hardened against hostile grammars — long terminal-reference chains,
very wide rule bodies, deeply nested regex literals and exponential
~Nrepeats can all abort it. Treat a grammar as trusted input; see thelark_from_source/lark_from_jsonnotes in the header. Parsing text with an already-builtLark*is iterative, so it does not overflow on deep input.
Introspection
Section titled “Introspection”Rule and terminal names map to and from stable integer ids: lark_rule_id /
lark_rule_name and lark_token_id / lark_token_name (with lark_rule_count /
lark_token_count). These are the id space a reduce-time fold callback receives,
so a lark_parse_fold callback can identify a bare rule_id / token_id.
Versioning
Section titled “Versioning”Two independent identities (see the header’s “version + ABI” block):
- Release version —
lark_version()/HYPERLARK_VERSION_STRING/HYPERLARK_ABI_VERSION(andlark_abi_version()), stamped from the crate. For logging, bug reports, and gating across releases. It does not change on a same-version ABI change, so it cannot detect a header and library built from different commits that share a version. - C ABI revision —
HYPERLARK_C_ABI/lark_c_abi_revision(), a small integer bumped on every breaking ABI change, independent of the release version. Assertlark_c_abi_revision() == HYPERLARK_C_ABIat startup to catch a header/library mismatch. (A library predating this symbol fails to link.)
The shared library ships as libhyperlark_c.{a,so} with no version suffix in the
beta; during pre-release the ABI is unstable — build the header and library from
the same checkout.
bindings/c/tests/run_tests.sh builds the static lib and compiles + runs the C
test programs (smoke.c, interactive.c, cursors.c, interface.c, edge.c,
fold_reduce.c, threads.c, and c_diff.c, which replays fixtures). The runner
is also a working reference for the exact build + link invocation.
Next steps
Section titled “Next steps”- Grammar reference — the
.larkgrammar language. - Feature matrix — what the C surface supports.
- Alternatives — hyperlark vs bison/flex, lemon, tree-sitter.
hyperlark.h— the annotated ABI contract itself.