[SoC] Final Report: Integrating Node.js with ZOO-services
Momtchil Momtchev
momtchil at momtchev.com
Mon Sep 12 09:20:58 PDT 2022
(this report is available online at
https://github.com/mmomtchev/mmomtchev/blob/master/GSoC-2022/ZOO-Project-mmomtchev-GSoC-2022-Final-Report.md)
ZOO-Project GSoC 2022 Final Report by @mmomtchev
Adding Node.js support for service implementation to be run from
the ZOO-Kernel
Abstract
The ZOO-Project is a solid WPS server able to handle services
implemented in various different programming languages. The
existing|ZOO-Kernel|supports C, C++, and JS implementations with the
SpiderMonkey engine. With this project, the objective was to add support
for NodeJS implementation in the|ZOO-Kernel|.
Mentors: Gérald Fenoy, Aditi Sawant, Rajat Shinde
State of the art
SpiderMonkey, the original JavaScript engine, has a very mature support
for being integrated in C++ projects as a shared library.
Node.js already supports being built as a shared library instead of an
executable. The main user and current maintainer of this feature is the
Electron project (https://www.electronjs.org/). Electron wraps around
Node.js in a rather complex manner, reimplementing some its features. It
does so by directly accessing the Node.js internals and the raw V8 API,
by directly linking to it.
Ubuntu and Debian already provide Node.js and libnode packages. These
are however built according to those distributions' guidelines and are
not updated once the distribution is released. As Node.js is a very fast
moving project, most users use the binaries provided by NodeSource,
which are maintained by developers directly linked to the Node.js team.
These are widely considered to be the reference binaries for each
platform. The notion of a reference binary exists because Node.js
supports native add-ons which are not always compatible between
different Node.js builds. The libnode packages in those distributions do
not allow access to the Node.js internals so these are in fact next to
useless. NodeSource, on the other hand, does not provide prebuilt
libnode packages.
Current libnode
/(The current state of libnode)/
An issue, with 14 hearts, on the Node.js tracker, proposes an
alternative libnode API, built upon the binary stable N-API used for
native addons:https://github.com/nodejs/node/issues/23265.
Current N-API
/(An overview of the excellent and very
successful|N-API|and|node-addon-api|)/
Very early in this project, it became clear that instead of developing
the ZOO-Project support around a very specific Node.js/V8 version, that
was to be included in the source distribution, it was much more future
proof to develop an abstraction layer, and then, in the spirit of the
GSoC program, to contribute it back to the open source community.
N-API Embedding
Node.js provides an excellent stable ABI for creating native addons. It
is a second-generation API - replacing the previous NAN (/Native
Abstractions for Node.js/) - and it is the result of the very
significant experience of the Node.js developers in the field of
distributing binary addons for a JavaScript runtime. It allows a third
party to compile and then to distribute a single binary per supported
platform that will be compatible with all future Node.js versions. This
magic takes place behind the scenes and allows the average user to
simply type|npm install pkg|and to retrieve binary code, in addition to
the JS library, that will simply work on its platform. The ABI has even
full C++ support that avoids any compiler runtime conflicts by being
entirely implemented as C++ templates to be built with the addon. These
templates reduce the C++ API to plain C calls using N-API across the
linking border of the shared library for maximum compatibility. This C++
API is called|node-addon-api|and it is a separate project from Node.js
itself.
This ABI provides stable abstractions for calling and being called from
JS code, decoding JS objects and interacting with the garbage collector
and the different worker threads (V8 isolates) which are not transparent
from the viewpoint of the C++ code. V8 isolates are represented by an
opaque reference, called a Node.js environment, that is passed to every
N-API method. It is this reference to a Node.js environment that is used
to provide an alternative implementation that allows speaking to an
embedded Node.js instance instead of the calling Node.js instance.
New libnode
/(Turning|N-API|and|node-addon-api|inside-out)/
The Platform Environment
The first major element of this libnode implementation is a new
environment type compatible with all the existing methods. This includes
N-API primitives for creation of a new environment that take care of
loading Node.js and V8 and initializing it:
|napi_status napi_create_platform(int argc, char** argv, int exec_argc,
char** exec_argv, char*** errors, int thread_pool_size, napi_platform*
result); napi_status napi_create_environment(napi_platform platform,
char*** errors, const char* main_script, napi_env* result); napi_status
napi_destroy_platform(napi_platform platform, int *exit_code);
napi_status napi_destroy_environment(napi_env env); |
These methods allow the creation of a|napi_env|object - the same type of
object used by Node.js to interact with a native C/C++ addon. It
describes a running V8 isolate with Node.js builtins.
The C++ classes|Napi::Platform|and|Napi::PlatformEnv|, implemented
in|node-addon-api|allow for a cleaner interface when using only C++.
The Asynchronous Execution Model
Node.js has a very peculiar asynchronous execution model that comes from
the Web legacy of JavaScript. This is something that must be taken into
account when embedding JavaScript code - as it expects its functions to
be run as callbacks from the event loop.
In order to provide clean and simple interface for calling C/C++
programs, that event loop has been completely abstracted.
For example, consider this simple JS function:
|function fn() { const r = { result: 'pending' }; setImmediate(() => {
r.result = 'result'; }); return r; } |
It returns an object with an attribute and then schedules a coroutine to
modify this object that will run at some later point. When this function
is called from C++, the calling program will need a method to trigger
the execution of this coroutine which will still be pending at the
moment the function returns.
Two methods allow flushing of the pending callbacks:
|napi_status napi_run_environment(napi_env env); napi_status
napi_await_promise(napi_env env, napi_value promise, napi_value *result); |
The first one spins the event loop until all pending callbacks have been
executed, the second one spins it until the passed|Promise|object has
been resolved (or rejected) and retrieves its value (or error). These
functions allow the execution of the pending JavaScript code in the
context of the currently running C++ thread.
|require|/|import|
At the time when SpiderMonkey was still the leading JS engine, JS did
not really had the notion of packages or code splitting. It is Node.js
that introduced the|require|method for loading of external JS code.
In order to provide a true sense of/linking/the JS code from C/C++, it
was decided to support directly calling|require|and|import|methods from
C/C++ - as this would be the way that JS code would load a package.
These methods - the traditional Node.js|require|and the new dynamic
function-like|import|(the static|import|is part of the language) are in
fact implemented as Node.js builtins and are loaded during the
boostrapping of the JS code.
In order to render them accessible, a new bootstrapping was implemented
- one that exported these methods in the|global|object so that they can
easily be accessed from C/C++.
PPA Packages
In order for the|libnode|to be an (almost) drop-in replacement for the
SpiderMonkey, Ubuntu PPA packages are provided as this is the way that
ZOO-Project used to retrieve and include SpiderMonkey - by using the now
obsolete packages|libmoz|.
These packages are currently provided only for Ubuntu LTS versions -
Bionic (18.04), Focal (20.04) and Jammy (22.04) and they exist both for
the Node.js 16.x (LTS) and 18.x (Current) branches:
https://launchpad.net/~mmomtchev
* |ppa:mmomtchev/libnode|
* |ppa:mmomtchev/libnode-18.x|
Multi-threading
In order to simplify the API and to avoid any V8 locking issues, there
is one very simple rule that is to be followed:
*After creating a new platform environment, only the thread that created
it is allowed to access it.*
This greatly simplifies the code and does not seem to impose any
significant additional restrictions.
Future of N-API Embedding
Embedding JS plugins in C/C++ software is a very common need. JS is
currently the fastest growing general purpose language in the world and
it is the first language for many young developers. C/C++ on the other
hand still has the largest installed base and it is not going away for
at least several more generations. Even if it is likely to be challenged
in this role during the coming decade, it is still the first language
for implementing complex system software such as operating systems,
database engines, network processing or interpreting higher-level
languages such as JS or Python.
N-API embedding|libnode|has a very great potential because it allows for
very easy support of Node.js plugins.
Short-term Plans
*
Provide Debian packages
*
Provide macOS and Windows binaries
Long-term Plans
*
Push forward with the merging of the
PRhttps://github.com/nodejs/node/pull/43542in the main Node.js tree
The whole point of N-API embedding, besides being easier for the
end-user, is that it is binary compatible with future Node.js
versions. In order for|libnode|to be truly future-proof, it has to
be merged at some point with the main Node.js tree.
Besides the sheer amount of work and testing required for merging a
large PR in a project of Node.js' stature, there is one particularly
problematic point -|libnode|currently carries a monkey-patch
for|libuv|that fixes a complex issue that never happens with the use
that Node.js makes of it:https://github.com/libuv/libuv/issues/3686.
This means that merging|libnode|will require simultaneous pushes to
both Node.js and libuv and an upgrade of the libuv version in Node.js.
In order to mitigate risks when it comes to ZOO-Project, Node.js
support in ZOO-Project has been implemented without using the very
practical shortcut|napi_await_promise|which requires modifying|libuv|.
Documentation
illustration
/(An example for calling|axios.get|from C++)/
*The program resulting from compiling this code will be a very small
binary executable, that will be dynamically linked with the 80Mb (on
Linux)|libnode.so|shared library and it will require the presence of
a|node_modules/axios|and all of its dependencies in the same directory
as the executable.*
Using|libnode|to implement support of JS plugins in C++ code
The full documentation for using N-API embedding|libnode|has been
integrated in the Node.js documentation in the|napi-libnode|branch:
https://github.com/mmomtchev/node/blob/napi-libnode/doc/api/embedding.mdhttps://github.com/mmomtchev/node/blob/napi-libnode/doc/api/n-api.md
Also, the|libnode|repository contains some examples allowing to
quick-start a new project:
https://github.com/mmomtchev/libnode/tree/main/examples
One should probably start by the*/Calling|axios|from C++/*example:
https://github.com/mmomtchev/libnode/blob/main/examples/axios-example.cc
Using Node.js JavaScript to implement a WPS service in ZOO-Project
The documentation geared towards WPS service authors has been integrated
in the ZOO-Project documentation and it is available in|nodejs|branch:
https://github.com/mmomtchev/ZOO-Project/blob/nodejs/docs/services/howtos.rst#javascript-node-js
An example Node.js service is available as PR for the examples
repository of ZOO-Project:
https://github.com/ZOO-Project/examples/pull/1
The Code
All the work done in Node.js is available in my Node.js forks:
* v16.x branchhttps://github.com/mmomtchev/node/tree/napi-libnode-v16.x
* v18.x branchhttps://github.com/mmomtchev/node/tree/napi-libnode-v18.x
* main branchhttps://github.com/mmomtchev/node/tree/napi-libnode
The PR for merging N-API embedding to the main branch of
Node.js:https://github.com/nodejs/node/pull/43542
(currently unmergeable until libuv is modified accordingly)
The Ubuntu PPA packages build system, along with instructions for
installing and using|libnode|are available in a dedicated repo:
https://github.com/mmomtchev/libnode
All the work done in ZOO-Project itself is available in the|nodejs|branch:
https://github.com/mmomtchev/ZOO-Project/tree/nodejs
The currently pending PR is available here:
https://github.com/ZOO-Project/ZOO-Project/pull/30
The slides from the|libnode|presentation atFOSS4G 2022
<https://2022.foss4g.org/>can be found here:
* (sources)https://github.com/mmomtchev/zoo-slides/blob/gsoc2022-nodejs/FOSS4G-2022/GSoC/Node.js/index.html
* (preview)https://htmlpreview.github.io/?https://github.com/mmomtchev/zoo-slides/blob/gsoc2022-nodejs/FOSS4G-2022/GSoC/Node.js/index.html#/
--
Cordialement,
Momtchil Momtchev <momtchil at momtchev.com>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.osgeo.org/pipermail/soc/attachments/20220912/ea20c6ed/attachment-0001.htm>
More information about the SoC
mailing list