使用 Google V8 引擎開發可定製的應用程序

Google V8 引擎使用

V8 引擎是 Google 的一個開源項目,是一個高效的 JavaScript 引擎,它可以作爲一個獨立的庫被嵌入到已有的 C++ 應用之中,爲軟件的靈活性,擴展性提供可能。使用 V8 的另外一個好處是,你不需要重新學習一本腳本語言,JavaScript 已經廣泛的被開發人員,尤其是前端開發人員所使用。

V8 引擎概覽

V8 引擎簡介

Google V8 引擎使用 C++ 代碼編寫,實現了 ECMAScript 規範的第五版,可以運行在所有的主流

操作系統中,甚至可以運行在移動終端 ( 基於 ARM 的處理器,如 HTC G7 等 )。V8 最早被開發用以嵌入到 Google 的開源瀏覽器 Chrome 中,但是 V8 是一個可以獨立的模塊,完全可以嵌入您自己的應用,著名的 Node.js( 一個異步的服務器框架,可以在服務端使用 JavaScript 寫出高效的網絡服務器 ) 就是基於 V8 引擎的。

和其他 JavaScript 引擎一樣,V8 會編譯 / 執行 JavaScript 代碼,管理內存,負責垃圾回收,與宿主語言的交互等。V8 的垃圾回收器採用了衆多技術,使得其運行效率大大提高。通過暴露宿主對象 ( 變量,函數等 ) 到 JavaScript,JavaScript 可以訪問宿主環境中的對象,並在腳本中完成對宿主對象的操作。

V8 引擎基本概念

圖 1. V8 引擎基本概念關係圖 ( 根據 Google V8 官方文檔 )
圖 1. V8 引擎基本概念關係圖 ( 根據 Google V8 官方文檔 )

handle

handle 是指向對象的指針,在 V8 中,所有的對象都通過 handle 來引用,handle 主要用於 V8 的垃圾回收機制。

在 V8 中,handle 分爲兩種:持久化 (Persistent)handle 和本地 (Local)handle,持久化 handle 存放在堆上,而本地 handle 存放在棧上。這個與 C/C++ 中的堆和棧的意義相同 ( 簡而言之,堆上的空間需要開發人員自己申請,使用完成之後顯式的釋放;而棧上的爲自動變量,在退出函數 / 方法之後自動被釋放 )。持久化 handle 與本地 handle 都是 Handle 的子類。在 V8 中,所有數據訪問均需要通過 handle。需要注意的是,使用持久化 handle 之後,需要顯式的調用 Dispose() 來通知垃圾回收機制。

作用域 (scope)

scope 是 handle 的集合,可以包含若干個 handle,這樣就無需將每個 handle 逐次釋放,而是直接釋放整個 scope。

在使用本地 handle 時,需要聲明一個 HandleScope 的實例,scope 是 handle 的容器,使用 scope,則無需依次釋放 handle。

 HandleScope handle_scope; 
 Local<ObjectTemplate> temp;

上下文 (context)

context 是一個執行器環境,使用 context 可以將相互分離的 JavaScript 腳本在同一個 V8 實例中運行,而互不干涉。在運行 JavaScript 腳本是,需要顯式的指定 context 對象。

數據及模板

由於 C++ 原生數據類型與 JavaScript 中數據類型有很大差異,因此 V8 提供了 Data 類,從 JavaScript 到 C++,從 C++ 到 JavaScrpt 都會用到這個類及其子類,比如:

 Handle<Value> Add(const Arguments& args){ 
	 int a = args[0]->Uint32Value(); 
	 int b = args[1]->Uint32Value(); 

	 return Integer::New(a+b); 
 }

Integer 即爲 Data 的一個子類。

V8 中,有兩個模板 (Template) 類 ( 並非 C++ 中的模板類 ):對象模板 (ObjectTempalte) 和函數模板 (FunctionTemplate),這兩個模板類用以定義 JavaScript 對象和 JavaScript 函數。我們在後續的小節部分將會接觸到模板類的實例。通過使用 ObjectTemplate,可以將 C++ 中的對象暴露給腳本環境,類似的,FunctionTemplate 用以將 C++ 函數暴露給腳本環境,以供腳本使用。

