Node.js And C++__10.Buffers
本書的第2章介紹了在將資料移動到C++外掛時使用典型的JavaScript資料型別。Node.js 引入了一種新的資料型別, `Buffer`,這在標準JavaScript中是找不到的(儘管新版本的JavaScript現在已經有了型別化陣列,它們提供了許多相同的功能)。 Node.js緩衝區物件用來表示原始的二進位制資料,類似於C++陣列(在本例中是整數陣列)。當你使用 Node.js輸入/輸出或TCP的時候,您可能會使用 `Buffer` 物件。雖然將 `Buffer` 物件轉換成JavaScript字串是很常見的(通過指定整數資料的編碼),但通常情況下,您可能希望直接在二進位制資料上操作。
Buffer
物件是C++addon開發的一個有趣的方面,首先因為它們實際上不是V8的一部分,而是node.js的一部分。其次, Buffer
物件資料是惟一的,因為它沒有在V8堆中分配——這一屬性可以讓我們在處理C++外掛和工作執行緒時避開一些資料複製(下面將對此進行討論)。
在本節中,我們將討論如何使用NAN將 Buffer
物件傳遞給C++外掛。之所以使用NAN,是因為Buffer
物件API最近發生了一些重大變化,而NAN將保護我們免受這些問題的影響。我們將通過影象轉換器來觀察 Buffer
o物件——特別是將二進位制png影象資料轉換成點陣圖格式的二進位制資料。
例子:PNG and BMP 轉換
影象處理,一般來說,是任何操縱/轉換影象的東西。當然,影象是二進位制資料的一大塊——在它最基本的狀態中,一個整數(或3或4)可以用來表示影象中的每個畫素,這些整數可以儲存在一個檔案中,或者儲存在一個連續分配的陣列中。通常,影象資料不是以原始資料形式儲存的,而是將其壓縮/編碼成影象格式標準,如png、gif、bmp、jpeg等。
影象處理是C++外掛的一個很好的候選,因為影象處理通常是耗時的,CPU密集型的,並且一些處理技術具有C++可以利用的並行性。對於我們現在要看的例子,我們將簡單地將png格式的資料轉換成bmp格式的資料。有很多現成的、開放原始碼的C++庫可以幫助我們完成這項任務,我將使用 LodePNG
LodePNG
可以在http://lodev.org/lodepng/中找到,它的原始碼在https://github.com/lvande/LodePNG。非常感謝開發者,Lode Vandevenne提供了這樣一個易於使用的庫!
addon
對於這個addon,我們將建立以下目錄結構,其中包括從https://github.com/lvande/lodepng下載的原始碼,也就是lodepng.h
和 lodepng.cpp
。
/png2bmp
|
|--- binding.gyp
|--- package.json
|--- png2bmp.cpp # the addon
|--- index.js # program to test the addon
|--- sample.png # input (will be converted to bmp)
|--- lodepng.h # from lodepng distribution
|--- lodepng.cpp # From loadpng distribution
lodepng.cpp
包含了進行影象處理的所有必要程式碼,我將不討論它的細節。此外,lodepng發行版包含了一些示例程式碼,允許您在png和bmp之間進行特定的轉換——我已經稍微修改了它,並將其放入addon原始碼檔案 png2bmp.cpp
,我們很快就會看一看。讓我們首先看看實際的JavaScript程式是什麼樣子的——在深入研究addon程式碼之前:
'use strict';
const fs = require('fs');
const path = require('path');
const png2bmp = require('./build/Release/png2bmp');
var png_file = process.argv[2];
var bmp_file = path.basename(png_file, '.png') + ".bmp";
var png_buffer = fs.readFileSync(png_file);
png2bmp.saveBMP(bmp_file, png_buffer, png_buffer.length);
這個程式只需要fs
、path
和addon,它將位於 ./build/Releast/png2bmp
。程式從命令列引數中獲取一個輸入(png)檔名,並將png讀取到Buffer中。然後,它將 Buffer
送入addon,它將轉換後的BMP儲存到指定的檔名中。因此,addon將png轉換為BMP,並將結果儲存到一個檔案中——不返回任何東西。
package.json
,它設定npm start呼叫 index.js
程式帶有一個命令列引數的 sample.png
.。這是一個很普通的圖片:
{
"name": "png2bmp",
"version": "0.0.1",
"private": true,
"gypfile": true,
"scripts": {
"start": "node index.js sample.png"
},
"dependencies": {
"nan": "*"
}
}
最後,讓我們看一看 binding.gyp
檔案——這是相當標準的,除了需要編譯lodepng所需的一些編譯器標誌之外。它還包括對NAN的必要引用。
{
"targets": [
{
"target_name": "png2bmp",
"sources": [ "png2bmp.cpp", "lodepng.cpp" ],
"cflags": ["-Wall", "-Wextra", "-pedantic", "-ansi", "-O3"],
"include_dirs" : ["<!(node -e \"require('nan')\")"]
}
]
}
png2bmp.cpp
將主要包含V8/NAN程式碼,但是它確實有一個影象處理實用功能——do_convert
,從lodepng的png到bmp示例程式碼。這個函式接受一個vector<unsigned char>
包含輸入資料(png格式)和一個vector<unsigned char>
輸出資料(bmp格式放入其中)。這個函式反過來呼叫 encodeBMP
,這是從lodepng示例中直接呼叫的。下面是這兩個函式的完整程式碼清單。細節對於理解addon Buffer
物件並不重要,但是在這裡包含了完整性。我們的addon入口點(s)將呼叫 do_convert
。
/*
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)
{
//3 bytes per pixel used for both input and output.
int inputChannels = 3;
int outputChannels = 3;
//bytes 0-13
bmp.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-53
bmp.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;
else if(c == 2) inc = 0;
bmp.push_back(image[inputChannels
* (w * y + x / outputChannels) + inc]);
}
else bmp.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;
}
對不起……這個清單很長,但重要的是要看看到底發生了什麼!讓我們開始將所有這些程式碼連線到JavaScript上。
向addon傳buffers
我們的第一個任務是建立 saveBMP
addon函式,它接受一個檔名(目標BMP)和png資料。當我們使用JavaScript時,png影象資料實際上是讀取的,所以它作為一個Node.js Buffer
傳遞過來。要認識到的第一個規則是,V8不知道 Buffer
是什麼,它是一個Node.js 結構。我們將使用NAN來訪問緩衝區本身(稍後建立新的 Buffer
物件)。每當緩衝區物件被傳遞給C++addons時,就有必要將它的長度指定為一個附加的引數,因為很難從C++中確定緩衝區的實際資料長度。
讓我們在NAN中設定第一個函式呼叫:
NAN_METHOD(SaveBMP) {
v8::String::Utf8Value val(info[0]->ToString());
std::string outfile (*val);
....
}
NAN_MODULE_INIT(Init) {
Nan::Set(target,
New<String>("saveBMP").ToLocalChecked(),
GetFunction(New<FunctionTemplate>(SaveBMP))
.ToLocalChecked());
}
NODE_MODULE(basic_nan, Init)
在 SaveBMP
中,我們要做的第一步是對輸出檔名進行簡單的提取。接下來,我們必須提取二進位制資料(將由無符號char型資料表示)。
unsigned char* buffer =
(unsigned char*) node::Buffer::Data(info[1]->ToObject());
unsigned int size = info[2]->Uint32Value();
注意這是多麼容易。Node.js在 Buffer
類上提供了一個靜態 Data
方法,它接受一個標準的 v8::Object
handle,並返回一個 unsigned char
指標到底層資料。這個指標並不指向由V8回憶管理的資料——它在普通的C++堆上,並且可以這樣工作。我們還從第三個引數提取到addon函式的大小。
通常在C++中,我們更傾向於處理STL容器而不是原始的記憶體陣列,所以我們可以很容易地從這個指標建立一個 vector
——我們需要做的就是呼叫 do_convert
.。下面是完整的程式碼清單——它將緩衝區的資料指標轉換成一個vector,呼叫do_convert
,它的工作原理是將bmp資料填充到我們給予它的向量中,最後將它儲存到所需的輸出檔案中(使用lodepng實用調-save_file
)。
NAN_METHOD(SaveBMP) {
v8::String::Utf8Value val(info[0]->ToString());
std::string outfile (*val);
// Convert the Node.js Buffer into a C++ Vector
unsigned char*buffer =
(unsigned char*) node::Buffer::Data(info[1]->ToObject());
unsigned int size = info[2]->Uint32Value();
std::vector<unsigned char> png_data(buffer, buffer + size);
// Convert to bmp, stored in another vector.
std::vector<unsigned char> bmp;
if ( do_convert(png_data, bmp)) {
info.GetReturnValue().Set(Nan::New(false));
}
else {
lodepng::save_file(bmp, outfile);
info.GetReturnValue().Set(Nan::New(true));
}
}
執行這個程式,執行 npm install
命令,執行npm start
,你會看到一個 sample.bmp
圖片被生成,它看起來與 sample.png
非常相似 ,只是一個大得多(bmp壓縮比png低得多)。
addon 返回 buffers
如果我們只是返回點陣圖影象資料,而不是在C++中儲存它,那麼這個addon將會更加靈活。要做到這一點,我們必須學習如何歸還 Buffer
物件。從表面上看,這個概念似乎很簡單——你可以在NAN的網站上看到一些例子,看看在C++中建立的新緩衝區,然後返回到JavaScript。不過,仔細看一下,我們必須小心一些問題,我們將在這裡解決。
讓我們建立一個新的addon入口點- getBMP
-它將從JavaScript中呼叫:
...
var png_buffer = fs.readFileSync(png_file);
bmp_buffer = png2bmp.getBMP(png_buffer, png_buffer.length);
fs.writeFileSync(bmp_file, bmp_buffer);
在原來的C++函式中,我們呼叫了 do_convert
,它把點陣圖資料放入 vector<unsigned int>
,我們把它寫到一個檔案中。現在,我們必須通過構造一個新的Buffer
來返回該資料。NAN的NewBuffer
呼叫巧妙地解決了這個問題——讓我們看一下addon函式的第一個草稿:
void buffer_delete_callback(char* data, void* hint) {
free(data);
}
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 = vector<unsigned char>();
if ( do_convert(png_data, bmp)) {
info.GetReturnValue().Set(
NewBuffer((char *)bmp.data(),
bmp.size(), buffer_delete_callback, 0)
.ToLocalChecked());
}
}
上面的程式碼示例遵循了大多數線上教程的倡導者。我們用char * (我們從bmp向量中獲取資料方法)呼叫 NewBuffer
,我們建立緩衝區的記憶體大小,然後還有兩個額外的引數可能會引起您的好奇心。 NewBuffer
的第三個引數是一個回撥——當你建立的 Buffer
被V8收集到垃圾時,它就會被呼叫。回想一下, Buffer
是JavaScript物件,它的資料儲存在V8之外——但是物件本身在V8的控制之下。從這個角度來看,回撥是很方便的——當V8銷燬 Buffer
時,我們需要某種方法來釋放我們建立的資料——它作為第一個引數傳遞到回撥中。回撥是由NAN-Nan::FreeCallback()
定義的。第4個引數是一個提示,幫助我們在deallocation中提供幫助,我們可以使用它。它很快就會有幫助,但是現在我們只傳遞null(0)。
這就是程式碼的問題:我們返回的緩衝區中包含的資料很可能在JavaScript使用之前就被刪除了。為什麼?如果你對C++很瞭解,你可能已經看到了問題:當我們的 GetBMP
函式返回時,bmp向量將會超出範圍。C++vector語義認為,當向量超出範圍時,vector的解構函式將刪除向量中的所有資料——在本例中,是我們的bmp資料!這是一個很大的問題,因為我們發回給JavaScript的緩衝區將會把它的資料從下面刪除。你可能會僥倖逃脫(比賽條件很有趣,對吧?),但它最終會導致你的程式崩潰。
我們如何解決這個問題?一種方法是建立一個包含bmp
vector資料副本的 Buffer
。我們可以這樣做:
if ( do_convert(png_data, bmp)) {
info.GetReturnValue().Set(
CopyBuffer(
(char *)bmp.data(),
bmp.size()).ToLocalChecked());
}
這確實是安全的,但它涉及到建立資料的副本——緩慢和記憶體浪費。避免這種混亂的一種方法不是使用一個 vector,而是將點陣圖資料儲存在一個動態分配的 char *
陣列中——然而這使得點陣圖轉換程式碼變得更加麻煩。值得慶幸的是,這個問題的答案允許我們仍然使用 vector,這是由 Nan::FreeCallback
呼叫簽名——即 hint
引數所建議的。由於我們的問題是包含點陣圖資料的vector超出了範圍,所以我們可以動態地分配vector本身,並將其傳遞給空閒回撥,當 Buffer
被垃圾收集時,它可以被正確地刪除。下面是完整的解決方案——現在請注意,我們正在使用回撥中的hint
引數,並且我們使用的是一個動態分配的(堆)向量,而不是一個堆疊變數。
void buffer_delete_callback(char* data, void* the_vector) {
delete reinterpret_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);
// allocate the vector on the heap because we
// are building a buffer out of it's data to
// return to Node - and don't want to allow
// it to go out of scope until the buffer
// does (see buffer_delete_callback).
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());
}
}
當您執行這個程式時,JavaScript現在可以安全地在返回的 Buffer
上操作,而不需要擔心刪除t vector
的向量。
Buffers 工作執行緒拷貝問題
閱讀上面的部分,您可能會想起第4章關於V8記憶體和工作執行緒的討論。在使用非同步外掛時,我們遇到了一個重要的問題,因為建立非同步工作的C++執行緒永遠不能直接訪問V8資料。除了在C++堆空間中建立資料副本之外,沒有真正的解決方案。對於大量的addons,如果可以的話——但是正如當時所建議的那樣,當在JavaScript和C++之間移動大量資料時,這是一個真正的問題。現在,我們可以看到一個可能的解決方案——將資料分配為緩衝區物件!
讓我們開發一個非同步版本的png到點陣圖轉換器。我們將使用 Nan::AsyncWorker
來執行C++工作執行緒中的實際轉換。然而,通過使用 Buffer
物件,我們將不需要建立png資料的副本——我們只需要持有一個指向底層資料的指標,這樣我們的工作執行緒就可以訪問它。同樣地,工作執行緒產生的資料( bmp
vector 可以用來建立一個新的緩衝區,而不需要複製資料,如上所示。由於我們已經在這本書中與AsyncWorker
合作了很多,我將簡單地向您展示下面的程式碼——這很簡單:
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;
}
void Execute() {
bmp = new vector<unsigned char>();
do_convert(png_data, *bmp);
}
void HandleOKCallback () {
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));
}
現在我們有了一個非同步函式來獲取點陣圖編碼的資料——沒有不必要的資料複製。
png2bmp.getBMPAsync(png_buffer,
png_buffer.length,
function(bmp_buffer) {
fs.writeFileSync(bmp_file, bmp_buffer);
});