(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自体は命令の実行のたびに新しく生成されるほうがうれしいのだが、上記現象の都合上そうすることはできない。