ILのキホン – 第五回Center CLR年末会

WP_20151226_14_13_25_Pro_LI「ILのキホン」と言う題目で、Center CLR年末会で登壇してきました。

Center CLRも第五回で、毎回参加の方と新しく参加の方で程よく盛り上がってうれしい感じです。ChalkTalkもそうですが、遠方からわざわざ参加して頂いた方もありがとうございます。来年も継続して行きたいと思います。

さて、今回のネタは結構前から決めていたものの、年末の忙しさの為に十分な時間が取れず、直前に方針を決めてバタバタしてしまいました。猛省…

と言う事だけではないんですが、ちょっとセッションの進行を変えてみました。


Intermediate Languageのキホン

ILの事については、第一回のChalkTalk CLRで「ChalkTalk CLR – 動的コード生成技術(式木・IL等)」と題してディスカッションを行ったのですが、今回は本会でその展開をしようと思ったのです。

briefingしかし、ただILを説明したのでは、実際にILを使う具体的なシチュエーションでも提起できない限り、右から左へ流れていってしまうだけだと考えて、ハンズオンのような形式で進行してみました。

Emitが可能なコードを一から書いていると、なかなか本題にたどり着けないため、GitHubに元ネタとなるコードを用意しておき、Emitコードから書き始めれられるようにしました。

/// <summary>
/// コードをILで動的に生成するヘルパークラスです。
/// </summary>
internal sealed class Emitter : IDisposable
{
	private readonly AssemblyBuilder assemblyBuilder_;
	private readonly ModuleBuilder moduleBuilder_;

	/// <summary>
	/// コンストラクタです。
	/// </summary>
	/// <param name="name">アセンブリ名</param>
	public Emitter(string name)
	{
		var assemblyName = new AssemblyName(name);
#if NET45
		assemblyBuilder_ = AssemblyBuilder.DefineDynamicAssembly(
			assemblyName,
			AssemblyBuilderAccess.RunAndSave);
#else
		assemblyBuilder_ = AssemblyBuilder.DefineDynamicAssembly(
			assemblyName,
			AssemblyBuilderAccess.Run);
#endif
		moduleBuilder_ = assemblyBuilder_.DefineDynamicModule(name + ".dll");
	}

	/// <summary>
	/// Disposeメソッドです。
	/// </summary>
	public void Dispose()
	{
#if NET45
		// デバッグ用に出力
		assemblyBuilder_.Save(moduleBuilder_.ScopeName);
#endif
	}

	/// <summary>
	/// 引数と戻り値を指定可能なメソッドを定義します。
	/// </summary>
	/// <typeparam name="TArgument">引数の型</typeparam>
	/// <typeparam name="TReturn">戻り値の型</typeparam>
	/// <param name="typeName">クラス名</param>
	/// <param name="methodName">メソッド名</param>
	/// <param name="emitter">Emitを実行するデリゲート</param>
	/// <returns>デリゲート</returns>
	public Func<TArgument, TReturn> EmitMethod<TArgument, TReturn>(
		string typeName,
		string methodName,
		Action<ILGenerator> emitter)
	{
		// クラス定義
		var typeAttribute = TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.Abstract;
		var typeBuilder = moduleBuilder_.DefineType(
			typeName,
			typeAttribute,
			typeof(object));

		// メソッド定義
		var methodAttribute = MethodAttributes.Public | MethodAttributes.Static;
		var methodBuilder = typeBuilder.DefineMethod(
			methodName,
			methodAttribute,
			typeof(TReturn),
			new [] { typeof(TArgument) });

		// ILGenerator
		var ilGenerator = methodBuilder.GetILGenerator();

		emitter(ilGenerator);

		// 定義を完了して型を得る
		var typeInfo = typeBuilder.CreateTypeInfo();
		var type = typeInfo.AsType();

		// メソッドを取得する
		var bindingFlags = BindingFlags.Public | BindingFlags.Static;
		var method = type.GetMethod(methodName, bindingFlags);

		// デリゲートを生成する
		return (Func<TArgument, TReturn>)method.CreateDelegate(
			typeof(Func<TArgument, TReturn>));
	}
}

public static class Program
{
	public static void Main(string[] args)
	{
		// 動的アセンブリを生成
		using (var emitter = new Emitter("TestAssembly"))
		{
			// メソッドを動的に生成
			var func = emitter.EmitMethod<int, string>(
				"TestNamespace.TestType",
				"TestMethod",
				ilGenerator =>
				{
					ilGenerator.Emit(OpCodes.Ldstr, "Hello IL coder!");
					ilGenerator.Emit(OpCodes.Ret);
				});

			// 実行
			var result = func(123);

			// 結果
			Console.WriteLine(result);
		}
	}
}

Emitterクラスは、System.Reflection.Emitの煩雑なコードを隠ぺいするために定義しています。課題の多くはMainメソッド内のラムダブロックのEmitメソッドをいじります。

# なお、このコードはnetcore5向けでも実行可能にしてあります。

課題はPart1~Part10まで用意し、はじめに少しだけ「スタックマシン」についての解説を行いました。


Part 1: 引数の値をToStringして返すようにする

part1GitHubのコードを軽く説明して、Emit本体のコードを課題に従って書き換えてもらいました。

テンプレートのコードは、”Hello IL coder!”と言う文字列を返すだけのものですが、引数で与えられたintの値を文字列に変換して返すのが課題です。

最初なので、こんなものだろうと思っていたのですが… 実はこれ、大変な罠が…

やるべきことは、スライド通り大きく3つあります。

引数の値はどうやって入手するか

ヒントに書いておいたのですが、「OpCodesクラス」を見ると、IL命令の一覧が確認できます。

そして、スタックに値を積む(プッシュ)するのは、「Ld~」で始まる命令であることも説明しました。OpCodesの一覧を見ていると、「Ldarg_0」命令が見つかります。これを使うと、引数の最初の値をスタックに積みます。「Ldarg」や「Ldarg_S」命令でもOKです。その場合は、Emitメソッドの引数にインデックス(0ですが)を指定します。

Int32.ToStringのMethodInfoはどうやって入手するか / staticメソッドの呼び出し

メソッドを呼び出すには、「Call」命令を使います。その時、Emitする引数に、呼び出すメソッドの「メソッド情報(MethodInfo)」が必要です。これには、リフレクションを使います。

// Int32のTypeクラスのインスタンスを得る
Type int32Type = typeof(System.Int32);

// Int32.ToStringメソッドのメソッド情報を得る
MethodInfo toStringMethod = int32Type.GetMethod("ToString", Type.EmptyTypes);

ToStringのオーバーロードは複数あるので、正しいメソッド情報を選択するために、Type.EmptyTypesフィールドを使って、「引数0」のオーバーロードを選択させています(このフィールドを使わなくても、0個の配列でもOKです)。

typeofを使うことに対して、一部の方がモヤモヤしていた(チートっぽい)ようです。厳密に0からInt32のTypeを取得したことにならないのではないかとの事で、確かにその通り。typeofを使うと、C#コンパイラがコンパイル時にInt32を解決しようとします。実行時に0から取得する事も不可能ではないのですが、その話をするとまた話がずれていくので、今は諦めてもらう事に。

# Call命令を使うかどうかの更なる議論がありますが、それは後のPartで…


「動かない!」

複数のチームから「動かない!」とか、「NullReferenceExceptionがスローされる!」とか騒ぎが…

ilGenerator.Emit(OpCodes.Ldarg_0);
ilGenerator.Emit(OpCodes.Call, toStringMethod);
ilGenerator.Emit(OpCodes.Ret);

どう見ても間違っていないように見える… そこで、「ILSpy」を使って、C#で書いた等価コードを逆アセンブルした所、引数をスタックに積む際に、「Ldarga」命令を使っていました。

やられた orz

Ldarga命令は、対象の値そのものではなく、その値を示す参照(ポインタ)をスタックに積みます。これは、対象の値が「値型(ValueType)」であり、Callでそのメソッドに遷移した先ではthis参照として扱われなければならないため、Ldarg命令ではなく、Ldarga命令を使う必要があったのです。

Int32の場合は値型と言っても、なぜ参照化しなければならないのか、ピンと来ないかもしれません。以下に構造体(構造体も値型)を使った例を示します:

// 構造体
public struct Hoge
{
	public int A;
	public int B;
	public int C;

	// 計算し、結果をCに代入する
	public void Compute()
	{
		// 計算結果を代入するには、this(自分自身への参照)が必要
		this.C = this.A + this.B;
	}
}

// メソッドを呼び出して計算させる
Hoge hoge = new Hoge();
hoge.A = 1;
hoge.B = 2;
hoge.C = 123;
hoge.Compute();

// hoge.Cは、123か3か?
Console.WriteLine(hoge.C);

Computeメソッドはインスタンスメソッドなので、内部でthis参照が使えます。this参照を使った場合、そのインスタンスは、呼び出し元のhogeとなるはずです。Computeメソッドから呼び出し元が保持するインスタンスにアクセスさせるには、文字通り「参照」が必要なのです。従って、ここではLdarga命令を使って、インスタンスへの参照を渡す必要があります。勿論、対象が参照型(クラス)であれば、Ldarg命令で問題ありません。

# NullReferenceExceptionがスローされたのは、本当に偶然のようで、真の原因は分かりません。


全然足りない!!

そんなわけで、このPart1を30分ぐらいでやるつもりだったのが、これだけで時間使い切ってしまいました。値型と参照型の違いは、実際にはPart6ぐらいでやってもらう予定だったのが、こんな所でやる事になったのが敗因…

時間を延長して続けるかどうかという話もあったのですが、他セッションもあったため、次回に持ち越しとなりました。次回に同じ題目で再チャレンジします。ごめんなさい。スライドはその時まで正式公開はしません。次回も同様に参加する場合は、Part1の内容の復習をしておいてくだしあ。


詰め込みすぎは良くない

年末は… ダメですね。この記事も31日に書いてるし (;´Д`)
でも楽しかったです。来年もよろしくお願いします!

真・Roslyn for Scripting! あなたのアプリケーションにもC#スクリプトを!!

roslynこの記事は「C# Advent Calendar 2015」の18日目の記事です(遅刻しました (;´Д`))。
そして、この記事は以前に書いた「Roslyn for Scriptingで、あなたのアプリケーションにもC#スクリプトを!!」の改訂版なので、先にこちらを眺めてもらえるとスムーズに理解できると思います。


Roslyn for Scriptingの正式版が公開されました!

前記事ではまだ正式版ではありませんでしたが、とうとう正式版が公開されました。この記事では、以前のバージョンと正式版との違いについて焦点を当てたいと思います。

背景

前回の記事で紹介したNuGetのパッケージは、Visual Studio 2015リリースに向けての追い込みの時期に公開されたものです。バージョンは1.0.0-rc2なので、Visual Studio 2015に合わせて正式リリースするつもりであるように見えました。ただ、バックログを見ても色々課題が残っており、本当にあと少しで間に合うのかという不安もあったのです。

すると、何とRoslynのScripting部分はキャンセルされてしまいました!! rc3でそれらのコードはごっそり削除、NuGetの更新も途絶えてしまったのです。

