Skip to main content

hashtree_embedded_ffi/
lib.rs

1use hashtree_embedded::{HostDaemonOptions, HostDaemonRuntime};
2use std::ffi::{c_char, c_void, CString};
3use std::path::PathBuf;
4use std::ptr;
5use std::sync::Mutex;
6
7struct EmbeddedDaemonHandle {
8    runtime: HostDaemonRuntime,
9}
10
11static LAST_ERROR: Mutex<Option<CString>> = Mutex::new(None);
12
13fn set_last_error(message: impl Into<String>) {
14    let message = sanitize_c_string(message.into());
15    *LAST_ERROR.lock().expect("last error lock") = Some(message);
16}
17
18fn clear_last_error() {
19    *LAST_ERROR.lock().expect("last error lock") = None;
20}
21
22fn sanitize_c_string(input: String) -> CString {
23    let sanitized = input.replace('\0', " ");
24    CString::new(sanitized).expect("CString conversion should succeed after sanitizing NULs")
25}
26
27unsafe fn string_from_ptr(ptr: *const c_char, label: &str) -> Result<String, String> {
28    if ptr.is_null() {
29        return Err(format!("{label} pointer was null"));
30    }
31    let c_str = unsafe { std::ffi::CStr::from_ptr(ptr) };
32    c_str
33        .to_str()
34        .map(|value| value.to_owned())
35        .map_err(|_| format!("{label} was not valid UTF-8"))
36}
37
38#[unsafe(no_mangle)]
39/// Start an embedded daemon rooted at the supplied filesystem path.
40///
41/// # Safety
42///
43/// `state_root` must point to a valid NUL-terminated C string for the duration
44/// of this call.
45pub unsafe extern "C" fn hashtree_embedded_start(state_root: *const c_char) -> *mut c_void {
46    clear_last_error();
47
48    let state_root = match unsafe { string_from_ptr(state_root, "state_root") } {
49        Ok(value) => value,
50        Err(error) => {
51            set_last_error(error);
52            return ptr::null_mut();
53        }
54    };
55
56    let options = HostDaemonOptions::new(PathBuf::from(state_root));
57    match HostDaemonRuntime::start(options) {
58        Ok(runtime) => Box::into_raw(Box::new(EmbeddedDaemonHandle { runtime })) as *mut c_void,
59        Err(error) => {
60            set_last_error(format!(
61                "failed to start embedded hashtree daemon: {error:#}"
62            ));
63            ptr::null_mut()
64        }
65    }
66}
67
68#[unsafe(no_mangle)]
69/// Stop and release an embedded daemon handle.
70///
71/// # Safety
72///
73/// A non-null `handle` must have been returned by [`hashtree_embedded_start`]
74/// and must not previously have been passed to this function.
75pub unsafe extern "C" fn hashtree_embedded_shutdown(handle: *mut c_void) {
76    if handle.is_null() {
77        return;
78    }
79    let mut handle = unsafe { Box::from_raw(handle.cast::<EmbeddedDaemonHandle>()) };
80    handle.runtime.shutdown();
81}
82
83#[unsafe(no_mangle)]
84/// Reload the configuration of a running embedded daemon.
85///
86/// # Safety
87///
88/// A non-null `handle` must be a live handle returned by
89/// [`hashtree_embedded_start`].
90pub unsafe extern "C" fn hashtree_embedded_reload(handle: *mut c_void) -> bool {
91    clear_last_error();
92    if handle.is_null() {
93        set_last_error("embedded daemon handle was null");
94        return false;
95    }
96    let handle = unsafe { &mut *handle.cast::<EmbeddedDaemonHandle>() };
97    match handle.runtime.reload() {
98        Ok(_) => true,
99        Err(error) => {
100            set_last_error(format!(
101                "failed to reload embedded hashtree daemon: {error:#}"
102            ));
103            false
104        }
105    }
106}
107
108#[unsafe(no_mangle)]
109/// Return the base URL of a running embedded daemon.
110///
111/// # Safety
112///
113/// A non-null `handle` must be a live handle returned by
114/// [`hashtree_embedded_start`]. The returned string must be released with
115/// [`hashtree_embedded_string_free`].
116pub unsafe extern "C" fn hashtree_embedded_get_base_url(handle: *const c_void) -> *mut c_char {
117    clear_last_error();
118    if handle.is_null() {
119        set_last_error("embedded daemon handle was null");
120        return ptr::null_mut();
121    }
122    let handle = unsafe { &*handle.cast::<EmbeddedDaemonHandle>() };
123    sanitize_c_string(handle.runtime.base_url()).into_raw()
124}
125
126#[unsafe(no_mangle)]
127/// Return the daemon's Nostr public key.
128///
129/// # Safety
130///
131/// A non-null `handle` must be a live handle returned by
132/// [`hashtree_embedded_start`]. The returned string must be released with
133/// [`hashtree_embedded_string_free`].
134pub unsafe extern "C" fn hashtree_embedded_get_self_npub(handle: *const c_void) -> *mut c_char {
135    clear_last_error();
136    if handle.is_null() {
137        set_last_error("embedded daemon handle was null");
138        return ptr::null_mut();
139    }
140    let handle = unsafe { &*handle.cast::<EmbeddedDaemonHandle>() };
141    sanitize_c_string(handle.runtime.self_npub().to_owned()).into_raw()
142}
143
144#[unsafe(no_mangle)]
145pub extern "C" fn hashtree_embedded_take_last_error() -> *mut c_char {
146    let mut last_error = LAST_ERROR.lock().expect("last error lock");
147    last_error
148        .take()
149        .map(CString::into_raw)
150        .unwrap_or(ptr::null_mut())
151}
152
153#[unsafe(no_mangle)]
154/// Release a string returned by this library.
155///
156/// # Safety
157///
158/// A non-null `value` must have been returned by a Hashtree embedded FFI string
159/// function and must not previously have been freed.
160pub unsafe extern "C" fn hashtree_embedded_string_free(value: *mut c_char) {
161    if value.is_null() {
162        return;
163    }
164    unsafe {
165        drop(CString::from_raw(value));
166    }
167}