ソフトウェアコンポーネントの作り方


ここではesオペレーティングシステムでソフトウェアコンポーネントを作成する方法について説明します。

コンポーネントソフトウェア

オペレーティングシステムやアプリケーションプログラムを構築するときに、ひとつの巨大なプログラムとして構築するのではなくて、ソフトウェアの部品『ソフトウェアコンポーネント』を組み合わせてそれらを構築しようというのがコンポーネントソフトウェアの基本的な考え方です。

さまざまなライブラリをリンクしてアプリケーションをビルドするという方法には、名前が衝突することがあったり、ライブラリの提供者ごとに命名規則や異なるコーディング規約を用いていたりするため、さまざまなベンダーのライブラリを多数組み合わせてアプリケーションを構築することが困難になるという問題があります。より現実的なのは特定のベンダーから提供された必要な機能がすべてそろったライブラリを使うという方法でした。

現在では、オープンソースという考え方が広まり、世界中のひとたちが作成した非常に多くのソフトウェアをインターネットを経由して容易に入手できるようになりました。そのようにして公開されているソフトウェアはまだまだ独立したアプリケーションとして実装されているものが多いのが現状ですが、それらをソフトウェアの部品として利用できるようになれば、ほかのひとたちによって作られたソフトウェアの部品を組み合わせて要求にあったシステムを自由に効率的に構築できるようになってくる可能性がでてきます。

そのような可能性を実現するためには、さまざまなソフトウェアコンポーネントで利用可能なインターフェイスの集合を策定していくことが重要になります。ここで言うインターフェイスは、いわゆるソフトウェア開発キット(SDK)のAPIやオペレーティングシステムのシステムコール、あるいは標準ライブラリ関数といったものとは異なる概念です。ここで言うインターフェイスとは、どのようなソフトウェア部品でもたとえばその中の要素の一覧を取得するときにはlistというメソッドを提供するインターフェイスを使う、listというメソッドはiteratorオブジェクトのインターフェイスを返すといった、すべてのソフトウェア部品が共通してまもっていく規約のことです。これはウィンドウシステムのユーザーインターフェイスでファイルメニューはメニューバーの一番左にあるように規定されて、ファイルメニューを使用するほぼすべてのアプリケーションがそれに従っている、というような事例に見られるインターフェイスの概念に近いものです。

インターフェイス定義言語(IDL)

esオペレーティングシステムでは(そのほかのコンポーネントシステムと同様に)、さまざまなインターフェイスをインターフェイス定義言語(IDL)を使って定義しています。C++のプログラマーなら、esのIDLはC++のクラス宣言とよく似ていることに気づくはずです。

以下はesのIBindingインターフェイスのIDLファイル(IBinding.idl)からの抜粋です。


    /** This interface represents a name-to-object binding.
     */
    interface IBinding : IInterface
    {
        #pragma ID IBinding = "DCE:33f5e13e-25dc-11db-9c02-0009bf000001";
        /** The object bound to this binding.
         */
        attribute IInterface object;

        /** The string representation of this binding.
         */
        readonly attribute string name;
    };

IBindingインターフェイスはIInterfaceというインターフェイスを継承しています。
そして object と name という属性を規定しています。次節でこのIDLから生成されるC++ヘッダーを示しますが
これらの属性から getObject, setObject, getName というC++のメソッドが生成されます。

なお、IDLの文法の詳細についてはIDLコンパイラesidlの仕様書を参照してください。

IBindingインターフェイスには33f5e13e-25dc-11db-9c02-0009bf000001というインターフェイスIDが割り当てられています。このようにesでは、すべてのインターフェイスはこのようにユニークなUUIDが振られています。ソフトウェアコンポーネント のメソッドを呼び出したときにどのインターフェイスを使用しているのかを識別するときには、IBindingという文字列のシンボルではなく、インターフェイスIDが使われます。これは、UUIDは原理的に誰がいつどこで生成しても必ず異なる番号が得られるので、文字列を使った場合のようにたまたま同じ名前がインターフェイスに付けられてしまって衝突するといったことを避けられるためです。

