ファイナライズの使用方法
ドキュメンテーションガイド: http://duktape.org/guide.html#finalization.
簡単な例
ファイナライズ例:
// finalize.js
var a;
function init() {
a = { foo: 123 };
Duktape.fin(a, function (x) {
try {
print('finalizer, foo ->', x.foo);
} catch (e) {
print('WARNING: finalizer failed (ignoring): ' + e);
}
});
}
// オブジェクトを作成し、'a'を通してそれを参照する。
init();
// 参照削除,参照カウントで即座に確定
print('refcount finalizer');
a = null;
// refcountingが無効の場合、マーク&スイープのファイナライズはここで発生します(遅くとも)。
print('mark-and-sweep finalizer')
Duktape.gc();
上の例のファイナライザーの内部で try-catch ラッパーを使用することを強く推奨します。捕捉されないファイナライザーエラーは黙って無視されるので、ファイナライザーが全く実行されないように見えるかもしれません。
これをDuktapeコマンドラインツール(デフォルトのDuktapeプロファイルを使用)で実行すると、次のようになります。
$ duk finalize.js
refcount finalizer
finalizer, foo -> 123
mark-and-sweep finalizer
Cleaning up...
プロトタイプオブジェクトへのファイナライザーの追加
同じ型のオブジェクトが多数ある場合、プロトタイプにファイナライザーを追加することで、オブジェクトインスタンスのプロパティカウントを最小にすることができます。
// プラットフォーム固有のファイル記述子に関連付けられた仮想的なSocketオブジェクトの例です。
function Socket(host, port) {
this.host = host;
this.port = port;
this.fd = Platform.openSocket(host, port);
}
Duktape.fin(Socket.prototype, function (x) {
if (x === Socket.prototype) {
return; // called for the prototype itself
}
if (typeof x.fd !== 'number') {
return; // already freed
}
try {
Platform.closeSocket(x.fd);
} catch (e) {
print('WARNING: finalizer failed for fd ' + x.fd + ' (ignoring): ' + e);
}
delete x.fd;
});
// Socketインスタンスに対して明示的なファイナライザーを登録することなく、任意のSocketインスタンスがファイナライズされるようになりました。
var sock = new Socket('localhost', 8080);
ヒープ破壊
Duktapeのヒープが破壊されるとき、ファイナライザーは通常通り呼び出されます。
- オブジェクトを救出することはできません。ファイナライザーは、最初のファイナライザー呼び出しでネイティブ・リソースを解放しなければなりません。なぜなら、ターゲット・オブジェクトへの新しい参照が作成されても、ファイナライザーは再び呼び出されないからです。
- ファイナライザーは、新しいファイナライズ可能なオブジェクトを作成することができ、これらもファイナライズされます。しかし、この処理には、暴走するファイナライザーを捕まえるための健全性の限界があります。通常、この制限に遭遇することはないはずです。
Duktape 1.4.0以降、ファイナライザーには第2引数が与えられています。これは、オブジェクトが救出できない(ヒープ破壊が進行している)かどうかを示すブール値です。この引数は、救出が不可能な場合は真、そうでない場合は偽となります。Duktape 1.3.0以前では、この引数は提供されません。
もしファイナライザーが、(1)解放しなければならないネイティブ・リソースを管理し、 (2)オブジェクトの救助を使い、後でファイナライザーが再び呼ばれることに依存しているなら、 例えば、ヒープ破壊の場合を明示的にチェックする必要があります。
function myFinalizer(obj, heapDestruct) {
if (heapDestruct) {
// ヒープが破壊されているので、すぐに解放する必要があります。
freeNativeResources();
return;
}
// 通常の場合:後でファイナライザが呼ばれることを保証して、オブジェクトを救出することができる。
}
ヒープ破壊時のファイナライザーの現在のサニティ・アルゴリズム
ヒープ破棄時のファイナライザーのサニティ制限は、現在おおよそ以下のように動作します(正確な詳細はリリース間で変更される可能性があります)。
- 割り当てられたすべてのヒープ・オブジェクトが反復処理され、ファイナライザが実行可能なオブジェクトはすべてファイナライズされます。ファイナライザーは特定のオブジェクトに対して二度と実行されません。これは DUK_HEAPHDR_FLAG_FINALIZED を使って追跡されます。n_total を全ヒープオブジェクトの数、n_finalized をこのラウンドでファイナライザを実行した(というか実行しようとした)全オブジェクトの数とします。
- ファイナライズ可能なオブジェクトの数の限界は次のように計算される。
- 最初のラウンド: n_limit = 2 * n_total.
- 次のラウンドでは、n_limit = n_limit * 3 / 4、つまり約25%減少します。
- n_finalized == 0 の場合、すべてのファイナライザーが終了し、終了します。
- n_finalized >= n_limit の場合、ファイナライズ可能なオブジェクトの数が予想通り減らないので、おそらくファイナライザが暴走していることが原因です。ファイナライズ処理は終了し、残りのファイナライザーは実行されません。
- その他の場合は、ループを再開してください。
このアルゴリズムの動機は、明らかに暴走したファイナライザーがあり、プロセスが終了できない場合を除き、ヒープ破壊ですべてのファイナライザーが実行されるようにすることです。
最終化可能なオブジェクトの数を増やすために、最初の制限値はかなり大きくなっていますが、その後ラウンドごとに少なくとも25%減少しなければ最終化処理は中止されます。
ヒープ破壊のアプローチについては、https://github.com/svaarala/duktape/pull/473 でいくつか議論されています。