简体   繁体   English

从 Rust 中的回调访问全局状态

[英]Accessing global state from a callback in rust

I am learning rust and I've been trying to rewrite a project that I did in C# in rust, and I got stuck trying to access global state from a callback,我正在学习 rust,我一直在尝试重写我在 C# 中用 rust 完成的项目,但在尝试从回调访问全局状态时遇到了困难,

Is there a simple way to do that in rust?有没有一种简单的方法可以在 Rust 中做到这一点? Keeping in mind the I can't add new parameter to the callback.请记住,我无法向回调添加新参数。

eg:例如:

use std::collections::HashMap;
use std::time::instant;
use lib::bar;

struct Struct2{
    foo: bar,
    word: String,
}

struct GlobalState{
    running: bool,
    now: Instant,
    map: HashMap<String, Struct2>,
    hook_id: usize,
    current_id: String,
}

impl GlobalState{
    fn init() -> Self{
        let hook_id = unsafe {set_ext_hook(HOOK_ID, system_hook)};
        // Omitted initialization code.
    }
    // Omitted state mutation functions.
}

unsafe extern "system" fn system_hook(ext_param1:usize, ext_param2: usize) -> isize {
    // use global state here
}

I tried using crates such as lazy_static and once_cell, but they didn't work because the external struct that I use (lib::bar in this example) "cannot be sent between threads safely"我尝试使用诸如lazy_static 和once_cell 之类的板条箱,但它们不起作用,因为我使用的外部结构(在本例中为lib::bar) “无法在线程之间安全地发送”

My code so far is single threaded (I plan on using a different thread for the program's gui when I implement it)到目前为止,我的代码是单线程的(我计划在实现它时为程序的 gui 使用不同的线程)

Any help is appreciated, thanks.任何帮助表示赞赏,谢谢。

You seem to be dealing with data that is neither Send nor Sync , so Rust won't allow you to place it in a global, even inside a mutex.您似乎正在处理既不是Send也不是Sync ,因此 Rust 不允许您将其放置在全局中,即使是在互斥锁中。 It's not clear from the question whether this is a result of lib::bar being genunely thread-unsafe, or just the unintended consequence of its use of raw pointers under the hood.从问题中不清楚这是否是lib::bar真正线程不安全的结果,或者只是它在后台使用原始指针的意外后果。 It is also unclear whether you are in the position to modify lib::bar to make its types Send and Sync .还不清楚您是否可以修改lib::bar以使其类型为SendSync

Assuming most conservatively that lib::bar cannot be changed, and taking into account that your program is single-threaded, your only safe option is to create a thread-local state:最保守地假设lib::bar不能更改,并考虑到您的程序是单线程的,您唯一安全的选择是创建线程本地状态:

use std::cell::RefCell;
use std::thread_local;

struct Foo(*const i32); // a non-Send/Sync type

struct GlobalState {
    foo: Foo,
    data: String,
    mutable_data: RefCell<String>,
}

thread_local! {
    static STATE: GlobalState = GlobalState {
        foo: Foo(std::ptr::null()),
        data: "bla".to_string(),
        mutable_data: RefCell::new("".to_string()),
    };
}

You can access that state (and modify its interior-mutable pieces) from any function:您可以从任何函数访问该状态(并修改其内部可变部分):

fn main() {
    STATE.with(|state| {
        assert_eq!(state.foo.0, std::ptr::null());
        assert_eq!(state.data, "bla");
        assert_eq!(state.mutable_data.borrow().as_str(), "");
        state.mutable_data.borrow_mut().push_str("xyzzy");
    });
    STATE.with(|state| {
        assert_eq!(state.mutable_data.borrow().as_str(), "xyzzy");
    });
}

Playground 操场

Note that if you try to access the "global" state from different threads, each will get its own copy of the state:请注意,如果您尝试从不同的线程访问“全局”状态,每个线程都会获得自己的状态副本:

fn main() {
    STATE.with(|state| {
        state.mutable_data.borrow_mut().push_str("xyzzy");
    });
    std::thread::spawn(|| {
        STATE.with(|state| {
            // change to "xyzzy" happened on the other copy
            assert_eq!(state.mutable_data.borrow().as_str(), "");
        })
    })
    .join()
    .unwrap();
}

Playground 操场

One option is "thread confinement" of your variable.一种选择是变量的“线程限制”。 This means that all access to the variable happens on one thread.这意味着对变量的所有访问都发生在一个线程上。 Typically you create a dedicated thread for this and create a proxy for your variable that is shared between other threads and is responsible for getting messages to and from the confining thread.通常,您为此创建一个专用线程,并为您的变量创建一个代理,该代理在其他线程之间共享,并负责从限制线程获取消息和从限制线程获取消息。

In rust this is kind of inter-thread communication is usually done using channels.在 Rust 中,这种线程间通信通常是使用通道完成的。 I'll show a cut-down version of your code - where lib::bar simply wraps an i32 pointer.我将展示您的代码的简化版本 - 其中lib::bar只是包装了一个 i32 指针。 Pointers do not implement Send+Sync and are a pretty good stand-in for your API.指针不实现发送+同步,是您 API 的一个很好的替代品。

The code is fairly verbose, and I've cheated and not implemented error handling on all the send and recv calls, which you should definitely do.代码相当冗长,我作弊并且没有对所有sendrecv调用实现错误处理,你绝对应该这样做。 Despite the verbosity, adding new functionality is pretty simple - it mostly consists of adding a variant to the Message and Reply enums, and copying the existing functionality.尽管冗长,但添加新功能非常简单——它主要包括向 Message 和 Reply 枚举添加一个变体,并复制现有功能。

use lazy_static::lazy_static;
use std::sync::mpsc::sync_channel;

pub mod lib {
    pub struct Bar(*mut i32);
    impl Bar {
        pub fn new() -> Self {
            Bar(Box::into_raw(Box::new(0)))
        }
        pub fn set(&mut self, v: i32) {
            unsafe { *self.0 = v };
        }
        pub fn get(&self) -> i32 {
            unsafe { *self.0 }
        }
    }
}

enum Message {
    Set(i32),
    Get,
    Shutdown,
}

enum Reply {
    Set,
    Get(i32),
    Shutdown,
}

fn confinement_thread(
    receiver: std::sync::mpsc::Receiver<(Message, std::sync::mpsc::SyncSender<Reply>)>,
) {
    // Create the confined state
    let mut bar = lib::Bar::new();

    // Handle messages and forward them
    loop {
        let (mesg, reply_channel) = receiver.recv().unwrap();
        match mesg {
            Message::Set(v) => {
                eprintln!("    worker: setting value to {}", v);
                bar.set(v);
                reply_channel.send(Reply::Set).unwrap();
            }
            Message::Get => {
                let v = bar.get();
                eprintln!("    worker: getting value = {}", v);
                reply_channel.send(Reply::Get(v)).unwrap();
            }
            Message::Shutdown => {
                eprintln!("    worker: shutting down");
                reply_channel.send(Reply::Shutdown).unwrap();
                break;
            }
        }
    }
}

// This can be cloned happily
// and supports Send+Sync
struct GlobalProxy {
    channel: std::sync::mpsc::SyncSender<(Message, std::sync::mpsc::SyncSender<Reply>)>,
}

impl GlobalProxy {
    pub fn set(&self, v: i32) {
        eprintln!("  proxy: setting value to {}", v);
        let (a, b) = sync_channel(0);
        self.channel.send((Message::Set(v), a)).unwrap();
        let m = b.recv().unwrap();
        assert!(matches!(m, Reply::Set));
    }

    pub fn get(&self) -> i32 {
        eprintln!("  proxy: getting value");
        let (a, b) = sync_channel(0);
        self.channel.send((Message::Get, a)).unwrap();
        let m = b.recv().unwrap();
        if let Reply::Get(v) = m {
            eprintln!("  proxy: got value={}", v);
            v
        } else {
            unreachable!();
        }
    }

    pub fn die(&self) {
        eprintln!("Telling worker thread to shut down");
        let (a, b) = sync_channel(0);
        self.channel.send((Message::Shutdown, a)).unwrap();
        let m = b.recv().unwrap();
        assert!(matches!(m, Reply::Shutdown));
    }
}

lazy_static! {
    static ref G: GlobalProxy = {
        // Create com channels
        let (to_global, from_world) = sync_channel(0);
        // Keep one end for the proxy,
        let global = GlobalProxy{ channel: to_global};
        // The other goes to the worker thread
        std::thread::spawn(|| {confinement_thread(from_world)});
        global
    };
}

pub fn main() {
    eprintln!("global.get() = {}", G.get());
    eprintln!("global.set(10)",);
    G.set(10);
    eprintln!("global.get() = {}", G.get());

    G.die()
}

Working version 工作版本

There is probably a lot of opportunities to make this less verbose using macros, but I find this version more instructive.可能有很多机会使用宏来减少冗长,但我发现这个版本更有指导意义。

Another improvement would be to put the reply channel into the message object - which would allow us to remove the Reply enum.另一项改进是将回复通道放入消息对象中——这将允许我们删除回复枚举。

In some cases, it may be possible to remove the Message object, by passing a function to the confinement thread to run, rather than a message.在某些情况下,通过将一个函数传递给限制线程来运行,而不是一条消息,可以移除 Message 对象。 Something like:就像是:

impl GlobalProxy { 
  fn run_confined(&self f: dyn Fn(&lib::Bar) + Send + Sync)
   {...}
}

But handling functions with return values in a nice way is tricky.但是以一种很好的方式处理带有返回值的函数是很棘手的。

I have worked example not from global state, but accessing variables from outer scope.我的例子不是从全局状态,而是从外部范围访问变量。 Maybe it will useful.也许它会有用。

use std::collections::HashMap;

fn main() {
    let a = String::from("Hello world");
    let b = String::from("Another world");
    let mut keys: HashMap<String, String> = HashMap::new();

    let callback = |line: String| {
        keys.insert(line.to_string(), line.to_string());
        println!("{}", b);
        println!("{}", line);
        println!("{:?}", keys);
    };

    compute(a, callback)
}

fn compute<F>(a: String, mut f: F)
    where F: FnMut(String)
{
    f(a)
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM