Static mut refs in single threaded wasm code

I have a web client for my game with all the rendering, networking, logic, etc. implemented in wasm (as a rust library) with minimal js glue. In the wasm module I have global static muts to store state such as entities like the current player, players, walls, bullets, etc.

static mut PLAYER: Player = Player::default();

static mut WALLS: Vec<Wall> = Vec::new();
static mut PLAYERS: Vec<Player> = Vec::new();
static mut BULLETS: Vec<Bullet> = Vec::new();

static mut CANVAS_WIDTH: usize = 0;
static mut CANVAS_HEIGHT: usize = 0;

static mut CAM: Camera = Camera::default();

I also have a render function that I export to js that will be called using requestAnimationFrame.

#[no_mangle]
extern "C" fn render(dt: f32) {
  unsafe {
    PLAYER.x += PLAYER.v_x * dt;
    PLAYER.y += PLAYER.v_y * dt;

    #[allow(static_mut_refs)]
    for p in &mut PLAYERS {
      p.x += p.v_x * dt;
      p.y += p.v_y * dt;
    }

    #[allow(static_mut_refs)]
    for b in &mut BULLETS {
      b.x += b.v_x * dt;
      b.y += b.v_y * dt;
    }
  }

  #[allow(static_mut_refs)]
  draw(&CTX, unsafe { &CAM });
}

It calls to draw() which is what actually does the rendering.

fn draw(ctx: &Context, cam: &Camera) {
  ctx.clear_rect(0.0, 0.0, canvas_width() as f32, canvas_height() as f32);

  ctx.fill_style("#efeff5");
  ctx.fill_rect(0.0, 0.0, canvas_width() as f32, canvas_height() as f32);

  ctx.save();
  ctx.translate(
    canvas_width() as f32 / 2.0 - cam.z * cam.x,
    canvas_height() as f32 / 2.0 - cam.z * cam.y,
  );
  ctx.scale(cam.z, cam.z);

  draw_grid(ctx);
  draw_border(ctx);
  #[allow(static_mut_refs)]
  draw_walls(ctx, unsafe { &WALLS });
  #[allow(static_mut_refs)]
  draw_bullets(ctx, unsafe { &BULLETS });
  #[allow(static_mut_refs)]
  draw_players(ctx, unsafe { &PLAYERS });
  #[allow(static_mut_refs)]
  draw_player(ctx, unsafe { &PLAYER });

  ctx.restore();

  #[allow(static_mut_refs)]
  draw_hud(ctx, unsafe { &HUD }, &MINIMAP);
}

There will never be 2 instances of this wasm module sharing the same underlying Memory while also being called simultaneously in different threads. So is it safe to use static mut in this way? Specifically, is it safe to:

  1. Use or pass a static mut as an immutable reference using unsafe { &MY_STATIC } as seen at the end of render() and in draw() above.
  2. Mutate a static mut as seen in render() above, whether that be directly or indirectly through a reference (see the 2 loops in render())
  3. Read a static mut such as in
fn canvas_width() -> usize {
  unsafe { CANVAS_WIDTH }
}

Side note: Everything works as intended and no unexpected behavior was observed. I ask only because I am now moving to 2024 edition where static_mut_refs is now deny by default

The answer (no) is in that link:

Merely taking such a reference in violation of Rust's mutability XOR aliasing requirement has always been instantaneous undefined behavior, even if the reference is never read from or written to. Furthermore, upholding mutability XOR aliasing for a static mut requires reasoning about your code globally, which can be particularly difficult in the face of reentrancy and/or multithreading.

I did read that and my understanding is that so long as I only produce 1 reference (mut or not) via unsafe { &MY_STATIC } and then pass it down a call chain (fn x(y: &my_static)), it should be ok since there is really only 1 reference that is being passed around rather than each function creating its own?

Nope. It says it is unconditional UB. That means there are no guarantees about the behavior at all. If it happens to work and you want to rely on that, you can, but UB means the behavior can change at any time (new compiler releases, different OS, among others).

The above statement is too general.

In other words this is UB

static mut X: u8 = 0;

fn do_something() {
  let x = unsafe { &mut X };
  x = 10;
  do_something_else(x);
}

fn do_something_else(_x: &mut u8) {
  let x = unsafe { &X }; // a mut ref already exists, so this is UB
  println!("{x}");
  // or
  let x = unsafe { &mut X }; // a mut ref already exists, so this is UB
  x = 100;
}

But it also says "in violation of Rust's mutability XOR aliasing requirement".
Is that not "either n refs" or "1 mut ref"?

If you never have a mutable reference the same time as any other reference, then it is fine. Unless you can actually prove that is true, I find this code extremely suspect. Absent of a formal proof, I don't find the situation much different than a C developer who insists no memory safety issues exist without actually proving it. It is far too easy for an implicit reference to be taken without realizing it.

2 Likes

Thankfully it is easy to prove since I create the references at the top level of the call chain and then pass them down rather recreating where needed. I guess to be extra safe I could create ALL references at the VERY top and then pass them down even if they do not get used at early levels

