• Using Buffers to share data between node.js and C++
  • By Scott Frees
  • The Nuggets translation Project
  • Translator: Jiang Haichao
  • Proofreader: Xiong Xianren, Lei Guo

One of the benefits of developing with node.js is that you can switch seamlessly between JavaScript and native C++ code – thanks to V8’s extended API. The ability to move from JavaScript to C++ is sometimes driven by processing speed, but more often than not we already have C++ code and we want to call it directly in JavaScript.

We can classify extensions to different use cases on (at least) two axes – (1) runtime of C++ code, and (2) data traffic between C++ and JavaScript.

Most documentation discussing C++ extensions to node.js focuses on the difference between the left and right quadrants. If you are in the left quadrant (short processing time), your extension is likely to be synchronous – meaning the C++ code runs directly in the node.js event loop when called.

“#nodejs allows us to seamlessly switch between #javascript and native C++ code “via @risingstack

In this scenario, the extension function blocks and waits for a return value, meaning that other operations cannot take place at the same time. In the right quadrant, add-ons are almost certainly designed in asynchronous mode. In an asynchronous extension function, the JavaScript calling function returns immediately. The calling code passes a callback to the extension function, which works on a separate worker thread. Because extension functions do not block, deadlocks in node.js event loops are avoided.

The differences between the top and bottom quadrants are often overlooked, but they are just as important.

V8 vs. C++ memory and data

If you don’t know how to write a native attachment, the first thing you need to understand is the difference between V8 data (available via C++ attachments) and normal C++ memory allocation.

When we say “owned by V8”, we mean the storage unit that holds JavaScript data.

These storage units are accessible through V8’s C++ API, but they are not normal C++ variables because they can only be accessed in restricted ways. When your extension can be limited to using only V8 data, it is more likely that it will also create its own variables in plain C++ code. These variables can be stack or heap variables and are completely independent of V8.

In JavaScript, primitive types (numbers, strings, booleans, etc.) are immutable, and a C++ extension cannot change the storage unit attached to the primitive type. These primitive JavaScript variables can be reassigned to new storage units created by C++ – but this means that changing the data will result in new memory allocation.

In the upper quadrant (small data transfers), this is no big deal. If you’re designing an add-on that doesn’t require frequent data exchanges, the overhead of all the new memory allocations might not be that great. As scaling gets closer to the lower quadrant, the allocation/copy overhead starts to be staggering.

On the one hand, this increases the maximum memory usage, and on the other, it degrades performance.

The time it takes to copy all the data between JavaScript(V8 storage unit) and C++ (return) usually sacrifices the performance bonus of running C++ in the first place! For extension applications in the lower left quadrant (low processing, high data utilization scenarios), the delay in data copying will direct your extension references to the right quadrant – forcing you to consider asynchronous design.

Node.js Buffer comes to the rescue

There are two related issues here.

  1. When using synchronous extensions, unless we don’t change/produce data, it can take a lot of time to move data between V8 storage units and old simple C++ variables – time consuming.
  2. When using asynchronous extensions, ideally we should minimize the time for event polling. This is the problem – due to V8’s multithreading limitations, we have to copy data in the event polling thread.

There is a feature in Node.js that is often overlooked to help with scaling – buffers. The official documentation for Nodes.js is here.

An instance of the Buffer class is similar to an integer array, but corresponds to V8 with a fixed out-of-heap size and raw memory allocation.

Isn’t that what we’ve always wanted – the data in Buffer is not stored in V8 storage cells, not subject to V8’s multi-threading rules. This means that a C++ worker thread started by asynchronous extension can interact with Buffer.

How to access Buffer in C++

When building node.js extensions, it’s best to start them using NAN (Node.js native abstraction) apis rather than directly using V8 apis – which can be a moving target. There are many tutorials online that start with NAN extensions – including examples from the NAN code base itself. I also write a lot of tutorials, which I hide in my ebook.

First, let’s look at how the extender accesses the Buffer that JavaScript sends to it. We will launch a simple JS program and introduce the extensions we will create later.

    'use strict';  

    //Introduce the extensions you will create later
    const addon = require('./build/Release/buffer_example');

    //Allocate memory outside of V8 with default ASCII "ABC"
    const buffer = Buffer.from("ABC");

    //Sync, +13 per character rotation
    addon.rotate(buffer, buffer.length.13);

    console.log(buffer.toString('ascii'));Copy the code

