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++應用中