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

JavaでLINQ03

$
0
0
昨日のHokuriku.Scala 第一回 : ATNDでだいぶ読み替え慣れてきた。一通りメソッドを用意。
PLINQ → Java のポーティングと、PLINQ → Scala のポーティングはどう考えても後者のほうが楽そう。
Scala で書いてコンパイルしたクラスを GAE に送り込んで、もし動けばこのシリーズはとりあえず終われるかな?

メイン:

import java.util.List;

public class Main {

public static void main(String[] args) throws InterruptedException {

System.out.println("map Result: ");
for (Integer i : Enumerable.range(1, 3)
.map(
new Func1<Integer, Integer>(){
public Integer _(Integer i) {
return i * 2;
}
}
)) {
System.out.println(i);
}
// map Result:
// 2
// 4
// 6


System.out.println("filter Result: ");
for (Integer i : Enumerable.range(1, 4)
.filter(
new Func1<Integer, Boolean>() {
public Boolean _(Integer i) {
return i % 2 == 0;
}
}
)) {
System.out.println(i);
}
// filter Result:
// 2
// 4


System.out.println("takeWhile Result: ");
for (Integer i : Enumerable.range(1, 5)
.takeWhile(
new Func1<Integer, Boolean>() {
public Boolean _(Integer i) {
return i <= 3;
}
}
)) {
System.out.println(i);
}
// takeWhile Result:
// 1
// 2
// 3


System.out.println("foldLeft Result: ");
Integer result = Enumerable.range(1, 10)
.foldLeft(0)
._(
new Func2<Integer, Integer, Integer>(){
public Integer _(Integer sum, Integer a) {
return sum + a;
}
}
);
System.out.println(result);
// foldLeft Result:
// 55


System.out.println("fibs Result: ");
for (Integer i : fibs().take(10)) {
System.out.println(i);
}
// fibs Result:
// 0
// 1
// 1
// 2
// 3
// 5
// 8
// 13
// 21
// 34
}

private static Enumerable<Integer> fibs() {

final Enumerable<Integer> initialValues =
Enumerable.yield(
0,
1
);

final Enumerable<Integer> fibsEnumeralbe =
new Enumerable<Integer>(
new Func0<Iterable<Integer>>() {
public Iterable<Integer> _() {
return fibs();
}
}
);

return initialValues.concat(
Enumerable.zip(
new Func0<Iterable<Integer>> () {
public Iterable<Integer> _() {
return fibsEnumeralbe;
}
},
new Func0<Iterable<Integer>> () {
public Iterable<Integer> _() {
return fibsEnumeralbe.skip(1);
}
}
)
.select(
new Func1<List<Integer>, Integer> () {
public Integer _(List<Integer> arg0) {
return arg0.get(0) + arg0.get(1);
}
}
)
);
}
}




関数オブジェクトの替わりのインターフェース達:

public interface Func0<TResult> {
TResult _();
}

public interface Func1<T, TResult> {
TResult _(final T arg0);
}

public interface Func2<T0, T1, TResult> {
TResult _(final T0 arg0, final T1 arg1);
}


Adaptor クラス:

import java.util.Iterator;


public class IteratorAdaptor<T> implements Iterator<T> {

@Override
public boolean hasNext() {
return false;
}

@Override
public T next() {
return null;
}

@Override
public void remove() {
}

public Iterable<T> asIterable() {
final IteratorAdaptor<T> _this = this;
return new Iterable<T>() {
@Override
public Iterator<T> iterator() {
return _this;
}
};
}

}


Enumerable クラス(これが本体):

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;


public class Enumerable<T> implements Iterable<T> {

private Func0<Iterable<T>> func;

public Enumerable(Func0<Iterable<T>> func) {
this.func = func;
}

public <TResult> Enumerable<TResult> select(final Func1<T, TResult> func) {

final Enumerable<T> _this = this;

final class IteratorImpl extends IteratorAdaptor<TResult> {

private Iterator<T> iterator;

@Override
public boolean hasNext() {
if (iterator == null) {
iterator = _this.func._().iterator();
}
return iterator.hasNext();
}

@Override
public TResult next() {
return func._(iterator.next());
}

}

return new Enumerable<TResult>(
new Func0<Iterable<TResult>>() {
public Iterable<TResult> _() { return new IteratorImpl().asIterable(); }
}
);
}

public <TResult> Enumerable<TResult> map(final Func1<T, TResult> func) {
return select(func);
}

public <TResult> Enumerable<TResult> selectMany(final Func1<T, Enumerable<TResult>> selector) {

final Enumerable<T> _this = this;

final class IteratorImpl extends IteratorAdaptor<TResult> {

private Iterator<T> iterator;
private Iterator<TResult> selectorIterator;

@Override
public boolean hasNext() {
if (iterator == null) {
iterator = _this.func._().iterator();
}
boolean hasNext;
if (selectorIterator == null) {
hasNext = iterator.hasNext();
if (hasNext) {
selectorIterator = selector._(iterator.next()).iterator();
hasNext = selectorIterator.hasNext();
}
else {
// 列挙終了。
}
}
else {
hasNext = selectorIterator.hasNext();
if (!hasNext) {
selectorIterator = null;
hasNext = hasNext();
}
}

return hasNext;
}

@Override
public TResult next() {
return selectorIterator == null ? null : selectorIterator.next();
}

}

return new Enumerable<TResult>(
new Func0<Iterable<TResult>>() {
public Iterable<TResult> _() { return new IteratorImpl().asIterable(); }
}
);
}

public Enumerable<T> where(final Func1<T, Boolean> predicate) {

final Enumerable<T> _this = this;

final class IteratorImpl extends IteratorAdaptor<T> {

private Iterator<T> iterator;

@Override
public boolean hasNext() {
if (iterator == null) {
iterator = _this.func._().iterator();
}
boolean hasNext;
while ((hasNext = iterator.hasNext())) {
if (predicate._(iterator.next())) {
break;
}
}
return hasNext;
}

@Override
public T next() {
return iterator.next();
}

}

return new Enumerable<T>(
new Func0<Iterable<T>>() {
public Iterable<T> _() { return new IteratorImpl().asIterable(); }
}
);
}

public Enumerable<T> filter(final Func1<T, Boolean> predicate) {
return where(predicate);
}

public Enumerable<T> take(final int count) {

final Enumerable<T> _this = this;

final class IteratorImpl extends IteratorAdaptor<T> {

private int index;
private Iterator<T> iterator;

@Override
public boolean hasNext() {
if (iterator == null) {
iterator = _this.func._().iterator();
}
return index++ < count ? iterator.hasNext() : false;
}

@Override
public T next() {
return iterator.next();
}

}

return new Enumerable<T>(
new Func0<Iterable<T>>() {
public Iterable<T> _() { return new IteratorImpl().asIterable(); }
}
);
}

public Enumerable<T> takeWhile(final Func1<T, Boolean> predicate) {

final Enumerable<T> _this = this;

final class IteratorImpl extends IteratorAdaptor<T> {

private Iterator<T> iterator;

@Override
public boolean hasNext() {
if (iterator == null) {
iterator = _this.func._().iterator();
}
boolean hasNext = iterator.hasNext();
return hasNext && predicate._(iterator.next());
}

@Override
public T next() {
return iterator.next();
}

}

return new Enumerable<T>(
new Func0<Iterable<T>>() {
public Iterable<T> _() { return new IteratorImpl().asIterable(); }
}
);
}

public Enumerable<T> skip(final int count) {

final Enumerable<T> _this = this;

final class IteratorImpl extends IteratorAdaptor<T> {

private Iterator<T> iterator;

@Override
public boolean hasNext() {
if (iterator == null) {
iterator = _this.func._().iterator();
for (int counter = 0; counter < count && iterator.hasNext(); counter++);
}
return iterator.hasNext();
}

@Override
public T next() {
return iterator.next();
}
}

return new Enumerable<T>(
new Func0<Iterable<T>>() {
public Iterable<T> _() { return new IteratorImpl().asIterable(); }
}
);
}

public Enumerable<T> concat(final Enumerable<T> enumerable) {

final Enumerable<T> _this = this;

final class IteratorImpl extends IteratorAdaptor<T> {

private Iterator<T> iterator;
private boolean nextIteratorInitialized;

@Override
public boolean hasNext() {
if (iterator == null) {
iterator = _this.func._().iterator();
}
boolean hasNext = iterator.hasNext();
if (!hasNext && !nextIteratorInitialized) {
iterator = enumerable.iterator();
nextIteratorInitialized = true;
hasNext = iterator.hasNext();
}
return hasNext;
}

@Override
public T next() {
return iterator.next();
}
}


return new Enumerable<T>(
new Func0<Iterable<T>>() {
public Iterable<T> _() { return new IteratorImpl().asIterable(); }
}
);
}

public Func1<Func2<T, T, T>, T> foldLeft(final T initialValue) {

final Enumerable<T> _this = this;

return new Func1<Func2<T, T, T>, T>() {
@Override
public T _(Func2<T, T, T> arg0) {
T lastValue = initialValue;

for (T current : _this.func._()) {
lastValue = arg0._(lastValue, current);
}

return lastValue;
}
};
}


@Override
public Iterator<T> iterator() {
return this.func._().iterator();
}

public static <T> Enumerable<T> iterate(final T initialValue, final Func1<T, T> func) {

final class IteratorImpl extends IteratorAdaptor<T> {

private boolean initialized;
private T next;

@Override
public boolean hasNext() {
if (!initialized) {
next = initialValue;
initialized = true;
}
else {
next = func._(next);
}
return true;
}

@Override
public T next() {
return next;
}

}

return new Enumerable<T>(
new Func0<Iterable<T>>() {
public Iterable<T> _() { return new IteratorImpl().asIterable(); }
}
);
}

public static Enumerable<Integer> range(final int start, final int count) {
return iterate(
start,
new Func1<Integer, Integer> () {
public Integer _(Integer arg0) {
return arg0 + 1;
}
}
)
.take(count);
}

public static <T> Enumerable<List<T>> zip(final Func0<Iterable<T>> func1, final Func0<Iterable<T>> func2) {

final class IteratorImpl extends IteratorAdaptor<List<T>> {

private Iterator<T> iterator1;
private Iterator<T> iterator2;

@Override
public boolean hasNext() {
if (iterator1 == null) {
iterator1 = func1._().iterator();
}
if (iterator2 == null) {
iterator2 = func2._().iterator();
}
boolean hasNext1 = iterator1.hasNext();
boolean hasNext2 = iterator2.hasNext();
return hasNext1 || hasNext2;
}

@Override
public List<T> next() {
List<T> tuple = new ArrayList<T>();
tuple.add(iterator1.next());
tuple.add(iterator2.next());
return tuple;
}
}

return new Enumerable<List<T>>(
new Func0<Iterable<List<T>>>() {
public Iterable<List<T>> _() { return new IteratorImpl().asIterable(); }
}
);
}

public static <T> Enumerable<T> yield(final Object ... objects) {

final class IteratorImpl extends IteratorAdaptor<T> {

private int index = -1;

@Override
public boolean hasNext() {
if (++index < objects.length) {
return true;
}
else {
return false;
}
}

@SuppressWarnings("unchecked")
@Override
public T next() {
if (objects[index] instanceof Func0) {
return (T)((Func0)objects[index])._();
}
else {
return (T)objects[index];
}
}

}

return new Enumerable<T>(
new Func0<Iterable<T>>() {
public Iterable<T> _() { return new IteratorImpl().asIterable(); }
}
);
}

public static <T> Enumerable<T> yieldWhile(final Func0<T> func) {
return yieldWhile(
func,
new Func1<T, Boolean>() {
public Boolean _(T arg0) {
return true;
}
}
);
}

public static <T> Enumerable<T> yieldWhile(final Func0<T> func, final Func1<T, Boolean> predicate) {
return iterate(
func._(),
new Func1<T, T>(){
public T _(T arg0) {
return func._();
}
}
)
.where(predicate);
}
}


モナドを C# で 01

$
0
0
C# で Maybe モナドっぽいものと State モナドっぽいもの。
型構成子とか関数の拡張が自動化できればよかったけれど、とりあえずごりごり記述。
確かに使う側での副作用はなさそう。

// Haskell の >>= がなんて呼ばれているかわからない…orz。とりあえず束縛。


using System;
using System.Data;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

class Program
{
static void Main(string[] args)
{
var result1 =
new Maybe<string[]>(new string[] { "012", "345", null, "678" })
.Bind(ss => ss.Skip(1))
.Bind(ss => ss.Skip(1).ElementAt(0))
.Bind(s => Regex.Match(s, "[0-3]+"))
.Bind(m => m.Value);

Console.WriteLine("result1 = " + result1.Apply());
// result1 =

var result2 =
new Maybe<string[]>(new string[] { "012", "345", "3", "678" })
.Bind(ss => ss.Skip(1))
.Bind(ss => ss.Skip(1).ElementAt(0))
.Bind(s => Regex.Match(s, "[0-3]+"))
.Bind(m => m.Value);

Console.WriteLine("result2 = " + result2.Apply());
// result2 = 3

var result3 =
new State<string, string>("123456789")
.Bind<string, int>(t => new Tuple<string, int>(t.State, t.State.Length))
.Bind<int, string>(t => new Tuple<int, string>(t.State, t.State.ToString()))
.Bind<string, string>(t => new Tuple<string, string>(t.State, t.State + "010101"));

Console.WriteLine("result3 = " + result3.Apply());
// result3 = 9010101


Console.ReadLine();
}
}


// Maybe モナド?
public class Maybe<T>
where T : class
{
private Func<T> func;

public Maybe(T value)
{
this.func = new Func<T>(() => value);
}

public Maybe(Func<T> func)
{
if (func == null) throw new ArgumentNullException("func");
this.func = func;
}

public Maybe<S> Bind<S>(Func<T, S> func)
where S : class
{
if (func == null) throw new ArgumentNullException("func");
return new Maybe<S>(new Func<S>(() => { var x = this.func(); return x == null ? null : func(x); }));
}

public T Apply()
{
return this.func();
}
}



// State モナド?
public class State<V1, S1>
{
private Func<V1, Tuple<V1, S1>> func;
public State(S1 s)
{
this.func = new Func<V1, Tuple<V1, S1>>(value => new Tuple<V1, S1>(value, s));
}

public State(Func<V1, Tuple<V1, S1>> func)
{
if (func == null) throw new ArgumentNullException("func");
this.func = func;
}

public State<V2, S2> Bind<V2, S2>(Func<Tuple<V1, S1>, Tuple<V2, S2>> func)
{
if (func == null) throw new ArgumentNullException("func");
return new State<V2, S2>(
new Func<V2, Tuple<V2, S2>>(value => { var t = func(this.func(default(V1))); return new Tuple<V2, S2>(t.Value, t.State); }));
}

public S1 Apply()
{
return this.func(default(V1)).State;
}
}


// C# にも Tuple 欲しいかも。
public struct Tuple<V1, S1>
{
private V1 value;
private S1 state;

public Tuple(V1 value, S1 state)
{
this.value = value;
this.state = state;
}

public V1 Value { get { return this.value; } set { this.value = value; } }
public S1 State { get { return this.state; } set { this.state = value; } }
}

Java で Continuation 01

$
0
0
なんとか目処が付いた。GAE/Jでどうやって動くかが楽しみ。簡単なサンプルで正常系1パス通った程度の完成度。あとは、いいデバッガ探してパターンつぶすだね。


