sensai2.0

325
15

1. 开发node addon的场合

1) 一些其他语言已开发完成的功能库,提供了源码或动态链接库,希望直接通过node require的方式把它拿来(例如node-sass: https://github.com/sass/node-sass

2) 由于node单进程单线程的原因,请求中不能出现长时间阻塞执行的代码(比如超复杂的模板渲染、验证码图片的生成、在线word到pdf的转换、在线文件加密等等),这时需要自行开发异步非阻塞的addons来实现需求(例如node-video: https://github.com/pkrumins/node-video

2. 构建框架

1) node-gyp与binding.gyp

node-gyp是node原生模块的构建工具,用于生成makefile以及ide相关文件

binding.gyp是native模块的编译配置文件,格式类似json,一个基本格式如下

{                                                  
  'targets': [                                     
    {                                              
      'target_name': 'nan',                        
      'sources': [                                 
          'src/nan_hello.cc'                       
      ],                                           
      'include_dirs': [                            
          '<!(node -e \'require("nan")\')'         
      ],                                           
      'conditions' : [                             
        [                                          
          'OS=="linux"', {                         
            'cflags!': [ '-fno-exceptions' ],      
            'cflags_cc!': [ '-fno-exceptions' ],   
            'libraries': [ '-lz' ]                 
          }                                        
        ],                                         
        [                                          
          'OS=="mac"', {                           
            'xcode_settings': {                    
              'GCC_ENABLE_CPP_EXCEPTIONS': 'YES'   
             },                                    
            'libraries' : [                        
              '<!@(pkg-config zlib --libs)'        
            ]                                      
          }                                        
        ],                                         
        [                                          
          'OS=="win"', {                           
            'include_dirs' : [                     
              '<(module_root_dir)/includes'        
            ],                                     
            'libraries' : [                        
              '<(module_root_dir)/gyp/lib/zlib.lib'
            ]                                      
          }                                        
        ]                                          
      ]                                            
    }                                              
  ]                                                
}                                                  

sources字段为源文件,include_dirs表示额外包含的文件列表,conditions下配置不同os环境下的编译选项。

<,<@,<!,<!@分别表示系统变量替换和shell执行替换,当执行结果为列表类型时,有<!@和<@会将结果转换成list,没@返回结果会转换成字符串

需要特殊配置的内容无非是第三方库、包含文件目录、c/c++版本等

binding.gyp具体写法可以参考: https://gyp.gsrc.io/docs/InputFormatReference.md

配置完成后开始build:

node-gyp configure build
将会生成Makefile并编译生成目标文件

使用 --debug 选项将会编译出debug版本

在各个平台下也可以执行对应指令生成ide的工程文件以便于开发,如:

// 生成vs2015工程
node-gyp configure --msvs_version=2015
// 生成xcode工程
node-gyp configure -- -f xcode

2) 基本结构

一个基于Node v6.10.8版本原生v8开发的demo模块如下,该demo主要实现了一个阻塞sleep的方法

入口程序:

#include <v8.h>
#include <node.h>

#include "sleep.h"

using namespace v8;

namespace demo {

    /**
     * 阻塞延时5s
     */
    void Sleep(const FunctionCallbackInfo<Value>& args) {
        Isolate *isolate = args.GetIsolate();

        unsigned int sleepSeconds = 1;
        if (1 == args.Length()) {
            Local<Number> number = Local<Number>::Cast(args[0]);
            sleepSeconds = number->Uint32Value();
        }

        commonSleep(sleepSeconds);
        args.GetReturnValue().Set(v8::Undefined(isolate));
    }

    void init(v8::Local<v8::Object> exports) {
        NODE_SET_METHOD(exports, "sleep", Sleep);
    }

    NODE_MODULE(addon, init)

}  // namespace demo

其中NODE_MODULE和NODE_SET_METHOD两个node.h中定义的宏用于声明主模块及为模块注入方法,显然"sleep"为方法名,函数指针Sleep为方法体

Sleep方法中通过GetIsolate获得js运行时句柄(可以认为每个native方法中数据的传递都需要有这个变量),然后通过args[0]获得传参并转换为unsigned int(延时秒数,默认为1s),接着调用跨平台的延时阻塞sleep方法,最后返回undefined

跨平台的commonSleep定义如下:

// sleep.h
#ifdef _WIN32                                
#include <windows.h>                         
#else                                        
#include <unistd.h>                          
#endif                                       
                                             
void commonSleep(const unsigned int seconds);

// sleep.cc
#include "sleep.h"                            
                                              
void commonSleep(const unsigned int seconds) {
#ifdef _WIN32                                 
    Sleep(seconds);                           
#else                                         
    usleep(seconds * 1000000);                
#endif                                        
}                                             

最终在build/Debug(Release)目录下得到xxx.node文件(xxx取决于binding.gyp中的target_name字段,而非NODE_MODULE宏中的定义)

.node文件实际上即特殊的动态链接库文件,调用方式如下:

#!/usr/bin/env node                              
const sleep = require('./build/Debug/sleep.node')
                                                 
let now = +new Date()                            
sleep.sleep()                                    
console.log(+new Date() - now)                   
                                                 
now = +new Date()                                
sleep.sleep(5)                                   
console.log(+new Date() - now)                   

​执行效果js进程分别阻塞1s和5s,然后console输出阻塞时间。


3) 基本调试

由于node引入native addon的方式类似于加载动态链接库,所以一般是先执行js代码并暂停到调用对应位置,然后调试器attach到对应进程。

以最基础的gdb/lldb为例:

先通过设置--debug生成debug版本的.node文件,同时js端引入debug版本的.node模块

然后在调试模式下执行目标js文件:

-> % node --inspect --debug-brk index.js                                                                                                                        09/16/17 19:28 
Debugger listening on port 9229.
Warning: This is an experimental feature and could change at any time.
To start debugging, open the following URL in Chrome:
    chrome-devtools://devtools/remote/serve_file/@60cd6e859b9f557d2312f5bf532f6aec5f284980/inspector.html?experiments=true&v8only=true&ws=127.0.0.1:9229/80ccdc9e-fc60-4674-8dad-04aa30085301
然后在Chrome中对js进行调试,并在对应位置加上断点


接下来通过ps或其他指令找到当前node的pid:

wisdom            1941   0.0  0.5  3040724  21288 s003  S+    7:28PM   0:00.26 node --i
nspect --debug-brk index.js                                                            
然后使用lldb/gdb attach到对应的pid上,并加上待调试方法的断点:

-> lldb
-> attach 1941
-> b -M demo::Sleep
完成后在Chrome中点击js模块调试继续,lldb将会进入Sleep方法断点,如下所示:


以上为基本调试方法。

与IDE结合的调试方式也基本类似,本质上与.net web应用attach到对应连接所在进程的调试方式大同小异。


3. V8编程

​开发node原生扩展的基本套路实际上是将原生实现的方法外面封装一层v8的io接口,所以基本上了解了js及对应v8的数据类型就可以轻松的进行扩展开发

1) 数据类型

js的数据类型与v8类基本一一对应:

日常开发使用的基本上就是v8::Value下派生出的所有类,以及在native层定义Function需要使用FunctionTemplate


2) 作用域

2.1) 变量句柄——Handle

所有js变量都是以堆的形式维护,v8以句柄(Handle)的形式维护这些变量指针

v8开发主要用到的Handle类型有v8::Local和v8::Persistent,分别表示局部变量和持久型变量

v8::Local声明的变量会自动被v8 GC进行回收,而v8::Persistent声明的变量需要手动调用Persistent::New, Persistent::Dispose方法进行内存管理,调用Persistent::MakeWeak方法后Persistent将可以被GC回收

一般情况下在模块方法体内定义变量直接使用v8::Local即可,v8::Persistent一般只用作定义c++ class封装的js object即异步回调函数(此处略去1000字...)

所以一般来说js变量在v8中声明对应如下:

let a = "a"
<=>
v8::Local<v8::String> a = v8::String::NewFromUtf8(v8::Isolate::GetCurrent(), "a");

2.2) 句柄作用域——HandleScope

HandleScope对象维护了当前作用域下的所有Local句柄对象,Local型的变量会在HandleScope变量销毁时释放,而Persistent型的变量则不会。

v8::HandleScope scope;
// 之后定义的v8::Local生命周期被scope约束

2.3) 上下文——Context

js中方法可以通过this对象获取当前上下文的变量,这里的上下文就对应了v8中的Context对象,通过定义新的Context对象可以避免变量名污染

一般通过Context::New新建一个上下文,然后执行Context::Scope对象来管理context

Context::Scope 仅仅做了在Context构造中调用 context->Enter(),而在析构函数中调用 context->Leave()。

v8::HandleScope scope;
v8::Local<v8::Context> context = Context::New();
v8::Context::Scope contextScope(context);
//...


3) Nan

node.js的版本号可谓日新月异,并且几乎每个版本的node.js都有一个对应的v8版本,并且不同v8版本之间的各种定义有略微的差别,所以为了兼容各个版本,node推出了nan(Native Abstractions for Node.js)这个模块,nan模块一般也是node扩展的标配

package.json中加一个nan,npm install后在binding.gyp中包含'<!(node -e \'require("nan")\')'即自动在include路径中引入nan模块,包含文件为 <nan.h>

​nan中提供了很多的宏定义和类型替换,既兼容各个版本的v8,又简化了书写的复杂度。

比如上面的demo仅仅支持node6,如果换成nan实现则可以兼容各个版本,修改如下:

#include <node.h>
#include <nan.h>

#include "sleep.h"

using namespace Nan;

NAN_METHOD(Sleep) {
    unsigned int sleepSeconds = 1;
    if (1 == info.Length()) {
        sleepSeconds = info[0]->Uint32Value();
    }
    commonSleep(sleepSeconds);
    info.GetReturnValue().SetUndefined();
}

NAN_MODULE_INIT(Init) {
    Set(target, New<v8::String>("sleep").ToLocalChecked(),
        GetFunction(New<v8::FunctionTemplate>(Sleep)).ToLocalChecked());
}

NODE_MODULE(addon, Init)

可以看到,NAN_MODULE_INIT和NAN_METHOD两个宏代替了之前定义方法的代码,并且提供了通用的定义变量方法Nan::New

NAN_MODULE_INIT中的target、NAN_METHOD中的info都是宏定义内部的变量,可以看出NAN进行了深度的封装。

例如NAN_METHOD(methodName)展开后为:

void methodName(const Nan::FunctionCallbackInfo<v8::Value>& info) {
  ...
}
除此之外,Nan模块还深度封装了C++ Class到JS Object的方法,甚至基于uv_work_t实现的AsyncWorker

有了Nan模块作为抽象层​,则可以更加专注于逻辑。

附. 参考文档

1) node.js官方文档: https://nodejs.org/dist/latest-v8.x/docs/api/addons.html

2) v8文档: https://v8docs.nodesource.com/

3) 一个学习node addon的网站: https://nodeaddons.com

4) node-gyp: https://github.com/nodejs/node-gyp

5) nan: https://github.com/nodejs/nan

6) V8嵌入指南: https://github.com/v8/v8/wiki/Embedder%27s-Guide


回复

对话列表

×