構造体のコツ

上位ワードと下位ワード

Win32 APIを使う際にたびたび必要になるのが4バイト整数値の上位2バイトと下位2バイトの概念です。これらは、WORD型が2バイトのデータ型であることから、上位ワード下位ワードとも呼ばれます。4バイト整数値を16進数で表すと8桁になりますが、このうちの上4桁のことを上位ワードといい、下4桁のことを下位ワードといいます。例えば、4バイト数値0x12345678の上位ワードは0x1234であり、下位ワードは0x5678となります。

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

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

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

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

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

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

とか、

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

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

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

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

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

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

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

とします。


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

low  = wpeek(value, 0)
high = wpeek(value, 2)

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

wpoke value, 0, low
wpoke value, 2, high

とすることができます。

構造体

構造体とメンバ

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

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

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

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

//」は、C++の文法でその行の以降がコメントであることを示します。

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

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

上の定義を見ると、この構造体は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上では、ほとんどの場合、数値型配列変数を構造体とみたてて使用することになります。もちろん、文字列型変数に割り当てることも可能ですが、Win32 APIで使用される多くの構造体のメンバは数値やポインタなどの4バイトデータなので、数値型変数に割り当てると扱いが簡単になることが多いのです。ここでは、上の例のstudent構造体を数値型配列変数stにとることを考えてみましょう。

この構造体の最初の4バイトはnumberメンバですが、これはst(0)にあたります。次のnameメンバは20バイトで、st(1)st(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(0)st(6)に対応していますから、データの読み書きは簡単ですね。

; メンバデータの読み取り
number = st(0)
age    = st(6)
; メンバデータの書き込み
st(0) = number
st(6) = age

4バイト整数データの読み書きをする場合には、lpeek関数とlpoke命令を使用することもできます(むしろ、変数stを文字列型にした場合はこの方法でなければいけません)。これらは構造体の先頭からのインデックスをバイト単位で指定する必要があるので、そのことに注意しましょう。

; メンバデータの読み取り (lpeek 関数を使う方法)
number = lpeek(st, 0)
age    = lpeek(st, 24)
; メンバデータの書き込み (lpoke 命令を使う方法)
lpoke st,  0, number
lpoke st, 24, age

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

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

; メンバデータの読み取り
getstr name, st, 4          ; 変数 name は自動的に文字列型変数になる

ただし、1つ注意しておかなければならないのは、getstr命令は、途中に改行コードがある場合に、その直前までしか文字列を取り出さないということです。getstr命令では第4パラメータに区切り文字を指定することができますが、このパラメータに何を指定したとしても、改行があれば強制的に区切られてしまいます。

これを回避する方法として、メモリブロックのコピーを行うmemcpy命令を使うこともできます。この場合は、コピーするサイズとして、メンバのサイズである20を指定することになります。ただし、この場合にはHSP変数のサイズ自動拡張機能が作用しないので、あらかじめ必要なサイズ(20バイト)を確保しておく必要があります。

; メンバデータの読み取り (memcpy 命令を使う方法)
sdim name, 20               ; メンバのサイズと同じかそれ以上の領域を確保
memcpy name, st, 20, 0, 4   ; 0 は省略可

別の回避策として、モジュールを使用したユーザ定義関数peekstrを使用した方法が『HSP Help Center』の『HSP3ラウンジ』にて挙がっていたので紹介します。(モジュール機能については後の項で詳しく説明しますが、新しい命令や関数を自分で作成できるとだけ覚えておいてください。)

; あらかじめ文字列を取り出すためのユーザ定義関数 peekstr を定義しておく
#module
#defcfunc peekstr var data, int offset
dupptr result, varptr(data) + offset, 1, 2
return result
#global

; メンバデータの読み取り (自分で定義した peekstr 関数を使う方法)
name = peekstr(st, 4)

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

; メンバデータの書き込み (poke 命令を使う方法)
name = "(格納する文字列)"
poke st, 4, name            ; wpoke や lpoke でも同じ
; メンバデータの書き込み (memcpy 命令を使う方法)
sdim name, 20               ; メンバのサイズと同じかそれ以上の領域を確保
name = "(格納する文字列)"
memcpy st, name, 20, 4, 0   ; 0 は省略可

Win32 APIで使用される構造体

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

Win32 APIでの構造体の定義

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

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

別に、C/C++の学習をしているわけではないので、詳しく覚える必要もありませんが、とりあえず簡単に説明しておきます。読み方を覚えておけば、Win32 APIの解説をしている他のWebページ(多くの場合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)