Quantcast
Channel: うらぶろぐ @urasandesu
Viewing all 46 articles
Browse latest View live

Snoop 大作戦 - Mission: in PowerShellable -

$
0
0
"なお、この記事は自動的に消滅する。"


初めましての方は初めまして!XAML Advent Calendar 2013、18 日目を担当させていただきます、杉浦と申します。

個人的には .NET(CLR)の仕組みや開発基盤のことに関心があり、Twitter ではいつもそんなことばかり呟いていますが、お仕事ではそうそう低レイヤーなことばかりできるはずもなく、実際は WPF を使ったデスクトップアプリを開発してる時間のほうが多かったりするのです。
そんなわけで、XAML Advent Calendar1 日目の ぐらばく(@Grabacr07)さんの記事を見た瞬間、これはどんぴしゃりだと。私も何か貢献できればと参加させていただいた次第です。

さて、今回私が取り上げるのは、WPF 向けの開発ツールとして有名な Snoop
ここで言う開発ツールとは、実行中のアプリのオブジェクトの状態を確認したり、デバッガではできても難しい、もしくは手間という操作をできるようにしてくれたりする補助的なツールのことを指しています。

WPF が出た当時は様々なものがあった開発ツールも、すっかり淘汰されて、今や残っているのは一握り。
逆に言うと、XAML プラットフォームでも最初のほうに出た WPF 開発はだいぶ枯れてきているのでしょう。

今さらな感じはあるのですが、改めて Snoopにスポットを当て、情報共有の場にさせていただければと思いますです。



こちらの情報を参考にさせていただきました。自分ももっと参考にされるようにならねば!(`・ω・´)
c# - Useful WPF utilities - Stack Overflow
XAML Wonderland » Blog Archive » Shazzam – WPF Pixel Shader Effect Testing Tool now available
XamlPadX 4.0 - Lester's WPF\Silverlight Blog - Site Home - MSDN Blogs
Mole | Enterprise Touch This and Karl on WPF
Mole For Visual Studio - With Editing - Visualize All Project Types - CodeProject
Kaxaml
Announcing Pistachio – “WPF Resource Visualizer” - Grant Hinkson Blog
ZAM 3D - 3D XAML 3D WPF 3ds to XAML and dxf to XAML converter Tool for Windows Vista and WinFX
XAML Exporter for Blender - Home
AB4D - Paste2Xaml application can convert clipboard and metafiles into XAML for WPF and Silverlight
.NET Reflector Add-Ins - Home
Crack.NET - Home
Bindingの状況をTraceする | 泥庭





目次

Mission1: 最初の指令
Snoop は、CodePlex からダウンロードできます。2013/12/18 現在の最新版は、2012/10/04 付けでリリースされている 2.8.0 となっています。
zip ファイルにはインストーラが同梱されていますので、それを実行します。特にデフォルト値から変更する必要はないと思います。インストール後、起動すると、こんな感じの棒状のアプリが起動するはずです。


これだけだとなんじゃらほいですので、説明のために以下のようなアプリを書いてみました(ソースコード一式はこちらに):
MainWindow.xaml

<Window x:Class="SnoopWithPowerShell.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
Title="MainWindow" Name="MainWindow1" Height="160" Width="350">
<Window.Resources>
<!-- デフォルトの枠スタイル -->
<Style TargetType="{x:Type Border}" x:Key="DefaultBorderStyleKey">
<Setter Property="Width" Value="300" />
<Setter Property="BorderThickness" Value="0.1" />
<Setter Property="BorderBrush" Value="Black" />
<Setter Property="CornerRadius" Value="5" />
<Setter Property="Padding" Value="5" />
<Setter Property="HorizontalAlignment" Value="Left" />
</Style>

<!-- int 型向けのデータテンプレート -->
<DataTemplate DataType="{x:Type sys:Int32}">
<Border Style="{StaticResource DefaultBorderStyleKey}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{Binding}" />
<Button Grid.Column="1" Content="押してね" Click="Button_Click" />
</Grid>
</Border>
</DataTemplate>

<!-- DateTime 型向けのデータテンプレート -->
<DataTemplate DataType="{x:Type sys:DateTime}">
<Border Style="{StaticResource DefaultBorderStyleKey}">
<StackPanel Orientation="Horizontal">
<TextBlock>
<Run FontWeight="Bold" Text="{Binding Year, Mode=OneWay}" /> 年も、もうすぐ終わりだよ
</TextBlock>
</StackPanel>
</Border>
</DataTemplate>

<!-- Decimal 型向けのデータテンプレート -->
<DataTemplate DataType="{x:Type sys:Decimal}">
<Border Style="{StaticResource DefaultBorderStyleKey}">
<TextBlock Text="{Binding StringFormat={}{0:C}}" />
</Border>
</DataTemplate>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>

<!-- ItemsControl と ComboBox に内容を並べてみる -->
<ItemsControl Grid.Row="0" ItemsSource="{Binding}" />
<ComboBox Grid.Row="1" Name="ComboBox1" ItemsSource="{Binding DataContxet, ElementName=MainWindow1}"
SelectionChanged="ComboBox1_SelectionChanged" />

</Grid>
</Window>

MainWindow.xaml.cs

using System;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;

namespace SnoopWithPowerShell
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext =
new ObservableCollection<object>()
{
42,
new DateTime(2013, 12, 18),
10000m
};
}

void ComboBox1_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
throw new NotImplementedException();
}

void Button_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show(((Button)sender).DataContext + "");
}
}
}

ビルドし、実行しますと、以下のような画面が立ち上がります。


先ほどの棒状のアプリの双眼鏡アイコンの右にあるターゲットマークをドラッグし、Snoop したいアプリ上でドロップすると・・・



WPF アプリケーションの描画要素、いわゆる Visual Tree が俯瞰できるようになります!




Mission2: 各項目値を奪え
感の良い方ならすでにお気づきかもしれませんが、今回調査対象にしているアプリは Binding がうまくいっていないところや、未実装の部分をわざと作ってあります。基本的な操作をおさらいしながら、実際に問題がある箇所を調査してみることにしましょう。
動かして気づくのは、ComboBox の中身が全然入っていないところです。


ComboBox には、ItemsSource にコレクションを Binding しているはずですね。Visual Tree を辿り、ItemsSource を確認してみると・・・


なにやらセルが赤くなっており、ただならぬ雰囲気を醸し出してます。まあ、実際エラーなのですが (^_^;)
この場合、大抵は右クリック - [Display Binding Errors] でエラーの内容を確認することができます。

"・・・BindingExpression path error: 'DataContxet' property not found on 'object'''MainWindow' (Name='MainWindow1')'.・・・"というわけで、DataContxet プロパティは MainWindow に存在しないよ、とのエラーメッセージががが。
よくよく見れば、DataContxet は DataContext の typo ですね!これを修正して、リビルドし、実行すると・・・

MainWindow.xaml

<ComboBox Grid.Row="1" Name="ComboBox1" ItemsSource="{Binding DataContext, ElementName=MainWindow1}"
SelectionChanged="ComboBox1_SelectionChanged" />



ComboBox にも、上に配置していた ItemsControl と同じものが表示されるようになりました!!
ちなみに Binding エラーは、PresentationTraceSourcesの TraceLevel レベルを指定することにより、デバッグ中の Visual Studio コンソールにもっと詳細な情報を出すこともできます。Snoop を使ったカジュアルな方法で確認しきれない場合は、そちらも試すと良いでしょう。





Mission3: 戦慄のスクリプター養成所
すでにここまででも、エラー箇所の直観的な把握や、実行中のプロパティ変更、DataTemplate 適用先の要素の型調査などができるようになるわけで、相当 WPF アプリ開発が捗るようになるわけですが、実は Snoop 2.7.1 → 2.8.0 のバージョンアップの際、さらにすばらしい機能の拡張が行われました。
そう、我らが Windows 標準搭載にして脅威の柔軟性を持つスクリプト言語、PowerShellの組み込みです!!!


"To get started, try using the $root and $selected variables."とありますので、とりあえずコンソールに $root と入力し、Enter キーを押下してみます。

snoop:> $root


MainVisual : SnoopWithPowerShell.MainWindow
Target : SnoopWithPowerShell.App
Parent :
Depth : 0
Children : {[001] MainWindow1 (MainWindow) 33}
IsSelected : True
IsExpanded : False
TreeBackgroundBrush : #FFF0F0F0
VisualBrush :
HasBindingError : False

 

※注※見易さのため、Snoop の PowerShell ペインとは若干見え方を変えてあります。
ここでは、実際に入力するコマンドを、snoop:> の隣に出していますが、実際は何も表示されません (>_<)
また、普通の PowerShell コンソールで実行しているスクリプトは、PS C:\> で始めるようにしています※注※


どうやら、Target プロパティに入っているものが、現在 Visual Tree 上で選択されているもののようですね。
PowerShell を使い慣れている方だと、ここでふと気づき、このコマンドを実行してみるかもしれません。結果は・・・

snoop:> pwd

Path
----
snoop:\

 
おおっ!通ります。やはりカスタムプロバイダーも実装されているようです。Visual Tree の移動も試してみましょう。

snoop:> dir


PSPath : snoop::MainWindow
PSParentPath :
PSChildName : MainWindow
・・・(略)・・・
Target : SnoopWithPowerShell.MainWindow
Parent : [000] (App) 34
・・・(略)・・・




snoop:> cd main*

snoop:> dir


PSPath : snoop::MainWindow\ResourceDictionary
PSParentPath : snoop::MainWindow
PSChildName : ResourceDictionary
・・・(略)・・・
Target : {DataTemplateKey(System.DateTime), DataTemplateKey(System.Decimal), DefaultBorderStyleKey, DataTemplateKey(System.Int32)}
Parent : [001] MainWindow1 (MainWindow) 33
・・・(略)・・・

PSPath : snoop::MainWindow\Border
PSParentPath : snoop::MainWindow
PSChildName : Border
・・・(略)・・・
Target : System.Windows.Controls.Border
Parent : [001] MainWindow1 (MainWindow) 33
・・・(略)・・・

 
ただ、さすがに全ての実装はされていない様子 (^^ゞ

snoop:> cls
"2" 個の引数を指定して "SetBufferContents" を呼び出し中に例外が発生しました: "メソッドまたは操作は実装されていません。"発生場所 行:9 文字:1
+ $Host.UI.RawUI.SetBufferContents($rect, $space)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], ParentContainsErrorRecordException
+ FullyQualifiedErrorId : NotImplementedException


snoop:> dir -r


PSPath : snoop::MainWindow\Border\AdornerDecorator
PSParentPath : snoop::MainWindow\Border
PSChildName : AdornerDecorator
・・・(略)・・・
Target : System.Windows.Documents.AdornerDecorator
Parent : [002] (Border) 32
・・・(略)・・・




cls(Clear-Host) は、キーボードの F12 キーを押下することで代用できるのですが、残念ながら dir(Get-ChildItem) -r(-Recurse) はそうも行きません。
まあ、Snoop は GitHub で運用されていますので、Pull Request を送ってみるのが一つの手かもしれませんね。





Mission4: 極秘?情報を奪回せよ
実は先ほどのカスタムプロバイダーのお話は、本流に Merge される前、こんな機能を付け足してみた、という作者さんのブログ記事で語られているお話なのですが、どうも公式には紹介されていない情報もあるような・・・。
コミットログを眺めていると、PowerShell 関係の修正の中で、度々 "Snoop.psm1"なるモジュールが変更されていることがわかります。
ん?Snoop 専用のモジュールってこと?調べてみましょう:

snoop:> gmo

ModuleType Version Name ExportedCommands
---------- ------- ---- ----------------
Script 0.0 Snoop {Find-By, Find-ByName, Find-ByType, Get-SelectedDataContext}

 
何やら Export されたメンバーが見れますね!ヘルプヘルプ・・・っと φ(..)

snoop:> help Find-By


























・・・orz。cls(Clear-Host) などと同様、完全に Host への出力がフックできているわけではないようです。そうすると・・・GitHub に上がっている本流のソースコードを読んでも良いですが、ここは実際にインストールされているものを確認するほうが賢明でしょう。

ちなみに、PowerShell のモジュールについての詳しい説明は、今年の PowerShell Advent Calendar 2 日目の記事として投稿されている、ぎたぱそ(@guitarrapc_tech)さんの記事が詳しいのでそちらも参考にされると良いと思います。

さて、Get-Module の結果を Format-List すると・・・

snoop:> gmo | fl


Name : Snoop
Path : C:\Program Files (x86)\Snoop\Scripts\Snoop.psm1
Description :
ModuleType : Script
Version : 0.0
NestedModules : {}
ExportedFunctions : {Find-By, Find-ByName, Find-ByType, Get-SelectedDataContext}
ExportedCmdlets :
ExportedVariables :
ExportedAliases :

 
なるほど。%Snoop のインストールディレクトリ%\Scripts\Snoop.psm1 に配置されているものということがわかります。普通の PowerShell コンソールで読み込んでみましょう。

PS C:\> ipmo 'C:\Program Files (x86)\Snoop\Scripts\Snoop.psm1'
PS C:\> gmo

ModuleType Version Name ExportedCommands
---------- ------- ---- ----------------
Manifest 3.1.0.0 Microsoft.PowerShell.Management {Add-Computer, Add-Content, Checkpoint-Computer, Clear-Con...
Manifest 3.1.0.0 Microsoft.PowerShell.Utility {Add-Member, Add-Type, Clear-Variable, Compare-Object...}
Script 0.0 Snoop {Find-By, Find-ByName, Find-ByType, Get-SelectedDataContext}


PS C:\> gcm -Module Snoop

CommandType Name ModuleName
----------- ---- ----------
Function Find-By Snoop
Function Find-ByName Snoop
Function Find-ByType Snoop
Function Get-SelectedDataContext Snoop

 
さあ、今度こそ!

PS C:\> help Find-By

名前
Find-By

概要
Recursively finds an element contained in the visual tree matched using a predicate.


構文
Find-By [-predicate] <ScriptBlock> [-select] [<CommonParameters>]

 
詳しい説明はありませんが、コマンドレット名と引数も合わせればなんとなく使い方がわかりますね。
Find-By は、引数に Visual Tree の各要素を取り、bool 値を返すスクリプトブロックを指定することで、条件に合致した要素を一発で取得することができます。Snoop 上で使うとこんな感じ。

snoop:> Find-By { $_.Target -match 'Items\.' }


・・・(略)・・・
Target : System.Windows.Controls.ItemsControl Items.Count:3
Parent : [005] (Grid) 28
・・・(略)・・・

・・・(略)・・・
Target : System.Windows.Controls.ComboBox Items.Count:3
Parent : [005] (Grid) 28
・・・(略)・・・

 
対象を ToString() した結果に 'Items\.'の正規表現にマッチする文字列が含まれている要素が列挙できました。お次は、Find-ByName:

PS C:\> help Find-ByName

名前
Find-ByName

概要
Recursively finds an element contained in the visual tree matched by name.


構文
Find-ByName [-name] <String> [-select] [<CommonParameters>]

 
これは Visual Tree の各要素を、その Name プロパティ値で検索するバージョンです。実行するとこんな感じに。

snoop:> Find-ByName 'ComboBox1'


・・・(略)・・・
Target : System.Windows.Controls.ComboBox Items.Count:3
Parent : [005] (Grid) 28
・・・(略)・・・

 
Find-ByType はこれの型名(GetType().Name でのマッチング)バージョンですね。Find-ByName と似たり寄ったりなので、使い方は割愛です (^_^;)

PS C:\> help Find-ByType

名前
Find-ByType

概要
Recursively finds an element contained in the visual tree matched by name.


構文
Find-ByType [-type] <String> [-select] [<CommonParameters>]

 
最後の Get-SelectedDataContext は、その名の通り、選択された要素の DataContext を取得するものです。

PS C:\> help Get-SelectedDataContext

名前
Get-SelectedDataContext

概要
Gets the currently selected tree item's data context.


構文
Get-SelectedDataContext [<CommonParameters>]

 
サンプルアプリの MainWindow に対しての実行結果はこんな感じに。

snoop:> cd snoop:\MainWindow

snoop:> Get-SelectedDataContext
42

2013年12月18日 0:00:00
10000

 






Mission5: プロファイル
ここまで来ると、自作したユーティリティや、いくつかの問題に当たる内に定型となった処理も、Snoop 起動時にいっしょに使えるようにしておきたい!となるのが人の性というものです。
Snoop にはそういう要望に応える形で、プロファイルの読み込み機能が用意されています。最後はこの機能を使ってみましょう。
なお、プロファイルの読み込みの優先順序は以下の通りとなっています:
1. %USERPROFILE% に置かれた SnoopProfile.ps1 ファイル
2. [My Documents] にある WindowsPowerShell ディレクトリに置かれた SnoopProfile.ps1 ファイル
3. mission4 で説明した Snoop.psm1 と同じ場所にある SnoopProfile.ps1 ファイル

さて、サンプルアプリが持っている問題に対処すべく、以下の関数を定義してみました:

function Get-SnoopRoutedEventHandlers {
param (
[System.Windows.UIElement]
$element,

[System.Windows.RoutedEvent]
$routedEvent
)

$eventHandlersStoreProperty = [System.Windows.UIElement].GetProperty("EventHandlersStore", ([System.Reflection.BindingFlags]'Instance, NonPublic'))
$eventHandlersStore = $eventHandlersStoreProperty.GetValue($element, $null)
$getRoutedEventHandlers = $eventHandlersStore.GetType().GetMethod("GetRoutedEventHandlers", ([System.Reflection.BindingFlags]'Instance, Public, NonPublic'))
$routedEventHandlers = $getRoutedEventHandlers.Invoke($eventHandlersStore, $routedEvent)
$routedEventHandlers
}



function Clear-SnoopRoutedEventHandlers {
param (
[System.Windows.UIElement]
$element,

[System.Windows.RoutedEvent]
$routedEvent
)

$routedEventHandlers = Get-SnoopRoutedEventHandlers $element $routedEvent
foreach ($routedEventHandler in $routedEventHandlers) {
$handler = $routedEventHandler.Handler
$element.RemoveHandler($routedEvent, $handler)
}
}

 
プロファイルを配置すると、PowerShell で表示されるメッセージがそれを読み込んだ旨のものに変わるようになります(ちなみに、再読み込みは F5 キー押下で可能です)。


Get-Command で確認すると・・・大丈夫そうですね!

snoop:> gcm *snoop*

CommandType Name ModuleName
----------- ---- ----------
Function Clear-SnoopRoutedEventHandlers
Function Get-SnoopRoutedEventHandlers

 
さて、サンプルアプリですが、ComboBox に項目の Binding ができるようになったのは良いものの、選択項目変更時のイベントハンドラが未実装だったことに気づきました。


1 つや 2 つならいったんアプリを落として、修正し、ビルドし直して再度実行すればよいのですが、他にも実行中に Snoop で値を変えたりしていて、いい感じにやり方や挙動がわかってきてあと一歩、というところだったりすると、こういう手間はモチベーションが下がったりするもの。何とかこのままちょっと動きが変えられないかなあ・・・。

そんな場面で、先ほどの関数にある Clear-SnoopRoutedEventHandlers を実行すると、邪魔なイベントハンドラを全て削除することができる、というものです。
さっそく実行してみます・・・

snoop:> (Find-ByName ComboBox1).IsSelected = 1

snoop:> Clear-SnoopRoutedEventHandlers $selected.Target ([System.Windows.Controls.ComboBox]::SelectionChangedEvent)

 
先ほどの Snoop モジュールも活用し、問題の ComboBox へ一発で辿り着いた後、イベントハンドラを全て削除してしまいます。そして、選択項目を変更すると・・・問題が発生しなくなりました!!!


ちなみに、この PowerShell 組み込みをされた作者さんの Blogには、実行中に ICommand 実装クラスを差し替えるなどして、動作を変えてしまうというサンプルが掲載されていたりします。
XAML といえども、通常は静的言語のビルドを通して初めて動きが変わるものですので、こういう動的言語な手法を見ると胸が高鳴りますね!!





終わりの終わり
駆け足になってしまいましたが、XAML Advent Calendar 2013 18 日目として、WPF 開発者向けツールとして有名な Snoop を取り上げてみました!
この記事が少しでも皆さんの XAMLer ライフに貢献できることをお祈りしております!それでは、Happy Merry XAML'mas!!!!!


PowerShell で DSL - Create the DSL for the template engine that generates automatically the C++ library code that hooks the JIT of a program written by C# -

$
0
0
"……君のような勘のいいガキは嫌いだよ"


PowerShell Advent Calendar 2013、25 日目!昨年に引き続き、今年も PowerShell Advent Calendar に参加させていただきました、杉浦と申します。
昨日は @oota_kenさんの『PesterのMock機能をもう少し詳しく│ソフトウェアテストラボ|アプリテスト|スマートフォンテスト|株式会社シフト』でした。単体テスト周りの話は、私自身、テストダブル生成フレームワークを作っていく過程で、今回題材にさせていただいているような DSL を実装するに至ったこともあり、非常にタメになるところです。お疲れ様でした!

さて、直近で 12/21 にありました 第1回 PowerShell勉強会で登壇させていただいたこともあり、今回はそのフォローアップ記事になればと。

発表に使った資料はこちらに公開してあります:

私はこのような場所でセッションを行うのは初めてでしたので、お聞き苦しい点やわかりにくい点もあったかと思いますが、いかがでしたでしょうか?
PowerShell が持つ脅威の柔軟性の、ほんの一端でも伝われば幸いに思います。

では、早速行ってみましょう!!

※今回の記事で扱うのは、PowerShell v2 の情報となります。PowerShell v3/v4 での情報は、また次の機会に・・・。
※まとめる内に、基本的な部分の解説と応用的な部分の解説とでは分けたほうが良さそうに思えてきましたので、今回は基本的な部分を。続きはまた後日執筆させていただければと思います・・・。


こちらの情報を参考にさせていただいています。もしご自分で何かしらの DSL を実装される場合は、何かと参考になるかもしれません! (゚∀゚)
PowerShellでプロトタイプベースのオブジェクト指向を記述する方法 - 趣味の無い人生は虚しい
Writing your own PowerShell Hosting App (Part 1. Introduction) PowerShell Station
JavaScrip のクラスの多重継承(の、ようなもの):みみちゃんblog - プログラムの園:So-netブログ
Rubyist Magazine - Refinementsとは何だったのか
Adding New Type Accelerators - Power Tips - PowerShell.com – PowerShell Scripts, Tips, Forums, and Resources
Detouring Win32 Function Calls in PowerShell Adam Driscoll's Blog
Generating Fakes Assemblies with PowerShell Adam Driscoll's Blog
Inside PowerShell 3 The New Parser and Compiler Adam Driscoll's Blog
http://ps2exe.codeplex.com/
c# - Programmatic equivalent of default(Type) - Stack Overflow
c# - alternative for using slow DynamicInvoke on muticast delegate - Stack Overflow
dot net figured out How to get PowerShell current runspace from C#
c# - How to prevent blank xmlns attributes in output from .NET's XmlDocument - Stack Overflow
powershell Adding the Using Statement - The Technical Adventures of Adam Weigert
オブジェクト指向 - アンサイクロペディア





目次

PSAnonym.Prototype 全体像
資料の中にありますPSAnonym.Linqは、昨年の Advent Calendar で紹介させていただいていますので、今回は PSAnonym.Prototypeのほうを。
PSAnonym.Prototype は、多重継承をサポートしたプロトタイプベースのオブジェクト指向言語です。PowerShell には通常存在しない、いわゆるクラス(というか、この場合プロトタイプですね)定義構文を導入するモジュールになります。
勉強会でやったものより、若干大き目のサンプルで、全体像を見てみることにしましょう:

# キャラを表すプロトタイプ
$キャラ =
Prototype キャラ {
# フィールド、プロパティ
Field 今日は話した $false -Hidden
Field 好感度 0 -Hidden
Field 難易度 10 -Hidden
Field 状態確認回数 10 -Hidden
Field 名前 ([string].default) -Hidden
Property 状態 {
if (0 -lt $Me.状態確認回数-- - $Me.難易度) {
'好感度: ' + $Me.好感度
}
}

# コンストラクタ
New {
if ($null -eq $Params -or 10 -lt $Params[0]) {
$難易度 = 10
} else {
$難易度 = $Params[0]
}
$Me.難易度 = $難易度
$Me.名前 = $Params[1]
}

# メソッド
Method 会う {
$Me.今日は話した = $false
$Me.会う中身()
}
AbstractMethod 会う中身

Method 話す {
if ($Me.今日は話した) {
$Me.好感度--
$Me.話す中身_好感度下げ()
} else {
$Me.今日は話した = $true
$Me.好感度++
$Me.話す中身_好感度上げ()
}
}
AbstractMethod 話す中身_好感度下げ
AbstractMethod 話す中身_好感度上げ

AbstractMethod 誘う
}

# ツンキャラを表すプロトタイプ
$ツンキャラ =
$キャラ |
Prototype ツンキャラ |
OverrideMethod 会う中身 { 'な、なによ' } |
OverrideMethod 話す中身_好感度下げ { 'バカ、しつこい' } |
OverrideMethod 話す中身_好感度上げ { 'べ、別に' } |
OverrideMethod 誘う { $Me.好感度--; 'バカ、なに考えてるのよ' }

# デレキャラを表すプロトタイプ
$デレキャラ =
$キャラ |
Prototype デレキャラ |
OverrideMethod 会う中身 { '....おはよう' } |
OverrideMethod 話す中身_好感度下げ { 'しつこいわよ' } |
OverrideMethod 話す中身_好感度上げ { 'そうね' } |
OverrideMethod 誘う { $Me.好感度++; 'ありがとうっ、楽しみね' }

# ツンデレキャラを表すプロトタイプ
$ツンデレキャラ =
$ツンキャラ, $デレキャラ |
Prototype ツンデレキャラ {
OverrideMethod 会う中身 {
if ($Me.好感度 % 3 -eq 0) {
$ツンキャラ.会う中身()
} else {
$デレキャラ.会う中身()
}
}

OverrideMethod 話す中身_好感度下げ {
if ($Me.好感度 % 3 -eq 0) {
$ツンキャラ.話す中身_好感度下げ()
} else {
$デレキャラ.話す中身_好感度下げ()
}
}

OverrideMethod 話す中身_好感度上げ {
if ($Me.好感度 % 3 -eq 0) {
$ツンキャラ.話す中身_好感度上げ()
} else {
$デレキャラ.話す中身_好感度上げ()
}
}

OverrideMethod 誘う {
if ($Me.好感度 % 3 -eq 0) {
$ツンキャラ.誘う()
} else {
$デレキャラ.誘う()
}
}
} -Force

$舞 = $ツンデレキャラ.New((2, '舞'))

'1 日目: {0} ---' -f $舞.名前
$舞.会う()
$舞.話す()
$舞.話す()
$舞.誘う()

'2 日目: {0} ---' -f $舞.名前
$舞.会う()
$舞.話す()
$舞.誘う()

'3 日目: {0} ---' -f $舞.名前
$舞.会う()
$舞.話す()
$舞.誘う()
$舞.誘う()

# 結果------------------------
# 1 日目: 舞 ---
# な、なによ
# そうね
# バカ、しつこい
# バカ、なに考えてるのよ
# 2 日目: 舞 ---
# ....おはよう
# べ、別に
# バカ、なに考えてるのよ
# 3 日目: 舞 ---
# ....おはよう
# べ、別に
# バカ、なに考えてるのよ
# ありがとうっ、楽しみね
私の Blog には、ちょっと似つかわしくない題材なのですが、大き目のサンプルで複数の生き物を合成するのは、どこかの錬金術師的な流れになる危険がありましたので。。。(^_^;)
もう少し柔らかめな表現しやすいものとして、定番のツンデレキャラを作ってみました。罵倒されても、挫けずに誘い続けることが大切です!・・・みたいな教訓めいたものは何もなく、適当に組んだらそれっぽい結果になったので驚いています。これがクリスマスの魔法でしょうか。

さて、コマンドと宣言構文で、何をしたいかは大体想像がついてしまうかもしれませんが、次節から、各項目の簡単な説明をさせていただければと思います。





Prototype(New-Prototype) 関数
プロトタイプは、そのまんまではありますが Prototype という宣言文で始まります。これは、New-Prototype 関数のエイリアスになっています。コマンドのシグニチャはこんな感じ(※共通のスイッチは…で省略しています):

PS C:\> New-Prototype -?
New-Prototype [-Name] <String> [[-Declaration] <ScriptBlock>] [-InputObject <PSObject[]>] [-Force] …
 
[-Name] はプロトタイプの名前で、多重継承の時などに、親プロトタイプを明示的に指定する時にも使います。必須の項目はこれだけですので、空のプロトタイプを作り、後からメソッドやプロパティを足すことも可能です。
次の [-Declaration] は、スクリプトブロックを用いた構文をサポートするために使うものです。上記のサンプルですと、$キャラ や $ツンデレキャラ を定義するのに、この構文を使っていますね。逆に、$ツンキャラ、$デレキャラ の宣言はパイプラインを使って定義されていることも見て取れると思います。

このようなスクリプトブロックを用いた汎用言語に近い構文と、パイプラインを使った構文の両方をサポートするのは、若干手間ではあるのですが、個人的には外せなかった要件でした。スクリプトブロックを用いたほうですと、処理がある程度の大きさになった場合にコメントを挟むなどして説明が入れやすくなるという利点があります。パイプラインでも、途中経過を変数に入れればできないことは無いのですが、どうしても宣言と処理がごっちゃになりやすい・・・。逆にある程度の大きさになるまでは、パイプラインで繋ぐほうが簡潔で読みやすく、また今回の勉強会でやったようなデモをやる場合もお手軽にできるという利点があります。
どちらも一長一短があり、状況に応じて使い分けたかったので、ハイブリッド構文を採用するに至った次第です。DSL を自分で実装する場合、この辺りのさじ加減を自分で決められるのは良い点ですね!

はい、説明に戻ります。最後の [-Force] スイッチは、重複するメンバーが存在する場合に、無理やり上書きするかどうかを指定するフラグです。私が思うに、本来、多重継承がうまく働くのは、状態を持ち、かつ重複しない概念を混ぜ合わせて、新たな概念を作りたくなった場合のはずですので、このフラグが必要になるようなものは、本当にキレイな設計では無い可能性があります。注意して見直しが必要でしょう。
※ただ、実際はそうそううまく分かれることもなかったり・・・。目的であるテンプレートエンジンへの適用という意味では、実益はあったので良いのですが・・・。多重継承の是非みたいな話は、個人的にはこの辺りを辿ると、わからないなりに納得できたように思いましたが、まだ勉強の必要がありそうです。。。





Field(Add-Field) 関数
プロトタイプには Field という宣言文でフィールドの定義が可能です。これも実際は Add-Field 関数のエイリアスですね。コマンドのシグニチャはこんな感じ:

PS C:\> Add-Field -?
Add-Field [-Name] <String> [-Value] <Object> [-InputObject <PSObject>] [-Hidden] …
 
[-Name] はフィールド名です。ここに付けた名前に対し、同じプロトタイプのメンバー(コンストラクタ、メソッドやプロパティ)からであれば、$Me.フィールド名 でアクセスが可能になります。
[-Value] で初期値を指定します。初期値は必須で、かつ最初に与えられた値で型が決まるようになっています。こういう書き方は、はじくようになっているということですね:

PS C:\> $a = Prototype A | Field Value 10
PS C:\> $a.Value = 'aiueo'
. : Cannot convert value "aiueo" to type "System.Int32". Error: "Input string was not in a correct format."

At line:1 char:4
+ $a. <<<< Value = 'aiueo'
+ CategoryInfo : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : PropertyAssignmentException

 
自分が静的言語に関わっている時間が長いこともあり、ちょっとした型注釈は、できればシステム側で見守っていてほしい派だったりします。
このため、フィールドには、型の決定が行えるよう、$null は直接指定できないようにしています。初期値として $null を指定したい場合は、上記の例のように($キャラ を定義している 9 行目)、([型名].default) で初期値を指定するようにします。

後は、[-Hidden] スイッチを付けることで、Get-Member の結果に現れなくなります。そのプロトタイプ内部で使うだけの属性であれば、このスイッチで隠してしまったほうが扱いやすくなると思います。
・・・おっと、[-InputObject <PSObject>] を忘れていました。それもそのはずで、これは通常、意識する必要はありません。パイプラインを使った構文でプロトタイプを宣言する場合に自動的に利用されるもので、プロトタイプ以外のものを指定すると、エラーになるようになっています。





Property(Add-Property) 関数
プロパティの宣言文は Property です。例によって、Add-Property 関数のエイリアスになっています。コマンドのシグニチャを見てみましょう:

PS C:\> Add-Property -?
Add-Property [-Name] <String> [[-Getter] <ScriptBlock>] [[-Setter] <ScriptBlock>] [-InputObject <PSObject>] [-Hidden] …
 
Getter と Setter で、それぞれスクリプトブロックを指定する必要がありますので、若干大き目ですが、内容に特筆すべきところは無いですね。
[-Name] にプロパティ名、[[-Getter] <ScriptBlock>] に取得処理のスクリプトブロック、[[-Setter] <ScriptBlock>] に設定処理のスクリプトブロック、となっています。あ・・・、今見ると [-Hidden] スイッチは、Override が絡むとうまく動いていないようですね・・・直さねば (-_-;)

ところで、標準の Add-Member ScriptProperty もそうなのですが、PowerShell のプロパティは、基本的に例外を握りつぶす仕様になっているようです:

PS C:\> $a = New-Object psobject | Add-Member ScriptProperty Value { throw New-Object NotImplementedException } -PassThru
PS C:\> $a.Value

# ここでは何も出ない
PS C:\> $a.psobject.properties.match('value')[0].getterscript.invoke()
invoke : Exception calling "Invoke" with "1" argument(s): "The method or operation is not implemented."

At line:1 char:61
+ $a.psobject.properties.match('value')[0].getterscript.invoke <<<< ()
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : DotNetMethodException

# Getter 部分を直接メソッドとして呼び出すと例外が発生していることがわかる
 
PSAnonym.Prototype でも、内部的には ScriptProperty を使っていますので、この動きに習う形になります。ただ、プロパティ内でエラーが発生していることに気づかないと、スクリプト実行後の $Error.Count がどえらいことになっていたりするので、時々調べてみるのをオススメしますです ヽ(;▽;)ノ





New(Set-New) 関数
コンストラクタの宣言文です。これも Set-New 関数のエイリアスとして、New が指定してあります。コマンドのシグニチャは一番単純ですね:

PS C:\> Set-New -?
Set-New [-Body] <ScriptBlock> [-InputObject <PSObject>] …
 
[-Body] でコンストラクタの処理を指定するだけです。構文自体も簡単ですが、その呼び出され方も極力簡略化されたものになっています。
サンプルで $キャラ に定義されたコンストラクタ(17 行目)ですが、その派生プロトタイプのいずれも、明示的に呼んではいないことに気づかれたでしょうか?
標準的な PowerShell の引数の引き回し方($Args 配列によるもの)に習い、PSAnonym.Prototype でも $Params という配列に一連の引数を詰めて引き回します。派生プロトタイプが途中で引数を加工しない限り、この引数はそのまま基底プロトタイプまで引き渡されますので、このような簡略化が可能になっているというわけですね。
ちなみに、自分自身を表す変数が Me だったり、コンストラクタが New だったりするのは、Visual Basic みたいだなと思われた方もいらっしゃるかもしれませんが、間違いなく VB の影響だということをここで告白しておきます。タイプ数が少なく、他と被りにくいキーワードとして優秀なのですよー! ゚ .(・∀・)゚ .゚





Method(Add-Method) 関数
基本的な構文の最後はメソッドの宣言です。Add-Method 関数のエイリアスとして Method が定義してあります。コマンドのシグニチャはこの通り:

PS C:\> Add-Method -?
Add-Method [-Name] <String> [-Body] <ScriptBlock> [-InputObject <PSObject>] [-Hidden] …
 
[-Body] <ScriptBlock> で例外が発生した場合、ちゃんと外側までスローされるのがプロパティとの大きな違いです。
それ以外は特に無いのですが、中心になる機能ということもあり、中身は色々やっています。パフォーマンス向上の工夫、多重継承の実現方法やモジュール性の確保の方法など・・・後日執筆予定の応用編では、それらに触れられればと思いますですね・・・(>_<)。





続く...?
PowerShell Advent Calendar 2013 の 25 日目を担当させていただきましたが・・・うーん、すみません、まとめきれませんでした・・・orz。
この PowerShell の、使えば使うほどできることが広がっていく感じは、実用性だけでなく、単純にプログラミング自体を楽しめるということにも繋がるのでは、なんて思っていたりするのですが、まだまだその域には達していない感じです。精進せねば!
また機会があれば、PowerShell が持つ力を広めるお手伝いができればと思います。それでは、良いお年を!!



続・PowerShell で DSL - The Sequel: Create the DSL for the template engine that generates automatically the C++ library code that hooks the JIT of a program written by C# -

$
0
0
"そうですな。休みを・・・・・・暇を頂きましょう"


この記事は、前回の第 1 回 PowerShell 勉強会のフォローアップ記事の続きとなります。

本来であれば 1 つの記事にできると良かったのですが、解説向けのコンパクトなサンプルを切り出すのが結構難しく、ちょっと時間が空いてしまいました。また、結構なボリュームになりそうだったため、結局、全 3 部作になる勢いです ((((;゚Д゚))))

今回の記事では、解説しきれなかった残りの構文とともに、ハイブリッドな構文の実現方法や、モジュール性確保の時の注意点などを情報共有できればと思います。パフォーマンス向上の工夫や、テンプレートエンジンの解説は次回ですかね。なお、プレゼン資料の中で紹介させていただいたテンプレートエンジン、Swathe.Automation の公開がまだ先になりそうということで、規模は小さいのですが、同じ考え方でテンプレートエンジンとその設定 DSL を改めて実装し、それを解説しようかと考えています。

それでは、最後までどうぞお付き合いいただければと思います!

※今回の記事で扱うのも、PowerShell v2 の情報となります。PowerShell v3/v4 での情報は、いつか必ず・・・。


こちらの情報も参考に。改めて見直すと、便利なものを見落としていたりするものです・・・...( = =)
PowerShell Advent Calendar 2013 ATND
第1回 PowerShell勉強会 ATND
第1回 PowerShell勉強会 #jpposh ツイートまとめ - Togetterまとめ
Dean Edwards: A Base Class for JavaScript Inheritance
Dean Edwards: Prototype and Base
抽象(Abstract)クラスを作成 - JavaScriptist
Prototypal Inheritance Using PowerShell - Code Pyre
Prototypal Inheritance Using PowerShell (Part 2) ScriptProperties - Code Pyre
Prototypal Inheritance Using PowerShell (Part 3) Mixins - Code Pyre





目次

残りの構文(Abstract~、Override~)
まずは前回ツンデレキャラサンプルを参照しつつ、解説していない構文をざっと見ていきたいと思います。残っていたのはこれだけ、のはず?:
AbstractProperty(Add-AbstractProperty) 関数
AbstractMethod(Add-AbstractMethod) 関数
OverrideProperty(Add-OverrideProperty) 関数
OverrideMethod(Add-OverrideMethod) 関数
さあ、順に見ていきましょう!


AbstractProperty(Add-AbstractProperty) 関数
抽象プロパティを定義するには、AbstractProperty を使います。Add-AbstractProperty 関数のエイリアスですね。

ちなみに、前回解説した構文だけでなく、私が作成し、公開している PowerShell のモジュール全てにおいて、実際の関数とエイリアスをそれぞれ定義するという形を取っています。理由は、PowerShell のモジュールを Import-Module した時に出る警告"WARNING: Some imported command names include unapproved verbs which might make them less discoverable. Use the Verbose parameter for more detail or type Get-Verb to see the list of approved verbs."を回避しつつ、文脈にあった命名(PSAnonym.Prototype であれば、型宣言らしい見た目)をできるようにするためです。

・・・ただこの方法、確かに要件は満たせているのですが、よく考えなくても管理が煩雑になることは目に見えています。他に良い方法があれば、是非共有させていただきたいところですね (^_^;)

では、コマンドのシグニチャを確認しておきましょう:

PS C:\> Add-AbstractProperty -?
Add-AbstractProperty [-Name] <String> [-Getter] [-Setter] [-InputObject <PSObject>] [-Hidden] …
 
抽象プロパティということで、通常のプロパティを定義する Property(Add-Property) 関数と比べると、Getter と Setter が、スクリプトブロックを指定しない、単なるコマンドスイッチになっていることが分かります。ツンデレキャラサンプルでは登場させる隙が無かったので、後ほど OverrideProperty(Add-OverrideProperty) 関数の解説の中で書き方を紹介できればと。
後のスイッチは変わり映えしませんね、どんどん行きましょう!


AbstractMethod(Add-AbstractMethod) 関数
抽象メソッドです。ツンデレキャラサンプルでも、AbstractMethod を使って、$キャラ 基底プロトタイプが持っている 誘う メソッドを抽象メソッドとして定義し、その処理は、派生プロトタイプである $ツンキャラ / $デレキャラ / $ツンデレキャラ で実装していることが見て取れるかと思います。
実際は Add-AbstractMethod 関数のエイリアス、ということでシグニチャを確認してみます:

PS C:\> Add-AbstractMethod -?
Add-AbstractMethod [-Name] <String> [-InputObject <PSObject>] [-Hidden] …
 
抽象プロパティと同じく、通常のメソッドを定義する Method(Add-Method) 関数と比べると、スクリプトブロックを指定していなくなっていますので、非常にシンプルなものとなっています。

抽象メンバの制御構文が出揃ったところで、これらが持っている特別な機能を 2 つほど解説したいと思います。
まず 1 つ目として、抽象メンバを持つプロトタイプは、抽象プロトタイプになり、それ以降新しいコピーを作ることが出来なくなります。例を見てみましょう:

※「抽象プロトタイプ」という言葉ですが、同じプロトタイプベースなオブジェクト指向言語である JavaScript でこういったことを表現する場合、該当するオブジェクトを抽象クラスと呼ぶようですね。自分はクラスベースなオブジェクト指向言語とごっちゃになりそう・・・と思いましたので、抽象プロトタイプという言葉を選んでいます。どちらが適切かどうかよくわかりませんが、雰囲気は伝わるかと (^_^;)。

PS C:\> $animal = Prototype Animal | AbstractMethod Cry
PS C:\> $animal1 = $animal.New()
New : Exception calling "New" with "0" argument(s): "This object is not constructible yet
because it has some abstract members 'Cry'."
At line:1 char:23
+ $animal1 = $animal.New <<<< ()
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : ScriptMethodRuntimeException
 
こんな感じで、コピーを作ろうとすると、エラーが出て作成できないようになっています。

次に、抽象メンバは、派生プロトタイプで明示的に上書きしようと思って上書きしなければダメようにしています。例えば、メソッドの場合だと、派生プロトタイプでは、Method(Add-Method) 関数ではなく、後述の OverrideMethod(Add-OverrideMethod) 関数を使って明示的にメンバを上書きする必要があります:

PS C:\> $animal = Prototype Animal | AbstractMethod Cry
PS C:\> $dog = $animal | Prototype Dog | Method Cry { 'わんわん' }
throw : The member 'Cry' has already existed in the designated object. If you want to override it, you could use
'Add-OverrideMethod' instead of 'Add-Method'.
パラメータ名: $Name
At C:\Users\User\Documents\WindowsPowerShell\Modules\Urasandesu.PSAnonym\Urasandesu\PSAnonym\Prototype.psm1:167 char:1
4
+ throw <<<< New-Object ArgumentException $message, '$Name'
+ CategoryInfo : OperationStopped: (:) [], ArgumentException
+ FullyQualifiedErrorId : The member 'Cry' has already existed in the designated object. If you want to override it,
you could use 'Add-OverrideMethod' instead of 'Add-Method'.
パラメータ名: $Name
 
これは、C# で、たまたま基底クラスと同じ名前のメンバを定義した場合に、"The keyword new is required on 'MyDerivedC.x' because it hides inherited member 'MyBaseC.x'."のような感じで警告が出るのと同じですね。
この辺りの厳密さは、動的型付け言語とすればやり過ぎなのかもしれませんが、前回記事で解説させていただいた Field(Add-Field) 関数と同様、個人的には、できればシステム側で見守っていてほしい派だったりするので、この形に落ち着いています。

お次は、これらの明示的上書きメンバですね。


OverrideProperty(Add-OverrideProperty) 関数
OverrideProperty を使って、抽象プロパティの実装を記述することができます。これももちろん、関数 Add-OverrideProperty のエイリアス。シグネチャを見てみましょう:

PS C:\> Add-OverrideProperty -?
Add-OverrideProperty [-Name] <String> [[-Getter] <ScriptBlock>] [[-Setter] <ScriptBlock>] [-InputObject <PSObject>] [-Hidden] …
 
先ほど説明した抽象メンバを上書きできる専用の構文である、ということを除けば、普通の Property(Add-Property) 関数と機能は同じです。ツンデレキャラサンプルでは登場させられませんでしたので、別のサンプルで使い方を見てみたいと思います:

PS C:\> $animal = Prototype Animal |
>> AbstractProperty FingersPerLimb -Getter |
>> AbstractProperty Limbs -Getter |
>> Property AllFingers { $Me.FingersPerLimb * $Me.Limbs }
>>
PS C:\> $snake = $animal |
>> Prototype Snake |
>> OverrideProperty FingersPerLimb { 0 } |
>> OverrideProperty Limbs { 0 }
>>
PS C:\> $horse = $animal |
>> Prototype Horse |
>> OverrideProperty FingersPerLimb { 1 } |
>> OverrideProperty Limbs { 4 }
>>
PS C:\> $chimera = $snake, $horse |
>> Prototype Chimera -Force |
>> OverrideProperty FingersPerLimb { $Snake.FingersPerLimb + $Horse.FingersPerLimb } |
>> OverrideProperty Limbs { $Snake.Limbs + $Horse.Limbs }
>>
PS C:\> $snake, $horse, $chimera | % { $_.AllFingers }
0
4
4
 
・・・はい、良い例が浮かばず、いつものキメラ錬成(干支風味)をしてみました (^^ゞ

最初に $animal: 動物 抽象プロトタイプを準備します。持っているプロパティは、FingersPerLimb: 肢毎の指の数、Limbs: 肢の数、AllFingers: 全ての指の数、となっています。
AllFingers は、FingersPerLimb * Limbs で求められるので、派生プロトタイプではこれらの情報を実装してもらうようにしました。$snake: 巳 派生プロトタイプは指も肢もありませんので、両方 0 です。$horse: 午 派生プロトタイプのほうは、指は各肢に 1 本ずつ、肢が 4 本となっています。$chimera: キメラ はこれらの合成ですね。計算結果はもうお分かりになるかと思いますが、それぞれ、0 本/4 本/4 本 という結果になります。

なお、ここにもちょっとしたチェック機構を設けています。
この例のように、抽象プロパティを宣言する時に Getter しか準備をしなかった場合、派生プロトタイプで Setter を実装しようとすると、こんな感じでエラーを発生させるようにしました:

PS C:\> $slime = $animal |
>> Prototype Slime |
>> Field m_rand (New-Object Random) -Hidden |
>> Method Reset { $Me.m_rand = New-Object Random } -Hidden |
>> OverrideProperty FingersPerLimb { $Me.m_rand.Next(1, 10) } { $Me.Reset() } |
>> OverrideProperty Limbs { $Me.m_rand.Next(10, 50) } { $Me.Reset() }
>>
throw : The designated member 'FingersPerLimb' can't be overridden because it isn't settable."
パラメータ名: $Name
At C:\Users\User\Documents\WindowsPowerShell\Modules\Urasandesu.PSAnonym\Urasandesu\PSAnonym\Prototype.psm1:238 char:1
4
+ throw <<<< New-Object ArgumentException ($AssertionMessages.IsSettable -f $Name), '$Name'
+ CategoryInfo : OperationStopped: (:) [], ArgumentException
+ FullyQualifiedErrorId : The designated member 'FingersPerLimb' can't be overridden because it isn't settable."
パラメータ名: $Name
 
$slime: スライム 派生プロトタイプです。スライムは不定形ですので、指の数や肢の数は乱数で決めるようにしています。

ある時(例えば、派生プロトタイプが増えてきたせいで当初の設計を忘れかけた頃)、各プロパティに値がセットされたタイミングで、乱数をリセットしたほうが良いかな?・・・とやってみましたが、それはできないと怒られ、ああそうだったと気づいた、みたいな。このように、ある程度インターフェースに制限を掛けることで、プログラムの規模が大きくなっても、問題が起きにくくなるのではと思っています。

次で未解説構文は最後ですね。


OverrideMethod(Add-OverrideMethod) 関数
抽象メソッドの実装の記述には、OverrideMethod を使います。例に漏れず、Add-OverrideMethod 関数のエイリアスになっています。シグネチャは以下の通り:

PS C:\> Add-OverrideMethod -?
Add-OverrideMethod [-Name] <String> [-Body] <ScriptBlock> [-InputObject <PSObject>] [-Hidden] …
 
OverrideProperty(Add-OverrideProperty) 関数と同様、抽象メンバを上書きするための専用の構文である、ということを除けば、通常の Method(Add-Method) 関数と全く同じものになっています。ツンデレキャラサンプルでも多用しているところですので、特に解説する必要はなさそうですね (`・ω・´)

以上で、残りの構文は全て説明できました。
・・・今思えば、コマンドのシグネチャもちゃんとヘルプを書いておけば、ここまで説明する必要なかったな、と思ったり。PowerShell は、コマンドごとのドキュメントの仕組みについても標準で持っていますので、PowerShell の良さを広めるのであれば、この辺りももっと活用しなければと反省しております・・・...( = =)

さて、次の項からは、PSAnonym.Prototype を実装する上で使っている実現方法や注意点などを解説したいと思います。





ハイブリッドな構文の実現方法
前回さらっと触れましたが、スクリプトブロックを用いた汎用言語に近い構文(以下、スクリプトブロック構文)と、パイプラインを使った構文(以下、パイプライン構文)の両方をサポートするのは、手間ではあるのですが、個人的には外せなかった要件でした。この実現方法を簡単に説明させていただきたいと思います。プロトタイプの始まりとなる Prototype(New-Prototype) 関数とメソッドの追加処理である Method(Add-Method) 関数を見てみましょう:

Prototype(New-Prototype) 関数 ※抜粋

function New-Prototype {

[CmdletBinding()]
[OutputType([psobject])]
param (
[Parameter(ValueFromPipeline = $true)]
[psobject[]]
$InputObject,

[parameter(Mandatory = $true, Position = 0)]
[string]
$Name,

[parameter(Position = 1)]
[scriptblock]
$Declaration,

[switch]
$Force
)

begin {
$state = @{ Prototype = $null; BaseObjectIndex = -1 }
} process {
if ($null -ne $InputObject) {
foreach ($InputObject_ in $InputObject) {
& $Inherit $InputObject_ $Name $state $Force
}
}
} end {
if ($null -eq $state.Prototype) {
$state.Prototype = & $NewPrototype $Name
}

if ($null -ne $Declaration) {
$declarations = @(& $Declaration)
foreach ($Declaration_ in $declarations) {
($member, $mode) = $Declaration_
& $AddOrOverride $state.Prototype $member $mode
}
}

$state.Prototype
}
}
 
スクリプトブロック構文で使われるのは、第 3 引数の -Declaration スイッチに指定するスクリプトブロックです。この引数はオプショナルになっていますので、省略されているかどうか(=$null かどうか)で、スクリプトブロック構文かパイプライン構文を決めています。
スクリプトブロック構文の場合、345 行目~にある通り、スクリプトブロックを実行してその中身を取り出し、宣言の数分回しながら、そのメンバを追加する、もしくは上書きするかの処理へ流しています。パイプライン構文の場合は、生成したプロトタイプを、ストリームに流すだけですね。ここにはこれといった処理は記述されていません。
「じゃあ、パイプライン構文の場合は、メンバの追加/上書きを行う各関数側で制御している?」と思われた方、鋭い!正解です!!

メンバの制御は似通っていますので、代表的なものとして、Method(Add-Method) 関数の中身を見てみたいと思います:

Method(Add-Method) 関数 ※抜粋

function Add-Method {

[CmdletBinding()]
param (
[Parameter(ValueFromPipeline = $true)]
[psobject]
$InputObject,

[Parameter(Mandatory = $true, Position = 0)]
[string]
$Name,

[Parameter(Mandatory = $true, Position = 1)]
[scriptblock]
$Body,

[switch]
$Hidden
)

if ($null -ne $InputObject) {
& $AssertIsPrototype $InputObject
& $AssertMemberNonexistence $InputObject $Name ($AssertionMessages.NonexistenceHelp1 -f 'Add-OverrideMethod', 'Add-Method')
}

$CacheScriptMethod = $CacheScriptMethod
$value = { & $CacheScriptMethod $this $Name $Body $Hidden $args }.GetNewClosure()
$scriptMethod = New-Object Management.Automation.PSScriptMethod $Name, $value

if ($null -ne $InputObject) {
$InputObject.psobject.Members.Add($scriptMethod)
$InputObject
} else {
, ($scriptMethod, ([Urasandesu.PSAnonym.Prototype.AddModes]::None))
}
}
 
前回の Field(Add-Field) 関数の解説でも、ちらっと触れていますが、メンバの追加/上書きを行う各関数は、必ず InputObject をパイプラインから取るようになっています。
InputObject が存在すればパイプライン構文、無ければスクリプトブロック構文で使われていると判断しているわけです。52 行目、61 行目でそれらしい分岐がありますね。
52 行目の分岐では、流れてきた psobject があれば、PSAnonym.Prototype で扱えるプロトタイプかどうかをチェックしています。また、前述した意図しない上書きなどのチェックもここで行っています。
57 行目~ 59 行目は、メソッドの表すオブジェクトを生成しています・・・もしかすると、そろそろ、「あれ?そういえば、なんでこの人、普通に関数呼び出しせずに、いつも変数にスクリプトブロック入れて & で起動してるんだ?」と思われる方も出てくるかもしれませんが・・・。これに関しては、次項のモジュール性確保の注意点で解説させていただければと思いますので、もう少々お待ちください <(_ _)>
61 行目の分岐では、流れてきた psobject があればこのメソッドをメンバーとして追加し、無ければこのメソッドがどのようなメソッドかという付帯情報を付けてストリームに流します。ストリームに流したものは、先ほど解説した Prototype(New-Prototype) 関数の 345 行目~で拾われることになります。

実は、このようなプロトタイプとそのメンバ、に限らず、上下関係というか階層構造が決まっているものは、このような手法を取ることで、機械的にスクリプトブロック構文とパイプライン構文の両方をサポートすることができるようになります。もし何かしらの DSL を PowerShell で設計されようとしているのであれば、手法の 1 つとして、頭の片隅に置いておくと良いかもしれませんね。





モジュール性確保の注意点
PowerShell には再利用のための仕組みとして、$PROFILE への直接的な関数記載や、スクリプトファイルの呼び出し、ドットソース化、スナップイン などいくつかの方法があります。その中でも、モジュールは後発(v2 ~)であることもあり、インストールがファイルコピーベースで導入が簡単だったり、どの関数を公開してどの関数を非公開にするといったアクセシビリティの制御がサポートされていたり、セッションへのロード/アンロードが可能だったりと機能も手厚く、これから再利用可能なライブラリを構築するのであれば、モジュールを選んでおいて失敗は無いでしょう。

ただ、このモジュール、PowerShell のセッションとスコープの扱いのせいか、ひとクセある状態になっています(この辺りの話は、昨年の Advent Calendarでもまとめさせていただいています)。例えば以下の例。今の時代、プログラミング言語が扱うスコープの趨勢はレキシカルですので、以下のスクリプトの奇妙さにお気づきになられない方も多いかと思います:

PS C:\> New-Module {
>> function Greet {
>> 'あけおめ!'
>> }
>> function New-Greeter {
>> { Greet }
>> }
>> Export-ModuleMember *-*
>> } |
>> Import-Module
>>
PS C:\> $greeter = New-Greeter
PS C:\> & $greeter
あけおめ!
PS C:\> function Greet {
>> 'あれ?なにかおかしい??'
>> }
>>
PS C:\> & $greeter
あけおめ!
 
この例では、無名のインメモリモジュールを作成し、2 つの関数を定義しています。New-Greeter は、モジュール内に定義された Greet を呼び出すスクリプトブロックを返します。
モジュールからは、*-* のパターンにマッチする名前の関数しかエクスポートしていませんので、Greet はモジュール外に出てしまえば、そのセッションのスコープからは見えないはず・・・つまり関数が見つからない旨のエラーになるですよね?

そう、PowerShell は、ダイナミックスコープが基本であるにも関わらず、モジュールが絡んだ時だけ、なぜかレキシカルスコープ的な動きをする場合があるのです。

なぜこのようなちぐはぐな仕様にしたのか納得いかないのですが・・・神本、PowerShell インアクションの次版には何かしらの理由が書かれているのでしょうか・・・(初版は v1 相当の内容のため、モジュールには触れられていないのです)。

ちなみに、このようなスクリプトブロックを返す関数は、往々にして何らかの環境を保持したくなる場合も多く、そんな場合は GetNewClosure を使ってクロージャを作成することになると思うのですが、そうすると、思い出したようにダイナミックスコープ的な動きに戻ります(!?!?):

PS C:\> New-Module {
>> function Greet {
>> 'あけおめ!'
>> }
>> function New-Greeter {
>> param (
>> $Message
>> )
>> { (Greet) + $Message }.GetNewClosure()
>> }
>> Export-ModuleMember *-*
>> } |
>> Import-Module
>>
PS C:\> $greeter = New-Greeter 午年だよ!
PS C:\> & $greeter
Greet : The term 'Greet' is not recognized as the name of a cmdlet, function,
script file, or operable program. Check the spelling of the name, or if a path was
included, verify that the path is correct and try again.
At line:9 char:9
+ { (Greet <<<< ) + $Message }.GetNewClosure()
+ CategoryInfo : ObjectNotFound: (Greet:String) [], CommandNotFoundException
+ FullyQualifiedErrorId : CommandNotFoundException

PS C:\> function Greet {
>> 'あーそうだった。ダイナミックスコープならこう動くべきだよね。'
>> }
>>
PS C:\> & $greeter
あーそうだった。ダイナミックスコープならこう動くべきだよね。午年だよ!
 
ここで、前述のハイブリッドな構文の実現方法で出てきた謎の構文の理由がわかっていただけるかと思います。ダイナミックスコープであっても、あらかじめ、そのスコープ取り込んだ変数に入っているスクリプトブロックを実行するのであれば、何も問題はないはずですよね:

PS C:\> New-Module {
>> New-Variable Greet {
>> 'あけおめ!'
>> } -Option ReadOnly
>> function New-Greeter {
>> param (
>> $Message
>> )
>> $Greet = $Greet # このスコープの変数に付け替え。GetNewClosure は現在のスコープの環境を保存するため。
>> { (& $Greet) + $Message }.GetNewClosure()
>> }
>> Export-ModuleMember *-*
>> } |
>> Import-Module
>>
PS C:\> $greeter = New-Greeter 午年だよ!
PS C:\> & $greeter
あけおめ!午年だよ!
PS C:\> New-Variable Greet {
>> 'この変数は無視され、取り込まれた変数が働いてる。これなら納得!'
>> } -Option ReadOnly
>>
PS C:\> & $greeter
あけおめ!午年だよ!
 
この動きがあることから、私が作成し、公開している PowerShell のモジュールにおいては、モジュール外に公開しない、内部的にしか利用しない共通関数について、基本的に変数にスクリプトブロックを割り当てる形で、実装を行っています。

・・・ただ、やっぱりわかりにくいですよね。他に良い方法があれば、是非ご教授いただきたいところです (^_^;)





これでフィニッシュ?
な訳無いデショ! L('ω')┘三└('ω')」

・・・失礼しました。
改めてまとめると、思っていた以上に色んなことを考えて作っていたのだなーと思い、感慨深いものがあります。誤解を恐れずに言えば、PSAnonym も、Swathe.Automation も、Prig を作る過程でできた、単なる副産物だったりするのですががが。実際は、作ってみると色々な知見が得られますし、PowerShell に習熟することで実務も効率化できたり、すごい方々にお会いして情報交換できたりするもので、やってみないとわからないものだなーと、年の初めにも関わらず遠い目をしています ...( = =)

そうそう、年の初めで思い出しましたが、その Prig、ついに制限付きながら 1 パス(C# のメソッドの JIT をフックし、処理を入れ替える)が動き始めました!

2014 年中には、何らかの形にして、なんとか世に出したいところです!


最後になってしまいましたが、改めて新年のご挨拶を。

明けましておめでとうございます。
昨年は色々とお世話になりました。
今年もどうぞよろしくお願いいたします!!! <(_ _)>



[新編] C# 動的メソッド入れ替え - New Feature: Apply a monkey patch to any static languages on CLR -

$
0
0
"あなたは、この世界が尊いと思う?欲望よりも秩序を大切にしてる?"


3 年前から続くこのシリーズもようやく中間報告ができる段階まで来ました。

構想も含めると 5 年以上・・・最初はどうにかマネージコードの世界だけで収めようと、式木こねくりまわして、AppDomain の Load/Unload 繰り返してたのですが、パフォーマンスの壁にぶち当たり、C++ とアンマネージ API の世界へ降りて行ったことを覚えています。しかし、今見返すと相当高い要件出してますね、自分・・・。これは中間報告と言いつつ、実はまだ第 1 四半期報告ぐらいなものなのかもしれません ((((;゚Д゚))))

ですが、Stack Overflow で昨年の中ごろまで編集が続いていたこのスレッドでも、結局は OSS で、私が作っているようなものが出てくることはありませんでしたので、現状でも少しは価値を提供できるかなと思い、見えるところに置いた次第。「世界初!」みたいなのは自分だけで思っていても痛い人ですし、もし万が一仮に本当にそうだった場合、私一人が知っているという状況が怖くなっただけ、とも言えます。「C# 動的メソッド入れ替え」で、(^q^) キョウユウ♪キョウユウ♪

まあ実際、私の観測する範囲では、「.NET Profilers aren’t scary(怖くない .NET プロファイラ)」っていう怖い資料を公開されている方や、「uMock」っていう Microsoft Moles/Fakes の OSS 代替実装になるべく進められている実証プロジェクトもあるようですので、遅かれ早かれ似たようなものは出てくると思います。
また、Premium エディション以上とは言え、Fakes が Visual Studio に標準搭載されたことも大きいでしょう。Microsoft 自身が、CLR 上の静的言語の世界に対し、冒頭にあるような問いかけをしているようなものですから(標準搭載したことについては、やはり否定的な意見が見られますね。中の人も必要悪だと言っているようですし、私もリリース時にはなるべく誤解のないようにしたいとは思います)。

さて、今回の記事では現状できる簡単な使い方の紹介と、気になるであろうその仕組みについて、短めに解説させていただこうと思います。それでは、どうぞ!
※現状、.NET 3.5 + x86 の組み合わせと、IL 全体の 3 割強しか対応ができていませんので、それを念頭に置いていただき、温かく見守っていただければ幸いです (`・ω・́)ゝ


以下の記事、ライブラリを使用/参考にさせていただいています。この場を借りて、改めてお礼させていただきます m(_ _)m
.net - What C# mocking framework to use? - Stack Overflow
.NET Profilers and IL Rewriting - DDD Melbourne 2
myarichuk-uMock
VS11 Fakes Framework Considered Harmful
VS11 Fakes Framework Harmful (Part Two)
Visual Studio Fakes Part 2 - Shims - Peter Provost's Geek Noise
c# - How Moles Isolation framework is implemented? - Stack Overflow





目次

Prig とは
今までもこの Blog で何度か単語としては登場していますが、このライブラリ、名前を Prig(PRototyping jIG) と言い、不完全ではありますが、ぼちぼち動き始めています。
はじめは公開リポジトリでプロトタイピングなどもしていたのですが、設計の失敗や基盤の再構成など、あまりにもイチから作り直すことが多かったため、途中からずっと GitHub のプライベートリポジトリに籠って開発をしていました。さすがに、このところだいぶ落ち着いてきたため、また本流にマージし公開リポジトリでの開発に戻ってきた次第です:

  urasandesu/Prig - GitHub

もし、.NET プロセスの JIT をフックして、メソッドの中身を入れ替えるライブラリのソースコードを参考にしてみたい!という奇特な方がいらっしゃるのであれば、クローンしていただき、中身をご確認いただければと思います。

ちなみに、"Prig"を辞書で引くと、スリとかコソ泥とか自惚れ屋とか、あまり良い印象の単語ではないのですが、おこがましくも "Git"(日本語訳:ばか、間抜け)にあやかり、将来的に .NET 開発基盤の 1 つとして使われるようになればなー、みたいな願いも込めてたりします。まあ、私が作るライブラリ群自体、どれも名前の付け方にセンスの無さというか悪趣味さが漂っていますががが (^_^;)

さて、README.md にもあるのですが、どのような感じで使うのかを見てみましょう。
よくある例で恐縮ですが、以下のようなコードをテストしたいとします:
皆さんご存知の通り、このままではテストできません。DateTime.Now という、開発者が予想できない外部の環境に依存する値を返すプロパティを直接利用しているためです。テスト可能にするためには、DateTime.Now をテスト用のダミー情報のような開発者が意図した値を常に返すテストダブル(モック)に入れ替えられるようにする必要があります。

しかしながら、相手は、static なプロパティであり、かつ mscorlib という Microsoft が提供する外部ライブラリにあるものですので、そちらに手を入れることは不可能です。このようなテストをしにくい API を使う際は、それを吸収する層を設け、モックに入れ替え可能にしたり、対象の情報を引数などから取るようにしたりする等、プロダクトコードを設計・実装する時点で、工夫をする必要がありました・・・そう、プロダクトコードを設計・実装する時点というのがミソですね。もし、このままのコードがリリースされた後、ここに新しい機能が追加されるなどしてだんだんコードが複雑になっていった時に、さてそろそろリファクタリングしないと・・・となってからでは大変な労力が必要になってしまうでしょう。

このような場面で、Prig を使うと、プロダクトコードに全く手を加えずに、対象の API をモックに入れ替えることができるようになります。どのような感じになるかを紹介したいと思います。

まず、以下のようなスタブを、「入れ替えたいメソッドを含む Assembly の dll 名」 + 「.Prig.dll」という名前で作成します(例えば、DateTime.Now は mscorlib.dll にありますので、ここで作成するスタブは mscorlib.Prig.dll に作成するようにします):
スタブを作ったら、テストのためのクラスライブラリを作成し、そこにこのスタブの参照を追加します。
テストコードでは、このスタブにダミー情報を返すモックを設定することで、テストが可能になるのです!
以前に比べると、C++ のコードが全く必要なくなりましたので、だいぶ実用性が上がったのではないでしょうか (^_^;)
さあ、テストができればしめたもの。忘れずにプロダクトコードのリファクタリングを行い、Prig のようなものを今後使わなくても良いように、うまい感じの構造に変更しておきましょう!





Prig の仕組み
アンマネージ API を使った処理の入れ替えは以前解説させていただいた通りですので、最後の部分となる、対象のメソッドへどのようにして迂廻路を埋め込んでいるかを見てみたいと思います・・・と言っても、これまでにも紹介させていただいたスレッド c# - How Moles Isolation framework is implemented? - Stack Overflowほぼそのままなのですががが (^_^;)

手順としては以下のような感じになります:
1. モジュールロード時:スタブ Assembly の存在確認
2. JIT 前:迂廻路の埋め込み対象かのチェック
3. JIT 前:迂廻路の埋め込み
4. 実行時:スタブ設定有無の確認
では、順に見ていきましょう。肝な部分はソースコードも合わせて解説したいと思います。なお、メインの処理は、Weaver.cppに集まっています。より詳細な動きを知りたい場合は、ここを起点として辿ると良いでしょう。


1. モジュールロード時:スタブ Assembly の存在確認
ModuleLoadFinished メソッドで通知されるモジュールロード完了のイベントで、スタブ Assembly の存在確認を行います。このメソッドは、アンマネージ プロファイリング API のインターフェースである ICorProfilerCallback2 に定義されたもので、第一引数に渡されてくる ModuleID から、そのモジュールのざっくりとした情報を取得することができます。
ここでは後述の通り、あまり詳細な情報を得ることはできませんので、モジュール名からスタブ Assembly が同一ディレクトリにあるかどうかだけをチェックします。


2. JIT 前:迂廻路の埋め込み対象かのチェック
JIT の開始を知らせる JITCompilationStarted メソッドで、現在のメソッドが迂廻路埋め込み対象化どうかをチェックします。あ。その前に、先ほどのモジュールロード時のチェックを通っているかの確認ですね。

ちなみに、私はネストが深くなるのが嫌なので、条件に合わなかったらすぐ return する派です。合わない人はごめんなさいね ★(ゝω・)
モジュールロード時のチェックを通っているということは、スタブ Assembly が存在するということですので、付与されている IndirectableAttribute を列挙し、そこに指定されたメタデータトークンをキャッシュしておきます。

気を付けなければならないことが 1 点。アンマネージ プロファイリング API は、開発者が「CoInitialize を呼び出さないようにする必要があります」と決められており、CoInitialize の呼び出しタイミングは CLR に委ねられています。つまり、これを呼び出さないと使えない API は、いつ呼べるようになるのかがわかりません。最初の if 文の条件の 1 つに指定されている pDisp->IsCOMMetaDataDispenserPrepared() はそんな状況に対応するための苦肉の策だったりします。
また、この苦肉の策を行ったとしても、私が試した限り、ModuleLoadFinished メソッドのタイミングで CoInitialize が行われていることはありませんでした。本当はキャッシュ制御みたいなことはせずに、ModuleLoadFinished メソッドで同時にできると良かったのですが・・・仕方無いね (´・ω・`)
【2014/03/21 追記】この制限は、.NET 2.0 までのものでした。。。.NET 4 以降は、.NET Framework 4 および 4.5 で追加された CLR ホスト インターフェイスにある通り、「CoCreateInstance 関数を使用するアパートメント モデル、集約、およびレジストリのアクティブ化はいずれも存在しません。」ので、特にチェックすることなく処理が進められるはずです。私のライブラリも最終的にはそうなる見込みです ☆(ゝω・)【2014/03/21 追記】

メタデータトークンの列挙まで終わっていれば、あとは JIT が始まった現在のメソッドのメタデータトークンが、その中にあるかどうかをチェックするだけです。


3. JIT 前:迂廻路の埋め込み
迂廻路の埋め込み処理です。

ローカル変数は、既存の順番が変わると大変面倒なことになりますので、初めにコピーします(pBody->GetLocals() で回しているところですね)。その後、迂廻路となる部分の IL ストリームとローカル変数をどばっと埋め込み(EmitIndirectMethodBody)、続いて元の IL ストリームを流し込みます(pBody->GetInstructions() で回しているところです)。あとは、迂廻路分のオフセットに気を付けながら、例外ハンドラを再設定するようにします(pBody->GetExceptionClauses() で回しています)。EmitIndirectMethodBody の中身は、スタブでやっていたことの逆を IL でガリガリ書いているだけですので特に解説はしません。


4. 実行時:スタブ設定有無の確認
DateTime.Now (.NET 3.5 まで)は、ildasm や ILSpy で確認すると、以下のような処理になっていますが:

Prig を通すとこうなります:

Moles の仕組みをほぼ踏襲した形になっていると思いますが、これにより、スタブから設定したデリゲートが存在すれば、それを使ってダミー情報を返し、存在しなければ元の処理を実行する、ということができるようになります。

ちなみに、LooseCrossDomainAccessor は、こちらで解説させていただいた、「同一プロセス内であれば AppDomain の仮想的な境界をそげぶできるアレ」ですね。mscorlib のようなドメイン中立として読み込まれる Assembly は、テストコード側から見ると AppDomain を 1 つ跨ぐ形になりますので、この仕組みが無いと、入れ替えできる処理に大きな足かせができてしまうのです。





次はリリース!?
はい!年初に立てた目標通り、何とか残りの IL と .NET バージョン、プラットフォームとサポートし、2014 年中のリリースに持っていきたい・・・といっても、最初に書いた要件を考えるとそれでもまだ半分ですね。先は長い・・・ ...( = =)
ですが、やっと一つの区切りが見えてきたところではあります。引き続き .NET の低レイヤーな技術を追い、また折を見て情報共有させていただければ思いますので、今後ともよろしくお願いしますね!



ニセモノ語り - Prig: Open Source Alternative to Microsoft Fakes -

$
0
0
"偽る事が悪い事なら、僕は悪い奴でいいんです。"


プリリリースです!

  Release Prig v0.0.0-alpha - urasandesu/Prig - GitHub

前回から少し時間が空いてしまいましたが、リリースに際して必要な機能が一通り実装できたこともあり、プリリリースする運びとなりました。
後は、リリースまでテスト!ドキュメント!サンプル!テスト!ドキュメント!サンプル!テスト!ドキュメント!サンプル!と、ひたすら仕上げ作業を行うことになると思います。

諸々の事情で、前回解説した使い方からはだいぶ変わってしまってますので、ご承知おきを。追加した機能もざっくりとですが、一通り紹介させていただければと思います。では、行ってみましょ!
※まだα版ということもありますので、それを頭の片隅に置いてご笑覧いただければと思いますですー m(_ _)m


以下の記事、ライブラリを使用/参考にさせていただいています。新機能となると、本当に先人のお知恵が身に沁みますね・・・。多謝! (`・ω・́)ゝ
Zero to Hero: Untested to Tested with Microsoft Fakes Using Visual Studio | TechEd North America 2014 | Channel 9
Test Isolation Is About Avoiding Mocks — Destroy All Software Blog
Microsoft Fakesを使ったVisualStudio単体テストをJenkinsで実行する blog.prvten.com
Expression Trees をシリアライズする - TAKESHIK.ORG
Powershell script from Visual Studio Post-build-event failing - Stack Overflow
Getting code coverage from your .NET testing using OpenCover. - CodeProject
Visual Studio Test Tooling Guides - Downloads
NCover Moles coverage support
Mocking and Isolation Frameworks Deep Dive in .NET - Roy Osherove
Profilers, in-process side-by-side CLR instances, and a free test harness - David Broman's CLR Profiling API Blog
Why do assemblies with the SecurityTransparent attribute cause instrumented code via a profiler to throw a VerificationException? - Stack Overflow
Building NUnit on Windows 8.1 - nunit-dev Wiki
Moq/moq - GitHub
Cheat Sheet - AutoFixture/AutoFixture Wiki - GitHub
c# - .Net Fakes - How to shim an inherited property when the base class is sealed? - Stack Overflow
Real time unit testing - or "how to mock now" - Programmers Stack Exchange
Microsoft Fakes; Testing the Untestable Code
sawilde/opencover - GitHub
Microsoft Fakes Framework—SVNUG Presentation 35 - YouTube
Write MSIL Code on the Fly with the .NET Framework Profiling API
ReJIT Limitations in .NET 4.5 - David Broman's CLR Profiling API Blog - Site Home - MSDN Blogs
Walking the stack of the current thread | Johannes Passing's Blog
slimtune - A free profiling and performance tuning tool for .NET applications - Google Project Hosting
c# - How to stub 2nd call of method? - Stack Overflow
Four Ways to Fake Time, Part 4 | Ruthlessly Helpful
Advanced Usage | JustMock Documentation
Generics and Your Profiler - David Broman's CLR Profiling API Blog - Site Home - MSDN Blogs





目次

クイックツアー
試作版スタブ生成器の追加やランナーの追加で導入手順はかなり変わりました。前回も登場した、「テストしにくい副作用を直接参照している処理」を例に見ていきます:

手順としては以下のような感じ:
Step 1: スタブ設定の作成
Step 2: スタブの生成
Step 3: テストの作成
Step 4: テストの実行
Final Step: リファクタリングしてキレイに!
では、実際にやってみたいと思います。


Step 1: スタブ設定の作成
以下のようなスタブ設定を作成します:


パッケージの中に "Urasandesu.Prig.Framework\PilotStubber.prig.template"としてテンプレートがありますので、そちらも参考にされると良いかと思います。


Step 2: スタブの生成
開発者コマンド プロンプト for VS2013 を実行し、スタブ生成のために PowerShell スクリプト "Invoke-PilotStubber.ps1"を実行します:


いちいち打ち込むのは大変ですので、実際は、*.csproj のビルド前イベント等にスクリプトを埋め込むのをオススメします。.NET のバージョンやプロセッサアーキテクチャ毎の構成を作る場合は、パッケージの中の "Test.program1\Test.program1.csproj"も参照していただければと。


Step 3: テストの作成
単体テストのための新しいクラスライブラリを作成し、スタブ Dll を参照に加えます。
テストコードでは、スタブを利用し、偽の情報を返すテストダブルに置き換えることで、テストが可能になるのです!




Step 4: テストの実行
本来は、プロファイラベースのモックツールを有効にするためには、環境変数を弄る必要があります。Microsoft Fakes/Typemock Isolator/Telerik JustMock は、そのための小さなランナーを提供しますので、Prig でも同様としました。なので、テストを実行するには "prig.exe"を以下のように使用する必要があります:




Final Step: リファクタリングしてキレイに!
テストができてしまえば、リファクタリングに躊躇はなくなりますね!例えば、以下のようにリファクタリングできることがわかると思います:


こんな感じで、Prig はテストしにくいライブラリに依存するようなコードをキレイにするのをサポートしてくれます。開発がまた楽しくなること請け合いですよ!


パフォーマンスも含め、使い勝手についてはまだ大いに改善する余地があると思います。特にスタブ生成周りはヒドイ・・・。もともと、こんな感じになってしまうことは分かっていましたので、「手動でスタブ作って ildasm 見てメタデータトークン埋め込んだほうが楽じゃね?」と思い、前回紹介させていただいたような方式を採っていたのですが、メタデータトークン直書きだと、Windows Update でヤられてしまうことがわかったのでした・・・。プリリリースに当たり、急遽試作版スタブ生成器を導入したわけですが、パターンが出し切れていないことから、使い勝手より柔軟性を重視する方向に振っています。このパターン、というのは例えば、sealed なクラスが持つ abstract な親の副作用付プロパティや、I/F に internal なアクセス修飾子の型が現れるメソッド、Fakes ですらリリース時にはサポートできていなかったジェネリック型制約など、ですね。最終的には、IL 直吐き型のスタブ生成器にする予定ですが、まだちょっと検討不足。ぼちぼちと考えていこうと思います。





元のメソッド呼び出しのサポート
迂回処理を書いていくと、テストケースによっては、「元の処理はそのまま呼び出して、引数に渡ってくるものを検証したい」とか、「n 回目以降だけダミーに入れ替えたい」とか、の要望は出てくると思います。そのためのガード機能が、IndirectionsContext.ExecuteOriginal。Fakes で言う ShimsContext.ExecuteWithoutShims と同じ機能です。
「元の処理は~」の方は、Fakes を使った単体テストの指南書、「Better Unit Testing with Microsoft Fakes」にも、「Validating (private) Implementation Details」(実装の詳細の確認)として例が挙げられていますが、ちょっと難しそうに見えましたので、もう少し簡単なサンプルで見てみます:


Village(村)オブジェクトは、生成するとランダムに 100 個未満の RicePaddy(田んぼ)とそれを繋ぐ FarmRoad(農道)を生成します。なお農道には、その距離として distance も設定されます。
あれれ?早速疑問が。Random の初期化は、デフォルトコンストラクタ(Environment.TickCount)ではなく、わざわざ Seed を指定するコンストラクタを使って DateTime.Now.Second を突っ込んでいますね。これでは簡単に同じ値になってしまい、同じ結果しか生まない村がたくさんできてしうまうはず・・・村ののんびりした雰囲気を出すためでしょうか?いやいやいやいや・・・。

まあ、これぐらいの規模であればえいやっと直してしまっても、レビューアーに突き返されることは無いでしょうが、大きくなってくるとテストを書き、仕様が囲えたほうが安心できますね(Fakes の資料も、実は効果を見せるために、わざと難しい例にしているのかもしれません!)。サンプルということでとりあえず書いてみましょう。こんな感じで:


スタブの埋め込み時間がまだバカにならないため、Fakes のサンプルと異なり、あらかじめ JIT をしておく必要があったり(new Random(); の部分ですね)、短い時間に間引くことが難しかったり(Thread.Sleep(TimeSpan.FromSeconds(1)); の部分ですね)するのですが、意図することは大体同様にできるはずです。
仕様が囲えたら、Thread.Sleep(TimeSpan.FromSeconds(1)); の部分をコメントアウトして、テストが失敗することを確認しましょう。これで安心して不具合修正ができますね!(そうそう、Environment.TickCount 案は、採用しても、失敗したテストを通すことができないことにすぐ気づくと思います。実はこのカウンタ、あまり精度良くないのですよね。簡単に見えて、実は落とし穴がある例でした Ψ(`∀´)Ψ)

ちょっと注意が必要なのが構造体。迂廻処理に、クラスと同じ取り回しで引数を与えると、コピーしたものに対する処理しかできなくなってしまいますので、シグネチャが参照渡しに変わっています。例えば、以下のようなコードがあって、:


コンストラクタでどのように Nullableが初期化されるかを確認したい場合は・・・:

のようになります。第一引数を置き換える感じで初期化をするんですね。ちなみに、私が探す限り、Fakes だと同じことができないようなのですが・・・まあ、もし本当に無かったとしても、構造体にこういうものが欲しくなることはめったにない、という判断なのでしょう。





ジェネリックのサポート
.NET の IL 周りをイチからやられている方であれは、きっと誰もが気絶しそうになることを想像に易いこの作業。
一度やってみると、「リフレクション API って、なんて抽象化されてて使いやすいんだ!」と感じること請け合いです。IL のような、ある程度抽象化された中間言語でもここまで難しいのだから、と、某 Java のジェネリックで型情報が消えてしまう件への悲しみが和らいだり、なぜ某 TypeScript が v0.9 でコンパイラをイチから書き直さなければならなかったのかの妄想が捗ったり、ますます某 C++ のテンプレートに対する畏怖の念が強まったりするかもしれません。

リフレクション API の話題が出たところでちょっと脱線しますが、最初に挙げていた要件を満たしつつ、最悪私一人のリソースでも保守して行けるような規模に抑えるためには、アンマネージで、かつリフレクション API や、Mono.Cecil に匹敵する使い勝手・抽象度の機能群を準備することは不可欠でした。
これに当たるのが、Prig のサブモジュールにもなっている Swathe です。

  urasandesu/Swathe - GitHub

文字通り、アンマネージ API という辛さを、.NET Framework のリフレクション API ライクな API で優しく包み、辛みを和らげてくれる"包帯"ですね。例えば「System.Linq.Enumerable クラスの Average メソッドについて、IEnumerable<int> を引数に取るオーバーロードのメタデータトークンは?」みたいな処理が、アンマネージ API を知らなくても、リフレクション API を知っていれば、こんな感じで、ごく自然に書けるようになってます(もちろん、C++ の初歩的な知識は必要なのですががが):


Prig は、そのプロジェクトの説明に "lightweight framework"とありますが、これは冗談やネタではなく、単にメインのプロジェクトだけをクローンした場合、その規模は 7K loc ほどにしかならないという、本当にシンプルなライブラリになっています。ここまでシンプルにできているのは、この Swathe のおかげ。また、Prig は、その Language statistics を見ても、C# + PowerShell が 7 割ぐらいで、残りが C/C++、とほとんどがマネージコードになっていますので、興味を持っていただければ、雰囲気を掴むのはさほど難しくないでしょう。

えっ、サブモジュールを取り込むとどうなるの、ですか?・・・まだ要件の半分も満たしていないのに 98K loc ほどになります(白目)。こちら、中身が、PowerShell を使った自動生成コードによる依存関係管理やら、C++ の TMP を使ったオブジェクトファクトリー/リポジトリ自動生成やら、ネット上に情報の少ない各種アンマネージ API との格闘の跡やらで溢れていますので、まだしばらくは、人様に中身を紹介できるものにはならないでしょうね ...( = =)

閑話休題。ジェネリックタイプとジェネリックメソッドの迂廻路の雰囲気を紹介しましょう。




まずはジェネリックタイプのサンプルとして、先ほど取り上げた Village にまた登場してもらいました。このテストでは、指定した田んぼ間の最短経路を求める GetShortestRoute メソッドの内部状態を確認しています。Random.Next(int) を乗っ取っていますので、生成される田んぼと農道は、常に一定の数と距離で繋がれることになります。こんな感じですね:



GetShortestRoute メソッドはダイクストラ法を使って経路を求めていますので、ローカル変数 handled に「ある田んぼから行ける田んぼの内、まだ調べていない一番近い田んぼへの経路(を識別する田んぼ)」が、わかる度に追加されていきます。従って、ジェネリックタイプである List<T>.Add(T) を乗っ取って、そこに入ってくる要素が意図通りであれば、内部状態が確認できることになるでしょう。considerations に溜め込んだ要素を Assert しているのがこれに当たります。図の通り、
1. 【開始】田んぼ 2 → 0(距離: 4)、2 → 1(距離: 1)、2 → 3(距離: 7)を調べ、田んぼ 1 へ。
2. 田んぼ 1 → 0(距離: 2)、1 → 2(調査済み)、1 → 3(距離: 6)を調べ、田んぼ 0 へ。
3. 田んぼ 0 → 1(調査済み)、0 → 2(調査済み)、0 → 3(距離: 3)を調べ、田んぼ 3 へ。
4. 田んぼ 3 で、全ての経路が調べられたため完了。【終了】
と計算が進みますので、considerations に入ってるべきは、テストにある通り、田んぼ 2 → 田んぼ 1 → 田んぼ 0 → 田んぼ 3 となります。

次はジェネリックメソッド。AppDomain の解説で使った、懐かしのテストしにくい設定読み込みクラスに登場していただきます。今回は不運にも、それを使う側になったシチュエーションで ヽ(;▽;)ノ


あああ・・・。
「設定ファイルに設定された曜日とその前曜日は休日」という仕様なのでしょうが、switch 文と分岐で愚直に表現されています。サイクロマティック複雑度も 16 と、何とも香ばしい・・・。業務で出会った日には「上限値が決まった列挙型なのだから modulo で表現できたろうに・・・」「せめてテーブルで表現されてればまだ・・・リファクタリングしたい・・・」「あ、これ設定ファイルに依存してるからもしかして・・・やっぱりテストも無いああああくぁwせdrftgyふじこlp」「テストがなければ、みんな死ぬしk」と何かが濁っていく感じが味わえそう。
幸運なことに、Prig を紹介するにあたっては恰好のサンプルにしかなりません。「テストがなければ、仕様を囲うコードを書けば良いじゃない?」


え、ちゃんと全部の分岐網羅できているか不安ですって?またそんな贅沢言ってこの娘は・・・しょうがないですね。
そんな方のために最後の機能、Profilers Chain を見ていきましょう!




Profilers Chain のサポート
Profilers Chain とは、複数のプロファイラを数珠繋ぎにして実行する機能です。なお、Profilers Chain という呼び名、私がこう呼んでいるだけで、本来の呼び方があるのかもしれません。詳しい方の突っ込み、お待ちしております <(_ _)>

直感的に「プロファイラを数珠繋ぎで実行?そんな風に使うことあるの??」と思われるかもしれませんが、.NET では、動的に IL を書き換えるタイミングが、プロファイル API が提供する JIT 時しかありません。ですので、実行する時に何かしらの計測や仕組みを入れ込む開発ツールは、必然的にここに集まることになります。メモリリークの検出や、パフォーマンス測定、トレースログ挿入によるデバッグ支援、埋め込みスクリプトによるエディット&コンティニュー…などなど。

問題になるのはこれらを組み合わせて使う時。プロファイル API の仕様上、一つのプロセスにアタッチできるプロファイラは自動的に一つになってしまいます。例えば、この Prig や Microsoft Fakes/Typemock Isolator/Telerik JustMock などのプロファイラベースの自動単体テスト向けテストダブル生成フレームワークと、NCover や OpenCover、JetBrains dotCover などのプロファイラベースのカバレッジ計測ツールを同時に使うと、「テストダブルは使えるがカバレッジが計測できない」「カバレッジは計測できるがテストダブルが使えない」と悲しみ溢れる状態に陥ります。



なので、本来プロファイラベースの某を作る場合は、その用途により、「もし既にプロファイラが組み込まれていたら、それを CLR からの指示に従って、透過的に実行してあげる」機能が必要かどうかを検討する必要があるのですね。



Prig は、Fakes の OSS 代替実装を謳う以上、この機能を持たないわけにはいかないので、もちろんサポートしています。OSS のカバレッジ計測ツールである OpenCover を使い、先ほどの例を実行してみましょう。引数が多くなってしまうのはどうしようもないのですが、こちらで紹介されている通り、*.bat を作っておくとちょっとは判りやすくなると思います:


で、実行します:


ReportGeneratorを使って結果を整形するとこんな感じ:






ちゃんと C0/C1 が網羅できていることが確認できました!もうリファクタリングに躊躇することはありません。保守しやすいコードに直し放題ですやったー! (((o(*゚▽゚*)o)))
ちなみにこの仕組み、Fakes が Visual Studio に標準搭載になって除外されてしまった残念な部分でもあります。Moles 時代はできたのに・・・自社製品で囲い込みたい気持ちは分かりますが、うーん (-_-;)





テスト!テスト!!テスト!!!
目に見える使い方は以上な感じ。あとは x86/x64 対応とか、.NET 4 の SxS 実行対応とか、地味な部分も手広くやってはいます。はじめはうんともすんとも言わなかった NUnit GUI での実行も、ここ数か月で動くようになり、安定性はだいぶ上がってきた実感がありますね。が、この解説記事向けのサンプルを書く中で未実装 OpCode が見つかるなど、やっぱりまだまだな状態。さあ、ここからが正念場です。テスト!テスト!!テスト!!!



クイックツアー変更分と既存 Mocking ライブラリとの連携 - from "Prig: Open Source Alternative to Microsoft Fakes" Wiki -

$
0
0
リリースです!!

  プロジェクトページ:Prig Open Source Alternative to Microsoft Fakes By @urasandesu
  NuGet:NuGet Gallery | Prig Open Source Alternative to Microsoft Fakes 1.0.0

テストやサンプルの追加が思ったよりうまく進み、また、課題だった部分が全て解決できたことや、ドキュメントがだいぶまとまってきたこともあり、予定より少し早めることができました。
リリースに際し、これまで日本語になっていなかったドキュメントは、この記事を含め順次日本語化していこうと思います。拙い英語なので、もし日本語記事を読んでいただいた後でドキュメントを読んで「あ、そういうことが言いたいんだったら、この言い回しにしたほうが良いよ」のようなことがありましたら、是非 @urasandesuにリプをしていただくなり、Issuesに積んでいただくなりしていただければありがたいです。これから全 9 回を予定していますが、よろしければ最後までお付き合いくださいませ。

さて、導入手順であるクイックツアーがまた変わりましたので(だいぶ簡単になりましたよ!)、そこから解説していきましょう。加えて、今回は伝統的なモッキングフレームワークとの連携を。ちなみに Prig は、伝統的なモッキングフレームワークの機能をサポートしていません。OSS で既に必要な機能を持つライブラリが存在するのであれば、それらを使えば良いだけですからね。ただ、そのようなライブラリと連携できないことには話になりません。代表的なライブラリとして、MoqNSubstituteRhino MocksFakeItEasyを例にとり、それらと Prig を連携して利用する方法を解説していきます。

それでは、行ってみましょう!


以下の記事、ライブラリを使用/参考にさせていただいています。この場を借りてお礼申し上げます m(_ _)m
Getting Started with Unit Testing Part 3 | Visual Studio Toolbox | Channel 9
GenericParameterAttributes Enumeration (System.Reflection)
How can I fake OLEDB.OleDBDataAdapter to shim OleDBDataAdapter.Fill(dataSet) to shim results for unit testing using NUnit - Stack Overflow
c# - Can I Change Default Behavior For All Shims In A Test - Stack Overflow
IMetaDataImport2::GetVersionString Method
Obtaining List of Assemblies present in GAC
How should I create or upload a 32-bit and 64-bit NuGet package? - Stack Overflow
visual studio - Can NuGet distribute a COM dll? - Stack Overflow
Project Template in Visual Studio 2012 - CodeProject
Preferred way to mock/stub a system resource like System.Threading.Mutex - Stack Overflow





目次

クイックツアー(変更分)
NuGet への対応や、前回挙げていた懸念事項が全て解けたことを経て、導入手順はまたかなり変わりました。毎回の登場で恐縮ですが、「テストしにくい副作用を直接参照している処理」を例に見ていきます:

手順としては以下のような感じ:
Step 1: NuGet からインストール
Step 2: スタブ設定の追加
Step 3: スタブ設定の修正
Step 4: テストの作成
Step 5: テストの実行
Final Step: リファクタリングしてキレイに!
「手順増えてるじゃないですかやだー」と思われるかもしれませんが、1 つ 1 つの手順が軽くなっているので大丈夫です (^-^; では、実際にやってみましょう!


Step 1: NuGet からインストール
Visual Studio 2013(Express for Windows Desktop 以上)を管理者権限で実行し、テストを追加します(例えば、ConsoleApplicationTest)。そして、以下のコマンドを Package Manager Console で実行します:

※注:ほとんどの場合、インストールは上手く行きます。が、Visual Studio のインストール直後だと上手く行かないことがあるようです。こちらの Issue にあるコメントもご参照ください


Step 2: スタブ設定の追加
以下のコマンドを Package Manager Console で実行します:

このコマンドは、テストのための間接スタブ設定を作成する、という意味です。mscorlib を指定しているのは、DateTime.Now が mscorlib に属しているからですね。コマンド実行後、外部からプロジェクトの変更があったけどどうします?という旨の確認メッセージが表示されますので、プロジェクトをリロードしてください。


Step 3: スタブ設定の修正
プロジェクトに設定ファイル <assembly name>.<runtime version>.v<assembly version>.prig を見つけられると思います(この場合だと、mscorlib.v4.0.30319.v4.0.0.0.prig ですね)。設定ファイルをコメントに従い修正したら、全てのプロジェクトをビルドします:



Step 4: テストの作成
テストコードにおいては、スタブの使用と偽の情報を返す Test Double への入れ替えを通じ、テスト可能になります:



Step 5: テストの実行
本来、プロファイラベースのモックツールを有効にするには、環境変数を弄る必要があります。そのため、そのようなライブラリ(Microsoft Fakes/Typemock Isolator/Telerik JustMock)は、要件を満たすための小さなランナーを提供しますが、それは Prig でも真となります。prig.exe を使い、以下の通りテストを実行します(引き続き Package Manager Console で)。



Final Step: リファクタリングしてキレイに!
テストができてしまえば、リファクタリングに躊躇はなくなりますね!例えば、以下のようにリファクタリングできるでしょう:

こんな感じで、Prig はテストしにくいライブラリに依存するようなコードをキレイにするのをサポートしてくれます。開発がまた楽しくなること請け合いですよ!


PowerShell モジュールによるサポートがだいぶ手厚くなったおかげで、手書きで *.csproj を触ったり、コマンドを組み立てたりする必要はなくなりました。ただ、その分環境に依存しやすくなっていますので、一長一短かなとも思います。あとはパフォーマンスですね。懸念事項が片付いた今なら、現状行っている DLL 解析⇒ソースコード生成⇒*.csproj 生成⇒MSBuild でソースコードをビルドし、スタブ Assembly を生成という冗長な処理を、DLL 解析⇒スタブ Assembly を生成、にまで最適化できるはず。ただ、これには、アンマネージ API をラップしている Swatheを、だいぶ整理する必要があるため、次のメジャーバージョンアップ時を目指して、ぼちぼちやっていく予定です。他にも、Code Diggerや、Pexのような、テストコード自動生成機能の搭載や、Telerik JustMock がやっているようなネイティブ API の入れ替え、導入手順のさらなる簡易化などもチャレンジしていきたいところですね。





既存 Mocking ライブラリとの連携
次は既存 Mocking ライブラリとの連携です。以下のクラスのテストについて考えてみましょう:

たぶん、「ValueWithRandomAdded は、PropertyChanged イベントをその名前で発火するべき」や「ValueWithRandomAdded は、渡された値 + Random.Next() を保持するべき」のようなテストをしたくなると思います。後者について、MoqNSubstituteRhino MocksFakeItEasyを使ってテストをする連携サンプルを紹介します。

共通の前準備として、Random.Next() のための間接スタブ設定を追加します。Prig をインストールし、各テストプロジェクトで以下のコマンドを実行してください:

スタブ設定ファイルが追加されたら、クラス Random のためのスタブ設定をクリップボードにコピーするため、以下のコマンドを実行します:

そして、それをスタブ設定ファイル mscorlib.v4.0.30319.v4.0.0.0.prig に貼り付けます。それでは、それぞれのモッキングフレームワークとの連携を見ていきましょう!



Moq
Moqの連携サンプルです。



NSubstitute
NSubstituteの連携サンプルです。



Rhino Mocks
Rhino Mocksの連携サンプルです。



FakeItEasy
FakeItEasyの連携サンプルです。

え?「全部同じようなサンプルに見える」ですって?はい、その通りです。重要なことはただ一つ、「デリゲートのためのモックを作成することを意識する」だけですから。そのような機能持っているものの中から、お好きなものをお選びください!(>ω・)




終わりに
つい先日、マイクロソフトから .NET Framework のオープンソース化や、フル機能無料版の Visual Studio の提供.NET Server Framework の Linux/MacOS X 向けディストリビューションの展開の発表があり、大きなニュースになりましたね。
ただ、オープンソース化された .NET Framework コアを見ると、本当のコア部分(ランタイムホストや JIT、プロファイラ内でも動くような制限のないメタデータ操作、リソースの検索・・・etc。いわゆる、昔、SSCLIとして公開された範囲ですね)は含まれていないですし(.NET Core Runtime について、"We’re currently figuring out the plan for open sourcing the runtime. Stay tuned!"と言っているので何かしら提供する気はありそうなのですが・・・)、無償化された Visual Studio Community 2013 はビジネスユースが限定されていたり、その機能は Professional 止まりだったりと、押さえるところは押さえてるっていう印象です。まあ、経営戦略上、必要なものを公開・無償化したというだけと言えばだけなのかもしれません。

私がずっと追っている自動ユニットテスト関連の Visual Studio 拡張の中でも、Microsoft Fakes は、Community エディションには搭載されませんでした。Fakes は、当初から全エディションに搭載して!という声が上がっているにも関わらず、Premium 以上の機能のままですので、こちらも当たり前と言えば当たり前。また、その前身の Molesが動く Visual Studio 2010 のメインストリームサポートが、来年の夏ごろ終わることを考えると、このタイミングで無償化されなかったということは、今後無償化されるとしても良いタイミングになるとは言えない気がします。

そんなこんなで、私が 5 年前から作っていたこのライブラリも、Microsoft Fakes のオープンソース代替実装として、収まるところに収まってしまいました。いや正直、開発し始めた当時、こんな大仰なことを謳えるようになるとは思ってもみなかったですががが… (∩´﹏`∩) 。

まだまだ至らない点があるかと思いますが、これを機に、Prig、.NET 開発のお供として、末永くお付き合いいただければ幸いでございます。



移行サンプル:Microsoft Research Moles による非同期パターンのモック化 - from "Prig: Open Source Alternative to Microsoft Fakes" Wiki -

$
0
0
まとまってきたドキュメントを日本語の記事にもしておくよシリーズ第 2 段!(元ネタ:Prigwikiより、MIGRATION: Mocking Asynchronous Pattern by Microsoft Research Moles。同シリーズの他記事:1

今回は、『neue cc - Rx + MolesによるC#での次世代非同期モックテスト考察』で解説されているような、イベントベースの非同期パターンを採用するクラスに対して Microsoft Research Molesでモック化するサンプルを Prig(と Moq)に移行してみましょう。前述のページをキーワード「ShowGoogle」で検索すると、例を見つけられると思います。


以下の記事、ライブラリを使用/参考にさせていただいています。この場を借りてお礼申し上げます m(_ _)m
James Dibble - Blog - Microsoft Fakes Part Two
Code rant: How To Add Images To A GitHub Wiki
トンネルの思い出 - 東広島市自然研究会
IronRuby.net
Can we use fakes to test AutomationElement methods
Which Isolation frameworks have you used in the past 3 months?
Battle of the mocking frameworks by @dhelper #mockingframework #softwaredevelopment
djUnit
unit testing - HOW to use MS Fakes shims with NSubstitute mocks? - Stack Overflow





目次

移行サンプル:Microsoft Research Moles による非同期パターンのモック化
プロジェクト MolesMigrationDemo に、メソッド ShowGoogle を持つ ULWebClient を作成し、そのテストプロジェクト MolesMigrationDemoTest を作成します:




MolesMigrationDemoTest に NUnit への参照を追加し、README.mdに従って Prig をインストールします。あ、Moqの追加を忘れずに:


mscorlib に属す Console、System に属す WebClient、DownloadStringCompletedEventArgs のモック化を有効にする必要がありますので、以下のコマンドを実行し、間接スタブ設定を作成していきましょう:

padd -as は、Add-PrigAssembly -Assembly のエイリアスです。このコマンドの実行により、mscorlib.v4.0.30319.v4.0.0.0.prig と System.v4.0.30319.v4.0.0.0.prig がテストプロジェクトに追加されます:



次に、以下のコマンドを実行します。これは、Console のための間接スタブ設定をクリップボードにコピーするという意味ですね。で、mscorlib.v4.0.30319.v4.0.0.0.prig に貼り付けます:


ちなみに、エイリアスについて説明しておくと、pfind は Find-IndirectionTarget、pget は Get-IndirectionStubSetting に当たります。貼り付けた結果はこんな感じ:



同様に、WebClient と DownloadStringCompletedEventArgs の設定を作成します。Get-IndirectionStubSetting は MethodBase の配列を渡しさえすれば、良い感じにスタブ設定を作成してくれますので、PowerShell で、標準のリフレクション API から取得した結果を使い、フィルタリングしても何ら問題はありません:

そうしたら、次のように System.v4.0.30319.v4.0.0.0.prig へ結果を貼り付けます:



さて、ビルドは通りましたか?そうであれば、間接スタブを使うことができます。以下のように、オリジナルの例から移行することができるでしょう:




さっくりとまとめ
去年の時点では、まだ私の観測範囲でもバリバリ使われてた感じの Moles。 これのインパクトがあったせいか、.NET で static/final/sealed/拡張メソッド入れ替え可能な Mocking フレームワーク、無償なヤツって無いの?、みたいな話題は、QA サイトに定期的に上がる感じ。残念ながら、今のところ、あまり進展は無い感じですね。

V1.0.0 にもなったことですし、今後は私のほうでも、そのような話題を見つけたら、積極的にアプローチしてみたいと思います。人ばs…改善のアイディアも、頂けるかもしれませんしね。

Prig. It is my weekend project but it is expressed an open source alternative to Microsoft Fakes!! ;)



移行サンプル:Microsoft Fakes による HttpWebRequest のモック化 - from "Prig: Open Source Alternative to Microsoft Fakes" Wiki -

$
0
0
まとまってきたドキュメントを日本語の記事にもしておくよシリーズ第 3 段!(元ネタ:Prigwikiより、MIGRATION: Mocking HttpWebRequest by Microsoft Fakes。同シリーズの他記事:12

今回は、Microsoft Fakes から Prig への移行サンプルを解説させていただきますね。Fakes の解説ドキュメント「Better Unit Testing with Microsoft Fakes」の章「Migrating from commercial and open source frameworks」に載せられているマイグレーションの説明を、例として挙げています。


以下の記事、ライブラリを使用/参考にさせていただいています。この場を借りてお礼申し上げます m(_ _)m
An Introduction To RSpec | Treehouse Blog
Ruby - refinementsについてグダグダ - Qiita
Medihack » Blog Archive » Intend to extend (metaprogramming in Ruby)
c# - How does WCF deserialization instantiate objects without calling a constructor - Stack Overflow
Runtime method hooking in Mono - Stack Overflow
.NET CLR Injection: Modify IL Code during Run-time - CodeProject
CLR Injection: Runtime Method Replacer - CodeProject
Moles: Tool-Assisted Environment Isolation with Closures - Microsoft Research





目次

移行サンプル:Microsoft Fakes による HttpWebRequest のモック化
テスト対象はこんな感じ(警告が発生していたため、若干修正してあります):

個人的に、副作用への入力を検証していなかったり、テストメソッド名に期待値が含まれていなかったりで、元の例はいただけません。それはさておき、とりあえずビルドが成功するぐらいには Prig へ移行してみましょう。なお、プロジェクトの Assembly 参照設定は Moles の移行サンプルと同様ですので、詳細はそちらをご参照ください。

Fakes の「Shim」と呼ばれるクラスは、Prig の「Indirection Stub」に当たります。命名規則によって、「Shim」として利用されているクラスは、HttpWebRequest、WebRequest そして HttpWebResponse ということがわかります。これらのクラスは全てアセンブリ System に属していますので、以下のコマンドで間接設定を追加してください:

間接設定 System.v4.0.30319.v4.0.0.0.prig が追加されたら、以下のコマンドを実行することによって作成される結果を貼り付けます:

? { !$_.IsAbstract } は、実装を持っていないメソッドを除外するフィルター、$_.DeclaringType -eq $_.ReflectedType はベースメソッドをオーバーライドするメソッドを除外するフィルターです。説明のために非常にざっくりとしたフィルターを掛けていますが、厳密なフィルターで最小限の設定を作成することをオススメします。広範囲に影響が出てしまうため、必要以上のメソッドを交換可能にしておくことは良いことではありません。

貼り付けたスタブ設定に対してビルドが正常に終了したら、こんな感じでテストが書けると思います:

ところで、この「Migrating from commercial and open source frameworks」、Fakes は、モックオブジェクトとしての機能を持っていませんので、Moq はそのまま使ったほうが簡単になったんじゃないかなと思うんですが・・・うーむ (-_-;)




付録
個人的にいただけなかった部分を Moq によって修正した例です:




さあ、次!次!
実際のところ、現状で Fakes を導入できている幸運な方は、そのままお使いいただければ良いと思います (´・_・`)

・・・一応機能的な利点を挙げるとすれば、Prig は、Fakes には無い、構造体のコンストラクト時差し替えや、サードパーティ製プロファイリングツールとの連携既存ライブラリにある、シグネチャに非公開な型を持つメソッドの入れ替えをサポートしていますが、まあ些細なことでしょう。
どちらかと言うと、OSS であるが故に、一部のニンゲンは Premium 使えるんだけど、他は Professional なんだよ?、とか CI 環境にまで Premium 以上の Visual Studio 入れなあかんの?、とか、テストだけじゃなく、例えば何か動的に処理を入れ替える仕組みを一時的に入れて開発効率を上げるみたいな、色んなことに使いたいんだけど?とか、そもそもどういう仕組みで動いてるの?とかの状況の方には、ご提案できるやもしれません・・・が、特殊なケースでしょうね。

まあ、本来はテスト向けのツールとして始めたわけじゃなかったですから・・・(震え声)

この辺は一段落したら追々考えるとして、とりあえず、次、行ってみよう! (・∀・)




デフォルトの振る舞い - from "Prig: Open Source Alternative to Microsoft Fakes" Wiki -

$
0
0
まとまってきたドキュメントを日本語の記事にもしておくよシリーズ第 4 段!(元ネタ:Prigwikiより、FEATURES: Default Behavior。同シリーズの他記事:123

自動化されたテストで既存の振る舞いを囲う時、そのメソッドが呼び出されているかどうかだけを、単にチェックしたくなることがあると思います。例えば、「対象のメソッドが、現在の環境やプラットフォームの情報にアクセスしているかどうかを知りたいので、クラス Environment のいずれかのメソッドの呼び出しを検出したい」というようなことが考えられますね。これを実現するために、Prig では間接スタブのデフォルトの振る舞いを変更する機能をサポートしています。今回はこの機能を解説していきましょう。


以下の記事、ライブラリを使用/参考にさせていただいています。この場を借りてお礼申し上げます m(_ _)m
Muhammad Shujaat Siddiqi Prefer 32-Bit with Any CPU Platform Target - Visual Studio 2012 Enhancements
何かのときにすっと出したい、プログラミングに関する法則・原則一覧 by @hiroki_daichi on @Qiita
OpenTouryoProject/OpenTouryo - GitHub
Code Generation in a Build Process
Microsoft.Fakes stub interface fails to be found - Stack Overflow
Microsoft Fakes: Trying to shim a class but dependencies are still there - Stack Overflow
c# - How do you call a constructor via an expression tree on an existing object? - Stack Overflow
Why did we build
rspec の書き方がわからない"" - Togetterまとめ
Myron Marston » Notable Changes in RSpec 3





目次

デフォルトの振る舞い
例として、以下のような「古き良き時代の」コードに対して、仕様の変更が発生した、ということを想像してみてください - もちろん、テストコードはありませんね (´・ω・`)

NotifyStartJob は、最初に、あるジョブを実行するための前提条件を検証し、全ての条件が満たされていれば、ジョブに引き渡すパラメータファイルを生成します。goto 文を使って、共通のエラー処理に飛ばす手法は、C 言語が主流だった時代によく見られたものですが、例外による通知が標準になった現在でも、そのエラーが本当の例外でなく単に業務的なエラーだったり、パフォーマンスの問題があったりすれば、まだ利用されることがあるかもしれません。

ちょっと簡単ではないということを感じたあなたは、テストコードを書くことにしました。まずは、古くからあるモックフレームワークとちょっとの修正で可能な範囲で、その振る舞いを囲うことにします。プロダクトコードを見回すと、CommunicationContext は構造体である必要はなさそう。クラスに変更し、メンバーを virtual にすることにしました:

メンバーが static であることも止めたいですが、それはできません。なぜなら、他の多くのメンバから参照されてしまっているためです。仕方がないので、ここで、Prig を使い間接スタブを作成することにします。Package Manager Console で、以下のコマンドを実行してスタブ設定を作成し・・・:

以下のコマンドで、UpdateJobParameterFile の設定をクリップボードにコピーし、スタブ設定(例えば、GofUntestable.v4.0.30319.v1.0.0.0.prig)に追加します:


上記の準備によって、全てのテストが書けるようになります。例えば、以下のようなケースが書けると思います:

ところで、今回の仕様変更は何だったかというと、「ライセンスによる検証を削除する」というものでした。具体的には、「VerifyProductLicense はもう呼ばれない」ということを実装すれば良いことになります。以下のようにテストの Assert の一部を書き換え、テストが失敗することを確認します:

あとは、テストが通るようにプロダクトコードを変更するだけですね!あなたは安心して、JobManager を以下のように書き換え、テストが成功することを確認します。簡単な仕事でしたね?

・・・アイヤー、これはヒドイ。問題に気づきましたか?これでは常に goto fail; へ行ってしまい、ジョブに渡すパラメータファイルが常に作成されてしまいます!加えて、残念なことに、JobManager は err を code として、UpdateJobParameterFile に 設定していますので、最悪ジョブが常に実行されることになるでしょう。今年 2 月にあった Apple 史上最悪のセキュリティバグが思い出されますね。

このような問題を防止するには、意図しないメソッドが呼び出されたら例外を投げるようにします。この例では、fail ラベルで、問題があった環境の情報をログに書き込む処理があり、Environment の各プロパティを参照しているようです。従って、それらが呼び出された時に例外をスローするのが良いでしょう。Environment は mscorlib に属すクラスで、ほとんどのメンバが static です。この場合、Prig を使って間接スタブを作成する以外に、選択の余地はありませんね:

上のコマンドを実行すると、mscorlib の間接スタブ設定を作成することができます。そうしましたら、Environment の public static なメンバーの間接スタブを作成しましょう(以下のコマンドの結果を mscorlib.v4.0.30319.v4.0.0.0.prig に貼り付けます):

さて、これで Environment の public static なメンバーに対して、一度にデフォルトの振る舞いを変更できるようになりました。テストコードに、以下の変更を加えます:

使用しているモックフレームワークによっては、既に予約されているメンバーがあるため、Environment のいくつかのメンバーを除外することがちょっとメンドクサイ(Moqのケースだと、上記の通り、Environment.CurrentManagedThreadId や Environment.OSVersion がそれに当たります)。ですが、これで意図しないメソッドの呼び出しを監視することができるようになりました。NotifyStartJob に対する上記の修正では、もはやテストが通らないことがわかると思います。これで本当に安全になったわけですね!

ちなみにこの機能、テストケースを書く時、モックのされ具合を確認することや、複雑な条件下にある外部アクセスから守るガードとしても利用ができます。また、IndirectionBehaviors.DefaultValue を設定すれば、デフォルト値を返すようなデフォルトの振る舞いに変更することもできます。レガシーコードにおいては、1 つのメソッドで何度も何度も void Foo() のようなシグネチャのメソッドが呼び出されている状況(中でメンバ変数 or グローバル変数ガシガシ変えてる!!!)も少なくないでしょう。しかし、テスト中のもの以外を何もしない振る舞いに設定すれば、簡単に観点を絞り込むことができるようになるはずです。



移行サンプル:Telerik JustMock によるモック化① final メソッドのモック化 - from "Prig: Open Source Alternative to Microsoft Fakes" Wiki -

$
0
0
まとまってきたドキュメントを日本語の記事にもしておくよシリーズ第 5 段!(元ネタ:Prigwikiより、MIGRATION: Final Mocking by Telerik JustMock。同シリーズの他記事:1234

やっと折り返し地点ですね。今回からは 3 回に渡る JustMock の公式ドキュメントにあるサンプルを実装するシリーズとなります。まずは、「Final Mocking samples」を Prig に移行してみましょう。


以下の記事、ライブラリを使用/参考にさせていただいています。この場を借りてお礼申し上げます m(_ _)m
Using Jekyll with Pages - GitHub Help
Can I share a Microsoft Fakes unit test with a developer using Visual Studio Professional - Stack Overflow
shim a sealed class singleton method and with MS Fakes - Stack Overflow
Search - GitHub Anonymously Hosted DynamicMethods Assembly
API Hooking with MS Detours - CodeProject
Incorrect solution build ordering when using MSBuild.exe - The Visual Studio Blog - Site Home - MSDN Blogs
visual studio 2010 - How do I target a specific .NET project within a Solution using MSBuild from VS2010 - Stack Overflow
Advanced Usage | JustMock Documentation
.net - Is there any free mocking framework that can mock static methods and sealed classes - Stack Overflow
Modern ASP.NET Webskills by @calebjenkins





目次

準備
このサンプルでは、シリーズということもあり、1 つのソリューションに複数のプロジェクトを追加するという構成を採っています。





なお、この例は、間接的に呼び出される対象が、GAC に登録されていない Assembly に含まれており、以前の例(MolesFakes)とは異なることに注意してくださいね。

これまでは Package Manager Console で間接スタブを作成する手順を実施しても問題ありませんでしたが、今後は、PowerShell(コンソール)を使えるようになることを推奨します。Package Manager Console は、仕様上、ネストしたプロンプトをサポートしていません。従って、1 度間接対象の解析を行うために Assembly を読み込むと、2 度とその Assembly を解放できないという問題があるのです。そもそも、いつも使う機能である、オートコンプリート、コマンド履歴なども PowerShell(コンソール)のものより機能的に劣ります。間接設定の追加やテストの実行時は Package Manager Console を使い、Assembly の解析時は PowerShell(コンソール)を使うということが、効率が良いでしょう。

※注※:以下の解説で、PM>で始まるコマンドは Package Manager Console で実行しますが、PS>で始まるコマンドは PowerShell(コンソール)で実行することに注意してください。

FinalMockingMigration をビルド後、出力ディレクトリを開き、以下のコマンドを実行します。プロジェクトがいくつもありますので、対象のテストプロジェクトを Default project: として選択することをお忘れなく:

ちなみに、padd -af は、Add-PrigAssembly -AssemblyFrom のエイリアスです。GAC に登録されていない Assembly の間接スタブ設定を追加する場合、コマンドの引数にフルパスを引き渡す必要があることに注意してください。PowerShell(スクリプト言語)では、コマンド (dir <target file>).FullName でフルファイルパスを取得することができます。FinalMockingMigrationTest に FinalMockingMigration.v4.0.30319.v1.0.0.0.prig が追加されれば成功です。

さて、次は、FinalMockingMigration の出力ディレクトリ上で PowerShell(コンソール)を実行し、Assembly を解析しましょう。Windows 8 を使っているのであれば、エクスプローラで対象のディレクトリを表示中、Alt、F、R のキーコンビネーションが便利ですよね。現在の位置を確認したら、ネストしたプロンプトを開始します:

Package Manager Console で利用が可能になっている種々のコマンド(Find-IndirectionTarget や Get-IndirectionStubSetting など)は、Prig をインストール後、モジュール $(SolutionDir)\packages\Prig.\tools\Urasandesu.Prig に配置されます。なので、それを PowerShell(コンソール)にインポートします:

GAC に登録していない Assembly を読み込みたい場合、System.Reflection.Assembly.LoadFrom(string) にフルパスを引き渡す必要があります。取得には・・・って、ちょっと前にやりましたね (^^ゞ:

Assembly を読み込み後、変数に設定します。ところで、履歴から実行したコマンドを呼び出し、ちょっと変更して再実行するという流れは PowerShell(コンソール)の真骨頂だと思います。Package Manager Console で同じことをしようものなら、重複した履歴を延々と遡る必要があったり、オートコンプリートによる意図しない消去を食らったりすることになるでしょう。ストレスがマッハになること請け合いです (-_-;):

GetTypes で型を確認すると、上から 3 つが対象ということがわかります。それらに対し、前のサンプルと同様のフィルタを掛けてみましょう:

結果が良さそうであれば、間接スタブ設定に変換し、クリップボードにコピーします。なお、解析が終わった Assembly を解放したい場合は、ネストしたプロンプトを終了すれば OK です。

Visual Studio に戻り、FinalMockingMigration.v4.0.30319.v1.0.0.0.prig に間接スタブ設定を貼り付けます。ビルドは通りましたか?それでは、ちょっと長くなりましたが、実際の移行を行っていきましょう!





Assert Final Method Setup
Fakes のサンプルでも言いましたが、Prig は Mock Objectとしての機能を持っていませんので、Moqのような別のモックフレームワークと一緒に使うことを推奨しています。しかし、どうも JustMock のサンプルは、単に Test Stubとしての振る舞いが紹介されているだけのものが大半ですので、Prig のサンプルもそれと一貫性を持たせるようにしました:

はじめに、final メソッドの戻り値を入れ替えてみましょう:

fooProxy.EchoInt32().Body = (@this, arg1) => 10; で、Prig は、その final メソッドを、定数 10 を返す処理に入れ替えています。大丈夫ですよね。次、行ってみましょう!





Assert Property Get
final プロパティを入れ替えるサンプルです:

fooProxy.FooPropGet().Body = @this =>"bar"; で、Prig は、その final プロパティを、定数 bar を返す処理に入れ替えています。どんどん行きますよ (^O^)





Assert Property Set
final プロパティのセッターを検証するサンプルです。これは Moqを使ったほうが簡単そうですね:

Telerik.JustMock.Behavior.Strict は、Moq.MockBehavior.Strict にあたります。従って、Setup で指定した条件を満たさない引数で対象のメンバーを呼び出した場合、MockException をスローするようになります。





Assert Method Overloads
各 final メソッドのオーバーロードを入れ替えてみましょう。間接スタブ設定を正しく追加していれば、特に迷うことは無いはずです:

特に問題は無いですよね?次に進みましょう。





Assert Method Callbacks
イベントとしてのコールバックを置換することを説明する例です。Moles の例で紹介しましたが、実装には若干のテクニックが必要です:

実装は以下の流れになります:
  1. fooProxy.AddOnEchoCallbackEchoEventHandler().Body = (@this, value) => handler += value; のように、入れ替えたいイベントの += 演算子を乗っ取り、渡されたハンドラ(value)をテストのためのデリゲート(handler)に紐付ける。
  2. 元のイベントを発行したいタイミングで、手順 1 で紐づけたデリゲート(handler)を代わりに実行する。
これで、元のイベントを発火することと同じ効果を得ることができるようになります。





Assert Generic Types and Methods
ジェネリックな型とメソッドを入れ替える例です:

ね、簡単でしょう?これに関しては、以前の記事で解説したジェネリックのサポートも、合わせてご覧いただければと思います。



移行サンプル:Telerik JustMock によるモック化② シールされたクラスのモック化 - from "Prig: Open Source Alternative to Microsoft Fakes" Wiki -

$
0
0
まとまってきたドキュメントを日本語の記事にもしておくよシリーズ第 6 段!(元ネタ:Prigwikiより、MIGRATION: Sealed Mocking by Telerik JustMock。同シリーズの他記事:12345

3 回に渡る JustMock の公式ドキュメントにあるサンプルを実装するシリーズ、Part 2 となります。今回は、「Sealed Mocking samples」を Prig に移行してみましょう。


以下の記事、ライブラリを使用/参考にさせていただいています。この場を借りてお礼申し上げます m(_ _)m
Unity を使って AOP - present
How do microsoft fakes' shims actually work internally? - Stack Overflow
c# - Good and free unit-testing alternatives to Telerik's JustMock - Software Recommendations Stack Exchange
Unable to create Fakes for Google APIs - Stack Overflow
Covering basics of unit testing with Typemock - .NET Unit Testing Tips
The Difference Between Unit Tests and Integration Tests - Typemock Isolator
Typemock Isolator Quick Start -
Home - Run Jekyll on Windows
Setup Jekyll on Windows - Yi Zeng
OctopressをWindows7にインストールしてみたメモ by @pon_zu on @Qiita





目次

準備
このサンプルでは、シリーズということもあり、1 つのソリューションに複数のプロジェクトを追加するという構成を採っています。前回のサンプルと同様、Package Manager Console と PowerShell を使って説明を続けたいと思います。各使用コマンドは前回のサンプルで解説していますので、詳細はそちらもご覧ください。

まずは、間接スタブ設定を作成しましょう:

あー・・・「やっべ!Default project: にテストプロジェクト設定してねえ!変なとこに設定が追加された!!!!」って方。

お気の毒さまですが、Prig はそれを削除するコマンドをサポートしていません(PreBuildEvent 内の解析ェ・・・)。恐縮ですが、手動で *.csproj を元に戻す必要があります。以下のイメージのように、\.prig"にマッチするタグ Reference、None、PreBuildEvent の全てを削除してください:









Git のようなバージョン管理システム下で作業することを強くお勧めしておきます (^q^)

次は、PowerShell(コンソール)による対象メソッドの絞り込みです。慣れれば一気にできるでしょう:

今回は、Moles の例以降、表に出てこなかったコマンド pfind(Find-IndirectionTarget)を使っています。実はこのコマンド、前回までのサンプルで、標準リフレクション API によって掛けていたざっくりとしたフィルタを、既に含んでいます。なので、上記のように手順の簡易化ができるようになるんですね。そうしたら、Visual Studio に戻り、前回と同様 SealedMockingMigration.v4.0.30319.v1.0.0.0.prig に貼り付けます。ビルドは通りましたか?さあ、実際の移行をしてみましょう!





Assert Final Method Call on a Sealed Class
シールされたクラスの final メソッドを置き換えてみましょう:

特定のインスタンスだけの置き換えるのでない時は、PProxy<original class name> を使うより、P<original class name> を使うほうが簡単かも。





Create Mock for Sealed Class with Internal Constructor
ところで、入れ替える型のコンストラクタが非 public だとどうなるのか、心配になったりします?:

はい!何も問題ありませんね!





Create Mock for Sealed Class with Interface
これはインターフェイスを使った振る舞いの例です。まずは直呼び出し:

次に、インターフェイスを通じた呼び出し:

加えて、明示的に実装されたインターフェイスのケースです。これについては、JustMock はサンプルを準備していないようですね。なので、一応念のため説明させていただきます:

明示的に実装されたインターフェースを入れ替える時は、間接スタブが特殊な名前になることだけに気を付ければ大丈夫です。通常の名前は、単に <メソッドの間接スタブ名> ですが、この場合の名前は、<名前空間> + <インターフェイスの間接スタブ名> + <メソッドの間接スタブ名> となります。



移行サンプル:Telerik JustMock によるモック化③ 静的メソッドのモック化 - from "Prig: Open Source Alternative to Microsoft Fakes" Wiki -

$
0
0
まとまってきたドキュメントを日本語の記事にもしておくよシリーズ第 7 段!(元ネタ:Prigwikiより、MIGRATION: Static Mocking by Telerik JustMock。同シリーズの他記事:123456

3 回に渡る JustMock の公式ドキュメントにあるサンプルを実装するシリーズ、Part 3 となります。最後は、「Static Mocking samples」を Prig に移行してみましょう。


以下の記事、ライブラリを使用/参考にさせていただいています。この場を借りてお礼申し上げます m(_ _)m
How often are fakes assemblies generated? - Stack Overflow
Brownfield Development: Taming Legacy Code with Better Unit Testing and Microsoft Fakes
Nested Types in Generic Classes - Haibo Luo's weblog - MSDN Blogs
c# - Behavedbase in fakes - Stack Overflow
CLR Profiler - Documentation
Resize image in the wiki of github usin markdown - Stack Overflow
Behind iPhone's Critical Security Bug, a Single Bad 'Goto' WIRED
microsoft fakes only stub static property of a static class - Stack Overflow
Home Page - Code Impact - .NET Community Event
PowerShell で SIGPIPE 連鎖 - NyaRuRuが地球にいたころ
Generics and Your Profiler - David Broman's CLR Profiling API Blog - Site Home - MSDN Blogs





目次

準備
このサンプルでは、シリーズということもあり、1 つのソリューションに複数のプロジェクトを追加するという構成を採っています(前回前々回の記事もご参照くださいませ)。これまでのサンプルと同様、Package Manager Console と PowerShell を使って説明を続けたいと思います。各使用コマンドは以前のサンプルで解説していますので、詳細はそちらもご覧ください。

さて、間接スタブ設定を作成しましょう。Package Manager Console を開き、Default project: をテストプロジェクトに変更します

次に、Assembly を解析します。PowerShell(コンソール)を開き、以下のコマンドで情報を取得します:

Visual Studio に戻り、各間接設定 StaticMockingMigration.v4.0.30319.v1.0.0.0.prig、System.Web.v4.0.30319.v4.0.0.0.prig に貼り付けます。ビルドは通りましたか?さあ、実際に移行してみましょう!





Static Constructor Mocking
静的コンストラクタの間接スタブは、文字通り StaticConstructor という名前になります:






General Static Method Mocking
一般的な静的メソッドのモック化です。んー、これは Moqと連携したほうがわかりやすいでしょうね:

ところで、個人的には ExpectedException より Assert.Throws のほうが好きだったり。ExpectedException を使うケースだと、意図しない場所で例外が発生しても(例えば、上記の例だと、Foo.Execute(10) が例外をスロー時)、テストは成功してしまいますからね。





Mocking Static Property Get
静的プロパティの getter を入れ替えます。何度か説明してきているように思いますが・・・一応紹介しておきましょう (^_^;)






Mocking Static Property Set
静的プロパティの setter を入れ替えます:

検証の方法について、JustMock のサンプルとは若干違いがあります。個人的には、MockBehavior.Strict を指定すれば、条件によってメソッドが呼び出されているかどうかを再度検証する必要はないとは思いますね。条件を満たさないメソッドの呼び出しがあれば、自動的に例外がスローされますので。





Mocking Internal Static Call
internal な静的メソッドを入れ替える例です。コメントに残すだけでなく、例外がスローされないことも検証すべきでしょう:






Mocking Static Class
静的クラスのメソッドを入れ替えるサンプルです。説明すべきことはあまりないですね (^-^;






Mocking Current HttpContext
現在の HTTP コンテキストを入れ替えるサンプルです。本来であれば、元の処理は HTTP リクエスト中にのみ有効なのですが、もはや制限はありません。






Mocking Extension Methods
拡張メソッドの入れ替えです。結局のところ、拡張メソッドは静的メソッドですので、前までのサンプルと同じように入れ替えることができます:




移行サンプル:Typemock Isolator による MessageBox を使うテストのモック化 - from "Prig: Open Source Alternative to Microsoft Fakes" Wiki -

$
0
0
まとまってきたドキュメントを日本語の記事にもしておくよシリーズ第 8 段!(元ネタ:Prigwikiより、MIGRATION: Test using MessageBox by Typemock Isolator。同シリーズの他記事:1234567

このシリーズも残り僅か。今回もよろしくお願いします。Typemock は、Isolator の Quick Startで、MessageBox をモックに入れ替えるサンプルを紹介しています。これも Prig(と Moq)に移行することが可能です。


以下の記事、ライブラリを使用/参考にさせていただいています。この場を借りてお礼申し上げます m(_ _)m
Testing code that rely on Microsoft Azure Management Libraries using Microsoft Fakes
c# - The type is defined in an assembly that is not referenced, how to find the cause - Stack Overflow
.net - Mircosoft fakes - shims without ShimsContext - Stack Overflow
Unit Test for ShimDataTableCollection Count
c# - How to know if a MemberInfo is an explicit implementation of a property - Stack Overflow
#5816 (any_range requires copyable elements) – Boost C++ Libraries
#10360 (Since 1.56, any_range use static cast of reference instead of implicit conversion) – Boost C++ Libraries
#10493 (Since 1.56, any_range with non-reference references can cause UB) – Boost C++ Libraries
hunting bugs with git bisect and submodules - Least Significant Bit
AdventCalendar - git bisect で問題箇所を特定する - Qiita
便利!電動歯ブラシ | Boost.勉強会 #16 大阪





目次

準備
まずは、間接設定を作成する必要があります。Package Manager Console を開き、Default project: をテストプロジェクトに変更してください。その後、以下のコマンドを実行します:

次に、Isolator のサンプルで使用しているメソッドのための間接設定を取得しましょう。PowerShell(コンソール)を開き、情報を取得するために以下のコマンドを実行します:

Visual Studio に戻り、IsolatorMigrationDemo.v4.0.30319.v1.0.0.0.prig と System.Windows.Forms.v4.0.30319.v4.0.0.0.prig に各々の間接設定を貼り付けます。ビルドが成功したら、サンプルを移行していきますよ!





Example Test 1 - Simple test using MessageBox
Isolatorは、プロファイリング API による強力なメソッドの入れ替え機能に加え、JustMockと同様、Mock Objectを生成する機能を持っています。Prig はそのような機能をサポートしていませんが、最初に説明した通りMoqと連携することで、それを実現することができましたね。

Isolate.WhenCalled は、Prig の間接スタブ(この場合、PMessageBox.ShowString().Body)に、Moq.Mock.Setup でセットアップした Mock Object を割り当てることで、置き換えることができます。Isolate.Verify.WasCalledWithExactArguments は、Moq.Mock.Verify と機能的に同じですね。問題は無いでしょう。次へ行きますよ!





Example Test 2 - Complex Test
「複雑な」と付いていますが、そう難しいものではありません ( ̄ー ̄)

特別な条件が無いのであれば、Isolate.WhenCalled(..).WillThrow は、Prig の間接スタブ(この場合、PSomeClass.MyMethod().Body)に、直接例外をスローする関数を割り当てることで、置き換えることができます。Isolate.WhenCalled(..).WillReturn や Isolate.Verify.WasCalledWithExactArguments は、前に説明しましたので・・・おっと、これで全部です!

ちなみに、対象が、MessageBox 処理があるにも関わらずテストコードを書きたくなるほど複雑な場合、設計をしくじっている可能性が高いと、個人的には思います。既存のコードやレガシーコードに対しては仕方がないでしょうが、こんなライブラリのような闇の力を、新規のプロダクトコードには使わなくて済むことを願いたいものですね (^^ゞ



非公開メソッドの入れ替え - from "Prig: Open Source Alternative to Microsoft Fakes" Wiki -

$
0
0
ソフトウェアテストあどべんとかれんだー2014 8日目!&まとまってきたドキュメントを日本語の記事にもしておくよシリーズ第 9 段!(元ネタ:Prigwikiより、FEATURES: Non Public Method Replacement。同シリーズの他記事:12345678

はじめましての方ははじめまして!ソフトウェアテストあどべんとかれんだー2014 8日目を担当させていただきます、@urasandesuこと杉浦と申します。

前日は、@hayabusa333さんの、Ruby - Gauntltによるセキュリティテスト #SWTestAdvent - Qiitaでしたね。セキュリティテスト自動化フレームワーク、そんなものもあるのだと興味深く拝読させていただきました。2014 年は、Heartbleedや、Apache Struts の脆弱性ShellshockPOODLEと、脆弱性の話題に事欠かない年になってしまいましたが、今後セキュリティに関するテストはますます重要になっていくんでしょうね。

さて、ソフトウェアテストに関連することということで、私からは打って変わって実装寄りのお話を。以前から私が作成しています Prigという、.NET 自動ユニットテスト向け迂廻路生成ライブラリについて、紹介させていただこうと思います。迂廻路生成?と思われるかもしれませんが、簡単に言うと、通常は行えない static メソッドや private メソッドの上書きをできるようにするというものです(.NET だと Microsoft Fakes、Java だと JMockitとかが有名どころでしょうか)。

題材は「既存ライブラリの非公開メソッドが絡む自動ユニットテスト」。言語は C# です。よく言われる通り、非公開なメソッドそのものをテストすることはよくないこととされていますが、テストで非公開なメソッドに対して何かしたくなることは、しばしばあるんじゃないでしょうか?例えば、テスト対象のメソッド内で使われる private な setter を持つプロパティに意味のある値を与えておきたいWeb にアクセスしにいってしまう private メソッドをモックに入れ替えたい、などなど。まあ、今まさに開発中のコンポーネントであれば、いくらでも対処方法はあるのですが、すでに稼働しているシステムだったり、外部から買い入れたコンポーネントだったりすると、途端に難易度が跳ね上がるのが困りもの。

ところで、C# の特徴的な機能の 1 つに、今から 7 年ほど前に出た C# 3.0 で追加された、拡張メソッドという機能があります。皆さんは拡張メソッドは好きですか?乱用するべきではないですが、その機能が、本質的に、そのライブラリがそのレイヤーでサポートしてほしいものであれば、設計上自然な API を実現できることがあるかと、私は思います。ただし、そのライブラリが、そのような拡張に対してオープンであるかどうかは、場合によるでしょう。特に、そのシグネチャに、非公開な属性の 1 つである internal なクラスが現れるようなメソッドが関係する場合は要注意。今回は、Prig によって、どのようにこの問題を解決するかを解説したいと思います。


以下の記事、ライブラリを使用/参考にさせていただいています。この場を借りてお礼申し上げます m(_ _)m
How to mock ConfigurationManager.AppSettings with moq - Stack Overflow
TDD, Unit testing and Microsoft Fakes with Sitecore Solutions
Basic mocking techniques - Stack Overflow
Hybrid Framework - http://our.umbraco.org
Unit testing Umbraco 7 | just this guy
How to Write 3v1L, Untestable Code
Generic Methods Implementation in Microsoft Fakes - CodeProject
Paulo Morgado - Mastering Expression Trees With .NET Reflector
Expression Tree Visualizer for VS 2010 - Home
mocking - Using Microsoft Fakes Framework with VSTO Application-Level Add-in an XML based Ribbon - Stack Overflow





目次

非公開メソッドの入れ替え
既存ライブラリに、以下のような DTO 群があるとしましょう:

「DB への接続」という副作用と、「テーブル自体のデータ」という状態を 1 つのクラスで管理しており、嫌な臭いを感じます。ただ、このライブラリを作ったニンゲンが、これ以上のリファクタリングをするモチベーションを持つことはないかもしれません。なぜならば、このライブラリだけ見れば internal なクラスが緩衝材としてあり、InternalsVisibleToAttributeを使えば、制限なくそのクラスにアクセスができるため、テストをするのに特に問題を感じないでしょうから。

さて、このライブラリはテーブルスキーマの自動生成ツールも提供しており、特定の列を自動生成してくれます。そのような列は、以下のような規約で命名されるとのことです:
  • <table name> + _ID ・・・ プライマリキー
  • DELETED ・・・ 論理削除フラグ
  • CREATED ・・・ 作成日時
  • MODIFIED ・・・ 更新日時
なるほどなるほど。そうすると「自動生成された列だけを取得する」や「手動で生成された列だけを取得する」などのようなことがやりたくなりますね。残念ながら、既存のライブラリは、そのような機能を提供していないとのこと。なので、今回は自分で作成することにしました。こんなシチュエーションでは拡張メソッドがピッタリでしょう。テストを書いてみます:

テーブル USER には、列 USER_ID、PASSWORD、USER_NAME、DELETED、CREATED、MODIFIED があるとします。そのテーブルに対し、拡張メソッド GetAutoGeneratedColumns を実行すると、自動生成された列が取得できるという寸法です。このままではビルドすら通りませんので、とりあえず以下のような最低限の実装を用意しました:

ほい、実行っと。NotImplementedException がスローされるでしょうから、とりあえずなコードを追記し・・・て・・・あれ?

ヴァー!変なとこで引っかかっとる!! ('A`)

実は、スタックトレースにも出力されているように、ULColumns は、メソッド ValidateState を使うことによって、列が変更可能かどうかを検証しています。テストケースにおいては、GetAutoGeneratedColumns を検証したいのですが、その前、users.Columns.Add(expected[0]); で例外がスローされていたわけですね。これはいけません・・・。

このような状況で、Prig を使うことで、不必要な検証を一時的に外すことができます。Prig をインストールし、その Assembly のスタブ設定を追加します:

以下のコマンドを実行し、ULColumns の設定をクリップボードにコピーします:

そうしましたら、追加されたスタブ設定ファイル(例:UntestableLibrary.v4.0.30319.v1.0.0.0.prig)にそれを貼り付け、ソリューションをビルドします:

上の準備が終わったら、以下のようにテストを書き直すことができるようになります:

こんどはどうでしょう?

よし!今度は NotImplementedException がスローされるという、意図した結果になりました。後は、テストを通すコードを書き、全てのテストが通ったらリファクタリングをし、新たなテストを追加する、という黄金の回転を回すだけ。良い感じじゃないですか?





付録
ところで、かの人が元のライブラリをどのように設計していれば、もっと簡単にテストができていたと思いますか?そもそも、状態を副作用から分離したライブラリとして再設計すべきだとは思いますが、既存のライブラリで、インターフェイスを変更するような再設計は難しいでしょうね。せめてなにかできることがあるとすれば、非 public なインターフェイスを public にするような変更ぐらいでしょう。

なお、次の点には注意してください:単にインターフェイスを公開する、例えば、ULTableStatus の全てのフィールドと、ULColumns のコンストラクタ .ctor(ULTableStatus) を公開するようなことをしてしまえば、簡単にデータの不整合が起きるようになってしまい、ライブラリが安全ではなくなってしまいます。ライブラリの安全性が保たれる範囲のインターフェイスだけを公開するべきでしょう。このケースでは、以下のような変更が考えられます:

ULColumns のコンストラクタは公開しましたが、ULTableStatus を直接指定することはせず、検証のためのメソッドだけを持ったインターフェイス IValidationを代わりに指定するようにしました。Prig を使って入れ替えたかったメソッド ValidateState が持つ機能を、外出ししたことになります。対象のメソッドは状態にアクセスはしますが、それを書き換えることはしません。従って、その部分を公開するだけであれば、ライブラリのデータの整合性は保ち続けられることになります。それから、ULTable の ULColumns を生成するプロパティを virtual 化します。

これらの再設計により、Prig を使わなければキーとなるメソッドに到達することすら難しかったテストは、以下のようにできます。Moqのような通常のモックフレームワークで、簡単にテストができるようになるのです:





終わりに
ソフトウェアテストあどべんとかれんだー2014 8日目、C# と自作ライブラリを題材に、自動ユニットテストにおける非公開メソッドの入れ替えを解説してみました。つい先月、V1.0.0 をリリースしたばかりということもあり、まだまだ問題もあるかと思いますが、もし興味を持っていただき、使っていただければ嬉しいです!問題などあれば、是非 @urasandesu宛てにお気軽に mention 下さいませ (((o(*゚▽゚*)o)))

さて、私のまとまってきたドキュメントを日本語の記事にもしておくよシリーズ(12345678)はこれで終わりですが、ソフトウェアテストあどべんとかれんだー2014はまだまだ続きますのでお見逃しなく!

明日は、@PoohSunnyさん。よろしくどうぞ!!

C# 魔改造 01 - Wish List(or Design Goal) -

$
0
0
ソフトウェアって思ったより"ソフト"じゃないじゃない?って思ったのが発端。

結果がわからないものや、とりあえず反応が見てみたいものはプロトタイピングしながら作っていくことが少なくないと思うけど、さて動くものができたって段階になると作り直すのもあれだし…って感情がたぶん湧く。お金掛けて作った(作ってもらった)ものは特に。こんな時に相談された場合には必ず「作り直したほうがいいものができる」って思うと思う。でも時間やお金の問題、色んな状況や立場の人がいらっしゃる場所では、完全にゼロからやり直すのは難しい。

プログラム作る時、テストを書く人が増えてきていると思う。納品物として、ユニットテストのソースコードも指定するお客さんがいたり、オープンソースのコードを読もうとすると大抵付いてたり。TDD って言葉も知ってる人が多いみたい。テストを書いておけば、特に最初の立ち上げが有利になるし、変更があった場合もデグレが発見されやすい。適当な場所にブレークを張って動かせるのは、動作の理解にも役に立つ。良いことが多いのだけれど、良いテストを書くにはたくさん訓練しなければならないし、最初から条件を揃えるのはやっぱり試行錯誤が必要になる。

経緯はこんな感じです。ソフトウェアの動作を安全にかつ根本的に変える治具?のようなライブラリがあってもいいんじゃないかと思いましたので、とりあえず今あるアイディア、吐き出してみました。



以下の記事/ライブラリを使用/参考にさせていただいてます。ありがたく使わせていただきますm(_ _)m
CodeDom Assistant - CodeProject
 http://www.codeproject.com/KB/cs/codedom_assistant.aspx
CodeDOM, <providerOption name="CompilerVersion" value="3.5" /> not working?! and how to retrieve providerOption at runtime?
 http://social.msdn.microsoft.com/Forums/en-US/netfxbcl/thread/512d9fdd-61af-4a0c-b78a-2f88738e651a
Creating and Initializing Objects in CodeDom [Benet Devereux] - BCL Team Blog - Site Home - MSDN Blogs
 http://blogs.msdn.com/b/bclteam/archive/2006/04/10/571096.aspx
DynamicProxy :: Castle Project
 http://www.castleproject.org/dynamicproxy/index.html
LINQ Expression Trees-Lambdas to CodeDom Conversion | Coding Day
 http://www.codingday.com/meta-programming-with-expression-trees-lambdas-to-codedom-conversion/
moq - Project Hosting on Google Code
 http://code.google.com/p/moq/
C# 3.0 Supplemental Library: Achiral - NyaRuRuの日記
 http://d.hatena.ne.jp/NyaRuRu/20080115/p1
NUnit - Home
 http://www.nunit.org/index.php?p=home
ECMA C# and Common Language Infrastructure Standards
 http://msdn.microsoft.com/en-us/netframework/aa569283.aspx
Cecil - Mono
 http://www.mono-project.com/Cecil
.NET Reflector, class browser, analyzer and decompiler for .NET
 http://www.red-gate.com/products/reflector/
Reflexil | Download Reflexil software for free at SourceForge.net
 http://sourceforge.net/projects/reflexil/
TomCarter.Developer - DSM Plugin for Reflector.NET
 http://tcdev.free.fr/
SequenceViz
 http://sequenceviz.codeplex.com/wikipage?title=ReflectorPlugin&ProjectName=sequenceviz
.NET Reflector Add-Ins ReflectionEmitLanguage
 http://reflectoraddins.codeplex.com/wikipage?title=ReflectionEmitLanguage&referringTitle=Home
Seasar.NET プロジェクト
 http://s2container.net.seasar.org/ja/seasarnet.html



1. 式木 + CIL - ExpressiveILProcessor -
式木がせっかく強力なので、もう少し微調整ができるとうれしいと思いついた、式木の中身を CIL に展開してくれるクラス。System.Reflection.Emit 名前空間や Mono.Cecil.Cil 名前空間のクラスと組み合わせて強力な縁の下の力持ちになってくれるはず。

ExpressiveILProcessor の利用イメージ
Program.cs
呼び側のイメージ。

using System;
using System.Reflection.Emit;
using Mono.Cecil;
using Urasandesu.NAnonym.CREUtilities;
using MC = Mono.Cecil;
using SR = System.Reflection;

namespace Test
{
class Program
{
static void Main()
{
// ToTypeDef は System.Type から Mono.Cecil.TypeDefinition に変換する拡張メソッド。
var mainDef = typeof(Program).ToTypeDef();

var testDef = new MethodDefinition("Test",
MC.MethodAttributes.Private | MC.MethodAttributes.Static, mainDef.Module.Import(typeof(void)));
mainDef.Methods.Add(testDef);

// 式木で直接 Emit。
var egen = new ExpressiveILProcessor(testDef);
egen.Emit(_ => Console.WriteLine("aiueo"));

// .NET 3.5 までだと変数の宣言や代入式無理。
// → ExpressiveILProcessor 自身のメソッドを定義。
int a = 0;
egen.Emit(_ => _.Addloc(() => a, default(int)));
egen.Emit(_ => _.Stloc(() => a, 100));

// ローカル変数に入れた上ににアクセスする場合。
var cachedAnonymousMethod = default(DynamicMethod);
var gen = default(ILGenerator);
var label27 = default(Label);
egen.Emit(_ => _.Addloc(() => cachedAnonymousMethod,
new DynamicMethod("cachedAnonymousMethod", typeof(string), new Type[] { typeof(string) }, true)));
egen.Emit(_ => _.Addloc(() => gen, _.Ldloc(() => cachedAnonymousMethod).GetILGenerator()));
egen.Emit(_ => _.Addloc(() => label27, _.Ldloc(() => gen).DefineLabel()));
egen.Emit(_ => _.Ldloc(() => gen).Emit(SR.Emit.OpCodes.Brtrue_S, _.Ldloc(() => label27)));

// 通常の Emit 処理混合。
egen.Emit(_ => _.Direct.Emit(MC.Cil.OpCodes.Ldc_I4_S, (sbyte)100));
egen.Emit(_ => _.Direct.Emit(MC.Cil.OpCodes.Stloc, _.Locals(() => a)));
}
}
}





2. Java の便利を C# にも - LocalClass -
interface を受け取るメソッドと匿名~が相性悪いと思いついた。Java のローカルクラスをイメージ。moqが近かったのだけれど、所詮は式木だったので (^_^;)。.NET 4.0 になればもう少し多機能化されるのかも。

LocalClass の利用イメージ
Program.cs
呼び側のイメージ。

using System;
using Urasandesu.NAnonym.DI;

namespace Test
{
public class Program
{
static void Main()
{
var localClass = new LocalClass<IHoge>();
// Setup で中身を編集。
localClass.Setup(the =>
{
// メソッドの設定。中身を定義し、Override で同じ I/F を持つメソッドをオーバーライド。
the.Method(() =>
{
if (DateTime.Now < new DateTime(2010, 1, 1))
{
Console.WriteLine("こんにちは!世界!");
}
else
{
Console.WriteLine("Hello, World!!");
}
})
.Override(_ => _.Output);

the.Method(() =>
{
return "Hello, Local Class !!";
})
.Override(_ => _.Print);

the.Method((string content) =>
{
return "Hello, " + content + " World !!";
})
.Override(_ => _.Print);



// プロパティの設定。中身を定義し、Override で同じ I/F を持つプロパティをオーバーライド。
int this_value = 0;
the.Property(() =>
{
return this_value;
})
.Override(_ => () => _.Value);

the.Property((int value) =>
{
this_value = value * 2;
})
.Override(_ => value => _.Value = value);
});

// Load でアセンブリ生成。キャッシュ。
localClass.Load();

// New でインスタンス化。
var hoge = localClass.New();

// 実行
hoge.Value = 10;
Console.WriteLine(hoge.Value);
Console.WriteLine(hoge.Print());
Console.WriteLine(hoge.Print("Local Class"));
/*
* 20
* Hello, Local Class !!
* Hello, Local Class World !!
*/
}
}

// 対象のインターフェース
interface IHoge
{
int Value { get; set; }
void Output();
string Print();
string Print(string content);
}
}




3. Load Time Weaving - GlobalClass -
これが一番核になると思う。I/F は LocalClass のそれを踏襲。.NET Framework はアセンブリの厳密名があるので、そこまで自由には Weaving できないけど、変更がだいぶ気前良くできるようになるはず。

GlobalClass の利用イメージ
Class1.cs
元のクラス 1

namespace Test
{
public class Class1
{
public string Print(string value)
{
return "Hello, " + new Class2().Print(value) + " World !!";
}
}
}



Class2.cs
元のクラス 2

namespace Test
{
public class Class2
{
public string Print(string value)
{
return "こんにちは、" + value + " 世界!";
}
}
}



GlobalClass1.cs
元のクラス 1を Weaving する設定。別 Assembly にする必要がありそう。

using Test.Urasandesu.NAnonym.Etc;
using Urasandesu.NAnonym.DI;

namespace Test
{
public class GlobalClass1 : GlobalClassBase // AppDomain を超える必要があるため、GlobalClassBase は MarshalByRefObjectを継承。
{
protected override GlobalClassBase SetUp()
{
var class1 = new GlobalClass<Class1>();
class1.SetUp(the =>
{
the.Method((string value) =>
{
return "Modified prefix at Class1.Print" + new Class2().Print(value) + "Modified suffix at Class1.Print";
})
.Instead(_ => _.Print);
});
class1.Load();
return class1;
}

private string NewPrint(string value)
{
return "Modified prefix at Class1.Print" + new Class2().Print(value) + "Modified suffix at Class1.Print";
}
}
}



GlobalClass2.cs
元のクラス 2を Weaving する設定。やはり別 Assembly にする必要がありそう。

using Test.Urasandesu.NAnonym.Etc;
using Urasandesu.NAnonym.DI;

namespace Test
{
public class GlobalClass2 : GlobalClassBase // AppDomain を超える必要があるため、GlobalClassBase は MarshalByRefObjectを継承。
{
protected override GlobalClassBase SetUp()
{
var class2 = new GlobalClass<Class2>();
class2.SetUp(the =>
{
the.Method((string value) =>
{
return "Modified prefix at Class2.Print" + value + "Modified suffix at Class2.Print";
})
.Instead(_ => _.Print);
});
return class2;
}
}
}



Program.cs
呼び側のイメージ。

using System;
using NUnit.Framework;
using Test.Urasandesu.NAnonym.DI;
using Test.Urasandesu.NAnonym.Etc;
using Urasandesu.NAnonym.DI;

namespace Test
{
public class Program
{
static Program()
{
// Inject、AcceptChanges は System.AppDomain の拡張メソッド。
// 新しい AppDomain を作成し、Load Time Weaving を行った後、元ファイルを入れ替える。
// ジェネリックパラメータに Weaving 設定用のクラスを指定。
// ※元のファイルが Load されるより先に実行させる必要がある。
AppDomain.CurrentDomain.Inject<GlobalClass1>();
AppDomain.CurrentDomain.Inject<GlobalClass2>();
AppDomain.CurrentDomain.AcceptChanges();
}

static void Main()
{
var class1 = new Class1();
var class2 = new Class2();

// 実行
string value = "aiueo";
Console.WriteLine(class1.Print(value));
Console.WriteLine(class2.Print(value));
/*
* Modified prefix at Class1.PrintModified prefix at Class2.Print aiueo Modified suffix at Class2.PrintModified suffix at Class1.Print
* Modified prefix at Class2.Print aiueo Modified suffix at Class2.Print
*/
}
}
}





作っていくうちに必要になったユーティリティもいっしょに公開していく予定。今年中には 1 パス動くといいな ...( = =)

C# 魔改造 02 - 言語内 DSL -

$
0
0
書き易さと読み易さのせめぎ合い?

登場が 2001 年だから、C# はもうすぐ 10 周年なのだなーと振り返りつつ。

大目標は相互運用。初めからマネージド/アンマネージドコードが混じり合えたし、P/Invoke で API の直叩きも簡単。COM とのやり取りも RCW が良しなにしてくれる。CLS に則っていさえすれば、VB .NET や C++/CLI で書かれたクラスやメソッドも何の苦労も無く利用できる。

2.0 ではコンパイル時埋め込み系の Generics 導入。Nullable 型で 値型か null を扱えるようになった。partial クラスで自動生成向けの部分と人が書く部分を別ファイルで扱えるように。あと匿名メソッドも。

3.0 ではラムダ式や拡張メソッドが扱えるようになって、関数型言語のパラダイムも取り込んでみたり。型推論や匿名型も入った。LINQ?なにそれおいしいの?

4.0 になると dynamic 型で Python や Ruby みたいな動的型付言語とも簡単に繋がるようになる。COM とのやり取りもさらに便利になった。PLINQ?もう for 文書いたら負けなのかも…。

こんな感じでなかなか盛りだくさんな言語仕様になってます (^_^;)。相互運用という大目標と、並列コンピューティングへの対応やむなしで、公式で今後も順次魔改造されていくはず。
なので、せめてライブラリ側で統一感を出したり、紋切り型の処理を簡単に呼び出せると良いかな?と。いくつかサンプル、並べてみました。



以下の記事/ライブラリを使用/参考にさせていただいてます。ありがたく使わせていただきますm(_ _)m
moq - Project Hosting on Google Code
 http://code.google.com/p/moq/
DynamicProxy :: Castle Project
 http://www.castleproject.org/dynamicproxy/index.html
C# 3.0 Supplemental Library: Achiral - NyaRuRuの日記
 http://d.hatena.ne.jp/NyaRuRu/20080115/p1
takeshik's linx at master - GitHub
 http://github.com/takeshik/linx
進化するアーキテクチャーと新方式の設計: 流れるようなインターフェース
 http://www.ibm.com/developerworks/jp/java/library/j-eaed14/?ca=drs-jp
プログラミング言語 Scala Wiki - トップページ
 http://www29.atwiki.jp/tmiya/pages/1.html



1. 3 項演算子(?: 演算子)
まずは null チェックして…って処理が毎回出てくるので。

NotDefault
サンプル

using System;
using System.Collections.Generic;

namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
// null チェックして null の場合はデフォルト値を返すバージョン。
Console.WriteLine("Value1: {0}", new A().NotDefault(a => a.B).NotDefault(b => b.Value));
Console.WriteLine("Value2: {0}", new A(new B()).NotDefault(a => a.B).NotDefault(b => b.Value));
Console.WriteLine("Value3: {0}", new A(new B(10)).NotDefault(a => a.B).NotDefault(b => b.Value));
/* 実行結果
*
* Value1: 0
* Value2: 0
* Value3: 10
*
*/


// null チェックして null の場合は何もしないバージョン。
new A().NotDefault(a => a.B).NotDefault(b => Console.WriteLine("Value1: {0}", b.Value));
new A(new B()).NotDefault(a => a.B).NotDefault(b => Console.WriteLine("Value2: {0}", b.Value));
new A(new B(10)).NotDefault(a => a.B).NotDefault(b => Console.WriteLine("Value3: {0}", b.Value));
/* 実行結果
*
* Value2: 0
* Value3: 10
*
*/
}
}

class A
{
public A() { }
public A(B b) { B = b; }
public B B { get; set; }
}

class B
{
public B() { }
public B(int value) { Value = value; }
public int Value { get; set; }
}

public static class My
{
public static S NotDefault<T, S>(this T obj, Func<T, S> f)
{
return EqualityComparer<T>.Default.Equals(obj, default(T)) ? default(S) : f(obj);
}

public static void NotDefault<T>(this T obj, Action<T> a)
{
if (!EqualityComparer<T>.Default.Equals(obj, default(T))) a(obj);
}
}
}





2. 複数の as キャスト
連続で as キャストしながら条件分岐する場合、使わない変数のスコープが有効になってしまうので。

