Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -361,3 +361,32 @@ jobs:
- name: Verify Android x86_64 binary
run: |
echo "Android x86_64 build completed successfully"

c-api:
name: C API (extern "C") - ${{ matrix.os }}
runs-on: ${{ matrix.os }}
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Cache CMake deps
uses: actions/cache@v5
with:
path: build-capi/_deps
key: ${{ runner.os }}-capi-deps-${{ hashFiles('CMakeLists.txt') }}
restore-keys: ${{ runner.os }}-capi-deps-

- name: Configure CMake (C API)
run: cmake -B build-capi -DCMAKE_BUILD_TYPE=Release -DVANEDB_BUILD_CAPI=ON -DVANEDB_BUILD_BENCHMARKS=OFF -DVANEDB_BUILD_EXAMPLES=OFF -DVANEDB_BUILD_PYTHON=OFF

# --config Release is required for MSVC (multi-config generator); harmless on single-config Unix generators.
- name: Build C API smoke test
run: cmake --build build-capi --target test_capi --config Release --parallel

- name: Run C API test
run: ctest --test-dir build-capi -R capi --build-config Release --output-on-failure
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ external/

# Build artifacts (multiple per-config build trees from benchmarking)
build_*/
build-capi/

# Coverage artifacts
*.gcda
Expand Down
27 changes: 26 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.20)
project(vanedb VERSION 0.1.0 LANGUAGES CXX)
project(vanedb VERSION 0.1.0 LANGUAGES CXX C)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
Expand All @@ -21,6 +21,9 @@ if(NOT MSVC)
set(CMAKE_CXX_FLAGS_RELEASE "-O3")
set(CMAKE_CXX_FLAGS_DEBUG "-O0 -g")
set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "-O2 -g")
set(CMAKE_C_FLAGS_RELEASE "-O3")
set(CMAKE_C_FLAGS_DEBUG "-O0 -g")
set(CMAKE_C_FLAGS_RELWITHDEBINFO "-O2 -g")
endif()

# Detect x86_64 for AVX2 support
Expand Down Expand Up @@ -178,6 +181,28 @@ if(VANEDB_BUILD_CUDA)
# hardware in CI. Metal is the supported GPU path.
endif()

# C API (shippable extern "C" surface; consumed by vanedb-bench)
option(VANEDB_BUILD_CAPI "Build the C API static library" OFF)
if(VANEDB_BUILD_CAPI)
add_library(vanedb_cpp_capi STATIC capi/vanedb_capi.cpp)
target_include_directories(vanedb_cpp_capi PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/src
${CMAKE_CURRENT_SOURCE_DIR})
target_compile_features(vanedb_cpp_capi PRIVATE cxx_std_20)

