いや、やや不透明ですが、内部ではLinuxカーネルが動いているので、当然といえば当然かも知れません。同様に、main関数のポーリングのようなコードは、epollと関係あるのかも知れません。(一応擁護しておくなら、頑張ってきれいに書こうとしている匂いはします)では、そろそろ今回やろうとしていることの本題に入りましょうか。Calendarでは収集つかなくなりそうなので、Blink同様のものを目標にします。真のLチカ(つまり、LEDの点滅のみ)をやってみます。ボタン入力に対応させてみます。C#らしいコードにします。生成されたコードを比較します。完成したサンプルコードはここにあります。上記サンプルコードを動かす場合は、リポジトリ全体をcloneした後、以下のように準備しておきます。adapter(拡張機能から)が入っている必要があります。SDKを入れてあるなら入っているでしょう。ルートにあるil2c.slnを開いてビルドします。1.0以降の全てのバージョンに依存しているため、環境によってはビルドできないかも知れません。Installerを起動して、とにかく全てのコンポーネントをインストールしてみるのはありかも知れません。私は常に全部入りです)。Explorerからテストを実行して全部パスすることを確認しておきます(結構時間がかかります。CPUコア数多いと速いです。i7-4790Kで5分ぐらい。toolchainをダウンロードするため、遅い回線だと辛いかもしれません)。masterブランチには安定的に動作するコードが入っているはずですが、テストを実行しておけば、少なくとも私が見た結果と同じものが得られるはずです。samples/AzureSphere/AzureSphere.slnを開いてビルドしてください。このソリューリョンには3つのプロジェクトが含まれていますが、依存関係が正しく設定されているはずなので、ビルドすれば全てが正しく構築されるはずです。Blinkのプロジェクトを一から書く場合の解説です。Standardで使われている、新しいシンプルなcsprojファイル)ライブラリプロジェクトを使って、net46以上かnetcoreapp2.0以降をターゲットにします。0.4.22以降(この記事を書いた時点の最新)を参照してください。これでビルド時に裏でIL2Cが自動的に実行され、C言語のソースコードが出力されるようになります(IL2Cのバイナリを明示的にインストールしたりする必要はありません)。(説明が前後しますが、C#プロジェクト自体は、IL2C.Buildパッケージを参照するだけで作ることが出来ます。今回、IL2Cのリポジトリ全体をcloneするのは、IL2C.Runtimeのビルドのためです。この辺りはスマートではない事は認知しているので、将来的にはもっと簡単に出来るようにしたいと思っています)C言語のソースコードは、デフォルトでは$(OutDir)/IL2C/の下に出力されます。Frameworkで使われているcsprojのライブラリプロジェクトでも出来ますが、IL2C.Interopパッケージが正しく参照されないため、手動でこのパッケージも追加する必要があります。net46より古いか、netstandard辺りのプラットフォームをターゲットとすることも出来ますが、現状のIL2C.Buildではビルドできないので、IL2C.exeを自力で呼び出す必要があります(PostBuildEventに書きます。引数の種類は限られているのでそれほど難しくはありませんが、ここでは省略します)。Interoperability(相互運用)と言うと、.NETのコードから、ネイティブのコード(例えばWin32やLinuxのAPI)を呼び出したりする事を指します。.NETのコードは、全てが.NETだけで実現されているわけではなく、例えばファイルの入出力やネットワークアクセスなどは、OSのAPIを呼び出して実現する必要があります。普通のプログラムは、これらの「外部リソース」にアクセスします。したがって、如何に簡単にプログラムの処理系からAPIを呼び出せるかが鍵となります。呼ばれる機能があり、これを使って相互運用を実現します。APIにはデバッグAPIがあって、デバッグ用の文字列を出力することが出来ます(デバッガのログに表示されます)。APIと紐付けられ、まるで.NETにそのメソッドが存在するかのように呼び出すことが出来るようになります。Coreやmonoで想定されるマルチプラットフォーム(つまりLinuxやMac)でも、全く同じ方法でネイティブのAPIを呼び出すことが出来ます。IL2Cでは、P/Invokeを踏襲した相互運用機能を実現するのと同時に、独自の相互運用機能も用意しています(注:現状、どちらの実装もまだ完全ではありません)。}}何故、IL2C/Invokeを用意したのかは、このコードを注意深く見ると気がつくかも知れません。P/InvokeのDllImport属性は、対象のAPIが「ダイナミックリンクライブラリ」に含まれている事を前提としています。kernel32.dllを参照することで、プログラムの実行時に指定されたライブラリを「動的」に読み込んで、APIの呼び出しを可能にします。IL2CはC言語のソースを出力し、それらがすべて事前にコンパイルされる必要があります。参照すべきAPIもコンパイル時に解決される必要があるため、ライブラリ本体ではなく、C言語のヘッダファイルへの参照が必要なのです。当初はDllImport属性を流用していたのですが、このような理由から独自の属性を定義して区別するようにしました。区別したことで、後で述べるような利点も生まれています。唯一、MethodImpl属性が邪魔ですね。これはC#コンパイラが要求していて省略できないため、どうしても一緒に書いておく必要があります。今のところ回避方法はありません。SDKのAPIを.NETから使えるようにしてみます。APIをカバーしてみましょう。SDKのtime.hに定義されています。引数にtimespec構造体へのポインタを取ります。P/Invokeで使われるのと同じ技法で、ポインタはrefで参照するようにします。.NETから構造体を参照できるように、timespec構造体を宣言します。timespec”)をSymbolNameプロパティで適用しておきます。宣言名を省略すれば、.NET構造体の名前がそのまま使用されます。SDKはC++をサポートしていないので、この場合は接頭語を省略できません)。先程、IL2C/Invokeには利点があるという話をしましたが、このNativeType属性が正にそれです。P/Invokeの場合、定義する構造体の型は、バイナリレイアウトを含めて完全な状態で再定義しなければならず、比較的難易度の高い作業でした。NativeType属性が適用された構造体は、基本的にシンボル名と型が合っていれば、問題なくネイティブコードと結合できます。NET構造体の定義はIL2Cによって無視され、C言語の構造体を(定義が同一であるという前提で)そのまま使うからです。...}C言語のソースコード中に、timespec構造体の詳細な定義が無く、typedefを使って本物のtimespec構造体にバイパスしているだけです。渡されるようになっています。渡すことも出来るようにしたり、P/Invokeと同じように、StringBuilderクラスを使ったやり取りも出来るようにする予定です。APIをカバーして、Lチカを完成させます。APIに渡すことで操作します。入力の場合はまた別のAPIを使いますが、まずはLチカを実現しましょう。ディスクリプタは32ビット整数(C言語でint)なので、.NETでもint(System.Int32)でOKですね。戻り値で得られるので、これをローカル変数に保存しておきます。NativeValue属性もIL2C/Invokeで使用する属性で、定数シンボル(C言語マクロなど)を参照出来るようにするために使用します。staticフィールドの初期値を割り当てないため、コンパイル時に警告が発生し、具合がよくありません。将来のバージョンではやり方を変えるかも知れません。ループ内で、現在のフラグに応じて、GPIO_Value_HighとGPIO_Value_Lowを切り替え、LEDの点滅を実現します。ここでもNativeType属性を便利に使うことが出来ます。列挙型GPIO_OutputMode_TypeとGPIO_Value_Typeを、C言語の列挙型に対応付けています。残念ながら、今はまだ、列挙型の値(数値)をC言語の定義と一致させる必要があります(将来的には省けるようにしようと考えています)。このコードを俯瞰すると、C#でP/Invokeを使ったことがある人なら「この醜い相互運用の定義は、別のクラスに移動しておこう。すれば、Mainメソッドがスッキリする」と想像出来ると思います。classを使えば良いでしょう。IL2CはC#のソースファイルではなく、コンパイルされたアセンブリファイル(MT3620Blink.dll)に対して処理を行うからです。:)サンプルコードでは、Interopsというクラスを作り、そこに移動しました。value);}IL2Cでは、ref引数でもout引数でもポインタ渡しになります。GPIO_GetValueは(ポインタ経由で)値を読み取ることは無いのでoutにしておきます。varを使って簡単に書けるようになります。nanosleepも直しておきます。}}ボタンの状態は瞬時に読み取られるので、連続的に待機時間が変わってしまわないように、ボタン状態の変化を見るようにしています。SDKのBlinkerサンプルは、ディスクリプタの取得に失敗した場合(-1が返されるなど)の処理もきちんと実装していますが、このコードでは省略しています。やるならC#らしく、例外を定義してスローするという方法が考えられます。epollを使って疑似イベント駆動にする最初に取り組むLチカ+αの課題はこれで十分だと思いますが、C#を使っているのにC言語でプログラムを書いているのとあまり変わりがなく、IL2C/Invokeのための記述が増えるだけで面白みがありませんね。SDKのBlinkerサンプルは、実装がもう一捻りしてあります(それであんなに複雑なことになっているのですが...)。それは、ボタンクリックのトリガーと点滅を、それぞれタイマーを割り当てて、タイマー経過をepollを使ってマルチプレクスして、擬似的なイベント駆動として処理しているのです。実際、前述のコードには些末ですが問題があります。それは、sleepを呼び出している間は、ボタンの入力が受け付けられないことです。最後にこの処理を、.NETとC#のパワーでキレイに対処して見ましょう。この図は、これからC#で作る型の関係を示したものです。いくつかのクラスとインターフェイスを定義して、このような継承関係を構築します。Descriptorクラスは、このすぐ後で説明する、APIのディスクリプタを管理する基底クラスです。Applicationクラスが、IEPollListenerインターフェイスを実装するインスタンスを受け取ってepollに登録し、発生するイベントの集約と配信を行います。GpioBlinkerとGpioPollerクラスが、LEDの点滅とボタンのトリガーを監視します。両方共にTimerクラスを継承して、インターバルタイマーの経過を使って処理を行います。それでは、順を追って説明します。}}このクラスのポイントは、基底クラスとして継承可能にしておき、IDisposableインターフェイスを実装してディスクリプタを破棄可能にします。コンストラクタで渡されたディスクリプタが負数の場合は、例外をスローしておきます。}}各APIへのアクセスは、前章で作ったInteropsクラスの定義をそのまま流用しています。ディスクリプタはDisposeメソッドで解放されるため、破棄についてはここでは何もしていません。説明不要で理解できると思います。次に、タイマーの処理もクラスにカプセル化してしまいましょう。その前に、以下のようなインターフェイスを用意しておきます。Raised();}タイマークラスは更に継承して、Raisedメソッドを実装することで、タイマー経過時の処理を簡単に記述できるようにしておきます。C#で書き始めた途端に、ものすごく設計が捗るようになった気がしますね。最後にepollを処理するクラスを用意します。}}この後で紹介しますが、このクラスはWinFormsやWPFのApplicationクラスに似た位置づけなので、同じようにApplicationクラスと名付けました。IEPollListenerインターフェイスを実装したクラスをepollに連携させるように登録・解除出来るように、RegisterDescriptorメソッドとUnregisterDescriptorメソッドを公開しています。Runメソッドでepollイベントの監視を行います。このコードには少し解説が必要でしょう。ポイントはGCHandle構造体です。この構造体はインスタンスへの参照をガベージコレクタの操作から保護します。この記事ではあえてIL2Cの内部構造に触れてきませんでしたが、IL2Cはマークアンドスイープ方式のガベージコレクタを持っています。クラスのインスタンスをnewすると、そのインスタンスはヒープメモリ(最終的にはmalloc関数)に格納されます。マークアンドスイープ方式では、メモリコンパクションと呼ばれる操作は行われませんが、使われていないインスタンスについては自動的に解放(free関数)されます。Core、monoではこれに加えて、インスタンスが別の場所に移動してしまうかもしれない問題もある(メモリコンパクションが発生)のですが、IL2Cではその問題は発生しません。これらの問題は、GCHandleを使えば解決できます。インスタンスが誤って解放されないようにするには、GCHandle.AllocメソッドをGCHandleType.Pinnedで呼び出します。これで指定されたインスタンスは、IL2C上で使用されていなくても破棄されなくなります。次に、GCHandle.ToIntPtrメソッドを呼び出すと、Allocで指定したインスタンスへのポインタが得られます。ポインタの型はSystem.IntPtr型(C言語ではintptr_t型)です。このポインタは、基本的に直接操作するためのものではありません。間接的にインスタンスを示す値と考えておけばよいでしょう。これで上記2点についてはクリアできました。注意すべきことがあります。一度GCHandle.Allocでインスタンスを固定すると、解除するまでは永久にヒープメモリに保持され続けます。不要になったタイミングでGCHandle.Freeメソッドを呼び出して固定を解除する事を忘れないようにしましょう。メモリリークに繋がります(具体的な方法についてはUnregisterDescriptorメソッドを参照して下さい)。上記で得たポインタは、epoll_event構造体の中のepoll_data_t構造体に保存します。この構造体はNativeType属性をつけてあります(実際は共用体です。data;}ptrフィールドはNativePointer型で宣言してあります。実際の型がvoid*で宣言されていますが、C#ではvoid*は限られた使用法でしか使えません。NativePointer型としておくと、System.IntPtr型と自由に変換することが出来るようになります。epoll_wait関数がイベントを受信すると、そのイベントに対応する情報がepoll_event構造体に格納されます。これは、epoll_ctl関数で登録した情報と同じものです。イベントが発生すると、GCHandle.ToIntPtrメソッドで得たポインタを取得できることになります。このポインタはGCHandle.FromIntPtrメソッドを使ってGCHandleに復元できます。GCHandleさえ手に入ってしまえば、Targetプロパティから(C#で認識可能な)インスタンス参照を得ることが出来ます。プロパティの型はSystem.Object型なので、明示的にIEpollListenerインターフェイスにキャストしておきましょう。最後にIEPollListener.OnRaisedメソッドを呼び出せば、イベントを登録したときのインスタンスのOnRaisedが呼び出されます(ここではTimer.OnRaisedメソッド)。これでインフラは全部揃いました。}}GpioBlinkerクラスはLEDを点滅させるクラス、GpioPollerはボタン入力を検知してLEDの点滅速度を変化させるクラス、そしてMainメソッドでApplicationクラスと2つのクラスを生成して登録し、イベント処理のループに入ります。今までインフラを作っていたので全容が見えにくかったのですが、このMainメソッドと2つのクラスだけを見れば、かなりシンプルでわかりやすいと思います。この図のように、Lチカ固有のコードにだけ関心を向けることが出来ます。Sphere)のライブラリとして固めておけば、更に捗りそうです。NuGetパッケージ化すればなお良いでしょう。