В моем последнем посте я представил WebAssembly - что это такое, почему нас это волнует и как это выглядит? В этом посте я хотел бы немного исследовать внутреннюю работу того, как мы можем общаться между Rust (wasm) и JavaScript. Как я упоминал в предыдущем посте, WebAssembly не является ни JavaScript, ни каким-то строго типизированным диалектом. Это автономный, скомпилированный переносимый двоичный файл. То, как вы отправляете данные и получаете данные из этого двоичного файла, связано с некоторыми тонкими нюансами работы WebAssembly.

Во-первых, давайте подведем итоги предыдущего поста. Мы написали функцию, которая выглядит так:

#[no_mangle]
pub extern fn add_one(a: u32) -> u32 {    
    a + 1
}

Мы смогли вызвать эту функцию из JavaScript с помощью:

wasm_module.instance.exports.add_one(2)

За кулисами произошло то, что все extern функции в нашем модуле Rust были добавлены в exports модуля WebAssembly, который мы создаем либо из буфера массива, либо, что предпочтительнее, из потока. Здесь нет никакой магии, просто WebAssembly.instantiate проделывает много работы.

Эта функция знает, как выглядит двоичный формат модуля wasm, и поэтому может просматривать его в поисках экспорта. В случае функции add_one мы получаем определение функции, которое на wasm выглядит так:

(func $add_one (export "add_one") (type $t0) (param $p0 i32) (result i32)

Это текстовое представление wasm (вы увидите, что это обозначается как wat в большинстве документации и примеров) и ясно показывает, что мы экспортируем функцию add_one.

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

Что меня раздражало, так это то, что большинство сэмплов «hello world» здесь останавливаются. Они не показывают вам, как пойти другим путем - как вызвать функцию JavaScript изнутри wasm / Rust? Это довольно просто, но требует немного больше работы.

Во-первых, давайте создадим функцию JavaScript, которую мы хотим вызвать:

function logit() {
   console.log('this was invoked by Rust/wasm, written in JS');
}

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

extern "C" {
    fn logit();
}

Теперь, когда у нас есть подпись для этой функции, мы можем вызывать ее изнутри нашей add_one функции:

#[no_mangle]
pub extern fn add_one(a: u32) -> u32 {
    logit();
    a + 1
}

Здесь все становится немного сложнее. Этот код не компилируется. Он не компилируется, потому что вызов функции extern по своей сути небезопасен. Любая функция, которая вызывает небезопасную функцию, должна быть помечена как небезопасная:

#[no_mangle]
pub unsafe extern fn add_one(a: u32) -> u32 {
    logit();
    a + 1
}

Теперь мы потенциально можем скомпилировать наш wasm модуль, запустить веб-сервер и запустить его. Однако мы увидели ошибку в консоли. Эта ошибка сводится к тому, что у нас есть внешние элементы, которые должны быть удовлетворены импортом, который не был передан в WebAssembly.instantiate. Другими словами, нам нужно обеспечить привязку из объявления Rust logit extern к конкретной logit реализации JavaScript. В то время как модуль экспорта был волшебным образом обработан компилятором, удовлетворение внешних запросов в WebAssembly - это ручной процесс.

Вот как мы можем вручную предоставить logit импорт в экземпляр нашего модуля WebAssembly (index.html):

<html>
   <head>
   <script>

   function logit() {
      console.log('this was invoked by Rust, written in JS');
   }

   let imports = {logit};

   fetch('wasm_project.gc.wasm')
     .then(r => r.arrayBuffer() )
     .then(r => WebAssembly.instantiate(r, { env: imports }))
     .then(wasm_module => {
        alert(`2 + 1 = ${wasm_module.instance.exports.add_one(2)}`);
     });
   </script>
   </head>
   <body></body>
</html>

Здесь мы передаем объект JavaScript в качестве второго параметра функции instantiate. Этот объект имеет поле env, которое содержит список импортируемых функций. Каждая функция, которую мы хотим вызывать из нашего кода Rust WebAssembly, должна быть явно включена в этот список импорта.

Теперь, когда мы запускаем это приложение (я только что запустил python3 -m http.server из корня моего проекта Cargo), я вижу сообщение журнала в консоли разработчика JavaScript:

Теперь мы наконец можем увидеть, как управление может передаваться в обоих направлениях - от JS к WebAssembly и от WebAssembly к JS. Мы были бы вправе начать восклицать, что эта новая вещь - утопия веб-разработки, но есть некоторые детали, которые нам нужно осветить, прежде чем мы будем слишком взволнованы.

Если вы посмотрите в Интернете некоторые из начальных руководств по WebAssembly, вы заметите, что большинство из них включает в себя эту функцию добавить один. Большинство из них недостаточно ясно дают понять, что это связано с тем, что вы можете иметь только числовые параметры функции в вызовах взаимодействия WebAssembly. Это напрямую связано со стековой природой wasm виртуальной машины и ее использованием линейной памяти. Если вы хотите передавать и возвращать строки, что мы считаем само собой разумеющимся почти во всех языках, вам придется преодолеть некоторые трудности. По понятным причинам мы не хотим помещать в стек строки произвольной длины. Это устранило бы все преимущества wasm в производительности и, вероятно, нанесло бы вред любому приложению даже с простейшими проектами.

Так что же нам делать? Мы используем концепцию WebAssembly под названием таблицы. Таблица, как следует из названия, содержит таблицу элементов определенного типа. Чтобы вставить строку в функцию в WebAssembly, мы могли бы добавить эту строку в таблицу, а затем вызвать функцию с индексом этой строки в таблице. На другой стороне вызова взаимодействия мы будем искать строку в таблице на основе предоставленного индекса. Мы также можем использовать этот шаблон для возврата недружелюбных к стеку значений.

Делать это для чего угодно, кроме простейших образцов «привет, мир», очень быстро станет утомительно.

Это момент на лекции профессора колледжа, когда он делает паузу, указывает на 37 полных досок непостижимых формул и говорит: «Но есть более простой способ сделать это ...»

Для Rust были написаны библиотеки, которые будут использовать генерацию кода и макросы для обработки низкоуровневой механики использования таблиц и других примитивов WebAssembly, чтобы взаимодействие выглядело более гладким. Тем не менее, я хотел убедиться, что я рассказал о том, что на самом деле происходит на самом низком уровне, чтобы когда я начал вести блог об этих библиотеках, вы знали, почему мы хотим их использовать, что проблемы, которые решают эти библиотеки, и концептуально как эти библиотеки решают эти проблемы.

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

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