KMCとPiet,そして最強のPietの為にdllを動的にC#で読みこむ話

はじめに

この記事はKMC Advent Calendar 2015 18日目の記事です。 前回の記事はwalkureさんによる京大マイコンクラブと旅するわたし - (。・ω・。)ノ・☆':*;':*でした。

KMCID: murataです。
最近Pietのことしか考えてないのでこの記事を書いたら半年ほどPiet禁します。 タイトルの「C#でDllを動的に読み込む話」はC#とかいう難しい言語でちょっとよくわかりにくいので 先に簡単で分かりやすい言語であるPietの話から始めますね。

最近僕が描いたPiet

3つ程描きました。

あぁ^~心がぴょんぴょんするんじゃぁ^~とは (アァーココロガピョンピョンスルンジャァーとは) [単語記事] - ニコニコ大百科に、 他のプログラミング言語はいっぱいあるのにPietだけなかったので描きました。 「あ~心がぴょんぴょんするんじゃ~」と出力します。 Pietはプログラミング出来い人にも画像という冗長性の手段によってコードの意味を伝えられる素晴らしい言語なんです。

髪の毛で72を取ってそれを顔を縦横無尽に横断しながらCHIHAYAという文字列を作って出力し続けています。

gyazo.com
これはここに描いているとおりの文字列を出力するコードです。 好きな文字列をわかりやすく表現出来ますね。 Pietは柔軟にコードを手軽に描ける良い言語なんです。

今年のKMCとPietの活動を振り返る

この記事はKMCアドベントカレンダーなので、 一応今年(2015年)のKMCの大きなPiet活動を簡単に振り返ろうと思います。

時系列順にPiet活動を振り返る

  • 3月 Pietの統合開発環境(IDE)であるPidetPietのエディタを作った話が発表される
    • これにより、開発がスムーズになりPietの開発が加速されました。
  • 5月 Pietを描く新入生プロジェクト発足
    • KMCの新入生に初めてのプログラミングでPietを教える…正気の沙汰ではないですね。 kmc.hatenablog.jp
  • 8月 YAPCでPietの講座が部員によって行われる
  • 11月 NFでPietが展示される
    • NF(京大の学祭)でPietを展示しました。 文字でしかプログラミング出来ないという常識を吹き飛ばすのがなかなか新鮮なのか意外と好評でした。

KMCの部員が開発しているツールを探してみた

Pietの中間言語コンパイラ、テストユーティリティ、インタプリタIDE…十分な開発環境が用意されてますね。 ここまでPietの開発が進んでいるのはKMCだけではないでしょうか?

僕の考えた最強のPiet

こんなに夢のあるPietをさらに実用的な言語にしようと、 僕は二週間くらいかけて最強のPiet拡張を考案しました。 概要だけ言うと数字だけでなくスタックを積んだりDllを扱えて、 ゲームやWebアプリケーションを手軽に作れるようになる予定です。 @Noname774さんのサークルにその仕様書を寄稿させてもらったのでよければお手に取り下さい。

C#でDllを動的に読み込む

僕の考えた最強のPietはC#で作られたPidetをベースに作る予定なので、 同じくC#で実装します。 僕の考えた最強のPietではdllを読み込む機能を搭載する予定です。 そのためにまずはC#で動的にdllを読み込める必要があります。 今回の記事のメインはその読み込み方についてです。 静的に(つまりコンパイル前に予め)読み込むのなら、 DllImport属性を利用することで簡単に利用することが出来ます。 (解説は他所に譲ります)。 しかし、この方法ではコンパイル後にそれで定義していない新しいDllの関数を動的に呼び出すことは出来ません。 ではどうすればいいのでしょうか?

.Net製のDllの場合

.Net製Dll、つまりC#VB.Netで作成されたdllの場合は比較的楽に呼び出すことが出来ます。

public static object Load_DotNet_Dll(string dllpath,string ClassName ,string FuncName, object[] Params) {
    try {
        var t = Assembly.LoadFrom(dllpath).GetType(ClassName);
        var obj = Activator.CreateInstance(t);
        return t.GetMethod(FuncName).Invoke(obj, Params);
    } catch {return 0;}
}

(usingは省略しました)。 使うときには、

Load_DotNet_Dll("Plugins/TestDLL.dll", "TestDll.CsDll.LangA",new object[] { 5, "RA" });

このように文字列と引数の配列だけを用います。 これで動的に好きなdllを呼び出せるようになります。 (この例では自作DllをPlugins下にTestDLL.dllという名前で配置していて、 そのDll内で定義された、TestDll名前空間のCsDllクラスのLangA関数を引数 5 と "RA" で呼び出しています。) .Net製dllは型など色々な情報がそのまま入っているため、扱いが容易なのですね。

C製のDllの場合

C製のDll(kernel32.dll,winmm.dllなど)はちょっと手間が必要です。 関数ポインタを得る為に kernel32.dllからLoadLibrary,FreeLibrary,GetProcAddressを呼び出す必要があります。 さらに引数に応じた関数デリゲードは動的に作成しなければならないのでなんかめんどかったです。 デリゲードをジェネリックで作成すると一見成功するように思えますが、そうすると いざ使う時に型がジェネリックのままなので使うことが出来ないです。

C#でLoadLibraryを使用してアンマネージDLLを使用する - 閑古鳥 C# GetDelegateForFunctionPointer with generic delegate - Stack Overflow

[DllImport("kernel32", CharSet = CharSet.Unicode, SetLastError = true)]
internal static extern IntPtr LoadLibrary(string lpFileName);
[DllImport("kernel32", SetLastError = true)]
internal static extern bool FreeLibrary(IntPtr hModule);
[DllImport("kernel32", CharSet = CharSet.Ansi, SetLastError = true, ExactSpelling = false)]
internal static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName);

public static class DelegateCreator {
    private static readonly Func<Type[], Type> MakeNewCustomDelegate 
        = (Func<Type[], Type>)Delegate.CreateDelegate(
            typeof(Func<Type[], Type>),
            typeof(Expression)
                .Assembly
                .GetType("System.Linq.Expressions.Compiler.DelegateHelpers")
                .GetMethod("MakeNewCustomDelegate", BindingFlags.NonPublic | BindingFlags.Static));
    public static Type NewDelegateType(Type ret, Type[] Params) {
        Type[] args = new Type[Params.Length+1];
        Params.CopyTo(args, 0);
        args[args.Length - 1] = ret;
        return MakeNewCustomDelegate(args);
    }
}


public static object Load_C_Dll(string dllpath,string func , object[] Params) {
    try {
        var Types = Params.Select(arg => arg.GetType()).ToArray();
        IntPtr handle = LoadLibrary(dllpath);
        IntPtr fadd = GetProcAddress(handle, func);
        Type t = DelegateCreator.NewDelegateType(typeof(IntPtr), Types);
        object res = Marshal.GetDelegateForFunctionPointer(fadd, t).DynamicInvoke(Params);
        FreeLibrary(handle);
        return res;
    } catch { return 0; }
}

私達はこの関数を定義したのでもうこれからは、 Load_C_Dll("user32.dll", "MessageBoxA", new object[] { IntPtr.Zero, "Hello World.", "Caption", (uint)0 }) のようにすることでコンパイル後でも、実行中の好きな時に好きなDllを呼び出すことが出来ます!

まとめ

このDll機能を利用して僕は最強のPietを作るつもりなので乞うご期待ですよ。

ついでに宣伝

https://play.google.com/store/apps/details?id=com.Paradigm.Ikada https://lh3.googleusercontent.com/1QTRWS-ETw4ul_5SHEA5RjG0E49JrxsT-wIGhOxvUIJ_tgVIo9cgunlJet19O94Wffme=h900 最近運ゲー排除マインスイーパーをAndroidアプリにした話と市場に対する雑感 - DNEK's blogみたいに自作ゲームを宣伝するのが流行っているっぽいので僕も最近作ったパズルゲームのAndroid版を公開します。 よかったら遊んでステージ作れるんで投稿して下さい。

最後に

明日の記事は、Kazakamiさんによる「shellチャンネルについて」です。 好きなコマンドが何もかも気にせず好き放題打てる夢の様なチャンネルShellチャンネルの話ですね! 期待しています!!!!!