会员登录 - 用户注册 - 设为首页 - 加入收藏 - 网站地图 前所未有,移植V8虚拟机到纯血鸿蒙体系!

前所未有,移植V8虚拟机到纯血鸿蒙体系

时间:2025-05-22 17:13:55 来源:锐评时讯 作者:咨询 阅读:962次

作者:京东。科技。于腾跃。

一、布景。

如图所示,Rom。a结构是咱们自主研制的动态化跨渠道处理方案,已支撑。iOS。android。,web三端。现在在京东金融APP现已有200+页面,200+乐高楼层运用,为确保依据Roma结构开发的事务能够零本钱、无缝运转到。鸿蒙体系。,需求将Roma结构适配到。鸿蒙。体系。

Roma结构是依据JS引擎运转的,在iOS体系运用体系内置的。Java。scri。ptC。or。e,在Android体系运用V8,可是,鸿蒙体系却没有能够履行Roma结构的JS引擎,因而需求移植一个JS引擎到鸿蒙渠道。

二、JS引擎选型。

现在干流的JS引擎有以下这些:

引擎称号。运用代表。公司。
V8。Chrome/Opera/Edge/。Node.js。/Electron。Google。
SpiderMonkey。firefox。Mozilla。
JavaScriptCore。Safari。Apple。
Chakra。IE。Microsoft。
Hermes。Re。ac。t Na。ti。ve。Facebook。
JerryScript/duktape/Qu。ic。kJS。小型而且可嵌入的Javascript引擎/首要运用于。IOT。设备。-。

其间最盛行的是Google开源的V8引擎,除了Chrome等浏览器,Node.js也是用的V8引擎。Chrome的市场占有率高达60%,而Node.js是JS后端。编程。的事实规范。别的,Electron(桌面运用结构)是依据Node.js与Chromium开发桌面运用,也是依据V8的。国内的许多浏览器,其实也都是依据Chromium浏览器开发,而Chromium相当于开源版别的Chrome,天然也是依据V8引擎的。甚至连浏览器界别出心裁的Microsoft也投靠了Chromium阵营。V8引擎使得JS能够运用在Web、APP、桌面端、服务端以及IOT等各个范畴。

三、V8引擎的。作业原理。

V8的首要使命是履行JavaScript代码,而且能够处理JavaScript源代码、即时编译(JIT)代码以及履行代码。v8是一个十分杂乱的项目,有超越100万行。C++。代码。

下图展现了它的根本作业流程:

如图所示,它经过词法剖析、语法剖析、字节码生成与履行、即时编译与机器码生成以及废物收回等进程,完结了对JavaScript源代码的高效履行。此外,V8引擎还经过监控代码的履行情况,对热门函数进行主动优化,然后进一步进步了代码的履行功用。其间Pa。rs。er(。解析器。)、Ignition(解说器)、TurboFan(编译器)、Orinoco(废物收回)是 V8 中四个中心作业模块,对应的V8源码目录如下图。

1、Parser:解析器。

担任将JavaScript源码转化为Abstract Syntax Tree (AST)笼统语法树,解析进程分为:词法剖析(Lexical Analysis)和语法剖析(Syntax Analysis)两个阶段。

1.1、词法剖析。

V8 引擎首要会扫描一切的源代码,进行词法剖析(Tokenizing/Lexing)(词法剖析是经过 S。can。ner 模块来完结的)。也称为分词,是将字符串办法的代码转化为符号(token)序列的进程。这儿的token是一个字符串,是构成源代码的最小单位,相似于英语中单词,例如,var a = 2;经过词法剖析得到的tokens如下:

从上图中能够看到,这句代码终究被分化出了五个词法单元:

var 要害字。

a 标识符。

= 运算符。

2 数值。

;分号。

一个能够在线检查Tokens的网站: https://esprima.org/demo/parse.html。

1.2、语法剖析。

语法剖析是将词法剖析发生的token依照某种给定的办法文法(这儿是JavaScript言语的语法规矩)转化成笼统语法树(AST)的进程。也便是把单词组组成语句的进程。这个进程会剖析语法过错:遇到过错的语法会抛出反常。AST是源代码的语法结构的树形表明。AST包含了源代码中的一切语法结构。信息。,但不包含代码的履行逻辑。

例如,var a = 2;经过语法剖析后生成的AST如下:

能够看到这段程序的类型是VariableDeclaration,也便是说这段代码是用来声明变量的。

一个能够在线检查AST结构的网站:https://astexplorer.net/。

2、Ignition:(interpreter)解说器。

担任将AST转化成字节码(By。te。code)并逐行解说履行字节码,供给快速的发动和较低的内存运用,一同会符号热门代码,搜集TurboFan优化编译所需的信息,比方函数参数的类型。

2.1、什么是字节码?

字节码(Bytecode)是一种介于AST和机器码之间的中心表明办法,它比AST更挨近机器码,它比机器码更笼统,也更轻量,与特定机器代码无关,需求解说器转译后才干成为机器码。字节码一般不像源码相同能够让人阅览,而是编码后的数值常量、引证、指令等构成的序列。

2.2、字节码的长处。

•不针对特定。CPU。架构。

•比原始的高档言语转化成机器言语更快。

•字节码比机器码占用内存更小。

•运用字节码,能够完结Com。pi。le Once,Run anywhere(一次编译处处运转)。

前期版别的 V8 ,并没有生成中心字节码的进程,而是将一切源码转化为了机器代码。机器代码尽管履行速度更快,可是占用内存大。

2.3、检查字节码。

Node.js是依据V8引擎完结的,因而node指令供给了许多V8引擎的选项,咱们能够经过这些选项,检查V8引擎中各个阶段的产品。运用node的--print-bytecode选项,能够打印出Ignition生成的Bytecode。

示例test.js如下。

//test.js function。 ad。d(a, b){ return a + b; } add(1,2); //V8不会编译没有被调用的函数,因而需求在终究一行调用add函数。

运转下面的node指令,打印出Ignition生成的字节码。

node --print-bytecode test.js [generated bytecode for function: add (0x29e627015191。 < SharedFunctionInfo add >)] Bytecode length: 6 Pa。ram。eter 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。

控制台输出的内容十分多,终究一部分是add函数的Bytecode。

字节码的详细信息如下:

•[generated bytecode for function: add (0x29e627015191)]: 这行告知咱们,接下来的字节码是为 add 函数生成的。0x29e627015191 是这个函数在内存中的地址。

•Bytecode length: 6: 整个字节码的长度是 6 字节。

•Parameter count 3: 该函数有 3 个。参数。。包含传入的 a,b 以及 this。

•Register count 0: 该函数没有运用任何。寄存器。

•Frame size 0: 该函数的帧巨细是 0。帧巨细是指在调用栈上分配给这个函数的空间巨细,用于存储局部变量、函数参数等。

•OSR urgency: 0: On-Stack Replacement(OSR)优化的紧迫程度是 0。OSR 是一种在运转时将解说履行的函数替换为编译履行的函数的技能,用于进步功用。

•Bytecode age: 0: 字节码的年纪是 0。字节码的年纪是指它被履行的次数,年纪越高,阐明这个字节码被履行的越频频,或许会被 V8 引擎优化。

•Ldar a1 表明将寄存器中的值加载到累加器中 ,这行是字节码的第一条指令。

•Add a0, [0] 从 a0 寄存器加载值而且将其与累加器中的值相加,然后将成果再次放入累加器 。

•Return 完毕当时函数的履行,并把控制权传给调用方,将累加器中的值作为回来值。

•S>表明这是一个“Safepoint”指令,V8 引擎能够在履行这条指令时进行废物收回等操作。

•E>表明这是一个“Effect”指令,或许会改动程序的状况。

•Constant pool (size = 0): 常量池的巨细是 0。常量池是用来存储函数中运用的常量值的。

•Handler Table (size = 0): 反常处理表的巨细是 0。反常处理表是用来处理函数中或许呈现的反常的。

•Source Position Table (size = 8): 源代码方位表的巨细是 8。源代码方位表是用来将字节码指令与源代码行号相关起来的,便利调试。

•0x29e627015bc1 : 这行是源代码方位表的详细内容,显现了每个字节码指令对应的源代码行号和列号。

能够看到,Bytecode某种程度上便是。汇编。言语,仅仅它没有对应特定的CPU,或许说它对应的是虚拟的CPU。这样的话,生成Bytecode时简略许多,无需为不同的CPU出产不同的代码。要知道,V8支撑9种不同的CPU,引进一个中心层Bytecode,能够简化V8的编译流程,进步可扩展性。假如咱们在不同。硬件。上去生成Bytecode,生成代码的指令是相同的.。

3、TurboFan:(compiler)编译器。

V8 的优化编译器也是v8完结即时编译(JIT)的中心,担任将热门函数的字节码编译成高效的机器码。

3.1、什么是JIT?

咱们需求先了解一下JIT(Just in Time)即时编译。

在运转C、C++以及Java等程序之前,需求进行编译,不能直接履行源码;但关于JavaScript来说,咱们能够直接履行源码(比方:node server.js),它是在运转的时分先编译再履行,这种办法被称为即时编译(Just-in-time compilation),简称为JIT。因而,V8也归于JIT编译器。

完结JIT编译器的体系一般会不断地剖析正在履行的代码,并确认代码的某些部分,在这些部分中,编译或从头编译所取得的加速将超越编译该代码的开支。 JIT编译是两种传统的机器代码翻译办法——提早编译(AOT)和解说——的结合,它结合了两者的长处和缺陷。大致来说,JIT编译将编译代码的速度与解说的灵活性、解说器的开支以及额定的编译开支(而不仅仅是解说)结合起来。

除了V8引擎,Java虚拟机、PHP 8也用到了JIT。

