Replace is_sync specialization trick with MaybeSync trait bound.

The `is_sync::<T>()` runtime check relied on implicit specialization via
`Copy`/`Clone` array behavior, which has changed in Rust 1.86+.
`UserDataRef` always taking an exclusive lock even for `Sync` userdata,
preventing concurrent shared borrows.

With the `send` feature flag enabled, userdata types must now be `Send + Sync`.
This is a breaking change, `T: Send + !Sync` userdata types can be wrapped in a `Mutex`
or used inside a `Scope` where this restriction is lifted.
This commit is contained in:
Alex Orlenko
2026-02-21 15:05:55 +00:00
parent 151adc0e87
commit 63a255bbc9
10 changed files with 57 additions and 129 deletions
+18 -31
View File
@@ -25,7 +25,7 @@ pub(crate) enum UserDataStorage<T> {
pub(crate) enum UserDataVariant<T> {
Default(XRc<UserDataCell<T>>),
#[cfg(feature = "serde")]
Serializable(XRc<UserDataCell<Box<DynSerialize>>>, bool), // bool is `is_sync`
Serializable(XRc<UserDataCell<Box<DynSerialize>>>),
}
impl<T> Clone for UserDataVariant<T> {
@@ -34,7 +34,7 @@ impl<T> Clone for UserDataVariant<T> {
match self {
Self::Default(inner) => Self::Default(XRc::clone(inner)),
#[cfg(feature = "serde")]
Self::Serializable(inner, is_sync) => Self::Serializable(XRc::clone(inner), *is_sync),
Self::Serializable(inner) => Self::Serializable(XRc::clone(inner)),
}
}
}
@@ -42,10 +42,12 @@ impl<T> Clone for UserDataVariant<T> {
impl<T> UserDataVariant<T> {
#[inline(always)]
pub(super) fn try_borrow_scoped<R>(&self, f: impl FnOnce(&T) -> R) -> Result<R> {
// We don't need to check for `T: Sync` because when this method is used (internally),
// Lua mutex is already locked.
// If non-`Sync` userdata is already borrowed by another thread (via `UserDataRef`), it will be
// exclusively locked.
// Shared (read) lock is always correct for in-place borrows:
// - this method is called internally while the Lua mutex is held, ensuring exclusive Lua-level
// access per call frame
// - with `send` feature, all owned userdata satisfies `T: Sync`, so simultaneous shared references
// from multiple threads are sound
// - without `send` feature, single-threaded execution makes shared lock safe for any `T`
let _guard = (self.raw_lock().try_lock_shared_guarded()).map_err(|_| Error::UserDataBorrowError)?;
Ok(f(unsafe { &*self.as_ptr() }))
}
@@ -80,7 +82,7 @@ impl<T> UserDataVariant<T> {
Ok(match self {
Self::Default(inner) => XRc::into_inner(inner).unwrap().value.into_inner(),
#[cfg(feature = "serde")]
Self::Serializable(inner, _) => unsafe {
Self::Serializable(inner) => unsafe {
let raw = Box::into_raw(XRc::into_inner(inner).unwrap().value.into_inner());
*Box::from_raw(raw as *mut T)
},
@@ -92,7 +94,7 @@ impl<T> UserDataVariant<T> {
match self {
Self::Default(inner) => XRc::strong_count(inner),
#[cfg(feature = "serde")]
Self::Serializable(inner, _) => XRc::strong_count(inner),
Self::Serializable(inner) => XRc::strong_count(inner),
}
}
@@ -101,7 +103,7 @@ impl<T> UserDataVariant<T> {
match self {
Self::Default(inner) => &inner.raw_lock,
#[cfg(feature = "serde")]
Self::Serializable(inner, _) => &inner.raw_lock,
Self::Serializable(inner) => &inner.raw_lock,
}
}
@@ -110,7 +112,7 @@ impl<T> UserDataVariant<T> {
match self {
Self::Default(inner) => inner.value.get(),
#[cfg(feature = "serde")]
Self::Serializable(inner, _) => unsafe { &mut **(inner.value.get() as *mut Box<T>) },
Self::Serializable(inner) => unsafe { &mut **(inner.value.get() as *mut Box<T>) },
}
}
}
@@ -119,24 +121,10 @@ impl<T> UserDataVariant<T> {
impl Serialize for UserDataStorage<()> {
fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
match self {
Self::Owned(variant @ UserDataVariant::Serializable(inner, is_sync)) => unsafe {
#[cfg(feature = "send")]
if *is_sync {
let _guard = (variant.raw_lock().try_lock_shared_guarded())
.map_err(|_| serde::ser::Error::custom(Error::UserDataBorrowError))?;
(*inner.value.get()).serialize(serializer)
} else {
let _guard = (variant.raw_lock().try_lock_exclusive_guarded())
.map_err(|_| serde::ser::Error::custom(Error::UserDataBorrowError))?;
(*inner.value.get()).serialize(serializer)
}
#[cfg(not(feature = "send"))]
{
let _ = is_sync;
let _guard = (variant.raw_lock().try_lock_shared_guarded())
.map_err(|_| serde::ser::Error::custom(Error::UserDataBorrowError))?;
(*inner.value.get()).serialize(serializer)
}
Self::Owned(variant @ UserDataVariant::Serializable(inner)) => unsafe {
let _guard = (variant.raw_lock().try_lock_shared_guarded())
.map_err(|_| serde::ser::Error::custom(Error::UserDataBorrowError))?;
(*inner.value.get()).serialize(serializer)
},
_ => Err(serde::ser::Error::custom("cannot serialize <userdata>")),
}
@@ -201,11 +189,10 @@ impl<T: 'static> UserDataStorage<T> {
#[inline(always)]
pub(crate) fn new_ser(data: T) -> Self
where
T: Serialize + crate::types::MaybeSend,
T: Serialize + crate::types::MaybeSend + crate::types::MaybeSync,
{
let data = Box::new(data) as Box<DynSerialize>;
let is_sync = super::util::is_sync::<T>();
let variant = UserDataVariant::Serializable(XRc::new(UserDataCell::new(data)), is_sync);
let variant = UserDataVariant::Serializable(XRc::new(UserDataCell::new(data)));
Self::Owned(variant)
}
+4 -6
View File
@@ -12,7 +12,6 @@ use crate::value::Value;
use super::cell::{UserDataStorage, UserDataVariant};
use super::lock::{LockGuard, RawLock, UserDataLock};
use super::util::is_sync;
#[cfg(feature = "userdata-wrappers")]
use {
@@ -63,11 +62,10 @@ impl<T> TryFrom<UserDataVariant<T>> for UserDataRef<T> {
#[inline]
fn try_from(variant: UserDataVariant<T>) -> Result<Self> {
let guard = if cfg!(not(feature = "send")) || is_sync::<T>() {
variant.raw_lock().try_lock_shared_guarded()
} else {
variant.raw_lock().try_lock_exclusive_guarded()
};
// Shared (read) lock is always correct:
// - with `send` feature, `T: Sync` is guaranteed by the `MaybeSync` bound on userdata creation
// - without `send` feature, single-threaded access makes shared lock safe for any `T`
let guard = variant.raw_lock().try_lock_shared_guarded();
let guard = guard.map_err(|_| Error::UserDataBorrowError)?;
let guard = unsafe { mem::transmute::<LockGuard<_>, LockGuard<'static, _>>(guard) };
Ok(UserDataRef::from_parts(UserDataRefInner::Default(variant), guard))
+6
View File
@@ -654,6 +654,12 @@ macro_rules! lua_userdata_impl {
// A special proxy object for UserData
pub(crate) struct UserDataProxy<T>(pub(crate) PhantomData<T>);
// `UserDataProxy` holds no real `T` value, only a type marker, so it is always safe to send/share.
#[cfg(feature = "send")]
unsafe impl<T> Send for UserDataProxy<T> {}
#[cfg(feature = "send")]
unsafe impl<T> Sync for UserDataProxy<T> {}
lua_userdata_impl!(UserDataProxy<T>);
#[cfg(all(feature = "userdata-wrappers", not(feature = "send")))]
-31
View File
@@ -1,6 +1,4 @@
use std::any::TypeId;
use std::cell::Cell;
use std::marker::PhantomData;
use std::os::raw::c_int;
use std::ptr;
@@ -11,35 +9,6 @@ use crate::error::{Error, Result};
use crate::types::CallbackPtr;
use crate::util::{get_userdata, rawget_field, rawset_field, take_userdata};
// This is a trick to check if a type is `Sync` or not.
// It uses leaked specialization feature from stdlib.
struct IsSync<'a, T> {
is_sync: &'a Cell<bool>,
_marker: PhantomData<T>,
}
impl<T> Clone for IsSync<'_, T> {
fn clone(&self) -> Self {
self.is_sync.set(false);
IsSync {
is_sync: self.is_sync,
_marker: PhantomData,
}
}
}
impl<T: Sync> Copy for IsSync<'_, T> {}
pub(crate) fn is_sync<T>() -> bool {
let is_sync = Cell::new(true);
let _ = [IsSync::<T> {
is_sync: &is_sync,
_marker: PhantomData,
}]
.clone();
is_sync.get()
}
// Userdata type hints, used to match types of wrapped userdata
#[derive(Clone, Copy)]
pub(crate) struct TypeIdHints {