diff options
Diffstat (limited to 'files/ru/webassembly/understanding_the_text_format/index.html')
-rw-r--r-- | files/ru/webassembly/understanding_the_text_format/index.html | 537 |
1 files changed, 537 insertions, 0 deletions
diff --git a/files/ru/webassembly/understanding_the_text_format/index.html b/files/ru/webassembly/understanding_the_text_format/index.html new file mode 100644 index 0000000000..9acdd0af80 --- /dev/null +++ b/files/ru/webassembly/understanding_the_text_format/index.html @@ -0,0 +1,537 @@ +--- +title: Описание текстового формата WebAssembly +slug: WebAssembly/Understanding_the_text_format +translation_of: WebAssembly/Understanding_the_text_format +--- +<div>{{WebAssemblySidebar}}</div> + +<p class="summary">Чтобы люди могли читать и редактировать код WebAssembly, существует текстовое представление двоичного формата wasm. Это промежуточная форма, предназначенная для отображения в текстовых редакторах, средствах разработки браузеров и т. д. В этой статье объясняется, как работает этот текстовый формат с точки зрения синтаксиса, как он связан с байт-кодом, который он представляет и оболочками объектов wasm в JavaScript.</p> + +<div class="note"> +<p><strong>Примечание</strong>: Ознакомление с данной статьей может оказаться излишним, если вы веб-разработчик, который просто хочет загрузить модуль wasm на страницу и использовать его в своем коде (см. <a href="/ru/docs/WebAssembly/Using_the_JavaScript_API">Использование WebAssembly JavaScript API</a>). Эта статья будет наиболее полезной, если вы хотите написать несколько модулей wasm для оптимизации производительности вашей библиотеки JavaScript или создать свой собственный компилятор WebAssembly.</p> +</div> + +<h2 id="S-выражения">S-выражения</h2> + +<p>Как в двоичном, так и в текстовом форматах основным блоком кода в WebAssembly является модуль. В текстовом формате модуль представлен как одно большое S-выражение. S-выражения - это очень старый и очень простой текстовый формат для представления деревьев. И поэтому мы можем думать о модуле как о дереве узлов, которые описывают структуру модуля и его код. В отличие от абстрактного синтаксического дерева в языке программирования, дерево WebAssembly довольно плоское и состоит в основном из списков инструкций.</p> + +<p>Во-первых, давайте посмотрим, как выглядит S-выражение. Каждый узел дерева входит в пару круглых скобок - <code>( ... )</code>. Первая метка в скобках сообщает вам, какой это тип узла, за ним следует разделенный пробелами список атрибутов или дочерних узлов. Давайте рассмотрим, что означает следующее S-выражение WebAssembly:</p> + +<pre>(module (memory 1) (func))</pre> + +<p>Это выражение представляет дерево с корневым узлом «module» и двумя дочерними узлами - узлом «memory» с атрибутом «1» и узлом «func». Мы вскоре увидим, что на самом деле означают эти узлы.</p> + +<h3 id="Самый_простой_модуль">Самый простой модуль</h3> + +<p>Давайте начнем с самого простого модуля wasm.</p> + +<pre>(module)</pre> + +<p>Этот модуль полностью пуст, но является допустимым.</p> + +<p>Если мы сейчас преобразуем наш модуль в двоичный формат (см. <a href="/en-US/docs/WebAssembly/Text_format_to_wasm">Перевод текстового формата WebAssembly в wasm</a>), мы увидим только 8-байтовый заголовок модуля, описанный в <a href="http://webassembly.org/docs/binary-encoding/#high-level-structure">двоичном формате</a>:</p> + +<pre>0000000: 0061 736d ; WASM_BINARY_MAGIC +0000004: 0100 0000 ; WASM_BINARY_VERSION</pre> + +<h3 id="Добавление_функциональности_в_ваш_модуль">Добавление функциональности в ваш модуль</h3> + +<p>Хорошо, это не очень интересно, давайте добавим немного исполняемого кода в этот модуль.</p> + +<p>Весь код в модуле сгруппирован в функции, которые имеют следующую структуру псевдокода:</p> + +<pre>( func <signature> <locals> <body> )</pre> + +<ul> + <li><strong>signature</strong> объявляет, что функция принимает (параметры) и возвращает (возвращаемые значения).</li> + <li><strong>locals</strong> похожи на переменные в JavaScript, но с определенными явными типами.</li> + <li><strong>body</strong> - это просто линейный список низкоуровневых инструкций.</li> +</ul> + +<p>Несмотря на то, что это S-выражение, оно очень напоминает функцию в других языках.</p> + +<h2 id="Сигнатуры_и_параметры">Сигнатуры и параметры</h2> + +<p>Сигнатура - это последовательность объявлений типов параметров, за которыми следует список объявлений возвращаемых типов. Здесь стоит отметить, что:</p> + +<ul> + <li> + <p class="syntaxbox"> Отсутствие возвращаемого типа <code>(result)</code> означает, что функция ничего не возвращает. </p> + </li> + <li> + <p class="syntaxbox">В текущей версии WebAssembly может быть не более 1 возвращаемого типа, но <a href="https://webassembly.org/docs/future-features#multiple-return">позже это значение будет изменено</a> на любое число.</p> + </li> +</ul> + +<p> Каждый параметр имеет явно объявленный тип; у wasm в настоящее время есть четыре доступных типа:</p> + +<ul> + <li> <code>i32</code>: 32-разрядное целое число</li> + <li> <code>i64</code>: 64-разрядное целое число</li> + <li> <code>f32</code>: 32-разрядное число с плавающей точкой</li> + <li> <code>f64</code>: 64-разрядное число с плавающей точкой</li> +</ul> + +<p>Один параметр можно записать как <code>(param i32)</code>, а тип возвращаемого значения как <code>(result i32)</code>. Двоичная функция, которая принимает два 32-разрядных целых числа и возвращает 64-разрядное число с плавающей запятой, будет записана следующим образом:</p> + +<pre>(func (param i32) (param i32) (result f64) ... )</pre> + +<p>После сигнатуры перечисляются локальные переменные с указанием типа, например <code>(local i32)</code>. Параметры в сигнатуре приравниваются к локальным переменным, которые инициализируются значением соответствующего аргумента, переданного вызывающей стороной.</p> + +<h2 id="Получение_и_установка_локальных_переменных_и_параметров_функции">Получение и установка локальных переменных и параметров функции</h2> + +<p>И параметры и локальные переменные могут быть прочитаны и записаны в теле функции с помощью инструкций <code>get_local</code> и <code>set_local</code>.</p> + +<p>Инструкции <code>get_local</code> и <code>set_local</code> ссылаются по индексу на параметр, который должен быть получен или установлен: сначала считаются параметры, а затем локальные переменные в порядке их объявления. Объясним это на примере следующей функции:</p> + +<pre>(func (param i32) (param f32) (local f64) + get_local 0 + get_local 1 + get_local 2)</pre> + +<p>Инструкция <code>get_local 0</code> получит параметр i32, <code>get_local 1</code> получит параметр f32, а get_local 2 получит локальную переменную local f64.</p> + +<p>Использование числовых индексов для ссылки на элементы может сбивать с толку и раздражать, поэтому текстовый формат позволяет присваивать имена параметрам, локальным переменным и большинству других элементов. Для этого нужно просто добавить имя с префиксом символа доллара (<code>$</code>) непосредственно перед объявлением типа.</p> + +<p>Таким образом, можно переписать нашу сигнатуру так:</p> + +<pre>(func (param $p1 i32) (param $p2 f32) (local $loc f64) …)</pre> + +<p>После чего можно было бы написать инструкцию получения <code>get_local $p1</code> вместо <code>get_local 0</code> и т.д. (Обратите внимание, что, когда этот текст преобразуется в двоичный файл, двоичный файл будет содержать только индексы.)</p> + +<h2 id="Стековые_машины">Стековые машины</h2> + +<p>Прежде чем мы сможем написать тело функции, мы должны поговорить еще о <strong>стековых машинах</strong>. Хотя браузер компилирует wasm-код во что-то более эффективное, выполнение его определяется в терминах стековой машины, где основная идея заключается в том, что каждый тип инструкции получает или помещает определенное количество значений <code>i32</code> / <code>i64</code> / <code>f32</code> / <code>f64</code> в стек или из стека.</p> + +<p>Например, инструкция <code>get_local</code> предназначена для помещения значения локальной переменной, которое она считала, в стек. А инструкция <code>i32.add</code> получает два значения <code>i32</code> (неявно получает два предыдущих значения, помещенных в стек), вычисляет их сумму и помещает назад в стек результат вычисления <code>i32</code>.</p> + +<p>Когда вызывается функция, для нее выделяется пустой стек, который постепенно заполняется и очищается при выполнении инструкций в теле функции. Так, например, после выполнения следующей функции:</p> + +<pre>(func (param $p i32) + get_local $p + get_local $p + i32.add)</pre> + +<p>Стек будет содержать ровно одно значение <code>i32</code> - результат выполнения выражения ($p + $p), которое обработалось инструкцией <code>i32.add</code>. Возвращаемое значение функции - это последнее значение, оставленное в стеке.</p> + +<p>Правила валидации WebAssembly гарантируют, выполнение следующего: если вы объявляете тип возвращаемого значения функции как <code>(result f32)</code>, то стек должен содержать ровно одно значение типа <code>f32</code> в конце. Если тип результата отсутствует, стек должен быть пустым.</p> + +<h2 id="Тело_функции">Тело функции</h2> + +<p>Как упоминалось ранее, тело функции - это просто список инструкций, которые выполняются при вызове функции. Объединяя это с тем, что мы уже изучили, мы можем наконец определить модуль, содержащий простую функцию:</p> + +<pre>(module + (func (param $lhs i32) (param $rhs i32) (result i32) + get_local $lhs + get_local $rhs + i32.add))</pre> + +<p>Эта функция получает два параметра, складывает их вместе и возвращает результат.</p> + +<p>Есть еще много инструкций, которые можно поместить в тело функции. Сейчас мы начнем с простых, а далее вы увидите гораздо больше примеров по мере продвижения. Полный список доступных инструкций смотрите в справочнике по <a href="http://webassembly.org/docs/semantics/">семантике webassembly.org</a>.</p> + +<h3 id="Вызов_функции">Вызов функции</h3> + +<p>Определение нашей функции само по себе почти ничего не делает - теперь нам нужно её вызвать. Как мы это сделаем? Как и в модуле ES2015, функции wasm должны быть явно экспортированы инструкцией <code>export</code> внутри модуля.</p> + +<p>Как и локальные переменные, функции идентифицируются индексом по умолчанию, но для удобства им можно присвоить имя. Давайте это сделаем: сначала добавим имя, которому предшествует знак доллара, сразу после ключевого слова <code>func</code>:</p> + +<pre>(func $add … )</pre> + +<p>Теперь нам нужно добавить объявление экспорта:</p> + +<pre>(export "add" (func $add))</pre> + +<p>Здесь <code>add</code> - это имя, по которому функция будет идентифицироваться в коде JavaScript, а <code>$add</code> определяет, какая функция внутри модуля WebAssembly будет экспортироваться.</p> + +<p>Итак, наш последний вариант модуля (на данный момент) выглядит так:</p> + +<pre>(module + (func $add (param $lhs i32) (param $rhs i32) (result i32) + get_local $lhs + get_local $rhs + i32.add) + (export "add" (func $add)) +)</pre> + +<p>Если вы хотите собственноручно скомпилировать пример, сохраните ранее написанный модуль в файле с именем <code>add.wat</code>, а затем преобразуйте его в двоичный файл с именем <code>add.wasm</code>, используя wabt (подробности смотрите в разделе <a href="/en-US/docs/WebAssembly/Text_format_to_wasm">Перевод текстового формата WebAssembly в wasm</a>).</p> + +<p>Затем мы загрузим наш двоичный файл, скомпилируем, создадим его экземпляр и выполним нашу функцию <code>add</code> в коде JavaScript (теперь нам доступна функция <code>add()</code> в свойстве <code><a href="/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Instance/exports">exports</a></code> экземпляра модуля):</p> + +<pre class="brush: js">WebAssembly.instantiateStreaming(fetch('add.wasm')) +.then(obj => { + console.log(obj.instance.exports.add(1, 2)); // "3" +});</pre> + +<div class="note"> +<p><strong>Примечание</strong>: Вы можете найти этот пример на GitHub в файле <a href="https://github.com/mdn/webassembly-examples/blob/master/understanding-text-format/add.html">add.html</a> (смотрите также это <a href="https://mdn.github.io/webassembly-examples/understanding-text-format/add.html">вживую</a>). Также смотрите {{jsxref("WebAssembly.instantiateStreaming()")}} для получения более подробной информации о функции создания экземпляра модуля.</p> +</div> + +<h2 id="Изучение_основ">Изучение основ</h2> + +<p>Теперь, когда мы рассмотрели простейшие примеры, давайте перейдем к рассмотрению некоторых более сложных возможностей.</p> + +<h3 id="Вызов_функций_из_других_функций_в_том_же_модуле">Вызов функций из других функций в том же модуле</h3> + +<p>Для вызова функци по индексу или имени используется инструкция <code>call</code>. Например, следующий модуль содержит две функции - первая просто возвращает значение <code>42</code>, вторая возвращает сумму результата вызова первой функции и единицы:</p> + +<pre>(module + (func $getAnswer (result i32) + i32.const 42) + (func (export "getAnswerPlus1") (result i32) + call $getAnswer + i32.const 1 + i32.add))</pre> + +<div class="note"> +<p><strong>Примечание</strong>: Инструкция <code>i32.const</code> создает 32-разрядное целое число и помещает его в стек. Вы можете поменять <code>i32</code> на любой другой доступный тип данных и изменить значение на любое другое (здесь мы установили значение <code>42</code>).</p> +</div> + +<p>В этом примере обратите внимание на секцию объявления экспорта <code>(export “getAnswerPlus1”)</code>, которая находится сразу после объявления второй функции <code>func</code>. Это сокращенный способ объявления, совмещенный с именем функции, которую мы хотим экспортировать.</p> + +<p>Функционально это эквивалентно включению отдельного объявления экспорта функции без функции, в любом месте модуля, например:</p> + +<pre>(export "getAnswerPlus1" (func $functionName))</pre> + +<p>Код JavaScript для вызова экспортируемой функции из нашего модуля выглядит так:</p> + +<pre class="brush: js">WebAssembly.instantiateStreaming(fetch('call.wasm')) +.then(obj => { + console.log(obj.instance.exports.getAnswerPlus1()); // "43" +});</pre> + +<div class="note"> +<p><strong>Примечание</strong>: Вы можете найти этот пример на GitHub как <a href="https://github.com/mdn/webassembly-examples/blob/master/understanding-text-format/call.html">call.html</a> (смотрите также <a href="https://mdn.github.io/webassembly-examples/understanding-text-format/call.html">вживую</a>). Еще посмотрите <a href="https://github.com/mdn/webassembly-examples/blob/master/wasm-utils.js">wasm-utils.js</a> для метода <code>fetchAndInstantiate()</code>.</p> +</div> + +<h3 id="Импорт_функций_из_JavaScript">Импорт функций из JavaScript</h3> + +<p>Мы уже видели JavaScript, вызывающий экспортируемые функции модуля WebAssembly, но как насчет WebAssembly модуля, вызывающего функции JavaScript? WebAssembly не имеет каких либо знаний о внешнем коде JavaScript, но у него есть способ импорта, который может принимать функции из JavaScript или wasm. Давайте посмотрим на пример:</p> + +<pre>(module + (import "console" "log" (func $log (param i32))) + (func (export "logIt") + i32.const 13 + call $log))</pre> + +<p>В инструкции импорта в модуль WebAssembly определено двухуровневое пространство имен, в котором мы указали импортировать функцию <code>log</code> из модуля <code>console</code>. Вы также можете видеть, что экспортируемая функция <code>logIt</code> вызывает импортированную функцию, используя инструкцию <code>call</code>, о которой мы говорили ранее.</p> + +<p>Импортируемые функции аналогичны обычным функциям: они имеют сигнатуру, которую WebAssembly проверяет статически, им присваивается индекс (в место которого можно присвоить имя) и их можно вызвать обычным способом.</p> + +<p>Функции JavaScript не имеют понятия сигнатуры, поэтому любую функцию JavaScript можно передать независимо от объявленной сигнатуры импорта. Если модуль объявляет импорт, вызывающая сторона (например метод {{jsxref("WebAssembly.instantiate()")}}) должна передать объект импорта, который должен иметь соответствующее свойство.</p> + +<p>Для иллюстрации вышесказанного нам нужен объект (назовем его <code>importObject</code>), в котором конечное свойство <code>importObject.console.log</code> должно содержать функцию JavaScript.</p> + +<p>Код будет выглядеть следующим образом:</p> + +<pre class="brush: js">var importObject = { + console: { + log: function(arg) { + console.log(arg); + } + } +}; + +WebAssembly.instantiateStreaming(fetch('logger.wasm'), importObject) +.then(obj => { + obj.instance.exports.logIt(); +});</pre> + +<div class="note"> +<p><strong>Примечание</strong>: Этот пример можно найти на GitHub в файле <a href="https://github.com/mdn/webassembly-examples/blob/master/understanding-text-format/logger.html">logger.html</a> (смотрите также <a href="https://mdn.github.io/webassembly-examples/understanding-text-format/logger.html">вживую</a>).</p> +</div> + +<h3 id="Определение_глобальных_переменных_WebAssembly">Определение глобальных переменных WebAssembly</h3> + +<p>WebAssembly имеет возможность создавать экземпляры глобальных переменных. Они доступны как в коде JavaScript, так и через импорт / экспорт для одиного и более экземпляров {{jsxref("WebAssembly.Module")}}. Это очень полезная возможность в плане динамического связывания нескольких модулей.</p> + +<p>В текстовом формате WebAssembly это выглядит примерно так (смотрите файл <a href="https://mdn.github.io/webassembly-examples/js-api-examples/global.html">global.html</a> в нашем репозитории на GitHub; смотрите также <a href="https://mdn.github.io/webassembly-examples/js-api-examples/global.html">вживую</a>):</p> + +<pre>(module + (global $g (import "js" "global") (mut i32)) + (func (export "getGlobal") (result i32) + (get_global $g)) + (func (export "incGlobal") + (set_global $g + (i32.add (get_global $g) (i32.const 1)))) +)</pre> + +<p>Это похоже на то, что мы делали раньше, за исключением того, что мы указываем глобальную переменную с помощью ключевого слова <code>global</code>. Также мы указываем ключевое слово <code>mut</code> вместе с типом данных значения (если хотим, чтобы глобальная переменная была изменяемой).</p> + +<p>Чтобы создать эквивалентный код с помощью JavaScript, вы должны использовать конструктор {{jsxref("WebAssembly.Global()")}}:</p> + +<pre class="brush: js">const global = new WebAssembly.Global({value:'i32', mutable:true}, 0);</pre> + +<h3 id="Память_WebAssembly">Память WebAssembly</h3> + +<p>Приведенный выше пример - довольно ужасная функция ведения журнала: она печатает только одно целое число! Что если мы хотим записать текстовую строку? Для работы со строками и другими более сложными типами данных WebAssembly предоставляет <strong>линейную память</strong>. Согласно технологии WebAssembly, линейная память - это просто большой массив байтов, который со временем может увеличиваться. WebAssembly код содержит ряд инструкций, наподобие <code>i32.load</code> и <code>i32.store</code> для чтения и записи значений из <a href="http://webassembly.org/docs/semantics/#linear-memory">линейной памяти</a>.</p> + +<p>Со стороны JavaScript, линейная память как будто находится внутри одного большого (расширяющегося) объекта {{domxref("ArrayBuffer")}}.</p> + +<p>Таким образом, строка - это просто последовательность байтов где-то внутри этой линейной памяти. Давайте предположим, что мы записали нужную строку байтов в память; как мы передадим эту строку в JavaScript?<br> + Ключевым моментом является то, что JavaScript может создавать экземпляры(объекты) линейной памяти WebAssembly через конструктор {{jsxref("WebAssembly.Memory()")}} и получать доступ к существующему экземпляру памяти (в настоящее время вы можете иметь только один экземпляр памяти на экземпляр модуля), используя соответствующие методы экземпляра модуля. Экземпляр памяти имеет свойство <code><a href="/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Memory/buffer">buffer</a></code>, которое возвращает объект <code>ArrayBuffer</code>, предоставляя всю линейную память модуля.</p> + +<p>Объекты памяти могут расширятся с помощью метода <code><a href="/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Memory/grow">Memory.grow()</a></code> из JavaScript. Когда происходит расширение, текущий объект <code>ArrayBuffer</code> не может изменить размер и он отсоединяется. Вместо него создается новый объект <code>ArrayBuffer</code>, указывающий на новую, увеличенную память. Пользуясь этими возможностями можно передать строку в JavaScript, её начальный индекс и её длину в линейной памяти.</p> + +<p>Хотя есть много разных способов кодировать длину строки в самой строке (например, как в строках в C); для простоты здесь мы просто передаем смещение и длину в качестве параметров:</p> + +<pre>(import "console" "log" (func $log (param i32) (param i32)))</pre> + +<p>На стороне JavaScript, мы можем использовать <a href="/en-US/docs/Web/API/TextDecoder">TextDecoder API</a>, чтобы легко декодировать наши байты в строку JavaScript. (Мы указываем кодировку utf8, хотя поддерживаются и другие кодировки.)</p> + +<pre class="brush: js">function consoleLogString(offset, length) { + var bytes = new Uint8Array(memory.buffer, offset, length); + var string = new TextDecoder('utf8').decode(bytes); + console.log(string); +}</pre> + +<p>Последний недостающий фрагмент головоломки - это место, где функция <code>consoleLogString</code> получает доступ к памяти (<code>memory</code>) WebAssembly. WebAssembly дает нам здесь много гибкости: либо мы можем создать объект <code><a href="/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Memory">Memory</a></code> в коде JavaScript и импортировать его в модуль WebAssembly, или мы можем создать его в модуле WebAssembly и затем экспортировать в JavaScript.</p> + +<p>Для простоты, давайте создадим объект памяти в JavaScript и импортируем его в WebAssembly модуль. Напишем следующее объявление импорта <code>(import</code>):</p> + +<pre>(import "js" "mem" (memory 1))</pre> + +<p>Число <code>1</code> указывает, что импортируемая память должна иметь по крайней мере 1 страницу памяти (WebAssembly определяет страницу как фиксированный блок памяти в 64КБ.)</p> + +<p>Давайте взглянем на наш последний вариант модуля, который выводит слово “Hi”. В обычной C программе, мы бы вызывали функцию для выделения памяти для строки. Но так как мы пишем собственную сборку и у нас есть собственная импортируемая память, то мы просто пишем содержание строки в линейную память, используя секцию <code>data</code>. Data-секция во время создания записывает строку байт, начиная с указанного отступа. И она действует также как и <code>.data</code> секция в “родных” форматах для исполнения.</p> + +<p>Наш последний вариант модуля выглядит так:</p> + +<pre>(module + (import "console" "log" (func $log (param i32 i32))) + (import "js" "mem" (memory 1)) + (data (i32.const 0) "Hi") + (func (export "writeHi") + i32.const 0 ;; pass offset 0 to log + i32.const 2 ;; pass length 2 to log + call $log))</pre> + +<div class="note"> +<p><strong>Примечание</strong>: Обратите внимание, что двойная точка с запятой (<code>;;</code>) позволяет оставлять комментарии в файлах WebAssembly.</p> +</div> + +<p>Теперь из JavaScript мы можем создать и передать объект памяти размером в 1 страницу. Результатом работы этого кода будет вывод “Hi” в консоль:</p> + +<pre class="brush: js">var memory = new WebAssembly.Memory({initial:1}); + +var importObject = { console: { log: consoleLogString }, js: { mem: memory } }; + +WebAssembly.instantiateStreaming(fetch('logger2.wasm'), importObject) +.then(obj => { + obj.instance.exports.writeHi(); +});</pre> + +<div class="note"> +<p><strong>Примечание</strong>: Вы можете найти полный исходный код на GitHub в файле <a href="https://github.com/mdn/webassembly-examples/blob/master/understanding-text-format/logger2.html">logger2.html</a> (также смотрите это <a href="https://mdn.github.io/webassembly-examples/understanding-text-format/logger2.html">вживую</a>).</p> +</div> + +<h3 id="Таблицы_WebAssembly">Таблицы WebAssembly</h3> + +<p>Чтобы завершить обзор текстового формата WebAssembly, давайте рассмотрим самую сложную и запутанную часть WebAssembly - <strong>таблицы</strong>. Таблицы - это массивы ссылок изменяемого размера, доступ к которым можно получить по индексу из кода WebAssembly.</p> + +<p>Чтобы понять, зачем нужны таблицы, нам нужно сначала обратить внимание, что инструкция <code>call</code>, которую мы видели ранее (см. {{anch("Вызов функций из других функций в том же модуле")}}), принимает статический индекс функции и может вызывать только определенную функцию. Но что, если вызываемый элемент будет значением, установленным во время выполнения?</p> + +<ul> + <li> В JavaScript это делается постоянно: функции являются ссылочными значениями.</li> + <li> В C/C++ это делается с помощью указателей на функции.</li> + <li> В C++ это делается с помощью виртуальных функций.</li> +</ul> + +<p>Для того чтобы сделать это в WebAssembly нужен был отдельный тип инструкции вызова. Поэтому мы создали инструкцию <code>call_indirect</code>, которая принимает операнд динамической функции. Проблема в том, что типы данных, которые мы должны использовать в операндах в WebAssembly, в настоящее время такие: <code>i32</code> / <code>i64</code> / <code>f32</code> / <code>f64</code>.</p> + +<p>Для WebAssembly можно было бы создать тип инструкции вызова <code>anyfunc</code> («любой», потому что эта инструкция смогла вызвать функции любой сигнатуры), но, к сожалению, операнд этого типа не может быть сохранен в линейной памяти по соображениям безопасности. Линейная память представляет содержимое хранимых значений в виде незащищенных байтов, и это позволяет содержимому wasm произвольно читать и изменять незащищенные адреса функций, что недопустимо для веб.</p> + +<p>Решением стало следующее. Хранить ссылки на функции в таблице и передавать вместо них индексы таблицы, которые являются просто значениями <code>i32</code>. Поэтому операндом инструкции <code>call_indirect</code> может выступить простое значение индекса <code>i32</code>.</p> + +<h4 id="Определение_таблицы_в_wasm">Определение таблицы в wasm</h4> + +<p>Так как же разместить функции wasm в нашей таблице? Подобно тому, как секции <code>data</code> могут использоваться для инициализации областей линейной памяти байтами, секции <code>elem</code> могут использоваться для инициализации областей таблиц с функциями:</p> + +<pre>(module + (table 2 anyfunc) + (elem (i32.const 0) $f1 $f2) + (func $f1 (result i32) + i32.const 42) + (func $f2 (result i32) + i32.const 13) + ... +)</pre> + +<ul> + <li> + <p>В <code>(table 2 anyfunc)</code>, 2 - это начальный размер таблицы (это означает, что она будет хранить две ссылки), а объявление <code>anyfunc</code> означает, что типом элемента этих ссылок является «функция с любой сигнатурой». В текущей версии WebAssembly, это единственный допустимый тип атрибута, но в будущем будет добавлено больше.</p> + </li> + <li> + <p>Секции функций <code>(func) </code>- это обычные объявления функций модуля wasm. Это те функции, на которые мы будем ссылаться в нашей таблице (каждая из них просто возвращает постоянное значение). Обратите внимание, что порядок объявления секций не имеет значения - вы можете объявить свои функции где угодно и по-прежнему ссылаться на них в секции <code>elem</code>.</p> + </li> + <li> + <p>Секция <code>elem</code> - это список функций, на которые ссылается таблица, в том порядке, в котором они указаны. Здесь можно перечислить любое количество функций, включая их дубликаты.</p> + </li> + <li> + <p>Значение <code>(i32.const 0)</code> внутри секции <code>elem</code> является смещением - его необходимо объявить в начале секции и указать, по какому индексу в таблице ссылок начинают заполняться ссылки на функции. Здесь мы указали 0, а размер таблицы указали как 2 (см. выше), поэтому мы можем заполнить две ссылки на индексы 0 и 1. Если бы мы захотели записать наши ссылки со смещением в 1, то нам нужно было бы написать <code>(i32.const 1)</code>, а размер таблицы должен был быть равен 3.</p> + </li> +</ul> + +<div class="note"> +<p><strong>Примечание</strong>: Неинициализированным элементам присваивается значение вызова по умолчанию.</p> +</div> + +<p>В JavaScript эквивалентный код для создания такого экземпляра таблицы ссылок будет выглядеть примерно так:</p> + +<pre class="brush: js">function() { + // table section + var tbl = new WebAssembly.Table({initial:2, element:"anyfunc"}); + + // function sections: + var f1 = function() { … } + var f2 = function() { … } + + // elem section + tbl.set(0, f1); + tbl.set(1, f2); +};</pre> + +<h4 id="Использование_таблицы">Использование таблицы</h4> + +<p>Мы определили таблицу, которую нам нужно как-то использовать. Для этого добавим следующую секцию кода:</p> + +<pre>(type $return_i32 (func (result i32))) ;; if this was f32, type checking would fail +(func (export "callByIndex") (param $i i32) (result i32) + get_local $i + call_indirect (type $return_i32))</pre> + +<ul> + <li>Секция <code>(type $return_i32 (func (result i32)))</code> определяет тип с заданным именем <code>$return_i32</code>. Этот тип используется при выполнении проверки сигнатуры функции в таблице функций. Здесь мы указываем, что ссылки должны быть функциями, возвращаемое значение которых должно быть с типом <code>i32</code>.</li> + <li>Далее мы определяем экспортируемую функцию с именем <code>callByIndex</code>. Для единственного параметра функции задан тип <code>i32</code>, которому присвоено имя <code>$i</code>.</li> + <li>Внутри функции мы помещаем одно значение в стек - любое значение, переданное в качестве параметра <code>$i</code> экспортируемой функции.</li> + <li>Наконец, мы используем инструкцию <code>call_indirect</code> для вызова функции из таблицы - она неявно получает значение <code>$i</code> из стека. Конечным результатом будет вызов функции из таблицы с индексом, указанным в <code>$i</code>.</li> +</ul> + +<p>Вы также можете объявить параметр <code>call_indirect</code> явно во время вызова инструкции, а не до него (неявным получением из стека), например так:</p> + +<pre>(call_indirect (type $return_i32) (get_local $i))</pre> + +<p>На языке высокого уровня, таком как JavaScript эти же действия вы можете представить в виде манипуляций с массивом (или, скорее, с объектом), содержащим функции. Псевдокод будет выглядеть примерно так: <code>tbl[i]()</code>.</p> + +<p>Итак, вернемся к проверке типов. Так как в коде WebAssembly проверяются типы, а атрибут <code>anyfunc</code> означает “сигнатура любой функции", мы должны предоставить предполагаемую сигнатуру в месте вызова, поэтому мы включаем тип с именем <code>$return_i32</code>, чтобы сообщить программе, что ожидается функция, возвращающая значение с типом <code>i32</code>. Если вызываемая функция не имеет соответствующей сигнатуры (скажем, вместо нее возвращается <code>f32</code>), выбросится исключение {{jsxref("WebAssembly.RuntimeError")}}.</p> + +<p>Так как инструкция <code>call_indirect</code> связывается с таблицей, с которой мы вызываем функцию? Ответ заключается в том, что на данный момент для каждого экземпляра модуля разрешена только одна таблица. Поэтому инструкция <code>call_indirect</code> выполняет неявный вызов именно из этой таблицы. В будущем, когда будет разрешено использование нескольких таблиц, нам нужно будет указать идентификатор таблицы, например так:</p> + +<pre>call_indirect $my_spicy_table (type $i32_to_void)</pre> + +<p>Весь модуль в целом выглядит следующим образом и может быть найден в нашем примере файла <a href="https://github.com/mdn/webassembly-examples/blob/master/understanding-text-format/wasm-table.wat">wasm-table.wat</a>:</p> + +<pre>(module + (table 2 anyfunc) + (func $f1 (result i32) + i32.const 42) + (func $f2 (result i32) + i32.const 13) + (elem (i32.const 0) $f1 $f2) + (type $return_i32 (func (result i32))) + (func (export "callByIndex") (param $i i32) (result i32) + get_local $i + call_indirect (type $return_i32)) +)</pre> + +<p>Загрузка модуля и использование экспортируемой функции в коде JavaScript будет выглядеть так:</p> + +<pre class="brush: js">WebAssembly.instantiateStreaming(fetch('wasm-table.wasm')) +.then(obj => { + console.log(obj.instance.exports.callByIndex(0)); // returns 42 + console.log(obj.instance.exports.callByIndex(1)); // returns 13 + console.log(obj.instance.exports.callByIndex(2)); // returns an error, because there is no index position 2 in the table +});</pre> + +<div class="note"> +<p><strong>Примечание</strong>: Этот пример можно найти на GitHub в файле <a href="https://github.com/mdn/webassembly-examples/blob/master/understanding-text-format/wasm-table.html">wasm-table.html</a> (смотрите это также <a href="https://mdn.github.io/webassembly-examples/understanding-text-format/wasm-table.html">вживую</a>)</p> +</div> + +<div class="note"> +<p><strong>Примечание</strong>: Как и в случае с памятью, таблицы также можно создавать из кода JavaScript (см. <code><a href="/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Table">WebAssembly.Table()</a></code>).</p> +</div> + +<h3 id="Изменяющиеся_таблицы_и_динамическое_связывание">Изменяющиеся таблицы и динамическое связывание</h3> + +<p>Поскольку JavaScript имеет полный доступ к ссылкам на функции, объект таблицы может быть изменен из кода JavaScript с помощью методов <code><a href="/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Table/grow">grow()</a></code>, <code><a href="/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Table/get">get()</a></code> и <code><a href="/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Table/set">set()</a></code>. Когда WebAssembly получит <a href="http://webassembly.org/docs/gc/">ссылочные типы</a>, код WebAssembly сможет изменять таблицы самостоятельно с помощью инструкций <code>get_elem</code> / <code>set_elem</code>.</p> + +<p>Поскольку таблицы являются изменяемыми, их можно использовать для реализации сложных схем <a href="http://webassembly.org/docs/dynamic-linking">динамического связывания</a> во время загрузки и во время выполнения. Когда программа динамически связана, несколько экземпляров могут совместно использовать линейную память и таблицу ссылок. Это похоже на поведение в обычном приложении где несколько скомпилированных <code>.dll</code> совместно используют адресное пространство одного процесса.</p> + +<p>Чтобы увидеть это в действии, мы создадим один объект импорта, содержащий объект памяти и объект таблицы. Далее мы передадим этот объект импорта при создании нескольких модулей с помощью метода <code><a href="/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/instantiate">instantiate()</a></code>.</p> + +<p>Наши примеры файлов <code>.wat</code> выглядят так:</p> + +<p><code>shared0.wat</code>:</p> + +<pre>(module + (import "js" "memory" (memory 1)) + (import "js" "table" (table 1 anyfunc)) + (elem (i32.const 0) $shared0func) + (func $shared0func (result i32) + i32.const 0 + i32.load) +)</pre> + +<p><code>shared1.wat</code>:</p> + +<pre>(module + (import "js" "memory" (memory 1)) + (import "js" "table" (table 1 anyfunc)) + (type $void_to_i32 (func (result i32))) + (func (export "doIt") (result i32) + i32.const 0 + i32.const 42 + i32.store ;; store 42 at address 0 + i32.const 0 + call_indirect (type $void_to_i32)) +)</pre> + +<p>Они работают следующим образом:</p> + +<ol> + <li>Функция <code>shared0func</code> определена в <code>shared0.wat</code> и сохраняется в нашей импортированной таблице.</li> + <li>Эта функция создает константу, содержащую значение <code>0</code>, затем инструкция <code>i32.load</code> получает значение из импортированной памяти по предоставленному константой индексу. Предоставленный индекс равен <code>0</code>. Как и другие подобные инструкции, <code>i32.load</code> неявно получает предоставленное значение из стека. Итак, <code>shared0func</code> загружает и возвращает значение, хранящееся в индексе памяти <code>0</code>.</li> + <li>В <code>shared1.wat</code> мы экспортируем функцию с именем <code>doIt</code> - эта функция размещает в стеке две константы, содержащие значения <code>0</code> и <code>42</code>. Затем она вызывает инструкцию <code>i32.store</code> для сохранения предоставленного значения по предоставленному индексу в импортированной памяти. Опять же, инструкция неявно получает эти значения из стека. Поэтому в результате <code>doIt</code> сохраняет значение <code>42</code> в индексе памяти <code>0</code>.</li> + <li>В последней части функции создается константа со значением <code>0</code>, затем вызывается функция с этим индексом (<code>0</code>) из таблицы. Это будет функция <code>shared0func</code> модуля <code>shared0.wat</code>, которая ранее была размещена там с помощью секции <code>elem</code>.</li> + <li>При вызове shared0func загружает число <code>42</code>, которые мы сохранили в памяти, с помощью ранее указанной инструкции <code>i32.store</code> в модуле <code>shared1.wat</code>.</li> +</ol> + +<div class="note"> +<p><strong>Примечание</strong>: Вышеприведенные выражения неявно извлекают значения из стека, но вместо этого вы можете объявить их явно в вызовах инструкций, например:</p> + +<pre>(i32.store (i32.const 0) (i32.const 42)) +(call_indirect (type $void_to_i32) (i32.const 0))</pre> +</div> + +<p>После преобразования текста в модули мы используем файлы <code>shared0.wasm</code> и <code>shared1.wasm</code> в JavaScript с помощью следующего кода:</p> + +<pre class="brush: js">var importObj = { + js: { + memory : new WebAssembly.Memory({ initial: 1 }), + table : new WebAssembly.Table({ initial: 1, element: "anyfunc" }) + } +}; + +Promise.all([ + WebAssembly.instantiateStreaming(fetch('shared0.wasm'), importObj), + WebAssembly.instantiateStreaming(fetch('shared1.wasm'), importObj) +]).then(function(results) { + console.log(results[1].instance.exports.doIt()); // prints 42 +});</pre> + +<p>Каждый из компилируемых модулей может импортировать общие объекты памяти и таблицы. Таким образом, они могут совместно использовать одну и ту же линейную память и таблицу ссылок.</p> + +<div class="note"> +<p><strong>Примечание</strong>: Этот пример можно найти на GitHub в файле <a href="https://github.com/mdn/webassembly-examples/blob/master/understanding-text-format/shared-address-space.html">shared-address-space.html</a> (смотрите это также <a href="https://mdn.github.io/webassembly-examples/understanding-text-format/shared-address-space.html">вживую</a>).</p> +</div> + +<h2 id="Резюме">Резюме</h2> + +<p>На этом мы завершаем обзор основных компонентов текстового формата WebAssembly и того, как они отображены в WebAssembly JS API.</p> + +<h2 id="Смотрите_также">Смотрите также</h2> + +<ul> + <li><a href="http://webassembly.org/docs/semantics">Семантика WebAssembly</a> для информации по всем возможным инструкциям.</li> + <li><a href="https://github.com/WebAssembly/spec/blob/master/interpreter/README.md#s-expression-syntax">Грамматика текстового формата</a>, который реализован в интерпретаторе спецификации.</li> +</ul> |