ArkTS Compile

在这一节中,我会介绍ArkTS的四大组件以及如何编译并成功运行ArkTS的前端和运行时。

ArkTS Components

方舟编译器(ArkCompiler)是为支持多种编程语言、多种芯片平台的联合编译、运行而设计的统一编译运行时平台。它支持包括动态类型和静态类型语言在内的多种编程语言,如JS、TS、ArkTS;它是支撑OpenHarmony系统成为打通手机、PC、平板、电视、车机和智能穿戴等多种设备的操作系统的编译运行时底座。

从结构上看,ArkCompiler主要分成两个部分:编译工具链与运行时。

编译工具链架构

ArkCompiler的编译工具链以ArkTS/TS/JS源码作为输入,将其编译生成为abc(ArkCompiler Bytecode,即方舟字节码)文件。其主要是通过在编译中产生的二进制程序es2abc进行工作:

1
es2abc hello.js

运行时架构

ArkCompiler运行时直接运行字节码文件,实现对应语言规范的语义逻辑。

主要由四个子系统组成:

  • Core Subsystem
    • Core Subsystem主要由与语言无关的基础运行库组成,包括承载字节码的File组件、支持Debugger的Tooling组件、负责适配系统调用的Base库组件等。
  • Execution Subsystem
    • Execution Subsystem包含执行字节码的解释器、快速路径内联缓存、以及抓取运行时信息的Profiler。
  • Compiler Subsystem
    • Compiler Subsystem包含Stub编译器、基于IR的编译优化框架和代码生成器。
  • Runtime subsystem
    • Runtime Subsystem包含了ArkTS/TS/JS运行相关的模块。
    • 内存管理:对象分配器与垃圾回收器(并发标记和部分内存压缩的CMS-GC和Partial-Compressing-GC)
      • 分析工具:DFX工具、cpu和heap的profiling工具
      • 并发管理:actor并发模型中的abc文件管理器
      • 标准库:Ecmascript规范定义的标准库、高效的container容器库与对象模型
      • 其他:异步工作队列、TypeScript类型加载、跟C++交互的JSNAPI接口等。

在本次任务中,我们着重关注Compiler Subsystem中的stubassember。以下给出了具体的一个工作目录结构:

1
2
3
4
5
6
7
8
/arkcompiler
├── ets_runtime # ArkTS运行时组件
├── ecmascript
├── compiler # Assember解析目录
└── stubs # stubs
├── runtime_core # 运行时公共组件
├── ets_frontend # ArkTS语言的前端工具
└── toolchain # ArkTS工具链

Compile ETS

本次编译过程中的项目组织结构为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
├── arkcompiler
│ ├── ets_frontend
│ ├── ets_runtime
│ ├── runtime_core
│ └── toolchain
├── ark.py -> arkcompiler/toolchain/build/compile_script/ark.py
├── build
│ ├── build_scripts
│ ├── config
│ ├── core
│ ├── docs
│ ├── ...
│ ├── gn_helpers.py
│ ├── ohos_var.gni
│ ├── prebuilts_download_config.json
│ ├── prebuilts_download.py
│ ├── prebuilts_download.sh
│ └── zip.py
├── developtools
├── download_packages
├── kernel
├── kernel_linux_patches -> kernel/linux/patches/
├── out
│ ├── install
│ ├── lib
│ ├── ...
│ ├── ohos-riscv64
│ ├── ohos-riscv64-install
│ ├── riscv64.release
│ └── x64.release
├── packages
├── prebuilts
├── README.md
├── third_party
└── toolchain
├── lldb-mi
└── llvm-project

Dependency && Compile

在正式开始编译之前,我们首先需要解决依赖配置。目前,我所使用的环境为Ubuntu 22.04 LTS发行版,

