今天我们主要介绍一个比较核心的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中的变量,经过查找,发现它其实是(因为文件太多,所以我只截图一小部分): 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 小菜