Win32 APIをてなずけよう – プログラミング生放送勉強会 第38回@名古屋

WP_20151212_17_51_06_Pro_LIプログラミング生放送勉強会 第38回@名古屋、今年もやってきました。「Win32 APIをてなずけよう」というお題で登壇してきました。

今回もギリギリまでかかりましたが、何とか形にできてよかったです。
普段は言語系に生息していますが、プロ生は総合力が試される良い機会だと思っています。きちんと形にするのは大変だ…

等身大のコレには、プロ生ちゃんの中の人のサインが!このサインには重大な意味があるらしいです…


.NETとWin32 APIの融合

Pronama.InteropDemoセッション中でも説明しましたが、WindowsのクライアントサイドはこれからUWPが環境の主役となっていくと思いますが、一方で制限された環境故に外部との連携にはどうしても大きなリソースを必要としてしまいます。

例えば、IoTデバイスであればRaspberry PiやBluetooth LEを持ったデバイス等であれば、UWPにもWiFiやBLE APIがあるので通信が可能ですが、生USBデバイスなどの、もっと軽量なデバイスとの通信のニーズもあるかと思います。また、C言語インターフェイスしか持たないサードパーティ製のライブラリなどもそうです。これらを.NET上で簡単に有効活用するための方法の一つとして、P/Invokeを紹介しました。

内容は、P/Invokeを使用してC#からWin32 APIを呼び出し、最終的にプロ生ちゃんをデスクトップのウインドウ上を走らせてジャンプさせる、というネタです。このセッションではWin32 APIを主眼に置いて、どうやってアプリを成立させるかと言う事を背景に解説しましたが、P/Invoke自体は定義さえ出来れば、Win32に限らず他のライブラリにも応用できるため、前述のターゲットも視野に入れることが出来ます。

ところで、セッション中は解説しませんでしたが、このビデオを見て気が付いた方は居るでしょうか? Visual Studioの上辺を走っている時だけ、ちょっと宙に浮いている気がしませんか?

これは、Visual Studioのウインドウ外周に光彩(ぼんやり光るヴェールのようなもの)のエフェクトがあり、このエフェクトを表示するために、外周を取り囲むように半透明のウインドウがあるからです(つまり、理論的にはバグではないです)。
詳しくはぐらばくさんの記事を見てもらうとして、これを無視するのもちょっと難しい課題ですね。究極的には、人間の目で見てそこに「上辺」があるかどうか、だと思いますが、それを言い始めると半透明だったらどうするんだとか、矩形じゃないウインドウはどうするんだとか、色々ありそうです。

さて、P/Invokeの説明を主体にしていますが、実際に作ったコードはほかの肉付けが大きかったです。ソースコードにはコメントを多数入れて、サンプルじゃなくて現実的なコードを作るにはどうしたらよいか、という事に注意しています。

ソースコードはGitHubに上げてあるので、スライドの最後で紹介した改良を試したりしてみてください。

オリジナルスライドはこちら:Win32 APIをてなずけよう.pptx

それではまた!

真・式の体を成して無い式を式の体を成すように式と式木で何とかする式

この投稿は「C# Advent Calendar 2015」の7日目の記事です。


朝、何となくスライドを読み直していたところ…

expression-tree-block昨日は「NGK2015B 名古屋合同懇親会2015忘年会」で式木のネタをLT登壇して発表してきました。この記事ではそれをブラッシュアップさせて公開しようと思っていたのです(そもそも5分LTでは、わかる人にしかわからない的なので)。で、改めてスライドを眺めていたところ…

あれ、 ( ゚д゚) … (つд⊂)ゴシゴシ

AndAlsoで繋げるとか言っておきながら、作る式木が「&&」で結合されてないじゃん? (;゚Д゚)

うーむ、間違ってる orz その辺りも含めてフォローしたいと思います…


式木とは

.NET 3.5で「Expression Tree」というライブラリが追加されました。C# 3.5ではこのライブラリを使用して、「式木」と呼ばれる機能をサポートしました。
式木とは、例えばC#で記述した式を実行時に解析出来るようなフレームワークです。とは言っても、C#のソースコード文字列を解析できるわけではなく、あくまで式の構造を表現するものです。例えば:

// 普通のラムダ式をデリゲートとして保持
Func<int, string> stringFunc = value => string.Format("Value = {0}", value);

// 例:デリゲートを実行
var result = stringFunc(123);
Debug.Assert(result == "Value = 123");

// ラムダ式を式木として保持
Expression<Func<int, string>> stringExpr = value => string.Format("Value = {0}", value);

// 例:ラムダ式の構造を調べる
Debug.Assert(stringExpr.Parameters[0].Name == "value");
Debug.Assert(stringExpr.Parameters[0].Type == typeof(int));
Debug.Assert(stringExpr.ReturnType == typeof(string));
var mcExpr = stringExpr.Body as MethodCallExpression;
Debug.Assert(mcExpr.Method.DeclaringType == typeof(string));
Debug.Assert(mcExpr.Method.Name == "Format");
Debug.Assert(mcExpr.Arguments[0].Type == typeof(string));

デリゲートと異なり、式木(Expressionクラス)としてラムダ式を受けるように書くことで、ラムダ式の構造を動的に探索することが可能となります。
また、以下のように、それを「コンパイル」して、実際に呼び出すことが出来ます。

// 式木を動的にコンパイルする
Func<int, string> compiledFunc = stringExpr.Compile();

// 出来上がったデリゲートを実行する
var compiledFuncResult = compiledFunc(123);
Debug.Assert(compiledFuncResult == "Value = 123");

わざわざ式木で受けておいて、動的にコンパイルするぐらいなら、初めからデリゲートで良いじゃないか!と思うかもしれませんが、逆なのです。式木を手で構築すれば、同じようにコンパイルが可能となり、高速実行可能なコードを動的に扱うことが出来ます。

// 式木を動的に構築する

// ラムダ式のパラメータに相当する式木
var pValueExpr = Expression.Parameter(typeof(int), "value");

// string.FormatメソッドのMethodInfo
var stringFormatMethod = typeof(string).
	GetMethod("Format", new[] { typeof(string), typeof(object) });

// ラムダ式の本体に相当する式木 「string.Format("Value = {0}", value)」
// Convertが必要なのは、引数の型はobjectなのでintから変換が必要なため(暗黙変換が行われない)
var bodyExpr = Expression.Call(
	stringFormatMethod,
	Expression.Constant("Value = {0}"),
	Expression.Convert(pValueExpr, typeof(object)));

// ラムダ式に相当する式木 「value => string.Format("Value = {0}", value)」
Expression<Func<int, string>> dynamicStringExpr =
	Expression.Lambda<Func<int, string>>(
		bodyExpr,
		pValueExpr);

// 式木を動的にコンパイルする
Func<int, string> compiledFunc = dynamicStringExpr.Compile();

// 出来上がったデリゲートを実行する
var compiledFuncResult = compiledFunc(123);
Debug.Assert(compiledFuncResult == "Value = 123");

そもそもの動機

前置きはこのぐらいで、要するに動的コード生成技術の一つの道具として、式木を使うことが出来ます。インタプリタ的に処理すると動作速度に難があるような場合、コンパイルして実行出来るというのは、非常に大きなアドバンテージです。内部ではILが生成されるので、C#で普通にソースコードを書いてコンパイルして実行したのとそん色ない速度を達成出来ます。

動的にコードを実行する手段として、Emitライブラリを使用してILで書くこともできますが、個人的なメリットとデメリットを挙げると:

式木(Expression Tree)

  • メリット:使用可能なCLRが多い。.NET 3.5以降・Silverlight・Silverlight for Windows Phone・Store App・Universal Windows Platform
  • デメリット:式木の論理構造について色々知っておく必要がある。.NET 3.5と.NET 4.0以降で機能面に差異がある。

IL by Emit (System.Reflection.Emit)

  • メリット:ILで許されるどんなコードでも書ける。ILのルールは比較的単純なので、ある意味習得は容易。新たなクラスやインターフェイス等も自由に定義できる。また、C#では書けないようなコードでも書ける。
  • デメリット:Emitが使えない環境が多い。特にStore AppとUniversal Windows Platformで使えないのが大きい。

Emitが使えないのは、本当に深刻な問題だと個人的には思っているのですが、こればっかりは仕方がない。こういうわけで、式木をメインに考えるのですが…

「.NET 3.5と.NET 4.0以降で機能面に差異がある」(本当はSliverlight for WP辺りにも同じような差がある)

という問題があり、特に.NET 3.5を切り捨てるかどうかで悩む局面がまだあります。3.5なんて古代文明だと言い切れれば無問題なんですが。それで、どうやって.NET 3.5と.NET 4.0の差を埋めるかと言う事を考えるわけです。


例1: Expression.Assignがない

問題点

Expression.Assignとは、代入式を示す式木の事です。

// 代入させたいフィールド
static int _fieldValue;

// ...

// 代入式の概念(C#ソースコードとしてコンパイルは不可)
Expression<Func<int, int>> assignExpr = value => _fieldValue = value;

var assignmentExpr = assignExpr.Body as BinaryExpression;
Debug.Assert(assignmentExpr.Type == typeof(int));
var fieldExpr = assignmentExpr.Left as MemberExpression;
Debug.Assert(fieldExpr.Member.Name == "_fieldValue");
Debug.Assert(fieldExpr.Type == typeof(int));
var valueExpr = assignmentExpr.Right as ParameterExpression;
Debug.Assert(valueExpr.Name == "value");
Debug.Assert(valueExpr.Type == typeof(int));

assignExprの行はコンパイルエラーになる(代入式を含むラムダ式はNGのため)ので、上記のコードは実行出来ませんが、仮に上記コードが成立した場合はこのように式木を探索できます。
C#のラムダ式としてコンパイル出来ないため、.NET 3.5でこの式木を表現可能にすることを見送ったのかもしれません。しかし、結局.NET 4.0では上記のようなコードをExpression.Assignを使って実現出来るようになりました。

// 動的に式木を構築すれば可能

// パラメータの式木
var pValueExpr = Expression.Parameter(typeof(int), "value");

// フィールドを示す式木
var fieldValueExpr = Expression.Field(null, typeof(Program).GetField("_fieldValue"));

// Expression.Assignを使って代入式を示す式木を作り、それをbodyにラムダ式木を生成
Expression<Func<int, int>> assignExpr =
	Expression.Lambda<Func<int, int>>(
		Expression.Assign(fieldValueExpr, pValueExpr),
		pValueExpr);

さて、これをどうやって.NET 3.5で実現するかという話ですが、Assignがない以上、どうやっても素で代入する式木は実現できません。また別の問題として、代入式には2種類あり、代入対象が「フィールド」なのか「プロパティ」なのかによって、生成されるはずのコードが大きく異なる事です。

フィールドへの代入は上記に示したような式で、最終的にはILのstfld命令に落ちます。しかし、プロパティの場合は(見えない)setterメソッドがあり、代入式と言いつつ実はsetterメソッドの呼び出しコードが生成されるのです。したがって、これらを分けて考えることにします。

フィールド代入のシミュレート

