文字列・変数ポインタを渡すには

文字列を渡す関数・アドレスを渡す関数

なぜポインタを渡すのか?

関数の中には、文字列へのポインタ(文字列のアドレス)や、メモリブロックへのポインタ(メモリブロックのアドレス)を引数として渡さなければならないものがあります。これらの関数がポインタを要求するのは主に以下のような場合です。

関数に文字列や構造体、配列などを渡す場合

例えば、Windowsのメッセージボックスを考えてみましょう。メッセージボックスとは、右のようなウィンドウのことで、MessageBox関数を呼び出すことで表示されます。このメッセージボックスを表示させるには、2つの文字列データが必要なのが分かりますね。すなわち、

です。実際には、関数の引数として、これらの文字列のアドレス(ポインタ)を指定しなくてはいけません。これについては、後で実際にスクリプトの例を示します。

関数から文字列や構造体、配列などを受け取る場合

API関数によって、何らかの情報を取得する場合などがこれにあたります。

例えば、プロセスのカレントディレクトリを取得するAPI関数GetCurrentDirectoryを呼び出すときです。(カレントディレクトリはHSP標準関数のdirinfoで取得することができますが、ここでは敢えてAPI関数を使用する場合を想定しています。) この場合には、カレントディレクトリを格納するためのメモリブロックをあらかじめ準備しておき、そのアドレスを引数として指定しなければいけません。

別の例として、あるウィンドウの現在の座標を取得するためのGetWindowRect関数を呼び出す場合には、ウィンドウ座標を格納するための構造体をあらかじめ準備して、そのアドレスを指定しなければいけません。

関数が複数の戻り値を返す場合

前にも述べたとおり、関数は通常、戻り値を1つしか持つことができません。したがって、さらに多くの戻り値を返さなくてはならないような関数は、データを戻り値として返す代わりに、引数として変数のアドレスを指定させ、その変数にデータを格納する、という手段をとります。

関数がメモリブロックのデータを参照し、さらに書き換える場合

これについてはいくつかの場合に分かれます。まず、元のメモリブロックに格納されているデータに変更を加えて、それを返すようなAPI関数の場合です。例えば、あるメモリブロックに格納されている文字列の末尾に別の文字列を結合するlstrcat関数があります。引数としては、結合したい2つの文字列のアドレスを渡すのですが、この関数は、結合された文字列を第3のメモリブロックに格納するのではなく、渡された文字列が格納されているメモリブロックの最初のものを、結合された文字列で上書きしてしまいます。

別の場合として、関数の呼び出しに必要となる情報を構造体として渡し、関数の結果が同じ構造体に返されるというものがあります。例えば、「ファイルを開く」ダイアログボックスを表示するGetOpenFileName関数がこれにあたります。この関数を呼び出すためには、あらかじめダイアログボックスの初期化情報を構造体に格納した形で準備し、その構造体のアドレスを引数として渡さなければいけません。そして、ダイアログボックスが閉じられると、取得された情報が同じ構造体に格納されるようになっているのです。

APIにポインタを渡す際の原則

ところで、引数としてアドレス(ポインタ)を渡すWin32 API関数には1つの原則があります。それは、データを格納するためのメモリブロックは呼び出し側が準備しなくてはならないということです。まれに関数側がメモリブロックを確保するAPI関数も存在しますが、ほとんどの場合は呼び出し側が確保しなければいけません。

このため、そのようなAPI関数を呼び出す際には、確実にデータを格納できるだけのサイズのバッファを確保しておかなくてはなりません。構造体を指定する場合には、その構造体のサイズ分の、また、文字列バッファの場合には、文字列が確実に格納できる分の大きさのバッファを準備しましょう。

そして、指定するアドレスも有効なものを指定しなくてはなりません。確保していない無効なアドレスを指定したり、アドレス以外のいいかげんな値を指定したりしてはいけません。

これらの事項を守らないと、プログラムが暴走したり、強制終了したり、あるいは、一見動作しているかのように見えて、まったく別の部分で原因不明のバグに悩まされたり、といった問題に直面することになってしまうかもしれません。

文字列を扱うAPI関数について

ANSI(マルチバイト)文字列とUnicode(ワイド)文字列

以前、文字列がHSPの文字列型変数にどのように格納されているのかについて述べましたね。文字列はメモリ上に「1バイト単位の数値の配列」として格納されていたのでした。1バイト文字だけからなるANSI文字列、あるいは、シフトJISなどの1バイト文字と2バイト文字とが混在したマルチバイト文字列についても、そのときに述べたとおりです。

一方、それらとは別の文字コード体系として、Unicode(ユニコード)というものが存在します。Unicodeは文字コードの世界標準となるべく作られた文字コードセットで、常に1文字2バイトで表します。英語、日本語、フランス語、ドイツ語、中国語、アラビア語なども含めて、世界のさまざまな言語の文字をコード化した文字セットとなっているので、アプリケーションやドキュメントの国際化が進むにつれて、その重要性は増していくことでしょう。

HSPでの文字列の扱いには(日本語Windows環境では)シフトJIS文字セットが使われていますし、テキストファイルなども一般的にはシフトJISが使われているので、Unicodeといっても、あまり馴染みがないかもしれませんね。しかし、インターネット上のWebページを記述するHTMLドキュメントやXMLドキュメントなどの分野でUnicodeが普及しつつあります。

2バイト文字セットを用いることで、理論上は65536種類の文字を表せることになりますが、実は、その程度の数では世界中の文字を表すことができなかったりします。そこで、2バイトUnicode文字セット(UCS-2と呼ばれる)を拡張した4バイトUnicode文字セットUCS-4が策定されています。そして、それらの文字コードの変換フォーマット、すなわち、文字列がメモリ上にどのような形式で格納されるのかを定義したものとして、UTF-7, UTF-8, UTF-16, UTF-32があります。

一般に、Unicode文字列と言った場合は、UTF-16の形式でメモリ上に格納されている文字列のことを指します。これは、はじめに述べたように、常に1文字に2バイトの領域を割り当てた形式です。一方、例えばUTF-8形式の場合は、1文字が1バイトから4バイトまで(現在は3バイトまで)の可変バイトで表されます。

ANSI版関数とUnicode版関数

データとして文字列を扱うタイプのWin32 API関数は、同じものが2種類存在します。具体的にはANSI文字列(またはマルチバイト文字列)用の関数Unicode文字列用の関数が存在するのです。これらは、それぞれ『ANSI版(ANSIバージョン)関数』および『Unicode版(Unicodeバージョン)関数』などというように呼ばれています。関数に渡したり、関数から受け取ったりする文字列がANSI文字列やマルチバイト文字列(シフトJISなど)の場合はANSI版の関数を、また、Unicode文字列(ワイド文字列)の場合はUnicode版の関数を使うことになっているのです。

文字列を扱うほとんどすべてのWin32 API関数では、ANSI版のものは関数名の最後が"A"で、Unicode版のものは関数名の最後が"W"で終わるようになっています。例えば、メッセージボックスを表示するMessageBox関数は、実際にはANSI版のMessageBoxAと、Unicode版のMessageBoxWの2つが存在するのです。シフトJIS文字セットで文字列を扱っているHSPでは、2つ存在する関数のうち、ANSI版のMessageBoxAを使うことになります。

しかし、実はすべての環境でこの2種類の関数が使えるというわけではないのです。Windows NT/2000/XPなどのいわゆるNTカーネルといわれるOSではANSI版とUnicode版の両方を使うことができますが、Windows 95/98/Meなどの9xカーネルといわれるOSでは、ほとんどのAPI関数でANSI版の関数だけしか使うことができないのです。また、HSP内部でも文字列の格納や処理をシフトJIS文字列の形で行っています。したがって、HSPでの関数呼び出しは、ほぼすべてANSI版の関数を使用することになります。

このドキュメントでは、本文中で関数名を示すときに、基本的には関数名の最後に"A"を付けないで説明しています。ただし、各API関数の定義の表記では"A"を付けて記述してあります。文字列を扱うAPI関数を呼び出すときには、そのことに注意してください。

実際に関数を呼び出してみる

