Мы уже видели насколько WebAssembly быстро компилируется, ускоряет js библиотеки и генерирует более компактные бинарники. У нас даже есть общее представление как наладить взаимодействие не только между сообществами Rust и JavaScript, но и с сообществами других языков. В прошлой статье мы упоминали специальный инструмент wasm-bindgen и сейчас я бы хотел остановиться на нем более подробно.

На данный момент спецификация WebAssembly описывает только четыре типа данных: два целочисленных и два с плавающей точкой. Однако большую часть времени JS и Rust разработчики используют куда более богатую систему типов. Например, JS разработчики взаимодействуют с объектом document для того чтоб добавить или изменить узлы HTML, в то время как Rust разработчики работают с такими типами как Result для обработки ошибок, и практически все разработчики работают со строками.

Быть ограниченными только теми типами, которые определяет WebAssembly, было бы слишком неудобно и тут нам на помощь приходит wasm-bindgen. Основная задача wasm-bindgen — это предоставить мост между системами типов Rust и JS. Он позволяет JS функции вызывать Rust API передавая обычные строки или Rust функции перехватить исключение из JS. wasm-bindgen компенсирует несовпадения типов и дает возможность эффективного и простого использования WebAssembly функций из JavaScript и обратно.

Более подробное описание проекта wasm-bindgen вы можете найти на нашем README. Для начала давайте разберем простой пример использования wasm-bindgen, а потом посмотрим как вы еще сможете его использовать.

Привет мир!

Вечная классика. Один из лучших способов попробовать новый инструмент — это изучить его вариацию вывода сообщения "Привет мир". В данном случае мы рассмотрим пример, который делает именно это — выводит диалоговое окно с надписью "Hello World".

Цель здесь проста, мы хотим создать Rust функцию, которая получая имя выводит диалоговое окно с надписью Hello, ${name}! . В JavaScript мы бы описали ее так:

export function greet(name) { alert(`Hello, ${name}!`); }

Однако мы хотим написать эту функцию на Rust. Для того чтоб это работало, нам потребуются следующие шаги

JavaScript должен вызвать модуль WebAssembly, который экспортирует функцию greet.

Rust функция примет строку, которая будет содержать имя, в качестве аргумента.

Внутри Rust функции мы создаем новую строку и интерполируем в нее переданное имя.

И, наконец, Rust вызовет JavaScript функцию alert используя созданную строку в качестве аргумента.

Для начала создадим новый Rust проект:

cargo new wasm-greet --lib

Эта команда создаст папку wasm-greet, в которой мы с вами будем работать. Следующим шагом надо добавить в наш Cargo.toml (аналог package.json для Rust) следующую информацию:

[lib] crate-type = ["cdylib"] [dependencies] wasm-bindgen = "0.2"

Содержимое секции lib мы пока пропустим, а в секции dependencies мы указываем зависимость нашего проекта от пакета wasm-bindgen. Этот пакет включает в себя все необходимое для использования wasm-bindgen в нашем проекте.

А теперь давайте добавим немного кода! Замените содержимое src/lib.rs следующим кодом:

#![feature(proc_macro, wasm_custom_section, wasm_import_module)] extern crate wasm_bindgen; use wasm_bindgen::prelude::*; #[wasm_bindgen] extern { fn alert(s: &str); } #[wasm_bindgen] pub fn greet(name: &str) { alert(&format!("Hello, {}!", name)); }

Если вы не знакомы с Rust, пример выше может показаться вам немного многословным, но не волнуйтесь. Проект wasm-bindgen постоянно совершенствуется и я уверен, что в будущем необходимость столь подробного описания будет устранена. Наиболее важная часть здесь это аттрибут #[wasm_bindgen] . Это аннотация в Rust, которая говорит, что эту функцию надо при необходимости обернуть в другую функцию. Обе наши функции(и импорт функции alert и экспорт функции greet ) имеют данный аттрибут. Чуть позже мы заглянем "под капот" и посмотрим что там происходит.

Но сначала давайте скомпилируем наш wasm код и откроем его в браузере:

$ rustup target add wasm32-unknown-unknown --toolchain nightly # потребуется только первый запуск $ cargo +nightly build --target wasm32-unknown-unknown

