構造体について

上位ワードと下位ワード

Win32 APIを使う際にたびたび必要になるのが4バイトデータの上位2バイトと下位2バイトの概念です。これらは、WORD型が2バイトのデータ型であることから、上位ワード(high-order word)下位ワード(low-order word)とも呼ばれます。

4バイトの数値を16進数で表したとき、8桁で表すことができます。上位ワードとはこのうちの上4桁のことを指します。下位ワードは下4桁を指します。例えば、4バイト数値0x12345678の上位ワードは0x1234であり、下位ワードは0x5678となります。

さて、Win32 APIを使う際には、ある4バイトの数値から、その上位2バイトのデータや、あるいは下位2バイトのデータを取得しなければならないことがたびたびあります。逆に、2つの2バイト数値から4バイトの数値を作り出すことも必要になる場合があります。

ある数値型変数aから上位ワードと下位ワードをそれぞれ取得することを考えてみましょう。ここで注意しておく必要があるのは、取り出される整数値が符号付きであるか符号なしであるかによって取得の仕方が異なるということです。というのは、2バイトの情報を4バイトへと拡張するときに、符号の情報(最上位ビット)をどう扱うべきかが変わってしまうためです。

まず、符号なしの値を取得する場合について考えてみましょう。WORD型は符号なし16ビット値として定義されているものなので、こちらの場合の方が多いと思います。符号なしの値を取得するには、ビット演算子を用いて、

low  =  a & $FFFF               ; 符号なし下位ワード
high = (a >> 16) & $FFFF        ; 符号なし上位ワード

とします。ただし、ここで

high = a >> 16                  ; 符号なし上位ワード?

とか、

high = (a & $FFFF0000) >> 16    ; 符号なし上位ワード?

としてもよいのではと思われるかもしれませんが、このようにしてはいけません。このように書いてしまうと、変数aの値が負のとき(16進数で0x800000000xFFFFFFFFのとき)正常に取得されなくなってしまいます。

次に、符号付きの値を取得する場合には、

low  = a << 16 >> 16            ; 符号付き下位ワード
high = a >> 16                  ; 符号付き上位ワード

のようにビットシフトを使うのが簡単です。

逆に、2つの2バイトデータから4バイトデータを作成するには、符号付き・符号なしの場合ともに

a = (low & 0xFFFF) | (high << 16)   ; "|"を"+"としてもよい

とします。


別の方法を考えてみましょう。4バイトのメモリ領域を考えたときに、下位ワードは前の2バイトを、上位ワードは後の2バイトを指します。このことから、バッファから2バイトのデータを取得するwpeek命令を用いて、

wpeek low,  a, 0
wpeek high, a, 2

のようにできることも覚えておきましょう。ただし、この方法は符号なしの整数値を取得する場合に限ります。また、同様に2バイトデータから4バイトのデータを作成する場合にはwpoke命令を用いて

wpoke a, 0, low
wpoke a, 2, high

とすることができます。

構造体

構造体とメンバ

Win32 APIを扱う上でもう1つ避けて通れないのが『構造体(structure)』の存在です。ポインタの概念と並んで、APIを使用するための最重要ポイントの1つです。

構造体とは、いくつかのデータを組み合わせて、それを新しいデータ型として扱うものです。これにより、1つの構造体だけで、関連したいくつもの変数をまとめて扱うことができるようにしています。通常、それらの変数は密接な関係を持っていて、構造体はそれらを1つの名のもとに統一する働きを持ちます。

例として、次のような構造体を考えてみます。(ここではC/C++風の記述をしています。)

struct student {
    int  number;       // 番号
    char name[20];     // 名前
    int  age;          // 年齢
};

//」は、C++の文法でその行の以降がコメントであることを示します。(HSPのセミコロン「」と同じ)

上の記述は、新しい構造体studentの定義を表しています。この構造体は、データ型としては、「struct student型」になります。(「student型」でないことに注意。)

