首頁  >  文章  >  web前端  >  C/C++寫node.js原生模組詳解

C/C++寫node.js原生模組詳解

零下一度
零下一度原創
2017-07-09 10:18:551781瀏覽

這篇文章主要為大家介紹了關於利用C/C++編寫node.js原生模組的相關資料,文中將實現的步驟一步步的介紹的非常詳細,對大家具有一定的參考學習價值,需要的朋友們下面跟著小編一起看看吧。

前言

一直想了解一下使用C/C++編寫nodejs原生模組,從網路上找到的博客,大多停留在如何搭建環境,然後一個Hello World完事。連更多的參考資料也沒有。於是就自己整理了一下,分享於此。

至於準備環境什麼的,網路上一抓一大把,就不再詳述 。

主要參考兩個地方:

  • #nodejs官方文件

  • v8文檔

其中第一個是nodejs的官方文檔,裡面介紹了幾個不錯的參考範例。

第二個是v8引擎的文檔,c++的,寫c++模組主要看這個文檔。

好了,我們開始幾個例子,逐步的了解如何使用c++寫nodejs模組。

Hello World

不能免俗,第一個先上來寫個Hello World吧,畢竟程式設計師認識的第一個程式就是Hello World。


#include <node.h>
void hello(const v8::FunctionCallbackInfo<v8::Value> &args) {
 v8::Isolate *isolate = args.GetIsolate();
 auto message = v8::String::NewFromUtf8(isolate, "Hello World!");
 args.GetReturnValue().Set(message);
}
void Initialize(v8::Local<v8::Object> exports) {
 NODE_SET_METHOD(exports, "hello", hello);
}
NODE_MODULE(module_name, Initialize)

好了,這是最簡單的一個HelloWorld,我們將文件命名為addon.cc,我們使用node-gyp編譯一下,然後在我們的js文件中直接使用require引入模組,然後就可以呼叫了。


