Skip to content

Commit

Permalink
[unsafe-fields] Initial commit
Browse files Browse the repository at this point in the history
TODO:
- Use in zerocopy (maybe in a follow-up commit?)
- Fill out `Cargo.toml` with more fields
- Include `build.rs` to enable version detection so we can support
  `as_mut` as a `const fn` on recent toolchains
- Confirm that it's sound to derive `Copy` and `Clone`

Closes #1931

gherrit-pr-id: If0e198c377137dd941ebd5dc68787766a593e1eb
  • Loading branch information
joshlf committed Oct 21, 2024
1 parent 35d9d4f commit 86df2f2
Show file tree
Hide file tree
Showing 3 changed files with 350 additions and 0 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ testutil = { path = "testutil" }
# sometimes change the output format slightly, so a version mismatch can cause
# CI test failures.
trybuild = { version = "=1.0.90", features = ["diff"] }
unsafe-fields = { path = "./unsafe-fields" }
# In tests, unlike in production, zerocopy-derive is not optional
zerocopy-derive = { version = "=0.9.0-alpha.0", path = "zerocopy-derive" }
# TODO(#381) Remove this dependency once we have our own layout gadgets.
Expand Down
12 changes: 12 additions & 0 deletions unsafe-fields/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Copyright 2024 The Fuchsia Authors
#
# Licensed under a BSD-style license <LICENSE-BSD>, Apache License, Version 2.0
# <LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0>, or the MIT
# license <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your option.
# This file may not be copied, modified, or distributed except according to
# those terms.

[package]
name = "unsafe-fields"
version = "0.1.0"
edition = "2021"
337 changes: 337 additions & 0 deletions unsafe-fields/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,337 @@
// Copyright 2024 The Fuchsia Authors
//
// Licensed under a BSD-style license <LICENSE-BSD>, Apache License, Version 2.0
// <LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0>, or the MIT
// license <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your option.
// This file may not be copied, modified, or distributed except according to
// those terms.

//! Support for unsafe fields.
//!
//! This crate provides the [`unsafe_fields!`] macro, which can be used to mark
//! fields as unsafe. Unsafe fields automatically have their types wrapped using
//! the [`Unsafe`] wrapper type. An `Unsafe` is intended to be used to for
//! struct, enum, or union fields which carry safety invariants. All accessors
//! are `unsafe`, which requires any use of an `Unsafe` field to be inside an
//! `unsafe` block.
//!
//! An unsafe field has the type `Unsafe<O, F, const NAME_HASH: u128>`. `O` is
//! the enclosing type (struct, enum, or union), `F` is the type of the field,
//! and `NAME_HASH` is the hash of the field's name. `O` prevents swapping
//! unsafe fields of the same `F` type between different enclosing types, and
//! `NAME_HASH` prevents swapping different fields of the same `F` type within
//! the same enclosing type. Note that swapping the same field between instances
//! of the same type [cannot be prevented](crate#limitations).
//!
//! # Examples
//!
//! ```
//! use unsafe_fields::{unsafe_fields, Unsafe};
//!
//! unsafe_fields! {
//! /// A `usize` which is guaranteed to be even.
//! pub struct EvenUsize {
//! // INVARIANT: `n` is even.
//! #[unsafe]
//! n: usize,
//! }
//! }
//!
//! impl EvenUsize {
//! /// Constructs a new `EvenUsize`.
//! ///
//! /// Returns `None` if `n` is odd.
//! pub fn new(n: usize) -> Option<EvenUsize> {
//! if n % 2 != 0 {
//! return None;
//! }
//! // SAFETY: We just confirmed that `n` is even.
//! let n = unsafe { Unsafe::new(n) };
//! Some(EvenUsize { n })
//! }
//! }
//! ```
//!
//! Attempting to swap unsafe fields of the same type is prevented:
//!
//! ```compile_fail,E0308
//! use unsafe_fields::{unsafe_fields, Unsafe};
//!
//! unsafe_fields! {
//! /// A range.
//! pub struct Range {
//! // INVARIANT: `lo <= hi`.
//! #[unsafe]
//! lo: usize,
//! #[unsafe]
//! hi: usize,
//! }
//! }
//!
//! impl Range {
//! pub fn swap(&mut self) {
//! // ERROR: Mismatched types
//! core::mem::swap(&mut self.lo, &mut self.hi);
//! }
//! }
//! ```
//!
//! # Limitations
//!
//! Note that we cannot prevent `Unsafe`s from being swapped between the same
//! field in instances of the same type:
//!
//! ```
//! use unsafe_fields::{unsafe_fields, Unsafe};
//!
//! unsafe_fields! {
//! /// A `usize` which is guaranteed to be even.
//! pub struct EvenUsize {
//! // INVARIANT: `n` is even.
//! #[unsafe]
//! n: usize,
//! }
//! }
//!
//! pub fn swap(a: &mut EvenUsize, b: &mut EvenUsize) {
//! core::mem::swap(&mut a.n, &mut b.n);
//! }
//! ```

