Skip to content

Commit c5ef04a

Browse files
committed
Refactor LevelFilterLayout to avoid unsafe code
1 parent 6b18f80 commit c5ef04a

File tree

2 files changed

+51
-28
lines changed

2 files changed

+51
-28
lines changed

spdlog/src/level.rs

Lines changed: 49 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
use std::{
22
fmt::{self, Debug},
3-
mem,
4-
mem::{align_of, size_of},
53
str::FromStr,
64
sync::atomic::Ordering,
75
};
86

9-
use crate::{utils::const_assert, Error};
7+
use crate::Error;
108

119
pub(crate) const LOG_LEVEL_NAMES: [&str; Level::count()] =
1210
["critical", "error", "warn", "info", "debug", "trace"];
@@ -188,11 +186,24 @@ impl FromStr for Level {
188186
}
189187
}
190188

189+
#[repr(u16)]
190+
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, bytemuck::NoUninit)]
191+
enum LevelFilterDiscriminant {
192+
Off,
193+
Equal,
194+
NotEqual,
195+
MoreSevere,
196+
MoreSevereEqual,
197+
MoreVerbose,
198+
MoreVerboseEqual,
199+
All,
200+
}
201+
191202
/// Represents log level logical filter conditions.
192203
///
193204
/// Use [`LevelFilter::test`] method to check if a [`Level`] satisfies the
194205
/// filter condition.
195-
#[repr(u16, align(4))]
206+
#[repr(align(4))]
196207
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
197208
pub enum LevelFilter {
198209
/// Disables all levels.
@@ -277,13 +288,17 @@ impl LevelFilter {
277288
}
278289

279290
#[must_use]
280-
fn discriminant(&self) -> u16 {
281-
// SAFETY:
282-
// Because `LevelFilter` is marked `repr(u16)`, its layout is a `repr(C)`
283-
// `union` between `repr(C)` structs, each of which has the `u16` discriminant
284-
// as its first field, so we can read the discriminant without offsetting the
285-
// pointer.
286-
unsafe { *<*const _>::from(self).cast::<u16>() }
291+
fn discriminant(&self) -> LevelFilterDiscriminant {
292+
match self {
293+
Self::Off => LevelFilterDiscriminant::Off,
294+
Self::Equal(_) => LevelFilterDiscriminant::Equal,
295+
Self::NotEqual(_) => LevelFilterDiscriminant::NotEqual,
296+
Self::MoreSevere(_) => LevelFilterDiscriminant::MoreSevere,
297+
Self::MoreSevereEqual(_) => LevelFilterDiscriminant::MoreSevereEqual,
298+
Self::MoreVerbose(_) => LevelFilterDiscriminant::MoreVerbose,
299+
Self::MoreVerboseEqual(_) => LevelFilterDiscriminant::MoreVerboseEqual,
300+
Self::All => LevelFilterDiscriminant::All,
301+
}
287302
}
288303

289304
#[must_use]
@@ -312,11 +327,17 @@ impl From<log::LevelFilter> for LevelFilter {
312327

313328
// Atomic
314329

315-
// This struct must have the same memory layout as `LevelFilter`.
330+
// This is a version of `LevelFilter` that does not contain uninitialized bytes.
331+
// For variants like `LevelFilter::{Off,All}`, since they have no field, their
332+
// field memory space is treated as uninitialized bytes. `Atomic<T>` from the
333+
// `atomic` crate uses `std::mem::transmute`, and its operations on
334+
// uninitialized bytes are undefined behavior. Therefore, we created this
335+
// `LevelFilterLayout` type, where uninitialized bytes are filled with
336+
// `UNDEFINED_FALLBACK`, enabling safe atomic operations.
316337
#[repr(C, align(4))]
317338
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, bytemuck::NoUninit)]
318339
struct LevelFilterLayout {
319-
discriminant: u16, // Keep the type in sync with the repr of `LevelFilter`
340+
discriminant: LevelFilterDiscriminant,
320341
level: Level,
321342
}
322343

@@ -326,8 +347,6 @@ impl LevelFilterLayout {
326347

327348
impl From<LevelFilter> for LevelFilterLayout {
328349
fn from(value: LevelFilter) -> Self {
329-
// Use `mem::transmute` here is undefined behavior, because `LevelFilter`
330-
// contains uninitialized bytes.
331350
Self {
332351
discriminant: value.discriminant(),
333352
level: value.level().unwrap_or(Self::UNDEFINED_FALLBACK),
@@ -336,22 +355,20 @@ impl From<LevelFilter> for LevelFilterLayout {
336355
}
337356

338357
impl From<LevelFilterLayout> for LevelFilter {
339-
fn from(inner: LevelFilterLayout) -> Self {
340-
// SAFETY:
341-
// - Repr of `LevelFilter` is C union.
342-
// - Repr of `LevelFilterLayout` is C struct, it has the same memory layout as
343-
// `LevelFilter`.
344-
// - `LevelFilterLayout` doesn't contain uninitialized bytes.
345-
unsafe { mem::transmute::<_, LevelFilter>(inner) }
358+
fn from(layout: LevelFilterLayout) -> Self {
359+
match layout.discriminant {
360+
LevelFilterDiscriminant::Off => Self::Off,
361+
LevelFilterDiscriminant::Equal => Self::Equal(layout.level),
362+
LevelFilterDiscriminant::NotEqual => Self::NotEqual(layout.level),
363+
LevelFilterDiscriminant::MoreSevere => Self::MoreSevere(layout.level),
364+
LevelFilterDiscriminant::MoreSevereEqual => Self::MoreSevereEqual(layout.level),
365+
LevelFilterDiscriminant::MoreVerbose => Self::MoreVerbose(layout.level),
366+
LevelFilterDiscriminant::MoreVerboseEqual => Self::MoreVerboseEqual(layout.level),
367+
LevelFilterDiscriminant::All => Self::All,
368+
}
346369
}
347370
}
348371

349-
// If these assertions are not satisfied, `mem::transmute` may result in
350-
// undefined behavior.
351-
const_assert!(size_of::<Level>() * 2 == size_of::<LevelFilter>());
352-
const_assert!(align_of::<LevelFilterLayout>() == align_of::<LevelFilter>());
353-
const_assert!(size_of::<LevelFilterLayout>() == size_of::<LevelFilter>());
354-
355372
/// Atomic version of [`LevelFilter`] which can be safely shared between
356373
/// threads.
357374
pub struct AtomicLevelFilter {
@@ -410,11 +427,15 @@ impl Debug for AtomicLevelFilter {
410427

411428
#[cfg(test)]
412429
mod tests {
430+
use std::mem::size_of;
431+
413432
use super::*;
433+
use crate::utils::const_assert;
414434

415435
const_assert!(atomic::Atomic::<Level>::is_lock_free());
416436
const_assert!(atomic::Atomic::<LevelFilter>::is_lock_free());
417437
const_assert!(atomic::Atomic::<LevelFilterLayout>::is_lock_free());
438+
const_assert!(size_of::<Level>() * 2 == size_of::<LevelFilter>());
418439

419440
#[test]
420441
fn from_usize() {

spdlog/src/utils/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ pub fn open_file_bufw(
4444
}
4545

4646
// Credits `static_assertions` crate
47+
#[allow(unused_macros)]
4748
macro_rules! const_assert {
4849
( $cond:expr $(,)? ) => {
4950
const _: [(); 0 - !{
@@ -52,4 +53,5 @@ macro_rules! const_assert {
5253
} as usize] = [];
5354
};
5455
}
56+
#[allow(unused_imports)]
5557
pub(crate) use const_assert;

0 commit comments

Comments
 (0)