開始嵌入 V8

此文件介紹一些 V8 的主要概念,並提供一個「hello world」範例,讓您開始使用 V8 程式碼。

受眾 #

此文件適用於希望在 C++ 應用程式中嵌入 V8 JavaScript 引擎的 C++ 程式設計師。它能協助您讓自己的應用程式的 C++ 物件和方法可供 JavaScript 使用,並讓 JavaScript 物件和函式可供您的 C++ 應用程式使用。

Hello world #

讓我們來看看一個 Hello World 範例,它會將 JavaScript 陳述式當成字串引數,以 JavaScript 程式碼執行它,並將結果列印到標準輸出。

首先,一些主要概念

這些概念會在 進階指南 中更詳細地討論。

執行範例 #

請按照下列步驟自行執行範例

  1. 按照 Git 指示 下載 V8 原始程式碼。

  2. 此 hello world 範例的指示最後一次測試時使用的是 V8 v11.9。您可以使用 git checkout branch-heads/11.9 -b sample -t 檢出此分支。

  3. 使用輔助指令碼建立建置組態

    tools/dev/v8gen.py x64.release.sample

    您可以執行下列動作來檢查和手動編輯建置組態

    gn args out.gn/x64.release.sample
  4. 在 Linux 64 系統上建置靜態函式庫

    ninja -C out.gn/x64.release.sample v8_monolith
  5. 編譯 hello-world.cc,連結到建置過程中建立的靜態函式庫。例如,在 64 位元 Linux 上使用 GNU 編譯器

    g++ -I. -Iinclude samples/hello-world.cc -o hello_world -fno-rtti -lv8_monolith -lv8_libbase -lv8_libplatform -ldl -Lout.gn/x64.release.sample/obj/ -pthread -std=c++17 -DV8_COMPRESS_POINTERS -DV8_ENABLE_SANDBOX
  6. 對於較複雜的程式碼,V8 在沒有 ICU 資料檔的情況下會失敗。將此檔案複製到二進位檔儲存的位置

    cp out.gn/x64.release.sample/icudtl.dat .
  7. 在命令列執行 hello_world 可執行檔。例如,在 Linux 中的 V8 目錄中執行

    ./hello_world
  8. 它會印出 Hello, World!。太棒了!

如果你正在尋找與主分支同步的範例,請查看檔案 hello-world.cc。這是一個非常簡單的範例,而且你可能不只想要執行腳本作為字串。以下進階指南包含更多 V8 嵌入者的資訊。

更多範例程式碼 #

以下範例是作為原始碼下載的一部分提供。

process.cc #

此範例提供必要的程式碼來擴充一個假設的 HTTP 要求處理應用程式,例如,它可以是網路伺服器的一部分,以便它可以編寫腳本。它將 JavaScript 腳本作為引數,該腳本必須提供一個稱為 Process 的函式。JavaScript Process 函式可以用於收集資訊,例如虛構的網路伺服器所提供的每個頁面的點閱次數。

shell.cc #

此範例將檔案名稱作為引數,然後讀取並執行其內容。包含一個命令提示字元,你可以在其中輸入 JavaScript 程式碼片段,然後執行這些片段。在此範例中,也透過使用物件和函式範本將其他函式(例如 print)新增到 JavaScript。

進階指南 #

現在你已經熟悉將 V8 用作獨立的虛擬機器,以及一些 V8 的主要概念,例如控制代碼、範圍和內容,讓我們進一步討論這些概念,並介紹一些其他概念,這些概念對於將 V8 嵌入你自己的 C++ 應用程式中至關重要。

V8 API 提供用於編譯和執行腳本、存取 C++ 方法和資料結構、處理錯誤和啟用安全性檢查的函式。你的應用程式可以使用 V8,就像使用任何其他 C++ 函式庫一樣。你的 C++ 程式碼透過包含標頭 include/v8.h,透過 V8 API 存取 V8。

控制代碼和垃圾回收 #

控制代碼提供對 JavaScript 物件在堆積中的位置的參考。V8 垃圾回收器回收不再可以存取的物件所使用的記憶體。在垃圾回收過程中,垃圾回收器通常會將物件移至堆積中的不同位置。當垃圾回收器移動物件時,垃圾回收器也會更新所有參考該物件的控制代碼,以使用物件的新位置。

如果無法從 JavaScript 存取物件,且沒有任何控制代碼參照它,則該物件會被視為垃圾。垃圾收集器會不時移除所有被視為垃圾的物件。V8 的垃圾收集機制是 V8 效能的關鍵。

