Skip to main content

kglite_c/
graph.rs

1//! `KgliteGraph` opaque handle — load_file, save_graph, free.
2//!
3//! Wraps `Arc<kglite::api::DirGraph>` so the C side can hold a
4//! cheap reference-counted snapshot. Session creation takes
5//! ownership of the handle (the underlying Arc moves into the
6//! Session); callers do NOT free the graph after handing it to
7//! [`kglite_session_new`](crate::kglite_session_new).
8
9use crate::status::KgliteStatusCode;
10use crate::strings::alloc_c_string;
11use kglite::api::{load_file, save_graph, DirGraph};
12use std::ffi::{c_char, CStr};
13use std::sync::Arc;
14
15/// Opaque handle for a knowledge graph. The C-side caller only
16/// ever sees `KgliteGraph*`; allocation, deallocation, and field
17/// access happen inside `kglite-c`.
18///
19/// cbindgen sees the `#[repr(C)]` empty struct and renders only a
20/// forward declaration in `kglite.h`. The actual state lives in
21/// the private [`GraphState`] sidecar: every `*mut KgliteGraph`
22/// the C side holds is really a `*mut GraphState` cast through
23/// the opaque facade.
24#[repr(C)]
25pub struct KgliteGraph {
26    _opaque: [u8; 0],
27    // Prevent C-side stack allocation: the !Send/!Sync marker isn't
28    // visible across the C ABI but stops downstream Rust callers
29    // from accidentally constructing one by value. (The real state
30    // is in GraphState; this struct is never instantiated.)
31    _marker: core::marker::PhantomData<(*mut u8, core::marker::PhantomPinned)>,
32}
33
34/// Private state backing a [`KgliteGraph`] handle. Never named at
35/// the C ABI surface — the C side only knows `KgliteGraph*`. We
36/// `Box::into_raw` a `GraphState`, cast the pointer to
37/// `*mut KgliteGraph`, and reverse the cast on free / use.
38pub(crate) struct GraphState {
39    pub(crate) inner: Arc<DirGraph>,
40}
41
42impl GraphState {
43    /// Allocate a new opaque handle wrapping `arc`.
44    pub(crate) fn into_handle(arc: Arc<DirGraph>) -> *mut KgliteGraph {
45        let boxed = Box::new(GraphState { inner: arc });
46        Box::into_raw(boxed).cast::<KgliteGraph>()
47    }
48
49    /// Mutably borrow the state behind a non-null handle. Caller
50    /// must uphold the C-ABI contract — the handle is valid, not
51    /// yet freed, and exclusively borrowed for the call. (A
52    /// `&mut` variant is the only borrower we need today: the
53    /// only read-only operation against a `GraphState` is
54    /// snapshot-taking, which we do by handing the graph to
55    /// `Session::from_arc` and moving ownership out via the Box.)
56    pub(crate) unsafe fn from_handle_mut<'a>(handle: *mut KgliteGraph) -> &'a mut GraphState {
57        unsafe { &mut *handle.cast::<GraphState>() }
58    }
59
60    /// Free a handle. Idempotent on null.
61    pub(crate) unsafe fn free_handle(handle: *mut KgliteGraph) {
62        if handle.is_null() {
63            return;
64        }
65        let _ = unsafe { Box::from_raw(handle.cast::<GraphState>()) };
66    }
67}
68
69/// Load a knowledge graph from disk. Accepts `.kgl` files
70/// (single-file mmap format) and directories (disk-backed CSR
71/// layout) — the loader picks the right path based on what's at
72/// `path`.
73///
74/// # Arguments
75///
76/// - `path` (in, borrowed): UTF-8 file path, null-terminated.
77/// - `out_graph` (out, owned): set to the loaded graph handle on
78///   success; caller must free via [`kglite_graph_free`]. Set to
79///   null on failure.
80/// - `out_error_msg` (out, owned): set to an owned error message
81///   on failure; caller must free via
82///   [`kglite_free_string`](crate::kglite_free_string). Set to
83///   null on success.
84///
85/// # Errors
86///
87/// - `KGLITE_ERR_NULL_POINTER` — `path` or `out_graph` is null
88/// - `KGLITE_ERR_INVALID_UTF8` — `path` isn't valid UTF-8
89/// - `KGLITE_ERR_FILE_NOT_FOUND` — `path` doesn't exist
90/// - `KGLITE_ERR_FILE_FORMAT` — file isn't a valid `.kgl` /
91///   disk-graph directory
92/// - `KGLITE_ERR_FILE_IO` — I/O failure during read
93///
94/// # Safety
95///
96/// `path` must point to a null-terminated UTF-8 string.
97/// `out_graph` must be a valid writable pointer to a
98/// `*mut KgliteGraph` slot. `out_error_msg` may be null (the
99/// caller doesn't care about the message); otherwise it must
100/// point to a valid writable `*const c_char` slot.
101#[no_mangle]
102pub unsafe extern "C" fn kglite_load_file(
103    path: *const c_char,
104    out_graph: *mut *mut KgliteGraph,
105    out_error_msg: *mut *const c_char,
106) -> KgliteStatusCode {
107    if path.is_null() || out_graph.is_null() {
108        return KgliteStatusCode::NullPointer;
109    }
110    let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
111        Ok(s) => s,
112        Err(_) => return KgliteStatusCode::InvalidUtf8,
113    };
114    match load_file(path_str) {
115        Ok(arc) => {
116            unsafe {
117                *out_graph = GraphState::into_handle(arc);
118            }
119            if !out_error_msg.is_null() {
120                unsafe {
121                    *out_error_msg = std::ptr::null();
122                }
123            }
124            KgliteStatusCode::Ok
125        }
126        Err(io_err) => {
127            unsafe {
128                *out_graph = std::ptr::null_mut();
129            }
130            let (code, message) = classify_io_error(&io_err);
131            if !out_error_msg.is_null() {
132                unsafe {
133                    *out_error_msg = alloc_c_string(&message);
134                }
135            }
136            code
137        }
138    }
139}
140
141/// Map a `std::io::Error` from `load_file` to a `KgliteStatusCode`
142/// plus a human-readable message. `load_file` returns `io::Error`
143/// regardless of the underlying cause; we sniff the `kind` to
144/// pick the right C-side code.
145fn classify_io_error(err: &std::io::Error) -> (KgliteStatusCode, String) {
146    let code = match err.kind() {
147        std::io::ErrorKind::NotFound => KgliteStatusCode::FileNotFound,
148        std::io::ErrorKind::InvalidData => KgliteStatusCode::FileFormat,
149        _ => KgliteStatusCode::FileIo,
150    };
151    (code, err.to_string())
152}
153
154/// Save a knowledge graph to disk. The on-disk format depends on
155/// the underlying storage mode — in-memory and mapped graphs
156/// produce a `.kgl` single-file; disk-backed graphs produce / fill
157/// a directory.
158///
159/// # Arguments
160///
161/// - `graph` (in, borrowed): the graph to save.
162/// - `path` (in, borrowed): UTF-8 destination path,
163///   null-terminated.
164/// - `out_error_msg` (out, owned): set to an owned error message
165///   on failure; caller must free via
166///   [`kglite_free_string`](crate::kglite_free_string). Set to
167///   null on success.
168///
169/// # Errors
170///
171/// - `KGLITE_ERR_NULL_POINTER` — `graph` or `path` is null
172/// - `KGLITE_ERR_INVALID_UTF8` — `path` isn't valid UTF-8
173/// - `KGLITE_ERR_FILE_IO` — write failed
174///
175/// # Safety
176///
177/// `graph` must be a valid `*mut KgliteGraph` previously returned
178/// by a `kglite_*` function and not yet freed. `path` must be a
179/// null-terminated UTF-8 string.
180#[no_mangle]
181pub unsafe extern "C" fn kglite_save_graph(
182    graph: *mut KgliteGraph,
183    path: *const c_char,
184    out_error_msg: *mut *const c_char,
185) -> KgliteStatusCode {
186    if graph.is_null() || path.is_null() {
187        return KgliteStatusCode::NullPointer;
188    }
189    let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
190        Ok(s) => s,
191        Err(_) => return KgliteStatusCode::InvalidUtf8,
192    };
193    // Safety: caller's responsibility per the function's safety
194    // doc — graph must be a valid handle. We take a transient
195    // &mut to its inner Arc (save_graph needs &mut Arc).
196    let state = unsafe { GraphState::from_handle_mut(graph) };
197    match save_graph(&mut state.inner, path_str) {
198        Ok(()) => {
199            if !out_error_msg.is_null() {
200                unsafe {
201                    *out_error_msg = std::ptr::null();
202                }
203            }
204            KgliteStatusCode::Ok
205        }
206        Err(msg) => {
207            if !out_error_msg.is_null() {
208                unsafe {
209                    *out_error_msg = alloc_c_string(&msg);
210                }
211            }
212            KgliteStatusCode::FileIo
213        }
214    }
215}
216
217/// Free a graph handle. Idempotent on null (no-op).
218///
219/// # Safety
220///
221/// `graph` must be either null or a pointer previously returned by
222/// [`kglite_load_file`] (or any future `kglite_*` function that
223/// returns a `*mut KgliteGraph`) and not yet freed. Calling twice
224/// on the same pointer is UB.
225///
226/// **Do NOT free** a graph handle that has been handed to
227/// [`kglite_session_new`](crate::kglite_session_new) — the session
228/// takes ownership and frees on its own teardown.
229#[no_mangle]
230pub unsafe extern "C" fn kglite_graph_free(graph: *mut KgliteGraph) {
231    // Safety: caller's responsibility per the function's safety doc.
232    unsafe { GraphState::free_handle(graph) };
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238    use std::ffi::CString;
239
240    #[test]
241    fn load_nonexistent_file_returns_file_not_found() {
242        let path = CString::new("/tmp/__kglite_c_does_not_exist__.kgl").unwrap();
243        let mut graph: *mut KgliteGraph = std::ptr::null_mut();
244        let mut err: *const c_char = std::ptr::null();
245        let rc =
246            unsafe { kglite_load_file(path.as_ptr(), &mut graph as *mut _, &mut err as *mut _) };
247        assert_eq!(rc, KgliteStatusCode::FileNotFound);
248        assert!(graph.is_null());
249        assert!(!err.is_null());
250        unsafe { crate::kglite_free_string(err) };
251    }
252
253    #[test]
254    fn load_null_path_returns_null_pointer() {
255        let mut graph: *mut KgliteGraph = std::ptr::null_mut();
256        let mut err: *const c_char = std::ptr::null();
257        let rc =
258            unsafe { kglite_load_file(std::ptr::null(), &mut graph as *mut _, &mut err as *mut _) };
259        assert_eq!(rc, KgliteStatusCode::NullPointer);
260    }
261
262    #[test]
263    fn graph_free_is_null_safe() {
264        unsafe { kglite_graph_free(std::ptr::null_mut()) };
265    }
266}