では実際に、ポインタを必要とする関数を呼び出すスクリプトを書いてみましょう。いくつかのパターンに分けて考えていきます。

関数に文字列を渡す場合

まずはじめに、関数に文字列を渡す場合について考えてみましょう。

ここでは、メッセージボックスを表示するWin32 API関数であるMessageBox関数を扱うことにします。この関数は、user32.dllによって提供されており、次のように定義されています。

int MessageBoxA(
    HWND   hWnd,       // owner window handle
    PCTSTR pszText,    // message string
    PCTSTR pszCaption, // title string
    UINT   uType       // type
);

まず、MessageBoxAというように、関数名の最後に"A"がついているのに気が付くと思います。これは、先ほど述べたように、この関数がANSI版の関数であることを表しています。

最初のhWndパラメータは、表示するメッセージボックスを所有するウィンドウ(『オーナーウィンドウ』と呼ばれます)のハンドルを指定します。ウィンドウハンドルについては以前にも説明しましたが、これはウィンドウを識別するための値のことです。このパラメータにはシステム変数hwndを指定します。システム変数hwndには、現在の描画先HSPウィンドウのハンドルが格納されています。

2番目のパラメータpszTextと3番目のパラメータpszCaptionは、それぞれ表示するメッセージ文字列のアドレスとタイトル文字列のアドレスを指定します。これらの引数の型がPCTSTR型になっているのが分かると思いますが、これは文字列のアドレスを表すデータ型です。これが今回注目すべきパラメータですね。

4番目のuTypeパラメータには、表示するメッセージボックスの種類や動作を指定する値を指定するのですが、今回はとりあえず、[OK]ボタンを表示するだけの0 (MB_OK) を指定するだけにします。

引数の型について1つ述べておきますと、PCTSTR(またはLPCTSTR/PCSTR/LPCSTRなど)に含まれる「C」の文字は“const”のことで、これは、このアドレスで指定されるメモリブロックに格納されている文字列が関数呼び出し時に変更されない、ということを保証するものです。すなわち、関数はこのメモリブロックから文字列を読み出すだけで、ここに書き込むことはしない、ということです。逆に、「C」の文字を含まないPTSTR(またはLPTSTR/PSTR/LPSTRなど)の型の場合は、そのアドレスで指定されるメモリブロックに新しい文字列が書き込まれたり、すでに格納されている文字列が変更されたりする可能性があります。今回の場合には、関数に文字列を渡すだけで、関数内で変更されることはありまん。したがって、引数の型も“const”属性を持つPCTSTRとなっているのです。


関数に文字列を渡す場合、実は、#funcの引数型指定をどのように行うかによって、いくつかの方法があるのです。具体的には、

といった具合です。それぞれどのようになるのかを見ていくことにしましょう。

型指定 "str" を使う方法

#func命令で関数を登録する際に、引数の型の指定として「str」を指定すると、呼び出し時には、パラメータとして文字列を直接指定することができます。こうすると、関数には文字列のアドレスが自動的に渡されるようになります。

#uselib "user32.dll"
#func MessageBox "MessageBoxA" int, str, str, int

MessageBox hwnd, "API関数を呼び出しています", "テスト", 0

このように記述すると、HSP内部で確保されているメモリブロックに指定された文字列が格納され、そのアドレスが渡されるようになります。

もちろん、文字列を直接指定するのではなく、文字列型変数を指定することもできます。

#uselib "user32.dll"
#func MessageBox "MessageBoxA" int, str, str, int

s1 = "API関数を呼び出しています"
s2 = "テスト"
MessageBox hwnd, s1, s2, 0

このように記述した場合にも、HSP内部で確保されているメモリブロックに文字列が格納されて、そのアドレスが渡されます。ここで注意しておくべきことは、このように書いた場合には、変数s1s2のアドレスが渡されているのではないということです。HSP内部のメモリ領域にいったん文字列がコピーされ、そのアドレスが渡されているに過ぎません。

型指定 "var" を使う方法

#func命令で引数の型の指定として「var」を指定すると、呼び出し時にパラメータとして変数を指定することができます。こうすると、関数には変数のアドレスが自動的に渡されるようになります。文字列を渡す場合には、あらかじめ変数に文字列を格納しておく必要があります。

