Win32デバッグ(12)・・・SEH(Structured Exception Handling)

まぁ、どうしようか悩んだが、とりあえず、進める所まで進んでみますsad

ということで今回のお題目は例外。結論から言うとDelphiではtry-except文による例外処理にしろ、try-finally文の終了処理にしろ、WindowsのSEH(Structured Exception Handling)を使って実装されているのだが、そのSEHについて。

SEHはWindowsによって提供されている構造化例外処理のためのメカニズムのことであるが、その流れについて説明する。

まず、0による除算や不正なメモリへのアクセスなどのハードウェア例外にしろ、Win32 APIのRaiseException関数によるソフトウェア例外にしろ、スレッド内で例外が発生するとOSに制御が移る。そして、OSは例外が発生したスレッドのCPUレジスタなどのコンテキスト情報の保存など必要な処理を行った後、例外をハンドルするための例外ハンドラを検索し、呼び出す。

具体的には、まず、OSは例外が発生したスレッドのスレッド環境ブロック(TEB:Thread Environment Block)またはスレッド情報ブロック(TIB:Thread Information Block)と呼ばれる領域の先頭4バイト(at FS:[0])の値を取得する。この4バイトの値は例外ハンドラのリンクリストの先頭をポイントしているので、この値から先頭の例外ハンドラから順に呼び出していくのである(つまり、ここに例外ハンドラを登録すれば例外発生時に例外ハンドラが呼び出されるようになるのだが、通常は、Delphiにしろ、C++にしろ、言語提供の例外処理などを利用すれば、コンパイラによって登録するコードが挿入されるので、普通は自前で登録しないが・・)。

リンクリストのノードは次のようなEXCEPTION_REGISTRATION構造体になっている。

prevメンバは次のノードへのアドレス、handlerメンバーには例外ハンドラのアドレスつまりOSから呼び出されるコールバック関数のアドレスを表す。呼び出されるコールバック関数のプロトタイプは次のようになる。

第1引数はEXCEPTION_RECORD構造体のアドレスである。EXCEPTION_RECORD構造体は例外の種類を表すコードや例外発生時のアドレスなどの発生した例外に関する様々な情報が格納されているので、例外ハンドラはこれらの情報を見て例外を処理するかを決定する。Delphiのtry-except文では例外オブジェクトのクラスとexceptのon句に指定したクラス(on E: ExceptionClass doのExceptionClassの部分)を比較して判定している。

呼び出された例外ハンドラで例外を処理せず、OSに次の例外ハンドラを呼び出させる場合はExceptionSearchException(1)を返して例外ハンドラからOSにリターンする。

また、例外が発生したアドレスから例外の原因を修正(0による除算が発生した場合、除数を0以外の値に修正)するなどして、再実行する場合はExceptionContinueExecute(0)を返してOSにリターンする。Delphiではこれは行われない。

例外ハンドラで例外を処理する場合は、例外ハンドラからOSにリターンせず、通常は、Win32APIのRtlUnwindなどでアンワインド(巻き戻し)したり、スタックフレームを適切に設定し直して、処理を続行する。Delphiではexceptのon句の後に指定した処理から実行が再開される。

RtlUnwindによるアンワインドはMSDNのドキュメントみても何の事だかさっぱり理解できないと思うが、簡単に言うと、例外ハンドラのリンクリストから不必要になった例外ハンドラのノードを削除することである。また、このとき、例外ハンドラの2回目の呼び出しが行われる。この2回目の呼び出しのときに、通常は終了処理を行う。Delphiではtry-finally文のfinally句の後に指定した終了処理がこの2回目の呼び出しのときに、実行される。

とまぁ、おおざっぱに書くとこんなところであろうが、詳細は

すべて、英語で書かれているが、特に一番上の記事にはSEHについて分かりやすく、詳細に書かれているのでお勧めです。自分は頑張って読みましたdash

| | コメント (0) | トラックバック (0)

Win32デバッグ(11)・・・番外編

今回は寄り道。というより、前回までに示したコードに少し問題があった。

第1回で

  • ReadProcessMemory
  • WriteProcessMemory

を使えば、プロセスハンドルからそのプロセスのアドレス空間を読み書きできるし、また、

  • GetThreadContext
  • SetThreadContext

を使えば、スレッドハンドルからそのスレッドのコンテキスト(レジスタの値)を読み書きできる。

と書いたが、大切な事が抜けていたshock

ということで、例えば、あるスレッドが次に実行する命令を読み込むプログラムを上記のAPIを使って書いてみる。x86アキーテクチャではインストラクションポインタ(EIP)レジスタが次に実行される命令をポイントするので、例えば、次のようになる(繰り返すが、対象スレッドは停止してないとまずいと思う)。

x86命令は可変長の命令で何バイト読みこめばよいか?という問題があるが、上記のプログラムではとりあえず4バイト読み込んでいる。上記のプログラムを実際に動かすと正しく動いているように見えるが、問題があるのである。

何が問題かと言うと、ReadProcessMemory/WriteProcessMemoryの第2引数には読み書きするメモリの仮想アドレスを渡すのであり、上記のプログラムでは、EIPレジスタの値を渡しているのであるが、EIPレジスタの値は仮想アドレスの値ではないのである。EIPレジスタに格納されているのは確かにアドレスであるが、CSレジスタに格納されているセグメントセレクタによってポイントされるセグメント(コードセグメント)内の相対アドレス(segment-relative address)なのである。

ということで、仮想アドレスに変換する必要があるのであるが、そのために使うAPIがGetThreadSelectorEntryである。GetThreadSelectorEntryによって引数で指定したセグメントの仮想アドレス空間内のセグメントのベースアドレスが求まるので、それを使って正しく先ほどのプログラムを書き換えると次のようになる。

と、肝心な事を忘れたdash

ちなみに、最初に示したプログラムが動いてしまうのは、どうも、NT環境ではコードセグメントのベースアドレスが常に0になるからのようである。特にESPレジスタによってポイントされるスタックトップを読み書きするような場合、スレッド毎に異なるスタックセグメント(SSレジスタ)が割り当てられるので、なお更まずい。

| | コメント (0) | トラックバック (0)

Win32デバッグ(10)・・・泣

このネタも10回目に突入してしまったbearing

早速、前回やり残した事をやる。前回で逆アセンブラスクラスを設計したので、今回は問題となっていたデバッグ情報からルーチンの終了位置のアドレスを求める部分を逆アセンブルして求めるよう次のように書き換える。

で、再びドライバプログラムを実行してターゲットプログラムをプロファイルしてみたところ、ターゲットプログラムが正常に動作したhappy02。プロファイルしたターゲットプログラムはコンソールアプリで次の通り。

意味のないプログラムであるが、関数Calcが10回呼び出され、その結果がコンソールに表示されるだけである。また、関数Calcからは関数SumとMulが呼び出される。このターゲットプログラムのコールツリーは次のようになった。

Win32debug_10_1_3

上の画面から確かに関数Calcが呼び出され、関数Calcから関数SumとMulが呼び出されているのが分かるだが、呼び出された回数つまりヒットカウントが10ではなく1になってるのである・・・全部合計すると10回なのであるが、ノードが分かれている・・・

がーんsad

で、デバッグして原因を調べたところ、肝心な事をすっかり忘れていたsad。それは、最近の言語でたいていはサポートされている例外処理・終了処理である。Delphiでいうところのtry-except,try-finally文である。この実現のためにコンパイラが生成してるコードに対応できていなかったのである。つまり、今までの設計では、ルーチンは必ずCALL命令で呼び出されRET命令でリータンする、また、RET命令は必ずルーチン内で1箇所だけ(現状、1つのルーチンに対して開始位置と終了位置の2つだけのブレークポイントを作成してるため)という仕様?前提?が甘かったのである・・・(JMP命令なども無視してるし・・・)

はぁ。

どうしよう??

と、本来はすべてが上手くいき今回で最終回にする予定であったのだが。どうしよう。

また、ドライバプログラムも含めて動作する形でソースをアップロードする予定であったのだが、どうせおかしなコールグラフが表示されるのでやめておく。

と言う事で次回に続くかは本気で分からない・・ははは。

| | コメント (0) | トラックバック (0)

間隔(インターバル)型の続き

以前の続きであるが、今回は間隔型のODBCでの対応について。

ODBCではSQL92の間隔型に相当するデータ型が定義されていて、そのSQLデータ型は次のようになる。

年月間隔

  • SQL_INTERVAL_YEAR
  • SQL_INTERVAL_MONTH
  • SQL_INTERVAL_YEAR_TO_MONTH

日時間隔

  • SQL_INTERVAL_DAY
  • SQL_INTERVAL_HOUR
  • SQL_INTERVAL_MINUTE
  • SQL_INTERVAL_SECOND
  • SQL_INTERVAL_DAY_TO_HOUR
  • SQL_INTERVAL_DAY_TO_MINUTE
  • SQL_INTERVAL_DAY_TO_SECOND
  • SQL_INTERVAL_HOUR_TO_MINUTE
  • SQL_INTERVAL_HOUR_TO_SECOND
  • SQL_INTERVAL_MINUTE_TO_SECOND

これら間隔型のデータをアプリケーションから入出力する時に、Cデータ型のSQL_C_CHARにマップつまり文字列として入出力してもよいのだが、間隔型専用のSQL_C_INTERVALの接頭辞で始まるCデータ型(SQL_C_INTERVAL_YEAR等)にマップして入出力することもできる。SQL_C_INTERVALの接頭辞で始まるCデータ型はSQL_INTERVAL_STRUCT構造体として定義され、次のようになっている。

まぁ、ここらへんはODBCのドキュメントに書いてあるので・・

ここでは、ちょっとWindowsのお話でも。.NET Frameworkでは間隔を扱うTimeSpan構造体というものが定義されているが、WindowsのネイティブAPIで間隔を扱うAPIなんてものがあるのかな?と前から思っていたら偶然発見したbearing

まずは、

間隔を文字列表現に変化するAPIであるが、実際に使って10万ミリ秒を変換してみた。

Interval2_1

このAPIはShell Lightwight Utility Functionsと呼ばれるAPIで将来のWindowsでは変更または利用不可になるかもしれないとの事・・・エクスプローラでファイルをコピーすると残りXX秒とかダイアログに表示されるが、そこで使われているのだろうか??

次は、Vistaで追加された

こちらも文字列表現に変換するAPIであるが、StrFromTimeIntervalと違いロケールや書式を指定できるとの事。これに合わせて、GetLocaleInfoなどでロケールに関する様々な情報を取得・設定できるが、間隔用のフォーマットのLOCALE_SDURATION定数が追加されている。Vistaもってないのでこららを試すことできないが・・・

| | コメント (0) | トラックバック (0)

Win32デバッグ(9)・・・逆アセンブラ

前回で、どうもx86命令を逆アセンブルする必要が出てきたので今回は逆アセンブルについて。

まずは、逆アセンブラを自作する方向で進めていたのであるが、ご存知のようにx86命令は可変長であったり、SSE命令などの拡張命令がどんどん追加されていったり、面倒そうなので、とりあえず、既存の逆アセンブラライブラリを利用する事にした。

ということで既存の逆アセンブラライブラリをいくつか挙げてみる。

逆アセンブラライブラリ
名前 サイト ライセンス
BeaEngine http://beatrix2004.free.fr/ LGPL
diStorm64 http://www.ragestorm.net/distorm/ BSD
Hacker Disassembler Engine(HDE) http://patkov-site.narod.ru/ Free
libdisasm http://bastard.sourceforge.net/libdisasm.html Free/Open Source
SysDasm http://rootkit.com/newsread.php?newsid=208 Free/Open Source
Udis86 http://udis86.sourceforge.net/ Free/Open Source
VirtualBox Disassembler Library http://www.woodmann.com/forum/showthread.php?t=11904 ?

各逆アセンブラライブラリの詳細については、各ライブラリのサイトを参照。ここではどのライブラリを使うかだが、ここでは上記の1つであるHacker Disassembler Engine(HDE)を使うことにした。理由は最終更新日が最近(2009年3月)であるのと、軽量である点だ。他のライブラリは結構高機能で逆アセンブルした機械語をアセンブラのニーモニックのテキスト形式に変換できたりするが、今回はx86命令のRET命令を検出できればよいので、命令長やオプコードなど最小限の情報だけを取得できるHDEにした(ちなみに、上記BeaEngineのサイトにLength Disassemblerというものもあるようだ・・)。

次に、新しいクラスを設計するが、その前にx86命令の大まかな命令フォーマットを知らないと設計できないので、x86命令の命令フォーマットを少し。x86命令の命令フォーマットは次のようになる。

X86instformat_2

先程も述べたように可変長なのであるが、各フィールドの詳細はIntelのマニュアルを参照ということで。

まずは、抽象クラスで逆アセンブルを行うTDisassemblerクラス。

各プロパティは先ほどのx86命令フォーマットの各フィールドに対応する。そのうちのいくつかのプロパティの型がVariant型なのは、対応するフィールドが必ずしも存在するとは限らないからである。DoNextメソッドは現在位置からx86命令を解析し、次に命令の先頭にポインタを進める抽象メソッドであるので、下位クラスではDoNextメソッドをオーバーライドしなければいけない、また、解析が成功したらTrueを返す。

そして、TDisassemblerクラスを継承し、HDEを使った逆アセンブラクラスTHdeDisassemblerのDoNextメソッドを次のように実装する。

ここは、使用する各逆アセンブラライブラリに依存するので詳しくは省略・・

後、一息なのであるが、今回はここまでbearing

最後に、HDEはC言語のライブラリなので、Delphiで利用するためには、ヘッダを移植し、ライブラリ自体はCコンパイラでコンパイルし、Delphiの$Lコンパイラ指令を使ってリンクすればよいはず。

| | コメント (0) | トラックバック (0)

Win32デバッグ(8)・・・そして

今回は、前回までで必要なクラスの設計そして実装ができたので、約束通りドライバを作成して動かしてみる。

で、ドライバとTD32デバッグ情報付きの簡単なターゲットプログラムを用意して動かしてみたのだが・・・

ターゲットプログラムの動きがおかしい・・・weep

動的コールグラフを求めるにあたって、前回までに述べた通り、ブレークポイントを設定するためターゲットプログラムのプロセスのメモリ空間のコード領域を書き換えてるのだがどうもそこらへんの書き換えがおかしいっぽい・・・

で、調べる事数時間・・・

予期せぬ原因が分かったweep。やはり、ブレークポイントを設定する位置のアドレスの計算に問題があった・・・。再掲載するが現在は

のように、ルーチンの呼び出しを検出するために、ルーチンの開始位置と終了位置のアドレスをデバッグ情報から取得できるセクションインデックスと開始位置のオフセット・長さから求め、開始位置のアドレスの計算は合っているようなのだが、終了位置のアドレスの計算が間違っているようなのである。現在、終了位置のアドレスは上の25行目のように、

  終了位置のアドレス=開始位置のアドレス+ルーチンの長さ

