Skip to content

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.

Terminal window
cargo build -p hyperlark-c # debug -> target/debug/
cargo build -p hyperlark-c --release # release -> target/release/

Each build produces both libraries:

artifactfile
static liblibhyperlark_c.a
shared liblibhyperlark_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:

platformrequired link flags
Linux-lpthread -ldl -lm
macOS-framework CoreFoundation -framework Security (usually automatic)

Against the static lib, from the repo root:

Terminal window
cc -std=c11 -I crates/hyperlark-c/include \
my_app.c target/debug/libhyperlark_c.a -lpthread -ldl -lm -o my_app

Or against the shared lib:

Terminal window
cc -std=c11 -I crates/hyperlark-c/include my_app.c \
-L target/debug -lhyperlark_c -o my_app
LD_LIBRARY_PATH=target/debug ./my_app

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:

Terminal window
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/stage

make 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.

Shared link:

Terminal window
cc -std=c11 $(pkg-config --cflags hyperlark) my_app.c \
$(pkg-config --libs hyperlark) -o my_app

To 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.

Terminal window
# GNU ld: static hyperlark, dynamic everything else
cc -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 directly
cc -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_app

Three 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/.dylib
target_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.

#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);

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 — disable maybe_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.

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 (returns false when the error carries none);
  • lark_last_error_context() — Lark’s get_context caret 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_pretty and 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 ~N repeats can all abort it. Treat a grammar as trusted input; see the lark_from_source / lark_from_json notes in the header. Parsing text with an already-built Lark* is iterative, so it does not overflow on deep input.

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.

Two independent identities (see the header’s “version + ABI” block):

  • Release versionlark_version() / HYPERLARK_VERSION_STRING / HYPERLARK_ABI_VERSION (and lark_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 revisionHYPERLARK_C_ABI / lark_c_abi_revision(), a small integer bumped on every breaking ABI change, independent of the release version. Assert lark_c_abi_revision() == HYPERLARK_C_ABI at 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.