diff --git a/source/ports/rs_port/src/lib.rs b/source/ports/rs_port/src/lib.rs index 74e612934a..32ed098265 100644 --- a/source/ports/rs_port/src/lib.rs +++ b/source/ports/rs_port/src/lib.rs @@ -77,6 +77,21 @@ pub(crate) use macros::private_macros::*; /// ``` pub mod load; +/// Contains MetaCall serialization/deserialization wrappers. +/// Provides safe Rust APIs for `metacall_serialize`, `metacall_deserialize`, +/// and `metacall_allocator_*` functions. Usage example: ... +/// ``` +/// // Create Allocator +/// let allocator = metacall::serial::MetaCallAllocator::new().unwrap(); +/// +/// // Serialize (None = use default serial format) +/// // let json = metacall::serial::serialize_to_string(raw_value, &allocator, None).unwrap(); +/// +/// // Deserialize +/// let value = metacall::serial::deserialize_from_string(r#"{"key": 42}"#, None).unwrap(); +/// ``` +pub mod serial; + mod types; #[doc(hidden)] diff --git a/source/ports/rs_port/src/serial.rs b/source/ports/rs_port/src/serial.rs new file mode 100644 index 0000000000..f2f5d9bb60 --- /dev/null +++ b/source/ports/rs_port/src/serial.rs @@ -0,0 +1,232 @@ +use crate::bindings::{ + metacall_allocator_create, metacall_allocator_destroy, metacall_allocator_free, + metacall_allocator_id, metacall_deserialize, metacall_serial, metacall_serialize, +}; +use std::ffi::{c_void, CStr, CString}; +use std::fmt; + +// --------------------------------------------------------------------------- +// Error type +// --------------------------------------------------------------------------- + +/// Errors that can occur during MetaCall serialization operations. +#[derive(Debug, Clone)] +pub enum MetaCallSerialError { + /// Failed to create the allocator. + AllocatorCreationFailed, + /// Failed to serialize the value. + SerializationFailed, + /// Failed to deserialize the string. + DeserializationFailed, + /// The serialized output contained invalid UTF-8. + InvalidUtf8, + /// The input string contained a null byte and could not be + /// converted to a C string. + NulInString, +} + +impl fmt::Display for MetaCallSerialError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::AllocatorCreationFailed => write!(f, "Failed to create MetaCall allocator"), + Self::SerializationFailed => write!(f, "MetaCall serialization returned null"), + Self::DeserializationFailed => write!(f, "MetaCall deserialization returned null"), + Self::InvalidUtf8 => write!(f, "Serialized output is not valid UTF-8"), + Self::NulInString => write!(f, "Input string contains an interior null byte"), + } + } +} + +impl std::error::Error for MetaCallSerialError {} + +// --------------------------------------------------------------------------- +// Allocator context (mirrors the C struct exactly) +// --------------------------------------------------------------------------- + +#[repr(C)] +struct MetaCallAllocatorStd { + malloc: unsafe extern "C" fn(usize) -> *mut c_void, + realloc: unsafe extern "C" fn(*mut c_void, usize) -> *mut c_void, + free: unsafe extern "C" fn(*mut c_void), +} + +extern "C" { + fn malloc(size: usize) -> *mut c_void; + fn realloc(ptr: *mut c_void, size: usize) -> *mut c_void; + fn free(ptr: *mut c_void); +} + +// --------------------------------------------------------------------------- +// MetaCallAllocator — RAII wrapper +// --------------------------------------------------------------------------- + +/// A reusable allocator for MetaCall serialization operations. +/// +/// Create one allocator and pass it to [`serialize_to_string`] as many +/// times as needed. The underlying C allocator is destroyed automatically +/// when this value is dropped. +/// +/// # Example +/// +/// ```no_run +/// use metacall::serial::{MetaCallAllocator, serialize_to_string}; +/// +/// let allocator = MetaCallAllocator::new().unwrap(); +/// +/// // Use `allocator` for multiple serialize calls… +/// ``` +pub struct MetaCallAllocator { + ptr: *mut c_void, + /// Prevent the context from being dropped while the allocator is alive. + _ctx: Box, +} + +unsafe impl Send for MetaCallAllocator {} + +impl MetaCallAllocator { + /// Create a new standard allocator backed by the C `malloc`/`realloc`/`free`. + pub fn new() -> Result { + let ctx = Box::new(MetaCallAllocatorStd { + malloc, + realloc, + free, + }); + let ptr = unsafe { + metacall_allocator_create( + metacall_allocator_id::METACALL_ALLOCATOR_STD, + &*ctx as *const MetaCallAllocatorStd as *mut c_void, + ) + }; + if ptr.is_null() { + return Err(MetaCallSerialError::AllocatorCreationFailed); + } + Ok(Self { ptr, _ctx: ctx }) + } + + /// Returns the raw allocator pointer for use in FFI calls. + pub fn as_ptr(&self) -> *mut c_void { + self.ptr + } +} + +impl Drop for MetaCallAllocator { + fn drop(&mut self) { + if !self.ptr.is_null() { + unsafe { metacall_allocator_destroy(self.ptr) }; + } + } +} + +// --------------------------------------------------------------------------- +// serialize_to_string +// --------------------------------------------------------------------------- + +/// Serialize a MetaCall value into a string. +/// +/// The caller must provide a reusable [`MetaCallAllocator`]. The serialized +/// buffer is copied into a Rust `String` and then freed; the allocator itself +/// remains valid for future calls. +/// +/// # Arguments +/// +/// * `value` — A valid MetaCall value pointer (e.g. from `metacallfv_s` or +/// `MetaCallValue::into_metacall_raw`). +/// * `allocator` — A reusable allocator created with [`MetaCallAllocator::new`]. +/// * `serial` — The serialization format to use (e.g. `"rapid_json"`). +/// Pass `None` to use the default format configured at MetaCall initialization. +/// +/// # Safety +/// +/// `value` must be a valid, non-null MetaCall value pointer. +/// +/// # Example +/// +/// ```no_run +/// use metacall::serial::{MetaCallAllocator, serialize_to_string}; +/// +/// let allocator = MetaCallAllocator::new().unwrap(); +/// // Assuming `ret` is a raw value pointer returned by a MetaCall function call: +/// // let json = serialize_to_string(ret, &allocator, None).unwrap(); +/// ``` +pub fn serialize_to_string( + value: *mut c_void, + allocator: &MetaCallAllocator, + serial: Option<&str>, +) -> Result { + let c_serial = serial.map(|s| CString::new(s).unwrap()); + let serial_ptr = match &c_serial { + Some(s) => s.as_ptr(), + None => unsafe { metacall_serial() }, + }; + + let mut size: usize = 0; + let buf = unsafe { metacall_serialize(serial_ptr, value, &mut size, allocator.as_ptr()) }; + if buf.is_null() { + return Err(MetaCallSerialError::SerializationFailed); + } + + // The buffer was allocated via malloc through our allocator, copy it into + // a Rust String and then free the C buffer. + let result = unsafe { + CStr::from_ptr(buf) + .to_str() + .map(|s| s.to_owned()) + .map_err(|_| MetaCallSerialError::InvalidUtf8) + }; + + // Free the serialized buffer through the allocator. + unsafe { metacall_allocator_free(allocator.as_ptr(), buf as *mut c_void) }; + + result +} + +// --------------------------------------------------------------------------- +// deserialize_from_string +// --------------------------------------------------------------------------- + +/// Deserialize a string into a MetaCall value. +/// +/// Returns a raw value pointer. **The caller must call +/// `metacall_value_destroy` on the returned pointer when done.** +/// +/// # Arguments +/// +/// * `json` — A valid serialized string to be deserialized. +/// * `serial` — The serialization format to use (e.g. `"rapid_json"`). +/// Pass `None` to use the default format configured at MetaCall initialization. +/// +/// # Example +/// +/// ```no_run +/// use metacall::serial::deserialize_from_string; +/// use metacall::bindings::metacall_value_destroy; +/// +/// let value = deserialize_from_string(r#"{"key": "value"}"#, None).unwrap(); +/// // Use `value` as an argument to metacallfv_s, etc. +/// // ... +/// unsafe { metacall_value_destroy(value) }; +/// ``` +pub fn deserialize_from_string( + json: &str, + serial: Option<&str>, +) -> Result<*mut c_void, MetaCallSerialError> { + let c_serial = serial.map(|s| CString::new(s).unwrap()); + let serial_ptr = match &c_serial { + Some(s) => s.as_ptr(), + None => unsafe { metacall_serial() }, + }; + + let c_json = CString::new(json).map_err(|_| MetaCallSerialError::NulInString)?; + let value = unsafe { + metacall_deserialize( + serial_ptr, + c_json.as_ptr(), + c_json.as_bytes_with_nul().len(), + std::ptr::null_mut(), + ) + }; + if value.is_null() { + return Err(MetaCallSerialError::DeserializationFailed); + } + Ok(value) +} diff --git a/source/ports/rs_port/tests/serial_test.rs b/source/ports/rs_port/tests/serial_test.rs new file mode 100644 index 0000000000..cd3c191ac3 --- /dev/null +++ b/source/ports/rs_port/tests/serial_test.rs @@ -0,0 +1,175 @@ +use metacall::{ + bindings::{ + metacall_value_count, metacall_value_create_int, metacall_value_create_string, + metacall_value_destroy, metacall_value_id, metacall_value_to_array, metacall_value_to_int, + metacall_value_to_map, metacall_value_to_string, + }, + initialize, is_initialized, + serial::{ + deserialize_from_string, serialize_to_string, MetaCallAllocator, MetaCallSerialError, + }, +}; +use std::ffi::CStr; +use std::slice; + +#[test] +fn serial() { + let _d = initialize().unwrap(); + + assert!(is_initialized()); + + test_allocator_creation(); + test_serialize_int(); + test_serialize_string(); + test_deserialize_object(); + test_deserialize_array(); + test_serialize_deserialize_roundtrip(); + test_deserialize_invalid_json(); + test_deserialize_with_explicit_serial(); +} + +fn test_allocator_creation() { + let allocator = MetaCallAllocator::new(); + assert!(allocator.is_ok(), "Allocator creation should succeed"); +} + +fn test_serialize_int() { + let allocator = MetaCallAllocator::new().unwrap(); + let value = unsafe { metacall_value_create_int(42) }; + + let json = serialize_to_string(value, &allocator, None).unwrap(); + assert_eq!(json, "42"); + + unsafe { metacall_value_destroy(value) }; +} + +fn test_serialize_string() { + let allocator = MetaCallAllocator::new().unwrap(); + let s = "hello world"; + let value = unsafe { metacall_value_create_string(s.as_ptr() as *const i8, s.len()) }; + + let json = serialize_to_string(value, &allocator, None).unwrap(); + assert_eq!(json, "\"hello world\""); + + unsafe { metacall_value_destroy(value) }; +} + +fn test_deserialize_object() { + let value = deserialize_from_string(r#"{"key":"value","num":42}"#, None).unwrap(); + assert!(!value.is_null()); + + unsafe { + // Verify type is Map + let type_id = metacall_value_id(value); + assert_eq!(type_id, metacall_value_id::METACALL_MAP); + + // Verify entry count + let count = metacall_value_count(value); + assert_eq!(count, 2, "Map should have 2 entries"); + + // Verify each key-value pair + let pairs = slice::from_raw_parts(metacall_value_to_map(value), count); + + let mut found_key = false; + let mut found_num = false; + + for &pair_ptr in pairs { + let pair = slice::from_raw_parts(metacall_value_to_array(pair_ptr), 2); + let key_str = CStr::from_ptr(metacall_value_to_string(pair[0])) + .to_str() + .unwrap(); + + match key_str { + "key" => { + let val = CStr::from_ptr(metacall_value_to_string(pair[1])) + .to_str() + .unwrap(); + assert_eq!(val, "value"); + found_key = true; + } + "num" => { + let val = metacall_value_to_int(pair[1]); + assert_eq!(val, 42); + found_num = true; + } + other => panic!("Unexpected key: {}", other), + } + } + + assert!(found_key, "Should have found 'key' entry"); + assert!(found_num, "Should have found 'num' entry"); + + metacall_value_destroy(value); + } +} + +fn test_deserialize_array() { + let value = deserialize_from_string(r#"[1, 2, 3]"#, None).unwrap(); + assert!(!value.is_null()); + + unsafe { + // Verify type is Array + let type_id = metacall_value_id(value); + assert_eq!(type_id, metacall_value_id::METACALL_ARRAY); + + // Verify element count + let count = metacall_value_count(value); + assert_eq!(count, 3, "Array should have 3 elements"); + + // Verify each element value + let elements = slice::from_raw_parts(metacall_value_to_array(value), count); + assert_eq!(metacall_value_to_int(elements[0]), 1); + assert_eq!(metacall_value_to_int(elements[1]), 2); + assert_eq!(metacall_value_to_int(elements[2]), 3); + + metacall_value_destroy(value); + } +} + +fn test_serialize_deserialize_roundtrip() { + let allocator = MetaCallAllocator::new().unwrap(); + + let original_json = r#"{"name":"metacall","version":1}"#; + let value = deserialize_from_string(original_json, None).unwrap(); + + // Serialize back and verify content is preserved + let result_json = serialize_to_string(value, &allocator, None).unwrap(); + assert!(result_json.contains("\"name\":\"metacall\"")); + assert!(result_json.contains("\"version\":1")); + + unsafe { metacall_value_destroy(value) }; +} + +fn test_deserialize_invalid_json() { + let result = deserialize_from_string("not valid json {{{", None); + assert!( + matches!(result, Err(MetaCallSerialError::DeserializationFailed)), + "Invalid JSON should return DeserializationFailed" + ); +} + +fn test_deserialize_with_explicit_serial() { + let value = deserialize_from_string(r#"{"key":"value"}"#, Some("rapid_json")).unwrap(); + assert!(!value.is_null()); + + unsafe { + // Verify type, count, and content + assert_eq!(metacall_value_id(value), metacall_value_id::METACALL_MAP); + assert_eq!(metacall_value_count(value), 1); + + let pairs = slice::from_raw_parts(metacall_value_to_map(value), 1); + let pair = slice::from_raw_parts(metacall_value_to_array(pairs[0]), 2); + + let key = CStr::from_ptr(metacall_value_to_string(pair[0])) + .to_str() + .unwrap(); + let val = CStr::from_ptr(metacall_value_to_string(pair[1])) + .to_str() + .unwrap(); + + assert_eq!(key, "key"); + assert_eq!(val, "value"); + + metacall_value_destroy(value); + } +}