v8-learning

本文主要参考 https://github.com/danbev/learning-v8

Intruction

V8 基本上由堆的内存管理和执行堆栈组成(简化助于说明。 回调队列、事件循环和 WebAPI(DOM、ajax、setTimeout 等)之类的东西可以在 Chrome 中找到,或者在 Node 的情况下,API 是 Node.js API:

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
+------------------------------------------------------------------------------------------+
| Google Chrome |
| |
| +----------------------------------------+ +------------------------------+ |
| | Google V8 | | WebAPIs | |
| | +-------------+ +---------------+ | | | |
| | | 堆 | | 栈 | | | | |
| | | | | | | | | |
| | | | | | | | | |
| | | | | | | | | |
| | | | | | | | | |
| | | | | | | | | |
| | +-------------+ +---------------+ | | | |
| | | | | |
| +----------------------------------------+ +------------------------------+ |
| |
| |
| +---------------------+ +---------------------------------------+ |
| | 事件循环 | | 任务/回调队列 | |
| | | | | |
| +---------------------+ +---------------------------------------+ |
| +---------------------------------------+ |
| | 微任务队列 | |
| | | |
| +---------------------------------------+ |
| |
| |
+------------------------------------------------------------------------------------------+

执行栈是一个帧指针栈。 对于每个调用的函数,该函数将被压入堆栈。 当该函数返回时,它将被删除。
如果该函数调用其他函数,它们将被压入堆栈。 当它们都返回后,可以从返回的点继续执行。
如果其中一个函数执行需要时间的操作,则在完成之前不会进行,因为完成的唯一方法是该函数返回并从堆栈中弹出。
当您拥有单线程编程语言时,就会发生这种情况。
这样就描述了同步函数,那么异步函数呢?
以调用 setTimeout 为例,setTimeout 函数将被压入调用堆栈并执行。 这就是回调队列和事件循环发挥作用的地方。 setTimeout 函数可以将函数添加到回调队列中。 当调用堆栈为空时,该队列将由事件循环处理。

Task

任务是可以通过将任务放在回调队列中来调度的函数。 这是由诸如 setTimeout 和 setInterval 之类的 WebAPI 完成的。
当事件循环开始执行任务时,它将运行当前在任务队列中的所有任务。 任何由 WebAPI
函数调用调度的新任务只会被推送到队列中,但在事件循环的下一次迭代之前不会执行。
当执行堆栈为空时,微任务队列中的所有任务都将运行,如果这些任务中的任何一个将任务添加到微任务队列中,微任务队列也会
运行,这与任务队列处理这种情况的方式不同。
在 Node.js 中的 setTimeout 和 setInterval…

Micro task

是在当前函数在当前调用堆栈上的所有其他函数之后运行之后执行的函数。微任务内部信息可以在中找到。

Microtask queue

当一个promise被创建时,它会立即执行,如果它已经被reovled,你可以调用它。

Template

这是对象模板 和 函数模板的super类,在javascript中,函数像对象一样拥有字段
例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class V8_EXPORT Template : public Data {//继承Data类
public:
void Set(Local<Name> name, Local<Data> value,
PropertyAttribute attributes = None);
void SetPrivate(Local<Private> name, Local<Data> value,
PropertyAttribute attributes = None);
V8_INLINE void Set(Isolate* isolate, const char* name, Local<Data> value);

void SetAccessorProperty(
Local<Name> name,
Local<FunctionTemplate> getter = Local<FunctionTemplate>(),
Local<FunctionTemplate> setter = Local<FunctionTemplate>(),
PropertyAttribute attribute = None,
AccessControl settings = DEFAULT);

Set 函数是设置使用此模板创建实例的名称和值。 SetAccessorProperty 用于使用函数获取/设置的属性。
PropertyAttribute和AccessControl定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
enum PropertyAttribute {

None = 0,
/** 不可写 **/
ReadOnly = 1 << 0,
/** 不可枚举. **/
DontEnum = 1 << 1,
/** 不可配置 **/
DontDelete = 1 << 2
};

enum AccessControl {
DEFAULT = 0,
ALL_CAN_READ = 1,
ALL_CAN_WRITE = 1 << 1,
PROHIBITS_OVERWRITING = 1 << 2
};

ObjectTemplate

在没有专用构造函数的情况下创建 JavaScript 对象如下。 当使用 ObjectTemplate 创建实例时,实例将具有在
ObjectTemplate 上配置的属性和功能。
如下:

1
const obj = {};

此类在include/v8.h中申明并扩展了模板

1
2
3
4
5
6
7
8
9
10
class V8_EXPORT ObjectTemplate : public Template { 
...
}
class V8_EXPORT Template : public Data {
...
}
class V8_EXPORT Data {
private:
Data();
};

我们创建一个 ObjectTemplate 实例,我们可以向它添加使用此 ObjectTemplate 实例创建的所有实例都将具有的属性。
这是通过调用模板类的成员 Set 来完成。 我们可以为属性指定一个 Local。 Name 是 Symbol 和 String 的超类,
它们都可以用作属性的名称。Set 的实现可以在 src/api/api.cc 中找到:

1
2
3
4
5
6
7
void Template::Set(v8::Local<Name> name, v8::Local<Data> value, v8::PropertyAttribute attribute) {
...

i::ApiNatives::AddDataProperty(isolate, templ, Utils::OpenHandle(*name),
value_obj,
static_cast<i::PropertyAttributes>(attribute));
}

另一个例子

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
38
39
40
41
42
43
44
45
46
#include <iostream>
#include "gtest/gtest.h"
#include "v8.h"
#include "libplatform/libplatform.h"
#include "v8_test_fixture.h"
#include "src/objects/objects.h"
#include "src/objects/objects-inl.h"
#include "src/api/api.h"

using namespace v8;

class ObjectTemplateTest : public V8TestFixture {
};

TEST_F(ObjectTemplateTest, AddProperty) {
const HandleScope handle_scope(isolate_);
Local<FunctionTemplate> constructor = Local<FunctionTemplate>();
Local<ObjectTemplate> ot = ObjectTemplate::New(isolate_, constructor);

// 添加一个属性,所有从该对象模板创建的实例都将拥有该属性(Set 是类 Template 的成员函数):
const char* prop_name = "prop_name";
const char* prop_value = "prop_value";
Local<Name> name = String::NewFromUtf8(isolate_, prop_name, NewStringType::kNormal).ToLocalChecked();
Local<Data> value = String::NewFromUtf8(isolate_, prop_value, NewStringType::kNormal).ToLocalChecked();
ot->Set(name, value, PropertyAttribute::None);

Handle<Context> context = Context::New(isolate_, nullptr, ot);
MaybeLocal<Object> maybe_instance = ot->NewInstance(context);
Local<Object> obj = maybe_instance.ToLocalChecked();

// 验证我们添加的属性是否存在于我们创建的实例中:
MaybeLocal<Array> maybe_names = obj->GetPropertyNames(context);
Local<Array> names = maybe_names.ToLocalChecked();
EXPECT_EQ(static_cast<int>(names->Length()), 1);
// 有趣的是 Array 除了 Length() 和三个静态方法(New、New 和 Cast)之外没有任何方法。 由于 Array extends
// Object 我们可以使用 Object::Get 和索引:
Local<Value> name_from_array = names->Get(context, 0).ToLocalChecked();
String::Utf8Value utf8_name{isolate_, name_from_array};
EXPECT_STREQ(*utf8_name, prop_name);

// 验证值是否正确。
Local<Value> val = obj->GetRealNamedProperty(context, name).ToLocalChecked();
EXPECT_TRUE(val->IsName());
String::Utf8Value utf8_value{isolate_, val};
EXPECT_STREQ(*utf8_value, prop_value);
}

FunctionTempate

用来创建函数的模板与objecttempate一样继承自template

1
2
class V8_EXPORT FunctionTemplate : public Template {
}

javascript中函数可以像对象一样拥有属性,创建函数模板方法如下:

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#include <iostream>
#include "gtest/gtest.h"
#include "v8.h"
#include "libplatform/libplatform.h"
#include "v8_test_fixture.h"
#include "src/objects/objects.h"
#include "src/objects/objects-inl.h"
#include "src/api/api-inl.h"

using namespace v8;

class FunctionTemplateTest : public V8TestFixture {
};

void function_callback(const FunctionCallbackInfo<Value>& info) {
Isolate* isolate = info.GetIsolate();
std::cout << "function_callback args= " << info.Length() << '\n';

// 如果函数是使用new调用的,则属性new.target(New Target)将被设置
Local<Value> new_target_value = info.NewTarget();
if (new_target_value.IsEmpty()) {
std::cout << "new_target_value is undefined: " << new_target_value->IsUndefined() << '\n';
}
//这里receiver 作为第二个参数传递给call函数,就像这样
Local<Object> receiver = info.This();
Local<Name> name = String::NewFromUtf8(isolate, "nr", NewStringType::kNormal).ToLocalChecked();
Local<Value> nr_local = receiver->GetRealNamedProperty(isolate->GetCurrentContext(), name).ToLocalChecked();
Local<Number> nr = nr_local->ToNumber(isolate->GetCurrentContext()).ToLocalChecked();

Local<Object> holder = info.Holder();

ReturnValue<Value> return_value = info.GetReturnValue();
double nr2 = nr->Value() + 2;
return_value.Set(nr2);

EXPECT_STREQ(*String::Utf8Value(isolate, info.Data()), "some info");
}

TEST_F(FunctionTemplateTest, FunctionTemplate) {
i::Isolate* i_isolate = V8TestFixture::asInternal(isolate_);
const HandleScope handle_scope(isolate_);
Handle<Context> context = Context::New(isolate_);
Context::Scope context_scope(context);

// 这个值和数据将由 FunctionCallbackInfo 提供
Local<Value> data = String::NewFromUtf8(isolate_, "some info", NewStringType::kNormal).ToLocalChecked();
Local<FunctionTemplate> ft = FunctionTemplate::New(isolate_, function_callback, data);
Local<Function> function = ft->GetFunction(context).ToLocalChecked();
Local<String> func_name = String::NewFromUtf8(isolate_, "SomeFunc", NewStringType::kNormal).ToLocalChecked();
function->SetName(func_name);
Local<Value> prototype = function->GetPrototype();
V8TestFixture::print_local(prototype);

Local<Object> recv = Object::New(isolate_);
Local<Name> name = String::NewFromUtf8(isolate_, "nr", NewStringType::kNormal).ToLocalChecked();
Local<Number> value = Number::New(isolate_, 18);
recv->Set(context, name, value).Check();

int argc = 0;
Local<Value> argv[] = {};
MaybeLocal<Value> ret = function->Call(context, recv, argc, nullptr);
if (!ret.IsEmpty()) {
Local<Number> nr = ret.ToLocalChecked()->ToNumber(context).ToLocalChecked();
EXPECT_EQ(nr->Value(), 20);
}

i::RootsTable roots_table = i_isolate->roots_table();
i::Heap* heap = i_isolate->heap();

//Local<Function> function2 = ft->GetFunction(context).ToLocalChecked();
//MaybeLocal<Value> ret = function->Call(context, recv, 0, nullptr);
}

TEST_F(FunctionTemplateTest, FunctionTemplateInfo) {
const HandleScope handle_scope(isolate_);
Handle<Context> context = Context::New(isolate_);
Context::Scope context_scope(context);

// // 这个值和数据将由 FunctionCallbackInfo 提供
Local<Value> data = String::NewFromUtf8(isolate_, "some info", NewStringType::kNormal).ToLocalChecked();
Local<FunctionTemplate> ft = FunctionTemplate::New(isolate_, function_callback, data);
i::Handle<i::FunctionTemplateInfo> ft_info = i::Handle<i::FunctionTemplateInfo>(
reinterpret_cast<i::Address*>(const_cast<FunctionTemplate*>(*ft)));
i::Isolate* i_isolate = V8TestFixture::asInternal(isolate_);
i::Handle<i::SharedFunctionInfo> sfi = i::FunctionTemplateInfo::GetOrCreateSharedFunctionInfo(
i_isolate, ft_info, i::MaybeHandle<i::Name>());
//std::cout << sfi->Name() << '\n';
//ft_info->GetCFunction(i_isolate);
}

创建函数模板的示例的方法如下:

1
2
Local<FunctionTemplate> ft = FunctionTemplate::New(isolate_, function_callback, data);
Local<Function> function = ft->GetFunction(context).ToLocalChecked();

并使用一下方法调用该函数:

1
MaybeLocal<Value> ret = function->Call(context, recv, 0, nullptr);

Function call在src/api/api.cc:

1
2
3
4
5
6
7
bool has_pending_exception = false;
auto self = Utils::OpenHandle(this);
i::Handle<i::Object> recv_obj = Utils::OpenHandle(*recv);
i::Handle<i::Object>* args = reinterpret_cast<i::Handle<i::Object>*>(argv);
Local<Value> result;
has_pending_exception = !ToLocal<Value>(
i::Execution::Call(isolate, self, recv_obj, argc, args), &result);

call的返回值是一个MaybeHandle将被传递给在api.h中定义的Tolocal:

1
2
3
4
5
6
7
8
9
template <class T>                                                              
inline bool ToLocal(v8::internal::MaybeHandle<v8::internal::Object> maybe,
Local<T>* local) {
v8::internal::Handle<v8::internal::Object> handle;
if (maybe.ToHandle(&handle)) {
*local = Utils::Convert<v8::internal::Object, T>(handle);
return true;
}
return false;

这时看一下Execution::call,位于execution/execution.cc中,它调用:

1
return Invoke(isolate, InvokeParams::SetUpForCall(isolate, callable, receiver, argc, argv));

SetUpForCall 将返回一个 InvokeParams。(仔细看看InvokeParams)

1
2
V8_WARN_UNUSED_RESULT MaybeHandle<Object> Invoke(Isolate* isolate,              
const InvokeParams& params) {
1
2
3
Handle<Object> receiver = params.is_construct                             
? isolate->factory()->the_hole_value()
: params.receiver;

在我们的例子中 is_construct 为false,因为我们没有使用 new 和接收器,函数中的 this 应该设置为我们传入的接收器。之后我们有
Builtins::InvokeApiFunction

1
2
3
auto value = Builtins::InvokeApiFunction(                                 
isolate, params.is_construct, function, receiver, params.argc,
params.argv, Handle<HeapObject>::cast(params.new_target));
1
2
result = HandleApiCallHelper<false>(isolate, function, new_target,        
fun_data, receiver, arguments);

api-arguments-inl.h 有:

1
2
3
4
5
6
7
FunctionCallbackArguments::Call(CallHandlerInfo handler) {
...
ExternalCallbackScope call_scope(isolate, FUNCTION_ADDR(f));
FunctionCallbackInfo<v8::Value> info(values_, argv_, argc_);
f(info);
return GetReturnValue<Object>(isolate);
}

对 f(info) 的调用是调用回调的方法,这只是一个普通的函数调用。回到 HandleApiCallHelper 有:

1
2
3
Handle<Object> result = custom.Call(call_data);                             

RETURN_EXCEPTION_IF_SCHEDULED_EXCEPTION(isolate, Object);

RETURN_EXCEPTION_IF_SCHEDULED_EXCEPTION 扩展为:

1
2
3
4
5
6
7
8
9
Handle<Object> result = custom.Call(call_data);                             
do {
Isolate* __isolate__ = (isolate);
((void) 0);
if (__isolate__->has_scheduled_exception()) {
__isolate__->PromoteScheduledException();
return MaybeHandle<Object>();
}
} while (false);

注意,如果出现异常,则返回一个空对象。 稍后在 execution.cca 中调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
auto value = Builtins::InvokeApiFunction(                                 
isolate, params.is_construct, function, receiver, params.argc,
params.argv, Handle<HeapObject>::cast(params.new_target));
bool has_exception = value.is_null();
if (has_exception) {
if (params.message_handling == Execution::MessageHandling::kReport) {
isolate->ReportPendingMessages();
}
return MaybeHandle<Object>();
} else {
isolate->clear_pending_message();
}
return value;

Address

Address在include/v8-internal.h:

1
typedef uintptr_t Address;

uintptr_t 是 cstdint 中指定的可选类型,能够存储数据指针。 它是一种无符号整数类型,任何指向 void
的有效指针都可以转换为这种类型(并返回)。

TaggedImpl

此类在 src/objects/tagged-impl.h 中声明,并有一个私有成员,声明为:

1
2
3
4
public
constexpr StorageType ptr() const { return ptr_; }
private:
StorageType ptr_;

可以使用以下方法创建实例:

1
i::TaggedImpl<i::HeapObjectReferenceType::STRONG, i::Address>  tagged{};

存储类型也可以是 globals.h 中定义的 Tagged_t:

1
using Tagged_t = uint32_t;

使用指针压缩时,它看起来可能是不同的值。

Object

这个类扩展了TaggedImpl:

1
class Object : public TaggedImpl<HeapObjectReferenceType::STRONG, Address> {       

可以使用默认构造函数创建对象,也可以通过传入 TaggedImpl 构造函数的地址来创建对象。
对象本身没有任何成员(除了从 TaggedImpl 继承的 ptr_ )。
因此,如果我们在堆栈上创建一个对象,这就像一个对象的指针/引用:

1
2
3
4
5
+------+
|Object|
|------|
|ptr_ |---->
+------+

现在, ptr_ 是一个 储存类型 所以它可能是一个 Smi 在这种情况下它会直接包含值,例如一个小整数:

1
2
3
4
5
+------+
|Object|
|------|
| 18 |
+------+

ObjectSlot

1
2
i::Object obj{18};
i::FullObjectSlot slot{&obj};
1
2
3
4
5
+----------+      +---------+
|ObjectSlot| | Object |
|----------| |---------|
| address | ---> | 18 |
+----------+ +---------+

Maybe

Maybe像一个可选项,它可以保存一个值,也可以什么都不保存

1
2
3
4
5
6
7
8
9
10
11
template <class T>                                                              
class Maybe {
public:
V8_INLINE bool IsNothing() const { return !has_value_; }
V8_INLINE bool IsJust() const { return has_value_; }
...

private:
bool has_value_;
T value_;
}

关于Maybe的理解如下:

1
2
 bool cond = true;
Maybe<int> maybe = cond ? Just<int>(10) : Nothing<int>();

有一些函数可以检查 Maybe 是否为空,如果是则使进程崩溃。 还可以使用 FromJust 检查并返回该值。
Maybe 的使用是 api 调用可能失败的地方,返回 Nothing 是一种表示这一点的方式。

MaybeLocal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template <class T>                                                              
class MaybeLocal {
public:
V8_INLINE MaybeLocal() : val_(nullptr) {}
V8_INLINE Local<T> ToLocalChecked();
V8_INLINE bool IsEmpty() const { return val_ == nullptr; }
template <class S>
V8_WARN_UNUSED_RESULT V8_INLINE bool ToLocal(Local<S>* out) const {
out->val_ = IsEmpty() ? nullptr : this->val_;
return !IsEmpty();
}

private:
T* val_;

如果 val_ 是 nullptr,ToLocalChecked 将使进程崩溃。 如果想避免崩溃,可以使用 ToLocal。

Data

Data是所有可以存在v8堆的对象的超类
avatar

1
2
3
4
class V8_EXPORT Data {                                                          
private:
Data();
};

Value

Value 扩展了 Data 并添加了许多方法来检查 Value 是否属于某种类型,例如 IsUndefined()、IsNull、IsNumber
等。它还具有转换为 Local 的有用方法,例如:

1
2
3
V8_WARN_UNUSED_RESULT MaybeLocal<Number> ToNumber(Local<Context> context) const;
V8_WARN_UNUSED_RESULT MaybeLocal<String> ToNumber(Local<String> context) const;
...

Handle

Handle 与 Object 和 ObjectSlot 的相似之处在于它也包含一个 Address 成员(称为 location_ 并在 HandleBase
中声明),但不同之处在于 Handle 充当抽象层并且可以由垃圾收集器重新定位。 可以在 src/handles/handles.h 中找到。

1
2
3
4
5
6
7
8
9
class HandleBase {  
...
protected:
Address* location_;
}
template <typename T>
class Handle final : public HandleBase {
...
}
1
2
3
4
5
+----------+                  +--------+         +---------+
| Handle | | Object | | int |
|----------| +-----+ |--------| |---------|
|*location_| ---> |&ptr_| --> | ptr_ | -----> | 5 |
+----------+ +-----+ +--------+ +---------+
1
2
(gdb) p handle
$8 = {<v8::internal::HandleBase> = {location_ = 0x7ffdf81d60c0}, <No data fields>}

注意 location_ 包含一个指针:

1
2
(gdb) p /x *(int*)0x7ffdf81d60c0
$9 = 0xa9d330

这与 obj 中的值相同:

1
2
(gdb) p /x obj.ptr_
$14 = 0xa9d330

我们可以使用任何指针访问 int:

1
2
3
4
5
6
7
8
(gdb) p /x *value
$16 = 0x5
(gdb) p /x *obj.ptr_
$17 = 0x5
(gdb) p /x *(int*)0x7ffdf81d60c0
$18 = 0xa9d330
(gdb) p /x *(*(int*)0x7ffdf81d60c0)
$19 = 0x5

HandleScope

包含许多本地/句柄(认为指向对象的指针,但由 V8 管理)并将负责为我们删除本地/句柄。 HandleScopes 是堆栈分配的
当 ~HandleScope 被调用时,在该范围内创建的所有句柄都将从 HandleScope 维护的堆栈中删除,
这使得句柄指向的对象有资格被 GC 从堆中删除。
HandleScope 只有三个成员:

1
2
3
internal::Isolate* isolate_;
internal::Address* prev_next_;
internal::Address* prev_limit_;

让我们仔细看看构造 HandleScope 时会发生什么:

1
v8::HandleScope handle_scope{isolate_};

构造函数调用将在 src/api/api.cc 中结束,构造函数只是委托给 Initialize:

1
2
3
4
5
6
7
8
9
10
11
HandleScope::HandleScope(Isolate* isolate) { Initialize(isolate); }

void HandleScope::Initialize(Isolate* isolate) {
i::Isolate* internal_isolate = reinterpret_cast<i::Isolate*>(isolate);
...
i::HandleScopeData* current = internal_isolate->handle_scope_data();
isolate_ = internal_isolate;
prev_next_ = current->next;
prev_limit_ = current->limit;
current->level++;
}

每个 v8::internal::Isolate 都有 HandleScopeData 类型的成员:

1
2
HandleScopeData* handle_scope_data() { return &handle_scope_data_; }
HandleScopeData handle_scope_data_;

HandleScopeData 是在 src/handles/handles.h 中定义的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct HandleScopeData final {
Address* next;
Address* limit;
int level;
int sealed_level;
CanonicalHandleScope* canonical_scope;

void Initialize() {
next = limit = nullptr;
sealed_level = level = 0;
canonical_scope = nullptr;
}
};

请注意,有两个指向下一个的指针 (Address* ) 和一个 limit。 当 HandleScope 初始化时,当前的 handle_scope_data
将从内部隔离中检索。 正在创建的 HandleScope 实例存储当前隔离的下一个/限制指针,以便在关闭此 HandleScope
时可以恢复它们(请参阅 CloseScope)。
那么在创建了 HandleScope 之后,Local 如何与这个实例交互呢?
当创建一个 Local 时,这将/可能通过 FactoryBase::NewStruct 它将分配一个新的 Map 然后为正在创建的 InstanceType
创建一个句柄:

1
Handle<Struct> str = handle(Struct::cast(result), isolate()); 

这将落在构造函数 Handlesrc/handles/handles-inl.h

1
2
3
4
5
template <typename T>                                                           
Handle<T>::Handle(T object, Isolate* isolate): HandleBase(object.ptr(), isolate) {}

HandleBase::HandleBase(Address object, Isolate* isolate)
: location_(HandleScope::GetHandle(isolate, object)) {}

请注意,object.ptr() 用于将地址传递给 HandleBase。 还要注意 HandleBase 将其 location_ 设置为
HandleScope::GetHandle 的结果。

1
2
3
4
5
6
Address* HandleScope::GetHandle(Isolate* isolate, Address value) {              
DCHECK(AllowHandleAllocation::IsAllowed());
HandleScopeData* data = isolate->handle_scope_data();
CanonicalHandleScope* canonical = data->canonical_scope;
return canonical ? canonical->Lookup(value) : CreateHandle(isolate, value);
}

在这种情况下将调用 CreateHandle,并且此函数将检索当前隔离的 handle_scope_data:

1
2
3
4
5
HandleScopeData* data = isolate->handle_scope_data();                         
Address* result = data->next;
if (result == data->limit) {
result = Extend(isolate);
}

在这种情况下,next 和 limit 都将为 0x0,因此将调用 Extend。 Extend 还将获取隔离物 handle_scope_data
并检查当前级别,然后获取隔离物 HandleScopeImplementer:

1
HandleScopeImplementer* impl = isolate->handle_scope_implementer();

HandleScopeImplementer 在 src/api/api.h 中声明
HandleScope:CreateHandle 将从隔离中获取handle_scope_data:

1
2
3
4
5
6
7
8
9
10
11
Address* HandleScope::CreateHandle(Isolate* isolate, Address value) {
HandleScopeData* data = isolate->handle_scope_data();
if (result == data->limit) {
result = Extend(isolate);
}
// Update the current next field, set the value in the created handle,
// and return the result.
data->next = reinterpret_cast<Address*>(reinterpret_cast<Address>(result) + sizeof(Address));
*result = value;
return result;
}

请注意,data->next 设置为传入的地址 + 地址的大小。
HandleScope 的析构函数将调用 CloseScope。 有关示例,请参见 handlescope_test.cc。

EscapableHandleScope

本地句柄位于堆栈上,并在调用适当的析构函数时被删除。 如果有一个本地
HandleScope,那么它会在范围返回时处理这个问题。 当没有对句柄的引用时,它可以被垃圾收集。
这意味着如果一个函数有一个 HandleScope 并且想要返回一个句柄/本地,那么它在函数返回后将不可用。 这就是
EscapableHandleScope 的用途,它使值能够被放置在封闭的句柄范围中以允许它生存。 当封闭的 HandleScope
超出范围时,它将被清理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class V8_EXPORT EscapableHandleScope : public HandleScope {                        
public:
explicit EscapableHandleScope(Isolate* isolate);
V8_INLINE ~EscapableHandleScope() = default;
template <class T>
V8_INLINE Local<T> Escape(Local<T> value) {
internal::Address* slot = Escape(reinterpret_cast<internal::Address*>(*value));
return Local<T>(reinterpret_cast<T*>(slot));
}

template <class T>
V8_INLINE MaybeLocal<T> EscapeMaybe(MaybeLocal<T> value) {
return Escape(value.FromMaybe(Local<T>()));
}

private:
...
internal::Address* escape_slot_;
};

来自 api.cc

1
2
3
4
5
EscapableHandleScope::EscapableHandleScope(Isolate* v8_isolate) {
i::Isolate* isolate = reinterpret_cast<i::Isolate*>(v8_isolate);
escape_slot_ = CreateHandle(isolate, i::ReadOnlyRoots(isolate).the_hole_value().ptr());
Initialize(v8_isolate);
}

因此,当创建 EscapableHandleScope 时,它将创建一个带the_hole_value的句柄并将其存储在地址类型的 escape_slot_ 中。
这个Handle 将在当前的 HandleScope 中创建,EscapableHandleScope 稍后可以为它想要转义的指针/地址设置一个值。 稍后当
HandleScope 超出范围时,它将被清理。 然后它像普通的 HandleScope 一样调用 Initialize。

1
2
3
i::Address* HandleScope::CreateHandle(i::Isolate* isolate, i::Address value) {
return i::HandleScope::CreateHandle(isolate, value);
}

来自handles-inl.h:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Address* HandleScope::CreateHandle(Isolate* isolate, Address value) {
DCHECK(AllowHandleAllocation::IsAllowed());
HandleScopeData* data = isolate->handle_scope_data();
Address* result = data->next;
if (result == data->limit) {
result = Extend(isolate);
}
// 更新当前的下一个字段,在创建的句柄中设置值,
// 并返回结果。
DCHECK_LT(reinterpret_cast<Address>(result),
reinterpret_cast<Address>(data->limit));
data->next = reinterpret_cast<Address*>(reinterpret_cast<Address>(result) +
sizeof(Address));
*result = value;
return result;
}

当调用 Escape 时,会发生以下情况(v8.h):

1
2
3
4
5
template <class T>
V8_INLINE Local<T> Escape(Local<T> value) {
internal::Address* slot = Escape(reinterpret_cast<internal::Address*>(*value));
return Local<T>(reinterpret_cast<T*>(slot));
}

在 EscapeableHandleScope::Escape (api.cc):

1
2
3
4
5
6
7
8
9
10
11
i::Address* EscapableHandleScope::Escape(i::Address* escape_value) {
i::Heap* heap = reinterpret_cast<i::Isolate*>(GetIsolate())->heap();
Utils::ApiCheck(i::Object(*escape_slot_).IsTheHole(heap->isolate()),
"EscapableHandleScope::Escape", "Escape value set twice");
if (escape_value == nullptr) {
*escape_slot_ = i::ReadOnlyRoots(heap).undefined_value().ptr();
return nullptr;
}
*escape_slot_ = *escape_value;
return escape_slot_;
}

如果 escape_value 为 null,则作为指向父 HandleScope 的指针的 escape_slot 被设置为 undefined_value()
而不是之前的洞值,并且 nullptr 将被返回。 这个返回的地址/指针将在转换为 T* 后返回。 接下来,我们看看当
EscapableHandleScope 超出范围时会发生什么。 这将调用 HandleScope::~
HandleScope,这很有意义,因为应该清理任何其他本地句柄。
Escape 将其参数的值复制到封闭范围中,删除所有本地句柄,然后返回可以安全返回的新句柄副本。

HeapObject

待补充

Local

有一个成员 val_,它是指向 T 的类型指针:

1
2
3
4
5
template <class T> class Local { 
...
private:
T* val_
}

请注意,这是一个指向 T 的指针。我们可以使用以下命令创建一个本地:

1
v8::Local<v8::Value> empty_value;

所以 Local 包含一个指向类型 T 的指针。我们可以使用 operator-> 和 operator* 访问这个指针。
我们可以使用 Local::Cast 从子类型转换为超类型:

1
2
v8::Local<v8::Number> nr = v8::Local<v8::Number>(v8::Number::New(isolate_, 12));
v8::Local<v8::Value> val = v8::Local<v8::Value>::Cast(nr);

而且还有

1
v8::Local<v8::Value> val2 = nr.As<v8::Value>();

PrintObject

使用 C++ 中的 _ v8_ internal_Print_Object:

1
2
$ nm -C libv8_monolith.a | grep Print_Object
0000000000000000 T _v8_internal_Print_Object(void*)

请注意,此函数没有命名空间。 我们可以将其用作:

1
2
3
extern void _v8_internal_Print_Object(void* object);

_v8_internal_Print_Object(*((v8::internal::Object**)(*global)));

让我们仔细看看上面的内容:

1
v8::internal::Object** gl = ((v8::internal::Object**)(*global));

我们使用解引用运算符来获取 Local (* global) 的值,它只是 T* 类型,一个指向 Local 类型的指针:

1
2
3
4
5
6
template <class T>
class Local {
...
private:
T* val_;
}

然后我们将其转换为指向 Object 的指针类型。

1
2
3
4
  gl**        Object*         Object
+-----+ +------+ +-------+
| |----->| |----->| |
+-----+ +------+ +-------+

v8::internal::Object 的一个实例只有一个数据成员,它是一个名为 ptr_ 类型为 Address 的字段:
src/objects/objects.h:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Object : public TaggedImpl<HeapObjectReferenceType::STRONG, Address> {
public:
constexpr Object() : TaggedImpl(kNullAddress) {}
explicit constexpr Object(Address ptr) : TaggedImpl(ptr) {}

#define IS_TYPE_FUNCTION_DECL(Type) \
V8_INLINE bool Is##Type() const; \
V8_INLINE bool Is##Type(const Isolate* isolate) const;
OBJECT_TYPE_LIST(IS_TYPE_FUNCTION_DECL)
HEAP_OBJECT_TYPE_LIST(IS_TYPE_FUNCTION_DECL)
IS_TYPE_FUNCTION_DECL(HashTableBase)
IS_TYPE_FUNCTION_DECL(SmallOrderedHashTable)
#undef IS_TYPE_FUNCTION_DECL
V8_INLINE bool IsNumber(ReadOnlyRoots roots) const;
}

让我们看一下其中一个功能,看看它是如何实现的。 例如在 OBJECT_TYPE_LIST 我们有:

1
2
3
4
5
#define OBJECT_TYPE_LIST(V) \
V(LayoutDescriptor) \
V(Primitive) \
V(Number) \
V(Numeric)

所以对象类将有一个看起来像这样的函数:

1
2
inline bool IsNumber() const;
inline bool IsNumber(const Isolate* isolate) const;

在 src/objects/objects-inl.h 我们将有实现:

1
2
3
bool Object::IsNumber() const {
return IsHeapObject() && HeapObject::cast(*this).IsNumber();
}

IsHeapObject 在 TaggedImpl 中定义:

1
2
3
4
5
6
7
8
 constexpr inline bool IsHeapObject() const { return IsStrong(); }

constexpr inline bool IsStrong() const {
#if V8_HAS_CXX14_CONSTEXPR
DCHECK_IMPLIES(!kCanBeWeak, !IsSmi() == HAS_STRONG_HEAP_OBJECT_TAG(ptr_));
#endif
return kCanBeWeak ? HAS_STRONG_HEAP_OBJECT_TAG(ptr_) : !IsSmi();
}

该宏可以在 src/common/globals.h 中找到:

1
2
3
#define HAS_STRONG_HEAP_OBJECT_TAG(value)                          \
(((static_cast<i::Tagged_t>(value) & ::i::kHeapObjectTagMask) == \
::i::kHeapObjectTag))

因此,我们将地址类型的 ptr_ 转换为 src/common/global.h 中定义的 Tagged_t
类型,并且可以根据是否使用压缩指针而有所不同。 如果它们不受支持,则与地址相同:

1
using Tagged_t = Address;

src/objects/tagged-impl.h:

1
2
3
4
5
template <HeapObjectReferenceType kRefType, typename StorageType>
class TaggedImpl {

StorageType ptr_;
}

HeapObjectReferenceType 可以是 WEAK 或 STRONG。 在这种情况下,存储类型是地址。 所以 Object
本身只有一个成员,它是从其唯一的超类继承的,这就是 ptr_。
所以下面是告诉编译器将我们的 Local,* global 的值视为一个指针(它已经是)指向一个指向一个内存位置的指针,该内存位置遵循
v8::internal::Object 的布局 类型,我们现在知道它有一个 prt_ 成员。 我们想取消引用它并将其传递给函数。

1
_v8_internal_Print_Object(*((v8::internal::Object**)(*global)));

ObjectTemplate

但是我仍然缺少 ObjectTemplate 和对象之间的联系。 当我们创建它时,我们使用:

1
Local<ObjectTemplate> global = ObjectTemplate::New(isolate);

在 src/api/api.cc 我们有:

1
2
3
4
5
6
7
8
9
10
11
12
13
static Local<ObjectTemplate> ObjectTemplateNew(
i::Isolate* isolate, v8::Local<FunctionTemplate> constructor,
bool do_not_cache) {
i::Handle<i::Struct> struct_obj = isolate->factory()->NewStruct(
i::OBJECT_TEMPLATE_INFO_TYPE, i::AllocationType::kOld);
i::Handle<i::ObjectTemplateInfo> obj = i::Handle<i::ObjectTemplateInfo>::cast(struct_obj);
InitializeTemplate(obj, Consts::OBJECT_TEMPLATE);
int next_serial_number = 0;
if (!constructor.IsEmpty())
obj->set_constructor(*Utils::OpenHandle(*constructor));
obj->set_data(i::Smi::zero());
return Utils::ToLocal(obj);
}

在这种情况下,什么是结构?
src/objects/struct.h

1
2
3
4
5
6
7
#include "torque-generated/class-definitions-tq.h"

class Struct : public TorqueGeneratedStruct<Struct, HeapObject> {
public:
inline void InitializeBody(int object_size);
void BriefPrintDetails(std::ostream& os);
TQ_OBJECT_CONSTRUCTORS(Struct)

请注意,包含指定扭矩生成的包含,可以在 /x64.release_gcc/gen/torque-generated/class-definitions-tq 中找到。
因此,在编译主源文件之前,必须在某处调用torque executable,该可执行文件生成代码存根汇编程序 C++ 头文件和源代码。 在
Building V8 中有并且有一个关于此的部分。
宏 TQ_OBJECT_CONSTRUCTORS 可以在 src/objects/object-macros.h 中找到并扩展为:

1
2
3
4
5
6
7
 constexpr Struct() = default;

protected:
template <typename TFieldType, int kFieldOffset>
friend class TaggedField;

inline explicit Struct(Address ptr);

那么 TorqueGeneratedStruct 是什么样的呢?

1
2
3
template <class D, class P>
class TorqueGeneratedStruct : public P {
public:

在这种情况下,其中 D 是 Struct 而 P 是 HeapObject。 但是上面是类型的声明,但是我们在 .h 文件中拥有的是生成的内容。
这种类型在 src/objects/struct.tq 中定义:

1
2
3
4
5
@abstract                                                                       
@generatePrint
@generateCppClass
extern class Struct extends HeapObject {
}

NewStruct 可以在 src/heap/factory-base.cc 中找到

1
2
3
4
5
6
7
8
9
10
template <typename Impl>
HandleFor<Impl, Struct> FactoryBase<Impl>::NewStruct(
InstanceType type, AllocationType allocation) {
Map map = Map::GetStructMap(read_only_roots(), type);
int size = map.instance_size();
HeapObject result = AllocateRawWithImmortalMap(size, allocation, map);
HandleFor<Impl, Struct> str = handle(Struct::cast(result), isolate());
str->InitializeBody(size);
return str;
}

存储在 v8 堆上的每个对象都有一个 Map (src/objects/map.h),用于描述所存储对象的结构。

1
class Map : public HeapObject {
1
2
3
1725    return Utils::ToLocal(obj);
(gdb) p obj
$6 = {<v8::internal::HandleBase> = {location_ = 0x30b5160}, <No data fields>}

所以这就是连接,我们看到的Local是一个HandleBase。

1
2
3
4
5
6
7
(lldb) expr gl
(v8::internal::Object **) $0 = 0x00000000020ee160
(lldb) memory read -f x -s 8 -c 1 gl
0x020ee160: 0x00000aee081c0121

(lldb) memory read -f x -s 8 -c 1 *gl
0xaee081c0121: 0x0200000002080433

您可以使用以下命令重新加载 .lldbinit:

1
(lldb) command source ~/.lldbinit

这在调试 lldb 命令时很有用。 您可以在该位置设置断点并中断并更新命令并重新加载,而无需重新启动 lldb。
目前,v8 附带的 lldb-commands.py 包含对传递给 ptr_arg_cmd 的参数的额外操作:

1
2
3
4
5
6
def ptr_arg_cmd(debugger, name, param, cmd):                                    
if not param:
print("'{}' requires an argument".format(name))
return
param = '(void*)({})'.format(param)
no_arg_cmd(debugger, cmd.format(param))

请注意,param 是我们要打印的对象,例如,假设它是一个名为 obj 的本地对象:

1
param = "(void*)(obj)"

然后这将被“传递”/格式化为命令字符串:

1
"_v8_internal_Print_Object(*(v8::internal::Object**)(*(void*)(obj))")

Threads

V8 是单线程的(堆栈功能的执行),但有支持线程用于垃圾收集、分析(IC,也许还有其他东西)(我认为)。 让我们看看有哪些线程:

1
2
3
4
5
$ LD_LIBRARY_PATH=../v8_src/v8/out/x64.release_gcc/ lldb ./hello-world 
(lldb) br s -n main
(lldb) r
(lldb) thread list
thread #1: tid = 0x2efca6, 0x0000000100001e16 hello-world`main(argc=1, argv=0x00007fff5fbfee98) + 38 at hello-world.cc:40, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1

所以在启动时只有一个线程,这是我们所期望的。 让我们跳到我们创建平台的地方:

1
2
3
4
5
6
7
Platform* platform = platform::CreateDefaultPlatform();
...
DefaultPlatform* platform = new DefaultPlatform(idle_task_support, tracing_controller);
platform->SetThreadPoolSize(thread_pool_size);

(lldb) fr v thread_pool_size
(int) thread_pool_size = 0

接下来检查 0,处理器数量 -1 用作线程池的大小:

1
2
(lldb) fr v thread_pool_size
(int) thread_pool_size = 7

这就是 SetThreadPoolSize 所做的一切。 在此之后,我们有:

1
2
3
4
platform->EnsureInitialized();

for (int i = 0; i < thread_pool_size_; ++i)
thread_pool_.push_back(new WorkerThread(&queue_));

new Worker Thread 将创建一个新的 pthread:

1
result = pthread_create(&data_->thread_, &attr, ThreadEntry, this);

可以在 src/base/platform/platform-posix 中找到 ThreadEntry

International Component for Unicode (ICU)

Unicode 国际组件 (ICU) 处理国际化 (i18n)。 ICU 提供支持区域设置敏感的字符串比较、日期/时间/数字/货币格式等。
有一个名为 ECMAScript 402 的可选 API,V8 支持并默认启用。 i18n-support 表示即使您的应用程序不使用 ICU,您仍然需要调用
InitializeICU :

1
V8::InitializeICU();

Local

1
Local<String> script_name = ...;

script_name是一个由 v8 GC 管理的对象引用。 GC 需要能够移动事物(指针)并跟踪事物是否应该被 GC
与持久句柄相比,本地句柄是轻量级的,并且主要使用本地操作。 这些句柄由 HandleScopes
管理,因此您必须在堆栈上有一个句柄范围,并且本地仅在句柄范围有效时才有效。 这使用资源获取即初始化 (RAII),因此当
HandleScope 实例超出范围时,它将删除所有本地实例。
Local 类(在 include/v8.h 中)只有一个成员,其类型为指向类型 T 的指针。所以对于上面的示例,它将是:

1
String* val_;

您可以在 include/v8.h 中找到 Local 的可用操作。

1
2
(lldb) p script_name.IsEmpty()
(bool) $12 = false

Local 重载了许多运算符,例如 ->:

1
2
(lldb) p script_name->Length()
(int) $14 = 7

其中 Length 是 v8 String 类的一个方法。
句柄堆栈不是 C++ 调用堆栈的一部分,但句柄范围嵌入在 C++ 堆栈中。 句柄范围只能堆栈分配,不能用 new 分配。

Persistent

https://v8.dev/docs/embed:持久句柄提供对堆分配的 JavaScript 对象的引用,就像本地句柄一样。有两种风格,它们处理的引用的生
命周期管理不同。当您需要为多个函数调用保留对对象的引用时,或者当句柄生存期与 C++
范围不对应时,请使用持久句柄。例如,谷歌浏览器使用持久句柄来引用文档对象模型 (DOM) 节点。
可以使用 PersistentBase::SetWeak 将持久句柄设为弱,以在对对象的唯一引用来自弱持久句柄时触发垃圾收集器的回调。
UniquePersistent 句柄依赖于 C++ 构造函数和析构函数来管理底层对象的生命周期。 Persistent
可以使用其构造函数构造,但必须使用 Persistent::Reset 显式清除。
那么持久对象是如何创建的呢?让我们编写一个测试并找出(test/persistent-object_text.cc):

1
2
$ make test/persistent-object_test
$ ./test/persistent-object_test --gtest_filter=PersistentTest.value

现在,要创建 Persistent 的实例,我们需要一个 Local 实例,否则 Persistent 实例将是空的。

1
Local<Object> o = Local<Object>::New(isolate_, Object::New(isolate_));

Local::New 可以在 src/api/api.cc 中找到:

1
2
3
4
5
6
7
Local<v8::Object> v8::Object::New(Isolate* isolate) {
i::Isolate* i_isolate = reinterpret_cast<i::Isolate*>(isolate);
LOG_API(i_isolate, Object, New);
ENTER_V8_NO_SCRIPT_NO_EXCEPTION(i_isolate);
i::Handle<i::JSObject> obj =
i_isolate->factory()->NewJSObject(i_isolate->object_function());
return Utils::ToLocal(obj);

发生的第一件事是公共 Isolate 指针被强制转换为指向内部 Isolate 类型的指针。 LOG_API 是同一源文件 (src/api/api.cc)
中的宏:

1
2
3
4
#define LOG_API(isolate, class_name, function_name)                           \
i::RuntimeCallTimerScope _runtime_timer( \
isolate, i::RuntimeCallCounterId::kAPI_##class_name##_##function_name); \
LOG(isolate, ApiEntryCall("v8::" #class_name "::" #function_name))

如果我们的情况,预处理器会将其扩展为:

1
2
3
i::RuntimeCallTimerScope _runtime_timer(
isolate, i::RuntimeCallCounterId::kAPI_Object_New);
LOG(isolate, ApiEntryCall("v8::Object::New))

LOG 是一个宏,可以在 src/log.h 中找到:

1
2
3
4
5
#define LOG(isolate, Call)                              \
do { \
v8::internal::Logger* logger = (isolate)->logger(); \
if (logger->is_logging()) logger->Call; \
} while (false)

这将扩展到:

1
2
v8::internal::Logger* logger = isolate->logger();
if (logger->is_logging()) logger->ApiEntryCall("v8::Object::New");

因此,随着 LOG_API 宏的扩展,我们有:

1
2
3
4
5
6
7
8
9
10
11
Local<v8::Object> v8::Object::New(Isolate* isolate) {
i::Isolate* i_isolate = reinterpret_cast<i::Isolate*>(isolate);
i::RuntimeCallTimerScope _runtime_timer( isolate, i::RuntimeCallCounterId::kAPI_Object_New);
v8::internal::Logger* logger = isolate->logger();
if (logger->is_logging()) logger->ApiEntryCall("v8::Object::New");

ENTER_V8_NO_SCRIPT_NO_EXCEPTION(i_isolate);
i::Handle<i::JSObject> obj =
i_isolate->factory()->NewJSObject(i_isolate->object_function());
return Utils::ToLocal(obj);
}

接下来我们有 ENTER_V8_NO_SCRIPT_NO_EXCEPTION:

1
2
3
4
#define ENTER_V8_NO_SCRIPT_NO_EXCEPTION(isolate)                    \
i::VMState<v8::OTHER> __state__((isolate)); \
i::DisallowJavascriptExecutionDebugOnly __no_script__((isolate)); \
i::DisallowExceptions __no_exceptions__((isolate))

因此,随着宏的扩展,我们有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Local<v8::Object> v8::Object::New(Isolate* isolate) {
i::Isolate* i_isolate = reinterpret_cast<i::Isolate*>(isolate);
i::RuntimeCallTimerScope _runtime_timer( isolate, i::RuntimeCallCounterId::kAPI_Object_New);
v8::internal::Logger* logger = isolate->logger();
if (logger->is_logging()) logger->ApiEntryCall("v8::Object::New");

i::VMState<v8::OTHER> __state__(i_isolate));
i::DisallowJavascriptExecutionDebugOnly __no_script__(i_isolate);
i::DisallowExceptions __no_exceptions__(i_isolate));

i::Handle<i::JSObject> obj =
i_isolate->factory()->NewJSObject(i_isolate->object_function());

return Utils::ToLocal(obj);
}

首先,我调用了isolate->object function() 并将结果传递给NewJSObject。 object_function 由名为 NATIVE_CONTEXT_FIELDS
的宏生成:

1
2
3
4
5
6
7
8
#define NATIVE_CONTEXT_FIELD_ACCESSOR(index, type, name)     \
Handle<type> Isolate::name() { \
return Handle<type>(raw_native_context()->name(), this); \
} \
bool Isolate::is_##name(type* value) { \
return raw_native_context()->is_##name(value); \
}
NATIVE_CONTEXT_FIELDS(NATIVE_CONTEXT_FIELD_ACCESSOR)

NATIVE_CONTEXT_FIELDS 是 src/contexts 中的一个宏

1
2
3
#define NATIVE_CONTEXT_FIELDS(V)                                               \
... \
V(OBJECT_FUNCTION_INDEX, JSFunction, object_function) \
1
2
3
4
5
6
7
Handle<type> Isolate::object_function() {
return Handle<JSFunction>(raw_native_context()->object_function(), this);
}

bool Isolate::is_object_function(JSFunction* value) {
return raw_native_context()->is_object_function(value);
}

我不清楚不同类型的上下文,有一个本地上下文,一个“正常/公共”上下文。 在 src/contexts-inl.h 我们有 native_context 函数:

1
2
3
4
5
Context* Context::native_context() const {
Object* result = get(NATIVE_CONTEXT_INDEX);
DCHECK(IsBootstrappingOrNativeContext(this->GetIsolate(), result));
return reinterpret_cast<Context*>(result);
}

Context 扩展了 FixedArray,因此 get 函数是 FixedArray 的 get 函数,而 NATIVE_CONTEXT_INDEX
是存储本机上下文的数组的索引。现在,让我们仔细看看 NewJSObject。
如果在 src/heap/factory.cc 中搜索 NewJSObject:

1
2
3
4
5
Handle<JSObject> Factory::NewJSObject(Handle<JSFunction> constructor, PretenureFlag pretenure) {
JSFunction::EnsureHasInitialMap(constructor);
Handle<Map> map(constructor->initial_map(), isolate());
return NewJSObjectFromMap(map, pretenure);
}

NewJSObjectFromMap

1
2
...
HeapObject* obj = AllocateRawWithAllocationSite(map, pretenure, allocation_site);

所以我们创建了一张新map

Map

HeapObject 包含指向 Map 的指针,或者更确切地说,具有返回指向 Map 的指针的函数。 我在HeapObject
类中看不到任何成员映射。让我们看看何时创建map。

1
(lldb) br s -f map_test.cc -l 63
1
2
3
4
5
6
7
8
9
10
Handle<Map> Factory::NewMap(InstanceType type,
int instance_size,
ElementsKind elements_kind,
int inobject_properties) {
HeapObject* result = isolate()->heap()->AllocateRawWithRetryOrFail(Map::kSize, MAP_SPACE);
result->set_map_after_allocation(*meta_map(), SKIP_WRITE_BARRIER);
return handle(InitializeMap(Map::cast(result), type, instance_size,
elements_kind, inobject_properties),
isolate());
}

我们可以看到上面是在堆实例上调用 AllocateRawWithRetryOrFail,传递一个大小为 88 并指定 MAP_SPACE:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
HeapObject* Heap::AllocateRawWithRetryOrFail(int size, AllocationSpace space,
AllocationAlignment alignment) {
AllocationResult alloc;
HeapObject* result = AllocateRawWithLigthRetry(size, space, alignment);
if (result) return result;

isolate()->counters()->gc_last_resort_from_handles()->Increment();
CollectAllAvailableGarbage(GarbageCollectionReason::kLastResort);
{
AlwaysAllocateScope scope(isolate());
alloc = AllocateRaw(size, space, alignment);
}
if (alloc.To(&result)) {
DCHECK(result != exception());
return result;
}
// TODO(1181417): Fix this.
FatalProcessOutOfMemory("CALL_AND_RETRY_LAST");
return nullptr;
}

对齐的默认值为 kWordAligned。 阅读标题中的文档,它说此函数将尝试在 MAP_SPACE 中执行大小为 88
的分配,如果失败,将执行完整的 GC 并重试分配。 让我们看一下 AllocateRawWithLigthRetry:

1
AllocationResult alloc = AllocateRaw(size, space, alignment);

AllocateRaw 可以在 src/heap/heap-inl.h 中找到。 根据空间参数,将采用不同的路径。 由于在我们的例子中是 MAP_
SPACE,我们将专注于该路径:

1
2
3
4
5
6
7
8
9
10
11
AllocationResult Heap::AllocateRaw(int size_in_bytes, AllocationSpace space, AllocationAlignment alignment) {
...
HeapObject* object = nullptr;
AllocationResult allocation;
if (OLD_SPACE == space) {
...
} else if (MAP_SPACE == space) {
allocation = map_space_->AllocateRawUnaligned(size_in_bytes);
}
...
}

map_space_ 是 Heap 的私有成员(src/heap/heap.h):

1
MapSpace* map_space_;

AllocateRawUnaligned 可以在 src/heap/spaces-inl.h 中找到:

1
2
3
4
5
6
7
8
9
AllocationResult PagedSpace::AllocateRawUnaligned( int size_in_bytes, UpdateSkipList update_skip_list) {
if (!EnsureLinearAllocationArea(size_in_bytes)) {
return AllocationResult::Retry(identity());
}

HeapObject* object = AllocateLinearly(size_in_bytes);
MSAN_ALLOCATED_UNINITIALIZED_MEMORY(object->address(), size_in_bytes);
return object;
}

update_skip_list 的默认值为 UPDATE_SKIP_LIST。 因此,让我们看一下 AllocateLinearly:

1
2
3
4
5
6
HeapObject* PagedSpace::AllocateLinearly(int size_in_bytes) {
Address current_top = allocation_info_.top();
Address new_top = current_top + size_in_bytes;
allocation_info_.set_top(new_top);
return HeapObject::FromAddress(current_top);
}

回想一下,在我们的例子中 size_in_bytes 是 88。

1
2
3
4
5
6
(lldb) expr current_top
(v8::internal::Address) $5 = 24847457492680
(lldb) expr new_top
(v8::internal::Address) $6 = 24847457492768
(lldb) expr new_top - current_top
(unsigned long) $7 = 88

请注意,首先将 top 设置为 new_top ,然后返回 current_top ,这将是指向内存中对象开始的指针(在本例中是
v8::internal::Map ,它也是 HeapObject 类型 )。 我一直想知道为什么 Map(和其他 HeapObject)没有任何成员字段,并且只有/
主要是组成对象的各种字段的 getter/setter。 答案是指向例如 Map 实例的指针指向实例的第一个内存位置。 getter/setter
函数使用索引来读取/写入内存位置。 索引大多以枚举字段的形式定义类型的内存布局。
接下来,在 AllocateRawUnaligned 中,我们有 MSAN_ALLOCATED_UNINITIALIZED_MEMORY 宏:

1
MSAN_ALLOCATED_UNINITIALIZED_MEMORY(object->address(), size_in_bytes);

MSAN_ALLOCATED_UNINITIALIZED_MEMORY 可以在 src/msan.h 中找到,ms 代表 Memory Sanitizer,只有在定义了 V8_US_MEMORY_
SANITIZER 时才会使用。 返回的对象将用于在返回时构造一个 AllocationResult。 回到 AllocateRaw 我们有:

1
2
3
4
5
6
if (allocation.To(&object)) {
...
OnAllocationEvent(object, size_in_bytes);
}

return allocation;

这将在 AllocateRawWithLightRetry 中返回我们:

1
2
HeapObject* result = AllocateRawWithLigthRetry(size, space, alignment);
if (result) return result;

并且该返回将返回到 src/heap/factory.cc 中的 NewMap:

1
2
3
4
result->set_map_after_allocation(*meta_map(), SKIP_WRITE_BARRIER);
return handle(InitializeMap(Map::cast(result), type, instance_size,
elements_kind, inobject_properties),
isolate());

InitializeMap:

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
map->set_instance_type(type);
map->set_prototype(*null_value(), SKIP_WRITE_BARRIER);
map->set_constructor_or_backpointer(*null_value(), SKIP_WRITE_BARRIER);
map->set_instance_size(instance_size);
if (map->IsJSObjectMap()) {
DCHECK(!isolate()->heap()->InReadOnlySpace(map));
map->SetInObjectPropertiesStartInWords(instance_size / kPointerSize - inobject_properties);
DCHECK_EQ(map->GetInObjectProperties(), inobject_properties);
map->set_prototype_validity_cell(*invalid_prototype_validity_cell());
} else {
DCHECK_EQ(inobject_properties, 0);
map->set_inobject_properties_start_or_constructor_function_index(0);
map->set_prototype_validity_cell(Smi::FromInt(Map::kPrototypeChainValid));
}
map->set_dependent_code(DependentCode::cast(*empty_fixed_array()), SKIP_WRITE_BARRIER);
map->set_weak_cell_cache(Smi::kZero);
map->set_raw_transitions(MaybeObject::FromSmi(Smi::kZero));
map->SetInObjectUnusedPropertyFields(inobject_properties);
map->set_instance_descriptors(*empty_descriptor_array());

map->set_visitor_id(Map::GetVisitorId(map));
map->set_bit_field(0);
map->set_bit_field2(Map::IsExtensibleBit::kMask);
int bit_field3 = Map::EnumLengthBits::encode(kInvalidEnumCacheSentinel) |
Map::OwnsDescriptorsBit::encode(true) |
Map::ConstructionCounterBits::encode(Map::kNoSlackTracking);
map->set_bit_field3(bit_field3);
map->set_elements_kind(elements_kind); //HOLEY_ELEMENTS
map->set_new_target_is_base(true);
isolate()->counters()->maps_created()->Increment();
if (FLAG_trace_maps) LOG(isolate(), MapCreate(map));
return map;

Context

上下文扩展了 FixedArray (src/context.h)。 所以这个 Context 的一个实例是一个 FixedArray,我们可以使用 Get(index)
等来获取数组中的条目。

V8_EXPORT

这可以在 v8 源代码的很多地方找到。 例如:

1
class V8_EXPORT ArrayBuffer : public Object {

它是一个预处理器宏,如下所示:

1
2
3
4
5
6
7
8
9
#if V8_HAS_ATTRIBUTE_VISIBILITY && defined(V8_SHARED)
# ifdef BUILDING_V8_SHARED
# define V8_EXPORT __attribute__ ((visibility("default")))
# else
# define V8_EXPORT
# endif
#else
# define V8_EXPORT
#endif

所以我们可以看到,如果 V8_HAS_ATTRIBUTE_VISIBILITY,并且定义(V8_SHARED),并且如果 BUILDING_V8_SHARED,V8_EXPORT
设置为 __attribute__((可见性(“default”))。但在所有其他情况下,V8_EXPORT
为空并且预处理器不插入任何东西(编译时什么都不会出现。但是 attribute ((visibility(“default”)) 这是什么?
在 GNU 编译器集合 (GCC) 环境中,用于导出的术语是可见性。由于它适用于共享对象中的函数和变量,可见性是指其他共享对象调用 C/
C++ 函数的能力。具有默认可见性的函数具有全局范围,可以从其他共享对象中调用。具有隐藏可见性的函数具有本地范围,不能从其他共享
对象调用。
可见性可以通过使用编译器选项或可见性属性来控制。在您的头文件中,无论您希望在当前动态共享对象 (DSO) 之外公开接口或 API
的任何位置,请将 attribute ((visibility (“default”))) 放在您希望公开的结构、类和函数声明中。使用
-fvisibility=hidden,您是在告诉 GCC,每个未明确标记为可见性属性的声明都具有隐藏的可见性。 build/common.gypi
中有这样一个标志

ToLocalChecked()

您将在 hello_world 示例中看到其中一些调用:

1
Local<String> source = String::NewFromUtf8(isolate, js, NewStringType::kNormal).ToLocalChecked();

NewFromUtf8 实际上返回一个包装在 MaybeLocal 中的 Local ,这会在使用它之前强制检查 Local<> 是否为空。 NewStringType
是一个枚举,可以是 kNormalString(k 表示常量)或 kInternalized。
以下是运行预处理器后(clang -E src/api.cc):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 5961 "src/api.cc"
Local<String> String::NewFromUtf8(Isolate* isolate,
const char* data,
NewStringType type,
int length) {
MaybeLocal<String> result;
if (length == 0) {
result = String::Empty(isolate);
} else if (length > i::String::kMaxLength) {
result = MaybeLocal<String>();
} else {
i::Isolate* i_isolate = reinterpret_cast<internal::Isolate*>(isolate);
i::VMState<v8::OTHER> __state__((i_isolate));
i::RuntimeCallTimerScope _runtime_timer( i_isolate, &i::RuntimeCallStats::API_String_NewFromUtf8);
LOG(i_isolate, ApiEntryCall("v8::" "String" "::" "NewFromUtf8"));
if (length < 0) length = StringLength(data);
i::Handle<i::String> handle_result = NewString(i_isolate->factory(), static_cast<v8::NewStringType>(type), i::Vector<const char>(data, length)) .ToHandleChecked();
result = Utils::ToLocal(handle_result);
};
return result.FromMaybe(Local<String>());;
}

Utils::ToLocal定义

1
2
3
4
5
6
MAKE_TO_LOCAL(ToLocal, String, String)

#define MAKE_TO_LOCAL(Name, From, To) \
Local<v8::To> Utils::Name(v8::internal::Handle<v8::internal::From> obj) { \
return Convert<v8::internal::From, v8::To>(obj); \
}

以上内容可以在 src/api.h 中找到。 Local、Local 等也是如此。

Small Integers

Smi 代表小整数。指针实际上只是一个被视为内存地址的整数。 我们可以使用该内存地址来获取位于该内存插槽中的数据的开始。
但我们也可以只在其中存储一个正常的值,比如 18。
在某些情况下,将一个小整数存储在堆中的某个位置并拥有指向它的指针可能没有意义,而是将值直接存储在指针本身中。
但这仅适用于小整数,因此需要知道我们想要的值是否存储在指针中,或者我们是否应该按照存储到堆中的值来获取值。
64 位机器上的一个字是 8 个字节(64 位),所有的指针都需要对齐到 8 的倍数。所以指针可以是:

1
2
3
4
5
1000       = 8
10000 = 16
11000 = 24
100000 = 32
1000000000 = 512

请记住,我们谈论的是指针,而不是存储在它们指向的内存位置的值。 我们可以看到指针中总是有三个位为零。
所以我们可以将它们用于其他用途,并在将它们用作指针时将它们屏蔽掉。
标记涉及借用 32 位中的一位,使其成为 31 位,并让剩余位表示一个标记。 如果标记为零,则这是一个普通值,但如果标记为
1,则必须跟随指针。 这不仅必须用于数字,还可以用于对象(我认为)
相反,小整数由 32 位加上一个指向 64 位数字的指针表示。 V8 需要知道存储在内存中的值是否代表 32 位整数,或者它是否真的是 64
位数字,在这种情况下,它必须跟随指针才能获得完整的值。 这就是标签的概念出现的地方。

Properties/Elements

取以下对象:

1
{ firstname: "Jon", lastname: "Doe' }

上面的对象有两个命名属性。 命名属性与使用数组时所拥有的整数索引不同。
JavaScript 对象的内存布局:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Properties                  JavaScript Object               Elements
+-----------+ +-----------------+ +----------------+
|property1 |<------+ | HiddenClass | +----->| |
+-----------+ | +-----------------+ | +----------------+
|... | +------| Properties | | | element1 |<------+
+-----------+ +-----------------+ | +----------------+ |
|... | | Elements |--+ | ... | |
+-----------+ +-----------------+ +----------------+ |
|propertyN | <---------------------+ | elementN | |
+-----------+ | +----------------+ |
| |
| |
| |
Named properties: { firstname: "Jon", lastname: "Doe' } Indexed Properties: {1: "Jon", 2: "Doe"}

我们可以看到属性和元素存储在不同的数据结构中。 元素通常实现为普通数组,索引可用于快速访问元素。 但对于属性,情况并非如此。
相反,属性名称和属性索引之间存在映射。
在 src/objects/objects.h 我们可以找到 JSObject:

1
2
3
class JSObject: public JSReceiver {
...
DECL_ACCESSORS(elements, FixedArrayBase)

并查看 DECL_ACCESSOR 宏:

1
2
3
4
5
6
7
#define DECL_ACCESSORS(name, type)    \
inline type* name() const; \
inline void set_##name(type* value, \
WriteBarrierMode mode = UPDATE_WRITE_BARRIER);

inline FixedArrayBase* name() const;
inline void set_elements(FixedArrayBase* value, WriteBarrierMode = UPDATE_WRITE_BARRIER)

请注意,JSObject 扩展了 JSReceiver,它被所有可以定义属性的类型扩展。 我认为这包括所有 JSObjects 和 JSProxy。 我们在
JSReceiver 中找到属性数组:

1
DECL_ACCESSORS(raw_properties_or_hash, Object)

现在属性(命名属性而不是元素)在内部可以是不同的类型。 这些工作就像外部的简单字典一样,但字典仅在运行时用于某些特定情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Properties                  JSObject                    HiddenClass (Map)
+-----------+ +-----------------+ +----------------+
|property1 |<------+ | HiddenClass |-------->| bit field1 |
+-----------+ | +-----------------+ +----------------+
|... | +------| Properties | | bit field2 |
+-----------+ +-----------------+ +----------------+
|... | | Elements | | bit field3 |
+-----------+ +-----------------+ +----------------+
|propertyN | | property1 |
+-----------+ +-----------------+
| property2 |
+-----------------+
| ... |
+-----------------+

JSObject

每个 JSObject 都有一个指向生成的 HiddenClass 的指针作为其第一个字段。 隐藏类包含从属性名称到属性数据类型索引的映射。
当创建一个 JSObject 的实例时,会传入一个 Map。如前所述,JSObject 继承自 JSReceiver,而 JSReceiver 继承自 HeapObject
例如,在 jsobject_test.cc 中,我们首先使用内部 Isolate Factory 创建一个新 Map:

1
2
3
v8::internal::Handle<v8::internal::Map> map = factory->NewMap(v8::internal::JS_OBJECT_TYPE, 24);
v8::internal::Handle<v8::internal::JSObject> js_object = factory->NewJSObjectFromMap(map);
EXPECT_TRUE(js_object->HasFastProperties());

当我们调用 js_object->HasFastProperties() 时,这将委托给map实例:

1
return !map()->is_dictionary_map();

Caching

是在动态语言(例如 JavaScript)中优化多态函数调用的方法。

Lookup caches

向接收者发送消息需要运行时使用接收者的运行时类型找到正确的目标方法。 查找缓存将接收方/
消息名称对的类型映射到方法并存储最近使用的查找结果。 首先查询缓存,如果缓存未命中,则执行正常查找并将结果存储在缓存中。

Inline caches

使用如上所述的查找缓存仍然需要相当长的时间,因为必须为每条消息探测缓存。可以观察到,目标的类型通常没有变化。如果对类型 A
的调用是在特定调用站点完成的,那么下一次调用它时,类型很可能也是 A。系统查找例程查找的方法地址可以被缓存,调用指令可以被覆盖
。后续相同类型的调用可以直接跳转到缓存的方法,完全避免查找。被调用方法的序言必须验证接收者类型没有改变,如果改变了就进行查找
(如果类型不正确,例如不再是 A)。
目标方法地址存储在调用者代码中,或与调用者代码“内联”,因此称为“内联缓存”。
如果 V8 能够对将传递给方法的对象类型做出很好的假设,它可以绕过计算如何访问对象属性的过程,
而是使用之前查找隐藏对象的存储信息类。

Polymorfic Inline cache (PIC)

多态呼叫站点是一个有许多同样可能的接收器类型(因此呼叫目标)的站点。
Monomorfic 表示只有一种接收器类型
Polymorfic 表示几种接收器类型,
Megamorfic 非常多的接收器类型
这种类型的缓存扩展了内联缓存,不仅可以缓存最后一次查找,还可以使用专门生成的存根缓存给定多态调用站点的所有查找结果。假设我们
有一个遍历类型列表并调用方法的方法。如果所有类型都相同(单态),那么 PIC
的作用就像一个内联缓存。调用将直接调用目标方法(方法序言后跟方法体)。如果列表中存在不同的类型,则 prolog 中将出现缓存未命中
并调用查找例程。在正常的内联缓存中,这将重新绑定调用,替换对这种类型目标方法的调用。每次类型更改时都会发生这种情况。
使用 PIC,缓存未命中处理程序将生成一个小的存根例程并将调用重新绑定到该存根。存根将检查接收器是否属于它以前见过的类型并分支到
正确的目标。由于此时目标的类型是已知的,因此它可以直接分支到目标方法体,而无需序言。如果在此之前没有看到该类型,它将被添加到
存根以处理该类型。最终,存根将包含所有使用的类型,并且不会再有缓存未命中/查找。
问题是我们没有类型信息,因此不能直接调用方法,而是查找方法。在静态语言中,可能使用了虚拟表。在 JavaScript
中没有继承关系,因此不可能提前知道 vtable 偏移量。可以做的是观察和了解程序中使用的“类型”。当看到一个对象时,可以存储它,并且
可以存储该方法调用的目标并将其内联到该调用中。基本上,将检查类型,如果在直接调用该方法之前已经看到了该特定类型。但是我们如何
在动态语言中检查类型呢?答案是隐藏类,它允许虚拟机根据隐藏类快速检查对象。
内联缓存源位于 src/ic。

–trace-ic

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
$ out/x64.debug/d8 --trace-ic --trace-maps class.js

before
[TraceMaps: Normalize from= 0x19a314288b89 to= 0x19a31428aff9 reason= NormalizeAsPrototype ]
[TraceMaps: ReplaceDescriptors from= 0x19a31428aff9 to= 0x19a31428b051 reason= CopyAsPrototype ]
[TraceMaps: InitialMap map= 0x19a31428afa1 SFI= 34_Person ]

[StoreIC in ~Person+65 at class.js:2 (0->.) map=0x19a31428afa1 0x10e68ba83361 <String[4]: name>]
[TraceMaps: Transition from= 0x19a31428afa1 to= 0x19a31428b0a9 name= name ]
[StoreIC in ~Person+102 at class.js:3 (0->.) map=0x19a31428b0a9 0x2beaa25abd89 <String[3]: age>]
[TraceMaps: Transition from= 0x19a31428b0a9 to= 0x19a31428b101 name= age ]
[TraceMaps: SlowToFast from= 0x19a31428b051 to= 0x19a31428b159 reason= OptimizeAsPrototype ]
[StoreIC in ~Person+65 at class.js:2 (.->1) map=0x19a31428afa1 0x10e68ba83361 <String[4]: name>]
[StoreIC in ~Person+102 at class.js:3 (.->1) map=0x19a31428b0a9 0x2beaa25abd89 <String[3]: age>]
[LoadIC in ~+546 at class.js:9 (0->.) map=0x19a31428b101 0x10e68ba83361 <String[4]: name>]
[CallIC in ~+571 at class.js:9 (0->1) map=0x0 0x32f481082231 <String[5]: print>]
Daniel
[LoadIC in ~+642 at class.js:10 (0->.) map=0x19a31428b101 0x2beaa25abd89 <String[3]: age>]
[CallIC in ~+667 at class.js:10 (0->1) map=0x0 0x32f481082231 <String[5]: print>]
41
[LoadIC in ~+738 at class.js:11 (0->.) map=0x19a31428b101 0x10e68ba83361 <String[4]: name>]
[CallIC in ~+763 at class.js:11 (0->1) map=0x0 0x32f481082231 <String[5]: print>]
Tilda
[LoadIC in ~+834 at class.js:12 (0->.) map=0x19a31428b101 0x2beaa25abd89 <String[3]: age>]
[CallIC in ~+859 at class.js:12 (0->1) map=0x0 0x32f481082231 <String[5]: print>]
2
[CallIC in ~+927 at class.js:13 (0->1) map=0x0 0x32f481082231 <String[5]: print>]
after

LoadIC (0->.) 表示它已从未初始化状态 (0) 转换到单态前状态 (.) 单态状态用 1 指定。这些状态可以在 src/ic/ic.cc 中找到。
我们正在做什么缓存有关 StoreIC/LoadIC 调用中先前看到的对象布局的知识。

1
$ lldb -- out/x64.debug/d8 class.js

HeapObject

此类描述堆分配的对象。 正是在这个类中,我们找到了有关对象类型的信息。 此信息包含在 v8::internal::Map 中。

v8::internal::Map

src/objects/map.h
bit_field1
bit_field2
bit field3 包含有关此 Map 具有的属性数量的信息,一个指向 DescriptorArray 的指针。 DescriptorArray 包含属性名称和值在
JSObject 中存储的位置等信息。 我注意到这些信息在 src/objects/map.h 中可用。

DescriptorArray

可以在 src/objects/descriptor-array.h 中找到。 此类扩展 FixedArray 并具有以下条目:

1
2
3
4
5
6
[0] the number of descriptors it contains  
[1] If uninitialized this will be Smi(0) otherwise an enum cache bridge which is a FixedArray of size 2:
[0] enum cache: FixedArray containing all own enumerable keys
[1] either Smi(0) or a pointer to a FixedArray with indices
[2] first key (and internalized String
[3] first descriptor

Factory

每个内部隔离都有一个用于创建实例的工厂。 这是因为所有句柄都需要使用工厂分配(src/heap/factory.h)

Objects

所有对象都扩展了抽象类 Object (src/objects/objects.h)。

Oddball

此类扩展 HeapObject 并描述空、未定义、真和假对象。

Map

扩展 HeapObject 并且所有堆对象都有一个描述对象结构的 Map。 在这·里您可以找到实例的大小,访问 inobject_properties。

Compiler pipeline

编译脚本时,会解析所有顶层代码。 这些是函数声明(但不是函数体)。

1
2
3
4
5
6
7
8
9
10
11
function f1() {       <- top level code
console.log('f1'); <- non top level
}

function f2() { <- top level code
f1(); <- non top level
console.logg('f2'); <- non top level
}

f2(); <- top level code
var i = 10; <- top level code

必须预先解析非顶级代码以检查语法错误。 顶层代码由 full-codegen 编译器解析和编译。
该编译器不执行任何优化,唯一的任务是尽快生成机器代码(这是之前的turbofan)

1
Source ------> Parser  --------> Full-codegen ---------> Unoptimized Machine Code

所以即使我们只为顶层代码生成代码,整个脚本也会被解析。 预解析(语法检查)没有以任何方式存储。 这些函数是惰性存根,
如果函数被调用时,函数就会被编译。 这意味着必须对函数进行解析(同样,第一次是预解析记住)。
如果确定某个函数很热,它将由两个优化编译器之一优化,用于 JavaScript 的旧部分或 Turbofan for Web Assembly (WASM)
和一些较新的 es6 功能。
V8 第一次看到一个函数时会将其解析为 AST,但在使用该函数之前不会对该树进行任何进一步的处理。

1
2
3
4
5
                     +-----> Full-codegen -----> Unoptimized code
/ \/ /\ \
Parser ------> AST -------> Cranshaft -----> Optimized code |
\ /
+-----> Turbofan -----> Optimized code

内联缓存 (IC) 在这里完成,这也有助于收集类型信息。 V8 也有一个分析器线程,它监控哪些函数是热的并且应该被优化。
这种分析还允许 V8 找出有关使用 IC 的类型的信息。然后可以将该类型信息馈送到Crankshaft/Turbofan。类型信息存储为 8 位值。
当一个函数被优化时,未优化的代码不能被丢弃,因为它可能是需要的,因为 JavaScript 是高度动态的,优化的函数可能会发生变化,在
这种情况下,我们会退回到未优化的代码。这会占用大量内存,这对于低端设备可能很重要。解析(两次)所花费的时间也需要时间。
Ignition 的想法是成为一个字节码解释器并减少内存消耗,与本机代码相比,字节码非常简洁,本机代码可能因目标平台而异。整个源代码
都可以解析和编译,与当前管道相比,具有上述预解析和解析阶段。所以即使是未使用的函数也会被编译。字节码成为事实的来源,而不是像
之前的 AST。

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
Source ------> Parser  --------> Ignition-codegen ---------> Bytecode ---------> Turbofan ----> Optimized Code ---+
/\ |
+--------------------------------------------------+

function bajja(a, b, c) {
var d = c - 100;
return a + d * b;
}

var result = bajja(2, 2, 150);
print(result);

$ ./d8 test.js --ignition --print_bytecode

[generating bytecode for function: bajja]
Parameter count 4
Frame size 8
14 E> 0x2eef8d9b103e @ 0 : 7f StackCheck
38 S> 0x2eef8d9b103f @ 1 : 03 64 LdaSmi [100] // 加载 100
38 E> 0x2eef8d9b1041 @ 3 : 2b 02 02 Sub a2, [2] // a2 是第三个参数. a2 是一个参数寄存器
0x2eef8d9b1044 @ 6 : 1f fa Star r0 // r0 是局部变量的寄存器。 这里只有d是局部变量
47 S> 0x2eef8d9b1046 @ 8 : 1e 03 Ldar a1 // 从寄存器参数 a1 加载累加器,它是 b
60 E> 0x2eef8d9b1048 @ 10 : 2c fa 03 Mul r0, [3] // 乘以我们在 r0 中的局部变量
56 E> 0x2eef8d9b104b @ 13 : 2a 04 04 Add a0, [4] // 将其添加到我们的参数寄存器 这是a
65 S> 0x2eef8d9b104e @ 16 : 83 Return // 返回累加器中的值?

Abstract Syntax Tree (AST)

在 src/ast/ast.h 中。 您可以使用 d8 的 –print-ast 选项打印 ast。
让我们使用以下 javascript 并查看 ast:

1
2
const msg = 'testing';
console.log(msg);
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
$ d8 --print-ast simple.js
[generating interpreter code for user-defined function: ]
--- AST ---
FUNC at 0
. KIND 0
. SUSPEND COUNT 0
. NAME ""
. INFERRED NAME ""
. DECLS
. . VARIABLE (0x7ffe5285b0f8) (mode = CONST) "msg"
. BLOCK NOCOMPLETIONS at -1
. . EXPRESSION STATEMENT at 12
. . . INIT at 12
. . . . VAR PROXY context[4] (0x7ffe5285b0f8) (mode = CONST) "msg"
. . . . LITERAL "testing"
. EXPRESSION STATEMENT at 23
. . ASSIGN at -1
. . . VAR PROXY local[0] (0x7ffe5285b330) (mode = TEMPORARY) ".result"
. . . CALL Slot(0)
. . . . PROPERTY Slot(4) at 31
. . . . . VAR PROXY Slot(2) unallocated (0x7ffe5285b3d8) (mode = DYNAMIC_GLOBAL) "console"
. . . . . NAME log
. . . . VAR PROXY context[4] (0x7ffe5285b0f8) (mode = CONST) "msg"
. RETURN at -1
. . VAR PROXY local[0] (0x7ffe5285b330) (mode = TEMPORARY) ".result"

可以在 ast.h 中找到 EXPRESSION 的声明。

Bytecode

可以在 src/interpreter/bytecodes.h 中找到
StackCheck 检查堆栈限制是否未超出以防止溢出。
Star 将累加器寄存器中的内容存储在寄存器(操作数)中。
来自寄存器参数 a1 的 Ldar Load 累加器,它是 b
寄存器不是机器寄存器,除了我理解的累加器,而是堆栈分配。

Parsing

解析是对 JavaScript 的解析和抽象语法树的生成。 然后访问该树并从中生成字节码。 本节试图找出这些操作在代码中的何处执行。
例如,以脚本为例。

1
2
3
4
$ make run-script
$ lldb -- run-script
(lldb) br s -n main
(lldb) r

让我们看一下以下行:

1
Local<Script> script = Script::Compile(context, source).ToLocalChecked();

这将使我们进入 api.cc

1
2
3
4
5
6
7
ScriptCompiler::Source script_source(source);
return ScriptCompiler::Compile(context, &script_source);

MaybeLocal<Script> ScriptCompiler::Compile(Local<Context> context, Source* source, CompileOptions options) {
...
auto isolate = context->GetIsolate();
auto maybe = CompileUnboundInternal(isolate, source, options);

CompileUnboundInternal 将调用 GetSharedFunctionInfoForScript(在 src/compiler.cc 中):

1
2
3
4
5
6
7
8
9
10
result = i::Compiler::GetSharedFunctionInfoForScript(
str, name_obj, line_offset, column_offset, source->resource_options,
source_map_url, isolate->native_context(), NULL, &script_data, options,
i::NOT_NATIVES_CODE);

(lldb) br s -f compiler.cc -l 1259

LanguageMode language_mode = construct_language_mode(FLAG_use_strict);
(lldb) p language_mode
(v8::internal::LanguageMode) $10 = SLOPPY

LanguageMode 可以在 src/globals.h 中找到,它是一个具有三个值的枚举:

1
enum LanguageMode : uint32_t { SLOPPY, STRICT, LANGUAGE_END };

我认为 SLOPPY 模式是没有“use strict”时的模式;。 请记住,这可以进入函数内部,而不必位于文件的顶层。

1
ParseInfo parse_info(script);

有一个单元测试显示如何创建和检查 ParseInfo 实例。
这将调用 ParseInfo 的构造函数(在 src/parsing/parse-info.cc 中),并将调用 ParseInfo::InitFromIsolate:

1
2
3
4
5
6
DCHECK_NOT_NULL(isolate);
set_hash_seed(isolate->heap()->HashSeed());
set_stack_limit(isolate->stack_guard()->real_climit());
set_unicode_cache(isolate->unicode_cache());
set_runtime_call_stats(isolate->counters()->runtime_call_stats());
set_ast_string_constants(isolate->ast_string_constants());

我对这些 ast_string_constants 很好奇:

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
(lldb) p *ast_string_constants_
(const v8::internal::AstStringConstants) $58 = {
zone_ = {
allocation_size_ = 1312
segment_bytes_allocated_ = 8192
position_ = 0x0000000105052538 <no value available>
limit_ = 0x0000000105054000 <no value available>
allocator_ = 0x0000000103e00080
segment_head_ = 0x0000000105052000
name_ = 0x0000000101623a70 "../../src/ast/ast-value-factory.h:365"
sealed_ = false
}
string_table_ = {
v8::base::TemplateHashMapImpl<void *, void *, v8::base::HashEqualityThenKeyMatcher<void *, bool (*)(void *, void *)>, v8::base::DefaultAllocationPolicy> = {
map_ = 0x0000000105054000
capacity_ = 64
occupancy_ = 41
match_ = {
match_ = 0x000000010014b260 (libv8.dylib`v8::internal::AstRawString::Compare(void*, void*) at ast-value-factory.cc:122)
}
}
}
hash_seed_ = 500815076
anonymous_function_string_ = 0x0000000105052018
arguments_string_ = 0x0000000105052038
async_string_ = 0x0000000105052058
await_string_ = 0x0000000105052078
boolean_string_ = 0x0000000105052098
constructor_string_ = 0x00000001050520b8
default_string_ = 0x00000001050520d8
done_string_ = 0x00000001050520f8
dot_string_ = 0x0000000105052118
dot_for_string_ = 0x0000000105052138
dot_generator_object_string_ = 0x0000000105052158
dot_iterator_string_ = 0x0000000105052178
dot_result_string_ = 0x0000000105052198
dot_switch_tag_string_ = 0x00000001050521b8
dot_catch_string_ = 0x00000001050521d8
empty_string_ = 0x00000001050521f8
eval_string_ = 0x0000000105052218
function_string_ = 0x0000000105052238
get_space_string_ = 0x0000000105052258
length_string_ = 0x0000000105052278
let_string_ = 0x0000000105052298
name_string_ = 0x00000001050522b8
native_string_ = 0x00000001050522d8
new_target_string_ = 0x00000001050522f8
next_string_ = 0x0000000105052318
number_string_ = 0x0000000105052338
object_string_ = 0x0000000105052358
proto_string_ = 0x0000000105052378
prototype_string_ = 0x0000000105052398
return_string_ = 0x00000001050523b8
set_space_string_ = 0x00000001050523d8
star_default_star_string_ = 0x00000001050523f8
string_string_ = 0x0000000105052418
symbol_string_ = 0x0000000105052438
this_string_ = 0x0000000105052458
this_function_string_ = 0x0000000105052478
throw_string_ = 0x0000000105052498
undefined_string_ = 0x00000001050524b8
use_asm_string_ = 0x00000001050524d8
use_strict_string_ = 0x00000001050524f8
value_string_ = 0x0000000105052518
}

因此,这些是使用隔离中的值在新 ParseInfo 实例上设置的常量。 不完全确定我想要什么,但我可能稍后会回来。 所以,我们回到 ParseInfo 的构造函数:

1
2
3
set_allow_lazy_parsing();
set_toplevel();
set_script(script);

脚本是 v8::internal::Script 类型,可以在 src/object/script.h 中找到
现在回到 compiler.cc 和 GetSharedFunctionInfoForScript 函数:

1
2
3
Zone compile_zone(isolate->allocator(), ZONE_NAME);
...
if (parse_info->literal() == nullptr && !parsing::ParseProgram(parse_info, isolate))

ParseProgram:

1
2
3
4
Parser parser(info);
...
FunctionLiteral* result = nullptr;
result = parser.ParseProgram(isolate, info);

parser.ParseProgram:

1
2
3
4
5
Handle<String> source(String::cast(info->script()->source()));


(lldb) job *source
"var user1 = new Person('Fletch');\x0avar user2 = new Person('Dr.Rosen');\x0aprint("user1 = " + user1.name);\x0aprint("user2 = " + user2.name);\x0a\x0a"

所以在这里我们可以将我们的 JavaScript 看作一个字符串。

1
2
3
std::unique_ptr<Utf16CharacterStream> stream(ScannerStream::For(source));
scanner_.Initialize(stream.get(), info->is_module());
result = DoParseProgram(info);

DoParseProgram:

1
2
3
4
5
(lldb) br s -f parser.cc -l 639
...

this->scope()->SetLanguageMode(info->language_mode());
ParseStatementList(body, Token::EOS, &ok);

此调用将落在 parser-base.h 及其 ParseStatementList 函数中。

1
2
3
4
5
(lldb) br s -f parser-base.h -l 4695

StatementT stat = ParseStatementListItem(CHECK_OK_CUSTOM(Return, kLazyParsingComplete));

result = CompileToplevel(&parse_info, isolate, Handle<SharedFunctionInfo>::null());

这将出现在 CompileTopelevel 中(在同一个文件 src/compiler.cc 中):

1
2
// Compile the code.
result = CompileUnoptimizedCode(parse_info, shared_info, isolate);

这将出现在 CompileUnoptimizedCode 中(在同一文件中,即 src/compiler.cc):

1
2
3
4
5
6
7
8
9
10
11
12
// 准备并执行最外层函数的编译。
std::unique_ptr<CompilationJob> outer_job(
PrepareAndExecuteUnoptimizedCompileJob(parse_info, parse_info->literal(),
shared_info, isolate));


std::unique_ptr<CompilationJob> job(
interpreter::Interpreter::NewCompilationJob(parse_info, literal, isolate));
if (job->PrepareJob() == CompilationJob::SUCCEEDED &&
job->ExecuteJob() == CompilationJob::SUCCEEDED) {
return job;
}

PrepareJobImpl:

1
2
3
CodeGenerator::MakeCodePrologue(parse_info(), compilation_info(),
"interpreter");
return SUCCEEDED;

codegen.cc MakeCodePrologue:
interpreter.cc ExecuteJobImpl:

1
generator()->GenerateBytecode(stack_limit());

src/interpreter/bytecode-generator.cc

1
RegisterAllocationScope register_scope(this);

字节码是基于寄存器的(如果这是正确的术语),我们之前有一个例子。 我猜这就是这个call的目的。
VisitDeclarations 将遍历文件中的所有声明,在我们的例子中是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var user1 = new Person('Fletch');
var user2 = new Person('Dr.Rosen');

(lldb) p *variable->raw_name()
(const v8::internal::AstRawString) $33 = {
= {
next_ = 0x000000010600a280
string_ = 0x000000010600a280
}
literal_bytes_ = (start_ = "user1", length_ = 5)
hash_field_ = 1303438034
is_one_byte_ = true
has_string_ = false
}

// 在正文之前执行堆栈检查。
builder()->StackCheck(info()->literal()->start_position());

因此该调用将输出一个堆栈检查指令,如上例所示:

1
14 E> 0x2eef8d9b103e @    0 : 7f                StackCheck

Performance

假设您有完整代码生成编译器可能产生的表达式 x + y:

1
2
3
movq rax, x
movq rbx, y
callq RuntimeAdd

如果 x 和 y 是整数,则使用加法运算会快得多:

1
2
3
movq rax, x
movq rbx, y
add rax, rbx

回想一下,函数是经过优化的,所以如果编译器必须退出并取消优化函数的一部分,那么整个函数都会受到影响,它会回到未优化的版本。

Bytecode

本节将研究以下 JavaScript 的字节码:

1
2
3
4
5
6
7
8
9
10
11
12
13
function beve() {
const p = new Promise((resolve, reject) => {
resolve('ok');
});

p.then(msg => {
console.log(msg);
});
}

beve();

$ d8 --print-bytecode promise.js

首先是没有名称的main:

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
[generating bytecode for function: ]
(The code that generated this can be found in src/objects.cc BytecodeArray::Dissassemble)
Parameter count 1
Frame size 32
// 将常量池中的 FixedArray[4] 加载到累加器中。
0x34423e7ac19e @ 0 : 09 00 LdaConstant [0]
// 将 FixedArray[4] 存储在寄存器 r1 中
0x34423e7ac1a0 @ 2 : 1e f9 Star r1
// 将零存储到累加器中。
0x34423e7ac1a2 @ 4 : 02 LdaZero
// 将零(累加器的内容)存储到寄存器 r2 中。
0x34423e7ac1a3 @ 5 : 1e f8 Star r2
//
0x34423e7ac1a5 @ 7 : 1f fe f7 Mov <closure>, r3
0x34423e7ac1a8 @ 10 : 53 96 01 f9 03 CallRuntime [DeclareGlobalsForInterpreter], r1-r3
0 E> 0x34423e7ac1ad @ 15 : 90 StackCheck
141 S> 0x34423e7ac1ae @ 16 : 0a 01 00 LdaGlobal [1], [0]
0x34423e7ac1b1 @ 19 : 1e f9 Star r1
141 E> 0x34423e7ac1b3 @ 21 : 4f f9 03 CallUndefinedReceiver0 r1, [3]
0x34423e7ac1b6 @ 24 : 1e fa Star r0
148 S> 0x34423e7ac1b8 @ 26 : 94 Return

Constant pool (size = 2)
0x34423e7ac149: [FixedArray] in OldSpace
- map = 0x344252182309 <Map(HOLEY_ELEMENTS)>
- length: 2
0: 0x34423e7ac069 <FixedArray[4]>
1: 0x34423e7abf59 <String[4]: beve>

Handler Table (size = 16) 将常量池条目 <name_index> 中带有 name 的全局加载
// 在 typeof 之外使用 FeedBackVector 插槽 <slot> 的累加器

LdaConstant 将常量池中的索引处的常量加载到累加器中。
Star 将累加器寄存器的内容存储在 dst 中。
Ldar 使用来自寄存器 src 的值加载累加器。
LdaGlobal 使用 typeof 外的 FeedBackVector 槽将常量池条目 idx 中的全局名称加载到累加器中。
mov , 存放寄存器的值
这些指令的声明在 src/interpreter/interpreter-generator.cc。

Unified code generation architecture

FeedbackVector

附属于每一个功能,负责记录和管理所有的执行反馈,这是关于类型启用的信息。 您可以在 src/feedback-vector.h 中找到此类的声明

BytecodeGenerator

目前是 V8 中唯一关于 AST 的部分。

BytecodeGraphBuilder

基于解释器字节码生成高级 IR 图。

TurboFan

是一个编译器后端,它接收控制流图,然后进行指令选择、寄存器分配和代码生成。 代码生成生成

Execution/Runtime

我不确定 V8 是否完全遵循这一点,但我听说并读到当引擎遇到函数声明时,它只会解析和验证语法并将 ref 保存到函数名。
在这个阶段不检查函数内的语句,只检查函数声明的语法(括号、参数、括号等)。

Function methods

Function 的声明可以在 include/v8.h 中找到(只是注意到这一点,因为我已经找了好几次了)

Symbol

Symbol 类的声明可以在 v8.h 中找到,内部实现可以在 src/api/api.cc 中找到。
众所周知的符号是使用宏生成的,因此您可以使用诸如“GetToPrimitive”之类的静态函数名称进行搜索来找到它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#define WELL_KNOWN_SYMBOLS(V)                 \
V(AsyncIterator, async_iterator) \
V(HasInstance, has_instance) \
V(IsConcatSpreadable, is_concat_spreadable) \
V(Iterator, iterator) \
V(Match, match) \
V(Replace, replace) \
V(Search, search) \
V(Split, split) \
V(ToPrimitive, to_primitive) \
V(ToStringTag, to_string_tag) \
V(Unscopables, unscopables)

#define SYMBOL_GETTER(Name, name) \
Local<Symbol> v8::Symbol::Get##Name(Isolate* isolate) { \
i::Isolate* i_isolate = reinterpret_cast<i::Isolate*>(isolate); \
return Utils::ToLocal(i_isolate->factory()->name##_symbol()); \
}

所以 GetToPrimitive 会变成:

1
2
3
4
Local<Symbol> v8::Symbol::GeToPrimitive(Isolate* isolate) {
i::Isolate* i_isolate = reinterpret_cast<i::Isolate*>(isolate);
return Utils::ToLocal(i_isolate->factory()->to_primitive_symbol());
}

Builtins

是 V8 提供的 JavaScript 函数/对象。 这些是使用 C++ DSL 构建的,并通过:

1
CodeStubAssembler -> CodeAssembler -> RawMachineAssembler.

内置程序需要为它们生成字节码,以便它们可以在 TurboFan 中运行。
src/code-stub-assembler.h
所有内置函数都由 BUILTIN_LIST_BASE 宏在 src/builtins/builtins-definitions.h 中声明。 有不同类型的内置函数(TF = Turbo Fan):
TFJ JavaScript 链接,这意味着它可以作为 JavaScript 函数调用
TFS CodeStub 链接。 带有存根链接的内置函数可用于将公共代码提取到单独的代码对象中,然后多个调用者可以使用该代码对象。
这些很有用,因为内置函数在编译时生成并包含在 V8 快照中。 这意味着它们是创建的每个隔离的一部分。 能够为多个内置函数共享通用代码将节省空间。
TFC CodeStub 与自定义描述符的链接
要了解它是如何工作的,我们首先需要禁用快照。 如果不这样做,我们将无法设置断点,因为堆将在编译时序列化并在 v8 启动时反序列化。
要找到禁用快照的选项,请使用:

1
2
3
4
5
6
$ gn args --list out.gn/learning --short | more
...
v8_use_snapshot=true
$ gn args out.gn/learning
v8_use_snapshot=false
$ gn -C out.gn/learning

构建完成后,我们应该能够在 bootstrapper.cc 及其函数 Genesis::InitializeGlobal 中设置断点:

1
(lldb) br s -f bootstrapper.cc -l 2684

让我们看看 JSON 对象是如何设置的:

1
2
Handle<String> name = factory->InternalizeUtf8String("JSON");
Handle<JSObject> json_object = factory->NewJSObject(isolate->object_function(), TENURED);

TENURED 意味着这个对象应该直接在old generation分配。

1
JSObject::AddProperty(global, name, json_object, DONT_ENUM);

DONT_ENUM 由一些内置函数检查,如果设置此对象将被这些函数忽略。

1
SimpleInstallFunction(json_object, "parse", Builtins::kJsonParse, 2, false);

在这里我们可以看到我们正在安装一个名为 parse 的函数,它有 2 个参数。 定义在 src/builtins/builtins-json.cc。
SimpleInstallFunction 有什么作用?
让我们以控制台为例,它是使用以下方法创建的:

1
2
3
4
5
6
7
8
9
10
11
12
13
Handle<JSObject> console = factory->NewJSObject(cons, TENURED);
JSObject::AddProperty(global, name, console, DONT_ENUM);
SimpleInstallFunction(console, "debug", Builtins::kConsoleDebug, 1, false,
NONE);

V8_NOINLINE Handle<JSFunction> SimpleInstallFunction(
Handle<JSObject> base,
const char* name,
Builtins::Name call,
int len,
bool adapt,
PropertyAttributes attrs = DONT_ENUM,
BuiltinFunctionId id = kInvalidBuiltinFunctionId) {

所以我们可以看到 base 是我们对 JSObject 的句柄,名称是“debug”。 Builtins::Name 是 Builtins:kConsoleDebug。 这是在哪里定义的?
你可以在 src/builtins/builtins-definitions.h 中找到一个名为 CPP 的宏:
CPP(控制台调试)
这个宏扩展成什么?
它是 builtin-definitions.h 中 BUILTIN_LIST_BASE 宏的一部分 我们必须查看 BUILTIN_LIST 的使用位置,我们可以在 builtins.cc 中找到它。
在builtins.cc 中,我们有一个BuiltinMetadata 数组,声明为:

1
2
3
4
5
6
const BuiltinMetadata builtin_metadata[] = {
BUILTIN_LIST(DECL_CPP, DECL_API, DECL_TFJ, DECL_TFC, DECL_TFS, DECL_TFH, DECL_ASM)
};

#define DECL_CPP(Name, ...) { #Name, Builtins::CPP, \
{ FUNCTION_ADDR(Builtin_##Name) }},

这将扩展到在数组中创建 BuiltinMetadata 结构条目。 BuildintMetadata 结构看起来像这样,这可能有助于理解发生了什么:

1
2
3
4
5
6
7
8
struct BuiltinMetadata {
const char* name;
Builtins::Kind kind;
union {
Address cpp_entry; // For CPP and API builtins.
int8_t parameter_count; // For TFJ builtins.
} kind_specific_data;
};

因此 CPP(ConsoleDebug) 将扩展为数组中的一个条目,如下所示:

1
2
3
4
5
6
{ ConsoleDebug, 
Builtins::CPP,
{
reinterpret_cast<v8::internal::Address>(reinterpret_cast<intptr_t>(Builtin_ConsoleDebug))
}
},

第三个参数是联合上的创建,这可能并不明显。
回到我试图回答的问题是:
“Buildtins::Name 是 Builtins:kConsoleDebug。这是在哪里定义的?”
为此,我们必须查看 builtins.h 和枚举名称:

1
2
3
4
5
6
enum Name : int32_t {
#define DEF_ENUM(Name, ...) k##Name,
BUILTIN_LIST_ALL(DEF_ENUM)
#undef DEF_ENUM
builtin_count
};

这将使用 DEF_ENUM 宏扩展为 builtin-definitions.h 中内置函数的完整列表。 所以 ConsoleDebug 的扩展看起来像:

1
2
3
4
5
enum Name: int32_t {
...
kDebugConsole,
...
};

因此,备份查看 SimpleInstallFunction 的参数,它们是:

1
2
3
4
5
6
7
8
9
10
11
SimpleInstallFunction(console, "debug", Builtins::kConsoleDebug, 1, false,
NONE);

V8_NOINLINE Handle<JSFunction> SimpleInstallFunction(
Handle<JSObject> base,
const char* name,
Builtins::Name call,
int len,
bool adapt,
PropertyAttributes attrs = DONT_ENUM,
BuiltinFunctionId id = kInvalidBuiltinFunctionId) {

我们知道 Builtins::Name,所以让我们看看 len 是一个,这是什么?
SimpleInstallFunction 将调用:

1
2
Handle<JSFunction> fun =
SimpleCreateFunction(base->GetIsolate(), function_name, call, len, adapt);

如果 adapt 为真,则使用 len,但在我们的例子中为假。 如果 adapt 为真,这就是它的用途:

1
fun->shared()->set_internal_formal_parameter_count(len);

我不确定这里指的是什么适应。
未指定 PropertyAttributes,因此它将获得 DONT_ENUM 的默认值。 最后一个类型为 BuiltinFunctionId 的参数也未指定,因此将使用
kInvalidBuiltinFunctionId 的默认值。 这是在 src/objects/objects.h 中定义的枚举。
此博客(https://v8project.blogspot.se/2017/11/csa.html) 提供了一个向 String 对象添加函数的示例。

1
$ out.gn/learning/mksnapshot --print-code > output

然后,您可以从中查看生成的代码。 这将生成一个可以通过 C++ 调用的代码存根。 让我们更新它以从 JavaScript 调用它:
更新 builtins/builtins-string-get.cc :

1
2
3
4
TF_BUILTIN(GetStringLength, StringBuiltinsAssembler) {
Node* const str = Parameter(Descriptor::kReceiver);
Return(LoadStringLength(str));
}

我们还必须更新 builtins/builtins-definitions.h:

1
TFJ(GetStringLength, 0)

和 bootstrapper.cc:

1
SimpleInstallFunction(prototype, "len", Builtins::kGetStringLength, 0, true);

如果您现在使用 ‘ninja -C out.gn/learning_v8’ 构建,您应该能够运行 d8 并尝试一下:

1
2
3
4
d8> const s = 'testing'
undefined
d8> s.len()
7

现在让我们仔细看看为此生成的代码:

1
$ out.gn/learning/mksnapshot --print-code > output

查看生成的输出,我惊讶地看到 GetStringLength 的两个条目(我更改名称只是为了确保没有其他东西生成第二个条目)。 为什么是两个?
以下使用英特尔汇编语法,这意味着没有寄存器/立即数前缀,第一个操作数是目标,第二个操作数是源。

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
--- Code ---
kind = BUILTIN
name = BeveStringLength
compiler = turbofan
Instructions (size = 136)
0x1fafde09b3a0 0 55 push rbp
0x1fafde09b3a1 1 4889e5 REX.W movq rbp,rsp // movq rsp into rbp

0x1fafde09b3a4 4 56 push rsi // push the value of rsi (first parameter) onto the stack
0x1fafde09b3a5 5 57 push rdi // push the value of rdi (second parameter) onto the stack
0x1fafde09b3a6 6 50 push rax // push the value of rax (accumulator) onto the stack

0x1fafde09b3a7 7 4883ec08 REX.W subq rsp,0x8 // make room for a 8 byte value on the stack
0x1fafde09b3ab b 488b4510 REX.W movq rax,[rbp+0x10] // move the value rpm + 10 to rax
0x1fafde09b3af f 488b58ff REX.W movq rbx,[rax-0x1]
0x1fafde09b3b3 13 807b0b80 cmpb [rbx+0xb],0x80 // IsString(object). compare byte to zero
0x1fafde09b3b7 17 0f8350000000 jnc 0x1fafde09b40d <+0x6d> // jump it carry flag was not set

0x1fafde09b3bd 1d 488b400f REX.W movq rax,[rax+0xf]
0x1fafde09b3c1 21 4989e2 REX.W movq r10,rsp
0x1fafde09b3c4 24 4883ec08 REX.W subq rsp,0x8
0x1fafde09b3c8 28 4883e4f0 REX.W andq rsp,0xf0
0x1fafde09b3cc 2c 4c891424 REX.W movq [rsp],r10
0x1fafde09b3d0 30 488945e0 REX.W movq [rbp-0x20],rax
0x1fafde09b3d4 34 48be0000000001000000 REX.W movq rsi,0x100000000
0x1fafde09b3de 3e 48bad9c228dfa8090000 REX.W movq rdx,0x9a8df28c2d9 ;; object: 0x9a8df28c2d9 <String[101]: CAST(LoadObjectField(object, offset, MachineTypeOf<T>::value)) at ../../src/code-stub-assembler.h:432>
0x1fafde09b3e8 48 488bf8 REX.W movq rdi,rax
0x1fafde09b3eb 4b 48b830726d0a01000000 REX.W movq rax,0x10a6d7230 ;; external reference (check_object_type)
0x1fafde09b3f5 55 40f6c40f testb rsp,0xf
0x1fafde09b3f9 59 7401 jz 0x1fafde09b3fc <+0x5c>
0x1fafde09b3fb 5b cc int3l
0x1fafde09b3fc 5c ffd0 call rax
0x1fafde09b3fe 5e 488b2424 REX.W movq rsp,[rsp]
0x1fafde09b402 62 488b45e0 REX.W movq rax,[rbp-0x20]
0x1fafde09b406 66 488be5 REX.W movq rsp,rbp
0x1fafde09b409 69 5d pop rbp
0x1fafde09b40a 6a c20800 ret 0x8

// this is where we jump to if IsString failed
0x1fafde09b40d 6d 48ba71c228dfa8090000 REX.W movq rdx,0x9a8df28c271 ;; object: 0x9a8df28c271 <String[76]\: CSA_ASSERT failed: IsString(object) [../../src/code-stub-assembler.cc:1498]\n>
0x1fafde09b417 77 e8e4d1feff call 0x1fafde088600 ;; code: BUILTIN
0x1fafde09b41c 7c cc int3l
0x1fafde09b41d 7d cc int3l
0x1fafde09b41e 7e 90 nop
0x1fafde09b41f 7f 90 nop


Safepoints (size = 8)

RelocInfo (size = 7)
0x1fafde09b3e0 embedded object (0x9a8df28c2d9 <String[101]: CAST(LoadObjectField(object, offset, MachineTypeOf<T>::value)) at ../../src/code-stub-assembler.h:432>)
0x1fafde09b3ed external reference (check_object_type) (0x10a6d7230)
0x1fafde09b40f embedded object (0x9a8df28c271 <String[76]\: CSA_ASSERT failed: IsString(object) [../../src/code-stub-assembler.cc:1498]\n>)
0x1fafde09b418 code target (BUILTIN) (0x1fafde088600)

--- End code ---

TF_BUILTIN macro

是定义 Turbofan (TF) 内置函数的宏,可以在 builtins/builtins-utils-gen.h 中找到
如果我们看一下文件 src/builtins/builtins-bigint-gen.cc 和以下函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
TF_BUILTIN(BigIntToI64, CodeStubAssembler) {                                       
if (!Is64()) {
Unreachable();
return;
}

TNode<Object> value = CAST(Parameter(Descriptor::kArgument));
TNode<Context> context = CAST(Parameter(Descriptor::kContext));
TNode<BigInt> n = ToBigInt(context, value);

TVARIABLE(UintPtrT, var_low);
TVARIABLE(UintPtrT, var_high);

BigIntToRawBytes(n, &var_low, &var_high);
Return(var_low.value());
}

让我们以上面的 GetStringLength 示例为例,看看处理此宏后将扩展为什么:

1
$ clang++ --sysroot=build/linux/debian_sid_amd64-sysroot -isystem=./buildtools/third_party/libc++/trunk/include -isystem=buildtools/third_party/libc++/trunk/include -I. -E src/builtins/builtins-bigint-gen.cc > builtins-bigint-gen.cc.pp
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
static void Generate_BigIntToI64(compiler::CodeAssemblerState* state);

class BigIntToI64Assembler : public CodeStubAssembler {
public:
using Descriptor = Builtin_BigIntToI64_InterfaceDescriptor;
explicit BigIntToI64Assembler(compiler::CodeAssemblerState* state) : CodeStubAssembler(state) {}
void GenerateBigIntToI64Impl();
Node* Parameter(Descriptor::ParameterIndices index) {
return CodeAssembler::Parameter(static_cast<int>(index));
}
};

void Builtins::Generate_BigIntToI64(compiler::CodeAssemblerState* state) {
BigIntToI64Assembler assembler(state);
state->SetInitialDebugInformation("BigIntToI64", "src/builtins/builtins-bigint-gen.cc", 14);
if (Builtins::KindOf(Builtins::kBigIntToI64) == Builtins::TFJ) {
assembler.PerformStackCheck(assembler.GetJSContextParameter());
}
assembler.GenerateBigIntToI64Impl();
}

void BigIntToI64Assembler::GenerateBigIntToI64Impl() {
if (!Is64()) {
Unreachable();
return;
}

TNode<Object> value = Cast(Parameter(Descriptor::kArgument));
TNode<Context> context = Cast(Parameter(Descriptor::kContext));
TNode<BigInt> n = ToBigInt(context, value);

TVariable<UintPtrT> var_low(this);
TVariable<UintPtrT> var_high(this);

BigIntToRawBytes(n, &var_low, &var_high);
Return(var_low.value());
}

从生成的类中,您可以看到如何在 TF_BUILTIN 宏中使用 Parameter。

Building V8

您需要将 Google V8 源代码签出到您的本地文件系统,并按照此处的说明进行构建。()

为学习-v8配置v8

有一个 make 目标可以为 V8 生成特定于该项目的构建配置。 可以使用以下命令运行它:

1
$ make configure_v8

然后编译该项目

1
$ make compile_v8

gclient sync

1
gclient sync #安装依赖

Troubleshooting build(构建疑难解答):

1
2
/v8_src/v8/out/x64.release/obj/libv8_monolith.a(eh-frame.o):eh-frame.cc:function v8::internal::EhFrameWriter::WriteEmptyEhFrame(std::__1::basic_ostream<char, std::__1::char_traits<char> >&): error: undefined reference to 'std::__1::basic_ostream<char, std::__1::char_traits<char> >::write(char const*, long)'
clang: error: linker command failed with exit code 1 (use -v to see invocation)

-stdlib=libc++ 是 llvm 的 C++ 运行模式.此运行时具有 __1 命名空间。
看起来上面的静态库是用 clangs/llvm 的 libc++ 编译的,因为我们看到了 __1 命名空间。
-stdlib=libstdc++ 是 GNU 的 C++ 运行模式
命名空间 std::__1 被使用,因此是 libc++ 的命名空间,它是 clangs libc++ 库。
解决问题方式
1.编译时更改 v8 构建以使用 glibc++,以便链接它时符号正确
2.更新链接器 (ld) 以使用 libc++。
在链接期间包含要链接的正确库,这需要指定:

1
-stdlib=libc++ -Wl,-L$(v8_build_dir)

如果我们查看 $(v8_build_dir),我们会找到 libc++.so。 我们还需要动态链接器在运行时使用 LD_LIBRARY_PATH 找到这个库:

1
$ LD_LIBRARY_PATH=../v8_src/v8/out/x64.release/ ./hello-world

这是使用我们路径中的 ld 。 我们可以通过 -B 选项告诉 clang 使用不同的搜索路径:

1
2
$ clang++ --help | grep -- '-B'
-B <dir> Add <dir> to search path for binaries and object files used implicitly

libgcc_s 是 GCC 低级运行时库。 和 glibc++ 库不同。
运行 cctest:

1
$ out.gn/learning/cctest test-heap-profiler/HeapSnapshotRetainedObjectInfo

要获取可用测试的列表:

1
$ out.gn/learning/cctest --list

检查格式化/linting:

1
2
git cl format
# 补充:Linting 代码的本意就是找出程序中的错误,这些错误包括潜在的语法错误,编译错误,拼写错误等。

然后您可以 git diff 并查看更改。
运行提交前检查:

1
$ git cl presubmit

然后使用上传:

1
$ git cl upload

Build details

所以当我们运行 gn 时,它会生成 Ninja 构建文件。 GN 本身是用 C++ 编写的,但有一个 Python 包装器。
gn 中的组只是其他目标的集合,使它们能够具有名称。
所以当我们运行 gn 时会生成一些 .ninja 文件。 如果我们查看输出目录的根目录,我们会发现两个 .ninja 文件:

1
build.ninja  toolchain.ninja

默认情况下,ninja 会查找 build.ninja,当我们运行 ninja 时,我们通常会指定 -C out/dir。
如果没有在命令行上指定目标,ninja将执行所有输出,除非有一个指定为默认值。 V8 具有以下默认目标:

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
default all

build all: phony $
./bytecode_builtins_list_generator $
./d8 $
obj/fuzzer_support.stamp $
./gen-regexp-special-case $
obj/generate_bytecode_builtins_list.stamp $
obj/gn_all.stamp $
obj/json_fuzzer.stamp $
obj/lib_wasm_fuzzer_common.stamp $
./mksnapshot $
obj/multi_return_fuzzer.stamp $
obj/parser_fuzzer.stamp $
obj/postmortem-metadata.stamp $
obj/regexp_builtins_fuzzer.stamp $
obj/regexp_fuzzer.stamp $
obj/run_gen-regexp-special-case.stamp $
obj/run_mksnapshot_default.stamp $
obj/run_torque.stamp $
./torque $
./torque-language-server $
obj/torque_base.stamp $
obj/torque_generated_definitions.stamp $
obj/torque_generated_initializers.stamp $
obj/torque_ls_base.stamp $
./libv8.so.TOC $
obj/v8_archive.stamp $
...

虚假规则可用于为其他目标创建别名。 ninja 中的 $ 是一个转义字符,因此对于 all 目标,它会转义新行,就像在 shell 脚本中使用 \ 一样。
让我们看一下 bytecode_builtins_list_generator:

1
build $:bytecode_builtins_list_generator: phony ./bytecode_builtins_list_generator

ninja build 语句的格式为:

1
build outputs: rulename inputs

我们再次看到 $ ninja 转义字符,但这次它转义了冒号,否则会被解释为分隔文件名。 这种情况下的输出是 bytecode_builtins_list_generator。
我猜,因为我找不到 ./bytecode_builtins_list_generator 和
在这种情况下,默认的 target_out_dir 是 //out/x64.release_gcc/obj。 生成此文件的 BUILD.gn
中的可执行文件未指定任何输出目录,因此我假设生成的 .ninja 文件位于 target_out_dir 中,在这种情况下我们可以找到 bytecode_builtins_list
_generator.ninja 该文件有一个名为:

1
label_name = bytecode_builtins_list_generator

嗯,请注意在 build.ninja 中有以下命令:

1
subninja toolchain.ninja

在 toolchain.ninja 我们有:

1
subninja obj/bytecode_builtins_list_generator.ninja

这就是使 ./bytecode_builtins_list_generator 可用的原因。

1
2
3
4
5
$ ninja -C out/x64.release_gcc/ -t targets all | grep bytecode_builtins_list_generator
$ rm out/x64.release_gcc/bytecode_builtins_list_generator
$ ninja -C out/x64.release_gcc/ bytecode_builtins_list_generator
ninja: Entering directory `out/x64.release_gcc/'
[1/1] LINK ./bytecode_builtins_list_generator

好的,所以我想了解在过程中何时运行torque以生成 TorqueGeneratedStruct 之类的类:

1
class Struct : public TorqueGeneratedStruct<Struct, HeapObject> {
1
2
3
4
5
6
./torque $                                                                  
./torque-language-server $
obj/torque_base.stamp $
obj/torque_generated_definitions.stamp $
obj/torque_generated_initializers.stamp $
obj/torque_ls_base.stamp $

和之前一样,我们可以在 toolchain.ninja 的 subninja 命令中找到 obj/torque.ninja:

1
subninja obj/torque.ninja

这是建立可执行torque,但它还没有运行。

1
2
3
4
5
6
7
8
9
10
11
$ gn ls out/x64.release_gcc/ --type=action
//:generate_bytecode_builtins_list
//:postmortem-metadata
//:run_gen-regexp-special-case
//:run_mksnapshot_default
//:run_torque
//:v8_dump_build_config
//src/inspector:protocol_compatibility
//src/inspector:protocol_generated_sources
//tools/debug_helper:gen_heap_constants
//tools/debug_helper:run_mkgrokdump

注意 run_torque 目标

1
$ gn desc out/x64.release_gcc/ //:run_torque

如果我们查看 toolchain.ninja,我们有一个名为 ___run_torque___build_toolchain_linux_x64__rule 的规则

1
2
3
4
5
6
command = python ../../tools/run.py ./torque -o gen/torque-generated -v8-root ../.. 
src/builtins/array-copywithin.tq
src/builtins/array-every.tq
src/builtins/array-filter.tq
src/builtins/array-find.tq
...

并且有一个构建指定 gen/torque-generated 中的 .h 和 cc 文件,如果它们发生更改,则其中包含此规则。

Building chromium

在对 V8 进行更改时,您可能需要验证您的更改没有破坏 Chromium 中的任何内容。
生成您的项目(gpy):您必须在构建之前运行一次:

1
2
$ gclient sync
$ gclient runhooks

Update the code base

1
2
3
$ git fetch origin master
$ git co master
$ git merge origin/master

Building using GN

1
$ gn args out.gn/learning

Building using Ninja

1
Building using Ninja

构建测试:

1
$ ninja -C out.gn/learning chrome/test:unit_tests

我第一次构建时遇到的错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
traceback (most recent call last):
File "./gyp-mac-tool", line 713, in <module>
sys.exit(main(sys.argv[1:]))
File "./gyp-mac-tool", line 29, in main
exit_code = executor.Dispatch(args)
File "./gyp-mac-tool", line 44, in Dispatch
return getattr(self, method)(*args[1:])
File "./gyp-mac-tool", line 68, in ExecCopyBundleResource
self._CopyStringsFile(source, dest)
File "./gyp-mac-tool", line 134, in _CopyStringsFile
import CoreFoundation
ImportError: No module named CoreFoundation
[6642/20987] CXX obj/base/debug/base.task_annotator.o
[6644/20987] ACTION base_nacl: build newlib plib_9b4f41e4158ebb93a5d28e6734a13e85
ninja: build stopped: subcommand failed.

我能够通过以下方式解决这个问题:

1
$ pip install -U pyobjc

Using a specific version of V8

以下说明有效,但也可以创建从 chromium/src/v8 到本地 v8 存储库和构建/测试的软链接。
因此,我们希望包含我们的 V8 更新版本,以便我们可以通过对 V8 的更改来验证它是否正确构建。 虽然我不确定这是不是正确的方法,但我能够更新 src (
chromium) 中的 DEPS 并将 v8 条目设置为 git@github.com:danbev/v8.git@064718a8921608eaf9b5eadbb7d734ec04068a87:

1
"git@github.com:danbev/v8.git@064718a8921608eaf9b5eadbb7d734ec04068a87"

在此之后,您必须运行 gclient sync。
另一种方法是不更新 DEPS 文件,这是一个版本控制文件,而是更新 .gclientrc 并添加一个 custom_deps 条目:

1
2
3
4
solutions = [{u'managed': False, u'name': u'src', u'url': u'https://chromium.googlesource.com/chromium/src.git', 
u'custom_deps': {
"src/v8": "git@github.com:danbev/v8.git@27a666f9be7ca3959c7372bdeeee14aef2a4b7ba"
}, u'deps_file': u'.DEPS.git', u'safesync_url': u''}]

Buiding pdfium

您可能必须编译这个项目(除了 chromium 来验证 v8 中的更改不会破坏 pdfium.xml 中的代码)。

Create/clone the project

1
2
3
4
$ mkdir pdfuim_reop
$ gclient config --unmanaged https://pdfium.googlesource.com/pdfium.git
$ gclient sync
$ cd pdfium

Building

1
$ ninja -C out/Default

Using a branch of v8

您应该能够更新 .gclient 文件,添加一个 custom_deps 条目:

1
2
3
4
5
6
7
8
9
10
solutions = [
{
"name" : "pdfium",
"url" : "https://pdfium.googlesource.com/pdfium.git",
"deps_file" : "DEPS",
"managed" : False,
"custom_deps" : {
"v8": "git@github.com:danbev/v8.git@064718a8921608eaf9b5eadbb7d734ec04068a87"
},
},

] cache_dir = None 在此之后您也必须运行 gclient sync。

Code in this repo

hello-world(https://github.com/danbev/learning-v8/blob/master/hello-world.cc)
hello-world 被大量评论并展示了从 JavaScript 公开和访问静态 int 的用法。
instances(https://github.com/danbev/learning-v8/blob/master/instances.cc)
实例展示了从 JavaScript 创建 C++ 类的新实例的用法。
run-script(https://github.com/danbev/learning-v8/blob/master/run-script.cc)
run-script 与 instance 基本相同,但读取外部文件 script.js 并运行脚本。
tests
测试目录包含 V8 中各个类/概念的单元测试,以帮助理解它们。

Building this projects code

1
$ make

Running

1
$ ./hello-world

Cleaning

1
$ make clean

Contributing a change to V8

1.使用 git new-branch name 创建一个工作分支
2.git cl 上传
有关更多详细信息,请参阅 Google 的贡献代码。(https://www.chromium.org/developers/contributing-code)

Find the current issue number

1
$ git cl issue

Debugging

1
2
$ lldb hello-world
(lldb) br s -f hello-world.cc -l 27

src/objects-printer.cc 中有许多有用的函数,它们也可以在 lldb 中使用。

1
(lldb) print _v8_internal_Print_Object(*(v8::internal::Object**)(*init_fn))
1
(lldb) p _v8_internal_Print_StackTrace()

Creating command aliases in lldb

创建一个名为 .lldbinit 的文件(在您的项目目录或主目录中)。 这个文件现在可以在 v8 的工具目录中找到。

Using d8

这是用于以下示例的源文件:

1
2
3
4
5
6
7
8
9
10
$ cat class.js
function Person(name, age) {
this.name = name;
this.age = age;
}
print("before");
const p = new Person("Daniel", 41);
print(p.name);
print(p.age);
print("after");

V8_shell startup

运行 v8_shell 时会发生什么?

1
2
3
$ lldb -- out/x64.debug/d8 --enable-inspector class.js
(lldb) breakpoint set --file d8.cc --line 2662
Breakpoint 1: where = d8`v8::Shell::Main(int, char**) + 96 at d8.cc:2662, address = 0x0000000100015150

首先调用 v8::base::debug::EnableInProcessStackDumping(),然后调用一些由宏保护的特定于 Windows 的代码。 接下来是使用 v8::Shell::SetOptions
设置所有选项
SetOptions 将调用 src/api.cc 中的 v8::V8::SetFlagsFromCommandLine:

1
i::FlagList::SetFlagsFromCommandLine(argc, argv, remove_flags);

这个函数可以在 src/flags.cc 中找到。 标志本身在 src/flag-definitions.h 中定义
接下来创建一个新的 SourceGroup 数组:

1
2
3
4
5
6
7
8
options.isolate_sources = new SourceGroup[options.num_isolates];
SourceGroup* current = options.isolate_sources;
current->Begin(argv, 1);
for (int i = 1; i < argc; i++) {
const char* str = argv[i];

(lldb) p str
(const char *) $6 = 0x00007fff5fbfed4d "manual.js"

然后执行检查以查看 args 是否为 –isolate 或 –module,或 -e,如果不是(如我们的例子中)

1
2
3
} else if (strncmp(str, "-", 1) != 0) {
// Not a flag, so it must be a script to execute.
options.script_executed = true;

TODO:我不完全确定 SourceGroups 是关于什么的,但只是注意到这一点,稍后会重新讨论。
这将带我们回到 src/d8.cc 中的 int Shell::Main

1
2
3
4
::V8::InitializeICUDefaultLocation(argv[0], options.icu_data_file);

(lldb) p argv[0]
(char *) $8 = 0x00007fff5fbfed48 "./d8"

更多细节请参见 ICU。
接下来初始化默认的 V8 平台:

1
g_platform = i::FLAG_verify_predictable ? new PredictablePlatform() : v8::platform::CreateDefaultPlatform();

v8::platform::CreateDefaultPlatform() 在我们的例子中会被调用。
然后我们回到 Main 并有以下几行:

1
2
2685 v8::V8::InitializePlatform(g_platform);
2686 v8::V8::Initialize();

这与我在 Node.js 启动过程中看到的非常相似。(https://github.com/danbev/learning-nodejs#startint-argc-char-argv)
我们没有在命令行上指定任何 natives_blob 或 snapshot_blob 作为选项,因此将使用默认值:

1
v8::V8::InitializeExternalStartupData(argv[0]);

回到 src/d8.cc 第 2918 行:

1
Isolate* isolate = Isolate::New(create_params);

此调用将带我们进入 api.cc 第 8185 行:

1
i::Isolate* isolate = new i::Isolate(false);

因此,我们正在调用 Isolate 构造函数(在 src/isolate.cc 中)。

1
isolate->set_snapshot_blob(i::Snapshot::DefaultSnapshotBlob());

api.cc:

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
isolate->Init(NULL);

compilation_cache_ = new CompilationCache(this);
context_slot_cache_ = new ContextSlotCache();
descriptor_lookup_cache_ = new DescriptorLookupCache();
unicode_cache_ = new UnicodeCache();
inner_pointer_to_code_cache_ = new InnerPointerToCodeCache(this);
global_handles_ = new GlobalHandles(this);
eternal_handles_ = new EternalHandles();
bootstrapper_ = new Bootstrapper(this);
handle_scope_implementer_ = new HandleScopeImplementer(this);
load_stub_cache_ = new StubCache(this, Code::LOAD_IC);
store_stub_cache_ = new StubCache(this, Code::STORE_IC);
materialized_object_store_ = new MaterializedObjectStore(this);
regexp_stack_ = new RegExpStack();
regexp_stack_->isolate_ = this;
date_cache_ = new DateCache();
call_descriptor_data_ =
new CallInterfaceDescriptorData[CallDescriptors::NUMBER_OF_DESCRIPTORS];
access_compiler_data_ = new AccessCompilerData();
cpu_profiler_ = new CpuProfiler(this);
heap_profiler_ = new HeapProfiler(heap());
interpreter_ = new interpreter::Interpreter(this);
compiler_dispatcher_ =
new CompilerDispatcher(this, V8::GetCurrentPlatform(), FLAG_stack_size);

src/builtins/builtins.cc,这是定义内置函数的地方。 TODO:理清这些宏的作用。
在 src/v8.cc 中,我们检查了传递的选项是否用于压力运行,但由于我们没有传递任何此类标志,因此将遵循此代码路径,该路径将调用 RunMain:

1
result = RunMain(isolate, argc, argv, last_run);

这将最终调用:

1
options.isolate_sources[0].Execute(isolate);

这将调用 SourceGroup::Execute(Isolate* isolate)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Use all other arguments as names of files to load and run.
HandleScope handle_scope(isolate);
Local<String> file_name = String::NewFromUtf8(isolate, arg, NewStringType::kNormal).ToLocalChecked();
Local<String> source = ReadFile(isolate, arg);
if (source.IsEmpty()) {
printf("Error reading '%s'\n", arg);
Shell::Exit(1);
}
Shell::options.script_executed = true;
if (!Shell::ExecuteString(isolate, source, file_name, false, true)) {
exception_was_thrown = true;
break;
}

ScriptOrigin origin(name);
if (compile_options == ScriptCompiler::kNoCompileOptions) {
ScriptCompiler::Source script_source(source, origin);
return ScriptCompiler::Compile(context, &script_source, compile_options);
}

它将委托给 ScriptCompiler(Local, Source* source, CompileOptions options):

1
auto maybe = CompileUnboundInternal(isolate, source, options);

CompileUnboundInternal

1
2
3
4
result = i::Compiler::GetSharedFunctionInfoForScript(
str, name_obj, line_offset, column_offset, source->resource_options,
source_map_url, isolate->native_context(), NULL, &script_data, options,
i::NOT_NATIVES_CODE);

src/compiler.cc

1
2
3
4
// Compile the function and add it to the cache.
ParseInfo parse_info(script);
Zone compile_zone(isolate->allocator(), ZONE_NAME);
CompilationInfo info(&compile_zone, &parse_info, Handle<JSFunction>::null());

回到 src/compiler.cc-info.cc:

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
result = CompileToplevel(&info);

(lldb) job *result
0x17df0df309f1: [SharedFunctionInfo]
- name = 0x1a7f12d82471 <String[0]: >
- formal_parameter_count = 0
- expected_nof_properties = 10
- ast_node_count = 23
- instance class name = #Object

- code = 0x1d8484d3661 <Code: BUILTIN>
- source code = function bajja(a, b, c) {
var d = c - 100;
return a + d * b;
}

var result = bajja(2, 2, 150);
print(result);

- anonymous expression
- function token position = -1
- start position = 0
- end position = 114
- no debug info
- length = 0
- optimized_code_map = 0x1a7f12d82241 <FixedArray[0]>
- feedback_metadata = 0x17df0df30d09: [FeedbackMetadata]
- length: 3
- slot_count: 11
Slot #0 LOAD_GLOBAL_NOT_INSIDE_TYPEOF_IC
Slot #2 kCreateClosure
Slot #3 LOAD_GLOBAL_NOT_INSIDE_TYPEOF_IC
Slot #5 CALL_IC
Slot #7 CALL_IC
Slot #9 LOAD_GLOBAL_NOT_INSIDE_TYPEOF_IC

- bytecode_array = 0x17df0df30c61

回到d8.cc:

1
maybe_result = script->Run(realm);

src/api.cc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
auto fun = i::Handle<i::JSFunction>::cast(Utils::OpenHandle(this));

(lldb) job *fun
0x17df0df30e01: [Function]
- map = 0x19cfe0003859 [FastProperties]
- prototype = 0x17df0df043b1
- elements = 0x1a7f12d82241 <FixedArray[0]> [FAST_HOLEY_ELEMENTS]
- initial_map =
- shared_info = 0x17df0df309f1 <SharedFunctionInfo>
- name = 0x1a7f12d82471 <String[0]: >
- formal_parameter_count = 0
- context = 0x17df0df03bf9 <FixedArray[245]>
- feedback vector cell = 0x17df0df30ed1 Cell for 0x17df0df30e49 <FixedArray[13]>
- code = 0x1d8484d3661 <Code: BUILTIN>
- properties = 0x1a7f12d82241 <FixedArray[0]> {
#length: 0x2c35a5718089 <AccessorInfo> (const accessor descriptor)
#name: 0x2c35a57180f9 <AccessorInfo> (const accessor descriptor)
#arguments: 0x2c35a5718169 <AccessorInfo> (const accessor descriptor)
#caller: 0x2c35a57181d9 <AccessorInfo> (const accessor descriptor)
#prototype: 0x2c35a5718249 <AccessorInfo> (const accessor descriptor)

}
1
2
3
i::Handle<i::Object> receiver = isolate->global_proxy();
Local<Value> result;
has_pending_exception = !ToLocal<Value>(i::Execution::Call(isolate, fun, receiver, 0, nullptr), &result);

src/execution.cc

Zone

直接取自 src/zone/zone.h:

1
2
3
4
5
// Zone支持非常快速地分配小块
// 空间。 这些块不能单独释放,而
// Zone 支持一次性释放所有块
// 手术。 Zone 用于保存临时数据结构,例如
// 抽象语法树,编译后释放。

V8 flags

1
$ ./d8 --help

d8

1
2
3
4
5
6
7
(lldb) br s -f d8.cc -l 2935

return v8::Shell::Main(argc, argv);

api.cc:6112
i::ReadNatives();
natives-external.cc

v8::String::NewFromOneByte

所以第一次看到这个函数名的时候有点迷茫,还以为跟字符串的长度有关系。 但是字节是构成字符串的字符的类型。 例如,一个单字节字符将被重新解释为
uint8_t:

1
2
3
const char* data

reinterpret_cast<const uint8_t*>(data)

task:
gdbinit 已更新。 检查是否有应该移植到 lldbinit 的东西

Invocation walkthrough

本节将通过调用脚本来了解 V8 中发生了什么。
我将使用 run-scripts.cc 作为示例。

1
2
$ lldb -- ./run-scripts
(lldb) br s -n main

我将逐步完成,直到以下调用:

1
script->Run(context).ToLocalChecked();

所以,Script::Run 是在 api.cc 中定义的。首先在这个函数中发生的事情是一个宏:

1
2
3
4
5
6
7
8
9
10
11
12
PREPARE_FOR_EXECUTION_WITH_CONTEXT_IN_RUNTIME_CALL_STATS_SCOPE(
"v8",
"V8.Execute",
context,
Script,
Run,
MaybeLocal<Value>(),
InternalEscapableScope,
true);
TRACE_EVENT_CALL_STATS_SCOPED(isolate, category, name);
PREPARE_FOR_EXECUTION_GENERIC(isolate, context, class_name, function_name, \
bailout_value, HandleScopeClass, do_callback);

那么,预处理器用什么来代替它:

1
auto isolate = context.IsEmpty() ? i::Isolate::Current()                               : reinterpret_cast<i::Isolate*>(context->GetIsolate());

我现在跳过 TRACE_EVENT_CALL_STATS_SCOPED。 PREPARE_FOR_EXECUTION_GENERIC 将被替换为:

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
if (IsExecutionTerminatingCheck(isolate)) {                        \
return bailout_value; \
} \
HandleScopeClass handle_scope(isolate); \
CallDepthScope<do_callback> call_depth_scope(isolate, context); \
LOG_API(isolate, class_name, function_name); \
ENTER_V8_DO_NOT_USE(isolate); \
bool has_pending_exception = false

auto fun = i::Handle<i::JSFunction>::cast(Utils::OpenHandle(this));

(lldb) job *fun
0x33826912c021: [Function]
- map = 0x1d0656c03599 [FastProperties]
- prototype = 0x338269102e69
- elements = 0x35190d902241 <FixedArray[0]> [FAST_HOLEY_ELEMENTS]
- initial_map =
- shared_info = 0x33826912bc11 <SharedFunctionInfo>
- name = 0x35190d902471 <String[0]: >
- formal_parameter_count = 0
- context = 0x338269102611 <FixedArray[265]>
- feedback vector cell = 0x33826912c139 <Cell value= 0x33826912c069 <FixedArray[24]>>
- code = 0x1319e25fcf21 <Code BUILTIN>
- properties = 0x35190d902241 <FixedArray[0]> {
#length: 0x2e9d97ce68b1 <AccessorInfo> (const accessor descriptor)
#name: 0x2e9d97ce6921 <AccessorInfo> (const accessor descriptor)
#arguments: 0x2e9d97ce6991 <AccessorInfo> (const accessor descriptor)
#caller: 0x2e9d97ce6a01 <AccessorInfo> (const accessor descriptor)
#prototype: 0x2e9d97ce6a71 <AccessorInfo> (const accessor descriptor)
}

i::JSFunction 的代码在 src/api.h 中生成。 让我们仔细看看这个。

1
2
3
4
5
#define DECLARE_OPEN_HANDLE(From, To) \
static inline v8::internal::Handle<v8::internal::To> \
OpenHandle(const From* that, bool allow_empty_handle = false);

OPEN_HANDLE_LIST(DECLARE_OPEN_HANDLE)

OPEN_HANDLE_LIST 如下:

1
2
3
#define OPEN_HANDLE_LIST(V)                    \
....
V(Script, JSFunction) \

所以让我们为 JSFunction 扩展它,它应该变成:

1
2
static inline v8::internal::Handle<v8::internal::JSFunction> \
OpenHandle(const Script* that, bool allow_empty_handle = false);

因此将有一个名为 OpenHandle 的函数,它将接受一个指向 Script 的 const 指针。
在 src/api.h 再往下一点,还有另一个宏,如下所示:

1
OPEN_HANDLE_LIST(MAKE_OPEN_HANDLE)

MAKE_OPEN_HANDLE:

1
2
3
4
5
6
#define MAKE_OPEN_HANDLE(From, To)
v8::internal::Handle<v8::internal::To> Utils::OpenHandle(
const v8::From* that, bool allow_empty_handle) {
return v8::internal::Handle<v8::internal::To>(
reinterpret_cast<v8::internal::Address*>(const_cast<v8::From*>(that)));
}

请记住,JSFunction 包含在 OPEN_HANDLE_LIST 中,因此在预处理器处理此标头后,源代码中将包含以下内容:具体示例如下所示:

1
2
3
4
v8::internal::Handle<v8::internal::JSFunction> Utils::OpenHandle(
const v8::Script* that, bool allow_empty_handle) {
return v8::internal::Handle<v8::internal::JSFunction>(
reinterpret_cast<v8::internal::Address*>(const_cast<v8::Script*>(that))); }

您可以使用以下命令检查预处理器的输出:
$ clang++ -I./out/x64.release/gen -I. -I./include -E src/api/api-inl.h > api-inl.output
那么 JSFunction 是在哪里声明的呢? 它在objects.h中定义

Ignition interpreter

用户 JavaScript 还需要为他们生成字节码,他们还使用 C++ DLS 并使用 CodeStubAssembler -> CodeAssembler ->
RawMachineAssembler,就像内置函数一样。

C++ Domain Specific Language (DLS)

Build failure

rebase后,我看到了以下问题:

1
2
3
$ ninja -C out/Debug chrome
ninja: Entering directory `out/Debug'
ninja: error: '../../chrome/renderer/resources/plugins/plugin_delay.html', needed by 'gen/chrome/grit/renderer_resources.h', missing and no known rule to make it

“解决方案”是删除 out 目录并重建。

Tasks

要找到合适的任务,您可以在 bugs.chromium.org 上使用 label:HelpWanted。

OpenHandle

这个调用有什么作用:

1
2
3
Utils::OpenHandle(*(source->source_string));

OPEN_HANDLE_LIST(MAKE_OPEN_HANDLE)

这是 src/api.h 中定义的宏:

1
2
3
4
5
6
7
8
9
10
11
#define MAKE_OPEN_HANDLE(From, To)                                             \
v8::internal::Handle<v8::internal::To> Utils::OpenHandle( \
const v8::From* that, bool allow_empty_handle) { \
DCHECK(allow_empty_handle || that != NULL); \
DCHECK(that == NULL || \
(*reinterpret_cast<v8::internal::Object* const*>(that))->Is##To()); \
return v8::internal::Handle<v8::internal::To>( \
reinterpret_cast<v8::internal::To**>(const_cast<v8::From*>(that))); \
}

OPEN_HANDLE_LIST(MAKE_OPEN_HANDLE)

如果我们仔细看一下宏,在我们的例子中应该扩展成这样的:

1
2
3
4
5
6
7
v8::internal::Handle<v8::internal::To> Utils::OpenHandle(const v8:String* that, false) {
DCHECK(allow_empty_handle || that != NULL); \
DCHECK(that == NULL || \
(*reinterpret_cast<v8::internal::Object* const*>(that))->IsString()); \
return v8::internal::Handle<v8::internal::String>( \
reinterpret_cast<v8::internal::String**>(const_cast<v8::String*>(that))); \
}

所以这会返回一个新的 v8::internal::Handle,构造函数在 src/handles.h:95 中定义。
src/objects.cc Handle WeakFixedArray::Add(Handle maybe_array, 10167 Handle value, 10168 int* assigned_index) { 注意第一个参数的名称
maybe_array 但它不是maybe类型?

Context

JavaScript 提供了一组内置函数和对象。 这些功能和对象可以通过用户代码进行更改。 每个上下文都是这些对象和函数的单独集合。
并且 internal::Context 在 deps/v8/src/contexts.h 中声明并扩展 FixedArray。

1
class Context: public FixedArray {

可以通过调用创建上下文:

1
2
3
4
const v8::HandleScope handle_scope(isolate_);
Handle<Context> context = Context::New(isolate_,
nullptr,
v8::Local<v8::ObjectTemplate>());

Context::New 可以在 src/api.cc:6405 中找到:

1
2
3
4
5
6
7
8
Local<Context> v8::Context::New(
v8::Isolate* external_isolate, v8::ExtensionConfiguration* extensions,
v8::MaybeLocal<ObjectTemplate> global_template,
v8::MaybeLocal<Value> global_object,
DeserializeInternalFieldsCallback internal_fields_deserializer) {
return NewContext(external_isolate, extensions, global_template,
global_object, 0, internal_fields_deserializer);
}

该函数的声明可以在 include/v8.h 中找到:

1
2
3
4
5
6
static Local<Context> New(
Isolate* isolate, ExtensionConfiguration* extensions = NULL,
MaybeLocal<ObjectTemplate> global_template = MaybeLocal<ObjectTemplate>(),
MaybeLocal<Value> global_object = MaybeLocal<Value>(),
DeserializeInternalFieldsCallback internal_fields_deserializer =
DeserializeInternalFieldsCallback());

所以我们可以看到我们不必指定 internal_fields_deserialize 的原因。 什么是扩展配置?
这个类可以在 include/v8.h 中找到,它只有两个成员,一个扩展名的计数和一个包含名称的数组。

如果指定,这些将由 Boostrapper::InstallExtensions 安装,后者将委托给 Genesis::InstallExtensions,两者都可以在 src/boostrapper.cc 中找到。
扩展在哪里注册?
每个进程执行一次,并从 V8::Initialize() 调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void Bootstrapper::InitializeOncePerProcess() {
free_buffer_extension_ = new FreeBufferExtension;
v8::RegisterExtension(free_buffer_extension_);
gc_extension_ = new GCExtension(GCFunctionName());
v8::RegisterExtension(gc_extension_);
externalize_string_extension_ = new ExternalizeStringExtension;
v8::RegisterExtension(externalize_string_extension_);
statistics_extension_ = new StatisticsExtension;
v8::RegisterExtension(statistics_extension_);
trigger_failure_extension_ = new TriggerFailureExtension;
v8::RegisterExtension(trigger_failure_extension_);
ignition_statistics_extension_ = new IgnitionStatisticsExtension;
v8::RegisterExtension(ignition_statistics_extension_);
}

扩展可以在 src/extensions 中找到。 您注册自己的扩展,可以在 test/context_test.cc 中找到一个示例。

1
2
3
(lldb) br s -f node.cc -l 4439
(lldb) expr context->length()
(int) $522 = 281

这个输出被拿走了
创建一个新的上下文是由 v8::CreateEnvironment 完成的

1
(lldb) br s -f api.cc -l 6565
1
2
3
4
InvokeBootstrapper<ObjectType> invoke;
6635 result =
-> 6636 invoke.Invoke(isolate, maybe_proxy, proxy_template, extensions,
6637 context_snapshot_index, embedder_fields_deserializer);

这稍后将在 Snapshot::NewContextFromSnapshot 中结束:

1
2
3
4
5
6
7
Vector<const byte> context_data =
ExtractContextData(blob, static_cast<uint32_t>(context_index));
SnapshotData snapshot_data(context_data);

MaybeHandle<Context> maybe_result = PartialDeserializer::DeserializeContext(
isolate, &snapshot_data, can_rehash, global_proxy,
embedder_fields_deserializer);

所以我们可以在这里看到 Context 是从快照中反序列化的。 在这个阶段上下文包含什么:

1
2
3
4
(lldb) expr result->length()
(int) $650 = 281
(lldb) expr result->Print()
// not inlcuding the complete output

让我们看一个条目:

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
(lldb) expr result->get(0)->Print()
0xc201584331: [Function] in OldSpace
- map = 0xc24c002251 [FastProperties]
- prototype = 0xc201584371
- elements = 0xc2b2882251 <FixedArray[0]> [HOLEY_ELEMENTS]
- initial_map =
- shared_info = 0xc2b2887521 <SharedFunctionInfo>
- name = 0xc2b2882441 <String[0]: >
- formal_parameter_count = -1
- kind = [ NormalFunction ]
- context = 0xc201583a59 <FixedArray[281]>
- code = 0x2df1f9865a61 <Code BUILTIN>
- source code = () {}
- properties = 0xc2b2882251 <FixedArray[0]> {
#length: 0xc2cca83729 <AccessorInfo> (const accessor descriptor)
#name: 0xc2cca83799 <AccessorInfo> (const accessor descriptor)
#arguments: 0xc201587fd1 <AccessorPair> (const accessor descriptor)
#caller: 0xc201587fd1 <AccessorPair> (const accessor descriptor)
#constructor: 0xc201584c29 <JSFunction Function (sfi = 0xc2b28a6fb1)> (const data descriptor)
#apply: 0xc201588079 <JSFunction apply (sfi = 0xc2b28a7051)> (const data descriptor)
#bind: 0xc2015880b9 <JSFunction bind (sfi = 0xc2b28a70f1)> (const data descriptor)
#call: 0xc2015880f9 <JSFunction call (sfi = 0xc2b28a7191)> (const data descriptor)
#toString: 0xc201588139 <JSFunction toString (sfi = 0xc2b28a7231)> (const data descriptor)
0xc2b28bc669 <Symbol: Symbol.hasInstance>: 0xc201588179 <JSFunction [Symbol.hasInstance] (sfi = 0xc2b28a72d1)> (const data descriptor)
}

- feedback vector: not available

所以我们可以看到这是 [Function] 类型,我们可以使用:

1
2
3
4
(lldb) expr JSFunction::cast(result->get(0))->code()->Print()
0x2df1f9865a61: [Code]
kind = BUILTIN
name = EmptyFunction
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
(lldb) expr JSFunction::cast(result->closure())->Print()
0xc201584331: [Function] in OldSpace
- map = 0xc24c002251 [FastProperties]
- prototype = 0xc201584371
- elements = 0xc2b2882251 <FixedArray[0]> [HOLEY_ELEMENTS]
- initial_map =
- shared_info = 0xc2b2887521 <SharedFunctionInfo>
- name = 0xc2b2882441 <String[0]: >
- formal_parameter_count = -1
- kind = [ NormalFunction ]
- context = 0xc201583a59 <FixedArray[281]>
- code = 0x2df1f9865a61 <Code BUILTIN>
- source code = () {}
- properties = 0xc2b2882251 <FixedArray[0]> {
#length: 0xc2cca83729 <AccessorInfo> (const accessor descriptor)
#name: 0xc2cca83799 <AccessorInfo> (const accessor descriptor)
#arguments: 0xc201587fd1 <AccessorPair> (const accessor descriptor)
#caller: 0xc201587fd1 <AccessorPair> (const accessor descriptor)
#constructor: 0xc201584c29 <JSFunction Function (sfi = 0xc2b28a6fb1)> (const data descriptor)
#apply: 0xc201588079 <JSFunction apply (sfi = 0xc2b28a7051)> (const data descriptor)
#bind: 0xc2015880b9 <JSFunction bind (sfi = 0xc2b28a70f1)> (const data descriptor)
#call: 0xc2015880f9 <JSFunction call (sfi = 0xc2b28a7191)> (const data descriptor)
#toString: 0xc201588139 <JSFunction toString (sfi = 0xc2b28a7231)> (const data descriptor)
0xc2b28bc669 <Symbol: Symbol.hasInstance>: 0xc201588179 <JSFunction [Symbol.hasInstance] (sfi = 0xc2b28a72d1)> (const data descriptor)
}

- feedback vector: not available

所以这是与反序列化上下文关联的 JSFunction。 不知道这是什么,因为查看源代码它看起来像一个空函数。
也可以在上下文中设置一个函数,所以我猜这可以访问设置后的上下文函数。 函数集在哪里,它可能是反序列化的,但我们可以看到它在 deps/v8/src/
bootstrapper.cc 中使用:

1
2
3
4
5
6
{
Handle<JSFunction> function = SimpleCreateFunction(isolate, factory->empty_string(), Builtins::kAsyncFunctionAwaitCaught, 2, false);
native_context->set_async_function_await_caught(*function);
}
​```console
(lldb) expr isolate()->builtins()->builtin_handle(Builtins::Name::kAsyncFunctionAwaitCaught)->Print()

Context::Scope 是一个用于进入/退出上下文的 RAII 类。 让我们仔细看看 Enter:

1
2
3
4
5
6
7
8
9
void Context::Enter() {
i::Handle<i::Context> env = Utils::OpenHandle(this);
i::Isolate* isolate = env->GetIsolate();
ENTER_V8_NO_SCRIPT_NO_EXCEPTION(isolate);
i::HandleScopeImplementer* impl = isolate->handle_scope_implementer();
impl->EnterContext(env);
impl->SaveContext(isolate->context());
isolate->set_context(*env);
}

所以当前上下文被保存,然后这个上下文环境被设置为隔离上的当前。 EnterContext 将推送传入的上下文(deps/v8/src/api.cc):

1
2
3
4
5
void HandleScopeImplementer::EnterContext(Handle<Context> context) {
entered_contexts_.push_back(*context);
}
...
DetachableVector<Context*> entered_contexts_;
1
2
3
4
5
DetachableVector is a delegate/adaptor with some additonaly features on a std::vector.
Handle<Context> context1 = NewContext(isolate);
Handle<Context> context2 = NewContext(isolate);
Context::Scope context_scope1(context1); // entered_contexts_ [context1], saved_contexts_[isolateContext]
Context::Scope context_scope2(context2); // entered_contexts_ [context1, context2], saved_contexts[isolateContext, context1]

现在,SaveContext 正在使用当前上下文,而不是此上下文 (env),并将其推送到 saved_contexts_vector的末尾。 当我们从 context_scope1 进入 context_
scope2 时,我们可以看到这个:
退出看起来像:

1
2
3
4
5
6
7
8
9
10
11
12
13
void Context::Exit() {
i::Handle<i::Context> env = Utils::OpenHandle(this);
i::Isolate* isolate = env->GetIsolate();
ENTER_V8_NO_SCRIPT_NO_EXCEPTION(isolate);
i::HandleScopeImplementer* impl = isolate->handle_scope_implementer();
if (!Utils::ApiCheck(impl->LastEnteredContextWas(env),
"v8::Context::Exit()",
"Cannot exit non-entered context")) {
return;
}
impl->LeaveContext();
isolate->set_context(impl->RestoreContext());
}

EmbedderData

上下文可以在其上设置embedder data。 就像上面描述的一样,Context 内部是一个 FixedArray。 Context中的SetEmbedderData在src/api.cc中实现:

1
2
3
const char* location = "v8::Context::SetEmbedderData()";
i::Handle<i::FixedArray> data = EmbedderDataFor(this, index, true, location);
i::Handle<i::FixedArray> data(env->embedder_data());

location 仅用于记录,我们现在可以忽略它。 EmbedderDataFor:

1
2
3
i::Handle<i::Context> env = Utils::OpenHandle(context);
...
i::Handle<i::FixedArray> data(env->embedder_data());

我们可以在 src/contexts-inl.h 中找到 embedder_data

1
2
3
4
5
#define NATIVE_CONTEXT_FIELD_ACCESSORS(index, type, name) \
inline void set_##name(type* value); \
inline bool is_##name(type* value) const; \
inline type* name() const;
NATIVE_CONTEXT_FIELDS(NATIVE_CONTEXT_FIELD_ACCESSORS)

以及 context.h 中的 NATIVE_CONTEXT_FIELDS:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#define NATIVE_CONTEXT_FIELDS(V)                                               \
V(GLOBAL_PROXY_INDEX, JSObject, global_proxy_object) \
V(EMBEDDER_DATA_INDEX, FixedArray, embedder_data) \
...

#define NATIVE_CONTEXT_FIELD_ACCESSORS(index, type, name) \
void Context::set_##name(type* value) { \
DCHECK(IsNativeContext()); \
set(index, value); \
} \
bool Context::is_##name(type* value) const { \
DCHECK(IsNativeContext()); \
return type::cast(get(index)) == value; \
} \
type* Context::name() const { \
DCHECK(IsNativeContext()); \
return type::cast(get(index)); \
}
NATIVE_CONTEXT_FIELDS(NATIVE_CONTEXT_FIELD_ACCESSORS)
#undef NATIVE_CONTEXT_FIELD_ACCESSORS

因此预处理器会将其扩展为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
FixedArray embedder_data() const;

void Context::set_embedder_data(FixedArray value) {
DCHECK(IsNativeContext());
set(EMBEDDER_DATA_INDEX, value);
}

bool Context::is_embedder_data(FixedArray value) const {
DCHECK(IsNativeContext());
return FixedArray::cast(get(EMBEDDER_DATA_INDEX)) == value;
}

FixedArray Context::embedder_data() const {
DCHECK(IsNativeContext());
return FixedArray::cast(get(EMBEDDER_DATA_INDEX));
}

我们可以看一下初始数据:

1
2
3
4
5
6
7
lldb) expr data->Print()
0x2fac3e896439: [FixedArray] in OldSpace
- map = 0x2fac9de82341 <Map(HOLEY_ELEMENTS)>
- length: 3
0-2: 0x2fac1cb822e1 <undefined>
(lldb) expr data->length()
(int) $5 = 3

设置后:

1
2
3
4
5
6
7
8
9
(lldb) expr data->Print()
0x2fac3e896439: [FixedArray] in OldSpace
- map = 0x2fac9de82341 <Map(HOLEY_ELEMENTS)>
- length: 3
0: 0x2fac20c866e1 <String[7]: embdata>
1-2: 0x2fac1cb822e1 <undefined>

(lldb) expr v8::internal::String::cast(data->get(0))->Print()
"embdata"

ENTER_V8_FOR_NEW_CONTEXT

此宏用于 CreateEnvironment (src/api.cc),此函数中的调用如下所示:

1
ENTER_V8_FOR_NEW_CONTEXT(isolate);

Factory::NewMap

本节将看看以下调用:

1
i::Handle<i::Map> map = factory->NewMap(i::JS_OBJECT_TYPE, 24);

让我们仔细看看可以在 src/factory.cc 中找到的这个函数:

1
2
3
4
5
6
7
8
9
Handle<Map> Factory::NewMap(InstanceType type, int instance_size,
ElementsKind elements_kind,
int inobject_properties) {
CALL_HEAP_FUNCTION(
isolate(),
isolate()->heap()->AllocateMap(type, instance_size, elements_kind,
inobject_properties),
Map);
}

如果我们查看 factory.h,我们可以看到 elements_kind 和 inobject_properties 的默认值:

1
2
3
Handle<Map> NewMap(InstanceType type, int instance_size,
ElementsKind elements_kind = TERMINAL_FAST_ELEMENTS_KIND,
int inobject_properties = 0);

如果我们展开 CALL_HEAP_FUNCTION 宏,我们将得到:

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
AllocationResult __allocation__ = isolate()->heap()->AllocateMap(type,
instance_size,
elements_kind,
inobject_properties),
Object* __object__ = nullptr;
RETURN_OBJECT_UNLESS_RETRY(isolate(), Map)
/* Two GCs before panicking. In newspace will almost always succeed. */
for (int __i__ = 0; __i__ < 2; __i__++) {
(isolate())->heap()->CollectGarbage(
__allocation__.RetrySpace(),
GarbageCollectionReason::kAllocationFailure);
__allocation__ = FUNCTION_CALL;
RETURN_OBJECT_UNLESS_RETRY(isolate, Map)
}
(isolate())->counters()->gc_last_resort_from_handles()->Increment();
(isolate())->heap()->CollectAllAvailableGarbage(
GarbageCollectionReason::kLastResort);
{
AlwaysAllocateScope __scope__(isolate());
t __allocation__ = isolate()->heap()->AllocateMap(type,
instance_size,
elements_kind,
inobject_properties),
}
RETURN_OBJECT_UNLESS_RETRY(isolate, Map)
/* TODO(1181417): Fix this. */
v8::internal::Heap::FatalProcessOutOfMemory("CALL_AND_RETRY_LAST", true);
return Handle<Map>();

所以,让我们看一下’src/heap/heap.cc’中的isolate()->heap()->AllocateMap:

1
2
HeapObject* result = nullptr;
AllocationResult allocation = AllocateRaw(Map::kSize, MAP_SPACE);

AllocateRaw 可以在 src/heap/heap-inl.h 中找到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
bool large_object = size_in_bytes > kMaxRegularHeapObjectSize;
HeapObject* object = nullptr;
AllocationResult allocation;
if (NEW_SPACE == space) {
if (large_object) {
space = LO_SPACE;
} else {
allocation = new_space_->AllocateRaw(size_in_bytes, alignment);
if (allocation.To(&object)) {
OnAllocationEvent(object, size_in_bytes);
}
return allocation;
}
}
} else if (MAP_SPACE == space) {
allocation = map_space_->AllocateRawUnaligned(size_in_bytes);
}
1
2
3
4
5
6
(lldb) expr large_object
(bool) $3 = false
(lldb) expr size_in_bytes
(int) $5 = 80
(lldb) expr map_space_
(v8::internal::MapSpace *) $6 = 0x0000000104700f60

AllocateRawUnaligned 可以在 src/heap/spaces-inl.h 中找到

1
HeapObject* object = AllocateLinearly(size_in_bytes);

v8::internal::Object

是对象层次结构中所有类的抽象超类,Smi 和 HeapObject 都是 Object 的子类,因此仅对象函数中没有数据成员。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
bool IsObject() const { return true; }
INLINE(bool IsSmi() const
INLINE(bool IsLayoutDescriptor() const
INLINE(bool IsHeapObject() const
INLINE(bool IsPrimitive() const
INLINE(bool IsNumber() const
INLINE(bool IsNumeric() const
INLINE(bool IsAbstractCode() const
INLINE(bool IsAccessCheckNeeded() const
INLINE(bool IsArrayList() const
INLINE(bool IsBigInt() const
INLINE(bool IsUndefined() const
INLINE(bool IsNull() const
INLINE(bool IsTheHole() const
INLINE(bool IsException() const
INLINE(bool IsUninitialized() const
INLINE(bool IsTrue() const
INLINE(bool IsFalse() const
...

v8::internal::Smi

扩展 v8::internal::Object 并且不在堆上分配。 没有成员,因为指针本身用于存储信息。
在我们的例子中,调用 v8::Isolate::New 由测试夹具完成:

1
2
3
virtual void SetUp() {
isolate_ = v8::Isolate::New(create_params_);
}

这将调用:

1
2
3
4
5
Isolate* Isolate::New(const Isolate::CreateParams& params) {
Isolate* isolate = Allocate();
Initialize(isolate, params);
return isolate;
}

在 Isolate::Initialize 中,我们将调用 i::Snapshot::Initialize(i_isolate):

1
2
if (params.entry_hook || !i::Snapshot::Initialize(i_isolate)) {
...

这将调用:

1
bool success = isolate->Init(&deserializer);

在此调用之前,所有根都未初始化。 阅读这个博客它说 Isolate 类包含一个根表。 在我看来,堆包含这个数据结构,但也许这就是他们的意思。

1
2
3
4
5
(lldb) bt 3
* thread #1, queue = 'com.apple.main-thread', stop reason = step over
* frame #0: 0x0000000101584f43 libv8.dylib`v8::internal::StartupDeserializer::DeserializeInto(this=0x00007ffeefbfe200, isolate=0x000000010481cc00) at startup-deserializer.cc:39
frame #1: 0x0000000101028bb6 libv8.dylib`v8::internal::Isolate::Init(this=0x000000010481cc00, des=0x00007ffeefbfe200) at isolate.cc:3036
frame #2: 0x000000010157c682 libv8.dylib`v8::internal::Snapshot::Initialize(isolate=0x000000010481cc00) at snapshot-common.cc:54

在 startup-deserializer.cc 我们可以找到 StartupDeserializer::DeserializeInto:

1
2
3
DisallowHeapAllocation no_gc;
isolate->heap()->IterateSmiRoots(this);
isolate->heap()->IterateStrongRoots(this, VISIT_ONLY_STRONG);

如果我们查看 src/roots.h 之后,我们可以在堆中找到只读根。 如果我们取 10 的值,即:

1
V(String, empty_string, empty_string)                                        \

然后我们可以检查这个值:

1
2
3
4
5
6
(lldb) expr roots_[9]
(v8::internal::Object *) $32 = 0x0000152d30b82851
(lldb) expr roots_[9]->IsString()
(bool) $30 = true
(lldb) expr roots_[9]->Print()
#

因此,此条目是指向托管堆上已从快照反序列化的对象的指针。
堆类有很多成员,在构造过程中由构造函数的主体初始化,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
{
// Ensure old_generation_size_ is a multiple of kPageSize.
DCHECK_EQ(0, max_old_generation_size_ & (Page::kPageSize - 1));

memset(roots_, 0, sizeof(roots_[0]) * kRootListLength);
set_native_contexts_list(nullptr);
set_allocation_sites_list(Smi::kZero);
set_encountered_weak_collections(Smi::kZero);
// Put a dummy entry in the remembered pages so we can find the list the
// minidump even if there are no real unmapped pages.
RememberUnmappedPage(nullptr, false);
}

我们可以看到,roots_ 填充了 0 个值。 我们可以使用以下方法检查roots_:

1
2
3
(lldb) expr roots_
(lldb) expr RootListIndex::kRootListLength
(int) $16 = 509

现在在这个阶段它们都是 0,那么这个数组什么时候会被填充?
这些将在 Isolate::Init 中发生:

1
2
3
4
5
6
7
8
  heap_.SetUp()
if (!create_heap_objects) des->DeserializeInto(this);

void StartupDeserializer::DeserializeInto(Isolate* isolate) {
-> 17 Initialize(isolate);
startup-deserializer.cc:37

isolate->heap()->IterateSmiRoots(this);

这将委托给调用 Heap::ConfigureHeap 的 ConfigureHeapDefaults():

1
2
3
4
5
enum RootListIndex {
kFreeSpaceMapRootIndex,
kOnePointerFillerMapRootIndex,
...
}
1
2
3
4
(lldb) expr heap->RootListIndex::kFreeSpaceMapRootIndex
(int) $3 = 0
(lldb) expr heap->RootListIndex::kOnePointerFillerMapRootIndex
(int) $4 = 1