Skip to content

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 the shared library from source:

cargo build -p decentdb

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:

bash tests/bindings/c/run.sh

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_t pointers are borrowed and must not be freed.
  • ddb_value_t text/blob payloads returned by copy functions are owned and must be released with ddb_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_options
  • ddb_db_open_with_options
  • ddb_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_json
  • ddb_db_watch_range_json
  • ddb_db_watch_query_json
  • ddb_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_json
  • ddb_extension_install_json
  • ddb_extension_enable_json
  • ddb_extension_disable_json
  • ddb_extension_list_json
  • ddb_extension_dependencies_json
  • ddb_extension_rebuild_json
  • ddb_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_null
  • ddb_stmt_bind_int64
  • ddb_stmt_bind_float64
  • ddb_stmt_bind_bool
  • ddb_stmt_bind_text
  • ddb_stmt_bind_blob
  • ddb_stmt_bind_geometry_wkb
  • ddb_stmt_bind_geography_wkb
  • ddb_stmt_bind_uuid
  • ddb_stmt_bind_decimal
  • ddb_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_view
  • ddb_stmt_bind_int64_step_i64_text_f64
  • ddb_stmt_fetch_row_views
  • ddb_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_json
  • ddb_db_describe_table_json
  • ddb_db_get_table_ddl
  • ddb_db_list_indexes_json
  • ddb_db_list_views_json
  • ddb_db_get_view_ddl
  • ddb_db_list_triggers_json
  • ddb_db_get_schema_snapshot_json
  • ddb_db_get_tooling_metadata_json
  • ddb_db_describe_query_json
  • ddb_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_checkpoint
  • ddb_db_save_as
  • ddb_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_json
  • ddb_sync_changeset_apply_json
  • ddb_sync_changeset_inspect_json
  • ddb_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_delete
  • branch_create, branch_list, branch_delete, branch_rename
  • branch_commit, branch_log, branch_diff
  • branch_restore
  • branch_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:

cargo build -p decentdb
bash tests/bindings/c/run.sh

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 repl are CLI behavior, not C ABI behavior.