-
-
Notifications
You must be signed in to change notification settings - Fork 224
Create nothreads-compatible thread local #1105
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,8 +17,7 @@ pub use sys::out; | |
|
||
#[cfg(feature = "trace")] | ||
pub use crate::meta::trace; | ||
#[cfg(all(debug_assertions, not(wasm_nothreads)))] | ||
use std::cell::RefCell; | ||
use std::cell::{Cell, RefCell}; | ||
|
||
use crate::global::godot_error; | ||
use crate::meta::error::CallError; | ||
|
@@ -321,12 +320,12 @@ pub(crate) fn has_error_print_level(level: u8) -> bool { | |
/// Internal type used to store context information for debug purposes. Debug context is stored on the thread-local | ||
/// ERROR_CONTEXT_STACK, which can later be used to retrieve the current context in the event of a panic. This value | ||
/// probably shouldn't be used directly; use ['get_gdext_panic_context()'](get_gdext_panic_context) instead. | ||
#[cfg(all(debug_assertions, not(wasm_nothreads)))] | ||
#[cfg(debug_assertions)] | ||
struct ScopedFunctionStack { | ||
functions: Vec<*const dyn Fn() -> String>, | ||
} | ||
|
||
#[cfg(all(debug_assertions, not(wasm_nothreads)))] | ||
#[cfg(debug_assertions)] | ||
impl ScopedFunctionStack { | ||
/// # Safety | ||
/// Function must be removed (using [`pop_function()`](Self::pop_function)) before lifetime is invalidated. | ||
|
@@ -351,19 +350,285 @@ impl ScopedFunctionStack { | |
} | ||
} | ||
|
||
#[cfg(all(debug_assertions, not(wasm_nothreads)))] | ||
thread_local! { | ||
/// A thread local which adequately behaves as a global variable when compiling under `experimental-wasm-nothreads`, as it does | ||
/// not support thread locals. Aims to support similar APIs as [`std::thread::LocalKey`]. | ||
pub(crate) struct GodotThreadLocal<T: 'static> { | ||
#[cfg(not(wasm_nothreads))] | ||
threaded_val: &'static std::thread::LocalKey<T>, | ||
|
||
#[cfg(wasm_nothreads)] | ||
non_threaded_val: std::cell::OnceCell<T>, | ||
|
||
#[cfg(wasm_nothreads)] | ||
initializer: fn() -> T, | ||
} | ||
|
||
// SAFETY: there can only be one thread with `wasm_nothreads`. | ||
#[cfg(wasm_nothreads)] | ||
unsafe impl<T: 'static> Sync for GodotThreadLocal<T> {} | ||
|
||
impl<T: 'static> GodotThreadLocal<T> { | ||
#[cfg(not(wasm_nothreads))] | ||
pub const fn new_threads(key: &'static std::thread::LocalKey<T>) -> Self { | ||
Self { threaded_val: key } | ||
} | ||
|
||
#[cfg(wasm_nothreads)] | ||
pub const fn new_nothreads(initializer: fn() -> T) -> Self { | ||
Self { | ||
non_threaded_val: std::cell::OnceCell::new(), | ||
initializer, | ||
} | ||
} | ||
|
||
/// Acquires a reference to the value in this TLS key. | ||
/// | ||
/// See [`std::thread::LocalKey::with`] for details. | ||
pub fn with<F, R>(&'static self, f: F) -> R | ||
where | ||
F: FnOnce(&T) -> R, | ||
{ | ||
#[cfg(not(wasm_nothreads))] | ||
return self.threaded_val.with(f); | ||
|
||
#[cfg(wasm_nothreads)] | ||
f(self.non_threaded_val.get_or_init(self.initializer)) | ||
} | ||
|
||
/// Acquires a reference to the value in this TLS key. | ||
/// | ||
/// See [`std::thread::LocalKey::try_with`] for details. | ||
#[allow(dead_code)] | ||
#[inline] | ||
pub fn try_with<F, R>(&'static self, f: F) -> Result<R, std::thread::AccessError> | ||
where | ||
F: FnOnce(&T) -> R, | ||
{ | ||
#[cfg(not(wasm_nothreads))] | ||
return self.threaded_val.try_with(f); | ||
|
||
#[cfg(wasm_nothreads)] | ||
Ok(self.with(f)) | ||
} | ||
} | ||
|
||
#[allow(dead_code)] | ||
impl<T: 'static> GodotThreadLocal<Cell<T>> { | ||
/// Sets or initializes the contained value. | ||
/// | ||
/// See [`std::thread::LocalKey::set`] for details. | ||
pub fn set(&'static self, value: T) { | ||
#[cfg(not(wasm_nothreads))] | ||
return self.threaded_val.set(value); | ||
|
||
// According to `LocalKey` docs, this method must not call the default initializer. | ||
#[cfg(wasm_nothreads)] | ||
if let Some(initialized) = self.non_threaded_val.get() { | ||
initialized.set(value); | ||
} else { | ||
self.non_threaded_val.get_or_init(|| Cell::new(value)); | ||
} | ||
} | ||
|
||
/// Returns a copy of the contained value. | ||
/// | ||
/// See [`std::thread::LocalKey::get`] for details. | ||
pub fn get(&'static self) -> T | ||
where | ||
T: Copy, | ||
{ | ||
#[cfg(not(wasm_nothreads))] | ||
return self.threaded_val.get(); | ||
|
||
#[cfg(wasm_nothreads)] | ||
self.with(Cell::get) | ||
} | ||
|
||
/// Takes the contained value, leaving `Default::default()` in its place. | ||
/// | ||
/// See [`std::thread::LocalKey::take`] for details. | ||
pub fn take(&'static self) -> T | ||
where | ||
T: Default, | ||
{ | ||
#[cfg(not(wasm_nothreads))] | ||
return self.threaded_val.take(); | ||
|
||
#[cfg(wasm_nothreads)] | ||
self.with(Cell::take) | ||
} | ||
|
||
/// Replaces the contained value, returning the old value. | ||
/// | ||
/// See [`std::thread::LocalKey::replace`] for details. | ||
pub fn replace(&'static self, value: T) -> T { | ||
#[cfg(not(wasm_nothreads))] | ||
return self.threaded_val.replace(value); | ||
|
||
#[cfg(wasm_nothreads)] | ||
self.with(|cell| cell.replace(value)) | ||
} | ||
} | ||
|
||
#[allow(dead_code)] | ||
impl<T: 'static> GodotThreadLocal<RefCell<T>> { | ||
/// Acquires a reference to the contained value. | ||
/// | ||
/// See [`std::thread::LocalKey::with_borrow`] for details. | ||
pub fn with_borrow<F, R>(&'static self, f: F) -> R | ||
where | ||
F: FnOnce(&T) -> R, | ||
{ | ||
#[cfg(not(wasm_nothreads))] | ||
return self.threaded_val.with_borrow(f); | ||
|
||
#[cfg(wasm_nothreads)] | ||
self.with(|cell| f(&cell.borrow())) | ||
} | ||
|
||
/// Acquires a mutable reference to the contained value. | ||
/// | ||
/// See [`std::thread::LocalKey::with_borrow_mut`] for details. | ||
pub fn with_borrow_mut<F, R>(&'static self, f: F) -> R | ||
where | ||
F: FnOnce(&mut T) -> R, | ||
{ | ||
#[cfg(not(wasm_nothreads))] | ||
return self.threaded_val.with_borrow_mut(f); | ||
|
||
#[cfg(wasm_nothreads)] | ||
self.with(|cell| f(&mut cell.borrow_mut())) | ||
} | ||
|
||
/// Sets or initializes the contained value. | ||
/// | ||
/// See [`std::thread::LocalKey::set`] for details. | ||
pub fn set(&'static self, value: T) { | ||
#[cfg(not(wasm_nothreads))] | ||
return self.threaded_val.set(value); | ||
|
||
// According to `LocalKey` docs, this method must not call the default initializer. | ||
#[cfg(wasm_nothreads)] | ||
if let Some(initialized) = self.non_threaded_val.get() { | ||
*initialized.borrow_mut() = value; | ||
} else { | ||
self.non_threaded_val.get_or_init(|| RefCell::new(value)); | ||
} | ||
} | ||
|
||
/// Takes the contained value, leaving `Default::default()` in its place. | ||
/// | ||
/// See [`std::thread::LocalKey::take`] for details. | ||
pub fn take(&'static self) -> T | ||
where | ||
T: Default, | ||
{ | ||
#[cfg(not(wasm_nothreads))] | ||
return self.threaded_val.take(); | ||
|
||
#[cfg(wasm_nothreads)] | ||
self.with(RefCell::take) | ||
} | ||
|
||
/// Replaces the contained value, returning the old value. | ||
/// | ||
/// See [`std::thread::LocalKey::replace`] for details. | ||
pub fn replace(&'static self, value: T) -> T { | ||
#[cfg(not(wasm_nothreads))] | ||
return self.threaded_val.replace(value); | ||
|
||
#[cfg(wasm_nothreads)] | ||
self.with(|cell| cell.replace(value)) | ||
} | ||
} | ||
|
||
impl<T: 'static> std::fmt::Debug for GodotThreadLocal<T> { | ||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
f.debug_struct("GodotThreadLocal").finish_non_exhaustive() | ||
} | ||
} | ||
|
||
#[cfg(not(wasm_nothreads))] | ||
macro_rules! godot_thread_local { | ||
// empty (base case for the recursion) | ||
() => {}; | ||
|
||
($(#[$attr:meta])* $vis:vis static $name:ident: $ty:ty = const $init:block; $($rest:tt)*) => { | ||
$crate::private::godot_thread_local!($(#[$attr])* $vis static $name: $ty = const $init); | ||
$crate::private::godot_thread_local!($($rest)*); | ||
}; | ||
|
||
($(#[$attr:meta])* $vis:vis static $name:ident: $ty:ty = const $init:block) => { | ||
$(#[$attr])* | ||
$vis static $name: $crate::private::GodotThreadLocal<$ty> = { | ||
::std::thread_local! { | ||
static $name: $ty = const $init | ||
} | ||
|
||
$crate::private::GodotThreadLocal::new_threads(&$name) | ||
}; | ||
}; | ||
|
||
($(#[$attr:meta])* $vis:vis static $name:ident: $ty:ty = $init:expr; $($rest:tt)*) => { | ||
$crate::private::godot_thread_local!($(#[$attr])* $vis static $name: $ty = $init); | ||
$crate::private::godot_thread_local!($($rest)*); | ||
}; | ||
|
||
($(#[$attr:meta])* $vis:vis static $name:ident: $ty:ty = $init:expr) => { | ||
$(#[$attr])* | ||
$vis static $name: $crate::private::GodotThreadLocal<$ty> = { | ||
::std::thread_local! { | ||
static $name: $ty = $init | ||
} | ||
|
||
$crate::private::GodotThreadLocal::new_threads(&$name) | ||
}; | ||
}; | ||
} | ||
|
||
#[cfg(wasm_nothreads)] | ||
macro_rules! godot_thread_local { | ||
// empty (base case for the recursion) | ||
() => {}; | ||
|
||
($(#[$attr:meta])* $vis:vis static $name:ident: $ty:ty = const $init:block; $($rest:tt)*) => { | ||
$crate::private::godot_thread_local!($(#[$attr])* $vis static $name: $ty = const $init); | ||
$crate::private::godot_thread_local!($($rest)*); | ||
}; | ||
|
||
($(#[$attr:meta])* $vis:vis static $name:ident: $ty:ty = const $init:block) => { | ||
$(#[$attr])* | ||
$vis static $name: $crate::private::GodotThreadLocal<$ty> = | ||
$crate::private::GodotThreadLocal::new_nothreads(|| $init); | ||
}; | ||
|
||
($(#[$attr:meta])* $vis:vis static $name:ident: $ty:ty = $init:expr; $($rest:tt)*) => { | ||
$crate::private::godot_thread_local!($(#[$attr])* $vis static $name: $ty = $init); | ||
$crate::private::godot_thread_local!($($rest)*); | ||
}; | ||
|
||
($(#[$attr:meta])* $vis:vis static $name:ident: $ty:ty = $init:expr) => { | ||
$(#[$attr])* | ||
$vis static $name: $crate::private::GodotThreadLocal<$ty> = | ||
$crate::private::GodotThreadLocal::new_nothreads(|| $init); | ||
}; | ||
} | ||
Comment on lines
+551
to
+615
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Some questions here:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll note that I took the macro code structure from stdlib for consistency, which felt like the easiest route for correctness at first. One goal here is that the macro should be drop-in compatible with the original one. But we can certainly deliver some minor improvements.
Sure, though I wonder if
Seems like a non-recursive approach would force all thread locals in the same block to be either
Technically they are not identical since the
We would have to require a semicolon, yeah. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fair points, then let's only do the improvements that have no downsides. |
||
|
||
pub(crate) use godot_thread_local; | ||
|
||
#[cfg(debug_assertions)] | ||
godot_thread_local! { | ||
static ERROR_CONTEXT_STACK: RefCell<ScopedFunctionStack> = const { | ||
RefCell::new(ScopedFunctionStack { functions: Vec::new() }) | ||
} | ||
} | ||
|
||
// Value may return `None`, even from panic hook, if called from a non-Godot thread. | ||
pub fn get_gdext_panic_context() -> Option<String> { | ||
#[cfg(all(debug_assertions, not(wasm_nothreads)))] | ||
#[cfg(debug_assertions)] | ||
return ERROR_CONTEXT_STACK.with(|cell| cell.borrow().get_last()); | ||
|
||
#[cfg(not(all(debug_assertions, not(wasm_nothreads))))] | ||
#[cfg(not(debug_assertions))] | ||
None | ||
} | ||
|
||
|
@@ -378,10 +643,10 @@ where | |
E: Fn() -> String, | ||
F: FnOnce() -> R + std::panic::UnwindSafe, | ||
{ | ||
#[cfg(not(all(debug_assertions, not(wasm_nothreads))))] | ||
let _ = error_context; // Unused in Release or `wasm_nothreads` builds. | ||
#[cfg(not(debug_assertions))] | ||
let _ = error_context; // Unused in Release. | ||
|
||
#[cfg(all(debug_assertions, not(wasm_nothreads)))] | ||
#[cfg(debug_assertions)] | ||
ERROR_CONTEXT_STACK.with(|cell| unsafe { | ||
// SAFETY: &error_context is valid for lifetime of function, and is removed from LAST_ERROR_CONTEXT before end of function. | ||
cell.borrow_mut().push_function(&error_context) | ||
|
@@ -390,7 +655,7 @@ where | |
let result = | ||
std::panic::catch_unwind(code).map_err(|payload| extract_panic_message(payload.as_ref())); | ||
|
||
#[cfg(all(debug_assertions, not(wasm_nothreads)))] | ||
#[cfg(debug_assertions)] | ||
ERROR_CONTEXT_STACK.with(|cell| cell.borrow_mut().pop_function()); | ||
result | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This differentiation occurs a dozen times, is it not possible to abstract?
Something like
where
value()
is#[cfg]
ed?Or is the
with
essential in all methods?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm using
with
even fornothreads
to keep the default initialization logic confined towith
, for convenience (I used to have a separate method like the one you propose, but it felt like a waste of space). Also, this let me simply copy and paste the important part of the implementation of each method from stdlib, giving me confidence that I'm not messing it up. :pThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry I might have misunderstood - if you mean that we could replace all cfg calls with
.value()
, then no, as LocalKey imposes the usage of.with
with a callback to read the value.Some of the cfgs are not really required (particularly some on the
RefCell
andCell
helpers), but I felt like keeping them in case some implementation detail on their side changes in a future rust version.But we can remove them if you prefer, as the current implementation should be identical in these cases.