在 XPCOM 的世界裡,透過 XPConnect 的幫忙,C 和 JavaScript 實作的元件可以互相地呼叫,因此我們可以自由選用合適的語言來開發各自的元件。
但是因為中間包了一層 wrapper,所以在某些特殊的情況下,程式的運作可能會不如你的預期:例如在 跨越語言的邊界 – 淺談 JS API 與 XPConnect 有提到,動態改變 object property 時只會改變到 wrapper 而不會改變原本的物件。
這在 XPCOM 以 interface 為溝通管道的方式之下,也通常沒有什麼問題,不過今天要討論的,是一個會發生問題的情形。
首先,我們實作了一組
nsIDownloader 介面,然後因為下載通常很慢,所以這個介面使用了一個
nsIDownloadEventListener 的介面來通知下載的結果:
interface nsIDownloadEventListener : nsISupports
{
void onComplete(in DOMString aUrl);
void onFailure(in DOMString aUrl);
};
interface nsIDownloader : nsISupports
{
attribute nsIDownloadEventListener listener;
void download(in DOMString aUrl);
};
1234567891011
interface nsIDownloadEventListener : nsISupports{ void onComplete(in DOMString aUrl); void onFailure(in DOMString aUrl);}; interface nsIDownloader : nsISupports{ attribute nsIDownloadEventListener listener; void download(in DOMString aUrl);};
這一切看起來是如此地美好,那我們就用 C 來實作這組介面吧(因為篇幅的關係,以下程式碼會省略部份錯誤檢查):
class nsDownloader : public nsIDownloader
{
// 略...
private:
nsCOMPtr<nsIDownloadEventListener> mListener;
};
1234567
class nsDownloader : public nsIDownloader{ // 略... private: nsCOMPtr<nsIDownloadEventListener> mListener;};
在這裡,
nsDownloader 使用了一個
nsCOMPtr 來存放 listener 的指標。
然後在另一個 class 裡,如果要使用
nsDownloader 的話,可能會這麼寫:
class nsFoo
: public nsIFoo
, public nsIDownloadEventListener
{
// 略...
nsresult Init() {
mDownloader = do_CreateInstance(NS_DOWNLOADER_CONTRACT_ID);
return mDownloader->SetListener(this);
}
nsresult Bar(const nsAString& aUrl) {
if (mDownloader) {
return mDownloader->Download(aUrl);
}
return NS_ERROR_FAILURE;
}
private:
nsCOMPtr<nsIDownloader> mDownloader;
};
123456789101112131415161718192021
class nsFoo : public nsIFoo , public nsIDownloadEventListener{ // 略... nsresult Init() { mDownloader = do_CreateInstance(NS_DOWNLOADER_CONTRACT_ID); return mDownloader->SetListener(this); } nsresult Bar(const nsAString& aUrl) { if (mDownloader) { return mDownloader->Download(aUrl); } return NS_ERROR_FAILURE; } private: nsCOMPtr<nsIDownloader> mDownloader;};
大功告成,一切正常…正當你這麼以為的時候,reviewer 就跳出來告訴你:這段程式碼會有 memory leak。
因為
nsFoo 握有
nsDownloader ,而
nsDownloader 也握有
nsFoo ,這個問題就是在 說說 nsCOMPtr 這東西 裡面提過的環狀參照。
怎麼辦呢?有兩個解法:說說 nsCOMPtr 這東西 告訴我們如果參照的對象有實作
nsIWeakReference 的話,就可以使用
nsWeakPtr 來解決這個問題,或是使用 淺談 Cycle Collection 提到的 Cycle Collection 機制。
我們先用
nsWeakPtr 來試著一解決問題,所以
nsDownloader 就變成這樣:
class nsDownloader : public nsIDownloader
{
// 略...
private:
nsWeakPtr mListener;
};
1234567
class nsDownloader : public nsIDownloader{ // 略... private: nsWeakPtr mListener;};
但是
nsFoo 沒有實作
nsIWeakReference 呀!怎麼辦?
當然,我們可以修改 <span class="lang:default decode:true casino pa natet crayon-inline “>nsFoo 讓它實作
nsIWeakReference ,當你這樣告訴
nsFoo 的作者時,他可能會說因為
nsDownloader 的生命週期被包含在
nsFoo 的裡面,也就是說
nsDownloader 裡面的
mListener 不會是一個 dangling pointer,於是他讓你用 raw pointer 直接指向 listener,然後就有了下列的修改。
class nsDownloader : public nsIDownloader
{
// 略...
private:
nsIDownloadEventListener* mListener;
};
1234567
class nsDownloader : public nsIDownloader{ // 略... private: nsIDownloadEventListener* mListener;};
因為 raw pointer 也算是一種 weak reference,這麼一來,環狀參照的問題解決了,程式似乎也正常地執行了。
故事就這麼結束了嗎?
過了幾天,另外一位仁兄用 JavaScript 寫了另一份
nsFoo 的實作。
function nsFooJS() {
this._downloader = Cc[DOWNLOADER_CONTRACT_ID].createInstance(Ci.nsIDownloader);
this._downloader.listener = this;
}
nsFooJS.prototype = {
// 略...
QueryInterface: XPCOMUtils.generateQI([Ci.nsIFoo, Ci.nsIDownloadEventListener]),
bar: function(aUrl) {
this._downloader.download(aUrl);
}
};
1234567891011121314
function nsFooJS() { this._downloader = Cc[DOWNLOADER_CONTRACT_ID].createInstance(Ci.nsIDownloader); this._downloader.listener = this;} nsFooJS.prototype = { // 略... QueryInterface: XPCOMUtils.generateQI([Ci.nsIFoo, Ci.nsIDownloadEventListener]), bar: function(aUrl) { this._downloader.download(aUrl); }};
然後就悲劇了…
NS_ERROR_UNEXPECTED: Component returned failure code: 0x8000ffff (NS_ERROR_UNEXPECTED) [nsIDownloader.download]
1
NS_ERROR_UNEXPECTED: Component returned failure code: 0x8000ffff (NS_ERROR_UNEXPECTED) [nsIDownloader.download]
為什麼呼叫
nsIDownloader.download 會失敗呢?這個
NS_ERROR_UNEXPECTED 又是哪裡來的?且讓我們繼續看下去。
XPConnect wrappers 告訴我們,從 C 的 code 要執行 JavaScript 的程式碼,會透過
nsXPCWrappedJS 來操作,另外在 XPCWrappedJS.cpp 裡面有一段註解提到 nsXPCWrappedJS object 的生命週期,意思是說當 nsXPCWrappedJS object 的參考計數降為 1 的話,表示只有它握有的那個 JavaScript object 指向它,這時候如果也沒有任何一個 weak reference 指向這個 nsXPCWrappedJS object 的話,表示 C 這邊已經不會再有人會存取這個物件了,那麼這個物件會立刻被銷毀!也就是說,使用 raw pointer 存的 listener 不管它實際上的 JavaScript object 是不是還活著,它的 nsXPCWrappedJS object 都會失效,而解法就是把 raw pointer 改成
nsWeakPtr 。
這就是最有趣的地方了,當你因為某種需求而要握有一個物件的參考時,你必須要求它支援
nsWeakPtr ,不然就只能使用 strong reference。你不能使用 raw pointer,否則若該物件是用 C 實作的話沒事,當那個物件如果是用 JavaScript 來實作的話就會發生
NS_ERROR_UNEXPECTED 錯誤。
好,為了避免上述的問題,這時候我們先修改
nsDownlaoder ,讓它改用
nsWeakPtr
class nsDownloader : public nsIDownloader
{
// 略...
private:
nsWeakPtr mListener;
}
1234567
class nsDownloader : public nsIDownloader{ // 略... private: nsWeakPtr mListener;}
要讓
nsFooJS 支援
nsWeakPtr ,在 Bug 66950 裡面有提到,只要讓 JavaScript object 在
QueryInterface 裡宣告有實作
nsISupportsWeakReference 即可,然後你會發現其實有很多 JavaScript 實作的元件都有宣告這個介面。
nsFooJS.prototype = {
// 略...
QueryInterface: XPCOMUtils.generateQI([Ci.nsIFoo, Ci.nsIDownloadEventListener, Ci.nsISupportsWeakReference]),
};
12345
nsFooJS.prototype = { // 略... QueryInterface: XPCOMUtils.generateQI([Ci.nsIFoo, Ci.nsIDownloadEventListener, Ci.nsISupportsWeakReference]),};
如果要讓
nsFoo 支援
nsWeakPtr 的話,可以參考 Weak Reference 的介紹。
可是如果要考慮並不是所有的對象都可以支援
nsWeakPtr 的話,那麼就只好使用
nsCOMPtr ,然後用 Cycle Collection 來處理可能發生的環狀參照問題了。
[1] 跨越語言的邊界 – 淺談 JS API 與 XPConnect
[2] 說說 nsCOMPtr 這東西
[5] XPCWrappedJS.cpp
[6] Bug 66950
[7] Weak Reference