作者:京東科技 于飛躍
一、背景
如圖所示,Roma框架是我們自主研發(fā)的動(dòng)態(tài)化跨平臺解決方案,已支持iOS,android,web三端。目前在京東金融APP已經(jīng)有200+頁面,200+樂高樓層使用,為保證基于Roma框架開發(fā)的業(yè)務(wù)可以零成本、無縫運(yùn)行到鴻蒙系統(tǒng),需要將Roma框架適配到鴻蒙系統(tǒng)。
Roma框架是基于JS引擎運(yùn)行的,在iOS系統(tǒng)使用系統(tǒng)內(nèi)置的JavascriptCore,在Android系統(tǒng)使用V8,然而,鴻蒙系統(tǒng)卻沒有可以執(zhí)行Roma框架的JS引擎,因此需要移植一個(gè)JS引擎到鴻蒙平臺。
二、JS引擎選型
目前主流的JS引擎有以下這些:
引擎名稱 | 應(yīng)用代表 | 公司 |
---|---|---|
V8 | Chrome/Opera/Edge/Node.js/Electron | |
SpiderMonkey | firefox | Mozilla |
JavaScriptCore | Safari | Apple |
Chakra | IE | Microsoft |
Hermes | React Native | |
JerryScript/duktape/QuickJS | 小型并且可嵌入的Javascript引擎/主要應(yīng)用于IOT設(shè)備 | - |
其中最流行的是Google開源的V8引擎,除了Chrome等瀏覽器,Node.js也是用的V8引擎。Chrome的市場占有率高達(dá)60%,而Node.js是JS后端編程的事實(shí)標(biāo)準(zhǔn)。另外,Electron(桌面應(yīng)用框架)是基于Node.js與Chromium開發(fā)桌面應(yīng)用,也是基于V8的。國內(nèi)的眾多瀏覽器,其實(shí)也都是基于Chromium瀏覽器開發(fā),而Chromium相當(dāng)于開源版本的Chrome,自然也是基于V8引擎的。甚至連瀏覽器界獨(dú)樹一幟的Microsoft也投靠了Chromium陣營。V8引擎使得JS可以應(yīng)用在Web、APP、桌面端、服務(wù)端以及IOT等各個(gè)領(lǐng)域。
三、V8引擎的工作原理
V8的主要任務(wù)是執(zhí)行JavaScript代碼,并且能夠處理JavaScript源代碼、即時(shí)編譯(JIT)代碼以及執(zhí)行代碼。v8是一個(gè)非常復(fù)雜的項(xiàng)目,有超過100萬行C++代碼。
下圖展示了它的基本工作流程:
如圖所示,它通過詞法分析、語法分析、字節(jié)碼生成與執(zhí)行、即時(shí)編譯與機(jī)器碼生成以及垃圾回收等步驟,實(shí)現(xiàn)了對JavaScript源代碼的高效執(zhí)行。此外,V8引擎還通過監(jiān)控代碼的執(zhí)行情況,對熱點(diǎn)函數(shù)進(jìn)行自動(dòng)優(yōu)化,從而進(jìn)一步提高了代碼的執(zhí)行性能。其中Parser(解析器)、Ignition(解釋器)、TurboFan(編譯器)、Orinoco(垃圾回收)是 V8 中四個(gè)核心工作模塊,對應(yīng)的V8源碼目錄如下圖。
1、Parser:解析器
負(fù)責(zé)將JavaScript源碼轉(zhuǎn)換為Abstract Syntax Tree (AST)抽象語法樹,解析過程分為:詞法分析(Lexical Analysis)和語法分析(Syntax Analysis)兩個(gè)階段。
1.1、詞法分析
V8 引擎首先會掃描所有的源代碼,進(jìn)行詞法分析(Tokenizing/Lexing)(詞法分析是通過 Scanner 模塊來完成的)。也稱為分詞,是將字符串形式的代碼轉(zhuǎn)換為標(biāo)記(token)序列的過程。這里的token是一個(gè)字符串,是構(gòu)成源代碼的最小單位,類似于英語中單詞,例如,var a = 2;經(jīng)過詞法分析得到的tokens如下:
從上圖中可以看到,這句代碼最終被分解出了五個(gè)詞法單元:
var 關(guān)鍵字
a 標(biāo)識符
= 運(yùn)算符
2 數(shù)值
;分號
一個(gè)可以在線查看Tokens的網(wǎng)站: https://esprima.org/demo/parse.html
1.2、語法分析
語法分析是將詞法分析產(chǎn)生的token按照某種給定的形式文法(這里是JavaScript語言的語法規(guī)則)轉(zhuǎn)換成抽象語法樹(AST)的過程。也就是把單詞組合成句子的過程。這個(gè)過程會分析語法錯(cuò)誤:遇到錯(cuò)誤的語法會拋出異常。AST是源代碼的語法結(jié)構(gòu)的樹形表示。AST包含了源代碼中的所有語法結(jié)構(gòu)信息,但不包含代碼的執(zhí)行邏輯。
例如,var a = 2;經(jīng)過語法分析后生成的AST如下:
可以看到這段程序的類型是VariableDeclaration,也就是說這段代碼是用來聲明變量的。
一個(gè)可以在線查看AST結(jié)構(gòu)的網(wǎng)站:https://astexplorer.net/
2、Ignition:(interpreter)解釋器
負(fù)責(zé)將AST轉(zhuǎn)換成字節(jié)碼(Bytecode)并逐行解釋執(zhí)行字節(jié)碼,提供快速的啟動(dòng)和較低的內(nèi)存使用,同時(shí)會標(biāo)記熱點(diǎn)代碼,收集TurboFan優(yōu)化編譯所需的信息,比如函數(shù)參數(shù)的類型。
2.1、什么是字節(jié)碼?
字節(jié)碼(Bytecode)是一種介于AST和機(jī)器碼之間的中間表示形式,它比AST更接近機(jī)器碼,它比機(jī)器碼更抽象,也更輕量,與特定機(jī)器代碼無關(guān),需要解釋器轉(zhuǎn)譯后才能成為機(jī)器碼。字節(jié)碼通常不像源碼一樣可以讓人閱讀,而是編碼后的數(shù)值常量、引用、指令等構(gòu)成的序列。
2.2、字節(jié)碼的優(yōu)點(diǎn)
?不針對特定CPU架構(gòu)
?比原始的高級語言轉(zhuǎn)換成機(jī)器語言更快
?字節(jié)碼比機(jī)器碼占用內(nèi)存更小
?利用字節(jié)碼,可以實(shí)現(xiàn)Compile Once,Run anywhere(一次編譯到處運(yùn)行)。
早期版本的 V8 ,并沒有生成中間字節(jié)碼的過程,而是將所有源碼轉(zhuǎn)換為了機(jī)器代碼。機(jī)器代碼雖然執(zhí)行速度更快,但是占用內(nèi)存大。
2.3、查看字節(jié)碼
Node.js是基于V8引擎實(shí)現(xiàn)的,因此node命令提供了很多V8引擎的選項(xiàng),我們可以通過這些選項(xiàng),查看V8引擎中各個(gè)階段的產(chǎn)物。使用node的--print-bytecode選項(xiàng),可以打印出Ignition生成的Bytecode。
示例test.js如下
//test.js function add(a, b){ return a + b; } add(1,2); //V8不會編譯沒有被調(diào)用的函數(shù),因此需要在最后一行調(diào)用add函數(shù)
運(yùn)行下面的node命令,打印出Ignition生成的字節(jié)碼。
node --print-bytecode test.js [generated bytecode for function: add (0x29e627015191 )] Bytecode length: 6 Parameter count 3 Register count 0 Frame size 0 OSR urgency: 0 Bytecode age: 0 33 S> 0x29e627015bb8 @ 0 : 0b 04 Ldar a1 41 E> 0x29e627015bba @ 2 : 39 03 00 Add a0, [0] 44 S> 0x29e627015bbd @ 5 : a9 Return Constant pool (size = 0) Handler Table (size = 0) Source Position Table (size = 8) 0x29e627015bc1
控制臺輸出的內(nèi)容非常多,最后一部分是add函數(shù)的Bytecode。
字節(jié)碼的詳細(xì)信息如下:
?[generated bytecode for function: add (0x29e627015191)]: 這行告訴我們,接下來的字節(jié)碼是為 add 函數(shù)生成的。0x29e627015191 是這個(gè)函數(shù)在內(nèi)存中的地址。
?Bytecode length: 6: 整個(gè)字節(jié)碼的長度是 6 字節(jié)。
?Parameter count 3: 該函數(shù)有 3 個(gè)參數(shù)。包括傳入的 a,b 以及 this。
?Register count 0: 該函數(shù)沒有使用任何寄存器。
?Frame size 0: 該函數(shù)的幀大小是 0。幀大小是指在調(diào)用棧上分配給這個(gè)函數(shù)的空間大小,用于存儲局部變量、函數(shù)參數(shù)等。
?OSR urgency: 0: On-Stack Replacement(OSR)優(yōu)化的緊急程度是 0。OSR 是一種在運(yùn)行時(shí)將解釋執(zhí)行的函數(shù)替換為編譯執(zhí)行的函數(shù)的技術(shù),用于提高性能。
?Bytecode age: 0: 字節(jié)碼的年齡是 0。字節(jié)碼的年齡是指它被執(zhí)行的次數(shù),年齡越高,說明這個(gè)字節(jié)碼被執(zhí)行的越頻繁,可能會被 V8 引擎優(yōu)化。
?Ldar a1 表示將寄存器中的值加載到累加器中 ,這行是字節(jié)碼的第一條指令
?Add a0, [0] 從 a0 寄存器加載值并且將其與累加器中的值相加,然后將結(jié)果再次放入累加器 。
?Return 結(jié)束當(dāng)前函數(shù)的執(zhí)行,并把控制權(quán)傳給調(diào)用方,將累加器中的值作為返回值
?S> 表示這是一個(gè)“Safepoint”指令,V8 引擎可以在執(zhí)行這條指令時(shí)進(jìn)行垃圾回收等操作。
?E> 表示這是一個(gè)“Effect”指令,可能會改變程序的狀態(tài)。
?Constant pool (size = 0): 常量池的大小是 0。常量池是用來存儲函數(shù)中使用的常量值的。
?Handler Table (size = 0): 異常處理表的大小是 0。異常處理表是用來處理函數(shù)中可能出現(xiàn)的異常的。
?Source Position Table (size = 8): 源代碼位置表的大小是 8。源代碼位置表是用來將字節(jié)碼指令與源代碼行號關(guān)聯(lián)起來的,方便調(diào)試。
?0x29e627015bc1 : 這行是源代碼位置表的具體內(nèi)容,顯示了每個(gè)字節(jié)碼指令對應(yīng)的源代碼行號和列號。
可以看到,Bytecode某種程度上就是匯編語言,只是它沒有對應(yīng)特定的CPU,或者說它對應(yīng)的是虛擬的CPU。這樣的話,生成Bytecode時(shí)簡單很多,無需為不同的CPU生產(chǎn)不同的代碼。要知道,V8支持9種不同的CPU,引入一個(gè)中間層Bytecode,可以簡化V8的編譯流程,提高可擴(kuò)展性。如果我們在不同硬件上去生成Bytecode,生成代碼的指令是一樣的.
3、TurboFan:(compiler)編譯器
V8 的優(yōu)化編譯器也是v8實(shí)現(xiàn)即時(shí)編譯(JIT)的核心,負(fù)責(zé)將熱點(diǎn)函數(shù)的字節(jié)碼編譯成高效的機(jī)器碼。
3.1、什么是JIT?
我們需要先了解一下JIT(Just in Time)即時(shí)編譯。
在運(yùn)行C、C++以及Java等程序之前,需要進(jìn)行編譯,不能直接執(zhí)行源碼;但對于JavaScript來說,我們可以直接執(zhí)行源碼(比如:node server.js),它是在運(yùn)行的時(shí)候先編譯再執(zhí)行,這種方式被稱為即時(shí)編譯(Just-in-time compilation),簡稱為JIT。因此,V8也屬于JIT編譯器。
實(shí)現(xiàn)JIT編譯器的系統(tǒng)通常會不斷地分析正在執(zhí)行的代碼,并確定代碼的某些部分,在這些部分中,編譯或重新編譯所獲得的加速將超過編譯該代碼的開銷。 JIT編譯是兩種傳統(tǒng)的機(jī)器代碼翻譯方法——提前編譯(AOT)和解釋——的結(jié)合,它結(jié)合了兩者的優(yōu)點(diǎn)和缺點(diǎn)。大致來說,JIT編譯將編譯代碼的速度與解釋的靈活性、解釋器的開銷以及額外的編譯開銷(而不僅僅是解釋)結(jié)合起來。
除了V8引擎,Java虛擬機(jī)、PHP 8也用到了JIT。
3.2、V8引擎的JIT
V8的JIT編譯包括多個(gè)階段,從生成字節(jié)碼到生成高度優(yōu)化的機(jī)器碼,根據(jù)JavaScript代碼的執(zhí)行特性動(dòng)態(tài)地優(yōu)化代碼,以實(shí)現(xiàn)高性能的JavaScript執(zhí)行。看下圖Ignition和TurboFan的交互:
當(dāng) Ignition 開始執(zhí)行 JavaScript 代碼后,V8 會一直觀察 JavaScript 代碼的執(zhí)行情況,并記錄執(zhí)行信息,如每個(gè)函數(shù)的執(zhí)行次數(shù)、每次調(diào)用函數(shù)時(shí),傳遞的參數(shù)類型等。如果一個(gè)函數(shù)被調(diào)用的次數(shù)超過了內(nèi)設(shè)的閾值,監(jiān)視器就會將當(dāng)前函數(shù)標(biāo)記為熱點(diǎn)函數(shù)(Hot Function),并將該函數(shù)的字節(jié)碼以及執(zhí)行的相關(guān)信息發(fā)送給 TurboFan。TurboFan 會根據(jù)執(zhí)行信息做出一些進(jìn)一步優(yōu)化此代碼的假設(shè),在假設(shè)的基礎(chǔ)上將字節(jié)碼編譯為優(yōu)化的機(jī)器代碼。如果假設(shè)成立,那么當(dāng)下一次調(diào)用該函數(shù)時(shí),就會執(zhí)行優(yōu)化編譯后的機(jī)器代碼,以提高代碼的執(zhí)行性能。
如果假設(shè)不成立,上圖中,綠色的線,是“去優(yōu)化(Deoptimize)”的過程,如果TurboFan生成的優(yōu)化機(jī)器碼,對需要執(zhí)行的代碼不適用,會把優(yōu)化的機(jī)器碼,重新轉(zhuǎn)換成字節(jié)碼來執(zhí)行。這是因?yàn)镮gnition收集的信息可能是錯(cuò)誤的。
例如:
function add(a, b) { return a + b; } add(1, 2); add(2, 2); add("1", "2");
add函數(shù)的參數(shù)之前是整數(shù),后來又變成了字符串。生成的優(yōu)化機(jī)器碼已經(jīng)假定add函數(shù)的參數(shù)是整數(shù),那當(dāng)然是錯(cuò)誤的,于是需要進(jìn)行去優(yōu)化,Deoptimize為Bytecode來執(zhí)行。
TurboFan除了上面基于類型做優(yōu)化和反優(yōu)化,還有包括內(nèi)聯(lián)(inlining)和逃逸分析(Escape Analysis)等,內(nèi)聯(lián)就是將相關(guān)聯(lián)的函數(shù)進(jìn)行合并。例如:
function add(a, b) { return a + b } function foo() { return add(2, 4) }
內(nèi)聯(lián)優(yōu)化后:
function fooAddInlined() { var a = 2 var b = 4 var addReturnValue = a + b return addReturnValue } // 因?yàn)?fooAddInlined 中 a 和 b 的值都是確定的,所以可以進(jìn)一步優(yōu)化 function fooAddInlined() { return 6 }
使用node命令的--print-code以及--print-opt-code選項(xiàng),可以打印出TurboFan生成的匯編代碼。
node --print-code --print-opt-code test.js
4、Orinoco:垃圾回收
一個(gè)高效的垃圾回收器,用于自動(dòng)管理內(nèi)存,回收不再使用的對象內(nèi)存;它使用多種垃圾回收策略,如分代回收、標(biāo)記-清除、增量標(biāo)記等,以實(shí)現(xiàn)高效內(nèi)存管理。
Orinoco的主要特點(diǎn)包括:
?并發(fā)標(biāo)記: Orinoco使用并發(fā)標(biāo)記技術(shù)來減少垃圾回收的停頓時(shí)間(Pause Time)。這意味著在應(yīng)用程序繼續(xù)執(zhí)行的同時(shí),垃圾回收器可以在后臺進(jìn)行標(biāo)記操作。
?增量式垃圾回收: Orinoco支持增量式垃圾回收,這允許垃圾回收器在小的時(shí)間片內(nèi)執(zhí)行部分垃圾回收工作,而不是一次性處理所有的垃圾。
?更高效的內(nèi)存管理: Orinoco引入了一些新的內(nèi)存管理策略和數(shù)據(jù)結(jié)構(gòu),旨在減少內(nèi)存碎片和提高內(nèi)存利用率。
?可擴(kuò)展性: Orinoco的設(shè)計(jì)考慮了可擴(kuò)展性,使得它可以適應(yīng)不同的工作負(fù)載和硬件配置。
?多線程支持: Orinoco支持多線程環(huán)境,可以利用多核CPU來加速垃圾回收過程。
四、V8移植工具選型
我們的開發(fā)環(huán)境各式各樣可能系統(tǒng)是Mac,Linux或者Windows,架構(gòu)是x86或者arm,所以要想編譯出可以跑在鴻蒙系統(tǒng)上的v8庫我們需要使用交叉編譯,它是在一個(gè)平臺上為另一個(gè)平臺編譯代碼的過程,允許我們在一個(gè)平臺上為另一個(gè)平臺生成可執(zhí)行文件。這在嵌入式系統(tǒng)開發(fā)中尤為常見,因?yàn)樵S多嵌入式設(shè)備的硬件資源有限,不適合直接在上面編譯代碼。交叉編譯需要一個(gè)特定的編譯器、鏈接器和庫,這些都是為目標(biāo)平臺設(shè)計(jì)的。此外,開發(fā)者還需要確保代碼沒有平臺相關(guān)的依賴,否則編譯可能會失敗。
v8官網(wǎng)上關(guān)于交叉編譯Android和iOS平臺的V8已經(jīng)有詳細(xì)的介紹。尚無關(guān)于鴻蒙OHOS平臺的文檔。V8官方使用的構(gòu)建系統(tǒng)是gn + ninja。gn是一個(gè)元構(gòu)建系統(tǒng),最初由Google開發(fā),用于生成Ninja文件。它提供了一個(gè)聲明式的方式來定義項(xiàng)目的依賴關(guān)系、編譯選項(xiàng)和其他構(gòu)建參數(shù)。通過運(yùn)行g(shù)n gen命令,可以生成一個(gè)Ninja文件。類似于camke + make構(gòu)建系統(tǒng)。
gn + ninja的構(gòu)建流程如下:
通過查看鴻蒙sdk,我們發(fā)現(xiàn)鴻蒙提供給開發(fā)者的native構(gòu)建系統(tǒng)是cmake + ninja,所以我們決定將v8官方采用的gn + ninja轉(zhuǎn)成cmake + ninja。這就需要將gn語法的構(gòu)建配置文件轉(zhuǎn)成cmake的構(gòu)建配置文件。
1、CMake簡介
CMake是一個(gè)開源的、跨平臺的構(gòu)建系統(tǒng)。它不僅可以生成標(biāo)準(zhǔn)的Unix Makefile配合make命令使用,還能夠生成build.ninja文件配合ninja使用,還可以為多種IDE生成項(xiàng)目文件,如Visual Studio、Eclipse、Xcode等。這種跨平臺性使得CMake在多種操作系統(tǒng)和開發(fā)環(huán)境中都能夠無縫工作。
cmake的構(gòu)建流程如下:
CMake構(gòu)建主要過程是編寫CMakeLists.txt文件,然后用cmake命令將CMakeLists.txt文件轉(zhuǎn)化為make所需要的Makefile文件或者ninja需要的build.ninja文件,最后用make命令或者ninja命令執(zhí)行編譯任務(wù)生成可執(zhí)行程序或共享庫(so(shared object))。
完整CMakeLists.txt文件的主要配置樣例:
# 1. 聲明要求的cmake最低版本 cmake_minimum_required( VERSION 2.8 ) # 2. 添加c++11標(biāo)準(zhǔn)支持 #set( CMAKE_CXX_FLAGS "-std=c++11" ) # 3. 聲明一個(gè)cmake工程 PROJECT(camke_demo) MESSAGE(STATUS "Project: SERVER") #打印相關(guān)消息 # 4. 頭文件 include_directories( ${PROJECT_SOURCE_DIR}/../include/mq ${PROJECT_SOURCE_DIR}/../include/incl ${PROJECT_SOURCE_DIR}/../include/rapidjson ) # 5. 通過設(shè)定SRC變量,將源代碼路徑都給SRC,如果有多個(gè),可以直接在后面繼續(xù)添加 set(SRC ${PROJECT_SOURCE_DIR}/../include/incl/tfc_base_config_file.cpp ${PROJECT_SOURCE_DIR}/../include/mq/tfc_ipc_sv.cpp ${PROJECT_SOURCE_DIR}/../include/mq/tfc_net_ipc_mq.cpp ${PROJECT_SOURCE_DIR}/../include/mq/tfc_net_open_mq.cpp ) # 6. 創(chuàng)建共享庫/靜態(tài)庫 # 設(shè)置路徑(下面生成共享庫的路徑) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/lib) # 即生成的共享庫在工程文件夾下的lib文件夾中 set(LIB_NAME camke_demo_lib) # 創(chuàng)建共享庫(把工程內(nèi)的cpp文件都創(chuàng)建成共享庫文件,方便通過頭文件來調(diào)用) # 這時(shí)候只需要cpp,不需要有主函數(shù) # ${PROJECT_NAME}是生成的庫名 表示生成的共享庫文件就叫做 lib工程名.so # 也可以專門寫cmakelists來編譯一個(gè)沒有主函數(shù)的程序來生成共享庫,供其它程序使用 # SHARED為生成動(dòng)態(tài)庫,STATIC為生成靜態(tài)庫 add_library(${LIB_NAME} STATIC ${SRC}) # 7. 鏈接庫文件 # 把剛剛生成的${LIB_NAME}庫和所需的其它庫鏈接起來 # 如果需要鏈接其他的動(dòng)態(tài)庫,-l后接去除lib前綴和.so后綴的名稱,以鏈接 # libpthread.so 為例,-lpthread target_link_libraries(${LIB_NAME} pthread dl) # 8. 編譯主函數(shù),生成可執(zhí)行文件 # 先設(shè)置路徑 set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/bin) # 可執(zhí)行文件生成 add_executable(${PROJECT_NAME} ${SRC}) # 鏈接這個(gè)可執(zhí)行文件所需的庫 target_link_libraries(${PROJECT_NAME} pthread dl ${LIB_NAME})
一般把CMakeLists.txt文件放在工程目錄下,使用時(shí)先創(chuàng)建一個(gè)叫build的文件夾(這個(gè)并非必須,因?yàn)閏make命令指向CMakeLists.txt所在的目錄,例如cmake ..表示CMakeLists.txt在當(dāng)前目錄的上一級目錄。cmake執(zhí)行后會生成很多編譯的中間文件,所以一般建議新建一個(gè)新的目錄,專門用來編譯),通常構(gòu)建步驟如下:
1.mkdir build 2.cd build 3.cmake .. 或者 cmake -G Ninja .. 4.make 或者 ninja
其中cmake ..在build文件夾下生成Makefile。make命令在Makefile所在的目錄下執(zhí)行,根據(jù)Makefile進(jìn)行編譯。
或者cmake -G Ninja ..在build文件夾下生成build.ninja。ninja命令在build.ninja所在的目錄下執(zhí)行,根據(jù)build.ninja進(jìn)行編譯。
2、CMake中的交叉編譯設(shè)置
配置方式一:
直接在CMakeLists.txt文件中,使用CMAKE_C_COMPILER和CMAKE_CXX_COMPILER這兩個(gè)變量來指定C和C++的編譯器路徑。使用CMAKE_LINKER變量來指定項(xiàng)目的鏈接器。這樣,當(dāng)CMake生成構(gòu)建文件時(shí),就會使用指定的編譯器來編譯源代碼。使用指定的鏈接器進(jìn)行項(xiàng)目的鏈接操作。
以下是一個(gè)簡單的設(shè)置交叉編譯器和鏈接器的CMakeLists.txt文件示例:
# 指定CMake的最低版本要求 cmake_minimum_required(VERSION 3.10) # 項(xiàng)目名稱 project(CrossCompileExample) # 設(shè)置C編譯器和C++編譯器 set(CMAKE_C_COMPILER "/path/to/c/compiler") set(CMAKE_CXX_COMPILER "/path/to/cxx/compiler") # 設(shè)置鏈接器 set(CMAKE_LINKER "/path/to/linker") # 添加可執(zhí)行文件 add_executable(myapp main.cpp)
另外我們還可以使用單獨(dú)工具鏈文件配置交叉編譯環(huán)境。
配置方式二:CMake中使用工具鏈文件配置
工具鏈文件(toolchain file)是將配置信息提取到一個(gè)單獨(dú)的文件中,以便于在多個(gè)項(xiàng)目中復(fù)用。包含一系列CMake變量定義,這些變量指定了編譯器、鏈接器和其他工具的位置,以及其他與目標(biāo)平臺相關(guān)的設(shè)置,以確保它能夠正確地為目標(biāo)平臺生成代碼。它讓我們可以專注于解決實(shí)際的問題,而不是每次都要手動(dòng)配置編譯器和工具。
一個(gè)基本的工具鏈文件示例如下:
創(chuàng)建一個(gè)名為toolchain.cmake的文件,并在其中定義工具鏈的路徑和設(shè)置:
該項(xiàng)目需要為ARM架構(gòu)的Linux系統(tǒng)進(jìn)行交叉編譯
# 設(shè)置C和C++編譯器 set(CMAKE_C_COMPILER "/path/to/c/compiler") set(CMAKE_CXX_COMPILER "/path/to/cxx/compiler") # 設(shè)置鏈接器 set(CMAKE_LINKER "/path/to/linker") # 指定目標(biāo)系統(tǒng)的類型 set(CMAKE_SYSTEM_NAME Linux) set(CMAKE_SYSTEM_PROCESSOR arm) # 其他與目標(biāo)平臺相關(guān)的設(shè)置 # ...
在執(zhí)行cmake命令構(gòu)建時(shí),使用-DCMAKE_TOOLCHAIN_FILE參數(shù)指定工具鏈文件的路徑:
cmake -DCMAKE_TOOLCHAIN_FILE=/path/to/toolchain.cmake /path/to/source
這樣,CMake就會使用工具鏈文件中指定的編譯器和設(shè)置來為目標(biāo)平臺生成代碼。
五、V8和常規(guī)C++庫移植的重大差異
常規(guī)C++項(xiàng)目按照上述交叉編譯介紹的配置即可完成交叉編譯過程,但是V8的移植必須充分理解builtin和snapshot才能完成!一般的庫,所謂交叉編譯就是調(diào)用目標(biāo)平臺指定的工具鏈直接編譯源碼生成目標(biāo)平臺的文件。比如一個(gè)C文件要給android用,調(diào)用ndk包的gcc、clang編譯即可。但由于v8的builtin實(shí)際用的是v8自己的工具鏈體系編譯成目標(biāo)平臺的代碼,所以并不能套用上面的方式。
1、builtin
1.1、builtin是什么
在V8引擎中,builtin即內(nèi)置函數(shù)或模塊。V8的內(nèi)置函數(shù)和模塊是JavaScript語言的一部分,提供了一些基本的功能,例如數(shù)學(xué)運(yùn)算、字符串操作、日期處理等。另外ignition解析器每一條字節(jié)碼指令實(shí)現(xiàn)也是一個(gè)builtin。
V8的內(nèi)置函數(shù)和模塊是通過C++代碼實(shí)現(xiàn)的,并在編譯時(shí)直接集成到V8引擎中。這些內(nèi)置函數(shù)和模塊不需要在JavaScript代碼中顯式地導(dǎo)入或引用,就可以直接使用。
以下是一些V8的內(nèi)置函數(shù)和模塊的例子:
?Math對象:提供了各種數(shù)學(xué)運(yùn)算的函數(shù),例如Math.sin()、Math.cos()等。
?String對象:提供了字符串操作的函數(shù),例如String.prototype.split()、String.prototype.replace()等。
?Date對象:提供了日期和時(shí)間處理的函數(shù),例如Date.now()、Date.parse()等。
?JSON對象:提供了JSON數(shù)據(jù)的解析和生成的函數(shù),例如JSON.parse()、JSON.stringify()等。
?ArrayBuffer對象:提供了對二進(jìn)制數(shù)據(jù)的操作的函數(shù),例如ArrayBuffer.prototype.slice()、ArrayBuffer.prototype.byteLength等。
?WebAssembly模塊:提供了對WebAssembly模塊的加載和實(shí)例化的函數(shù),例如WebAssembly.compile()、WebAssembly.instantiate()等。
這些內(nèi)置函數(shù)和模塊都是V8引擎的重要組成部分,提供了基礎(chǔ)的JavaScript功能。它們是V8運(yùn)行時(shí)最重要的“積木塊”;
1.2、builtin是如何生成的
v8源碼中builtin的編譯比較繞,因?yàn)関8中大多數(shù)builtin的“源碼”,其實(shí)是builtin的生成邏輯,這也是理解V8源碼的關(guān)鍵。
builtin和snapshot都是通過mksnapshot工具運(yùn)行生成的。mksnapshot是v8編譯過程中的一個(gè)中間產(chǎn)物,也就是說v8編譯過程中會生成一個(gè)mksnapshot可執(zhí)行程序并且會執(zhí)行它生成v8后續(xù)編譯需要的builtin和snapshot,就像套娃一樣。
例如v8源碼中字節(jié)碼Ldar指令的實(shí)現(xiàn)如下:
IGNITION_HANDLER(Ldar, InterpreterAssembler) { TNode value = LoadRegisterAtOperandIndex(0); SetAccumulator(value); Dispatch(); }
上述代碼只在V8的編譯階段由mksnapshot程序執(zhí)行,執(zhí)行后會產(chǎn)出機(jī)器碼(JIT),然后mksnapshot程序把生成的機(jī)器碼dump下來放到匯編文件embedded.S里,編譯進(jìn)V8運(yùn)行時(shí)(相當(dāng)于用JIT編譯器去AOT)。
builtin被dump到embedded.S的對應(yīng)v8源碼在v8/src/snapshot/embedded-file-writer.h
void WriteFilePrologue(PlatformEmbeddedFileWriterBase* w) const { w->Comment("Autogenerated file. Do not edit."); w->Newline(); w->FilePrologue(); }
上述Ldar指令dump到embedded.S后匯編代碼如下:
Builtins_LdarHandler: .def Builtins_LdarHandler; .scl 2; .type 32; .endef; .octa 0x72ba0b74d93b48fffffff91d8d48,0xec83481c6ae5894855ccffa9104ae800 .octa 0x2454894cf0e4834828ec8348e2894920,0x458948e04d894ce87d894cf065894c20 .octa 0x4d0000494f808b4500001410858b4dd8,0x1640858b49e1894c00000024bac603 .octa 0x4d00000000158d4ccc01740fc4f64000,0x2045c749d0ff206d8949285589 .octa 0xe4834828ec8348e289492024648b4800,0x808b4500001410858b4d202454894cf0 .octa 0x858b49d84d8b48d233c6034d00004953,0x158d4ccc01740fc4f64000001640 .octa 0x2045c749d0ff206d89492855894d0000,0x5d8b48f0658b4c2024648b4800000000 .octa 0x4cf7348b48007d8b48011c74be0f49e0,0x100000000ba49211cb60f43024b8d .octa 0xa90f4fe800000002ba0b77d33b4c0000,0x8b48006d8b48df0c8b49e87d8b4cccff .octa 0xcccccccccccccccc90e1ff30c48348c6 .byte 0xcc,0xcc,0xcc,0xcc,0xcc,0xcc,0xcc,0xcc,0xcc,0xcc,0xcc,0xcc,0xcc,0xcc,0xcc,0xcc
builtin在v8源代碼v8srcbuiltinsbuiltins-definitions.h中定義,這個(gè)文件還include一個(gè)根據(jù)ignition指令生成的builtin列表以及torque編譯器生成的builtin定義,一共1700+個(gè)builtin。每個(gè)builtin,都會在embedded.S中生成一段代碼。
builtin生成的v8源代碼在:v8srcbuiltinssetup-builtins-internal.cc
void SetupIsolateDelegate::SetupBuiltinsInternal(Isolate* isolate) { Builtins* builtins = isolate->builtins(); DCHECK(!builtins->initialized_); PopulateWithPlaceholders(isolate); // Create a scope for the handles in the builtins. HandleScope scope(isolate); int index = 0; Code code; #define BUILD_CPP(Name) code = BuildAdaptor(isolate, Builtin::k##Name, FUNCTION_ADDR(Builtin_##Name), #Name); AddBuiltin(builtins, Builtin::k##Name, code); index++; #define BUILD_TFJ(Name, Argc, ...) code = BuildWithCodeStubAssemblerJS( isolate, Builtin::k##Name, &Builtins::Generate_##Name, Argc, #Name); AddBuiltin(builtins, Builtin::k##Name, code); index++; #define BUILD_TFC(Name, InterfaceDescriptor) /* Return size is from the provided CallInterfaceDescriptor. */ code = BuildWithCodeStubAssemblerCS( isolate, Builtin::k##Name, &Builtins::Generate_##Name, CallDescriptors::InterfaceDescriptor, #Name); AddBuiltin(builtins, Builtin::k##Name, code); index++; #define BUILD_TFS(Name, ...) /* Return size for generic TF builtins (stub linkage) is always 1. */ code = BuildWithCodeStubAssemblerCS(isolate, Builtin::k##Name, &Builtins::Generate_##Name, CallDescriptors::Name, #Name); AddBuiltin(builtins, Builtin::k##Name, code); index++; #define BUILD_TFH(Name, InterfaceDescriptor) /* Return size for IC builtins/handlers is always 1. */ code = BuildWithCodeStubAssemblerCS( isolate, Builtin::k##Name, &Builtins::Generate_##Name, CallDescriptors::InterfaceDescriptor, #Name); AddBuiltin(builtins, Builtin::k##Name, code); index++; #define BUILD_BCH(Name, OperandScale, Bytecode) code = GenerateBytecodeHandler(isolate, Builtin::k##Name, OperandScale, Bytecode); AddBuiltin(builtins, Builtin::k##Name, code); index++; #define BUILD_ASM(Name, InterfaceDescriptor) code = BuildWithMacroAssembler(isolate, Builtin::k##Name, Builtins::Generate_##Name, #Name); AddBuiltin(builtins, Builtin::k##Name, code); index++; BUILTIN_LIST(BUILD_CPP, BUILD_TFJ, BUILD_TFC, BUILD_TFS, BUILD_TFH, BUILD_BCH, BUILD_ASM); #undef BUILD_CPP #undef BUILD_TFJ #undef BUILD_TFC #undef BUILD_TFS #undef BUILD_TFH #undef BUILD_BCH #undef BUILD_ASM // ... }
BUILTIN_LIST宏內(nèi)定義了所有的builtin,并根據(jù)其類型去調(diào)用不同的參數(shù),在這里參數(shù)是BUILD_CPP, BUILD_TFJ...這些,定義了不同的生成策略,這些參數(shù)去掉前綴代表不同的builtin類型(CPP, TFJ, TFC, TFS, TFH, BCH, ASM)
mksnapshot執(zhí)行時(shí)生成builtin的方式有兩種:
?直接生成機(jī)器碼,ASM和CPP類型builtin使用這種方式(CPP類型只是生成適配器)
?先生成turbofan的graph(IR),然后由turbofan編譯器編譯成機(jī)器碼,除ASM和CPP之外其它builtin類型都是這種
例如:DoubleToI是一個(gè)ASM類型builtin,功能是把double轉(zhuǎn)成整數(shù),該builtin的JIT生成邏輯位于Builtins::Generate_DoubleToI,如果是x64的window,該函數(shù)放在v8/src/builtins/x64/builtins-x64.cc文件。由于每個(gè)CPU架構(gòu)的指令都不一樣,所以每個(gè)CPU架構(gòu)都有一個(gè)實(shí)現(xiàn),放在各自的builtins-ArchName.cc文件。
x64的實(shí)現(xiàn)如下:
void Builtins::Generate_DoubleToI(MacroAssembler* masm) { Label check_negative, process_64_bits, done; // Account for return address and saved regs. const int kArgumentOffset = 4 * kSystemPointerSize; MemOperand mantissa_operand(MemOperand(rsp, kArgumentOffset)); MemOperand exponent_operand( MemOperand(rsp, kArgumentOffset + kDoubleSize / 2)); // The result is returned on the stack. MemOperand return_operand = mantissa_operand; Register scratch1 = rbx; // Since we must use rcx for shifts below, use some other register (rax) // to calculate the result if ecx is the requested return register. Register result_reg = rax; // Save ecx if it isn't the return register and therefore volatile, or if it // is the return register, then save the temp register we use in its stead // for the result. Register save_reg = rax; __ pushq(rcx); __ pushq(scratch1); __ pushq(save_reg); __ movl(scratch1, mantissa_operand); __ Movsd(kScratchDoubleReg, mantissa_operand); __ movl(rcx, exponent_operand); __ andl(rcx, Immediate(HeapNumber::kExponentMask)); __ shrl(rcx, Immediate(HeapNumber::kExponentShift)); __ leal(result_reg, MemOperand(rcx, -HeapNumber::kExponentBias)); __ cmpl(result_reg, Immediate(HeapNumber::kMantissaBits)); __ j(below, &process_64_bits, Label::kNear); // Result is entirely in lower 32-bits of mantissa int delta = HeapNumber::kExponentBias + base::Double::kPhysicalSignificandSize; __ subl(rcx, Immediate(delta)); __ xorl(result_reg, result_reg); __ cmpl(rcx, Immediate(31)); __ j(above, &done, Label::kNear); __ shll_cl(scratch1); __ jmp(&check_negative, Label::kNear); __ bind(&process_64_bits); __ Cvttsd2siq(result_reg, kScratchDoubleReg); __ jmp(&done, Label::kNear); // If the double was negative, negate the integer result. __ bind(&check_negative); __ movl(result_reg, scratch1); __ negl(result_reg); __ cmpl(exponent_operand, Immediate(0)); __ cmovl(greater, result_reg, scratch1); // Restore registers __ bind(&done); __ movl(return_operand, result_reg); __ popq(save_reg); __ popq(scratch1); __ popq(rcx); __ ret(0); }
看上去很像匯編(編程的思考方式按匯編來),實(shí)際上是c++函數(shù),比如這行movl
__ movl(scratch1, mantissa_operand);
__是個(gè)宏,實(shí)際上是調(diào)用masm變量的函數(shù)(movl)
#define __ ACCESS_MASM(masm) #define ACCESS_MASM(masm) masm->
而movl的實(shí)現(xiàn)是往pc_指針指向的內(nèi)存寫入mov指令及其操作數(shù),并把pc_指針前進(jìn)指令長度。
ps:一條條指令寫下來,然后把內(nèi)存權(quán)限改為可執(zhí)行,這就是JIT的基本原理。
除了ASM和CPP的其它類型builtin都通過調(diào)用CodeStubAssembler API(下稱CSA)編寫,這套API和之前介紹ASM類型builtin時(shí)提到的“類匯編API”類似,不同的是“類匯編API”直接產(chǎn)出原生代碼,CSA產(chǎn)出的是turbofan的graph(IR)。CSA比起“類匯編API”的好處是不用每個(gè)平臺各寫一次。
但是類匯編的CSA寫起來還是太費(fèi)勁了,于是V8提供了一個(gè)類javascript的高級語言:torque,這語言最終會編譯成CSA形式的c++代碼和V8其它C++代碼一起編譯。
例如Array.isArray使用torque語言實(shí)現(xiàn)如下:
namespace runtime { extern runtime ArrayIsArray(implicit context: Context)(JSAny): JSAny; } // namespace runtime namespace array { // ES #sec-array.isarray javascript builtin ArrayIsArray(js-implicit context: NativeContext)(arg: JSAny): JSAny { // 1. Return ? IsArray(arg). typeswitch (arg) { case (JSArray): { return True; } case (JSProxy): { // TODO(verwaest): Handle proxies in-place return runtime::ArrayIsArray(arg); } case (JSAny): { return False; } } } } // namespace array
經(jīng)過torque編譯器編譯后,會生成一段復(fù)雜的CSA的C++代碼,下面截取一個(gè)片段
TNode Cast_JSProxy_1(compiler::CodeAssemblerState* state_, TNode p_context, TNode p_o, compiler::CodeAssemblerLabel* label_CastError) { // other code ... if (block0.is_used()) { ca_.Bind(&block0); ca_.SetSourcePosition("../../src/builtins/cast.tq", 162); compiler::CodeAssemblerLabel label1(&ca_); tmp0 = CodeStubAssembler(state_).TaggedToHeapObject(TNode{p_o}, &label1); ca_.Goto(&block3); if (label1.is_used()) { ca_.Bind(&label1); ca_.Goto(&block4); } } // other code ... }
和上面講的Ldar字節(jié)碼一樣,這并不是跑在v8運(yùn)行時(shí)的Array.isArray實(shí)現(xiàn)。這段代碼只運(yùn)行在mksnapshot中,這段代碼的產(chǎn)物是turbofan的IR。IR經(jīng)過turbofan的優(yōu)化編譯后生成目標(biāo)機(jī)器指令,然后dump到embedded.S匯編文件,下面才是真正跑在v8運(yùn)行時(shí)的Array.isArray:
Builtins_ArrayIsArray: .type Builtins_ArrayIsArray, %function .size Builtins_ArrayIsArray, 214 .octa 0xd10043ff910043fda9017bfda9be6fe1,0x540003a9eb2263fff8560342f81e83a0 .octa 0x7840b063f85ff04336000182f9401be2,0x14000007d2800003540000607110907f .octa 0x910043ffa8c17bfd910003bff85b8340,0x35000163d2800020d2800023d65f03c0 .octa 0x540000e17102d47f7840b063f85ff043,0xf94da741f90003e2f90007ffd10043ff .octa 0x17ffffeef85c034017fffff097ffb480,0xaa1b03e2f9501f41d2800000f90003fb .octa 0x17ffffddf94003fb97ffb477aa0003e3,0x840000000100000002d503201f .octa 0xffffffff000000a8ffffffffffffffff .byte 0xff,0xff,0xff,0xff,0x0,0x1,0xcc,0xcc,0xcc,0xcc,0xcc,0xcc,0xcc,0xcc,0xcc,0xcc
在這個(gè)過程中,JIT編譯器turbofan同樣干的是AOT的活。
1.3、builtin是怎么加載使用的
mksnapshot生成的包含所有builtin的產(chǎn)物embedded.S會和其他v8源碼一起編譯成最終的v8庫,embedded.S中聲明了四個(gè)全局變量,分別是:
?v8_Default_embedded_blob_code_:初始化為第一個(gè)builtin的起始位置(全部builtin緊湊的放在一個(gè)代碼段里)
?v8_Default_embedded_blob_data_:指向一塊數(shù)據(jù),這塊數(shù)據(jù)包含諸如各builtin相對v8_Default_embedded_blob_code_的偏移,builtin的長度等等信息
?v8_Default_embedded_blob_code_size_:所有builtin的總長度
?v8_Default_embedded_blob_data_size_:v8_Default_embedded_blob_data_數(shù)據(jù)的總長度
在v8/src/execution/isolate.cc中聲明了幾個(gè)extern變量,鏈接embedded.S后v8/src/execution/isolate.cc就能引用到那幾個(gè)變量:
extern "C" const uint8_t* v8_Default_embedded_blob_code_; extern "C" uint32_t v8_Default_embedded_blob_code_size_; extern "C" const uint8_t* v8_Default_embedded_blob_data_; extern "C" uint32_t v8_Default_embedded_blob_data_size_;
v8_Default_embedded_blob_data_中包含了各builtin的偏移,這些偏移組成一個(gè)數(shù)組,放在isolate的builtin_entry_table,數(shù)組下標(biāo)是該builtin的枚舉值。調(diào)用某builtin就是builtin_entry_table通過枚舉值獲取起始地址調(diào)用。
2、snapshot
在V8引擎中,snapshot是指在啟動(dòng)時(shí)將部分或全部JavaScript堆內(nèi)存的狀態(tài)保存到一個(gè)文件中,以便在后續(xù)的啟動(dòng)中可以快速恢復(fù)到這個(gè)狀態(tài)。這個(gè)技術(shù)可以顯著減少V8引擎的啟動(dòng)時(shí)間,特別是在大型應(yīng)用程序中。
snapshot文件包含了以下幾個(gè)部分:
?JavaScript堆的內(nèi)存布局:包括了所有對象的地址、大小和類型等信息。
?JavaScript代碼的字節(jié)碼:包括了所有已經(jīng)編譯的JavaScript函數(shù)的字節(jié)碼。
?全局對象的狀態(tài):包括了全局對象的屬性值、函數(shù)指針等信息。
?其他必要的狀態(tài):例如,垃圾回收器的狀態(tài)、Just-In-Time (JIT) 編譯器的緩存等。
當(dāng)V8引擎啟動(dòng)時(shí),如果存在有效的Snapshot文件,V8會直接從這個(gè)文件中讀取JavaScript堆的狀態(tài)和字節(jié)碼,而不需要重新解析和編譯所有的JavaScript代碼。這可以大幅度縮短V8引擎的啟動(dòng)時(shí)間。V8的Snapshot技術(shù)有以下幾個(gè)優(yōu)點(diǎn):
?快速啟動(dòng):可以顯著減少V8引擎的啟動(dòng)時(shí)間,特別是在大型應(yīng)用程序中。
?低內(nèi)存占用:由于部分或全部JavaScript堆的狀態(tài)已經(jīng)被保存到文件中,所以在啟動(dòng)時(shí)可以節(jié)省內(nèi)存。
?穩(wěn)定性:Snapshot文件是由V8引擎生成的,保證了與引擎的兼容性和穩(wěn)定性。
如果不是交叉編譯,snapshot生成還是挺容易理解的:v8對各種對象有做了序列化和反序列化的支持,所謂生成snapshot,就是序列化,通常會以context作為根來序列化。
mksnapshot制作快照可以輸入一個(gè)額外的腳本,也就是生成snapshot前允許執(zhí)行一段代碼,這段代碼調(diào)用到的函數(shù)的編譯結(jié)果也會序列化下來,后續(xù)加載快照反序列化后等同于執(zhí)行過了這腳本,就免去了編譯過程,大大加快的啟動(dòng)的速度。
mksnapshot制作快照是通過調(diào)用v8::SnapshotCreator完成,而v8::SnapshotCreator提供了我們輸入外部數(shù)據(jù)的機(jī)會。如果只有一個(gè)Context需要保存,用SnapshotCreator::SetDefaultContext就可以了,恢復(fù)時(shí)直接v8::Context::New即可。如果有多于一個(gè)Context,可以通過SnapshotCreator::AddContext添加,它會返回一個(gè)索引,恢復(fù)時(shí)輸入索引即可恢復(fù)到指定的存檔。如果保存Context之外的數(shù)據(jù),可以調(diào)用SnapshotCreator::AddData,然后通過Isolate或者Context的GetDataFromSnapshot接口恢復(fù)。
//保存 size_t context_index = snapshot_creator.AddContext(context, si_cb); //恢復(fù) v8::Local context = v8::Context::FromSnapshot(isolate, context_index, di_cb).ToLocalChecked();
結(jié)合交叉編譯時(shí)就會有個(gè)很費(fèi)解的地方:我們前面提到mksnapshot在交叉編譯時(shí),JIT生成的builtin是目標(biāo)機(jī)器指令,而js的運(yùn)行得通過跑builtin來實(shí)現(xiàn)(Ignition解析器每個(gè)指令就是一個(gè)builtin),這目標(biāo)機(jī)器指令(比如arm64)怎么在本地(比如linux 的x64)跑起來呢?mksnapshot為了實(shí)現(xiàn)交叉編譯中目標(biāo)平臺snapshot的生成,它做了各種cpu(arm、mips、risc、ppc)的模擬器(Simulator)
通過查看源碼交叉編譯時(shí),mksnapshot會用一個(gè)目標(biāo)機(jī)器的模擬器來跑這些builtin:
//srccommonglobals.h #if !defined(USE_SIMULATOR) #if (V8_TARGET_ARCH_ARM64 && !V8_HOST_ARCH_ARM64) #define USE_SIMULATOR 1 #endif // ... #endif //srcexecutionsimulator.h #ifdef USE_SIMULATOR Return Call(Args... args) { // other code ... return Simulator::current(isolate_)->template Call( reinterpret_cast(fn_ptr_), args...); } #else DISABLE_CFI_ICALL Return Call(Args... args) { // other code ... } #endif // USE_SIMULATOR
如果交叉編譯,將會走USE_SIMULATOR分支。arm64將會調(diào)用到v8/src/execution/simulator-arm64.h,v8/src/execution/simulator-arm64.cc實(shí)現(xiàn)的模擬器。上面Call的處理是把指令首地址賦值到模擬器的_pc寄存器,參數(shù)放寄存器,執(zhí)行完指令從寄存器獲取返回值。
六、V8移植的具體步驟
一般我們將負(fù)責(zé)編譯的機(jī)器稱為host,編譯產(chǎn)物運(yùn)行的目標(biāo)機(jī)器稱為target。
?本文使用的host機(jī)器是Mac M1 ,Xcode版本Version 14.2 (14C18)
?鴻蒙IDE版本:DevEco Studio NEXT Developer Beta5
?鴻蒙SDK版本是HarmonyOS-NEXT-DB5
?目標(biāo)機(jī)器架構(gòu):arm64-v8a
如果要在Mac M1上交叉編譯鴻蒙arm64的builtin,步驟如下:
?調(diào)用本地編譯器,編譯一個(gè)Mac M1版本mksnapshot可執(zhí)行程序
?執(zhí)行上述mksnapshot生成鴻蒙平臺arm64指令并dump到embedded.S
?調(diào)用鴻蒙sdk的工具鏈,編譯鏈接embedded.S和v8的其它代碼,生成能在鴻蒙arm64上使用的v8庫
1.首先安裝cmake及ninja構(gòu)建工具
鴻蒙sdk自帶構(gòu)建工具我們可以將它們加入環(huán)境變量中使用
2.編寫交叉編譯V8到鴻蒙的CMakeList.txt
總共有1千多行,部分CMakeList.txt片段:
3.使用host本機(jī)的編譯工具鏈編譯
$ mkdir build $ cd build $ cmake -G Ninja .. $ ninja 或者 cmake --build .
首先創(chuàng)建一個(gè)編譯目錄build,打開build執(zhí)行cmake -G Ninja ..生成針對ninja編譯需要的文件。
下面是控制臺打印的工具鏈配置信息,使用的是Mac本地xcode的工具鏈:
build文件夾下生成以下文件:
其中CMakeCache.txt是一個(gè)由CMake生成的緩存文件,用于存儲CMake在配置過程中所做的選擇和決策。它是根據(jù)你的項(xiàng)目的CMakeLists.txt文件和系統(tǒng)環(huán)境來生成一個(gè)初始的CMakeCache.txt文件。這個(gè)文件包含了所有可配置的選項(xiàng)及其默認(rèn)值。
build.ninja文件是Ninja的主要輸入文件,包含了項(xiàng)目的所有構(gòu)建規(guī)則和依賴關(guān)系。
這個(gè)文件的內(nèi)容是Ninja的語法,描述了如何從源文件生成目標(biāo)文件。它包括了以下幾個(gè)部分:
?規(guī)則:定義了如何從源文件生成目標(biāo)文件的規(guī)則。例如,編譯C++文件、鏈接庫等。
?構(gòu)建目標(biāo):列出了項(xiàng)目中所有需要構(gòu)建的目標(biāo),包括可執(zhí)行文件、靜態(tài)庫、動(dòng)態(tài)庫等。
?依賴關(guān)系:描述了各個(gè)構(gòu)建目標(biāo)之間的依賴關(guān)系。Ninja會根據(jù)這些依賴關(guān)系來確定構(gòu)建的順序。
?變量:定義了一些Ninja使用的變量,例如編譯器、編譯選項(xiàng)等。
然后執(zhí)行cmake --build .或者ninja
查看build文件夾下生成的產(chǎn)物:
其中紅框中的三個(gè)可執(zhí)行文件是在編譯過程中生成,同時(shí)還會在編譯過程中執(zhí)行。bytecode_builtins_list_generator主要生成是字節(jié)碼對應(yīng)builtin的生成代碼。torque負(fù)責(zé)將.tq后綴的文件(使用torque語言編寫的builtin)編譯成CSA類型builtin的c++源碼文件。
torque編譯.tq文件生成的c++代碼在torque-generated目錄中:
bytecode_builtins_list_generator執(zhí)行生成字節(jié)碼函數(shù)列表在下面目錄中:
mksnapshot則鏈接這些代碼并執(zhí)行,執(zhí)行期間會在內(nèi)置的對應(yīng)架構(gòu)模擬器中運(yùn)行v8,最終生成host平臺的buildin匯編代碼——embedded.S和snapshot(context的序列化對象)——snapshot.cc。它們跟隨其他v8源代碼一起編譯生成最終的v8靜態(tài)庫libv8_snapshot.a。目前build目錄中已經(jīng)編譯出host平臺的完整v8靜態(tài)庫及命令行調(diào)試工具d8。
mksnapshot程序自身的編譯生成及執(zhí)行在CMakeList.txt中的配置代碼如下:
4.使用鴻蒙SDK的編譯工具鏈編譯
因?yàn)樵诰幾gtarget平臺的v8時(shí)中間生成的bytecode_builtins_list_generator,torque,mksnapshot可執(zhí)行文件是針對target架構(gòu)的無法在host機(jī)器上執(zhí)行。所以首先需要把上面在host平臺生成的可執(zhí)行文件拷貝到/usr/local/bin,這樣在編譯target平臺的v8過程中執(zhí)行這些中間程序時(shí)會找到/usr/local/bin下的可執(zhí)行文件正確的執(zhí)行生成針對target的builtin和snapshot快照。
$ cp bytecode_builtins_list_generator torque mksnapshot /usr/local/bin $ mkdir ohosbuild #創(chuàng)建新的鴻蒙v8的編譯目錄 $ cd ohosbuild #使用鴻蒙提供的工具鏈文件 $ cmake -DOHOS_STL=c++_shared -DOHOS_ARCH=arm64-v8a -DOHOS_PLATFORM=OHOS -DCMAKE_TOOLCHAIN_FILE=/Applications/DevEco-Studio.app/Contents/sdk/HarmonyOS-NEXT-DB5/openharmony/native/build/cmake/ohos.toolchain.cmake -G Ninja .. $ ninja 或者 cmake --build .
執(zhí)行第一步cmake配置后控制臺的信息可以看到,使用了鴻蒙的工具鏈
執(zhí)行完成后ohosbuild文件夾下生成了鴻蒙平臺的v8靜態(tài)庫,可以修改CMakeList.txt配置合成一個(gè).a或者生成.so。
七、鴻蒙工程中使用v8庫
1.新建native c++工程
2.導(dǎo)入v8庫
將v8源碼中的include目錄和上面編譯生成的.a文件放入cpp文件夾下
3.修改cpp目錄下CMakeList.txt文件
設(shè)置c++標(biāo)準(zhǔn)17,鏈接v8靜態(tài)庫
4.添加napi方法測試使用v8
下面是簡單的demo
導(dǎo)出c++方法
、
arkts側(cè)調(diào)用c++方法
運(yùn)行查看結(jié)果:
八、JS引擎的發(fā)展趨勢
隨著物聯(lián)網(wǎng)的發(fā)展,人們對IOT設(shè)備(如智能手表)的使用越來越多。如果希望把JS應(yīng)用到IOT領(lǐng)域,必然需要從JS引擎角度去進(jìn)行優(yōu)化,只是去做上層的框架收效甚微。因?yàn)閷τ贗OT硬件來說,CPU、內(nèi)存、電量都是需要省著點(diǎn)用的,不是每一個(gè)智能家電都需要裝一個(gè)驍龍855。那怎么可以基于V8引擎進(jìn)行改造來進(jìn)一步提升JS的執(zhí)行性能呢?
?使用TypeScript編程,遵循嚴(yán)格的類型化編程規(guī)則;
?構(gòu)建的時(shí)候?qū)ypeScript直接編譯為Bytecode,而不是生成JS文件,這樣運(yùn)行的時(shí)候就省去了Parse以及生成Bytecode的過程;
?運(yùn)行的時(shí)候,需要先將Bytecode編譯為對應(yīng)CPU的匯編代碼;
?由于采用了類型化的編程方式,有利于編譯器優(yōu)化所生成的匯編代碼,省去了很多額外的操作;
基于V8引擎來實(shí)現(xiàn),技術(shù)上應(yīng)該是可行的:
?將Parser以及Ignition拆分出來,用于構(gòu)建階段;
?刪掉TurboFan處理JS動(dòng)態(tài)特性的相關(guān)代碼;
這樣可以將JS引擎簡化很多,一方面不再需要parse以及生成bytecode,另一方面編譯器不再需要因?yàn)镴avaScript動(dòng)態(tài)特性做很多額外的工作。因此可以減少CPU、內(nèi)存以及電量的使用,優(yōu)化性能,唯一的問題是必須使用嚴(yán)格的TS語法進(jìn)行編程。
Facebook的Hermes差不多就是這么干的,只是它沒有要求用TS編程。
如今鴻蒙原生的ETS引擎Panda也是這么干的,它要求使用ets語法,其實(shí)是基于TS只不過做了更加嚴(yán)格的類型及語法限制(舍棄了更多的動(dòng)態(tài)特性),進(jìn)一步提升js的執(zhí)行性能。
將V8移植到鴻蒙系統(tǒng)是一個(gè)巨大的嵌入式范疇工作,涉及交叉編譯、CMake、CLang、Ninja、C++、torque等各種知識,雖然我們經(jīng)歷了巨大挑戰(zhàn)并掌握了V8移植技術(shù),但出于應(yīng)用包大小、穩(wěn)定性、兼容性、維護(hù)成本等維度綜合考慮,如果華為系統(tǒng)能內(nèi)置V8,對Roma框架及業(yè)界所有依賴JS虛擬機(jī)的跨端框架都是一件意義深遠(yuǎn)的事情,通過和華為持續(xù)溝通,鴻蒙從API11版本提供了一個(gè)內(nèi)置的JS引擎,它實(shí)際上是基于v8的封裝,并提供了一套c-api接口。
如果不想用c-api并且不考慮包大小的問題仍然可以自己編譯一個(gè)獨(dú)立的v8引擎嵌入APP,直接使用v8面向?qū)ο蟮腃++ API。
Roma框架是一個(gè)涉及JavaScript、C&C++、Harmony、iOS、Android、Java、Vue、Node、Webpack等眾多領(lǐng)域的綜合解決方案,我們有各個(gè)領(lǐng)域優(yōu)秀的小伙伴共同前行,大家如果想深入了解某個(gè)領(lǐng)域的具體實(shí)現(xiàn),可以隨時(shí)留言交流~
審核編輯 黃宇
-
javascript
+關(guān)注
關(guān)注
0文章
516瀏覽量
53850 -
虛擬機(jī)
+關(guān)注
關(guān)注
1文章
914瀏覽量
28158 -
鴻蒙系統(tǒng)
+關(guān)注
關(guān)注
183文章
2634瀏覽量
66302
發(fā)布評論請先 登錄
相關(guān)推薦
評論