bevy/crates/bevy_ecs/src/intern.rs
Zachary Harrold d70595b667
Add core and alloc over std Lints (#15281)
# Objective

- Fixes #6370
- Closes #6581

## Solution

- Added the following lints to the workspace:
  - `std_instead_of_core`
  - `std_instead_of_alloc`
  - `alloc_instead_of_core`
- Used `cargo +nightly fmt` with [item level use
formatting](https://rust-lang.github.io/rustfmt/?version=v1.6.0&search=#Item%5C%3A)
to split all `use` statements into single items.
- Used `cargo clippy --workspace --all-targets --all-features --fix
--allow-dirty` to _attempt_ to resolve the new linting issues, and
intervened where the lint was unable to resolve the issue automatically
(usually due to needing an `extern crate alloc;` statement in a crate
root).
- Manually removed certain uses of `std` where negative feature gating
prevented `--all-features` from finding the offending uses.
- Used `cargo +nightly fmt` with [crate level use
formatting](https://rust-lang.github.io/rustfmt/?version=v1.6.0&search=#Crate%5C%3A)
to re-merge all `use` statements matching Bevy's previous styling.
- Manually fixed cases where the `fmt` tool could not re-merge `use`
statements due to conditional compilation attributes.

## Testing

- Ran CI locally

## Migration Guide

The MSRV is now 1.81. Please update to this version or higher.

## Notes

- This is a _massive_ change to try and push through, which is why I've
outlined the semi-automatic steps I used to create this PR, in case this
fails and someone else tries again in the future.
- Making this change has no impact on user code, but does mean Bevy
contributors will be warned to use `core` and `alloc` instead of `std`
where possible.
- This lint is a critical first step towards investigating `no_std`
options for Bevy.

---------

Co-authored-by: François Mockers <francois.mockers@vleue.com>
2024-09-27 00:59:59 +00:00

279 lines
8 KiB
Rust

//! Provides types used to statically intern immutable values.
//!
//! Interning is a pattern used to save memory by deduplicating identical values,
//! speed up code by shrinking the stack size of large types,
//! and make comparisons for any type as fast as integers.
use core::{fmt::Debug, hash::Hash, ops::Deref};
use std::sync::{OnceLock, PoisonError, RwLock};
use bevy_utils::HashSet;
/// An interned value. Will stay valid until the end of the program and will not drop.
///
/// For details on interning, see [the module level docs](self).
///
/// # Comparisons
///
/// Interned values use reference equality, meaning they implement [`Eq`]
/// and [`Hash`] regardless of whether `T` implements these traits.
/// Two interned values are only guaranteed to compare equal if they were interned using
/// the same [`Interner`] instance.
// NOTE: This type must NEVER implement Borrow since it does not obey that trait's invariants.
/// ```
/// # use bevy_ecs::intern::*;
/// #[derive(PartialEq, Eq, Hash, Debug)]
/// struct Value(i32);
/// impl Internable for Value {
/// // ...
/// # fn leak(&self) -> &'static Self { Box::leak(Box::new(Value(self.0))) }
/// # fn ref_eq(&self, other: &Self) -> bool { std::ptr::eq(self, other ) }
/// # fn ref_hash<H: std::hash::Hasher>(&self, state: &mut H) { std::ptr::hash(self, state); }
/// }
/// let interner_1 = Interner::new();
/// let interner_2 = Interner::new();
/// // Even though both values are identical, their interned forms do not
/// // compare equal as they use different interner instances.
/// assert_ne!(interner_1.intern(&Value(42)), interner_2.intern(&Value(42)));
/// ```
pub struct Interned<T: ?Sized + 'static>(pub &'static T);
impl<T: ?Sized> Deref for Interned<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
self.0
}
}
impl<T: ?Sized> Clone for Interned<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T: ?Sized> Copy for Interned<T> {}
// Two Interned<T> should only be equal if they are clones from the same instance.
// Therefore, we only use the pointer to determine equality.
impl<T: ?Sized + Internable> PartialEq for Interned<T> {
fn eq(&self, other: &Self) -> bool {
self.0.ref_eq(other.0)
}
}
impl<T: ?Sized + Internable> Eq for Interned<T> {}
// Important: This must be kept in sync with the PartialEq/Eq implementation
impl<T: ?Sized + Internable> Hash for Interned<T> {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.0.ref_hash(state);
}
}
impl<T: ?Sized + Debug> Debug for Interned<T> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
self.0.fmt(f)
}
}
impl<T> From<&Interned<T>> for Interned<T> {
fn from(value: &Interned<T>) -> Self {
*value
}
}
/// A trait for internable values.
///
/// This is used by [`Interner<T>`] to create static references for values that are interned.
pub trait Internable: Hash + Eq {
/// Creates a static reference to `self`, possibly leaking memory.
fn leak(&self) -> &'static Self;
/// Returns `true` if the two references point to the same value.
fn ref_eq(&self, other: &Self) -> bool;
/// Feeds the reference to the hasher.
fn ref_hash<H: core::hash::Hasher>(&self, state: &mut H);
}
impl Internable for str {
fn leak(&self) -> &'static Self {
let str = self.to_owned().into_boxed_str();
Box::leak(str)
}
fn ref_eq(&self, other: &Self) -> bool {
self.as_ptr() == other.as_ptr() && self.len() == other.len()
}
fn ref_hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.len().hash(state);
self.as_ptr().hash(state);
}
}
/// A thread-safe interner which can be used to create [`Interned<T>`] from `&T`
///
/// For details on interning, see [the module level docs](self).
///
/// The implementation ensures that two equal values return two equal [`Interned<T>`] values.
///
/// To use an [`Interner<T>`], `T` must implement [`Internable`].
pub struct Interner<T: ?Sized + 'static>(OnceLock<RwLock<HashSet<&'static T>>>);
impl<T: ?Sized> Interner<T> {
/// Creates a new empty interner
pub const fn new() -> Self {
Self(OnceLock::new())
}
}
impl<T: Internable + ?Sized> Interner<T> {
/// Return the [`Interned<T>`] corresponding to `value`.
///
/// If it is called the first time for `value`, it will possibly leak the value and return an
/// [`Interned<T>`] using the obtained static reference. Subsequent calls for the same `value`
/// will return [`Interned<T>`] using the same static reference.
pub fn intern(&self, value: &T) -> Interned<T> {
let lock = self.0.get_or_init(Default::default);
{
let set = lock.read().unwrap_or_else(PoisonError::into_inner);
if let Some(value) = set.get(value) {
return Interned(*value);
}
}
{
let mut set = lock.write().unwrap_or_else(PoisonError::into_inner);
if let Some(value) = set.get(value) {
Interned(*value)
} else {
let leaked = value.leak();
set.insert(leaked);
Interned(leaked)
}
}
}
}
impl<T: ?Sized> Default for Interner<T> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use core::hash::{Hash, Hasher};
use std::collections::hash_map::DefaultHasher;
use crate::intern::{Internable, Interned, Interner};
#[test]
fn zero_sized_type() {
#[derive(PartialEq, Eq, Hash, Debug)]
pub struct A;
impl Internable for A {
fn leak(&self) -> &'static Self {
&A
}
fn ref_eq(&self, other: &Self) -> bool {
core::ptr::eq(self, other)
}
fn ref_hash<H: Hasher>(&self, state: &mut H) {
core::ptr::hash(self, state);
}
}
let interner = Interner::default();
let x = interner.intern(&A);
let y = interner.intern(&A);
assert_eq!(x, y);
}
#[test]
fn fieldless_enum() {
#[derive(PartialEq, Eq, Hash, Debug, Clone)]
pub enum A {
X,
Y,
}
impl Internable for A {
fn leak(&self) -> &'static Self {
match self {
A::X => &A::X,
A::Y => &A::Y,
}
}
fn ref_eq(&self, other: &Self) -> bool {
core::ptr::eq(self, other)
}
fn ref_hash<H: Hasher>(&self, state: &mut H) {
core::ptr::hash(self, state);
}
}
let interner = Interner::default();
let x = interner.intern(&A::X);
let y = interner.intern(&A::Y);
assert_ne!(x, y);
}
#[test]
fn static_sub_strings() {
let str = "ABC ABC";
let a = &str[0..3];
let b = &str[4..7];
// Same contents
assert_eq!(a, b);
let x = Interned(a);
let y = Interned(b);
// Different pointers
assert_ne!(x, y);
let interner = Interner::default();
let x = interner.intern(a);
let y = interner.intern(b);
// Same pointers returned by interner
assert_eq!(x, y);
}
#[test]
fn same_interned_instance() {
let a = Interned("A");
let b = a;
assert_eq!(a, b);
let mut hasher = DefaultHasher::default();
a.hash(&mut hasher);
let hash_a = hasher.finish();
let mut hasher = DefaultHasher::default();
b.hash(&mut hasher);
let hash_b = hasher.finish();
assert_eq!(hash_a, hash_b);
}
#[test]
fn same_interned_content() {
let a = Interned::<str>(Box::leak(Box::new("A".to_string())));
let b = Interned::<str>(Box::leak(Box::new("A".to_string())));
assert_ne!(a, b);
}
#[test]
fn different_interned_content() {
let a = Interned::<str>("A");
let b = Interned::<str>("B");
assert_ne!(a, b);
}
}