XORveR.com の日記

XORveR.com の公式ブログです。

ニッチ過ぎて誰からも「いいね」されなかったシリーズ

まえがき

元文書は5年前の日付(さすがにMUIはある2.0台)なので、現在のNSISにも有効なのか不明です。
NSIS スクリプトをそろそろ書かないとならないため、おさらいしておく目的で整理し転記します。

ただ、WIX とかいうものがあるそうで、・・・まあ記事のネタに。

NSISスクリプトでの関数の作り方

NSIS の構文は基本的にアセンブラ言語がベースです。
そのため、変数操作にはスタックを多用して実装するスタイルになります。
CASL と比べるとスタック要素の交換命令である Exch の存在で、ずいぶん楽になっています。

● スタック操作

スタック操作命令は以下のものが使用可能です。

	Push (変数|レジスタ|リテラル)
		オペランドを「スタックトップ」にプッシュします。
	Pop (変数|レジスタ)
		「スタックトップ」をオペランドにポップします。
	Exch (変数|レジスタ|リテラル|正整数値)
		「スタックトップ」とオペランドを交換します。

		Exchは特殊で、正整数値の場合は異なる動作をします。
		正整数値はオフセットです。
		スタックトップとオフセット位置のアイテムを交換します。

このように命令が曲者です。
基本的には「スタックトップ」以外は操作できないからです。
スタックに積まれた値を利用したければ、Exch 命令でスタックトップに引き出して使う必要があります。

さあ、めくるめくアセンブラ風プログラミングの世界にダイブ!

● 関数の書き方

再利用のため、関数の機構が用意されています。
ただし、関数呼び出しに引数を渡す構文はありません。
フレームポインタのような高級な機能すらありません。

ですので、手書きのスタック操作でフレームを構築します。(凶悪)

実例として

	func(p0,p1,p2,p3) {
		var ret;
		var v1;
		var v2;

		(処理)

		return ret;
	}

というような関数を実現したい場合、以下のように設計していきます。

○ 1.入出力を決めます。

関数の呼び出し前後でどのような変数状態であるかということです。

  • 呼び出し前に、引数をスタックに Push しておく。
  • 呼び出し後に、結果はスタックに Push されている。
  • レジスタは呼び出し前後で変わらない。

というのが定石です。

ここで制約事項をまとめます。

	・ユーザ変数と衝突する可能性があるために名前付き変数は使用しません。
	 ですから使えるのはスタックとレジスタ ($0~$9,$R0~$R9) のみとなります。

	・引数はレジスタに取り出さなければ使用できません。
	 よって現在のレジスタの内容は逆にスタックに退避する必要があります。

	・自動変数として使用するレジスタは破壊されてしまいます。
	 よって現在のレジスタの内容はスタックに退避する必要があります。
	 これはレジスタ退避データと呼びましょうか。

	・レジスタ退避データは処理の最後でレジスタに復元します。
	 スタックポイントは、Pop することで調整します。

	・話が前後してしまいますが、関数内部で結果の格納用として使用するレジスタは
	 処理の最後でレジスタ退避データと Exch で交換する必要があります。
	 つまりスタックの底、スタックボトムの直前に位置する必要があります。
	 ※ ただし、定数値を返す場合は定数を Push するだけなので不要です。

実例の場合、引数が四個、変数が二個、戻り値が一個の場合なので、

	呼び出し前:スタックにp0~p3が積まれている。
	呼び出し中:$R0(ret), $R1(p0), ... $R4(p3), $R5(v1), $R6(v2) を使用する。
	呼び出し後:スタックからレジスタ復元し、$R0(ret)を交換して終了。

とします。


具体的に呼び出し方は

	Push p0
	Push p1
	Push p2
	Push p3
	Call func
	Pop $結果

というコードになります。

