V8引擎嵌入指南


轉自:http://www.grati.org/?p=344

譯自:http://code.google.com/apis/v8/embed.html,轉載請注明譯文鏈接。

如果已讀過V8編程入門那你已經熟悉了如句柄(handle)、作用域(scope)和上下文(context)之類的關鍵概念,以及如何將V8引擎作為一個獨立的虛擬機來使用。本文將進一步討論這些概念,並介紹其他有關V8引擎嵌入C++應用程序的關鍵概念。

V8引擎的API提供了編譯執行腳本、訪問C++方法和數據結構、處理錯誤、執行安全檢查等功能。你的應用程序可以像使用其他C++庫一樣使用V8引擎。你可以通過在C++代碼中包含頭文件include/v8.h來訪問V8引擎的API。

V8引擎設計理念這篇文章中提供的背景信息,可能會對您優化您的應用程序有所幫助。

目錄

目標讀者

本文件的目標讀者是想要將V8JavaScript引擎嵌入C++應用的程序員。本文將幫助你實現C++對象和方法對JavaScript可見,並使JavaScript對象和方法對C++應用程序可見。

返回頁首

句柄和垃圾收集

句柄是對保存在堆(heap)中的JavaScript對象的引用。V8垃圾收集器會回收不可訪問對象所占用的內存。在垃圾回收的過程中,垃圾收集器常常移動堆中對象的位置。當垃圾收集器移動對象的時候的它也更新所有指向這些對象的句柄,使其指向對象新的位置。

如果一個對象在JavaScript代碼中無法訪問,且沒有任何句柄指向這個對象(c++代碼中),它就會被認為是垃圾。垃圾收集器不停的刪除所有被視為垃圾的對象。V8引擎的垃圾收集機制,是V8引擎擁有高性能的關鍵。要了解更多相關信息,請參考V8引擎設計理念 。

有兩種類型的句柄(handle):

    • 本地句柄保存在堆棧(stack)中,在其析構函數被調用的時候從堆棧中刪除(自身)。這些句柄的生命周期是由句柄作用域(handle scope)決定的,作用域(scope)常常在調用函數時創建。當句柄作用域被刪除時,垃圾收集器將釋放這些句柄之前所引用的對象,因為他們將不能通過JavaScript或其他句柄訪問。V8編程入門中的例子使用的就是這種類型的句柄。Local<SomeType>代表本地句柄,它也可以存儲在父類Handle<SomeType>的實例中。

注意 :保存句柄的棧並不是C++的調用堆棧,但是句柄作用域卻嵌入在C++堆棧中。句柄作用域對象只能是堆棧分配,不能使用new生成實例。

  • 持久句柄並不保存在堆棧中,並且僅當你專門刪除它們時才會被刪除。就像一個本地句柄,持久句柄也保存指向堆分配對象的引用。如果你需要在多個函數中訪問同一個對象,或者句柄的生命周期和C++作用域並不相同時,你可以使用持久句柄。例如,Google Chrome使用持久句柄保存文檔對象模型(DOM)節點的引用。使用Persistent::NewPersistent::Dispose創建和銷毀一個持久句柄。持久句柄可以使用Persistent::MakeWeak將其標記為weak,這將在對一個對象的唯一引用是弱持久句柄(weak persistent)時觸發垃圾收集器執行一個回調。Persistent<SomeType>代表持久句柄,它也可以存儲在父類Handle<SomeType>的實例中。

當然,每創建一個對象都會創建一個句柄,着可能會導致出現大量的句柄!這時句柄作用域就非常有用了。你可以將句柄作用域當做一個可以保存很多句柄的容器。句柄作用域的析構函數被調用時會從堆棧中刪除該作用域內所有的句柄。正如您所期望的,這會使所有被刪除句柄所指向的對象被垃圾回收器標記為從堆中刪除。

回到V8 編程入門中那個很簡單的例子 ,在下面的圖中你可以看到句柄堆棧(handle-stack)和在堆中分配的對象(heap-allocated objects)。請注意Context::New()返回的持久句柄並不在句柄堆棧中。