インターフェイスのセマンティクスがIDLによって明確にされていることでソフトウェアコンポーネントは同じプロセスの中にあっても、別のプロセス中にあっても(呼び出しにかかる時間の差を除けば)、その違いを意識することなく利用することができます。

IDL コンパイラ esidl

IDLで記述されたインターフェイスを提供するソフトウェアコンポーネントを実装するために必要になるC++のヘッダーファイルやインターフェイスのリフレクションデータを生成するのがIDLコンパイラesidlです。esidlはIBinding.idlからIBinding.hとIBinding.irdを生成します。IBinding.hはC++のヘッダーファイル、IBinding.irdはesオペレーティングシステムがコンポーネント間のメソッド呼び出しを仲介するために必要なリフレクションデータがバイナリ形式で保存されています。(esidlは、そのほかのコンポーネントシステムとは違って、インターフェイスごとにパラメータのマーシャリングを行うスタブコードを生成することはありません。マーシャリング相当の処理はesオペレーティングシステムのカーネルがリフレクション データを元に自動的に実行します。)

IBinding.idlをesidlでコンパイルすると、 下記のようなIBinding.hが生成されます。


    /** This interface represents a name-to-object binding.
     */
    class IBinding : public IInterface
    {
    public:
        /** The object bound to this binding.
         */
        virtual IInterface* getObject() = 0;
        virtual void setObject(IInterface* object) = 0;
        /** The string representation of this binding.
         */
        virtual int getName(char* name, int nameLength) = 0;
        static const Guid& iid()
        {
            static const Guid iid =
            {
                0x33f5e13e, 0x25dc, 0x11db, { 0x9c, 0x02, 0x00, 0x09, 0xbf, 0x00, 0x00, 0x01 }
            };
            return iid;
        }
    };

IDLで定義した属性 object, name を操作するためのメソッド(getObject, setObject, getName)が生成されています。 また、インターフェイスIDは IBinding::iid() で取得することができます。

esではIDLファイルの中にjavadoc形式でインターフェイスやメソッドの説明を記述しています。esidlコンパイラはIDLファイル中のjavadoc形式のコメントをそのままC++のヘッダーファイルに書き出します。esのhtmlのリファレンスマニュアルはそのようにして生成されたC++のヘッダーファイルからさらにオープンソースのCppDocを使って自動的に生成されています。

ソフトウェアコンポーネントの実装 - サーバープログラム

esでソフトウェアコンポーネントを実装するプログラムを紹介します。binder.cppはIBindingインターフェイスを備えたオブジェクトを提供するコンポーネントソフトウェアの 簡単なソースコードです。

class BinderがIBindingインターフェイスを備えたオブジェクトのクラスを実際に実装しています。そのなかのaddRef, release, queryInterfaceはIBindingインターフェイスが継承しているIInterfaceインターフェイスで規定されているメソッドです。(IInterfaceのルーツはMicrosoft社のComponent Object Model(COM)のIUnknownインターフェイスにあります。)addRef, releaseはオブジェクトの生存期間を参照数に基づいて管理します。queryInterfaceは指定されたインターフェイスIDに対応するオブジェクトのインターフェイスポインタを返すものです(ソフトウェアコンポーネントのインターフェイスはシンボルではなくインターフェイスIDによって識別されることはこれまでに説明しました)。

esではすべてのソフトウェアコンポーネントのオブジェクトはこのIInterfaceインターフェイスを実装している必要があります。ただし幸いなことに以下のコードでも分かるようにオブジェクトを利用する側がこれらのメソッドを直接意識しないといけない場面は ほとんどありません。

以下ではbinder.cppのmain関数の処理を順番に見ていきます。


int main(int argc, char* argv[])
{
    esReport("This is the Binder server process.\n");
    System()->trace(true);

    Handle<IContext> nameSpace = System()->getRoot();
    Handle<IClassStore> classStore = nameSpace->lookup("class");
    TEST(classStore);

    // Register Binder factory.
    Handle<IClassFactory> binderFactory(new(ClassFactory<Binder>));
    classStore->add(CLSID_Binder, binderFactory);

    // Create a client process.
    Handle<IProcess> client;
    client = reinterpret_cast<IProcess*>(
        classStore->createInstance(CLSID_Process, client->iid()));
    TEST(client);

    // Start the client process.
    Handle<IFile> file = nameSpace->lookup("file/binderClient.elf");
    TEST(file);
    client->start(file);

    // Wait for the client to exit
    client->wait();

    // Unregister Binder factory.
    classStore->remove(CLSID_Binder);

    System()->trace(false);
}

    esReport("This is the Binder server process.\n");