3.2、V8引擎的JIT。

V8的JIT编译包含多个阶段,从生成字节码到生成高度优化的机器码,依据JavaScript代码的履行特性动态地优化代码,以完结高功用的JavaScript履行。看下图Ignition和TurboFan的交互:

当 Ignition 开端履行 JavaScript 代码后,V8 会一向调查 JavaScript 代码的履行情况,并记载履行信息,如每个函数的履行次数、每次调用函数时,传递的参数类型等。假如一个函数被调用的次数超越了内设的阈值,监视器就会将当时函数符号为热门函数(Hot Function),并将该函数的字节码以及履行的相关信息发送给 TurboFan。TurboFan 会依据履行信息做出一些进一步优化此代码的假定,在假定的根底大将字节码编译为优化的机器代码。假如假定建立,那么当下一次调用该函数时,就会履行优化编译后的机器代码,以进步代码的履行功用。

假如假定不建立,上图中,绿色的线,是“去优化(Deoptimize)”的进程,假如TurboFan生成的优化机器码,对需求履行的代码不适用,会把优化的机器码,从头转化成字节码来履行。这是由于Ignition搜集的信息或许是过错的。

例如:

function add(a, b) { return a + b; } add(1, 2); add(2, 2); add("1", "2");

add函数的参数之前是整数,后来又变成了字符串。生成的优化机器码现已假定add函数的参数是整数,那当然是过错的,所以需求进行去优化,Deoptimize为Bytecode来履行。

TurboFan除了上面依据类型做优化和反优化,还有包含内联(inlining)和逃逸剖析(Escape Analysis)等,内联便是将相相关的函数进行兼并。例如:

function add(a, b) { return a + b } function foo() { return add(2, 4) }。

内联优化后:

function fooAddInlined() { var a = 2 var b = 4 var addReturnValue = a + b return addReturnValue } // 由于 fooAddInlined 中 a 和 b 的值都是确认的,所以能够进一步优化 function fooAddInlined() { return 6 }。

运用node指令的--print-code以及--print-opt-code选项,能够打印出TurboFan生成的汇编代码。

node --print-code --print-opt-code test.js。

4、Orinoco:废物收回。

一个高效的废物收回器,用于主动办理内存,收回不再运用的方针内存;它运用多种废物收回战略,如分代收回、符号-铲除、增量符号等,以完结高效内存办理。

Orinoco的首要特点包含:

•并发符号: Orinoco运用并发符号技能来削减废物收回的中止时刻(Pause Time)。这意味着在运用程序继续履行的一同,废物收回器能够在后台进行符号操作。

•增量式废物收回: Orinoco支撑增量式废物收回,这答应废物收回器在小的时刻片内履行部分废物收回作业,而不是一次性处理一切的废物。

•更高效的内存办理: Orinoco引进了一些新的内存办理战略和数据结构,旨在削减内存碎片和进步内存运用率。

•可扩展性: Orinoco的规划考虑了可扩展性,使得它能够习惯不同的作业负载和硬件装备。

•多线程支撑: Orinoco支撑多线程环境,能够运用多核CPU来加速废物收回进程。

四、V8移植东西选型。

咱们的开发环境林林总总或许体系是Mac,Linux。或许Windows,架构是x86或许。arm。,所以要想编译出能够跑在鸿蒙体系上的v8库咱们需求运用穿插编译,它是在一个渠道上为另一个渠道编译代码的进程,答应咱们在一个渠道上为另一个渠道生成可履行文件。这在。嵌入式体系。开发中尤为常见,由于许多。嵌入式。设备的硬件资源有限,不适合直接在上面编译代码。穿插编译需求一个特定的编译器、链接器和库,这些都是为方针渠道规划的。此外,开发者。还需求确保代码没有渠道相关的依靠,不然编译或许会失利。

v8官网上关于穿插编译Android和iOS渠道的V8现已有详细的介绍。尚无关于鸿蒙OHOS渠道的文档。V8官方运用的构建体系是gn + ninja。gn是一个元构建体系,开始由Google开发,用于生成Ninja文件。它供给了一个声明式的办法来界说项目的依靠联系、编译选项和其他构建参数。经过运转gn gen指令,能够生成一个Ninja文件。相似于camke + make构建体系。

gn + ninja的构建流程如下:

经过检查鸿蒙。sd。k,咱们发现鸿蒙供给给开发者的native构建体系是cmake + ninja,所以咱们决定将v8官方选用的gn + ninja转成cmake + ninja。这就需求将gn语法的构建装备文件转成cmake的构建装备文件。

1、CMake简介。

CMake是一个开源的、跨渠道的构建体系。它不仅能够生成规范的Unix Makefile合作make指令运用,还能够生成build.ninja文件合作ninja运用,还可认为多种IDE生成项目文件,如Visual Studio、Eclipse、Xcode等。这种跨渠道性使得CMake在多种。操作体系。和开发环境中都能够无缝作业。

cmake的构建流程如下:

CMake构建首要进程是编写CMakeLists.txt文件,然后用cmake指令将CMakeLists.txt文件转化为make所需求的Makefile文件或许ninja需求的build.ninja文件,终究用make指令或许ninja指令履行编译使命生成可履行程序或同享库(so(shared object))。

完好CMakeLists.txt文件的首要装备样例:

# 1. 声明要求的cmake最低版别 cmake_minimum_required( VERSION 2.8 ) # 2. 增加c++11规范支撑 #set( CMAKE_CXX_FLAGS "-std=c++11" ) # 3. 声明一个cmake工程 PROJECT(camke_demo) MESSAGE(STATUS "Project: SERVER") #打印相关音讯 # 4. 头文件 include_directories( ${PROJECT_SOURCE_DIR}/../include/mq ${PROJECT_SOURCE_DIR}/../include/incl ${PROJECT_SOURCE_DIR}/../include/rapidjson ) # 5. 经过设定SRC变量,将源代码途径都给SRC,假如有多个,能够直接在后面继续增加 set(SRC ${PROJECT_SOURCE_DIR}/../include/incl/。tf。c_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. 创立同享库/静态库 # 设置途径(下面生成同享库的途径) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/lib) # 即生成的同享库在工程文件夹下的lib文件夹中 set(LIB_NAME camke_demo_lib) # 创立同享库(把工程内的cpp文件都创立成同享库文件,便利经过头文件来调用) # 这时分只需求cpp,不需求有主函数 # ${PROJECT_NAME}是生成的库名 表明生成的同享库文件就叫做 lib工程名.so # 也能够专门写cmakelists来编译一个没有主函数的程序来生成同享库,供其它程序运用 # SHARED为生成动态库,STATIC为生成静态库 add_library(${LIB_NAME} STATIC ${SRC}) # 7. 链接库文件 # 把刚刚生成的${LIB_NAME}库和所需的其它库链接起来 # 假如需求链接其他的动态库,-l后接去除lib前缀和.so后缀的称号,以链接 # libpthread.so 为例,-lpthread target_link_libraries(${LIB_NAME} pthread dl) # 8. 编译主函数,生成可履行文件 # 先设置途径 set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/bin) # 可履行文件生成 add_executable(${PROJECT_NAME} ${SRC}) # 链接这个可履行文件所需的库 target_link_libraries(${PROJECT_NAME} pthread dl ${LIB_NAME})。

一般把CMakeLists.txt文件放在工程目录下,运用时先创立一个叫build的文件夹(这个并非有必要,由于cmake指令指向CMakeLists.txt地点的目录,例如cmake ..表明CMakeLists.txt在当时目录的上一级目录。cmake履行后会生成许多编译的中心文件,所以一般主张新建一个新的目录,专门用来编译),一般构建进程如下:

1.mkdir build 2.cd build 3.cmake .. 或许 cmake -G Ninja .. 4.make 或许 ninja。

其间cmake ..在build文件夹下生成Makefile。make指令在Makefile地点的目录下履行,依据Makefile进行编译。

或许cmake -G Ninja ..在build文件夹下生成build.ninja。ninja指令在build.ninja地点的目录下履行,依据build.ninja进行编译。

2、CMake中的穿插编译设置。

装备办法一:

直接在CMakeLists.txt文件中,运用CMAKE_C_COMPILER和CMAKE_CXX_COMPILER这两个变量来指定C和C++的编译器途径。运用CMAKE_LINKER变量来指定项目的链接器。这样,当CMake生成构建文件时,就会运用指定的编译器来编译源代码。运用指定的链接器进行项目的链接操作。

以下是一个简略的设置穿插编译器和链接器的CMakeLists.txt文件示例:

# 指定CMake的最低版别要求 cmake_minimum_required(VERSION 3.10) # 项目称号 project(C。ros。sCompileExample) # 设置C编译器和C++编译器 set(CMAKE_C_COMPILER "/path/to/c/compiler") set(CMAKE_CXX_COMPILER "/path/to/cxx/compiler") # 设置链接器 set(CMAKE_LINKER "/path/to/linker") # 增加可履行文件 add_executable(myapp main.cpp)。

别的咱们还能够运用独自东西链文件装备穿插编译环境。

装备办法二:CMake中运用东西链文件装备。

东西链文件(toolchain file)是将装备信息提取到一个独自的文件中,以便于在多个项目中复用。包含一系列CMake变量界说,这些变量指定了编译器、链接器和其他东西的方位,以及其他与方针渠道相关的设置,以确保它能够正确地为方针渠道生成代码。它让咱们能够专心于处理实践的问题,而不是每次都要手动装备编译器和东西。

一个根本的东西链文件示例如下:

创立一个名为toolchain.cmake的文件,并在其间界说东西链的途径和设置:

该项目需求为ARM架构的Linux体系进行穿插编译。

