Skip to content

Commit 7a339fa

Browse files
committedApr 22, 2021
Added metadata key/value iteration (#28)
This commit adds the ability to get and iterate over metadata for Pact messages.
1 parent 44700c8 commit 7a339fa

File tree

3 files changed

+165
-35
lines changed

3 files changed

+165
-35
lines changed
 

‎rust/pact_matching_ffi/src/models/message.rs

+161-21
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use crate::util::*;
99
use crate::{as_mut, as_ref, cstr, ffi, safe_str};
1010
use anyhow::{anyhow, Context};
1111
use libc::{c_char, c_int, c_uint, EXIT_FAILURE, EXIT_SUCCESS};
12+
use std::ops::Drop;
1213

1314
/*===============================================================================================
1415
* # Re-Exports
@@ -110,8 +111,7 @@ pub unsafe extern "C" fn message_get_description(
110111
params: [message],
111112
op: {
112113
let message = as_ref!(message);
113-
let description = string::into_leaked_cstring(&message.description)?;
114-
Ok(description)
114+
Ok(string::to_c(&message.description)? as *const c_char)
115115
},
116116
fail: {
117117
ptr::null_to::<c_char>()
@@ -222,15 +222,14 @@ pub unsafe extern "C" fn message_find_metadata(
222222
name: "message_find_metadata",
223223
params: [message, key],
224224
op: {
225+
// Reconstitute the message.
225226
let message = as_ref!(message);
227+
// Safely get a Rust String out of the key.
226228
let key = safe_str!(key);
227-
228-
match message.metadata.get(key) {
229-
None => Ok(ptr::null_to::<c_char>()),
230-
Some(value) => {
231-
Ok(string::into_leaked_cstring(value)?)
232-
},
233-
}
229+
// Get the value, if present, for that key.
230+
let value = message.metadata.get(key).ok_or(anyhow::anyhow!("invalid metadata key"))?;
231+
// Leak the string to the C-side.
232+
Ok(string::to_c(value)? as *const c_char)
234233
},
235234
fail: {
236235
ptr::null_to::<c_char>()
@@ -276,14 +275,13 @@ pub unsafe extern "C" fn message_insert_metadata(
276275
}
277276
}
278277

279-
/*
280-
/// Get a copy of the metadata list from this message.
281-
/// It is in the form of a list of (key, value) pairs,
282-
/// in an unspecified order.
283-
/// The returned structure must be deleted with `metadata_list_delete`.
278+
/// Get an iterator over the metadata of a message.
284279
///
285-
/// Since it is a copy, the returned structure may safely outlive
286-
/// the `Message`.
280+
/// This iterator carries a pointer to the message, and must
281+
/// not outlive the message.
282+
///
283+
/// The message metadata also must not be modified during iteration. If it is,
284+
/// the old iterator must be deleted and a new iterator created.
287285
///
288286
/// # Errors
289287
///
@@ -294,23 +292,165 @@ pub unsafe extern "C" fn message_insert_metadata(
294292
#[no_mangle]
295293
#[allow(clippy::missing_safety_doc)]
296294
#[allow(clippy::or_fun_call)]
297-
pub unsafe extern "C" fn message_get_metadata_list(
295+
pub unsafe extern "C" fn message_get_metadata_iter(
298296
message: *mut Message,
299297
) -> *mut MetadataIterator {
300298
ffi! {
301-
name: "message_get_metadata_list",
299+
name: "message_get_metadata_iter",
302300
params: [message],
303301
op: {
304302
let message = as_mut!(message);
305303

306-
todo!()
304+
let iter = MetadataIterator {
305+
keys: message.metadata.keys().cloned().collect(),
306+
current: 0,
307+
message: message as *const Message,
308+
};
309+
310+
Ok(ptr::raw_to(iter))
311+
},
312+
fail: {
313+
ptr::null_mut_to::<MetadataIterator>()
314+
}
315+
}
316+
}
317+
318+
/// Get the next key and value out of the iterator, if possible
319+
///
320+
/// Returns a pointer to a heap allocated array of 2 elements, the pointer to the
321+
/// key string on the heap, and the pointer to the value string on the heap.
322+
///
323+
/// The user needs to free both the contained strings and the array.
324+
#[no_mangle]
325+
#[allow(clippy::missing_safety_doc)]
326+
#[allow(clippy::or_fun_call)]
327+
pub unsafe extern "C" fn metadata_iter_next(
328+
iter: *mut MetadataIterator,
329+
) -> *mut MetadataPair {
330+
ffi! {
331+
name: "metadata_iter_next",
332+
params: [iter],
333+
op: {
334+
// Reconstitute the iterator.
335+
let iter = as_mut!(iter);
336+
337+
// Reconstitute the message.
338+
let message = as_ref!(iter.message);
339+
340+
// Get the current key from the iterator.
341+
let key = iter.next().ok_or(anyhow::anyhow!("iter past the end of metadata"))?;
342+
343+
// Get the value for the current key.
344+
let (key, value) = message.metadata.get_key_value(key).ok_or(anyhow::anyhow!("iter provided invalid metadata key"))?;
345+
346+
// Package up for return.
347+
let pair = MetadataPair::new(key, value)?;
348+
349+
// Leak the value out to the C-side.
350+
Ok(ptr::raw_to(pair))
307351
},
308352
fail: {
309-
ptr::null_to::<MetadataIterator>()
353+
ptr::null_mut_to::<MetadataPair>()
310354
}
311355
}
312356
}
313-
*/
357+
358+
/// Free the metadata iterator when you're done using it.
359+
#[no_mangle]
360+
#[allow(clippy::missing_safety_doc)]
361+
pub unsafe extern "C" fn metadata_iter_delete(
362+
iter: *mut MetadataIterator,
363+
) -> c_int {
364+
ffi! {
365+
name: "metadata_iter_delete",
366+
params: [iter],
367+
op: {
368+
ptr::drop_raw(iter);
369+
Ok(EXIT_SUCCESS)
370+
},
371+
fail: {
372+
EXIT_FAILURE
373+
}
374+
}
375+
}
376+
377+
/// Free a pair of key and value returned from `message_next_metadata_iter`.
378+
#[no_mangle]
379+
#[allow(clippy::missing_safety_doc)]
380+
pub unsafe extern "C" fn metadata_pair_delete(
381+
pair: *mut MetadataPair,
382+
) -> c_int {
383+
ffi! {
384+
name: "metadata_pair_delete",
385+
params: [pair],
386+
op: {
387+
ptr::drop_raw(pair);
388+
Ok(EXIT_SUCCESS)
389+
},
390+
fail: {
391+
EXIT_FAILURE
392+
}
393+
}
394+
}
395+
396+
/// An iterator that enables FFI iteration over metadata by putting all the keys on the heap
397+
/// and tracking which one we're currently at.
398+
///
399+
/// This assumes no mutation of the underlying metadata happens while the iterator is live.
400+
#[derive(Debug)]
401+
pub struct MetadataIterator {
402+
/// The metadata keys
403+
keys: Vec<String>,
404+
/// The current key
405+
current: usize,
406+
/// Pointer to the message.
407+
message: *const Message,
408+
}
409+
410+
impl MetadataIterator {
411+
fn next(&mut self) -> Option<&String> {
412+
let idx = self.current;
413+
self.current += 1;
414+
self.keys.get(idx)
415+
}
416+
}
417+
418+
/// A single key-value pair exported to the C-side.
419+
#[derive(Debug)]
420+
#[repr(C)]
421+
#[allow(missing_copy_implementations)]
422+
pub struct MetadataPair {
423+
key: *const c_char,
424+
value: *const c_char,
425+
}
426+
427+
impl MetadataPair {
428+
fn new(key: &str, value: &str) -> anyhow::Result<MetadataPair> {
429+
Ok(MetadataPair {
430+
key: string::to_c(key)? as *const c_char,
431+
value: string::to_c(value)? as *const c_char,
432+
})
433+
}
434+
}
435+
436+
// Ensure that the owned strings are freed when the pair is dropped.
437+
//
438+
// Notice that we're casting from a `*const c_char` to a `*mut c_char`.
439+
// This may seem wrong, but is safe so long as it doesn't violate Rust's
440+
// guarantees around immutable references, which this doesn't. In this case,
441+
// the underlying data came from `CString::into_raw` which takes ownership
442+
// of the `CString` and hands it off via a `*mut pointer`. We cast that pointer
443+
// back to `*const` to limit the C-side from doing any shenanigans, since the
444+
// pointed-to values live inside of the `Message` metadata `HashMap`, but
445+
// cast back to `*mut` here so we can free the memory.
446+
//
447+
// The discussion here helps explain: https://github.com/rust-lang/rust-clippy/issues/4774
448+
impl Drop for MetadataPair {
449+
fn drop(&mut self) {
450+
string::string_delete(self.key as *mut c_char);
451+
string::string_delete(self.value as *mut c_char);
452+
}
453+
}
314454

315455
/*===============================================================================================
316456
* # Status Types

‎rust/pact_matching_ffi/src/util/ptr.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ pub(crate) fn null_mut_to<T>() -> *mut T {
3636
/// Get an immutable reference from a raw pointer
3737
#[macro_export]
3838
macro_rules! as_ref {
39-
( $name:ident ) => {{
39+
( $name:expr ) => {{
4040
$name
4141
.as_ref()
4242
.ok_or(anyhow!(concat!(stringify!($name), " is null")))?
@@ -46,7 +46,7 @@ macro_rules! as_ref {
4646
/// Get a mutable reference from a raw pointer
4747
#[macro_export]
4848
macro_rules! as_mut {
49-
( $name:ident ) => {{
49+
( $name:expr ) => {{
5050
$name
5151
.as_mut()
5252
.ok_or(anyhow!(concat!(stringify!($name), " is null")))?

‎rust/pact_matching_ffi/src/util/string.rs

+2-12
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,14 @@
11
use libc::c_char;
22
use std::ffi::CString;
3-
use std::mem;
43

54
/// Converts the string into a C-compatible null terminated string,
65
/// then forgets the container while returning a pointer to the
76
/// underlying buffer.
87
///
98
/// The returned pointer must be passed to CString::from_raw to
109
/// prevent leaking memory.
11-
pub(crate) fn into_leaked_cstring(
12-
t: &str,
13-
) -> anyhow::Result<*const c_char> {
14-
let copy = CString::new(t)?;
15-
let ptr = copy.as_ptr();
16-
17-
// Intentionally leak this memory so that it stays
18-
// valid while C is using it.
19-
mem::forget(copy);
20-
21-
Ok(ptr)
10+
pub(crate) fn to_c(t: &str) -> anyhow::Result<*mut c_char> {
11+
Ok(CString::new(t)?.into_raw())
2212
}
2313

2414
/// Delete a string previously returned by this FFI.

0 commit comments

Comments
 (0)
Please sign in to comment.