Skip to content
Mazhar Ahmed edited this page Aug 23, 2026 · 1 revision

C API

include/qql.h is committed, so C consumers need no Rust toolchain.

#include <stdio.h>
#include <qql.h>

int main(void) {
    qql_context_t *ctx = qql_context_create("./sources");
    if (!ctx) return 1;

    char *result = qql_context_execute(ctx, "Q:2:1-5,255;Q:1;");
    printf("%s\n", result);

    qql_free_string(result);
    qql_context_destroy(ctx);
    return 0;
}
cargo build --release
cc example.c -Iinclude target/release/libqql.a -lpthread -ldl -lm -o example   # static
cc example.c -Iinclude -Ltarget/release -lqql -o example                       # shared

The static link needs -lpthread -ldl -lm because libqql.a does not carry Rust's runtime dependencies. The shared library already records them.

The surface

const char    *qql_version(void);
qql_context_t *qql_context_create(const char *data_directory);
char          *qql_context_execute(qql_context_t *ctx, const char *query);
void           qql_context_destroy(qql_context_t *ctx);
char          *qql_execute(const char *query);
void           qql_free_string(char *ptr);

That is the whole ABI. qql_context_t is opaque — only the pointer crosses.

Memory

qql_context_execute()  allocates the returned buffer
qql_free_string()      frees it
  • Returned strings are NUL-terminated UTF-8, and the caller owns them.
  • Release them with qql_free_string() — never free(), never a host language's allocator.
  • qql_version() is the one exception: a static string that must not be freed.
  • Results stay valid after qql_context_destroy — they are independently owned.

Guarantees

  • qql_context_execute never returns null and never returns malformed JSON. A null context, a null query and invalid UTF-8 all come back as {"ok":false,…}.
  • Panics never cross the boundary. Every entry point catches unwinds and returns QQL_INTERNAL_ERROR instead.
  • Invalid UTF-8 reports the byte offset where it went wrong rather than being lossily replaced.
  • qql_context_destroy(NULL) and qql_free_string(NULL) are no-ops.

Threading

One qql_context_t must not be used from two threads at once. In Rust the compiler enforces this; across the ABI it cannot, so it is your responsibility. Separate contexts on separate threads are fine.

qql_execute() uses a process-wide default context behind a mutex, reading $QQL_DATA (default ./sources). Thread-safe but serialized — prefer an explicit context for anything concurrent.

Keeping the header honest

scripts/c-smoke.sh compiles a C client against include/qql.h under -Wall -Wextra -Werror and links it to the real library, so a signature that drifts from src/ffi.rs fails there rather than at a user's link step.

./scripts/c-smoke.sh
./scripts/c-smoke.sh 'q:1:"الحمد"'

Careful: that script runs cargo build --release without features, so it overwrites the library with one that has no search engines. Rebuild with --features vector,fulltext afterwards if you need them.

Safety notes

All unsafe in the crate lives in src/ffi.rs — about 230 lines with no query logic. The rest is #![deny(unsafe_code)]. The FFI tests run under Miri and cover the abuse cases: null pointers, freeing null, invalid UTF-8, and strings outliving their context.

Clone this wiki locally