const myAddon = require(&#39;./build/Release/addon&#39;) ;
console.log(myAddon.hello());

如無意外,將會在終端機上列印Hello World!。

我們簡單來看程式碼,第一行#include 7f7d395e5ec8b32ec491af5a6adcd1ae是C++中引入node.h頭檔的程式碼。頭檔可理解為接口,我們在裡面只定義了接口方法,並未實現,然後透過其他文件實現,C++連結器負責將這兩個連結在一起。

然後定義了一個方法hello() ,沒有回傳值。方法參數透過const v8::FunctionCallbackInfoefcceec664dd4e974e6278d25693ab6b &args傳遞,注意,這裡我們加了v8::前綴註解,也可以直接在檔案開始使用using v8;這樣就可以不用每次都使用這個註解了。

v8::Isolate *isolate = args.GetIsolate();這裡,我們在函式中存取了javascript的作用域。

auto message = v8::String::NewFromUtf8(isolate, "Hello World!");我們建立了一個字串類型的變量,賦值Hello World!並將其綁定到作用域。

我們透過args.GetReturnValue()取得了我們函數的回傳值

Initialize()方法用於初始化模組方法,將方法和要導出的模組的方法名稱進行綁定。

最後NODE_MODULE導出這個模組。

上面這個範例很簡單,如果是js程式碼的話:


&#39;use strict&#39;;
let hello = function hello() {
 let message = "Hello World!";
 return message;
};
module.exports = {
 hello:hello
};

好了,第一個HelloWorld就結束了。網路上很多介紹nodejs C++模組的部落格文章,到這裡就結束了。看完之後,一臉懵逼,啥啊這是?我想再寫個傳參數,並對參數做簡單操作的方法該怎麼寫?

sum(a,b)

好。那我們就再寫一個sum(a,b)函數,傳遞兩個數字類型參數a,b,並求兩個參數的和回傳。

js中程式碼簡單到下:


let sum = function(a,b){
 if(typeof a == &#39;number&#39; && typeof b == &#39;number&#39;){
  return a + b;
 }else{
  throw new Error(&#39;参数类型错误&#39;);
 }
}

那麼,C++該如何寫:


void sum(const FunctionCallbackInfo<Value> &args) {
 Isolate *isolate = args.GetIsolate();
 if(!args[0]->IsNumber()){
  isolate->ThrowException(v8::Exception::TypeError(
   v8::String::NewFromUtf8(isolate, "args[0] must be a number")));
  return ;
 }
 if(!args[1]->IsNumber()){
  isolate->ThrowException(v8::Exception::TypeError(
   v8::String::NewFromUtf8(isolate, "args[1] must be a number")));
  return ;
 }
 args.GetReturnValue().Set(args[0]->NumberValue() + args[1]->NumberValue());
}

先判斷兩個參數是否為Number型,如果不是,直接拋出例外。如果是,則將返回值設為兩個參數的和。

這裡我們並沒有在參數列表中,直接使用a,b作為參數,而是直接使用 args 物件。 這和js是類似的,第一個參數是 args[0] ,第二個參數是 args[1]

呼叫IsNumber()來判斷是否是數字型別。如果不是,請拋出一個TypeError類型錯誤異常。
如果型別沒問題,使用args[0]->NumberValue()取得參數的數字值,然後相加,並賦值給回傳值。

可能你會問,args[0] 這是個啥?它的IsNumber()方法又是怎麼來的?哪裡有文件可以查閱呢?

這裡其實是v8引擎內部型,基本和js的內建物件是一一對應的。可以查閱v8類型說明文件。

上面這個圖是不是很熟悉,和js的型別系統特別像。

js的Array,Date,Function,String等等都是繼承自Object,而v8引擎內部,Object和Primitive都是繼承自Value型別。

这里的IsNumber()方法就是Value类型的方法。那么除了这个方法,还有什么方法呢?

上面这张图,我只是截了一小部分,全部的可以直接去查阅文档。看,这里有各种方法,判断是否是数字类型的IsNumber(),判断是否是日期类型的IsDate() ,判断是否是数组的IsArray()方法等等。

v8的接口实现的也很完善了,即使并不精通C++的开发者也可以照猫画虎的实现个简单的模块。

args[0]->NumberValue()返回的是一个double的值,是的,这里是实打实的C++里的double类型,可以直接进行加减运算的。类似的还有BooleanValue()方法等等,都是获取不同类型的值使用的方法。

第二个例子中,我们简单实现了一个sum()方法,传递两个参数,求和。但是这里涉及到的只是整型的值,那如果有其他类型的值怎么办呢?比如数组。

sumOfArray(array)

下面将方法升级一下,传递一个数组,然后求数组中所有值的和。js的话:


let sumOfArray = function(array){
 if(!Array.isArray(array)){
  throw new Error(&#39;参数错误,必须为Array类型&#39;);
 }
 let sum = 0;
 for(let item of array){
  sum += item;
 }
 return sum;
}

逻辑很简单,就是将传过来的数组进行遍历一遍,然后将所有项累加即可。C++也是如此:


void sumOfArray(const FunctionCallbackInfo<Value> &args){
 Isolate *isolate = args.GetIsolate();
 if(!args[0]->IsArray()){
  isolate->ThrowException(v8::Exception::TypeError(
   v8::String::NewFromUtf8(isolate, "args[0] must be an Array")));
  return ;
 }
 Local<Object> received_v8_obj = args[0]->ToObject();
 Local<Array> keys = received_v8_obj->GetOwnPropertyNames();
 int length = keys->Length();
 double sum = 0;
 for(int i=0;i<length;i++){
  sum += received_v8_obj->Get(keys->Get(i))->NumberValue();
 }
 args.GetReturnValue().Set(sum);
}

先判断是否是数组,没什么问题。

然后我们定义了一个Object类型的received_v8_obj属性,将其赋值为args[0]->ToObject() 。这里调用ToObject()方法将其转换为一个对象。

然后调用这个对象的GetOwnPropertyNames()方法获取所有的键,然后根据键获取对象的值,进行累加。

为什么不直接将其转换为数组,然后进行遍历呢?

我们都知道,js中的数组并不是真正的数组,其实质还是对象。其内部都是键值对存储的。因此这里也是一样,Value类型并不提供直接转换为数组的ToArray()方法,而是将其转换为Object对象,通过对象的形式进行操作。

那么对象有哪些操作呢,看文档。

但是你会发现,v8确实有个Array类,继承自Object类。那么Array有什么方法呢?

看文档就知道了,少的可怜:

所以,对数组的操作都将转换为对象操作。

createObj()

说到对象了,那么我们就来写一个创建对象的方法。传递两个参数,一个name,一个age,创建一个对象,表示一个人,名叫啥,多大年纪。


void createObj(const FunctionCallbackInfo<Value> &args){
  Isolate *isolate = args.GetIsolate();
  Local<Object> obj = Object::New(isolate);
  obj->Set(String::NewFromUtf8(isolate,"name"),args[0]->ToString());
  obj->Set(String::NewFromUtf8(isolate,"age"),args[1]->ToNumber());
  args.GetReturnValue().Set(obj);
}

这个方法,参照文档,基本没啥可说的。

通过Object::New(isolate)创建一个对象,然后设置两个属性name,age,将参数依次赋值给这两个属性,然后返回这个对象即可。

如果用js写:


let createObj = function(name,age){
  let obj = {};
  obj.name = name;
  obj.age = age;
  return obj;
};

callback

上面说的,都没提到js中一个重要的东西,回调函数。如果参数中传一个回调函数,那么我们该如何执行呢?

来一个简单的例子。


let cb = function(a,b,fn){
  if(typeof a !== &#39;number&#39; || typeof b !== &#39;number&#39;){
    throw new Error(&#39;参数类型错误,只能是Number类型&#39;);
  }
  if(typeof fn !== &#39;function&#39;){
    throw new Error(&#39;参数fn类型错误,只能是Function类型&#39;);
  }
  fn(a,b);
};

这个例子很简单,我们传两个数字类型参数a,b和一个回调函数fn,然后将a,b作为fn的参数调用fn回调函数。这里我们对a,b的操作转交给回调函数。回调函数里我们可以求和,也可以求积,随你。

这个例子中,暂时还没涉及到的是如何调用回调函数。

先上代码:


void cb(const FunctionCallbackInfo<Value> &args){
  Isolate *isolate = args.GetIsolate();
  if(!args[0]->IsNumber()){
    isolate->ThrowException(v8::Exception::TypeError(
      v8::String::NewFromUtf8(isolate, "args[0] must be a Number")));
  }
  if(!args[1]->IsNumber()){
    isolate->ThrowException(v8::Exception::TypeError(
      v8::String::NewFromUtf8(isolate, "args[1] must be a Number")));
  }
  if(!args[2]->IsFunction()){
    isolate->ThrowException(v8::Exception::TypeError(
      v8::String::NewFromUtf8(isolate, "args[2] must be a Function")));
  }
  Local<Function> jsfn = Local<Function>::Cast(args[2]);
  Local<Value> argv[2] = { Number::New(isolate,args[0]->NumberValue()),Number::New(isolate,args[1]->NumberValue())};
  Local<Value> c = jsfn->Call(Null(isolate),2,argv);
  args.GetReturnValue().Set(c);
}

上面三个判断参数类型,略过。

我们定义一个Function类型属性jsfn,将args[2]强制转换为Function并赋值给jsfn。

然后定义一个具有两个值的参数argv,这两个值就是args[0] , args[1]的数字值。

然后通过jsfn->Call(Null(isolate),2,argv)调用回调函数。

argv是一个数组,其个数我们在定义时指定,2个。

Call()方法为函数类型的值进行调用的方法。

Local< Value > | Call (Handle< Value > recv, int argc, Handle< Value > argv[])

查阅文档,可以看出,Call()方法传3个参数,第一个参数是执行上下文,用于绑定代码执行时的this,第二个参数为参数个数,第三个为参数列表,数组形式。

上面几个例子,只是冰山一角,连一角都算不上。只为了解一下nodejs使用C/C++编写原生模块,如果要编写一个可用的,高性能的C模块,那么,要求程序员一定要精通C/C++,并且对js底层也很精通,包括v8和libuv等等。

以上是C/C++寫node.js原生模組詳解的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn