Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

如何知根知底使用Node.js C++ Addon #4

Open
tsy77 opened this issue Jun 18, 2018 · 0 comments
Open

如何知根知底使用Node.js C++ Addon #4

tsy77 opened this issue Jun 18, 2018 · 0 comments

Comments

@tsy77
Copy link
Owner

tsy77 commented Jun 18, 2018

最初的想法是去写一个node C++ addon,但在了解如何去写node addon的过程中,发现想要知根知底的去使用node addon,需要对node架构、V8、libuv、模块加载等要点都有一个了解。所以本文的目的就是梳理如何清楚的使用node addon?

本文将从以下几点进行阐述:

1.node 架构
2.V8
3.libuv
4.深入node源码了解其模块加载
5.addon

node架构

node的架构相信大家都不陌生,以模块加载为角度架构图如下:

V8 engine是Google开发的javascript引擎,是一个独立运行的虚拟机,node以第三方依赖的形式引入V8(与libuv等依赖放在deps目录下)。除了作为Javascript运行引擎外,V8提供了嵌入API,为编译和执行JS脚本, 访问 C++ 方法和数据结构, 错误处理, 开启安全检查等提供了函数接口,承担着是node中js与C++桥接的重要作用。

libuv是专门为node开发的库,提供跨平台的异步I/O能力。其基于异步的、事件驱动模型,提供一个event-loop,还有基于I/O和其它事件通知的回调函数。

Builtin modules是node提供的C++模块。

Native module是node提供的js模块,其被使用者直接调用,并且有些Native module会借助下层的Builtin module。在native模块中使用builtin模块,利用的是node提供的process.binding方法(后面模块加载会有介绍)。

Addon是一个用C++写的node的动态链接库,使用者可以直接使用require()方法进行加载,当然前提是addon已经编译好。Addon主要用来扩展node的底层能力,具体应用可能是计算密集型的模块(C++的运行性能高,可以利用libuv异步和事件循环的能力,同时可以使用多进程、多线程)。

V8

V8提供了对外的使用API,可以参考V8嵌入指南

下面主要对其中主要概念进行简要梳理

Isolate是一个独立的V8实例,也可以说一个独立虚拟机,其中可以包含一个或多个线程,但同一时间,只有一个线程是执行状态。

Context代表一个执行上下文(执行环境),它使得可以在一个 V8 实例中运行相互隔离且无关的 JavaScript 代码. 你必须为你将要执行的 JavaScript 代码显式的指定一个 context。Context支持嵌套。

Handle是一个指向堆内存的指针,在V8中JavaScript的值和对象也都存放在堆中,Handle提供了一个JS对象在堆内存中的地址的引用。有人会有疑问我们直接操作JS变量指针不可以嘛?由于V8的GC策略,可能会对堆中的JS变量移动其内存位置,Handle的出现可以跟踪相应变量的地址。

Handle Scope是一个Handle的容器,为了解决一个个释放handle过于繁琐,将一些handle接入handle scope中,方便统一管理(释放等)。

下图主要是为了大家理解Isolate、Context、Handle Scope、Handle的大小关系,在细节上不够准确。

libuv

libuv是一个跨平台的异步I/O库。其架构图如下:

上图的左侧是网络相关的I/O,使用的都是各个平台比较有效率的多路I/O模型,Linux上的epoll,OSX和BSD类OS上的kqueue,SunOS上的event ports以及Windows上的IOCP机制。

右侧File类型的I/O,基于线程池的方式来实现异步的请求和处理。

具体讲解可参考libuv 教程

node 模块加载

node模块可分为Native Module、Builtin Module、Constants。

Native Module在下载node源码并编译后,会在out/Release/obj/gen目录下node_natives.h。该文件由 js2c.py 生成,其会将node源代码中的lib目录下所有js文件以及src目录下的node.js文件中每一个字符转换成对应的ASCII码,并存放在相应的数组里面。

Builtin模块会被main()之前加载到modlist_builtin中,当使用时,从链表中将模块取出即可。在每个builtin模块中,都会通过宏NODE_BUILTIN_MODULE_CONTEXT_AWARE预编译阶段将其转为函数_register_ ## modname,函数会调用node_module_register方法将其加载进modlist_builtin。tcp_wrap中宏定义如下:

NODE_BUILTIN_MODULE_CONTEXT_AWARE(tcp_wrap, node::TCPWrap::Initialize)

模块加载

我们加载node C++ addon时,可以直接使用process.binding方法。其实process.binding是node require()的基础,所以后面也介绍了require的实现

process.binding()

