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源码粗读(3):node_js2c的前世今生 #10

Open
xtx1130 opened this issue Nov 27, 2017 · 8 comments
Open

node源码粗读(3):node_js2c的前世今生 #10

xtx1130 opened this issue Nov 27, 2017 · 8 comments

Comments

@xtx1130
Copy link
Owner

xtx1130 commented Nov 27, 2017

今天我们主要介绍一个比较核心的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下留言进行交流。

by 小菜

@Power-kxLee
Copy link

tim 20171128102139

@For-me
Copy link

For-me commented Nov 28, 2017

@xxchevi
Copy link

xxchevi commented Nov 28, 2017

@y-ui
Copy link

y-ui commented Nov 28, 2017

a

@xtx1130
Copy link
Owner Author

xtx1130 commented Nov 28, 2017

exp2

@y-ui
Copy link

y-ui commented Nov 28, 2017

e4d0a9c981aa02e3530e3529b2e7c4a8

@ghost
Copy link

ghost commented Nov 28, 2017

33299320-03f9ea48-d426-11e7-85ee-3740e42ecab3

@For-me
Copy link

For-me commented Nov 28, 2017

wz3rcxsjlvu rx6muhu0f3

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

No branches or pull requests

5 participants