關於V8 JavaScript Engine的使用方法研究(轉)

一、寫在前面的話 
隨 着google io大會上對android 2.2系統展示,一個經過高度優化的android系統(從dalvik虛擬機,到瀏覽器)呈現在大家面前。開發者們會非常自然地將目光落在dalvik 虛擬機方面的改進(包括ndk工具對jni聯機單步調試的支持),很多應用接口的調整以及以此爲基礎的新的應用程序(偶是屬於那種喜新不厭舊,找抽性質的 人)。對於android 2.2在瀏覽器方面的優化和改進,在google io大會上只提到了已經全面支持v8 javascript引擎,這種引擎會將瀏覽器的運行速度提升2-3倍(儘管firefox已經官方發表聲明說他們在未來的firefox中會使用一個叫 做tracemonkey的javascript引擎,它要比v8更快,但目前來看v8引擎是所有現存javascript引擎中最快的)。

hoho, 好東西嘛,自然少不了偶了,下面偶就把自己對v8引擎的一些使用方面的心得體會簡單地寫一下,希望能夠對遊戲開發者或者應用程序引擎開發者有一些用處。 (稍微表達一下對google的意見,雖然android 2.2已經正式發佈了,但source code還沒有發佈出來,偶等得花兒都謝了。)

二、v8引擎特性簡介 
v8引擎的最根本的特性就是運行效率非常高,這得益於v8與衆不同的設計。
從技術角度來看,v8的設計主要有三個比較特別的地方:

(1)快速對象屬性存取機制 
javascript 這語言很邪門,很不規範,但是動態特性很高,甚至可以在運行時增加或減少對象的屬性,傳統的javascript引擎對於對象屬性存取機制的實現方法是 ——爲運行中的對象建立一個屬性字典,然後每次在腳本中存取對象屬性的時候,就去查這個字典,查到了就直接存取,查不到就新建一個屬性。

如 此設計雖然很方便,但是很多時間都浪費到這個“查字典”的工作上了。而v8則採取另外一種方式——hidden class(隱藏類?!偶怕翻譯得不貼切因此直接把原文寫上來了)鏈的方式,在腳本中每次爲對象添加一個新的屬性的時候,就以上一個hidden class爲父類,創建一個具有新屬性的hidden class的子類,如此往復遞歸進行,而且上述操作只在該對象第一次創建的時候進行一次,以後再遇到相同對象的時候,直接把最終版本的hidden class子類拿來用就是了,不用維護一個屬性字典,也不用重複創建。

這樣的設計體現了google裏面天才工程師們的才華(當然第一次運行的時候肯定要慢一些,所以google那邊強調,v8引擎在多重循環,以及重複操作一些對象的時候速度改善尤爲明顯,大概這種設計也是其中的一個原因吧,當然最主要的原因還在動態機器碼生成機制)

(2)動態機器碼生成機制 
這 一點可以類比一下java虛擬機裏面的jit(just in time)機制,地球人都知道,java的運行效率很低,尤其在使用多重循環(也就是,for循環裏面還有個for循環裏面還有for循 環*^&@*#^$。。。就當此註釋是廢話好了)的時候,sun爲了解決這個問題,在jvm虛擬機裏面加入了jit機制,就是在.class運行 的時候,把特別耗時的多重循環編譯成機器碼(也就是跟exe或elf中保存的代碼一樣的可執行二進制代碼),然後當下次再運行的時候,就直接用這些二進制 代碼跑,如此以來,自然運行效率就提高了。android 2.2在dalvik裏面也已經加入了jit技術,所以會有如此大的性能提升,但是對於一個javascript引擎中引入此技術來提高腳本的運行效率, 偶還是第一次看到(或許是偶孤陋寡聞了,歡迎對此有研究的朋友不吝斧正)。

這種設計在本文的下半部分,研究如何在c++程序中嵌入v8引擎、執行javascript腳本的時候,會有更加深入的理解,因爲每次運行腳本之前,首先要調用compile的函數,需要對腳本進行編譯,然後才能夠運行,由此可以看到動態代碼生成機制的影響深遠。
這 種設計的好處在於可以極大限度地加速javascript腳本運行,但是自然也有一些問題,那就是移植的問題,目前從v8的代碼上來看,v8已經支持 ia32(也就是x86了),arm,x64(64位的,偶現在還沒那麼幸運能用上64位的機器),mips(是apple們用的),其他的 javascript引擎,只需要把代碼重新編譯一下,理論上就能夠在其他不同的硬件平臺上跑了,但是從這個動態機器碼生成的機制來看,雖然v8很好,很 強大,但是把它弄到其他的平臺上似乎工作量不小。

(3)高效的垃圾回收機制 
垃圾回收,從原理上來說就是對象的引用計數,當一個對象不再被腳本中其他的對象使用了,就可以由垃圾回收器(garbage collector)將其釋放到系統的堆當中,以便於下一次繼續使用。
v8 採用的是stop-the-world(讓世界停止?!其實真正的意思就是在v8進行垃圾回收的時候,中斷整個腳本的執行,回收完成後再繼續執行腳本,如 此以來,可以集中全部cpu之力在較短的時間內完成垃圾回收任務,在正常運行過程中堅決不回收垃圾,讓全部cpu都用來運行腳本)垃圾回收機制。從偶的英 文水平來看,其他的描述,諸如:快速、正確、下一代之類的都是浮雲,stop-the-world纔是根本。

以上是偶對v8設計要點和特性方面的簡單研究,英語好的朋友可以無視偶在上面的聒噪,直接看v8的design elements原文,原文的地址如下:
http://code.google.com/apis/v8/design.html

三、下載和編譯v8的方法 
ok,既然v8引擎這麼好,那麼現在就開始動手,搞一個出來玩玩。與以往一樣,偶的開發環境是slackware13.1。
關於v8引擎的下載和編譯方法,英文好的朋友可以直接看google code上面的介紹,具體的鏈接地址如下:
http://code.google.com/apis/v8/build.html

偶在此只是簡單地把要點提一下,順便聊聊注意事項:
(1)v8可以在winxp, vista, mac os, linux(arm和intel的cpu都行)環境下編譯。

(2)基本的系統要求:
a、svn版本要大於等於1.4
b、win xp要打sp2補丁(現在最新的補丁應該是sp3了)
c、python版本要大於等於2.4
d、 scons版本要大於等於1.0.0(google這幫傢伙們還真能折騰,用gmake就那麼費勁嗎?非要弄個怪異的編譯工具,這個scons是基於 python的自動化編譯工具,功能上跟linux下面的Makefile非常類似,不一樣的是Makefile的腳本是gmake的語法,而scons 的配置腳本的語法則是python,看來v8引擎的開發者們是python的鐵桿粉絲,這個scons的安裝方法偶就不再聒噪了,python install setup.sh,相信熟悉python的朋友一定非常清楚了。)
e、gcc編譯器的版本要大於4.x.x

(3)v8的下載地址:
svn checkout http://v8.googlecode.com/svn/trunk/ v8-read-only

(4)基本的編譯方法:
a、查看v8配置腳本中參數的方法:scons --help
b、查看scons命令本身提供參數的方法:scons -H (這裏的“H”一定要大寫)
c、設置環境變量:
export GCC_VERSION=44(這個一定要設置,否則會導致一大堆錯誤,天知道google guys們是如何編寫scons的配置腳本的,個人感覺他們寫這個編譯腳本的時候應該是用mac book,在leopard系統上玩的,而偶還在用價廉物美的lenovo,使用slackware。。。)
d、開始編譯,編譯的命令很簡單:scons mode=release library=shared snapshot=on
e、 經過漫長的編譯過程,會看到一個叫做libv8.so的庫(當然用library=static可以編譯出libv8.a的靜態庫),把這個so庫手工拷 貝到/usr/local/lib,然後,ldconfig一下就好了,然乎把v8-read-only/include目錄下的幾個.h文件拷貝到 /usr/local/include目錄下。到此爲止,v8引擎已經順利地安裝到了機器上。
f、經過e以後,我們可以簡單地測試一下是否能夠工作。還需要編譯一個可執行程序出來,例如——shell程序。編譯的方法非常簡單:scons sample=shell,然後就是等待即可。

好了,經過上面的過程,大家應該能夠很順利地生成libv8.so這個庫了,下一步偶開始研究如何在自己的c++代碼中調用這個庫了。

四、v8引擎的調用方法 
1、基本概念 
在使用v8引擎之前,必須知道三個基本概念:句柄(handle),作用域(scope),上下文環境(context,大爺的老外的這個context就是繞口,沒法翻譯成中文,可以簡單地理解爲運行環境也可以)
(1)句柄(Handle) 
從實質上來說,每一個句柄就是一個指向v8對象的指針,所有的v8對象必須使用句柄來操作。這是先決條件,如果一個v8對象沒有任何句柄與之相關聯,那麼這個對象很快就會被垃圾回收器給幹掉(句柄跟對象的引用計數有很大關係)。

(2)作用域(Scope) 
從 概念上理解,作用域可以看成是一個句柄的容器,在一個作用域裏面可以有很多很多個句柄(也就是說,一個scope裏面可以包含很多很多個v8引擎相關的對 象),句柄指向的對象是可以一個一個單獨地釋放的,但是很多時候(尤其是寫一些“有用”的程序的時候),一個一個地釋放句柄過於繁瑣,取而代之的是,可以 釋放一個scope,那麼包含在這個scope中的所有handle就都會被統一釋放掉了。

(3)上下文環境(Context) 
從 概念上講,這個上下文環境(以前看一些中文的技術資料總出現這個詞,天知道當初作者們是如何想的,不過這事情就是約定俗成,大家都這麼叫也就習慣了)也可 以理解爲運行環境。這就好比是linux的環境變量,在執行javascript腳本的時候,總要有一些環境變量或者全局函數(這些就不用偶解釋了吧?! 就是那些直接拿過來就用,根本不需要關心這些變量或者函數在什麼地方定義的)。偶們如果要在自己的c++代碼中嵌入v8引擎,自然希望提供一些c++編寫 的函數或者模塊,讓其他用戶從腳本中直接調用,這樣纔會體現出javascript的強大。從概念上來講,java開發中,有些功能jvm不提供,大家可 以用c/c++編寫jni模塊,通過java調用c/c++模塊來實現那些功能。而類比到javascript引擎,偶們可以用c++編寫全局函數,讓其 他人通過javascript進行調用,這樣,就無形中擴展了javascript的功能。java+jni的開發模式與 javascript+c++module是一樣的思路,只是java更加複雜,系統庫更加豐富;而javascript相對java來說比較簡單,系統 庫比較少。僅此而已。

2、開始在c++代碼中嵌入v8引擎 
(1)基本的編譯方法 
基 本的編譯方法很簡單,只要上面安裝v8引擎的過程中沒有什麼問題,就可以直接把v8引擎作爲一個普通的動態鏈接庫來使用,例如:在編譯的時候加入 -I/usr/local/include,在鏈接的時候加入-L/usr/local/lib -lv8就足夠了。這裏需要提一句,由於v8引擎是完全使用c++編寫的(hoho,最近linus在blog上跟人吵架,聲稱c++是垃圾程序員使用的 垃圾語言,鬧得沸沸揚揚。偶也十分喜歡c語言,但是在此不對linus的言論做任何評論,好東西嘛能用、會用就是了。)

例如:
g++ -c test.cpp -I/usr/local/include 
g++ -o test test.o -L/usr/local/lib -lv8

(2)在使用v8引擎中定義的變量和函數之前,一定不要忘記導入v8的名字空間 
using namespace v8;

(3)在c++程序中簡單地執行v8腳本引擎的方法如下: 
// 創建scope對象,該對象銷燬後,下面的所有handle就都銷燬了
  HandleScope handle_scope ;  

// 創建ObjectTemplate對象,這個對象可以用來註冊c++的全局函數供給javascript調用
// 在此演示中先可以忽略
  Handle<ObjectTemplate> global_templ = ObjectTemplate::New() ; 

// 創建運行環境
  Handle<Context> exec_context ;

// 創建javascript腳本的存儲對象,該對象存放從文件中讀取的腳本字符串
  Handle<String> js_source ; 

// 創建用於存放編譯後的腳本代碼的對想
  Handle<Script> js_compiled ; 

// 從文件中把javascript腳本讀入js_source對象
  js_source = load_js(js_fname) ; 

// 把c++編寫的函數註冊到全局的ObjectTemplate對象中,
// 例如,在偶的代碼中,有一個叫做set_draw_color的函數,那麼這個函數在javascript腳本
// 中如果希望調用,應該叫什麼名字呢?這一句——String::New("set_draw_color")就用來指定
// 在腳本中的函數名稱,FunctionTemplate用來表示在c++中的函數,利用指向函數的指針把該函數
// 封裝成函數對象。以下的幾個Set都是相同的功能,就是用來把c++函數註冊到腳本的運行環境中。
  global_templ->Set(String::New("set_draw_color"), 
                    FunctionTemplate::New(set_draw_color)) ; 

  global_templ->Set(String::New("draw_line"), 
                    FunctionTemplate::New(draw_line)) ; 

  global_templ->Set(String::New("commit"), 
                    FunctionTemplate::New(commit)) ; 

  global_templ->Set(String::New("clear"), 
                    FunctionTemplate::New(clear)) ; 

  global_templ->Set(String::New("draw_bmp"), 
                    FunctionTemplate::New(draw_bmp)) ; 

// 新建執行對象,把剛剛註冊了c++函數的global_templ關聯到腳本的運行環境中去
  exec_context = Context::New(NULL, global_templ) ; 

// 創建運行環境的作用域,當然,言外之意,v8可以支持多個配置不同的運行環境
  Context::Scope context_scope(exec_context) ; 

// 注意,這裏就是編譯javascript腳本的源代碼了
  js_compiled = Script::Compile(js_source) ; 
  if(js_compiled.IsEmpty()) {
    LOG("run_js, js_compiled is empty!") ; 
    return ; 
  }
  
// 最後這一句就是運行,執行剛剛從文件中載入以及編譯的javascript腳本了
  js_compiled->Run() ; 

(4)由javascript調用的c++模塊的編寫方法 
以剛剛的set_draw_color這個函數爲例,在javascript中的調用方法假定爲:
set_draw_color(r, g, b) ; 
例如:
// 設置爲紅色
set_draw_color(255, 0, 0) ; 

雖然調用此函數看上去非常簡單,但在c++中該如何編寫這個函數呢?該如何從javascript中得到相應的行參呢?
參見如下代碼:
static Handle<Value> set_draw_color(const Arguments & args) {
  int r, g, b ; 
  if(args.Length() == 3) {
    r = args[0]->Int32Value() ; 
    g = args[1]->Int32Value() ; 
    b = args[2]->Int32Value() ; 
    g_canv_ptr->SetDrawColor(r, g, b) ; 
  }

  return Undefined() ; 
}

這 裏的const Arguments & args就用來解決從javascript向c++傳遞參數的問題。args.Length()用來返回在javascript腳本中一共傳入了多少個參 數,而Arguments類本身是重載了“[]”運算符的,因此,可以使用類似普通數組的下標的方式對參數進行存取。至於Int32Value()這類的 函數,是在Handle<Value>類中有定義的,可以通過查看v8.h頭文件得到所有的Handle類型對象的定義,例 如:Handle<Number>,Handle<Integer>,Handle<String>,Handle<Function> 等等,總之,源碼之下了無祕密,大家可以查看源代碼得到所有問題的解答。

(5)從c++代碼中調用javascript腳本中編寫的函數的方法 
javascript 調用c++函數,只是實現了單方向地調用;那麼如何在v8中實現雙方向的調用呢?也就是由c++代碼去調用javascript中的函數。這一點十分有 用,例如,偶可以在c++代碼中捕獲鍵盤或鼠標事件,對於這些事件的處理方法(例如:鼠標在屏幕上的座標,鍵盤按下的鍵值),則可以把c++代碼中採集到 的數據傳入腳本中定義的函數,根據腳本上定義的函數去處理,由此可以極大地加強c++代碼的靈活性。

例如,偶在javascript中定義了一個OnClick函數,作用是在鼠標點擊的地方貼一張圖片,那麼偶的javascript可以這樣寫:
function OnClick(x, y) {
    draw_bmp(x, y, 4) ; 
    commit() ; 
}

先不論具體的實現細節,先看這個函數的參數,x和y,那麼偶該如何從c++代碼中把鼠標點按的x和y座標傳給OnClick函數呢?畢竟這個函數是在javascript中定義的。

具體的方法其實很簡單,前半部分與定義和調用javascript的步驟一致,只不過從js_compiled->Run(),這一句以後,還沒有完,還要繼續做下面的事情:
  Handle<String> js_func_name ; 
  Handle<Value>  js_func_val ; 
  Handle<Function> js_func ; 
  Handle<Value>  argv[argc] ; 
  Handle<Integer> int_x ; 
  Handle<Integer> int_y ; 

// 這一句是創建函數名對象
  js_func_name = String::New("OnClick") ; 

// 從全局運行環境中進行查找,看看是否存在一個叫做“OnClick”的函數
  js_func_val = exec_context->Global()->Get(js_func_name) ; 
  if(!js_func_val->IsFunction()) {
    LOG("on_click, js_func_val->IsFunction check failed!") ; 
  } else {

// 利用handle的強制類型轉換,把js_func_val轉換成一個函數對象
    js_func = Handle<Function>::Cast(js_func_val) ;

// 初始化參數,所有數據都要定義成javascript可以識別的數據類型,例如Integer對象
// javascript中是沒有內建數據類型的(int, char, short是c/c++中的用的類型) 
    int_x = Integer::New(x) ; 
    int_y = Integer::New(y) ; 

// 把這些對象放到argv數組中去
    argv[0] = int_x ; 
    argv[1] = int_y ; 

// 利用函數對象去調用該函數,當然需要傳入腳本的運行環境,以及參數個數和參數的值。
    js_func->Call(exec_context->Global(), argc, argv) ; 
  }

ok,到此爲止,偶已經把c++->javascript以及javascript->c++的雙向調用,以及參數傳遞方法講完了。
其他的v8引擎的特性還需要進一步探索和研究。

偶 自己寫了一個簡單的驗證程序,該程序使用sdl庫來作爲c/c++模塊的繪圖工具,然後向v8導出了若干繪圖函數(例如畫線,貼圖等函數),然後通過 javascript在屏幕上可以隨心所欲地畫圖。本程序在linux下面編譯和運行通過,此驗證效果還不錯,包含了v8引擎的c++和 javascript代碼之間雙向調用和通信,現在把代碼分享出來供大家研究和參考。

 

from:http://my.chinaunix.net/space.php?uid=20221192&do=blog&id=353078

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