Macro core::ub_checks::assert_unsafe_precondition

source ·
macro_rules! assert_unsafe_precondition {
    ($kind:ident, $message:expr, ($($name:ident:$ty:ty = $arg:expr),*$(,)?) => $e:expr $(,)?) => { ... };
}
🔬This is a nightly-only experimental API. (ub_checks)
Expand description

Checks that the preconditions of an unsafe function are followed.

The check is enabled at runtime if debug assertions are enabled when the caller is monomorphized. In const-eval/Miri checks implemented with this macro for language UB are always ignored.

This macro should be called as assert_unsafe_precondition!(check_{library,language}_ub, "message", (ident: type = expr, ident: type = expr) => check_expr) where each expr will be evaluated and passed in as function argument ident: type. Then all those arguments are passed to a function with the body check_expr. Pick check_language_ub when this is guarding a violation of language UB, i.e., immediate UB according to the Rust Abstract Machine. Pick check_library_ub when this is guarding a violation of a documented library precondition that does not immediately lead to language UB.

If check_library_ub is used but the check is actually guarding language UB, the check will slow down const-eval/Miri and we’ll get the panic message instead of the interpreter’s nice diagnostic, but our ability to detect UB is unchanged. But if check_language_ub is used when the check is actually for library UB, the check is omitted in const-eval/Miri and thus if we eventually execute language UB which relies on the library UB, the backtrace Miri reports may be far removed from original cause.

These checks are behind a condition which is evaluated at codegen time, not expansion time like debug_assert. This means that a standard library built with optimizations and debug assertions disabled will have these checks optimized out of its monomorphizations, but if a caller of the standard library has debug assertions enabled and monomorphizes an expansion of this macro, that monomorphization will contain the check.

Since these checks cannot be optimized out in MIR, some care must be taken in both call and implementation to mitigate their compile-time overhead. Calls to this macro always expand to this structure:

ⓘ
if ::core::intrinsics::check_language_ub() {
    precondition_check(args)
}

where precondition_check is monomorphic with the attributes #[rustc_nounwind], #[inline] and #[rustc_no_mir_inline]. This combination of attributes ensures that the actual check logic is compiled only once and generates a minimal amount of IR because the check cannot be inlined in MIR, but can be inlined and fully optimized by a codegen backend.

Callers should avoid introducing any other let bindings or any code outside this macro in order to call it. Since the precompiled standard library is built with full debuginfo and these variables cannot be optimized out in MIR, an innocent-looking let can produce enough debuginfo to have a measurable compile-time impact on debug builds.