私が知っている(issueにも挙げた)課題としては、以下のようなものがありました。

  • 外部から与えるグローバルインスタンスがDynamicObjectの場合でも、スクリプト環境はそれを認識できない。
  • メソッドの即時実行を行った場合に、非同期メソッドであった場合は、グローバルなクロージャが非同期コンテキストを認識できない。
  • スクリプティング環境では、Roslyn自体は内包されていてアクセス出来ない。IntelliSenseみたいな事をしたくても出来ない

# グロサミで私の写真がちらっと出た時はびっくりしました。ちょっと嬉しかった

これらがどのように解決されたのかも検証したいと思います。


インターフェイスの変更

正式版のバージョンは1.1.1です。これは、Visual Studio 2015 Update1とともにリリースされました。ただし、RoslynやScriptingのNuGetパッケージは独立しているので、VSのバージョンと関係なく個別に使用することができます。出来るのですが… 実際には.NET Framework 4.6以上が必要です。NuGetのパッケージのターゲットフレームワークが「dotnet」となっており、いわゆる「netcore5」に対応するのですが、従来の環境では4.6以上が必要になります。

実現内容を考えると、もっと古いターゲットでも動作させられる可能性はあると思うのですが、netcore5から導入されたライブラリのNuGet細分化を前提としているため、移植は別の意味で面倒かもしれません。

パッケージ名は「Microsoft.CodeAnalysis.CSharp.Scripting」です。NuGetでサクッとインストールしましょう。

PM> Install-Package Microsoft.CodeAnalysis.CSharp.Scripting

さて、正式版で変更を受けたポイントは2つあります。

  • 時間のかかる処理は、すべて非同期(Task)ベースとなりました。
  • デフォルトオプションは完全に「空」になりました。

時間のかかる処理とは、Scriptingで言うならRunメソッドです。rc2ではRunメソッドを呼び出すと、スクリプトが実行されて結果が返却されました。この部分がTaskベースの非同期処理となり、名前も「RunAsync」メソッドとなりました。後方互換性は維持されていないため、正式版を使用する場合は修正が必要です。とは言っても、難しいことは無いでしょう。

// 指定されたスクリプトを非同期で実行する
public static async Task ExecuteScriptAsync()
{
	Trace.WriteLine("Start script...");
	
	var script = CSharpScript.Create("Console.WriteLine(\"Hello C# scripting!\");");
	await script.RunAsync();

	Trace.Write("Finished script");
}

上の例のように、RunAsyncメソッドをawaitで待機します。あるいは、戻り値のTaskを使ってゴニョゴニョすればOKです。合理的な変更だと思います。また、Createメソッドはジェネリックバージョンもあり、RunAsyncの戻り値を型指定で伝搬させ、スクリプトの式評価の結果をタイプセーフに得る事もできます。この辺りも「こうなればいいのに」と思っていたことが実現されていて好感が持てます。

consolenotfoundさて、実は上のコードは動作しません。RunAsync呼び出し時に例外が発生します。

