Node.js是壹個強大的平臺,理想狀態下壹切都都可以用javascript寫成。然而,妳可能還會用到許多遺留的庫和系統,這樣的話使用c++編寫Node.JS擴展會是壹個不錯的註意。
以下所有例子的源代碼可在node擴展示例中找到 。
編寫Node.js C + +擴展很大程度上就像是寫V8的擴展; Node.js增加了壹些接口,但大部分時間妳都是在使原始的V8數據類型和方法,為了理解以下的代碼,妳必須首先閱讀V8引擎嵌入指南。
Javascript版本的Hello World
在講解C++版本的例子之前,先讓我們來看看在Node.js中用Javascript編寫的等價模塊是什麽樣子。這是壹個最簡單的Hello World,也不是通過HTTP,但它展示了node模塊的結構,而其接口也和大多數C++擴展要提供的接口差不多:
HelloWorldJs = function() {
this.m_count = 0;
};
HelloWorldJs.prototype.hello = function()
{
this.m_count++;
return “Hello World”;
};
exports.HelloWorldJs = HelloWorldJs;
正如妳所看到的,它使用prototype為HelloWorldJs類創建了壹個新的方法。請註意,上述代碼通過將HelloWorldJS添加到exports變量來暴露構造函數。
要在其他地方使用該模塊,請使用如下代碼:
var helloworld = require(‘helloworld_js’);
var hi = new helloworld.HelloWorldJs();
console.log(hi.hello()); // prints “Hello World” to stdout
C++版本的Hello World
要開始編寫C++擴展,首先要能夠編譯Node.js(請註意,我們使用的是Node.js 2.0版本)。本文所講內容應該兼容所有未來的0.2.x版本。壹旦編譯安裝完node,編譯模塊就不在需要額外的東西了。
完整的源代碼可以在這裏找到 。在使用Node.js或V8之前,我們需要包括相關的頭文件:
#include <v8.h>
#include <node.h>
using namespace node;
using namespace v8;
在本例子中我直接使用了V8和node的命名空間,使代碼更易於閱讀。雖然這種用法和谷歌的自己的C++編程風格指南相悖,但由於妳需要不停的使用V8定義的類型,所以目前為止的大多數node的擴展仍然使用了V8的命名空間。
接下來,聲明HelloWorld類。它繼承自node::ObjectWrap類 ,這個類提供了幾個如引用計數、在V8內部傳遞contex等的實用功能。壹般來說,所有對象應該繼承ObjectWrap:
class HelloWorld: ObjectWrap
{
private:
int m_count;
public:
聲明類之後,我們定義了壹個靜態成員函數,用來初始化對象並將其導入Node.js提供的target對象中。設個函數基本上是告訴Node.js和V8妳的類是如何創建的,和它將包含什麽方法:
static Persistent<FunctionTemplate> s_ct;
static void Init(Handle<Object> target)
{
HandleScope scope;
Local<FunctionTemplate> t = FunctionTemplate::New(New);
s_ct = Persistent<FunctionTemplate>::New(t);
s_ct->InstanceTemplate()->SetInternalFieldCount(1);
s_ct->SetClassName(String::NewSymbol(“HelloWorld”));
NODE_SET_PROTOTYPE_METHOD(s_ct, “hello”, Hello);
target->Set(String::NewSymbol(“HelloWorld”),
s_ct->GetFunction());
}
在上面這個函數中target參數將是模塊對象,即妳的擴展將要載入的地方。(譯著:這個函數將妳的對象及其方法連接到
這個模塊對象,以便外界可以訪問)首先我們為New方法創建壹個FunctionTemplate,將於稍後解釋。我們還為該對象添加壹個內部字段,並命
名為HelloWorld。然後使用NODE_SET_PROTOTYPE_METHOD宏將hello方法綁定到該對象。最後,壹旦我們建立好這個函數模板後,將他分配給target對象的HelloWorld屬性,將類暴露給用戶。
接下來的部分是壹個標準的C++構造函數:
HelloWorld() :
m_count(0)
{
}
~HelloWorld()
{
}
接下來,在::New 方法中V8引擎將調用這個簡單的C++構造函數:
static Handle<Value> New(const Arguments& args)
{
HandleScope scope;
HelloWorld* hw = new HelloWorld();
hw->Wrap(args.This());
return args.This();
}
此段代碼相當於上面Javascript代碼中使用的構造函數。它調用new HelloWorld
創造了壹個普通的C++對象,然後調用從ObjectWrap繼承的Wrap方法,
它將壹個C++HelloWorld類的引用保存到args.This()的值中。在包裝完成後返回args.This(),整個函數的行為和
javascript中的new運算符類似,返回this指向的對象。
現在我們已經建立了對象,下面介紹在Init函數中被綁定到hello的函數:
static Handle<Value> Hello(const Arguments& args)
{
HandleScope scope;
HelloWorld* hw = ObjectWrap::Unwrap<HelloWorld>(args.This());
hw->m_count++;
Local<String> result = String::New(“Hello World”);
return scope.Close(result);
}
函數中首先使用ObjectWrap模板的方法提取出指向HelloWorld類的指針,然後和javascript版本的HelloWorld壹樣遞增計數器。我們新建壹個內容為“HelloWorld”的v8字符串對象,然後在關閉本地作用域的時候返回這個字符串。
上面的代碼實際上只是針對v8的接口,最終我們還需要讓Node.js知道如何動態加載我們的代碼。為了使Node.js的擴展可以在執行時從動態鏈接庫加載,需要有壹個dlsym函數可以識別的符號,所以執行編寫如下代碼:
extern “C” {
static void init (Handle<Object> target)
{
HelloWorld::Init(target);
}
NODE_MODULE(helloworld, init);
}
由於c++的符號命名規則,我們使用extern
C,以便該符號可以被dysym識別。init方法是Node.js加載模塊後第壹個調用的函數,如果妳有多個類型,請全部在這裏初始化。
NODE_MODULE宏用來填充壹個用於存儲模塊信息的結構體,存儲的信息如模塊使用的API版本。這些信息可以用來防止未來因API不兼容導致的崩
潰。
到此,我們已經完成了壹個可用的C++ NodeJS擴展。
Node.js也提供了壹個用於構建模塊的簡單工具:
node-waf首先編寫壹個包含擴展編譯方法的wscript文件,然後執行node-waf configure &&
node-waf build完成模塊的編譯和鏈接工作。對於這個helloworld的例子來說,wscript內容如下:
def set_options(opt):
opt.tool_options(“compiler_cxx”)
def configure(conf):
conf.check_tool(“compiler_cxx”)
conf.check_tool(“node_addon”)
def build(bld):
obj = bld.new_task_gen(“cxx”, “shlib”, “node_addon”)
obj.cxxflags = [“-g”, “-D_FILE_OFFSET_BITS=64”, “-D_LARGEFILE_SOURCE”, “-Wall”]
obj.target = “helloworld”
obj.source = “helloworld.cc”
異步IO的HelloWorld
對於實際的應用來說,HelloWorld的示例太過簡單了壹些,Node.js主要的優勢是提供異步IO。
Node.js內部通過libeio將會產生阻塞的操作全都放入線程池中執行。如果需要和遺留的c庫交互,通常需要使用異步IO來為javascript
代碼提供回調接口。
通常的模式是提供壹個回調,在異步操作完成時被調用——妳可以在整個Node.js的API中看到這種模式。
Node.js的filesystem模塊提供了壹個很好的例子,其中大多數的函數都在操作完成後通過調用回調函數來傳遞數據。和許多傳統的GUI框架壹
樣,Node.js只在主線程中執行JavaScript,因此主線程以外的任何操作都不應該直接和V8或Javascript交互。
同樣helloworld_eio.cc源代碼在GitHub上。我只強調和原來HelloWorld之間的差異,其中大部分代碼保持不變,變化集中在Hello方法中:
static Handle<Value> Hello(const Arguments& args)
{
HandleScope scope;
REQ_FUN_ARG(0, cb);
HelloWorldEio* hw = ObjectWrap::Unwrap<HelloWorldEio>(args.This());
在Hello函數的入口處 ,我們使用宏從參數列表的第壹個位置獲取回調函數,在下壹節中將詳細介紹。然後,我們使用相同的Unwarp方法提取指向類對象的指針。
hello_baton_t *baton = new hello_baton_t();
baton->hw = hw;
baton->increment_by = 2;
baton->sleep_for = 1;
baton->cb = Persistent<Function>::New(cb);
這裏我們創建壹個baton結構,並將各種參數保存在裏面。請註意,我們為回調函數創建了壹個永久引用,因為我們想要在超出當前函數作用域的地方使用它。如果不這麽做,在本函數結束後將無法再調用回調函數。
hw->Ref();
eio_custom(EIO_Hello, EIO_PRI_DEFAULT, EIO_AfterHello, baton);
ev_ref(EV_DEFAULT_UC);
return Undefined();
}
如下代碼是真正的重點。首先,我們增加HelloWorld對象的引用計數,這樣在其他線程執行的時候他就不會被回收。
函數eio_custom接受兩個函數指針作為參數。EIO_Hello函數將在線程池中執行,然後EIO_AfterHello函數將回到在“主線程”
中執行。我們的baton結構也被傳遞進各函數,這些函數可以使用baton結構中的數據完成相關的操作。同時,我們也增加event
loop的引用。這很重要,因為如果event
loop無事可做,Node.js就會退出。最終,函數返回Undefined,因為真正的工作將在其他線程中完成。
static int EIO_Hello(eio_req *req)
{
hello_baton_t *baton = static_cast<hello_baton_t *>(req->data);
sleep(baton->sleep_for);
baton->hw->m_count += baton->increment_by;
return 0;
}
這個回調函數將在libeio管理的線程中執行。首先,解析出baton結構,這樣可以訪問之前設置的各種參數。然後
sheep
baton->sleep_for秒,這麽做是安全的,因為這個函數運行在獨立的線程中並不會阻塞主線程中javascript的執行。然後我們的
增計數器,在實際的系統中,這些操作通常需要使用Lock/Mutex進行同步。
當上述方法返回後,libeio將會通知主線程它需要在主線成上執行代碼,此時EIO_AfterHello將會被調用。
static int EIO_AfterHello(eio_req *req)
{
HandleScope scope;
hello_baton_t *baton = static_cast<hello_baton_t *>(req->data);
ev_unref(EV_DEFAULT_UC);
baton->hw->Unref();
進度此函數時,我們提取出baton結構,刪除事件循環的引用,並減少HelloWorld對象的引用。
Local<Value> argv[1];
argv[0] = String::New(“Hello World”);
TryCatch try_catch;
baton->cb->Call(Context::GetCurrent()->Global(), 1, argv);
if (try_catch.HasCaught()) {
FatalException(try_catch);
}
新建要傳遞給回調函數的字符串參數,並放入字符串數組中。然後我們調用回調傳遞壹個參數,並檢測可能拋出的異常。
baton->cb.Dispose();
delete baton;
return 0;
}
在執行過回調之後,應該銷毀持久引用,然後刪除之前創建的baton結構。
最後,妳可以使用如下形式在Javascript中使用該模塊:
var helloeio = require(‘./helloworld_eio’);
hi = new helloeio.HelloWorldEio();
hi.hello(function(data){
console.log(data);
});
參數傳遞與解析
除了HelloWorld之外,妳還需要理解最後壹個問題:參數的處理。在helloWorld EIO例子中,我們使用壹個REQ_FUN_ARG宏,然我們看看這個宏到底都做些什麽。
#define REQ_FUN_ARG(I, VAR) \
if (args.Length() <= (I) || !args[I]->IsFunction()) \
return ThrowException(Exception::TypeError( \
String::New(“Argument ” #I ” must be a function”))); \
Local<Function> VAR = Local<Function>::Cast(args[I]);
就像Javascript中的argument變量,v8使用數組傳遞所有的參數。由於沒有嚴格的類型限制,所以傳遞給函數的參數數目可能和期待的不同。為了對用戶友好,使用如下的宏檢測壹下參數數組的長度並判斷參數是否是正確的類型。如果傳遞了錯誤的參數類型,該宏將會拋出TypeError異常。為簡化參數的解析,目前為止大多數的Node.js擴展都有壹些本地作用域內的宏,用於特定類型參數的檢測。
二、揭秘node.js事件
要使用NodeJS,妳需要知道壹個重要的東西:事件(events)。Node中有很多對象都可以觸發事件,Node
的文檔中有很多示例。但文檔也許並不能清晰的講解如何編寫自定義事件以及監聽函數。對於壹些簡單的程序妳可以不使用自定義事件,但這樣很難應對復雜的應
用。那麽如何編寫自定義事件?首先需要了解的是在node.js中的’events’模塊。
快速概覽
要訪問此模塊,只需使用如下語句:
require(‘events’)
requires(‘events’).EventEmitter
特別說明,node中所有能觸發事件的對象基本上都是後者的實例。讓我們創建壹個簡單的演示程序Dummy:
dummy.js
view plaincopy to clipboardprint?
// basic imports
var events = require(‘events’);
// for us to do a require later
module.exports = Dummy;
function Dummy() {
events.EventEmitter.call(this);
}
10.
11. // inherit events.EventEmitter
12. Dummy.super_ = events.EventEmitter;
13. Dummy.prototype = Object.create(events.EventEmitter.prototype, {
14. constructor: {
15. value: Dummy,
16. enumerable: false
17. }
18. });
// basic imports
var events = require(‘events’);
// for us to do a require later
module.exports = Dummy;
function Dummy() {
events.EventEmitter.call(this);
}
// inherit events.EventEmitter
Dummy.super_ = events.EventEmitter;
Dummy.prototype = Object.create(events.EventEmitter.prototype, {
constructor: {
value: Dummy,
enumerable: false
}
});
上述代碼中重點展示如何使用EventEmitter擴充對象,並從中繼承所有的原型對象,方法…等等。
現在,我們假設Dummy有壹個cooking()的方法,壹旦把食物做熟之後它會觸發’cooked’事件,並調用壹個名為’eat’的回調函數。
dummy-cooking.js
view plaincopy to clipboardprint?
Dummy.prototype.cooking = function(chicken) {
var self = this;
self.chicken = chicken;
self.cook = cook(); // assume dummy function that’ll do the cooking
self.cook(chicken, function(cooked_chicken) {
self.chicken = cooked_chicken;
self.emit(‘cooked’, self.chicken);
});
10. return self;
11. }
Dummy.prototype.cooking = function(chicken) {
var self = this;
self.chicken = chicken;
self.cook = cook(); // assume dummy function that’ll do the cooking
self.cook(chicken, function(cooked_chicken) {
self.chicken = cooked_chicken;
self.emit(‘cooked’, self.chicken);
});
return self;
}