esReportはesのprintf相当のライブラリ関数です。esでは少なくとも現状ではC言語のstdioはサポートしていません。

    System()->trace(true);

System()はICurrentProcessインターフェイスポインタを返すライブラリ関数です(libes++.aの中で定義されています)。esのユーザープロセスが唯一最初から利用できるインターフェイスポインタがこのICurrentProcessインターフェイスです。ICurrentProcessを使うとカレントプロセスに割り当てられている標準入出力ストリームのインターフェイスポインタを取得するといったことができます。

ここではICurrentProcessのtraceメソッドを使って、プロセスの実行しているシステムコール呼び出しやリモートプロシージャコールの様子をデバッグ出力するように指示しています。

    Handle<IContext> nameSpace = System()->getRoot();

ここではルート名前空間のIContextインターフェイスポインタを取得しています。生のインターフェイスポインタの扱いは参照数の管理など面倒な面がありますので、esではIContext*と書く代わりにhandle.hで定義されているスマートポインタクラスHandleを使ってHandle<IContext>と記述します。Handle<IContext>はaddRef, release, queryInterfaceの呼び出しを内部で自動的に処理します。

    Handle<IClassStore> classStore = nameSpace->lookup("class");

続いて名前空間からクラスストアのIClassStoreインターフェイスポインタを取得します。クラスストアはソフトウェアコンポーネントの ファクトリークラスを登録する場所です。なお、特別に意識する必要はありませんが、IContextインターフェイスのlookupメソッドの戻り値の型はIInterface*です。classStoreで使用するIClassStore*型のポインタを取得するには、queryInterfaceメソッドを 呼び出して新しいインターフェイスポインタを取得したり、参照数を調整したりする必要があるのですが、そういった煩わしい処理はHandleテンプレートクラスが内部で処理しています。

    // Register Binder factory.
    Handle<IClassFactory> binderFactory(new(ClassFactory<Binder>));

クラスストアに登録できるのはIClassFactoryインターフェイスを備えたファクトリークラスのオブジェクトだけです。ここではClassFactoryテンプレートクラスを使ってBinderクラスのファクトリークラス のオブジェクトを生成しています。あるクラスにデフォルトコンストラクタが定義されていれば、ClassFactoryテンプレートクラスを使って簡単にそのクラス用のファクトリークラスのインスタンスを作成することができます。

    classStore->add(CLSID_Binder, binderFactory);

ここでは、binderFactoryをクラスストアに登録しています。クラスストアにファクトリークラスを登録するときも名前の変わりにユニークなクラスIDを指定して登録します。これも名前のかわりにクラスIDを使用することで、さまざまなひとたちの作ったファクトリークラスの名前が偶然に 衝突してしまったりすることを避けるためです。

esオペレーティングシステムのプロセスは、クラスストアにファクトリークラスを登録できた時点で、そのほかのプロセスからそのインスタンスを作成して利用することを可能にするサーバープロセスとして の機能をはじめています。

binder.cppでは続いて実際にBinderを利用するクライアントプロセスを起動しています。

// Create a client process.
Handle<IProcess> client;
client = reinterpret_cast<IProcess*>(classStore->createInstance(CLSID_Process, client->iid()));

プロセスのクラスIDを指定してクラスストアに新しいプロセスの生成を依頼し、クライアントプロセス用のIProcessインターフェイスポインタを取得します。プロセスはesオペレーティングシステムのカーネルがあらかじめクラスストアに登録しているカーネルオブジェクトです。

    // Start the client process.
    Handle<IFile> file = nameSpace->lookup("file/binderClient.elf");

次にクライアントプロセスに実行させる実行形式ファイルを名前空間から探してIFileインターフェイスポインタを取得しています。

    client->start(file);