當析構函數HandleScope::~HandleScope被調用,即刪除句柄作用域時,如果沒有別的句柄引用,被刪除作用域內的所有句柄所指向的對象都會在下次垃圾回收時被標記為刪除。垃圾收集器還可以從堆中刪除source_objscript_obj對象,因為他們不再被任何句柄引用且不能在JavaScript中訪問。由於上下文句柄(context handle)是一個持久句柄,它不會在超出句柄作用於后被刪除。刪除上下文句柄唯一的方法是顯式的調用它的Dispose方法。

 :在本文中“句柄(handle)”一詞指的是本地句柄(local handle),當提及“持久句柄(persistent handle)”時,我們會使用持久句柄的全稱。

返回頁首

上下文(context)

在V8中,上下文(context)是一個執行環境,允許JavaScript應用程序獨立地、不相關地運行在一個V8引擎實例中。要執行任何JavaScript代碼,您必須顯式的指定其運行的上下文。

這為什么是必要的?由於JavaScript提供了一套內置的實用函數和對象,而它們可以被JavaScript代碼修改。例如,當兩個完全不相干的JavaScript函數都以同樣的方式改變了全局對象,則很可能會產生意外結果。

從CPU時間和內存的角度看來,創建一個新的執行環境並建立必須的對象,可能是非常昂貴的開銷。不過,V8引擎中廣泛存在的緩存特性可以保證,第一次創建上下文是比較昂貴的,但其后創建上下文的花費會少很多。這是因為第一個上下文(context)需求創建內置對象並解析內置的JavaScript代碼(built-in JavaScript code),但隨后創建的上下文只需要創建自己的內置對象即可。隨着V8引擎快照功能(通過構建參數snapshot=yes來激活,默認處於激活狀態)的出現,第一次創建上下文所花的時間將會被很大程度的優化。因為快照功能包含一個經過序列化的堆,其中已經包括編譯好的JavaScript內置對象的代碼。與垃圾收集機制一樣,V8引擎中廣泛存在的緩存也是V8引擎高性能的關鍵,更多信息請參閱V8引擎設計理念 。

當您創建了一個上下文之后,可以任意次數的進入和退出該上下文。當在上下文A中時,您也可以進入一個不同的上下文B,這意味着您使用B上下文取代A成為當前上下文。當您從B中退出時,A恢復成為當前上下文。如下所示:

請注意,內置的實用函數和對象在各上下文中是獨立的。當創建上下文時,您也可以設置一個可選的安全令牌。更多信息請參見安全模型章節。

V8引擎使用上下文的初衷是,使每個窗口和瀏覽器的iframe可以有它們自己的JavaScript環境。

返回頁首

模板

在上下文中,模板是JavaScript函數和對象的框架。您可以使用模板將C++函數和數據結構包裝在JavaScript對象中,使他們可以通過JavaScript腳本來操作。例如,Google Chrome使用模板將C++ DOM節點包裝成JavaScript對象,並將其安裝在全局命名空間中。您可以創建一套模板,在多個上下文中使用。你可以創建任意多的模板,但在一個上下文中,每個模版只能有一個實例存在。

在JavaScript中,函數和對象之間有強烈的兩重性。在Java或C++中要創建一個新類型的實例,通常情況下你會首先定義一個新類。在JavaScript中你會創建一個新的函數,並使用這個函數作為構造函數創建新的實例。一個JavaScript對象的結構和功能是和它的構造函數緊密相關的。這將在V8模版的工作方式中體現。V8有兩種類型的模板:

  • 函數模板
    函數模板是一個函數的框架。你可以在想要生成JavaScript函數的上下文中調用模板的”GetFunction”方法來創建一個該模版的JavaScript實例。您還可以將一個c++回調函數和一個函數模版相關聯,這個回調函數將在JavaScript函數執行時被調用。
  • 對象模板
    每個函數模板都有一個與之關聯的對象模板。它用來配置使用這個函數作為構造函數所創建的對象。對於對象模板,你可以將兩種C++回調函數與之關聯:
    -“存取回調(accessor callbacks)”在對象的指定屬性被腳本訪問時調用。
    -“攔截回調(interceptor callbacks)”在對象的任意屬性被腳本訪問時調用。
    存取器(Accessors)攔截器(interceptors)將在下文中討論。

下面的演示代碼創建全局對象模板,並設置內置的全局函數。

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

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

此示例代碼取自process.cc中的JsHttpProcessor::Initializer函數。

返回頁首

存取器(accessors)

