未加星标

Node.js C++ Addon应用实践

字体大小 | |
[前端(javascript) 所属分类 前端(javascript) | 发布者 店小二05 | 时间 2019 | 作者 红领巾 ] 0人收藏点击收藏

近期项目中尝试使用Electron来实现跨平台桌面客户端。由于Node.js支持c++实现native addon,将c++接口封装供js调用,我们考虑将已经被多平台使用的SDK作为native扩展库引入Electron工程。

Node.js有两套API可以供c++侧选择来实现v8接口的封装,NAN和N-API。NAN是受到广泛认可(包括Node官方)的三方接口层,内部兼容各种Node版本v8接口的变动,对外提供稳定的API,免去长期以来c++开发者升级node版本需要频繁兼容c++接口的苦恼;N-API则是Node官方自行推出的新接口层,并非取代原有接口,而是旨在提供更稳定更贴近c风格的API,同样是为了避免c++开发者频繁改动,更利于生态圈的发展。由于我们在启动Electron版本的计划时,Electron的稳定版本使用的8.9.x版本的Node.js,该版本的官方文档中,N-API还处于实验阶段,权衡之后,我们在几个重要的SDK中使用成熟稳定的NAN来为我们electron桌面端提供native封装模块;后续一些新增的小模块,则尝试使用N-API来实现。

c++ addon的本质

在编写自己的addon模块之前,可以随便拿一个.node文件来探查一下它的本质究竟是什么。直接查看一个.node文件的二进制数据,结果如下

CFFAEDFE 07000001 03000000 08000000 0D000000 30070000 85800100 00000000 19000000 C8020000 5F5F5445 58540000 00000000 00000000 00000000 00000000 00D01D00 00000000 00000000 00000000 00D01D00 00000000 07000000 05000000 08000000 ...

可以看到文件头标识有0xCFFAEDFE,根据 https://opensource.apple.com/source/xnu/xnu-792/EXTERNAL_HEADERS/mach-o/loader.h 头文件中的定义

/* Constant for the magic field of the mach_header_64 (64-bit architectures) */ #define MH_MAGIC_64 0xfeedfacf /* the 64-bit mach magic number */ #define MH_CIGAM_64 NXSwapInt(MH_MAGIC_64) //0xcffaedfe (little-endian)

这串标识是苹果Mach-O文件在64位机器上的魔数,表明.node文件在mac平台上是一个Mach-O文件,我们可以通过lipo/otool/nm等工具对其结构进行查看。

$ otool -hv DemoSDK.node Mach header magic cputype cpusubtype caps filetype ncmds sizeofcmds flags MH_MAGIC_64 X86_64 ALL 0x00 BUNDLE 23 4088 NOUNDEFS DYLDLINK TWOLEVEL WEAK_DEFINES BINDS_TO_WEAK

.node文件mach header中的fileType是MH_BUNDLE,也即.node文件是一个Bundle类型的动态库。一般此类动态库用于在运行时加载,允许被作为插件动态扩展程序的行为。Bundle类型动态库的后缀名可以自行指定,生成的库若不携带资源,也可以选择不生成类似framework那样的目录结构,而只输出一个二进制文件(相当于只能显式加载的.dylib)。这就是.node模块的本质,同样在windows上,.node模块实质就是一个dll。至此,mac开发者似乎都嗅到了一股熟悉的味道。

// Loads a module at the given file path. Returns that module's // `exports` property. Module.prototype.require = function(path) { assert(path, 'missing path'); assert(typeof path === 'string', 'path must be a string'); return Module._load(path, this, /* isMain */ false); }; // Given a file name, pass it to the proper extension handler. Module.prototype.load = function(filename) { ... assert(!this.loaded); this.filename = filename; this.paths = Module._nodeModulePaths(path.dirname(filename)); var extension = path.extname(filename) || '.js'; if (!Module._extensions[extension]) extension = '.js'; Module._extensions[extension](this, filename); this.loaded = true; ... }; //Native extension for .node Module._extensions['.node'] = function(module, filename) { return process.dlopen(module, path._makeLong(filename)); }; 在node源码中找到require()方法的实现,require()调用了_load()方法,_load()方法检查该模块是否已存在缓存,如果需要执行载入,则调用真正的加载方法load()。这表明一个c++模块在整个node进程中的引入是全局唯一的,node底层会对每一个导入的c++模块进行缓存,这是高效且合理的。Module.load()方法里面主要对加载路径进行了处理。对于c++ 模块来说,会执行Module._extensions[‘.node’] 的闭包,即调用process.dlopen来动态加载模块。 // DLOpen is process.dlopen(module, filename). // Used to load 'module.node' dynamically shared objects. static void DLOpen(const FunctionCallbackInfo<Value>& args) { Environment* env = Environment::GetCurrent(args); uv_lib_t lib; ... Local<Object> module = args[0]->ToObject(env->isolate()); // Cast node::Utf8Value filename(env->isolate(), args[1]); // Cast const bool is_dlopen_error = uv_dlopen(*filename, &lib); ... }