use core::marker::PhantomData;

/// A field with safety invariants.
///
/// `Unsafe` should not be named directly - instead, use [`unsafe_fields!`] to
/// declare a type with unsafe fields.
///
/// See the [crate-level documentation](crate) for more information.
#[derive(Copy, Clone)] // TODO: We need Copy in case user types need to be Copy. Is this sound?
#[repr(transparent)]
pub struct Unsafe<O: ?Sized, F: ?Sized, const NAME_HASH: u128> {
_marker: PhantomData<O>,
field: F,
}

impl<O: ?Sized, F: ?Sized, const NAME_HASH: u128> Unsafe<O, F, { NAME_HASH }> {
/// Gets a reference to the inner value.
///
/// # Safety
///
/// The caller is responsible for upholding any safety invariants associated
/// with this field.
#[inline(always)]
pub const unsafe fn as_ref(&self) -> &F {
&self.field
}

/// Gets a mutable reference to the inner value.
///
/// # Safety
///
/// The caller is responsible for upholding any safety invariants associated
/// with this field.
#[inline(always)]
pub unsafe fn as_mut(&mut self) -> &mut F {
&mut self.field
}
}

impl<O: ?Sized, F, const NAME_HASH: u128> Unsafe<O, F, { NAME_HASH }> {
/// Constructs a new `Unsafe<O, F, NAME_HASH>`.
///
/// # Safety
///
/// The caller is responsible for upholding any safety invariants associated
/// with this field.
#[inline(always)]
pub const unsafe fn new(field: F) -> Unsafe<O, F, { NAME_HASH }> {
Unsafe { _marker: PhantomData, field }
}

/// Extracts the inner `F` from `self`.
///
/// # Safety
///
/// The caller is responsible for upholding any safety invariants associated
/// with this field.
#[inline(always)]
pub const unsafe fn into(self) -> F {
use core::mem::ManuallyDrop;

let slf = ManuallyDrop::new(self);

#[repr(C)]
union Transmute<O: ?Sized, F, const NAME_HASH: u128> {
src: ManuallyDrop<Unsafe<O, F, { NAME_HASH }>>,
dst: ManuallyDrop<F>,
}

// SAFETY: `ManuallyDrop<Unsafe<_, F, _>>` has the same size and bit
// validity as `Unsafe<_, F, _>`. [1] `Unsafe<_, F, _>` is
// `#[repr(transparent)]` and has no other fields, and so it has the
// same size and bit validity as `F`.
//
// [1] Per https://doc.rust-lang.org/1.81.0/core/mem/struct.ManuallyDrop.html:
//
// `ManuallyDrop<T>` is guaranteed to have the same layout and bit
// validity as `T`
let dst = unsafe { Transmute { src: slf }.dst };
ManuallyDrop::into_inner(dst)
}
}

/// Defines a type with unsafe fields.
///
/// See the [crate-level documentation](crate) for more information.
// TODO: Allow specifying *which* fields are unsafe.
#[macro_export]
macro_rules! unsafe_fields {
($(#[$attr:meta])* $vis:vis struct $name:ident {
$($(#[$field_attr:tt])? $field:ident: $field_ty:ty),* $(,)?
}) => {
$(#[$attr])*
$vis struct $name {
$(
$field: unsafe_fields!(@field $(#[$field_attr])? $field: $field_ty),
)*
}
};
(@field #[unsafe] $field:ident: $field_ty:ty) => {
$crate::Unsafe<Self, $field_ty, {$crate::macro_util::hash_field_name(stringify!($field))}>
};
(@field $_field:ident: $field_ty:ty) => {
$field_ty
}
}

#[doc(hidden)]
pub mod macro_util {
// TODO: Implement a stronger hash function so we can basically just ignore
// collisions. If users encounter collisions in practice, we can just deal
// with it then, publish a new version, and tell them to upgrade.
pub const fn hash_field_name(field_name: &str) -> u128 {
// An implementation of FxHasher, although returning a u128. Probably
// not as strong as it could be, but probably more collision resistant
// than normal 64-bit FxHasher.
let field_name = field_name.as_bytes();
let mut hash = 0u128;
let mut i = 0;
while i < field_name.len() {
// This is just FxHasher's `0x517cc1b727220a95` constant
// concatenated back-to-back.
const K: u128 = 0x517cc1b727220a95517cc1b727220a95;
hash = (hash.rotate_left(5) ^ (field_name[i] as u128)).wrapping_mul(K);
i += 1;
}
hash
}
}

#[cfg(test)]
mod tests {
use core::mem;

use super::*;

unsafe_fields! {
/// A `Foo`.
#[allow(unused)]
struct Foo {
#[unsafe]
a: usize,
b: usize,
}
}

unsafe_fields! {
/// A `Bar`.
#[allow(unused)]
struct Bar {
#[unsafe]
a: usize,
#[unsafe]
b: usize,
}
}

#[test]
fn test_unsafe_fieds() {
let mut foo = Foo { a: unsafe { Unsafe::new(0) }, b: 0 };
let mut bar = Bar { a: unsafe { Unsafe::new(0) }, b: unsafe { Unsafe::new(0) } };

// These all fail to compile:
// mem::swap(&mut foo.a, &mut foo.b);
// mem::swap(&mut foo.a, &mut bar.a);
// mem::swap(&mut bar.a, &mut bar.b);
}
}

/// This module exists so that we can use rustdoc to perform compile-fail tests
/// rather than having to set up an entire trybuild set suite.
///
/// ```compile_fail,E0308
/// use unsafe_fields::*;
///
/// unsafe_fields! {
/// struct Foo {
/// #[unsafe]
/// a: usize,
/// b: usize,
/// }
/// }
///
/// impl Foo {
/// // Swapping an unsafe field with a non-unsafe field is a compile error.
/// fn swap(&mut self) {
/// core::mem::swap(&mut self.a, &mut self.b);
/// }
/// }
/// ```
///
/// ```compile_fail,E0308
/// use unsafe_fields::*;
///
/// unsafe_fields! {
/// struct Foo {
/// #[unsafe]
/// a: usize,
/// #[unsafe]
/// b: usize,
/// }
/// }
///
/// impl Foo {
/// // Swapping an unsafe field with another unsafe field is a compile
/// // error.
/// fn swap(&mut self) {
/// core::mem::swap(&mut self.a, &mut self.b);
/// }
/// }
/// ```
///
/// ```compile_fail,E0308
/// use unsafe_fields::*;
///
/// unsafe_fields! {
/// struct Foo {
/// #[unsafe]
/// a: usize,
/// }
/// }
///
/// unsafe_fields! {
/// struct Bar {
/// #[unsafe]
/// a: usize,
/// }
/// }
///
/// // Swapping identically-named unsafe fields from different types is a
/// // compile error.
/// fn swap(foo: &mut Foo, bar: &mut Bar) {
/// core::mem::swap(&mut foo.a, &mut bar.a);
/// }
/// ```
#[doc(hidden)]
pub mod compile_fail {}

0 comments on commit 86df2f2

Please sign in to comment.