de:code 2019 CM12 – Interoperability of .NET assemblies.

de:code 2019

はじめに

この記事は、2019年5月29,30日に東京で開催される「de:code 2019」のセッション「CM12: .NET Core マルチプラットフォームの本質」に対応するサンプルコードと解説を公開しています。

ご注意: 原本はGitHubにあります。このブログの記事は、タグ1.0のコピーです。GitHubのドキュメントに対する検索性の悪さのため、ここに転載するものです(ブログスタイルに合わせるため、HTMLは多少変更していますが、記載内容は同じです)。

  • 本記事はKouji Matsuiが独自に構成、執筆したものであり、「Microsoft MVPパーソナルスポンサー」として提供するものです。de:code 2019のオフィシャルドキュメントではありません。
  • 本記事執筆時には、セッションの概要のみ公開されているため、内容が密接に連携しない場合があります。また、内容は、原稿執筆時の情報によります。
  • サンプルコードの開発検証環境は、Visual Studio 2017 (15.9.11)です。2019でも動作すると思われますが、都合により未確認です。また、Linux環境はWSL1のUbuntu 18.04 + dotnet-sdk-2.2 (2.2.204-1)で検証しました。
  • 読者の想定難易度は、セッションの基準であるLevel 400より多少低いところを出発点とし、章を追うことでセッションの基準難易度に近づくようにしています。できるだけ抽象的な解説を避け、具体例や図による解説、既存のコードへの参照を含めています。

著作権表記

  • Copyright (c) 2019 Kouji Matsui All rights reserved.
    本コンテンツの著作権、および本コンテンツ中に出てくる商標権、団体名、ロゴ、製品、サービスなどはそれぞれ、各権利保有者に帰属します。
  • ライセンスは外部参照を除き、MITとします。コードにはいかなる保証もありません。ご自身のプロジェクトに適用する場合は、各自の判断で行ってください。
  • この資料はコミュニティコンテンツです。ライセンスに従っていただけるのであれば、GitHub上でissueとpull requestを受け付けます

サンプルコード

  • このリポジトリ内に配置され、ドキュメント内からリンクされているので参照してください。
  • Azure PipelinesでCIしています: Azure PipelinesでCIしています

概要

.NETにおけるマルチプラットフォーム対応を支えるための、アセンブリレベルでの互換性の対処方法について、サンプルコードと解説を行います。章が進むにつれて、より細部についての解説を行っています。

  • マネージドサイド
    • インターフェイス分離
    • Bait and switchテクニック
  • アンマネージ連携
    • P/Invoke
    • ガベージコレクションの影響
  • ランタイムサイド
    • System.Private.CoreLib
    • ランタイム機能呼び出しの手段

マネージドサイド

マルチプラットフォーム戦略で最も一般的に使用されるテクニックは、インターフェイス分離と依存注入(Dependency Injection)でしょう。この章では、インターフェイス分離と、それに似たもう一つのテクニックについて解説します。

インターフェイス分離

インターフェイス分離は、最も一般的で基本的なOOPのテクニックです。.NETのインターフェイス型を使用して、異なる実装の詳細を、同一のインターフェイスで分離する事により、マルチプラットフォームに対応した処理として共通に扱えるようにします。

Part1_SplitByInterfaceプロジェクトは、以下の機能を持ちます:

  • 複数の値を入力すると、計算結果が出力される。
  • 入出力の対象は、コンソール(コマンドライン)と、GUI(WPF)である。

計算を司る共通のライブラリ「Calculator.Core.dll」と、それらを使用する各アプリケーションをインターフェイス定義で分離します。

コンソールアプリケーションの場合:

Part1_Diagram1

GUIアプリケーションの場合:

Part1_Diagram2

この図のように、右側の計算ライブラリには、プラットフォーム固有の実装は全く含まれず、左側のアプリケーション側に固有の実装を用意しておき、インターフェイス定義経由で計算ライブラリから操作できるようにします。

ConsoleCalculatorの実行結果:

Part1_ConsoleCalculator

WpfCalculatorの実行結果:

Part1_WpfCalculator

ここでは、手動での依存注入(メソッド引数で直接注入する)を行っています(より発展した例として、MEFや各種依存注入ライブラリを併用することが考えられますが、割愛します)。このサンプルコードで重要な点を以下に挙げます:

  • 計算を実行する本体(Calculator.Core)ライブラリは、プラットフォーム中立であり、netstandard2.0としてビルドしている。
  • Calculatorライブラリには、コンソールやGUIへの入出力コードは一切含まれておらず、それぞれ独立したアセンブリ(ConsoleCalculator, WpfCalculator)にのみ含まれる。
  • 同じ手法を用いて、その他のプラットフォームにも移植できる。その際、計算ライブラリに手を入れる必要がない。

Bait and switchテクニック

このテクニックは、以下のような二種類のアセンブリを用意しておき、コードのビルド時と実行時で異なるアセンブリを使用するものです:

  • ビルド用アセンブリ:
    ユーザーのアプリケーションやライブラリのビルドに使用するためだけのアセンブリ(「参照アセンブリ」と呼ぶ)。アセンブリ内の実装コードは、まったく存在しないか部分的にしか存在しない。コンパイラが、型のメタデータ定義(クラス名やメソッド名、型や引数群など)さえ確認できれば良い。
  • 実行時用アセンブリ:
    実行時に使用するアセンブリ。それぞれのプラットフォームに応じたアセンブリ(但し、アセンブリの厳密名とインターフェイス定義は上記と同一)を用意する。当然、アセンブリ内部の実装はそれぞれのプラットフォームによって異なる。

これは.NET標準に含まれるライブラリでも活用されています。例えば、「System.Runtime.dll」について考えます。ビルド時に参照するアセンブリ(SDKに含まれる)は、実は内部に実装が存在しません。しかし、型のメタデータ定義は存在するため、ビルド自体は成功します。その結果、アプリケーション内部には、System.Runtime.dllを使用している、という参照情報だけが残ります。

次に、アプリケーションを実行環境に移して実行すると、実行環境に存在する「別の」System.Runtime.dllが使用されます(Windowsの場合、GACに配置されているか、.NET Coreのランタイムディレクトリに配置されています)。このアセンブリにはプラットフォーム環境に依存した実装が入っていますが、型のメタデータ定義は同一であり、アセンブリローダーは問題なくロード出来るため、アプリケーションが実行できます。

Part2_Diagram

もちろん、参照アセンブリとメタデータが異なっていた場合(例えば、あるクラスのメソッドの引数の型が違っているなど)は、実行時にMissingMethodExceptionなどの例外が発生する可能性があります。そのため、参照アセンブリと実行時アセンブリのメタデータが同一となるように、注意深く実装しなければなりません。

Part2_BaitAndSwitchプロジェクトは、最初の計算アプリケーションをBait and switchテクニックを使って実装したものです:

  • 参照アセンブリとして、Host.Referenceプロジェクトを用意する。実装は空で、クラスとメソッドが定義されているだけである。
  • 上記参照アセンブリの定義と同一だが、それぞれコンソールとWPFに対応する実装を含んだ、Host.ConsoleプロジェクトHost.Wpfプロジェクトを用意する(アセンブリのファイル名はすべて同一で、Host.Core.dll)。
  • 他のアセンブリはすべて共通で、アプリケーション実行前にHost.RefereceをHost.ConsoleまたはHost.Wpfに入れ替える。
    • Console.batから起動すると、Host.Consoleに置き換えて実行する。
    • Wpf.batから起動すると、Host.Wpfに置き換えて実行する。
    • 置き換えずに実行すると、Host.Referenceを使うことになるため、実行時例外が発生する。

Bait and switchテクニックは、実装を入れ替えることができると言う点で、インターフェイス分離とよく似ています。しかし、インターフェイス分離の場合は、設計者が最初から適切に分離設計をする必要があります。Bait and switchは、CLRがアセンブリを同一視する方法に乗じて、コードに全く手を入れずに、プラットフォーム依存処理を入れ替えることを可能にします。

以下にこの2つの分離手法を比較した表を示します:

手法 メリット デメリット
インターフェイス分離 OOPの一般的な手法。誰でもリスクなしで使用でき、コンパイラが型チェックでエラーを検出してくれる 初めから使用する前提で設計する必要がある
Bait and switch 後から依存性の分離を行うことができる 型チェックはコンパイル時に行われないため、問題があると実行時エラーを起こす

かつてのPortable Class Libraryは、Bait and switch手法で互換性を実現しました。ここまでの解説で、PCLが何十というプロファイルを持っていたことを考えると、互換性を維持することがいかに困難であったかが想像出来ます。

Bait and switchを適用する場合は、メタデータの不一致を何らかの方法で開発時に検証できるようにするか、あるいはテンプレートコードの自動生成や合成ができるようにするなどの手法を用意し、開発の困難さを軽減することが重要です。


アンマネージ連携

P/Invokeは、.NETのアンマネージドコード連携の手段の一つです。もう一つはCOM連携ですが、本稿では割愛します。

P/Invoke

P/InvokeはCLRに組み込まれている、ネイティブコードライブラリとの連携機能です。また、C#などのコンパイラは、P/Invokeを簡便なシンタックスで利用可能にしています。例えば、以下のコードは、C#から直にWin32 APIを呼び出します:

public static class Program
{
    // Win32デバッグメッセージ出力API
    [DllImport("kernel32.dll", CharSet=CharSet.Unicode)]
    private static extern void OutputDebugString(string lpOutputString);

    public static void Main(string[] args) =>
        OutputDebugString("Hello P/Invoke!");
}

ネイティブコードのライブラリがダイナミックリンクライブラリとして用意されていれば、全く同じ手法でコードを記述することが出来ます。LinuxやMacOSといったプラットフォーム向けのコードは、それぞれで利用可能なネイティブコードライブラリが異なることが多々ありますが、P/Invoke自体の使用方法は同じです。.NET Coreやmono向けのライブラリも、同じように作ることができます。特に、ネイティブコードライブラリがクロスプラットフォームで(dll、dylib、soなど、ライブラリの形式が異なっても)同じAPIを提供していれば、同一のP/Invokeコードがそれぞれのプラットフォームで実行できます。

前章で述べた、インターフェイス分離設計やBait and switchテクニックをP/Invokeと組み合わせると、.NETでマルチプラットフォームの一貫したライブラリ設計を行うことができます。つまり、前章の依存コードの部分をP/Invokeを使って実装すれば、そのプラットフォーム固有のAPIがネイティブコードであったとしても、部品として使える共通のアセンブリを構築できる、と言うことです。

Part3_PInvokeプロジェクトは、これまでの説明を踏まえた、マルチプラットフォームアプリケーションです:

  • コマンドライン引数に指定した文字列が、デバッグメッセージとしてシステムに送信される。
  • デバッグメッセージは以下のように処理される:
    • Bait and switchテクニックを使用する。Windows用とLinux用で、実装は異なるが共通のメタデータを持つアセンブリを用意する。
    • Windows環境の場合、Win32のOutputDebugString APIを使用して出力する。これはSysinternalsのDebugViewユーティリティで確認できるほか、各種デバッガがアタッチされていれば、デバッガ上で確認できる。
    • Linux環境の場合、syslog APIを使用して出力する。

注意: 一般的な開発でこの程度の規模であれば、Bait and switchテクニックを使用する理由は全くありません。ここでは、後述のランタイムサイドの解説に関係するため、あえてBait and switchで実装を行っています。

DebugMessageプロジェクトは、DebugMessage.Referenceの参照アセンブリを使ってビルドします。実際に必要なのは、実行用のアセンブリ、DebugMessage.Win32とDebugMessage.Linuxです。前章のサンプルと同じく、これらを実行時に置き換える必要があります。

以下はWindows環境でWin32.batを実行したときの、DebugViewユーティリティの出力です:

Part3_DebugMessage1

以下はWSL1のUbunt 18.04環境でlinux.shを実行したときの、/var/log/syslogの出力です (syslogを使うため、あらかじめservice rsyslog startでrsyslogを動かしておく必要があります):

Part3_DebugMessage2

ガベージコレクションの影響

P/Invokeを初めて使う場合、ネイティブコード実行中の.NETインスタンスがどのように扱われるのか、疑問に思う方も居ると思います。例えば、.NETで生成したインスタンスをP/Invokeを通じてネイティブコードに渡した場合、ガベージコレクタが意図せず回収する可能性は無いのか、という点です。

例えば、以下の疑似コード:

// foo.dll内のReadToBuffer APIを呼び出す
// extern "C" void ReadToBuffer(uint8_t* pBuffer, int32_t size);
[DllImport("foo.dll")]
private static extern void ReadToBuffer(byte[] buffer, int size);

public void Read()
{
    var buffer = new byte[100];

    // byte配列は自動的にマーシャリングされる。
    // API呼び出し中にbufferがGCによって回収されることは無いのか?
    ReadToBuffer(buffer, buffer.Length);

    // (A)
    var data0 = buffer[0];
}

ガベージコレクタは、引数・ローカル変数・フィールドなどからインスタンスが参照されているかどうかを追跡しています。この場合、bufferのインスタンスが回収されてしまうことはありません。一般的に、APIが実行を終えて戻ってきたとき(A)、当然bufferにアクセス出来ることが期待できます。

(一般的には、マーシャリングという用語は、データ形式の相互変換とスレッドなどのコンテキスト切り替えの両方の意味で使いますが、本稿では前者のデータ形式の相互変換にのみ使用します。)

ガベージコレクタがインスタンスを回収するかどうかの他にも、ネイティブコードとして考慮すべき点があります。それは、インスタンスの物理的なアドレス(ポインタ値、但し仮想メモリ上の)が変わるのか同じ位置にあるのか、ということです。ガベージコレクタの手法にもよりますが、.NET CLRの場合は、ヒープコンパクションと呼ばれる機能により、必要に応じてインスタンスが移動する事があります。これらをまとめると、以下のようになります:

維持される 回収されるかも知れない
移動しない Pinned
移動するかもしれない Normal Weak

ネイティブコードライブラリを設計する場合は、CLRの伺い知れないところで動作するため、通常、インスタンスが維持され、かつインスタンスが移動しないこと(Pinned)が要求されます。稀にインスタンスが存在し続けて欲しいが、インスタンス自体にはアクセスしない(Normal)、という場合もあります(Weakについては本資料では割愛します)。

先程説明したように、インスタンスが追跡可能な状態であれば、何もしなくてもNormalと同様に維持されます。インスタンスがどこからも追跡できなくなるような状況下では、明示的にNormalまたはPinnedとしてCLRに通知する必要があります。

GCHandle構造体を使用すると、追跡可能かどうかにかかわらず、PinnedやNormalの状態を作り出すことができます。Pinnedとした場合は、インスタンスへの生のポインタを取得できます。例えば:

// foo.dll内のReadToBuffer APIを呼び出す
// extern "C" void ReadToBuffer(uint8_t* pBuffer, int32_t size);
[DllImport("foo.dll")]
private static extern void ReadToBuffer(IntPtr buffer, int size);

public void Read()
{
    var buffer = new byte[100];

    // GCHandleを使って生のポインタを得る(手動マーシャリング)
    // 生のポインタを得るには、アドレスが固定されなければならない(Pinned)
    var bufferHandle = GCHandle.Alloc(buffer, GCHandleType.Pinned);

    try
    {
        // pBufferは配列の先頭を示す、本物のアドレス
        // (この他にToIntPtrメソッドがあるが、これはGCHandleの抽象表現であり、ポインタとしては使えない)
        var pBuffer = bufferHandle.AddrOfPinnedObject();
        ReadToBuffer(pBuffer, buffer.Length);
    }
    finally
    {
        // 不要になったら解放する必要がある。解放しなかった場合はメモリリークする。
        bufferHandle.Free();
    }
}

このコードは、先程の例とほぼ同じです。P/Invoke呼び出しのためのポインタへのマーシャリングを自動で行うか、手動で行うかの違いです。以下に特徴を示します:

手法 メリット デメリット
自動マーシャリング 安全で簡潔に記述できる マーシャリングのタイミングをコントロールできないため、パフォーマンスの問題につながることがある
手動マーシャリング マーシャリングのタイミングをコントロールできるため、パフォーマンスを最大化出来る ガベージコレクタに誤って解放されたり、メモリリークしないように注意を払う必要がある

例えば、ネイティブライブラリの関数によっては、引数として渡されたポインタを保持して、ずっと後になってから参照して使うこともあります。そのような場合には、最初に明示的にNormalとし、明らかにネイティブコードで使用されなくなるまで保持し、最終的に解放する必要があります。

また、バッファへのポインタが頻繁に必要になる場合は、自動マーシャリングするよりも、一度だけ手動マーシャリングしてPinnedとし、連続してポインタを使用し、最後に手動で解放したほうが効率的かもしれません。

Part4_Marshalingプロジェクトは、手動マーシャリング処理の例です:

  • Win32NativeLibraryプロジェクトは、Visual C++のネイティブプロジェクトで、GenerateData APIを公開します。GenerateData APIは指定されたバッファアドレスに、数列を書き込みます。
  • BothMarshalingTypesプロジェクトから、P/InvokeでGenerateData APIを呼び出します。
  • この呼び出しの、自動マーシャリングと手動マーシャリングの例を示します。
  • 両者の処理時間差を、簡易的に計測します。具体的な結果は、環境に依存するためここでは示しませんが、自動マーシャリング > 手動マーシャリング、となっている事を確かめることが出来ると思います。
    • 両者の差はかなり小さいはずです。なぜなら、配列のマーシャリングコストはそれほど高くないからです。

この例では、非常に単純なデータ構造(配列)ですが、実際のマーシャリングシナリオはもっと複雑です。場合によっては、手動マーシャリングを行わないと、現実的なパフォーマンスを引き出せない可能性があります。マーシャリングの詳細については、「プラットフォーム呼び出しによるデータのマーシャリング」を参照してください。

以下に、P/Invokeを使用する際に、暗黙または意図して注意する必要のある事項をまとめます:

  • マーシャリングを自動で行うのか、手動で行うのか、及びパフォーマンスへの影響。
  • ガベージコレクタによって意図せずインスタンスが移動・解放されないようにする、あるいは正しく解放されるようにする。

