node源码粗读(3):node_js2c的前世今生
发布于 2 年前 作者 xtx1130 1643 次浏览 来自 分享

今天我们主要介绍一个比较核心的gyp构建项目:node_js2c

前言

我相信了解node的人,应该对node_js2c不陌生,如果您感到陌生的话,那么不知道下面这几个词组会不会让您感到亲切点:js2c.py、node_javascript.cc、bootstrap_node.js。如果您能想到什么,那么估计node_js2c离您也会很近,因为是gyp的这个target–node_js2c,串起来了这些文件。

node_js2c的前世

node_js2c是什么呢,大家可以翻看一下node.gyp中,对它的定义:

{
      'target_name': 'node_js2c',
      'type': 'none',
      'toolsets': ['host'],
      'actions': [
        {
          'action_name': 'node_js2c',
          'process_outputs_as_sources': 1,
          'inputs': [
            '<@(library_files)',
            './config.gypi',
          ],
          'outputs': [
            '<(SHARED_INTERMEDIATE_DIR)/node_javascript.cc',
          ],
          'conditions': [
            [ 'node_use_dtrace=="false" and node_use_etw=="false"', {
              'inputs': [ 'src/notrace_macros.py' ]
            }],
            ['node_use_lttng=="false"', {
              'inputs': [ 'src/nolttng_macros.py' ]
            }],
            [ 'node_use_perfctr=="false"', {
              'inputs': [ 'src/noperfctr_macros.py' ]
            }]
          ],
          'action': [
            'python',
            'tools/js2c.py',
            '<@(_outputs)',
            '<@(_inputs)',
          ],
        },
      ],
    }, # end node_js2c

没错,它就是gyp构建的一个项目的名称。gyp构建了这么多项目,我为何单独选这个项目来说呢?我相信对node源码感兴趣的人应该也看过类似的文章,不过您也可以看下去,相信会有意外的收获。
接下来,我们详细分析一下,这个构建究竟都做了哪些事情:

inputs

'inputs': [
        '<@(library_files)',
        './config.gypi',
],

首先来看gyp中action的输入,这里的inputs是作为增量更新构建时候使用的参数,而这也是action的特性即:如果里面的文件没有更新,他是不会再次编译action的。inputs顾名思义,是输入信息,这里有两个输入信息,我们首先来看第一个:

  • <@(library_files)
    这是python中的变量,经过查找,发现它其实是(因为文件太多,所以我只截图一小部分): issue10-1 js文件路径的集合,而路径主要分为三部分: ./lib、./deps/v8/tools、./deps/node-inspect/lib。 ./lib 相信凡是知道node源码的应该都了解这个文件夹,主要存放的是node内置模块的js代码,今天对这一块不做过多介绍。
    ./deps/v8/tools下面的js文件,主要用来做v8profile分析的js文件。
    ./deps/node-inspect/lib这个很明显,就是做node inspect的js文件。

  • ./config.gypi
    gypi文件是gyp的一个统一配置文件,这里就不做过多分析了,里面是一些配置项,而这个文件,是通过./configure生成的。
    action的input这部分其实我们就比较明了了,他的输入主要由两部分构成:相关的js文件路径以及编译的配置。

outputs

看完inputs,我们来看一下outputs:

'outputs': [
            '<(SHARED_INTERMEDIATE_DIR)/node_javascript.cc',
          ]

outputs的配置项也很简单,经过查找,输出路径其实是$(obj)/gen/node_javascript.cc,而$(obj)在debug模式下,是out/Debug/obj。obj一般来说是中间代码所在的文件夹,node这么做其实也是这个意思。

node_js2c的今生

上面介绍了这个构建的前世,接下来我们说说他到底做了什么。上一步我们说到,node_js2c生成的文件node_javascript.cc在Debug模式下其实是在/obj文件夹中,也就是说,这个命令生成的是中间代码!那么这些代码是如何生成的呢?我们接下来继续看一下node.gyp文件中node_js2c的action:

'action': [
            'python',
            'tools/js2c.py',
            '<@(_outputs)',
            '<@(_inputs)',
          ],

这段文字我给大家翻译一下:

python tools/js2c.py "$(obj)/gen/node_javascript.cc" lib/internal/bootstrap_node.js lib/async_hooks.js ...#后面的js太多,就不一一罗列了

文字翻译完了,那么重点就在js2c.py中咯。接下来我们直接扒到这个文件中,先揪出main函数:

def main():
  natives = sys.argv[1]
  source_files = sys.argv[2:]
  JS2C(source_files, [natives])

定眼一看,sys明显是sys包,argv明显是cli参数,natives也就是$(obj)/gen/node_javascript.cc,而source_files则是后面的一串js文件。接下来我们看一下JS2C函数实现了什么。

