d.sunnyone.org
sunnyone.org

ページ

ラベル fb の投稿を表示しています。 すべての投稿を表示
ラベル fb の投稿を表示しています。 すべての投稿を表示

2014-05-11

MSILでわかるC# のラムダ式

そういえばLINQ to Objectsでコードを書いたときに裏でどういう動きになっているのかなぁ、とILでイメージができなかったので読んでみたら、単にC#のラムダ式がどうなっているかという話だけだったのでまとめる。

長いのでサマリ:
  • ラムダ式を使うと、基本的に入れ子クラスが作られ、ラムダ式が使う変数をとっておかれる。
  • ラムダ式の中身に書いたものは、基本的に入れ子クラスのメソッドとして定義される。
  • 入れ子クラスは不要なときには作られない。

評価スタックでピンとこない方は「C#でHelloWorldプログラムを作成する」を読んでおくのをおすすめする。

今回の説明用のサンプルコードはこちら。「.Count(x => x == val)」がどうなっていくのか、というお話。
namespace ConsoleApplicationLinq
{
    class Program
    {
        static void Main(string[] args)
        {
            int val = 100;
            var format = "Count of {0}: {1}";
            
            var array = new int[0];
            var count = array.Count(x => x == val);

            System.Console.WriteLine(format, val, count);
        }
    }
}

サイズ0のint配列に100がいくつあるか調べてWriteするというコード(0に決まっている)。説明の都合、順番などが不自然な感じになっている。

このソースをILに逆アセンブルした結果がこちら。
https://gist.github.com/sunnyone/0d3ed8285b5d91495399
別ウィンドウで開きながら説明を見るといいかも。

さて、これからILを見ていく。書いた部分がコンパイルされているであろうMainメソッドの実装を見ようとすると、そのMainメソッドの前に見知らぬ「<>c__DisplayClass1」という入れ子クラスが作られていることがわかる。

そのクラスの内容はこうなっている。

.class auto ansi sealed nested private beforefieldinit '<>c__DisplayClass1'
         extends [mscorlib]System.Object
  {
    (CompilerGeneratedAttributeの部分は省略)
    
    .field public int32 val // valフィールド
    
    (ここにコンストラクタの定義があるが省略)
    
    // <Main>b__0メソッドの定義
    .method public hidebysig instance bool 
            '<Main>b__0'(int32 x) cil managed
    {
      .maxstack  8
      
      // xをロード(評価スタックにpush)
      IL_0000:  ldarg.1
      
      // valフィールドをロード
      IL_0001:  ldarg.0
      IL_0002:  ldfld      int32 ConsoleApplicationLinq.Program/'<>c__DisplayClass1'::val
      
      // 二つの値=xとvalの値を比較し、同じなら1・異なるときは0
      IL_0007:  ceq
      
      // return
      IL_0009:  ret
    } // end of method '<>c__DisplayClass1'::'<Main>b__0'

  } // end of class '<>c__DisplayClass1'

このクラスは要は「x => x == val」の部分を<Main>b__0というメソッドに実装し、加えてvalフィールドを持っている。

次に本体のMainメソッド。Mainメソッドはこの入れ子クラスを活用して動作する。

まずメソッドとローカル変数の定義。format, array, countのほかに、先ほどの<>c__DisplayClass1が「CS$<>8__locals2」として用意されているのがわかる。valはないことに注意。
.method private hidebysig static void  Main(string[] args) cil managed
  {
    .entrypoint
    .maxstack  3

    .locals init ([0] string format,
             [1] int32[] 'array',
             [2] int32 count,
             [3] class ConsoleApplicationLinq.Program/'<>c__DisplayClass1' 'CS$<>8__locals2')

ここから処理開始だが、記述したC#コードに対応する部分に先立って、<>c__DisplayClass1がnewされ、ローカル変数に入る。
// <>c__DisplayClass1クラスのインスタンスを生成してローカル変数3番目にセット
    IL_0000:  newobj     instance void ConsoleApplicationLinq.Program/'<>c__DisplayClass1'::.ctor()
    IL_0005:  stloc.3

次に「int val = 100;」の部分。ここがポイント。
C#コード上ではローカル変数に見えているが、実際には内部の<>c__DisplayClass1クラスのフィールドになっている。
入れ子になったクラスにあるラムダ式の実体がこの変数を使うためにフィールドに入れている。
// CS$<>8__locals2と「100」をロードして、CS$<>8__locals2のvalにセット
    IL_0006:  ldloc.3
    IL_0007:  ldc.i4.s   100 
    IL_0009:  stfld      int32 ConsoleApplicationLinq.Program/'<>c__DisplayClass1'::val

次に「var format = "Count of {0}: {1}";」の部分。このように、ラムダ式と関係ない部分はふつうのローカル変数になる。
// 説明省略
    IL_000e:  ldstr      "Count of {0}: {1}"
    IL_0013:  stloc.0

次に「var array = new int[0];」だが、ここもローカル変数に入れるだけ。
// 説明省略
    IL_0014:  ldc.i4.0
    IL_0015:  newarr     [mscorlib]System.Int32
    IL_001a:  stloc.1

次に実際のラムダ式が登場する「var count = array.Count(x => x == val);」の部分。
先ほどvalのために生成したDisplayClassのメソッドを使ってFuncオブジェクトを生成し、Countメソッドに渡している。

// ローカル変数1番目:arrayをロード
    IL_001b:  ldloc.1
    
    // <Main>b__0メソッドのポインタをロード
    IL_001c:  ldloc.3
    IL_001d:  ldftn      instance bool ConsoleApplicationLinq.Program/'<>c__DisplayClass1'::'<Main>b__0'(int32)
    
    // <Main>b__0メソッドのポインタを使って、Funcオブジェクトを生成
    IL_0023:  newobj     instance void class [mscorlib]System.Func`2::.ctor(object,
                                                                                        native int)
                                                                                        
    // System.Linq.Enumerable::CountメソッドにarrayとFuncオブジェクトを渡す
    IL_0028:  call       int32 [System.Core]System.Linq.Enumerable::Count(class [mscorlib]System.Collections.Generic.IEnumerable`1,
                                                                                 class [mscorlib]System.Func`2)

    // ローカル変数2番目にセット
    IL_002d:  stloc.2

最後に「System.Console.WriteLine(format, val, count);」の部分。
ポイントはvalを使うのにラムダ式用の<>c__DisplayClass1を使っているところ。
// ローカル変数0番目: formatをロード
    IL_002e:  ldloc.0
    
    // ローカル変数3番目: CS$<>8__locals2のvalフィールドをロード、intなのでboxingする
    IL_002f:  ldloc.3
    IL_0030:  ldfld      int32 ConsoleApplicationLinq.Program/'<>c__DisplayClass1'::val
    IL_0035:  box        [mscorlib]System.Int32
    
    // ローカル変数2番目: countをロード, boxing
    IL_003a:  ldloc.2
    IL_003b:  box        [mscorlib]System.Int32
    
    // WriteLine
    IL_0040:  call       void [mscorlib]System.Console::WriteLine(string,
                                                                  object,
                                                                  object)
    IL_0045:  ret
  } // end of method Program::Main

このように、ラムダ式は、必要な変数を入れ子クラスのインスタンスにとっておいて、ラムダ式に書いた内容のメソッドが実行される、という形で実装されている。
しかし、ラムダ式があれば必ず入れ子クラスが作られるかというとそうではなく、変数をとっておく必要がない場合、違う形にコンパイルされる。

たとえば、先のコードのint val = 100;にconstをつけてconst int val = 100;にするだけで、ILはこうなってしまう。

.class private auto ansi beforefieldinit ConsoleApplicationLinq.Program
       extends [mscorlib]System.Object
{
  // ProgramクラスそのものにFuncのフィールドが用意される
  .field private static class [mscorlib]System.Func`2<int32,bool> 'CS$<>9__CachedAnonymousMethodDelegate1'
  
  (CompilerGeneratedAttributeの部分は省略)
  
  .method private hidebysig static void  Main(string[] args) cil managed
  {
    // ローカル変数の定義などなど
    .entrypoint
    .maxstack  3
    .locals init ([0] string format,
             [1] int32[] 'array',
             [2] int32 count)
    IL_0000:  ldstr      "Count of {0}: {1}"
    IL_0005:  stloc.0
    IL_0006:  ldc.i4.0
    IL_0007:  newarr     [mscorlib]System.Int32
    IL_000c:  stloc.1
    IL_000d:  ldloc.1
    
    // CS$<>9__CachedAnonymousMethodDelegate1をロードして、存在すればこの先の処理まで飛ばす
    IL_000e:  ldsfld     class [mscorlib]System.Func`2 ConsoleApplicationLinq.Program::'CS$<>9__CachedAnonymousMethodDelegate1'
    IL_0013:  brtrue.s   IL_0026

    // なければProgramクラスに定義された<Main>b__0メソッド(「x => x == val」の実装)を使ってFuncオブジェクトを作る
    IL_0015:  ldnull
    IL_0016:  ldftn      bool ConsoleApplicationLinq.Program::'<Main>b__0'(int32)
    IL_001c:  newobj     instance void class [mscorlib]System.Func`2::.ctor(object,
                                                                                        native int)
    // 作ったらフィールドに格納して、ロードしておく
    IL_0021:  stsfld     class [mscorlib]System.Func`2 ConsoleApplicationLinq.Program::'CS$<>9__CachedAnonymousMethodDelegate1'
    IL_0026:  ldsfld     class [mscorlib]System.Func`2 ConsoleApplicationLinq.Program::'CS$<>9__CachedAnonymousMethodDelegate1'
    
    // 以下同じ
    IL_002b:  call       int32 [System.Core]System.Linq.Enumerable::Count(class [mscorlib]System.Collections.Generic.IEnumerable`1,
                                                                                 class [mscorlib]System.Func`2)
    IL_0030:  stloc.2
    IL_0031:  ldloc.0
    IL_0032:  ldc.i4.s   100
    IL_0034:  box        [mscorlib]System.Int32
    IL_0039:  ldloc.2
    IL_003a:  box        [mscorlib]System.Int32
    IL_003f:  call       void [mscorlib]System.Console::WriteLine(string,
                                                                  object,
                                                                  object)
    IL_0044:  ret
  } // end of method Program::Main

  (コンストラクタの定義は省略)
  
  .method private hidebysig static bool  '<Main>b__0'(int32 x) cil managed
  {
    (CompilerGeneratedAttributeの部分は省略)
    
    .maxstack  8
    IL_0000:  ldarg.0
    IL_0001:  ldc.i4.s   100 // 「100」はここに埋め込まれるので、フィールドから取る必要がない
    IL_0003:  ceq
    IL_0005:  ret
  } // end of method Program::'<Main>b__0'

} // end of class ConsoleApplicationLinq.Program

こっちのコードだとご丁寧にもFuncオブジェクトをstaticフィールドにキャッシュしている。

ちなみに、valをローカル変数ではなくフィールドに持つようなクラスを作った場合も上述のconstの形に近くなり、入れ子クラスは作られない(Funcオブジェクトのキャッシュはしなくなる)。

まぁ、この差異はプログラム全体からしたらたいしたことはないと思うので、書くときに意識することはないと思うが、知っておいても悪くない…かな?
(一応、array.Count(x => x == val)を10000000回実行したら、valがconst/フィールド/ローカル変数それぞれの場合でStopwatchクラス読みで240ms/240ms/300msだった)

こうしてILコードを見ていると、ラムダ式がどういうものなのかしっくりきたのだけど、みなさまはどうだろうか?

2013-09-14

LibreOffice Calcの選択セルをMediaWiki表形式に変換するBasicマクロを作った

MediaWikiで表を書くとき、最初に作るときにフォーマットを思い出すのが辛いので、LibreOffice Calcの選択セルをMediaWikiの表形式に変換するマクロを書いてみた。ずっと書こうと思っていたのだけど、ようやく書けた。LibreOfficeにしておけば、気兼ねなくインストールして使えるので。



使い方は簡単で、選択してこのマクロを実行するだけ。そうするとメッセージボックスが出てくるので、このボックスはテキスト選択可能なのでコピペすればOK。ツールバーの空き部分にでもボタンを貼っておけばいいと思う。MediaWikiの表は、もっと高機能だけど、ひとまずプレーンな形に整形するだけ。

これがマクロ。

全然エレガントじゃない。

書くときのポイント

  • LibreOfficeについているマクロエディタは補完もないので、最初はNotepad++でVBモードで書くのがおすすめ。

参考にしたもの

辛かったこと

  • 流儀がわからない
    • プロパティ?の参照は .Abc = "ABC" だったり、setAbc("ABC") だったり書いてある場所によってぶれぶれ。変数名の命名はoから?とか。まぁ最初は動けばいいんだろうけど、結局すっきり書けなかった。
  • クリップボードがめちゃくちゃつらい。Win32 APIプログラミングですかっていう低レベルさ。
 プログラミングを始めたときのようなもどかしさを味わうことができた。



2013-07-28

「仕組み」の名前の考え方

名前に関する記事のタイトルがいまいちなのはさすがによくないので、タイトルを直した(2013/7/29)
---

最近ちょっと近くで話題になっていた「名前」の考えていきかたについて整理してみた。

ブランド名のように、独自性を持たせることを目的とするのではなく、後で呼びやすく混乱しないための名前付け。主に「それ」を知る人間同士がその物を識別するために使うための名前のガイドライン。「わかりやすい」「混乱しにくい」名前をつけるにはどう考えていけばいいか、というお話。

自分の立ち位置的に、ソフトウェア設計の視点が強くなってしまうが、仕組みを作る立場であれば似たような話になると思う。

経験がたまれば、更新するかも。

1) 文をそのまま使う

「これは何ですか?(何をするものですか?何をすることですか?)」と聞かれたと想定して、それで出てきた説明文をそのまま呼び名にすることを検討する。「何ですか?」の回答そのものなので、意図が伝わりやすい。長くなりがちだが、明快な説明ができる人間の命名であれば、短いことを上回るメリットがあることが多い。

確認ポイント

使う頻度は高くないか?
使う頻度が少ない(例えば、個別の機能のいちモジュール名だったり、年一回の事務処理の細かい手順のひとつだったり)のであれば、意図が伝わりやすいことのメリットが大きい。 しかし、毎日呼び合うような名前だと、短い名前をつけることを検討したあと、決定したほうがよい。 なぜかというと、ただ呼びやすいだけのあまり考えられていない(混乱しやすい・わかりにくい)名前で呼ばれてしまいやすくなるから。
一部の単語だけピックアップして呼び続けられたりしないか?ピックアップされても混乱はないか?
ピックアップしても区別できないから、本当に汎用な言葉がピックアップされることは少ない。例えば「顧客情報データ」を「データ」と略して呼び続けることは少ない。 しかし、文脈を共有すると選んで呼びやすくなってしまう語がある。例えば「クライアント」とか。 混乱がなければそれでもいいのだけど、混乱することがすでに想定できるのであれば、別の名前をつけてあげるのがよい。
そこから頭字語・略語はできるか?
これでつけてみた名前で、頭文字を取ったらいい感じに呼べる名前だったとか、略したら呼びやすかったりしたら、最初から略はこうです、という形で 明示しておくと混乱がない。一方で、その略語が他と同じ場合、かえって混乱を招くので、そもそも略していない状態でも他の呼び名を検討する。

2) 同じジャンルの意図することそのものの単語を使う

1) のそのまま使うパターンにマッチせず、意図した内容が既に一般的な名前として決まっている場合(「一般的に決まった名前」は以後、「定義された名前」とする)は、それを使うことを検討する。ずばりそのものということなので、ほとんどの場合良いが、安易に使うと大混乱を招くケースがある。

確認ポイント

元の語と明白に異なる意図を持っていないか?
新しくつける名前側が従で、定義された名前のほうが先に来ているようなケースでは、それが前提である以上、異なる意図は存在しにくいので、この名前付けパターンは良い選択である。 例えば、定義された名前が何かの規格で、その規格に乗ることこそ意図されたような場合であれば、それを名前につけるのは、受け取る側の意識付けの意味としても、非常に有効である。 しかし、定義された名前が従の場合、注意が必要である。「これこれはAで、あれそれはB…これってXYZって名前ついてるじゃん!」というケース。 こういった場合、一部の重要な部分が異なる場合がある。例えば、「定義された名前ではそれそれはCなんだけど、自分が考えるのはそれそれはDなんだよね…」といった感じ。 このようなことが名前付けの段階で明らかに分かっている場合は、その名前は使うべきではないことが多い。 なぜかというと、聞いた側は、名前が既に定義されているが故、その定義側の意図のほうを名前に感じてしまうから。 「俺のXYZはそうじゃないんだ!」といっても、最初はいいかもしれないが、よっぽどの強さがないと、元の語の意図を書き換えるのは難しいので、どんどんその意図は薄れて行ってしまう。
元の語に付加したい強い意図は持っていないか?意図を狭めたくはないか?
定義された名前は確かにずばりなんだけれど、もっと範囲を狭めておきたいという意図がある場合は、言葉を付加するなり、別の名前付けをするなりを検討すべき。 そうしておけば、広すぎて何を言っているかわからないという事態を避けられる。

3) 別ジャンルの意図することと関係のある単語を使う

文そのものでも、既存の言葉もない場合は、別ジャンルの近そうな言葉がないかを考える。メタファー/こじつけの世界。
一回目は聞いてもわからないが、わからない故、確認が必要となる。ゆえに、混乱を招きにくい。比喩になっているので、一回認識してしまえば、覚えやすい。

確認ポイント

選んだ言葉から受ける意図が、他の一面と衝突しないか?
単語の持つ意味は一面ではないことがあるので、ある面を見て比喩を選んでも、別の面で衝突している可能性がある。 例えば、固いイメージを与えたいからといって「フライパン」と名付けたとしても、冷たいという意図を併せ持つものだったりすると、「フライパン」の持つ熱せられそうなイメージと衝突するので、混乱を招く。
元のジャンルでその言葉は別の意味を持っていたりしないか?
比喩として使われていたりなどで、すでに使われていないか。強い意図を持ってジャンルの壁を越えてきているので、うっかりこの ケースで意図がバッティングすると、大混乱を招く。

4) 別ジャンルの全く関係ない単語を使う

3)に近いが、名前付けに意図がないので、さらにぱっと見/ぱっと聞きでわかりにくくなるが、混乱も小さくなる。
数が必要な場合に選ぶとよい。

5) 新単語を作る

どうしても困る場合は、新しい言葉を作ってしまうのもひとつ(文を略語化するのもこのパターンの一つなんだけど)。

決めた後の確認

似たような意図の語がすでに用意されていないか?それとの違いは何か?
最初にやるべきだけど、名前をつけることで明確になることもあるので、再度確認する。
似たような単語が既に使われていないか?
別の意図なのに、同じあるいは同じような用語が使われていないか確認する。

2013-04-14

WinDbgとSOS拡張でVSを使わずに.NETアプリをデバッグ - 異常終了時の調査

.NET Frameworkには「SOS デバッガー拡張」という機能があり、デバッガからCLRの状態を追いかけることができる。これを使うと、重いVisual Studioを使わなくても、WinDbgで.NET Frameworkのアプリケーションをデバッグすることができる。今回は、.NETアプリが異常終了したときにその状態を調べるという方法について。

この役に立たないダイアログも、価値のあるものになるはず。


設定編

1. WinDbgをインストールする

今回はWinDbgを使うことにするので、WinDbgをインストールする。WinDbgをインストールするには「Windows 用デバッグ ツールのダウンロードとインストール」にある通り、WDKあるいはWindows SDKをインストールすればついてくる。途中でコンポーネント選択の画面があるので、デバッガのみ選べば、他のコンポーネントをインストールしないことも可能。

2. 自動デバッガの設定をする

上の「役に立たないダイアログ」の「プログラムをデバッグします」で起動するプログラムを変更する。やりかたは「Configuring Automatic Debugging」に書いてある通り、レジストリキー「HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug」にある「Debugger」文字列値を追加/変更し、以下のようにする。
"C:\Program Files\Windows Kits\8.0\Debuggers\x86\windbg.exe" -p %ld -e %ld -g

x64版Windowsの場合は、SOS拡張を利用するには、実行するアプリケーションと同じアーキテクチャのwindbg.exeを使う必要があるので、上記のキーはx64版のwindbg.exeに向ける。
"C:\Program Files (x86)\Windows Kits\8.0\Debuggers\x64\windbg.exe" -p %ld -e %ld -g

x86版のアプリを起動したとき向けに、WOW64側のレジストリキー「HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows NT\CurrentVersion\AeDebug」の「Debugger」文字列値をx86版windbg.exeに向けておく。
"C:\Program Files (x86)\Windows Kits\8.0\Debuggers\x64\windbg.exe" -p %ld -e %ld -g

一応、上記のパスにインストールされていた場合(=Windows8 SDKでWinDbgをデフォルト設定でインストールした場合)に使えるregファイルを置いておいた。
x64用 / x86用

Visual Studioがインストールされている場合、Visual Studioのデバッガを使うようすでに構成されているため、注意が必要(上記の設定をするとこのダイアログからVisual Studioが使えなくなる)。

これで設定は完了。

実行編

MSBuild Launcher v0.1.1には、「存在しないエディタが設定されていると、異常終了する」というバグがあるので、これを例にする。なお、ポイントとなりそうな部分は勝手に太字にしてある。

基本的には、「SOS.dll (SOS デバッガー拡張)」を見てコマンドを選ぶことになる。

1. 異常終了+デバッガ起動

MSBuild Launcherの「Settings」ボタンをクリックし、「Editor」テキストボックスに存在しないパスを入力する。その後、適当なmsbuildファイルを開き、「Edit」ボタンを押すと、異常終了し、上の役に立たないダイアログが出てくるので、「プログラムをデバッグします」をクリックすると、WinDbgが立ちあがる。ワークスペースを保存するか聞かれるので、Noを選ぶと、以下の画面になる。


2. SOS拡張のロード

上記の状態になったら以下の通り入力する。
0:000> .loadby sos clr
これは「clr.dllの横にあるsos.dllをロードする」という意味。今回のケースではすでにclr.dllがロードされているので、特にメッセージなく終了するはず。なお、「clr」とするのは.NET Framework 4の場合で、.NET Frameworkの2.0/3.xの場合「clr」は「mscorwks」とする。

今回のケースとはずれるが、ロードされていない場合、例えば起動直後の場合、
0:000> sxe ld clr
としてclrのロードで止めるよう設定して
0:000> g
で進めたあと「.loadby~」を行うか、「.load」で直接DLLの場所を指定してもOK(この場合、.NETのバージョンやアーキテクチャに注意する)。
0:000> .load C:\Windows\Microsoft.NET\Framework64\v4.0.30319\SOS.dll

これで、SOS拡張がロードされた。

3. Exceptionの内容を表示する(!pe)

「CLR exception - code e0434352」が出ている通り、Exceptionが発生しているので、最後のExceptionの内容を表示する!pe (!PrintException)を実行する。

0:000> !pe
PDB symbol for clr.dll not loaded
Exception object: 0000000003078d10
Exception type:   System.ComponentModel.Win32Exception
Message:          指定されたファイルが見つかりません。
InnerException:   
StackTrace (generated):
    SP               IP               Function
    00000000001BD130 000007FEEB0CCBF3 System_ni!System.Diagnostics.Process.StartWithShellExecuteEx(System.Diagnostics.ProcessStartInfo)+0x4a3
    00000000001BD220 000007FEEB0CD03C System_ni!System.Diagnostics.Process.Start(System.Diagnostics.ProcessStartInfo)+0x3c
    00000000001BD260 000007FEE9BD1C51 PresentationCore_ni!System.Windows.EventRoute.InvokeHandlersImpl(System.Object, System.Windows.RoutedEventArgs, Boolean)+0x271
    00000000001BD490 000007FEE9BB8BBC PresentationCore_ni!System.Windows.UIElement.RaiseEventImpl(System.Windows.DependencyObject, System.Windows.RoutedEventArgs)+0x15c
    00000000001BD520 000007FEE91EC26D PresentationFramework_ni!System.Windows.Controls.Button.OnClick()+0xad
    00000000001BD590 000007FEE914B868 PresentationFramework_ni!System.Windows.Controls.Primitives.ButtonBase.OnMouseLeftButtonUp(System.Windows.Input.MouseButtonEventArgs)+0x148
(中略)
    00000000001BE9D0 000007FEE85486DF PresentationFramework_ni!System.Windows.Application.RunInternal(System.Windows.Window)+0x12f
    00000000001BEA40 000007FEE8547CEB PresentationFramework_ni!System.Windows.Application.Run()+0xbb
    00000000001BEA90 000007FF0015024F MsbuildLauncher!MsbuildLauncher.App.Main()+0x12f

StackTraceString: 
HResult: 80004005

これでもう「System.Diagnostics.Process.StartWithShellExecuteEx」を呼んだ結果「System.ComponentModel.Win32Exception」が発生し「指定されたファイルが見つかりません。」というメッセージが出ていることがわかった。MSBuild Launcherにおいて「System.Windows.Controls.Button.OnClick」から「System.Diagnostics.Process.Start」を呼ぶところなんて限られてくるので、もうわかったも同然なのだが、もう少し調べてみる。

4. スタックのオブジェクトを一覧する(!dso)

今回の場合「InnerException」がnoneなので、そこは調べられない。SOS拡張では、!dso (!DumpStackObjects)で、スタックのオブジェクトを一覧できるので、表示してみる。
0:000> !dso
OS Thread Id: 0x1208 (0)
RSP/REG          Object           Name
r15              0000000002e69448 System.Windows.Input.NotifyInputEventArgs
00000000001BCE68 0000000003078d10 System.ComponentModel.Win32Exception
00000000001BCF08 0000000003078d10 System.ComponentModel.Win32Exception
00000000001BCF50 0000000003074ca0 System.Windows.EventRoute
00000000001BCF68 0000000003078d10 System.ComponentModel.Win32Exception
00000000001BCF80 0000000003078d10 System.ComponentModel.Win32Exception
00000000001BCF98 0000000003078d10 System.ComponentModel.Win32Exception
00000000001BCFA0 0000000003078d10 System.ComponentModel.Win32Exception
00000000001BD030 0000000003074ca0 System.Windows.EventRoute
00000000001BD048 0000000003078db0 System.Text.StringBuilder
00000000001BD080 0000000003078d10 System.ComponentModel.Win32Exception
00000000001BD090 0000000003078d10 System.ComponentModel.Win32Exception
00000000001BD0B0 0000000003074ca0 System.Windows.EventRoute
00000000001BD0B8 0000000002e69448 System.Windows.Input.NotifyInputEventArgs
00000000001BD120 0000000003078c40 Microsoft.Win32.NativeMethods+ShellExecuteInfo
00000000001BD130 0000000003078d10 System.ComponentModel.Win32Exception
00000000001BD168 0000000003078360 System.Windows.RoutedEventArgs
00000000001BD1D0 0000000003078c40 Microsoft.Win32.NativeMethods+ShellExecuteInfo
00000000001BD1F8 0000000003078860 System.Diagnostics.ProcessStartInfo
00000000001BD200 0000000003078360 System.Windows.RoutedEventArgs
00000000001BD210 0000000003078b28 System.Diagnostics.Process
00000000001BD220 0000000003078b28 System.Diagnostics.Process
(略)

太字の 「System.Diagnostics.ProcessStartInfo」オブジェクトを見ればもうすこしわかりそうなので、表示してみることにする。

5. オブジェクトの内容を表示する(!do)

オブジェクトの内容を表示するには、!do (!DumpObj)コマンドを使う。引数には、先程の「System.Diagnostics.ProcessStartInfo」のオブジェクトアドレスを指定する。

0:000> !do 0000000003078860
Name:        System.Diagnostics.ProcessStartInfo
MethodTable: 000007feeab807a0
EEClass:     000007feea86a628
Size:        128(0x80) bytes
File:        C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System\v4.0_4.0.0.0__b77a5c561934e089\System.dll
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
000007feeb866738  4002fc9        8        System.String  0 instance 0000000003078808 fileName
000007feeb866738  4002fca       10        System.String  0 instance 0000000003078aa8 arguments
000007feeb866738  4002fcb       18        System.String  0 instance 0000000000000000 directory
000007feeb866738  4002fcc       20        System.String  0 instance 0000000000000000 verb
000007feeabb2ae8  4002fcd       68         System.Int32  1 instance                0 windowStyle
000007feeb86d450  4002fce       6c       System.Boolean  1 instance                0 errorDialog
000007feeb873318  4002fcf       60        System.IntPtr  1 instance                0 errorDialogParentHandle
000007feeb86d450  4002fd0       6d       System.Boolean  1 instance                1 useShellExecute
000007feeb866738  4002fd1       28        System.String  0 instance 0000000000000000 userName
000007feeb866738  4002fd2       30        System.String  0 instance 0000000000000000 domain
000007feeb87e6d0  4002fd3       38 ...rity.SecureString  0 instance 0000000000000000 password
000007feeb86d450  4002fd4       6e       System.Boolean  1 instance                0 loadUserProfile
000007feeb86d450  4002fd5       6f       System.Boolean  1 instance                0 redirectStandardInput
000007feeb86d450  4002fd6       70       System.Boolean  1 instance                0 redirectStandardOutput
000007feeb86d450  4002fd7       71       System.Boolean  1 instance                0 redirectStandardError
000007feeb873738  4002fd8       40 System.Text.Encoding  0 instance 0000000000000000 standardOutputEncoding
000007feeb873738  4002fd9       48 System.Text.Encoding  0 instance 0000000000000000 standardErrorEncoding
000007feeb86d450  4002fda       72       System.Boolean  1 instance                0 createNoWindow
000007feeb887510  4002fdb       50 System.WeakReference  0 instance 0000000000000000 weakParentProcess
000007feeab80948  4002fdc       58 ....StringDictionary  0 instance 0000000000000000 environmentVariables

「fileName」が使えそうなので、さらに表示してみる。

0:000> !do 0000000003078808
Name:        System.String
MethodTable: 000007feeb866738
EEClass:     000007feeb3eed68
Size:        86(0x56) bytes
File:        C:\Windows\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
String:      C:\Windows\invalid-notepad.exe
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
000007feeb86c620  4000103        8         System.Int32  1 instance               30 m_stringLength
000007feeb86b160  4000104        c          System.Char  1 instance               43 m_firstChar
000007feeb866738  4000105       10        System.String  0   shared           static Empty
                                 >> Domain:Value  00000000002dafa0:0000000002a11420 <<

これで、「C:\Windows\invalid-notepad.exe」という文字列がfileNameに入っていることがわかった。これでは実行できそうもない。

---
と、このような感じで、異常終了時の例外情報を調べるくらいならわりと簡単にできる。.NETプログラマなら覚えておくのがおすすめ。

本当は、AppDomainの中を旅するだけの機能もついているのだけど、それはまた機会があれば。

2013-03-12

LibreOffice用カラーテーブルを作った

[2014/12/14追記] LibreOffice 4.2以降では、12色になったことによってこの8色版はまだらになってしまいます。12色対応版をどうぞ

LibreOfficeは、もうちょっとした表や図を作るには十分なのだけど、デフォルトだと色が選択しにくい。いくつかプリセットがあるもの、どうもしっくりこなかったので、自分で作った。






ダウンロードはここから: https://github.com/sunnyone/locolor/raw/master/sunnyone.soc

作ってみたはいいものの、使い勝手がいいかどうかはまだわからない。

特徴

色相と明度/彩度の2軸で選択可能

デフォルトのカラーテーブルは一部そういう部分もあるものの、表で見たときけっこうまばらなので選びづらい。


順番に並んでいるカラーテーブルもあるのだけど、横8列であることを意識して作られていないので、まだらになってしまっている。

同じ色は縦に並んでいる(似通った濃さは横に並んでいる)

LibreOfficeは一番上のスクリーンショットの通り、2通りの色選択のUIがあり、リスト形式の場合は表の横の順に8つ出てくる。つまり、表の横1列が表示される。オフィスソフトで色を選択するときは、色相よりも先にトーン(濃さだとか)が決まっていることが多いので、同一トーンのものが一画面に出てくると選びやすい(はず)。

使い方

一時的に試してみたい場合は、適当に図を挿入して、右クリック→「領域...」でダイアログが出てくるので、「色」タブからフォルダのアイコンをクリックして出てくるファイル選択の画面でsunnyone.socを選べばOK。



デフォルトの設定にしてしまいたい場合は、LibreOfficeをいったん閉じたあと、configのディレクトリ(最近のWindowsならC:\Users\(Username)\AppData\Roaming\LibreOffice\4\user\config)のstandard.socをバックアップした後、入れ替えればOK。

作りかた

上の配色が気にいらない場合でも、このカラーテーブルの作りかたは参考になるかもしれないので、一応書いておく。

1. 色相を選択する

マンセル環から色をチョイスする。マンセル表色系の色相は、5色とその間の5色、さらに倍…という感じなので、8色はちょっとつらいんだけど、がんばって4色+補色を選んだ。


2. 彩度/明度を選択する

マンセル表色系では、色相によって最高彩度が違うのでしんどいのだけど、概ね横の三角形の形をしているので、三角形の頂点を決めたあと、間の点を機械的に選んだ(実際はマンセルの等色相面はいびつな形をしているので、本当はない色があるかもしれない)。



詳しくはレポジトリに置いてあるシートを参照。

3. Color::Model::Munsell::Utilを使ってマンセル値からRGB値に変換

マンセル表色系に対応しているライブラリはけっこう少ないのだけど、PerlのColor::Model::Munsell::Utilは対応していたので、これで変換した。
マンセル値は直接RGBにはできないので、テーブルでCIE-XYZに変換して、そこからsRGBに変換している(はず)。
出力はLibreOfficeで使っている簡単なXMLにするだけで、完了。

詳しくはリポジトリに置いてあるスクリプトを参照。

Q&A

なんでPCCSじゃないの?

色彩+トーン(彩度・明度)という考え方は、まさしくPCCS向けなのだけど、PCCSは(恐らく)物の色向けで、かつトーンの名前が人の感覚でつけられているので、RGBの世界に持ってくると、この手の世界になじみがない自分にはちょっと違和感があった。なので、類似した感じのマンセル表色系で色をチョイスした。

↑これが、「v2」というvトーンの赤、つまりvividなred (4R 4.5 / 14)をRGBに変換した色なんだけど、画面で見るとにぶく見える気がするのよねぇ。

機械的に計算するならHSVのほうがいいんじゃ?

できたあとに思った。実験してみてないのだけど、高明度の色は、HSVで機械的に出したほうがきれいな薄い色になったんじゃないかなーと思ったりもする。

が、マンセル値にしておけば、表記が人の理解できる範囲になると思うので、これはこれでいいかなと思っている。

なんで色を選ぶUIが2種類あるの?

ひとつだったらもう少し作りやすかったのに…

2013-01-08

ドラッグした範囲をキャプチャしてMediaWikiに上げるツール「ScreenToWiki」を作った

MediaWikiは画像のアップロード面倒だよねーという話があったのですが、「MediaWikiほどにもなればAPIついてるんじゃね?」と「WPFで全画面に透明ウィンドウ張れば、gyazo的範囲選択も別に難しくないよね」「画像保存もKiritoriMageの感じでやればいいよね」となったので、「Wikiにいきなりアップロードするツール作るの簡単じゃね?」ということで作ってみました。その名もScreenToWiki

ダウンロード

ダウンロードはこちらから。
例によって.NET Framework 4.0が必要です。

使い方

使い方は、実行すれば選択範囲の選択がでてくるので、ドラッグで選んだあと、WikiのURLとかユーザ名とかを入力すればアップロードできます。gyazoだと思えば概ねOKだと思います。ただ、エラーハンドリングは弱いので、なにかあるとあまりわかりません…。

パスワードの保存は、人が見えない程度にしか変えない(Base64するだけ)ので、まったくおすすめできないです。セキュリティはWikiごとの事情があると思うので、事情に応じておねがいします。

MediaWiki 1.16以降でないとuploadのAPIがないそうなので動きません。Ubuntu 12.04が1.15でしばらく悩みました。

その他

MediaWikiしか使えないのにToWikiはおかしいじゃないかって?今のところその通りです。

でも他のものにも対応できるようにMEFでクラスを取ってくるようにしているので、IImageUploaderを実装したクラスを作れば他のWikiにもアップロードできますよ! Wikiでなくてもおもしろいかも。

もともとGitHubであげようと思っていたのですが、Goodbye, Uploadsということで、アーカイブのアップロードがなくなってしまったので、オープンソースなWindows系ソフトだったらcodeplexかなぁということでCodeplexに載せてみました。ダウンロードが目立つのでユーザには使いやすいかも。

2012-12-15

PowerShellでApacheのログを集計する

この記事はPowerShell Advent Calendar 2012向けです。PowerShellはまだまだマスターには遠いですが、ありがたみがわかってきたので、書くことにしました。昨日の牟田口さんの記事を見るとAdd-TypeもPSObjectも使わずにPowerShellでクラスを作ってみろと言われているような気がしますが、誘惑を振り切りもともと想定していたネタです。

今日は、PowerShellを使ってApacheのログを集計してみます。Windowsを使っている人だったら、「bashとsedともげもげで十分じゃん」「Perl使うよ」と言わず、いいところがあるので見てください。特にログ集計でshもLLもわずらわしくなって、SQLiteやPostgreSQLにワンタイムのDBを作ってSQLでログ集計したことがある人におすすめです。

メリット

そもそもなにがうれしいの?という部分ですが、PowerShellには「パイプラインをオブジェクトが流れる」という特徴があり、これを利用するとshやPerlやRubyでの処理と比較して以下のようなメリットがあります。
  • (shで処理する場合と比較して)パーサがパイプにテキストではなく、オブジェクト群を流せるので、再利用が容易。きちんとパイプに流れてきたオブジェクトを処理するコマンド(コマンドレット)が用意されている。空白区切りテキストを見て、「うーん、User-Agent何番目かな…1, 2, 3...」と数えなくてよい。
  • (Perl等で処理する場合と比較して)sedやawk, sort等々をつなげて作っていくような考え方で、パイプに流して絞り込んでいく形で処理を記述できるので、試行錯誤で試していくのがラク。
要は、いいとこ取りというわけです。具体的にはこれから見ていきます。

対象ログ

今回対象とするのは、"combined"として定義されているタイプのログです。こんな感じです。


ひとつ正規表現を変えるだけなので、ちょっと違ってても大きな問題はないです。

パーサ(モジュール)の準備

まずは、ログのパーサを準備します。とはいっても、テキストにマッチしてオブジェクトをばんばん返す(流す)関数を書くだけです。
以下のスクリプトを(Win7/Vistaの場合)「C:\Users\(ユーザ名)\Documents\WindowsPowerShell\Modules\ApacheLogParser\ApacheLogParser.psm1」に配置します。



ここはあまりPowerShellっぽくないので、ポイントだけ書きます。
  • 関数名は動詞-名詞にする。使える動詞は「Approved Verbs for Windows PowerShell Commands」を参照。
  • 正規表現のマッチ結果である$matches ハッシュテーブルを活用する。
  • PowerShellの文字列の特殊文字のエスケープは`。 例: `"。
  • 正規表現では、名前つきキャプチャを活用する。 例: (?<Host>.*?)。こうすると、$matchesに指定した名前で入ってくる。
  • パイプラインには、ハッシュをPSObjectにして流す。その際は、「New-Object」の-Propertyを使うと便利。