ランタイムサイド

ここまでは、一般の開発者がマルチプラットフォーム対応アプリケーションを設計する場合において考慮すべき点を、如何に依存コードを分離して実現するか、という視点で解説しました。

この節では、.NET自身が、マルチプラットフォーム対応するためにどのような手段を使用しているのかを述べます。つまり、.NET Coreにおいて、coreclrやcorefxがどのようにマルチプラットフォームを実現しているのかという点に注目します。

基本的に、今まで述べてきたテクニックが利用できる部分では、そのまま利用できます。例えば、System.IO.FileStreamクラスは、ファイルへのアクセスにWindowsとLinuxで異なるAPIを使用するはずです。しかし、インターフェイスで分離されているわけではないため、Bait and switchを利用して切り分け、P/InvokeでそれぞれのAPIを呼び出している、と想像出来ます。

System.Private.CoreLib

かつて、.NET Frameworkでは、BCL (Base class library)を、「mscorlib.dll」という単一のアセンブリで担っていました。

.NET Core 1.0のリリースにあたり、.NET Frameworkの歴史で巨大化してしまったmscorlib.dllを分割するために、前節で例示した「System.Runtime.dll」と「System.Private.CoreLib.dll」、その他の細かいアセンブリ群に細分化しました。特に、.NET CoreCLRランタイムと密接に絡む実装が含まれるアセンブリが、System.Private.CoreLibアセンブリです。

Part5_Diagram1

この図は、Part1_SplitByInterfaceプロジェクトのCalculator.Core.dllをILSpyで確認したものです。左上がCalculator.Coreアセンブリで、netstandardアセンブリに依存しています。netstandardアセンブリはかなり多くのアセンブリに依存していますが、その中にSystem.Runtimeアセンブリが存在します。

System.RuntimeアセンブリはSystem.Private.CoreLibアセンブリに依存し、ここから先に依存する.NETのアセンブリはありません。そして、System名前空間を確認すると、見慣れたArray,Boolean,Byte,Charといった型が定義されていることがわかります。

  • 余談: monoは、.NET Framework 1.0が出てから数年でプロジェクトが始まっていますが、アセンブリ参照の厳密名さえ同じであれば、実体が別のアセンブリをロードして実行できるという性質を利用して、mscorlib.dllという同じ名前を使いつつ中身はmono独自に実装されている、別のアセンブリをロードすることで、.NET Frameworkとの互換性を実現していました。マルチプラットフォーム動作の実現については、後述するQCallやFCallに類似する仕組みを用いて、同一の実装アセンブリで実現していました。ただし、monoの実装は、corefxのコード取り込み作業が継続的に進んでいることもあって、流動的です。

System.Private.CoreLibアセンブリ内には、かなり多くのクラスや構造体が定義されています。これらの型のメソッドは、そのプラットフォームに依存した処理が実装されているはずです。例として、System.Diagnostics.Debugクラスを追ってみましょう:

Debug.Write(string)

internal static Action<string> s_WriteCore = WriteCore;

public static void Write(string message)
{
    lock (s_lock)
    {
        if (message == null)
        {
            // 空文字を出力
            s_WriteCore(string.Empty);
        }
        else
        {
            if (s_needIndent)
            {
                message = GetIndentString() + message;
                s_needIndent = false;
            }
            // 引数の文字列を出力
            s_WriteCore(message);
            if (message.EndsWith(Environment.NewLine))
            {
                s_needIndent = true;
            }
        }
    }
}

付加機能のためのコードが前後にありますが、結局s_WriteCore(message)でメッセージを出力しているようです。このフィールドはActionデリゲートで、初期化時にはWriteCore(string)メソッドを指しています。

Debug.WriteCore(string)

private static void WriteCore(string message)
{
    lock (s_ForLock)
    {
        if (message == null || message.Length <= 4091)
        {
            WriteToDebugger(message);
        }
        else
        {
            int i;
            for (i = 0; i < message.Length - 4091; i += 4091)
            {
                WriteToDebugger(message.Substring(i, 4091));
            }
            WriteToDebugger(message.Substring(i));
        }
    }
}

引数のmessageを4091文字づつ分割して、WriteToDebugger()メソッドで出力しています (4091文字づつである理由については定かではありませんが、PAGE_SIZE * 2に収まる範囲、とかそういう事かもしれません)。

Debug.WriteToDebugger(string)

private static void WriteToDebugger(string message)
{
    if (Debugger.IsLogging())
    {
        Debugger.Log(0, null, message);
    }
    else
    {
        Interop.Kernel32.OutputDebugString(message ?? string.Empty);
    }
}

見慣れたOutputDebugStringというシンボルが出てきました。

Interop.Kernel32.OutputDebugString(string)

// Win32 API OutputDebugString
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "OutputDebugStringW", ExactSpelling = true)]
internal static extern void OutputDebugString(string message);

これで、Windows環境のSystem.Private.CoreLibアセンブリには、Win32 APIを直接呼び出す実装が含まれていることが確認できました。Linux版.NET Core SDK 2.2のSystem.Private.CoreLibアセンブリをILSpyで見ると、以下のようになっていました:

(/usr/share/dotnet/shared/Microsoft.NETCore.App/2.2.5/ 配下にあります。バージョンは各自の環境に合わせて読み替えて下さい)

private static void WriteToDebugger(string message)
{
    if (Debugger.IsLogging())
    {
        Debugger.Log(0, null, message);
    }
    else
    {
        Interop.Sys.SysLog((Interop.Sys.SysLogPriority)15, "%s", message);
    }
}
[DllImport("System.Native", EntryPoint = "SystemNative_SysLog")]
internal static extern void SysLog(SysLogPriority priority, string message, string arg1);

Windowsの方はWin32 APIを直接呼び出すようになっていましたが、Linuxの方はSystem.Nativeと付けられたネイティブライブラリのSystemNative_SysLogメソッドを呼び出しているようです。確認してみると、確かに存在します:

Part5_LinuxSystemNative

このネイティブライブラリを、「PAL (Platform abstraction layer)」と呼んでいます。歴史的に、Windowsへの実装が第一であったことからか、Win32 APIのインターフェイスを模すように作られて、今に至るようです。

corefxの実装

以上を踏まえて、ソースコードがどうなっているかを確認してみます。System.Diagnostics.Debugクラスのコードはここにあります。partial classで分割されていて、WriteToDebuggerメソッドの実装は、DebugProvider.Windows.csと、DebugProvider.Unix.csの2つのファイルに分割されています。おそらくビルド時に、Windows版とLinux版で使い分けるのでしょう。内容はILSpyで見たものと全く同じです。

ところで、.NET CoreはLinuxだけではなく、FreeBSDもサポートしています。しかし、DebugProvider.Unix.csは名前の通り、Unix環境で共通に使われるようです。ここではこれ以上掘り下げませんが、LinuxとFreeBSDで実装に違いがあるとすれば、以下の図のようにネイティブライブラリのPALの方で吸収しているのではないかと思います。

Part5_Diagram2

  • 余談: PALの呼び出しは、#if FEATURE_PALと言うプリプロセッサ指令で切り分けられていることがあります。興味深いことにこのシンボル名は、.NET Frameworkのソースコードを参照できる「referencesource.microsoft.com」でも、ところどころで見ることが出来ます。つまり、.NET Core世代ではなく、.NET Frameworkの頃から、PALによるネイティブ実装の切り替えを想定(あるいは使用)していた可能性があります。残念ながら、サイトで見ることができるソースコードはマネージド側だけなので、PALが何に移植されていたのかはわかりません。

ランタイム機能呼び出しの手段

前節では、プラットフォームごとの実装を切り分けるのに、System.Private.CoreLibアセンブリを基準として、Bait and switchで実行アセンブリが差し替わっていることを確認しました。例としてDebugクラスの実装を追いましたが、そこでは普通にP/Invoke機能を使ってネイティブライブラリを呼び出していました:

// Windows環境において、Win32 APIを呼び出すP/Invokeの定義
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "OutputDebugStringW", ExactSpelling = true)]
internal static extern void OutputDebugString(string message);

// Linux(Unix)環境において、PALの実装を呼び出すP/Invokeの定義
[DllImport("System.Native", EntryPoint = "SystemNative_SysLog")]
internal static extern void SysLog(SysLogPriority priority, string message, string arg1);

しかし、.NET CLRの内部機能を呼び出す場合は、P/Invokeではなく、別の方法を使用する必要があります。以下に、3種類の呼び出し方法QCall, FCall, HCallについて示します。それぞれの手法は異なり、目的によって使い分けられています。同じ機能を別の呼び出し方法で呼び出すことは出来ません。

QCall

QCallは、P/Invokeにかなり似ています。P/Invokeで想定できるマーシャリングは、プリミティブ型であればそのまま使えます。文字列も、自動的にLPCWSTR (const wchar_t*)でマーシャリングされます。P/Invokeの場合、文字列の返却にStringBuilderを使いますが、代わりの低コストな手段が用意されています。それに加えてP/Invokeでは実現しない、ネイティブからの例外のスローが可能です。

内部でQCall呼び出しをラップする、C#のメソッドの実装例を示します:

// QCallでネイティブコードを呼び出すための宣言
// (P/Invokeと似ているが異なる。基本的にpublicにはできない)
[DllImport("QCall", CharSet = CharSet.Unicode)]
private static extern bool Foo(
    int arg1, string arg2,
    System.Runtime.CompilerServices.StringHandleOnStack returnValue);

// ラップして安全なメソッドとして公開
public static string Foo(int arg1, string arg2)
{
    // 文字列を結果として受け取る場所を、スタックに用意する
    string returnValue = null;

    // ネイティブメソッドの呼び出し
    // スタック上のreturnValueを指すように、StringHandleOnStackが初期化される
    if (!Foo(arg1, arg2,
        System.Runtime.CompilerServices.JitHelpers.GetStringHandle(ref returnValue)))
    {
        throw new InvalidOperationException("...");
    }
    return returnValue;
}

第一に、QCallはP/Invokeと同じようにDllImport属性を使います。ライブラリ名は固定的に”QCall”を指定しておきます。ネイティブ側の実装は、マネージド側に値を返したい場合(この例のように、戻り値として文字列を返す想定)は、スタック上の文字列参照を追跡できるようにするStringHandleOnStackという型を使います。同様に、任意の参照型(objref)には、ObjectHandleOnStackを使います。これらを使うことで、P/Invokeで必要であったマーシャリングのコストを削減できます。

しかし、そもそもStringHandleOnStackや、そのインスタンスを取得するJitHelpersは、publicではありません(ソースコードはこちら)。

つまり、QCallを処理するメソッドは、ほぼ公開メソッドにはなり得ず、System.Private.CoreLibアセンブリ内に閉じている必要があります。したがって上記の例のように、QCallメソッドはprivateとし、publicなファサードメソッドを定義する必要があります。そして、当然ですが、QCallの対象となるネイティブメソッドの実装は、.NET CLR内部に用意しておく必要があります。

仮に実装した場合、以下のような体裁を持ちます:

// C++で記述する
BOOL QCALLTYPE FooNative::Foo(
    int arg1, LPCWSTR arg2,
    QCall::StringHandleOnStack returnValue)
{
    // QCall呼び出しを処理可能にするためのマクロ
    QCALL_CONTRACT;

    // QCall処理中に発生する例外が正しく伝搬するようにするマクロ
    BEGIN_QCALL;

    if (arg1 < 0)
    {
        // 例外をスローする
        COMPlusThrow(kArgumentException, L"arg1");
    }

    // 文字列もC/C++で想定するように、普通に使える
    // (つまり、文字列を示すポインタがGCによっていきなり移動したりしない。Pinned相当)
    printf("%d, %S", arg1, arg2);

    // 文字列への参照を、直接呼び出し元のスタックに設定できる
    // (P/InvokeにおけるStringBuilderほどのオーバーヘッドはないが柔軟性には欠ける)
    returnValue.Set(L"Hello");

    // QCallの後始末マクロ
    END_QCALL;

    // (BEGIN_QCALLからEND_QCALLまでの間で、returnすることは出来ない)
    return TRUE;
}

このメソッドを呼び出せるようにする(マネージド側のQCall宣言との結び付け)には、ecalllist.hヘッダファイルに対応を記述する必要があります(次に解説するFCallの宣言も、ここに記述します)。

ところで、QCallで実装したネイティブコードを実行中は、ガベージコレクションが起きる可能性が常に存在します。つまり、P/Invokeのときと同様に、ヒープに確保されたインスタンスに生ポインタでアクセスする場合は、ガベージコレクションによって移動したり回収されたりしないように、実装者が管理する必要があります。

FCall

QCallの概要を見ると、その他の呼び出し手段は必要ないように見えます。FCallがQCallと決定的に異なるのは、FCallの呼び出しを実行している間、自動的にガベージコレクタが停止することです。

P/Invokeの場合、ガベージコレクションによる破壊を回避するには、GCHandleを使ってインスタンスをPinned状態にするかNormal状態に強制することでした。QCallの場合は、QCallのためのヘルパー型を使うことで、安全に処理することができますが、そこには少なからずコストを伴います。

FCall呼び出しを使用すると、その間ガベージコレクタが動かないため、いつでも生ポインタをPinnedされているものとして扱えるようになります。当然ですが、FCallで呼び出されたメソッドの処理時間が長引くと、.NETプロセス全体に悪影響を及ぼします。

以下は、System.Stringクラスのインデクサのgetter実装です:

[IndexerName("Chars")]
public char this[int index]
{
    // System.Stringのインデクサ: get_Chars()
    // DllImportではなく、以下の属性を使う
    [MethodImpl(MethodImplOptions.InternalCall)]
    get;
}

ILSpyを定常的に使っている方は、メソッド呼び出しをたどっていくと、この属性に行き当たるのを見たかも知れません。あなたが目にしたのは、.NET CLR内部のFCallメソッドへの扉、と言うわけです。

ecalllist.hには、インデクサに対応するネイティブメソッドの対応付けが定義されています:

// ecalllist.h: get_Chars()とGetCharAt()を結びつける宣言
FCFuncStart(gStringFuncs)
    FCIntrinsic("get_Chars", COMString::GetCharAt, CORINFO_INTRINSIC_StringGetChar)
FCFuncEnd()

そして、対応するネイティブ実装です:

// C++で記述する
FCIMPL2(FC_CHAR_RET, COMString::GetCharAt,
    StringObject* str, INT32 index)    // (strは生ポインタであることに注意)
{
    // FCall呼び出しを処理可能にするためのマクロ
    FCALL_CONTRACT;

    // GCを長時間止めておく想定はなく、処理中にGCポーリングを必要としない
    FC_GC_POLL_NOT_NEEDED();

    VALIDATEOBJECT(str);
    if (str == NULL) {
        // 例外をスローする
        FCThrow(kNullReferenceException);
    }
    _ASSERTE(str->GetMethodTable() == g_pStringClass);

    // indexが文字数範囲内なら、バッファポインタから1文字を返す
    if (index >=0 && index < (INT32)str->GetStringLength()) {
        return str->GetBuffer()[index];
    }

    // 例外をスローする
    FCThrow(kIndexOutOfRangeException);
}
// (FCallのためのFCIMPLマクロの終端定義)
FCIMPLEND

この例では、対象のSystem.Stringインスタンス(つまり文字列のthis)は、str引数で参照されますが、これは生ポインタです。つまりFCallなら、ガベージコレクタがインスタンスを移動したり回収したりする心配をすることなく、生ポインタを直接操作することが出来ます。

インデクサアクセスは、上記の通りO(1)です。FCallではガベージコレクタが動かないと言っても、コードのサイズは小さいため、処理中にそのタイミングが重なる可能性はかなり低いと思われます。もし、FCallで呼び出されたメソッドが長時間の処理を必要とする場合は、途中で(問題ないタイミングで)一時的にガベージコレクションを行わせることができます。この事をGCポーリングと呼びます(上記コードでは使用していません。ここでは詳細を省きます)。

FCallとQCallの使い分けについての決定的な差は、上記の通り、ガベージコレクション操作とマーシャリングの影響を最小限に抑えたいかどうかが、判断の基準となります。あるいは、非常に密接にCLRの内部操作と連携する必要がある場合(その間にガベージコレクションが実行されては困る場合)にも、FCallを使うことがあります。

HCall

HCallはFCallとほとんど違いがありません。HCallとして実装したネイティブメソッドから例外をスローした場合に、記録されるスタックフレームにHCallのメソッドが含まれなくなります。これは、主にJITが生成したコードが使うヘルパーメソッドに使われ、例外をスローする際に内部的なメソッドを記録しないようにしています。例えば、JIT_ChkCastArrayは、配列がキャスト可能かどうかの判定を行うHCallメソッドです。キャストできない場合に例外をスローしますが、例外のスタックトレースにこのメソッドは含まれません。

まとめ

マルチプラットフォームの環境によっては、表面的に同じ操作を行うメソッドであっても、内部のネイティブコード連携では、QCall,FCall,HCall(場合によってはP/Invoke)を使い分けるということがあるかもしれません。そのような場合でも、Bait and switchテクニックで、System.Private.CoreLibアセンブリが差し替わることで、柔軟に対応できることがわかります。

Try writing code using both the Azure Sphere Development Kit and C#

This blog post’s 23th entries of “.NET, .NET Core and mono runtimes/frameworks/libraries Advent Calendar 2018 (Japanese)” and “Seeed Users group Advent Calendar 2018 (Japanese)”.

(For Japanese edition, click here / 日本語の記事はこちら)

“Azure Sphere Development Kit” is the evaluation electorical circuit board for “Azure Sphere MCU” produced by Seeed copmpany and the development SDK produced by Microsoft.

“MT3620” is a MCU chip of “Azure sphere specification” designed by MEDIATEK company. In this blog post, it evaluation board is called “MT3620.”

Currently we can design and write a program only “C language” because Azure Sphere SDK limitation, so I tried using for C# language with IL2C.

The IL2C is an open source project I started. It’s a translator for ECMA-335 CIL/MSIL to C language.

(Hosting on GitHub)

