Can someone help me understand what's happening here, and why?

Code from Rust paint:

    let context = Rc::new(context); // Rc to have multiple owners
    let pressed = Rc::new(Cell::new(false)); // ^... but with Cell to make it mutable?
    {
        let context = context.clone(); // another owner
        let pressed = pressed.clone(); // ^...       
        let closure = Closure::<dyn FnMut(_)>::new(move |event: web_sys::MouseEvent| { // ?
            context.begin_path();
            context.move_to(event.offset_x() as f64, event.offset_y() as f64);
            pressed.set(true);
        });
        canvas.add_event_listener_with_callback("mousedown", closure.as_ref().unchecked_ref())?; // ?
        closure.forget(); // ?
    }
    {       ...same for "mousemove"... }
    {       ...same for "mouseup"...   }

I don't really follow the closure, I don't understand what's happening, or why it's used.
I don't really follow the order of code execution either, when is the 6th line of code, begin_path() read f.e.? After the 9th line?
ELI5 would help a lot ^^

This is partially confusing because it's interacting with JavaScript APIs, so there's extra work you have to do so the wasm module can interact with JS in useful ways.

Basically Closure takes a rust closure that doesn't have any references to it's environment (which is why you need move) and stores it somewhere semi-permanently. Wasm_bindgen takes care of mapping that stored rust closure to the JavaScript callback that gets passed to the matching JavaScript API. That JavaScript API call is what actually sets up the callback with the DOM canvas object.

So the closure is called by JavaScript by way of invoking a JS callback, which calls into wasm_bindgen, which translates the callback to the appropriate rust closure.

forget transfers ownership of the closure to the JavaScript garbage collector, which is important here since you're passing the closure into JS and not keeping it around in any capacity. In practice this apparently leaks memory at least until weak references are accepted to the JS standard according to the docs.

2 Likes

Okay thanks. I see I might be in over my head, or at least too early. I want to invoke it a lot, and even with this paint program it seems like you'd invoke it a lot, which would cause a buildup. Not sure why it doesn't say that in the example, however.

Calling forget leaks memory, but the closure being called doesn't. The start function gets called once when the wasm module is instantiated so it isn't really leaking memory in practice. The JS callback would be keeping it alive anyway if you were working with pure JS.

1 Like

I don't think I understand memory leak.
Could you help me understand it a bit better?

What I think would be happening now is that Rust creates a closure? and hands it off to JS, so Rust uses some memory, and than JS as well. However, it continues with JS and "leaves" Rust. So the Rust closure memory is still dangling there.

Yeah that's basically correct.

The rust code is stuffing the rust closure somewhere, because JS "owns" it now so it needs to stay valid. But there's not a great way to make sure the JS callback can dispose of the rust closure when it gets garbage collected. Leaking the memory is the simplest solution, though I assume you could set up a system to avoid always leaking it if you needed to.

In this example it basically doesn't matter though. Normal usage of that wasm module would have the same memory footprint with and without the leak, because the closure actually does need to exist for the whole lifetime of the program[1]


  1. assuming I didn't miss any code that was resetting that callback anyway ↩ī¸Ž

1 Like

So, then; it doesn't matter if the user clicks, or draws, multiple times and invokes the code? Since the closure will be alive in JS to use anyway?

When does JS disregard the closure than? Or never? Unless you explicitly tell JS to forget it, it will be alive?

All in all, it doesn't rerun this code, or these closures, each time the user clicks or moves the mouse, because these closures get allocated in memory only once, at compile time or something?

In normal JavaScript the callback you set as an event handler will live as long as these conditions hold:

  • The element you set it as an event handler on still exists
  • The event handler hasn't been removed

This isn't plain JS though so things get a lot more complicated.

The code inside the closures will run every time the associated event fires in the DOM. The start function itself only runs once, but it sets up those closures to run in response to some events.


I've annotated the source with some comments that will hopefully help clarify what's happening

// start is executed automatically once when the wasm 
// module is loaded by the JavaScript engine
// https://rustwasm.github.io/wasm-bindgen/reference/attributes/on-rust-exports/start.html
#[wasm_bindgen(start)]
pub fn start() -> Result<(), JsValue> {
    // Perform one time setup for the program
    let document = web_sys::window().unwrap().document().unwrap();
    let canvas = document
        .create_element("canvas")?
        .dyn_into::<web_sys::HtmlCanvasElement>()?;
    document.body().unwrap().append_child(&canvas)?;
    canvas.set_width(640);
    canvas.set_height(480);
    canvas.style().set_property("border", "solid")?;
    let context = canvas
        .get_context("2d")?
        .unwrap()
        .dyn_into::<web_sys::CanvasRenderingContext2d>()?;
    
    // Create the data structures that will be shared by the callbacks
    let context = Rc::new(context);
    let pressed = Rc::new(Cell::new(false));

    {
        // Clone the Rcs so that we can move them into the closure
        let context = context.clone();
        let pressed = pressed.clone();

        // Create a closure that references our shared state.
        let closure = Closure::<dyn FnMut(_)>::new(move |event: web_sys::MouseEvent| {
            // The contents of the closure are only run when the 
            // closure is called by the JS event handler. 
            // The code inside the closures is the only part of this 
            // program that runs repeatedly.
            context.begin_path();
            context.move_to(event.offset_x() as f64, event.offset_y() as f64);
            pressed.set(true);
        });

        // Register our closure as an event handler on the 
        // canvas DOM element 
        // (indirectly via JavaScript, this is managed by wasm_bindgen)
        canvas.add_event_listener_with_callback("mousedown", closure.as_ref().unchecked_ref())?;

        // We need the closure to be retained since we passed it to JS, 
        // and JS doesn't know how to retain rust data.
        closure.forget();
    }
    // Additional code omitted...
}
2 Likes

Thanks for giving the explanation and helping a lot with understanding what's happening.

It's getting late, but what I'm getting is that it's not really a memory leak in this case, since JS needs it still.

BTW my actual problem is; I either use Rust to read user mouse input like this (which is what I wanted to avoid, because I don't understand what's happening), or I find a way to do this: original question, if you have some insights on that?

Either way I'll get a fresh read tmr. Thanks, again.

Well I absolutely would not swear by it, but I modified the paint example to handle the callbacks from JS and manually call into the wasm module from the callbacks. It does appear to work

lib.rs

use std::cell::Cell;
use std::rc::Rc;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::CanvasRenderingContext2d;
use web_sys::HtmlCanvasElement;

#[wasm_bindgen]
pub struct Context {
    canvas: HtmlCanvasElement,
    context: Rc<CanvasRenderingContext2d>,
    pressed: Rc<Cell<bool>>,
}

#[wasm_bindgen]
impl Context {
    // Need an explicit getter for canvas since HtmlCanvasElement isn't Copy
    #[wasm_bindgen(getter)]
    pub fn canvas(&self) -> HtmlCanvasElement {
        self.canvas.clone()
    }

    #[wasm_bindgen]
    pub fn mousedown(&self, event: web_sys::MouseEvent) {
        self.context.begin_path();
        self.context
            .move_to(event.offset_x() as f64, event.offset_y() as f64);
        self.pressed.set(true);
    }

    #[wasm_bindgen]
    pub fn mousemove(&self, event: web_sys::MouseEvent) {
        if self.pressed.get() {
            self.context
                .line_to(event.offset_x() as f64, event.offset_y() as f64);
            self.context.stroke();
            self.context.begin_path();
            self.context
                .move_to(event.offset_x() as f64, event.offset_y() as f64);
        }
    }

    #[wasm_bindgen]
    pub fn mouseup(&self, event: web_sys::MouseEvent) {
        self.pressed.set(false);
        self.context
            .line_to(event.offset_x() as f64, event.offset_y() as f64);
        self.context.stroke();
    }
}

#[wasm_bindgen]
pub fn setup() -> Result<Context, JsValue> {
    let document = web_sys::window().unwrap().document().unwrap();
    let canvas = document
        .create_element("canvas")?
        .dyn_into::<web_sys::HtmlCanvasElement>()?;
    document.body().unwrap().append_child(&canvas)?;
    canvas.set_width(640);
    canvas.set_height(480);
    canvas.style().set_property("border", "solid")?;
    let context = canvas
        .get_context("2d")?
        .unwrap()
        .dyn_into::<web_sys::CanvasRenderingContext2d>()?;
    let context = Rc::new(context);
    let pressed = Rc::new(Cell::new(false));

    Ok(Context {
        canvas,
        context,
        pressed,
    })
}

index.js

import('./pkg')
  .catch(console.error).then((pkg) => {
    const context = pkg.setup()

    context.canvas.addEventListener('mousemove', event => {
      context.mousemove(event)
    })

    context.canvas.addEventListener('mousedown', event => {
      context.mousedown(event)
    })

    context.canvas.addEventListener('mouseup', event => {
      context.mouseup(event)
    })
  });

It's my understanding that context in this example is effectively leaked, even if all the references to it from JS get cleaned up.

Hopefully that gives you an idea of how you could do what you were trying to do in that other thread

1 Like

Trying to make a game on the web. Need to read user mouse position and give it to Rust to do some calculations with, hand some results back to JS, and use those to draw on context in JS. However, both seemed not well doable in JS with Rust, so ending up having to do these actions in Rust (so I don't have to pass the values back and forth).

Even though there's some memory leak here, I think it'd be okay, right? It's not a lot, and it doesn't really build up at least, as far as I understand.

I think the answer you gave solves basically everything and showed me a lot. Thanks so much!

Yeah if you only create a single Context the memory leak is effectively invisible. If you need to create multiple (for example maybe you load a new one to restart the game without reloading the page) I think you can call free on the JS object before you lose the previous reference to it, and then you should be good? But i didn't dig very far into that. The page I linked to at the end of my last post has some more details about that.

My index.js there is basically doing exactly what you're describing here, so it's definitely possible!

1 Like

Already been reading up a lot from the link and JS/wasm conversions! It's a lot of information for me since I'm new to Rust and (lower) coding, plus, wasm is quite a headache by itself I feel like.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.