このような、.NET 3.5の式木で表現できないコードは、表現できるところでアウトソース的に実行させるようにすれば良いのです。フィールドの場合、「指定されたフィールドに値を代入させる」ようなメソッドがあったとして、そのメソッドを呼び出す式木を構築すれば実現できます。

このような戦略で考えたヘルパーメソッドが、以下のようなものです。

// フィールドに値を代入させるためのヘルパーメソッド
public static TValue FieldSetter<TValue>(out TValue field, TValue newValue)
{
	field = newValue;	// outなので、対象に代入される
	return newValue;	// ちゃんと値を返すことで、式の体を成す
}

このようなメソッドがあれば、次のようなコードで「間接的」にフィールドに値が代入できます。

// _fieldValue = 123; のシミュレート
var result = FieldSetter<int>(out _fieldValue, 123);

あとはこれを式木で表現します。

// FieldSetterのクローズジェネリックメソッド
var fieldSetterMethod = typeof(Program).
	GetMethod("FieldSetter").
	MakeGenericMethod(fieldValueExpr.Type);

// FieldSetterを呼び出す式木から、ラムダ式木を生成
var assignExpr =
	Expression.Lambda<Func<int, int>>(
		Expression.Call(fieldSetterMethod,
			fieldValueExpr, 	// outでもうまいことやってくれる
			pValueExpr),
		pValueExpr);

面白いのは、メソッド引数がoutパラメータであっても、式木上は特に何もしなくてもライブラリ内でよしなにやってくれることです。頭の中で考えていた時は、ここをどうするかが問題だなと思っていましたが、無問題でした。

プロパティ代入のシミュレート

プロパティへの代入が、実際にはsetterメソッドの呼び出しである事は前に述べたとおりです。では、それをそのままFieldSetterのように呼び出してしまえば目的が達成出来そうですが、厄介な問題があります。

// 代入させたいプロパティ
public static int TestProperty { get; set; }

// ...

// resultの値は?
var result = TestProperty = 123;
Debug.Assert(result == 123);

// プロパティのsetterメソッドの戻り値はvoidなので、以下はNG(疑似コード)
// (プロパティのsetterメソッド名は実際には異なる可能性があります)
var result = set_TestProperty(123);

上記のように、代入式の結果は、代入された値となるはずです。なので、Expression.Assignの結果はintの123になります。しかし、単にsetterメソッドを呼び出した場合、そのメソッドの戻り値はvoidであるため、異常な式木となってしまいます。

結局、ここでもヘルパーメソッドの登場となるわけです。

// プロパティに値を代入させるためのヘルパーメソッド
public static TValue PropertySetter<TValue>(Action<TValue> setter, TValue newValue)
{
	setter(newValue);	// デリゲートに設定させる
	return newValue;	// ちゃんと値を返すことで、式の体を成す
}

このヘルパーメソッドは、指定されたデリゲートを実行して、値を返します。フィールドの時のようにoutパラメータが使えれば良いのですが、プロパティメンバに対してoutパラメータを適用することは出来ないので、同じ方法では実現できません。

そこで、「プロパティに代入するラムダ式」を指定させ、デリゲートとして実行することで、間接的にvoid(を返す式木)の問題を回避しようというわけです。つまり:

// TestProperty = 123; のシミュレート
var result = PropertySetter<int>(innerValue => TestProperty = innerValue, 123);

// 更に生っぽく
var result = PropertySetter<int>(innerValue => set_TestProperty(innerValue), 123);

実はExpression.Lambda<T>は、FuncだけではなくActionデリゲートでも定義できます。つまり、値を返さない(void)なラムダ式を示す式木は作ることが出来ます。上記のように、PropertySetterの引数に指定するラムダ式の式木は作成できるのです。

// プロパティを示す式木
var testProperty = typeof(Program).GetProperty("TestProperty");
var testPropertyExpr = Expression.Property(null, testProperty);

// PropertySetterの引数に渡すラムダ式
// innerValue => set_TestProperty(innerValue);
var pInnerValueExpr = Expression.Parameter(typeof(int), "innerValue");
var setterExpr =
	Expression.Lambda(
		typeof(Action<>).MakeGenericType(testPropertyExpr.Type),
		Expression.Call(
			null,
			testProperty.GetSetMethod(false),	// プロパティのsetterメソッドを取得する
			pInnerValueExpr),
		pInnerValueExpr);

// PropertySetterのクローズジェネリックメソッド
var propertySetterMethod = typeof(Program).
	GetMethod("PropertySetter").
	MakeGenericMethod(testPropertyExpr.Type);

// PropertySetterを呼び出す式木から、ラムダ式木を生成
var assignExpr =
	Expression.Lambda<Func<int, int>>(
		Expression.Call(propertySetterMethod,
			setterExpr,
			pValueExpr),
		pValueExpr);

ちょっと式木として大きくなってしまいましたね。.NET 3.5ではパフォーマンスが低下する要因となるかもしれませんが、同じインフラが使える妥協点としては良いんじゃないでしょうか。


例2: Expression.Blockがない

.NET 4.0以降では、Assign同様、以下のようなブロックを含む式を「Expression.Block」を使って作ることが出来ます。

// ブロックを含む式の概念(C#ソースコードとしてコンパイルは不可)
Expression<Action<int, int>> blockedExpr = (a, b) =>
{
	Console.WriteLine("{0}+{1}", a, b);
	Console.WriteLine("{0}*{1}", a, b);
};

// 動的に式木を構築すれば可能

// パラメータの式木
var paExpr = Expression.Parameter(typeof(int), "a");
var pbExpr = Expression.Parameter(typeof(int), "b");

// Console.WriteLineのメソッド
var consoleWriteLineMethod = typeof(Console).
	GetMethod("WriteLine", new[] {typeof(string), typeof(object), typeof(object)});

// ブロック内のそれぞれの式
var innerExpr1 = Expression.Call(consoleWriteLineMethod,
	Expression.Constant("{0}+{1}"),
	Expression.Convert(paExpr, typeof(object)),
	Expression.Convert(pbExpr, typeof(object)));
var innerExpr2 = Expression.Call(consoleWriteLineMethod,
	Expression.Constant("{0}*{1}"),
	Expression.Convert(paExpr, typeof(object)),
	Expression.Convert(pbExpr, typeof(object)));

var blockedExpr = Expression.Lambda<Action<int, int>>(
	Expression.Block(
		innerExpr1,
		innerExpr2),
	paExpr,
	pbExpr);

さて、Blockがない.NET 3.5でこれをどう調理するか。

Expression.Blockがやる事

Blockは、n個の任意の式を順番に実行します。NGKのスライドでは、n個の式を逐次実行する方法として、それぞれの式が常にtrueを返す式と見なせたとすれば、Expression.AndAlsoを使って数珠つなぎにすることで、逐次実行が出来ることを示しました。

// それぞれの式が常にtrueを返すと仮定すれば、以下の式で逐次実行できる
var dummy = ((true && expr0) && expr1) && expr2;

ここの部分をTruenizeメソッドを呼び出すように書けばよい的に紹介したんですが、AndAlsoがどこにもない (;´Д`) 実際はこんな感じです。

// 結果を受け取るが、常にtrueを返すヘルパーメソッド
public static bool Truenize<TValue>(TValue result)
{
	return true;
}

// Truenizeを使って式の結果を常にtrue化し、それをAndAlsoで連結する
var dummy = ((true && Truenize(expr0)) && Truenize(expr1)) && Truenize(expr2);

しかし、スライドで示したTruenizeでも連結実行出来てますね… あれも方法としてはアリでしょう。多分風邪にやられてトチ狂った結果ってことで勘弁。

もう一つ、式が値を返さない(void)場合は、上記TruenizeではTValueがダメなので、PropertySetter同様、ラムダ式を使って間接的に実行させるTruenizeが必要になります。以下のような感じです:

// 結果を受け取るが、常にtrueを返すヘルパーメソッド
public static bool Truenize(Action action)
{
	// デリゲートを実行
	action();
	return true;
}

// Truenizeを使って式の結果を常にtrue化し、それをAndAlsoで連結する
var dummy = ((true && Truenize(() => expr0)) && Truenize(() => expr1)) && Truenize(() => expr2);

非ジェネリックTruenizeでデリゲートを実行させて、voidな式木をラムダ式として実行させるようにします。実際の実装はPropertySetterと同様なので省略。

スコープセパレーション

もう一つの重要な特徴も忘れてました(やばい、これかなり大きなトピックだった)。それは、「スコープを狭める役割」もある事です。ブロック内で宣言された変数はブロック外では使えません。以下は疑似コード:

// 外側のスコープ
var localVariable = 123;

{
	// 内側のスコープ(実際にはコンパイル不可)
	var localVariable = 456;
	Debug.Assert(localVariable == 456);
}

// 影響ない
Debug.Assert(localVariable == 123);

スコープの問題は、式木全体としてちゃんと考えれば影響ないかもしれませんが、式木がどのように組み立てられるか分からない場合は、相互に影響を与えなくてすむ方法があれば、それに越したことはありません。

これのシミュレートはかなり悩みました。色々考えた結果、同じようにスコープを制限しているという特徴を利用したらどうか、と言うものです。それは何か? ラムダ式ですよ。

// ラムダ式の「引数」は、スコープ内のローカル変数と見なせる(疑似コード・実際にはコンパイル不可)
Action<int> localBlockExpr = localVariable =>
{
	// 外側に影響を与えない
	localVariable = 456;
	Debug.Assert(localVariable == 456);
};

// 外側のスコープ
var localVariable = 123;

// デリゲートを実行
localBlockExpr(default(int));

// 影響ない
Debug.Assert(localVariable == 123);

ラムダ式を使ってラップすることで、引数をローカル変数としてみなし、スコープを強制することが出来ます。これと同じことを動的に式木を組み立てて実現すれば良いわけです。但し、まだ問題もあります。上記のままだと、スコープ内のローカル変数が増えると、Actionデリゲートのジェネリック引数を増やす必要があり、実装上も上限的にも辛い香りが漂います。代替案として思いつく方法は:

// ローカル変数としての受け皿を保持するクラスを動的に生成する(疑似コード)
public sealed class LocalVariableClosure
{
	public int localVariable1;
	public int localVariable2;
	public int localVariable3;
	// ...
}

// これを引数にする
Action<LocalVariableClosure> localBlockExpr = localVariableClosure =>
{
	// LocalVariableClosureをクロージャ的に扱う
	localVariableClosure.localVariable1 = 111;
	localVariableClosure.localVariable2 = 222;
	localVariableClosure.localVariable3 = 333;
	// ...
};

// LocalVariableClosureを生成して渡す
localBlockExpr(new LocalVariableClosure());

アイデアとしては良いんですが、お手付きですね。LocalVariableClosureクラスを「動的」に作るには、Emitが必要です。そんなわけで、Emitしなくても同じような事が出来ないか頭をひねるとこうなります:

// object配列の引数を渡し
Action<object[]> localBlockExpr = localVariables =>
{
	// 配列の各要素をローカル変数と見なす
	localVariables[0] = 111;
	localVariables[1] = 222;
	localVariables[2] = 333;
	// ...
};

// object配列を生成して渡す(要素数が分かる必要がある)
localBlockExpr(new object[3]);

しかし、まだ足りない。上記コードのように配列に代入したり参照したりするためには、objectとの型の変換が常に必要になります。式木として扱う場合、式木の各要素は基底クラスの「Expressionクラス」から継承しているわけですが、この代入を成立させるには型変換(Convert)も式木に盛り込まなければなりません。また、その変換は左辺側の式木内で吸収出来る必要があります。段々気絶級になってきた…

// object配列の引数を渡し
Action<object[]> localBlockExpr = localVariables =>
{
	// 型変換(Expression.Convert)が必要。しかも本当は左辺での実現が必要
	localVariables[0] = (object)111;
	otherVariable = (int)localVariables[0];
};

# C++なら参照使って左辺で表現出来るんですが…

結論の前に、ローカルスコープ内の式木を、例1のフィールドへの代入式の例で表現するとこうなります:

// ローカル変数パラメータ
var localVariablesExpr = Expression.Parameter(typeof(object[]), "localVariables");

// 代入式
var assignerExpr = Expression.Call(fieldSetterMethod,
	fieldValueExpr,
	Expression.Convert(	// どう頑張っても右辺でしか表現できない
		Expression.ArrayIndex(
			localVariablesExpr,
			Expression.Constant(0)),
		typeof(int)));

// ラムダ式にしてテスト
var testExpr = Expression.Lambda<Action<object[]>>(
	assignerExpr,
	localVariablesExpr);
var compiledAction = testExpr.Compile();

var testLocalVariables = new object[] {123};
compiledAction(testLocalVariables);
Debug.Assert(_fieldValue == 123);

Blockのシミュレートアイデアとしてはこれで全てです。が、これを使って.NET 3.5でも.NET 4.0でも同じように記述可能にするには、Expressionクラスをラップするエミュレーションレイヤーが必要になります。特に左辺をローカル変数式木としてだまして(.NET 4.0ではExpression.Variable)置きながら、実際に生成される式木では配列参照とConvertを挟む必要があるため、ユーティリティメソッドレベルの対応ではたぶん無理だろうと考えました。

そんなわけで、上記の全てを組み合わせたコードはあまりにも大きいため、ここでは示しません。エミュレーションレイヤーとしてExpressionクラスを再定義した例を以下のコードに示します。

.NET 3.5での実装:
CenterCLR.ExaSerializers/ExpressionCompatibilityLayer_Strict.cs

.NET 4.0での実装:
CenterCLR.ExaSerializers/ExpressionCompatibilityLayer_Extended.cs

文中のサンプルコードはここ:
CenterCLR.ExpressionTreeCompatibility


まとめ

.NET 3.5の式木サポートで不足しているものは他にも色々あると思います。ループとかもそうですね。.NET 3.5に欠けているものは、C# 3.0の式木として受け入れられないもの、のように思います。そこから慎重にドリルダウンすることで、シミュレートするアイデアが得られるのではないかなと思います。とはいえ、やってみて分かったのは、想像以上に沼地だったと言う事です ( ´Д`)=3 フゥ

