Как вы можете сделать безопасный статический синглтон в Rust?

Это что-то спорная тема, так что позвольте мне начать с объяснением моего случая использования, а затем говорить о реальной проблеме.

Я нахожу, что для кучи небезопасных вещей важно убедиться, что вы не просачиваете память; это на самом деле довольно легко сделать, если вы начнете использовать transmute() и forget(). Например, передавая экземпляр в коробке на C-код в течение произвольного промежутка времени, затем извлекая его обратно и "воскрешая его", используя transmute.

Представьте, что у меня есть безопасная оболочка для такого API:

trait Foo {}
struct CBox;

impl CBox {
    /// Stores value in a bound C api, forget(value)
    fn set<T: Foo>(value: T) {
        // ...
    }

    /// Periodically call this and maybe get a callback invoked
    fn poll(_: Box<Fn<(EventType, Foo), ()> + Send>) {
        // ...
    }
}

impl Drop for CBox {
    fn drop(&mut self) {
        // Safely load all saved Foo here and discard them, preventing memory leaks
    }
}

Для проверки этого на самом деле не происходит утечка какой-либо памяти, я хочу, чтобы некоторые тесты были такими:

#[cfg(test)]
mod test {

    struct IsFoo;
    impl Foo for IsFoo {}
    impl Drop for IsFoo {
        fn drop(&mut self) {
            Static::touch();
        }
    }

    #[test]
    fn test_drops_actually_work() {
        guard = Static::lock(); // Prevent any other use of Static concurrently
        Static::reset(); // Set to zero
        {
            let c = CBox;
            c.set(IsFoo);
            c.set(IsFoo);
            c.poll(/*...*/);
        }
        assert!(Static::get() == 2); // Assert that all expected drops were invoked
        guard.release();
    }
}

Как вы можете создать этот тип статического одноэлементного объекта?

Он должен использовать блокировку защиты стиля Semaphore, чтобы гарантировать, что несколько тестов не выполняются одновременно, а затем без всякого доступа к некоторому статическому изменяемому значению.

Я подумал, что возможно эта реализация будет работать, но практически говоря, она терпит неудачу, потому что изредка условия гонки приводят к дублированию выполнения init:

/// Global instance
static mut INSTANCE_LOCK: bool = false;
static mut INSTANCE: *mut StaticUtils = 0 as *mut StaticUtils;
static mut WRITE_LOCK: *mut Semaphore = 0 as *mut Semaphore;
static mut LOCK: *mut Semaphore = 0 as *mut Semaphore;

/// Generate instances if they don't exist
unsafe fn init() {
    if !INSTANCE_LOCK {
        INSTANCE_LOCK = true;
        INSTANCE = transmute(box StaticUtils::new());
        WRITE_LOCK = transmute(box Semaphore::new(1));
        LOCK = transmute(box Semaphore::new(1));
    }
}

Обратите внимание, что в отличие от обычной программы, в которой вы можете быть уверены, что ваша точка входа (основная) всегда работает в одной задаче, тестовый бегун в Rust не предлагает какой-либо отдельной точки входа, как это.

Другое, очевидно, чем указание максимального количества задач; учитывая десятки тестов, только небольшая потребность в этом, и это медленно и бессмысленно ограничивать пул тестовых задач одним только для этого случая.

Ответы

Ответ 1

Он выглядит как прецедент для std::sync::Once:

use std::sync::{Once, ONCE_INIT};
static INIT: Once = ONCE_INIT;

Затем в ваших тестах звоните

INIT.doit(|| unsafe { init(); });

Once гарантирует, что ваш init будет выполняться только один раз, независимо от того, сколько раз вы вызываете INIT.doit().

Ответ 2

См. также lazy_static, что делает вещи немного более эргономичными. Это по существу то же самое, что и статическая Once для каждой переменной, но обертывает ее типом, который реализует Deref, чтобы вы могли получить к нему доступ, как обычную ссылку.

Использование выглядит следующим образом (из документации):

#[macro_use]
extern crate lazy_static;

use std::collections::HashMap;

lazy_static! {
    static ref HASHMAP: HashMap<u32, &'static str> = {
        let mut m = HashMap::new();
        m.insert(0, "foo");
        m.insert(1, "bar");
        m.insert(2, "baz");
        m
    };
    static ref COUNT: usize = HASHMAP.len();
    static ref NUMBER: u32 = times_two(21);
}

fn times_two(n: u32) -> u32 { n * 2 }

fn main() {
    println!("The map has {} entries.", *COUNT);
    println!("The entry for `0` is \"{}\".", HASHMAP.get(&0).unwrap());
    println!("A expensive calculation on a static results in: {}.", *NUMBER);
}

Обратите внимание, что autoderef означает, что вам даже не нужно использовать * всякий раз, когда вы вызываете метод для вашей статической переменной. Эта переменная будет инициализирована при первом Deref 'd.

Однако, lazy_static переменные неизменяемы (поскольку они находятся за ссылкой). Если вы хотите изменить статичность, вам нужно использовать Mutex:

lazy_static! {
    static ref VALUE: Mutex<u64>;
}

impl Drop for IsFoo {
    fn drop(&mut self) {
        let mut value = VALUE.lock().unwrap();
        *value += 1;
    }
}

#[test]
fn test_drops_actually_work() {
    // Have to drop the mutex guard to unlock, so we put it in its own scope
    {
        *VALUE.lock().unwrap() = 0;
    }
    {
        let c = CBox;
        c.set(IsFoo);
        c.set(IsFoo);
        c.poll(/*...*/);
    }
    assert!(*VALUE.lock().unwrap() == 2); // Assert that all expected drops were invoked
}