[譯] V8 使用者文檔

若是你已經閱讀過了上手指南,那麼你已經知道了如何做爲一個單獨的虛擬機使用 V8 ,而且熟悉了一些 V8 中的關鍵概念,如句柄上下文。在本文檔中,還將繼續深刻討論這些概念而且介紹其餘一些在你的 C++ 應用中使用 V8 的關鍵點。c++

V8 的 API 提供了編譯和執行腳本,訪問 C++ 方法和數據結構,處理錯誤和啓用安全檢查的函數。你的應用能夠像使用其餘的 C++ 庫同樣使用 V8 。你的 C++ 應用能夠經過引入頭文件 include/v8.h 來訪問 V8 API 。程序員

當你想要優化你的應用時,V8 設計概要文檔能夠提供不少有用的背景知識。編程

前言

這篇文檔的受衆是那些想要在本身的 C++ 程序中使用 V8 JavaScript 引擎的人。它將能幫助你在 JavaScript 中使用你應用中的 C++ 對象和方法,亦能幫助你在 C++ 應用中使用 JavaScript 對象和方法。數組

句柄和垃圾回收

一個句柄提供了對在堆中的一個 JavaScript 對象地址的引用。V8 的垃圾回收器會在該對象不能再次被訪問到時,將其回收。在垃圾回收的過程當中,垃圾回收器可能會改變對象在堆中的位置。當垃圾回收器移動對象時,全部引用到該對象的句柄也會被一同更新。瀏覽器

當一個對象在 JavaScript 中已經不可被訪問而且沒有任何指向它的句柄時,它就會被垃圾回收。V8 的垃圾回收機制是 V8 性能表現的關鍵。更多信息可參閱 V8 設計概要文檔緩存

句柄分爲許多種:安全

  • 本地句柄會被分配在棧中,而且當對應的析構函數被調用時,它也會被刪除。這些本地句柄的生命週期取決於它對應的句柄域(handle scope),句柄域一般在一個函數調用的開始被建立。當句柄域被刪除時,垃圾回收器就能夠清除以前分配在該句柄域中的全部句柄了,由於它們不能再被 JavaScript 或其餘句柄所訪問到。這也正是在上手指南中使用的句柄。
    本地句柄可經過類 Local<SomeType> 建立。數據結構

注意:句柄棧並非 C++ 調用棧中的一部分,但句柄域卻在 C++ 棧中。故句柄域不可經過 new 關鍵字來分配。app

  • 持久句柄提供了對分配在堆中的 JavaScript 對象的引用。當你須要在超過一次函數中保持對一個對象的引用時,或者句柄的生命週期與 C++ 的塊級域不相符時,你應使用持久句柄。例如,在 Google Chrome 中,持久句柄被用來引用 DOM 節點。一個持久句柄能夠經過 PersistentBase::SetWeak ,變爲弱(weak)持久句柄,當一個對象所剩的惟一引用是來自於一個弱持久句柄時,便會觸發垃圾回收。編程語言

    • 一個 UniquePersistent<SomeType> 依賴於 C++ 構造函數和析構函數來管理下層對象的生命週期。

    • 一個 Persistent<SomeType> 能夠經過自身的構造函數來建立,但必須手動地經過 Persistent::Reset 來清除。

  • 還有兩種不多會被使用到的句柄,在這裏咱們僅對它們作一個簡單的介紹:

    • 永久(Eternal)句柄是一種爲被認爲永遠不會被刪除的 JavaScript 對象所設計的持久句柄。它的建立開銷更低,由於它不須要被垃圾回收。

    • Persistent<SomeType>UniquePersistent<SomeType> 都不能被複制,因此它們不能做爲 C++ 11 以前的標準庫容器中的值來使用。PersistentValueMapPersistentValueVector 爲它們提供了容器類,提供了相似於集合和向量的語義。

固然,每當你爲了建立一個對象而建立一個本地句柄時,每每可能會致使建立了許多句柄。這就是句柄域所存在的意義。你能夠將句柄域視做一個保存了許多句柄的容器。當句柄域的析構函數被調用時,全部它裏面的句柄都會被從棧中移除。正如你所指望的,這些句柄作指向的對象隨後就能夠被垃圾回收。

回到咱們在上手指南中的例子,在下面的圖示中,你能夠看到句柄棧和堆中的對象。值得注意的是,Context::New() 的返回值是一個本地句柄,而後咱們基於它建立了一個新的持久句柄,用以闡述持久句柄的用途。

local_persist_handles_review.png

HandleScope::~HandleScope 析構函數被調用時,該句柄域便會被刪除。全部被句柄域中的句柄所引用的對象,都將能夠在下次垃圾回收時被刪除,若是沒有其餘對於它們的引用存在。垃圾回收器一樣還會刪除堆中的 source_objscript_obj 對象,由於它們不被任何句柄所引用,而且也不可被 JavaScript 訪問。因爲 context 對象是一個持久句柄,因此當句柄域退出時,它並不會被移除,惟一能夠刪除它的辦法就是調用它的 Reset 方法。

注意:後文中的句柄若是不加註明,都指的是本地句柄。

在這個模型下,有一個很是常見的陷阱須要注意:你不能夠直接地在一個聲明瞭句柄域的函數中返回一個本地句柄。若是你這麼作了,那麼你試圖返回的本地句柄,將會在函數返回以前,在句柄域的析構函數中被刪除。正確的作法是使用 EscapableHandleScope 來代替 HandleScope 建立句柄域,而後調用 Escape 方法,而且傳入你想要返回的句柄。例子:

// 這個函數會返回一個帶有 x,y 和 z 三個元素的新數組
Local<Array> NewPointArray(int x, int y, int z) {
  v8::Isolate* isolate = v8::Isolate::GetCurrent();

  // 咱們將會建立一些臨時的句柄,因此咱們先建立一個句柄域
  EscapableHandleScope handle_scope(isolate);

  // 建立一個空數組
  Local<Array> array = Array::New(isolate, 3);

  // 若是在建立數組時產生異常,則返回一個空數組
  if (array.IsEmpty())
    return Local<Array>();

  // 填充數組
  array->Set(0, Integer::New(isolate, x));
  array->Set(1, Integer::New(isolate, y));
  array->Set(2, Integer::New(isolate, z));

  // 經過 Escape 返回該數組
  return handle_scope.Escape(array);
}

Escape 方法複製參數中的值至一個封閉的域中,而後刪除其餘本地句柄,最後返回這個能夠被安全返回的新句柄副本。

上下文

在 V8 中,上下文是一個容許多個分別獨立的,不相關的 JavaScript 應用在一個單獨的 V8 實例中運行的執行環境。你必須爲每個你想要執行的 JavaScript 代碼指定一個上下文。

這樣作是必要的麼?這麼作的緣由是,JavaScript 自己提供了一組內建的工具函數和對象,但它們又能夠被 JavaScript 代碼所修改。例如,兩個徹底沒有關聯的 JavaScript 函數同時修改了一個全局對象,那麼可能就會形成不可預期的後果。

從 CPU 時間和內存的角度來看,建立一個擁有指定數量的內建對象的執行上下文彷佛開銷很大。可是,V8 的緩存機制能夠確保,雖然建立的第一個上下文開銷很是大,但後續的上下文建立的開銷都會小不少。這是由於第一次建立上下文時,須要建立內建對象和解析內建的 JavaScript 代碼,然後續的上下文建立則只需爲它們的上下文建立內建對象便可。若是開啓了 V8 的快照特性(可經過選項 snapshot=yes 開啓,默認值即爲開啓),第一次建立上下文的時間花銷也會被極大的優化,由於快照中包含了這些所需的內建 JavaScript 代碼已然被編譯後的版本。除了垃圾回收外,V8 的緩存也是 V8 性能表現的關鍵,更多詳情可參閱V8 設計概要文檔

當你建立了一個上下文後,你能夠隨意地進入和離開它,沒有次數的限制。當你已經在上下文 A 中時,你還能夠再次進入另外一個上下文 B ,這覺得着當前的上下文環境變成了 B 。當你離開了 B 後,A 就再次成爲了當前上下文。如圖示:

intro_contexts.png

須要注意的是,JavaScript 內建工具函數和對象是相互獨立的。當你建立一個上下文時,你能夠同時設置可選的安全標識(security token)。更多詳情請參閱下文的安全模型章節。

在 V8 中使用上下文的最初動機是,在一個瀏覽器中,每個窗口和 iframe 都須要有各自獨立的 JavaScript 環境。

模板

一個模板即爲一個上下文中 JavaScript 函數和對象的藍圖。你能夠在 JavaScript 對象內使用一個模板來包裹 C++ 函數和數據結構,導致它們能夠被 JavaScript 腳本所操縱。例如,Google Chrome 使用模板來將 C++ DOM 節點包裹爲 JavaScript 對象,而後在全局命名空間下注冊函數。你能夠建立一個模板集合,而後在不一樣的上下文中使用它。模板的數量並無限制,可是在一個指定的上下文中,每個模板都只容許有一個它的實例。

在 JavaScript 中,函數和對象間有強烈的二元性。在 Java 或 C++ 中,若是要建立一個新類型的對象,你須要首先定義一個新的類。而在 JavaScript 中,你須要定義一個新的函數,而後把這個函數視做一個構造函數。一個 JavaScript 對象的外形和功能都與它的構造函數關係密切。這些也都反應在了 V8 模板的工做方式中。模板分爲兩種類型:

  • 函數模板
    一個函數模板就是一個獨立函數的藍圖。在一個你想要實例化 JavaScript 函數的上下文中,你能夠經過調用模板的 GetFunction 方法來建立一個模板的 JavaScript 實例。當 JavaScript 函數實例被調用時,你還能夠爲模板關聯一個 C++ 回調函數一同被調用。

  • 對象模板
    每個函數模板都有一個與之關聯的對象模板。對象模板用來配置將這個函數做爲構造函數而建立的對象。你能夠爲對象模板關聯兩種類型的 C++ 回調:

    • 訪問器回調會在指定對象原型被腳本訪問時被調用。

    • 攔截器回調會在任何對象原型被腳本訪問時被調用。

訪問器和攔截器的詳情會在後文中繼續討論。

下面的例子中,咱們將建立一個關聯全局對象的模板,而後設置一些內建的全局函數。

// 建立一個關聯全局對象的模板,而後設置一些內建的全局函數。
Local<ObjectTemplate> global = ObjectTemplate::New(isolate);
global->Set(String::NewFromUtf8(isolate, "log"), FunctionTemplate::New(isolate, LogCallback));

Persistent<Context> context = Context::New(isolate, NULL, global);

該例子取自於 process.cc 中的 JsHttpProcessor::Initialiser

訪問器

訪問器爲當一個 JavaScript 對象原型被腳本訪問時,執行的一個 C++ 回調函數,它計算並返回一個值。訪問器須要經過一個對象模板來配置,經過它的 SetAccessor 方法。這個方法的第一個參數爲關聯的屬性,最後一個參數爲當腳本試圖讀寫這個屬性時執行的回調。

訪問的複雜度取決於你想要其控制的數據類型:

  • 訪問靜態全局變量

  • 訪問動態變量

訪問靜態全局變量

假設有兩個名爲 xy 的 C++ 整形變量,它們須要成爲一個上下文的 JavaScript 中的全局變量。爲了達成這個目的,當腳本讀或寫這些變量時,你須要調用 C++ 訪問器函數。這些訪問器函數使用 Integer::New 來把 C++ 整形數轉換爲 JavaScript 整形數,而且使用 Int32Value 來把 JavaScript 整形數轉換爲 C++ 整形數。例子:

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

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

  // YGetter/YSetter 十分相似,這裏就省略了

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

注意代碼中的對象模板和上下文幾乎在同時建立。模板能夠提早建立好,而後在任意數量的上下文中使用它。

訪問動態變量

在上面的例子中,變量是靜態和全局的。那麼,若是數據是動態的,像瀏覽器中的 DOM 樹這樣呢?假設咱們有一個 C++ 類 Point,它有兩個屬性 xy

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

爲了讓任意數量的 C++ point 實例能夠經過 JavaScript 訪問,咱們須要爲每個 C++ point 實例建立一個 JavaScript 對象。這能夠經過外部(external)值和內部(internal)屬性共同辦到。

首先建立一個對象模板,用以包裹 point 實例:

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

每個 JavaScript 中的 point 對象都保持了對 C++ 對象的引用,由於它之內部屬性的方式被包裹。這些屬性不可經過 JavaScript 訪問,只能經過 C++ 代碼訪問到。一個對象能夠有任意數量的內部屬性,這個數量需經過如下方法來設置:

point_templ->SetInternalFieldCount(1);

上面的例子中,內部屬性的數量被設置爲了 1,代表這對象有一個內部屬性,索引值爲 0。

向模板添加 xy 訪問器:

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

接下來,咱們經過建立一個新的模板實例來包裹 C++ point 實例,而後將內部屬性 0 設置爲 p 的外部包裹。

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

一個外部對象僅被用來在內部屬性中存儲引用。JavaScript 對象不能直接地引用 C++ 對象,因此外部值就像從 JavaScript 到 C++ 的「一座橋樑」。因此外部值是句柄的相反面,由於句柄的做用是讓咱們在 C++ 中能夠獲取 JavaScript 對象的引用。

如下即是 x 的讀和寫訪問器的定義,y 的定義和 x 的十分相似,只需將 x 替換爲 y 便可:

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

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

訪問器抽象了對於 C++ point 對象的引用和對其的讀寫操做。這樣這些訪問器就能夠被用於任意數量的被包裹後的 point 對象中了。

攔截器

你還能夠在一個腳本訪問任意對象屬性時,設置一個回調函數。這些回調函數稱爲攔截器。攔截器分爲兩種類型:

  • 具名屬性攔截器,它會在訪問名稱爲字符串的屬性時被調用,如瀏覽器環境中的 document.theFormName.elementName

  • 索引屬性攔截器,它會在訪問索引屬性時被調用,如瀏覽器環境中的 document.forms.elements[0]

V8 源碼中的 process.cc 文件中,包含了一個攔截器的使用實例。下面例子中的 SetNamedPropertyHandler 設置了 MapGetMapSet 這兩個攔截器:

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

MapGet 攔截器源碼以下:

void JsHttpRequestProcessor::MapGet(Local<String> name,
                                    const 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(String::NewFromUtf8(value.c_str(), String::kNormalString, value.length()));
}

和訪問器同樣,特定的回調函數會在一個屬性被訪問後觸發。它和訪問器的區別就是,訪問器會回調僅會在一個特定的屬性被訪問時觸發,而攔截器回調則會在任意屬性被訪問時觸發。

安全模型

「同源策略」(首次出現於網景瀏覽器 2.0 中),用於阻止從另外一個「源」中加載腳本或文檔到本地「源」裏。這個源的概念中包含了域名(www.example.com),協議(http 或 https)和端口(如 www.example.com:81 和 www.example.com 不一樣源)。以上部分所有同樣,才能被視爲同源。若是沒了這層保護,許多網頁就能夠會遭到其餘惡意網頁的攻擊。

在 V8 中,「源」即爲上下文。在一個上下文中訪問另外一個上下文默認是不被容許的。若是必定訪問,那麼必須使用安全標識(security tokens)或安全回調(security callbacks)。一個安全標識能夠是任意類型的值,但一般是一個 symbol 或一個惟一字符串。當你設置一個上下文時,能夠經過 SetSecurityToken 可選地設置一個安全標識。若是你沒有明確地指明一個安全標識,那麼 V8 將會爲該上下文自動生成一個。