最後にサンプルで紹介したコードは、高速バイナリシリアライザです。テストはかなり通してあるので、興味があればいじってみて下さい。特に逆シリアル化でストリーミング出来るようにしたのはイケてると、勝手に思ってます。

GitHub: CenterCLR.ExaSerializers

次はvarmilさん、よろしくお願いします!

式の体を成して無い式を式の体を成すように式と式木で何とかする式 – NGK2015B 名古屋合同懇親会2015忘年会

WP_20151205_14_03_21_Pro_LI年末ラッシュの季節です。NGK2015B 名古屋合同懇親会2015忘年会で、「式の体を成して無い式を式の体を成すように式と式木で何とかする式」の題目で登壇してきました。

NGKは忘年会なので、ネタが多くて面白いです。全セッション完全5分LT形式です。

「高校生がプログラミングを三年間で学んだ軌跡」とか「リアル波動拳をつくってみました」とか、爆笑しました (*´Д`)

今年も気の利いたネタを披露したかったのですが、秋口から体調が優れず、普通ネタを高速展開するという、いつか見たアレになってしまった…

.NET 3.5環境で、.NET 4以降で使えるExpression Treeのシミュレートをしようという内容です。
パズルっぽい感じで何とかなった感あります。ただし、すべてのExpression Tree Nodeがシミュレートできるのかどうかはわかりません。

オリジナルスライドはこちら:式の体を成して無い式を式の体を成すように式と式木で何とかする式.pptx


この記事には、まとめなおした完全版があります。参考にどうぞ:「真・式の体を成して無い式を式の体を成すように式と式木で何とかする式」

MVPとシアトルとWindows Phone (2)

WP_20151101_07_35_54_Rich今年も年末に突入しました。この記事は「Windows Phone / Windows 10 Mobile Advent Calendar 2015」の4日目の記事の本編です。(前編はこちら)

MVP Global Summitへ

WP_20151101_12_24_33_Richそんなわけで、11月第一週は、MVP Global Summitに参加すべく、米国ワシントン州・シアトル・レドモンドへと行ってきました。国外に出るのは初めてなので、色々準備が必要だったのですが、特にパスポートの受領とスーツケースの新調が大きかったです。スーツケースは以前に長期出張の時に持っていっていたものがあったのですがすっかり忘れていて、どのみちもう重いのも辛いので、軽いやつを買いました。大きさは中ぐらい?何かお土産が出来た時に入るぐらいの大きさってことで考えました。

飛行機は成田からシアトルまでのデルタ航空便でした。搭乗時間は9時間。搭乗時間が午後3時ぐらいですが、シアトルに着くと朝だそうです。機内でどんな暇つぶししようかと思い、ノートPCとか手荷物にしたのですが、とにかく乗ったらすぐに寝ないとヤヴァイ事が判明し、さっさと寝ることに。まあ、そんなに簡単に寝れる訳ないんですが…

WP_20151101_13_52_17_Richそんなこんなでシアトルに到着。周りはみんなネイティブで何言ってるか分からない… 何か乗客が流れるままに地下鉄のようなシアトル空港内の移動システムに乗り、無事に外に出てきました。ここからはツアーバスでシアトルのダウンタウンに移動して、しばし見物など。MVPのサミットセッションは翌日からなのです(前編で紹介した通り、Global Summitは全部NDAなので、残念ながらこの記事では何も紹介できません…)。

シアトルの気候は写真の通り、いつも曇り時々小雨、のような、煮え切らない天気です。なのですが、私はすごく過ごしやすく気持ちよさを感じました。気温は日本と同じぐらい。この季節だと少し肌寒い感じです。が、湿気が低いんでしょうか?それとも(名古屋に比べて)空気が良いのか、ホント快適に感じました。帰って来てからもいつも思い出すのは、この空気感ですね。ちょっと恋しいです。

WP_20151101_12_31_42_Richシアトルのダウンタウンには、有名な「スターバックス一号店」があります。見ての通り、お客さんでごった返していて、とても入れる感じではなかったのが残念です(しかもまだ現地に着いたばかりで、英語に圧倒されてて何もできなかった)。

ダウンタウンの探索はあまり時間もなく、恐る恐る徘徊してたこともあって、色々なところを見て回れなかった… ちょっと良い雰囲気の店もあったりして、シアトルに観光に行くというのは珍しいのかもしれないけど、良い所だなと思いました。


現地でプリペイドSIMを調達して使えるようにするまで

ところで、機内で(というより出発前から)心配だったことの一つが、現地で通信の確保をどうするのか、でした。参加者に話を聞いてみると:

  • 国内キャリアの完全ローミング契約:ソフトバンクに何か良い契約があるらしいです。が、うちはIIJMioなので関係ない。
  • 旅行者向けのWiFiルーターレンタル:国内に居る時に(場合によっては空港で)契約してすぐに使える。日本語が通じるうちに解決可能なので良いかも。
  • 現地でプリペイドSIMを買う:チャレンジャー

やりましたよ、チャレンジャー (;´Д`)
折角Lumia持ってるし、これはやっておかないと。それに自分で試さないと別の国の文化などモノに出来ないぃぃっっ! とか、勝手に盛り上がってました。しかし、一体どこでSIMが買えるのか、それもプリペイドSIMで、しかもLumiaに適合しなければならない。

Lumiaの仕様はここで確認できます。 例えばLumia 1520のSpecification/Cellular connectivityを見ると、

  • WCDMA network: Band 1 (2100 MHz), Band 2 (1900 MHz), Band 5 (850 MHz), Band 8 (900 MHz)
  • LTE FDD network: Band 1 (2100 MHz), Band 3 (1800 MHz), Band 7 (2600 MHz)

と書いてあります。私が知っている米国のモバイル通信業者は、AT&T・Verizon・T-Mobileぐらい。これらの事業者が対応するBandが、上記に当てはまる必要があります。とりあえずどこかでSIMを確保しなければと思って行ったのが、スーパーマーケット(?)のTARGET。しかし、ここのスマートフォンコーナーで店員捕まえて「Prepaid SIM」を連呼したら、どうも扱っていない様子。ヤヴァイ、ヤヴァイぞと思って隣の家電量販店(?)のFry’sに行ったら、ありましたよSIMが。WP_20151203_21_49_21_Pro_LI

「AT&T Prepaid SIM」の名目に、これ以外にもT-Mobileがあった気がするけど、これしかなかろう的な感じで。値段は$10でお安い。どうせ足りなかったらチャージすればいいんだし(出来るかどうかの自信はなかったが)。店の中で写真を撮ったらヤヴァイんじゃないかとか思って取らなかったのですが、これがぶら下がってる陳列棚からどうしてもこのパッケージを外せなくて、誰も見ていないかオロオロと確認しつつ、無理やり引きちぎってダッシュでレジに持って行ったのは内緒です。

WP_20151203_22_39_37_Pro_LIAT&Tだと、HSPAのband5とGSMが行けます。LTEだとかすらない。実はこれ以外にも、NEC Aterm MR04LNも準備してきました。こっちも同じくHSPAのband5とGSMが行けます。残念ながらLTEは合いませんが、とにかくメッセンジャーだけでも開通すれば、安心感がかなり違うので良しとします。それに、シアトルにはWiFiのフリースポットが沢山あります。ちょっと困っても、どこの店のスポットか分からないのですが、無料で接続できるのです(セキュリティ的に危険なのは承知の上)。だから、あくまで担保と言う事で。

AT&TのSIMは買ってみて改めて分かったことですが、契約をフレキシブルに後から決めることが出来ます。プリペイド契約の他に、一般的な月極の契約も可能で、SIMの「アクティベート」時に決める感じです。しかし、このアクティベートが問題で、電話モードにした後で特定の番号を入力し、「ZIP code」を入力します。これつまり「郵便番号」の事なんですが、当然米国在住じゃないので、ZIP codeなんてあるわけがない…

で、まだ開通していないSIMで電話番号入力させて開通させるというのも、認証とかのプロトコルってどうなってるのかなーと技術者らしい事を考えつつも、「そうだ、タブレットだとこうは行かないはずだから、別の手段があるかもしれない」と考え直し、for Tabletsという冊子を読んでみると、ありましたよやっぱり、Webでの契約方法が。

