Skip to content

プログラミングモデル

概要

Duktapeを使ったプログラミングは非常に簡単です。

  • Duktapeのソース (duktape.c) とヘッダ (duktape.h と duk_config.h) をビルドに追加してください。もし、デフォルトの設定が適切でなければ、 python2 tools/configure.py を使って、カスタム設定用の Duktape ソースとヘッダを準備します。
  • Duktapeヒープ(ガベージコレクション領域)と初期コンテキスト(基本的にスレッドハンドル)をプログラム内に作成します。
  • 必要なECMAScriptスクリプト・ファイルをロードし、Duktape/C関数を登録します。Duktape/C関数は、ECMAScriptコードから呼び出すことができるC関数で、性能向上やネイティブ・ライブラリへのバインディングなどのために利用できます。
  • ECMAScriptの関数を呼び出すには、適宜、Duktape APIを使用してください。Duktape APIは、関数との間で値を受け渡しするために使用されます。値は、C言語の表現とDuktape内部(ECMAScript互換)の表現との間で変換されます。
  • Duktape APIは、(ECMAScriptから呼び出された)Duktape/C関数が呼び出しの引数にアクセスしたり、戻り値を提供したりする際にも使用されます。

それでは、すべてのステップとそれに関連する概念をより詳しく見ていきましょう。

ヒープとコンテキスト

Duktapeのヒープとは、ガベージコレクションのための1つの領域です。ヒープは、文字列やECMAScriptオブジェクト、その他の可変サイズのガベージコレクション・データのためのストレージを割り当てるために使用されます。ヒープ内のオブジェクトは、参照カウント、マーク&スイープ・ガベージコレクション、オブジェクトのファイナライゼーションなどに必要な情報を提供する内部ヒープ・ヘッダを備えています。ヒープオブジェクトは互いに参照し合うことができ、ガベージコレクションの観点から到達可能性グラフが作成される。例えば、ECMAScript オブジェクトのプロパティは、そのオブジェクトのプロパティセットのキーと値の両方を参照します。複数のヒープを持つことができますが、異なるヒープにあるオブジェクトはお互いを直接参照することができません。ヒープ間で値を渡すにはシリアライズを使用する必要があります。

DuktapeコンテキストはECMAScriptの「実行スレッド」であり、特定のDuktapeヒープに住んでいるものです。Duktape APIでは、コンテキストは duk_context * で表され、Duktape内部のコルーチン(協調スレッドの一種)に関連付けられます。各コンテキストはグローバル・オブジェクトからなる環境とも関連付けられています。コンテキストは同じグローバル環境を共有することもできますが、異なる環境を持つこともできます。コンテキスト・ハンドルはほぼ全てのDuktape APIコールに与えられており、呼び出し側はDuktapeコルーチンのバリュースタックと対話することができます:値の挿入や問い合わせ、関数の呼び出しなど。

各コルーチンは、実行を制御するコールスタックを持ち、ECMAScript エンジン内のネイティブまたは ECMAScript の関数呼び出しを追跡します。各コルーチンはまた、コルーチンのアクティブなコールスタックのすべての ECMAScript 値を格納するバリュースタックを持っています。バリュースタックは常に最新の関数呼び出しのアクティブ・スタック・フレームを持ちます(関数呼び出しが行われていない場合、アクティブ・スタック・フレームはそのままバリュースタックとなります)。Duktape APIの呼び出しは、ほとんど現在アクティブなスタック・フレームだけで動作します。また、try-catch-finallyなどを使ってエラー・キャッチのサイトを確立するための内部ブックキーピングもあります。

複数のコンテキストが同じDuktapeヒープを共有することができます。より具体的に言うと、これは複数のコンテキストが同じガベージコレクションの状態を共有でき、安全にオブジェクト参照を交換できることを意味します。異なるヒープにあるコンテキストは、直接オブジェクト参照を交換することはできません。すべての値は、何らかの方法でシリアライズされなければなりません。

Duktape APIが提供するほぼすべてのAPIコールは、その最初の引数としてコンテキスト・ポインタを取ります。グローバル変数や状態は使用されず、複数の独立したDuktapeヒープとコンテキストを同時に実行することに何の制限もありません。ただし、マルチスレッドに関する制限はあります。1つのヒープ内で任意のコードを実行できるネイティブ・スレッドは1つだけです。

Duktapeヒープとヒープ内の初期コンテキストを作成するには、単純に次のようにします。

c
duk_context *ctx = duk_create_heap_default();
if (!ctx) { exit(1); }

独自のメモリ割り当て関数や致命的なエラーハンドラ関数を用意する場合(推奨)は

c
duk_context *ctx = duk_create_heap(my_alloc,
                                   my_realloc,
                                   my_free,
                                   my_udata,
                                   my_fatal);
if (!ctx) { exit(1); }

同じヒープ内に新しいコンテキストを作成し、そのコンテキストが同じグローバルオブジェクトを共有するようにします。

c
duk_context *new_ctx;

(void) duk_push_thread(ctx);
new_ctx = duk_get_context(ctx, -1 /*index*/);

同じヒープ内に新しいコンテキストを作成するが、グローバルオブジェクトのセットは新しいものにする。

c
duk_context *new_ctx;

(void) duk_push_thread_new_globalenv(ctx);
new_ctx = duk_get_context(ctx, -1 /*index*/);

コンテキストは到達不可能になった時点で自動的にガベージコレクションされます。これはまた、もしあなたのCコードがduk_context *を保持しているなら、対応するDuktapeコルーチンはガベージコレクションの観点から到達可能でなければならない(MUST)ことを意味します。

ヒープは、呼び出し元がそれを使い終わったときに明示的に破棄されなければなりません。

c
duk_destroy_heap(ctx);

これにより、割り当てられたすべてのヒープオブジェクトが解放され、そのようなオブジェクトへのポインタがすべて無効になる。特に、呼び出し元のプログラムが、ヒープに関連付けられたコンテキストのバリュースタックに存在する値への文字列ポインタを保持していた場合、そのポインタは無効になり、ヒープ破棄呼び出しが戻った後は決して再参照してはならない。

コールスタック

コンテキストのコールスタックは、呼び出し元からは直接見えません。これは、あるコンテキストで現在実行されている C または ECMAScript の関数呼び出しの連鎖を記録しています。この簿記の主な目的は、関数の呼び出し元と呼び出し先の間で引数と結果の受け渡しを容易にし、関数呼び出しの間でバリュースタックがどのように分割されたかを追跡することです。また、コールスタックによって、Duktapeはエラー時のトレースバックを構築することができます。

Duktapeはテールコールをサポートしているため、コールスタックは必ずしも真のコールチェインを正確に表しているとは限りません。

Cスタックと混同しないように。

バリュースタックとバリュースタックインデックス

コンテキストのバリュースタックは、コルーチンの現在の実行状態に関連するタグ付き型の値の配列である。使用されるタグ付き型は、undefined, null, boolean, number, string, object, buffer, pointer, and lightfunc です。利用可能なタグ付き型の詳細については、「型」を参照してください。

バリュースタックは、コルーチンのコールスタック上の現在アクティブな関数呼び出し(アクティベーション)間で分割されます。いつでも、スタック上の要素をインデックスするための原点を提供するアクティブなスタックフレ ームがあります。より具体的には、Duktape APIではインデックス・ゼロで参照されるボトム(底)が常に存在します。また、現在使用されている最も高い要素のすぐ上にあるスタック要素を特定する概念的なトップがあります。以下の図がこれを示しています。

15エントリ(絶対インデックス)のバリュースタック

.----.
| 15 |
| 14 |
| 13 |
| 12 |     アクティブスタックフレーム(スタックボトムからの相対インデックス)
| 11 |      
| 10 |
|  9 |      .---.   スタックトップは6(スタックボトムとの相対値)。
|  8 | <--- | 5 |   APIインデックス5が最も多く使用されている(バリュースタックでは8の位置)。
|  7 |      | 4 |
|  6 |      | 3 |
|  5 |      | 2 |
|  4 |      | 1 |
|  3 | <--- | 0 |   APIインデックス0は最下位(バリュースタックでは3の位置)です。
|  2 |      `---'
|  1 |
|  0 |
`----'

内部バリュースタックの要素を参照する直接的な方法はありません。Duktape APIは、常に現在アクティブなスタック・フレームを扱います。Duktape APIは、常に現在アクティブなスタック・フレームを扱うため、文書全体を通してスタック・フレームは水平に表示されます。例えば、上図のアクティブなスタック・フレームは、次のように表示されます。

| 0 | 1 | 2 | 3 | 4 | 5 |

スタック・インデックスは、Duktape APIで使用される符号付き整数のインデックスで、現在のアクティブなスタック・フレームの要素を、現在のフレームの底から相対的に参照するために使用されます。

負ではない(>= 0)のインデックスは、フレームの底を基準として、現在のスタック・フレーム内のスタック・エントリーを参照します。

| 0 | 1 | 2 | 3 | 4 | <5> |

負の(< 0)インデックスは、スタックの先頭からの相対的なエントリーを指します。

| -6 | -5 | -4 | -3 | -2 | <-1> |

特殊定数 DUK_INVALID_INDEX は、無効なスタックインデックスを示す負の整数である。これは API 呼び出しから返され、また API 呼び出しに与えて "値がない" ことを示すことができます。

スタックトップ(または単に "トップ")は、最も使用されているインデックスのすぐ上の仮想要素の非負のインデックスである。例えば、最も使用されているインデックスの上は 5 であるので、スタックトップは 6 である。 トップは現在のスタックサイズを示し、またスタックにプッシュされる次の要素のインデックスである。

| 0 | 1 | 2 | 3| 4 | <5> | (6) |

API のスタック操作は、常に現在のスタックフレームに限定される。現在のフレームより下のスタックエントリを参照する方法はない。これは意図的なものであり、コールスタック内の関数が互いの値に影響を与えないようにするためである。

Cスタックと混同しないように。

バリュースタックを成長させる

コンテキストのバリュースタックは、いつでもある最大数のエントリに割り当てられています。割り当てられたサイズを超えて値をプッシュしようとすると、エラーがスローされ、バリュースタックが自動的に拡張されることはありません。これは、内部実装を単純化し、また、関数中にある数のエントリが必要であることが事前に分かっている場合に、再割り当てを最小限に抑えることでパフォーマンスを向上させるものです。

バリュースタックが作成されるとき、あるいは Duktape/C 関数が入力されるとき、バリュースタックは常に呼び出し引数と DUK_API_ENTRY_STACK (現在 64)要素のための空間を持つことが保証されています。一般的なケースでは、これは十分すぎるほどで、Duktape/C関数の大部分はバリュースタックを拡張する必要がありません。より大きなスペースを必要とする関数、あるいは入力に依存したスペースを必要とする関数だけが、バリュースタックを拡張する必要があります。

duk_check_stack() または (通常より好ましくは) duk_require_stack() で明示的にスタック割り当てを拡張することができます。いったん拡張に成功すると、指定された数の要素をスタックにプッシュできることが再び保証されます。Duktape/C関数から戻る以外、割り当てを縮小する方法はありません。

例えば、入力された ASCII 文字列を大文字に変換する次のような関数を考えてみましょう。この例では、必要なバリュースタックのエントリ数が入力に依存することが示されています(そうでなければ、これは文字列を大文字にするためのあまり良い方法とは言えません)。

c
/* uppercase.c */
#include <stdio.h>
#include <stdlib.h>
#include "duktape.h"

static int dummy_upper_case(duk_context *ctx) {
    size_t sz;
    const char *val = duk_require_lstring(ctx, 0, &sz);
    size_t i;

    /* スタックに 'sz' 個の追加エントリが必要です。 */
    duk_require_stack(ctx, sz);

    for (i = 0; i < sz; i++) {
        char ch = val[i];
        if (ch >= 'a' && ch <= 'z') {
            ch = ch - 'a' + 'A';
        }
        duk_push_lstring(ctx, (const char *) &ch, 1);
    }

    duk_concat(ctx, sz);
    return 1;
}

int main(int argc, char *argv[]) {
    duk_context *ctx;

    if (argc < 2) { exit(1); }

    ctx = duk_create_heap_default();
    if (!ctx) { exit(1); }

    duk_push_c_function(ctx, dummy_upper_case, 1);
    duk_push_string(ctx, argv[1]);
    duk_call(ctx, 1);
    printf("%s -> %s\n", argv[1], duk_to_string(ctx, -1));
    duk_pop(ctx);

    duk_destroy_heap(ctx);
    return 0;
}

Duktapeは、ユーザー予約要素に加えて、全てのAPIコールがさらなる割り当てなしに動作するのに十分なバリュー・スタック・スペースを確保するために、自動的に内部のバリュー・スタック・リザーブを保持します。また、メモリの再割り当てを最小限に抑えるため、バリュースタックはある程度大きなステップで拡張・縮小されます。その結果、呼び出し元が指定した余分な値を超えて利用可能なバリュースタック要素の内部数は、かなり変化します。呼び出し元はこれを考慮する必要はなく、利用可能な追加要素に依存するべきでは決してない。

ECMAScriptの配列インデックス

ECMAScript のオブジェクトおよび配列のキーは、文字列またはシンボルのみとする。配列のインデックス (例: 0, 1, 2) は、それぞれの数値の標準文字列表現 (例: "0", "1", "2") として表現されます。より技術的には、範囲 [0, 2**32-2] の整数のすべての標準文字列表現が有効な配列インデックスとなります。

ECMAScript の配列インデックスの取り扱いを説明するために、次の例を考えてみましょう。

javascript
var arr = [ 'foo', 'bar', 'quux' ];

print(arr[1]);     // refers to 'bar'
print(arr["1"]);   // refers to 'bar'

print(arr[1.0]);   // refers to 'bar', canonical encoding is "1"
print(arr["1.0"]); // undefined, not an array index

ECMAScript の配列上で動作するいくつかの API 呼び出しは、配列の数値インデックス引数を受け付けます。これは実際には、その数値の文字列変換を示すショートハンドに過ぎません。例えば、APIに整数の123が与えられた場合、これは実際にはプロパティ名 "123 "を指します。

内部的には、Duktapeは可能な限り数値インデックスを実際の文字列に変換することを避けようとします。したがって、関連する場合は配列インデックスAPIコールを使用することが望ましいです。同様に、ECMAScriptのコードを書く場合にも、文字列インデックスではなく、数値を使うことが望ましいです。

Duktape API

Duktape APIは、duktape.hで定義され、APIリファレンスで文書化されたユーザー呼び出し可能なAPIコールの集合体です。

Duktape APIコールは一般的にエラーに寛容で、全ての引数にエラー(NULLポインタなど)がないかをチェックします。しかし、フットプリントを最小にするため、ctx引数はチェックされず、呼び出し側は NULLコンテキストでDuktape APIコールを呼び出してはいけません(MUST NOT)。

すべてのDuktape APIコールは、潜在的にマクロです。呼び出し側のコードは、Duktape APIコールが関数ポインタとして利用可能であることに 依存してはいけません。あるAPIコールの実装は、互換性のあるリリース間であっても、マクロと実際の関数の間で変更される可能性があります。Duktape APIは、マクロに対して以下の保証を行います。

  • 引数は2回以上評価されない(明示的に記述されていない限り)。ただし、現在のバージョンで引数が無視される場合、引数は全く評価されない可能性があります。
  • 戻り値のあるAPIコールは、式として使用できる。APIマクロが複数のステートメントを含む場合、カンマ式(例:(foo, bar, quux))として実装される。
  • 戻り値が void の API 呼び出しは、必ずしも式の一部として動作しない場合がある。APIマクロは、ブロック文またはダミーの do {...} while (0) ループとして実装されることがある。

Duktape/C 関数

Duktape/C APIシグネチャを持つC関数は、ECMAScript関数オブジェクトと関連付けることができ、ECMAScript関数オブジェクトが呼び出されたときに呼び出されます。Duktape/C API関数は、以下のような形をしています。

c
duk_ret_t my_func(duk_context *ctx) {
    duk_push_int(ctx, 123);
    return 1;
}

この関数は、ECMAScript の呼び出し引数を ctx のバリュースタックで取得し、 duk_get_top() は、バリュースタック上に存在する引数の数を示します。このバインディングは、自動的にバリュースタックにプッシュされません; 必要であれば、それにアクセスするために duk_push_this() を使用してください。Duktape/C 関数に関連付けられた ECMAScript 関数オブジェクトを作成する際に、希望する引数の数を選択することができます。余分な引数は削除され、足りない引数は undefined に置き換えられます。関数は、(引数数としてDUK_VARARGSを与えることにより)vararg関数として登録することもでき、この場合、呼び出し引数はC関数入力前に修正されません。

この関数は、以下のいずれかを返すことができる。

  • 戻り値 1 は、スタックの一番上の値が戻り値として解釈されることを示す。
  • 戻り値 0 は、バリュースタック上に明示的な戻り値がないことを示し、undefined が呼び出し元に返される。
  • 負の戻り値は、自動的にエラーが投げられることを示す。DUK_RET_xxx というエラーコードは、特定の種類のエラーに対応します(正の値である DUK_ERR_xxx と混同しないでください)。これはオプションの省略記法で、フットプリントは小さいですが、いくつかの欠点もあります (例えば、エラーメッセージがないなど)。
  • ECMAScript は Edition 5.1 では複数の戻り値をサポートしていないので、1 以上の戻り値は現在未定義です (1 以上の値は、ECMAScript Edition 6 で複数の戻り値をサポートするようになるかもしれません。)。

Duktape/C関数が戻るとき、バリュースタックは自動的に巻き戻されるので、関数が戻ったときにバリュースタックを手動でクリーンアップする必要はありません。

負のエラー返り値は、一般的なエラー処理を簡素化することを目的としており、Duktape APIコールで明示的にエラーを構築して投げることの代替となります。Duktapeが自動的にメッセージを作成するため、エラーメッセージを与えることはできません。例えば

c
duk_ret_t my_func(duk_context *ctx) {
    if (duk_get_top(ctx) == 0) {
        /* 引数が与えられない場合、TypeErrorを投げる */
        return DUK_RET_TYPE_ERROR;
    }
    /* ... */
}

すべてのDuktape/C関数は、ECMAScriptの意味においてストリクト(厳密)であるとみなされます。DuktapeのAPIコールは、たとえDuktape/C関数の外部で、つまりコールスタックが空の状態でAPIコールが行われたとしても、常にECMAScriptのstrictモードのセマンティックに従います。例えば、duk_del_prop()を使って設定不可能なプロパティを削除しようとすると、エラーがスローされます。これは、ECMAScriptの厳密な関数でも同様です。

