2012年9月24日 星期一

FireBreath 心得 - Mac 篇

2017/10/13 敬告: FireBreath 在非 Windows 平台上所依賴的 NPAPI 已經被各大主流瀏覽器廢棄、禁用。因此 FireBreath 的開發也已暫停 (估計不可能再開)。此處心得只能留作回憶。


上次有發了篇 FireBreah 的心得, 但裡面主要是針對 Windows 上的使用經驗. 最近將同樣的工作移植到了 Mac 平台上, 原本以為熟悉 FireBreath 的大致結構後移植的時間應該會比第一次在 Windows 中開發時來得快. 最後竟花了約預期時間兩倍的時間才大致搞定. 我本身對 Mac 平台並不熟悉, 很多平台細節如視窗事件的處理在原先預期上應該要靠 FireBreath 幫忙處理掉的, 但最後卻發現它也沒有提供很完美的抽象層, 導致最後還是將視窗事件獨立拉出來處理. 所以目前對 Mac 平台也累積了一些 FireBreath 使用經驗, 整理於此供後續參考:



在開始進行前, 建議先讀過Stuart Morgan 寫的這篇文章, 裡面解釋了在 Mac 平台上開發 NPAPI 瀏覽器插件的基本背景資訊. 理論上 FireBreath 應該要提供完美的抽象層讓所有開發者不需知道特定平台的細節也能進行開發, 但實際以我自己的經驗來說它還沒完美到這個地步, 某些細節上還是需要這些資訊否則難以做適當的選擇. 另外也必需先看過 FireBreath 官方 Wiki 上這篇介紹 Mac Plugin 和不同事件/繪圖模型相關性的文章. 我自己最後用了 Carbon + CoreGraphics 的事件/繪圖模型搭配. 以下就記錄這個搭配模式下遇到的一些困難和解決方式.


(2012/10/09 補充: Mac 上最近的一次 Google Chrome Stable Release 似乎已經完全移除對 Carbon 的支援. 所以我最近又被迫將原本的 Carbon + CoreGraphics 改為 Cocoa + CoreGraphics 的實作. 這部分的內容追加在最後面.)


如何取得 Window Handler?


我進行的項目類似是要在瀏覽器上開一塊視窗區域 (即插件的顯示區域), 然後要想辦法把該塊區域的 Window Handler 取回讓現有的 Redering Library 用為輸出視窗. 在 Windows 上這個 Window Handler 指的就是 HWND, FireBreath 有現成的 API 可以很方便的取回. 然而在 Mac 上近似這個觀念的是名為 WindowRef 的東西, 並且在 FireBreath 中你一定要將繪圖模型選為 QuickDraw 或是 CoreGraphics 並且使用 Carbon 事件模型才能正確取回 WindowRef, 其它事件/繪型模型的搭配下也會有這個 API 但它總是會返回 null WindowRef, 這一點在官方文件中並沒有很清楚的說明. 以下是 FireBreath 的 onWindowAttached callback 中視不同平台取回 Window Handler 的示意寫法:
bool MyPlugin::onWindowAttached(FB::AttachedEvent *evt, FB::PluginWindow *win)
{
#ifdef _WIN32
    // Get HWND
    HWND hWnd = win->get_as<FB::PluginWindowWin>()->getHWND();
#elif __APPLE__
    // Get WindowRef
    WindowRef winref = win->get_as<FB::PluginWindowMac>()->getWindowRef();
#enduf
    return true;
}


如何轉換 WindowRef 為 NSView* ?


我不是 Mac 專家所以這邊的結論可能有些誤差. 但就我在網路上找到的資訊來看, WindowRef 是一種 Carbon 事件模型中專有的 Window Handler 型別. 而在一般使用 Cocoa 開發的三方函式庫中, 若要對已有的視窗進行 Rendering, 通常都是要求傳入一個 NSView 的指標. Cocoa 有提供 API 把 WindowRef 轉換成 NSWindow, 只需把轉換好的 NSWindow 中的 contentView 取出就是了:
// In your objtive-c helper class:

#include <AppKit/AppKit.h>
#include <Carbon/Carbon.h>

void* MyPluginObjcHelper::getNSViewFromWindowRef(void* winref)
{
    NSWindow *win = [[NSWindow alloc] initWithWindowRef:(WindowRef)winref];
    NSView *view = [win contentView];

    // Do some config on win/view ...

    return view;
}

使用到 Cocoa 的 API 時無法避免地要牽涉到 Objective-C 寫作. 這裡有點尷尬的是 FireBreath 原本是要提供一個完美的 C++ 抽象層, 所以預設只會把 .cpp/.h 視為專案源碼, 另行增加的 .m/.mm 並不會被預設的 CMake rule 採用. 建議的修正方式是, 將所有 Mac 平台才會用到的 Objective-C 源碼檔都集中放在你專案目錄中的 Mac 資料夾中 (FireBreath 應該已經幫你建好了), 然後修改其下的 projectDef.cmake, 把 .m/.mm 也加進預設的 file group 裡, 再次調用 prepmac.sh 建立 XCode project file 的時候就會發現這些 .m/.mm 能被正確加進專案裡了.
#/**********************************************************\ 
# Auto-generated Mac project definition file for the
# MoregeekWebEngine project
#\**********************************************************/