つまり、流れとして、フリーWiFiスポットとかで接続を確保して、AT&Tのサイトに行ってWebで契約すれば、ZIP code必要ないかも?

が、もっと甘かった。Webでの契約ではクレジットカードが必要なんですが、そこにはZIP codeどころか住所から入力する欄があり、しかも米国前提で州から入力するフォーム…

こりゃいよいよヤヴァイな。通信経路が確保できないと、ちょっと冒険的に街を徘徊することも出来ないかも知れない。と暗雲立ち込めてきたのですが、もう住所適当にホテルとかで入れようかどうせカードと一致してないとNGだろうけど何でPayPalに対応してないんだブツブツ… とか何とか言いながらトライしてたら、

通ってしまいました (;´Д`) 開通したスクリーンショット撮っておけば良かったなぁ。

WP_20151203_23_38_19_Pro_LIしかし住所やらZIP codeの情報は何に使ってるんだか。と言う事は初めからLumiaでZIP codeをタイプしてたら、電話も含めて使えるようになっていたのかもしれない(タブレット契約だと音声通話は出来ない)。何はともあれ、これで少しは探索めいたこともできなくもないかという事に。通信の重要性を改めて感じました。

なお、契約は$30のプランにしておきました。Auto renewをオフっておいたので、30日経過後は自動的に終了ってことで、プリペイド相当になります。ここらへんも非常にフレキシブルで進んでるなーと思います。


Microsoft Official StoreとPhone

WP_20151101_15_57_20_RichシアトルのHyatt bellvueがホテルなんですが、そのすぐそばに「Microsoft Store」があります。

ここはおひざ元らしく、発売されたばかりのSurface BookやBand2の在庫がありました。Band2は買うつもりだったのですが、残念ながら欲しいMサイズは売り切れのため、入手する事かなわず。Bookにも惹かれましたが、さすがに$3000近くの買い物をサクッとやる勇気はなく…(30万超えると損金計上出来ないというのが大きい)

このときのJapan MVPによるBook威力購買事件 (;´Д`) はこっちにまとまっているのでどうぞ。

WP_20151101_15_52_16_Richさてさて、ずらりと並んだWindows Phone。この反対側にもサードパーティ製Windows Phoneがズラリと並ぶ光景。日本では見られない景色です。この中にとあるLumiaがありました。

WP_20151203_23_23_13_Proそれがこの「Lumia 640 LTE」です。色々いっぱいっぱいで忘れてたんですが、今箱見たら$79って書いてありましたが、なんか「クソ安い」感じだったんです。togetterでもまとめ買いしている人が居たようですが、これ本当に安い。何しろContract(期間が指定される契約)ではないので、プリペイドとして使うことも出来るのにこの値段。もちろんSIMロックされていますが…

(写真手前のテーブルには「Unlocked phones」と書かれていますが、これがSIMロックなしです。もちろん高いです)

買ってすぐに使い始めたんですが、液晶の視野角はあまり広くないものの、色の具合は1520を超えているかもしれない。艶やかな感じで良いです。タッチの反応も1520ほどではありませんが、MADOSMAを超えてる気がします。エントリーモデルなのに高級感を醸し出しているのが良いです。当然ですが、AT&TのSIMが付いていたので、最初に買ったSIMは不要な感じに。まぁこうなる事が分かってたら買わないわけで、こればっかりは仕方がない。

勢いで買ってしまった感があったんですが、非常に満足感が高かった。勿論、その日のうちにWindows 10 Mobile Insider Previewに更新。ちなみにSIMロックですが、6か月経つとAT&Tのサイトでアンロックできるようになるらしいです(すぐにアンロック出来るのかと勘違いしてトライしたけど、流石にダメだった)。ほかの方法として、某SIMロック解除サイトを使うと、$15ぐらいでアンロック出来ることも判明。参考までに。


まとめ

MVP Global Summitに参加するのが主題だったんですが、初の海外旅行・シアトル良いところ・サミットセッションももちろん、偶然入手したWindows Phoneや現地SIM調達など、ものすごく盛り沢山の旅行で本当に良かったです。次回MVPを受賞しているかどうかはわかりませんが、また行ってみたいなぁと思っています。

次のカレンダーはアヤノフさんです、よろしく!

MVPとシアトルとWindows Phone (1)

WP_20151101_15_44_16_Rich今年も年末に突入しました。この記事は「Windows Phone / Windows 10 Mobile Advent Calendar 2015」の4日目の記事の前編です。(前日に公開しています。本編は予定通り明日公開です)

さて、今年私は「Microsoft MVP for .NET(今はカテゴリーが再編されたので、Visual Studio and Development Technology)」を受賞しました。MVPを受賞すると特典の一つとして、一年に一度行われる「MVP Global Summit」への参加資格が得られます。生まれてこの方、一度も日本を出たことがなかったので、この節目に行ってみようかと思いました。この記事ではその話に絡めてWindows Phoneの話をしようかと思いましたが、その前に「Microsoft MVP Award」(長いのでMSMVPと略します)についての紹介もしようかと思います。


Microsoft MVP Awardとは

MSMVPの紹介ページには、以下のような事が書いてあります。

「Microsoft Most Valuable Professional (MVP) は、自身のマイクロソフト技術に関する知識や経験を最大限に活かしながら、他のユーザーを積極的にサポートしている、非常に優れたコミュニティのリーダーです。彼らは、技術専門知識に加えて自主性と情熱を兼ね備え、マイクロソフト製品の実用的な活用方法をコミュニティやマイクロソフトと共有しています。」

コミュニティのリーダーかどうかは分かりませんが (;´Д`) コミュニティ活動を積極的に行っていることに対して、Microsoftが表彰する制度の事です。ただ、これは「受賞を目指した」り「受賞する」までは、実にどんな制度なのか分かりにくい面があると思います(実際、某フォーラムでは、MVPだからオフィシャル的な云々のような辛みのあるポストが度々発生)そこの所を端的に分かるようにしてみます。

MSMVPの表彰は、一年に一度、過去一年間に渡ってのコミュニティ活動が評価されて表彰されます

例えば、私の場合は2015.04受賞ですが、応募期間は3か月前が締め切りなので、2014年年始から2014年の年末までのコミュニティ活動が評価されて受賞したことになります。年末・3月末・6月末・9月末、が応募締め切りとなるので、狙っている方はその時期の早めにサイトをチェックすると良いでしょう。

MSMVPの応募は、自薦と他薦です

ただ、自薦が多いんじゃないかな。私も自薦応募です。自薦・他薦の比率は分かりませんが、あまり重要ではないでしょう。応募の際には、過去1年間の活動状況をシートに記述する必要があります。例えば勉強会で登壇したり、ブログ記事を書いているなどの活動記録を、あらかじめ収集しておくことをお勧めします。最初は面倒ですが、受賞後はオンラインのフォームに入れておくだけで、MVPプロフィールと連動して評価されるらしいので、楽になると思います。

「コミュニティ活動」とは何?

私が無償で勉強会やブログ記事を書いたりしていますが、これもコミュニティ活動です。一方、有償セミナーや書籍を沢山書いて出版している著者の方も受賞していたりするので、結構幅広くとらえられているようです。どのような基準で評価されるのかは、非公開であるため分かりません。受賞した場合や落選した場合でも、その理由は明らかにされません(試験と一緒ですね)。

受賞カテゴリーが決まる必要があります

少し前にカテゴリーが大幅に整理されました。MSMVPへの応募を行う場合は、これらのカテゴリーの何れか一つだけを指定する必要があります。ご自身の最も専門である分野を選ぶことになりますが、審査の過程でカテゴリーが異なると判断された場合は、別のカテゴリーで受賞する可能性があります。

MSMVPに与えられる特典とは

WP_20150414_21_04_18_Raw
多分、非受賞者からみて、ここが最もモヤモヤするところなのではないかと思います。

  • MSMVP Award Kitの受領:写真のような、立派なトロフィーや証書がもらえます。
  • MSDN Enterprise subscription / Office 365 1年分
    恐らく開発者にとっては、一番欲しいと思っているものかも知れません。MSMVPは1年間の受賞なので、次年度選考に外れてしまうと、Subscriptionは継続できません。
  • 米国本社のカテゴリー別開発チームとの、NDAベースのディスカッションの機会が得られる
    メールでスケジュールとか飛んできます。Skypeで参加するので、基本的に英語が喋れないと参加は難しいです。おまけに時差の影響があるので、ほとんどが夜中となります。日本人には敷居が高いかも…
  • MVP Global Summitへの参加権
    毎年11月ぐらいに催されているようです。期間中、米国本社(ワシントン・シアトル付近)のホテルに無料で宿泊できますが、飛行機代やそれ以外のコストは自腹でねん出する必要があります。また、ここで見聞きする技術情報はNDAベースなので、最新情報を聞いて帰ってきても、喋ることは出来ません。

NDAとは、秘密非開示契約というもので、見聞きした事は第三者に漏らしてはならないというものです。だから、どれだけNDA情報を知ったところで、twitterやfacebookでつぶやくどころかブログに書いたりオフで誰かに喋ったりなどは全く出来ません。これは、同じMSMVP受賞者同士にも当てはまります。どうですか?「それほど」強力な特典でもない気がして来ましたか? 物的な特典を期待していると、それほどでもないと思います。

誤解のもとになっている事かも

MSMVPに対する誤解の一つが、MVPというブランド的な力が良くも悪くも強いため、MVP受賞者というだけでMicrosoftの関係者と同じか、あるいはそこにきわめて近しい人のように見えてしまうことにあるのかなと思います。上に挙げたようにMSMVPの特典は良いものですが、その一方で、MVP受賞者だからとある第三者に何か便宜を図る事が出来たりとか、チートな権力を持っていたりとかは全くありません。また、受賞者は普段からMicrosoftの「製品」や「技術」をコミュニティに広げようとしているだけなので、賄賂めいた何かがあるわけでもありません。実際、広く活動している方が翌年度には落選したりすることもあります。しかも、その理由は明らかにならないのです。

だから、あまりフォーラムや掲示板で無理ゲーなことを言われても困惑する事になるわけです。

また、専門分野があるので、それ以外の事を聞かれてもまた困ってしまいます。いまやMicrosoftの技術は一人ですべてを把握するのは不可能なぐらい幅が広いです。.NET受賞者である私に「Excelのマクロのこの関数が…」と聞かれてもさっぱりわからないのです。MSMVP受賞者、というだけで超サイヤ人的に見られるのも結構つらみがあります。

追記: de:code 2016で、MVP Ask Meというキャンペーンが行われました。その時に配布されたパンフレットを公開しています。

「Windows Phone / Windows 10 Mobile Advent Calendar 2015」本編の(2)へつづく

ChalkTalk CLR – 動的コード生成技術(式木・IL等)

ChalkTalkCLR12122636_1505045119820235_1955812025993410087_n「ChalkTalk CLR – 動的コード生成技術(式木・IL等) 」というお題で、小規模で濃い話が出来る場を設けてディスカッションしてきました。

今回の会場は、「Maker Lab NAGOYA」さんをお借りすることが出来ました。モノづくり主題のワークスペースで、色んな加工装置とかあって、IoTネタとか常時面白そうなことをやっています。案内頂いたMatsuokaさんは、.NET Micro Framework (GR-PEACH) のバグフィックスとかやっていて、わぉーと言う感じです。

WP_20151017_12_58_31_Pro__highres


開催の背景

「ChalkTalk」という名称は、チョークと黒板を使って、密なディスカッションをする、のような意(のはず)です。普段のCenter CLR勉強会でもディスカッションと言う事は意識しているのですが、もっと密にやりたかったという動機があります。

  • 今回のテーマが、ILや式木なので、いきなり展開するにはディープ過ぎる。
  • でも、個人的にはやってみたい。なので、小規模でやってみて、このネタをどうやって本会でやるのかを練ってみたい。
  • そもそも、Center CLRに近しいメンバーが、この辺りの技術にどれだけ長けているか・追従できるか、と言う事を知りたい。
  • そして、ChalkTalkは面白い(知ったのはde:code 2015)ので、自分でもやりたい。

と言う事が背景にあって、オーガナイザーなんだから、やってみればいいよね、ダメなら今回で終わりにすればいいし、と思って開きました。

WP_20151017_17_49_59_Pro__highres告知通り、事前に資料の準備なし(以前にやった勉強会の資料とかはありますが)としたのですが、あまりにフリーダムだと議論が発散する可能性もあったので、KPTを使って、参加者がどのような課題を持っているのかを視覚化してみようと思いつき、このような感じで5分で書き出してもらいました。

一部の参加者が「COMが~COMが~」と言っていたのですが、COMの基礎をやり始めるとそれだけで潰れてしまうので、やむなく我慢してもらうことに (;´Д`) 何故COM?と聞いてみたところ、「WinRTのバックグラウンドにCOMがあるらしいけど、もうググっても情報が得られなくて云々…」との事。この、某学生MVPには、やたら熱くアピールされたので、COMについてはまたChalkTalkでリベンジすることにしました。

結局、KPTでは各参加者のレベル確認的な所となり、順当にILとかCLSの説明からスタートする事に。KPTの導入は、私のアジャイル方面への試みの一つでもあるんですが、全然生かせていないのは今後の課題だなと感じました。
題目が「IL・式木」と言うように、初めから技術にフォーカスしていて、これらの技術を使う動機というか、バックグラウンドと言うか、そういうところに焦点が当たりにくかったのも敗因かなと思っています(技術フォーカスだとわかりやすいし、もともとは自分がやりたかった話なので、仕方ないんですが)。


ILとCLSの話

makerlab1x86とかx64のような物理CPUアーキテクチャの話と、それをつなぐCLSやILのコードの関係・実行時に何が起こっているのかという概要の話をしました。参加者には経験が浅くてILとかそもそも中間言語や仮想CPUとかわからない方も居たので、概要レベルでの導入を試みました(どんなエキスパートでも、最初に通る道なので、知らなくても恥じる必要はないです)

特に、仮想的なCPUが「スタックマシン」であり、スタックの出し入れで処理がおこなわれる、という所が焦点だと思ったので、ILSpyで逆コンパイルした結果を見せ、仮想CPUのスタックにどのように値が出し入れされるのかを、詳しく説明しました。この部分は、以前にMGK2015(三重合同懇親会)で、LTでILを説明するという無謀を試みたことがあり :)、その話も交えながら。写真のように、白板にスタックの図を描いて、ILコードからどのようにスタックが操作されるのかを詳しく説明しました。

ILの仮想CPUがスタックマシンであることと、もう一つ重要な概念として「box化(boxing)」とその解除の話をしました。.NETにはValueType型があり、この型を継承した型が構造体(struct)となって、ILのレベルでは扱いが異なると言う事を話しました。マサカリ来ましたねー (;´Д`) ValueType型自身は構造体ではなく、クラスでしたね(今、ちゃんとリファレンスで再確認しました)。

構造体のインスタンスは、ローカル変数領域(.locals)や、フィールド変数などで、直接そこに値が格納されます(物理CPUでどう実装されるのかは、ひょっとしたら違いがあるかもしれません)。それに対して、「参照型(クラス)」は、不透明なポインタ値がそこに格納され、実際の値(インスタンス)は、ヒープ領域に格納されます。object型のフィールドや変数にインスタンスを格納する場合、構造体のインスタンスであれば「box命令」「unbox.any命令」で、ヒープ領域への格納と取り出しが行われる、というのを図示しました。この操作にコストがかかることについても理解してもらえたと思います。

トリビアとして、box化を忘れたり、やらなくてもよい所でbox化や解除したりすると、おかしな現象が起きたり起きなかったり、InvalidProgramExceptionで落ちるよ、みたいな話もしました。CLRがロード時にILに対してどこまでの検査をしている・していないのか、というのは興味深いところです。bleisさんからは、PeVerifyツールを紹介して頂きました。自分でEmitで生成したコードに問題がないかどうかを、静的に解析できます。ILSpyは、ILの使用方法が間違っていても、一見それっぽく逆コンパイルコードを表示してくる(クラッシュする場合もある)ので、初めのうちは特に注意した方が良いでしょう。

ILについては、あとは命令のバリエーションを理解すれば、少なくともILSpyで逆コンパイルしながらコードを調整していくことで、そこそこILが書けるようになると思います。と、ここで、「IL Support」という拡張機能の事についても教えて貰えました。これを導入しておくと、il用のソースを別に用意しておき、C#側からそれを参照する定義をexternで書いておくだけで、動的にEmitしまくる事無く静的に自分のコードにILを導入できます。更にILソースファイルは、pdbにデバッグ情報も出力されるので、デバッガでILにステップイン出来るという、正に神ツールです。すばらすぃ….

ILを書いたソースコードファイルをhoge.ilのようなファイルで保存し:

.namespace CenterCLR
{
	.class public ILSupportSampleClass
	{
		.method public static int32
			Compute(int32 a, int32 b) cil managed 
		{
			ldarg.0
			ldarg.1
			add
			ret        
		}
	}
}

C#側からはMethodImpl属性で参照可能にします。

namespace CenterCLR
{
	public class ILSupportSampleClass
	{
		[MethodImpl(MethodImplOptions.ForwardRef)]
		public static extern int Compute(int a, int b);
	}
}

式木の話

ILの話は低レベルでなじみが薄いため、一般的にはILの方が難しいような印象がありますが、上記の話を押さえておけば、結構誰でも掛けたります。間違ってると簡単にクラッシュもしますが。しかし、式木については概念からわかってないと扱いづらい(≒解説が長期化してしまう)と言う事もあって、どうやってディスカッションするか、考えあぐねていました。

第一回~第三回のCenterCLR勉強会でも度々取り上げてきたので、参加者には「ぼんやりと」式木の概念は伝わっていたようですが… ここでもbleisさんに、式木についての説明をしてもらいました。王道的に計算式的な定義から、ノードに分解される様、そしてノードの木構造があれば、プログラマブルに探索が可能・変形も可能、という感じでの解説でした。簡潔で分かりやすい。ちゃんと伝わったかな?

式木は:

  • (C#において)ラムダ式からExpressionクラスのインスタンスを取得して、式を動的に解釈する(Entity FrameworkのようなO/Rマッパーが行っている手法)
  • Expressionクラスのインスタンスを動的に組み立て、Compileメソッドを呼び出して、実際に実行可能なデリゲートを入手する
  • 式木ノードを探索して、メタデータ参照に役立てる

というような代表的な用途があることを説明しました。

最初の例では、Entity Frameowrkの話だけでは終わってしまうので :)、Final LINQ Extensions IIIで解説した、即席O/Rマッパーについて軽く復習して、式木の内容をどうやってサーバーサイドで実行するか、のような話をしました。

次に、がりっち=サンに話を振って、「某ronia」というツイッタークライアントのフィルタ条件を動的に構築するために、入力されたフィルタ式をパースして(ここは手動)、Expressionを生成して式木を組み立てて、フィルタ処理を実現した、という話をして貰いました。

最後に、EXCEL仕様書の駆逐ネタでやった、メタデータの参照利用としての式木の話をしました。

そのほか、貴重なお話として、bleisさんの業務での式木の利用経験として、F#の式木をどうやって解釈するのかを考えたという話をしてもらいました。F#の式木は、概念的にはC#(というより.NET FrameworkのExpression)と同じようなものですが実際は異なり、高速化のためにExpressionにマップしてCompileするか、またはF#式木を直接解析してILを生成するか、のような事で、いくつか障害があって頭を悩ましたという話を聞くことが出来ました。この話、以前に何かで見た記憶があるなーと思っていたのですが、今になって繋がった!みたいな、妙な感動がありました。

# 関数型言語にまつわるドタバタで徘徊していた時に見た気がする。
# (えーと、私は面白い技術であればおk的な立場です)

ちなみに、このF#の解析器、手伝ってくれる人募集中とのこと。私が手伝うには、F#をモノにする必要があるな….


総括

WP_20151017_17_54_15_Pro__highres結局、結構時間もおしてしまって、いつも通り(?)最後にバタバタしてしまったのは申し訳ないです。クロージングでKPTを再度書いてもらったのですが、振り返りの時間がまともに取れなかったのでダメですねー。でも、ディスカッションとしては楽しい時間を過ごせました。参加者の皆さんありがとうございます。

課題的にはやはりというか、参加者に想定する知識レベルの差をどうやって埋めるか、と言う事が課題だなと思います。長い目で見た場合、Center CLRへの参加者のレベルが全体的に底上げされて来れば、だんだん解消されるかなと思っています。気の長い話ですが、コミュニティベースなので、このぐらいが良いかなと。ChalkTalkについても、レベル差で細分化した方が良いのかもしれません。私のマンパワーにも限界があるので、中々難しい所ですが、あまり小難しく考えず、トライして方針を修正していく、という感じでやりたいと思っています。

それにしても、白板に書きまくったのに、写真全然撮ってなかったのも残念。だれかカメラ担当お願いすれば良かったです orz

不健康なIT戦士を健康的にするアレの話 – 第13回まどべんよっかいち勉強会

WP_20151010_13_18_22第13回まどべんよっかいち勉強会で、「不健康なIT戦士を健康的にするアレの話」というタイトルで登壇してきました。
今回の会場は、別の部屋でニコニコNTもやっていたので、ちょっとびっくりしました。

内容ですが、今回はIT技術の話からちょっとだけ離れて、健康の話をしてきました。たまにはこういう話も刺激になっていいんじゃないかな~的な感じです。こういう方面も、ちょっと掘り下げて調べると、色々なことが分かって面白いです。

今回のセッションはLTだったのを無理やりショートセッションぐらい使ってしまった(ごめんなさいごめんなさい)のですが、まだまだ盛り込んでいない要素があります。希望が多ければ、どこかで完全版をやろうかなと思っています。


オリジナルスライドはこちら: 不健康なIT戦士を健康的にするアレの話.pptx

Final LINQ Extensions III – 第四回 Center CLR 勉強会

finallinqextensions3第四回Center CLR勉強会で、Final LINQ Extensions IIIのタイトルで登壇してきました。

ご静聴ありがとうございます。
式木と並列化の話をしました。ようやく、ここに完結です!!
初回から参加していない方は、是非初回スライドから見て下さい。入門的な内容から網羅しています。

Final LINQ Extensions – 第二回Center CLR勉強会
Final LINQ Extensions II – 第三回Center CLR勉強会


オリジナルスライドはこちら: Final LINQ Extensions III.pptx
GitHub: CenterCLR.CustomLINQProviderDemo


11875075_746804792098116_7791419725665778286_o今回のCenter CLRは参加人数も20名を超え、何だか大所帯を感じました。ディスカッションをメインにしているので、あんまり多いとアレなんですが、それはそれとして嬉しい限り。

がりっちさんのUWPの細部の話・可知さんのVB14の知らなかった機能・松岡さんの.NET MicroFrameworkとWindows 10 IoTの選択の話・そして、平内さんの


女子高生とLINEでお友達になるにはとんでもなく敷居が高い件

など、主催しておきながら凄く面白いネタで楽しかったです。

また次回もよろしくお願いします。

SourceTreeで始めよう! Gitへの乗り換え指南 – Atlassian User Group NAGOYA 第3回 ユーザーミーティング

「SourceTreeで始めよう! Gitへの乗り換え指南」というタイトルで、AUG名古屋 第3回 ユーザーミーティングで登壇させて頂きました!

augn1Atlassianの製品では、JIRAを少しだけ、後はSourceTreeをどっぷりと使う、という経験値なのですが、以前登壇したGitの解説を元に、SourceTreeやBitbucketに絡めて話をさせて頂きました。

スライドの分量は多めなので、適当に端折って進行しました。
途中、解説が甘い部分にマサカリが (;´Д`) すいません、精進します。

