Портируем C/C++ библиотеку на JavaScript (xml.js)

Original author: azakai
  • Translation
Статья является дополненным переводом статьи «HOWTO: Port a C/C++ Library to JavaScript (xml.js)» (автор: azakai). Автор оригинальной статьи имеет приличный опыт портирования C/C++ библиотек в JavaScript. В частности, он успешно портировал lzma.js и sql.js. В своей статье он описывает общую схему портирования C/C++ кода на примере libxml – открытой библиотеки для валидации XML.

Помимо этого данная статья содержит полную последовательность действий, которые потребовались для портирования libxml в окружении Ubuntu 12.04. В том числе необходимую настройку окружения и emscripten.

Установка и настройка Emscripten


Emscripten — компилятор из LLVM байт-кода в JavaScript. C/C++ код может быть скомпилирован в LLVM байт-код с помощью компилятора clang. Некоторые другие языки так же имеют компиляторы в LLVM байт-код. Emscripten на основе байт-кода генерирует соответствующий JavaScript-код, который может быть выполнен любым интерпретатором JavaScript, например современным браузером. С помощью emscripten ребята из Mozilla не так давно успешно портировали Doom.

Emscripten предоставляет: emconfigure – утилита настройки окружения и последующего запуска ./configure; emmake – утилита для настройки окружения и последующего запуска make; emcc – компилятор LLVM в JavaScript;

Итак, настроим окружение для работы с emscripten (см. руководство).

Устанавливаем clang+llvm(>=3.0):
wget llvm.org/releases/3.0/clang+llvm-3.0-i386-linux-Ubuntu-11_10.tar.gz
tar xfv clang+llvm-3.0-i386-linux-Ubuntu-11_10.tar.gz

Устанавливаем node.js (>=0.5.5):
sudo apt-get install nodejs

Выгружаем текущую версию emscripten:
git clone git://github.com/kripken/emscripten.git
cd emscripten

Проверяем работоспособность clang:
../clang+llvm-3.0-i386-linux-Ubuntu-11_10/bin/clang tests/hello_world.cpp
./a.out
>> hello, world!

Проверяем работоспособность node.js:
node tests/hello_world.js
>> hello, world!

Запускаем emcc в первый раз, чтобы создать конфигурационный файл '~/.emscripten':
./emcc

В конфигурационном файле нужно указать директорию clang+llvm, а так же директорию установки emscripten:
EMSCRIPTEN_ROOT = os.path.expanduser('~/path/emscripten') # this helps projects using emscripten find it
LLVM_ROOT = os.path.expanduser('~/path/clang+llvm-3.0-i386-linux-Ubuntu-11_10/bin')

Нужно запустить emcc повторно, чтобы убедиться, что он сконфигурирован правильно. В этом случае он выдаст сообщение 'emcc: no input files':
./emcc
>> emcc: no input files

Теперь можно проверить, что все работает корректно, скомпилировав hello_wolrd.cpp с использованием emcc:
./emcc tests/hello_world.cpp
node a.out.js
>> hello, world

Часть 1: Компилируем исходники C/C++


Прежде чем приступить к портированию, следует убедиться, что исходные коды проекта компилируются без ошибок компилятором C/C++.

Выгружаем libxml из репозитория и компилируем:
git clone git://git.gnome.org/libxml2
cd libxml2
git checkout v2.7.8
CC=~/path/clang+llvm-3.0-i386-linux-Ubuntu-11_10/bin/clang ./autogen.sh --without-debug --without-ftp --without-http --without-python --without-regexps --without-threads --without-modules
make

В состав libxml входит консольная утилита xmllint для валидации xml-схем. Её можно использовать для проверки корректности работы скомпилированного кода. Выполнять такого рода проверки необходимо, в том числе, чтобы убедится, что оригинальная и портированная версии работают одинаково корректно. Тестирование с помощью xmllint выглядит примерно так:
$./xmllint --noout --schema test.xsd test.xml
>> test.xml validates

Если все работает корректно, внесите несколько изменений в файл test.xml и тогда xmllint выведет сообщение об ошибке.

Часть 2: Конфигурирование


Сконфигурировать проект для компиляции с использованием emscripten можно командой:
~/path/emscripten/emconfigure ./autogen.sh --without-debug --without-ftp --without-http --without-python --without-regexps --without-threads --without-modules

emconfigure устанавливает переменные окружения таким образом, чтобы ./configure использовал emcc компилятор вместо gcc или clang. Он настраивает окружение так, чтобы ./configure работал корректно, включая конфигурационные тесты (которые компилируют нативный код).

