Собираем ваш первый WebAssembly-компонент

Original author: Nick Larsen
  • Translation
Когда я впервые услышал о технологии WebAssembly — она сразу показалось мне крутой вещью и мне сразу захотелось попробовать её в деле. От первого желания, до чего-то работающего мне, однако, пришлось потратить немало времени и порой испытать кое-какие разочарования. Для того, чтобы сохранить ваше время и ваши нервы, если вам захочется повторить тот же путь, и написана данная статья.

image
Предупреждение читателю

Эта статья написана 24-го июня 2016-го года. Поскольку WebAssembly очень молодая и динамично развивающаяся технология, со временем многие описанные в данной статье вещи устареют или полностью изменятся — учитывайте это.

А теперь поехали.

Что такое WebAssembly?

Официальная документация говорит следующее: «WebAssembly или wasm это новый портабельный, эффективный по размеру и скорости загрузки формат компиляции для веба». Эм-м-м-м… Что? Формат чего? Текстовый или бинарный? Да, это откровенно плохое описание. Так что убирайте уже ваши баззворд-бинго карточки и я, на основе моего опыта, дам своё определение:

«WebAssembly или wasm это спецификация байткода для написания производительных, браузеро-независимых компонентов для веба». Это определение, тоже, конечно, не вершина эпистолярного жанра, но я попробую его дополнить. WebAssembly позволяет повысить производительность с помощью использования статически типизированных переменных, которые обходятся на рантайме значительно дешевле динамических. WebAssembly разрабатывается W3C Community Group и планируется быть внедрённым во все основные браузеры. И с этого момента на стол выкладывается киллер-фича: вы сможете писать код веб-компонентов на любом языке программирования.

Теперь звучит лучше, неправда ли?

Давайте начнём

Когда я изучаю какую-нибудь новую вещь, я обычно ищу минимально возможный пример, достаточный для того, чтобы посмотреть, как всё работает. К сожалению, этот подход не очень-то возможен с WebAssembly. В текущего состоянии спецификации wasm — это просто формат байткода. Представьте себе как в каком-нибудь 1996-ом году инженеры Sun Microsystems представляли бы JVM… но без Java. Разговор шел бы как-то так:

"- Эй вы все, зацените какую классную машину для выполнения байткода мы создали!
— Круто! А как под неё писать код?
— А вот так:

image

— Эм-м-м-м… круто… Я попробую как-нибудь на досуге.
— Отлично, дай нам знать, если будут какие-нибудь проблемы или идеи!
— Да-да. Но я тут немного занят, нужно посмотреть несколько других вещей… Но как только — так сразу!"

И даже это плохой пример, поскольку JVM хотя бы базируется на языке Java, а с WebAssembly у нас нет и этого. Я надеюсь, вы уловили мысль. Если вы представляете байткод без инструмента, который компилирует в этот байткод код какого-нибудь языка программирования — вам будет трудновато продвигать его. Так как же нам всё-таки начать работать с WebAssembly?

Что было до WebAssembly?

Большинство технологий являются результатом развития каких-то предшествующих технологий, в особенности когда планируемой целью является получить некоторую формальную спецификацию. WebAssembly не исключение, это продолжение разработки идей, заложенных когда-то в asm.js, спецификации, предназначенной для написания javascript-кода таким образом, чтобы его было возможно скомпилировать со статической типизацией. Wasm развил эти идеи созданием спецификации байткода, который может быть создан компилятором любого языка программирования, затем переслан через Интернет в виде бинарного файла, пригодного для исполнения любым современным браузером.

asm.js это лишь спецификация для написания javascript-кода с использованием подмножества возможностей языка Javascript. Вы можете написать код на asm.js вручную и, если вам не терпится уже взять и что-то написать — самое время начать.

function MyMathModule(global) {
    "use asm";
    var exp = global.Math.exp;
    function doubleExp(value) {
        value = +value;
        return +(+exp(+value) * 2.0);
    }
    return { doubleExp: doubleExp };
}

Это не очень полезная функция, но она написана согласно спецификации asm.js. Если она для вас выглядит слегка глуповато — так знайте, что вы не единственный, кто так считает. Однако, все эти «странные» символы (все эти унарные операторы) необходимы. Они указывают компилятору на типы данных в операциях. Код весьма простой, но если вы где-нибудь ошибётесь — консоль отладки покажет достаточно читабельное сообщение об ошибке.

Если вы захотите использовать данную функцию, сделать это можно как-то так:

var myMath = new MyMathModule(window);
for(var i = 0; i < 5; i++) {
    console.log(myMath.doubleExp(i));
}

И, если вы всё сделали верно, то на выходе должны увидеть нечто подобное:

image

И, наконец, переходим к WebAssembly

На данный момент у нас есть работающий кусочек кода на asm.js. Мы можем пойти на официальную страницу WebAssembly на GitHub и найти там инструменты для компиляции этого кода в wasm. Беда лишь в том, что нам придётся собрать эти инструменты самостоятельно. Это, откровенно говоря, худшая часть всего квеста. Данные инструменты постоянно меняются и время от времени находятся в сломанном состоянии, особенно в плане использования их под Windows.

Для сборки вам понадобятся make и cmake. Если вы работаете под Windows — понадобится ещё и Visual Studio 2015. Вот инструкции по сборке под Mac, а вот — под Windows.

image

Нужно отметить, что распространение собранных бинарников этих утилит было бы огромным шагом вперёд по популяризации технологии WebAssembly.

Если вы прошли через всё вышеописанное без проблем, то получили папку bin в папке binaryen, где и находятся инструменты для конвертации нашего asm.js кода в wasm. Первый инструмент называется asm2wasm.exe. Он преобразовывает код на asm.js в формат кода .s, который является текстовым представлением абстрактного синтаксического дерева (AST) формата wasm. Запустив asm2wasm на своём asm.js коде, вы получите что-то вроде этого:

(module
(memory 256 256)
(export "memory" memory)
(type $FUNCSIG$dd (func (param f64) (result f64)))
(import $exp "global.Math" "exp" (param f64) (result f64))
(export "doubleExp" $doubleExp)
(func $doubleExp (param $0 f64) (result f64)
(f64.mul
(call_import $exp
(get_local $0)
)
(f64.const 2)
)
)
)

Можно разобрать этот код по строкам, но сейчас я просто хочу подчеркнуть, что поскольку wasm это бинарный формат, просто кликнуть в браузере на чём-то и посмотреть код, как вы привыкли делать это с Javascript, уже не получится (по крайней мере на данный момент). То, что вы увидите, будет очень похоже на код выше.

Следующим шагом будет конвертация этого .s формата в wasm-бинарник, для этого мы воспользуемся утилитой wasm-as.exe. Применив её для вашего .s-файла на выходе вы получите байткод, ради которого мы и затевали всю эту историю.

image

image

Теперь возьмите последнюю версию Firefox или Chrome Canary и включите в них WebAssembly.

Для Firefox вам понадобиться открыть about:config и набрать «wasm» в строке поиска. После этого изменить значение опции javascript.options.wasm на true и перезапустить браузер. Для Chrome Canary вам нужно открыть chrome://flags, найти и включить опцию Experimental WebAssembly, после чего перезапустить браузер.

Теперь нам нужно запустить наш модуль в браузере. Для меня это поначалу оказалось проблемой, поскольку совершенно не очевидно, как это сделать. Я открыл консоль в Chrome Canary и попробовал набрать «WebAsse» — и ничего, никаких подсказок. Затем я набрал «Was» и получил подсказку! Этот объект в инспекторе выглядел весьма убого в плане документации. Я опущу весь рассказ о том, как я рылся в поисках работающего пример, скажу лишь что в конце-концов я его нашел в некотором файле JS.md в репозитории WebAssembly. Там было что-то вроде документации и примера, вот он:

fetch("my-math-module.wasm")
    .then(function(response) {
        return response.arrayBuffer();
    })
    .then(function(buffer) {
        var dependencies = {
            "global": {},
            "env": {}
        };
        dependencies["global.Math"] = window.Math;
        var moduleBufferView = new Uint8Array(buffer);
        var myMathModule = Wasm.instantiateModule(moduleBufferView, dependencies);
        console.log(myMathModule.exports.doubleExp);
        for(var i = 0; i < 5; i++) {
            console.log(myMathModule.exports.doubleExp(i));
        }
    });

Забросьте это в свой html-файл, поднимите локальный веб-сервер и откройте этот файл в браузере. Вот как это будет выглядеть:

image

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

image
Примите поздравления!

Вы создали ваш первый WebAssembly-компонент. Что дальше? Ну, мы лишь слегка сбросили покровы тайны. Написание asm.js кода было ключевым моментом данного примера и написание сколько-нибудь нетривиальной функциональности потребует времени и терпения. С использованием emscripten компиляция нетривиальных приложений в asm.js становится значительно проще. Я советую вам почитать спецификацию asm.js, особенно раздел о модели памяти, поскольку многие концепции перешли в WebAssembly напрямую из asm.js. Ещё один важный момент: в данный момент вы не можете передавать массивы как аргументы функции. Есть некоторая договорённость, что это должно измениться, но пока что это не отражено в спецификации. Самое время освежить в памяти логику работы с указателями.

Ещё один нюанс: когда вы начнёте писать нетривиальные вещи на wasm, то, возможно, заметите, что производительность порой может быть медленнее старого доброго Javascript. Просто учтите, что современные движки Javascript во всех браузерах весьма высоко оптимизированы и у wasm займёт какое-то время достичь их эффективности. Теоретический предел производительности у WebAssembly выше, чем у кода на Javascript в текстовой форме, но WebAssembly ещё не готов к промышленному применению.
Инфопульс Украина
0.00
Creating Value, Delivering Excellence
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 30

    +2
    Смотрел недавно исходники LLVM и нашел там WebAssembly target, так что можно уже наверное писать код на C без Emscripten.
      +2
      Интересно, я так понимаю данную технологию можно применять не только для повышения производительности, но и для обфускации кода JS. А обратное преобразование есть? Из байткода в JS (или в другой читабельный формат)?
        0
        А получится ли такой «фарш провернуть назад»? :)
          0
          Однозначно, да. Есть несколько инструментов для трансляции Байт кода jvm в исходный java код. Появление трансляторов wasm -> js вопрос времени.
            0
            Вернуть обратно в тот JS, который конвертировали в wasm — это можно, но зачем? Реальные-то исходники обычно на C/C++…
              0
              Ну на самом деле будет скорее сишный код — виртуальная машина, как я понял, поддерживает int, uint, float, double и указатели на кучу.
            0
            Получится, только вот если пере компиляцией кода в байт код, исходники обфусцировать, а потом скомпилировать, то при обратном процессе ты получишь очень не понятный код.
              0
              Для защиты кода — это как раз и хорошо.
          0
          Главный для меня вопрос, получается я смогу писать код для браузера на c#? И разор будет работать на клиенте? да целый asp.net получается можно вытащить на клиента, а сервер будет RESP api например + отдача загрузчика
            +3
            Существуют трансляторы из C# в JS, так что вы можете попробовать уже сейчас, без WebAssembly. Но ваш скрипт будет тащить за собой реализацию BCL под JS — в лучшем случае, слинкуются только прямо или косвенно используемые части (которых все равно будет очень много), а худшем — вся стандартная библиотека целиком.

            Если хотите получить нормальный, поддерживаемый результат — искренне советую использовать нативные инструменты и не превращать стек .NET в серебряную пулю. Попробуйте тот же Typescript — после C# он вполне привычный и удобный.
                0
                Вся хитрость в том, что вы считаете «лишним». Давайте для примера возьмем следующий код на C#:

                Console.WriteLine("hello".StartsWith("hell"));
                

                Идиоматическое решение на JS заняло бы также одну строчку:

                console.log("hello".indexOf("hell") === 0);
                

                Но автоматический транслятор так не работает. Он видит, что вы использовали метод StartsWith — значит в вашем приложении используется класс System.String, реализацию которого нужно перенести целиком. Открываем содержимое класса с помощью ILSpy и видим добрые две сотни методов! А если заглянуть внутрь некоторых, то увидим, что их реализация использует другие классы, например System.Convert, System.Text.Normalization, System.Text.StringBuilderCache, System.Globalization.CultureInfo. Их тоже нужно перенести, и их зависимости, и зависимости их зависимостей, и так далее.

                Процесс определения зависимостей крайне трудоемок. Нет способа быстро определить, на какие классы ссылается определенный класс в ходе своей работы. Более того, при использовании рефлексии или даже Activator.CreateInstance это может быть вообще невозможно определить статически. Поэтому придется подтянуть следующую по размеру единицу разбиения кода, которая может работать автономно и для которой зависимости видны явно — сборку. В итоге, ваша одна строчка кода потребовала подтянуть за собой mscorlib целиком.

                Невольно возникает вопрос — почему транслятор такой тупой? Мы же в 2016 году живем, у нас тут каждый второй — программист, работающий в коворкинге над стартапом по краудфандингу лэндингов, телефоны понимают голосовые команды, машины на автопилоте катаются — а написать идиоматичный код на JS нельзя? Да можно, конечно, но это потребует титанических усилий и никак не окупится. Изучить родные веб-технологии и переписать приложение на них все равно окажется проще.
                  +1
                  Но автоматический транслятор так не работает. Он видит, что вы использовали метод StartsWith — значит в вашем приложении используется класс System.String, реализацию которого нужно перенести целиком. Открываем содержимое класса с помощью ILSpy и видим добрые две сотни методов!

                  Это неправда! Хороший вырезатель зависимостей работает совсем не так как вы думаете! Агрессивный dataflow-анализ способен творить чудеса.

                  Процесс определения зависимостей крайне трудоемок. Нет способа быстро определить, на какие классы ссылается определенный класс в ходе своей работы.

                  Это правда. К сожалению, задача определения зависимостей плохо масштабируется, параллелится и плохо дружит с инкрементальной компиляцией. Тем не менее, процесс определения зависимостей нужен только для сборки production, там не сильно принципиально, что компилятор проработает минут 10 на CI-сервере. А во время разработки определять зависимости не обязательно.

                  Невольно возникает вопрос — почему транслятор такой тупой? Мы же в 2016 году живем, у нас тут каждый второй — программист, работающий в коворкинге над стартапом по краудфандингу лэндингов

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

                  а написать идиоматичный код на JS нельзя

                  А это и не требуется. Для программ на 10 строк вовсе не обязательно использовать трансляторы с «тяжёлых» языков вроде Java или C#. А для более серьёзных по объёму программ разница становится не слишком существенной.

                  Да можно, конечно, но это потребует титанических усилий и никак не окупится.

                  Титанических усилий это не требует. А вот чего требует — так это титанических усилий от маркетологов. Впрочем, с компиляторами всегда так — очень сложно вывести на рынок новый язык или рантайм, это всегда титаническая работа.

                  Изучить родные веб-технологии и переписать приложение на них все равно окажется проще.

                  Изучить родные веб-технологии действительно несложно, но тут есть другой фактор. Если у меня уже есть тонна кода для серверной части, которая при этом обвешана различными тулами для сборки проекта, статической проверки кода и т.д. то хотелось бы их по-максимуму переиспользовать для клиентского кода, а не выстраивать параллельно всю инфраструктуру фронтэнда с нуля. Да и частично переиспользовать сам серверный код на клиенте тоже очень приятно.
                    0
                    Это неправда! Хороший вырезатель зависимостей работает совсем не так как вы думаете! Агрессивный dataflow-анализ способен творить чудеса.

                    Хочется верить, что я действительно преувеличил и такие утилиты существуют, но среди бесплатных такого еще не встречал. Вышеупомянутый JSIL просто загружает целиковые сборки. Перспективный на первый взгляд Bridge.NET по факту подменяет стандартную библиотеку своей собственной частичной реализацией, и всё равно тащит за собой рантайм в размере 20к строк. Возможно, можно прогнать сгенерированный код через отдельные утилиты, но я не пробовал.
                    Если у меня уже есть тонна кода для серверной части, которая при этом обвешана различными тулами для сборки проекта, статической проверки кода и т.д. то хотелось бы их по-максимуму переиспользовать для клиентского кода, а не выстраивать параллельно всю инфраструктуру фронтэнда с нуля.

                    Такой подход имеет право на жизнь, но у него есть два существенных недостатка. Во-первых, такое решение будет сложнее поддерживать — программисты с достаточными знаниями встречаются реже и стоят дороже, чем веб-программисты. Во-вторых, транслятор сам по себе может содержать баги, обнаружить и устранить которые — отдельное приключение.
                      0
                      Хочется верить, что я действительно преувеличил и такие утилиты существуют, но среди бесплатных такого еще не встречал

                      Про .NET не знаю, но для Java есть мой проект, который очень хорошо вырезает неиспользуемый код.

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

                      Проблема в том, что зачастую тратится очень много времени на коммуникацию с этими веб-программистами. Кроме того, в последнее время веб усложняется и требования к квалификации веб-программистов растут.

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

                      Транслятор (в моём случае) написан на Java, которая содержит баги, которая в свою очередь линкуется с glibc, который содержит баги и который запускается на Linux, который содержит баги и который запускается на железе, которое, представьте себе, тоже содержит баги.
                        0
                        TeaVM — крутая штука. Спасибо за ссылку, поставил звёздочку — возможно, когда-нибудь пригодится.

                        Кроме того, в последнее время веб усложняется и требования к квалификации веб-программистов растут.
                        В этом и проблема. Человеку, который пишет веб-приложение на Java с помощью автоматического транслятора эти квалификации все равно потребуются. А веб-разработчику будет проще их наверстать из-за более узкой специализации.

                        Транслятор (в моём случае) написан на Java, которая содержит баги, которая в свою очередь...
                        Вы правы, но не забывайте, что количество пользователей Java/glibc/Linux и конкретно взятого транслятора различаются на несколько порядков. Это влияет как на скорость (вероятность?) исправления багов, так и на шанс получить помощь в незнакомой ситуации.

                        Только что попробовал повозиться с упомянутым выше Bridge.NET чуть более подробно. Буквально за пару минут наткнулся на идеальный пример того, почему бы не стал использовать подобное решение в продакшене. Вот так у них реализован класс System.Uri:

                        // @source Uri.js
                        
                        Bridge.define("System.Uri", {
                            constructor: function (uriString) {
                                this.absoluteUri = uriString;
                            },
                        
                            getAbsoluteUri: function () {
                                return this.absoluteUri;
                            }
                        });
                        

                        Вот как поступить в ситуации, если исходный код его использовал? Использовать свой велосипед вместо стандартного класса, патчить стандартную библиотеку транслятора, отправлять feature request и ждать у моря погоды? Ни одно из решений мне не кажется хорошим.
                      0
                      Мы уже год занимаемся выкидыванием похожей технологии из проекта и конца этому не видно.
                      Началось со здравой в общем-то идее — создадим структуры данных и модели на сервере, а для клиента — скомпилируем их же в JavaScript и придумаем «универсальный синхронизатор» который будет поддерживать данные в актуальном состоянии по обе стороны. Потом решили, что раз уже модели на C# — давайте и клиентский код на нем писать, благо bindings к jQuery шли с компилятором. А потом взяли нескольких фронтэндщиков, которые писали сразу на JS а чтобы не «отрываться от коллектива» — создавали заглушки на C# и вставляли в них свой код с помощью специальной аннотации. В результате получилась каша.
                      0
                  +1
                  Для этого надо не только сделать компилятор C# кода в wasm (или преобразователь из MSIL), но и проехаться им по стандартной библиотеке .NET и библиотекам ASP.NET.
                  А потом завернуть это вместе со своим приложением и получить файл на много-много мегабайт.
                  Поэтому, как мне кажется, если и можно будет писать на C# (или Java), то только используя некоторое подмножество стандартной библиотеки.
                    –1
                    .net core? Как я понимаю все компилируется заранее, лишнее вырезается. Так что клиент получает то, что ему надо.
                      –1
                      Действительно, итоговый файл получится легче, но все равно огромным. net core не делает никакой магии, просто теперь базовая библиотека классов подключается по отдельным сборкам (смотря что нужно), а не поставляется на машину целиком. Однако если вы в коде где-то использовали System.Collections.Generic, то все равно придется в WebAssembly тащить его целиком.
                        0
                        Хм, Юнитевский IL2CPP режет код даже очень неплохо. ЧТо мешает макрософту сделать так же?
                      –1
                      Есть различные техники, чтобы очень аккуратненько вырезать всё то, что ненужно. Мой проект использует такие техники, так что на выходе генерит достаточно маленький JS со всеми необходимым либами.

                      Конечно, всё это хорошо работает только в отсутствие reflection. Есть устоявшийся стереотип, что без reflection некоторые вещи написать невозможно. На самом деле, я разработал альтернативный механизм, на котором я успешно реализовал такие традиционно требующие reflection вещи, как сериализация/десериализация объектов в JSON и REST-клиент. Конечно, переиспользовать существующий код с reflection не получается, но всё равно, это гораздо лучше, чем TypeScript, в котором вообще нельзя переиспользовать код C#.

                      Правда, получается подмножество стандартной библиотеки, но оно достаточно большое, чтобы можно было писать вполне себе серьёзные приложения, и при этом получать достаточно маленькие JS-файлы.
                        0
                        Вот здесь http://cshtml5.com/#learn-more
                        пишут, что .NET Types and Framework Classes (45% supported)
                      –5
                      Использовать технологию slim binaries в 2016 году, это, конечно круто, главное, не забывать, откуда всё пошло и что это такое. Читаем 7 Эффективный подход к переносимому коду в статье «Оберон с гаджетами...», 1999.
                      Ну и погуглить на тему этих самых slim binaries и имени Michael Franz, который, по-сути, и является автором сеё штуки.
                      Это я так, исторических основ и перспектив ради.
                        0
                        Чувствуется если оно таки выйдет в production на большинстве браузеров, JS как ЯП потеряет как минимум 50% аудитории.
                        И бешеная популярность сегодня обернется стремительным падением…
                          –1
                          Немного классики по теме
                          https://www.destroyallsoftware.com/talks/the-birth-and-death-of-javascript
                          0
                          А как работать из WebAssembly с DOM?
                          0
                          Есть какие-либо исследования, связанные с информационной безопасностью для WebAssembly?

                          Only users with full accounts can post comments. Log in, please.