Skip to content

Duktapeデバッガ

はじめに

概要

Duktapeには、以下の基本的なデバッグ機能があります。

  • 実行状況:ファイル/ラインでの実行/一時停止、コールスタック、ローカル変数
  • 実行制御:一時停止、再開、ステップオーバー、ステップイン、ステップアウト
  • ブレークポイント:対象となるファイル/行のペア、ブレークポイントリスト、"debugger "ステートメント
  • 一時停止中にコールスタック上の任意の活性化のコンテキストで評価(基本的なウォッチ式の実装に使用可能)
  • 内部メタデータ、プロパティを列挙してヒープオブジェクトを検査し、プロトタイプチェーンを歩く
  • 任意のコールスタックレベルで変数を取得/配置する。
  • アプリケーション定義のリクエスト(AppRequest)および通知(AppNotify)のためのメカニズム
  • ロガーの書き込みを転送する
  • ヒープフルダンプ(デバッガーのウェブUIでJSONに変換される)

Duktapeのデバッグ・アーキテクチャは、以下の主要な部分から構成されています。

  • Duktapeが直接実装する標準的なデバッグ・プロトコルです。
  • アプリケーションによって実装される、信頼性の高い デバッグトランスポート ストリーム。
  • Duktapeヒープにデバッガをアタッチ/デタッチするためのデバッグAPIです。
  • デバッグプロトコルエンドポイントを実装し、ユーザーインターフェースを提供する、オフターゲットで動作するデバッグクライアントである。
  • オプションの JSON デバッグプロトコルプロキシ は、デバッグターゲットと対話するための、より簡単なJSONベースのインターフェイスを提供します。 Node.jsとDukLuvで書かれたプロキシ実装の例がDuktapeに含まれています。

本書では、これらの作品について詳しく説明します。

はじめに:"duk "を使ったデバッグについて

デバッグターゲットとして duk --debugger を、デバッグクライアントとして debugger/duk_debug.js を使用する具体的な方法については debugger/README.rst を参照してください。

はじめに:ターゲットのデバッグ

ターゲットにデバッガサポートを組み込むには、以下のことが必要です。

  • 設定オプションの確認: Duktape のデバッガーサポートを有効にするには、DUK_USE_DEBUGGER_SUPPORTDUK_USE_INTERRUPT_COUNTER を有効にしてください。また、その他のデバッグ関連のコンフィグオプションも考慮してください。
  • 具体的なストリーム伝送機構を実装する: ターゲット・デバイスとDuktapeデバッガの両方に必要です。 最適なトランスポートはターゲットに依存します。例えば、TCP ソケット、 シリアルリンク、あるいは既存のカスタムプロトコルにデバッグデータを埋め込む、などです。 TCP デバッグトランスポートの例は examples/debug-trans-socket/duk_trans_socket_unix.c で提供されています。
  • デバッガを添付するコードの追加: はデバッグを開始する時に duk_debugger_attach() を呼び出します。Duktapeは実行を一時停止し、デバッグメッセージを処理します(必要ならブロッ キングします)。 実行はデバッグクライアントの制御下で再開されます。
  • 終了後、デバッガをデタッチします: デバッグを停止するには duk_debugger_detach() を呼び出します。デバッグストリームエラーも自動的に切り離されます。 デバッグストリームエラーが発生すると、Duktapeは通常の実行を再開し、ブレークポイントなどを無視します(デバッグクライアントから明示的に要求された場合やDuktapeがデバッグストリームエラーを検出した場合にもデタッチが発生することがあります。)。
  • イベントループがある場合: オプションで、Duktape の呼び出しが行われていない時に、たまに duk_debugger_cooperate() を呼び出します。 これにより、デバッグコマンドを Duktape の呼び出しの外で実行することができるようになります。

Duktapeには、プレーンなTCPトランスポートをサポートするデバッグ・クライアントが付属しています。また、サードパーティ製のデバッグクライアントもいくつかあり、ターゲットと通信できるようにすることができます。これらは同じデバッグプロトコルを共有しているので、トランスポートだけを適合させる必要があります。

また、デバッグプロトコルのクライアント側を実装することで、独自のデバッグクライアントを作成することもできます。 デバッグ・クライアントはターゲットのデバッグ・プロトコルのバージョンに適応するよう意図されているので、Duktapeデバッグ・プロトコルの進化に伴って、デバッグ・クライアントの変更が必要になる場合があります。 デバッグ・プロトコルは、Duktape APIと同じセマンティック・バージョニングの原則でバージョン管理されています。

バイナリー・デバッグ・プロトコルをデバッグ・クライアントに直接実装することもできますが、より簡単な方法として、デバッグ・プロトコルのJSONマッピングを使用すると、より使い勝手が良くなります。 Duktapeには、JSONマッピングと、ターゲット上で実際に動作するバイナリー・デバッグ・プロトコルを変換するプロキシサーバーが含まれています。

クライアントとサーバーのデバッグ例

Duktapeレポには、デバッグ・トランスポートにTCPを使用し、Duktapeコマンドライン・ツール(「duk」)と通信できるデバッガーのウェブUIの例が含まれています。 この実行例は、デバッグコマンドの具体的な詳細とデバッグトランスポートの実装方法をさらに文書化するためのものです。 ウェブコンソールは、TCPデバッグトランスポートを使用する他のデバッグターゲットと直接対話することも可能です。

デバッガーのサンプルには:

  • Duktapeのコマンドラインツールの --debugger オプションは、 DUK_CMDLINE_DEBUGGER_SUPPORTDUK_USE_DEBUGGER_SUPPORT の両方を使用することで有効にすることができます。コマンドラインツールは examples/debug-trans-socket/ で提供される TCP ソケットベースのサンプルトランスポートを使用します。

  • NodeJS + ExpressJS ベースの最小デバッガー Web UI が debugger/ ディレクトリにあり、デバッグ転送に TCP ソケットを使用します。

TCPは良い例のトランスポートではあるが、「標準」トランスポートではない。トランスポートは常に最終的にはユーザーコード次第である。

ローカルデバッガの例

通常、リモート・デバッグ・クライアントが望ましいのですが、場合によってはDuktapeが動作しているのと同じプロセスでデバッグ接続を終了させるのが便利なことがあります。 Duktapeの観点からは、「ローカル」デバッガはリモートのデバッガと同じです:デバッグ・トランスポートの実装がDuktapeとの違いを隠してくれます。ローカルなd値エンコーダー/デコーダーを持つデバッグ・トランスポートの例があります。

  • examples/debug-trans-dvalue/

サンプルのトランスポートは、dvalueのエンコードとデコードの詳細を隠し、ローカルのデバッグクライアントを書きやすくします。 このトランスポートは、C言語でdvalueを扱う例としても役立ちます(Node.jsのデバッガには、Javascriptのための同様の例があります)。

Duktapeが提供しないもの

標準的なデバッグ用トランスポート

最適なトランスポートは千差万別なので、これはユーザーコード次第です。Wi-Fi、シリアルポート、などなど。 しかし、TCP を使用しない特別な理由がないのであれば、おそらく TCP は良いデフォルトのトランスポートとなるでしょう。 バンドルされているサンプルのデバッガー Web UI と JSON デバッグプロキシは、トランスポートとして TCP を使用します。

標準的なデバッガーUI

ユーザー・コードは、Duktapeがサポートするデバッグ・コマンドの上に、具体的なデバッガー・インターフェイスを実装する必要があります。 しかし、Duktapeには、完全に機能するデバッガの例が含まれています。 必要に応じてこれを拡張することもできますし、自分で書くこともできます。

機能ソースコード

Duktapeは現在、デバッグ・プロトコルで関数のソース・コードを提供していません。 デバッグクライアントは、一致するソースコードにアクセスでき、特定のファイル名に一致するソースファイルを見つける能力を持っていることが前提です。 これは、eval を使って作成された関数は、ソースが存在する状態でデバッグできないことも意味します。

デバッガサポート有効化の影響

パフォーマンス

デバッガが接続され、実行中の関数にアクティブなブレークポイントがある場合を除き、パフォーマンスへの影響は非常に小さいはずです。

バイトコードエクゼキュータが再起動するとき、デバッガが接続されていないことをすぐに判断し、ブレークポイントを処理する必要はありません。 バイトコード実行に何らかの影響を与えるデバッガを使用するには、バイトコードエクゼキュータ割り込みを有効にする必要があります。

Duktapeは、デバッガが接続され、現在の関数にアクティブなブレークポイントがある場合、「チェック済み実行」になります。 チェック実行(詳細は後述)は、通常の実行よりはるかに遅く、バイトコード命令ごとに割り込みハンドラが実行されます。

コードフットプリント

デバッガサポートにより、フットプリントが約15-20kB増加します(有効なデバッガ機能に依存)。 デバッガ機能が有効な場合

メモリフットプリント

ヒープレベルのデバッガー状態のため、 duk_heap 構造体のサイズが増加します。細かく調整されたメモリプールを使用している場合、メモリプールのサイズを再チューニングする必要があるかもしれません。

関数インスタンスは常に内部の _Varmap プロパティを保持し、ローカル変数が常に名前で検索できるようにします。 デバッガのサポートがない場合、 _Varmap は実行中に必要となる場合のみ保持されます (例: 関数に eval コールが含まれている場合など)。

そうでなければ、メモリフットプリントは無視できるほど小さくなるはずです。 Duktape はデバッグ・メッセージのバッファリングを維持する必要がありません。なぜなら、全てのデバッグ・データはストリームで入出力されるからです。

セキュリティ

デバッガープロトコル経由で利用可能なデバッグコマンドは、潜在的に悪用可能なメモリ安全でない動作をトリガーするために(誤って)使用される可能性があります。 たとえば、デバッグクライアントは、悪用される可能性のあるファブリケーションポインタから/への読み取り/書き込みを行う可能性があります。

これがセキュリティ上の懸念である場合、デバッグトランスポートは認証、暗号化、および完全性保護を提供する必要があります。 例えば、相互認証されたTLS接続を使用することができます。 Duktape自体は、トランスポートによって提供される以上のセキュリティ対策を提供しません。

デバッグAPI

duk_debugger_attach()

アプリケーションがDuktapeヒープにデバッガーを取り付けたいときに呼び出されます::

c
duk_debugger_attach(ctx,
                    my_trans_read_cb,         /* 読み取りコールバック */
                    my_trans_write_cb,        /* 書き込みコールバック */
                    my_trans_peek_cb,         /* ピークコールバック (オプション) */
                    my_trans_read_flush_cb,   /* 読み取りフラッシュコールバック (オプション) */
                    my_trans_write_flush_cb,  /* 書き込みフラッシュコールバック (オプション) */
                    my_request_cb,            /* アプリリクエストコールバック (オプション) */
                    my_detached_cb,           /* デバッガデタッチドコールバック */
                    my_udata);                /* デバッグudata */

呼び出されると、Duktapeはデバッグ・モードに入り、実行を一時停止し、デバッグ・クライアントからのさらなる指示を待ちます。 Duktapeのデバッガ・サポートが有効でない場合、エラーがスローされます。

トランスポートコールバックは、開始要求の一部として与えられる。 Duktape はデバッグの開始/停止サイクルごとに新しい仮想ストリームを期待し、 duk_debugger_attach() が呼ばれるたびにプロトコルバージョン識別子を送信します。

detached コールバックはデバッガが切り離されたときに呼び出されます。 これは、明示的な要求 (duk_debugger_detach()) やデバッグメッセージ/トランスポートエラー、Duktape ヒープの破壊によって起こります。

APIドキュメントに明示的に記載されていない限り、どのコールバックもDuktape APIを呼び出すことはできません(ほとんどの場合、 ctx 引数を取得しないのもこのためです)。そうすると、メモリが安全でない挙動を引き起こす可能性があります。 具体的な例として、もしユーザーの読み出しコールバックが読み出し中に Duktape API を呼び出すと、その API 呼び出しがガベージコレクションの引き金になる可能性があります。 ガベージコレクションは任意の副作用を持つ可能性があるため、実行中のデバッガコマンド (src-input/duk_debugger.c で実装) が非常に混乱した方法でブレークする可能性があります。

duk_debugger_detach()

アプリケーションがデバッガーをデタッチしたいときに呼び出されます::

c
duk_debugger_detach(ctx);

デバッガが切り離されると、Duktapeは通常の実行を再開します。 残っているデバッグ状態(ブレークポイントなど)は無視されます。

Duktapeのデバッガ・サポートが有効でない場合、エラーが投げられます。

duk_debugger_cooperate()

Duktapeへの呼び出しがアクティブでない場合に、受信デバッグ・コマンドを処理するためのオプションの呼び出しです。

c
duk_debugger_cooperate(ctx);

保留中のデバッグコマンドは ctx スレッドのコンテキスト内で実行されます。ブロックせずに実行できるすべてのデバッグコマンドは、呼び出しの間に実行されます。 この呼び出しはブロックしないので、イベントループの中に埋め込んでも安全です。 この呼び出しは、デバッグがサポートされていないときやアクティブでないときは無意味なので、デバッグの状態をチェックせずに呼び出すことができます。

注意点:

  • 呼び出し元は、Duktapeへの呼び出しがアクティブなときに、このAPI関数を呼び出さないようにする責任があります(どのようなコンテキストでも)。
  • duk_debugger_cooperate() の呼び出しの間隔は、保留中のデバッグ・コマンドに対する Duktape の反応速度に影響します。

このAPIコールは、Duktapeへの呼び出しがアクティブでないときにEvalなどのデバッグ・コマンドを実行できるようにするために、一部のアプリケーションで必要とされています。 例えば、以下のようになります。

c
for (;;) {
    /* イベントまたはタイムアウトを待ちます。 */
    wait_for_events_or_timeout();
    /* プロセスイベント。 */
    if (event1) {
        ...
    }
    /*...*/
    /* Duktapeデバッガと連携する。 */
    duk_debugger_cooperate(ctx);
}

このAPIコールは、保留中の受信メッセージをすべて処理するため(ブロックせずに利用可能)、次のように使用することも可能です。

c
for (;;) {
    /* イベントまたはタイムアウトを待ちます。 */
    wait_for_events_or_timeout();
    /* プロセスイベント。 */
    if (got_inbound_debugger_data) {
        /* Duktapeデバッガと協力:新しい受信データが到着するまで、保留中のメッセージをすべて処理する。 */
        duk_debugger_cooperate(ctx);
    }
    /*...*/
}

duk_debugger_pause()

ターゲットはいつでもこれを呼び出して、ECMAScript の実行を一時停止し、添付のデバッグクライアントに制御を移すよう要求することができます。

c
duk_debugger_pause(ctx);

要求された一時停止はすぐには起こらないかもしれませんが、次のバイトコードオペレーションディスパッチで実行されます。詳細はAPIドキュメントを参照してください。

このコールの一般的な使用例は、ホットキーにバインドすることで、ユーザーが無限ループから抜け出し、デバッグすることを可能にします。 しかし、他のDuktape APIコールと同様に、このコールはスレッドセーフではないので、デバッグ対象のECMAScriptコードを実行するために使用するのと同じスレッドから呼び出す必要があります。

duk_debugger_notify()

デバッグトランスポートを通じてアプリケーション固有の通知を送信するためのオプションのコール::

c
duk_bool_t sent;

duk_push_string(ctx, "BatteryLevel");
duk_push_uint(ctx, 130);  /* 130 of 1000 */
sent = duk_debugger_notify(ctx, 2 /*nvalues*/);
/* 'sent' は、notify が正常に送信されたか否かを示す。 */

この呼び出しは0を返し、デバッガサポートがコンパイルされていないときや、デバッガが接続されていないときは事実上無視されます。

詳細については、以下の「カスタムリクエストと通知」を参照してください。

デバッグトランスポート

概要

Duktapeのデバッガ・コードは、TCPコネクションやシリアル・リンクに似たセマンティクスを持つ、抽象化された信頼性の高いストリーム・トランスポートを介してデバッグ・メッセージを送受信します。 異なる環境への移植性を最大化するために、Duktapeはユーザーコードが duk_debugger_attach() に与えるコールバックという形で、このトランスポートの具体的な実装を提供することを期待しています。

トランスポートが提供する論理的なサービスは、以下のプリミティブを持つ信頼性の高いバイトストリームである。

  • バイト読み(部分読みOK,最低1バイト読みが必要ならブロック)
  • バイト書き込み(部分書き込みOK,最低1バイトの書き込みが必要な場合はブロックする)
  • ブロッキングせずに受信バイトをPeekする。
  • フラッシュヒントを読む
  • 書き込みフラッシュヒント

トランスポート・コールバックの実装をできるだけ簡単にするために、部分的な 読み書きを許可しています。 Duktapeは、必要な回数だけreadとwriteを呼び出すことで、「完全に読む」「完全に書く」セマンティクスを自動的に処理します。

Peekingは、Duktapeがブロックすることなく、受信したデバッグ・メッセージを検出することを可能にします。これにより、Duktapeが(一時停止状態ではなく)通常通り動作している場合でも、デバッグ・メッセージを処理することができます。

書き込みフラッシュは、トランスポート実装が書き込みを確実に合体させることを可能にする。リードフラッシュは、トランスポート実装が受信ウィンドウをより効率的に管理 することを可能にする。 読み取り/書き込みフラッシュコールバックは、いくつかのタイプのトランスポートに おいてのみ必要とされる。

このセクションでは、各コールバックの詳細なセマンティクスをカバーし、フロー制御、圧縮、セキュリティなどの他のトランスポート関連の一般的な問題について議論しています。

重要:アプリケーションは read/write チャンクバウンダリに何の意味も持たせるべきではありません。 リード、ライト、ピーキング、フラッシュコールがデバッグメッセージの境界に対応する保証はありません。

コールバックのセマンティクスを読み取る

  • 読み取り長は≧1が保証される。
  • バッファポインタは非NULLであることが保証される。
  • Duktapeは、少なくとも1バイト、最大でも「長さ」バイトの読み取りを要求しています。 部分的な読み出しはOKだが、少なくとも1バイトは読み出さなければならない。 ユーザーコードが少なくとも1バイトを読み取れない場合、読み取れるまでブロックしなければならない(MUST)。 1バイト以上が利用可能な場合、ユーザーコードはブロックしてはならない(MUST NOT)。
  • 1,lengthの範囲の戻り値は、与えられたバッファにいくつのバイトが読み込まれたかを示す。
  • 戻り値0は、ストリーム・エラー(サニティ・タイムアウト、コネクション・クローズなど)を示します。 Duktapeはそのストリームを壊れたとみなし、それ以上操作を行いません。 デバッガは自動的に切り離されます。

コールバックのセマンティクスを書き込む

  • 書き込み長は≧1であることが保証される。
  • バッファポインタは非NULLであることが保証される。
  • Duktapeは、最低でも1バイト、最大でも「length」バイトの書き込みを要求しています。 部分的な書き込みはOKだが、少なくとも1バイトは書き込まなければならない。もしユーザー・コードが少なくとも1バイトを書き込めない場合は、書き込めるようになるまでブロックしなければならない(MUST)。
  • 1,lengthの範囲の戻り値は、与えられたバッファから何バイトが書き込まれたかを示します。
  • 戻り値0は、ストリーム・エラー(サニティ・タイムアウト、コネクション・クローズなど)を示します。 Duktapeはそのストリームを壊れたとみなし、それ以上操作を行いません。 デバッガは自動的に切り離されます。

Peekコールバックのセマンティクス

  • peekコールバックの実装はオプションです(NULLは duk_debugger_attach() で渡すことができます)が、強く推奨します。 コールバックが提供されない場合、(Duktapeが正常に動作している間に)「突然」実行を一時停止するようないくつかの機能は動作しなくなります。
  • Peekコールバックには引数がありません。
  • Duktapeは入力ストリームを覗くことを要求しています。つまり、少なくとも1バイトがブロックされずに読み込めるかどうかを確認するためです。
  • 戻り値0は、ブロックせずに読み取ることができるバイトがないことを示す。
  • 戻り値 > 0 は、ブロッキングせずに読み込めるバイト数を示します。 現在、Duktapeは少なくとも1バイトが利用可能かどうかだけを気にしているので、 0か1を返せば十分です。
  • Duktapeは現在、少なくとも1バイトが利用可能であれば、デバッグメッセージ全体を読み取ることができると仮定しています(必要に応じてブロックし、部分的な読み取りを処理します)。

リードフラッシュコールバックのセマンティクス

  • リードフラッシュコールバックの実装はオプションです (duk_debugger_attach() で NULL を渡すことができます)。
  • リードフラッシュコールバックは引数を持たない。
  • Duktapeはユーザー・コードに対して「リード・フラッシュ」を指示しています。 Duktapeは、その場ではもう読み込みをしないかもしれないのに、"read flush "を指示することが保証されています。 (ただし、Duktapeはその直後から読み込みを続けていても、読み込みのフラッシュを示すことがあります)。
  • ほとんどのトランスポートでは、リードフラッシュは重要ではない。 トランスポートプロトコルが制限された読み取りウィンドウを使用し、リ モートピアにウィンドウの状態を更新するプロトコルを持つ場合、ウィンドウ 制御メッセージは次の読み取りフラッシュに延期できる(読み取りバッファが空の状態 など、それを送信する他の緊急の理由がない場合)。

書き込みフラッシュコールバックセマンティクス

  • 書き込みフラッシュコールバックの実装はオプションです (duk_debugger_attach() で NULL を渡すことができます)。
  • 書き込みフラッシュコールバックは引数を持ちません。
  • Duktapeは、ユーザーコードに対して「書き込みフラッシュ」を示しています。 Duktapeは、その特定の機会にこれ以上書き込みをしないかもしれないとき、 「書き込みフラッシュ」を示すことが保証されています。 (ただし、Duktapeは書き込みの直後でも書き込みフラッシュを指示することが あります)。
  • この表示は、ユーザー・トランスポートが書き込みをより大きなチャンクにまと める場合に有用です。 ユーザーコードは、バッファリングされたデータが十分に大きくなるか、書き込みフラッシ ュが指示されると、チャンクを送り出すことができます。 ユーザーコードは、重要なときにライトフラッシュが起こることを信頼することができる。
  • ユーザーコードは、この表示が基礎となるトランスポートに適用されない場合(例えば、TCPを使用する場合、書き込みの自動合体のためのメカニズムがすでにあります)、または保留中のバイトが最終的に送信されることを確実にするための他のメカニズム(例えば、タイマー)がある場合、無視する自由もあります。

トランスポートが壊れたことを示すマーク

Duktapeは、次の場合に輸送が壊れたとマークします。

  • ユーザー・コールバックがストリーム・エラーを示した場合
  • Duktapeがデバッグ・ストリームをパースする際に、パース・エラーに遭遇した場合。

デバッグ・トランスポートが壊れたとマークされたとき。

  • デバッガは自動的に切り離され、通常のECMAScriptの実行が直ちに再開されます。 デタッチド コールバックが存在する場合は、それが呼び出されます。
  • Duktapeは、ストリームのユーザー・コールバックに対して、これ以上呼び出しを行いません。
  • Duktape内部のデバッグ用読み込みコールは、ダミー値(バイト読み込み時は0、整数読み込み時は0、文字列読み込み時は空文字列など)を返し、書き込みは黙って無視されます。 これにより、実装は読み書きのたびにエラーをチェックすることなくデータの読み書きができます。「壊れたトランスポート」に対する明示的なチェックは、最も便利な場所で行うことができます。

ピーキングリクエストノート

Duktapeは、入ってくるデバッグ・コマンドを検出し、それを処理するためにピー ク・リクエストを使用します。ピー クは、通常の実行時 (関連するブレークポイントがなく、ステッピングがアクティブでない時) と チェック実行時 (1つ以上のアクティブなブレークポイントがあり、ステッピングがアクティブな時) の両方に使用されます。

Duktapeは、日付ベースのタイムスタンプを使用して、ピーク要求のレートを自動的に制限しています。

フラッシュ・ノートの作成

Duktapeは、書き込みフラッシュを使用して、この機会にこれ以上データを送信しない可能性があること、および、アプリケーションがキューに入れた保留データを送信する必要があることを示します。

Duktapeは、送信するデバッグ・メッセージを非常に小さく分割して書き込むので、アプリケーションが送信待ちデータのバッファを維持するのは理にかなっているかもしれません。Duktapeが書き込みを行う際、データをバッファに追加することができます。 データは、バッファが十分に大きくなった時か、Duktapeが書き込みフラッシュを実行した時に送出されます。

書き込みフラッシュは、Duktapeがメッセージのセットを処理し終わった時に発生するこ とが保証されており、アプリケーションは、保留中の書き込みをフラッシュするための タイマー・メカニズムなどを別に持つ必要がありません。 書き込みフラッシュは、送信されるデバッグ・メッセージの後では保証され ません(現在のDuktapeの実装はそのように動作しますが)。

ユーザー・コードは、Duktapeがいつ書き込みフラッシュを示すかについて、それが起こったときに保留中のバイトを送信すること以外、何も仮定してはいけません

信頼性

Duktapeは、トランスポートが信頼できるものであることを期待します。すなわち、 バイトの並び替えや紛失、重複がないことです。 具体的なトランスポートは、アプリケーション固有の手段で信頼性を提供しな ければならない。 例えば、TCP ソケットが使われる場合、信頼性は TCP によって自動的に提供されます。 信頼性のないパケットトランスポートの場合、ユーザコードは再送、重複検出、順序付けを提供しなければならない。

フロー制御

抽象的なトランスポートレベルではフロー制御はないが、アプリケーションはト ランスポートの一部としてフロー制御を自由に実装することができる。 例えば、TCPソケットが使用される場合、TCPの一部として自動的なフロー制御が存在する。

フロー制御は、バッファの過剰な確保を避けるために、メモリ量の非常に少ないデバイスでは必要かもしれない。

圧縮

非常に低速なリンクでは、アプリケーション固有のトランスポートがデバッグトラフィックのストリーム圧縮を使用することが適切である場合があります。 圧縮は、ストリームを非圧縮サイズの10〜30%程度に減らすことができる。

セキュリティ

環境によっては、デバッグトランスポートはセキュリティ上重要である場合があります。 そのような場合、アプリケーションはデバッグトランスポートに認証と暗号化(例:トランスポートにSSL/TLSを使用)を使用する必要があります。

パケットベースのトランスポートの上に実装する

このトピックは別のセクションで扱います。

開発時間輸送拷問オプション

DUK_USE_DEBUGGER_TRANSPORT_TORTURE という設定オプションを使うと、Duktape はすべてのデバッグトランスポートの読み込み/書き込み操作を 1 バイト単位で行うようになり、あるサイズのチャンクの読み込み/書き込みに関する不正な仮定をキャッチするのに役立ちます。

デバッグストリームフォーマット

概要

デバッグ・プロトコルは、Duktape内部とデバッグ・クライアントの間で交わされる会話です。 ユーザーコードはデバッグプロトコルの内容を意識することはなく、デバッグターゲットとデバッグクライアントの間でストリームのチャンクを運ぶためのデバッグトランスポートを提供するだけです。