有幾種類型的控制代碼

當然,每次建立物件時建立一個區域處理程序會產生很多處理程序!這時處理程序範圍就非常有用。您可以將處理程序範圍視為一個包含大量處理程序的容器。當呼叫處理程序範圍的解構函式時,在該範圍內建立的所有處理程序都會從堆疊中移除。正如您所預期的,這會導致處理程序指向的物件符合垃圾收集器從堆中刪除的資格。

回到 我們非常簡單的 Hello World 範例,在以下圖表中,您可以看到處理程序堆疊和堆配置的物件。請注意,Context::New() 會傳回一個 Local 處理程序,而我們會根據它建立一個新的 Persistent 處理程序,以示範 Persistent 處理程序的用法。

當呼叫解構函式 HandleScope::~HandleScope 時,處理程序範圍會被刪除。如果沒有其他對它們的參照,則在下次垃圾收集時,處理程序範圍中處理程序所參照的物件符合移除資格。垃圾收集器也可以從堆中移除 source_objscript_obj 物件,因為它們不再被任何處理程序參照或從 JavaScript 存取。由於內容處理程序是一個持久處理程序,因此在退出處理程序範圍時不會將其移除。移除內容處理程序的唯一方法是明確呼叫其上的 Reset

注意:在本文檔中,「處理程序」一詞是指區域處理程序。在討論持久處理程序時,會完整使用該術語。

了解此模型的一個常見陷阱非常重要:您無法直接從宣告處理程序範圍的函式傳回區域處理程序。如果您這樣做,您嘗試傳回的區域處理程序將會在函式傳回之前立即被處理程序範圍的解構函式刪除。傳回區域處理程序的正確方法是建立一個 EscapableHandleScope,而不是 HandleScope,並在處理程序範圍上呼叫 Escape 方法,傳入您要傳回其值之處理程序。以下是如何在實務中運作的範例

// This function returns a new array with three elements, x, y, and z.
Local<Array> NewPointArray(int x, int y, int z) {
v8::Isolate* isolate = v8::Isolate::GetCurrent();

// We will be creating temporary handles so we use a handle scope.
v8::EscapableHandleScope handle_scope(isolate);

// Create a new empty array.
v8::Local<v8::Array> array = v8::Array::New(isolate, 3);

// Return an empty result if there was an error creating the array.
if (array.IsEmpty())
return v8::Local<v8::Array>();

// Fill out the values
array->Set(0, Integer::New(isolate, x));
array->Set(1, Integer::New(isolate, y));
array->Set(2, Integer::New(isolate, z));

// Return the value through Escape.
return handle_scope.Escape(array);
}

Escape 方法會將其參數的值複製到封裝的範圍中,刪除其所有本機控制代碼,然後回傳新的控制代碼副本,可以安全地回傳。

內容 #

在 V8 中,內容是一個執行環境,允許獨立、不相關的 JavaScript 應用程式在 V8 的單一執行個體中執行。您必須明確指定要執行任何 JavaScript 程式碼的內容。

為什麼需要這樣做?因為 JavaScript 提供了一組內建的公用程式函式和物件,可以透過 JavaScript 程式碼變更。例如,如果兩個完全不相關的 JavaScript 函式都以相同的方式變更全域物件,則很可能會發生意外的結果。

就 CPU 時間和記憶體而言,建立新的執行內容似乎是一項昂貴的操作,因為必須建立許多內建物件。然而,V8 的廣泛快取確保,雖然您建立的第一個內容有些昂貴,但後續的內容便宜得多。這是因為第一個內容需要建立內建物件並剖析內建的 JavaScript 程式碼,而後續的內容只需要為其內容建立內建物件。透過 V8 快照功能(使用建置選項 snapshot=yes 啟用,這是預設值),建立第一個內容所花費的時間將會高度最佳化,因為快照包含一個序列化堆積,其中包含內建 JavaScript 程式碼的已編譯程式碼。除了垃圾回收之外,V8 的廣泛快取也是 V8 效能的關鍵。

建立內容後,您可以隨時進入和離開它。當您在內容 A 中時,您也可以進入不同的內容 B,這表示您使用 B 取代 A 作為目前的內容。當您離開 B 時,A 會還原為目前的內容。這在下方說明

請注意,每個內容的內建公用程式函式和物件會保持獨立。您可以在建立內容時選擇設定安全權杖。有關更多資訊,請參閱 安全模型 部分。

在 V8 中使用內容的動機是,瀏覽器中的每個視窗和 iframe 都可以有自己的全新 JavaScript 環境。