You're right and I shouldn't have made such a blanket statement. It's possible that you're not violating those rules, but the code you've shown doesn't prove or guarantee that they're not violated.

Easier said than done. Unless you have actually proven it via Coq or similar, I find the code terrifying. There is precedent in proving the aliasing rules have not been violated, but good luck with that. @RalfJung is a professor at ETH Zürich, so it's not surprising he and company could formally prove GhostCell is correct. For most mortals though, such an undertaking is out of our league.

You should replace all of your static mut with static RefCell static Mutex. If your uses are correct, then it will be completely equivalent, and the overhead should be insignificant; if your uses are incorrect, it will tell you so.

(I could also say things about reasons for avoiding statics entirely, but soundness is far more important, so I’ll stick to the simple thing.)

1 Like

That is actually a good idea. Do you happen to know whether UnsafeCell would have the same performance as simple static muts? If so I could write a wrapper over UnsafeCell that simply uses UnsafeCell's api to act like a RefCell (with no checks) so that in development I could use RefCell and in production I could simply find+replace with the wrapper

Is there some reason you worried about the overhead of the RefCell check? The check only occurs when you borrow (get the reference) and is very lightweight.

Maximum performance. Premature optimization? Maybe

I don't think anyone replied to this directly, so -- it is, yes. (Run Miri under Tools, top right.)

And so is this:

fn do_something() {
  let x = unsafe { &mut *&raw mut X };
  do_something_else();
  *x = 10;
}

fn do_something_else() {
  let x = unsafe { &*&raw const X };
  println!("{x}");
}

Because the references exist at the same time (at different places on the stack). See the first dozen or so comments here. In particular, note how single-threadedness doesn't prevent all the possible UB.

You could strengthen this by putting the statics in the top level of the call chain, so it's only possible to create the references there. Have the entry point be responsible for only that, even.

(But avoiding unsafe is even better IMO.)

As long as you only RefCell::borrow_mut() Mutex::lock() once or a few times within an exported extern function, the cost of the check will be utterly insignificant next to the cost of crossing the JS/Wasm boundary at all.

1 Like

static RefCells aren't allowed, since RefCell is !Sync, so you'd have to use static Mutexes instead. (The overhead should also be relatively small, though it may be larger if and when WASM atomics, or WASM threading in general, becomes a thing. It also likely won't detect deadlocks as well as a RefCell, unless it has a specialized implementation that I'm unaware of.)

In general, if I had a bunch of static mut variables that I wanted to access by name instead of passing their references around everywhere, I'd consider a 'phased' design, where in one phase, I access the variables as strictly read-only (holding onto as many immutable references as I'd like), and in the other phase, I carefully read from and write to the variables one at a time, not holding onto any dangerous references between each access. (In a game context, these two phases might be rendering the state vs. updating the state.)

Alternatively, as much as possible, I'd consider encapsulating the static muts into safe getter and setter functions that don't keep any references. E.g., instead of directly iterating over &mut PLAYERS, create a function that allows updating them by value:

pub fn update_players(mut f: impl FnMut(Player) -> Player) {
    let len = unsafe { (*&raw const PLAYERS).len() };
    for i in 0..len {
        let old_value = std::mem::take(unsafe { &mut PLAYERS[i] });
        let new_value = f(old_value);
        unsafe { PLAYERS[i] = new_value };
    }
}

/* ... */

update_players(|mut p| {
    p.x += p.v_x * dt;
    p.y += p.v_y * dt;
    p
});

(Of course, it wouldn't have to be quite so generic: an update_player_pos() function containing an ordinary for p in &mut PLAYERS { ... } loop would also work. The important part is that each wrapper function only accesses one variable at a time, and it doesn't let any references to the variable get out, similarly to a safe Cell.)

Note that in that work we did not consider Stacked Borrows / Tree Borrows requirements you are discussing here. We showed that the type does not violate any type system requirements, but we have yet to merge that line of work with the full suite of Rust language requirements including the aliasing rules.

OTOH, proving some concrete program is a lot easier than proving soundness of a reusable library.

The same goes for shared references. So really, the statement should be: if you exclusively use raw pointers, you are fine.

Note that we have not decided yet whether MY_STATIC = 5; and *(&raw mut MY_STATIC) = 5 are equivalent. Until we have decided that, direct accesses to the static should not be mixed with raw pointer accesses.

2 Likes

Of course it's easier to prove for a specific case than the general; however the point I was attempting to make is that there is a big difference between saying "it is easy to prove…" and actually proving that thing whether that "thing" is a general case or specific example.

When one wants to escape the guarantees Rust's type system provides, then I think it's reasonable to set a higher bar for what is acceptable due to the ramifications of safety violations that can occur. Anyone that has read some of your posts should know just how crazily easy it is to write incorrect code. Mind you I'm not someone who automatically finds a crate without forbid(unsafe_code) as "inferior" to a crate that has it—I write unsafe code when I deem it necessary/worth it—but I do hold such code to a higher level of accountability.

I shouldn't have stated that GhostCell was proven to not violate the aliasing rules though. I apologize for the mistake.