javascript
function f() {
    'use strict';
    var arr = [1, 2, 3];
    return delete arr.length;  // 配列 'length' は設定不可
}

print(f());  // f() がストリクトであるため、エラーを発生させます。

Duktape/C関数の厳密性のもう一つの帰結は、Duktape/C関数に与えられたこのバインディングが強制されないということです。これはECMAScriptの厳密なコードにも当てはまります。

javascript
function strictFunc() { 'use strict'; print(typeof this); }
function nonStrictFunc() { print(typeof this); }

strictFunc.call('foo');     // prints 'string' (uncoerced)
nonStrictFunc.call('foo');  // prints 'object' (coerced)

Duktape/Cの関数は、現在のところ常にコンストラクタブルで、つまりnew Foo()式の中で常に使用することが可能です。ある関数がコンストラクタ・モードで呼び出されたかどうかは、以下のようにして確認することができます。

c
static duk_ret_t my_func(duk_context *ctx) {
    if (duk_is_constructor_call(ctx)) {
        printf("called as a constructor\n");
    } else {
        printf("called as a function\n");
    }
}

メモリを節約するために、Duktape/C関数はデフォルトでprototypeプロパティを持たないので、デフォルトのオブジェクト・インスタンス(thisとしてコンストラクタに与えられる)はObject.prototypeを継承しています。カスタム・プロトタイプを使用するには、Duktape/C関数に対して明示的にプロトタイプを定義します。ECMAScript関数と同様に、コンストラクターがオブジェクトの値を返す場合、その値はデフォルトのオブジェクト・インスタンスを置き換え、新しい式の値となります。

このバインディングは自動的にバリュースタックにプッシュされないので、アクセスするには duk_push_this() を使ってください。

Duktape/C関数の状態を格納する

Duktape/C関数にアウトオブバンドで、つまり明示的な呼び出し引数以外で、パラメータや追加ステートを提供したい場合があります。これを実現するためには、いくつかの方法があります。

関数のプロパティ

まず、Duktape/C関数はそのFunctionオブジェクトを使って、状態やパラメーターを保存することができます。ある Duktape/C 関数(実際の C 関数)は常に ECMAScript の Function オブジェクトで表され、内部的に基礎となる C 関数と関連付けられています。Functionオブジェクトは、その関数の特定のインスタンスに関連するプロパティを保存するために使用することができます。ある Duktape/C 関数は、複数の独立した Function オブジェクトに関連付けることができ、したがって、独立した状態にすることができることに注意してください。

Duktape/C関数に関連するECMAScript Functionオブジェクトにアクセスするのは簡単です。

c
duk_push_current_function(ctx);
duk_get_prop_string(ctx, -1, "my_state_variable");

'this'バインディング

状態を保存するためのもう一つの方法は、Duktape/C関数をメソッドとして呼び出し、状態を保存するためにthisバインディングを使用することです。例えば、asと呼ばれるDuktape/C関数を考えてみましょう。

javascript
foo.my_c_func()

Duktape/C関数は呼び出されると、このバインディングとしてfooを取得し、fooに直接ステートを格納することができます。関数オブジェクトのアプローチと異なるのは、すべてのメソッドで同じオブジェクトが共有される点です。

thisバインディングにアクセスするのは簡単です。

c
duk_push_this(ctx);
duk_get_prop_string(ctx, -1, "my_state_variable");

隠されたシンボルのプロパティ

データをオブジェクトと関連付ける必要があるが、ECMAScript コードから隠されている場合、隠しシンボルをプロパティキーとして使用することができます。このキーは、C API から ECMAScript コードに渡されない限り、C API を介してのみアクセス可能です。一般的な使用例としては、C メモリへのポインタ/データのバッキングを関連付けることです。シンボルは文字列として作成されますが、シンボルとしてマークするマクロで区別されます。

例えば、この上に隠されたシンボルを設定したり、取得したりする。

c
my_context_data_t *my_context_data = malloc(sizeof(my_context_data_t));
duk_push_this(ctx);
duk_push_pointer(ctx, my_context_data);
duk_put_prop_string(ctx, -2, DUK_HIDDEN_SYMBOL("my_context_data"));
/* ... */
duk_push_this(ctx);
duk_get_prop_string(ctx, -1, DUK_HIDDEN_SYMBOL("my_context_data"));
my_context_data_t *my_context_data = duk_get_pointer(ctx, -1);

関数のマジックバリュー

Duktape/C関数オブジェクトは、16ビット符号付き整数の「マジック」値(デフォル トではゼロ)を、余分なメモリー・コストなしに内部に保存することができます。このマジック・バリューは、フラグや小さな値を最小限のコストで Duktape/C 関数に渡すために使用でき、これにより、単一のネイティブ関数が複数の関数 オブジェクトに対してわずかに異なる動作を提供することができます。

c
/* マジックバリューの例: 
 * 最下位2ビットをプレフィックスインデックスに
 * ビット2(0x04)をログ書き込みヘルパーの
 * 改行スタイルに使用する場合。
 */

const char *prefix[4] = { "INFO", "WARN", "ERROR", "FATAL" };
duk_int_t magic = duk_get_current_magic(ctx);

printf("%s: %s", prefix[magic & 0x03], duk_safe_to_string(ctx, 0));
if (magic & 0x04) {
    printf("\r\n");
} else {
    printf("\n");
}

APIの使用例については、テストケースである test-get-set-magic.c を参照してください。Duktapeは、コンパイルされたコードのサイズを最小化するために、内部的に マジック値を多用しています(例えば、duk_bi_math.cを参照してください)。

マジック・バリューの仕組みは、Duktapeのメジャー・バージョン間で変更される可能性があり、利用可能な予備ビットの数が変わるからです。マジック・バリューを使うのは、フットプリントが本当に重要なときだけにしてください。関数オブジェクトに保存されるプロパティは、より安定した代替手段です。

ヒープスタッシュ

ヒープ・スタッシュは、Cコードからだけ見えるオブジェクトです。これは Duktape ヒープに関連付けられ、Duktape/C コードが ECMAScript コードに公開されない「アンダー・ザ・フード」状態データを保存することを可能にします。これは duk_push_heap_stash() API 呼び出しでアクセスします。

グローバルスタッシュ

グローバルスタッシュはヒープスタッシュと似ていますが、グローバルオブジェクトと関連付けられています。duk_push_global_stash() APIコールでアクセスできます。同じヒープ内に、異なるグローバル・オブジェクトを持つ複数の環境が存在することがあります。

スレッド隠し場所

スレッド・スタッシュはヒープ・スタッシュと似ていますが、Duktapeスレッド(つまりctxポインタ)に関連付けられます。duk_push_thread_stash() APIコールでアクセス可能です。

Duktapeのバージョン固有コード

Duktapeのバージョンは、DUK_VERSION定義を通じて、 (major * 10000) + (minor * 100) + patchという数値で利用可能です。同じ値は、Duktape.versionを通じて、ECMAScriptコードで利用可能です。呼び出し側のコードは、Duktapeのバージョンに特化したコードのために、この定義を利用することができます。

Cコードの場合。

c
#if (DUK_VERSION >= 20403)
/* Duktape 2.4.3 or later */
#elif (DUK_VERSION >= 10500)
/* Duktape 1.5.0 or later */
#else
/* Duktape lower than 1.5.0 */
#endif

ECMAScriptのコード用(Duktape build-insも参照)。

javascript
if (typeof Duktape !== 'object') {
    print('not Duktape');
} else if (Duktape.version >= 20403) {
    print('Duktape 2.4.3 or higher');
} else if (Duktape.version >= 10500) {
    print('Duktape 1.5.0 or higher (but lower than 2.4.3)');
} else {
    print('Duktape lower than 1.5.0');
}

数値エラー・コード

Duktape API でエラーが発生した場合、呼び出し側はそのエラーに数値のエラー・コードを 割り当てなければなりません。エラーコードは正の整数であり、現時点では 24 ビットに制限されています。組み込みのエラーコードは duktape.h で定義されており、例えば DUK_ERR_TYPE_ERROR がある。

残りの上位ビットは、例えば追加のフラグを運ぶために内部的に使用される。負のエラー値は、Duktape/C APIにおいて、自動的にエラーを投げるための省略記法として使用されます。

エラー・ハンドリング

Duktape APIにおけるエラー処理は、ECMAScriptがエラーを処理する方法と似ています:エラーは明示的または暗黙的にスローされ、その後キャッチされ処理されます。エラーは明示的または暗黙的にスローされ、その後キャッチされ処理されます。Cコードはtry-catch文の代わりにprotected Duktape APIコールを使って、Cコード内でエラーをキャッチし処理できるポイントを確立します。保護された呼び出しを除く全てのDuktape APIコールは、エラーを投げる可能性があります。ほとんどのECMAScript操作は、状況によってはエラースローを引き起こす可能性があり、メモリ不足エラーはほとんど全ての状況で起こり得ます。throw サイトと catch サイト間の長い制御転送は、 setjmp()/longjmp() (またはそのプラットフォーム固有のバージョン)、または C++ 例外スロー (DUK_USE_CPP_EXCEPTIONS が有効な場合) に基づいています、長い制御転送を参照してください。

