Основы разработки нативных расширений для Node.js
Если в двух словах рассказать о нативных расширениях для Node.js, можно отметить, что они представляют собой некую логику, реализованную на C++, которую можно вызывать из JavaScript-кода. Здесь стоит прояснить особенности работы Node.js и рассмотреть составные части этой платформы. Важно знать, что, в контексте Node.js, мы можем говорить о двух языках — JavaScript и C++. Всё это можно описать так:
- JavaScript — это язык программирования, на котором пишут проекты под Node.js.
- V8 — это движок, занятый выполнением JS-кода.
- Libuv — это библиотека, написанная на C, реализующая асинхронное выполнение кода.
Где же в этой схеме находятся нативные расширения? Рассмотрим это на примере операций для работы с диском. Доступ к дисковой подсистеме не входит в возможности JavaScript или V8. Libuv даёт возможности асинхронного выполнения кода. Однако, пользуясь Node.js, можно писать данные на диск и читать их. Именно здесь на помощь приходят нативные расширения. Модуль
fs
реализован средствами C++ (у него имеется доступ к диску), он даёт нам методы, вроде writeFile
и readFile
, которые можно вызывать из JavaScript.Взаимодействие с нативными расширениями из JS-кода
Понимая этот механизм, мы можем сделать первые шаги в разработке нативных расширений. Но, прежде чем заняться программированием, поговорим об инструментах.
Базовые инструменты
?Файл binding.gyp
Этот файл позволяет настраивать параметры компиляции нативных расширений. Один из важнейших моментов, которые нам надо определить, заключается в том, какие файлы будут компилироваться, и в том, как мы будем вызывать готовую библиотеку. Структура этого файла напоминает JSON, он содержит настройки цели (target) и исходников (sources) для компиляции.
?Инструмент node-gyp
Средство node-gyp предназначено для компиляции нативных расширений. Оно реализовано на базе Node.js и доступно в npm, что позволяет скомпилировать расширение соответствующей командой. В ответ на эту команду система обнаружит файл
binging.gyp
, находящийся в корневой директории, и приступит к компиляции расширения.Кроме того,
node-gyp
позволяет, по умолчанию, формировать релизные сборки или сборки для отладки. В результате, в зависимости от настроек, после компиляции, в папке release
или debug
, будет создан бинарный файл с расширением .node
.?Инструмент node-bindings
Пакет node-bindings позволяет экспортировать нативные расширения. Он отвечает за поиск соответствующих файлов в папке
build
или release
.?API n-api
N-api — это API, созданное средствами C, которое позволяет взаимодействовать с движком абстрактным способом, не зависящим от нижележащей среды исполнения. На наш взгляд, такой подход является результатом развития платформы, в ходе которого предпринимались усилия по портированию Node.js на различные архитектуры.
N-api даёт стабильность и совместимость при работе с различными версиями Node.js. Таким образом, если некое нативное расширение было скомпилировано для Node 8.1, не потребуется компилировать его снова для Node 8.6 или 9.3. Это упрощает жизнь тем, кто занимается поддержкой расширения или участвует в его разработке.
В данный момент n-api находится в экспериментальном состоянии.
?Инструмент node-addon-api
Модуль node-addon-api даёт в распоряжение разработчика C++-реализацию n-api, которая позволят пользоваться возможностями этого языка.
Первые шаги в мире нативных расширений
Обратите внимание на то, что для этого примера использована платформа Node 9.3.
Для того, чтобы приступить к разработке нативных расширений, мы напишем классическое приложение «Hello World». Идея этого приложения позволяет реализовать его с помощью достаточно простого кода, не перегруженного дополнительной логикой, что даст нам возможность сосредоточиться на основных конструкциях, рассмотреть минимально необходимый код.
Начнём с инициализации npm, что позволит затем установить зависимости.
npm init
Теперь устанавливаем зависимости.
npm i node-addon-api bindings
На данном этапе нужно создать файл на C++, содержащий логику расширения.
#include <napi.h>
Napi::String SayHi(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
return Napi::String::New(env, "Hi!");
}
Napi::Object init(Napi::Env env, Napi::Object exports) {
exports.Set(Napi::String::New(env, "sayHi"), Napi::Function::New(env, SayHi));
return exports;
};
NODE_API_MODULE(hello_world, init);
В этом файле имеется три важных части, которые мы рассмотрим, начиная с той, которая находится в нижней части текста.
NODE_API_MODULE
. Первый аргумент представляет собой имя нативного расширения, второй — имя функции, которая инициализирует это расширение.init
. Это — функция, которая ответственна за инициализацию расширения. Тут мы должны экспортировать функции, которые будут вызываться из JS-кода. Для того чтобы это сделать, нужно записать имя функции в объектexports
и задать саму функцию, которая будет вызываться. Функцияinit
должна возвращать объектexports
.sayHi
. Эта функция будет выполнена при вызове нашего нативного расширения из JavaScript.
Теперь создадим файл
binding.gyp
, который будет содержать конфигурацию нативного расширения.{
"targets": [
{
"cflags!": [ "-fno-exceptions" ],
"cflags_cc!": [ "-fno-exceptions" ],
"include_dirs" : [
"<!@(node -p \"require('node-addon-api').include\")"
],
"target_name": "hello_world",
"sources": [ "hello_world.cc" ],
'defines': [ 'NAPI_DISABLE_CPP_EXCEPTIONS' ]
}
]
}
А вот как выглядит JavaScript-код, в котором подключается расширение и осуществляется работа с ним.
const hello_world = require('bindings')('hello_world')
console.log(hello_world.sayHi());
Теперь осталось лишь скомпилировать расширение и запустить JS-файл. Вот как это выглядит.
Компиляция и использование нативного расширения
История нативных расширений для Node.js и полезные материалы
Полагаем, полезно рассказать об истории нативных расширений, так как изучение этого вопроса позволяет исследовать большой объём полезной документации и обнаружить множество примеров. N-api пришло на смену nan. Эта аббревиатура расшифровывается как Native Abstraction for Node.js. Nan — это C++-библиотека, которая не отличается той же гибкостью, что и n-api. Она позволяет, в абстрактном виде, работать с V8, но привязана к релизу V8. В результате, в новых релизах Node.js могут присутствовать изменения V8, которые способны нарушить работу нативных расширений. Решение этой проблемы — одна из причин появления n-api.
Знание о существовании nan позволяет нам исследовать соответствующие примеры и документацию. Всё это может стать полезным источником знаний для тех, кто изучает разработку нативных расширений для Node.js.
Вот список полезных материалов, посвящённых разработке нативных расширений для Node:
- Примеры использования n-api.
- Примеры работы с node-addon-api.
- Примеры использования nan.
- Тесты, ценный источник знаний.
- Ресурс, посвящённый нативным расширениям для Node.js.
Итоги
Автор этого материала говорит, что изучение разработки нативных расширений для Node.js помогло ему лучше понять устройство этой платформы и особенности её функционирования. Существует множество сценариев, в которых подобные расширения могут оказаться полезными. Среди них — создание высокопроизводительных приложений, интеграция Node.js-проектов с C/C++ библиотеками, или использование в таких проектах устаревшего кода. Кроме того, разработка нативных расширений — это отличный способ изучения внутренних механизмов Node.js.
Уважаемые читатели! Пользуетесь ли вы нативными расширениями для Node.js в своих проектах?
Комментарии (3)
xxiidd
13.04.2018 13:47Приходилось для своего проекта дорабатывать node-привязку замечательной либы для работы с аудиотегами taglib2. В привязке были реализованы только статические популярные теги, нужно было реализовать динамические. До этого с С++ дела не имел — познал немного боли, но в итоге за пару вечеров справился. Крутой опыт на самом деле.
chupAkabRRa
13.04.2018 13:47Пользовался, да. Надо было как-то предоставить доступ к методам нативной плюсовой библиотеки через веб-сервис. Решили проинтегрировать в виде модуля к NodeJS. Полезли дикие какие-то крэши, когда библиотека пыталасть проинициализировать себя. Выяснилось, что расширения для NodeJS не дружат с плюсовым std::unordered_map от слова совсем. Даже на StackOverflow поднимали этот вопрос: Error with node-gyp addon when in use of insert method — std::unordered_map. В итоге пришлось workaround делать: библиотеку обернули в демон-процесс, с которым общались из нативного NodeJS расширения через пайпы. В целом, опыт позитивный: все быстро и красиво. Еще б эти странности с unordered_map пофиксили…
sT331h0rs3
Большое спасибо! Давно искал статью с простым примером нативного расширения.