After an ASCII rotation of 13 for “ABC”, the expected output is “NOP”. Check out the extension! It consists of three files (all in the same directory for convenience).

// binding.gyp
{
  "targets": [
    {
        "target_name": "buffer_example",
        "sources": [ "buffer_example.cpp" ],
        "include_dirs" : ["<!(node -e \"require('nan')\")"]
    }
  ]
}

Copy the code
//package.json
{
  "name": "buffer_example"."version": "0.0.1"."private": true."gypfile": true."scripts": {
    "start": "node index.js"
  },
  "dependencies": {
      "nan": "*"}}Copy the code

// buffer_example.cpp
#include <nan.h>
using namespace Nan;  
using namespace v8;

NAN_METHOD(rotate) {  
    char* buffer = (char*) node::Buffer::Data(info[0]->ToObject());
    unsigned int size = info[1]->Uint32Value();
    unsigned int rot = info[2]->Uint32Value();

    for(unsigned int i = 0; i < size; i++ ) {
        buffer[i] += rot;
    }   
}

NAN_MODULE_INIT(Init) {  
   Nan::Set(target, New<String>("rotate").ToLocalChecked(),
        GetFunction(New<FunctionTemplate>(rotate)).ToLocalChecked());
}

NODE_MODULE(buffer_example, Init)
Copy the code

The most interesting file is buffer_example.cpp. Note that we use the node:Buffer Data method to convert the first argument passed to the extension into a character array. Now we can manipulate arrays in any way we see fit. In this case, we just performed an ASCII rotation of the text. Note that there is no return value; the associated memory of Buffer has been modified.

Build the extension with NPM Install. Package. json tells NPM to download NAN and build the extension using the binding.gyp file. Running index.js returns the expected “NOP” output.

We can also create new buffers in the extension. Modify the rotate function to add input and return a string buffer generated by decreasing the corresponding value.

NAN_METHOD(rotate) {  
    char* buffer = (char*) node::Buffer::Data(info[0]->ToObject());
    unsigned int size = info[1]->Uint32Value();
    unsigned int rot = info[2]->Uint32Value();

    char * retval = new char[size];
    for(unsigned int i = 0; i < size; i++ ) {
        retval[i] = buffer[i] - rot;
        buffer[i] += rot;
    }   

   info.GetReturnValue().Set(Nan::NewBuffer(retval, size).ToLocalChecked());
}
Copy the code
var result = addon.rotate(buffer, buffer.length.13);

console.log(buffer.toString('ascii'));  
console.log(result.toString('ascii'));Copy the code

The resulting buffer is now ‘456’. Note the use of NAN’s NewBuffer method, which wraps the dynamic allocation of retval data in Node buffers. Doing so transfers the use of this memory to Node.js, so the memory associated with Retval will be redeclared (by calling free) when buffer passes JavaScript scope. More on this later – we don’t want to restate it all the time, after all.

You can find more information about how NAN handles buffers here.

Set the extension

We are going to create the following directory structure, including from github.com/lvandeve/lo… Lodepng. h and lodepng.cpp.

    /png2bmp
     |
     |--- binding.gyp
     |--- package.json
     |--- png2bmp.cpp  # the add-on
     |--- index.js     # program to test the add-on
     |--- sample.png   # input (will be converted to bmp)
     |--- lodepng.h    # from lodepng distribution
     |--- lodepng.cpp  # From loadpng distribution
Copy the code

Lodepng.cpp contains all the code necessary to do image processing, and I won’t go into the details of how it works. In addition, the Lodepng package contains simple code that allows you to specify transformations between PNP and BMP. I made a few minor changes to it and put it in the extension source file png2bmp.cpp, which we’ll see in a moment.

Let’s take a look at the JavaScript program before extending further:

    'use strict';  
    const fs = require('fs');  
    const path = require('path');  
    const png2bmp = require('./build/Release/png2bmp');

    const png_file = process.argv[2];  
    const bmp_file = path.basename(png_file, '.png') + ".bmp";  
    const png_buffer = fs.readFileSync(png_file);

    const bmp_buffer = png2bmp.getBMP(png_buffer, png_buffer.length);  
    fs.writeFileSync(bmp_file, bmp_buffer);Copy the code

This program passes in the filename of a PNG image as a command line argument. The getBMP extension function is called, which accepts the buffer containing the PNG file and its length. This extension is synchronous, and we’ll see the asynchronous version later.

