V8嵌入指南

中英文地址:https://github.com/Chunlin-Li/Chunlin-Li.github.io/blob/master/blogs/javascript/V8_Embedder's_Guide_CHS.md

如果你已經閱讀了入門指南, 一定知道 V8 是一個獨立運行的虛擬機, 其中有些關鍵性概念比如: handles, scopes 和 contexts. 本文將深入討論這些概念, 並且引入一些其他對於將 c++ 程序嵌入到 V8 中非常重要的信息.
V8 API 爲編譯和執行JS腳本, 訪問 C++ 方法和數據結構, 錯誤處理, 開啓安全檢查等提供了函數接口. 你的 C++ 應用可以像使用其他 C++ 庫一樣使用 V8, 只需要在你的代碼中 include V8 頭文件(include/v8.h).
如果你需要優化你的V8應用, "V8 設計元素"一文將會爲你提供一些有用的背景知識.

本文是爲那些想要將 V8 Javascript 引擎嵌入到 C++ 程序中的工程師而撰寫. 它將幫你搭建 C++ 程序的對象和方法與 JavaScript 對象和函數之間的橋樑.

Handles and Garbage Collection (Handle 與 GC 垃圾回收)

Handle 提供了一個JS 對象在堆內存中的地址的引用. V8 垃圾回收器將回收一個已無法被訪問到的對象佔用的堆內存空間. 垃圾回收過程中, 回收器通常會將對象在堆內存中進行移動. 當回收器移動對象的同時, 也會將所有相應的 Handle 更新爲新的地址.當一個對象在 JavaScript 中無法被訪問到, 並且也沒有任何 Handle 引用它, 則這個對象將被當作 "垃圾" 對待. 回收器將不斷將所有判定爲 "垃圾" 的對象從堆內存中移除. V8 的垃圾回收機制是其性能的關鍵所在. 更多相關信息見 "V8 設計元素" 一文.

Local Handles 保存在一個棧結構中, 當棧的析構函數(destructor)被調用時將同時被銷燬. 這些 handle 的生命週期取決於 handle scope (當一個函數被調用的時候, 對應的 handle scope 將被創建). 當一個 handle scope 被銷燬時, 如果在它當中的 handle 所引用的對象已無法再被 JavaScript 訪問, 或者沒有其他的 handle 指向它, 那麼這些對象都將在 scope 的銷燬過程中被垃圾回收器回收. 入門指南中的例子使用的就是這種 Handle. 

Local handle 對應的類是 Local<SomeType>

注意 : Handle 棧並不是 C++ 調用棧的一部分, 不過 handle scope 是被嵌入到C++棧中的. Handle scope只支持棧分配, 而不能使用 new 進行堆分配.

Persistent handle 是一個堆內存上分配的 JavaScript 對象的引用, 這點和 local handle 一樣. 但它有兩個自己的特點, 是對於它們所關聯的引用的生命週期管理方面. 當你 希望 持有一個對象的引用, 並且超出該函數調用的時期或範圍時, 或者是該引用的生命週期與 C++ 的作用域不一致時, 就需要使用 persistent handle 了. 例如 Google Chrome 就是使用 persistent handle 引用 DOM 節點. Persistent handle 支持弱引用, 即 PersistentBase::SetWeak, 它可以在其引用的對象只剩下弱引用的時候, 由垃圾回收器出發一個回調.

  • 一個 UniquePersistent<SomeType> 依賴 C++ 的構造函數和析構函數來管理其引用的對象的生命週期.
  • 當使用構造函數創建一個 Persistent<SomeType> 後, 必須在使用完後顯式調用 Persistent::Reset.
  • 還有其他類型的 handle, 但是很少用到, 這裏只簡單提一下:

    • Eternal 是一個用於預期永遠不會被釋放的 JavaScript 對象的 persistent handle, 使用它的代價更小, 因爲它減輕了垃圾回收器判定對象是否存活的負擔.
    • Persistent 和 UniquePersistent 都無法被拷貝, 使得它無法成爲 C++11 之前的標準庫容器的值. PersistentValueMap 和 PersistentValueVector 爲persistent 值提供了容器類, 並且帶有 Map 或類 Vector 的語義. C++11 的開發者不需要他們, 因 C++11 改變了語義, 解決了潛在的問題.
    • 當然, 每次創建對象的時候, 都創建一個相應的 local handle 會產生大量的 handle. 此時, handle scope 就派上用處了. 你可以將 handle scope 看作是存有許多 handle 的容器. 當 handle scope 銷燬時, 其中的所有 handle 也隨即銷燬, 這樣, 這些 handle 所引用的對象就能夠在下一次垃圾回收的時候被恰當的處理了.
      回到我們在入門指南中的簡單示例上 , 在下面這張圖表中你可以看到 handle-stack 和在堆內存上分配的對象. 注意, Context::New() 將返回一個 Local handle, 基於它, 我們創建了一個新的 Persistent handle 來演示 Persistent handle 的用法.
    • 當析構函數 HandleScope::~HandleScope 被調用時, handle scope 被刪除, 其中的 handle 所引用的對象將在下次 GC 的時候被適當的處理. 垃圾回收器會移除 source_obj 和 script_obj 對象, 因爲他們已經不再被任何 handle 引用, 並且在 JS 代碼中也無法訪問到他們. 而 context handle 即使在離開 handle scope 後也並不會被移除, 因爲它是 persistent handle, 只能通過對它顯式調用 Reset 才能將其移除.
      注意 : 整篇文檔中的名詞 handle 都表示 local handle, 如果要表示 persistent handle 會使用全稱.
    • // This function returns a new array with three elements, x, y, and z.
      // 該函數返回一個新數組, 其中包含 x, y, 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.
        // 我們稍後需要創建一個臨時 handle, 因此我們需要使用 handle scope.
        EscapableHandleScope handle_scope(isolate);
      
        // Create a new empty array.
        // 創建一個新的空數組.
        Local<Array> array = Array::New(isolate, 3);
      
        // Return an empty result if there was an error creating the array.
        // 如果創建數組失敗, 返回空.
        if (array.IsEmpty())
          return Local<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.
        // 通過 Escape 返回數組
        return handle_scope.Escape(array);
      }

      在這裏要注意這個模型的一個陷阱: 你無法從一個在 handle scope 中聲明的函數中返回一個 local hanle. 如果你這麼做了, 那麼這個 local handle 將在返回前, 首先在 handle scope 的析構函數被調用時被刪除. 返回一個 local handle 的正確方法應該是構建一個 EscapableHandleScope 而不是 HandleScope, 並調用其 Escape()方法, 將你想要返回的 handle 傳遞給它. 以下是一個實踐中的例子:

      Escape 方法將其參數的值拷貝到一個封閉作用域中, 然後照常刪除所有 Local handle, 然後將一個含有指定值的新的 handle 送回給調用方.


    • Contexts (上下文)

      在 V8 中, 一個 context 就是一個執行環境, 它使得可以在一個 V8 實例中運行相互隔離且無關的 JavaScript 代碼. 你必須爲你將要執行的 JavaScript 代碼顯式的指定一個 context.
      之所以這樣是因爲 JavaScript 提供了一些內建的工具函數和對象, 他們可以被 JS 代碼所修改. 比如, 如果兩個完全無關的 JS 函數都在用同樣的方式修改一個 global 對象, 很可能就會出現一個意外的結果.
    • 如果要爲所有必須的內建對象創建一個新的執行上下文(context), 在 CPU 時間和內存方面的開銷可能會比較大. 然而, V8 的大量緩存可以對其優化, 你創建的第一個 context 可能相對比較耗時, 而接下來的 context 就快捷很多. 這是因爲第一個 context 需要創建內建對象並解析內建的 JavaScript 代碼. 而後續的 context 只需要爲它自己創建內建對象即可, 而不用再解析 JS 代碼了. 伴隨 V8 的快照 (snapshot) 特性 (通過 build 選項 snapshot=yes 開啓, 默認打開), 首次創建 context 的時間將會得到大量優化, 因爲快照包含了一個序列化的堆, 其中包含了已解析編譯過的內建 JavaScript 代碼. 隨着垃圾回收, V8 大量的緩存也是其高性能的關鍵因素, 更多信息請參閱 "V8 設計元素"一文.
    • 當你創建一個 context 後, 你可以進出此上下文任意多的次數. 當你在 context A 中時, 還可以再進入 context B. 此時你將進入 B 的上下文中. 當退出 B 時, A 又將成爲你的當前 context. 正如下圖所展示的那樣.

      注意, 每個 context 中的內建函數和對象是相互隔離的. 你也可以在創建一個 context 的時候設置一個安全令牌. 更多信息請參閱安全模型一節.
      在 V8 中使用 context 的動機是, 瀏覽器中的每個 window 和 iframe 可以擁有一個屬於自己的乾淨的執行環境.


  • Templates (模板)

    在一個 context 中, template 是 JavaScript 函數和對象的一個模型. 你可以使用 template 來將 C++ 函數和數據結構封裝在一個 JavaScript 對象中, 這樣它就可以被 JS 代碼操作. 例如, Chrome 使用 template 將 C++ DOM 節點封裝成 JS 對象, 並且將函數安裝在 global 命名空間中. 你可以創建一個 template 集合, 在每個創建的 context 中你都可以重複使用它們. 你可以按照你的需求, 創建任意多的 template. 然而在任意一個 context 中, 任意 template 都只能擁有一個實例.
在 JS 中, 函數和對象之間有很強的二元性. 在 C++ 或 Java 中創建一種新的對象類型通常要定義一個類. 而在 JS 中你卻要創建一個函數, 並以函數爲構造器生成對象實例. JS 對象的內部結構和功能很大程度上是由構造它的函數決定的. 這些也反映在 V8 的 template 的設計中, 因此 V8 有兩種類型的 template:

  • FunctionTemplate
    一個 Function Template 就是一個 JS 函數的模型. 我們可以在我們指定的 context 下通過調用 template 的 GetFunction 方法來創建一個 JS 函數的實例. 你也可以將一個 C++ 回調與一個當 JS 函數實例執行時被調用的 function template 關聯起來.

  • ObjectTemplate
    每一個 Function Template 都與一個 Object Template 相關聯. 它用來配置以該函數作爲構造器而創建的對象. 你也可以給這個 Object Template 關聯兩類 C++ 回調:

    • 存取器回調. 當指定的對象屬性被 JS 訪問時調用.
    • 攔截器回調. 當任意對象屬性被訪問時調用.

    存取器和攔截器將在後面的部分講到.

// Create a template for the global object and set the
// built-in global functions.
// 爲 global 對象創建一個 template 並設置內建全局函數
Local<ObjectTemplate> global = ObjectTemplate::New(isolate);
global->Set(String::NewFromUtf8(isolate, "log"), FunctionTemplate::New(isolate, LogCallback));

// Each processor gets its own context so different processors
// do not affect each other.
// 每個任務都有屬於自己的 context, 所以不同的任務相互之間不影響.
Persistent<Context> context = Context::New(isolate, NULL, global);
This example code is taken from JsHttpProcessor::Initializer in the process.cc sample.

以上代碼提供了一個 爲 global 對象創建 tamplate 並設置內建全局函數的例子.

示例代碼是 process.cc 中 JsHttpProcessor::Initializer 的片段.


Accessors (存取器)

存取器是一個當對象屬性被 JS 代碼訪問的時候計算並返回一個值的 C++ 回調. 存取器是通過 Object Template 的 SetAccessor 方法進行配置的. 該方法接收屬性的名稱和與其相關聯的回調函數, 分別在 JS 讀取和寫入該屬性時觸發.

