Skip to main content

agentic_memory/ffi/
c_api.rs

1//! C-compatible FFI bindings.
2
3use std::ffi::CStr;
4use std::os::raw::c_char;
5use std::path::Path;
6
7use crate::format::{AmemReader, AmemWriter};
8use crate::graph::MemoryGraph;
9use crate::types::{CognitiveEventBuilder, Edge, EdgeType, EventType};
10
11const AMEM_OK: i32 = 0;
12const AMEM_ERR_IO: i32 = -1;
13const AMEM_ERR_INVALID: i32 = -2;
14const AMEM_ERR_NOT_FOUND: i32 = -3;
15const AMEM_ERR_OVERFLOW: i32 = -4;
16const AMEM_ERR_NULL_PTR: i32 = -5;
17
18/// Create a new empty graph. Returns handle or NULL on failure.
19#[no_mangle]
20pub extern "C" fn amem_graph_new(dimension: u32) -> *mut std::ffi::c_void {
21    std::panic::catch_unwind(|| {
22        let graph = Box::new(MemoryGraph::new(dimension as usize));
23        Box::into_raw(graph) as *mut std::ffi::c_void
24    })
25    .unwrap_or(std::ptr::null_mut())
26}
27
28/// Load a graph from an .amem file. Returns handle or NULL on failure.
29///
30/// # Safety
31///
32/// The caller must ensure that all pointer arguments are valid and non-null
33/// (unless the function explicitly handles null pointers), and that any
34/// pointed-to data is valid for the expected type and lifetime.
35#[no_mangle]
36pub unsafe extern "C" fn amem_graph_open(path: *const c_char) -> *mut std::ffi::c_void {
37    std::panic::catch_unwind(|| {
38        if path.is_null() {
39            return std::ptr::null_mut();
40        }
41        let path_str = unsafe { CStr::from_ptr(path) };
42        let path_str = match path_str.to_str() {
43            Ok(s) => s,
44            Err(_) => return std::ptr::null_mut(),
45        };
46        match AmemReader::read_from_file(Path::new(path_str)) {
47            Ok(graph) => Box::into_raw(Box::new(graph)) as *mut std::ffi::c_void,
48            Err(_) => std::ptr::null_mut(),
49        }
50    })
51    .unwrap_or(std::ptr::null_mut())
52}
53
54/// Save a graph to an .amem file. Returns AMEM_OK or error code.
55///
56/// # Safety
57///
58/// The caller must ensure that all pointer arguments are valid and non-null
59/// (unless the function explicitly handles null pointers), and that any
60/// pointed-to data is valid for the expected type and lifetime.
61#[no_mangle]
62pub unsafe extern "C" fn amem_graph_save(graph: *mut std::ffi::c_void, path: *const c_char) -> i32 {
63    std::panic::catch_unwind(|| {
64        if graph.is_null() || path.is_null() {
65            return AMEM_ERR_NULL_PTR;
66        }
67        let graph = unsafe { &*(graph as *const MemoryGraph) };
68        let path_str = unsafe { CStr::from_ptr(path) };
69        let path_str = match path_str.to_str() {
70            Ok(s) => s,
71            Err(_) => return AMEM_ERR_INVALID,
72        };
73        let writer = AmemWriter::new(graph.dimension());
74        match writer.write_to_file(graph, Path::new(path_str)) {
75            Ok(()) => AMEM_OK,
76            Err(_) => AMEM_ERR_IO,
77        }
78    })
79    .unwrap_or(AMEM_ERR_IO)
80}
81
82/// Free a graph handle.
83///
84/// # Safety
85///
86/// The caller must ensure that all pointer arguments are valid and non-null
87/// (unless the function explicitly handles null pointers), and that any
88/// pointed-to data is valid for the expected type and lifetime.
89#[no_mangle]
90pub unsafe extern "C" fn amem_graph_free(graph: *mut std::ffi::c_void) {
91    if !graph.is_null() {
92        let _ = std::panic::catch_unwind(|| unsafe {
93            drop(Box::from_raw(graph as *mut MemoryGraph));
94        });
95    }
96}
97
98/// Get node count.
99///
100/// # Safety
101///
102/// The caller must ensure that all pointer arguments are valid and non-null
103/// (unless the function explicitly handles null pointers), and that any
104/// pointed-to data is valid for the expected type and lifetime.
105#[no_mangle]
106pub unsafe extern "C" fn amem_graph_node_count(graph: *mut std::ffi::c_void) -> u64 {
107    std::panic::catch_unwind(|| {
108        if graph.is_null() {
109            return 0;
110        }
111        let graph = unsafe { &*(graph as *const MemoryGraph) };
112        graph.node_count() as u64
113    })
114    .unwrap_or(0)
115}
116
117/// Get edge count.
118///
119/// # Safety
120///
121/// The caller must ensure that all pointer arguments are valid and non-null
122/// (unless the function explicitly handles null pointers), and that any
123/// pointed-to data is valid for the expected type and lifetime.
124#[no_mangle]
125pub unsafe extern "C" fn amem_graph_edge_count(graph: *mut std::ffi::c_void) -> u64 {
126    std::panic::catch_unwind(|| {
127        if graph.is_null() {
128            return 0;
129        }
130        let graph = unsafe { &*(graph as *const MemoryGraph) };
131        graph.edge_count() as u64
132    })
133    .unwrap_or(0)
134}
135
136/// Get dimension.
137///
138/// # Safety
139///
140/// The caller must ensure that all pointer arguments are valid and non-null
141/// (unless the function explicitly handles null pointers), and that any
142/// pointed-to data is valid for the expected type and lifetime.
143#[no_mangle]
144pub unsafe extern "C" fn amem_graph_dimension(graph: *mut std::ffi::c_void) -> u32 {
145    std::panic::catch_unwind(|| {
146        if graph.is_null() {
147            return 0;
148        }
149        let graph = unsafe { &*(graph as *const MemoryGraph) };
150        graph.dimension() as u32
151    })
152    .unwrap_or(0)
153}
154
155/// Add a node. Returns the assigned node ID, or -1 on error.
156///
157/// # Safety
158///
159/// The caller must ensure that all pointer arguments are valid and non-null
160/// (unless the function explicitly handles null pointers), and that any
161/// pointed-to data is valid for the expected type and lifetime.
162#[no_mangle]
163pub unsafe extern "C" fn amem_graph_add_node(
164    graph: *mut std::ffi::c_void,
165    event_type: u8,
166    content: *const c_char,
167    session_id: u32,
168    confidence: f32,
169) -> i64 {
170    std::panic::catch_unwind(|| {
171        if graph.is_null() || content.is_null() {
172            return -1i64;
173        }
174        let graph = unsafe { &mut *(graph as *mut MemoryGraph) };
175        let content_str = unsafe { CStr::from_ptr(content) };
176        let content_str = match content_str.to_str() {
177            Ok(s) => s,
178            Err(_) => return -1,
179        };
180        let et = match EventType::from_u8(event_type) {
181            Some(et) => et,
182            None => return -1,
183        };
184        let event = CognitiveEventBuilder::new(et, content_str)
185            .session_id(session_id)
186            .confidence(confidence)
187            .build();
188        match graph.add_node(event) {
189            Ok(id) => id as i64,
190            Err(_) => -1,
191        }
192    })
193    .unwrap_or(-1)
194}
195
196/// Get a node's content. Writes to buffer. Returns content length or error.
197///
198/// # Safety
199///
200/// The caller must ensure that all pointer arguments are valid and non-null
201/// (unless the function explicitly handles null pointers), and that any
202/// pointed-to data is valid for the expected type and lifetime.
203#[no_mangle]
204pub unsafe extern "C" fn amem_graph_get_content(
205    graph: *mut std::ffi::c_void,
206    node_id: u64,
207    buffer: *mut c_char,
208    buffer_size: u32,
209) -> i32 {
210    std::panic::catch_unwind(|| {
211        if graph.is_null() || buffer.is_null() {
212            return AMEM_ERR_NULL_PTR;
213        }
214        let graph = unsafe { &*(graph as *const MemoryGraph) };
215        match graph.get_node(node_id) {
216            Some(node) => {
217                let content_bytes = node.content.as_bytes();
218                if content_bytes.len() + 1 > buffer_size as usize {
219                    return AMEM_ERR_OVERFLOW;
220                }
221                unsafe {
222                    std::ptr::copy_nonoverlapping(
223                        content_bytes.as_ptr(),
224                        buffer as *mut u8,
225                        content_bytes.len(),
226                    );
227                    *buffer.add(content_bytes.len()) = 0; // null terminator
228                }
229                content_bytes.len() as i32
230            }
231            None => AMEM_ERR_NOT_FOUND,
232        }
233    })
234    .unwrap_or(AMEM_ERR_INVALID)
235}
236
237/// Get a node's confidence. Returns -1.0 if not found.
238///
239/// # Safety
240///
241/// The caller must ensure that all pointer arguments are valid and non-null
242/// (unless the function explicitly handles null pointers), and that any
243/// pointed-to data is valid for the expected type and lifetime.
244#[no_mangle]
245pub unsafe extern "C" fn amem_graph_get_confidence(
246    graph: *mut std::ffi::c_void,
247    node_id: u64,
248) -> f32 {
249    std::panic::catch_unwind(|| {
250        if graph.is_null() {
251            return -1.0;
252        }
253        let graph = unsafe { &*(graph as *const MemoryGraph) };
254        graph
255            .get_node(node_id)
256            .map(|n| n.confidence)
257            .unwrap_or(-1.0)
258    })
259    .unwrap_or(-1.0)
260}
261
262/// Get a node's event type. Returns -1 if not found.
263///
264/// # Safety
265///
266/// The caller must ensure that all pointer arguments are valid and non-null
267/// (unless the function explicitly handles null pointers), and that any
268/// pointed-to data is valid for the expected type and lifetime.
269#[no_mangle]
270pub unsafe extern "C" fn amem_graph_get_event_type(
271    graph: *mut std::ffi::c_void,
272    node_id: u64,
273) -> i32 {
274    std::panic::catch_unwind(|| {
275        if graph.is_null() {
276            return -1;
277        }
278        let graph = unsafe { &*(graph as *const MemoryGraph) };
279        graph
280            .get_node(node_id)
281            .map(|n| n.event_type as i32)
282            .unwrap_or(-1)
283    })
284    .unwrap_or(-1)
285}
286
287/// Add an edge. Returns AMEM_OK or error code.
288///
289/// # Safety
290///
291/// The caller must ensure that all pointer arguments are valid and non-null
292/// (unless the function explicitly handles null pointers), and that any
293/// pointed-to data is valid for the expected type and lifetime.
294#[no_mangle]
295pub unsafe extern "C" fn amem_graph_add_edge(
296    graph: *mut std::ffi::c_void,
297    source_id: u64,
298    target_id: u64,
299    edge_type: u8,
300    weight: f32,
301) -> i32 {
302    std::panic::catch_unwind(|| {
303        if graph.is_null() {
304            return AMEM_ERR_NULL_PTR;
305        }
306        let graph = unsafe { &mut *(graph as *mut MemoryGraph) };
307        let et = match EdgeType::from_u8(edge_type) {
308            Some(et) => et,
309            None => return AMEM_ERR_INVALID,
310        };
311        let edge = Edge::new(source_id, target_id, et, weight);
312        match graph.add_edge(edge) {
313            Ok(()) => AMEM_OK,
314            Err(_) => AMEM_ERR_INVALID,
315        }
316    })
317    .unwrap_or(AMEM_ERR_INVALID)
318}
319
320/// Get edges from a node. Returns edge count or error.
321///
322/// # Safety
323///
324/// The caller must ensure that all pointer arguments are valid and non-null
325/// (unless the function explicitly handles null pointers), and that any
326/// pointed-to data is valid for the expected type and lifetime.
327#[no_mangle]
328pub unsafe extern "C" fn amem_graph_get_edges(
329    graph: *mut std::ffi::c_void,
330    source_id: u64,
331    target_ids: *mut u64,
332    edge_types: *mut u8,
333    weights: *mut f32,
334    max_edges: u32,
335) -> i32 {
336    std::panic::catch_unwind(|| {
337        if graph.is_null() || target_ids.is_null() || edge_types.is_null() || weights.is_null() {
338            return AMEM_ERR_NULL_PTR;
339        }
340        let graph = unsafe { &*(graph as *const MemoryGraph) };
341        let edges = graph.edges_from(source_id);
342        let count = edges.len().min(max_edges as usize);
343        for (i, edge) in edges.iter().take(count).enumerate() {
344            unsafe {
345                *target_ids.add(i) = edge.target_id;
346                *edge_types.add(i) = edge.edge_type as u8;
347                *weights.add(i) = edge.weight;
348            }
349        }
350        count as i32
351    })
352    .unwrap_or(AMEM_ERR_INVALID)
353}
354
355/// Traverse from a start node. Returns visited count.
356///
357/// # Safety
358///
359/// The caller must ensure that all pointer arguments are valid and non-null
360/// (unless the function explicitly handles null pointers), and that any
361/// pointed-to data is valid for the expected type and lifetime.
362#[no_mangle]
363pub unsafe extern "C" fn amem_graph_traverse(
364    graph: *mut std::ffi::c_void,
365    start_id: u64,
366    edge_types_ptr: *const u8,
367    edge_type_count: u32,
368    direction: u8,
369    max_depth: u32,
370    visited_ids: *mut u64,
371    max_results: u32,
372) -> i32 {
373    std::panic::catch_unwind(|| {
374        if graph.is_null() || edge_types_ptr.is_null() || visited_ids.is_null() {
375            return AMEM_ERR_NULL_PTR;
376        }
377        let graph_ref = unsafe { &*(graph as *const MemoryGraph) };
378
379        let edge_types: Vec<EdgeType> = (0..edge_type_count)
380            .filter_map(|i| {
381                let val = unsafe { *edge_types_ptr.add(i as usize) };
382                EdgeType::from_u8(val)
383            })
384            .collect();
385
386        let dir = match direction {
387            0 => crate::graph::TraversalDirection::Forward,
388            1 => crate::graph::TraversalDirection::Backward,
389            _ => crate::graph::TraversalDirection::Both,
390        };
391
392        let query_engine = crate::engine::QueryEngine::new();
393        let params = crate::engine::TraversalParams {
394            start_id,
395            edge_types,
396            direction: dir,
397            max_depth,
398            max_results: max_results as usize,
399            min_confidence: 0.0,
400        };
401
402        match query_engine.traverse(graph_ref, params) {
403            Ok(result) => {
404                let count = result.visited.len().min(max_results as usize);
405                for (i, &id) in result.visited.iter().take(count).enumerate() {
406                    unsafe {
407                        *visited_ids.add(i) = id;
408                    }
409                }
410                count as i32
411            }
412            Err(_) => AMEM_ERR_NOT_FOUND,
413        }
414    })
415    .unwrap_or(AMEM_ERR_INVALID)
416}
417
418/// Resolve: follow SUPERSEDES chain. Returns final node ID or error.
419///
420/// # Safety
421///
422/// The caller must ensure that all pointer arguments are valid and non-null
423/// (unless the function explicitly handles null pointers), and that any
424/// pointed-to data is valid for the expected type and lifetime.
425#[no_mangle]
426pub unsafe extern "C" fn amem_graph_resolve(graph: *mut std::ffi::c_void, node_id: u64) -> i64 {
427    std::panic::catch_unwind(|| {
428        if graph.is_null() {
429            return -1i64;
430        }
431        let graph_ref = unsafe { &*(graph as *const MemoryGraph) };
432        let query_engine = crate::engine::QueryEngine::new();
433        match query_engine.resolve(graph_ref, node_id) {
434            Ok(node) => node.id as i64,
435            Err(_) => -1,
436        }
437    })
438    .unwrap_or(-1)
439}
440
441/// Record a correction. Returns new node ID or error.
442///
443/// # Safety
444///
445/// The caller must ensure that all pointer arguments are valid and non-null
446/// (unless the function explicitly handles null pointers), and that any
447/// pointed-to data is valid for the expected type and lifetime.
448#[no_mangle]
449pub unsafe extern "C" fn amem_graph_correct(
450    graph: *mut std::ffi::c_void,
451    old_node_id: u64,
452    new_content: *const c_char,
453    session_id: u32,
454) -> i64 {
455    std::panic::catch_unwind(|| {
456        if graph.is_null() || new_content.is_null() {
457            return -1i64;
458        }
459        let graph_mut = unsafe { &mut *(graph as *mut MemoryGraph) };
460        let content_str = unsafe { CStr::from_ptr(new_content) };
461        let content_str = match content_str.to_str() {
462            Ok(s) => s,
463            Err(_) => return -1,
464        };
465        let write_engine = crate::engine::WriteEngine::new(graph_mut.dimension());
466        match write_engine.correct(graph_mut, old_node_id, content_str, session_id) {
467            Ok(id) => id as i64,
468            Err(_) => -1,
469        }
470    })
471    .unwrap_or(-1)
472}
473
474/// Touch a node (update access tracking).
475///
476/// # Safety
477///
478/// The caller must ensure that all pointer arguments are valid and non-null
479/// (unless the function explicitly handles null pointers), and that any
480/// pointed-to data is valid for the expected type and lifetime.
481#[no_mangle]
482pub unsafe extern "C" fn amem_graph_touch(graph: *mut std::ffi::c_void, node_id: u64) -> i32 {
483    std::panic::catch_unwind(|| {
484        if graph.is_null() {
485            return AMEM_ERR_NULL_PTR;
486        }
487        let graph_mut = unsafe { &mut *(graph as *mut MemoryGraph) };
488        let write_engine = crate::engine::WriteEngine::new(graph_mut.dimension());
489        match write_engine.touch(graph_mut, node_id) {
490            Ok(()) => AMEM_OK,
491            Err(_) => AMEM_ERR_NOT_FOUND,
492        }
493    })
494    .unwrap_or(AMEM_ERR_INVALID)
495}