По завершению мы получим wasm файл, который будет находиться target/wasm32-unknown-unknown/debug/wasm_greet.wasm . Если мы воспользуемся чем-то вроде wasm2wat и заглянем внутрь этого файла, его содержимое может показаться немного пугающим. Оказывается, что wasm файл еще не готов для использования из JS. Для этого нам потребуется еще один шаг:

$ cargo install wasm-bindgen-cli # потребуется только первый запуск $ wasm-bindgen target/wasm32-unknown-unknown/debug/wasm_greet.wasm --out-dir .

Как раз на этом шаге и происходит вся магия. Команда wasm-bindgen выполняет обработку wasm файла и делает его готовым к использованию. Чуть позже мы рассмотрим что значит "готов к использованию", а сейчас достаточно сказать, что если мы импортируем только что созданный модуль wasm_greet.js , то там будет содержаться функция greet , которая объявлена в Rust.

Теперь мы можем использовать упаковщик и создать HTML страницу, на которой и выполнится наш код. На момент написания этой статьи только Webpack 4.0 имеет достаточную поддержку WebAssembly чтоб работать из коробки(однако на данный момент есть проблема с браузером Хром). Несомненно, со временем все больше упаковщиков будут добавлять поддержку WebAssembly. Я не буду вдаваться в детали. Вы можете посмотреть примерную конфигурацию для WebPack в репозитории. Если мы посмотрим на содержимое нашего JS файла, то увидим следующее:

const rust = import("./wasm_greet"); rust.then(m => m.greet("World!"));

… и на этом все. Открыв нашу страницу в браузере мы увидим диалоговое окно с надписью Hello, World! , которое создано в Rust.

Как работает wasm-bindgen

Фух, это был довольно большой Hello, World! . Давайте посмотрим немного на то, что происходит под капотом и как этот инструмент работает.

Один из наиболее важных аспектов wasm-bindgen — это то, что интеграция основана на фундаментальной концепции что wasm модуль это просто другой тип ES модуля. В примере выше мы просто хотели создать ES модуль со следующей сигнатурой(TypeScript):

export function greet(s: string);

У WebAssembly нет возможности сделать это(помните, что на данный момент wasm поддерживает только числа), по этому мы используем wasm-bindgen чтоб заполнить пробелы. На последнем шаге прошлого примера, когда мы запустили команду wasm-bindgen она создала не только файл wasm_greet.js , но и wasm_greet_bg.wasm . Первый — это и есть наш JS интерфейс, который и позволяет нам вызвать Rust код. А файл *_bg.wasm содержит реализацию и весь скомпилированный код.

Когда мы импортируем модуль ./wasm_greet , мы получаем тот Rust код, который бы хотели вызывать из JS, но на данном этапе у нас нет возможности делать это нативно. Теперь, когда мы рассмотрели процесс интеграции, давайте посмотрим на выполнение этого кода.

const rust = import("./wasm_greet"); rust.then(m => m.greet("World!"));

Здесь мы асинхронно импортируем наш интерфейс, ждем пока он будет готов(скачивание и компиляция wasm модуля), и вызываем функцию greet .

Обратите внимание на то, что асинхронная загрузка — это требование Webpack, но это возможно будет не всегда и может быть реализовано по-другому в других упаковщиков.

Если мы посмотрим на содержимое файла wasm_greet.js , который был сгенерирован wasm-bindgen , то мы увидим нечто подобное:

import * as wasm from './wasm_greet_bg'; // ... export function greet(arg0) { const [ptr0, len0] = passStringToWasm(arg0); try { const ret = wasm.greet(ptr0, len0); return ret; } finally { wasm.__wbindgen_free(ptr0, len0); } } export function __wbg_f_alert_alert_n(ptr0, len0) { // ... }

Обратите внимание. Это не оптимизированный и сгенерированный автоматически код и он не всегда красивый или маленький. В процессе оптимизации при линковке, релизной сборки в Rust и после прохождения через минификатор он будет намного меньше.

Здесь мы видим как wasm-bindgen сгенерировал для нас функцию greet . Под капотом он все еще вызывает функцию greet и wasm модуля, но теперь она вызывается не со строкой, а с передачей указателя и длины в качестве аргументов. Больше информации о функции passStringToWasm вы можете найти в статье от Lin Clark. Если бы мы не использовали wasm-bindgen, нам бы пришлось написать весь этот код самостоятельно. Чуть позже мы вернемся к функции __wbg_f_alert_alert_n .