As
サンプル

using System;

namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
NormalAs(4);
NormalAs(4d);
NormalAs("4");
try
{
NormalAs(4m);
}
catch (NotSupportedException e)
{
Console.WriteLine(e.Message);
}
/* 実行結果
*
* Result: 16
* Result: 8
* Result: Hello, 4
* 指定されたメソッドはサポートされていません。
*
*/


MyAs(4);
MyAs(4d);
MyAs("4");
try
{
MyAs(4m);
}
catch (NotSupportedException e)
{
Console.WriteLine(e.Message);
}
/* 実行結果
*
* Result: 16
* Result: 8
* Result: Hello, 4
* 指定されたメソッドはサポートされていません。
*
*/
}

static void NormalAs(object o)
{
// 通常版。
var ni = default(int?);
var nd = default(double?);
var s = default(string);
if ((ni = o as int?) != null)
{
Console.WriteLine("Result: {0}", ni * ni);
}
else if ((nd = o as double?) != null)
{
Console.WriteLine("Result: {0}", nd + nd);
}
else if ((s = o as string) != null)
{
Console.WriteLine("Result: {0}", "Hello, " + s);
}
else
{
throw new NotSupportedException();
}
}

static void MyAs(object o)
{
// DSL 版。
o.
As<int?>().Do(ni => Console.WriteLine("Result: {0}", ni * ni)).
As<double?>().Do(nd => Console.WriteLine("Result: {0}", nd + nd)).
As<string>().Do(s => Console.WriteLine("Result: {0}", "Hello, " + s)).
As().Do(_o => { throw new NotSupportedException(); });
}
}

public static class My
{
static class Default
{
public static readonly EmptyAs EmptyAs = new EmptyAs(null);
}

static class Default<T>
{
public static readonly EmptyAs<T> EmptyAs = new EmptyAs<T>(null);
}

public static As<T> As<T>(this object o)
{
return o == null ? Default<T>.EmptyAs : new As<T>(o);
}

public static As As(this object o)
{
return o == null ? Default.EmptyAs : new As(o);
}
}

public class As
{
protected readonly object o;
public As(object o)
{
this.o = o;
}

public virtual void Do(Action<object> action)
{
action(o);
}
}

public class EmptyAs : As
{
public EmptyAs(object o)
: base(o)
{
}

public override void Do(Action<object> action)
{
}
}

public class As<T> : As
{
public As(object o)
: base(o)
{
}

public virtual object Do(Action<T> action)
{
if (o is T)
{
action((T)o);
return null;
}
else
{
return o;
}
}
}

public class EmptyAs<T> : As<T>
{
public EmptyAs(object o)
: base(o)
{
}

public override void Do(Action<object> action)
{
}

public override object Do(Action<T> action)
{
return null;
}
}
}





3. ローンパターン(Loan Pattern)
IDisposable なクラスであれば標準で using があるけど、それ以外でも統一的な構文があると良いかと。

使い終わったら消える一時ファイル
サンプル

using System;
using System.IO;
using Microsoft.VisualBasic.FileIO;

namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
My.UsingTempFile(tempFile =>
{
using (var fileStream = new FileStream(tempFile, FileMode.Create, FileAccess.Write))
using (var streamWriter = new StreamWriter(fileStream))
{
streamWriter.WriteLine("Hello, Internal DSL !!");
}

using (var fileStream = new FileStream(tempFile, FileMode.Open, FileAccess.Read))
using (var streamReader = new StreamReader(fileStream))
{
Console.WriteLine(streamReader.ReadToEnd());
}

throw new ApplicationException();
});
/* 実行結果(※エラーが発生してもファイルは削除される)
*
* Hello, Internal DSL !!
*
* TestCase 'M:ConsoleApplication1.Program.Main(System.String[])' failed: アプリケーションでエラーが発生しました。
* System.ApplicationException: アプリケーションでエラーが発生しました。
* Program.cs(28,0): 場所 ConsoleApplication1.Program.<Main>b__0(String tempFile)
* Program.cs(45,0): 場所 ConsoleApplication1.My.UsingTempFile(Action`1 action)
* Program.cs(14,0): 場所 ConsoleApplication1.Program.Main(String[] args)
*
*/
}
}

public static class My
{
public static void UsingTempFile(Action<string> action)
{
string tempFile = Path.GetFileNameWithoutExtension(FileSystem.GetTempFileName()) + ".txt";
try
{
action(tempFile);
}
finally
{
try
{
File.Delete(tempFile);
}
catch { }
}
}
}
}



使い終わったらアンロードされる AppDomain
サンプル

using System;
using System.Reflection;

namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Domain Name: {0}", AppDomain.CurrentDomain.FriendlyName);
My.UsingNewDomain(() =>
{
Console.WriteLine("Domain Name: {0}", AppDomain.CurrentDomain.FriendlyName);
});
/* 実行結果(※エラーが発生しても AppDomain は(ry)
*
* Domain Name: ConsoleApplication1.exe
* Domain Name: NewDomain
*
*/
}
}

public class MarshalByRefAction : MarshalByRefObject
{
public MarshalByRefAction(Action action)
{
Action = action;
}

public Action Action { get; private set; }

public void Do()
{
if (Action != null) Action();
}
}

public static class My
{
public static void UsingNewDomain(Action action)
{
var domain = default(AppDomain);
try
{
domain = AppDomain.CreateDomain("NewDomain", null, AppDomain.CurrentDomain.SetupInformation);
var marshalByRefAction =
(MarshalByRefAction)domain.CreateInstanceAndUnwrap(
typeof(MarshalByRefAction).Assembly.FullName,
typeof(MarshalByRefAction).FullName,
true,
BindingFlags.Default,
null,
new object[] { action },
null,
null,
null);
marshalByRefAction.Do();
}
finally
{
if (domain != null)
AppDomain.Unload(domain);
}
}
}
}



使い終わったら元に戻る Singleton オブジェクト
サンプル

using System;
using System.Reflection;

namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
Singleton.Instance.Action();
Singleton.Instance.Action();
My.UsingMockSingleton(new MockSingleton(), () =>
{
// ここでは Mock に入れ替わっている。
Singleton.Instance.Action();
});
Singleton.Instance.Action();
/* 実行結果
*
* This is real object !! 48649253
* This is real object !! 48649253
* This is mock object !!
* This is real object !! 48649253
*
*/
}
}

public static class My
{
public static void UsingMockSingleton(Singleton mock, Action action)
{
var last = Singleton.Instance;
var instanceField = typeof(Singleton).GetField("instance", BindingFlags.Static | BindingFlags.NonPublic);
try
{
instanceField.SetValue(null, mock);
action();
}
finally
{
instanceField.SetValue(null, last);
}
}
}

public class Singleton
{
protected Singleton() { }
static Singleton instance = new Singleton();
public static Singleton Instance { get { return instance; } }

public virtual void Action()
{
Console.WriteLine("This is real object !! {0}", this.GetHashCode());
}
}

public class MockSingleton : Singleton
{
public MockSingleton()
{
}

public override void Action()
{
Console.WriteLine("This is mock object !!");
}
}
}





4. 匿名型の型推論
List<T> とかの Generics な型に匿名型を指定したい場合に必要…というか匿名型冷遇され過ぎな気がががが(´・ω・`)
ちなみに、このサンプルで出てくるような、「インターフェースのメソッドをデリゲートで入れ替えられるようなもの」をいくつも作るのが面倒になって思いついたのが NAnonym の LocalClass だったり。

匿名型から Generics な型を生成
サンプル

using System;
using System.Collections.Generic;

namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
var array = new[] { new { Key = 1, Value = "aaaa" }, new { Key = 3, Value = "cccc" }, new { Key = 2, Value = "bbbb" } };
var list = My.CreateList(array[0]);
list.AddRange(array);
list.Insert(1, new { Key = 4, Value = "dddd" });
list.Add(new { Key = 5, Value = "eeee" });

list.ForEach(item =>
{
Console.WriteLine(item);
});
/* 実行結果
*
* { Key = 1, Value = aaaa }
* { Key = 4, Value = dddd }
* { Key = 3, Value = cccc }
* { Key = 2, Value = bbbb }
* { Key = 5, Value = eeee }
*
*/

var comparer = My.CreateComparer(array[0], (x, y) => x.Key - y.Key);
list.Sort(comparer);

list.ForEach(item =>
{
Console.WriteLine(item);
});
/* 実行結果
*
* { Key = 1, Value = aaaa }
* { Key = 2, Value = bbbb }
* { Key = 3, Value = cccc }
* { Key = 4, Value = dddd }
* { Key = 5, Value = eeee }
*
*/
}
}

public static class My
{
public static List<T> CreateList<T>(T obj)
{
return new List<T>();
}

public static IComparer<T> CreateComparer<T>(T obj, Func<T, T, int> comparer)
{
return new DelegateComparer<T>(comparer);
}
}

public class DelegateComparer<T> : IComparer<T>
{
public static readonly Func<T, T, int> DefaultComparer = (x, y) => Comparer<T>.Default.Compare(x, y);

Func<T, T, int> comparer;

public DelegateComparer()
: this(null)
{
}

public DelegateComparer(Func<T, T, int> comparer)
{
this.comparer = comparer == null ? DefaultComparer : comparer;
}

#region IComparer<T> Member

public int Compare(T x, T y)
{
return comparer(x, y);
}

#endregion
}
}





すみません…だいぶ長くなりましたのでとりあえずこの辺で。順次構文拡大中なので、また面白いものがあれば紹介させていただきます。ところで、個人的には、Scala の名前渡しパラメータがすごく欲しかったり。見栄えがきれいになるのだよー。~(´ー`~)

Memo 01

$
0
0
Twilogで遡るのが億劫になってきたので、資料とか手順とか経緯とか。取り留めのないメモ。

C# の魔改造を始めて早 1 年…。やり始めた頃に比べると知識も増えた反面、取り入れたけど結局使わなかった技術や、なんでこれ使うようになったんだっけという技術も増えつつあり、成果物ということでは全然形にはなっていないのだけど、一度ちょっと棚卸しないといけないと思いまして、のエントリです。


以下の書籍・記事/ライブラリを参考/使用させていただいてます。著作者の方々には本当に感謝です!
Amazon.co.jp: メタプログラミングRuby: Paolo Perrotta, 角征典: 本
Amazon.co.jp: レガシーコード改善ガイド (Object Oriented SELECTION): マイケル・C・フェザーズ, ウルシステムズ株式会社, 平澤 章, 越智 典子, 稲葉 信之, 田村 友彦, 小堀 真義: 本
Amazon.co.jp: .NET Common Language Runtime Unleashed: Kevin Burton: 本
Amazon.co.jp: Domain-Specific Languages (Addison-Wesley Signature Series (Fowler)): Martin Fowler: 本
SEXYHOOKで始めるテスト とある関数の接合部(1):CodeZine
SEXYHOOK
Matzにっき(2010-11-13)
Moles - Isolation framework for .NET - Microsoft Research
neue cc - Rx + MolesによるC#での次世代非同期モックテスト考察
さすがMoles!Moq たちにできない事を平然とやってのけるッ - present
Moles - .NETのモック・スタブフレームワーク - Jamzzの日々



NAnonym の現状
GitHub で公開している NAnonym(えぬ・あのにむ)。対象をものすごく限定してしまっているのだけど、処理の中身を実行時に入れ替えられる。とりあえずこんなテストは動くようになってました。

Test.Urasandesu.NAnonym.Etc.dll
元の処理。

namespace Test.Urasandesu.NAnonym.Etc
{
public class Class1
{
public string Print(string value)
{
return "Hello, " + new Class2().Print(value) + " World !!";
}
}

public class Class2
{
public string Print(string value)
{
return "こんにちは、" + value + " 世界!";
}
}
}



Test.Urasandesu.NAnonym.Cecil.DW.dll
入れ替える設定を書く。

using Test.Urasandesu.NAnonym.Etc;
using Urasandesu.NAnonym.DW;
using Urasandesu.NAnonym.Cecil.DW;

namespace Test.Urasandesu.NAnonym.Cecil.DW
{
public class GlobalClass1 : GlobalClass
{
protected override DependencyClass OnRegister()
{
// Class1 の処理を入れ替える。
var class1GlobalClass = new GlobalClass<Class1>();
class1GlobalClass.Setup(o =>
{
o.HideMethod<string, string>(_ => _.Print).By(
value =>
{
return "Modified prefix at Class1.Print" + new Class2().Print(value) + "Modified suffix at Class1.Print";
});
});
return class1GlobalClass;
}

protected override string CodeBase
{
get { return typeof(Class1).Assembly.CodeBase; }
}

protected override string Location
{
get { return typeof(Class2).Assembly.Location; }
}
}

public class GlobalClass2 : GlobalClass
{
protected override DependencyClass OnRegister()
{
// Class2 の処理を入れ替える。
var class2GlobalClass = new GlobalClass<Class2>();
class2GlobalClass.Setup(o =>
{
o.HideMethod<string, string>(_ => _.Print).By(
value =>
{
return "Modified prefix at Class2.Print" + value + "Modified suffix at Class2.Print";
});
});
return class2GlobalClass;
}

protected override string CodeBase
{
get { return typeof(Class2).Assembly.CodeBase; }
}

protected override string Location
{
get { return typeof(Class2).Assembly.Location; }
}
}
}



Test.Urasandesu.NAnonym.Cecil.dll
テスト。ちなみに、「入れ替えるクラスを登録。」の処理~「入れ替え。」の処理をコメントアウトすると、ちゃんとテストに通らなくなる。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NUnit.Framework;
using Test.Urasandesu.NAnonym.Etc;
using Urasandesu.NAnonym.Cecil.DW;
using Urasandesu.NAnonym.Test;
using Assert = Urasandesu.NAnonym.Test.Assert;

namespace Test.Urasandesu.NAnonym.Cecil.DW
{
[TestFixture]
public class GlobalClassTest
{
[TestFixtureSetUp]
public void TestFixtureSetUp()
{
// 元の状態に戻す。
GlobalDomain.Revert();

// 入れ替えるクラスを登録。
GlobalDomain.Register<GlobalClass1>();
GlobalDomain.Register<GlobalClass2>();

// 入れ替え。
GlobalDomain.Load();
}


[Test]
public void Class1Class2Test()
{
var class1 = new Class1();
var class2 = new Class2();
string value = "aiueo";

Assert.AreEqual(
"Modified prefix at Class1.Print" +
"Modified prefix at Class2.Print" +
value +
"Modified suffix at Class2.Print" +
"Modified suffix at Class1.Print",
class1.Print(value));

Assert.AreEqual(
"Modified prefix at Class2.Print" +
value +
"Modified suffix at Class2.Print",
class2.Print(value));
}
}
}



入れ替えるための設定で書く言語内 DSL がわかりにくい上に使いにくい(特に VB からだと本当にゴメンナサイってなる)、入れ替え対象の Assembly を AppDomain へ Load するタイミングとの兼ね合いで、入れ替えるための設定を別 Assembly にしなくちゃいけないし、テスト実行のタイミングによっては Revert がうまく行かなくて元に戻らなくなる、等々細かなまずい部分はあるのだけれど、設計から見直さないとどうにもならないなーっていうのが、

  • パフォーマンス


上に書いたような簡単な入れ替えでも 2 秒以上かかっちゃって(Windows XP SP3, CPU: Core 2 Duo 1.2 GHz, RAM: 2 GB, HDD: Intel SSD X25-M 80GB)、目標値より 200 倍ほど遅い。一応ネックになる処理はわかっているのだけれども、

  • AppDomain の Load/Unload


という、またどうにもならなさげなもの。このままの設計で進めるのは難しいと判断し、しばらく触るのを止めてます。
@super_rtiさんの SEXYHOOKみたく、テスト中は実行時のメモリ状態だけを書き換えられて、動的言語のモンキーパッチみたく、実際の実行時はバイナリに織り込むようなことができれば最高なのだけれど…。



Moles - Isolation framework for .NET
NAnonym を作り出してから知ったのですけど、私がやりたいことの半分は Microsoft Research で作成されている Molesを使えば実現できます。公式のドキュメントは、ここの中ほどにあります「Unit Testing with Microsoft Moles」がステップバイステップで使い方が書いてあってわかりやすいです。リファレンスも、同じページに「Microsoft Moles Reference Manual」を見つけられます。
日本語のサイトですと、@neueccさんの「neue cc - Rx + MolesによるC#での次世代非同期モックテスト考察」や、@t_nakamuraさんの「さすがMoles!Moq たちにできない事を平然とやってのけるッ - present」、@Jamzzさんの「Moles - .NETのモック・スタブフレームワーク - Jamzzの日々」が参考になると思いますです。


Moles でいいんじゃね?
半分は~っていうのは、2 点ほどどうにもならないことがあって、

  • 入れ替えた結果を固定できない(バイナリに織り込めない)

  • ライセンス


…はい、別に固定できなくても、毎回 Moles 経由して実行(もしくは Moles をキックするランチャー経由で実行)すればいいだけ、と思ったこともあったんですが、そもそも商用サービスのために Moles を使うことはライセンスで止められてるんですよね(ライセンスに関してはこちらに。下のほうに「Moles Visual Studio 2010 Power Tools」ってところがあって、そこからソフトウェア使用許諾契約書がダウンロードできる)。名前の通り、開発時に使うツール、という限定された使い方しかできないということでしょう。
また、Microsoft Research の成果っていうのは Moles に限ったことではないようですが、ソースコードが公開されていないため、処理の解析や参考にもできません。ぐぬぬ…。



\(^o^)/
いいえ。Google で "microsoft moles how implemented"みたいな検索をすると、「c# - How Moles Isolation framework is implemented? - Stack Overflow」っていう QA が見つかります。その回答を見ると処理内容はこんなふうになってるよ、って言い切っててすごいっ、と思うのですが、回答者の Peli さんのプロフィールを見ると「I'm working on Pex at Microsoft Research.」って書いてあります・・・中の人、答え書いちゃったよ!
まあ全然極秘事項ってわけではなく、Moles のマニュアルを読んでいると、「the Moles framework uses a profiler to rewrite the method bodies to be able to redirect method calls.」とか「The mole types rely on runtime code rewriting, which is implemented by a CLR profiler.」、「The mole types require a CLR profiler to be installed on the machine to execute the tests.」っていう記述が出てきてますし、そもそも MSDN のアンマネージ API リファレンスのプロファイル API のトピック、「Profiling Overview」には、「プロファイル API を使用すると、JIT コンパイルをフックして、メモリ内 MSIL コード ストリームを変更できるよー。」ってあるので、CLR のプロファイラに関わる仕事されてる方にとっては、よく知られたことなのでしょう。



C++、アンマネージ API の世界へ
私は基本的に仕事でも C# を使っていて、アンマネージ API で主戦力となる C++ 使いではありません。テンプレートやら STL やら Boost やら COM やら ATL やら…歴史がある言語な分、覚えなければならないこともたくさんあるし、公開されているソースコードのビルド通すにも暗黙の了解があったり、デバッグの仕方がよくわからなかったり…で遅々としてしか進んでいないのが現状です。次の Memo では、そんな C++ の自分向け覚書と、アンマネージ API の入り口を書き留められればと...( = =)

SSCLI2.0 ビルド戦記 - Battle Against Makefile -

$
0
0
Win XP(x86) + VS2008 でビルドしようなんて思うから…。

アンマネージ API を使いたい時に困るのがサンプルコードの探しにくさ。日本語の情報はもちろん絶望的だし、本家 MSDN にあるリファレンスも、I/F の簡単な説明しかない…。まずは、"Hello, world!!"から始めたくても、一連の流れがわかるのは .NET Framework SDK Version 1.1についてくる Metadata Dump Utility や SMC - Simple Managed Compiler Sample が、唯一の頼みの綱という状態。…かと言って、これらでも全ての API 網羅してるわけではないわけで。

発端は、SMC のコードを追っていて。
SMC って、MethodBody の RVA についての計算と領域の確保を手動でやってるんです。あれ?アンマネージ API リファレンスに ICeeGen::AllocateMethodBuffer ってあるけど、これは使わないの?と。grep すると案の定どこからも呼ばれていない。一縷の望みにかけて Google 先生にお伺いすると…。




うわあああああああ知らなかった \(^o^)/『Download Details - Microsoft Download Center - Shared Source Common Language Infrastructu… http://bit.ly/qgLBod』
Microsoft 謹製の CLI リファレンス実装があったなんて!なんといういい仕事!これを参考にせず何を参考にするのか!

まあソースコードを動かしながら参照しようにも、やはり日本語のビルド手順はないですし、以前の『うらぶろぐ @urasandesu: Windows で AVRDUDE ビルド』でハマったように、低レイヤ開発環境ビルドの難しさは知っていました。
ですが、図書館で調べながら設計や実装を進められたり、持ち出して人に現状を見ていただき意見や感想をいただける利点が捨てがたく、何とかいつも使っているノート PC で動くようにした顛末です。


こちらのページ/ソフトウェアを参考に/使用させていただきました!偉大な先人の方々に感謝です。
Download Details - Microsoft Download Center - Shared Source Common Language Infrastructure 2.0 Release
Download Details - Microsoft Download Center - .NET Framework Software Development Kit Version 1.1
Cygwin
ActivePerl is Perl for Windows, Mac, Linux, AIX, HP-UX & Solaris | ActiveState
SSCLI 2.0 and Visual Studio 2008 - Jeremy Kuhne's Blog - Site Home - MSDN Blogs
Shared Source Common Language Infrastructure 2.0 - Memo+
- 自動化のための nmake 入門講座
A Night with Rotor and Rotor Resources : Sam Gentile's Blog
8.3形式の短いファイル名を生成しないようにする - @IT
普通のpatchコマンドで取り込めるdiffファイルをgitで作成する - kanonjiの日記



とりあえず結果から
Jeremy Kuhne さんのブログですが、結構 typo とかあるのでパッチを作りました。運が良ければ当てて終わりです!

sscli20_WinXPx86_VS2008_JP.patch (11 KB) - Google Docs

1. SSCLI2.0 をダウンロード、解凍。必要ソフトをインストール。
Download Details - Microsoft Download Center - Shared Source Common Language Infrastructure 2.0 Releaseから、ソースコードがまとまった sscli20_20060311.tgz をダウンロードし、解凍する(ここでは、C:\sscli20_20060311 へ)。Active PerlCygwin等、必要なツールをインストール。

2. Cygwin を起動し、パッチ当て
ダウンロードした sscli20_WinXPx86_VS2008_JP.patch を解凍したフォルダ(ここでは、C:\sscli20_20060311)に置き、Cygwin で以下のようにコマンドを実行、パッチを当てる。

user@COMPUTER ~
$ cd /cygdrive/c/sscli20_20060311/

user@COMPUTER /cygdrive/c/sscli20_20060311
$ ls
sscli20  sscli20_WinXPx86_VS2008_JP.patch

user@COMPUTER /cygdrive/c/sscli20_20060311
$ patch --dry-run -p0 -i sscli20_WinXPx86_VS2008_JP.patch
patching file sscli20/clr/src/classlibnative/float/sources.inc
patching file sscli20/clr/src/classlibnative/float/wks/sources
patching file sscli20/clr/src/classlibnative/nls/sources.inc
~(中略)~
patching file sscli20/tools/cppmunge/sources
patching file sscli20/tools/resourcecompiler/sources
patching file sscli20/win.env.bat

user@COMPUTER /cygdrive/c/sscli20_20060311
$ patch -p0 -i sscli20_WinXPx86_VS2008_JP.patch
patching file sscli20/clr/src/classlibnative/float/sources.inc
patching file sscli20/clr/src/classlibnative/float/wks/sources
patching file sscli20/clr/src/classlibnative/nls/sources.inc
~(中略)~
patching file sscli20/tools/cppmunge/sources
patching file sscli20/tools/resourcecompiler/sources
patching file sscli20/win.env.bat



3. コマンドプロンプトを起動し、ビルド
後は通常の手順である readfirst.html(ここでは、C:\sscli20_20060311\sscli20\readfirst.html) の Building and Running Code を参考にビルドする。

Microsoft Windows XP [Version 5.1.2600]
(C) Copyright 1985-2001 Microsoft Corp.

C:\Documents and Settings\user>cd C:\sscli20_20060311\sscli20

C:\sscli20_20060311\sscli20>env
Setting environment for using Microsoft Visual Studio 2008 x86 tools.
32-bit build
Checked Environment
Building for Operating System - NT32
             Processor Family - x86
                    Processor - i386
                   Build Type - chk

C:\sscli20_20060311\sscli20>buildall

--- Copy prebuilt files ---

C:\sscli20_20060311\sscli20\prebuilt\idl\clrdata.h
~(中略)~
C:\sscli20_20060311\sscli20\prebuilt\yacc\asmparse.c
        1 個のファイルをコピーしました。

--- Building the PAL ---

Build successful.

--- Building the binplace tool ---

Build successful.

--- Building the build tool ---

Build successful.

--- Building the cppmunge tool ---

Build successful.

--- Building Resource Compiler ---

BUILD: Using 2 child processes
~(中略)~
1>Linking Executable - objc\rotor_x86\resourcecompiler.exe for i386
BUILD: Done

    1 executable built
    1 browse database built

--- Building PAL RT ---

BUILD: Using 2 child processes
~(中略)~
b\chk\rotor_x86\rotor_palrt.lib for all platforms
BUILD: Done

    1 file compiled -     5 LPS
    4 libraries built
    3 executables built
    4 browse databases built
    70 files binplaced

--- Building C# compiler ---

BUILD: Using 2 child processes
~(中略)~
2>Linking Executable - scc\pkg\objc\rotor_x86\csc.exe for i386
BUILD: Done

    4 files compiled -     8 LPS
    5 libraries built
    4 executables built
    5 browse databases built
    6 files binplaced

--- Building CLR ---

BUILD: Using 2 child processes
~(中略)~
BUILD: Examining C:\sscli20_20060311\sscli20\clr\src directory tree for files to compile.
    C:\sscli20_20060311\sscli20\clr\src
BUILD: Done

    30 files compiled
    44 libraries built
    25 executables built
    48 browse databases built
    225 files binplaced

--- Building FX - pass1 only ---

BUILD: Using 2 child processes
~(中略)~
    C:\sscli20_20060311\sscli20\fx\src
BUILD: Done

    10 files compiled
    1 file binplaced

--- Building Remoting - pass1 only ---

BUILD: Using 2 child processes
~(中略)~
    C:\sscli20_20060311\sscli20\clr\src\managedlibraries
BUILD: Done

    4 files compiled

--- Building JScript - pass1 only ---

BUILD: Using 2 child processes
~(中略)~
    C:\sscli20_20060311\sscli20\jscript
BUILD: Done

    4 files compiled

--- Building FX - System.dll ---

BUILD: Using 2 child processes
~(中略)~
BUILD: Done

    1 file compiled
    1 executable built
    1 file binplaced

--- Setting up System.dll ---


--- Building FX ---

BUILD: Using 2 child processes
~(中略)~
    C:\sscli20_20060311\sscli20\fx\src
BUILD: Done

    3 files compiled
    5 executables built
    5 files binplaced

--- Setting up System.Configuration.dll ---


--- Setting up System.Configuration.dll ---


--- Setting up System.Xml.dll ---


--- Setting up System.Data.SqlXml.dll ---


--- Building FX - Misc ---

BUILD: Using 2 child processes
~(中略)~
BUILD: Done

    1 file binplaced

--- Building Remoting ---

BUILD: Using 2 child processes
~(中略)~
BUILD: Done

    2 files compiled
    2 executables built
    2 files binplaced

--- Building JScript ---

BUILD: Using 2 child processes
~(中略)~
    C:\sscli20_20060311\sscli20\jscript
BUILD: Done

    2 files compiled
    3 executables built
    3 files binplaced

--- Building dactablegen ---

BUILD: Using 2 child processes
~(中略)~
BUILD: Done

    1 executable built
    1 file binplaced

--- Building dacupdatedll ---

BUILD: Using 2 child processes
~(中略)~
1>Binplacing - objc\rotor_x86\daccess.i for rotor_x86
BUILD: Done

    1 file binplaced

--- Building strike dll ---

BUILD: Using 2 child processes
~(中略)~
1>Binplacing - sos_stacktrace.h for i386
BUILD: Done

    1 file compiled -     1 LPS
    1 library built
    1 executable built
    1 browse database built
    5 files binplaced

--- Setting up everything ---


--- Building samples ---

BUILD: Using 2 child processes
~(中略)~
1>Linking Executable - pigui\pigpad\@objc\rotor_x86\csc.rsp for all platforms
BUILD: Done

    35 executables built
    78 files binplaced

--- Building pdb to ildb Tool ---

BUILD: Using 2 child processes
~(中略)~
1>Linking Executable - objc\rotor_x86\ildbconv.exe for i386
BUILD: Done

    1 executable built
    1 browse database built
    1 file binplaced

C:\sscli20_20060311\sscli20>




だめだった人ー?
(・∀・)人(・∀・)ナカーマ

