tech.guitarrapc.cóm

Technical updates

PowerShell の Add-Type と [Reflection.Assembly]

Add-Type の方が、[reflection.assembly]::LoadWithPartialName() よりいいよ!やったね。と書こうと思ったのです。 はじめは。

大体の場合は、 Add-Type はイイ感じに動作します。が、せっかくAdd-Type を作るときに Microsoft はちょっと、それは。と思うような動作も許してしまっています。 なので、結局一概にはいえないな、と思うのでちょっと記事にしておきます。

目次

[Reflection.Assembly]

Windows は、 .Net を利用することで快適に使えます。ただ.Net は膨大な規模なのですけど、PowerShell のデフォルトでは コアになる部分しか読み込まれていません。

PowerShell で デフォルトで参照されていない.Net を利用したい。わけで、.Netアセンブリをメモリに読み込む機会も多々あります。*1

たとえば、.Net 4.5 で追加された HttpClient を利用しようとすると

[System.Net.Http.HttpClient]

参照できずエラーがでます。

Unable to find type [System.Net.Http.HttpClient]. Make sure that the assembly that contains this type is loaded.
At line:1 char:1
+ [System.Net.Http.HttpClient]
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (System.Net.Http.HttpClient:TypeName) [], RuntimeException
    + FullyQualifiedErrorId : TypeNotFound

PowerShell 1.0 では、明示的に参照するために [System.Reflection.Assembly] のStaticメソッドで読み込みをしました。

あ、System は省略可能なので、 [Reflection.Assembly] と書けます。

Load メソッド

まずは Load() メソッドでしょうか。え、使わない?ソウデスネ。Fullname を求めるので使ってられません。そんなの覚えてられるか。書きたくない!

[Reflection.Assembly]::Load("System.Net.Http, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")
LoadFrom メソッド

それなら LoadFrom() って? C# のように参照に追加してあとはさくっと名前で参照とかできないのでツラいですね。

[Reflection.Assembly]::LoadFrom("C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETCore\v4.5.1\System.Net.Http.dll")
LoadWithPartialName() メソッド

で、良く出てきているのがこれです。

[Reflection.Assembly]::LoadWithPartialName("System.Net.Http")

気分的には こっちが見慣れた フルネーム ですし、うまく動いていますね。しかし、Stringなのでタブ補完効かないしめんどくさい。

[System.Net.Http.HttpClient]
IsPublic IsSerial Name       BaseType                          
-------- -------- ----       --------                          
True     False    HttpClient System.Net.Http.HttpMessageInvoker

Add-Type

PowerShell 2.0 で、 PowerShell は改善策を打ってきました。それが、Add-Type Cmdlet です。

たとえば、先ほどの

[Reflection.Assembly]::LoadWithPartialName("System.Net.Http")

は、Add-Type でこう書けます。

Add-Type -AssemblyName System.Net.Http

これなら、タブ補完も効くしいい感じですね!やった!だいたいの場合は。

問題は、Partial Name で判別ができない場合です。

複数バージョンのアセンブリがインストールされている場合が問題

しかし、 Add-Type は、これとは別の内部テーブルを使っているようで、 [Reflection.Assembly]::LoadWithPartialName()とは違う結果がもたらされます。

もし 複数のアセンブリが異なるバージョンでインストールされている場合、 Add-Type -AssembryName は判別する方法を持ちません。で、だいたいの場合は古いバージョンが参照されて、新しいバージョンを参照しているスクリプトは失敗します。

Add-Type の失敗例

例えば SQLServer,SMO は、厄介です。

v10.0.0.0 と v11.0.0.0 などがありますが、Add-Type で最新が参照されるでしょーっと気にせずに追加しようとすると...。

Add-Type -AssemblyName Microsoft.SqlServer.SMO

PowerShellエンジンは 古いバージョンを読み込むどころか、アセンブリの読み込みに失敗します。v9.0.242.0 って、やだー。ないよそんなのれがしー。

Add-Type : Could not load file or assembly 'Microsoft.SqlServer.Smo, Version=9.0.242.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91' or one of its dependencies. The system cannot find the file specified.
At line:1 char:1
+ Add-Type -AssemblyName Microsoft.SqlServer.SMO
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [Add-Type], FileNotFoundException
    + FullyQualifiedErrorId : System.IO.FileNotFoundException,Microsoft.PowerShell.Commands.AddTypeCommand
Add-Type ではフルネームが必要になる場合がある

この場合、バージョンを的確に指定する、つまりフルネームならいけます。

例えば私の環境*2の場合は、v11.0.0.0 を指定すればokです。

Add-Type -AssemblyName "Microsoft.SqlServer.SMO, Version=11.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91"

