UWSCでコールバック関数を処理する

方法を見つけてしまった。
いや、見つけた、というほどのことではないのだけど。
もちろん、Asmモジュールを使ってCプログラムを作れば呼べる。
でもそれは、個々のCallbackそれぞれに対して、Cプログラムが必要となる。
汎用性がない。
今回の発見はそこを打破する。



スクリプト

Callback.uws

OPTION EXPLICIT

CALL Asm


IFB GET_UWSC_NAME = "Callback.uws" THEN

	DEF_DLL EnumWindows(DWORD,DWORD): BOOL: user32
	DIM para = Callback.Create(8, "SampleEnumWindowsProc(para, op)", 10, TRUE)
	MSGBOX("EnumWindows: " + Callback.Do(para, "EnumWindows(addr, op)", 135))
	Callback.Dispose(para)

	Callback.Dispose()
ENDIF

PROCEDURE SampleEnumWindowsProc(para, wait=10)
	DIM hwnd, lParam, addr = Callback.Addr(para)
	WHILE Callback.IsAlive(para)
		IFB Callback.WaitEvent(para) = 0 THEN
			hwnd = Asm.GetDword(addr)
			lParam = Asm.GetDword(addr + 4)
			PRINT FORMAT(hwnd, 8, -1) + " " + STATUS(HNDTOID(hwnd), ST_TITLE) + " " + lParam
			Callback.SetDone(para)
		ENDIF
	WEND
FEND



MODULE Callback
	DEF_DLL CreateEventW(DWORD,BOOL,BOOL,wstring): DWORD: kernel32
	DEF_DLL SetEvent(DWORD): BOOL: kernel32

	CONST PARA_BASE = 40
	PUBLIC minWait = 15

	HASHTBL _allocs, _results

	FUNCTION Create(size, evalstr, op=EMPTY, ret=0, wait=1000, free=0, stdcall=TRUE)
		DIM addr = Asm.Set("U7tEMyIRuP////+6AAAAAIP7AHQ8i0sk4xX8i8aL14v0g8YIi/uDxyjzpIvwi/r/" + _
					"cwj/E/9zFP9zDP9TBIlDIItDGLoAAAAAg3scAHQDi1MkW1kD4lHD")
		DIM paraSize = size + PARA_BASE, para = addr + 96, i = 0
		DIM hEvWait = CreateEventW(NULL, FALSE, FALSE, NULL)
		DIM hEvNext = CreateEventW(NULL, FALSE, FALSE, NULL)

		IFB addr > 0 AND para > 0 AND hEvWait > 0 AND hEvNext > 0 THEN
			i = i + Asm.SetDword(para + i, Asm.GetProcAddress(Asm.hK32, "SetEvent"))
			i = i + Asm.SetDword(para + i, Asm.GetProcAddress(Asm.hK32, "WaitForSingleObject"))
			i = i + Asm.SetDword(para + i, hEvWait)
			i = i + Asm.SetDword(para + i, hEvNext)
			i = i + Asm.SetDword(para + i, free)	// free
			i = i + Asm.SetDword(para + i, wait)	// Wait time
			i = i + Asm.SetDword(para + i, ret)		// callback ret
			i = i + Asm.SetDword(para + i, stdcall)	// stdcall
			i = i + Asm.SetDword(para + i, 0)		// wait result
			i = i + Asm.SetDword(para + i, size)	// param size
			Asm.SetDword(addr + 2, para)

			_allocs[para] = addr
			THREAD PrivateThreadEval(para, evalstr, op)
			RESULT = para
		ELSE
			IF addr > 0 THEN Asm.Free(addr)
			IF para > 0 THEN Asm.Free(para)
			IF hEvWait > 0 THEN Asm.CloseHandle(hEvWait)
			IF hEvNext > 0 THEN Asm.CloseHandle(hEvNext)
			addr = 0
			para = 0
			hEvWait = 0
			hEvNext = 0
		ENDIF
	FEND

	PROCEDURE PrivateThreadEval(para, evalstr, op, addr=0, ev=0)
		DIM ret = 0
		TRY
			ret = Eval(evalstr)
		EXCEPT
			ret = TRY_ERRMSG
		ENDTRY
		IFB ev > 0 THEN
			SetEvent(ev)
			_results[para] = ret
		ENDIF
	FEND

	FUNCTION Do(para, evalstr, op=EMPTY)
		DIM addr = _allocs[para], ev = CreateEventW(NULL, FALSE, FALSE, NULL)
		THREAD PrivateThreadEval(para, evalstr, op, addr, ev)
		DIM ret = 258
		WHILE ret = 258
			ret = Asm.WaitForSingleObject(ev, minWait)
		WEND
		RESULT = _results[para]
		Asm.CloseHandle(ev)
		SLEEP(0.01)
	FEND

	PROCEDURE Dispose(para=-1)
		IFB para = -1 THEN
			WHILE LENGTH(_allocs)
				Dispose(_allocs[0, HASH_KEY])
			WEND
		ELSE
			DIM addr = _allocs[para]
			DIM ret = _allocs[para, HASH_REMOVE]
			ret = _results[para, HASH_REMOVE]
			Asm.CloseHandle(Asm.GetDword(para + 8))
			Asm.CloseHandle(Asm.GetDword(para + 12))
			Asm.Free(para)
			Asm.Free(addr)
		ENDIF
	FEND

	FUNCTION WaitEvent(para)
		DIM ev = Asm.GetDword(para + 8)
		RESULT = 258
		WHILE RESULT = 258
			RESULT = Asm.WaitForSingleObject(ev, minWait)
		WEND
	FEND
	FUNCTION SetDone(para)
		RESULT = SetEvent(Asm.GetDword(para + 12))
	FEND
	FUNCTION IsAlive(para)
		RESULT = _allocs[para, HASH_EXISTS] AND !_results[para, HASH_EXISTS]
	FEND
	FUNCTION Data(para, pos, data=EMPTY)
		RESULT = Asm.GetDword(para + pos)
		IF data <> EMPTY THEN Asm.SetDword(para + pos, data)
	FEND
	FUNCTION Free(para, data=EMPTY)
		RESULT = Data(para, 16, data)
	FEND
	FUNCTION Wait(para, data=EMPTY)
		RESULT = Data(para, 20, data)
	FEND
	FUNCTION Ret(para, data=EMPTY)
		RESULT = Data(para, 24, data)
	FEND
	FUNCTION Call(para, data=EMPTY)
		RESULT = Data(para, 28, data)
	FEND
	FUNCTION WaitRet(para, data=EMPTY)
		RESULT = Data(para, 32, data)
	FEND
	FUNCTION Addr(para)
		RESULT = para + PARA_BASE
	FEND

ENDMODULE

サンプルスクリプトは、EnumWindows。
UWSCには、GETALLWIN関数があるので、全く不要なスクリプトですけど。

使い方

以下の流れでスクリプトを組みます。

  1. コールバック関数用UWSCスクリプトの作成
  2. Callback.Create関数の実行
  3. Callback.Do関数の実行
コールバック関数用UWSCスクリプトの作成

サンプルスクリプトでは、SampleEnumWindowsProc関数のようなものです。
paraは必ず受け取るようにしてください。
他はCallback.Create関数経由でopが受け取れます。(任意)
Callback.IsAlive(para)の間ループしている想定です。
Callback.WaitEvent(para)のリターンが0の場合、コールバックイベントが発生しています。
Callback.Addr(para)を先頭に、コールバック引数のパラメーターが入っています。
Asm.GetDword等で取得してください。
コールバック関数の戻り値を変更したい場合、Callback.Ret(para, 戻り値)で変更してください。
Callback.SetDone(para)で、コールバック関数処理を終わり、次のイベント待ちします。

Callback.Create関数の実行

第一引数:コールバック関数の引数のサイズ(Byte)
第二引数:上で作成したスクリプト関数の呼び出し文字列。必ずparaを渡す。他渡したいものがあればopに詰める
第三引数:op。コールバック関数用スクリプト関数に渡したいもの(任意)
第四引数:コールバック関数の初期戻り値。Callback.Retで変更可能
第五引数:コールバック関数用スクリプト関数の終了待ち時間
第六引数:自由使用エリア。4Byte。Callback.Freeで変更可能。第三引数は初回の値だがこれは外部からも変更可
第七引数:呼び出し規約。通常TRUE:stdcall(省略値)。FALSE:cdecl
戻りは、paraでCallbackの関数は基本的にこの値を必要とする

Callback.Do関数の実行

第一引数:para
第二引数:実行関数呼び出し文字列。コールバック関数のポインターは、addrであることに注意
第三引数:実行関数に渡したい場合、opとして使用(任意)

備考

WndProcとして使えるかって?
UWSCスクリプトで大量のメッセージを裁こうとするのは、難しいみたいよ。
WndProc用にするなら、専用ネイティブスレッドが必要みたい。


今回の汎用関数は、__declspec(naked)でアセンブリで書きました。