C/C++ ABI¶
DecentDB exposes a stable C ABI through include/decentdb.h. This is the lowest-level native integration surface and the shared boundary used by the higher-level language bindings.
C++ applications can include the same header directly. The header wraps the public declarations in extern "C" when compiled as C++, so the exported symbols keep C linkage. DecentDB does not currently ship a separate idiomatic C++ wrapper library.
Source Of Truth¶
| Item | Location |
|---|---|
| Public header | include/decentdb.h |
| Rust ABI implementation | crates/decentdb/src/c_api.rs |
| C smoke test | tests/bindings/c/smoke.c |
| C memory churn test | tests/bindings/c/memory_churn.c |
| Local compile script | tests/bindings/c/run.sh |
Build And Link¶
Build the shared library from source:
Then compile a C program against the header and shared library:
cc \
-I/path/to/decentdb/include \
app.c \
-L/path/to/decentdb/target/debug \
-Wl,-rpath,/path/to/decentdb/target/debug \
-ldecentdb \
-o app
The repository smoke test uses the same pattern:
When using a release artifact, point -I at the directory containing decentdb.h and point -L / your runtime library path at the extracted native library.
Status And Errors¶
Every fallible C ABI call returns ddb_status_t.
static void check(ddb_status_t status, const char *context) {
if (status != DDB_OK) {
const char *error = ddb_last_error_message();
fprintf(stderr, "%s failed with status %u: %s\n", context, status,
error == NULL ? "<null>" : error);
exit(1);
}
}
Common status codes:
| Code | Meaning |
|---|---|
DDB_OK | Success |
DDB_ERR_IO | I/O failure |
DDB_ERR_CORRUPTION | Corruption or invalid database state |
DDB_ERR_CONSTRAINT | Constraint violation |
DDB_ERR_TRANSACTION | Transaction error |
DDB_ERR_SQL | SQL parse, bind, or execution error |
DDB_ERR_INTERNAL | Internal engine error |
DDB_ERR_PANIC | Panic caught at the ABI boundary |
DDB_ERR_UNSUPPORTED_FORMAT_VERSION | Database file format is newer than this engine |
DDB_ERR_BUSY | Resource is busy |
DDB_ERR_TIMEOUT | Operation timed out before it could run or complete |
DDB_ERR_CANCELED | Operation was canceled before execution started |
DDB_ERR_QUEUE_FULL | Write queue capacity is exhausted |
DDB_ERR_QUEUE_CLOSED | Write queue is shutting down or closed |
ddb_last_error_message() returns a borrowed thread-local error string. Treat the pointer as valid only until the next DecentDB call on the same thread.
DDB_ABI_VERSION for this contract is currently 7. Callers should prefer ddb_last_error_json(char **out_json) for machine-readable details when available:
char *json = NULL;
ddb_status_t status = ddb_db_execute(db, "SELEC bad", NULL, 0, &result);
if (status != DDB_OK && ddb_last_error_json(&json) == DDB_OK) {
// parse json diagnostics
puts(json);
ddb_string_free(&json);
}
The JSON accessor does not replace ddb_last_error_message().
Ownership Rules¶
The C ABI uses opaque handles for databases, prepared statements, and query results:
| Owned value | Free function |
|---|---|
ddb_db_t * | ddb_db_free(&db) |
ddb_stmt_t * | ddb_stmt_free(&stmt) |
ddb_result_t * | ddb_result_free(&result) |
ddb_watch_t * | ddb_watch_close(&watch) |
owned strings returned as char * | ddb_string_free(&value) |
| owned copied cell values | ddb_value_dispose(&value) |
Rules:
- Free each successful owned handle exactly once.
- Pass the address of the pointer to free functions; they null the pointer.
- Do not free DecentDB-owned memory with
free(). ddb_value_view_tpointers are borrowed and must not be freed.ddb_value_ttext/blob payloads returned by copy functions are owned and must be released withddb_value_dispose.- Do not call free functions concurrently from multiple threads on the same pointer or handle.
Open Options And Local Security¶
The option-aware open functions accept a UTF-8 key=value string separated by whitespace, commas, or semicolons:
ddb_db_create_with_optionsddb_db_open_with_optionsddb_db_open_or_create_with_options
Named durable profiles are available through profile. Explicit options in the same string override the selected profile:
ddb_db_t *db = NULL;
check(ddb_db_open_or_create_with_options(
"app.ddb",
"profile=embedded_fast;cache_size=64MB",
&db),
"open tuned embedded db");
Available profiles are default, low_memory, balanced, embedded_fast, and tuned_durable. embedded_fast is the recommended opt-in starting point for single-process embedded applications with a hot working set and repeated small writes; it keeps durable WAL sync enabled.
Common open-option keys:
| Key | Values / notes |
|---|---|
profile / performance_profile | default, low_memory, balanced, embedded_fast, tuned_durable |
cache_size / cache_size_mb | integer page count, <n>MB, <n>M, <n>GB, or <n>G |
retain_paged_row_sources_after_commit | boolean |
paged_row_storage | boolean |
persistent_pk_index | boolean |
wal_autocheckpoint | page threshold; 0 disables page and byte auto-checkpoint triggers |
wal_checkpoint_threshold_pages | page-version threshold |
wal_checkpoint_threshold_bytes | byte threshold |
wal_sync_mode / synchronous | full, normal, or async_commit:<milliseconds> |
process_coordination | auto, required, or single_process_unsafe |
process_coordination_timeout_ms | unsigned integer milliseconds |
write_queue_enabled | boolean advisory for high-level bindings |
write_queue_capacity | queued-write capacity |
write_queue_default_timeout_ms | 0 means no configured default timeout |
write_queue_strict_group_commit / write_queue_group_commit | boolean |
write_queue_max_batch | maximum ready requests per executor pass |
write_queue_max_group_delay_us | optional group-commit collection delay |
plan_cache_enabled | boolean |
plan_cache_max_bytes | connection-local plan cache budget |
encryption_key_hex / tde_key_hex | hex key bytes |
encryption_key / tde_key | UTF-8 key bytes |
allow_extension | name@sha256:<hash> or name@sha256:<hash>@<key_id>@<public_key> |
allow_unsigned_extensions | development-only boolean |
TDE can be enabled with encryption_key_hex or encryption_key:
ddb_db_t *db = NULL;
check(ddb_db_create_with_options(
"secure.ddb",
"encryption_key_hex=00112233445566778899aabbccddeeff",
&db),
"create encrypted db");
encryption_key_hex / tde_key_hex decode hex bytes. encryption_key / tde_key use the UTF-8 bytes of the option value. Avoid logging option strings that contain key material.
Cross-process WAL coordination can be required explicitly:
ddb_db_t *db = NULL;
check(ddb_db_open_or_create_with_options(
"app.ddb",
"process_coordination=required;process_coordination_timeout_ms=30000",
&db),
"open coordinated db");
Supported process_coordination values are auto (default), required, and single_process_unsafe. Coordinated opens create or reuse a rebuildable app.ddb.coord sidecar on local filesystems with byte-range lock support. Use SELECT * FROM sys.process_coordination, sys.process_readers, and sys.process_lock_metrics for diagnostics.
Audit context can be set through SQL or through the C ABI:
const char *actor = "alice@example.com";
check(ddb_db_set_audit_context_text(
db,
"actor",
actor,
strlen(actor)),
"set actor");
check(ddb_db_clear_audit_context(db, "actor"), "clear actor");
The SQL layer also supports SET AUDIT CONTEXT, CREATE POLICY, and CREATE MASK. See Local Data Security.
Queued Writes¶
ddb_db_execute_queued submits one SQL statement to the engine-owned write queue. It returns the same result handle shape as ddb_db_execute.
ddb_result_t *result = NULL;
check(ddb_db_execute_queued(
db,
"INSERT INTO events (id, name) VALUES (1, 'queued')",
NULL,
0,
DDB_WRITE_QUEUE_TIMEOUT_DEFAULT,
&result),
"queued insert");
check(ddb_result_free(&result), "free queued result");
Pass DDB_WRITE_QUEUE_TIMEOUT_DEFAULT to use the database configured default timeout. Pass 0 for immediate timeout behavior.
Queue behavior and strict group commit are documented in Write Concurrency. Metrics are available through ddb_db_write_queue_metrics:
ddb_write_queue_metrics_t metrics;
check(ddb_db_write_queue_metrics(db, &metrics), "queue metrics");
printf("admitted=%llu committed=%llu syncs=%llu\n",
(unsigned long long)metrics.admitted,
(unsigned long long)metrics.committed,
(unsigned long long)metrics.group_commit_syncs);
Reactive Watch Handles¶
The C ABI exposes reactive subscriptions as opaque ddb_watch_t handles with JSON requests and JSON event polling. Watches are in-process only and observe committed state after the initial event.
ddb_watch_t *watch = NULL;
check(ddb_db_watch_query_json(
db,
"{\"sql\":\"SELECT name FROM users ORDER BY id\"}",
&watch),
"watch query");
char *event_json = NULL;
check(ddb_watch_next_json(watch, 1000, &event_json), "initial event");
puts(event_json);
check(ddb_string_free(&event_json), "free initial event");
/* Run writes through any handle in the same process. */
check(ddb_watch_next_json(watch, 1000, &event_json), "invalidation event");
puts(event_json);
check(ddb_string_free(&event_json), "free invalidation event");
check(ddb_watch_close(&watch), "close watch");
Available creation functions:
ddb_db_watch_table_jsonddb_db_watch_range_jsonddb_db_watch_query_jsonddb_db_change_stream_json
ddb_watch_next_json returns DDB_ERR_TIMEOUT when no event is available before the requested timeout. Returned event strings are freed with ddb_string_free.
Lua Extension JSON Bridge¶
Lua extension package lifecycle APIs are exposed as JSON bridges. Each successful call that returns JSON transfers ownership of a char * that must be freed with ddb_string_free.
Available functions:
ddb_extension_validate_jsonddb_extension_install_jsonddb_extension_enable_jsonddb_extension_disable_jsonddb_extension_list_jsonddb_extension_dependencies_jsonddb_extension_rebuild_jsonddb_extension_purge_json
Validate a local package:
char *json = NULL;
check(ddb_extension_validate_json(
"{\"path\":\"./text_tools\",\"allow_unsigned\":true}",
&json),
"validate extension");
puts(json);
check(ddb_string_free(&json), "free validation json");
Install and enable an extension:
check(ddb_extension_install_json(
db,
"{\"path\":\"./text_tools\",\"allow_unsigned\":true}",
&json),
"install extension");
check(ddb_string_free(&json), "free install json");
check(ddb_extension_enable_json(db, "{\"name\":\"text_tools\"}", &json),
"enable extension");
check(ddb_string_free(&json), "free enable json");
Open-time extension trust is configured through the open-with-options entry points:
ddb_db_t *db = NULL;
check(ddb_db_open_or_create_with_options(
"app.ddb",
"allow_extension=text_tools@sha256:7b3f...",
&db),
"open with extension trust");
Use allow_unsigned_extensions=true only for local development databases. For production, pass exact allow_extension=name@sha256:<hash> entries. A trust entry may also include a key id and public key: name@sha256:<hash>@<key_id>@base64:<public_key>.
See Lua Extensions for the manifest, sandbox, signature, and SQL invocation contract.
Minimal C Example¶
This example mirrors the repository smoke test.
#include "decentdb.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static void check(ddb_status_t status, const char *context) {
if (status != DDB_OK) {
const char *error = ddb_last_error_message();
fprintf(stderr, "%s failed with status %u: %s\n", context, status,
error == NULL ? "<null>" : error);
exit(1);
}
}
int main(void) {
ddb_db_t *db = NULL;
ddb_result_t *result = NULL;
size_t rows = 0;
check(ddb_db_open_or_create(":memory:", &db), "open_or_create");
check(ddb_db_execute(db,
"CREATE TABLE smoke (id INT64 PRIMARY KEY, name TEXT)",
NULL, 0, &result),
"create");
check(ddb_result_free(&result), "free create");
check(ddb_db_execute(db,
"INSERT INTO smoke (id, name) VALUES (1, 'c-smoke')",
NULL, 0, &result),
"insert");
check(ddb_result_free(&result), "free insert");
check(ddb_db_execute(db, "SELECT id, name FROM smoke", NULL, 0, &result),
"select");
check(ddb_result_row_count(result, &rows), "row_count");
if (rows != 1) {
fprintf(stderr, "expected 1 row, got %zu\n", rows);
return 1;
}
check(ddb_result_free(&result), "free select");
check(ddb_db_free(&db), "free db");
return 0;
}
Reading Result Values¶
ddb_db_execute returns a materialized ddb_result_t. Use ddb_result_row_count, ddb_result_column_count, and ddb_result_value_copy to inspect it.
ddb_value_t value;
check(ddb_value_init(&value), "init value");
check(ddb_result_value_copy(result, 0, 1, &value), "copy value");
if (value.tag == DDB_VALUE_TEXT) {
printf("name=%.*s\n", (int)value.len, (const char *)value.data);
}
check(ddb_value_dispose(&value), "dispose value");
Text, blob, geometry, and geography values are byte buffers. They are not guaranteed to be NUL-terminated; always use the returned length. Spatial values are returned as normalized EWKB with DDB_VALUE_GEOMETRY or DDB_VALUE_GEOGRAPHY tags.
Semantic values have dedicated ABI tags in ddb_value_t and ddb_value_view_t:
| Tag | Payload fields |
|---|---|
DDB_VALUE_ENUM | enum_type_id, enum_label_id |
DDB_VALUE_IPADDR | ip_family, ip_cidr_addr_bytes |
DDB_VALUE_CIDR | ip_family, cidr_prefix_len, ip_cidr_addr_bytes |
DDB_VALUE_DATE | date_days |
DDB_VALUE_TIME | time_micros |
DDB_VALUE_TIMESTAMPTZ_MICROS | timestamptz_micros |
DDB_VALUE_INTERVAL | interval_months, interval_days, interval_micros |
DDB_VALUE_MACADDR | ip_family as length (6 or 8), ip_cidr_addr_bytes |
For inserts, bind text or integer values in a statement where the destination column type is known; the engine performs the semantic cast during execution.
For read-heavy streaming paths, prefer the statement row-view APIs described below to avoid per-cell heap allocation.
Prepared Statements¶
Use ddb_db_prepare for repeated statements and bind parameters with one-based indexes:
ddb_stmt_t *stmt = NULL;
check(ddb_db_prepare(db,
"INSERT INTO users (id, name) VALUES ($1, $2)",
&stmt),
"prepare insert");
check(ddb_stmt_bind_int64(stmt, 1, 1), "bind id");
check(ddb_stmt_bind_text(stmt, 2, "Ada", 3), "bind name");
uint8_t has_row = 0;
check(ddb_stmt_step(stmt, &has_row), "step insert");
check(ddb_stmt_free(&stmt), "free stmt");
Available typed bind helpers include:
ddb_stmt_bind_nullddb_stmt_bind_int64ddb_stmt_bind_float64ddb_stmt_bind_boolddb_stmt_bind_textddb_stmt_bind_blobddb_stmt_bind_geometry_wkbddb_stmt_bind_geography_wkbddb_stmt_bind_uuidddb_stmt_bind_decimalddb_stmt_bind_timestamp_micros
The spatial bind helpers accept WKB/EWKB byte buffers. GEOGRAPHY bindings are normalized to SRID 4326 on insert.
Use ddb_stmt_reset to clear a statement's result cursor and ddb_stmt_clear_bindings to remove existing parameter values.
Streaming Row Views¶
For read-heavy paths, the ABI exposes borrowed row views:
ddb_stmt_t *stmt = NULL;
check(ddb_db_prepare(db,
"SELECT id, name FROM users WHERE id >= $1 ORDER BY id",
&stmt),
"prepare select");
check(ddb_stmt_bind_int64(stmt, 1, 1), "bind min id");
for (;;) {
const ddb_value_view_t *values = NULL;
size_t columns = 0;
uint8_t has_row = 0;
check(ddb_stmt_step_row_view(stmt, &values, &columns, &has_row),
"step row view");
if (!has_row) {
break;
}
if (columns >= 2 && values[1].tag == DDB_VALUE_TEXT) {
printf("name=%.*s\n", (int)values[1].len,
(const char *)values[1].data);
}
}
check(ddb_stmt_free(&stmt), "free stmt");
Borrowed row-view pointers are valid until the next DecentDB call that mutates or advances the same statement.
The ABI also includes specialized fast paths for common benchmark and binding shapes:
ddb_stmt_bind_int64_step_row_viewddb_stmt_bind_int64_step_i64_text_f64ddb_stmt_fetch_row_viewsddb_stmt_fetch_rows_i64_text_f64
Transactions¶
Explicit transactions are available through database-handle functions:
uint64_t lsn = 0;
check(ddb_db_begin_transaction(db), "begin");
check(ddb_db_execute(db,
"INSERT INTO users (id, name) VALUES (2, 'Grace')",
NULL, 0, &result),
"insert");
check(ddb_result_free(&result), "free insert");
check(ddb_db_commit_transaction(db, &lsn), "commit");
Use ddb_db_rollback_transaction to discard an active transaction and ddb_db_in_transaction to inspect transaction state.
Metadata And Maintenance¶
The C ABI exposes JSON-returning helpers for schema and storage metadata:
ddb_db_list_tables_jsonddb_db_describe_table_jsonddb_db_get_table_ddlddb_db_list_indexes_jsonddb_db_list_views_jsonddb_db_get_view_ddlddb_db_list_triggers_jsonddb_db_get_schema_snapshot_jsonddb_db_get_tooling_metadata_jsonddb_db_describe_query_jsonddb_db_inspect_storage_state_json
Each successful string-returning call transfers ownership of a char * to the caller:
char *json = NULL;
check(ddb_db_list_tables_json(db, &json), "list tables");
puts(json);
check(ddb_string_free(&json), "free json");
ddb_db_get_tooling_metadata_json returns the stable schema/tooling contract: engine version, format version, schema cookies, deterministic schema fingerprint, rich schema snapshot, native type metadata, and capability flags.
ddb_db_describe_query_json parses and analyzes SQL without executing it:
char *contract = NULL;
check(ddb_db_describe_query_json(db,
"SELECT id, email FROM users WHERE id = $1",
&contract),
"describe query");
puts(contract);
check(ddb_string_free(&contract), "free contract");
Maintenance helpers:
ddb_db_checkpointddb_db_save_asddb_evict_shared_wal
ddb_db_checkpoint folds committed WAL frames into the database file and can truncate the WAL when no active readers require retained versions. Under the default full WAL sync mode, commit acknowledgement is already the durability barrier; checkpointing is for WAL size, recovery time, and snapshot/export workflows.
Local-First Sync JSON Bridge¶
The C ABI exposes sync operations through a compact JSON bridge:
char *response = NULL;
check(ddb_db_sync_execute_json(db,
"{\"op\":\"status\"}",
&response),
"sync status");
puts(response);
check(ddb_string_free(&response), "free sync response");
The higher-level sync command set is documented in Local-first sync and CLI Reference.
Production changesets also have dedicated C ABI JSON entry points:
char *changeset = NULL;
check(ddb_sync_changeset_create_json(
db,
"{\"source\":{\"kind\":\"checkpoint\",\"peer\":\"relay\",\"since_sequence\":0}}",
&changeset),
"create changeset");
puts(changeset);
check(ddb_string_free(&changeset), "free changeset");
Available functions:
ddb_sync_changeset_create_jsonddb_sync_changeset_apply_jsonddb_sync_changeset_inspect_jsonddb_sync_changeset_invert_json
Each function returns an owned JSON string that must be freed with ddb_string_free.
Branch Workflow JSON Bridge¶
The C ABI also exposes snapshot, branch, diff, restore, and merge workflows through a JSON bridge:
char *response = NULL;
check(ddb_db_branch_execute_json(
db,
"{\"op\":\"branch_create\",\"name\":\"work\",\"from\":\"main\"}",
&response),
"create branch");
puts(response);
check(ddb_string_free(&response), "free branch response");
Supported op values are:
snapshot_create,snapshot_list,snapshot_deletebranch_create,branch_list,branch_delete,branch_renamebranch_commit,branch_log,branch_diffbranch_restorebranch_merge
See Branching, Diff, Restore, And Time Travel and CLI Reference for command semantics and safety rules.
For typed branch-local SQL execution, use ddb_db_execute_on_branch. It returns the same owned ddb_result_t shape as ddb_db_execute, so callers read columns, rows, values, and affected-row counts through the normal result accessors and release the handle with ddb_result_free:
ddb_result_t *result = NULL;
ddb_value_t params[1] = {0};
params[0].tag = DDB_VALUE_INT64;
params[0].int64_value = 42;
check(ddb_db_execute_on_branch(
db,
"work",
"SELECT id, name FROM items WHERE id = $1",
params,
1,
&result),
"query branch");
check(ddb_result_free(&result), "free branch result");
C++ Usage¶
C++ code can include decentdb.h directly:
#include "decentdb.h"
#include <stdexcept>
#include <string>
class DbHandle {
public:
explicit DbHandle(const char *path) {
ddb_status_t status = ddb_db_open_or_create(path, &db_);
if (status != DDB_OK) {
const char *msg = ddb_last_error_message();
throw std::runtime_error(msg == nullptr ? "DecentDB open failed" : msg);
}
}
DbHandle(const DbHandle &) = delete;
DbHandle &operator=(const DbHandle &) = delete;
~DbHandle() {
if (db_ != nullptr) {
ddb_db_free(&db_);
}
}
ddb_db_t *get() const { return db_; }
private:
ddb_db_t *db_ = nullptr;
};
This pattern is a convenience wrapper around the C ABI. The stable public contract remains include/decentdb.h.
Validation¶
Run the C binding smoke test from the repository root:
The release workflow also runs this smoke path. The nightly memory-safety workflow builds and runs the C smoke and memory churn programs under Valgrind where available.
Current Limits¶
- There is no separate C++ package or object-oriented C++ API.
- The C ABI is intentionally lower level than the .NET, Go, Python, Node, Dart, and JDBC bindings.
- Dot commands from
decentdb replare CLI behavior, not C ABI behavior.