あと、最終版がありましたので、ここで公開するスライドは最終版で公開しておきます。

ご静聴ありがとうございました。

オリジナルスライド:SourceTreeで始めよう! Gitへの乗り換え指南.pptx

augn2あと、登壇特典貰いました!Atlassianロゴ入りBluetoothスピーカーですよ!! やっほい!

会場は美味しい食べ物と酒が呑める(私は食べる専門ですが)場所で、あんなお店でプロジェクター完備だとは思いませんでした。Atlassian ExpertsのHSDさんの協賛で、会費もお値打ちでした。いつかうちでもやってみたいですね。

スライド中でも紹介しましたが、Windows系のSourceTree向けに、マージツールとして「TortoiseMerge」のポータブル版を作ったので、公開してあります。

GitHub: TortoiseMerge Portable

tortoisemergeportableSourceTreeは、サードパーティ製のマージツールをいくつかサポートしていて、TortoiseMergeも対応しています。しかし、公式パッケージはTortoiseSVN内に含まれた形で配布されており、TortoiseMergeだけを使いたい場合でも、TortoiseSVNをインストールしなければならず、インストールするとエクスプローラーがとてもアレになってしまうのが嫌だ… という向きに最適です。

ポータブルと銘打っていますが、純粋なポータブルとしてのバイナリファイルを固めただけの物と、MSIによるインストーラー版があります。インストーラー版は32ビット版しか用意していませんが、ぶっちゃけ、64ビット版が必要になるとも思えないので、64ビット版を作る気は多分起きないです(勝手にフォークして作ってください)。

注意点として、既にTortoiseSVNを使っている環境には入れないで下さい(64ビット版TortoiseSVN環境に、32ビットのインストーラー版を入れるとおかしくなるかも知れません)。

それではまた。

.NET非同期処理(async-await)と例外の制御

Taskクラスとasync-awaitを使用して非同期処理を記述する場合の、例外にまつわるあれこれをまとめました。

概要:

  • 表面上は殆ど変らない
  • 現実の動作
  • タスクコンテキストとスレッドコンテキスト
  • スタックウォーク
  • 処理されない例外

この記事は、非同期処理と例外処理について、多少難易度の高い話題を含みます。もし、もっと基本的な記述方法や、安全に例外を処理する方法を知りたい場合は、この記事をお勧めします: 「.NET非同期処理で例外を安全に対処する」

この記事の前に、非同期処理の基本を扱った記事もあります: 「.NET非同期処理(async-await)を制御する、様々な方法」


非同期処理中に発生する例外の捕捉

非同期処理中に発生する例外を捕捉する方法は、一般的な例外の捕捉とほとんど変わりません。

// 指定されたURLからHTMLコンテンツをダウンロードする
public static async Task<XElement> StartFetchContentAsync(Uri url)
{
	using (var httpClient = new HttpClient())
	{
		try
		{
			// HTTP GETでURLに非同期的に要求する
			using (var stream = await httpClient.GetStreamAsync(url))
			{
				// (パース)
				return SgmlReader.ParseXElement(stream);
			}
		}
		// 非同期処理中(又は同期処理中)に発生した例外を捕捉
		catch (Exception ex)
		{
			// 何かエラー処理
			logger_.WriteLog(ex.Message);
			// 再スロー
			throw;
		}
	}
}

上記のコードにより:

  • GetStreamAsync非同期メソッドの完了待機中(await)に発生した例外の捕捉
  • それ以外の例外(例:SgmlReaderのパース)の捕捉

の両方とも、catch構文で例外を捕捉出来ます。
上記の例では示しませんが、C#5.0では、catchブロック中(又はfinally)で更に非同期メソッドを呼び出して待機(await)することは出来ません。C#6.0では可能です。

Task.WhenAllを使って非同期処理を集約している場合、一度に複数の例外が発生するかもしれないため、発生した例外は「AggregateException」クラスに内包されてスローされます。

// 指定されたURL群からHTMLコンテンツをダウンロードする
public static async Task<XElement[]> StartFetchContentsAsync(IEnumerable<Uri> urls)
{
	try
	{
		return await Task.WhenAll(
			urls.Select(async url =>
			{
				using (var httpClient = new HttpClient())
				{
					// HTTP GETでURLに非同期的に要求する
					using (var stream = await httpClient.GetStreamAsync(url))
					{
						// (パース)
						return SgmlReader.ParseXElement(stream);
					}
				}
			}));
	}
	// Task.WhenAllで発生した例外を捕捉
	catch (AggregateException ex)
	{
		// 何かエラー処理
		ex.InnerExceptions.ForEach(iex => logger_.WriteLog(iex.Message));
		// 再スロー
		throw;
	}
}

非同期処理の遷移

上記のように、非同期メソッドを呼び出して例外を処理するコードを書いても、表面上は同期メソッド呼び出しと殆ど変りません。また、実際に考慮すべき事も殆ど変りません。しかし、内部で発生している例外がどのように処理されているのかという点では、かなり異なります。

以下の図を見て下さい:

(なお、まるでこれがHttpClientの実装であるかのように書いていますが、実際のHttpClientは別の手法で実現している可能性があります)

asyncexception11これは、非同期メソッドではなく、同期メソッドでの例外のフローです(疑似的に、HttpClientに「GetStream」という同期メソッドがあると仮定して書いています。実際にはありません)。

GetStreamを呼び出したものの、指定されたURLに接続出来なかったり、HTTPサーバーがエラーを返すなどした場合、メソッド内で例外がスローされます。それはそのまま呼び出し元のcatchブロックでキャッチされます。当たり前ですが、この時の呼び出し元スレッドの実行パスは、赤い矢印で図示した通りの経路をたどります。

では、非同期メソッドを呼び出した場合はどうなるでしょうか?

asyncexception21まず、GetStreamAsyncは非同期メソッドなので、異なる実行コンテキスト上で接続処理が行われます。「実行コンテキスト」とは、今はまだ「ワーカースレッド」と言うように読み替えて構いません。そして、実行コンテキストが異なるという事は、呼び出し元スレッドの実行パスは、接続処理を行っているコンテキストとは別に、平行して処理が可能である事を意味します。

この例では、呼び出し元のスレッドがGetStreamAsyncから抜けた(Task<Stream>の戻り値を得る)際に、すぐに「await」を実行 – つまり非同期的に待機します。その間、まだ接続処理は別のコンテキスト(ワーカースレッド)で実行中です。ここで、接続に失敗するなどして例外が発生したとします。

asyncexception3先ほど、実行コンテキストはワーカースレッドと読み替えて良いという話をしました。そのため、接続処理で発生した例外は、「そのスレッドコンテキスト内のcatchブロック」で捕捉されます。図ではそれを疑似的に示しましたが、これでは、ワーカースレッド上では例外を捕捉出来ても、呼び出し元では捕捉出来ない事になります。

従来のThreadクラスによるワーカースレッド処理では、ワーカースレッドで発生した例外を、どうやって元のスレッドに通知するのかという事も、設計者が考慮する必要がありました。しかし、Taskベースの非同期処理では、例外を通知するのはTaskクラスの役割となっていて、一貫した操作が可能となっています。

asyncexception4「.NET非同期処理(async-await)を制御する、様々な方法」でも書きましたが、能動的な方法で通知するのであれば、「TaskCompletionSourceクラス」を使えば、SetExceptionメソッドを使用して、呼び出し元のスレッドに通知する事が出来ます。

BookJournal余談ですが、仮に接続処理が「Task.Runメソッド」によって実行されていた場合、スローされた例外は自動的にRunメソッドによって捕捉され、呼び出し元のスレッドに通知されます。


実行コンテキストとは

非同期メソッド内の非同期的な処理を行う主体を、「ワーカースレッド」ではなく「実行コンテキスト」という抽象的な呼び方をしました。上記の説明の通り、非同期的な処理が実際にワーカースレッドで実現されているのか、又はそれ以外のなんらかの方法で実現されているのかは、メソッドの呼び出し元からは知る由もなく、知る必要のない、実装の詳細です(現実には把握したい場合もあるかもしれませんが)。

asyncexception5ワーカースレッドでなければ、どうやって非同期処理を実現するのでしょうか? 一例を挙げるなら、Win32 APIでは一般的な「コールバック」です。Win32では伝統的に、処理の完了や失敗をコールバックで通知する事が多いのです。これは、Windowsのカーネルモード内部奥深く、デバイスドライバーが発する完了のタイミングの処理から、鮭が川を上ってくるように「逆方向」にメソッドが呼び出され、手元のコールバックハンドラメソッドが呼び出される、というように通知されます(「Async訪ねて3000里」を参照)。

ここで、TaskCompletionSourceを使い、SetResultやSetExceptionを呼び出すことで、非同期処理の完了を通知します。

従って、非同期処理とは、必ずしも特定のスレッドに紐づいた処理では無い事を意味します。そのため、「実行コンテキスト」という呼び方をしました。更に、呼び出し元のスレッドにとっては、非同期メソッドである事の証は「Taskクラス」だけです。GetStreamAsyncメソッドの戻り値は「Task<Stream>」で、このインスタンスを「await」する事によって待機します。ここでも、スレッド基準ではなくタスク基準です。

タスク基準の非同期処理の管理を行う場合に、タスクを認識して処理を行う主体に名前が欲しいと思う事があるため、特に「タスクコンテキスト」と呼んでいます(公式ではありません)。

さて、ここまでで、「非同期メソッド内の処理の主体」が、必ずしもスレッドに紐づかないという展開をしましたが、これがそっくりそのまま、呼び出し元にも当てはまります。何故なら、呼び出し元は「await」した時点でスレッドによるハードブロックではなく、疑似的に非同期処理を待機しているかのように見せているからです。

asyncexception6スレッドをブロックしない、という事は、この図のように、awaitの直前までのスレッドコンテキストは解放されることを意味します。そのスレッドがどこに行ってしまうのか?はともかくとして、ここでも実行コンテキストはスレッド基準ではなくタスク基準である事が分かります。

GetStreamAsyncがSetExceptionで例外を通知する時、awaitしているタスクコンテキストは起こされ、catchブロックの処理を継続します。処理を継続するにはコードを実行しなければならないため、物理的なスレッドコンテキストが必要です。それは図のように「メインスレッド」なのでしょうか? 元のメインスレッドはawait時にどこかに行ってしまったかも知れませんね。新たに割り当てられたワーカースレッドが、代わりに処理を継続するかもしれません。処理を継続するスレッドが何になるのかは、「SynchronizationContextクラス」がカギを握っていて、WPFやWindows Formsでは結局メインスレッドが再び処理を継続します。

SynchronizationContextクラスの動作に踏み込むと本題と外れてしまうため、ここでは呼び出し元の処理もまた「非同期メソッド」となり、実行コンテキストはスレッド基準ではなくタスク基準となる、という事が分かれば良いと思います。


タスクベース処理のデバッグ

参考までに、タスク基準でのデバッグ手法について触れておきます。

今まで、スレッドベースでのプログラミングに慣れてきたため、デバッグ時にはどのメソッドがどのメソッドを呼び出して来て今の状態に至るのかを「呼び出し履歴(スタックトレース)」を観察するのが基本でした。しかし、タスクベースの非同期処理をデバッグする場合は、スタックトレースを眺めても何も得られない可能性があります。タスクコンテキストはスレッドに紐づく場合もあればそうでない場合もあり、更にたまたま割り当てられたワーカースレッドによって実行コンテキストが得られている場合もあります。

そのため、Visual Studioのデバッガには「タスク」ウインドウが追加されました。asyncexception7

このウインドウで、待機中のタスクコンテキストの状態と、待機が発生した位置を確認する事が出来ます。残念ながら、その時点の待機に至った履歴(疑似的な、非同期待機のスタックトレースのような)は見る事が出来ません。そのため、各タスク間にどのような依存性があるのかないのかは、このウインドウを見てもはっきりとは分かりません。将来的に改善されると良いなと思います。


例外のスタックトレース(古代CLR)

C# 1.0がリリースされた時、言語仕様がJavaに似ている事から様々な点が比較されました。例外についてもかなり似通っているのですが、決定的に異なる点が少なくとも一つ存在します。それは、スタックトレースの扱いです。C#でやってはいけないと言われている事の一つに、例外の再スローの方法があります。

public static void RethrowExceptionSample()
{
	try
	{
		// 何らかの処理
		throw new Exception();
	}
	catch (Exception ex)
	{
		// 例外の処理
		logger_.WriteLog(ex);

		// 再スロー(ここからスタックトレースが再構築されてしまう)
		throw ex;
	}
}

Javaからの移行組の人はついついやってしまいますが、C#ではこのコードはエラーになりませんがやってはいけません。何故なら、「throw ex」すると、ex内に記録されているスタックトレースの情報が失われ(リセット)、新たにスタックトレースが再構築されるからです。正しい再スローは以下の通りです。

public static void RethrowExceptionSample()
{
	try
	{
		// 何らかの処理
		throw new Exception();
	}
	catch (Exception ex)
	{
		// 例外の処理
		logger_.WriteLog(ex);

		// 再スロー(スタックトレースは維持される)
		throw;
	}
}

throw句に何も指定しなければ、(例外処理ブロック中であれば)その例外が再スローされます。これは、IL命令でのrethrowに変換されます。つまり、特別な処理としてCLRに認識されているのです。

この事は、前述の「Threadクラスで生成したワーカースレッド内で発生した例外を、元のスレッドにどうやって通知するか」と言う事に大きな制約を生じさせます。非同期処理の外側では、内部実装でワーカースレッドを使っているかどうかは関係なく、関心の無い事です。従って、ワーカースレッドを使っていたとしても、メソッド内で発生した例外は、あたかも連続したスレッドコンテキストで発生したかのように、スタックトレースが観察されて欲しい、と思う筈です。

例えば、以下のようなコードで、ワーカースレッドで発生した例外を別のスレッドで再スローしたらどうでしょうか?

public static void RethrowAnotherThreadBoundExceptionSample()
{
	// ワーカースレッドで発生した例外を保持する変数
	Exception caughtException= null;

	// ワーカースレッドの生成
	var thread = new Thread(() =>
		{
			try
			{
				// 何らかの処理
				throw new Exception();
			}
			catch (Exception ex)
			{
				// 例外を記録
				caughtException = ex;
			}
		});

	// ワーカースレッドの実行と完了の待機
	thread.Start();
	thread.Join();

	// ワーカースレッドで例外が発生していたら
	if (caughtException != null)
	{
		// error CS0156: 引数なしの throw ステートメントは catch 句以外では使えません。
		throw;
		// スタックトレースを失う
		// throw caughtException;
	}
}

このコードはコンパイル出来ません。何故なら、再スロー構文「throw」は、catchブロック内でのみ使用出来るからです。かといって、「throw caughtException」と書いてしまうと、スタックトレースを失ってしまいます。

ファッ○ン CLR!!

と思いましたか? 私は思いましたよ、.NET Remotingを知るまでは。

asyncexception17CLR設計者に聞いたわけではありませんが、この仕様は恐らく意図的なものです。.NETは最初のバージョンから「アプリケーションドメイン」という考え方を導入しています。これは、「マイクロプロセス」と呼べるもので、同一のプロセス内に、サンドボックス的な分離構造を持たせる事が出来る機能です。同一プロセスであっても、異なるアプリケーションドメイン間の通信には大きな制約が生じます。これにより、アプリケーションドメイン間の安全性を高め、かつ、プロセス起動・終了による非常に大きなコストを排除するのが狙いです。

.NET Remotingは、このアプリケーションドメイン間の通信や、プロセスワイド、又はマシンワイド間での「リモート参照」機能を提供して、通信の実装負担を軽減します。リモート参照は一見してクラスやインターフェイスそのものに見えます。そこにはメソッドが定義されていて、メソッドを呼び出すとリモートのメソッドが呼び出されるという、非常に透過的で便利な機構です。


BookJournal補足:現代においては、CLRのリモート参照機能はややレガシーとして扱われています。一般的にXMLやJSONによるHTTP REST APIが良く使われていますが、そこに皮を被せる形でリモート参照を使うケースはまだまだあるでしょう。また、アプリケーションドメインとは切っても切り離せない関係にあるため、当分の間、CLRリモート参照の機能が廃止されることはないと思われます。


この「リモートメソッドの呼び出し」は、当然、引数や戻り値もハンドリングします。引数や戻り値をリモートメソッド間でやり取りするためには、インスタンスのシリアル化と逆シリアル化という非常に大きなトピックが含まれます。更に、リモートメソッドで発生した例外は、そのまま呼び出し元に例外として通知されます。

例外の通知を実現するためには、例外もまたシリアル化・逆シリアル化可能でなければなりません。シリアル化可能であるためには、例外クラスに含まれる情報が全てシリアル化可能である必要があります。そこには、例外のメッセージ文字列のような単純なものもありますが、「スタックトレース」も含まれているのです。

スタックトレース情報は文字列ではありません(ToStringする事により文字列化されます)。スタックトレースは「StackTraceクラス」や「StackFrameクラス」を使って、実行時に動的にトラバースする事が出来ます。仮に、シリアル化が完全に機能するためには、これらのクラスの中身もシリアル化されなければなりません。StackFrameクラスは、リフレクションのMethodInfoクラスを保持しています。MethodInfoはこれが定義されているTypeクラスを参照し、Typeはそれが定義されたModuleやAssemblyクラスを参照しているでしょう。完全にシリアル化するためには、それらの型情報が保持された全てのDLLが特定されなければならず、逆シリアル化の際には呼び出し元でもすべての参照が解決されなければなりません。

これは明らかに大げさすぎます。リモートメソッド呼び出し側は単に例外の種類(例外クラス)と、メッセージぐらいが判別できれば良いのかもしれません。あるいは例外がスローされることによって、リモート側のスタックの全貌が観察出来てしまうのは、セキュリティリスクです。

このような理由かどうかは分かりませんが、CLRが再スロー可能なタイミングを強制しているのは:

  • 「いつでもスタックトレースが維持される」という状況は望ましくない場合がある。
  • そうであるなら再スローは限られた状況下でのみ機能するようにデザインすれば良い。

と判断したように見えます。

さて、このようなCLRのデザインの為、残念ながらExceptionクラスは、スタックトレースを維持したまま、別のスレッドで再スローする事が出来ない事になります。.NETのスタンダードなソリューションとして、「TargetInvocationException」というクラスが、元の例外のインスタンスをInnerExceptionに保持してスローする事になっています。こうすれば、元の例外のスタックトレースを失うことなく、新たなスタックトレースを構築できます。但し、元の例外クラスを指定してcatch出来ないため、「何このうっとおしい例外は!」と思うかもしれません。


BookJournal補足:TargetInvocationException.InnerExceptionに保持したからと言って、スタックトレースの物理的な情報をシリアル化するわけではありません。アプリケーションドメインを超える時、.NET Remotingのインフラが、スタックトレースを文字列のような安全なデータに固定化します。そのため、リモート例外を受信したスレッドは、リモートのスタックトレースの「動的な」トラバースを行う事は出来ません。また、セキュリティ要件として、この機能を構成で無効化する事も出来ます。つまり、例外は捕捉可能でも、スタックトレースは全く参照出来ないようにする事も出来ます(むしろ、プロセス間以上のRemotingは、デフォルトが逆だったかもしれません。忘れてしまいました)。


例外のスタックトレース(CLR 4.0・4.5以降)

昔の話はこれぐらいにしておきましょう。CLR 4.0にてTaskクラスが導入され、タスクベースの非同期処理が可能になりました。しかし、これまで述べてきたスタックトレースの問題は依然として残っています。従って、awaitで待機中のタスクコンテキストに例外を通知する場合でも、迂闊にそのまま通知することは出来ない事になります。

// CLR 4.0以降
public static Task RethrowAnotherThreadBoundExceptionSampleAsync()
{
	// タスクコンテキストの拠り所を生成
	var tcs = new TaskCompletionSource<object>();

	// ワーカースレッドの生成
	var thread = new Thread(() =>
	{
		try
		{
			// 何らかの処理
			throw new Exception();
		}
		catch (Exception ex)
		{
			// 例外を通知
			tcs.SetException(ex);
		}
	});

	// ワーカースレッドの実行
	thread.Start();

	// Taskを返す
	return tcs.Task;
}

static void Main(string[] args)
{
	try
	{
		// タスクの完了をハードウェイトする
		RethrowAnotherThreadBoundExceptionSampleAsync().Wait();
	}
	catch (Exception ex)
	{
		Debug.Assert(ex.GetType() == typeof(AggregateException));
		Console.WriteLine(ex.ToString());
	}
}