取得したIFileインターフェイスポインタを引数にしてstartメソッドを呼び出すと、クライアントプロセスは指定されたプログラムの実行を開始します。

    // Wait for the client to exit
    client->wait();

binder.cppはここでクライアントプロセスが終了するまで待ちます。

    // Unregister Binder factory.
    classStore->remove(CLSID_Binder);

クライアントプロセスが終了したら、クラスストアからBinderのクラスファクトリを抹消して、binder.cpp自体もトレースを中止して終了します。

    System()->trace(false);

クライアントプログラム

binderClient.cppはbinder.cppが起動するクライアント側のプログラムのソースコードです。binder.cppではcreateInstanceメソッドでCLSID_Processを指定してプロセスを生成しましたが、ここではCLSID_Binderを指定してBinderを作っています。またgetNameメソッドを呼び出して、Binderの名前を表示しています。


int main(int argc, char* argv[])
{
    esReport("This is the Binder client process.\n");
    System()->trace(true);

    Handle<IContext> nameSpace = System()->getRoot();
    Handle<IClassStore> classStore = nameSpace->lookup("class");

    // Create binder objects.
    Handle<IBinding> binder[2];
    for (int i(0); i < 2; ++i)
    {
        binder[i] = reinterpret_cast<IBinding*>(
            classStore->createInstance(CLSID_Binder,
                                       binder[i]->iid()));
        char name[14];
        binder[i]->getName(name, sizeof(name));
        esReport("%s\n", name);
    }
    System()->trace(false);
}

新しいプロセスを生成するときも、Binderのオブジェクトを生成するときも、使用するインターフェイスは同じです。また、別のプロセスで実行されているオブジェクトのメソッドを呼び出すときも、カーネル内部に実装されているオブジェクトのメソッドを呼び出すときも、クライアントからみればどちらもインターフェイスポインタを使った純粋仮想関数呼び出しとして記述するだけ 構いません。

これまで見てきたようにesオペレーティングシステムにはインターフェイスポインタを使用しない独立したシステムコールはありません。オペレーティングシステム自体もソフトウェアコンポーネントの集まりとして実現できるようになってい ます。

実行例

binderを実際に実行してみましょう。ビルドツリーのcmd/testsuite/binderというスクリプトを起動すると実際にesカーネルが起動してbinderの実行を始めます。

% cmd/testsuite/binder

もしtraceをonにしていなければ、画面には

This is the Binder server process.
This is the Binder client process.
id1
id2
done.

と表示されるはずです。しかしカーネルトレースを有効にしてあるので、実際にはオペレーティングシステムのカーネルとサーバープロセス、クライアント プロセスとの間でさまざまなやり取りが行われている様子が見られます。

注意深くここまで読まれた方の中には、サーバープロセスがwait()で実行を中断していたことを憶えているかもしれません。Binderのnew()やgetName()がどのように サーバープロセスの中で実行されたのか不思議に思ったひともいるかもしれません。

クライアントプロセスがgetNameを呼び出したあたりには以下のようなトレースが表示されています。

system call[9:8011B820]: IBinding::getName(name, len);
upcall: IBinding::getName(name, len);
return from upcall: IBinding::getName(name, len);

system call[9:8011B820]: IBinding::getName(name, len);

というトレースは、クライアントからの

        binder[i]->getName(name, sizeof(name));

というgetNameメソッドの呼び出しによって、いったん制御がカーネルに移っていることを示しています。Binderがプロセスのようにカーネル内部で実現されているオブジェクトであれば、カーネルがリクエストを処理してシステムコールを完了します。しかし、Binderは実際には別プロセスで実装されている オブジェクトです。

upcall: IBinding::getName(name, len);

というトレースは、カーネルがクライアントに代わってサーバーのgetNameメソッドを呼び出そうとしていることを示しています。esのカーネル内部ではクライアントプロセスで処理を行っていたカーネルスレッドがそのままサーバープロセスに移動してgetNameをアップコールするようになっています。そのためサーバープロセスのメインスレッドはクライアントプロセスの終了待ちで停止していても、サーバープロセス中のgetNameメソッドを実行できたわけです。

