Getting Started: C
libpurrdf is a stable, SemVer-disciplined extern "C" surface over the
native PurRDF stack: parse, serialize, pattern iteration, copy-on-write
mutation, SPARQL, SHACL validation/entailment, and GTS container round-trips.
The committed, reproducible header
include/purrdf.h
is the ABI contract — CI fails if it drifts from the crate.
It is one shared library: libpurrdf statically reuses the purrdf-gts Rust
crate, so a language shim links libpurrdf alone and still reads/writes
.gts containers.
Building
The library, header, and pkg-config file are produced by
cargo-c:
make capi-build # cargo capi build: libpurrdf.{so,a} + purrdf.h + purrdf.pc
make capi-install PREFIX=/usr # cargo capi install into a prefix
make capi-check # verify the committed header is current + run the C smoke
A first program
Adapted from the repository’s C smoke test
(crates/rdf-capi/tests/smoke.c):
#include "purrdf.h"
#include <stdio.h>
#include <string.h>
int main(void) {
const char *doc = "<http://a> <http://b> <http://c> .";
PurrdfDataset *dataset = NULL;
PurrdfError *error = NULL;
int rc = purrdf_parse((const uint8_t *)doc, strlen(doc), "text/turtle",
NULL, NULL, &dataset, &error);
if (rc != PURRDF_STATUS_OK) return 1;
size_t quad_count = 0;
purrdf_dataset_quad_count(dataset, &quad_count);
printf("%zu quad(s)\n", quad_count);
/* Iterate every quad through a pattern cursor. */
PurrdfGraphMatch any;
memset(&any, 0, sizeof(any));
any.kind = PURRDF_GRAPH_MATCH_KIND_ANY;
PurrdfCursor *cursor = NULL;
purrdf_quads_for_pattern(dataset, NULL, NULL, NULL, &any, &cursor, &error);
PurrdfTermView s, p, o, g;
uint8_t has_graph = 0;
while (purrdf_cursor_next(cursor, &s, &p, &o, &g, &has_graph) == PURRDF_STATUS_OK) {
printf("subject=%.*s\n", (int)s.lexical.len, (const char *)s.lexical.ptr);
}
purrdf_cursor_free(cursor);
purrdf_dataset_free(dataset);
return 0;
}
purrdf_project and purrdf_lift add deterministic graph/tabular/research-object carrier
archives to this same handle model. Project returns independent archive and
loss-ledger buffers; lift returns a dataset plus a ledger buffer. The compiled
purrdf_project_with_assets entry point accepts a bounded payload-only USTAR and
emits an attached RO-Crate with deterministic metadata and preview. The compiled
crates/rdf-capi/examples/projection_roundtrip.c example demonstrates the full
ownership/free order. Profiles and configuration are described in
Graph, Tabular & Research-Object Projections.
The ABI contract
- No unwinding across the boundary. Every function runs inside
catch_unwind; a caught panic becomesPURRDF_STATUS_PANIC, never a process abort across FFI. int32_tstatus + out-params. Fallible functions return aPurrdfStatusvalue and write results through out-pointers.PURRDF_STATUS_CURSOR_EXHAUSTEDis the (non-error) end-of-rows signal.- SemVer-frozen ABI. The status enum is append-only; new fields and
functions are additive.
purrdf_abi_versionreports the current ABI version at runtime; thePURRDF_ABI_MAJOR/_MINOR/_PATCHmacros give the same triple for the header you compiled against, and comparing the two is how you check a library you did not build. The minor number tracks the exported signatures: any change to a parameter list, a return contract, or the documented behaviour of an exported symbol bumps it, additive parameters included, because an additive parameter is still a recompile for every C consumer.
Ownership and lifetimes
- Every handle/buffer/error/cursor has exactly one matching
*_free(purrdf_dataset_free,purrdf_graph_free,purrdf_cursor_free,purrdf_rowcursor_free,purrdf_buffer_free,purrdf_error_free). FreeingNULLis a no-op. - The C side never
free()s aPurrdfStr.ptr— it borrows library-owned memory; copy the bytes out if they must outlive the borrow. Term views frompurrdf_cursor_nextare valid until the nextpurrdf_cursor_nexton that cursor orpurrdf_cursor_free. - Pattern cursors pin the dataset and pull rows lazily from the selected core index; opening a cursor does not allocate a matching-row snapshot.
PurrdfDatasetis frozen andSend + Sync— readable concurrently from many threads.PurrdfGraph(the copy-on-write mutable delta) and cursors are single-threaded.
GTS star-layer round-trip
The GTS star layer round-trip (purrdf_to_gts → purrdf_from_gts of a
dataset containing quoted triples / reifier bindings) succeeds with
PURRDF_STATUS_OK, the same as a star-free round-trip. The C ABI calls the
canonical kernel path (to_gts → read_graph → import_gts_graph); a
characterization test,
gts_star_roundtrip_preserves_the_statement_layer in
crates/rdf-capi/tests/abi.rs, pins the restored dataset’s quoted triple and
reifier binding so a regression in either layer fails there.
Full contract details — status codes, term crossing representations,
thread-safety per handle — are in the
purrdf-capi README.