process.binding()做了什么呢?
static void GetBinding(const FunctionCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args);

  CHECK(args[0]->IsString());

  Local<String> module = args[0].As<String>();
  node::Utf8Value module_v(env->isolate(), module);

  node_module* mod = get_builtin_module(*module_v);
  Local<Object> exports;
  if (mod != nullptr) {
    exports = InitModule(env, mod, module);
  } else if (!strcmp(*module_v, "constants")) {
    exports = Object::New(env->isolate());
    CHECK(exports->SetPrototype(env->context(),
                                Null(env->isolate())).FromJust());
    DefineConstants(env->isolate(), exports);
  } else if (!strcmp(*module_v, "natives")) {
    exports = Object::New(env->isolate());
    DefineJavaScript(env, exports);
  } else {
    return ThrowIfNoSuchModule(env, *module_v);
  }

  args.GetReturnValue().Set(exports);
}

static Local<Object> InitModule(Environment* env,
                                 node_module* mod,
                                 Local<String> module) {
  Local<Object> exports = Object::New(env->isolate());
  // Internal bindings don't have a "module" object, only exports.
  CHECK_EQ(mod->nm_register_func, nullptr);
  CHECK_NE(mod->nm_context_register_func, nullptr);
  Local<Value> unused = Undefined(env->isolate());
  mod->nm_context_register_func(exports,
                                unused,
                                env->context(),
                                mod->nm_priv);
  return exports;
}

process.binding()主要做了对不同类型的模块做了不同的处理:

1.Builtin模块,直接从modlist_builtin获取
2.constants模块,通过 constants 导出。
3.Native模块,从node_natives.h中获取

require

首先node中require()方法做了什么呢?
// Loads a module at the given file path. Returns that module's
// `exports` property.
Module.prototype.require = function(id) {
  if (typeof id !== 'string') {
    throw new ERR_INVALID_ARG_TYPE('id', 'string', id);
  }
  if (id === '') {
    throw new ERR_INVALID_ARG_VALUE('id', id,
                                    'must be a non-empty string');
  }
  return Module._load(id, this, /* isMain */ false);
};
代码前面时一些path校验,那么Module._load做了什么呢?
// Check the cache for the requested file.
// 1. If a module already exists in the cache: return its exports object.
// 2. If the module is native: call `NativeModule.require()` with the
//    filename and return the result.
// 3. Otherwise, create a new module for the file and save it to the cache.
//    Then have it load  the file contents before returning its exports
//    object.
Module._load = function(request, parent, isMain) {
  if (parent) {
    debug('Module._load REQUEST %s parent: %s', request, parent.id);
  }

  if (experimentalModules && isMain) {
    asyncESM.loaderPromise.then((loader) => {
      return loader.import(getURLFromFilePath(request).pathname);
    })
    .catch((e) => {
      decorateErrorStack(e);
      console.error(e);
      process.exit(1);
    });
    return;
  }

  var filename = Module._resolveFilename(request, parent, isMain);

  var cachedModule = Module._cache[filename];
  // 如果在缓存
  if (cachedModule) {
    updateChildren(parent, cachedModule, true);
    return cachedModule.exports;
  }
  
  // 原生模块
  if (NativeModule.nonInternalExists(filename)) {
    debug('load native module %s', request);
    return NativeModule.require(filename);
  }
	
  // 创建新module
  // Don't call updateChildren(), Module constructor already does.
  var module = new Module(filename, parent);

  if (isMain) {
    process.mainModule = module;
    module.id = '.';
  }

  Module._cache[filename] = module;

  tryModuleLoad(module, filename);

  return module.exports;
};

Module._load()主要做了三件事:

1.缓存模块直接从缓存取
2.原生模块调用`NativeModule.require`
3.否则,创建新模块,加入缓存
我们再深度遍历代码到NativeModule.require
NativeModule.require = function(id) {
    if (id === loaderId) {
      return loaderExports;
    }

    const cached = NativeModule.getCached(id);
    // 判断是否缓存
    if (cached && (cached.loaded || cached.loading)) {
      return cached.exports;
    }

    if (!NativeModule.exists(id)) {
      // Model the error off the internal/errors.js model, but
      // do not use that module given that it could actually be
      // the one causing the error if there's a bug in Node.js
      // eslint-disable-next-line no-restricted-syntax
      const err = new Error(`No such built-in module: ${id}`);
      err.code = 'ERR_UNKNOWN_BUILTIN_MODULE';
      err.name = 'Error [ERR_UNKNOWN_BUILTIN_MODULE]';
      throw err;
    }

    moduleLoadList.push(`NativeModule ${id}`);

    const nativeModule = new NativeModule(id);

    nativeModule.cache();
    nativeModule.compile();

    return nativeModule.exports;
 };

NativeModule.require主要做了两件事:

1.缓存模块直接从缓存取
2.否则,加入到moduleLoadList(bootstrapInternalLoaders中的私有变量)数组中,创建新的 NativeModule 对象,缓存,最后`nativeModule.compile()`。
nativeModule.compile()做了什么呢?
NativeModule.getSource = function(id) {
  return NativeModule._source[id];
};

NativeModule.wrap = function(script) {
  return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
};

NativeModule.wrapper = ['(function (exports, require, module, __filename, __dirname) {','\n});' ];

NativeModule.prototype.compile = function() {
    let source = NativeModule.getSource(this.id);
    source = NativeModule.wrap(source);

    this.loading = true;

    try {
      const script = new ContextifyScript(source, this.filename);
      // Arguments: timeout, displayErrors, breakOnSigint
      const fn = script.runInThisContext(-1, true, false);
      const requireFn = this.id.startsWith('internal/deps/') ?
        NativeModule.requireForDeps :
        NativeModule.require;
      fn(this.exports, requireFn, this, process);

      this.loaded = true;
    } finally {
      this.loading = false;
    }
};

nativeModule.compile()就是将源码wrap起来,使用script.runInThisContext 去运行。

script.runInThisContext 做了什么呢?
const {
  ContextifyScript,
  kParsingContext,
  makeContext,
  isContext: _isContext,
} = process.binding('contextify');

class Script extends ContextifyScript {...}

function createScript(code, options) {
  return new Script(code, options);
}

function runInThisContext(code, options) {
  if (typeof options === 'string') {
    options = { filename: options };
  }
  return createScript(code, options).runInThisContext(options);
}

Contextify中的runInThisText如何实现的呢?

static void RunInThisContext(const FunctionCallbackInfo<Value>& args) {
    Environment* env = Environment::GetCurrent(args);

    CHECK_EQ(args.Length(), 3);

    CHECK(args[0]->IsNumber());
    int64_t timeout = args[0]->IntegerValue(env->context()).FromJust();

    CHECK(args[1]->IsBoolean());
    bool display_errors = args[1]->IsTrue();

    CHECK(args[2]->IsBoolean());
    bool break_on_sigint = args[2]->IsTrue();

    // Do the eval within this context
    EvalMachine(env, timeout, display_errors, break_on_sigint, args);
}

Node Addon

加载

上面讲解了node的模块加载,那么Node Addon为什么能够正确加载呢?

原因在于每个Node Addon模块入口中,需要#include <node.h>node.h包含者一些宏定义,其中有:

#define NODE_MODULE(modname, regfunc)                                 \
  NODE_MODULE_X(modname, regfunc, NULL, 0)  // NOLINT (readability/null_usage)
  
 #define NODE_MODULE_X(modname, regfunc, priv, flags)                  \
  extern "C" {                                                        \
    static node::node_module _module =                                \
    {                                                                 \
      NODE_MODULE_VERSION,                                            \
      flags,                                                          \
      NULL,  /* NOLINT (readability/null_usage) */                    \
      __FILE__,                                                       \
      (node::addon_register_func) (regfunc),                          \
      NULL,  /* NOLINT (readability/null_usage) */                    \
      NODE_STRINGIFY(modname),                                        \
      priv,                                                           \
      NULL   /* NOLINT (readability/null_usage) */                    \
    };                                                                \
    NODE_C_CTOR(_register_ ## modname) {                              \
      node_module_register(&_module);                                 \
    }                                                                 \
  }

我们又看到了我们熟悉的node_module_register方法,我们再写一个addon时调用的NODE_MODULE方法,实际上就是将该模块编译后的结果加入到modlist_builtin,我们调用process.binding()就可以将其加载。

NAN

为什么要有NAN呢?

原因在于随着Node.js和V8的版本迭代,其底层API可能会发生变化,我们写的这些原生模块又依赖了变化了的API的话,包就作废了。除非包的维护者去支持新版的API,不过这样依赖,老版Node.js下就又无法编译通过新版的包了。

为了解决这种尴尬的局面,NAN出现了,其在nan.h中定义了许多判断宏,会判断当前node版本,我们使用nan.h宏定义中的方法,编译器会将其展开成不同的结果。

Node v8.0之后,官方推出了N-API,它与NAN的区别在于,NAN在适配不同版本时,需要每次重新编译,而N-API将其底层接口抽象,所有版本都适用一套API即可。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant