2010年2月24日水曜日

PowerShellからWin32APIを使う

つい最近の話ですが、
PowerShellからiniファイルのデータを拾いたいと思う事がありました。
Win32APIのGetPrivateProfileString関数を
使えれば早いのですがそんなことが可能なのか。。。

実は、.NET FrameworkのP/Invoke(Platform Invokeの略?)の
仕組み機能を使用することで、PowerShellからWin32APIを使用できます。
例によって、.NET FrameworkをPowerShellから呼び出します。

今回のは、.NETにそんなに詳しくない身としては少々ハードルが高く、
一から実装することは難しいですので、
今回はこちらのサイトからソースコードを拝借しました。
ありがとうございます!

単にコードを示すだけでは私自身の勉強にならないため、
MSDNのクラスライブラリ等を使い、なるべくコードの意味を調べるようにしました。
関連リンクは後ほど。。。

サンプルコード:

# PowerShellからのPlatform APIの呼び出しは、
# .NET Frameworkの提供するP/Invoke機能を介して実施することができる。
# Invoke-Win32では、P/Invokeを実行用に一時的な型を定義し、
# そこに実装したStaticなメソッドを介してPlatformAPIを実行する。

function Invoke-Win32()
{
param
( [string]$dllName, [Type]$returnType, [string]$methodName, [Object[]]$parameterInfos )

$parameterTypes = $parameterInfos | %{ $_[ 0 ] }
$parameters = $parameterInfos | %{ $_[ 1 ] }

# カレントのアプリケーションドメインに、
# P/Invokeを実行するメソッドを持つ独自の型を定義する。

$domain = [AppDomain]::CurrentDomain
$name = New-Object Reflection.AssemblyName 'PInvokeAssembly'
$assembly = $domain.DefineDynamicAssembly( $name, 'Run' )
$module = $assembly.DefineDynamicModule( 'PInvokeModule')
$type = $module.DefineType( 'PInvokeType', "Public,BeforeFieldInit" )

# P/Invokeを呼び出すためのパラメーターを準備する。
# PlatformAPIに渡すパラメーターを保持する配列

$inputParameters = @()
# PSReference型パラメーターの位置を保持する配列
$refParameters = @()

for( $counter = 0; $counter -lt $parameterTypes.Length; $counter++ )
{
# パラメーターの型が「PSReference」のものについては、関数から戻ってきた値を拾えるようにする必要がある。
if( $parameterTypes[ $counter ] -eq [Ref] )
{
# 呼び出しの際に[out]をつける必要があるため、そのパラメーターの位置を退避しておく。
# 配列の数より1つ大きな数を保持しておく必要がある。(理由は後述)

$refParameters += $counter

# PSReference型を、.NETオブジェクトの参照型に書き換える。
$parameterTypes[ $counter ] = $parameters[ $counter ].Value.GetType().MakeByRefType()

# 関数呼び出し時に使用するパラメーター一覧に値を追加する
$inputParameters += $parameters[ $counter ].Value
}
# そうでないものについては、関数呼び出し時に使用するパラメーター一覧にただ追加するのみ
else
{
$inputParameters += $parameters[ $counter ]
}
}

# アセンブリーの動的メソッドとして、PlatformAPIを定義する
$method = $type.DefineMethod( $methodName, `
'Public,HideBySig,Static,PinvokeImpl', `
$returnType, `
$parameterTypes )
# PSReference型のパラメーターは、out属性のパラメーターとする。
foreach( $refParameter in $refParameters )
{
# 0番目の要素は戻り値の情報になるため、配列の要素番号+1を指定する必要がある。
$method.DefineParameter( ( $refParameter + 1 ), "Out", $null )
}

# P/Invokeのコンストラクターを設定する
$ctor = [Runtime.InteropServices.DllImportAttribute].GetConstructor( [string] )
$attr = New-Object Reflection.Emit.CustomAttributeBuilder $ctor, $dllName
$method.SetCustomAttribute( $attr )

# 一時的な型を作成し、そのメソッドとしてPlatformAPIを実行する。
$realType = $type.CreateType()
$returnObject = $realType.InvokeMember( $methodName, `
'Public,Static,InvokeMethod', `
$null, `
$null, `
$inputParameters )

# PSReference型で受け取ったパラメーターの値を更新する。
foreach( $refParameter in $refParameters )
{
$parameterInfos[ $refParameter ][ 1 ].Value = $inputParameters[ $refParameter ]
}
# PlatformAPIの戻り値を返す
return $returnObject
}

# GetPrivateProfileStringを呼び出してみる。
$iniFilePath = 'C:\test.ini'
$returnValue = New-Object System.Text.StringBuilder 500
$parameterInfos = @( @( [string], [string]"FrontOtherService1" ), `
@( [string], [string]"Name" ), `
@( [string], [string]"" ), `
@( [System.Text.StringBuilder], [System.Text.StringBuilder]$returnValue ), `
@( [int], [int]$returnValue.Capacity ), `
@( [string], [string]$file ) )

$returnValue = Invoke-Win32 -dllName "kernel32.dll" `
-returnType ( [UInt32] ) `
-methodName "GetPrivateProfileString" `
-parameterInfos $parameterInfos
$returnValue.ToString()



他にもいろいろなWin32API関数が使えるはずです。
その際、.NET上で動くコード(マネージコード)と
Win32APIを動かすときのコード(ネイティブコード)との対応を
理解しておく必要がありますが、ツールで調べることもできます
P/Invoke Interop Assistant)。

で、今新たに知りたいと思っているのは、
PowerShellからDelegateを使用する方法です。
イベントを使ったり、マルチスレッドを実装する場合には欠かせません。
これについても、おいおい勉強していければいいかなぁ。。。

参考コマンド:


参考リンク:
Precision Computing : Get the Owner of a Process in PowerShell – P/Invoke and Ref/Out Parameters
MSDN : A Closer Look at Platform Invoke
P/Invoke.NET
CodePlex : P/Invoke Interop Assistant

関連投稿:
PowerShellから.NETのアセンブリを呼ぶ

修正履歴:

0 件のコメント: