V8这个概念你们都不陌生了,那么你动手编译过V8源码吗?编译后有尝试去了解V8背后的一些概念吗?若是没有,那么也不用心慌,下文将跟你们一一解释这些东西。在编译V8以前咱们先要了解一个东西-构建系统html
写惯前端的童鞋可能不是很明白这个东西是干啥用的?可是其实平时你都会接触到,只是概念不一样而已。前端咱们通常称其为打包构建,相似工具诸如webpack、parcel作的事情。其实最后的目标都是想获得一些目标性的文件。这里能够简单地说起一下软件工程中的构建系统的历史。前端
构建系统的需求是随着软件规模的增大而提出的。若是只是作简单的demo,一般代码量比较小,编写的源代码只有几个文件。好比你编写了一段代码放入helloworld.cpp文件中,要编译这段代码,只须要执行如下命令:node
g++ helloworld.c -o helloworld
复制代码
当软件规模逐渐增长,这时可能有几十个源代码文件,并且有了模块划分,有的要编译成静态库,有的要编译成动态库,最后连接成可执行代码,这时命令行方式就捉襟见肘,须要一个构建系统。常见的构建系统有GNU Make。须要注意的是,构建系统并非取代gcc这样的工具链,而是定义编译规则,最终仍是会调用工具链编译代码。python
当软件规模进一步扩大,特别是有多平台支持需求的时候,编写GNU Makefile将是一件繁琐和乏味的事情,并且极容易出错。这时就出现了生成Makefile的工具,好比Cmake
、AutoMake
等等,这种构建系统称做元构建系统(meta build system)。在Linux上软件仓库的概念尚未普及的时候,一般咱们安装软件的步骤是:linux
./configure
make
make install
复制代码
第一步就是调用一些自动化工具,根据系统环境(系统的版本众多,软件安装状况也不同),生成GNU Makefile。而后第二步才使用gcc或者g++命令去编译全部文件,最后一步即是将全部文件连接起来成可执行命令并安装到系统的某个指定目录。webpack
通常后两个步骤都是比较固化的,能提升工做效率的也就是在第一步了。因而V8团队针对本身的项目特色,撸了一个叫作GYP(Generate Your Projects)的构建系统,后面你要是看到node-gyp
其实就是基于这个作的js版本。不事后面GYP被v8团队废弃掉,改用GN(Generate Ninja)构建系统。两者的区别不是本文重点,有兴趣的童鞋能够查看这篇文章: chromium中的GN构建系统。c++
有意思的是尽管v8完全废弃掉了GYP,可是nodejs仍然在使用GYP,这个R大在建立deno项目的时候有说起到:Design Mistakes in Node。git
GN(Generate Ninja)是chromium project用来取代GYP的新工具,因为GN是用C++编写,比起用 python写的GYP快了不少,GN新的DSL的语法也被认为是比较好写以及维护的。web
在v8项目的根目录下有个.gn
文件,内容以下(去掉全部注释了):chrome
import("//build/dotfile_settings.gni")
buildconfig = "//build/config/BUILDCONFIG.gn"
check_targets = []
exec_script_whitelist = build_dotfile_settings.exec_script_whitelist + []
复制代码
咱们关注buildconfig
这个配置。.gn
所在的目录会被GN工具认定是项目的根目录,.gn
的内容基本就是用buildconfig
来指定build config的位置,其中//build//config/BUILDCONFIG.gn
是相对于项目根目录下路径的配置文件。
可是你会发现如今v8源码目录下并无叫作build的目录,这个目录要咋生成呢?这些知识咱们会在稍后的编译v8代码中说起。
假设如今你有build目录了,咱们找到BUILDCONFIG.gn
文件,文件里面会根据系统和平台设置对应的编译工具链:
... ...
if (custom_toolchain != "") {
set_default_toolchain(custom_toolchain)
} else if (_default_toolchain != "") {
set_default_toolchain(_default_toolchain)
}
... ...
复制代码
好比获得的_default_toolchain
值为:_default_toolchain = "//build/toolchain/linux:clang_x86
,那么你在build/toolchain/linux
目录下的BUILD.gn
能够找到这么一个配置:
clang_toolchain("clang_x86") {
# Output linker map files for binary size analysis.
enable_linker_map = true
toolchain_args = {
current_cpu = "x86"
current_os = "linux"
}
}
复制代码
由于GN没有内建的toolchain
规则,toolchain
里的各类tool
例如 cc,cxx,link等必须本身指定,指定的文件是build/toolchain/gcc_toolchain.gni
文件,在文件中咱们能够看到GN给定义的一些动做:
tool("cc") {
depfile = "{{output}}.d"
precompiled_header_type = "gcc"
command = "$cc -MMD -MF $depfile ${rebuild_string}{{defines}} {{include_dirs}} {{cflags}} {{cflags_c}}${extra_cppflags}${extra_cflags} -c {{source}} -o {{output}}"
depsformat = "gcc"
description = "CC {{output}}"
outputs = [
"$object_subdir/{{source_name_part}}.o",
]
}
复制代码
最后项目根目录下会有一个BUILD.gn
的文件,指定生成可执行文件的指令,好比:
v8_executable("v8_hello_world") {
sources = [
"samples/hello-world.cc",
]
configs = [
# Note: don't use :internal_config here because this target will get
# the :external_config applied to it by virtue of depending on :v8, and
# you can't have both applied to the same target.
":internal_config_base",
]
deps = [
":v8",
":v8_libbase",
":v8_libplatform",
"//build/win:default_exe_manifest",
]
}
复制代码
这样一套完整的GN构建系统便完成了。
有了GN,为啥还要Ninja呢?刚才咱们知道GN的英文意思是Generator Ninja,可见GN生成的东西并非咱们最终GNU Makefile形式。而Ninja才是最后生成Makefile的终极法器。Ninja 做为一个新型的编译工具,小巧而又高效,据谷歌官方的说法是速度有了好几倍的提高。
这个时候咱们尚未生成任何的Ninja文件,须要咱们使用GN命令去生成:
gn args out/foo
这下子你在out/foo
下就能够看到好多ninja文件:
Ninja使用build.ninja
文件来定义构建规则,和Makefile
里的元编程不一样,build.ninja
几乎是彻底静态的,动态生成依赖其余工具,如gn或者CMake。
build.niinja至关于ninja的makefile,一个简单的build.ninja文件以下,分为rule和dependency两部分。
phony: 能够建立其余target的别名。
default: 若是没有在命令行中指定target,可使用default来指定默认的target。
pools: 为了支持并发做业,Ninja还支持pool的机制,和用-j并行模式同样。
Make vs Ninja Performance Comparison将Ninja和Make进行了测试对比。
接下来咱们开始进行v8代码的编译操做。官网的文档给的已经很齐全了,这里只是再简单说一下,并说起一些官网没有给出的基本知识。
这一步注意了,不要直接从v8仓库使用git clone命令下载代码,这样下载下来的代码是无效的,会缺失不少东西,要使用官方提供的工具depot_tools
整个步骤汇总以下:
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
export PATH=$PATH:/path/to/depot_tools
gclient config https://chromium.googlesource.com/v8/v8
gclient sync
mkdir ~/v8
cd ~/v8
fetch v8
cd v8
复制代码
编译v8代码官网一样给的很详细:传送门,这里总结一下而已,有两种编译方式
使用gm
这个集成全部为一体的python脚本能够几个命令就搞定:
alias gm=/path/to/v8/tools/dev/gm.py
gm x64.release
gm x64.release.check
复制代码
按照咱们以前说的流程,咱们须要使用GN去生成ninja文件,再生成makefile,最后才是编译,所以:
可使用gn args out/foo
或者gn gen out/foo --args='is_debug=false target_cpu="x64" v8_target_cpu="arm64" use_goma=true'
来生成ninja文件。
这一行命令官网没有详细解释,我在这里解释一下:
gn args out/foo => 经过参数形式指定输出目录,这个命令会弹出文本让你配置参数
gn gen out/foo => 指定GN构建输出的目录, 能够指定参数: --args='is_debug=false target_cpu="x64" v8_target_cpu="arm64" use_goma=true',这个命令不会弹出文本窗让你配置
gn args out/foo --list => 查看这个构建输出目录当时配置的参数
复制代码
若是嫌上面的方式麻烦,那么v8还提供了另一个脚原本集成这些步骤:v8gen
,命令以下:
alias v8gen=/path/to/v8/tools/dev/v8gen.py
v8gen -b 'V8 Linux64 - debug builder' -m client.v8 foo
复制代码
v8gen的原理是借助mb_config.pyl
文件。根据master配置(-m
)和builder配置(-b
)来生成编译文件,咱们在mb_config.pyl
找到对应的配置:
最后一个参数foo
是指定生成的二级目录,默认一级目录是out.gn
,以下:
你也可使用默认配置,直接v8gen foo
接下去使用ninja来编译:
ninja -C out/foo
若是想要指定生成指定目标则:
ninja -C out/foo d8
上述编译正常会报错:goma/gomacc: No such file or directory
。由于咱们本地没有安装goma,因此想要正常编译下去,还须要安装一下goma,goma是什么东西呢?从官网上看,它是一个辅助编译加速的工具,详细能够参考:goma
除了上述总体v8工程编译,若是你想利用v8编译单个文件的话,好比在官网提到的编译Hello.cc
中使用到了g++
命令,对于g++
命令有些参数是你必须了解的,这里整理了一份,请参考:
g++ -I. -Iinclude samples/hello-world.cc -o hello_world -lv8_monolith -Lout.gn/x64.release.sample/obj/ -pthread -std=c++0x
G++命令解释以下:
-std=
决定使用的语言标准,当编译C和C++的时候该选择支持配置。
上述命令中的`c++0x`表示:
语言标准使用即将发布的ISO c++ 0x标准的工做草案。此选项支持可能包含在c++ 0x中的实验性特性。工做草案在不断地变化,若是GCC的将来版本不属于c++ 0x标准,那么由这个标志启用的任何特性均可能被删除。
更多标准请参考:[g++](https://linux.die.net/man/1/g++)
-pthread
使用POSIX线程库添加对多线程的支持。此选项为预处理器和连接器设置标志。它不影响编译器生成的目标代码的线程安全性,也不影响与其提供的库的线程安全性。这些是特定于HP-UX的标志。
-I dir
将目录dir添加到要搜索头文件的目录列表中。在系统标准包含目录以前,搜索由**-I**指定的目录。若是目录*dir*是标准的系统包含目录,则忽略该选项,以确保不会破坏系统目录的默认搜索顺序和对系统头文件的特殊处理。若是*dir*以"="开头,则"="将被sysroot前缀替换。
-o file
指定输出文件。这与将file指定为cpp的第二个非选项参数相同。gcc 对第二个非选项参数的有另外一种解释,所以必须使用-o指定输出文件
-llibrary
-l library
连接时搜索名为library的库。(第二种指定库文件的方式仅适用于POSIX听从性,不建议使用。)
在命令中编写这个选项的位置会有所不一样;连接器按照指定的顺序搜索和处理库和目标文件。所以,`foo.o -lz bar.o`是在文件foo.o以后搜索库z。但在bar.o以前。若是bar.o是引用到了z库中的函数,这些函数是不能被加载。
连接器搜索库的标准目录列表,其实是一个名为`liblibrary.a`的文件。而后连接器使用这个文件,就好像它是经过名称精确指定的同样。
搜索的目录包括几个标准系统目录,以及您使用-L指定的任何目录。
一般以这种方式找到的文件是库文件——其成员是目标文件的归档文件。连接器经过扫描成员来处理存档文件,这些成员定义了到目前为止已经引用但还没有定义的符号。可是,若是找到的文件是一个普通的对象文件,则以一般的方式连接它。
-Ldir
添加`dir`目录到搜索目录列表中去供`-l`使用
复制代码
这样上述命令想必一目了然了吧
在[译文]V8学习的高级进阶完整详细地介绍了不少概念,这里只是再把这些概念简化掉,让你们的记忆更加深入。
这个概念在[译文]V8学习的高级进阶没有说起到,它表示的一个独立的V8虚拟机,拥有本身的堆栈。因此才取名isolate,意为“隔离”。在v8中使用如下语法进行初始化:
Isolate* isolate = Isolate::New(create_params);
复制代码
handle是指向对象的指针,在V8中,全部的对象都经过handle来引用,handle主要用于V8的垃圾回收机制。在 V8 中,handle 分为两种:持久化 (Persistent)handle 和本地 (Local)handle,持久化 handle 存放在堆上,而本地 handle 存放在栈上。好比我要使用本地句柄,句柄指向的内容是一个string,那么你要这么定义:
Local<String> source = String::NewFromUtf8(isolate, "'Hello' + ', World'", NewStringType::kNormal).ToLocalChecked();
鉴于一个个释放Handle比较麻烦,v8又提供了HandleScope
来批量处理,你能够在handle以前声明好:
HandleScope handle_scope(isolate);
context 是一个执行器环境,使用 context 能够将相互分离的 JavaScript 脚本在同一个 V8 实例中运行,而互不干涉。在运行 JavaScript 脚本是,须要显式的指定 context 对象。建立上下文,须要这样:
// 建立一个上下文
Local<Context> context = Context::New(isolate);
// 进入上下文编译和运行脚本
Context::Scope context_scope(context);
复制代码
因为 C++ 原生数据类型与 JavaScript 中数据类型有很大差别,所以 V8 提供了 Data 类,从 JavaScript 到 C++,从 C++ 到 JavaScrpt 都会用到这个类及其子类,好比:
String::NewFromUtf8(info.GetIsolate(), "version").ToLocalChecked()
复制代码
这里的String即是V8的数据类型。再好比:
v8::Integer::New(info.GetIsolate(), 10);
复制代码
这两个模板类用以定义 JavaScript 对象和 JavaScript 函数。咱们在后续的小节部分将会接触到模板类的实例。经过使用 ObjectTemplate,能够将 C++ 中的对象暴露给脚本环境,相似的,FunctionTemplate 用以将 C++ 函数暴露给脚本环境,以供脚本使用。
就此,对于v8的了解应该有了必定的雏形了,v8里面有不少重要的概念,想要继续深刻的能够参考另一篇v8的实际应用文章了:如何正确地使用v8嵌入到咱们的C++应用中