1
sudo apt-get install git-lfs git bison flex gnupg build-essential zip curl zlib1g-dev gcc-multilib g++-multilib libc6-dev-i386 lib32ncurses-dev x11proto-core-dev libx11-dev libc++1 lib32z1-dev ccache libgl1-mesa-dev libxml2-utils xsltproc unzip m4 libtinfo5 bc npm genext2fs liblz4-tool libssl-dev ruby openjdk-8-jre-headless gdb python3-pip libelf-dev libxcursor-dev libxrandr-dev libxinerama-dev

这里是参考了方舟运行时使用指南的依赖配置。

需要注意的是,在Ubuntu18.04Ubuntu20.04时,可能还存在python(通常新式的系统都直接支持python3)。因此需要通过符号链接生成一个python可执行文件:

1
ln -s python3 python

完成这一步后,我们需要下载一些额外的依赖,这一点可以通过项目中的下载脚本自动配置:

1
2
toolchain/llvm-project/llvm-build/env_prepare.sh
arkcompiler/toolchain/build/prebuilts_download/prebuilts_download.sh

下载完毕后,就可以通过build.py进行自动化构建ArkTS编译所需要的clang,在此处,我们需要的x64riscv64架构的构建:

1
2
3
4
5
python3 ./toolchain/llvm-project/llvm-build/build.py \
--no-build-arm \
--no-build-aarch64 \
--no-build-mipsel \
--no-build windows

我当前的配置为11th Gen Intel i5-11400H (8) @ 2.688GHz,使用的虚拟机配置为八核16G内存,通常编译clang的时长在三个半小时左右。编译完成后,需要将产出的clang拷贝到prebuilts文件夹中:

1
2
mv prebuilts/clang/ohos/linux-x86_64/llvm prebuilts/clang/ohos/linux-x86_64/llvm.origin 
cp -r out/install/linux-x86_64/clang-dev/ prebuilts/clang/ohos/linux-x86_64/llvm

为了构建在riscv64架构下适配的ArkTS,我们需要单独构建riscv64架构的LLVM lib

1
python3 toolchain/llvm-project/llvm-build/build-ohos-riscv64.py

执行build-ohos-riscv64.py通常需要半个小时左右,完成后,需要将riscv64架构的LLVM lib产出拷贝到prebuilts目录下:

1
2
3
mkdir -p prebuilts/ark_tools/ark_js_prebuilts/llvm_prebuilts_riscv64/{build,llvm}
cp -r out/ohos-riscv64-install/include prebuilts/ark_tools/ark_js_prebuilts/llvm_prebuilts_riscv64/llvm/
cp -r out/ohos-riscv64/{include,lib} prebuilts/ark_tools/ark_js_prebuilts/llvm_prebuilts_riscv64/build/

这样,前期准备就完成了。现在,就可以开始执行ark.py脚本来编译对应架构下的ets_*工具链了。

编译x86_64ArkTS前端、运行时:

1
python3 ./arkcompiler/toolchain/build/compile_script/ark.py x64.release --verbose

编译riscv64ArkTS前端、运行时:

1
python3 ./arkcompiler/toolchain/build/compile_script/ark.py riscv64.release --verbose

需要注意的是,在未修改的依赖项中riscv64架构下编译不会对ets_frontend生效。关于如何启用和修改bug,将在后面进行介绍。

然后你可以看到在out目录中的一个目录树结构:

1
2
3
4
5
6
7
out/
├── install
├── ...
├── ohos-riscv64
├── ohos-riscv64-install
├── riscv64.release
└── x64.release

我们的目标生成文件位于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
out/x64.release/
├── args.gn
├── arkcompiler
│ ├── ets_frontend
│ ├── ets_runtime
│ ├── runtime_core
│ └── toolchain
├── ...
├── gen
│ ├── arkcompiler
│ ├── isa
│ └── libpandabase
└── toolchain.ninja

out/riscv64.release/
├── args.gn
├── arkcompiler
│ ├── ets_runtime
│ ├── runtime_core
│ └── toolchain
├── ...
├── gen
│ ├── arkcompiler
│ ├── isa
│ └── libpandabase
└── toolchain.ninja

