[Перевод] От JavaScript к Rust и обратно: рассказ о wasm-bindgen

kguscp_kr5s-y3wesrilgm2kl3q.png


Мы уже видели насколько 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] и ключевое слово exter), которая принимает указатель и длину строки. Затем он конвертирует эту пару в &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.

© Habrahabr.ru