什麼是JavaScript引擎
什麼是JavaScript引擎?簡單來講,就是能夠提供執行JavaScript代碼的運行環境。要解釋這一概念,需要了解一些編譯原理的基礎概念和現代語言需要的一些新編譯技術。
首先來看C/C++語言。它們是比較悠久的語言了,實際上就是使用編譯器直接將它們編譯成本地代碼,這一切都是由開發人員在代碼編寫完成之後實施。 用戶只是使用這些編譯好的本地代碼,這些本地代碼被系統的加載器加載執行,這些本地代碼(也就是機器指令)由操作系統調度CPU直接執行,無需其它額外的 輔助虛擬機等。這一過程基本上是從源代碼開始,然後抽象語法樹,之後中間表示,最後到本地代碼。
其次,來看看Python等腳本語言。處理腳本語言通常的做法是開發者將寫好的代碼直接交給用戶,用戶使用腳本的解釋器將腳本文件加載然後解釋執 行。當然,現在Python也可以支持將腳本編譯生成中間表示,當然這是後話。所以這表示,對於腳本語言,沒有開發人員的編譯過程,當然也不是絕對,這主 要因爲使用場景不一樣,它的目的不是高性能。這一過程是源代碼,到抽象語法樹,再到解釋器解釋執行。
然後來看看Java語言。其做法可以理解爲比較明顯的兩個階段,首先是像C++語言一樣的編譯器,但是,同C++編譯器生成的本地代碼結果不同,經 過編譯器編譯之後的是字節碼,字節碼是平臺無關的。在運行字節碼階段,Java的運行環境也就是Java虛擬機會加載字節碼,使用解釋執行這些字節碼。如 果僅是這樣,那Java的性能就差C++太多了。現代Java虛擬機一般都引入了JIT技術,也就是前面說的將字節碼轉變成本地代碼來提高執行效率。這主 要兩個階段,第一階段對時間要求不嚴格,第二階段則對每個步驟所花費的時間非常敏感,時間越短越好。
最後回到JavaScript語言上來。前面已經說了它是一種解釋性腳本語言。是的,它的確是,但是隨着衆多工程師不斷投入資源來提高它的速度,這 使得它能夠使用了Java虛擬機和C++編譯器中衆多的技術,它的工作方式也在演變。早期也是一樣由解釋器來解釋它們即可,就是將源代碼轉變成抽象語法 樹,然後在抽象語法樹上解釋執行。隨着將Java虛擬機的JIT技術引入,現在的做法是將抽象語法樹轉成中間表示(也就是字節碼),然後通過JIT技術轉 成本地代碼,這能夠大大的提高了執行效率。當然也有些做法直接從抽象語法樹生成本地代碼的JIT技術,例如V8。這是因爲JavaScript跟Java 還是有以下一些區別的:
其一當然是類型,JavaScript是無類型的語言,這使得對於對象的表示和屬性的訪問上比Java存在比較大的性能損失。不過現在有一些新的技術,參考C++或者Java的類型系統的優點,構建隱式的類型信息,這些後面逐一介紹。
其二是Java語言通常是將源代碼編譯成字節碼,這個同執行階段是分開的,也就是從源代碼到抽象語法樹到字節碼這段時間的長短是無所謂的(或者說不 是特別重要),所以主要是儘可能的生成高效的字節碼即可。而對於JavaScript而言,這些都是在網頁和JavaScript文件下載後同執行階段一 起在網頁的加載和渲染過程中來實施的,所以對它們的處理時間也有着很高的要求。
描述JavaScript代碼執行的過程,這一過程中因爲不同技術的引入,導致非常複雜,而且因爲都是在代碼運行過程中來處理這些步驟,所以每個階 段時間越少越好,而且每引入一個階段都是額外的時間開銷,可能最後的本地代碼執行效率很高,但是之前步驟如果耗費太多時間的話,最後的執行結果可能並不會 好。所以不同的JavaScript引擎選擇了不同的路徑,這裏先不仔細介紹,後面再描述它們。
所以一個JavaScript引擎不外乎包括以下部分:
第一, 編譯器。主要工作是將源代碼編譯成抽象語法樹,然後在某些引擎中還包含將抽象語法樹轉換成字節碼。
第二, 解釋器。在某些引擎中,解釋器主要是接受字節碼,解釋執行這個字節碼,然後也依賴來及回收機制等。
第三, JIT工具。一個能夠能夠JIT的工具,將字節碼或者抽象語法樹轉換成本地代碼,當然它也需要依賴牢記
第四, 垃圾回收器和分析工具(profiler)。它們負責垃圾回收和收集引擎中的信息,幫助改善引擎的性能和功效。
JavaScript引擎和渲染引擎
前面介紹了網頁的工作過程需要使用到兩個引擎,也就是渲染引擎和JavaScript引擎。從模塊上看,目前,它們是兩個獨立的模塊,分別負責不同 的事情:JavaScript引擎負責執行JavaScript代碼,而渲染引擎負責渲染網頁。JavaScript引擎提供調用接口被渲染引擎使用,渲 染引擎使用JavaScript引擎來處理JavaScript代碼並獲取結果。這當然不是全部,事情不是這麼簡單,JavaScript引擎需要能夠訪 問渲染引擎構建的DOM樹,所以JavaScript引擎通常需要提供橋接的接口,而渲染引擎則根據橋接接口來提供讓JavaScript訪問DOM的能 力。在現在衆多的HTML5能力中,很多都是通過JavaScript接口提供給開發者的,所以這部分同樣需要根據橋接接口來實現具體類,以便讓 JavaScript引擎能夠回調渲染引擎的具體實現。下圖是一個簡單的關於兩者引擎的關係。在WebKit/Blink項目中,這兩種引擎通過橋接接口來訪問DOM結構,這對性能來說是一個重大的損失,因爲每次JavaScript代碼訪問 DOM都需要通過複雜和低效的橋接接口來完成。鑑於訪問DOM樹的普遍性,這是一個常見的問題。希望以後可以減少這一方面的開銷。
JavaScriptCore引擎和V8引擎
WebKit中的JavaScript引擎
在WebKit項目中,最初只有JavaScriptCore引擎。在Blink還獨立出來之前,爲了支持不同的JavaScript引 擎,WebKit設計了一套接口可以切換使用不同的JavaScript引擎(事實上,這一接口會降低性能),所以,WebKit當時可以支持兩種類型的 JavaScript引擎,那就是JavaScriptCore引擎和V8引擎。兩者都是基於WebKit所提供的接口來同渲染引擎協同工作。
JavaScriptCore引擎
JavaScriptCore引擎是WebKit中默認的引擎,在早期階段,它的性能不是特別突出。特別是,它只有解釋器來解釋執行 JavaScript代碼。這一效率十分的低效,當然從2008年開始,JavaScriptCore引擎開始一個新的優化工作,就是重新實現了編譯器和 字節碼解釋器,這就是SquirrelFish。該工作對於引擎的性能優化作了比較大的改進。隨後,蘋果內部代號爲”Nitro”的JavaScript 引擎也是基於JavaScriptCore項目的,它的性能還是非常出色的,鑑於其是內部項目,所以具體還有什麼特別的處理就不得而知了。在這之後,開發 者們又將內嵌緩存、基於正則表達式的JIT和簡單的JIT引入到JavaScriptCore中。然後,又陸續加入的字節碼解釋器。可以看 出,JavaScriptCore引擎也在不斷的高速發展中。
上圖是JavaScriptCore最簡單的處理部分,它主要是將源代碼翻譯成抽象語法樹,之後是平臺無關的字節碼,在最初的版本中,字節碼會被 JavaScriptCore引擎解釋執行。在後面的版本中,逐漸加入了JIT編譯器,將熱點函數生成本地代碼,後面再詳細介紹它們。
V8引擎
V8是一個開源項目,也是一個JavaScript引擎的實現。最開始是一幫語言方面的專家設計出來的,之後被Google收購,成爲了 JavaScript引擎和衆多相關技術的引領者。它的目的很簡單,就是爲了提高性能,除了性能還是性能。因爲之前不管JavaScriptCore引擎 還是其它JavaScript引擎,在當時的情況下,它們的性能都不能令人非常滿意。爲了達到高性能的JavaScript代碼執行效率從而獲得更好的網 頁瀏覽效果,它甚至採用直接將JavaScript編譯成本地代碼的方式。
V8支持衆多的操作系統,包括但是不限於Windows、Linux、Android、Mac OS X等。同時它也是能夠支持衆多的硬件架構,例如IA32、X64、ARM、MIPS等。這麼看下來,它將主流軟硬件平臺一網打盡,由於它是一個開源項目, 開發者可以自由的使用它的強大能力,一個例子就是目前炙手可熱的NodeJS項目,它就是基於V8項目的。開源的好處就是大家可以很方便地學習、貢獻和使 用,下圖是V8引擎最基礎的代碼執行過程。
從圖中可以看出,首先它也是將源代碼轉變成抽象語法樹的,這一點同JavaScriptCore引擎一樣,之後兩個引擎開始分道揚鑣。不同於 JavaScriptCore引擎,V8引擎並不將抽象語法樹轉變成字節碼或者其它中間表示,而是通過JIT編譯器的全代碼生成器(full code generator)從抽象語法樹直接生成本地代碼,所以沒有像Java一樣的虛擬機或者字節碼解釋器。這樣做的原因,主要是因爲減少這抽象語法樹到字節 碼的轉換時間,這一切都在網頁加載時候完成,雖然可以提高優化的可能,但是這些分析可能帶來巨大的時間浪費。當然,缺點也很明顯,至少包括兩點:第一是某 些JavaScript使用場景其實使用解釋器更爲合適,因爲沒有必要生成本地代碼;第二是因爲沒有中間表示,會減少優化的機會因爲缺少一箇中間表示層。 至於有些文章說的喪失了移植性,個人覺得對於JavaScript這種語言來說不是問題,因爲並沒有將JavaScript代碼先編譯然後再運行的明顯兩 個分開階段的用法,例如像Java語言那樣。但是,針對V8設計思想來說,筆者認爲它的理念比較先進,做法雖然比較激進,但是確實給JavaScript 引擎設計者們帶來很多新思路。
在之後的版本中,V8工程師們引入了Crankshaft編譯器,它能夠對熱點的JS函數進行生成的分析和優化後再生成本地代碼,原因在於不是所有 的JavaScript代碼都合適做如此深層次的優化,因爲優化本身也需要花費一定的時間。有關V8的細節,希望以後有時間能夠介紹它們。
目前V8和JavaScriptCore引擎各有特色,相信兩者的發展能夠不斷推進JavaScript語言的執行性能
1. 概要
JavaScript是一種廣泛用於Web客戶端開發的腳本語言, 常用來控制瀏覽器的DOM樹,給HTML網頁添加動態功能。目前JavaScript遵循的web標準的是ECMAScript262。由於 JavaScript提供了豐富的內置函數、良好的對象機制。所以JavaScript還可以嵌入到某一種宿主語言中,彌補宿主語言的表現力,從而實現快 速、靈活、可定製的開發。
現有的主流瀏覽器基本上都實現了一個自己的JavaScript引擎。這些JavaScript引擎可以分析、編譯和執行JavaScript 腳本。這些JavaScript引擎都是用C或者C++語言寫的,都對外提供了API接口。所以在C、C++語言中使用這些JavaScript引擎,嵌 入JavaScript是非常方便的。有一些著名的開源項目都使用了這一種方式,來進行混合的編程,比如Node.js, K-3D等。
已知著名的JavaScript引擎有Google的V8引擎、IE的Trident 引擎、Firefox的SpiderMonkey引擎、Webkit的JavaScriptCore引擎、Opera的Carakan引擎(非開源的,本 文沒有分析)等。這些JavaScript引擎對外提供的API接口在細節上各不相同,但是這些API的一個基本的設計思路都類似。C、C++要使用這些 引擎,首先要獲得一個全局的Global對象。這個全局的Global對象有屬性、方法、事件。比如在JavaScript環境中有一個window窗口 對象。它描述的是一個瀏覽器窗口。一般JavaScript要引用它的屬性和方法時,不需要用“window.xxx”這種形式,而直接使用“xxx”。 它是JavaScript中最大的對象,所有的其他JavaScript對象、函數或者是它的子對象,或者是子對象的子對象。C、C++通過對這個最大的 Global對象調用get、set操作就可以實現與JavaScript進行雙向交互了。
下面的介紹涉及到比較多的代碼細節,先給個結論吧,不想看C++細節代碼可以不看了。
編寫語言 | API接口 | C、C++與JavaScript交互(變量、函數、類) |
windows xp
vc2005編譯 靜態庫的大小 |
示例EXE的大小 | 執行、解析JavaScript的速度 | |
Google V8 | C++ | C++ | 可以 | 23.1M | 1.1M | 最快 |
Firefox3.5以前 SpiderMonkey | C | C | 可以 | 1.3M | 500K | 慢 |
Firefox高版本SpiderMonkey | C++ | C | 可以 | 15.3M | 1.7M | 一般 |
Webkit JavaScriptCore | C++ | C | 可以 | 26.2M | 1.4M | 一般 |
IE | 未知 | COM | 可以 | 未知 | 100K(沒有鏈接庫) | 一般 |
如果優先考慮庫的體積,建議使用Firefox的老版本。對執行效率有要求的話,建議使用V8。
2. Google V8
2.1. 介紹
Google Chrome是google 2008年9月發佈的瀏覽器,Chrome的網頁渲染部分使用的是Webkit的渲染引擎,Chrome的JavaScript引擎就是大名鼎鼎的V8 了。V8是C++語言編寫的,是開放源碼的,是所有的JavaScript引擎中速度最塊的。其開源項目地址爲:http://code.google.com/p/v8。
V8對外的API接口是C++的接口。V8的API定義了幾個基本概念:句柄(handle),作用域(scope),上下文環境(Context)。模板(Templates),瞭解這些基本的概念纔可以使用V8。
l 上 下文環境Context就是腳本的運行環境,JavaScript的變量、函數等都存在於上下文環境Context中。Context可以嵌套,即當前函 數有一個Context,調用其它函數時如果又有一個Context,則在被調用的函數中javascript是以最近的Context爲準的,當退出這 個函數時,又恢復到了原來的Context。
l 句柄(handle)就是一個指向V8對象的指針,有點像C++的智能指針。所有的v8對象必須使用句柄來操作。沒有句柄指向的V8對象,很快會被垃圾回收器回收了。
l 作用域(scope)是句柄的容器,一個作用域(scope)可以有很多句柄(handle)。當離開一個作用域(scope)時,所有在作用域(scope)裏的句柄(handle)都會被釋放了。
l 模板(Templates)分爲函數模板和對象模板,是V8對JavaScript的函數和對象的封裝。方便C++語言操作JavaScript的函數和對象。
l V8 API定義了一組類或者模板,用來與JavaScript的語言概念一一對應。比如:
V8的 Function模板與JavaScript的函數對應
V8的Object類與JavaScript的對象對應
V8的String類與JavaScript的字符對應
V8的Script類與JavaScript的腳本文本對應,它可以編譯並執行一段腳本。
2.2. C++調用JavaScript
使用V8,在C++中訪問Javascript腳本中的內容,首先要調用Context::GetCurrent()->Global()獲取到Global全局對象,再通過Global全局對象的Get函數來提取Javascript的全局變量、全局函數、全局複雜對象。C++代碼示例如下:
//獲取Global對象
Handle<Object>globalObj = Context::GetCurrent()->Global();
//獲取Javascrip全局變量
Handle<Value>value = globalObj->Get(String::New(“JavaScript變量名”));
intn = value ->ToInt32()->Value();
//獲取Javascrip全局函數,並調用全局函數
Handle<Value>value = globalObj->Get(String::New(“JavaScript函數名”));
Handle<Function> func = Handle<Function>::Cast(value) ;//轉換爲函數
Local<Value> v1 = Int32::New(0);
Local<Value> v2 = Int32::New(1);
Handle<Value> args[2] = { v1, v2 }; //函數參數
func->Call(globalObj, 2, args);
//獲取Javascrip全局對象,並調用對象的函數
Handle<Value>value = globalObj->Get(String::New(“JavaScript複雜對象名”));
Handle<Object> obj = Handle<Object>::Cast(value);//轉換爲複雜對象
Handle<Value> objFunc = obj ->Get(String::New(“JavaScript對象函數名”));
Handle<Value> args[] = {String::New(“callobject function “)};//函數參數
objFunc->Call(globalObj, 1, args);
2.3. JavaScript調用C++
使用V8,在Javascript腳本中想要訪問C++中的內容,必須先將C++的變 量、函數、類注入到Javacsript中。注入時,首先要調用Context::GetCurrent()->Global()獲取到 Global對象,再通過Global對象的Set函數來注入全局變量、全局函數、類對象。
全局變量、全局函數的注入過程與上一節的代碼類似,這裏就省略不寫了。比較麻煩的是將C++的類、類對象注入到Javascript中。其基本的過程如下:
1. 首先定義一個C++類,定義一個類對象指針
2. 定義一組C++全局函數,封裝V8對C++類的調用,提供給V8進行CALLBACK回調。
3. 最後調用V8 API,將定義的C++類和C++函數注入到Javascript中
在V8的API接口中,還提供了一個“內部數據”(Internal Field)的概念,“內部數據”就是允許V8對象保存一個C++的void*指針。當V8回調C++全局函數時,C++可以設置或者獲取該void*指針。
下面是一個將C++類、類變量注入到Javascript中的C++代碼示例。
//一個C++類test、
class test
{
public:
test(){number=0;};
voidfunc(){number++;}
int number;
};
//一個全局對象g_test
//目的是:在Javascript中可以直接使用這個對象,例如g_test.func()
test g_test;
//封裝V8調用test類構造函數
//在Javascript中如果執行:var t = new test;V8就會調用這個C++函數
//在C++中執行NewInstance函數注入對象時,也會調用這個函數
//默認的test類的構造函數沒有參數,C++注入對象時,提供一個額外的參數
v8::Handle<v8::Value> testConstructor(constv8::Arguments& args)
{
v8::Local<v8::Object>self = args.Holder();
//這裏假定有兩個“內部數據”(Internal Field)
//第一個“內部數據”保存test對象的指針
//第二個“內部數據”爲1 就表示這個對象是由C++注入的
//第二個“內部數據”爲0 就表示這個對象是JS中自己建立的
if(args.Length())
{
//默認爲0,當C++注入對象時,會填充這個“內部數據”
self->SetInternalField(0,v8::External::New(0));
self->SetInternalField(1,v8::Int32::New(1));
}
else
{
self->SetInternalField(0,v8::External::New(new test));
self->SetInternalField(1,v8::Int32::New(0));
}
return self;
}
//封裝V8調用test類func方法
//在Javascript中如果執行:t.func();V8就會調用這個C++函數
v8::Handle<v8::Value> testFunc(constv8::Arguments& args)
{
//獲取構造函數testConstructor時,設置的對象指針
v8::Local<v8::Object>self = args.Holder();
v8::Local<v8::External> wrap =v8::Local<v8::External>::Cast(self->GetInternalField(0));
void* ptr =wrap->Value();
//調用類方法
static_cast<test*>(ptr)->func();
returnv8::Undefined();
}
//封裝V8調用test類成員變量number
//在Javascript中如果執行:t.number;V8就會調用這個C++函數
v8::Handle<v8::Value>getTestNumber(v8::Local<v8::String> property, const v8::AccessorInfo& info)
{
//獲取構造函數testConstructor時,設置的對象指針
v8::Local<v8::Object>self = info.Holder();
v8::Local<v8::External> wrap =v8::Local<v8::External>::Cast(self->GetInternalField(0));
void* ptr =wrap->Value();
//返回類變量
returnv8::Int32::New(static_cast<test*>(ptr)->number);
}
//C++類和全局的函數定義好以後,就可以開始將類、變量注入V8 Javascript中了
//獲取global對象
v8::Handle<v8::Object> globalObj =context->Global();
//新建一個函數模板,testConstructor是上面定義的全局函數
v8::Handle<v8::FunctionTemplate> test_templ =v8::FunctionTemplate::New(testConstructor);
//設置類名稱
test_templ->SetClassName(v8::String::New(“test”));
//獲取Prototype,
v8::Handle<v8::ObjectTemplate> test_proto =test_templ->PrototypeTemplate();
//增加類成員函數,testFunc是上面定義的全局函數
test_proto->Set(“func”,v8::FunctionTemplate::New(testFunc));
//設置兩個內部數據,用於構造函數testConstructor時,存放類對象的指針。
v8::Handle<v8::ObjectTemplate> test_inst =test_templ->InstanceTemplate();
test_inst->SetInternalFieldCount(2)
//增加類成員變量
//getTestNumber是上面定義的全局函數
//只提供了成員變量的get操作,最後一個參數是成員變量的set操作,這裏省略了
test_inst->SetAccessor(v8::String::New(“number”),getTestNumber, 0);
//將test類的定義注入到Javascript中
v8::Handle<v8::Function> point_ctor =test_templ->GetFunction();
globalObj->Set(v8::String::New(“test”),point_ctor);
//新建一個test對象,並使得g_test綁定新建的對象
v8::Handle<v8::Value> flag = v8::Int32::New(1);
v8::Handle<v8::Object> obj =point_ctor->NewInstance(1, &flag);
obj->SetInternalField(0, v8::External::New(&g_test));
globalObj->Set(v8::String::New(“g_test”), obj);
將C++類和類指針注入到V8 JavaScript後,在JavaScript中就可以這樣使用了:
g_test.func();
var n = g_test.number;
var t = new test;
3. Firefox SpiderMonkey
3.1. 介紹
Firefox的JavaScript引擎是 SpiderMonkey,SpiderMonkey包括解析器、編譯器和JIT即時編譯器。JIT即時編譯器是JavaScript引擎的核心部分,它 決定了一個JavaScript引擎的效率和速度。Firefox的JIT即時編譯器有很多個版本,最早的JIT名稱是TraceMonkey, 現在使用的是JägerMonkey,未來準備開發的是IonMonkey。在一些通用的JavaScript測試標準(比如SunSpider) 中,Firefox的JavaScript引擎表現都不好,比IE、V8的執行速度差。Firefox的JS引擎源碼地 址:http://ftp.mozilla.org/pub/mozilla.org/js/
SpiderMonkey對外的API接口是C語言的。與WebkitJavaScriptCore的API比較類似,SpiderMonkeyAPI的主要數據結構有:
l JSRuntime:JSRuntime是內存空間,在執行JS函數或腳本之前,首先要調用JS_NewRunTime來初始化一個JSRuntime,JS_NewRunTime只有一個參數,就是內存的大小,當JS引擎使用的內存超出了指定的大小,垃圾回收器就會啓動運行。
l JSContext:JavaScript全局上下文。也就是JavaScript的執行環境。
l jsval:JavaScript中的變量
l JSObject:JavaScript中的對象
l JSFunction:JavaScript中的函數
l JSString:JavaScript中的字符
SpiderMonkey API的主要函數有:
l JS_NewRuntime JS_DestroyRuntime:新建和銷燬JSRuntime
l JS_NewContext JS_DestroyContext:新建和銷燬JSContext
l JS_NewObject:新建一個對象
l JS_SetGlobalObject JS_GetGlobalObject:設置和獲取全局對象
l JS_GetProperty JS_SetProperty:JavaScript對象的屬性操作
l JS_CallFunctionName:調用JavaScript函數
l JS_DefineFunctions:定義一組JavaScript函數
l JS_InitClass:定義一個JavaScript類
l JS_ExecuteScript:執行JavaScript腳本
SpiderMonkey API還定義了一些宏,用來在jsval與C++類型之間裝換
l JSVAL_TO_OBJECT JSVAL_TO_STRING JSVAL_TO_INT:將jsval裝換爲C++類型
l OBJECT_TO_JSVAL STRING_TO_JSVAL JSVAL_TO_INT:將C++類型裝換爲jsval
3.2. C++調用JavaScript
使用SpiderMonkey,在C++中訪問Javascript腳本中的內容,首先要調用JS_GetGlobalObject獲取到Global全局對象,再調用JS_GetProperty函數來提取Javascript的全局變量、全局函數、全局複雜對象。示例如下:
JSRuntime *rt;
JSContext*cx;
JSObject *glob;
//新建JSRuntime
rt = JS_NewRuntime(64L * 1024L* 1024L);
//新建JavaScript全局上下文
cx = JS_NewContext(rt,gStackChunkSize);
//新建JavaScript全局Global對象
glob = JS_NewObject(cx,&global_class, NULL,NULL);
JS_SetGlobalObject(cx, glob);
//獲取JavaScript的全局變量
jsval var;
JSBool success = JS_GetProperty(cx, glob, “JavaScript全局變量名”, &var);
long value = JSVAL_TO_INT(var);
//調用全局函數
jsval args[1]; //函數參數
arg[0] = INT_TO_JSVAL(1);
jsval ret;
success= JS_CallFunctionName(cx,glob, “JavaScript全局函數名”, 1, args, &ret);
//處理返回值
long value = JSVAL_TO_INT(ret);
//獲取JS複雜對象
sval object;
success = JS_GetProperty(cx,glob, “JS複雜對象名”, & object);
JSObject* obj = JSVAL_TO_OBJECT(var);
jsval retValue;
//調用對象的方法,這裏省略了參數和返回值的處理
success = JS_CallFunctionName(cx,obj, “JS複雜對象的方法名”, 0, 0, &retValue);
3.3. JavaScript調用C++
在Javascript腳本中想要訪問C++中的內容,必須先將C++的變量、函數、類注入到Javacsript中。注入時,首先要調用JS_GetGlobalObjec獲取到Global全局對象,調用JS_SetProperty函數來注入全局變量,調用JS_DefineFunctions注入全局函數、調用JS_InitClass注入類。
全局變量注入過程與上一節的代碼類似,這裏就省略不寫了。
注入全局函數的示例:
//C++全局函數,功能:將傳入的兩個參數相加
//cx 上下文,obj 目標對象,argc 參數個數,argv參數,rval返回值
JSBool Add (JSContext *cx, JSObject *obj, uintN argc, jsval*argv, jsval *rval){
long value = JSVAL_TO_INT(argv[0]) + JSVAL_TO_INT(argv[1);
*rval = INT_TO_JSVAL(value);
return JS_TRUE;
}
//需要注入到JS中的函數,可以有多個,最後一個必須是{0}
static JSFunctionSpec functions[] =
{
{” Add “, Add, 0},
{0}
};
//注入C++全局函數函數
JS_DefineFunctions(cx, glob, functions);
函數注入後,在JavaScript中可以直接調用這個函數,例如:
var n = Add(100, 100);
使用SpiderMonkey注入C++類的過程和使用V8注入C++類的過程類似。 SpiderMonkey的API也提供了一個類似於V8的“內部數據”的結構。可以給某個JavaScript對象指定一個void*數據,當 SpiderMonkey CallBack回調C++時,就可以獲取該void*數據進行操作了。
下面是一個示例代碼:
//C++ 類定義
class test
{
public:
test(){number=0;};
voidfunc(){number++;}
int number;
};
test g_test;//變量定義
//定義一個結構,表示對象的“內部數據”
struct testPrivate
{
test* t; //test指針
bool externObject;//是否是C++注入的對象
};
//test類構造函數
//在Javascript中如果執行:var t = new test;V8就會CallBack調用這個C++函數
//在C++中執行NewInstance函數注入對象時,也會調用這個函數
//默認的test類的構造函數沒有參數,C++注入對象時,提供一個額外的參數,這個參數就是C++類的指針
JSBool testConstructor(JSContext *cx, JSObject *obj, uintNargc, jsval *argv, jsval *rval)
{
testPrivate* tp =new testPrivate;
if(argc)
{
tp->t =(test*)JSVAL_TO_PRIVATE(argv[0]);//注入的對象
tp->externObject= true;
}
else
{
tp->t = newtest;//不是注入的對象就新建一個
tp->externObject= false;
}
//設置“內部數據”
if ( !JS_SetPrivate(cx, obj, tp) )
return JS_FALSE;
*rval =OBJECT_TO_JSVAL(obj);
return JS_TRUE;
}
//test類析構造函數,JavaScript垃圾回收時,會CallBack調用這個函數
void testDestructor(JSContext *cx, JSObject *obj)
{
//獲取設置“內部數據
testPrivate* tp =(testPrivate*)JS_GetPrivate(cx, obj);
if(tp==NULL)
return;
if(!tp->externObject)//注入的對象,不刪除
delete tp->t;
delete tp;
}
//封裝test類func方法
//在Javascript中如果執行:t.func();就會CallBack調用這個C++函數
JSBool testFunc(JSContext *cx, JSObject *obj, uintN argc,jsval *argv, jsval *rval)
{
//獲取設置“內部數據
testPrivate* tp =(testPrivate*)JS_GetPrivate(cx, obj);
tp->t->func();
return JS_TRUE;
}
//定義一個枚舉,每個枚舉值表示一個test類的成員變量。
enum
{
test_number,
};
//test類的成員變量,可以有多個
JSPropertySpec testProperties[] =
{
{“number”, test_number, JSPROP_ENUMERATE },
{ 0 }
};
//test類的成員變量的get操作
//在Javascript中如果執行:var n= t.number; 就會調用這個C++函數
JSBool testGetProperty(JSContext *cx, JSObject *obj, jsvalid, jsval *vp)
{
if(JSVAL_IS_INT(id))
{
testPrivate* tp= (testPrivate*)JS_GetPrivate(cx, obj);
switch(JSVAL_TO_INT(id))
{
casetest_number:
*vp =INT_TO_JSVAL(tp->t->number);
break;
}
}
return JS_TRUE;
}
//test類的成員變量的set操作
//在Javascript中如果執行:t.number= 100; 就會調用這個C++函數
JSBool testSetProperty(JSContext *cx, JSObject *obj, jsvalid, jsval *vp)
{
if(JSVAL_IS_INT(id))
{
testPrivate* tp= (testPrivate*)JS_GetPrivate(cx, obj);
switch(JSVAL_TO_INT(id))
{
casetest_number:
tp->t->number= JSVAL_TO_INT(*vp);
break;
}
}
return JS_TRUE;
}
//test類方法列表,可以有多個
static JSFunctionSpec testMethods[] = {
{“func”,testFunc, 0},
{0}
};
JSClass testClass =
{
“test”,JSCLASS_HAS_PRIVATE,
JS_PropertyStub,JS_PropertyStub,
testGetProperty,testSetProperty,
JS_EnumerateStub,JS_ResolveStub,
JS_ConvertStub,testDestructor
};
//將test類定義注入JavaScript中
JSObject *newTestObj = JS_InitClass(cx, glob, NULL,&testClass,
testConstructor,0,
NULL,testMethods,
NULL, NULL);
JS_DefineProperties(cx, newTestObj, testProperties);
//在JavaScript腳本中注入一個新對象,並使這個新對象與g_test綁定
jsval arg = PRIVATE_TO_JSVAL(&g_test);
JSObject* gtestObj = JS_ConstructObjectWithArguments(cx,&testClass, NULL, NULL, 1, &arg);
jsval vp = OBJECT_TO_JSVAL(gtestObj);
JS_SetProperty(cx, glob, “g_test”, &vp);
將C++類和類指針注入到V8 JavaScript後,在JavaScript中就可以這樣使用了:
g_test.func();
var n = g_test.number;
var t = new test;
4. Webkit JavaScriptCore
4.1. 介紹
WebKit是一個開源的瀏覽器引 擎。很多瀏覽器都使用了WebKit瀏覽器引擎,比如蘋果的Safari、Google的Chrome(只使用了排版和渲染部分)。WebKit包含一個 網頁排版渲染引擎WebCore和一個腳本引擎JavaScriptCore。JavaScriptCore引擎的API接口是C語言的API接口。關於 JavaScriptCoreAPI的文檔資料比較少,不過可以參考蘋果公司的JSCocoa文檔(基於Objective-C語言的)。蘋果的 Safari瀏覽器重寫了JavaScriptCore,新的項目名稱是SquirrelFish Extreme。SquirrelFish Extreme的對外API與JavaScriptCore是一樣的。Webkit源碼SVN地址:http://svn.webkit.org/repository/webkit/trunk
JavaScriptCore API的主要數據結構有:
l JSGlobalContextRef:JavaScript全局上下文。也就是JavaScript的執行環境。
l JSValueRef:JavaScript的一個值,可以是變量、object、函數。
l JSObjectRef:JavaScript的一個object或函數。
l JSStringRef:JavaScript的一個字符串。
l JSClassRef:JavaScript的類。
l JSClassDefinition:JavaScript的類定義,使用這個結構,C、C++可以定義和注入JavaScript的類。
JavaScriptCore API的主要函數有:
l JSGlobalContextCreateJSGlobalContextRelease:創建和銷燬JavaScript全局上下文。
l JSContextGetGlobalObject:獲取JavaScript的Global對象。
l JSObjectSetPropertyJSObjectGetProperty:JavaScript對象的屬性操作。
l JSEvaluateScript:執行一段JS腳本。
l JSClassCreate:創建一個JavaScript類。
l JSObjectMake:創建一個JavaScript對象。
l JSObjectCallAsFunction:調用一個JavaScript函數。
l JSStringCreateWithUTF8CstringJSStringRelease:創建、銷燬一個JavaScript字符串
l JSValueToBooleanJSValueToNumber JSValueToStringCopy:JSValueRef轉爲C++類型
l JSValueMakeBooleanJSValueMakeNumber JSValueMakeString:C++類型轉爲JSValueRef
4.2. C++調用JavaScript
使用JavaScriptCore,在C++中訪問Javascript腳本中的內容,首先要調用JSContextGetGlobalObject獲取到Global全局對象,再調用JSObjectGetProperty函數來提取Javascript的全局變量、全局函數、全局複雜對象。示例如下:
//獲取Global對象
JSGlobalContextRef ctx = JSGlobalContextCreate(NULL);
JSObjectRef globalObj = JSContextGetGlobalObject(ctx);
//獲取全局變量
JSStringRef varName = JSStringCreateWithUTF8CString(“JavaScript變量名”);
JSValueRef var = JSObjectGetProperty(ctx, globalObj, varName,NULL); JSStringRelease(varName);
//轉化爲C++類型
int n = JSValueToNumber(ctx, var, NULL);
//獲取全局函數
JSStringRef funcName = JSStringCreateWithUTF8CString(“JavaScript函數名”);
JSValueRef func = JSObjectGetProperty(ctx, globalObj, funcName,NULL); JSStringRelease(funcName);
//裝換爲函數對象
JSObjectRef funcObject = JSValueToObject(ctx,func, NULL);
//組織參數,將兩個數值1和2作爲兩個參數
JSValueRef args[2];
args[0] = JSValueMakeNumber(ctx, 1);
args[1] = JSValueMakeNumber(ctx, 2);
//調用函數
JSValueRef returnValue = JSObjectCallAsFunction(ctx, funcObject,NULL, 2, args, NULL);
//處理返回值
int ret = JSValueToNumber(ctx, returnValue, NULL);
//獲取複雜的對象
JSStringRef objName=JSStringCreateWithUTF8CString(“JavaScript複雜對象名”);
JSValueRef obj = JSObjectGetProperty(ctx, globalObj, objName,NULL); JSStringRelease(objName);
//裝換爲對象
JSObjectRef object = JSValueToObject(ctx,obj, NULL);
//獲取對象的方法
JSStringRef funcObjName =JSStringCreateWithUTF8CString(“JavaScript複雜對象的方法”);
JSValueRef objFunc = JSObjectGetProperty(ctx, object, funcObjName,NULL); JSStringRelease(funcObjName);
//調用複雜對象的方法,這裏省略了參數和返回值
JSObjectCallAsFunction(ctx, objFunc, NULL, 0, 0, NULL);
4.3. JavaScript調用C++
在Javascript腳本中想要訪問C++中的內容,必須先將C++的變量、函數、類注入到Javacsript中。注入時,首先要調用JSContextGetGlobalObject獲取到Global全局對象,再調用JSObjectSetProperty函數來注入全局變量、全局函數、類對象。
全局變量、全局函數的注入過程與上一節的代碼類似,這裏就省略不寫了。
將C++的類、類對象注入到Javascript中。其基本的過程如下:
1. 首先定義一個C++類,
2. 定義一組C++全局函數,封裝JavaScriptCore對C++類的調用,提供給JavaScriptCore進行CALLBACK回調。
3. 最後調用JSClassCreate函數,將定義的C++類和C++函數注入到Javascript中
C++示例代碼如下
//C++ 類定義
class test
{
public:
test(){number=0;};
voidfunc(){number++;}
int number;
};
test g_test;//變量定義
//全局函數,封裝test類的func方法調用
JSValueRef testFunc(JSContextRef ctx, JSObjectRef ,JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[],JSValueRef*)
{
test* t =static_cast<test*>(JSObjectGetPrivate(thisObject));
t->func();
returnJSValueMakeUndefined(ctx);
}
//全局函數,封裝test類的成員變量number的get操作
JSValueRef getTestNumber(JSContextRef ctx, JSObjectRefthisObject, JSStringRef, JSValueRef*)
{
test* t =static_cast<test*>(JSObjectGetPrivate(thisObject));
returnJSValueMakeNumber(ctx, t->number);
}
//使用一個函數, 創建JavaScript類
JSClassRef createTestClass()
{
//類成員變量定義,可以有多個,最後一個必須是{ 0, 0, 0 }
//也可以指定set操作
static JSStaticValuetestValues[] = {
{“number”, getTestNumber, 0, kJSPropertyAttributeNone },
{ 0, 0, 0, 0}
};
//類的方法定義,可以有多個,最後一個必須是{ 0, 0, 0 }
staticJSStaticFunction testFunctions[] = {
{“func”, testFunc, kJSPropertyAttributeNone },
{ 0, 0, 0 }
};
//定義一個類
staticJSClassDefinition classDefinition = {
0,kJSClassAttributeNone, “test”, 0, testValues, testFunctions,
0, 0, 0, 0,0, 0, 0, 0, 0, 0, 0
};
// JSClassCreate執行後,就創建一個了JavaScript test類
staticJSClassRef t = JSClassCreate(&classDefinition);
return t;
}
//創建JavaScript類
createTestClass ();
JSGlobalContextRef ctx = JSGlobalContextCreate(NULL);
JSObjectRef globalObj = JSContextGetGlobalObject(ctx);
//新建一個JavaScript類對象,並使之綁定g_test變量
JSObjectRef classObj= JSObjectMake(ctx,testClass(), &g_test);
//將新建的對象注入JavaScript中
JSStringRef objName= JSStringCreateWithUTF8CString(“g_test”);
JSObjectSetProperty(ctx,globalObj,objName,classObj,kJSPropertyAttributeNone,NULL);
將C++類和類指針注入到JavaScript後,在JavaScript中就可以這樣使用了:
g_test.func();
var n = g_test.number;
var t = new test;
5. 微軟JavaScript
5.1. 介紹
IE的Trident引擎是非開源的,微軟JavaScript引擎也是非開源的。微軟對外提供了一組COM接口。使用這組COM接口,能夠將微軟的JavaScript、VBScript嵌入到C、C++宿主語言中。
這組COM接口主要有如下一些定義:
l IDispatch
IDispatch是COM跨語言調用的基本接口。
l 命名空間
這裏的“命名空間”的意思相當於JavaScript中的Global對象,宿主語言必須要實現一個“命名空間”。當JavaScript執行腳本,遇見未知的變量、函數、類時候,就會調用該“命名空間”的IDispatch接口,來進行解析和執行。
l IActiveScript
IActiveScript是由JavaScript腳本引擎所實現的接口,C、C++宿主語言調用該COM接口,就可以建立和初始化一個腳本的執行環境。
l IActiveScriptParse
IActiveScriptParse是由JavaScript腳本引擎所實現的接口,該接口可以分析、執行一段JavaScript腳本,並且還可以控制JavaScript腳本的執行過程。
l IActiveScriptSite
IActiveScriptSite是由C、C++宿主語言所實現的接口,該接口可以給JavaScript腳本提供“命名空間”,並且當JavaScript引擎出現錯誤、異常時,就會使用該接口進行回調通知。
在C++中使用這組COM接口的示例代碼如下:
//獲取JavaScript的CLSID
CLSID clsid;
CLSIDFromProgID(_T(“JScript”), &clsid);
//創建JavaScript對象,獲取IActiveScript接口指針
CComPtr<IActiveScript> activeScript = NULL;
CoCreateInstance(clsid, NULL, CLSCTX_ALL, IID_IActiveScript,(void **)&activeScript);
//創建宿主對象,CMyScriptSite實現了IActiveScriptSite接口
IActiveScriptSite* site = new CComObject<CMyScriptSite>;
site->AddRef();
//將宿主對象IActiveScriptSite與IActiveScript綁定
hr = activeScript->SetScriptSite(site);
//增加一個命名空間,會觸發IActiveScriptSite的GetItemInfo方法被調用
_bstr_t name = “test”
hr=activeScript->AddNamedItem(name,SCRIPTITEM_ISVISIBLE|SCRIPTITEM_GLOBALMEMBERS);
//獲取腳本解析和執行接口
CComQIPtr<IActiveScriptParse> activeScriptParse= activeScript;
//初始化一個執行環境
activeScriptParse->InitNew();
//解析一段JS
_bstr_t srcipt = “g_test.func()”;
activeScriptParse->ParseScriptText(srcipt,name,NULL,NULL,0,0,0,NULL,NULL);
//開始執行JS
activeScript->SetScriptState(SCRIPTSTATE_STARTED);
//宿主對象CMyScriptSite,必須要實現的一個方法
//與IActiveScript的AddNamedItem對應
//返回給JavaScript一個命名空間
HRESULT CMyScriptSite::GetItemInfo(LPCOLESTR pstrName, DWORDdwReturnMask, IUnknown **ppunkItem, ITypeInfo **ppti){
if (name == _bstr_t(pstrName) )
{ //這裏簡單將宿主對象自己當做一個“命名空間”返回給JavaScript
HRESULT hr =QueryInterface(IID_IUnknown, (void**)ppunkItem);
return hr;
}
return E_FAIL;
}
5.2. JavaScript與C++的交互
在上一節中,宿主對象返回給JavaScript一個“命名空間”,這個“命名空間”必須實現IDispatch接口。同樣的,宿主對象通過調用IActiveScript的GetScriptDispatch方 法,也可以獲得JavaScript的IDispatch接口指針。C、C++宿主對象和JavaScript腳本對象都互相持有對方的 IDispatch接口。所以C、C++調用JavaScript,實際上就轉化爲IDispatch接口的調用。JavaScript調用宿主C、 C++,實際上就轉化爲宿主語言C、C++對IDispatch接口的實現。
IDispatch是微軟跨語言調用的一個標準COM接口。對於這個接口的調用、實現,網上到處都有,這裏就忽略不寫了。