nautilus_core/ffi/
parsing.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 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, or is invalid JSON.
49#[must_use]
50pub unsafe fn bytes_to_string_vec(ptr: *const c_char) -> Vec<String> {
51    assert!(!ptr.is_null(), "`ptr` was NULL");
52
53    let c_str = unsafe { CStr::from_ptr(ptr) };
54    let bytes = c_str.to_bytes();
55
56    let json_string = std::str::from_utf8(bytes).expect("C string contains invalid UTF-8");
57    let value: serde_json::Value =
58        serde_json::from_str(json_string).expect("C string contains invalid JSON");
59
60    match value {
61        serde_json::Value::Array(arr) => arr
62            .into_iter()
63            .filter_map(|value| match value {
64                serde_json::Value::String(string_value) => Some(string_value),
65                _ => None,
66            })
67            .collect(),
68        _ => Vec::new(),
69    }
70}
71
72/// Convert a slice of `String` into a C string pointer (JSON encoded).
73///
74/// # Panics
75///
76/// Panics if JSON serialization fails or if the generated string contains interior null bytes.
77#[must_use]
78pub fn string_vec_to_bytes(strings: &[String]) -> *const c_char {
79    let json_string = serde_json::to_string(strings).expect("Failed to serialize strings to JSON");
80    let c_string = CString::new(json_string).expect("JSON string contains interior null bytes");
81
82    c_string.into_raw()
83}
84
85/// Convert a C bytes pointer into an owned `Option<HashMap<String, Value>>`.
86///
87/// # Safety
88///
89/// Assumes `ptr` is a valid C string pointer.
90///
91/// # Panics
92///
93/// Panics if `ptr` is not null but contains invalid UTF-8 or JSON.
94#[must_use]
95pub unsafe fn optional_bytes_to_json(ptr: *const c_char) -> Option<HashMap<String, Value>> {
96    if ptr.is_null() {
97        None
98    } else {
99        let c_str = unsafe { CStr::from_ptr(ptr) };
100        let bytes = c_str.to_bytes();
101
102        let json_string = std::str::from_utf8(bytes).expect("C string contains invalid UTF-8");
103        let result = serde_json::from_str(json_string).expect("C string contains invalid JSON");
104
105        Some(result)
106    }
107}
108
109/// Convert a C bytes pointer into an owned `Option<HashMap<Ustr, Ustr>>`.
110///
111/// # Safety
112///
113/// Assumes `ptr` is a valid C string pointer.
114///
115/// # Panics
116///
117/// Panics if `ptr` is not null but contains invalid UTF-8 or JSON.
118#[must_use]
119pub unsafe fn optional_bytes_to_str_map(ptr: *const c_char) -> Option<HashMap<Ustr, Ustr>> {
120    if ptr.is_null() {
121        None
122    } else {
123        let c_str = unsafe { CStr::from_ptr(ptr) };
124        let bytes = c_str.to_bytes();
125
126        let json_string = std::str::from_utf8(bytes).expect("C string contains invalid UTF-8");
127        let result = serde_json::from_str(json_string).expect("C string contains invalid JSON");
128
129        Some(result)
130    }
131}
132
133/// Convert a C bytes pointer into an owned `Option<Vec<String>>`.
134///
135/// # Safety
136///
137/// Assumes `ptr` is a valid C string pointer.
138///
139/// # Panics
140///
141/// Panics if `ptr` is not null but contains invalid UTF-8 or JSON.
142#[must_use]
143pub unsafe fn optional_bytes_to_str_vec(ptr: *const c_char) -> Option<Vec<String>> {
144    if ptr.is_null() {
145        None
146    } else {
147        let c_str = unsafe { CStr::from_ptr(ptr) };
148        let bytes = c_str.to_bytes();
149
150        let json_string = std::str::from_utf8(bytes).expect("C string contains invalid UTF-8");
151        let result = serde_json::from_str(json_string).expect("C string contains invalid JSON");
152
153        Some(result)
154    }
155}
156
157/// Return the decimal precision inferred from the given C string.
158///
159/// # Safety
160///
161/// Assumes `ptr` is a valid C string pointer.
162///
163/// # Panics
164///
165/// Panics if `ptr` is null.
166#[unsafe(no_mangle)]
167pub unsafe extern "C" fn precision_from_cstr(ptr: *const c_char) -> u8 {
168    abort_on_panic(|| {
169        assert!(!ptr.is_null(), "`ptr` was NULL");
170        let s = unsafe { cstr_as_str(ptr) };
171        precision_from_str(s)
172    })
173}
174
175/// Return the minimum price increment decimal precision inferred from the given C string.
176///
177/// # Safety
178///
179/// Assumes `ptr` is a valid C string pointer.
180///
181/// # Panics
182///
183/// Panics if `ptr` is null.
184#[unsafe(no_mangle)]
185pub unsafe extern "C" fn min_increment_precision_from_cstr(ptr: *const c_char) -> u8 {
186    abort_on_panic(|| {
187        assert!(!ptr.is_null(), "`ptr` was NULL");
188        let s = unsafe { cstr_as_str(ptr) };
189        min_increment_precision_from_str(s)
190    })
191}
192
193/// Return a `bool` value from the given `u8`.
194#[must_use]
195pub const fn u8_as_bool(value: u8) -> bool {
196    value != 0
197}
198
199////////////////////////////////////////////////////////////////////////////////
200// Tests
201////////////////////////////////////////////////////////////////////////////////
202#[cfg(test)]
203mod tests {
204    use std::ffi::CString;
205
206    use rstest::rstest;
207
208    use super::*;
209
210    #[rstest]
211    fn test_optional_bytes_to_json_null() {
212        let ptr = std::ptr::null();
213        let result = unsafe { optional_bytes_to_json(ptr) };
214        assert_eq!(result, None);
215    }
216
217    #[rstest]
218    fn test_optional_bytes_to_json_empty() {
219        let json_str = CString::new("{}").unwrap();
220        let ptr = json_str.as_ptr().cast::<c_char>();
221        let result = unsafe { optional_bytes_to_json(ptr) };
222        assert_eq!(result, Some(HashMap::new()));
223    }
224
225    #[rstest]
226    fn test_string_vec_to_bytes_valid() {
227        let strings = vec!["value1", "value2", "value3"]
228            .into_iter()
229            .map(String::from)
230            .collect::<Vec<String>>();
231
232        let ptr = string_vec_to_bytes(&strings);
233
234        let result = unsafe { bytes_to_string_vec(ptr) };
235        assert_eq!(result, strings);
236    }
237
238    #[rstest]
239    fn test_string_vec_to_bytes_empty() {
240        let strings = Vec::new();
241        let ptr = string_vec_to_bytes(&strings);
242
243        let result = unsafe { bytes_to_string_vec(ptr) };
244        assert_eq!(result, strings);
245    }
246
247    #[rstest]
248    fn test_bytes_to_string_vec_valid() {
249        let json_str = CString::new(r#"["value1", "value2", "value3"]"#).unwrap();
250        let ptr = json_str.as_ptr().cast::<c_char>();
251        let result = unsafe { bytes_to_string_vec(ptr) };
252
253        let expected_vec = vec!["value1", "value2", "value3"]
254            .into_iter()
255            .map(String::from)
256            .collect::<Vec<String>>();
257
258        assert_eq!(result, expected_vec);
259    }
260
261    #[rstest]
262    fn test_bytes_to_string_vec_invalid() {
263        let json_str = CString::new(r#"["value1", 42, "value3"]"#).unwrap();
264        let ptr = json_str.as_ptr().cast::<c_char>();
265        let result = unsafe { bytes_to_string_vec(ptr) };
266
267        let expected_vec = vec!["value1", "value3"]
268            .into_iter()
269            .map(String::from)
270            .collect::<Vec<String>>();
271
272        assert_eq!(result, expected_vec);
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}