Even if we have time and experience programming in JS, there are times when the performance of JavaScript is not enough, so you have to rely more on native Node.js modules. Read on to find out how to do this.
While native extensions are definitely not a beginner's topic, I would recommend this article to all Node.js developers to gain a little insight into how they work.
Common use cases for native Node.js modules
Knowledge of native modules is useful when you are adding a native extension as a dependency, which you could have already done!
Just do a little look at the list of some popular modules that use native extensions. You are using at least one of them, or am I wrong?
- https://github.com/wadey/node-microtime
- https://github.com/node-inspector
- https://github.com/node-inspector/v8-profiler
- http://www.nodegit.org/
There are a few reasons why one might consider module creation Node.js native speakers, including but not limited to:
- Performance Critical Applications: Let's be honest, Node.js is great for performing asynchronous input and output operations, but when it comes to true number computation, it's not such a good choice.
- Connection to the API lower level, for example: operating system.
- Bridging the C or C ++ Libraries and Node.js
What are the native modules?
Node.js plugins are dynamically linked shared objects written in C or C ++, which can be loaded into Node.js using the function require ()
, and are used as if they were an ordinary Node.js module.
This means that (if done correctly) the quirks of C / C ++ can be hidden from the module consumer. What you will see instead is that your module is a Node.js module, as if there were
Node.js runs on the V8 JavaScript engine, which is a C program on its own. We can write code that interacts directly with this C program in your own language, which is great because we can avoid a lot of expensive serialization and communication costs.
Also, in an entry of Blog Previously we learned about the cost of the Node.js garbage collector. Although garbage collection can be avoided entirely if you decide to manage memory yourself (because C and C++ don't have a concept of GC), it will create memory problems much more easily.
Writing native extensions requires knowledge of one or more of the following topics:
- Libuv
- V8
- Node.js internals
They all have excellent documentation. If you are entering this field, I recommend you read them.
Without further ado, let's start with the strong theme of this post:
Prerequisites
- For Linux operating system:
- Use python (I recommend using v2.7, since v3.xx is not supported)
- Make.
- Use a suitable C or C ++ compilation toolchain, such as GCC
- For Mac operating system:
- Have Xcode installed: make sure you not only install it, but start it at least once and accept its terms and conditions; otherwise it won't work!
- For Windows operating system:
- Run
cmd.exe
as administrator and enter the commandnpm install --global --production windows-build-tools
, which will install everything for you. - Another option is to install Visual Studio: (it has all the C / C ++ build tools preconfigured)
- Or use the Linux subsystem provided by the latest Windows build. With that, follow the LINUX instructions above.
- Run
Creating our native Node.js extension
We are going to create our first file for the native extension. We can use the extension .DC which means it's C with classes, or the extension .cpp which is the default for C++. The Style Guide for Google recommends .DC, so for this tutorial I'll stick with it.
At this point we are going to see the complete file and then explain it line by line.
#include const int maxValue = 10; int numberOfCalls = 0; void WhoAmI (const v8 :: FunctionCallbackInfo & args) {v8 :: Isolate * isolate = args.GetIsolate (); auto message = v8 :: String :: NewFromUtf8 (isolate, "I am Node Hero!"); args.GetReturnValue (). Set (message); } void Increment (const v8 :: FunctionCallbackInfo & args) {v8 :: Isolate * isolate = args.GetIsolate (); if (! args [0] -> IsNumber ()) {isolate-> ThrowException (v8 :: Exception :: TypeError (v8 :: String :: NewFromUtf8 (isolate, "The argument must be a number"))); return; } double argsValue = args [0] -> NumberValue (); if (numberOfCalls + argsValue> maxValue) {isolate-> ThrowException (v8 :: Exception :: Error (v8 :: String :: NewFromUtf8 (isolate, "The counter went through the roof!"))); return; } numberOfCalls + = argsValue; auto currentNumberOfCalls = v8 :: Number :: New (isolate, static_cast (numberOfCalls)); args.GetReturnValue (). Set (currentNumberOfCalls); } void Initialize (v8 :: Local exports) {NODE_SET_METHOD (exports, "whoami", WhoAmI); NODE_SET_METHOD (exports, "increment", Increment); } NODE_MODULE (module_name, Initialize)
Now we are going to see the file line by line
#include
The include
in C ++ it's like require ()
in JavaScript. It will extract everything from the given file, but instead of linking it directly to the source, in C ++ we have the concept of header files.
We can declare the exact interface in header files without implementation and then we can include the implementations by their header file. The C++ linker will take care of linking these two together. Think of it as a documentation file that describes your contents, which can be reused from your code.
void WhoAmI(const v8::FunctionCallbackInfo<v8::value>& args) { v8::Isolate* isolate = args. GetIsolate(); auto message = v8::String::NewFromUtf8(isolate, "I am Node Hero!"); args. GetReturnValue(). Set(message); } [php] Because this will be a native extension, the v8 namespace is available for use. Note the notation <strong>v8 ::</strong>, which is used to access the v8 interface. If you do not want to include <strong>v8 ::</strong> Before using any of the types provided by v8, you can add it using <strong>using</strong> <strong>v8;</strong> to the top of the file. You can then omit all the specifiers <strong>v8 ::</strong> namespace their types, but this can introduce name collisions into your code, so be careful when using them. To be 100% clear, I'll use the notation <strong>v8 ::</strong> for all v8 types in the code shown. In our example code, we have access to the arguments with which the function was called (from JavaScript), via the object <strong>args</strong> which also provides us with all the information related to the call. With <strong>v8::Isolate*</strong> we are getting access to the current JavaScript scope for our function. Scopes work just like in JavaScript: we can assign variables and bind them to the lifetime of that specific code. We don't have to worry about de-allocating these chunks of memory, because we allocate them just like we would in JavaScript, and the garbage collector will take care of them automatically. [php] function () { var a = 1; } // scope
Via args.GetReturnValue () we access the return value of our function. We can set it to whatever we want as long as be from space v8 :: of names.
C ++ has built-in types for storing integers and strings, but JavaScript only understands its own v8 :: object types. As long as we are in the realm of the C ++ world, we can use the ones that are built into C ++, but when dealing with JavaScript objects and interoperability with JavaScript code, we have to transform the C ++ types into other than they understand each other. by the JavaScript context. These are the types that are exposed in v8 :: namespace as v8 :: Stringo v8 :: Object.
void WhoAmI (const v8 :: FunctionCallbackInfo & args) {v8 :: Isolate * isolate = args.GetIsolate (); auto message = v8 :: String :: NewFromUtf8 (isolate, "I am Node Hero!"); args.GetReturnValue (). Set (message); }
Let's look at the second method in our file that increments a counter by a supplied argument to an upper bound of 10.
This function also accepts a JavaScript parameter. When you are accepting JavaScript parameters, you have to be careful because they are loosely written objects. (You are probably used to this in JavaScript by now.)
Array array contains v8 :: Objects, so they are all JavaScript objects, but be careful with these, because in this context we can never be sure what they may contain. We have to explicitly check the types of these objects. Fortunately, there are helper methods that are added to these classes to determine their type before the conversion of types.
To maintain compatibility with existing JavaScript code, we should throw an error if the type of arguments is wrong. To throw a type error, we have to create an error object with the constructor
v8 :: Exception :: TypeError (). The next block will launch a TypeError if the first argument is not a number.
if (! args [0] -> IsNumber ()) {isolate-> ThrowException (v8 :: Exception :: TypeError (v8 :: String :: NewFromUtf8 (isolate, "The argument must be a number"))); return; }
In JavaScript that snippet would look like:
If (typeof arguments [0]! == 'number') {throw new TypeError ('The argument must be a number')}