#[ffi_export]
Auto-generated sanity checks
The whole design of the ReprC
trait, i.e., a trait that expresses that a
type has a C layout, i.e., that it has an associated "raw"
C type (types with no validity invariants whatsoever), means that
the actual #[no_mangle]
-exported function is one using the associated
C types in its function signature. This ensures that a foreign call
to such functions (i.e., C calling into that function) will not directly
trigger "instant UB", contrary to a hand-crafted definition.
-
Indeed, if you were to export a function such as:
#[repr(C)] enum LogLevel { Error, Warning, Info, Debug, } #[no_mangle] pub extern "C" fn set_log_level (level: LogLevel) { // ... }
then C code calling
set_log_level
with a value different to the four only possible discriminants ofLogLevel
(0, 1, 2, 3
in this case) would instantly trigger Undefined Behavior no matter what the body ofset_log_level
would be.Instead, when using
safer_ffi
, the following code:use ::safer_ffi::prelude::*; #[derive_ReprC] #[repr(u8)] // Associated CType: a plain `u8` enum LogLevel { Error, Warning, Info, Debug, } #[ffi_export] fn set_log_level (level: LogLevel) { // ... }
unsugars to (something along the lines of):
fn set_log_level (level: LogLevel) { // ... } mod hidden { #[no_mangle] pub unsafe extern "C" fn set_log_level (level: u8) { match ::safer_ffi::layout::from_raw(level) { | Some(level /* : LogLevel */) => { super::set_log_level(level) }, | None => { // Got an invalid `LogLevel` bit-pattern if compile_time_condition() { eprintln!("Got an invalid `LogLevel` bit-pattern"); abort(); } else { use ::std::hint::unreachable_unchecked as UB; UB() } }, } } }
So, basically, there is an attempt to transmute
the input
C type to the expected ReprC
type, but such attempt can fail
if the auto-generated sanity-check detects that so doing would not be
safe (e.g., input integer corresponding to no enum
variant, NULL pointer
when the ReprC
type is guaranteed not to be it, unaligned pointer when
the ReprC
type is guaranteed to be aligned).
Caveats
Such check cannot be exhaustive (in the case of pointers for instance, safer_ffi
cannot possibly know if it is valid to dereference a non-null and well-aligned
pointer). This means that there are still cases where UB can be triggered
nevertheless, hence it being named a sanity check and not a safety check.
- Only in the case of a (field-less)
enum
cansafer_ffi
ensure lack of UB no matter the (integral) C type instance given as input.
As you may notice by looking at the code, there is a compile_time_condition()
to actually abort
instead of triggering UB. This means that when such
condition is not met, UB is actually triggered and we are back to the
#[no_mangle]
case.
This is by design: such runtime checks may have a performance impact that some programmers may deem unacceptable, so it is logical that there be some escape hatch in that regard.
As of this writing, compile_time_condition()
is currently
cfg!(debug_assertions)
, which means that by default such checks are
disabled on release.
-
This is not optimal safety-wise, since the default configuration is too loose. The author of the crate is aware of that and intending to replace that with:
- an
unsafe
attribute parameter that would nevertheless only truly opt-out whendebug_assertions
are disabled.
- an