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

2018年9月10日月曜日

System.Reflection.Metadataを使用する

みなさん、こんにちは、旧HAZAMAです。他の場所では既にハンドルネームを昔のものに戻しているんですが、こちらでも戻すことにしました。改めましてtrain12です。

こちらのブログの投稿としては4ヶ月ぶりになってしまいました。今回は表題の通り、System.Reflection.Metadataというライブラリについての投稿です。
.NETが変化し始めてもう数年でしょうか、.NET Coreが2.1になり、だいぶ成熟していますが、これを使う上で問題になることが一つあります。そう、標準では式木などで生成したコードをアセンブリファイルに保存できないのです。.NET Frameworkでは、アセンブリを生成する際にオプションを指定することで保存も可能になるのですが、.NET Coreにはそんなオプションは用意されていません。そこで使用することになるのが表題のライブラリです。System名前空間にあることからわかる通り、将来的にはCoreに添付されるようになるのかもしれません。現状は、NuGet経由でインストールすることになります。
さて、肝心のこのライブラリの使用方法ですが、驚くことにドキュメントがありません。オープンソースなのでソースコードを見に行くとそこにはXMLコメントがあるのですが、その程度。基本的な使い方ですら、いろいろ試行錯誤して捻り出さなければならないという始末になっています。MSが開発しているようなのですが、ちょっとここらへんはお粗末としか言いようがありません。
さて、単独でアセンブリを生成する方法はまだこれから試行錯誤しなければならない段階にあるのですが、この記事では既にSystem.Reflection名前空間で吐いたアセンブリを読み込んで編集してファイルに保存し直す方法をお見せします。ただ、コードを全部載せると長くなりすぎてしまう(具体的には、System.Reflection.Metadataで動く状態のインスタンスを作るコードだけで360行ありました)ので、ここでは概要だけを示すに留めます。単純に既にあるアセンブリをいじって保存したいだけなのにできない人たちの手助けになればいいでしょう。


  1. まずは何はともあれ、PEHeaderBuilderを作成します。基本的に元の値をそのまま入れれば大丈夫なはずです。
  2. ILをコピーします。ここがちょっと曲者で、なしでも動くかもしれませんが私は、Roslynのコードを参考に多少変形をかけています。この時にMethodBodyStreamEncoder.MethodBodyからOffsetを拾ってくるのを忘れずに。これを記録しておかないと、RVA(RelationalVirtualAddressの略です、多分)が算出できなくなります。
  3. MetadataBuilderを作ります。これはMetadataBuilderのインスタンスを生成後、各テーブルの情報を押し込むところまで含めています。この部分だけで200行近く行くはずです。
  4. MetadataBuilderからMetadataRootBuilderを作ります。
  5. あとはこれらをManagedPEBuilderに放り込み、BlobBuilderにSerializeして、最後にWriteContentToでStreamに書き込めば完成のはずです。
こんな概要を見るよりも、私の自作言語のコンパイラの該当箇所を読んだ方が早いかもしれないので、該当箇所のURLを貼っておきます。

これがMSの中の人に聞きつつ、捻り出したSystem.Reflection.Metadataの使い方です。ドキュメンテーションのないライブラリを利用するのは辛いですね。今回ので身に沁みました。
中の人によると、全然ドキュメンテーションが書けてないようなので、しばらくはこれが活用されるかもしれませんね。

2018年2月26日月曜日

interfaceを動的に継承する方法

どうも、こんにちは、はざまです。今回は需要があるかどうかわかりませんが久しぶりに単独の技術ネタを書こうかなと思います。
先日の記事で私が自作言語を作っていることは周知のことかと思いますが、そのコードを書いている際にある問題に出くわしました。Interfaceの実装問題です。C#にはTypeBuilderというクラスがあり、これを使えば、型(interfaceもclassもstructも)を定義できるはずなのですが、なぜかinterfaceを実装するclassを定義しようとしてもうまくいかない。MSDNのTypeBuilder.AddInterfaceImplemetationメソッドの例の通りに書いているのにうまくいかない、なんでだろうと1週間以上試行錯誤を経てたどり着いた結果が、生成したクラスがobjectを継承していないことでした。つまり、C#でinterfaceを継承した型を動的に生成する最小のコードは、以下のような感じになるでしょう。
using System;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;