# Mac template platform definition CMake file
# Included from ../CMakeLists.txt

# remember that the current source dir is the project root; this file is in Mac/
file (GLOB PLATFORM RELATIVE ${CMAKE_CURRENT_SOURCE_DIR}
    Mac/[^.]*.cpp
    Mac/[^.]*.h
    Mac/[^.]*.m
    Mac/[^.]*.mm
    Mac/[^.]*.cmake
    )



如何自行處理 Carbon 事件?


若是使用 WindowRef 轉 NSWindow/NSView 的方式, 實際上運作起來會像是在瀏覽器中插件的顯示區域上又另開了新視窗疊在上面, 拖動瀏覽器時會發現這個新視窗會跟著改變位置. 當新視窗蓋在插件顯示區域上時, 所有用戶輸入事件就會被它吃走, 而不會落入 FireBreath 的事件分配器中, 這點也跟 Windows 上的開發經驗不同. 我的解決方案是直接在我的專案中使用 Carbon 的事件函式庫註冊事件 callback, 在內部自行解決用戶事件, 不依賴 FireBreath.

對於像我一樣的 Mac 門外漢, 我建議從兩個方面入門 Carbon Input 事件處理. 一個是 O'Reilly 的 "Learning Carbon" 這本書. 正好官網上免費 Preview 的章節是 Chapter 6 Carbon Events, 這節內容對照我建議的另一個參考實作: OIS. 應該就能實作出自行處理 Carbon 輸入事件的大致雛形. OIS 是一個跨平台的開源函式庫, 專為處理使用者輸入事件(滑鼠, 鍵盤, 搖桿), Ogre3D 引擎就是採用 OIS 來處理用戶輸入. 它在 Mac 上是使用 Carbon API, 可以作為一個非常好的實作參考. 舉鍵盤的事件處理的例子來說, Callback 的註冊可參考 src/mac/MacKeyboard.cpp, 而所有 Callback 的具體實作則位於 src/mac/MacHelpers.cpp 內.


Mac 上的 Plugin Debugging


我的建議是使用 Google Chrome. 直接在 command line 中執行 Google Chrome 的 binary (/Applications/Google Chrome.app/Contents/MacOS/Google Chrome), 附加上 --plugin-startup-dialog 的參數. 之後在觸發 Plugin 執行時, command line 下會秀出 Plugin Process 的 PID, 並且暫停 Plugin 執行等待 Debugger Attach. 這時從 XCode4 的 Product -> Attach to Process 就能將 Debugger 掛上 Plugin Process 進行 Debug.


(以下為 Cocoa + CoreGraphics 追加部分)


如何在基於 Cocoa 事件模型的 NPAPI 插件中取得 NSView* ?


若直接將 FireBreath 的事件模型設為 Cocoa, 是否有另一條路能取到 NSView* ? 關於這一點 FireBreath 的作者有在 StackOverflow 上回應過是不可行的作法. 新型的瀏覽器大都採用獨立 process 去運行插件 (Sandbox), 以免插件運作異常連帶讓瀏覽器也掛掉.也因為跨 process 的關係, 插件就無法直接存取原本是在瀏覽器網頁 process 的視窗內容. 目前在 Mac 上的 NPAPI 實作中, 插件只能在每次 redraw 事件中獲得 CGContextRef, 這個東西有點類似 Windows 中的 DC (Device Context), 可以透過它來進行繪圖操作. 它背後其實是對應到一塊 Memory Buffer, 等插件繪製完畢後, 會再由瀏覽器將 Buffer 中的內容複製到 Plugin View 中. 插件在整個過程中是不能直接面對 Plugin View 的.

我自己的解決方案是直接用 Cocoa 的 NSWindow 函數另外開了一個視窗, 這樣一來就完全跟瀏覽器各玩各的. 這樣的實作在 Apple 開發手冊中有寫明是不建議的. 但這是我找到的唯一可行辦法.


如何自行處理 Cocoa 輸入事件?


切換為使用 Cocoa 事件模型之後, 前述在 Carbon 下的處理輸入事件的那套作法就不復使用. 我目前的作法是改用 CoreGraphics 函式庫中提供的 CGCreateEventTap() 函數. 這個函數類似於 Windows 中的 Event Hook, 可以在視窗的事件接收源頭就安插 filter 來嗅探甚至攔截事件. 詳細使用方法可參考這裡.

Event Tap 要能正常運作的另一項前提是系統必需勾選使用輔助設備. 可在 "系統偏好設定" - "輔助使用" 頁面中勾選 "允許使用輔助設備" 來開啟.


1 則留言:

  1. 請問 "直接用 Cocoa 的 NSWindow 函數另外開了一個視窗" 這部分是如何做到的?

    回覆刪除