我們正在舊金山招聘系統工程師,共同打造 JavaScript 的未來!
當套件在 Node.js 中可以運作,但在 Bun 中卻無法運作時,我們會將其視為 Bun 的錯誤。
原生 C 和 C++ API 經常在 JavaScript 生態系統中使用,用於效能至關重要的函式庫,例如 2D Canvas、資料庫驅動程式、CPU 偵測等等。Bun 和 Node.js 實作了 Node-API
(napi),這是建議使用的引擎獨立 C API,用於與 JavaScript 介接。
但是,許多流行的原生模組直接使用 Node.js 暴露的 V8 引擎 API。在撰寫本文時,要求支援 V8 API 的問題在 Bun 的追蹤器上,依按讚數排序是第 11 高的未解決問題。
這對 Bun 來說是一項挑戰,因為我們使用 JavaScriptCore 作為 JavaScript 引擎 (Safari 使用的引擎),這與使用 V8 (Chrome 使用的引擎) 的 Node.js 不同。JavaScriptCore 是一個完全不同的 JavaScript 引擎實作,具有不同的設計選擇。
JavaScriptCore | V8 | |
---|---|---|
垃圾回收機制 | 非移動式、保守式 | 移動式、精確式 |
值表示法 | JSC::JSValue | v8::Local<T> |
值生命週期 | 堆疊掃描 | Handle 作用域 |
...還有很多很多... | ... | ... |
先前,當在 Bun 中載入其中一個依賴 V8 C++ API 的套件時,您有時會看到像這樣的錯誤
bun ./index.js
dyld[26946]: missing symbol called
自 Bun v1.1.25 以來,我們一直在使用 JavaScriptCore 實作一個穩定成長的 V8 C++ API 清單。這解鎖了 Bun 中流行的 Node.js 原生模組,例如 cpu-features
。以下是我們的做法。
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 數字
- 傳回結果
但它們以不同的方式執行。
值表示法,第 1 部分
第一個主要差異之一是 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[]
,這就是為什麼它看起來像陣列索引)。如果我們需要,該物件也是我們如何在兩個 API 中存取this
值的方式。 - 我們的 JSC 函數傳回
EncodedJSValue
,這是我們如何指示我們傳回給 JavaScript 的內容。在 V8 中,我們的函數本身傳回void
,我們使用info.GetReturnValue().Set(...)
來傳回值 (如果我們不呼叫此函數,則預設為undefined
)。 - 我們的 JSC 函數使用
JSC_HOST_CALL_ATTRIBUTES
進行註解。此巨集展開為編譯器特定的屬性,可確保我們函數的呼叫慣例正確。JSC 對於 JIT 編譯的機器碼使用 Unix 的 System V 呼叫慣例。這使得它們的程式碼產生更簡單,因為它們不必產生使用兩種不同呼叫慣例的程式碼,但這也意味著 JavaScript 呼叫的任何原生函數都需要使用正確的呼叫慣例。否則,原生函數會在與 JIT 編譯程式碼放置引數不同的暫存器中尋找其引數。V8 沒有與此等效的機制,因為它始終對給定的平台使用標準呼叫慣例。 - 我們的 JSC 函數會獲得指向
JSGlobalObject
的指標。這是一個類別,它封裝了 JavaScript 可存取的全域作用域,以及與 JSC 互動的原生函數使用的全域狀態。Bun 子類別化了 JSC 的通用版本,以新增我們自己的全域可存取功能,例如Bun
物件 (Zig::GlobalObject
為 7,528 位元組!)。我們還將看到JSC::VM
的用法,您可以從全域物件存取它,並封裝更多 JavaScript 程式碼的執行狀態。雖然兩者都不完全等效,但我們看到的兩個類似的 V8 類型是Isolate
和Context
。對我們來說,重要的細節是,一個 isolate 可以包含多個 context,但這些 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 標頭中宣告為 inline,但將實作複製到 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 中也是如此,這本應讓我當時感到擔憂)。相反,它們將接收一個 this
指標,該指標實際上只是一個 JSValue
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
是一個 inline 函數,它執行一些檢查,然後在檢查失敗時呼叫 v8::Object::SlowGetInternalField
。由於 inline 函數將從 V8 標頭檔取得其定義,並且該程式碼會編譯到任何原生模組中,因此我們無法控制它做什麼。我曾希望通常會採用慢速路徑,並且它會呼叫我們可以控制的 SlowGetInternalField
函數 (它不是 inline 函數)。但是,當我第一次測試內部欄位時,我在 V8 的 inline 函數內部遇到了區段錯誤。
讓我們看一下 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);
}
}
幸運的是,在偵錯模式下建置我的原生模組後,我仍然可以逐步執行 inline 函數,並看到正確的堆疊追蹤,就好像該函數不是 inline 函數一樣。崩潰發生在 ValueAsAddress
內部,它也是 inline 函數。讓我們稍微重構這個函數,以便更容易了解正在發生的事情。在 Node.js 的標頭中,既未定義 V8_ENABLE_CHECKS
也未定義 V8_COMPRESS_POINTERS
,因此將展開外部 #ifndef
中的程式碼,但不會展開內部 #ifdef
中的程式碼。internal::Address
是 uintptr_t
的類型別名。以下是此函數呼叫的一些 inline 函數的定義
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
- 將 11 新增到
map
,並從該位置讀取 2 個位元組以取得實例類型
請記住,在目前的實作中,this
根本不是指向 Address
的指標,而是一個 JSValue
。因此,V8 會盲目地取消參考 JSValue
看起來像指標的任何內容,再次取消參考它以尋找 map
是什麼,然後取消參考該指標以取得實例類型。請記住,這一切都是編譯到每個原生模組中的程式碼。我們無法更改任何內容。
在這一點上,我認為我最好的希望是盡可能設定這些指標,使 V8 能夠找到一個 instance_type
,使 CanHaveInternalField
傳回 false
(如果該函數傳回 true
,則 GetInternalField
會在直接指標偏移量處讀取內部欄位,這比我們看到的所有其他內容更難在 JSC 下運作)。然後,它將始終呼叫 SlowGetInternalField
,我可以控制其實作,因為它不是 inline 函數。但即使達到這一點也很困難,並且需要拋棄我們最初將 JSValue
直接儲存在 Local
中的方案。
讓我們回到基本知識,更仔細地了解 JSValue
和 Local
的表示方式。
值表示法,第 2 部分
JavaScript 引擎面臨著表示動態類型系統的挑戰,在動態類型系統中,值可能會隨時更改其類型,同時在靜態類型系統中實作,在靜態類型系統中,編譯器必須知道每個變數的類型。
數字的表示形式也是 JavaScript 引擎中的一個重要選擇。根據規範,JavaScript 數字的行為類似於 IEEE 754 雙精度浮點數 (C 或 C++ 中的 double
,以及 Zig 或 Rust 中的 f64
)。位元運算確實必須將數字轉換為整數才能操作其位元,但此類運算的結果仍然需要表現得像 double ((5 << 1) / 4
必須為 2.5
)。因此,最簡單的實作是始終將 JavaScript 數字儲存為 double,並且僅在必要時將它們短暫地轉換為整數,然後再轉換回 double。
但是,將所有數字儲存為 double 會產生嚴重的效能成本。Double 的運算速度比整數慢得多,而且典型程式使用的大多數數字無論如何都是整數 (例如陣列索引)。因此,幾乎每個 JavaScript 引擎都包含一個最佳化,以便在可能的情況下將數字表示為整數。這可以在不違反規範的情況下完成,只要您小心在必要時將數字轉換回 double 即可。您還需要確保不允許表示 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 裝箱的技術。這利用了 NaN 的雙精度表示形式中存在 51 個未使用位元的事實。函式庫和硬體浮點實作通常將這些位元設定為零,因此這些位元中的任何非零值都可以用於編碼非浮點值。JSC 為非浮點表示形式設定了這些位元中的上兩個位元,以便可以將較低的 49 個位元設定為任何內容,而不會與真實的 NaN 混淆。JSC 使用這 49 個位元的最上位元來區分 48 位元指標 (我們支援的任何 64 位元平台實際上都沒有使用超過 48 位元進行記憶體定址) 和 32 位元整數。false
、true
、undefined
和 null
表示為特定的無效指標。
我上面描述的內容實作了 NaN 裝箱,其中非 double 值表示為 NaN 的有效雙精度編碼。但是,這樣做會強制在所有非 double 值中設定某些高位元,這對於指標來說是不方便的,因為我們必須在取消參考指標之前將這些位元歸零。JSC 的最後一個技巧是從如上所述編碼的雙精度值中減去 2^49。這使得我們討論過的值類型落入以下範圍
Pointer { 0000:PPPP:PPPP:PPPP
/ 0002:****:****:****
Double { ...
\ FFFC:****:****:****
Integer { FFFE:0000:IIII:IIII
由於指標現在已將其上位元設定為零,因此在檢查 JSValue
是否為指標之後,可以直接使用它。Double 和整數只需要簡單的整數數學運算即可轉換為其真實值。
如果這沒有太多意義 (或者即使有意義),我建議查閱 定義 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|
您可能會注意到 double 不符合此方案。V8 將 double 儲存在一個單獨的配置中,稱為 HeapNumber
。我們將在第 3 部分中探討 V8 如何處理布林值、null
和 undefined
。
若要深入了解 V8 的值表示法,請參閱 tagged.h
以及 Igor Sheludko 和 Santiago Aboy Solanes 的部落格文章,其中解釋了指標壓縮。指標壓縮是一個可選的建置時 V8 功能,即使在 64 位元平台上,它也會將標記指標的大小變更為 32 位元。Node.js 未啟用指標壓縮,因此 Bun 不必模仿該表示法,但該文章仍然包含對非壓縮方案的良好解釋,以及有關指標壓縮權衡的有趣細節。
Local 和 Handle 作用域
現在我們了解了標記指標,但是 Local
中有什麼呢?
V8 (與 JSC 不同) 使用移動式垃圾回收機制。這表示垃圾回收機制可能需要變更物件在記憶體中的位置。為此,它需要找到對物件的所有參考,並將位址變更為物件的新位置。這對於使用原始指標的 C 程式碼來說是不可行的。相反,V8 將我們討論的標記指標儲存在 Handle 作用域中。HandleScope
管理一組物件參考或控制代碼。它維護所有位址的儲存空間,垃圾回收機制可以看到這些位址。反過來,Local
儲存指向儲存在 Handle 作用域中的位址的指標。當原生函數需要從 Local
存取資料時,它必須取消參考指向位址的指標,然後取消參考該位址以轉到實際物件。當垃圾回收機制需要移動物件時,它可以簡單地搜尋所有 Handle 作用域中與物件舊位址相符的指標,並將這些指標替換為新位址。
那麼整數呢?這些仍然使用 Handle 作用域,但儲存在 Handle 作用域中的標記指標的低位元將設定為零,表示它是 32 位元有號整數,而不是指標。GC 知道它可以跳過這些控制代碼。
Handle 作用域始終儲存在堆疊記憶體中,並且它們本身也構成一個堆疊:當您建立 HandleScope
時,它會保留對前一個 Handle 作用域的參考,並且當您退出建立 HandleScope
的作用域時,它會釋放它包含的所有控制代碼,並使前一個 Handle 作用域再次處於活動狀態。但是,Handle 作用域的堆疊並不總是與原生呼叫堆疊相符,因為您始終可以在目前活動的 Handle 作用域中建立控制代碼,即使您沒有在目前的函數中建立自己的控制代碼也是如此。
Maps
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);
}
請注意 handle 作用域中儲存的所有內容如何使用上一節討論的標籤指標表示法,這就是堆積物件的記憶體位址儲存為奇數的原因。 foo_string
和 baz_number
的 handle 的最低 2 位元設定為 01
,表示它們是強指標。 但是為了取得它們指向的實際位址,我們必須將這些位元更改為零,從而得到物件的實際位置,寫在 "@" 之後。
在「堆積」下方,我們可以看到每個物件指向哪個 Map
。 這些指向 Map
的指標當然是標籤指標,因此它們也設定了最低有效位元。 字串和堆積數字當然在 map 指標之後還有更多欄位,但這些欄位(幸運的是)不是我們必須匹配的佈局的一部分,因此這裡沒有顯示。
在我們可以對 V8 API 執行任何有用的操作之前,我們需要想出一種方法,使 JSC 的類型以 V8 期望的方式適合此圖表。 我們還需要確保 JSC 的垃圾回收器可以找到原生插件已建立的所有 V8 值,以便在仍然可以存取它們時不會刪除它們。 最後,我們需要為 JSC 沒有精確公開的 V8 概念實作許多額外的類型。
接下來
在本系列的下一部分中,我們將研究 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 boxing:更詳細的理由和解釋 JSC 用於緊湊表示不同類型值的一種技術
- javascript 實作中的值表示:對生產 JavaScript 直譯器使用的不同值表示形式及其權衡的調查