styx_ffi/
lib.rs

1#![doc = include_str!("../README.md")]
2//! C bindings for the Styx configuration language parser.
3//!
4//! This crate provides a C-compatible API for parsing Styx documents.
5
6use std::ffi::{CStr, CString};
7use std::os::raw::c_char;
8use std::ptr;
9
10use styx_tree::{BuildError, Document, Object, Payload, Sequence, Value};
11
12/// Opaque handle to a parsed Styx document.
13pub struct StyxDocument {
14    inner: Document,
15}
16
17/// Opaque handle to a Styx value.
18pub struct StyxValue {
19    #[allow(dead_code)]
20    inner: *const Value,
21}
22
23/// Opaque handle to a Styx object.
24pub struct StyxObject {
25    #[allow(dead_code)]
26    inner: *const Object,
27}
28
29/// Opaque handle to a Styx sequence.
30pub struct StyxSequence {
31    #[allow(dead_code)]
32    inner: *const Sequence,
33}
34
35/// Result of a parse operation.
36#[repr(C)]
37pub struct StyxParseResult {
38    /// The parsed document (null if error).
39    pub document: *mut StyxDocument,
40    /// Error message (null if success). Must be freed with `styx_free_string`.
41    pub error: *mut c_char,
42}
43
44/// Type of a Styx value's payload.
45#[repr(C)]
46pub enum StyxPayloadKind {
47    /// No payload (unit or tag-only).
48    None,
49    /// Scalar text.
50    Scalar,
51    /// Sequence of values.
52    Sequence,
53    /// Object (key-value pairs).
54    Object,
55}
56
57// =============================================================================
58// Parsing
59// =============================================================================
60
61/// Parse a Styx document from a UTF-8 string.
62///
63/// # Safety
64/// - `source` must be a valid null-terminated UTF-8 string.
65/// - The returned `StyxParseResult` must have its fields freed appropriately:
66///   - If `document` is non-null, free it with `styx_free_document`.
67///   - If `error` is non-null, free it with `styx_free_string`.
68#[unsafe(no_mangle)]
69pub unsafe extern "C" fn styx_parse(source: *const c_char) -> StyxParseResult {
70    if source.is_null() {
71        let error = CString::new("source is null").unwrap();
72        return StyxParseResult {
73            document: ptr::null_mut(),
74            error: error.into_raw(),
75        };
76    }
77
78    let source = match unsafe { CStr::from_ptr(source) }.to_str() {
79        Ok(s) => s,
80        Err(_) => {
81            let error = CString::new("source is not valid UTF-8").unwrap();
82            return StyxParseResult {
83                document: ptr::null_mut(),
84                error: error.into_raw(),
85            };
86        }
87    };
88
89    match Document::parse(source) {
90        Ok(doc) => {
91            let boxed = Box::new(StyxDocument { inner: doc });
92            StyxParseResult {
93                document: Box::into_raw(boxed),
94                error: ptr::null_mut(),
95            }
96        }
97        Err(e) => {
98            let error_msg = format_error(&e);
99            let error =
100                CString::new(error_msg).unwrap_or_else(|_| CString::new("unknown error").unwrap());
101            StyxParseResult {
102                document: ptr::null_mut(),
103                error: error.into_raw(),
104            }
105        }
106    }
107}
108
109fn format_error(e: &BuildError) -> String {
110    match e {
111        BuildError::UnexpectedEvent(msg) => format!("unexpected event: {}", msg),
112        BuildError::UnclosedStructure => "unclosed structure".to_string(),
113        BuildError::EmptyDocument => "empty document".to_string(),
114        BuildError::Parse(kind, span) => {
115            format!("parse error at {}-{}: {}", span.start, span.end, kind)
116        }
117    }
118}
119
120/// Free a parsed document.
121///
122/// # Safety
123/// - `doc` must be a valid pointer returned by `styx_parse`, or null.
124/// - After calling this function, `doc` is invalid and must not be used.
125#[unsafe(no_mangle)]
126pub unsafe extern "C" fn styx_free_document(doc: *mut StyxDocument) {
127    if !doc.is_null() {
128        drop(unsafe { Box::from_raw(doc) });
129    }
130}
131
132/// Free a string returned by the library.
133///
134/// # Safety
135/// - `s` must be a valid pointer returned by a styx_* function, or null.
136#[unsafe(no_mangle)]
137pub unsafe extern "C" fn styx_free_string(s: *mut c_char) {
138    if !s.is_null() {
139        drop(unsafe { CString::from_raw(s) });
140    }
141}
142
143// =============================================================================
144// Document access
145// =============================================================================
146
147/// Get the root object of a document.
148///
149/// # Safety
150/// - `doc` must be a valid pointer to a `StyxDocument`.
151/// - The returned pointer is valid as long as `doc` is valid.
152#[unsafe(no_mangle)]
153pub unsafe extern "C" fn styx_document_root(doc: *const StyxDocument) -> *const StyxObject {
154    if doc.is_null() {
155        return ptr::null();
156    }
157    let doc = unsafe { &*doc };
158    &doc.inner.root as *const Object as *const StyxObject
159}
160
161/// Get a value by path from a document.
162///
163/// # Safety
164/// - `doc` must be a valid pointer to a `StyxDocument`.
165/// - `path` must be a valid null-terminated UTF-8 string.
166/// - The returned pointer is valid as long as `doc` is valid.
167#[unsafe(no_mangle)]
168pub unsafe extern "C" fn styx_document_get(
169    doc: *const StyxDocument,
170    path: *const c_char,
171) -> *const StyxValue {
172    if doc.is_null() || path.is_null() {
173        return ptr::null();
174    }
175    let doc = unsafe { &*doc };
176    let path = match unsafe { CStr::from_ptr(path) }.to_str() {
177        Ok(s) => s,
178        Err(_) => return ptr::null(),
179    };
180    match doc.inner.get(path) {
181        Some(value) => value as *const Value as *const StyxValue,
182        None => ptr::null(),
183    }
184}
185
186// =============================================================================
187// Value access
188// =============================================================================
189
190/// Get the payload kind of a value.
191///
192/// # Safety
193/// - `value` must be a valid pointer to a `StyxValue`.
194#[unsafe(no_mangle)]
195pub unsafe extern "C" fn styx_value_payload_kind(value: *const StyxValue) -> StyxPayloadKind {
196    if value.is_null() {
197        return StyxPayloadKind::None;
198    }
199    let value = unsafe { &*(value as *const Value) };
200    match &value.payload {
201        None => StyxPayloadKind::None,
202        Some(Payload::Scalar(_)) => StyxPayloadKind::Scalar,
203        Some(Payload::Sequence(_)) => StyxPayloadKind::Sequence,
204        Some(Payload::Object(_)) => StyxPayloadKind::Object,
205    }
206}
207
208/// Check if a value is unit (no tag, no payload).
209///
210/// # Safety
211/// - `value` must be a valid pointer to a `StyxValue`.
212#[unsafe(no_mangle)]
213pub unsafe extern "C" fn styx_value_is_unit(value: *const StyxValue) -> bool {
214    if value.is_null() {
215        return false;
216    }
217    let value = unsafe { &*(value as *const Value) };
218    value.is_unit()
219}
220
221/// Get the tag name of a value (null if no tag).
222///
223/// # Safety
224/// - `value` must be a valid pointer to a `StyxValue`.
225/// - The returned string must be freed with `styx_free_string`.
226#[unsafe(no_mangle)]
227pub unsafe extern "C" fn styx_value_tag(value: *const StyxValue) -> *mut c_char {
228    if value.is_null() {
229        return ptr::null_mut();
230    }
231    let value = unsafe { &*(value as *const Value) };
232    match value.tag_name() {
233        Some(name) => CString::new(name)
234            .map(|s| s.into_raw())
235            .unwrap_or(ptr::null_mut()),
236        None => ptr::null_mut(),
237    }
238}
239
240/// Get the scalar text of a value (null if not a scalar).
241///
242/// # Safety
243/// - `value` must be a valid pointer to a `StyxValue`.
244/// - The returned string must be freed with `styx_free_string`.
245#[unsafe(no_mangle)]
246pub unsafe extern "C" fn styx_value_scalar(value: *const StyxValue) -> *mut c_char {
247    if value.is_null() {
248        return ptr::null_mut();
249    }
250    let value = unsafe { &*(value as *const Value) };
251    match value.scalar_text() {
252        Some(text) => CString::new(text)
253            .map(|s| s.into_raw())
254            .unwrap_or(ptr::null_mut()),
255        None => ptr::null_mut(),
256    }
257}
258
259/// Get the object payload of a value (null if not an object).
260///
261/// # Safety
262/// - `value` must be a valid pointer to a `StyxValue`.
263/// - The returned pointer is valid as long as the parent document is valid.
264#[unsafe(no_mangle)]
265pub unsafe extern "C" fn styx_value_as_object(value: *const StyxValue) -> *const StyxObject {
266    if value.is_null() {
267        return ptr::null();
268    }
269    let value = unsafe { &*(value as *const Value) };
270    match &value.payload {
271        Some(Payload::Object(obj)) => obj as *const Object as *const StyxObject,
272        _ => ptr::null(),
273    }
274}
275
276/// Get the sequence payload of a value (null if not a sequence).
277///
278/// # Safety
279/// - `value` must be a valid pointer to a `StyxValue`.
280/// - The returned pointer is valid as long as the parent document is valid.
281#[unsafe(no_mangle)]
282pub unsafe extern "C" fn styx_value_as_sequence(value: *const StyxValue) -> *const StyxSequence {
283    if value.is_null() {
284        return ptr::null();
285    }
286    let value = unsafe { &*(value as *const Value) };
287    match &value.payload {
288        Some(Payload::Sequence(seq)) => seq as *const Sequence as *const StyxSequence,
289        _ => ptr::null(),
290    }
291}
292
293/// Get a nested value by path.
294///
295/// # Safety
296/// - `value` must be a valid pointer to a `StyxValue`.
297/// - `path` must be a valid null-terminated UTF-8 string.
298/// - The returned pointer is valid as long as the parent document is valid.
299#[unsafe(no_mangle)]
300pub unsafe extern "C" fn styx_value_get(
301    value: *const StyxValue,
302    path: *const c_char,
303) -> *const StyxValue {
304    if value.is_null() || path.is_null() {
305        return ptr::null();
306    }
307    let value = unsafe { &*(value as *const Value) };
308    let path = match unsafe { CStr::from_ptr(path) }.to_str() {
309        Ok(s) => s,
310        Err(_) => return ptr::null(),
311    };
312    match value.get(path) {
313        Some(v) => v as *const Value as *const StyxValue,
314        None => ptr::null(),
315    }
316}
317
318// =============================================================================
319// Object access
320// =============================================================================
321
322/// Get the number of entries in an object.
323///
324/// # Safety
325/// - `obj` must be a valid pointer to a `StyxObject`.
326#[unsafe(no_mangle)]
327pub unsafe extern "C" fn styx_object_len(obj: *const StyxObject) -> usize {
328    if obj.is_null() {
329        return 0;
330    }
331    let obj = unsafe { &*(obj as *const Object) };
332    obj.len()
333}
334
335/// Get a value from an object by key.
336///
337/// # Safety
338/// - `obj` must be a valid pointer to a `StyxObject`.
339/// - `key` must be a valid null-terminated UTF-8 string.
340/// - The returned pointer is valid as long as the parent document is valid.
341#[unsafe(no_mangle)]
342pub unsafe extern "C" fn styx_object_get(
343    obj: *const StyxObject,
344    key: *const c_char,
345) -> *const StyxValue {
346    if obj.is_null() || key.is_null() {
347        return ptr::null();
348    }
349    let obj = unsafe { &*(obj as *const Object) };
350    let key = match unsafe { CStr::from_ptr(key) }.to_str() {
351        Ok(s) => s,
352        Err(_) => return ptr::null(),
353    };
354    match obj.get(key) {
355        Some(value) => value as *const Value as *const StyxValue,
356        None => ptr::null(),
357    }
358}
359
360/// Get the key at a given index in an object.
361///
362/// # Safety
363/// - `obj` must be a valid pointer to a `StyxObject`.
364/// - `index` must be less than `styx_object_len(obj)`.
365/// - The returned pointer is valid as long as the parent document is valid.
366#[unsafe(no_mangle)]
367pub unsafe extern "C" fn styx_object_key_at(
368    obj: *const StyxObject,
369    index: usize,
370) -> *const StyxValue {
371    if obj.is_null() {
372        return ptr::null();
373    }
374    let obj = unsafe { &*(obj as *const Object) };
375    match obj.entries.get(index) {
376        Some(entry) => &entry.key as *const Value as *const StyxValue,
377        None => ptr::null(),
378    }
379}
380
381/// Get the value at a given index in an object.
382///
383/// # Safety
384/// - `obj` must be a valid pointer to a `StyxObject`.
385/// - `index` must be less than `styx_object_len(obj)`.
386/// - The returned pointer is valid as long as the parent document is valid.
387#[unsafe(no_mangle)]
388pub unsafe extern "C" fn styx_object_value_at(
389    obj: *const StyxObject,
390    index: usize,
391) -> *const StyxValue {
392    if obj.is_null() {
393        return ptr::null();
394    }
395    let obj = unsafe { &*(obj as *const Object) };
396    match obj.entries.get(index) {
397        Some(entry) => &entry.value as *const Value as *const StyxValue,
398        None => ptr::null(),
399    }
400}
401
402// =============================================================================
403// Sequence access
404// =============================================================================
405
406/// Get the number of items in a sequence.
407///
408/// # Safety
409/// - `seq` must be a valid pointer to a `StyxSequence`.
410#[unsafe(no_mangle)]
411pub unsafe extern "C" fn styx_sequence_len(seq: *const StyxSequence) -> usize {
412    if seq.is_null() {
413        return 0;
414    }
415    let seq = unsafe { &*(seq as *const Sequence) };
416    seq.len()
417}
418
419/// Get an item from a sequence by index.
420///
421/// # Safety
422/// - `seq` must be a valid pointer to a `StyxSequence`.
423/// - `index` must be less than `styx_sequence_len(seq)`.
424/// - The returned pointer is valid as long as the parent document is valid.
425#[unsafe(no_mangle)]
426pub unsafe extern "C" fn styx_sequence_get(
427    seq: *const StyxSequence,
428    index: usize,
429) -> *const StyxValue {
430    if seq.is_null() {
431        return ptr::null();
432    }
433    let seq = unsafe { &*(seq as *const Sequence) };
434    match seq.get(index) {
435        Some(value) => value as *const Value as *const StyxValue,
436        None => ptr::null(),
437    }
438}