Sorry, MSMVP will have to wait for another time.

(This post is edited after machine translated from Japanese.)

It’s been a while since I’ve posted anything.

I’ve been awarded the Microsoft Most Valuable Professional, or MSMVP for short, since 2015, but I’ve decided to decline it after 2021 term.

I would like to thank the MSMVP office and all the people who have helped me through this program. There will be no change in the community activities for the time being. In the near future, I will probably mainly write OSS because I like to write code. I have some projects on GitHub, so it would be nice if you could take a look.

I’ve been asked by someone who told me privately “if the `Center CLR` (My held community group) is going away?”, but it’s not :) We’re still active in the slack group. If you’re interested, feel free to check out the connpass page for directions (In Japanese)..

Here I would like to mention two good things about the MSMVP award, although they are short and almost a mere diary.

A shining thing

I’m a sucker for “hikarimono (shining thing)”, like jewels or jewelry, and I like the MSMVP trophy because it’s very well made.

This one is made of glass, and the blue-colored triangular pillar at the base and the flat monolithic stone monument-like part are perfectly fused together, making it shine and shine very brightly. This plaque can be fitted with a `Year ring` that you get every time you renew your award (just insert it from the side), and this will grow as the award year continues. I think the person who came up with this sculpture has very good taste.

I’m afraid that if I decline the MSMVP, this won’t grow anymore, which is a shame… I wonder if I can just get a Year ring every year :)

MVP Global Summit

The Global Summit is an event for MVPs held at MS headquarters in North America.

One of the perks of being an award winner is that you get to learn about new (unannounced) products and projects that are planned for the near future and discuss them with direct engineering team members, which is said to be a great benefit (another benefit is the MSDN subscription, but in my case I already had one, so…)

You have to pay for your own transportation, but they will pay for your accommodation (in a pre-arranged hotel).

So, for myself, I didn’t really feel that the advantages I mentioned above were that much of an advantage. The reason lies in the award categories.

My award category is Developer Technologies, which is about language processing systems and development environments, such as Visual Studio and C#/F#. Excepted Visual Studio, processing systems such as compilers and .NET runtime information now have public repositories on GitHub, where discussions are held daily. So the information available at Global Summit (in other words, NDA) was almost non-existent when I attended. So there was hardly anything that made me go to the Global Summit and mumble “Meow” (footnote). (Even if I had heard such information, mostly it would have been released a week later, so there was little benefit in hearing it now. And I was able to predict the general flow of events.)

As for why I’m introducing it here, I’ll never forget the memories of getting to know the MVPs who gathered at the Global Summit, and especially the F# area where they treated me well.

At the F# meetup held in the evening, I jumped into a place where I was the only Japanese person and forced myself to speak for about 10 minutes (I couldn’t speak). How is F# in Japan? I was really happy and enjoyed talking to them.

I was prepared to move to the hotel by myself on my way home at night, so I was trembling with fear that I might be kidnapped by the Uber driver to an unknown place and go back to Japan as a corpse (or maybe not anymore)… But he said, “Oh, I’m that way too, so you can take an Uber back with me!”

(To add, I had heard that the area around Seattle in North America was relatively safe, but even so, I knew there was a certain amount of risk involved in traveling alone at night.)

If it hadn’t been for that experience, I wouldn’t have ever thought of attending another overseas meetup, or even jumping into another overseas meetup. I’m not sure I would have thought of that.

Since I went to the Global Summit last term, I haven’t been able to contribute much to F#, and I still have my own regrets in that regard. I hope to be able to do something someday, so I follow up when I can, and in the OSS project I’m working on now, I always support F# with an awareness of it, unless it makes no sense.

Now that I’ve declined MVP (and even if COVID dies down), the thought that I won’t be able to participate in Global Summit anymore makes me shed tears, but when I get the chance (and if I have the budget), I’d like to go abroad on my own this time to meet the people I met, or go to other meetups.


Footnote: “Meow” is a word that MVP winners have no choice but to drop on Twitter and other social networking sites when they see or hear something exciting but cannot be disclosed under NDA. But this is frowned upon because it sounds like a kind of bragging to those who can’t participate. I did it once myself, and I’m sorry…

.NET非同期処理で例外を安全に対処する

.NETの非同期処理で例外を処理する方法について、重要な点を簡潔にまとめました。

ついつい例外処理を汚く書いてしまうとか、手を抜いてしまう、またはGUI環境で例外処理が悩みの種、という人向けです(一部、やや深い話をしていますが、Level 200~300ぐらいです)。async voidへの対処で悩んでいる方にも、方法を示します。

コード例はC#で書いています。ターゲットはすべての.NET処理系、つまり、.NET Framework, .NET Core, .NET 5, mono, Xamarin, Unityなど、全てです。


基本的な例外処理

例外を補足するには、try…catchを使用します:

public static FooBar Fetch()
{
    try
    {
        var body = webClient.DownloadString("https://example.com/foobar");
        return JsonConvert.DeserializeObject<FooBar>(body);
    }
    catch (IOException ex)
    {
        Trace.WriteLine($"Log: {ex}");
        throw;
    }
}

catch句には、そのブロックで受信すべき例外の種類を型名で指定出来ます。上の例では、IOException、またはそれを継承したクラスがスローされると、catchブロックのTrace.WriteLine(…)が実行されます。
注意すべき点は以下の通りです:

  • 再スロー、つまり例外を呼び出し元に伝搬させる場合は、throw句を単独で使います。throw exのように書いてしまうと、例外中に保存された、発生元を示すスタックトレースの情報が失われ、throw exを書いた場所からにリセットされてしまいます。
  • catch句に、例外の型を指定しない事もできます:
try
{
    // ...
}
catch // 受信する型を書かない
{
    Trace.WriteLine("Log: Caught uninterpreted exception.");
    throw;
}

これは、どのような例外をも受信し、catchブロックを実行するというものです。一見すると、catch (Exception ex)のように、Exceptionクラスを指定するのと同じに見えるかもしれません(.NETの例外は、すべてExceptionクラスを継承するため)。しかし、「すべての例外」とは、.NETでは補足されない(出来ない)ような例外を含む、という意味です。

代表的な例として、SEH (Structured Exception Handling:構造化例外処理) と呼ばれる、Windows環境の例外を挙げます。これは.NETだけではなく、Win32のネイティブコード実行時に発生する「非同期例外」という種類の例外です。名前が非常に紛らわしいのですが、.NETの非同期処理で扱われる例外とSEHの非同期例外は、まったく関係ありません。

(なお、都合でWindows環境のネイティブコードをx86と呼称していますが、arm64を含む全てのWindows環境に当てはまります。)

.NETのマネージ例外と構造化例外を対比させてみます:

  • .NETマネージ例外: 私たちに馴染みのある例外。IOException, ArgumentException, …
  • Structured Exception (構造化例外)
    * ハードウェア例外: CPU由来の例外や、外部割込みによって発生するもの。代表的な例外に、0除算エラーがある。
    * ソフトウェア例外: 意図して発生させる構造化例外。.NETマネージ例外をスローする事に似ている。Win32のRaiseException APIで発生させる。

構造化例外の説明を見て、色々疑問が湧くかもしれません。


構造化例外と.NETマネージ例外の位置づけ

0除算エラーとDivideByZeroException

広義の0除算エラーといえば、こういうコードです:

public static int Div(int a, int b)   // bに0を与える...
{
    try
    {
        return a / b;
    }
    catch (DivideByZeroException ex)
    {
        Trace.WriteLine($"Log: {ex}");
        throw;
    }
}

.NETでは、0除算エラーは、DivideByZeroExceptionクラスで受信、つまりcatchする事が出来ます。

SEHでは、EXCEPTION_INT_DIVIDE_BY_ZEROというコードを使用して、RaiseExceptionを呼び出し… ません。ネイティブコードであっても、0除算エラーが発生するような計算命令を実行するだけで、(明示的に判定するようなコードを書かなくても)、自動的に、このコードのSEH例外が発生します。

ネイティブコードで発生する0除算エラーは、.NETのランタイムによって自動的にDivideByZeroException例外に変換されて、マネージ例外として再スローされます。そのため、私たちは普段、内部でSEH例外が発生したということは意識しません(これは、JITがCIL(MSIL)を直接x86の除算命令に置き換えても、まったく同じ方法で0除算を検出することに符合します)。

しかし、0除算エラーのような代表的な例外ではなく、カスタムコードを持つSEH例外が発生する可能性もあるので、そのような例外は.NETランタイムが変換できません。そういった場合でも、冒頭の、型を指定しないcatch句であれば受信できるというわけです。

SEH例外が「非同期例外」と呼ばれる理由を補足すると、意図しないタイミングで発生する可能性があるから、という意味を含みます。0除算エラーの場合、x86のdiv命令を実行中に発生するのですが、CILのDivからすると、div命令を含むx86 2~3命令に対応します。そのため、どのx86命令までが正常に実行できて、どこで実際のSEH例外が発生したのか、を正確に判断するのが難しくなります。0除算エラーを含むいくつかのSEH例外は、.NETランタイムから見て保障可能なものに限り、DivideByZeroExceptionのようなマネージ例外に自動的にマッピング出来ているのです。

最初の脇固め

そのようなわけで、全ての例外的事象に対応して、何らかの処理を行わなければならない場合は、catch (Exception ex) { … } ではなく、catch { … } を使いましょう。但し、後者は例外が発生したという事実だけで、例外の内容を識別する情報が得られません。そこで、このようなboilerplateが必要になります:

try
{
    // ...
}
// すべての.NETマネージ例外
catch (Exception ex)
{
    Trace.WriteLine($"Log: {ex}");
    throw;
}
// すべての例外
catch
{
    Trace.WriteLine("Log: Caught uninterpreted exception.");
    throw;
}

これは… まあ… めんどくさいですね…


本題

構造化例外なんて、Linuxで.NET CoreやってるAさんや、Xamarin iOSやってるBさんや、Unity for AndroidやってるCさんには何も関係ないので、気にする必要ないと思うかも知れません。実際、私もそう思うのですが、ここからが本題です。

私たちは、結構な頻度でこのようなコードを書きがちです:

// WPF, Windows Forms, Xamarinなどのボタンクリックイベント
public async void Button_Click(object sender, EventArgs e)
{
    // 非同期で取得
    var body = await httpClient.GetStringAsync("https://example.com/foobar");
    var foobar = JsonConvert.DeserializeObject<FooBar>(body);

    // 表示する
    textBox.Text = $"${foobar.Price}";
}

これの何が問題なのかと言うと、サーバーへのアクセスに失敗したときの例外の処理が何もないことです。いや、Applicationクラスのグローバル例外ハンドラで処理しているから問題ないと思うかもしれません:

public void Main(string[] args)
{
    // WPFの場合、Applicationクラスのイベントをフックすることで、未処理の例外を捕捉できる
    Application.DispatcherUnhandledException += (s, e) =>
        Trace.WriteLine($"Log: {e.Exception}");

    // ...
}

あるいは、コンソールアプリケーションやXamarin Formsなら、AppDomainの例外ハンドラを使うこともできます:

public void Main(string[] args)
{
    // TIPS: ExceptionObjectがobject型なのは、SEHを意識しているからです
    AppDomain.CurrentDomain.UnhandledException += (s, e) =>
        Trace.WriteLine($"Log: {e.ExceptionObject}");

    // ...
}

他の環境でも、大体似たようなグローバル例外ハンドラが用意されている筈です。

しかし、残念ながら、上記のようなasync voidのシグネチャを持つ非同期メソッドで発生した例外は、これらの方法では受信できません。

思っていたのと違う原因

問題は3点あります:

  • 非同期処理の例外をフックする方法が異なる: 非同期処理で発生した未処理の例外は、TaskScheduler.UnobservedTaskExceptionを使えば受信できます。但し…
  • 例外が発生したタイミングで受信できない。
  • 補足できても対処可能なことが限られる。

非同期処理中に発生した例外は、上記の通りTaskSchedulerクラスのUnobservedTaskExceptionで同様にフックする事が出来ます(これを推奨している記事も結構あるのですが、ここでは敢えて参照しません):

public void Main(string[] args)
{
    Application.DispatcherUnhandledException += (s, e) =>
        Trace.WriteLine($"Log: {e.Exception}");

    // 非同期処理の未処理例外も補足する
    TaskScheduler.UnobservedTaskException += (s, e) =>
        Trace.WriteLine($"Log: {e.Exception}");       // <-- (A)
}

ところが、この (A) の箇所が呼ばれるタイミングが、実際に例外が発生したタイミングよりもかなり遅れます。まず、async voidとして実装したメソッドの等価実装を見て下さい:

// ボタンクリックイベント
public void Button_Click(object sender, EventArgs e)
{
    // 非同期処理を呼び出し、そしてTaskを捨てる
    var task = this.OnClickAsync(sender, e);
}

// 実処理 (Taskを返す)
private async Task OnClickAsync(object sender, EventArgs e)
{
    // 非同期で取得
    var body = await httpClient.GetStringAsync("https://example.com/foobar");
    var foobar = JsonConvert.DeserializeObject<FooBar>(body);

    // 表示する
    textBox.Text = $"${foobar.Price}";
}

このように、内部的には非同期処理を示すTaskが捨てられた格好になります。Taskのインスタンスは誰も保持していないため、例外で処理が中断した後はしばらくメモリ上に残り、その後、ガベージコレクタが回収しようとします。ここで、Taskのファイナライザーが(ファイナライザースレッドから)呼び出されます。

Taskのファイナライザーは、自身が保持している未処理の例外を、TaskScheduler.UnobservedTaskExceptionを呼び出して処理させます。このような動作なので、例外が発生したタイミングと、ハンドラが呼び出されるタイミングがずれるのです。

そして、(A) の時点のスレッドは、ファイナライザースレッドなので:

  • ハンドラ内からユーザーインターフェイスを操作できない。
  • 何なら他の操作もできないか、または制限がある。

事になります。上のコードで示したようにコンソールに出力するだけであれば、実害はないかもしれません。しかし、一般的なライブラリは、ファイナライザースレッドから呼び出される事を想定していないと思われるので、ファイルにアクセスしたり、DBにログを出力したり、別のサーバーに要求を投げたりすると、もしかしたら更に別の問題を引き起こして頭を悩ませるかもしれません。

(注: ファイナライザースレッドから呼び出されるという事は、少なくともマルチスレッドでアクセスすることが可能である必要があり、かつ、ファイナライザースレッドの厳しい環境が回避可能でなければなりません。大半のクラスライブラリはこれを満たしません。実際のところ、Trace.WriteLine()が安全かどうかも疑わしいです…)

また、例外が発生したタイミングを追跡しにくい、という問題が残ります。自分の周りでは、いつの間にかUnobservedTaskExceptionがデバッガのコンソールやログファイルに(意図しないタイミングで)表示され、これの原因をどうやって特定しようか、という悩みをちょくちょく観測します。


対処方法

結局、例外が未処理で放置されないように、根元で受信することです:

public async void Button_Click(object sender, EventArgs e)
{
    try
    {
        // 非同期で取得
        var body = await httpClient.GetStringAsync("https://example.com/foobar");
        var foobar = JsonConvert.DeserializeObject<FooBar>(body);

        // 表示する
        textBox.Text = $"${foobar.Price}";
    }
    // 未処理の例外を逃さず受信する
    catch (Exception ex)
    {
        // ここなら比較的安全にいろんなことができる...
        Trace.WriteLine($"Log: {ex}");
        // 再スローしない
    }
}

おっと、そういえばSEHの件がありましたね:

public async void Button_Click(object sender, EventArgs e)
{
    try
    {
        // 非同期で取得
        var body = await httpClient.GetStringAsync("https://example.com/foobar");
        var foobar = JsonConvert.DeserializeObject<FooBar>(body);

        // 表示する
        textBox.Text = $"${foobar.Price}";
    }
    // 未処理の例外を逃さず受信する
    catch (Exception ex)
    {
        // ここなら比較的安全にいろんなことができる...
        Trace.WriteLine($"Log: {ex}");
        // 再スローしない
    }
    // SEHも逃さない
    catch
    {
        // ここなら比較的安全にいろんなことができる...
        Trace.WriteLine("Log: Caught uninterpreted exception.");
        // 再スローしない
    }
}

他のプラットフォームの例として、Unityのハンドラも挙げておきます。と言っても、メソッドのシグネチャが違うだけで、やることは全く変わりません:

// UnityでUX buttonを使った時のクリックハンドラ
public async void UXButton_OnClick()
{
    try
    {
        // 非同期で取得
        var body = await httpClient.GetStringAsync("https://example.com/foobar");
        var foobar = JsonConvert.DeserializeObject<FooBar>(body);

        // ...
    }
    // 未処理の例外を逃さず受信する
    catch (Exception ex)
    {
        // ここなら比較的安全にいろんなことができる...
        Debug.LogError($"Log: {ex}");
        // 再スローしない
    }
    // SEHも逃さない
    catch
    {
        // ここなら比較的安全にいろんなことができる...
        Debug.LogError("Log: Caught uninterpreted exception.");
        // 再スローしない
    }
}

めんどくさい…

まあそうですよね。SEHのリカバリーを捨てたとしても、毎回(忘れないように)catch句を書くのは非常にだるいです。なので、boilerplateを発展させましょう:

// 非同期処理で発生する例外を安全に処理する
public static async void SafeAsyncBlock(Func<ValueTask> action)
{
    try
    {
        // 非同期処理を実行
        await action();
    }
    // 未処理の例外を逃さず受信する
    catch (Exception ex)
    {
        // ここなら比較的安全にいろんなことができる...
        Trace.WriteLine($"Log: {ex}");
        // 再スローしない
    }
    // SEHも逃さない
    catch
    {
        // ここなら比較的安全にいろんなことができる...
        Trace.WriteLine("Log: Caught uninterpreted exception.");
        // 再スローしない
    }
}

こういうものを作っておくと、このように書き直せます:

public void Button_Click(object sender, EventArgs e) =>
    SafeAsyncBlock(async () =>
    {
        // 非同期で取得
        var body = await httpClient.GetStringAsync("https://example.com/foobar");
        var foobar = JsonConvert.DeserializeObject<FooBar>(body);

        // 表示する
        textBox.Text = $"${foobar.Price}";
    });

かなり楽に書けるようになりました。ValueTaskではなくTaskでも構いません。ValueTaskにしておけば、アロケーションコストを削減できる可能性があります。

Level 400~500: 非常に細かいことで、普通は考慮する必要はないと思いますが、SEHを非同期処理の例外として伝搬し、継続させることはできません。従って、catch句で受信できるSEHは、最初のタスクコンテキストスイッチが発生する以前のSEHのみとなります。一度タスクコンテキストスイッチが発生した後の非同期継続処理でSEHが発生した場合は、この方法では対処できません。恐らくはThreadPoolから割り当てられたワーカースレッドの根元に伝搬するか、SynchronizationContextがホストするスレッドの根元に伝搬します。AppDomainでフックする以外に、良い方法は、ないかも知れません…


まとめ

.NETの非同期処理で例外に安全に対処する方法をまとめました。この方法なら、冒頭に示した通り、あらゆる.NET環境で例外を安全に対処できます。

ですが、私としては、これらの対処が行われている補助ライブラリやフレームワークを利用することをお勧めします。恐らく、SafeAsyncBlockのようなboilerplateを見て「これだ!」と思った方は、全て自前で実装しようとしていると思いますが、抜け漏れは往々にして発生しますし、細かい考慮点はほかにもいくつかあります。

XAML環境では、私が作ったEpoxyでは当然このような対処を行っています(ICommandのハンドラを非同期処理にできますが、内部では同様の実装を行っています)。また、他の著名なライブラリも対処していると推測するので、検討してみると良いでしょう。

それではまた。

非同期処理、なにもわからない

最近のお仕事で、C#の非同期処理の書き方が分からずに、溶岩地帯に自爆していくコードを沢山みるようになったので、ケースとして日常風の記事にしてみました。

どんな風にハマってしまい、どうやって解決するのかが分かると思います。

結論

先に書いておきます。

  1. Task.Run()を使ってはいけません
  2. Task.Result, Task.Wait()を使ってはいけません
  3. Threadクラスを使ってはいけません
  4. async-await構文だけを使って書きます
  5. async voidにするのは特殊な場合だけです

がんばれー、わかってしまえば難しくない!

補足: もしあなたがJavaScriptで非同期処理を書いた事があるなら、その知識をそのまま生かせます。
JavaScriptではTaskの代わりにPromiseを使いますが、Promiseには上記1,2,3は存在しません。しかし、それで困ることはありませんよね? (これはとても重要な示唆です)

平和の終焉

以下のような実装を考えます:

// Web APIからデータを取得する
public string FetchData()
{
    // TODO: ここにWeb APIからのデータ取得コードを書く
}

ここからコードを書こうとしている人は既に罠にはまっているわけですが、気が付けないので、そのままここに肉付けをしていくわけです。

Web APIにアクセスするには、今はHttpClientを使いますが、彼らは使い慣れているWebClientを使います:

// Web APIからデータを取得する
public string FetchData()
{
    WebClient wc = new WebClient();
    string body = wc.DownloadString("https://example.com/api/data");
    return body;
}

出来ました。ただ、WebClientのドキュメントを見ると、今から使用するのは推奨しない、代わりにHttpClientを使うように、という事が書かれています。また、レビュアーに指摘されたり、ググったりして見ても、やはりもうWebClientは使うな、と言うような情報が得られます。そこでこれを書き換えようとするわけです。

(なお、今回の題材はWeb APIですが、例えばRedisだとかAzureのサービスとかで提供されるSDKとか、もう大半が非同期処理対応のAPIしか用意されていなかったりするので、Web API固有の話ではないという事に注意して下さい)

Day 1

Sync氏は、WebClientをHttpClientに書き換えていきます。しかし、GetStringがありません。代わりに、GetStringAsyncという似たようなのがあるので、これを使おうとします:

// Web APIからデータを取得する
public string FetchData()
{
    HttpClient hc = new HttpClient();

    // エラー: 型が合わない
    string body = hc.GetStringAsync("https://example.com/api/data");

    return body;
}

GetStringAsyncというのを使ったら、型が合わないというエラーが出ます(幸いなことに? Sync氏はvarを使わないので、ここでエラーを検出)。よくよく見ると、Task<string>という型です。

ここで彼らの運命は分岐します。どちらも溶岩地帯です。

Day 2 – ピュアなResult

MSDN公式のTaskクラスのドキュメントを読むとサンプルコードがあり、どうやらResultプロパティで結果を取得できるらしい。

(すいません、この記事書いててちょっと頭クラクラしてます… 上のドキュメントは初心者をサイレントに殺しに掛かっているので、見ない方が良いです。リンクは消しました)

// Web APIからデータを取得する
public string FetchData()
{
    HttpClient hc = new HttpClient();

    // Taskは良く分からないけど、Resultプロパティを見れば結果が得られそうだ
    // 型もばっちり一致して、コンパイルが通ったぞ!
    string body = hc.GetStringAsync("https://example.com/api/data").Result;

    return body;
}

起きうる結果について:

  • GUIアプリケーション(WinForms, WPF, Xamarinなど): 何かの拍子にアプリケーションが固まる。原因不明
  • サーバーサイド(ASP.NETなど): 同時接続によるパフォーマンスが悪い、全然伸びない

Day 3 – 分身するResult

GUIアプリケーションを苦労してデバッグしていくと、どうもResultが返ってこないことがあることが分かった。ASP.NETの方は、皆目見当がつかない。そこそこ良いVMを割り当てているのに、.NETって性能悪いの?それともWindows VMのせいなの? Linuxにすれば良いの?

そうは言っても納期が迫っているので、何とかしなければならない…

そうだ、ワーカースレッドでResult取得するところを分離すれば固まらなく出来るのでは? 性能上げられるのでは?? さっきのドキュメント読んだら、Task.Runを使えば簡単にワーカースレッド使えるみたいだし、これで行こう!

// Web APIからデータを取得する
public string FetchData()
{
    HttpClient hc = new HttpClient();

    // Taskって便利だね!ワーカースレッド簡単に使えるんだ
    string body = Task.Run(() =>
        hc.GetStringAsync("https://example.com/api/data").Result).Result;

    return body;
}

起きうる結果について:

  • GUIアプリケーション(WinForms, WPF, Xamarinなど): 何かの拍子にアプリケーションが固まる。原因不明
  • サーバーサイド(ASP.NETなど): 同時接続によるパフォーマンスが悪い、全然伸びない。Day 2より悪化

Day 4 – async-awaitの存在を見出す

もうわかんない、HttpClientとかTaskとか何もわからない… Taskでググるとasync-awaitとか言うやつが出てくるから、これを使えばいいのかな…

// Web APIからデータを取得する
public string FetchData()
{
    HttpClient hc = new HttpClient();

    // async-awaitの書き方本当にわからない...
    // 1日試行錯誤してやっとエラー出なくなった...
    string body = Task.Run(async () =>
        await hc.GetStringAsync("https://example.com/api/data")).Result;

    return body;
}