在node.cc文件中可以看到,process.dlopen对应的c++函数为DLOpen(),这个是Node程序一启动就注册好的。DLOpen()函数实际上是调用了libuv库的uv_dlopen()方法。libuv库是个跨平台库,其unix版本代码调用了系统函数dlopen() 。顺带一提,在windows上,uv_dlopen也是调用了系统函数LoadLibraryExW()来实现c++模块的导入。

用node-gyp构建c++ addon

构建一个c++ addon模块,一般是通过node-gyp命令行工具来执行。其实早期是用另外一款工具的,不过过去的就让他过去吧…构建c++addon时,node-gyp会根据binding.gyp配置文件调用各平台上的编译工具集来进行编译,mac上是Xcode Command Line Tools,windows上则是Visual Studio。

不论是window还是mac平台上,在安装了Node环境后,都可以方便地通过npm来全局安装node-gyp

$ npm install -g node-gyp

node-gyp 的常用命令如下:

$ cd node-addon-root-directory $ node-gyp clean configure build --debug --silly #node $ node-gyp clean configure build --target=2.0.2 --arch=x64 --dist-url=https://atom.io/download/atom-shell #electron

clean: 用于清空build缓存

configure: 根据binding.gyp文件进行编译链接相关的配置

build: 则启动构建过程

rebuild:包含了clean configure build 三个命令的集合操作

比较常见的选项:

―debug: 表明目标将构建为debug版本, 默认为release版本

―silly: 输出全部构建日志详情,包括所有编译链接选项和进度等

―target ―arch ―diet-url: 用于指定一个具体的版本的node进行编译,若要指定electron对应的node版本可以加上这些参数

―msvs_version 用于windows上指定vs版本

node-gyp的其他命令及选项可以参考 https://github.com/nodejs/node-gyp

binding.gyp文件的结构很简单,其实就是json格式的配置文件。一般实际工程中涉及的配置大体是下面这个样子:

{ "targets": [ { "target_name": "DemoSDK", "sources": [ "DemoSDK4Node.cpp", ... ], "include_dirs": [ "<!(node -e \"require('nan')\")", "DemoSDK/src/", ... ], "libraries": [ "../Libraries/mac/lib.a", "-F../Libraries/mac -framework DemoLib", ... ], "defines": [ ... ], 'conditions': [ ['OS=="mac"', { 'configurations': { 'Debug': { "xcode_settings": { "GCC_OPTIMIZATION_LEVEL": "0", "DEPLOYMENT_POSTPROCESSING": "NO", "GCC_GENERATE_DEBUGGING_SYMBOLS": "YES", "GCC_SYMBOLS_PRIVATE_EXTERN" : "YES", "DEBUG_INFORMATION_FORMAT": "dwarf", "GCC_ENABLE_CPP_RTTI": "YES", "GCC_ENABLE_CPP_EXCEPTIONS": "YES", "OTHER_CPLUSPLUSFLAGS" : [ '-ObjC++', "-std=c++11", "-stdlib=libc++", "-fobjc-arc", '-fvisibility=hidden'], "OTHER_LDFLAGS": [ "-stdlib=libc++"], "MACOSX_DEPLOYMENT_TARGET": "10.10" } }, 'Release': { "xcode_settings": { "GCC_OPTIMIZATION_LEVEL": "s", "COPY_PHASE_STRIP": "YES", "DEAD_CODE_STRIPPING": "YES", "DEPLOYMENT_POSTPROCESSING": "YES", "GCC_GENERATE_DEBUGGING_SYMBOLS": "YES", "GCC_SYMBOLS_PRIVATE_EXTERN" : "YES", "STRIP_INSTALLED_PRODUCT": "YES", "STRIP_STYLE": "non-global", "DEBUG_INFORMATION_FORMAT": "dwarf-with-dsym", "LD_GENERATE_MAP_FILE" : "YES", "GCC_ENABLE_CPP_RTTI": "YES", "GCC_ENABLE_CPP_EXCEPTIONS": "YES", "OTHER_CPLUSPLUSFLAGS" : [ '-ObjC++', "-std=c++11", "-stdlib=libc++", "-fobjc-arc", '-fvisibility=hidden'], "OTHER_LDFLAGS": [ "-stdlib=libc++"], "MACOSX_DEPLOYMENT_TARGET": "10.10"

本文前端(javascript)相关术语:javascript是什么意思 javascript下载 javascript权威指南 javascript基础教程 javascript 正则表达式 javascript设计模式 javascript高级程序设计 精通javascript javascript教程

代码区博客精选文章
分页:12
转载请注明
本文标题:Node.js C++ Addon应用实践
本站链接:https://www.codesec.net/view/628477.html


1.凡CodeSecTeam转载的文章,均出自其它媒体或其他官网介绍,目的在于传递更多的信息,并不代表本站赞同其观点和其真实性负责;
2.转载的文章仅代表原创作者观点,与本站无关。其原创性以及文中陈述文字和内容未经本站证实,本站对该文以及其中全部或者部分内容、文字的真实性、完整性、及时性,不作出任何保证或承若;
3.如本站转载稿涉及版权等问题,请作者及时联系本站,我们会及时处理。
登录后可拥有收藏文章、关注作者等权限...
技术大类 技术大类 | 前端(javascript) | 评论(0) | 阅读(62)