Skip to main content

heeranjid_ffi/
lib.rs

1use std::cell::RefCell;
2use std::ffi::{CStr, c_char, c_int};
3
4use heeranjid::{HeerId, RanjId};
5
6// ── Error handling ──────────────────────────────────────────────────────
7
8thread_local! {
9    static LAST_ERROR: RefCell<String> = const { RefCell::new(String::new()) };
10}
11
12fn set_last_error(msg: impl Into<String>) {
13    LAST_ERROR.with(|e| *e.borrow_mut() = msg.into());
14}
15
16/// Returns 0 if no error is stored.
17/// Otherwise copies the last error message into `buf` (up to `buf_len` bytes
18/// including the NUL terminator) and returns the number of bytes written
19/// (excluding the NUL). If the buffer is too small the message is truncated.
20///
21/// # Safety
22///
23/// `buf` must point to a writable buffer of at least `buf_len` bytes, or be null.
24#[unsafe(no_mangle)]
25pub unsafe extern "C" fn heer_last_error(buf: *mut c_char, buf_len: c_int) -> c_int {
26    LAST_ERROR.with(|e| {
27        let msg = e.borrow();
28        if msg.is_empty() {
29            return 0;
30        }
31        if buf.is_null() || buf_len <= 0 {
32            return msg.len() as c_int;
33        }
34        let max = (buf_len as usize) - 1; // reserve space for NUL
35        let copy_len = msg.len().min(max);
36        unsafe {
37            std::ptr::copy_nonoverlapping(msg.as_ptr(), buf as *mut u8, copy_len);
38            *buf.add(copy_len) = 0; // NUL terminator
39        }
40        copy_len as c_int
41    })
42}
43
44// ── HeerId types and functions ──────────────────────────────────────────
45
46/// HeerId is represented as a plain i64 across the FFI boundary.
47pub type HeerIdT = i64;
48
49/// Decode a HeerId into its component parts.
50/// Returns 0 on success, -1 on error (check `heer_last_error`).
51///
52/// # Safety
53///
54/// Output pointers must be valid or null. Null pointers are safely skipped.
55#[unsafe(no_mangle)]
56pub unsafe extern "C" fn heer_id_decode(
57    id: HeerIdT,
58    timestamp_ms: *mut u64,
59    node_id: *mut u16,
60    sequence: *mut u16,
61) -> c_int {
62    match HeerId::from_i64(id) {
63        Ok(hid) => {
64            let parts = hid.into_parts();
65            unsafe {
66                if !timestamp_ms.is_null() {
67                    *timestamp_ms = parts.timestamp_ms;
68                }
69                if !node_id.is_null() {
70                    *node_id = parts.node_id;
71                }
72                if !sequence.is_null() {
73                    *sequence = parts.sequence;
74                }
75            }
76            0
77        }
78        Err(e) => {
79            set_last_error(e.to_string());
80            -1
81        }
82    }
83}
84
85/// Convert a HeerId to its string representation.
86/// Writes into `buf` (up to `buf_len` bytes including NUL).
87/// Returns the number of bytes written (excluding NUL) on success, -1 on error.
88///
89/// # Safety
90///
91/// `buf` must point to a writable buffer of at least `buf_len` bytes, or be null.
92#[unsafe(no_mangle)]
93pub unsafe extern "C" fn heer_id_to_string(id: HeerIdT, buf: *mut c_char, buf_len: c_int) -> c_int {
94    match HeerId::from_i64(id) {
95        Ok(hid) => {
96            let s = hid.to_string();
97            if buf.is_null() || buf_len <= 0 {
98                set_last_error("null buffer");
99                return -1;
100            }
101            let max = (buf_len as usize) - 1;
102            if s.len() > max {
103                set_last_error("buffer too small");
104                return -1;
105            }
106            unsafe {
107                std::ptr::copy_nonoverlapping(s.as_ptr(), buf as *mut u8, s.len());
108                *buf.add(s.len()) = 0;
109            }
110            s.len() as c_int
111        }
112        Err(e) => {
113            set_last_error(e.to_string());
114            -1
115        }
116    }
117}
118
119/// Parse a HeerId from a NUL-terminated string.
120/// On success writes the result into `*out` and returns 0.
121/// On error returns -1.
122///
123/// # Safety
124///
125/// `s` must be a valid NUL-terminated C string. `out` must be a valid pointer.
126#[unsafe(no_mangle)]
127pub unsafe extern "C" fn heer_id_from_string(s: *const c_char, out: *mut HeerIdT) -> c_int {
128    if s.is_null() || out.is_null() {
129        set_last_error("null pointer");
130        return -1;
131    }
132    let cstr = unsafe { CStr::from_ptr(s) };
133    let rust_str = match cstr.to_str() {
134        Ok(v) => v,
135        Err(e) => {
136            set_last_error(e.to_string());
137            return -1;
138        }
139    };
140    match rust_str.parse::<HeerId>() {
141        Ok(hid) => {
142            unsafe { *out = hid.as_i64() };
143            0
144        }
145        Err(e) => {
146            set_last_error(e.to_string());
147            -1
148        }
149    }
150}
151
152// ── RanjId types and functions ──────────────────────────────────────────
153
154/// RanjId is represented as 16 raw UUID bytes across the FFI boundary.
155#[repr(C)]
156pub struct RanjIdT {
157    pub bytes: [u8; 16],
158}
159
160/// Decode a RanjId into its component parts.
161/// Returns 0 on success, -1 on error.
162///
163/// # Safety
164///
165/// `id` must point to a valid `RanjIdT`. Output pointers must be valid or null.
166#[unsafe(no_mangle)]
167pub unsafe extern "C" fn ranj_id_decode(
168    id: *const RanjIdT,
169    timestamp_us: *mut u64,
170    node_id: *mut u16,
171    sequence: *mut u16,
172) -> c_int {
173    if id.is_null() {
174        set_last_error("null pointer");
175        return -1;
176    }
177    let bytes = unsafe { &(*id).bytes };
178    let uuid = uuid::Uuid::from_bytes(*bytes);
179    match RanjId::from_uuid(uuid) {
180        Ok(rid) => {
181            unsafe {
182                if !timestamp_us.is_null() {
183                    // RanjId timestamp can be up to 90 bits, but for practical use
184                    // it fits in u64 for the foreseeable future.
185                    *timestamp_us = rid.timestamp_micros() as u64;
186                }
187                if !node_id.is_null() {
188                    *node_id = rid.node_id();
189                }
190                if !sequence.is_null() {
191                    *sequence = rid.sequence();
192                }
193            }
194            0
195        }
196        Err(e) => {
197            set_last_error(e.to_string());
198            -1
199        }
200    }
201}
202
203/// Convert a RanjId to its UUID string representation.
204/// Writes into `buf` (up to `buf_len` bytes including NUL).
205/// Returns bytes written (excluding NUL) on success, -1 on error.
206///
207/// # Safety
208///
209/// `id` must point to a valid `RanjIdT`. `buf` must be writable for `buf_len` bytes.
210#[unsafe(no_mangle)]
211pub unsafe extern "C" fn ranj_id_to_string(
212    id: *const RanjIdT,
213    buf: *mut c_char,
214    buf_len: c_int,
215) -> c_int {
216    if id.is_null() {
217        set_last_error("null pointer");
218        return -1;
219    }
220    let bytes = unsafe { &(*id).bytes };
221    let uuid = uuid::Uuid::from_bytes(*bytes);
222    match RanjId::from_uuid(uuid) {
223        Ok(rid) => {
224            let s = rid.to_string();
225            if buf.is_null() || buf_len <= 0 {
226                set_last_error("null buffer");
227                return -1;
228            }
229            let max = (buf_len as usize) - 1;
230            if s.len() > max {
231                set_last_error("buffer too small");
232                return -1;
233            }
234            unsafe {
235                std::ptr::copy_nonoverlapping(s.as_ptr(), buf as *mut u8, s.len());
236                *buf.add(s.len()) = 0;
237            }
238            s.len() as c_int
239        }
240        Err(e) => {
241            set_last_error(e.to_string());
242            -1
243        }
244    }
245}
246
247/// Parse a RanjId from a NUL-terminated UUID string.
248/// On success writes the result into `*out` and returns 0.
249/// On error returns -1.
250///
251/// # Safety
252///
253/// `s` must be a valid NUL-terminated C string. `out` must be a valid pointer.
254#[unsafe(no_mangle)]
255pub unsafe extern "C" fn ranj_id_from_string(s: *const c_char, out: *mut RanjIdT) -> c_int {
256    if s.is_null() || out.is_null() {
257        set_last_error("null pointer");
258        return -1;
259    }
260    let cstr = unsafe { CStr::from_ptr(s) };
261    let rust_str = match cstr.to_str() {
262        Ok(v) => v,
263        Err(e) => {
264            set_last_error(e.to_string());
265            return -1;
266        }
267    };
268    match rust_str.parse::<RanjId>() {
269        Ok(rid) => {
270            unsafe {
271                (*out).bytes = *rid.as_uuid().as_bytes();
272            }
273            0
274        }
275        Err(e) => {
276            set_last_error(e.to_string());
277            -1
278        }
279    }
280}