起きうる結果について:

  • GUIアプリケーション(WinForms, WPF, Xamarinなど): 何かの拍子にアプリケーションが固まる。原因不明
  • サーバーサイド(ASP.NETなど): 同時接続によるパフォーマンスが悪い、全然伸びない。Day 2ぐらい。多分この人はその違いを測定してない…

Day 5 – スレッド手動操作

わかったわかった、Taskなんて使うからダメなんだ。初めからThreadクラスを使えば良かったんだよ。

// Web APIからデータを取得する
public string FetchData()
{
    HttpClient hc = new HttpClient();

    // Taskなんて意味わかんないし、これでいいや。
    // でも何でこんなの使うの? WebClientでよくね?
    string body;
    Thread thread = new Thread(() =>
        body = hc.GetStringAsync("https://example.com/api/data").Result);
    thread.Start();
    thread.Join();

    return body;
}

起きうる結果について:

  • GUIアプリケーション(WinForms, WPF, Xamarinなど): 一応動作はする。但し、Web APIアクセス中は固まる
  • サーバーサイド(ASP.NETなど): 同時接続によるパフォーマンスが悪い、全然伸びない。Day 2と同じ

わけがわからないよ。どうしてみんなはそんなにHttpClientにこだわるんだい?

Day 6 – 真実

Sync氏「HttpClientだと固まったり性能が上がりません。HttpClientは動作が怪しいので多分バグってます。WebClientを使う方が安全です」

レビュアー「駄目です、HttpClientを使ってください。こう書きます」

// Web APIからデータを取得する
public async Task<string> FetchDataAsync()
{
    var hc = new HttpClient();
    return await hc.GetStringAsync("https://example.com/api/data");
}

レビュアー「非同期処理を行うメソッドは、必ずTaskクラスを返すようにします。また、実際にはHttpClientはstaticで保持る必要があります。とりあえずこんな感じで他のも全部直してください」

Sync氏「え、でもこれだと、メソッド名も戻り値も変わっちゃってるので、呼び出し元にも修正が入ってしまいます」

レビュアー「呼び出し元も直してください。他にうまくやる方法はありません。実際には、メソッド名は直さなくても良いですが、直しておいた方が後々困らないです」

Sync氏「(絶望…)」

(補足: 文字通りです。大げさな話ではなく、他に方法はありません。試行錯誤は時間の無駄になります。警告しましたよ?)

Day 7 – GUIの真実

Sync氏は、必死にGUIのコードを直していた。

// コンストラクタ
public FoolishForm()
{
    // エラー: 型が合わない
    button.Click += this.Button_ClickAsync;
}

// ボタンがクリックされたら、データを取得して反映
public async Task Button_ClickAsync(object sender, EventArgs e)
{
    // データを得る
    var data = await this.FetchDataAsync();
    // テキストボックスに反映
    this.textBox.Text = data;
}

良い所まで来ていたが、Button_ClickAsyncメソッドをイベントハンドラとして登録するところで、型のエラーが発生して悩んでいた。

数時間後…

Sync氏「イベントハンドラの部分はどう書けばいいんですか?型が合わないと言われてしまいます」

レビュアー「こう書きます」

// コンストラクタ
public FoolishForm()
{
    // 問題ない
    button.Click += this.Button_Click;
}

// voidでいい / Task返さないので~Asyncも不要
public async void Button_Click(object sender, EventArgs e)
{
    // データを得る
    var data = await this.FetchDataAsync();
    // テキストボックスに反映
    this.textBox.Text = data;
}

Sync氏「え、でも非同期処理ではTaskを返さなければならないのでは?」

レビュアー「この場合だけ、つまりイベントハンドラだけ、async voidを使えます。この場合だけと考えてほぼOKです。他では使わないでください」

数分後…

Sync氏「試してみたら固まらなくなりました!ただ、動いているときにボタンを連打すると、何度もサーバーにアクセスしてしまうのですが」

レビュアー「古典的なJavaScriptの問題と同じで、アクセス中にボタンをクリックできないように仕様変更して下さい。実装はこんな感じにすれば良いと思います」

// ボタンがクリックされたら、データを取得して反映
public async void Button_Click(object sender, EventArgs e)
{
    // まずボタンをクリックできなくする
    this.button.IsEnabled = false;

    try
    {
        // データを得る
        var data = await this.FetchDataAsync();
        // テキストボックスに反映
        this.textBox.Text = data;
    }
    finally
    {
        // ボタンをクリック可能にする
        this.button.IsEnabled = true;
    }
}

Sync氏「ああなるほど! そうか、ロード中くるくる回るやつを追加で表示したほうが良いかもしれないですね」

レビュアー「そうですね。あと、async voidとする場合は、必ず例外をcatchして下さい」

Sync氏「何故ですか? 今はApplicationクラスの共通例外ハンドラで処理しているので問題ないと思いますが…」

レビュアー「その例外は、共通例外ハンドラでは処理できません。見たことの無い例外がデバッガのログに出てませんでしたか?」

Sync氏「ああ… UnobservedTaskExceptionというのが出てました。それも変なタイミングで表示されていた気がします」

レビュアー「私はこれから出張があるので詳しくは省きますが、メソッド内でcatchして処理するのがベストです」

