SDL2, Emscripten/asmjs, and 'Invalid renderer' panic

I've solved this problem. It turns out it was probably an overly "unsafe" version of the "emscripten.rs" wrapper that called the "emscripten_set_main_loop()". This didn't allow me to see that I probably needed a "move" for the closure. When switching to a slightly different "emscripten.rs" wrapper, this became apparent. So here's the better "emscripten.rs":

// Based on emscripten.rs from https://github.com/therocode/rust_emscripten_main_loop
// This file interacts with the Emscripten API to provide a scheduling mechanism for main looping.

// Since Emscripten only schedules the looping to be executed later by the browser, we need to make sure that the
// data object looped upon lives as long as the looping is scheduled, as well as being properly destroyed afterwards.

// The Emscripten function used for this is emscripten_set_main_loop which will do the scheduling as well as terminate the current code flow
// to prevent scopes from being exited which would cause objects to be destroyed prematurely. To be able to destroy the data object properly
// as looping is terminated, the object is stored in thread_local storage.

#[cfg(target_os = "emscripten")]
pub mod emscripten {
    use std::cell::RefCell;
    use std::os::raw::c_int;

    // Declare our FFI to the Emscripten functions we need. These will be linked in when building for Emscripten targets.
    #[allow(non_camel_case_types)]
    type em_callback_func = unsafe extern "C" fn();

    extern "C" {
        pub fn emscripten_set_main_loop(
            func: em_callback_func,
            fps: c_int,
            simulate_infinite_loop: c_int,
        );
        pub fn emscripten_cancel_main_loop();
    }

    thread_local! {
        // This is where the data object will be kept during the scheduled looping. The storage structure is justified as follows

        // thread_local - we need it outside of function scope. thread_local is enough since we only expect interactions from the same thread.
        // RefCell<..> - allows for mutable access from anywhere which we need to store and then terminate. Still borrow-checked in runtime.
        // Option<..> - we don't always have anything scheduled
        // Box<dyn ...> - make it work generically for any closure passed in

        static MAIN_LOOP_CLOSURE: RefCell<Option<Box<dyn FnMut()>>> = RefCell::new(None);
    }

    // Schedules the given callback to be run over and over in a loop until it returns MainLoopEvent::Terminate.
    // Retains ownership of the passed callback
    pub fn set_main_loop_callback<F: 'static>(callback: F)
    where
        F: FnMut(),
    {
        // Move the callback into the data storage for safe-keeping
        MAIN_LOOP_CLOSURE.with(|d| {
            *d.borrow_mut() = Some(Box::new(callback));
        });

        // Define a wrapper function that is compatible with the emscripten_set_main_loop function.
        // This function will take care of extracting and executing our closure.
        unsafe extern "C" fn wrapper<F>()
        where
            F: FnMut(),
        {
            // Access and run the stashed away closure
            MAIN_LOOP_CLOSURE.with(|z| {
                if let Some(closure) = &mut *z.borrow_mut() {
                    (*closure)();
                }
            });

        }

        // Schedule the above wrapper function to be called regularly with Emscripten
        unsafe {
            emscripten_set_main_loop(wrapper::<F>, 0, 1);
        }
    }

    // This is used to de-schedule the main loop function and destroy the kept closure object
    pub fn cancel_main_loop() {
        // De-schedule
        unsafe {
            emscripten_cancel_main_loop();
        }

        // Remove the stored closure object
        MAIN_LOOP_CLOSURE.with(|d| {
            *d.borrow_mut() = None;
        });
    }
}

...and here is the update "main.rs" that has "move" added before the main_loop closure:

//Original from: https://puddleofcode.com/story/definitive-guide-to-rust-sdl2-and-emscriptem
extern crate sdl2;

use std::process;
use sdl2::rect::{Rect};
use sdl2::event::{Event};
use sdl2::keyboard::Keycode;

#[cfg(target_os = "emscripten")]
pub mod emscripten;

fn main() {
    let ctx = sdl2::init().unwrap();
    let video_ctx = ctx.video().unwrap();

    let window  = match video_ctx
        .window("updating SDL2 example with rust and asmjs...", 640, 480)
        .position_centered()
        //.opengl()
        .build() {
            Ok(window) => window,
            Err(err)   => panic!("failed to create window: {}", err)
        };

    let mut renderer = match window
        // replace "renderer()" with "into_canvas()" since the "renderer()" method no 
        // longer seems available in the upgrade from rust-SDL2 v0.29.0 to v0.35.0. 
        // Compiling with v0.29.0 is apparently incompatible with my recent 
        // version of emscripten (2.0.31).
        //.renderer()
        .into_canvas()
        .build() {
            Ok(renderer) => renderer,
            Err(err) => panic!("failed to create renderer: {}", err)
        };

    let mut rect = Rect::new(10, 10, 10, 10);

    let black = sdl2::pixels::Color::RGB(0, 0, 0);
    let white = sdl2::pixels::Color::RGB(255, 255, 255);

    let mut events = ctx.event_pump().unwrap();

    let mut main_loop = move || {
        for event in events.poll_iter() {
            match event {
                Event::Quit {..} | Event::KeyDown {keycode: Some(Keycode::Escape), ..} => {
                    process::exit(1);
                },
                Event::KeyDown { keycode: Some(Keycode::Left), ..} => {
                    rect.x -= 10;
                },
                Event::KeyDown { keycode: Some(Keycode::Right), ..} => {
                    rect.x += 10;
                },
                Event::KeyDown { keycode: Some(Keycode::Up), ..} => {
                    rect.y -= 10;
                },
                Event::KeyDown { keycode: Some(Keycode::Down), ..} => {
                    rect.y += 10;
                },
                _ => {}
            }
        }

        let _ = renderer.set_draw_color(black);
        let _ = renderer.clear();
        let _ = renderer.set_draw_color(white);
        let _ = renderer.fill_rect(rect);
        let _ = renderer.present();
    };

    #[cfg(target_os = "emscripten")]
    use emscripten::{emscripten};

    #[cfg(target_os = "emscripten")]
    emscripten::set_main_loop_callback(main_loop);

    #[cfg(not(target_os = "emscripten"))]
    loop { main_loop(); }
}
1 Like