#uselib "user32.dll"
#func MessageBox "MessageBoxA" int, var, var, int

s1 = "API関数を呼び出しています"
s2 = "テスト"
MessageBox hwnd, s1, s2, 0

1つ上でstr型指定に対して変数を渡した場合には、HSPの内部領域にいったん文字列がコピーされて、その領域のアドレスが渡されていましたが、今回のようにvar型指定の場合には、変数s1s2のアドレスが直接渡されるようになります。

型指定 "int" を使う方法

#func命令の引数型指定「int」は、前回もやったように、パラメータとして渡された数値をそのまま関数呼び出しの引数とするものです。そのため、この方法で文字列を渡すには、varptr関数を使って、文字列変数のアドレスを指定するようにしなければいけません。

#uselib "user32.dll"
#func MessageBox "MessageBoxA" int, int, int, int

s1 = "API関数を呼び出しています"
s2 = "テスト"
MessageBox hwnd, varptr(s1), varptr(s2), 0

この方法の利点は、アドレスを指定するパラメータに数値を指定することもできるということです。例えば、いくつかの関数はアドレスを指定するパラメータにNULLポインタを指定できるようになっています。このMessageBox関数の場合、タイトル文字列を指定する第3パラメータに0 (NULL) を指定すると、タイトルが自動的に「エラー」という文字列になるという特徴があります。上記の型指定strvarを使った方法では、NULLを指定することはできませんが、int指定ならば可能です。

#uselib "user32.dll"
#func MessageBox "MessageBoxA" int, int, int, int

s1 = "API関数を呼び出しています"
MessageBox hwnd, varptr(s1), 0, 0    ; この場合は「エラー」タイトルを表示

型指定 "sptr" を使う方法

上記のように#func命令にstr型指定を使う方法は、文字列を直接指定できますが、NULLポインタなどを渡せない欠点がありました。一方、#func命令にint型指定を使う方法は、NULLポインタなどの数値を指定できるという長所がある反面、文字列をいったん変数に格納しなければならないという短所も持っています。

これらの問題を解決するために、#func命令で引数の型の指定として「sptr」があります。これは、パラメータが文字列(または文字列型変数)の場合にはその文字列のアドレスを渡し、パラメータが数値(または数値型変数)の場合にはその数値をそのまま渡すというものです。すなわち、sptr型指定はstr型指定の長所とint型指定の長所を兼ね備えているのです。

直接、文字列を指定するには次のように行います。

#uselib "user32.dll"
#func MessageBox "MessageBoxA" int, sptr, sptr, int

MessageBox hwnd, "API関数を呼び出しています", "テスト", 0

int型指定の場合と同様、varptrを使って変数のアドレスを指定することもできます。

#uselib "user32.dll"
#func MessageBox "MessageBoxA" int, sptr, sptr, int

s1 = "API関数を呼び出しています"
s2 = "テスト"
MessageBox hwnd, varptr(s1), varptr(s2), 0

NULLポインタも直接指定することができます。

#uselib "user32.dll"
#func MessageBox "MessageBoxA" int, sptr, sptr, int

MessageBox hwnd, "API関数を呼び出しています", 0, 0   ; 「エラー」タイトル

関数から文字列を受け取る場合

次は、関数から文字列を受け取る場合についてです。これは、関数が渡された変数を書き換える場合についても含まれます。

例として、カレントディレクトリの名前を取得するためのGetCurrentDirectory関数があります。カレントディレクトリはHSP標準関数のdirinfoで取得することができますが、ここではあえてAPI関数を使ってみましょう。GetCurrentDirectory関数はkernel32.dllに含まれる関数で、次のように定義されています。

DWORD GetCurrentDirectoryA(
    DWORD nSize,    // buffer size
    PTSTR pBuffer   // buffer for directory name
);

2番目のパラメータpBufferには、ディレクトリ名を格納するメモリブロックのアドレスを指定しなければなりません。最初のnSizeパラメータには、そのメモリブロックのサイズを指定します。HSPの場合には、あらかじめ文字列変数を確保しておいて、そのアドレスとサイズを渡すということになります。