(補足: この事については、別に記事を書きましたので、興味があれば参照してください

Day 8 – サーバーサイドの真実

さらに数日後…

Sync氏「サーバー側の実装もわかりました、前は呼び出し元はこうしていたのですが」

[HttpPost]
public string GetData()
{
    // データを得る
    var data = this.FetchData();
    // テキストボックスに反映
    this.textBox.Text = data;
}

Sync氏「こうすれば良いという事ですね?」

// APIエンドポイント名にAsyncとついていても、自動的に無視される
[HttpPost]
public async Task<string> GetDataAsync()
{
    // データを得る
    var data = await this.FetchDataAsync();
    // 返却する
    return data;
}

レビュアー「はい、それで良いと思います」

Sync氏「GUI側と違って、サーバー側の実装は特に変わったことは無いんですね… もう一つだけ、わからない事があるんですが」

レビュアー「なんですか?」

Sync氏「今回の非同期処理対応で、結局Task.Runメソッドを使わなかったのですが、あれはいつ使うものなのですか? C#の非同期処理についてググってみると、大体Task.Runを使う例が載っているので使おうとしてああいうコード書いてしまったんですが…」

レビュアー「大量の計算だけを行う処理を並行して同時に処理させたい時だけに使います。我々が普段書いているコードで、そういう処理は殆ど無いですよね? なので、知らなくても良いレベルで使わない筈です」

(注意: 初稿ではHttpPost属性でAsyncを付けていない名前を明示していましたが、不要でした。Ovisさん、ありがとう)

輝かしい未来への第一歩

Sync氏に横たわっていた暗い霧は、今晴れわたった。そこにはまだ何も存在しないが、やっとスタート地点に立った。そう感じたのである…


お約束の…

ここで挙げた例は、どうしてTask.RunやTask.Resultを使いたがってしまうのかという所に焦点を当てているため、実際にはもっと効率の良い実装テクニックが色々ありますが、最低限このように書けば、Taskをいじり倒して溶岩地帯に突っ込んで死亡する、ということは無くなると思います。なお、ASP.NET (Core), WinForms, WPF, Xamarinについて取り上げましたが、これらだけに適用される手法ではなく、コマンドラインプログラムやUnity、独自の.NET環境など、すべての.NETの非同期処理で適用できる手法です。

もう少しかっちりした解説が必要なら、こちらを参照してください

では、いつものやつ、行ってみましょう:

Task.Run()も、Task.Factory.StartNew()も、Task.Start()も、Task.ContinueWith()も、Task.Wait()も、Task.Resultも出ませんでしたね? これらを使う事はまずありません(断言)。

今、記事冒頭の「結論」の項を読み返せば、納得できると思います。「C# 非同期処理」とか「C# async await」などでググってみると、Task.Run()やTask.Resultを最初に説明したり引き合いに出す記事の何と多い事か… これらは本当に、知らなくて良いんですよ

もし、自信が無くなってきたら、JavaScriptにもPromiseとasync-await構文があるのに、これらに該当するものは無くても実用出来ているということを思い出すと良いと思います。

(更に補足すると、.NET/C#の場合は、async-awaitよりも先にTaskが生まれてしまった事が、不幸の始まりだったのです)

この記事で少しでも救われる人が増えますように。

それではまた。

2021年のXAML事情とEpoxyを作った話

昔からXAMLのMVVMライブラリで自分に合うものがなく、作ろうと何度かトライ(しては飽きた)していたのですが、とうとうまともに使えるレベルにまで持って行けたので、先日宣伝を垂れ流しました。

Epoxy は、.NETの様々なXAML環境で使うことが出来る、MVVM (Model-View-ViewModel)を楽に実装するための補助ライブラリです。特徴を抜粋すると:

  • 非同期処理 (async-await) を安全に書くことが出来るように配慮しています。
  • C# 8.0でサポートされた、null許容参照型を使えます。
  • 小さなライブラリで、理解しやすいAPIです。
  • プラットフォーム標準以外のフレームワークやライブラリに依存していません。
  • 大げさにならない、最小の手間とコストで Model-View-ViewModel 設計を実現します。Viewにコードビハインドを書かずに済むことが着地点ですが、そのために煩雑な処理を記述しなければならなくなる事を避ける方針です。
  • MVVMビギナーが躓きそうな部分に焦点を当てています。
  • ほかのフレームワークライブラリ(例: ReactiveProperty)と組み合わせて使えるように、余計な操作や暗黙の前提を排除しています。

Epoxyの具体的な解説や動画などは、GitHubのREADMEに書いておいたので、そちらを参照してもらえればと思います(現在はまだありませんが、スタートガイドを書くか撮るかしようかなとは思っています)。フィードバックがあると嬉しいです。

(ここまで、前振りとEpoxyの宣伝)


今や、.NETでXAML環境と言うとかなり選択肢が増えていて、観測しているだけでも以下のようなものがあります:

  • WPF (Windows Presentation Foundation)
  • UWP (Universal Windows platform / 旧Windows Store App)
  • Xamarin Forms
  • Avalonia
  • Uno platform
  • WinUI 3
  • MAUI

(多分、たまたまXamarin Forms公開直前に作ってプチバスってしまったジョークGtk XAML環境、のようなものなら、探せばいくらでも出てきそうです)

EpoxyをこれらのXAML環境で同じように使えるようにするため、移植を試みてみました。MAUIは理由があって取りやめたのですが、それ以外の環境では動作するようになりました。

この記事では、移植作業によって分かった、各XAML環境の現状の差異をまとめてみようと思います。対象読者は、現在のXAML環境の選択肢について知りたい人や、XAML環境の知識のアップデートをしたい人、各XAML環境の差異を知りたい人です。

但し、Epoxyの移植を下敷きにしているため、フレームワークアーキテクチャの情報に偏っていることに注意して下さい。一般的にXAMLに期待されることとして、リッチなユーザーインターフェイスの実現、という視点がありますが、そこはすっぽり抜けていると思います。

なお、この記事の情報は2021年2月時点のものです。

XAMLについて

そもそもXAMLとは、という話を一応しておきます。XAMLはユーザーインターフェイスの定義をXMLで記述するものです。生まれた頃(.NET Framework 3.0)は、丁度XMLがデータ記述形式として広く用いられるようになった頃で、Windows Formsの後継技術として生まれました。

Windows Formsは、画面上(Visual Studio)で、ユーザーインターフェイスのボタンやテキストボックスなどを視覚的に配置することが出来て、その結果(例えば座標とか色とか)が、C#のソースコードとして自動的に生成・変更され、ビルド時に一緒にコンパイルされることでGUIアプリケーションが完成する、というものでした。ここで:

  1. コントロールの合成に難がある。例えば、リストビュー内にボタンやドロップダウンリストを配置するとかは、難易度が高い
  2. 時々、自動編集されたソースコードが壊れる。逆に、手動で定義するのは現実的ではない
  3. すべてのコントロールがWindows HWNDベース(ウインドウを管理する最小単位)のためにパフォーマンスが悪い

と言う問題があり、後継のUIフレームワーク、つまりWPFを作ることになったのではないかと思います。

XMLで記述することで、1と2は解決(2の自動編集は、今はRoslynがあるので、難易度は高いがXMLでなくとも実現出来ると言えるかも)し、HWNDによる単一のウインドウ上で、WPFコントロールをすべて自前で描画することにより3を解決した(場合にとってはDirectXを使う)、という感じです。

以下にXAMLで記述したユーザーインターフェイスの例を示します:

<Window x:Class="EpoxyHello.Wpf.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="EpoxyHello.Wpf" Height="450" Width="800">
    <ListBox ItemsSource="{Binding Items}">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <StackPanel Margin="5">
                    <Image Margin="5,3,5,0"
                        Source="{Binding Image}"
                        Stretch="UniformToFill" />
                </StackPanel>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
</Window>

この記事で挙げるXAML環境には:

  • XMLの階層構造で、ユーザーインターフェイスの論理的な構造を表す
  • コントロールの合成が可能。例えば(意味があるかどうかは別として)ボタンの表面に文字だけではなくテキストボックスを配置したりみたいな無茶も簡単にできる
  • コントロールのプロパティがインスタンス基準のものの他に、定義を後付けすることが出来る「添付プロパティ」という概念がある
  • 測定(Measure)と配置(Arrange)、と呼ばれる、ユーザーインターフェイス要素の配置位置を計算する、2段階のステップが組み込まれている
  • データバインディングと呼ばれる、コントロールと対になる情報を提供するインスタンスを結びつける機能がある

と言うような、共通の特性があります。逆に言えば、これら以外の部分は、それぞれのXAML環境によってバラバラと言えます。

(余談ですが、ユーザーインターフェイス以外の環境にXAMLを適用した例がありますが、あまりにマイナーであり、Epoxyとは関係ないため、本記事では省きます)

WPF (Windows Presentation Foundation)

そもそものMVVMの始まりは、WPFからでした。それも(私見ですが)はじめからMVVMの概念があったわけではなく、WPFのデータバインディングの機能・構造を体系化するとこうなる、というような後付けのアーキテクチャ概念だったと思います。

(それが、MVVMをやってると歯がゆい事がある理由だと思っています)

歴史はともかく、MVVMの基本的な構造は殆どこのWPFを基準にしています。私も、WPFを触っていた期間は長いため、まずWPF向けにEpoxyを作って、そこから移植を開始しました。

WPFは3.0の頃から、ほとんど構造は変わっていません。現在は、.NET Framework 4.8が最終バージョンであり、このバージョンがそのまま動く(事になっている)のが、.NET Core 3.0で移植されたWPFです。当初、.NET Coreへの移植という事から、WPFがマルチプラットフォーム対応したのではないか?という誤解がありましたが、あくまで.NET Core 3.0 on Windowsのみがターゲットです。

「マルチプラットフォーム対応は無理だったけど、もうすぐ.NET Frameworkがカンストしちゃうので、何とかWindowsだけでも動くようにしたから、とりあえず.NET Coreに移行してね?」という意図が感じられました。そういうわけで、今でもWPFを選択するという事は、Windowsでしか動作しないGUIアプリケーションを作ることと同義になります。なお、.NET 5においても(恐らくは.NET 6においても)WPFはWindows上でのみ動作します。

(ダメ出ししておくと、WPFを他のプラットフォームに移植するには、C++/CLIで書かれたコードを移植しなければならず、そこを全部リライトするか、またはまずVisual C++をターゲットの環境向けに移植して公開するところから始める必要があるので、UWPとその後継にあれだけリソースをつぎ込んでいるのを見るに、今後も移植は無いだろうなと思います)

余談ですが、WPFは成功したのでしょうか?私のように、次世代UIとの売り言葉に乗って、早くからWPFを触ってきた技術者にとっては、とりあえず無くてはならないフレームワークになっていると思います。一方で、2020年越えの現在でも、Windows Formsを使い続けるユーザーは居て、WPFやXAML環境の何れかに乗り換えようという気配はあまりありません(Epoxyはそのようなユーザーの事も考えて作っています)。

個人的に思うのは、WPFは段階的に移行できるAPIや手段を用意しなかったのが痛いのと、設計概念の変更に踏み込んだのに、その事を詳しく解説せず、コントロールインターフェイスの表面的に似ているところばかり強調していた(個人の感想)ことが大きいです。これは、次節のUWPでも同じことを繰り返しているなと思います。おそらく、今Windows Formsに留まっているデベロッパーは、このままなすすべなく経過するか、どうせ同じ苦労を味わうなら、もっと別の選択肢を選ぼう(.NETに限らず。今はいろいろ選択肢がありますね?)、と思っているような気がします。

EpoxyははじめにWPFで作りましたが、同じXAMLのインフラ(構造)を使うのだから、他のプラットフォームへの移植はそこまで難しくないだろうとは思っていました。それは、部分的に正しく、部分的に間違ってもいました。以降では、各XAML環境の簡単な紹介と、差についてまとめます。

UWP (Universal Windows Platform)

UWPとその前身のWindows Store Appは、Windows 8でアプリケーションのモダン化を目指したものです。スマートフォンアプリケーションが、検証されて配布されることで一定の安心感を得られるのと同じように、MicrosoftがUWPで作られたアプリケーションを検証して公開することで、ユーザーが簡単に安全に利用できるようにしようとしました。

が、様々な理由から、UWPが市民権を得ているとは言い難い状況です。EpoxyをUWPに移植するにあたり、技術的には、以下のような課題がありました:

  • UWPの構造が、WinMDというCOMをベースとしたメタデータインターフェイスで定義され、WPFの「ほぼ普通の.NETクラスベース」と異なる。具体例だが、WPFでは普通のメソッド実装だったのが、UWPで明示的インターフェイス実装として定義されていて、キャストしないとアクセス出来ない場面があった。他にも、C#が普通に使用できない情報が含まれている事がある(イベントのaddメソッドが値を返し、removeメソッドの引数が要求する型が異なるなんて想像したことがあるだろうか?)。Epoxyの移植作業以前にもちょくちょくこのような定義を見かけているので、割と沢山の落とし穴があるように思う。
  • UIスレッドの考え方の違い。WPFではすべてメインスレッドからアクセスすることになっているが、UWPでは基準となるウインドウ毎に別のスレッドを使用することになっているため、今までのセオリーをそのまま適用すると問題が起こることがある(そのUIをホストしているDispatcherを使用する)。但しこれは、UIメッセージキューをウインドウ毎に分けたいという願望が実現されたとも言えるので、必ずしも悪い事ではないが、面食らうと思う。
  • .NETメタデータアクセスに制約がある。特にリフレクションは、.NETの常識が通用しない場合がある。

もちろん、WPFのコントロールと、直接対応するUWPコントロールは、存在する場合もあれば存在しない場合もある(例: TextBoxクラスは双方に存在するが、DockPanelクラスに直接対応するものは無い)ので、XAMLの記述と言う点では、WPFとUWPに直接の互換性はありません。名前空間にも違いがあります。

.NETメタデータの操作については、リフレクションに関するクラスやメソッドは存在しますが、一部の使用には制約があります(コンパイル時に警告は出してくれる)。これは、UWPアプリケーションが、不正な動作を誘発させていないことを(Windowsストアの審査で)確認可能にするために行った仕様変更や制約です。

Epoxyの移植では、イベントをバインディングする機能の移植のところで、UWP専用のAPIを使用する必要がありました(しかも情報がほとんどなく、Rxのコードを参考にした)。個人的な意見としては、もっと段階的に移行可能な方法があったのではないかと思います。このような制約のために、リフレクションに限らず、例えばストリームI/O操作も.NETと異なる独自のクラスやインターフェイスが定義されているため、UWPを使う場合はそれらについて知っておく必要があります。

WPFと似たアーキテクチャのような体裁でありながら、XAMLとは無関係なところで、一般の.NET開発で常識と思われる知識が使えないことが多いと感じます。これらすべての欠点は、今俯瞰してみると、すべて「ネイティブコードへの変換時に、可能な限りの最適化を行う」ことを前提としているように思います。しかし、次節のXamarin Formsでも、iOSではネイティブコードへの変換(AOT)を行うわけで、効率に差はあるにしても、この欠点を受け入れてでも実現すべき事だったのかはわかりません。

私的には、もう少し冷静に判断して欲しかったところですが、スマートフォンとタブレットの迫る外圧に負けたのかもしれません…

Xamarin Forms

元々、Xamarin自体は、AndroidやiOSのアプリケーションをC#で記述可能で、ランタイムにmonoを使用する、という環境でした。ここには、AndroidとiOSを共通のコードで記述出来る、という意味は「含まれていません」。AndroidのアプリをC#で書ける、あるいはiOSのアプリをC#で書ける、という以上の意味は無く、それぞれの環境にあるAPIを使用します。

Xamarin Formsは、そこに共通のAPIをXAMLと共に定義し、可能な限り同一のコードで、AndroidやiOSのアプリを記述可能にしようとしたものです。例えば、ターゲットがAndroidやiOSのどちらであっても、共通のTextBoxクラスやButtonクラスを使います。

AndroidにはAndroidのスタートアップコードがあり、iOSにはiOSのスタートアップコードがあり、それらが共通のアプリケーションコードを呼び出す(共通コードでXAMLを定義し、Xamarin FormsのAPIを使う)、と言うような構造を採ります。

WPFからXAMLの歴史が始まっていることもあり、コントロールの扱いはWPFに似ていますが、現実的なスマートフォンアプリケーションを実装可能にするため、やはりWPFとは異なるコントロールは多いです。基本的にはUWPと同じように、ユーザーインターフェイス設計は、Xamarin Forms専用に考える必要があります。

Xamarin Formsを使う利点は、何といってもUWPのように独自の似て異なる.NET APIを使う必要が無いことです。例えば、ストリームI/OはStreamクラスを使えば良いし、リフレクションも基本的に.NETのそれと同一です。もちろん、iOSの場合はAOTコンパイルされるため、リフレクションAPIの使用には一定の制約があります(使うと実行時に死ぬという問題があります)が、APIが同一なので、.NETの一般的な知識がそのまま通用します。

Xamarinは、.NETでモバイル開発を行う選択肢としては、速い段階で実用レベルに達していたため、Xamarin Formsは実用可能な処理系として、急速に浸透しました。なので、商用サービスとしてのアプリケーション開発では、選択肢に加えることが出来そうなXAML環境の一つです。

なお、Xamarin FormsはUWPを実行環境とすることも出来ます。これは、ビルドした結果がUWPアプリケーションとして、ストアアプリ展開可能という意味です。もちろん、同じXAML環境であっても、UWPとXamarin FormsのXAMLには互換性は無く、この方法で実装する場合はあくまでXamarin Formsとして開発する事になります。Epoxyはもちろん、Xamarin Forms on UWPにも移植しています。

また、Xamarin FormsはGtkで動かすことも出来るため、macOSやLinuxのようなPosix互換環境(つまりはX Window System)をターゲットにすることも出来ます。但し、まだ標準的な手順でHello worldが動くというレベルに無いため、試してみるには追加の手作業が必要になります。別の例として、Unity(ゲームエンジン)にXamarin Formsを移植した例もあります。

Avalonia

Avaloniaは割と昔からあるXAML環境の一つで、最初からマルチプラットフォーム対応を謳っていました。Xamarin Forms同様に、WindowsだけではなくmacOSやPosix互換環境を選択可能で、開発時点ではまだ.NET Coreは無かったので、mono上で動作させることが前提でした。

現在は.NET Core/.NET 5環境でも動作させることが出来ます。XAML環境として見た場合、基本的なコントロールはサポートされており、私見ですが、リッチなユーザーインターフェイスでなければ、今現在マルチプラットフォームXAML環境としての完成度は高いように思います。XAMLはコントロールの合成が可能なので、この時点でも十分な能力を備えていると言えるのかも知れません。

その昔、かなり初期のバージョンで試したことがありましたが、その頃とは比較するまでもなく一発で動作し、不安定なそぶりはありませんでした。

Avaloniaの良い所は、WPFの構造上、あまりイケてない部分が改良されている事です:

  • WPFでは、object型やDependencyObject型から型キャストを要求される場面が多いが、Avalonia APIはそういう所がきちんとジェネリック引数を要求するように変更されており、コンパイル時型チェックが機能する
  • ほぼすべてのコントロールに、インターフェイス型が併設されている。このお陰で、インターフェイスを使った高度な抽象実装が可能だったり、独自のコントロールが基底クラスに縛られない
  • ターゲットプラットフォームがnet461, netcoreapp2.0, netstandard2.0なので、非常にニュートラルであり、多くのライブラリに適合する
  • C# 8.0のnull許容参照型が有効になっている(0.10.0現在)。ソースコードを参照すると、結構な勢いでC#の新しい構文で書き直されている。

全体的に自然な環境・綺麗な設計・実装で、アーキテクチャとして奇抜なところが無いので、.NETデベロッパは素直に扱えると思います。よく練られていて、WPFがこうだったら良かったのに、という感想です。一部のイベントハンドリングはRxを前提としていて、なるほどと思わせる部分があり、専用のReactiveUIライブラリを完備しています。また、現時点では、あまりトラブルを起こすことなく、LinuxやmacOS(注: macOSは未評価)で実際に動かせるというのも大きいでしょう。dotnet CLIにも対応しているので、すぐに始められます。

欠点もあります。それは、AvaloniaのXAML編集時に、インテリセンスが全く効かない事です(Visual Studio 2019上)。XAMLのインテリセンスは、この記事で紹介するすべての環境で満足行く状態ではない(C#インテリセンスとの比較)と考えていますが、Avaloniaはそもそも全くインテリセンスを使えないため、注意が必要です。恐らくauthorは問題を認識しているでしょうから、将来改善される見込みはあると思います。

また、マルチプラットフォーム対応として挙げているAndroidとiOSは、まだ追加の作業が必要なので、一般の開発者が扱える状態にはありません。この部分を見ると、Xamarin Formsとは真逆の状況にある印象です。

Avaloniaについて色々書きたいと思ったのですが、あまり書くことがありませんでした。その位良い実装だったという事です。Epoxyの移植時も、「そうだよ、それだよそれ!」って言いながらやっていたぐらいです。

(追記: なぜかAvalonia XAMLのインテリセンスが働くようになっていました。この記事を書いてから2~3日の話なので、何かアップデートがあったとかそう言う事ではない気がします。自席環境のせいかもしれません。XAMLインテリセンスは機能的に良くないと書いていますが、それでもあるのと無いのとでは雲泥の差です。もし、原因など分かれば、別記事で改めて紹介しようと思います)

Uno platform

Uno platformは最近出てきたXAML環境です。当然マルチプラットフォーム対応を謳っているのですが、wasm (Web Assembly、つまりはChromeなどのウェブブラウザ上で動作する)に対応しているなど、かなり尖っています。そのためか、少し前に大きな話題となりました(その頃は私事で忙しく、試している余裕が無かった)。

そのようなわけで、期待と共にEpoxyの移植に取り掛かったのですが、その内容は色々な面で驚きでした:

  1. UnoのAPIはUWP準拠。つまり、UWPの知識があれば、殆ど問題なくコードを書けることになる。UWPを下敷きにするのは、大胆と言うかびっくりですが、エコシステムを考えると、他のXAML環境のように一から設計するよりも良いのかも知れない。
  2. 大半のクラスに基底クラスが無い。え、そんな馬鹿な?UWPを下敷きにしているのでは?DependencyObjectがあるのでは?と思うが、何とDependencyObjectはクラスではなくインターフェイスとして定義されている。
  3. この時点で(既存のXAML環境の知識がある場合)、一体どうなっているのかわけがわからないと思うだろうが、何とDependencyObjectに相当する実装は、コンパイル時に自動生成されて(C#のpartialを使って)流し込まれる。
  4. 以上はUno on UWP以外の環境の話であり、Uno on UWPの場合は、大半の実装はUWPの実装をそのまま使う。つまりUnoがUWP準拠なのは、実際にUWPを使う場合はほぼそのまま、それ以外の環境では半自動生成された実装が流し込まれて、UWPのAPIにそれっぽく似せた状態にした上で、不足する実装を環境固有のライブラリで補う、と言う形式になっている

よくこんな事を思いついたなというのが感想で、アーキテクチャは間違いなく一番尖っています。メタプログラミングを駆使したXAML環境です。これを見たときには、半ばスルーしていたUWPを真面目に頑張ってみようかと思いました。

しかし、完成度がかなり残念な状態でした…

UnoをUWP環境で使う場合は、実際UWPのAPIをそのまま使うので、ちゃんと機能します。しかし、それ以外の環境では、エミュレーションライブラリに頼るわけですが、ここがまだまだこれからという状態です。例えば、UWP用ストリームI/O APIを使った以下のようなシンプルなコード:

static async ValueTask<ImageSource?> FetchImageAsync(Uri url)
{
    // Redditから画像をダウンロードする (UWP)
    var raStream = new InMemoryRandomAccessStream();
    await raStream.WriteAsync(
        (await Reddit.FetchImageAsync(url)).AsBuffer());
    raStream.Seek(0);
    var bitmap = new BitmapImage();
    await bitmap.SetSourceAsync(raStream);
    return bitmap;
}

は、Uno on UWPでは動作しますが、それ以外ではNotImplementedExceptionとなります。また、XAMLを試行錯誤中、ビルド時に何度もMSBuildタスクがエラーになってビルドが失敗することがありました。そのため、Hello worldぐらいならwasmでも動作して「おお、ブラウザで動くXAMLアプリが作れるぞ?」と思いますが、その後が続きません。

Epoxyでは移植を行いましたが、このような理由でUno on UWPのみ検証しています。

斬新で、他のXAML環境とも全く異なるアーキテクチャのアプローチでしたが(嫌いじゃない)、まだまだ問題を自力で解決できる強い人向けという感じです。私的には、もう少し時間を置いてから評価したいと思います。実際に完成すれば、かなり良い選択肢になる可能性はあると思います。

WinUI 3

WinUIは現在バージョン3の評価中で、内容は見も蓋もありませんが、UWPの焼き直しです。実際に移植を行ってわかったのは、名前空間を変更するだけで殆どそのままUWPのコードを移植できる、という事です。また、新たに、普通のデスクトップアプリケーションとして振る舞わせる、つまり、一般的なウインドウを持つようなアプリケーションを開発できるようになったのが新しい(古い?)所です。

UWPやWinUI、そしてこの後に紹介するMAUIも含めて、直近の.NETユーザーインターフェイスフレームワークは混乱を極めていて、UWPからWinUIに移行するのも、私は本当のところの理由を正確に理解していません。UWPでは、GUIの部分(つまり今回記事にしているXAMLとその周り)と、Windowsに新たに追加された非ユーザーインターフェイスAPIを分離する、という話をチラ見した記憶があるため、その結果がWinUIなのかも知れません。

参照パッケージをUWPからWinUIに切り替えると、当然シンボル名などが未定義になったりします。そこで、名前空間をWinUIのものに合わせてやると解消します。例えば以下のような感じです:

#if WINDOWS_UWP || UNO
using Windows.UI;
using Windows.UI.Xaml.Media;
#endif

#if WINUI
using Windows.UI;
using Microsoft.UI.Xaml.Media;
#endif

// ...
var brush = new SolidColorBrush(Color.FromArgb(...))

これはEpoxyの実装を一部抜き出したものですが、見てなにか気が付きますか? WinUIでは、一部の名前空間がUWPのままだったりします。これがユーザーインターフェイスとそれ以外のAPIの分離結果なのかも知れませんが、詳しく確認すると、SolidColorBrushは「Microsoft.UI.Xaml.Media」、Colorは「Windows.UI」に定義が存在します。もはや覚えられません…

勿論、C#であるならインテリセンスで自動補完が効くので、それほど問題ではないのでは?と思うかも知れませんが、Epoxyの移植時にはかなり混乱しました。

それでも、UWPのメンテナンスを行っている方は、WinUIへの移行に大きな負担は無いと思われます。同時に、デスクトップアプリケーションとしてもサポートされるので、今までスマートフォンやタブレットライクなアプリの挙動に躊躇していた方も使える環境と言えるかも知れません。

しかし、注意すべき点は、デスクトップアプリケーションとしてビルドした場合も、動作させるのにいわゆる「デプロイ」操作が必要で、そのためにパッケージングする必要もあります。ビルドすればEXEファイルが出来上がって、それをクリックすれば実行できる、とか、ファイルをzipで固めて別のマシンで展開して実行したい、のような事を考えていると、コレジャナイ感があると思います(つまり、xcopyデプロイは出来ません。WinUI3のissueとして数件挙がっていたので、やっぱりそうだよねと思った)。

そのような事もあり、実際にWinUIを直接使う必要性があまり感じられない(≒他のマルチプラットフォーム向けXAML環境を使ったほうが良いのでは?)と言う疑問を退けられませんでした。UWPのアップグレードと考えると、どうしてもUWPであるが故の制約(既に解説しました)が存在する事になります。デスクトップアプリケーションを開発すると考えた場合、これが制約に映ってしまいます。

今後もストアアプリでアプリケーションを展開したいと考えている場合は、有効な選択肢の一つでしょう。

MAUI

MAUIは次世代の.NETマルチプラットフォームユーザーインターフェイスフレームワークです。一方、Xamarin Formsの焼き直しという話をどこかで見ました。それだとちょっと見も蓋もないので、把握したことを書くと:

  • .NET 6のリリースに合わせて作業中で、まだ公開されていない(?)
  • Xamarin FormsはMAUIに置き換える予定で、とりあえず名前空間は変わります、という話は見た(アレ?これどこかで?)
  • MVU (Model-View-Update)アーキテクチャに対応。Cometと呼ばれるプロジェクトで進行中

結局、まだ動くMAUIの実装は参照できないようなので、Epoxyの移植を試みるのは諦めました。そのため、MAUIでわかっているのは上記の事だけです。すいません。

TAN-Yさんが、MVUを試す記事を書いているのを見つけました。

MVUは、ユーザーインターフェイスをXMLではなくコードで記述します。アーキテクチャも異なりますが、XAML視点で見た場合、誤解を恐れずに書くならば、ある意味Windows Formsへの先祖返りのようにも思えます。最終的な仕上げには、Visual Studio上でのデザイン時編集を可能にするというゴールがあると思われ、今ならRoslynがあるので、それを実現する土台はあると言えると思います。また、XAMLのインテリセンスが散々であることからも、コードで書いたほうが精神的にも楽なのは想像出来ることです。

EpoxyがMVVMのためのライブラリなので、MVUやCometについて何か考慮することは無いと思います。

MAUI上でCometが実装されるより、CometのようなMVUフレームワークが、他の既存のXAML環境でも使えるようになると良いと思います。私がちょっと不安に思うのは、これでまたリタイヤしてしまうOSSが出るのではないかという事です。AvaloniaもUnoも良い挑戦だと思います。相互に発展するような方法で伸びることが出来ると良いのですが。

まとめ

ここまで読んでいただいた方なら、XAML環境の決定打は存在しない、という現実について大体わかっていただけたかなと思います。個人的には、できればデスクトップ開発はAvaloniaにシフト、モバイル開発はXamarin Formsという使い分けで行こうかと思っています。

デスクトップ開発は引き続きWPFを死に絶えるまで使うという選択肢はありだと思いますが、私の場合メインマシンをUbuntuに乗り換えてしまったこともあり… (EpoxyはUWPとかあるので、結局Windows上で開発していますが)

他にも、UWP系列を使わないのは、UWPの技術的な制約だけではなく、Microsoftの標準技術という位置づけでありながら、.NET側から見ると異端すぎること。そしてビルド環境が特殊なせいで、ビルドの再現性やCI/CDに馴染みにくい(イミュータビリティやポータビリティが殆どないので、自動化でいつも頭を悩ます)事を挙げておきます。WPFのビルド環境が.NET Coreで遜色ない方法でサポートされ、非常に親和性が高くなったので、余計にUWPの欠点が目立ちます。

このオチはある程度予想していたものですが、元々Epoxyが、複数の異なるXAML環境でも同じように使えるMVVMライブラリという落とし所を目指していたので、Epoxyの存在理由も含めて、丁度良かったかなと思っています。

(開発で使用するXAMLの環境を何にするのかは、それぞれの個人やお仕事上の事情があるでしょう。この記事は、いずれかのXAML環境への移行を推奨しているわけではありません。Epoxyの開発理由もそこにあります)

また、MAUIにはあまり過度の期待はしていませんが、一方でMVVMアーキテクチャがMVUに取って替わられる可能性は十分ありうるとも思います。Epoxyの開発は、MVVMでのあらゆる面倒くささが動機づけになっているので、それが解消する可能性があるならMVVMにこだわり続ける理由はありません。果たして実用に足りるのか?2022年には判明するかも知れないですね。

SPARC LTのビネガーシンドロームを直した

SPARC LT、SPARCの名前を聞いたことがある人でも、実物を見たことは無いという人の多い、謎マシン。私はこれを2台持っています。

但し、一台は起動不能でかつ液晶が割れているという状態で、要するに部品取り用です。

今回、これのレストアをしたので、全世界で興味ある2~3人ぐらい(不明)のために、情報を残しておこうと思います。

SPARCの歴史(の一部)

このSPARCマシンは東芝製で、コンピューター博物館にも収集されているようです。有名なのは「プラズマディスプレイ」と呼ばれる、朱色に近い赤色のモノクロとして表示されるディスプレイを備えたバージョンで、私も一度だけ実物を見たことがあります。

SPARCプロセッサを搭載したワークステーション(もはや死語ですが)は、かなり多くのシェアを占めました。何といってもSun(現ORACLE)が作ったSPARC Stationシリーズが有名です。ただ、SPARCプロセッサアーキテクチャは、今でいうARMのように仕様をライセンス配布したこともあって、Sunの事実上の純正CPUの他にも、CPUを設計・製造したメーカーがいくつかあります。また、その仕様に準じれば、Sunが作っていたOS「Solaris」が、建前上、ドライバーを書くだけで動くということも、HPC市場で広まった理由かもしれません。

東芝は当初、SPARCの仕様に合わせたアーキテクチャのワークステーションを、完全に独自に設計・開発しました。それがこのSPARC LTです。SPARC LTはSun製のSPARC/Super SPARC CPUを使っていましたが、それ以外の部分は独自に設計したものでした。この上でSunが開発したSolarisが動きます。

しかし、「建前上」と言った通り、Sunの純正Solarisは無修正では動かず、低レベルをいくつか独自にカスタマイズしてパッケージングした、SPARC LT専用のSolarisが必要だったのです。

アブノーマルSolaris

Solarisを使ったことのある人は、プロセッサアーキテクチャとして、”sun4c”, “sun4m”, “sun4u”と言った名前を知っていると思いますが、このSPARC LT専用Solarisはそれらのどれでも無く、”tsb”というアーキテクチャ名でした。ええ、私も手に入れる前には独自っぽいというのは目にしていましたが、実際に入手してから初めて知りました。

環境構築したことがあれば分かりますが、これは結構大変です。中身はほとんど普通のSolarisで、実際のところsun4c相当なので、sun4c向けのバイナリが無修正で動いたりするのですが、例えばgccのコンパイルとインストールにおいて、”–host=sparc-sun-solaris”とかやるところを”–host=sparc-tsb-solaris”とかやっても、そんなの知らんわ! みたいにはじかれてしまうため、いちいち微細な修正が必要です。

まあ、それでも無理やりシンボリックリンク張ってごまかしたりして、何とかgccを入れる事が出来れば、bashとか入れてどうにか使えるところまで持っていく事が出来ます。

(補足しておくと、Sun SPARCompilerのライセンスを持っていない場合は、まずCコンパイラをどうしよう、と言う所から始まります。何を言っているのかわからなくても問題はありません)

ところで、ほかのOSはどうなのかという話ですが、まず、現代のOpenSolaris(もはや現代でも無くなってしまった…)は、リリース当初からsun4cはサポート外なので、tsbは論外です。

Linux方面はDebianがしばらくサポートしていましたが、そもそも64ビットSPARC(sun4u)主体で、32ビットの方は有志が頑張るみたいな状況でした。それもずいぶん前に切り捨てられて、今はSPARCをLinuxで動かすという目は無いように思います。

最も普通に動きそうなのはNetBSDです。32ビット/64ビットSPARCを両方ともまだサポートしていたと思います。Solarisに比べても圧倒的に軽いので、実用(?)するならNetBSDを使うのが良いかもしれません。しかし、残念ながら、SPARC LTは東芝独自の設計(tsb)のために、低レイヤーで非互換の部分があり、動きません…

自力で調べた範囲では、NVRAM周りの構造とメモリマップが違っているのは見て取れたのですが、OpenPROM(今でいうUEFIシェルみたいなものです)のForthが壊滅的にわからなくて、あの上でいろいろ調べる気にならず。

結局、SPARC LTを動かすには、Solaris(しかもtsb)を使うしか選択肢はありません。

余談ですが、SunのSPARCマシンでは必ずやることになる、NVRAM健忘症への対処は、tsbでは必要ありません。NVRAMはあるのですが、そこにシリアルナンバーやMACアドレスなど、重要な固有IDを保持していないようなのです。NVRAMが健忘になっていても、IDが全部ffになったりとかしません。このことから、NVRAM辺りにも非互換がある事がわかります。

ニコイチ計画のはずが

前振りが長くなりましたが、ずっと温存していた計画があって、2台のSPARC LTは次のような状態だったので:

  • どちらも同一モデル。TOSHIBA SPARC LT AS1000/C80 。プラズマディスプレイではなくカラー液晶(この頃は非常に高価だった。但し発色はTNより悪い味)
  • No 1. 完全に動作する。ディスクはSSDに変更してある。ガワが汚い。
  • No 2. 完全に壊れている。液晶も割れている。ガワがきれい。

これらをニコイチにして、きれいなSPARC LTを完成させようと思っていました。しかし、SPARC LTはすごい重い上に、全バラするのが大変なので、まあそのうち気が向いたらと思って保存していました。その間にも、こんな催しに出張参加して、奇異の目で見られたりして、喜んでいました :)

そして先日、ちょうど良いタイミングだから今ニコイチをやろうと思って、出してきたのです:

なんじゃこりゃーーー!! 液晶がぁーーー!!! ガビガビになってるーーーー

しかもなんだか酸っぱい臭いが…

これ、知らなかったのですが、「ビネガーシンドローム」と言うらしいです。主に古い写真のフィルムなどで発生するらしく、貴重な映像資料の管理でも問題になっているようです。そして我々の業界では、液晶ディスプレイの表面に使っている「偏光板」が、この化学反応を起こしてこうなってしまうと。レトロPCの保存とかでも問題になっているようです。

写真で見ると斜めに割れているように見えますが、フィルムが劣化して斜めにしわくちゃになっているという感じです(フィルムの収縮具合によっては、ガラスが割れてしまうこともあるようです)。

ちょうど、これを発見する一週間前にも、某氏が古いノートPCを出して来たら液晶が変なことになっていたって騒いでいた(割れていないのに変な事ってなんじゃそりゃ、と、笑いながらスルーしていた…)ので、これの事かーー!! という感じ。自分の身に降りかかるとは思わなかった。人の不幸を笑ってはいけない。

修復方法

博物館で収集されるような物品を持つものとしては、修復する義務がある、などと勝手に理由付けしつつ、方法を探る事に。

どうやら、ガラスにまでダメージが行っていなければ、偏光板を交換すれば良いようです。が、簡単ではなさそう。まず、偏光板はガラス面に結構強固に貼り付けられている事が多いので、これをきれいにはがす必要があります。当然、その過程で基底のガラスを割ってしまう危険があります。もう一つの問題は、偏光板の入手の問題です。単なる偏光板は、今やAmazonでも入手可能です(以下は一例)。

https://amzn.to/38t9f6A

これらは科学の実験教材として売られています。が、液晶に流用すると、透過率があまり良くないようです。そのため、液晶専用に設計された偏光板を使うのが良いのですが、途端に入手性が悪くなります。

また、サイズの問題もあります。液晶の場合、偏光角度が対角(45度)になっていることがあるらしく、つまり0度又は90度になっている偏光板だと、斜めにして切り落とす必要があります。そうすると、必要なサイズをかなり大きめに見積もっておかなければなりません。

うーん、めんどくさい、これはめんどくさいぞ…

修復した!

ビネガーシンドロームLCDの修復は、丁寧に偏光板を剥がす事なのですが、ガラス割ってしまいそうだし、ガチガチに固着しているためにとても剥がせる気がしない。やるにしても、万が一を考えて、代替のLCDを調達できるかどうかを確認しておく必要がありました。

そして、全く同じLCD型番がAliExpressの業者で扱っている(!!!)のを発見し、これで替えられるなら修復しなくても良いか、という事で取り寄せました。
液晶のスペックが現代のに比べてアレなので、相対的にかなり高額の出費となりました (金額は自分で言うと萎えるので伏せておきます…)。

それでは見てください: TOSHIBA SPARC LT AS1000/C80 resurrected from vinegar syndrome LCD

また、その業者が最初に送ってきたものは液晶バキバキで orz、代替品を要求して更に2週間後、やってきたのは基板の銅箔が析出してるデンジャラスなやつ… これらをニコイチして、ようやくまともなLCDを手に入れました。

AliExpressの業者は、どうも大量に新品・中古・偽物のパーツを入手して備蓄しておいて「全て新品、動作確認済み!」と称して販売しているところが多いようです。レアLCDでここ以外に売っている所がないからどうにか堪えて冷静に取引しましたが、そうでなかったらブチ切れものだった…

(ここの交渉も面白かった部分なんですが、面倒なのでカット)

レトロマシンの手入れは大変です。欲しいと言うかどうかは分からないけど、Living Computer Museumに寄贈しようかとか考えてしまった…

Raspberry Pi 1をNanoPi NEOに置き換える

(えっと、久々の更新ですが、Buildのお話は皆さんブログとか書いているようなのでお任せして、デバイス系のネタです)

かなり長期に渡って運用していたRaspberry Pi 1が気になっていて、特に異常は発生していなかったのですが、そろそろ更新しようかなと思いました。

実物はこれ:

RS componentsのケースに入れていました。RPiなので、GPIO繋いでIoTっぽい何かをするみたいな有用な使い方ではなく、完全にUnixマシンとしてDHCPとDNSサーバーの運用をしていました。RPiで運用していた理由として:

  • 入手が容易であること(壊れたりしたときにサクッと入れ替えられる)
  • 低消費電力
  • 有線LAN
  • USB(UPSのプライマリ受信)
  • 扱いが堅牢である(ファンNG、つまり発熱があまりないこと。また、ボードむき出しとか不可)

という辺りで丁度ミートしていました。もちろん、動かす用途がDHCPとDNSサーバーなので、スペックもそれほど高い必要はないです。DHCPとDNSは他のマシンと兼用させることも不可能ではない(例えばNASにやらせる)のですが、経験的にそれらがあぼーんした時に面倒な事態に陥りがちだったので、独立して堅牢に動作することを求めています。

(もう少しいうと、static A recordの管理と、DHCPによる自動レコード更新をやりたいというのが背景にあります。それがなければルーターの代理応答だけでいい。ところで最初の1年ぐらいは、今は亡きcallweaver入れて、SIPサーバーとしてフレッツ光回線の電話側を収容していました。パフォーマンス的にはギリギリ、ちょっとプツプツノイズが入る感じでした)

しかし、もう忘れていましたが、RPi 1もそろそろ10年選手になるわけで、流石にちょっと心配です。不安の最大要因は、意外と発熱があることでした。写真にあるようにヒートシンクを付けておきましたが、長期間の運用では温度が高いとコンデンサーの寿命を削っていきます。そんなわけで、壊れていないけどもう入れ替えようと言うわけです。

そうそう、Unix、とお茶を濁す書き方をしましたが、これはRaspbianではなくNetBSDを動かしていました。NetBSD、管理系はシンプルで扱いやすいんですよね。私の出自もBSD系ではあるので。しかし新興ハードに対して選択肢が厳しいのも事実なので、Linux系への移行も前提とすることにしました。

VoCore2 Ultimate

で、最近だとRPi4が出ているわけですが、上記の点でもう選択肢から外れました。RPi ZeroシリーズにはLANがないのでやっぱりダメ。それで、他の選択肢を探す事にしました。ググって初めに目についたのは、VoCoreのVoCore2 Ultimateです。

ちっさ!!! ブレイクアウトボックスじゃないですよコレ…

しかし、小さすぎるのも扱いにくい要因になるので、一応ざっと仕様を眺めたのですが、CPUはMediaTekのMT7628でMIPS系、クロックは580MHzとかなり低いので、消費電力は期待できそうです。WiFiも載っているようですが、今回は使わない(&TELなんとか)ので問題はない。有線LAN 100MとUSBがあるので、I/Oの問題はなさそう。

あとはOSですが、LinuxベースのOpenWRTのようです。OpenWRTは使う機会がなかなか訪れないためまだ試していませんが、DHCP&DNSの運用なら却って面倒がないのかもしれません。但し、RPiのDebianやUbuntuのように、既存知識は活かせない可能性があります。

まあ、考えてても分からない、そこまで高くない、実際に来るとなると時間が掛かると思われたので、それ以上悶々とせずにポチりました。

NanoPi NEO

おい、VoCoreはどうなったんだとツッコまれそうですが、実はaliexpressを眺めていたら、こういう写真に出くわしたのです:

そういえば、昔見たことあるな、と。で、改めて調べてみると、FriendlyELECNanoPiシリーズでした。このシリーズは結構沢山出していて、RPiっぽいフォームファクタやら、豪華なインターフェイスがついているものや、VoCoreのように最小限に絞ったものまで色々ありました。

その中で写真のデバイスは、NanoPi NEO2にOLEDディスプレイとボタンを付けた、NanoPi NEO2 Metal Complete Kitという物のようです。

(なお、最初にaliexpressの写真上げましたが、劣化コピー品掴まされてトラブルに合わないように、本家から買いましょう)


このOLEDとボタンというのは、微妙にCobalt Qubeを思い出してちょっと欲しくなるんですが、どうせ今回の用途で使うことはないので、破損箇所を減らす意味でももっとミニマムなやつでいいと、探したのがこのNanoPi NEOです:

これも大きさという点ではVoCoreに毛が生えたぐらいの小ささですが、堅牢なアルミケースに入っているというのが気に入りました。なお、アルミケースということもあり、WiFiは使えません。他のラインナップではWiFiを内蔵しているものもあるので、気になったら探してみると良いと思います(RPiとの差別化が難しいかもしれませんが)。

あと、基本的な仕様としては、CPUがAllwinner H3 512MB 最大1.2GHz、armhfアーキテクチャのQuad coreなので、消費電力や発熱はちょっと心配です。VoCoreよりはRPiに近いんですかね。OSはFriendlyWRT(要するにOpenWRT)、FriendlyCore(Ubuntu Core)、DietPiなどが使用できるようです。あと、NanoPi NEOはNEO2というシリーズがあり、そちらは更にパワーアップ(arm64)しているようですが、先日見たところでは在庫切れでした。興味はありましたが、今回の目的には合わないと考えて、素直に上のNEO Metal Basic Kitにしました。

とにかく安い(VoCoreより安いってどうなってんの)ですが、今の時期にChina Postだとどこかに行ってしまう不安があったので、EMSを選択しました。それだとちょっと高くなってしまうため、予備と遊び用とで、合わせて3台注文。一週間ぐらいで届きました(この時点でVoCoreはまだ届かず…):

(ところで見直してたら気が付いたのですが、ZeroPiの方がよさそうですね。GPIOは全く付いてないですが、Gigabit Ether使えてNEOより安いです。こうも安いとどんぐりの背比べですが。買いなおそうかな…)

もう本当にセットトップボックスとか完成されたかのようなBox linuxマシンなので、あえて蓋も開けていません。実際に電源を入れてみましたが、熱くなるというよりは、ほんのり温かみを感じる、ぐらい(apt upgrade実行中)なので、熱的な心配はRPi 1よりも良さそうです。また、消費電力を測ってみましたが、最大目視で0.45A、アイドル時は0.14Aと、もうこれで良いのでは?という感じです。

Linuxディストリビューションの選択

さて、公式ドキュメントによると、いくつかのファームウェアからインストールしたいOSを選択できるようなんですが… ファイル名にxenialだとかjessieとかで不安しかない。2020年ですよ今。

で、もう少しリサーチしてみると、どうやらArmbianというディストリビューションでDebianかUbuntuライクな実装がコミュニティベースで配布されているようなので、こちらで行くことにしました(一応公式ファームウェアのうち、friendlycoreはユーザー名とパスワードがドキュメント通りではないのでログインも出来ず、jessieはログイン出来たものの、古すぎる上にaptのシグネチャファイル弄ったあとがあるので速攻で破棄…)

Ubuntuに一番慣れているので、Armbian Bionicを選択してwin32diskimagerでSDカードに書き込んで、サクッと起動しました。

(2020/05/20執筆時、ダウンロードしたイメージのバージョンは5.4.20.7でした)

LANを繋いでおいてください。初回起動で30秒かそれ以上待ってから、angryipか何かを使ってIPアドレスを調べます。又は、自動的にDHCPでIPアドレスを取得しているはずで、名前はnanopineoなので、DNS自動レコード更新をやっている環境なら、単にssh nanopineoで繋がると思います。

初回はユーザーroot、パスワード1234で、ログイン時にパスワード更新を求められます。


You are required to change your password immediately (root enforced)
_ _ ____ _ _ _
| \ | | _ \(_) | \ | | ___ ___
| \| | |_) | | | \| |/ _ \/ _ \
| |\ | __/| | | |\ | __/ (_) |
|_| \_|_| |_| |_| \_|\___|\___/

Welcome to Armbian Bionic with Linux 5.4.20-sunxi

System load: 0.00 0.06 0.05 Up time: 7 min
Memory usage: 12 % of 491MB IP: 192.168.***.***
CPU temp: 32°C
Usage of /: 3% of 29G

[ General system configuration (beta): armbian-config ]

Last login: Sat May 23 11:59:19 2020 from 192.168.***.***
Changing password for root.
(current) UNIX password:


ディスクの状態(SANDISK Extreme PRO 32GB V3A1、ファイルシステムの拡張は勝手に行われます)

root@nanopineo:~# df
Filesystem 1K-blocks Used Available Use% Mounted on
udev 181392 0 181392 0% /dev
tmpfs 50336 2928 47408 6% /run
/dev/mmcblk0p1 30391004 778512 29278964 3% /
tmpfs 251680 0 251680 0% /dev/shm
tmpfs 5120 0 5120 0% /run/lock
tmpfs 251680 0 251680 0% /sys/fs/cgroup
tmpfs 251680 4 251676 1% /tmp
/dev/zram0 49584 884 45116 2% /var/log
tmpfs 50336 0 50336 0% /run/user/0
root@nanopineo:~#

あとは、apt update, apt upgradeで更新。実にスムーズで、ファーストステップまでに何かにハマるところはないと思います。ドキュメント不要でここまで完成度高いと、人に勧めやすい。良くわかってるじゃん(公式配布じゃないけど)。

ArmbianはVoCoreに対応していないのが惜しいな…(MIPSだから仕方がない)

DHCPとDNSの構成はもとのRPi 1からconfig類を持ってきて細部を修正して、isc-dhcp-serverとbind9を入れて、無事に差し替えを完了しました。このconfigは昔自力で作ったんですが、公開しようと思うと色々マスクしなければならず… もしDHCP-DNS自動連携に興味があるなら、この記事を参照すると良いと思います。記事の公開日や想定している環境は古いですが、configの構成のところはほぼ同じです。

UPSの監視もする必要があるのですが、後日やることにします。


おまけ

root@dhcpdns:/home/kouji/test# apt install mono-devel
Reading package lists… Done
Building dependency tree
Reading state information… Done
mono-devel is already the newest version (4.6.2.7+dfsg-1ubuntu1).
0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.
root@dhcpdns:/home/kouji/test# cat Program.cs
using System;

public static class Program
{
public static void Main(string[] args)
{
Console.WriteLine("Hello mono on NanoPi NEO!");
}
}

root@dhcpdns:/home/kouji/test# mcs Program.cs
root@dhcpdns:/home/kouji/test# mono Program.exe
Hello mono on NanoPi NEO!

root@dhcpdns:/home/kouji/test#

詳しくはビデオで: Windows Formsでマルチプラットフォーム?

“パターンでわかる! .NET Coreの非同期処理” と楽屋ネタ

先日、“.NET Conf 2019 meetup in AICHI”を開催・登壇したので、その話をまとめます。

2つ目のセッションの話を先に

私は2つのセッションで喋りました:

1. “.NET Core 3の更新内容・紹介”
2. “パターンでわかる! .NET Coreの非同期処理”

最初の方はキーノートのまとめ的なものにしようと思っていたのですが、その話はまた後で。

非同期処理のセッションですが、今回はあんまり内部の技術的な話ではなくて、.NET Core 3.0とC# 8.0で.NETの非同期処理が一応の完成を見たと思っているので、改めて全体を網羅するような内容にしようと思ったものです。その上で、

* 度々非同期処理の誤用が話題に登ったり、辛しみのあるコードを目撃することから「こんな」記事を書いたりしていたのですが、もっと網羅的にまとめたいなーと思っていたので良い機会として共有しよう

というところを目標にしました。

スライド: パターンでわかる! .NET Coreの非同期処理 (SlideShare)

セッション50分でこの分量は無理ゲーなのはわかっていたので、スライドは後で読み返すことが出来るような構成にして、如何に喋りで補えるかに注意しました。が、セッションを聞きながら見ていない場合は、少し足りない(わかりにくい)部分があるような気はしています。セッションビデオと両方で補完してもらえればと思います。

なお、最近twitter上で非同期の事について話ししたものをいくつか置いておきます。この辺の話が今回のセッションをやりたいと思う動機につながっています:

上記の内容や過去の自分が書いた記事、構文的な書き方はわかるんだけど、具体にどう書いていいのかわからない… みたいなところから、ちょっとずつネタを拝借。

で、最初のセッション

最初のセッションの方は、参加した方には普通に楽しんでもらえたと思っているのですが、懇親会で舞台裏の話をしていたら、それをブログにまとめろとのお達しが… なので、普段、この方面の話は書かないのですが、楽屋ネタを書きます。

Scott Hanselmanこのセッションは、当初は私が全部.NET Core 3.0のキーポイントをまとめて1セッション構成するつもりだったのですが、水面下ではMSHQのScott Hanselmanが、日本のコミュニティ向けに何か喋ってくれるかもしれない、という話がもちあがり、それならその方向から構成を考えるために、決定するまで待つかと思っていました。

みなさんご存知の通り、大変に忙しいキーパーソンなので、この試みがキャンセルされるという事態も考えられましたが、関係者が頑張って調整に入っているようなので、ギリギリまで待つつもりでした。

ところで、コミュニティイベントを開催された方なら同意してもらえると思うのですが、イベント開催のためには以下のようなハードルをこなす必要があります:

* 会場の地理的なアクセスのしやすさの検討
* 会場収容人数
* 会場は有償か無償か。有償なら費用をどうするか
* プロジェクターや音声設備の有無・レンタル費用
* WiFiや有線LANなどの通信経路の確保
* 運営メンバーの募集と決定
* 登壇者の募集・登壇内容の調整
* ビデオ撮りあるいはストリーミングライブ配信するのかどうか(合意形成も含めて)
* チェックイン作業
* スポンサーを募集するのかどうか
* 集金や支払いなどの金回り
* ノベルティーなどのグッズ・抽選の有無
* 提供するおかしなど
* 終了後のハッピーアワーや懇親会の検討・場所の確保と誘導・支払い徴収

運営は本当に頭の痛い問題なのですが(これで折れてしまう方もいるので、私は自分が出来る範囲で出来ることしかやらない事を守るスタンスです)、登壇する側としても、セッションネタを考える場合に、上記の制限条件からネタ構成を考える必要も出てきます。例えば:

* 収容人数: 10名・30名・50名・100名かそれ以上、によって、セッションの構成(しゃべる内容や構成、進行方法)を変えたほうが良い。人数が多くなると、後ろの方の席の人は、スライドが読みにくいだとか、声が聞こえないとか、没入感が得られない、といった潜在的な問題が発生しそう。
* プロジェクターや音声設備: スライドの色校正(たいてい黒地だとハマる会場は多いが、パリッとした映像を出せる会場なら、逆に黒地が映える。動画を流す場合は音量に注意しないと、うるさいとか音が小さいとかの問題につながる。しかし、実際に会場で調整しないと思ったとおりには行かない(マイクとかミキサーなどの設備にも影響される)ので、セッションのネタにするのは結構なリスクとなる)
* WiFiや有線LAN: 参加人数が多いと、特別に設計されたWiFiアンテナなどを備えた会場でないと、話にならない事態になります。私はあまりオンライン向きのネタを扱わないので良いのですが、最近だとクラウドアクセスをデモの前提にする必要があったりして、非常にやりにくいでしょうね。逆に、ネットワークの信頼性が高い事が前提にできるなら、クラウドだろうがYouTubeだろうが、セッションの子道具としてバリバリ使えます。

まあ、細かいところを挙げるとキリがないのでこのぐらいで。つまり(人にもよりますが)、セッションネタ一つ決めるのにも、こういう要素が絡んできます。なので、登壇する側は事前にこれらの要素が早く確定していれば、それだけ登壇者や参加者にとって、実りのある内容に詰めていける可能性があるわけです。

で、冒頭の話につながるのですが、今回は会場を日立ソリューションズさん(HISOLさん)にお借り出来たのですが、その前にもいくつか候補があって調整していました。全ての会場が完全に同一条件なら、「まあ確保できるところでやればいいよね」と、問題の一つが片付くのですが、現実はそうじゃないのでヤキモキします。

結局、検討していた会場は確保できなかったのでHISOLさんに打診したところ、快諾得られたので良かったのですが、確定までに2週間前までかかったこともあって、なかなか会場を公表できませんでした。登壇者のみなさんごめんなさい。

こういう点で、名古屋は非常に苦労するというのはなかなか理解してもらえないところでツライです。東京に近いところは楽そうだなと思うし、もっと地方ではそもそも会場になる場所さえ無く、勉強会というものを想像するのも難しいと言うのが現実でしょう。

で、問題は上記の会場環境の話に繋がります。HISOLさんの会場では、残念ながらパブリックなWiFi設備を借りる事が出来ません(もちろん有線LANも)。すると例えば、クラウドベースのデモとかかなり厳しい(デモの悪魔に取り憑かれる可能性が高い)わけです。

言わずもがな、YouTubeを参照したりなど出来ず、オンラインで遠隔会場と連携しながらビデオチャットでセッションを進行するという、海の向こうでは割とポピュラーな手法も取れず… これはイベントの一体感もあるので、ある程度の規模のイベントではやりたくなることの一つです。

実際、もしかしたらHanselmanさんとTeamsで完全オンラインでの相互コミュニケーション、というのも検討の一つとしてありましたが、まあ会場的に無理なので、実現するとなったら有償で会場を確保するかということも考えなければなりません。

(そうすると参加費500円とかでは無理ですし、そもそもそんなギリギリで会場を予約できるか怪しいし、堅苦しい契約書をかわさなければならないですよ、個人で。請求や領収、今だと税金をどうするかみたいな話も出てくるかも)

なので、主催が本当に小規模な勉強会を小規模な会場でやる分には良いんですが、ちょっとでも規模が大きくなり始めると、非常に辛くなります。そこを踏ん張ってやるからには、それを超える動機づけが必要になるでしょう。私の場合は自分の技術的な知見をシェアする、まあ淡良くば技術ポートフォリオとして誰かの目に止まるかどうか、ぐらいで、これで食っていく気は全く無い(食える気がしない)ので、必然的に掛けられる労力もそのぐらいになります。

さて、そういうわけで、様々な軸での考えがあるため、これらをどこにバランスさせて、セッションのネタにするのか、というのは、真面目に考えるととんでもなく難しいです。今回のネタとして、実は以下のような移り変わりがありました:

ごく初期

.NET Confの開催より後に開くことを前提に、.NET ConfでMSHQで公開されるビデオをネタに同じような感じでローカライズバージョンをやればいいかな(鼻ホジ)

環境リッチな会場で開催できるか

.NET Confを拾い上げるにしても、様々なセッションから何をネタにするのかは、上で挙げたような要素が、特に会場確保によって大きく変わるので、まずはどこの会場が使えるのかが最大の課題。

第一としていた候補はダメ。あと2~3考えていたが、それらはどれも環境的に厳し目なため(WiFiが無いとか、ちょっと会場の母体の趣旨と合わないとか)、この時点でセッション内容についてはかなり幅が狭くなる。

Hanselmanさんがビデオメッセージを送ってくれる(あるいはリアルタイムで!)かも??という打診

すごい話だけど、運営側としては、完全に確定だからあとはやれるように組む、というのと違い、不確定だとどう動けば良いのかわからずツライ(それもギョムの合間を縫って行動する必要があるので)。早く決まって欲しい… 早く… 早く…

別の会場を打診

アテの一つは早々に辞退(上記の後者側)、もう一つを打診。

自分の登壇ネタを検討する

.NET Confのセッションを拾い上げるという案は、会場の自由度があるから考えられる手法であって、もうそれは無理なので、結局自分でネタをひねり出すしか無い。非同期の話をしたいから、まあ2枠って事になるかな。

で、問題はもうほかの登壇者さんの募集とか打診とかして枠が埋まりつつ合ったので、もしHanselmanさんの話が現実化する場合、どこの枠(時間帯)をその話にするのかを考える必要がある。内容がキーノート的なものなら、時間的にはやっぱり最初に喋って欲しいけど、そうでないならべつの時間帯でも良いわけだけど、果たしてオンラインで喋るとすれば、本当にJST(日本時間)に合わせてくれるの?(いや、そもそも無理ちゃうの?)

WiFiすら使えない環境でどうやって実現するの? 安定性は? Hanselmanの生の話が聞けるなら、みたいな期待で参加者が押し寄せて、出来ませんでしたテヘッ、みたいな事態を想定しなきゃいかんのか、とか…

非常にツライ…

正直なところは、予定が決まらなかったら、こちらから辞退することも視野に入れていました。自分のネタを考えているはずが、全体の進行まで気になり始めて、それどころじゃない感じ。全然集中できない…

気を取り直してネタ考案

最悪、キャンセルになって自分で全部仕切る(つまり2枠考えて喋る)ということを想定して準備。なんか色々頑張っても無駄に終わりそうで、それもつらい。 最初の方は、.NET Confのキーノートを読み込んで概要にまとめて、面白そうなトピックを実演を交えて喋る、みたいに構成するしかないかな(もはやキャパオーバーだけど、主催自分だから仕方ない)。

3日前の夜に、Hanselmanさんのビデオが送られてきた!!

もうこの時点では、8割方断る方向で心中決まっていて(でないと間に合わない)、手間が空いたときに断りのメッセージを送るしか無いか、と、散々たる気持ちでいたのですが、こっちからメッセージを送る前に向こうから来た。しかも、いきなりビデオが来た… マジか… もうこれで構成するしか無い(非同期のセッションをほぼ書き終えていたので、首の皮一枚つながった感じ)

ということで、20分のメッセージをまずはAzure Video
Indexerで字幕データを作り
、それを補正(わりと全自動では駄目な感じの字幕なので、手修正する)して、ffmpegで合成して英語版字幕付きビデオを作る(成果物はここ、但しビデオそのものは含まれていません。時間の関係で、訳を直したりタイムラインを調整したりと言ったところはまだ細部が荒いです。関係者はエンハンスして欲しいです、是非に)。

2日前

ビデオの内容を読み込み、字幕がアレだったことから日本語字幕化する事を考え始めた。でも、SRTファイルをそのままGoogle Translatorにかけるとフォーマットが壊れてしまうのと、センテンスがつながっていないのが原因でダメな翻訳になってしまうこともあって考えあぐねていた(Azure Video Indexerで日本語訳出せばいいじゃん、と思うかもしれません。機能的にはありますが、まあ、察してください…)

それと同時に、セッションをどう進行するかを考えていたけど、ビデオ流しながら補足を入れつつ、(50分枠なので)後半は小さいコードをライブで何かデモするか、そうだねぇ、あんまりどこにも話題にかかっていないgRPCとか良いかもしれない、と考えて、概要を調べる。

その後、字幕の調整のために、ぼーっとSRTファイルの中身を見てたら、これ、フォーマット簡単だよね? もしや自分でパース出来るのでは?と思いつく。そこで、あえてビデオを流さず、いきなりライブコーディングでパーサーを書き、Google Translator APIを使って日本語に翻訳(4G経由の通信なので、ここにリスクはあるが)して日本語字幕ファイルを出力するツールを書き、ffmpegで合成して、日本語字幕のついたビデオをその場でみんなに見てもらう、というのは、かなりエキサイティングじゃないか?

というのを思いついたので、パーサーを書き始める。パーサーは簡単だった。しかもここに非同期イテレーターを使っているので、.NET Core 3としてもふさわしい。

そして、Google Translator APIのメチャクチャ簡単で使いやすいこと!! しかも.NET向けのNuGetパッケージもあって、面倒くさそうなアカウントキーもどこからサインアップしてGetすればいいのか、わかりやすいドキュメント… なんてことだ!15分ぐらいで実際に翻訳出来た!! ええ、なんでGoogle API使っているのかって、自問してましたよ…

しかしここで字幕特有の難しさにブチ当たる。それは、1センテンス(日本語なら読点まで)が必ず1画面に収まるわけではなく、分割されたセンテンスに推測不能(一定時間ではない)時間タグがついて分割されている、ということなのです。

“My name is (ここで分割されている) Kouji Matsui.”

“私の名前は松井幸治です。”

果たしてどこで分割する? System.Stringで英文の割合で発生する分割点と同じ割合の文字で切るか? 例えば、46/54なら、「は」の後ろで丁度切れるかもしれない。しかし、こんなにうまくフィットするわけがなくて、普通は非常に読みにくい位置で分割されてしまう…

では、形態素解析するか?形態素解析すれば、助詞とか句読点位置を検出してそこで区切って、自然な感じの字幕に出来るのでは? えーやったこと無いぞ、今からは無理では… .NET実装はあるのか?と考えて検索したら、あった: .NET形態素解析エンジンNMeCab (SourceForge.jp)

NMeCabで検索すると、割と紹介記事があるのだが… ライブラリ更新されてない… net20とか古すぎる。そうか、じゃあNMeCabをベースにチャッチャと移植して、これはこれで別の機会にネタとしてまとめよう、と思いついて移植を始める。

1日前

NMeCabの.NET Coreへの移植が出来た… アンセーフブロック使ってたり古いリソース(Settingsとか)して、ちょっとハマる点があったけど、そのあたりを軽く手を入れ、net40, net45, netstandard1.3, netstandard2.0, netstandard2.1対応のNuGetパッケージ、しかも標準のIPADIC同梱でありながら、自分の辞書を使う事も出来るように構成可能にして公開しました。

まあ、気がつくともう翌日が本番なのに何やってるんだろう、みたいな…

で、日本語字幕を付けるコードの続きを書き始めたのですが、日本語字幕センテンスを割合で再分割してSRTのフレームにする部分が思いの外面倒で、疲れ切った頭では正しいコードをひねり出せず… もう寝る時間が無くなって本番の運営に支障をきたしそうなので、無念だけどここでピボットする事に。

MeCab.DotNetのことと日本語字幕ツールの話は、また後日のネタにする事にして、当日はHanselmanの英語字幕付きビデオを流してコメントを入れつつ、その後にHanselmanのデモと同じことを私が再現して、多分言い足りない部分を補う感じにすれば、丁度50分に収まるだろうと考えました。

(今回のセッションではなく)不特定の人に英語技術ビデオを見た後に感想を聞くと、なんか腑に落ちない顔をしていることがあります。英語や英語の字幕のせいでピンときていないのかも知れないし、ビデオでは簡単にやって見せているけど、周到に準備してるからじゃないの?というような疑念があるんじゃないかと思っているので、同じことを目の前でやって見せることで払拭したかったのです。

(Hanselmanさんのデモ自体は、上記のような不安要素は殆どなくて、押さえるべきところが押さえられていて流石だなと思っています。)

ポイントはそんなに多くなくて、実際やって見せればシンプルな話だと思っていて、目の前で動いているプロセスの状態を見たりとか、今まさにWindows側でビルドしたコードが、WSLのubuntu上でそのまま動きましたよね(間違いなく)、みたいなことじゃないかなと。

そういうわけで、これで50分のセッションを構成することにしました。この内容の利点は、資料が一切不要なこと。自分の喋りと進行をその場で工夫して乗り切ればOKだ。この時点でもう23時なので、他の手段は取れそうにない…

妄想

ということで、今回のイベントの裏がどうなっていたのかが何となくわかっていただけたかと思います。半分は運営全体の話、半分は個人的な事情によるものですが、こうしてみるとやっぱり運営に割ける時間が足りない、運営に手間を取られすぎる、ということが大きかったと思います。

コミュニティへの貢献は色んな方法が考えられます。私が一番して欲しいと思っていることは、ぜひ、自分の時事ネタでも興味のあることや知見でもなんでも良いので、発表・シェアしてほしいです。元々そういう場なので。

そして、コミュニティに物理的な(生々しい話をするなら金銭的な)貢献をして頂ける場合は、運営の裏舞台、特に発表とは関係のない諸々の作業(これは私の場合ですが、それぞれのコミュニティの事情を読み取って)の負担を軽減してもらえて、結果的に時間を削減出来ると、本当に助かります。時間を確保出来るようになれば、発表する内容をより吟味したり、面白く楽しい内容に出来るようになり、より良いコミュニティイベントを開催・継続できることにつながると思います。

(あえて挙げるとすれば、名古屋の場合はやっぱり会場ですかね。リッチな環境が整備された会場が提供されると、悩みの半分は解決します。場合によっては7割ぐらい楽になる感じ。今や募集とかはconnpassのようなサービスで出来るし、あとは当日そこに行って発表する事を考えるだけなので。いつも会場提供して頂ける団体や組織には、本当に感謝しています :)

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との違いとしては、ポータビリティとフットプリントかなと思っています。まだまだ完成には遠い感じですが、これからも、ただの概念実験にならないようにしていきたいと思います。