1. 程式人生 > >Node.js And C++__​10.Buffers

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.hlodepng.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);

​ 這個程式只需要fspath和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);
});