Como você pode fazer um singleton estático seguro em Rust?

Esse é um assunto controverso, portanto, deixe-me começar explicando meu caso de uso e, em seguida, fale sobre o problema real.

Eu acho que para um monte de coisas inseguras, é importante ter certeza de que você não vazará memory; isso é realmente muito fácil se você começar a usar transmute() e forget() . Por exemplo, passando uma instância em checkbox para o código C por um período de tempo arbitrário, então recuperando-a e “ressuscitando-a” usando transmute .

Imagine que eu tenha um wrapper seguro para esse tipo de API:

 trait Foo {} struct CBox; impl CBox { /// Stores value in a bound C api, forget(value) fn set(value: T) { // ... } /// Periodically call this and maybe get a callback invoked fn poll(_: Box<Fn + Send>) { // ... } } impl Drop for CBox { fn drop(&mut self) { // Safely load all saved Foo's here and discard them, preventing memory leaks } } 

Para testar isso não é realmente vazando qualquer memory, eu quero alguns testes como este:

 #[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(); } } 

Como você pode criar esse tipo de object singleton estático?

Ele deve usar um bloqueio de proteção de estilo Semaphore para garantir que vários testes não sejam executados simultaneamente e, em seguida, acessar de forma insegura algum tipo de valor mutável estático.

Eu pensei que talvez essa implementação funcionasse , mas na prática ela falha porque ocasionalmente as condições de corrida resultam em uma execução duplicada do 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)); } } 

Observe especificamente que, diferentemente de um programa normal em que você pode ter certeza de que seu ponto de input (principal) está sempre em execução em uma única tarefa, o executor de teste no Rust não oferece nenhum tipo de ponto de input único como este.

Outro, obviamente, do que especificar o número máximo de tarefas; Dadas dezenas de testes, apenas um punhado precisa fazer esse tipo de coisa, e é lento e sem sentido limitar o pool de tarefas de teste a um só para este caso.

Parece um caso de uso para std::sync::Once :

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

Então, na sua chamada de testes

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

Once garante que seu init só será executado uma vez, não importa quantas vezes você chame INIT.doit() .

Veja também lazy_static , o que torna as coisas um pouco mais ergonômicas. Ele faz essencialmente a mesma coisa que uma estática Once para cada variável, mas a envolve em um tipo que implementa Deref para que você possa acessá-la como uma referência normal.

Uso parece com isso ( da documentação ):

 #[macro_use] extern crate lazy_static; use std::collections::HashMap; lazy_static! { static ref HASHMAP: HashMap = { 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); } 

Note que autoderef significa que você não precisa usar * sempre que chamar um método em sua variável estática. A variável será inicializada na primeira vez que for Deref ‘d.

No entanto, as variables ​​lazy_static são imutáveis ​​(já que estão por trás de uma referência). Se você quer uma estática mutável, você precisará usar um Mutex :

 lazy_static! { static ref VALUE: Mutex; } 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 }