しかし、もう1回言いますがフルネームはない。

[Reflection.Assembly]::LoadWithPartialName() なら大丈夫

Microsoft.SqlServer.Smo だけで読み込んで欲しいじゃないですか。で、これならok。

[Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.Smo")

はい、ok。

GAC    Version        Location                                                                                                                                                                                             
---    -------        --------                                                                                                                                                                                             
True   v2.0.50727     C:\Windows\assembly\GAC_MSIL\Microsoft.SqlServer.SMO\11.0.0.0__89845dcd8080cc91\Microsoft.SqlServer.SMO.dll                                                                                          

ただし、この場合は どのバージョンが読み込まれるかは保証できません、が、とりあえず読んでくれる。やった。Add-Typeどの~。

PowerShell の Type Conversion

この問題には、 PowerShell の型変換システムが絡んでいます。

[Reflection.Assembly]::LoadWithPartialName() メソッドは裏でうまくアセンブリを判別し、またPowerShellの型変換アルゴリズムを使ってくれています。素晴らしい。*3

PowerShell の10ある型変換アルゴリズムはここを見てください。

Understanding PowerShell's Type Conversion Magic

  1. Direct assignment. If your input is directly assignable, simply cast your input to that type
  2. Language-based conversion. These language-based conversions are done when the target type is void, Boolean, String, Array, Hashtable, PSReference (i.e.: [ref]), XmlDocument (i.e.: [xml]). Delegate (to support ScriptBlock to Delegate conversions), and Enum
  3. Parse conversion. If the target type defines a Parse() method that takes that input, use that
  4. Static Create conversion. If the target type defines a static ::Create() method that takes that input, use that
  5. Constructor conversion. If the target type defines a constructor that takes your input, use that
  6. Cast conversion. If the target type defines a implicit or explicit cast operator from the source type, use that. If the source type defines an implicit or explicit cast operator to the target type, use that
  7. IConvertible conversion. If the source type defines an IConvertible implementation that knows how to convert to the target type, use that
  8. IDictionary conversion. If the source type is an IDictionary (i.e.: Hashtable), try to create an instance of the destination type using its default constructor, and then use the names and values in the IDictionary to set properties on the source object
  9. PSObject property conversion. If the source type is a PSObject, try to create an instance of the destination type using its default constructor, and then use the property names and values in the PSObject to set properties on the source object. . If a name maps to a method instead of a property, invoke that method with the value as its argument
  10. TypeConverter conversion. If there is a registered TypeConverter or PSTypeConverter that can handle the conversion, do that. You can register a TypeConverter through a types.ps1xml file (see: $pshome\Types.ps1xml), or through Update-TypeData

しかし、Add-Type は この型変換を使わず、内部に変換テーブルを持っているようです。で、失敗すると。

しかも、Add-Type があるためか、 [Reflection.Assembly]::LoadWithPartialName() は、deprecated 予定とか PowerShell Team も言ってますし、ほげー。*4

結論

もし、 Add-Type でアセンブリが読める + 読まれたバージョンで問題がないなら

Add-Type -AssemblyName System.Net.Http

もし、Add-Type の内部テーブルにアセンブリがなく読み込めない、バージョンがわからないけどとりあえずどれが読まれてもいいなら、

[Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.Smo")

もし、アセンブリのバージョンを指定したい、または将来 上の2つが使えなくなった場合は、

Add-Type -AssemblyName "Microsoft.SqlServer.SMO, Version=11.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91"

がいいでしょう。

まとめ

Add-Type かわいいよ。もっと賢くなるともっと好き。

余談

あ、来月 2014年04月12日 第1回 PowerShell勉強会@大阪 でセッションをします。

Japan PowerShell User Group (JPPOSH) の第2回としてとかなんとか。前回のJPPOSH第1回は東京での開催で、今回が初の地方で大阪です。

私は、 PowerShell を使ってて本当に役に立ってる!隙間的な役割を十全に果たせているのはどこか、と聞かれると 「デプロイ」 だと思っています。そこで、今回は DSC と デプロイ に関してセッションしようかと。

valentia をはじめとして AppVeyor - AppRolla なども自作のPowerShellによるデプロイを持っています。

DSC と、これらはどう違うのか。何を目指しているのか。

まだまだ席があるようなので、ぜひよろしくお願いします。

*1:PowerShell 3.0 以上で明示的に読む必要がいくつかでなくなっており便利になっています。

*2:Windows 8.1 Pro / Visual Studio 2013

*3:他のCmdletでもPowerShellの型変換はこれに従います

*4:Assembly.LoadWithPartialName メソッド (String)