returnを書いているので、値を返すにはそうしないといけなそうに見えますが、このケースは別に書かなくても平気です。どういうことかというと、値を返すコマンドレットを使ったら、|Out-Nullとか、>$null とかして捨てないと、どんどん「流れ」ていってしまうのです。shでコマンドを実行したらstdout/stderrに出力が出るようなものです。不要なものは捨てるようにします。

Pathを受けとるのではなく、ログ本文を受け取るようにして、この関数自体をパイプで使うのが「ぽい」ような気もしますが、物理ファイルを指定するケースが多そうなのでとりあえずこの形にしました。需要に応じて変更するのがいいと思います。Perlの<>や、RubyのARGFみたいにstdin or 引数のファイルからのInputというのが簡単にできるといいのですが。

作業開始前の準備

使う段になったら、powershellを起動後、「Import-Module」で先程のモジュールをロードします。
PS C:\temp> Import-Module ApacheLogParser

試しに、「Read-ApacheLog」にログを食べさせてみましょう。そのまま出すと出すぎるので、「Select-Object(エイリアス:select)」の-Lastオプションで数を減らしたものが以下です。


各行の「 : 」の左側に書かれたプロパティ名を使って、さまざまな操作をしていくことになります。


実践

先程のパーサを使って、具体的に集計をしてみます。

トータルのアクセス数

もっともシンプルなものとして、全てのアクセス数を数えてみましょう。ここでは、いろいろ数えてくれる「Measure-Object(エイリアス: measure)」を使います。

PS C:\temp> Read-ApacheLog .\access.log | measure


Count    : 281
Average  :
Sum      :
Maximum  :
Minimum  :
Property :
281アクセスあることがわかりました(テスト用に作ったので少なくてごめんなさい)

特定のパスのみのアクセス数

先の「Measure-Object」に加えて、与えた条件を満たすもののみを流す「Where-Object(エイリアス:where、?)」を使います。

PS C:\temp> Read-ApacheLog .\access.log | where { $_.Path -eq "/favicon.ico" } | measure


Count    : 2
Average  :
Sum      :
Maximum  :
Minimum  :
Property :
"/favicon.ico"へのアクセスは2回あるようです。

ステータス別アクセス数

ステータスコードで分類してみましょう。今度は指定したプロパティが同じ値を持つオブジェクトをグループ化する「Group-Object(エイリアス:group)」と、表示順を整えるために「Sort-Object(エイリアス:sort)」を使います。

PS C:\temp> Read-ApacheLog .\access.log | group Status | sort Name

