x64には存在しないScriptControlの代わり

x64環境には、ScriptControlが存在しない。
例えば、x64のPowershellJScriptオブジェクトを受け取ってしまった場合、扱いに困る。
Get-Memberしても、何も返ってこない、、、。
ScriptControlがいれば、AddObjectして解釈できるのだけど。
ということで、方法を考えてみた。



追記 2015/03/05
ieに依存すると、外部プロセスとなるため、htmlfileに依存して内部プロセスにしたバージョンを書いた。
http://d.hatena.ne.jp/junjun777/20150305/x64_scriptcontrol

スクリプトと言えば

ブラウザ、それも通常は存在するIEでしょう、やはり。
IEならCOMオブジェクトで生成できます。
Powershellで書くなら

$js = New-Object PSObject
$js | Add-Member NoteProperty "ie" (New-Object -ComObject InternetExplorer.Application)
$js.ie.Navigate("about:blank")
$js.ie.Document.IHTMLDocument2_write(@"
<html><head>
<meta http-equiv='X-UA-Compatible' content='IE=edge' />
<title>ie for powershell</title>
<script>
document.__js = {
	eval: function(code, p) {
		var ret = null
		try {
			ret = eval(code)
		} catch(e) {
			this.lastErrNo = e.number
			this.lastErrMsg = e.description
			throw e
		}
		return ret;
	},
	get: function(obj, name) { return obj[name] },
	set: function(obj, name, val) { obj[name] = val },
	getAttrs: function(obj) {
		var ret = ''
		for(var a in obj) ret += ',' + a
		return ret.substr(1)
	},
	parse: function(str) { return JSON.parse(str) },
	stringify: function(obj, ind) { return JSON.stringify(obj, null, ind) }
}
</script>
</head><body></body></html>
"@)
#$js.ie.Visible = $true
$js | Add-Member NoteProperty "e" ([System.__ComObject].InvokeMember("__js", [System.Reflection.BindingFlags]::GetProperty, $null, $js.ie.Document, $null))
$js | Add-Member ScriptMethod "Eval" {
	if($args.length -gt 0) {
		$para = $null
		if($args.length -gt 1) { $para = $args[1] }
		$this.e.eval($args[0], $para)
	}
}
$js | Add-Member ScriptMethod "Get" {
	$ret = $null
	if($args.length -gt 1) {
		$ns = $args[1]
		if($ns -isnot [Array]) { $ns = $ns -split '\.' }
		$len = $ns.Length
		$ret = $this.e.get($args[0], $ns[0])
		if($args.length -gt 2 -and $args[2] -and $ret -eq $null) {
			$ret = $this.e.eval("({})", $null)
			$this.e.set($args[0], $ns[0], $ret)
		}
		if($len -gt 1) { $ret = $this.Get($ret, $ns[1..($len-1)]) }
	}
	return $ret
}
$js | Add-Member ScriptMethod "Set" {
	if($args.length -gt 2) {
		$ns = $args[1]
		if($ns -isnot [Array]) { $ns = $ns -split '\.' }
		$len = $ns.Length
		if($len -gt 1) {
			$add = $true
			if($args.length -gt 3) { $add = $args[3] }
			$this.e.set($this.Get($args[0], $ns[0..($len-2)], $add), $ns[($len-1)], $args[2])
		} else {
			$this.e.set($args[0], $args[1], $args[2])
		}
	}
}
$js | Add-Member ScriptMethod "List" {
	$ret = @()
	if($args.length -gt 0) {
		$tar = $args[0]
		if($args.length -gt 1) { $tar = $this.Get($tar, $args[1]) }
		$as = $this.e.getAttrs($tar) -split ","
		foreach($a in $as) {
			$val = $this.Get($tar, $a)
			#if($val -is [__ComObject]) { $val = $this.List($val) }
			$ret += ,@($a, $val)
		}
	}
	return $ret
}
$js | Add-Member ScriptMethod "Run"{
	if($args.length -gt 1) {
		$obj = $null
		if($args.length -gt 2) { $obj = $args[2] }
		$ns = $args[1]
		if($ns -isnot [Array]) { $ns = $ns -split '\.' }
		$len = $ns.Length
		if($len -gt 1) {
			([System.__ComObject].InvokeMember($ns[($len-1)], [System.Reflection.BindingFlags]::InvokeMethod, $null, $this.Get($args[0], $ns[0..($len-2)]), $obj))
		} else {
			[System.__ComObject].InvokeMember($args[1], [System.Reflection.BindingFlags]::InvokeMethod, $null, $args[0], $obj)
		}
	}
}
$js | Add-Member ScriptMethod "Dispose" {
	if($this.ie -ne $null) {
		$this.ie.Quit()
		$this.ie = $null
	}
}

使い方

スクリプトを実行すると、$jsオブジェクトができます。
$jsオブジェクトには、二つのプロパティと、6つのScriptMethodがあります。

$js.ie

InternetExplorer.Application COMオブジェクトです。
Scriptの実行環境になります。

$js.e

ie.document.__jsを呼び出すのが面倒なので、エントリーポイントを確保しています。
$js.e自体は、eval/get/set/getAttrs/parse/stringifyという6つの関数を持っており、対応するScriptMethodに機能を提供しています。
IEも8以降なら、ネイティブJSONサポートらしいので、parse/stringify追加しています。(ポイントはmetaタグ)
stringifyの第二引数はインデント。
ちなみに、UWSCでScriptControl+json2.jsとIE8のparse/stringifyを繰り返したら、json2の勝ちだったという、、、。
誰が遅いかは未調査。

$js.Eval($code, $p)

$codeを$js.e.eval()に渡すだけです。
スクリプト実行環境にて、単純にevalされます。


引数を渡したい場合、$js.eに設定すればthisで参照できますが、面倒なので第二引数をpで参照できるようにしています。

$js.Eval("p * 8", 2)

は16を返してきます。
オブジェクトも渡せます。(JScriptオブジェクトなら、可能なことに気づきました)


エラーが発生すると、例外をそのままスローしますが、詳細はGetして確認してください。

$js.Get($js.e, "lastErrMsg")

エラー番号が欲しいなら、「$js.Get($js.e, "lastErrNo")」。

$js.Get($obj, $name)

対象オブジェクトのメンバー値を取得します。
オブジェクトも取得可能です。
$obj.a.bの値は、$js.Get($obj, "a.b")で取得できます。

$js.Set($obj, $name, $val)

対象オブジェクトの指定メンバーに値を設定します。
オブジェクトも設定可能です。
$obj.a.b=値は、$js.Set($obj, "a.b", 値)で設定できます。
この際、$obj.aが存在しなければ、オブジェクトとして追加したいのですが、、、上手く機能してません。

$js.List($obj)

オブジェクトの内容を二次元配列(のようなもの)で返します。
メンバー名と値のタプルイメージ。
$js.e.stringifyが使えるなら、不便なだけに思える、、、。

$js.Run($obj, $name, $paras)

オブジェクトのメソッドを実行します。
第三引数は与えるパラメーター。省略可能です。
基本的には必要のないメソッドかと思っていますが、$objのメソッドが直接呼べない時利用してください。

$js.Dispose()

実行環境を破棄します。
$js.e.parseや$js.Evalでオブジェクトを作成していた場合、使えなくなります。
(実体は実行環境にあるのですから)

IEに依存したくない場合

JScript.NetとInvokeMemberを使う。
ただしこの方法で、JScriptオブジェクトのメンバーを列挙する方法を見つけてない。
メンバーがわかっていれば扱える、というもの。

$source = @'
import System;
public class JSEnv {
	public var _ = {};
	public function eval(code, p) {
		return eval(code);
	}
}
'@
$jsn = New-Object PSObject
$jsn | Add-Member NoteProperty "c" ((Add-Type -Language JScript -TypeDefinition $source -PassThru)[1].GetConstructors()[0])
$jsn | Add-Member NoteProperty "e" ($jsn.c.Invoke($null))
$jsn | Add-Member ScriptMethod "Eval" {
	if($args.length -gt 0) {
		$para = $null
		if($args.length -gt 1) { $para = $args[1] }
		$this.e.eval($args[0], $para)
	}
}
$jsn | Add-Member ScriptMethod "Get" {
	$ret = $null
	if($args.length -gt 1) {
		$ns = $args[1]
		if($ns -isnot [Array]) { $ns = $ns -split '\.' }
		$len = $ns.Length
		$ret = ([System.__ComObject].InvokeMember($ns[0], [System.Reflection.BindingFlags]::GetProperty, $null, $args[0], $null))
		if($args.length -gt 2 -and $args[2] -and $ret -eq $null) {
			$ret = $this.e.eval("p." + $ns[0] + "={}", $args[0])
		}
		if($len -gt 1) { $ret = $this.Get($ret, $ns[1..($len-1)]) }
	}
	return $ret
}
$jsn | Add-Member ScriptMethod "Set" {
	if($args.length -gt 2) {
		$ns = $args[1]
		if($ns -isnot [Array]) { $ns = $ns -split '\.' }
		$len = $ns.Length
		if($len -gt 1) {
			$add = $true
			if($args.length -gt 3) { $add = $args[3] }
			([System.__ComObject].InvokeMember($ns[($len-1)], [System.Reflection.BindingFlags]::SetProperty, $null, $this.Get($args[0], $ns[0..($len-2)], $add), $args[2]))
		} else {
			([System.__ComObject].InvokeMember($args[1], [System.Reflection.BindingFlags]::SetProperty, $null, $args[0], $args[2]))
		}
	}
}
$jsn | Add-Member ScriptMethod "Run"{
	if($args.length -gt 1) {
		$obj = $null
		if($args.length -gt 2) { $obj = $args[2] }
		$ns = $args[1]
		if($ns -isnot [Array]) { $ns = $ns -split '\.' }
		$len = $ns.Length
		if($len -gt 1) {
			([System.__ComObject].InvokeMember($ns[($len-1)], [System.Reflection.BindingFlags]::InvokeMethod, $null, $this.Get($args[0], $ns[0..($len-2)]), $obj))
		} else {
			[System.__ComObject].InvokeMember($args[1], [System.Reflection.BindingFlags]::InvokeMethod, $null, $args[0], $obj)
		}
	}
}

IEを使うパターンとほぼ同等だけど、EvalはJScript.Net環境で実行される。
このため、IEWSHJScriptオブジェクトが上手く扱えない。
JSONの機能を入れたいなら、.Net 3.5以降になりますが、以下を追加すれば簡単です。

Add-Type -AssemblyName System.Web.Extensions
$jsn | Add-Member NoteProperty "o" (New-Object System.Web.Script.Serialization.JavaScriptSerializer)

$jsn.o.DeserializeObjectでパースができて、$jsn.o.Serialize()でStringify(インデントは不可)
ただし、IEWSHJScriptオブジェクトはSerializeできません。
なお、JScript.Netのオブジェクトは何ら問題なくPowershellで扱えるので、場合によってはこっちが便利。
シリアライズにはいろいろ問題がありますが)
JScriptオブジェクトを相手にしないなら、Get/Set/Runは不要。


Evalは同様に、第二引数をpとして参照できます。
$jsn.e内に持たせてしまいたい場合、「_」が自由に使えるオブジェクトとなります。

雑感

PowershellJScriptオブジェクトを上手く扱えない、と書いたので解決してみた。
UWSCのpowershell関数をスピードアップする - じゅんじゅんのきまぐれ
わかってしまえば、JScriptオブジェクトを扱うのも、そんなに大変ではないです。


でも、JScriptオブジェクトを貰うより、JSON形式で貰ってJScript.Netオブジェクトにした方が、扱いが楽ですね。
JScriptオブジェクトを貰ってしまった場合、$jsn.o.DeserializeObject($js.e.stringify($obj,0))とJSON形式経由で、JScript.Netに輸入してしまえば、楽かもしれません。