捕捉されないエラーは致命的なエラーハンドラを呼び出しますが、これは回復不可能な状況とみなされ、通常は避けるべきです。致命的なエラーを回避するために、一般的なアプリケーション・コードでは、他のDuktape APIを呼び出す前に、エラー・キャッチ・サイトを確立する必要があります。これは、例えば保護されたDuktape APIコールを使って行われます。

コードの評価 (duk_peval())、コードのコンパイル (duk_pcompile())、関数の呼び出し (duk_pcall()) には protected コールを使用する。 単一の duk_safe_call() を使って、安全でないプリミティブを安全な呼び出しの内部で自由に使えるように、エラー・キャッチャーを確立する。

最初の手法の例。

javascript
/* duk_safe_call() を使って
 * すべての安全でないコードを別のC関数にラップしてください。
 * この方法は、すべてのAPIコールを自動的にカバーする
 * という利点がありますが、少し冗長になります。
 */

static duk_ret_t unsafe_code(duk_context *ctx, void *udata) {
    /* Here we can use unprotected calls freely. */

    (void) udata;  /* 'udata' may be used to pass e.g. a struct pointer */

    push_file_as_string(ctx, "myscript.js");
    duk_eval(ctx);

    /* ... */

    return 0;  /* success return, no return value */
}

/* elsewhere: */

if (duk_safe_call(ctx, unsafe_code, NULL /*udata*/, 0 /*nargs*/, 1 /*nrets */) != 0) {
    /* 'nrets' 引数はエラーが発生した場合、
     * スタックにエラー値が残るように最低でも 1 でなければならない。
     * さらなるエラーを避けるために
     * duk_safe_to_string() を使って安全にエラーを出力する。
     */

    printf("Unexpected error: %s\n", duk_safe_to_string(ctx, -1));
}
duk_pop(ctx);

プロテクトされたコール内でも、内部エラーのように致命的なエラーを引き起こすか、プロテクトされたAPIコールから外部にエラーを伝播させるような稀なケースがあります。これらは異常時にのみ発生し、回復可能とはみなされません。これらのケースをうまく処理するために、生産品質のアプリケーションは常に致命的なエラーを処理するための合理的な戦略を持つ致命的なエラーハンドラを持つべきです。そのような戦略は、必然的にアプリケーションに依存しますが、次のようなものになるでしょう。

  • 組み込みデバイスでは、致命的なエラーハンドラは致命的なエラー情報をフラッシュファイルに書き込んで、デバイスをリブートすることができます。リブート後、致命的なエラーを診断サーバーに報告し、調査できるようにする。
  • UNIX システムでは、致命的エラーハンドラは単にプロセスを終了させ (デフォルトの致命的ハンドラは abort() を使用します)、 ラッパースクリプトにアプリケーションを再開させることができます。

アプリケーションによっては、エラー・キャッチャーなしでAPIコールを行い、 致命的なエラーにつながるキャッチできないエラーを投げる危険性があることは、 問題にならないかもしれないことに注意してください。致命的なエラーが発生した後、実行を継続することは安全ではないので、そのようなアプリケーションは、致命的なエラーが発生した場合、通常、単に終了します。実際の回復手段がない場合でも、致命的なエラーハンドラを使用して、例えばプロセス終了前に標準エラーに致命的なエラー情報を書き込む必要があります。

通常エラーと致命的エラー

通常のエラーは throw 文、duk_throw() API 呼び出し(または類似のもの)、あるいは内部で回復可能な Duktape エラーによって発生します。通常のエラーは、ECMAScript コードでは try-catch で、C コードでは duk_pcall() (API calls tagged protected を参照)で捕捉できます。

致命的なエラーは、キャッチできないエラー、アサーションの失敗(有効な場合)、duk_fatal()への明示的なコール、Duktape内部の回復不能なエラーによって引き起こされます。

各Duktapeヒープは、duk_create_heap()で登録されたヒープ全体の致命的なエラー・ハンドラを持っています。ハンドラが与えられない場合、組み込みのデフォルト・ハンドラが使用されます。

  • デフォルト設定: 組み込みのデフォルトの致命的なエラーハンドラは、デバッグログメッセージを書きますが、標準出力や標準エラーには何も書きません。デバッグログはデフォルトで無効になっており、致命的なエラーメッセージはデフォルトでは表示されません。ハンドラは次にabort()を呼び出します。abort() が何らかの理由で終了した場合、ハンドラは無限ループに入り、致命的なエラーの後に実行が再開されないことを保証します。
  • DUK_USE_CPP_EXCEPTIONS が有効: 組み込みのデフォルトの致命的なエラーハンドラは duk_fatal_exception を投げます。この例外は std::runtime_error を継承しているので簡単に捕捉でき、致命的なエラーメッセージを読み込むための ::what() メソッドを提供します。致命的なエラーをキャッチした後に実行を継続するのは安全ではありません。
  • DUK_USE_FATAL_HANDLER が定義されている場合、C++ 例外を有効にしても、常に組み込みのデフォルトの致命的なエラーハンドラとして使用されます。

(1) ヒープ生成時にカスタム致命的エラーハンドラを提供する、 (2) duk_config.h の DUK_USE_FATAL_HANDLER() オプションを使って、 組み込みのデフォルト致命的エラーハンドラを置き換える、の両方を強くお勧めします。

致命的なエラーの後に実行を再開する安全な方法はありません。そのため、致命的なエラー・ハンドラは戻ったりDuktape APIを呼び出してはいけません。その代わり、ハンドラは例えばコンソールやログ・ファイルにメッセージを書き出し、その後プロセスを終了(または組み込みデバイスを再起動)する必要があります。これは、C++の例外を長い制御転送メカニズムとして使用する場合にも適用されます。致命的なエラーの後、アプリケーションが実行を継続すると、すべての賭けがなくなります。メモリリークが発生する可能性があり、メモリ安全性が損なわれるかもしれません。

致命的なエラーは、ヒープコンテキスト無しで発生することもあり、その場合 Duktapeはヒープ固有の致命的なエラー・ハンドラを探すことができません。その場合、Duktapeは常に組み込みのデフォルトの致命的エラーハンドラを呼び出します(handler userdata引数をNULLに設定した場合)。この方法で処理される致命的なエラーは、現在のところアサーションの失敗に限られています。そのため、アサーションを有効にしない場合、そのようなエラーは現在発生せず、すべての致命的なエラー処理は、アプリケーションが直接制御するヒープに関連する致命的なエラー・ハンドラを通じて行われます。

より詳細な情報や例については、致命的なエラーの処理方法を参照してください。

ロング・コントロール・トランスファー

Duktapeがエラーのスローやキャッチのために内部で使用している特定の長い制御転送メカニズムは、アプリケーション・コードからは直接見えません。アプリケーション・コードはエラーをキャッチするために保護された呼び出しを使用し、エラーはDuktapeまたはアプリケーションによって、例えばduk_error()などの様々なDuktape API呼び出しを使用してスローされます。

DUK_USE_CPP_EXCEPTIONS を使用しないデフォルトの構成

デフォルトの構成では、Duktape API の保護された呼び出しは setjmp() を使用してキャッチ・サイトを確立します。エラースローサイトでは、longjmp()を使用して、ネイティブCスタックを巻き戻し、(最も近い)キャッチサイトに戻ります。いくつかのプラットフォームでは、sigsetjmp() や siglongjmp などの呼び出しの変種が使用されています。呼び出しの亜種の間には、例えばパフォーマンスやシグナル処理に関して、小さな違いがあります。呼び出しの種類は duk_config.h で選択されます。

longjmp() は、longjmp() と setjmp() の間のすべてのネイティブ C スタックフレームを巻き戻しま す。しかし、この巻き戻し処理は C++ の自動デストラクタを呼び出さないので、C++ アプリケーションによっては重大な制限となる可能性があります。

致命的なエラー、例えば捕捉されないエラーについては、デフォルトの致命的なエラーハンドラは abort() を使用します、正常なエラーと致命的なエラーを参照してください。

C++モード、DUK_USE_CPP_EXCEPTIONSを使用した場合

DUK_USE_CPP_EXCEPTIONSが有効な場合、長い制御転送はC++の例外スローに基づいて行われます。保護された呼び出しは、C++のトライ・キャッチを使用してキャッチ・サイトを確立します。これはDuktape内部で発生し、アプリケーションからは見えないことに注意してください。エラースローサイトはduk_internal_exceptionをスローし、(最も近い)Duktapeのキャッチサイトによってキャッチされます。アプリケーション・コードでは、この例外をキャッチ(スロー)してはいけません。そのリスクを最小限にするために、この例外は標準的な例外クラスを継承していないので、お決まりの std::exception catch サイトで捕捉されることはありません。

致命的なエラー、例えばキャッチされないエラーの場合、duk_fatal_exception はデフォルトの致命的なエラーハンドラによって投げられます。この例外は std::runtime_error を継承しており、ユーザコードで捕捉されることを意図しています。この例外は、致命的なエラーメッセージを返す ::what() メソッドを提供します。致命的なエラーは捕捉可能であっても、エラーを捕捉した後に実行を継続することは安全ではありません。正常なエラーと致命的なエラーを参照してください。

どちらの C++ 例外タイプでも、C++ ネイティブのスタック巻き戻し処理は、throw サイトと catch サイトの間のスタックフレームの自動デストラクタをサポートしています。