IL2C translation process illustrates in this graph. We can write a program with C# language. Then generate the .NET assembly file using the C# compiler (Roslyn).

Next step, IL2C will generate the C language source code both the header “SphereApp.h” and source code “SphereApp.c” files from the assembly file “SphereApp.dll”.

Finally we can build native binary using the target platform C compiler.

In the short word, “Write code using both the Azure Sphere Development Kit and C#” ;)

We’re able to receive advantage for using IL2C, very little footprint less than .NET Core native, ngen and mono with tiny option. I shown demonstration for IL2C on these platforms:

  • Polish notation calculator: On the Win32 native application. (Truly native apps without .NET CLR)
  • Polish notation calculator: The UEFI application. (Truly native apps without any OSes)
  • Polish notation calculator: M5Stack with ten-key block option. (The embedded apps on ESP32)
  • The kernel mode WDM driver, it’s realtime hooking and overwriting for the storage sector fragment.
  • LED arrow feedback with the accelometer sensor on micro:bit. (Both C# and F#)

That’s meaning for we can use the C# language power to any device and any platform. The goal for this blog post, I’ll add Azure Sphere in this list.

If you’ll test this post, we have to receive the device claim at first time use MT3620, I recommend reading for several Azure Sphere SDK documents.

And you can refer the total document for MT3620 in this Seeed studio site.

(By the way, since I don’t have work since next year 2019 ¯\_(ツ)_/¯ If you have a job that will use this solution, please send twitter DM (@kozy_kekyo) so I’m welcome ;)


The strategy: Analyze the LED Blinker sample SDK code

This is a part of the LED Blinker sample code for Azure Sphere development kit. (You can see all code by creating the Blinker sample project at “New solution” menu on Visual Studio 2017.)

/// <summary>
///     Main entry point for this application.
/// </summary>
int main(int argc, char *argv[])
{
    Log_Debug("Blink application starting.\n");
    if (InitPeripheralsAndHandlers() != 0) {
        terminationRequired = true;
    }

    // Use epoll to wait for events and trigger handlers, until an error or SIGTERM happens
    while (!terminationRequired) {
        if (WaitForEventAndCallHandler(epollFd) != 0) {
            terminationRequired = true;
        }
    }

    ClosePeripheralsAndHandlers();
    Log_Debug("Application exiting.\n");
    return 0;
}

I feel there are several topics:

  • We can show the logging message by the “Log_Debug” function.
  • There’re device initialization and finalizing code at the “InitPeripheralsAndHandlers” and the “ClosePeripheralsAndHandlers” functions.
  • The center code has the polling structure.

I analyze at the InitPeripheralsAndHandlers function:

static int InitPeripheralsAndHandlers(void)
{
    struct sigaction action;
    memset(&action, 0, sizeof(struct sigaction));
    action.sa_handler = TerminationHandler;
    sigaction(SIGTERM, &action, NULL);

    epollFd = CreateEpollFd();
    if (epollFd < 0) {
        return -1;
    }

    // ...

These code fragments use the POSIX signals and the epoll API. Azure Sphere contains the Linux kernel (But fully customized, distributed by Microsoft and we can’t do manipulating directly), so our application code can use these APIs.

And, we don’t immediately know where the LED is blinking… The sample contains a lot of code fragments.

I feel this Blinker sample code is too complex and painful for first step!! Because these code are overwork strictly. It contains the discarding resource, multiplex events using epoll API and using the timers. It’s noisy, but we can improve and hiding it if we use C# language abilities!

This is agenda for this post:

  1. Prepare for use the C# language.
  2. Only LED blinking.
  3. Handles button input and completes Blinker like code.
  4. Improves by C# language style.
  5. Comparison binaries between C language and C# language.

Step1: Prepare for use the C# language

This link contains the completed code on the GitHub repository. If you try this sample code, you have to clone from this repo and folloing these steps below:

  1. Enable C# language, C++ (VC++) language and NUnit3 test adapter from extension manager on Visual Studio 2017.
  2. Open the il2c.sln file and build entire solution. If you can’t success building, you can skip next step for the unit tests.
  3. Run all of the unit tests at the Test Explorer. The test execution has to past long time. (Note: It’ll download the MinGW gcc toolchain sets from internet for first time execution.)
  4. Open the samples/AzureSphere/AzureSphere.sln file and build entire solution. This solution contains three projects and it builds automated by predefined project dependencies.

The AzureSphere.sln solution contains these three projects:

  • IL2C.Runtime: Build IL2C runtime code library. It’s VC++ project.
    (It’s referrering to the runtime code file at IL2C repository, so you have to clone entire IL2C repository if you would like to build successful.)
  • MT3620Blink: The Blink project written by C# language. (This blog post focused it.)
  • MT3620App: It’s VC++ project, only deployment usage for MT3620. (Reduced not important code from SDK sample app.)

The C# project file (MT3620Blink.csproj) is written by new simple MSBuild format. We have to choice the target framework moniker values net46 and above or netcoreapp2.0.

And add reference NuGet package named “IL2C.Build.” (version 0.4.22 or above.) It’ll compile by Roslyn and translate by IL2C automatically.

IL2C will store the translated C language source files defaulted into under the directory “$(OutDir)/IL2C/” . We have to change store directory to under the MT3620App project folder, add the “IL2COutputPath” element at this csproj xml node:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net46</TargetFramework>
    <OutputType>Library</OutputType>
    <AssemblyName>MT3620Blink</AssemblyName>

    <!-- ... -->

    <!-- IL2C puts on for translated source files -->
    <IL2COutputPath>$(ProjectDir)../MT3620App/Generated</IL2COutputPath>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="IL2C.Build" Version="0.4.6" />
  </ItemGroup>
</Project>

The interoperability topics for IL2C

.NET supports interoperability technologies named “P/Invoke.” It’s standard for .NET world and supports by IL2C. But one more option has IL2C named “IL2C/Invoke.”

IL2C/Invoke is easier interoperability for the C language world. For example, if we would like to use “OutputDebugString” the debugger API at the Windows using the P/Invoke, following below:

public static class InteroperabilityDemonstration
{
    // Write message to the debugger console
    [DllImport("kernel32.dll", EntryPoint = "OutputDebugStringW", CharSet = CharSet.Unicode)]
    private static extern void OutputDebugString(string message);

    public static void WriteMessageToDebugger(string message)
    {
        // We can direct call the native API using standard .NET method rules.
        OutputDebugString(message);
    }
}

The “DllImport” attribute understands by .NET CLR (.NET Core CLR and mono runtime), and the runtime will analyze and call bypassing to the native API inside the “kernel32.dll” dynamic link library.

It’s same as using IL2C/Invoke:

public static class InteroperabilityDemonstration
{
    // Write message to the debugger console
    [NativeMethod("windows.h", SymbolName = "OutputDebugStringW", CharSet = NativeCharSet.Unicode)]
    [MethodImpl(MethodImplOptions.InternalCall)]
    private static extern void OutputDebugString(string message);

    public static void WriteMessageToDebugger(string message)
    {
        // We can direct call the native API using standard .NET method rules.
        OutputDebugString(message);
    }
}

If we uses IL2C/Invoke, replace the DllImport attribute to the “NativeMethod” attribute and we have to set the C language header file name at the attribute argument.

Because IL2C translates to the C language source code. It has to refer to the API sets via C header files. The NativeMethod attribute translates to the “#include” preprocessor directive.

(And unfortunately, we have to apply with “MethodImpl” attribute because the C# compiler (Roslyn) requires it if method declaration has “extern”.)

IL2C/Invoke likes as P/Invoke for now, but it has advantages, will describe it later.

IL2C/Invoke first example

We can use Azure Sphere SDK with IL2C/Invoke. For first try, will cover the “nanosleep” API:

namespace MT3620Blink
{
  [NativeType("time.h", SymbolName = "struct timespec")]
  internal struct timespec
  {
      public int tv_sec;
      public int tv_nsec;
  }

  public static class Program
  {
      [NativeMethod("time.h")]
      [MethodImpl(MethodImplOptions.InternalCall)]
      private static extern void nanosleep(ref timespec time, ref timespec remains);

      public static int Main()
      {
          var sleepTime = new timespec { tv_sec = 1 };
          var dummy = new timespec();
        
          while (true)
          {
              nanosleep(ref sleepTime, ref dummy);
          }
      }
  }
}

The nanosleep API declarated into the “time.h” header from Azure Sphere SDK. It has the pointer of “timespec” structure type. We have to replace the pointer argument to the ref attribute same as P/Invoke.

And declares timespec value type inside .NET world. We have to apply the “NativeType” attribute to timespec value type. The defaulted name “timespec” overwrites to the symbol named “struct timespec”.

The defaulted name becomes from .NET type name, but SDK’s timespec structure type is NOT typedef’ed, so we have to apply prefix “struct” keyword before name.

It’s IL2C/Invoke advantage. P/Invoke requires binary layout equality both C structure types and the .NET value types. It’s difficult for average developer.

If NativeType attribute applies for the .NET value type, IL2C doesn’t generate the structure fields and places typedef’ed alias to the C structure type by only symbol naming. For example, This is illustrates for IL2C translated C source code (noise stripped):

#include <time.h>

// The alias for "struct timespec"
typedef struct timespec MT3620Blink_timespec;

void MT3620Blink_Program_nanosleep(MT3620Blink_timespec* time, MT3620Blink_timespec* remains)
{
    nanosleep(time, remains);
}

int32_t MT3620Blink_Program_Main(void)
{
    MT3620Blink_timespec sleepTime = { 1 };
    MT3620Blink_timespec dummy;
    // ...
    MT3620Blink_Program_nanosleep(&sleepTime, &dummy);
    // ...
}

The string type (System.String) translates to NULL terminated “wchar_t*”. I’ll support passing “char*” if apply UTF8 flag at the NativeMethod’s CharSet property, and will support StringBuilder class too.

Step2: Only LED blinking

We’re able to finish the Blink code with the GPIO API. It’s illustrated:

namespace MT3620Blink
{
  [NativeType("applibs/gpio.h")]
  internal enum GPIO_OutputMode_Type
  {
      GPIO_OutputMode_PushPull = 0,
      GPIO_OutputMode_OpenDrain = 1,
      GPIO_OutputMode_OpenSource = 2
  }

  [NativeType("applibs/gpio.h")]
  internal enum GPIO_Value_Type
  {
      GPIO_Value_Low = 0,
      GPIO_Value_High = 1
  }

  public static class Program
  {
      [NativeValue("mt3620_rdb.h")]
      private static readonly int MT3620_RDB_LED1_RED;

      [NativeMethod("applibs/gpio.h")]
      [MethodImpl(MethodImplOptions.InternalCall)]
      private static extern int GPIO_OpenAsOutput(
          int gpioId, GPIO_OutputMode_Type outputMode, GPIO_Value_Type initialValue);

      [NativeMethod("applibs/gpio.h")]
      [MethodImpl(MethodImplOptions.InternalCall)]
      private static extern int GPIO_SetValue(int gpioFd, GPIO_Value_Type value);

      public static int Main()
      {
          var fd = GPIO_OpenAsOutput(
              MT3620_RDB_LED1_RED,
              GPIO_OutputMode_Type.GPIO_OutputMode_PushPull,
              GPIO_Value_Type.GPIO_Value_High);
          var flag = false;

          while (true)
          {
              GPIO_SetValue(fd, flag ? GPIO_Value_Type.GPIO_Value_High : GPIO_Value_Type.GPIO_Value_Low);
              flag = !flag;

              // nanosleep(...);
          }
      }
  }
}

The GPIO output manipulates indirectly from GPIO_SetValue API. It returns the “descriptor” value by 32bit integer. It equals System.Int32 type on the .NET world.

IL2C/Invoke third feature, the “NativeValue” attribute can refer the C language world symbols (macro and anything) directly. But it has limitation because causes C# compiler’s “uninitialized field” warning. Future release for IL2C, maybe it cancel or replace for another methods.

We can blink LEDs by enum type values both GPIO_Value_High and GPIO_Value_Low.

The NativeType attribute is usable too when apply to the enum type. These examples apply to enum types both GPIO_OutputMode_Type and GPIO_Value_Type. Unfortunately, IL2C requires applying to the real numeric value at these enum value field. I’ll improve removing it for future release.

These interoperability code fragment is too ugly for the application layer. We can refactor moving to another class or appling partial class by the C# feature. IL2C doesn’t concern and not problem for using these technics, because it handles only CIL/MSIL (intermediate language.)

Our interoperability code moved to the “Interops” class.

Step3: Handles button input and completes Blinker like code

Next step for the button input, we can handle it with both GPIO_OpenAsInput and GPIO_GetValue API:

internal static class Interops
{
    // ...

    [NativeValue("mt3620_rdb.h")]
    public static readonly int MT3620_RDB_BUTTON_A;

    [NativeMethod("applibs/gpio.h")]
    [MethodImpl(MethodImplOptions.InternalCall)]
    public static extern int GPIO_OpenAsInput(int gpioId);

    [NativeMethod("applibs/gpio.h")]
    [MethodImpl(MethodImplOptions.InternalCall)]
    public static extern int GPIO_GetValue(int gpioFd, out GPIO_Value_Type value);
}

If we defined the arguments with “ref” or “out” attributes, IL2C will translate to the C language pointer type. But in this case, GPIO_GetValue’s argument is only output direction. So we can apply “out” attribute. It’s able to use with “out var” C# style syntax sugar.

Completed this step:

// Wait for nsec
private static void sleep(int nsec)
{
    var sleepTime = new timespec { tv_nsec = nsec };
    Interops.nanosleep(ref sleepTime, out var dummy);
}

public static int Main()
{
    var ledFd = Interops.GPIO_OpenAsOutput(
        Interops.MT3620_RDB_LED1_RED,
        GPIO_OutputMode_Type.GPIO_OutputMode_PushPull,
        GPIO_Value_Type.GPIO_Value_High);

    var buttonFd = Interops.GPIO_OpenAsInput(
        Interops.MT3620_RDB_BUTTON_A);

    var flag = false;

    // Interval durations (nsec)
    var blinkIntervals = new[] { 125_000_000, 250_000_000, 500_000_000 };
    var blinkIntervalIndex = 0;

    var lastButtonValue = GPIO_Value_Type.GPIO_Value_High;

    while (true)
    {
        Interops.GPIO_SetValue(
            ledFd,
            flag ? GPIO_Value_Type.GPIO_Value_High : GPIO_Value_Type.GPIO_Value_Low);
        flag = !flag;

        // Read button state: we can write with "out var"
        Interops.GPIO_GetValue(buttonFd, out var buttonValue);
        // If changed button state for last reading
        if (buttonValue != lastButtonValue)
        {
            // If current button state is pushing?
            if (buttonValue == GPIO_Value_Type.GPIO_Value_Low)
            {
                // Change interval duration for next value
                blinkIntervalIndex = (blinkIntervalIndex + 1) % blinkIntervals.Length;
            }
        }
        lastButtonValue = buttonValue;

        sleep(blinkIntervals[blinkIntervalIndex]);
    }
}

GPIO_GetValue API is nonblocking-reader. So we have to monitor it has changed.

This code doesn’t have any error test. For example if calls GPIO_OpenAsInput and the API failing, we have to check return value for API. I know we can throw the exception.

Current code applied the ac2018-step1 tag on GitHub.

Step4: Improves by C# language style

It’s bit good. We can use some Visual Studio C# IDE ability at Azure Sphere development. With full power intellisense, refactoring and a lot of extensions.

But maybe you’ll feel not fun, because these code fragments are simply replaced from C language to C# language. Further we have to write additional interoperability declarations.

Azure Sphere SDK’s Blinker sample has one of more topic. Blinker sample uses multiplexing technics by the “epoll” API for timer events both detector the button trigger and LED blinking. (It’s too complex because these reason.)

Our code has a minor issue, can’t detect button trigger at “stop the world” if code call the nanosleep API.

We try to fix these problems with the .NET and C# power!

This graph is the .NET types of we’ll make. We’ll declarate some classes and a interface type and construct these derived topology.

The “Descriptor” class is base class for managing the descriptor value.

The “Application” class receives the instance of IEPollListener interface and will register using epoll API, and aggregates event triggers and dispatching.

The “GpioBlinker” class handles LED blinking by the timer event. The “GpioPoller” class handles LED blinking by the timer event too. Both classes derived from the “Timer” class. It handles timer event and do callback.

Let’s walk through!

First topic, the all APIs handle with the “descriptor value” from opening APIs. We declare the Descriptor class below:

public abstract class Descriptor : IDisposable
{
    public Descriptor(int fd)
    {
        if (fd < 0)
        {
            throw new Exception("Invalid descriptor: " + fd);
        }
        this.Identity = fd;
    }

    public virtual void Dispose()
    {
        if (this.Identity >= 0)
        {
            Interops.close(this.Identity);
            this.Identity = -1;
        }
    }

    protected int Identity { get; private set; }
}

This class marks the base class and implements the “System.IDisposable” interface type. Because the descriptor value is the unmanaged resource, we can use RAII technics by the C# languauge “using” clause.

And throw the exception if descriptor value isn’t valid.

We can implement classes both the GPIO input/output where derive from the Descriptor class:

internal sealed class GpioOutput : Descriptor
{
    public GpioOutput(int gpioId, GPIO_OutputMode_Type type, bool initialValue)
        : base(Interops.GPIO_OpenAsOutput(
            gpioId,
            type,
            initialValue ? GPIO_Value_Type.GPIO_Value_High : GPIO_Value_Type.GPIO_Value_Low))
    {
    }

    public void SetValue(bool value) =>
        Interops.GPIO_SetValue(
            this.Identity,
            value ? GPIO_Value_Type.GPIO_Value_High : GPIO_Value_Type.GPIO_Value_Low);
}

internal sealed class GpioInput : Descriptor
{
    public GpioInput(int gpioId)
        : base(Interops.GPIO_OpenAsInput(gpioId))
    {
    }

    public bool Value
    {
        get
        {
            Interops.GPIO_GetValue(this.Identity, out var value);
            return value == GPIO_Value_Type.GPIO_Value_High;
        }
    }
}

It uses our Interops class. The descriptor value will safe free at the inherited Dispose method.

Next step, the timer function does into a class. The class implements the “IEPollListener” interface, it will use receiving the timer event by the epoll API.

// Receives the timer event by the epoll API
public interface IEPollListener
{
    // The descriptor for managing by epoll API
    int Identity { get; }
    // The callback method for raise the event
    void OnRaised();
}

“Timer” class has to implement this interface, so it can receive event from the epoll API:

internal abstract class Timer : Descriptor, IEPollListener
{
    [NativeValue("time.h")]
    private static readonly int CLOCK_MONOTONIC;
    [NativeValue("time.h")]
    private static readonly int TFD_NONBLOCK;

    protected Timer()
        : base(Interops.timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK))
    {
    }

    public void SetInterval(long nsec)
    {
        var tm = new timespec
        {
            tv_sec = (int)(nsec / 1_000_000_000L),
            tv_nsec = (int)(nsec % 1_000_000_000L)
        };
        var newValue = new itimerspec
        {
            it_value = tm,
            it_interval = tm
        };

        Interops.timerfd_settime(this.Identity, 0, ref newValue, out var dummy);
    }

    // The descriptor for managing by epoll API
    int IEPollListener.Identity => this.Identity;

    // The callback method for raise the event
    void IEPollListener.OnRaised()
    {
        // Consume current timer event.
        Interops.timerfd_read(this.Identity, out var timerData,(UIntPtr)(sizeof(ulong)));
        // Invoke the overrided callback method.
        Raised();
    }

    // The abstract method for timer triggered (Will override)
    protected abstract void Raised();
}

We gonna do derive from it and override the “Raised” method, we can write interval action easier.

The “Application” class handles all events using the epoll APIs and invoke to the target instance implemented IEPollListener interface. The epoll APIs relate for the descriptor value too, so we can inherit from the Descriptor class:

public sealed class Application : Descriptor
{
    public Application()
        : base(Interops.epoll_create1(0))
    {
    }

    // The IEPollListener implemented instance register the epoll API
    public void RegisterDescriptor(IEPollListener target)
    {
        // "Pinned (Fixed)" the instance and get the handle value
        GCHandle handle = GCHandle.Alloc(target, GCHandleType.Pinned);

        // The informations (with the handle) for the epoll API
        var ev = new epoll_event {
            events = Interops.EPOLLIN,
            data = new epoll_data_t { ptr = GCHandle.ToIntPtr(handle) }
        };

        // Register it
        Interops.epoll_ctl(
            this.Identity,
            Interops.EPOLL_CTL_ADD,
            target.Identity,
            ref ev);
    }

    // Unregister the instance
    public void UnregisterDescriptor(IEPollListener target)
    {
        // ...
    }

    public void Run()
    {
        while (true)
        {
            var ev = new epoll_event();
            var numEventsOccurred = Interops.epoll_wait(this.Identity, ref ev, 1, -1);

            if (numEventsOccurred == -1)
            {
                break;
            }
            if (numEventsOccurred == 1)
            {
                // Reconstruct (restore) the handle value uses GCHandle.ToIntPtr method
                GCHandle handle = GCHandle.FromIntPtr(ev.data.ptr);
                // Get the instance reference from the handle value
                var target = (IEPollListener)handle.Target;
                target.OnRaised();
            }
        }
    }
}

It’s same as usage the Windows Forms and the WPF, so I applied this name. It exposes the “RegisterDescriptor” and the “UnregisterDescriptor” methods, these methods register descriptor value sending from he IEPollListener interface to the epoll API. And the “Run” method observes these epoll events.

Maybe you would like to hear what and how works these code fragments. The topic is the “GCHandle” value type. It saves instance references from the garbage collector actions.

This blog post doesn’t explain IL2C’s internal structures, it has the mark-sweep algorithm based garbage collector. If we do “new a class instance”, it’ll place into the heap memory (using the “malloc” function the C runtime.)

The mark-sweep algorithm doesn’t compact memory (not move instance), but automatically free the unreferenced instances (by the “free” function.)

It has two problems at this point:

  • Transfer the instance reference pointer safely.
  • And guards freeing the floating instance by the garbage collector.

(If we use at truly the .NET CLR, .NET Core and mono, we have to save memory compaction. But IL2C doesn’t do it.) These problems can fix using GCHandle value type.

At first, we guard freeing instance, using GCHandle.Alloc method with GCHandle.Pinned argument. The instance isn’t freed even if unreferenced from IL2C.

Next, we can get the real pointer for the instance using the GCHandle.ToIntPtr method. It’s the “System.IntPtr” value type (means “intptr_t” type at the C language.) We can’t any action for this pointer value, it’s opaque but the alias number for this instance.

We must understand the instance absolutely not released if pinned by the GCHandle.Alloc method. It can free again with call the GCHandle.Free method strictly. If we don’t free it, may leak memory. (For more information, see the UnregisterDescriptor method.)

The pointer from the “GCHandle.ToIntPtr”, store into the epoll_data_t structure (nested into the epoll_event structure.) This type applied the NativeType attribute. (The magic shown you again, it’s the union type for C language. IL2C/Invoke easier for use it.):

[NativeType("sys/epoll.h")]
internal struct epoll_data_t
{
    // The epoll_data_t.ptr field is "void*" type on the C language
    public NativePointer ptr;
}

[NativeType("sys/epoll.h", SymbolName = "struct epoll_event")]
internal struct epoll_event
{
    public uint events;
    public epoll_data_t data;
}

The “ptr” field declared by the “NativePointer” type. It real C language type is “void*”, but the C# language can’t use it at the field. We can use NativePointer type in this case, it can transfer from/to the System.IntPtr type.

Then, the related information will store into the “epoll_event” structure if the “epoll_wait” API receives a event. It equals for registered by the “epoll_ctl” API. Therefore we can get the handle value from the “ptr” field using the “GCHandle.FromIntPtr” method.

The instance reference will come from the “GCHandle.Target” property. It type is the System.Object, so can cast to the IEPollListener interface. We made it!

The last step, we have to call the “IEPollListener.OnRaised” method, it delegates to the real class implementation. This is the “Timer.OnRaised” method.

These are the infrastructures. Finally we get the entire Blink samples using it:

public static class Program
{
    private sealed class GpioBlinker : Timer
    {
        private readonly long[] blinkIntervals = new[] { 125_000_000L, 250_000_000L, 500_000_000L };
        private readonly GpioOutput output;
        private bool flag;
        private int blinkIntervalIndex;

        public GpioBlinker(int gpioId)
        {
            output = new GpioOutput(
                gpioId,
                GPIO_OutputMode_Type.GPIO_OutputMode_PushPull,
                true);
            this.NextInterval();
        }

        public override void Dispose()
        {
            base.Dispose();
            output.Dispose();
        }

        protected override void Raised()
        {
            output.SetValue(flag);
            flag = !flag;
        }

        public void NextInterval()
        {
            this.SetInterval(blinkIntervals[blinkIntervalIndex]);

            blinkIntervalIndex++;
            blinkIntervalIndex %= 3;
        }
    }

    private sealed class GpioPoller : Timer
    {
        private readonly GpioInput input;
        private readonly GpioBlinker blinker;
        private bool last;

        public GpioPoller(int gpioId, GpioBlinker blinker)
        {
            input = new GpioInput(gpioId);
            last = input.Value;
            this.blinker = blinker;
            this.SetInterval(100_000_000L);
        }

        public override void Dispose()
        {
            base.Dispose();
            input.Dispose();
        }

        protected override void Raised()
        {
            var current = input.Value;
            if (current != last)
            {
                if (!current)
                {
                    blinker.NextInterval();
                }
            }
            last = current;
        }
    }

    public static int Main()
    {
        using (var epoll = new Application())
        {
            using (var ledBlinker = new GpioBlinker(Interops.MT3620_RDB_LED1_RED))
            {
                using (var buttonPoller = new GpioPoller(Interops.MT3620_RDB_BUTTON_A, ledBlinker))
                {
                    epoll.RegisterDescriptor(ledBlinker);
                    epoll.RegisterDescriptor(buttonPoller);

                    epoll.Run();
                }
            }
        }

        return 0;
    }
}

The “GpioBlinker” class blinks the LED, the “GpioPoller” class detects clicking the button and controlling blinking rate by the GpioBlinker. And the “Main” method registers both class instances by the Application class and dispatches the events.

A lot of code fragments are the infrastructure, so we are hard to understand it. But it can the simplist implementation for at the “Program” class. We can design the only concern (We designed LED blinker in this post, you forgot? ;)

The infrastructure can make the library (The .NET assembly.) We can package by the NuGet and can expose it better reusable.

Finished. These code in the ac2018-step2 tag in the repository.

Step5: Comparison binaries between C language and C# language.

Finally, comparison the compiled binary footprint between generated by IL2C and handmaded C source code.

The binary file come from the gcc (not deployment image.) For example location “$(OutDir)/Mt3620App.out” and disable debugger informations using the gcc option “-g0”:

Debug Release
SDK Blinker (C sample code) 16KB 16KB
IL2C Step1 (Non OOP) 111KB 42KB
IL2C Step2 (OOP) 142KB 54KB

The SDK Blinker size same as both Debug and Release. It’s maybe minimum and simplest code, the gcc optimizer can’t do it. The optimizer did hardworks at both IL2C Step1 and Step2. I predict it by IL2C design.

IL2C outputs are simpler (but easier detects optimizing topics at the C compiler) and predicts eliminating the unused symbols/references by the gcc options (-fdata-sections -ffunction-sections -Wl,–gc-sections).

The IL2C’s runtime code is small now, but gonna do larger if merge the corefx library. Even so, this premise is important because we want binaries to contain only the minimum necessary code.

Another design, IL2C uses the standard C runtime library as much as we can. Because larger the footprint if IL2C implements self-designed functions.

For example, the “System.String” class has to the string manipulation methods. IL2C’s runtime delegates to the C library called “wcslen”, “wcscat” and etc. And the garbage collector only uses strategy for mark-sweep with the “malloc” and “free” standard C heap backends. (It means don’t use memory compaction technics.)

However, it is still somewhat large compared to the size of SDK Blinker. It is still feeling that effort is missing…

I still have various ideas to make footprint smaller, so I will try it in a future version.

Conclusion

I tried to write modernized C# style code using IL2C on the Azure Sphere MCU based MT3620 evaluation board. I already did feedback known problems and suggestions. The “Dogfooding” makes better and improves the projects.

The likely projects come the world. It uses the LLVM based. It makes C code from Roslyn abstract syntax tree directly. It’s bootable independent OS with C#, it surprised me.

The IL2C different to these projects. It’ll have and focus the portability and footprint. It attracts the .NET powers to the everywhere.

Azure Sphere Development Kitで C# を使ってコードを書いてみる

この記事は、.NET, .NET Core, monoのランタイム・フレームワーク・ライブラリ Advent Calendar 2018と、Seeed UG Advent Calendar 2018の23日目のダブルエントリーです(遅刻)。

(For English edition, click here)

Azure Sphere Development Kitとは、 Seeedさんが出しているAzure Sphere評価ボード と、開発用SDKのことです。リンクはこのSeeed公式のページからでも買えますが、国内にも代理店がいくつかあるので、今ならそちらから買うほうが入手性も良いでしょう。

MT3620とは、 MEDIATEK社が開発した、Azure Sphereの仕様に対応したMCU(CPUチップ) です。そして、上記のSeeedさんが販売しているボードはMT3620を載せた、評価用の開発ボードです。なので、ちょっと語弊あるのですが、この評価ボードの事を、以降MT3620と呼びます。

で、何でこれをAdvent Calendarのネタにしたかと言うと、現状のAzure Sphere SDKは、「C言語」でのみ開発が可能というもので… .NET界隈の読者はもう言わなくてもわかりますよね ;)

Seeed UGから来られた方は、タイトルにはC言語じゃなくてC#って書いてあるんだけど? と、若干意味不明に思うかも知れないので、以下に背景を書いておきます。

私のblogをちょっと遡ると 「IL2C」 に関する記事が沢山出てきますが、これは私が始めた「.NETのバイナリをC言語のソースコードに変換する」オープンソースのプロジェクトの事です。まだ完成していなくて、開発途上です。

開発を始めたのは、丁度1年半ぐらい前からです。1年前には、 .NET Conf 2017 とか、いくつかの勉強会やグロサミやらで細部とか進捗とかを発表していました。今年はつい先日開催した dotNET600 2018 でも発表しています。あと、YouTubeに大量にIL2Cを解説したビデオを公開しています。

資料などはそれぞれの勉強会サイトやGitHubをあたってもらえると良いと思いますが、ざっくり言うと、この図のとおりです。

C#でプログラムを書いて(もちろん、MT3620向けに)、それを普通にC#のプロジェクトとしてビルドします。そうすると、”SphereApp.dll”のような.NETのバイナリが出来ます。これをIL2Cに掛けると、C言語のソースコードに変換されたファイルが生成されます。これらのソースコードファイルを、ターゲットデバイスのC言語コンパイラ(この記事ではAzure Sphere SDKが提供するgcc)に掛けて、デバイス用のネイティブバイナリを生成します。

つまり簡単に言えば、この記事のタイトル通り、「Azure Sphere Development Kitで C# を使ってコードを書く」と言うことになるわけです。

IL2Cの最大の特徴であり目標は、例えば.NET Coreのネイティブビルドやngen、monoの極小ビルドと比較しても、到底不可能なぐらいにフットプリントを小さく出来て、内部構造がシンプルで、ネイティブコードとの連携をコントローラブルにする、というものです。これまでにデモンストレーションで作ってみたものとしては:

  • 簡易電卓: Win32ネイティブコンソールアプリケーション(つまり.NET CLRを使わず、Win32ネイティブで動く)
  • 簡易電卓: UEFIアプリケーション(つまりOSなしで動く電卓をC#で書いたって事です)
  • 簡易電卓: M5Stack テンキーブロック(ESP32上で組み込みソフトウェアとして)
  • ディスクのセクタデータをリアルタイムに書き換える、カーネルモードWDM(つまりWindowsカーネルモードで動くコードをC#で書いたって事です)
  • micro:bitで、加速度センサーを使用してLEDに方向をフィードバックするプログラム(これはC#だけではなく、F#でもデモしました)

という感じで、極小リソースのデバイスでも.NET ILを変換したネイティブコードを、インタプリタではなくAOTコンパイルしたコードで動かせるという実証や、UEFIやWDMなどの極めて特殊な環境でも動作するという実証実験をしています。今回はここに、Azure Sphereを加えるという目標です。

ということで、

  • MT3620のセットアップの流れ
  • サンプルコードの構造の確認とC#での実現方法

の大きく二章立て構成で解説します。

(ところで、来年から仕事無いので、これをやらせてくれる仕事ありましたら、凄く嬉しいのでDMください)


MT3620のセットアップの流れ

MT3620は割と早い時期に入手していたのですが、(某人がこれの環境構築に四苦八苦してたのを横目で見てたこともあり…)色々忙しくて放置してました。なので、今の時期にデバイスクレーム(MT3620の登録作業)からやる話は、同様にこれから始める人向けにも良いんじゃないかと思ってます。

@linyixian さんの、Azure Sphere 開発ボードでLチカをやってみる が非常に参考になりました。ほぼこの通りで出来ましたが、私固有の問題が少しあったので、ログ含めてセットアップの記録を残しておきます。

なお、使用したAzure Sphere SDKのバージョンは “18.11.3.23845” でした。SDKもそこそこバージョンアップされているので、あまりバージョンがかけ離れると、ここに書いてあることも無意味になるかも知れません。

さて、Azure AD面倒くさいんですが、組織アカウントを作っておくところまでは、記事通りやっておきます。私の場合、個人的にいくつかのサブスクリプションと組織が紐付いている状態で実験したりしていた関係で、シンプルに無料チャージから始めた方とは事情が違ってしまっている可能性があります。Azure ADはいつ作ったか作ってないのかわからないものが一つだけあったので、この中に組織アカウントを作りました(管理的に問題のある選択だったかも知れない… 消さないようにしなければ)。

Azure ADに組織アカウントを作ったら、Azure Sphereテナントを作ります。テナントはAzure Sphere SDKのコマンドプロンプトで作業します。つまり、ここからの操作はAzureのダッシュボードは不要です。が…

C:\>azsphere tenant create -n ***********
error: An unexpected problem occurred. Please try again; if the issue persists, please refer to aka.ms/azurespheresupport for troubleshooting suggestions and support.
error: Command failed in 00:00:39.0889958.

何故か失敗(以下、ログはIDとか伏せ字です)…

しかも意味不明なエラーメッセージなので、こりゃ早速サポート案件か… と思ったのですが、@linyixian さんのLチカ記事読み直すと、この時点でMT3620をUSB接続していたようなので(テナント生成にデバイス関係あるの?という疑問は残るものの)MT3620を接続して試したら、うまく行きました:

C:\>azsphere tenant create -n ***********
warn: Your device's Azure Sphere OS version (TP4.2.1) is deprecated. Recover your device using 'azsphere device recover' and try again. See aka.ms/AzureSphereUpgradeGuidance for further advice and support.
Created a new Azure Sphere tenant:
 --> Tenant Name: ***********
 --> Tenant ID:   ***********
Selected Azure Sphere tenant '***********' as the default.
You may now wish to claim the attached device into this tenant using 'azsphere device claim'.
Command completed successfully in 00:00:23.2329116.

(そう言えばMatsuoka氏も最初の頃そんな事を言っていた事を思い出した)

ただ、上記のように、私のMT3620はファームウェアが古すぎだと言われた(まあ、放ったらかしにしてたしね)ので、早速suggestされたコマンドで更新をかけました。

C:\>azsphere device recover
Starting device recovery. Please note that this may take up to 10 minutes.
Board found. Sending recovery bootloader.
Erasing flash.
Sending images.
Sending image 1 of 16.
Sending image 2 of 16.
Sending image 3 of 16.
Sending image 4 of 16.
Sending image 5 of 16.
Sending image 6 of 16.
Sending image 7 of 16.
Sending image 8 of 16.
Sending image 9 of 16.
Sending image 10 of 16.
Sending image 11 of 16.
Sending image 12 of 16.
Sending image 13 of 16.
Sending image 14 of 16.
Sending image 15 of 16.
Sending image 16 of 16.
Finished writing images; rebooting board.
Device ID: ***********
Device recovered successfully.
Command completed successfully in 00:02:41.2721537.

難なく、更新されたようです。では、いよいよ後戻りできない、MT3620のデバイスクレームを実行します:

C:\>azsphere device claim
Claiming device.
Successfully claimed device ID '***********' into tenant '***********' with ID '***********'.
Command completed successfully in 00:00:04.6818769.

これで、このMT3620はこのAzure Sphereテナントに登録されてしまったので、もう誰にも譲れなくなりました… (Azure Sphere MCU対応デバイスは、デバイスクレームを一度しか実行できません。詳しくはググってみてください)

さて、気を取り直して、WiFi出来るようにしておきます。

C:\>azsphere device wifi add --ssid *********** --key ***********
Add network succeeded:
ID                  : 0
SSID                : ***********
Configuration state : enabled
Connection state    : unknown
Security state      : psk

Command completed successfully in 00:00:01.7714741.

WiFi接続ができているかどうかを確認

C:\>azsphere device wifi show-status
SSID                : ***********
Configuration state : enabled
Connection state    : connected
Security state      : psk
Frequency           : 5500
Mode                : station
Key management      : WPA2-PSK
WPA State           : COMPLETED
IP Address          : ***********
MAC Address         : ***********

Command completed successfully in 00:00:01.0424492.

いい感じです。一度動き出せば、管理は非常に簡単です。WiFi構成はいつでも上記コマンドで変更できます。

最後に、MT3620を「開発モード」にします。

MT3620自体はDevKitなので、文字通り「開発」に使わないのなら何に使うんだ? ということで、この操作は意味不明に思われるかも知れません(特にArduinoやESP32のようなプラットフォームを触っていると)。冒頭で触れたように、元々のAzure Sphere MCUは、様々な組み込み機器のCPUとして使われることを想定しており、真にIoTデバイスとして使う場合は、逆に開発モードと言う危なげな状態にならないほうが良いわけです。

この辺は、最近のスマートフォンでも同じように制限を掛けていると思うので、それと同じように考えればわかりやすいと思います。では、開発モードに変更します:

C:\>azsphere device prep-debug
Getting device capability configuration for application development.
Downloading device capability configuration for device ID '***********'.
Successfully downloaded device capability configuration.
Successfully wrote device capability configuration file 'C:\Users\k\AppData\Local\Temp\tmp6CD5.tmp'.
Setting device group ID '***********' for device with ID '***********'.
Successfully disabled over-the-air updates.
Enabling application development capability on attached device.
Applying device capability configuration to device.
Successfully applied device capability configuration to device.
The device is rebooting.
Installing debugging server to device.
Deploying 'C:\Program Files (x86)\Microsoft Azure Sphere SDK\DebugTools\gdbserver.imagepackage' to the attached device.
Image package 'C:\Program Files (x86)\Microsoft Azure Sphere SDK\DebugTools\gdbserver.imagepackage' has been deployed to the attached device.
Application development capability enabled.
Successfully set up device '***********' for application development, and disabled over-the-air updates.
Command completed successfully in 00:00:32.3129153.

これで開発モードに切り替えできました。

後は、@linyixian さんのLチカの記事通り、Blinkサンプルを実行してLチカ出来る、言い換えればDevKitの用を成しているかどうかをを確認しておきます。

これで、MT3620が使える状態になったかどうかの確認は完了です。

DevKitの詳細については、

を参照すると良いでしょう。


サンプルコードの構造の確認とC#での実現方法

で、ここからが本題なのですが、とりあえず先程のBlinkのサンプル(このサンプルはあくまでC言語)を眺めてみると、以下のような定義が見つかります:

/// <summary>
///     Main entry point for this application.
/// </summary>
int main(int argc, char *argv[])
{
    Log_Debug("Blink application starting.\n");
    if (InitPeripheralsAndHandlers() != 0) {
        terminationRequired = true;
    }

    // Use epoll to wait for events and trigger handlers, until an error or SIGTERM happens
    while (!terminationRequired) {
        if (WaitForEventAndCallHandler(epollFd) != 0) {
            terminationRequired = true;
        }
    }

    ClosePeripheralsAndHandlers();
    Log_Debug("Application exiting.\n");
    return 0;
}

なんだか、至って普通の、なんのヒネリもないmain関数です。あと、いくつか気がつくことがあります:

  • Log_Debug関数でログが出せそう。
  • InitPeripheralsAndHandlersとClosePeripheralsAndHandlersでデバイスの初期化処理と終了処理らしきことをしている。これはSDKの関数ではなく、このサンプル内にコードがあるので、すぐ後で見てみます。
  • ど真ん中にポーリングのようなコードがある。コメントに”SIGTERM”とかコメントがある。

ぐらいですかね。InitPeripheralsAndHandlersを見てみると:

static int InitPeripheralsAndHandlers(void)
{
    struct sigaction action;
    memset(&action, 0, sizeof(struct sigaction));
    action.sa_handler = TerminationHandler;
    sigaction(SIGTERM, &action, NULL);

    epollFd = CreateEpollFd();
    if (epollFd < 0) {
        return -1;
    }

    // ...

えぇ… POSIX signal使えるんだ… ちょっと予想外だった。いや、やや不透明ですが、内部ではLinuxカーネルが動いているので、当然といえば当然かも知れません。同様に、main関数のポーリングのようなコードは、epollと関係あるのかも知れません。

また、C言語なのである程度は仕方がないのですが、質素なLチカのサンプルコードなのにグローバル変数がてんこ盛りかつ複雑なので、これで何かしようというやる気が初っ端からへし折られていく感じがします… (一応擁護しておくなら、頑張ってきれいに書こうとしている匂いはします)

では、そろそろ今回やろうとしていることの本題に入りましょうか。

  1. 本格的に何か出来るようにしようとすると、Advent Calendarでは収集つかなくなりそうなので、Blink同様のものを目標にします。
  2. 真のLチカ(つまり、LEDの点滅のみ)をやってみます。
  3. ボタン入力に対応させてみます。
  4. C#らしいコードにします。
  5. 生成されたコードを比較します。

ビルド環境の準備

完成したサンプルコードはここにあります。

上記サンプルコードを動かす場合は、リポジトリ全体をcloneした後、以下のように準備しておきます。

  1. Visual Studio 2017にC#, C++(VC++), NUnit3 test adapter(拡張機能から)が入っている必要があります。普通はVC++入れないと思いますが、Azure Sphere SDKを入れてあるなら入っているでしょう。
  2. ルートにあるil2c.slnを開いてビルドします。どうしてもビルドが通らない場合は、3のテストは諦めることも出来ます(.NET Framework 4.0以降・.NET Core 1.0以降・.NET Standard 1.0以降の全てのバージョンに依存しているため、環境によってはビルドできないかも知れません。Visual Studio Installerを起動して、とにかく全てのコンポーネントをインストールしてみるのはありかも知れません。私は常に全部入りです)。
  3. Test Explorerからテストを実行して全部パスすることを確認しておきます(結構時間がかかります。CPUコア数多いと速いです。i7-4790Kで5分ぐらい。あと、1GBぐらいのディスクスペースが必要で、初回実行時に一度だけMinGWのgcc toolchainをダウンロードするため、遅い回線だと辛いかもしれません)。masterブランチには安定的に動作するコードが入っているはずですが、テストを実行しておけば、少なくとも私が見た結果と同じものが得られるはずです。
  4. samples/AzureSphere/AzureSphere.slnを開いてビルドしてください。このソリューリョンには3つのプロジェクトが含まれていますが、依存関係が正しく設定されているはずなので、ビルドすれば全てが正しく構築されるはずです。

AzureSphere.slnソリューションに含まれるプロジェクトは、以下のとおりです:

  • IL2C.Runtime: IL2Cのランタイムコードをライブラリ化するVC++プロジェクト
    (このプロジェクトはIL2Cリポジトリに含まれるランタイムのソースファイルを参照しているので、このプロジェクトをビルドするためにリポジトリ全体が必要です)
  • MT3620Blink: C#で書かれたBlinkのプロジェクト(以降で説明するC#で書くコード)
  • MT3620App: MT3620向けのデプロイ用VC++プロジェクト(SDKのサンプルコードから不要なコードを省いたものです)

以降の解説は、上記プロジェクトのうちのC# Blinkのプロジェクトを一から書く場合の解説です。

このC#プロジェクトは、いわゆるMSBuildの新形式(.NET Coreや.NET Standardで使われている、新しいシンプルなcsprojファイル)ライブラリプロジェクトを使って、net46以上かnetcoreapp2.0以降をターゲットにします。そして、NuGetパッケージのIL2C.Build 0.4.22以降(この記事を書いた時点の最新)を参照してください。

これでビルド時に裏でIL2Cが自動的に実行され、C言語のソースコードが出力されるようになります(IL2Cのバイナリを明示的にインストールしたりする必要はありません)。

(説明が前後しますが、C#プロジェクト自体は、IL2C.Buildパッケージを参照するだけで作ることが出来ます。今回、IL2Cのリポジトリ全体をcloneするのは、IL2C.Runtimeのビルドのためです。この辺りはスマートではない事は認知しているので、将来的にはもっと簡単に出来るようにしたいと思っています)

C言語のソースコードは、デフォルトでは$(OutDir)/IL2C/の下に出力されます。今回は、別のVC++プロジェクト配下のGeneratedフォルダに出力したいため、csprojのPropertyGroupに以下のように1行加えておきます:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net46</TargetFramework>
    <OutputType>Library</OutputType>
    <AssemblyName>MT3620Blink</AssemblyName>

    <!-- ... -->

    <!-- C言語ソースコードの出力先フォルダを指定 -->
    <IL2COutputPath>$(ProjectDir)../MT3620App/Generated</IL2COutputPath>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="IL2C.Build" Version="0.4.6" />
  </ItemGroup>
</Project>

なお、旧形式のMSBuild、つまり.NET Frameworkで使われているcsprojのライブラリプロジェクトでも出来ますが、IL2C.Interopパッケージが正しく参照されないため、手動でこのパッケージも追加する必要があります。

また、net46より古いか、netstandard辺りのプラットフォームをターゲットとすることも出来ますが、現状のIL2C.Buildではビルドできないので、IL2C.exeを自力で呼び出す必要があります(PostBuildEventに書きます。引数の種類は限られているのでそれほど難しくはありませんが、ここでは省略します)。

IL2CのInteroperability(相互運用機能)

.NETの世界で Interoperability(相互運用)と言うと、.NETのコードから、ネイティブのコード(例えばWin32やLinuxのAPI)を呼び出したりする事を指します。.NETのコードは、全てが.NETだけで実現されているわけではなく、例えばファイルの入出力やネットワークアクセスなどは、OSのAPIを呼び出して実現する必要があります。

普通のプログラムは、これらの「外部リソース」にアクセスします。したがって、如何に簡単にプログラムの処理系からAPIを呼び出せるかが鍵となります。.NETの場合、標準の相互運用機能に P/Invoke と呼ばれる機能があり、これを使って相互運用を実現します。

例えば、WindowsのWin32 APIにはデバッグAPIがあって、デバッグ用の文字列を出力することが出来ます(デバッガのログに表示されます)。これを.NET上から呼び出すには、以下のようなコードを書きます:

public static class InteroperabilityDemonstration
{
    // Win32 APIのデバッガAPIへの呼び出しを定義する
    [DllImport("kernel32.dll", EntryPoint = "OutputDebugStringW", CharSet = CharSet.Unicode)]
    private static extern void OutputDebugString(string message);

    public static void WriteMessageToDebugger(string message)
    {
        // デバッガに文字列を出力する
        OutputDebugString(message);
    }
}

詳細は省きますが、DllImportという属性を適用したメソッドは、対応するWin32 APIと紐付けられ、まるで.NETにそのメソッドが存在するかのように呼び出すことが出来るようになります。

例としてWin32 APIを挙げましたが、.NET Coreやmonoで想定されるマルチプラットフォーム(つまりLinuxやMac)でも、全く同じ方法でネイティブのAPIを呼び出すことが出来ます。

IL2Cでは、P/Invokeを踏襲した相互運用機能を実現するのと同時に、独自の相互運用機能も用意しています(注:現状、どちらの実装もまだ完全ではありません)。この、独自の相互運用機能(便宜上、IL2C/Invokeと呼びます)で同じ事を実現する場合は、以下のように書きます:

public static class InteroperabilityDemonstration
{
    // Win32 APIのデバッガAPIへの呼び出しを定義する
    [NativeMethod("windows.h", SymbolName = "OutputDebugStringW", CharSet = NativeCharSet.Unicode)]
    [MethodImpl(MethodImplOptions.InternalCall)]
    private static extern void OutputDebugString(string message);

    public static void WriteMessageToDebugger(string message)
    {
        // デバッガに文字列を出力する
        OutputDebugString(message);
    }
}

何故、IL2C/Invokeを用意したのかは、このコードを注意深く見ると気がつくかも知れません。P/InvokeのDllImport属性は、対象のAPIが「ダイナミックリンクライブラリ」に含まれている事を前提としています。kernel32.dllを参照することで、プログラムの実行時に指定されたライブラリを「動的」に読み込んで、APIの呼び出しを可能にします。

しかし、IL2CはC言語のソースを出力し、それらがすべて事前にコンパイルされる必要があります。参照すべきAPIもコンパイル時に解決される必要があるため、ライブラリ本体ではなく、C言語のヘッダファイルへの参照が必要なのです。当初はDllImport属性を流用していたのですが、このような理由から独自の属性を定義して区別するようにしました。また、区別したことで、後で述べるような利点も生まれています。

唯一、MethodImpl属性が邪魔ですね。これはC#コンパイラが要求していて省略できないため、どうしても一緒に書いておく必要があります。今のところ回避方法はありません。

Azure Sphere SDKのAPIを参照する

このIL2C/Invokeを使って、Azure Sphere SDKのAPIを.NETから使えるようにしてみます。まずはnanosleep APIをカバーしてみましょう。

namespace MT3620Blink
{
  [NativeType("time.h", SymbolName = "struct timespec")]
  internal struct timespec
  {
      public int tv_sec;
      public int tv_nsec;
  }

  public static class Program
  {
      [NativeMethod("time.h")]
      [MethodImpl(MethodImplOptions.InternalCall)]
      private static extern void nanosleep(ref timespec time, ref timespec remains);

      public static int Main()
      {
          var sleepTime = new timespec { tv_sec = 1 };
          var dummy = new timespec();
        
          while (true)
          {
              nanosleep(ref sleepTime, ref dummy);
          }
      }
  }
}

nanosleep APIは、Azure Sphere SDKのtime.hに定義されています。そして、引数にtimespec構造体へのポインタを取ります。P/Invokeで使われるのと同じ技法で、ポインタはrefで参照するようにします。

また、.NETから構造体を参照できるように、timespec構造体を宣言します。ここに、NativeType属性を適用して、この構造体がどのヘッダファイルに定義されているのかを宣言し、合わせて実際の構造体の宣言名(ここでは “struct timespec”)をSymbolNameプロパティで適用しておきます。

宣言名を省略すれば、.NET構造体の名前がそのまま使用されます。しかし、SDKのtimespec構造体はtypedefで別名宣言されていないため、”struct”の接頭語を付けておく必要があります(Azure Sphere SDKはC++をサポートしていないので、この場合は接頭語を省略できません)。

先程、IL2C/Invokeには利点があるという話をしましたが、このNativeType属性が正にそれです。P/Invokeの場合、定義する構造体の型は、バイナリレイアウトを含めて完全な状態で再定義しなければならず、比較的難易度の高い作業でした。

NativeType属性が適用された構造体は、基本的にシンボル名と型が合っていれば、問題なくネイティブコードと結合できます。何故なら、NET構造体の定義はIL2Cによって無視され、C言語の構造体を(定義が同一であるという前提で)そのまま使うからです。

以下に、変換結果を概念的にしたものを示します:

#include <time.h>

// struct timespec構造体の別名を定義
typedef struct timespec MT3620Blink_timespec;

void MT3620Blink_Program_nanosleep(MT3620Blink_timespec* time, MT3620Blink_timespec* remains)
{
    nanosleep(time, remains);
}

int32_t MT3620Blink_Program_Main(void)
{
    MT3620Blink_timespec sleepTime = { 1 };
    MT3620Blink_timespec dummy;
    // ...
    MT3620Blink_Program_nanosleep(&sleepTime, &dummy);
    // ...
}

C言語のソースコード中に、timespec構造体の詳細な定義が無く、typedefを使って本物のtimespec構造体にバイパスしているだけです。

なお、文字列(.NETのSystem.String型)は、NULLで終端されている wchar_t* が渡されるようになっています。NativeMethod属性のCharSetにUTF8を指定することで char* を渡すことも出来るようにしたり、P/Invokeと同じように、StringBuilderクラスを使ったやり取りも出来るようにする予定です。

GPIO APIをカバーする

GPIO APIをカバーして、Lチカを完成させます。以下に、GPIOにアクセスする部分だけを抜き出したC#のコードを載せます:

namespace MT3620Blink
{
  [NativeType("applibs/gpio.h")]
  internal enum GPIO_OutputMode_Type
  {
      GPIO_OutputMode_PushPull = 0,
      GPIO_OutputMode_OpenDrain = 1,
      GPIO_OutputMode_OpenSource = 2
  }

  [NativeType("applibs/gpio.h")]
  internal enum GPIO_Value_Type
  {
      GPIO_Value_Low = 0,
      GPIO_Value_High = 1
  }

  public static class Program
  {
      [NativeValue("mt3620_rdb.h")]
      private static readonly int MT3620_RDB_LED1_RED;

      [NativeMethod("applibs/gpio.h")]
      [MethodImpl(MethodImplOptions.InternalCall)]
      private static extern int GPIO_OpenAsOutput(
          int gpioId, GPIO_OutputMode_Type outputMode, GPIO_Value_Type initialValue);

      [NativeMethod("applibs/gpio.h")]
      [MethodImpl(MethodImplOptions.InternalCall)]
      private static extern int GPIO_SetValue(int gpioFd, GPIO_Value_Type value);

      public static int Main()
      {
          var fd = GPIO_OpenAsOutput(
              MT3620_RDB_LED1_RED,
              GPIO_OutputMode_Type.GPIO_OutputMode_PushPull,
              GPIO_Value_Type.GPIO_Value_High);
          var flag = false;

          while (true)
          {
              GPIO_SetValue(fd, flag ? GPIO_Value_Type.GPIO_Value_High : GPIO_Value_Type.GPIO_Value_Low);
              flag = !flag;

              // nanosleep(...);
          }
      }
  }
}

MT3620のGPIOは、GPIO_OpenAsOutput APIで出力用にデバイスをオープンして、その時得られたディスクリプタをGPIO_SetValue 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メソッドがスッキリする」と想像出来ると思います。

実際、別のクラスに定義を移動しても問題ありませんし、フットプリントが気になるならpartial classを使えば良いでしょう。IL2CはC#のソースファイルではなく、コンパイルされたアセンブリファイル(MT3620Blink.dll)に対して処理を行うからです。

(というように各自で工夫してくれると良いなーと思ってたのですが、Matsuoka氏に見せたら初見で同じことをつぶやいたので、まあ作戦は成功したと言えるでしょう :)

サンプルコードでは、Interopsというクラスを作り、そこに移動しました。

横道にそれましたが、ボタン入力をGPIO APIで取得するには、GPIO_OpenAsInput APIでオープンしてから、GPIO_GetValue APIを使います。これらは以下のように定義できます:

internal static class Interops
{
    // ...

    [NativeValue("mt3620_rdb.h")]
    public static readonly int MT3620_RDB_BUTTON_A;

    [NativeMethod("applibs/gpio.h")]
    [MethodImpl(MethodImplOptions.InternalCall)]
    public static extern int GPIO_OpenAsInput(int gpioId);

    [NativeMethod("applibs/gpio.h")]
    [MethodImpl(MethodImplOptions.InternalCall)]
    public static extern int GPIO_GetValue(int gpioFd, out GPIO_Value_Type value);
}

IL2Cでは、ref引数でもout引数でもポインタ渡しになります。しかし、GPIO_GetValueは(ポインタ経由で)値を読み取ることは無いのでoutにしておきます。すると、メソッドを使う側ではout varを使って簡単に書けるようになります。nanosleepも直しておきます。全体的なコードは以下の通りです:

// 指定されたnsecだけ待機する
private static void sleep(int nsec)
{
    var sleepTime = new timespec { tv_nsec = nsec };
    Interops.nanosleep(ref sleepTime, out var dummy);
}

public static int Main()
{
    var ledFd = Interops.GPIO_OpenAsOutput(
        Interops.MT3620_RDB_LED1_RED,
        GPIO_OutputMode_Type.GPIO_OutputMode_PushPull,
        GPIO_Value_Type.GPIO_Value_High);

    var buttonFd = Interops.GPIO_OpenAsInput(
        Interops.MT3620_RDB_BUTTON_A);

    var flag = false;

    // 待機時間(nsec)
    var blinkIntervals = new[] { 125_000_000, 250_000_000, 500_000_000 };
    var blinkIntervalIndex = 0;

    var lastButtonValue = GPIO_Value_Type.GPIO_Value_High;

    while (true)
    {
        Interops.GPIO_SetValue(
            ledFd,
            flag ? GPIO_Value_Type.GPIO_Value_High : GPIO_Value_Type.GPIO_Value_Low);
        flag = !flag;

        // ボタンの状態を読み取る: out varで簡単に書ける
        Interops.GPIO_GetValue(buttonFd, out var buttonValue);
        // 直前のボタンの状態から変化していれば
        if (buttonValue != lastButtonValue)
        {
            // ボタンが押されていれば(ボタンの信号は論理が逆なので注意)
            if (buttonValue == GPIO_Value_Type.GPIO_Value_Low)
            {
                // 待機時間を変更
                blinkIntervalIndex = (blinkIntervalIndex + 1) % blinkIntervals.Length;
            }
        }
        lastButtonValue = buttonValue;

        sleep(blinkIntervals[blinkIntervalIndex]);
    }
}

ボタンの状態は瞬時に読み取られるので、連続的に待機時間が変わってしまわないように、ボタン状態の変化を見るようにしています。

Azure Sphere SDKのBlinkerサンプルは、ディスクリプタの取得に失敗した場合(-1が返されるなど)の処理もきちんと実装していますが、このコードでは省略しています。もしやるならC#らしく、例外を定義してスローするという方法が考えられます。

なお、ここまでのコードは ac2018-step1のタグ で保存してあります。

応用: epollを使って疑似イベント駆動にする

最初に取り組むLチカ+αの課題はこれで十分だと思いますが、C#を使っているのにC言語でプログラムを書いているのとあまり変わりがなく、IL2C/Invokeのための記述が増えるだけで面白みがありませんね。

また、Azure Sphere SDKのBlinkerサンプルは、実装がもう一捻りしてあります(それであんなに複雑なことになっているのですが…)。それは、ボタンクリックのトリガーと点滅を、それぞれタイマーを割り当てて、タイマー経過をepollを使ってマルチプレクスして、擬似的なイベント駆動として処理しているのです。実際、前述のコードには些末ですが問題があります。それは、sleepを呼び出している間は、ボタンの入力が受け付けられないことです。

最後にこの処理を、.NETとC#のパワーでキレイに対処して見ましょう。

この図は、これからC#で作る型の関係を示したものです。いくつかのクラスとインターフェイスを定義して、このような継承関係を構築します。

Descriptorクラスは、このすぐ後で説明する、APIのディスクリプタを管理する基底クラスです。Applicationクラスが、IEPollListenerインターフェイスを実装するインスタンスを受け取ってepollに登録し、発生するイベントの集約と配信を行います。

GpioBlinkerとGpioPollerクラスが、LEDの点滅とボタンのトリガーを監視します。両方共にTimerクラスを継承して、インターバルタイマーの経過を使って処理を行います。

それでは、順を追って説明します。

まず、すべてのAPIへのアクセスは、共通の「ディスクリプタ」で行われている、という事実に着目し、ディスクリプタを管理するクラスを用意します:

public abstract class Descriptor : IDisposable
{
    public Descriptor(int fd)
    {
        if (fd < 0)
        {
            throw new Exception("Invalid descriptor: " + fd);
        }
        this.Identity = fd;
    }

    public virtual void Dispose()
    {
        if (this.Identity >= 0)
        {
            Interops.close(this.Identity);
            this.Identity = -1;
        }
    }

    protected int Identity { get; private set; }
}

このクラスのポイントは、基底クラスとして継承可能にしておき、IDisposableインターフェイスを実装してディスクリプタを破棄可能にします。コンストラクタで渡されたディスクリプタが負数の場合は、例外をスローしておきます。

このクラスを継承して、GPIOのIn,Outを実装してみます:

internal sealed class GpioOutput : Descriptor
{
    public GpioOutput(int gpioId, GPIO_OutputMode_Type type, bool initialValue)
        : base(Interops.GPIO_OpenAsOutput(
            gpioId,
            type,
            initialValue ? GPIO_Value_Type.GPIO_Value_High : GPIO_Value_Type.GPIO_Value_Low))
    {
    }

    public void SetValue(bool value) =>
        Interops.GPIO_SetValue(
            this.Identity,
            value ? GPIO_Value_Type.GPIO_Value_High : GPIO_Value_Type.GPIO_Value_Low);
}

internal sealed class GpioInput : Descriptor
{
    public GpioInput(int gpioId)
        : base(Interops.GPIO_OpenAsInput(gpioId))
    {
    }

    public bool Value
    {
        get
        {
            Interops.GPIO_GetValue(this.Identity, out var value);
            return value == GPIO_Value_Type.GPIO_Value_High;
        }
    }
}

各APIへのアクセスは、前章で作ったInteropsクラスの定義をそのまま流用しています。また、ディスクリプタはDisposeメソッドで解放されるため、破棄についてはここでは何もしていません。特に説明不要で理解できると思います。

次に、タイマーの処理もクラスにカプセル化してしまいましょう。その前に、以下のようなインターフェイスを用意しておきます。これは、後でepollがタイマーの経過を通知するための窓口となるものです:

// epollを使ってイベントを受信するクラスが実装するインターフェイス
public interface IEPollListener
{
    // epollで管理に使用するディスクリプタ
    int Identity { get; }
    // epollがイベントを受信した際に呼び出すコールバックのメソッド
    void OnRaised();
}

タイマークラスはこのインターフェイスを実装して、epollからのイベントを受信できるようにします:

internal abstract class Timer : Descriptor, IEPollListener
{
    [NativeValue("time.h")]
    private static readonly int CLOCK_MONOTONIC;
    [NativeValue("time.h")]
    private static readonly int TFD_NONBLOCK;

    protected Timer()
        : base(Interops.timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK))
    {
    }

    public void SetInterval(long nsec)
    {
        var tm = new timespec
        {
            tv_sec = (int)(nsec / 1_000_000_000L),
            tv_nsec = (int)(nsec % 1_000_000_000L)
        };
        var newValue = new itimerspec
        {
            it_value = tm,
            it_interval = tm
        };

        Interops.timerfd_settime(this.Identity, 0, ref newValue, out var dummy);
    }

    // epollで管理に使用するディスクリプタ
    int IEPollListener.Identity => this.Identity;

    // epollがイベントを受信した際に呼び出すコールバックのメソッド
    void IEPollListener.OnRaised()
    {
        // タイマーイベントを消化する
        Interops.timerfd_read(this.Identity, out var timerData,(UIntPtr)(sizeof(ulong)));
        // 派生クラスの実装を呼び出す
        Raised();
    }

    // タイマー経過を示す純粋仮想メソッド(継承して処理を実装)
    protected abstract void Raised();
}

タイマークラスは更に継承して、Raisedメソッドを実装することで、タイマー経過時の処理を簡単に記述できるようにしておきます。

C#で書き始めた途端に、ものすごく設計が捗るようになった気がしますね。最後にepollを処理するクラスを用意します。epoll自体もディスクリプタで管理されるので、Descriptorクラスを継承します:

public sealed class Application : Descriptor
{
    public Application()
        : base(Interops.epoll_create1(0))
    {
    }

    // 指定されたインスタンスをepollに登録する
    public void RegisterDescriptor(IEPollListener target)
    {
        // インスタンスの参照を固定する
        GCHandle handle = GCHandle.Alloc(target, GCHandleType.Pinned);

        // epollにインスタンスハンドルを結びつけるための情報
        var ev = new epoll_event {
            events = Interops.EPOLLIN,
            data = new epoll_data_t { ptr = GCHandle.ToIntPtr(handle) }
        };

        // 登録する
        Interops.epoll_ctl(
            this.Identity,
            Interops.EPOLL_CTL_ADD,
            target.Identity,
            ref ev);
    }

    // 指定されたインスタンスをepollから解除する
    public void UnregisterDescriptor(IEPollListener target)
    {
        // ...
    }

    public void Run()
    {
        while (true)
        {
            var ev = new epoll_event();
            var numEventsOccurred = Interops.epoll_wait(this.Identity, ref ev, 1, -1);

            if (numEventsOccurred == -1)
            {
                break;
            }
            if (numEventsOccurred == 1)
            {
                // GCHandle.ToIntPtrメソッドで得たポインタを使ってGCHandleを復元する
                GCHandle handle = GCHandle.FromIntPtr(ev.data.ptr);
                // GCHandleからインスタンス参照を入手する
                var target = (IEPollListener)handle.Target;
                target.OnRaised();
            }
        }
    }
}

この後で紹介しますが、このクラスはWinFormsやWPFのApplicationクラスに似た位置づけなので、同じようにApplicationクラスと名付けました。IEPollListenerインターフェイスを実装したクラスをepollに連携させるように登録・解除出来るように、RegisterDescriptorメソッドとUnregisterDescriptorメソッドを公開しています。そして、Runメソッドでepollイベントの監視を行います。

このコードには少し解説が必要でしょう。ポイントはGCHandle構造体です。この構造体はインスタンスへの参照をガベージコレクタの操作から保護します。

この記事ではあえてIL2Cの内部構造に触れてきませんでしたが、IL2Cはマークアンドスイープ方式のガベージコレクタを持っています。クラスのインスタンスをnewすると、そのインスタンスはヒープメモリ(最終的にはmalloc関数)に格納されます。マークアンドスイープ方式では、メモリコンパクションと呼ばれる操作は行われませんが、使われていないインスタンスについては自動的に解放(free関数)されます。

ここで問題が2つあります:

  • ネイティブAPIに、インスタンス参照のポインタを渡す方法
  • ネイティブAPIがそのポインタを操作する可能性がある時に、誤ってインスタンスを解放してしまわないようにする方法

本物の.NET CLRや.NET 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属性をつけてあります(実際は共用体です。IL2C/Invokeなら簡単に共用体を扱えます):

[NativeType("sys/epoll.h")]
internal struct epoll_data_t
{
    // C言語のepoll_data_t.ptrは、void*型
    public NativePointer ptr;
}

[NativeType("sys/epoll.h", SymbolName = "struct epoll_event")]
internal struct epoll_event
{
    public uint events;
    public epoll_data_t 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メソッド)。

これでインフラは全部揃いました。Blinkのサンプルとしての実装を含めた、Mainメソッドの実装です:

public static class Program
{
    private sealed class GpioBlinker : Timer
    {
        private readonly long[] blinkIntervals = new[] { 125_000_000L, 250_000_000L, 500_000_000L };
        private readonly GpioOutput output;
        private bool flag;
        private int blinkIntervalIndex;

        public GpioBlinker(int gpioId)
        {
            output = new GpioOutput(
                gpioId,
                GPIO_OutputMode_Type.GPIO_OutputMode_PushPull,
                true);
            this.NextInterval();
        }

        public override void Dispose()
        {
            base.Dispose();
            output.Dispose();
        }

        protected override void Raised()
        {
            output.SetValue(flag);
            flag = !flag;
        }

        public void NextInterval()
        {
            this.SetInterval(blinkIntervals[blinkIntervalIndex]);

            blinkIntervalIndex++;
            blinkIntervalIndex %= 3;
        }
    }

    private sealed class GpioPoller : Timer
    {
        private readonly GpioInput input;
        private readonly GpioBlinker blinker;
        private bool last;

        public GpioPoller(int gpioId, GpioBlinker blinker)
        {
            input = new GpioInput(gpioId);
            last = input.Value;
            this.blinker = blinker;
            this.SetInterval(100_000_000L);
        }

        public override void Dispose()
        {
            base.Dispose();
            input.Dispose();
        }

        protected override void Raised()
        {
            var current = input.Value;
            if (current != last)
            {
                if (!current)
                {
                    blinker.NextInterval();
                }
            }
            last = current;
        }
    }

    public static int Main()
    {
        using (var epoll = new Application())
        {
            using (var ledBlinker = new GpioBlinker(Interops.MT3620_RDB_LED1_RED))
            {
                using (var buttonPoller = new GpioPoller(Interops.MT3620_RDB_BUTTON_A, ledBlinker))
                {
                    epoll.RegisterDescriptor(ledBlinker);
                    epoll.RegisterDescriptor(buttonPoller);

                    epoll.Run();
                }
            }
        }

        return 0;
    }
}

GpioBlinkerクラスはLEDを点滅させるクラス、GpioPollerはボタン入力を検知してLEDの点滅速度を変化させるクラス、そしてMainメソッドでApplicationクラスと2つのクラスを生成して登録し、イベント処理のループに入ります。

今までインフラを作っていたので全容が見えにくかったのですが、このMainメソッドと2つのクラスだけを見れば、かなりシンプルでわかりやすいと思います。この図のように、Lチカ固有のコードにだけ関心を向けることが出来ます。

インフラの部分は、MT3620(又はAzure Sphere)のライブラリとして固めておけば、更に捗りそうです。NuGetパッケージ化すればなお良いでしょう。

これで完成です。このコードは ac2018-step2のタグ で保存してあります。

生成されたコードを比較する

最後に、IL2Cが生成するコードが、手でC言語コードを書いたものと比較してどの程度のフットプリントなのかを見てみます。

比較対象は、ビルドするとgccによって生成されるネイティブバイナリです(デプロイ用パッケージではありません)。例えば、$(OutDir)/Mt3620App.outです。なお、以下の全ての計測は、デバッグ情報を含んでいない(-g0)サイズです:

Debug Release
SDK Blinker (C sample code) 16KB 16KB
IL2C Step1 (Non OOP) 111KB 42KB
IL2C Step2 (OOP) 142KB 54KB

SDK BlinkerのサイズがDebugとReleaseで同一なのは、間違いではありません。それだけ、コードの複雑性がなく、最適化の余地は無かったということでしょう。IL2CのStep1・Step2共に、最適化がかかった場合のフットプリント縮小が劇的です。これはIL2Cの設計上狙ったとおりの結果です。

IL2Cは、比較的単調なコード(言い換えればC言語コンパイラが最適化を行いやすいコード)を出力し、かつ、gccのオプション(-fdata-sections -ffunction-sections -Wl,–gc-sections)によって、使用されていないシンボルをリンク時に削除する事を前提としています。

IL2Cのランタイムはまだ小規模ですが、やがてcorefxのマージを行うようになると、非常に大きくなることが予想されます。その場合でも、最低限必要なコードだけがバイナリに含まれて欲しいので、この前提が重要になります。

他にも、C言語標準ライブラリをできるだけ使用する、という方針もあります。独自のコードを含めば含んだだけ、フットプリントが増えてしまうからです。System.Stringの文字列操作も、内部ではC言語のwcslenやwcscatなどの関数を使ったり、マークアンドスイープガベージコレクタが、mallocやfree関数をバックエンドとして使っている(そしてコンパクションを行わない)のも、このような背景によるものです。

しかし、それでもSDK Blinkerのサイズと比較すると、やや大きいですね。不本意です。まだ努力が足りない感じです。フットプリントを小さくするアイデアは、まだ色々あるので、今後のバージョンでトライして行こうと思います。

まとめ

IL2Cを使って、Azure Sphere MCUを搭載したMT3620で、C#を使ってモダンなスタイルのコードを書くという試みを行いました。今回の試行でうまく動くようにするための技術的なフィードバックは、結構沢山出てきました(詳しくはコミットログを参照して下さい。かなり難易度高くて、初見では意味不明かもしれませんが…)。ドッグフーディングはやはりやっておくべきですね。

IL2Cのような試みは他でもいくつか行われていて、LLVM向けのものとか、Roslynの構文木からC言語のコードを生成するとか、はてはC#でブート可能なOSを作る、なんてものもあります(ググってみて下さい)。それらのコミュニティプロジェクトや、本家.NET Core・monoとの違いとしては、ポータビリティとフットプリントかなと思っています。まだまだ完成には遠い感じですが、これからも、ただの概念実験にならないようにしていきたいと思います。

Center CLR Try!開発 #3

今日は、「Center CLR Try!開発 #3」に参加ました。

クリエイトベース金山さんのスペースでやりました。冬場だからか、自分も含めて体調良くない人が居るようですね…

私はIL2Cのオブジェクト参照の追跡が足りない部分の修正と本を読む、MatsuokaさんはnanoFrameworkとMbed-Cli、neco3csさんはAngularの勉強、中村さんはNGK2018BのLT資料作成、加藤さんは非公開案件、という目標でした。

NGK2018B、今年はLT登壇エントリーしなかったので、気が楽ですわ :)


IL2C、昨日の時点で、 classとvalue typeのメソッドオーバーライド・オーバーロード・interfaceの暗黙実装・明示実装とそれぞれが複雑に絡んだパターンについてテストを書きまくって網羅した ので、長らく放置してた value type内のobjrefが追跡されていないので勝手にGCに回収されてしまう問題 を対処する、その方策を考えました。

今日中に対処できれば良いんですが、そう簡単でも無いことは分かっていたので、今日は算段をつけるという感じ。
例えば以下のようなコード: (ちょっとC#とIL混ぜちゃってますが、適当に読んでください)

public struct ObjRefInsideValueTypeType
{
    public string Value;  // <-- ここに動的に生成された文字列"ABCDEF"が保持される
    public ObjRefInsideValueTypeType(string value) => this.Value = value;
}

.class public IL2C.RuntimeSystems.ValueTypes
{
    .method public static string ObjRefInsideValueType() cil managed
    {
        .maxstack 3
        .locals init (
            [0] valuetype IL2C.RuntimeSystems.ObjRefInsideValueTypeType   // <-- IL2Cはこの構造体から上のValueフィールドを追跡できなければならない(がしていない)
    	)
        ldloca.s 0
        ldstr "ABC"
        ldstr "DEF"
        call string [mscorlib]System.String::Concat(string, string)
        call instance void IL2C.RuntimeSystems.ObjRefInsideValueTypeType::.ctor(string)

        // Release concat string from the evaluation stack
        ldstr "dummy1"
        ldstr "dummy2"   // <-- IL2Cはevaluation stackをCのローカル変数として確保するので、そこに参照が残っていると追跡されてしまうことから
        pop              //     別の参照を上書かせて追跡されないようにしてテストしている。
        pop              //     本来なら、こうしたとしても、value typeのフィールドは別の手段で追跡されなければならない(が今はダメ)

        call void [mscorlib]System.GC::Collect()
        ldloc.0
        ldfld string IL2C.RuntimeSystems.ObjRefInsideValueTypeType::Value
        ret
    }
}

で、ObjRefInsideValueType()を呼ぶとこういったケースのテストを行うのですが、構造体ObjRefInsideValueTypeType内のValueフィールドにセットされた文字列(System.String.Concatによって動的に生成されてヒープに配置された文字列のインスタンス)が、GC.Collect()によって回収されてしまうという問題です。

(文字列を結合しているのは、単なるリテラル文字列だとstatic constに配置されてしまってGCから無視されてしまうので、動的に生成させています。まあboxingされたインスタンスとか使っても良かったんですが)

とりあえずこのテストを書いて実行すれば、(IL2Cの問題によって) 文字列はGCに回収されてしまい、メソッドから返された文字列への参照 (IL2C上はポインタ) は無効な値を示していて、その後のアサートで刺さるからテストに失敗する、というシナリオです。

なんですが… 何故かテストが成功する…

で、VC++で実際にデバッグしてみると、問題なく無効ポインタで刺さったことが検出されます。IL2CのランタイムがこれをNullReferenceExceptionとしてスローする所に問題があって、想像してなかった死に方をしましたが(Unhandled exceptionはTODOで放置してたんだった…)

IL2CのテストはNUnitで書いていますが、実際には:

  1. テストコードのC#は、Roslynによって普通にアセンブリ(IL)になる
  2. アセンブリを普通に実行して、正しい結果が得られることをNUnitでアサートする
  3. アセンブリをIL2C.Coreに食わせてCソースコードを生成する
  4. Cソースコードをコンパイル(MinGW gcc4 32bitを使用)してネイティブの実行コードを生成する(今の所Windowsでやってるので、test.exe的なものが生成される)
  5. 実行コードを実際に実行する。内部ではテスト結果が同じようにアサートされる(この実行には.NET CLRは一切関与しない)。成功時は”Success”とstdoutに出しているので、それを確認

という感じで、テスト結果が完全に一致することを自動的に確認するようにしています。なので、今度はVC++とgccで結果が異なるという可能性がありえたので、VSCodeでデバッグ(C++ extensionでGDBを使ってデバッグできる)してみたのですが、こちらも正しく無効ポインタを踏んで死んでました。

んー何故なのか… と調べてるところで時間切れ。嫌な感じだな…


Try!開発は定期的にやる予定なので、興味があれば是非参加してください。

Center CLR Try!開発 #2

今日は、「Center CLR Try!開発 #2」に参加ました。

クリエイトベース金山さんのスペースを借りることが出来ました。募集が遅かったので人の集まりが悪かったのですが、参加者3人ともいろいろ吸収出来たようで良かったです。

私はIL2Cの例外処理、Matsuokaさんは次週のプレゼン資料、ayumaxさんはUnityで年末忘年会用の出し物を作る、という目標でした。


IL2Cは例外処理に取り組んでいます。
例外のうち、単純なcatch・複数のcatchの呼び分け・ネストしたcatch・rethrowと、それらそれぞれについてlocal unwind・global unwindは動いていて、finallyを変換可能にするのが今日の目標でしたがダメでした。

この図はIL内にtry-catch-finallyブロックの定義があった場合に、どのようなCのソースコードに変換すれば実現できるかを検討していたものです。ざっくり言うと、sjlj方式でsetjmpの戻り値によって例外ブロックに分岐して処理させ、finallyはそれらがbreakでブロック外に遷移したときに面倒を見る、という感じです。

(sjljの効率が悪いことは承知。移植性重視で、この作業が終わってからWindowsについてはSEHを使えるように、その先ではlibunwindを使うことも考えています)

悩んで解決できなかった部分が、最近C# 6で追加された例外フィルタ条件のサポートです。これはVB.net 1.0から使える(つまりはIL的には最初から実現可能だった)ものです。以下にC# 6で書いた例を示します:

class Program
{
    static string GetMessage(Exception ex, string banner)
    {
        Console.WriteLine(banner);
        return ex.Message;
    }
    static void Main(string[] args)
    {
        try
        {
            try
            {
                // C D B E F H
                throw new Exception("111");
                // C D [Unhandled exception]
                //throw new Exception("333");
            }
            catch (Exception ex) when (GetMessage(ex, "C") == "222")
            {
                Console.WriteLine("A");
            }
            finally
            {
                Console.WriteLine("B");
            }
            Console.WriteLine("G");
        }
        catch (Exception ex) when (GetMessage(ex, "D") == "111")
        {
            Console.WriteLine("E");
        }
        finally
        {
            Console.WriteLine("F");
        }
        Console.WriteLine("H");
    }
}

C#クイズとかで出てきそうですが、例外フィルタ式(コード上 GetMessage()を呼び出しているwhen句)の呼び出し順序を見て、のけ反る人もいるかも知れません。コメントに書いておきました。

例外がスローされるとき、

  1. 現在のスタックフレームの直近に存在するcatchブロックのうち、指定された例外型にキャスト可能なものがあるかどうかを探索し、なければよりスタックの底に向かって探索し続ける。
  2. 上記が存在する場合、更にフィルタ条件式があればそれを実行し、結果が満たされるかどうかを確認する。
  3. 1又は2が満たされてから、そのcatchブロックに遷移する。

という順序で実行する必要があります。ここで苦しいのは、スタックの底まですべての条件を確認し終えるまでは、catchブロックに遷移してはならない、という点です。したがって、まず、catch句の条件とwhenのフィルタ条件をすべてチェックする必要があります(満たされるものが見つかった時点で打ち切ることは可能)。

問題なのは、例外の型のチェックだけなら、IL2Cが自分の都合の良い判定Cコードを生成すればいいのですが、フィルタ条件式はILで指定されるので、ILを実行してフィルタ条件を満たしているかどうかを確認しなければなりません。しかし、catchブロックやfinallyブロックは、そのブロックを実行することが判明した時点でそこまでスタックを巻き戻し(sjljであればlongjmpする)ても問題ないことが自明ですが、フィルタ条件式の実行中は、まだスタックを巻き戻すことは出来ません。巻き戻してしまったらもう元に戻せず、それまでのスタック上の情報はすべて失われます(移植性を考えると、失われたと見なす必要があります)。

(上記のサンプルコードはまだ良いのですが、メソッドのローカル変数を使ったフィルタ条件式を考えてみると、問題の大きさがわかります)

これは頭を抱えました。

そもそも、例外フィルタ条件式を真面目に処理することをやめて、以下のように評価するという代替え案も考えられます:

try
{
    throw new Exception("111");
}
catch (Exception ex) // when (GetMessage(ex, "C") == "222")
{
    // Rethrow if additional condition is false
    if (!(GeMessage(ex, "C") == "222")) throw;
    Console.WriteLine("A");
}

しかし、この方法には2つの問題があります:

  • 例外ハンドラに遷移してから、更にrethrowで遷移するので、コストが高い。
  • 同じ型でcatchするブロックについて、一つのブロックで処理するように統合する必要がある。

11/11追記: これもやっぱり駄目ですね。スタック上位にfinallyブロックがある場合、このcatchに遷移した時点でfinallyを実行してしまいます。例外フィルタ式を使った場合と、実行順序が入れ替わってしまいます。

色々考えた結果、結局、以下の理由で、今その対処を行うことをやめました:

  • 現在(メソッドの)ローカル変数群のうち、オブジェクト参照を追跡する必要のある変数(GCがトラッキングして、不要なインスタンスではないことを確認する)は、そのアドレス群を “EXECUTION_FRAME”という構造体に記録し、リンクリストに追加することでGCがトラッキングできるようにしていますが、ここの実行効率が悪いことがわかっています。
  • 近い内にこれを改良する予定ですが、その際にこれらのローカル変数へのベースとなるポインタが用意に得られるようになるはずなので、フィルタ条件式のILを変換する場合には、このポインタ経由でローカル変数群にアクセス可能にすれば、スタックを一時的に巻き戻さなくとも式の評価が出来るはずです。
  • そのため、手順として、EXECUTION_FRAMEの改良が終わってから、改めて考えても良いかも?

と考えました。
なので、本日の目標としては、フィルタ条件式に対応しないが例外は処理できる、という感じに決定しました(そして、完成しなかった… 8割ぐらい?家に戻ったらfinishしたい)

ちなみに、.NET Framework CLRや.NET Core、monoとかはどうやっているのかという興味が出てきますね。自力でネイティブコードを出力してるので、如何様にでも出来るといえば出来ますが…

——-

Try!開発は定期的にやる予定なので、興味があれば是非参加してください。

ただの素人がフロー解析を通過した – 言語実装 Advent Calendar 2017

この記事は「言語実装 Advent Calendar 2017」の11日目のネタです。

今、私は「IL2C」 というプロジェクトをやっているのですが、その実装中に起きた予期せぬフロー解析を、素人なりに実装した話です。IL2Cについては以下を参照してもらえればわかりますが、.NETのIL (Intermediate Language) を、C言語ソースコードに変換します。

GitHub: IL2C step-by-step design project
YouTube: Playlist: Making archive IL2C

「AOT技術 Advent Calendar 2017」でもこれまでの内容を要約して書いてます。この記事はダブルエントリーにしました。

また、「Extensive Xamarin」 でその時点までの技術的なポイントを執筆したので、興味があれば参照して下さい… いや、買って下さい :)

※本記事は抜粋ですが、.NETにあまり明るくない方向けに、細部を省いて焦点に絞って編集してあるため、省略した解説があります。


厨二変換病

私は、IL2Cを実際に作り始めるまでは、こう考えていた —

(ゲームエンジンの)Unityには、”IL2CPP”というツールがあります。IL2CPPとは、.NETのIL(Intermediate Language)を、C++のコードに変換するツールです。ILとは、JavaのバイトコードやLLVM-IRに相当する、.NETの中間言語です。

“IL2CPP”と聞けば、ILのバイトコードを(多少面倒ではあるものの)パースし、一対一でC++のコードに変換していけば完成し、後は、ランタイムをどう実装するのかが大きなトピックだろうと想像できます。

プリミティブタイプは、C++の対応する型に置き換えればよいでしょう。.NET CLRが扱うクラスや構造体の型は、C++のクラスにマッピング出来そうな気がします。

型全体について、System.Object(.NETにおける基底クラス)と同じような基底クラスを軸に、C++でも継承関係を構築すれば、例外クラスもほぼそのまま変換できるように思えます。しかも、C++にはtry-catchが存在するため、例外ハンドリングも楽に実装できるかもしれません。

そして、C++で記述できるコードは、多少冗長になったり面倒になったりしますが、C言語だけでも実装できることを知っています。むしろツール化によって自動生成するのであれば、C言語で出力することは大した問題ではありません。

大きな問題があるとすれば、ガベージコレクションとスレッド周りですが、これらは主にランタイムとの協調が主軸となります —

しかし、IL2Cの設計を進めていくうちに、型の扱いがそれほど簡単でもないことに気が付いた…

立ちはだかる様々な問題のうち、全く想定していなかった「フロー解析」についての知見を残しておきます。同じような事を妄想しちゃった仲間のために :)


.NET CLR評価スタック

.NETのランタイムのことを、「.NET CLR」と呼びます。CLRには仮想計算器が定義されています。この計算器(CPUと読み替えてよい)は、ILのバイトコードを逐次解釈しながら、OpCodeの定義に従った計算を実行します。CLRの仮想計算器は「スタックマシン」と呼ばれている種類のアーキテクチャです。

スタックマシンとは、仮想計算器が使用するスクラッチパッドに、文字通り「スタック」構造のバッファを使います。このスタックのことを「評価スタック」と呼んでいます。例えば、以下のような3つの引数の値を加算するメソッドを見ます:

.method public hidebysig static 
    int64 Add (uint8 a, int32 b, int64 c) cil managed 
{
    ldarg.0     // (1)
    ldarg.1     // (2)
    add         // (3)
    conv.i8     //  |  (expand int32 --> int64)
    ldarg.2     // (4)
    add         // (5)
    ret         // (6)
}

これを、「擬似的」にC++ STLで書いてみます:

int64_t Add(uint8_t a, int32_t b, int64_t c)
{
    // The evaluation stack
    std::stack<object> stack();

    stack.push_back(a);   // (1)
    stack.push_back(b);   // (2)

    // (3)
    stack.push_back((int64_t)(stack.pop_back() + stack.pop_back()));

    stack.push_back(c);   // (4)

    // (5)
    stack.push_back(stack.pop_back() + stack.pop_back());

    // (6)
    return (int64_t)stack.pop_back();
}

評価スタックとは「スタック」と呼ばれるように、LIFO構造のスクラッチパッドです。ポイントは、評価スタックが「スタック」ゆえに、必ずスタックに積まれた順序を守りながら操作するということです。この点は、一般的な物理CPU、たとえばx86・x64・ARMなどのCPUと異なります。これらのCPUは「レジスタ」と呼ばれるスクラッチパッドを使い、評価スタックのようにLIFOであることを想定しません。

また、評価スタックにはどのような値も入れることが出来ます。プリミティブ型・値型・オブジェクト参照・マネージ参照などです。

以下に評価スタックの処理を示します:

  1. aの値を評価スタックにプッシュ
  2. bの値を評価スタックにプッシュ
  3. 2つの値を取り出し(ポップ)ながら加算処理を行い、結果をプッシュ
  4. cの値を評価スタックにプッシュ
  5. 2つの値を取り出し(ポップ)ながら加算処理を行い、結果をプッシュ
  6. 値を取り出し(ポップ)、呼び出し元に返す

評価スタックの正体

このC++コードには、明らかに問題のありそうな実装が含まれています。もう一度、ILと対応するC++コードを示します:

.method public hidebysig static 
    int64 Add (uint8 a, int32 b, int64 c) cil managed 
{
    ldarg.0     // uint8 a
    ldarg.1     // int32 b
    add
    conv.i8     // int32 --> int64
    ldarg.2     // int64 c
    add
    ret         // int64
}
int64_t Add(uint8_t a, int32_t b, int64_t c)
{
    // First problem: cannot declaration object type using C++
    std::stack<object> stack();

    stack.push_back(a);
    stack.push_back(b);

    // Second problem: cannot find operator +() overload
    stack.push_back(stack.pop_back() + stack.pop_back());

    stack.push_back(c);

    // Third problem: cannot find operator +() overload
    stack.push_back(stack.pop_back() + stack.pop_back());

    return (int64_t)stack.pop_back();
}

まず、std::stackのテンプレート型引数”object”が解決できません。つまり、C++にはSystem.Object(.NETにおける基底クラス)に対応するクラスが存在しないため、このコードは成り立ちません。(関連して、operator +()が解決できないという問題も存在します)

仮に、全ての型の基底となるクラスを導入すれば解決できそうな気もします(同程度の新たな問題も発生しますが、ここでは述べません)。しかし、私達の目標はC++ではなくCです。C言語で上記のコードを模倣する場合、次の方法が考えられます:

  1. std::stackと等価な、テンプレートクラスベースではないスタック操作関数を用意する
    この方法は、スタックポインタの操作(現在のスタック位置を把握する)を実装する必要があり、Cコンパイラが最適化によってこれらを不要と検出できるかどうかが鍵となります(実際のところ、スタックポインタの計算処理が含まれるため、難しいかもしれません)。しかし、結局、System.Object型をC言語でどのように扱うのかが解決されません。
  2. スタックに値が2個しか出し入れされていない前提で、それぞれをローカル変数に置き換える
    「人間の目」で確認すれば、評価スタックのインデックス(スタックに積まれた位置)毎に、何型の値を格納しているのかがわかります。

次のILの例では:

ldarg.0     // [0] int32
ldarg.1     // [1] int32    (A)
add         // [0] int32    (B)
conv.i8     // [0] int64    (C)
ldarg.2     // [1] int64    (D)
add         // [0] int64    (E)

OpCode実行後のスタックの位置とその型が定められているため、この情報を使って、次のようなCコードに変換できます:

// Evaluation stack simulated by C
int32_t stack0_0;
int32_t stack1_0;
int64_t stack0_1;
int64_t stack1_1;

// ldarg.0
stack0_0 = a;
// ldarg.1  (A)
stack1_0 = b;

// add      (B)
stack0_0 = stack0_0 + stack1_0;

// conv.i8  (C)
stack0_1 = stack0_0;

// ldarg.2  (D)
stack1_1 = c;

// add      (E)
stack0_1 = stack0_1 + stack1_1;

// ret
return stack0_1;

評価スタックの使われ方を図示すると、次のようになります:

これで、評価スタックをCコードで問題なくシミュレートできたことになります。出力されるコードの問題自体は解決しましたが、IL2Cの変換処理は非常に複雑になります。当初の想定では、リフレクションAPIで取得できるメタデータ情報とILのバイトコードを、逐一Cコードに置き換えるだけでした。

たとえば、メソッドの引数群・戻り値・ローカル変数群の型は、Typeクラスのインスタンスによって直接かつ容易に特定できます。しかし、評価スタックの操作だけは、型が明示的に指定されない(PushやPop時に型指定されない)ため、PushやPop操作を追跡し、どこでどのスタック位置をどのように使っているか、を解析しなければならないのです。


評価スタック解析の方法

ILのバイトコードを上から順に解析するのではなく、評価スタックの操作を追跡して解析しなければならないとすると、実際に仮想計算器が解析を行う挙動を模倣する必要があります。具体的には、バイトコードのエントリポイント(先頭)から順に解析を行いつつ、

  1. OpCodeのPushやPopの操作を追跡する(その際に、Pushされた型を記憶する)
  2. ブランチ命令を発見した場合は、ブランチ先を新たなフローとしてキューに保存する
  3. ret命令や、解析済みのバイトコードに到達した時点で、そのフロー解析を終了する

という動作を、キューを全て消費するまで繰り返します。ここで重要なのは、評価スタックの使用方法のチェックです。

ブランチ命令によって遷移する先のバイトコードは、すでにフローとして解析済みの可能性があります。その場合、そこから先で使用(Pop)する予定のスタックの型が、ブランチ直前のスタックの型と一致していれば、Cコード上でもまったく同じコードが使用可能であるとみなせます。しかし、スタックにPushされている値の型が異なる場合、ブランチ元で想定される型で新たにCコードを出力しなければなりません。次のコードは、手動で技巧的なILを構成した例です:

// Handmade IL code
.method public hidebysig static 
    native int Multiple10 (native int a) cil managed 
{
    .locals (
        [0] int32 count
    )

    ldc.i4.s 10
    stloc.0
    ldc.i4.0            // [0] int32   (A)

L_0000:
    ldarg.0             // [1] native int
    add                 // [0] native int  (B)
    ldloc.0
    ldc.i4.1
    sub
    stloc.0
    ldloc.0
    brtrue.s L_0000     // (C)

    ret
}

OpCodeの”add”命令は、次のような評価スタックの入力を受け付けます(他にもバリエーションがありますが省略します):

  1. int32 + int32 → int32
  2. int64 + int64 → int64
  3. int32 + native int → native int
  4. native int + native int → native int

native intとは、”System.IntPtr”のことです。これは32ビット環境では32ビット値、64ビット環境では64ビット値を保持します。(但し、現在のIL2Cは、まだnative intをサポートしていません)

ここで問題なのは、(B)で計算されるadd命令が、初回は(A)により3)であり、次回以降が4)として処理されることです。こういったコードはC#では書くことができませんが、ILとしては有効です。上記の判定は、ILのバイトコードをデコードした、静的な情報だけではわからないため、フローを解析し、

  1. 最初のフローで(A)によって、スタック[0]位置がint32として扱われていること
  2. (C)のブランチによってL_0000へ遷移する新たなフローが発生し、かつ、その際のスタックの使用状況がint32→native intと変わっていること
  3. (C)のブランチが条件を満たさない場合の、後続のフローが存在すること

を判断し、特に2)によって、「似たような、しかし異なるCコードを生成する必要」を判断します。

結果として、次のようなCのコードを出力します:

intptr_t Multiple10 (intptr_t a)
{
    int32_t count;

    int32_t stack0_0;
    intptr_t stack0_1;
    int32_t stack1_0;
    intptr_t stack1_1;
    int32_t stack2_0;

    stack0_0 = 10;
    count = stack0_0;

    stack0_0 = 0;

    stack1_1 = a;
    stack0_1 = stack0_0 + stack1_1;     // (D)

    stack1_0 = count;                   // ---+
    stack2_0 = 1;                       //    |
    stack1_0 = stack1_0 - stack2_0;     //    |
    count = stack1_0;                   //    | (F)
                                        //    |
    stack1_0 = count;                   //    |
    if (stack1_0 != 0) goto L_0000      //    |
                                        //    |
    return stack0_1;                    // ---+

L_0000:
    stack1_1 = a;
    stack0_1 = stack0_1 + stack1_1;     // (E)

    stack1_0 = count;                   // ---+
    stack2_0 = 1;                       //    |
    stack1_0 = stack1_0 - stack2_0;     //    |
    count = stack1_0;                   //    | (F)
                                        //    |
    stack1_0 = count;                   //    |
    if (stack1_0 != 0) goto L_0000      //    |
                                        //    |
    return stack0_1;                    // ---+
}

(D)と(E)によって、スタック[0]に想定される型が異なるのがわかりますか? C言語では、ローカル変数に型を指定しなければなりません。同じスタック位置でも異なる型を必要とする場合は、
その変数を使用する全ての箇所で、ソースコードを再生成する必要があります。

但し、(F)の部分は完全に同一なので、ラベルとgotoを駆使することで共通化できる可能性があります。(現在のIL2Cはその検知を行っていません。判定は複雑ではなさそうですが、難易度は高いと思います。また、ただgotoで遷移させただけだと、可読性が極端に低下する可能性があります)

まとめ

.NET ILのメタデータには、.NETの型情報が全て含まれています。リフレクション(やメタデータを解析するサードパーティのライブラリ)を使用すれば、型の情報は容易に引き出すことが出来ます。これによって、フィールドの型・メソッドの戻り値の型・メソッド引数群の型・ローカル変数群の型など、あらゆる型を直接特定できます。しかし、評価スタックの各スロットに格納される値の型については、どのような型が格納されるのかを静的に定めることが出来ません。

IL2Cでは、IL内での評価スタックの使われ方を解析して、評価スタックのスロットにどんな型を想定しうるかを調べています。そして、その情報を元に、完全に静的に定める必要のあるC言語向けに、型ごとのコード生成を行っています。