asyncexception9もはや忘れているかもしれません(自分でも書いてて思い出した)、async-awaitはCLR 4.5からのサポートです。従って、上記のようなコードを書いても、await時にどうなるかは分かり難いですね。Task.Waitメソッドを呼び出してハードウェイトした場合、発生した例外は「AggregateExceptionクラス」という、新たなクラスに保持されてスローされます。使われ方はTargetInvocationExceptionと同じですが、AggregateExceptionは複数の例外を内包出来るところが異なります。つまるところ、それまでのスタックトレースは維持されるものの、結局スタックトレースは結合されず分断される、と言う訳です。

スクリーンショットは、処理中に「throw new Exception()」を発行した様子です。先頭はcatchしたAggregateExceptionで、下の方に辿っていくと「(内部例外 #0)」という行が見つかります。この行が、AggregateExceptionが内包しているExceptionクラスの例外のスタックトレースです。シームレスに繋がっているように見えるのは、ToStringがうまく文字列フォーマットしているだけで、本質的にスタックトレースが結合している訳ではありません。

これが、CLR 4.5以上でasync-awaitを使い、非同期的に待機した結果、どうなるかと言うと:

// 非同期待機(await)出来るようにするため、非同期メソッドを定義
public static async Task CallerAsync()
{
	try
	{
		// 非同期メソッドを呼び出して非同期的に待機
		await RethrowAnotherThreadBoundExceptionSampleAsync();
	}
	catch (Exception ex)
	{
		Debug.Assert(ex.GetType() == typeof(Exception));
		Console.WriteLine(ex.ToString());
	}
}

static void Main(string[] args)
{
	CallerAsync().Wait();
}

asyncexception8スクリーンショットを見て分かりますか? AggregateExceptionがありません。catch句はまるでExceptionクラスの例外を直接受信しているかのようです。実際に、catch句のexは、Exceptionクラスのインスタンスです。これでは、まるでスタックトレースがシームレスに結合しているようではありませんか!!

重要な事です:awaitで非同期待機中に受信した例外は、スタックトレースが結合され、同期メソッドの例外と同じように振る舞います。

今までは不可能な事だったので、これには仕掛けがあります。CLR 4.5で新たに追加された「ExceptionDispatcherInfoクラス」を使います。一般の開発者がこのクラスを直接操作するのは、あまり良い事ではありません。フレームワーク設計者はこのクラスを使って、スタックトレースを厳密に操作したくなるかもしれませんね。

もし、await待機中に受信した例外が全てAggregateExceptionにラップされてしまうと、async-awaitを使用したプログラミングはかなり非効率的になってしまいます。すべての例外はAggregateExceptionをキャッチし、改めて内包例外を判定する必要があります。これでは、細部を知らない開発者は「なんでこんな変な事になっているんだ」と文句を垂れる事になるでしょう。

しかし、スタックトレースが結合され、AggregateExceptionを使わない事で、今まで掘り下げてきたことは何も知らなくても、とりあえずasync-await構文を使って普通にコードが書ける、という、最初の説明に繋がるわけです。

CLRの世代を経て、結局スタックトレースが維持可能になったのは少し残念ですね。これではTargetInvocationExceptionやAggregateExceptionは何だったのかと思えてきます。しかも、いまさら無理な話ですが、Task.Waitの挙動もExceptionDispatcherInfoを使えばスタックトレースを結合出来てウハウハではありませんか (;´Д`)

しかし、再度この問題をひっくり返すネタが、次の本題です。


放置された非同期処理の行方

さて、ようやく本題です。この記事を書こうと思った要因となるネタですが、今までの説明はこの話の導入に必要でした。以下のように、2つのI/O操作を効率よく実行するため、それぞれを非同期メソッド呼び出しで開始させ、その後awaitで完了を待ちます。

public async Task ExecuteMultipleIOAsync(Stream stream1, byte[] data1, Stream stream2, byte[] data2)
{
	try
	{
		// 非同期処理を開始
		var task1 = stream1.WriteAsync(data1, 0, data1.Length);
		var task2 = stream2.WriteAsync(data2, 0, data2.Length);

		// それぞれの完了を待機
		await task1;
		await task2;
	}
	catch (Exception ex)
	{
		Console.WriteLine(ex.ToString());
	}
}

このような実装はデザイン的に避ける必要があります。とりあえず、一例を図示してみましょう。

asyncexception10メソッド内部で呼び出した非同期メソッドは、それぞれtask1, task2のバーで示しました。これらの非同期メソッドの内部で何が行われているのかは不明です。ワーカースレッドを使っているのかもしれませんし、ネイティブなWin32 APIによって駆動されているのかもしれません。単にそれらを緑枠で「タスクコンテキスト」として扱っています。

二つのサブタスクはほぼ同時に開始されます。メインタスクとは関係なく非同期で動作するため、サブタスクのどちらが先に終了するのかは、良く分かりません。図中では、task1→task2の順で完了した場合を示しています。

仮に逆だった場合:

asyncexception13メインタスクは「await task1」として、task1の完了を先に待機しているため、通知もtask1→task2の順になります。つまり、非同期処理の順序がどのように処理されたとしても、両方の完了が正しく処理されるのです。

本当でしょうか?

asyncexception14この例は、task1は処理が成功し、task2は失敗して例外をスローするパターンです。「await task1」は完了し、「await task2」にて例外が再スローされ、catchブロックでキャッチされます。何も問題なさそうですね?

逆のパターンはどうでしょうか:

asyncexception15task1の処理が失敗して例外がスローされた場合、メインタスクは例外を受け取ってcatchブロックで処理します。すると、非同期で動作中のtask2の結果(正常終了)を受け取る機会が失われます。何故ならもう「await task2」という文は実行されないからです。

正常終了の通知を受け取らなかった場合、その通知はどうなってしまうのか?

とても重要:どうにもなりません

この通知は、メインタスクだけでなく、あらゆるタスクが受け取る機会を失います。従って、この結果通知は「無視」されます。このコード例では、WriteAsyncは結果を返しません(非ジェネリックなTaskを返します)。そのため、task2が成功しようが失敗しようが、結果を確認できなかったとしても、恐らく問題にはなりません。メインタスクの「関心」は、task1が例外をスローした時点で例外処理に遷移しているのです。task2がどうなろうが、知ったことではない… と解釈されます。

さあ、問題の核心です:

asyncexception16もう、何となくわかったかもしれません。この図では、task1もtask2も処理に失敗して例外を通知しようとします。task1はawaitしているので、その時点で例外が通知され、メインタスクはcatch句に遷移し、例外が処理されます。task2の例外は通知しようにも、受け取るタスクが無いのです。前の例と同じように、「await task2」は実行されないので、

きわめて重要:task2の例外(あるいは成功)は、メインタスクで処理「されません」

最初に解説した通り、async-awaitの構文は、殆ど同期メソッドを使用したコードと同じように、非同期メソッドの実装を可能にします。しかし、本質的には全く動作が異なるのです。この問題は、同期メソッドの実装の感覚で理解していると、ハマってかつ理解不能、というパターンかも知れません。

task1とtask2を順番にawaitするというコードは、意図して書いたのであれば問題ありません。意図してという事は、「task2がどうなろうと知ったことではないが、task1が正常に終了するときには面倒を見たい」のような場合です。こういう要求は、現実には殆ど無いと言って良いと思います。従って、このようなコードを書かないように注意する必要があります。


この問題に対処する方法ですが、まず、成功はともかく、例外が完全に無視されてしまうのは問題と思われます。「無視されず、例外が発生すればいいんでね?」と思うかもしれません。その通りではあるのですが、受け取るタスクが存在しないのです。実は例外を発生させる方法はあります。それは、App.configに次のような指定を入れる事です。

<configuration> 
	<runtime> 
		<ThrowUnobservedTaskExceptions enabled="true"/> 
	</runtime> 
</configuration>

この指定は、CLR 4.5移行で有効です。CLR 4ではこの指定に関わらず、「UnobservedTaskException例外」がスローされます(CLR 4ではasync-awaitが使えないので、これはあまり問題にならないかもしれません)。しかし、例外をスローしても、受け取るタスクが居ないんでしたよね。誰がどこからスローするんだ?という話になるのですが、実は「TaskExceptionHolder」という内部クラスが、「ファイナライザースレッドコンテキスト」でスローします。

ファイナライザースレッドなので、これが分かったとしても何も出来ません。何かしても、もう手遅れで、この後プロセスは死亡します。そもそもこの例外を受け取れる可能性があるのは、「AppDomain.UnhandledExceptionイベント」しか無い、という事になります。

そのようなわけで、もう少し使いやすいイベントハンドラがあります。「TaskScheduler.UnobservedTaskExceptionイベント」です。このイベントをフックすれば、発生した非同期例外と、その例外を「処理済み」としてマークするかどうかの選択肢が得られます。例えば、ログに記録してから無視する、等の方法が取れます。

起きてしまった非同期例外のフォローはこのような対策ですが、そもそも問題を起こさないようにするには、どうすれば良いでしょうか?

public async Task ExecuteMultipleIOAsync(Stream stream1, byte[] data1, Stream stream2, byte[] data2)
{
	// 非同期処理を開始
	var task1 = stream1.WriteAsync(data1, 0, data1.Length);
	var task2 = stream2.WriteAsync(data2, 0, data2.Length);

	try
	{
		// task1の完了を待機
		await task1;
	}
	catch (Exception ex)
	{
		Console.WriteLine(ex.ToString());
	}

	try
	{
		// task2の完了を待機
		await task2;
	}
	catch (Exception ex)
	{
		Console.WriteLine(ex.ToString());
	}
}

しかし、これは面倒です。awaitの度に、例外が発生したか否かをいちいちcatchで確認しなければなりません。根本的には、Taskインスタンスを持ってまわるようなコードを書いた時点で、不測の事態が起きうることを覚悟する必要があります。どうしてもこのようなコードを書かなければならないのか、は考える必要があります。

もっと良い例を挙げましょう:

public async Task ExecuteMultipleIOAsync(Stream stream1, byte[] data1, Stream stream2, byte[] data2)
{
	// 非同期処理を開始
	var task1 = stream1.WriteAsync(data1, 0, data1.Length);
	var task2 = stream2.WriteAsync(data2, 0, data2.Length);

	try
	{
		// task1とtask2のの完了を同時に待機
		await Task.WhenAll(task1, task2);
	}
	catch (Exception ex)
	{
		// (非同期例外であれば、AggregateExceptionに内包されている)
		Console.WriteLine(ex.ToString());
	}
}

原点回帰、というか、Task.WhenAllを使って複数のタスクを同時に待機します。今回の例はtask1とtask2が完了するのを待つ、というのが主目的なので、WhenAllを使えば目的は達成します。しかも、片方又は両方で例外が(どのような順序で)発生した場合でも、確実に例外を補足できます。

そして、ここで、AggregateExceptionの必要性が生まれます。前節でスタックトレースの結合が出来た事で、AggregateExceptionのような例外内包クラスは不要ではないかという例を示しましたが、非同期で発生する複数の例外を安全に呼び出し元のタスクに通知するために、この例外が必要になるのです。


長くなりましたが、非同期例外にまつわる技術的な背景を書いてみました。より安全な非同期処理が書けると良いですね。