ほのぼのとまったりアプリ開発日誌

ほのぼのがまったりとデスクトップアプリを作る開発日誌

摩訶不思議な #undef

linter-hsp3 をデバッグするには、わざとエラーになるソースコードが必要です。

なので、様々なシチュエーションを想像して、コードを書きました。

その時に見つけた不思議なエラーたちをここで紹介したいと思います。

また、紹介するコードは HSP スクリプトエディタで実行できるので、試してみて下さい。

#undef の普通の使い方

エラーを紹介する前に、正しい #undef の使い方を見てみましょう。

#define で登録した名称を取り除く使い方

#undef の機能は、指定したマクロ名(名称とも)を取り除くことです。

取り除かれた名称は、コメントアウトのような状態になります。

#define M
#undef M
#ifdef M
mes "use M"
#else
mes "no use M"
#endif

F5で実行した結果は…

no use M

#ifdef は、マクロ名称が登録されていれば、#else か #endif までをコンパイルします。そして、#else から #endif までのコードを取り除きます。

これで、#define で登録した M という名称が #undef で取り除かれたことが分かりました。

ここで疑問が浮かびました… #undef を使った位置がずれていたらどうなるのでしょうか?

とても興味深いことです。やってみましょう!

#define M
#ifdef M
mes "use M"
#else
mes "no use M"
#endif

#undef M

; try again
#ifdef M
mes "use M"
#else
mes "no use M"
#endif

実行した結果は…

use M
no use M

一行目は #undef で取り除く前のコードです。そして、二行目は #undef された後のコードです。つまり、#define と #undef は、HSP3のプログラムのように、コードの上から登録と取り除きが行われています!

この特徴を使えば、重複して同じソースファイルをincludeするのを防ぐことができます。

common\user32.as の最初と最後を見てみると…

;(user32.as)
#ifdef __hsp30__
#ifndef __USER32__
#define global __USER32__
#uselib "USER32.DLL"
   #func global ActivateKeyboardLayout "ActivateKeyboardLayout" sptr,sptr
   #func global AdjustWindowRect "AdjustWindowRect" sptr,sptr,sptr
    ~ ~ ~ 中略 ~ ~ ~
   #func global wvsprintfA "wvsprintfA" sptr,sptr,sptr
   #func global wvsprintfW "wvsprintfW" wptr,wptr,wptr
#endif
#endif

#ifndef でマクロが登録されていなければ、コンパイルするように囲まれています。試しに二回includeしてみましょう。

#include "user32.as"
#include "user32.as"
if varptr(MessageBox) != 0 :mes "ok"    ; 最適化対策

実行結果は、ちゃんとエラーにならずに ok と表示されました!

これ以外にも、様々な使い方ができます。

単体で実行するとテストを走らせる。前提となるソースファイルをincludeするなど、他には……

……これ以上広げると、#undefじゃない話になりそうなので、元に戻します。

標準キーワードを置き換える使い方

mes などの最初から使えるキーワードを標準キーワードと呼びます。

#undef は、標準キーワードを取り除くことができます。

取り除いた後に、#deffunc などで名称登録すれば、標準キーワードを置き換えられます。

やってみましょう!mes命令を猫が喋るように置き換えてみます。

goto *exit
#undef mes
#deffunc mes str p1, int p2
  mes@hsp "猫「"+p1+"」", p2
  return
*exit
mes "よろしくお願いします。"

実行結果は…

猫「よろしくお願いします。」

猫が喋ってます。うまくいきました!

主な使い道は、使ってはいけない標準キーワードを使っている外部ソースファイルを見つけたり、機能を拡張させることでしょうか。

標準キーワードを変更する場合、影響範囲が大きくなるので、注意が必要です。

#undef のあやしい使い方

ドキュメントに書かれていた使い方は、先の二つだけです。つまり、それ以外は未知の動作……洞窟探検の気分で挑みましょう。

#module は置き換えられない

nekoモジュールに age というテーブル変数を追加しようと、#undef を使って置き換えるコードです。

#module neko name
#modinit str p1
  name = p1
  return
#global

