深入浅出Node.js2.4 C/C++扩展模块_深入浅出Node.js2.4 C/C++扩展模块试读-查字典图书网
查字典图书网
当前位置: 查字典 > 图书网 > 编程 > 深入浅出Node.js > 2.4 C/C++扩展模块

深入浅出Node.js——2.4 C/C++扩展模块

对于前端工程师来说,C/C++扩展模块或许比较生疏和晦涩,但是如果你了解了它,在模块出现性能瓶颈时将会对你有极大的帮助。 JavaScript的一个典型弱点就是位运算。JavaScript的位运算参照Java的位运算实现,但是Java位运算是在int型数字的基础上进行的,而JavaScript中只有double型的数据类型,在进行位运算的过程中,需要将double型转换为int型,然后再进行。所以,在JavaScript层面上做位运算的效率不高。 在应用中,会频繁出现位运算的需求,包括转码、编码等过程,如果通过JavaScript来实现,CPU资源将会耗费很多,这时编写C/C++扩展模块来提升性能的机会来了。 C/C++扩展模块属于文件模块中的一类。前面讲述文件模块的编译部分时提到,C/C++模块通过预先编译为.node文件,然后调用process.dlopen()方法加载执行。在这一节中,我们将分析整个C/C++扩展模块的编写、编译、加载、导出的过程。 在开始编写扩展模块之前,需要强调的一点是,Node的原生模块一定程度上是可以跨平台的,其前提条件是源代码可以支持在*nix和Windows上编译,其中*nix下通过g++/gcc等编译器编译为动态链接共享对象文件(.so),在Windows下则需要通过Visual C++的编译器编译为动态链接库文件(.dll),如图2-6所示。这里有一个让人迷惑的地方,那就是引用加载时却是.node文件。其实.node的扩展名只是为了看起来更自然一点,不会因为平台差异产生不同的感觉。实际上,在Windows下它是一个.dll文件,在*nix下则是一个.so文件。为了实现跨平台,dlopen()方法在内部实现时区分了平台,分别用的是加载.so和.dll的方式。图2-6为扩展模块在不同平台上编译和加载的详细过程。 值得注意的是,一个平台下的.node文件在另一个平台下是无法加载执行的,必须重新用各自平台下的编译器编译为正确的.node文件。 图2-6 扩展模块不同平台上的编译和加载过程 2.4.1 前提条件 如果想要编写高质量的C/C++扩展模块,还需要深厚的C/C++编程功底才行。除此之外,以下这些条目都是不能避开的,在了解它们之后,可以让你在编写过程中事半功倍。 GYP项目生成工具。在Node 0.6中,第三方模块通过它自身提供的node_waf工具实现编译,但是它是*nix平台下的产物,无法实现跨平台编译。在Node 0.8中,Node决定摒弃掉node_waf而采用跨平台效果更好的项目生成器,它就是GYP工具,即“Generate Your Projects”短句的缩写。它的好处在于,可以帮助你生成各个平台下的项目文件,比如Windows下的Visual Studio解决方案文件(.sln)、Mac下的XCode项目配置文件以及Scons工具。在这个基础上,再动用各自平台下的编译器编译项目。这大大减少了跨平台模块在项目组织上的精力投入。 Node源码中一度出现过各种项目文件,后来均统一为GYP工具。这除了可以减少编写跨平台项目文件的工作量外,另一个简单的原因就是Node自身的源码就是通过GYP编译的。为此,Nathan Rajlich基于GYP为Node提供了一个专有的扩展构建工具node-gyp,这个工具通过npm install -g node-gyp这个命令即可安装。 V8引擎C++库。V8是Node自身的动力来源之一。它自身由C++写成,可以实现JavaScript与C++的互相调用。 libuv库。它是Node自身的动力来源之二。Node能够实现跨平台的一个诀窍就是它的libuv库,这个库是跨平台的一层封装,通过它去调用一些底层操作,比自己在各个平台下编写实现要高效得多。libuv封装的功能包括事件循环、文件操作等。 Node内部库。写C++模块时,免不了要做一些面向对象的编程工作,而Node自身提供了一些C++代码,比如node::ObjectWrap类可以用来包装你的自定义类,它可以帮助实现对象回收等工作。 其他库。其他存在deps目录下的库在编写扩展模块时也许可以帮助你,比如zlib、openssl、http_parser等。 2.4.2 C/C++扩展模块的编写 在介绍C/C++内建模块时,其实已经介绍了C/C++模块的编写方式。普通的扩展模块与内建模块的区别在于无须将源代码编译进Node,而是通过dlopen()方法动态加载。所以在编写普通的扩展模块时,无须将源代码写进node命名空间,也不需要提供头文件。下面我们将采用同一个例子来介绍C/C++扩展模块的编写。 它的JavaScript原型代码与前面的例子一样: exports.sayHello = function () { return 'Hello world!'; }; 新建hello目录作为自己的项目位置,编写hello.cc并将其存储到src目录下,相关代码如下: #include <node.h> #include <v8.h> using namespace v8; // 实现预定义的方法 Handle<Value> SayHello(const Arguments& args) { HandleScope scope; return scope.Close(String::New("Hello world!")); } // 给传入的目标对象添加sayHello()方法 void Init_Hello(Handle<Object> target) { target->Set(String::NewSymbol("sayHello"), FunctionTemplate::New(SayHello)->GetFunction()); } // 调用NODE_MODULE()方法将注册方法定义到内存中 NODE_MODULE(hello, Init_Hello) C/C++扩展模块与内建模块的套路一样,将方法挂载在target对象上,然后通过NODE_MODULE声明即可。 由于不像编写内建模块那样将对象声明到node_module_list链表中,所以无法被认作是一个原生模块,只能通过dlopen()来动态加载,然后导出给JavaScript调用。 2.4.3 C/C++扩展模块的编译 在GYP工具的帮助下,C/C++扩展模块的编译是一件省心的事情,无须为每个平台编写不同的项目编译文件。写好.gyp项目文件是除编码外的头等大事,然而你也无须担心此事太难,因为.gyp项目文件是足够简单的。node-gyp约定.gyp文件为binding.gyp,其内容如下所示: { 'targets': [ { 'target_name': 'hello', 'sources': [ 'hello.cc' ], 'conditions': [ ['OS == "win"', { 'libraries': ['-lnode.lib'] } ] ] } ] } 然后调用: $ node-gyp configure 会得到如下的输出结果: gyp info it worked if it ends with ok gyp info using node-gyp@0.8.3 gyp info using node@0.8.14 | darwin | x64 gyp info spawn python gyp info spawn args [ '/usr/local/lib/node_modules/node-gyp/gyp/gyp', gyp info spawn args 'binding.gyp', gyp info spawn args '-f', gyp info spawn args 'make', gyp info spawn args '-I', gyp info spawn args '/Users/jacksontian/git/diveintonode/examples/02/addon/build/config.gypi', gyp info spawn args '-I', gyp info spawn args '/usr/local/lib/node_modules/node-gyp/addon.gypi', gyp info spawn args '-I', gyp info spawn args '/Users/jacksontian/.node-gyp/0.8.14/common.gypi', gyp info spawn args '-Dlibrary=shared_library', gyp info spawn args '-Dvisibility=default', gyp info spawn args '-Dnode_root_dir=/Users/jacksontian/.node-gyp/0.8.14', gyp info spawn args '-Dmodule_root_dir=/Users/jacksontian/git/diveintonode/examples/02/addon', gyp info spawn args '--depth=.', gyp info spawn args '--generator-output', gyp info spawn args 'build', gyp info spawn args '-Goutput_dir=.' ] gyp info ok node-gyp configure这个命令会在当前目录中创建build目录,并生成系统相关的项目文件。 在*nix平台下,build目录中会出现Makefile等文件;在Windows下,则会生成vcxproj等文件。 继续执行如下代码: $ node-gyp build 会得到如下的输出结果: gyp info it worked if it ends with ok gyp info using node-gyp@0.8.3 gyp info using node@0.8.14 | darwin | x64 gyp info spawn make gyp info spawn args [ 'BUILDTYPE=Release', '-C', 'build' ] CXX(target) Release/obj.target/hello/hello.o SOLINK_MODULE(target) Release/hello.node SOLINK_MODULE(target) Release/hello.node: Finished gyp info ok 编译过程会根据平台不同,分别通过make或vcbuild进行编译。编译完成后,hello.node文件会生成在build/Release目录下。 2.4.4 C/C++扩展模块的加载 得到hello.node结果文件后,如何调用扩展模块其实在前面已经提及。require()方法通过解析标识符、路径分析、文件定位,然后加载执行即可。下面的代码引入前面编译得到的.node文件,并调用执行其中的方法: var hello = require('./build/Release/hello.node'); console.log(hello.sayHello()); 以上代码存为hello.js,调用node hello.js命令即可得到如下的输出结果: Hello world! 对于以.node为扩展名的文件,Node将会调用process.dlopen()方法去加载文件: //Native extension for .node Module._extensions['.node'] = process.dlopen; 对于调用者而言,require()是轻松愉快的。对于扩展模块的编写者来说,process.dlopen()中隐含的过程值得了解一番。 如图2-7所示,require()在引入.node文件的过程中,实际上经历了4个层面上的调用。 加载.node文件实际上经历了两个步骤,第一个步骤是调用uv_dlopen()方法去打开动态链接库,第二个步骤是调用uv_dlsym()方法找到动态链接库中通过NODE_MODULE宏定义的方法地址。这两个过程都是通过libuv库进行封装的:在*nix平台下实际上调用的是dlfcn.h头文件中定义的dlopen()和dlsym()两个方法;在Windows平台则是通过LoadLibraryExW()和GetProcAddress()这两个方法实现的,它们分别加载.so和.dll文件(实际为.node文件)。 图2-7 require()引入.node文件的过程 这里对libuv函数的调用充分表现Node利用libuv实现跨平台的方式,这样的情景在很多地方还会出现。 由于编写模块时通过NODE_MODULE将模块定义为node_module_struct结构,所以在获取函数地址之后,将它映射为node_module_struct结构几乎是无缝对接的。接下来的过程就是将传入的exports对象作为实参运行,将C++中定义的方法挂载在exports对象上,然后调用者就可以轻松调用了。 C/C++扩展模块与JavaScript模块的区别在于加载之后不需要编译,直接执行之后就可以被外部调用了,其加载速度比JavaScript模块略快。 使用C/C++扩展模块的一个好处在于可以更灵活和动态地加载它们,保持Node模块自身简单性的同时,给予Node无限的可扩展性。 关于node-gyp工具的更多细节可以参见https://github.com/TooTallNate/node-gyp(作者为Nathan Rajlich,Node源码的核心贡献者之一)。

展开全文

推荐文章

猜你喜欢

附近的人在看

推荐阅读

拓展阅读

《深入浅出Node.js》其他试读目录

• 1.1 Node的诞生历程
• 1.2 Node的命名与起源
• 1.3 Node给JavaScript带来的意义
• 1.4 Node的特点
• 1.5 Node的应用场景
• 1.6 Node的使用者
• 1.7 参考资源
• 2.1 CommonJS规范
• 2.2 Node的模块实现
• 2.3 核心模块
• 2.4 C/C++扩展模块 [当前]
• 2.5 模块调用栈
• 2.6 包与NPM
• 2.7 前后端共用模块
• 2.8 总结
• 2.9 参考资源