範本 #

範本是內容中 JavaScript 函式和物件的藍圖。您可以使用範本來包裝 JavaScript 物件中的 C++ 函式和資料結構,以便 JavaScript 腳本可以處理它們。例如,Google Chrome 使用範本來將 C++ DOM 節點包裝為 JavaScript 物件,並在全域名稱空間中安裝函式。您可以建立一組範本,然後將它們用於您建立的每個新內容。您可以擁有任意數量的範本。但是,在任何給定的內容中,您只能有一個範本的執行個體。

在 JavaScript 中,函式和物件之間有很強的二元性。要在 Java 或 C++ 中建立新類型的物件,您通常會定義一個新類別。在 JavaScript 中,您會建立一個新的函式,並使用該函式作為建構函式建立執行個體。JavaScript 物件的配置和功能與建立它的函式密切相關。這反映在 V8 範本的工作方式上。有兩種範本

下列程式碼提供建立全域物件範本和設定內建全域函式的範例。

// Create a template for the global object and set the
// built-in global functions.
v8::Local<v8::ObjectTemplate> global = v8::ObjectTemplate::New(isolate);
global->Set(v8::String::NewFromUtf8(isolate, "log"),
v8::FunctionTemplate::New(isolate, LogCallback));

// Each processor gets its own context so different processors
// do not affect each other.
v8::Persistent<v8::Context> context =
v8::Context::New(isolate, nullptr, global);

此範例程式碼取自 process.cc 範例中的 JsHttpProcessor::Initializer

存取器 #

存取器是 C++ 回呼,在 JavaScript 腳本存取物件屬性時計算並傳回值。存取器透過物件範本,使用 SetAccessor 方法設定。此方法會採用與其關聯的屬性名稱,以及在腳本嘗試讀取或寫入屬性時要執行的兩個回呼。

存取器的複雜性取決於您操作的資料類型

存取靜態全域變數 #

假設有兩個 C++ 整數變數 xy,要讓 JavaScript 在內容中使用為全域變數。為此,您需要在腳本讀取或寫入這些變數時,呼叫 C++ 存取器函式。這些存取器函式會使用 Integer::New 將 C++ 整數轉換為 JavaScript 整數,並使用 Int32Value 將 JavaScript 整數轉換為 C++ 整數。以下提供範例

void XGetter(v8::Local<v8::String> property,
const v8::PropertyCallbackInfo<Value>& info) {
info.GetReturnValue().Set(x);
}

void XSetter(v8::Local<v8::String> property, v8::Local<v8::Value> value,
const v8::PropertyCallbackInfo<void>& info) {
x = value->Int32Value();
}

// YGetter/YSetter are so similar they are omitted for brevity

v8::Local<v8::ObjectTemplate> global_templ = v8::ObjectTemplate::New(isolate);
global_templ->SetAccessor(v8::String::NewFromUtf8(isolate, "x"),
XGetter, XSetter);
global_templ->SetAccessor(v8::String::NewFromUtf8(isolate, "y"),
YGetter, YSetter);
v8::Persistent<v8::Context> context =
v8::Context::v8::New(isolate, nullptr, global_templ);

請注意,上述程式碼中的物件範本與內容同時建立。範本可以事先建立,然後用於任何數量的內容。

存取動態變數 #

在前面的範例中,變數是靜態且全域性的。如果要處理的資料是動態的,就像瀏覽器中的 DOM 樹一樣,該怎麼辦?假設 xy 是 C++ 類別 Point 上的物件欄位

class Point {
public:
Point(int x, int y) : x_(x), y_(y) { }
int x_, y_;
}

若要讓 JavaScript 使用任何數量的 C++ point 實例,我們需要為每個 C++ point 建立一個 JavaScript 物件,並在 JavaScript 物件和 C++ 實例之間建立連線。這會透過外部值和內部物件欄位來完成。

首先為 point 包裝器物件建立一個物件範本

v8::Local<v8::ObjectTemplate> point_templ = v8::ObjectTemplate::New(isolate);

每個 JavaScript point 物件都會透過一個內部欄位來保留它作為包裝器的 C++ 物件的參考。這些欄位之所以如此命名,是因為它們無法從 JavaScript 中存取,只能從 C++ 程式碼存取。一個物件可以有任意數量的內部欄位,內部欄位的數量會在物件範本中設定,如下所示

point_templ->SetInternalFieldCount(1);

這裡的內部欄位計數設定為 1,表示物件有一個內部欄位,索引為 0,指向一個 C++ 物件。

xy 存取器加入範本

