我们正在旧金山招聘系统工程师,共同打造 JavaScript 的未来!
当软件包在 Node.js 中可用但在 Bun 中不可用时,我们将其视为 Bun 的一个 bug。
原生 C 和 C++ API 经常在 JavaScript 生态系统中用于性能关键型库,如 2D Canvas、数据库驱动程序、CPU 检测等。Bun 和 Node.js 都实现了 Node-API (napi),这是与 JavaScript 交互推荐的与引擎无关的 C API。
但是,许多流行的原生模块直接使用 Node.js 暴露的 V8 引擎 API。在撰写本文时,请求支持 V8 API 的 issue 按点赞数排序是 Bun 跟踪器上第 11 大的未关闭 issue。
这对 Bun 来说是一个挑战,因为我们使用 JavaScriptCore 作为 JavaScript 引擎(用于 Safari),而 Node.js 使用 V8(用于 Chrome)。JavaScriptCore 是一个完全不同的 JavaScript 引擎实现,具有不同的设计理念。
| JavaScriptCore | V8 | |
|---|---|---|
| 垃圾收集器 | 非移动、保守式 | 移动、精确式 |
| 值表示 | JSC::JSValue | v8::Local<T> |
| 值生命周期 | 堆栈扫描 | 句柄范围 |
| ...还有更多... | ... | ... |
以前,在 Bun 中加载依赖于 V8 C++ API 的包时,有时会看到类似以下的错误:
bun ./index.jsdyld[26946]: missing symbol called自 Bun v1.1.25 起,我们一直在使用 JavaScriptCore 实现一个不断增长的 V8 C++ API 列表。这使得像 cpu-features 这样的流行 Node.js 原生模块在 Bun 中可以正常工作。以下是我们的实现方法。
JavaScriptCore 与 V8 API 示例
首先,为了有一个高层次的概述,我们来看一些使用 JSC API 的示例代码及其在 V8 API 中的等效代码。我们将编写一个函数,该函数从 JavaScript 接收两个数字并返回它们的乘积。如果用户提供的参数少于或多于两个,或者任何一个参数不是数字,我们将返回 undefined。请注意,我们只展示 C++ 函数实现,但实际上需要更多的胶水代码才能将其注册为可从 JavaScript 调用。
JavaScriptCore
EncodedJSValue JSC_HOST_CALL_ATTRIBUTES multiply(JSGlobalObject* globalObject, CallFrame* callFrame) {
if (callFrame->argumentCount() != 2) {
return JSValue::encode(jsUndefined());
}
JSValue arg1 = callFrame->argument(0);
JSValue arg2 = callFrame->argument(1);
if (!arg1.isNumber() || !arg2.isNumber()) {
return JSValue::encode(jsUndefined());
}
double number1 = arg1.asNumber();
double number2 = arg2.asNumber();
EncodedJSValue returnValue = JSValue::encode(jsNumber(number1 * number2));
return returnValue;
}
V8 版本
void multiply(const FunctionCallbackInfo<Value>& info) {
Isolate* isolate = info.GetIsolate();
if (info.Length() != 2) {
return;
}
Local<Value> arg1 = info[0];
Local<Value> arg2 = info[1];
if (!arg1->IsNumber() || !arg2->IsNumber()) {
return;
}
double number1 = arg1.As<Number>()->Value();
double number2 = arg2.As<Number>()->Value();
Local<Number> returnValue = Number::New(isolate, number1 * number2);
info.GetReturnValue().Set(returnValue);
}
这些代码都执行基本相同的操作:
- 检查参数数量
- 检查参数类型
- 将两个参数转换为 C++
double - 将参数相乘
- 将结果转换为 JavaScript 数字
- 返回结果
但它们以不同的方式执行。
值表示,第一部分
第一个主要区别是 JavaScript 值在两个引擎中的表示方式。JSC 使用 JSValue,这是一个 8 字节(Bun 只支持 64 位 CPU)的类,可以表示任何 JavaScript 类型。我们还看到了 EncodedJSValue,它是一个与 JSValue 大小相同的整数,用于在 ABI 边界上传递(JSValue::encode 只是重新解释相同的位作为不同的类型)。
另一方面,在 V8 中,通常会看到模板类 Local<T>,它也是 8 字节。Local<T> 重载了 T* operator*() 和 T* operator->(),这使得原生代码可以将其视为指向 T 的指针。因此,请注意 V8 代码中 . 和 -> 的用法:. 表示我们正在调用 Local 上的函数,而 -> 表示我们正在调用被包装类型上的函数。Local<Value> 是表示任何 JavaScript 可以使用的值的最通用形式。在对 Local<Value> 执行几乎任何操作之前,您需要将其转换为某种特定类型的 Local。在这里,我们调用
bool Value::IsNumber()来检查两个参数的类型Local<S> Local<T>::As<S>()将它们强制转换为Local<Number>(如果它们不是数字,这将是未定义行为)double Number::Value()从 JavaScript 数字中提取 C++double
API 比较
我们的 V8 值是 Local 这个事实具有重要的生命周期影响,但我们将在稍后详细讨论这一点,以及 JSValue 和 Local 的实现细节。现在,让我们回顾一下 multiply 函数,并讨论 V8 和 JSC 之间的一些其他相似之处和区别。
- 两者都接收一个对象(
CallFrame和FunctionCallbackInfo)来表示从 JavaScript 传递到我们函数的信息。我们同时使用它们来获取参数数量和参数本身(V8 在第二步中使用operator[],这就是为什么它看起来像数组索引)。如果我们有需要,该对象也是我们获取this值的方式。 - 我们的 JSC 函数返回
EncodedJSValue,这是我们指示返回给 JavaScript 的内容的方式。在 V8 中,我们自己的函数返回void,我们使用info.GetReturnValue().Set(...)来返回一个值(如果我们不调用它,它默认为undefined)。 - 我们的 JSC 函数被注解了
JSC_HOST_CALL_ATTRIBUTES。这个宏会展开成一个特定于编译器的属性,以确保我们函数的调用约定正确。JSC 使用 Unix 的 System V 调用约定来处理 JIT 编译的机器码。这使得它们的代码生成更简单,因为它们不必生成使用两种不同调用约定的代码,但这也意味着 JavaScript 调用任何原生函数都需要使用正确的调用约定。否则,原生函数会去查找寄存器中不属于 JIT 编译代码放置参数的寄存器。V8 没有等效的,因为它总是使用平台标准的调用约定。 - 我们的 JSC 函数接收一个指向
JSGlobalObject的指针。这是一个类,它封装了 JavaScript 可以访问的全局作用域,以及与 JSC 交互的原生函数使用的全局状态。Bun 对此的通用版本进行子类化,以添加我们自己的全局可访问功能,例如Bun对象(Zig::GlobalObject为 7,528 字节!)。我们还会看到JSC::VM的用法,您可以从全局对象访问它,它封装了更多 JavaScript 代码的执行状态。虽然两者不完全等同,但我们在 V8 中看到的两个相似的类型是Isolate和Context。对我们来说,重要的细节是,一个 isolate 可以包含多个 context,但在给定时间只有一个 context 正在运行 JavaScript(因为两个线程不能共享一个 isolate)。V8 的这篇文章 提供了关于区别的更多细节。Isolate 不会直接传递到我们的 V8 原生函数中,但我们可以很容易地从FunctionCallbackInfo中获取它,并且如果我们有需要,我们也可以要求 isolate 告诉我们当前的 context。 - 在 V8 中,我们必须传递 isolate 才能将乘法结果转换为
Local<Number>,而在 JSC 中,我们没有向jsNumber传递任何额外的参数。在 V8 中,即使对于布尔值、null和undefined这样的值,我们也必须这样做,而在 JSC 中,这些都有简单的函数直接返回JSValue,并且不依赖于JSGlobalObject或VM。我们稍后会看到为什么这些看似简单的函数需要访问当前的 isolate。
实现 V8 函数,第一次尝试
由于 V8 的 Local 和 JSC 的 JSValue 都是 8 字节,我首先尝试简单地重新解释它们。Local<T>::operator*() 和 Local<T>::operator->() 只是将内容作为指针返回(由于它们在 V8 头文件中声明为内联,我们实际上无法更改这些函数的实现,但将实现复制到 Bun 的代码中将有助于我们编写其他处理 Local 的 V8 函数)。
namespace v8 {
template<class T>
class Local final {
public:
T* ptr;
T* operator*() const { return ptr; }
};
}
在我们的实现中,T* 实际上并不是指向 T 的指针。相反,它将是一个重新解释的 JSValue。这意味着我们表示 V8 值的类(如 Number)实际上不包含任何字段(V8 也是如此,这当时应该引起我的警觉)。相反,它们将接收一个实际上只是 JSValue 的 this 指针。
class Number {
public:
BUN_EXPORT static Local<Number> New(Isolate* isolate, double value);
BUN_EXPORT double Value() const;
};
Local<Number> Number::New(Isolate* isolate, double value)
{
JSC::JSValue jsv = JSC::jsDoubleNumber(value);
JSC::EncodedJSValue encoded = JSC::JSValue::encode(jsv);
auto ptr = reinterpret_cast<Number*>(encoded);
return Local<Number> { ptr };
}
double Number::Value() const
{
auto encoded = reinterpret_cast<JSC::EncodedJSValue>(this);
JSC::JSValue jsv = JSC::JSValue::decode(encoded);
return jsv.asNumber();
}
也许令人惊讶的是,这实际上奏效了!并且在一段时间内一直奏效。我为这些 API 编写了测试,这些测试在 Node.js 和 Bun 中打印出相同的结果(尽管它们必须包装在 Node-API 函数中,因为我还没有实现 Node.js 加载原生模块的方式)。可悲的是,这个设计的根本缺陷在我尝试实现对象时变得显而易见。
问题
V8 有一个名为 ObjectTemplate 的类,它可以由原生代码创建,然后用于构建多个对象,这些对象可以传递给 JavaScript。这些对象支持称为内部字段的功能,这些字段由数字索引,可以由原生代码访问,但不能由 JavaScript 访问。您可以使用 ObjectTemplate 来配置它创建的对象应有多少个内部字段。原生模块可以使用内部字段将“句柄”对象返回给 JavaScript,这些对象表示原生资源,而不会让 JavaScript 干扰这些资源的内部状态。
当我第一次尝试测试访问对象的内部字段时,问题变得显而易见(好吧,可以说是显而易见)。我之前注意到 v8::Object::GetInternalField 是一个内联函数,它执行一些检查,然后在检查失败时调用 v8::Object::SlowGetInternalField。由于内联函数将从 V8 头文件中获取其定义,并且该代码被编译到任何原生模块中,因此我们无法控制它的行为。我曾希望慢速路径通常会被采用,并且它会调用我们可以控制的 SlowGetInternalField 函数(它不是内联的)。但当我第一次测试内部字段时,我在 V8 的内联函数中遇到了一个段错误。
让我们看一下 V8 的实现。
namespace v8 {
class V8_EXPORT Object : public Value {
public:
// ...
V8_INLINE Local<Data> GetInternalField(int index);
// ...
};
// ...
Local<Data> Object::GetInternalField(int index) {
#ifndef V8_ENABLE_CHECKS
using A = internal::Address;
using I = internal::Internals;
A obj = internal::ValueHelper::ValueAsAddress(this);
// Fast path: If the object is a plain JSObject, which is the common case, we
// know where to find the internal fields and can return the value directly.
int instance_type = I::GetInstanceType(obj);
if (I::CanHaveInternalField(instance_type)) {
int offset = I::kJSAPIObjectWithEmbedderSlotsHeaderSize +
(I::kEmbedderDataSlotSize * index);
A value = I::ReadRawField<A>(obj, offset);
#ifdef V8_COMPRESS_POINTERS
// We read the full pointer value and then decompress it in order to avoid
// dealing with potential endiannes issues.
value = I::DecompressTaggedField(obj, static_cast<uint32_t>(value));
#endif
auto isolate = reinterpret_cast<v8::Isolate*>(
internal::IsolateFromNeverReadOnlySpaceObject(obj));
return Local<Data>::New(isolate, value);
}
#endif
return SlowGetInternalField(index);
}
}
幸运的是,通过以调试模式构建我的原生模块,我仍然可以进入内联函数并像函数没有内联一样看到正确的堆栈跟踪。崩溃发生在 ValueAsAddress 中,它也是内联的。让我们重构一下这个函数,以便更容易理解它的工作原理。在 Node.js 的头文件中,既没有定义 V8_ENABLE_CHECKS,也没有定义 V8_COMPRESS_POINTERS,因此将展开外层 #ifndef 中的代码,但不会展开内层 #ifdef 中的代码。internal::Address 是 uintptr_t 的类型别名。以下是它调用的几个内联函数的定义:
ValueAsAddress
template <typename T>
V8_INLINE static Address ValueAsAddress(const T* value) {
return *reinterpret_cast<const Address*>(value);
}
GetInstanceType
V8_INLINE static int GetInstanceType(Address obj) {
Address map = ReadTaggedPointerField(obj, kHeapObjectMapOffset);
#ifdef V8_MAP_PACKING
// omitted
#endif
return ReadRawField<uint16_t>(map, kMapInstanceTypeOffset);
}
ReadRawField 和 ReadTaggedPointerField
template <typename T>
V8_INLINE static T ReadRawField(Address heap_object_ptr, int offset) {
Address addr = heap_object_ptr + offset - kHeapObjectTag;
#ifdef V8_COMPRESS_POINTERS
// omitted
#endif
return *reinterpret_cast<const T*>(addr);
}
V8_INLINE static Address ReadTaggedPointerField(Address heap_object_ptr,
int offset) {
#ifdef V8_COMPRESS_POINTERS
// omitted
#else
return ReadRawField<Address>(heap_object_ptr, offset);
#endif
}
在 Bun 支持的平台上,kHeapObjectTag 为 1,kHeapObjectMapOffset 为 0,kMapInstanceTypeOffset 为 12。
让我们首先重写 GetInternalField 以直接使用 ReadRawField。
Local<Data> Object::GetInternalField(int index) {
using A = internal::Address;
using I = internal::Internals;
A obj = internal::ValueHelper::ValueAsAddress(this);
int instance_type = I::GetInstanceType(obj);
A obj = *reinterpret_cast<const A*>(this);
A map = ReadRawField<Address>(obj, 0);
int instance_type = ReadRawField<uint16_t>(map, 12);
if (I::CanHaveInternalField(instance_type)) {
// omitted
}
return SlowGetInternalField(index);
}ReadRawField 所做的只是从传入的地址减去 kHeapObjectTag,加上偏移量,然后将结果地址转换为指针并对其进行解引用。所以我们可以进一步重写为:
Local<Data> Object::GetInternalField(int index) {
using A = internal::Address;
using I = internal::Internals;
A obj = *reinterpret_cast<const A*>(this);
A map = ReadRawField<Address>(obj, 0);
int instance_type = I::GetInstanceType(obj);
A map = *reinterpret_cast<const A*>(obj - 1 + 0);
int instance_type = *reinterpret_cast<const uint16_t*>(map - 1 + 12);
if (I::CanHaveInternalField(instance_type)) {
// omitted
}
return SlowGetInternalField(index);
}所以,我们...
- 将
this(其类型为Object*,尽管它实际上不是)重新解释为指向Address的指针,并解引用该指针以获取obj。 - 从
obj中减去 1,并从该位置读取一个地址以获取map。 - 在
map中加上 11,并从该位置读取 2 个字节以获取实例类型。
请记住,在当前实现中,this 根本不是指向 Address 的指针,它是一个 JSValue。因此,V8 将盲目地解引用 JSValue 看起来像指针的任何内容,再解引用一次以找到 map,然后再次解引用它以获取实例类型。并且请记住,所有这些代码都被编译到每个原生模块中。我们无法更改其中任何一个。
此时,我认为我最好的希望是某种程度上设置这些指针,以便 V8 找到一个使 CanHaveInternalField 返回 false 的 instance_type(如果该函数返回 true,GetInternalField 将在直接指针偏移量处读取内部字段,这比我们已经看到的其他任何东西都更难在 JSC 下实现)。然后,它将始终调用 SlowGetInternalField,因为它不是内联的,我可以控制其实现。但是,即使达到这一点也很困难,而且需要放弃我们最初的将 JSValue 直接存储在 Local 中的方案。
让我们回到基本知识,更仔细地研究 JSValue 和 Local 的表示方式。
值表示,第二部分
JavaScript 引擎面临着表示动态类型系统的挑战,其中一个值可能随时更改其类型,同时又在静态类型系统中实现,其中每个变量的类型都必须由编译器知道。
数字的表示也是 JavaScript 引擎中的一个重要选择。根据规范,JavaScript 数字的行为类似于 IEEE 754 双精度浮点数(C 或 C++ 中的 double,Zig 或 Rust 中的 f64)。按位运算确实需要将数字转换为整数来操作其位,但这类操作的结果仍然需要像双精度浮点数一样((5 << 1) / 4 必须是 2.5)。因此,最简单的实现是始终将 JavaScript 数字存储为双精度浮点数,并只短暂地将其转换为整数然后再转换回双精度浮点数。
然而,将所有数字存储为双精度浮点数会产生严重的性能成本。双精度浮点数的操作速度比整数慢得多,而且典型的程序使用的大多数数字本身就是整数(如数组索引)。因此,几乎每个 JavaScript 引擎都包含一项优化,可以在可能的情况下将数字表示为整数。这可以在不违反规范的情况下完成,只要您小心地在必要时将数字转换回双精度浮点数。您还需要确保不允许表示 double 无法表示的值。例如,使用 64 位整数是不合适的,因为对于非常大的整数,它们的精度大于 double。在 64 位平台上,并且当 V8 的指针压缩禁用时,JSC 和 V8 都使用 32 位有符号整数来进行此优化。
V8 和 JSC 都提出了值表示,可以在 8 字节内区分常见类型的值。我们先来看看 JSC 的解决方案。
JSValue
JSValue 占 64 位,可以表示以下任何一种:
- 64 位双精度浮点数
- 一个 48 位
JSCell指针。JSCell是分配在堆上的任何 JavaScript 值的基类。 - 一个 32 位有符号整数
false、true、undefined或null
它们是如何做到的?JSC 使用一种称为NaN boxing的技术。这利用了这样一个事实:在 NaN 的双精度表示中,有 51 个未使用的位。库和硬件浮点实现通常将这些设置为零,因此这些位中的任何非零值都可以用于编码非浮点值。JSC 将这些位的最高两位设置为非浮点表示,这样低 49 位可以设置为任何值而不会与真正的 NaN 混淆。JSC 使用这 49 位中的最高位来区分 48 位指针(我们支持的 64 位平台上,内存寻址实际上不会超过 48 位)和 32 位整数。false、true、undefined 和 null 被表示为特定的无效指针。
上面我描述的是实现了 NaN boxing,其中非双精度值被表示为有效的双精度 NaN 编码。然而,这样做会迫使所有非双精度值的高位被设置,这对指针来说很不方便,因为在解引用指针之前我们必须将这些位清零。JSC 的最后一个技巧是,从上面描述的双精度值编码中减去 2^49。这使得我们讨论过的类型落入以下范围:
Pointer { 0000:PPPP:PPPP:PPPP
/ 0002:****:****:****
Double { ...
\ FFFC:****:****:****
Integer { FFFE:0000:IIII:IIII
由于指针现在将高位设置为零,在检查 JSValue 是否为指针后,它就可以直接使用。双精度浮点数和整数只需要简单的整数数学运算即可转换为其真实值。
如果这没有多大意义(即使有),我建议查阅 定义 JSValue 的头文件中的此注释,它非常好地解释了该方案。
带标签的指针
V8 使用一种称为指针标记的方案。指针的最低 2 位用于指示值的类型。这没问题,因为 V8 的垃圾收集器分配的所有对象至少都是 4 字节对齐的,所以有效地址中的最低 2 位总是零。在 64 位平台上(Bun 支持的唯一平台),如果值是指针,则最低位设置为 1,如果指针是弱指针,则第二低位设置为 1。如果值不是指针,则最低 32 位全部设置为零,而高 32 位存储有符号整数(V8 称之为“小整数”或“Smi”)
|----- 32 bits -----|----- 32 bits -----|
Pointer: |________________address______________w1|
Smi: |____int32_value____|0000000000000000000|
您可能会注意到双精度浮点数不符合此方案。V8 将双精度浮点数存储在称为 HeapNumber 的单独分配中。我们将在第三部分中介绍 V8 如何处理布尔值、null 和 undefined。
要了解更多关于 V8 值表示的信息,请参阅 tagged.h 以及 Igor Sheludko 和 Santiago Aboy Solanes 关于指针压缩的博客文章。指针压缩是一个可选的编译时 V8 功能,它将带标签指针的大小更改为 32 位,即使在 64 位平台上也是如此。Node.js 不启用指针压缩,因此 Bun 不必模仿该表示,但该文章仍然很好地解释了非压缩方案以及指针压缩权衡的一些有趣细节。
Local 和 Handle Scope
现在我们理解了带标签的指针,那么 Local 中有什么呢?
V8(与 JSC 不同)使用移动垃圾收集器。这意味着垃圾收集器可能需要更改对象在内存中的位置。要做到这一点,它需要找到所有指向该对象的引用,并更改指向该对象新位置的地址。这对于使用原始指针的 C 代码来说是不可行的。取而代之的是,V8 将我们上面讨论的带标签指针存储在句柄范围中。HandleScope 管理一组对象引用或句柄。它维护所有地址的存储,垃圾收集器可以看到这些地址。反过来,Local 存储指向句柄范围中存储的地址的指针。当原生函数需要访问 Local 中的数据时,它必须解引用指向地址的指针,然后解引用该地址以转到实际的对象。当垃圾收集器需要移动对象时,它可以简单地查找所有句柄范围中与对象旧地址匹配的指针,并将这些指针替换为新地址。
那么整数呢?它们仍然使用句柄范围,但存储在句柄范围中的带标签指针将最低位设置为零,表示它是一个 32 位有符号整数而不是指针。GC 知道它可以跳过这些句柄。
Handle 作用域始终存储在栈内存中,它们本身也构成了一个栈:当你创建一个 HandleScope 时,它会保留对前一个的引用,当你退出创建 HandleScope 的作用域时,它会释放它包含的所有句柄,并使前一个作用域重新激活。然而,句柄作用域的栈并不总是与原生调用栈匹配,因为即使你没有在当前函数中创建自己的 HandleScope,你也可以随时在当前激活的句柄作用域中创建句柄。
Map
V8 使用称为 Map 的对象(不要与 JavaScript 的 Map 混淆)来表示堆分配的 JavaScript 类型的布局。JSC 有一个类似的概念,称为 Structure。幸运的是,我们不需要担心太多的实现细节;只需要足够支持 GetInstanceType 函数即可。从该函数的代码中,我们知道:
- 每个对象的第一字段都是一个指向其
Map的标记指针。 Map在偏移量 12 处有一个两字节整数,用于表示实例类型。
由于 Map 是堆对象,每个 Map 的第一个字段也包含一个指向其 Map 的指针。这些都指向同一个 Map,即描述 Map 布局的 Map。我还没有遇到过内联 V8 函数依赖于此的情况,但我已经实现了该字段,因为它并不难。
整合起来:V8 内存布局
例如,让我们看看在 Node.js 中运行以下代码(在 baz 返回之前)后内存的样子:
void foo(const FunctionCallbackInfo<Value>& info) {
Isolate* isolate = info.GetIsolate();
HandleScope foo_scope(isolate);
Local<String> foo_string = String::NewFromUtf8(isolate, "hello").ToLocalChecked();
bar(isolate);
}
void bar(Isolate* isolate) {
Local<Number> bar_number = Number::New(isolate, 2.0);
baz(isolate);
}
void baz(Isolate* isolate) {
HandleScope baz_scope(isolate);
Local<Number> baz_number = Number::New(isolate, 3.5);
}
请注意,句柄作用域中存储的所有内容都使用了上一节讨论的标记指针表示法,这就是为什么堆对象的内存地址存储为奇数的原因。foo_string 和 baz_number 的句柄的最低 2 位设置为 01,表示它们是强指针。但是,要获取它们实际指向的地址,我们需要将这些位更改为零,从而得到对象实际写入 "@" 之后的地址。
在“堆”(Heap)下,我们可以看到每个对象指向哪个 Map。这些指向 Map 的指针当然是标记指针,因此它们也具有最低有效位。字符串和堆数字自然在其 Map 指针之后还有更多字段,但这些字段(幸运的是)不属于我们必须匹配的布局,因此在此未显示。
在能够使用 V8 API 做任何有用的事情之前,我们需要找到一种方法,使 JSC 的类型能够像 V8 期望的那样适配此图。我们还需要确保 JSC 的垃圾回收器能够找到本机插件创建的所有 V8 值,这样它就不会在它们仍然可以访问时删除它们。最后,我们需要为 V8 中 JSC 未精确公开的概念实现许多额外的类型。
即将推出
在本系列 的下一部分 中,我们将探讨 Bun 如何在避免不必要的减速或内存浪费的同时,操纵 JSC 类型以匹配 V8 期望的布局。
链接
- 使用 V8 进行嵌入入门:从用户角度概览许多 V8 API,并包含一些内部细节。
- V8 中的指针压缩:解释 V8 中有和无指针压缩(即使在 64 位系统上,标记指针也仅占用 32 位)的值表示,以及优化压缩和解压缩指针的例程的实现细节。Node.js 未启用指针压缩,但 Chromium 和 Electron 已启用。
tagged.h:V8 指针标记系统的实现。- V8 绑定设计:从 V8 在 Chromium 中的使用角度描述了 Isolates 和 Contexts 等概念之间的区别。
JSCJSValue.h:详细说明了 JSC 基本值类型的 32 位和 64 位(仅后者与 Bun 相关)编码。- Crafting Interpreters 关于 NaN 盒装的讨论:更详细地论证和解释了 JSC 用于紧凑表示不同类型值的一种技术。
- JavaScript 实现中的值表示:对生产 JavaScript 解释器使用的不同值表示及其权衡的调查。