で求めているのであるが、この式によって求めた終了位置のアドレスがルーチンによって想定外の位置を指してたりと・・・。要するに、上の式によって求めたルーチンの終了位置のアドレスは必ずx86命令のRET命令の先頭を指していると思っていたのだが、甘かった・・

そして、対応策を色々考えたのだが、どうも、まさかここまでする必要はないと思ってこのネタをやりだしたのだが、開始位置のアドレスは正しいようなので、開始位置のアドレスから自前で逆アセンブルして、x86命令のRET命令の位置を求めることに・・・(まぁ、必ずしもすべてのコンパイラがルーチンの呼び出し・リターンにx86命令のCALL・RET命令を使ったコードを生成するとは限らない?が、まぁ、そこらへんはよしとしよう・・)。

続く・・・

| | コメント (0) | トラックバック (0)

Win32デバッグ(7)・・・動的コールグラフ(クラス設計の続き)

今回は残りの必要なクラスをさっさと設計してまおうbearing。また、長くなりそうだが・・・

まずは、要となるクラス。動的グラフとはつまるところルーチンの呼び出し関係を表すグラフ理論?で言うところ有向グラフなのであるが、そのルーチンの呼び出しを表すノードを表すTCallGraphNodeクラスを次のように定義する。

Callerプロパティはこのノードが表すルーチンを呼び出したルーチンのノードをあらわす。

Calleesプロパティは、次に出てくるTCallGraphNodeListクラスのプロパティであるが、これはこのルーチンから呼び出されたルーチンのノードリストを表す。あるルーチンから同じルーチンが複数回呼び出される時、呼び出し毎にノードを作成しても良いがメモリ効率が悪そうなのでここでは1つにまとめることにする。

HitCountプロパティはこのノードが表すルーチンが呼び出された回数を表す(もちろん、前述のCallerプロパティのノードが表すルーチンによって呼び出された回数である)。

Symbolプロパティは呼び出されたルーチンを表す。

次に、TCallGraphNodeのリストを保持するTCallGraphNodeListクラス。

ここでは、1点だけ、先程、あるルーチン内から同じルーチンへの直接の呼び出しは1つにまとめると書いたが、そのため、このリストは同じシンボルを参照する複数のノードを追加しようとすると例外が発生するようにしてある。シンボルの等価性の判定には、ここでは単純にTSymbolクラスのインスタンスの参照を比較してるがまぁ、これで問題ないだろう・・

ふぅ。コーヒーブレイクcafe

次に、ここで定義したクラスをどう使うかであるが、対象プロセスがマルチスレッドアプリケーションの場合、その動的コールグラフってどうなるんだ??って事の解を探る気力が全くないbearing(最近、思考するのが疲れたので息抜きでブログ書いてるのであるが・・・)ので、ここでは、スレッド毎にコールグラフを作成する事にするのでTThreadクラスにいくつか修正を加えてみた。

CallGraphプロパティは動的コールグラフを表すTCallGraphクラスのプロパティである。初出のクラスであるが、これは動的コールグラフの先頭のヘッドノードのリストを管理するクラスである。なぜ、リストを管理する必要があるかと言うと、少し考えれば分かるのだが、ルーチンの呼び出し階層に現われるルーチンにすべてに対するシンボル情報が存在もしくては提供されるとは限らないので、動的コールグラフが切れる可能性があるためである。クラスの定義はここでは省略。

CallStackプロパティはコールスタックを表すスタックである。動的コールグラフを成長させる時、内部で一時的に使用する。ルーチンの実行の開始の検出時には、そのルーチンのノードをスタックにプッシュ、ルーチンの実行の終了の検出時には、スタックからノードをポップする。このようにして、現在実行されているルーチンを追跡する(スタックトップのノードのルーチンが現在実行されているルーチンになる)。まぁ、実際、スタックなんて大袈裟なものを容易しなくても、現在実行されているルーチンのノードを表す変数1個用意すればすむのだが・・・

いよいよ、終盤であるが。以上を踏まえて、実際に動的コールグラフを成長させるプログラムは例えば次のようになる。

以外にシンプルだった。以上。

次回以降は、実際に今まで定義したクラスを使うドライバプログラムを作成して実際に動かしてみようと思う。

ここまでのプログラムはいつものようにSkyDriveから。

| | コメント (0) | トラックバック (3)

Win32デバッグ(6)・・・動的コールグラフ(クラス設計)

今回はWin32デバッグの続きで、新たなクラスを設計してみるgood

まずは、動的コールグラフにおいて、シンボルを表すクラス。

Sectionプロパティはセクションインデックス、StartOffset・Lengthプロパティはシンボルというよりここでは関数や手続きもしくはメソッドつまりルーチンのセクション内での開始位置のオフセット・長さを表す。このクラスはDelphiの言語上厳密に、抽象クラスではないが、抽象クラス的な使い方をする。理由は後述。

お約束?通り、わざわざ専用のリストクラスを定義する必要ないかもしれないが、TSymbolクラスのリストを保持するTSymbolListクラスを次のように定義してみる。

このクラスのリストは実行を追跡するルーチンだけを保持するために使われる。つまり、コンパイラやリンカによって生成されるデバッグ情報のすべてのルーチンの実行を追跡するのではなく、必要に応じて選択できるようにする。

また、最終的なアウトプットとして動的コールグラフを画面に表示したりする必要があるのだが、その時、上記TSymbolクラスのセクションインデックスや開始位置のオフセット・長さなどを表示してもユーザーには区別できないので、ルーチンの名前など表示したりするわけだが、そのためには、例えば、TD32デバッグ情報の場合、例えば、

として、TSymbolListクラスのリストには実際にはこのクラスのインタンスを格納する事になる。デバッグ情報に格納されている情報はそれを生成したコンパイラやリンカもしくはフォーマットによって変わるので、このような設計にする。先ほどTSymbolクラスは抽象クラス的な使い方をすると書いたがこのためである(まぁ、ルーチンの名前はどのコンパイラ・リンカでも持ってると思うが・・・)。

次に、各ルーチンの実行の追跡においてブレークポイントを設定・解除する必要があるが、ここではそれ専用のクラスを設計する。まずは、1個のブレークポントを表すTBreakpointクラスを定義する。

Addressプロパティはブレークポイントのアドレスを表す、アドレスは上述したシンボルの情報を元に計算する。OrignalCodeプロパティはブレークポイントを設定する位置の元の命令コードを表す。ブレークポイントの設定では1バイトのINT 3命令(具体的には16進数の0xCC)で置き換えるので1バイトのByte型である。Symbolプロパティはブレークポイントの元になるTSymbolクラスのシンボルであり、Startプロパティはブレークポイントがその元になったシンボルの開始位置または終了位置のどちらに対応するかを表す。と、1つのTSymbolクラスのインタンスからルーチンの2つのTBreakpointクラスのインスタンスを作成する。

また、実際にブレークポイントを設定、解除するためのSetBreakpoint、ResetBreakpointメソッドをヘルパとして追加してある。実装はこんな感じ。

同じく、ブレークポイントのリストクラスであるTBreakpointListクラスを定義する。

リスト内のすべてのブレークポイントを設定・解除するSetAllBreakpointsメソッド、ResetAllBreakpointsメソッドをヘルパとして追加してある。

ふぅ。疲れたdespair

実際にこれらをどう使うかだが、デバッグ対象のプロセスのメインの実行可能モジュール(EXEファイル)内のルーチンだけ実行を追跡するならプロセスレベルのTProcessクラスでもよいのだが、前回に書いたように、DLLファイル内のルーチンの実行の追跡もできるように、TModuleクラスにTSymbolListクラスのSymbolsプロパティ、TBreakpontListクラスのBreakpointsプロパティを追加する事にする(もちろん、DLLのデバッグ情報が見つかればの話だが・・・)。

また、実際にブレークポイントを設定するために、CREATE_PROCESS_DEBUG_EVENT、LOAD_DLL_DEBUG_EVENTでブレークポイントを設定するように修正。

と長くなるので、今回はここまでで、2回にわけようと思うbearing。作ったソースは次回ダウンロードできるようにする。というより、クラス図書いた方が速いとか・・

| | コメント (0) | トラックバック (2)

Win32デバッグ(5)・・・動的コールグラフ

前回までで、Win32のデバッグAPIそしてTD32デバッグ情報について述べ、これで、色々できるようになったので、なんか、実用的というより現実的?なソフトウェアを作ってみるhappy02

お題はタイトルにもあるように動的コールグラフ生成ツール。動的コールグラフとは、実行時の各ルーチンの呼び出し関係を表すグラフの事であるが、これを生成するツールを作ってみる。

ということで開始するのであるが、今回はそのための基本的な事柄・設計や処理のおおまかな流れについて。

まず、動的コールグラフを作成するには各ルーチンの呼び出しを検出する必要があるが、この方法について。色々なやり方があるのかもしれないが、ここでは、x86系プロセッサに備わっているソフトウェアブレークポイントと呼ばれるINT 3命令(ブレークポイント命令)とシングルステップモードを使用する方法を採用する。

INT 3命令とは、x86プロセッサの命令の1つで、実行されるとブレークポイント(BP)例外というトラップクラスの例外が発生する。例外が発生するとCPUは指定された例外ハンドラと呼ばれるルーチンに制御を移すが、通常、その例外ハンドラはOSによって提供され、Windowsの場合、INT 3命令を実行したスレッドのプロセスがデバッグされていると、例外コードEXCEPTION_BREAKPOINTのDEBUG_EXCEPTIONイベントとして、通知されるのでこれをルーチンの開始と終了の検出に利用する。

具体的にはプロセスのアドレス空間内のルーチンのコードの開始と終了位置の命令をINT 3命令に置き換え、ブレークポイントを設定する。置き換えられた元の命令はルーチンの開始または終了の検出後に実行する必要があるので退避する。また、ルーチンのコードの開始と終了位置はロードされたモジュールのベースアドレスとデバッグ情報から求まる。

DEBUG_EVENTイベント発生時(ブレークポイント例外)には、ルーチンの開始または終了の検出に応じてコールグラフを成長させる。また、ContinueForDebugEvent関数による実行の再開において、INT 3命令で置き換えられた元の命令を実行する必要があるので、ブレークポイントを解除し、元の命令に戻すのであるが、この時、再び同じルーチンの呼び出しを検出するため、シングルステップモードで実行を再開する。シングルステップモードにするとCPUは1命令を実行した直後にフォルトクラスのデバッグ(DB)例外を発生させる。これはWindowsでは例外コードEXCEPTION_SINGLE_STEPのDEBUG_EXCEPTIOINイベントとして通知される。

DEBUG_EVENTイベント例外発生時(シングルステップ例外)には、上記の通り、該当のブレークポイントを再設定する。

これらをデバッグイベント別にまとめると次のようになる。

  • CREATE_PROCESS_EVENTイベント発生時
    • ロードされたモジュールにデバッグ情報があれば、必要に応じて各ルーチンのコードの開始・終了位置にブレークポイントを設定
  • LOAD_DLL_EVENTイベント発生時
    • ロードされたモジュールにデバッグ情報があれば、必要に応じて各ルーチンのコードの開始・終了位置にブレークポイントを設定
  • UNLOAD_DLL_EVENTイベント発生時
    • アンロードされたモジュールで設定されている全てのブレークポイントを解除
  • EXIT_PROCESS_EVENTイベント発生時
    • アンロードされたモジュールで設定されている全てのブレークポイントを解除
  • EXCEPTION_EVENTイベント発生時
    • ブレークポイント例外(例外コードEXCEPTION_BREAKPOINT)
      • ブレークポイント例外が発生したアドレスから自身が設定したブレークポイントによる例外かを判定。もし、そうならブレークポイントを解除し、その位置からシングルステップモードで実行再開
    • シングルステップ例外(例外コードEXCEPTION_SINGLE_STEP)
      • 該当のブレークポイントを再設定。

こんな感じだろうか???(LOAD_DLL_EVENT、EXIT_PROCESS_EVENTイベント発生時にブレークポイントをわざわざ解除する必要がないかもしれない・・・とかそういう細かい事は気にしないbearing)。

今回はここまでで、次回実際にクラス設計を行ってみる。

| | コメント (0) | トラックバック (0)

Win32デバッグ(4)・・・デバッグ情報(Borland/CodeGear編)

今回は具体的にデバッグ情報の中身を実際に覗いてみるgood。覗いてみると宣言するように、デバッグ情報の構造やファイルフォーマットなどの詳細まで書くと確実に倒れるbearingので、あくまでイメージを掴むために覗くだけにする。ははは・・。というより、基本的にファイルフォーマットはコンパイラなどの開発環境のメーカーによってその製品のバージョンアップ時に改訂されることがあるので、デバッグ情報にアクセスするためのインターフェースだけを公開し、フォーマットの詳細は公開しないのが現在の流れであるようなので、フォーマットの詳細は知りませんbearing

また、Microsoftの開発ツールによって生成されるデバッグ情報はMicrosoftのよって公開されているDbgHelpライブラリもしくはDIA(Debug Interface Access) SDKを使ってアクセスできるので、詳細はMSDNを参照ということで。DbgHelpライブラリはOSインストール時にインストールされるが、DIA SDKはデフォルトでインストールされていないので、使用する場合には、Visual Studioの統合開発環境をインストールするか、もしくは、再配布可能パッケージvcredist_x86.exeをインストールし、COMサーバーなのでmsdiaXX.dll(XXは80とか90)をregsvr32.exeなどでシステムに登録する必要がある。

ここではタイトルにもあるように自分が使っているBorland/CodeGearの開発ツールiよって生成されるデバッグ情報の中身を覗いてみるのであるが、DelphiのIDEに統合されている統合デバッガが利用しているデバッグ情報にアクセスするためのAPIが公開されていないのである。ガーンcoldsweats02

ということ終了・・って事になるなら、普通は初めからこんなタイトルの記事を書かない事からも分かるように、ここでは、TD32デバッグ情報と呼ばれるデバッグ情報を生成でき、また、アクセスするためのAPIも公開されているのでその中身を見てみる。歴史は詳しくわからないが、TD32デバッグ情報はTurbo Debuggerと呼ばれる社名もまだBorlandの頃の古いデバッガで利用していたデバッグ情報の事だと思う・・・

まず、デフォルトではTD32デバッグ情報は生成されないので、TD32デバッグ情報の生成の仕方から。

適当なプロジェクトを作成して、「プロジェクト」メニューから「オプション」メニューを選択、そして、「リンカ」設定を選択、

Win32debug_4_1_3

上の画像のように「EXEとDLLのオプション」の「TD32デバッグ情報を含める」にチェック。

Win32debug_4_2_2

以上であるが、「コンパイラ」設定の「デバッグ情報」「ローカルシンボル」あたりのオプションも生成されるTD32デバッグ情報の詳細レベルの影響するので・・

で、プロジェクトをコンパイルする。これによってTD32デバッグ情報を含んだ実行可能ファイルが生成される。

TD32デバッグ情報にアクセスするためのAPI(BorDebug.DLLとそのCヘッダ)はCodeGearの以下のサイトからダウンロードできる。

また、BorDebug.DLLの説明とDelphi用のヘッダファイルは以下のサイトからダウンロードできる。

英語の文書を読むのが苦にならない人は上記のサイトからダウンロードできるPDFファイルとCヘッダのコメント読んだ方が手っ取り早いので・・bearing

でいよいよ、実際にTD32デバッグ情報を覗いてみるのだが、そのためのツールTD32Scannerを作ってみた(製品に付属のTDUMP.EXEを使えば、中身をダンプできるのであるが、後でBorDebug.DLLを使ってプログラムからアクセスしたいので、その使い方を覚えるために作ってみたbearing)。実行画面は次の通り。

Win32debug_4_3_2

左側の「SubSection Headers」ウィンドウにはTD32デバッグ情報に含まれるサブセクションのヘッダ一覧が表示されている。最初にデバッグ情報の構造やファイルフォーマットまで詳細に書かないと述べたが、話を進めるために最低限の事は書こうと思う。、まずは、サブセクションから。サブセクションとはTD32デバッグ情報に含まれる論理的な情報のグループのことで、サブセクションに含まれる情報によって、次のタイプに分けられる。

  • Module
  • SrcModule
  • AlignSym
  • GlobalSym
  • GlobalPub
  • Names
  • GlobalTypes
  • Browse

順にこれらのうちのいくつかを見ていくが、その前に今回は、次の簡単なコンソールプログラムのTD32デバッグ情報を例にとり話を進めるので・・

1×1から9×9まで計算するプログラムで、Multiplyは2つの値を積を返す関数である。

まずは、Moduleサブセクション。Moduleサブセクションはモジュールに関する情報が格納されているサブセクションである。Delphiでいうとモジュールは.DCU(Delphi Compiled Unit)ファイルに相当する。上の画像より今回の例ではModuleIndexが1から7までの7つのModuleサブセクションがあることがわかる。「SubSection Headers」ウィンドウのグリッドからModuleIndexが7のModuleサブセクションをダブルクリックしてその詳細を見てみると、次のようになる。

Win32debug_4_5_2

上の画像のNameIndexより、モジュールTD32Target.DCUに関するModuleサブセクションであることが分かる。

また、Segmentsグリッドにはそのモジュールに含まれるコードが最終的な実行可能ファイルのどこに含まれているかの対応関係を表す情報が表示されている。

Windowsにおける実行可能ファイルのフォーマットであるPE(Portable Executale)フォーマットの場合、コンパイラによって生成される機械語命令は最終的にはセクションと呼ばれるブロックに格納されるが、先ほどのSegmentsグリッドのSegment列はそのセクションのインデックスを表す。

この事から、先ほどの画像より、例えば、TD32Target.DCUに含まれるコードがセクションインデックス1の(コード)セクションのオフセット0x00007070からオフセット0x00007213までの420バイトの領域に含まれている事が分かる(ややこしいが、TD32Scannerにおいて、Segmentと表記されているものは、PEファイルにおけるセクションのインデックスを表すので注意・・・・bearing)。

次に、ソースに関する情報が格納されているSrcModuleサブセクション。ここでは、TD32Target.DCUの元になるソースに関する情報を見るとして、「SubSection Headers」ウィンドウのModuleIndexが7のSrcModuleサブセクションをダブルクリックする。

Win32debug_4_6

SourceFilesグリッドより、TD32Target.DCUは1つのソースファイルTD32Target.dprから構成されていることが分かる。

次に、ローカルシンボルに関する情報が格納されているAlignSymサブセクション。SrcModuleの時と同じように、TD32Target.DCUに含まれるシンボルを見るとして、ModuleIndexが7のAlignSymサブセクションをダブルクリックする。

Win32debug_4_8

Symbolsグリッドには含まれるすべてのシンボルが列挙されている。SymbolKindはシンボルの種類を表すがその一覧はここでは省略するとして、ここではローカルプロシージャを表すシンボルであるLPROC32シンボルの詳細を見てみる。例えば、上の画像において、上から2つめのLPROCシンボルを選択してみる。

Win32debug_4_9_2

NameIndexより自分で定義したMultiply関数のシンボルである事が分かる。また、Segment、Offset、Lengthより、この関数の機械語命令のコードがセクションインデックス1のセクションのオフセット0x00007070から0x0000708Bまでの28バイトの領域に含まれていることが分かる。

とまぁ、残りは省略するが、デバッグ情報の内容とはこんな感じなのである。

作ったTD32ScannerはSkyDriveにアップロードしておいた。現在、GlobalTypesサブセクションにはしっかり対応していないのでsad。型に関する情報多すぎbearing

ふぅ。

| | コメント (0) | トラックバック (0)

Win32デバッグ(3)・・・デバッグ情報

前回までで、Win32のデバッグAPIなどを見てきて、GetThreadContextやWriteProcessMemory関数を使えば、スレッドのコンテキストやプロセスのアドレス空間のメモリを書き換えることができることなどと書いたが、デバッガやプロファイラなどの実用的なソフトウェアを作成するにはこれだけではまだ不十分なのである。

ということで、今回はそのような実用的なソフトウェアを作成するために必要となるであろうデバッグ情報について。

デバッグ情報とは、コンパイラやリンカによって生成される実行可能モジュールなどをデバッグするために必要な情報(そのまんまじゃん・・wobbly)、例えば、ソースファイル内で定義した関数なら、実行可能モジュールを実行すると、ローダーと呼ばれるOSのコンポーネントによって、実行可能モジュールのイメージがプロセスのアドレス空間にロードされ、実行が開始されるが、ロード後のプロセスのアドレス空間における関数のコードの開始と終了位置のアドレスを求めるために必要な情報などそのような類似の情報の総称の事である。

生成されるデバッグ情報の詳細レベルは使用しているコンパイラやリンカの種類やまたそれらのオプションスイッチによって変わるが、まぁ、ソースファイル内の行からロード後のアドレスを求めるための行レベルの情報など程度の差こそあれ基本的に似たような情報が格納されているのでここでは問題にしない。

また、デバッグ情報はその情報がシンボルとして格納されているのが一般的なのでシンボリックデバッグ情報とも呼ばれる。シンボルには、宣言した変数や関数などのソースファイル内に現われる具体的なシンボルの他にもソースコードをコンパイル・リンクしたコンパイラやリンカの情報を表すシンボルなどもあり、これも使用しているコンパイラやリンカによって異なるでここでは問題にしない。

Microsoftの統合開発環境Visual Studioを使ってる場合、基本的にそのような情報は拡張子PDBのプログラムデータベースファイル(Program Database File)と呼ばれるファイルに格納され、自分が使ってるBorland/CodeGearの統合開発環境だとorz・・・・

と今回はこれぐらいにするが、ちょっと抽象的な説明ばかりなので、次回以降に具体的にデバッグ情報というものの中身を簡単に覗いてみようかなと・・

最後に、前2回、えらく、長くなってしまったが、ココログにホームページを作成する機能があるっぽさそうなので、ホームページとして作成すれば良かったのかもdespair。まず、このブログの幅leftrightが狭いので広げたいのであるが・・・とりあえず、このネタはブログの記事として終わらせるとして、今度使ってみようgood

| | コメント (0) | トラックバック (0)

Win32デバッグ(2)・・・クラス設計

前回はWin32のデバッグAPIの基本的なことを書いたが、今回はより実践的な事をやろうと思う。

前回、待機関数WaitForDebugEventでDEBUG_EVENT構造体に発生したイベントに関する情報が返されると書いたが、最初にこの構造体を内容を簡単に見てみる。まずは、DEBUG_EVENT構造体の定義から(定義だけはC言語を使うので、あしからず・・)

前半の3つメンバと後半の共用体の部分から構成されている。まずは、前半の3つのメンバから。これらは前回も触れたが、dwDebugCodeEventには、デバッグイベントの種類、dwProcessId、dwThreadIdにはデバッグイベントが発生したプロセスとスレッドの識別子がそれざれ返される。残りの共用体の部分には、デバッグイベントの種類に応じた追加の情報が返されるのであるが、例えば、CREATE_PROCESS_DEBUG_EVENTイベント発生時には、共用体の部分にCREATE_PROCESS_DEBUG_INFO構造体として返される。

順にCREAET_PROCESS_DEBUG_INFO構造体の定義は

となっている。例えば、上記構造体において、hProcess、hThreadには作成されたプロセス、スレッドのハンドルが返されるのであるが、シングルスレッドアプリケーションをデバッグするのならまだしも、マルチスレッドアプリケーションさらにはCreateProcess関数でDEBUG_PROCESSオプションを指定して、子孫のプロセスまでデバッグする場合、これらの情報を適切に管理しておくと、デバッガAPIを使った何かしらの実践的なアプリケーション(例えば、デバッガ自体やプロファイラなど)を作成する時に楽なので、それらをさっさと設計してしまおうというのが今回の趣旨なのである。

と、前置き長すぎbearing

ということで、ここでは、クラスとして設計するのであるが、まずは、プロセスから。デバッグ対象のプロセスの作成、終了は述べたとおり、CREATE_PROCESS_DEBUG_EVENT、EXIT_PROCESS_DEBUG_EVENTイベントで通知され、また、プロセスはプロセス識別子で一意に識別できるので、例えば、プロセスを表すクラスTProcessは次のようになる。

ProcessIdプロパティはプロセスのプロセス識別子、Handleプロパティはプロセスのプロセスハンドルを表す、また、プロセスは1つ以上のスレッドから構成されるので、そのリストであるTThreadListクラスのThreadsプロパティ、同様に、プロセスは、1つ以上のモジュールから構成されるので、そのリストであるTModuleListクラスのModulesプロパティからなる。また、プロセスを強制終了するTerminiateメソッド、プロセスのアドレス空間のメモリを読み書きできるようヘルパーとしてReadXXX、WriteXXXメソッドも追加してある。ReadXXX、WriteXXXメソッドの実装は前回に出てきたAPIを使って、こんな感じ。

次に、複数のプロセスをデバッグできるようにTProcessクラスのリストを保持するTProcessListクラスを定義する。

Addメソッドはリストに指定のプロセス識別子のプロセスを追加する。戻り値は上記のTProcessクラスのインスタンスで、重複したプロセス識別子を指定すると例外が発生する。Removeメソッドは、指定のプロセス識別子のプロセスをリストから削除する。Itemsプロパティは指定したプロセス識別子のプロセスを表すTProcessクラスのインスタンスを返す(存在しなければnil)。

同様に、スレッドを表すTThreadクラス、モジュールを表すTModuleクラスとそれらのリストクラスであるTThreadList、TModuleListクラスを定義する。プロセス、スレッドの場合はそれぞれプロセス識別子、スレッド識別子で一意に識別できるが、モジュールの場合は、モジュールがロードされたアドレスを使って一意に識別する(実行イメージやDLLはローダーによって衝突しないようプロセスのアドレス空間に配置されるのでロードされたアドレスはプロセス内で一意である、また、CREATE_PROCESS_DEBUG_EVENT、EXIT_PROCESS_DEBUG_EVENT、LOAD_DLL_DEBUG_EVENT、UNLOAD_DLL_DEBUG_EVENTイベント発生時にDEBUG_INFO構造体にそのアドレスが返される)。

TProcessとそのリストTProcessListと同様の設計になっているが、TThreadクラスには、スレッドのコンテキストを取得・設定するGetContextメソッド、SetContextメソッドを、また、スレッドを再開・停止・強制終了するResume、Suspend、Terminateメソッド、TModuleクラスには、モジュールのPEファイルのセクションヘッダを読み込むReadSectionHeadersメソッドをヘルパとして追加してある。

と、以上のクラスを定義すれば、各デバッグイベントのイベントハンドラは例えば次のように書ける。

最後に今回、作成したプログラムのダウンロードはSkyDriveのここから。これだけじゃ、ドライバが無いのでコンパイルもできないがとりあえず、アップロードしといた。Debug.pasは、今回作成したTProcessなどのクラスを定義したユニット、Debugger.pasでは、デバッグループやイベントハンドラを含むTDebuggerクラスを定義してある。複数のプロセスをデバッグできるようスレッドとして動作するようにしてある。

以上。ふぅhappy02

| | コメント (0) | トラックバック (0)

Win32デバッグ

プログラミングを行ってる人はソフトウェアを作成する過程で特定のソースコード位置などで実行を停止して、その時の変数の値を確認したり、また、1行ずつコードを実行して、処理の実際の流れを確認したりと、デバッガと呼ばれるソフトウェアもしくは機能を利用する事になると思うが、今回はそのようなデバッガやパフォーマンスプロファイラなどを作成するためにWindowsで提供されているデバッグAPIについて。

Windowsで提供されているデバッグAPIはイベント駆動型のモデルを採用していて、デバッグ対象のプロセス内でデバッグイベントと呼ばれるイベントが発生するのを待機し、発生したら発生したイベントに応じて処理を行うという、Windows上でGUIアプリケーションを作成する時に見かけられるWindowsメッセージを処理するためのメッセージループに似たループを行うことになる。発生するデバッグイベントには次のようなものがある。

デバッグイベント
種類 概要
CREATE_PROCESS_DEBUG_EVENT 新しいデバッグ対象のプロセスが作成される、または、既存のプロセスにデバッグのためアタッチされた時に発生
EXIT_PROCESS_DEBUG_EVENT デバッグ対象のプロセスが終了される時に発生
CREATE_THREAD_DEBUG_EVENT デバッグ対象のプロセス内で新しいスレッドが作成される時に発生
EXIT_THREAD_DEBUG_EVENT デバッグ対象のプロセス内のスレッドが終了する時に発生
LOAD_DLL_DEBUG_EVENT デバッグ対象のプロセス内でDLLがロードされる時に発生
UNLOAD_DLL_DEBUG_EVENT デバッグ対象のプロセス内でDLLがアンロードされる時に発生
EXCEPTION_DEBUG_EVENT デバッグ対象のプロセス内で例外が生成された時に発生
OUTPUT_DEBUG_STRING_EVENT デバッグ対象のプロセス内でOutputDebugString関数が呼び出された時に発生
RIP_DEBUG_EVENT

上記イベントの厳密な発生条件などはMSDNのここを参照。

新規にプロセスを作成してデバッグを開始するには、CreateProcess関数で第6引数dwCreationFlagsにDEBUG_PROCESSオプションを指定する。DEBUG_PROCESSを指定するとCreateProcess関数で作成されたプロセスによって作成される子孫のプロセスもデバッグ対象になるが、CreateProcess関数で作成されるプロセスだけに限定する場合は、DEBUG_PROCESSオプションの代わりにDEBUG_ONLY_THIS_PROCESSオプションを指定する(紛らわしいがDEBUG_PROCESSまたはDEBUG_ONLY_THIS_PROCESSのどちららかを指定する。論理和とって両方指定するのではない.ので注意)。

また、既存の実行中のプロセスのデバッグを開始するには、DebugActiveProcess関数で引数に既存の実行中のプロセスのプロセス識別子を指定し、プロセスにアタッチする。

CreateProcessもしくはDebugActiveProcessでデバッグを開始した後は、先ほど述べたループに突入するのであるが、Delphiで書くと全体の流れは次のような感じなる。

WaitForDebugEventはデバッグイベントが発生するのを待機する待機関数で、第2引数にタイムアウトを指定する。第2引数にINFINITEを指定するとこの関数を呼び出した側へWaitForDebugEvent関数から制御が返らない。デバッグベントが発生すると第1引数で指定したDEBUG_EVENT構造体に発生したイベントの種類、イベントが発生させたプロセスとスレッドの識別子、イベントの種類に応じた追加の情報が返される。

デバッグ対象のプロセス内でデバッグイベントが発生するとデバッグ対象のプロセス内のすべてのスレッドの実行が停止されるが、ContinueDebugEventで実行を再開する。

ContinueDebugEventの第1、2引数にはデバッグイベントが発生して、デバッグ対象のプロセスが停止した原因となったプロセス、スレッドの識別子を指定するが、これらの値はDEBUG_EVENT構造体のdwProcessId、dwThreadIdにWaitForDebugEvent関数から返されているのでこれらの値をそのまま指定する(もちろん、デバッグ対象がシングルスレッドアプリケーションで、CreateProcess関数を使ってデバッグを開始するのならCreateProcess関数で返されるPROCESS_INFORMATION構造体の値を指定してもいいのだが、DEBUG_EVENT構造体の値を使った方が汎用的である)。

ContinueDebugEventの第3引数には以下の続行オプションのいずれかを指定するのであるが、これは、ややこしいbearing

  • DBG_CONTINUE
  • DBG_EXCEPTION_NOT_HANDLED

完全に説明すると、Windowsの構造化例外処理(Structured Exception Handling)の話から書かないといけないので、EXCEPTION_DEBUG_EVENTイベント発生時以外は、MSDNのドキュメントに書いてあるように上記2つのオプションに違いはないので、上記サンプルではDBG_CONTINUEオプションを、EXCEPTION_DEBUG_EVENTイベント発生時は、発生した例外の種類がシングルステップ例外(EXCEPTION_SINGLE_STEP)、ブレークポイント例外(EXCEPTION_BREAKPOINT)以外の時は、通常の例外処理を行わせるためにDBG_EXCEPTION_NOT_HANDLEDを指定、シングルステップ例外、ブレークポイント例外の場合は、DBG_CONTINUEを指定した。

ということで。ははは。

とMSDNのドキュメントを反復してるだけでbearing、MSDNのドキュメントを読んだ方が正確だし手っ取り早いのであるが、ここでは、実際にデバッグAPIを使った時の注意点をいくつか。

まず、CREATE_PROCESS_DEBUG_EVENT、LOAD_DLL_DEBUG_EVENTイベント発生時に返されるファイルハンドル(hFileメンバ)について。これらイベントの発生時、DEBUG_EVENT構造体を通して、ロードされたプロセスの実行イメージのファイルハンドル(CREAET_PROCESS_DEBUG_EVENTの場合)、もしくは、ロードされたDLLのファイルハンドル(LOAD_DLL_DEBUG_EVENT)が返されるが、MSDNのドキュメントにもあるように、使い終わったらこのハンドルは明示的にCloseHandle関数でクローズしないとハンドルがオープンされたままになるので注意。上記のプログラム例では、88行目あたり。

また、

特に、EXIT_PROCESS_DEBUG_EVENTイベント発生時に、発生直後すぐにブレイクしてデバッグループを抜ける例をよく見かけるが、これだと、CREATE_PROCESS_DEBUG_EVENTイベントなどで返されるプロセスハンドルやスレッドハンドルがオープンされたままになるので、通常は1回、ContinueDebugEvent関数を呼んだ後にループからブレイクする方が無難である(ContinueDebugEvent関数で自動的にハンドルがクローズされるので)。上記のプログラム例では、66行目あたり。

最後は、WaitForDebugEvent関数はDEBUG_PROCESSまたはDEBUG_ONLY_THIS_PROCESSオプションを指定したCreateProcess関数またはDebugActiveProcess関数を呼んでデバッグを開始したスレッドでしか呼ぶことができないとの事。

とここまでくれば、後はデバッグAPIの他の関数である

  • ReadProcessMemory
  • WriteProcessMemory

を使えば、プロセスハンドルからそのプロセスのアドレス空間を読み書きする事ができるし、

  • GetThreadContext
  • SetThreadContext

を使えば、スレッドのコンテキスト(x86プラットフォームのWindowsの場合は、EAXやEBXなど各レジスタなど)を読み書きできる。これらのAPIは何もDEBUG_PROCESSまたはDEBUG_ONLY_THIS_PROCESSオプションを指定したCreateProcess関数によって作成されたプロセスやDebugActiveProcessでアタッチしたプロセスでなくてもよいのだが、使用する時は基本的に対象のプロセス・スレッドをSuspendThread関数などで停止していないとおかしくなると思う(WaitForDebugEvent関数でデバッグイベントを受信した時はデバッグ対象プロセス内のすべてのスレッドが停止しているのでSuspendThread関数で停止する必要はないが・・・)。

以上長々と書いたが次回以降は具体的に使ってみようと。

というより、TrueType/OpenTypeフォントの時の成果物であるT2FAnalyzerと同じようにブログ書きながら最終的に実用的?なソフトウェアが出来上がればいいのであるが・・・ははは。

| | コメント (0) | トラックバック (0)

MLang(Multilingual Language)の続き(4)・・・IMLangFontLink

今回はMLangの続きで、IMLangFontLinkインターフェース。フォントリンクとは、選択したフォントで表示できない文字を、その選択したフォント(ベースフォント)にリンクしたフォントで表示できれば、それを使って表示するメカニズムのことであるが、このインターフェースを利用すれば、フォントリンクを利用したり、カスタムフォントを作成できるとのことなので、実際に使ってみる。

IMLangFontLinkインターフェースにいくつかのメソッドがあるが、フォントリンクのサービースを利用するには、主にその内の

  • GetFontCodePagesメソッド
  • GetCharCodePagesメソッド
  • MapFontメソッド
  • ReleaseFontメソッド(とResetFontMappingメソッド)

を使用する。まずは、GetFontCodePagesメソッドである。MSDNの説明によれば、

Gets the set of code pages whose characters can be output by the specified font on the given device context.

である。指定したデバイスコンテキスト上で指定したフォントによって表示できる文字のコードページ集合が返される。最初、指定したフォントだけで表示できる文字のコードページ集合が返されると勘違いしてしまったが、返されるのは指定したフォントとそれにリンクしたフォントで表示できる文字のコードページ集合である。

次に、GetCharCodePagesである。これは指定した文字のコードページ集合を求めるメソッドである。指定した文字が指定したフォント(とそれにリンクされたフォント)で表示できるか判定するには、上記2つのメソッドを使って返されたコードページ集合のビットごとの論理積を取ればよい。

MapFontメソッドは指定したコードページ集合とソースフォントからそれを表示できるリンクしたフォント?を返すメソッドらしい??。

と、MapFontメソッドで返されるフォントの理解に苦しむが、いつも通りU+0000からU+FFFFの各文字に対してMapFontメソッドを呼び出しどのようなフォントが返されるか調べてみる。実行結果は次の通り。

Fontlink1_1_3

ベースフォントはTahomaである。Tahomaフォントを指定してGetFontCharCodePagesメソッドを呼び出して返されるコードページ集合は2032127であるが、そのコードページ集合に含まれる各コードページは、

Fontlink1_2

である(まぁ、ここらへんは実行環境によって変わるはず)。先ほどの、GetFontCodePagesメソッドの説明で、返されるコードページ集合は、指定したフォントとそれにリンクしたフォントによって表示できる文字のコードページ集合と書いたが、Tahomaは日本語の文字に対するグリフを持っていないが、上の画像に日本語のシフトJISに対応するコードページが含まれていることからも、リンクしたフォントも考慮されている事が分かる。また、最初の画像より、コードポイントU+0041の「A」(LATIN CAPITAL LETTER A)に対するMapFontメソッドによって返されるフォントの名前がArialであることが分かる(MapFontメソッドによって返されるのはHFONT型のフォントハンドルなのでそれからWin32APIのGetObject関数を使いLOGFONT構造体を取得してそのlfFaceNameメンバの値をここでは表示した)。

次に、コードポイントU+3042の「あ」(KATAKANA LETTER A)を見てみる。

Fontlink1_3

MS PゴシックフォントがMapFontメソッドによって返されているのが分かる。

ところで、この結果をCharCodePagesが0の結果でフィルタしてみると、

Fontlink1_4_2

MSDNのヘルプに、IMLangFontLinkはMicrosoftのコードページベースでフォントリンクを行うと書いてあるように、CharCodePagesが0の文字に対してMapFontメソッドの呼び出しが失敗していることが分かる(FontNameがFailedになってるが実際にFailedという名前のフォントが返されたのではない事に注意)。

ふむふむ。

最後にいつものように、作ったプログラムはここのMultiLang.zip。メモリ食うし重いので注意を。

| | コメント (0) | トラックバック (1)

GetStringTypeEx

最近ブログ書くのさぼってた。モチベーションを維持するのは大変bearing

ということで久しぶりに書いてみる。お題はWin32のGetStringTypeEx。指定した文字列中の各文字に文字情報を取得するAPIである。取得できる文字情報の種類は

  • CType 1(文字種情報)
  • CType 2(左右文字方向情報)
  • CType 3(テキスト処理情報)

の3種類である。CType 1で返される種類は、

CType 1
定数 種類 意味
C1_UPPER 0x0001 大文字
C1_LOWER 0x0002 小文字
C1_DIGIT 0x0004 10進数字
C1_SPACE 0x0008 スペース文字
C1_PUNC 0x0010 区切り記号
C1_CNTRL 0x0020 制御文字
C1_BLANK 0x0040 ブランク文字
C1_XDIGIT 0x0080 16進数字
C1_ALPHA 0x0100 言語上の文字(表音文字・音節文字・表意文字)
C1_DEFINED 0x0200 他のC1_*の文字でない文字

で、ANSI CとPOSIX(LC_CTYPE)の文字種関数をサポートするそうである。また、1度に1種類の情報しか取得できないので、上記3種類すべての情報を取得するにはGetStringTypeExを3回呼び出す必要がある。この種類のAPIはMSDNのマニュアルを参照した方が手っ取り早いので、詳細はここを参照。

ということで、ここでは、実際にこのAPIを使ってみる。いつも通り?各Unicode文字(コードポイントU+0000からU+FFFFまで)に対してどのような結果が返されるか列挙してみる。実行結果は次の通り。

Getstringtypeex1

当たり前のことを書くが上の画像においてコードポイントU+0041の「A」(LATIN CAPITAL LETTER A)の文字に対して、CType 1の大文字であることを表すC1_UPPERフラグがセットされていることがわかる。

まぁ、この種のAPIは結果を実際に目で見て確認した方が早いということで・・

作ったプログラムはここのGetStringTypeEx.zip。以前は、プログラムをココログにアップロードしてたのだが、アップロードできるファイルの最大サイズが1Mバイトまでになってしまったので、MicrosoftのストレージサービスSkyDriveを利用させていただくことにしましたbearing。この際、ブログも引っ越すかな?はは。

ところで、上の画像でChar Info列に各Unicode文字のUnicodeプロパティという情報が表示されているのだが、この情報を表示させるためには実行ファイルGetStringTypeEx.exeと同じフォルダにUnicodeコンソーシアムのサイトからダウンロードできるUnicode Character Databaseの各ファイルを置いてください。具体的には、ここ

  • UnicodeData.txt
  • Scripts.txt
  • Blocks.txt

の3つです。

そいうえば、このプログラムを作ってる時に、Unicode Character Databaseの情報を表示できるビューアみたいなプログラムを作ってみようかなと思いついたgood。というのも、そのようなツールはインターネットで検索すれば何種類か見つかるのだが、自分好みのがほしい。

| | コメント (0) | トラックバック (0)

MLang(Multilingual Language)の続き(3)・・・IMLangCodePages

今回はIMLangCodePagesインターフェース。

IMLangCodePagesは与えられたUnicode文字または文字列を含むコードページ集合(a set of code pages)を求めるメソッドを提供する。また、個々のコードページ識別子とコードページ集合間の変換を行うメソッドも提供する。コードページ集合は、32ビット符号無し整数(DWORD)として表現され、32ビット符号無し整数の各ビットが特定のコードページに対応する(コードページ集合にコードページが含まれていれば、そのビットが1にセットされる)。各ビットがどのコードページに対応するかはドキュメント化されていないが、IMLangCodePagesインタフェースのCodePagesToCodePageCodePageToCodePagesメソッドを使って、コードページ集合に含まれている各コードページを次のように求めることができる。

Unicode文字または文字列からそれらコードページ集合を求めるにはGetCharCodePagesGetStrCodePagesメソッドを使う。

ということで実際にこれらのメソッドを使ってコードポイントがU+0000からU+FFFFまでの範囲の各Unicode文字のコードページ集合を次のコードのような感じで求めてみる(サロゲートペアや結合文字は通常単独では使われない等細かい事は考えないbearing)。

実行結果は次の通り。

Mlang3_1

上の画像より、U+3042の「あ」(KATAKANA LETTER A)のコードページ集合に、シフトJISを表すコードページ932に含まれていることがわかる。また、簡体字中国語のGB2312を表すコードページ936も含まれているが、ネットで検索するとどうやらGB2312に日本語の平仮名や片仮名などの文字が含まれているらしい・・

次に、U+FF71の「ア」(HALF WIDTH KATAKANA LETTER A)のコードページ集合を見てみる。

Mlang3_2

半角はさすがにGB2312などに含まれていないか・・・

最後に、U+0041の「A」(LATIN CAPITAL LETTER A)。

Mlang3_3

案の定、U+0000からU+007Fの範囲はASCIIと互換であるので、ほとんどの各国語キャラクタセットに含まれているってことかな??

以上。

| | コメント (0) | トラックバック (0)

MLang(Multilingual Language)の続き(2)・・・IEnumCodePage,IEnumRfc1766

前回の続きであるが、今回はIEnumCodePageIEnumRfc1766インターフェースについて。

IEnumCodePage、IEnumRfc1766インターフェースはMIMEデータベースからそれぞれコードページ、ロケールを列挙するインターフェースで、列挙オブジェクトであるCode Page EnumerationオブジェクトLocale Enumeration オブジェクトによって実装されていて、そのメンバであるNext,Skip,Resetのメソッドを使って、次の項目を取得(Next)、次の項目をスキップ(Skip),列挙位置をリセット(Reset)する事ができる。

そして、これらのインターフェースは、IMultiLanguage::EnumCodePages、IMultiLanguage::EnumRfc1766メソッドを呼び出してそのポインタを取得できる。

ということで実際に使ってみる。

まずは、IEnumCodePageから。

Mlang2_1

::EnumCodePagesメソッドを呼び出す時にFlagsパラメータで列挙するコードページを制御できるのだそうだが、ややこしそうなので深くは立ち入らない。上の画像で日本語EUCのエントリが見つかったりもする。

コードは次のような感じ。

続いて、IEnumRfc1766。

Mlang2_2

コードは次のような感じ。

以上。

| | コメント (0) | トラックバック (0)

MLang(Multilingual Language)

今回はMLangについて。MLang(Multilingual Language)は、国際化・多言語の環境に存在する問題を開発者が解決するのを助ける様々なサービスの集合で、これらによって開発者は国際化・多言語の環境における様々な問題を取り扱えるようになる。また、WindowsのCOM(Component Object Model)を使ってインプロセスサーバとして実装されている(MLang.dll)。MLang.dllはインターネットエクスプローラ4.0以上と共に配布されているので、今日の環境においてMLang.dllが存在しないと言った事は稀であると思われる。

MLangによって提供される機能は次のようになる。

  • MIMEデータベースからのコードページとロケールの列挙(IEnumCodePage,IEnumRfc1766)
  • コードページ、ロケール、キャラクタセットの情報の取得(IMultiLanguage)
  • 異なるキャラクタセット間のキャラクタセット変換
  • 与えられた文字または文字列を含むコードページの決定(IMLangCodePages)
  • 文字列データから使われている可能性のあるコードページ、言語の決定(IMultiLanguage)
  • 様々なキャラクタセットからの文字を一度に表示するためのカスタムフォントの作成(IMLangFontLink)
  • コンソールベースのアプリケーションのためのロケールに基づいた行分割(IMLangLineBreakConsole)

と、色々な便利な機能が提供されているのだが、これらの内のいくつかを実際に使ってみる。上記の括弧内は主に使用するインターフェースである。また、上記の機能を利用するには、最初にMultiLanguageオブジェクトというオブジェクトを作成する。MultiLanguageオブジェクトはMLangによって実装される他の全てのオブジェクト(Code Page EnumerationオブジェクトLocale Enumerationオブジェクトなど)とインターフェースへのアクセスを提供する。MultiLanguageオブジェクトによって実装されているインターフェースは次のようになる。

  • IMLangCodePages
  • IMLangFontLink
  • IMLangFontLink2
  • IMLangLineBreakConsole
  • IMultiLanguage
  • IMultiLanguage2

MultiLanguageオブジェクトを作成するにはCLSIDパラメータにCLSID_CMultiLanguageを指定してCoCreateInstanceを呼び出す。

Delphiで利用する場合はこんな感じ。

注:CreateComObjectは内部でCoCreateInstanceを呼び出すDelphiのラッパー関数である。

今回はここまで。

| | コメント (0) | トラックバック (0)

コードページの続き(3)・・・コードページの列挙(EnumSystemCodePages)

以前といっても1年以上前にコードページに関する事を書いたが(ここここ)、今回はコードページの列挙について。EnumSystemCodePages関数で、システムにインストールされているコードページまたはシステムによってサポートされているコードページを列挙できる。このEnumSystemCodePages関数の使い方は他の列挙する関数ど同様、アプリケーション定義のコールバック関数のポインタを引数に取る。詳細は簡単なのでSDKを参照

ということで、以前の記事で作ったプログラムを改造してEnumSystemCodePages関数を使ってコードページを列挙してみる。Windows2000上でサポートされているコードページを列挙(CP_SUPPORTED)した結果は次の通り。

Enumsystemcodepages1

133個のコードページが列挙された。これと「コントロールパネル」->「地域のオプション」->「システムの言語設定」->「詳細」で表示される「地域の詳細オプション」ダイアログに列挙されるコードページ変換テーブルの一覧と比較したのだが、微妙に違っているorz・・・

Enumsystemcodepages2

まぁ、ここでは余り深く考えるのはやめて、次に列挙されたコードページ識別子(Code Page Identifier)に対応する文字セット識別子(Charset Identifer)をTranslateCharsetInfo関数を使って求めてみる。文字セット識別子は例えば、CreateFont関数で論理フォントを作成する時にLOGFONT.lfCharsetフィールドに指定するやつです。

実行結果は次のとおり。

Enumsystemcodepages3

上の画像より例えば、日本語のシフトJISを表すコードページ932に対応する文字セットは128のSHIFTJIS_CHARSETであることが分かる。

以上。

些細なプログラムであるが作ったプログラムはここ

| | コメント (0) | トラックバック (0)

数値型の憂鬱(2)

前回の続きである。

高精度の数値の演算つまり多倍長演算のルーチンを自作するのもいいのだが、なるべくなら既存の信頼におけるものを使いたいものである。Googleなどの検索サイトで多倍長演算ライブラリを検索すれば、様々なライブラリが見つかり、開発環境の言語に合わせて選択できるが、WindowsのOS自体に用意されているライブラリを使う事もできる。

正確に言うと、WindowsのOS自体というより、WindowsのOLEオートメーションで主に使われるバリアントのために用意されているライブラリで、oleaut32.dll

  • VarDecAdd(加算)
  • VarDecSub(減算)
  • VarDecMul(乗算)
  • VarDecDiv(除算)
  • VarDecAbs(絶対値)
  • VarDecFix
  • VarDecInt
  • VarDecRound
  • VarDecNeg
  • VarDecCmp(比較)

などのAPIを使って多倍長演算を行うことができる。これらのAPIは、主に、DECIMAL構造体へのポインタを引数に取り、DECIMAL構造体は次のように宣言されている。

Numeric2_1_3 

wReservedフィールドはバリアントに格納されているデータの型を表し、Decimal型の場合、vt_decimal(14)、scaleフィールドは小数点の位置を表し、signフィールドを符号を表し、hi32、low32、mid32フィールドにスケーリングされた数値が整数値として格納される。

これとODBCを組み合わせてもよいのだが、上述のDecimal構造体のスケーリングされた整数値を格納する領域が96ビットなので、2の96乗つまり最大精度28桁までの数値しか表現できないwobbly。28桁だと最大精度38桁のOracleのNUMBER型はもとよりDB2のNUMBER型すら完全に表現できない・・・

んー。OSで多倍長演算ももっとサポートしてほしいのだが・・あまり、そのような要望はないのだろうか?

| | コメント (0) | トラックバック (0)

ScriptGetPropertiesの続き

前回の続きである。

前回はScriptGetPropertiesよって、グローバルスクリプトプロパティテーブル内の各スクリプトの属性を列挙したが、今回は、各Unicodeコードポイントにどのスクリプトが割り当てられているかを調べてみる。これは単純にScriptItemizeによって得られた実行単位のSCRIPT_ANALYSIS構造体のeScriptメンバを調べただけである。

実行結果はこんな感じに。

Scprops2_1

上の画像から、例えば、コードポイントがU+3000~U+D7FFの文字には、12というスクリプトインデックスのスクリプトが割り当てられているがわかる。ちなみに、この範囲には日本語のひらがながなどが含まれているが、前回作成したプログラムからスクリプトインデックスが12というスクリプトの属性を見ると、

Scprops2_2

fComplex=falseと、コンプレックスシェーピングが必要ないことが分かる。前にも書いたがここらへんはUniscribeのバージョンによって変わるので注意を。

で先ほどのコードポイントとスクリプトインデックスの対応をよく見ると、

Scprops2_3

3088など有効な範囲外のスクリプトインデックスが割り当てられているコードポイントがいくつかある。上の例のU+0621~U+065Fはアラビア文字のブロックの一部で、自分のバージョンのUniscribeではサポートされていない文字だからなのか、はたまた、前後に他の文字が必要で通常単独では存在しない文字(今回は、便宜上、1コードポイントづつScriptItemizeを呼び出した)だからなのかは調べていないから分からないが、グローバルスクリプトテーブルにアクセスする時に、ScriptItemizeによって分割される実行単位のスクリプトインデックス(eScript)の範囲をチェックした方が無難そうである。ちなみに、有効なスクリプトインデックスの範囲は、MSDNのヘルプによると、0~piNumScripts-1(piNumScriptsはScriptGetPropertiesの第2パラメータで返される値)である。

せっかくなので、作ったプログラムをダウンロードできるようにしておいた。プログラムはここのGetStringProps.zip。作ったプログラムはこんな感じ。

Scprops2_4

| | コメント (0) | トラックバック (0)

ScriptGetProperties

最近、寒い。寒くて集中する必要がある作業に集中できん。というより、そういう事は避けていた・・・

今回はScriptGetProperties。Uniscribeでは、まず、テキストをScriptItemizeでシェーピングエンジンの変わり目や読む方向の変わり目などで、実行単位(ラン)に分割して処理するが、この分割された実行単位のスクリプトに関する情報にScriptGetPropertiesというAPIを使ってアクセスできる。

具体的には、ScriptGetPropertiesでスクリプトに関する情報が格納されたグローバルスクリプトプロパティテーブル(Global Script Properties Table)へのポインタが返されるが、これと、各実行単位のSCRIPT_ANALYSIS構造体のeScriptメンバに格納されているスクリプトインデックスを使って実行単位のスクリプトの情報にアクセスできる。

で、今回はScrigetGetPropertiesだけを使って、グローバルスクリプトテーブル内の各スクリプトの属性(SCRIPT_PROPERTIES構造体)を表示させてみた。こんな感じに・・

Scprops1_2

言語IDの列はSCRIPT_PROPERTIES.langidメンバをわかり易く表示してある。この列はそのスクリプトで使われる言語を表す。複数の言語で使われるスクリプトの場合、その主な?言語が返される。fComplexメンバはそのスクリプトがComplex Shapingを必要とするかを表す。細かいことはMSDNを参照ということで・・

ちなみに、上の画像は自分のWindows2000の環境上で実行したものであるが、自分のWindows2000上では、計53種類のスクリプトが返された。

で、同じプログラムをWindowsXP SP2上で実行してみる。、

Scprops2

上の画像のように、計74種類のスクリプトが返された。LANG_MONGORIANなどモンゴル語??系のスクプリトが追加されていることもわかる。各スクリプトのスクリプトインデックスやUnicodeコードポイントの範囲などは、Uniscribeのバージョンアップで変わったりするので、公開はされておらず、不定であるので注意を。

| | コメント (0) | トラックバック (0)

デフォルトオブジェクト

今回はデバイスコンテキストのデフォルトオブジェクトについて。

昔、MSDNのどこかのページでこのデフォルトオブジェクトについての記述を見たような記憶があるのだが、あらためて捜してみると見つからない(見つかったらリンクを貼ろうと思っているが)。仕方がないので、他のサイトを参考にすると、デバイスコンテキストには、デフォルトオブジェクトとしてストックオブジェクトの黒のペンと白のブラシが選択されているそうなのでこれを確認してみる。デバイスコンテキストに現在選択されているオブジェクトのハンドルはWin32APIのGetCurrentObjectで取得できる。また、ストックオブジェクトのハンドルは同じくGetStockObjectで取得できるので、これらのAPIによって返されるハンドルをWM_PAINTメッセージの処理内で表示してみた。

Defobjects1

上段がペンについて。引数にOBJ_PENを指定してGetCurrentObjectで現在選択されているペンオブジェクトのハンドルと引数にBLACK_PENを指定してGetStockObjectでストックオブジェクトの黒のペンのハンドルを表示した。確かにハンドルが一致する。

下段がブラシについて。ペンと同様に引数にOBJ_BRUSHを指定してGetCurrentObjectで現在選択されているブラシオブジェクトのハンドルと引数にWHITE_BRUSHを指定してGetStockObjectでストックオブジェクトの白のブラシのハンドルを表示した。こちらもハンドルが一致する。

一番下の図形はデフォルトのペンとブラシを使ってWin32APIのRectangleを使って描画した矩形である。確かに輪郭が黒で内部が白で塗りつぶされている。

最後にフォントについて。フォントはストックオブジェクトのSYSTEM_FONTフォントが選択されるそうなので、同様に試してみた。結果は下の通り。

Defobjects2_1

現在選択されているフォントオブジェクトのハンドルは引数にOBJ_FONTを指定してGetCurrentObjectを呼び出せば取得できる。

ちなみに、デバイスコンテキストには厳密にはクラスデバイスコンテキスト、コモンデバイスコンテキスト、プライベートデバイスコンテキストの3種類があるが、ここでは、コモンデバイスコンテキストを扱った。

| | コメント (0) | トラックバック (0)

フォントの列挙

今回はフォントの列挙。

Windowsではフォントを列挙するAPIとしてEnumFonts、EnumFontFamilies、EnumFontFamiliesExが用意されているが、MSDNによると、前の二つは16bit版の互換性のために用意されているそうなので、ここでは、EnumFontFamiliesExを使うことにするが、このAPIを使ってフォントを列挙してみる。ここでは、フォントダイアログの一覧に列挙されているフォントの一覧を取得してみる。

Enumfont1_1

EnumFontFamiliesでは、第2引数として渡すLogFont構造体のlfCharsetとlfFaceNameフィールドによって、列挙するフォントを制御するが、まずは、それぞれにDEFAULT_CHARSETと空文字列を設定して、すべて列挙してみる。結果は下の通り。

Enumfont1_2_7      

フォントタイプは、コールバック関数の第3引数として返されるFontTypeを使ってフォントの種類(ラスタフォント、ベクタフォント、TrueTypeフォント(OpenTypeを含む))かを判断している。タイプフェイス名(フォントファミリ名)には、第2引数として返されるNewTextMetricExのelfLogFontのlfFaceNameフィールド、文字セットにはNewTextMetricExのelfLogFontのlfCharset、スクリプトにはEnumLogFontExのlfScriptの値を表示している。

MSDNの説明にもあるように、lfCharsetにDEFAULT_CHARSET、lfFaceNameに空文字列を設定するとすべてのタイプフェイスのすべての文字セットが列挙されているのがわかる。で、これをいかにフィルタするかが、肝である。

先ほどの結果をどうフィルタするかだが、タイプフェース名が重複する行を排除すればまず問題ないであろうが、MSDNのCreateFontのfdwOutputPrecisionパラメータの説明にあるように、種類の異なる同じ名前をもつフォントがある可能性があるので、(フォントタイプ、タイプフェース)の組で重複する行を排除したほうがよさそうである。実行結果は下のとおり。

Enumfont1_3

自分の環境ではリストアップされたタイプフェースは50個であった。そして、すべてのフォントを表示するように設定したフォントダイアログでも50個のタイプフェースがリストアップされたのでよさそうである。まぁ、このAPIについてはあまり深入りするつもりはないのでここまで。参考までに、英語であるが、ここここここを読んでみるといいかもしれない。

最後に、タイプフェースとフォントの違いについて触れておく。タイプフェースとは、同一の書体を持つフォントのグループのことである。参考にあげた記事の中でも触れているが、例えば、Arialタイプフェースについて見てみよう。Arialタイプフェースには、スタイルに応じて、4つのフォントが用意されている。標準スタイルのArialフォント、太字スタイルのArial Boldフォント、斜体スタイルのArial Italicフォント、太字・斜体スタイルのArial Bold Italicである。「コントロールパネル」->「フォント」からそれを確認できる。

Enumfont1_4

これら、スタイルの異なる4つのフォントがグループ化され、Arialタイプフェイス(一般的な用語を使うとArialフォント)と呼ばれているのである。ちなみに、上の画面でArial Blackとあるが、これは別グループでArial Blackタイプフェイス(Arial Blackフォント)と呼ばれる。また、MS Sans Serifはベクタフォントであるが、TrueType以外のフォントはフォント名とタイプフェース名が同じになる。また、日本語のWindows環境でよく使うフォントとして、MS ゴシックなどがあるが、通常スタイルのフォントしか用意されていなく、他のスタイルを指定したときは、GDIのラスタライザがエミュレーションでグリフのビットマップを作りだしているようである。なにせ、漢字など収録字数が多いので、各スタイル用のフォントを作るのは大変だからなのかもしれない。

| | コメント (0) | トラックバック (0)

コードページの続き

引き続きコードページのお話。

前回作成したプログラムを動かしてたら、IsValidCodePageでtrueが返ってきてるのに、GetCPInfoExで、情報が取得できてないコードページがあるのに気づいた。次のように。

Cp2_1_7   

うーむ。小一時間色々調べたがよくわからない。で、GetCPInfoExの戻り値をチェックしてなかったので、エラーが発生したらGetLastErrorで拡張エラー情報を表示するように、プログラムを変えてみた。

Cp2_2_2

MSDNの説明のとおり、GetLastErrorでERROR_INVALID_PARAMETER(87)が返ってる。で、あることに気づいた。MSDNの説明でコードページに関して「インストール済み(installed)」または「利用可能な(available)」と表現を使い分けている。で、もう一度、GetCPInfoExの説明を詳しく見ると、

コードページは、システムにインストールされている場合にのみ、有効と判断されます。」

英語版では

A code page is considered valid only if it is installed on the operating system

とある。IsValidCodePageはコードページがシステムにインストールされているか判断するのであって、インストールされていても利用可能であるとは限らないのだ。まぁ、詳しくはわからないが、コーディングする時は要注意だ。

で、今まではWindows2000上で作成したプログラムを動かしていたが、XP上で動かしてみた。

Cp2_3_1

XP上では(デフォルトで?)利用可能なコードページが増えている。コードページ54936のGB18030は中国の文字コードであるらしいが、MaxCharSizeが4でLeadByteが空である。GB18030はどんな文字コードか調べようとグーグルで検索すると、中国政府がメーカーに強制的に採用を義務づけたとかおもしろい話も見つかる(参考1参考2)。まぁ、そんな話は置いといて、国際化アプリケーションを作成する場合、コードページをごりごり切り替えるより素直に内部の文字コードをUnicodeにした方が楽なような気がする。

で、最後に作ったアプリを簡単そうなのでここからダウンロードできるようにしようかなと思ってもみたり。気がむいたらだけど。

| | コメント (0) | トラックバック (0)

Windowsのコードページ

色々話題がぶっとんだりしてるが、まぁ、気にしないでおこう。

で、今回はWindowsのコードページ。現在一般的に使われてるWindowsの内部の文字コードはUnicode(UTF-16LE)になっているが、Unicode以前のさまざまな国・地域専用の文字コードとUnicode間の変換テーブルをコードページと呼び、コードページ識別子(Code Page Identifier)を割り振り管理している。日本語の文字コードに関して言えば、例えば、シフトJISには932というコードページ識別子が与えられている。で、今回は、そのコードページ関連のWinAPIであるIsValidCodePageとGetCPInfoEx(GetCPInfo)を色々試してみた。Windowsでは既に150以上(詳細はここ)ものコードページ識別子が定義されているが、それぞれのコードページに対して、上記のAPIがどのような結果を返すプログラムを作成してみた。実行結果は下図の通り。

Cp1_3

ちなみに、先ほど述べたシフトJISの2バイトで表される2バイト文字の先行バイト(リードバイト)の範囲は、0x80-0x9F、0xE0-0xFCであるが、確かに、上の図でも、(0x80-0x9F)(0xE0-0xFC)となっており結果はよさそうである。また、先行バイトはWin32APIのIsDBCSLeadByteを使っても調べられるが、こちらは、コードページを指定できない。CPINFOEX構造体のMaxCharSizeには、そのコードページ内の文字の最大バイト長が返されるが、シフトJISはDBCSなので、2が返されるのが上図からも分かる。DefaultCharとUnicodeDefaultCharはWideCharToMultiByteとMultiByteToWideCharでMBCS<->Unicode間の変換を行ったときに、変換できない文字に対して割り当てるデフォルト文字を明示的に指定しないときに使用される文字である。

とまぁ、詳細はMSDNを読んだほうが速いのでここまで。

追記:コードページを指定できるIsDBCSLeadByteExがありましたね

| | コメント (0) | トラックバック (0)

Uniscribeの概要(2)

前回の続きである。

今回は実行単位(ラン)について書いてみる。Uniscribeは様々なスクリプトを処理するために、複数のシェーピングエンジンを用意しているが、ScriptItemizeはテキストをシェーピングエンジンの変わり目または読む方向の変わり目によってテキストを実行単位に分割する。Microsoftのタイポグラフィのサイトによると、少なくとも以下の10個のスクリプトエンジンが存在する???ようである。

  1. Standard Scripts
  2. Arabic Script
  3. Korean Hangul Script
  4. Hebrew Script
  5. Indic Scripts
  6. Khmer Script
  7. Lao Script
  8. Syriac Script
  9. Thaana Script
  10. Thai Script

スクリプトによって最低限必要な言語処理は変わると思うが、前回例に出した、アラビアスクリプトでどのような言語処理が走るか覗いてみる。

上記のサイトによると、以下のような処理が行われるそうである。

Uniscribeoverview2_1_2

ちなみに、前回、アラビア語では、文字の位置によって字形が変わる文字があると書いたが、その形式を

  • 単独形(isolated form)
  • 語頭形(initial form)
  • 語中形(medial form)
  • 語末形(final form)

と呼ぶ。

これらのアラビアスクリプトに必要な言語処理のグリフ置換を行っているのが、最初に示した図の1-bから1-eである。もちろん、日本語にこれらの処理は必要ない。ところで、日本語の処理にはどのスクリプトエンジンが使われるのだろうか??lol

また、標準Win32APIでグリフインデックスを直接処理する時と同様、フォントリンクなどは働かないので、ScriptShapeで欠損グリフが発生した時に、代替フォントを使って表示する必要があれば、自前で実装する必要がある。が、どうもExtTextOutやUniscribeのScriptString系のAPI内部で具体的にどのように代替フォントを探してるか知るよしもないので、それらのAPIと全く同じ結果を得ようとするのが大変らしい・・・

| | コメント (0) | トラックバック (0)

Uniscribeの概要

まず、UniscribeのAPI一覧をMSDNなどで見ると分かると思うが、UniscribeのAPIは主に二つに大別できる。

  • ScriptString系のAPI
  • それ以外の低レベルAPI

まず、ScriptString系のAPIは高レベルのAPIで、プレーンテキストつまり同一スタイル(フォントやサイズや色などが同じ)のテキストを処理する時に使用するAPIである。様々な国や地域で使われるスクリプトを含むテキストはアプリケーション側が任意の位置で勝手に分割して処理すると適切な結果を得られない可能性があり、基本的には、行単位などで処理する必要があるが(ExtTextOutを使う時も同様)、エディタなどで見られるキーワードの色分け表示などをする必要がなく同一スタイルで表示する時に使用するAPIである。次に、ScriptString系以外の低レベルAPIは、色分け表示など複数のスタイルでテキストを処理するときに使用するAPIである。また、以前にGetCharacterPlacementを用いたカーソル位置の決め方のブログを書いたが、UniscribeにはそのためのScriptCPtoXやScriptXtoCP(ScriptString系はScriptStringCPtoX、ScriptStringXtoCP)といった便利なAPIが既に用意されている。

ScriptString系の使い方は引数が多いことを除けば、以外に簡単に使用できるので、ここでは主に低レベルAPIを扱ってみる。

主な描画の流れは以下のようになる。

  1. ScriptItemizeで、テキストを実行単位(ラン)に分割。
  2. 複数のスタイルでテキストを処理する必要があれば、1で得られた実行単位を更に細かい実行単位に分割。
  3. ScriptShapeでシェーピングを行いグリフと表示属性を生成。
  4. ScriptPlaceでポジショニングを行いグリフの有効幅と2次元オフセット情報を生成。
  5. ScriptTextOutで描画。

まず、シェーピングとは、主にテキスト文字列からテキスト文字列を表すグリフインデックスの列を生成する処理を指す。以前紹介したWin32APIのGetGlyphIndicesも与えらたテキスト文字列からグリフインデックスの列を生成するが、GetGlyphIndicesは言語毎に必要な言語処理のグリフ置換(Glyph Substitution)が行われない。通常、フォントファイルには、文字コードからフォントファイル内のグリフを識別するグリフインデックスへの対応を表すマッピングテーブルを持つが、GetGlyphIndicesは単純にこのマッピングテーブルを用いた変換を行う。ちなみに、OpenTypeフォントでは、このテーブルをcmapテーブルと呼ぶ。そして、言語によっては、適切な表示を得るためにこの変換の後に更に変換が必要でこの処理を言語処理という。例えば、アラビア語を例にとると、アラビア語の文字は、文字の位置によって、字形が変化するものがある。これに対応するためには、更にグリフを変換する必要があり、これがグリフ置換である。ちなみに、これを行うときに使用されるテーブルをOpenTypeフォントではGSUBテーブルと呼ぶ。

次にポジショニングであるが、これはその名の通り、生成されたグリフをどう配置するかを決める位置決めの処理である。

とりあえず、ここまで。

| | コメント (0) | トラックバック (0)

そしてUniscribe

今まで色々やってきたが、Unicodeを使って様々な国や地域のスクリプトを適切に処理するには、どうもUniscribeを避けて通ることはできないような気がしてきた(表示するだけならExtTextOutで十分だと思うが)。で、前々から色々Uniscribeの事を調べているが、いかんせん、ネットを検索しても情報がほとんどない。英語の資料も見てるが、英語をスラスラ読み書きできるわけではないので、遅々として進まない。

とりあえず、国際化の情報として、Microsoftのサイト

の情報を一通り読んでみるといいかも。全部英語だけど。概要を把握するにはお勧めです。

それにしても先は険しそう。

| | コメント (0) | トラックバック (0)

GetCharacterPlacementの続き

また、GetCharacterPlacementのお話である。前回、双方向テキストをETO_GLYPH_INDEXなしのExtTextOutで出力させてみたが、今回は GetCharacterPlacementを使って同じ結果を出力させてみる。 流れとしては、GetCharacterPlacementでGCP_RESULTS構造体のGlyphsフィールド、Dxフィールドにそれぞれグリフインデックスと隣接する文字セルの原点間の距離が返されるので、それらを使ってExtTextOut(ETO_GLYPH_INDEXつき)で描画してみる。ちなみに、扱うテキストは前回と同じ。フォントはTahoma。

まずは、XP上での実行結果から。

Gcp1

前回の結果と見比べるとわかると思うが、XP上でのETO_GLYPH_INDEXなしのExtTextOutの実行結果と同じである。

次は、2000上での実行結果。

Gcp2

これは、XP上でのETO_GLYPH_INDEXなしのExtTextOutの実行結果と同じである。

やはり2000上で双方向テキストというかコンプレックススクリプトを正しく扱うには、Uniscribeを使うしかないようである。ちなみに、上の二つの図では、GetCharacterPlacementを呼び出すときに、GCP_REORDERと GCP_GLYPHSHAPEフラグを指定した。

ちなみに、この時のCaretPosフィールドで返されるカーソル位置は下の図のとおり。

Gcp3 Gcp4

左がXP上、右が2000上での実行結果。XP上でアラビア語の部分が右から左に表示されたように、カーソル位置のX座標がアラビア語の部分で減少していくのがわかる(82->78->78->61->56->49->49)。ここまでは、問題なさそうである。

次に、テキストを「ABCあいうえおABC」に変えてみる。フォントは同じくTahoma。

まずは、XP上の実行結果から。

Gcp5

次に2000上の実行結果。

Gcp6_1

まず、前にも書いたが、Tahomaは欧文フォントで日本語のグリフが存在しない。また、グリフインデックスを直接処理しているので、フォントリンクなどは働かないので、日本語の部分は文字化けするであろうと想定されたが、XP上ではグリフが存在しない時の代替グリフすら表示されていない。 2000上では矩形のグリフが表示されたが、3つ目と4つ目の矩形だけがくっついて微妙である。

うーん。

| | コメント (0) | トラックバック (0)

Unicodeと双方向テキスト

今回は双方向テキストである。初めに、自分はこの方面に詳しくないので、用語の使い方や説明が適切でないかもしれないのでご勘弁を。また、詳しいことは別のサイトにお任せするとして、ここでは、Windows絡みのお話を。

Unicodeは世界の様々な国・地域で使われる言語のスクリプトを収録した文字コードの規格であるが、Unicodeを使ってプログラミングを始めると、日本人には馴染みの薄い問題に出くわす。その中の一つにテキストの双方向性がある。日本語を読み書きする方向はは通常左から右(縦書きというのもあるが)であるが、アラビア語やヘブライ語は通常、数字は左から右、その他の部分は右から左に読み書きする。で、Unicodeを使ってこれら複数のスクリプトが混在するテキストを自前で正しく表示・処理するとなるとややこしい。

WindowsではXP以降標準のAPIでこららコンプレックススクリプト(Complex Script)を正しく処理できるようになったそうなので、それを確かめてみる。(ちなみに、XP以前でもUniscribe(Unicode Script Processor)というAPIを使用すれば、コンプレックススクリプトを正しく処理できるそうであるが、詳しい事はここでは割愛する)

まずは、扱うテキストは次の図のテキストである。

Bidi1

これは、17のコードポイントで表されるテキストを1コードポイントずつ左から右に並べて表示したときの図である。両端の英語の「Hello」「World」のフレーズの間にアラビア語のフレーズがはさまれているテキストである。

次に、このテキストを正しく方向を考慮して表示したときの図が下の図である。

Bidi2

これは、Uniscribeを使いUnicode双方向アルゴリズムにしたがって、テキストを表示したときの図である。分かりずらいかもしれないが、両端の英語の部分は左から右へ、中央のアラビア語の部分は右から左に表示されている。つまり、中央の部分はテキストのメモリ上での格納順序である論理順序(Logical Order)とテキストの表示順序(Visual Order)が一致しないのである。

で、これらを元にWindowsの標準テキストAPIであるExtTextOutを使って(ETO_GLYPH_INDEXなし)で表示してみる。

まずは、Windows2000上での結果から。

Bidi3

先ほどの結果と照らしあわせれば分かると思うが、方向が考慮されず、中央のアラビア語の部分も両端の英語の部分と同様、左から右に表示されている。

次は、WindowsXP上での結果。

Bidi4

上の図のように、Uniscribeの結果と同じように、中央のアラビア語の部分が右から左に表示されている。

ちなみに、Unicodeでは、文字ごとに文字の方向属性という属性が定められていて、それらはWindowsAPIのGetStringTypeExを使って取得することができる。

| | コメント (0) | トラックバック (0)

GetCharacterPlacementとカーソル配置

うーん。ブログの更新が随分とあいてしまった。そういえば、GYAOで無料配信をしているスターゲートシリーズがおもしろい。続きをみたいところだけど、GYAOで続きが配信されるか疑わしい。なんか、最初のエピソードだけ流して興味あったら、レンタルするなりしなさいってスタンスのような気がする。

気を取り直して今回は、カーソル(キャレット)の配置。Windowsの標準コントロールであるEditコントロールみたいな機能を自前で実装するとなると、テキストの表示にはExtTextOutなどを使うとして、カーソルの位置をどうやって決めるかちょっと考えさせられる。以前、取り上げた、GetTextExtentExPoint、GetTextExtentPoint32やGetCharWidth32でできないことはないかもしれない。ちなみに、MSDNのドキュメントにも書いてあるように、文字列の幅を求めるのに、GetCharWidth32で個々の文字の幅を求めて、その和をとるのは、正確ではない。なぜなら、デバイスによってはカーニング等が行われるからだ。

で、こんな時に便利そうなのがGetCharacterPlacementである。GetCharacterPlacementのGCP_RESULTS構造体のCaretPosフィールドにカーソル位置の情報が返される。この情報の示す位置にカーソルを配置すればよいのだ。

で、さっそく、CaretPosフィールドに返された位置に線を引いてみたのが下の図である。

Caretpos1_3

フォントはMS ゴシックである。上の図のように、CaretPosフィールドに返される位置にカーソルを配置すれば、問題なさそうである。

ちなみに、上の図は、テキストをExtTextOutのETO_GLYPH_INDEXなしで表示したものである。GetCharacterPlacementのドキュメントを見れば分かるように、このAPIで、グリフインデックスも取得できるので、グリフインデックスを取得し、ExtTextOutでETO_GLYPH_INDEX付きで表示しても問題なさそうである。下がETO_GLYPH_INDEX付きで表示したときの図である。

Caretpos2_1

問題なさそうである。しかし、ここでフォントをTahomaに変えてみよう。結果は下のようになる。

Caretpos3_1

カーソル位置がおもいっきりずれているのが分かるであろう。まず、文字化けを起こしているが、これは、Tahomaは欧文フォントなので、日本語で使用するグリフが含まれていない。また、ETO_GLYPH_INDEX付きでグリフインデックスを直接処理しているので、フォントリンクが働かないので文字化けが起きているのである。(つまり、ETO_GLYPH_INDEXなしの時にExtTextOutで自動で行われるフォントリンクのような機能は自前で実装する必要がある)。この文字化けは、ある程度想定?できた結果なのでスルーして次の問題に進んでみる。

次の問題とはカーソル位置がずれていることである。ううーん。このカーソル位置はいったい何の位置なのだろうか・・これは、ETO_GLYPH_INDEXなしでExtTextOutを呼び出した結果を重ねてみれば一目瞭然である。

Caretpos4_1

上の図をみれば、わかるようにCaretPosで返されるカーソル位置は常にフォントリンクの影響を受けるということらしい。まぁ、これはそういう仕様と割り切るしかない。

追記:どうやら、上の現象はWindowsXP上では、異なる結果になる模様です。汗汗。ちなみに、上の現象はWindows2000上。

| | コメント (49) | トラックバック (0)

スクロールバーあれこれの続き

前回、ScrollBar関係のAPIを使って制御する時、毎回、頭を痛めると書いたが、表現が適切じゃなかった。ただ自分が使い方を誤解していたのである。で、初めて使う人にやりがちな決め方(自分だけかも知れない)を書いてみる。というか、非常にかったるい話です。

テキストをM行表示する場合を考えてみよう。この時、ウィンドウのクライアント領域の高さの制限からN(N<=M)行しか表示できないとする。まず、範囲(nMinとnMax)を決めると思うので範囲を決めてみる。(M-N)行表示されていない部分があるので、(M-N)+1通りの状態を表せればよいので、例えば、nMin=0、nMax=M-Nとする。で、これで位置についてはよさそうなので、次はページを決めてみる。この場合、全体M行のうちN行つまりページサイズが全体のN/Mになればよいので、nPage=全体*N/M=(M-N + 1)*N/Mでいいかなと・・

つまり、この場合、

  • nMin=0
  • nMax=M-N
  • nPage=(M-N + 1)*N/M

になる。で、具体的に、10行(M=10)表示するとしてクライアント領域に5行(N=5)しか表示できないとして、代入してみると、

  • nMin = 0
  • nMax = 5
  • nPage =3

となり、ちょうど、ページサイズが3と全体6(=5-0+1)の半分になっていて、これでいいのかななんて考える。で、今度はM=10、N=6としてみると、うぉ。nPageの型が整数なんだけど、整数にならない。切り捨て・切り上げても問題ないのかなぁ・・・と。こうやって、ドツボにはまったのである。

そもそも使い方が間違っていて、範囲の決め方が問題なのである。様々なサイトを見ると、nMinとnMaxにスクロール範囲を指定すればよいなどと書かれているが、語弊があるかもしれないが、表示したい内容全体の量を*範囲*で指定するのである。この場合、M行表示したいので、nMin=0とすれば、nMax=M-1(nMin=1とすれば、nMax=10、nMin=5とすれば、nMax=14・・・どの場合も、nMax - nMin + 1=M=一定)でいいのである。でページサイズには、実際に表示できる量つまりこの場合N行なので、nPage=Nになる。つまり、

  • nMin=0
  • nMax=M - 1
  • nPage=N

でいいのである。で、先ほどど同じように、M=10、N=5としてみると

  • nMin=0
  • nMax=9
  • nPage=5

になる。でポイントはここからだ。この場合実際表示されていない行は5行なのに、nMin=0、nMax=9だから、サムの位置が0から9と変化できて問題あるんじゃ?と思うだろう。だが、実際、SetScrollInfoやSetScrollPosでサムの位置だけを変えてあげると、0から5までしか移動できないのである。つまり、サムが取れる位置はページサイズの影響をうけるのだ。サムの位置の最大値=nMaxではなく

サムの位置の最大値=(nMax - nMin + 1) - nPageになる。

これを理解して初めて、SCROLLINFO構造体のnMin、nMax、nPosそしてnPageに何を設定すればいいか理解できた。ちなみに、今回の例では行単位で考えたが、ピクセル単位で考えても同じことが言える。疲れた。

| | コメント (0) | トラックバック (0)

スクロールバーあれこれ

今回はスクロールバーのお話。Windowsには主に2種類のスクロールバーがある。

  • 標準スクロールバー
  • スクロールバーコントール

である。標準スクロールバーは別名ビルインスクロールバーと言われ、ウィンドウハンドルを持つウィンドウの一部として表示されるスクロールバーである。スクロールバーコントロールは、EDITクラスのエディットボックスやBUTTONクラスのプッシュボタンなどのシステムによって提供される事前定義済みのクラスのウィンドウである(つまり、それ自体でウィンドウハンドルを持つ)。ちなみに、標準スクロールバーはAPIのCreateWindowでウィンドウを作成するときに、ウィンドウスタイルとしてWS_HSCROLL、WS_VSCROLLを指定するか、ウィンドウ表示後、ShowScrollBarを使って表示できる。スクロールバーコントロールはCreateWindowでウィンドウを作成するときに指定する、ウィンドウクラス名をSCROLLBARにすればよい。

Scrollbarcontrol

で、このスクロールバーをAPIを使って制御する時に、毎回、自分が頭を痛めることは、スクロールの範囲、サム(つまみ)の位置、ページのサイズの取り方である。慣れれば、難しくはないと思うが・・具体的に言うとGetScrollInfoやSetScrollInfoで指定するSCROLLINFO構造体の中身のことである。ちなみに、この構造体のnMinからnMaxまでがスクロールの範囲、nPosがサムの位置、nPageがページのサイズを表す。

ページとは、、現在表示されているコンテンツがコンテンツ全体のうち、どれくらいの割合になるかを示す量である。例えば、上の図では、水平スクロールバーの横幅に対するサムの横幅の割合がだいたい目算で1/10くらいなので、ユーザーは現在、実際に表示されているコンテンツは全体の1/10くらいなんだなと推測できる。

で、続く・・・・

| | コメント (0) | トラックバック (0)

Iである

Iである。今回はこのIに焦点を当ててみる。アイとは何ぞや?ってことだが、MSDNのフォント・テキスト関連のAPIの一覧をみると、末尾にIがつくものがいくつかみつかる。GetCharWidthIやGetCharABCWidthIやGetTextExtentPointIなどである。説明を見ればわかるとおり、グリフインデックス(GlphyIndex)のIである(と思う。)

文字列を描画するときに、ExtTextOutなどのAPIに文字コード(UnicodeやShift-JISなど)で表現した文字列を渡して描画するのが一般的だが、直接、フォント内の各グリフのインデックスを使って描画することもできる。まずは、グリフインデックスの取得方法であるが、GetGlyphIndicesを使って、指定した文字列のグリフインデックスの配列を取得できる。このとき、第5引数のフラグflにGGI_MARK_NONEXISTING_GLYPHSを指定すると、フォントにグリフが存在しない場合、0xFFFFが指定しない場合0x0000が返ってくる。この配列を使い、文字コードではなく、グリフインデックスを使って描画したイメージが下の図の下の文字列である。

Indices

ちなみに、グリフインデックスを使用する場合、ExtTextOutの第3引数にETO_GLYPH_INDEXを指定する。また、Tahomaという欧文フォントを使用している。上の文字列はいつも通りの文字コードを使って同じ文字列をExtTextOutで描画したものである。もう何が起きているのかわかると思うが、GetGlyphIndicesを使用して取得するとフォントリンク機能が働かないのである。よって、「あいうえお」の文字列に対応するグリフインデックスが「0xFFFFまたは0x0000」になっている。それで、文字化けが起こったのである。それだけである・・・・

ちなみに、GetGlyphIndicesのグリフインデックスの配列を受け取る第4引数の型がLPWORDである。つまり、各インデックスは2バイトでAPIの仕様の制限かフォントファイルの制限かわからないが、フォントに収録できる文字数は2バイトまでなのかなぁ・・なんて推測してみたり。と、これで思い出したのが、サロゲートである。サロゲート文字は1文字を2ワード(4バイト)で表現する文字であるが、GetCharABCWidthなどで指定する文字コードの型がUINTになっていて、これらのAPIがサロゲートを正しく処理するかは、おいといて、インターフェースを変えずにサロゲートも渡せるよう既になっているなと安堵してみたり。

| | コメント (0) | トラックバック (0)

GetTextExtentExPointの締めくくり

えらい、まとまりのない文章になってしまった。前回でいきなり問題にぶつかったとかいてるが、要は自前でGetTextExtentExPointを使いクリッピングを行った方が速いか遅いかだ。ということで、パフォーマンスをとってみた。文字数10240の文字列(Unicodeだから20KBytes)をウィンドウのクライアント領域の左隅から計1万回描画してみた。

  • GDIのクリッピングに任せた場合:約4100ms
  • 自前でGetTextExtentExPointでクリッピングした場合:約5600ms

ははは。って感じである。逆に遅くなった・・・今回、GetTextExtentExPointを呼び出す時に渡すnMaxExtentにクライアント領域の幅ではなくクライアント領域の幅+マージンを渡している。なぜなら、今回はワードラップを行うのではなく、文字列を切り捨てるためにGetTextExtentExPointを呼び出しているので、クライアント領域の幅を渡すと、場合によっては、クライアント領域の一番右側に表示される文字がかって切り捨てられて、GDIのクリッピングに任せた場合と結果が異なってしまうからだ。マージンをどれくらい渡すかだが、GetTextMetricで返される構造体のtmAveCharWidth(平均文字幅)の3倍以上とっとけば、結果がずれることはほぼないだろう。ExtTextOutの呼び出しは前々回と同じように、GetTextExtentExPointで返されたlpnFitを文字数として、ExtTextOutの第7引数cbCountに渡している。

ところで、やはり、自前クリッピングの方が遅くなってるのは、前回書いたように、GetTextExtentExPointの呼び出しで毎回文字列全体の寸法(lpSize)が求められているためだろう・・・ちなみに、触れずにきたが、この話では、固定ピッチフォントと可変ピッチの両方を想定しているので、単純に幅の計算や切り捨てる部分の計算できない。

というか、巨大な長さの文字列を描画するはめにはなるかもしれんが、そんな文字列を現実問題1万回描画するシチュエーションが見つからない。描画対象が画面の場合、少なくとも縦方向の解像度数以上描画しても意味ないし、縦1200ピクセルだと多くても120回くらいになる。ということで、素直にGDIのクリッピングに任せた方が吉。ってことで終了。ははは

| | コメント (0) | トラックバック (0)

GetTextExtentExPointの続き

で、続きである。みんな同じ事を考えると思うが、ワードラップ以外の目的でもこのGetTextExtentExPointが使えないかな?と思ったのである。例えば、非常に長い文字列をワードラップをせずに描画する場合を考えてみよう(文字数64K超などの)。WindowsのGDIはクリッピング領域外に対する描画は行わないので、何も考えずに、長い文字列をExtTextOutに渡しても、見た目上は問題ない。問題はパフォーマンスなのである。パフォーマンスをとって見るとわかるが、かなり速度に違いがでる。ExtTextOutへの呼び出しは文字列のポインタを渡すので、文字列が巨大であろうとなかろうと、文字列の受け渡しに関するパフォーマンスの違いはほとんど無いことがわかる。

要は、クリッピングに時間がかかってるのである。Windows自体内部でどのようにクリッピングを行っているのかわからないが、文字数64K超の文字列なんて、あきらかにそのほんの一部しか描画されないので、ExtTextOutの呼び出しの前にこのGetTextExtentExPointを使用し、文字列の表示されない部分を切り捨てて、自前でクリッピングを行なおうと思ったのである。

で、いきなり問題につきあたったのである。その問題とは、GetTextExtentExPointの最後の引数であるlpSizeである。これは、GetTextExtentPoint32で取得する文字列の寸法と全く同じものが返されるのであるが、日本語のMSDNによるとNULLを指定できると書いてある。が、実際にはNULLを指定できず、英語のMSDNには、NULLを指定できないと書いてあったのである。つまり、NULLを指定できないって事は、GetTextExtentExPointの呼び出し時に、毎回律儀に文字列全体の寸法を計算しているのである。このAPIをlpnFitの値だけを利用して表示されない部分の切り捨てのために使おうと思っていたので、最後の引数lpSizeにNULLを指定できて、NULLが指定されたとき、lpnFitが求まったら、そこでAPIに処理を打ち切ってもらいたかったのである。

と、今日はいきなり問題につきあたったので、次回に続く。

| | コメント (0) | トラックバック (0)

GetTextExtentExPoint