Спустившись на уровень ниже, мы найдем следующий интересный пункт — функция greet в WebAssembly. Давайте посмотрим на код, который видит компилятор Rust. Обратите внимание, что подобно JS коду, который сгенерирован выше, вы не писали руками экспортируемый символ greet . wasm-bindgen сгенерировал все необходимое самостоятельно, а именно:

pub fn greet(name: &str) { alert(&format!("Hello, {}!", name)); } #[export_name = "greet"] pub extern fn __wasm_bindgen_generated_greet(arg0_ptr: *mut u8, arg0_len: usize) { let arg0 = unsafe { ::std::slice::from_raw_parts(arg0_ptr as *const u8, arg0_len) } let arg0 = unsafe { ::std::str::from_utf8_unchecked(arg0) }; greet(arg0); }

Здесь мы видим нашу функцию greet , а так же дополнительно сгенерированную при помощи аттрибута #[wasm_bingen] функцию __wasm_bindgen_generated_greet . Это и есть экспортируемая функция (на это указывает аттрибут #[export_name] и ключевое слово extern ), которая принимает указатель и длину строки. Затем он конвертирует эту пару в &str(строка в Rust) и передает её нашей функции greet .

Другими словами wasm-bindgen генерирует две обёртки: одну в JavaScript, которая преобразует типы из JS в wasm и одну в Rust, которая принимает типы wasm и конвертирует в Rust.

Хорошо, давайте посмотрим на последний набор оберток для функции alert . Функция greet в Rust использует стандартный макрос format! для создания новой строки и затем передает её функции alert . Помните, когда мы объявили функцию alert , мы использовали аттрибут #[wasm_bindgen] , теперь давайте посмотрим, что увидит компилятор Rust:

fn alert(s: &str) { #[wasm_import_module = "__wbindgen_placeholder__"] extern { fn __wbg_f_alert_alert_n(s_ptr: *const u8, s_len: usize); } unsafe { let s_ptr = s.as_ptr(); let s_len = s.len(); __wbg_f_alert_alert_n(s_ptr, s_len); } }

Это не совсем то, что мы написали, но зато мы можем здесь наглядно видеть что происходит. Функция alert на самом деле это тонкая обертка, которая принимает строку &str и далее конвертирует его в понятные для wasm числа. Затем вызывается функция __wbg_f_alert_alert_n и тут есть любопытная часть — это аттрибут #[wasm_import_module] .

Для того чтоб импортировать функцию в WebAssembly нужен модуль, который её содержит. И так как wasm-bindgen построен на ES модулях то импорт такой функции из wasm будет интерпретирован как import из ES модуля. Модуль __wbindgen_placeholder__ на самом деле не существует, эта срока указывает на то, что это импорт должен быть обработан wasm-bindgen и сгенерирована обертка для JS.

И, наконец, мы получаем наш последний кусочек пазла — сгенерированный JS файл, который содержит:

export function __wbg_f_alert_alert_n(ptr0, len0) { let arg0 = getStringFromWasm(ptr0, len0); alert(arg0) }

Как выяснилось, довольно много всего происходит под капотом и мы прошли довольно долгий путь для вызова JS функции в браузере. Но не переживайте, ключевой аспект wasm-bindgen в том, что все это скрыто. Вы можете просто писать Rust код с несколькими аттрибутами #[wasm_bindgen] тут и там. А потом ваш JS код сможет его использовать так, как будто это еще один JavaScript модуль.

На что еще способен wasm-bindgen?

Проект wasm-bindgen весьма амбициозен, охватывает большую область и на данный момент у меня нет достаточного количества времени чтоб все описать. Хороший способ увидеть его в деле — это ознакомиться с нашими примерами, от простого Hello World!, до манипуляции узлами DOM дерева из Rust.

В общих чертах, основные возможности wasm-bindgen:

Импортирование JS структур, функций, объектов и т.д. для использования в wasm. Вы можете вполне естественно вызывать JS методы у структур и получать доступ к свойствам из Rust после того как выставлены все аттрибуты #[wasm_bindgen]

Экспортировать структуры и функции Rust для использования в JS. Вместо того чтоб работать только с числами вы можете экспортировать структуру из Rust, которая превратится в JS класс. Вы сможете передавать не просто числа, но и структуры туда и обратно. Следующий пример даст вам представление о возможной интер операбельности.

И другие возможности вроде использования глобальных функций(таких, как alert ), перехватывать исключения из JS, используя тип данных Result в Rust и обобщенный способ симуляции сохранения значений из JS в программе на Rust.

Если вам интересно узнать о дополнительных функциях следите за нашим трекером.

Что дальше для wasm-bindgen?

До завершения я бы хотел рассказать немного о будущем проекта wasm-bindgen так как это одна из самых волнительных тем.

Поддержка других языков, кроме Rust

С самого первого дня wasm-bindgen был спроектирован с прицелом на то, что он сможет быть использован из многих языков. В то время как Rust пока что единственный поддерживаемый язык, инструмент позволит в дальнейшем так же добавить C/C++. Аттрибут #[wasm_bindgen] создает дополнительную секцию в файле .wasm , которую парсит и затем удаляет wasm-bindgen . В этой секции описано какие биндинги надо сгенерировать в JS и их интерфейс. В этой секции нет ничего Rust-специфичного, так что плагин с С/С++ компилятору так же сможет создать ее, чтоб потом была возможность использовать wasm-bindgen .

Для меня это наиболее волнующий момент потому что я верю, что именно это позволит инструментам вроде wasm-bindgen стать стандартом для обеспечения взаимодействия WebAssembly и JS. Я надеюсь, что возможность обойтись без лишнего конфигурационного кода станет преимуществом для всех языков, которые могут быть скомпилированы в WebAssembly.

Автоматическая генерация биндингов к JS

На данный момент, один из недостатков при импортировании JS функции с помощью #[wasm_bindgen] — это то, что вам надо описывать все функции самостоятельно и следить за тем, чтоб не возникло ошибок. Временами этот процесс может быть весьма утомительным(и быть источником ошибок) и он требует автоматизации.

Все Web API указаны и описаны в WebIDL и это должно быть вполне возможно сгенерировать все биндинги автоматически из WebIDL. Это означает, что вам не надо будет определять функцию alert как мы делали в примере выше, вместо этого вы могли бы написать что-то вроде этого:

#[wasm_bindgen] pub fn greet(s: &str) { webapi::alert(&format!("Hello, {}!", s)); }

В этом случае пакет webapi мог бы быть автоматически сгенерирован из описаний WebIDL API и это бы гарантировало отсутствие ошибок.

Мы можем развить эту идею еще дальше и использовать впечатляющую работу TypeScript сообщества и генерировать биндинги так же из TypeScript. Это позволит автоматически использовать любой пакет с npm у которого есть поддержка TypeScript.

Более быстрые операции с DOM чем в JS

И последний по порядку, но не последний по значимости на горизонте wasm-bindgen, супер быстрые манипуляции c DOM — святой грааль многих JavaScript фреймворков. Сегодня все вызовы функций для работы с DOM проходят через дорогостоящие преобразования при переходе от JavaScript к C++ движкам. С помощью WebAssembly эти преобразования могут стать необязательными. Известно, что система типов WebAssembly… есть!

Генерация кода wasm-bindgen с самого первого дня спроектирована с прицелом на поддержку бидингов к хосту. Как только эта функция появится в WebAssembly, у нас будет возможность напрямую использовать импортированные функции без оберток, которые генерирует wasm-bindgen. Более того, это позволит JS движкам агрессивно оптимизировать манипуляции с DOM из WebAssembly, так как все интерфейсы будут строго типизированны и больше не будет необходимости их валидировать. И в таком случае wasm-bindgen не только сделает проще работу с различными типами данных, но и обеспечит лучшую в своем роде производительность при работе с DOM.

Подводя итоги

Я считаю работу с WebAssembly невероятно интересной не только из-за сообщества, но так же из-за того с какой скоростью он развивается. У проекта wasm-bindgen светлое будущее. Он не только обеспечивает простую интероперабельность между JS и Rust, но и в долгосрочной перспективе откроет новые возможности по мере развития WebAssembly.

Попробуйте wasm-bindgen, создайте запрос на новую функцию, и оставайтесь на связи с Rust и WebAssembly.