1

I have a library "business logic" crate I want to be able to write multiple binary crate "frontends" or "platform layers" for. These platform layers tend to use libraries calling platform APIs and that tends to imply long compile times. I want to be able to iterate on the business logic crate without needing to recompile the platform layer. But, I want to be able to compile the project into a single binary for each platform, and I'd rather not mess with shared object files/DLLs if I don't have to.

I have a way to do this using a fn pointer and static mut, but there's this rust-lang/rust issue about potentially removing it, so I'd like to know if there is a way to get the results I want without using it.

For reference, the way I got it working was like this:

use platform_types::{Input, Output};

fn update_and_render(input: Input) -> Output {
    static mut STATE: logic::State = logic::new();
    let state: &mut logic::State = unsafe { &mut STATE };

    logic::update_and_render(state, input)
}

fn main() {
    platform_layer::run(update_and_render);
}

where the above code is in the main crate, logic is the business logic crate, platform_layer is the platform layer crate, and platform_types contains the common types between the three other crates.

I tried using a RefCell with try_borrow_mut I got the error std::cell::RefCell<State> cannot be shared between threads safely and mentions that Sync is not implemented for std::cell::RefCell<State> and the error did not go away if I tried implementing Sync for State experimentally. I then tried a Mutex, but as far as I can tell I can't put one in a static.

EDIT: If it makes a difference, I don't actually expect to need to call the function pointer from multiple threads, although I understand that handing one out implicitly allows that. If I ever (accidentally?) do call the function pointer from multiple threads, a failed debug_assert! or similar is obviously preferable to UB.

Ryan1729
  • 940
  • 7
  • 25
  • As a note: `static mut` won't be removed in Rust (< 2.0), because it would be a breaking change and Rust doesn't do that so you're free to use it (although it's not recommended) – hellow Mar 20 '19 at 07:04
  • [Issue #53639](https://github.com/rust-lang/rust/issues/53639) exists because a large proportion of people that use `static mut` *misuse* it, introducing memory unsafety. If you care about safety (and you should), then you should try to remove it. – Shepmaster Mar 20 '19 at 13:08
  • 1
    I believe your question is answered by the answers of [How do I create a global, mutable singleton?](https://stackoverflow.com/q/27791532/155423) or [How can you make a safe static singleton in Rust?](https://stackoverflow.com/q/27221504/155423). If you disagree, please **[edit]** your question to explain the differences. Otherwise, we can mark this question as already answered. – Shepmaster Mar 20 '19 at 13:09

2 Answers2

2

You can move state to main()

fn main() {
    let mut state = logic::new();
    platform_layer::run(move |input| logic::update_and_render(&mut state, input));
}
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Laney
  • 1,571
  • 9
  • 7
  • This is certainly the simplest answer! But it requires me to change the type `run` accepts to either a generic `F: FnMut(Input) -> Output` or `impl FnMut(Input) -> Output` either of which turns out to slow down my compiles when I edit `logic` significantly, (around 4s to around 15s). I'm not sure why this is, but it's enough to prevent me from using this method. – Ryan1729 Mar 21 '19 at 04:10
  • interesting behavior. @Ryan1729 have you tried to combine it with your approach, i.e. wrap State into RefCell? – Laney Mar 21 '19 at 06:01
  • If I wrap `State` in a `RefCell` and call `try_borrow_mut` in the closure I seem to get slightly shorter compile times than without it (three runs clocked in at 13.53s, 13.72s, and 12.58s respectively) but the mutex solution is still compiling much faster. – Ryan1729 Mar 22 '19 at 01:50
2

You can use a static mutex and get a local mutable reference when needed:

#[macro_use]
extern crate lazy_static;

use std::sync::Mutex;

fn update_and_render(input: Input) -> Output {
    lazy_static! {
        static ref STATE_MUTEX: Mutex<logic::State> = Mutex::new(logic::new());
    }
    let mut state = STATE_MUTEX.lock().unwrap();
    logic::update_and_render(state, input)
}

This is fast, safe, and ready for access from multiple threads.

Denys Séguret
  • 372,613
  • 87
  • 782
  • 758