Count Name                      Group
----- ----                      -----
  259 200                       {@{Time=2012/12/14 0:09:47; Host=127.0.0.1; Request=OPTIONS * HTTP/1.0; TimeString=1...
    1 301                       {@{Time=2012/12/14 0:17:35; Host=10.0.2.2; Request=GET /mediawiki/index.php HTTP/1.1...
    1 302                       {@{Time=2012/12/14 0:17:44; Host=10.0.2.2; Request=POST /mediawiki/index.php?title=M...
   11 304                       {@{Time=2012/12/14 0:17:41; Host=10.0.2.2; Request=GET /mediawiki/skins/common/share...
    9 404                       {@{Time=2012/12/14 0:14:26; Host=10.0.2.2; Request=GET /favicon.ico HTTP/1.1; TimeSt...
200が多いですが、3xxや4xxもちらほらあることがわかります。

トップディレクトリごとのアクセス数

今度はトップディレクトリごとのアクセス数を集計してみましょう。「Group-Object」に置換を行うブロックを渡します(パスのファイル名部分と、サブディレクトリ以下をカットします)。

PS C:\temp> Read-ApacheLog .\access.log | group { $_.Path -replace '[^/]*$','' -replace '^(/.+?)/.*','$1' } | sort -Descending Count

Count Name                      Group
----- ----                      -----
  207 /pukiwiki                 {@{Time=2012/12/14 0:23:12; Host=127.0.0.1; Request=GET /pukiwiki/ HTTP/1.1; TimeStr...
   58 /mediawiki                {@{Time=2012/12/14 0:14:30; Host=10.0.2.2; Request=GET /mediawiki/ HTTP/1.1; TimeStr...
   11                           {@{Time=2012/12/14 0:09:47; Host=127.0.0.1; Request=OPTIONS * HTTP/1.0; TimeString=1...
    4 /                         {@{Time=2012/12/14 0:14:26; Host=10.0.2.2; Request=GET / HTTP/1.1; TimeString=14/Dec...
    1 /trac                     {@{Time=2012/12/14 0:17:51; Host=10.0.2.2; Request=GET /trac/ HTTP/1.1; TimeString=1...

"/pukiwiki"にたくさんアクセスされているようです。

時間帯別アクセス数

パーサスクリプトの中で、時刻はSystem.DateTime型に変換しておいたので「Hour」プロパティが使えます。今までと同じように「Group-Object」を使えばOKです。

PS C:\temp> Read-ApacheLog .\access.log | group { $_.Time.Hour }

Count Name                      Group
----- ----                      -----
  227 0                         {@{Time=2012/12/14 0:09:47; Host=127.0.0.1; Request=OPTIONS * HTTP/1.0; TimeString=1...
   54 18                        {@{Time=2012/12/14 18:30:05; Host=127.0.0.1; Request=GET /pukiwiki/index.php?InterWi...

例がとてもよくないのですが、0時台にたくさんアクセスがあり、18時台に少しアクセスがあるようです。他の時間帯には使われていません。

日付/時間帯別アクセス数をCSV出力する

CSV出力することも可能です。「Export-Csv」コマンドレットを使います。
(日付+時間を作るには、若干トリッキーですがDateTime型の「Date」プロパティで日付部をもらい、「AddHours」メソッドを呼び出して時間を付加しています。)

PS C:\temp> Read-ApacheLog .\access.log | group { $_.Time.Date.AddHours($_.Time.Hour) } | select Name,Count | Export-Csv output.csv -Encoding default


平均レスポンスバイト数を調べる

combinedフォーマットには「%b:レスポンスのバイト数」があるので、レスポンスのサイズを集計することも可能です。「Where-Object」「Measure-Object」を使って、/pukiwiki以下の返答の平均バイト数を出してみましょう。

PS C:\temp> Read-ApacheLog .\access.log | where { $_.Path -match "/pukiwiki" } | measure -Property BytesSent -Average


Count    : 207
Average  : 3019.21256038647
Sum      :
Maximum  :
Minimum  :
Property : BytesSent

このようにひとつのログ形式に対して、パーサを1回作ってあげれば、あとは直感的に集計処理が可能になります。量的にどこまで耐えられるかは検証していないのですが、GUIな表計算ソフトでうんぬんするよりはさっくりできると思います。もちろんApacheのログでなくて、テキストのログは同じように処理することが可能ですので、しょっちゅうログを処理するような方は、試してみてはいかがでしょうか。

2012-12-03

ILGeneratorの「向こう側」でTypeやMethodInfoを得るために"ldtoken"命令と~FromHandleメソッドを使う

最初のころは黒魔術だったのにだんだん慣れてきちゃった。とはいえ、書いておかないと忘れかねないので書いておく。今回のテーマはタイトルの通りなので、タイトルを読んでわかる人には不要の話。

概要

ILGeneratorを使ってコードを書いているような状況では、ILGeneratorの「向こう側」(生成したILコードが動作している文脈のこと。ILを使ってコードを生成していると、文脈を区別したくなるので、勝手にこう呼んでいる)でTypeやMethodInfoなどのリフレクション用のオブジェクトが欲しくなるときがある。もちろん、Nameを使ってGetMethod等々を「向こう側」で呼ぶということも可能だが、「こっち側」(ILコードを生成している側の文脈)でType等々が識別できている状況では、もう少しいい方法がある。それは以下の通り。

  1. "ldtoken"命令と「メタデータ トークン」を使って、評価スタックにRuntimeHandleをpushする。
  2. 各種~FromHandleメソッドを使って、TypeやMethodInfoなどを得る。

実践 - Typeの場合

Typeの場合こんな感じ。
var typeObj = il.DeclareLocal(typeof(Type));   // ローカル変数の準備
il.Emit(OpCodes.Ldtoken, typeof(SampleClass)); // -- (1)
il.EmitCall(OpCodes.Call, typeof(Type).GetMethod("GetTypeFromHandle"), null); // -- (2)
il.Emit(OpCodes.Stloc, typeObj); // ローカル変数へのストア

(1) メタデータトークンからRuntimeTypeHandleを得る

まず、「メタデータ トークン」を使ってldtoken命令を実行し、RuntimeHandleを得る。
書式 / アセンブリ形式 / 説明
D0 < T > / ldtoken token / メタデータ トークンをそのランタイム表現に変換します。
(from OpCodes.Ldtoken フィールド)

しかし、メタデータ トークンとは何か?
メタデータは、ランタイム型 (クラス、値型、およびインターフェイス)、グローバル関数、グローバル変数などの抽象化の宣言情報です。
(from メタデータ トークン)
ということで、詳細は上記ページに書いてあるが、型やメソッドなんかのプリミティブな表現くらいに思っておけば大丈夫だと思う。

幸いなことに、ILGenerator.Emit(OpCodes.Ldtoken, xxx)なら、TypeやMethodInfoを渡せば裏でうまいことやってくれるので、あまり意識しなくていい。

このメタデータトークンを使って"ldtoken"を使うと、「RuntimeHandle」が得られる。またややこしいものがでてきたが、
内部メタデータ トークンを使用する型を表します。
(RuntimeTypeHandle 構造体)
ということで、メタデータトークンの構造体表現くらいだと思っておけばいいと思う。

(2) RuntimeTypeHandleからTypeオブジェクトを得る

RuntimeTypeHandleからTypeオブジェクトを得るには、Type.GetTypeFromHandle(RuntimeTypeHandle)を使う。

これでTypeが得られたので、あとは好きに使えばOK.
たとえば、Nameを表示したければこんな感じ。
// 試しに名前を出してみる
il.Emit(OpCodes.Ldloc, typeObj);
il.EmitCall(OpCodes.Callvirt, typeof(Type).GetProperty("Name").GetGetMethod(), null);
il.EmitCall(OpCodes.Call,
  typeof(System.Console).GetMethod("WriteLine", new Type[] { typeof(String) }),
 null);

このコードで生成したコードがこんな感じ。
ldtoken [HelloMetadata]HelloMetadata.SampleClass
call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
stloc.0
 
ldloc.0
callvirt instance string [mscorlib]System.Reflection.MemberInfo::get_Name()
call void [mscorlib]System.Console::WriteLine(string)

実践 - MethodInfoの場合

MethodInfoの場合はこんな感じ。
var methodInfoObj = il.DeclareLocal(typeof(MethodInfo));
il.Emit(OpCodes.Ldtoken, typeof(SampleClass).GetMethod("SampleMethod"));
il.EmitCall(OpCodes.Call, typeof(MethodBase).GetMethod("GetMethodFromHandle", new Type[] { typeof(RuntimeMethodHandle) } ), null);
il.Emit(OpCodes.Stloc, methodInfoObj);

概ねTypeの場合と同じだが、Methodの場合は二つ注意点がある。
  • GetMethodFromHandle()は、MethodInfo/ConstructorInfoの親であるMethodBaseに存在する。
  • GetMethodFromHandle()は、Typeが指定できるオーバーロードがあるので、メソッド名だけではGetMethod()できない。

名前を表示したければ、同じようにこんなかんじ。
// 試しに名前を出してみる
il.Emit(OpCodes.Ldloc, methodInfoObj);
il.EmitCall(OpCodes.Callvirt, typeof(MethodInfo).GetProperty("Name").GetGetMethod(), null);
il.EmitCall(OpCodes.Call,
   typeof(System.Console).GetMethod("WriteLine", new Type[] { typeof(String) }),
   null);

これで生成されたコードがこんな感じ。
ldtoken method void [HelloMetadata]HelloMetadata.SampleClass::SampleMethod()
call class [mscorlib]System.Reflection.MethodBase [mscorlib]System.Reflection.MethodBase::GetMethodFromHandle(valuetype [mscorlib]System.RuntimeMethodHandle)
stloc.1

ldloc.1
callvirt instance string [mscorlib]System.Reflection.MemberInfo::get_Name()
call void [mscorlib]System.Console::WriteLine(string)

その他

FieldInfoでは試してないけど、OpCodes.Ldlocの説明を見る限り、概ね同じ方法でいけると思う。
また、RuntimeMethodHandleについては、IL生成をしていない場面でもメソッドを識別するのに便利な場面があるかもしれない。

2012-12-02

自室の照明をスマートフォンからOn/Offする

寝たいんだけど、電気消しに行くのめんどくさくて寝れない。そんなことありませんか?今回はそんな人向けのソリューション。でも、うまく動かなくて勝手についたり、チカチカしちゃったりしても自己責任でお願いします。

ちなみに、おおかたの期待を裏切り、今回はハードウェア工作はナシ。既製品だけ。

仕組み

「電気を消したいだけなのに複雑だ」と言われたので絵にしてみた。


複雑だろうか?ちなみに、結構昔からある方法。

使った物

  1. 天井照明器具専用 リモコンスイッチOCR-04 (\1,500くらい)
  2. パソコン用学習リモコン PC-OP-RS1 (\4,500くらい)
  3. Debian GNU/Linuxの入った箱:今回は玄箱PRO (\不明:今ならGuruPlug (\20,000くらい)や、Raspberry Pi (\4,000くらい)がいいかも)
OCR-04以外は家にあったものなのでかかったのは1,500くらいだけ。

作り方

Step1. OCR-04を設置する

OCR-04はこんな形をしたもの。箱に書いてある通り、照明器具と天井の引っ掛けシーリングの間に受信部をセットするだけ。元々のスイッチはOnにしっぱなしにしないと使いものにならないので、ついてきたリモコンは、スイッチがあったところの隣に置いておく。

Step2. Linux boxとPC-OP-RS1をつなぐ

今回はNASとして稼動していた玄箱PROを使ってしまったが、USBがついたLinuxの箱ならなんでもいいはず。LinuxからPC-OP-RS1は、USBシリアルデバイスとして認識できるが、VendorID/ProductIDが登録されていないので、指示してあげる必要がある。コマンドは以下の通り。
# modprobe ftdi_sio vendor=0x0411 product=0x00b3
これで/dev/ttyUSBXができたらひとまずOK。再起動しても認識される&あとで使うための権限が調整できるよう、以下の設定を行う。

まずvid/pidの設定。

/etc/modprobe.d/remosta.conf作成
alias remosta ftdi_sio
options ftdi_sio vendor=0x0411 product=0x00b3

起動時にロードされるようにする。

/etc/modules追記
remosta

このデバイスが使える人のグループを作る。
# groupadd remosta

このデバイスを/dev/remostaXにし、グループを変更する。

/etc/udev/rules.d/92-remosta.rules作成(92なのは91でこのデバイスのグループ変更がされていたから)
KERNEL=="ttyUSB*", ACTION=="add", \
  ATTRS{idVendor}=="0411", ATTRS{idProduct}=="00b3", \
  MODE="0660", GROUP="remosta", NAME="%k", SYMLINK="remosta%n"
ここの設定は間違えるとOSが起動しなくなるから注意!(一回やってHDD抜き出した…)

再起動して、以下の状態になっていればOK。
$ ls -al /dev/remosta* /dev/ttyUSB0
lrwxrwxrwx 1 root root         7 Nov 23 20:58 /dev/remosta0 -> ttyUSB0
crw-rw---- 1 root remosta 188, 0 Dec  1 22:59 /dev/ttyUSB0

Step3. コマンドラインで送受信してみる

Rubyでスクリプトを書いたので、それを使う。

Ruby/serialportが必要なので、インストールしておく。
# apt-get install libserialport-ruby

あとはこのスクリプトをダウンロードして、以下の通りで動く。

受信
$ ruby remosta.rb recv
Receiving. →ここでPC-OP-RS1に対して、リモコンで送ってみる
fffff070000e03fc07f80f.... →受信した信号
Received.

送信
$ ruby remosta.rb send fffff070000e03fc07f80f....(信号)
Sending.
Sent.

これで「A」と書いてあるほうの黄色いシールがついてるLEDから信号が発信される。

Step3. Web UIをつける

凝るほどでもないので、レガシーにRuby/CGIでWebを作った。
https://github.com/sunnyone/lightremo

スクリプトを/var/www/lightremoに配置したら、以下の感じで設定する。
<Directory /var/www/lightremo>
            AddHandler cgi-script .cgi
            AllowOverride None
            Options +ExecCGI
        </Directory>
今回は自宅の無線LANでしか接続できないところに置いたから、CGIとしてはとくに認証もしていないけど、必要ならかける。

www-dataを"remosta"グループに所属させるのを忘れない。

Apacheの設定をリロードして、http://[SERVER]/lightremo/lightremo.cgiにアクセスすれば以下の画面がでてくるはず。


onやoffを押して実際に照明がOn/Offされたら成功!





2012-10-27

MSBuild Launcher 0.0.1をリリース

ここでちょっと触れたMSBuildの良いGUIが見つからなかったので、さくっと作った。

ダウンロードはこちらから。
https://github.com/sunnyone/MsbuildLauncher


MSBuildファイルを開くと、左側にターゲットのボタンができるので、クリックすれば実行される。
コマンドライン引数にも対応しているので、.msbuildを関連付けることも可能。

2012-10-25

C#の"ref"引数のMSIL的取り扱い(.NETと参照渡し)

C#でrefを使ったメソッドのMSIL的作り方/呼び出し方(参照渡しの方法)についてメモしておく。

サマリ

  • メソッドに渡す際は、アドレスを渡す。Cのポインタで言う&valのイメージ。
    • 引数のpushには ldloca命令を使う。
  • メソッド内では、アドレスの解決を行う。Cのポインタで言う*aのイメージ。
    • 取得にはldind命令シリーズ(ldobj)、設定にはstind命令シリーズ(stobj)を使う。
  • 型は、実際の型に&がついた「ref型」を利用する(例: System.Int32&)
    • Type.MakeByRefTypeでref型化、Type.GetElementTypeで非ref型化できる。
    • ref型かどうかは、Type.IsByRefで判断できる。

具体例として、参照を使うコードにありがちなSwapメソッドを作ってみる。

C#コード

void Swap(ref int x, ref int y) {
    int tmp;
    tmp = x;
    x = y;
    y = tmp;
}

void SampleFunc()
{
    int x = 10; int y = 20;
    Swap(ref x, ref y);
    System.Console.WriteLine("x, y: {0},{1}", x, y);
}

これをコンパイルしたものを逆アセンブルしたものが以下。
.method private hidebysig instance void  Swap(int32& x,
                                              int32& y) cil managed
{
  .maxstack  2
  .locals init ([0] int32 tmp)
  IL_0000:  nop
  IL_0001:  ldarg.1
  IL_0002:  ldind.i4
  IL_0003:  stloc.0
  IL_0004:  ldarg.1
  IL_0005:  ldarg.2
  IL_0006:  ldind.i4
  IL_0007:  stind.i4
  IL_0008:  ldarg.2
  IL_0009:  ldloc.0
  IL_000a:  stind.i4
  IL_000b:  ret
}

.method private hidebysig instance void  SampleFunc() cil managed
{
  .maxstack  3
  .locals init ([0] int32 x,
           [1] int32 y)
  IL_0000:  nop
  IL_0001:  ldc.i4.s   10
  IL_0003:  stloc.0
  IL_0004:  ldc.i4.s   20
  IL_0006:  stloc.1
  IL_0007:  ldarg.0
  IL_0008:  ldloca.s   x
  IL_000a:  ldloca.s   y
  IL_000c:  call       instance void ConsoleApplication1.Program::Swap(int32&,
                                                                       int32&)
  IL_0011:  nop
  IL_0012:  ldstr      "x, y: {0},{1}"
  IL_0017:  ldloc.0
  IL_0018:  box        [mscorlib]System.Int32
  IL_001d:  ldloc.1
  IL_001e:  box        [mscorlib]System.Int32
  IL_0023:  call       void [mscorlib]System.Console::WriteLine(string,
                                                                object,
                                                                object)
  IL_0028:  nop
  IL_0029:  ret
} // end of method Program::SampleFunc

呼ばれるSwapメソッド

適当に端折りながら、上から見ていく。
(評価スタックのイメージを忘れてしまった場合は、この絵を思い出してほしい。)

まずメソッドの定義。
.method private hidebysig instance void  Swap(int32& x,
                                              int32& y) cil managed
int32ではなく、int32&型が使われている。これは、
Type t = typeof(Int32).MakeByRefType();
てな感じで得られる。

そしてtmp = x;の部分。
IL_0001:  ldarg.1
  IL_0002:  ldind.i4
  IL_0003:  stloc.0
これは、
  1. 1番目の引数(ref x)を持ってくる
  2. 持ってきたアドレスからint32の値を持ってくる
  3. 0番目のローカル変数に格納する
というイメージである。

x = y;はいったん飛ばして、y = tmp;部分を見るとこう。
IL_0008:  ldarg.2
  IL_0009:  ldloc.0
  IL_000a:  stind.i4
これは、
  1. 2番目の引数(ref y)を持ってくる
  2. 0番目のローカル変数からint32の値を持ってくる
  3. 持ってきたアドレスに持ってきた値を入れる
というイメージである。

x = yは上記の組み合わせ。
IL_0004:  ldarg.1
  IL_0005:  ldarg.2
  IL_0006:  ldind.i4
  IL_0007:  stind.i4

ここでは、int32なので、ldind.i4/stind.i4だが、型によって使うべき命令は違う。ただし、
すべての ldind 命令は、対応している組み込み値クラスを指定する Ldobj 命令のショートカットです。
(OpCodes.Ldind_I4 フィールド)なので、ldobj [type]でもよい。

ldobj/stobjの例としては、ref int?を使うと、以下の感じで生成される。
IL_0008: stobj valuetype [mscorlib]System.Nullable`1

Swap()を呼ぶメソッド

こちらは実際に呼び出しているところだけ。
IL_0001:  ldc.i4.s   10
  IL_0003:  stloc.0
  IL_0004:  ldc.i4.s   20
  IL_0006:  stloc.1
  IL_0007:  ldarg.0
  IL_0008:  ldloca.s   x
  IL_000a:  ldloca.s   y
  IL_000c:  call       instance void ConsoleApplication1.Program::Swap(int32&,
                                                                       int32&)
10、20をそれぞれx、yに入れたあと、this、xのアドレス、yのアドレスの順に持ってきて、関数をコールしている。

リフレクションでの扱い

すでにあるメソッドのMethodInfoをGetMethod()で持ってきて、ParameterTypeを見ると、
&のついたref型で入っている。この元の型を得たいときは、こんな感じになる。
if (parameterType.IsByRef) {
  Type valueType = parameterType.GetElementType();
}
(Type.MakeByRefType メソッド)

2012-09-22

画像分割ツールKiritoriMage公開

画像分割ツールKiritoriMage version 0.0.1をリリースした。GUIアプリケーションのフリーソフトウェアの新規リリースは、もう10年ぶりくらいになる。

http://github.com/sunnyone/kiritorimage/
ダウンロード: https://github.com/downloads/sunnyone/kiritorimage/KiritoriMage-0.0.1.zip

どんなソフトウェアかというと、区切り線を指定して、その区切られた領域をクリックしていくことで、画像を分割していくツール。以下の画像を見てもらえれば、どんなアプリケーションかわかると思う。


たいていの画像処理ソフトは、1回の保存オペレーションでひとつの範囲しか保存できないのに対し、このツールは複数の範囲が保存できるのが特徴。ちなみに、mageと呼ぶほどすごいことはできない。

自分用なので、保存形式がJPEG固定だったり、保存フォルダは画像と同じフォルダのみ(実は裏技があるのだけど)だったりと、制限は多いのだけど、ひとまずは使えるようになったので公開。

MITライセンスにしているので、改変や再配布も自由。もし拡張とかあればpull requestしてもらえれば。