如何编写 Node.js 扩展

JavaScript016

如何编写 Node.js 扩展,第1张

一、编写Node.js原生扩展

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

}

您好,答案如下,望采纳

ie6下的js调试工具companion js[转]2011-06-12 18:35ie6下的js调试工具companion js

做web开发的朋友都清楚,js程序的调试是相当郁闷的,因为首先这种语言语法比较灵活,它是一种弱类型的脚本语言,很多错误是无法控制的,这些不谈, 最痛苦的是没有什么好的调试工具,现在的情况比以前稍好,在Firefox下还有firebug,这的确是一个不错的js调试工具,但在IE下使用就很麻 烦,而且效果很不好,鄙人一直苦于寻找一个很好的IE下的js调试工具,能够自动捕获错误,并定位位置和原因,没想到今天竟在无意中寻找到了这么个好工 具,不敢私藏,共享出来,希望能为各位web开发者带来方便

这个工具的名字叫Companion.JS, 请注意,这可不是一个js文件,而是一个名字,它是作为ie的插件来安装使用的,而且需要结合Microsoft Script Debugger使用,通过安装这个工具,但页面出现错误时会在左上角弹出一个小错误提示,点击会在IE下面显示出一个错误控制台,就如FF下的 firebug控制台一样。错误信息提示很详细。

如下是官网的一个错误提示示例图:

官网地址:http://www.my-debugbar.com/wiki/CompanionJS/HomePage

具体使用方法为:

1、先下载Companion.JS安装文件,然后安装。

2、下载安装Microsoft Script Debugger,如果您的机器已经安装过了就可以免过这一步。

3、打开IE菜单“工具”--“Internet选项”--“高级”,找到“禁用脚本调试(Internet Explorer)”和“禁用脚本调试(在Internet Explorer之外)”,将两个选项前面的对钩都去掉,然后重启IE。

4、在Ie中输入:http://www.my-debugbar.com/wiki/uploads/CompanionJS/dummy.htm,然后点击click me链接,如果左上角弹出一个小错误提示或下面控制台出现了错误信息提示,就说明您已经安装成功了。

提示:Microsoft Script Debugger下载可到MS官网(需要经过MS的操作系统正版验证):http://www.microsoft.com/downloads/details.aspx?displaylang=zh-cn&FamilyID=E606E71F-BA7F-471E-A57D-F2216D81EC3D#filelist或到baidu里输入“Microsoft Script Debugger 下载”寻找一下。

RIP之前我们先要做的是把ISO文件打开,最简单就是用winrar打开,然后把ISO解压到硬盘上。

RIP时候碰到的情况有好几种,分开说吧。

1.dummy文件

这种是值得去掉的,它严重地浪费了我们宝贵的记忆棒空间,目前知道的有两种,一种如刀锋那种看不到的,我们对ISO重新打包后就会消失,刀锋重新打包后一下子少掉了几百兆就是这个原因。

另外一种是可见的,常见的特征是文件名为dummy或pad之类的,比如FIFA2006中的pad0.pad和pad1.pad(这种方法EA常用,极品飞车、nbalive2006和fifa2006里面的dummy文件都不小),最保险的判断方法是用winrar对文件进行压缩,如果一个很大的文件,压缩后最终只有几k,那么这个就一定是dummy文件了。更快捷的方法是压缩的时候看进度条的颜色,如果颜色一直是白色的,那么说明压缩比率很大,不用说,它就是了。另外如果你会用winrar的预测压缩率的功能,那么也可以。

这种dummy文件通常不能直接删除,保险的方法是点右键,新建一个空文本文件,或是其它文件也可以。然后把它改成和dummy文件一样的名字,记得后缀也一定要修改,然后替换掉原来那个大文件。通常情况下,如果你经常会做一些这种对文件的操作之类的,让你的电脑显示所有的文件后缀是一个很好的习惯。

dummy文件的去除基本上就是这些,还有一种更复杂的,后面再说。

下面的rip方法我个人并不喜欢,因为它会破坏游戏的完整性。不过这个很多时候还是必须的,如果你想在小棒子上运行大游戏的话

2.动画文件

其实PSP的动画文件并不算大,因为它们都采取h.264格式保存,压缩率已经很大了,通常都比较小。不过再小的文件多了的时候也就变大了。比如死神2,在去掉动画之前是六百多兆,是无法放到512MB的记忆棒里面的。

动画文件并不能直接替换成0K的文件,这样的结果大多是在播放动画的时候死机。我们可以找一些很小的PMF文件来替换。个人比较喜欢用PSP的启动动画来替换,因为它是全屏的。方法是把小的PMF文件改成我们要替换的PMF动画名,然后替换它。网上某个蜘蛛侠的rip版本就是这么制作的,所以不要以为制作者的水平高到能修改程序来调用PSP的内置动画,其实你也能做到。

3.声音文件

相对起动画文件,声音文件的rip方法更简单。但是声音文件并不好找,因为声音文件虽然都是at3格式压缩,但是后缀各种各样,线索是它通常在一些名叫MUSIC或SOUND的文件夹里面。

一些游戏的声音文件可以直接删除,不过有一些游戏的声音文件不能直接删除,但是可以用0k大小的同名文件替换,方法同dummy文件的替换了。不过还有一种,我在rip校园迷糊大王的时候碰到,声音文件必须能够播放出来,否则就会导致死机。这时候我们可以做的是对文件切割。网上有很多能够对文件切割的工具,比如我在rip的是候用的是一个随便搜索出来的海啸文件切割器,用它把文件切成很多小块,不用切割完毕,我们得到了第一块小文件后就可以中断程序了。通常切割程序会把文件名修改,我们把第一块小文件的文件名改成原来的大文件名字然后替换大文件就可以了。

4.游戏文件的去除

这个方法是严重不推荐的,以我个人来说,如果要做到这一步的话,通常我就会放弃这个游戏。这就是利用一些文件在游戏中并不会从开始就读取到的特点,把这个文件用dummy文件的替换文件替换甚至干脆删除,比如捉猴P的去掉后面的关卡地图。ssx的去掉几个位置靠后的大地图文件,32MB记忆棒玩脱狱潜龙等等都属于此类。

这种方法已经严重地破坏了文件的完整性。不到万不得已的时候还是别采用了,玩到高兴的时候死机可是件扫兴的事。

5.文件的精简

这一步对于大部分人来说已经是完全可以不看了。因为大部分人,包括我在内都没有足够的编程能力。比如实况,它能够完美精简到610MB就是用这个方法。这些游戏的文件类似我们对一批文件进行打包(基本上是属于不压缩纯打包的那种),然后游戏读取的就是这个打包后的文件。而且dummy文件也打包在里面。如果你有足够的编程能力的话,可以编写一个解包程序,然后将文件还原成原来的零散文件,进行需要的操作(这个时候就是上面1~4的方法了),最后再次打包。

RIP的方法大致就是这些,最后我们需要的是把游戏文件重新打包成ISO,这个推荐使用UMDGEN。

值得一提的是,有的文件并不能重新打包,比如大众高尔夫,重新打包,即使不经过任何修改,游戏还是一样不能运行。碰到这种游戏的话,考虑到会编程的人远远多于会对ISO进行LBA级操作的人,基本上只能放弃RIP的想法了。

----------------------------------------------------------------------

本文转载自电玩巴士