例外の内容を見ると、「Console」が見つからないと出力されています。rc2では、デフォルトのインポート節(C#で言うところのusing句)として「System」が含まれていたのですが、正式版ではこれが削除されて、何もインポートされていない状態となっています。そのため、自分でインポートを追加する必要があります(または、スクリプトコードにusing句を追加するか、完全限定名でクラスを指定します)。

修正したコードは以下の通りです。

// 指定されたスクリプトを非同期で実行する
public static async Task ExecuteScriptAsync()
{
	Trace.WriteLine("Start script...");
	
	var script = CSharpScript.Create("Console.WriteLine(\"Hello C# scripting!\");").
		WithOptions(ScriptOptions.Default.
			WithImports("System").
			WithReferences(typeof(object).Assembly));
	await script.RunAsync();

	Trace.Write("Finished script");
}

WithOptionsメソッドで、スクリプトにオプションを指定できます。前回の記事ではインポート(名前空間)と参照を加えていましたが、ここの扱いは今回も変わっていません。明示的に”System”を追加してあります。参照も追加してありますが、mscorlibの範疇の型は追加しなくても認識出来るようです。


グローバルコードの扱い

グローバルコードの解釈が練られ直されたようです。

前回紹介したように、スクリプティング環境では、単なる

// 以下はスクリプト:
// いきなりvar
var hoge = 123;

// いきなりメソッド呼び出し
Console.WriteLine("ABC");

のように、名前空間の指定もクラスや構造体もメソッドの定義もない所からの実行可能な式や文を許容します(許容する仕様です)。これが満たされないと、スクリプティングの逐次実行(要するにインタプリタのように使う)が出来ないので、魅力半減です。

このような文は、内部でコード生成された時点で、不透明な外郭クロージャクラスとクロージャメソッドが作られ、その中で文を実行する事になります。

所で、次のようなスクリプトを考えてみます。

// 以下はスクリプト:
using (var httpClient = new HttpClient())
{
	return await httpClient.GetStringAsync("http://www.bing.com/");
}

このコードの戻り値はTask<string>となります。コード中でawaitを使っているからなのですが、これも通常のコードとしてコンパイルする場合は、メソッドシグネチャに「async」と書かないとコンパイルエラーになります。同じRoslynを使ってパースしているのですが、グローバルコードではasyncを書かなくても(書けないが)、awaitを認識して、Taskクラスのインスタンスとして返却するのです。

この仕様はかなり奇妙ですが、コードを書かれるまでは、それが同期的なのか非同期的なのかが分からないので、仕方ないと割り切った感があります(これを許すなら、互換性のためのasync句とは何だったのかという気もしなくもない)。

クロージャーとなる実装がどのようなものかを、簡単に眺めてみます。

public static async Task ExecuteScriptAsync()
{
	Trace.WriteLine("Start script...");

	var script = CSharpScript.Create(
@"var sf = new StackFrame();
var method = sf.GetMethod();
var type = method.DeclaringType;
Console.WriteLine(""{0} - {1}"", type.FullName, method.Name);
").
		WithOptions(ScriptOptions.Default.
			WithImports(
				"System",
				"System.Diagnostics",
				"System.Reflection").
			WithReferences(typeof(object).Assembly));

	await script.RunAsync();
	Trace.Write("Finished script");
}

この結果、コンソールにはこのように表示されました:

Submission#0+<<Initialize>>d__0 - MoveNext

興味深い事です。クロージャーの実装は、”Submission#0″クラスのインナークラス”<>d__0″に作られた”MoveNext”メソッドのようです。これは、C#コンパイラがAwaitableなステートマシンを作るときの形に似ているので、おそらくそのまま再現されているのでしょう。そして、仮にスクリプトブロック内にawaitを使わなかったとしても、初めからasync-awaitが有効なコードブロックとして評価している事が分かります。

しかし、問題はそれだけではありません。「遅延実行」が絡む場合は、いつ実際に評価を行うのかが難しいのです。前回の説明で逐次処理を行う方法を説明しましたが、2つのawaitが分割されて処理されたらどうなるのでしょうか?

// 以下はスクリプト:
// 最初の処理単位(わざとusingを使わない)
var httpClient = new HttpClient();
await httpClient.GetStringAsync("http://www.bing.com/");

// (逐次処理評価で分断)

// 2回目の継続処理単位
await httpClient.GetStringAsync("http://www.google.com/");

なまじ、async-awaitがクロージャーに内包されて実行されている事を知っていると、このコード片がどのように実行されるのかに迷いが生じます。つまり、最初と2回目の継続処理は、単一のクロージャーにまとめられる必要があるのではないかと。しかし、「スクリプトである」と考えれば明確で、逐次処理単位毎に実行されます(つまり書いた通りの感じで逐次実行される)。

逐次処理の検証をしてみます。ScriptStateのAPIも整理されました。RunAsyncの結果がScriptStateなので、このクラスのContinueWithAsyncメソッドを使うと簡単に書けます:

public static async Task ExecuteScriptAsync3()
{
	Trace.WriteLine("Start script...");

	var script1 = CSharpScript.Create(
@"var httpClient = new HttpClient();
await httpClient.GetStringAsync(""http://www.bing.com/"");
").
		WithOptions(ScriptOptions.Default.
			WithImports(
				"System",
				"System.Net.Http").
			WithReferences(
				typeof(object).Assembly,
				typeof(HttpClient).Assembly));

	var scriptState1 = await script1.RunAsync();

	var scriptState2 = await scriptState1.ContinueWithAsync(
@"await httpClient.GetStringAsync(""http://www.google.com/"");
");

	Trace.Write("Finished script");
}

これで、最初のRunAsyncでBingが、次のContinueWithAsyncでGoogleが参照されます。しかし、結果は返されません。「return」句を入れると、スクリプトとしてのコードの性質がはっきりします。

public static async Task ExecuteScriptAsync3()
{
	Trace.WriteLine("Start script...");

	// GetStringAsyncをawait後にreturnする
	var script1 = CSharpScript.Create(
@"var httpClient = new HttpClient();
return await httpClient.GetStringAsync(""http://www.bing.com/"");
").
		WithOptions(ScriptOptions.Default.
			WithImports(
				"System",
				"System.Net.Http").
			WithReferences(
				typeof(object).Assembly,
				typeof(HttpClient).Assembly));

	// 戻り値を得る
	var scriptState1 = await script1.RunAsync();
	var result1 = scriptState1.ReturnValue;

	// GetStringAsyncをawait後にreturnする
	var scriptState2 = await scriptState1.ContinueWithAsync(
@"return await httpClient.GetStringAsync(""http://www.google.com/"");
");
	var result2 = scriptState2.ReturnValue;

	Trace.Write("Finished script");
}

結局、逐次処理の単位毎に、式の結果として評価された値が返される事が分かります。同時に、逐次処理させても、内部のクロージャーは包含された大きなメソッドを作るわけではなく分割され、しかし定義された変数は引き続き使えるようにしているのです。

このような挙動を見ていると、同じC#でRoslynでありながら、直観的なコードはかなり感じが違うなと思いました。勿論、メソッドやクラスを定義する事は可能なので、そこまでやればコンパイル前提のC#のコードと変わりませんが…


DynamicObjectの扱い

ホスト環境との通信として、グローバルにアクセス可能なインスタンスを用意して提供すると、スクリプトからアクセス出来る、という機能がありました。新しいバージョンでも引き続きサポートされていますが、その時こんなことを考えました:

// ホストオブジェクトクラス
public sealed class HostObject
{
	public HostObject()
	{
		this.Target = new ExpandoObject();
		this.Target.Id = 123;
		this.Target.Name = "ABC";
	}

	// ExpandoObjectを使ってダイナミックアクセスを指定するホスト環境のプロパティ
	public dynamic Target
	{
		get;
		private set;
	}
}

public static async Task ExecuteScriptAsync()
{
	Trace.WriteLine("Start script...");

	// HostObject.Targetにアクセスする
	var script1 = CSharpScript.Create(
@"Console.WriteLine(""Id={0}, Name={1}"", Target.Id, Target.Name);
",
		ScriptOptions.Default.
		WithImports(
			"System",
			"System.Diagnostics").
		WithReferences(
			typeof(object).Assembly,
			typeof(Microsoft.CSharp.RuntimeBinder.Binder).Assembly),
		typeof(HostObject));	// ホストオブジェクトの型を指定

	// ホストオブジェクト
	var hostObject = new HostObject();

	await script1.RunAsync(hostObject);

	Trace.Write("Finished script");
}

dynamicidnotfoundRunメソッドからRunAsyncメソッドに変わりましたが、引数にホストオブジェクトを渡すことが可能な点は変わっていません。但し、ホストオブジェクトの型を明示的に指定する必要があります。おそらく、C#スクリプトから見て、「静的」には何の型に見えるのかを明示する必要があるのでしょう。CSharpScript.Createに引数で指定します(例ではオプションの指定も引数に寄せてあります)。

また、Microsoft.CSharp.RuntimeBinder.Binderクラスのアセンブリを参照していますが、これは”Microsoft.CSharp”アセンブリを使うためで、要するにDLR対応です(dynamicキーワードを使う場合に必要)。

rc2では上記のコードを書いた場合、IdとNameプロパティには「アクセス出来ません」でした。issueに上げたのですが、どうやら今回も対応しなかったようです。これが出来ると、ホスト側のアクセス可能な要素を動的に定義できると思ったんですけどね。結構根が深そうです。


AST(抽象構文木)の参照

Roslyn for Scriptingは、当然内部でRoslynを使用しているので、スクリプトのパース結果はRoslynの抽象構文木で表現されているはずです。rc2では一体それがどこからアクセス可能なのか良くわかりませんでしたが、1.1.1ではScriptクラスのGetCompilationメソッドからアクセス出来ます。

public static async Task ExecuteScriptAsync()
{
	Trace.WriteLine("Start script...");

	var script1 = CSharpScript.Create(
@"var target = new
{
	Id = 123,
	Name = ""ABC""
};
Console.WriteLine(""Id={0}, Name={1}"", target.Id, target.Name);
").
		WithOptions(
			ScriptOptions.Default.
			WithImports(
				"System",
				"System.Diagnostics").
			WithReferences(
				typeof(object).Assembly);

	var compilation = script1.GetCompilation();
	var tree = compilation.SyntaxTrees.ElementAt(0);
	var compilationUnitSyntax = (CompilationUnitSyntax)tree.GetRoot();
	var members = compilationUnitSyntax.Members.Select(member => member.GetText().ToString());
	
	Console.WriteLine(string.Join(Environment.NewLine, members));

	Trace.Write("Finished script");
}

ToStringして表示しているだけなので何の捻りもありませんが、パース結果をロジック的に探索出来ます。これを使ってIntelliSenseのような表示の仕掛けも可能でしょう。


各種リゾルバー

スクリプトのコンソールインターフェイスが存在する場合は、ソースコードの参照先パスや、アセンブリの参照先パスを指定して、動的に読み込みをサポートしたくなるかもしれません。これらは通常はScriptOptions.WithFilePathやWithReferencesを使用して指定しますが、スクリプト環境内からの参照解決要求に応答するように実装する事も出来ます。

ScriptOptions.SourceResolverやMetadataResolverがそれに該当し、リゾルバー基底クラスを継承して、独自の参照解決を実装して指定する事が出来ます。

リゾルバーがいつ呼び出されるのか、ですが、Roslyn for Scriptingでは、「REPL」と呼ばれる帯域外コマンドが正式に決定されました。例えば、以下のように「#r」コマンドを使うと、スクリプト内で環境にアセンブリをロードすることが出来ます。

public static async Task ExecuteScriptAsync()
{
	Trace.WriteLine("Start script...");

	// スクリプト内で System.Net.Http アセンブリをロードして使う
	var script1 = CSharpScript.Create(
@"#r ""System.Net.Http""
var httpClient = new System.Net.Http.HttpClient();
return await httpClient.GetStringAsync(""http://www.bing.com/"");
").
		WithOptions(
			ScriptOptions.Default.
			WithImports(
				"System").
			WithReferences(
				typeof(object).Assembly));

	var scriptState = await script1.RunAsync();

	Trace.Write("Finished script");
}

この時、ScriptOptions.MetadataResolverには、RuntimeMetadataReferenceResolverがデフォルトで設定されており、このリゾルバーが標準的な.NETのアセンブリロード処理を行う(この場合は、System.Net.Http.dllをGACから読み取る)ので、#rコマンドが成立するのです。


まとめ

前回と今回の記事で、Roslyn for Scriptingの足掛かりは出来たかなと思います。Roslyn、特にASTの辺りはスクリプティングとは関係なく、これまた膨大なトピックが詰まっていると思います。Roslynで言うならば、上はこのスクリプティングやコンパイラインターフェイス、下はVisual Studio 2015の「アナライザー」が、Roslynへの入り口となると思います。

OSS化され、引き続き改良が進むと思われるため、当分の間はこのインフラが使用される事でしょう。参考になれば幸いです。

それでは。

ChalkTalk CLR – COMのすべて

WP_20151219_11_31_11_Pro_LI本日は「ChalkTalk CLR – COMのすべて」と題して、COM(Component Object Model)についてのディスカッションを行ってきました。

参加された方はCOM方面に強い方が半数近くいて(MVP4人、元MVP2人、中には現役で開発やってるという人も)、とにかくこれ以上ないぐらい強力なメンバーで、濃い議論が交わされました。多分、もうこういう企画は無いかな感全開 (;´Д`) 遠方からの参加もありがとうございます。 ChalkTalkバンジャーイ

この記事では、内容を「要約」して記載します。図については内容を考えて起こしなおしました。

前回と同じく、Maker Lab NAGOYAさんのスペースをお借りしました。ありがとうございました。


事前の洗い出し

WP_20151219_18_18_54_Proまず、各参加者のCOMに対してのスキルセットを「バリバリ解説OK」~「COMって何」のレベルで、かつ自分がCOMに対して抱えている課題、という切り口で、いつも通り付箋に5分で書いてもらって張り出しました。

写真は横に集約した後の状態なので分かりにくいですが、上から「COMって何」~「バリバリ解説OK」の順で並べてあります。要約すると:

  • COMの概念的なものへの疑問
  • COMとWinRTとの関係
  • COMの将来展望
  • COMの実践的な実装方法
  • スレッドアパートメントとは何か
  • マーシャリングとは何か

という内容が全体的に知りたいとことと認識できたので、順に掘り下げていきました。


COMの概念的なものへの疑問

そもそもCOMとは何で、何のための技術?技法?なのか? という基本的な疑問です。ディスカッションでは、Essential COM第一章や、Inside OLEでのIUnknownインターフェイスの実装例の話から:

  • 処理系の問題1: リンカー(C言語の)のリンケージの問題
  • 処理系の問題2: メソッドの呼び出し規約の問題
  • コンポーネントインスタンスの生存期間の問題
  • インターフェイス型の認識の問題

と言う問題の解決に集約されるという意見交換がなされました。

処理系の問題

処理系の問題とは、C/C++コンパイラ・リンカーが、それぞれの製品(例:VC++、gcc、BC++など)で、シンボル名の内部的な命名規約が独自仕様である事と、メソッドのエントリポイントが特定されたとしても、その呼び出し規約が異なる事がある、と言う事です。

処理系によってシンボル名が異なる(特にC++において)問題は、シンボル名に引数の型情報を含ませることから発生します。このシンボル名の操作の事を「マングリング(Mangling)」と呼び、その仕様は処理系によって異なります。また、同じ処理系でもバージョンによっても異なる可能性があります。

dependencywalker1(.NETではなく)Win32レベルでのDLLのエクスポートシンボルを調べるツールとして「Dependency Walker」と言うツールがあります。これを使ってEXEやDLLを調べると、エクスポートシンボルの名前が分かります。

このスクリーンショットでは「KERNEL32.DLL」のエクスポートシンボルを見ています。赤枠に表示されていますが、普通にWin32 APIのシンボル名が見えます。シンボル名が普通に見える関数は、呼び出し規約が「stdcall」である事を示しています。

dependencywalker2対比する形で、「MSVCRT.DLL」を見てみます。赤枠を見ると、「exception」とか「badcast」とか読み取れる単語もありますが、何やらわけのわからない記号が沢山見えます。これがマングリングされた状態のシンボル名で、VC++のシンボル名規約に従って変換された結果です。

dependencywalker3そういえば、実際のところこれがC++のどんなシンボルに相当するのかを実演しなかったのを思い出しました。これが、デマングリングした結果です(メニューから「Undecorate C++ symbol」を選択すると表示されます。見比べてみると面白いと思います)。

このような、C言語で言うところの関数名だけでは表現できない、C++のオーバーロードやクラスメンバ関数、const関数などを、シンボル名だけで識別可能にするために、マングリング操作が行われます。そしてこのマングリング操作が、処理系依存(かつバージョン依存)なのです。

したがって、例えばVC++ 6.0で普通に作ったDLLとVBで作ったDLLを直接静的にリンクすることは出来ません。他の処理系についても同様です。COMは、これらの処理系依存シンボルを使わず、COMのランタイムが提供する情報だけでクラスのファクトリを特定し、メンバ関数の位置はvtableと呼ばれる関数ポインタのテーブルの並びの整合性を担保することで、この問題を回避します。

# vtableの話はディスカッションでやらなかったですね。vtableはC++のvtableそのままで、仮想関数へのポインタのテーブルです。

どうやってクラスを特定するのかは、レジストリのHKEY_CLASSES_ROOT配下の情報を使います。ATLでプロジェクトを新規に生成すると、プロジェクト内に*.rgsのようなファイルが配置されます。これはコンポーネントレジストラスクリプトと呼び、要するにregsvr32された時にレジストリのどこにどんな値を書き込むのかを表し、このスクリプトでCOMのクラス(coclass)が特定できるようにします。

# 特定には、COMコンポーネントの実装が、どこのパスにあるDLL内にあり、どのクラス(CLSID –> GUID)なのかという情報を含みます。

もう一つの問題がメソッドの呼び出し規約で、これはWikipediaにあるように複数の規約があり、VC++では各関数単位でこれを指定して実装できるのですが、COMでは例外なく「stdcall」を使います。

生存期間

生存期間とは、COMのクラスのインスタンスが、いつまで生き続けるのかと言う問題です。.NETのインスタンスは、誰からも参照されなくなり、GCが回収する(場合によってはファイナライザーが呼び出され)と死んだ事になるのですが、C言語のようなアンマネージな世界では、「malloc」や「new」によってインスタンスが(ヒープに)生成され、「free」や「delete」によってメモリから取り除かれます。

// 確保する
auto p = static_cast<int*>(malloc(123 * sizeof(int)));

// 使う...

// 解放する
free(p);

このような簡単な例であれば、確保して解放するというサイクルから、解放のタイミングの時点で正しく解放されることが分かります。しかし:

static const int* pUseLate = nullptr;

static void ComputeStep1(const int* pTarget)
{
	// 後で使いたいので保存
	pUseLate = pTarget;
}

static void ComputeStep2()
{
	// (pUseLateを使って何かやる)
}

// 確保する
auto p = static_cast<int*>(malloc(123 * sizeof(int)));

// 使う...

// 処理その1
ComputeStep1(p);

// 処理が終わったので解放する
free(p);

// 処理その2
ComputeStep2();

このような処理があった場合、ComputeStep2の内部実装では、既にpが解放されていることを知る由はありません。このサンプルコードは非常に短く、メインの処理とComputeStepの実装が並べて書いてあるので「こんなのダメに決まってるじゃん!」と分かりますが、ComputeStepの実装は「別のだれか」がやるとしたらどうでしょうか? あるいは、既にComputeStepは別の誰かが実装済みであり、これからメインの処理を書く場合、ComputeStep2の実行前にpを解放してはならない事はどうやって分かるのでしょうか?

このような、インスタンスの生存期間の問題を解決する方法として、COMではクラス実装側に参照カウンタを持ち、このカウンタ値を監視することで不要になったかどうかを判定します。

生存期間のカウンタは、IUnknownインターフェイスAddRefによってカウントアップされ、Releaseによってカウントダウンされます。カウンタの初期値は1で、AddRefのたびにインクリメントされ、Releaseが呼び出されるとデクリメントされます。カウンタが0になると、自己消滅するように実装します。

使用する側は、ポインタをコピーするときにはAddRef、使い終わったらReleaseするという規約を守る事により、生存期間が正しく管理されるようになります。

インターフェイス型の認識

インターフェイス型の認識の問題もあります。C++上でインターフェイスを示すポインタをキャストして、目的のインターフェイスへのポインタを取得しようとしたとします。

// CoCreateInstanceなどで取得したインターフェイスポインタ
IUnknown* pUnknown = ...;

// 目的のインターフェイスポインタを得るためにキャスト
ICalc* pCalc = static_cast<ICalc*>(pUnknown);

このキャストが正しく解釈されるかどうかは、処理系に依存します。IUnknownをICalcにするにはダウンキャストが必要ですが、C++の世界ではコンパイラがその判断を行えるものの、COMの世界ではインターフェイスの情報しか存在しないため、正しくキャストできる保証がありません。そのため、キャストと言う処理自体も、IUnknown.QueryInterfaceメソッド呼び出しで解決します。

// CoCreateInstanceなどで取得したインターフェイスポインタ
IUnknown* pUnknown = ...;

// 目的のインターフェイスポインタを得るためにキャスト
ICalc* pCalc = nullptr;
pUnknown->QueryInterface(IID_ICalc, reinterpret_cast<void**>(&pCalc));

第一引数はインターフェイスID(GUID)で、これが必要なのはCOMの世界は.NETとは異なりリフレクションが存在しないので、インターフェイス型を一意に識別するIDが必要だからです。勿論、この場合のIID_ICalcは、ICalcインターフェイスのIDである前提です。

QueryInterfaceメソッド内でキャストを実装することにより、処理系の範囲内で正しくキャストが行えます。返されるポインタはキャスト後のポインタであるため、呼び出し元は処理系に依存することなくキャスト出来たことになります。

このようなメソッドを提供することで、処理系に(C++固有の)型を認識させる必要をなくし、互換性を維持します。

COMの概念への展開

結局、IUnknownの実装を通じて、どうやって処理系に依存しないでバイナリ互換性を維持するのかという事が、COMの中心にある概念であることを確認しました。COMのIUnknownとその実装はこんな感じだ!というのを、C#ライブコーディングで疑似実装して、参照カウンタの動きや、QueryInterfaceによるキャストの挙動を示して、内容の共有をしました。

# 以下のコードは、クラスファクトリも含めて穴埋めしたコードです。

// (あくまで概念コードです。これは正式なCOMのコードではありません)
public interface IUnknown
{
	T QueryInterface<T>() where T : IUnknown;
	int AddRef();
	int Release();
}

public interface ICalc : IUnknown
{
	int Add(int a, int b);
}

// クラスは直接公開されない
internal sealed class CalcImpl : IUnknown, ICalc
{
	private int count_ = 1;

	public int AddRef()
	{
		return Interlocked.Increment(ref count_);
	}

	public int Release()
	{
		var current = Interlocked.Decrement(ref count_);
		if (count == 0)
		{
			// インスタンスの破棄処理
			// Disposeに近いが、メモリストレージも削除される。
			// 破棄処理を隠蔽することで、処理系依存を排除。
		}
	}

	public T QueryInterface<T>() where T : IUnknown
	{
		// CやC++にはリフレクションは無いので、本当はインターフェイスID(GUID)で判定する。
		// キャスト処理を隠蔽することで、処理系依存を排除。
		if (typeof(T) == typeof(ICalc))
		{
			Interlocked.Increment(ref count_);
			return (T)(object)this;
		}
		if (typeof(T) == typeof(IUnknown))
		{
			Interlocked.Increment(ref count_);
			return (T)(object)this;
		}

		throw new NotImplementedException();
	}

	public int Add(int a, int b)
	{
		return a + b;
	}
}

public interface IClassFactory
{
	T CreateInstance<T>(Guid clsid) where T : IUnknown;
}

// クラスファクトリクラスも公開されない
internal sealed class CalcClassFactory : IClassFactory
{
	public T CreateInstance<T>(Guid clsid) where T : IUnknown
	{
		if (clsid == CLSID_Calc)
		{
			// newの実装を隠蔽することで、処理系依存を排除。
			return new CalcImpl();
		}

		throw new NotImplementedException();
	}
}

// CalcImplを外部から特定するためのGuid
public static readonly Guid CLSID_Calc = new Guid("BD4D1DDD-9C28-4432-A8DD-9CFA77E6433F");

// DLL外からはこのエントリポイントだけが見える
public static IClassFactory DllGetClassObject()
{
	return new CalcClassFactory();
}

もう既にかなりアレですが、まだまだ続きます。


COMとWinRT

discussion_allaboutcomWinRT(ストアアプリ・UWPのフレームワークとして使われるライブラリ)は、基礎がCOMで出来ていることは分かっていましたが、それ以上の理解は私の中になく、あんまり調べる意欲もなかったので、知りたかった部分です。

全てのCOMクラスはIUnknownインターフェイスを実装しますが、WinRTのクラスは、IInspectableインターフェイスも実装します。COMの世界ではITypeInfoインターフェイスをタイプライブラリ情報を公開するのに使いますが、これのWinRT版のような位置づけです。WinRTでは型情報は.NETのメタデータをそのまま使用します(winmd)。この情報との対応付けにIInspectableインターフェイスを使います。したがって、ITypeInfoのように、情報の細部までトラバース可能なメソッドセットは持っていません。

なお、COMにおいてITypeInfoインターフェイスの実装は任意ですが、WinRTのIInspectableの実装は必須です。

WinRTにおいてのもう一つの事情は、スレッドアパートメントの扱いです。WinRTでは、メインスレッドに相当するスレッドがASTAに属し、それ以外のスレッドはMTAに属するそうです。各アパートメント間の関係は、通常のCOMと同一とのこと。なるほど。

# 余談ですが、MSDNライブラリのリンク壊れてたりするよねー、直してほしいよねーみたいな話が… TechNetもぶっ壊れてるなー |д゚) チラッ

こんな話をすると、当然「アパートメントとは何か」みたいなところを避けては通れないけど、とりあえずは次の課題へ。


COMの実践的な実装方法

ようするに、どうやってCOMを書けば良いのかわからない(.NETとかVB6以外で)。

ATLでやる

ということで、一からIUnknownをCで実装するというハードは方法は時間もないのでパスし、ATL(Active Template Library)での実装方法をライブデモしました。ライブデモは私がやったのですが、ATLでバリバリ書いていたのはVisual Studio 2005の時だったので、2005を使ってデモ(こんなこともあろうかと、2005はインスコしてあるのだよ)。ATLは途中から、C#のような属性ベースの指定で楽に書けるようになったはずなのですが、結局私的には移行しなかったので分からず…

atlwizard新しいプロジェクトの追加で、「ATLプロジェクト」を追加し、「プロジェクト-追加-クラス」で、「ATLシンプルオブジェクト」を選択すると、COMをATLで実装するクラスのひな型が追加されるウィザードが表示されます。短い名前の所にクラス名を入れると、各ファイル名とか良しなに決定されます。問題は次のウィザード。

ここのスレッドモデルとアグリゲーションの選択も、結構ディープだよね、ああそれとフリースレッドマーシャラーとかも。でもこの説明をするには、やっぱりアパートメントを避けては通れないしデモが中断してしまうので、ちょっと横に置いておく事に。

これでひな型が生成されたら、あとは「クラスビュー」のCOMインターフェイスのツリーから「追加-メソッド」とか「追加-プロパティ」とかやると、メンバを追加するウィザードが表示されて、メンバ定義を簡単に追加できます。もし、手動で定義するとなると、IDLファイル・ヘッダファイル・ソースファイルを同時に修正しなければならないため、かなり面倒です。そして、追加はウィザードで簡単にできますが、修正するとなると同じように全部直さなければならないので、結構苦痛…

で、実装出来たらビルドしますが、Visual Studioが管理者権限で起動していないと、最後にエラーが出ます。これはregsvr32でDLLを登録しようとしたときに、コンポーネントレジストラスクリプトがレジストリを変更するためで、素直に再起動。

TestPropという文字列(BSTR)のプロパティを追加し:

// メンバには以下のようなフィールドを定義
private: _bstr_t temp;

STDMETHODIMP CTest::get_TestProp(BSTR* pVal)
{
	// TODO: ここに実装コードを追加してください。

	*pVal = temp.copy();
	return S_OK;
}

STDMETHODIMP CTest::put_TestProp(BSTR newVal)
{
	// TODO: ここに実装コードを追加してください。

	temp = newVal;
	return S_OK;
}

テストするコードはC#で書きました。
COM側のビルドとregsvr32が完了していれば、C#の参照設定でCOMコンポーネントが参照出来ます。

using ATLSampleLib;

class Program
{
	static void Main(string[] args)
	{
		var testClass = new TestClass();
		testClass.TestProp = "ABCX";
		Console.WriteLine(testClass.TestProp);
	}
}

これで無事、コンソールに”ABCX”が表示されました。

WinRTでは文字列をHSTRING型で扱いますが、これはリテラル文字列をいちいちメモリ確保してコピーして…というコストを削減するためだそうです。なるほど、それだとCLRの世界での文字列と同じように扱われる訳で、イメージは湧きやすい。BSTRを使うと、内部ではSysAllocString APIなどでメモリを確保したり解放したりするので、コストは高いです。

いつ解放されるのか

上記テストコードで、AddRefもReleaseも呼び出していないのは?という疑問に対し、CLRの世界にはRCW(ランタイム呼び出し可能ラッパー)があって、これがAddRefとかReleaseを呼び出しているのだ、しかし、この例では使用後にすぐプロセスが終了してしまうので、果たして呼び出されるのか?という疑問があり、じゃあ、Marshal.FinalReleaseComObjectを呼び出せばどうだ、いや、それを呼び出すと挙動がおかしい、などの説があって、試しました。

using ATLSampleLib;

class Program
{
	static void Main(string[] args)
	{
		var testClass = new TestClass();
		testClass.TestProp = "ABCX";
		Console.WriteLine(testClass.TestProp);

		Marshal.FinalReleaseComObject(testClass);
	}
}

ATL側では、インスタンスの寿命が尽きる時(参照カウンタが0になったとき)、FinalReleaseメソッドが呼び出されます。ここにブレークを張ってテストしたのですが、やってこない…

FinalReleaseComObjectの代わりにGCを手動実行したらどうだろうと言う事でテスト:

using ATLSampleLib;

class Program
{
	static void Main(string[] args)
	{
		var testClass = new TestClass();
		testClass.TestProp = "ABCX";
		Console.WriteLine(testClass.TestProp);

		GC.Collect(2, GCCollectionMode.Forced, true);
		GC.Collect(2, GCCollectionMode.Forced, true);
		GC.Collect(2, GCCollectionMode.Forced, true);
	}
}

すると来ましたFinalRelease。うーむ、FinalReleaseComObjectとは何なのか? これだとインターフェイスポインタを直接弄った方が安心感があるよなぁと思わなくもない(真相は分からず)。

finalreleasestacktrace1もう一つ気が付いたことが。FinalReleaseでブレークを張ったところ、スタックトレースがこのように。

これワーカースレッドから呼ばれてるよね、何でだ?と思ったらMainメソッドに、STAThread属性を付けていなかった。

finalreleasestacktrace2で、STAThreadを追加したところ、無事にメインスレッドから呼び出された(このCOMコンポーネントがワーカースレッドから呼び出されること自体に問題はないんですが、一瞬えっと思ったので気が付いた次第)。

# しかし、このスタックトレースも大概だなぁ。当然こうなる事は予測できるんだけど…

さて、これの何が問題かと言う事は、やっぱりアパートメントの話に踏み込まないと説明できないよね。と言う事で、そろそろ参加者の意識共有も出来たので、アパートメントを話をし始めました。


アパートメントとは何なのか?

apartment_diagramアパートメントとは、スレッドとCOMのインスタンスをグループ分けする概念で、そのグループとは何なの?というディスカッションをしました。グループの種類は「STA(シングルスレッドアパートメント)」と「MTA(マルチスレッドアパートメント)」の二種類(あと、概念的にはメインSTA)があります。

アパートメントの概念が難しいのは、用語の説明から入らざるを得ないからかなーと思います。このような図が一般的に説明に用いられますが、内部実装的にこうなっているわけではなく、あくまで概念的に分割される、という事も、難解なところかなと。混乱すると良くないので、内部もこの通りになっていると仮定してもいいんじゃないかと思います(実際は違うって事が分かるようになったら、もう説明も不要になってるはず)。

シングルスレッドアパートメント

一つのスレッドだけが特定のSTAに属します。新たなスレッドを作り、STAに属させると、図の通り新しいSTAが作られます。メインスレッドがSTAとなった場合は、特別に「メインSTA」と呼びます(実際には、プロセス内で最初にSTAとして初期化されたスレッドです)。スレッドをSTAに属させるには、CoInitializeEx APIを実行します。.NETの場合は、メインスレッドであれば、Mainメソッドに「STAThread属性」を適用し、ワーカースレッドを作る場合は、Thread.SetApartmentStateメソッドで指定します。

図のように、STAは他のアパートメントと(概念的に)分離されており、そこに属するスレッドやインスタンスは、別のアパートメントから直接参照することが出来ないという原則に従います。

マルチスレッドアパートメント

MTAは、プロセス内にただ一つだけ存在します。そして、MTAに属するスレッドは複数存在できます。スレッドをMTAに属させる方法も、STAとほぼ同等です。Mainメソッドに適用する場合は、「MTAThread属性」を使用します。

STAとアパートメント間の特性

apartment_diagram2STAとMTAの最大の違いは、STAは「ウインドウメッセージキュー」に依存していると言う事です。今までのCOMの説明にはウインドウとかコントロールのようなユーザーインターフェイスの話は全く出て来なかったので、「えっ」てなってました。

ここで説明の順序を逆にして、遠い昔、Win16(つまりWindows 3.1以下)の頃は、マルチスレッド対応がそもそも無く(_beginthreadex APIとか存在しない)、すべての操作を非同期的に処理するためには、メッセージポンプにメッセージを配信する形で協調的に動作させる(ノンプリエンティブマルチタスク)必要がありました。

この頃に作られたアプリケーションは、マルチスレッドのように横から突然割り込まれてメソッドが実行されるような事を全く想定していません(スレッドがそもそも存在しないので)。今でいうところのモニターロックや排他制御がまったく不要だったのです。そのため、マルチスレッド環境で別のスレッドからこのような実装を呼び出すと、容易に破たんします。そして、現在のユーザーインターフェイスも全く同様の構造なのです。

つまり、異なるスレッドからウインドウ要素を操作したりする場合は、すべからく安全策をとる必要があります。WPFであればDispatcherを使う、Windows FormsであればInvokeメソッドを使って簡単に呼び出しの「マーシャリング」を実行できます。しかし、COMが考案されたころには、そのような強力な道具立てがなかったのです。

更に、既存のマルチスレッド非対応コードがバイナリライブラリとして「変更不能」な状態で存在するため、互換性も担保する必要があります。この問題をフレームワーク(COMランタイムライブラリ)で隠蔽し、どうにかして安全性と実装の軽減を両立させたかったのだと推測されます。

apartment_diagram5STAがウインドウメッセージキューに依存しているとは、STA内のCOMコンポーネントインスタンスを操作する場合は、このキューに一旦要求(メソッド呼び出し情報)を格納し、「協調的」に処理させると言う事です。

勿論、STA内のスレッドであれば、同じSTA内のインスタンスは、キューを介することなく操作できます。勿論、メソッド呼び出し結果の返却も、キューに一旦結果を格納して受け取ります。このときのキューは、呼び出し側のキューを使います。

このように、ウインドウメッセージキューを介在させることで、ウインドウメッセージ(ウインドウの移動やリサイズ・ボタンのクリック・テキストの変更など)にいきなり割り込むことなく、順番に規則的に実行されるので、マルチスレッド競合について考えなくても良くなる、というのが、STAに属する利点となり、同時に高いオーバーヘッドが欠点となります。

apartment_diagram6ところで、STAとMTA間で呼び出しを処理する場合は、呼び出し側がMTAであれば、呼び出し時はSTAのキューを使いますが、処理結果はキューに入らず、直接元のスレッドに伝達されます。逆であれば、呼び出しがキューに入らず、結果は一旦キューに入ります。

これは、MTAは特定のウインドウメッセージキューに紐づく事がないからです。したがって、ユーザーインターフェイスを含むCOMコンポーネントをMTAに属させると、クラッシュするなどの予期しない結果を引き起こします。

そして、MTAには複数のスレッドが同居出来て、MTA内のCOMインスタンスはすべて介在される事なく直接呼び出しが成立するので、ほぼコスト0で高速にメソッド呼び出しが行われます。これが、IISにホストされるCOMコンポーネントをSTAではなくMTAで動作させなければならない理由です(MTAにするとパフォーマンスが向上するらしい、という理由で、背景を考えずにMTAに属させるコードを書くと、間違いなくハマります)。

COMインスタンスはどこのアパートメントに配置されるか

atlwizardあるアパートメントに属しているスレッドがCOMコンポーネントのインスタンスを生成(CoCreateInstance API)すると、コンポーネントに指定された属性に従って、インスタンスが配置されるアパートメントが変わります。再びATL COMオブジェクトウィザードのスクリーンショットですが、ここで、「スレッドモデル」を選択可能です。

このスレッドモデルの選択と、現在のスレッドがどのアパートメントに属しているのかによって、配置されるアパートメントが決定されます。昔、この組み合わせの表をどこかで見た記憶があるのですが、改めて書き出してみました(ただし、すべて検証しているわけではないので、間違っているかもしれません)。

メインSTA STA MTA
シングル メインSTA メインSTA メインSTA
アパートメント メインSTA STA STA
両方(Both) メインSTA STA MTA
フリー(Free) MTA MTA MTA
ニュートラル メインSTA STA MTA

この表を眺めていると、何を選択すべきかが見えてきます。

  • ユーザーインターフェイス(ウインドウメッセージを必要とする)を含むコンポーネントであれば、「アパートメント」を選択する。これには、俗に言う「ActiveXコントロール」も含まれます。
  • その中でも特にメインスレッド(メインSTA)でのみ動作可能なコンポーネントであれば、「シングル」を選択する。
    (今やこのようなコードは想像しにくいかもしれません。例えばグローバル変数に状態を持っていて、これを操作しているようなUIコンポーネントは、多分メインSTAでしか動作しません)
  • アパートメントの影響を受けないコンポーネントであれば、「両方」又は「ニュートラル」を選択する。
  • MTAにのみ必ず属させ、ハイパフォーマンスでローコストな環境を必要とするコンポーネントであれば、「フリー」を選択する。

再度確認したのが、MTAに属させたからと言ってハイパフォーマンスになるわけではなく、マーシャリングコストを最小化可能であることがミソなので、ここで不用意に「フリー」を選択すると、アパートメントの選択同様ドツボにはまる事に注意が必要です。

一番無難な選択肢が「両方(Both)」ですが、表の上では「ニュートラル」も同じとなっています。両方とニュートラルの違いは、両方とすると、メインSTA・STA・MTAのどこにでも配置出来るものの、配置されるとそのアパートメントに完全に紐づきます。対して、ニュートラルとは、本当のところは「どのアパートメントにも属さない」状態となります。この違いは、マーシャリングの動作に影響します。


マーシャリングとは

アパートメントの話は抽象的なので、もう少し実装に近寄ります。

apartment_diagram7アパートメントの境界を超えるために、COMのランタイムによって「プロキシ・スタブ」という一種のファサードが自動的に介在し、メソッド呼び出しを自動的にメッセージキューに保存し、目的のSTAのスレッドでこれを取り出して実行する、という処理を行う事を説明しました。一般的にこのような操作を行う事を「マーシャリング」と言います。

マーシャリングは、カスタム「プロキシ・スタブ」コードで完全に独自の実装を行うか、あるいは「COMスタンダードマーシャラー」を使うかを選択できます。正直カスタムマーシャラーは深くて闇で資料も少なく、これを自前で実装する事は殆ど無いと思います。

どちらにしてもこの図のように、アパートメント境界の間に入って、メッセージキューの操作をしたり、スタックを操ってリモートメソッド呼び出しを成立させる役割を担います。呼び出し元は、カスタムマーシャラーが「何故か」目的のインターフェイスを実装したインスタンスに見えるので、アパートメント内呼び出しと全く同じように、インターフェイスメソッドを呼び出す事が出来ます。

# IUnknown.QueryInterfaceメソッドは、インターフェイスIDを受け取って、そのインターフェイスへのポインタを返します。と言う事は、COMスタンダードマーシャラーが、実際には実装を用意していなかったとしても、IID_ICalcのようなインターフェイスIDが存在して、キャストも成立させたように見せかければ、この状況を作り出す事ができます。返されるインターフェイスポインタは、ICalcを動的に実装したスタブクラスです。

さて、このマーシャラーが一体いつ挿入されるのかが、背景を知っていないと分からないと思うため、その説明をしました。

インスタンス生成時

apartment_diagram8CoCreateInstance APIでインスタンスを生成するとき、呼び出し元のスレッドが属するアパートメントと、COMコンポーネントに指定された「スレッドモデル」に応じて、インスタンスが配置されるアパートメントが決定されます。では、呼び出し元スレッドのアパートメントと異なるアパートメントにインスタンスが生成され、配置された場合、呼び出し元はどうやってそのインスタンスにアクセス出来るのか?

答えは、CoCreateInstanceが返すインターフェイスポインタが、「既にCOMスタンダードマーシャラーなどのファサードを示している」ので、呼び出し元は何も考えなくても良い、と言う事です。

この図のように、MTAのワーカースレッドは、CoCreateInstanceが返すインターフェイスポインタを、本物の「ICalc」を実装したインスタンスだと思い込んでいます。しかしその実態はマーシャラーであり、ICalcのメソッドを呼び出すとマーシャラーのメソッドを呼び出し、後は前述のとおり、マーシャリング動作が行われます。

インターフェイスポインタの自動伝搬時

内部的にはここが難しい所で、一度マーシャラー経由のインターフェイスポインタを取得した後は、メソッド呼び出し引数や戻り値にインターフェイスポインタが含まれていれば、マーシャラーが自動的にそのインターフェイスポインタにもマーシャラーを挿入します。

// 呼び出し元の例:
IHogeService* pHogeService = ...;

// 例えば、戻り値がインターフェイスポインタの場合
IResultData* pResultData = nullptr;
pHogeService->GetResult(&pResultData);

// (pResultDataは何を指しているか?)

apartment_diagram9GetResultメソッドの戻り値がIResultDataという、別のCOMコンポーネントインスタンスへのポインタである場合、その実態は呼び出し先のアパートメントに存在する可能性が高いです(必ずと言うわけではない)。

その場合、戻り値として返されるインターフェイスポインタは、あくまで呼び出し先のアパートメントのインスタンスを示しているので、それがそのまま呼び出し元に返されても、その後が困ります(そのまま使うとマーシャリングが行われないので、アパート境界が守られず、酷い問題が発生する)。

そのため、スタンダードマーシャラーは、戻り値のインターフェイスポインタを「マーシャリング」し、新たなマーシャラーを割り当て、マーシャラーのインスタンスへのポインタを呼び出し元に返却します。

呼び出し元がIResultDataのメソッドを呼び出すと、裏ではマーシャラーを介してインスタンスを操作することになり、安全性が担保されます。呼び出し元はIResultDataがマーシャラーである事は知る由もなく、直接呼び出しているのと変わりません。

さて、これを実現するには、引数や戻り値に「インターフェイスポインタが含まれているかどうか」を認識出来なければなりません。CやC++のソースコード上では、インターフェイス型を指定するので(人間の目には)判別できます。しかし、コンパイル後のバイナリには、型情報が含まれないため、自動的に検出する事が出来ません。COMスタンダードマーシャラーが、ノーメンテで動作するには、型の自動判別が可能である必要があるのです。

方法は2つあります。一つは、インターフェイス型のメタデータをタイプライブラリ(*.tlb)として読み取り可能になるようにファイルを配置するか、リソースとしてDLLに埋め込み、「プロキシ/スタブデータ」を提供することです。これにより、COMスタンダードマーシャラーは、引数や戻り値の型情報にアクセスし、それがマーシャリングの対象である事を認識できます。

もう一つは、同じくタイプライブラリを提供する必要がありますが、引数や戻り値の型にVARIANT型を使う事です(プロキシ/スタブデータは不要)。VARIANT型は、格納できるサブタイプを指定して値を格納します。サブタイプの種類にハードコーディングされた制限がありますが、基本的なプリミティブ型やインターフェイスポインタなどを含むことが出来、何が含まれているのかを検知出来ます。但し、VARIANT型を使うと、タイプセーフ性は低下します。

# もちろん、カスタムマーシャラーを書いた場合は、自分でマーシャリングを行うため、タイプライブラリの準備やVARIANT型への依存は不要です。また、IDispatchインターフェイス経由でのみ公開されるメソッドの場合は、すべてVARIANT型である事を想定可能なので、タイプライブラリは不要です。

インターフェイスポインタの手動伝搬時

自動マーシャラーを全く介さないというソリューションもあります。一つは「フリースレッドマーシャラー(FTM)」を集約する事です。詳細は省きますが、この場合はマーシャリングを自分で面倒見なければなりません。もう一つが、スレッドモデルを「ニュートラル」とした場合です。この場合もマーシャリングは自分で面倒を見る必要があります。

マーシャラーを全く介さないと、メソッド呼び出しはダイレクトにCOMインスタンスのメソッドを呼び出す事になります。当然、引数や戻り値に渡されるインターフェイスポインタも生ポインタであるので、アパートメントの境界を越えてアクセスして良いかどうかは全く分かりません。

そのような場合に自分でマーシャリングを実行する必要がありますが、2つの方法があります。

APIやGITを使うと、インターフェイスポインタを再取得したときに、マーシャリングが必要だと判断されれば、マーシャラーが生成されて、マーシャラーへのポインタが返されます。不要と判断されると、直接COMコンポーネントインスタンスへのポインタが返されます。

# 回り回って元のアパートメントでインターフェイスポインタを取得すると、ちゃんと再計算されて、生ポインタが取得できた、ハズ…

現在ではGITを使う方が簡単です。特にATLを使う場合は、CComGITPtrクラスを使うと、非常に簡単にマーシャリングを行うことが出来ます。GITで言うところの「Cookieデータ」が、APIを使う場合の抽象的なデータに相当します。

何故、FTMやニュートラルというオプションがあるのかと言うと、お互いのCOMコンポーネントが何であるかが確実で、お互いに共謀出来るのであれば、異なるアパートメントであろうが、直接生ポインタを操作しても問題ない(と分かっている)はず、という非常に厳しい制約の元でマーシャリングコストを0にすることも出来る、ということです。つまり、ただでさえ複雑なスレッドモデルを正しく理解する必要があるので、FTMやニュートラルを使う事は、よほどの理由がない限り避けた方が良いと言えます(せっかくCOMのランタイムが色々頑張ってくれる機能を放棄することに近しいです)。

DCOMへの拡張

ここまで見てきたアパートメントとマーシャリングの話が分かれば、「DCOM(Distributed COM)」は、延長上の技術でしかない事がわかります。

  • 今までの話は、プロセス内でのアパートメント境界を超える場合のマーシャリング処理ですが、
  • プロセス間通信にも応用できますね?(アウトプロセスCOMサーバー・サービス)
  • プロセス間通信が出来るなら、マシン間でも通信できるよね?(DCOM)

DCOMとは、要するにそういう事です。勿論、マシン境界をまたぐ場合は、セキュリティのトピック(認証と認可をどうするかなど)が存在しますが、基本は全く変わりません。


COMの将来展望(クロージング)

WP_20151219_18_26_52_Pro_LI時間が押してきたので、ほかにもトピックはあるのですがクロージングに入りました。

今回、主題として「All about COM」と銘打ったのですが、実は「隠された副題」がありました。それは、「COM requiem」です。COMの全盛期は2000年頃(つまりもう15年も前)と思っているのですが、まだまだいたるところで使われており、すぐにomitすることは当面出来そうにありません。しかし、プログラミング環境は完全に.NETが主体となり、C++でコードを書く際も、ライブラリのインフラとしてCOMを選択すると言う事は殆ど無くなりました。

自分なりの答えはあったのですが、「何故COMは失敗したのか」あるいは「何故COMは継続してメジャー足り得なかったのか」と言う事を、参加者に聞いてみたかったのです。

意見としては:

  • 設計思想は凄かった。特に「コンポーネント指向」として、処理系依存の排除やバイナリ互換性を維持する方法論、スレッド安全性をランタイムで担保する抽象性の高さ。
  • 「インターフェイス指向設計」に根差している。COMは実態のクラスに一切タッチしないので、インターフェイス分割設計と言う事に強く意識させられた。
  • やっぱり複雑すぎた。時代が早すぎた。複雑性を軽減する開発環境の補助も、現在と比べて全く不足していた。
  • .NETで複雑な処理も簡単に実装できるようになったので、相対的にCOMのランタイムが複雑に見えるようになった。
  • アパートメントという抽象性の高い概念が理解できなくて躓く。
  • COMを正しく実装するのが大変。VB6か、スクリプトコンポーネント(VBSやJSをCOMコンポーネントとして実行可能な、今考えるとやはり早すぎた感のある技術)でないと辛い。

これだけ複雑であるにも関わらず、COMを知っていて良かったと思える事として、やはり「インターフェイス」を強く意識する事になったことが、数人の意見で一致しました。この経験が、現在の.NETのインターフェイスや、ウェブシステムでのAPI設計の粒度やさじ加減と言った点にとても役立っています。

これは、COMのインターフェイスがほぼ最小のメソッドレベルの粒度で公開可能で、しかもDCOMによってマシン間のRPC呼び出しへの簡単にスケールアップ出来るにも関わらず、その細かい(Chatty)呼び出しが、システムのパフォーマンスと安定性に問題を起こす事が経験として理解できたと言う事です。

なので、COMはとても良い経験を与えてくれた良き技術・通過点でした。そろそろ卒業の頃だと思います。

個人的な思い

このブログでも、COMの解説を試みた記事を書いています(未完)。

COMのアパートメント (1) スレッド同期の隠蔽
COMのアパートメント (2) コンポーネントファサード
COMのアパートメント (3) スレッドの属性とコンポーネント
COMのアパートメント (4) スレッド親和性
COMのアパートメント (5) CoInitializeExは初期化ではない
COMのアパートメント (6) アパートメントの種類はどのように決まるのか

これが未完となっていたのと、内容としてこなれていないのが、ずっと喉の奥に引っかかっていたのです。今回のChalkTalkで完全に咀嚼して、完了させたかった。その目的は達成できたかなと。やってよかったなと思います。

COMは愛すべき対象でしたね。「ナナリィ…」って感じです。

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


追記

ここで扱っていない、COMの重要な要素として以下のものがあります。メモとして残しておきます。

  • インターフェイスの集約(Aggregation)
  • IDispatchインターフェイス・スクリプティングサポート・イベントソースインターフェイス
  • モジュール生存期間の管理(ATLを使うなら、自動でやってくれる)
  • フリースレッドマーシャラー(FTM・FTMを集約すると、ニュートラルスレッドモデルと同じように振る舞える)
  • モニカ(IMoniker)
  • 情報管理(IStorage・IStream・IPropertyBag)
  • インターフェイスポインタのキャッシュや動的生成
  • ActiveX Controlとは
  • COM+とステートレス設計

メモリを使用する、とは

この投稿は「Windows & Microsoft技術 基礎 Advent Calendar 2015」の16日目の記事です。

本稿では、Windows(広く一般のOSでも、基礎的な知識としては適合する)の、「メモリ使用量」の取り扱いについてまとめたものです。特に、コードからメモリを使用するとはどういうことなのかがちょっとでも明らかになれば良いかなと思っています。


普通の人、普通のプログラム、普通のプロセス

.NET環境であったり、C++で各ネイティブなコードであったり、通常プログラムを書くと「ユーザープロセス空間」で動くコードがビルドされます。C#でコードを書けば、newしたりすることで、「どこかにあるメモリ」を適量確保し、それを使用可能にしてくれます。

このメモリ使用量はどのように決まってくるのか? 例えば以下のコード:

var data = new byte[10 * 1000 * 1000];

は、配列の型がbyteなので、要素数=メモリ使用量となる事が分かります(配列管理に使用する付加的なメモリなどを除く)。他の型であれば、それぞれの「ストレージスペース」と呼ばれるサイズだけ使用されます。.NET環境の場合、クラス(参照型)の場合はもっと事情は複雑ですが、なんにせよ、その使用量は予測可能と言えます。

一般にメモリ使用量を見積もるのが大変な作業であると見られている理由の一つは、こういった個々のメモリ確保量(と解放量)がトータルでどのぐらいなのかを積算するのが難しいからだと思われます。上に挙げたような、コードを見ただけで一目瞭然の場合は問題ありませんが、

// ファイルパスを示す正規表現(Dobon.netから)
var regex = new Regex(@"\A(?:[a-z]:|{1})(?:{1}[^{0}]+)+\z");

のような、実装がカプセル化されている場合、そのメモリ使用量はどのぐらいでしょうか? プログラム全体(例えばコンソールアプリケーション)でどのぐらいのメモリ使用量かと言うのは、この例がスケールアップされたものとして考えられます。

他にもメモリ使用量の見積もりを難しくしている要因はあるのでしょうか? 実際、あまり理解されていない、重要な側面があるのではないかと思っています。以下の記事で、このメモリ使用量について掘り下げていきたいと思います。


Windowsにおけるメモリ使用量の指標

procexpmemoryこのスクリーンショットは、Process Explorerのシステムメモリ使用量の表示です。

赤枠で示した部分に、メモリ使用量の統計情報が表示されています。

  • Commit Charge: コミットチャージとは、アプリケーションが「これだけのメモリを使用する」と宣言した総量です。例えば前述のようなコードで示したメモリ確保コードによって、このコミットチャージが増加します。
  • Physical Memory: 物理メモリ使用量です。PCに搭載されているメモリと言えば、物理メモリを指すわけで、実際にこの物理的なメモリをどれだけ使っているのかを示します。
  • Kernel Memory: Windowsカーネルが使用しているメモリ量です。実際にはWindowsシステムだけではなく、デバイスドライバが確保したメモリなども、多くの場合ここに含まれます。
  • Paging: ページングメモリとは、カーネルが自由に移動可能なメモリの事です。

何故、このようにメモリ使用量を示す指標が沢山あるのかと言うと、「仮想メモリ技術」が存在するからです。


仮想メモリ技術とは

仮想メモリ技術は、文字通り、メモリを仮想化します。メモリを仮想化すると何が嬉しいのかと言うと、2つの見方があります。

ページング・スワッピング処理

physical-virtual-mapping搭載されている物理メモリの総量を超えて、更に多くのメモリを使用可能にします。この処理を、ページングやスワッピングと言います。

ページングとは、物理メモリをある一定のサイズ(現代のOSはほぼ4KBを基準とする)で区切り、これを1ページとして、この単位でメモリの管理を行います。このページを仮想メモリ空間に「割り付ける」事で、プロセスがメモリにアクセス出来るようにしています。

この図中に、緑の線で関連付けられたページがあります。このように、単一の物理メモリ空間を、複数のプロセスから同時に参照できるようにマッピングすることも可能です。こうすることで、コストがほぼ0の共有メモリが実現します。

また、わざとらしく、プロセス123と456の仮想メモリ空間の同じ位置にマッピングしていますが、マッピングする位置が同一でなくても構いません。この図で同一の場所にマッピングした例を示したのは、WindowsのPEローダー(EXEやDLLをメモリにロードする処理)がこの手法を使うためです。EXEやDLLをメモリにロードするとき、基本的に「コード」は不変です(自己書き換えと言う手法もありますが)。PEローダーはロード時にリロケーション処理と呼ばれる処理を行って、コードを少し修正します。その結果は、仮想メモリ空間の同じ位置にロードしようとする場合に限り、全く同じように修正されます。と言う事は、既に物理メモリ上にリロケーション処理済みのコードがある場合は、単にそのページを新しいプロセスの同じ仮想メモリ空間にマッピングすれば、再利用出来ることになります。

この図から、プロセスに割り当てられている仮想メモリは、必ずしも物理メモリ上に連続で配置されている必要も、昇順で並んでいる必要もない事にも注意してください。裏を返せば、Windowsカーネルは、物理メモリ上でいつでもこれを好きなように再配置することが出来て、しかも各プロセスはその事を知らない(感知できない)と言う事です。

pagefile搭載されている物理メモリが8GBとします。もし、仮想メモリ技術が無い場合は、どう頑張っても8GBを超えるメモリを使用する事は出来ません。8GBというと、複数のアプリケーション(複数のプロセス)が同時にメモリを要求するので、簡単に使い切ってしまう恐れがあります。

スワッピングは、仮想メモリ技術を使って、ハードディスクなどの外部ストレージに一時的に使用メモリ情報を(ページ単位で)退避したり復元したりする事で、まるで搭載メモリ以上のメモリが使用可能であるかのように見せかけます。この技術は、Windowsカーネルやデバイスドライバが「自動的」に行っているため、アプリケーションがこの操作を直接認識することは出来ません(Process Explorerのように、統計情報として取得することは出来ます)。

スワッピングは、物理メモリとディスクとの間で退避・復元を行う事に注意してください。プロセスから見える仮想メモリ空間から、直接ディスクが見えているわけではありません。これは、MapViewOfFile APIのような、ファイルマッピング技術においても同様です。つまり、スワッピングを成立させるには、必ず対応する物理メモリが必要です(原理上は不可能ではありませんが、実用的ではないため)。

メモリ空間のセパレーション

動作中の各アプリケーションのプロセス同士は、特別な手順を踏まない限りは不可侵です。不可侵とは、互いのプロセスのメモリ空間内を覗き見ることが出来ないことを指します(前述の共有されたページを除く)。

各プロセスは、それぞれが自身に専用に用意されたメモリを「個別に」十分に持っているかのように見えます。まるで、プロセス毎に専用の物理メモリが存在するかのようです。しかし、当たり前ですがそんな事はあり得ません。仮想メモリ技術が、本物の物理メモリから、一部のページを、それぞれのプロセスにマッピングして分け与えているのです。

セパレーションが行われる前提に立つと、アプリケーションコードは常に同じメモリ環境で動くことを想定出来るので、管理が簡略化されます。かつてのプログラムは、同一のメモリ空間に同居する必要があり、メモリを分割して影響を与えないようにするのは、プログラムを書いた人の責任でした。

# リロケーションと呼びますが、現在のコードは自動リロケーションも可能なので、この点での利点は薄れつつあります。
# また、.NETのようにメモリの管理がランタイムによって完全に支配的に行われる環境では、安全のためにメモリ空間を分割するという意味合いは多少薄れています。


メモリの確保とは

ここで、以下のようなC#のコードを使って、Win32のメモリ割り当てAPIを使用してみます。C++でコードを書いても結果はほとんど同じです。Win32 APIを直接使用する事で、.NETのガベージコレクタやヒープマネージャ、Visual C++のランタイムライブラリが行う追加の操作で結果が分かりにくくなることを回避します。

[Flags]
private enum AllocationType : uint
{
	COMMIT = 0x1000,
	RESERVE = 0x2000,
}

[Flags]
private enum MemoryProtection : uint
{
	READWRITE = 0x04,
}

[DllImport("kernel32.dll", SetLastError = true)]
private static extern UIntPtr VirtualAlloc(
	UIntPtr lpAddress,
	UIntPtr dwSize,
	AllocationType flAllocationType,
	MemoryProtection flProtect);

public static unsafe void Main(string[] args)
{
	// [1]: 仮想メモリ領域を割り当てる
	var size = 30UL*1000*1000*1000;
	var p = VirtualAlloc(
		UIntPtr.Zero,
		new UIntPtr(size),
		AllocationType.COMMIT | AllocationType.RESERVE,
		MemoryProtection.READWRITE);
	if (p == UIntPtr.Zero)
	{
		Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
	}

	// [2]: 割り当てたメモリ領域に値を書き込む
	var pBase = (byte*)p.ToPointer();
	var count = size / 4;
	Parallel.ForEach(
		Enumerable.Range(0, 4),
		baseIndex =>
		{
			var pRangeBase = (ulong*)(pBase + count * (ulong)baseIndex);
			for (ulong index = 0; index < (count / 8); index++)
			{
				*(pRangeBase + index) = ulong.MaxValue;
			}
		});
}

このコードは、VirtualAlloc APIを使用して、プロセス内に30GBのメモリを割り当てます。このときのProcess Explorerの様子を見てみます(テストしたシステムは16GBの物理メモリを搭載したWindows 10 x64です)。

※それぞれのタイミングをブレークポイントで一旦停止させています。

procexpmemory2これは、[1]の仮想メモリの割り当てが完了した直後の状態です。Commit Charge及び、上段の”System Commit”のグラフが36GBに跳ね上がっています。VirtualAlloc APIを呼び出すことで、プロセスの「仮想メモリ」の割り当てが行われたのです。物理メモリは16GBなので、明らかにそれ以上のメモリが「存在する」かのように扱われています。実際、VirtualAllocの戻り値には、メモリの読み書きに使用可能な、有効なポインタが返されます。

commit-bytesと、同時に興味深いこともわかります。Physical Memoryの使用量(Total – Available)又は、上段の”Physical Memory”のグラフは、全く変化していないのです。これはつまり、仮想メモリが割り当てられ、ポインタまで取得できて読み書き可能な状態であるのに、全く物理メモリの割り当てに変化がない、より直接的に言うならば「物理メモリは消費していない」のです。

更に、VirtualAllocの処理は一瞬で完了します。まるで、Windowsカーネルは、メモリを割り当てた「ふり」をして、実際には何もしていないかのようです。このような、メモリを割り当てることを宣言する(必ずしも物理メモリに関連づかない)事を「コミットする(Committed bytes)」と言います。

procexpmemory3この状態から、[2]を実行したときの様子がこのスクリーンショットです。

[2]の処理は、VirtualAllocで返されたポインタが、本当に正しい値を返しているのか、念のため実際にメモリにアクセス(ライト)しています。Parallel.ForEachを使っている理由は後で説明しますが、要するに高速化です(同時に4コアで4分割した領域にライトする)。

VirtualAllocが返したポインタが不誠実なものであれば、このコードは何らかの例外(AccessViolationExceptionなど)を発生させて死ぬでしょう。しかし、実際にはエラー無く成功します。但し、「とてつもなく遅い」のです。

  • ① コードの実行が開始されると、”Physical Memory”のグラフが跳ね上がり、16GB近辺まで上昇します。ここで、Windowsシステム全体が操作不能に近いぐらいにスローダウンします。グラフはスパイク状になっていますが、実際には上昇し切った時点でProcess Explorerもログ不能に陥ったようで、操作が回復するまでログが取れなかったためです。
  • ② 操作が回復したとき、Physical Memoryの値が急激に低下していますが、Parallel.ForEachの処理は終わっても、まだプロセスは終了していません。
  • ③ コードの実行が完了してプロセスが終了すると、System Commitのグラフも低下し、プロセス起動前の状態に戻っています。

physical-full仮想メモリへのポインタにアクセスした途端に、物理メモリを消費しはじめ、ほぼ実装メモリ量上限の16GBまで使い切っています。この事から、仮想メモリは実際に使用するまで、物理メモリを消費しない事が分かります。プロセスの仮想メモリ空間には、いつでもアクセス可能なメモリがそこに存在するように見えますが、使うまでは物理メモリにノータッチです。

①の最初に起きたことが、この図です。物理メモリ空間の未使用のページが、全てプロセスの仮想メモリ空間にマッピングされています。物理メモリ空間が全てマッピングされたため、物理メモリの空きが無くなりました。しかし、要求している仮想メモリは30GBなので不足しており、どうやってもこれ以上割り当てることは出来ません。

physical-full2ここで、スワッピングが始まります。物理メモリ上で「使用頻度の低い」ページが検索され、これらのページがディスクに書き出されます(スワップアウトと言う)。使用頻度が低い、とは、例えばですが、LRUアルゴリズムに従って判断したりすることになります。Windowsの場合はこのアルゴリズムは非公開であるので、どのようにページが決定されているのかはわかりません。

この図では、今まさに大量のアクセスが発生しているページではなく、プログラム開始時に確保した物理メモリ(仮想メモリ空間での緑色枠)が選択されたと仮定して、これらがディスクに書き込まれ、対応する物理メモリのページは「未使用(Unused)」としてマークされました。

プロセス内の仮想メモリ空間で、スワップアウトされたページ(緑色枠)は、「そこにメモリは存在する事になっているが、実際はディスクに存在していて、対応する物理メモリは存在しない」状態です。しかし、実害はありません。プロセス内のコードがこのページにアクセスしないうちは、この事実を知る者は(プロセス内には)居ないからです。この状態は、丁度VirtualAllocでメモリを確保した直後の状態に似ています。

physical-full3さて、これで幾らか物理メモリに空きが出来たので、仮想メモリ空間に割り当てることが出来ました。

が、まだまだ足りない…

physical-full4そうすると、「更に」、物理メモリ上で「使用頻度の低い」ページが検索され、これらのページがディスクに書き出されます。この場合、明らかに使用頻度が低いページはもうないのですが、直近ではスタート直後にライトしたページが、使用頻度が低いと言えなくもないです。もう一度言いますが、どのページが使用頻度が低いと判断するのかは非公開のため、実際にはここで述べたような判断基準ではない可能性がありますが、とにかく何らかの方法で犠牲(?)となるページを決定し、これをディスクに書き出してスワップアウトします。

仮にですが、このアルゴリズムがアホだったりすると、現在アクセスが頻繁に発生しているページをスワップアウトし、すぐにアクセスされてスワップインするという、システムがとてつもなくパフォーマンス低下を引き起こす原因となります。だから、どのページをスワップアウトさせるのかという決定ロジックは非常に重要です。

# 例えば、単にファイルがマッピングされているような状態であれば、復元はファイルを再マッピングするだけなので、ページの解放はディスクにスワップアウトを必要としません。このようなページがあれば、優先的に解放することが出来ます。EXEやDLLであれば、固定データやリソースデータ(COFFの.rdataや.rsrcセクション)などが該当します。

あとは、これをひたすら繰り返し、プロセスが仮想メモリにアクセスするの止める(実行を完了する)まで、仮想メモリへのアクセスを満足させればよいのです。

結局、問題となるのはどこでしょうか? 物理メモリをスワップアウトさせてディスクに書き込んだり、スワップインで復元させる時、ここが最も時間のかかる処理であるため、スワップ処理で忙しくなると、システム全体の応答性が悪くなるのです。

メモリ確保のまとめ

これが、メモリの仮想化による恩恵で、「不要」な、使用中のメモリの情報を一旦ディスクに退避し、これにより物理メモリ空間を未使用状態にし、新たに必要になった仮想メモリ空間に再配置して使用可能にする操作です。この操作はWindowsカーネルが内部で行っているので、アプリケーションは全くそんな事を感知せず、問題なく動作を継続します。

但し、ディスクへの退避と、あとで復元するために読み出す操作は、当然非常に時間がかかります。それが、システムがスローダウンした原因です。また、Parallel.ForEachで書き込み処理を並列化したのは、この退避処理を忙しくてそのほかの処理を行う余裕がないように仕向けるためです。もし、ディスクが非常に高速であった場合、システムがスローダウンせず、更にはスワッピングが高速に行われることで、物理メモリを上限まで消費しない可能性があったためです。

ここまでの実験で、物理メモリ(Physical Memory)の使用量を眺めていても、アプリケーションが消費するメモリ量というのは全く予測出来ない事が分かると思います。この結果から②がメモリ消費の見積もりとして何を意味するのかを考えるのは、無意味であることも想像できます。②で何故物理メモリ使用量が低下したのか、は、アプリケーションのメモリ使用量の考察とは何の関係もないからです。

# 大量の物理メモリを使用した後に、物理メモリのページが解放される挙動は、Windowsカーネルの最適化処理によるものと考えられます。メモリ不足が深刻な状況では、素早く空き物理メモリを確保した方が良いと思われるため、積極的に先回りして解放する手法が考えられます。


ワーキングセットと言う名の亡霊

前節のような、超高負荷アプリケーションが動作していても、ほかのプロセスが異常終了することはありません。もちろん、スワッピングに使用するディスク容量が残っていれば、の話です。しかし、あるプロセスがこのように大量のページに対して読み書きしていたら、ほかのプロセスに割り当てられているページはどうなってしまうのでしょうか?

スワッピングアルゴリズムの例を紹介しましたが、この動作がシステムの全てのプロセスに対して行われると考えればわかります。つまり、「システム全体で見て」使用頻度の低いページをスワップアウトして行くのです。例えば、Windowsのサービスプロセスは、用事があるまで待機したままである事がほとんどですが、このようなプロセスの仮想メモリは、対応する物理メモリがいつもスワップアウトしていても構いません。何故なら「時々しか動作せず、その時にだけページにアクセスされる」からです。

しかし、現在のWindowsは、そもそも物理メモリに余裕がある場合は、積極的にスワップアウトは行わないようです。スワップアウトするにはディスクにアクセスする必要があります。不必要と判断されたときは「暇」だと考えられるので、スワップアウト自体問題になりませんが、いざ動き始めた時にページがスワップアウトされているとスワップインしなければならず、これは高コストであるため、応答性に影響を与えます。

workingsetWindowsのメモリ使用量統計に「ワーキングセット」という数値もあります。これはProcess Explorerでワーキングセットを表示させてみたものです。標準のタスクマネージャでも「メモリ」というあやふやなカラム名で表示されるので、この数値がプロセスが使用するメモリ使用量だと思っている方も多いと思います。

前節の検証により、物理メモリの使用量を見ていても、アプリケーションのメモリ使用量は分からないという話をしましたね?

workingset-detailこのスクリーンショットは、ProcessExplorerで特定のプロセスのプロパティを表示させたものです。赤枠内にワーキングセットの統計値が出ています。が、この枠をよく見て下さい。”Physical Memory”と書いてあります。

ワーキングセットとは、ある時間において、そのプロセスの仮想メモリにマッピングされている物理メモリの総容量(総ページ数)です。つまり、この値は「物理メモリ量」を表しているのですよ!!!

「一体何を見ていたんだ俺たちは…」と思っていただけましたか?

# ほとんどこれが言いたかった (´Д`)

workingset3実際、先ほどのテストコードを実行したときに、テストコード「以外」の各プロセスのワーキングセットの推移を見てみると、とても面白い現象が目撃できます。

これは、テストコード実行直後のスクリーンショットですが、各プロセスのワーキングセットが軒並み低い値になっています。これは、テストコードのプロセスがあまりに大量の物理メモリを必要としたため、ほかのプロセスに割り当てられていた物理メモリもスワップアウトされて流用された結果です。かろうじて残っているワーキングセットは、「頻繁に使用されているページ」と判断されたのでしょう。

# 例えば、ウインドウプロシージャなどのコールバック関数が配置されたページは、コールバックが発生したらすぐに動作する必要があるため、対応するページは保持される可能性が高いでしょう。他にもロックされたページの可能性はありますが、ここでは述べません。

さて、ワーキングセットの統計値ですが、まあ、全く無意味な指標と言うわけでもありません。この値が普段から大きいと言う事は、メモリ参照の局所性が低い可能性があります。テストコードの場合は、30GBのメモリを丸々アクセスしているので仕方ありませんが、特に思い当たる節もなくワーキングセットが大きい場合、効率が良くないアルゴリズムを使っていたりすることが考えられます。

大げさなたとえですが、配列で保持すれば済むようなデータを、わざわざリンクリストで保持したりすると、ページにまたがってデータが分散する可能性があるので、少ない消費量でも複数ページをあっという間に消費する可能性があるのです。

ワーキングセットが大きいと、それだけスワッピングが発生したときのコストが高くなります。また、CPU内のキャッシュにも収まらなくなり、パフォーマンスが低下する原因となります。だから、その予兆を見極める目的で、ワーキングセットを使う事が出来ます。


メモリ使用量を見積もる、と言う事

私たちはシステム導入に際して、PCやサーバーに必要なの物理メモリ量の見積もりをするわけですが(実際のところ、あまりやりたくはない)、一体何を基準に見積もるのか、と言う事は十分考える必要があります。

見積もりの方法の一つとして、原則的にスワッピングやページングを発生させないほどの物理メモリを搭載する、と考える方法があります。これならば、System CommitsがPhysical Memoryを超えないぐらいに物理メモリを乗せれば良いわけで、基準としてはアリだと思います。しかし、この基準を適用すると、システムの直観的な消費量からは程遠い程の大量のメモリが必要になるかもしれません。それは、Committed bytesよりもプロセスのワーキングセットが極端に小さくなるような、メモリ参照局所性の高いアプリケーションに当てはまります。

自分が見積もりを行なおうとしているアプリケーションは、果たして前者のような極端なメモリ量依存の高いアプリケーションなのか、それとも、メモリ参照局所性の高いアプリケーションなのか? あるいはその中間なのか? 今まで示した仮想メモリの処理を前提に、それはどうやったら分かると思いますか?

どうしても見積もってくれと言われれば、そりゃ、計測するしかないですよね、アプリケーションそのものを実環境に近い環境で動かすか、あるいはそれに近しいシミュレーションコードで(それって、コスト掛かる作業ですよね。検証に見合うの?)。なので、私は、前提も明らかではない確定的なメモリ使用量の見積もりを見ると疑ってかかります。

「なにこの数字!どっから来たんだよっ!!」


次は、__yossy__さん、おねがいします!

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)へつづく