Эта статья была добавлена ​​в коллекцию Node.js Габриэлем Шульхофом.

Node.js предоставляет широкий спектр чрезвычайно полезных API-интерфейсов для выполнения работы приложения с помощью встроенных модулей, таких как fs, http, net и многих других. Все эти модули выполняют собственный код при вызове из JavaScript. Так они обеспечивают свою функциональность. Собственный код C / C ++ для этих модулей компилируется и поставляется вместе с Node.js.

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

Эти собственные надстройки доступны через npm, как и пакеты чистого JavaScript. Примеры включают serialport, farmhash, gRPC и многие другие. Многие приложения зависят от таких собственных надстроек, часто косвенно, в результате их прямых зависимостей, в свою очередь, в зависимости от них.

Итак, нативные надстройки были частью Node.js с самого начала. Итак, есть модули JS 😉 Итак, что это значит, что они теперь близки к «нормальному»? Почему только сейчас?

Причина в том, что собственные надстройки повлекли за собой накладные расходы на обслуживание, которых никогда не было у модулей JS. Такие накладные расходы ложатся дополнительным бременем на разработчиков собственных надстроек, а также на тех, кто поддерживает приложения, чьи приложения зависят от собственных надстроек. Кроме того, нативным надстройкам не хватало определенных фундаментальных возможностей, которые всегда были у модулей JS. Тем не менее, в последних версиях Node.js появились функции, которые устраняют многие из дополнительных затрат на обслуживание, с которыми до сих пор сталкивались разработчики собственных надстроек по сравнению с разработчиками пакетов, содержащих только JavaScript.

Давайте сначала посмотрим на сходство между собственными надстройками и модулями JS. Оба могут быть require()-ed:

// A JS module can be loaded from a package:
const jsModulePackage = require('my-js-module');
// It can also be loaded from a specific file:
const jsModuleFile = require('./my-js-module.js');
// A native add-on can also be loaded from a file:
const nativeAddonFile = require('./build/Release/my-native-add-on.node');

Оба возвращают объект JavaScript, содержащий полезные функции, которые в противном случае были бы полностью недоступны или реализация которых в противном случае потребовала бы много времени.

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

1. Компиляция

В этом всегда будет принципиальная разница между модулями JavaScript и собственными надстройками. Процесс, посредством которого исходный код, написанный на C или C ++ (или даже на другом языке, таком как Rust или Go), объединяется в собственное дополнение, всегда будет дополнительным шагом в дополнение к публикации пакета npm, который пользователи могут использовать. Фактически, если сопровождающий не потратит время на предоставление предварительно собранных пакетов с помощью таких инструментов, как node-pre-gyp, prebuildify или prebuild, то те, кто будет зависеть от нативного надстройки должны установить компилятор и построить цепочку инструментов, чтобы позволить встроенному пакету надстройки построить в их системе, прежде чем они смогут запустить свое приложение. Тем не менее, пакеты npm, предоставляющие предварительно созданные нативные надстройки, могут создать впечатление, будто нативные надстройки могут быть установлены так же легко, как и модуль JavaScript.

2. Независимость от платформы и архитектуры

Один .js файл определяет один модуль JavaScript, потому что по сути это не что иное, как простой текст, который считывается из файловой системы с помощью Node.js и затем интерпретируется. Напротив, собственный аддон - это двоичный файл, содержащий машинный код. Таким образом, один двоичный файл может обслуживать только определенный тип машины, на которой запущен определенный тип операционной системы. Например, один двоичный файл может обслуживать только Windows, работающую на x86 (Intel или AMD). Другой двоичный файл необходим для OSX на x86, и еще один необходим для Linux на x86 и так далее. Инструменты сопровождающих надстроек, такие как node-pre-gyp и prebuild, также могут восполнить этот пробел. Они позволяют разработчикам создавать двоичный файл для каждой комбинации версий платформа / архитектура / Node.js, которую они хотят поддерживать, чтобы во время установки пакета сценарий установки npm мог выбрать соответствующий двоичный файл, загрузить его и передать его узлу. .js для загрузки и выполнения.

3. Независимость от версии Node.js

После написания модулей JavaScript будут работать с той версией Node.js, для которой они были написаны, а также будут работать без переустановки во всех последующих версиях Node.js. Ошибки могут возникнуть только в том случае, если API-интерфейсы JavaScript, используемые модулем, изменены несовместимым, «ломающим» образом. Это может случиться, но команда Node.js стремится поддерживать стабильность всех интерфейсов JavaScript. Критические изменения вводятся только тогда, когда для этого есть веские причины и когда нет альтернативного решения, которое считается адекватным.

