在 Node.js 和 C++ 之間使用 Buffer 共享數(shù)據(jù)

使用 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ù)流量。

CPU vs. 數(shù)據(jù)象限
CPU vs. 數(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ù)拷貝上。

為 C++ 工作線程創(chuàng)建輸入輸出拷貝
為 C++ 工作線程創(chuàng)建輸入輸出拷貝

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

從 C++ 工作線程中直接訪問(wèn) V8 數(shù)據(jù)
從 C++ 工作線程中直接訪問(wèn) V8 數(shù)據(jù)

Node.js Buffer 來(lái)救命

這里有兩個(gè)相關(guān)的問(wèn)題。

  1. 當(dāng)使用同步擴(kuò)展時(shí),除非我們不改變/產(chǎn)生數(shù)據(jù),那么可能會(huì)需要花費(fèi)大量時(shí)間在 V8 存儲(chǔ)單元和老的簡(jiǎn)單 C++ 變量之間移動(dòng)數(shù)據(jù) - 十分費(fèi)時(shí)。
  2. 當(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:BufferData 方法來(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.hlodepng.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 installnpm 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 一體化電子書。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容