當試圖去訪問一個全局變量時,V8 的安全系統首先會去檢查被訪問的全局變量的上下文的安全標識與訪問代碼的上下文的安全標識是否一致,若一致,則容許訪問。若是安全標識不一致,那麼 V8 將會觸發一個回調函數來判斷這個訪問是否該被容許。你能夠經過在對象模板的方法 SetAccessCheckCallbacks ,來設置這個安全回調。這個回調的參數爲,將會被訪問的對象,將會被訪問的屬性名,和訪問的類型(如讀,寫或刪除)而且返回值即表示是否容許此次訪問。

在 Google Chrome 中,這套安全機制運用在如下幾處:window.focus()window.blur()window.close()window.locationwindow.open()history.forward()history.back()history.go()

異常

當一個錯誤發生時,V8 將會拋出一個異常。例如,當一個腳本或函數試圖去讀取一個不存在的屬性時,或一個非函數對象被調用時。

若是一次操做失敗了,V8 將會返回空句柄。由於在進一步操做前,檢查返回值是不是空句柄就變得尤其重要。咱們能夠經過本地句柄類(Local)的成員函數 IsEmpty() 來進行檢查。

你也能夠經過 TryCatch 類捕獲異常,例子:

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

若是返回值是一個空句柄,而且你沒有使用 TryCatch ,那麼你的代碼必需要終止。若是你使用了 TryCatch ,那麼你的代碼則能夠繼續執行。

繼承

JavaScript 是第一個不基於類的面向對象編程語言。它使用了基於原型的繼承。這對於一直使用傳統面向對象編程語言(如 C++ 和 Java)的程序員來講,可能會有些困惑。

傳統的面向對象編程語言(如 C++ 和 Java)一般基於兩個概念:類和繼承。JavaScript 是一個基於原型的編程語言,因此它和傳統的面向對象編程語言不一樣,它只有對象。JavaScript 並不原生支持基於類聲明的繼承。可是 JavaScript 的原型機制簡化了爲實例添加自定義屬性和方法的過程。在 JavaScript 中,你能夠爲單個實例添加自定義的屬性。例子:

// 建立一個對象 bicycle
function bicycle(){
}
// 建立一個名爲 roadbike 的實例
var roadbike = new bicycle()
// 爲 roadbike 定義一個自定義屬性 wheels
roadbike.wheels = 2

自定義屬性僅僅存在於當前這個實例中。若是咱們建立了另外一個 bicycle 實例,如 mountainbikemountainbike.wheels 將會是 undefined

某些時候,這就是咱們想要的。而又有些時候,咱們想要爲全部的實例都添加上這個屬性。由於畢竟全部的自行車都有輪子。這是咱們就會使用到原型機制。咱們只需爲對象的 prototype 屬性上添加咱們想要的自定義屬性便可:

// 建立一個對象 bicycle
function bicycle(){
}
// 將 wheels 屬性添加到對象的原型上
bicycle.prototype.wheels = 2

這樣,全部的 bicycle 實例都將會擁有 wheels 屬性。

在 V8 的模板中,作法也是同樣的。每個 FunctionTemplate 類實例都有一個 PrototypeTemplate 方法來給出函數的原型。你能夠在其上添加屬性,爲這些屬性關聯 C++ 函數。都會影響到該模板關聯全部的實例中。例子:

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

上面的代碼將會使全部的 biketemplate 實例擁有一個 wheels 方法。當該方法被調用時,C++ 函數 MyWheelsMethodCallback 就會執行。

V8 的 FunctionTemplate 類提供了一個公開成員函數 Inherit() ,當你想要一個函數模板繼承於另外一個函數模板時,你可使用它,例子:

void Inherit(Local<FunctionTemplate> parent);

最後

原文連接:https://developers.google.com/v8/embed

相關文章
相關標籤/搜索