Напротив, собственные надстройки привязаны к той версии Node.js, для которой они были написаны. Для данной платформы и архитектуры для версии 8.x Node.js должен быть предоставлен другой двоичный файл, чем для версии 10.x. Причина этого в том, что собственный API, предоставляемый Node.js разработчикам собственных надстроек, изменяется от одной основной версии к другой таким образом, что собственное дополнение больше не загружается или, в худшем случае, не загружается. загрузка, но сбой, казалось бы, необъяснимо. Чтобы сэкономить много часов поиска, казалось бы, необъяснимых сбоев, была добавлена ​​простая схема управления версиями, основанная на единственном целочисленном значении, при этом версия Node.js, на основе которой было создано надстройка, записывается в надстройке во время компиляции и проверяется. при загрузке дополнения. Если значение не соответствует значению, передаваемому Node.js, он не сможет загрузить надстройку, создав сообщение, знакомое многим:

Error: The module '/home/user/node_hello_world/build/Release/hello.node'
was compiled against a different Node.js version using
NODE_MODULE_VERSION 57. This version of Node.js requires
NODE_MODULE_VERSION 64. Please try re-compiling or re-installing
the module (for instance, using `npm rebuild` or `npm install`).
    at Object.Module._extensions..node (internal/modules/cjs/loader.js:718:18)
    at Module.load (internal/modules/cjs/loader.js:599:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:538:12)
    at Function.Module._load (internal/modules/cjs/loader.js:530:3)
    at Module.require (internal/modules/cjs/loader.js:637:17)
    at require (internal/modules/cjs/helpers.js:22:18)
    at bindings (/home/user/node_hello_world/node_modules/bindings/bindings.js:76:44)
    at Object.<anonymous> (/home/user/node_hello_world/hello.js:1:94)
    at Module._compile (internal/modules/cjs/loader.js:689:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:700:10)

Таких ошибок также можно избежать с помощью утилит предварительной сборки, поскольку они позволяют разработчикам дополнений распространять двоичные файлы для каждой версии Node.js, которую они хотят поддерживать. Однако это возлагает еще большую нагрузку на разработчиков надстроек, а именно перестраивать двоичные файлы, которые они предоставляют при каждом выпуске Node.js. Это также увеличивает количество двоичных файлов, которые им необходимо поддерживать, чтобы поддерживать большое количество комбинаций архитектуры / платформы / версии. Кроме того, он не позволяет разработчикам приложений просто добавить новую версию Node.js в свою производственную среду. Действительно, им необходимо переустановить свое приложение, даже если его функциональность не изменилась.

N-API был введен для решения проблемы двоичной совместимости собственных надстроек с версиями Node.js, на основе которых они были созданы , а также со всеми последующими версиями. Эта двоичная совместимость означает не только то, что исходный код не нужно трогать для сборки с более поздней версией Node.js, но также и то, что двоичные файлы, созданные для более ранней версии Node.js, будут продолжать связываться со всеми последующими и корректно работать с ними. версии Node.js. Его конечная цель - устранить проблемы, связанные с поломкой надстроек при выходе новой версии Node.js.

N-API абстрагирует основной движок JavaScript и API-интерфейсы Node.js, представляя их разработчикам собственных надстроек как набор API-интерфейсов, устойчивых к ABI. Стабильность N-API обусловлена ​​тем фактом, что все API-интерфейсы являются API-интерфейсами C, а не C ++ API, и обязательством со стороны сообщества разработчиков ядра Node.js избегать несовместимого изменения интерфейса.

N-API исключает одну из трех переменных, вызывающих рост числа двоичных файлов, необходимых для предоставления единственной собственной надстройки Node.js: целочисленное значение, представляющее версию Node.js, с которой двоичный файл предназначен для работы (NODE_MODULE_VERSION).

Поскольку N-API предоставляет независимый от движка JavaScript интерфейс для языковых функций, его доступность позволяет переносить другие движки JavaScript в Node.js. Проект node-chakracore - это пример версии Node.js с другим движком JavaScript под капотом.

N-API также может служить интерфейсом для движка JavaScript вне Node.js. Проект ShadowNode находится в процессе реализации N-API с использованием движка JavaScript JerryScript в качестве серверной части. Такие усилия позволяют предоставлять собственные надстройки для различных сред, таких как устройства IoT с ограничениями, с использованием единой кодовой базы.

4. Множественная загрузка

