Skip to main content

harmonia_memory/
lib.rs

1use std::collections::HashMap;
2use std::ffi::{CStr, CString};
3use std::fs;
4use std::os::raw::c_char;
5use std::path::{Path, PathBuf};
6use std::sync::{OnceLock, RwLock};
7
8const VERSION: &[u8] = b"harmonia-memory/0.2.0\0";
9
10#[derive(Default)]
11struct MemoryState {
12    file_path: Option<PathBuf>,
13    entries: HashMap<String, String>,
14}
15
16static MEMORY: OnceLock<RwLock<MemoryState>> = OnceLock::new();
17static LAST_ERROR: OnceLock<RwLock<String>> = OnceLock::new();
18
19fn state() -> &'static RwLock<MemoryState> {
20    MEMORY.get_or_init(|| RwLock::new(MemoryState::default()))
21}
22
23fn last_error() -> &'static RwLock<String> {
24    LAST_ERROR.get_or_init(|| RwLock::new(String::new()))
25}
26
27fn set_error(msg: impl Into<String>) {
28    if let Ok(mut slot) = last_error().write() {
29        *slot = msg.into();
30    }
31}
32
33fn clear_error() {
34    if let Ok(mut slot) = last_error().write() {
35        slot.clear();
36    }
37}
38
39fn cstr_to_string(ptr: *const c_char) -> Result<String, String> {
40    if ptr.is_null() {
41        return Err("null pointer".to_string());
42    }
43    // Safety: caller provides valid null-terminated string.
44    let c = unsafe { CStr::from_ptr(ptr) };
45    Ok(c.to_string_lossy().into_owned())
46}
47
48fn to_c_string(value: String) -> *mut c_char {
49    match CString::new(value) {
50        Ok(c) => c.into_raw(),
51        Err(_) => std::ptr::null_mut(),
52    }
53}
54
55fn encode_entry(k: &str, v: &str) -> String {
56    let key = k.replace('\\', "\\\\").replace('\n', "\\n");
57    let val = v.replace('\\', "\\\\").replace('\n', "\\n");
58    format!("{key}\t{val}\n")
59}
60
61fn decode_line(line: &str) -> Option<(String, String)> {
62    let (k, v) = line.split_once('\t')?;
63    let key = k.replace("\\n", "\n").replace("\\\\", "\\");
64    let val = v.replace("\\n", "\n").replace("\\\\", "\\");
65    Some((key, val))
66}
67
68fn persist_to_disk(path: &Path, map: &HashMap<String, String>) -> Result<(), String> {
69    if let Some(parent) = path.parent() {
70        fs::create_dir_all(parent).map_err(|e| format!("create dir failed: {e}"))?;
71    }
72    let mut body = String::new();
73    for (k, v) in map {
74        body.push_str(&encode_entry(k, v));
75    }
76    let tmp = path.with_extension("tmp");
77    fs::write(&tmp, body).map_err(|e| format!("write tmp failed: {e}"))?;
78    fs::rename(&tmp, path).map_err(|e| format!("rename failed: {e}"))?;
79    Ok(())
80}
81
82#[no_mangle]
83pub extern "C" fn harmonia_memory_version() -> *const c_char {
84    VERSION.as_ptr().cast()
85}
86
87#[no_mangle]
88pub extern "C" fn harmonia_memory_healthcheck() -> i32 {
89    1
90}
91
92#[no_mangle]
93pub extern "C" fn harmonia_memory_init(file_path: *const c_char) -> i32 {
94    let path = match cstr_to_string(file_path) {
95        Ok(v) => v,
96        Err(e) => {
97            set_error(e);
98            return -1;
99        }
100    };
101    let path_buf = PathBuf::from(path);
102    let mut entries = HashMap::new();
103    if path_buf.exists() {
104        match fs::read_to_string(&path_buf) {
105            Ok(body) => {
106                for line in body.lines() {
107                    if let Some((k, v)) = decode_line(line) {
108                        entries.insert(k, v);
109                    }
110                }
111            }
112            Err(e) => {
113                set_error(format!("read memory file failed: {e}"));
114                return -1;
115            }
116        }
117    }
118    match state().write() {
119        Ok(mut st) => {
120            st.file_path = Some(path_buf);
121            st.entries = entries;
122            clear_error();
123            0
124        }
125        Err(_) => {
126            set_error("memory lock poisoned");
127            -1
128        }
129    }
130}
131
132#[no_mangle]
133pub extern "C" fn harmonia_memory_put(key: *const c_char, value: *const c_char) -> i32 {
134    let key = match cstr_to_string(key) {
135        Ok(v) => v,
136        Err(e) => {
137            set_error(e);
138            return -1;
139        }
140    };
141    let value = match cstr_to_string(value) {
142        Ok(v) => v,
143        Err(e) => {
144            set_error(e);
145            return -1;
146        }
147    };
148
149    let mut st = match state().write() {
150        Ok(v) => v,
151        Err(_) => {
152            set_error("memory lock poisoned");
153            return -1;
154        }
155    };
156
157    st.entries.insert(key, value);
158    if let Some(path) = st.file_path.clone() {
159        if let Err(e) = persist_to_disk(&path, &st.entries) {
160            set_error(e);
161            return -1;
162        }
163    }
164    clear_error();
165    0
166}
167
168#[no_mangle]
169pub extern "C" fn harmonia_memory_get(key: *const c_char) -> *mut c_char {
170    let key = match cstr_to_string(key) {
171        Ok(v) => v,
172        Err(e) => {
173            set_error(e);
174            return std::ptr::null_mut();
175        }
176    };
177    let st = match state().read() {
178        Ok(v) => v,
179        Err(_) => {
180            set_error("memory lock poisoned");
181            return std::ptr::null_mut();
182        }
183    };
184    match st.entries.get(&key) {
185        Some(v) => {
186            clear_error();
187            to_c_string(v.clone())
188        }
189        None => {
190            set_error(format!("missing key: {key}"));
191            std::ptr::null_mut()
192        }
193    }
194}
195
196#[no_mangle]
197pub extern "C" fn harmonia_memory_last_error() -> *mut c_char {
198    let msg = last_error()
199        .read()
200        .map(|v| v.clone())
201        .unwrap_or_else(|_| "memory lock poisoned".to_string());
202    to_c_string(msg)
203}
204
205#[no_mangle]
206pub extern "C" fn harmonia_memory_free_string(ptr: *mut c_char) {
207    if ptr.is_null() {
208        return;
209    }
210    // Safety: ptr must come from CString::into_raw in this crate.
211    unsafe { drop(CString::from_raw(ptr)) };
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217    use std::env;
218    use std::process;
219    use std::time::{SystemTime, UNIX_EPOCH};
220
221    fn temp_path(label: &str) -> PathBuf {
222        let ts = SystemTime::now()
223            .duration_since(UNIX_EPOCH)
224            .unwrap_or_default()
225            .as_nanos();
226        env::temp_dir().join(format!("harmonia-memory-{label}-{}-{ts}.db", process::id()))
227    }
228
229    #[test]
230    fn healthcheck_returns_one() {
231        assert_eq!(harmonia_memory_healthcheck(), 1);
232    }
233
234    #[test]
235    fn version_ptr_is_non_null() {
236        assert!(!harmonia_memory_version().is_null());
237    }
238
239    #[test]
240    fn put_and_get_roundtrip() {
241        let path = temp_path("roundtrip");
242        let cpath = CString::new(path.to_string_lossy().to_string()).unwrap();
243        assert_eq!(harmonia_memory_init(cpath.as_ptr()), 0);
244
245        let key = CString::new("dna/core").unwrap();
246        let val = CString::new("(rewrite-count . 3)").unwrap();
247        assert_eq!(harmonia_memory_put(key.as_ptr(), val.as_ptr()), 0);
248
249        let ptr = harmonia_memory_get(key.as_ptr());
250        assert!(!ptr.is_null());
251        let got = unsafe { CStr::from_ptr(ptr) }.to_string_lossy().to_string();
252        harmonia_memory_free_string(ptr);
253        assert_eq!(got, "(rewrite-count . 3)");
254    }
255}