関数内でのスタックの状態は呼び出し前の時点で

		[+0]	: p3
		[+1]	: p2
		[+2]	: p1
		[+3]	: p0
		[+4]	: スタックボトム、もしくは別の処理で使用中

呼び出し中は

		[+0]	: $R6 退避データ
		[+1]	: $R5 退避データ
		[+2]	: $R4 退避データ
		[+3]	: $R3 退避データ
		[+4]	: $R2 退避データ
		[+5]	: $R1 退避データ
		[+6]	: $R0 退避データ
		[+7]	: スタックボトム、もしくは別の処理で使用中

呼び出し後には

		[+0]	: res
		[+1]	: スタックボトム、もしくは別の処理で使用中

となります。

これを元に、実装してみます。

○ 2.処理のためのレジスタを準備します。

結果の格納用として使うレジスタはスタックのボトム直前に退避しておきます。
これは最後に Exch で交換するためです。

	Push $R0	; $R0 をスタックに退避 ($R0 を res として確保)

		[+0]	: $R0 退避データ
		[+1]	: p3
		[+2]	: p2
		[+3]	: p1
		[+4]	: p0
		[+5]	: スタックボトム、もしくは別の処理で使用中

	Exch 4		; [+0] と [+4] を交換

		[+0]	: p0
		[+1]	: p3
		[+2]	: p2
		[+3]	: p1
		[+4]	: $R0 退避データ
		[+5]	: スタックボトム、もしくは別の処理で使用中

引数データをレジスタと交換していきます。

	Exch $R1	; [+0] と $R1 を交換 ($R1 = p0)

		[+0]	: $R1 退避データ
		[+1]	: p3
		[+2]	: p2
		[+3]	: p1
		[+4]	: $R0 退避データ
		[+5]	: スタックボトム、もしくは別の処理で使用中

レジスタとの交換はスタックの先頭としかできませんので奥のアイテムと交換します。

	Exch 3		; [+0] と [+3] を交換

		[+0]	: p1
		[+1]	: p3
		[+2]	: p2
		[+3]	: $R1 退避データ
		[+4]	: $R0 退避データ
		[+5]	: スタックボトム、もしくは別の処理で使用中

引数の全てを同様に処理します。

	Exch $R2	; [+0] と $R2 を交換 ($R2 = p1)

		[+0]	: $R2 退避データ
		[+1]	: p3
		[+2]	: p2
		[+3]	: $R1 退避データ
		[+4]	: $R0 退避データ
		[+5]	: スタックボトム、もしくは別の処理で使用中

	Exch 2		; [+0] と [+2] を交換

		[+0]	: p2
		[+1]	: p3
		[+2]	: $R2 退避データ
		[+3]	: $R1 退避データ
		[+4]	: $R0 退避データ
		[+5]	: スタックボトム、もしくは別の処理で使用中

	Exch $R3	; [+0] と $R3 を交換 ($R3 = p2)

		[+0]	: $R3 退避データ
		[+1]	: p3
		[+2]	: $R2 退避データ
		[+3]	: $R1 退避データ
		[+4]	: $R0 退避データ
		[+5]	: スタックボトム、もしくは別の処理で使用中

	Exch 1		; [+0] と [+1] を交換

		[+0]	: p3
		[+1]	: $R3 退避データ
		[+2]	: $R2 退避データ
		[+3]	: $R1 退避データ
		[+4]	: $R0 退避データ
		[+5]	: スタックボトム、もしくは別の処理で使用中

	Exch $R4	; [+0] と $R4 を交換 ($R4 = p3)

		[+0]	: $R4 退避データ
		[+1]	: $R3 退避データ
		[+2]	: $R2 退避データ
		[+3]	: $R1 退避データ
		[+4]	: $R0 退避データ
		[+5]	: スタックボトム、もしくは別の処理で使用中

自動変数は、使用するレジスタを退避するだけです。

	Push $R5	; スタックに $R5 を退避 ($R5 を v1 として確保)
	Push $R6	; スタックに $R6 を退避 ($R6 を v2 として確保)

		[+0]	: $R6 退避データ
		[+1]	: $R5 退避データ
		[+2]	: $R4 退避データ
		[+3]	: $R3 退避データ
		[+4]	: $R2 退避データ
		[+5]	: $R1 退避データ
		[+6]	: $R0 退避データ
		[+7]	: スタックボトム、もしくは別の処理で使用中

これで、$R0(ret), $R1(p0), ... $R4(p3), $R5(v1), $R6(v2) を使用することができるようになりました。

○ 3.関数の処理の記述

以下のレジスタを変数として使用して、処理を記述します。

	$R0		: res
	$R1		: p0
	$R2		: p1
	$R3		: p2
	$R4		: p3
	$R5		: v1
	$R6		: v2
○ 4.return の書き方

自動変数として使っていたレジスタを復元します。

結果以外は Pop して呼び出し前のレジスタを復元します。
結果はスタックに積まれる形なので交換します。

	Pop $R6		; スタックから $R6 を復元
	Pop $R5		; スタックから $R5 を復元
	Pop $R4		; スタックから $R4 を復元
	Pop $R3		; スタックから $R3 を復元
	Pop $R2		; スタックから $R2 を復元
	Pop $R1		; スタックから $R1 を復元
	Exch $R0	; スタック[0] ($R0) と res を交換

これでスタックは

		[0]	: res
		[+1]	: スタックボトム、もしくは別の処理で使用中

となり、インタフェース要件を満たします。

● まとめ

最後に関数の構文として纏めます。

	Function func
		Push $R0	; $R0 をスタックに退避 ($R0 を res として確保)
		Exch 4		; [+0] と [+4] を交換
		Exch $R1	; [+0] と $R1 を交換 ($R1 = p0)
		Exch 3		; [+0] と [+3] を交換
		Exch $R2	; [+0] と $R2 を交換 ($R2 = p1)
		Exch 2		; [+0] と [+2] を交換
		Exch $R3	; [+0] と $R3 を交換 ($R3 = p2)
		Exch 1		; [+0] と [+1] を交換
		Exch $R4	; [+0] と $R4 を交換 ($R4 = p3)
		Push $R5	; スタックに $R5 を退避 ($R5 を v1 として確保)
		Push $R6	; スタックに $R6 を退避 ($R6 を v2 として確保)

		(処理内容)

		Pop $R6		; スタックから $R6 を復元
		Pop $R5		; スタックから $R5 を復元
		Pop $R4		; スタックから $R4 を復元
		Pop $R3		; スタックから $R3 を復元
		Pop $R2		; スタックから $R2 を復元
		Pop $R1		; スタックから $R1 を復元
		Exch $R0	; スタック[0] ($R0) と res を交換
	FunctionEnd

実際は条件によって色々な解が存在しますので、この実装は例に過ぎません。
応用として、複数の結果を返す(スタックに複数の値を積んで戻る)実装もあります。

● マクロの使用

実は、インストールセクションで使える関数とアンインストールセクションで使える関数は明確に命名規則が定められています。
アンインストールセクションから呼ばれる関数は接頭語として un. が付いている必要があります。(本稿の例ではun.funcとなります)

両方で使用するための書き方として、マクロを使用するのが定石です。
マクロはプリプロセッサによりインラインで展開されます。

ですから、通常では関数は

	; マクロ定義
	!macro func un
	; 関数定義
	Function ${un}func
		・・・
	FunctionEnd
	!macroend

	; インストール時に使用される $func としての展開
	!insertmacro func ""

	; アンインストール時に使用される $un.func としての展開
	!insertmacro func "un."

という書き方をすることが多いです。

これで同じ内容で両セクションで使用できる関数に展開され、アンインストール
セクションからも un.func として呼び出せるようになります。

では!


予告

シリーズ第二弾は wsf の書き方になる予定!