存取器(accessors)是一個C++回調函數,在JavaScript腳本存取對象屬性時計算並返回該屬性的值。存取器可以通過對象模板的SetAccessor方法來設置。此方法接受一個屬性名稱和兩個回調函數分別用於讀取和寫入屬性的值。

一個存取器的復雜程度取決於它們操作的數據類型:

存取靜態全局變量

比方說,有兩個C++整形變量xy,將要提供給JavaScript作為一個上下文中的全局變量。要做到這一點,腳本每次讀取或寫入該變量時都需要調用C++存取器函數。 這些存取函數使用Integer::New將C++中的整形變量轉化為JavaScript中的整形變量,使用Int32Value將JavaScript中的整形變量轉化為C++中的整形變量。下面是一個例子:

Handle<Value> XGetter(Local<String> property,
                        const AccessorInfo& info) {
    return Integer::New(x);
  }

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

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

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

請注意上述代碼中的對象模板和上下文是同時創建的。模板可以事先創建,並可在任意多個上下文中使用。

返回頁首

存取動態變量

前面例子中的變量是靜態的全局變量,如果數據是動態的,比如說是瀏覽器中的DOM樹。讓我們想象xy是c++類Point的對象字段:

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

為了使任意數量的C++point類的實例都可用於JavaScript,我們需要為每個C++point的實例都創建一個與之對應的JavaScript對象 ,並在JavaScript對象和C++實例之間建立聯系。這個操作通過外部對象(external values)和內部字段(internal object fields)來完成。

首先創建一個對象模板(object template)包裝point對象:

Handle<ObjectTemplate> point_templ = ObjectTemplate::New();

每個JavaScript的point對象保留一個對C++對象的引用,這是包含內部字段(internal field)的包裝(wrapper)。這些字段被稱為內部字段是因為他們不能在JavaScript中訪問,它們只能從C++代碼中訪問。一個對象可以有任意數量的內部字段,使用如下方式設置內部字段的數量:

point_templ->SetInternalFieldCount(1);

在這里,內部字段計數被設置為1 ,這意味着該對象有一個內部字段指向一個C++對象,其索引為0

添加xy的存取器:

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

然后,新建一個模版的實例來包裝c++ point對象,並設置索引為0的內部字段指向point實例p的外部包裝器(external wrapper)。

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

外部對象(external object,即上面提到的外部包裝器)僅僅是一個void*的包裝。外部對象只能用於在內部字段中保存引用值。JavaScript對象不能直接引用C++對象,所以使用外部對象作為連接JavaScript和C++的“橋梁”。從這個意義上,外部對象的作用和句柄是相反的,因為句柄允許C++保留JavaScript對象的引用。

下面是xgetset存取器的定義,y存取器和x完全相同:

 Handle<Value> GetPointX(Local<String> property,
                          const AccessorInfo &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_;
    return Integer::New(value);
  }

  void SetPointX(Local<String> property, Local<Value> value,
                 const AccessorInfo& 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();
  }

存取器從JavaScript對象的包裝中取出point對象的引用,然后通過這個引用讀取或寫入相關的字段。這樣這些通用的存取器可用於任何包裝過的point對象。

返回頁首

攔截器(Interceptors)

您還可以指定一個在腳本訪問對象的任意屬性時都會觸發的回調。這些回調被稱為攔截器(interceptors)。為了提高效率,V8中有兩種類型的攔截器:

  • 命名屬性攔截器 -使用屬性名訪問屬性時被調用。
    例如,在瀏覽器環境中,使用document.theFormName.elementName
  • 索引屬性攔截器 -使用索引訪問屬性時被調用。例如,在瀏覽器環境中,使用document.forms.elements[0]

V8引擎源代碼中的示例process.cc,提供了一個使用攔截器的例子。在下面的代碼片段中SetNamedPropertyHandler函數設置了MapGetMapSet攔截器:

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

MapGet攔截器代碼如下:

Handle<Value> JsHttpRequestProcessor::MapGet(Local<String> name,
                                             const AccessorInfo &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 Handle<Value>();

  // Otherwise fetch the value and wrap it in a JavaScript string.
  const string &value = (*iter).second;
  return String::New(value.c_str(), value.length());
}

和存取器相同,只要屬性被訪問,指定的回調都會被調用。存取器和攔截器之間的不同是:攔截器可以處理所有屬性,但存取器只與一個特定的屬性相關聯。