#undef neko
#module neko name, age
#modinit str p1, int p2
  name = p1
  age = p2
  return
#global

dimtype this, vartype("struct")
newmod this, neko, "tama"

しかし、コンパイルエラーになります。エラーメッセージは…

#Error:定義済みの識別子は使用できません [name@neko] in line 8 [blog\02.hsp]
#重大なエラーが検出されています

どうやら、モジュール名を取り除くだけでは付随したテーブル変数まで除いてくれないようです。

では、その変数も #undef で取り除いてみましょう。以下のコードを挿入します…

#undef neko
#undef name@neko    ; 挿入
#module neko name, age

編集した結果は、エラーです!

#識別子「neko」の定義位置: line 3 in [blog\02.hsp]
blog\02.hsp(11) : error 26 : パラメーター引数名は使用されています (11行目)
--> #struct neko var name@neko,var age@neko

テーブル変数は、名称だけでなく、パラメーター引数名として登録されていました。これで #undef では取り除けないことが分かりました!

#deffunc で登録した名称を置き換える

モジュールが置き換えられないなら、#deffunc はどうでしょうか?

やってみましょう!

まず最初にneko命令を登録します。それを#undefで取り除いてから、mes命令の第二引数に対応した新しいneko命令を登録します。

#module
#deffunc neko str p1
  mes "猫「"+p1+"」"
  return
#global

#undef neko
#module
#deffunc neko str p1, int p2
  mes "猫「"+p1+"」", p2
  return
#global

neko "よろしくお願いします。", 1
neko "猫です。", 1

実行結果は…

猫「よろしくお願いします。」猫「猫です。」

うまくいきました!

#deffunc で登録した名称は、#undef で取り除き、再度登録することができるのが分かりました。

#modfunc は置き換えられない

では #modfunc はどうでしょうか?モジュールが置き換えられないことが分かっていますが…

#module neko name
#modinit str p1
  name = p1
  return
#modfunc show
  mes name
  return
#global

#undef show
#module mod v1
#modfunc show
  mes "名前は"+v1+"です。"
  return
#global

  newmod this, neko, "maro"
  show this

最初に登録したshowメソッドを置き換えるコードです。結果はコンパイルエラーになりました。

blog\08.hsp(12) : error 25 : 命令として定義できない名前です (12行目)
--> #deffunc show modvar mod

しかし、真ん中でメソッドを呼ぶとコンパイルエラーは発生しませんでした。

#module neko name
#modinit str p1
  name = p1
  return
#modfunc show
  mes name
  return
#global

  newmod this, neko, "maro"
  show this

#undef show
#module mod v1
#modfunc show
  mes "名前は"+v1+"です。"
  return
#global

実行結果は、maro と表示されました。おそらく最適化によって、置き換え先のモジュール諸共は削除されてしまったのでしょう。

#const/#enum で登録した名称を置き換える

#defineが置き換えられたのなら、定数を登録する #const と #enum でも同様に置き換えらそうです。

やってみましょう!

#enum ringo = 88
#const mikan 398

#undef ringo
#enum ringo = 100
#undef mikan
#const mikan 789

mes ringo
mes mikan

実行結果は…

100
789

期待通りです!#define と同じように使えることが分かりました。

#func で登録した名称は置き換えられない

include先で外部DLLが登録されているシチュエーションは多いと思います。1

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

#undef MessageBox
#func global MessageBox "MessageBoxA" sptr,sptr,sptr,sptr

if varptr(MessageBox) != 0 :mes "ok"

色々と試しましたが、#undef は #func で登録した名称を取り除くことはできませでした。コンパイルエラーのメッセージは以下の通りです。

#識別子「messagebox」の定義位置: line 2 in [blog\10.hsp]
blog\10.hsp(5) : error 30 : 拡張命令の名前はすでに使用されています (5行目)
--> #func messagebox "MessageBoxA" sptr,sptr,sptr,sptr

この場合の迂回策は、名称を名前空間付きで登録することです。

#uselib "user32.dll"
#func global MessageBox "MessageBoxA" sptr,sptr,sptr,sptr
#func MessageBox@my "MessageBoxA" sptr,sptr,sptr,sptr

