diff --git a/rust/pact_matching_ffi/src/log/ffi.rs b/rust/pact_matching_ffi/src/log/ffi.rs index 39320bd58..4410d3a5c 100644 --- a/rust/pact_matching_ffi/src/log/ffi.rs +++ b/rust/pact_matching_ffi/src/log/ffi.rs @@ -56,7 +56,7 @@ pub unsafe extern "C" fn logger_attach_sink( level_filter: LevelFilter, ) -> c_int { // Get the specifier from the raw C string. - let sink_specifier = unsafe { CStr::from_ptr(sink_specifier) }; + let sink_specifier = CStr::from_ptr(sink_specifier); let sink_specifier = match sink_specifier.to_str() { Ok(sink_specifier) => sink_specifier, Err(_) => return Status::SpecifierNotUtf8 as c_int, diff --git a/rust/pact_matching_ffi/src/models/message.rs b/rust/pact_matching_ffi/src/models/message.rs index 0fa902d3f..b93d747ab 100644 --- a/rust/pact_matching_ffi/src/models/message.rs +++ b/rust/pact_matching_ffi/src/models/message.rs @@ -1,23 +1,34 @@ //! The Pact `Message` type, including associated matching rules and provider states. -use crate::ffi; +/*=============================================================================================== + * # Imports + *---------------------------------------------------------------------------------------------*/ + use crate::models::pact_specification::PactSpecification; use crate::util::*; -use anyhow::Context; +use crate::{as_mut, as_ref, cstr, ffi, safe_str}; +use anyhow::{anyhow, Context}; use libc::{c_char, c_int, c_uint, EXIT_FAILURE, EXIT_SUCCESS}; -use std::collections::HashMap; -use std::ffi::CStr; -use std::ffi::CString; + +/*=============================================================================================== + * # Re-Exports + *---------------------------------------------------------------------------------------------*/ // Necessary to make 'cbindgen' generate an opaque struct on the C side. pub use pact_matching::models::message::Message; -pub use pact_matching::models::provider_states::ProviderState as NonCProviderState; +pub use pact_matching::models::provider_states::ProviderState; + +/*=============================================================================================== + * # FFI Functions + *---------------------------------------------------------------------------------------------*/ /// Get a mutable pointer to a newly-created default message on the heap. #[no_mangle] -pub extern "C" fn message_new() -> *mut Message { +#[allow(clippy::missing_safety_doc)] +pub unsafe extern "C" fn message_new() -> *mut Message { ffi! { name: "message_new", + params: [], op: { Ok(ptr::raw_to(Message::default())) }, @@ -27,50 +38,29 @@ pub extern "C" fn message_new() -> *mut Message { } } -/// Destroy the `Message` being pointed to. -#[no_mangle] -pub extern "C" fn message_delete(message: *mut Message) -> c_int { - ffi! { - name: "message_delete", - params: [message], - op: { - ptr::drop_raw(message); - Ok(EXIT_SUCCESS) - }, - fail: { - EXIT_FAILURE - } - } -} - /// Constructs a `Message` from the JSON string #[no_mangle] -pub extern "C" fn message_from_json( +#[allow(clippy::missing_safety_doc)] +pub unsafe extern "C" fn message_new_from_json( index: c_uint, json_str: *const c_char, spec_version: PactSpecification, ) -> *mut Message { ffi! { - name: "message_from_json", + name: "message_new_from_json", + params: [index, json_str, spec_version], op: { - if json_str.is_null() { - anyhow::bail!("json_str is null"); - } - - let json_str = unsafe { CStr::from_ptr(json_str) }; - let json_str = json_str - .to_str() - .context("Error parsing json_str as UTF-8")?; + let json_str = safe_str!(json_str); let json_value: serde_json::Value = serde_json::from_str(json_str) - .context("Error parsing json_str as JSON")?; + .context("error parsing json_str as JSON")?; let message = Message::from_json( index as usize, &json_value, &spec_version.into()) - .map_err(|e| anyhow::anyhow!("Pact error: {}", e))?; + .map_err(|e| anyhow::anyhow!("{}", e))?; Ok(ptr::raw_to(message)) }, @@ -80,6 +70,23 @@ pub extern "C" fn message_from_json( } } +/// Destroy the `Message` being pointed to. +#[no_mangle] +#[allow(clippy::missing_safety_doc)] +pub unsafe extern "C" fn message_delete(message: *mut Message) -> c_int { + ffi! { + name: "message_delete", + params: [message], + op: { + ptr::drop_raw(message); + Ok(EXIT_SUCCESS) + }, + fail: { + EXIT_FAILURE + } + } +} + /// Get a copy of the description. /// The returned string must be deleted with `string_delete`. /// @@ -93,19 +100,18 @@ pub extern "C" fn message_from_json( /// This function may fail if the Rust string contains embedded /// null ('\0') bytes. #[no_mangle] -pub extern "C" fn message_get_description( +#[allow(clippy::missing_safety_doc)] +#[allow(clippy::or_fun_call)] +pub unsafe extern "C" fn message_get_description( message: *const Message, ) -> *const c_char { ffi! { name: "message_get_description", + params: [message], op: { - if message.is_null() { - anyhow::bail!("message is null"); - } - - let description = unsafe { &(*message).description }; - - Ok(string::into_leaked_cstring(description.clone())?) + let message = as_ref!(message); + let description = string::into_leaked_cstring(&message.description)?; + Ok(description) }, fail: { ptr::null_to::() @@ -121,168 +127,32 @@ pub extern "C" fn message_get_description( /// This function will only reallocate if the new string /// does not fit in the existing buffer. #[no_mangle] -pub extern "C" fn message_set_description( +#[allow(clippy::missing_safety_doc)] +#[allow(clippy::or_fun_call)] +pub unsafe extern "C" fn message_set_description( message: *mut Message, description: *const c_char, -) { +) -> c_int { ffi! { name: "message_set_description", + params: [message, description], op: { - if message.is_null() { - anyhow::bail!("message is null"); - } - - if description.is_null() { - anyhow::bail!("description is null"); - } - - let message = unsafe { &mut (*message) }; - let description = unsafe { CStr::from_ptr(description) }; - let description = description.to_string_lossy(); + let message = as_mut!(message); + let description = safe_str!(description); // Wipe out the previous contents of the string, without - // deallocating. + // deallocating, and set the new description. message.description.clear(); + message.description.push_str(description); - message.description.push_str(&description); - Ok(()) + Ok(EXIT_SUCCESS) }, fail: { + EXIT_FAILURE } } } -/// FFI structure mirroring the internal Rust ProviderState struct. -/// Contains the name of this Provider State, -/// and a list of (key, value) parameters as an array of structures. -/// The number of elements is stored in 'params_length'. -/// -/// This structure should not be mutated. -#[allow(missing_copy_implementations)] -#[repr(C)] -#[derive(Debug)] -pub struct ProviderState { - /// null terminated string containing the name - pub name: *const c_char, - /// pointer to array of key, value pairs - pub params_list: *const ProviderStateParamsKV, - /// number of elements in `params_list` - pub params_length: usize, - /// private, tracks allocated capacity of the underlying Vec - capacity: usize, -} - -/// FFI structure representing a (key, value) pair -/// for the ProviderState parameters. -/// -/// The `value` field is a JSON object, serialized to a string. -/// -/// This structure should not be mutated. -#[allow(missing_copy_implementations)] -#[repr(C)] -#[derive(Debug)] -pub struct ProviderStateParamsKV { - /// null terminated string containing the key - pub key: *const c_char, - /// null terminated JSON string - pub value: *const c_char, -} - -/// Create and leak a ProviderState. Must be passed back to -/// impl_provider_state_delete to clean up memory. -fn into_leaked_provider_state( - provider_state: &NonCProviderState, -) -> Result<*const ProviderState, anyhow::Error> { - let name = &provider_state.name; - let params = &provider_state.params; - let mut list = Vec::with_capacity(params.len()); - - // First check all the strings for embedded null. - // This prevents leaking memory in the case where - // an error occurs after some strings were intentionally - // leaked, but before they can be passed to C. - - if name.find(|c| c == '\0').is_some() { - anyhow::bail!( - "Found embedded null in \ - a provider state name: '{}'", - name - ); - } - - for (k, _v) in params.iter() { - if k.find(|c| c == '\0').is_some() { - anyhow::bail!( - "Found embedded null in \ - a provider state key name: '{}'", - k - ); - } - } - - for (k, v) in params.iter() { - // It is safe to unwrap, since the strings were already - // checked for embedded nulls. - let kv = ProviderStateParamsKV { - key: string::into_leaked_cstring(k.clone()).unwrap(), - value: string::into_leaked_cstring(v.to_string()).unwrap(), - }; - - list.push(kv); - } - - let provider_state_ffi = ProviderState { - // It is safe to unwrap, since the string was already - // checked for embedded nulls. - name: string::into_leaked_cstring(name.clone()).unwrap(), - params_list: list.as_ptr(), - params_length: list.len(), - capacity: list.capacity(), - }; - - std::mem::forget(list); - - let output = Box::new(provider_state_ffi); - - Ok(Box::into_raw(output)) -} - -/// Manually delete a ProviderState. -/// Returns all leaked memory into Rust structures, which will -/// be automatically cleaned up on Drop. -fn impl_provider_state_delete(ptr: *const ProviderState) { - let provider_state = - unsafe { Box::from_raw(ptr as *mut ProviderState) }; - - let _name = - unsafe { CString::from_raw(provider_state.name as *mut c_char) }; - - let list = unsafe { - Vec::from_raw_parts( - provider_state.params_list as *mut ProviderStateParamsKV, - provider_state.params_length, - provider_state.capacity, - ) - }; - - for kv in list { - let _k = unsafe { CString::from_raw(kv.key as *mut c_char) }; - let _v = unsafe { CString::from_raw(kv.value as *mut c_char) }; - } -} - -/// The result of calling message_get_provider_state -#[repr(C)] -#[derive(Debug, Copy, Clone, PartialEq)] -pub enum GetProviderStateResult { - /// Success - Success, - /// The requested index was out of bounds - IndexOutOfBounds, - /// Some other error occured; check error messages - OtherError, -} - /// Get a copy of the provider state at the given index from this message. /// A pointer to the structure will be written to `out_provider_state`, /// only if no errors are encountered. @@ -299,212 +169,28 @@ pub enum GetProviderStateResult { /// This function may fail if the index requested is out of bounds, /// or if any of the Rust strings contain embedded null ('\0') bytes. #[no_mangle] -pub extern "C" fn message_get_provider_state( +#[allow(clippy::missing_safety_doc)] +#[allow(clippy::or_fun_call)] +pub unsafe extern "C" fn message_get_provider_state( message: *const Message, index: usize, - out_provider_state: *mut *const ProviderState, -) -> GetProviderStateResult { - use GetProviderStateResult::*; +) -> *const ProviderState { ffi! { name: "message_get_provider_state", + params: [message, index], op: { - if message.is_null() { - anyhow::bail!("message is null"); - } - - let message = unsafe { &(*message) }; - - match message.provider_states.get(index) { - None => { - Ok(IndexOutOfBounds) - } - Some(provider_state) => { - unsafe { std::ptr::write( - out_provider_state, - into_leaked_provider_state(provider_state)?) }; - - Ok(Success) - } - } - }, - fail: { - OtherError - } - } -} - -/// Delete a ProviderState previously returned by this FFI. -/// -/// It is explicitly allowed to pass a null pointer to this function; -/// in that case the function will do nothing. -#[no_mangle] -pub extern "C" fn provider_state_delete( - provider_state: *const ProviderState, -) { - ffi! { - name: "provider_state_delete", - op: { - if provider_state.is_null() { - return Ok(()); - } - - impl_provider_state_delete(provider_state); - Ok(()) - }, - fail: { - } - } -} - -/// FFI structure representing a list of (key, value) pairs. -/// It is an array with a number of elements equal to `length`. -/// -/// This structure should not be mutated. -#[allow(missing_copy_implementations)] -#[repr(C)] -#[derive(Debug)] -pub struct MetadataList { - /// pointer to array of key, value pairs - pub list: *const MetadataKV, - /// number of elements in `list` - pub length: usize, - /// private, tracks allocated capacity of the underlying Vec - capacity: usize, -} - -/// FFI structure representing a (key, value) pair. -/// -/// This structure should not be mutated. -#[allow(missing_copy_implementations)] -#[repr(C)] -#[derive(Debug)] -pub struct MetadataKV { - /// null terminated string containing the key - pub key: *const c_char, - /// null terminated string containing the value - pub value: *const c_char, -} - -/// Create and leak a MetadataList. Must be passed back to -/// impl_metadata_list_delete to clean up memory. -fn into_leaked_metadata_list( - metadata: &HashMap, -) -> Result<*const MetadataList, anyhow::Error> { - let mut list = Vec::with_capacity(metadata.len()); - - // First check all the strings for embedded null. - // This prevents leaking memory in the case where - // an error occurs after some strings were intentionally - // leaked, but before they can be passed to C. - for (k, v) in metadata.iter() { - if k.find(|c| c == '\0').is_some() - || v.find(|c| c == '\0').is_some() - { - anyhow::bail!( - "Found embedded null in \ - a (key, value) pair: ('{}', '{}')", - k, - v - ); - } - } - - for (k, v) in metadata.iter() { - // It is safe to unwrap, since the strings were already - // checked for embedded nulls. - let kv = MetadataKV { - key: string::into_leaked_cstring(k.clone()).unwrap(), - value: string::into_leaked_cstring(v.clone()).unwrap(), - }; - - list.push(kv); - } - - let metadata_list = MetadataList { - list: list.as_ptr(), - length: list.len(), - capacity: list.capacity(), - }; - - std::mem::forget(list); - - let output = Box::new(metadata_list); - - Ok(Box::into_raw(output)) -} - -/// Manually delete a MetadataList. -/// Returns all leaked memory into Rust structures, which will -/// be automatically cleaned up on Drop. -fn impl_metadata_list_delete(ptr: *const MetadataList) { - let metadata_list = - unsafe { Box::from_raw(ptr as *mut MetadataList) }; - - let list = unsafe { - Vec::from_raw_parts( - metadata_list.list as *mut MetadataKV, - metadata_list.length, - metadata_list.capacity, - ) - }; - - for kv in list { - let _k = unsafe { CString::from_raw(kv.key as *mut c_char) }; - let _v = unsafe { CString::from_raw(kv.value as *mut c_char) }; - } -} - -/// Get a copy of the metadata list from this message. -/// It is in the form of a list of (key, value) pairs, -/// in an unspecified order. -/// The returned structure must be deleted with `metadata_list_delete`. -/// -/// Since it is a copy, the returned structure may safely outlive -/// the `Message`. -/// -/// # Errors -/// -/// On failure, this function will return a NULL pointer. -/// -/// This function may fail if any of the Rust strings contain -/// embedded null ('\0') bytes. -#[no_mangle] -pub extern "C" fn message_get_metadata_list( - message: *const Message, -) -> *const MetadataList { - ffi! { - name: "message_metadata_list", - op: { - if message.is_null() { - anyhow::bail!("message is null"); - } - - let message = unsafe { &(*message) }; - into_leaked_metadata_list(&message.metadata) - }, - fail: { - ptr::null_to::() - } - } -} - -/// Delete a MetadataList previously returned by this FFI. -/// -/// It is explicitly allowed to pass a null pointer to this function; -/// in that case the function will do nothing. -#[no_mangle] -pub extern "C" fn metadata_list_delete(list: *const MetadataList) { - ffi! { - name: "metadata_list_delete", - op: { - if list.is_null() { - return Ok(()); - } - - impl_metadata_list_delete(list); - Ok(()) + let message = as_ref!(message); + // Get a raw pointer directly, rather than boxing it, as its owned by the `Message` + // and will be cleaned up when the `Message` is cleaned up. + let provider_state = message + .provider_states + .get(index) + .ok_or(anyhow!("index is out of bounds"))? + as *const ProviderState; + Ok(provider_state) }, fail: { + ptr::null_to::() } } } @@ -526,31 +212,23 @@ pub extern "C" fn metadata_list_delete(list: *const MetadataList) { /// invalid UTF-8, or if the Rust string contains embedded null ('\0') /// bytes. #[no_mangle] -pub extern "C" fn message_metadata_get( +#[allow(clippy::missing_safety_doc)] +#[allow(clippy::or_fun_call)] +pub unsafe extern "C" fn message_find_metadata( message: *const Message, key: *const c_char, ) -> *const c_char { ffi! { - name: "message_metadata_get", + name: "message_find_metadata", + params: [message, key], op: { - if message.is_null() { - anyhow::bail!("message is null"); - } - - if key.is_null() { - anyhow::bail!("key is null"); - } - - let message = unsafe { &(*message) }; - let key = unsafe { CStr::from_ptr(key) }; - let key = key - .to_str() - .context("Error parsing key as UTF-8")?; + let message = as_ref!(message); + let key = safe_str!(key); match message.metadata.get(key) { None => Ok(ptr::null_to::()), Some(value) => { - Ok(string::into_leaked_cstring(value.clone())?) + Ok(string::into_leaked_cstring(value)?) }, } }, @@ -560,89 +238,90 @@ pub extern "C" fn message_metadata_get( } } -/// Result from an attempt to insert into a HashMap -#[repr(C)] -#[derive(Debug, Copy, Clone, PartialEq)] -pub enum HashMapInsertResult { - /// The value was inserted, and the key was unset - SuccessNew, - /// The value was inserted, and the key was previously set - SuccessOverwrite, - /// An error occured, and the value was not inserted - Error, -} - /// Insert the (`key`, `value`) pair into this Message's /// `metadata` HashMap. /// This function returns an enum indicating the result; -/// see the comments on HashMapInsertResult for details. +/// see the comments on HashMapInsertStatus for details. /// /// # Errors /// /// This function may fail if the provided `key` or `value` strings /// contain invalid UTF-8. #[no_mangle] -pub extern "C" fn message_metadata_insert( +#[allow(clippy::missing_safety_doc)] +#[allow(clippy::or_fun_call)] +pub unsafe extern "C" fn message_insert_metadata( message: *mut Message, key: *const c_char, value: *const c_char, -) -> HashMapInsertResult { - use HashMapInsertResult::*; +) -> c_int { + use HashMapInsertStatus as Status; + ffi! { - name: "message_metadata_insert", + name: "message_insert_metadata", + params: [message, key, value], op: { - if message.is_null() { - anyhow::bail!("message is null"); - } - - if key.is_null() { - anyhow::bail!("key is null"); - } - - if value.is_null() { - anyhow::bail!("value is null"); - } - - let message = unsafe { &mut (*message) }; - let key = unsafe { CStr::from_ptr(key) }; - let key = key - .to_str() - .context("Error parsing key as UTF-8")?; - - let value = unsafe { CStr::from_ptr(value) }; - let value = value - .to_str() - .context("Error parsing value as UTF-8")?; + let message = as_mut!(message); + let key = safe_str!(key); + let value = safe_str!(value); match message.metadata.insert(key.to_string(), value.to_string()) { - None => Ok(SuccessNew), - Some(_) => Ok(SuccessOverwrite), + None => Ok(Status::SuccessNew as c_int), + Some(_) => Ok(Status::SuccessOverwrite as c_int), } }, fail: { - Error + Status::Error as c_int } } } -/// Delete a string previously returned by this FFI. +/* +/// Get a copy of the metadata list from this message. +/// It is in the form of a list of (key, value) pairs, +/// in an unspecified order. +/// The returned structure must be deleted with `metadata_list_delete`. +/// +/// Since it is a copy, the returned structure may safely outlive +/// the `Message`. +/// +/// # Errors +/// +/// On failure, this function will return a NULL pointer. /// -/// It is explicitly allowed to pass a null pointer to this function; -/// in that case the function will do nothing. +/// This function may fail if any of the Rust strings contain +/// embedded null ('\0') bytes. #[no_mangle] -pub extern "C" fn string_delete(string: *mut c_char) { +#[allow(clippy::missing_safety_doc)] +#[allow(clippy::or_fun_call)] +pub unsafe extern "C" fn message_get_metadata_list( + message: *mut Message, +) -> *mut MetadataIterator { ffi! { - name: "string_delete", + name: "message_get_metadata_list", + params: [message], op: { - if string.is_null() { - return Ok(()); - } + let message = as_mut!(message); - let string = unsafe { CString::from_raw(string) }; - std::mem::drop(string); - Ok(()) + todo!() }, fail: { + ptr::null_to::() } } } +*/ + +/*=============================================================================================== + * # Status Types + *---------------------------------------------------------------------------------------------*/ + +/// Result from an attempt to insert into a HashMap +enum HashMapInsertStatus { + /// The value was inserted, and the key was unset + SuccessNew = 0, + /// The value was inserted, and the key was previously set + SuccessOverwrite = -1, + /// An error occured, and the value was not inserted + Error = -2, +} diff --git a/rust/pact_matching_ffi/src/models/metadata.rs b/rust/pact_matching_ffi/src/models/metadata.rs new file mode 100644 index 000000000..46da2e4c9 --- /dev/null +++ b/rust/pact_matching_ffi/src/models/metadata.rs @@ -0,0 +1,129 @@ +//! The `Metadata` type and operations on it. + +/* +use crate::ffi; +use crate::util::string; +use libc::c_char; +use std::collections::HashMap; +use std::ffi::CString; + +/// FFI structure representing a list of (key, value) pairs. +/// It is an array with a number of elements equal to `length`. +/// +/// This structure should not be mutated. +#[allow(missing_copy_implementations)] +#[repr(C)] +#[derive(Debug)] +pub struct MetadataList { + /// pointer to array of key, value pairs + pub list: *const MetadataKV, + /// number of elements in `list` + pub length: usize, + /// private, tracks allocated capacity of the underlying Vec + capacity: usize, +} + +/// FFI structure representing a (key, value) pair. +/// +/// This structure should not be mutated. +#[allow(missing_copy_implementations)] +#[repr(C)] +#[derive(Debug)] +pub struct MetadataKV { + /// null terminated string containing the key + pub key: *const c_char, + /// null terminated string containing the value + pub value: *const c_char, +} + +/// Create and leak a MetadataList. Must be passed back to +/// impl_metadata_list_delete to clean up memory. +fn into_leaked_metadata_list( + metadata: &HashMap, +) -> Result<*const MetadataList, anyhow::Error> { + let mut list = Vec::with_capacity(metadata.len()); + + // First check all the strings for embedded null. + // This prevents leaking memory in the case where + // an error occurs after some strings were intentionally + // leaked, but before they can be passed to C. + for (k, v) in metadata.iter() { + if k.find(|c| c == '\0').is_some() + || v.find(|c| c == '\0').is_some() + { + anyhow::bail!( + "Found embedded null in \ + a (key, value) pair: ('{}', '{}')", + k, + v + ); + } + } + + for (k, v) in metadata.iter() { + // It is safe to unwrap, since the strings were already + // checked for embedded nulls. + let kv = MetadataKV { + key: string::into_leaked_cstring(k.as_ref()).unwrap(), + value: string::into_leaked_cstring(v.as_ref()).unwrap(), + }; + + list.push(kv); + } + + let metadata_list = MetadataList { + list: list.as_ptr(), + length: list.len(), + capacity: list.capacity(), + }; + + std::mem::forget(list); + + let output = Box::new(metadata_list); + + Ok(Box::into_raw(output)) +} + +/// Manually delete a MetadataList. +/// Returns all leaked memory into Rust structures, which will +/// be automatically cleaned up on Drop. +fn impl_metadata_list_delete(ptr: *const MetadataList) { + let metadata_list = + unsafe { Box::from_raw(ptr as *mut MetadataList) }; + + let list = unsafe { + Vec::from_raw_parts( + metadata_list.list as *mut MetadataKV, + metadata_list.length, + metadata_list.capacity, + ) + }; + + for kv in list { + let _k = unsafe { CString::from_raw(kv.key as *mut c_char) }; + let _v = unsafe { CString::from_raw(kv.value as *mut c_char) }; + } +} + +/// Delete a MetadataList previously returned by this FFI. +/// +/// It is explicitly allowed to pass a null pointer to this function; +/// in that case the function will do nothing. +#[no_mangle] +pub extern "C" fn metadata_list_delete(list: *const MetadataList) { + ffi! { + name: "metadata_list_delete", + params: [list], + op: { + if list.is_null() { + return Ok(()); + } + + impl_metadata_list_delete(list); + Ok(()) + }, + fail: { + } + } +} +*/ diff --git a/rust/pact_matching_ffi/src/models/mod.rs b/rust/pact_matching_ffi/src/models/mod.rs index 6f90e659d..887bba4f4 100644 --- a/rust/pact_matching_ffi/src/models/mod.rs +++ b/rust/pact_matching_ffi/src/models/mod.rs @@ -1,4 +1,5 @@ //! Represents messages in `pact_matching`. pub mod message; +pub mod metadata; pub mod pact_specification; diff --git a/rust/pact_matching_ffi/src/util/ffi.rs b/rust/pact_matching_ffi/src/util/ffi.rs index ff9e9a90d..3fb88c7b7 100644 --- a/rust/pact_matching_ffi/src/util/ffi.rs +++ b/rust/pact_matching_ffi/src/util/ffi.rs @@ -9,16 +9,15 @@ #[macro_export] macro_rules! ffi { ( op: $op:block, fail: $fail:block ) => {{ - compile_error!("the ffi macro must include a name"); + compile_error!("the ffi macro must include a name and a list of params"); }}; ( name: $name:literal, op: $op:block, fail: $fail:block ) => {{ - ffi! { - name: $name, - params: [], - op: $op, - fail: $fail - } + compile_error!("the ffi macro must include a list of params"); + }}; + + ( params: [ $( $params:ident ),* ], op: $op:block, fail: $fail:block ) => {{ + compile_error!("the ffi macro must include a name"); }}; ( name: $name:literal, params: [ $( $params:ident ),* ], op: $op:block, fail: $fail:block ) => {{ @@ -28,7 +27,7 @@ macro_rules! ffi { $( log::trace!(target: TARGET, "@param $params = {:?}", $params); - ),* + )* let output = $crate::error::catch_panic(|| $op).unwrap_or($fail); diff --git a/rust/pact_matching_ffi/src/util/ptr.rs b/rust/pact_matching_ffi/src/util/ptr.rs index c03eb2572..b02851271 100644 --- a/rust/pact_matching_ffi/src/util/ptr.rs +++ b/rust/pact_matching_ffi/src/util/ptr.rs @@ -32,3 +32,23 @@ pub(crate) fn null_to() -> *const T { pub(crate) fn null_mut_to() -> *mut T { ptr::null_mut() as *mut T } + +/// Get an immutable reference from a raw pointer +#[macro_export] +macro_rules! as_ref { + ( $name:ident ) => {{ + $name + .as_ref() + .ok_or(anyhow!(concat!(stringify!($name), " is null")))? + }}; +} + +/// Get a mutable reference from a raw pointer +#[macro_export] +macro_rules! as_mut { + ( $name:ident ) => {{ + $name + .as_mut() + .ok_or(anyhow!(concat!(stringify!($name), " is null")))? + }}; +} diff --git a/rust/pact_matching_ffi/src/util/string.rs b/rust/pact_matching_ffi/src/util/string.rs index f65ac72a1..a61fc93e7 100644 --- a/rust/pact_matching_ffi/src/util/string.rs +++ b/rust/pact_matching_ffi/src/util/string.rs @@ -1,5 +1,6 @@ use libc::c_char; use std::ffi::CString; +use std::mem; /// Converts the string into a C-compatible null terminated string, /// then forgets the container while returning a pointer to the @@ -7,15 +8,64 @@ use std::ffi::CString; /// /// The returned pointer must be passed to CString::from_raw to /// prevent leaking memory. -pub fn into_leaked_cstring( - string: String, -) -> Result<*const c_char, anyhow::Error> { - let copy = CString::new(string)?; +pub(crate) fn into_leaked_cstring( + t: &str, +) -> anyhow::Result<*const c_char> { + let copy = CString::new(t)?; let ptr = copy.as_ptr(); // Intentionally leak this memory so that it stays // valid while C is using it. - std::mem::forget(copy); + mem::forget(copy); Ok(ptr) } + +/// Delete a string previously returned by this FFI. +/// +/// It is explicitly allowed to pass a null pointer to this function; +/// in that case the function will do nothing. +#[no_mangle] +pub extern "C" fn string_delete(string: *mut c_char) { + ffi! { + name: "string_delete", + params: [string], + op: { + if string.is_null() { + return Ok(()); + } + + let string = unsafe { CString::from_raw(string) }; + std::mem::drop(string); + Ok(()) + }, + fail: { + } + } +} + +/// Construct a CStr safely with null checks. +#[macro_export] +macro_rules! cstr { + ( $name:ident ) => {{ + use std::ffi::CStr; + + if $name.is_null() { + anyhow::bail!(concat!(stringify!($name), " is null")); + } + + CStr::from_ptr($name) + }}; +} + +/// Construct a `&str` safely with null checks. +#[macro_export] +macro_rules! safe_str { + ( $name:ident ) => {{ + cstr!($name).to_str().context(concat!( + "error parsing ", + stringify!($name), + " as UTF-8" + ))? + }}; +}