Появление рабочих потоков в Node.js и даже более ранних модулях, таких как vm, позволяет загружать модули несколько раз. Это легко сделать с модулями JavaScript, потому что Node.js просто перечитывает и повторно интерпретирует файл, определяющий модуль JavaScript, и результирующий объект находится в отдельном контексте, без непреднамеренного обмена каким-либо состоянием с его ранее загруженной инкарнацией. Напротив, только недавно стало возможным загружать нативные надстройки более одного раза. Причина этого - архитектурная. В частности, это связано с тем, что механизм загрузки нативной надстройки был следующим:

  1. Node.js вычисляет абсолютный путь к собственному надстройке.
  2. Node.js вызывает метод process.dlopen(), передавая ранее вычисленный абсолютный путь.
  3. На нативной стороне Node.js вызывает dlopen(3) или uv_dlopen() в Windows.
  4. Надстройка загружена и выполняет так называемую функцию конструктора DSO как часть процесса загрузки. Функция передает указатель на структуру типа node::nm_module в Node.js.
  5. Член структуры node::node_module::nm_register_func или node::node_module::nm_context_register_func содержит указатель на функцию, которая отвечает за заполнение объекта, который станет результатом загрузки модуля (module.exports).

Проблема с функциями конструктора DSO заключается в том, что они запускаются только при первой загрузке надстройки в память. В последующих случаях dlopen(3) или uv_dlopen() выполнит короткое замыкание, вернув ссылку на уже загруженное дополнение, и функция не будет запущена. Таким образом, последующие попытки загрузить надстройку терпят неудачу.

Решение этой проблемы было введено в 3828fc62. Помимо использования конструктора DSO, в качестве запасного варианта Node.js теперь ищет хорошо известный символ, экспортируемый модулем: node_register_module_v<number>, где <number> - целое число, представляющее версию надстройки. Это происходит только в том случае, если конструктор DSO не запускается. Хорошо известные символы - это адрес функции инициализации модуля, который также хранится в node::node_module::nm_context_register_func.

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

Модули N-API также можно загружать несколько раз. У них есть собственный хорошо известный символ, не зависящий от версии Node.js, который Node.js пытается получить в качестве резервной копии.

5. Безопасность нитей

Поскольку модули JavaScript являются самодостаточными, даже с учетом их собственной области видимости во время загрузки модуля с помощью Node.js, они не вызывают проблем с безопасностью потоков. Напротив, собственные надстройки должны избегать глобального состояния, чтобы считаться потокобезопасными. Это требует четкого проектного решения со стороны сопровождающего. К счастью, данные для каждого экземпляра надстройки можно выделить в куче во время инициализации модуля и передать каждой собственной привязке, доступной из надстройки, как показано в примере надстройки с учетом контекста. Это относится как к надстройкам N-API, так и к надстройкам V8.

6. Разгрузка модуля

Когда рабочий поток завершается, все модули JavaScript, которые были загружены в течение его жизненного цикла, просто отбрасываются. Напротив, выгрузка собственных надстроек довольно рискованна, если выполняется неправильно, потому что привязки, которые они предоставляют для JavaScript, указывают обратно в области памяти, занятые надстройкой. Если надстройка выгружается до того, как среда должным образом очищается, возможно, что Node.js попытается выполнить привязки, которые больше не загружаются. Это приведет к сбою.

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

Заключение

Хотя необходимость рендеринга нативной надстройки в виде нескольких двоичных файлов, по одному для каждой платформы и архитектуры, которую она поддерживает, не может быть устранена, с недавними улучшениями Node.js мы можем считать, что нативные надстройки сделали значительный шаг к тому, чтобы быть « наравне »с модулями JavaScript:

Картина, которая складывается для разработчиков собственных надстроек, заключается в том, что теперь у них есть инструменты, позволяющие предоставить разработчикам приложений собственные пакеты надстроек, которые

  • не нужно повторно публиковать при выпуске новой версии Node.js
  • Сопровождающие приложения не должны переустанавливать заново только потому, что они хотят разместить новую версию Node.js в своей производственной среде,
  • можно безопасно использовать в многопоточной среде выполнения JavaScript.

Со стороны разработчиков собственных надстроек требуется приложить усилия, чтобы использовать эти новые инструменты, однако выгода состоит в том, что в результате таких усилий им не придется повторно публиковать свои пакеты в ответ на новый узел. Релиз .js. Для сопровождающих приложений доступность пакетов, использующих эти новые инструменты, означает, что, как только все их зависимости будут удовлетворены встроенными надстройками, включенными с помощью этих инструментов, они также смогут избежать повторной сборки / повторного развертывания в ответ на новый узел. Релизы .js.

В настоящее время предпринимаются усилия по перемещению экосистемы собственных надстроек в такие стандартные пакеты. Пожалуйста, подумайте о присоединении к нашим усилиям!

Спасибо всем, кто способствовал этому тексту!