ちなみに、Java の勉強も兼ねていたが、「バイトコードが読める」ようになるというのは。。。(^^ゞ



処理について
・Java では、肝になる goto をコンパイラが禁止しているため、バイトコードレベルで処理を埋め込む必要がある。
・バイトコードを弄くるためのツールは、AspectJ、Javassist、BCEL、ASM などが候補に。最終的に ASM に落ち着く。
・大きな流れは、
-Annotation や Serializable インターフェースを目印に、継続に必要な、現在のコンテキストの変数をかき集める。
-新たなインナークラスに保持させ、まとめてシリアライズ。
-継続時は、上記の逆を行う。callcc で読み込み先に飛ぶための switch & goto 文を展開。


処理埋め込み前

package com.blogspot.urasandesu;

import com.blogspot.urasandesu.asm.annotation.Target;

public class ContinuationTest {

private Continuation c;
private boolean first;

@Target
public void hoge() {

if (c != null) {
c.callcc();
}

int x = 0;
int y = 0;

if (Math.random() < 0.5) {
x = 50;
y = 50;
}
else {
x = 100;
y = 100;
}

c = new Continuation();
if (!first) {
first = true;
c.save();
}

System.out.println("x: " + x + ", y: " + y);
}

}



処理埋め込み後

package com.blogspot.urasandesu;

import java.io.Serializable;

import org.objectweb.asm.Opcodes;

import com.blogspot.urasandesu.asm.annotation.Target;
import com.blogspot.urasandesu.asm.util.MethodKey;

public class ContinuationTest {

private Continuation c;
private boolean first;

public static final class _306F51 implements Serializable {
public boolean first;
public int x;
public int y;
}

@Target
public void hoge() {

_306F51 _306F51 = new _306F51();

if (c != null) {
c.callcc();
switch (c.getIndex()) {
case 0:
goto LABEL0; // 実際はコンパイルエラー。イメージ。
}
}

int x = 0;
int y = 0;

if (Math.random() < 0.5) {
x = 50;
y = 50;
}
else {
x = 100;
y = 100;
}

c = new Continuation();
_306F51.first = first;
_306F51.x = x;
_306F51.y = y;
if (false) {
LABEL0: {
_306F51 =
(_306F51)c.load(
new ContinuationKey(
"com/blogspot/urasandesu/ContinuationTest",
new MethodKey(
Opcodes.ACC_PUBLIC,
"hoge",
"()V",
null,
null
)
)
);
first = _306F51.first;
x = _306F51.x;
y = _306F51.y;
}
}
if (!first) {
first = true;
c.save(
new ContinuationKey(
"com/blogspot/urasandesu/ContinuationTest",
new MethodKey(
Opcodes.ACC_PUBLIC,
"hoge",
"()V",
null,
null
)
),
_306F51
);
}

System.out.println("x: " + x + ", y: " + y);
}

}

Eclipse で Processing

$
0
0
自分用備忘録。開発環境はいつも使ってるのを使いたいよね。

Processingは Java そのもの。画像を簡単に扱えるようにしたライブラリ、ぐらいの認識のほうがいいのかも。
そうなると、外とのやりとりに Apache Software Foundationの成果を使いたくなるでしょうし、さらにがりがり書こうとすると、やれバージョン管理やら、やれパッケージングやら、やれ入力補完やら欲しくなってくる…。ということで、Eclipse、使います。
設定方法の公式ページはこちら。基本的にざっくり訳しただけというメモ書きです。(^^ゞ
- Processing in Eclipse \ Processing 1.0
 http://processing.org/learning/eclipse/

1. Eclipse を Download してインストール
http://www.eclipse.org/downloads/から Eclipse IDE for Java Developers をダウンロードする。ダウンロードしたファイルは好きなところに解凍。

2. 新しいプロジェクトを作成
Welcome 画面閉じて、[Package Explorer]で右クリック、[New]-[Java Project]。プロジェクト名はとりあえず「TestProcessing」で。

3. Processing のライブラリをインポート
Processing インストールした時にできる lib フォルダから必要な jar ファイルをビルドパスに追加する。先ほど作成したプロジェクトを右クリック、[Build Path]-[Add External Archives...]。lib フォルダにある core.jar を選択する。
ちなみに core.jar 、GNU LGPL なんでその辺り気になる方はお気を付けを。

4. クラスの作成、コードの記述
サンプルコードを書いてみる。

import processing.core.PApplet;


public class MyProcessingSketch extends PApplet {

private static final long serialVersionUID = -1530381177827393671L;

@Override
public void draw() {
stroke(255);
if (mousePressed) {
line(mouseX, mouseY, pmouseX, pmouseY);
}
}

@Override
public void setup() {
size(200, 200);
background(0);
}

}


適当な解説とか。

  • processing.core.* パッケージに必要なクラスが色々入ってる。適宜 Import する(Ctrl+Shift+O!)。

  • processing.core.PApplet クラスを extends する。Processing の全ての機能が利用できるようになる。

  • setup(), draw() メソッドを Override する(Alt+Shift+S, v!)。



5. 実行
[Package Explorer]で作成したクラスを右クリック、[Run As]-[Java Applet]。
普通の Java アプリケーションとして実行したい場合は、以下の通り main メソッドを追加して、[Run As]-[Java Application]。

import processing.core.PApplet;


public class MyProcessingSketch extends PApplet {

private static final long serialVersionUID = 8801683180749406362L;

@Override
public void draw() {
stroke(255);
if (mousePressed) {
line(mouseX, mouseY, pmouseX, pmouseY);
}
}

@Override
public void setup() {
size(200, 200);
background(0);
}

public static final void main(String[] args) {
PApplet.main(new String[] { "--present", "MyProcessingSketch" });
}
}



複数のクラスを使う場合
このサンプルスケッチを Eclipse で作る場合、Processing のスケッチで使われてる Stripe クラスは実際はインナークラスとして定義されてる。
こんな感じで。

import processing.core.PApplet;

public class MyProcessingSketch2 extends PApplet {

private static final long serialVersionUID = 978193620686319475L;

// An array of stripes
Stripe[] stripes = new Stripe[50];

@Override
public void setup() {
size(200, 200);
// Initialize all "stripes"
for (int i = 0; i < stripes.length; i++) {
stripes[i] = new Stripe();
}
}

@Override
public void draw() {
background(100);
// Move and display all "stripes"
for (int i = 0; i < stripes.length; i++) {
stripes[i].move();
stripes[i].display();
}
}

class Stripe {
float x; // horizontal location of stripe
float speed; // speed of stripe
float w; // width of stripe
boolean mouse; // state of stripe (mouse is over or not?)

Stripe() {
x = 0; // All stripes start at 0
speed = random(1); // All stripes have a random positive speed
w = random(10, 30);
mouse = false;
}

// Draw stripe
void display() {
fill(255, 100);
noStroke();
rect(x, 0, w, height);
}

// Move stripe
void move() {
x += speed;
if (x > width + 20)
x = -20;
}
}
}



バージョン管理とかする場合、複数のファイルに分割して、手を入れる範囲を少なくしておきたいと思う。分割する場合は、親クラスを生成時に渡してあげるといい。こんな感じで。

MyProcessingSketch2.java

import processing.core.PApplet;

public class MyProcessingSketch2 extends PApplet {

private static final long serialVersionUID = -3546131844473457390L;

// An array of stripes
Stripe[] stripes = new Stripe[50];

@Override
public void setup() {
size(200, 200);
// Initialize all "stripes"
for (int i = 0; i < stripes.length; i++) {
stripes[i] = new Stripe(this);
}
}

@Override
public void draw() {
background(100);
// Move and display all "stripes"
for (int i = 0; i < stripes.length; i++) {
stripes[i].move();
stripes[i].display();
}
}
}



Stripe.java

import processing.core.PApplet;


public class Stripe {

PApplet parent;

float x; // horizontal location of stripe
float speed; // speed of stripe
float w; // width of stripe
boolean mouse; // state of stripe (mouse is over or not?)

Stripe(PApplet parent) {
this.parent = parent;
x = 0; // All stripes start at 0
speed = parent.random(1); // All stripes have a random positive speed
w = parent.random(10, 30);
mouse = false;
}

// Draw stripe
public void display() {
parent.fill(255, 100);
parent.noStroke();
parent.rect(x, 0, w, parent.height);
}

// Move stripe
public void move() {
x += speed;
if (x > parent.width + 20)
x = -20;
}
}



よし、次は Eclipse で Gainerするです。

Eclipse で Gainer

$
0
0
自分用備忘録。開発環境はいつも使ってるのを使いたいよね その2。

Gainerは、Flash、Max/MSP、Processingで操作できる I/O モジュール。

技術者じゃない人/コンピュータ初心者の人が面白い!便利!だと思ってくれるようなものを作りたいって時、人との I/F が難しい。やっぱりマウスとキーボードの操作って難しいし、直感的でないこともしばしばある。
私は仕事でソフトウェアをやっていますが、組み込みだとかは学生時代に工学実験した程度。
そんな人間が、こんな I/F があると簡単/便利なんだろうなと思った時に、結構手軽にプロトタイプを作れそうなのがこの Gainerです。

タイトルにもあるように、今回は Eclipse で Processing使って Gainer操作してみます。

1. Gainer ドライバのインストール
チュートリアルを参考に USB ドライバをインストールしておく。

2. Eclipse で Processing を使えるようにする
前回のエントリを参考に、Eclipse で Processing を使えるようにしておく。
※作成したサンプルプロジェクト「TestProcessing」をそのまま使って行きます。

3. Processing 用 Gainer ライブラリを Download
Processing 用 Gainer ライブラリをここからダウンロードする。適当な場所で解凍。

4. ビルドパスの追加
ライブラリを解凍した時にできる libraries\gainer\library フォルダから必要な jar ファイルをビルドパスに追加する。
プロジェクトを右クリック、[Build Path]-[Add External Archives...]。libraries\gainer\library フォルダにある gainer.jar, RXTXcomm.jar を選択する。

5. ライブラリのインポート
Gainer ライブラリを解凍した時にできる libraries\gainer\library フォルダから必要なライブラリをプロジェクトにインポートする。
プロジェクトを右クリック、[Import...]。
開いたダイアログで[General]-[File System]を選択し、[Next>]。
libraries\gainer\library フォルダにある librxtxSerial.jnilib, librxtxSerial.so, rxtxSerial.dll を選択。
[Into folder]を出力フォルダに変更し(出力フォルダがデフォルトのままであれば、TestProcessing/bin)、[Finish]。

6. クラスの作成、コードの記述
コード例1:buttonをとりあえず記述。

Sketch01.java

import processing.core.PApplet;
import processing.gainer.Gainer;

public class Sketch01 extends PApplet {

private static final long serialVersionUID = 8419122392829951234L;

Gainer gainer;

@Override
public void setup() {
size(200, 200);
gainer = new Gainer(this);
}

@Override
public void draw() {
background(0);
if (gainer.buttonPressed) {
background(255);
}
}

}



7. 実行
[Package Explorer]で作成したクラスを右クリック、[Run As]-[Java Applet]。


よし、次は Eclipse で Arduinoするです。

Eclipse で Arduino 01

$
0
0
自分用備忘録。開発環境はいつも使ってるのを使いたいよね その3。

Arduinoは、フィジカルコンピューティングのためのオープンソースプラットフォーム。

以前の Gainer同様、私のような組み込み初心者でも、気軽にプロトタイピングを行えるのが魅力。ただ、違うのは、作ったプログラムをボードに書き込んで動かすってこと。この辺は心理的に大きな壁になると思います。なぜなら、デバッグが大変だから。

話が一旦飛びますが、私、スマートフォンアプリの開発技術も必要で、 Androidの勉強もやっているのですが、私のような組み込み初心者が、最初に驚くのは、実際に実機に繋いでデバッグできるってところだと思います。
Eclipse でデバッグ実行したら、ちょいちょいブレークポイントで止めて、標準出力にデータ流しながら、センサー用の閾値をとりあえず決めたりできるのはすごい衝撃でした。

この件があったもので、Arduinoを知って初めて触り、いくつかサンプルを作ったところで、参考書通りに作るのはいいけど、この先、この IDE でどうしよう…と。

そこで、タイトルにもあるように、今回も Eclipse で Arduino操作してみます。

こちらのページを参考に作成しています。偉大な先人に感謝です!
- Arduino playground - Eclipse
 
http://www.arduino.cc/playground/Code/Eclipse
- exploration ≫ Blog Archive ≫ Arduino in Eclipse
 
http://robertcarlsen.net/2009/10/31/arduino-in-eclipse-989
- Debugging - AVR-Eclipse
 
http://avr-eclipse.sourceforge.net/wiki/index.php/Debugging
- 小ネタ[AVR mkII Lite (JTAGICE mkII / DebugWIRE clone)を使ってみた。 (2008-08-10)]
 
http://www.kako.com/neta/2008-006/2008-006.html
- avrdude-GUI (yuki-lab.jp Version)
 
http://yuki-lab.jp/hw/avrdude-GUI/index.html


1. デバッガの準備
Arduinoだけでは Eclipse でデバッグできないため、デバッガを準備する。純正のデバッガは高価だったため、安価なクローンを利用。
※ KEE のデバッガは対応するマイコンによっていくつかバージョンがある。私が使用したのは新しい Arduino Duemilanove (http://arduino.cc/en/Main/ArduinoBoardDuemilanove)で、載っているのは ATmega328 なので上記デバッガを選択。
※ SparkFun での配送方法について。USPSのFirst-Class以外は追跡ができる。



2. Arduino 改造(※標準の Arduino IDE で利用できなくなります)
ATmega328 で使われてる debugWIRE は RESET ピンで通信する。余計な回路が繋がってるとうまく動かない。DTR 信号が入ってこないよう、チップコンデンサを外す。
Arduino Duemilanove のページにある Schematic & Reference Design から回路図はダウンロードできる。「C13」のコンデンサがそれ。
※ デジタル回路難しいです (^_^;) 勉強せねば。
※ ATmega328 と ATmega328P は省電力版かどうかの違い。




3. デバッガのケーブル交換、インストール
KEE mkII Lite v2 は標準で 10 ピンの ISP I/F ケーブルが付いてる(ISP2JTAG アダプタも付いてる)。しかし、Arduino のボードにあるのは 6 ピンのミニ ICSP I/F なので変換が必要となる。AVR - ICSP Adapter と交換。
付属の CD-R からドライバをインストールしておく。
※ 変換基板部分が思ったより長く、交換したものの蓋が閉められなくなった。カパカパ状態で運用中。




4. Eclipse を Download してインストール
Eclipse Downloads http://www.eclipse.org/downloads/から Eclipse IDE for C/C++ Developers をダウンロードする。ダウンロードしたファイルは好きなところに解凍。以下、Eclipse 3.5 Galileo について記述。


5. Plugin を Download してインストール
[Help]-[Install New Software...]を選択。Install ダイアログが開いたら、http://avr-eclipse.sourceforge.net/updatesite/から AVR Plugin をインストールする。使用許諾書が表示されるため、受諾。


6. AVR tools を Download してインストール
WinAVR(http://winavr.sourceforge.net/) のページへ行き [Download] メニューからダウンロード。現在は SourceForge.net の WinAVR プロジェクトサイト(http://sourceforge.net/projects/winavr/files/) へのリンクがあるだけなので、結局はそちらでダウンロードすることになるはず。
この記事を書いた時点では、「WinAVR-20100110-install.exe」が最新。ダウンロードしてインストール。インストール後は Eclipse を再起動。


7. AVRDUDE を USB 対応版に変更
こちらの記事を参考に、USB 対応版 AVRDUDE を作成。
生成したバイナリを書き込む AVRDUDE だが、WinAVR に同梱されているものは USB を有効にしたビルドが行われていない。
http://yuki-lab.jp/hw/avrdude-GUI/index.htmlから USB を有効にした AVRDUDE をダウンロードし、WinAVR のものと交換しておく(デフォルトインストールしていれば、「C:\WinAVR-20100110\bin\avrdude.conf」と「C:\WinAVR-20100110\bin\avrdude.exe」が対象)。
LibUSB-Win32 も合わせてインストールしておく。
※ この記事を書いた時点で、AVRDUDE はバージョンは 5.8 まで上がっているので、できればソースからビルドしたいのですが、Cygwin 上での configure が上手く行かず保留中です。(^_^;) わかり次第新しい記事を書くです。
※ 新しい記事書けたのでそちらをご参照くださいませ。<(_ _)>(2010/03/29 追記)


8. Eclipse のセットアップ
Eclipse を再起動したら、[Window]-[Preference] を開く。
[AVR]-[Paths] と辿り、先ほどインストールした WinAVR のインストール情報が自動的に設定されていることを確認。
[AVR]-[AVRDude] と辿り、configuration を追加する。

  1. [Add] ボタンをクリックし、[Edit AVRDude Programmer Configuration New Configuration] ダイアログを開く。

  2. [Configuration name] に「Atmel JTAG ICE mkII ISP」を入力。[Description] には適宜コメントを。

  3. [Programmer Hardware] は「Atmel JTAG ICE mkII in ISP mode」を選択。

  4. [Override default port] に「usb」を入力。

  5. [Override default baudrate] は「115200」を選択。

  6. [Delay between avrdude invocations] に「2000」を入力。

  7. [OK] ボタンをクリックし、作成。同様の手順で、[Programmer Hardware] が「Atmel JTAG ICE mkII in debugWire mode」のものを作成。

  8. [Preference] を [Apply] し、[OK] で閉じる。






9. Arduino プロジェクトの作成(前半)
Arduino プロジェクト用の環境を作成する。前半スタート。

  1. [File] - [New] - [C++ Project] を選択

  2. [C++ Project] ダイアログで、[Project name:] を "Hello_Blink"、[Project type:] を [AVR Cross Target Application] - [Empty Project] で設定し、[Finish]

  3. 生成された "Hello_Blink"を選択し、右クリックメニューを開く。[Properties] を選択

  4. [Properties] ダイアログで、[AVR] - [AVRDude] と辿り、[Programmer] タブの [Programmer configuration] を先ほど設定した「Atmel JTAG ICE mkII ISP」に変更

  5. デバッガ、Arduino を接続。[Properties] ダイアログで、[AVR] - [TargetHardware] と辿り、[MCU Type] を「ATmega328P」に変更し、[Load from MCU]ボタンをクリック。エラーが出なければ接続成功なので、[OK] しダイアログを閉じる。






10. Arduino コアライブラリの取り込み
Eclipse で開発する場合、Arduino IDE の場合に自動的に生成されるコアライブラリを取り込む必要がある。

  1. Arduino IDE を実行する

  2. [File] - [Examples] - [Digital] - [Blink] を選択し、Blink スケッチを開く

  3. [Verify] ボタンをクリックし、スケッチをコンパイル。[Upload] ボタンをクリックし、アップロードを行う

  4. C:\Users\<username>\AppData\Local\Temp\buildXXXXXXXXXXXXXXXXXXXXX.tmp(Windows XP の場合は、C:\Documents and Settings\<username>\Local Settings\Temp\buildXXXXXXXXXXXXXXXXXXXXX.tmp)に、スケッチのコンパイル・アップロード結果が生成されるため、フォルダを開く

  5. 中にある「core.a」を Eclipse で現在作成している "Hello_Blink"プロジェクトへドラッグ & ドロップする。

  6. プロジェクトに入った「core.a」を右クリックし、[Rename...] で名前を「libcore.a」に変更する






11. Arduino プロジェクトの作成(後半)
後半ではビルド用の環境を整える。

  1. Eclipse に戻り、"Hello_Blink"を右クリック、[Properties] メニューを選択する

  2. [Properties] ダイアログで、[C/C++ Build] - [Settings] と辿り、[Tool Settings] タブを選択する

  3. [Additional Tools in Toolchain] を選択。[Generate HEX file for Flash memory]、[Print Size] にチェックが入っていること、それ以外は未チェックであることを確認する

  4. [AVR C++ Compiler] - [Directories] を選択。[Include Paths] で Arduino 用のインクルードファイルがあるフォルダを追加する。場所は「[Arduinoインストールフォルダ]\hardware\cores\arduino」

  5. [AVR C++ Compiler] - [Optimization] を選択。[Optimization Level] を「Size Optimizations (-Os)」にしておく

  6. [AVR C++ Linker] - [Libraries] を選択。[Libraries] に「core」、[Libraries Path] に「${workspace_loc:/Hello_Blink}」を設定する

  7. [OK]し、ダイアログを閉じる






12. main.cpp の追加
とりあえず Blink(http://arduino.cc/en/Tutorial/Blink) を実装してみる。元のソースと変わるのは適宜インクルードが必要になることと、main 関数を追加するぐらい。あと、Linker でコアライブラリとリンクするときにエラーが出るので、それに従って __cxa_pure_virtual 関数を externする。

/*
Blink

Turns on an LED on for one second, then off for one second, repeatedly.

The circuit:
* LED connected from digital pin 13 to ground.

* Note: On most Arduino boards, there is already an LED on the board
connected to pin 13, so you don't need any extra components for this example.


Created 1 June 2005
By David Cuartielles

http://arduino.cc/en/Tutorial/Blink

based on an orginal by H. Barragan for the Wiring i/o board

*/

#include "WProgram.h"
extern "C" void __cxa_pure_virtual() {
while (1)
;
}

int ledPin = 13; // LED connected to digital pin 13

// The setup() method runs once, when the sketch starts

void setup() {
// initialize the digital pin as an output:
pinMode(ledPin, OUTPUT);
}

// the loop() method runs over and over again,
// as long as the Arduino has power

void loop() {
digitalWrite(ledPin, HIGH); // set the LED on
delay(1000); // wait for a second
digitalWrite(ledPin, LOW); // set the LED off
delay(1000); // wait for a second
}

int main(void) {
init();

setup();

for (;;)
loop();

return 0;
}



13. 実行
ビルド後、プロジェクトの右クリック、[AVR] - [Upload Project to Target Device] でイメージをアップロードする。




長くなりすぎたかも?AVRDUDE のビルドとか、まだ宿題も残っているので次回もこの絡みで続くです。

Windows で AVRDUDE ビルド

$
0
0
前回の宿題。

WinAVR に同梱されている AVRDUDE は USB Support を有効にしてのビルドがされていない。実行すると、

C:\WinAVR-20100110\bin>avrdude -pm328p -cjtag2isp -Pusb
avrdude was compiled without usb support.
avrdude: ser_send(): write error: sorry no info avail

みたいな感じで取り付く島もない。

前回の記事ではyukiさんビルドのAVRDUDEを使わせていただきましたが、頼ってばかりじゃだめなので、一からビルドする方法、まとめてみました。


1. MinGW/MSYS のインストール
Cygwin だと make 時の "-mno-cygwin"オプションではじかれてしまうため、MinGW/MSYS を利用する。
まずは、MinGW。
HOWTO Install the MinGW (GCC) Compiler Suite | MinGWを参考にダウンロード、インストール。なお、ここの記事中にもある通り、以後、configureする際は、

../path/to/configure --prefix=/mingw

もしくは、

../path/to/configure --prefix=`cd /mingw; pwd -W`

のようなprefix指定が必須になる。

MSYS は MSYS | MinGWを参考にダウンロード、インストール。


2. Cygwin のインストール
make 中に yacc やら bison やら必要になるが、それらを MinGW/MSYS 上でインストールしていくのは非常に骨が折れるため、Cygwin で利用できるものは利用する。
Cygwin Information and Installationを参考にダウンロード、インストール。インストール後、Cygwin の bin へパスを通ため、MSYS で /etc/profile (C:\msys\1.0\etc\profile) を好きなエディタで開き(※改行コードは LF なので注意)、

export HOME LOGNAME MSYSTEM HISTFILE

のすぐ下辺りに、

export PATH=$PATH:/c/cygwin/bin

を追記、保存。


3. LibUsb-Win32 のインストール
USB Support を有効にするためには、LibUsb-Win32 のヘッダファイルやライブラリの Static Link が必要になる。LibUsb-Win32からダウンロードし、Cドライブ直下等にインストールしておく。


4. AVRDUDE のビルド
AVR Downloader/UploaDEr - Summary [Savannah]からソースコードをダウンロード。順にコマンドを叩いていく。

user@COMPUTER ~/user
$ tar zxvf avrdude-5.8.tar.gz 

avrdude-5.8
avrdude-5.8/README
avrdude-5.8/configure.ac
~(中略)~
avrdude-5.8/doc/texinfo.tex
avrdude-5.8/doc/avrdude.texi
avrdude-5.8/doc/avrdude.info

user@COMPUTER ~/user
$ cd avrdude-5.8

user@COMPUTER ~/user/avrdude-5.8
$ export CPPFLAGS="-I/c/LibUSB-Win32/include"

user@COMPUTER ~/user/avrdude-5.8
$ export CFLAGS="-I/c/LibUSB-Win32/include"

user@COMPUTER ~/user/avrdude-5.8
$ export LDFLAGS="-L/c/LibUSB-Win32/lib/gcc"

user@COMPUTER ~/user/avrdude-5.8
$ ./configure --prefix=/mingw

checking build system type... i686-pc-mingw32
checking host system type... i686-pc-mingw32
checking target system type... i686-pc-mingw32
~(中略)~
config.status: creating avrdude.conf.tmp
config.status: creating ac_cfg.h
config.status: executing depfiles commands

user@COMPUTER ~/user/avrdude-5.8
$ make

make  all-recursive
make[1]: Entering directory `/home/user/avrdude-5.8'
Making all in windows
make[2]: Entering directory `/home/user/avrdude-5.8/windows'
gcc -DHAVE_CONFIG_H -I. -I..   -I/c/LibUSB-Win32/include  -I/c/LibUSB-Win32/include -mno-cygwin -DWIN32NATIVE -MT loaddrv.o -MD -MP -MF .deps/loaddrv.Tpo -c -o loaddrv.o loaddrv.c
mv -f .deps/loaddrv.Tpo .deps/loaddrv.Po
gcc  -I/c/LibUSB-Win32/include -mno-cygwin -DWIN32NATIVE -mno-cygwin -L/c/LibUSB-Win32/lib/gcc -static -o loaddrv.exe loaddrv.o  
make[2]: Leaving directory `/home/user/avrdude-5.8/windows'
make[2]: Entering directory `/home/user/avrdude-5.8'
/bin/sh ./ylwrap config_gram.y y.tab.c config_gram.c y.tab.h config_gram.h y.output config_gram.output -- bison -y  -d
cygwin warning:
  MS-DOS style path detected: C:/msys/1.0/home/user/avrdude-5.8/config_gram.y
  Preferred POSIX equivalent is: /cygdrive/c/msys/1.0/home/user/avrdude-5.8/config_gram.y
  CYGWIN environment variable option "nodosfilewarning" turns off this warning.
  Consult the user's guide for more details about POSIX paths:
    http://cygwin.com/cygwin-ug-net/using.html#using-pathnames
updating config_gram.h
~(中略)~
ar cru libavrdude.a libavrdude_a-config_gram.o libavrdude_a-lexer.o libavrdude_a-arduino.o libavrdude_a-avr.o libavrdude_a-avr910.o libavrdude_a-avrpart.o libavrdude_a-bitbang.o libavrdude_a-butterfly.o libavrdude_a-config.o libavrdude_a-confwin.o libavrdude_a-crc16.o libavrdude_a-fileio.o libavrdude_a-jtagmkI.o libavrdude_a-jtagmkII.o libavrdude_a-lists.o libavrdude_a-par.o libavrdude_a-pgm.o libavrdude_a-ppi.o libavrdude_a-ppiwin.o libavrdude_a-safemode.o libavrdude_a-serbb_posix.o libavrdude_a-serbb_win32.o libavrdude_a-ser_avrdoper.o libavrdude_a-ser_posix.o libavrdude_a-ser_win32.o libavrdude_a-stk500.o libavrdude_a-stk500v2.o libavrdude_a-stk500generic.o libavrdude_a-usbasp.o libavrdude_a-usb_libusb.o libavrdude_a-usbtiny.o libavrdude_a-update.o 
ranlib libavrdude.a
gcc -DHAVE_CONFIG_H -I.  -DCONFIG_DIR=\"/mingw/etc\" -I/c/LibUSB-Win32/include -Wall -I/c/LibUSB-Win32/include -mno-cygwin -DWIN32NATIVE -MT avrdude-main.o -MD -MP -MF .deps/avrdude-main.Tpo -c -o avrdude-main.o `test -f 'main.c' || echo './'`main.c
mv -f .deps/avrdude-main.Tpo .deps/avrdude-main.Po
gcc -DHAVE_CONFIG_H -I.  -DCONFIG_DIR=\"/mingw/etc\" -I/c/LibUSB-Win32/include -Wall -I/c/LibUSB-Win32/include -mno-cygwin -DWIN32NATIVE -MT avrdude-term.o -MD -MP -MF .deps/avrdude-term.Tpo -c -o avrdude-term.o `test -f 'term.c' || echo './'`term.c
mv -f .deps/avrdude-term.Tpo .deps/avrdude-term.Po
gcc -Wall -I/c/LibUSB-Win32/include -mno-cygwin -DWIN32NATIVE  -L/c/LibUSB-Win32/lib/gcc -static -o avrdude.exe avrdude-main.o avrdude-term.o ./libavrdude.a -lusb -lhid -lsetupapi -lm 
make[2]: Leaving directory `/home/user/avrdude-5.8'
make[1]: Leaving directory `/home/user/avrdude-5.8'



5. 元の WinAVR の AVRDUDE と入れ替え
ビルドでできた「avrdude.conf」「avrdude.exe」を WinAVR の AVRDUDE と入れ替える。これで実行すれば、

C:\WinAVR-20100110\bin>avrdude -pm328p -cjtag2isp -Pusb
avrdude: usbdev_open(): did not find any USB device "usb"

のように USB を探しに行ってくれる。


ちなみに MinGW/MSYS は Windows の環境変数設定の影響を結構受けるので、仮想マシンなどでクリーンなビルド環境が用意できると、変なところで悩まずに済みます。(^_^;)←

Eclipse で Arduino 02

$
0
0
Arduinoに実際にイメージがアップロードできることが確認できたら、次はデバッガをアタッチしてみる。前々回の積み残し。

…と言っても、すでに 8 割がた環境構築は済んでいますので、デバッガの設定の紹介だけではなく、実際にセンサ用の閾値を変えながら動かしてみます。(^_^;)

こちらのページを参考に作成しています。偉大な先人に感謝です!
- Debugging - AVR-Eclipse
 
http://avr-eclipse.sourceforge.net/wiki/index.php/Debugging
- なんでも作っちゃう、かも。 Arduinoで遊ぼう - 赤外線距離センサ(GP2Y0A21YK0F)
 http://arms22.blog91.fc2.com/blog-entry-240.html
- avr-libc: Data in Program Space
 http://www.nongnu.org/avr-libc/user-manual/pgmspace.html
- Arduino - Tone
 http://arduino.cc/en/Tutorial/Tone
- Arduino - Tone2
 http://arduino.cc/en/Tutorial/Tone2


1. サンプルスケッチ - Hello Pitch follower2 -
デバッグするサンプルスケッチは、Arduino 公式サイトにある圧電スピーカサンプルに、赤外線式の距離センサを組み合わせた簡易テルミン的なもの。距離センサから出てくる値をちょいちょいチューニングしてみる。

ブレッドボード


回路図


プログラムリスト
pitches.h

/*************************************************
* Public Constants
*************************************************/

#define NOTE_B0 31
#define NOTE_C1 33
#define NOTE_CS1 35
#define NOTE_D1 37
#define NOTE_DS1 39
#define NOTE_E1 41
#define NOTE_F1 44
#define NOTE_FS1 46
#define NOTE_G1 49
#define NOTE_GS1 52
#define NOTE_A1 55
#define NOTE_AS1 58
#define NOTE_B1 62
#define NOTE_C2 65
#define NOTE_CS2 69
#define NOTE_D2 73
#define NOTE_DS2 78
#define NOTE_E2 82
#define NOTE_F2 87
#define NOTE_FS2 93
#define NOTE_G2 98
#define NOTE_GS2 104
#define NOTE_A2 110
#define NOTE_AS2 117
#define NOTE_B2 123
#define NOTE_C3 131
#define NOTE_CS3 139
#define NOTE_D3 147
#define NOTE_DS3 156
#define NOTE_E3 165
#define NOTE_F3 175
#define NOTE_FS3 185
#define NOTE_G3 196
#define NOTE_GS3 208
#define NOTE_A3 220
#define NOTE_AS3 233
#define NOTE_B3 247
#define NOTE_C4 262
#define NOTE_CS4 277
#define NOTE_D4 294
#define NOTE_DS4 311
#define NOTE_E4 330
#define NOTE_F4 349
#define NOTE_FS4 370
#define NOTE_G4 392
#define NOTE_GS4 415
#define NOTE_A4 440
#define NOTE_AS4 466
#define NOTE_B4 494
#define NOTE_C5 523
#define NOTE_CS5 554
#define NOTE_D5 587
#define NOTE_DS5 622
#define NOTE_E5 659
#define NOTE_F5 698
#define NOTE_FS5 740
#define NOTE_G5 784
#define NOTE_GS5 831
#define NOTE_A5 880
#define NOTE_AS5 932
#define NOTE_B5 988
#define NOTE_C6 1047
#define NOTE_CS6 1109
#define NOTE_D6 1175
#define NOTE_DS6 1245
#define NOTE_E6 1319
#define NOTE_F6 1397
#define NOTE_FS6 1480
#define NOTE_G6 1568
#define NOTE_GS6 1661
#define NOTE_A6 1760
#define NOTE_AS6 1865
#define NOTE_B6 1976
#define NOTE_C7 2093
#define NOTE_CS7 2217
#define NOTE_D7 2349
#define NOTE_DS7 2489
#define NOTE_E7 2637
#define NOTE_F7 2794
#define NOTE_FS7 2960
#define NOTE_G7 3136
#define NOTE_GS7 3322
#define NOTE_A7 3520
#define NOTE_AS7 3729
#define NOTE_B7 3951
#define NOTE_C8 4186
#define NOTE_CS8 4435
#define NOTE_D8 4699
#define NOTE_DS8 4978

main.cpp

/*
Hello Pitch follower2

Plays a pitch that changes based on a changing analog input.
This refered to Pitch follower and it was made.
see also:
* Arduino - Tone, http://arduino.cc/en/Tutorial/Tone
* Arduino - Tone2, http://arduino.cc/en/Tutorial/Tone2

The very useful distance measuring library - Distance.*, are made by arms22.
I'd like to take this occasion to express my gratitude to him all.
see also:
* なんでも作っちゃう、かも。 Arduinoで遊ぼう - 赤外線距離センサ(GP2Y0A21YK0F), http://arms22.blog91.fc2.com/blog-entry-240.html

created 4 Apr 2010
by Akira Sugiura

This example code is in the public domain.

http://urasandesu.blogspot.com/2010/04/eclipse-arduino-02.html

*/

#include "WProgram.h"
extern "C" void __cxa_pure_virtual() {
while (1)
;
}

#include "pitches.h"
#include <avr/pgmspace.h>
#include "Distance.h"

const uint16_t
PROGMEM TONE_SAMPLES[] = { NOTE_B0, NOTE_C1, NOTE_D1, NOTE_E1, NOTE_F1,
NOTE_G1, NOTE_A1, NOTE_B1, NOTE_C2, NOTE_D2, NOTE_E2, NOTE_F2,
NOTE_G2, NOTE_A2, NOTE_B2, NOTE_C3, NOTE_D3, NOTE_E3, NOTE_F3,
NOTE_G3, NOTE_A3, NOTE_B3, NOTE_C4, NOTE_D4, NOTE_E4, NOTE_F4,
NOTE_G4, NOTE_A4, NOTE_B4, NOTE_C5, NOTE_D5, NOTE_E5, NOTE_F5,
NOTE_G5, NOTE_A5, NOTE_B5, NOTE_C6, NOTE_D6, NOTE_E6, NOTE_F6,
NOTE_G6, NOTE_A6, NOTE_B6, NOTE_C7, NOTE_D7, NOTE_E7, NOTE_F7,
NOTE_G7, NOTE_A7, NOTE_B7, NOTE_C8, NOTE_D8 };
const int TONE_SAMPLES_LENGTH = sizeof(TONE_SAMPLES) / sizeof(uint16_t);
int toneSamplesMin = 0;
int toneSamplesMax = TONE_SAMPLES_LENGTH;

Distance distance = Distance(0);
int numberOfDistanceMin = 10;
int numberOfDistanceMax = 800;
int delayTime = 130;

int toneDuration = 100;

void setup() {
Serial.begin(9600);
}

void loop() {

distance.available();
int numberOfDistance = distance.numberOfDistance();
Serial.print(numberOfDistance);
Serial.println(" mm");

int toneSamplesIndex = map(numberOfDistance, numberOfDistanceMin,
numberOfDistanceMax, toneSamplesMin, toneSamplesMax);
Serial.print("toneSamplesIndex: ");
Serial.println(toneSamplesIndex);

unsigned int toneSample = pgm_read_word(&TONE_SAMPLES[toneSamplesIndex]);
Serial.print("toneSample: ");
Serial.println(toneSample);

tone(8, toneSample, toneDuration);

delay(delayTime);
}

int main(void) {
init();

setup();

for (;;)
loop();

return 0;
}




2. AVaRICE の設定
デバッガにアタッチするためには、gdbserver を起動しておく必要がある。ここでは JTAG/Debug Wire 接続に対応した AVaRICE を使用する。メニューから [Run] - [External Tools] - [External Tools Configurations...] を開く。

  1. [Name] に設定名を入れる(例."Start Avarice")。

  2. [Location] に AVaRICE のパスを設定する(例."C:\WinAVR-20100110\bin\avarice.exe")。

  3. [Working Directory] に AVaRICE の実行フォルダを設定する(例."C:\WinAVR-20100110\avr\bin")。

  4. [Arguments] に AVaRICE への引数を設定する(例."-2 -I -w -j usb :4242")。こちらで Linux man が見られるのでそちらも参考のこと。ちなみに、ここで例に挙げている引数は、"-2":JTAG ICE mkII を利用、"-I":割り込みの無視、"-w":Debug Wire 接続、"-j usb":JTAG ICE デバッガへの接続方式をUSBに指定、":4242":localhost(省略可能)の 4242 番ポートで待ち受け、となる。

  5. [Apply] し、[Close] で閉じる。







3. GDB Hardware Debugging の設定
gdbserver を起動すれば、Eclipse デバッガのアタッチができるようになる。gdbserver 同様、最初に設定が必要になるので、[Run] - [Debug Configurations...]を開いて設定を行う。

  1. [Name] に設定名を入れる(例."Hello_PitchFollower2")。

  2. [C/C++ Application] に実行するファイルのパスを設定する(例."Debug/Hello_PitchFollower2.elf")。[Search Project...] ボタンをクリックすれば、該当するファイルが一覧できる。

  3. [GDB Command] にデバッガのパスを設定する(例."avr-gdb")。

  4. [Command Set] は Standard(Windows)、[Protocol Version] は mi に設定する。

  5. [Use Remote Target] にチェックを入れ、[JTAG Device] を Generic、[Host name or IP address] を localhost、[Port number] は gdbserver で設定した 4242 を設定する。

  6. [Load Image] にチェックを入れ、[Image file name] は実行するファイルパスと同じものを指定する。(例."${workspace_loc:\Hello_PitchFollower2\Debug\Hello_PitchFollower2.elf}")。こちらも、[Workspace...] ボタンをクリックすれば、該当するファイルを一覧から選択できる。

  7. [Runtime Options] は、[Set breakpoint at] で、main で止まるよう指定し、[Resume] にチェックを入れておく。

  8. [Apply] し、[Close] で閉じる。







4. Terminal エミュレータの用意
シリアルの情報が見られる Terminal エミュレータを用意する。Windows であれば Tera Termが有名どころ。適宜ダウンロードしてインストールしておく。



5. デバッグ
実行イメージを Arduino にアップロードし、gdbserver → Eclipse デバッガの順に起動すると、設定しておいた main で止まる。あとは、普通のソフトをデバッグするのと同様に、ブレークポイントで止めて変数の中身を眺めたり、変更したりできる。レジスタやメモリの情報ももちろん同様に可能。

デバッグの様子




やっとこさ、下準備が整ってきたかな?先は長いです。(^_^;)

DotNetOpenAuth でサンプルコード

$
0
0
サンプルはシンプルがいいよね。

OAuth の公式ページ、OAuth — An open protocol to allow secure API authorization in a simple and standard method from desktop and web applications.Codeで、.NET Framework 向けのライブラリとして最初に掲示されてる DotNetOpenAuth

ライセンスも Microsoft Public License (Ms-PL) で提供されてるので、いいじゃんと思って使おうとしたら、サンプルコードがやたらがっつり書いてある。

こういうのは、処理の流れがわかる最低限の部分がわかればいいんだろうなと思いましたので、シンプルなサンプル、書いてみました。ちょうど Twitter の認証方式が OAuth に一本化されるということなので、今回はそれ向けで。(^^♪

利用する場合は以下のライブラリが必要になります。ありがたく使わせていただきますm(_ _)m
DotNetOpenAuth
 http://www.dotnetopenauth.net/
Apache log4net - Apache log4net: Home
 http://logging.apache.org/log4net/index.html


1. サンプルソース - DotNetOpenAuthSimpleSample -
サンプルは、GET statuses/home_timeline | dev.twitter.comの API をたたくもの。ここでは主要なクラスのみ紹介。

メイン
Program.cs
XmlSerializer に Parse させようとしている statuses クラスは、いったん API に生の XML を吐かせて Xsd.exe で XSD のガワを作成→少し整形 した自動生成コード。

using System;
using System.Diagnostics;
using System.Xml.Serialization;
using DotNetOpenAuth.Messaging;

namespace DotNetOpenAuthSimpleSample
{
class Program
{
static void Main(string[] args)
{
var statusesHomeTimeline = new MessageReceivingEndpoint(
"http://api.twitter.com/1/statuses/home_timeline.xml", HttpDeliveryMethods.GetRequest);

using (var resourceResponse = statusesHomeTimeline.Open(Verify))
using (var responseStream = resourceResponse.GetResponseReader())
{
var xmlSerializer = new XmlSerializer(typeof(statuses));
var statuses = (statuses)xmlSerializer.Deserialize(responseStream);

foreach (var status in statuses.status)
{
Console.WriteLine(
string.Format("User: {0}, Text: {1}, Location: {2}",
status.user.name, status.text, status.user.location));
}
}

Console.ReadLine();
}

private static string Verify(Uri authorizationLocation)
{
Process.Start(authorizationLocation.AbsoluteUri);
Console.Write("Please enter PIN code: ");
return Console.ReadLine();
}
}
}



Twitter 向け Consumer
TwitterConsumer.cs
ServiceProviderDescription を各サービス向けに書き換えれば良い。ThreadStatic になってるのは、自分の用途として、マルチスレッドで別々の認証情報を扱いながら処理を進めるってことが多いから。

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Net;
using DotNetOpenAuth.Messaging;
using DotNetOpenAuth.OAuth;
using DotNetOpenAuth.OAuth.ChannelElements;

namespace DotNetOpenAuthSimpleSample
{
public static class TwitterConsumer
{
public delegate string VerifyCallback(Uri authorizationLocation);

[ThreadStatic]
private static ServiceProviderDescription serviceProviderDescription;

private static ServiceProviderDescription ServiceProviderDescription
{
get
{
if (serviceProviderDescription == null)
{
serviceProviderDescription = new ServiceProviderDescription
{
RequestTokenEndpoint = new MessageReceivingEndpoint("https://twitter.com/oauth/request_token",
HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
UserAuthorizationEndpoint = new MessageReceivingEndpoint("https://twitter.com/oauth/authorize",
HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
AccessTokenEndpoint = new MessageReceivingEndpoint("https://twitter.com/oauth/access_token",
HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
TamperProtectionElements = new ITamperProtectionChannelBindingElement[] { new HmacSha1SigningBindingElement() },
ProtocolVersion = ProtocolVersion.V10a,
};
}

return serviceProviderDescription;
}
}

public static bool IsConfigured
{
get
{
return !string.IsNullOrEmpty(ConfigurationManager.AppSettings["consumerKey"]) &&
!string.IsNullOrEmpty(ConfigurationManager.AppSettings["consumerSecret"]);
}
}

[ThreadStatic]
private static DesktopConsumer current;

private static DesktopConsumer Current
{
get
{
if (current == null)
{
if (IsConfigured)
{
var tokenManager = new InMemoryTokenManager();
tokenManager.ConsumerKey = ConfigurationManager.AppSettings["consumerKey"];
tokenManager.ConsumerSecret = ConfigurationManager.AppSettings["consumerSecret"];
current = new DesktopConsumer(ServiceProviderDescription, tokenManager);
}
else
{
throw new InvalidOperationException(
"No Twitter OAuth consumer key and secret could be found in App.config AppSettings.");
}
}

return current;
}
}

[ThreadStatic]
private static string accessToken;

public static IncomingWebResponse Open(this MessageReceivingEndpoint endpoint, VerifyCallback verify)
{
return Open(endpoint, verify, default(object));
}

public static IncomingWebResponse Open(
this MessageReceivingEndpoint endpoint, VerifyCallback verify, IDictionary<string, string> extraData)
{
return Open(endpoint, verify, extraData);
}

public static IncomingWebResponse Open(
this MessageReceivingEndpoint endpoint, VerifyCallback verify, IEnumerable<MultipartPostPart> binaryData)
{
return Open(endpoint, verify, binaryData);
}

private static IncomingWebResponse Open(this MessageReceivingEndpoint endpoint, VerifyCallback verify, object data)
{
if (accessToken == null)
{
string requestToken = null;
Uri authorizationLocation = Current.RequestUserAuthorization(null, null, out requestToken);
string verifier = verify(authorizationLocation);
var grantedAccess = Current.ProcessUserAuthorization(requestToken, verifier);
accessToken = grantedAccess.AccessToken;
}

HttpWebRequest request = null;
IDictionary<string, string> extraData = null;
IEnumerable<MultipartPostPart> binaryData = null;

if ((extraData = data as IDictionary<string, string>) != null)
{
request = Current.PrepareAuthorizedRequest(endpoint, accessToken, extraData);
}
else if ((binaryData = data as IEnumerable<MultipartPostPart>) != null)
{
request = Current.PrepareAuthorizedRequest(endpoint, accessToken, binaryData);
}
else
{
request = Current.PrepareAuthorizedRequest(endpoint, accessToken);
}

return Current.Channel.WebRequestHandler.GetResponse(request);
}
}
}





2. ダウンロード
ソリューション全体のダウンロードは こちら



Arduino 、Android、GAE とか新しいことやり始めて、改めてサンプルの大切さ、学んでます。( ..)φメモメモ

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

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

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

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

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



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



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

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

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

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

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

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

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

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

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





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

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

using System;
using Urasandesu.NAnonym.DI;

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

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

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



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

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

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

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

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

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




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

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

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



Class2.cs
元のクラス 2

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



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

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

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

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



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

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

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



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

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

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

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

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





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

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

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

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

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

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

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

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

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



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



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

NotDefault
サンプル

using System;
using System.Collections.Generic;

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


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

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

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

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

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





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

As
サンプル

using System;

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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





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

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

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

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

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

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

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



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

using System;
using System.Reflection;

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

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

public Action Action { get; private set; }

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

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



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

using System;
using System.Reflection;

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

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

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

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

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

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





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

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

using System;
using System.Collections.Generic;

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

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

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

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

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

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

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

Func<T, T, int> comparer;

public DelegateComparer()
: this(null)
{
}

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

#region IComparer<T> Member

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

#endregion
}
}





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

Memo 01

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

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


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



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

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

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

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



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

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

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

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

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

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

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

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



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

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

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

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

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


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

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

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



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

  • パフォーマンス


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

  • AppDomain の Load/Unload


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



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


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

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

  • ライセンス


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



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



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

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

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

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

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




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

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


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



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

sscli20_WinXPx86_VS2008_JP.patch (11 KB) - Google Docs

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

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

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

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

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

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



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

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

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

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

C:\sscli20_20060311\sscli20>buildall

--- Copy prebuilt files ---

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

--- Building the PAL ---

Build successful.

--- Building the binplace tool ---

Build successful.

--- Building the build tool ---

Build successful.

--- Building the cppmunge tool ---

Build successful.

--- Building Resource Compiler ---

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

    1 executable built
    1 browse database built

--- Building PAL RT ---

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

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

--- Building C# compiler ---

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

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

--- Building CLR ---

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

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

--- Building FX - pass1 only ---

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

    10 files compiled
    1 file binplaced

--- Building Remoting - pass1 only ---

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

    4 files compiled

--- Building JScript - pass1 only ---

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

    4 files compiled

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

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

    1 file compiled
    1 executable built
    1 file binplaced

--- Setting up System.dll ---


--- Building FX ---

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

    3 files compiled
    5 executables built
    5 files binplaced

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


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


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


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


--- Building FX - Misc ---

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

    1 file binplaced

--- Building Remoting ---

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

    2 files compiled
    2 executables built
    2 files binplaced

--- Building JScript ---

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

    2 files compiled
    3 executables built
    3 files binplaced

--- Building dactablegen ---

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

    1 executable built
    1 file binplaced

--- Building dacupdatedll ---

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

    1 file binplaced

--- Building strike dll ---

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

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

--- Setting up everything ---


--- Building samples ---

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

    35 executables built
    78 files binplaced

--- Building pdb to ildb Tool ---

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

    1 executable built
    1 browse database built
    1 file binplaced

C:\sscli20_20060311\sscli20>




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

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

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

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

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

    C:\Program Files>

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

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



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

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

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

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

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

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



目次

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

using System;

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

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

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

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

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


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




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

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



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



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



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

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



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



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



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



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



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



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



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



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



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

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


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




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

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


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



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



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



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



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



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



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



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


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



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



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


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




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

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

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


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

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

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

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

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


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


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

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

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

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

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


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

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

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

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


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

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

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

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

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

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

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

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

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


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

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

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

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


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

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


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

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


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

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

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


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

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

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


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

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


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

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

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

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


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

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


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

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

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


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

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


2-11.Module の名前の設定

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


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

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

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


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

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

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

CreateCeeFileGenPtr pfnCreateCeeFileGen = NULL;
DestroyCeeFileGenPtr pfnDestroyCeeFileGen = NULL;

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

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


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

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

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

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

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


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

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

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

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

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

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

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

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

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


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

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




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

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

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

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

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

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

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


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




目次

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


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

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


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

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

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

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

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


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

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

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

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

        ::SymInitialize(m_hProcess, userSearchPath_, TRUE);

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

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

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

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

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

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

            if (0 < skipCount) 
            {
                --skipCount;
                continue;
            }

            StackFrame *pFrame = new StackFrame();
            pFrame->Init(m_hProcess, sf.AddrPC.Offset);
            m_frames.push_back(pFrame);
        }
    }
 
これを、DefaultSTDMETHODWrapper.h でちらっと見えていた例外クラス、NAnonymException のコンストラクタで呼び出すようにします。

#line 18 "CppTroll\ProfilingApiSample02\Urasandesu\NAnonym\NAnonymException.cpp"
    NAnonymException::NAnonymException() : 
        m_pStackTrace(boost::make_shared<StackTrace>())
    {
        CaptureStackTrace(this);
    }
    
    NAnonymException::NAnonymException(std::string const &what) : 
        m_what(what),
        m_pStackTrace(boost::make_shared<StackTrace>())
    { 
        CaptureStackTrace(this);
    }
    
    NAnonymException::NAnonymException(std::string const &what, NAnonymException &innerException) : 
        m_what(what),
        m_pStackTrace(boost::make_shared<StackTrace>())
    {
        CaptureStackTrace(this);
        *this << boost::errinfo_nested_exception(boost::copy_exception(innerException));
    }
    
    
    // …(略)…
    
    

    void NAnonymException::CaptureStackTrace(NAnonymException *this_)
    {
        this_->m_pStackTrace->Capture(3);
        *this_ << ThrowStackTrace(this_->m_pStackTrace.get());
    }
 
例外の中身を出すために使っていた Boost.Exceptionのメソッド、boost::diagnostic_information は、boost::error_info で登録しておいた型を引数に持つ to_string を定義しておくことで、それを呼び出してくれるようになってます。この仕組みを利用し、C# や Java の出力方式を参考に、出力文字列を構成しています。

#line 51 "CppTroll\ProfilingApiSample02\Urasandesu\NAnonym\NAnonymException.cpp"
    inline std::string to_string(StackTrace *pStackTrace)
    {
        using namespace std;
        using namespace boost;
        
        ostringstream oss;
        ptr_vector<StackFrame> *pFrames = pStackTrace->GetStackFrames();
        for (ptr_vector<StackFrame>::iterator i = pFrames->begin(), i_end = pFrames->end(); i != i_end; ++i)
        {
            oss << "at ";
            oss << i->GetSymbolName();
            oss << " in ";
            oss << i->GetModuleName();
            if (0 < i->GetFileLineNumber())
            {
                oss << "(";
                oss << i->GetFileName();
                oss << ":";
                oss << i->GetFileLineNumber();
                oss << ")";
            }
            oss << "\n";
        }
        return oss.str();
    }
 

BOOST_THROW_EXCEPTION(NAnonymException("An error is occurred!!")) して、catch (...) して、cout << diagnostic_information(current_exception()) << endl するだけの、簡単なテストを書いて実行してみると・・・。

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

C:\Documents and Settings\User>cd C:\Documents and Settings\User\CppTroll\Debug

C:\Documents and Settings\User\CppTroll\Debug>ProfilingApiSample02Test.exe --gtest_filter=ProfilingApiSample02TestSuite.ProfilingApiSample02Test
Running main() from gtest_main.cc
Note: Google Test filter = ProfilingApiSample02TestSuite.ProfilingApiSample02Test
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from ProfilingApiSample02TestSuite
[ RUN      ] ProfilingApiSample02TestSuite.ProfilingApiSample02Test
c:\documents and settings\User\cpptroll\profilingapisample02test\profilingapisample02test.cpp(32): Throw in function void __thiscall `anonymous-namespace'::ProfilingApiSample02TestSuite_ProfilingApiSample02Test_Test::TestBody(void)
Dynamic exception type: class boost::exception_detail::clone_impl<class Urasandesu::NAnonym::NAnonymException>
std::exception::what: An error is occurred!!
[struct Urasandesu::NAnonym::tag_stack_trace *] = at `anonymous namespace'::ProfilingApiSample02TestSuite_ProfilingApiSample02Test_Test::TestBody in ProfilingApiSample02Test.exe(c:\documents and settings\User\cpptroll\profilingapisample02test\profilingapisample02test.cpp:32)
at testing::internal::HandleSehExceptionsInMethodIfSupported<testing::Test,void> in ProfilingApiSample02Test.exe(c:\gtest-1.6.0-vc90\src\gtest.cc:2075)
at testing::internal::HandleExceptionsInMethodIfSupported<testing::Test,void> in ProfilingApiSample02Test.exe(c:\gtest-1.6.0-vc90\src\gtest.cc:2126)
at testing::Test::Run in ProfilingApiSample02Test.exe(c:\gtest-1.6.0-vc90\src\gtest.cc:2162)
at testing::TestInfo::Run in ProfilingApiSample02Test.exe(c:\gtest-1.6.0-vc90\src\gtest.cc:2342)
at testing::TestCase::Run in ProfilingApiSample02Test.exe(c:\gtest-1.6.0-vc90\src\gtest.cc:2446)
at testing::internal::UnitTestImpl::RunAllTests in ProfilingApiSample02Test.exe(c:\gtest-1.6.0-vc90\src\gtest.cc:4238)
at testing::internal::HandleSehExceptionsInMethodIfSupported<testing::internal::UnitTestImpl,bool> in ProfilingApiSample02Test.exe(c:\gtest-1.6.0-vc90\src\gtest.cc:2075)
at testing::internal::HandleExceptionsInMethodIfSupported<testing::internal::UnitTestImpl,bool> in ProfilingApiSample02Test.exe(c:\gtest-1.6.0-vc90\src\gtest.cc:2126)
at testing::UnitTest::Run in ProfilingApiSample02Test.exe(c:\gtest-1.6.0-vc90\src\gtest.cc:3874)
at main in ProfilingApiSample02Test.exe(c:\gtest-1.6.0-vc90\src\gtest_main.cc:39)
at __tmainCRTStartup in ProfilingApiSample02Test.exe(f:\dd\vctools\crt_bld\self_x86\crt\src\crt0.c:266)
at mainCRTStartup in ProfilingApiSample02Test.exe(f:\dd\vctools\crt_bld\self_x86\crt\src\crt0.c:182)
at RegisterWaitForInputIdle in kernel32.dll


[       OK ] ProfilingApiSample02TestSuite.ProfilingApiSample02Test (359 ms)
[----------] 1 test from ProfilingApiSample02TestSuite (359 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran. (359 ms total)
[  PASSED  ] 1 test.

C:\Documents and Settings\User\CppTroll\Debug>
 
いいですね~。VC++ ランタイムのソースコードは f ドライブで開発されてたのかなーみたいな不必要な情報まで出てますが、大は小を兼ねるってことで (b^ー゚)
これで準備が整いました!では、実装を始めましょう!




Hello, Profiling API !!
まず手始めに、一番簡単に追加ができる #US ヒープを弄ってみます。文字列を返すメソッドについて、環境変数を通じて与えた文字列に入れ替えられるようにしましょう。書き換える対象のプログラムはこんな感じ。

#line 1 "CppTroll\ProfilingApiSample01Target\Program.cs"
using System;

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine(new Class1().Print("Hello, World !!"));
        Console.WriteLine(new Class2().Print("こんにちは、世界!"));
    }
}

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

public class Class2
{
    public string Print(string value)
    {
        return "こんにちは、" + value + " 世界!";
    }
}
 
章立ては以下の通りです。
  1. 初期設定
  2. JIT 開始のフック、処理の入れ替え
    2-1. FunctionID から MethodDef テーブルレコードへの変換
    2-2. メソッド/クラスの詳細な情報を取得
    2-3. IL メソッドボディの書き換え
  3. 結果


1. 初期設定
ICorProfilerCallback* インターフェースを実装したコクラスで、一番に呼ばれるのが Initialize メソッド。このメソッドは最初に説明した最低限の設定で呼ばれるのですが、この後の処理は個別に CLR に呼び出してもらうよう設定する必要があります(SetEventMask)。このサンプルでは、JIT 開始をフックするだけで良いですので、COR_PRF_MONITOR_JIT_COMPILATION フラグを立てています(45 行目)。
順番が前後してしまいましたが、Initialize メソッドでは CLR とやりとりをするための ICorProfilerInfo* インターフェース実装オブジェクトが引数で渡されてきます。ただ、引数で渡されるのは、ここしかありませんので、グローバル変数もしくはメンバ変数に保持しておきましょう(36 行目)。
あとは、環境変数から、書き換える文字列を取得しておきます(50 行目~52 行目)。書き換え対象となるメソッド名は NANONYM_TARGET_METHOD を、#US ヒープに追加する文字列は NANONYM_NEW_MESSAGE を通じて取得するようにしました。こっそりと環境変数を取得するためのユーティリティクラス(Environment)を追加してますが、_dupenv_s をラップし、エラーがあれば前述のスタックトレース付き例外をスローするようにしただけですので、特に解説はしません。
ちなみに、アダプタクラスを作ったおかげで、本処理が記述されている ExeWeaver.cpp は、たったの 198 行です。携帯からでも、難なく読めちゃうかもですね (^_^;)

#line 31 "CppTroll\ProfilingApiSample01\ExeWeaver.cpp"
    // Reset the timer.
    m_timer.restart();
    

    // Initialize the profiling API.
    hr = pICorProfilerInfoUnk->QueryInterface(IID_ICorProfilerInfo2, 
                                              reinterpret_cast<void**>(&m_pInfo));
    if (FAILED(hr)) 
        BOOST_THROW_EXCEPTION(NAnonymCOMException(hr));


    // Set a value that specifies the types of events for which the profiler 
    // wants to receive notification from CLR.
    DWORD event_ = COR_PRF_MONITOR_JIT_COMPILATION;
    hr = m_pInfo->SetEventMask(event_);
    if (FAILED(hr)) 
        BOOST_THROW_EXCEPTION(NAnonymCOMException(hr));

    
    // Get the name of the target method and the new message.
    m_targetMethodName = wstring(CA2W(Environment::GetEnvironmentVariable("NANONYM_TARGET_METHOD").c_str()));
    m_newMessage = wstring(CA2W(Environment::GetEnvironmentVariable("NANONYM_NEW_MESSAGE").c_str()));
 


2. JIT 開始のフック、処理の入れ替え
各メソッドの JIT の開始は、JITCompilationStarted メソッドが通知してくれます。早速、中を見ていきましょう。

2-1. FunctionID から MethodDef テーブルレコードへの変換

#line 80 "CppTroll\ProfilingApiSample01\ExeWeaver.cpp"
    // Convert FunctionID to MethodDef token.
    mdMethodDef mdmd = mdMethodDefNil;
    CComPtr<IMetaDataImport2> pImport;
    hr = m_pInfo->GetTokenAndMetaDataFromFunction(functionId, IID_IMetaDataImport2, 
                                                  reinterpret_cast<IUnknown**>(&pImport), 
                                                  &mdmd);
    if (FAILED(hr)) 
        BOOST_THROW_EXCEPTION(NAnonymCOMException(hr));
    
 
Profiling API の共通の概念として、プロファイル ID というものがあります。各イベントを通知するメソッドの引数で渡されるこの情報、実際の意味は、各項目を説明するメモリのアドレスなのですが、そのままでは扱い辛いので、人間が読める情報に変換します。最終的に、MethodDef テーブルのレコード情報が欲しいので、Meta Data API 用のインターフェースを取り出しておきます。

2-2. メソッド/クラスの詳細な情報を取得

#line 90 "CppTroll\ProfilingApiSample01\ExeWeaver.cpp"
    // Get the properties of this method.
    mdTypeDef mdtd = mdTypeDefNil;
    WCHAR methodName[MAX_SYM_NAME] = { 0 };
    {
        ULONG methodNameSize = sizeof(methodName);
        DWORD methodAttr = 0;
        PCCOR_SIGNATURE pMethodSig = NULL;
        ULONG methodSigSize = 0;
        ULONG methodRVA = 0;
        DWORD methodImplFlag = 0;
        hr = pImport->GetMethodProps(mdmd, &mdtd, methodName, methodNameSize, 
                                     &methodNameSize, &methodAttr, &pMethodSig, 
                                     &methodSigSize, &methodRVA, &methodImplFlag);
        if (FAILED(hr)) 
            BOOST_THROW_EXCEPTION(NAnonymCOMException(hr));
        
        ULONG callConv = IMAGE_CEE_CS_CALLCONV_MAX;
        pMethodSig += ::CorSigUncompressData(pMethodSig, &callConv);

        ULONG paramCount = 0;
        pMethodSig += ::CorSigUncompressData(pMethodSig, &paramCount);

        CorElementType retType = ELEMENT_TYPE_END;
        pMethodSig += ::CorSigUncompressElementType(pMethodSig, &retType);
        if (retType != ELEMENT_TYPE_STRING)
            return S_OK;
    }
    

    // Get the properties of type that this method is declared.
    WCHAR typeName[MAX_SYM_NAME] = { 0 };
    {
        ULONG typeNameSize = sizeof(typeName);
        DWORD typeAttr = 0;
        mdToken mdtTypeExtends = mdTokenNil;
        hr = pImport->GetTypeDefProps(mdtd, typeName, typeNameSize, &typeNameSize, 
                                      &typeAttr, &mdtTypeExtends);
        if (FAILED(hr)) 
            BOOST_THROW_EXCEPTION(NAnonymCOMException(hr));
    }
    

    // Check, is this method the instrumentation target?
    wstring methodFullName(typeName);
    methodFullName += L".";
    methodFullName += methodName;
    if (methodFullName != m_targetMethodName)
        return S_OK;
    
 
Meta Data API を通じ、メソッドの詳細情報と、それが定義されたクラスの詳細情報を取得します。106 行目~115 行目で行っているメソッドシグネチャの Parse は、本当はもっとちゃんと行う必要があるのですが・・・。今回はとりあえず、戻り値の型が文字列であることさえわかればいいので、最低限の処理しかしていません。132 行目で取得したクラス名とメソッド名を連結し、環境変数から取得した、書き換え対象となるメソッドかどうかを判定しています。

2-3. IL メソッドボディの書き換え

#line 140 "CppTroll\ProfilingApiSample01\ExeWeaver.cpp"
    // Define the new message to #US heap.
    CComPtr<IMetaDataEmit2> pEmit;
    hr = pImport->QueryInterface(IID_IMetaDataEmit2, 
                                 reinterpret_cast<void**>(&pEmit));
    if (FAILED(hr)) 
        BOOST_THROW_EXCEPTION(NAnonymCOMException(hr));

    mdString mdsMessage = mdStringNil;
    hr = pEmit->DefineUserString(m_newMessage.c_str(), m_newMessage.size(), &mdsMessage);
    if (FAILED(hr))
        BOOST_THROW_EXCEPTION(NAnonymCOMException(hr));
    

    // Emit the new IL method body.
    SimpleBlob sb;
    sb.Put<BYTE>(OpCodes::Encodings[OpCodes::CEE_LDSTR].byte2);
    sb.Put<DWORD>(mdsMessage);
    sb.Put<BYTE>(OpCodes::Encodings[OpCodes::CEE_RET].byte2);
    
    COR_ILMETHOD ilMethod;
    ::ZeroMemory(&ilMethod, sizeof(COR_ILMETHOD));
    ilMethod.Fat.SetMaxStack(1);
    ilMethod.Fat.SetCodeSize(sb.Size());
    ilMethod.Fat.SetLocalVarSigTok(mdTokenNil);
    ilMethod.Fat.SetFlags(0);
    
    unsigned headerSize = COR_ILMETHOD::Size(&ilMethod.Fat, false);
    unsigned totalSize = headerSize + sb.Size();
    

    // Allocate the area for new IL method body and set it.
    ModuleID mid = NULL;
    {
        ClassID cid = NULL;
        mdMethodDef mdmd_ = mdMethodDefNil;
        hr = m_pInfo->GetFunctionInfo(functionId, &cid, &mid, &mdmd_);
        if (FAILED(hr)) 
            BOOST_THROW_EXCEPTION(NAnonymCOMException(hr));
    }

    CComPtr<IMethodMalloc> pMethodMalloc;
    hr = m_pInfo->GetILFunctionBodyAllocator(mid, &pMethodMalloc);
    if (FAILED(hr)) 
        BOOST_THROW_EXCEPTION(NAnonymCOMException(hr));

    BYTE *pNewILFunctionBody = reinterpret_cast<BYTE*>(pMethodMalloc->Alloc(totalSize));

    BYTE *pBuffer = pNewILFunctionBody;
    pBuffer += COR_ILMETHOD::Emit(headerSize, &ilMethod.Fat, false, pBuffer);
    ::memcpy_s(pBuffer, totalSize - headerSize, sb.Ptr(), sb.Size());

    hr = m_pInfo->SetILFunctionBody(mid, mdmd, pNewILFunctionBody);
    if (FAILED(hr)) 
        BOOST_THROW_EXCEPTION(NAnonymCOMException(hr));
    
 
環境変数から取得した文字列を #US ヒープに追加し(140 行目~150 行目)、それを使って IL ぷちぷち(153 行目~167 行目)。Profiling API の機能を使って新しい IL メソッドボディ領域を作ったら、そこに IL の並びをコピーします(170 行目~193 行目)。打ち込む IL が単純なので、特筆すべきところはないですね (^^ゞ


3. 結果
さて、新しいコマンドプロンプトを立ち上げ、実行してみましょう!サンプルプログラムの中の Class2.Print メソッドについて、返す文字列を変更してみます。

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

C:\Documents and Settings\User>cd C:\Documents and Settings\User\CppTroll\ProfilingApiSample01Target\bin\Debug

C:\Documents and Settings\User\CppTroll\ProfilingApiSample01Target\bin\Debug>dir
 ドライブ C のボリューム ラベルは S3A4509D001 です
 ボリューム シリアル番号は 758E-2116 です

 C:\Documents and Settings\User\CppTroll\ProfilingApiSample01Target\bin\Debug のディレクトリ

2011/10/29  17:01    <DIR>          .
2011/10/29  17:01    <DIR>          ..
2011/10/29  17:05             5,120 ProfilingApiSample01Target.exe
2011/10/29  17:05            13,824 ProfilingApiSample01Target.pdb
               2 個のファイル              18,944 バイト
               2 個のディレクトリ  23,052,824,576 バイトの空き領域

C:\Documents and Settings\User\CppTroll\ProfilingApiSample01Target\bin\Debug>SET COR_PROFILER={1DC70D11-5E46-48C6-BB07-75CFFF188327}

C:\Documents and Settings\User\CppTroll\ProfilingApiSample01Target\bin\Debug>SET NANONYM_TARGET_METHOD=Class2.Print

C:\Documents and Settings\User\CppTroll\ProfilingApiSample01Target\bin\Debug>SET NANONYM_NEW_MESSAGE=Hello, Dynamic Languages World!!

C:\Documents and Settings\User\CppTroll\ProfilingApiSample01Target\bin\Debug>SET COR_ENABLE_PROFILING=0

C:\Documents and Settings\User\CppTroll\ProfilingApiSample01Target\bin\Debug>ProfilingApiSample01Target.exe
Hello, こんにちは、Hello, World !! 世界! World !!
こんにちは、こんにちは、世界! 世界!

C:\Documents and Settings\User\CppTroll\ProfilingApiSample01Target\bin\Debug>SET COR_ENABLE_PROFILING=1

C:\Documents and Settings\User\CppTroll\ProfilingApiSample01Target\bin\Debug>ProfilingApiSample01Target.exe
Hello, Hello, Dynamic Languages World!! World !!
Hello, Dynamic Languages World!!
Time Elapsed: 0.078000s

C:\Documents and Settings\User\CppTroll\ProfilingApiSample01Target\bin\Debug>dir
 ドライブ C のボリューム ラベルは S3A4509D001 です
 ボリューム シリアル番号は 758E-2116 です

 C:\Documents and Settings\User\CppTroll\ProfilingApiSample01Target\bin\Debug のディレクトリ

2011/10/29  17:01    <DIR>          .
2011/10/29  17:01    <DIR>          ..
2011/10/29  17:05             5,120 ProfilingApiSample01Target.exe
2011/10/29  17:05            13,824 ProfilingApiSample01Target.pdb
               2 個のファイル              18,944 バイト
               2 個のディレクトリ  23,051,894,784 バイトの空き領域

C:\Documents and Settings\User\CppTroll\ProfilingApiSample01Target\bin\Debug>
 
おー!入れ替わりましたね!28 行目~29 行目で出ていた内容が、COR_ENABLE_PROFILING=1 することによって、34 行目~35 行目では見事に入れ替わっています。ファイルのタイムスタンプに変わりは無いため、実行時に、メモリ上にある IL メソッドボディだけが変化していることがわかるかと思います。次はもう少し複雑なサンプルを作ってみましょう。




Monkey ... Swapping ?
今度はメソッドの中身を丸ごと交換してみます。シグネチャが同じであれば、そう難しくはなさそうですね。また今後を考えて、ぼちぼち使い勝手の良いラッパーも設計していきます。仕事と違って、何度も作り直しながらブラッシュアップできるのは気が楽です (^_^;)
書き換える対象のプログラムはこんな感じでいかがでしょう。

#line 1 "CppTroll\ProfilingApiSample02Target\Program.cs"
using System;

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Hello world!!");
    }
}

class AlternativeProgram
{
    static void Main(string[] args)
    {
        Console.WriteLine(@"Welcome to the low layer world of CLR!! In the normal case, 
you can not replace the method body at the runtime, 
but you can do it by using the unmanaged profiling API!!");
    }
}
 
とりあえず動くもの、ということでラッパーには最低限必要な処理だけ持たせることにします。なので、クラスやメソッドの識別には、メタデータテーブルの ID を直接指定することにしました。ildasm を使えば、あらかじめその辺りを調べておくことができます。全体の雰囲気がわかって来たら、細かなところを作りこんで行く予定です。指定の方法は先ほどと同じく、環境変数を通じて行うこととしましょう。章立ては以下の通りです。
  1. 初期設定
  2. AppDomain 作成開始のフック
  3. Assembly 読み込み開始のフック
  4. Module 読み込み開始のフック
  5. JIT 開始のフック、処理の入れ替え
  6. 結果
中身を交換できるのは同じ Assembly に定義してあるクラス/メソッドだけ、しかも Generics や例外は無視しているという手抜きっぷりですが、シグネチャさえ合えば中身をごっそり換えられるということで、できることは先ほどのサンプルとは段違いに多いです。それでも本処理が記述されている ExeWeaver2.cpp は、たったの 183 行。我ながらイイ線行ってるのではないでしょうか (^^ゞ


1. 初期設定

#line 53 "CppTroll\ProfilingApiSample02\ExeWeaver2.cpp"
    // Reset the timer.
    m_timer.restart();


    // Initialize the unmanaged profiling API.
    m_pProfInfo->Init(pICorProfilerInfoUnk);
    m_pProfInfo->SetEventMask(COR_PRF_MONITOR_ASSEMBLY_LOADS | 
                              COR_PRF_MONITOR_MODULE_LOADS | 
                              COR_PRF_MONITOR_APPDOMAIN_LOADS | 
                              COR_PRF_MONITOR_JIT_COMPILATION);


    // Get name of the target assembly, replaced method and its declaring type and 
    // replacing method and its declaring type.
    m_targetAssemblyName = wstring(CA2W(Environment::GetEnvironmentVariable("NANONYM_TARGET_ASSEMBLY").c_str()));
    {
        istringstream is(Environment::GetEnvironmentVariable("NANONYM_REPLACE_TYPE_FROM"));
        is >> hex >> m_mdtdReplaceTypeFrom;
    }
    {
        istringstream is(Environment::GetEnvironmentVariable("NANONYM_REPLACE_METHOD_FROM"));
        is >> hex >> m_mdmdReplaceMethodFrom;
    }
    {
        istringstream is(Environment::GetEnvironmentVariable("NANONYM_REPLACE_TYPE_TO"));
        is >> hex >> m_mdtdReplaceTypeTo;
    }
    {
        istringstream is(Environment::GetEnvironmentVariable("NANONYM_REPLACE_METHOD_TO"));
        is >> hex >> m_mdtdReplaceMethodTo;
    }


    // Create pseudo AppDomain to load mscorlib.dll.
    m_pProfInfo->GetCurrentProcess()->GetPseudoDomain();
 
Initialize メソッドでやることは先ほどと大体一緒です。環境変数を通じて取得する情報(65 行目~83 行目)は以下の通りとしました。
環境変数名概要
NANONYM_TARGET_ASSEMBLY入れ替えるクラス/メソッドが定義されている Assembly 表示名。「ProfilingApiSample02Target」とか。
NANONYM_REPLACE_TYPE_FROM入れ替え元のクラス(TypeDef テーブルのレコード ID)。「0x02000002」とか。
NANONYM_REPLACE_METHOD_FROM入れ替え元のメソッド(MethodDef テーブルのレコード ID)。「0x06000001」とか。
NANONYM_REPLACE_TYPE_TO入れ替え先のクラス(TypeDef テーブルのレコード ID)。「0x02000003」とか。
NANONYM_REPLACE_METHOD_TO入れ替え先のメソッド(MethodDef テーブルのレコード ID)。「0x06000003」とか。
Profiling API のラッパーは、この API を通じて CLR が通知してくるイベントを元にモデル化しています。ただ、mscorlib.dll は決まった AppDomain に読み込まれることがないようなので、モデルを揃えるために、ここで擬似 AppDomain を作成しています(87 行目)。


2. AppDomain 作成開始のフック

#line 109 "CppTroll\ProfilingApiSample02\ExeWeaver2.cpp"
    // Create the wrapper referring the AppDomainID.
    m_pProfInfo->GetCurrentProcess()->CreateIfNecessary<AppDomainProfile>(appDomainId);
 
AppDomain 作成開始は、AppDomainCreationStarted を通じて通知されます。ここでは AppDomainID に紐付ける Profiling API ラッパーを作成しておきます。.NET Framework の機能上、AppDomain 単位での Load/Unload ができるため、AppDomain に対応する Profiling API ラッパーが解放されると、それに紐付く情報が全て解放されるような形を目指しています。また、同じ AppDomain であれば、Assembly や Module、Type、Method ・・・は常に同じものを指すため、map を使い、プロファイル ID からこれまで作成したインスタンスを引けるような構造にしてみました。
あと、Profiling API に限ったことではないのですが、CLR では %INCLUDE%\WinError.h に定義されている通常の HRESULT に加え、%INCLUDE%\CorError.h に定義された HRESULT も使用しています。最終的には、これも例外メッセージにうまく載せたいところです。


3. Assembly 読み込み開始のフック

#line 120 "CppTroll\ProfilingApiSample02\ExeWeaver2.cpp"
    // Create the wrapper referring the AssemblyID through the current AppDoamin.    
    AppDomainProfile *pDomainProf = m_pProfInfo->GetCurrentProcess()->GetCurrentDomain();
    AssemblyProfile *pAsmProf = pDomainProf->CreateIfNecessary<AssemblyProfile>(assemblyId);
    if (pAsmProf->GetName() != m_targetAssemblyName)
        return S_OK;
    
    m_pTargetAssemblyProf = pAsmProf;
 
Assembly 読み込み開始を通知してくれるのは、AssemblyLoadStarted メソッドとなります。現在の AppDomain を表す Profiling API ラッパーから、AssemblyID に紐付ける Profiling API ラッパーを作成します。もし、その名前が、環境変数で指定されていた対象となる Assembly の修飾名であれば、それを保持しておきましょう。


4. Module 読み込み開始のフック

#line 137 "CppTroll\ProfilingApiSample02\ExeWeaver2.cpp"
    if (m_pTargetAssemblyProf == NULL || m_pConv->HasInitialized())
        return S_OK;
    
    // Initialize the value converter to convert the wrapper of the profiling API 
    // to the wrapper of the meta data API.
    ModuleProfile *pModProf = m_pTargetAssemblyProf->CreateIfNecessary<ModuleProfile>(moduleId);
    AssemblyMetaData *pAsmMeta = m_pMetaInfo->CreatePseudo<AssemblyMetaData>();
    m_pConv->Initialize(pAsmMeta, m_pProfInfo->GetCurrentProcess(), pModProf);
 
Module 読み込み開始のフックは ModuleLoadStarted で行います。対象となる Assembly を表すラッパーが作成されているのであれば、Meta Data Api ラッパーと相互変換するためのオブジェクトを初期化します。


5. JIT 開始のフック、処理の入れ替え

#line 156 "CppTroll\ProfilingApiSample02\ExeWeaver2.cpp"
    if (!m_pConv->HasInitialized())
        return S_OK;

    // Get the properties this method.
    MethodProfile *pMethodProfFrom = m_pProfInfo->GetCurrentProcess()->CreateIfNecessary<MethodProfile>(functionId);
    MethodMetaData *pMethodMetaFrom = m_pConv->Convert(pMethodProfFrom);
    if (pMethodMetaFrom->GetToken() != m_mdmdReplaceMethodFrom)
        return S_OK;

    
    // Get the properties of the type that this method is declared.
    TypeMetaData *pTypeMetaFrom = pMethodMetaFrom->GetDeclaringType();
    if (pTypeMetaFrom->GetToken() != m_mdtdReplaceTypeFrom)
        return S_OK;

    
    // Replace the IL method body of them that are set above.
    ModuleMetaData *pModMetaFrom = pTypeMetaFrom->GetModule();
    TypeMetaData *pTypeMetaTo = pModMetaFrom->GetType(m_mdtdReplaceTypeTo);
    TypeProfile *pTypeProfTo = m_pConv->ConvertBack(pTypeMetaTo);   // NOTE: To resolve the type defined explicitly 
    MethodMetaData *pMethodMetaTo = pTypeMetaTo->GetMethod(m_mdtdReplaceMethodTo);
    MethodProfile *pMethodProfTo = m_pConv->ConvertBack(pMethodMetaTo);
    MethodBodyProfile *pBodyProfTo = pMethodProfTo->GetMethodBody();
    pMethodProfFrom->SetMethodBody(pBodyProfTo);
 
JIT 開始のフックは先ほどのサンプルでも出てきました。これまでの処理で、情報が揃った場合、実際の交換を行います。FunctionID から入れ替え元のメソッドの詳細情報を取得(159 行目~163 行目)、そのメソッドが定義されたクラスの TypeDef テーブル ID を取得し(166 行目~169 行目)、条件が合うのであれば入れ替え先のメソッドから IL メソッドボディをコピーし、入れ替え元のメソッドの IL メソッドボディを設定し直します(172 行目~ 179 行目)。若干嵌ったのは、Profiling API には、Meta Data API で扱うメタデータテーブルの ID から、プロファイル ID に変換するメソッドがあるのですが、どうも上から順(Assembly → Module → Type → Method →…)に解決しないと、E_INVALIDARG にされてしまうようです。175 行目の処理はこれを示しています。


6. 結果
こちらも、新しいコマンドプロンプトを立ち上げ実行してみます。サンプルプログラムの中の Program(0x02000002).Main(0x06000001)メソッドについて、別のクラスのメソッド AlternativeProgram(0x02000003).Main(0x06000003)に変更してみましょう。

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

C:\Documents and Settings\User>cd C:\Documents and Settings\User\CppTroll\ProfilingApiSample02Target\bin\Debug

C:\Documents and Settings\User\CppTroll\ProfilingApiSample02Target\bin\Debug>dir
 ドライブ C のボリューム ラベルは S3A4509D001 です
 ボリューム シリアル番号は 758E-2116 です

 C:\Documents and Settings\User\CppTroll\ProfilingApiSample02Target\bin\Debug のディレクトリ

2011/10/29  18:53    <DIR>          .
2011/10/29  18:53    <DIR>          ..
2011/10/29  18:53             5,120 ProfilingApiSample02Target.exe
2011/10/29  18:53            11,776 ProfilingApiSample02Target.pdb
               2 個のファイル              16,896 バイト
               2 個のディレクトリ  23,116,296,192 バイトの空き領域

C:\Documents and Settings\User\CppTroll\ProfilingApiSample02Target\bin\Debug>SET COR_PROFILER={F60DB91B-5932-4964-818A-CA697CF46A5F}

C:\Documents and Settings\User\CppTroll\ProfilingApiSample02Target\bin\Debug>SET NANONYM_TARGET_ASSEMBLY=ProfilingApiSample02Target

C:\Documents and Settings\User\CppTroll\ProfilingApiSample02Target\bin\Debug>SET NANONYM_REPLACE_TYPE_FROM=0x02000002

C:\Documents and Settings\User\CppTroll\ProfilingApiSample02Target\bin\Debug>SET NANONYM_REPLACE_METHOD_FROM=0x06000001

C:\Documents and Settings\User\CppTroll\ProfilingApiSample02Target\bin\Debug>SET NANONYM_REPLACE_TYPE_TO=0x02000003

C:\Documents and Settings\User\CppTroll\ProfilingApiSample02Target\bin\Debug>SET NANONYM_REPLACE_METHOD_TO=0x06000003

C:\Documents and Settings\User\CppTroll\ProfilingApiSample02Target\bin\Debug>SET COR_ENABLE_PROFILING=0

C:\Documents and Settings\User\CppTroll\ProfilingApiSample02Target\bin\Debug>ProfilingApiSample02Target.exe
Hello world!!

C:\Documents and Settings\User\CppTroll\ProfilingApiSample02Target\bin\Debug>SET COR_ENABLE_PROFILING=1

C:\Documents and Settings\User\CppTroll\ProfilingApiSample02Target\bin\Debug>ProfilingApiSample02Target.exe
Welcome to the low layer world of CLR!! In the normal case,
you can not replace the method body at the runtime,
but you can do it by using the unmanaged profiling API!!
Time Elapsed: 0.063000s

C:\Documents and Settings\User\CppTroll\ProfilingApiSample02Target\bin\Debug>dir
 ドライブ C のボリューム ラベルは S3A4509D001 です
 ボリューム シリアル番号は 758E-2116 です

 C:\Documents and Settings\User\CppTroll\ProfilingApiSample02Target\bin\Debug のディレクトリ

2011/10/29  18:53    <DIR>          .
2011/10/29  18:53    <DIR>          ..
2011/10/29  18:53             5,120 ProfilingApiSample02Target.exe
2011/10/29  18:53            11,776 ProfilingApiSample02Target.pdb
               2 個のファイル              16,896 バイト
               2 個のディレクトリ  23,116,296,192 バイトの空き領域

C:\Documents and Settings\User\CppTroll\ProfilingApiSample02Target\bin\Debug>
 
さっくり変わります(34 行目→39 行目~41 行目)。すばらしい。
ところで、プロセスの初期化から、終了までの処理時間を入れていますが、凄まじいですね。Profiling API を謳うだけあって、パフォーマンスの劣化がほとんど感じられないのはさすがです。式木こねくりまわして、AppDomainのLoad/Unload繰り返してた頃の自分に教えてあげたいですね (^_^;)




さらなる魔改造!
Profiling API の使い方を見てきましたが、いかがでしたでしょうか。メソッドのトレースとかまではちょっと紹介できませんでしたが、マネージコードではどうしようもなかったことができるようになるのは、ちょっとは自分の自信に繋がりそう。これまで魔法にしか思えなかったものが、タネがある手品だったということがわかった感じですね! (>_<)
そして、シンプルな方法だけでの入れ替えを試した結果ではありますが、やはりパフォーマンスは段違いだということがわかりました。以前行き詰っていた状態から、実に 30 倍以上の高速化が図れています。これはすごい!アンマネージ万歳!COM 万歳!C++ 万歳!
・・・まあ、最終的には C# のコード側から、変更情報(対象の名前やらMSIL ストリームやら…)を、名前付きパイプ経由で送り込むようなアーキテクチャになると思いますので、ここまでサクサクになるかはわかりませんが、だいぶ希望が持てる結果になったと思います。もう少し使い方に慣れてから、C# 側のデザインも考えて行こうと思います。


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

$
0
0
申し訳程度の C# 要素です (>_<)

@urasandesuこと、杉浦と申します。はじめましての方ははじめまして!
C# Advent Calendar 2011の 13 日目を担当させていただくことになりました。よろしくお願いいたします。

ネタは、.NET 開発の中でも日の当たりにくい低レイヤな部分、「アンマネージ API」を取り上げます。

タイトルにもある通り「C# で動的にメソッドを入れ替える」という、マネージコードだけではなかなか実現できないことをやってみることにします。また、元々日本語になっている資料やサンプルコードが少ない分野ですので、これからコンパイラやプロファイラなどをやってみたい方の学習の一助として、もしくはどんなものかちょっと覗いてみたい方への参考になればと思う次第です。
ちなみに、私の Blog のこの記事この記事辺りの続編となりますので、もし興味がありましたらそちらもどうぞ。

それでは始めることにしましょう。あ。冒頭にも書きました通り、C# の Advent Calendar にも関わらず、C# のプログラムは申し訳程度しか現れませんが、よろしければご笑覧くださいませ <(_ _)>

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


こちらのページ/ソフトウェアを参考にさせていただきました!いつもお世話になっております! (`・ω・ )ゝ
Download Details - Microsoft Download Center - Shared Source Common Language Infrastructure 2.0 Release
Cecil - Mono
ECMA C# and Common Language Infrastructure Standards
Boost C++ Libraries
ファイルをメモリのように「メモリマップドファイルクラス」
@IT:インサイド .NET Framework [改訂版]第2回 アセンブリのアイデンティティ
Chapter 14. Boost.Program_options - Boost 1.47.0
Filesystem Home - Boost 1.47.0
Chapter 1. Boost.ScopeExit - Boost 1.47.0
Pro Git - Pro Git 6.6 Git のさまざまなツール サブモジュール
Fusion GAC API Samples - Junfeng Zhang's Windows Programming Notes - Site Home - MSDN Blogs
xUnit.net - Unit testing framework for C# and .NET (a successor to NUnit)





目次

お題(C#)
ここにテストコードの無いレガシーなシステムがあり、以下の機能を持っているとしましょう。
  ・現在の時刻がお昼休みかどうかを判定し、結果に応じた文字列を標準出力に吐き出す。
  ・現在の曜日について、設定ファイルに休日であると設定された曜日と比較し、結果に応じた文字列を標準出力に吐き出す。

#line 102 "CppTroll\ProfilingApiSample03Target\Program.cs"
using System;
using MyLibrary;
using ThirdPartyLibrary;

namespace ProfilingApiSample03Target
{
class Program
{
static void Main(string[] args)
{
LifeInfo.LunchBreak();
LifeInfo.Holiday();
}
}
}

namespace MyLibrary
{
public static class LifeInfo
{
public static void LunchBreak()
{
var now = DateTime.Now;
Console.WriteLine("時刻: " + now.Hour + "\t" +
(12 <= now.Hour && now.Hour < 13 ? "お昼休みなう!" : "お仕事なう・・・"));
}

public static void Holiday()
{
var dayOfWeek = ConfigurationManager.GetProperty("Holiday", DayOfWeek.Sunday);
var now = DateTime.Now;
Console.WriteLine("曜日: " + now.DayOfWeek + "\t" +
(now.DayOfWeek == dayOfWeek ? "休日なう!" : "お仕事なう・・・"));
}
}
}
 
ちなみに、設定ファイルの読み込みに利用しているクラス、ConfigurationManager は、サードパーティ製のライブラリのようで、ソースコードはなく、中身はわかりません。


・・・さて、よくある例で恐縮ですが、このシステムの機能を Web サービスとしても公開したい、という話が出てきました。考えられる対応は以下のようなものでしょう。
a. イチから作り直す
【工数[大]】
同じ機能を持ったシステムを Web サービス用に作り直そう。仕様の整理もできるし、実はあきらめてた別の要件も、今後盛り込みやすくなるかも。

b. 出力された情報だけを使ってとりあえずなんとかする
【工数[小]】
中身にはいっさい手を入れず、出力されてる情報を Web サービス用に整形するだけ。間に合わせだけど、今回だけならなんとかなるでしょう。期間も短いようだし。

c. リファクタリングし、新しいインターフェースを追加できるようにする
【工数[中]】
現状のコードをある程度生かす。今後の拡張性をある程度持たせた上で、Web サービス用のインターフェースを追加したらどうか。レガシーなシステムだと、いくら影響範囲を調べたところで、進めていくうちにどうしても元の機能の書き換えが必要な部分が出てくるリスクもあるんじゃない?例えば、設定ファイルでやってるところを、DB マスタ化しないとどうにもならなくなっちゃったとか。ここサードパーティ製だから嫌な予感するんだよね。

この程度のシステムでしたら、迷わず a. を選んでいただければ良いとは思いますが、色々な状況がありますので、どれになるかはその時次第です。まあ今回は話の都合上、c. で進めることにさせてください (^_^;)
あと、最初に一回りサイクルが回ればあとは何とかなると思いますので、解説は最初のテストが通るまでです。最終形は皆さんへの宿題にさせていただければと思います・・・API の解説に夢中になって忘れていたとも言う・・・orz




レガシーコードをテスト可能にしていくお仕事(C#)
リファクタリングした上で新しい機能を追加するわけですから、何はともあれ自動テストを作り、簡単にリグレッションテストができるようにしておくのが良いでしょう。
コアとなっている LifeInfo クラスは、現在を判定した後、そのまま標準出力に吐き出しているので、これが拡張のし難さに繋がっているように思います。これの現状の動作を固めるのが先決です。
こんなテストを書いてみました。

#line 357 "CppTroll\ProfilingApiSample03TargetTest\ProgramTest.cs"
using System;
using System.IO;
using MyLibrary;
using MyLibraryTest.Helper;
using Xunit;

namespace MyLibraryTest
{
public class LifeInfoTest
{
[Fact]
public void LunchBreakTest01_NowIsLunchBreak()
{
using (new ConsoleContext())
using (var sw = new StringWriter())
{
Console.SetOut(sw);
LifeInfo.LunchBreak();
Assert.Equal("時刻: 12\tお昼休みなう!" + sw.NewLine, sw.ToString());
}
}

[Fact]
public void LunchBreakTest02_NowIsNotLunchBreak()
{
using (new ConsoleContext())
using (var sw = new StringWriter())
{
Console.SetOut(sw);
LifeInfo.LunchBreak();
Assert.Equal("時刻: 13\tお仕事なう・・・" + sw.NewLine, sw.ToString());
}
}

// こんな感じでテストが続く
// ・・・
}
}
 
370、382 行目で表れる ConsoleContext は、例外が発生しても、乗っ取った Console の標準出力を元に戻せるようにするためのヘルパークラスです。
さあ、実行してみましょう!

C:\CppTroll\Debug>xunit.console.exe ProfilingApiSample03TargetTest.dll /noshadow
xUnit.net console test runner (32-bit .NET 2.0.50727.3625)
Copyright (C) 2007-11 Microsoft Corporation.

xunit.dll: Version 1.8.0.1545
Test assembly: C:\CppTroll\Debug\ProfilingApiSample03TargetTest.dll

MyLibraryTest.LifeInfoTest.LunchBreakTest01_NowIsHoliday [FAIL]
Assert.Equal() Failure
Position: First difference is at position 4
Expected: 曜日: Sunday 休日なう!

Actual: 曜日: Tuesday お仕事なう・・・
Stack Trace:
場所 Xunit.Assert.Equal[T](T expected, T actual, IEqualityComparer`1 comparer)
場所 Xunit.Assert.Equal[T](T expected, T actual)
場所 MyLibraryTest.LifeInfoTest.LunchBreakTest01_NowIsHoliday() 場所 C:\CppTroll\ProfilingApiSample03TargetTest\ProgramTest.cs:行 391

MyLibraryTest.LifeInfoTest.LunchBreakTest01_NowIsLunchBreak [FAIL]
Assert.Equal() Failure
Position: First difference is at position 4
Expected: 時刻: 12 お昼休みなう!

Actual: 時刻: 7 お仕事なう・・・
Stack Trace:
場所 Xunit.Assert.Equal[T](T expected, T actual, IEqualityComparer`1 comparer)
場所 Xunit.Assert.Equal[T](T expected, T actual)
場所 MyLibraryTest.LifeInfoTest.LunchBreakTest01_NowIsLunchBreak() 場所 C:\CppTroll\ProfilingApiSample03TargetTest\ProgramTest.cs:行 367

MyLibraryTest.LifeInfoTest.LunchBreakTest02_NowIsNotLunchBreak [FAIL]
Assert.Equal() Failure
Position: First difference is at position 4
Expected: 時刻: 13 お仕事なう・・・

Actual: 時刻: 7 お仕事なう・・・
Stack Trace:
場所 Xunit.Assert.Equal[T](T expected, T actual, IEqualityComparer`1 comparer)
場所 Xunit.Assert.Equal[T](T expected, T actual)
場所 MyLibraryTest.LifeInfoTest.LunchBreakTest02_NowIsNotLunchBreak() 場所 C:\CppTroll\ProfilingApiSample03TargetTest\ProgramTest.cs:行 379

MyLibraryTest.LifeInfoTest.LunchBreakTest02_NowIsNotHoliday [FAIL]
Assert.Equal() Failure
Position: First difference is at position 4
Expected: 曜日: Monday お仕事なう・・・

Actual: 曜日: Tuesday お仕事なう・・・
Stack Trace:
場所 Xunit.Assert.Equal[T](T expected, T actual, IEqualityComparer`1 comparer)
場所 Xunit.Assert.Equal[T](T expected, T actual)
場所 MyLibraryTest.LifeInfoTest.LunchBreakTest02_NowIsNotHoliday() 場所 C:\CppTroll\ProfilingApiSample03TargetTest\ProgramTest.cs:行 403

4 total, 4 failed, 0 skipped, took 0.397 seconds

C:\CppTroll\Debug>
 
はい!テスト通りません!

もうやる前からわかってらっしゃった方も少なくないと思いますが、LifeInfo クラスの中身を見ると、DateTime.Now という環境に依存してしまう情報を直接見ているため、ということがわかります。

じゃあ自動化できないじゃん/(^o^)\

はい・・・と思っていた時期が私にもあったのですが、ある時気づいたのは、現状の動作を保障したいというホワイトボックス的な観点のテストから言えば、実際の処理は、ある DataTime 値による単なる分岐でしかない、ということでした。最終的には、もちろんブラックボックス的な観点のテストが必要ですが、ここでの目的を達成するには、この分岐分の DataTime 値のパターンが用意できれば良いだけということになりますよね。
となると、テスト実行前に DateTime.Now が返す値を固定の値に変更できれば良いわけです。イメージ的には、こんな感じで処理を入れ替えたいです(ビルドは通りません)。

#line 295 "CppTroll\ProfilingApiSample03TargetTest\ProgramTest.cs"
[Fact]
public void LunchBreakTest01_NowIsLunchBreak()
{
using (new ConsoleContext())
using (var sw = new StringWriter())
{
Console.SetOut(sw);
// Replace the content of DateTime.Now with a mock which always returns fixed value.
// NOTE: This idea does not work.
DateTime.Now = () => new DateTime(2011, 12, 13, 12, 00, 00);
LifeInfo.LunchBreak();
Assert.Equal("時刻: 12\tお昼休みなう!" + sw.NewLine, sw.ToString());
}
}

// こんな感じでテストが続く
// ・・・
 
304 行目で、DateTime.Now の中身を、常に固定値を返すモックに入れ替える気持ちです。このイメージに近い方法で処理を入れ替えられる仕組みを考えましょう。以下の 2 つの戦略を考えていきます。
  1. 呼び出すクラス側(この例の場合、LifeInfo)で、処理を入れ替えられる仕組みを用意する。
  2. 呼び出されるクラス側(この例の場合、DateTime)で、処理を入れ替えられる仕組みを用意する。
ちなみに 1. は以下で解説する通り、手動である程度シミュレートできますし、2. は Microsoft Research 謹製の Molesという有名なライブラリがあります(Moles のほうは、今回の C# Advent Calendar でも解説される方がいらっしゃりそうですね)ので、私が現在作っているライブラリを待つ必要は全然無いです・・・あれ? (´・ω・`)




レガシーコードにクサビを打ち込むお仕事(C#)
先ほどの 1. に当たる手順を、とりあえず手動でやってみます。
手を入れるといっても、いきなりがっつり変更するのでは、リグレッションテストをする前にデグレが混入する可能性がありますので、なるべく機械的に、かつなるべく変更を局所的に抑えていきましょう。
まずは打ち込むクサビを作成します。こんな感じでいかがでしょう。

#line 87 "CppTroll\ProfilingApiSample03Target\Program.cs"
// ・・・

namespace System.Wedge
{
struct WDateTime
{
public static class NowGet
{
static Func<DateTime> m_body = () => DateTime.Now;
public static Func<DateTime> Body { get { return m_body; } set { m_body = value; } }
}
}
}
 
Now プロパティの getter を入れ替えたいので、名前は安易に決めています。入れ子にされた型になっているのは、ジェネリック メソッドに対応させるときに都合が良さそうだから、ぐらいの理由です。デフォルト値は、元の処理の通り、DateTime.Now を返すようにしておきましょう(94 行目)
さて、そうしましたら、"DateTime.Now"を "WDateTime.NowGet.Body()"に一括置換しましょう。

#line 68 "CppTroll\ProfilingApiSample03Target\Program.cs"
// ・・・
public static class LifeInfo
{
public static void LunchBreak()
{
var now = WDateTime.NowGet.Body();
Console.WriteLine("時刻: " + now.Hour + "\t" +
(12 <= now.Hour && now.Hour < 13 ? "お昼休みなう!" : "お仕事なう・・・"));
}

public static void Holiday()
{
var dayOfWeek = ConfigurationManager.GetProperty("Holiday", DayOfWeek.Sunday);
var now = WDateTime.NowGet.Body();
Console.WriteLine("曜日: " + now.DayOfWeek + "\t" +
(now.DayOfWeek == dayOfWeek ? "休日なう!" : "お仕事なう・・・"));
}
}
 
73 行目、83 行目が置換されました。ビルドをしますと・・・問題無さそうですね。
ところでこの方法を手動でシミュレートするには、SCM(バージョン管理システム) があると便利です。gitのような DVCS(分散型バージョン管理システム) ですと、作業単位毎にコミットが作れますので、もし失敗しても柔軟に対応することができると思います。
テストコードはこんな感じになりました。

#line 159 "CppTroll\ProfilingApiSample03TargetTest\ProgramTest.cs"
[Fact]
public void LunchBreakTest01_NowIsLunchBreak()
{
using (new WDateTimeContext.NowGet())
using (new ConsoleContext())
using (var sw = new StringWriter())
{
Console.SetOut(sw);
WDateTime.NowGet.Body = () => new DateTime(2011, 12, 13, 12, 00, 00);
LifeInfo.LunchBreak();
Assert.Equal("時刻: 12\tお昼休みなう!" + sw.NewLine, sw.ToString());
}
}

// こんな感じでテストが続く
// ・・・
 
167 行目で常に固定値を返すモックに処理を入れ替えています。イメージにあった書き方に大分近いと思いますがいかがでしょうか?
using 文が追加されていますが(162 行目)、使ったクサビは元に戻しやすいよう、このような IDisposable を実装した Context クラスや Rent パターンを使ったヘルパークラスがあると便利だと思います。
これを実行してみると・・・

C:\CppTroll\Debug>xunit.console.exe ProfilingApiSample03TargetTest.dll /noshadow
xUnit.net console test runner (32-bit .NET 2.0.50727.3625)
Copyright (C) 2007-11 Microsoft Corporation.

xunit.dll: Version 1.8.0.1545
Test assembly: C:\CppTroll\Debug\ProfilingApiSample03TargetTest.dll

4 total, 0 failed, 0 skipped, took 0.372 seconds

C:\CppTroll\Debug>
 
はい!テスト通りました!

後は煮るなり焼くなり、お好きにリファクタリングしていただけると思います。
なお、実際の場面ですと、一括置換の際に引数をかわさないといけなかったり、改行が途中にあったりで一筋縄ではいかないかもしれません。そうすると・・・ちょっとは私のライブラリも存在意義が出てくるかもしれませんね (^_^;)




レガシーコードにコソドロを紛れ込ませるお仕事(C++/C#)
戦略の 2. に当たる手順です。いよいよ本題のアンマネージ API ですね!リファレンスに載っている API 一覧に従えば、今回利用しているのは以下の API のものとなります。興味がある方は、MSDN を眺めていただければと思います。
サンプルコードが全く無いところが悲しいですけどね・・・(-_-;)
Fusion API
ランタイムホストがアプリケーションのリソースにアクセスするための手段を提供する。
例えば、アセンブリの完全修飾名からファイルパスを取得、など。

Host API
アンマネージアプリケーションに CLR を統合するための手段を提供する。
例えば、AppDomain の管理、CLI で規定されたメタデータを含む PE ファイル生成、など。

Meta Data API
CLR によって読み込まれる型を使用せずに、メタデータにアクセス/生成するための手段を提供する。
例えば、アセンブリに定義されたモジュールの読み取り、新しい型の追加、など。

Profiling API
CLR によってプログラムの実行を監視するための手段を提供する。
例えば、メモリリークの追跡、パフォーマンスやカバレッジの計測のための処理の書き換え、など。

Strong Naming API
アセンブリの厳密名署名を管理するための手段を提供する。
例えば、公開キーと秘密キーのペアの生成、アセンブリの署名、など

実行時に C# のメソッドを動的に入れ替えるのは、前回もやっていますので、今回は差分を見ていきましょう。
大きく異なるのは、入れ替えた部分に C# 側からアクセスできるよう、イメージにあったようなインターフェースを提供するスタブを作るプログラムを作る必要があることです。
DateTime.Now の中身を入れ替える基本的な戦略は、前回と同じく、JIT 開始をフックしてやることにしましょう。
さて、今回は元のソースコードには全く手を入れません。

#line 28 "CppTroll\ProfilingApiSample03Target\Program.cs"
// ・・・
public static class LifeInfo
{
public static void LunchBreak()
{
var now = DateTime.Now;
Console.WriteLine("時刻: " + now.Hour + "\t" +
(12 <= now.Hour && now.Hour < 13 ? "お昼休みなう!" : "お仕事なう・・・"));
}

public static void Holiday()
{
var dayOfWeek = ConfigurationManager.GetProperty("Holiday", DayOfWeek.Sunday);
var now = DateTime.Now;
Console.WriteLine("曜日: " + now.DayOfWeek + "\t" +
(now.DayOfWeek == dayOfWeek ? "休日なう!" : "お仕事なう・・・"));
}
}
 
テストコードのほうは、これから作成するスタブ生成プログラムから吐き出されるスタブを使ったつもりで書きます(このままではビルドは通りません)。

#line 21 "CppTroll\ProfilingApiSample03TargetTest\ProgramTest.cs"
[Fact]
public void LunchBreakTest01_NowIsLunchBreak()
{
using (new PDateTimeContext.NowGet())
using (new ConsoleContext())
using (var sw = new StringWriter())
{
Console.SetOut(sw);
PDateTime.NowGet.Body = () => new DateTime(2011, 12, 13, 12, 00, 00);
LifeInfo.LunchBreak();
Assert.Equal("時刻: 12\tお昼休みなう!" + sw.NewLine, sw.ToString());
}
}

// こんな感じでテストが続く
// ・・・
 
では各プログラムを見ていきましょう!
  1. スタブを生成するプログラム
  2. 処理を入れ替えるプログラム
  3. 結果

なお、.NET コア機能に手を入れる必要があったり、Func<TResult> のような汎用ジェネリック デリゲートを使う必要があったりで、ちょっと大変ですが、とりあえずサンプルということで、分岐も状態もほとんどない、バッチ的なコードになってます。
長くなりますので、結果をご覧になりたい方はこちらへどうぞ ~(´ー`~)


スタブを生成するプログラム
最終形になると、引数に渡されたアセンブリ(例えば、mscorlib)を解析し、全ての型を解析したり、オプションによって作成するスタブを制御できる形になるかと思います。Boost.Program_options のような便利なライブラリを使えば、私のような初学者でも、C++ で難しいことをやらずに済むのはありがたいですね。
ただ、今回はサンプルですので、利用するアセンブリは決め打ちとしましょう。

#line 62 "CppTroll\ProfilingApiSample03Stubber\ProfilingApiSample03Stubber.cpp"
// Preapre Fusion API to retrieve creating its instance method.
path corSystemDirectoryPath;
path fusionPath;
{
WCHAR buffer[MAX_PATH] = { 0 };
DWORD length = 0;
hr = ::GetCORSystemDirectory(buffer, MAX_PATH, &length);
if (FAILED(hr))
BOOST_THROW_EXCEPTION(CppAnonymCOMException(hr));

corSystemDirectoryPath = buffer;
fusionPath = buffer;
fusionPath /= L"fusion.dll";
}

HMODULE hmodCorEE = ::LoadLibraryW(fusionPath.c_str());
if (!hmodCorEE)
BOOST_THROW_EXCEPTION(CppAnonymSystemException(::GetLastError()));
BOOST_SCOPE_EXIT((hmodCorEE))
{
::FreeLibrary(hmodCorEE);
}
BOOST_SCOPE_EXIT_END

typedef HRESULT (__stdcall *CreateAsmCachePtr)(IAssemblyCache **ppAsmCache, DWORD dwReserved);

CreateAsmCachePtr pfnCreateAsmCache = NULL;
pfnCreateAsmCache = reinterpret_cast<CreateAsmCachePtr>(
::GetProcAddress(hmodCorEE, "CreateAssemblyCache"));
if (!pfnCreateAsmCache)
BOOST_THROW_EXCEPTION(CppAnonymSystemException(::GetLastError()));

CComPtr<IAssemblyCache> pAsmCache;
hr = pfnCreateAsmCache(&pAsmCache, 0);
if (FAILED(hr))
BOOST_THROW_EXCEPTION(CppAnonymCOMException(hr));
 
Fusion API の準備です。この API も、インスタンスを生成するメソッドが見つからなくて困ったパターンです。MSDN では、mscoree.dll が必要なことになっていますし、頼りの SSCLI でも、LoadLibraryW する DLL は mscoree.dll になっているというトラップ・・・(-_-;)
正解は、こちらの Blogにもある通り、CLR のシステムディレクトリにある、fusion.dll となります。今後は、Dependency Walker の grep 版みたいなツールが必要になってくるのかもしれませんね。
68 行目で CLR のシステムディレクトリを取得したら、それを Boost.FileSystem を使って fusion.dll のフルパスを作成します。77 行目で取得したハンドルは、Boost.ScopeExit に後処理を任せ、次に進みましょう。今回必要になるのは、IAssemblyCache オブジェクトになりますので、そのインスタンスを生成するメソッドを 89 行目で参照できるようにしておきます。実際にインスタンスを生成しているのは 95 行目ですね。

#line 101 "CppTroll\ProfilingApiSample03Stubber\ProfilingApiSample03Stubber.cpp"
// Retrieve the place of System.Core.dll from GAC with Fusion API.
path systemCorePath;
{
WCHAR buffer[MAX_PATH] = { 0 };
ASSEMBLY_INFO asmInfo;
::ZeroMemory(&asmInfo, sizeof(ASSEMBLY_INFO));
asmInfo.cbAssemblyInfo = sizeof(ASSEMBLY_INFO);
asmInfo.pszCurrentAssemblyPathBuf = buffer;
asmInfo.cchBuf = MAX_PATH;
hr = pAsmCache->QueryAssemblyInfo(0, L"System.Core, Version=3.5.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, processorArchitecture=MSIL", &asmInfo);
if (FAILED(hr))
BOOST_THROW_EXCEPTION(CppAnonymCOMException(hr));

D_WCOUT1(L"System.Core is here: %|1$s|", buffer);
systemCorePath = buffer;
}
 
Func<TResult> 汎用ジェネリック デリゲートを参照設定したいですので、それを定義した System.Core への物理パスを、Fusion API を使って GAC に問い合わせます(110 行目)。ちなみに、Mono.Cecil なんかだと、Portability が必要になるためか、この辺りは完全に手動構築になっていました。そもそも、アンマネージ API に当たる機能が、Mono にあるかどうか怪しいのですが、低レイヤな部分を移植可能にしておくのは一筋縄では行かなさそうですね。

#line 137 "CppTroll\ProfilingApiSample03Stubber\ProfilingApiSample03Stubber.cpp"
// Get TypeDef records to add to TypeRef table.
mdTypeDef mdtdFunc1 = mdTypeDefNil;
hr = pImpSystemCore->FindTypeDefByName(L"System.Func`1", NULL, &mdtdFunc1);
if (FAILED(hr))
BOOST_THROW_EXCEPTION(CppAnonymCOMException(hr));

D_COUT1("Token of TypeDef for System.Func<T>: 0x%|1$08X|", mdtdFunc1);
 
得意の Meta Data API を使い、型の定義を表す TypeDef テーブルのレコードを名前で探します(139 行目)。この API は以前も出てきましたが、ジェネリック型ということで再登場です。指定する型名は、[ジェネリック型のパラメータを取り除いた型名]`[ジェネリック型のパラメータ数]が決まりのように見えますが・・・実はメタデータ的には、TypeDef から、ジェネリック型のパラメータの定義を表す GenericParam への参照は存在しません。その逆はもちろんあるのですが。
ですので、ジェネリック型かどうかをちゃんと調べるには、必ず GenericParam の列挙を試みないといけないわけで、正直めんどくさいかも・・・ (-_-;)
まあしかしこの辺り、後方互換を目指す上では仕方がなかったんでしょうね。System.Type クラスにある、IsGenericType プロパティを参考に、うまく抽象化できればと思います。

#line 478 "CppTroll\ProfilingApiSample03Stubber\ProfilingApiSample03Stubber.cpp"
// Add to TypeRef table with the name.
mdTypeRef mdtrFunc1 = mdTypeRefNil;
hr = pEmtMSCorLibPrig->DefineTypeRefByName(mdarSystemCore, L"System.Func`1", &mdtrFunc1);
if (FAILED(hr))
BOOST_THROW_EXCEPTION(CppAnonymCOMException(hr));

D_COUT1("Token of TypeRef for System.Func`1: 0x%|1$08X|", mdtrFunc1);
 
一気に 300 ステップほど進みました (^_^;)
いかに決まりきったことをやっているかという・・・早くライブラリ化しないとですね・・・。
型の参照を表す TypeRef テーブルのレコードの作成にジェネリック型を作成する場合も、定義を探す時と同様です(480 行目)。
え、ジェネリック型インスタンスはどうやって作るのさ、と思うわけですが、これには TypeSpec テーブルに、改めてレコードを作成することになります。

#line 520 "CppTroll\ProfilingApiSample03Stubber\ProfilingApiSample03Stubber.cpp"
// Add TypeRef records retrieved in above to TypeSpec table.
mdTypeSpec mdtsSystemFunc1DateTime = mdTypeSpecNil;
{
COR_SIGNATURE pSigBlob[] = {
ELEMENT_TYPE_GENERICINST, // TYPE: GENERICINST
ELEMENT_TYPE_CLASS, // CLASS
0x05, // TypeRef: 0x01000001(System.Func`1)
1, // Generics Arguments Count: 1
ELEMENT_TYPE_VALUETYPE, // VALUETYPE
0x0D // TypeRef: 0x01000003(System.DateTime)
};
ULONG sigBlobSize = sizeof(pSigBlob) / sizeof(COR_SIGNATURE);
hr = pEmtMSCorLibPrig->GetTokenFromTypeSpec(pSigBlob, sigBlobSize,
&mdtsSystemFunc1DateTime);
if (FAILED(hr))
BOOST_THROW_EXCEPTION(CppAnonymCOMException(hr));
}

D_COUT1("Token of TypeSpec for System.Func<DateTime>: 0x%|1$08X|", mdtsSystemFunc1DateTime);
 
532 行目にある TypeSpec テーブルへレコードを追加するメソッドは、GetTokenFromTypeSpec というやることに反した名前をしています。なんで他と同じ Define** にしなかったし (-_-;) また、とうとうシグネチャが現れてしまいました(523 ~ 530 行目)。カスタム属性の時も思いましたが、インターフェースに困ったらシグネチャという感じです。
Mono.Cecil や System.Reflection.Emit 名前空間にある API が、いかに使いやすいかと身にしみて思います。
もう System.Type クラスにある、MakeGenericType メソッドの使い方が複雑だなんて言わないよ絶対。

#line 615 "CppTroll\ProfilingApiSample03Stubber\ProfilingApiSample03Stubber.cpp"
// Sign to this assembly with Strong Naming API.
path msCorLibPrigKeyPairPath = L"..\\ProfilingApiSample03Patch\\ProfilingApiSample03Patch.snk";
auto_ptr<BYTE> pMSCorLibPrigKeyPair;
DWORD msCorLibPrigKeyPairSize = 0;
auto_ptr<PublicKeyBlob> pMSCorLibPrigPubKey;
DWORD msCorLibPrigPubKeySize = 0;
{
HANDLE hSnk = NULL;
hSnk = ::CreateFileW(msCorLibPrigKeyPairPath.c_str(), GENERIC_READ, FILE_SHARE_READ, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, NULL);
if (hSnk == INVALID_HANDLE_VALUE)
BOOST_THROW_EXCEPTION(CppAnonymSystemException(::GetLastError()));
BOOST_SCOPE_EXIT((hSnk))
{
::CloseHandle(hSnk);
}
BOOST_SCOPE_EXIT_END

msCorLibPrigKeyPairSize = ::GetFileSize(hSnk, NULL);
if (msCorLibPrigKeyPairSize == -1)
BOOST_THROW_EXCEPTION(CppAnonymSystemException(::GetLastError()));

pMSCorLibPrigKeyPair = auto_ptr<BYTE>(new BYTE[msCorLibPrigKeyPairSize]);
if (::ReadFile(hSnk, pMSCorLibPrigKeyPair.get(),
msCorLibPrigKeyPairSize, &msCorLibPrigKeyPairSize, NULL) == FALSE)
BOOST_THROW_EXCEPTION(CppAnonymSystemException(::GetLastError()));

BYTE *pPubKey = NULL;
if (!::StrongNameGetPublicKey(NULL, pMSCorLibPrigKeyPair.get(),
msCorLibPrigKeyPairSize, &pPubKey, &msCorLibPrigPubKeySize))
BOOST_THROW_EXCEPTION(CppAnonymCOMException(::StrongNameErrorInfo()));

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

if (msCorLibPrigPubKeySize)
::StrongNameFreeBuffer(pPubKey);
}
 
mscorlib のような厳密な名前を持っているアセンブリには、厳密な名前を持っているアセンブリしか参照させることはできません。今回作成するスタブにも署名が必要になります。その名の通り、Strong Naming API を使いましょう。あらかじめ作成しておいたキーペアファイルを読み込み(623、633、638 行目)、そこから公開鍵を取得しておくことで(643 行目)、署名のための準備をしておきます。

#line 750 "CppTroll\ProfilingApiSample03Stubber\ProfilingApiSample03Stubber.cpp"
// Create NestedClass records.
mdTypeDef mdtdNowGet = mdTypeDefNil;
hr = pEmtMSCorLibPrig->DefineNestedType(L"NowGet",
tdAbstract | tdAnsiClass |
tdSealed | tdNestedPublic | tdBeforeFieldInit,
mdtrObject, NULL, mdtdPDateTime, &mdtdNowGet);
if (FAILED(hr))
BOOST_THROW_EXCEPTION(CppAnonymCOMException(hr));

D_COUT1("Token of TypeDef for System.Prig.PDateTime.NowGet: 0x%|1$08X|", mdtdNowGet);
 
入れ子にされた型の定義です(752 行目)。入れ子にされた型の名前には、名前空間や外側の型の名前は必要ないです・・・特筆すべきことでは無かったかも (^_^;)

#line 763 "CppTroll\ProfilingApiSample03Stubber\ProfilingApiSample03Stubber.cpp"
// Create Field records.
mdFieldDef mdfdNowGetm_body = mdFieldDefNil;
{
COR_SIGNATURE pSigBlob[] = {
IMAGE_CEE_CS_CALLCONV_FIELD, // FIELD
ELEMENT_TYPE_GENERICINST, // TYPE: GENERICINST
ELEMENT_TYPE_CLASS, // CLASS
0x05, // TypeRef: 0x01000001(System.Func`1)
1, // Generics Arguments Count: 1
ELEMENT_TYPE_VALUETYPE, // VALUETYPE
0x0D // TypeRef: 0x01000003(System.DateTime)
};
ULONG sigBlobSize = sizeof(pSigBlob) / sizeof(COR_SIGNATURE);
hr = pEmtMSCorLibPrig->DefineField(mdtdNowGet, L"m_body",
fdPrivate | fdStatic,
pSigBlob, sigBlobSize,
ELEMENT_TYPE_VOID, NULL, 0, &mdfdNowGetm_body);
if (FAILED(hr))
BOOST_THROW_EXCEPTION(CppAnonymCOMException(hr));
}

D_COUT1("Token of FieldDef for System.Prig.PDateTime.NowGet.m_body: 0x%|1$08X|", mdfdNowGetm_body);
 
フィールドの定義です(776 行目)。

#line 875 "CppTroll\ProfilingApiSample03Stubber\ProfilingApiSample03Stubber.cpp"
// Create Param records.
mdParamDef mdpdNowGetset_Body0value = mdParamDefNil;
hr = pEmtMSCorLibPrig->DefineParam(mdmdNowGetset_Body, 0,
L"value", 0,
ELEMENT_TYPE_VOID, NULL, 0,
&mdpdNowGetset_Body0value);
if (FAILED(hr))
BOOST_THROW_EXCEPTION(CppAnonymCOMException(hr));

D_COUT1("Token of ParamDef for System.Prig.PDateTime.NowGet.set_Body, 0: value: 0x%|1$08X|", mdpdNowGetset_Body0value);
 
メソッドのパラメータの定義です(877 行目)。使うのは Body プロパティの setter 側ですね。static メソッドですので、パラメータの配置は 0 番目からです。

#line 888 "CppTroll\ProfilingApiSample03Stubber\ProfilingApiSample03Stubber.cpp"
// Create StandAloneSig records.
mdSignature mdsNowGetInitializeget_BodyLocals = mdSignatureNil;
{
COR_SIGNATURE pSigBlob[] = {
IMAGE_CEE_CS_CALLCONV_LOCAL_SIG,// LOCAL_SIG
0x01, // Count: 1
ELEMENT_TYPE_VALUETYPE, // Type[0]: VALUETYPE
0x0D // TypeRef: 0x01000003(System.DateTime)
};
ULONG sigBlobSize = sizeof(pSigBlob) / sizeof(COR_SIGNATURE);
hr = pEmtMSCorLibPrig->GetTokenFromSig(pSigBlob, sigBlobSize,
&mdsNowGetInitializeget_BodyLocals);
if (FAILED(hr))
BOOST_THROW_EXCEPTION(CppAnonymCOMException(hr));
}

D_COUT1("Token of StandAloneSig for System.Prig.PDateTime.NowGet.Initializeget_Body Locals: 0x%|1$08X|", mdsNowGetInitializeget_BodyLocals);
 
ローカル変数の定義です(898 行目)。
こんな簡単なプログラムにローカル変数必要?と思われる方もいらっしゃるかもしれませんが・・・。
実は当初、DateTime.Now の getter 側を新しいメソッドとして JIT 時に複製し(DateTime.CopiedNow みたいなのを作成)、それをスタブに参照させておくという戦略を取ったのですが、実行してみると MissingMethodException の嵐で・・・。泣く泣く元のメソッドの内容を表す IL ストリームについて、**Def → **Ref 変換しながらコピーするという方針転換をしました。
DateTime.Now の中身って、Version=2.0.0.0 までは DateTime.UtcNow.ToLocalTime() なので、値型の持つメソッド使うのに、一度変数に受けてから ldloca しないといけないという・・・。

あ。ちなみにこの方法、IL ストリーム中に private なものが混じってきたら終わりなので、また新しい戦略、考えます (ToT)

#line 908 "CppTroll\ProfilingApiSample03Stubber\ProfilingApiSample03Stubber.cpp"
// Create Property records.
mdProperty mdpNowGetBody = mdPropertyNil;
{
COR_SIGNATURE pSigBlob[] = {
IMAGE_CEE_CS_CALLCONV_PROPERTY, // PROPERTY
0, // ParamCount: 0
ELEMENT_TYPE_GENERICINST, // TYPE: GENERICINST
ELEMENT_TYPE_CLASS, // CLASS
0x05, // TypeRef: 0x01000001(System.Func`1)
1, // Generics Arguments Count: 1
ELEMENT_TYPE_VALUETYPE, // VALUETYPE
0x0D // TypeRef: 0x01000003(System.DateTime)
};
ULONG sigBlobSize = sizeof(pSigBlob) / sizeof(COR_SIGNATURE);
hr = pEmtMSCorLibPrig->DefineProperty(mdtdNowGet, L"Body",
0,
pSigBlob, sigBlobSize,
ELEMENT_TYPE_VOID, NULL, 0,
mdmdNowGetset_Body,
mdmdNowGetget_Body,
NULL, &mdpNowGetBody);
if (FAILED(hr))
BOOST_THROW_EXCEPTION(CppAnonymCOMException(hr));
}

D_COUT1("Token of Property for System.Prig.PDateTime.NowGet.Body: 0x%|1$08X|", mdpNowGetBody);
 
プロパティの定義です(922 行目)。ここまでで作成した、setter と getter を指定し、DefineProperty を呼ぶだけですね。
次に続く IL は代わり映えしないので割愛したいと思います。
PE フォーマットファイルは・・・若干新しいものが登場しますね。

#line 1165 "CppTroll\ProfilingApiSample03Stubber\ProfilingApiSample03Stubber.cpp"
{
// System.Prig.PDateTime.NowGet.Initializeget_Body method has Fat format header.
// Note that SetLocalVarSigTok is called with StandAloneSig for local variables,
// and GetSectionBlock is called with setting to DWORD size alignment.
COR_ILMETHOD_FAT fatHeader;
::ZeroMemory(&fatHeader, sizeof(COR_ILMETHOD_FAT));
fatHeader.SetMaxStack(1);
fatHeader.SetCodeSize(mbNowGetInitializeget_Body.Size());
fatHeader.SetLocalVarSigTok(mdsNowGetInitializeget_BodyLocals);
fatHeader.SetFlags(CorILMethod_InitLocals);

unsigned headerSize = COR_ILMETHOD::Size(&fatHeader, false);
unsigned totalSize = headerSize + mbNowGetInitializeget_Body.Size();

BYTE *pBuffer = NULL;
hr = pCeeFileGen->GetSectionBlock(textSection, totalSize, sizeof(DWORD),
reinterpret_cast<void**>(&pBuffer));
if (FAILED(hr))
BOOST_THROW_EXCEPTION(CppAnonymCOMException(hr));

ULONG offset = 0;
hr = pCeeFileGen->GetSectionDataLen(textSection, &offset);
if (FAILED(hr))
BOOST_THROW_EXCEPTION(CppAnonymCOMException(hr));

offset -= totalSize;
ULONG codeRVA = 0;
hr = pCeeFileGen->GetMethodRVA(ceeFile, offset, &codeRVA);
if (FAILED(hr))
BOOST_THROW_EXCEPTION(CppAnonymCOMException(hr));

hr = pEmtMSCorLibPrig->SetMethodProps(mdmdNowGetInitializeget_Body, -1, codeRVA, 0);
if (FAILED(hr))
BOOST_THROW_EXCEPTION(CppAnonymCOMException(hr));

pBuffer += COR_ILMETHOD::Emit(headerSize, &fatHeader, false, pBuffer);
::memcpy_s(pBuffer, totalSize - headerSize, mbNowGetInitializeget_Body.Ptr(),
mbNowGetInitializeget_Body.Size());
}
 
ローカル変数を持つメソッドは、問答無用で Fat フォーマットなメソッドになります。基本は、Tiny フォーマットなメソッドと同じなのですが、作っておいたローカル変数を表す StandAlongSig の指定(1173 行目)と、それらをデフォルト初期化する指定(1174 行目)、4 バイトアライメントで領域を生成すること(1180 行目)が追加で必要になります。ちなみに ICeeFileGen が属すのは Host API です。名称から連想しにくいためか、私なんかは探すときにいつもあっち行ったりこっち行ったりしてしまいます (>_<)

#line 1207 "CppTroll\ProfilingApiSample03Stubber\ProfilingApiSample03Stubber.cpp"
// Reserve a strong name area to sign to the assembly.
{
DWORD msCorLibPrigSignReserveSize = 0;
if (!::StrongNameSignatureSize(
reinterpret_cast<PBYTE>(pMSCorLibPrigPubKey.get()),
msCorLibPrigPubKeySize, &msCorLibPrigSignReserveSize))
BOOST_THROW_EXCEPTION(CppAnonymCOMException(::StrongNameErrorInfo()));

BYTE *pBuffer = NULL;
hr = pCeeFileGen->GetSectionBlock(textSection,
msCorLibPrigSignReserveSize, 1,
reinterpret_cast<void**>(&pBuffer));
if (FAILED(hr))
BOOST_THROW_EXCEPTION(CppAnonymCOMException(hr));

ULONG offset = 0;
hr = pCeeFileGen->GetSectionDataLen(textSection, &offset);
if (FAILED(hr))
BOOST_THROW_EXCEPTION(CppAnonymCOMException(hr));

offset -= msCorLibPrigSignReserveSize;
ULONG codeRVA = 0;
hr = pCeeFileGen->GetMethodRVA(ceeFile, offset, &codeRVA);
if (FAILED(hr))
BOOST_THROW_EXCEPTION(CppAnonymCOMException(hr));

::ZeroMemory(pBuffer, msCorLibPrigSignReserveSize);

hr = pCeeFileGen->SetStrongNameEntry(ceeFile, msCorLibPrigSignReserveSize, codeRVA);
if (FAILED(hr))
BOOST_THROW_EXCEPTION(CppAnonymCOMException(hr));
}
 
アセンブリに署名をするための領域を予約します。取得しておいた公開鍵から、署名のための領域サイズを取得し(1210 行目)、.text セクションに続けて配置します(1215 ~ 1231 行目)。そして SetStrongNameEntry を使い、紐付けを行います(1235 行目)。

#line 1257 "CppTroll\ProfilingApiSample03Stubber\ProfilingApiSample03Stubber.cpp"
// Sign to the assembly with Strong Naming API.
if (!::StrongNameSignatureGenerationEx(moduleNameOfMSCorLibPrig, NULL,
pMSCorLibPrigKeyPair.get(), msCorLibPrigKeyPairSize,
NULL, NULL, 0))
BOOST_THROW_EXCEPTION(CppAnonymCOMException(::StrongNameErrorInfo()));
 
最後に署名をして完成です!早速実行してみましょう!

C:\CppTroll\Debug>ProfilingApiSample03Stubber.exe
System.Core is here: C:\WINDOWS\assembly\GAC_MSIL\System.Core\3.5.0.0__b77a5c561934e089\System.Core.dll
Token of TypeDef for System.Func<T>: 0x02000058
Token of MethodDef for System.Func<T>..ctor: 0x06000232
Token of Assembly for System.Core.dll: 0x20000001
Assembly Name: System.Core
mscorlib is here: C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll
Token of TypeDef for System.Object: 0x02000002
Token of TypeDef for System.DateTime: 0x02000032
Token of TypeDef for System.Runtime.CompilerServices.CompilationRelaxationsAttribute: 0x020005CC
Token of TypeDef for System.Runtime.CompilerServices.RuntimeCompatibilityAttribute: 0x020005CC
Token of MethodDef for System.DateTime.get_Now: 0x060002D1
Token of MethodDef for System.Runtime.CompilerServices.CompilationRelaxationsAttribute..ctor: 0x0600374D
Token of MethodDef for System.Runtime.CompilerServices.RuntimeCompatibilityAttribute..ctor: 0x06003772
Token of Assembly for mscorlib.dll: 0x20000001
Assembly Name: mscorlib
Token of AssemblyRef for mscorlib.dll: 0x23000001
Token of AssemblyRef for System.Core.dll: 0x23000002
Token of TypeRef for System.Func`1: 0x01000001
Token of TypeRef for System.Object: 0x01000002
Token of TypeRef for System.DateTime: 0x01000003
Token of TypeRef for System.Runtime.CompilerServices.CompilationRelaxationsAttribute: 0x01000004
Token of TypeRef for System.Runtime.CompilerServices.RuntimeCompatibilityAttribute: 0x01000005
Token of TypeSpec for System.Func<DateTime>: 0x1B000001
Token of MemberRef for System.Func<DateTime>..ctor: 0x0A000001
Token of MemberRef for System.Runtime.CompilerServices.CompilationRelaxationsAttribute..ctor: 0x0A000002
Token of MemberRef for System.Runtime.CompilerServices.RuntimeCompatibilityAttribute..ctor: 0x0A000003
Token of MemberRef for System.DateTime.get_UtcNow: 0x0A000004
Token of MemberRef for System.DateTime.ToLocalTime: 0x0A000005
Public Key Blob of mscorlib.Prig:
SigAlgID: 0x00002400
HashAlgID: 0x00008004
Public Key: 0602000000240000525341310004000001000100B31D0603CEE69405E9D120F774839A632ABE14EB7E53812300ACF21778579F95720DE1B3F1E98BA0282B947D0FC1B177CD1BD5DFA2C781261ACE5C9D597F1CAB4565FA557C86AF9F5B550E1F3B88B70CB0C1B22E1413C2DCCD6352C4593FAF6E7FC7B2CB41A7744FDC097F4649396594F4C840429AA86D8B0EE48C5DF81613BD
Raw data: 0024000004800000940000000602000000240000525341310004000001000100B31D0603CEE69405E9D120F774839A632ABE14EB7E53812300ACF21778579F95720DE1B3F1E98BA0282B947D0FC1B177CD1BD5DFA2C781261ACE5C9D597F1CAB4565FA557C86AF9F5B550E1F3B88B70CB0C1B22E1413C2DCCD6352C4593FAF6E7FC7B2CB41A7744FDC097F4649396594F4C840429AA86D8B0EE48C5DF81613BD
Token of Assembly for mscorlib.Prig: 0x20000001
Token of CustomAttribute for System.Runtime.CompilerServices.CompilationRelaxationsAttribute: 0x0C000001
Token of CustomAttribute for System.Runtime.CompilerServices.RuntimeCompatibilityAttribute: 0x0C000002
Token of TypeDef for System.Prig.PDateTime: 0x02000002
Token of TypeDef for System.Prig.PDateTime.NowGet: 0x02000003
Token of FieldDef for System.Prig.PDateTime.NowGet.m_body: 0x04000001
Token of MethodDef for System.Prig.PDateTime.NowGet.get_Body: 0x06000001
Token of MethodDef for System.Prig.PDateTime.NowGet.set_Body: 0x06000002
Token of MethodDef for System.Prig.PDateTime.NowGet..cctor: 0x06000003
Token of MethodDef for System.Prig.PDateTime.NowGet.Initializeget_Body: 0x06000004
Token of ParamDef for System.Prig.PDateTime.NowGet.set_Body, 0: value: 0x08000001
Token of StandAloneSig for System.Prig.PDateTime.NowGet.Initializeget_Body Locals: 0x11000001
Token of Property for System.Prig.PDateTime.NowGet.Body: 0x17000001

C:\CppTroll\Debug>
 
GitHub に上がっているものは OUTPUT_DEBUG が有効になっていますので、こんな感じでベロベロと情報が出力されます。例外が発生しなければ、同フォルダに、処理を盗むコソドロを紛れ込ませた mscorlib.Prig.dll が生成されているはずです。中身を Reflectorで確認するとこんな感じ。

え、今回の処理だったら、普通に C# で書けるって?・・・はい・・・確かに・・・そうですよね・・・ (´・ω・`)


2. 処理を入れ替えるプログラム
プロファイラの記述にマネージコードが使えればいいのですが、MSDN にもあるとおり、そううまくはいかないみたいですので、きっとスタブ作るプログラム書いたのも無駄にはならないと信じましょう (^_^;)
さて、処理を入れ替えるためのプロファイラです。こちらも前回からの差分だけ見ていきます。

#line 58 "CppTroll\ProfilingApiSample03\ExeWeaver3.cpp"
// Initialize the profiling API.
hr = pICorProfilerInfoUnk->QueryInterface(IID_ICorProfilerInfo2,
reinterpret_cast<void**>(&m_pInfo));
if (FAILED(hr))
BOOST_THROW_EXCEPTION(CppAnonymCOMException(hr));


// Set a value that specifies the types of events for which the profiler
// wants to receive notification from CLR.
// NOTE: If you want to profile core APIs such as types in mscorlib,
// you should set COR_PRF_USE_PROFILE_IMAGES.
DWORD event_ = COR_PRF_MONITOR_MODULE_LOADS |
COR_PRF_MONITOR_JIT_COMPILATION |
COR_PRF_USE_PROFILE_IMAGES;
hr = m_pInfo->SetEventMask(event_);
if (FAILED(hr))
BOOST_THROW_EXCEPTION(CppAnonymCOMException(hr));
 
mscorlib のような標準ライブラリを含めてプロファイルする時は、COR_PRF_USE_PROFILE_IMAGES フラグを指定して拡張イメージ検索を有効にする必要があります(69 行目)。後はほとんど同じですね。

#line 99 "CppTroll\ProfilingApiSample03\ExeWeaver3.cpp"
// Convert ModuleID to the name.
LPCBYTE pBaseLoadAddress = NULL;
WCHAR modName[MAX_SYM_NAME] = { 0 };
ULONG modNameSize = sizeof(modName);
AssemblyID asmId = 0;

hr = m_pInfo->GetModuleInfo(moduleId, &pBaseLoadAddress,
modNameSize, &modNameSize, modName, &asmId);
if (hr != CORPROF_E_DATAINCOMPLETE && FAILED(hr))
BOOST_THROW_EXCEPTION(CppAnonymCOMException(hr));


// If target module is detected, the object implemented IMetaDataEmit is initialized
// to use the following process.
path modPath(modName);

V_WCOUT1(L"ModuleLoadStarted: %|1$s|", modName);
if (modPath.filename().wstring() == MODULE_NAME_OF_MS_COR_LIB)
{
V_WCOUT(L"The target module is detected. Getting module meta data is started.");

hr = m_pInfo->GetModuleMetaData(moduleId, ofRead | ofWrite, IID_IMetaDataEmit2,
reinterpret_cast<IUnknown **>(&m_pEmtMSCorLib));
if (hr != CORPROF_E_DATAINCOMPLETE && FAILED(hr))
BOOST_THROW_EXCEPTION(CppAnonymCOMException(hr));
}
 
プロファイル中に、メモリ上のアセンブリを書き換えますので、読み書きモードで Meta Data API への参照を取得しておきます。利用する Profiling API によっては、まだ完全にデータが揃っていないことを示す CORPROF_E_DATAINCOMPLETE を返してくることもありますが、必要なデータさえあるのであれば無視してしまって良いでしょう。
さて・・・続き・・・と思いましたが、後は以前解説した API とスタブ生成の時に使用した API しか使っていませんので割愛します。
書き換えのイメージはこんな感じになります。

// Before
public struct DateTime
{
public static DateTime Now
{
get
{
return UtcNow.ToLocalTime();
}
}
}

↓↓↓↓

// After
public struct DateTime
{
public static DateTime Now
{
get
{
return PDateTime.NowGet.Body();
}
}
}
 


結果
実行です・・・とその前にビルドが通らなかったテストコードをビルドしておきます。mscorlib.Prig.dll を参照設定してビルドっと。
・・・O.K. ですね!
こちらのやりかたですと、元々のコードに全く手を加えていませんので、普通に実行すると元の通りの動きになります。

C:\CppTroll\Debug>xunit.console.exe ProfilingApiSample03TargetTest.dll /noshadow
xUnit.net console test runner (32-bit .NET 2.0.50727.3625)
Copyright (C) 2007-11 Microsoft Corporation.

xunit.dll: Version 1.8.0.1545
Test assembly: C:\CppTroll\Debug\ProfilingApiSample03TargetTest.dll

MyLibraryTest.LifeInfoTest.LunchBreakTest01_NowIsLunchBreak [FAIL]
Assert.Equal() Failure
Position: First difference is at position 4
Expected: 時刻: 12 お昼休みなう!

Actual: 時刻: 7 お仕事なう・・・
Stack Trace:
場所 Xunit.Assert.Equal[T](T expected, T actual, IEqualityComparer`1 comparer)
場所 Xunit.Assert.Equal[T](T expected, T actual)
場所 MyLibraryTest.LifeInfoTest.LunchBreakTest01_NowIsLunchBreak() 場所 C:\CppTroll\ProfilingApiSample03TargetTest\ProgramTest.cs:行 31

MyLibraryTest.LifeInfoTest.LunchBreakTest02_NowIsNotLunchBreak [FAIL]
Assert.Equal() Failure
Position: First difference is at position 4
Expected: 時刻: 13 お仕事なう・・・

Actual: 時刻: 7 お仕事なう・・・
Stack Trace:
場所 Xunit.Assert.Equal[T](T expected, T actual, IEqualityComparer`1 comparer)
場所 Xunit.Assert.Equal[T](T expected, T actual)
場所 MyLibraryTest.LifeInfoTest.LunchBreakTest02_NowIsNotLunchBreak() 場所 C:\CppTroll\ProfilingApiSample03TargetTest\ProgramTest.cs:行 45

MyLibraryTest.LifeInfoTest.LunchBreakTest01_NowIsHoliday [FAIL]
Assert.Equal() Failure
Position: First difference is at position 4
Expected: 曜日: Sunday 休日なう!

Actual: 曜日: Tuesday お仕事なう・・・
Stack Trace:
場所 Xunit.Assert.Equal[T](T expected, T actual, IEqualityComparer`1 comparer)
場所 Xunit.Assert.Equal[T](T expected, T actual)
場所 MyLibraryTest.LifeInfoTest.LunchBreakTest01_NowIsHoliday() 場所 C:\CppTroll\ProfilingApiSample03TargetTest\ProgramTest.cs:行 59

MyLibraryTest.LifeInfoTest.LunchBreakTest02_NowIsNotHoliday [FAIL]
Assert.Equal() Failure
Position: First difference is at position 4
Expected: 曜日: Monday お仕事なう・・・

Actual: 曜日: Tuesday お仕事なう・・・
Stack Trace:
場所 Xunit.Assert.Equal[T](T expected, T actual, IEqualityComparer`1 comparer)
場所 Xunit.Assert.Equal[T](T expected, T actual)
場所 MyLibraryTest.LifeInfoTest.LunchBreakTest02_NowIsNotHoliday() 場所 C:\CppTroll\ProfilingApiSample03TargetTest\ProgramTest.cs:行 73

4 total, 4 failed, 0 skipped, took 0.442 seconds

C:\CppTroll\Debug>
 
プロファイラを有効にして、処理を入れ替えます。
本来であれば、環境変数を弄って実行環境を整えるための **Runner のようなアプリが欲しいところですが、とりあえず手動でプロファイラを有効にしましょう。

C:\CppTroll\Debug>SET COR_ENABLE_PROFILING=1

C:\CppTroll\Debug>SET COR_PROFILER={ACC35A1C-B127-4A75-9EB8-B4E54A49F6CF}

C:\CppTroll\Debug>xunit.console.exe ProfilingApiSample03TargetTest.dll /noshadow
xUnit.net console test runner (32-bit .NET 2.0.50727.3625)
Copyright (C) 2007-11 Microsoft Corporation.

xunit.dll: Version 1.8.0.1545
Test assembly: C:\CppTroll\Debug\ProfilingApiSample03TargetTest.dll

4 total, 0 failed, 0 skipped, took 0.463 seconds

C:\CppTroll\Debug>
 
はい!テスト通りました!

後は煮るなり焼くなり、お好きにリファクタリングしていただけると思います。
ところで、まだ構想段階ですが、どうせ後発でこういうもの作るなら、もっと API を整理して、処理を入れ替えるにしても、テスト時は手早く、リリース時は確実に(もしくはパフォーマンスを上げて)織り込んで、のような柔軟性があると面白いと思います。
それこそ、動的言語のような Monkey Pachingができると、うれしい場面も少なくないでしょう。そうすると・・・ちょっとは私のライブラリも存在意義が出てくるかもしれませんね (^_^;)




終わりに
C# Advent Calendar 2011 13 日目は、「アンマネージ API」を取り上げさせていただきました。いかがでしたか?
ほんと、申し訳程度の C# 要素でゴメンナサイ (>_<) でも、こんな世界もあるということで、低レイヤなことに興味を持っていただける方が少しでも増えれば幸いです。

最後になりましたが、Advent Calendar の参加をどうしようか迷ってたときに背中を押してくださった @biacさん、ありがとうございます!このような錚々たる面々の記事の中で、今年の成果を報告できたことはすごく刺激になりました <(_ _)>

もし来年もこのようなイベントがあれば、ぜひまた参加させていただければと思います。
次は C# ばかりのサンプルになることを祈りつつ、ですね!・・・( = =)

二つの AppDomain の狭間で - I wish we human would be gatekeepers of CLR... -

$
0
0
もっと境界を自由に行き来できるようになりたい!

.NET Framework を使われている方は、もしかしたら聞いたことがあるかもしれない機能、AppDomain。
MSDN では「アプリケーションを分離する利点」として次のような説明がされています。
  1. あるアプリケーションで実行されているコードは、他のアプリケーションのコードに直接アクセスできないよ。
  2. あるアプリケーションで発生したエラーが他のアプリケーションに影響することはないよ。
  3. プロセス全体を停止せずに、各アプリケーションを停止できるよ。
  4. コードの構成情報(読み込むアセンブリのバージョンポリシーや場所)をアプリケーション毎に決められるよ。
  5. コードに与えるアクセス許可をアプリケーション毎に制御できるよ。
プロセスに比べ、その生成コストや相互通信コストが低いため、サーバーに利用すればそのスケーラビリティは飛躍的に向上し、セキュリティレベルを上げたサンドボックスとして利用することで、安全にサードパーティ製のコードを実行でき、自作アプリ/業務アプリ問わず、任意のプログラミング言語で、生産性向上のためのマクロや新しい機能のアドインを作成できる・・・etc
と各所でイチオシされる CLR の中核機能なのですが、利用されてる方が周りにあまりいらっしゃらないのが悲しいところ (ToT)

今回は、最初の一歩と開発時に役立つ小品など、AppDomain をもっと身近に感じられるような情報を共有させていただければというエントリとなります。

同じ仕組みは CreateProcess やら LoadLibrary やらの Platform API を叩いてもできるとは思いますが、それよりだいぶ手軽かと思いますので、もしこれを機に使ってみようという方が増えれば光栄に思いますです <(_ _)>

※文中にあるソリューションは以下のリンクからダウンロードできます。ビルド環境は、Visual Studio C# 2010、あと C# 側の自動テストに NUnitを使っています。環境を整える際は、各ライブラリのインストールをお願いしますです。


こちらのページ/ソフトウェアを参考にさせていただきました。この場を借りてお礼申し上げます<(_ _)>
Download Details - Microsoft Download Center - Shared Source Common Language Infrastructure 2.0 Release
Cecil - Mono
ECMA C# and Common Language Infrastructure Standards
Microsoft.ExtendedReflection
IL rewriting : calli opcode and metadata token for a stand-alone signature
Haibo Luo's weblog[FeedShow RSS reader] Turn MethodInfo to DynamicMethod
DebuggerVisualizer for DynamicMethod (Show me the IL) - Haibo Luo's weblog - Site Home - MSDN Blogs
An Overview of Managed/Unmanaged Code Interoperability
#6238 (has_binary_operator.hpp contains some non-ASCII characters) Boost C++ Libraries
WOW64 and Your Profiler - David Broman's CLR Profiling API Blog - Site Home - MSDN Blogs
表示設定 - システム - PCテクニック - オンラインPC館
Editor Guidelines - Visual Studio Gallery
Profiler Attach and Detach
Joel Pobar's Weblog | SSCLI 2.0 Patch for VS 2010
c# - DynamicMethod and out-parameters? - Stack Overflow
64ビット対応のDLLインジェクション(CreateRemoteThread+LoadLibrary) - SIN@SAPPOROWORKSの覚書
Cross AppDomain Singleton - Blog'A'Little
Cross-AppDomain Singleton in C#
第8回c#ユーザー会-AppDomain
NUnit - Home





目次

何を分離してるし
普段、我々がアプリケーションと言えばプロセスそのものを指しますので、プロセスの中にアプリケーションがいくつもある、って言われてもなかなかイメージが湧かないかもしれません。ここで、CLR で実行される exe や dll について、未実行時の構造と実行時の構造をちょっと比べてみましょう。

【未実行時】
未実行時は左の図のような構造になっています。exe や dll 1 つ 1 つを Assembly と呼びます。この Assembly、未実行だと最上位に位置し、exe や dll 1 つ につき 1 つしか存在しません。Assembly の下に Module がぶら下がり、Module の下に Type が、その下に Method、Field、Property、Event、Type(Nested Type) がぶら下がる格好になります。

【実行時】
対して、実行中は上に階層が伸び、未実行時は exe や dll 1 つにつき 1 つしか存在しなかった Assembly が、Process x AppDomain 分存在することになります(JIT されたメソッド本体など、条件によっては共有できるものもあります)。
Process というのが、普段我々が目にしているプロセスそのものであり、その下の AppDomain が CLR がアプリケーションと呼んでいるものに当たります。
従って、AppDomain というのは、Process に読み込まれる Assembly、所謂コードが読み込まれる領域(メモリ領域)を表す、ということになります。文字通り、アプリケーションのドメイン(領域)を分離しているんですね。

今時の OS や開発言語では、Process 内の処理をある単位で分離して扱える Thread を持っていることがほとんどかと思いますが、CLR ではさらに進めて、それらが扱う領域を分離して扱うことが可能になっているわけです。もちろん 1 つの Thread が複数の AppDomain にある情報を処理して回ることもできれば、1 つの AppDomain にある情報を複数の Thread が処理することもできます。

ちなみに、Java をやられてる方であれば、階層構造や Bytecode Instrumentation が無い ClassLoader をイメージしていただければ大体合っているようです。.NET で細かなカスタマイズをやるには、アンマネージ API が必要になりますので・・・ね・・・(ToT)。




簡単な使い方と制限
アプリケーション毎の領域を分離することが目的なので、その境界を越えるには特別なルールが必要になっています。それは、
  ・異なる AppDomain で情報を共有するためには、値をコピーするか、参照を保持する Proxy を用意しなければならない
というものです。

前者は SerializableAttribute 属性の適用、後者は MarshalByRefObject 型の継承をすることで実現できるようになっています。
以下簡単なサンプルを。まずは、SerializableAttribute 属性の適用からやってみましょう。

#line 1 "NTroll\Urasandesu.NTroll.DomainFree\SClass.cs"
using System;
using System.Runtime.CompilerServices;

namespace Urasandesu.NTroll.DomainFree
{
[Serializable]
public class SClass
{
public object InstanceMember { get; set; }
public static object StaticMember { get; set; }
public void Run()
{
Console.WriteLine("AppDomain: {0}",
AppDomain.CurrentDomain.FriendlyName);
Console.WriteLine("Serializable Class Static Member: {0}",
RuntimeHelpers.GetHashCode(StaticMember));
Console.WriteLine("Serializable Class Instance Member: {0}",
RuntimeHelpers.GetHashCode(InstanceMember));
}
}
}
 

#line 75 "NTroll\Urasandesu.NTroll.DomainFree\Program.cs"
using System;
using System.Runtime.Remoting;

namespace Urasandesu.NTroll.DomainFree
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Serializable Class Test: " +
"Default AppDomain -> Test Domain");
{
SClass.StaticMember = new object();
var o = new SClass();
Console.WriteLine("o is Transparent Proxy?: {0}",
RemotingServices.IsTransparentProxy(o));
o.InstanceMember = new object();
o.Run();

var info = AppDomain.CurrentDomain.SetupInformation;
var testDomain = AppDomain.CreateDomain("Test Domain", null, info);
testDomain.DoCallBack(new CrossAppDomainDelegate(o.Run));
AppDomain.Unload(testDomain);
}
Console.WriteLine();


Console.WriteLine("Serializable Class Test: " +
"Default AppDomain <- Test Domain");
{
var info = AppDomain.CurrentDomain.SetupInformation;
var testDomain = AppDomain.CreateDomain("Test Domain", null, info);
var t = typeof(SClass);
var o = (SClass)testDomain.CreateInstanceAndUnwrap(t.Assembly.FullName, t.FullName);
Console.WriteLine("o is Transparent Proxy?: {0}",
RemotingServices.IsTransparentProxy(o));

SClass.StaticMember = new object();
o.InstanceMember = new object();
o.Run();

testDomain.DoCallBack(new CrossAppDomainDelegate(o.Run));
AppDomain.Unload(testDomain);
}
Console.WriteLine();

// The example displays the following output:
//
// Serializable Class Test: Default AppDomain -> Test Domain
// o is Transparent Proxy?: False
// AppDomain: Urasandesu.NTroll.DomainFree.exe
// Serializable Class Static Member: 54267293
// Serializable Class Instance Member: 18643596
// AppDomain: Test Domain
// Serializable Class Static Member: 0
// Serializable Class Instance Member: 18796293
//
// Serializable Class Test: Default AppDomain <- Test Domain
// o is Transparent Proxy?: False
// AppDomain: Urasandesu.NTroll.DomainFree.exe
// Serializable Class Static Member: 12289376
// Serializable Class Instance Member: 43495525
// AppDomain: Test Domain
// Serializable Class Static Member: 0
// Serializable Class Instance Member: 43942917
}
}
}
 
Process に紐付いた最初の AppDomain は自動的に作成されます。追加で AppDomain を作成したい時ですが、手順は非常に簡単で、AppDomain.CreateDomain を呼べば作成できます(95 行目、106 行目)。DoCallBack で作成した AppDomain にオブジェクトを送り込み、実行することができます(96 行目、116 行目)。作成した AppDomain 内にオブジェクトを生成するには一連の CreateInstance** メソッドが利用できます(108 行目)。必要なくなったら、AppDomain.Unload を呼び、アンロード依頼を出しておきましょう(97 行目、117 行目)。現在の Thread が実行中の AppDomain にアクセスするには、AppDomain.CurrentDomain を使います(14 行目)。あと、AppDomain.FriendlyName で CreateDomain 時に付けた名前が取得できます。デフォルトでは Module 名になります(同じく、14 行目)。

さて、SerializableAttribute なクラスの場合、保持しているメンバの情報もコピーされます。インスタンスメンバはコピーされた結果、新しいオブジェクトになっていることがわかります(127 行目、130 行目、136 行目、139 行目)。静的メンバも最初に書きました通り、AppDomain 毎に領域が取られますので、新しいオブジェクトになります。明示的に初期化していませんので、null になっているのがわかりますね(126 行目⇒129 行目、135 行目⇒138 行目)。

次は、MarshalByRefObject 型の継承を見てみましょう。

#line 1 "NTroll\Urasandesu.NTroll.DomainFree\MClass.cs"
using System;
using System.Runtime.CompilerServices;

namespace Urasandesu.NTroll.DomainFree
{
public class MClass : MarshalByRefObject
{
public object InstanceMember { get; set; }
public static object StaticMember { get; set; }
public void Run()
{
Console.WriteLine("AppDomain: {0}",
AppDomain.CurrentDomain.FriendlyName);
Console.WriteLine("MarshalByRefObject Class Static Member: {0}",
RuntimeHelpers.GetHashCode(StaticMember));
Console.WriteLine("MarshalByRefObject Class Instance Member: {0}",
RuntimeHelpers.GetHashCode(InstanceMember));
}
}
}
 

#line 6 "NTroll\Urasandesu.NTroll.DomainFree\Program.cs"
using System;
using System.Runtime.Remoting;

namespace Urasandesu.NTroll.DomainFree
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("MarshalByRefObject Class Test: " +
"Default AppDomain -> Test Domain");
{
MClass.StaticMember = new object();
var o = new MClass();
Console.WriteLine("o is Transparent Proxy?: {0}",
RemotingServices.IsTransparentProxy(o));
o.InstanceMember = new object();
o.Run();

var info = AppDomain.CurrentDomain.SetupInformation;
var testDomain = AppDomain.CreateDomain("Test Domain", null, info);
testDomain.DoCallBack(new CrossAppDomainDelegate(o.Run));
AppDomain.Unload(testDomain);
}
Console.WriteLine();


Console.WriteLine("MarshalByRefObject Class Test: " +
"Default AppDomain <- Test Domain");
{
var info = AppDomain.CurrentDomain.SetupInformation;
var testDomain = AppDomain.CreateDomain("Test Domain", null, info);
var t = typeof(MClass);
var o = (MClass)testDomain.CreateInstanceAndUnwrap(t.Assembly.FullName, t.FullName);
Console.WriteLine("o is Transparent Proxy?: {0}",
RemotingServices.IsTransparentProxy(o));

MClass.StaticMember = new object();
o.InstanceMember = new object();
o.Run();

testDomain.DoCallBack(new CrossAppDomainDelegate(o.Run));
AppDomain.Unload(testDomain);
}
Console.WriteLine();

// The example displays the following output:
//
// MarshalByRefObject Class Test: Default AppDomain -> Test Domain
// o is Transparent Proxy?: False
// AppDomain: Urasandesu.NTroll.DomainFree.exe
// MarshalByRefObject Class Static Member: 54267293
// MarshalByRefObject Class Instance Member: 18643596
// AppDomain: Urasandesu.NTroll.DomainFree.exe
// MarshalByRefObject Class Static Member: 54267293
// MarshalByRefObject Class Instance Member: 18643596
//
// MarshalByRefObject Class Test: Default AppDomain <- Test Domain
// o is Transparent Proxy?: True
// AppDomain: Test Domain
// MarshalByRefObject Class Static Member: 0
// MarshalByRefObject Class Instance Member: 55915408
// AppDomain: Test Domain
// MarshalByRefObject Class Static Member: 0
// MarshalByRefObject Class Instance Member: 55915408
}
}
}
 
MarshalByRefObject なクラスの場合、保持しているメンバは Proxy を通じてアクセスされます。元の AppDomain と全く同じものを指しているようにも見えますが、作成した AppDomain 内で構築したオブジェクトに対して RemotingServices.IsTransparentProxy を呼べば、それが Proxy であることがわかります(21 行目、41 行目)。
異なる AppDomain のオブジェクトをリモート操作できるような使い勝手で、非常に強力なのですが(57 行目~70 行目)、単一継承しか許されない C# のような言語では、あまり気軽にとは行かないかもしれません。

以上が制限になります。結構厳しいかも (^_^;)
コンパイラが自動生成をがんばってしまう C# や VB .NET ですと、気付かないうちに匿名メソッドやラムダ式の中に外部の環境を取り込んでしまい、いつの間にか境界を越えることができなくなってたりしますのでご注意を。いざとなったら、ILSpyのような逆アセンブルツールに掛け、意図していないことになっていないか確認すると良いでしょう。

簡単なサンプルを見たところで、次は実用的なサンプルを見ていきましょう。
「領域を分離すること」というキーワードと開発者が馴染み深いと言えば・・・そう、自動化された単体テストです!



シングルトンと仲良くなる! - Singleton Pattern must die! -
シングルトンパターンとは、GoF による 23 種のデザインパターンの 1 つ。もう説明するまでも無いほどよく知られているとは思いますが、そのクラスのインスタンスが 1 つしか生成されないことを保証するためのデザインパターンです。 この特性から、用途としては、以下のようなことをするクラスに使われていることが多いと思います。
  • パフォーマンス向上のため、DB の接続をプールしたり、ネットワーク通信をキャッシュする。
  • イベントログなど、アプリケーション全体で制御する必要があるファイルへの書き込みを行う。
  • アプリケーションの設定情報やリソースなどを起動時に読み込み、アクセス手段を一元化する。

ところで、何らかの自動化された単体テストを書かれている方々の界隈で有名な書籍に「レガシーコード改善ガイド」というものがあります。「テストがないコードはレガシーコードだ!」の帯が衝撃的ですよね (>_<)
曰く、以下のことを行うテストは単体テストではありません。
  • DB とやりとりやネットワークを介した通信をする。
  • ファイルシステムにアクセスする。
  • 実行に特別の環境設定を必要とする(設定ファイルの編集など)。


うわー・・・なにこの一致・・・


シングルトンパターンが使われているクラスの用途と見事に符合しますね・・・。
実はこのパターン、単体テストしにくいことでも有名なパターンだそうなのですが、なるほど納得です (^_^;)

しかしながら、このような境界面というのはまた、不具合が発生しやすいということでも皆さんの支持を得られるところでしょう。接続した際にうまく動かないなんて時、やりとりするデータの仕様にミスや勘違いがあるのか、はたまた単に機能がバグっているのか。問題の切り分けのためにも、単体での動作は保証しておきたいところなのですが・・・。

さあ、こんな時こそ AppDomain の出番です!論より実装、さっそく例を見ていきましょう。


設定ファイルの読み込み(ConfigurationManager)
前回の具体性に欠ける例で申し訳ないのですが、ここで設定を読み込むのに使われていた ConfigurationManager を取り上げます。あなたはこの ThirdPartyLibrary を提供している会社の開発者の一人ですが、当初 ConfigurationManager を担当していた人間はいない・・・という設定で話を進めてみましょう。

