Friday, April 29, 2011

NPAPI & anothor single triangle


不久前看到Insomniac game他們的新工具是利用各種網業技術做出來的,包含html,java script等等,其中有個地方還滿特別的,他們的場景編輯器的3D視窗是利用瀏覽器的plugin方式,直接嵌進瀏覽器中。

於是,我發現了一個長久以來我都視為理所當然,可是一直忽略的事實,那就是瀏覽器應該可以用native code寫plugin吧...flash,quick time,甚至Unity3D引擎,quake live這些技術,看起來不可能是用script寫出來的。

於是我用google查了web browser native plugin或類似的東西,結果就掉出了google html5 native code計畫,html 5大業的確很有潛力,sand box的環境也可能提供比較好的安全性,不過native code計畫看起來還不太成熟,目前只有gcc的環境,而我是個用visual studio的軟派程式員,只好放棄這個方向。不過flash,或unity引擎,都是在html 5的native code計畫出現前就有的東西,顯然有其他的方式可以達成,經過一堆亂七八糟的關鍵字搜尋,終於找到了關鍵字是甚麼---NPAPI和ActiveX。

NPAPI是給非IE用的瀏覽器plugin API(Firefox,Chrome等等),ActiveX就是...恩,微軟的IE。這兩個API,都可以讓我們在瀏覽器中,用C/C++寫plugin。另外這也是為甚麼plugin都分IE和非IE兩種...

有關NPAPI的資訊,其實在網路上還滿少的,官方文件和這個blog,我覺得算是比較好的資源。其中那位blog的作者,開發了FireBreath這套可以跨NPAPI和ActiveX的plugin API。另外在codeproject上有一個範例

因為我只想用OpenGL畫一個三角形,於是我下載了codeproject上的範例修改了一下,把有關scriptable的部分全部拿掉,然後就畫了一個三角形。

真的有興趣的人,可以用svn checkout http://ianjoker.googlecode.com/svn/trunk/npgl下載source code。

稍微紀錄一下NPAPI的設計,首先,他是"真正"的plugin設計,compile好的NPAPI plugin是一個動態函示庫(.dll或.so),而且,這個plugin理論上可以給FireFox,Chrome,Safari或任何支援NPAPI的瀏覽器使用,而不用重新compile或link任何東西。這與一些不太plugin架構的plugin很不一樣,例如Ogre引擎中的RenderSystem或SceneManager等plugin,只要OgreMain有改變重新compile,這些plugin就必須重新compile或link OgreMain.lib,否則不保證plugin可以用。

可是NPAPI plugin不用重新link卻可以給各個瀏覽器(還包括不同版本,不同compiler等)使用,這是怎麼辦到的呢?好吧,原因很簡單,因為一開始就根本不用link任何.lib。

NPAPI靠的,是用C語言的ABI(Application binary interface)非常統一的特性,幾乎所有的interface都使用C語言制定,其中有三個C的inteface是瀏覽器與plugin的接口。

NPError OSCALL NP_GetEntryPoints(NPPluginFuncs* pFuncs);
NPError OSCALL NP_Initialize(NPNetscapeFuncs* bFuncs);
NPError OSCALL NP_Shutdown();


C的interface,可以利用GetProcAddress(windows)或dlsym(Linux)來取得位址。因此瀏覽器不用去link任何plugin的lib就可以呼叫這三個function。這當然是任何plugin設計的基礎,但是還缺了一半,如果plugin不link瀏覽器的.lib,那plugin就無法呼叫任何瀏覽器本身的function,瀏覽器無法提供任何服務給plugin使用!

解決方案很簡單,NP_Initialize這個function會在plugin被瀏覽器載入時呼叫,這時候會傳進來一個NPnetscapeFuncs,這其實是一個function pointer的table,裡面每個function pointer就是瀏覽器提供給plugin呼叫的function,例如我想跟瀏覽器alloc一塊記憶體,我可以呼叫NPnetscapeFuncs::memalloc。因此plugin不用link瀏覽器的.lib,link這件事情,是在runtime執行NP_Initialize的時候做的。

反向的則是NP_GetEntryPoints會需要我們填function pointer進NPPluginFuncs這個function pointer table,這些function pointers是plugin提供給瀏覽器呼叫的callback,例如當瀏覽器需要new一個新的plugin的instance的時候,瀏覽器會呼叫NPPluginFuncs::newp這個callback,我們必須實做這些function,然後把他們填入NPPluginFuncs交給瀏覽器。

因為我想做的是利用OpenGL畫三角形,想要OpenGL就必須要有window handle,而NPPluginFuncs正好有提供setwindow這個callback,會在瀏覽器建plugin window的時候呼叫,瀏覽器呼叫這個callback的時候,會傳進一個NPWindow struct,這個struct可以拿到window handle!任何一個graphics programmer,只要能拿到window handle就可以解決一切難題!!!

當然,事情永遠不會這麼簡單的...

首先是API的header問題,雖然說理論上這是一個跨瀏覽器的API,包含了npapi.h,npfunctions.h,npruntime.h,nptypes.h四個檔案,不過只要有人的地方,就有紛爭,更不用說瀏覽器這種兵家必爭之地了。我在demo中放了Mozilla的官方SDK版本。我試過在FireFox 3.x或4.x都是ok的,不過很可惜的是Chrome似乎不能用。Google他們自己可能也看到了這個問題,因此他們maintain了一份號稱可以跨平台的header,可惜的是我試過似乎在FireFox下會當掉。我還沒有仔細檢查哪裡出了問題,不過如果真的要寫跨平台的plugin,說不定比較好的方式還是去用FireBreath那套API比較好,否則要自己去處理一些跨瀏覽器的問題。


另外是plugin安裝的方式,按照Mozilla官方文件說法,在windows平台上要去registry註冊一些機碼好讓瀏覽器可以認得plugin放的地方,application的MIMEType,附檔名等資訊等等。不過我試了老半天沒辦法work,去看了codeproject的demo做法是直接用.rc檔內嵌在編譯出來的.dll中了,然後必須把編譯好的.dll丟到(FireFox安裝資料夾)\plugins\這個資料夾下面,如果沒有這個資料夾就自己建一個。當然這好像不是理想的做法,因為plugin無法安裝到我們自己想安裝的地方。另外還有個小地方也很奇怪,那個.rc檔裡面有個語言的選項,因為我的VC是中文版的,所以他預設都設成中文,可是中文的plugin無法用,一定要改成英文才行,這裡也是完全不知所以然

最後,因為plugin畫面更新是lazy的方式,瀏覽器覺得需要更新那個window才會有windows message重畫那個window,當然這樣做無法做real time的遊戲,如果要自己控制更新window,只好自己開另一個thread來建OpenGL context。總之,這只是implementation detail,好讓我可以得到60 FPS的triangle。60 FPS就是比較帥(自high)~

結論是,雖然這不是甚麼很難的技術,不過感覺網路上資源還滿少的,如果真的想要把這鬼東西放進工具,可能很多小細節都會讓實作的人很痛苦吧,而且那個痛苦的人看起來就是我啊...


題外話,IE9的javascript支援真是爛到爆了,編個blogger結果無法存檔也無法publish,而且這已經不是我第一次犯這個錯了。

No comments: