V8这个概念大家都不陌生了,那么你动手编译过V8源码吗?编译后有尝试去了解V8背后的一些概念吗?如果没有,那么也不用心慌,下文将跟大家一一解释这些东西。在编译V8之前我们先要了解一个东西-构建系统

1、构建系统

1.1、构建系统是啥?

写惯前端的童鞋可能不是很明白这个东西是干啥用的?但是其实平时你都会接触到,只是概念不同而已。前端我们一般称其为打包构建,类似工具诸如webpack、parcel做的事情。其实最后的目标都是想得到一些目标性的文件。这里可以简单地提及一下软件工程中的构建系统的历史。

构建系统的需求是随着软件规模的增大而提出的。如果只是做简单的demo,通常代码量比较小,编写的源代码只有几个文件。比如你编写了一段代码放入helloworld.cpp文件中,要编译这段代码,只需要执行以下命令:

g++ helloworld.c -o helloworld

当软件规模逐渐增加,这时可能有几十个源代码文件,而且有了模块划分,有的要编译成静态库,有的要编译成动态库,最后链接成可执行代码,这时命令行方式就捉襟见肘,需要一个构建系统。常见的构建系统有GNU Make。需要注意的是,构建系统并不是取代gcc这样的工具链,而是定义编译规则,最终还是会调用工具链编译代码。

当软件规模进一步扩大,特别是有多平台支持需求的时候,编写GNU Makefile将是一件繁琐和乏味的事情,而且极容易出错。这时就出现了生成Makefile的工具,比如CmakeAutoMake等等,这种构建系统称作元构建系统(meta build system)。在Linux上软件仓库的概念还没有普及的时候,通常我们安装软件的步骤是:

./configure
make
make install

第一步就是调用一些自动化工具,根据系统环境(系统的版本众多,软件安装情况也不一样),生成GNU Makefile。然后第二步才使用gcc或者g++命令去编译所有文件,最后一步便是将所有文件链接起来成可执行命令并安装到系统的某个指定目录。

一般后两个步骤都是比较固化的,能提高工作效率的也就是在第一步了。于是V8团队针对自己的项目特点,撸了一个叫做GYP(Generate Your Projects)的构建系统,后面你要是看到node-gyp其实就是基于这个做的js版本。不过后面GYP被v8团队废弃掉,改用GN(Generate Ninja)构建系统。二者的区别不是本文重点,有兴趣的童鞋可以查看这篇文章: chromium中的GN构建系统

有意思的是尽管v8彻底废弃掉了GYP,但是nodejs仍然在使用GYP,这个R大在创建deno项目的时候有提及到:Design Mistakes in Node

1.1.1、GN构建系统简介

GN(Generate Ninja)是chromium project用来取代GYP的新工具,由于GN是用C++编写,比起用 python写的GYP快了很多,GN新的DSL的语法也被认为是比较好写以及维护的。

在v8项目的根目录下有个.gn文件,内容如下(去掉所有注释了):

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构建系统便完成了。

1.1.2、Ninja构建系统

有了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.ninja

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进行了测试对比。

2、编译并测试V8代码

接下来我们开始进行v8代码的编译操作。官网的文档给的已经很齐全了,这里只是再简单说一下,并提及一些官网没有给出的基本知识。

2.1、下载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

2.2、编译v8代码

编译v8代码官网同样给的很详细:传送门,这里总结一下而已,有两种编译方式

2.2.1、超便捷方式

使用gm这个集成所有为一体的python脚本可以几个命令就搞定:

alias gm=/path/to/v8/tools/dev/gm.py
gm x64.release
gm x64.release.check

2.2.2、手动编译方式

按照我们之前说的流程,我们需要使用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

3、编译单个引用到v8库的C++文件

除了上述整体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`使用

这样上述命令想必一目了然了吧

4、v8引擎基本概念简述

[译文]V8学习的高级进阶完整详细地介绍了很多概念,这里只是再把这些概念简化掉,让大家的记忆更加深刻。

4.1、isolate

这个概念在[译文]V8学习的高级进阶没有提及到,它表示的一个独立的V8虚拟机,拥有自己的堆栈。所以才取名isolate,意为“隔离”。在v8中使用以下语法进行初始化:

Isolate* isolate = Isolate::New(create_params);

4.2、handle

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);

4.3、context

context 是一个执行器环境,使用 context 可以将相互分离的 JavaScript 脚本在同一个 V8 实例中运行,而互不干涉。在运行 JavaScript 脚本是,需要显式的指定 context 对象。创建上下文,需要这样:

// 创建一个上下文
Local<Context> context = Context::New(isolate);

// 进入上下文编译和运行脚本
Context::Scope context_scope(context);

4.4、V8的数据类型

由于 C++ 原生数据类型与 JavaScript 中数据类型有很大差异,因此 V8 提供了 Data 类,从 JavaScript 到 C++,从 C++ 到 JavaScrpt 都会用到这个类及其子类,比如:

String::NewFromUtf8(info.GetIsolate(), "version").ToLocalChecked()

这里的String便是V8的数据类型。再比如:

v8::Integer::New(info.GetIsolate(), 10);

4.5、对象模板和函数模板

这两个模板类用以定义 JavaScript 对象和 JavaScript 函数。我们在后续的小节部分将会接触到模板类的实例。通过使用 ObjectTemplate,可以将 C++ 中的对象暴露给脚本环境,类似的,FunctionTemplate 用以将 C++ 函数暴露给脚本环境,以供脚本使用。

最后

就此,对于v8的了解应该有了一定的雏形了,v8里面有很多重要的概念,想要继续深入的可以参考另外一篇v8的实际应用文章了:如何正确地使用v8嵌入到我们的C++应用中

参考

  1. chromium中的GN构建系统
  2. GYP,GN和Ninja
  3. depot_tools_tutorial(7) Manual Page
  4. GN Reference