最初は問題がなかったようなのですが、使われ始めてしばらくすると以下のような改善要望が挙がってくるようになりました。

「現地で設定を手修正することもあるのだが、現状だとエラーチェックが厳しすぎる。
本来であれば構成ファイルの設定用ツールがあるべきだとは思うが・・・もう少しなんとかならないだろうか。」

話を聞くと、現状では「Sunday」を「sunday」や「SUNDAY」と入れただけでも例外が発生してしまうとのこと。なるほど確認してみます、とソースコードを覗いてみました。

#line 148 "NTroll\Urasandesu.NTroll.DomainFree\ConfigurationManager.cs"
using System;
using System.Collections.Generic;
using System.Threading;

namespace ThirdPartyLibrary
{
public class ConfigurationManager
{
public static T GetProperty<T>(string key, T defaultValue)
{
var value = global::System.Configuration.ConfigurationManager.AppSettings[key];
var impl = GetPropertyImpl<T>.CreateInstanceWithCache(GetPropertyImpl<T>.CreateInstance);
if (impl == null)
throw new NotSupportedException();
return impl.ToPropertyWithCache(key, value, defaultValue);
}

protected abstract class GetPropertyImpl<T> // 省略...


class GetPropertyImplForDayOfWeek : GetPropertyImpl<DayOfWeek>
{
protected override DayOfWeek ToPropertyCore(string key, string value, DayOfWeek defaultValue)
{
if (string.IsNullOrEmpty(value))
{
return defaultValue;
}
else if (Enum.IsDefined(typeof(DayOfWeek), value))
{
return (DayOfWeek)Enum.Parse(typeof(DayOfWeek), value);
}
else
{
var fmt = "The value \"{0}\" linked by key \"{1}\" is invalid. " +
"It must take one of the following values: \r\n{2}.";
var msg = string.Format(fmt, value, key, string.Join(", ", Enum.GetNames(typeof(DayOfWeek))));
throw new ArgumentException(msg, "value");
}
}
}

protected ConfigurationManager() { }
}
}
 
あー・・・Enum.IsDefined は大文字・小文字を区別するチェックしかできないんでしたっけ(260 行目)。.NET 4 以降になれば、Enum.TryParse<T> メソッドが使えるんでしょうが、レガシーシステムだけあってそれは無理な感じ。取りうる値を、IgnoreCase な正規表現にして並べるのが確実そうです。
さて、やり方も決めたし、とりあえずテストコードを直しますか・・・ConfigurationManagerTest・・・あったこれですね。ちょいちょいっと・・・。

#line 339 "NTroll\Test.Urasandesu.NTroll.DomainFree\ConfigurationManagerTest.cs"
using System;
using System.IO;
using NUnit.Framework;
using ThirdPartyLibrary;
using Urasandesu.NTroll.DomainFree;

namespace Test.Urasandesu.NTroll.DomainFree
{
[TestFixture]
public class ConfigurationManagerTest
{
[TestFixtureSetUp]
public void TestFixtureSetUp()
{
}

[TestFixtureTearDown]
public void TestFixtureTearDown()
{
}

[Test]
public void GetPropertyTestSuccess01ExistKeyExistValue()
{
var holiday = ConfigurationManager.GetProperty("Holiday", DayOfWeek.Sunday);
Assert.AreEqual(DayOfWeek.Monday, holiday);
}

[Test]
[Ignore("This test cannot be passed through. " +
"Though it needs modifying App.config, I could not do it " +
"because the configurations have already been cached " +
"when passing the success path.")]
public void GetPropertyTestError01AppConfigNotFound()
{
//
}

[Test]
[ExpectedException(typeof(System.Configuration.ConfigurationErrorsException))]
[Ignore("This test cannot be passed through. " +
"Though it needs modifying App.config, I could not do it " +
"because the configurations have already been cached " +
"when passing the success path.")]
public void GetPropertyTestError02InvalidAppConfig()
{
//
}

[Test]
[Ignore("This test cannot be passed through. " +
"Though it needs modifying App.config, I could not do it " +
"because the configurations have already been cached " +
"when passing the success path.")]
public void GetPropertyTestError03HolidayPropertyNotFound()
{
//
}

[Test]
[Ignore("This test cannot be passed through. " +
"Though it needs modifying App.config, I could not do it " +
"because the configurations have already been cached " +
"when passing the success path.")]
public void GetPropertyTestError04HolidayPropertyIsEmpty()
{
//
}

[Test]
[ExpectedException(typeof(ArgumentException))]
[Ignore("This test cannot be passed through. " +
"Though it needs modifying App.config, I could not do it " +
"because the configurations have already been cached " +
"when passing the success path.")]
public void GetPropertyTestError05HolidayPropertyIsInvalidIfIgnoredCase()
{
//
}

[Test]
[ExpectedException(typeof(ArgumentException))]
[Ignore("This test cannot be passed through. " +
"Though it needs modifying App.config, I could not do it " +
"because the configurations have already been cached " +
"when passing the success path.")]
public void GetPropertyTestError06HolidayPropertyIsInvalidIfCaseSensitive()
{
//
}
}
}
 


(´・ω・`)


(´・ω:;.:...


(´:;....::;.:. :::;.. .....


ちょ・・・肝心なところが Ignore になってるじゃん /(^o^)\

Reason を見ると、例外のテストのためにはアプリケーション構成ファイルの書き換えが必要なんですけど、正常系パスを通す時にすでにキャッシュされてしまうのでできませんでした、的な言い訳が書いてあります。
・・・ (´・ω・`)そういうことですか・・・

ダークサイドに堕ちる前に
AppDomain を知らないと「前の人もやってなかったんだから、あなたもやらなくていいんじゃん? Ψ(`∀´)Ψケケケ」という悪魔の誘いに乗ってしまうかもしれません。暗黒面に堕ちてしまう前に、急いでいくつかのクラスを追加します。

#line 8 "NTroll\Urasandesu.NTroll.DomainFree\MarshalByRefRunners.cs"
public class MarshalByRefAction : MarshalByRefObject
{
public Action Action { get; set; }
public void Run()
{
if (Action != null)
Action();
}
}
 
MarshalByRefAction は、指定されたデリゲートを、自身が作成された AppDomain で実行するだけの簡単なユーティリティです。

#line 1 "NTroll\Urasandesu.NTroll.DomainFree\AppDomainMixin.cs"
using System;
using System.Security.Policy;

namespace Urasandesu.NTroll.DomainFree
{
public static class AppDomainMixin
{
public static void RunAtIsolatedDomain(this AppDomain source, Action action)
{
if (source == null)
throw new ArgumentNullException("source");
RunAtIsolatedDomain(source.Evidence, source.SetupInformation, action);
}

public static void RunAtIsolatedDomain(this AppDomain source, Evidence securityInfo, Action action)
{
if (source == null)
throw new ArgumentNullException("source");
RunAtIsolatedDomain(securityInfo, source.SetupInformation, action);
}

public static void RunAtIsolatedDomain(this AppDomain source, AppDomainSetup info, Action action)
{
if (source == null)
throw new ArgumentNullException("source");
RunAtIsolatedDomain(source.Evidence, info, action);
}

public static void RunAtIsolatedDomain(Evidence securityInfo, AppDomainSetup info, Action action)
{
if (action == null)
throw new ArgumentNullException("action");
if (!action.Method.IsStatic)
throw new ArgumentException("The parameter must be the reference of a " +
"static method.", "action");

var domain = default(AppDomain);
try
{
domain = AppDomain.CreateDomain("Domain " + action.Method.ToString(),
securityInfo, info);
var type = typeof(MarshalByRefAction);
var runner = (MarshalByRefAction)domain.CreateInstanceAndUnwrap(
type.Assembly.FullName, type.FullName);
runner.Action = action;
runner.Run();
}
finally
{
try
{
if (domain != null)
AppDomain.Unload(domain);
}
catch { }
}
}
}
}
 
そして AppDomainMixin。簡単な使い方と制限で説明した処理をひとまとめにし、指定された処理を分離された環境で実行するというものになります(40 行目~46 行目)。

話は逸れますが、このような「リソースのオープン時にクローズも確実にした処理を共通化しておき、ユーザーにリソースを貸し出す」定石をローンパターン(Loan Pattern)と呼ぶそうですが、何を勘違いしたのか、ずっとレントパターン(Rent Pattern)と言っていました(恥。C++ でよく言われる RAII(Resource Acquisition Is Initialization)を R から始まる単語~みたいな感じで覚えてましたので混じっちゃったんですかね (^_^;)

さてさて、話を戻します。これを使って ConfigurationManagerTest で出来ていなかった異常系を書いてみましょう。

#line 201 "NTroll\Test.Urasandesu.NTroll.DomainFree\ConfigurationManagerTest.cs"
[Test]
public void GetPropertyTestError01AppConfigNotFound()
{
var info = new AppDomainSetup();
info.ApplicationBase = AppDomain.CurrentDomain.SetupInformation.ApplicationBase;
info.ShadowCopyFiles = AppDomain.CurrentDomain.SetupInformation.ShadowCopyFiles;
info.ConfigurationFile = Path.GetTempFileName();
File.Delete(info.ConfigurationFile);
AppDomain.CurrentDomain.RunAtIsolatedDomain(info, () =>
{
var holiday = ConfigurationManager.GetProperty("Holiday", DayOfWeek.Sunday);
Assert.AreEqual(holiday, DayOfWeek.Sunday);
});
}

[Test]
[ExpectedException(typeof(System.Configuration.ConfigurationErrorsException))]
public void GetPropertyTestError02InvalidAppConfig()
{
var info = new AppDomainSetup();
info.ApplicationBase = AppDomain.CurrentDomain.SetupInformation.ApplicationBase;
info.ShadowCopyFiles = AppDomain.CurrentDomain.SetupInformation.ShadowCopyFiles;
info.ConfigurationFile = Path.GetTempFileName();
using (var sw = new StreamWriter(info.ConfigurationFile))
{
sw.Write("Hoge");
}
AppDomain.CurrentDomain.RunAtIsolatedDomain(info, () =>
{
var holiday = ConfigurationManager.GetProperty("Holiday", DayOfWeek.Sunday);
});
}

[Test]
public void GetPropertyTestError03HolidayPropertyNotFound()
{
var info = new AppDomainSetup();
info.ApplicationBase = AppDomain.CurrentDomain.SetupInformation.ApplicationBase;
info.ShadowCopyFiles = AppDomain.CurrentDomain.SetupInformation.ShadowCopyFiles;
info.ConfigurationFile = Path.GetTempFileName();
using (var sw = new StreamWriter(info.ConfigurationFile))
{
sw.Write(@"<?xml version=""1.0"" encoding=""utf-8"" ?>
<configuration>
<appSettings>
</appSettings>
<startup>
<supportedRuntime version=""v2.0.50727"" sku=""Client""/>
</startup>
</configuration>");
}
AppDomain.CurrentDomain.RunAtIsolatedDomain(info, () =>
{
var holiday = ConfigurationManager.GetProperty("Holiday", DayOfWeek.Sunday);
Assert.AreEqual(holiday, DayOfWeek.Sunday);
});
}

[Test]
public void GetPropertyTestError04HolidayPropertyIsEmpty()
{
var info = new AppDomainSetup();
info.ApplicationBase = AppDomain.CurrentDomain.SetupInformation.ApplicationBase;
info.ShadowCopyFiles = AppDomain.CurrentDomain.SetupInformation.ShadowCopyFiles;
info.ConfigurationFile = Path.GetTempFileName();
using (var sw = new StreamWriter(info.ConfigurationFile))
{
sw.Write(@"<?xml version=""1.0"" encoding=""utf-8"" ?>
<configuration>
<appSettings>
<add key=""Holiday"" value="""" />
</appSettings>
<startup>
<supportedRuntime version=""v2.0.50727"" sku=""Client""/>
</startup>
</configuration>");
}
AppDomain.CurrentDomain.RunAtIsolatedDomain(info, () =>
{
var holiday = ConfigurationManager.GetProperty("Holiday", DayOfWeek.Sunday);
Assert.AreEqual(holiday, DayOfWeek.Sunday);
});
}

[Test]
[ExpectedException(typeof(ArgumentException))]
public void GetPropertyTestError05HolidayPropertyIsInvalidIfIgnoredCase()
{
var info = new AppDomainSetup();
info.ApplicationBase = AppDomain.CurrentDomain.SetupInformation.ApplicationBase;
info.ShadowCopyFiles = AppDomain.CurrentDomain.SetupInformation.ShadowCopyFiles;
info.ConfigurationFile = Path.GetTempFileName();
using (var sw = new StreamWriter(info.ConfigurationFile))
{
sw.Write(@"<?xml version=""1.0"" encoding=""utf-8"" ?>
<configuration>
<appSettings>
<add key=""Holiday"" value=""aaaaaaaaaaaaa"" />
</appSettings>
<startup>
<supportedRuntime version=""v2.0.50727"" sku=""Client""/>
</startup>
</configuration>");
}
AppDomain.CurrentDomain.RunAtIsolatedDomain(info, () =>
{
var holiday = ConfigurationManager.GetProperty("Holiday", DayOfWeek.Sunday);
});
}

[Test]
[ExpectedException(typeof(ArgumentException))]
public void GetPropertyTestError06HolidayPropertyIsInvalidIfCaseSensitive()
{
var info = new AppDomainSetup();
info.ApplicationBase = AppDomain.CurrentDomain.SetupInformation.ApplicationBase;
info.ShadowCopyFiles = AppDomain.CurrentDomain.SetupInformation.ShadowCopyFiles;
info.ConfigurationFile = Path.GetTempFileName();
using (var sw = new StreamWriter(info.ConfigurationFile))
{
sw.Write(@"<?xml version=""1.0"" encoding=""utf-8"" ?>
<configuration>
<appSettings>
<add key=""Holiday"" value=""monday"" />
</appSettings>
<startup>
<supportedRuntime version=""v2.0.50727"" sku=""Client""/>
</startup>
</configuration>");
}
AppDomain.CurrentDomain.RunAtIsolatedDomain(info, () =>
{
var holiday = ConfigurationManager.GetProperty("Holiday", DayOfWeek.Sunday);
});
}
 
それ ほーら もうできた(ちゃちゃちゃ♪

AppDomainSetup クラスは、AppDomain を生成するためのいくつかの情報を指定するためのクラスです。
アプリケーションが格納されたディレクトリを表す ApplicationBase や、シャドウコピーの設定 ShadowCopyFiles は、現在の AppDomain のものをそのまま利用し(205・206 行目、221・222 行目、238・239 行目、263・264 行目、290・291 行目、316・317 行目)、アプリケーションの構成ファイルの読み込み先をテンポラリファイルに変更しています(207 行目、223 行目、240 行目、265 行目、292 行目、318 行目)。

異常系のパターンですが、
  1. アプリケーション構成ファイルが無い。
  2. アプリケーション構成ファイルのフォーマットが不正。
  3. Holiday キーが要素丸々消えている。
  4. Holiday キーの持つ値が空。
  5. Holiday キーの持つ値が異常(大文字・小文字区別しなくても判定できない)
  6. Holiday キーの持つ値が異常(大文字・小文字区別しなければ判定可能)
をやるようにしました。
今回の改修では、6. が正常系になるわけです。そのように書き換え(ExpectedException 属性を外しました)、テストが失敗することを確認します。

#line 146 "NTroll\Test.Urasandesu.NTroll.DomainFree\ConfigurationManagerTest.cs"
[Test]
public void GetPropertyTestError06HolidayPropertyIsInvalidIfCaseSensitive()
{
var info = new AppDomainSetup();
info.ApplicationBase = AppDomain.CurrentDomain.SetupInformation.ApplicationBase;
info.ShadowCopyFiles = AppDomain.CurrentDomain.SetupInformation.ShadowCopyFiles;
info.ConfigurationFile = Path.GetTempFileName();
using (var sw = new StreamWriter(info.ConfigurationFile))
{
sw.Write(@"<?xml version=""1.0"" encoding=""utf-8"" ?>
<configuration>
<appSettings>
<add key=""Holiday"" value=""monday"" />
</appSettings>
<startup>
<supportedRuntime version=""v2.0.50727"" sku=""Client""/>
</startup>
</configuration>");
}
AppDomain.CurrentDomain.RunAtIsolatedDomain(info, () =>
{
var holiday = ConfigurationManager.GetProperty("Holiday", DayOfWeek.Sunday);
});
}
 
そうしたら、実際のプロダクトコードを修正し、テストが通ることを確認しましょう。

#line 109 "NTroll\Urasandesu.NTroll.DomainFree\ConfigurationManager.cs"
class GetPropertyImplForDayOfWeek : GetPropertyImpl<DayOfWeek>
{
const string DayOfWeekName = "DayOfWeek";
static readonly string DayOfWeekRegexPattern =
string.Format(@"^\s*(?<{0}>" +
@"(Sunday)|" +
@"(Monday)|" +
@"(Tuesday)|" +
@"(Wednesday)|" +
@"(Thursday)|" +
@"(Friday)|" +
@"(Saturday)" +
@")\s*$", DayOfWeekName);
static readonly Regex DayOfWeekRegex = new Regex(DayOfWeekRegexPattern, RegexOptions.Compiled | RegexOptions.IgnoreCase);
protected override DayOfWeek ToPropertyCore(string key, string value, DayOfWeek defaultValue)
{
var m = default(Match);
if (string.IsNullOrEmpty(value))
{
return defaultValue;
}
else if ((m = DayOfWeekRegex.Match(value)) != null && m.Success)
{
return (DayOfWeek)Enum.Parse(typeof(DayOfWeek), m.Groups[DayOfWeekName].Value, true);
}
else
{
var fmt = "The value \"{0}\" linked by key \"{1}\" is invalid. " +
"It must take one of the following values: \r\n{2}.";
var msg = string.Format(fmt, value, key, string.Join(", ", Enum.GetNames(typeof(DayOfWeek))));
throw new ArgumentException(msg, "value");
}
}
}
 
完全な単体テストを目指すのであれば、さらに System.Configuration.ConfigurationManager を入れ替え可能にするなどのリファクタリングも行えば良いと思います。テストがあるのですから、もう躊躇する必要はないですね!( ^ー゚)b




今回はここまで
今回は AppDomain という、.NET の根幹にあたる機能を取り上げてみました。その目的である「アプリケーションの分離」というものに、興味を持っていただけた方が少しでもいらっしゃれば幸いに思います。
そうそう、私自身、.NET と関わり始めて 7、8 年というところですが、アンマネージ API に足を突っ込んでからのこの半年弱は、それまでできなかった色々な発見ができるようになりました。マネージ API で理解なかった挙動が理解できるようになったり、これまでダークサイドに堕ちてしまっていた問題も解決できるようになったり。なんでもっと早く足突っ込まなかったしと反省しきりです (>_<)
もっとこういう「境界」に当たる部分を議論できるお仲間が増えれば、色々良いことがあるのではないかと儚い夢を見ております。

さて、私事で申し訳ないですが、ついに先日 12/23 で三十路を迎えてしまいました ...( = =)
ショックですが、オヤジギャクが憚られず言える歳になったと、今年も前向きにがんばって行きたい所存です。

境界を越えるついでに三十路を越えました・・・なんちゃって ///


・・・失礼いたしました (^_^;)
最後に 1 つ。実は AppDomain 周りについては、もう 1 つ報告すべきことがあります。
Prigに必要だった、パズルの最後の 1 ピース。近いうちに報告させていただければと思いますです <(_ _)>

AppDomain、その幻想をぶち殺す - How to use unmanaged code as Illusion Killer against CLR -

$
0
0
前回の積み残しです。

AppDomain は便利な仕組みなんですが、なにぶんなんでもかんでも分離してしまうため、ちょっと融通が利かないところがあったりします。
前回の記事の終わりにも「パズルの最後の 1 ピース」みたいな書き方をしましたが、アンマネージコードの力を借りればなんとかなるだろうということを感じてはいつつ、それを具体的に実現する方法を公表されている方は、今現在もいらっしゃらないようでした。

私が探せていないだけだろうとは思うのですが・・・。Google 先生、私にはいつも厳しいんですよね (>_<)
仕方がないですので、これまで通り、つっこみどころ満載な自己流解決法で行きましょう!

というわけで、ゆるめの AppDomain 攻略講座、はっじまっるよー。

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


こちらのページ/ソフトウェアを参考にさせていただきました。いつもお世話になり、頭が下がるばかりです<(_ _)>
Download Details - Microsoft Download Center - Shared Source Common Language Infrastructure 2.0 Release
Cecil - Mono
ECMA C# and Common Language Infrastructure Standards
Microsoft.ExtendedReflection
NUnit - Home
技術/Windows/メモリダンプ取得方法メモ - Glamenv-Septzen.net
How to use SafeHandle in a Resilient Library - NyaRuRuの日記
PEフォーマットを解釈せよ! - @IT
Detours - Microsoft Research
Debugging .Net framework source code within Windbg ≪ Naveen's Blog
SOSEX - A New Debugging Extension for Managed Code - Steve's Techspot
Need way to invoke full manual JIT on assembly | Microsoft Connect
C# によるプログラミング入門
ReSharper:: The Most Intelligent Extension for Visual Studio





目次

融通が利かないところ
前回の例にも挙げた通り、.NET 開発者でも、特にレガシーコードと戦う方々には、AppDomain の仕組みや目的を知っていただき、活用できるようになることは、私は非常に意義があることだと考えています。ただ、使い始めてすぐに気付く不便な部分があることも事実です。いくつか例を挙げてみましょう。


例 1: 必要な情報がすでに別の情報と紐づけられている
なんらかの自動化された Unit Test を書かれている方にはおなじみと思われる NUnit。GUI ツールがついててわかりやすいですよね。
コマンドラインで動きさせすれば、CI 環境には事足りるとは言え、やっぱり使い方が直観的にわかる GUI ツールがあるということは、導入には欠かせません。自分専用にカスタマイズできない職場の PC じゃ・・・ということで、マウス一筋派/どちらかというとマウス派の方も少なくないかと思いますし。

さてさて。Assert で出力値をチェックできているとは言え、実際に出力値を標準出力に出して見てみたいという場面もあるでしょう。
コマンドラインで動かす場合はともかく、NUnit の GUI ツールで確認する場合はどうしてるの?というわけですが、[Text Output] というタブがあり、ちゃんと標準出力がリダイレクトされて出てきます。

#line 83 "CppTroll\ProfilingApiSample04FrameworkTest\ConsoleTest.cs"
using System;
using NUnit.Framework;

namespace ProfilingApiSample04FrameworkTest
{
[TestFixture]
public class ConsoleTest
{
[Test]
public void Test()
{
Console.WriteLine("AppDomain: {0}", AppDomain.CurrentDomain.FriendlyName);
}
}
}
 


ただ、前回解説した AppDomainMixin.RunAtIsolatedDomain を使うと・・・。

#line 59 "CppTroll\ProfilingApiSample04FrameworkTest\ConsoleTest.cs"
using System;
using NUnit.Framework;
using ProfilingApiSample04Framework.Mixin.System;

namespace ProfilingApiSample04FrameworkTest
{
[TestFixture]
public class ConsoleTest
{
[Test]
public void Test()
{
Console.WriteLine("AppDomain: {0}", AppDomain.CurrentDomain.FriendlyName);

AppDomain.CurrentDomain.RunAtIsolatedDomain(() =>
{
Console.WriteLine("AppDomain: {0}", AppDomain.CurrentDomain.FriendlyName);
});

Console.WriteLine("AppDomain: {0}", AppDomain.CurrentDomain.FriendlyName);
}
}
}
 


死ーん・・・
・・・2 つ目の WriteLine はどこに行った?(゚Д゚;≡;゚д゚)

ソースコードを読むとわかるのですが、NUnit の GUI ツールでは、テスト実行前に Console.SetOut を呼び出し、[Text Output] タブに出力するためのオリジナル TextWriter、EventListenerTextWriter に差し替え、標準出力をフックするようにしています。
で、Console.SetOut は何をやっているかというと、中で持ってる static メンバを、渡された TextWriter に入れ替えるんですね。
環境を分離するのが AppDomain の目的、というわけで、この依存もきれいさっぱり切り離してくれたわけでした。

たぶん、この例に限らず、ある一部の依存関係だけは持ち込みたいっていう場合は往々にしてあるかと思います。Console.Out の型である TextWriter などは、幸い MarshalByRefObject を継承していますので、引数から引き回せば良いのですが・・・。


例 2: 既存のライブラリ、標準のクラスが AppDomain 越えに対応していない
前述の TextWriter や、自分たちで手をいれているものでしたら、MarshalByRefObject や SerializableAttribute で修飾すれば良いのですが、困るのが既存のライブラリや標準の型。

ところで、AppDomain の作成というのは、いくら Process の作成より軽いといっても、Assembly の再ロードが発生するわけですから、それなりにコストがかかります。ここで、どのぐらいかかるのか測ってみましょう。あ・・・AppDomain 別にすると出力出なくなっちゃいましたっけ。とりあえず引数で引き回すことにしましょうか (^_^;)

#line 57 "CppTroll\ProfilingApiSample04FrameworkTest\StopwatchTest.cs"
using System;
using System.Diagnostics;
using System.IO;
using NUnit.Framework;
using ProfilingApiSample04Framework.Mixin.System;

namespace ProfilingApiSample04FrameworkTest
{
[TestFixture]
public class StopwatchTest
{
[Test]
public void Test()
{
using (var sw = new StringWriter())
{
var stopwatch = new Stopwatch();
stopwatch.Restart();

sw.WriteLine("Elapsed: {0} ms", stopwatch.ElapsedMilliseconds);

AppDomain.CurrentDomain.RunAtIsolatedDomain<StringWriter, Stopwatch>((sw_, stopwatch_) =>
{
sw_.WriteLine("Elapsed: {0} ms", stopwatch_.ElapsedMilliseconds);
}, sw, stopwatch);

sw.WriteLine("Elapsed: {0} ms", stopwatch.ElapsedMilliseconds);

Console.WriteLine(sw.ToString());
}
}
}
}
 
時間を計ると言えば Stopwatch です。AppDomainMixin.RunAtIsolatedDomain は引数を受け取れるよう拡張しました。さて、これを実行すると・・・。


ああああああ (´Д`;)

Stopwatch の定義は・・・と見ると、見事になにも修飾されていないですね (T_T)
こういう場合、正攻法で行くなら、利用したいクラスの I/F をそっくりラップした MarshalByRefObject を作成することになるかと思いますが・・・いやはや、めんどうです。Code DOMや、T4などを利用し、自動生成をする仕組みを作ってもよいのですが、こういうクラスが見つかるたびにラッパを作る必要がある、というのではなかなか気軽にはできません。ぐぬぬ・・・。


例 3: コンパイラが対象のクラスを自動生成する
もっともわかりにくいのがこのパターンです。

前回紹介し、今回も登場している AppDomainMixin.RunAtIsolatedDomain ですが、引数に引き渡される action について、static なメソッドのみを許していたのを、お気づきになられた方もいらっしゃったかもしれません。
今回のサンプルでは AppDomain を越えられる型であれば引数に渡せるようにしたりして、若干拡張していますがチェックは同様に行っています。再掲しましょう。

#line 107 "CppTroll\ProfilingApiSample04Framework\Mixin\System\AppDomainMixin.cs"
・・・
static void RunAtIsolatedDomain(Evidence securityInfo,
AppDomainSetup info, Delegate action, params object[] args)
{
if (action == null)
throw new ArgumentNullException("action");

if (!action.Method.IsStatic)
throw new ArgumentException(
"The parameter must be designated a static method.", "action");


var domain = default(AppDomain);
try
{
domain = AppDomain.CreateDomain("Domain " + action.Method.ToString(),
securityInfo, info);
var type = typeof(MarshalByRefRunner);
var runner = (MarshalByRefRunner)domain.CreateInstanceAndUnwrap(
type.Assembly.FullName, type.FullName);
runner.Action = action;
runner.Run(args);
}
catch (SerializationException e)
{
throw new ArgumentException("The parameter must be domain crossable. " +
"Please confirm that the type inherits MarshalByRefObject, " +
"or it is applied SerializableAttribute.", e);
}
finally
{
try
{
if (domain != null)
AppDomain.Unload(domain);
}
catch { }
}
}
・・・
 
114 行目にある if 文で、メソッドが static でなければ ArgumentException をスローするようにしていますね。

ここで、デリゲートに関する薀蓄を一つ。
デリゲートは、作ると自動的に Delegate クラスを継承したクラスが生成されるのですが、Delegate クラス自体は MarshalByRefObject ではなく、SerializableAttribute が適用されており、同時に ISerializable を実装していることをご存じでしたでしょうか?
つまり、デリゲートは AppDomain を越えるときにコピーされます。従って、そのメンバとして保持される Target、Method も AppDomain を越えられる型でなくてはなりません。
Method は MethodInfo(RuntimeMethodInfo)ですので特に問題はないですが、Target となる型は要注意です。通常は、デリゲートに渡しているものが、参照透過なラムダ式か、外部の環境を取り込んだクロージャなのか、なんて気にすることはないと思いますが・・・。
args で渡される引数を全て調べきるのは効率が悪いため、SerializationException を一括して処理し、引数に原因があることにしていますが(130 行目)、ここにデリゲートを指定して試してみましょう。
こんなソースコードを・・・

#line 83 "CppTroll\ProfilingApiSample04FrameworkTest\DelegateTest.cs"
using System;
using NUnit.Framework;
using ProfilingApiSample04Framework.Mixin.System;

namespace ProfilingApiSample04FrameworkTest
{
[TestFixture]
public class DelegateTest
{
[Test]
public void Test()
{
var adder = default(Func<int, int, int>);
adder = (x, y) => x + y; // Referencial transparent lambda
AppDomain.CurrentDomain.RunAtIsolatedDomain<Func<int, int, int>>(adder_ =>
{
Assert.AreEqual(2, adder_(1, 1));
}, adder);
}
}
}
 


こんな風に書き換えれば・・・

#line 83 "CppTroll\ProfilingApiSample04FrameworkTest\DelegateTest.cs"
using System;
using NUnit.Framework;
using ProfilingApiSample04Framework.Mixin.System;

namespace ProfilingApiSample04FrameworkTest
{
[TestFixture]
public class DelegateTest
{
[Test]
public void Test()
{
var adder = default(Func<int, int, int>);
var z = 1;
adder = (x, y) => x + y + z; // To closure(capture local variable z)
AppDomain.CurrentDomain.RunAtIsolatedDomain<Func<int, int, int>>(adder_ =>
{
Assert.AreEqual(3, adder_(1, 1));
}, adder);
}
}
}
 


オワタ\(^o^)/

Resharperのように、このような気付きにくい動作に対して警告してくれるアドオンもあるようですが、未導入の現場では、標準の ildasm等、外部の逆アセンブラで確認するしかありません。
AppDomainMixin.RunAtIsolatedDomain では、このような動きを気に病む必要がないように、基本的に action に指定するデリゲートについては、static なメソッドしか許さないようにしたのでした(static なメソッドの場合、Target は必ず null になるため)。
もちろん、こちらもラッパを作れば動作の解決にはなるのですが、元々処理の流れ的にその場所にしか使われないし、使われたくもないがためにラムダ式を使っているのに、そんな解決策をとってしまっては本末転倒です。

そうそう、よく C# と比較に挙がる Java や C++ には、こういう「局所的だが状態を持つカプセル化された処理」を作る機能として局所クラスっていうのがあるんですよね。なんでそこパkr・・・インスパイアしなかったし (^_^;)




攻略準備
最後の例などは C# 初学者には意味が分からない恐怖の対象でしょう。LINQ 怖い。AppDomain 怖い。
AppDomain みたいなよくわからないものを使わない、のももちろんありです。要は初めからこんな工夫が必要ないように、保守しやすい/拡張しやすい実装を行えば良いだけですから。難しいようであれば、その旨ちゃんと上長やお客さんに伝えて、交渉してみるのも手かと思います。

まあ今回は、アンマネージの力をちょっとだけ借りて、境界チェックがゆるめな AppDomain 越えアクセッサを作ることで、前述の問題を解決してみます。毎度のことながら話の都合上ですが、そこは発信者特権ということで (^_^;)

さて、本題に入る前にマネージコードとアンマネージコードが連携するための準備をしましょう。


関数ポインタ
唐突ですが、関数ポインタの話です。
CLI では、全てのプログラミング言語は、まずメタデータ + IL という中間形式に変換され、それから実行時コンパイルされネイティブコードに変換されることになっています。MS の実装でそれを担うのが CLR の JIT 処理になるのですが、皆さんは CLR が何を使ってマネージコードからネイティブコードへの呼び出しを紐付けているのかご存じでしょうか。
私もつい最近まで知らなかったのですが、以下の書籍に詳細な流れが書き込まれていることを Twitter で知り、即注文しました。
Amazon.co.jp: Essential .NET ― 共通言語ランタイムの本質: ドン・ボックス, クリス・セルズ, Don Box, Chris Sells, 吉松 史彰: 本

英語版だったら、Google Book から冒頭部分を立ち読みできますね。
Essential.NET: The common language runtime - Don Box, Chris Sells - Google ブックス

結論から言うと、それにはネイティブコードへの関数ポインタが使われます。
同一プロセス上であれば、あるネイティブコードへの関数ポインタは基本変わることは無いですから、これを直接参照すれば AppDomain のような疑似的な境界は関係なくなるわけですね。

それでは、ここで、JIT の流れを確認しておきましょう。こんなプログラムがあったとすると、

class Program
{
static void Main(string[] args)
{
A.DoSomething();
A.DoSomething();
B.DoSomething();
}
}

class A
{
public static void DoSomething()
{
}
}

class B
{
public static void DoSomething()
{
A.DoSomething();
}
}
 
JIT は AppDomain 毎に以下の順序で行われます。
  1. Main(エントリポイントとして呼ばれた時)
  2. A.DoSomething(5 行目に呼ばれた時)
  3. B.DoSomething(7 行目に呼ばれた時)

最初に実行されるまで、JIT は行われないというのがミソです。名前の通りと言えば名前の通りなのですが、実装は素直にはできません。
なぜなら、あるメソッドを JIT する際、そこから呼び出されているメソッドの呼び先を確定させなければ、ネイティブコードが作成できないからです。呼び先のネイティブコードはまだできあがっていないのに!
上記の例で言うと、Main から呼ばれている A.DoSomething は、Main の JIT が行われる時にはまだネイティブコードができていないため、call 命令(ネイティブ)のオペランドに指定する関数ポインタが決まりません。

CLR でこれをどのように解決しているかというと、スタブを介してネイティブコードへの関数ポインタにアクセスする形を取るようにしています。
call 命令(ネイティブ)に渡すアドレスに、一先ずスタブの関数ポインタを指定するのです。スタブは、そのメソッドのネイティブコードができているか確認し、できていなければ JIT します。そして、JIT したネイティブコードへの関数ポインタを呼び出します。
疑似コードで書くとこんな感じでしょうか。

1. の JIT 後

class Program
{
static void (*MainNativePtr)(string[] args) = NULL;

static void MainStub(string[] args)
{
if (MainNativePtr == NULL)
MainNativePtr = Runtime.CreateNativeCode();
MainNativePtr();
}

static void MainNative(string[] args)
{
A.DoSomethingStub();
A.DoSomethingStub();
B.DoSomethingStub();
}

static void Main(string[] args)
{
A.DoSomething();
A.DoSomething();
B.DoSomething();
}
}

class A
{
public static void (*DoSomethingNativePtr)() = NULL;

public static void DoSomethingStub()
{
if (DoSomethingNativePtr == NULL)
DoSomethingNativePtr = Runtime.CreateNativeCode();
DoSomethingNativePtr();
}

public static void DoSomething()
{
}
}

class B
{
public static void (*DoSomethingNativePtr)() = NULL;

public static void DoSomethingStub()
{
if (DoSomethingNativePtr == NULL)
DoSomethingNativePtr = Runtime.CreateNativeCode();
DoSomethingNativePtr();
}

public static void DoSomething()
{
A.DoSomething();
}
}
 
いきなりダイナミックに変わりましたが、まずは Main メソッドのネイティブコード(MainNative: 12 行目)から呼び出すスタブが一気に作成されるということが伝わればと思います。まだ、A.DoSomething や B.DoSomething に対応するネイティブコードは作成されていないことが確認できます。
そして、2. の JIT 後です。

class Program
{
static void (*MainNativePtr)(string[] args) = NULL;

static void MainStub(string[] args)
{
if (MainNativePtr == NULL)
MainNativePtr = Runtime.CreateNativeCode();
MainNativePtr();
}

static void MainNative(string[] args)
{
A.DoSomethingStub();
A.DoSomethingStub();
B.DoSomethingStub();
}

static void Main(string[] args)
{
A.DoSomething();
A.DoSomething();
B.DoSomething();
}
}

class A
{
public static void (*DoSomethingNativePtr)() = NULL;

public static void DoSomethingStub()
{
if (DoSomethingNativePtr == NULL)
DoSomethingNativePtr = Runtime.CreateNativeCode();
DoSomethingNativePtr();
}

public static void DoSomethingNative()
{
}

public static void DoSomething()
{
}
}

class B
{
public static void (*DoSomethingNativePtr)() = NULL;

public static void DoSomethingStub()
{
if (DoSomethingNativePtr == NULL)
DoSomethingNativePtr = Runtime.CreateNativeCode();
DoSomethingNativePtr();
}

public static void DoSomething()
{
A.DoSomething();
}
}
 
Main メソッドのネイティブコードから、A.DoSomethingStub が呼ばれることで(14 行目)、A.DoSomething メソッドのネイティブコードが作成されます(A.DoSomethingNative: 38 行目)。
さらに処理が進み、3. の JIT が行われると、

class Program
{
static void (*MainNativePtr)(string[] args) = NULL;

static void MainStub(string[] args)
{
if (MainNativePtr == NULL)
MainNativePtr = Runtime.CreateNativeCode();
MainNativePtr();
}

static void MainNative(string[] args)
{
A.DoSomethingStub();
A.DoSomethingStub();
B.DoSomethingStub();
}

static void Main(string[] args)
{
A.DoSomething();
A.DoSomething();
B.DoSomething();
}
}

class A
{
public static void (*DoSomethingNativePtr)() = NULL;

public static void DoSomethingStub()
{
if (DoSomethingNativePtr == NULL)
DoSomethingNativePtr = Runtime.CreateNativeCode();
DoSomethingNativePtr();
}

public static void DoSomethingNative()
{
}

public static void DoSomething()
{
}
}

class B
{
public static void (*DoSomethingNativePtr)() = NULL;

public static void DoSomethingStub()
{
if (DoSomethingNativePtr == NULL)
DoSomethingNativePtr = Runtime.CreateNativeCode();
DoSomethingNativePtr();
}

public static void DoSomethingNative()
{
A.DoSomethingNative();
}

public static void DoSomething()
{
A.DoSomething();
}
}
 
こんな感じになります。B.DoSomething のネイティブコード(B.DoSomethingNative: 58 行目)の作成時には、もう A.DoSomething のネイティブコードができているのがわかりますので、そのままネイティブコードへの関数ポインタを使うことができます。

「関数ポインタを直接参照すれば~」ということでしたが、これは、RuntimeMethodHandle.GetFunctionPointer で取得できます。
ただし、上記のようなことから、JIT 前と JIT 後で値が変わります。JIT 前はスタブへの参照、JIT 後はネイティブコードへの参照ということですね。これを知らないと、Domain 越えアクセッサを設計する際、「あるマネージメソッドを表す関数ポインタは Process で一つ」と勘違いして扱って嵌ります・・・はい。嵌った人間がこちらになります (ToT)
キーには Process で一意になるもの、例えば実行前のメタデータが持つような情報を扱うのが良いでしょう。


calli 命令(IL)
関数ポインタを直接呼び出すにはこの命令を使います。C++/CLI でネイティブな関数ポインタを呼び出すコードを記述すると、コンパイルされた IL に現れますね。
"i"は Indirect method call の "i"らしいです・・・ldftn 命令(IL)もそうですが、どうも略し方がよくわかりません (^_^;)
なにはともあれ、これは DynamicMethod 経由で利用します。気を付けることがあるとすれば、ILGenerator.Emit ではなく、ILGenerator.EmitCalli を使って打ち込む必要があることぐらいです。


CLR の型チェックタイミング
上述の書籍にもありますが、CLR があるオブジェクトについて、ある AppDomain に属しているかどうかをチェックするのは、マネージコードで AppDomain を越えようとした時だけです。
あとはこれに加え、型は AppDomain 毎に一意になりますので、AppDomain A で使っていたオブジェクトを AppDomain B で同じ型にキャストすることはできないことに注意する必要があります。もしこれを行おうとすれば、「ハンドルされていない例外: System.InvalidCastException: 型 'MyClass'のオブジェクトを型 'MyClass'にキャストできません。」のようなエラーになってしまいます。わかりにくいですね (-_-;) エラーメッセージに型が属す AppDomain の Friendly Name か何かを付加してくれればありがたかったのですが・・・。
まあ、上述の問題を解決するようなアクセッサとして利用するだけでしたらほとんど障害にはなりませんので、もし何かに応用される際はちょっと気に留めておいていただければと思います。




問・題・解・決!
準備が整いましたのでさくっと実装しましょう!
まずは、関数ポインタを保存しておくリポジトリから。

#line 1 "CppTroll\ProfilingApiSample04\InstanceGetters.h"
#pragma once

#ifndef INDIRETIONINTERFACES_H
#define INDIRETIONINTERFACES_H

#ifdef URASANDESU_PRIG_EXPORTS
#define URASANDESU_PRIG_API __declspec(dllexport)
#else
#define URASANDESU_PRIG_API __declspec(dllimport)
#endif

EXTERN_C URASANDESU_PRIG_API STDMETHODIMP_(BOOL) InstanceGettersTryAdd(LPCWSTR key, void const *pFuncPtr);
EXTERN_C URASANDESU_PRIG_API STDMETHODIMP_(BOOL) InstanceGettersTryGet(LPCWSTR key, void const **ppFuncPtr);
EXTERN_C URASANDESU_PRIG_API STDMETHODIMP_(BOOL) InstanceGettersTryRemove(LPCWSTR key, void const **ppFuncPtr);
EXTERN_C URASANDESU_PRIG_API STDMETHODIMP_(VOID) InstanceGettersClear();

#endif // #ifndef INDIRETIONINTERFACES_H
 

#line 1 "CppTroll\ProfilingApiSample04\InstanceGetters.cpp"
#include "StdAfx.h"
#include "InstanceGetters.h"
#include "GlobalSafeDictionary.h"

typedef GlobalSafeDictionary<std::wstring, void const *> InstanceGetters;

EXTERN_C URASANDESU_PRIG_API STDMETHODIMP_(BOOL) InstanceGettersTryAdd(LPCWSTR key, void const *pFuncPtr)
{
InstanceGetters &ing = InstanceGetters::GetInstance();
return ing.TryAdd(std::wstring(key), pFuncPtr);
}

EXTERN_C URASANDESU_PRIG_API STDMETHODIMP_(BOOL) InstanceGettersTryGet(LPCWSTR key, void const **ppFuncPtr)
{
_ASSERTE(ppFuncPtr != NULL);
InstanceGetters &ing = InstanceGetters::GetInstance();
return ing.TryGet(std::wstring(key), *ppFuncPtr);
}

EXTERN_C URASANDESU_PRIG_API STDMETHODIMP_(BOOL) InstanceGettersTryRemove(LPCWSTR key, void const **ppFuncPtr)
{
_ASSERTE(ppFuncPtr != NULL);
InstanceGetters &ing = InstanceGetters::GetInstance();
return ing.TryRemove(std::wstring(key), *ppFuncPtr);
}

EXTERN_C URASANDESU_PRIG_API STDMETHODIMP_(VOID) InstanceGettersClear()
{
InstanceGetters &ing = InstanceGetters::GetInstance();
ing.Clear();
}
 
マネージ側との I/F になる InstanceGetters** らです。解説するまでもなく、ただのガワですね・・・(^_^;)
本処理は GlobalSafeDictionary に任せます。

#line 1 "CppTroll\ProfilingApiSample04\GlobalSafeDictionary.h"
#pragma once
#ifndef GLOBAL_SAFE_DICTIONARY_H
#define GLOBAL_SAFE_DICTIONARY_H

template<
typename Key,
typename Value,
typename Hash = boost::hash<Key>,
typename Pred = std::equal_to<Key>,
typename Alloc = std::allocator<std::pair<Key const, Value>>
>
class GlobalSafeDictionary : boost::noncopyable
{
public:
typedef typename boost::call_traits<Key>::param_type in_key_type;
typedef typename boost::call_traits<Value>::param_type in_value_type;
typedef typename boost::call_traits<Value>::reference out_value_type;

static GlobalSafeDictionary &GetInstance()
{
static GlobalSafeDictionary im;
return im;
}

BOOL TryAdd(in_key_type key, in_value_type value)
{
m_lock.Lock();
BOOST_SCOPE_EXIT((&m_lock))
{
m_lock.Unlock();
}
BOOST_SCOPE_EXIT_END


if (m_map.find(key) == m_map.end())
{
m_map[key] = value;
return TRUE;
}
else
{
return FALSE;
}
}

BOOL TryGet(in_key_type key, out_value_type rValue)
{
m_lock.Lock();
BOOST_SCOPE_EXIT((&m_lock))
{
m_lock.Unlock();
}
BOOST_SCOPE_EXIT_END


if (m_map.find(key) == m_map.end())
{
return FALSE;
}
else
{
rValue = m_map[key];
return TRUE;
}
}

BOOL TryRemove(in_key_type key, out_value_type rValue)
{
m_lock.Lock();
BOOST_SCOPE_EXIT((&m_lock))
{
m_lock.Unlock();
}
BOOST_SCOPE_EXIT_END


if (m_map.find(key) == m_map.end())
{
return FALSE;
}
else
{
rValue = m_map[key];
m_map.erase(key);
return TRUE;
}
}

void Clear()
{
m_lock.Lock();
BOOST_SCOPE_EXIT((&m_lock))
{
m_lock.Unlock();
}
BOOST_SCOPE_EXIT_END


m_map.clear();
}

private:
GlobalSafeDictionary() { }
ATL::CComAutoCriticalSection m_lock;
boost::unordered_map<Key, Value, Hash, Pred, Alloc> m_map;
};

#endif // #ifndef GLOBAL_SAFE_DICTIONARY_H
 
GlobalSafeDictionary も大したことはしていなくて、boost::unordered_map をシングルトンかつスレッドセーフにラップしただけのものです。Boost.ScopeExit は Critical Section を Lock/Unlock するにも便利に使えますね。アンマネージコードはこれだけです。続いてマネージ側へ。

#line 1 "CppTroll\ProfilingApiSample04Framework\InstanceGetters.cs"
using System;
using System.Runtime.InteropServices;

