Как я могу обойтись без возможности экспортировать функции со временем жизни при использовании 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 изнутри, поэтому это не работает.

Кто-то предложил альтернативный подход - следовать шаблону, показанному в жизненный пример. На высоком уровне он включает в себя экспорт типа, который содержит состояние игры и включает общедоступные функции tick() и render(), которые можно вызывать из игрового цикла javascript. Это не работает для меня, потому что мое игровое состояние требует параметров времени жизни, поскольку оно фактически просто обертывает структуры спецификацииWorld и Dispatcher, последняя из которых имеет параметры времени жизни. В конечном итоге это означает, что я не могу экспортировать его с помощью #[wasm_bindgen].

Мне сложно найти способы обойти эти ограничения, и я ищу предложения.

6
0
1 212
2

Ответы 2

Самый простой способ смоделировать это, вероятно, оставить вызовы 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.

Это определенно приблизило меня, но сейчас я столкнулся с другой проблемой. Я создал структуру Game, которая обертывает любые типы, для которых требуется объявить время жизни 'static. Я поменял подпись main_loop на fn main_loop(mut game: Game) и поставил закрывающую игру move. Это не работает, потому что теперь закрытие не может быть обернуто wasm Closure. Казалось бы, единственный другой вариант - использовать unsafe и объявить static mut для хранения состояния игры. Или есть еще одно более чистое решение?

w.brian 02.11.2018 02:08

Хм, я не уверен, что понимаю, что не работает с новым game: Game, у вас есть суть сообщения об ошибке, почему его нельзя обернуть в wasm Closure?

alexcrichton 02.11.2018 22:28

Проблема в том, что при добавлении move закрытие становится FnOnce, что несовместимо с Closure::wrap(). Это имеет смысл, учитывая, что нет никакого способа статически гарантировать, что FnOnce, обернутый Closure, на самом деле запускается только один раз в js land. Кстати, очень ценю вашу помощь!

w.brian 03.11.2018 18:30

Ах да, понятно! Для этого вы можете пока обойти это, используя Option<T>, а затем take, чтобы это было закрытие FnMut (но паникуйте, если вызовите дважды). Это то, что мы должны добавить поддержку в wasm-bindgen! (Закрытие FnOnce)

alexcrichton 05.11.2018 16:33

Я новичок в Rust, но вот как я решил ту же проблему.

Вы можете устранить проблемную рекурсию window.request_animation_frame и одновременно реализовать ограничение FPS, вызвав window.request_animation_frame из обратного вызова window.set_interval, который проверяет Rc<RefCell<bool>> или что-то еще, чтобы увидеть, есть ли еще ожидающий запрос кадра анимации. Я не уверен, будет ли поведение неактивной вкладки отличаться на практике.

Я помещаю bool в состояние своего приложения, так как в любом случае использую 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_interval и timer_closure в Option в вашем игровом состоянии, чтобы ваша игра могла очистить себя, если это необходимо по какой-либо причине (возможно? Я не пробовал это, и, похоже, это приведет к освобождению от self? ). Циклическая ссылка не стирается, пока не будет нарушена (тогда вы эффективно сохраняете Rc в приложении внутри приложения). Это также должно позволить вам изменять максимальную частоту кадров во время бега, останавливая интервал и создавая другой, используя то же закрытие.

Другие вопросы по теме