if varptr(MessageBox) != 0 :mes "ok"
if varptr(MessageBox@my) != 0 :mes "ok"

コンパイルが通って、ok が表示されました。名前空間無しのグローバルな名称と、名前空間付きの名称は異なっていることが分かります。

名前空間が変わるモジュールを使えば、名前空間を書かなくても自動で名称に追加されるので、大量の#funcにも対応できます。さらに、別のモジュールで名前が登録されていても、名前空間がちがうので、名前の衝突を回避できます!

#module A
  #uselib "user32.dll"
  #func MessageBox "MessageBoxA" sptr,sptr,sptr,sptr
#global

#module B
  #uselib "user32.dll"
  #func MessageBox "MessageBoxW" wptr,wptr,wptr,wptr

  #deffunc dialogW
  ; 同じ名前空間のMessageBoxが呼ばれる
  MessageBox 
  return
#global

if varptr(MessageBox@A) != 0 :mes "ok"
if varptr(MessageBox@B) != 0 :mes "ok"

#cmd で登録した名称は取り除けられる

HSP3になってからは、見る機会が減った気がする #cmd でも #undef で取り除けられるか試してみます。

#runtime "hsp3gp"
#regcmd 9
#cmd gpreset  $60
#cmd gpgetlog $f6

#undef gpreset
#cmd gpreset  $60
#undef gpgetlog
#cmd gpgetlog $f6
gpreset 0
gpgetlog log
logmes log

真っ暗な画面が出てきました、次に #undef の後の #cmd をコメントアウトして実行してみます。

#runtime "hsp3gp"
#regcmd 9
#cmd gpreset  $60

#undef gpreset
gpreset 0

結果は、文法エラーになりました。

blog\11.hsp(6) : error 2 : 文法が間違っています (6行目)
--> gpreset 0

#cmd で登録された gpreset が、#undef で取り除かれているのが分かります!

おまけ 最適化の謎

#module mod
       #deffunc func int p1
        mes "func"+p1
    return
#global

    func 1

#undef func
#undef mod
#module mod
   #deffunc func int p1
        mes "func2:"+p1
    return
#global

    func 2

このまま実行すると、以下のエラーメッセージが表示されます。

blog\05.hsp(12) : error 25 : 命令として定義できない名前です (12行目)
--> #deffunc func int p1@mod

しかし、最後のfunc 2コメントアウトすると、エラーは解決します。そして、画面には func2:1 と表示されています!置き換えられています!

興味深いことに、error 25 は配列変数に対するエラーIDですが、メッセージとは異なっています。それに、”命令として定義できない名前です”というエラーメッセージに関する情報をhdlから見つけることはできませんでした。

さらに、先のコードに名前空間で書くことで、エラー発生行数をおかしくさせることもできました。

#module mod
   #deffunc local func int p1
        mes "func"+p1
    return
#global

func@mod 1

#undef func
#undef mod
#module mod
   #deffunc local func int p1
        mes "func2:"+p1
    return
#global

func@mod 2

エラーメッセージは…

(1701232) : error 25 : 命令として定義できない名前です (1701232行目)

行数だけでなく、ファイルパスも消えてしまいました。

おまけのおまけ

#cmpopt ppout 1
#module mod
   #deffunc func
        mes "func"
    return
#global

func@mod 1

#undef func
#undef mod
#module mod
   #deffunc local func
        mes "func2"
    return
#global

func@mod 2

同じ名前空間名でユーザー定義命令を書いてみたかったのですが、コンパイルは通りませんでした。

tmp.hsp(16) : error 7 : ラベル名はすでに使われています (16行目)
--> *_mod_exit

プログラム実行時、モジュール内へ突入を防止するためのラベル定義が干渉しているようです。

おわりに

何に対して #undef できるか、これで大雑把に分ったと思います。

今まで書いた記事の中で、一番長いものになりました。

まだ記事にするネタのストックはありますが、別の機会で書こうと思っています。


  1. 代表格は、#include “user32.as"と#include "llmod3/llmod3.hsp"の組み合わせだと思います。