返回頁首

安全模型

“同源規則”(在Netscape Navigator 2.0中第一次引入)阻止從一個“源”中加載的文件或腳本獲取或設置不同“源”的文檔屬性。這里的“源”包括域名(www.example.com)、協議(http或https)和端口(例如,www.example.com:81和www.example.com是不同的)。所有這三個必須匹配,才被視為同源。如果沒有這種保護,惡意網頁可能會影響其他正常的網頁。

在V8中“源”被定義為一個上下文。默認情況下,不能在一個作用域中訪問其他作用域。要訪問和當前作用域不同的作用域,你需要使用安全令牌(security token)或安全回調(security callbacks)。一個安全令牌可以是任何值,但通常是一個符號或一個唯一的字符串。當定義上下文時,您可以通過SetSecurityToken指定安全令牌。如果你不指定安全令牌V8引擎將自動為新建的上下文生成令牌。

當試圖將訪問一個全局變量時的V8引擎安全系統首先檢查被訪問全局對象的安全令牌和訪問代碼的安全令牌。如果令牌匹配則授予訪問權限。如果不匹配V8引擎會執行一個回調,用以判定是否應該允許訪問。通過使用模板對象的SetAccessCheckCallbacks方法,你可以設置一個安全回調,這個回調函數決定是否允許對該對象的訪問。V8引擎安全系統就可以獲取被訪問對象的安全回調,並執行它來決定是否允許來自另一個上下文的訪問。這個回調函數的參數中包含被訪問的對象,被訪問屬性的名稱,訪問的類型(如讀、寫、刪除等)並返回是否允許訪問。

Google Chrome使用了的這種機制,如果安全令牌不匹配,瀏覽器將使用特殊的安全回調決定是否可以訪問如下對象: window.focus() window.blur() window.close() window.location ,window.open() history.forward() history.back()history.go()

返回頁首

異常

V8引擎在發生錯誤時會拋出異常,例如-當一個腳本或函數試圖讀取一個不存在的屬性,或者如果一個不是函數的對象被當作函數調用。

如果操作沒有成功V8引擎將返回一個空的句柄。因此,在繼續執行操作之前,檢查代碼返回值是不是空句柄非常重要。你可以使用Handle類的公共成員函數IsEmpty()來檢查句柄是否為空,

並使用TryCatch來捕獲異常,例如:

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

如果返回值是一個空句柄,而你的代碼沒有TryCatch,那程序將會退出。如果有一個TryCatch捕獲異常,則程序可以繼續執行。

返回頁首

繼承

JavaScript是一無類型的面向對象語言,而且它使用原型繼承而非類繼承。對於從傳統面向對象語言如C++和Java轉換而來的程序員而言,這相當令人費解。

基於類的面向對象語言,如Java和C++,是建立在兩個不同的實體的概念:類和實例之上的。JavaScript是一種基於原型的語言,因此不會作出這種區別:它只有對象。JavaScript不支持原生的類層次的聲明,然而,JavaScript的原型機制,簡化了添加自定義屬性和方法到對象實例的過程。在JavaScript中,你可以將自定義屬性添加到對象。例如:

// 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()的另一個實例,例如mountainbike,除非顯式的添加wheels屬性,否則mountainbike.wheels將返回undefined

有時候這正是我們所需要的,但有時候將自定義屬性添加到一個對象的所有實例會也許會更有幫助,畢竟所有自行車都有輪子。這是JavaScript的prototype對象非常有用的原因。要使用原型對象,在添加自定義屬性之前使用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++的函數與之關聯,隨后該函數模板的所有實例都將擁有該屬性。例如:

 Handle<FunctionTemplate> biketemplate = FunctionTemplate::New();
 biketemplate.PrototypeTemplate().Set(
     String::New("wheels"),
     FunctionTemplate::New(MyWheelsMethodCallback)
 )

這將使biketemplate的所有實例都從原型鏈繼承wheels方法,並在wheels方法被調用的時候執行C++函數MyWheelsMethodCallback

V8的FunctionTemplate類提供了公共成員函數Inherit(),使用它可以使一個函數模板繼承另一個函數模板。如下所示:

void Inherit(Handle<FunctionTemplate> parent);


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM