Skip to content

Commit

Permalink
add LazyId
Browse files Browse the repository at this point in the history
  • Loading branch information
matsadler committed Mar 9, 2023
1 parent de53ce4 commit 65fea42
Show file tree
Hide file tree
Showing 4 changed files with 210 additions and 1 deletion.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
### Added
- `value::Opaque` can be used to wrap a Ruby type to make it `Send` + `Sync`.
- `value::OpaqueId` is a `Send` + `Sync` version of `value::Id`.
- `value::LazyId` is an `Id` with a `const` constructor so can be assigned to a
`static` and derefs to `OpaqueId`.
- The `#[magnus(opaque_attr_reader)]` attribute can be set on `Opaque` wrapped
fields of a struct when deriving `TypedData` to generate a method to return
the inner value.
Expand Down
12 changes: 11 additions & 1 deletion src/symbol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use crate::{
try_convert::TryConvert,
value::{
private::{self, ReprValue as _},
Id, NonZeroValue, OpaqueId, ReprValue, StaticSymbol, Value,
Id, LazyId, NonZeroValue, OpaqueId, ReprValue, StaticSymbol, Value,
},
Ruby,
};
Expand Down Expand Up @@ -281,6 +281,16 @@ impl PartialEq<OpaqueId> for Symbol {
}
}

impl PartialEq<LazyId> for Symbol {
/// # Panics
///
/// Panics if the first call is from a non-Ruby thread. The `LazyId` will
/// then be *poisoned* and all future use of it will panic.
fn eq(&self, other: &LazyId) -> bool {
self.as_static().map(|s| s == *other).unwrap_or(false)
}
}

impl PartialEq<StaticSymbol> for Symbol {
fn eq(&self, other: &StaticSymbol) -> bool {
self.as_static().map(|s| s == *other).unwrap_or(false)
Expand Down
187 changes: 187 additions & 0 deletions src/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,18 @@ mod flonum;

use std::{
borrow::{Borrow, Cow},
cell::UnsafeCell,
convert::TryFrom,
ffi::CStr,
fmt,
hash::{Hash, Hasher},
marker::PhantomData,
mem::transmute,
num::NonZeroUsize,
ops::{Deref, DerefMut},
os::raw::{c_char, c_int, c_long, c_ulong},
ptr,
sync::Once,
};

#[cfg(ruby_use_flonum)]
Expand Down Expand Up @@ -2271,6 +2274,16 @@ impl PartialEq<OpaqueId> for StaticSymbol {
}
}

impl PartialEq<LazyId> for StaticSymbol {
/// # Panics
///
/// Panics if the first call is from a non-Ruby thread. The `LazyId` will
/// then be *poisoned* and all future use of it will panic.
fn eq(&self, other: &LazyId) -> bool {
OpaqueId::from(*self) == *other
}
}

impl PartialEq<Symbol> for StaticSymbol {
fn eq(&self, other: &Symbol) -> bool {
other.as_static().map(|o| *self == o).unwrap_or(false)
Expand Down Expand Up @@ -2472,6 +2485,12 @@ impl IntoId for Symbol {
}
}

impl IntoValue for Id {
fn into_value_with(self, handle: &Ruby) -> Value {
StaticSymbol::from(self).into_value_with(handle)
}
}

impl From<Symbol> for Id {
fn from(sym: Symbol) -> Self {
sym.into_id_with(&Ruby::get_with(sym))
Expand All @@ -2484,6 +2503,12 @@ impl PartialEq<OpaqueId> for Id {
}
}

impl PartialEq<LazyId> for Id {
fn eq(&self, other: &LazyId) -> bool {
self.0 == (*other).0
}
}

impl PartialEq<StaticSymbol> for Id {
fn eq(&self, other: &StaticSymbol) -> bool {
*self == other.into_id_with(&Ruby::get_with(*other))
Expand Down Expand Up @@ -2560,6 +2585,12 @@ impl PartialEq<Id> for OpaqueId {
}
}

impl PartialEq<LazyId> for OpaqueId {
fn eq(&self, other: &LazyId) -> bool {
*self == Self::from(**other)
}
}

impl PartialEq<StaticSymbol> for OpaqueId {
fn eq(&self, other: &StaticSymbol) -> bool {
*self == other.into_id_with(&Ruby::get_with(*other))
Expand All @@ -2571,3 +2602,159 @@ impl PartialEq<Symbol> for OpaqueId {
other.as_static().map(|o| *self == o).unwrap_or(false)
}
}

/// An [`Id`] that can be assigned to a `static` and [`Deref`]s to [`OpaqueId`].
pub struct LazyId {
init: Once,
inner: UnsafeCell<LazyInner>,
}

union LazyInner {
name: &'static str,
value: OpaqueId,
}

impl LazyId {
/// Create a new `LazyId`.
pub const fn new(name: &'static str) -> Self {
Self {
init: Once::new(),
inner: UnsafeCell::new(LazyInner { name }),
}
}

/// Force evaluation of a `LazyId`.
///
/// # Panics
///
/// Panics if the `LazyId` is *poisoned*. See [`LazyId::get`].
pub fn force(this: &Self, handle: &Ruby) {
Self::get_with(this, handle);
}

/// Get an [`OpaqueId`] from a `LazyId`.
///
/// # Panics
///
/// Panics if the first call is from a non-Ruby thread. The `LazyId` will
/// then be *poisoned* and all future use of it will panic.
pub fn get(this: &Self) -> OpaqueId {
*this.deref()
}

/// Get a [`Id`] from a `LazyId`.
///
/// # Panics
///
/// Panics if the `LazyId` is *poisoned*. See [`LazyId::get`].
pub fn get_with(this: &Self, handle: &Ruby) -> Id {
unsafe {
this.init.call_once(|| {
let inner = this.inner.get();
(*inner).value = handle.intern((*inner).name).into();
});
(*this.inner.get()).value.into_id_with(handle)
}
}

/// Get an [`OpaqueId`] from a `LazyId`, if it has already been evaluated.
///
/// This function will not call Ruby and will not initialise the inner
/// `OpaqueId`. If the `LazyId` has not yet been initialised, returns
/// `None`.
///
/// This function will not panic, if the `LazyId` is *poisoned* it will
/// return `None`.
pub fn try_get(this: &Self) -> Option<OpaqueId> {
unsafe { this.init.is_completed().then(|| (*this.inner.get()).value) }
}
}

unsafe impl Send for LazyId {}
unsafe impl Sync for LazyId {}

impl fmt::Debug for LazyId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
#[allow(non_camel_case_types)]
#[derive(Debug)]
struct uninit();

f.debug_tuple("LazyId")
.field(
Self::try_get(self)
.as_ref()
.map(|v| v as &dyn fmt::Debug)
.unwrap_or(&uninit()),
)
.finish()
}
}

impl Deref for LazyId {
type Target = OpaqueId;

/// # Panics
///
/// Panics if the first call is from a non-Ruby thread. This `LazyId` will
/// then be *poisoned* and all future use of it will panic.
fn deref(&self) -> &Self::Target {
unsafe {
self.init.call_once(|| {
let inner = self.inner.get();
(*inner).value = Ruby::get().unwrap().intern((*inner).name).into();
});
&(*self.inner.get()).value
}
}
}

impl Hash for LazyId {
/// # Panics
///
/// Panics if the first call is from a non-Ruby thread. This `LazyId` will
/// then be *poisoned* and all future use of it will panic.
fn hash<H: Hasher>(&self, state: &mut H) {
self.deref().hash(state);
}
}

impl PartialEq for LazyId {
/// # Panics
///
/// Panics if the first call is from a non-Ruby thread. This `LazyId` will
/// then be *poisoned* and all future use of it will panic.
fn eq(&self, other: &Self) -> bool {
self.deref() == other.deref()
}
}
impl Eq for LazyId {}

impl PartialEq<Id> for LazyId {
/// # Panics
///
/// Panics if the first call is from a non-Ruby thread. This `LazyId` will
/// then be *poisoned* and all future use of it will panic.
fn eq(&self, other: &Id) -> bool {
*self.deref() == *other
}
}

impl PartialEq<StaticSymbol> for LazyId {
/// # Panics
///
/// Panics if the first call is from a non-Ruby thread. This `LazyId` will
/// then be *poisoned* and all future use of it will panic.
fn eq(&self, other: &StaticSymbol) -> bool {
*self == other.into_id_with(&Ruby::get_with(*other))
}
}

impl PartialEq<Symbol> for LazyId {
/// # Panics
///
/// Panics if the first call is from a non-Ruby thread. This `LazyId` will
/// then be *poisoned* and all future use of it will panic.
fn eq(&self, other: &Symbol) -> bool {
other.as_static().map(|o| *self == o).unwrap_or(false)
}
}
10 changes: 10 additions & 0 deletions tests/lazy_id.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use magnus::{rb_assert, value::LazyId};

static FOO: LazyId = LazyId::new("foo");

#[test]
fn it_works() {
let _ruby = unsafe { magnus::embed::init() };

rb_assert!("foo == :foo", foo = *FOO);
}

0 comments on commit 65fea42

Please sign in to comment.