当初はこのような事を考えていなかったので、必要性を知ったときには「これは大変そうだ」と思いましたが、何とか乗り切ったので良かったです。

Making archive IL2C #6-28 … 30

この記事は「AOT技術 Advent Calendar 2017」の10日目です。
というか、今のところこのカテゴリに私しかエントリーしてないので、漏れた日を埋めていこうかな、ぐらいの感じです。

YouTube: Playlist: Making archive IL2C
GitHub: IL2C step-by-step design project

ネタをひねり出すのもアレなので、今まで蓄積したMaking archive IL2Cのダイジェストをやっていこうかと思います。

(イメージをクリックするとYouTubeが開きます)


#6-28 (.NET Conf 2017 Tokyo)

今までに至る解説を「.NET Conf 2017 Tokyo」で登壇 して喋ってきました。

unpluged枠だったので、スライドは殆ど無く、喋りだけです。この時にも幾つか過去のビデオを見直していたのですが、やっぱりフロー解析の辺りが一番大変でしたね。

#6-29

#6-27に引き続き、Value typeのインスタンスフィールド・メソッドの対応を行います。

インスタンスフィールドにアクセスするにはldfldやstfldを使えばよいのですが、インスタンスメンバにアクセスするには、インスタンスへのアドレス(マネージ参照)が必要です。MSDNではマネージ参照のことを「アドレス」と呼んでおり、かなり違和感があります(アンマネージポインタのようだ…)。