ディレクトリを格納するための文字列型変数は、あらかじめ必要分だけ確保しなければなりません。この関数などのように、ファイル名やディレクトリ名を格納する変数は、最低でも260バイトだけ確保しておかなければいけません。Win32では、260MAX_PATHという定数名で定義しており、ファイル名・ディレクトリ名を格納するメモリブロックの必要サイズと定めています。

引数の型を先ほどのものと比較して見てみますと、ここではPTSTRとなっていますね。先ほどの文字列を関数に渡す場合のPCTSTRとは違って、“const”属性を持っていないことが分かります。すなわち、そのアドレスで指定される文字列が書き換えられるということを示しているのです。


関数から文字列を受け取る場合には、上で述べたような文字列を渡す場合とは異なり、選択肢が限られます。

見て分かると思いますが、#func命令の引数型指定にstrを指定することができませんね。sptr指定でも、文字列型変数を直接指定することはできず、varptrを介さなくてはなりません。

型指定 "var" を使う方法

#func命令の引数型varを指定する場合には、パラメータに文字列型変数を直接指定することができます。

#uselib "kernel32.dll"
#func GetCurrentDirectory "GetCurrentDirectoryA" int, var

sdim s1, 260
GetCurrentDirectory 260, s1
dialog "カレントディレクトリは[" + s1 + "]です。"

型指定 "int" または "sptr" を使う方法

#func命令の引数型intsptrを指定する場合には、varptrで文字列型変数のアドレスを取得し、それを指定しなければいけません。

#uselib "kernel32.dll"
#func GetCurrentDirectory "GetCurrentDirectoryA" int, int

sdim s1, 260
GetCurrentDirectory 260, varptr(s1)
dialog "カレントディレクトリは[" + s1 + "]です。"

構造体やメモリブロック(文字列以外)のアドレスを渡す場合

最後に、文字列以外のメモリブロックのアドレスを取り扱う場合、例えば関数に構造体を渡す場合について説明しましょう。これについては、上で述べた文字列を受け取る場合とまったく同様です。

ここでは、ウィンドウの座標を取得する関数であるGetWindowRect関数を呼び出すことにします。この関数はuser32.dllにより提供されていて、次のように定義されています。

BOOL GetWindowRect(
    HWND   hWnd,   // window handle
    LPRECT pRect   // address of RECT structure
);

hWndパラメータには、ウィンドウのハンドルを指定します。ウィンドウのハンドルについての詳しい説明はまた後でしますので、ここではとりあえず、HSPの描画先ウィンドウハンドルを表すシステム変数hwndを指定しておきます。そうすることにより、HSPウィンドウの座標を取得できます。

pRectパラメータには、RECT構造体の座標を指定します。以前にも一度出てきましたが、RECT構造体は次のように定義されている構造体です。

typedef struct tagRECT {
    LONG left;
    LONG top;
    LONG right;
    LONG bottom;
} RECT, *LPRECT;

関数を呼び出すと、渡された構造体に、ウィンドウの左上と右下の座標が格納されます。


先に述べたとおり、関数にパラメータを渡す方法は、文字列を受け取る場合と同じです。ただし、今回の場合は変数が文字列型変数とは限りません。

型指定 "var" を使う方法

#func命令の引数型varを指定する場合には、構造体として割り当てる変数を直接指定します。

#uselib "user32.dll"
#func GetWindowRect "GetWindowRect" int, var

dim rc, 4
GetWindowRect hwnd, rc
dialog "("+rc(0)+","+rc(1)+")-("+rc(2)+","+rc(3)+")", 0, "ウィンドウ座標"

型指定 "int" または "sptr" を使う方法

#func命令の引数型intsptrを指定する場合には、varptrで構造体として割り当てる変数のアドレスを指定します。

#uselib "user32.dll"
#func GetWindowRect "GetWindowRect" int, int

dim rc, 4
GetWindowRect hwnd, varptr(rc)
dialog "("+rc(0)+","+rc(1)+")-("+rc(2)+","+rc(3)+")", 0, "ウィンドウ座標"