在这里我们也可以发现,riscv64架构下的arkcompiler目录中缺少了ets_frontend的生成。

Dependency Detail && Fix

在上面我们发现,我们实际真正需要的riscv64ArkTS工具链中,ets_frontend缺失,并且,关于stub实际上也没有真正编译。在这一小节中,我会对arkcompilerGN依赖进行分析,然后逐步修复错误使得ets_frontend至少可用。

Dependency Detail

我们是通过ark.py进行自动化构建的,现在对ark.py进行溯源,其原始目录位于:arkcompiler/toolchain/build/compile_script/,然后进入ark.py中进行解析:

1
2
3
4
5
6
7
8
9
10
11
12
def build_for_gn_target(self, out_path: str, gn_args: list, arg_list: list, log_file_name: str):
# prepare log file
build_log_path = os.path.join(out_path, log_file_name)
str_to_build_log = "================================\nbuild_time: {0}\nbuild_target: {1}\n\n".format(
str_of_time_now(), " ".join(arg_list))
_write(build_log_path, str_to_build_log, "a")
# gn command
print("=== gn gen start ===")
code = call_with_output(
"{0} gen {1} --args=\"{2}\"".format(
self.gn_binary_path, out_path, " ".join(gn_args).replace("\"", "\\\"")),
build_log_path)

build_for_gn_target是主要构建函数,通过gn来进行构建,而gn会通过同级目录下的.gn文件进行配置:

1
2
3
4
5
# The location of the build configuration file.
buildconfig = "//arkcompiler/toolchain/build/config/BUILDCONFIG.gn"

# The source root location.
root = "//arkcompiler/toolchain/build/core/gn"

我们暂时不需要关注buildconfig中的配置,我们主要的构建逻辑位于root中。并且,在开启riscv64后,我们需要首先知道这个信息:

1
2
3
current_os=ohos,   current_cpu=riscv64
host_os=linux, host_cpu=x64
target_os=ohos, target_cpu=riscv64

首先//root设置了一个基本的默认构建逻辑,如果host_os不在MacOS上运行时,则对于ArkTS的四个组件都应该构建:

1
2
3
4
5
6
7
8
9
10
group("default") {
if (host_os != "mac") {
deps = [
":ets_frontend",
":ets_runtime",
":runtime_core",
":toolchain",
]
}
}

ets_runtime

然后,我们来看一看ets_runtime的构建逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
group("ets_runtime") {
deps = [
"$js_root:libark_jsruntime",
"$js_root/ecmascript/js_vm:ark_js_vm",
"$js_root/ecmascript/quick_fix:quick_fix",
]
if ((target_os == "linux" && target_cpu == "x64") ||
(target_cpu == "arm64" && target_os == "ohos")) {
deps += [
"$js_root/ecmascript/compiler:ark_aot_compiler",
"$js_root/ecmascript/compiler:ark_stub_compiler",
"$js_root/ecmascript/pgo_profiler/prof_dump:profdump",
]
}
}

可以看见,在以来中,我们会生成两个可执行文件ark_js_vmquick_fix。我们主要关注ark_js_vm的生成。在后面我们发现,这里只对于x64架构的ets_runtime提供了ark_stub_compiler提供了构建依赖,而对于riscv64并没有,因此,我猜测,在后续实现时,我们需要在这里添加对于riscv64-ohos架构的支持

但是目前而言,因为riscv stub的实现暂时有些许问题,因此先忽视。在这里,ets_runtime都是能够正常编译的。

ets_frontend

现在,我们来看一看ets_frontend的构建逻辑:

1
2
3
4
5
6
7
8
group("ets_frontend") {
if ((target_os == "linux" && target_cpu == "x64") || target_os == "mingw") {
deps = [
"$ets_frontend_root/es2panda:es2panda",
"$ets_frontend_root/merge_abc:merge_abc",
]
}
}

在这里我们看到,ets_frontend只支持了x64的完整支持,因此需要做一定的修改:

