- 原文作者:Scott Frees
- 譯文出自:掘金翻譯計(jì)劃
- 譯者:Jiang Haichao
- 校對(duì)者:熊賢仁, Lei Guo
使用 Node.js 開(kāi)發(fā)的一個(gè)好處是簡(jiǎn)直能夠在 JavaScript 和 原生 C++ 代碼之間無(wú)縫切換 - 這要得益于 V8 的擴(kuò)展 API。從 JavaScript 進(jìn)入 C++ 的能力有時(shí)由處理速度驅(qū)動(dòng),但更多的情況是我們已經(jīng)有 C++ 代碼,而我們想要直接用 JavaScript 調(diào)用。
我們可以用(至少)兩軸對(duì)不同用例的擴(kuò)展進(jìn)行分類 - (1)C++ 代碼的運(yùn)行時(shí)間,(2)C++ 和 JavaScript 之間數(shù)據(jù)流量。

大多數(shù)文檔討論的 Node.js 的 C++ 擴(kuò)展關(guān)注于左右象限的不同。如果你在左象限(短處理時(shí)間),你的擴(kuò)展有可能是同步的 - 意思是當(dāng)調(diào)用時(shí) C++ 代碼在 Node.js 的事件循環(huán)中直接運(yùn)行。
"#nodejs 允許我們?cè)?javascript 和原生 C++ 代碼之間無(wú)縫切換" via @RisingStack
在這個(gè)場(chǎng)景中,擴(kuò)展函數(shù)阻塞并等待返回值,意味著其他操作不能同時(shí)進(jìn)行。在右側(cè)象限中,幾乎可以確定要用異步模式來(lái)設(shè)計(jì)附加組件。在一個(gè)異步擴(kuò)展函數(shù)中,JavaScript 調(diào)用函數(shù)立即返回。調(diào)用代碼向擴(kuò)展函數(shù)傳入一個(gè)回調(diào),擴(kuò)展函數(shù)工作于一個(gè)獨(dú)立工作線程中。由于擴(kuò)展函數(shù)沒(méi)有阻塞,則避免了 Node.js 事件循環(huán)的死鎖。
頂部和底部象限的不同時(shí)常容易被忽視,但是他們也同樣重要。
V8 vs. C++ 內(nèi)存和數(shù)據(jù)
如果你不了解如何寫一個(gè)原生附件,那么你首先要掌握的是屬于 V8 的數(shù)據(jù)(可以 通過(guò) C++ 附件獲取的)和普通 C++ 內(nèi)存分配的區(qū)別。
當(dāng)我們提到 “屬于 V8 的”,指的是持有 JavaScript 數(shù)據(jù)的存儲(chǔ)單元。
這些存儲(chǔ)單元是可通過(guò) V8 的 C++ API 訪問(wèn)的,但它們不是普通的 C++ 變量,因?yàn)樗麄冎荒軌蛲ㄟ^(guò)受限的方式訪問(wèn)。當(dāng)你的擴(kuò)展 可以 限制為只使用 V8 數(shù)據(jù),它就更有可能同樣會(huì)在普通 C++ 代碼中創(chuàng)建自身的變量。這些變量可以是?;蚨炎兞浚彝耆?dú)立于 V8。
在 JavaScript 中,基本類型(數(shù)字,字符串,布爾值等)是 不可變的,一個(gè) C++ 擴(kuò)展不能夠改變與基本類型相連的存儲(chǔ)單元。這些基本類型的 JavaScript 變量可以被重新分配到 C++ 創(chuàng)建的 新存儲(chǔ)單元 中 - 但是這意味著改變數(shù)據(jù)將會(huì)導(dǎo)致 新 內(nèi)存的分配。
在上層象限(少量數(shù)據(jù)傳遞),這沒(méi)什么大不了。如果你正在設(shè)計(jì)一個(gè)無(wú)需頻繁數(shù)據(jù)交換的附加組件,那么所有新內(nèi)存分配的開(kāi)銷可能沒(méi)有那么大。當(dāng)擴(kuò)展更靠近下層象限時(shí),分配/拷貝的開(kāi)銷會(huì)開(kāi)始令人震驚。
一方面,這會(huì)增大最高的內(nèi)存使用量,另一方面,也會(huì) 損耗性能。
在 JavaScript(V8 存儲(chǔ)單元) 和 C++(返回)之間復(fù)制所有數(shù)據(jù)花費(fèi)的時(shí)間通常會(huì)犧牲首先運(yùn)行 C++ 賺來(lái)的性能紅利!對(duì)于在左下象限(低處理,高數(shù)據(jù)利用場(chǎng)景)的擴(kuò)展應(yīng)用,數(shù)據(jù)拷貝的延遲會(huì)把你的擴(kuò)展引用往右側(cè)象限引導(dǎo) - 迫使你考慮異步設(shè)計(jì)。
V8 內(nèi)存與異步附件
在異步擴(kuò)展中,我們?cè)谝粋€(gè)工作線程中執(zhí)行大塊的 C++ 處理代碼。如果你對(duì)異步回調(diào)并不熟悉,看看這些教程(這里 和 這里)。
異步擴(kuò)展的中心思想是 你不能在事件循環(huán)線程外訪問(wèn) V8 (JavaScript)內(nèi)存。這導(dǎo)致了新的問(wèn)題。大量數(shù)據(jù)必須在工作線程啟動(dòng)前 從事件循環(huán)中 復(fù)制到 V8 內(nèi)存之外,即擴(kuò)展的原生地址空間中去。同樣地,工作線程產(chǎn)生或修改的任何數(shù)據(jù)都必須通過(guò)執(zhí)行事件循環(huán)(回調(diào))中的代碼拷貝回 V8 引擎。如果你致力于創(chuàng)建高吞吐量的 Node.js 應(yīng)用,你應(yīng)該避免花費(fèi)過(guò)多的時(shí)間在事件循環(huán)的數(shù)據(jù)拷貝上。