存取器的複雜性源於你所操作的數據的訪問方式:

  • 訪問靜態全局變量
  • 訪問動態變量
  • 假設有兩個 C++ 整數變量 x 和 y, 要讓他它們可以在 JS 中通過 global 對象進行訪問. 我們需要在 JS 代碼讀寫這些變量的時候調用相應的 C++ 存取器函數. 這些存取函數將一個 C++ 整數通過 Integer::New 轉換成 JS 整數, 並將 JS 整數轉換成32位 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 are so similar they are omitted for brevity
    
      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);

    注意上述代碼中的 Object Template 是和 context 同時創建的. 事實上 Template 可以提前創建好, 並可以在任意 context 中使用.

Accessing Dynamic Variables (訪問動態變量)

在前一個例子中, 變量是靜態全局的, 那麼如果是一個動態操縱的呢? 比如用於標記一個 DOM 樹是否在瀏覽器中的變量? 我們假設 x 和 y 是 C++ 類 Point 上的成員:
class Point {
   public:
    Point(int x, int y) : x_(x), y_(y) { }
    int x_, y_;
  }
爲了讓任意多個 C++ Point 實例在 JS 中可用, 我們需要爲每一個 C++ Point 創建一個 JS 對象, 並將它們聯繫起來. 這可以通過外部值和內部成員實現.
首先爲 point 創建一個 Object template 封裝對象:
Local<ObjectTemplate> point_templ = ObjectTemplate::New(isolate);

每個 JS point 對象持有一個 C++ 封裝對象的引用, 封裝對象中有一個 Internal Field, 之所以這麼叫是因爲它們無法在 JS 中訪問, 而只能通過 C++ 代碼訪問. 一個對象可以有任意多個 Internal Field, 其數量可以按以下方式在 Object Template 上設置.

point_templ->SetInternalFieldCount(1);

此處的 internal field count 設置爲了 1, 這表示該對象有一個 internal field, 其 index 是 0, 指向一個 C++ 對象.

將 x 和 y 存取器添加到 template 上:

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

接下來通過創建一個新的 template 實例來封裝一個 C++ point, 將封裝對象的 interanl field 設置爲 0.

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

以上代碼中, 外部對象就是一個 void* 的封裝體. 外部對象只能用來在 internal field 上存儲引用值. JS 對象無法直接引用 C++ 對象, 因此可以將外部值當作是一個從 JS 到 C++ 的橋樑. 從這種意義上來說, 外部值是和 handle 相對的概念( handle 是 C++ 到 JS 對象的引用 ).
以下是 x 的存取器的定義, y 的和 x 一樣.
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();
  }
存取器抽取出了被 JS 對象封裝起來的 point 對象的引用, 並讀寫相關聯的成員. 這樣, 這些通用的存取器就可以被所有封裝後的 point 對象使用了.

Interceptors (攔截器)

我們可以設置一個回調, 讓它在對應對象的任意屬性被訪問時都會被調用. 這就是 Interceptor. 考慮到效率, 分爲兩種不同的 interceptor:

  • 屬性名攔截器: 當通過字符串形式的屬性名訪問時調用. 比如在瀏覽器中使用 document.theFormName.elementName 進行訪問.
  • 屬性索引攔截器: 當通過屬性的下標/索引訪問時調用. 比如在瀏覽器中使用 document.forms.elements[0] 進行訪問.

    V8 源碼 process.cc 的代碼中, 包含了一個使用 interceptor 的例子. 在下面的代碼片段中, SetNamedPropertyHandler 指定了 MapGet 和 MapSet 兩個 interceptor:

Local<ObjectTemplate> result = ObjectTemplate::New(isolate);
result->SetNamedPropertyHandler(MapGet, MapSet);
The MapGet interceptor is provided below:

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()));
}
和存取器一樣, 對應的回調將在每次屬性被訪問的時候調用, 只不過攔截器會處理所有的屬性, 而存取器只針對相關聯的屬性.


Security Model (安全模型)

同源策略用來防止從一個源載入的文檔或腳本存取另外一個源的文檔. 這裏所謂的 "同源" 是指相同的 protocal + domain + port, 這三個都相同的兩個網頁才被認爲是同源. 如果沒有它的保護, 惡意網頁將危害到其他網頁的完整性.

同源策略在 Netscape Navigator 2.0 中首次引入