残念ですが…。もし、エラー内容が以下のような雰囲気であれば、私と同じような感じで行けるかもしれません。
  • 「*** Error while building C:\sscli20_20060311\sscli20\pal\win32 Open C:\sscli20_20060311\sscli20\pal\win32\buildc.log to see the error log.」ってエラーメッセージが出た。
  • 言われた通り、buildc.log を見ると「.\rotor_pal.rc(15) : fatal error RC1015: cannot open include file 'ntverp.h'.」ってエラーメッセージが出てる。
  • 最初の env コマンドで環境変数設定されてるんじゃないの?と思って set | more で環境変数確認すると…!? INCLUDE が定義されてない???
  • buildc.log でエラー吐いてる「Microsoft (R) Windows (R) Resource Compiler」は、INCLUDE 環境変数使わないのかな?と思って、MSDN で確認すると、やっぱり使うことがわかる
  • /\binclude\b\s*=/i で grep してみる。結果を分類すると、それっぽいのが十数個見つかる。…★
  • rc 実行する箇所も探してみる。/\brc\b/i で grep 。結果を分類すると、★とファイルが重複するものが見つかる(devdiv.def、ここでは C:\sscli20_20060311_\sscli20\env\bin\devdiv.def)。怪しい。…★★
  • ビルドスクリプトで、いつこれが呼ばれるのかわからない。buildall.cmd(ここでは、C:\sscli20_20060311_\sscli20\buildall.cmd)を読んでみる。
  • 1 行目に「@if "%_echo%"=="" echo off」ってある。ほむほむ。set _echo=1 で buildall 2>&1 > buildall.log してみる。
  • echo でデバッグプリント。INCLUDE 環境変数に変化なし。Makefile 内が怪しい。
  • 問題の Makefile (ここでは、C:\sscli20_20060311_\sscli20\pal\win32\makefile)を確認してみると…先頭で「!INCLUDE $(NTMAKEENV)\devdiv.def」してる!★★と繋がった!
  • devdiv.def がビルドスクリプトからそのまま呼ばれるのがわかったので、ここでもデバッグプリントを順に埋め込んでみる。最初 @echo で出力させようとして半日ハマる(!MESSAGE プリプロセッサを使わないといけないという罠。マクロは、コマンドの前に評価されるのになかなか気づかず…orz)。
  • あー、C:\PROGRA~1\Microsoft SDKs\Windows\v6.0A\include が「INCLUDES = $(INCLUDES: =)」の処理でスペース除去されちゃうんだ…。
  • でも、C:\Program Files が C:\PROGRA~1 になってるんだから、Microsoft SDKs も短縮されていいはずなのに…。意味がわかりません (((・・;)
  • 「.\rotor_pal.rc(15) : fatal error RC1015: cannot open include file 'ntverp.h'.」でググってみる。
  • む? short filename が作られていない?またまたそんな…と、C:\Program Files に移動しコマンドを叩く。

  • C:\Program Files>dir /x "Microsoft SDKs*"
     ドライブ C のボリューム ラベルは S3A4509D001 です
     ボリューム シリアル番号は 758E-2116 です

     C:\Program Files のディレクトリ

    2009/05/14  20:50    <DIR>                       Microsoft SDKs
                   0 個のファイル                   0 バイト
                   1 個のディレクトリ  25,656,471,552 バイトの空き領域

    C:\Program Files>

    ( ゚д゚ )
  • どうしてこんなことに…。しかたないので、@IT にある記事の通り fsutil を実行して再度 short filename を有効に。まだプチフリをよく起こす SSD 使ってたころの設定、そのまま持ってきちゃったのかな...( = =)
  • フォルダを再作成し、中身をごっそりコピー。
  • そして今一度 buildall・・・!行った!おめでとー!

普段 Visual Studio を利用している開発者にとって、Makefile のデバッグは慣れなくて大変だと思います。ですが、各コマンドのリファレンスを片手に、!MESSAGE プリプロセッサと grep で上手く絞り込んで行ければ、それほど時間はかからないでしょう。



お次は念願の "Hello, world!!"?
少しずつ近づいている気はするのですが、なかなか辿り着かないこの入り口。勉強会等々で、もっと情報交換できるといいのだけれど。英語がんばるのも、これからの課題ですね。

11 年目の "Hello World!" - Basics of CLI begun with 11th year -

$
0
0
.NET開発者向けツール開発な話題が増えるといいな。

拙い英語力は技術力でカバー!・・・とか言ってみたいところですが、そうも行かず。かと言って、先週末受けた TOEIC は悲壮感が漂う始末...( = =)
CLI が世に出て 11 年が経とうというのに、この辺りの情報は相変わらず日本語の情報が絶望的な状態。まあ嘆いても仕方がないので、前回に引き続きアンマネージ API です。まずはまずは "Hello World!"をををををと調べ続けた結果をご報告いたします。
ECMA C# and Common Language Infrastructure Standardsにある CLI の仕様書、Common Language Infrastructure (CLI) Partition II: Metadata Definition and Semanticsを読み解き、バイナリを解析し、API の助けを借りて、一番基本的な CLI アプリケーションを作ってみようという試み。拙い英語力のため、時折変な訳が混じっているかもですが、ご容赦くださいませ。

なお、文中にある "Hello World!"バイナリと、それを作成するプログラムのソリューションは以下のリンクからダウンロードできます。プログラムは Visual Studio 2008 にて Boost C++ Libraries Ver.1.47.0を利用して作成していますので、ビルドの際はいくらか環境を整える必要があります。

こちらのページ/ソフトウェアを参考に/使用させていただきました。いつも本当にお世話になっております <(_ _)>
Download Details - Microsoft Download Center - Shared Source Common Language Infrastructure 2.0 Release
Cecil - Mono
ECMA C# and Common Language Infrastructure Standards
Boost C++ Libraries
Windows実行ファイルのバイナリ概要(1/2):CodeZine
EXEファイルの内部構造(PEヘッダ)(1/3):CodeZine
EXEファイルの内部構造(セクション)(1/3):CodeZine
プログラムからEXEファイルを生成してみよう(1/3):CodeZine
Peering Inside the PE: A Tour of the Win32 Portable Executable File Format
Inside Windows: An In-Depth Look into the Win32 Portable Executable File Format
Inside Windows: An In-Depth Look into the Win32 Portable Executable File Format, Part 2
逆アセのスス乂
[x86 asm] help understanding JMP opcodes - GameDev.net
Understanding the Import Address Table
Can't find a way to get the ICeeGen interface - .NET Framework
re-assembling .net assemblies with ICeeFileGen
Stirlingの詳細情報 : Vector ソフトを探す!



目次

Hello PE file format!
Microsoft Windows での実行形式ファイルといえば、Portable Executable(PE) フォーマット。CLI のためのファイルフォーマットも、例に漏れずこの形式に沿ったものとなっています。CLI での仕様は、Common Language Infrastructure (CLI) Partition II: Metadata Definition and Semanticsの [25 File format extensions to PE] からまとめられており、ここを見れば一通りの情報は得られる感じ。
PE ファイルフォーマット自体の入門には、山本 大祐さんが CodeZine で書かれている、『Windows実行ファイルのバイナリ概要(1/2):CodeZine』や『EXEファイルの内部構造(PEヘッダ)(1/3):CodeZine』、『EXEファイルの内部構造(セクション)(1/3):CodeZine』がとても参考になりました。
仕様を追っていくときの参考例として、以下のような "Hello World!"プログラムをコンパイルし、見比べながら追っていくことにします。

using System;

class MainApp {
   public static void Main() {
      Console.WriteLine("Hello World!");
   }
}
コンパイルした結果できあがった hello.exe を、お気に入りのバイナリエディタで開き、各構成要素をわかりやすく色分けしたのが以下の図です。構成要素は大きく分けて以下の通り:
1. MS-DOS Header、MS-DOS 用スタブプログラム
2. ヘッダ
  2-1. PE ヘッダ
  2-2. .text セクションヘッダ
  2-3. .reloc セクションヘッダ
3. .text セクション
  3-1. Import Address Table(IAT) RVAs
  3-2. CLI ヘッダ
  3-3. IL メソッドボディ
  3-4. メタデータ
  3-5. Import Table Import Directory エントリ
  3-6. Import Lookup Table RVAs
  3-7. Import Method Hint Name
  3-8. Import DLL Name
  3-9. x86 エントリポイントスタブ
4. .reloc セクション
  4-1. Fix-Up Table

先に共通のルールを 3 つほど。1 つ目は「常に~」のようなフィールドがあります。これは[24.1 Fixed fields] で "When writing these fields it is best that they be set to the value indicated, on reading they should be ignored.(そういう値は、書き込む時は指定された値を入れるのが良くて、読み込むときは無視するべき)"と説明されています。コンパイラによっては、仕様とは微妙に違う値が入っていると思いますが、特に気にしなくて良いでしょう。

次は、RVA(Relative Virtual Address)。聞きなれない言葉、というかまず普通は出会わない言葉でしょう。Meta Data API のリファレンス読むと、まずこれがわけわかめです。項目がメモリに読み込まれたアドレスから、イメージファイルのベースアドレスを減算したもので、ファイルが読み込まれたベースアドレスからのオフセットという言い方もできる、みたいな。あうあう・・・(ToT)
実際にどのように使われるかはさておき、ある項目のファイル上でのアドレスを p、RVA を r、それが属すセクションのファイル上でのアドレスを l、RVA を s とすると、p == r - l + s みたいな式が成り立ちます。例えば、hello.exe の例で言うと、Entry Point RVA(x86 エントリポイントスタブの RVA)として 0x0000232E って指定があります。これは、同じく Section Alignment が 0x2000 という指定から、各セクションが 0x2000 の倍数の位置の RVA に対応することになることがわかり、これが属すセクション(.text セクション)のファイル上のアドレスは 0x00000200 ですので、この項目の RVA が指すものの実際のファイル上のアドレスは、0x0000232E - 0x2000 + 0x00000200 で 0x0000052E となります。実際に確認すると、{ 0xFF, 0x25, 0x00, 0x20, 0x40, 0x00, ・・・ } とあり、これは仕様通り、無条件ジャンプ命令(JMP)ですね、と。

最後はバイナリ値の格納方法。リトルエンディアンです。これは x86 系 CPU が主流な Windows の場合は、標準的なことなのかもしれませんね。

さて、各要素について順番に見ていくことにしましょう。


1. MS-DOS Header、MS-DOS 用スタブプログラム
私もちゃんと使ったことはないのですが、昔 MS-DOS という OS があった頃の名残とのこと。Windows 環境では特に意味はありません。もし MS-DOS 環境で実行された場合に、"This program cannot be run in DOS mode."と表示し、すぐ終了するような処理が書かれています。CLI でも同じ感じで、前述の仕様書の [25.2.1 MS-DOS header] には以下の定義が載っています。
0x4D0x5A0x900x000x030x000x000x000x040x000x000x000xFF0xFF0x000x00
0xB80x000x000x000x000x000x000x000x400x000x000x000x000x000x000x00
0x000x000x000x000x000x000x000x000x000x000x000x000x000x000x000x00
0x000x000x000x000x000x000x000x000x000x000x000x00lfanew
0x0E0x1F0xBA0x0E0x000xB40x090xCD0x210xB80x010x4C0xCD0x210x540x68
0x690x730x200x700x720x6F0x670x720x610x6D0x200x630x610x6E0x6E0x6F
0x740x200x620x650x200x720x750x6E0x200x690x6E0x200x440x4F0x530x20
0x6D0x6F0x640x650x2E0x0D0x0D0x0A0x240x000x000x000x000x000x000x00
lfanew は次に紹介する PE ヘッダへのファイル上のアドレスが入ります。上記の通り、MS-DOS Header、MS-DOS 用スタブプログラムは基本的に決まりきったことしか書かないので、固定値 { 0x80, 0x00, 0x00, 0x00 } で良いでしょう。
hello.exe では以下の図の通り、0x00000000 ~ 0x0000007F の内容がこれに該当します。




2. ヘッダ
PE フォーマットファイルのヘッダは、PE ヘッダ、PE オプショナルヘッダとそれに続くセクションヘッダで構成されます。特にあれ?な部分はないですのでサクサク進みましょう。

2-1. PE ヘッダ
PE ヘッダは [25.2.2 PE file header]、[25.2.3 PE optional header] に説明があります。PE シグネチャへの言及について、なぜか上述の [25.2.1 MS-DOS header] に記述があるので、最初はハマりました (-_-;) 全てがっちゃんこした定義は以下のようになります。ちなみに、IMAGE_NT_HEADERS 構造体として、%INCLUDE%\WinNT.h にも定義されてます。
OffsetSizeFieldDescription
04Signatureシグネチャ。''P', 'E', '\0', '\0'。
42Machineマシン種別。0x14C(Intel 386 を表す)。
62Number of Sectionsセクションの数。
84Time/Date Stampファイルが作成された時刻と日付を、1970/01/01 00:00:00 から秒刻みで指定。もしくは 0。
124Pointer to Symbol Table常に 0。
164Number of Symbols常に 0。
202Optional Header SizePE オプショナルヘッダのサイズ。sizeof(IMAGE_OPTIONAL_HEADER)。
222Characteristicsファイルの属性。CLI の場合、0x0002(実行可能かどうか)、0x0004(ファイルから行番号が除去されているかどうか)、0x0008(ファイルからローカルシンボルが除去されているかどうか)、0x0100(32bit ワードマシンかどうか)、0x2000(DLL かどうか)が指定される。
242Magicシグネチャ。0x10B。
261LMajor常に 6。
271LMinor常に 0。
284Code Sizeコード(.text)セクションのサイズ。もしそれらが複数のセクションに渡って存在するのであれば、それら全ての合計。
324Initialized Data Size初期化データを持つセクションのサイズ。もしそれらが複数のセクションに渡って存在するのであれば、それら全ての合計。
364Uninitialized Data Size未初期化データを持つセクションのサイズ。もしそれらが複数のセクションに渡って存在するのであれば、それら全ての合計。
404Entry Point RVAx86 エントリポイントスタブの RVA。
444Base Of Codeコードセクション(.text セクション)の RVA(ローダーへのヒント)。
484Base Of Dataデータセクション(.reloc セクション)の RVA(ローダーへのヒント)。
524Image Base常に 0x400000。
564Section Alignment常に 0x2000。
604File Alignment0x200 か 0x1000 のいずれか。
642OS Major常に 4。
662OS Minor常に 0。
682User Major常に 0。
702User Minor常に 0。
722SubSys Major常に 4。
742SubSys Minor常に 0。
764Reserved常に 0。
804Image Size全てのヘッダ、パディングを含めたサイズ。Section Alignment の倍数で指定。
844Header SizeMS-DOS Header、MS-DOS 用スタブプログラム、ヘッダを合わせたサイズ。パディングも含める。File Alignment の倍数で指定。
884File Checksum常に 0。
922SubSystemMAGE_SUBSYSTEM_WINDOWS_CE_GUI (0x3) か IMAGE_SUBSYSTEM_WINDOWS_GUI (0x2) のいずれか
942DLL Flags常に 0。
964Stack Reserve Size常に 0x100000 (1Mb)。
1004Stack Commit Size常に 0x1000 (4Kb)。
1044Heap Reserve Size常に 0x100000 (1Mb)。
1084Heap Commit Size常に 0x1000 (4Kb)。
1124Loader Flags常に 0。
1164Number of Data Directories常に 0x10。
1208Export Table常に 0。
1288Import TableImport Table Import Directory エントリへの RVA とサイズ。
1368Resource Table常に 0。
1448Exception Table常に 0。
1528Certificate Table常に 0。
1608Base Relocation TableRelocation Table への RVA とサイズ。無ければ 0。
1688Debug常に 0。
1768Copyright常に 0。
1848Global Ptr常に 0。
1928TLS Table常に 0。
2008Load Config Table常に 0。
2088Bound Import常に 0。
2168IATImport Address Table(IAT) RVAs への RVA とサイズ。
2248Delay Import Descriptor常に 0。
2328CLI HeaderCLI ヘッダ への RVA とサイズ。
2408Reserved常に 0。
後からでないとわからない項目も多いので、API を使わずにバイト配列でごりごり作る場合は、コンテンツ→ヘッダの順に作ることになると思います。
hello.exe では以下の図の通り、0x00000080 ~ 0x00000177 の内容がこれに該当します。



2-2. .text セクションヘッダ
PE ヘッダに続いて、各セクションのヘッダが配置されます。最初は .text セクション(コードセクション)ヘッダ。ヘッダの定義は全て同じですので、ここで紹介するのみとします。ちなみに、IMAGE_SECTION_HEADER 構造体として、%INCLUDE%\WinNT.h にも定義されてます。
OffsetSizeFieldDescription
08Name8バイト長、NULL パディングされた ASCII 文字列。
84VirtualSizeセクションの長さ。SizeOfRawData との差分は 0 パディングされる。
124VirtualAddressメモリにロードされた際の、実行イメージのためのセクションの先頭アドレス。
164SizeOfRawDataファイル上のセクションサイズ。PE ヘッダで指定した File Alignment の倍数で指定。
204PointerToRawDataファイル上のセクションオフセット。PE ヘッダで指定した File Alignment の倍数で指定。
244PointerToRelocationsRelocation セクションへの RVA。
284PointerToLinenumbers常に 0。
322NumberOfRelocationsRelocation の数。無ければ 0。
342NumberOfLinenumbers常に 0。
364Characteristicsセクションの属性。0x00000020(実行可能コードを含む)、0x00000040(初期化データを含む)、0x00000080(未初期化データを含む)、0x20000000(実行可)、0x40000000(読み込み可)、0x80000000(書き込み可)を指定する。
hello.exe では以下の図の通り、0x00000178 ~ 0x0000019F の内容がこれに該当します。



2-3. .reloc セクションヘッダ
続いて .reloc セクションヘッダ。定義は省略。何に使われるかまだよくわかっていないです。近々ではまだ必要なさそうですが、追々調べないとだめですね・・・ (^_^;)
hello.exe では以下の図の通り、0x000001A0 ~ 0x000001C7 の内容がこれに該当します。



3. .text セクション
コードセクションと呼ばれ、実際の実行コードはここに入ります。色々入り混じっているためか、概観を自分で描いてみるまでは、構造が全くイメージできませんでした。仕様書で言うと、[25.3.1 Import Table and Import Address Table (IAT)]、[25.3.3 CLI header]、[25.4 Common Intermediate Language physical layout]、[24 Metadata physical layout] 辺りに記述があります。バイナリ上に登場する順に説明していきます。

3-1. Import Address Table(IAT) RVAs
インポートするメソッドを表す Hint/Name Table への RVAs が格納されます。PE ヘッダにある IAT の指す先がここ。NULL 終端の配列になっていますが、マネージドアセンブリでは、通常 mscoree.dll の _CorExeMain(exe 向け) か _CorDllMain(dll 向け)しかインポートしませんので、1 要素のみがある状態になります。似たようなテーブルに Import Lookup Table がありますが、IAT のほうは実行時に実際のメソッドへのアドレスに書き換えられます。DWORD の配列ですが、慣習的に %INCLUDE%\WinNT.h に定義されてる IMAGE_THUNK_DATA 構造体の配列として表されるみたいですね。
hello.exe では以下の図の通り、0x00000200 ~ 0x00000207 の内容がこれに該当します。



3-2. CLI ヘッダ
CLR のためのヘッダーです。定義は以下の通り。IMAGE_COR20_HEADER 構造体として、%INCLUDE%\WinNT.h にも定義を見つけることができます。ここに定義があるってことは、完全に Windows の機能として取り込まれてるのかな?
OffsetSizeFieldDescription
04Cbヘッダのサイズ。sizeof(IMAGE_COR20_HEADER)。
42MajorRuntimeVersionこのプログラムを実行するのに必要な CLR の最低メジャーバージョン。現在は 2。
62MinorRuntimeVersion同じくマイナーバージョン。現在は 0。
88MetaDataメタデータへの RVA とサイズ。
164Flagsラインタイムイメージの属性。0x00000001(常にセット)、0x00000002(32bit プロセスにのみ読み込まれるかどうか)、0x00000008(厳密名を持っているかどうか)、0x00010000(常にリセット)を必要に応じて指定する。
204EntryPointTokenランタイムイメージのエントリポイント(MethodDef トークン)を指定。
248Resourcesリソースの RVA とサイズ。
328StrongNameSignatureハッシュデータの RVA。バインディングとバージョニングのために、CLI ローダーによって利用される。
408CodeManagerTable常に 0。
488VTableFixups関数ポインタの配列(vtable スロットなど)への RVA。
568ExportAddressTableJumps常に 0。
648ManagedNativeHeader常に 0。
VTableFixups はアンマネージド DLL との相互運用用に使われるようですが、とりあえずはちゃんとした調査は後回しです・・・ (>_<)
hello.exe では以下の図の通り、0x00000208 ~ 0x0000024F の内容がこれに該当します。



3-3. IL メソッドボディ
IL については、今回の説明に使っている仕様書とは別に一本仕様書があるぐらいなので、ちょっとボリュームががが (^_^;) 今回は配置する際に必要になるメソッドヘッダの定義と、今回使用した部分だけ紹介させていただきますね。
CLI には 2 種類のメソッドヘッダが用意されていおり、それぞれ Tiny フォーマット、Fat フォーマットと呼ばれてます。今後触っていく予定の Profiling API で IL メソッドボディを取り替えるような場合には、これらの判定が必要になるでしょう。以下のような違いがあります。
・Tiny フォーマット
  以下の条件の全てに当てはまる。
  • ローカル変数なし。
  • 例外なし。
  • 拡張データセクションなし。
  • IL のオペランドスタックが 8 以下。
・Fat フォーマット
  以下の条件のいずれかに当てはまる。
  • エンコードサイズが大きすぎる。少なくとも 64 バイト。
  • 例外あり。
  • 拡張データセクションあり。
  • ローカル変数あり。
  • IL のオペランドスタックが 8 より大きい。
対象の IL メソッドボディが Tiny フォーマットで格納されているか、Fat フォーマットで格納されているかは、最初の 2 ビットを見ればわかるようになってます(0x2: Tiny、0x3: Fat)。今回使うのは Tiny フォーマットのみで、以下のような定義になってます。
Start BitCount of BitsDescription
02Tiny フォーマットを表すビットフラグ(0x2)。
26メソッドボディのサイズ。
領域をビット単位で細かく刻んでるので、バイト列の解釈は要注意。ただ、%INCLUDE%\corhlpr.h に定義されている COR_ILMETHOD_TINY 構造体には、その辺りの計算を良しなにしてくれる便利メソッドが用意されてますので、それらを利用すると良いかもです。
hello.exe では以下の図の通り、0x00000250 ~ 0x00000267 の内容がこれに該当します。Tiny メソッドヘッダを持つ 2 つの IL メソッドボディが見つけられるかな?(0x00000250 ~ 0x0000025D, 0x0000025E ~ 0x00000265。残りは 0 パディング)



3-4. メタデータ
メタデータについては後で詳細を説明するので、ここでは省略。
hello.exe では以下の図の通り、0x00000268 ~ 0x000004DB の内容がこれに該当します。



3-5. Import Table Import Directory エントリ
インポートするメソッドを含む DLL の Name への RVA や、インポートするメソッドを表す Hint/Name Table への RVA が格納されます。PE ヘッダにある Import Table の指す先がここ。%INCLUDE%\WinNT.h に定義されてる IMAGE_IMPORT_DESCRIPTOR 構造体の NULL 終端配列になってます。ただ、やはりマネージドアセンブリでは、通常 mscoree.dll の _CorExeMain(exe 向け) か _CorDllMain(dll 向け)しかインポートしませんので、1 要素のみがある状態に。仕様書にある 20 バイトの 0 パディング領域は、これを明示的に示したものとなります。定義は以下の通り。
OffsetSizeFieldDescription
04ImportLookupTableImport Lookup Table RVAs への RVA。
44DateTimeStamp常に 0。
84ForwarderChain常に 0。
124NameNULL 終端の ASCII 文字列、"mscoree.dll" への RVA。
164ImportAddressTableImport Address Table(IAT) RVAs への RVA。
20200 パディング
hello.exe では以下の図の通り、0x000004DC ~ 0x00000503 の内容がこれに該当します。



3-6. Import Lookup Table RVAs
インポートするメソッドを表す Hint/Name Table への RVAs が格納されます。Import Table Import Directory エントリにある ImportLookupTable の指す先がここ。NULL 終端の配列になっていますが、マネージドアセンブリでは、通常 mscoree.dll の _CorExeMain(exe 向け) か _CorDllMain(dll 向け)しかインポートしませんので、1 要素のみがある状態になります。似たようなテーブルに IAT がありますが、Import Lookup Table のほうは実行時も書き換えられることはありません。DWORD の配列ですが、慣習的に %INCLUDE%\WinNT.h に定義されてる IMAGE_THUNK_DATA 構造体の配列として表されるのは同じですね。
hello.exe では以下の図の通り、0x00000504 ~ 0x0000050B の内容がこれに該当します。



3-7. Import Method Hint Name
インポートするメソッドを表す Hint/Name が格納されます。Import Address Table(IAT) RVAs および Import Lookup Table RVAs が指す先がここ。定義は以下の通りです。
OffsetSizeFieldDescription
02Hint0 初期化。
2variableName大文字・小文字を識別する、NULL 終端 ASCII 文字列。exe 向けには"_CorExeMain"、dll 向けには"_CorDllMain"が入る。
hello.exe では以下の図の通り、0x00000510 ~ 0x0000051D の内容がこれに該当します。



3-8. Import DLL Name
インポートする DLL を表す Name が格納されます。Import Table Import Directory エントリにある Name の指す先がここ。NULL 終端の ASCII 文字列になっています。マネージドアセンブリでは、"mscoree.dll"が入ることになります。
hello.exe では以下の図の通り、0x0000051E ~ 0x0000052D の内容がこれに該当します。



3-9. x86 エントリポイントスタブ
0xFF, 0x25 は無条件ジャンプ命令(JMP)、オペランドには Import Address Table RVAs の先頭アドレスを指定することになってます。PE オプショナルヘッダの Entry Point RVA の指す先がここ。これで、mscoree.dll の _CorExeMain(exe 向け) か _CorDllMain(dll 向け)へジャンプすることになります。
hello.exe では以下の図の通り、0x0000052E ~ 0x00000533 の内容がこれに該当します。



4. .reloc セクション
これがまだなんのためにあるのかわかっていないというのが今後の課題です (^_^;) x86 エントリポイントスタブのオペランドが配置される RVA について、0xFFFFF000 のマスクをかけた情報を Fix-Up Table に持っているようなのですが、これがなんに使われるのか・・・。

4-1. Fix-Up Table
上にも書いた通り、入っているものはわかるのですが、いまいち用途がわかってません。仕様書では、[25.3.2 Relocations] に記載があります。CIL のみで構成されたイメージの場合、最低限 IMAGE_REL_BASED_HIGHLOW (0x3) 型の Fixup が x86 エントリポイントスタブのために必要となる、みたいなことが書いてあります。定義は以下の通り。IMAGE_BASE_RELOCATION 構造体として、%INCLUDE%\WinNT.h にも定義されてます。
OffsetSizeFieldDescription
04PageRVAFixup の適用を必要とする RVA が入る。下位 12 ビットが 0 になるよう、0xFFFFF000 でマスクされたものが入る。
44Block SizeFixup ブロックのトータルサイズ。PageRVA と Block Size フィールドも含む。4 の倍数になるよう調整。
84 bitsType適用する Fixup 種別が入る。
812 bitsOffsetPageRVA で指定した RVA の残り 12 ビットを Offset として格納。
hello.exe では以下の図の通り、0x00000600 ~ 0x0000060B の内容がこれに該当します。


ここまでで PE ファイルフォーマットに関する仕様は終わりです。お次は積み残してあったメタデータの内容に切り込んで行きましょう。




Hello Meta Data!
最初はここまで中身を知ろうとは全くもって考えていなかったんですが、後から紹介する Meta Data API や他のアンマネージ API を使おうとすると、結局ここの知識が必要になるという・・・。System.Reflection.Emit 名前空間の各クラスや、Mono.Cecil がいかにユーザーフレンドリーに作られているかが身にしみてわかります。
ただ、PE ファイルフォーマットにあるような、あっちに行ったりこっちに行ったりということが少ないと思いますので、一度雰囲気が分かれば、目リフレクションもできないわけでもないかも。
仕様書では、[24 Metadata physical layout] というセクションで説明がされています。PE ファイルフォーマットで位置だけ触れた 3-4. メタデータの中身について、さらに細かく色分けしてみたのが以下の図です。構成要素は以下の通り:
1. メタデータルート
2. #~ ストリームヘッダ
3. #Strings ヒープヘッダ
4. #US ヒープヘッダ
5. #GUID ヒープヘッダ
6. #Blob ヒープヘッダ
7. #~ ストリーム
8. #Strings ヒープ
9. #US ヒープ
10.#GUID ヒープ
11.#Blob ヒープ

PE ファイルフォーマットの時にあった共通のルールはここでも有効です。では、各要素について早速見て行きたいと思います。


1. メタデータルート
PE ファイルフォーマット上では、IL メソッドボディに続いて、なにやら意味ありげな 'B', 'S', 'J', 'B', ・・・の Magic Signature が、メタデータルートの開始を教えてくれます。1998 年当時、CLR の開発チームの主要メンバーだった、Brian Harry さん、Susan Radke-Sproull さん、Jason Zander さん、そして Bill Evans さんらの頭文字を取ったものらしいですが、あらゆる .NET アプリケーションに、自身のイニシャルが刻み込まれるというのはすごいですよね。
偉大な先人に思いを馳せつつ、定義を見ていくことにします。仕様書では [24.2.1 Metadata root] で説明されています。
OffsetSizeFieldDescription
04SignatureMagic Signature:0x424A5342。
42MajorVersionメジャーバージョン。1 で固定。
62MinorVersionマイナーバージョン。1 で固定。
84Reserved常に 0。予約されてる。
124LengthVersion 文字列に割り当てられるバイト長(NULL 終端含む)。4 の倍数で調整される。
16mVersionNULL 終端、UTF8 文字列。長さは Length で示される。
16+mx-m4 の倍数で調整。余りは 0 パディング。
16+x2Flags常に 0。予約されてる。
16+x+22Streamsストリーム数。
Version もコンパイラによって色々な値が設定されます・・・はい、自分が作成したバイナリが動かなかったら、SSCLI 2.0 の csc で作ったバイナリと DIFF 取ればいいじゃん、と考えていた時期が私にもありました。長さを別項に持つ、可変長 Version 文字列がこの位置にあるということは、リビジョン番号の桁が増えると・・・ (ToT)
hello.exe では以下の図の通り、0x00000268 ~ 0x00000287 の内容がこれに該当します。



2. #~ ストリームヘッダ
「ちるだ すとりーむ へっだ」と読めばいいのかな?#~ ストリームはメタデータの論理構造であるメタデータテーブルの物理的な表現ですが、これへのオフセット/長さを示します。なお、これ以降、メタデータルートに示されたストリーム数分ヘッダが続きますが、ヘッダの定義は全て同じですのでここで紹介するのみとしましょう。
OffsetSizeFieldDescription
04Offsetこのヘッダが示すストリーム開始位置のオフセット。メタデータルートの先頭からのバイト数で表す。
44Sizeこのヘッダが示すストリームのサイズ。4 の倍数で調整。
8Nameこのヘッダが示すストリームの名前。NULL 終端、ASCII 文字列。4 の倍数で長さが調整され、残りは 0 パディング。32 文字以内に制限される。
hello.exe では以下の図の通り、0x00000288 ~ 0x00000293 の内容がこれに該当します。



3. #Strings ヒープヘッダ
#Strings ヒープへのオフセット/長さを提示します。
hello.exe では以下の図の通り、0x00000294 ~ 0x000002A7 の内容がこれに該当します。



4. #US ヒープヘッダ
#US ヒープへのオフセット/長さを提示します。
hello.exe では以下の図の通り、0x000002A8 ~ 0x000002B3 の内容がこれに該当します。



5. #GUID ヒープヘッダ
#GUID ヒープへのオフセット/長さを提示します。
hello.exe では以下の図の通り、0x000002B4 ~ 0x000002C3 の内容がこれに該当します。



6. #Blob ヒープヘッダ
#Blob ヒープへのオフセット/長さを提示します。
hello.exe では以下の図の通り、0x000002C4 ~ 0x000002D3 の内容がこれに該当します。



7. #~ ストリーム
メタデータの論理構造であるメタデータテーブルの物理的な表現です。仕様書では、[24.2.6 #~ stream] に説明があります。以下のような定義になってます。
OffsetSizeFieldDescription
04Reserved常に 0。予約されてる。
41MajorVersionメタデータテーブルスキーマのメジャーバージョン。2 で固定。
51MinorVersionメタデータテーブルスキーマのマイナーバージョン。0 で固定。
61HeapSizesヒープサイズを表すフラグ。0x01(#String ヒープサイズ >= 2^16)、0x02(#GUID ヒープサイズ >= 2^16)、0x04(#Blob ヒープサイズ >= 2^16)を指定。
71Reserved常に 1。予約されてる。
88Validどのメタデータテーブルが存在するかを表すビット列。ビット位置が各メタデータテーブルの識別子に対応する。
168Sortedどのメタデータテーブルがソートされているかを表すビット列。ビット位置が各メタデータテーブルの識別子に対応する。
244*nRows各メタデータテーブルの行数を unsigned int の配列で保持。
24+4*nTablesメタデータテーブルが列挙される。
Valid/Sorted の意味がわかりにくい・・・私も理解するのに丸 1 日費やしました (>_<)
見方は両方とも同じなので、Valid について hello.exe を例にとって説明します。そこには { 0x47, 0x14, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, ・・・ } という値が設定されてるはずですが、これはメモリ上の表現で最下位ビットから順に書き出すと、1110001000101000000000000000000010010000000000000000000000000000・・・ になります(リトルエンディアン!リトルエンディアン!)。この順にメタデータテーブルの識別子と対応を取っていくことになります。0 ビット目セット → Module テーブル(0x00)あり、1 ビット目セット → TypeRef テーブル(0x01)あり、2 ビット目セット → TypeDef テーブル(0x02)あり、3 ビット目は該当するメタデータテーブルないので飛ばし、4 ビット目リセット → Field テーブル(0x04)なし・・・みたいな感じで。しかしここは Mono.Cecil のソースコード読めなかったらホント理解できずに終わってました。危ない危ない。
各テーブルのレコード内容については、Meta Data API を使ったソースコード解説の時にも見ますので、ここでは割愛です。 hello.exe では以下の図の通り、0x000002D4 ~ 0x000003B7 の内容がこれに該当します。



8. #Strings ヒープ
各メタデータテーブルで扱う文字列がここに収められます。アセンブリ名とかメソッド名とかですね。可変長の NULL 終端 UTF-8 文字列が 1 つ 1 つのレコードになっています。各テーブルからはインデックスで参照されるので、効率が気になるところ。必要になるまでは名前を取得しない(メタデータトークンのまま引き回す)、1 回目でうまくキャッシュしておく、などの工夫が必要そうです。ちなみに 1 レコード目は必ず空文字になります。仕様書では、[24.2.3 #Strings heap] に説明があります。
hello.exe では以下の図の通り、0x000003B8 ~ 0x0000046B の内容がこれに該当します。


9. #US ヒープ
プログラム中で扱う固定文字列がここに収められます。hello.exe の例ですと"Hello World!"。
後述の #Blob ヒープと同様、各レコードは先頭にバイト長を持ち、その後その中身が続くという構造になってます。例によって先頭レコードは必ず "\0"です。hello.exe の例ですと、先頭の 1 バイトがバイト長を表す情報として扱われていまs・・・あれ?それだと 127 文字(#US ヒープでは NULL 終端 Unicode で文字列が取り扱われる)しか使えなくない?と思ってしまうのですが、以下のルールで無駄な領域を圧縮してるだけですのでご安心ください (^_^;)
  • 最初の 4 バイトが 110bbbbb(2) + x + y + z(最上位ビットが 1、次のビットも 1、その次のビットが 0)だったら、bbbbb(2) << 24 + x << 16 + y << 8 + z(上位ワードの上位バイト残りの 5 ビットを 24 ビット左シフトして、上位ワードの下位バイトを 16 ビット左シフトしたものと、下位ワードの上位バイトを 8 ビット左シフトしたものと、下位ワードの下位バイトと足す)を実際の数値として扱う。
  • 最初の 2 バイトが 10bbbbbb(2) + x(最上位ビットが 1、次のビットが 0)だったら、bbbbbb(2) << 8 + x(上位バイト残りの 6 ビットを 8 ビット左シフトして、下位バイトと足す)を実際の数値として扱う。
  • 最初の 1 バイトが 0bbbbbbb(2)(最上位ビットが 0)だったら、bbbbbbb(2)(残りの 7 ビット)を実際の数値として扱う。
#US ヒープであれば、64 文字未満は一番下の方法、64 文字以上 16384 文字未満は 2 番目の方法、そして 16384 文字以上 268435456 文字未満は一番上の方法になる、と。まあ大体 2 番目の方法までで収まりそうな気はします。 hello.exe では以下の図の通り、0x0000046C ~ 0x00000487 の内容がこれに該当します。



10.#GUID ヒープ
Module と紐付く GUID がここに収められます。仕様書では [24.2.5 #GUID heap] に記述があります。ここもコンパイルの度に変化する部分なので、DIFF を取る場合はうまく避けなければいけないですね。データ自体は特に取り立ててエンコーディングもされず、そのまま格納されます。
hello.exe では以下の図の通り、0x00000488 ~ 0x00000497 の内容がこれに該当します。



11.#Blob ヒープ
メソッドのシグネチャ、フィールドのシグネチャ、プロパティのシグネチャ、ローカル変数のシグネチャ、カスタム属性の情報、引数のシグネチャ、戻り値のシグネチャ、ジェネリクス等々、ありとあらゆる情報がごった煮になっている領域です。まあ、名前の通りといえばそうなのですが・・・。
#US ヒープと同様、各レコードは先頭にバイト長を持ち、その後その中身が続くという構造になってます。例によって先頭レコードは必ず "\0"です。仕様書では、[23.2 Blobs and signatures] や [24.2.4 #US and #Blob heaps] に記載があります。シグネチャも膨大な仕様がありますので、とりあえずは今回使用するものだけ、後ほど Meta Data API を使ったソースコードの説明をする際にみていただければと。
hello.exe では以下の図の通り、0x00000498 ~ 0x000004DB の内容がこれに該当します。


さあ、準備が整いました!意図しないバイナリが作られたとしても、もう怖くないでしょう。ソースコードを使った Meta Data API 解説へ進みます!




Hello Meta Data API!
ここまでの説明で、大体のアセンブリのバイナリについて、ざっくりは読めるようにはなったかと思います。そうすると、今度は実際に作って見ようという話になると思いますが、バイト列をごりごり書き込むのはさすがに大変なので、用意されている API に少しは負担をしてもらいましょう。前のセクションで概要だけ説明したメタデータテーブルの詳細な情報が必要になりますので、仕様書の [22 Metadata logical format: tables] や [23 Metadata logical format: other structures]、[25 File format extensions to PE] を参照しつつ書いていきます。
なお、申し訳ないことに、次回に続く Profiling API の調査につなげる意味もあり、今回のサンプルは COM サーバーとしてしか動かないです。冒頭で紹介させていただいたソースコードを動かす場合は、CppTroll/MetaDataApiSample01Test/MetaDataApiSample01Test.cpp で書いているようなドライバ(COM クライアント)が必要ですのでご注意を。メインの処理はできるだけ平易になるよう、CppTroll/MetaDataApiSample01/MetaDataApiSample01.cpp(48) にある HRESULT CExeCreator::Create(BSTR) にまとめました。"Hello World!"をコンソール出力するだけのプログラムを出力するという簡単な処理ですが、全体で 2000 loc ほどあるという・・・ (-_-;) まあ、のんびり見ていきましょう。

処理の概観は以下の通りです:
  1. IMetaDataImport を使ってメタデータをインポート
    1-1. インポートするメンバ、型、そしてアセンブリを取得するために mscorlib.dll をオープン
    1-2. インポートする型を探す
    1-3. インポートするメンバを探す
    1-4. インポートするアセンブリの準備
  2. IMetaDataEmit を使ってメタデータをエミット
    2-1. エミットするメンバ、型、そしてアセンブリを格納するために hello.exe を定義
    2-2. エミットするアセンブリの準備
    2-3. AssemblyRef テーブルの作成
    2-4. TypeRef テーブルの作成
    2-5. MemberRef テーブルの作成
    2-6. Assembly テーブルの作成
    2-7. CustomAttribute テーブルの作成
    2-8. TypeDef テーブルの作成
    2-9. MethodDef テーブルの作成
    2-10.#US ヒープの作成
    2-11.Module の名前の設定
  3. IL メソッドボディのエミット
  4. ICeeFileGen を使って PE フォーマットファイルを生成
    4-1. ICeeFileGen のための生成/破棄メソッドを抽出
    4-2. PE フォーマットファイル生成のための準備
    4-3. 上で作成したメタデータと PE フォーマットファイルのマージ
    4-4. PE フォーマットファイルに全てのデータを書き込み

Meta Data API にも共通の決め事は適用されます・・・ってことは、RVA とかシグネチャが生のまま出てくるってことですね (T_T)


1. IMetaDataImport を使ってメタデータをインポート
Meta Data API でメタデータをインポートするには IMetaDataImport を使います。この API を使うことで、読み込みの際に PE ファイルフォーマットを意識しなくて済むようになるのはいいですね。CppTroll/MetaDataApiSample01/MetaDataApiSample01.cpp(59) 辺りからがこの処理。順に見ていきます。

1-1. インポートするメンバ、型、そしてアセンブリを取得するために mscorlib.dll をオープン

//////////////////////////////////////////////////////////////////////////////////////
// Open mscorlib.dll to get importing members, types and the assembly
// 
path corSystemDirectoryPath;
path mscorlibPath;
{
    WCHAR buffer[MAX_PATH] = { 0 };
    DWORD length = 0;
    hr = ::GetCORSystemDirectory(buffer, MAX_PATH, &length);
    if (FAILED(hr)) 
        return COMError(hr, __FILE__, __LINE__);

    corSystemDirectoryPath = buffer;
    mscorlibPath = buffer;
    mscorlibPath /= L"mscorlib.dll";
}

CComPtr<IMetaDataDispenserEx> pDispMSCorLib;
hr = ::CoCreateInstance(CLSID_CorMetaDataDispenser, NULL, CLSCTX_INPROC_SERVER, 
                        IID_IMetaDataDispenserEx, 
                        reinterpret_cast<void**>(&pDispMSCorLib));
if (FAILED(hr))
    return COMError(hr, __FILE__, __LINE__);


CComPtr<IMetaDataImport2> pImpMSCorLib;
hr = pDispMSCorLib->OpenScope(mscorlibPath.wstring().c_str(), ofRead, 
                              IID_IMetaDataImport2, 
                              reinterpret_cast<IUnknown**>(&pImpMSCorLib));
if (FAILED(hr))
    return COMError(hr, __FILE__, __LINE__);
Meta Data API では、1 つ 1 つのアセンブリを扱う領域をスコープと呼んでます。今回利用するのは mscorlib.dll にある型だけですので、これを新しいスコープに読み込みます。69 行目~77 行目で CLR のシステムディレクトリを取得し、mscorlib.dll へのフルパスを生成。80 行目~85 行目でスコープを扱うための IMetaDataDispenserEx オブジェクトを生成。88 行目~93 行目で mscorlib.dll をスコープに読み込み、IMetaDataImport オブジェクトを生成します。


1-2. インポートする型を探す

//////////////////////////////////////////////////////////////////////////////////////
// Find importing types
// 
mdTypeDef mdtdObject = mdTypeDefNil;
hr = pImpMSCorLib->FindTypeDefByName(L"System.Object", NULL, &mdtdObject);
if (FAILED(hr))
    return COMError(hr, __FILE__, __LINE__);

mdTypeDef mdtdCompilationRelaxationsAttribute = mdTypeDefNil;
hr = pImpMSCorLib->FindTypeDefByName(
                   L"System.Runtime.CompilerServices.CompilationRelaxationsAttribute",
                   NULL, &mdtdCompilationRelaxationsAttribute);
if (FAILED(hr))
    return COMError(hr, __FILE__, __LINE__);

mdTypeDef mdtdRuntimeCompatibilityAttribute = mdTypeDefNil;
hr = pImpMSCorLib->FindTypeDefByName(
                     L"System.Runtime.CompilerServices.RuntimeCompatibilityAttribute",
                     NULL, &mdtdRuntimeCompatibilityAttribute);
if (FAILED(hr))
    return COMError(hr, __FILE__, __LINE__);

mdTypeDef mdtdConsole = mdTypeDefNil;
hr = pImpMSCorLib->FindTypeDefByName(L"System.Console", NULL, &mdtdConsole);
if (FAILED(hr))
    return COMError(hr, __FILE__, __LINE__);
100 行目に mdTypeDef って型が現れました。Meta Data API では、前述のメタデータテーブルと 1 対 1 に対応するメタデータトークンを使ってメタデータを操作します。mdTypeDef もこのメタデータトークンの 1 つで TypeDef テーブル(0x02)に対応します。型と言っても、内部的には単なる unsigned int 型で、最上位バイトが種別、下位 3 バイトがレコード番号(RID)を表すことになってます。例えば、101 行目で "System.Object"を対象のスコープから探していますが、"System.Object"は mscorlib.dll の TypeDef テーブル(0x02)に 2 番目のレコードとして登録されていますので、mdtdObject には、0x02000002 が入ってくることになります。105 行目~122 行目も同様に、必要な型のメタデータトークンを取得しておきます。


1-3. インポートするメンバを探す

//////////////////////////////////////////////////////////////////////////////////////
// Find importing members
// 
mdMethodDef mdmdCompilationRelaxationsAttributeCtor = mdMethodDefNil;
{
    COR_SIGNATURE pSigBlob[] = { 
        IMAGE_CEE_CS_CALLCONV_HASTHIS,  // HASTHIS 
        1,                              // ParamCount
        ELEMENT_TYPE_VOID,              // RetType
        ELEMENT_TYPE_I4                 // ParamType
    };
    ULONG sigBlobSize = sizeof(pSigBlob) / sizeof(COR_SIGNATURE);
    hr = pImpMSCorLib->FindMethod(mdtdCompilationRelaxationsAttribute, L".ctor", 
                                  pSigBlob, sigBlobSize, 
                                  &mdmdCompilationRelaxationsAttributeCtor);
    if (FAILED(hr))
        return COMError(hr, __FILE__, __LINE__);
}

mdMethodDef mdmdRuntimeCompatibilityAttributeCtor = mdMethodDefNil;
{
    COR_SIGNATURE pSigBlob[] = { 
        IMAGE_CEE_CS_CALLCONV_HASTHIS,  // HASTHIS  
        0,                              // ParamCount
        ELEMENT_TYPE_VOID               // RetType
    };                                  
    ULONG sigBlobSize = sizeof(pSigBlob) / sizeof(COR_SIGNATURE);
    hr = pImpMSCorLib->FindMethod(mdtdRuntimeCompatibilityAttribute, L".ctor", 
                                  pSigBlob, sigBlobSize, 
                                  &mdmdRuntimeCompatibilityAttributeCtor);
    if (FAILED(hr))
        return COMError(hr, __FILE__, __LINE__);
}

// ・・・ 同様に、System.Console.WriteLine、System.Object..ctor を探す。
型が見つかったら、次はインポートするメンバを探します。シグネチャはここで必要になります。ぱっと見、なるほど、リフレクションと同じで、引数の型や数を問い合わせに含めればいいだけね、と思うのですが、バイトの並びが決まっているという大きな罠ががが・・・これはめんどい・・・('A`) 使いやすいライブラリを作るときは、ここのラップは必須ですね。シグネチャの定義は、[23.2.1 MethodDefSig]、[23.2.11 RetType]、[23.2.10 Param]、[23.2.12 Type] を参照しながら、当てはまる項目を並べていきましょう。


1-4. インポートするアセンブリの準備

//////////////////////////////////////////////////////////////////////////////////////
// Prepare importing assembly
// 
CComPtr<IMetaDataAssemblyImport> pAsmImpMSCorLib;
hr = pImpMSCorLib->QueryInterface(IID_IMetaDataAssemblyImport, 
                                  reinterpret_cast<void**>(&pAsmImpMSCorLib));
if (FAILED(hr))
    return COMError(hr, __FILE__, __LINE__);

mdAssembly mdaMSCorLib = mdAssemblyNil;
hr = pAsmImpMSCorLib->GetAssemblyFromScope(&mdaMSCorLib);
if (FAILED(hr))
    return COMError(hr, __FILE__, __LINE__);

auto_ptr<PublicKeyBlob> pMSCorLibPubKey;
DWORD msCorLibPubKeySize = 0;
auto_ptr<WCHAR> msCorLibName;
ASSEMBLYMETADATA amdMSCorLib;
::ZeroMemory(&amdMSCorLib, sizeof(ASSEMBLYMETADATA));
DWORD msCorLibAsmFlags = 0;
{
    ULONG nameSize = 0;
    DWORD asmFlags = 0;
    hr = pAsmImpMSCorLib->GetAssemblyProps(mdaMSCorLib, NULL, NULL, NULL, NULL, 0, 
                                           &nameSize, &amdMSCorLib, 
                                           &asmFlags);
    if (FAILED(hr))
        return COMError(hr, __FILE__, __LINE__);

    msCorLibAsmFlags |= (asmFlags & ~afPublicKey);
    msCorLibName = auto_ptr<WCHAR>(new WCHAR[nameSize]);
    amdMSCorLib.szLocale = amdMSCorLib.cbLocale ? 
                                new WCHAR[amdMSCorLib.cbLocale] : NULL;
    amdMSCorLib.rOS = amdMSCorLib.ulOS ? new OSINFO[amdMSCorLib.ulOS] : NULL;
    amdMSCorLib.rProcessor = amdMSCorLib.ulProcessor ? 
                                new ULONG[amdMSCorLib.ulProcessor] : NULL;

    void *pPubKey = NULL;
    hr = pAsmImpMSCorLib->GetAssemblyProps(mdaMSCorLib, 
                                           const_cast<const void**>(&pPubKey), 
                                           &msCorLibPubKeySize, NULL, 
                                           msCorLibName.get(), nameSize, NULL, 
                                           &amdMSCorLib, NULL);
    if (FAILED(hr))
        return COMError(hr, __FILE__, __LINE__);

    if (msCorLibPubKeySize)
        if (!::StrongNameTokenFromPublicKey(reinterpret_cast<BYTE*>(pPubKey), 
                                            msCorLibPubKeySize, 
                                            reinterpret_cast<BYTE**>(&pPubKey), 
                                            &msCorLibPubKeySize))
            return COMError(::StrongNameErrorInfo(), __FILE__, __LINE__);

    pMSCorLibPubKey = auto_ptr<PublicKeyBlob>(
                    reinterpret_cast<PublicKeyBlob*>(new BYTE[msCorLibPubKeySize]));
    ::memcpy_s(pMSCorLibPubKey.get(), msCorLibPubKeySize, pPubKey, 
               msCorLibPubKeySize);

    if (msCorLibPubKeySize)
        ::StrongNameFreeBuffer(reinterpret_cast<BYTE*>(pPubKey));
}
IMetaDataImport として扱っていたオブジェクトを一旦 IMetaDataAssemblyImport に扱い直し(196 行目~)、アセンブリを最終的にインポートする際に必要になる情報の準備をしておきます。mscorlib.dll は厳密名を持っていますので、アセンブリを参照するには公開キーが必要となります。領域の節約のため、通常は公開キーそのものでなく、公開キーを表すトークン(PublicKeyToken)を扱うので、その変換も 239 行目~252 行目で行っています。


2. IMetaDataEmit を使ってメタデータをエミット
Meta Data API でメタデータをエミットするには IMetaDataEmit を使います。この API を使えば、最終的に PE ファイルフォーマットを意識せずに書き込める、なんてことはありません(えー。IMetaDataEmit がやってくれることは、前述のメタデータ部分の作成を手伝ってくれることで、IL メソッドボディや、PE ファイルフォーマットは別に扱うことになるんですよね・・・。
さて、メタデータテーブルの作成でしたら、IMetaDataEmit にはほぼ 1 対 1 に対応したインターフェースがありますので、これを使っていきます。CppTroll/MetaDataApiSample01/MetaDataApiSample01.cpp(260) 辺りからがこの処理になります。
あと、「エミット」って日本語になりきれてないとは思うのですが、いい翻訳が見つからない・・・ (T_T) スミマセン。

2-1. エミットするメンバ、型、そしてアセンブリを格納するために hello.exe を定義

//////////////////////////////////////////////////////////////////////////////////////
// Define hello.exe to store emitting members, types and the assembly
// 
CComPtr<IMetaDataDispenserEx> pDispHello;
hr = ::CoCreateInstance(CLSID_CorMetaDataDispenser, NULL, CLSCTX_INPROC_SERVER, 
                                   IID_IMetaDataDispenserEx, 
                                   reinterpret_cast<void**>(&pDispHello));
if (FAILED(hr))
    return COMError(hr, __FILE__, __LINE__);

CComPtr<IMetaDataEmit2> pEmtHello;
hr = pDispHello->DefineScope(CLSID_CorMetaDataRuntime, 0, IID_IMetaDataEmit2, 
                             reinterpret_cast<IUnknown**>(&pEmtHello));
if (FAILED(hr))
    return COMError(hr, __FILE__, __LINE__);
hello.exe を作成する新しいアセンブリを扱うことになるので、やはり新しいスコープを生成し、IMetaDataEmit に割り当てます。特に何か特殊なことをやっているわけではないので、次へ進みます。


2-2. エミットするアセンブリの準備

//////////////////////////////////////////////////////////////////////////////////////
// Prepare emitting assembly
// 
CComPtr<IMetaDataAssemblyEmit> pAsmEmtHello;
hr = pEmtHello->QueryInterface(IID_IMetaDataAssemblyEmit, 
                               reinterpret_cast<void**>(&pAsmEmtHello));
if (FAILED(hr))
    return COMError(hr, __FILE__, __LINE__);
アセンブリの情報を扱うために、IMetaDataEmit を IMetaDataAssemblyEmit としても扱えるようにしておきます。さくさく進みます。


2-3. AssemblyRef テーブルの作成

//////////////////////////////////////////////////////////////////////////////////////
// Create AssemblyRef table
// 
mdAssemblyRef mdarMSCorLib = mdAssemblyRefNil;
hr = pAsmEmtHello->DefineAssemblyRef(pMSCorLibPubKey.get(), msCorLibPubKeySize, 
                                     msCorLibName.get(), &amdMSCorLib, 
                                     NULL, 0, 
                                     msCorLibAsmFlags, &mdarMSCorLib);
if (FAILED(hr))
    return COMError(hr, __FILE__, __LINE__);
1-4. インポートするアセンブリの準備で作成しておいた公開キーを表すトークンと名前を使い、AssemblyRef テーブルを作成します。仕様書では、[22.5 AssemblyRef : 0x23] に詳細な情報が記述されています。#Strings ヒープや #Blob ヒープへの書き込みは、意識しなくていいようになってます。


2-4. TypeRef テーブルの作成

//////////////////////////////////////////////////////////////////////////////////////
// Create TypeRef table
// 
mdTypeRef mdtrObject = mdTypeRefNil;
hr = pEmtHello->DefineTypeRefByName(mdarMSCorLib, L"System.Object", &mdtrObject);
if (FAILED(hr))
    return COMError(hr, __FILE__, __LINE__);

mdTypeRef mdtrCompilationRelaxationsAttribute = mdTypeRefNil;
hr = pEmtHello->DefineTypeRefByName(mdarMSCorLib, 
                   L"System.Runtime.CompilerServices.CompilationRelaxationsAttribute", 
                   &mdtrCompilationRelaxationsAttribute);
if (FAILED(hr))
    return COMError(hr, __FILE__, __LINE__);
// ・・・ 同様に System.Runtime.CompilerServices.RuntimeCompatibilityAttribute、System.Console への TypeRef レコードを追加
TypeRef テーブルの作成には DefineTypeRefByName メソッドを使います。もう一つそれっぽい DefineImportType メソッドっていうのがあるのですが、こちらはどうも上手く動きません。頼りの SSCLI 2.0 の csc でも、利用箇所が非常に限定されており、デバッグブレークできる状態に持っていけなかったり。TypeRef テーブルについては、仕様書 [22.38 TypeRef : 0x01] にある通り、DefineTypeRefByName に渡している情報で事足りるようなのでこれでいいのかもしれませんが、次に出てくる DefineImportMember と対象性がないせいで心配になります。また一段絡したら調べてみたいですね。


2-5. MemberRef テーブルの作成

//////////////////////////////////////////////////////////////////////////////////////
// Create MemberRef table
// 
mdMemberRef mdmrCompilationRelaxationsAttributeCtor = mdMemberRefNil;
hr = pEmtHello->DefineImportMember(pAsmImpMSCorLib, NULL, 0, pImpMSCorLib, 
                                   mdmdCompilationRelaxationsAttributeCtor, 
                                   pAsmEmtHello, 
                                   mdtrCompilationRelaxationsAttribute, 
                                   &mdmrCompilationRelaxationsAttributeCtor);
if (FAILED(hr))
    return COMError(hr, __FILE__, __LINE__);

mdMemberRef mdmrRuntimeCompatibilityAttributeCtor = mdMemberRefNil;
hr = pEmtHello->DefineImportMember(pAsmImpMSCorLib, NULL, 0, pImpMSCorLib, 
                                   mdmdRuntimeCompatibilityAttributeCtor, 
                                   pAsmEmtHello, 
                                   mdtrRuntimeCompatibilityAttribute, 
                                   &mdmrRuntimeCompatibilityAttributeCtor);
if (FAILED(hr))
    return COMError(hr, __FILE__, __LINE__);
// ・・・ 同様に System.Console.WriteLine、System.Object..ctor への MemberRef レコードを追加
MemberRef テーブルの作成です。仕様書では、[22.25 MemberRef : 0x0A] に言及があります。仰々しい数の引数を要求されますが、今まで使ったものを指定するだけです。ただ、前述の通り、TypeRef テーブルの作成時に使うメソッドと対象性がなく、また要求される情報が仕様書に比べ極端に多いので不安が残るのですが・・・。とりあえず今回は、次に進みましょう。あ、IL メソッドボディの作成に必要になるので、各メソッドの mdMemberRef トークンには、後からでもアクセスできるようにしておく必要があります。


2-6. Assembly テーブルの作成

//////////////////////////////////////////////////////////////////////////////////////
// Create Assembly table
// 
mdAssembly mdaHello = mdAssemblyNil;
ASSEMBLYMETADATA amdHello;
::ZeroMemory(&amdHello, sizeof(ASSEMBLYMETADATA));
hr = pAsmEmtHello->DefineAssembly(NULL, 0, CALG_SHA1, L"hello", &amdHello, afPA_None, 
                                  &mdaHello);
if (FAILED(hr))
    return COMError(hr, __FILE__, __LINE__);
Assembly テーブルの作成です。Assembly テーブルの作成は、IMetaDataEmit ではなく IMetaDataAssemblyEmit を使うので、ちょっと注意が必要かもです。仕様書では、[22.2 Assembly : 0x20] として記載があります。


2-7. CustomAttribute テーブルの作成

//////////////////////////////////////////////////////////////////////////////////////
// Create CustomAttribute table
// 
WORD const CustomAttributeProlog = 0x0001;

mdCustomAttribute mdcaCompilationRelaxationsAttribute = mdCustomAttributeNil;
{
    SimpleBlob sb;
    sb.Put<WORD>(CustomAttributeProlog);        // Prolog
    sb.Put<DWORD>(8);                           // FixedArg, int32: 8
    sb.Put<WORD>(0);                            // NumNamed, Count: 0
    hr = pEmtHello->DefineCustomAttribute(mdaHello, 
                                          mdmrCompilationRelaxationsAttributeCtor, 
                                          sb.Ptr(), 
                                          sb.Size(), 
                                          &mdcaCompilationRelaxationsAttribute);
    if (FAILED(hr))
        return COMError(hr, __FILE__, __LINE__);
}

mdCustomAttribute mdcaRuntimeCompatibilityAttribute = mdCustomAttributeNil;
{
    SimpleBlob sb;
    sb.Put<WORD>(CustomAttributeProlog);        // Prolog
    sb.Put<WORD>(1);                            // NumNamed, Count: 1
    sb.Put<BYTE>(SERIALIZATION_TYPE_PROPERTY);  // PROPERTY
    sb.Put<BYTE>(ELEMENT_TYPE_BOOLEAN);         // FieldOrPropType: bool
    {
        string name("WrapNonExceptionThrows");
        sb.Put<BYTE>(name.size());              // Name Length: 22
        sb.Put(name.c_str(), name.size());      // Name: "WrapNonExceptionThrows"
    }
    sb.Put<BYTE>(1);                            // FixedArg, bool: 1
    hr = pEmtHello->DefineCustomAttribute(mdaHello, 
                                          mdmrRuntimeCompatibilityAttributeCtor, 
                                          sb.Ptr(), 
                                          sb.Size(), 
                                          &mdcaRuntimeCompatibilityAttribute);
    if (FAILED(hr))
        return COMError(hr, __FILE__, __LINE__);
}
CustomAttribute テーブルの作成も非常に骨が折れるものの 1 つですね。仕様書にある [23.3 Custom attributes] を参照しながら、注意深く #Blob ヒープに入れ込む情報を構成する必要があります。395 行目に現れる SimpleBlob クラスは型に応じて自動的にメモリ領域を拡張しながらバイト列を生成していくクラスです。中ではパフォーマンスのため、ある程度のサイズのメモリを最初に確保し、そこから切り崩していくような形でメモリの割り当てができる、MS 謹製の CQuickArray<T> というクラスを利用してます。%INCLUDE%\corhlpr.h、%INCLUDE%\corhlpr.cpp 辺りにはこのような CLI 向けのヘルパーがいくつも定義されてますので、参考にしてみるのも良いかもしれません。


2-8. TypeDef テーブルの作成

//////////////////////////////////////////////////////////////////////////////////////
// Create TypeDef table
// 
mdTypeDef mdtdMainApp = mdTypeDefNil;
hr = pEmtHello->DefineTypeDef(L"MainApp", tdNotPublic | tdBeforeFieldInit, mdtrObject, 
                              NULL, &mdtdMainApp);
if (FAILED(hr))
    return COMError(hr, __FILE__, __LINE__);
TypeDef テーブルの作成は、仕様書 [22.37 TypeDef : 0x02] のごちゃごちゃした定義の割りには、シンプルなメソッドを呼び出すだけで済むようになってます。ここではガワだけ作成し、FieldList や MethodList、他のクラスの継承やインターフェースの実装などは後付設定が可能なようですね。が、とりあえず複雑なクラス定義を作成するときまで、それらの使用方法は先延ばし。先に進みます。


2-9. MethodDef テーブルの作成

//////////////////////////////////////////////////////////////////////////////////////
// Create MethodDef table
// 
mdMethodDef mdmdMainAppMain = mdMethodDefNil;
{
    COR_SIGNATURE pSigBlob[] = {
        IMAGE_CEE_CS_CALLCONV_DEFAULT,  // DEFAULT   
        0,                              // ParamCount
        ELEMENT_TYPE_VOID               // RetType
    };                                  
    ULONG sigBlobSize = sizeof(pSigBlob) / sizeof(COR_SIGNATURE);
    hr = pEmtHello->DefineMethod(mdtdMainApp, L"Main", 
                                 fdPublic | mdHideBySig | mdStatic, pSigBlob, 
                                 sigBlobSize, 0, 0, &mdmdMainAppMain);
    if (FAILED(hr))
        return COMError(hr, __FILE__, __LINE__);
}

mdMethodDef mdmdMainAppCtor = mdMethodDefNil;
{
    COR_SIGNATURE pSigBlob[] = {
        IMAGE_CEE_CS_CALLCONV_HASTHIS,  // HASTHIS  
        0,                              // ParamCount
        ELEMENT_TYPE_VOID               // RetType
    };                                  
    ULONG sigBlobSize = sizeof(pSigBlob) / sizeof(COR_SIGNATURE);
    hr = pEmtHello->DefineMethod(mdtdMainApp, L".ctor", 
                                 fdPublic | mdHideBySig | mdSpecialName, pSigBlob, 
                                 sigBlobSize, 0, 0, &mdmdMainAppCtor);
    if (FAILED(hr))
        return COMError(hr, __FILE__, __LINE__);
}
MethodDef テーブルの作成では、1-3. インポートするメンバを探すと同様、シグネチャの設定が必要です。[23.2.1 MethodDefSig]、[23.2.11 RetType]、[23.2.10 Param]、[23.2.12 Type] を参照しながら、当てはまる項目を並べていきましょう。


2-10.#US ヒープの作成

//////////////////////////////////////////////////////////////////////////////////////
// Create #US stream
// 
mdString mdsHelloWorld = mdStringNil;
{
    wstring text(L"Hello World!");
    hr = pEmtHello->DefineUserString(text.c_str(), text.size(), &mdsHelloWorld);
    if (FAILED(hr))
        return COMError(hr, __FILE__, __LINE__);
}
コンソールに表示する "Hello World!"は #US ヒープに作成します。DefineUserString メソッドは特に複雑なことをやる必要はないですね。この後、IL メソッドボディで使うので、mdString トークンは取っておきます。


2-11.Module の名前の設定

//////////////////////////////////////////////////////////////////////////////////////
// Set Module name
hr = pEmtHello->SetModuleProps(L"hello.exe");
if (FAILED(hr))
    return COMError(hr, __FILE__, __LINE__);
最後に Module の名前を設定してエミットは一通り終わり。あれ?Module テーブルって作ってなくね?と感じられる方もいらっしゃるかと思いますが、どうも DefineScope とかの際に自動的に生成されてるみたいです。ちなみに、SetModuleProps をやらなくてもアプリは動きます。Reflector では逆アセンブルができなくなりますががが。


3. IL メソッドボディのエミット

//////////////////////////////////////////////////////////////////////////////////////
// Emit Method body
// 
SimpleBlob mbMainAppMain;
mbMainAppMain.Put<BYTE>(OpCodes::Encodings[OpCodes::CEE_NOP].byte2);
mbMainAppMain.Put<BYTE>(OpCodes::Encodings[OpCodes::CEE_LDSTR].byte2);
mbMainAppMain.Put<DWORD>(mdsHelloWorld);
mbMainAppMain.Put<BYTE>(OpCodes::Encodings[OpCodes::CEE_CALL].byte2);
mbMainAppMain.Put<DWORD>(mdmrConsoleWriteLine);
mbMainAppMain.Put<BYTE>(OpCodes::Encodings[OpCodes::CEE_NOP].byte2);
mbMainAppMain.Put<BYTE>(OpCodes::Encodings[OpCodes::CEE_RET].byte2);

SimpleBlob mbMainAppCtor;
mbMainAppCtor.Put<BYTE>(OpCodes::Encodings[OpCodes::CEE_LDARG_0].byte2);
mbMainAppCtor.Put<BYTE>(OpCodes::Encodings[OpCodes::CEE_CALL].byte2);
mbMainAppCtor.Put<DWORD>(mdmrObjectCtor);
mbMainAppCtor.Put<BYTE>(OpCodes::Encodings[OpCodes::CEE_RET].byte2);
手元に各メタデータテーブル要素のトークンが揃っていれば、IL メソッドボディのエミットは驚くほど簡単です。本当にちゃんと作っていくためには、SimpleBlob では機能が足りないですが、結構行けるものですね。ちなみに、nop 命令が入っているのは、動かなかったときに、SSCLI 2.0 の csc で出力した場合の IL メソッドボディに、なるべく近づくようにしたつもりなのですがなくても良かったかも (^_^;)
ldstr 命令のオペランドには、#US ヒープに格納した "Hello World!"を表すトークンを(508 行目)、call 命令のオペランドには、MemberRef テーブルに格納した System.Console.WriteLine を表すトークンを(510 行目)指定します。コンストラクタでは、基底クラスのコンストラクタを呼ぶようにするのをお忘れなく(517 行目)。
ここで使用している OpCodes ですが、SMC や SSCLI 2.0 の csc をみると、%INCLUDE%\opcode.def をうまいこと列挙型や const な配列内に #include していることがわかります。マクロがある言語ならではの技ですね。今回のもそれを参考にしてみました。


4. ICeeFileGen を使って PE フォーマットファイルを生成
IMetaDataEmit が PE ファイルフォーマットの面倒まで見てはくれないと知ってから PE フォーマットの勉強を始めたのですが、最終的にはそれ用の API があったというオチ。その名も ICeeFileGenです。アンマネージ API リファレンスを見ると、Host API に属すこのクラス(COM インターフェースではなく、クラスです!)、MSDN 的には詳細な説明もなく、使って欲しくない感全開なのですが、リファレンス実装である SSCLI 2.0 の csc を参考にすることで使い方はバッチリわかるのでしれっと利用します。ちなみに、以前の記事で使い方を探していた ICeeGen は罠なので気をつけましょう(インスタンスを生成する方法がない。加えて .NET 4 になってからは Obsolete とされてしまったので、今後これが使えるようになる日はないと思われ)。

4-1. ICeeFileGen のための生成/破棄メソッドを抽出

//////////////////////////////////////////////////////////////////////////////////
// Extract the creating/destroying methods for ICeeFileGen
// 
typedef HRESULT (__stdcall *CreateCeeFileGenPtr)(ICeeFileGen **ceeFileGen);
typedef HRESULT (__stdcall *DestroyCeeFileGenPtr)(ICeeFileGen **ceeFileGen);

CreateCeeFileGenPtr pfnCreateCeeFileGen = NULL;
DestroyCeeFileGenPtr pfnDestroyCeeFileGen = NULL;

pfnCreateCeeFileGen = reinterpret_cast<CreateCeeFileGenPtr>(
                                ::GetProcAddress(hmodCorPE, "CreateICeeFileGen"));
if (!pfnCreateCeeFileGen)
    return SystemError(::GetLastError(), __FILE__, __LINE__);
    
pfnDestroyCeeFileGen = reinterpret_cast<DestroyCeeFileGenPtr>(
                               ::GetProcAddress(hmodCorPE, "DestroyICeeFileGen"));
if (!pfnDestroyCeeFileGen)
    return SystemError(::GetLastError(), __FILE__, __LINE__);

ICeeFileGen* pCeeFileGen = NULL;
hr = pfnCreateCeeFileGen(&pCeeFileGen);
if (FAILED(hr))
    return COMError(hr, __FILE__, __LINE__);
BOOST_SCOPE_EXIT((pCeeFileGen)(pfnDestroyCeeFileGen))
{
    pfnDestroyCeeFileGen(&pCeeFileGen);
}
BOOST_SCOPE_EXIT_END
使い方がバッチリ、とは言っても元々 .NET インフラ向けのクラスですので、生成や破棄は一手間かかります。mscoree.dll から ICeeFileGen 生成用のメソッドである CreateCeeFileGen の取得(549 行目)および 破棄用のメソッドである DestroyCeeFileGen を取得(554 行目)します。生成と破棄処理の事前登録もここで行っておきましょう(560 行目~567 行目)。


4-2. PE フォーマットファイル生成のための準備

//////////////////////////////////////////////////////////////////////////////////
// Prepare to generate the PE format file
// 
HCEEFILE ceeFile = NULL;
hr = pCeeFileGen->CreateCeeFileEx(&ceeFile, ICEE_CREATE_FILE_PURE_IL);
if (FAILED(hr))
    return COMError(hr, __FILE__, __LINE__);
BOOST_SCOPE_EXIT((ceeFile)(pCeeFileGen))
{
    pCeeFileGen->DestroyCeeFile(&ceeFile);
}
BOOST_SCOPE_EXIT_END

hr = pCeeFileGen->SetOutputFileName(ceeFile, L"hello.exe");
if (FAILED(hr))
    return COMError(hr, __FILE__, __LINE__);

hr = pCeeFileGen->SetComImageFlags(ceeFile, COMIMAGE_FLAGS_ILONLY);
if (FAILED(hr))
    return COMError(hr, __FILE__, __LINE__);

hr = pCeeFileGen->SetSubsystem(ceeFile, IMAGE_SUBSYSTEM_WINDOWS_CUI, 4, 0);
if (FAILED(hr))
    return COMError(hr, __FILE__, __LINE__);
ICeeFileGen を利用し、PE フォーマットファイル出力のための事前準備をしてしまいます。情報の操作に利用する共通のハンドルを生成し(575 行目)、ファイル名の設定(584 行目)、イメージのフラグ設定(588 行目)、イメージのサブシステムの設定(592 行目)を行っておきます。


4-3. 上で作成したメタデータと PE フォーマットファイルのマージ

//////////////////////////////////////////////////////////////////////////////////
// Merge the meta data created above and the PE format file
// 
HCEESECTION textSection = NULL;
hr = pCeeFileGen->GetIlSection(ceeFile, &textSection);
if (FAILED(hr))
    return COMError(hr, __FILE__, __LINE__);

{
    COR_ILMETHOD_FAT fatHeader;
    ::ZeroMemory(&fatHeader, sizeof(COR_ILMETHOD_FAT));
    fatHeader.SetMaxStack(1);
    fatHeader.SetCodeSize(mbMainAppMain.Size());
    fatHeader.SetLocalVarSigTok(mdTokenNil);
    fatHeader.SetFlags(0);
    
    unsigned headerSize = COR_ILMETHOD::Size(&fatHeader, false);
    unsigned totalSize = headerSize + mbMainAppMain.Size();

    BYTE *pBuffer = NULL;
    hr = pCeeFileGen->GetSectionBlock(textSection, totalSize, 1, 
                                      reinterpret_cast<void**>(&pBuffer));
    if (FAILED(hr))
        return COMError(hr, __FILE__, __LINE__);

    ULONG offset = 0;
    hr = pCeeFileGen->GetSectionDataLen(textSection, &offset);
    if (FAILED(hr))
        return COMError(hr, __FILE__, __LINE__);

    offset -= totalSize;
    ULONG codeRVA = 0;
    hr = pCeeFileGen->GetMethodRVA(ceeFile, offset, &codeRVA);
    if (FAILED(hr))
        return COMError(hr, __FILE__, __LINE__);
    
    hr = pEmtHello->SetMethodProps(mdmdMainAppMain, -1, codeRVA, 0);
    if (FAILED(hr))
        return COMError(hr, __FILE__, __LINE__);

    pBuffer += COR_ILMETHOD::Emit(headerSize, &fatHeader, false, pBuffer);
    ::memcpy_s(pBuffer, totalSize - headerSize, mbMainAppMain.Ptr(), 
               mbMainAppMain.Size());
}
// ・・・ 同様に MainApp..ctor の IL メソッドボディも作成

pCeeFileGen->SetEntryPoint(ceeFile, mdmdMainAppMain);
if (FAILED(hr))
    return COMError(hr, __FILE__, __LINE__);

hr = pCeeFileGen->EmitMetaDataEx(ceeFile, pEmtHello);
if (FAILED(hr))
    return COMError(hr, __FILE__, __LINE__);
ここまで来ればもう一息!今まで作成した IL メソッドボディ、メタデータを順にマージします。PE フォーマットのところで書いた通り、IL メソッドボディはメタデータとは別に領域を確保し、RVA の計算などをする必要がありますが、ICeeFileGen のメソッドを使うことで、GetSectionBlock(領域確保)→GetSectionDataLen(確保後の.text セクションサイズ取得)→GetMethodRVA(オフセットを RVA に変換)のわずか 3 ステップでできちゃいます(617 行目~632 行目)。出来上がった RVA は、IMetaDataEmit::SetMethodProps で対応する MethodDef テーブルに記録しておきます。また、今回は FAT メソッドヘッダは使わないと言いつつ COR_ILMETHOD_FAT 構造体が出てきてますが(607 行目)、SSCLI 2.0 の csc のソースを見る限り兼用してるみたい。COR_ILMETHOD::Emit でよしなにしてくれます(638 行目)。
あとはこれ以外のメタデータ。ICeeFileGen::EmitMetaDataEx に IMetaDataEmit を与えるだけというお手軽さです。なぜ最後だけがんばったし (^_^;)


4-4. PE フォーマットファイルに全てのデータを書き込み

//////////////////////////////////////////////////////////////////////////////////
// Write all data to the PE format file
// 
hr = pCeeFileGen->GenerateCeeFile(ceeFile);
if (FAILED(hr))
    return COMError(hr, __FILE__, __LINE__);
書き込みもこれだけです。なぜ最後だけがんばったし (^_^;)
さて、実行すれば、晴れて hello.exe が出力されるはずです。お気に入りのバイナリエディタで中身を見て、また最初から見直すと理解が深まるでしょう。




おわった~
長すぎです (ToT) 駆け足でやってもこのボリュームとは・・・。11 年目に始める CLI の基礎。仕事ではなかなかこういう低レイヤな部分をいじることはできないですが、いかがでしたか?これを機に開発者向けツール開発に興味を持っていただける方が、少しでも増えるとうれしいですね。

私はこれでやっと次に進めます。次は Profiling API ですっ!

C# 動的メソッド入れ替え - Apply a monkey patch to any static languages on CLR -

$
0
0
たまにはこんな、手品の話も。

会社の飲み会の時のネタとしては使えないですが、仕事では使えること請け合いです! (`・ω・´)
手元にコンパイル済みの dll/exe があるとき、その振る舞いを実行時にだけ変えてみたいってことがしばしばあるかと。例えば、以下のようなシチュエーションが思いつきます・・・というか実際ありました (^_^;)
  • ドキュメントが不十分で挙動がよくわからない。調査するために、デバッグプリントを仕込みたい。
  • 特定の条件になると内部で例外がスローされちゃう。どうもバグみたいなので、修正したいけどソースコードがない。
  • パラメータを変化させながらタイミングを調整したい。開発効率を上げるために、動的言語と連携するような仕組みを一時的に入れ込みたい。
  • 外部機器やネットワーク、DB に依存してて自動テストがしにくい。テスト中はスタブに入れ替えたい。
Ruby や Python みたいな動的言語の世界には「Monkey Patching」って言葉があって、上に書いたようなことが、結構普通に行われてるみたい。こんな強力なことが簡単にできるのはうらやましい限りですが、何事もやりすぎはよろしくないですので、ご利用は計画的にというところでしょう。
これに対して、静的言語である C# や Java、C++ はどうかっていうと、やればできないことはない、というレベル。Java だったら、クラスがロードされるときに Bytecode を書き換えれば良いですし、C++ みたいなメモリを直接触れる言語であれば、ジャンプ先を書き換えちゃうとかで実現は可能だったりします。我らがごった煮言語、C# でも例に漏れず、今回紹介する Profiling API (アンマネージ API)を使えば実現可能です。.NET Framework で良いのは、この API を使ったプロファイルが、CLR 上で動く全てのマネージコードに適用できるということですね。寂しいのは、これを解説して下さる方が、現在日本にいらっしゃらなさそうなところでしょうか (´・ω・`)

お仲間を増やすためにも、できかけでも情報共有させていただくのが早道と、今日も拙い成果を晒させていただきます。C++ 初学者にありがちな、見苦しい失敗もしていそうですが、よろしければ最後までお付き合いくださいませ。

なお、文中にあるソリューションは以下のリンクからダウンロードできます。ビルド環境は、Visual Studio C++ 2008 + Boost C++ Libraries Ver.1.47.0、あと自動テストは Google Test 1.6.0を使っています。環境を整える際は、各ライブラリのインストールをお願いしますです。


こちらのページ/ソフトウェアを参考にさせていただきました!情報発信されてる方には感謝するばかりです (´д⊂)
Download Details - Microsoft Download Center - Shared Source Common Language Infrastructure 2.0 Release
Cecil - Mono
ECMA C# and Common Language Infrastructure Standards
Boost C++ Libraries
Rewrite MSIL Code on the Fly with the .NET Framework Profiling API
boost.exceptionでエラー箇所を階層ごとに記録する。 - in the box
Visual Studio で GoogleTest を使う - かおるんダイアリー
Boostライブラリのビルド方法 - boostjp
2006-01-08 ■[C++][Boost]動的削除子 (dynamic deleter) - 意外と知られていない? boost::shared_ptr の側面 - Cry’s Diary
usskim / ワトソン博士
スタックトレースを表示する - encom wiki
DebugInfo.com - Examples
C言語系/呼び出し規約/x86/naked - Glamenv-Septzen.net
simple-assembly-explorer - Simple Assembly Explorer - Google Project Hosting
MSDN Magazine & Microsoft News
型システム - Wikipedia




目次

準備とか
まずは Profiling API 事始。そもそもプロファイルといえば、元々は、別のアプリケーションの実行を監視するツールということで、各メソッドの実行時間や、アプリケーションのメモリ使用状況を、一定の時間測定するということが主な目的だったはずです。しかし、.NET Framework においては、その対象をコードカバレッジを計測するためのユーティリティや、デバッグ支援ツールなどまでに広げてるのが、ごった煮好きな Microsoft らしいところ。CLR 上で動くアプリケーションには、プロセス境界より柔軟にアプリケーションを分離する仕組みである AppDomin や GC、マネージ例外処理や JIT が存在するため、通常のマシン語コードのプロファイルより難しいのが常ですが、そこは Profiling API が良しなにしてくれることになってます。
カバレッジやデバッグ支援って話が出ましたが、そのための準備を行うのに JIT は絶好の機会。Profiling API は、メモリ内 MSIL コードストリームを変更するための仕組みも提供してるので、動的な処理の変更が可能になるわけですね。
前口上はこれぐらいにして、実際にどうすればいいのかを見ていきます。また、その後実際に実装に入る際の、最低限のフレームワークも作っておきましょう。
  1. .NET Framework でのプロファイルのやり方
  2. アダプタ
  3. スタックトレース


1. .NET Framework でのプロファイルのやり方
ここは日本語になってるので、MSDN の情報が一番でしょう。中にはめずらしく図付きの解説もあります Σ (゚Д゚;)
要点をまとめると以下の通りです。
  • データ収集部分とデータ分析部分は別々のプロセス空間で動く必要があるよ。
  • データ収集部分は、Profiling API を使用して、CLR とやり取りできる dll にしてね。
  • データ分析部分は、データ収集 DLL とはログファイルや名前付きパイプでやり取りする dll/exe にしてね。
データ収集部分で言っている「CLR とやり取りできる dll」というのは、いわゆるインプロセス COM サーバーです。前回のサンプルで、申し訳ないことになっていたのは、この実験も兼ねていたせいもありました (>_<)
具体的には、(1)ICorProfilerCallback* インターフェースを実装したコクラスを作る、(2)それの CLSID を環境変数に登録する、(3)プロファイルを有効にする、という手順を踏むと、CLR のほうで CreateInstance してくれます。インターフェースの実装は後で行うので、先に環境変数の設定を見てみましょう。

C:\Documents and Settings\User>SET COR_PROFILER={1DC70D11-5E46-48C6-BB07-75CFFF188327}
 
C:\Documents and Settings\User>SET COR_ENABLE_PROFILING=1
 
COR_PROFILER に指定するのが、ICorProfilerCallback* インターフェースを実装したコクラスの CLSID になります。MSDNには、このような CLSID 形式の指定以外に、ProgID 形式の指定でも可能とありますが、なぜか私の環境ではうまく拾ってくれませんでした・・・。この状態から、さらに COR_ENABLE_PROFILING を 1 に設定すると、プロファイルが有効になります。無効化する際は、こちらのフラグを 0 に設定するだけで大丈夫です。
なお、あるセッションの環境変数でプロファイルを有効にすると、それ以降、そのセッションから立ち上がる全てのマネージプロセスについて、データ収集する dll がアタッチされることになります。Windows サービスをプロファイルするなどの特別な目的が無い限り、システム環境変数などには登録しないほうが良いでしょう ...( = =)(ぉ


2. アダプタ
データ収集に必要になる ICorProfilerCallback* インターフェースを実装したコクラスですが、実は最新の ICorProfilerCallback2 インターフェースになると、実装しなければならないメソッドはなんと 77 もの数に上ります(ぇー('A`) 私のように、とりあえず動かしながら実装や設計を詰めていく人にとっては、これを毎回作成するだけでも嫌になると思います。ですので、デフォルトで何もしないような実装を提供してくれる、アダプタクラスを作っておきましょう。
ちなみに、まだ全部は試してないのですが、Profiling API は、HRESULT で異常な結果コードを返しても、基本的に握りつぶすような挙動をするようです。ですので、例外を確実に catch し、標準出力に中身のデバッグプリントをベロベロと出してくれるよう機能も持たせておくと、デバッグが楽になるでしょう。
ところで、こういう定型処理は、マクロを持つ言語では非常に楽ですね。Boost.Preprocessorのようなライブラリを組み合わせることで、実際の記述はかなり読みやすく、かつ省略することができます。

#line 11 "CppTroll\ProfilingApiSample02\Urasandesu\NAnonym\DefaultSTDMETHODWrapper.h"
#define NANONYM_DECLARE_DEFAULT_STDMETHOD_WRAPPER_ARG(r, data, i, elem) \
    BOOST_PP_COMMA_IF(i) BOOST_PP_TUPLE_ELEM(2, 0, elem) BOOST_PP_TUPLE_ELEM(2, 1, elem)

#define NANONYM_DECLARE_DEFAULT_STDMETHOD_WRAPPER_LOAD_ARG(r, data, i, elem) \
    BOOST_PP_COMMA_IF(i) BOOST_PP_TUPLE_ELEM(2, 1, elem)

#define NANONYM_DECLARE_DEFAULT_STDMETHOD_WRAPPER(method, args_tuple_seq) \
    public: \
        STDMETHOD(method)(BOOST_PP_SEQ_FOR_EACH_I(NANONYM_DECLARE_DEFAULT_STDMETHOD_WRAPPER_ARG, _, args_tuple_seq)) \
        { \
            using namespace std; \
            using namespace boost; \
            using namespace Urasandesu::NAnonym; \
             \
            try \
            { \
                return method##Core(BOOST_PP_SEQ_FOR_EACH_I(NANONYM_DECLARE_DEFAULT_STDMETHOD_WRAPPER_LOAD_ARG, _, args_tuple_seq)); \
            } \
            catch (NAnonymException &e) \
            { \
                cout << diagnostic_information(e) << endl; \
            } \
            catch (...) \
            { \
                cout << diagnostic_information(current_exception()) << endl; \
            } \
             \
            return S_OK; \
        } \
         \
    protected: \
        STDMETHOD(method##Core)(BOOST_PP_SEQ_FOR_EACH_I(NANONYM_DECLARE_DEFAULT_STDMETHOD_WRAPPER_ARG, _, args_tuple_seq)) \
        { \
            return S_OK; \
        }
 
Boost.Preprocessorのデータ構造を使うことで、Sequence の中に Tuple で (型,変数名)のような定義を埋め込むことができます(args_tuple_seq の部分)。method が実装するメソッド名で、全体を try ~ catch で囲み、catch した例外の中身は全て標準出力に流し込むようにしました。method##Core をオーバーライドし、本来やりたい処理を記述することになります。
これを使って作った、ICorProfilerCallback2 のアダプタクラスはこんな感じです。

#line 11 "CppTroll\ProfilingApiSample02\Urasandesu\NAnonym\Profiling\ICorProfilerCallback2Impl.h"
    template<
        class Base
    >
    class ATL_NO_VTABLE ICorProfilerCallback2Impl : public Base
    {
        NANONYM_DECLARE_DEFAULT_STDMETHOD_WRAPPER(Initialize, ((IUnknown*,pICorProfilerInfoUnk)))
        NANONYM_DECLARE_DEFAULT_STDMETHOD_WRAPPER(Shutdown, BOOST_PP_EMPTY())
        NANONYM_DECLARE_DEFAULT_STDMETHOD_WRAPPER(AppDomainCreationStarted, ((AppDomainID,appDomainId)))
        NANONYM_DECLARE_DEFAULT_STDMETHOD_WRAPPER(AppDomainCreationFinished, ((AppDomainID,appDomainId))((HRESULT,hrStatus)))
        // … こんな感じで 77 個デフォルト実装が続く。
    };
 


3. スタックトレース
C# や Java には、例外をスローすると、自動的に実行時のメソッド呼び出し履歴をキャプチャし、後から参照できる「スタックトレース」なんて便利な機能があります。残念ながら C++ には標準にはありません。なので、例外を作成したときに、同様のことを行う機能を作っておくと便利に使えそうです。%INCLUDE%\DbgHelp.h の各デバッグヘルパー関数を、こちらこちらで説明して下さっているものを参考に作ってみました。メインの処理はこんな感じになってます。

#line 23 "CppTroll\ProfilingApiSample02\Urasandesu\NAnonym\StackTrace.cpp"
    void StackTrace::Capture(INT skipCount,
                 HANDLE hProcess, 
                 HANDLE hThread, 
                 LPCWSTR userSearchPath, 
                 LPCONTEXT pContext)
    {
        using boost::filesystem::path;
        using boost::filesystem::absolute;
        using boost::filesystem::exists;

        m_hProcess = hProcess;
        
        std::string absoluteSearchPath = absolute(userSearchPath).string();
        PSTR userSearchPath_ = exists(absoluteSearchPath) ? 
                                 const_cast<PSTR>(absoluteSearchPath.c_str()) : NULL;

        DWORD options = ::SymGetOptions();
        options |= SYMOPT_LOAD_LINES;
        options &= ~SYMOPT_UNDNAME;
        ::SymSetOptions(options); 

        ::SymInitialize(m_hProcess, userSearchPath_, TRUE);

        STACKFRAME sf; 
        ::ZeroMemory(&sf, sizeof(STACKFRAME));
        if (!pContext)
        {
            unsigned long instPtr;
            unsigned long stackPtr;
            unsigned long basePtr; 

            __asm call(x)
            __asm x: pop eax
            __asm mov [instPtr], eax
            __asm mov [stackPtr], esp
            __asm mov [basePtr], ebp

            sf.AddrPC.Offset = instPtr;
            sf.AddrPC.Mode = AddrModeFlat;
            sf.AddrStack.Offset = stackPtr;
            sf.AddrStack.Mode = AddrModeFlat;
            sf.AddrFrame.Offset = basePtr;
            sf.AddrFrame.Mode = AddrModeFlat;
        }
        else
        {
            sf.AddrPC.Offset = pContext->Eip;
            sf.AddrPC.Mode = AddrModeFlat;
            sf.AddrStack.Offset = pContext->Esp;
            sf.AddrStack.Mode = AddrModeFlat;
            sf.AddrFrame.Offset = pContext->Ebp;
            sf.AddrFrame.Mode = AddrModeFlat;
        }

        while (::StackWalk(IMAGE_FILE_MACHINE_I386, m_hProcess, hThread, &sf, NULL, 
                           NULL, ::SymFunctionTableAccess, ::SymGetModuleBase, NULL) == TRUE) 
        {
            if (sf.AddrFrame.Offset == 0) 
                break;

            if (sf.AddrPC.Offset == 0) 
                continue;

            if (sf.AddrPC.Offset == sf.AddrReturn.Offset) 
                continue;

            if (0 < skipCount) 
            {
                --skipCount;
                continue;
            }

            StackFrame *pFrame = new StackFrame();
            pFrame->Init(m_hProcess, sf.AddrPC.Offset);
            m_frames.push_back(pFrame);
        }
    }
 
これを、DefaultSTDMETHODWrapper.h でちらっと見えていた例外クラス、NAnonymException のコンストラクタで呼び出すようにします。

#line 18 "CppTroll\ProfilingApiSample02\Urasandesu\NAnonym\NAnonymException.cpp"
    NAnonymException::NAnonymException() : 
        m_pStackTrace(boost::make_shared<StackTrace>())
    {
        CaptureStackTrace(this);
    }
    
    NAnonymException::NAnonymException(std::string const &what) : 
        m_what(what),
        m_pStackTrace(boost::make_shared<StackTrace>())
    { 
        CaptureStackTrace(this);
    }
    
    NAnonymException::NAnonymException(std::string const &what, NAnonymException &innerException) : 
        m_what(what),
        m_pStackTrace(boost::make_shared<StackTrace>())
    {
        CaptureStackTrace(this);
        *this << boost::errinfo_nested_exception(boost::copy_exception(innerException));
    }
    
    
    // …(略)…
    
    

    void NAnonymException::CaptureStackTrace(NAnonymException *this_)
    {
        this_->m_pStackTrace->Capture(3);
        *this_ << ThrowStackTrace(this_->m_pStackTrace.get());
    }
 
例外の中身を出すために使っていた Boost.Exceptionのメソッド、boost::diagnostic_information は、boost::error_info で登録しておいた型を引数に持つ to_string を定義しておくことで、それを呼び出してくれるようになってます。この仕組みを利用し、C# や Java の出力方式を参考に、出力文字列を構成しています。

#line 51 "CppTroll\ProfilingApiSample02\Urasandesu\NAnonym\NAnonymException.cpp"
    inline std::string to_string(StackTrace *pStackTrace)
    {
        using namespace std;
        using namespace boost;
        
        ostringstream oss;
        ptr_vector<StackFrame> *pFrames = pStackTrace->GetStackFrames();
        for (ptr_vector<StackFrame>::iterator i = pFrames->begin(), i_end = pFrames->end(); i != i_end; ++i)
        {
            oss << "at ";
            oss << i->GetSymbolName();
            oss << " in ";
            oss << i->GetModuleName();
            if (0 < i->GetFileLineNumber())
            {
                oss << "(";
                oss << i->GetFileName();
                oss << ":";
                oss << i->GetFileLineNumber();
                oss << ")";
            }
            oss << "\n";
        }
        return oss.str();
    }
 

BOOST_THROW_EXCEPTION(NAnonymException("An error is occurred!!")) して、catch (...) して、cout << diagnostic_information(current_exception()) << endl するだけの、簡単なテストを書いて実行してみると・・・。

Microsoft Windows XP [Version 5.1.2600]
(C) Copyright 1985-2001 Microsoft Corp.

C:\Documents and Settings\User>cd C:\Documents and Settings\User\CppTroll\Debug

C:\Documents and Settings\User\CppTroll\Debug>ProfilingApiSample02Test.exe --gtest_filter=ProfilingApiSample02TestSuite.ProfilingApiSample02Test
Running main() from gtest_main.cc
Note: Google Test filter = ProfilingApiSample02TestSuite.ProfilingApiSample02Test
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from ProfilingApiSample02TestSuite
[ RUN      ] ProfilingApiSample02TestSuite.ProfilingApiSample02Test
c:\documents and settings\User\cpptroll\profilingapisample02test\profilingapisample02test.cpp(32): Throw in function void __thiscall `anonymous-namespace'::ProfilingApiSample02TestSuite_ProfilingApiSample02Test_Test::TestBody(void)
Dynamic exception type: class boost::exception_detail::clone_impl<class Urasandesu::NAnonym::NAnonymException>
std::exception::what: An error is occurred!!
[struct Urasandesu::NAnonym::tag_stack_trace *] = at `anonymous namespace'::ProfilingApiSample02TestSuite_ProfilingApiSample02Test_Test::TestBody in ProfilingApiSample02Test.exe(c:\documents and settings\User\cpptroll\profilingapisample02test\profilingapisample02test.cpp:32)
at testing::internal::HandleSehExceptionsInMethodIfSupported<testing::Test,void> in ProfilingApiSample02Test.exe(c:\gtest-1.6.0-vc90\src\gtest.cc:2075)
at testing::internal::HandleExceptionsInMethodIfSupported<testing::Test,void> in ProfilingApiSample02Test.exe(c:\gtest-1.6.0-vc90\src\gtest.cc:2126)
at testing::Test::Run in ProfilingApiSample02Test.exe(c:\gtest-1.6.0-vc90\src\gtest.cc:2162)
at testing::TestInfo::Run in ProfilingApiSample02Test.exe(c:\gtest-1.6.0-vc90\src\gtest.cc:2342)
at testing::TestCase::Run in ProfilingApiSample02Test.exe(c:\gtest-1.6.0-vc90\src\gtest.cc:2446)
at testing::internal::UnitTestImpl::RunAllTests in ProfilingApiSample02Test.exe(c:\gtest-1.6.0-vc90\src\gtest.cc:4238)
at testing::internal::HandleSehExceptionsInMethodIfSupported<testing::internal::UnitTestImpl,bool> in ProfilingApiSample02Test.exe(c:\gtest-1.6.0-vc90\src\gtest.cc:2075)
at testing::internal::HandleExceptionsInMethodIfSupported<testing::internal::UnitTestImpl,bool> in ProfilingApiSample02Test.exe(c:\gtest-1.6.0-vc90\src\gtest.cc:2126)
at testing::UnitTest::Run in ProfilingApiSample02Test.exe(c:\gtest-1.6.0-vc90\src\gtest.cc:3874)
at main in ProfilingApiSample02Test.exe(c:\gtest-1.6.0-vc90\src\gtest_main.cc:39)
at __tmainCRTStartup in ProfilingApiSample02Test.exe(f:\dd\vctools\crt_bld\self_x86\crt\src\crt0.c:266)
at mainCRTStartup in ProfilingApiSample02Test.exe(f:\dd\vctools\crt_bld\self_x86\crt\src\crt0.c:182)
at RegisterWaitForInputIdle in kernel32.dll


[       OK ] ProfilingApiSample02TestSuite.ProfilingApiSample02Test (359 ms)
[----------] 1 test from ProfilingApiSample02TestSuite (359 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran. (359 ms total)
[  PASSED  ] 1 test.

C:\Documents and Settings\User\CppTroll\Debug>
 
いいですね~。VC++ ランタイムのソースコードは f ドライブで開発されてたのかなーみたいな不必要な情報まで出てますが、大は小を兼ねるってことで (b^ー゚)
これで準備が整いました!では、実装を始めましょう!




Hello, Profiling API !!
まず手始めに、一番簡単に追加ができる #US ヒープを弄ってみます。文字列を返すメソッドについて、環境変数を通じて与えた文字列に入れ替えられるようにしましょう。書き換える対象のプログラムはこんな感じ。

#line 1 "CppTroll\ProfilingApiSample01Target\Program.cs"
using System;

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine(new Class1().Print("Hello, World !!"));
        Console.WriteLine(new Class2().Print("こんにちは、世界!"));
    }
}

public class Class1
{
    public string Print(string value)
    {
        return "Hello, " + new Class2().Print(value) + " World !!";
    }
}

public class Class2
{
    public string Print(string value)
    {
        return "こんにちは、" + value + " 世界!";
    }
}
 
章立ては以下の通りです。
  1. 初期設定
  2. JIT 開始のフック、処理の入れ替え
    2-1. FunctionID から MethodDef テーブルレコードへの変換
    2-2. メソッド/クラスの詳細な情報を取得
    2-3. IL メソッドボディの書き換え
  3. 結果


1. 初期設定
ICorProfilerCallback* インターフェースを実装したコクラスで、一番に呼ばれるのが Initialize メソッド。このメソッドは最初に説明した最低限の設定で呼ばれるのですが、この後の処理は個別に CLR に呼び出してもらうよう設定する必要があります(SetEventMask)。このサンプルでは、JIT 開始をフックするだけで良いですので、COR_PRF_MONITOR_JIT_COMPILATION フラグを立てています(45 行目)。
順番が前後してしまいましたが、Initialize メソッドでは CLR とやりとりをするための ICorProfilerInfo* インターフェース実装オブジェクトが引数で渡されてきます。ただ、引数で渡されるのは、ここしかありませんので、グローバル変数もしくはメンバ変数に保持しておきましょう(36 行目)。
あとは、環境変数から、書き換える文字列を取得しておきます(50 行目~52 行目)。書き換え対象となるメソッド名は NANONYM_TARGET_METHOD を、#US ヒープに追加する文字列は NANONYM_NEW_MESSAGE を通じて取得するようにしました。こっそりと環境変数を取得するためのユーティリティクラス(Environment)を追加してますが、_dupenv_s をラップし、エラーがあれば前述のスタックトレース付き例外をスローするようにしただけですので、特に解説はしません。
ちなみに、アダプタクラスを作ったおかげで、本処理が記述されている ExeWeaver.cpp は、たったの 198 行です。携帯からでも、難なく読めちゃうかもですね (^_^;)

#line 31 "CppTroll\ProfilingApiSample01\ExeWeaver.cpp"
    // Reset the timer.
    m_timer.restart();
    

    // Initialize the profiling API.
    hr = pICorProfilerInfoUnk->QueryInterface(IID_ICorProfilerInfo2, 
                                              reinterpret_cast<void**>(&m_pInfo));
    if (FAILED(hr)) 
        BOOST_THROW_EXCEPTION(NAnonymCOMException(hr));


    // Set a value that specifies the types of events for which the profiler 
    // wants to receive notification from CLR.
    DWORD event_ = COR_PRF_MONITOR_JIT_COMPILATION;
    hr = m_pInfo->SetEventMask(event_);
    if (FAILED(hr)) 
        BOOST_THROW_EXCEPTION(NAnonymCOMException(hr));

    
    // Get the name of the target method and the new message.
    m_targetMethodName = wstring(CA2W(Environment::GetEnvironmentVariable("NANONYM_TARGET_METHOD").c_str()));
    m_newMessage = wstring(CA2W(Environment::GetEnvironmentVariable("NANONYM_NEW_MESSAGE").c_str()));
 


2. JIT 開始のフック、処理の入れ替え
各メソッドの JIT の開始は、JITCompilationStarted メソッドが通知してくれます。早速、中を見ていきましょう。

2-1. FunctionID から MethodDef テーブルレコードへの変換

#line 80 "CppTroll\ProfilingApiSample01\ExeWeaver.cpp"
    // Convert FunctionID to MethodDef token.
    mdMethodDef mdmd = mdMethodDefNil;
    CComPtr<IMetaDataImport2> pImport;
    hr = m_pInfo->GetTokenAndMetaDataFromFunction(functionId, IID_IMetaDataImport2, 
                                                  reinterpret_cast<IUnknown**>(&pImport), 
                                                  &mdmd);
    if (FAILED(hr)) 
        BOOST_THROW_EXCEPTION(NAnonymCOMException(hr));
    
 
Profiling API の共通の概念として、プロファイル ID というものがあります。各イベントを通知するメソッドの引数で渡されるこの情報、実際の意味は、各項目を説明するメモリのアドレスなのですが、そのままでは扱い辛いので、人間が読める情報に変換します。最終的に、MethodDef テーブルのレコード情報が欲しいので、Meta Data API 用のインターフェースを取り出しておきます。

2-2. メソッド/クラスの詳細な情報を取得

#line 90 "CppTroll\ProfilingApiSample01\ExeWeaver.cpp"
    // Get the properties of this method.
    mdTypeDef mdtd = mdTypeDefNil;
    WCHAR methodName[MAX_SYM_NAME] = { 0 };
    {
        ULONG methodNameSize = sizeof(methodName);
        DWORD methodAttr = 0;
        PCCOR_SIGNATURE pMethodSig = NULL;
        ULONG methodSigSize = 0;
        ULONG methodRVA = 0;
        DWORD methodImplFlag = 0;
        hr = pImport->GetMethodProps(mdmd, &mdtd, methodName, methodNameSize, 
                                     &methodNameSize, &methodAttr, &pMethodSig, 
                                     &methodSigSize, &methodRVA, &methodImplFlag);
        if (FAILED(hr)) 
            BOOST_THROW_EXCEPTION(NAnonymCOMException(hr));
        
        ULONG callConv = IMAGE_CEE_CS_CALLCONV_MAX;
        pMethodSig += ::CorSigUncompressData(pMethodSig, &callConv);

        ULONG paramCount = 0;
        pMethodSig += ::CorSigUncompressData(pMethodSig, &paramCount);

        CorElementType retType = ELEMENT_TYPE_END;
        pMethodSig += ::CorSigUncompressElementType(pMethodSig, &retType);
        if (retType != ELEMENT_TYPE_STRING)
            return S_OK;
    }
    

    // Get the properties of type that this method is declared.
    WCHAR typeName[MAX_SYM_NAME] = { 0 };
    {
        ULONG typeNameSize = sizeof(typeName);
        DWORD typeAttr = 0;
        mdToken mdtTypeExtends = mdTokenNil;
        hr = pImport->GetTypeDefProps(mdtd, typeName, typeNameSize, &typeNameSize, 
                                      &typeAttr, &mdtTypeExtends);
        if (FAILED(hr)) 
            BOOST_THROW_EXCEPTION(NAnonymCOMException(hr));
    }
    

    // Check, is this method the instrumentation target?
    wstring methodFullName(typeName);
    methodFullName += L".";
    methodFullName += methodName;
    if (methodFullName != m_targetMethodName)
        return S_OK;
    
 
Meta Data API を通じ、メソッドの詳細情報と、それが定義されたクラスの詳細情報を取得します。106 行目~115 行目で行っているメソッドシグネチャの Parse は、本当はもっとちゃんと行う必要があるのですが・・・。今回はとりあえず、戻り値の型が文字列であることさえわかればいいので、最低限の処理しかしていません。132 行目で取得したクラス名とメソッド名を連結し、環境変数から取得した、書き換え対象となるメソッドかどうかを判定しています。

2-3. IL メソッドボディの書き換え

#line 140 "CppTroll\ProfilingApiSample01\ExeWeaver.cpp"
    // Define the new message to #US heap.
    CComPtr<IMetaDataEmit2> pEmit;
    hr = pImport->QueryInterface(IID_IMetaDataEmit2, 
                                 reinterpret_cast<void**>(&pEmit));
    if (FAILED(hr)) 
        BOOST_THROW_EXCEPTION(NAnonymCOMException(hr));

    mdString mdsMessage = mdStringNil;
    hr = pEmit->DefineUserString(m_newMessage.c_str(), m_newMessage.size(), &mdsMessage);
    if (FAILED(hr))
        BOOST_THROW_EXCEPTION(NAnonymCOMException(hr));
    

    // Emit the new IL method body.
    SimpleBlob sb;
    sb.Put<BYTE>(OpCodes::Encodings[OpCodes::CEE_LDSTR].byte2);
    sb.Put<DWORD>(mdsMessage);
    sb.Put<BYTE>(OpCodes::Encodings[OpCodes::CEE_RET].byte2);
    
    COR_ILMETHOD ilMethod;
    ::ZeroMemory(&ilMethod, sizeof(COR_ILMETHOD));
    ilMethod.Fat.SetMaxStack(1);
    ilMethod.Fat.SetCodeSize(sb.Size());
    ilMethod.Fat.SetLocalVarSigTok(mdTokenNil);
    ilMethod.Fat.SetFlags(0);
    
    unsigned headerSize = COR_ILMETHOD::Size(&ilMethod.Fat, false);
    unsigned totalSize = headerSize + sb.Size();
    

    // Allocate the area for new IL method body and set it.
    ModuleID mid = NULL;
    {
        ClassID cid = NULL;
        mdMethodDef mdmd_ = mdMethodDefNil;
        hr = m_pInfo->GetFunctionInfo(functionId, &cid, &mid, &mdmd_);
        if (FAILED(hr)) 
            BOOST_THROW_EXCEPTION(NAnonymCOMException(hr));
    }

    CComPtr<IMethodMalloc> pMethodMalloc;
    hr = m_pInfo->GetILFunctionBodyAllocator(mid, &pMethodMalloc);
    if (FAILED(hr)) 
        BOOST_THROW_EXCEPTION(NAnonymCOMException(hr));

    BYTE *pNewILFunctionBody = reinterpret_cast<BYTE*>(pMethodMalloc->Alloc(totalSize));

    BYTE *pBuffer = pNewILFunctionBody;
    pBuffer += COR_ILMETHOD::Emit(headerSize, &ilMethod.Fat, false, pBuffer);
    ::memcpy_s(pBuffer, totalSize - headerSize, sb.Ptr(), sb.Size());

    hr = m_pInfo->SetILFunctionBody(mid, mdmd, pNewILFunctionBody);
    if (FAILED(hr)) 
        BOOST_THROW_EXCEPTION(NAnonymCOMException(hr));
    
 
環境変数から取得した文字列を #US ヒープに追加し(140 行目~150 行目)、それを使って IL ぷちぷち(153 行目~167 行目)。Profiling API の機能を使って新しい IL メソッドボディ領域を作ったら、そこに IL の並びをコピーします(170 行目~193 行目)。打ち込む IL が単純なので、特筆すべきところはないですね (^^ゞ


3. 結果
さて、新しいコマンドプロンプトを立ち上げ、実行してみましょう!サンプルプログラムの中の Class2.Print メソッドについて、返す文字列を変更してみます。

Microsoft Windows XP [Version 5.1.2600]
(C) Copyright 1985-2001 Microsoft Corp.

C:\Documents and Settings\User>cd C:\Documents and Settings\User\CppTroll\ProfilingApiSample01Target\bin\Debug

C:\Documents and Settings\User\CppTroll\ProfilingApiSample01Target\bin\Debug>dir
 ドライブ C のボリューム ラベルは S3A4509D001 です
 ボリューム シリアル番号は 758E-2116 です

 C:\Documents and Settings\User\CppTroll\ProfilingApiSample01Target\bin\Debug のディレクトリ

2011/10/29  17:01    <DIR>          .
2011/10/29  17:01    <DIR>          ..
2011/10/29  17:05             5,120 ProfilingApiSample01Target.exe
2011/10/29  17:05            13,824 ProfilingApiSample01Target.pdb
               2 個のファイル              18,944 バイト
               2 個のディレクトリ  23,052,824,576 バイトの空き領域

C:\Documents and Settings\User\CppTroll\ProfilingApiSample01Target\bin\Debug>SET COR_PROFILER={1DC70D11-5E46-48C6-BB07-75CFFF188327}

C:\Documents and Settings\User\CppTroll\ProfilingApiSample01Target\bin\Debug>SET NANONYM_TARGET_METHOD=Class2.Print

C:\Documents and Settings\User\CppTroll\ProfilingApiSample01Target\bin\Debug>SET NANONYM_NEW_MESSAGE=Hello, Dynamic Languages World!!

C:\Documents and Settings\User\CppTroll\ProfilingApiSample01Target\bin\Debug>SET COR_ENABLE_PROFILING=0

C:\Documents and Settings\User\CppTroll\ProfilingApiSample01Target\bin\Debug>ProfilingApiSample01Target.exe
Hello, こんにちは、Hello, World !! 世界! World !!
こんにちは、こんにちは、世界! 世界!

C:\Documents and Settings\User\CppTroll\ProfilingApiSample01Target\bin\Debug>SET COR_ENABLE_PROFILING=1

C:\Documents and Settings\User\CppTroll\ProfilingApiSample01Target\bin\Debug>ProfilingApiSample01Target.exe
Hello, Hello, Dynamic Languages World!! World !!
Hello, Dynamic Languages World!!
Time Elapsed: 0.078000s

C:\Documents and Settings\User\CppTroll\ProfilingApiSample01Target\bin\Debug>dir
 ドライブ C のボリューム ラベルは S3A4509D001 です
 ボリューム シリアル番号は 758E-2116 です

 C:\Documents and Settings\User\CppTroll\ProfilingApiSample01Target\bin\Debug のディレクトリ

2011/10/29  17:01    <DIR>          .
2011/10/29  17:01    <DIR>          ..
2011/10/29  17:05             5,120 ProfilingApiSample01Target.exe
2011/10/29  17:05            13,824 ProfilingApiSample01Target.pdb
               2 個のファイル              18,944 バイト
               2 個のディレクトリ  23,051,894,784 バイトの空き領域

C:\Documents and Settings\User\CppTroll\ProfilingApiSample01Target\bin\Debug>
 
おー!入れ替わりましたね!28 行目~29 行目で出ていた内容が、COR_ENABLE_PROFILING=1 することによって、34 行目~35 行目では見事に入れ替わっています。ファイルのタイムスタンプに変わりは無いため、実行時に、メモリ上にある IL メソッドボディだけが変化していることがわかるかと思います。次はもう少し複雑なサンプルを作ってみましょう。




Monkey ... Swapping ?
今度はメソッドの中身を丸ごと交換してみます。シグネチャが同じであれば、そう難しくはなさそうですね。また今後を考えて、ぼちぼち使い勝手の良いラッパーも設計していきます。仕事と違って、何度も作り直しながらブラッシュアップできるのは気が楽です (^_^;)
書き換える対象のプログラムはこんな感じでいかがでしょう。

#line 1 "CppTroll\ProfilingApiSample02Target\Program.cs"
using System;

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Hello world!!");
    }
}

class AlternativeProgram
{
    static void Main(string[] args)
    {
        Console.WriteLine(@"Welcome to the low layer world of CLR!! In the normal case, 
you can not replace the method body at the runtime, 
but you can do it by using the unmanaged profiling API!!");
    }
}
 
とりあえず動くもの、ということでラッパーには最低限必要な処理だけ持たせることにします。なので、クラスやメソッドの識別には、メタデータテーブルの ID を直接指定することにしました。ildasm を使えば、あらかじめその辺りを調べておくことができます。全体の雰囲気がわかって来たら、細かなところを作りこんで行く予定です。指定の方法は先ほどと同じく、環境変数を通じて行うこととしましょう。章立ては以下の通りです。
  1. 初期設定
  2. AppDomain 作成開始のフック
  3. Assembly 読み込み開始のフック
  4. Module 読み込み開始のフック
  5. JIT 開始のフック、処理の入れ替え
  6. 結果
中身を交換できるのは同じ Assembly に定義してあるクラス/メソッドだけ、しかも Generics や例外は無視しているという手抜きっぷりですが、シグネチャさえ合えば中身をごっそり換えられるということで、できることは先ほどのサンプルとは段違いに多いです。それでも本処理が記述されている ExeWeaver2.cpp は、たったの 183 行。我ながらイイ線行ってるのではないでしょうか (^^ゞ


1. 初期設定

#line 53 "CppTroll\ProfilingApiSample02\ExeWeaver2.cpp"
    // Reset the timer.
    m_timer.restart();


    // Initialize the unmanaged profiling API.
    m_pProfInfo->Init(pICorProfilerInfoUnk);
    m_pProfInfo->SetEventMask(COR_PRF_MONITOR_ASSEMBLY_LOADS | 
                              COR_PRF_MONITOR_MODULE_LOADS | 
                              COR_PRF_MONITOR_APPDOMAIN_LOADS | 
                              COR_PRF_MONITOR_JIT_COMPILATION);


    // Get name of the target assembly, replaced method and its declaring type and 
    // replacing method and its declaring type.
    m_targetAssemblyName = wstring(CA2W(Environment::GetEnvironmentVariable("NANONYM_TARGET_ASSEMBLY").c_str()));
    {
        istringstream is(Environment::GetEnvironmentVariable("NANONYM_REPLACE_TYPE_FROM"));
        is >> hex >> m_mdtdReplaceTypeFrom;
    }
    {
        istringstream is(Environment::GetEnvironmentVariable("NANONYM_REPLACE_METHOD_FROM"));
        is >> hex >> m_mdmdReplaceMethodFrom;
    }
    {
        istringstream is(Environment::GetEnvironmentVariable("NANONYM_REPLACE_TYPE_TO"));
        is >> hex >> m_mdtdReplaceTypeTo;
    }
    {
        istringstream is(Environment::GetEnvironmentVariable("NANONYM_REPLACE_METHOD_TO"));
        is >> hex >> m_mdtdReplaceMethodTo;
    }


    // Create pseudo AppDomain to load mscorlib.dll.
    m_pProfInfo->GetCurrentProcess()->GetPseudoDomain();
 
Initialize メソッドでやることは先ほどと大体一緒です。環境変数を通じて取得する情報(65 行目~83 行目)は以下の通りとしました。
環境変数名概要
NANONYM_TARGET_ASSEMBLY入れ替えるクラス/メソッドが定義されている Assembly 表示名。「ProfilingApiSample02Target」とか。
NANONYM_REPLACE_TYPE_FROM入れ替え元のクラス(TypeDef テーブルのレコード ID)。「0x02000002」とか。
NANONYM_REPLACE_METHOD_FROM入れ替え元のメソッド(MethodDef テーブルのレコード ID)。「0x06000001」とか。
NANONYM_REPLACE_TYPE_TO入れ替え先のクラス(TypeDef テーブルのレコード ID)。「0x02000003」とか。
NANONYM_REPLACE_METHOD_TO入れ替え先のメソッド(MethodDef テーブルのレコード ID)。「0x06000003」とか。
Profiling API のラッパーは、この API を通じて CLR が通知してくるイベントを元にモデル化しています。ただ、mscorlib.dll は決まった AppDomain に読み込まれることがないようなので、モデルを揃えるために、ここで擬似 AppDomain を作成しています(87 行目)。


2. AppDomain 作成開始のフック

#line 109 "CppTroll\ProfilingApiSample02\ExeWeaver2.cpp"
    // Create the wrapper referring the AppDomainID.
    m_pProfInfo->GetCurrentProcess()->CreateIfNecessary<AppDomainProfile>(appDomainId);
 
AppDomain 作成開始は、AppDomainCreationStarted を通じて通知されます。ここでは AppDomainID に紐付ける Profiling API ラッパーを作成しておきます。.NET Framework の機能上、AppDomain 単位での Load/Unload ができるため、AppDomain に対応する Profiling API ラッパーが解放されると、それに紐付く情報が全て解放されるような形を目指しています。また、同じ AppDomain であれば、Assembly や Module、Type、Method ・・・は常に同じものを指すため、map を使い、プロファイル ID からこれまで作成したインスタンスを引けるような構造にしてみました。
あと、Profiling API に限ったことではないのですが、CLR では %INCLUDE%\WinError.h に定義されている通常の HRESULT に加え、%INCLUDE%\CorError.h に定義された HRESULT も使用しています。最終的には、これも例外メッセージにうまく載せたいところです。


3. Assembly 読み込み開始のフック

#line 120 "CppTroll\ProfilingApiSample02\ExeWeaver2.cpp"
    // Create the wrapper referring the AssemblyID through the current AppDoamin.    
    AppDomainProfile *pDomainProf = m_pProfInfo->GetCurrentProcess()->GetCurrentDomain();
    AssemblyProfile *pAsmProf = pDomainProf->CreateIfNecessary<AssemblyProfile>(assemblyId);
    if (pAsmProf->GetName() != m_targetAssemblyName)
        return S_OK;
    
    m_pTargetAssemblyProf = pAsmProf;
 
Assembly 読み込み開始を通知してくれるのは、AssemblyLoadStarted メソッドとなります。現在の AppDomain を表す Profiling API ラッパーから、AssemblyID に紐付ける Profiling API ラッパーを作成します。もし、その名前が、環境変数で指定されていた対象となる Assembly の修飾名であれば、それを保持しておきましょう。


4. Module 読み込み開始のフック

#line 137 "CppTroll\ProfilingApiSample02\ExeWeaver2.cpp"
    if (m_pTargetAssemblyProf == NULL || m_pConv->HasInitialized())
        return S_OK;
    
    // Initialize the value converter to convert the wrapper of the profiling API 
    // to the wrapper of the meta data API.
    ModuleProfile *pModProf = m_pTargetAssemblyProf->CreateIfNecessary<ModuleProfile>(moduleId);
    AssemblyMetaData *pAsmMeta = m_pMetaInfo->CreatePseudo<AssemblyMetaData>();
    m_pConv->Initialize(pAsmMeta, m_pProfInfo->GetCurrentProcess(), pModProf);
 
Module 読み込み開始のフックは ModuleLoadStarted で行います。対象となる Assembly を表すラッパーが作成されているのであれば、Meta Data Api ラッパーと相互変換するためのオブジェクトを初期化します。


5. JIT 開始のフック、処理の入れ替え

#line 156 "CppTroll\ProfilingApiSample02\ExeWeaver2.cpp"
    if (!m_pConv->HasInitialized())
        return S_OK;

    // Get the properties this method.
    MethodProfile *pMethodProfFrom = m_pProfInfo->GetCurrentProcess()->CreateIfNecessary<MethodProfile>(functionId);
    MethodMetaData *pMethodMetaFrom = m_pConv->Convert(pMethodProfFrom);
    if (pMethodMetaFrom->GetToken() != m_mdmdReplaceMethodFrom)
        return S_OK;

    
    // Get the properties of the type that this method is declared.
    TypeMetaData *pTypeMetaFrom = pMethodMetaFrom->GetDeclaringType();
    if (pTypeMetaFrom->GetToken() != m_mdtdReplaceTypeFrom)
        return S_OK;

    
    // Replace the IL method body of them that are set above.
    ModuleMetaData *pModMetaFrom = pTypeMetaFrom->GetModule();
    TypeMetaData *pTypeMetaTo = pModMetaFrom->GetType(m_mdtdReplaceTypeTo);
    TypeProfile *pTypeProfTo = m_pConv->ConvertBack(pTypeMetaTo);   // NOTE: To resolve the type defined explicitly 
    MethodMetaData *pMethodMetaTo = pTypeMetaTo->GetMethod(m_mdtdReplaceMethodTo);
    MethodProfile *pMethodProfTo = m_pConv->ConvertBack(pMethodMetaTo);
    MethodBodyProfile *pBodyProfTo = pMethodProfTo->GetMethodBody();
    pMethodProfFrom->SetMethodBody(pBodyProfTo);
 
JIT 開始のフックは先ほどのサンプルでも出てきました。これまでの処理で、情報が揃った場合、実際の交換を行います。FunctionID から入れ替え元のメソッドの詳細情報を取得(159 行目~163 行目)、そのメソッドが定義されたクラスの TypeDef テーブル ID を取得し(166 行目~169 行目)、条件が合うのであれば入れ替え先のメソッドから IL メソッドボディをコピーし、入れ替え元のメソッドの IL メソッドボディを設定し直します(172 行目~ 179 行目)。若干嵌ったのは、Profiling API には、Meta Data API で扱うメタデータテーブルの ID から、プロファイル ID に変換するメソッドがあるのですが、どうも上から順(Assembly → Module → Type → Method →…)に解決しないと、E_INVALIDARG にされてしまうようです。175 行目の処理はこれを示しています。


6. 結果
こちらも、新しいコマンドプロンプトを立ち上げ実行してみます。サンプルプログラムの中の Program(0x02000002).Main(0x06000001)メソッドについて、別のクラスのメソッド AlternativeProgram(0x02000003).Main(0x06000003)に変更してみましょう。

Microsoft Windows XP [Version 5.1.2600]
(C) Copyright 1985-2001 Microsoft Corp.

C:\Documents and Settings\User>cd C:\Documents and Settings\User\CppTroll\ProfilingApiSample02Target\bin\Debug

C:\Documents and Settings\User\CppTroll\ProfilingApiSample02Target\bin\Debug>dir
 ドライブ C のボリューム ラベルは S3A4509D001 です
 ボリューム シリアル番号は 758E-2116 です

 C:\Documents and Settings\User\CppTroll\ProfilingApiSample02Target\bin\Debug のディレクトリ

2011/10/29  18:53    <DIR>          .
2011/10/29  18:53    <DIR>          ..
2011/10/29  18:53             5,120 ProfilingApiSample02Target.exe
2011/10/29  18:53            11,776 ProfilingApiSample02Target.pdb
               2 個のファイル              16,896 バイト
               2 個のディレクトリ  23,116,296,192 バイトの空き領域

C:\Documents and Settings\User\CppTroll\ProfilingApiSample02Target\bin\Debug>SET COR_PROFILER={F60DB91B-5932-4964-818A-CA697CF46A5F}

C:\Documents and Settings\User\CppTroll\ProfilingApiSample02Target\bin\Debug>SET NANONYM_TARGET_ASSEMBLY=ProfilingApiSample02Target

C:\Documents and Settings\User\CppTroll\ProfilingApiSample02Target\bin\Debug>SET NANONYM_REPLACE_TYPE_FROM=0x02000002

C:\Documents and Settings\User\CppTroll\ProfilingApiSample02Target\bin\Debug>SET NANONYM_REPLACE_METHOD_FROM=0x06000001

C:\Documents and Settings\User\CppTroll\ProfilingApiSample02Target\bin\Debug>SET NANONYM_REPLACE_TYPE_TO=0x02000003

C:\Documents and Settings\User\CppTroll\ProfilingApiSample02Target\bin\Debug>SET NANONYM_REPLACE_METHOD_TO=0x06000003

C:\Documents and Settings\User\CppTroll\ProfilingApiSample02Target\bin\Debug>SET COR_ENABLE_PROFILING=0

C:\Documents and Settings\User\CppTroll\ProfilingApiSample02Target\bin\Debug>ProfilingApiSample02Target.exe
Hello world!!

C:\Documents and Settings\User\CppTroll\ProfilingApiSample02Target\bin\Debug>SET COR_ENABLE_PROFILING=1

C:\Documents and Settings\User\CppTroll\ProfilingApiSample02Target\bin\Debug>ProfilingApiSample02Target.exe
Welcome to the low layer world of CLR!! In the normal case,
you can not replace the method body at the runtime,
but you can do it by using the unmanaged profiling API!!
Time Elapsed: 0.063000s

C:\Documents and Settings\User\CppTroll\ProfilingApiSample02Target\bin\Debug>dir
 ドライブ C のボリューム ラベルは S3A4509D001 です
 ボリューム シリアル番号は 758E-2116 です

 C:\Documents and Settings\User\CppTroll\ProfilingApiSample02Target\bin\Debug のディレクトリ

2011/10/29  18:53    <DIR>          .
2011/10/29  18:53    <DIR>          ..
2011/10/29  18:53             5,120 ProfilingApiSample02Target.exe
2011/10/29  18:53            11,776 ProfilingApiSample02Target.pdb
               2 個のファイル              16,896 バイト
               2 個のディレクトリ  23,116,296,192 バイトの空き領域

C:\Documents and Settings\User\CppTroll\ProfilingApiSample02Target\bin\Debug>
 
さっくり変わります(34 行目→39 行目~41 行目)。すばらしい。
ところで、プロセスの初期化から、終了までの処理時間を入れていますが、凄まじいですね。Profiling API を謳うだけあって、パフォーマンスの劣化がほとんど感じられないのはさすがです。式木こねくりまわして、AppDomainのLoad/Unload繰り返してた頃の自分に教えてあげたいですね (^_^;)




さらなる魔改造!
Profiling API の使い方を見てきましたが、いかがでしたでしょうか。メソッドのトレースとかまではちょっと紹介できませんでしたが、マネージコードではどうしようもなかったことができるようになるのは、ちょっとは自分の自信に繋がりそう。これまで魔法にしか思えなかったものが、タネがある手品だったということがわかった感じですね! (>_<)
そして、シンプルな方法だけでの入れ替えを試した結果ではありますが、やはりパフォーマンスは段違いだということがわかりました。以前行き詰っていた状態から、実に 30 倍以上の高速化が図れています。これはすごい!アンマネージ万歳!COM 万歳!C++ 万歳!
・・・まあ、最終的には C# のコード側から、変更情報(対象の名前やらMSIL ストリームやら…)を、名前付きパイプ経由で送り込むようなアーキテクチャになると思いますので、ここまでサクサクになるかはわかりませんが、だいぶ希望が持てる結果になったと思います。もう少し使い方に慣れてから、C# 側のデザインも考えて行こうと思います。

Viewing all 46 articles
Browse latest View live