初始化 context 是使用 V8 引擎所必需的過程,代碼非常簡單:

 Persistent<Context> context = Context::New();

V8 引擎使用示例

有了上面所述的基本概念之後,我們來看一下一個使用 V8 引擎的應用程序的基本流程:

  1. 創建 HandleScope 實例
  2. 創建一個持久化的 Context
  3. 進入 Context
  4. 創建腳本字符串
  5. 創建 Script 對象,通過 Script::Compile()
  6. 執行腳本對象的 Run 方法
  7. 獲取 / 處理結果
  8. 顯式的調用 Context 的 Dispose 方法

基本代碼模板

清單 1. 代碼模塊
 #include <v8.h> 

 using namespace v8; 

 int main(int argc, char *argv[]) { 
	 // 創建一個句柄作用域 ( 在棧上 ) 
	 HandleScope handle_scope; 

	 // 創建一個新的上下文對象
	 Persistent<Context> context = Context::New(); 

	 // 進入上一步創建的上下文,用於編譯執行 helloworld 
	 Context::Scope context_scope(context); 

	 // 創建一個字符串對象,值爲'Hello, Wrold!', 字符串對象被 JS 引擎
	 // 求值後,結果爲'Hello, World!'
	 Handle<String> source = String::New("'Hello' + ', World!'"); 

	 // 編譯字符串對象爲腳本對象
	 Handle<Script> script = Script::Compile(source); 

	 // 執行腳本,獲取結果
	 Handle <Value> result = script->Run(); 

	 // 釋放上下文資源
	 context.Dispose(); 

	 // 轉換結果爲字符串
	 String::AsciiValue ascii(result); 

	 printf("%s\n", *ascii); 

	 return 0; 
 }

以上代碼爲一個使用 V8 引擎來運行腳本的基本模板,可以看到,開發人員可以很容易的在自己的代碼中嵌入 V8 來處理 JavaScript 腳本。我們在下面小節中詳細討論如何在腳本中訪問 C++ 資源。

使用 C++ 變量

在 JavaScript 與 V8 間共享變量事實上是非常容易的,基本模板如下:

清單 2. 共享變量
 static type xxx; 

 static Handle<Value> xxxGetter( 
	 Local<String> name, 
	 const AccessorInfo& info){ 

	 //code about get xxx 
 } 

 static void xxxSetter( 
	 Local<String> name, 
	 Local<Value> value, 
	 const AccessorInfo& info){ 

	 //code about set xxx 
 }

首先在 C++ 中定義數據,並以約定的方式定義 getter/setter 函數,然後需要將 getter/setter 通過下列機制公開給腳本:

 global->SetAccessor(String::New("xxx"), xxxGetter, xxxSetter);

其中,global 對象爲一個全局對象的模板:

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

下面我們來看一個實例:

清單 3. 實例 1
 static char sname[512] = {0}; 

 static Handle<Value> NameGetter(Local<String> name, 
		 const AccessorInfo& info) { 
	 return String::New((char*)&sname,strlen((char*)&sname)); 
 } 

 static void NameSetter(Local<String> name, 
		 Local<Value> value, 
		 const AccessorInfo& info) { 
	 Local<String> str = value->ToString(); 
	 str->WriteAscii((char*)&sname); 
 }

定義了 NameGetter, NameSetter 之後,在 main 函數中,將其註冊在 global 上:

 // Create a template for the global object. 
 Handle<ObjectTemplate> global = ObjectTemplate::New(); 

 //public the name variable to script 
 global->SetAccessor(String::New("name"), NameGetter, NameSetter); 

在 C++ 中,將 sname 的值設置爲”cpp”:

 //set sname to "cpp" in cpp program 
 strncpy(sname, "cpp", sizeof(sname)); 

然後在 JavaScript 中訪問該變量,並修改:

 print(name); 

 //set the variable `name` to "js"
 name='js'; 
 print(name);

運行結果如下:

 cpp 
 js

運行腳本,第一個 print 調用會打印在 C++ 代碼中設置的 name 變量的值:cpp,然後我們在腳本中修改 name 值爲:js,再次調用 print 函數則打印出設置後的值:js。

調用 C++ 函數

在 JavaScript 中調用 C++ 函數是腳本化最常見的方式,通過使用 C++ 函數,可以極大程度的增強 JavaScript 腳本的能力,如文件讀寫,網絡 / 數據庫訪問,圖形 / 圖像處理等等,而在 V8 中,調用 C++ 函數也非常的方便。

在 C++ 代碼中,定義以下原型的函數:

 Handle<Value> function(constArguments& args){ 
	 //return something 
 }

然後,再將其公開給腳本:

 global->Set(String::New("function"),FunctionTemplate::New(function));

同樣,我們來看兩個示例:

清單 4. 實例 2
 Handle<Value> Add(const Arguments& args){ 
	 int a = args[0]->Uint32Value(); 
	 int b = args[1]->Uint32Value(); 

	 return Integer::New(a+b); 
 } 

 Handle<Value> Print(const Arguments& args) { 
	 bool first = true; 
	 for (int i = 0; i < args.Length(); i++) { 
		 HandleScope handle_scope; 
		 if (first) { 
			 first = false; 
		 } else { 
			 printf(" "); 
		 } 
		 String::Utf8Value str(args[i]); 
		 const char* cstr = ToCString(str); 
		 printf("%s", cstr); 
	 } 
	 printf("\n"); 
	 fflush(stdout); 
	 return Undefined(); 
 }

函數 Add 將兩個參數相加,並返回和。函數 Print 接受任意多個參數,然後將參數轉換爲字符串輸出,最後輸出換行。

 global->Set(String::New("print"), FunctionTemplate::New(Print)); 
 global->Set(String::New("add"), FunctionTemplate::New(Add));

我們定義以下腳本:

 var x = (function(a, b){ 
	 return a + b; 	
 })(12, 7); 

 print(x); 

 //invoke function add defined in cpp 
 var y = add(43, 9); 
 print(y);

運行結果如下:

 19 
 52

使用 C++ 類

如果從面向對象的視角來分析,最合理的方式是將 C++ 類公開給 JavaScript,這樣可以將 JavaScript 內置的對象數量大大增加,從而儘可能少的使用宿主語言,而更大的利用動態語言的靈活性和擴展性。事實上,C++ 語言概念衆多,內容繁複,學習曲線較 JavaScript 遠爲陡峭。最好的應用場景是:既有腳本語言的靈活性,又有 C/C++ 等系統語言的效率。使用 V8 引擎,可以很方便的將 C++ 類”包裝”成可供 JavaScript 使用的資源。

我們這裏舉一個較爲簡單的例子,定義一個 Person 類,然後將這個類包裝並暴露給 JavaScript 腳本,在腳本中新建 Person 類的對象,使用 Person 對象的方法。

首先,我們在 C++ 中定義好類 Person:

清單 5. 定義類
 class Person { 
 private: 
	 unsigned int age; 
	 char name[512]; 

 public: 
	 Person(unsigned int age, char *name) { 
		 this->age = age; 
		 strncpy(this->name, name, sizeof(this->name)); 
	 } 

	 unsigned int getAge() { 
		 return this->age; 
	 } 

	 void setAge(unsigned int nage) { 
		 this->age = nage; 
	 } 

	 char *getName() { 
		 return this->name; 
	 } 

	 void setName(char *nname) { 
		 strncpy(this->name, nname, sizeof(this->name)); 
	 } 
 };

Person 類的結構很簡單,只包含兩個字段 age 和 name,並定義了各自的 getter/setter. 然後我們來定義構造器的包裝:

 Handle<Value> PersonConstructor(const Arguments& args){ 
	 Handle<Object> object = args.This(); 
	 HandleScope handle_scope; 
	 int age = args[0]->Uint32Value(); 

	 String::Utf8Value str(args[1]); 
	 char* name = ToCString(str); 

	 Person *person = new Person(age, name); 
	 object->SetInternalField(0, External::New(person)); 
	 return object; 
 }

從函數原型上可以看出,構造器的包裝與上一小節中,函數的包裝是一致的,因爲構造函數在 V8 看來,也是一個函數。需要注意的是,從 args 中獲取參數並轉換爲合適的類型之後,我們根據此參數來調用 Person 類實際的構造函數,並將其設置在 object 的內部字段中。緊接着,我們需要包裝 Person 類的 getter/setter:

 Handle<Value> PersonGetAge(const Arguments& args){ 
	 Local<Object> self = args.Holder(); 
	 Local<External> wrap = Local<External>::Cast(self->GetInternalField(0)); 

	 void *ptr = wrap->Value(); 

	 return Integer::New(static_cast<Person*>(ptr)->getAge()); 
 } 

 Handle<Value> PersonSetAge(const Arguments& args) 
 { 
	 Local<Object> self = args.Holder(); 
	 Local<External> wrap = Local<External>::Cast(self->GetInternalField(0)); 

	 void* ptr = wrap->Value(); 

	 static_cast<Person*>(ptr)->setAge(args[0]->Uint32Value()); 
	 return Undefined(); 
 }

而 getName 和 setName 的與上例類似。在對函數包裝完成之後,需要將 Person 類暴露給腳本環境:

首先,創建一個新的函數模板,將其與字符串”Person”綁定,並放入 global:

 Handle<FunctionTemplate> person_template = FunctionTemplate::New(PersonConstructor); 
 person_template->SetClassName(String::New("Person")); 
 global->Set(String::New("Person"), person_template);

然後定義原型模板:

 Handle<ObjectTemplate> person_proto = person_template->PrototypeTemplate(); 

 person_proto->Set("getAge", FunctionTemplate::New(PersonGetAge)); 
 person_proto->Set("setAge", FunctionTemplate::New(PersonSetAge)); 

 person_proto->Set("getName", FunctionTemplate::New(PersonGetName)); 
 person_proto->Set("setName", FunctionTemplate::New(PersonSetName));

最後設置實例模板:

 Handle<ObjectTemplate> person_inst = person_template->InstanceTemplate(); 
 person_inst->SetInternalFieldCount(1);

隨後,創建一個用以測試的腳本:

 //global function to print out detail info of person 
 function printPerson(person){ 
    print(person.getAge()+":"+person.getName()); 
 } 

 //new a person object 
 var person = new Person(26, "juntao"); 

 //print it out 
 printPerson(person); 

 //set new value 
 person.setAge(28); 
 person.setName("juntao.qiu"); 

 //print it out 
 printPerson(person);

運行得到以下結果:

 26:juntao 
 28:juntao.qiu

簡單示例

在這一小節中,我們將編寫一個簡單的桌面計算器:表達式求值部分通過 V8 引擎來進行,而流程控制部分則放在 C++ 代碼中,這樣可以將表達式解析等複雜細節繞開。同時,我們還得到了一個額外的好處,用戶在腳本中可以自定義函數,從而可以在計算器中定義自己的運算規則。

桌上計算器

計算器程序首先進入一個 MainLoop,從標準輸入讀取一行命令,然後調用 V8 引擎去求值,然後將結果打印到控制檯,然後再進入循環:

清單 6. 桌面計算器示例
 void MainLoop(Handle<Context> context) { 
	 while(true) { 
		 char buffer[1024] = {0}; 
		 printf("$ "); 
		 char *str = fgets(buffer, sizeof(buffer), stdin); 
		 if(str == NULL) { 
			 break; 
		 } 
		 HandleScope handle_scope; 
		 ExecuteString(String::New(str), String::New("calc"), true); 
	 } 
 }

在 main 函數中設置全局對象,創建上下文對象,並進入 MainLoop:

 int main(int argc, char *argv[]){ 
	 HandleScope handle_scope; 

	 // Create a template for the global object. 
	 Handle<ObjectTemplate> global = ObjectTemplate::New(); 

	 // Expose the local functions to script 
	 global->Set(String::New("load"), FunctionTemplate::New(Load)); 
	 global->Set(String::New("print"), FunctionTemplate::New(Print)); 
	 global->Set(String::New("quit"), FunctionTemplate::New(Quit)); 

	 // Create a new execution environment containing the built-in 
	 // functions 
	 Handle<Context> context = Context::New(NULL, global); 

	 // Enter the newly created execution environment. 
	 Context::Scope context_scope(context); 

	 // Enter main loop 
	 MainLoop(context); 

	 V8::Dispose(); 


	 return 0; 
 }

在 main 函數中,爲腳本提供了三個函數,load 函數用以將用戶指定的腳本加載進來,並放入全局的上下文中一邊引用,print 函數用以打印結果,而 quit 提供用戶退出計算器的功能。

測試一下:

 $ 1+2 
 3 

 $ (10+3)/(9.0-5) 
 3.25 

 $ typeof print 
 function 

 $ typeof non 
 undefined 

 // 自定義函數
 $ function add(a, b){return a+b;} 
 $ add(999, 2323) 
 3322 

 // 查看 print 標識符的內容
 $ print 
 function print() { [native code] }

load 函數提供了用戶自定義函數的功能,將腳本文件作爲一個字符串加載到內存,然後對該字符串編譯,求值,並將處理過的腳本對象放入當前 context 中,以便用戶使用。

 Handle<Value> Load(const Arguments& args){ 
	 if(args.Length() != 1){ 
		 return Undefined(); 
	 } 

	 HandleScope handle_scope; 
	 String::Utf8Value file(args[0]); 

	 Handle<String> source = ReadFile(*file); 
	 ExecuteString(source, String::New(*file), false); 

	 return Undefined(); 
 } 

而 ExecuteString 函數,負責將字符串編譯運行:

 bool ExecuteString(Handle<String> source, 
				   Handle<Value> name, 
				   bool print_result) 
 { 
	 HandleScope handle_scope; 
	 TryCatch try_catch; 
	 Handle<Script> script = Script::Compile(source, name); 
	 if (script.IsEmpty()) { 
		 return false; 
	 } else { 
		 Handle<Value> result = script->Run(); 
		 if (result.IsEmpty()) { 
			 return false; 
		 } else { 
			 if (print_result && !result->IsUndefined()) { 
				 String::Utf8Value str(result); 
				 const char* cstr = ToCString(str); 
				 printf("%s\n", cstr); 
			 } 
			 return true; 
		 } 
	 } 
 }

將下列內容存入一個文本文件,並命令爲 calc.js:

 function sum(){ 
	 var s = 0; 
	 for(var i = 0; i < arguments.length; i++){ 
		 s += arguments[i]; 
	 } 
	 return s; 
 } 

 function avg(){ 
	 var args = arguments; 
	 var count = args.length; 
	 var sum = 0; 
	 for(var i = 0; i < count; i++){ 
		 sum += args[i]; 
	 } 
	 return sum/count; 
 }

然後在計算器中測試:

 // 此時 sum 符號位定義
 $ typeof sum 
 undefined 

 // 加載文件,並求值
 $ load("calc.js") 

 // 可以看到,sum 的類型爲函數
 $ typeof sum 
 function 

 $ sum(1,2,3,4,5,6,7,8,9) 
 45

結束語

使用 V8 引擎,可以輕鬆的將腳本的好處帶進 C++ 應用,使得 C++ 應用更具靈活性,擴展性。我們在文中討論了基本的模板,如何使用 C++ 變量,函數,以及類。最後的實例中給出了一個計算器的原型。由於 V8 的設計原則,開發人員可以快速的將其嵌入到自己的應用中,並且無需太過擔心腳本語言的執行效率。


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