Дополнения C++¶
Аддоны - это динамически связанные общие объекты, написанные на C++. Функция require()
может загружать аддоны как обычные модули Node.js. Аддоны обеспечивают интерфейс между JavaScript и библиотеками C/C++.
Существует три варианта реализации аддонов: Node-API, nan или прямое использование внутренних библиотек V8, libuv и Node.js. Если нет необходимости в прямом доступе к функциональности, которая не раскрывается Node-API, используйте Node-API. Дополнительную информацию о Node-API см. в C/C++ addons with Node-API.
Если не использовать Node-API, реализация аддонов сложна и требует знания нескольких компонентов и API:
-
V8: библиотека C++, которую Node.js использует для реализации JavaScript. V8 предоставляет механизмы для создания объектов, вызова функций и т.д. API V8 документирован в основном в заголовочном файле
v8.h
(deps/v8/include/v8.h
в дереве исходников Node.js), который также доступен online. -
libuv: Библиотека на языке Си, реализующая цикл событий Node.js, его рабочие потоки и все асинхронное поведение платформы. Она также служит кроссплатформенной библиотекой абстракций, предоставляя простой, POSIX-подобный доступ во всех основных операционных системах ко многим общим системным задачам, таким как взаимодействие с файловой системой, сокетами, таймерами и системными событиями. libuv также предоставляет абстракцию потоков, подобную POSIX-потокам, для более сложных асинхронных аддонов, которым необходимо выйти за рамки стандартного цикла событий. Авторы аддонов должны избегать блокирования цикла событий при вводе/выводе или других трудоемких задач, перегружая работу через libuv на неблокирующие системные операции, рабочие потоки или пользовательское использование потоков libuv.
-
Внутренние библиотеки Node.js. Сам Node.js экспортирует C++ API, которые могут использовать аддоны, наиболее важным из которых является класс
node::ObjectWrap
. -
Node.js включает другие статически связанные библиотеки, включая OpenSSL. Эти другие библиотеки находятся в директории
deps/
в дереве исходников Node.js. Только символы libuv, OpenSSL, V8 и zlib целенаправленно реэкспортируются Node.js и могут в различной степени использоваться аддонами.
Все следующие примеры доступны для загрузки и могут быть использованы в качестве отправной точки для аддона.
Hello world¶
Этот пример "Hello world" представляет собой простой аддон, написанный на C++, который является эквивалентом следующего кода JavaScript:
1 |
|
Сначала создайте файл hello.cc
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
Все аддоны Node.js должны экспортировать функцию инициализации по образцу:
1 2 |
|
После NODE_MODULE
не ставится точка с запятой, поскольку это не функция (см. node.h
).
Имя module_name
должно совпадать с именем файла конечного бинарного файла (исключая суффикс .node
).
Так, в примере hello.cc
функция инициализации - Initialize
, а имя модуля аддона - addon
.
При создании аддонов с помощью node-gyp
, использование макроса NODE_GYP_MODULE_NAME
в качестве первого параметра NODE_MODULE()
обеспечит передачу имени конечного бинарного модуля в NODE_MODULE()
.
Аддоны, определенные с помощью NODE_MODULE()
, не могут быть загружены в нескольких контекстах или нескольких потоках одновременно.
Контекстно-зависимые аддоны¶
Существуют среды, в которых аддоны Node.js могут быть загружены несколько раз в различных контекстах. Например, среда выполнения Electron запускает несколько экземпляров Node.js в одном процессе. Каждый экземпляр будет иметь свой собственный кэш require()
, и поэтому каждому экземпляру потребуется собственный аддон для корректного поведения при загрузке через require()
. Это означает, что аддон должен поддерживать несколько инициализаций.
Контекстно-зависимый аддон может быть построен с помощью макроса NODE_MODULE_INITIALIZER
, который расширяется до имени функции, которую Node.js будет ожидать найти при загрузке аддона. Таким образом, аддон может быть инициализирован, как показано в следующем примере:
1 2 3 4 5 6 7 8 |
|
Другой вариант - использовать макрос NODE_MODULE_INIT()
, который также создаст аддон с учетом контекста. В отличие от NODE_MODULE()
, который используется для построения аддона вокруг заданной функции инициализатора аддона, NODE_MODULE_INIT()
служит для объявления такого инициализатора, за которым следует тело функции.
Следующие три переменные могут быть использованы в теле функции после вызова NODE_MODULE_INIT()
:
Local<Object> exports
,Local<Value> module
, иLocal<Context> context
Выбор в пользу создания контекстно-зависимого аддона влечет за собой ответственность за тщательное управление глобальными статическими данными. Поскольку аддон может быть загружен несколько раз, возможно даже из разных потоков, любые глобальные статические данные, хранящиеся в аддоне, должны быть надлежащим образом защищены и не должны содержать постоянных ссылок на объекты JavaScript. Причина этого заключается в том, что объекты JavaScript действительны только в одном контексте, и при обращении к ним из неправильного контекста или из потока, отличного от того, в котором они были созданы, скорее всего, произойдет сбой.
Контекстно-зависимый аддон может быть структурирован таким образом, чтобы избежать глобальных статических данных, выполнив следующие шаги:
- Определите класс, который будет хранить данные для каждого экземпляра аддона и который имеет статический член вида
1 2 3 |
|
-
Выделите экземпляр этого класса в инициализаторе аддона. Это можно сделать с помощью ключевого слова
new
. -
Вызовите
node::AddEnvironmentCleanupHook()
, передав ему созданный выше экземпляр и указатель наDeleteInstance()
. Это гарантирует, что экземпляр будет удален, когда среда будет снесена. -
Храните экземпляр класса в
v8::External
, и -
Передайте
v8::External
всем методам, открываемым для JavaScript, передав его вv8::FunctionTemplate::New()
илиv8::Function::New()
, которые создают функции JavaScript с поддержкой родного языка. Третий параметрv8::FunctionTemplate::New()
илиv8::Function::New()
принимаетv8::External
и делает его доступным в нативном обратном вызове с помощью методаv8::FunctionCallbackInfo::Data()
.
Это гарантирует, что данные для каждого экземпляра аддона достигнут каждого связывания, которое может быть вызвано из JavaScript. Данные экземпляра аддона также должны быть переданы в любые асинхронные обратные вызовы, которые аддон может создать.
Следующий пример иллюстрирует реализацию контекстно-зависимого аддона:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
|
Поддержка Worker¶
Чтобы загружаться из нескольких окружений Node.js, таких как основной поток и рабочий поток, дополнение должно либо:
- Быть аддоном Node-API, либо
- быть объявлено как контекстно-зависимое с помощью
NODE_MODULE_INIT()
, как описано выше.
Для поддержки потоков Worker
аддонам необходимо очистить все ресурсы, которые они могли выделить, когда такой поток существует. Этого можно достичь с помощью функции AddEnvironmentCleanupHook()
:
1 2 3 |
|
Эта функция добавляет хук, который будет выполняться перед выключением данного экземпляра Node.js. При необходимости такие хуки могут быть удалены до их запуска с помощью RemoveEnvironmentCleanupHook()
, которая имеет ту же сигнатуру. Обратные вызовы выполняются в порядке "последний-первый-выход".
При необходимости существует дополнительная пара перегрузок AddEnvironmentCleanupHook()
и RemoveEnvironmentCleanupHook()
, где крючок очистки принимает функцию обратного вызова. Это можно использовать для отключения асинхронных ресурсов, таких как любые обработчики libuv
, зарегистрированные аддоном.
Следующий addon.cc
использует AddEnvironmentCleanupHook
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
|
Протестируйте на JavaScript, выполнив:
1 2 |
|
Сборка¶
После написания исходного кода его необходимо скомпилировать в бинарный файл addon.node
. Для этого создайте файл binding.gyp
на верхнем уровне проекта, описывающий конфигурацию сборки модуля с помощью JSON-подобного формата. Этот файл используется node-gyp, инструментом, написанным специально для компиляции аддонов Node.js.
1 2 3 4 5 6 7 8 |
|
Версия утилиты node-gyp
поставляется и распространяется с Node.js как часть npm
. Эта версия не предоставляется разработчикам напрямую для использования и предназначена только для поддержки возможности использования команды npm install
для компиляции и установки аддонов. Разработчики, желающие использовать node-gyp
напрямую, могут установить его с помощью команды npm install -g node-gyp
. Дополнительную информацию, включая требования для конкретной платформы, смотрите в node-gyp
инструкции по установке.
После создания файла binding.gyp
используйте команду node-gyp configure
для создания соответствующих файлов сборки проекта для текущей платформы. В результате будет создан либо Makefile
(на платформах Unix), либо vcxproj
(на Windows) в каталоге build/
.
Затем вызовите команду node-gyp build
для создания скомпилированного файла addon.node
. Он будет помещен в каталог build/Release/
.
При использовании npm install
для установки аддона Node.js, npm использует свою собственную версию node-gyp
для выполнения того же набора действий, генерируя скомпилированную версию аддона для платформы пользователя по требованию.
После сборки бинарный аддон можно использовать из Node.js, указав require()
на собранный модуль addon.node
:
1 2 3 4 5 |
|
Поскольку точный путь к скомпилированному бинарному файлу аддона может меняться в зависимости от того, как он был скомпилирован (например, иногда он может находиться в ./build/Debug/
), аддоны могут использовать пакет bindings для загрузки скомпилированного модуля.
Хотя реализация пакета bindings
более сложна в определении местоположения модулей аддонов, по сути, она использует схему try...catch
, подобную следующей:
1 2 3 4 5 |
|
Связывание с библиотеками, включенными в Node.js¶
Node.js использует статически связанные библиотеки, такие как V8, libuv и OpenSSL. Все аддоны должны ссылаться на V8 и могут ссылаться на любые другие зависимости. Обычно для этого достаточно включить соответствующие утверждения #include <...>
(например, #include <v8.h>
), и node-gyp
автоматически найдет соответствующие заголовки. Однако есть несколько предостережений, о которых следует знать:
-
Когда
node-gyp
запускается, он определяет конкретную версию выпуска Node.js и загружает либо полный tarball исходников, либо только заголовки. Если загружается полный исходник, аддоны получат полный доступ к полному набору зависимостей Node.js. Однако если загружены только заголовки Node.js, то будут доступны только символы, экспортируемые Node.js. -
node-gyp
может быть запущен с флагом--nodedir
, указывающим на локальный образ исходного кода Node.js. При использовании этой опции аддон будет иметь доступ к полному набору зависимостей.
Загрузка аддонов с помощью require()
¶
Расширение имени файла скомпилированного бинарного файла аддона - .node
(в отличие от .dll
или .so
). Функция require()
написана для поиска файлов с расширением .node
и инициализации их как динамически подключаемых библиотек.
При вызове require()
расширение .node
обычно можно опустить, и Node.js все равно найдет и инициализирует аддон. Однако есть одна оговорка: Node.js сначала попытается найти и загрузить модули или файлы JavaScript, которые имеют одинаковое базовое имя. Например, если есть файл addon.js
в том же каталоге, что и двоичный файл addon.node
, то require('addon')
отдаст предпочтение файлу addon.js
и загрузит его вместо него.
Нативные абстракции для Node.js¶
Каждый из примеров, проиллюстрированных в этом документе, напрямую использует API Node.js и V8 для реализации аддонов. API V8 может значительно изменяться от одного выпуска V8 к другому (и от одного основного выпуска Node.js к другому). При каждом изменении может потребоваться обновление и перекомпиляция аддонов для продолжения их работы. График выпуска Node.js разработан таким образом, чтобы минимизировать частоту и влияние таких изменений, но Node.js мало что может сделать для обеспечения стабильности API V8.
Native Abstractions for Node.js (или nan
) предоставляет набор инструментов, которые разработчикам аддонов рекомендуется использовать для поддержания совместимости между прошлыми и будущими выпусками V8 и Node.js. Смотрите nan
примеры для иллюстрации того, как его можно использовать.
Node-API¶
Стабильность: 2 – Стабильная
АПИ является удовлетворительным. Совместимость с NPM имеет высший приоритет и не будет нарушена кроме случаев явной необходимости.
Node-API - это API для создания собственных аддонов. Он не зависит от базовой среды выполнения JavaScript (например, V8) и поддерживается как часть самого Node.js. Этот API будет стабилен к бинарному интерфейсу приложения (ABI) во всех версиях Node.js. Он предназначен для того, чтобы изолировать аддоны от изменений в базовом движке JavaScript и позволить модулям, скомпилированным для одной версии, работать на более поздних версиях Node.js без перекомпиляции. Аддоны собираются/пакуются с использованием тех же подходов/инструментов, которые описаны в этом документе (node-gyp и т.д.). Единственное различие заключается в наборе API, которые используются родным кодом. Вместо использования API V8 или Native Abstractions for Node.js используются функции, доступные в Node-API.
Создание и поддержка аддона, пользующегося стабильностью ABI, обеспечиваемой Node-API, сопряжено с определенными соображениями по реализации.
Чтобы использовать Node-API в приведенном выше примере "Hello world", замените содержимое файла hello.cc
следующим. Все остальные инструкции остаются неизменными.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
Доступные функции и способы их использования описаны в C/C++ addons with Node-API.
Примеры аддонов¶
Ниже приведены примеры аддонов, призванные помочь разработчикам начать работу. В примерах используются API V8. Обратитесь к онлайновой V8 reference для справки по различным вызовам V8, а также к Embedder's Guide для объяснения некоторых используемых понятий, таких как дескрипторы, диапазоны, шаблоны функций и т.д.
Каждый из этих примеров использует следующий файл binding.gyp
:
1 2 3 4 5 6 7 8 |
|
В случаях, когда существует более одного файла .cc
, просто добавьте имя дополнительного файла в массив sources
:
1 |
|
Когда файл binding.gyp
готов, примеры аддонов можно настроить и собрать с помощью node-gyp
:
1 |
|
Аргументы функции¶
Аддоны обычно предоставляют объекты и функции, доступ к которым можно получить из JavaScript, запущенного в Node.js. Когда функции вызываются из JavaScript, входные аргументы и возвращаемое значение должны быть отображены в код на C/C++.
Следующий пример иллюстрирует, как читать аргументы функции, переданные из JavaScript, и как возвращать результат:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
|
После компиляции пример аддона может быть востребован и использован из Node.js:
1 2 3 4 |
|
Обратные вызовы¶
В аддонах принято передавать функции JavaScript в функцию C++ и выполнять их оттуда. В следующем примере показано, как вызывать такие обратные вызовы:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
|
В этом примере используется двухаргументная форма Init()
, которая получает полный объект модуля
в качестве второго аргумента. Это позволяет аддону полностью переписать exports
одной функцией вместо того, чтобы добавлять функцию как свойство exports
.
Чтобы проверить это, выполните следующий JavaScript:
1 2 3 4 5 6 7 |
|
В этом примере функция обратного вызова вызывается синхронно.
Фабрика объектов¶
Аддоны могут создавать и возвращать новые объекты внутри функции C++, как показано в следующем примере. Объект создается и возвращается со свойством msg
, которое повторяет строку, переданную в createObject()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
|
Чтобы протестировать его на JavaScript:
1 2 3 4 5 6 7 |
|
Фабрика функций¶
Другой распространенный сценарий - создание функций JavaScript, которые обертывают функции C++, и возвращение этих функций обратно в JavaScript:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
|
Для проверки:
1 2 3 4 5 6 |
|
Обертывание объектов C++¶
Также можно обернуть объекты/классы C++ таким образом, чтобы можно было создавать новые экземпляры с помощью оператора JavaScript new
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
Затем, в myobject.h
, класс-обертка наследуется от node::ObjectWrap
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
|
В файле myobject.cc
реализуйте различные методы, которые должны быть открыты. Ниже метод plusOne()
реализуется путем добавления его в прототип конструктора:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
|
Чтобы собрать этот пример, файл myobject.cc
должен быть добавлен к файлу binding.gyp
:
1 2 3 4 5 6 7 8 |
|
Протестируйте его:
1 2 3 4 5 6 7 8 9 10 |
|
Деструктор для объекта-обертки будет запущен, когда объект будет собран. Для проверки деструктора существуют флаги командной строки, которые можно использовать для принудительной сборки мусора. Эти флаги предоставляются базовым движком V8 JavaScript. Они могут быть изменены или удалены в любое время. Они не документированы Node.js или V8, и их никогда не следует использовать вне тестирования.
При завершении процесса или рабочих потоков деструкторы не вызываются движком JS. Поэтому пользователь несет ответственность за отслеживание этих объектов и их надлежащее уничтожение во избежание утечки ресурсов.
Фабрика обернутых объектов¶
В качестве альтернативы можно использовать паттерн фабрики, чтобы избежать явного создания экземпляров объектов с помощью оператора JavaScript new
:
1 2 3 |
|
Во-первых, метод createObject()
реализован в файле addon.cc
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
В myobject.h
добавлен статический метод NewInstance()
для обработки инстанцирования объекта. Этот метод заменяет использование new
в JavaScript:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
Реализация в myobject.cc
аналогична предыдущему примеру:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
|
И снова, чтобы собрать этот пример, файл myobject.cc
должен быть добавлен в binding.gyp
:
1 2 3 4 5 6 7 8 |
|
Протестируйте его:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
Передача обернутых объектов¶
В дополнение к обертыванию и возврату объектов C++, можно передавать обернутые объекты, разворачивая их с помощью вспомогательной функции Node.js node::ObjectWrap::Unwrap
. В следующих примерах показана функция add()
, которая может принимать два объекта MyObject
в качестве входных аргументов:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
|
В myobject.h
добавлен новый публичный метод, позволяющий получить доступ к приватным значениям после разворачивания объекта.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
Реализация myobject.cc
аналогична предыдущей:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
|
Протестируйте его с помощью:
1 2 3 4 5 6 7 8 9 |
|