I'll take a quiet life. A handshake of carbon monoxide.

0%

ChakraCore学习笔记(二):从Hello World看ChakraCore顶层数据结构

原创文章,转载请标明出处:Soul Orbit
本文链接地址:ChakraCore学习笔记(二):从Hello World看ChakraCore顶层数据结构

上一篇里我们已经大概了解了如何编译和使用ChakraCore,现在我们来仔细看一看ChakraCore的Hello World,以此出发来了解一下ChakraCore的主体结构大概长的什么样子吧。

1. 代码结构

在看代码之前,先让我们来大概看一眼ChakraCore的代码结构:

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
.
├─bin :: 生成可执行文件的工程,比如ChakraCore.dll
├─Build :: 各个平台下的makefile或项目工程
├─jenkins :: 给jenkins使用的检查和编译的脚本
├─lib :: ChakraCore主要实现
│ ├─Backend :: JIT的实现
│ ├─Common :: ChakraCore的基础库
│ │ ├─Codex :: 编码转换,比如UTF-8
│ │ ├─Common :: 基础类,比较杂,比如一些和整型相关的数学计算,时间处理之类的
│ │ ├─Core :: 也比较杂,和Common的区别在于这里面一般和ChakraCore的功能相关,比如配置文件等等
│ │ ├─DataStructures :: 基础数据结构,比如数组,链表,HashMap等等
│ │ ├─Exceptions :: 常用异常
│ │ ├─Memory :: 内存管理,GC
│ │ ├─PlaceHolder :: 啊……EXO ME?
│ │ ├─PlatformAgnostic :: 和平台相关的信息的封装,但是这里面只有定义,实现不在这
│ │ └─Util :: 啊……EXO ME?
│ ├─JITClient :: 进程外JIT的客户端
│ ├─JITIDL :: 进程外JIT的协议,IDL定义
│ ├─JITServer :: 进程外JIT的服务端
│ ├─Jsrt :: Jsrt API,其本身都比较简单,主要是用来将ChakraCore内部结构合理的暴露出来
│ ├─Parser :: JavaScript Parser
│ ├─Runtime :: Runtime,ChakraCore最关键的部分之一
│ │ ├─Base :: Runtime中的顶层数据结构,比如我们马上会提到的ThreadContext和ScriptContext
│ │ ├─ByteCode :: ByteCode相关的实现
│ │ ├─Debug :: 用于支持调试的类
│ │ ├─Language :: 比较杂,和JavaScript语言相关但又不太好归类的部分
│ │ ├─Library :: JavaScript对象模型的实现,从基本的bool,int,map到高级的regex和promise
│ │ ├─Math :: 计算相关,加减乘除等等
│ │ ├─PlatformAgnostic :: 平台相关的实现
│ │ └─Types :: 用来实现对象模型的基础类型,比如抽象的Type和RecylableObject
│ └─WasmReader :: WebAssembly加载器
├─manifests :: 用于定义ETW Events的manifest
├─pal :: 一个C标准库的实现,可以使用USING_PAL_STDLIB宏来启用
├─test :: 测试
└─tools :: 一些小工具

2. Runtime和执行上下文(Execution context)

从Hello World中我们可以看到,如果我们需要ChakraCore来运行一段JS脚本,我们至少需要创建两样东西:JsRuntimeHandle(Runtime)和JsContextRef(执行上下文,execution context),这也是ChakraCore一开始最重要的两个概念。

2.1. Runtime:JsrtRuntime

Runtime这个词一听就知道非常重要,在这里也一样,它表示一个完整的用来支持JavaScript运行的环境,它具有自己独立的堆,编译器,JIT的线程和垃圾回收的线程。

在ChakraCore里,Runtime和线程是紧密相关的,但是他们并不是一对一的,一个线程上可以存在多个Runtime,但在任意时刻都只允许存在一个活动的Runtime,而一个Runtime在任意时刻也只能运行在一个线程上,不过如果一个Runtime没有被任何地方使用,比如正在执行一段JS,那么你可以安全地在另一个没有被占用的线程上调用它。

在JSRT API里,JsRuntimeHandle用于表示一个Runtime,如果我们进入JsCreateRuntime这个API,我们就会发现它其实就是其真正Runtime的实现——JsrtRuntime的内存地址。

2.2. 执行上下文:JsrtContext

执行上下文和Runtime不同,它用来隔离JavaScript脚本的执行环境,换句话说,一个Runtime里面可以有多个执行上下文,每个执行上下文里面都有自己的GlobalObject,执行上下文互相之间默认看不到对方定义的变量。就比如浏览器中,一个页面可能会有很多个iframe一样。

创建执行上下文的时候我们需要指定一个Runtime,新的执行上下文会被绑定到这个Runtime中,并且不能更改。另外多个执行上下文之间其实是共享了一个堆的,因为ChakraCore的堆是跟着Runtime走的,而且这样执行上下文之间才能互相访问对方的变量,而不用担心多线程的问题。

在JSRT API里,JsContextRef用于表示一个执行上下文,同样,如果我们进入JsCreateContext这个API,我们会发现它其实也是其真正的实现JsrtContext的内存地址。

2.3. 原始关系图