在 V8 中, 同源被定義爲相同的 context. 默認情況下, 是無法訪問別的 context 的. 如果一定要這樣做, 需要使用安全令牌或安全回調. 安全令牌可以是任意值, 但通常來說是個唯一的規範字符串. 當建立一個 context 時, 我們可以通過 SetSecurityToken 來指定一個安全令牌, 否則 V8 將自動爲該 context 生成一個.

當試圖訪問一個全局變量時, V8 安全系統將先檢查該全局對象的安全令牌, 並將其和試圖訪問該對象的代碼的安全令牌比對. 如果匹配則放行, 否則 V8 將觸發一個回調來判斷是否應該放行. 我們可以通過 object template 上的 SetAccessCheckCallbacks 方法來定義該回調來並決定是否放行. V8 安全系統可以用被訪問對象上的安全回調來判斷訪問者的 context 是否有權訪問. 該回調需要傳入被訪問的對象, 被訪問的屬性以及訪問的類型(例如讀, 寫, 或刪除), 返回結果爲是或否.

Chrome 實現了這套機制, 對於安全令牌不匹配的情況, 只有以下這些纔可以通過安全回調的方式來判斷是否可以放行: window.focus()window.blur(),window.close()window.locationwindow.open()history.forward()history.back(), 和 history.go().


Exceptions (異常)

如果發生錯誤, V8 會拋出異常. 比如, 當一個腳本或函數試圖讀取一個不存在的屬性時, 或者一個不是函數的值被當作函數進行調用執行時.

如果一個操作不成功, V8 將返回一個空的 handle. 因此我們應該在代碼中檢查返回值是否是一個空的 handle, 可以使用 Local 類的公共成員函數 isEmpty() 來檢查 handle 是否爲空.

我們也可以像以下示例一樣 Try Catch 代碼中發生的異常:

 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);
    // ...
  }
如果 value 以一個空 handle 返回, 而你沒有 TryCatch 它, 你的程序掛掉, 反之則可以繼續執行.

Inheritance (繼承)

JS 是一個無類的面向對象編程語言, 因此, 它使用原型繼承而不是類繼承. 這會讓那些接受傳統面嚮對象語言(比如 C++ 和 Java)訓練的程序員感到迷惑.基於類的面向對象編程語言, 比如 C++ 和 Java, 是建立在兩種完全不同實體的概念上的: 類和實例. 而 JS 是基於原型的語言, 因此沒有這些區別, 它只有對象. JS 本身並不原生支持 類這個層級的聲明; 然而, 它的原型機制簡化了給對象實例添加自定義屬性或方法的過程. 在 JS 中, 你可以像以下代碼這樣給對象添加屬性:

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

這種方式定義的屬性只存在於該對象實例上. 如果創建另一個 bicycle() 實例則其並沒有 wheels 屬性, 進行訪問將返回 undefined. 除非顯式的將 wheels 屬性添加上去.

有時這正是我們所需要的, 但有時我們希望將屬性添加到所有這些實例上去, 這是 JS 的 prototype 對象就派上用處了. 爲了使用原型對象, 可以通過 prototype 關鍵詞訪問對象原型, 然後在它上面添加自定義的屬性:

function bicycle(){ 
}
// Assign the wheels property to the object's prototype
bicycle.prototype.wheels = 2

此後, 所有 bicycle() 的實例都將預置該屬性值了.

V8 通過 template 可以使用同樣的方法. 每個 FunctionTemplate 都有一個 PrototypeTemplate 方法可以返回該函數的原型. 我們可以給它設置屬性, 也可以將 C++ 函數關聯到這些屬性, 然後所有該 FunctionTemplate 對應的實例上都將有這些屬性和對應的值或函數:

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

以上代碼將使所有 biketemplate 的原型鏈上都具有 wheels 方法, 當在對應實例上調用 wheels 方法時, MyWheelsMethodCallback 將被執行.

V8 的 FunctionTemplate 類提供了公共的成員函數 Inherit(), 當我們希望當前 function template 繼承另外一個 function template 的時候可以調用該方法:

void Inherit(Local<FunctionTemplate> parent);



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章