GetTextExtentExPointである。以前、GetTextExtentPoint32を使ったが、そのEx版である。GetTextExtentPoint32はパラメータとして渡した文字列の幅を返してくれるが、GetTextExtentExPointはMSDNのヘルプにあるように、ワードラップを行うのに便利なのである。具体的に言うと、ある幅をもった領域に長い文字列を描画する時に、その文字列の何文字目までが、文字が途中で切れずに描画できるか求めるのに便利なのである。

例えば、幅100のウィンドウにMSPゴシックのサイズ10のフォントでなにも考えずにExtTextOutで描画すると下の図のようになる。

Expoint1

この場合、「あいうえおかきくけこ」を描画したのだが、「け」の文字が切れてしまっている。そこで、GetTextExtentExPointの出番である。この場合、横幅100の領域に収めたいから、第4引数nMaxExtentに100を指定して呼び第5引数lpnFitの文字数分だけ表示したのが下の図の下の文字列である。

Expoint2

ただ、単純にGetTextExtentExPointで返されたlpnFitの値をExtTextOutの文字数を表す第7引数cbCountに渡しただけである。この場合lpnFitに8が返されていた。もちろん、文字列の幅だけを返すGetTextExtentPointを使えば、どの文字でちょん切れるか計算できないこともない。ただ、GetTextExtentPointを使って求める場合、工夫をしないと何度もGetTextExtentPointを呼び出すはめになるだけだ。

で、次回に続く・・

| | コメント (0) | トラックバック (0)

Tab文字の展開

文字列にTab文字(U+0009)が含まれている時、何も考えずに、Windows32APIのTextOutやExtTextOutで文字列を描画すると、通常は、Tab文字は使用するフォントのDefaultCharのグリフに置き換えられて描画されてしまう。下のイメージのように。

Exttextouttahoma

DefaultCharというのは、使用するフォントに対応するグリフが無いときに使用される文字である。ちなみに、DefaultCharは、GetTextMetricsで取得できる構造体のtmDefaultCharメンバで取得できる。上の図はフォントにTahoma(DefaultCharはU+001F)、下の図は、MSゴシックを使用したきの図である(DefaultCharはU+FF65)。

Exttextoutgothic

ところで、よくテキストエディタではTab文字を使用して位置揃えを行えるようになっている。そこで、便利なAPIがWindowsに用意されている。TabbedTextOutとGetTabbedTextExtentである。下にこれらのAPIを使って文字列を描画したときのイメージを示す。

Tabbedtextout

どれも、「ABC + Tab文字 + DEFG + Tab文字 + あいうえ」という文字列をTabbedTextOutに渡すパラメータを変えながら、描画したときのイメージである。上から順に、第2引数の描画開始位置のX座標を、それぞれ30、20、0にして渡している。よく、テキストエディタでTab文字を含むテキストを表示したときのイメージと同じように、表示されているのがわかる。ここで、ポイントは、第8引数のnTabOriginである、ここにタブの展開を開始するX座標を指定するのであるが、どの場合も0を指定している。よって、たとえ、第2引数の文字列の描画開始位置のX座標を30、20、0と変えてもタブ展開の開始位置を揃えているので、上のイメージのように、Tab文字の次の文字の位置が揃うのである。このように便利な機能を提供してくれるが、一つ困ったことがある。それは文字列の幅の取得である。

GetTabbedTextExtentで、一見するとTab文字を展開した後の文字列の幅が取得できるのであるが、この引数を見ると、足りないものが一つある。それは、nTabOriginがないのである。すなわち、このAPIはnTabOriginを0とした時の幅しか返さないのである。

上のイメージを見てもわかるように、TabbedTextOutのnTabOriginに渡すパラメータと描画開始位置によって文字列の幅(イメージの先頭文字の「A」の左側から最後の文字「え」の右側まで)が変わるのだ。ちょっと、ありゃありゃと思っていたら、取得する方法があった。それは、なんと

TabbedTextOutの戻り値の下位ワードに幅が返されるのだ。

これを知って喜んだのだが、すぐに、気づいたのが、TabbedTextOutを呼び出すってことは、実際に文字列をデバイスコンテキストに描画しなければならないってことだ。はぁ。

色々書いたが、まぁ、TabbedTextOutを呼び出して幅を求めるのは、Tab文字を展開しないときに使用するGetTextExtentPoint32と比較してそこまでパフォーマンス悪いとは言えないのかもしれない。GetTextExtentPoint32で幅を求めるときも、どの道、GDIのレンダリングエンジンに文字列を渡し、描画イメージを求めなければ、求められないのだから(たぶん)・・・TabbedTextOutで幅だけを求める場合、クリッピング領域外に描画させて求めればとりあえずは問題ないのかもしれない。

補足して、実際はTabbedTextOutWやGetTabbedTextExtentWなどのUnicode版を使用したのが、ANSI版とUnicode版の表記を使い分けるのは面倒なので、ことわりが無い限りUnicode版を指す。それと、Windowsで使用するフォントにグリフが無いときに、そのフォントのDefaultCharのグリフが使われると書いたが、どうも、見てると、複数のグリフが使われてるっぽい。たぶん、これは、フォントリンクの影響だと思う。Windowsでは、フォントのリンクが設定されていれば、たとえ、使用するフォントにグリフがなくても、リンクされたフォントのグリフを探しにいく。で、ここでもグリフがみつからないと、このフォントのDefaultCharのグリフが使われてるのでは?と。

| | コメント (0) | トラックバック (0)

CharPrevWとCharNextW

WindowsAPIのCharPrevWとCharNextW。どっかで、CharPrevWとCharNextWは合成文字をきちんと処理してくれると記事で読んだ。

で、前にGetFontUnicodeRangesの記事でGetExtentPoint32Wに前に基底文字が存在しない単独の合成文字だけを渡すと0の幅を返すと書いたが、それと同様、前に基底文字が存在しない状態でCharPrevWとCharNextWを呼んでみた。Windows2000ではCharNextWはポインタを次の文字に進めてくれたが、CharPrevWはポインタを前に進めてくれない。ということは、そのような不完全な文字列でCharPrevWを使いループしてると終了条件によっては、無限ループになる。不完全な文字列というのは、外部からデータをファイルなどとして入力すると大いにありえる。ははは。WindowsXPでは、とりあえず、ポインタを進めてくれる。というか、ころころ仕様を変えないでほしい。

サロゲートペアについては、最新のVistaでも、2000とXP同様、サロゲートは2ワードで1文字を表すが、1ワード分しかポインタを進めてくれないらしい。合掌。

| | コメント (0) | トラックバック (0)

Oracleの接続方法

しばらく、OracleというかDBXInspector周りの話をしようかなと思う。PDFの話はまたいずれかの機会に。と、いっても書く内容は、Web上で検索すれば、ごろごろ出てくるような内容で、目新しいことはない。なぜなら、自分が詳しくないからだ。まずは、Oracleデータベースの接続方法から。プログラムからOracleデータベースに接続する方法として、大体以下のようなものがあげられる。(Windowsからの接続に限定する。)

  1. OCI(Oracle Call Interface)の直接呼出し
  2. ODBC(Open DataBase Connectivity)を用いた接続
  3. OLE DBを用いた接続
  4. ADO(ActiveX Data Objects)を用いた接続
  5. OO4O(Oracle Objects For OLE)を用いた接続
  6. JDBC(Java DataBase Connectivity)を用いた接続
  7. ADO.NETを用いた接続

まず、1のOCIの直接呼出しとは、Oracleデータベースへの接続・SQL文の実行などの機能を提供するライブライリがDLL(oci.dll等)として用意されているので、それを用いて接続を行う方法である。非常に低レベル(機能が貧弱という意味ではない)なAPIなので、習得するのが大変であるが、低レベルである分、非常に細かな制御を行うことができる。

2のODBCインターフェースとは、接続するデータベースによらない共通のインターフェ-スを開発者に提供する目的で開発されたもので、このインターフェースとOracle用のODBCドライバを用いて接続を行う方法である。

3のOLEDBインターフェースは、ODBCと同様の目的で、Microsoft社がOLE DB技術を用いて開発したインターフェースである。Microsoft社はこれをシステムレベルのインターフェースと位置づけているように、実際の業務システムの開発で使うには煩雑である。ODBCと同様、各データベース向けのOLE DBプラバイダと共に用いて接続を行う方法である。従来のODBCドライバを用いて接続できるように、ODBCへの橋渡し(ブリッジ)を行う、例えば、Microsoft OLE DB providers for ODBC driversなどのブリッジドライバもある。

4のADOとは、3のOLE DBの上にかぶさるレイヤー(層)であり、OLE DBに比べて非常にあつかいやすくなっている。.NETではないVisual Basicを用いた開発でよく見かけられる。

5のOO4OはOracleが開発したOracle向けに特化したライブラリでCOM/OLEの技術を用いたライブラリで、ADO同様、Visual Basicを用いた開発でよく見かけられる。OO4Oは内部では、最終的にはOCIを呼んでいる。

6にJDBCであるが、これはJava言語を用いて接続する場合に用いられる。OracleからはJDBC Type2とType 4のドライバが用意されている。

7のADO.NETは.NET Framework向けのデータアクセス技術・ライブラリであり、これも他と同様、各データベース向けのADO.NET プロバイダと共に用いて接続する。ADO同様、ブリッジドライバも存在する。.NETアプリの場合、これが標準方法である。

と、色々書いたが、DBXInspectorは1の方法を用いているので、他のドライバ・プロバイダと称されるものを用いた接続よりオーバーヘッドは少なくなるので、高速かもしれない。しかも、.NET Frameworkを必要としないWin32ネィティブアプリでもある。

って。結論が3行って・・・・・・

DBXInspectorのスクリーンショット

Screenshot_2

| | コメント (0) | トラックバック (0)

PDFとDBXInspector

あけおめである。今年は何かいいことがたくさんありそうだ(嘘)。

それでは、PDFである。あらためて言うまでもないほど、メジャーである。自分はパソコンにOSを再インストールするときは、真っ先にAdobeのサイトから最新版のAdobe Readerをダウンロードしてインストールする。で、そのPDF文書とDBXInspectorに何の関係があるのかと言うと、SQL文を作成してる時、SQL文の細かな構文を忘れるというのはありがちなパターンだ。で、大多数の人はOracle社から提供されているマニュアルを見ると思うが、そのマニュアルがPortable Document File形式の文書なのである(今では、HTML形式のマニュアルも用意されているが・・・)。

で、数ヶ月前に思いついたアイデアが、DBXInspectorで、簡単に構文を調べたい関数やSQL文をOracleのPDFマニュアルを使って検索できると便利かな?である。

で、その時、早速実装をしてみたのだが、表示はできたのだが、目的の項目を表示させる方法がわからない。ページ指定はできるのだが、もっと細かな制御の仕方がわからない。で実を言わなくても、Googleで色々調べたが、見つけられなく、実装をあきらめたもしくは保留にしたのである。

で、最近ちょっと、偶然かわからないが、発見したのある。Adobeソフトウェア開発キット(SDK)。これである。英語の資料であるが、ちょっと読んでみたが、Interapplication Communications(IAC)の欄に知りたい事が書いてあった。Named Destination(名前付き宛て先)というのがポイントである。つまり、ページ以外にも、指定の名前付き宛て先に移動できる機能がプログラムからも利用可能なのだ。名前付き宛て先もっとわかりやすく言えば、しおりがOracleのマニュアルで各関数やSQL文毎に設定され、その名称がわかれば、自分の実現したい事がかなう。で、PDFの文書を解析しなければいけないのだが、これはツールを探してきて調べよう。PDFのフォーマットの中身までに手を出すとやけどしそうだ・・・

というか、前置き?が長すぎーーー。

あ、そう、前回のロケールで便利なツールを見つけたのだ。LocaleExplorerである。まだ、β版だが。使いこなせれば開発者にとって便利そう。

| | コメント (0) | トラックバック (0)

システムロケール、ユーザーロケールetc

話の内容に一貫性がないが、そういえば、ちょっとWindowsロケール周りについて色々読んでみた。ロケールって言ってもWindowsには、システムロケール・ユーザーロケールやらスレッドロケールやら色々あり、ややこしく頭を痛めていたが、システムロケールについて一言。「システム」という名前が付くぐらいだから、何かしら重要な設定なんだと思ったが、どうも違うらしい。

Windowsは非Unicodeウィンドウに対して送信される文字関連のメッセージに含まれる文字の文字コードを自動的に変換するが、システムロケールはただ単純にこの変換で使用するコードページ(変換テーブル)を決めるだけのものらしい。つまり、RegisterClassExWなどで登録されたウィンドウを使用するUnicodeに対応したアプリには全く影響を与えない。

それなのに、「システム」ロケールである。ちょっと、紛らわしいと思ったら、最新のMSDNのドキュメントでは呼び名がLanguage for non-Unicode programsになってました。気づきませんでした。ユーザーロケール。これは、ユーザー毎に設定できて、通貨の記号や、日付・時刻の表示に使用する書式などを決めるものであるが、これもStandards and formatsになっている。これで、こんがらなくてすむ。

システムロケールは一度に一つの値しか設定できないが、非Unicodeアプリに対して、アプリ毎に使用するコードページを指定できるWindowsXP用のAppLocale UtilityなんてソフトがMicrosoftから出ていたりもする。

まぁ、いずれにしよ、今後ますます、Unicodeアプリが出てくると思われるので、システムロケールの事は忘れ去れる予感がする。

| | コメント (0) | トラックバック (0)

GetFontUnicodeRanges

Win32APIのGetFontUnicodeRanges。これで、第一引数で指定されるデバイスコンテキスに設定されたフォントでどの文字のグリフを持っているか調べることができるんですが、フォントリンク機能は考慮しませんね。他のGDI関数TextOutやExtTextOutなどでは、指定したフォントがグリフを持っていなくても、リンク設定されたフォントのグリフを自動的に使用し表示します。このAPIの第2引数に渡す構造体のサイズは、GetFontUnicodeRanges(DC, null)で呼び出せば、必要なサイズが返されます。このサイズは構造体全体のサイズです。

後、GetCharABCWidthsWとGetTextExtentPoint32W。どれも文字というか、グリフの幅が返されるんだけど、実際文字列を描画するために、ExtTextOutを呼び出すと、カーニングや文字の合成をGDIのレンダリングエンジンが行ってくれるが、GetTextExtentPoint32Wはそれらの処理が行われた後の幅のイメージ。GetCharABCWidthsWは行われる前の幅をあらわすイメージ。つまり、一つのUnicodeコードポイントで表される文字についても上記のAPIが返す幅は異なる。例えば、U+0302あたりの合成文字のダイアクリティカルマーク。これらは基底文字と一緒に表示されることを考慮?してGetTextExtentPoint32Wは0の幅を返す(WindowsXP以降?)で、前に基底文字がない単独のダイアクリティカルマークをプログラムによって作り出したが、表示上は幅があって見える。が、選択できない。ははは。というか、最近Unicodeについて勉強してるが、難しい。

| | コメント (0) | トラックバック (0)