01 背景 如图所示,Roma框架是我们自主研发的动态化跨平台解决方案,已支持iOS,android,web三端。目前在京东金融APP已经有200+页面,200+乐高楼层使用,为保证基于Roma框架开发的业务可以零成本、无缝运行到鸿蒙系统,需要将Roma框架适配到鸿蒙系统。 Roma框架是基于JS引擎运行的,在iOS系统使用系统内置的JavascriptCore,在Android系统使用V8,然而,鸿蒙系统当时却没有可以执行Roma框架的JS引擎,因此需要移植一个JS引擎到鸿蒙平台。 02 JS引擎选型
JS引擎选型
引擎名称 | 应用代表 | 公司 |
---|---|---|
V8 | Chrome/Opera/Edge/Node.js/Electron | |
SpiderMonkey | firefox | Mozilla |
JavaScriptCore | Safari | Apple |
Chakra | IE | Microsoft |
Hermes | React Native | |
JerryScript/duktape/QuickJS | 小型并且可嵌入的Javascript引擎/主要应用于IOT设备 | - |
V8移植工具选型
我们的开发环境各式各样可能系统是Mac,Linux或者Windows,架构是x86或者arm,所以要想编译出可以跑在鸿蒙系统上的v8库我们需要使用交叉编译,它是在一个平台上为另一个平台编译代码的过程,允许我们在一个平台上为另一个平台生成可执行文件。这在嵌入式系统开发中尤为常见,因为许多嵌入式设备的硬件资源有限,不适合直接在上面编译代码。
gn + ninja的构建流程如下:
1. CMake简介
cmake的构建流程如下:
2. CMake中的交叉编译设置
创建一个名为toolchain.cmake的文件,并在其中定义工具链的路径和设置:
# 设置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命令构建时,使用-DCMAKE_TOOLCHAIN_FILE参数指定工具链文件的路径:
cmake -DCMAKE_TOOLCHAIN_FILE=/path/to/toolchain.cmake /path/to/source
这样,CMake就会使用工具链文件中指定的编译器和设置来为目标平台生成代码。
1. builtin
1.1 builtin是什么
1.2 builtin是如何生成的
例如v8源码中字节码Ldar指令的实现如下:
IGNITION_HANDLER(Ldar, InterpreterAssembler) {
TNode<Object> value = LoadRegisterAtOperandIndex(0);
SetAccumulator(value);
Dispatch();
}
上述代码只在V8的编译阶段由mksnapshot程序执行,执行后会产出机器码(JIT),然后mksnapshot程序把生成的机器码dump下来放到汇编文件embedded.S里,编译进V8运行时(相当于用JIT编译器去AOT)。
上述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源代码v8\src\builtins\builtins-definitions.h中定义,这个文件还include一个根据ignition指令生成的builtin列表以及torque编译器生成的builtin定义,一共1700+个builtin。每个builtin,都会在embedded.S中生成一段代码。builtin生成的v8源代码在:v8\src\builtins\setup-builtins-internal.cc文件,其中BUILTIN_LIST宏内定义了所有的builtin,并根据其类型去调用不同的参数,参数有BUILD_CPP, BUILD_TFJ...这些,定义了不同的生成策略,这些参数去掉前缀代表不同的builtin类型(CPP, TFJ, TFC, TFS, TFH, BCH, ASM)。
直接生成机器码,ASM和CPP类型builtin使用这种方式(CPP类型只是生成适配器)
先生成turbofan的graph(IR),然后由turbofan编译器编译成机器码,除ASM和CPP之外其它builtin类型都是这种
例如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
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 ...
}
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的活。
2. snapshot
一般我们将负责编译的机器称为host,编译产物运行的目标机器称为target。
本文使用的host机器是Mac M1 ,Xcode版本Version 14.2 (14C18)
鸿蒙IDE版本:DevEco Studio NEXT Developer Beta5
鸿蒙SDK版本是HarmonyOS-NEXT-DB5
目标机器架构:arm64-v8a
调用本地编译器,编译一个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文件夹下生成的产物:
torque编译.tq文件生成的c++代码在torque-generated目录中:
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快照。
local/bin cp bytecode_builtins_list_generator torque mksnapshot /usr/
$ 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/HarmonyOS-NEXT-DB5/openharmony/native/build/cmake/ohos.toolchain.cmake -G Ninja ..
$ ninja 或者 cmake --build .
1. 新建native c++工程
2. 导入v8库
将v8源码中的include目录和上面编译生成的.a文件放入cpp文件夹下
3. 修改cpp目录下CMakeList.txt文件
设置c++标准17,链接v8静态库
4. 添加napi方法测试使用v8
导出c++方法
使用TypeScript编程,遵循严格的类型化编程规则;
构建的时候将TypeScript直接编译为Bytecode,而不是生成JS文件,这样运行的时候就省去了Parse以及生成Bytecode的过程;
运行的时候,需要先将Bytecode编译为对应CPU的汇编代码;
由于采用了类型化的编程方式,有利于编译器优化所生成的汇编代码,省去了很多额外的操作。
将V8移植到鸿蒙系统是一个巨大的嵌入式范畴工作,涉及交叉编译、CMake、CLang、Ninja、C++、torque等各种知识,虽然我们经历了巨大挑战并掌握了V8移植技术,但出于应用包大小、稳定性、兼容性、维护成本等维度综合考虑,如果华为系统能内置V8,对Roma框架及业界所有依赖JS虚拟机的跨端框架都是一件意义深远的事情,通过和华为持续沟通,鸿蒙从API11版本提供了一个内置的JS引擎,它实际上是基于v8的封装,并提供了一套c-api接口。
Roma框架是一个涉及JavaScript、C&C++、Harmony、iOS、Android、Java、Vue、Node、Webpack等众多领域的综合解决方案,我们这里有各个领域优秀的小伙伴共同前行,大家如果想深入了解某个领域的具体实现,可以随时留言交流~