Pull to refresh

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

Reading time7 min
Views21K
Original author: azakai
Статья является дополненным переводом статьи «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 и схемой.
Tags:
Hubs:
Total votes 53: ↑50 and ↓3+47
Comments28

Articles