Результаты конфигурации по умолчанию (без флагов) включают множество ненужного на данном этапе функционала, например поддержка HTTP и FTP. Мы же просто хотим валидировать xml-схемы, поэтому следует сконфигурировать проект, исключив ненужный функционал. Вообще, это хорошая идея – исключать лишний функционал при портировании. Благодаря этому код будет меньше по размеру, что важно для сетевого окружения. Кроме того, некоторые заголовочные файлы могут потребовать ручной правки (те файлы, которые используют newlib, а не glibc).

Часть 3: Сборка проекта


Сборка выполняется командой:
~/path/emscripten/emmake make

emmake похож на emconfigure: он так же устанавливает переменные окружения. Благодаря emmake во время сборки генерируется LLVM байт-код вместо нативного кода. Это сделано для того, чтобы избежать генерации JavaScript-кода для каждого объектного файла и последующей его компоновки. Вместо этого используется компоновщик LLVM байт-кода.

В результате сборки строится множество различных файлов. Но они не могут быть выполнены. Как было сказано выше, это LLVM байт-код (его можно просматривать с помощью BC), поэтому нам нужен следующий шаг.

Часть 4: Преобразование в JavaScript


xmllint зависит xmllint.o и libxml2.a. LLVM компоновщик не поддерживает динамическую компоновку (позднее связывание) и emcc игнорирует его. Поэтому придется вручную указать статическую библиотеку libxml2.a для компоновки.

Чуть менее очевидна зависимость от libz (открытая библиотека для сжатия). Если выполнить компоновку без libz.a, то во время выполнения при попытке вызова функции «gzopen» произойдет ошибка. Соответственно, нужно собрать libz.a:
cd ~/path
wget zlib.net/zlib-1.2.7.tar.gz
tar xfv zlib-1.2.7.tar.gz
cd zlib-1.2.7
~/path/emscripten/emconfigure ./configure --static
~/path/emscripten/emmake make

Теперь можно скомпилировать JavaScript-код:
cd ~/path/libxml2
~/path/emscripten/emcc -O2 xmllint.o .libs/libxml2.a ../zlib-1.2.7/libz.a -o xmllint.test.js --embed-file test.xml --embed-file test.xsd

Где:
  • emcc – замена для gcc или clang (см. выше);
  • -O2 – флаг оптимизации. Выполняются LLVM- и дополнительные оптимизации на уровне JavaScript, включая Closure Compiler (в режиме advanced);
  • файлы, которые нужно скомпоновать;
  • -o – результирующий файл xmllint.test.js. Суффикс «js» указывает emcc на формат генерируемого кода, в данном случае JavaScript;
  • --embed-file – указывает emcc включить содержимое указанного файла в генерируемый код и настроить виртуальную файловую систему так, чтобы эти файлы были доступны через стандартные вызовы stdio (fopen, fread, etc.). Это самый простой способ получить доступ к файлам из скомпилированного кода.


Часть 5: Тестируем JavaScript


JavaScript-консоль, предоставляемая Node.js, SpiderMonkey или V8 может быть использована для запуска этого кода:
node xmllint.test.js --noout --schema test.xsd test.xml
>> test.xml validates

Результат должен быть в точности такой же, как и у нативного кода. Точно так же, если внести ошибки в xml-схему, xmllint должен их обнаружить.

Важно: все аргументы, используемые для нативной и JavaScript-сборки должны быть в точности идентичными.

Часть 6: Рефакторинг и повторное использование


На данный момент в скрипте захардкожены два файла для валидации. Нам же нужна обобщенная функция для валидации любого XML-файла по схеме. На самом деле сделать это просто, правда нужно учитывать, что код оптимизируется с помощью Closure Compiler, что добавляет работы.

Первое, что необходимо – вызывать emcc с опцией --pre-js. Она добавляет JavaScript-код перед сгенерированным кодом (post-js, соответственно, – после). Важно то, что --pre-js добавляет код еще до выполнения оптимизаций. А это значит, что код будет оптимизирован совместно с сгенерированным кодом, что необходимо для корректной оптимизации. С другой стороны, оптимизатор Closure Compiler может отбросить нужные нам функции как неиспользуемые.

Вот скрипт, который нужно включить с использованием опции --pre-js:
  Module['preRun'] = function() {
    FS.createDataFile(
      '/',
      'test.xml',
      Module['intArrayFromString'](Module['xml']),
      true,
      true);
    FS.createDataFile(
      '/',
      'test.xsd',
      Module['intArrayFromString'](Module['schema']),
      true,
      true);
  };
  Module['arguments'] = ['--noout', '--schema', 'test.xsd', 'test.xml'];
  Module['return'] = '';
  Module['print'] = function(text) {
    Module['return'] += text + '\n';
  };