namespace ProfilingApiSample04Framework
{
public static class InstanceGetters
{
[DllImport("ProfilingApiSample04.dll", EntryPoint = "InstanceGettersTryAdd")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool TryAdd([MarshalAs(UnmanagedType.LPWStr)] string key, IntPtr pFuncPtr);

[DllImport("ProfilingApiSample04.dll", EntryPoint = "InstanceGettersTryGet")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool TryGet([MarshalAs(UnmanagedType.LPWStr)] string key, out IntPtr ppFuncPtr);

[DllImport("ProfilingApiSample04.dll", EntryPoint = "InstanceGettersTryRemove")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool TryRemove([MarshalAs(UnmanagedType.LPWStr)] string key, out IntPtr ppFuncPtr);

[DllImport("ProfilingApiSample04.dll", EntryPoint = "InstanceGettersClear")]
public static extern void Clear();
}
}
 
P/Invoke で InstanceGetters** らを呼び出します。はい、それだけです (´・ω・`)

#line 1 "CppTroll\ProfilingApiSample04Framework\InstanceHolder.cs"
using ProfilingApiSample04Framework.Mixin.System;

namespace ProfilingApiSample04Framework
{
public abstract class InstanceHolder<T> where T : InstanceHolder<T>
{
protected InstanceHolder() { }
static T ms_instance = TypeMixin.ForciblyNew<T>();
public static T Instance { get { return ms_instance; } }
}
}
 
インスタンスを保持するだけのシンプルなクラスです。TypeMixin.ForciblyNew<T> (8 行目)は、単に非公開コンストラクタを強制的に呼び出すだけのメソッドですので解説はしません。だんだん「こんなので行けるのか?」と不安になってこられているかもしれませんが・・・なんと次で終わりです!(ぇー

#line 1 "CppTroll\ProfilingApiSample04Framework\LooseCrossDomainAccessor.cs"
using System;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Threading;

namespace ProfilingApiSample04Framework
{
public class LooseCrossDomainAccessor
{
protected LooseCrossDomainAccessor() { }

public static void Register<T>() where T : InstanceHolder<T>
{
LooseCrossDomainAccessor<T>.Register();
}

public static void Unload<T>() where T : InstanceHolder<T>
{
LooseCrossDomainAccessor<T>.Unload();
}

public static T Get<T>() where T : InstanceHolder<T>
{
return LooseCrossDomainAccessor<T>.Holder;
}

public static T GetOrRegister<T>() where T : InstanceHolder<T>
{
var holder = default(T);
if ((holder = LooseCrossDomainAccessor<T>.HolderOrDefault) == null)
{
LooseCrossDomainAccessor<T>.Register();
holder = LooseCrossDomainAccessor<T>.Holder;
}
return holder;
}

public static bool TryGet<T>(out T holder) where T : InstanceHolder<T>
{
holder = LooseCrossDomainAccessor<T>.HolderOrDefault;
return holder != null;
}
}

public class LooseCrossDomainAccessor<T> where T : InstanceHolder<T>
{
static readonly object ms_lockObj = new object();
static T ms_holder = null;
static bool ms_ready = false;
static readonly Type ms_t = typeof(T);
static readonly string ms_key = ms_t.AssemblyQualifiedName;

protected LooseCrossDomainAccessor() { }

public static void Register()
{
var instance = ms_t.GetProperty("Instance", BindingFlags.Public |
BindingFlags.Static |
BindingFlags.FlattenHierarchy);
var instanceGetter = instance.GetGetMethod();
RuntimeHelpers.PrepareMethod(instanceGetter.MethodHandle);
var funcPtr = instanceGetter.MethodHandle.GetFunctionPointer();
InstanceGetters.TryAdd(ms_key, funcPtr);
}

static T GetHolder()
{
var funcPtr = default(IntPtr);
if (!InstanceGetters.TryGet(ms_key, out funcPtr))
throw new InvalidOperationException("T has not been registered yet. " +
"Please call Register method.");

return GetHolderCore(funcPtr);
}

static bool TryGetHolder(out T holder)
{
var funcPtr = default(IntPtr);
if (!InstanceGetters.TryGet(ms_key, out funcPtr))
{
holder = null;
return false;
}
else
{
holder = GetHolderCore(funcPtr);
return true;
}
}

static T GetHolderCore(IntPtr funcPtr)
{
var extractor = new DynamicMethod("Extractor", ms_t, null, ms_t.Module);
var gen = extractor.GetILGenerator();
if (IntPtr.Size == 4)
{
gen.Emit(OpCodes.Ldc_I4, funcPtr.ToInt32());
}
else if (IntPtr.Size == 8)
{
gen.Emit(OpCodes.Ldc_I8, funcPtr.ToInt64());
}
else
{
throw new NotSupportedException();
}
gen.EmitCalli(OpCodes.Calli, CallingConventions.Standard, ms_t, null, null);
gen.Emit(OpCodes.Ret);
return ((Func<T>)extractor.CreateDelegate(typeof(Func<T>)))();
}

public static T Holder
{
get
{
if (!ms_ready)
{
lock (ms_lockObj)
{
if (!ms_ready)
{
ms_holder = GetHolder();
Thread.MemoryBarrier();
ms_ready = true;
}
}
}
return ms_holder;
}
}

public static T HolderOrDefault
{
get
{
if (!ms_ready)
{
lock (ms_lockObj)
{
if (!ms_ready)
{
var holder = default(T);
if (TryGetHolder(out holder))
{
ms_holder = holder;
Thread.MemoryBarrier();
ms_ready = true;
}
}
}
}
return ms_holder;
}
}

public static void Unload()
{
if (ms_ready)
{
lock (ms_lockObj)
{
if (ms_ready)
{
var funcPtr = default(IntPtr);
InstanceGetters.TryRemove(ms_key, out funcPtr);
ms_holder = null;
Thread.MemoryBarrier();
ms_ready = false;
}
}
}
}
}
}
 
今までで最長ですが、呼びやすいように I/F 追加したり、わかりやすいように例外投げたりしているだけで、コア部分は GetHolderCore でやっていることが全てです(92 行目~111 行目)。x86/x64 両対応のために、IntPtr.Size で処理を分けるという小細工をしていますが、基本「関数ポインタを calli 命令(IL)を使って直接呼び出す」という、準備でお話した方針そのままですね。

全部合わせても 400 ステップぐらいです。これで本当に AppDomain の呪縛から逃れられるの?と思われるかもしれませんが、論より Run です!それぞれの問題の解決して行きましょう!


例 1: 必要な情報がすでに別の情報と紐づけられている
そうそう、もはや MarshalByRefObject や SerializableAttribute とか関係ないですので、Generic なインスタンス持ち運び用のクラスを InstanceHolder から継承して作っておきましょう。

#line 1 "CppTroll\ProfilingApiSample04Framework\GenericHolder.cs"
namespace ProfilingApiSample04Framework
{
public class GenericHolder<T> : InstanceHolder<GenericHolder<T>>
{
GenericHolder() { }
public T Source { get; set; }
}
}
 
で、こう書き換えます。

#line 7 "CppTroll\ProfilingApiSample04FrameworkTest\ConsoleTest.cs"
using System;
using NUnit.Framework;
using ProfilingApiSample04Framework.Mixin.System;
using LooseConsole =
ProfilingApiSample04Framework.LooseCrossDomainAccessor<
ProfilingApiSample04Framework.GenericHolder<System.IO.TextWriter>>;

namespace ProfilingApiSample04FrameworkTest
{
[TestFixture]
public class ConsoleTest
{
[TestFixtureSetUp]
public void FixtureSetUp()
{
LooseConsole.Unload();
LooseConsole.Register();
LooseConsole.Holder.Source = Console.Out;

// Pre-call to run the action that was registered in this AppDomain,
// not in other AppDomain but in this AppDomain.
// Because the event loop that is managed by NUnit GUI - contains calling
// Write or WriteLine method - runs in other thread.
Console.Write(string.Empty);
Console.Out.Flush();
}

[TestFixtureTearDown]
public void FixtureTearDown()
{
LooseConsole.Holder.Source = null;
LooseConsole.Unload();
}

[Test]
public void Test()
{
LooseConsole.Holder.Source.WriteLine("AppDomain: {0}",
AppDomain.CurrentDomain.FriendlyName);

AppDomain.CurrentDomain.RunAtIsolatedDomain(() =>
{
LooseConsole.Holder.Source.WriteLine("AppDomain: {0}",
AppDomain.CurrentDomain.FriendlyName);
});

LooseConsole.Holder.Source.WriteLine("AppDomain: {0}",
AppDomain.CurrentDomain.FriendlyName);
}
}
}
 
TestFixtureSetUp と TestFixtureTearDown で、持ち運び用のクラスのアンロード、再登録を行っています(22 行目~24 行目、37 行目~38 行目)。あとは一括置換ですね。
一つ気を付けなければならないところと言えば、最初の AppDomain で行われる処理と別の AppDomain で行われる処理が混合してはまずい、ということです。準備の時に触れた CLR の型チェックタイミングに運悪く引っかかると、ExecutionEngineException が吐かれてアプリケーションが異常終了してしまいますので。
NUnit の GUI ツールは、UI の更新のためのイベントループを独自に持ち、[Text Output] タブへの出力も非同期で行われています。30 行目で行っている Write メソッドの事前呼び出しはこれをあらかじめ実行させてしまうためのものです。
さて、実行してみましょう。


キタ━(゚∀゚)━!!

2 つ目の WriteLine 結果もうまく出力されるようになりました!
でもこれだけですと、TextWriter は MarshalByRefObject なのでうまく動いているだけにも思えてしまいますね・・・。次の例も試してみましょう。


例 2: 既存のライブラリ、標準のクラスが AppDomain 越えに対応していない
GenericHolder はそのまま利用できますので、テストの書き換えだけです。

#line 7 "CppTroll\ProfilingApiSample04FrameworkTest\ConsoleTest.cs"
using System;
using System.Diagnostics;
using System.IO;
using NUnit.Framework;
using ProfilingApiSample04Framework.Mixin.System;
using LooseStopwatch =
ProfilingApiSample04Framework.LooseCrossDomainAccessor<
ProfilingApiSample04Framework.GenericHolder<System.Diagnostics.Stopwatch>>;

namespace ProfilingApiSample04FrameworkTest
{
[TestFixture]
public class StopwatchTest
{
[TestFixtureSetUp]
public void FixtureSetUp()
{
LooseStopwatch.Unload();
LooseStopwatch.Register();
LooseStopwatch.Holder.Source = new Stopwatch();
}

[TestFixtureTearDown]
public void FixtureTearDown()
{
LooseStopwatch.Holder.Source = null;
LooseStopwatch.Unload();
}

[Test]
public void Test()
{
using (var sw = new StringWriter())
{
LooseStopwatch.Holder.Source.Restart();

sw.WriteLine("Elapsed: {0} ms", LooseStopwatch.Holder.Source.ElapsedMilliseconds);

AppDomain.CurrentDomain.RunAtIsolatedDomain<StringWriter>(sw_ =>
{
sw_.WriteLine("Elapsed: {0} ms", LooseStopwatch.Holder.Source.ElapsedMilliseconds);
}, sw);

sw.WriteLine("Elapsed: {0} ms", LooseStopwatch.Holder.Source.ElapsedMilliseconds);

Console.WriteLine(sw.ToString());
}
}
}
}
 
AppDomainMixin.RunAtIsolatedDomain の引数で Stopwatch を引き回すのは無理ですので廃止し(45 行目)、代わりに GenericHolder に入れて持ち運びます。どうでしょうか・・・?


キマシタワ - .∵・(゚∀゚)・∵. - ッ!!

ちなみに、私の PC 環境(TOSHIBA ウルトラブック dynabook R631、OS: Windows 7 Home Premium 64 bit、CPU: Core i5-2467M、メモリ: 4GB、SSD)ですと、30 ~ 40 ms で AppDomain の生成~終了ができているようです。比べて、Process の起動~終了の場合、120 ms ~ 130 ms かかりましたので、3 ~ 4 倍は効率が良いようです。なるほどなるほど。


例 3: コンパイラが対象のクラスを自動生成する
Generic なエイリアスは作成できませんので、1 つ新しいクラスを切っていますが、やることはこれまでと同様です。

#line 7 "CppTroll\ProfilingApiSample04FrameworkTest\DelegateTest.cs"
using System;
using NUnit.Framework;
using ProfilingApiSample04Framework;
using ProfilingApiSample04Framework.Mixin.System;

namespace ProfilingApiSample04FrameworkTest
{
class LooseFunc<T1, T2, TResult> : LooseCrossDomainAccessor<GenericHolder<Func<T1, T2, TResult>>> { }

[TestFixture]
public class DelegateTest
{
[TestFixtureSetUp]
public void FixtureSetUp()
{
LooseFunc<int, int, int>.Unload();
LooseFunc<int, int, int>.Register();
}

[TestFixtureTearDown]
public void FixtureTearDown()
{
LooseFunc<int, int, int>.Holder.Source = null;
LooseFunc<int, int, int>.Unload();
}

[SetUp]
public void SetUp()
{
LooseFunc<int, int, int>.Holder.Source = null;
}

[TearDown]
public void TearDown()
{
LooseFunc<int, int, int>.Holder.Source = null;
}

[Test]
public void Test()
{
var adder = default(Func<int, int, int>);
var z = 1;
adder = (x, y) => x + y + z; // To closure(capture local variable z)
LooseFunc<int, int, int>.Holder.Source = adder;
AppDomain.CurrentDomain.RunAtIsolatedDomain(() =>
{
Assert.AreEqual(3, LooseFunc<int, int, int>.Holder.Source(1, 1));
});
}
}
}
 
Stopwatch の例と同様、引数で引き回すのはやめ、持ち運び用のクラスに入れて取りまわすことにしています(51 行目)。これも実行します!


キタ━━━━━ー二三ヘ( ゚∀゚)ノ━━━━━━!!!!!

もはやなんでもありですね (+_+) ただ、もう AppDomain では簡単に分離できなくなってしまうわけですから、これを使わざるを得なくなった状況の、二の舞を演じないように気を使う必要はあります。SetUp や TearDown でやっているように(36 行目、42 行目)、初期化や後始末には細心の注意を払いましょう。




終わりに
前回に引き続き、今回も AppDomain を取り上げましたが、他では見られないようなハックをしてみました。いかがでしたでしょうか?
かなりマニアックな内容で恐縮ですが、私が進めているような、言語(というかプラットフォーム)を拡張するようなライブラリを書こうとすると、やはりどうしても必要になってくる知識だと思います。

ところで、C# などは結構アグレッシブに言語が拡張されているように思いますが、過去の遺産を次の資産として作り直すための拡張って、なかなかないように思います(4.0 の時に「COM 相互運用時の特別処理」っていうのはありましたね)。まあ、C# に限ったことではないとは思いますが、やはり新しい機能のための拡張ということなのでしょう。
しかし、表面的な移行手順はあるとは言え、プログラムの作り方や、古いアーキテクチャをどうやって新しいアーキテクチャに持って行くかというノウハウが、私なんかはもっと言語機能に反映されてもいいと思うのですが、どうなんでしょうか。IT 業界でかかるコストの 7 割は運用・保守となる、ということがわかって、もう 10 年近くが経つのではないかと思うのですが、どうもまだ歴史は繰り返されそうな予感はしています。

私がやっている「Monkey Patching を CLR で動くあらゆる静的言語でも行えるようにする」という活動も、いかにして、既存のものを安全に一歩ずつ変更し、拡張しやすく次の技術へ対応しやすい形に持って行くか、という考えが根底にありますので、これに絡む色々な情報を、今後もウォッチしていきたいと思います。

・・・さて、パズルのピースは揃いました。今後は順次、ライブラリを拡充しながら、引き続き技術情報の発信をしていきたいと思います!



再考! PowerShell で LINQ - Terrific! LINQ to PowerShell -

$
0
0
PowerShell Advent Calendar 2012、5 日目です!

はじめましての人ははじめまして!PowerShell Advent Calendar 2012の 5 日目を担当させていただきます、杉浦と申します。
自分は、本業的には、.NET の静的型付けな言語で構築されたシステムに関わることがほとんどで、PowerShell を扱うのは素人なのですが、業務効率改善や不具合調査等、色んなところで助けられたこともあり、今回少しでも PowerShell 遣いな方々と情報共有できればと、こちらに参加させていただいた次第です。よろしくお願いいたします!

今回扱うテーマは、「PowerShell で LINQ」。もうこれまで散々議論されて来た感がありますが、.NET 開発者の 99.9% の人が知らないあの機能と同様、まだ見落とされてきた何かがあるのでは・・・!?と、私の記事では、これまで Web 上で発表された成果を振り返りつつ、別の手法を改めて考えてみたいと思います。では、早速行ってみましょー!

※今回の記事で扱うのは、PowerShell v2 の情報となります。PowerShell v3 での情報は、また別の機会に検証させていただければと思います <(_ _)>


こちらの情報を参考にさせていただきました。この場を借りてお礼申し上げます。この世界、ホント奥が深いとです・・・...( = =)
統合開発環境「PowerShell ISE」を使ってみよう:CodeZine
add-types.ps1 - poor man's using for PowerShell - BUGBUG poor title - Site Home - MSDN Blogs
PowerShell で LINQ - NyaRuRuが地球にいたころ
Powershell 2.0でチャーチ数の夢を見た - めらんこーど地階
Hey! Don’t break my pipe! IT Pro PowerShell experience
Best Practices for Windows PowerShell
LINQ for PowerShell
Tellingmachine PSUnit PowerShell Unit Testing Framework – Getting Started Guide – Installation - Version 2 Beta 1
Further Down the Rabbit Hole PowerShell Modules and Encapsulation
Pester - BDD style testing framework for PowerShell - @fsugiyamaの技術日誌
if($array -eq $null) には要注意! - PowerShell Scripting Weblog
PowerShell 2.0の新機能(2) ――リモート処理編 :CodeZine
HOW change Microsoft Windows PowerShell"! Ui Culture the time of a PSSession





目次

まずは成果を眺めてみる
作成途中ではあるのですが、もしよろしければ、どんな感じか触ってみていただくのが良いかもしれません。Pesterみたく、PsGetできるとかっこいいのですが、とりあえずは手動です (^_^;)
  1. urasandesu / PSAnonym - GitHubからリポジトリをダウンロード(もしくは git clone)。
  2. 必要に応じて、管理者権限で PowerShell を起動し、Set-ExecutionPolicy を使って、スクリプト実行可能に。
  3. ダウンロードしたリポジトリの中にある、Urasandesu.PSAnonym ディレクトリを $env:PSModulePath にコピー。
  4. Import-Module 'Urasandesu.PSAnonym'でモジュールをインポート。

早速、単純な例として、MSDN にもある「1 から 10 個の整数シーケンスを生成し、それぞれを 2 乗する」をやってみましょう。

PS C:\> QRange 1 10 | QSelect { $_ * $_ } | QToArray
1
4
9
16
25
36
49
64
81
100
 

select や where は既に予約されていますので、Prefix として "Q"を付けてみました(linQ の Q です!)。また、ラムダ式に当たる部分は、ScriptBlock を使って表現しています。引数は、1 つだけの時は、$_ が、複数ある場合は、$1, $2, ・・・がプレースホルダとして使えます。もちろん ScriptBlock ですので、明示的に param () を使って別の名前で受け取ることもできます。集計操作の時などは、意味のある名前を付けたほうがわかりやすいかもしれませんね。
こちらも MSDN にある例で恐縮ですが、「""(半角スペース)で区切られた文字列を逆順にする」を書いてみました。

PS C:\> $sentence = 'the quick brown fox jumps over the lazy dog'
PS C:\> $words = $sentence -split ''
PS C:\> $reversed = { $words } |
>> QAggregate {
>> param ($workingSentence, $next)
>> $next + '' + $workingSentence
>> }
>>
PS C:\> $reversed
dog lazy the over jumps fox brown quick the
 

プレースホルダを使った版はこんな感じになります。C++ の Boostとかをよく使われているのであれば、こちらのほうが親しみがあるかもしれません。

PS C:\> $sentence = 'the quick brown fox jumps over the lazy dog'
PS C:\> $words = $sentence -split ''
PS C:\> $reversed = { $words } | QAggregate { $2 + '' + $1 }
PS C:\> $reversed
dog lazy the over jumps fox brown quick the
 

これと同じですね。

#include <boost/algorithm/string.hpp>
#include <boost/lambda/lambda.hpp>
#include <boost/range/numeric.hpp>
#include <iostream>
#include <string>
#include <vector>

int main(int argc, char* argv[])
{
using namespace boost;
using namespace boost::algorithm;
using namespace boost::lambda;
using namespace std;

string sentence = "the quick brown fox jumps over the lazy dog";

vector<string> words;
split(words, sentence, is_any_of(" "));

string reversed = accumulate(words, string(), _2 + " " + _1);

cout << reversed << endl;
// This code produces the following output:
// dog lazy the over jumps fox brown quick the

return 0;
}
 

最後の例は、良く知られた FizzBuzz 問題。「無限リストを生成し、最初の 36 個の数字について Fizz Buzz する」、こんな感じになるでしょうか。

PS C:\> $query = QRange 1 ([int]::MaxValue) |
>> QSelect {
>> switch ($_) {
>> { $_ % 15 -eq 0 } { 'Fizz Buzz'; break }
>> { $_ % 3 -eq 0 } { 'Fizz'; break }
>> { $_ % 5 -eq 0 } { 'Buzz'; break }
>> default { $_ }
>> }
>> } |
>> QTake 36
>>
PS C:\> ($query | QToArray) -join ', '
1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz, 11, Fizz, 13, 14, Fizz Buzz, 16, 17, Fizz, 19, Buzz, F
izz, 22, 23, Fizz, Buzz, 26, Fizz, 28, 29, Fizz Buzz, 31, 32, Fizz, 34, Buzz, Fizz
 

最終的に用意するものは、以下の表のようなものを考えています。
PowerShell 関数名エイリアス機能元の LINQ クエリ実装
状況
Get-Aggregated QAggregateシーケンスにアキュムレータ関数を適用する。 Aggregate 100%
Get-AllSatisfied QAllシーケンスのすべての要素が条件を満たしているかどうかを判断する。 All<TSource> 100%
Get-AnySatisfied QAnyシーケンスの要素が存在するか、または条件を満たすかどうかを判断する。 Any 100%
Get-Average QAverage数値のシーケンスの平均値を計算する。 Average 100%
ConvertTo-Casted
Select-Casted
QCast列挙子の要素を、指定した型に変換する。 Cast<TResult> 100%
Join-Concatenated QConcat 2 つのシーケンスを連結する。 Concat<TSource> 100%
Get-Contained QContains指定した要素がシーケンスに格納されているかどうかを判断する。 Contains 100%
Get-Count QCountシーケンス内の要素数を返する。 Count 100%
Get-DefaultIfEmpty
Select-DefaultIfEmpty
QDefaultIfEmpty列挙子の要素を返する。シーケンスが空の場合は既定値を持つシングルトン コレクションを返する。 DefaultIfEmpty 100%
ConvertTo-Distinct QDistinctシーケンスから一意の要素を返する。 Distinct 100%
Get-ElementAt QElementAtシーケンス内の指定されたインデックス位置にある要素を返する。 ElementAt<TSource> 0%
Get-ElementAtOrDefault QElementAtOrDefaultシーケンス内の指定されたインデックス位置にある要素を返する。インデックスが範囲外の場合は既定値を返する。 ElementAtOrDefault<TSource> 0%
New-Empty QEmpty指定した型引数を持つ空の 列挙子を返する。 Empty<TResult> 0%
Join-Except QExcept 2 つのシーケンスの差集合を生成する。 Except 0%
Get-First QFirstシーケンスの最初の要素を返する。 First 0%
Get-FirstOrDefault QFirstOrDefaultシーケンスの最初の要素を返する。要素が見つからない場合は既定値を返する。 FirstOrDefault 0%
Group-SequenceBy QGroupByシーケンスの要素をグループ化する。 GroupBy 100%
Join-GroupedBy QGroupJoinキーが等しいかどうかに基づいて 2 つのシーケンスの要素を相互に関連付け、その結果をグループ化する。 GroupJoin 0%
Join-Intersect QIntersect 2 つのシーケンスの積集合を生成する。 Intersect 0%
Join-Sequence QJoin一致するキーに基づいて 2 つのシーケンスの要素を相互に関連付ける。 Join 0%
Get-Last QLastシーケンスの最後の要素を返する。 Last 0%
Get-LastOrDefault QLastOrDefaultシーケンスの最後の要素を返する。要素が見つからない場合は既定値を返する。 LastOrDefault 0%
Get-LongCount QLongCountシーケンス内の要素数を表す Int64 を返する。 LongCount 0%
Get-Max QMax値のシーケンスの最大値を返する。 Max 0%
Get-Min QMin値のシーケンスの最小値を返する。 Min 0%
ConvertTo-OfType QOfType指定された型に基づいて 列挙子の要素をフィルタ処理する。 OfType<TResult> 0%
Select-OrderBy QOrderByシーケンスの要素を昇順に並べ替える。 OrderBy 100%
Select-OrderByDescending QOrderByDescendingシーケンスの要素を降順に並べ替える。 OrderByDescending 100%
New-Range QRange指定した範囲内の整数のシーケンスを生成する。 Range 100%
New-Repeat QRepeat繰り返される 1 つの値を含むシーケンスを生成する。 Repeat<TResult> 100%
ConvertTo-Reversed QReverseシーケンスの要素の順序を反転させる。 Reverse<TSource> 0%
Select-Sequence QSelectシーケンスの各要素を新しいフォームに射影する。 Select 100%
Select-ManySequence QSelectManyシーケンスの各要素を 列挙子に射影し、結果のシーケンスを 1 つのシーケンスに平坦化する。 SelectMany 100%
Get-SequenceEquality QSequenceEqual等値比較演算子に従って 2 つのシーケンスが等しいかどうかを判断する。 SequenceEqual 0%
Get-Single QSingle値のシーケンスの 1 つの特定の要素を返する。 Single 0%
Get-SingleOrDefault QSingleOrDefault値のシーケンスの 1 つの特定の要素を返する。そのような要素が見つからない場合は既定値を返する。 SingleOrDefault 0%
Skip-Sequence QSkipシーケンス内の指定された数の要素をバイパスし、残りの要素を返する。 Skip<TSource> 0%
Skip-SequenceWhile QSkipWhile指定された条件が満たされる限り、シーケンスの要素をバイパスした後、残りの要素を返する。 SkipWhile 0%
Get-Sum QSum数値のシーケンスの合計を計算する。 Sum 0%
Find-CountOf QTakeシーケンスの先頭から、指定された数の連続する要素を返する。 Take<TSource> 100%
Find-While QTakeWhile指定された条件を満たされる限り、シーケンスから要素を返した後、残りの要素をスキップする。 TakeWhile 100%
Select-ThenBy QThenByシーケンス内の後続の要素を昇順で配置する。 ThenBy 100%
Select-ThenByDescending QThenByDescendingシーケンス内の後続の要素を降順で配置する。 ThenByDescending 100%
ConvertTo-Array QToArray列挙子から配列を作成する。 ToArray<TSource> 100%
ConvertTo-Dictionary QToDictionary列挙子から Hashtable を作成する。 ToDictionary 0%
ConvertTo-List QToList列挙子から ArrayList を作成する。 ToList<TSource> 0%
ConvertTo-Lookup QToLookup列挙子から Lookup を作成する。 ToLookup 0%
Join-Union QUnion 2 つのシーケンスの和集合を生成する。 Union 0%
Find-Sequence QWhere述語に基づいて値のシーケンスをフィルタ処理する。 Where 100%
Invoke-Linq QRunクエリを単純に起動する。 - 100%
2012/12/05 現在、進捗率は 30%。なるべく今月中に一通り仕上げたいところですが、師走でもあり何かとありまして・・・ぼちぼち進められればと思いますです (^_^;)
2012/12/24 現在、進捗率は 48%。思ったより多い…。途中、今後に向けた仕込みもしていたためか、進捗は芳しくありません。ちょいちょい進めますです (^^ゞ

さてさて、実は最後に挙げた例ですが、PowerShell に LINQ するような小品を作りたいと思った場合、障害となるいくつかの特徴を持っています。ここから先は、ちょっとお堅い話になりますので、興味があればご覧ください~。




先人の成果を検証させていただく
2012/12/05 現在、Bing で、キーワード "PowerShell LINQ"を使った検索をしてみると、先人の方々が試行錯誤された結果がずらずらと見つかります。その結果、44 万件以上・・・。それは、PowerShell が動的型付け言語であるにも関わらず、元の静的型付けで Generics な I/F を使いやすくするような Wrapper であったり、Dynamic Query を使った統一的な表記で DB へのアクセスを可能にした、1 つの言語内 DSL であったり、と本当に多種多様です。

これらの中から、今回は検索結果の 1 ページ目に現れる、ある程度のクエリが定義された以下の 2 つについて検証させていただくことにしました。

LINQ for PowerShell
  • 2010/02 ごろに公開された Josh Einstein 氏作成のライブラリです。
  • *.psm1 形式になっていますので、ダウンロード後、Import-Module … し、インストールを行います。
  • 20 以上の LINQ 処理が定義されており、一通りの処理はこれだけでできるようになっています。
参考サンプルコードを引用します:

Import-Module LINQ

function Assert-AreEqual($Expected, $Actual) {
if (@(Compare-Object $Expected $Actual -SyncWindow 0).Length) {
$OFS = ','
Write-Error "Assert-AreEqual Failed: Expected=($Expected), Actual=($Actual)"
}
}

# Take
Assert-AreEqual -Expected @(1..3) -Actual @(1..5 | Linq-Take 3)

# TakeWhile
Assert-AreEqual -Expected @(1..2) -Actual @(1..5 | Linq-TakeWhile { $_ -lt 3})

# Repeat
Assert-AreEqual -Expected @(1..5) -Actual @(1..5 | Linq-Repeat 1)
 

PowerShell で LINQ - NyaRuRuが地球にいたころ
  • NyaRuRu 氏解説の日本語による記事です。2008/12 ごろに公開ということは、なんと今から 4 年も前!
  • ダウンロードファイル等はないようですので、適当に *.psm1 や *.ps1 としてコピペして使うことになります。
  • 定義されている LINQ 処理は 5 つ程ですが、元々ある機能も十分強力ですので、組み合わせて使うことを想定されているのでしょう。
参考サンプルコードを引用します:

# win.ini のダンプ
xrepeat {new-object IO.StreamReader 'c:\windows\win.ini'} `
| xselect { $_.ReadLine() } `
| xtakewhile { $_ -ne $null } `
| Out-Default

#1 桁の乱数を 100 個得る
xrepeat {new-object Random} `
| xselect { $_.Next(0,10) } `
| xtake 100 `
| Out-Default
 




ん?何が違う?
サンプルが異なると、パッと見同じ雰囲気に見えますね。ですので、先ほどの FizzBuzz 問題をどのライブラリでも扱えるよう変形して、処理を揃えてみましょうか。

PS C:\> function NewCounter {
>> New-Object psobject |
>> Add-Member NoteProperty m_counter 0 -PassThru |
>> Add-Member ScriptMethod Increment `
>> {
>> $this.m_counter++
>> $this.m_counter
>> } -PassThru
>> }
>>
 
無限リストのためのカウンターを生成する補助関数です。どの例からでも参照できるように最初に定義しておきましょう。
これを使った私のものの例はこうなります。

PS C:\> $query = QRepeat { NewCounter } |
>> QSelect { $_.Increment() } |
>> QSelect {
>> switch ($_) {
>> { $_ % 15 -eq 0 } { 'Fizz Buzz'; break }
>> { $_ % 3 -eq 0 } { 'Fizz'; break }
>> { $_ % 5 -eq 0 } { 'Buzz'; break }
>> default { $_ }
>> }
>> } |
>> QTake 36
>>
PS C:\> ($query | QToArray) -join ', '
1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz, 11, Fizz, 13, 14, Fizz Buzz, 16, 17, Fizz, 19, Buzz, F
izz, 22, 23, Fizz, Buzz, 26, Fizz, 28, 29, Fizz Buzz, 31, 32, Fizz, 34, Buzz, Fizz
 

Einstein 氏のライブラリを使うと、こんな感じの表現になります。

PS C:\> $query = NewCounter |
>> Linq-Repeat (40) |
>> Linq-Select { $_.Increment() } |
>> Linq-Select {
>> switch ($_) {
>> { $_ % 15 -eq 0 } { 'Fizz Buzz'; break }
>> { $_ % 3 -eq 0 } { 'Fizz'; break }
>> { $_ % 5 -eq 0 } { 'Buzz'; break }
>> default { $_ }
>> }
>> } |
>> Linq-Take 36
>>
PS C:\> $query -join ', '
1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz, 11, Fizz, 13, 14, Fizz Buzz, 16, 17, Fizz, 19, Buzz, F
izz, 22, 23, Fizz, Buzz, 26, Fizz, 28, 29, Fizz Buzz, 31, 32, Fizz, 34, Buzz, Fizz
 
2 行目の Linq-Repeat、上限が 40 に制限されていますが、これを元の問題であったような無限リストに近づけようと、[int]::MaxValue にすることはできません。
スクリプトが終わらなくなってしまうからです。
PowerShell v2 でよく問題として挙げられる挙動に、Select-Object コマンドレットの -First パラメータを指定しても、パイプラインが止まらないというものがあると思いますが、これも同様の問題を抱えているようです。

NyaRuRu 氏のライブラリの場合は、こんな感じでしょうか。

PS C:\> $query = $(do {
>> xrepeat { NewCounter } |
>> xselect { $args[0].Increment() } |
>> xselect {
>> switch ($args[0]) {
>> { $_ % 15 -eq 0 } { 'Fizz Buzz'; break }
>> { $_ % 3 -eq 0 } { 'Fizz'; break }
>> { $_ % 5 -eq 0 } { 'Buzz'; break }
>> default { $_ }
>> }
>> } |
>> xtake 36
>> } until ($true))
>>
PS C:\> $query -join ', '
1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz, 11, Fizz, 13, 14, Fizz Buzz, 16, 17, Fizz, 19, Buzz, F
izz, 22, 23, Fizz, Buzz, 26, Fizz, 28, 29, Fizz Buzz, 31, 32, Fizz, 34, Buzz, Fizz
 
Einstein 氏のライブラリと比べると、無限リストであることは良いのですが、全体が do { } until (...) で囲まれるようになりました。残念ながら、これを外すことはできません。
スクリプトが途中で終了してしまうからです。
元の xtake の中に break 文を見つけることができますが、本来これは foreach/for/while/do/switch 文をただちに終了するためのものです。break の外側にこれらの囲いが無い場合、容赦なくスクリプトを止めてしまうのですね。

あとは共通した問題として、1 行目の $query が遅延評価されていない、というものがあります(どちらの例でも、Take に与える数を増やした分だけ、最初の式の実行に時間がかかるようになります)。元々の LINQ では、foreach に引き渡したり、スカラー値を戻すメソッド(All や Cout、ToArray など)を呼ばない限りは、実際の計算を先延ばしにすることができました。
しかしながら、PowerShell の動きとして、IEnumerable や IEnumerator を実装したオブジェクトは、ありとあらゆる場所で先行評価が試みられます。そのポイントは、-eq 演算子の左辺への指定や、ValidateNotNull 検証属性が付与された関数への引き渡し、変数への格納のタイミングなど、本当にいたるところでその対象になってしまいます。よほど気を付けていないと回避はできません。LINQ で連鎖させることになっている IEnumerableオブジェクトは、扱うのが困難という結論になってしまうのです。




どうやって解決を?
「間接法をもう一段増やせば解けない問題はない.―Butler Lampson」ですね。先行評価されないものを一枚被せれば良いのです。
構文的・ライブラリのサポートが充実しているということから、私は ScriptBlock を被せてみました。さらに、これなら最後の評価文を do { } until (...) で囲むことで、中で break しても安全に実行できるといううれしいオマケも付いてきます。最初の例で使った 3 つのクエリの中身を覗いてみましょう。

function New-Range {
[CmdletBinding()]
[OutputType([scriptblock])]
param (
[Parameter(Position = 0, Mandatory = $true)]
[int]
$Start,

[Parameter(Position = 1, Mandatory = $true)]
[int]
$Count
)

if (($Count -lt 0) -or ([int]::MaxValue -lt ($Start + $Count - 1))) {
$exParamName = '$Count'
$exName = 'ArgumentOutOfRangeException'
$ex = New-Object $exName -ArgumentList $exParamName
throw $ex
}

{
[CmdletBinding()]
param (
[switch]
$WithSpecialInvoker = $(throw New-Object Urasandesu.PSAnonym.Linq.InvalidInvocationException)
)

$Start = $Start
$Count = $Count

for ($index = $Start; $index -lt $Start + $Count; $index++) {
,$index
}

}.GetNewClosure()
}

New-Alias QRange New-Range
 

function Select-Sequence {
[CmdletBinding()]
[OutputType([scriptblock])]
param (
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[scriptblock]
$InputObject,

[Parameter(Position = 0, Mandatory = $true)]
[scriptblock]
$Selector
)

{
[CmdletBinding()]
param (
[switch]
$WithSpecialInvoker = $(throw New-Object Urasandesu.PSAnonym.Linq.InvalidInvocationException)
)

$InputObject = $InputObject
$Selector = $Selector

& $InputObject -WithSpecialInvoker |
ForEach-Object {
, (& $Selector.GetNewClosure() $_)
}

}.GetNewClosure()

}

New-Alias QSelect Select-Sequence
 

function ConvertTo-Array {
[CmdletBinding()]
[OutputType([scriptblock])]
param (
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[scriptblock]
$InputObject
)

@(do {
& $InputObject -WithSpecialInvoker |
ForEach-Object {
$_
}
} until ($true))

}

New-Alias QToArray ConvertTo-Array
 
QRange/QSelect が返す ScriptBlock 内では、関数内の変数を参照する必要がありますので、GetNewClosure で環境を保存しておきます。また、決まった実行方法で実行しないとスクリプトが途中で終了してしまうことがありますので、それを防ぐために返した ScriptBlock の呼び出し方法を若干面倒にしてみました。QToArray では、実行する ScriptBlock を do { } until (...) で囲み、配列に変換しているという寸法です。
これで、基本的に同様の idiom で、これまでの問題は解決できることになります。つまり、全ての LINQ クエリを定義することができるようになったのです・・・はい、後は力業ですね (`・ω・ )ゝ




終わりに
PowerShell Advent Calendar 2012の 5 日目は、PowerShell に LINQ するライブラリを再考してみました。いかがでしたか?たまには振り返るのも良いかな、と。Windows 8 の新基盤 WinRT で、また COM の知識が見直されたりしていますしね。

PowerShell、個人的には、Windows 7 以降、標準で搭載されている強力な統合開発環境(PowerShell ISE)や、.NET Framework との親和性の高さなどもあり、Windows 環境においては、最強クラスのスクリプト言語の 1 つだろうと感じています。
この記事が、少しでも PowerShell 遣いの方々のお役に立てば、また、他の .NET 系言語で LINQ を知った方が、PowerShell をちょっとでも触ってみようかと思っていただければ幸いに思いますです。

さあ、引き続き PowerShell Advent Calendar 2012楽しみたいと思います。明日の担当は @twit_ahfさんです。
よろしくどうぞ~(´ー`~)


about_SessionState - Things That Are Precious To a PowerShell Librarian -

$
0
0
PowerShell Advent Calendar 2012、24 日目です♪

今年ももうすぐ終わり・・・...( = =)、PowerShell Advent Calendar 2012も残りわずかとなりましたね。
昨日は @MasayaSawadaさんの『Windows Server 2012 Essentials の Powershell』でした。


さて街は「クリスマス」一色で煌びやかな 24 日。こちらも負けじと、PowerShell の華(と私が勝手に思っている)、セッションのお話をさせていただければと。

セッション。それは、ヘルプ トピック上、いたるところで登場し、構成要素を追加されたり、検索されたり、削除されたり、インポートされたり、エクスポートされたりする領域。個人的には、PowerShell の超中心的概念だと思うのですが、驚くべきことに、これを説明するヘルプ トピックはありません。代わりに、スコープやモジュール、PSSession、関数、変数のトピックを見て回ることにより、それが PowerShell が持つ環境そのものであり、なるべく問題領域を局所化し、他の部分へ影響を与えにくくするための仕組みであることが、朧げながら見えてくるように思います。

まあ、イマドキのプログラミング言語であれば、このような仕組みを何かしら持っているはずで、目新しいことは無いのですが・・・。ただ、PowerShell に限らず、IDE 側のサポートが少なくなりがちな動的型付け言語では、このような言語側の仕組みに頼るだけでなく、なるべく扱いやすい規模でファイルを分割したり、状態を制御できるように保ったり、といったことを、人間側で、ある程度お膳立てしないといけないのだなー、と感じる今日この頃。先日の記事に登場するライブラリ、PSAnonymも、この辺りの考え方を盛り込み、今後なるべく機能の追加や拡張をしやすい形に整備しました。

そんな試行錯誤から、今回は、about_SessionState と銘打ち、セッションが何なのかということを、自分の理解の整理も兼ねて、できるだけ俯瞰的に解説できればと思います。Tips のような、これは便利!というものではないのですが、PowerShell で、これからある程度まとまったものを作ろうと考えられている方へ参考になれば幸いです。

それではぼちぼち始めましょう~。

※今回の記事で扱うのは、PowerShell v2 の情報となります。PowerShell v3 での情報は、また別の機会に検証させていただければと思います <(_ _)>


こちらの情報を参考にさせていただきました。いつもお世話になっております! (`・ω・ )ゝ
Best Practices for Windows PowerShell
PowerShellでプロトタイプベースのオブジェクト指向を記述する方法 - 趣味の無い人生は虚しい
Parallel LINQ (PLINQ)
Writing your own PowerShell Hosting App (Part 1. Introduction) PowerShell Station
How to Host PowerShell in a WPF Application
snoopshell The marriage of Snoop WPF and PowerShell





目次

PS1 means Pathologically Suppleness no.1!?
「PowerShell でまとまったものなんて作らないよ」・・・(ToT)
いきなりそんなことおっしゃらずに・・・いやいや、奥さん、ちょっとこんなデータがあるんですよ。

まとまったものを作る時にプログラマの誰しもが考えるのが、書くコードの一貫性、すなわちコーディングスタイル。
いくつかのプログラミング言語の状況を比べてみますと、2012/12/24 現在、Bing での検索結果は以下のようなものになりました:

  "JavaScript Coding Style"で検索: 約 1,460,000 件 / "JavaScript"で検索: 約 819,000,000 件⇒ 約 0.2 %
  "Scala Coding Style"で検索: 約 67,300 件 / "Scala"で検索: 約 14,200,000 件⇒ 約 0.5 %
  "Basic Coding Style"で検索: 約 2,490,000 件 / "Basic"で検索: 約 314,000,000 件⇒ 約 0.8 %
  "PHP Coding Style"で検索: 約 2,150,000 件 / "PHP"で検索: 約 282,000,000 件⇒ 約 0.8 %
  "Ruby Coding Style"で検索: 約 626,000 件 / "Ruby"で検索: 約 71,700,000 件⇒ 約 0.9 %
  "Java Coding Style"で検索: 約 1,460,000 件 / "Java"で検索: 約 94,000,000 件⇒ 約 1.6 %
  "C++ Coding Style"で検索: 約 1,020,000 件 / "C++"で検索: 約 22,200,000 件⇒ 約 4.6 %
  "C# Coding Style"で検索: 約 860,000 件 / "C#"で検索: 約 18,600,000 件⇒ 約 4.6 %
  "Perl Coding Style"で検索: 約 776,000 件 / "Perl"で検索: 約 11,800,000 件⇒ 約 6.6 %
  "F# Coding Style"で検索: 約 75,600 件 / "F#"で検索: 約 735,000 件⇒ 約 10.3 %

  "PowerShell Coding Style"で検索: 約 99,900 件 / "PowerShell"で検索: 約 496,000 件約 20.1 %

ちょっとスゴいことになっていますね。この割合を鵜呑みにするならば、PowerShell をやられている方の 5 人に 1 人がコーディングスタイルに悩まれていることになります。
個人的には、柔軟な書き方ができる言語(1 つのことをするのに何通りもやり方がある言語)ほど、このような議論が発生しやすいと思っているのですが、あの C++ や Perl、他の Microsoft 製ごった煮言語、C#/F# を抑えての堂々の 1 位とは・・・。また、これだけ高い割合で、コードの書き方に悩まれる方がいらっしゃる、ということは PowerShell で何かしら作ろうとされている方が多いことの表れとも考えられるのではないでしょうか。・・・えっ、無理やり過ぎるですって?そこは発信者特権ということで (^_^;)

この結果を信じるかどうかはさておき、コーディングスタイルで悩むことの 1 つに、「1 ファイルの行数は何行?」「1 メソッドの行数は何行?」といったものがあるかと思います。近頃は関数型なエッセンスを取り込んでいる言語も少なくなく、1 行の裏側で色んなことが行われていますので、上記のような制限を掛けるまでもなくどんどん簡素化される印象があります。
しかしながら、特に低レイヤな仕組みを構築するに当たり、処理が増えていった場合にどのようにして分割すると問題が起こりにくくなるかをあらかじめ検討しておくことは、後々気を病まずに済むことになるでしょう。
PowerShell において、このような分割を検討するに当たり、覚えておく必要があるのが、セッションということになります。




about_Scopes
そのものずばりなヘルプ トピックは首記の通り無いのですが、比較的俯瞰された説明があるのが about_Scopesになります。セッションに触れられるのはちょこっとなのですが、PowerShell のセッションに似た概念であるスコープは 1 つトピックが切られており、それに類似する概念も解説されています。
ざっと書き出してみましょう:

  ・スコープは以下のルールにより、変更してはいけない項目を誤って変更することを防止する:
    ・スコープ内に作成できる項目は、変数、エイリアス、関数、スクリプトまたは PowerShell ドライブである。
    ・スコープ内に作成された項目は、明示的にプライベートとして宣言した場合を除き、項目が作成された
     スコープ内およびその子スコープ内でのみ可視になる。
    ・スコープ内に作成した項目は、他のスコープを明示的に指定した場合を除き、項目が作成されたスコープ内でのみ変更できる。
  ・スコープには以下の種類がある:
    ・グローバル
    ・ローカル
    ・スクリプト
    ・プライベート
    ・番号付きスコープ
  ・スコープは以下のタイミングで生成される:
    ・スクリプトまたは関数を実行したとき
    ・PSSession を作成したとき
    ・PowerShell の新しいインスタンスを起動したとき
  ・スコープに類似した概念に、以下のような自己完結型の環境がある:
    ・PSSession
    ・モジュール
    ・入れ子になったプロンプト

ここにセッションを絡めてみます。文字ばかりだとややこしいですので、図でまとめてみるとこんな感じになるかと思います。


・・・ややこしいのは相変わらずですが、少しは俯瞰しやすくなったでしょうか (^_^;)

ちょっと補足しますと、PowerShell のヘルプ トピックでセッションと言った場合、「.NET におけるアプリケーションのドメイン(AppDomain)」を同時に指している場合や、「PowerShell の新しいインスタンスを起動すると自動的に開始される環境」を単に指している場合、「コンピューター毎に作成できる PSSession」を同時に指している場合・・・というパターンがあるようなのです。

一括りにすると混乱すると感じましたので図でも分けてありますが、この記事では、単に「セッション」と言った場合、基本的には 2 番目のものを指すものとしたいと思います。PowerShell のヘルプ トピック上で単に「セッション」とあっても、最初のものを指していると思われる場合は、AppDomain + セッションとし、3 番目を指していると思われる場合は、PSSession + セッションとしています。

また、モジュールがセッションを持っていることにしています。これは about_Modules トピックには記載はないのですが、PowerShell の振る舞いから便宜上そうしているものです。スコープについても、基本的にセッション + スコープという形で動作しています。

あとは、about_Scopes トピックの関連項目にちらっと記載があるだけの環境変数や作業フォルダなどの OS の環境ですが、変数としては特別に扱われるため、注意が必要です。

さて、外側の領域から、OS の環境/PSSession + セッション、AppDomain + セッション、セッション + スコープと、コマンドを叩きながら代表的な構成要素の動きを見ていきたいと思います。




OS の環境/PSSession + セッション
OS の環境である環境変数や作業フォルダですが、PSSession では新しいものが 1 つ切られ、プロンプトでは起動元のプロンプトのものが引き継がれます。
PSSession はコンピューター毎に作成されるものですし、環境変数などが起動元のプロンプトのものが引き継がれるのは標準のコマンド プロンプトと同じ動きと同じですので、これは問題無さそうですね。

C:\>:: 標準のコマンド プロンプトで環境変数を変更。
C:\>set EnvironmentVariable=Environment Variable

C:\>echo %EnvironmentVariable%
Environment Variable

C:\>:: PowerShell プロンプトを実行。環境変数が引き継がれる。
C:\>powershell -nol -nop -c "& { $Env:EnvironmentVariable }"
Environment Variable

C:\>:: 入れ子になったPowerShell プロンプトを実行。環境変数が引き継がれる。
C:\>powershell -nol -nop -c "& { powershell -nol -nop -c '& { $Env:EnvironmentVariable }' }"
Environment Variable

C:\>:: PSSession では新しい OS の環境が用意される。
C:\>powershell -nol -nop -c "& { icm { iex '$Env:EnvironmentVariable' } -session (nsn localhost) }"

C:\>
 

PSSession って実際は何なの?と思って調べてみると、以下のような結果に。WinRM サービスから起動される、PowerShell.exe とは別の PowerShell ホスティング プロセスのようです。

PS C:\> Enter-PSSession localhost
[localhost]: PS C:\Users\User\Documents> [Diagnostics.Process]::GetCurrentProcess() | Format-List


Id : 1292
Handles : 279
CPU : 1.6224104
Name : wsmprovhost



[localhost]: PS C:\Users\User\Documents>
 





AppDomain + セッション
PowerShell に限らず、.NET を扱うプロセスでは、基本的に AppDomain の呪縛からは逃れられませんので、無理に隠ぺいして良いことはないと思うのですがどうなんでしょう・・・? (^_^;)
ヘルプ トピックでは単にセッションという記載しか無いのにも関わらず、実際には AppDomain + セッションを指しているものは、Add-PSSnapin や Add-Type、Assembly 指定版の Import-Module が該当します。無理している感じは、例えば、Add-PSSnapin と対になる Remove-PSSnapin のヘルプ トピックにある、「スナップインは、現在のセッションから削除した後も読み込まれた状態のまま」という説明の苦しさからもわかります。読み込まれた状態ってどこに読み込まれたままなのー???と疑問に思う方は少なくないと思いますが、その答えが AppDomain になります。

PS C:\> # 現在のプロセスの AppDomain に読み込まれている Assembly の数を表示する。
PS C:\> [AppDomain]::CurrentDomain.GetAssemblies().Length
21
PS C:\> # スナップインを追加してみる。
PS C:\> Add-PSSnapin SqlServerCmdletSnapin100
PS C:\> # コマンドは増えた?
PS C:\> (gcm).Length
414
PS C:\> # もう一度 AppDomain に読み込まれている Assembly の数を表示する。
PS C:\> [AppDomain]::CurrentDomain.GetAssemblies().Length
23
PS C:\> # コマンド、読み込まれている Assembly、共に 2 つ増えた!スナップインを削除してみる。
PS C:\> Remove-PSSnapin SqlServerCmdletSnapin100
PS C:\> # コマンド、読み込まれている Assembly を確認すると、コマンドは減ってるけど
PS C:\> # Assembly は減っていない。
PS C:\> (gcm).Length
412
PS C:\> [AppDomain]::CurrentDomain.GetAssemblies().Length
23
PS C:\>
 

同じ AppDomain に、同一名称の別の型は読み込めません。標準の PowerShell ホスティング プロセスは、基本的にはシングル ドメインしか扱いませんので、一度 Add-Type した型を定義し直したい場合などは、プロンプトもしくは PSSession を使って新しいプロセスを開始する必要があります。

PS C:\> # 入れ子になったプロンプトの開始。
PS C:\> powershell -nol -nop
PS C:\> # 型の追加。
PS C:\> Add-Type NewType -m 'public static string NewTypeMethod() { return "New Type"; }' -names $null
PS C:\> [NewType]::NewTypeMethod()
New Type
PS C:\> # ここでは再定義はできない。
PS C:\> Add-Type NewType -m 'public static string NewTypeMethod() { return "New Other Type"; }' -names $null
Add-Type : 型を追加できません。型名 '.NewType'は既に存在しています。
発生場所 行:1 文字:9
+ Add-Type <<<< NewType -m 'public static string NewTypeMethod() { return "New Other Type"; }' -names $null
+ CategoryInfo : InvalidOperation: (.NewType:String) [Add-Type]、Exception
+ FullyQualifiedErrorId : TYPE_ALREADY_EXISTS,Microsoft.PowerShell.Commands.AddTypeCommand

PS C:\> # 入れ子になったプロンプトの終了。
PS C:\> exit
PS C:\> # 入れ子になったプロンプトの再起動。
PS C:\> powershell -nol -nop
PS C:\> # 再定義。
PS C:\> Add-Type NewType -m 'public static string NewTypeMethod() { return "New Other Type"; }' -names $null
PS C:\> [NewType]::NewTypeMethod()
New Other Type
PS C:\>
 





セッション + スコープ
変数、エイリアス、関数、スクリプトブロック、PowerShell ドライブ、スクリプト、モジュールは、親の構成要素のセッションに追加される形で動作します。
変数、エイリアス、関数、スクリプトブロック、PowerShell ドライブ、スクリプトは、それ自身のセッションを持たない代わりにスコープの制御が行われます。モジュールは逆に、それ自身のセッションを持つ代わりにスコープの制御は行われません。

違いが分かりやすい例としてスクリプトとモジュールを取り上げてみましょう。スクリプトをプロンプトのセッションで実行します。変数はスコープを抜けると、セッションから削除されます。

PS C:\> # スクリプト内で、変数を宣言(変数は、プロンプトのセッションに、
PS C:\> # スクリプト スコープのものとして追加される)
PS C:\> @"
>> `$Script1 = 'New Script 1'; gv Script*
>> "@ > Script1.ps1
>>
PS C:\> @"
>> `$Script2 = 'New Script 2'; gv Script*
>> "@ > Script2.ps1
>>
PS C:\> # 各スクリプトの実行が終わると、スクリプト スコープの変数は、
PS C:\> # プロンプトのセッションから削除される。
PS C:\> .\Script1.ps1

Name Value
---- -----
Script1 New Script 1


PS C:\> gv Script*
PS C:\> .\Script2.ps1

Name Value
---- -----
Script2 New Script 2


PS C:\> gv Script*
PS C:\> ri .\Script1.ps1; ri .\Script2.ps1
PS C:\>
 

スクリプトはセッションを持ちませんので、スクリプト内でモジュールをインポートした場合、プロンプトのセッションに追加されます。

PS C:\> # スクリプト内で、モジュールをインポート(スクリプトはセッションを持たないため、モジュールは、
PS C:\> # プロンプトのセッションにインポートされる)
PS C:\> @"
>> 'Module1' | nmo { function Module1Func { 'Module 1 Func' } } | ipmo
>> "@ > Script1.ps1
>>
PS C:\> @"
>> 'Module2' | nmo { function Module2Func { 'Module 2 Func' } } | ipmo
>> "@ > Script2.ps1
>>
PS C:\> # モジュールはスコープを持たないため、インポートしたセッションが終了もしくは
PS C:\> # 明示的に削除が行われるまでは残ったまま。
PS C:\> .\Script1.ps1
PS C:\> gmo

ModuleType Name ExportedCommands
---------- ---- ----------------
Script Module1 Module1Func


PS C:\> .\Script2.ps1
PS C:\> gmo

ModuleType Name ExportedCommands
---------- ---- ----------------
Script Module1 Module1Func
Script Module2 Module2Func


PS C:\> rmo *; ri .\Script1.ps1; ri .\Script2.ps1
PS C:\>
 

逆にモジュールはセッションを持ちます。モジュール内でモジュールをインポートした場合、入れ子になったモジュールはモジュールのセッションに追加されることに注意しなければなりません。
特にまとまったものを作る場合、モジュールの中にモジュールを定義したくなる場面が多々あるかと思いますが、このことを忘れなければ混乱せずに済むでしょう。

PS C:\> # モジュール内で、モジュールをインポート(入れ子になったモジュールは、モジュールのセッションに
PS C:\> # インポートされる)
PS C:\> @"
>> 'NestedModule1' | nmo { function NestedModule1Func { 'Nested Module 1 Func' } } | ipmo
>> "@ > Module1.psm1
>>
PS C:\> @"
>> 'NestedModule2' | nmo { function NestedModule2Func { 'Nested Module 2 Func' } } | ipmo
>> "@ > Module2.psm1
>>
PS C:\> # プロンプトのセッションには入れ子になったモジュールは無く、モジュールのセッションにインポート
PS C:\> # されていることがわかる。
PS C:\> $m1 = ipmo .\Module1.psm1 -pa
PS C:\> gmo

ModuleType Name ExportedCommands
---------- ---- ----------------
Script Module1 NestedModule1Func


PS C:\> & $m1 { gmo }

ModuleType Name ExportedCommands
---------- ---- ----------------
Script NestedModule1 NestedModule1Func
Script Module1 NestedModule1Func


PS C:\> $m2 = ipmo .\Module2.psm1 -pa
PS C:\> gmo

ModuleType Name ExportedCommands
---------- ---- ----------------
Script Module1 NestedModule1Func
Script Module2 NestedModule2Func


PS C:\> & $m2 { gmo }

ModuleType Name ExportedCommands
---------- ---- ----------------
Script NestedModule2 NestedModule2Func
Script Module1 NestedModule1Func
Script Module2 NestedModule2Func


PS C:\> # ただし、同時に親セッションにメンバーがエクスポートされているため(NestedModule1 -> Module1 ->
PS C:\> # プロンプト、NestedModule2 -> Module2 -> プロンプト)、関数は呼べる。
PS C:\> NestedModule1Func
Nested Module 1 Func
PS C:\> NestedModule2Func
Nested Module 2 Func
PS C:\> rmo *; ri .\Module1.psm1; ri .\Module2.psm1
PS C:\>
 





終わりに
PowerShell Advent Calendar 2012の 24 日目、about_SessionState。いかがでしたか。願わくは、いつか PowerShell v3 のヘルプ トピックのどこかに、この辺りの話が載ればと思うばかりです。

ただ、まとめてみて思いましたが・・・ある意味「苦理済ます」にふさわしい、己の心を一人っきりで見つめる修行の日にぴったりの内容になってしまったかも・・・ <(ToT)>
お堅い話ばかりで申し訳ないです。本当は、もう少し実際のライブラリの中を歩きながら解説をしようとも思ったのですが、そちらの進捗が芳しくないですので、出来上がりの頃に改めて記事を書かせていただければと。

あ、話は変わるのですが、こちらばかりじゃなく、本体である Prigも早いところ形にしたいところだったりします。5 年近く同じテーマをやっていてなかなかモノが出てこないのは切ないですし。メモリ周りやビルドの問題も大方片付いたはずですので、近いうちに IL 打ち込む作業に入れるはず。来年こそは、自分を含めた開発者の方の心躍らせられるようなモノ、出せるようにがんばるです。 (`・ω・ )ゝ

さあ、いよいよ明日は大トリ、@mutaguchiさんです。よろしくお願いします!

Viewing all 46 articles
Browse latest View live