UWSCでGetLastError情報を取得する

MSDNを見てると、Falseが返ったらErrorだからGetLastErrorしろ、とか良く書いてある。
CreateFileもINVALID_HANDLE_VALUE(0xFFFFFFFF)ならエラーだから、GetLastErrorしろ、と。
でも、、、UWSCでは、GetLastErrorが呼べない。
いや、呼べるけど、呼ぶ過程で?SetLastError(0)されているみたいで常に0。
何がいけないのか、わかりません、、、。


ということで、StructモジュールとAsmモジュールを使って、APIとGetLastErrorを同時に呼ぶモジュールを作ってみた。



スクリプト

Dll.uws

OPTION EXPLICIT

CALL Struct
CALL Asm


// なんでGetLastErrorできないの!って時用
// ま、デバッグ用途な気がする

IFB GET_UWSC_NAME = "Dll.uws" THEN

	DIM name = "SetLastError"
	MSGBOX(name + "試験")
	DIM ret, e
	// DWORD値を渡す場合、第五引数は0(省略値)
	IF Dll.Run(ret, e, Asm.GetProcAddress(Asm.hK32, name), 12345) THEN
		MSGBOX(name + "(12345)の結果: " + e)
	ELSE
		MSGBOX(name + " Dll.Run失敗 " + e)
	ENDIF

	name = "GetOpenFileNameA"
	MSGBOX(name + "試験<#CR><#CR>構造体定義はStructモジュール参照")
	DIM openFileName = Struct.Create(name, _open_file_name)
	openFileName.lpstrFilter = "uwsファイル(*.uws)" + chr(0) + "*.uws"
	openFileName.nMaxFile = 256
	openFileName.lpstrFile = FORMAT(CHR(0), openFileName.nMaxFile)
	openFileName.lpstrInitDir = GET_CUR_DIR
	openFileName.lpstrTitle = "ファイルを選んでね"
	openFileName.Flags = OFN_HIDEREADONLY OR OFN_FILEMUSTEXIST OR OFN_EXPLORER
	//openFileName.lpstrCustomF = ""
	DIM ptr = Struct.Alloc(openFileName)
	// ポインターを渡す場合、第五引数は0
	IF Dll.Run(ret, e, name, ptr, 0, "comdlg32") THEN
		IF ret THEN
			Struct.Update(openFileName, ptr, FALSE)
			MSGBOX(name + " 選んだファイル: " + openFileName.lpstrFile)
		ELSEIF e = 0 THEN
			MSGBOX(name + " キャンセルされました")
		ELSE
			MSGBOX(name + "失敗 " + e)
		ENDIF
	ELSE
		MSGBOX(name + " Dll.Run失敗 " + e)
	ENDIF
	Struct.Free(ptr)

	name = "CreateFileA"
	MSGBOX(name + "試験")
	TEXTBLOCK _create_file
	{
		lpFileName:				['string', null],
		dwDesiredAccess:		['DWORD'],
		dwShareMode:			['DWORD'],
		lpSecurityAttributes:	['LPVOID'],
		dwCreationDisposition:	['DWORD'],
		dwFlagsAndAttributes:	['DWORD'],
		hTemplateFile:			['DWORD']
	}
	ENDTEXTBLOCK
	DIM createFile = Struct.Create(name, _create_file)
	createFile.lpFileName = openFileName.lpstrFile
	createFile.dwDesiredAccess = $80000000	//GENERIC_READ
	createFile.dwShareMode = 1	//FILE_SHARE_READ
	createFile.dwCreationDisposition = 3	//OPEN_EXISTING
	ptr = Struct.Alloc(createFile)
	// 引数が複数の場合、第五引数はサイズ(バイト単位)
	IF Dll.Run(ret, e, name, ptr, Struct.GetSize(name), "kernel32") THEN
		IF ret <> $FFFFFFFF THEN
			MSGBOX(name + "成功 " + ret)
			Asm.CloseHandle(ret)
		ELSE
			MSGBOX(name + "失敗 " + e + " " + Dll.FormatMessage(e))
		ENDIF
	ELSE
		MSGBOX(name + " Dll.Run失敗 " + e)
	ENDIF
	Struct.Free(ptr)

	Struct.Dispose()
	Dll.Dispose()
ENDIF


MODULE Dll
	DEF_DLL LoadLibraryW(wstring): DWORD: kernel32
	DEF_DLL FreeLibrary(DWORD): BOOL: kernel32
	DEF_DLL FormatMessageW(DWORD,DWORD,DWORD,DWORD,var wstring,DWORD,DWORD): DWORD: kernel32

	DIM _procAddr, _paraSize, _paraAddr


	PROCEDURE Dll
		Asm.Asm
		_procAddr = Asm.Set("VYvsg+wMi0UIiUX8x0X4AAAAAItN/IN5CAB0ZIll+ItV/IN6DAB0QYtF/ItN+CtI_
			DIlN+MdF9AAAAADrCYtV9IPCAYlV9ItF/ItN9DtIDHMWi1X8i0IIi034A030i1X0_
			igQQiAHr1usUi034g+kEiU34i1X4i0X8i0gIiQqLZfiLVfyLQgT/0IlF+ItN/ItV_
			+IlREItF/IsI/9GLVfyJQhSLRfiL5V3CBADM")
		_paraSize = 24
		_paraAddr = Asm.Alloc(_paraSize)
		IF _paraAddr > 0 THEN Asm.SetDword(_paraAddr, Asm.GetProcAddress(Asm.hK32, "GetLastError"))
	FEND

	PROCEDURE Dispose()
		Asm.Free(_paraAddr)
		Asm.Free(_procAddr)
	FEND

	// paraSize:即値の場合は0.中身の場合はそのサイズを指定
	FUNCTION Run(var ret, var e, func, para=0, paraSize=0, dll=EMPTY)
		RESULT = FALSE
		DIM hMod = NULL, i = 4, type
		IFB dll <> EMPTY THEN
			hMod = LoadLibraryW(dll)
			IFB !hMod THEN
				e = "LoadLibrary fail. " + dll
				EXIT
			ENDIF
		ENDIF
		IFB hMod = NULL AND VARTYPE(func) <> VAR_BSTR THEN
			i = i + Asm.SetDword(_paraAddr + i, func)
		ELSEIF hMod <> NULL AND VARTYPE(func) = VAR_BSTR THEN
			type = Asm.GetProcAddress(hMod, func)
			IFB !type THEN
				e = "GetProcAddress fail. " + func
				EXIT
			ENDIF
			i = i + Asm.SetDword(_paraAddr + i, type)
		ELSE
			e = "Invalid func. " + func
			EXIT
		ENDIF
		i = i + Asm.SetDword(_paraAddr + i, para)
		i = i + Asm.SetDword(_paraAddr + i, paraSize) + 4
		Asm.SetDword(_paraAddr + i, e)
		ret = Asm.Run(_procAddr, _paraAddr, type)
		e = Asm.GetDword(_paraAddr + i)
		RESULT = (type = 0)
		IF hMod <> NULL THEN FreeLibrary(hMod)
	FEND

	FUNCTION FormatMessage(msgId, langId=0)
		RESULT = EMPTY
		DIM p = 256, i = p
		WHILE i >= p - 1
			p = p * 2
			RESULT = FORMAT(CHR(0), p)
			i = FormatMessageW($1200, 0, msgId, langId, RESULT, p, 0)
		WEND
	FEND

ENDMODULE

必須は、Asmモジュール。
UWSCにできないことはない? - じゅんじゅんのきまぐれ
Structモジュールはあると便利(排除可能)
UWSCで構造体を使う - じゅんじゅんのきまぐれ

使い方

テストコードを参照してもらえれば、わかると思いますが、、、

DWORDが一つの関数を呼びたい場合(引数が4Byteまでの関数を呼ぶ場合)

Dll.Run関数の引数は

  1. 関数の戻り値用バッファ
  2. 直後のGetLastError値用バッファ
  3. 呼び出し関数名もしくは関数ポインター
  4. 関数に渡す値(DWORD)
  5. 0(即値の場合は0)
  6. 関数を持つDLL名(関数ポインター指定時は省略)

Dll.Run関数の戻りがTRUEの場合、Dll.Run成功。


テストコードには、SetLastErrorを使った例で書いてあります。
(SetLastErrorは戻り値voidなので、第一引数は気にしないこと)
もちろん、関数ポインターでなく、名前でも呼べます。
Dll.Run(ret, e, "SetLastError", 12345, 0, "kernel32")


あ、引数がない関数を呼びたい場合は、第四引数は適当で良いです。
引数が4Byteまでの関数の場合、エンディアンとstdcall呼び出し規約を考慮して、数値変換してください。
具体的には、WORD二つの場合、引数で渡すDWORDの上位が第二引数・下位が第一引数です。
BYTE四つなら、下位から順に第一・第二・第三・第四引数を設定してください。
(式的には、(((第四*256)+第三)*256+第二)*256+第一、となります)
4Byte未満なら下位から順に埋めて、上位が空く形です。
(式的には、上と同じ式で、三つなら第四=0なだけです)

ポインター一つの関数を呼びたい場合

第四引数をポインターにするだけ。
ポインターは、stuncloudさんのモジュールや私が書いたモジュールで作成してください。


テストコードには、GetOpenFileNameAで書いてあります。

上記以外の場合

第四引数はポインター
第五引数にそのサイズを渡す。


ポインターなのか実体なのかで、混乱しやすいところなので、要注意。
テストコードには、CreateFileAで書いてあります。


テストコードを実行した場合、GetOpenFileNameAでキャンセルすると、CreateFileAが3(パスなし)のエラーになり、GetOpenFileNameAで選択したファイルを、「CreateFile試験」前に削除すると、2(ファイルなし)になります。

応用

みんな大好きEVAL悪魔ですが、DEF_DLLはできません。
が、これを使えばEVALでDLL関数が呼べてしまいます。
もしかして、、、EVALでできないことが、、、OPTION指定くらいになったかな?
定数定義は、ま、できなくても困らない。(変数としてしまえばOK)
関数定義は、自前でがんばれば模すことができないことはない。(やりたくないけど)
OPTION指定も、、、がんばれば、、、いやだけど、、、。


ということで、2年ほど前に作ったUWSCインタープリター。
UWSCインタープリター - じゅんじゅんのきまぐれ
DLL呼べないな、と思ってましたが、こっちのスクリプトをCALLしておけば、呼べますね。
呼ぶ準備、面倒だけど。
簡易構文でも用意しようかしらん。
いや、それこそDEF_DLLを文字列で受け取る関数を作れば良いだけな気がする、、、。
うむ、可能じゃな。
要望があれば、、、そのうちやる、、、かもしれない。

解説

やっていることと言えば、関数を呼び出すことと、GetLastErrorを呼び出すこと。
それをネイティブスレッド内で順次行うことで、LastErrorが上書きされることを防いでいます。
呼び出したい関数の引数が不明なので、stdcall規約に従ってスタック操作しています。
多分、、、cdeclでも問題ない、、、はず?はず。


成否だけ返すAPIUWSCでコールすると、自分のスクリプトの不備が何かわからなくて、腹立たしい。
CreateFileにしても、失敗時は理由がわからない。
ということで、我慢できなくなり作りました。
Structモジュール作っている時も、GetOpenFileNameAのヤローが、キャンセルなのかエラーなのかわからない。
あのときは、ウインドウが出るなら100msecくらい過ぎるだろう、とせこい判定にしたけど、これで安心。
ま、キャンセルとエラーなんて同じでいいじゃん、とか聞こえてきそうですが。


パフォーマンス?
関数一つ呼ぶだけのために、スレッド起動してますからね。
良いわけないじゃない!(調べてないけど)
名前で呼ぶと、LoadLibraryして呼び終わった後、FreeLibraryしているから、かなり効率悪そうね。
UWSCが呼び出しているDLLを除く)


Cのコードとしては、以下のような感じのものが動いてます。
引数をスタックに積む処理がダサい。

DWORD Proc(PPara p)
{
	DWORD res = 0;
	if(p->pPara) {
		_asm mov dword ptr [res], esp;
		if(p->nPara) {
			res -= p->nPara;
			for(DWORD i = 0; i < p->nPara; i++) ((BYTE*)res)[i] = ((BYTE*)p->pPara)[i];
		} else {
			res -= sizeof(DWORD);
			*(DWORD*)res = (DWORD)p->pPara;
		}
		_asm mov esp, dword ptr [res];
	}
	res = p->Func();
	p->nRet = res;
	p->nLastError = p->GetLastError();
	return res;
}

エラートラップ等さぼりまくりなので、何か間違えるとすぐ落ちる。


なお、Dll.FormatMessageでシステムエラーコードを変換できます。
第二引数LangIDは0だと適当なのになるので、日本語環境なら日本語に変換されるかと思います。
英語にしたいなら、en_US($0409)でもどうぞ。


最近、アセンブリと親しみすぎな気がする、、、。
時代逆行も甚だしい。
でも、アセンブリならCPU依存なので、Intelが無事な限り使える。
MacIntelだし、意外と良いのかも。