namespace Test
{
    class Main
    {
        public static void Main(string[] args)
        {
            var name = new AssemblyName("test");
            var asm_builder = Thread.GetDomain().DefineDynamicAssembly(name, AssemblyBuilderAccess.RunAndSave, "./");
            var file_name = "test.exe";
            var mod_builder = asm_builder.DefineDynamicModule(file_name);
            var interface_builder = mod_builder.DefineType("IInterface", TypeAttributes.Abstract | TypeAttributes.Interface);
            interface_builder.DefineMethod("DoSomeBehavior", MethodAttributes.Public | MethodAttributes.Abstract | MethodAttributes.Virtual, typeof(int), null);
            var interface_type = interface_builder.CreateType();

            var type_builder = mod_builder.DefineType("TestClass", TypeAttributes.NotPublic | TypeAttributes.Class, typeof(object), new []{interface_type});
            var ctor = type_builder.DefineConstructor(MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName,
                CallingConventions.Standard, Enumerable.Empty<Type>().ToArray());
            var il_generator = ctor.GetILGenerator();
            il_generator.Emit(OpCodes.Ret);

            type_builder.AddInterfaceImplementation(interface_type);
            var method_builder = type_builder.DefineMethod("DoSomeBehavior", MethodAttributes.Public | MethodAttributes.Virtual, typeof(int), Enumerable.Empty<Type>().ToArray());
            var il_generator2 = method_builder.GetILGenerator();
            il_generator2.Emit(OpCodes.Ldc_I4_0);
            il_generator2.Emit(OpCodes.Ret);

            var class_type = type_builder.CreateType();
            var instance = Activator.CreateInstance(class_type);

            asm_builder.Save(file_name);
            var method = class_type.GetMethod("DoSomeBehavior");
            var return_value = method.Invoke(instance, null);
            Console.WriteLine(return_value);
        }
    }
}

注意点としては、classにobjectを継承させる以外にも、interfaceはabstractにすること、interfaceのメソッドは、abstractかつvirtual、publicで宣言すること、classのメソッドは、publicかつvirtualで宣言することが挙げられます。今回は、フィールドを宣言したり、アクセスしたりしないので簡潔になっていますが、フィールドにメソッド内でアクセスしようとすると途端に複雑になり、TypeBuilder単独では達成できなくなったりしますが、それはまた別の話です。そこに関しては、他の方が記事にしているので、当方では記事にするつもりはありません。

2017年10月30日月曜日

C#のforとforeachに関する思想録

どうも、はざまです。つい先日、とあるニコ生を見ている際に、forとforeachの違いについて言及される場面があり、私がforeachを使うことを勧めたところ、foreachの方がiteratorオブジェクトの破棄がループごとに発生するから遅くなるという指摘をいただき、気になったので速度比較してみることにしました。私はそのとき、え〜Releaseビルドなら、ループ全体でiteratorを使い回すように最適化してくれるんじゃないと答えたのですが、果たして結果は(よく考えたら、iteratorは使いまわさないとおかしいですね。ループ状態を内包しているはずなので)。
今回試したコードは、以下のようなものです。
const int Max = 10_000_000;

var stopwatch = Stopwatch.StartNew();
int result = 0;
foreach(var i in Enumerable.Range(0, Max))
    result += i;

stopwatch.Stop();
Console.WriteLine("foreach Result: {0}/{1}ms", result, stopwatch.ElapsedMilliseconds);

var stopwatch2 = Stopwatch.StartNew();
int result2 = 0;
for(int i = 0; i < Max; ++i)
    result2 += i;

stopwatch2.Stop();
Console.WriteLine("for Result: {0}/{1}ms", result2, stopwatch2.ElapsedMilliseconds);


Stopwatchインスタンスを生成して、foreach/forループ内で足しこむだけです。最後にそれぞれの結果を出力するのは、デッドコード削除で足しこみ自体省略されたら、嫌だなという意図です。このコードだとそれぞれ、確実にオーバーフローが発生しますが、何も書かなければC#はオーバーフローを無視してくれるはずなので、今回は特に考慮していません。さて、この単純なコードで出た結果がこちら。

Debugビルド時
foreach Result: -2014260032/197ms
for Result: -2014260032/41ms

Releaseビルド時
foreach Result: -2014260032/171ms
for Result: -2014260032/26ms

テスト環境はMacBook Pro (Retina, 13-inch, Early 2015)で2.7Ghz Intel Core i5、メモリ16GBです。Debugビルド時でおよそ5倍、Releaseビルド時で7倍弱程度の差ができてしまいました。これを見る限り、ループごとに一時変数を用意してGC走らせていそうなのは間違いなさそうですね。それにしても、結構インパクトでかいです。そっか〜、foreachって遅かったのか〜。とは言っても、可読性重視したいので、使い続けますけどね。for使うとめんどくさいですしね。
速度にうるさいC++のrange-based forとかはどんな実装になってるんだろう。ループ全体で一時変数を使いまわしたりしてないのかな〜。まあ、そうしたら、この一時変数のスコープが大きくなっちゃうんですがね。
では、また(๑╹ω╹๑ )

2013年8月29日木曜日

マルチリンガルで行こう(WPF編)

えー、前回いつ更新したか覚えてないくらいにはご無沙汰してます、はざまです。
今回は、趣味グラミングアプリを作っていてちょっとはまった部分を自分用メモも兼ねてエントリーにしておこうと思います。題材にするのはその名も、WPFLocalizationExtensionです。このライブラリ、名前の通りWPFで作成するアプリのローカライゼーションを容易にしてくれます。具体的にどんなことができるのかは公式サイトのイントロダクション動画を参照していただくとして、残念ながらこのライブラリーはドキュメンテーションの質がイマイチなので、ここでは準備方法と使用方法の解説をしたいと思います。

まず、インストール方法ですが、nuGetが使えるならそこから入手するのが一番楽です。というか、その方法しか試したことはありません。一からビルドする場合には、ご自身でその方法を調査してみてください。
インストールが終わったら、まずサンプルとして以下のような.xamlファイルを作成します。

<Window
    x:Class="WPFLocalizationExtensionExample.ExampleView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:lex="http://wpflocalizeextension.codeplex.com"
    lex:LocalizeDictionary.DesignCulture="en"
    lex:ResxLocalizationProvider.DefaultAssembly="WPFLocalizationExtension"
    lex:ResxLocalizationProvider.DefaultDictionary="StringResources">
    <Grid>
        <TextBlock Text="{lex:Loc ExampleTexts_HelloWorld}" />
    </Grid>
</Window>

上記のコードに関していくつか補足しておきます。まず5行目の式は見た目通り、WPFLocalizationExtensionの名前空間をlexという名前でXAMLに取り込むことを指定しています。なぜlexなのかは、公式サイトなどでそう推奨されているからとしか答えようがありません。
次の6~8行の式はこのXAMLファイル全体に影響するWPFLocalizationExtensionの設定です。具体的に上から"VSなどのデザイナーで表示する言語設定","WPFLocalizationExtensionがリソースを検索する際に使用するアセンブリー名","WPFLocalizationExtensionがリソースを検索する際に使用する.resxファイルの名前"となっています。これらの式はXAMLファイル中のいずれの場所にも記述できて逐次、設定を変更できるようです。
なお、ここに記すメモはRESXファイルをリソースとして使用することを前提としています。WPFLocalizationExtensionの機能として適切なILocalizationProviderを用意すれば、.csvや.xml、はたまた.txt,.jsonファイルなどもリソースファイルとして使用できるようですが、話が煩雑になるためここでは一切触れません。
さて、XAMLファイルを用意したら次は、リソースを作成します。公式サイトの説明によると、RESXファイルはVSで新規プロジェクトを作成した際にプロジェクト直下に作られるPropertiesという名前のフォルダーになければならないらしいので、その場所に今回はStringResources.resxという名前でRESXファイルを作成します。そして、このファイルに次のエントリーを追加します。
名前(key):ExampleTexts_HelloWorld
値(value):Hello world!
さらにそのリソースファイルを開いたままVS2012の場合は、ファイルタブの直下を見てください。
上の画像で示されたアクセス修飾子というプルダウンリストから"Public"を指定してください。これがないとライブラリーがリソースにアクセスできなくなってしまうようです。
ここまで作業が終わったら先ほどのXAMLファイルのデザイナータブを開いてみてください。全て正常に動作していれば、Hello world!という文字列を表示するウインドウがデザイナー画面に表示されるはずです。これでひとまず、WPFLocalizationExtensionから文字列リソースを扱う準備は整いました。ここからがいよいよ本領発揮となる多言語への対応です。ここでは日本語リソースを追加してみましょう。
まず、Propertiesフォルダー内にStringResources.ja.resxというファイルを作成します。そして、次のエントリーを追加してください。
名前(key):ExampleTexts_HelloWorld
値(value):こんにちは、世界!
エントリーを追加したら一旦IDEを終了し、適当なテキストエディターでIDEで開いていたプロジェクトファイルを開いてください。すると、そこに以下のような要素があるはずです。

<EmbeddedResource Include="Properties\StringResources.ja.resx">

この要素の中身を次のように書き換えてください。
<SubType>Designer</SubType>
<DependentUpon>StringResources.resx</DependentUpon>

書き換えが終わったらしっかり保存してIDEに戻りましょう。この状態でデザイナー画面を開き、XAMLファイル内のlex:LocalizeDictionary.DesignCultureの値をjaにすれば、晴れてウインドウ内に表示されるテキストが日本語化されるはずです。
これでWPFアプリケーションのグローバリゼーションは完了です。あとは、他の言語のRESXファイルを追加するなり、文字列以外のリソースを追加するなり色々試してみてください。WPFLocalizationExtensionは汎用性が高い分、使用方法が幾分煩雑になっていますが、適切な前準備さえ終えてしまえばコードに"WPFLocalizeExtension.Engine.LocalizeDictionary.Instance.Culture = 使用したい言語を表すSystem.Globalization.CultureInfoオブジェクト"の一文を追加するだけで表示する言語が変わるなど非常に使い勝手のいいライブラリーだと思います。



最後に、タイトルの(WPF編)という一言について補足します。WPF編と銘打っているからには今後、シリーズ化して他のプラットフォームでのグローバリゼーションの話題も出すんだろうなと突っ込まれても困ります。シリーズ化するかは未定です。