Вступление
Думаю, что многие сталкивались с проблемой скорости работы приложений, написанных на Node.JS, ведь JavaScript не отличается высокой скоростью при сравнении с тем же C++. Но мало кто знает, что от этой проблемы можно избавиться при помощи написания нативных V8 модулей. Давайте разберёмся, что же это такое.
Что же такое V8?
V8 - последняя версия движка JavaScript, который распространяется с открытым исходным кодом и разрабатывается компанией Google. Помимо этого движка существуют и другие, но производительность V8 в разы выше них. Большая часть браузеров в данный момент используют движок Chromium, который, в свою очередь, написан на движке V8.
Что такое нативный модуль в Node.JS?
Нативный модуль представляет из себя скомпилированный C++ код на движке V8 (Node.JS тоже написан на нём), который работает с примерно такой же скоростью, что и код на чистом C++. Далее в статье мы сравним скорость программы, которая написана на чистом JS с нативным модулем V8.
Помимо повышения производительности мы можем использовать V8 модули для взаимодействия с внешним или внутренним API, то есть появляется возможность использования win32 API, DLL библиотек (в чистом Node.JS их использовать нельзя), а также СОВЕРШЕННО всё, что можно сделать на C++.
Почему бы не использовать обычный C++?
Конечно, многие могут сказать, что это бессмысленно, ведь можно просто писать на C++ без лишней мороки, но не торопитесь с выводами. Тот, кто хотя бы раз углублялся в тот же win32 API, понимает, что создание интерфейсов без использования сторонних библиотек, например, QT, - занятие адской сложности. Что же мы можем получить в связке с Node.JS? В Node.JS существуют такие фреймворки, как Electron и NW.JS, которые упрощают создание пользовательских интерфейсов во много раз, т.к. их создание на стеке HTML + CSS + React/Vue/Angular - задача легче лёгкого, а уже это позволяет увеличить скорость разработки, облегчить дальнейшее обновление интерфейса, упростить в сотни раз процесс создания анимации элементов.
Подготовка к работе
Для компиляции модулей нам потребуется NPM пакет node-gyp.
Все инструкции по его установке в доступном виде могут быть найдены на странице GitHub репозитория, поэтому не вижу смысла расписывать его более подробно.
Начало работы
1. Первым делом напишем простой скрипт на JS и засечём время его исполнения...
Как вы можете заметить: для выполнения такой, казалось бы, простой задачи JS требуется 2 миллисекунды.
2. Теперь создаём файл module.cc со следующим исходным кодом:
Давайте разберёмся, что к чему...
Первым делом мы подключаем заголовочный файл node.h, который автоматически добавит к проекту пакет node-gyp.
Аргументом функции является аргумент, в который Node.JS передаёт данные о текущем состоянии скрипта во время вызова функции.
Функция GetIsolate возвращает текущий JS контекст, с помощью которого мы можем выполнять операции по типу перевода значений int, bool, std::string и т.д. в понятный для JS вид, что и делается на 16-ой строке кода.
Функция GetReturnValue получает объект, в котором хранится значение, которое будет возвращено в JS.
Функция Initialize содержит в себе код инициализации модуля, в котором присваиваются все созданные функции через функцию NODE_SET_METHOD, первым аргументом которого является exports, вторым - название переменной после экспорта в JS, третьим же - сама функция.
3. Следующим шагом нам нужно создать файл binding.gyp, в котором будет находиться вся информации о компиляции нашего модуля.
Более подробно об этом файле и его содержимом вы можете прочитать в официальной/неофициальной документации, но в данном случае нам нужно только это:
4. Открываем CMD/PowerShell в директории с binding.gyp и module.cc, пишем команды:
$ node-gyp configure
$ node-gyp build
Если при выполнении одной из этих команд у вас возникли ошибки, то убедитесь, что вы корректно выполнили установку/настройку пакета node-gyp (инструкция настройки для разных ОС находится на странице официального репозитория).
Если же компиляция прошла успешно, то по пути build/Release у вас должен лежать файл с расширение node (в нашем случае - test-native-module.node).
5. Подключим наш модуль в index.js файл и выполним функцию с подсчётом времени выполнения.
6. Выполняем index.js для того, чтобы, наконец, узнать результат.
Думаю, разница видна невооружённым глазом. Несмотря на разную скорость выполнения функций (время отображено в МИКРОСЕКУНДАХ, а НЕ в миллисекундах, как это было во время прошлого измерения), мы можем заметить, что обе функции возвращают одно и то же значение.
Итог
Что же в итоге? Мы можем понять, что скорость выполнения нативной функции выше обычной аж в 30 раз, что даёт огромный прирост производительности, особенно если задача будет сложнее.
В следующих статьях мы рассмотрим использование win32 API, примеры с выполнением реальных задач при помощи нативных модулей (например, интеграция со SteamWorks API в Node.JS).
Ссылки
Репозиторий node-gyp: https://github.com/nodejs/node-gyp
Документация по модулям: https://nodejs.org/api/addons.html
V8 Engine API: https://v8docs.nodesource.com/node-4.8/