理想情況下,我們更傾向于這么做:

Node.js Buffer 來(lái)救命
這里有兩個(gè)相關(guān)的問(wèn)題。
- 當(dāng)使用同步擴(kuò)展時(shí),除非我們不改變/產(chǎn)生數(shù)據(jù),那么可能會(huì)需要花費(fèi)大量時(shí)間在 V8 存儲(chǔ)單元和老的簡(jiǎn)單 C++ 變量之間移動(dòng)數(shù)據(jù) - 十分費(fèi)時(shí)。
- 當(dāng)使用異步擴(kuò)展時(shí),理想情況下我們應(yīng)該盡可能減少事件輪詢的時(shí)間。這就是問(wèn)題所在 - 由于 V8 的多線程限制,我們 必須 在事件輪詢線程中進(jìn)行數(shù)據(jù)拷貝。
Node.js 里有一個(gè)經(jīng)常會(huì)被忽視的特性可以幫助我們進(jìn)行擴(kuò)展開(kāi)發(fā) - Buffer。Nodes.js 官方文檔 在此。
Buffer 類的實(shí)例與整型數(shù)組類似,但對(duì)應(yīng)的是 V8 堆外大小固定,原始內(nèi)存分配空間。
這不就是我們一直想要的嗎 - Buffer 里的數(shù)據(jù) 并不存儲(chǔ)在 V8 存儲(chǔ)單元內(nèi),不受限于 V8 的多線程規(guī)則。這意味著可以通過(guò)異步擴(kuò)展啟動(dòng)的 C++ 工作線程與 Buffer 進(jìn)行交互。
Buffer 是如何工作的
Buffer 存儲(chǔ)原始的二進(jìn)制數(shù)據(jù),可以通過(guò) Node.js 的讀文件和其他 I/O 設(shè)備 API 訪問(wèn)。
借助 Node.js 文檔里的一些例子,可以初始化指定大小的 buffer,指定預(yù)設(shè)值的 buffer,由字節(jié)數(shù)組創(chuàng)建的 buffer 和 由字符串創(chuàng)建的 buffer。
// 10 個(gè)字節(jié)的 buffer:const buf1 = Buffer.alloc(10);
// 10 字節(jié)并初始化為 1 的 buffer:const buf2 = Buffer.alloc(10, 1);
//包含 [0x1, 0x2, 0x3] 的 buffer:const buf3 = Buffer.from([1, 2, 3]);
// 包含 ASCII 字節(jié) [0x74, 0x65, 0x73, 0x74] 的 buffer:const buf4 = Buffer.from('test');
// 從文件中讀取 buffer:const buf5 = fs.readFileSync("some file");
Buffer 能夠傳回傳統(tǒng) JavaScript 數(shù)據(jù)(字符串)或者寫回文件,數(shù)據(jù)庫(kù),或者其他 I/O 設(shè)備中。
C++ 中如何訪問(wèn) Buffer
構(gòu)建 Node.js 的擴(kuò)展時(shí),最好是通過(guò)使用 NAN(Node.js 原生抽象)API 啟動(dòng),而不是直接用 V8 API 啟動(dòng) - 后者可能是一個(gè)移動(dòng)目標(biāo)。網(wǎng)上有許多用 NAN 擴(kuò)展啟動(dòng)的教程 - 包括 NAN 代碼庫(kù)自己的 例子。我也寫過(guò)很多 教程,在我的 電子書 里藏得比較深。
首先,來(lái)看看擴(kuò)展程序如何訪問(wèn) JavaScript 發(fā)送給它的 Buffer。我們會(huì)啟動(dòng)一個(gè)簡(jiǎn)單的 JS 程序并引入稍后創(chuàng)建的擴(kuò)展。
'use strict';
// 先引入稍后創(chuàng)建的擴(kuò)展
const addon = require('./build/Release/buffer_example');
// 在 V8 之外分配內(nèi)存,預(yù)設(shè)值為 ASCII 碼的 "ABC"
const buffer = Buffer.from("ABC");
// 同步,每個(gè)字符旋轉(zhuǎn) +13
addon.rotate(buffer, buffer.length, 13);
console.log(buffer.toString('ascii'));
"ABC" 進(jìn)行 ASCII 旋轉(zhuǎn) 13 后,期望輸出是 "NOP"。來(lái)看看擴(kuò)展!它由三個(gè)文件(方便起見(jiàn),都在同一目錄下)組成。
// binding.gyp
{
"targets": [
{
"target_name": "buffer_example",
"sources": [ "buffer_example.cpp" ],
"include_dirs" : ["<!(node -e \"require('nan')\")"]
}
]
}
//package.json
{
"name": "buffer_example",
"version": "0.0.1",
"private": true,
"gypfile": true,
"scripts": {
"start": "node index.js"
},
"dependencies": {
"nan": "*"
}
}
// 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)
最有趣的文件就是 buffer_example.cpp。注意我們用了 node:Buffer 的 Data 方法來(lái)把傳入擴(kuò)展的第一個(gè)參數(shù)轉(zhuǎn)換為字符數(shù)組?,F(xiàn)在我們能用任何覺(jué)得合適的方式來(lái)操作數(shù)組了。在本例中,我們僅僅執(zhí)行了文本的 ASCII 碼旋轉(zhuǎn)。要注意這沒(méi)有返回值,Buffer 的關(guān)聯(lián)內(nèi)存已經(jīng)被修改了。
通過(guò) npm install 構(gòu)建擴(kuò)展。package.json 會(huì)告知 npm 下載 NAN 并使用 binding.gyp 文件構(gòu)建擴(kuò)展。運(yùn)行 index.js 會(huì)返回期望的 "NOP" 輸出。
我們還可以在擴(kuò)展里創(chuàng)建 新 buffer。修改 rotate 函數(shù)增加輸入,并返回減小相應(yīng)數(shù)值后生成的字符串 buffer。
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());
}
var result = addon.rotate(buffer, buffer.length, 13);
console.log(buffer.toString('ascii'));
console.log(result.toString('ascii'));
現(xiàn)在結(jié)果 buffer 是 '456'。注意 NAN 的 NewBuffer 方法的使用,它包裝了 Node buffer 里 retval 數(shù)據(jù)的動(dòng)態(tài)分配。這么做會(huì) 轉(zhuǎn)讓這塊內(nèi)存的使用權(quán) 給 Node.js,所以當(dāng) buffer 越過(guò) JavaScript 作用域時(shí) retval 的關(guān)聯(lián)內(nèi)存將會(huì)(通過(guò)調(diào)用 free)重新聲明。稍后會(huì)有更多關(guān)于這一點(diǎn)的解釋 - 畢竟我們不希望總是重新聲明。
你可以在 這里 找到 NAN 如何處理 buffer 的更多信息。
?? :PNG 和 BMP 圖片處理
上面的例子非常基礎(chǔ),沒(méi)什么興奮點(diǎn)。來(lái)看個(gè)更具有實(shí)操性的例子 - C++ 圖片處理。如果你想要拿到上例和本例的全部源碼,請(qǐng)到我的 GitHub 倉(cāng)庫(kù) https://github.com/freezer333/nodecpp-demo,代碼在 'buffers' 目錄下。
圖片處理用 C++ 擴(kuò)展處理再合適不過(guò),因?yàn)樗臅r(shí),CPU 密集,許多處理方法并行,而這些正是 C++ 所擅長(zhǎng)的。本例中我們會(huì)簡(jiǎn)單地將圖片由 png 格式轉(zhuǎn)換為 bmp 格式。
png 轉(zhuǎn)換 bmp 不是 特別耗時(shí),使用擴(kuò)展可能有點(diǎn)大材小用了,但能很好的實(shí)現(xiàn)示范目的。如果你在找純 JavaScript 進(jìn)行圖片處理(包括不止 png 轉(zhuǎn) bmp)的實(shí)現(xiàn)方式,可以看看 JIMP,https://www.npmjs.com/package/jimphttps://www.npmjs.com/package/jimp。
有許多開(kāi)源 C++ 庫(kù)可以幫我們做這件事。我要使用的是 LodePNG,因?yàn)樗鼪](méi)有依賴,使用方便。LodePNG 在 http://lodev.org/lodepng/,它的源碼在 https://github.com/lvandeve/lodepng。多謝開(kāi)發(fā)者 Lode Vandevenne 提供了這么好用的庫(kù)!
設(shè)置擴(kuò)展
我們要?jiǎng)?chuàng)建以下目錄結(jié)構(gòu),包括從 https://github.com/lvandeve/lodepng 下載的源碼,也就是 lodepng.h 和 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
lodepng.cpp 包含所有進(jìn)行圖片處理必要的代碼,我不會(huì)就其工作細(xì)節(jié)進(jìn)行討論。另外,lodepng 包囊括了允許你指定在 pnp 和 bmp 之間進(jìn)行轉(zhuǎn)換的簡(jiǎn)單代碼。我對(duì)它進(jìn)行了一些小改動(dòng)并放入擴(kuò)展源文件 png2bmp.cpp 中,馬上我們就會(huì)看到。
在深入擴(kuò)展之前來(lái)看看 JavaScript 程序:
'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);
這個(gè)程序把 png 圖片的文件名作為命令行參數(shù)傳入。調(diào)用了 getBMP 擴(kuò)展函數(shù),該函數(shù)接受包含 png 文件的 buffer 和它的長(zhǎng)度。此擴(kuò)展是 同步 的,在稍后我們也會(huì)看到異步版本。
這是 package.json 文件,設(shè)置了 npm start 命令來(lái)調(diào)用 index.js 程序并傳入 sample.png 命令行參數(shù)。這是一張普通的圖片。
{
"name": "png2bmp",
"version": "0.0.1",
"private": true,
"gypfile": true,
"scripts": {
"start": "node index.js sample.png"
},
"dependencies": {
"nan": "*"
}
}
這是 binding.gyp 文件 - 在標(biāo)準(zhǔn)文件的基礎(chǔ)上設(shè)置了一些編譯器標(biāo)識(shí)用于編譯 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 代碼。不過(guò),它也有一個(gè)圖片處理通用函數(shù) - do_convert,從 lodepng 的 png 轉(zhuǎn) bmp 例子里采納過(guò)來(lái)的。
encodeBMP 函數(shù)接受 vector<unsigned char> 參數(shù)用于輸入數(shù)據(jù)(png 格式)和 vector<unsigned char> 參數(shù)來(lái)存放輸出數(shù)據(jù)(bmp 格式,直接參照 lodepng 的例子。
這是這兩個(gè)函數(shù)的全部代碼。細(xì)節(jié)對(duì)于理解擴(kuò)展的 Buffer 對(duì)象不重要,包含進(jìn)來(lái)是為了程序完整性。擴(kuò)展程序入口會(huì)調(diào)用 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>~~~~~~~~
Sorry... 代碼太長(zhǎng)了,但對(duì)于理解運(yùn)行機(jī)制很重要!把這些代碼在 JavaScript 里運(yùn)行一把看看。
同步 Buffer 處理
當(dāng)我們?cè)?JavaScript 里,png 圖片數(shù)據(jù)會(huì)被真實(shí)讀取,所以會(huì)作為 Node.js 的 Buffer 傳入。我們用 NAN 訪問(wèn) buffer 自身。這里是同步版本的完整代碼:
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)
在 GetBMP 函數(shù)里,我們用熟悉的 Data 方法打開(kāi) buffer,所以我們能夠像普通字符數(shù)組一樣處理它。接著,基于輸入構(gòu)建一個(gè) vector,才能夠傳入上面列出的 do_convert 函數(shù)。一旦 bmp 向量被 do_convert 函數(shù)填滿,我們會(huì)把它包裝進(jìn) Buffer 里并返回 JavaScript。
這里有個(gè)問(wèn)題:返回的 buffer 里的數(shù)據(jù)在 JavaScript 使用之前可能會(huì)被刪除。為啥?因?yàn)楫?dāng) GetBMP 函數(shù)返回時(shí),bmp 向量要傳出作用域。C++ 向量語(yǔ)義當(dāng)向量傳出作用域時(shí),向量析構(gòu)函數(shù)會(huì)刪除向量里所有的數(shù)據(jù) - 在本例中,bmp 數(shù)據(jù)也會(huì)被刪掉!這是個(gè)大問(wèn)題,因?yàn)榛貍鞯?JavaScript 的 Buffer 里的數(shù)據(jù)會(huì)被刪掉。這最后會(huì)使程序崩潰。
幸運(yùn)的是,NewBuffer 的第三和第四個(gè)可選參數(shù)可控制這種情況。
第三個(gè)參數(shù)是當(dāng) Buffer 被 V8 垃圾回收結(jié)束時(shí)調(diào)用的回調(diào)函數(shù)。記住,Buffer 是 JavaScript 對(duì)象,數(shù)據(jù)存儲(chǔ)在 V8 之外,但是對(duì)象本身受到 V8 的控制。
從這個(gè)角度來(lái)看,就能解釋為什么回調(diào)有用。當(dāng) V8 銷毀 buffer 時(shí),我們需要一些方法來(lái)釋放創(chuàng)建的數(shù)據(jù) - 這些數(shù)據(jù)可以通過(guò)第一個(gè)參數(shù)傳入回調(diào)函數(shù)中?;卣{(diào)的信號(hào)由 NAN 定義 - Nan::FreeCallback()。第四個(gè)參數(shù)則提示重新分配內(nèi)存地址,接著我們就可以隨便使用。
因?yàn)槲覀兊膯?wèn)題是向量包含 bitmap 數(shù)據(jù)會(huì)傳出作用域,我們可以 動(dòng)態(tài) 分配向量,并傳入回調(diào),當(dāng) Buffer 被垃圾回收時(shí)能夠被正確刪除。
以下是新的 delete_callback,與新的 NewBuffer 調(diào)用方法。 把真實(shí)的指針傳入向量作為一個(gè)信號(hào),這樣它就能夠被正確刪除。
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());
}
}
npm install 和 npm start 運(yùn)行程序,目錄下會(huì)生成 sample.bmp 文件,和 sample.png 非常相似 - 僅僅文件大小變大了(因?yàn)?bmp 壓縮遠(yuǎn)沒(méi)有 png 高效)。
異步 Buffer 處理
接著開(kāi)發(fā)一個(gè) png 轉(zhuǎn) bitmap 轉(zhuǎn)換器的異步版本。使用 Nan::AsyncWorker 在一個(gè) C++ 線程中執(zhí)行真正的轉(zhuǎn)換方法。通過(guò)使用 Buffer 對(duì)象,我們能夠避免復(fù)制 png 數(shù)據(jù),這樣我們只需要拿到工作線程可訪問(wèn)的底層數(shù)據(jù)的指針。同樣的,工作線程產(chǎn)生的數(shù)據(jù)(bmp 向量),也能夠在不復(fù)制數(shù)據(jù)情況下用于創(chuàng)建新的 Buffer。
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));
}
我們新的 GetBMPAsync 擴(kuò)展函數(shù)首先解壓縮從 JavaScript 傳入的 buffer,接著初始化并用 NAN API 把新的 PngToBmpWorker 工作線程入隊(duì)。這個(gè)工作線程對(duì)象的 Execute 方法在轉(zhuǎn)換結(jié)束時(shí)被工作線程內(nèi)的 libuv 調(diào)用。當(dāng) Execute 函數(shù)返回,libuv 調(diào)用 Node.js 事件輪詢線程的 HandleOKCallback 方法,創(chuàng)建一個(gè) buffer 并調(diào)用 JavaScript 傳入的回調(diào)函數(shù)。
現(xiàn)在我們能夠在 JavaScript 中使用這個(gè)擴(kuò)展函數(shù)了:
png2bmp.getBMPAsync(png_buffer,
png_buffer.length,
function(bmp_buffer) {
fs.writeFileSync(bmp_file, bmp_buffer);
});
總結(jié)
本文有兩個(gè)核心賣點(diǎn):
不能忽視 V8 存儲(chǔ)單元和 C++ 變量之間的數(shù)據(jù)拷貝消耗。如果你不注意,本來(lái)你認(rèn)為把工作丟進(jìn) C++ 里執(zhí)行可以提高的性能,就又被輕易消耗了。
Buffer 提供了一個(gè)在 JavaScript 和 C++ 共享數(shù)據(jù)的方法,這樣避免了數(shù)據(jù)拷貝。
我希望通過(guò)旋轉(zhuǎn) ASCII 文本的簡(jiǎn)單例子,和同步與異步進(jìn)行圖片轉(zhuǎn)換實(shí)戰(zhàn)使用 Buffer 很簡(jiǎn)單。希望本文對(duì)你提升擴(kuò)展應(yīng)用的性能有所幫助!
再次提醒,本文內(nèi)的所有代碼均能在 https://github.com/freezer333/nodecpp-demo 中找到,位于 "buffers" 目錄下。
如果你正在尋找關(guān)于如何設(shè)計(jì) Node.js 的 C++ 擴(kuò)展的小貼士,可以訪問(wèn)我的 C++ 和 Node.js 一體化電子書。