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-capiproducescdylibandstaticlib. - Depends on the safe
oxilandfacade only; never the reverse (ADR-002). - Library soname / pkg-config name:
oxiland(headers underlibrdf.hfor 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:
- a type tag checked on entry;
- null rejected before dereference;
- 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);
- 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
(
mallocvia Rust global allocator /libc::mallocpairing). - Callers free them only with
librdf_free_memory. - Handles are freed only with their typed
librdf_free_*functions. - Mixing system
freewith Oxiland allocations is undefined and documented as unsupported.
Panic and error translation¶
- Every
extern "C"entry wraps the body incatch_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;nameis 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.