This is the package.json file with the NPM start command set to call the index.js program and pass sample. PNG command line arguments. This is an ordinary picture.

    {
      "name": "png2bmp"."version": "0.0.1"."private": true."gypfile": true."scripts": {
        "start": "node index.js sample.png"
      },
      "dependencies": {
          "nan": "*"}}Copy the code

This is the binding.gyp file – based on the standard file some compiler flags are set to compile lodepng. Necessary references to NAN are also included.

{
  "targets": [
    {
      "target_name": "png2bmp",
      "sources": [ "png2bmp.cpp", "lodepng.cpp" ],
      "cflags": ["-Wall", "-Wextra", "-pedantic", "-ansi", "-O3"],
      "include_dirs" : ["<!(node -e \"require('nan')\")"]
    }
  ]
}
Copy the code

Png2bmp. CPP mainly includes V8/NAN code. However, it also has a general image processing function, do_convert, adopted from the LOdepng PNG to BMP example.

The encodeBMP function accepts vector

for input data (in PNG format) and vector

for output data (in BMP format).

That’s all the code for these two functions. The details are not important to understand the extended Buffer object; they are included for program integrity. The extender entry calls do_convert.

~~~~~~~~<del>{#binding-hello .cpp} /* ALL LodePNG code in this file is adapted from lodepng's examples, found at the following URL: https://github.com/lvandeve/lodepng/blob/ master/examples/example_bmp2png.cpp' */void encodeBMP(std::vector<unsigned char>& bmp, const unsigned char* image, int w, int h) { //3bytes per pixel used for both input and output. int inputChannels = 3; int outputChannels = 3; //bytes 0-13bmp.push_back('B'); bmp.push_back('M'); //0: bfType bmp.push_back(0); bmp.push_back(0); bmp.push_back(0); bmp.push_back(0); bmp.push_back(0); bmp.push_back(0); //6: bfReserved1 bmp.push_back(0); bmp.push_back(0); //8: bfReserved2 bmp.push_back(54 % 256); bmp.push_back(54 / 256); bmp.push_back(0); bmp.push_back(0); //bytes 14-53bmp.push_back(40); bmp.push_back(0); bmp.push_back(0); bmp.push_back(0); //14: biSize bmp.push_back(w % 256); bmp.push_back(w / 256); bmp.push_back(0); bmp.push_back(0); //18: biWidth bmp.push_back(h % 256); bmp.push_back(h / 256); bmp.push_back(0); bmp.push_back(0); //22: biHeight bmp.push_back(1); bmp.push_back(0); //26: biPlanes bmp.push_back(outputChannels * 8); bmp.push_back(0); //28: biBitCount bmp.push_back(0); bmp.push_back(0); bmp.push_back(0); bmp.push_back(0); //30: biCompression bmp.push_back(0); bmp.push_back(0); bmp.push_back(0); bmp.push_back(0); //34: biSizeImage bmp.push_back(0); bmp.push_back(0); bmp.push_back(0); bmp.push_back(0); //38: biXPelsPerMeter bmp.push_back(0); bmp.push_back(0); bmp.push_back(0); bmp.push_back(0); //42: biYPelsPerMeter bmp.push_back(0); bmp.push_back(0); bmp.push_back(0); bmp.push_back(0); //46: biClrUsed bmp.push_back(0); bmp.push_back(0); bmp.push_back(0); bmp.push_back(0); //50: biClrImportant int imagerowbytes = outputChannels * w; //must be multiple of 4 imagerowbytes = imagerowbytes % 4 == 0 ? imagerowbytes : imagerowbytes + (4 - imagerowbytes % 4); for(int y = h - 1; y >= 0; y--) { int c = 0; for(int x = 0; x < imagerowbytes; x++) { if(x < w * outputChannels) { int inc = c; //Convert RGB(A) into BGR(A) if(c == 0) inc = 2; elseif(c == 2) inc = 0; bmp.push_back(image[inputChannels * (w * y + x / outputChannels) + inc]); } elsebmp.push_back(0); c++; if(c >= outputChannels) c = 0; } } // Fill in the size bmp[2] = bmp.size() % 256; bmp[3] = (bmp.size() / 256) % 256; bmp[4] = (bmp.size() / 65536) % 256; bmp[5] = bmp.size() / 16777216; } bool do_convert( std::vector<unsigned char> & input_data, std::vector<unsigned char> & bmp) { std::vector<unsigned char> image; //the raw pixels unsigned width, height; unsigned error = lodepng::decode(image, width, height, input_data, LCT_RGB, 8); if(error) { std::cout << "error " << error << ": " << lodepng_error_text(error) << std::endl; return false; } encodeBMP(bmp, &image[0], width, height); return true; } </del>~~~~~~~~Copy the code

Sorry… The code is too long, but it’s important to understand how it works! Let’s run this code in JavaScript.

Synchronous Buffer processing

PNG image data is actually read when we’re in JavaScript, so it’s passed in as a Buffer in Node.js. We use NAN to access buffer itself. Here’s the full code for the synchronized version:

    NAN_METHOD(GetBMP) {  
        unsigned char*buffer = (unsigned char*) node::Buffer::Data(info[0]->ToObject());  
        unsigned int size = info[1]->Uint32Value();

        std::vector<unsigned char> png_data(buffer, buffer + size);
        std::vector<unsigned char> bmp;

        if ( do_convert(png_data, bmp)) {
            info.GetReturnValue().Set(
                NewBuffer((char *)bmp.data(), bmp.size()/*, buffer_delete_callback, bmp*/).ToLocalChecked());
        }
    }  

    NAN_MODULE_INIT(Init) {  
       Nan::Set(target, New<String>("getBMP").ToLocalChecked(),
            GetFunction(New<FunctionTemplate>(GetBMP)).ToLocalChecked());
    }

    NODE_MODULE(png2bmp, Init)
Copy the code

In the GetBMP function, we open buffer with the familiar Data method, so we can treat it like a normal character array. Next, build a vector based on the input to pass in the do_convert function listed above. Once the BMP vector is filled with the do_convert function, we wrap it in Buffer and return JavaScript.

There is a problem: the data in the returned buffer may be deleted before JavaScript can be used. Why? Because when the GetBMP function returns, the BMP vector flows out of the scope. When a vector exits the scope, the vector destructor deletes all data in the vector – in this case, BMP data too! This is a big problem because the data returned to the JavaScript Buffer is deleted. This will eventually crash the program.

Fortunately, the third and fourth optional parameters of NewBuffer control this situation.

The third argument is the callback function to be called when Buffer has been garbage collected by V8. Remember, buffers are JavaScript objects, and the data is stored outside of V8, but the objects themselves are controlled by V8.

From this perspective, this explains why callbacks are useful. When V8 destroys the buffer, we need some way to free the created data – which can be passed into the callback function with the first argument. The signal for the callback is defined by NAN – NAN ::FreeCallback(). The fourth parameter prompts you to reallocate the memory address, which you can then use as you like.

Since our problem is that vectors containing bitmap data will flow out of scope, we can dynamically allocate vectors and pass in callbacks that will be deleted correctly when buffers are garbage collected.

Here is the new delete_callback, with the new NewBuffer call method. Pass the real pointer to the vector as a signal so that it can be deleted correctly.

    void buffer_delete_callback(char* data, void* the_vector){  
      deletereinterpret_cast<vector<unsigned char> *> (the_vector);
    }

    NAN_METHOD(GetBMP) {

      unsigned char*buffer =  (unsigned char*) node::Buffer::Data(info[0]->ToObject());
      unsigned int size = info[1]->Uint32Value();

      std::vector<unsigned char> png_data(buffer, buffer + size);
      std::vector<unsigned char> * bmp = new vector<unsigned char>();

      if ( do_convert(png_data, *bmp)) {
          info.GetReturnValue().Set(
              NewBuffer(
                (char *)bmp->data(),
                bmp->size(),
                buffer_delete_callback,
                bmp)
                .ToLocalChecked());
      }
    }
Copy the code

NPM install and NPM start run the program, and sample.bmp files are generated in the directory, very similar to sample.png – only the file size is larger (because BMP compression is far less efficient than PNG).

conclusion

There are two core selling points:

1. Data copy consumption between V8 storage units and C++ variables cannot be ignored. If you’re not careful, the performance gains you thought you’d get by throwing your work into C++ will be wasted again.

2. Buffer provides a way to share data between JavaScript and C++, thus avoiding data copying.

I want to make it easy to use Buffer by rotating simple examples of ASCII text and converting images synchronously and asynchronously. Hopefully this article has helped you improve the performance of your extended application!

Again, all code in this article can be found at github.com/freezer333/… In the “buffers” directory.

If you’re looking for tips on how to design C++ extensions to node.js, check out my C++ and node.js ebook.