最初の単語「struct」は、これが構造体であることを示すものです。ここでは「student」という名前の構造体を定義しています。numbernameageを構造体studentメンバ(member)といいます。メンバとは、その構造体を構成する個々のデータのことです。また、メンバ名の直前の語は、そのメンバのデータ型を表します。メンバ名の後に角括弧 [ ] で囲まれた数値がある場合には、配列変数を表します。

上の定義を見ると、この構造体は3つのメンバを持っていることが分かります。

まず最初に、番号の情報(すなわち整数)を格納するnumberメンバがあります。このメンバはint型のメンバですから、整数が格納されます。つまり、データサイズは4バイトになるわけです。

その次に、名前(すなわち文字列)を格納するためのnameメンバがあります。このメンバはchar型のデータ20個分の配列であることが分かります。char型データのサイズは1バイトですから、このメンバのサイズは20バイトということになります。普通、char型の配列には文字列が格納されます。つまり、ここには、20バイトまでの文字列が格納できるということです(ヌル文字(ナル文字)(終端文字:コード00)がありますので、実際のデータとしては19バイトになりますが)。名前を格納するのに20バイトじゃ少なすぎるのではという話は、この際、置いておきましょう。

最後に、年齢(すなわち整数)を格納するためのageメンバがあります。これもint型のメンバですから、データサイズは4バイトになります。


では、この構造体が実際にメモリ上に配置された時にどうなるかを考えます。メモリ上では、構造体のメンバは、定義されているサイズで、定義された順番に配置されます。上の構造体では、3つのメンバが、4バイト、20バイト、4バイトというように、下に示したように配置されます。構造体の実体は、このように、メンバデータを連続したメモリ領域に格納したものになります。

student構造体
numberメンバ
(4バイト)
nameメンバ
(20バイト)
ageメンバ
(4バイト)

メンバデータへのアクセス

では、HSPスクリプト上で構造体にデータを格納したり、構造体からデータを取り出す操作を考えてみましょう。

HSP上では、ほとんどの場合、数値型配列変数を構造体とみたてて使用することになります。ここでは、変数stに、上の例のstudent構造体をとることを考えてみましょう。

この構造体の最初の4バイトはnumberメンバですが、これはst.0にあたります。次のnameメンバは20バイトで、st.1st.5の部分です。そして、その次の4バイトがageメンバで、st.6にあたります。

構造体 student構造体
numberメンバ
(4バイト)
nameメンバ
(20バイト)
ageメンバ
(4バイト)
HSP変数 st.0 st.1 st.2 st.3 st.4 st.5 st.6

numberメンバとageメンバについては、それぞれst.0st.6に対応していますから、データの読み書きは簡単ですね。

; データの読み取り
number = st.0
age    = st.6
; データの書き込み
st.0 = number
st.6 = age

さて、問題はnameメンバです。このメンバには文字列が格納されますが、配列変数st自体は数値型の変数ですから、直接参照することはできません。そこで、通常は、いったん別の文字列型変数を介します。

nameメンバ(st.1st.5)に格納されている文字列データを読み込む場合は、まずいったん別の文字列型変数を用意して、そこにコピーするという方法をとります。これには、peek命令またはwpeek命令を使います。これらの命令は、第1パラメータが文字列変数の場合には、データが文字列であるとみなして、文字列の最後までを取り出してくれます。nameメンバは、構造体の先頭から4バイトの位置にあるので、第3パラメータに4を指定します。

name = ""                       ; 文字列変数であることが必要
peek name, st, 4                ; wpeek でも同じ

nameメンバがst.1の位置から始まっていることから、以下のようにしてもOKです。

name = ""
peek name, st.1                 ; wpeek でも同じ

あるいは、メモリブロックのコピーを行なうmemcpy命令でもいいでしょう。この場合は、コピーするサイズとして、メンバのサイズである20を指定することになります。

name = ""
memcpy name, st, 20, 0, 4       ; 0 は省略可
name = ""
memcpy name, st.1, 20, 0, 0     ; 0 は省略可

今度は、逆に、文字列のデータを構造体のメンバ(st.1st.5の部分)に格納する方法を考えてみましょう。この場合は、上の時とは逆に、poke命令またはwpoke命令を使うことになります。これらの命令は第3パラメータとして文字列(または文字列型変数)を指定した時に、文字列全体を指定された部分にコピーしてくれます。または、先ほどと同様にmemcpy命令を使うこともできます。以下のうち、どのようにしてもOKです。(文字列は20バイト未満である必要はありますが。)

name = "(格納する文字列)"
poke st, 4, name                ; wpoke でも同じ
name = "(格納する文字列)"
poke st.1, 0, name              ; wpoke でも同じ
name = "(格納する文字列)"
memcpy st, name, 20, 4, 0       ; 0 は省略可
name = "(格納する文字列)"
memcpy st.1, name, 20, 0, 0     ; 0 は省略可

Win32 APIで使用される構造体

ここで、Win32 APIで使われる構造体について説明しておきましょう。

Win32 APIでの構造体の定義

Win32 APIで用いられる構造体は、一般的に次のように定義されています。(これはPOINT構造体の定義例です。)

typedef struct tagPOINT {
    LONG x;
    LONG y;
} POINT, *LPPOINT;

別に、C/C++の学習をしているわけではないので、詳しく覚える必要もありませんが、とりあえず簡単に説明しておきます。読み方を覚えておけば、Win32 APIの解説をしている他のホームページ(多くの場合C/C++が使われています)を見たときに理解しやすくなると思いますので。

上の定義は、構造体の定義と、データ型(構造体)に別名をつけるのを同時に行なっているので、やや分かりづらいものがあります。この2つの作業を分けて書くと、次のように書き換えられます。このように見ると、やや分かりやすくなります。

struct tagPOINT {
    LONG  x;
    LONG  y;
};

typedef struct tagPOINT  POINT;

typedef struct tagPOINT *LPPOINT;

まず、最初の部分では、LONG型(4バイト整数)の2つのメンバxyを持つtagPOINTという名前の構造体を定義している、ということは分かると思います。

2つ目の部分にある「typedef」は、データ型の別名(同義名)をつけるためのものです。データ型「struct tagPOINT」に「POINT」という別名をつけたということを表しています。つまり、POINTというのは、tagPOINTという名前で定義された構造体のデータ型を表します。

3つ目の部分は、ちょっと分かりにくいですが、データ型「struct tagPOINT *」に「LPPOINT」という別名をつけたということを表しています。「struct tagPOINT *型」というのは、「struct tagPOINT型」を指すポインタのデータ型です。すなわち、構造体メンバの定義で

LPPOINT lppt;

と記述されていたら、lpptメンバはPOINT構造体へのポインタであり、POINT構造体のデータが格納されたメモリブロックのアドレスが格納されるのだ、ということです。

ちなみに、構造体によっては次のように定義されている場合もあります。再びPOINT構造体を例に取ると、

typedef struct {
    LONG x;
    LONG y;
} POINT, *LPPOINT;

といった感じです。はじめに記述した定義のtagPOINTにあたる部分がありません。この場合は、「struct tagPOINT型」の代わりに、「struct {...}型」に対して別名をつけたと考えれば上と同じだと分かります。

入れ子の構造体

Win32 APIで使われる構造体には、入れ子の構造体というものも多く存在します。入れ子の構造体とは、構造体のメンバとして他の構造体を持つようなもののことをいいます。例えば、TPMPARAMS構造体はメンバとしてRECT構造体を持っています。これらの構造体は以下のように定義されています。

typedef struct tagRECT {
    LONG left;
    LONG top;
    LONG right;
    LONG bottom;
} RECT, *LPRECT;
typedef struct tagTPMPARAMS { 
    UINT cbSize;
    RECT rcExclude;  // ←入れ子の RECT 構造体
} TPMPARAMS, *LPTPMPARAMS;

これを見れば分かりますが、TPMPARAMS構造体は第2メンバにRECT構造体を持っています。TPMPARAMS構造体を大雑把な目で見れば、サイズ4バイト(UINT型)のデータとサイズ16バイト(RECT型)のデータから構成されていると見ることができますし、細かく見ればサイズ4バイト(UINT型およびLONG型)のデータ5個から構成されていると見ることもできます。すなわち、この構造体はサイズ20バイトであることが分かります。さらに、TPMPARAMS構造体を配列変数tpmにとってみると、以下のような対応になります。

構造体 TPMPARAMS構造体
cbSizeメンバ rcExcludeメンバ(RECT構造体)
leftメンバ topメンバ rightメンバ bottomメンバ
HSP変数 tpm.0 tpm.1 tpm.2 tpm.3 tpm.4

HSPのBMSCR構造体

HSPでは、HSPウィンドウを管理するために、内部でBMSCR構造体にウィンドウに関するさまざまな情報を格納しています。スクリプト上では、mref命令を使用することによって、ウィンドウに関連付けられているBMSCR構造体を変数に割り当てることができます。

以下のように記述すると、現在描画中のウィンドウについてのBMSCR構造体を変数bmscrに割り当てることができます。

mref bmscr, 67

BMSCR構造体にはさまざまな情報が含まれていて、それらすべてをここで説明することはできませんが、3つの重要なメンバをピックアップしておきましょう。

HSP変数 メンバ 意味
bmscr.4 hdc 画面のデバイスコンテキストのハンドル
bmscr.13 hwnd ウィンドウハンドル
bmscr.14 hInst インスタンスハンドル

デバイスコンテキストのハンドル

Windowsでは、画像のコピーやウィンドウへの描画などは、Windowsが提供するGDI(グラフィック・デバイス・インターフェース)によって行なわれますが、このGDIで使用されるさまざまなオブジェクトの情報を管理しているのがデバイスコンテキストです。デバイスコンテキストはハンドルによって識別されていて、Win32 APIによってHSP画面に描画を行なったりする場合にはHSP画面のデバイスコンテキストのハンドルが必要になります。

ただし、BMSCR構造体に格納されているハンドルは、メモリデバイスコンテキストと呼ばれるもので、実際にディスプレイに表示されているウィンドウのデバイスコンテキストとは異なるものです。そのため、メモリデバイスコンテキストのハンドルを用いて描画を行なった場合には、redraw命令を使用して実際のウィンドウに反映させる必要があります。

mref bmscr, 67        ; BMSCR 構造体
hdc = bmscr.4         ; デバイスコンテキストのハンドル取得

デバイスコンテキストについては、いずれ使うことになると思いますので、そのときに詳しく説明します。

ウィンドウハンドル

ウィンドウハンドルとは、ウィンドウを識別するための値のことです。このハンドルはHSPウィンドウを識別するためのもので、Win32 APIでHSPウィンドウを操作対象とする場合には、このハンドルが必要になります。

mref bmscr, 67         ; BMSCR 構造体
hwnd = bmscr.13        ; ウィンドウハンドル取得

これいついても、必要になったときにまた説明します。

インスタンスハンドル

メモリ上にロードされたすべての実行可能ファイル(.exe)やDLLには、それぞれに対して、そのプロセス上でユニーク(一意)なインスタンスハンドルが割り当てられます。はじめに起動された実行可能ファイル(例えばHSPの場合は「hsp2.exe」)にも、当然インスタンスハンドルは割り当てられます。HSPでは、このインスタンスハンドルがBMSCR構造体に格納されていて、Win32 APIでこれを使用することができます。

DLLのインスタンスハンドルについては、次の『Win32 APIの呼び出し』の項で説明します。

mref bmscr, 67         ; BMSCR 構造体
hInst = bmscr.14       ; インスタンスハンドル取得

インスタンスハンドルは、主に、その実行可能ファイルやDLLが持っているリソースをロードする時に使用されます。例えばHSPの実行可能ファイルには、カップの絵で表示されるアイコンリソースが含まれていますが、これをロードしてプログラム中で使用するためには、このインスタンスハンドルを指定する必要があります。