3 回答

TA貢獻(xiàn)1993條經(jīng)驗(yàn) 獲得超6個(gè)贊
一、編寫(xiě)Node.js原生擴(kuò)展
Node.js是一個(gè)強(qiáng)大的平臺(tái),理想狀態(tài)下一切都都可以用javascript寫(xiě)成。然而,你可能還會(huì)用到許多遺留的庫(kù)和系統(tǒng),這樣的話使用c++編寫(xiě)Node.JS擴(kuò)展會(huì)是一個(gè)不錯(cuò)的注意。
以下所有例子的源代碼可在node擴(kuò)展示例中找到 。
編寫(xiě)Node.js C + +擴(kuò)展很大程度上就像是寫(xiě)V8的擴(kuò)展; Node.js增加了一些接口,但大部分時(shí)間你都是在使原始的V8數(shù)據(jù)類(lèi)型和方法,為了理解以下的代碼,你必須首先閱讀V8引擎嵌入指南。
Javascript版本的Hello World
在講解C++版本的例子之前,先讓我們來(lái)看看在Node.js中用Javascript編寫(xiě)的等價(jià)模塊是什么樣子。這是一個(gè)最簡(jiǎn)單的Hello World,也不是通過(guò)HTTP,但它展示了node模塊的結(jié)構(gòu),而其接口也和大多數(shù)C++擴(kuò)展要提供的接口差不多:
HelloWorldJs = function() {
this.m_count = 0;
};
HelloWorldJs.prototype.hello = function()
{
this.m_count++;
return “Hello World”;
};
exports.HelloWorldJs = HelloWorldJs;
正如你所看到的,它使用prototype為HelloWorldJs類(lèi)創(chuàng)建了一個(gè)新的方法。請(qǐng)注意,上述代碼通過(guò)將HelloWorldJS添加到exports變量來(lái)暴露構(gòu)造函數(shù)。
要在其他地方使用該模塊,請(qǐng)使用如下代碼:
var helloworld = require(‘helloworld_js’);
var hi = new helloworld.HelloWorldJs();
console.log(hi.hello()); // prints “Hello World” to stdout
C++版本的Hello World
要開(kāi)始編寫(xiě)C++擴(kuò)展,首先要能夠編譯Node.js(請(qǐng)注意,我們使用的是Node.js 2.0版本)。本文所講內(nèi)容應(yīng)該兼容所有未來(lái)的0.2.x版本。一旦編譯安裝完node,編譯模塊就不在需要額外的東西了。
完整的源代碼可以在這里找到 。在使用Node.js或V8之前,我們需要包括相關(guān)的頭文件:
#include <v8.h>
#include <node.h>
using namespace node;
using namespace v8;
在本例子中我直接使用了V8和node的命名空間,使代碼更易于閱讀。雖然這種用法和谷歌的自己的C++編程風(fēng)格指南相悖,但由于你需要不停的使用V8定義的類(lèi)型,所以目前為止的大多數(shù)node的擴(kuò)展仍然使用了V8的命名空間。
接下來(lái),聲明HelloWorld類(lèi)。它繼承自node::ObjectWrap類(lèi) ,這個(gè)類(lèi)提供了幾個(gè)如引用計(jì)數(shù)、在V8內(nèi)部傳遞contex等的實(shí)用功能。一般來(lái)說(shuō),所有對(duì)象應(yīng)該繼承ObjectWrap:
class HelloWorld: ObjectWrap
{
private:
int m_count;
public:
聲明類(lèi)之后,我們定義了一個(gè)靜態(tài)成員函數(shù),用來(lái)初始化對(duì)象并將其導(dǎo)入Node.js提供的target對(duì)象中。設(shè)個(gè)函數(shù)基本上是告訴Node.js和V8你的類(lèi)是如何創(chuàng)建的,和它將包含什么方法:
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());
}
在上面這個(gè)函數(shù)中target參數(shù)將是模塊對(duì)象,即你的擴(kuò)展將要載入的地方。(譯著:這個(gè)函數(shù)將你的對(duì)象及其方法連接到
這個(gè)模塊對(duì)象,以便外界可以訪問(wèn))首先我們?yōu)镹ew方法創(chuàng)建一個(gè)FunctionTemplate,將于稍后解釋。我們還為該對(duì)象添加一個(gè)內(nèi)部字段,并命
名為HelloWorld。然后使用NODE_SET_PROTOTYPE_METHOD宏將hello方法綁定到該對(duì)象。最后,一旦我們建立好這個(gè)函數(shù)模板后,將他分配給target對(duì)象的HelloWorld屬性,將類(lèi)暴露給用戶。
接下來(lái)的部分是一個(gè)標(biāo)準(zhǔn)的C++構(gòu)造函數(shù):
HelloWorld() :
m_count(0)
{
}
~HelloWorld()
{
}
接下來(lái),在::New 方法中V8引擎將調(diào)用這個(gè)簡(jiǎn)單的C++構(gòu)造函數(shù):
static Handle<Value> New(const Arguments& args)
{
HandleScope scope;
HelloWorld* hw = new HelloWorld();
hw->Wrap(args.This());
return args.This();
}
此段代碼相當(dāng)于上面Javascript代碼中使用的構(gòu)造函數(shù)。它調(diào)用new HelloWorld
創(chuàng)造了一個(gè)普通的C++對(duì)象,然后調(diào)用從ObjectWrap繼承的Wrap方法,
它將一個(gè)C++HelloWorld類(lèi)的引用保存到args.This()的值中。在包裝完成后返回args.This(),整個(gè)函數(shù)的行為和
javascript中的new運(yùn)算符類(lèi)似,返回this指向的對(duì)象。
現(xiàn)在我們已經(jīng)建立了對(duì)象,下面介紹在Init函數(shù)中被綁定到hello的函數(shù):
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);
}
函數(shù)中首先使用ObjectWrap模板的方法提取出指向HelloWorld類(lèi)的指針,然后和javascript版本的HelloWorld一樣遞增計(jì)數(shù)器。我們新建一個(gè)內(nèi)容為“HelloWorld”的v8字符串對(duì)象,然后在關(guān)閉本地作用域的時(shí)候返回這個(gè)字符串。
上面的代碼實(shí)際上只是針對(duì)v8的接口,最終我們還需要讓Node.js知道如何動(dòng)態(tài)加載我們的代碼。為了使Node.js的擴(kuò)展可以在執(zhí)行時(shí)從動(dòng)態(tài)鏈接庫(kù)加載,需要有一個(gè)dlsym函數(shù)可以識(shí)別的符號(hào),所以執(zhí)行編寫(xiě)如下代碼:
extern “C” {
static void init (Handle<Object> target)
{
HelloWorld::Init(target);
}
NODE_MODULE(helloworld, init);
}
由于c++的符號(hào)命名規(guī)則,我們使用extern
C,以便該符號(hào)可以被dysym識(shí)別。init方法是Node.js加載模塊后第一個(gè)調(diào)用的函數(shù),如果你有多個(gè)類(lèi)型,請(qǐng)全部在這里初始化。
NODE_MODULE宏用來(lái)填充一個(gè)用于存儲(chǔ)模塊信息的結(jié)構(gòu)體,存儲(chǔ)的信息如模塊使用的API版本。這些信息可以用來(lái)防止未來(lái)因API不兼容導(dǎo)致的崩
潰。
到此,我們已經(jīng)完成了一個(gè)可用的C++ NodeJS擴(kuò)展。
Node.js也提供了一個(gè)用于構(gòu)建模塊的簡(jiǎn)單工具:
node-waf首先編寫(xiě)一個(gè)包含擴(kuò)展編譯方法的wscript文件,然后執(zhí)行node-waf configure &&
node-waf build完成模塊的編譯和鏈接工作。對(duì)于這個(gè)helloworld的例子來(lái)說(shuō),wscript內(nèi)容如下:
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
對(duì)于實(shí)際的應(yīng)用來(lái)說(shuō),HelloWorld的示例太過(guò)簡(jiǎn)單了一些,Node.js主要的優(yōu)勢(shì)是提供異步IO。
Node.js內(nèi)部通過(guò)libeio將會(huì)產(chǎn)生阻塞的操作全都放入線程池中執(zhí)行。如果需要和遺留的c庫(kù)交互,通常需要使用異步IO來(lái)為javascript
代碼提供回調(diào)接口。
通常的模式是提供一個(gè)回調(diào),在異步操作完成時(shí)被調(diào)用——你可以在整個(gè)Node.js的API中看到這種模式。
Node.js的filesystem模塊提供了一個(gè)很好的例子,其中大多數(shù)的函數(shù)都在操作完成后通過(guò)調(diào)用回調(diào)函數(shù)來(lái)傳遞數(shù)據(jù)。和許多傳統(tǒng)的GUI框架一
樣,Node.js只在主線程中執(zhí)行JavaScript,因此主線程以外的任何操作都不應(yīng)該直接和V8或Javascript交互。
同樣helloworld_eio.cc源代碼在GitHub上。我只強(qiáng)調(diào)和原來(lái)HelloWorld之間的差異,其中大部分代碼保持不變,變化集中在Hello方法中:
static Handle<Value> Hello(const Arguments& args)
{
HandleScope scope;
REQ_FUN_ARG(0, cb);
HelloWorldEio* hw = ObjectWrap::Unwrap<HelloWorldEio>(args.This());
在Hello函數(shù)的入口處 ,我們使用宏從參數(shù)列表的第一個(gè)位置獲取回調(diào)函數(shù),在下一節(jié)中將詳細(xì)介紹。然后,我們使用相同的Unwarp方法提取指向類(lèi)對(duì)象的指針。
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);
這里我們創(chuàng)建一個(gè)baton結(jié)構(gòu),并將各種參數(shù)保存在里面。請(qǐng)注意,我們?yōu)榛卣{(diào)函數(shù)創(chuàng)建了一個(gè)永久引用,因?yàn)槲覀兿胍诔霎?dāng)前函數(shù)作用域的地方使用它。如果不這么做,在本函數(shù)結(jié)束后將無(wú)法再調(diào)用回調(diào)函數(shù)。
hw->Ref();
eio_custom(EIO_Hello, EIO_PRI_DEFAULT, EIO_AfterHello, baton);
ev_ref(EV_DEFAULT_UC);
return Undefined();
}
如下代碼是真正的重點(diǎn)。首先,我們?cè)黾親elloWorld對(duì)象的引用計(jì)數(shù),這樣在其他線程執(zhí)行的時(shí)候他就不會(huì)被回收。
函數(shù)eio_custom接受兩個(gè)函數(shù)指針作為參數(shù)。EIO_Hello函數(shù)將在線程池中執(zhí)行,然后EIO_AfterHello函數(shù)將回到在“主線程”
中執(zhí)行。我們的baton結(jié)構(gòu)也被傳遞進(jìn)各函數(shù),這些函數(shù)可以使用baton結(jié)構(gòu)中的數(shù)據(jù)完成相關(guān)的操作。同時(shí),我們也增加event
loop的引用。這很重要,因?yàn)槿绻鹐vent
loop無(wú)事可做,Node.js就會(huì)退出。最終,函數(shù)返回Undefined,因?yàn)檎嬲墓ぷ鲗⒃谄渌€程中完成。
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;
}
這個(gè)回調(diào)函數(shù)將在libeio管理的線程中執(zhí)行。首先,解析出baton結(jié)構(gòu),這樣可以訪問(wèn)之前設(shè)置的各種參數(shù)。然后
sheep
baton->sleep_for秒,這么做是安全的,因?yàn)檫@個(gè)函數(shù)運(yùn)行在獨(dú)立的線程中并不會(huì)阻塞主線程中javascript的執(zhí)行。然后我們的
增計(jì)數(shù)器,在實(shí)際的系統(tǒng)中,這些操作通常需要使用Lock/Mutex進(jìn)行同步。
當(dāng)上述方法返回后,libeio將會(huì)通知主線程它需要在主線成上執(zhí)行代碼,此時(shí)EIO_AfterHello將會(huì)被調(diào)用。
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();
進(jìn)度此函數(shù)時(shí),我們提取出baton結(jié)構(gòu),刪除事件循環(huán)的引用,并減少HelloWorld對(duì)象的引用。
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);
}
新建要傳遞給回調(diào)函數(shù)的字符串參數(shù),并放入字符串?dāng)?shù)組中。然后我們調(diào)用回調(diào)傳遞一個(gè)參數(shù),并檢測(cè)可能拋出的異常。
baton->cb.Dispose();
delete baton;
return 0;
}
在執(zhí)行過(guò)回調(diào)之后,應(yīng)該銷(xiāo)毀持久引用,然后刪除之前創(chuàng)建的baton結(jié)構(gòu)。
最后,你可以使用如下形式在Javascript中使用該模塊:
var helloeio = require(‘./helloworld_eio’);
hi = new helloeio.HelloWorldEio();
hi.hello(function(data){
console.log(data);
});
參數(shù)傳遞與解析
除了HelloWorld之外,你還需要理解最后一個(gè)問(wèn)題:參數(shù)的處理。在helloWorld EIO例子中,我們使用一個(gè)REQ_FUN_ARG宏,然我們看看這個(gè)宏到底都做些什么。
#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使用數(shù)組傳遞所有的參數(shù)。由于沒(méi)有嚴(yán)格的類(lèi)型限制,所以傳遞給函數(shù)的參數(shù)數(shù)目可能和期待的不同。為了對(duì)用戶友好,使用如下的宏檢測(cè)一下參數(shù)數(shù)組的長(zhǎng)度并判斷參數(shù)是否是正確的類(lèi)型。如果傳遞了錯(cuò)誤的參數(shù)類(lèi)型,該宏將會(huì)拋出TypeError異常。為簡(jiǎn)化參數(shù)的解析,目前為止大多數(shù)的Node.js擴(kuò)展都有一些本地作用域內(nèi)的宏,用于特定類(lèi)型參數(shù)的檢測(cè)。
二、揭秘node.js事件
要使用NodeJS,你需要知道一個(gè)重要的東西:事件(events)。Node中有很多對(duì)象都可以觸發(fā)事件,Node
的文檔中有很多示例。但文檔也許并不能清晰的講解如何編寫(xiě)自定義事件以及監(jiān)聽(tīng)函數(shù)。對(duì)于一些簡(jiǎn)單的程序你可以不使用自定義事件,但這樣很難應(yīng)對(duì)復(fù)雜的應(yīng)
用。那么如何編寫(xiě)自定義事件?首先需要了解的是在node.js中的’events’模塊。
快速概覽
要訪問(wèn)此模塊,只需使用如下語(yǔ)句:
require(‘events’)
requires(‘events’).EventEmitter
特別說(shuō)明,node中所有能觸發(fā)事件的對(duì)象基本上都是后者的實(shí)例。讓我們創(chuàng)建一個(gè)簡(jiǎn)單的演示程序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
}
});
上述代碼中重點(diǎn)展示如何使用EventEmitter擴(kuò)充對(duì)象,并從中繼承所有的原型對(duì)象,方法…等等。
現(xiàn)在,我們假設(shè)Dummy有一個(gè)cooking()的方法,一旦把食物做熟之后它會(huì)觸發(fā)’cooked’事件,并調(diào)用一個(gè)名為’eat’的回調(diào)函數(shù)。
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;
}
- 3 回答
- 0 關(guān)注
- 631 瀏覽
添加回答
舉報(bào)