Symbian OSアプリ開発の手引き

Symbian OSアプリ開発の手引き(7)

オブジェクト指向的ファイル操作とは?
〜 ストリームで扱う抽象的で安全なファイル入出力 〜

大久保 潤 管理工学研究所 2008/11/26

Symbian OSの全体像を概観した「Symbian OS開発の勘所」の続編となる今回は、“実際にどのようなプログラムを書くのか?”をテーマにSymbian OS向けのアプリケーション開発における心得を分かりやすく伝授する。(編集部)

- PR -

 前回「Symbian OSのファイル操作を悩まずに使いこなす」では基本機能シリーズの最終回と宣言しつつ、ファイル回りの低水準のAPIについてアレコレ説明したところで紙幅が尽きてしまいました。

 というわけで、今回はファイル関連の残り半分、ファイルを使って情報の永続化を行うためにSymbian OSが用意している「ストリーム(とストア)」というフレームワークについて説明を行います(注1)


注1:「何でもかんでもフレームワークなのか!」といわれそうですが……。「ハイ」そのとおりです。無手勝流でコードを書き進めたり、車輪の再発明を恐れないことが許されたフロンティアの時代は、組み込みにおいてももはや遠い過去のことなのです。

前回解説されなかった“謎”のコード

 では、前回の最後の部分からリプレイです。Carbide.c++が生成したスケルトンコードでは、RFileからの読み込み結果をHBufCのインスタンスに直接受けるのではなく、いったんRFileReadStreamというクラスのインスタンスを構築し、それ経由で値の設定を行っています(図1)

図1 ECommand2ハンドラの未解説部分(前回の図5)

 なぜ、RFileのメソッドである

IMPORT_C TInt Read(TDes8& aDes) const; ……(A)

などを使ってファイルの読み書きを行わないのでしょうか? ここにポイントがあります。

 (A)の引数を見てみると、aDesは「バイナリディスクリプタ」です(コラム参照)。考えてみれば当たり前ですが、入出力されるデータが何を意味しているのかはファイルを使う側だけが知っていることです。だから低レベルのファイル操作APIでは、どのようなデータでも受けられる(だけど、意味については関知しない)バイナリ形式でインターフェイスを取ることになります。

 とはいえ、プログラム中で扱うデータの形式とファイルに読み書きするときのデータの形式にギャップがあるのは、ファイルを使うプログラムを書く方からすると面白くアリマセン。やりたいことは32bit型の整数や文字列(注2)などをファイルに適切に保存して、使いたいときに持ってくることなのであって、バイナリ形式に変換し、保存するときのアレコレをいちいち考慮したいわけではないからです。そこで、ギャップを埋めるものが必要になってきます。そのために用意されている仕掛けが、ストリームです。

注2:エンコーディングを意識するなぞ、もってのほかです。

コラム バイナリディスクリプタ

 連載第5回「貴重な情報源“ヘッダファイル”を読んでいますか?」で説明したとおり、Symbian OSでは文字が16ビットの幅である「UCS-2」である場合と8ビット幅である「CP1252」である場合、それぞれのためにTDesC16/TDes16、およびTDesC8/TDes8という型を用意し、条件コンパイルによって文字列を表す、TDesC/TDesに割り当てています。この結果、文字の幅をコード上から意識することなくプログラムが行えるようになっています。逆にいえば1文字に必要なビット幅をプログラム上で意識するのはご法度です。せっかく隠ぺいしているのに台無しですよね。ところが、(A)にある引数の型は隠されてしかるべき情報、すなわち“8bitの幅を持つ”と明言しているディスクリプタです。なぜこのような書き方をするかというと、ここで扱うものが文字列ではなく単なるバイト列(ということはバイナリデータですね)であるからです。

 ディスクリプタはデータの長さと実体(書き換え可能な場合は、加えてバッファの長さ)を管理する仕掛けですから、文字列であろうがバイナリデータであろうが扱いに差はアリマセン(注3)。そのため、CP1252文字列を扱うためのディスクリプタは、そのままバイト列を扱うための仕掛けに転用できます。

 TDesC8/TDes8(およびそれらの派生)を明示的に使い、文字列ではなくバイト列(バイナリデータ)を扱うディスクリプタをバイナリディスクリプタと呼びます。連載第5回の最後でその存在が予告されていたものがコレです。「文字列もバイト列も同じインターフェイスで使えて便利だなぁ」くらいに思っておいてください。ちなみに連載第5回で、

……これは主としてクライアントサーバ(ファーストシーズンの8回目「クライアントサーバという究極の設計思想」)のデータ転送で用いられるものです。

と書きましたが、ファイル操作はファイルサーバが提供している機能なので、(A)でTDes8&が引数として登場してくるのはまったくそのとおりなのです。

注3:FindF()やCompareF()のような、ロケールを考慮する文字列処理関数はさすがにアウトですが。

ストリーム

 Javaなどがそうであるように、最近の開発環境(言語+フレームワーク)では、入出力の源泉となるクラス(以後、ソースと記します)と、入出力のインターフェイスを行うクラス(ストリーム)を分離する構成を取るのが一般的です。ストリームはその名前から明らかなように、データを順次読み込み、または書き出しする操作を抽象化するためのもので、“読み書きの方向”と“現在位置”という概念を持っています(任意の位置にシークできるかどうかはソース次第です)。また、読み込みストリーム、書き出しストリームは異なるクラスで提供されることがもっぱらです。

 もちろん、Symbian OSでも図1にあるとおりストリームが提供されているわけですが、それは以下のメリットが期待できるからです。

A)データを読み書きする利用者側のコードからソースの実装を分離し、差異を隠ぺいできる
B)実行中のプログラムが扱う形式(内部形式)と保存するときの形式(外部形式)に変換を行うためのレイヤを設定できる

 一般にストリームというとA)の性質(利点)が強調されがちですが、内部形式←→外部形式変換がサポートされることも同様に重要です。ということで、これらの性質にフォーカスして、Symbian OSのストリームの概観を説明していきます。ただし、Symbian OSにおいても、ストリームは読み込み、書き出しで異なるクラス定義を持っており、両方同時に追いかけていくと混乱を来す恐れもあるので、まずは読み込みストリームからはじめることにします。

RReadStream

 では、図1で出てきたRFileReadStreamをDocBrowser(もしくは、s32file.h)で確認してみましょう。RReadStreamというクラスが基底クラスであることが分かります。これこそが読み込み側ストリームの大本です。先ほどのA)の性質を満たすよう、RReadStream(s32strm.h在中)は以下の派生クラスを持ちます(表1)

表1 読み込み側ストリーム
クラス名
ソース
定義場所
1
RFileReadStream ファイル s32file.h
2
RMemReadStream メモリ(ポインタと長さ) s32mem.h
3
RDesReadStream メモリ(TDesC8) s32mem.h
4
RBufReadStream CBufBase派生の動的配列 s32mem.h
5
RStoreReadStream ストア s32std.h
6
RDictionaryReadStream ディクショナリストア s32stor.h
7
RDbColReadStream TableまたはViewのRowset d32dbms.h
8
RSqlColumnReadStream DBのカラム sqldb.h

 このように多様なソースが読み込みストリームの派生クラスによって支援されています。ところで、図1ではHBufCを以下のように構築していました。

HBufC* fileData = HBufC::NewLC(inputFileStream, 32);

 しかし、HBufCのファクトリメソッドであるHBufC::NewLCのシグネチャは以下のとおりです。

static IMPORT_C HBufC16 *NewL(RReadStream &aStream, TInt aMaxLength);

 つまり、HBufC::NewLC自体は、RReadStreamの派生クラスを経由することで、ファイルに限らず表1にあるソースを用いてTDesCのインスタンスを構築できるわけです。ソースをファイルからDBに変えたとしても、ストリームを差し替えればほかのプログラムに手を入れる必要がないのですから、これは確かにアリガタイことです。これこそ、A)が提供しようとしていた利便性です。

 対して、B)の性質は派生クラスを通じてではなく、RReadStreamのメソッド定義自体によって保証されます。RReadStreamの定義をのぞいてみましょう(図2)

図2 RReadStreamのメソッド抜粋

 引数が異なるReadL()メソッド(オーバーロードですね)と、Read<戻り値型名>L()の形式を持つメソッドが多数提供されています。個数はありますが、これらのメソッドは同じ定義の機能を提供します。

  • 引数または戻り値で示される型の値が、ストリームの現在位置から読み出せると信じて読み込みを行う

 この機能の意味するところは、「ストリーム中にどのような形式(外部形式)で保存されているかを隠ぺいしつつ、実行中のプログラムが扱う形式(内部形式)で取り出すことを保証する」です。読めると信じた型で値を受け取るメソッドを発行すればデータが取り出せるわけですから(注4)、これはまさにB)の性質です。

注4:型変換をやるとは書いてないことに注意してください。指定された型でストリームからデータを拾いにいくので、もし型が違っていればPANICの対象となります。つまり、書かれた順番どおりに読み込みを行う必要があり、この順序はInternalize・Externalize(後述)を通じて保証します。

 なお、オーバーロードと、型名込みの命名規則(Read<戻り値型名>L()の形式)を使って、Readメソッドを大量に定義しているのは、データを読み出す型ごとにメソッドを分離して、タイプセーフにするためです。古典的なCのスタイルでAPIを定義すると、多分以下のようになると思います(図3)

図3 Cスタイルのインターフェイス(もちろんAntiPattern)

 上記のような場合、void* aStoreをTReadType aTypeの値に合わせてキャストして参照、書き込みすることになりますが、このスタイル、まったくもってタイプセーフではアリマセン。CからC++への引っ越しが完了した開発者であれば、図2のインターフェイス定義の方が、図3のスタイルよりベターであると自然に思えるはずです。

  • 連載バックナンバー
  • 全記事インデックス
  • 組み込み開発トップ
  • MONOistトップ

スキルアップ/キャリアアップ(JOB@IT)

スポンサーからのお知らせ

- PR -
@IT Sepcial

震災関連・復興支援情報

震災関連・復興支援情報
@IT MONOist/EE Times Japan/環境メディアの製造業技術者向け3メディアを中心に、震災関連/復興支援情報を集めました

次世代エンベデッドコーナー

次世代エンベデッド
“次世代”の組み込み機器を開発するエンジニアを支援するコーナー。新潮流・新技術をインタビューやコラム、解説記事で分かりやすく紹介!

Windows Embeddedコーナー

Windows Embedded
Windows Embedded専門コーナー。Windows Embedded StandardやWindows Embedded CEをはじめとする「Windows Embedded」ファミリの最新動向や技術情報をお届けします!!

Androidコーナー

Android
Android専門コーナー。組み込みデバイスへの適用からアプリケーション開発、イベントレポート、ニュースなどAndroidに関するさまざまな技術情報がここに集結!!

@IT MONOist 求人情報

- PR -