Wasm_bindgen: How to drop Rc inside Closure

Hey,

I have just a quick question about the use of Rc inside Closures in wasm_bindgen.

The code below adds a click event to a paragraph. Randomly it will re-render the "app" and add a new paragraph to the DOM. The old <P> will be gone, but the state_clone (Rc) will still be around and Rc::strong_count(&state) will still increase the count.

  • How can I drop the Rc inside the Closure once the paragraph will be detached from the DOM?
  • Is it a big issue to have dangling Rc clones?
  • Will this magically be solved once I use --weak-refs?
  • Or, is there an other simpler way to achieve what I am trying to do?

Thanks a lot!

use std::{cell::RefCell, rc::Rc};
use wasm_bindgen::{prelude::*, JsCast};
use web_sys::MouseEvent;

#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace = console)]
    fn log(s: &str);
}

fn render(state: Rc<RefCell<String>>) {
    let window = web_sys::window().unwrap();
    let document = window.document().unwrap();
    let body = document.body().unwrap();
    body.set_inner_html("");

    // print string count
    log(format!("strong_count: {}", Rc::strong_count(&state)).as_str());

    // crate paragraph
    let p = document.create_element("p").unwrap();
    p.set_inner_html("hello");
    body.append_child(&p).unwrap();

    // crate closure
    let state_clone = Rc::clone(&state);
    let on_click = Closure::<dyn Fn(MouseEvent)>::new(move |_e: MouseEvent| {
        log(state_clone.borrow().as_str());
        if js_sys::Math::random() > 0.5 {
            log("re-render");
            render(state_clone.clone());
            // I would like to drop the Rc here
            // std::mem::drop(state_clone);
        }
    });
    p.add_event_listener_with_callback("click", on_click.as_ref().unchecked_ref())
        .unwrap();
    on_click.forget();
}

#[wasm_bindgen(start)]
pub fn main() {
    let state = "hello".to_string();
    let state = Rc::new(RefCell::new(state));
    render(state);
}

The callback can get called more than once right? So hypothetically, if you managed to drop the Rc on call N; what is call N+1 supposed to do? Crash?

The code is supposed to be structured such that the element which calls that callback ceases to exist right after the call which drops the Rc. This is not something we can enforce, however, so Rust has to err of the side of safety and not allow this. However, if this closure isn't used anywhere else, it's supposed to be garbage-collected on JS side anyway, right?

In my limited understanding of wasm_bindgen, I'm 99% sure this is not the case.

on_click.forget();

I'm 99% sure that is a (purposeful) memory leak and the JS side does not gc it. We leak memory on purpose since we pass a REFERENCE to the JS side in the previous line at:

    p.add_event_listener_with_callback("click", on_click.as_ref().unchecked_ref())
        .unwrap();

If that is the case (I'm not sure it is); I think the solution here is to setup a Rc<RefCell<Option<Rc<Callback>>>> somewhere.

Then, in the callback, set this RefCell to None. In doing so, we cause the Rc<Callback> count to decrease by 1, and if that is the only ref to it, it gets GC-ed. However, care must then be taken to ensure that the JS side is no longer pointing to a non-existent ref in the Rust side.

Eventually yes, currently no. Once the weak references proposal is standardized and implemented wasm-bindgen will be able to generate JS that can run finalizers to clean up resources when the closure is garbage collected.

https://rustwasm.github.io/docs/wasm-bindgen/reference/weak-references.html

The thing you actually want to do is make sure the on_click closure gets dropped after you remove the child elements from the body. Instead of forgeting the closure you can store it similarly to how you're storing state. Then take the old closure and drop it, and all the captured variables should be dropped.

3 Likes

Thanks a lot! That's what I did and it works!
I store the closure in a vec and it seems to get dropped when I call vec.clear().
See the modified code:

use std::{cell::RefCell, rc::Rc};
use wasm_bindgen::{prelude::*, JsCast};
use web_sys::MouseEvent;

#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace = console)]
    fn log(s: &str);
}

struct Ctx {
    state: String,
    events: Vec<Closure<dyn Fn(MouseEvent)>>,
}

fn render(ctx: Rc<RefCell<Ctx>>) {
    // get and clear the body
    let window = web_sys::window().unwrap();
    let document = window.document().unwrap();
    let body = document.body().unwrap();
    body.set_inner_html("");

    // print some info
    log(format!("strong_count: {}", Rc::strong_count(&ctx)).as_str());
    log(&format!("total_events: {}", ctx.borrow().events.len()));

    // clear all events
    ctx.borrow_mut().events.clear();

    // crate paragraph
    let p = document.create_element("p").unwrap();
    p.set_inner_html(ctx.borrow().state.as_str());
    body.append_child(&p).unwrap();

    // crate closure
    let ctx_clone = Rc::clone(&ctx);
    let on_click = Closure::<dyn Fn(_)>::new(move |_e: MouseEvent| {
        log(ctx_clone.borrow().state.as_str());
        // re-render the component
        if js_sys::Math::random() > 0.5 {
            log("re-render");
            render(ctx_clone.clone());
        }
        // set a new state
        else {
            ctx_clone.borrow_mut().state = js_sys::Math::random().to_string();
        }
    });
    // add event listener
    p.add_event_listener_with_callback("click", on_click.as_ref().unchecked_ref())
        .unwrap();
    // add closure to ctx.events
    ctx.borrow_mut().events.push(on_click);
}

#[wasm_bindgen(start)]
pub fn main() {
    let ctx = Ctx {
        state: "hello".to_string(),
        events: vec![],
    };
    let ctx = Rc::new(RefCell::new(ctx));

    render(ctx);
}