Рассмотрим этот скрипт:
  • Module – объект, посредством которого сгенерированный с помощью emscripten код взаимодействует с другим JavaScript-кодом.
  • Важно использовать строковые имена для доступа к модулю, например Module['name'] вместо Module.name. В этом случае Closure оставит имя неизменным.
  • Первое, что необходимо сделать — изменить Module.preRun, выполняющийся непосредственно перед сгенерированным кодом (но после настройки окружения). В функции preRun создаются два файла с использованием API файловой системы (Emscripten FileSystem API). Для простоты используются те же имена файлов, что и в предыдущих тестах (test.xml и test.xsd). Содержимое этих файлов устанавливается равным Module['xml'] и Module['xsd']. Эти переменные должны содержать XML и XML-схему. Строки преобразуются в массив с помощью intArrayFromString.
  • Устанавливаем Module.arguments — эквивалент списка аргументов для консольной команды. Аргументы должны быть точно такими, которые мы использовали ранее при тестировании. Единственное отличие в том, что файлы test.xml и test.xsd будут содержать пользовательские данные.
  • Module.print вызывается в тот момент, когда код пытается вызвать операцию из stdio. Сохраняем весь вывод в буфер, чтобы впоследствии считать его.

Таким образом, мы добились того, что входные файлы test.xml и test.xsd будут содержать информацию, введенную пользователем, а результаты валидации сохранятся в буфер.

Однако, это еще не все. Скомпилируем код:
~/path/emscripten/emcc -O2 xmllint.o .libs/libxml2.a ../zlib-1.2.7/libz.a -o xmllint.raw.js --pre-js pre.js

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

После компиляции xmllint.raw.js содержит оптимизированный и минифицированный код. Для удобства использования обернем его JavaScript-функцией:
  function validateXML(xml, schema) {
    var Module = {
      xml: xml,
      schema: schema
    };
    {{{ GENERATED_CODE }}}
    return Module.return;
  }

GENERATED_CODE должен быть замещен результатом компиляции (xmllint.raw.js). Функция validateXML присваивает полям xml и schema соответствующие аргументы. Тем самым мы добиваемся того, что файлы test.xml и test.xsd содержат пользовательские данные. После того как сгенерированный код выполнится, функция возвратит результаты валидации.

Вот и все! xml.js может быть использована из обычного JavaScript-кода. Все, что нужно – это просто включить js-файл и вызвать функцию validateXML c xml и схемой.
Share post

Similar posts

