前一篇最後簡略提了一下 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);}