デバッグプロトコルはシンプルな3つのライフサイクルを持っています。

  • ストリームが接続され、バージョン識別(Duktapeによって送信される)を待機しています。
  • ストリームが接続され、アクティブに使用されている状態。 デバッグ・メッセージは、それぞれの方向で自由に交換されます。
  • ストリームは切断されます。 これは明示的なデタッチ要求(つまり duk_debugger_detach() への呼び出し、ユーザーのトランスポートコールバックによって示される読み込み/書き込みエラー、Duktapeによって検出されるメッセージ構文エラー、またはDuktapeヒープ破棄によって起こります。

プロトコルはリクエストパイプラインを使用する。つまり、各ピアは前のリ クエストに対する応答を待つことなく、複数のリクエストを送ることが許される。 これを促進するために、すべてのリクエストは対応する応答/エラーメッセージを持ち、リクエストは常に再順序付けされることなく処理される。 どちらのピアもパイプライン化されたリクエストを送る必要はなく、例えばデバッグクライアントが別のリクエストを送る前に応答を待つことは全く問題ありません。

バージョン識別

デバッグ・トランスポートが装着されているとき、Duktapeはバージョン識別をUTF-8でエンコードされた:という形の行として書き込みます。

xml
<protocolversion> <SP (0x20)> <additional text, no LF> <LF (0x0a)>

現在のプロトコルのバージョンは「2」であり、識別行は現在、次のような形になっています。

xml
2 <DUK_VERSION> <DUK_GIT_DESCRIBE> <target string> <LF>

プロトコルバージョン番号の後に続くものはすべて情報提供のみです。 例::

xml
2 20000 v2.0.0 duk command built from Duktape repo

デバッグプロトコルのバージョンは、ユーザーコードへの定義として利用可能です (duktape.h で定義されます)::

xml
DUK_DEBUG_PROTOCOL_VERSION

これは、ターゲットがそのデバッグ機能を宣伝できる場合などに有用である。

デバッグクライアントはその行を解析し、最初にプロトコルバージョンをチェックする必要があります。プロトコルバージョンがサポートされていない場合、デバッグ接続は閉じられるべきです。 デバッグクライアントは、常にターゲットに存在するプロトコルバージョンに適応します。 バージョン識別に対する確認応答はなく、デバッグクライアントからの対応するハンドシェイクメッセージもありません。

バージョン識別(ハンドシェイク)が完了すると、デバッグストリームは以下に説明する異なるフレーミングに切り替わります。 このフレーミングはプロトコルのバージョンに依存する可能性があり、そのためバージョン識別が最初に処理されなければなりません。

バージョン識別形式に関するいくつかの根拠。

  • 1行のテキスト文字列は一般的なハンドシェイク手法であり、(TCPトランスポートを使用している場合)ターゲットにtelnet接続でき、デバッガーポートに接続したことを容易に確認できるという利点があります。 また、例えばDuktapeがオプションの機能をアドバタイズできるように、簡単に拡張することができます(それが必要になった場合)。
  • バージョン識別は、ハンドシェイク形式を変更することなく、将来的にプロトコルのフレームを変更することを可能にします。 もしバージョン識別が以下に述べるような複雑なフレームを使用するならば、 バージョンの互換性をより難しくするでしょう。
  • Duktapeは、ただやみくもにバージョン識別を送信し、応答を解析する必要がないので、例えば1バイトのバージョンを送信することと比較して、人間が読めるバージョン識別行を持つことにほとんどコストはかかりません。
  • デバッグ・クライアントのためにバージョン識別を追加することは、Duktapeにとって不必要なパース状態を意味します。 Duktapeにデバッグ・クライアントのバージョンを認識させるメリットはほとんどない。

Dvalue

バージョン識別ハンドシェイクの後、デバッグストリームはそれぞれの方向に送られる dvalues と呼ばれる型付き値で構成される。 Dvalues はメッセージフレームマーカー、整数、文字列、タグ付き ECMAScript 値などを表現する。 これらはコンテキストなしで解析することができ、ダンプに便利であり、またコンテキストなしで dvalues (とデバッグメッセージ) をスキップすることができる。 デバッグメッセージは、開始マーカ、0個以上のd値、メッセージ終了マーカからなる一連のd値として構築される。

次の表は、dvalueとそのフォーマットについてまとめたものである。 初期バイト(IB)は、タイプタグとして、また、値の一部を含むものとして使用される場合がある。

Byte sequenceTypeDescription
0x00EOMメッセージの終わり
0x01REQリクエストメッセージの開始
0x02REP成功返信メッセージの開始
0x03ERRエラー返信メッセージの開始
0x04NFY通知メッセージの開始
0x05...0x0freserved
0x10 <int32>integer4 バイト整数,符号付き 32 ビット整数,ネットワーク順で先頭バイトに続く。
0x11 <uint32> <data>string4 バイト文字列、符号なし 32 ビット文字列長でネットワーク順、文字列データは先頭バイトに続く。
0x12 <uint16> <data>string2 バイト文字列、符号なし 16 ビット文字列長、ネットワーク順、文字列データは先頭バイトに続く。
0x13 <uint32> <data>buffer4 バイトバッファ、符号なし 32 ビットバッファ長、ネットワーク順、バッファデータは先頭バイトに続く。
0x14 <uint16> <data>buffer2 バイトバッファ、符号なし 16 ビットバッファ長、ネットワーク順、バッファデータは先頭バイトに続く
0x15unused内部的にはマッピングされていない配列エントリをマークするために使用され、デバッガプロトコルでは "none "の結果を示すために使用されます。
0x16undefinedECMAScript "undefined"
0x17nullECMAScript "null"
0x18trueECMAScript "true"
0x19falseECMAScript "false"
0x1a <8 bytes>numberIEEEダブル(ネットワークエンディアン)
0x1b <uint8> <uint8> <data>objectクラス番号、ポインタ長、ポインタデータ(ネットワークエンディアン)
0x1c <uint8> <data>pointerポインタ長、ポインタデータ(ネットワークエンディアン)
0x1d <uint16> <uint8> <data>lightfuncLightfuncフラグ、ポインタ長、ポインタデータ(ネットワークエンディアン)
0x1e <uint8> <data>heapptrポインタの長さ、ポインタデータ(ネットワークエンディアン);ヒープオブジェクトへのポインタ、DumpHeapで使用される
0x1freserved
0x20...0x5freserved
0x60...0x7f <data>string長さ [0,31] の文字列、文字列の長さは IB - 0x60, データは以下の通り。
0x80...0xbfinteger整数 [0,63]、整数値はIB - 0x80
0xc0...0xff <uint8>integer整数 [0,16383], 整数値は ((IB - 0xc0) << 8) + followup_byte です。

すべての "integer "表現は意味的に同じであり,整数が期待されるところではすべて使用することができる。 文字列 "と "バッファ "表現も同様である。

dvalue の型付けは duk_tval の値を表現するのに十分であり、型付けを保持することができます (例えば、文字列とバッファは別々の型を持っています)。

dvalueは以下のテキストで以下のように表現される(テキスト中のすべての型に必要なわけではない)::

xml
EOM
REQ
REP
ERR
NFY
<int: field name>      e.g. <int: error code>
<str: field name>      e.g. <str: error message>
<buf: field name>      e.g. <buf: buffer data>
<ptr: field name>      e.g. <ptr: prototype pointer>
<tval: field name>     e.g. <tval: eval result>
<obj: field name>      e.g. <obj: target>
<heapptr: field name>  e.g. <heapptr: target>

これらの追加的な表記は次の通りである。

xml
# 1つの整数または2つの文字列のような代替。
(<int: foo> | <str: bar> <str: quux>)

# 繰り返し、例えば0-N個の整数。
[<int: foo>]*

# 繰り返し、例えば1-N個の値、各文字列または整数。
[<str: foo> | <int: bar>]+

フィールドが ECMAScript の値と正確に関連しない場合、例えばフィールドがデバッガ制御フィールドである場合、型付けは緩くなることがある。 例えば、boolean フィールドは integer dvalue として、任意のバイナリ文字列は string dvalue として表現されることがあります。 各コマンドで使用される具体的な型は、以下のコマンドごとのセクションで説明します。

dvalue形式の背後にある意図は、次のとおりです。

  • 最下層のプロトコルを型付けし、解析される特定のメッセージを知ることなく、dvalueとメッセージをダンプできるようにする。
  • EOMマーカーをスキャンすることで、メッセージの内容を理解せずに メッセージをスキップする、あるいはメッセージの末尾のフィールドを 無視する方法を提供する。これは、サポートされていないリクエストを処理したり、既存のものに dvalue を追加してメッセージを拡張したりするのに便利である。 しかし、信頼性のあるスキップは、実装がすべてのd値の型を解析し、その長さを知ることができる場合にのみ可能であることに注意してください。 特に、(EOMに使用される)ゼロバイトはdvalueの内部にも現れることが あるので、ゼロバイトにスキップすることは信頼できるスキップの方法 ではない。
  • これは、両方のピアが、それ自身のリクエストに対するリプライと、相手から 開始されたリクエストまたは通知とを確実に区別できるようにするために必要であ る。
  • 最終メッセージの長さを事前に知ることなく、デバッグメッセージのストリーム書き込みを許可する(これは、たとえばフレーミングが先行メッセージ長フィールドを持つ場合に必要である)。 これは、メッセージのサイズを事前に計算したり、送信前に完全なメッセージを 作成するために蓄積バッファを使用する必要性を回避するのに便利です。
  • すべての duk_tval 値を情報を失うことなく表現します。
  • 低帯域幅のデバッグ用トランスポート(シリアル回線など)のトラフィックを最小化するために、典型的な数値や文字列には短いエンコーディングフォームを使用します。
    • 整数の範囲 [0,63] は1バイトにエンコードされ、コマンド番号、ステータスコード、ブール値などに有効です。
    • 整数範囲 [0,16383] は2バイトにエンコードされ、例えば行番号、典型的な配列インデックス、ループカウンタ値などに有効です。
    • 長さ [0,31] の短い文字列は、1 バイトと文字列データにエンコードされます。 これは、典型的なファイル名、プロパティ名、変数名などに有効です。

注意事項

  • duk_tval を送信しない場合、整数の値は常にプレーンな整数としてエンコードされなければなりません (IEEE double エンコーディングではありません)。
  • duk_tval の値をパースする際には、プレーンな整数値と IEEE double 値の両方を受け入れなければなりません。 プレーンな整数は IEEE doubles に一意に対応するので、情報の損失はありません。 負の 0 は,符号を保持するために IEEE double として表現しなければならないことに注意してください.
  • 高速整数(fastint)はデバッガープロトコルにおいて通常の数値と区別されません。
  • バッファの値は明示的に表現されますが、バッファオブジェクト(Node.js Buffer, ArrayBuffer, DataView, TypedArray ビュー)はオブジェクトとして表現されます。 つまり、その内容は送信されず、ヒープポインタとクラス番号のみが送信されます。
  • 未使用」値は特別です。Duktapeが内部的にマッピングされていない配列のエントリーをマークするために使用されますが、実際の値(値スタック上のエントリー、プロパティ値など)に使用されることは意図されていません。 unused" 値は、デバッガ・プロトコルで、いくつかのコマンド・リプライの欠落/無値を示すために使用されます。 リクエストでは使用されないので、デバッグ・クライアントはリクエスト(PutVarなど)で「unused」d値を送信してはいけません。

エンディアン

原則として、すべての値はネットワークオーダー(ビッグエンディアン)にシリアライズされます。これはポインタ値やIEEEダブル値にも適用されます。

ポインタやIEEEダブルがバッファデータの一部である場合、それらはメモリ上に存在する任意の順序でエンコードされます。 これは、例えばDumpHeapによってダンプされたバイトコードは、プラットフォーム固有のバイトオーダーでバッファーの値として表現されることを意味します。 バイトオーダーを変更すると、デバッガーコードが特定のバッファ値のメモリレイアウトを認識する必要があるため、非常に厄介なことになります。

duk_tvalの値を表現する

duk_tval の値には、以下の dvalue タイプが使用される。

  • 未使用(undefined/unused/none):特定の dvalue。
  • 未定義:特定の dvalue
  • null: 固有の dvalue
  • boolean: truefalse に対応する特定の dvalue です。
  • 数値:符号付き 32 ビット整数は,単純な整数の d 値で表現できる(負の 0 を除く),その他の数値はリテラルの IEEE 倍数として表現される.
  • 文字列: いくつかの文字列長に対応する特定の d 値
  • buffer: 数少ないバッファ長に対する特定のd値
  • オブジェクト: ポインタとして表現される (デバッグクライアントからデバッグターゲットに送信される場合は危険)
  • ポインター: ポインターとして表現
  • lightfunc: ポイントとフラグフィールドで表現される(デバッグクライアントからデバッグター ゲットに送信される場合は危険)。

<tval: フィールド名> という記法では、 duk_tval と互換性のある任意の dval を使用することができます。 しかし、いくつかの値はデバッグクライアントからターゲットに送信する際に危険であることに注意してください。例えば、PutVar への引数として lightfunc 値を送信することは可能ですが、非常に注意しなければ簡単にセグメンテーションフォルトを起こすことができます。

Request, replies, and notifications

リクエストは次のような形式です。

xml
REQ <int: command> <0-N dvalues> EOM

成功応答は次のような形式です。

xml
REP <0-N dvalues> EOM

エラー応答は、コマンドに依存しない 固定フォーマット である。

xml
ERR <int: error code> <str: error message or empty string> EOM

通知には、次のような形式があります。

xml
NFY <int: command> <0-N dvalues> EOM

注意事項

  • リクエストとリプライはメッセージIDを持たない: それは必要ない。 各ピアは、送られてくるリクエストに順番に応答することが要求される。また、 すべてのリクエストは、一つの成功またはエラー応答を持つことが要求されるので、 応答を以前に送ったリクエストに確実に関連付けることができる。 返信メッセージは、他の方向でピアから送られたリクエストや通知とまだ混 在しているかもしれない、ということに注意すること。
  • 返信メッセージは、デバッグストリームでの順序に基づいて暗黙のうちに リクエスト/通知と関連付けられる。
  • エラー応答は、エラー処理を統一するために決まった形式を持ちます。デバッグクライアントが新しいコマンドがサポートされているかどうかを簡単に チェックし、サポートされていなければ他のコマンドにフォールバックできるように、 「unsupported command」に対する特定のエラーコードを用意しています。
  • Duktapeは現在、通知のみを送信し、リクエストは送信しません。

Error codes

CodeDescription
0x00不明または未指定のエラー
0x01非対応のコマンド
0x02多すぎる(例:ブレークポイントが多すぎる、新規に追加できないなど)
0x03見つかりません(例:無効なブレークポイントインデックス)
0x04アプリケーションエラー(例:AppRequest関連エラー)

インバウンドリクエストの対応

どちらかのピアが何か予期せぬことが起こったと判断した場合、単にトランスポートをドロップすることができます。 Duktapeがこれを検出するとすぐに、デバッガは自動的に切り離され、通常の実行が再開されます。 これは予期せぬエラーに対する統一されたハンドリングを提供し、例えば以下のような場合に適切な動作となります。

  • 無効な、あるいは非常識なdvalueフォーマットに遭遇した場合。 このような場合、確実に実行を継続する方法がないことがよくあります。
  • サポートされているコマンドが処理されているときにパースエラーが発生した。 このような状況は、ピアにバグがあるか、一貫性のない状態であることを示しています。

正確なエラー処理規則は、ここではあまり詳細に規定しませんが、重要な規則がいくつかあります。 拡張性のために重要なルールがいくつかある。

  • ピアは、サポートされていないコマンド番号のリクエストを受信した場 合、そのコマンドがサポートされていることを示すエラー応答を返さなければ ならず[MUST NOT]、デバッグ接続を切断してはならない[MUST NOT]。 この動作は、ピアがコマンドを試してたまたまサポートされてい るかどうかを確認し、サポートされていない場合は他の動作にフォールバッ クできるようにするために、重要である。 その結果、常にプロトコルのバージョンを厳密に上げることなく新しいコマンドを追加することができ、オプションやカスタム、ターゲット固有のコマンドを追加して、それらを「調査」することが可能になります。
    • 今のところ、これはDuktapeにのみ適用されます。Duktapeはリクエストを送信せず、通知のみを送信します。 Duktapeはリクエストを送ることはなく、通知だけを送ります。 * サポートされていないコマンド番号の通知を相手が受け取った場合、その通知を無視 しなければならず、デバッグ接続を切断してはいけません(MUST NOT)。その理由はリクエストと同じである。
  • サポートされているコマンドがパースされ、EOMの前に追加のdvalueが ある場合、最後のdvalueは無視されなければならない[MUST]。 これは、新しいコマンド番号を割り当てたり、プロトコルのバージョンを上げ たりすることなく、既存のコマンドを(場合によっては)拡張することができる ようにするものである。

これらの単純なルールは実装が簡単で、いくつかの一般的なケースでプロトコルを優雅に拡張することができます(もちろんすべてではありませんが)。

d値およびデバッグメッセージのテキスト表示

これは、このドキュメントとduk_debug.jsのダンプで使用される情報提供の慣習です

Duktapeデバッグクライアントは、以下の規約を使用して、dvalueをテキストで表現します。 Duktapeデバッグ・クライアントは、dvaluesをテキストとして表現するために、以下の規約を使用します。

  • マーカバイト: EOM, REQ, REP, ERR, NFY.
  • 整数: -123 などのように、文字列を正規化したもの。
  • 文字列は、バイト列 (0x00...0xff) からコードポイント U+0000...U+00FF に1対1でマッピングされ、JSON にエンコードされる。 JSONエンコーディングは、結果にエスケープされていない改行がないことを保証します。 標準的なJSONは、コードポイントU+0080...U+00FFをすべてエスケープしないので、残念ながらおかしなことになります(ASCIIのみのシリアライズが望ましいでしょう)。
  • その他の型は、JSONマッピングのようにJSONエンコードされています、以下を参照してください。

デバッグメッセージは、関連するすべてのd値(メッセージタイプマーカーとEOMを含む)を空白で区切った1行で表現されるだけです。これにより、テキストダンプは読みやすく、カットアンドペーストや診断などが容易になります。

例として、ペイロードが文字列「touché」、整数「123」、整数「-321」からなる応答を考えてみよう。 この文字列は、Duktape内部でUTF-8シーケンス:.Touchéとして表現されます。

xml
74 6f 75 63 68 c3 a9

返信メッセージの生のバイトは、(パイプで区切られたdvaluesで)次のようになります:。

xml
02 | 67 74 6f 75 63 68 c3 a9 | c0 7b | 10 ff ff fe bf | 00

これは、テキストのワンライナーとしてレンダリングされます:

xml
REP "touch\u00c3\u00a9" 123 -321 EOM

Duktape内部で文字列が使用する正確なバイト数を維持するために、奇数文字列のマッピングが選択されます。 Duktapeの文字列の中には、意図的に無効なUTF-8を使用しているものがあるので、Unicodeにマッピングすることが常に選択肢になるわけではないことに注意してください。 この文字列マッピングは、バッファ・データを表現するためにも使用されます。

デバッグ・プロトコルのためのJSONマッピング

このセクションで説明するマッピングは、デバッグのd値やメッセージをJSONの値にマッピングするために使用されます。 このマッピングは JSON デバッグプロキシーの実装に使用され、デバッグクライアントはバイナリプロトコルを全く実装せずに、クリーンな JSON メッセージだけを使用してデバッグターゲットと対話できるようになります。

dvalues の JSON 表現

  • Unused::
json
{ "type": "unused" }
  • Undefined::
json
{ "type": "undefined" }
  • Null、true、falseはJSONに直接マッピングされます。
json
null
true
false
  • 整数は、JSONの数値型に直接マッピングされます:。
json
1234
  • JSONの数値として損失なく表現できない数値(無限大、NaN、負のゼロなど)は、次のように表現されます。
json
// data contains IEEE double in big endian hex encoded bytes
// (here Math.PI)
{ "type": "number", "data": "400921fb54442d18" }

このオブジェクトには、オプションで value フィールドを含めることができ、これは JSON 互換の近似値として数値を提供します。 このフィールドは、JSON互換の近似値として数値を提供します。生のIEEE doubleと比較して、いくつかの精度が失われる可能性があります。 また、NaNや無限大の場合は null となり、コードを書く際に JSON.stringify() で値をエンコードすることができるようになります。 例::

json
// 4.5
{ "type": "number", "data": "4012000000000000", "value": 4.5 }

// +Infinity
{ "type": "number", "data": "7ff0000000000000", "value": null }

重要: value のキーは機械的に処理されてはならず、JSON プロトコルのテキストを直接読みやすくするためにのみ存在する。 パースするコードは常にこれを無視し、代わりに data を使用しなければならない。

  • 文字列はテキスト表現と同様にマッピングされます。すなわち、バイト 0x00...0xff は Unicode コードポイント U+0000...U+00FF: にマッピングされます。
json
// 4 バイトの文字列 0xde 0xad 0xbe 0xef
"\u00de\00ad\00be\00ef"

この表現は、バイト単位で正確であり、UTF-8以外の文字列を正しく表現し、かつほとんどの実用的な(ASCII)文字列に対して人間が読みやすいという理由で使用されています。

  • バッファデータは、16進エンコードされた形でオブジェクトに包まれて表現されます:。
json
{ "type": "buffer", "data": "deadbeef" }
  • メッセージフレーミングの d 値 (EOM, REQ, REP, NFY, ERR) は、JSON プロトコルでは見えません。 これらは duk_debug.js が内部で次のようなフォーマットで使用しています:
json
{ "type": "eom" }
{ "type": "req" }
{ "type": "rep" }
{ "type": "err" }
{ "type": "nfy" }
  • Object:
json
// classは数値,ポインタは16進数である.
{ "type": "object", "class": 10, "pointer": "deadbeef" }
  • Pointer:
json
// ポインタは16進数である
{ "type": "pointer", "pointer": "deadbeef" }
  • Lightfunc:
json
// flagsはJSONの数値として表現される16ビット整数、ポインタは16進数で表現されます。
{ "type": "lightfunc", "flags": 1234, "pointer": "deadbeef" }
  • Heap pointer:
json
// ポインタは16進数である
{ "type": "heapptr", "pointer": "deadbeef" }

デバッグメッセージのJSON表現

メッセージは、以下のようにメッセージタイプマーカーとEOMマーカーを削除したJSONオブジェクトとして表現される。

request メッセージは、'request' キーと dvalue のリスト (EOM は省略) を含む 'args' 配列を用いてコマンドを指定します:

json
{
    "request": "AddBreak",
    "args": [ "foo.js", 123 ]
}

args' 引数はオプションである。これがない場合は、空の配列と同じように扱われる:

json
{
    "request": "AddBreak"
}

通常、デバッグコマンドは文字列として指定され、プロキシはデバッガのメタデータを使用して文字列をコマンド番号に自動的にマッピングします。 コマンド番号は明示的に指定することができ、さらに次のように両方指定することもできます。

json
// 明示的なコマンド番号(例:メタデータがカスタムコマンドを知らない)。
{
    "request": 24,
    "args": [ "foo.js", 123 ]
}

// 同上、これは以前から使われていた形式です(上記の形式が望ましい)。
{
    "request": true,
    "command": 24,
    "args": [ "foo.js", 123 ]
}

// 'request' にはコマンド名を指定し、'command' には予備のコマンドを数値で指定することも可能です。  コマンド名がコマンドメタデータで解決できない場合は、'command' に指定したコマンド番号が使用されます (その場合のみ)。
{
    "request": "AddBreak",
    "command": 24,
    "args": [ "foo.js", 123 ]
}

返信メッセージはコマンド番号を持たないので、メッセージの種類を区別できるように、 「reply」キーに「true」をセットしておく。 引数は再び'args'(EOMは省略)になります。

json
{
    "reply": true,
    "args": [ 3 ]
}

エラーメッセージは返信のようなもので、'error' キーには "true" 値が、'args' にはエラー引数(EOM は省略)が含まれます。

json
{
    "error": true,
    "args": [ 2, "no space for breakpoint" ]
}

通知メッセージはリクエストと同じ形式ですが、「request」キーが「notify」に置き換えられています。

json
{
    "notify": "Status",
    "args": [ 0, "foo.js", "frob", 123, 808 ]
}

通知コマンド番号の指定には、notifiesの代替形式も用意されています。

json
{
    "notify": 1,
    "args": [ 0, "foo.js", "frob", 123, 808 ]
}

{
    "notify": true,
    "command": 1,
    "args": [ 0, "foo.js", "frob", 123, 808 ]
}

{
    "notify": "Status",
    "command": 1,
    "args": [ 0, "foo.js", "frob", 123, 808 ]
}

引数リストが空の場合、'args' はどのメッセージでも省略可能である。

要求メッセージと通知メッセージは、要求/通知コマンド名と番号の両方を含み、いくつかの形式がサポートされている。 コマンド名/番号は以下のように解決される。

  • request / notify にコマンド名が文字列で指定されている場合、コマンドのメタデータ からコマンドを検索します。 コマンド名がわかっている場合、コマンドメタデータのコマンド番号を使用します (「command」キーの可能性は無視します)。
  • request / notify でコマンド番号が指定されている場合は、その番号をそのまま使用します。
  • command' がコマンド番号を提供している場合、それをそのまま使用します。 requestnotifytrue 値で存在する場合もありますが、無視されます。
  • 上記の手順が失敗した場合、request/notifyは処理できません。

その他のJSONメッセージ

上記のコアメッセージフォーマットに加えて、デバッグプロトコルのバージョン情報やトランスポートイベントのためのいくつかのカスタムメッセージがあります。 これらは、アンダースコアで始まる特別なコマンド名とコマンド番号のない「通知」メッセージとして表現されます。 これらは主に人間の読みやすさを向上させるためのもので、細かい部分は必要に応じて変更される可能性があります。

ターゲットへの接続が試みられると、次のようなnotifyが送信されます

json
{
    "notify": "_TargetConnecting",
    "args": [ "1.2.3.4", 9091 ]
}

ターゲットに接続すると、バージョン識別がそのまま中継される。

json
{
    "notify": "_TargetConnected",
    "args": [ "1 10199 v1.1.0-173-gecd806e-dirty duk command built from Duktape repo" ]
}

ターゲットが切断されたとき。

json
{
    "notify": "_TargetDisconnected"
}

トランスポートエラーが発生した場合(ターミナルエラーとは限らないので、複数回表示される可能性がある)。

json
{
    "notify": "_Error",
    "args": [ "some kind of error" ]
}

JSONプロキシ接続が切断される寸前の場合:。

json
{
    "notify": "_Disconnecting"
}

オプションで reason 引数を含めることができる:。

json
{
    "notify": "_Disconnecting",
    "args": [ "Target disconnected" ]
}

JSONプロトコルのラインフォーマット

JSONメッセージは、コンパクトなワンライナー形式でエンコードされ、メッセージの最後に改行(LF文字1つ、0x0a)を入れて送信されます。(上記の例は、複数行のフォーマットで書かれていますが、これは not 許可されていないことに注意してください; これは単に分かりやすくするためです)。

この規約により、メッセージの読み書きが容易になります。 メッセージは簡単にカット・ペーストでき、メッセージ・ログを効果的にグ レープすることができます。

プロトコルの拡張とバージョンの互換性

バージョン識別行は、デバッグプロトコルに互換性のない変更を加えるために使用されるプロトコルのバージョン番号を提供します; デバッグクライアントは常にターゲットのデバッグプロトコルバージョンに準拠するものと想定されます。

また、以下の基本的な方法で、プロトコルのバージョン番号を変更することなくプロトコルを拡張することが可能です。

  • 新しいコマンドを追加する。 新しいコマンドを追加する。コマンドがサポートされていない場合、ピアは未知の/サポートされていないコマンドを示す特定のエラーを送り返す。

  • リクエスト、レスポンス、または通知に末尾のフィールドを追加する。 ピアはサポートするフィールドを読み込んで処理した後、未知の末尾フィールドをスキップしてEOMに進むことが要求される。 メッセージの中には、可変数のフィールド(例えば、変数名/値ペアのリスト)を持つものがあり、その場合、このアプローチは不可能かもしれない。

これらの拡張は、(1) メッセージの内容を理解せずに EOM にスキップする機能、および (2) 未知のメッセージと未知の末尾 d 値に対する処理要件によって実現されています。

一般的な設計ルールとして、Duktape内部はバージョン特有の処理や回避策を排除しておく必要があります。 もし、ある機能が互換性のある方法できれいに実装できない場合、コマンドの並列変種を追加したり、他の厄介な妥協をしたりする代わりに、プロトコルのバージョンを上げる必要があります。デバッガのコードを小さくきれいに保ち、ターゲットでのコードフットプリントが損なわれないようにすることが重要です。

Duktape が送るコマンド

状態通知(0x01)

フォーマット::

xml
NFY <int: 1> <int: state> <str: filename> <str: funcname> <int: linenumber> <int: pc> EOM

例::

xml
NFY 1 0 "foo.js" "frobValues" 101 679 EOM

何も実行していない時(例えば、Duktape の起動時以外から duk_debug_cooperate() が呼ばれた時) filename と funcname は未定義(d 値として "undefined" が使われる)、 pc/line は 0 になります。

状態は、以下のいずれかである。

  • 0x00: 実行中
  • 0x01: 一時停止中、デバッグクライアントを再開する必要があります。

実行状態が変化した場合(例:一時停止から実行中、またはその逆)、Duktapeは常にStatus通知を送信します。

デバッガを接続してDuktapeを実行している場合、どのファイル/行/関数が実行されているかをデバッグ・クライアントに知らせるために、随時ステータス通知を送信します。

ステータス更新のレートは、日付ベースのタイムスタンプを使用して自動的に制限されるため、Duktapeが通常モードまたはチェック・モードで動作している場合、ステータス更新は最大で200msごとに送信されます。

予約済み (0x02)

(Duktape 2.0.0で削除、Duktape 1.xで印刷通知)

予約済み (0x03)

(Duktape2.0.0で削除。Duktape1.xではアラート通知)

ログ通知(0x04)

(Duktape2.0.0で削除。Duktape1.xでログ通知)

throw通知(0x05)

Format::

xml
NFY <int: 5> <int: fatal> <str: msg> <str: filename> <int: linenumber> EOM

Example::

xml
NFY 5 1 "ReferenceError: identifier not defined" "pig.js" 812 EOM

Fatalは、その一つです。

  • 0x00: キャッチ
  • 0x01: 致命的(捕捉されない)

Duktapeは、ランタイム・エラーによってDuktapeによって、あるいはECMAScriptコードによって直接、エラーが投げられるたびにThrow通知を送信します。

msgはスローされる文字列強制の値です。 ファイル名と行番号は、スローされたオブジェクトがErrorインスタンス(拡張後)であれば直接取得され、そうでなければ、これらの値はバイトコード・エグゼキュータの状態から計算されます。

通知解除 (0x06)

フォーマット::

xml
NFY <int: 6> <int: reason> [<str: msg>] EOM

例:

xml
NFY 6 1 "d値のパースエラー" EOM

理由は以下のいずれかです。

  • 0x00: 正常なデタッチ
  • 0x01: ストリームエラーによるデタッチ

Duktapeは、デバッガがデタッチする際にDetaching通知を送信します。 クライアントがこの通知を見ることなくターゲットがトランスポートをドロップした場合、接続が失われたと仮定し、それに応じて対応することができます(例えば、リンクを再確立しようとする)。

msg`` はオプションの文字列で、切り離しの理由を詳しく説明します。 これは切り離しの性質によって、存在する場合としない場合があります。

AppNotify notification (0x07)

フォーマット:

xml
NFY <int: 0x07> [<tval>]* EOM

例:

xml
NFY 7 "DebugPrint" "Everything is going according to plan!" EOM

これはカスタム通知メッセージで、その意味とセマンティクスはアプリケーションに依存します。 アプリケーションに依存します。

AppNotifyメッセージは、Duktapeデバッグ・プロトコルを介したデバッグ・クライアントとデバッグ・ターゲット間の直接通信のために使用されます。 カスタム・メッセージの意味とそれに含まれるdvalueの両方は、完全に実装次第であり、アプリケーションのニーズによっては、まったくサポートされないこともあります。

詳細については、以下の「カスタム・リクエストと通知」を参照してください。

デバッグクライアントから送信されるコマンド

BasicInfo request (0x10)

Format:

xml
REQ <int: 0x10> EOM
REP <int: DUK_VERSION> <str: DUK_GIT_DESCRIBE> <str: target info>
    <int: endianness> <int: sizeof(void *)> EOM

Example:

xml
REQ 16 EOM
REP 10099 "v1.0.0-254-g2459e88" "Arduino Yun" 2 4 EOM

Endianness:

  • 1 = little endian
  • 2 = mixed endian (doubles in ARM "mixed" endian, integers little endian)
  • 3 = big endian

エンディアンは、少数のd値のデコードに影響する。

ターゲット情報は、コンパイル可能な文字列で、例えば、デバイスの種類を記述することができます。

Void ポインタサイズとは、ポインタに関連する値に対して使用されるポインタサイズを示す。ただし、関数ポインタは異なるサイズを持つことがある。

TriggerStatus request (0x11)

Format:

xml
REQ <int: 0x11> EOM
REP EOM

Example:

xml
REQ 17 EOM
REP EOM

その後、Duktapeはステータス通知を再送信します。

Pause request (0x12)

Format:

xml
REQ <int: 0x12> EOM
REP EOM

Example:

xml
REQ 18 EOM
REP EOM

Duktape がすでに一時停止している場合は、ノー・オペレーションとなります。Duktapeが実行中であれば、Duktapeは時々デバッグ・メッセージの着信をチェックします。Duktapeが一時停止要求に気付くと(数秒かかることもあります)、要求に応答して実行を一時停止し、一時停止したことを示すStatus通知を送ります。

Resume request (0x13)

Format:

xml
REQ <int: 0x13> EOM
REP EOM

Example:

xml
REQ 19 EOM
REP EOM

Duktapeがすでに実行されている場合は、ノー・オペレーションです。Duktapeが一時停止している場合、一時停止状態に関連するデバッグ・メッセージ・ループ(制御は完全にデバッグ・クライアントの手中にある)を終了して実行を再開し、実行中であることを示すステータス通知を送信します。

StepInto request (0x14)

Format:

xml
REQ <int: 0x14> EOM
REP EOM

Example:

xml
REQ 20 EOM
REP EOM

実行が現在の行を抜けるとき、別の関数に入るとき、現在の関数を抜けるとき、現在の関数を過ぎてエラーが投げられたとき(この場合、エラーキャッチャーがあれば、その中で実行を一時停止する)、実行を再開し一時停止する。現在の関数が行情報を持っていない場合(ネイティブなど)、関数の出入りやエラーの発生で一時停止する。

StepOver request (0x15)

Format:

xml
REQ <int: 0x15> EOM
REP EOM

Example:

xml
REQ 21 EOM
REP EOM

実行が現在の行を抜けるとき、現在の関数を抜けるとき、現在の関数を過ぎてエラーが投げられたとき(この場合、エラーキャッチャーがあれば、その中で実行を一時停止する)、実行を再開し一時停止する。現在の関数が行情報を持っていない場合(ネイティブなど)、関数終了時またはエラースロー時に一時停止する。

StepOut request (0x16)

Format:

xml
REQ <int: 0x16> EOM
REP EOM

Example:

xml
REQ 22 EOM
REP EOM

実行が現在の関数を終了したとき、または現在の関数を過ぎてエラーが発生したときに、実行を再開し、一時停止する(この場合、エラーキャッチャーがあれば、その中で実行を一時停止する)。

ListBreak request (0x17)

Format:

xml
REQ <int: 0x17> EOM
REP [ <str: fileName> <int: line> ]* EOM

Example (two breakpoints):

xml
REQ 23 EOM
REP "foo.js" 102 "bar.js" 99 EOM

AddBreak request (0x18)

Format:

xml
REQ <int: 0x18> <str: fileName> <int: line> EOM
REP <int: breakpoint index> EOM

Example:

xml
REQ 24 "foo.js" 109 EOM
REP 3 EOM

ブレイクポイントを置くスペースがない場合は、Too manyエラーを送信します:

xml
REQ 24 "foo.js" 109 EOM
ERR 2 "no space for breakpoint" EOM

DelBreak request (0x19)

Format:

xml
REQ <int: 0x19> <int: index> EOM
REP EOM

Example:

xml
REQ 25 3 EOM
REP EOM

無効なインデックスを使用した場合は、エラー応答を送信します。

GetVar request (0x1a)

Format:

xml
REQ <int: 0x1a> <int: level> <str: varname> EOM
REP <int: 0/1, found> <tval: value> EOM

Example:

xml
REQ 26 -1 "testVar" EOM
REP 1 "myValue" EOM

Levelはコールスタックの深さを指定し、-1が最上位(現在の)関数、-2が呼び出し関数などである。指定しない場合は、最上位の関数が使用されます。

PutVar request (0x1b)

Format:

xml
REQ <int: 0x1b> <int: level> <str: varname> <tval: value> EOM
REP EOM

Example:

xml
REQ 27 -1 "testVar" "newValue" EOM
REP EOM

Levelはコールスタックの深さを指定し、-1が最上位(現在の)関数、-2が呼び出し関数などである。指定しない場合は、最上位の関数が使用されます。

GetCallStack request (0x1c)

Format:

xml
REQ <int: 0x1c> EOM
REP [ <str: fileName> <str: funcName> <int: lineNumber> <int: pc> ]* EOM

Example:

xml
REQ 28 EOM
REP "foo.js" "doStuff" 100 317 "bar.js" "doOtherStuff" 210 880 EOM

コールスタックのエントリーを上から下へリストアップします。

GetLocals request (0x1d)

Format:

xml
REQ <int: 0x1d> <int: level> EOM
REP [ <str: varName> <tval: varValue> ]* EOM

Example:

xml
REQ 29 -1 EOM
REP "x" "1" "y" "3.1415" "foo" "bar" EOM

指定された活性化(内部の _Varmap )からローカル変数名をリストアップします。Levelはコールスタックの深さを指定し、-1は最上位(現在の)関数、-2は呼び出し関数、などである。

その結果、varで宣言されたローカル変数とローカルに宣言された関数だけが含まれます。外部関数やグローバル変数など、現在の関数スコープの外にある変数は含まれません。

INFO

ローカル変数のリストには、eval()などで動的に宣言された変数や、try-catchのcatch変数のような動的スコープを持つ変数は現在含まれていません。これは、将来のバージョンで修正される予定です。

Eval request (0x1e)

Format:

xml
REQ <int: 0x1e> (<int: level> | <null>) <str: expression> EOM
REP <int: 0=success, 1=error> <tval: value> EOM

Example:

xml
REQ 30 null "1+2" EOM
REP 0 3 EOM

Levelはコールスタックの深さを指定し、-1が最上位(現在の)関数、-2が呼び出し関数などである。指定しない場合は、最上位の関数が使用されます(実際の eval() と同様)。レベルは、評価されるコードの字句の範囲にのみ影響します。コールスタックはそのままで、スタックトレースやDuktape.act()などで確認することができます。レベルは、間接的なEvalを実行するためにNULLにすることもできます。

有効なコールスタックレベルが指定された場合、指定されたコールスタックレベルで指定されたレキシカルスコープで、実行が一時停止した位置でevalへの直接呼び出しを実行したかのようにeval式が評価されます。evalの直接呼び出しは、呼び出された関数と同じレキシカルスコープを共有します(evalの間接呼び出しはそうではありません)。例えば、次のように実行するとします:

js
function foo(x, y) {
    print(x);  // (A)
    print(y);  // (B) <== paused here (before print(y))
}

foo(100, 200);

で、evalする:

js
print(x + y); y = 10; "quux"

Evalは、あたかもそのコードがあったかのように実行される:

js
function foo(x, y) {
    print(x);
    eval('print(x + y); y = 10; "quux");
    print(y);
}

foo(100, 200);

というように、Eval文がそうなるように:

  • 300をプリントアウトする(printを使用)。
  • y`に10を代入して、ステートメントBが(200の代わりに)10を表示するようにする。
  • evalの最終結果は文字列"quux"となり、デバッグクライアントUIに表示されます。

duk_debugger_cooperate()呼び出し中など、Duktapeの起動外からEvalが要求された場合、ECMAScriptが起動されていないため、直接Evalを実行することができません。その場合、Evalは間接Evalとして実行されます。上記のように、コールスタックレベルにnullを送ることで、間接Evalを明示的に要求することができます。

現在の限界:

  • 無限ループにはまる可能性がある。
  • デバッグコードは実際の eval() 呼び出しの内部で実行され、コールスタックに影響を与えます。例えば、Duktape.act()は、追加のスタックフレームを見ることになります。

Detach request (0x1f)

Format:

xml
REQ <int: 0x1f> EOM
REP EOM

Example:

xml
REQ 31 EOM
REP EOM

Duktapeにデバッガを切り離すよう要求する。Duktapeがユーザートランスポートコードにトランスポート接続を閉じるよう要求し、通常の実行を再開する。

DumpHeap request (0x20)

Format:

xml
REQ <int: 0x20> EOM
REP <dvalues> EOM

Example:

xml
REQ 32 EOM
REP <dvalues> EOM

Duktape ヒープ全体の内容をダンプします。ヒープダンプのフォーマットはやや複雑で、 duk_debugger.c を参照してください。

ヒープ状態のJSONダンプをダウンロードして解析できるデバッガーUI機能の実装に使用されます。

INFO

このコマンドは、現時点ではやや不完全なものです。ヒープブラウザを実装するのに便利で、おそらく何らかのUIと一緒に完成することになるでしょう。

INFO

ダンプのフォーマットは、個々のヒープオブジェクトの詳細を読み取るためにGetHeapObjInfoを活用するように変更される可能性があります。このコマンドは、デバッグクライアントが独自に検査できるオブジェクトのリストを提供するだけになります。

GetBytecode request (0x21)

Format:

xml
REQ <int: 0x21> (<int: level> | <obj: target> | <heapptr: target>) EOM
REP <int: numconsts> (<tval: const>){numconsts}
    <int: numfuncs> (<tval: func>){numfuncs}
    <str: bytecode> EOM

引数なしの例では、現在の関数のバイトコードを取得します:

xml
REQ 33 EOM
REP 2 "foo" "bar" 0  "...bytecode..." EOM

コールスタックレベルは明示的に与えることができ、例えば-3はコールスタックトップから数えて3番目のコールスタックレベルです:

xml
REQ 33 -3 EOM
REP 2 "foo" "bar" 0  "...bytecode..." EOM

ECMAScriptの関数オブジェクトは、"object "または "heapptr "のdvalueを使用して明示的に与えることもできます:

xml
REQ 33 {"type":"object","class":6,"pointer":"00000000014839e0"} EOM
REP 2 "foo" "bar" 0  "...bytecode..." EOM

場合、エラー返信が返されます:

  • 引数は存在するが、無効な型を持っているか、ECMAScript関数でないターゲット値を指している。
  • Callstack entry doesn't exist or isn't an ECMAScript activation.

Notes:

  • バイトコードのエンディアンはターゲットに依存するため、デバッグクライアントはターゲットのエンディアンを取得し、それに基づいてバイトコードを解釈する必要があります。
  • Duktape 1.4.0からのマイナーチェンジ:コールスタック・エントリーが存在しない場合、Duktape 1.5.x 以上では空の結果ではなく、エラーを返します。

INFO

このコマンドは現時点ではやや不完全なもので、デバッガーUIでこれを行う最良の方法が判明した時点で修正される可能性があります。

INFO

このコマンドは、GetHeapObjInfoを使用して同じバイトコード情報を取得するために、削除することができます。

AppRequest request (0x22)

Format:

xml
REQ <int: 0x22> [<tval>*] EOM
REP [<tval>*] EOM

Example:

xml
REQ 34 "GameInfo" "GetTitle" EOM
REP "Spectacles: Bruce's Story" EOM

ターゲットがリクエストコールバックを登録していない場合、Duktapeは応答する:

xml
ERR 2 "AppRequest unsupported by target" EOM

アプリケーションリクエストコールバックは、例えば、エラーを示すこともある:

xml
ERR 4 "missing argument for SetFrameRate"

これはカスタムリクエストメッセージであり、その意味とセマンティクスはアプリケーションに依存する。

AppRequestメッセージは、Duktapeデバッグプロトコルでデバッグクライアントとデバッグターゲットの間で直接通信するために使用されます。カスタムメッセージの意味や含まれるdvalueは、完全に実装次第であり、アプリケーションのニーズによっては、まったくサポートされないこともあります。

詳しくは後述の「カスタムリクエストと通知」をご覧ください。

GetHeapObjInfo (0x23)

Format:

xml
REQ <int: 0x23> (<heapptr: target> | <object: target> | <pointer: target>) EOM
REP [<int: flags> <str/int: key> [<tval: value> | <obj: getter> <obj: setter>]]* EOM

Example:

xml
REQ 35 { "type": "heapptr", "pointer": "deadbeef" } EOM
REP 0 "class_name" "ArrayBuffer" ... EOM

提供されたヒープ・ポインタを使用してヒープ・オブジェクトを検査します。ポインタを含む任意のd値型が許可されます: heapptr, object, pointer.デバッグ・クライアントは、ポインタが安全であること、すなわちポインタが有効であり、ポインタ・ターゲットがまだDuktapeヒープにあることを保証する責任があります:

  • デバッガが一時停止されると、ガベージコレクションは自動的に無効になり、デバッガが一時停止している間に取得されたポインタはすべて安全です。実行が Resume またはステップ コマンドを使用して再開されると、すべてのポインターがガベージコレクションによって無効になる可能性があります。
  • デバッガが一時停止していないとき、デバッグクライアントは、オブジェクトが到達可能であり、したがって検査しても安全であることが100%確実に知られている場合、オブジェクトを安全に検査することができます。これは一般的に安全な仮定ではないので、本当に必要な場合を除き、この仮定を行うことは避けるべきです。
  • 警告:安全でないポインタを検査すると、メモリが安全でない動作になり、クラッシュなどにつながる可能性があります。

結果は、人工的なプロパティエントリーのリストで、それぞれフラグフィールド、キー、および値を含んでいます。使用される共有フォーマットについては、GetObjPropDescを参照してください。

人工プロパティは実際にはプロパティテーブルに存在しませんが、duk_heaphdrフラグなどに基づいて生成され、バージョン管理を容易にするために文字列キーが付けられています。人工プロパティは、バージョン間で変更される可能性のある内部フィールドを公開し、バージョン保証の一部ではありません。そのため、人工プロパティのキーや値はバージョン間で変更される可能性があります。しかし、プロパティが文字列でキー設定されているため、デバッグ・クライアントがそのような変更に対応するのは比較的簡単です。

現在の人工キーについては、「ヒープオブジェクト検査」の項で説明しています。

GetObjPropDesc (0x24)

Format:

xml
REQ <int: 0x24> <obj: target> <str: key> EOM
REP <int: flags> (<str: key> | <int: key>) (<tval: value> | <obj: getter> <obj: setter>) EOM

Example:

xml
REQ 36 { "type": "object", "class": 10, "pointer": "deadbeef" } "message" EOM
REP 7 "message" "Hello there!" EOM

ゲッターコールやProxyトラップなどの副作用を起こすことなく、特定の文字列キーを使用してECMAScriptオブジェクトのプロパティを検査します。結果は以下のどちらかです:

  • 以下のフォーマットでプロパティ値を指定する。
  • プロパティが存在しない場合、"not found "エラーとなる。

内部配列に格納されているプロパティは、整数ではなく「3」のような数値文字列のキーでインデックスされます。

プロキシオブジェクトは、トラップを呼び出すことなく、そのまま検査されます。通常利用できるプロパティは、ターゲットを示すDuktape固有の内部制御プロパティと、トラップを持つハンドラーオブジェクトだけです。Proxyオブジェクトは、GetHeapObjInfoが返す人工プロパティ exotic_proxyobj を使って確実に検出することができます。

ポインタの安全性に関する注意事項については、GetHeapObjInfoを参照してください。

各プロパティ項目は、以下のdvalueのシーケンスを使用して記述されます(このフォーマットは、GetHeapObjInfoやGetObjPropDescRangeなどの他のプロパティ関連コマンドと共有されています):

  • フラグフィールド
    • ビットマスク(後述)
  • キー
    • 常に文字列。配列のインデックスプロパティでは、インデックスを正規のインデックス文字列に変換する(例:"3")。
  • プロパティ値:
    • プロパティがアクセサーでない場合(flagsフィールドから明らか): duk_tvalを表す単一のdvalue。
    • プロパティがアクセサの場合:ゲッター関数とセッター関数を指す2つのd値(それぞれ)

flags フィールドは、以下のビットを持つ符号なし整数のビットマスクである:

BitmaskDescription
0x01プロパティ属性:書き込み可能、DUK_PROPDESC_FLAG_WRITABLEに一致します。
0x02プロパティ属性:enumerable、DUK_PROPDESC_FLAG_ENUMERABLEにマッチします。
0x04プロパティ属性:設定可能、DUK_PROPDESC_FLAG_CONFIGURABLEにマッチします。
0x08プロパティ属性:アクセサー、DUK_PROPDESC_FLAG_ACCESSORにマッチします。
0x10プロパティが仮想であり、DUK_PROPDESC_FLAG_VIRTUALに一致します。
0x100プロパティキーはSymbolです。
0x200プロパティは、通常のECMAScriptのコードからは見えない隠しSymbolです。

人工プロパティ(GetHeapObjInfoによって返される)については、プロパティ属性は関連性がなく(ゼロとして送信される)、値は現在決してアクセッサではありません。

GetObjPropDescRange (0x25)

Format:

xml
REQ <int: 0x25> <obj: target> <int: idx_start> <int: idx_end> EOM
REP [<int: flags> (<str: key> | <int: key>) (<tval: value> | <obj: getter> <obj: setter>)]* EOM

Example:

xml
REQ 37 { "type": "object", "class": 10, "pointer": "deadbeef" } 0 2 EOM
REP 7 "name" "Example object" 7 "message" "Hello there!" EOM

ECMAScriptオブジェクトの "own "プロパティの範囲 [idx_start,idx_end[ を検査します。結果は見つかったプロパティを含む。もし開始/終了インデックスが利用可能なプロパティ数より大きければ、それらの値は結果から完全に欠落する。例えば、オブジェクトに3つのプロパティがあり、範囲[0,10]を要求した場合、結果には3つのプロパティのみが含まれます。インデックスが交差している場合(例:[10,5[)には、空の結果が返されます。

範囲 [idx_start,idx_end[ のインデックスは、(1) ガベージコレクションを防ぐために実行を一時停止し、 (2) オブジェクトが変異しない限り安定であることが保証されている概念的なインデックス空間を参照しています。インデックス空間内のプロパティの順序は特に保証されておらず、必ずしも列挙順序と一致しない。特定の表示順序が必要な場合は、デバッグクライアントがプロパティを並び替える必要がある。

現在のインデックス空間(将来のバージョンでは変更される可能性があります)には、以下のものが含まれています:

  • オブジェクトの内部配列部分、添字 [0,a_size[.ここで、a_sizeは密な配列部分に割り当てられたスペースであり、配列の見かけ上の .length プロパティよりも大きいかもしれない。マッピングされていない値や配列のインデックスがない場合は、"unused" dvaluesとして返されます。
  • objectの内部エントリ部分、インデックス [0,e_next[.エントリ部には削除されたプロパティが含まれることがあり、そのプロパティは "unused "dvalueとして返されます。

デバッグクライアントは、これらの詳細を気にする必要はなく、unusedの値を正しく処理すれば、任意の範囲(2つの部分にまたがるものでも可)を読み取ることができます。

デバッグクライアントは、インデックス範囲 [0,0x7fffff[ (今のところ符号付きインデックス) を要求するだけで、すべてのプロパティを要求することができます。結果は、実際に存在するプロパティと同じ数だけ含まれます。

デバッグクライアントは、次のようにプロパティセットに対してインクリメンタルに反復処理することもできます:

  • 例えば、[0,10[[10,20[などのように、インデックス範囲を順番にリクエストします。
  • 部分的な結果(ここでは10個未満のプロパティ)を受け取ったら、終了とする。同じような方法として、完全に空の結果を受け取ったときに反復処理を停止することもできます。

インデックス空間に含まれるプロパティは、副作用のない対象オブジェクトの⾵⾊のプロパティです:

  • プロパティの属性は、flagsフィールドで提供されます。内部プロパティは、現在0xFFバイトで始まるキーを使って実装されているが、デバッグクライアントがマーカーバイト(将来のバージョンでは変更される可能性がある)を個別にチェックする必要がないように、明示的にフラグを立てる。
  • アクセサー・プロパティは、セッターとゲッターのペアとして、ゲッターを呼び出さずにそのまま記述する。デバッグクライアントが望むなら、明示的にそれを行うことができる。
  • 継承されたプロパティは、列挙されません。デバッグクライアントは、prototype人工プロパティを検索し、そのオブジェクトを個別に検査することで、プロトタイプチェーンを手動で歩くことができます。プロトタイプのウォーキングは、プロトタイプのループで失敗しないように注意する必要があります。
  • 完全に仮想化された方法で実装されているいくつかのプロパティは、ECMAScriptの列挙で見えるが、検査では見えないかもしれない。例えば、Stringオブジェクトは文字列の仮想インデックスプロパティ(0, 1, 2, ...)を持っていますが、これらは現時点では検査結果に含まれません。ただし、GetObjPropDescで読み取ることは可能です。
  • Proxyのトラップは発動されず、返されるプロパティはProxy自身のプロパティです。通常、ProxyはProxyのターゲットとハンドラテーブルを特定するDuktape固有の内部制御プロパティしか持っていません。

Arrayオブジェクトには密と疎があることに注意してください。密な配列には配列項目が格納される配列部分があり、疎な配列には配列部分がなく、配列項目は通常の文字列キー付きプロパティと一緒にメインプロパティテーブルに格納されます。したがって、スパース配列の配列項目は、通常の文字列キー付きプロパティとして表示され、インデックスの昇順でない場合があります。デバッグクライアントは、常に優先表示順序に合うようにプロパティを並べ替える必要があります。配列の隙間は、欠落したキーとして、またはd値 "unused "を持つキーとして表示することができる。現在、疎な配列の隙間は欠落したキーとして表示され、密な配列の隙間は "unused "というd値として表示されます。

ポインタの安全性に関する注意事項については、GetHeapObjInfoを参照してください。

カスタムリクエストと通知

Duktape 1.5.xから、Duktapeは特別なAppRequestとAppNotifyメッセージを使用して、同じトランスポート上でデバッグクライアントとデバッグターゲットの間の直接通信をサポートしています。これらのメッセージはDuktapeにとって意味を持たず、定義されたAPIを通じて前後にマーシャリングする役割を果たすだけです。

AppNotify メッセージは、メッセージの内容をスタックにプッシュして duk_debugger_notify() を呼び出し、プッシュした値の数を渡すことで送信することができます。プッシュされた各値は、メッセージの中でdvalueとして送信されます。つまり、"foo "という文字列と "bar "という文字列をpushすると、クライアントにはNFY 7 "foo" "bar" EOMと表示されます。

AppRequestは、ECMAScriptの実行とは直接関係なく、実装に依存する可能性のある要求をターゲットに行うために使用されます。例えば、AppRequestは次のように使用されるかもしれない:

  • デバッグ対象のファイルシステムからソースファイルを直接ダウンロードする。
  • ゲームエンジンのフレームレートを変更する
  • デバッグ中に組み込みターゲットデバイスをリセット/リブートする。
  • ソフトウェアやスクリプトのアップデートの実行またはトリガー

AppRequest をサポートしたいターゲットは、duk_debugger_attach() を呼び出す際にリクエストコールバックを提供する必要があります。AppRequestを受信すると、リクエストコールバックは値スタック上のメッセージの内容で起動され、返信で送信される独自の値をプッシュすることができます。リクエストコールバックは、必要に応じてブロックすることができる(例えば、コールバックはハードウェアボタンが押されるのを待つかもしれない)。しかし、コールバックが実行される間、Duktapeもブロックされるので、場合によっては望ましくないし、デバッグ・クライアントがタイムアウトする原因になるかもしれない(もちろん、これは完全にデバッグ・クライアントに依存する)ことに注意してください。

これは、最小限の何もしないリクエストコールバックです:

c
duk_idx_t duk_cb_debug_request(duk_context *ctx, void *udata, duk_idx_t nvalues) {
    /* Number of return values is returned: here empty reply. */
    return 0;
}

上記のダミーコールバックは、すべてのリクエストに対して REP EOM (空の返信) で応答するだけです。

より有用なコールバックは、受け取った値を値スタックで処理し、返信として送信する値を自分でプッシュし、プッシュした値の数を示す非負の整数を返すべきである。以下は、もう少し便利な実装です:

c
duk_idx_t duk_cb_debug_request(duk_context *ctx, void *udata, duk_idx_t nvalues) {
    const char *cmd_name = NULL;

    /* Callback must be very careful NEVER to access values below
     * 'nvalues' topmost value stack elements.
     */
    if (nvalues >= 1) {
        /* Must access values relative to stack top. */
        cmd_name = duk_get_string(ctx, -nvalues + 0);
    }

    if (cmd_name == NULL) {
        /* Return -1 to send an ERR reply.  The value on top of the stack
         * should be a string which will be used for the error text sent
         * to the debug client.
         */
        duk_push_string(ctx, "missing application specific command name");
        return -1;
    } else if (strcmp(cmd_name, "VersionInfo") == 0) {
        /* Return a positive integer to send a REP containing values pushed
         * to the stack.  The return value indicates how many dvalues you
         * are including in the response.
         */
        duk_push_string(ctx, "My Awesome Program");
        duk_push_int(ctx, 81200);  /* ver. 8.12.0 */
        return 2;  /* 2 dvalues */
    } else {
        duk_push_sprintf(ctx, "unrecognized application specific command name: %s",
                         cmd_name);
        return -1;
    }
}

アタッチ時にリクエストコールバックが提供されない場合、AppRequestはサポートされていないコマンドとして扱われ、Duktapeからその旨のERR返信を引き出す。ターゲットは常にAppNotifyメッセージを自由に送信することができます。

予防措置として、ターゲットは、クライアントが受信して検査するまでにヒープポインタが古くなる可能性があるため、JSオブジェクトのような構造化された値を通知メッセージで送信しないようにする必要があります。これは、特にターゲットが実行中に送信された通知について当てはまります。数値、ブーリアン、文字列など、ユニークなdvalue表現を持つプリミティブにこだわるのがよいでしょう。構造化された値を送信する必要がある場合は、JSON/JXなどでエンコードして文字列として送信すればよい(キャッチされないエラーを注意深く回避する)。

リクエストコールバックに関する重要な注意事項

リクエストコールバックには duk_context ポインタが提供され、このポインタを使用して値スタックにアクセスすることができ、信頼されていると見なされます。このコールバックには、やってはいけないことがあります(MUST NOT)。具体的には

  • それは nvalues が特定の値を持つことを想定してはならない(MUST NOT)。特に、コールバックの引数(アプリケーション固有のコマンドを識別するために慣習的に使用される文字列でさえも)がないように、それはゼロであるかもしれない。
  • コールバックは、与えられた nvalues とコールバック自身がプッシュした値を超えて、スタックの最上位にある値にアクセスしたり、ポップしたりしようとしてはならない(MUST NOT)。
  • また、duk_get_top() や同様のプリミティブに対して、特定の値を仮定してはなりません(MUST NOT)。実際のところ、これは値へのアクセスに負のスタックインデックスを使用することを意味します。
  • エラーを投げてはならない(MUST NOT)。スタックの値を直接扱うと、誤ってエラーを投げてしまうことが非常に多いので、ここでは注意が必要です。

この契約に違反すると、未定義の動作となり、デバッガーの状態を破損したり、不正な動作を引き起こしたり、あるいはセグフォールトにつながる可能性があります。例えば、関数をサンドボックス化して、無関係なスタック値にアクセスできないようにし、安全にエラーを投げることができるようにするなど、将来的にはより堅牢にしたいものです。

メッセージの dvalue は、受信した順にプッシュされます。スタック上の任意の値の相対位置が値の総数に依存するため、負のインデックスを使用してアクセスするのは不便です。しかし、コールバックは値の総数をパラメータとして受け取るので、便利な慣例として、スタックに次のようなインデックスを付けることができます:

c
if (nvalues < 3) {
    duk_push_string(ctx, "not enough arguments");
    return -1;
}
cmd_name = duk_get_string(ctx, -nvalues + 0);
val_1 = duk_get_string(ctx, -nvalues + 1);
val_2 = duk_get_int(ctx, -nvalues + 2);

AppRequest/AppNotifyコマンドフォーマット

一般的な慣習として、AppRequestやAppNotifyメッセージのコマンド番号の後の最初のフィールドは、コマンドを識別する文字列にすることが推奨されます。これにより、異なるクライアントやターゲットの相互運用が容易になります。認識できないコマンド名は無視できますが、整数コマンドなどは、使用するデバッグクライアントやターゲットによって解釈が異なる可能性があります。

もし、あるコマンドがあなたのアプリケーションに特有なもの(目的や動作)であれば、例えば「MyApp-AwesomeCmd」のように接頭辞を付けるとよいでしょう。これにより、似たような名前のコマンドを持つ他のターゲットとの衝突を避けることができます。

結局のところ、アプリケーション・メッセージの内容に関する規約や全体的な形式は、Duktapeによって実際に強制されることはありません。したがって、ピアは、そのメッセージがどこから来たのか正確に知らない限り、AppRequestまたはAppNotifyメッセージの内容に関していかなる仮定も立ててはならない。

ヒープオブジェクトの検査

人工キーはバージョン間で変更になる可能性があります。

ただし、以下のものはバージョン保証があります:

  • prototype`:内部プロトタイプ(外部プロトタイプである㊙プロパティと混同しないでください)。
  • class_name`: オブジェクトクラスの文字列名。
  • class_number`: オブジェクトのクラス番号、オブジェクトの dvalue と一致する。

Duktape 1.5.0

以下のリストは、Duktape 1.5.0に含まれる人工キーの説明です。最新の動作は src-input/duk_debugger.c を参照してください:

Artificial property keyObject type(s)Description
heaphdr_flagsduk_heaphdr (all)生の duk_heaphdr フラグフィールド。個々のフラグは、個別の人工プロパティとしても提供されます。
heaphdr_typeduk_heaphdr (all)duk_heaphdr型フィールド、[`duk_HTYPE_xxx]{.title-ref}.
refcountduk_heaphdr (all)参照回数。refcount がサポートされていない場合は省略される。
extensibleduk_hobjectDUK_HOBJECT_FLAG_EXTENSIBLE
constructableduk_hobjectDUK_HOBJECT_FLAG_CONSTRUCTABLE
callableduk_hobjectDUK_HOBJECT_FLAG_CALLABLE
boundduk_hobjectDUK_HOBJECT_FLAG_BOUND
compfuncduk_hobjectDUK_HOBJECT_FLAG_COMPFUNC
natfuncduk_hobjectDUK_HOBJECT_FLAG_NATFUNC
bufobjduk_hobjectDUK_HOBJECT_FLAG_BUFOBJ
fastrefsduk_hobjectDUK_HOBJECT_FLAG_FASTREFS
array_partduk_hobjectDUK_HOBJECT_FLAG_ARRAY_PART
strictduk_hobjectDUK_HOBJECT_FLAG_STRICT
notailduk_hobjectDUK_HOBJECT_FLAG_NOTAIL
newenvduk_hobjectDUK_HOBJECT_FLAG_NEWENV
namebindingduk_hobjectDUK_HOBJECT_FLAG_NAMEBINDING
createargsduk_hobjectDUK_HOBJECT_FLAG_CREATEARGS
have_finalizerduk_hobjectDUK_HOBJECT_FLAG_HAVE_FINALIZER
exotic_arrayduk_hobjectDUK_HOBJECT_FLAG_EXOTIC_ARRAY
exotic_stringobjduk_hobjectDUK_HOBJECT_FLAG_EXOTIC_STRINGOBJ
exotic_argumentsduk_hobjectDUK_HOBJECT_FLAG_EXOTIC_ARGUMENTS
exotic_proxyobjduk_hobjectDUK_HOBJECT_FLAG_EXOTIC_PROXYOBJ
special_callduk_hobjectDUK_HOBJECT_FLAG_SPECIAL_CALL
class_numberduk_hobjectDuktape internal class number (same as object dvalue).
class_nameduk_hobject文字列のクラス名、例:"ArrayBuffer"
prototypeduk_hobject有効な(内部)プロトタイプを指し、クライアントコントロールで継承されたプロパティを列挙することができる。
propsduk_hobject現在のプロパティテーブルの割り当て。
e_sizeduk_hobjectエントリーパーツサイズです。
e_nextduk_hobject入力部次のインデックス(=使用サイズ)。
a_sizeduk_hobjectアレイの部品サイズ。
h_sizeduk_hobjectハッシュパーツサイズです。
lengthduk_harray配列 .長さ。
length_nonwritableduk_harrayArray .length の書き込み可能(false)または書き込み不可能(true)。
threadduk_hdecenvオープンな宣言型環境のためのスレッド。
varmapduk_hdecenvオープンな宣言型環境のためのVarmap。
regbaseduk_hdecenvオープンな宣言型環境のためのRegbase。
targetduk_hobjenvオブジェクト環境の対象オブジェクト。
has_thisduk_hobjenvオブジェクトの環境から'この'バインディングが提供される場合、真。
(not present yet)duk_hnatfuncネイティブ関数ポインタ。
nargsduk_hnatfuncスタック引数の数。
magicduk_hnatfunc魔法値です。
varargsduk_hnatfunc関数が変数引数を持つ場合、真。
(not present yet)duk_hcompfuncバイトコードを含むECMAScript関数データ領域。
lex_envduk_hcompfunc機能字句の環境。
var_envduk_hcompfunc関数変数環境。
nregsduk_hcompfuncバイトコードエグゼキュータレジスタの数。
nargsduk_hcompfuncスタック引数の数。
start_lineduk_hcompfuncソースコードの1行目。
end_lineduk_hcompfuncソースコードの最終行。
(no properties yet)duk_hthreadスレッドプロパティはまだありません。
bufferduk_hbufobj下地となるプレーンバッファ(heapptrとして提供)。
slice_offsetduk_hbufobjスライスの開始のための基礎となるバッファへのバイトオフセット。
slice_lengthduk_hbufobjスライスのバイト長。
elem_shiftduk_hbufobj要素のシフト値、例:Uint64 -> 3.
elem_typeduk_hbufobjDUK_HBUFOBJ_ELEM_xxx
is_typedarrayduk_hbufobjbufferobjectが型付き配列(例:Uint8Array)であれば真。
extdataduk_hstringduk_hstring_flag_extdata
bytelenduk_hstring文字列のバイト長。
charlenduk_hstring文字列の文字数。
hashduk_hstring文字列ハッシュ。アルゴリズムは設定オプションに依存する。
dataduk_hstringプレーンな文字列の値。
dynamicduk_hbufferduk_hbuffer_flag_dynamic
externalduk_hbufferduk_hbuffer_flag_external
sizeduk_hbufferバッファのバイトサイズ。
dataptrduk_hbuffer現在のデータ領域への生ポインタ。
dataduk_hbufferバッファデータ

現在、使用不可

これらはコード上では無効(if #0'd out)であり、有用であれば再び追加することができる:

Artificial property keyObject type(s)Description
reachableduk_heaphdr (all)DUK_HEAPHDR_FLAG_REACHABLE
temprootduk_heaphdr (all)DUK_HEAPHDR_FLAG_TEMPROOT
finalizableduk_heaphdr (all)DUK_HEAPHDR_FLAG_FINALIZABLE
finalizedduk_heaphdr (all)DUK_HEAPHDR_FLAG_FINALIZED
readonlyduk_heaphdr (all)DUK_HEAPHDR_FLAG_READONLY
arridxduk_hstringDUK_HSTRING_FLAG_ARRIDX
symbolduk_hstringDUK_HSTRING_FLAG_SYMBOL (DUK_HSTRING_FLAG_INTERNAL in Duktape 1.x)
hiddenduk_hstringDUK_HSTRING_FLAG_HIDDEN
reserved_wordduk_hstringDUK_HSTRING_FLAG_RESERVED_WORD
strict_reserved_wordduk_hstringDUK_HSTRING_FLAG_STRICT_RESERVED_WORD
eval_or_argumentsduk_hstringDUK_HSTRING_FLAG_EVAL_OR_ARGUMENTS

デバッガ文

ECMAScriptにはデバッガ文があります:

js
a = 123;
debugger;
a = 234;

E5仕様では、以下のように記載されています:

DebuggerStatement productionを評価することで、デバッガで実行したときにブレークポイントを発生させる実装が可能になる場合があります。デバッガが存在しないか、またはアクティブでない場合、このステートメントは観察可能な効果を持ちません。

他のECMAScriptエンジンは、通常、デバッガステートメントをブレイクポイント

Duktapeはこれをブレークポイントとしても解釈します。つまり、デバッグクライアントが接続されているときにデバッグ文に遭遇すると、実行が一時停止されます。これにより、匿名のevalコードでもブレークポイントを設定することができます(ただし、ソースコードへのアクセスはできません)。

パケットベースのトランスポートの上にデバッグトランスポートを実装する

パケットベースの下位プロトコル上にデバッグトランスポートを実装することは、パックベースのプロトコル上にTCPストリームや仮想シリアルリンクを転送することと本質的に同じ問題です。そうすることで、Duktape特有の問題はほとんどなく、この問題はかなりよく理解されています。このセクションでは、いくつかのポインタを提供します。

基本的な課題

  • 任意のデータチャンクを、順序を入れ替えたり重複させたりせずに、確実に送受信する仕組みが必要です。このメカニズムは、ターゲットとデバッグクライアントの両方に必要です。
  • バッファリングが問題になる場合は、フロー制御機構を実装する必要があるかもしれません。通常、バッファリングが問題になるのはデバッグターゲットだけなので、通常は片方向のフロー制御で十分です。
  • デバッグターゲットから送信されるデータチャンクが適度な大きさになるように、Duktapeによって行われるデバッグトランスポートライトを合体させ、これ以上データが送信されないときに保留中のバイトをフラッシュアウトするために⽶書フラッシュを使う必要があるかもしれません。また、TCPと同じようにタイマーを使うこともできます。

独自のデバッグクライアントを実装する場合は、受信したデータチャンクからデバッグストリームを解析する必要があります(例えば、トライアルパースなど):

  • 受信したデータチャンクを読み、入力バイトバッファに追加する。
  • デバッグメッセージの試行解析は、完全なメッセージが解析できなくなるまで行う。その後、次の受信データチャンクを待つ。
  • デバッグメッセージの境界は、Duktapeがトランスポート実装に行う読み取り/書き込み呼び出しと一致することが保証されていないため、デバッグメッセージをトランスポート実装が送信/受信するデータチャンクに一致させようとするべきではありません!

コアレスティングの書き込み例

  • アウトバウンドライトのために最大NバイトのバッファBUFを維持する。
  • Duktapeトランスポートの各書き込みコールに対して:
    • 書き込みデータがBUFに収まるなら、それを追加する。そうでない場合は、残りのBUFスペースに収まるだけのバイトを追加する(部分書き込み)。
    • バッファが満杯(Nバイト)になった場合は、送信してバッファを空にする。
    • Duktapeへの戻り値は、消費された、つまりBUFに追加された値の数を示します。
  • 各Duktapeトランスポートに対して、フラッシュを書き込みます:
    • BUFにバイトがある場合、バッファを送信して空にする。
    • Duktapeが書き込みを終了し、読み取りをブロックしたり、実行を再開したりする前に、書き込みフラッシュを実行することを頼りにしてください。ライトフラッシュは、他のタイミングでも行われることがあります。**例えば、書き込みフラッシュがデバッグ・メッセージの境界と一致することは保証されません!**フラッシュに他の意味を割り当てないでください。

一方通行のフロー制御例

デバッグターゲットを確実にするためのシンプルな一方向のフロー制御メカニズムは、MAXBUFバイトの固定受信バッファで実装できます(MAXBUFは256など小さいものです):

  • デバッグクライアントは、2つのバイトカウントを保持します: 1.SENTは、デバッグ接続の開始以来、何バイトが送信されたかを示します。 2.ACKEDは、デバッグターゲットが消費したことが確認されたバイト数を示します。SENT-ACKEDは、ターゲットの入力バッファに潜在するバイト数である。
  • デバッグクライアントは、ターゲットが少なくともMAXBUF - (SENT - ACKED)バイトをバッファリングできることを知っているので、その量を送信するために自由であることがわかります。
  • デバッグターゲットがデバッグクライアントからデータチャンクを受信すると、次のようになります:
    • データチャンクを受信データバッファに追加する。デバッグ・クライアントが正しく動作していれば、データのためのスペースが常にあるはずです。
  • Duktapeがデバッグ・トランスポートの読み取りコールバックを呼び出すとき:
    • インバウンドデータバッファからバイトを消費する。
    • デバッグクライアントにトランスポート固有の通知を送り、ACKEDバイトカウント(=Duktapeリードコールによって消費されたバイト数)を更新する。

Duktapeは小さな読み込みを多く行うので、便利な場合があります:

  • デバッグトランスポートの読み取りコールバックで
    • 以前に送信した値に対する変化が十分に大きくない限り、更新されたACKEDバイト数に対して通知を送信しない。
  • デバッグトランスポートの "read flush "表示に依存する:
    • 受信したら、常に更新されたACKEDバイト数の通知を送信する。

他にも、例えば、ACKEDのバイト数を更新して送るなど、様々なオプションがあります:

  • デバッグターゲットからバイトを受信する。
  • Duktapeがバイトを読み込むとき、完全に満杯の入力バッファから読み込んだときのみ、更新されたACKEDバイトカウントを送信します(つまり、デバッグクライアントは現在、私たちがスペースがあることを通知するまでデータを送信していません)。

実施上の注意

概要

このセクションでは、Duktapeの内部に関する実装上の注意点を説明します。

Duktapeデバッガサポートはオプションで、configオプションで有効になります。また、デバッガサポートが有効な場合、バイトコードエグゼキュータ割り込み機能は必須となります。

ソースファイル

デバッガーのサポートは、ほとんど以下のファイルで実装されています:

  • duk_js_executor.c: チェック実行、ブレークポイント、ステップイン/オーバー、デバッガとの連動 メッセージループ
  • duk_hthread_stacks.c: step out handling
  • duk_debugger.c: デバッグトランスポート、デバッグコマンドハンドリング
  • duk_api_debug.c: デバッガAPIエントリポイント

デバッガーのヒープへのアタッチとデタッチ

ユーザーコードが duk_debugger_attach() を使ってデバッガーをアタッチすると、Duktape は duk_heap の状態を更新してデバッガーがアタッチされたことを反映し、コールバックなどを格納します。

デバッガはDuktapeのヒープレベルで動作します。他のオプションは混乱した結果につながるようです。例えば、デバッガが1つのスレッドに接続されている場合、ブレークポイントはそのスレッドによってのみトリガーされることになります。それでも、ブレークポイントが発動すると、ヒープ全体が一時停止される。単一のスレッドを一時停止して、他のスレッドの実行を再開する方法はない。

実行モード、実行者割り込み、restart_execution

デバッガの実装に必要な最も重要な機能は、アクティブなブレークポイントの検出、ブレークポイントでのトリガー、ステップ実行の効率的な方法を持つことでしょう。これらはDuktapeバイトコードエグゼキュータに以下のように実装されています。

デバッガのサポートは、バイトコードの実行を定期的またはバイトコード命令ごとに中断する機能を提供する、エクゼキュータ割り込み機能に依存しています。このメカニズムは、3つの概念的な実行モードを実装するために使用されます:

  • Normal: バイトコードエグゼキュータは全速力で実行し、たまにエグゼキュータ割り込みに呼び出します。割り込みでは、デバッグクライアントメッセージ(これにより、例えば、突然の一時停止が可能になる)、実行タイムアウトなどを覗き見します。
  • Checked: バイトコードエクゼキュータは、命令ごとにエクゼキュータ割り込みを呼び出して、一度に1つずつオペコードを実行します。割り込みは、行の遷移を検出し、ブレークポイントやステップ関連の条件がトリガーされたかどうかをチェックし、デバッグクライアントメッセージを覗き見(ただし待ち受けブロックはしない)します。
  • Paused: バイトコードエグゼキュータはエグゼキュータ割り込みを呼び出し、エグゼキュータ割り込みは、デバッグクライアントがステップオーバー/イント/アウトやレジュームなどのコントロールフロー関連のコマンドを発行するまでデバッグクライアントのメッセージを処理します。実行はデバッグクライアントの完全な制御下にあります。

一時停止モードは,何らかの再開・離脱コマンドに遭遇するまでデバッグメッセージを処理することで,実行者割り込みに具体的に実装されています.

チェックモードは、割り込みカウンタを注意深く管理することで実現されます。これは、エクゼキュータのファストパスに追加のチェックを導入しないために重要です。実行が再開されると、チェック実行の必要性が検出され(例えば、アクティブなブレークポイントがある、またはステッピングがアクティブである)、どのオペコードも実行される前に割り込みをトリガするように割り込みカウンタが構成される。チェックされたモードを維持する必要がある場合、割り込みハンドラは、再び割り込みハンドラに戻る前に、1つのオペコードのみが実行されるように割り込みカウンタを構成する。

通常実行モードも同様ですが、バイトコードエクゼキュータに戻る際の割り込みカウンタが高い値(例えば、10万オペコード毎に割り込み)に設定されます。

バイトコードエグゼキュータの restart_execution: ラベルは、重要な制御ポイントです。これは、バイトコード・エグゼキュータが新しいアクティベーションの実行を開始しようとするときにいつでも呼び出されますが、デバッグ・コマンドがブレークポイントの状態を調整したときなどにも明示的に呼び出されることがあります。restart実行」操作は、多くの重要なことを行います:

  • デバッガーの接続/非接続の状態をチェックします。デタッチされている場合、他のすべてのデバッガ関連のチェックはスキップされます。
  • 現在の関数でアクティブなブレークポイントをチェックし、実行者割り込みでブレークポイントのトリガーチェックを高速化するためにアクティブなブレークポイントリストを書き出します。
  • アクティブなステッピング状態をチェックします。ステップインとステップオーバーの両方を処理する必要があります。
  • 一時停止状態もチェックする。バイトコードエクゼキュータの外部で一時停止フラグを設定できる場合がある。例えば、"step out "をするとき、コールスタックの巻き戻しコードは、ステップアウトしているアクティベーションを巻き戻すときに "paused "フラグをセットする。これを検出するのは、次回に「実行再開」を呼び出すときだけです。
  • 最終的に、チェックモードとノーマルモードのどちらで実行するかを決定する。

実行後は、エクゼキュータ割り込み機構と割り込みハンドラの助けにより、正常に実行されます。実行モードは、割り込みハンドラが割り込みカウンターを高い値に設定し始めるか、再度restart_executionを起動した場合のみ変更可能です。

バイトコードエグゼキュータの観点からは、統合は非常にシンプルです:

  • restart_executionは、実行の設定としてデバッガの処理を多く行っている。
  • 割り込みカウンタ機構は割り込みハンドラへの呼び出しに使用され、実際のオペコード実行者はそれ以外を気にする必要はない。

ステップとポーズ

以下の内部ヒープレベルの状態が必要です:

  • 一時停止状態:ヒープ幅の広いフラグで、デバッグクライアントが継続の許可を出すまで会話する必要があることを示す。

ステップの状態が割と厄介です:

  • ステップオーバー:元のスレッド、アクティベーションインデックス、スタートラインを追跡する。開始行が変わるまでチェックモードで実行し、その後、一時停止する。他の関数を呼び出した場合、状態は保持され、戻って行番号が変わったら一時停止する。
  • ステップイン:元のスレッド、アクティベーションインデックス、スタートラインを追跡する。開始線が変わるまでチェックモードで実行する。別の関数に呼び出す場合は、その関数に入るときに一時停止する必要がある。
  • ステップアウト:元のスレッドと活性化インデックスを追跡する(スタートラインは関係ない)。通常モードで実行する(もちろんブレークポイントがある場合は除く)。何らかの理由でアクティベーションが解除された場合、一時停止モードに入る。つまり、エラーが投げられたら、キャッチャーで実行を再開する。ステップアウト処理はコールスタック巻き戻しの一部として具体的に実装されており、他のステップコマンドの実装方法とは全く異なる。

コールスタックが巻き戻されないので、コルーチンの降伏は発生しません。

ステップオーバー/イン状態を実行器で確認する ¦「実行を再開」する の操作を行います。

ブレイクポイント

ブレークポイントは、ヒープレベルのファイル/行リストとして保持されます。バイトコードエグゼキュータが "restart execution "操作をすると、ブレークポイントリストを再チェックし、どのブレークポイントがアクティブであるかを把握します。デバッグコマンドの実行などでブレークポイントの状態が変化した場合、バイトコードエグゼキュータが "restart execution "操作を行うことで、ブレークポイントが正しく再チェックされアクティブになります。

アクティブなブレークポイントが1つ以上ある場合、チェックモードで実行が再開されます。ブレークポイントがアクティブでない場合(他にチェックモードにする理由がない場合)、通常モードで実行を再開する。これは、現在実行中の関数の外にあるブレークポイントがアクティブな場合に、実行性能を最大化するために重要です。

アクティブなブレークポイントを把握する上で重要な問題の1つは、内部関数をどのように扱うかです。これについては、以下の別のセクションで取り上げます。

ブレークポイントはDuktapeによって直接処理され、合理的に効率化されています。もう一つの設計案は、ユーザーコードが独自にブレークポイントを実装できるように、ステップ実行のAPIやプロトコル機構を用意することです。この場合、統合されたブレークポイント機構よりも柔軟性が高くなりますが、その分、速度も遅くなります。

ファイル/行のペアを使用してブレークポイントを定義することには、多くの設計上の選択肢があります。現在のファイル/ラインのアプローチは直感的ですが、次のことを意味します:

  • 1行の関数など、1行の途中で改行する方法がない。これは、最小化されたECMAScriptのコードにも影響します。
  • 同じ場所から作成された複数のECMAScript関数インスタンス(つまり duk_hcompfunc オブジェクト)が存在する可能性があります。ブレークポイントはそれら全てにマッチします。

ライン遷移

行番号をアクティブなブレークポイントのPCに変換するために、行からPCへの変換プリミティブが必要であるように最初は思えるかもしれない。しかし、このようなアプローチは、以下に述べるいくつかの理由から、実際には機能しない。

1つの行から複数の命令を生成することができるので、典型的なケースでは同じ行番号の命令が複数存在する。また、ある行番号に対応するオペコードは、フロー制御の構成要素など、(必ずしも直線的、局所的にではなく)コード中に散在させることができます。次のようなことは、まったく可能であり、正常である:

sh
PC      Line
--      ----

50      98
51      99
52      100   <--
53      100   <--
54      100   <--
55      100   <--
56      102
57      103
58      103
59      104
60      105
61      100   <--

また、ある行番号のエントリポイントになるPCが複数存在する場合があります。これは、例えば、ループ構成で起こります。

また、一致するバイトコード命令がない行番号にブレークポイントが設定されることもあります。これは、ブレークポイントが空の行に割り当てられている場合に些細なことで起こりますが、生成されたバイトコードの行番号が1つずれている場合など、非自明な場合にも起こります。期待される動作は、ブレークポイント行に遷移したとき、またはブレークポイント行を越えたときにブレークポイントが一致することです。しかし、このブレークポイントルールを使うには、いくつかの困難があります:

  • 複数の "next lines "や "next opcodes "が存在する可能性がある。switch文の途中の空行にブレークポイントを設けることを考える。
  • (prev_line<break_line) AND (curr_line >= break_line)をブレークポイントのトリガールールとして使用すると、ほとんどの場合動作しますが、特にブレークポイントがスキップされるが実行されない条件付きコードブロックにある場合、直感的ではないブレークポイントの動作を引き起こします。での議論を参照してください:https://github.com/svaarala/duktape/issues/263。(Duktape1.2.xではこのブレークポイントルールを使用していましたが、Duktape1.3.xではルールが変更されました)

ブレークポイントトリガーの現在のルール(Duktape 1.3.x)は、次のとおりです:

js
(prev_line != break_line) AND (curr_line == break_line)

つまり、正確なブレークポイント行に遷移したときにブレークポイントが発生するのです。https://github.com/svaarala/duktape/issues/263の議論を参照してください。

PCの値ではなく、行の遷移でブレークポイントを実装することで、ある行でブレークポイントが発生した場合、どのように実装するかという問題も解決されます。/ "ステップオーバー"?ブレークポイント行から離れるとは、現在の行がブレークポイント行と異なる値に変わるまでバイトコード命令を実行する必要があるということです。制御フローが後方へジャンプすることもあるので、必ずしも次の行やそれ以上の行番号になるとは限らないことに注意してください。

そこで、今現在Duktapeでは、以下のようにブレークポイントを実装しています:

  • 1つ以上のブレークポイントが有効な場合、バイトコードエグゼキュータはチェック付き実行に入ります。チェックされた実行では、バイトコード割り込みメカニズムがすべてのオペコードの前に呼び出されます。チェック実行は、ブレークポイントが性能を低下させないようにするため、可能な限り避けるようにします。
  • 割り込み機構は、行の遷移を検出するために、行情報(前の行、現在の行)を追跡します。つまり、Duktapeは実行されたすべてのオペコードに対してpc-to-lineを実行します。これは現在最適化されておらず、毎回pc-to-lineビットストリームを参照することになります。将来の改善方法については、将来の作業を参照してください。
  • ブレークポイントやステッピングは、行の遷移が発生したとき、つまり prev_line != curr_line のときにチェックされます。

内部関数とブレークポイント

ブレークポイントは、ソースコード内の最も内側の関数でのみ有効であるべきです。例えば、次のように考えてください:

js
1  function foo() {
2      print('foo 1');
3      function bar() {
4          print('bar 1');
5      }
6      print('foo 2');
7      bar();
8  }
9  foo();

現在2行目で実行中で、4行目にブレークポイントが追加されたとします。シングルステップにするとどうなるか?

素朴な実装では、実行者は4行目のブレークポイントをfoo()起動のために有効だと考え、2行目から6行目への行遷移を検出すると、ブレークポイントがトリガーされます。実行は6行目で停止し、"foo 2 "と表示されます。

これを避けるために、ブレークポイントは常に、それが現れる最も内側の関数と関連付けられる(だけ)。これは、各関数の行範囲(最小行番号と最大行番号)を追跡することですぐに検出することができます。そして、ある関数FUNCのアクティブなブレークポイントを次のように決定することができる:

  • ブレークポイントのファイル名が異なる場合、拒否します。
  • ブレークポイントの行番号がFUNC行の範囲外の場合、拒否する。(foo()の場合は1~8、bar()の場合は3~5が行の範囲となります)
  • FUNCのすべての内部関数IFUNCをループする:
    • FUNCのIFUNC:ブレークポイントの行番号がIFUNCの中にある場合、拒否する。IFUNCはブレークポイントを捕捉したものと見なす。
  • FUNCのIFUNC:ブレークポイントの行番号がIFUNCの中にある場合は却下。

PCと行番号の取り扱い

内部帳簿では、duk_activationのPCフィールドは、次に実行される命令を指しています。このPCは、必ずしも報告すべき正しいものではありません。概念的には、前の命令(PC-1)がまだ実行されていることもあれば、PC-1の実行を終えて2つのオペコードの途中にいることもある。

使用する正しいPCは、文脈によって異なります。例えば、以下のような場合です:

  • スタックトレースでは、すべてのコールスタックレベルに対してPC-1が使用されます。コールスタックトップ以下の起動では、PC-1はまだ実行されている命令(コール命令)です。コールスタックトップの場合、PC-1は "offending "命令である。
  • デバッガのStatus通知では、PC-1を概念的に完了し、PCを実行しようとしているため、PCが使われる。また、ブレークポイントは、PCのオペコードが実行される前に、PCでトリガーされる。デバッガUIでは、ハイライトされた行が次に実行される行であり、まだ実行されていないことを意味する。
  • デバッガのGetCallStack PC-1は、コールスタック・トップ以下のすべてのコールスタック・レベルに対して使用されます:スタック・トレースのように、これらの呼び出し命令はまだ実行されています。ただし、コールスタックトップでは、PCはStatusと一致するように使用され、報告された行は次にどの行が実行されるかを示している。

参照: https://github.com/svaarala/duktape/issues/281

メッセージのネスト(入れ子)を避ける

次のシナリオを考えてみましょう:

  • 仮想のGetLocalVarsAndValuesリクエストを使用して、ローカル変数名と値に対するクライアントリクエストをデバッグします。
  • Duktapeはリクエストの処理を開始し、REPマーカーをストリームアウトし、その後に変数名と値をストリームアウトします。
  • 変数値の一つはゲッターで、リクエストハンドラは単純な読み込みで変数値を取得し、ゲッターを起動させます。
  • ゲッターは print() を呼び出し、デバッグクライアントに転送されます。print()`ハンドラは、印刷データを含む通知メッセージを書きます。
  • この通知はGetLocalVarsAndValuesレスポンスの途中で終了し、デバッグストリームを破損させる。

このようなネストしたデバッグメッセージは、常に避けなければなりません。これを実現するためのいくつかの方法:

  • デバッグコマンドが1つの値(値のリストではない)を扱うだけであれば、レスポンスをストリームアウトする前に、値を読み取り、安全な形式に強制します。
  • 一般的なルールとして、副作用のないデバッグ・コマンドを使用する。
  • 副作用のある安全でないプリミティブでは、(任意に長い値のリストではなく)1つの値だけを扱うデバッグコマンドを推奨します。このようなプリミティブは、応答を書き出す前に、安全に取得された無制限の値のリストをバッファリングする必要がないため、安全に実装するのが簡単です。
  • 具体的な例として、GetLocalVarsAndValuesを修正するには、以下の方法が考えられます: a. アクセサーを呼び出さないように変更する。 b. 変数名のリストだけを返すように変更し、ローカル変数の値(GetLocalVar)を取得するプリミティブを別に追加する。このプリミティブはゲッターを呼び出すことができますが、レスポンスのストリームアウトを開始する前に呼び出す必要があります。まず変数名を読み、次にパイプライン化された大きなリクエストのセットですべての変数名の読み込みを発行します。

この問題は、あちこちのいろいろなものに影響を及ぼします:

  • GC が呼び出された場合、マーク・アンド・スウィープ・コードの内部から GC 通知を発することが魅力的かもしれません。GCは値スタックを含むあらゆる操作によって容易に起動されるため、これは非常に安全ではないでしょう。

デザイン目標

このセクションでは、デバッガ設計の背後にある目標について、いくつかのメモを提供します(これは包括的なリストではありません)。

カスタムターゲットとの迅速な統合

カスタムターゲットにデバッグサポートを非常に早く、例えば1日で統合できるようにする必要があります。

  • これは現在のソリューションで達成できるはずです。ターゲットデバイスとduk_debug.jsの両方にカスタムトランスポートを実装する必要があり、その後、デバッガーのウェブUIを使用してターゲットをデバッグすることができます。

デバッグソリューションの断片化を最小限に抑える

デバッガー・アーキテクチャーは、Duktapeのデバッグ機能の向上がユーザー間で共有されるようにする必要があります。理想的には、異なる環境向けに開発されたデバッグクライアントを混在させることができる。

  • これが、デバッグAPIではなく、デバッグプロトコルを設計の基礎とした主な理由である。デバッグAPIを使用すると、すべてのユーザーが独自のデバッグプロトコルを定義する必要があり、デバッグプロトコルとその結果としてのデバッグクライアントの両方が断片化することになります。
  • この目標は、どのデバッグクライアントもどのターゲットとも対話できるようにすることで、かなりの程度達成されています。しかし、トランスポート・メカニズムを適応させる必要があるかもしれないので、完全に自動化されているわけではありません。

輸送の中立性

デバッグプロトコルは、非常に異なる環境や通信回線(Wi-Fi、Bluetooth、シリアルなど)での組み込みをサポートするために、トランスポートニュートラルであるべきです。

  • 具体的な解決策としては、信頼性の高い(TCPのような)バイトストリームを想定し、ユーザーコードで具体的なトランスポートを提供することです。

トランスポート帯域

デバッガは、低速のシリアルリンクなど、低速のトランスポートで動作する必要があります。

  • これがバイナリプロトコルが使われる理由です:圧縮をせずに合理的にコンパクトにするためです。圧縮は可能な解決策ですが、非常に低いメモリデバイスには好ましくありません(メモリオーバーヘッド)。

デバッガは高遅延トランスポート(数百ミリ秒)で動作する必要があります。

  • パイプラインは、複数のコマンドを送信することができ、ブロックのラウンドトリップ待ち時間を短縮します。
  • パイプライン化により、デバッグコマンドを小さな単純な操作から構築することができ、(同期リクエスト/リプライモデルと比較して)追加のレイテンシを最小限に抑えることができます。

ヒューマンリーダブルプロトコル

プロトコルがプレーンテキストなど、人間が読めるものであることは良いことだと思います。

  • デバッグプロトコルがバイナリであるため、現時点では実現されていません。
  • バイナリプロトコルは、テキストベースのプロトコルをパースするよりもコンパクトで、コードフットプリントが小さいため、現時点では使用されています。このような解析は、GCへの影響やその他の副作用なしに行われる必要があるため、既存のECMAScriptメカニズム(数値解析など)は必ずしもそのまま使用することができないことに注意してください。

コード・フットプリント

デバッガーのサポートは、フットプリントが大きいので、オプションにすべきです。

非常に低いメモリデバイス(例えば256kBフラッシュ)でもデバッガサポートを有効にすることができるはずです。

  • 現時点では、デバッガーサポートのための追加コードフットプリントは約15〜20kBです。

メモリ(RAM)フットプリントと最小限のチャーン(乗り換え)

デバッガの実装は、デバッグ・コマンド自体が必要とする量の上に、最小限のRAMを消費する必要があります。

  • 低メモリデバイスの場合、可変アロケーションよりも固定アロケーションが望ましい。

デバッガ・コマンドは、Duktapeの内部状態を乱すことを避ける必要があります。例えば、デバッグ・コマンドがDuktapeヒープのダンプを要求した場合、そのコマンドはレスポンスのシリアライズ中にヒープに変更を与えないようにしなければなりません。具体的には、次のような意味です:

  • GCを引き起こす可能性のあるメモリ割り当てを行うことなく、デバッグメッセージの読み書きが可能である必要があります。これは、特に、値スタックに値をプッシュすることや、文字列をインターリングすることを除外するものです。メモリ確保は、アロケーションコールバックの生の呼び出しを使用して行うことができますが、メモリ確保を完全に避けることができることが望ましいです。
  • なお、すべてのデバッグコマンドを副作用なく実装することは必須条件ではありません。例えば、変数の読み出しでは、ゲッターを呼び出したり、副作用のある内部機構を使用したりすることがあります。必要であれば、副作用のないデバッグ・コマンドを書くことが可能であることが目標である。

インバウンドメッセージの解析やアウトバウンドメッセージの構築のために、大きく可変なサイズのバッファは避けるべきです。これらは、低メモリデバイスでは非常に問題となる。

  • この目標は、デバッグプロトコルがストリームトランスポートを使用する重要な理由である。ストリームトランスポートは、例えばヒープ全体を、可変サイズの出力バッファリングなしでシリアライズすることを可能にします。
  • この目標は、デバッグプロトコルがJSONなどではなくバイナリである理由の1つでもあります:JSONパーシングは、現在のパーサーを使用した場合、大幅なメモリ消費を引き起こします。デバッグのために別のパーサーを追加するのは無駄なことです。

パフォーマンス

デバッガが添付されていない(しかしデバッガサポートはコンパイルされている)場合、パフォーマンスは可能な限り正常に近いものであるべきです。

デバッガが接続されているが、アクティブなブレークポイントがない場合、パフォーマンスは可能な限り正常に近くなるはずです。

アクティブブレークポイントの性能は重要ではありませんが、遅いターゲットでは、タイミングに敏感なアプリケーションがデバッグ時に正しく動作する可能性があるため、重要です。

その他のデザインノート

雑多な問題や却下された代替案など、いくつかのデザインメモ。

デバッグAPIの代わりにデバッグコマンドを使用する

デバッグプロトコルの代わりに、Duktapeはユーザーコードが独自にデバッガを実装できるように、APIプリミティブのセットを提供することができます。これにはいくつかの欠点があります:

  • Duktapeの内部へ深くアクセスできる新しいパブリックAPIプリミティブがたくさん必要です。このようなAPIは、将来的に大きなメンテナンスの問題となるでしょう。Duktapeの内部が変更された場合、古いAPIの約束を守らなければならないからです。デバッグ・プロトコルは、より効果的に内部の詳細を隠蔽することができます。
  • デバッグを必要とするすべてのユーザー・アプリケーションは、独自のデバッグ・プロトコルを実装する必要があります。 デバッガーになります。Duktapeデバッガーの統合は、それぞれ異なるでしょう。

エンベデッドインタープリタであることの影響

組み込み型ということは、例えばJVMのような標準的な起動がないことを意味します。デバッガは実行中のインスタンスに接続する必要があり、インスタンスの起動はユーザー次第である。また、ソースコードに簡単にアクセスできないこともあります。ソースコードのロード方法はユーザー次第で、ソースコードの一部はCコードから、おそらくプログラム的に与えられています。

デバッガをいつアタッチするかは、アプリケーション次第です。例えば、起動時にデバッガを付ける(「再起動してデバッグ」モード)とか、実行時にデバッガを付けた時だけ付けるとか。

パケットベースプロトコル

デバッグトランスポートは、区切られたデバッグパケットをベースにすることができます。V8とSpidermonkeyのデバッグプロトコルは、どちらも(JSON)パケットベースです。

パケットベースのプロトコルでは、インバウンドメッセージは処理するためにメモリに存在する必要があります。同様に、アウトバウンドメッセージは、送信前にフルパケットとして形成されます。これは、デバッグパケットの最大サイズを制限することが困難であるため、低メモリデバイスではうまく機能しません:

  • 例えば、デバッグパケットに含まれる文字列が1つだけだったとしても(おそらくevalの結果)、その文字列のサイズは大きく変化する可能性がある。デバッグパケットのサイズに上限があると、メモリに収まるような値がデバッグプロトコルで送れないという事態が発生しやすくなります。
  • 断片化された読み込みを行うことで、この問題を軽減することができます。つまり、デバッグプロトコルは、デバッグクライアントが文字列をチャンクで読み込むことを許可しています。これは文字列のライフサイクルの問題があり、このような断片化プロトコルは、実際には粗雑な方法でストリームトランスポートをエミュレートしています。
  • 同様のアプローチは、オブジェクト値のシリアライズや、潜在的に他の多くのデバッグコマンドにも必要であり、プロトコル設計の観点からは非常に厄介です。

リクエスト/レスポンスフレームを使用しないストリームプロトコル

デバッグプロトコルは、リクエスト/レスポンスフレームを持たないストリームプロトコルである可能性もあります。これは、どちらかのパーティがロックステップなしでメッセージを開始する可能性がある場合、うまく機能しません。例えば、デバッグクライアントがリクエストを送信し、ターゲットが通知を送信した場合、デバッグクライアントは受信したバイトが応答ではなく、無関係な通知であることをどのように知ることができるでしょうか?

少なくともレスポンスと他のメッセージを分離するために、何らかのフレーミングが必要である。

パイプラインと非同期メッセージの比較

現在の設計では、リクエストのパイプライン化を可能にしています。各リクエストは1つの返信(またはエラー)を持ち、リクエストは決して並べ替えられません。このモデルでは、リクエスト/リプライの識別子は必要ありません。

別の設計としては、各パーティが任意の順序で(非同期で)受信コマンドに対する応答を送信できるようにすることが考えられます。これは、ある操作に時間がかかり、バックグラウンドで処理できる一方で、より緊急性の高い操作をその間に処理できる場合に有効である。

実際には、特にデバッグターゲットでの実装は困難であり、より多くの状態追跡が必要になる。また、完了順序が保証されないため、(パイプライン化に比べて)複数のリクエストを送信することが難しくなる。

型にはまらないデバッグメッセージのエンコーディング

つまり、デバッグクライアントとターゲットの両方が、メッセージが持つべき正確なデータを知っているため、例えば整数や文字列といった値をタグ付けする必要がないのです。

これは効率的ですが、互換性のある方法で拡張することは困難です。その代わり、デバッグプロトコルはマイナーチェンジごとにハードバージョンアップが必要になり、デバッグクライアントはすべてのプロトコルの変種をサポートする必要があります。しかし、デバッグクライアントがバージョンを認識する必要があるため、これは必ずしもショーストッパーではありません。

可変長整数符号化

デバッグ・プロトコルは、大小の整数を大量にやり取りする。拡張UTF-8エンコーディングが最初に使われましたが、これはDuktapeの他の可変長整数エンコーディングと整合性があります。

しかし、現在のタグ初期バイト(IB)が追加されると、タグバイトを使って小さな整数を符号化し、大きな整数のバイト長を符号化することが非常に自然になった。この表現は、実はCBOR: https://tools.ietf.org/html/rfc7049とよく似ています。

アクセサーとプロキシ vs. 変数の取得・設定

  • セッターやゲッターをトリガーすることは望ましくないかもしれません。
  • Object.getOwnPropertyDescriptor()のような値を返し、必要に応じてデバッグクライアントがゲッターを呼び出すことができるようにするとか?(ヒープウォーキングは現在同様の機能を提供しています。)
  • プロキシとターゲットに別々にアクセスする?

その他のデバッガ実装

概要

V8とSpidermonkeyは、プロトコルの多くがJSONでフォーマットされたパケットベースのデバッグプロトコルを使用しています。これは非常に直感的なアプローチですが、Duktapeでは、JSONを使用することによるメモリ不足を回避し、メモリ内に完全なデバッグメッセージを形成することが問題となる非常に低いメモリデバイスをより良くサポートするために、ストリームベースのバイナリプロトコルを使用しています。

クローム/V8

Chrome/V8はパケットベースのデバッグプロトコルを採用しており、各パケットはJSONメッセージとなっています:

こちらもご覧ください:

Firefox

Mozillaはパケットベースのデバッグプロトコルを使用しており、パケットはJSONまたはバイナリブロブのいずれかです。ストリームにマッピングすることができます:

こちらもご覧ください:

Eclipse

Eclipseデバッガは、Duktapeデバッガプロトコルを使用して実装することができます。そのためのリソースをいくつか紹介します:

既知の問題

Valgrind の未初期化バイトの警告

DumpHeapを行う際に、以下のようになることがあります:

xml
==17318== Syscall param write(buf) points to uninitialised byte(s)
==17318==    at 0x5466700: __write_nocancel (syscall-template.S:81)
==17318==    by 0x427ADA: duk_trans_socket_write_cb (duk_trans_socket_unix.c:237)
==17318==    by 0x403538: duk_debug_write_bytes.isra.11 (duk_debugger.c:379)
==17318==    by 0x4036AC: duk_debug_write_strbuf (duk_debugger.c:463)
[...]

アンパックされたduk_tvalを使用している場合、ある値がduk_tvalに書き込まれたとき、duk_tvalの全バイトが必ずしもセットされるとは限りません。これは、Duktapeが未初期化バイトを読み込んだり、使用したりすることは通常ないため、安全上の問題はない。しかし、コンパイルされた関数のデータ領域にある未初期化バイトは、そのままDumpHeapに書き出されるため、上記のようなvalgrindの不満が発生します(害はありません)。

今後の課題

エラー処理

デバッグコードにエラーハンドリングラッパーを追加する。例えば、メモリ不足になった場合、回復策として自動的にデタッチする?

現在安全でない動作は、内部エラー(メモリ不足など)や、例えばGetVarによるゲッターエラーが引き金となる場合があります。

チェックされた実行のための高速なpc-to-line

チェック実行中に、行の遷移を正確に追跡できるように、現在のPCの行番号を把握する必要があります。現在、PCから行へのビットストリームは毎回ステートレスで参照されるため、時間がかかります(ただし、チェックされた実行、つまり現在の関数に対してアクティブなブレークポイントがある場合にのみ影響があります)。

これを高速化する方法はいくつかあります:

  • PCからラインへの変換状態をキャッシュする。PCが1つ増えたら、ほとんどの場合、ビットストリームから1行のデルタをデコードすればよく、非常に効率的でデータフォーマットの変更も必要ありません。
  • チェックされた実行に入るとき、ルックアップが単純な配列ルックアップとして実行できるように、アンパックされたpc-to-line配列を作成する。
  • デバッグが有効な場合は、一般的にプレーン配列としてpc-to-line変換情報を格納します。これは、デバッガが接続されていない場合(ただし、Duktapeデバッガサポートはコンパイルされている)でも、すべての関数に対してメモリフットプリントの影響があるため、この方法はあまり望ましくありません。
  • 明示的なライン遷移オペコードを出力する。これは、デバッガが接続されていない場合でも、メモリとパフォーマンスに影響を与えるので、このアプローチもあまり好ましくはありません。

コンパイラの行番号精度を向上

ECMAScriptコンパイラは、出力されるバイトコードに行番号を割り当てますが、その際、必ずしも完璧な仕事をするわけではありません。あるステートメントの行番号が1つずれることがあり(前のステートメントと一致する)、デバッガUIでおかしなことになるケースがいくつかあります。

根本的な問題は、アクティブなトークンが「前のトークン」スロットと「現在のトークン」スロットにあるとき、コンパイラがバイトコードのオペコードを出力することです。式の解析では通常、アクティブ・トークンは前のトークン・スロットにあり、文の解析(特に初期キーワードの解析)ではアクティブ・トークンは現在のトークン・スロットにあります。これは、正しく修正するためにいくつかの手直しが必要です。

ソースコード

ソースコードの扱いはDuktapeの範囲外であり、実行中の関数の「fileName」プロパティから適切なソースファイルを探し出すことができると考えています。

将来の選択肢はたくさんあります:

  • ターゲットデバイスからダウンロードする(コードが最初に読み込まれた場所と同じ)。
  • デバッグモードでコンパイルする際にソースを保存し、メモリへの影響を軽減するために些細な圧縮を使用することができる。
  • ターゲット上で計算されたハッシュを使用してソースコードのテキストを識別し、対応するソースをより確実に見つけることができます。

ソースマップ

Javascriptのコードを最小化することはよくあることです。その過程で行番号の情報が失われることが多く、様々な理由でデバッグが困難なコードになります:

  • ソースコードの読みやすさが悪い
  • ファイル/ラインを対象としたブレークポイントの仕組みが非常に貧弱

ソースマップは、オリジナルのライン番号情報を記録しています:

もしDuktapeがソースマップをサポートしていれば、コンパイル時にソースマップを考慮し、関数pc-to-lineマッピングで元の未消化のソースコードを参照できるため、よりデバッガーフレンドリーなものとなるでしょう。

より柔軟な一時停止が可能

一時停止のためのさまざまなトリガーを追加することができます:

  • 機能入力/終了時のポーズ
  • 次の発言でポーズ
  • 歩留まり/再開時のポーズ
  • 実行タイムアウト時のポーズ

よりフレキシブルなステッピング

追加のステッピングパラメーターを実装することも可能です:

  • PCを1台ずつステップアップ
  • N個のバイトコード命令に対するステップ
  • 約Nミリ秒のステップ

ローカル変数リストで動的に宣言された変数

GetLocalsが返すローカル変数リストには、動的に宣言された変数や、関数全体より小さなスコープを持つ変数は含まれません:

js
function test() {
    var foo = 123;   // 'foo' is included

    eval('var bar = 321');  // 'bar' is not included

    try {
        throw 'foo';
    } catch (e) {
        // 'e' is not included
    }
}

これは、ローカルに動的変数も含まれるように修正されるべきです。これは特にtry-catchで重要です。

Evalコマンドは動的変数の読み書きもできるので、現状ではEvalを使うのが回避策となります。例えば、catch句の中で、Eval "e" を使って、キャッチしたエラーを読み込む。

式に依存するブレークポイント

式が真理値として評価されたときに一時停止する。

表情を見る

ウォッチ式は現在、デバッグクライアントでEvalコマンドを使用して実装されています。

例えば、デバッガーのWeb UIでは、1つの式に対して自動evalが実装されています。この式は、Duktapeが一時停止状態になったときに自動的に評価されます。これは、複数のウォッチ式に対して簡単に拡張することができます。

内部イベントの通知

など、社内の面白いイベントが発生したら、通知を送る:

  • 通常のGC
  • エマージェンシーGC
  • スレッド作成
  • スレッド破壊
  • 実行タイムアウト

これらは非常に慎重に実装する必要があります。例えば、デバッグコマンド(例えば、"get locals")に応答している最中にGCが起動した場合、GC通知はマーク&スイープコードからインラインで送信することはできず、"get locals "応答の途中に表示される可能性があります。その代わりに、イベントはフラグを立てるか、カウンターに基づくか、キューに入れる必要があります。

新コマンドやコマンドの改良の可能性

  • より包括的なコールスタックの検査、少なくともスタックトレースが提供するものと同程度のもの
  • エラーでレジュームする、つまりインジェクトエラーする
  • ヒープ内のスレッドを列挙する
  • ヒープ内の全オブジェクトを列挙する
  • PutVarの成功/失敗を示すステータス。
  • PutVarのエラー処理
  • GetVarで副作用(ゲッター呼び出し)を回避する。

構造化された価値観をダイレクトにサポート

現在の duk_tval 値と dvalue の間のマッピングは機能しますが、構造化された型を表現することはできません。例えば、グローバル変数を設定する仮想的なデバッグコマンドが引数 value を dvalue として読み込んだ場合、[1,2,3] のような値をグローバル変数に書き込むことはできません。

これはもちろん、引数に対して eval() を実行するか、値をJSONとして表現することで解決できます(これは多かれ少なかれ同じことです)。

別の方法として、構造化された値を直接dvaluesで表現するサポートを追加することで、Cコードがaを実行する際に、dvaluesを使用することができます:

c
duk_debug_read_tval(thr);

任意に複雑なオブジェクトの値(おそらく任意のオブジェクトグラフも)をデコードして、値スタックにプッシュすることができます。

ヒープウォーキングのサポートにより、構造化データがDuktapeのヒープに存在する、または配置できる場合、この問題を緩和することができます。

ヒープダンプビューア

DumpHeapコマンドは、すべてのヒープオブジェクトのスナップショットを提供し、デバッガーのWeb UIはJSONダンプに変換する。ダンプのビューアがあれば、オブジェクト・グラフを走査して、文字列や値を探すのが簡単になるね。

Eclipse デバッガ

Eclipseのデバッガは、組み込み開発用のIDEとして非常に人気があるので、非常に便利です。Visual Studio Codeとの統合はすでにあります。

アタッチ/デタッチ時のブレークポイント処理

現在、ブレークポイントのリストはアタッチやデタッチではクリアされないので、デタッチした後に再アタッチしても古いブレークポイントが設定されたままになっています。デバッグクライアントは、アタッチ時にすべてのブレークポイントを削除することができますが、アタッチまたはデタッチのどちらかでブレークポイントを削除したほうがすっきりしますよ。

ファスティックの状態を示す

Fastintで動作するコードをデバッグする際、ある値が内部的にFastintとして表現されているのか、完全なIEEE doubleとして表現されているのかを確認することができれば便利です。現在、この情報はプロトコルによって伝達されず、すべてのfastintは他の数値と同じように表示されます。

バッファオブジェクトのサポート

バッファオブジェクトの内容を見やすくする(プレーンバッファのように)。

トランスポートの状態をチェックするためのコールバックを別に用意

Duktapeが実行状態にあるとき(一時停止していないとき)、Duktapeは呼び出しのみを行います:

  • peekコールバックは、読むべきものがあるかどうかを定期的に確認する。今、peekでトランスポートのデタッチ/エラーを示す方法はない。
  • 書き込みコールバックは、Status通知送信の副次的な効果として、定期的に行われる。これは、実行中の状態で壊れたトランスポートを検出するための、現在の主なメカニズムである。もしStatus notifiesが削除されたら、Duktapeは他の何かがデバッグトランスポートへの書き込みを促さない限り、トランスポートの破損に気づかないだろう。

どちらかを用意した方がすっきりするかもしれません:

  • トランスポートの状態を明示的にチェックするコールバックで、おそらくエラーメッセージを表示することもできるようにする。
  • トランスポートが壊れていることを示すために、ユーザーコードが積極的に Duktape を呼び出せるようにします(duk_debugger_detach()を呼ぶ以上のこと)。

しかし、いくつかのトランスポートでは、実際に書き込みを試みることなく、トランスポートステータス情報を取得することができないかもしれない。これは、例えば、トランスポートの性質や基礎となるプラットフォーム API の制限によって引き起こされるかもしれない。したがって、このようなトランスポートステータスコールバックはオプションで なければならず、このような場合にトランスポートエラーを検出するために、 定期的な書き込み(何もなければkeepalive)を確保することがまだ必要かもしれない。

ERRメッセージをプログラム文字列エラーコードで拡張する

現在のエラーメッセージは、以下のような形になっています:

xml
ERR <error number> <message> EOM

番号空間は、モジュール的に管理するのが厄介で、AppRequestメッセージなどに有用なアプリケーション固有のエラーにはうまく機能しない。エラー形式を拡張する:

xml
ERR <error number> <error string code> <message> EOM

文字列コードは、"NOT_FOUND"のような全角の規約に従うことができる:

xml
ERR 3 "NOT_FOUND" "breakpoint not found" EOM

文字列のエラーコードは、番号順のような矛盾がなく、拡張が容易です。

ゲッターの起動を回避するためにコールスタックのエントリーを変更する

ヒープウォーキングを導入すれば、コールスタック変数のダンプによって、データとアクセッサのプロパティを区別するための必要な情報を提供し、デバッグクライアントがゲッターを呼び出すかどうかを決定できるようになります。

コールスタック・プリミティブは、GetHeapObjInfoコマンドと一致するフォーマットで変数リストを返すことも可能である。

GetBytecodeをオブジェクト検査に置き換える

GetBytecodeコマンドは、現在の関数への参照を提供し、オブジェクト検査を使用して、GetBytecode㊤で現在返されているバイトコード・データ、つまりバイトコード、定数などを取得することで削除することができます。

一時停止状態でのガベージコレクションの動作を改善

現在の動作:一時停止中に発生したゴミ(refzeroや参照ループ内のオブジェクト)は、いずれもヒープに残され、最終的にはマークアンドスイープで回収されます。

様々な改良が可能です。https://github.com/svaarala/duktape/pull/617 の議論を参照してください。