Skip to content

0.8 C ABI design

Status: accepted design for milestone 0.8
Decisions: ADR-002, ADR-023
Companion: storage-backend-expansion.md, milestone 0.8

Goal

Ship oxiland-capi as an auditable C source-compat preview: Redland-shaped headers and symbols sufficient for one end-to-end program, with explicit unsupported behavior for the rest of librdf. Full ABI and downstream compatibility remain 0.9.

Crate boundary

  • Workspace crate crates/oxiland-capi produces cdylib and staticlib.
  • Depends on the safe oxiland facade only; never the reverse (ADR-002).
  • Library soname / pkg-config name: oxiland (headers under librdf.h for source-shaped include compatibility).
  • Symbol versioning: GNU ld version script on ELF; documented export allowlist on all platforms. Preview exports are exactly the milestone allowlist.

Opaque handles

Every exported pointer is an opaque heap object with:

  1. a type tag checked on entry;
  2. null rejected before dereference;
  3. destruction that frees owned Rust state once (second free is a no-op that records an error, matching Redland's practical double-free defenses where documented);
  4. no Rust references exposed across the boundary.
Handle Owns Thread-safety
librdf_world World Send+Sync after world_open; serialize feature mutations
librdf_storage backend id + path options (not open engine alone) not thread-safe across open/free
librdf_model Model same as Rust Model (Send+Sync for concurrent readers; writers serialize)
librdf_uri owned IRI string / NamedNode immutable after create; Send+Sync
librdf_node owned term immutable after create; Send+Sync
librdf_statement owned triple parts not shared across threads while mutating
librdf_stream statement iterator state bound to model not Send; invalid after model free
librdf_parser / librdf_serializer syntax config not thread-safe
librdf_query query string + language not thread-safe
librdf_query_results result cursor not Send; invalid after model free

Allocator contract

  • All strings and buffers returned to C are allocated with the crate allocator (malloc via Rust global allocator / libc::malloc pairing).
  • Callers free them only with librdf_free_memory.
  • Handles are freed only with their typed librdf_free_* functions.
  • Mixing system free with Oxiland allocations is undefined and documented as unsupported.

Panic and error translation

  • Every extern "C" entry wraps the body in catch_unwind.
  • Panics never unwind into C; they set last-error and return NULL / nonzero.
  • Last-error is thread-local: message + category string for diagnostics.
  • Preview does not implement Redland's full log-handler callback surface; world logging stays on the Rust/Python side unless a later ADR adds it.

Storage through C

librdf_new_storage(world, storage_name, name, options) accepts:

  • "memory" — in-memory model (ignore path);
  • "fjall" — durable format-v1 store; name is the filesystem path.

Unknown names fail. Known-but-not-compiled optional backends (e.g. redb) fail with an explicit unsupported message distinct from unknown. Opening uses the same registry as Rust StorageBackend / CLI -s / Python.

Header generation

include/librdf.h declares the preview allowlist with Redland-compatible names and opaque typedefs. Unsupported Redland APIs are omitted from the header in 0.8 (callers that need them must wait for 0.9) rather than stubbed as always-failing prototypes that imply support.

Verification

See milestone evidence gates: representative .c example, ASan/LSan, export allowlist, alloc/destroy tests, null/UTF-8/double-free defenses, and a safety comment on every unsafe block.