phylactery

Crates.iophylactery
lib.rsphylactery
version2.0.2
created_at2025-08-10 21:44:02.083139+00
updated_at2025-09-08 03:44:13.926788+00
descriptionSafe and thin wrappers around lifetime extension to allow non-static values to cross static boundaries.
homepage
repositoryhttps://github.com/Magicolo/phylactery
max_upload_size
id1789327
size53,689
Magicolo (Magicolo)

documentation

https://docs.rs/phylactery/latest/phylactery/

README

phylactery 2.0.2

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 T with Soul<T>::new(value).
  • Pin the Soul with core::pin::pin! or Box/Arc/Rc::pin.
  • Bind Lich<dyn Trait> to the Soul with soul.bind::<dyn Trait>() (where Trait is a trait implemented by T).
  • Use the Lich in a lifetime-extended context (such as crossing a std::thread::spawn 'static boundary or storing it in a static variable).
  • Make sure to drop all Liches before dropping the Soul.
  • On drop, the Soul will block the thread until all Liches 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.
  • phylactery is actively maintained and pull requests are welcome.
  • If phylactery was useful to you, please consider leaving a star!

Commit count: 156

cargo fmt