point_templ->SetAccessor(v8::String::NewFromUtf8(isolate, "x"),
GetPointX, SetPointX);
point_templ->SetAccessor(v8::String::NewFromUtf8(isolate, "y"),
GetPointY, SetPointY);

接著,透過建立範本的新實例來包裝一個 C++ 點,然後將內部欄位 0 設定為點 p 周圍的外部包裝器。

Point* p = ...;
v8::Local<v8::Object> obj = point_templ->NewInstance();
obj->SetInternalField(0, v8::External::New(isolate, p));

外部物件只是 void* 周圍的包裝器。外部物件只能用於在內部欄位中儲存參考值。JavaScript 物件無法直接參考 C++ 物件,因此外部值用作從 JavaScript 進入 C++ 的「橋樑」。在這個意義上,外部值與句柄相反,因為句柄讓 C++ 可以參考 JavaScript 物件。

以下是 xgetset 存取器的定義,y 存取器的定義相同,只是 y 取代了 x

void GetPointX(Local<String> property,
const PropertyCallbackInfo<Value>& info) {
v8::Local<v8::Object> self = info.Holder();
v8::Local<v8::External> wrap =
v8::Local<v8::External>::Cast(self->GetInternalField(0));
void* ptr = wrap->Value();
int value = static_cast<Point*>(ptr)->x_;
info.GetReturnValue().Set(value);
}

void SetPointX(v8::Local<v8::String> property, v8::Local<v8::Value> value,
const v8::PropertyCallbackInfo<void>& info) {
v8::Local<v8::Object> self = info.Holder();
v8::Local<v8::External> wrap =
v8::Local<v8::External>::Cast(self->GetInternalField(0));
void* ptr = wrap->Value();
static_cast<Point*>(ptr)->x_ = value->Int32Value();
}

存取器會擷取由 JavaScript 物件封裝的 point 物件的參照,然後讀取和寫入相關欄位。如此一來,這些通用存取器就能用於任意數量的封裝點物件。

攔截器 #

您也可以指定一個回呼,用於在腳本存取任何物件屬性時執行。這些稱為攔截器。為了效率,有兩種攔截器

V8 原始碼提供的範例 process.cc 包含使用攔截器的範例。在以下程式碼片段中,SetNamedPropertyHandler 指定 MapGetMapSet 攔截器

v8::Local<v8::ObjectTemplate> result = v8::ObjectTemplate::New(isolate);
result->SetNamedPropertyHandler(MapGet, MapSet);

MapGet 攔截器如下所示

void JsHttpRequestProcessor::MapGet(v8::Local<v8::String> name,
const v8::PropertyCallbackInfo<Value>& info) {
// Fetch the map wrapped by this object.
map<string, string> *obj = UnwrapMap(info.Holder());

// Convert the JavaScript string to a std::string.
string key = ObjectToString(name);

// Look up the value if it exists using the standard STL idiom.
map<string, string>::iterator iter = obj->find(key);

// If the key is not present return an empty handle as signal.
if (iter == obj->end()) return;

// Otherwise fetch the value and wrap it in a JavaScript string.
const string &value = (*iter).second;
info.GetReturnValue().Set(v8::String::NewFromUtf8(
value.c_str(), v8::String::kNormalString, value.length()));
}

與存取器一樣,指定的回呼會在存取屬性時呼叫。存取器和攔截器的差別在於,攔截器會處理所有屬性,而存取器會與一個特定屬性關聯。

安全性模型 #

「同源政策」(最初在 Netscape Navigator 2.0 中推出)會防止從一個「來源」載入的文件或腳本取得或設定來自不同「來源」的文件的屬性。這裡定義的來源是網域名稱(例如 www.example.com)、通訊協定(例如 https)和埠的組合。例如,www.example.com:81www.example.com 不是同一個來源。兩個網頁必須在這三個方面都相符,才算具有相同的來源。沒有這項保護,惡意的網頁可能會危害另一個網頁的完整性。

在 V8 中,「來源」定義為一個內容。預設不允許存取您呼叫來源以外的任何內容。若要存取您呼叫來源以外的內容,您需要使用安全性權杖或安全性回呼。安全性權杖可以是任何值,但通常是符號,即在其他任何地方都不存在的正規字串。您可以在設定內容時使用 SetSecurityToken 選擇性地指定安全性權杖。如果您未指定安全性權杖,V8 會自動為您建立的內容產生一個安全性權杖。

