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}