(HSP)local変数のポインタを渡してはいけない(戒め)
タイトルですべてが終わっている話だが、自分のスクリプトで原因不明の謎な挙動が発生した経験から戒めとして残しておく。
HSP(Hot Soup Processor)という言語は基本的にすべての変数がグローバル変数になるという特徴があり、大規模なプログラムを作るときにはさまざまな工夫が必要になる。
そういった中で唯一ローカル変数として扱えるものが、モジュール機能のlocal宣言変数と呼ばれるもの。命令実行と同時に作成され、命令終了とともに破棄される。実行単位で新たに作成されることから、再起的に呼び出す場合などに多用される。(普通の変数だと再帰的に呼び出しても同じ変数を参照してしまう)
www.onionsoft.net
今般あるプログラムを開発中に、命令の終了とともに破棄されるこのlocal変数のポインタをWIN32 APIに突っ込んだ結果、さまざまな怪現象を引き起こしてしまった。原理がわかっていればさもありなんという感じだが、開発中は勿論特段エラーメッセージなどは表示されなかったため、原因究明に手間取ってしまった。
せっかくなので、そんな怪現象を引き起こしてしまったスクリプトを共有してみる(実行してへんなことが起こっても責任は取りませんのであしからず)。
このスクリプト自体は、ある周波数のサイン波の音をWIN32 APIを直接叩いて鳴らし続けるだけ。
そのうち Enter や Space キーを押下すると、怪現象のトリガーとなりうる命令が実行される。
冒頭の bugmode という変数で、「まともな」?コードと「バグった」コードを切り替えられるようにしており、0だと何も起きないはずだが、1だといろんな怪現象が起きる、はず。
#include "winmm.as" bugmode = 0 //1だとバグったコードが実行される goto *main #deffunc testfnc str _txt return #deffunc waveOut var _wave, int _nBufferLength, int _nUser, local whdrl, local lpData, local dwBufferLength, local dwUser // WAVEHDR 構造体 lpData = varptr(_wave) dwBufferLength = _nBufferLength dwBytesRecorded = 0 dwUser = _nUser dwFlags = 12 // WHDR_BEGINLOOP | WHDR_ENDLOOP dwLoops = 1 // ループ回数 lpNext = 0 reserved = 0 if bugmode = 1{ whdrl = lpData, dwBufferLength, dwBytesRecorded, dwUser, dwFlags, dwLoops, lpNext, reserved // 再生 waveOutPrepareHeader hWaveOut, varptr(whdrl), 32 // sizeof(WAVEHDR) waveOutWrite hWaveOut, varptr(whdrl), 32 // sizeof(WAVEHDR) }else{ whdrl_global(0, _nUser) = lpData, dwBufferLength, dwBytesRecorded, dwUser, dwFlags, dwLoops, lpNext, reserved // 再生 waveOutPrepareHeader hWaveOut, varptr(whdrl_global(0, _nUser)), 32 // sizeof(WAVEHDR) waveOutWrite hWaveOut, varptr(whdrl_global(0, _nUser)), 32 // sizeof(WAVEHDR) } return *main oncmd gosub *womessage,$3BD //MM_WOM_DONE dim whdrl_global,8,2 f = 300.0 //周波数 srate = 44100 buffersize = 20000 blocksize = 4 // WAVEFORMATEX 構造体 wBytesPerSample = 2 // 量子化バイト数 wFormatTag = 0x0001 // WAVE_FORMAT_PCM nChannels = 2 // ステレオ nSamplesPerSec = srate // サンプルレート wBitsPerSample = wBytesPerSample * 8 nBlockAlign = nChannels * wBytesPerSample nAvgBytesPerSec = nSamplesPerSec * nBlockAlign cbSize = 0 wfex = wFormatTag | (nChannels << 16), nSamplesPerSec, nAvgBytesPerSec, nBlockAlign | (wBitsPerSample << 16), cbSize hWaveOut = 0 waveOutOpen varptr(hWaveOut), -1, varptr(wfex), hwnd, 0, 0x00010000 // WAVE_MAPPER, CALLBACK_WINDOW sdim wave, buffersize*blocksize, 2 buffernum = 0 bufreadyflg = 0,0 //1だとバッファ格納済み repeat //波形の作成 repeat buffersize wpoke wave(buffernum), cnt * 4 , int(sin(2.0 * M_PI * f * yyyyy / srate) * 0x7FFF ) & 0xFFFF wpoke wave(buffernum), cnt * 4 + 2, int(sin(2.0 * M_PI * f * yyyyy / srate) * 0x7FFF ) & 0xFFFF yyyyy++ loop //波形の再生が終わるまで待つ buffernum = -1 repeat foreach bufreadyflg if bufreadyflg(cnt) = 0 : buffernum = cnt: break loop if buffernum >= 0 : break await 0 loop bufreadyflg(buffernum) = 1 //波形を流す waveOut wave(buffernum), buffersize*blocksize, buffernum buffernum = (buffernum+1)\2 stick sticks if sticks & 16{//space title "ユーザー定義命令に長い文字列を流す" testfnc "tttttttttttttttttttttttttttt" } if sticks & 32{//enter title "splitで文字列を複数個に分割する" tmp="a/a" split tmp, "/", tmp2 } loop *womessage dupptr whdr,lParam,8*4 if whdr(3) >= 0{ if length(bufreadyflg@) <= whdr(3) { title "eee" bufreadyflg@(0) = 0 bufreadyflg@(1) = 0 }else{ title ""+whdr(3) bufreadyflg@(whdr(3)) = 0 } } return 0
(参考)
www13.plala.or.jp
archive.kerupani129.net
以下でこのスクリプトの解説を少ししておく。
音の再生のためには、波形データをメモリ上に展開しておく必要がある。ここでWIN32 APIが提供している機能は、メモリ上に展開した波形を登録・再生する機能(waveOutWrite)と、再生が終わったことをウィンドウメッセージ(MM_WOM_DONE)で知らせる機能である。
動的に波形を生成する場合には、波形を生成→WIN32 APIに登録→波形を生成… という処理を繰り返す必要がある。前の波形の再生が終わってすぐに波形を生成・登録したとしてもわずかな時間のギャップが生まれてしまう。つまり音が途切れ途切れになってしまうので、前の波形の再生が終わる前に新しい波形を生成・登録しておく必要がある。
ここで再生中の波形データを上書きして登録するというのはいまいちなため、メモリ上の別の場所に展開してこちらを登録しておく。そして前の波形データの再生が終わったら、次はそちらの場所を再利用する。
このように、2箇所のメモリバッファを交互に使って再生することを、ダブルバッファリングという。
上記スクリプト中では、WAVEHDR 構造体(whdrl)に波形データを指すポインタなどを登録して再生、再生が終わったらサブルーチン「*womessage」でMM_WOM_DONEを受け取る、という処理を2個のバッファに対して交互に繰り返し実行することで波形データを再生している。
このうち今回のスクリプトで怪現象を発生させている変数が、ユーザー定義命令waveOut中にあるlocal宣言変数whdrlである。
bugmode = 1とした場合、こいつをwaveOutWriteに投げたあとでwaveOut命令の実行が終了、変数が破棄されてしまうためにその後の動作が色々不正となってしまう。
自分の環境では以下のような現象が発生した。
- (Enterキー押下により)split命令を行うと、2つあるバッファのうち1つからMM_WOM_DONEが帰ってこなくなり、ダブルバッファリングが無効化されて音が途切れ途切れになってしまう。
- (spaceキー押下により)ユーザー定義命令に長い文字列を渡すと、MM_WOM_DONEは帰ってくるものの、最初に渡した WAVEHDR 構造体を指しているはずのlParamが謎のメモリアドレスを指している。さらに以降でも(HSP側の)変数の参照がおかしくなったためか、bufreadyflgへの代入でスクリプトが止まってしまう。
どういう原理でこうなっているのかはわからないものの、HSP側の変数の管理に異常を来たしていることは明らかなようである。
ちなみに、バグらないほう(bugmode = 0とした場合)は、whdrlをグローバル変数で(バッファを2個用意している都合上)2個確保するよう対処している。
変数whdrl自体は命令の実行のたびに新しく生成されるほうがうれしいのだが、上記現象の都合上そうすることはできない。