Skip to main content

kglite_c/
embedder.rs

1//! `KgliteEmbedder` opaque handle + concrete-impl factories.
2//!
3//! v1 ships only concrete embedder factories (fastembed,
4//! feature-gated). Trait objects (`Arc<dyn Embedder>`) can't
5//! cross the C ABI as such; the C side allocates a concrete impl
6//! via the factory and gets back an opaque handle, then attaches
7//! it to a session via [`kglite_session_set_embedder`].
8//!
9//! Future v2 may add a user-supplied-embedder pattern
10//! (function-pointer + opaque context callback) for bindings that
11//! want to plug in their own embedder (OpenAI, Cohere, etc.). Out
12//! of scope for the H.3 initial cut.
13
14use crate::session::{KgliteSession, SessionState};
15use crate::status::KgliteStatusCode;
16use kglite::api::Embedder;
17use std::sync::Arc;
18
19#[cfg(feature = "fastembed")]
20use crate::strings::alloc_c_string;
21#[cfg(feature = "fastembed")]
22use std::ffi::{c_char, CStr};
23
24/// Opaque handle for an embedder. See
25/// [`KgliteGraph`](crate::KgliteGraph) for the rationale on the
26/// empty `#[repr(C)]` facade pattern — cbindgen renders only a
27/// forward declaration; the actual state lives in
28/// [`EmbedderState`].
29#[repr(C)]
30pub struct KgliteEmbedder {
31    _opaque: [u8; 0],
32    _marker: core::marker::PhantomData<(*mut u8, core::marker::PhantomPinned)>,
33}
34
35/// Private state backing a [`KgliteEmbedder`] handle. Holds the
36/// concrete `Arc<dyn Embedder>` — the trait object never crosses
37/// the C ABI; only the handle does.
38pub(crate) struct EmbedderState {
39    pub(crate) inner: Arc<dyn Embedder>,
40}
41
42impl EmbedderState {
43    // Used only by feature-gated factories today (fastembed); future
44    // factories (user-supplied embedder, OpenAI, etc.) will reach
45    // for the same constructor. Suppress dead-code when only the
46    // default features are enabled.
47    #[allow(dead_code)]
48    pub(crate) fn into_handle(inner: Arc<dyn Embedder>) -> *mut KgliteEmbedder {
49        let boxed = Box::new(EmbedderState { inner });
50        Box::into_raw(boxed).cast::<KgliteEmbedder>()
51    }
52
53    pub(crate) unsafe fn from_handle<'a>(handle: *const KgliteEmbedder) -> &'a EmbedderState {
54        unsafe { &*handle.cast::<EmbedderState>() }
55    }
56
57    unsafe fn free_handle(handle: *mut KgliteEmbedder) {
58        if handle.is_null() {
59            return;
60        }
61        let _ = unsafe { Box::from_raw(handle.cast::<EmbedderState>()) };
62    }
63}
64
65/// Free an embedder handle. Idempotent on null.
66///
67/// # Safety
68///
69/// `embedder` must be either null or a valid pointer previously
70/// returned by a `kglite_embedder_*_new` factory and not yet
71/// freed. Calling twice on the same pointer is UB.
72///
73/// **Do NOT free** an embedder that has been handed to
74/// [`kglite_session_set_embedder`] — the session retains a clone
75/// of the inner Arc; you may free your handle after the call to
76/// set_embedder (the Arc keeps the embedder alive until the
77/// session drops). For symmetry with other handles, the safest
78/// pattern is: factory → set_embedder → free_embedder. Once the
79/// Arc is shared, the original handle is no longer special.
80#[no_mangle]
81pub unsafe extern "C" fn kglite_embedder_free(embedder: *mut KgliteEmbedder) {
82    unsafe { EmbedderState::free_handle(embedder) };
83}
84
85/// Attach an embedder to a session. The session retains a clone
86/// of the embedder's inner `Arc`, so subsequent
87/// [`kglite_session_execute_read`](crate::kglite_session_execute_read)
88/// calls have access to `text_score()` and other embedder-backed
89/// Cypher functions.
90///
91/// The caller may free the embedder handle after this call
92/// returns — the `Arc` clone keeps the underlying embedder
93/// alive for the session's lifetime.
94///
95/// # Safety
96///
97/// `session` and `embedder` must be valid handles previously
98/// returned by `kglite_session_new` and a `kglite_embedder_*_new`
99/// factory respectively, neither yet freed.
100#[no_mangle]
101pub unsafe extern "C" fn kglite_session_set_embedder(
102    session: *mut KgliteSession,
103    embedder: *const KgliteEmbedder,
104) -> KgliteStatusCode {
105    if session.is_null() || embedder.is_null() {
106        return KgliteStatusCode::NullPointer;
107    }
108    let session_state = unsafe { SessionState::from_handle_mut(session) };
109    let embedder_state = unsafe { EmbedderState::from_handle(embedder) };
110    session_state.embedder = Some(Arc::clone(&embedder_state.inner));
111    KgliteStatusCode::Ok
112}
113
114// ───────────────────────── concrete factories ──────────────────────────
115
116/// Construct a fastembed-rs-backed embedder.
117///
118/// fastembed-rs downloads ONNX model weights on first
119/// `embed()` call (cached at `~/.cache/fastembed/`). The factory
120/// does NOT block on download — model name validation only. The
121/// first Cypher query using `text_score()` triggers the download.
122///
123/// # Arguments
124///
125/// - `model_name` (in, borrowed): a known fastembed model name,
126///   e.g. `"BAAI/bge-m3"`, `"sentence-transformers/all-MiniLM-L6-v2"`.
127///   See fastembed-rs's TextEmbedding::list_supported_models() for
128///   the full list.
129/// - `out_embedder` (out, owned): on success, set to an embedder
130///   handle. Caller must free via [`kglite_embedder_free`] (or
131///   transfer ownership via [`kglite_session_set_embedder`]).
132/// - `out_error_msg` (out, owned, may be null): on failure, set to
133///   an owned error string.
134///
135/// # Errors
136///
137/// - `KGLITE_STATUS_CODE_NULL_POINTER` — required pointer is null
138/// - `KGLITE_STATUS_CODE_INVALID_UTF8` — `model_name` isn't valid UTF-8
139/// - `KGLITE_STATUS_CODE_INVALID_ARGUMENT` — `model_name` isn't a known
140///   fastembed model
141///
142/// # Feature gate
143///
144/// Available only when `kglite-c` is built with the `fastembed`
145/// Cargo feature.
146///
147/// # Safety
148///
149/// `model_name` must be a null-terminated UTF-8 string.
150/// `out_embedder` must be a valid writable pointer.
151#[cfg(feature = "fastembed")]
152#[no_mangle]
153pub unsafe extern "C" fn kglite_embedder_fastembed_new(
154    model_name: *const c_char,
155    out_embedder: *mut *mut KgliteEmbedder,
156    out_error_msg: *mut *const c_char,
157) -> KgliteStatusCode {
158    if model_name.is_null() || out_embedder.is_null() {
159        return KgliteStatusCode::NullPointer;
160    }
161    let model_str = match unsafe { CStr::from_ptr(model_name) }.to_str() {
162        Ok(s) => s,
163        Err(_) => return KgliteStatusCode::InvalidUtf8,
164    };
165    match kglite::api::FastEmbedAdapter::new(model_str) {
166        Ok(adapter) => {
167            let arc: Arc<dyn Embedder> = Arc::new(adapter);
168            unsafe {
169                *out_embedder = EmbedderState::into_handle(arc);
170            }
171            if !out_error_msg.is_null() {
172                unsafe {
173                    *out_error_msg = std::ptr::null();
174                }
175            }
176            KgliteStatusCode::Ok
177        }
178        Err(msg) => {
179            unsafe {
180                *out_embedder = std::ptr::null_mut();
181            }
182            if !out_error_msg.is_null() {
183                unsafe {
184                    *out_error_msg = alloc_c_string(&msg);
185                }
186            }
187            KgliteStatusCode::InvalidArgument
188        }
189    }
190}