The background of XCube Delegate mechanism;ja
From xoopscube
Contents |
The background of XCube_Delegate mechanism
XOOPS Cube コアを利用した開発ではデリゲートを上手に使用することで、モジュール間連携の可能性や、プリロードを利用した 1 File Hacking の幅を広げることができます。
プログラミング的な観点からみると、デリゲートはオーバーライドと同じく、メソッドを抽象化する仕様です。デリゲートを用いてメソッドを抽象化するか、オーバーライドしやすい(template patternのような)クラス設計をとるかは、利用シーンで判断することになります。
本ドキュメントは、より低レベルな視点からオーバーライドとデリゲート(コールバック)をとらえ、メソッドの抽象化手法を使い分けるひとつ判断材料を習得することを目的にしています。また、プロトタイプベースオブジェクト指向言語にも少し触れたいと思います。
はじめに
XOOPS Cube というウェブアプリケーションは、エンドユーザーが組み合わせるモジュールによって最終的な形が変化します。そして、それは実行時に結合しコンパイルされ、実行されます。
プログラマの観点からみると、これはかなり特殊なコンセプトです。現代のプログラムにおいて、プログラムのモジュール化を試みたことがない人はいないでしょう。会社やプロジェクトで連綿と受け継がれてきたモジュール、あるいはミドルウェア会社より購入したモジュール、さらにはオープンソースプロジェクトのモジュールを入手し、それを"縫合"して私達はアプリケーションを仕上げてゆきます。
オブジェクト指向プログラミング言語は「クラスライブラリ」という概念をもたらし、オープンソース時代は完全なソースコードが入手可能なモジュールの登場を促しました。それにより、クラス継承を用いたモジュールに対する差分プログラミングや、モジュールのブラックボックス化が過去の話となったことで直接改変も容易になりました。
いずれにせよ、プログラマはモジュール化されたプログラムをつなぎあわせるためにプログラムを書き、コンパイルし、結合して、アプリケーションを完成させました。そこにデリゲートと類似した(あるいはデリゲートそのもの)メカニズムはほぼ必須ですが、まったく使用しなくてもアプリケーション開発を遂行できるケースがあることもまた事実です。
開発者によってはデリゲートやコールバックはあまり経験したことがない概念かもしれません。 XCube_Delegate の強力さはすでに多くの XOOPS Cub プログラマに伝わっていますが、デリゲートの持つ関数抽象化……すなわち間接条件によるプログラムジャンプのメカニズムを実践する方法は他にもあるため、「デリゲートの守備範囲がいまいち分からない」という人も少なくないと思われます。
- デリゲートを公開することが連携メカニズムとして正しいかどうか自信がない(オーバーライドではだめなのか?)
- すべてのメソッドをデリゲート化したい誘惑にかられる
- デリゲートマネージャによって特性が変化する点がつかみにくい
デリゲートはオーバーライドと同様にメソッドを抽象化するメカニズムとして利用できます。デリゲートも、クラス継承を用いたオーバーライドや拡張も、 XOOPS Cube では重要です。デリゲートの守備範囲をつかむためにも、まずはオーバーライドとの区別をつけることが大切になるでしょう。
デリゲートの長所のひとつは、このメカニズムがプログラムの根本原理に基づいた最も標準的なテクニックのうちのひとつであるため、時間をかけてデリゲートを理解する価値があることです。恐らく XOOPS Cube が途絶えるより長く技術者の人生は続きますが、デリゲートを理解することはその後の技術者人生で必ず役に立ちます。
このドキュメントはデリゲートとオーバーライドの基底動作である間接ジャンプの原理について理解しながら、デリゲートへの理解を深めることを目的とした技術者向けの文書です。
命令ポインタ
PHPは豊富な関数を内蔵した高級な言語ですが、コンピュータプログラムが動作するために必要な挙動はわずか数種類に限定されます。ネイティブプログラムは最終的にマシン語にコンパイルされ、実行時にメモリ上に配置されます。同様にスクリプト言語は最終的にバイトコードにコンパイルされ、実行時に仮想マシン環境(VM)のメモリ上に配置されます。
マシン語やバイトコードは単純な命令がメモリ上に順列した状態でロードされます。バイトコードは比較的多様な命令を持つ傾向にありますが、いずれにせよあなたが普段目にするソースコードとは形態が異なる原始的な命令の並びになっています。
プログラムの実行は、この命令の並びを先頭から順番に実行していくことで行われます。
現在実行中の命令の位置をしめす情報として、 PC (プログラムカウンタ)と呼ばれるものがあります。プロセッサによっては IP (インストラクションポインタ:命令ポインタ)と呼ばれます。この命令ポインタという呼び方は言い得て妙なので、本ドキュメントでは命令ポインタという標記を用います。 Internet Protocol (IP) と混同しないよう注意してください。
CPU もしくは VM は、命令ポインタが指している命令を実行した後、次の命令を指すように居場所を移動させます。これを繰り返すことでプログラムは実行されていきます。
原則的に命令ポインタは前進しかしません。最後の命令を実行するとプログラムは実行を終了することになります。
直接ジャンプ
プログラムのもっとも単純な再利用の概念としてサブルーティン(関数)があります。サブルーティンはメインルーティン内で呼び出すことがない限り実行されません。
通常、命令ポインタはメインルーティンの最初の命令の位置に初期化され、最後の命令の位置へ向かって前進します。サブルーティンは、メインルーティンの巡回では訪問されない位置に配置されており、前進の仕組みだけではサブルーティンの命令に、命令ポインタがポイントを当てることはありません。
サブルーティンへ"飛ぶ"ために、コンパイラ(もしくはバイトコードコンパイラ)は、ソースコード上で関数を呼び出している処理を、命令ポインタのアドレス書き換え命令に翻訳します。
この地点に命令ポインタが到達すると、命令ポインタの指している住所が、サブルーティンの先頭アドレスに書き換られます。そして次の命令を指すために移動する代わりに、本来到達するはずのなかったサブルーティン内の先頭の命令をポイントするようになります。
このように、プログラムがロードされた時点で決定された住所へジャンプする処理を直接ジャンプと呼びます。
コールスタック
コールスタックはデリゲートの理解には不要ですが、サブルーティンにジャンプした命令ポインタがどうやってメインルーティンに戻るのか知りたい人はこの項目を読んでください。
先ほどはジャンプの説明をしましたが、その内容は一方通行的なものでした。
実際にはサブルーティンのジャンプでは、サブルーティンの命令をすべて実行するか、 return にあたる命令によって、命令ポインタはサブルーティンにジャンプした元のアドレスに戻り、処理を継続しなければなりません。(Jump Sub Routine:ジャンプサブルーティン)
そこで実際の実行では、プロセッサもしくは仮想マシンは、ジャンプするために命令ポインタの値を書き換る前に、元の命令ポインタの値(リターンアドレス)などをスタックフレームと呼ばれる一連の情報にまとめてコールスタックに退避させておきます。そしてリターン時に、コールスタックの先頭に保管されているスタックフレームのリターンアドレスで命令ポインタの値を書き戻します。これで命令ポインタはサブルーティンの呼び出し元に戻って処理を継続することができます。
この仕組みによって、サブルーティン先で別のサブルーティンにジャンプする流れや、サブルーティン内部で同じサブルーティンを呼び出す再帰呼び出しを実現しています。ただし、なんらかのミスでリターンが行われずに繰り返しサブルーティン呼び出しが行われる等で、コールスタックの容量を越えてしまうと、スタックオーバーフローと呼ばれる障害を引き起こすことになります。
コールスタックは機構上必要なスタック領域ですが、デバッガ上で関数の呼び出し履歴を一望できるなど、開発者にとっても身近な存在になっています。たとえば複数個所から呼び出される低レベル汎用関数内で望ましくない入力値によるエラーが発生した場合、コールスタックを辿ることで問題の呼び出し元を簡単に突き止めることができます。
メソッド
このようにサブルーティンはメインルーティンの経路上から離れた位置に配置され、固定のアドレスを持っています。これはクラスのメンバ関数……いわゆる「メソッド」でも同じことが言えます。
メソッドには動的なオブジェクトメソッドと静的なクラスメソッドのニ種類が存在しますが、ジャンプ制御の理屈に大きな違いはありません。たとえば C++ などの言語では、オブジェクトメソッドと静的なメソッドの違いは、第一パラメータに this を隠し持つかどうかの違いでしかありません。
したがって動的メソッドのアドレスも固定的に求めることができ、直接ジャンプで呼び出すことが可能できます。
間接ジャンプ
しかし、プロセッサや仮想マシンの直接ジャンプ命令だけでは、効率のよいプログラムを書くことはできません。直接ジャンプの応用に間接ジャンプという技術があります。
間接ジャンプとは、あらかじめ決められた値(固定アドレス)で命令ポインタを書き換るのではなく、計算によって命令ポインタの値を決定する方法です。
直接ジャンプが「何番地に飛べ!」という命令なら、間接ジャンプは「メモに書いてある住所へ飛べ!」という命令になります。メモに書かれている住所を書き換ることで、プログラムの実行中に、同じジャンプ命令でも飛び先を変えることができるようになります。
この技術の使い道がもっとも分かりやすいのがコールバックでしょう。
たとえばある低水準のマイクロカーネル上でプログラムをしていたとしましょう。このマイクロカーネルは、リセットボタンを押されると、A番地に書かれているアドレスへジャンプする間接ジャンプ命令がセットされていたとします。
A番地にはデフォルトで「リセット関数」のアドレスが書きこまれているため、リセットボタンを押すと「リセット関数」が実行されることになります。
さてあなたは、あなたのプログラムのために、リセットボタンが押された場合に、デフォルトのリセット関数を実行する前に自分の独自の終了プログラムを実行しなければなりません。どうやって実現しますか?
もしリセットボタンを押した時、リセット関数に直接ジャンプするのであれば、リセット時に処理を割り込ませることはできません。しかし、間接ジャンプであれば可能です。リセット時に実行される間接ジャンプのアドレス決定情報が書き込まれているA番地の値を、自分の関数のアドレスで書き換えてしまえばよいのです。
一般的にこれをフックと呼びます。
命令が書き換られているわけではないことに注目してください。本来のプログラムが間接ジャンプを持っており、そこを命令ポインタが通過していったことに違いはありません。命令ポインタの新しいアドレスを決定する情報を書き換る(変数的な要因が介在する)ことで、飛び先を変更しています。
デリゲート
デリゲートは典型的な間接ジャンプであり、典型的なフック処理です。私達がデリゲートに add する行為は、デリゲートの間接ジャンプのジャンプ先のアドレスを書き込んでいる行為に相当します。
また自分のクラスにデリゲートを備えて公開することは、アドレスの追加・書き換えが可能な間接ジャンプメカニズムを、外部に提供することに他なりません。
仮想関数とポリモーフィズム
ここまでくれば、オーバーライドとデリゲートの本質的な類似と、相違点を理解するまであと一歩といったところです。
「メソッド」のセクションで説明したように、オブジェクトメソッドも固定アドレスを持つ関数です。しかし、実際に私達がオブジェクト指向プログラミングを行う場合は、メソッドを抽象化することができ、同じ名前のメソッドを呼び出しても、別々のメソッドを実行することができます。
これをオブジェクト指向プログラミングの世界では「ポリモーフィズム:多態性」と呼んでいます。しかし、実のところこれは単に間接ジャンプ命令の応用のひとつでしかありません。
C++ がもっともイメージのつかみ易い言語です。抽象化していない関数は多態性を持たないため、直接ジャンプ命令にコンパイルされます。図をご覧ください。仮想関数は多態性を持ち、 obj インスタンスの foo() メソッド A::foo() を呼び出したとき、 B::foo() へジャンプするか C::foo() へジャンプするか、コンパイルの段階で決定することはできません。
プログラムの実行時に動的にジャンプ先を決定するには間接ジャンプ命令を使うという理屈を思い出してください。実は、 C++ ではインスタンスに vtable と呼ばれる特別なルックアップテーブルを持たせています。 A::foo() では、このルックアップテーブルから自分がジャンプすべきアドレスを読み出して、間接ジャンプを行っています。
例えば……図を見てください。class B で作成されたインスタンスの vtable には B::foo() のアドレスが書き込まれますので、 obj->foo() (=A::foo())を通過すると、命令ポインタは B::foo() にジャンプします。また class C で作成されたインスタンスの vtable には C::foo() のアドレスが書き込まれますので、 obj->foo() は C::foo() にジャンプすることになります。
もしこのルックアップテーブルに任意の関数のアドレスを書き込むことができれば、リセットボタンの振る舞いを変えたときのように、フックをすることが可能になります。はい、オーバーライドも機構的には間接ジャンプであり、要するにフックをかけているということです。
ただし、この vtable はデリゲートと異なり、任意の値で書き換えることはできず、インスタンスを生成する最初の一回で書き込まれた値を、インスタンス消滅まで使い続けることになります。
スクリプト言語である PHP の仮想マシン環境の挙動は、ここで説明したものと異なりますが、基本的な考え方は同じです。インスタンスは、メソッドが呼び出されたときに命令ポインタをジャンプさせるアドレスを書き込んだルックアップテーブルを備えています。インスタンスを生成する際に、ルックアップテーブルを準備することで、多態性が実現するのです。
デリゲートとの違い
さて、デリゲートは、アドレス書き換え可能な間接ジャンプメカニズムを備えることが目的でした。間接ジャンプですから、メソッドは抽象化されています。
一方、オーバーライドも同じく(ルックアップテーブルを用いた)間接ジャンプメカニズムですから、メソッドは抽象化されています。デリゲートとの違いは、間接ジャンプ先となるアドレスを保持しているルックアップテーブルが不可視で、インスタンスの生成後にこの情報を書き換ることができない点です。高級言語のプログラマが干渉できないレベルでルックアップテーブルが作成され、一切手出しができません。
デリゲートは生成後のインスタンスのジャンプ先アドレスを書き換ることができますが、オーバーライドはインスタンス生成時に作られるルックアップテーブルがすべてです。
使い分けるポイント
以上を踏まえると、オーバーライドとデリゲートの使い分けが見えてくるでしょう。
オーバーライドを用いたオブジェクト指向プログラミングのテクニックのひとつに template pattern と呼ばれるものがあります。このテクニックについて考えてみましょう。
template pattern は、一連の処理の中で交換性を用たせたい部分を仮想関数で実装し、サブクラスでその仮想関数をオーバーライドすることで挙動をカスタマイズする技法です。上で説明したように、これも単なるフックでしかありませんが、私達が普段それを意識することはありません。
しかし、オーバーライドですから、肝心の仮想関数をのっとるには、インスタンスの生成に関わらなければなりません。もし私達のプログラムがインスタンス obj を取得できるが、インスタンス obj の生成に関わることができないのであれば、生成後にルックアップテーブルを書き換る手段が存在しない以上、 obj の振る舞いをカスタマイズすることは出来ないのです。
もし仮想関数がデリゲートによってもたらされたものであれば、 obj を取得した後に、間接ジャンプのジャンプ先アドレス値を書き換ることで、振る舞いを変えることが出来ます。
つまり XOOPS Cube のように部分的なプログラムしか行わないシステム上では、いかにオブジェクト指向言語の特性を生かして設計しようと、オーバーライドという間接ジャンプ機構だけでは十分とはいえないのです。インスタンスの生成に関わらずにメソッドの多態性を実現するデリゲートのような仕組みが重宝されるわけです。
間接ジャンプとデリゲート
デリゲートの本質的な挙動は間接ジャンプですから、オーバーライドの亜種として使用するほかに、間接ジャンプが有効な様々な局面で役立ちます。メソッド抽象化に次いで使用されているものとしては、イベントが挙げられます。イベントも結局は間接ジャンプ命令でしかなく、デリゲートで行っていること完全に同じ処理内容を持っています。
そのため XOOPS Cube ではイベントとデリゲートをとくに異なる存在として取り扱っていません。というより、私達の仕様にイベントという言葉はありません。ここまで読み終えた方であれば、イベントとデリゲートを別々に定義する理由がないことが理解できると思います。
イベントとデリゲートは目的の違いであり、コアの与える機能はデリゲートだけで十分です。
最後に
間接ジャンプはプログラミングに欠かせない存在です。言語がアセンブリから高級言語に発展するにつれ、関数ポインタ、仮想関数(オーバーライド)、ファンクタ、デリゲートなど、より高級言語にマッチする形で扱えるようになりました。そして今日も私達プログラマの開発を支えてくれています。
XCube_Delegate は「統一的なコールバック手続き」と表現される仕様です。 PHP 環境では"書き換え可能な間接ジャンプ"の方法がいくつかあり、型として定義されておらず、その実装はプログラマによってまちまちです。 XOOPS Cube はモジュールで構築されるアプリケーションですので、そのやり方を統一することにはメリットがあります。
これは決してハイレベルな仕組みではなく、むしろプリミティブなプログラムの挙動に基づいていることを忘れないでください。
オーバーライドとデリゲートの感覚の違いを掴むのに、プロトタイプベースのオブジェクト指向言語はうってつけかもしれません。プロトタイプベースのオブジェクト指向言語にはクラス定義の概念がなく、メソッドの追加やオーバーライドを生成後に行うことができます。このような言語では XCube_Delegate のような実装のデリゲートは必要とされません。
