C ABI reference
The C ABI (engine.h, ENGINE_ABI_VERSION 3) is the single boundary every runtime binds to — bun:ffi, NAPI, or a static link. It is frozen: new backends and capabilities are added behind the same symbols without changing these signatures (the vector type added only a new bind tag, no new symbols). Opaque handles only; no Rust panic ever crosses this line.
Handles
Three opaque handle types. Callers never see the Rust layout behind them; they move them only through the functions below.
EngineHandle- A connection. Created by
engine_openorengine_branch; freed byengine_close. EngineResult- A buffered query result (all rows materialised). Produced by
engine_query; freed byengine_result_free. EngineStmt- A prepared statement with a row cursor. Produced by
engine_prepare; freed byengine_finalize.
Status codes
Every non-constructor function returns an EngineStatus. The seven codes are exhaustive.
| Code | Value | Meaning |
|---|---|---|
ENGINE_OK | 0 | Success. |
ENGINE_ERR_SQL | 1 | Parse / plan / type error. |
ENGINE_ERR_CONSTRAINT | 2 | Unique / foreign-key / check violation. |
ENGINE_ERR_CONFLICT | 3 | Serialization / write conflict — retryable. |
ENGINE_ERR_STORAGE | 4 | Backend I/O, CAS rejected, or S3 fault. |
ENGINE_ERR_TXN | 5 | Illegal state-machine transition (e.g. DDL inside a transaction). |
ENGINE_ERR_MISUSE | 6 | Null handle, use-after-free, or bad argument. |
ENGINE_ERR_INTERNAL | 7 | A bug — a caught panic. The engine stays defined; never UB. |
Lifecycle
EngineHandle* engine_open(const char* url)- Open a connection. The NUL-terminated
urlselects the storage backend by scheme (file://./local.db,s3://bucket/mydb?region=…). ReturnsNULLon failure — the caller then has no handle to query. void engine_close(EngineHandle* h)- Close and free a connection. Idempotent on
NULL.
One-shot execution
EngineStatus engine_exec(EngineHandle* h, const char* sql)- Run DDL/DML with no result set. The affected row count is read afterward with
engine_changes. EngineStatus engine_query(EngineHandle* h, const char* sql, EngineResult** out)- Run a query. On
ENGINE_OK,*outreceives an ownedEngineResult; otherwise*outisNULL. Values are NUL-terminated text; a SQLNULLis reported as aNULLpointer. Free the result withengine_result_free.
Prepared statements
Prepare once, bind typed parameters, and step the cursor row by row. Reuse with engine_reset (bindings are kept).
EngineStatus engine_prepare(EngineHandle* h, const char* sql, EngineStmt** out)- Compile
sqlinto a statement. OnENGINE_OK,*outreceives the ownedEngineStmt. EngineStatus engine_bind(EngineStmt* s, int idx, const char* value)- Bind a parameter by 1-based positional
idx.valueis a NUL-terminated typed literal (see Parameter encoding). EngineStatus engine_step(EngineStmt* s, int* done)- Advance the cursor. On
ENGINE_OK,*done == 0means a row is current (read its columns withengine_column_value);*done == 1means there are no more rows. EngineStatus engine_finalize(EngineStmt* s)- Free the statement.
EngineStatus engine_reset(EngineStmt* s)- Re-execute from the start, keeping the current bindings.
Transactions
EngineStatus engine_begin(EngineHandle* h)- Start an explicit transaction.
EngineStatus engine_commit(EngineHandle* h)- Commit. Blocks until the WAL is durable before returning.
EngineStatus engine_rollback(EngineHandle* h)- Discard the transaction's pending changes.
Branching
EngineHandle* engine_branch(EngineHandle* h, const char* name)- Create a copy-on-write branch off the database
his connected to, at its current committed LSN, and return a new connection handle bound to that branch. The branch shares the base's immutable history but writes in isolation — neither the base nor any sibling sees a branch's writes. The returned handle is owned by the caller and freed withengine_close. ReturnsNULLon failure (e.g. inside an active transaction, or branch-of-branch); the reason is available viaengine_last_error(h).
Result and row access
Read a buffered EngineResult by row and column. Returned const char* pointers are borrowed and valid until engine_result_free(r).
int engine_result_rows(const EngineResult* r)- Number of rows in the result.
int engine_result_cols(const EngineResult* r)- Number of columns.
const char* engine_result_colname(const EngineResult* r, int col)- Borrowed column name for
col. const char* engine_result_value(const EngineResult* r, int row, int col)- Borrowed cell value at
(row, col).NULLfor a SQLNULLcell. Valid untilengine_result_free(r).
Statement cursor access
Read the column metadata and the current row of a stepping EngineStmt.
int engine_column_count(const EngineStmt* s)- Number of columns the statement produces.
const char* engine_column_name(const EngineStmt* s, int col)- Borrowed name of column
col. const char* engine_column_value(const EngineStmt* s, int col)- Borrowed value of column
colfor the current row.NULLfor a SQLNULLcell. Valid only until the nextengine_step/engine_reset/engine_finalizeons.
Copy borrowed pointers before they expire
A cursor value pointer is invalidated by the next step/reset/finalize; a result value pointer is invalidated by engine_result_free. Copy any value you need to keep out before the owning object advances or is freed.
Errors and metadata
const char* engine_last_error(EngineHandle* h)- Borrowed, per-handle C string describing the last error on
h. Valid until the next call onh. Empty string if there is none. long long engine_changes(EngineHandle* h)- Rows affected by the last statement.
long long engine_last_lsn(EngineHandle* h)- Commit LSN of the last commit.
int engine_abi_version(void)- The ABI version this library exports — currently
3. Check it againstENGINE_ABI_VERSIONat load time.
Freeing
void engine_result_free(EngineResult* r)- Free a query result. Idempotent on
NULL.
Statements are freed by engine_finalize; handles by engine_close.
Parameter encoding
engine_bind takes each parameter as a NUL-terminated typed literal: a one-character tag followed by the encoded value. This is how the string-only ABI carries a typed argument.
| Tag | Type | Example |
|---|---|---|
i | Integer | i42 |
f | Float | f3.5 |
s | Text | shello |
b | Bytes (base64) | b<base64> |
n | NULL | n |
v | Vector | v[1,2,3] or v1,2,3 |
Safety contract
- MUST assume no Rust panic crosses the boundary — a caught panic becomes
ENGINE_ERR_INTERNALand the handle stays defined and queryable. - MUST treat a null or invalid handle as
ENGINE_ERR_MISUSE— null handles, use-after-free, and bad arguments all map to this code rather than undefined behaviour. - MUST read every value as NUL-terminated text; a SQL
NULLis aNULLpointer, distinct from the empty string. - MUST copy a borrowed
const char*out before the owning object advances or is freed — cursor values expire on the nextstep/reset/finalize; result values onengine_result_free; error strings on the next call to the handle. - MUST NOT use a single handle concurrently from multiple threads. Distinct handles are thread-safe with respect to one another; one handle is not.
Minimal C example
Open, run a one-shot statement, query, then free and close. All < / > below are literal C source.
#include <stdio.h>
#include "engine.h"
int main(void) {
EngineHandle* h = engine_open("file://./demo.db");
if (!h) return 1;
engine_exec(h, "CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, title TEXT)");
engine_exec(h, "INSERT INTO notes (id, title) VALUES (1, 'hello')");
EngineResult* r = NULL;
if (engine_query(h, "SELECT id, title FROM notes ORDER BY id", &r) == ENGINE_OK) {
int rows = engine_result_rows(r);
int cols = engine_result_cols(r);
for (int i = 0; i < rows; i++) {
for (int c = 0; c < cols; c++) {
const char* v = engine_result_value(r, i, c);
printf("%s%s", v ? v : "NULL", c + 1 < cols ? "\t" : "\n");
}
}
engine_result_free(r);
} else {
printf("query failed: %s\n", engine_last_error(h));
}
engine_close(h);
return 0;
}
From Bun you rarely touch this directly
The @twilldb/bun client wraps every function here over bun:ffi, encodes the typed-literal parameters, and pins EXPECTED_ABI_VERSION to catch a stale binary. See Connect from Bun for the ergonomic typed API.