# 设置C和C++编译器 set(CMAKE_C_COMPILER "/path/to/c/compiler") set(CMAKE_CXX_COMPILER "/path/to/cxx/compiler") # 设置链接器 set(CMAKE_LINKER "/path/to/linker") # 指定方针体系的类型 set(CMAKE_SYSTEM_NAME Linux) set(CMAKE_SYSTEM_PROCESSOR arm) # 其他与方针渠道相关的设置 # ...。

在履行cmake指令构建时,运用-。DC。MAKE_TOOLCHAIN_FILE参数指定东西链文件的途径:

cmake -DCMAKE_TOOLCHAIN_FILE=/path/to/toolchain.cmake /path/to/source。

这样,CMake就会运用东西链文件中指定的编译器和设置来为方针渠道生成代码。

五、V8和惯例C++库移植的严重差异。

惯例C++项目依照上述穿插编译介绍的装备即可完结穿插编译进程,可是V8的移植有必要充沛了解builtin和snapshot才干完结!一般的库,所谓穿插编译便是调用方针渠道指定的东西链直接编译源码生成方针渠道的文件。比方一个C文件要给android用,调用ndk包的gcc、clang编译即可。但由于v8的builtin实践用的是v8自己的东西链体系编译成方针渠道的代码,所以并不能套用上面的办法。

1、builtin。

1.1、builtin是什么。

在V8引擎中,builtin即内置函数或模块。V8的内置函数和模块是JavaScript言语的一部分,供给了一些根本的功用,例如数学运算、字符串操作、日期处理等。别的ignition解析器每一条字节码指令完结也是一个builtin。

V8的内置函数和模块是经过C++代码完结的,并在编译时直接集成到V8引擎中。这些内置函数和模块不需求在JavaScript代码中显式地导入或引证,就能够直接运用。

以下是一些V8的内置函数和模块的比方:

•Math方针:供给了各种数学运算的函数,例如Math.sin()、Math.cos()等。

•String方针:供给了字符串操作的函数,例如String.prototype.split()、String.prototype.replace()等。

•Date方针:供给了日期和时刻处理的函数,例如Date.now()、Date.parse()等。

•JSON方针:供给了JSON数据的解析和生成的函数,例如JSON.parse()、JSON.stringify()等。

•ArrayBuffer方针:供给了对二进制数据的操作的函数,例如ArrayBuffer.prototype.slice()、ArrayBuffer.prototype.byteLength等。

•WebAssembly模块:供给了对WebAssembly模块的加载和实例化的函数,例如WebAssembly.compile()、WebAssembly.instantiate()等。

这些内置函数和模块都是V8引擎的重要组成部分,供给了根底的JavaScript功用。它们是V8运转时最重要的“积木块”;

1.2、builtin是怎样生成的。

v8源码中builtin的编译比较绕,由于v8中大大都builtin的“源码”,其实是builtin的生成逻辑,这也是了解V8源码的要害。

builtin和snapshot都是经过mksnapshot东西运转生成的。mksnapshot是v8编译进程中的一个中心产品,也便是说v8编译进程中会生成一个mksnapshot可履行程序而且会履行它生成v8后续编译需求的builtin和snapshot,就像套娃相同。

例如v8源码中字节码Ldar指令的完结如下:

IGNITION_HANDLER(Ldar, InterpreterAssembler) { TNode。< Object >value = LoadRegisterAtOperandIndex(0); SetAccumulator(value); Dispatch(); }。

上述代码只在V8的编译阶段由mksnapshot程序履行,履行后会产出机器码(JIT),然后mksnapshot程序把生成的机器码dump下来放到汇编文件embedded.S里,编译进V8运转时(相当于用JIT编译器去AOT)。

builtin被dump到embedded.S的对应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中界说,这个文件还include一个依据ignition指令生成的builtin列表以及torque编译器生成的builtin界说,总共1700+个builtin。每个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, Inte。rf。aceDescriptor) /* 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宏内界说了一切的builtin,并依据其类型去调用不同的参数,在这儿参数是BUILD_CPP, BUILD_TFJ...这些,界说了不同的生成战略,这些参数去掉前缀代表不同的builtin类型(CPP, TFJ, TFC, TFS, TFH, BCH, ASM)。

mksnapshot履行时生成builtin的办法有两种:

•直接生成机器码,ASM和CPP类型builtin运用这种办法(CPP类型仅仅生成适配器)。

•先生成turbofan的graph(IR),然后由turbofan编译器编译成机器码,除ASM和CPP之外其它builtin类型都是这种。

例如:DoubleToI是一个ASM类型builtin,功用是把double转成整数,该builtin的JIT生成逻辑坐落Builtins::Generate_DoubleToI,假如是x64的window,该函数放在v8/src/builtins/x64/builtins-x64.cc文件。由于每个CPU架构的指令都不相同,所以每个CPU架构都有一个完结,放在各自的builtins-ArchName.cc文件。

x64的完结如下:

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; __。 sub。l(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); }。

看上去很像汇编(编程的考虑办法按汇编来),实践上是c++函数,比方这行movl。

__ movl(scratch1, mantissa_operand);

__是个宏,实践上是调用masm变量的函数(movl)。

#define __ ACCESS_MASM(masm) #define ACCESS_MASM(masm) masm->。

而movl的完结是往pc_指针指向的内存写入mov指令及其操作数,并把pc_指针行进指令长度。

ps:一条条指令写下来,然后把内存权限改为可履行,这便是JIT的根本原理。

除了ASM和CPP的其它类型builtin都经过调用CodeStubAssembler API(下称CSA)编写,这套API和之前介绍ASM类型builtin时说到的“类汇编API”相似,不同的是“类汇编API”直接产出原生代码,CSA产出的是turbofan的graph(IR)。CSA比起“类汇编API”的长处是不必每个渠道各写一次。

可是类汇编的CSA写起来仍是太费力了,所以V8供给了一个类javascript的高档言语:torque,这言语终究会编译成CSA办法的c++代码和V8其它C++代码一同编译。

例如Array.isArray运用torque言语完结如下:

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。

经过torque编译器编译后,会生成一段杂乱的CSA的C++代码,下面截取一个片段。

TNode。< JSProxy >Cast_JSProxy_1(compiler::CodeAssemblerState* state_, TNode。< Context >p_context, TNode。< Object >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。< Object >{p_o}, &label1); ca_.Goto(&block3); if (label1.is_used()) { ca_.Bind(&label1); ca_.Goto(&block4); } } // other code ... }。

和上面讲的Ldar字节码相同,这并不是跑在v8运转时的Array.isArray完结。这段代码只运转在mksnapshot中,这段代码的产品是turbofan的IR。IR经过turbofan的优化编译后生成方针机器指令,然后dump到embedded.S汇编文件,下面才是真实跑在v8运转时的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。

在这个进程中,JIT编译器turbofan相同干的是AOT的活。

1.3、builtin是怎样加载运用的。

mksnapshot生成的包含一切builtin的产品embedded.S会和其他v8源码一同编译成终究的v8库,embedded.S中声明晰四个大局变量,分别是:

•v8_Default_embedded_blob_code_:初始化为第一个builtin的开始方位(悉数builtin紧凑的放在一个代码段里)。

•v8_Default_embedded_blob_data_:指向一块数据,这块数据包含比方各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_数据的总长度。

在v8/src/execution/isolate.cc中声明晰几个extern变量,链接embedded.S后v8/src/execution/isolate.cc就能引证到那几个变量:

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的偏移,这些偏移组成一个数组,放在isolate的builtin_entry_table,数组下标是该builtin的枚举值。调用某builtin便是builtin_entry_table经过枚举值获取开始地址调用。

2、snapshot。

在V8引擎中,snapshot是指在发动时将部分或悉数JavaScript堆内存的状况保存到一个文件中,以便在后续的发动中能够快速康复到这个状况。这个技能能够明显削减V8引擎的发动时刻,特别是在大型运用程序中。

snapshot文件包含了以下几个部分:

•JavaScript堆的内存布局:包含了一切方针的地址、巨细和类型等信息。

•JavaScript代码的字节码:包含了一切现已编译的JavaScript函数的字节码。

•大局方针的状况:包含了大局方针的特点值、函数指针等信息。

•其他必要的状况:例如,废物收回器的状况、Just-In-Time (JIT) 编译器的缓存等。

当V8引擎发动时,假如存在有用的Snapshot文件,V8会直接从这个文件中读取JavaScript堆的状况和字节码,而不需求从头解析和编译一切的JavaScript代码。这能够大幅度缩短V8引擎的发动时刻。V8的Snapshot技能有以下几个长处:

•快速发动:能够明显削减V8引擎的发动时刻,特别是在大型运用程序中。

•低内存占用:由于部分或悉数JavaScript堆的状况现已被保存到文件中,所以在发动时能够节约内存。

•稳定性:Snapshot文件是由V8引擎生成的,确保了与引擎的兼容性和稳定性。

假如不是穿插编译,snapshot生成仍是挺简略了解的:v8对各种方针有做了序列化和反序列化的支撑,所谓生成snapshot,便是序列化,一般会以context作为根来序列化。

mksnapshot制造快照能够输入一个额定的脚本,也便是生成snapshot前答应履行一段代码,这段代码调用到的函数的编译成果也会序列化下来,后续加载快照反序列化后等同于履行过了这脚本,就免去了编译进程,大大加速的发动的速度。

mksnapshot制造快照是经过调用v8::SnapshotCreator完结,而v8::SnapshotCreator供给了咱们输入外部数据的时机。假如只要一个Context需求保存,用SnapshotCreator::SetDefaultContext就能够了,康复时直接v8::Context::New即可。假如有多于一个Context,能够经过SnapshotCreator::AddContext增加,它会回来一个。索引。,康复时输入索引即可康复到指定的存档。假如保存Context之外的数据,能够调用SnapshotCreator::AddData,然后经过Isolate或许Context的GetDataFromSnapshot。接口。康复。

//保存 size_t context_index = snapshot_creator.AddContext(context, si_cb); //康复 v8::Local。< v8::Context >context = v8::Context::FromSnapshot(isolate, context_index, di_cb).ToLocalChecked();

结合穿插编译时就会有个很隐晦的当地:咱们前面说到mksnapshot在穿插编译时,JIT生成的builtin是方针机器指令,而js的运转得经过跑builtin来完结(Ignition解析器每个指令便是一个builtin),这方针机器指令(比方arm64)怎样在本地(比方linux 的x64)跑起来呢?mksnapshot为了完结穿插编译中方针渠道snapshot的生成,它做了各种cpu(arm、mips、。risc。、ppc)的。模仿。器(。Sim。ulator)。

经过检查源码穿插编译时,mksnapshot会用一个方针机器的模仿器来跑这些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。< Return >( reinterpret_cast。< Address >(fn_ptr_), args...); } #else DISABLE_CFI_ICALL Return Call(Args... args) { // other code ... } #endif // USE_SIMULATOR。

假如穿插编译,将会走USE_SIMULATOR分支。arm64将会调用到v8/src/execution/simulator-arm64.h,v8/src/execution/simulator-arm64.cc完结的模仿器。上面Call的处理是把指令首地址赋值到模仿器的_pc寄存器,参数放寄存器,履行完指令从寄存器获取回来值。

六、V8移植的详细进程。

一般咱们将担任编译的机器称为host,编译产品运转的方针机器称为target。

•本文运用的host机器是Mac M1 ,Xcode版别Version 14.2 (14C18)。

•鸿蒙IDE版别:DevEco Studio NEXT Developer Beta5。

•鸿蒙SDK版别是。HarmonyOS。-NEXT-DB5。

•方针机器架构:arm64-v8a。

假如要在Mac M1上穿插编译鸿蒙arm64的builtin,进程如下:

•调用本地编译器,编译一个Mac M1版别mksnapshot可履行程序。

•履行上述mksnapshot生成鸿蒙渠道arm64指令并dump到embedded.S。

•调用鸿蒙sdk的东西链,编译链接embedded.S和v8的其它代码,生成能在鸿蒙arm64上运用的v8库。

1.首要装置cmake及ninja构建东西。

鸿蒙sdk自带构建东西咱们能够将它们参加环境变量中运用。

2.编写穿插编译V8到鸿蒙的CMakeList.txt。

总共有1千多行,部分CMakeList.txt片段:

3.运用host本机的编译东西链编译。

$ mkdir build $ cd build $ cmake -G Ninja .. $ ninja 或许 cmake --build .。

首要创立一个编译目录build,翻开build履行cmake -G Ninja ..生成针对ninja编译需求的文件。

下面是控制台打印的东西链装备信息,运用的是Mac本地xcode的东西链:

build文件夹下生成以下文件:

其间CMakeCache.txt是一个由CMake生成的缓存文件,用于存储CMake在装备进程中所做的挑选和决议计划。它是依据你的项目的CMakeLists.txt文件和体系环境来生成一个初始的CMakeCache.txt文件。这个文件包含了一切可装备的选项及其默认值。

build.ninja文件是Ninja的首要输入文件,包含了项目的一切构建规矩和依靠联系。

这个文件的内容是Ninja的语法,描绘了怎样从源文件生成方针文件。它包含了以下几个部分:

•规矩:界说了怎样从源文件生成方针文件的规矩。例如,编译C++文件、链接库等。

•构建方针:列出了项目中一切需求构建的方针,包含可履行文件、静态库、动态库等。

•依靠联系:描绘了各个构建方针之间的依靠联系。Ninja会依据这些依靠联系来确认构建的次序。

•变量:界说了一些Ninja运用的变量,例如编译器、编译选项等。

然后履行cmake --build .或许ninja。

检查build文件夹下生成的产品:

其间红框中的三个可履行文件是在编译进程中生成,一同还会在编译进程中履行。bytecode_builtins_list_generator首要生成是字节码对应builtin的生成代码。torque担任将.tq后缀的文件(运用torque言语编写的builtin)编译成CSA类型builtin的c++源码文件。

torque编译.tq文件生成的c++代码在torque-generated目录中:

bytecode_builtins_list_generator履行生成字节码函数列表鄙人面目录中:

mksnapshot则链接这些代码并履行,履行期间会在内置的对应架构模仿器中运转v8,终究生成host渠道的buildin汇编代码——embedded.S和snapshot(context的序列化方针)——snapshot.cc。它们跟从其他v8源代码一同编译生成终究的v8静态库libv8_snapshot.a。现在build目录中现已编译出host渠道的完好v8静态库及指令行调试东西d8。

mksnapshot程序本身的编译生成及履行在CMakeList.txt中的装备代码如下:


4.运用鸿蒙SDK的编译东西链编译。


由于在编译target渠道的v8时中心生成的bytecode_builtins_list_generator,torque,mksnapshot可履行文件是针对target架构的无法在host机器上履行。所以首要需求把上面在host渠道生成的可履行文件拷贝到/usr/local/bin,这样在编译target渠道的v8进程中履行这些中心程序时会找到/usr/local/bin下的可履行文件正确的履行生成针对target的builtin和snapshot快照。

$ cp bytecode_builtins_list_generator torque mksnapshot /usr/local/bin $ mkdir ohosbuild #创立新的鸿蒙v8的编译目录 $ cd ohosbuild #运用鸿蒙供给的东西链文件 $ cmake -DOHOS_STL=c++_shared -DOHOS_ARCH=arm64-v8a -DOHOS_PLATFORM=OHOS -DCMAKE_TOOLCHAIN_FILE=/Applications/DevEco-Studio.app/Contents/sdk/。Harmony。OS-NEXT-DB5/。openharmony。/native/build/cmake/ohos.toolchain.cmake -G Ninja .. $ ninja 或许 cmake --build .。

履行第一步cmake装备后控制台的信息能够看到,运用了鸿蒙的东西链。

履行完结后ohosbuild文件夹下生成了鸿蒙渠道的v8静态库,能够修正CMakeList.txt装备组成一个.a或许生成.so。

七、鸿蒙工程中运用v8库。

1.新建native c++工程。

2.导入v8库。

将v8源码中的include目录和上面编译生成的.a文件放入cpp文件夹下。


3.修正cpp目录下CMakeList.txt文件。

设置c++规范17,链接v8静态库。

4.增加napi办法测验运用v8。

下面是简略的demo。

导出c++办法。

、。

arkts侧调用c++办法。

运转检查成果:

八、JS引擎的开展趋势。

跟着。物联网。的开展,人们对IOT设备(如。智能。手表)的运用越来越多。假如期望把JS运用到IOT范畴,必定需求从JS引擎视点去进行优化,仅仅去做上层的结构收效甚微。由于关于IOT硬件来说,CPU、内存、电量都是需求省着点用的,不是每一个智能家电都需求装一个骁龙855。那怎样能够依据V8引擎进行改造来进一步提高JS的履行功用呢?

•运用TypeScript编程,遵从严厉的类型化编程规矩;

•构建的时分将TypeScript直接编译为Bytecode,而不是生成JS文件,这样运转的时分就省去了Parse以及生成Bytecode的进程;

•运转的时分,需求先将Bytecode编译为对应CPU的汇编代码;

•由于选用了类型化的编程办法,有利于编译器优化所生成的汇编代码,省去了许多额定的操作;

依据V8引擎来完结,技能上应该是可行的:

•将Parser以及Ignition拆分出来,用于构建阶段;

•删掉TurboFan处理JS动态特性的相关代码;

这样能够将JS引擎简化许多,一方面不再需求parse以及生成bytecode,另一方面编译器不再需求由于JavaScript动态特性做许多额定的作业。因而能够削减CPU、内存以及电量的运用,优化功用,仅有的问题是有必要运用严厉的TS语法进行编程。

Facebook的Hermes差不多便是这么干的,仅仅它没有要求用TS编程。

现在鸿蒙原生的ETS引擎Panda也是这么干的,它要求运用ets语法,其实是依据TS只不过做了愈加严厉的类型及语法约束(放弃了更多的动态特性),进一步提高js的履行功用。

将V8移植到鸿蒙体系是一个巨大的嵌入式范畴作业,触及穿插编译、CMake、CLang、Ninja、C++、torque等各种常识,尽管咱们阅历了巨大应战并把握了V8移植技能,但出于运用包巨细、稳定性、兼容性、保护本钱等维度归纳考虑,假如华为体系能内置V8,对Roma结构及业界一切依靠JS虚拟机的跨端结构都是一件含义深远的工作,经过和华为继续沟通,鸿蒙从API11版别供给了一个内置的JS引擎,它实践上是依据v8的封装,并供给了一套c-api接口。

假如不想用c-api而且不考虑包巨细的问题依然能够自己编译一个独立的v8引擎嵌入APP,直接运用v8面向方针的C++ API。

Roma结构是一个触及JavaScript、C&C++、Harmony、iOS、Android、Java、Vue、Node、Webpack等许多范畴的归纳处理方案,咱们有各个范畴优异的小伙伴一起前行,我们假如想深化了解某个范畴的详细完结,能够随时留言沟通~。

审阅修改 黄宇。

内容来源:https://artdesignphuong.com/app-1/đời và đá remix mp3,https://chatbotjud-hml.saude.mg.gov.br/app-1/395-bet

(责任编辑:人文)

    系统发生错误

    系统发生错误

    您可以选择 [ 重试 ] [ 返回 ] 或者 [ 回到首页 ]

    [ 错误信息 ]

    页面发生异常错误,系统设置开启调试模式后,刷新本页查看具体错误!