if(VANEDB_BUILD_TESTS)
add_executable(test_capi tests/capi/test_capi.c)
target_include_directories(test_capi PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(test_capi PRIVATE vanedb_cpp_capi)
# The static lib is C++; link the C test with the C++ runtime.
set_target_properties(test_capi PROPERTIES LINKER_LANGUAGE CXX)
add_test(NAME capi COMMAND test_capi)
endif()

install(TARGETS vanedb_cpp_capi ARCHIVE DESTINATION lib)
install(FILES capi/vanedb_capi.h DESTINATION include/vanedb)
endif()

# Install
install(TARGETS vanedb EXPORT vanedb-targets)
install(DIRECTORY src/core/ DESTINATION include/vanedb FILES_MATCHING PATTERN "*.h" PATTERN "*.cuh")
Expand Down
124 changes: 124 additions & 0 deletions capi/vanedb_capi.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
#include "capi/vanedb_capi.h"
#include "core/vector_store.h"
#include "core/hnsw_index.h"
#include "core/mmap_vector_store.h"

using namespace vanedb;

namespace {
DistanceMetric to_metric(vanedb_metric m) {
switch (m) {
case VANEDB_COSINE: return DistanceMetric::COSINE;
case VANEDB_DOT: return DistanceMetric::DOT;
case VANEDB_L2:
default: return DistanceMetric::L2;
}
}
} // namespace

extern "C" {

float vanedb_cpp_l2_sq(const float* a, const float* b, size_t dim) {
return l2_sq(a, b, dim);
}
float vanedb_cpp_cosine_distance(const float* a, const float* b, size_t dim) {
return cosine_distance(a, b, dim);
}
float vanedb_cpp_dot_product(const float* a, const float* b, size_t dim) {
return dot_product(a, b, dim);
}

vanedb_cpp_store* vanedb_cpp_store_new(size_t dim, vanedb_metric metric) {
try { return reinterpret_cast<vanedb_cpp_store*>(new VectorStore(dim, to_metric(metric))); }
catch (...) { return nullptr; }
}
int vanedb_cpp_store_add(vanedb_cpp_store* s, uint64_t id, const float* v) {
if (!s) return 1;
try { reinterpret_cast<VectorStore*>(s)->add(id, v); return 0; }
catch (...) { return 1; }
}
size_t vanedb_cpp_store_search(vanedb_cpp_store* s, const float* q, size_t k,
uint64_t* out_ids, float* out_dists) {
if (!s) return 0;
try {
auto res = reinterpret_cast<VectorStore*>(s)->search(q, k);
size_t n = res.size() < k ? res.size() : k;
for (size_t i = 0; i < n; ++i) { out_ids[i] = res[i].id; out_dists[i] = res[i].distance; }
return n;
} catch (...) { return 0; }
}
void vanedb_cpp_store_free(vanedb_cpp_store* s) {
delete reinterpret_cast<VectorStore*>(s);
}

vanedb_cpp_hnsw* vanedb_cpp_hnsw_new(size_t dim, vanedb_metric metric, size_t capacity,
size_t M, size_t ef_construction, uint64_t seed) {
try {
return reinterpret_cast<vanedb_cpp_hnsw*>(
// seed is uint64_t in the ABI (Rust parity) but the core takes uint32_t; high bits are dropped.
new HNSWIndex(dim, to_metric(metric), capacity, M, ef_construction,
static_cast<uint32_t>(seed)));
} catch (...) { return nullptr; }
}
int vanedb_cpp_hnsw_add(vanedb_cpp_hnsw* h, uint64_t id, const float* v) {
if (!h) return 1;
try { reinterpret_cast<HNSWIndex*>(h)->add(id, v); return 0; }
catch (...) { return 1; }
}
size_t vanedb_cpp_hnsw_search(vanedb_cpp_hnsw* h, const float* q, size_t k, size_t ef_search,
uint64_t* out_ids, float* out_dists) {
if (!h) return 0;
try {
auto* idx = reinterpret_cast<HNSWIndex*>(h);
idx->set_ef_search(ef_search);
auto res = idx->search(q, k);
size_t n = res.size() < k ? res.size() : k;
for (size_t i = 0; i < n; ++i) { out_ids[i] = res[i].id; out_dists[i] = res[i].distance; }
return n;
} catch (...) { return 0; }
}
int vanedb_cpp_hnsw_save(vanedb_cpp_hnsw* h, const char* path) {
if (!h) return 1;
if (!path) return 1;
try { reinterpret_cast<HNSWIndex*>(h)->save(path); return 0; }
catch (...) { return 1; }
}
vanedb_cpp_hnsw* vanedb_cpp_hnsw_load(const char* path) {
if (!path) return nullptr;
try { return reinterpret_cast<vanedb_cpp_hnsw*>(HNSWIndex::load(path).release()); }
catch (...) { return nullptr; }
}
void vanedb_cpp_hnsw_free(vanedb_cpp_hnsw* h) {
delete reinterpret_cast<HNSWIndex*>(h);
}

int vanedb_cpp_mmap_build(const char* path, size_t dim, vanedb_metric metric,
const uint64_t* ids, const float* vecs, size_t n) {
if (!path) return 1;
try {
MMapVectorStoreBuilder b(dim, to_metric(metric));
for (size_t i = 0; i < n; ++i) b.add(ids[i], vecs + i * dim);
b.save(path);
return 0;
} catch (...) { return 1; }
}
vanedb_cpp_mmap* vanedb_cpp_mmap_open(const char* path) {
if (!path) return nullptr;
try { return reinterpret_cast<vanedb_cpp_mmap*>(new MMapVectorStore(path)); }
catch (...) { return nullptr; }
}
size_t vanedb_cpp_mmap_search(vanedb_cpp_mmap* m, const float* q, size_t k,
uint64_t* out_ids, float* out_dists) {
if (!m) return 0;
try {
auto res = reinterpret_cast<MMapVectorStore*>(m)->search(q, k);
size_t n = res.size() < k ? res.size() : k;
for (size_t i = 0; i < n; ++i) { out_ids[i] = res[i].id; out_dists[i] = res[i].distance; }
return n;
} catch (...) { return 0; }
}
void vanedb_cpp_mmap_free(vanedb_cpp_mmap* m) {
delete reinterpret_cast<MMapVectorStore*>(m);
}

}
68 changes: 68 additions & 0 deletions capi/vanedb_capi.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#ifndef VANEDB_CAPI_H
#define VANEDB_CAPI_H
#include <stddef.h>
#include <stdint.h>

#ifdef __cplusplus
extern "C" {
#endif

typedef enum { VANEDB_L2 = 0, VANEDB_COSINE = 1, VANEDB_DOT = 2 } vanedb_metric;

/*
* ABI conventions (C API v0 — unstable until a tagged release):
* - Handles are opaque; every constructor (_new/_open) has a matching _free.
* - Handle pointers are intentionally non-const (incl. read-only search/save)
* to keep this ABI byte-identical to the parallel Rust C API (vanedb_rs_*),
* which the benchmark harness calls through one uniform FFI. Do not add const.
* - int returns: 0 = success, non-zero = failure. Constructors return NULL on
* failure. _search returns the number of results written (0 on error/empty).
* - out_ids / out_dists are caller-owned buffers of length k.
* - vanedb_cpp_hnsw_search takes ef_search per call (the implementation sets it
* then searches). This mirrors the Rust ABI; it is not thread-safe to call
* concurrently with different ef_search values on the same handle, which the
* single-threaded benchmark consumer never does. Do not remove this parameter.
* Callers needing concurrent search must use a separate handle per thread.
* - to_metric maps any unrecognized metric value to L2 (no error).
* - vanedb_cpp_hnsw_new: seed is uint64_t for ABI parity but only the low 32 bits are used
* (the C++ core takes uint32_t); cross-implementation graphs differ by RNG regardless,
* so this does not affect the recall-based comparison.
*/

/* Distance (stateless) */
float vanedb_cpp_l2_sq(const float* a, const float* b, size_t dim);
float vanedb_cpp_cosine_distance(const float* a, const float* b, size_t dim);
float vanedb_cpp_dot_product(const float* a, const float* b, size_t dim);

/* VectorStore (brute force) */
typedef struct vanedb_cpp_store vanedb_cpp_store;
vanedb_cpp_store* vanedb_cpp_store_new(size_t dim, vanedb_metric metric);
int vanedb_cpp_store_add(vanedb_cpp_store* s, uint64_t id, const float* v);
size_t vanedb_cpp_store_search(vanedb_cpp_store* s, const float* q, size_t k,
uint64_t* out_ids, float* out_dists);
void vanedb_cpp_store_free(vanedb_cpp_store* s);

/* HNSW */
typedef struct vanedb_cpp_hnsw vanedb_cpp_hnsw;
vanedb_cpp_hnsw* vanedb_cpp_hnsw_new(size_t dim, vanedb_metric metric, size_t capacity,
size_t M, size_t ef_construction, uint64_t seed);
int vanedb_cpp_hnsw_add(vanedb_cpp_hnsw* h, uint64_t id, const float* v);
size_t vanedb_cpp_hnsw_search(vanedb_cpp_hnsw* h, const float* q, size_t k, size_t ef_search,
uint64_t* out_ids, float* out_dists);
int vanedb_cpp_hnsw_save(vanedb_cpp_hnsw* h, const char* path);
vanedb_cpp_hnsw* vanedb_cpp_hnsw_load(const char* path);
void vanedb_cpp_hnsw_free(vanedb_cpp_hnsw* h);

/* MMap store */
typedef struct vanedb_cpp_mmap vanedb_cpp_mmap;
int vanedb_cpp_mmap_build(const char* path, size_t dim, vanedb_metric metric,
const uint64_t* ids, const float* vecs, size_t n);
vanedb_cpp_mmap* vanedb_cpp_mmap_open(const char* path);
size_t vanedb_cpp_mmap_search(vanedb_cpp_mmap* m, const float* q, size_t k,
uint64_t* out_ids, float* out_dists);
void vanedb_cpp_mmap_free(vanedb_cpp_mmap* m);

#ifdef __cplusplus
}
#endif
#endif /* VANEDB_CAPI_H */
Loading
Loading