- 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.
In asynchronous extension, we execute a chunk of C++ processing code in a worker thread. If you’re not familiar with asynchronous callbacks, check out these tutorials (here and here).
The central idea of asynchronous extension is that you cannot access V8 (JavaScript) memory outside of the event loop thread. This leads to new problems. Large amounts of data must be copied from the event loop out of V8 memory, into the extended native address space, before the worker thread starts. Similarly, any data generated or modified by the worker thread must be copied back to the V8 engine by executing the code in the event loop (callback). If you are committed to building high-throughput Node.js applications, you should avoid spending too much time copying data in event loops.
Ideally, we’d prefer to do this:
Node.js Buffer comes to the rescue
There are two related issues here.
- 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.
- 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.
Buffers store raw binary data that can be accessed through Node.js read files and other I/O device apis.
With the help of some examples in the Node.js documentation, you can initialize buffers of specified size, buffers of specified default values, buffers created from byte arrays, and buffers created from strings.
// 10 bytes buffer: const buf1 = buffer.alloc (10); // 10 bytes buffer: const buf1 = buffer.alloc (10); // 10 bytes initialized to 1 buffer: const buf2 = buffer.alloc (10, 1); // 10 bytes initialized to 1 buffer: const buf2 = buffer.alloc (10, 1); // Buffer containing [0x1, 0x2, 0x3] const buf3 = buffer. From ([1, 2, 3]); // Buffer containing ASCII bytes [0x74, 0x65, 0x73, 0x74] : const buf4 = buffer. from('test'); // Const buf5 = fs.readfilesync ("some file"); // Const buf5 = fs.readfilesync ("some file");
Copy the code
Buffers can pass back traditional JavaScript data (strings) or write back to files, databases, or other I/O devices.
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.
The above example is very basic and not exciting. Let’s look at a more practical example – C++ image processing. If you want to get the full source code of the above example and this example, please go to my GitHub repository github.com/freezer333/… , code is in the ‘buffers’ directory.
Image processing is perfect for C++ extension processing because it is time consuming, CPU intensive, and has many parallel processing methods that C++ excels at. In this example, we will simply convert the image from PNG format to BMP format.
PNG conversion to BMP is not particularly time-consuming, and using the extension may be overkill, but it works well for demonstration purposes. If you are looking for pure JavaScript image processing (including more than turn the PNG BMP) is implemented, can see JIMP, www.npmjs.com/package/jim… www.npmjs.com/package/jim… .
There are many open source C++ libraries that can help us do this. I’m going to use LodePNG because it has no dependencies and is easy to use. LodePNG is at lodev.org/lodepng/, and its source code is at github.com/lvandeve/lo… . Thanks to developer Lode Vandevenne for providing such a good library!
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).
Then develop an asynchronous version of the PnG-bitmap converter. Use Nan::AsyncWorker to perform real conversion methods in a C++ thread. By using Buffer objects, we can avoid copying PNG data, so we only need to get a pointer to the underlying data that is accessible to the worker thread. Similarly, data generated by worker threads (BMP vectors) can be used to create new buffers without copying the data.
class PngToBmpWorker : public AsyncWorker {
public:
PngToBmpWorker(Callback * callback,
v8::Local<v8::Object> &pngBuffer, int size)
: AsyncWorker(callback) {
unsigned char*buffer =
(unsigned char*) node::Buffer::Data(pngBuffer);
std::vector<unsigned char> tmp(
buffer,
buffer + (unsigned int) size);
png_data = tmp;
}
voidExecute(){
bmp = new vector<unsigned char>();
do_convert(png_data, *bmp);
}
voidHandleOKCallback(){
Local<Object> bmpData =
NewBuffer((char *)bmp->data(),
bmp->size(), buffer_delete_callback,
bmp).ToLocalChecked();
Local<Value> argv[] = { bmpData };
callback->Call(1, argv);
}
private:
vector<unsigned char> png_data;
std::vector<unsigned char> * bmp;
};
NAN_METHOD(GetBMPAsync) {
int size = To<int>(info[1]).FromJust();
v8::Local<v8::Object> pngBuffer =
info[0]->ToObject();
Callback *callback =
new Callback(info[2].As<Function>());
AsyncQueueWorker(
new PngToBmpWorker(callback, pngBuffer , size));
}
Copy the code
Our new GetBMPAsync extension first decompresses the buffer passed in from JavaScript, then initializes it and enlists the new PngToBmpWorker thread using the NAN API. The Execute method of the worker thread object is called by libuv in the worker thread at the end of the transformation. When the Execute function returns, Libuv calls the HandleOKCallback method of the Node.js event polling thread, creating a buffer and calling the callback function passed in by JavaScript.
Now we can use this extension function in JavaScript:
png2bmp.getBMPAsync(png_buffer,
png_buffer.length,
function(bmp_buffer) {
fs.writeFileSync(bmp_file, bmp_buffer);
});
Copy the code
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.