ffizz_passby/
unboxed.rs

1use crate::util::check_size_and_alignment;
2use std::default::Default;
3use std::marker::PhantomData;
4use std::mem;
5
6/// Unboxed is used to model values that are passed by reference, but where the memory allocation
7/// is handled by C. This approach allows the C code to allocate space for the value on the stack
8/// or in other structs, often avoiding unnecessary heap allocations.
9///
10/// The two type parameters, RType and CType, must share the same alignment, and RType must not be
11/// larger than CType. Functions in this type will cause a runtime panic in debug builds if these
12/// requirements are violated.
13///
14/// If the fields of the struct are meant to be accessible to C, RType and CType may be the same
15/// type, trivially ensuring the alignment and size requirements are met.
16///
17/// Define your C and Rust types, then a type alias parameterizing Unboxed:
18///
19/// ```
20/// # use ffizz_passby::Unboxed;
21/// #[repr(C)]
22/// struct ComplexInt {
23///     re: i64,
24///     im: i64,
25/// }
26/// type UnboxedComplexInt = Unboxed<ComplexInt, ComplexInt>;
27/// ```
28///
29/// Then call static methods on that type alias.
30///
31/// # Opaque CType
32///
33/// It is _not_ a requirement that the fields of the types match. In fact, a common use of this
34/// type is with an "opaque" C type that only contains a "reserved" field large enough to contain
35/// the Rust type.  There is no constant way to determine the space required for a Rust value, but
36/// it is possible to make a conservative guess, possibly leaving some unused space.  The suggested
37/// C type is represented in Rust as
38///
39/// ```
40/// # const N: usize = 2;
41/// struct CType([u64; N]);
42/// ```
43///
44/// for some N large enough to contain the Rust type on the
45/// required platforms.  In C, this type would be defined as
46///
47/// ```text
48/// struct ctype_t {
49///     _reserved size_t[N];
50/// }
51/// ```
52///
53/// for the same N.  The types must also have the same alignment; typically using `size_t`
54/// accomplishes this.
55///
56/// # Constructors
57///
58/// This type provides two functions useful for initialization of a CType given a value of type
59/// RType: `to_out_param` takes an "output argument" pointing to an uninitialized value, and
60/// initializes it; while `return_val` returns a struct value that can be used to initialize a C
61/// variable.  Both function similarly, so choose the one that makes the most sense for your API.
62/// For example, a constructor which can also return an error may prefer to put the error in the
63/// return value and use `to_out_param`.
64///
65/// # Safety
66///
67/// C allows uninitialized values, while Rust does not.  Be careful in the documentation for the C
68/// API to ensure that values are properly initialized before they are used.
69#[non_exhaustive]
70pub struct Unboxed<RType: Sized, CType: Sized> {
71    _phantom: PhantomData<(RType, CType)>,
72}
73
74impl<RType: Sized, CType: Sized> Unboxed<RType, CType> {
75    /// Take a CType and return an owned value.
76    ///
77    /// This approach is uncommon in C APIs. It leaves behind a value in the C allocation which
78    /// could be used accidentally, resulting in a use-after-free error. Prefer [`Unboxed::take_ptr`]
79    /// unless the type is Copy.
80    ///
81    /// # Safety
82    ///
83    /// * cval must be a valid CType value
84    pub unsafe fn take(cval: CType) -> RType {
85        // SAFETY:
86        //  - cval is a valid CType (see docstring)
87        unsafe { Self::from_ctype(cval) }
88    }
89
90    /// Take a pointer to a CType and return an owned value.
91    ///
92    /// This is intended for C API functions that take a value by reference (pointer), but still
93    /// "take ownership" of the value.  It leaves behind an invalid value, where any non-padding
94    /// bytes of the Rust type are zeroed.  This makes use-after-free errors in the C code more
95    /// likely to crash instead of silently working.  Which is about as good as it gets in C.
96    ///
97    /// # Safety
98    ///
99    /// Do _not_ pass a pointer to a Rust value to this function:
100    ///
101    /// ```ignore
102    /// let rust_value = RustType::take_ptr_nonnull(&mut c_value); // BAD!
103    /// ```
104    ///
105    /// This creates undefined behavior as Rust will assume `c_value` is still initialized. Use
106    /// [`Unboxed::take`] in this situation.
107    ///
108    /// * `cptr` must not be NULL and must point to a valid CType value (see [`Unboxed::take_ptr`] for a
109    ///   version allowing NULL)
110    /// * The memory pointed to by `cptr` is uninitialized when this function returns.
111    pub unsafe fn take_ptr_nonnull(cptr: *mut CType) -> RType {
112        check_size_and_alignment::<CType, RType>();
113        if cptr.is_null() {
114            panic!("NULL value not allowed");
115        }
116
117        // convert cptr to a reference to MaybeUninit<RType> (which is, for the moment,
118        // actually initialized)
119
120        // SAFETY:
121        // - casting to a pointer type with the same alignment and smaller size
122        let rref = unsafe { &mut *(cptr as *mut mem::MaybeUninit<RType>) };
123        let mut owned = mem::MaybeUninit::<RType>::zeroed();
124        // swap the actual value for the zeroed value
125        mem::swap(rref, &mut owned);
126
127        // SAFETY:
128        //  - owned contains what cptr was pointing to, which the caller guaranteed to be valid
129        unsafe { owned.assume_init() }
130    }
131
132    /// Call the contained function with a shared reference to the value.
133    ///
134    /// # Safety
135    ///
136    /// * `cptr` must not be NULL and must point to a valid CType value (see [`Unboxed::with_ref`] for a
137    ///   version allowing NULL).
138    /// * no other thread may mutate the value pointed to by `cptr` until the function returns.
139    /// * ownership of the value remains with the caller.
140    pub unsafe fn with_ref_nonnull<T, F: FnOnce(&RType) -> T>(cptr: *const CType, f: F) -> T {
141        check_size_and_alignment::<CType, RType>();
142        if cptr.is_null() {
143            panic!("NULL value not allowed");
144        }
145
146        // SAFETY:
147        // - casting to a pointer type with the same alignment and smaller size
148        f(unsafe { &*(cptr as *const RType) })
149    }
150
151    /// Call the contained function with an exclusive reference to the data type.
152    ///
153    /// # Safety
154    ///
155    /// * `cptr` must not be NULL and must point to a valid CType value (see [`Unboxed::with_ref_mut`] for a
156    ///   version allowing NULL).
157    /// * No other thread may _access_ the value pointed to by `cptr` until the function returns.
158    /// * Ownership of the value remains with the caller.
159    pub unsafe fn with_ref_mut_nonnull<T, F: FnOnce(&mut RType) -> T>(cptr: *mut CType, f: F) -> T {
160        check_size_and_alignment::<CType, RType>();
161        if cptr.is_null() {
162            panic!("NULL value not allowed");
163        }
164
165        // SAFETY:
166        // - casting to a pointer type with the same alignment and smaller size
167        f(unsafe { &mut *(cptr as *mut RType) })
168    }
169
170    /// Return a CType containing `rval`, moving `rval` in the process.
171    ///
172    /// # Safety
173    ///
174    /// * The caller must ensure that the value is eventually freed.
175    pub unsafe fn return_val(rval: RType) -> CType {
176        Self::into_ctype(rval)
177    }
178
179    /// Initialize the value pointed to arg_out with `rval`, "moving" `rval` into the pointer.
180    ///
181    /// If the pointer is NULL, `rval` is dropped.  Use [`Unboxed::to_out_param_nonnull`] to
182    /// panic in this situation.
183    ///
184    /// # Safety
185    ///
186    /// * The caller must ensure that the value is eventually freed.
187    /// * If not NULL, `arg_out` must point to valid, properly aligned memory for CType.
188    pub unsafe fn to_out_param(rval: RType, arg_out: *mut CType) {
189        if !arg_out.is_null() {
190            // SAFETY:
191            //  - arg_out is not NULL (just checked)
192            //  - arg_out is properly aligned and points to valid memory (see docstring)
193            unsafe { *arg_out = Self::into_ctype(rval) };
194        }
195    }
196
197    /// Initialize the value pointed to arg_out with `rval`, "moving" `rval` into the pointer.
198    ///
199    /// If the pointer is NULL, this method will panic.
200    ///
201    /// # Safety
202    ///
203    /// * The caller must ensure that the value is eventually freed.
204    /// * `arg_out` must not be NULL and must point to valid, properly aligned memory for CType.
205    pub unsafe fn to_out_param_nonnull(rval: RType, arg_out: *mut CType) {
206        if arg_out.is_null() {
207            panic!("out param pointer is NULL");
208        }
209        // SAFETY:
210        //  - arg_out is not NULL (see docstring)
211        //  - arg_out is properly aligned and points to valid memory (see docstring)
212        unsafe { *arg_out = Self::into_ctype(rval) };
213    }
214
215    /// Transmute a Rust value into a C value.
216    fn into_ctype(rval: RType) -> CType {
217        check_size_and_alignment::<CType, RType>();
218
219        // This looks like a lot of code, but most of it is type arithmetic.  Only the
220        // `std::ptr::copy` could potentially generate machine instructions, and in many cases even
221        // that will be optimized away.
222
223        // create a new value of type CType, uninitialized, and make a pointer to it
224        let mut cval = mem::MaybeUninit::<CType>::uninit();
225        let cptr = &mut cval as *mut mem::MaybeUninit<CType>;
226
227        // create a pointer to rval
228        let selfptr = (&mem::MaybeUninit::<RType>::new(rval)) as *const mem::MaybeUninit<RType>;
229
230        // cast cptr to a pointer to RType
231        // SAFETY:
232        // - casting to a pointer type with the same alignment and smaller size
233        let dest = unsafe { cptr as *mut mem::MaybeUninit<RType> };
234
235        // copy the data
236        // SAFETY:
237        // - selfptr is valid for a read of 1 x RType (it's of type MaybeUninit, but was
238        //   initialized)
239        // - dest is valid for write of 1 x RType
240        // - both are properly aligned (Rust ensures this)
241        unsafe { std::ptr::copy(selfptr, dest, 1) };
242
243        // SAFETY: dest pointed to cval, which is now valid
244        unsafe { cval.assume_init() }
245    }
246
247    /// Transmute a C value into a Rust value.
248    ///
249    /// # Safety
250    ///
251    /// * `cval` must be a valid CType; that is, when interpreted as an RType (possibly with
252    ///   tailing padding bytes), it must be a valid RType.
253    unsafe fn from_ctype(cval: CType) -> RType {
254        check_size_and_alignment::<CType, RType>();
255
256        // wrap cval in a MaybeUninit.  It is initialized right now, but will not be
257        // after the transmute_copy below.
258        let cval = mem::MaybeUninit::new(cval);
259
260        // SAFETY:
261        //  - cval is a valid instance of CType, so its bytes interpreted as RType are valid
262        //  (see docstring)
263        //  - CType is larger than RType (guaranteed by check_size_and_alignment)
264        unsafe { mem::transmute_copy(&cval) }
265    }
266}
267
268impl<RType: Sized + Default, CType: Sized> Unboxed<RType, CType> {
269    /// Call the contained function with a shared reference to the value.
270    ///
271    /// If the given pointer is NULL, the contained function is called with a reference to RType's
272    /// default value, which is subsequently dropped.
273    ///
274    /// # Safety
275    ///
276    /// * If not NULL, `cptr` must point to a valid CType value.
277    /// * No other thread may mutate the value pointed to by `cptr` until the function returns.
278    /// * Ownership of the value remains with the caller.
279    pub unsafe fn with_ref<T, F: FnOnce(&RType) -> T>(cptr: *const CType, f: F) -> T {
280        check_size_and_alignment::<CType, RType>();
281        if cptr.is_null() {
282            let nullval = RType::default();
283            return f(&nullval);
284        }
285
286        // SAFETY:
287        // - casting to a pointer type with the same alignment and smaller size
288        f(unsafe { &*(cptr as *const RType) })
289    }
290
291    /// Call the contained function with an exclusive reference to the data type.
292    ///
293    /// If the given pointer is NULL, the contained function is called with a reference to RType's
294    /// default value, which is subsequently dropped.
295    ///
296    /// # Safety
297    ///
298    /// * If not NULL, `cptr` must point to a valid CType value.
299    /// * No other thread may _access_ the value pointed to by `cptr` until the function returns.
300    /// * Ownership of the value remains with the caller.
301    pub unsafe fn with_ref_mut<T, F: FnOnce(&mut RType) -> T>(cptr: *mut CType, f: F) -> T {
302        check_size_and_alignment::<CType, RType>();
303        if cptr.is_null() {
304            let mut nullval = RType::default();
305            return f(&mut nullval);
306        }
307
308        // SAFETY:
309        // - casting to a pointer type with the same alignment and smaller size
310        f(unsafe { &mut *(cptr as *mut RType) })
311    }
312
313    /// Take a pointer to a CType and return an owned value.
314    ///
315    /// This is similar to [`Unboxed::take_ptr_nonnull`], but if given a NULL pointer will return the
316    /// default value.
317    ///
318    /// # Safety
319    ///
320    /// * If not NULL, `cptr` must point to a valid CType value.
321    /// * The memory pointed to by `cptr` is uninitialized when this function returns.
322    pub unsafe fn take_ptr(cptr: *mut CType) -> RType {
323        check_size_and_alignment::<CType, RType>();
324        if cptr.is_null() {
325            return RType::default();
326        }
327
328        // convert cptr to a reference to MaybeUninit<RType> (which is, for the moment,
329        // actually initialized)
330        // SAFETY:
331        // - casting to a pointer type with the same alignment and smaller size
332        let rref = unsafe { &mut *(cptr as *mut mem::MaybeUninit<RType>) };
333        let mut owned = mem::MaybeUninit::<RType>::zeroed();
334
335        // swap the actual value for the zeroed value
336        mem::swap(rref, &mut owned);
337
338        // SAFETY:
339        //  - owned contains what cptr was pointing to, which the caller guaranteed to be valid
340        unsafe { owned.assume_init() }
341    }
342}
343
344#[cfg(test)]
345mod test {
346    mod size_panic {
347        use super::super::*;
348        struct TwoInts(u64, u64);
349        struct OneInt(u64);
350
351        type UnboxedTwoInts = Unboxed<TwoInts, OneInt>;
352
353        #[test]
354        #[should_panic]
355        fn test() {
356            let cval = OneInt(10);
357            unsafe {
358                UnboxedTwoInts::with_ref_nonnull(&cval as *const OneInt, |_rval| {});
359            }
360        }
361    }
362
363    mod align_panic {
364        use super::super::*;
365        struct OneInt(u64);
366        struct EightBytes([u8; 8]);
367
368        type UnboxedOneInt = Unboxed<OneInt, EightBytes>;
369
370        #[test]
371        #[should_panic]
372        fn test() {
373            let cval = EightBytes([0u8; 8]);
374            unsafe {
375                UnboxedOneInt::with_ref_nonnull(&cval as *const EightBytes, |_rval| {});
376            }
377        }
378    }
379
380    use super::*;
381    #[derive(Default)]
382    struct RType(u32, u64);
383    struct CType([u64; 3]); // NOTE: larger than RType
384
385    type UnboxedTuple = Unboxed<RType, CType>;
386
387    #[test]
388    fn intialize_and_with_methods() {
389        unsafe {
390            let mut cval = mem::MaybeUninit::<CType>::uninit();
391            UnboxedTuple::to_out_param(RType(10, 20), cval.as_mut_ptr());
392            let mut cval = cval.assume_init();
393
394            UnboxedTuple::with_ref_nonnull(&cval, |rref| {
395                assert_eq!(rref.0, 10);
396                assert_eq!(rref.1, 20);
397            });
398
399            UnboxedTuple::with_ref_mut_nonnull(&mut cval, |rref| {
400                assert_eq!(rref.0, 10);
401                assert_eq!(rref.1, 20);
402                rref.0 = 30;
403            });
404
405            UnboxedTuple::with_ref_mut(&mut cval, |rref| {
406                assert_eq!(rref.0, 30);
407                rref.0 += 1;
408                assert_eq!(rref.1, 20);
409                rref.1 += 1;
410            });
411
412            UnboxedTuple::with_ref(&cval, |rref| {
413                assert_eq!(rref.0, 31);
414                assert_eq!(rref.1, 21);
415            });
416
417            let rval = UnboxedTuple::take(cval);
418            assert_eq!(rval.0, 31);
419            assert_eq!(rval.1, 21);
420
421            let mut cval = mem::MaybeUninit::<CType>::uninit();
422            UnboxedTuple::to_out_param_nonnull(RType(100, 200), cval.as_mut_ptr());
423            let cval = cval.assume_init();
424
425            let rval = UnboxedTuple::take(cval);
426            assert_eq!(rval.0, 100);
427            assert_eq!(rval.1, 200);
428        }
429    }
430
431    #[test]
432    fn with_null_ptrs() {
433        unsafe {
434            UnboxedTuple::with_ref_mut(std::ptr::null_mut(), |rref| {
435                assert_eq!(rref.0, 0);
436                assert_eq!(rref.1, 0);
437                rref.1 += 1;
438            });
439
440            UnboxedTuple::with_ref(std::ptr::null(), |rref| {
441                assert_eq!(rref.0, 0);
442                assert_eq!(rref.1, 0);
443            });
444        }
445    }
446
447    #[test]
448    #[should_panic]
449    fn with_ref_nonnull_null() {
450        unsafe {
451            UnboxedTuple::with_ref_nonnull(std::ptr::null(), |_| {});
452        }
453    }
454
455    #[test]
456    #[should_panic]
457    fn with_ref_mut_nonnull_null() {
458        unsafe {
459            UnboxedTuple::with_ref_mut_nonnull(std::ptr::null_mut(), |_| {});
460        }
461    }
462
463    #[test]
464    fn to_out_param_null() {
465        unsafe {
466            UnboxedTuple::to_out_param(RType(10, 20), std::ptr::null_mut());
467            // nothing happens
468        }
469    }
470
471    #[test]
472    #[should_panic]
473    fn to_out_param_nonnull_null() {
474        unsafe {
475            UnboxedTuple::to_out_param_nonnull(RType(10, 20), std::ptr::null_mut());
476            // nothing happens
477        }
478    }
479
480    #[test]
481    fn return_val() {
482        unsafe {
483            let cval = UnboxedTuple::return_val(RType(10, 20));
484            let rval = UnboxedTuple::take(cval);
485            assert_eq!(rval.0, 10);
486            assert_eq!(rval.1, 20);
487        }
488    }
489
490    fn take_ptr_test(nonnull: bool) {
491        unsafe {
492            // allocate enough bytes for a cval without initializing them
493            let cval = Box::new(mem::MaybeUninit::<CType>::uninit());
494            let cvalptr = Box::into_raw(cval) as *mut CType;
495
496            // initialize the value
497            UnboxedTuple::to_out_param(RType(10, 20), cvalptr);
498
499            // take the value and leave behind zeroed memory
500            let rval = if nonnull {
501                UnboxedTuple::take_ptr_nonnull(cvalptr)
502            } else {
503                UnboxedTuple::take_ptr(cvalptr)
504            };
505            assert_eq!(rval.0, 10);
506            assert_eq!(rval.1, 20);
507
508            // Verify that the memory is zeroed -- don't do this IRL!  NOTE: in practice only the
509            // non-padding bytes of the value are actually zeroed, so we cannot assert that all of
510            // the bytes pointed to by cvalptr are zero.
511            let zeroedref = unsafe { &*(cvalptr as *const RType) };
512            assert_eq!(zeroedref.0, 0);
513            assert_eq!(zeroedref.1, 0);
514
515            // deallocate by turning cvalptr back into a Box and dropping the Box, but
516            // using MaybeUninit to prevent dropping the (invalid) enclosed CType.
517            unsafe { Box::from_raw(cvalptr as *mut mem::MaybeUninit<CType>) };
518        }
519    }
520
521    #[test]
522    fn take_ptr() {
523        take_ptr_test(false);
524    }
525
526    #[test]
527    fn take_ptr_null() {
528        unsafe {
529            let rval = UnboxedTuple::take_ptr(std::ptr::null_mut());
530            assert_eq!(rval.0, 0);
531            assert_eq!(rval.1, 0);
532        }
533    }
534
535    #[test]
536    fn take_ptr_nonnull() {
537        take_ptr_test(true);
538    }
539
540    #[test]
541    #[should_panic]
542    fn take_ptr_nonnull_null() {
543        unsafe {
544            UnboxedTuple::take_ptr_nonnull(std::ptr::null_mut());
545        }
546    }
547}