當嘗試存取全域變數時,V8 安全性系統會先檢查要存取的全域物件的安全權杖與嘗試存取全域物件的程式碼安全權杖。如果權杖相符,則授予存取權。如果權杖不相符,V8 會執行回呼,以檢查是否應允許存取。您可以使用物件範本的 SetAccessCheckCallbacks 方法設定物件的安全回呼,以指定是否允許存取物件。接著,V8 安全性系統可以擷取要存取物件的安全回呼,並呼叫它以詢問是否允許其他內容存取它。此回呼會提供要存取的物件、要存取的屬性名稱、存取類型(例如讀取、寫入或刪除),並傳回是否允許存取。

此機制實作在 Google Chrome 中,因此如果安全權杖不相符,將使用特殊回呼,僅允許存取下列項目:window.focus()window.blur()window.close()window.locationwindow.open()history.forward()history.back()history.go()

例外 #

如果發生錯誤,V8 會擲回例外,例如當指令碼或函式嘗試讀取不存在的屬性,或呼叫非函式的函式時。

如果操作未成功,V8 會傳回空的控制代碼。因此,您的程式碼在繼續執行前,務必檢查傳回值是否為空的控制代碼。使用 Local 類別的公開成員函式 IsEmpty() 檢查空的控制代碼。

您可以使用 TryCatch 捕捉例外,例如

v8::TryCatch trycatch(isolate);
v8::Local<v8::Value> v = script->Run();
if (v.IsEmpty()) {
v8::Local<v8::Value> exception = trycatch.Exception();
v8::String::Utf8Value exception_str(exception);
printf("Exception: %s\n", *exception_str);
// ...
}

如果傳回值為空的控制代碼,而且您沒有 TryCatch,您的程式碼必須退出。如果您有 TryCatch,例外會被捕捉,您的程式碼可以繼續處理。

繼承 #

JavaScript 是一種無類別的物件導向語言,因此它使用原型繼承,而不是傳統繼承。這可能會讓受過 C++ 和 Java 等傳統物件導向語言訓練的程式設計師感到困惑。

基於類別的物件導向語言,例如 Java 和 C++,建立在兩個不同實體的概念上:類別和實例。JavaScript 是一種基於原型的語言,因此沒有這種區別:它只包含物件。JavaScript 本身不支援宣告類別階層;然而,JavaScript 的原型機制簡化了將自訂屬性和方法新增到物件所有實例的流程。在 JavaScript 中,您可以將自訂屬性新增到物件。例如

// Create an object named `bicycle`.
function bicycle() {}
// Create an instance of `bicycle` called `roadbike`.
var roadbike = new bicycle();
// Define a custom property, `wheels`, on `roadbike`.
roadbike.wheels = 2;

這樣新增的自訂屬性只存在於物件的該實例中。如果我們建立另一個名為 `mountainbike` 的 `bicycle()` 實例,則 `mountainbike.wheels` 會傳回 `undefined`,除非明確新增 `wheels` 屬性。

有時這正是需要的,其他時候將自訂屬性新增到物件的所有實例會很有幫助 - 畢竟所有腳踏車都有輪子。這正是 JavaScript 的原型物件非常有用的地方。若要使用原型物件,請在物件上參照關鍵字 `prototype`,然後再將自訂屬性新增到它,如下所示

// First, create the “bicycle” object
function bicycle() {}
// Assign the wheels property to the object’s prototype
bicycle.prototype.wheels = 2;

現在 `bicycle()` 的所有實例都會預先內建 `wheels` 屬性。

V8 中使用範本時會採用相同的方法。每個 `FunctionTemplate` 都有一個 `PrototypeTemplate` 方法,它會提供函式原型的範本。您可以在 `PrototypeTemplate` 上設定屬性,並將 C++ 函式與這些屬性關聯,然後這些屬性會出現在對應 `FunctionTemplate` 的所有實例中。例如

v8::Local<v8::FunctionTemplate> biketemplate = v8::FunctionTemplate::New(isolate);
biketemplate->PrototypeTemplate().Set(
v8::String::NewFromUtf8(isolate, "wheels"),
v8::FunctionTemplate::New(isolate, MyWheelsMethodCallback)->GetFunction()
);

這會導致 `biketemplate` 的所有實例在其原型鏈中有一個 `wheels` 方法,當呼叫該方法時,會呼叫 C++ 函式 `MyWheelsMethodCallback`。

V8 的 `FunctionTemplate` 類別提供公開成員函式 `Inherit()`,當您希望函式範本繼承自另一個函式範本時,您可以呼叫它,如下所示

void Inherit(v8::Local<v8::FunctionTemplate> parent);