使用wasm-bindgen时,如何解决无法导出具有生存期的功能?

时间:2018-10-26 01:58:53

标签: rust webassembly wasm-bindgen

我正在尝试编写一个可以在浏览器中运行的简单游戏,并且由于浏览器,rust和wasm-bindgen的综合限制,我很难建模游戏循环。

浏览器中的典型游戏循环遵循以下一般模式:

function mainLoop() {
    update();
    draw();
    requestAnimationFrame(mainLoop);
}

如果我要在rust / wasm-bindgen中建模这种确切的模式,它将看起来像这样:

let main_loop = Closure::wrap(Box::new(move || {
    update();
    draw();
    window.request_animation_frame(main_loop.as_ref().unchecked_ref()); // Not legal
}) as Box<FnMut()>);

与javascript不同,我无法从自身内部引用main_loop,所以这行不通。

有人建议的另一种方法是遵循game of life example中说明的模式。从高层次上讲,它涉及导出包含游戏状态并包括公共tick()render()函数的类型,可以从javascript游戏循环中调用它们。这对我不起作用,因为我的游戏状态需要生命周期参数,因为它实际上只是包装了specs WorldDispatcher结构,后者具有生命周期参数。最终,这意味着我无法使用#[wasm_bindgen]导出它。

我很难找到解决这些限制的方法,并且正在寻找建议。

2 个答案:

答案 0 :(得分:5)

对此进行建模的最简单方法可能是将requestAnimationFrame的调用留给JS,而只是在Rust中实现更新/绘制逻辑。

但是,在Rust中,您还可以利用以下事实:实际上并未捕获任何变量的闭包大小为零,这意味着该闭包的Closure<T>不会分配内存,并且您可以放心地忘记它。例如,类似这样的方法应该起作用:

#[wasm_bindgen]
pub fn main_loop() {
    update();
    draw();
    let window = ...;
    let closure = Closure::wrap(Box::new(|| main_loop()) as Box<Fn()>);
    window.request_animation_frame(closure.as_ref().unchecked_ref());
    closure.forget(); // not actually leaking memory
}

如果状态中存在生命周期,那么很遗憾,这与返回JS不兼容,因为当您一路返回JS事件循环时,所有WebAssembly堆栈框架都会弹出,这意味着任何生命周期都会失效。这意味着您的游戏状态在main_loop的各个迭代中持续存在,需要为'static

答案 1 :(得分:1)

我是Rust的新手,但这是我解决相同问题的方法。

您可以通过从window.request_animation_frame回调中调用window.request_animation_frame来检查window.set_interval或要查看的内容,从而消除有问题的Rc<RefCell<bool>>递归并实现FPS上限如果仍然有动画帧请求待处理。我不确定无效的标签页行为在实践中是否会有所不同。

因为我一直使用Rc<RefCell<...>>来进行其他事件处理,所以我将布尔值置于应用程序状态。我尚未检查下面的代码是否可以按原样编译,但这是我执行此操作的相关部分:

pub struct MyGame {
    ...
    should_request_render: bool, // Don't request another render until the previous runs, init to false since we'll fire the first one immediately.
}

...

let window = web_sys::window().expect("should have a window in this context");
let application_reference = Rc::new(RefCell::new(MyGame::new()));

let request_animation_frame = { // request_animation_frame is not forgotten! Its ownership is moved into the timer callback.
    let application_reference = application_reference.clone();
    let request_animation_frame_callback = Closure::wrap(Box::new(move || {
        let mut application = application_reference.borrow_mut();
        application.should_request_render = true;
        application.handle_animation_frame(); // handle_animation_frame being your main loop.
    }) as Box<FnMut()>);
    let window = window.clone();
    move || {
        window
            .request_animation_frame(
                request_animation_frame_callback.as_ref().unchecked_ref(),
            )
            .unwrap();
    }
};
request_animation_frame(); // fire the first request immediately

let timer_closure = Closure::wrap(
    Box::new(move || { // move both request_animation_frame and application_reference here.
        let mut application = application_reference.borrow_mut();
        if application.should_request_render {
            application.should_request_render = false;
            request_animation_frame();
        }
    }) as Box<FnMut()>
);
window.set_interval_with_callback_and_timeout_and_arguments_0(
    timer_closure.as_ref().unchecked_ref(),
    25, // minimum ms per frame
)?;
timer_closure.forget(); // this leaks it, you could store it somewhere or whatever, depends if it's guaranteed to live as long as the page

您可以在游戏状态下将set_intervaltimer_closure的结果存储在Option中,以便您的游戏可以出于某些原因(如有必要)进行清理(也许?没有尝试过,这似乎会导致self释放?)。循环引用除非被破坏(除非将Rc有效地存储到应用程序内部的应用程序中),否则不会擦除自身。它还应该使您可以在运行时更改最大fps,方法是停止间隔并使用相同的闭包创建另一个。