Skip to main content

confium_tc/
ffi.rs

1// FFI entry points accept raw pointers and null-check them before
2// dereferencing; they are not `unsafe` from the C caller's perspective.
3// This mirrors the same suppression used in confium-core's ffi module.
4#![allow(clippy::not_unsafe_ptr_arg_deref)]
5
6//! `cfm_tc_*` C ABI for the threshold-cryptography session interface.
7//!
8//! These symbols are declared `#[unsafe(no_mangle)] pub extern "C"` and
9//! linked into whichever cdylib depends on `confium-tc` (today,
10//! `confium-core`'s `libconfium` cdylib, eventually a dedicated
11//! `confium-ffi`). The crate itself is an `rlib`, not a `cdylib`.
12//!
13//! See `TODO.roadmap/04-threshold-cryptography.md` for the protocol
14//! rationale and `crates/confium-core/src/ffi/rng.rs` for the FFI
15//! pattern these entry points mirror.
16//!
17//! ## Memory ownership
18//!
19//! - `CFMTcMessage **outgoing` produced by `cfm_tc_session_round` is a
20//!   heap array allocated by the framework; the caller frees it with
21//!   [`cfm_tc_messages_destroy`].
22//! - `CFMTcShare *` produced by `cfm_tc_dkg_output_share` is a heap
23//!   allocation; the caller frees it with [`cfm_tc_share_destroy`].
24//! - `FFITcSession *` is owned by the caller; destroy with
25//!   [`cfm_tc_session_destroy`].
26
27use std::ffi::CStr;
28use std::ffi::CString;
29use std::os::raw::c_char;
30
31use snafu::ResultExt;
32
33use crate::error;
34use crate::error::Result as TcResult;
35use crate::message::Message;
36use crate::party::{Party, PartyList};
37use crate::session::{Session, SessionParams};
38use crate::share::Share;
39
40/// Opaque session handle returned to C.
41pub enum FFITcSession {}
42
43/// Roster of parties, C view.
44///
45/// `party_ids` and `transport_endpoints` are parallel arrays of
46/// NUL-terminated C strings. `transport_endpoints[i]` may be NULL when
47/// party `i` has no network endpoint (in-process sessions).
48#[repr(C)]
49pub struct CFMTcPartyList {
50    pub party_ids: *const *const c_char,
51    pub transport_endpoints: *const *const c_char,
52    pub count: u32,
53}
54
55/// One inter-party message, C view. `to_party_id == NULL` means
56/// broadcast.
57#[repr(C)]
58pub struct CFMTcMessage {
59    pub from_party_id: *const c_char,
60    pub to_party_id: *const c_char,
61    pub round: u8,
62    pub payload: *const u8,
63    pub payload_len: u32,
64}
65
66/// A party's share of a distributed secret, C view.
67#[repr(C)]
68pub struct CFMTcShare {
69    pub scheme: *const c_char,
70    pub bytes: *const u8,
71    pub len: u32,
72}
73
74/// Heap allocation returned by `cfm_tc_session_round` for the outgoing
75/// messages array. The framework owns the backing storage; the caller
76/// frees the whole bundle with [`cfm_tc_messages_destroy`].
77#[repr(C)]
78pub struct CFMTcMessageArray {
79    pub items: *mut CFMTcMessage,
80    pub count: u32,
81}
82
83fn cstr_to_string(ptr: *const c_char, param: &'static str) -> TcResult<String> {
84    if ptr.is_null() {
85        return error::NullPointerSnafu { param }.fail();
86    }
87    unsafe { CStr::from_ptr(ptr) }
88        .to_str()
89        .context(error::InvalidUTF8Snafu {})
90        .map(str::to_string)
91}
92
93fn opt_cstr_to_string(ptr: *const c_char) -> TcResult<Option<String>> {
94    if ptr.is_null() {
95        return Ok(None);
96    }
97    unsafe { CStr::from_ptr(ptr) }
98        .to_str()
99        .context(error::InvalidUTF8Snafu {})
100        .map(|s| Some(s.to_string()))
101}
102
103/// Read a parallel-array `CFMTcPartyList` into an owned [`PartyList`].
104fn party_list_from_c(cpl: &CFMTcPartyList) -> TcResult<PartyList> {
105    let count = cpl.count as usize;
106    let ids = if cpl.party_ids.is_null() {
107        return error::NullPointerSnafu { param: "party_ids" }.fail();
108    } else {
109        unsafe { std::slice::from_raw_parts(cpl.party_ids, count) }
110    };
111    let endpoints = if cpl.transport_endpoints.is_null() {
112        // endpoints array may be NULL meaning "no party has an endpoint".
113        vec![std::ptr::null::<c_char>(); count]
114    } else {
115        unsafe { std::slice::from_raw_parts(cpl.transport_endpoints, count) }.to_vec()
116    };
117    if endpoints.len() != count {
118        return error::NullPointerSnafu {
119            param: "transport_endpoints",
120        }
121        .fail();
122    }
123    let mut parties = Vec::with_capacity(count);
124    for (i, id_ptr) in ids.iter().enumerate() {
125        let id = cstr_to_string(*id_ptr, "party_id")?;
126        let ep = if endpoints[i].is_null() {
127            None
128        } else {
129            Some(cstr_to_string(endpoints[i], "transport_endpoint")?)
130        };
131        parties.push(Party::new(id, ep));
132    }
133    Ok(PartyList::from_parties(parties))
134}
135
136/// Read an optional `CFMTcShare *` into an owned [`Share`].
137fn share_from_c(cshare: *const CFMTcShare) -> TcResult<Option<Share>> {
138    if cshare.is_null() {
139        return Ok(None);
140    }
141    let cshare = unsafe { &*cshare };
142    let scheme = cstr_to_string(cshare.scheme, "share.scheme")?;
143    if cshare.bytes.is_null() && cshare.len != 0 {
144        return error::NullPointerSnafu {
145            param: "share.bytes",
146        }
147        .fail();
148    }
149    let bytes = if cshare.len == 0 {
150        Vec::new()
151    } else {
152        unsafe { std::slice::from_raw_parts(cshare.bytes, cshare.len as usize) }.to_vec()
153    };
154    Ok(Some(Share::new(scheme, bytes)))
155}
156
157/// Build a heap-allocated `CFMTcShare` from an owned [`Share`]. The
158/// caller frees it with [`cfm_tc_share_destroy`].
159fn share_to_c(share: Share) -> TcResult<*mut CFMTcShare> {
160    let scheme_c = CString::new(share.scheme()).context(error::NulByteSnafu {})?;
161    let bytes = share.into_bytes();
162    let bytes_len = bytes.len() as u32;
163    let bytes_box = bytes.into_boxed_slice();
164    let bytes_ptr = Box::into_raw(bytes_box) as *const u8;
165    let scheme_ptr = scheme_c.into_raw();
166    let cshare = Box::new(CFMTcShare {
167        scheme: scheme_ptr,
168        bytes: bytes_ptr,
169        len: bytes_len,
170    });
171    Ok(Box::into_raw(cshare))
172}
173
174/// Build a heap-allocated `CFMTcMessageArray` from owned [`Message`]s.
175/// Each message's string fields and payload are separately heap-allocated
176/// and owned by the bundle; the caller frees the whole array with
177/// [`cfm_tc_messages_destroy`].
178fn messages_to_c_array(msgs: Vec<Message>) -> TcResult<*mut CFMTcMessageArray> {
179    let count = msgs.len() as u32;
180    if msgs.is_empty() {
181        let bundle = Box::new(CFMTcMessageArray {
182            items: std::ptr::null_mut(),
183            count: 0,
184        });
185        return Ok(Box::into_raw(bundle));
186    }
187    let mut items: Vec<CFMTcMessage> = Vec::with_capacity(msgs.len());
188    for m in msgs {
189        let from_c = CString::new(m.from_party_id).context(error::NulByteSnafu {})?;
190        let to_c = match m.to_party_id {
191            Some(t) => Some(CString::new(t).context(error::NulByteSnafu {})?),
192            None => None,
193        };
194        let payload_len = m.payload.len() as u32;
195        let payload_box = m.payload.into_boxed_slice();
196        let payload_ptr = Box::into_raw(payload_box) as *const u8;
197        let from_ptr = from_c.into_raw();
198        let to_ptr: Option<*mut c_char> = to_c.map(|c| c.into_raw());
199        // to_ptr may be null; that's the broadcast sentinel.
200        items.push(CFMTcMessage {
201            from_party_id: from_ptr,
202            to_party_id: to_ptr.unwrap_or(std::ptr::null_mut()),
203            round: m.round,
204            payload: payload_ptr,
205            payload_len,
206        });
207    }
208    let items_box = items.into_boxed_slice();
209    let items_ptr = Box::into_raw(items_box) as *mut CFMTcMessage;
210    let bundle = Box::new(CFMTcMessageArray {
211        items: items_ptr,
212        count,
213    });
214    Ok(Box::into_raw(bundle))
215}
216
217/// Free a single message's owned fields. Does NOT free the message
218/// struct itself (it lives inside the array's backing slice).
219unsafe fn free_message_fields(m: &CFMTcMessage) {
220    unsafe {
221        if !m.from_party_id.is_null() {
222            drop(CString::from_raw(m.from_party_id as *mut c_char));
223        }
224        if !m.to_party_id.is_null() {
225            drop(CString::from_raw(m.to_party_id as *mut c_char));
226        }
227        if m.payload_len != 0 && !m.payload.is_null() {
228            let slice =
229                std::slice::from_raw_parts_mut(m.payload as *mut u8, m.payload_len as usize);
230            drop(Box::from_raw(slice as *mut [u8]));
231        }
232    }
233}
234
235/// Convert a `TcResult<()>` into a `u32` FFI return code, zero on
236/// success. Mirrors `confium-core`'s `ffi_return_err!` but without the
237/// out-error-pointer machinery (the TC ABI returns codes only).
238#[allow(dead_code)]
239fn code_of<T>(r: TcResult<T>) -> u32 {
240    match r {
241        Ok(_) => 0,
242        Err(e) => e.code(),
243    }
244}
245
246// ---------------------------------------------------------------------------
247// FFI entry points
248// ---------------------------------------------------------------------------
249
250/// Create a new threshold session.
251///
252/// On success, `*out` is a heap-allocated `FFITcSession *` owned by the
253/// caller. Destroy with [`cfm_tc_session_destroy`].
254#[unsafe(no_mangle)]
255pub extern "C" fn cfm_tc_session_create(
256    out: *mut *mut FFITcSession,
257    scheme: *const c_char,
258    party_list: *const CFMTcPartyList,
259    threshold: u32,
260    this_party_idx: u32,
261    local_share: *const CFMTcShare,
262    message: *const u8,
263    message_len: u32,
264) -> u32 {
265    if out.is_null() {
266        return error::ErrorCode::NULL_POINTER.into();
267    }
268    let result = (|| -> TcResult<*mut FFITcSession> {
269        let scheme_name = cstr_to_string(scheme, "scheme")?;
270        if party_list.is_null() {
271            return error::NullPointerSnafu {
272                param: "party_list",
273            }
274            .fail();
275        }
276        let cpl = unsafe { &*party_list };
277        let parties = party_list_from_c(cpl)?;
278        let local_share = share_from_c(local_share)?;
279        let message = if message.is_null() || message_len == 0 {
280            None
281        } else {
282            Some(unsafe { std::slice::from_raw_parts(message, message_len as usize) }.to_vec())
283        };
284        let params = SessionParams {
285            scheme: scheme_name,
286            parties,
287            threshold,
288            this_party_idx: this_party_idx as usize,
289            local_share,
290            message,
291        };
292        let session = Session::create(&params)?;
293        Ok(Box::into_raw(Box::new(session)) as *mut FFITcSession)
294    })();
295    match result {
296        Ok(ptr) => {
297            unsafe { *out = ptr };
298            0
299        }
300        Err(e) => e.code(),
301    }
302}
303
304/// Step the session forward one round.
305///
306/// - `incoming` / `incoming_count`: messages received since the last
307///   round (may be NULL / 0 for the first round).
308/// - `outgoing` / `outgoing_count`: on success set to a heap-allocated
309///   [`CFMTcMessageArray`]; free with [`cfm_tc_messages_destroy`].
310/// - `complete`: set to 1 when the session has produced its result.
311#[unsafe(no_mangle)]
312pub extern "C" fn cfm_tc_session_round(
313    session: *mut FFITcSession,
314    incoming: *const CFMTcMessage,
315    incoming_count: u32,
316    outgoing: *mut *mut CFMTcMessageArray,
317    outgoing_count: *mut u32,
318    complete: *mut u8,
319) -> u32 {
320    if session.is_null() {
321        return error::ErrorCode::NULL_POINTER.into();
322    }
323    if outgoing.is_null() || outgoing_count.is_null() || complete.is_null() {
324        return error::ErrorCode::NULL_POINTER.into();
325    }
326    let session = unsafe { &mut *(session as *mut Session) };
327
328    // Materialize incoming messages into owned Rust values.
329    let incoming_vec: TcResult<Vec<Message>> = (|| {
330        if incoming.is_null() || incoming_count == 0 {
331            return Ok(Vec::new());
332        }
333        let slice = unsafe { std::slice::from_raw_parts(incoming, incoming_count as usize) };
334        let mut out = Vec::with_capacity(slice.len());
335        for cm in slice {
336            let from = cstr_to_string(cm.from_party_id, "incoming.from_party_id")?;
337            let to = opt_cstr_to_string(cm.to_party_id)?;
338            let payload = if cm.payload_len == 0 || cm.payload.is_null() {
339                Vec::new()
340            } else {
341                unsafe { std::slice::from_raw_parts(cm.payload, cm.payload_len as usize) }.to_vec()
342            };
343            out.push(Message {
344                from_party_id: from,
345                to_party_id: to,
346                round: cm.round,
347                payload,
348            });
349        }
350        Ok(out)
351    })();
352    let incoming_vec = match incoming_vec {
353        Ok(v) => v,
354        Err(e) => return e.code(),
355    };
356
357    match session.round_step(&incoming_vec) {
358        Ok(rr) => match messages_to_c_array(rr.outgoing) {
359            Ok(arr_ptr) => {
360                let arr = unsafe { &mut *arr_ptr };
361                unsafe {
362                    *outgoing = arr_ptr;
363                    *outgoing_count = arr.count;
364                    *complete = if rr.complete { 1 } else { 0 };
365                }
366                0
367            }
368            Err(e) => e.code(),
369        },
370        Err(e) => e.code(),
371    }
372}
373
374/// Read the final session artifact into `out`.
375///
376/// Returns `INSUFFICIENT_BUFFER` when `out_max` is too small; `*out_len`
377/// is always set to the required length.
378#[unsafe(no_mangle)]
379pub extern "C" fn cfm_tc_session_result(
380    session: *mut FFITcSession,
381    out: *mut u8,
382    out_max: u32,
383    out_len: *mut u32,
384) -> u32 {
385    if session.is_null() {
386        return error::ErrorCode::NULL_POINTER.into();
387    }
388    if out_len.is_null() {
389        return error::ErrorCode::NULL_POINTER.into();
390    }
391    let session = unsafe { &*(session as *mut Session) };
392    match session.result() {
393        Ok(bytes) => {
394            let needed = bytes.len();
395            unsafe { *out_len = needed as u32 };
396            if (out_max as usize) < needed {
397                return error::ErrorCode::INSUFFICIENT_BUFFER.into();
398            }
399            if !out.is_null() {
400                unsafe {
401                    std::ptr::copy_nonoverlapping(bytes.as_ptr(), out, needed);
402                }
403            }
404            0
405        }
406        Err(e) => e.code(),
407    }
408}
409
410/// For DKG sessions: extract the per-party share and the shared public
411/// key.
412///
413/// - `share_out`: set to a heap-allocated [`CFMTcShare`]; free with
414///   [`cfm_tc_share_destroy`].
415/// - `public_key_out` / `pk_max` / `pk_len`: shared public key bytes.
416#[unsafe(no_mangle)]
417pub extern "C" fn cfm_tc_dkg_output_share(
418    session: *mut FFITcSession,
419    share_out: *mut *mut CFMTcShare,
420    public_key_out: *mut u8,
421    pk_max: u32,
422    pk_len: *mut u32,
423) -> u32 {
424    if session.is_null() {
425        return error::ErrorCode::NULL_POINTER.into();
426    }
427    if share_out.is_null() || pk_len.is_null() {
428        return error::ErrorCode::NULL_POINTER.into();
429    }
430    let session = unsafe { &*(session as *mut Session) };
431
432    // The public key is the session result (shared across all parties).
433    let pk = match session.dkg_public_key() {
434        Ok(b) => b,
435        Err(e) => return e.code(),
436    };
437    let needed = pk.len();
438    unsafe { *pk_len = needed as u32 };
439    if (pk_max as usize) < needed {
440        return error::ErrorCode::INSUFFICIENT_BUFFER.into();
441    }
442    if !public_key_out.is_null() {
443        unsafe {
444            std::ptr::copy_nonoverlapping(pk.as_ptr(), public_key_out, needed);
445        }
446    }
447
448    // The framework skeleton does not yet synthesize a per-party share
449    // from the session state — that requires scheme cooperation. Return
450    // an empty share tagged with the session's scheme so the caller
451    // shape is exercised; scheme plugins override this via their own
452    // protocol-specific entry point in a later iteration.
453    let empty_share = Share::new(session.scheme_name(), Vec::new());
454    match share_to_c(empty_share) {
455        Ok(ptr) => {
456            unsafe { *share_out = ptr };
457            0
458        }
459        Err(e) => e.code(),
460    }
461}
462
463/// Destroy a session. Safe to call with NULL.
464#[unsafe(no_mangle)]
465pub extern "C" fn cfm_tc_session_destroy(session: *mut FFITcSession) {
466    if session.is_null() {
467        return;
468    }
469    unsafe {
470        let session = Box::from_raw(session as *mut Session);
471        let mut session = session;
472        session.destroy();
473        drop(session);
474    }
475}
476
477/// Free a `CFMTcMessageArray` returned by [`cfm_tc_session_round`]. Safe
478/// to call with NULL.
479#[unsafe(no_mangle)]
480pub extern "C" fn cfm_tc_messages_destroy(arr: *mut CFMTcMessageArray) {
481    if arr.is_null() {
482        return;
483    }
484    unsafe {
485        let bundle = Box::from_raw(arr);
486        if bundle.count == 0 || bundle.items.is_null() {
487            return;
488        }
489        let items = std::slice::from_raw_parts_mut(bundle.items, bundle.count as usize);
490        for m in items.iter() {
491            free_message_fields(m);
492        }
493        let _ = Box::from_raw(items as *mut [CFMTcMessage]);
494    }
495}
496
497/// Free a `CFMTcShare` returned by [`cfm_tc_dkg_output_share`]. Safe to
498/// call with NULL.
499#[unsafe(no_mangle)]
500pub extern "C" fn cfm_tc_share_destroy(share: *mut CFMTcShare) {
501    if share.is_null() {
502        return;
503    }
504    unsafe {
505        let cshare = Box::from_raw(share);
506        if !cshare.scheme.is_null() {
507            drop(CString::from_raw(cshare.scheme as *mut c_char));
508        }
509        if cshare.len != 0 && !cshare.bytes.is_null() {
510            let slice =
511                std::slice::from_raw_parts_mut(cshare.bytes as *mut u8, cshare.len as usize);
512            let _ = Box::from_raw(slice as *mut [u8]);
513        }
514    }
515}
516
517#[cfg(test)]
518mod tests {
519    use super::*;
520    use crate::registry::{SessionImpl, TcScheme, TcSchemeKind};
521    use crate::session::SessionParams;
522
523    /// Round-trip a `CFMTcPartyList` through [`party_list_from_c`].
524    #[test]
525    fn party_list_round_trip_c() {
526        let id1 = CString::new("a").unwrap();
527        let id2 = CString::new("b").unwrap();
528        let ep1 = CString::new("quic://a:443").unwrap();
529        let ids = [id1.as_ptr(), id2.as_ptr()];
530        let eps = [ep1.as_ptr(), std::ptr::null()];
531        let cpl = CFMTcPartyList {
532            party_ids: ids.as_ptr(),
533            transport_endpoints: eps.as_ptr(),
534            count: 2,
535        };
536        let list = party_list_from_c(&cpl).expect("parse");
537        assert_eq!(list.len(), 2);
538        assert_eq!(list.get(0).unwrap().id, "a");
539        assert_eq!(
540            list.get(0).unwrap().transport_endpoint.as_deref(),
541            Some("quic://a:443")
542        );
543        assert_eq!(list.get(1).unwrap().id, "b");
544        assert!(list.get(1).unwrap().transport_endpoint.is_none());
545    }
546
547    /// A scheme that produces a broadcast then completes, used to test
548    /// the full FFI lifecycle.
549    struct FfiTestScheme;
550    impl TcScheme for FfiTestScheme {
551        fn name(&self) -> &'static str {
552            "ffi-test"
553        }
554        fn kind(&self) -> TcSchemeKind {
555            TcSchemeKind::Signature
556        }
557        fn create_session(
558            &self,
559            params: &SessionParams,
560        ) -> crate::error::Result<Box<dyn SessionImpl>> {
561            let our_id = params.parties.get(params.this_party_idx)?.id.clone();
562            Ok(Box::new(FfiTestSession {
563                our_id,
564                round_done: 0,
565                payload: params.message.clone().unwrap_or_default(),
566            }))
567        }
568    }
569    struct FfiTestSession {
570        our_id: String,
571        round_done: u8,
572        payload: Vec<u8>,
573    }
574    impl SessionImpl for FfiTestSession {
575        fn round(
576            &mut self,
577            _incoming: &[Message],
578        ) -> crate::error::Result<crate::registry::RoundResult> {
579            self.round_done += 1;
580            if self.round_done == 1 {
581                Ok(crate::registry::RoundResult::new(
582                    vec![Message::broadcast(&self.our_id, 1, self.payload.clone())],
583                    false,
584                ))
585            } else {
586                Ok(crate::registry::RoundResult::done())
587            }
588        }
589        fn result(&self) -> crate::error::Result<Vec<u8>> {
590            Ok(self.payload.clone())
591        }
592        fn destroy(&mut self) {
593            self.payload.fill(0);
594        }
595    }
596    inventory::submit! {
597        crate::registry::RegisteredScheme {
598            scheme: &FfiTestScheme as &dyn TcScheme
599        }
600    }
601
602    fn build_party_list_c() -> (
603        Vec<CString>,
604        Vec<CString>,
605        Vec<*const c_char>,
606        Vec<*const c_char>,
607    ) {
608        let ids = vec![
609            CString::new("a").unwrap(),
610            CString::new("b").unwrap(),
611            CString::new("c").unwrap(),
612        ];
613        let id_ptrs: Vec<*const c_char> = ids.iter().map(|c| c.as_ptr()).collect();
614        let eps: Vec<CString> = Vec::new();
615        let ep_ptrs: Vec<*const c_char> = vec![std::ptr::null(); 3];
616        (ids, eps, id_ptrs, ep_ptrs)
617    }
618
619    #[test]
620    fn ffi_full_session_lifecycle() {
621        let (_ids, _eps, id_ptrs, ep_ptrs) = build_party_list_c();
622        let cpl = CFMTcPartyList {
623            party_ids: id_ptrs.as_ptr(),
624            transport_endpoints: ep_ptrs.as_ptr(),
625            count: 3,
626        };
627        let scheme = CString::new("ffi-test").unwrap();
628        let msg = b"sign-this-message";
629        let mut session_ptr: *mut FFITcSession = std::ptr::null_mut();
630        let code = cfm_tc_session_create(
631            &mut session_ptr,
632            scheme.as_ptr(),
633            &cpl,
634            2,
635            0,
636            std::ptr::null(),
637            msg.as_ptr(),
638            msg.len() as u32,
639        );
640        assert_eq!(code, 0, "create should succeed");
641        assert!(!session_ptr.is_null());
642
643        // Round 1.
644        let mut outgoing: *mut CFMTcMessageArray = std::ptr::null_mut();
645        let mut outgoing_count: u32 = 0;
646        let mut complete: u8 = 99;
647        let code = cfm_tc_session_round(
648            session_ptr,
649            std::ptr::null(),
650            0,
651            &mut outgoing,
652            &mut outgoing_count,
653            &mut complete,
654        );
655        assert_eq!(code, 0, "round 1 should succeed");
656        assert_eq!(complete, 0, "round 1 not complete");
657        assert_eq!(outgoing_count, 1);
658        assert!(!outgoing.is_null());
659        // Read the broadcast message.
660        unsafe {
661            let arr = &*outgoing;
662            let m = &*arr.items.add(0);
663            assert_eq!(CStr::from_ptr(m.from_party_id).to_str().unwrap(), "a");
664            assert!(m.to_party_id.is_null(), "broadcast to_party_id is null");
665            assert_eq!(m.round, 1);
666            assert_eq!(m.payload_len, msg.len() as u32);
667        }
668        cfm_tc_messages_destroy(outgoing);
669
670        // Round 2 — completes.
671        let code = cfm_tc_session_round(
672            session_ptr,
673            std::ptr::null(),
674            0,
675            &mut outgoing,
676            &mut outgoing_count,
677            &mut complete,
678        );
679        assert_eq!(code, 0, "round 2 should succeed");
680        assert_eq!(complete, 1, "round 2 complete");
681        assert_eq!(outgoing_count, 0);
682        cfm_tc_messages_destroy(outgoing);
683
684        // Read the result.
685        let mut out_buf = [0u8; 64];
686        let mut out_len: u32 = 0;
687        let code = cfm_tc_session_result(
688            session_ptr,
689            out_buf.as_mut_ptr(),
690            out_buf.len() as u32,
691            &mut out_len,
692        );
693        assert_eq!(code, 0, "result should succeed");
694        assert_eq!(out_len as usize, msg.len());
695        assert_eq!(&out_buf[..out_len as usize], msg);
696
697        cfm_tc_session_destroy(session_ptr);
698    }
699
700    #[test]
701    fn ffi_session_result_insufficient_buffer() {
702        let (_ids, _eps, id_ptrs, ep_ptrs) = build_party_list_c();
703        let cpl = CFMTcPartyList {
704            party_ids: id_ptrs.as_ptr(),
705            transport_endpoints: ep_ptrs.as_ptr(),
706            count: 3,
707        };
708        let scheme = CString::new("ffi-test").unwrap();
709        let msg = b"twelve-bytes";
710        let mut session_ptr: *mut FFITcSession = std::ptr::null_mut();
711        cfm_tc_session_create(
712            &mut session_ptr,
713            scheme.as_ptr(),
714            &cpl,
715            2,
716            0,
717            std::ptr::null(),
718            msg.as_ptr(),
719            msg.len() as u32,
720        );
721        // Drive to completion.
722        let mut outgoing: *mut CFMTcMessageArray = std::ptr::null_mut();
723        let mut outgoing_count: u32 = 0;
724        let mut complete: u8 = 0;
725        cfm_tc_session_round(
726            session_ptr,
727            std::ptr::null(),
728            0,
729            &mut outgoing,
730            &mut outgoing_count,
731            &mut complete,
732        );
733        cfm_tc_messages_destroy(outgoing);
734        cfm_tc_session_round(
735            session_ptr,
736            std::ptr::null(),
737            0,
738            &mut outgoing,
739            &mut outgoing_count,
740            &mut complete,
741        );
742        cfm_tc_messages_destroy(outgoing);
743
744        // Too-small buffer.
745        let mut out_buf = [0u8; 4];
746        let mut out_len: u32 = 0;
747        let code = cfm_tc_session_result(session_ptr, out_buf.as_mut_ptr(), 4, &mut out_len);
748        assert_eq!(code, error::ErrorCode::INSUFFICIENT_BUFFER as u32);
749        assert_eq!(out_len as usize, msg.len(), "out_len reports required size");
750
751        cfm_tc_session_destroy(session_ptr);
752    }
753
754    #[test]
755    fn ffi_create_rejects_null_out() {
756        let scheme = CString::new("ffi-test").unwrap();
757        let code = cfm_tc_session_create(
758            std::ptr::null_mut(),
759            scheme.as_ptr(),
760            std::ptr::null(),
761            1,
762            0,
763            std::ptr::null(),
764            std::ptr::null(),
765            0,
766        );
767        assert_eq!(code, error::ErrorCode::NULL_POINTER as u32);
768    }
769
770    #[test]
771    fn ffi_share_round_trip() {
772        let original = Share::new("ffi-test", vec![0xCA, 0xFE]);
773        let ptr = share_to_c(original).expect("to_c");
774        let back = share_from_c(ptr).expect("from_c").expect("Some");
775        cfm_tc_share_destroy(ptr);
776        assert_eq!(back.scheme(), "ffi-test");
777        assert_eq!(back.bytes(), &[0xCA, 0xFE]);
778    }
779
780    #[test]
781    fn ffi_destroy_null_is_safe() {
782        cfm_tc_session_destroy(std::ptr::null_mut());
783        cfm_tc_messages_destroy(std::ptr::null_mut());
784        cfm_tc_share_destroy(std::ptr::null_mut());
785    }
786}