nautilus_core/ffi/
parsing.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Helper functions that convert common C types (primarily UTF-8 encoded `char *` pointers) into
17//! the Rust data structures used throughout NautilusTrader.
18//!
19//! The conversions are opinionated:
20//!
21//! * JSON is used as the interchange format for complex structures.
22//! * `ustr::Ustr` is preferred over `String` where possible for its performance benefits.
23//!
24//! All functions are `#[must_use]` and, unless otherwise noted, **assume** that the input pointer
25//! is non-null and points to a valid, *null-terminated* UTF-8 string.
26
27use std::{
28    collections::HashMap,
29    ffi::{CStr, CString, c_char},
30};
31
32use serde_json::Value;
33use ustr::Ustr;
34
35use crate::{
36    ffi::{abort_on_panic, string::cstr_as_str},
37    parsing::{min_increment_precision_from_str, precision_from_str},
38};
39
40/// Convert a C bytes pointer into an owned `Vec<String>`.
41///
42/// # Safety
43///
44/// Assumes `ptr` is a valid C string pointer.
45///
46/// # Panics
47///
48/// Panics if `ptr` is null, contains invalid UTF-8/JSON, or the JSON value
49/// is not an array of strings.
50#[must_use]
51pub unsafe fn bytes_to_string_vec(ptr: *const c_char) -> Vec<String> {
52    assert!(!ptr.is_null(), "`ptr` was NULL");
53
54    // SAFETY: Caller guarantees ptr is valid per function contract
55    let c_str = unsafe { CStr::from_ptr(ptr) };
56    let bytes = c_str.to_bytes();
57
58    let json_string = std::str::from_utf8(bytes).expect("C string contains invalid UTF-8");
59    let value: serde_json::Value =
60        serde_json::from_str(json_string).expect("C string contains invalid JSON");
61
62    let arr = value
63        .as_array()
64        .expect("C string JSON must be an array of strings");
65
66    arr.iter()
67        .map(|value| {
68            value
69                .as_str()
70                .expect("C string JSON array must contain only strings")
71                .to_owned()
72        })
73        .collect()
74}
75
76/// Convert a slice of `String` into a C string pointer (JSON encoded).
77///
78/// # Panics
79///
80/// Panics if JSON serialization fails or if the generated string contains interior null bytes.
81#[must_use]
82pub fn string_vec_to_bytes(strings: &[String]) -> *const c_char {
83    let json_string = serde_json::to_string(strings).expect("Failed to serialize strings to JSON");
84    let c_string = CString::new(json_string).expect("JSON string contains interior null bytes");
85
86    c_string.into_raw()
87}
88
89/// Convert a C bytes pointer into an owned `Option<HashMap<String, Value>>`.
90///
91/// # Safety
92///
93/// Assumes `ptr` is a valid C string pointer.
94///
95/// # Panics
96///
97/// Panics if `ptr` is not null but contains invalid UTF-8 or JSON.
98#[must_use]
99pub unsafe fn optional_bytes_to_json(ptr: *const c_char) -> Option<HashMap<String, Value>> {
100    if ptr.is_null() {
101        None
102    } else {
103        // SAFETY: Caller guarantees ptr is valid per function contract
104        let c_str = unsafe { CStr::from_ptr(ptr) };
105        let bytes = c_str.to_bytes();
106
107        let json_string = std::str::from_utf8(bytes).expect("C string contains invalid UTF-8");
108        let result = serde_json::from_str(json_string).expect("C string contains invalid JSON");
109
110        Some(result)
111    }
112}
113
114/// Convert a C bytes pointer into an owned `Option<HashMap<Ustr, Ustr>>`.
115///
116/// # Safety
117///
118/// Assumes `ptr` is a valid C string pointer.
119///
120/// # Panics
121///
122/// Panics if `ptr` is not null but contains invalid UTF-8 or JSON.
123#[must_use]
124pub unsafe fn optional_bytes_to_str_map(ptr: *const c_char) -> Option<HashMap<Ustr, Ustr>> {
125    if ptr.is_null() {
126        None
127    } else {
128        // SAFETY: Caller guarantees ptr is valid per function contract
129        let c_str = unsafe { CStr::from_ptr(ptr) };
130        let bytes = c_str.to_bytes();
131
132        let json_string = std::str::from_utf8(bytes).expect("C string contains invalid UTF-8");
133        let result = serde_json::from_str(json_string).expect("C string contains invalid JSON");
134
135        Some(result)
136    }
137}
138
139/// Convert a C bytes pointer into an owned `Option<Vec<String>>`.
140///
141/// # Safety
142///
143/// Assumes `ptr` is a valid C string pointer.
144///
145/// # Panics
146///
147/// Panics if `ptr` is not null but contains invalid UTF-8 or JSON.
148#[must_use]
149pub unsafe fn optional_bytes_to_str_vec(ptr: *const c_char) -> Option<Vec<String>> {
150    if ptr.is_null() {
151        None
152    } else {
153        // SAFETY: Caller guarantees ptr is valid per function contract
154        let c_str = unsafe { CStr::from_ptr(ptr) };
155        let bytes = c_str.to_bytes();
156
157        let json_string = std::str::from_utf8(bytes).expect("C string contains invalid UTF-8");
158        let result = serde_json::from_str(json_string).expect("C string contains invalid JSON");
159
160        Some(result)
161    }
162}
163
164/// Return the decimal precision inferred from the given C string.
165///
166/// # Safety
167///
168/// Assumes `ptr` is a valid C string pointer.
169///
170/// # Panics
171///
172/// Panics if `ptr` is null.
173#[unsafe(no_mangle)]
174pub unsafe extern "C" fn precision_from_cstr(ptr: *const c_char) -> u8 {
175    abort_on_panic(|| {
176        assert!(!ptr.is_null(), "`ptr` was NULL");
177        // SAFETY: Caller guarantees ptr is valid per function contract
178        let s = unsafe { cstr_as_str(ptr) };
179        precision_from_str(s)
180    })
181}
182
183/// Return the minimum price increment decimal precision inferred from the given C string.
184///
185/// # Safety
186///
187/// Assumes `ptr` is a valid C string pointer.
188///
189/// # Panics
190///
191/// Panics if `ptr` is null.
192#[unsafe(no_mangle)]
193pub unsafe extern "C" fn min_increment_precision_from_cstr(ptr: *const c_char) -> u8 {
194    abort_on_panic(|| {
195        assert!(!ptr.is_null(), "`ptr` was NULL");
196        // SAFETY: Caller guarantees ptr is valid per function contract
197        let s = unsafe { cstr_as_str(ptr) };
198        min_increment_precision_from_str(s)
199    })
200}
201
202/// Return a `bool` value from the given `u8`.
203#[must_use]
204pub const fn u8_as_bool(value: u8) -> bool {
205    value != 0
206}
207
208#[cfg(test)]
209mod tests {
210    use std::ffi::CString;
211
212    use rstest::rstest;
213
214    use super::*;
215
216    #[rstest]
217    fn test_optional_bytes_to_json_null() {
218        let ptr = std::ptr::null();
219        let result = unsafe { optional_bytes_to_json(ptr) };
220        assert_eq!(result, None);
221    }
222
223    #[rstest]
224    fn test_optional_bytes_to_json_empty() {
225        let json_str = CString::new("{}").unwrap();
226        let ptr = json_str.as_ptr().cast::<c_char>();
227        let result = unsafe { optional_bytes_to_json(ptr) };
228        assert_eq!(result, Some(HashMap::new()));
229    }
230
231    #[rstest]
232    fn test_string_vec_to_bytes_valid() {
233        let strings = vec!["value1", "value2", "value3"]
234            .into_iter()
235            .map(String::from)
236            .collect::<Vec<String>>();
237
238        let ptr = string_vec_to_bytes(&strings);
239
240        let result = unsafe { bytes_to_string_vec(ptr) };
241        assert_eq!(result, strings);
242    }
243
244    #[rstest]
245    fn test_string_vec_to_bytes_empty() {
246        let strings = Vec::new();
247        let ptr = string_vec_to_bytes(&strings);
248
249        let result = unsafe { bytes_to_string_vec(ptr) };
250        assert_eq!(result, strings);
251    }
252
253    #[rstest]
254    fn test_bytes_to_string_vec_valid() {
255        let json_str = CString::new(r#"["value1", "value2", "value3"]"#).unwrap();
256        let ptr = json_str.as_ptr().cast::<c_char>();
257        let result = unsafe { bytes_to_string_vec(ptr) };
258
259        let expected_vec = vec!["value1", "value2", "value3"]
260            .into_iter()
261            .map(String::from)
262            .collect::<Vec<String>>();
263
264        assert_eq!(result, expected_vec);
265    }
266
267    #[rstest]
268    #[should_panic(expected = "array must contain only strings")]
269    fn test_bytes_to_string_vec_invalid() {
270        let json_str = CString::new(r#"["value1", 42, "value3"]"#).unwrap();
271        let ptr = json_str.as_ptr().cast::<c_char>();
272        let _ = unsafe { bytes_to_string_vec(ptr) };
273    }
274
275    #[rstest]
276    fn test_optional_bytes_to_json_valid() {
277        let json_str = CString::new(r#"{"key1": "value1", "key2": 2}"#).unwrap();
278        let ptr = json_str.as_ptr().cast::<c_char>();
279        let result = unsafe { optional_bytes_to_json(ptr) };
280        let mut expected_map = HashMap::new();
281        expected_map.insert("key1".to_owned(), Value::String("value1".to_owned()));
282        expected_map.insert(
283            "key2".to_owned(),
284            Value::Number(serde_json::Number::from(2)),
285        );
286        assert_eq!(result, Some(expected_map));
287    }
288
289    #[rstest]
290    #[should_panic(expected = "C string contains invalid JSON")]
291    fn test_optional_bytes_to_json_invalid() {
292        let json_str = CString::new(r#"{"key1": "value1", "key2": }"#).unwrap();
293        let ptr = json_str.as_ptr().cast::<c_char>();
294        let _result = unsafe { optional_bytes_to_json(ptr) };
295    }
296
297    #[rstest]
298    #[case("1e8", 0)]
299    #[case("123", 0)]
300    #[case("123.45", 2)]
301    #[case("123.456789", 6)]
302    #[case("1.23456789e-2", 2)]
303    #[case("1.23456789e-12", 12)]
304    fn test_precision_from_cstr(#[case] input: &str, #[case] expected: u8) {
305        let c_str = CString::new(input).unwrap();
306        assert_eq!(unsafe { precision_from_cstr(c_str.as_ptr()) }, expected);
307    }
308}