在桌面應用程式中使用 WebKit (3) - JavaScriptCore

前一篇最後簡略提了一下 JavaScriptCore Framework。JavaScriptCore 是 WebKit 的 JavaScript 引擎,目前的實作就是代號叫做 SquirrelFish,吉祥物是一隻長相恐怖的祕雕魚。

一般來說,在 Mac OS X 上,我們想要製作各種網頁與 Native API 程式互動的功能,大概不會選擇使用 JavaScriptCore,因為現在寫 Mac OS X 的桌面應用程式,多半會直接選擇使用 Objective C 語言與 Cocoa API,各種需要的功能,都有像在前兩篇提到的 Obj C 方案-使用 WebKit Framework 中的 WebScriptObject 與各種 DOM 物件。

在 Obj C 程式中當然可以使用 JavaScriptCore 所提供的 C API,但是在實際撰寫程式時,遠比使用 Obj C 呼叫麻煩-你的主要 controller 物件還是用 Obj C 撰寫的,如果直接用 JavaScriptCore 產生 JS 可以呼叫的 callback,這些 callback C function 還是會需要向 Obj C 物件要資料,等於是多繞了一圈。

然而如果真的有需要的話,我們的確可以混用 JavaScriptCore 與 WebKit 的 Obj C API,每個 WebScriptObject 中,都可以用 JSObject 這個 method 取得對應的 JSObjectRef。就跟許多 Mac OS X 或 iPhone 上的 framework 一樣,Obj C 物件中往往包含 C 的 API,我們於是可以從 UIColor 得到 CGColor,從 UIImage 得到 CGImage,在 WebScriptObject 中,就是 JSObjectRef。

不過,在 WebKitGTK+ 上,由於 GTK 本身是 C API,所以透過 JavaScriptCore,看來就變成 GTK 應用程式與網頁內的 JS 程式相互呼叫、傳遞資料最重要的方法(不太熟悉 GTK,所以不太確定還有哪些其他的方法)。

在 GTK 網站上有一個簡單的範例程式(連結),除了示範怎樣產生一個新 Window,裡頭放一個 WebView 開始執行外,還包括怎樣產生一個網頁中 JS 可以呼叫的 C function,並且將這個 C function 註冊到成 JS 的 window 物件的某個成員 function。看這個程式還頂有趣的-絕大部分程式的命名規則與風格,都是屬於 GTK 的風格,但是在呼叫到 JavaScriptCore 的時候,卻又是蘋果的 CoreFoundation 的風格。

JavaScriptCore 雖然是 WebKit 的 JS 引擎,但是其實並不一定需要 WebKit。JavaScriptCore 中所有的 API 都是對著一個 context 操作,我們要操控 WebView,就是對著 WebView 中某個 frame 的 global context 操作([webFrame globalContext]),而這個 context 可以不需要有一個 WebFrame 物件就可以自己產生,我們可以在完全沒有圖形介面、不用到 Web 排版引擎的狀況下,產生、執行 JS 程式碼。

所以可以看到,不少專案將這個引擎從 WebKit 中拆分出來,另外橋接其他的 library,在 GTK 上就有 SEED,讓你可以用 JavaScript 呼叫 GObject,用 JavaScript 寫 GTK 應用程式。在 Mac OS X 上則有橋接 JavaScript 與各種 Cocoa 物件的專案,像是 JSCocoa,以及以 JSCocoa 為基礎,作為代替 AppleScript 的 JSTalk,說起來可以用 Javascript 寫 Cocoa 應用程式該算是一件不錯的事情,不過 JSCocoa 卻又去橋接了 Objective J 的語法-話說既然你都會 Obj J,何不直接寫 Obj C?

說到 Obj J。不久前 Motorola 併購了 280 North,根據報導,Motorola 的說法是,可以透過 280 North 在 Web 上面的經驗,讓 Android 可以擁有更多的 Web 服務與應用,老實說,印象中 Motorola 也沒有什麼 Web 服務。(以下為不負責發言)-倒是以前用 GTK 搞過 MotoMagx,而現在又可以用 JS 呼叫 GTK Library,搞不好 Motorola 是對自家的的 Linux 手機作業系統還不死心,打算做一個用 Obj J 寫手機軟體的系統出來,或是想搞個像 WebOS 那樣的東西。

總之,JavaScript 是來自 Web 的技術,但是也大量應用於桌面應用程式中。目前還比較看不到拿 JS 寫算是中、大型的應用程式,但是在桌面上做一些自動執行的事情,像 JSTalk 希望可以代替 AppleScript,之前提到在 iOS 4.0 中,增加了一項 UI 自動測試功能,可以讓你寫一小段 JS 程式碼放在 Instrument 裡頭執行,自動執行在畫面上點了哪個按鈕應該要有什麼效果出來。我猜想 iOS 的這項自動測試功能也是呼叫 JavaScriptCore Framework,因為想不出來蘋果有什麼必要另外弄一個 JS 引擎。

不過,在這便只大概講一下用 WebView 開啟的網頁,怎麼透過 JavaScriptCore 呼叫 C Function。

老實說我對 JavaScriptCore 這個東西不算熟,就像前面說的,真的在寫 Cocoa 程式往往直接呼叫 WebScriptObject。JavaScriptCore 裡頭有幾種基本的資料:

  • JSGlobalContextRef:執行 JS 的 context
  • JSValueRef:在 JavaScript 中所使用的各種資料,包括字串、數字以及 function,都會包裝成 Value,我們可以從數字、JSStringRef 或 JSObject 產生 JSValueRef,也可以轉換回來。需要特別注意的是,JS 裡頭的 null 也是一個 JSValueRef(JSValueMakeUndefined 與 JSValueMakeNull)。
  • JSStringRef:JavaScriptCore 使用的字串。用完記得要 release。
  • JSObjectRef:JS Array、Function 等。

然後,來看一下前一篇裡頭的程式碼。

- (void)webView:(WebView *)sender didClearWindowObject:(WebScriptObject *)windowObject forFrame:(WebFrame *)frame
{
	JSGlobalContextRef globalContext = [frame globalContext];
	JSStringRef name = JSStringCreateWithUTF8CString("myFunc");
	JSObjectRef obj = JSObjectMakeFunctionWithCallback(globalContext, name, (JSObjectCallAsFunctionCallback)myFunc);
	JSObjectSetProperty (globalContext, [windowObject JSObject], name, obj, 0, NULL);
	JSStringRelease(name);	
}

就像前一篇說的,因為每次重新載入網頁,JS 裡頭的 window 這個物件的內容就會更新一次,所以我們要等待 WebView 告訴我們應該要更新 windowObject 的時候,我們才做我們要做的事情-在 window 中加入一個可以讓 JS 呼叫的 C function。我們首先要做兩件事情,第一是取得 WebFrame 裡頭的 globalContext([frame globalContext]),還有 windowObject 這個 Obj C 物件裡頭的 JSObject([windowObject JSObject])。

接著,我們要用 C 產生一個 JS function 物件,在這邊用的是 JSObjectMakeFunctionWithCallback,代表我們想要產生一個可以用來呼叫 C Function 的 JS function,我們要提供這個 JS function 的名稱,還有對應到哪個 C function。如果我們想產生的 JS function 不需要呼叫 C function,可以改用JSObjectMakeFunction;最後,我們把這個 function 物件,註冊給 windowObject 的 JSObject 上,於是我們現在便可以在 JS 中呼叫 window.myFunc() 了。

來個綜合練習-我們現在在 JS 中傳入兩個數字,透過 C function 加完之後,執行一段 JS callback。我們的 JS 程式這麼寫-

window.myFunc(1, 1, function(result) {
	var main = document.getElementById('main');
	main.innerText = result;
});

在 myFunc 中,我們來練習一下 JavaScriptCore 裡頭的一些東西:

JSValueRef myFunc(JSContextRef ctx, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
{
	if (argumentCount < 3) {
		JSStringRef string = JSStringCreateWithUTF8CString("UTF8String");
		JSValueRef result = JSValueMakeString(ctx, string);
		JSStringRelease(string);
		return result;
	}
	if (!JSValueIsNumber(ctx, arguments[0])) {
		JSStringRef string = JSStringCreateWithCFString((CFStringRef)@"NSString");
		JSValueRef result = JSValueMakeString(ctx, string);
		JSStringRelease(string);
		return result;		
	}
	if (!JSValueIsNumber(ctx, arguments[1])) {
		return JSValueMakeNumber(ctx, 42.0);
	}
	if (!JSValueIsObject(ctx, arguments[2])) {
		return JSValueMakeNull(ctx);
	}	
	
	double leftOperand = JSValueToNumber(ctx, arguments[0], exception);
	double rightOperand = JSValueToNumber(ctx, arguments[1], exception);
	JSObjectRef callback = JSValueToObject(ctx, arguments[2], exception);
	JSValueRef result = JSValueMakeNumber(ctx, leftOperand + rightOperand);
	JSValueRef myArguments[1] = {result};
	JSObjectCallAsFunction(ctx, callback, thisObject, 1, myArguments, exception);	
	return JSValueMakeNull(ctx);
}

我們希望至少要有三個參數傳進來,前兩個參數是數字,最後一個參數是 JSObject,如果不是的話,就簡單回傳一點東西-在這邊可以看到,JSStringRef 除了可以用 UTF8 字串產生,也可以從 CFString 產生。我們接下來把 JSValue 轉成 double,簡單做個加法,最後用 JSObjectCallAsFunction,執行 JS callback-其實這邊還應該要用 JSObjectIsFunction,來檢查一下這個 JSObject 到底是不是 function 才是。

3 thoughts on “在桌面應用程式中使用 WebKit (3) - JavaScriptCore

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.