Comments 28

    +5
    Прекрасный пошаговый walkthrough, спасибо.
      +9
      Ребята, Вы что, серьезно, считаете «перекомпиляцию» байт-кода в код на js «портированием»? Вы хоть видели код, который получился — будет он вообще работать-то в браузерной песочнице на реальных объемах данных ?!

      Я понимаю, если бы был только объектный код, но у Вас же есть исходник на С/С++!
        +6
        Так многое было портирована на js, и ничего, работает.
          +1
          портировано*
          0
          А, простите — Зачем?

          Нет, можно, конечно, но я подозреваю что здесь речь всё таки не совсем о браузере.
            0
            Так что — само то.
            +1
            Мне кажется emscripten это проект больше на будущее. Он развивает экосистему LLVM и JavaScript. А генеренный код можно использовать например в nodejs. На данном этапе xml.js работает заметно медленнее своего нативного аналога, но я надеюсь что в будущем оптимизирующие возможности emscripten тоже подрастут.
              +1
              FAQ гласит, что джаваскрипт работает втрое-вчетверо медленнее, чем итог компиляции «gcc -O3».

              Если веровать закону Гордона-Мура (удвоение мощностей за два года), то употребление Emscripten как бы отбрасывает современные компьютеры к мощностям 2008 года, что не так уж и плохо.
                +4
                Учитывая однопоточность — дальше…
            +1
            Спасибо, действительно – Very Nice.
            +1
              +5
              Вот и дожили до момента, когда плюсовые либы портируют в js :)
                0
                Сразу возникает вопрос: а где Emscripten работает — только под Linux?

                В Википедии я не нашёл списка поддерживаемых операционных систем, в README на Гитхабе и в тамошней вики на заглавной странице тоже не нашёл.

                Только добравшись до FAQ, насилу прочёл, что работает в Windows, OS X и Linux, хотя автор проверяет его только на Linux. В пособии также сказано, что под Windows требуется Cygwin для «make».
                  0
                  Я ничего не понял
                  У вас была программа (xmllint), которая в качестве аргументов командной строки берет имена xml и xcd файлов и проверяет соответствие xml xcd. Результат проверки выводится в консоль.
                  В результате всех этих манипуляций вы (или автор оригинальной статьи) получили js-файл с единственной функцией ValidateXML; в качестве параметров она получает имена файлов, в качестве результата выдает то же самое, что и xmllint
                  Если все так, то
                  а) почему везде написано «портирование библиотеки», когда на самом деле портируется программа, использующая библиотеку
                  б) возможно ли все-таки портировать именно libxml так, чтобы получить нечто вроде github.com/polotek/libxmljs но на чистом js без зависимостей?
                    0
                    Библиотека — libxml, программа — xmllint.
                      +3
                      Выше уже пробегала мысль что это не есть портирование в чистом виде. Скорее это транслирование LLVM байт-кода в JavaScript. По какой причине в оригинальной статье используется термин портирование мне не известно, для себя я счел это не важным. Скорее это вопрос терминологии.

                      А суть что происходит вы уловили правильно. Это лишь общая схема показывающая как можно выдернуть конкретную функцию из libxml.

                      Для того что бы получить порт библиотеки нужно написать обертку для вызова каждой конкретной функции из libxml2. Выполнив команду:
                      ~/path/emscripten/emcc .libs/libxml2.a ../zlib-1.2.7/libz.a -o libxml2.js

                      Построится файл libxml2.js который будет содержать функции соответствующих статических библиотек, эти функции можно напрямую вызывать из JavaScript кода. Правда делается это не очень красиво, как-то так:
                      int_sqrt = cwrap('int_sqrt', 'number', ['number'])
                      int_sqrt(12)

                      Пример показывает как обернуть c-функцию, подробнее тут.

                        –1
                        Какая разница каким образом было сделано портирование? Портирование нацелено на результат, а не процесс.
                          –1
                          Разница примерно такая же, как результат перевода человеком или каким-нибудь «промтом». Вроде бы и всё понятно, но местами коряво, местами смешно.
                            0
                            Нет. Языки программирования имеют очень жесткую семантику и синтаксис в отличии от языков на которых мы говорим.
                              0
                              Это каким-то образом мешает писать говнокод? Нет.
                              Это мешает писать глючные программы? Ни капли.
                              Тогда при чём тут жёсткая семантика?
                              Можно написать очень понятный и чёткий код, который и человек хорошо прочитает, и интерпретатор/компилятор хорошо оптимизируют.
                              А можно написать нечто ужасное. Вот итог работы автоматических трансляторов (особенно через несколько этапов трансляции), программ оптимизированных совсем не под конечную платформу — и есть такой ужас.
                              При этом без возможности нормально исправить баг вылезший на неродной платформе.
                                0
                                Цикл for будет всегда циклом for, а не будет менять свое значение в зависимости от ситуации и происхождения программиста. Так понятнее?
                                  –1
                                  Программа состоит не только из циклов for. И есть ситуации когда то, что эффективно в оригинале, транслированное становится очень неэффективным. И есть конструкции, имеющиеся в оригинальной платформе, но эмулируемые на конечной. Или в Вашем понимании вся трансляция сводится к замене одного синтаксиса на другой? Если бы всё было так просто — не было бы такого количества языков, а была бы масса препроцессоров для одного.
                                    –1
                                    Хорошо, попробую объяснить свою точку зрения на практическом примере: великолепный проект repl.it/#
                                    Возьмём питон. Вот он транслированный в javascript: raw.github.com/replit/empythoned/master/dist/python.opt.js
                                    Задача: впилить сюда интеграцию с браузером и DOM страницы.
                                    Теперь Вы понимаете разницу между портировать и просто транслировать? Портированный — был бы читабельным аналогом оригинального кода и можно было бы легко найти соответствия и оперировать ими, можно было бы легко изучать структуру. Транслрованный — просто мешанина однообразных символов с точки зрения человека. Про неоптимальность этого кода для целевой платформы — я уже тоже упоминал.
                                    Да, это работает. Но, сделать с этим что-то вменяемое по целесообразной цене — нереально.
                                      0
                                      А вы понимаете, что транслирование это только первый этап?
                                        –2
                                        А вы понимаете, чем отличается первый этап от завершённой работы?
                                          +1
                                          А вы понимаете, что в некоторых случаях первого этапа достаточно?
                                            –1
                                            Ну так если в каком-то частном случае оказалось достаточно «заглушки» — это не значит, что «заглушка» равноценна полноценному инструменту
                                        0
                                        Так же, если результат тралирования работает без доработок — было выполнено портирование. Понимаете?
                                          –1
                                          Не было выполнено — поскольку «работает» без доработок лишь частично: оно работает хуже и с ошибками. И эти баги исправлять — задача на порядки более сложная.

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