とにかく、Value typeのメンバにアクセスする場合は、マネージ参照を評価スタックにpushすれば良いため、ldlocaを使ってローカル変数へのマネージ参照をスタックにpushしておきます。

実はメンバを参照するだけ(例えばフィールドから値をldfldで取得する)であれば、マネージ参照ではなく値そのものでも問題ありません。しかし、値をストアする(stfld)場合は、必ずマネージ参照をpushする必要があります。これはちょっと考えると当たり前で、スタックに格納されている値型のフィールドを変更しても、それはスタックに保持されているので、すぐに破棄されてしまいます。

#6-30

引き続き、Value typeのフィールドアクセスに必要なILに対処しました。ldloca.s・initobj・stfld命令のILConverterを実装します。

口に出していませんが、この辺から、評価スタックに付けているマングリングされたシンボル名がやたら長くなるのが気になってきました。

それにしても、InlinePhi、何なんでしょうね…

Making archive IL2C #6-25 … 27

この記事は「AOT技術 Advent Calendar 2017」の9日目です。
というか、今のところこのカテゴリに私しかエントリーしてないので、漏れた日を埋めていこうかな、ぐらいの感じです。

YouTube: Playlist: Making archive IL2C
GitHub: IL2C step-by-step design project

ネタをひねり出すのもアレなので、今まで蓄積したMaking archive IL2Cのダイジェストをやっていこうかと思います。