def Render(var, data):
  # Treat non-ASCII as UTF-8 and convert it to UTF-16.
  if any(ord(c) > 127 for c in data):
    template = TWO_BYTE_STRING
    data = map(ord, data.decode('utf-8').encode('utf-16be'))
    data = [data[i] * 256 + data[i+1] for i in xrange(0, len(data), 2)]
    data = ToCArray(data)
  else:
    template = ONE_BYTE_STRING
    data = ToCString(data)
  return template.format(var=var, data=data)
//JS2C code
definitions.append(Render(key, name))
definitions.append(Render(value, lines))
initializers.append(INITIALIZER.format(key=key, value=value))

在这里我只截取一部分代码片段,通过这个片段就能看出,其实他把js文件转换成了ASCII码,而有些文件有超出ASCII 范围的字符,则最终转成了UTF-16。通过读模板可以了解到,对于static const uint8_t raw_internal_tls_key[]这类命名所代表的文件,对应于lib/interal/tls.js。大家可能好奇,超出ASCII范围的到底是什么字符呢?因为英文代码就这么多,里面也不会掺杂着汉字。在这里给大家举一个例子:

'use strict';

// This module exists entirely for regression testing purposes.
// See `test/parallel/test-internal-unicode.js`.

module.exports = '✓';

这是lib/test/unicode.js文件,其中的✓(10003)就超出了ASCII的范围。

node_javascript.cc

大家可能会产生疑问了,把这些文件做成了ASCII码,又是在哪使用的呢?接下来,我们以bootstrap_node.js为例介绍一下:

static struct : public v8::String::ExternalOneByteStringResource {
  const char* data() const override {
    return reinterpret_cast<const char*>(raw_internal_bootstrap_node_value);
  }
  size_t length() const override { return arraysize(raw_internal_bootstrap_node_value); }
  void Dispose() override { /* Default calls `delete this`. */ }
  v8::Local<v8::String> ToStringChecked(v8::Isolate* isolate) {
    return v8::String::NewExternalOneByte(isolate, this).ToLocalChecked();
  }
} internal_bootstrap_node_value;

在这里声明了一个结构体,并实例化为internal_bootstrap_node_value。结构体里面进行了两个重载,一个是对string的data()方法进行了重载,返回的是bootstrap_node的vaule,另一个则是重载了长度。
调用这个结构体的代码如下:

v8::Local<v8::String> MainSource(Environment* env) {
  return internal_bootstrap_node_value.ToStringChecked(env->isolate());
}

通过这个调用流程可以看出来,最终通过ToStringChecked传入isolate,来把这个js源码加入到一个v8的实例中。
在NewExternalOneByte中给这个源码开辟了空间存储,在这个函数中有段代码(v8/src/api.cc):

MaybeLocal<String> v8::String::NewExternalOneByte(
    Isolate* isolate, v8::String::ExternalOneByteStringResource* resource) {
 //省略。。。
  if (resource->length() > 0) {
    i::Handle<i::String> string = i_isolate->factory()
                                      ->NewExternalStringFromOneByte(resource)
                                      .ToHandleChecked();
    i_isolate->heap()->RegisterExternalString(*string);
    return Utils::ToLocal(string);
  } else {
    // The resource isn't going to be used, free it immediately.
    resource->Dispose();
    return Utils::ToLocal(i_isolate->factory()->empty_string());
  }
}

在这里调用了Utils::ToLocal (v8/src/api.h)来开辟出了handle。
而调用的地方在node.cc中:

void LoadEnvironment(Environment* env) {
//...
Local<String> script_name = FIXED_ONE_BYTE_STRING(env->isolate(),
                                                    "bootstrap_node.js");
  Local<Value> f_value = ExecuteString(env, MainSource(env), script_name);
//...
}

LoadEnvironment这个api在第一篇文章曾经介绍过,其实是node在start的时候调用的函数,这里就不做过多介绍了,主要看其中和本次话题有关的代码。大家是不是觉得MainSource(env)有点熟悉?没错,通过MainSource实现了把script的ASCII存储到了一个v8实例中,而ExecuteString方法在shell.cc中,在这里就不截图了,它最终调用了v8::Script::Compile来把存在内存中的string最终执行了编译操作,使之成为可运行的代码,整个程序也在此形成了一个闭环。

结语

老生常谈就不说了,什么这种方式增加了js载入速度之类的,想必大家对node源码感兴趣的,应该也了解过。写这篇文章的主要目的是想经过一番溯源,让大家对node_js2c熟悉,了解其中的一些调用细节。如果有什么不懂的或者想问的地方,可以在issue下留言进行交流。

原文地址:https://github.com/xtx1130/blog/issues/10,如果大家感觉我写的文章有问题,欢迎指正,请在git或者cnode上评论留言。如果觉得我写的对您有帮助,欢迎star。
by 小菜

回到顶部