Как вернуть закрытие Rust в JavaScript через WebAssembly?

Комментарии к closure.rs довольно хороши, однако я не могу заставить его возвращать замыкание из библиотеки WebAssembly.

У меня есть такая функция:

#[wasm_bindgen]
pub fn start_game(
    start_time: f64,
    screen_width: f32,
    screen_height: f32,
    on_render: &js_sys::Function,
    on_collision: &js_sys::Function,
) -> ClosureTypeHere {
    // ...
}

Внутри этой функции я делаю замыкание, предполагая Closure::wrap это одна часть головоломки, и копирование с closure.rs):

let cb = Closure::wrap(Box::new(move |time| time * 4.2) as Box<FnMut(f64) -> f64>);

Как мне вернуть этот обратный звонок от start_game и что должно ClosureTypeHere быть?

Идея в том, что start_game создаст локальные изменяемые объекты - например, камеру, и сторона JavaScript должна иметь возможность вызывать функцию, возвращаемую Rust для обновления этой камеры.

2 ответа

Решение

Это хороший вопрос, и у него тоже есть свой нюанс! Стоит назвать пример замыканий в wasm-bindgen руководство (и раздел о передаче замыканий в JavaScript), и было бы неплохо внести свой вклад в это, если это необходимо!

Чтобы начать, вы можете сделать что-то вроде этого:

use wasm_bindgen::{Closure, JsValue};

#[wasm_bindgen]
pub fn start_game(
    start_time: f64,
    screen_width: f32,
    screen_height: f32,
    on_render: &js_sys::Function,
    on_collision: &js_sys::Function,
) -> JsValue {
    let cb = Closure::wrap(Box::new(move |time| {
        time * 4.2
    }) as Box<FnMut(f64) -> f64>);

    // Extract the `JsValue` from this `Closure`, the handle
    // on a JS function representing the closure
    let ret = cb.as_ref().clone();

    // Once `cb` is dropped it'll "neuter" the closure and
    // cause invocations to throw a JS exception. Memory
    // management here will come later, so just leak it
    // for now.
    cb.forget();

    return ret;
}

Над возвращаемым значением находится просто старый JS-объект (здесь как JsValue) и мы создаем это с Closure типа вы уже видели. Это позволит вам быстро вернуть замыкание в JS, и вы сможете также вызвать его из JS.

Вы также спрашивали о хранении изменяемых объектов и тому подобного, и все это можно сделать с помощью обычных замыканий в Rust, захвата и т. Д. Например, объявление FnMut(f64) -> f64 выше подпись функции JS, и это может быть любой набор типов, таких как FnMut(String, MyCustomWasmBindgenType, f64) -> Vec<u8> если ты действительно хочешь. Для захвата локальных объектов вы можете сделать:

let mut camera = Camera::new();
let mut state = State::new();
let cb = Closure::wrap(Box::new(move |arg1, arg2| { // note the `move`
    if arg1 {
        camera.update(&arg2);
    } else {
        state.update(&arg2);
    }
}) as Box<_>);

(или что-то типа того)

Здесь camera а также state Переменные будут принадлежать закрытию и удаляться одновременно. Больше информации о замыканиях можно найти в книге Rust.

Здесь также стоит кратко остановиться на аспекте управления памятью. В приведенном выше примере мы звоним forget() Это приводит к утечке памяти и может стать проблемой, если функция Rust вызывается много раз (так как это приведет к утечке большого количества памяти). Основная проблема заключается в том, что в куче WASM выделена память, на которую ссылается созданный объект функции JS. Эта распределенная память теоретически должна быть освобождена всякий раз, когда объект функции JS является GC'd, но мы не можем знать, когда это произойдет (до WeakRef существует!)

Тем временем мы выбрали альтернативную стратегию. Связанная память освобождается всякий раз, когда Closure Сам тип отбрасывается, обеспечивая детерминированное уничтожение. Это, однако, затрудняет работу, так как нам нужно выяснить вручную, когда отбрасывать Closure, Если forget не работает для вашего случая использования, некоторые идеи для Closure являются:

  • Во-первых, если JS-замыкание вызывается только один раз, вы можете использовать Rc/RefCellбросить Closure внутри самого замыкания (используя некоторые внутренние изменчивые махинации). Мы также должны в конечном итоге оказать нативную поддержку FnOnce в wasm-bindgen также!

  • Затем вы можете вернуть вспомогательный объект JS в Rust, который имеет руководство freeметод. Например, #[wasm_bindgen]Аннотированная обертка. Эта обертка должна быть вручную освобождена в JS при необходимости.

Если вы можете пройти, forget На данный момент это самая простая вещь, но это определенно больно! Мы не можем ждать WeakRef существовать:)

Насколько я понимаю из документации, не предполагается экспортировать замыкания в Rust, они могут передаваться только в качестве параметров импортированным функциям JS, но все это происходит в коде Rust.

https://rustwasm.github.io/wasm-bindgen/reference/passing-rust-closures-to-js.html

Я провел пару экспериментов, и когда функция Rust возвращает упомянутый тип 'Closure', компилятор выдает исключение: the trait wasm_bindgen::convert::IntoWasmAbi is not implemented for wasm_bindgen::prelude::Closure<(dyn std::ops::FnMut() -> u32 + 'static)>

Во всех примерах замыкания заключены в произвольный контур, но после этого вы уже не можете вызывать это на стороне JS.

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