Я пишу все большее количество Ржавчина-основанный на Васм за последние несколько лет. В Интернете много мнений о Васме, и Васм-биндген скажем так, не всеми любим, но по мере того, как я получаю больше опыта работы с ним и узнаю, как обойти его недостатки, я обнаружил некоторые закономерности, которые значительно улучшили мои отношения с ним.
Я хочу заранее прояснить две вещи:
- Я глубоко ценю работу Васм-биндген сопровождающие.
- Вполне возможно, что существуют более эффективные способы работы с привязкой, чем представленные здесь; это именно то, что сработало у меня на практике!
Я видел, как отличные программисты действительно боролись с биндгеном. Я не претендую на ответы на все вопросы, но в этом посте описан набор закономерностей, которые сделали Rust+Wasm для меня значительно менее болезненным.
Если у вас нет веской причины не делать этого:
- Передайте все за границу Васма
&reference - Предпочитать
Rcили> Arc1 над> &mut - Не выводить
Copyпо экспортируемым типам - Использовать
wasm_refgenдля любого типа, которому необходимо пересечь границу коллекции (Vecи т. д) - Префикс всех типов, экспортированных в Rust, с
Wasm*и установитеjs_name/js_classк имени без префикса - Префикс всех типов, импортированных из JS, с
Js* - Осуществлять
Fromс использованиемfor JsValue js_sys::Errorдля всех типов ошибок, экспортированных в Rust
Некоторые из них могут показаться странными без дополнительных объяснений. Ниже приведем более подробное обоснование.
wasm-bindgen генерирует связующий код, который позволяет вызывать структуры, методы и функции Rust из JS/TS. Некоторые типы Rust имеют прямые представления JS (те, которые реализуют IntoWasmAbi); другие полностью живут на стороне Wasm, и доступ к ним осуществляется через непрозрачные дескрипторы.
Привязки Wasm часто выглядят примерно так:
#[wasm_bindgen(js_name = Foo)]
pub struct WasmFoo(RustFoo)
#[wasm_bindgen(js_name = Bar)]
pub struct WasmBar(RustBar)
Концептуально JS содержит крошечные объекты, которые выглядят как { __wbg_ptr: 12345 }которые индексируются в таблицу на стороне Wasm, которая содержит реальные значения Rust.
Сложность заключается в том, что вы одновременно манипулируете двумя моделями памяти:
- JavaScript: сборщик мусора, реентерабельный, асинхронный
- Ржавчина: явное владение, заимствование, правила псевдонимов
Биндген пытается помочь, но он подходит как недостаточно, так и слишком: некоторые безопасные модели отвергаются, а некоторые прямые ножи с радостью принимаются. В конечном счете, все, что пересекает границу, должно иметь некоторое JS-представление, поэтому полезно знать, что это за представление.
WasmFoonn subgraph WasmFoon arc1[RustFoo]n endnn objID2((71902)) --> WasmBarn subgraph WasmBarn arc2[RustBar]n endn endn endnn jsFoo -.-> objID1n jsBar -.-> objID2"">flowchart TD
subgraph JavaScript
subgraph Foo
jsFoo["{ __wbg_ptr: 42817 }"]
end
subgraph Bar
jsBar["{ __wbg_ptr: 71902 }"]
end
end
subgraph Wasm
subgraph table[Boundary Table]
objID1((42817)) --> WasmFoo
subgraph WasmFoo
arc1[RustFoo]
end
objID2((71902)) --> WasmBar
subgraph WasmBar
arc2[RustBar]
end
end
end
jsFoo -.-> objID1
jsBar -.-> objID2
Стоит ли писать ручные привязки?
Я отношусь к большинству вещей так: «ты делаешь ты», и это во многом дело вкуса. Я вижу в Интернете довольно много кода, который, похоже, предпочитает ручное преобразование с помощью js_sys. Это разумная стратегия, но я считаю, что она требует много времени и хрупка. Если вы измените типы Rust, компилятор не поможет вам при вызове вручную dyn_into для выполнения проверок во время выполнения. Bindgen собирается вставлять одни и те же проверки во время выполнения в любом случае, но если вы прислушаетесь к его связующему звену (в том числе к некоторым шаблонам, представленным здесь), вы сможете получить гораздо лучшую обратную связь во время компиляции.
Это старая шутка, что две самые сложные проблемы в информатике — это именование, кэширование и ошибки с точностью до единицы. Именование чрезвычайно важно для мысленного оформления и отслеживания происходящего, и то и другое может стать большим источником боли при работе с биндгеном. Как правило, я использую текущие соглашения об именах:
Черта
IntoWasmAbi[…] Признак для всего, что можно преобразовать в тип, который может напрямую пересекать Wasm ABI.
— Источник
Это примитивные типы Wasm, такие как u32, String, Vecи так далее. Они преобразуются в/из собственных типов JS и Rust, когда пересекают границу. Нам не нужно ничего делать с этими типами.
Структуры, экспортированные из Rust Wasm*
Здесь вы обычно проводите большую часть своего времени. Обертывание перечислений и структур Rust в новые типы для повторного использования их в JS — это хлеб с маслом Wasm. Эти оболочки имеют префикс Wasm* чтобы отличить их от интерфейсов, импортированных из JS, IntoWasmAbi типы и простые объекты Rust. На стороне JS мы можем удалить Wasmпоскольку он получит только одно представление, и (если все сделано правильно) стороне JS обычно не нужно различать, откуда взялся тип.
#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq)]
pub enum StreetLight {
Red,
Yellow,
Gree,
}
#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq)]
#[wasm_bindgen(js_name = StreetLight)]
pub struct WasmStreetLight(StreetLight)
#[wasm_bindgen(js_class = StreetLight)]
impl WasmStreetLight {
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
Self(StreetLight::Red)
}
// ...
}
На стороне JS есть только один StreetLight, поэтому префикс исчезает. Со стороны Rust префикс визуально отличает экспортируемые типы от:
- Обычные типы ржавчины
- JS-импортированные интерфейсы
IntoWasmAbiценности
Интерфейсы, импортированные из JS Js*
Любой интерфейс, добавленный в Rust через extern "C" получить утка набрала интерфейс (по умолчанию). Они пересекают границу без ограничений, что делает их очень полезным аварийным люком ___.
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_name = logCurrentTime)]
pub fn js_log_current_time(timestamp: u32);
}
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_name = Hero)]
type JsCharacter;
#[wasm_bindgen(method, getter, js_name = hp)]
pub fn js_hp(this: &JsCharacter) -> u32;
}
// Elsewhere
const gonna_win: bool = maelle.js_hp() != 0
Утиная типизация действительно полезна в тех случаях, когда вы хотите предоставить JS особенность Rust: пока ваш экспортированный из Rust тип реализует интерфейс, вы можете принять ваш экспортированный из Rust тип как импортированный из JS, сохраняя при этом возможность заменять его типами, импортированными из JS. Конкретный пример: если вы экспортируете интерфейс хранилища, у вас, вероятно, есть реализация Rust по умолчанию, но вам нужна расширяемость, если последующие разработчики захотят предоставить ему серверную часть IndexedDB или S3.
Мы собираемся злоупотреблять этим «JS-импортом с утиным типом при экспорте из Rust» позже для
wasm_refgen.
Основные ошибки этого подхода заключаются в том, что 1. он становится хрупким, если интерфейс изменяется, и 2. если вы не добавляете к своим методам на стороне Rust префикс js_*вы можете столкнуться с коллизиями пространств имен (поэтому я рекомендую использовать их везде по соглашению). В качестве дополнительного бонуса это позволит вам очень знать, где вы вызываете методы за границей Wasm.
Copy позволяет легко случайно дублировать значение Rust, которое на самом деле является тонким дескриптором ресурса, что приводит к нулевым указателям. Просто возьмите за привычку избегать этого в экспортируемых упаковках. Эту мышечную память сложно сломать, поскольку обычно мы хотеть Copy везде, где это возможно, в обычном коде Rust.
Copy приемлемо только при экспорте переноса чистых данных, которые имеют IntoWasmAbiникогда для ручек. Я считаю это оптимизацией; по умолчанию нетCopy если только ты не Действительно конечно, все в порядке.
Как бы ни старался, wasm-bindgen не может предотвратить поломку дескрипторов во время выполнения. Распространенной причиной является передача в Rust собственного значения:
#[wasm_bindgen(js_class = Foo)]
impl WasmFoo {
#[wasm_bindgen(js_name = "doSomething")]
pub fn do_something(&self, bar: Bar) -> Result<(), Error> {
// ...
}
#[wasm_bindgen(js_name = "doSomethingElse")]
pub fn do_something_else(&self, bars: Vec<Bar>) -> Result<(), Error> {
// ...
}
}
Если вы сделаете вышеперечисленное, это, конечно, поглотит ваш Bar(s), но поскольку это выходит за рамки, вы не получаете никакой помощи от компилятора в том, как вы управляете частью JS! Объект будет освобожден на стороне Rust, но у вас все еще будет дескриптор JS, который теперь ни на что не указывает. Вы можете сказать что-то вроде «так много для безопасности памяти», и не ошибетесь.
Почему вы оказались в такой ситуации? Есть пара причин:
- Биндген запрещает
&[T]пока неT: IntoWasmAbi Vec<&T>не разрешено- Вы просто хотите, чтобы компилятор перестал кричать
Типы, которые имеют IntoWasmAbi это не Copy клонируются за границу (без дескриптора), поэтому они ведут себя иначе, чем не-IntoWasmAbi и Copy типы
Предпочитаю передачу по ссылке (по умолчанию)
Если вы возьмете что-то из этого поста, возьмите это:
Никогда не используйте экспортированные значения через границу, если у вас нет для этого явной причины и вы не собираетесь управлять дескриптором на стороне JS.
Это довольно просто: передавать все по ссылке. Использование значения абсолютно «законно» для компилятора, поскольку оно с радостью освободит память на стороне Rust. но дескриптор на стороне JS не будет очищен. В следующий раз, когда вы воспользуетесь этим дескриптором, он throw ошибка. Если вы не делаете что-то конкретное с управлением памятью, просто избегайте такой ситуации: проходите мимо &reference и использовать внутреннюю изменчивость.
Это довольно простой шаблон: по умолчанию используется перенос не-IntoWasmAbi вводит Rc или Arc1 в зависимости от того, структурирован ли ваш код для асинхронности и если да, то как. Стоимость перехода границы Васма определенно затмевает Rc удар, поэтому маловероятно, что это станет узким местом в производительности.
#[derive(Debug, Clone)]
#[wasm_bindgen(js_name = Foo)]
pub struct WasmFoo(pub(crate) Rc<RefCell<Foo>>)
#[derive(Debug, Clone)]
#[wasm_bindgen(js_name = Bar)]
pub struct WasmBar(pub(crate) Rc<RefCell<Bar>>)
#[wasm_bindgen(js_class = Foo)]
impl WasmFoo {
#[wasm_bindgen(js_name = "doSomething")]
pub fn do_something(&self, bar: WasmBar) -> Result<(), Error> {
// ...
}
}
Избегать &mut
Когда это происходит, это может быть очень неприятно: бывают случаи, когда принятие &mut self может вызывать ошибки времени выполнения из-за повторного входа. Это появляется чаще, чем я ожидал, учитывая, что поведение JS по умолчанию является однопоточным, но JS async не обязан уважать эксклюзивность Rust во время компиляции2 чеки.
Если вы не можете доказать исключительность, не притворяйтесь, что она у вас есть. Используйте соответствующий примитив внутренней изменчивости для вашей модели параллелизма.
Как упоминалось ранее, вы можете использовать extern "C" JS-импорт для моделирования любого интерфейса с утиным типом, включая Ржавчина-экспорт. Это означает, что мы можем обойти несколько ограничений.3 в wasm-bindgen.
Ограничение на принадлежащую коллекцию
Bindgen ограничивает типы, которые можно передавать через границу. Первое, с чем люди часто сталкиваются, это то, что &[T] работает только тогда, когда T является IntoWasmAbi (включая типы, импортированные из JS4) — то есть обычно это не ваши структуры, экспортированные из Rust. Это означает, что вам часто приходится создавать Vec. Это имеет смысл, поскольку JS возьмет на себя контроль над результирующим массивом JS и сможет изменять его по своему усмотрению. Это также означает, что когда тип возвращается, вы не можете принять его как &[T] или Vec если только раньше IntoWasmAbi применяется оговорка.
Классическим примером этого является возврат принадлежащего Vec вместо вместо кусочка, когда T не реализует тип, управляемый JS. То, что вернулось в JS, не является кучей Tс, а скорее ручки (например { __wbg_ptr: 12345 }) к Tкоторые живут на стороне Васма.4
С другой стороны, были способен обрабатывать дескрипторы как объекты типа «утка», соответствующие некоторому интерфейсу. Дескрипторы гораздо менее ограничены, чем типы, экспортированные в Rust, и их можно передавать более свободно.
Обходной путь довольно прост:
- Сделайте экспортированный тип дешевым для клонирования
- Предоставление метода клонирования в пространстве имен
- Импортируйте этот метод через интерфейс JS.
- Преобразуйте с удобной эргономикой (
.into)
// Step 1: make it inexpensive to `clone` (i.e. using `Rc` or `Arc` if not already cheap)
#[derive(Debug, Clone)]
#[wasm_bindgen(js_name = Character)]
pub struct WasmCharacter(Rc<RefCell<Character>>)
#[wasm_bindgen(js_class = Character)]
impl WasmCharacter {
// ...
// Step 2: expose a *namespaced* (important!) `clone` function on the Wasm export
#[doc(hidden)]
pub fn __myapp_character_clone(&self) -> Self {
self.clone()
}
}
#[wasm_bindgen]
extern "C" {
type JsCharacter
// Step 3: create a JS-imported interface with that namespaced `clone`
pub fn __myapp_character_clone(this: &JsCharacter) -> WasmCharacter;
}
// Step 4: for convenience, wrap the namespaced clone in a `.from`
impl From<JsChcaracter> for WasmCharacter {
fn from(js: JsCharacter) -> Self {
js.__myapp_character_clone()
}
}
// Nicely typed Vec
// Step 5: use it! vvvvv
pub fn do_many_things(js_foos: Vec<JsFoo>) {
let rust_foos: Vec<WasmFoo> = js_foos.iter().map(Into::into).collect();
// ... ^^^^^^^
// Converted
}
Это по-прежнему требует, чтобы вы вручную отслеживали, какие части Bindgen считает JS-импортом, а какие — экспортом Rust, но с нашей соглашение об именах довольно ясно, что происходит. Преобразование не бесплатноно (IMO) это делает ваши интерфейсы значительно более гибкими и разборчивыми.
Использовать wasm_refgen
Приведенный выше шаблон может быть немного хрупким — даже при написании шаблона — поскольку все имена должны выстраиваться в линию. просто таки вы не получите помощь компилятора при таком пересечении границы. Чтобы сделать это более надежным, я обернул этот шаблон в виде макроса, экспортированного из wasm_refgen.
use std::{rc::Rc, cell::RefCell};
use wasm_bindgen::prelude::*;
use wasm_refgen::wasm_refgen;
#[derive(Clone)]
#[wasm_bindgen(js_name = "Foo")]
pub struct WasmFoo {
map: Rc<RefCell<HashMap<String, u8>>>, // Cheap to clone
id: u32 // Cheap to clone
}
#[wasm_refgen(js_ref = JsFoo)] // <-- THIS
#[wasm_bindgen(js_class = "Foo")]
impl WasmFoo {
// ... your normal methods
}
Вот диаграмма из README о том, как это работает:
┌───────────────────────────┐
│ │
│ JS Foo instance │
│ Class: Foo │
│ Object { wbg_ptr: 12345 } │
│ │
└─┬──────────────────────┬──┘
│ │
│ │
Implements │
│ │
│ │
┌───────────▼───────────────┐ │
│ │ │
│ TS Interface: Foo │ Pointer
│ only method: │ │
│ __wasm_refgen_to_Foo │ │
│ │ │
└───────────┬───────────────┘ │
JS/TS │ │
─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┼ ─ ─ ─ ─ ─
Wasm │ │
│ │
┌───────────┼──────────────────────┼───────────┐
│ ▼ ▼ │
│ ┌────────────────┐ ┌────────────────┐ │
│ │ │ │ │ │
│ │ &JsFoo ◀────────▶ WasmFoo │ │
│ │ Opaque Wrapper │ │ Instance #1 │ │
│ │ │ │ │ │
│ └────────────────┘ └────────────────┘ │
└──────────────────────┬───────────────────────┘
│
│
Into::into
(uses `__wasm_refgen_to_Foo`)
(which is a wrapper for `clone`)
│
│
▼
┌────────────────┐
│ │
│ WasmFoo │
│ Instance #2 │
│ │
└────────────────┘
Ссылки, передаваемые через границу, уже передаются по принадлежности привязке, но эти дескрипторы захватывают ссылку из граничной таблицы. Напомним, что наш Into::into звонки clone под капотом, поэтому их всегда можно безопасно употреблять, не ломая ручку JS!
pub fn do_many_things(js_foos: Vec<JsFoo>) {
let rust_foos: Vec<WasmFoo> = js_foos.iter().map(Into::into).collect();
// ...
}
Есть несколько способов обработки ошибок, исходящих от Wasm, но, по моему мнению, лучший баланс детализации и удобства — превратить их в js_sys::Errorони едут в JsValue. Это позволяет нам вернуться Result вместо Result.
Например, предположим, что у нас есть этот тип:
#[derive(Debug, Clone, thiserror::Error)]
pub enum RwError {
#[error("cannot read {0}")]
CannotRead(String),
#[error("cannot write")]
CannotWrite
}
Тот факт, что это перечисление, на самом деле не является проблемой (остальная часть метода будет работать), но если вы обертываете другой контейнер, вам понадобится оболочка newtype:
// Important: no #[wasm_bindgen]
#[derive(Debug, Clone, thiserror::Error)]
#[error(transparent)]
pub struct WasmRwError(#[from] RwError) // #[from] gets us `?` notation to lift into the newtype
Мы «могли» дать пощечину #[wasm_bindgen] на этом и заканчиваем, но тогда мы не получим хорошую информацию об ошибках на стороне JS. Вместо этого мы преобразуем в JsValue себя с этим последним кусочком клея:
impl From<WasmRwError> for JsValue {
fn from(wasm: WasmRwError) -> Self {
let err = js_sys::Error::new(&wasm.to_string()); // Error message
err.set_name("RwError"); // Nice JS error type
err.into() // Convert to `JsValue`
}
}
Теперь ты можешь вернуться Resultв том числе если вы хотите вызвать функцию, завернутую в Wasm, в другом месте вашего кода. Он сохраняет приятную ошибку на стороне Rust (минимальные типы в документации). Вы также получаете ? обозначение без необходимости делать на месте JsValue конвертация везде, где возникает эта ошибка; bingen выполнит преобразование за вас.
- Типизированные ошибки Rust
?распространение- Настоящий JS
Errorобъекты - Нулевой шаблон на сайтах звонков
Это работает как шаблон копирования-вставки; Я подумывал обернуть его как макрос, но это меньше 10 LOC. Я был действительно удивлен, что что-то вроде #[wasm_bindgen(error)] был недоступен (возможно, он есть, и я просто не могу его найти; черт возьми, может быть, стоит внести свой вклад).
Это улучшение качества жизни, которое избавило меня от многих часов горя: распечатайте точную версию сборки, грязный статус и хэш Git в файл. console при запуске. Если вы работаете над своим проектом Wasm одновременно с разработкой библиотеки на чистом JS, которая его использует, приобретите сборщик JS, например Vite восприятие изменений может быть в лучшем случае ненадежным.
Это требует некоторой настройки, особенно если вы находитесь в рабочем пространстве Cargo, но оно того стоит. Вот моя текущая настройка:
[workspace]
resolver = "3"
members = [
"build_info",
# ...
]
[package]
name = "build_info"
publish = false
# ...
use std::{
env, fs,
path::{Path, PathBuf},
process::Command,
time::{SystemTime, UNIX_EPOCH},
};
#[allow(clippy::unwrap_used)]
fn main() {
let ws = env::var("CARGO_WORKSPACE_DIR").map_or_else(
|_| PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()),
PathBuf::from,
);
let repo_root = find_repo_root(&ws).unwrap_or(ws.clone());
let git_dir = repo_root.join(".git");
watch_git(&git_dir);
let git_hash = cmd_out(
"git",
&[
"-C",
#[allow(clippy::unwrap_used)]
repo_root.to_str().unwrap(),
"rev-parse",
"--short",
"HEAD",
],
)
.unwrap_or_else(|| "unknown".to_string());
let dirty = cmd_out(
"git",
&["-C", repo_root.to_str().unwrap(), "status", "--porcelain"],
)
.is_some_and(|s| !s.is_empty());
let git_hash = if dirty {
let secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
format!("{git_hash}-dirty-{secs}")
} else {
git_hash
};
println!("cargo:rustc-env=GIT_HASH={git_hash}");
}
fn cmd_out(cmd: &str, args: &[&str]) -> Option<String> {
Command::new(cmd).args(args).output().ok().and_then(|o| {
if o.status.success() {
Some(String::from_utf8_lossy(&o.stdout).trim().to_string())
} else {
None
}
})
}
fn find_repo_root(start: &Path) -> Option<PathBuf> {
let mut cur = Some(start);
while let Some(dir) = cur {
if dir.join(".git").exists() {
return Some(dir.to_path_buf());
}
cur = dir.parent();
}
None
}
fn watch_git(git_dir: &Path) {
println!("cargo:rerun-if-changed={}", git_dir.join("HEAD").display());
if let Ok(head) = fs::read_to_string(git_dir.join("HEAD")) {
if let Some(rest) = head.strip_prefix("ref: ").map(str::trim) {
println!("cargo:rerun-if-changed={}", git_dir.join(rest).display());
println!(
"cargo:rerun-if-changed={}",
git_dir.join("packed-refs").display()
);
}
}
println!("cargo:rerun-if-changed={}", git_dir.join("index").display());
let fetch_head = git_dir.join("FETCH_HEAD");
if fetch_head.exists() {
println!("cargo:rerun-if-changed={}", fetch_head.display());
}
}
#![no_std]
pub const GIT_HASH: &str = env!("GIT_HASH");
… и, наконец, где его можно распечатать в Wasm:
use wasm_bindgen::prelude::*;
// ...
#[wasm_bindgen(start)]
pub fn start() {
set_panic_hook();
// I actually use `tracing::info!` here,
// but that's out of scope for this article
web_sys::console.info1(format!(
"️your_package_wasm v{} ({})",
env!("CARGO_PKG_VERSION"),
build_info::GIT_HASH
));
}
Rust+Wasm — мощный инструмент, но он не прощает ошибок, если вы притворяетесь, что границы нет. Будьте откровенны, четко называйте вещи, передавайте по ссылке и избегайте ввода любых (необоснованных) ограничений, налагаемых на вас привязкой.
Если повезет, это поможет другим! Я могу обновить это со временем, поскольку буду использовать больше шаблонов.
2026-03-08 09:24:00
1772967211
#Примечания #по #написанию #Wasm
Ещё по этой теме
