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 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 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}