(イメージをクリックするとYouTubeが開きます)


#6-25

アセンブリ・モジュール・型・メソッド・メソッドボディの関係を説明しました。

で、実装して確認してみると、method tokenからMethodInfo取ってみたら違うメソッドが取れるww 対象のモジュールを間違えていました。method tokenはそれが定義されているモジュールで一意なので、正しいモジュールからResolveする必要があります。

結果として、以下のような出力が得られました:

#include <stdbool.h>
#include <stdint.h>

int32_t IL2C_ConverterTest_CallTestMethod(void)
{
  int32_t local0;

  int32_t __stack0_int32_t;
  int32_t __stack1_int32_t;

  __stack0_int32_t = 1;
  __stack1_int32_t = 2;
  __stack0_int32_t = IL2C_ConverterTest_CallStaticMethodTestType_Test(
    __stack0_int32_t, (int16_t)__stack1_int32_t);
  local0 = __stack0_int32_t;
  goto L_0000;
L_0000:
  __stack0_int32_t = local0;
  return __stack0_int32_t;
}

第二引数はint16であり、評価スタックが常にint32以上となるので、明示的にキャストを挿入しています。

#6-26

次に、Value type内のスタティックフィールドの変換です。

.NETでのスタティックフィールドとは、一意なインスタンスの格納先となるのですが、C言語に対応するものは「グローバル変数」です。思い出しましたか? えぇ、「グローバル変数」ですよ。そうだっけ?みたいな感じでした :)

C言語でグローバル変数を実現する場合にも、staticキーワードを付けることが出来ます。このキーワードは、シンボルをファイルスコープに制約するもので、(C#構文と似ているのに).NETとは似ても似つかない感じですね。.NETで対応する概念は、アクセス修飾子 ですかね。

この回ではValue typeに対応する定義までを行いました。

#6-27

引き続き、Value typeのスタティックフィールドに対処しました。

スタティックフィールドの初期値を埋めるために、FieldInfo.GetValue()を使いましたが、ここでType initializerの問題に気が付きました。

ここで示した方法は、リフレクションでスタティックフィールドの初期値を取り出していますが、この方法には重大な問題があります。値の取得時に暗黙にType initializerが実行されてしまうということです。これは結構厄介な問題で、現在(#50)の時点ではissueに積んで保留しています。

Making archive IL2C #6-22 … 24

この記事は「AOT技術 Advent Calendar 2017」の8日目です。
というか、今のところこのカテゴリに私しかエントリーしてないので、漏れた日を埋めていこうかな、ぐらいの感じです。

YouTube: Playlist: Making archive IL2C
GitHub: IL2C step-by-step design project

ネタをひねり出すのもアレなので、今まで蓄積したMaking archive IL2Cのダイジェストをやっていこうかと思います。

(イメージをクリックするとYouTubeが開きます)


#6-22

16ビットプリミティブの対応を行いました。8ビットとほとんど同じ対応で行けました。

ldc.i2が存在しないので、果たしてリテラル16ビット値がどのように扱われるのか、ちょっと興味がありました。

  • 小さい数値はldc.i4.2とかの単形式ILを使う
  • 大きい数値はldc.i4で、16ビットの範囲内でpushする – どうせ32ビットに拡張され、切り捨てられるので問題ない

のような最適化(というほどのものでもありませんが)がRoslynで行われていました。

#6-23

micro:bitでデモを行う ための準備を行いました。il2c.exeを放置していたので、これを最低限のツールとしての体裁を整えます。

ツールとして、コマンドラインのオプションを受け取れるようにするのも課題ですが、コマンドラインからメソッドの実体を注入することは出来ないため:

  • コマンドラインに対象のアセンブリファイルを指定する
  • アセンブリに含まれる全てのメソッドを変換の対象とする
  • メソッド名は、名前空間と型名を含めた、マングリングされた関数名に変換する
  • 結果を単一のCソースファイルとして出力する

というように決めました。ここで初めて、マングリングされたシンボル名が具体的にどんな具合なのかを見ました。シンボル名解決のためにC++を使いたいなーという感想。

#6-24

とうとうValue typeに着手しました。メンバー(フィールド・メソッド)とスタティック・インスタンスの組み合わせによる定義を変換可能にします。

OR(Object reference)型をやろうとすると、あまりに多くのフィーチャーを一度に考えなければならないので、Value typeが先です。OR型はガベージコレクタ・参照追跡の手法・オブジェクト階層・インターフェイス実装・仮想メソッドなどなど、盛り沢山過ぎます :)
Value typeであれば、メンバーの構成方法・型の定義方法ぐらいです、それでもかなり大掛かりですが…

まずは、スタティックメソッドの呼び出しを処理させます。call命令のoperandから取得できる、method tokenからMethodInfoを取得して、対応する関数呼び出し式に変換します。スタティックなので、callvirtではなくcall命令を使います。また、method tokenを取得する際に参照するモジュールと、何故.NETには「モジュール(.netmodule)」という考え方があるのか? と言う話をしています。Moduleが特定できれば、method token値からMethodInfoを入手できます。ここから、関数呼び出しのソースコードを構成します。

メソッド呼び出しのパラメータ群は、評価スタックに積まれているので、これを逆順で取り出します。DecodeContextには評価スタックに相当する式をシンボル名(”__stack0″のような)で取得できるので、これをカンマで結合すれば、関数の引数群を構成できます。

最後に、少しだけALM(ビルドシステムとしてどこからどこまでどのように実現するか)の事に触れています。

Making archive IL2C #6-19 … 21

この記事は「AOT技術 Advent Calendar 2017」の7日目です。
というか、今のところこのカテゴリに私しかエントリーしてないので、漏れた日を埋めていこうかな、ぐらいの感じです。

YouTube: Playlist: Making archive IL2C
GitHub: IL2C step-by-step design project

ネタをひねり出すのもアレなので、今まで蓄積したMaking archive IL2Cのダイジェストをやっていこうかと思います。

(イメージをクリックするとYouTubeが開きます)


#6-19

前回から引き続き、ブランチ命令と、ラベルの対応付けを行う実装を行いました。これでifによる現実的なコードの変換が行われることが確認できました。

近所のインドカレー屋の不思議な運営の雑談もあります(?)

#6-20

if文の対応から横道にそれましたが、やっと、8ビット・16ビットプリミティブ型の対応を行いました。8ビットと言えばByteとShortですが、C言語で8ビットリテラル値がどのように扱われるのか… 今更C言語のリテラル表現を調べることになるとは思いもよらず。

そして、ここでようやく評価スタックに8ビットの値を入れる場合は32ビットのint32として自動的に符号拡張される、という事実を知ることに。また、ローカル変数に格納する場合も、int32から自動的に上位24ビットが切り捨てられる。前回その辺りをまとめましたが、実際にはこの回ぐらいで理解することになりました。

#6-21

引き続き、8ビットのSByte対応を行いました。8ビットの符号ビットの計算は、特別な処理が必要かどうかを検証しています。なぜなら、8ビットの値を評価スタックにpushする際には32ビット(int32)としてpushするので、ここで符号拡張(又はpopするときに縮退又は切り捨て)の区別をつける必要があるかどうかが重要だからです。結局、切り捨てるだけで良かったので、符号拡張ではなくビット幅を広げ、popした時に24ビットを切り捨てれば辻褄が合います。

ここで検討していたときには気が付きませんでしたが、今考えると、評価スタックのスロットを異なる型で再利用しなければ問題は発生しない、という条件が付きます。端的に言えば、問題のあるILが来るとマズイですが、InvalidProgramExceptionな案件のような気がします(IL2Cはこの例外を送出しませんが)。