现在来看,Runtime和执行上下文的关系主要如下:
jsrt-top-level-data-structures-quick-overview
[//]: <> ()

3. ThreadContext和ScriptContext

在了解了JSRT API里看到的顶层结构之后,让我们来看看他们的具体实现吧。

3.1. JsrtRuntime和ThreadContext

我们先来看看JsrtRuntime。在Debugger下我们可以看见,这个类里面直接包含的信息其实非常的少,而且也没有看到我们期望的Recycler,和我们刚刚在介绍代码结构里提到的一样,它更像是一个最外层的封装,但是里面的另外一个类却很有意思——ThreadContext。
jsrtruntime-fields

3.1.1. ThreadContext

CreateRuntimeCore里,我们可以看到,在创建JsrtRuntime之前我们需要创建一个ThreadContext,而主要的初始化都是在ThreadContext上进行的。在ThreadContext里面,我们还可以看到比JsrtRuntime多得多的成员变量,并且有很多我们都非常感兴趣,比如:

  • 和内存管理相关的Recycler,各种Page Allocator
  • 和控制流相关的异常信息
  • 各种统计信息
  • 我们马上会提到的JsrtContext的主要实现——ScriptContext的列表
  • ……等等

可以看出,上面我们提到的Runtime提供的主要功能基本都在ThreadContext里面,可以说它是JsrtRuntime的主要实现。而通过代码我们可以看得到,JsrtRuntime和ThreadContext是一对一的,所以在读ChakraCore的代码时,我们基本可以把他们认为是一个东西。

3.1.2. ThreadContextScope

CreateRuntimeCore的最后,我们可以看到一些只有在开启调试之类状态下才会有的初始化,而在初始化之前ChakraCore会创建一个叫做ThreadContextScope的类,这个类是干什么的呢?

还记得前面提到一个线程上虽然可以有多个Runtime,但是只能有一个活动的Runtime么,这个类就是用来将Runtime注册为活动并放入TLS中的。而在此之后就可以使用ThreadContext::GetContextForCurrentThread来获取当前活动的Runtime了,不过这个函数调用的地方非常的少,基本一些异常处理和Sanity Check在用,主要还是需要传入或者通过ScriptContext去拿。

threadcontext-tls
[//]: <> ()

3.1.3. 其他的类

除了ThreadContext以外,JsrtRuntime里还有一些别的类:

  • 和ChakraCore内存管理相关的:比如AllocationPolicyManager和JsrtThreadService,我们在后面写内存管理的时候再来看这几个类,
  • JSRT API Callback:比如beforeCollectCallback等等。

3.2. JsrtContext,JavascriptLibrary和ScriptContext

JsrtContext和JsrtRuntime类似,它也只是一个Jsrt API里最外层的一个封装,其主要的实现实在两个类之中:JavascriptLibrary和ScriptContext。
jsrtcontext-fields

3.2.1. ScriptContext

虽然在上图中ScriptContext并没有直接被JsrtContext所持有,而是放在了JavascriptLibrary之中,但是我们还是先来看看这个类,因为这个类其实更加重要也更加的靠上层。

在JsrtContext的构造函数里面,我们可以看到第一步就是创建ScriptContext,而在销毁JsrtContext时,其主要做的事情也是由ScriptContext来完成的,可见ScriptContext其实就是JsrtContext的真实实现。(其实看名字我们也看的出来……)

还记得JsrtContext提供的功能么?在ScriptContext中,我们都可以在其成员变量中找到踪迹:

  • globalObject:这个就是浏览器里JavaScript中的window变量。
  • url:当前ScriptContext的创建者的URL。
  • sourceList:用于储存每个ScriptContext中加载的代码。

3.2.2. JavascriptLibrary

现在让我们看回JavascriptLibrary,这个类的作用非常的大,它保存着所有类型的Prototype对象和类型对象,并且是所有Javascript对象的工厂,虽然名字有点让人无力吐槽,因为每次一说JavascriptLibrary,别人总以为是啥开源库……

ScriptContext初始化时,它会创建并初始化GlobalObject,而GlobalObject初始化的时候,它会创建并初始化JavascriptLibrary,在它初始化的函数里面我们可以看到JavascriptLibrary会做如下几样事情:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void JavascriptLibrary::Initialize(ScriptContext* scriptContext, GlobalObject * globalObject)
{
......
// Note: InitializePrototypes and InitializeTypes must be called first.
InitializePrototypes(); // 创建所有对象类型的Prototype。

InitializeTypes(); // 使用创建好的Prototype对象来创建所有的类型对象(Type)。
// Prototype和Type的关系是:所有的对象都有对应的Type对象,而Type对象里
// 保存着它的Prototype对象。

InitializeGlobal(globalObject); // 初始化GlobalObject,创建全局变量和方法,并使用创建好的Type对象在
// GlobalObject上创建工厂方法,这些工厂方法的实现都在JavascriptLibrary上。

InitializeComplexThings(); // 创建Regex的Prototype和Type,因为它需要创建一个空的Regex。

InitializeStaticValues(); // 获取Javascript对象的虚表地址,并保存在一个列表之中,之后会传递给进程
// 外的JIT服务端来使用。
......
}

为了了解JavascriptLibrary的生命周期,最后再来看看它什么时候被销毁,这个函数是在ScriptContext被关闭的时候被调用的,所以它的生命周期基本就是跟着ScriptContext走的。

3.3. 顶层数据结构主要关系图

好了,现在我们已经基本对ChakraCore的顶层数据结构有所了解了,让我们来再次梳理一下他们之前的关系吧:
jsrt-top-level-data-structures-overview
[//]: <> ()

4. 结语

希望这篇文章能帮助大家了解ChakraCore顶层数据结构,从而让使用JSRT API或者阅读ChakraCore的代码都变得更加容易。当然,我也是现学现写点笔记,所以难免有出错的地方,还请大家能多多指教,谢谢。

同系列文章: