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



Основы разработки нативных расширений для 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:


Итоги


Автор этого материала говорит, что изучение разработки нативных расширений для Node.js помогло ему лучше понять устройство этой платформы и особенности её функционирования. Существует множество сценариев, в которых подобные расширения могут оказаться полезными. Среди них — создание высокопроизводительных приложений, интеграция Node.js-проектов с C/C++ библиотеками, или использование в таких проектах устаревшего кода. Кроме того, разработка нативных расширений — это отличный способ изучения внутренних механизмов Node.js.

Уважаемые читатели! Пользуетесь ли вы нативными расширениями для Node.js в своих проектах?

Комментарии (3)


  1. sT331h0rs3
    13.04.2018 13:47

    Большое спасибо! Давно искал статью с простым примером нативного расширения.


  1. xxiidd
    13.04.2018 13:47

    Приходилось для своего проекта дорабатывать node-привязку замечательной либы для работы с аудиотегами taglib2. В привязке были реализованы только статические популярные теги, нужно было реализовать динамические. До этого с С++ дела не имел — познал немного боли, но в итоге за пару вечеров справился. Крутой опыт на самом деле.


  1. 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 пофиксили…