31 releases (12 stable)
| new 2.0.9 | Feb 13, 2026 |
|---|---|
| 2.0.5 | Oct 26, 2025 |
| 1.0.3 | Aug 28, 2025 |
| 0.5.3 | Aug 13, 2025 |
#268 in Concurrency
41KB
455 lines
phylactery 2.0.9
Crafted through a vile ritual, a phylactery is a magical receptacle that holds a necromancer's soul, permanently binding it to the mortal world as an immortal lich.
Safe and thin wrappers around lifetime extension to allow non-static values to cross static boundaries.
In Brief
- Wrap a value
TwithSoul<T>::new(value). - Pin the
Soulwithcore::pin::pin!orBox/Arc/Rc::pin. - Bind
Lich<dyn Trait>to theSoulwithsoul.bind::<dyn Trait>()(whereTraitis a trait implemented byT). - Use the
Lichin a lifetime-extended context (such as crossing astd::thread::spawn'staticboundary or storing it in astaticvariable). - Make sure to drop all
Liches before dropping theSoul. - On drop, the
Soulwill block the thread until allLiches are dropped, potentially creating a deadlock condition (in the name of memory safety).
Since this library makes use of some unsafe code, all tests are run with miri to try to catch any unsoundness.
This library supports #[no_std] (use default-features = false in your 'Cargo.toml').
Examples
examples/thread_spawn_bridge.rs
/// Trivially reimplement [`thread::scope`] in a more powerful way.
///
/// Contrary to other `scope` solutions, here, the captured reference can be
/// returned (as a [`Soul<T>`]) while the threads continue to execute.
#[cfg(feature = "shroud")]
pub mod thread_spawn_bridge {
use core::{num::NonZeroUsize, pin::Pin};
use phylactery::Soul;
use std::thread;
pub fn broadcast<F: Fn(usize) + Send + Sync>(
parallelism: NonZeroUsize,
function: F,
) -> Pin<Box<Soul<F>>> {
// Pin the `Soul` to the heap to be able to return it.
let soul = Box::pin(Soul::new(function));
// Spawn a bunch of threads that will all call `F`.
for index in 0..parallelism.get() {
// `Soul::bind` requires a pinning.
let lich = soul.as_ref().bind::<dyn Fn(usize) + Send + Sync>();
// The non-static function `F` crosses a `'static` boundary protected by the
// `Lich` and is called on another thread. `Send/Sync` requirements still apply.
thread::spawn(move || lich(index));
}
// The `Soul` continues to track the captured `F` and will guarantee that it
// becomes inaccessible when it itself drops.
//
// If a `Lich` bound to this `Soul` still lives at the time of drop,
// `<Soul as Drop>::drop` will block the current thread until all `Lich`es are
// dropped.
soul
}
}
fn main() {
#[cfg(feature = "shroud")]
thread_spawn_bridge::broadcast(
std::thread::available_parallelism().unwrap_or(core::num::NonZeroUsize::MIN),
&|index| println!("{index}"),
);
}
examples/scoped_static_logger.rs
/// Implements a thread local scoped logger available from anywhere that can
/// borrow values that live on the stack.
#[cfg(feature = "shroud")]
pub mod scoped_static_logger {
use core::{cell::RefCell, fmt::Display, pin::pin};
use phylactery::{Lich, Soul, shroud};
// Use the convenience macro to automatically implement the required `Shroud`
// trait for all `T: Log`.
#[shroud]
pub trait Log {
fn parent(&self) -> Option<&dyn Log>;
fn prefix(&self) -> &str;
fn format(&self) -> &str;
fn arguments(&self) -> &[&dyn Display];
}
pub struct Logger<'a> {
parent: Option<&'a dyn Log>,
prefix: &'a str,
format: &'a str,
arguments: &'a [&'a dyn Display],
}
impl Log for Logger<'_> {
fn parent(&self) -> Option<&dyn Log> {
self.parent
}
fn prefix(&self) -> &str {
self.prefix
}
fn format(&self) -> &str {
self.format
}
fn arguments(&self) -> &[&dyn Display] {
self.arguments
}
}
// This thread local storage allows preserving this thread's call stack while
// being able to log from anywhere without the need to pass a logger around.
//
// Note that the `Lich<dyn Log>` can have the `'static` lifetime.
thread_local! {
static LOGGER: RefCell<Option<Lich<dyn Log>>> = RefCell::default();
}
pub fn scope<T: Display, F: FnOnce(&T)>(prefix: &str, argument: &T, function: F) {
let parent = LOGGER.take();
{
// This `Logger` captures some references that live on the stack.
let logger = Logger {
parent: parent.as_deref(),
prefix,
format: "({})",
arguments: &[argument],
};
// The `Soul` must be pinned since `Lich`es will refer to its memory.
let soul = pin!(Soul::new(logger));
// The `Lich` is bound to the `Soul` as a `dyn Trait` wrapper.
let lich = soul.as_ref().bind::<dyn Log>();
// Push this logger as the current scope.
// The non-static `Logger` crosses a `'static` boundary.
LOGGER.set(Some(lich));
// Call the function.
function(argument);
// Pop the logger.
LOGGER.take().expect("`Lich` has been pushed");
// If a `Lich` bound to this `Soul` still lives at the time of drop,
// `<Soul as Drop>::drop` will block the current thread until all
// `Lich`es are dropped.
}
// Put back the old logger.
LOGGER.set(parent);
}
}
fn main() {
#[cfg(feature = "shroud")]
scoped_static_logger::scope("some-prefix", &37, |value| {
assert_eq!(*value, 37);
});
}
See the examples and tests folder for more detailed examples.
Contribute
- If you find a bug or have a feature request, please open an issues.
phylacteryis actively maintained and pull requests are welcome.- If
phylacterywas useful to you, please consider leaving a star!
Dependencies
~0–11MB
~52K SLoC