return from upcall: IBinding::getName(name, len);

というトレースはサーバーでgetNameメソッドの処理が完了してクライアントに戻ろうとしていることを示しています。このときカーネルは、サーバープロセスで生成された"id1"という文字列をサーバープロセスからクライアントプロセスのname[14]という配列にコピーして

        binder[i]->getName(name, sizeof(name));

の次の行からクライアントプロセスの実行を再開します。

インターフェイスの拡張

binderの例ではesオペレーティングシステムのカーネルに最初から登録されているIBindingインターフェイスを提供するサーバープログラムの作成方法を示しました。esオペレーティングシステムでは、カーネルに最初から登録されていないインターフェイスをコンポーネントで独自に定義してほかのアプリケーションプログラムに提供することもできます。

location.cppはILocationという新しいインターフェイスを定義してサービスを提供するプログラムです。ILocationを定義しているIDLファイルILocation.idlをesidlコンパイラでコンパイルすると、ILocation.hという ヘッダーファイルと同時にインターフェイスのリフレクションデータが書き込まれているILocation.irdというファイルが生成されます。この生成されたリフレクションデータをカーネルに登録することによって、ILocationという新しいインターフェイスをlocation.cpp以外の別のプログラムからも利用することができるようになります。

新しいリフレクションデータをカーネルに登録するには名前空間からIInterfaceStoreインターフェイスポインタを取得して、以下のようにaddメソッドを呼び出します。

    // Register ILocation interface.
    Handle<IInterfaceStore> interfaceStore = nameSpace->lookup("interface");
    interfaceStore->add(ILocationInfo, ILocationInfoSize);
ビルドツリーのcmd/testsuite/locationというスクリプトを起動すると実際にesカーネルが起動してlocationサーバーの実行を始めます。locationサーバーが管理しているLocationクラスのオブジェクトの位置や名前を外部のプログラムlocationClient.cppからローカルのオブジェクトのように自然に操作することができていることがわかると思います。

まとめ

ここではesオペレーティングシステムでソフトウェアコンポーネントを作成する方法を紹介しました。 オブジェクトのクラスを定義したら、そのファクトリークラスをクラスストアに登録するだけで、プロセスがソフトウェアコンポーネントとして機能することを示しました。esオペレーティングシステムでは、ローカルなリモートメソッド呼び出しはシステムコールとアップコールの組み合わせに過ぎません。es以外のオペレーティングシステムでよく見られる、ユーザープログラム側のスタブコードが引数や戻り値をマーシャリングしたデータをメッセージ通信用のシステムコールで送受信してリモート手続き呼び出しを実現する方法と比較した場合、esではユーザープログラムの実装もIDLコンパイラの実装も非常に単純なものになっています。

esのIInterfaceインターフェイスで規定されているaddRef, release, queryInterfaceの概念はMicrosoft社のCOMのIUnknownインターフェイスとして確立されてきたものから派生しています。これは非常に基本的な概念で、Microsoft社のCOMにとどまらずMozilla FoundationのXPCOM、QUALCOMM社のBREW、CERNのGaudiフレームワークといった非常に幅広い領域で応用されています。COM技術について解説した 書籍としては以下の本をお勧めします。

    Don Box著/長尾高弘訳. Essential COM. アスキー・アジソンウェスレイシリーズ, 1999.

COM技術が確立されたのはISO C++の標準化がひとまず完了する1998年よりもさらに数年前のことで、当時はテンプレートによるスマートポインタの利用方法も十分には開発されていませんでした。そのためここで紹介したものよりも やや煩雑な印象を受けるかもしれませんが、 『Essential COM』はコンポーネントソフトウェアの基本的な考え方を理解する上でも参考になると思います。

[esオペレーティングシステムのホームページに戻る]


Copyright © 2006, 2007
Nintendo Co,. Ltd.

Permission to use, copy, modify, distribute and sell this software and its documentation for any purpose is hereby granted without fee, provided that the above copyright notice appears in all copies and that both that copyright notice and this permission notice appear in supporting documentation. Nintendo makes no representations about the suitability of this software for any purpose. It is provided "as is" without express or implied warranty.

SourceForge.jp