1
2
3
4
5
6
7
8
9
10
11
group("ets_frontend") {
# FIX: Add the ohos-riscv64 ets_frontend dependency
if ((target_os == "linux" && target_cpu == "x64") ||
(target_os == "mingw") ||
(target_os == "ohos" && target_cpu == "riscv64")) {
deps = [
"$ets_frontend_root/es2panda:es2panda",
"$ets_frontend_root/merge_abc:merge_abc",
]
}
}

修改后我们尝试运行:

1
2
3
4
5
6
7
8
9
10
11
ERROR at //arkcompiler/toolchain/build/templates/cxx/cxx.gni:182:7: Script returned non-zero exit code.
exec_script("$build_root/templates/cxx/external_deps_handler.py",
^----------

See //arkcompiler/toolchain/build/third_party_gn/protobuf/BUILD.gn:85:1: whence it was called.
ohos_static_library("protobuf_lite_static") {
^--------------------------------------------

See //arkcompiler/ets_frontend/merge_abc/BUILD.gn:148:5: which caused the file to be included.
"$ark_third_party_root/protobuf:protobuf_lite_static",
^----------------------------------------------------

就会产生报错,通过查看错误日志后发现,这里是有一个依赖不能够被external_deps_handler.py处理,该依赖是[hilog:libhilog]

通过日志发现,最先出现的调用栈为:arkcompiler/ets_frontend/merge_abc/BUILD.gn:148:5,因此可以查看:

1
2
3
4
5
6
# Cause Proto -> hilog:libhilog
deps = [
":arkcompiler_generate_proto",
"$ark_third_party_root/protobuf:protobuf_lite_static",
"$ark_third_party_root/protobuf:protobuf_static",
]

可以发现是protobuf_lite_static依赖出现问题,因此进入arkcompiler/toolchain/build/third_party_gn/protobuf/BUILD.gn:85:1中进行查看:

1
2
3
4
5
6
7
8
9
10
11
12
ohos_static_library("protobuf_lite_static") {
...
if (!is_mingw) {
if (current_toolchain != host_toolchain) {
# target build, not host build
defines = [ "HAVE_HILOG" ]
external_deps = [ "hilog:libhilog" ]
}
} else {
defines = [ "_FILE_OFFSET_BITS_SET_LSEEK" ]
}
}

可以发现,在ohos_static_library("protobuf_lite_static")逻辑中,因为这里只是简单的判断current_toolchain != host_toolchain然后就添加了external_deps = [ "hilog:libhilog" ]从而导致无法处理[ hilog:libhilog ]

因此,我们做出以下修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# To Append the `enable_hilog` identifier
import("//arkcompiler/runtime_core/ark_config.gni")

ohos_static_library("protobuf_lite_static") {
...
if (!is_mingw) {
if (enable_hilog && current_toolchain != host_toolchain) {
# target build, not host build
defines = [ "HAVE_HILOG" ]
external_deps = [ "hilog:libhilog" ]
}
} else {
defines = [ "_FILE_OFFSET_BITS_SET_LSEEK" ]
}
}

现在再次运行riscv64的构建脚本:

1
2
3
4
5
6
7
python3 ./arkcompiler/toolchain/build/compile_script/ark.py riscv64.release --verbose

...

Done. Made 3926 targets from 543 files in 661ms

=== gn gen success! ===

runtime_core && toolchain

对于这两个结构而言,没有太多需要注意的地方,只有runtime_core构建出的两个可执行文件在后续可能会用上ark_asmark_disasm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
group("runtime_core") {
deps = [
"$ark_root/assembler:ark_asm",
"$ark_root/disassembler:ark_disasm",
]
}

group("toolchain") {
if (target_cpu != "mipsel") {
deps = [
"$toolchain_root/inspector:ark_debugger",
"$toolchain_root/inspector:connectserver_debugger",
"$toolchain_root/tooling:libark_ecma_debugger",
]
}
}

Compile Show

ArkTS result