Microsoft platform developers. C#, LINQ, F#, IL, metaprogramming, Windows Phone or like. Microsoft MVP for VS and DevTech. CSM, CSPO. http://amzn.to/1SeuUwD
(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…
Web APIにアクセスするには、今はHttpClientを使いますが、彼らは使い慣れているWebClientを使います:
// Web APIからデータを取得する
public string FetchData()
{
WebClient wc = new WebClient();
string body = wc.DownloadString("https://example.com/api/data");
return body;
}
// Web APIからデータを取得する
public string FetchData()
{
HttpClient hc = new HttpClient();
// エラー: 型が合わない
string body = hc.GetStringAsync("https://example.com/api/data");
return body;
}
// Web APIからデータを取得する
public string FetchData()
{
HttpClient hc = new HttpClient();
// Taskは良く分からないけど、Resultプロパティを見れば結果が得られそうだ
// 型もばっちり一致して、コンパイルが通ったぞ!
string body = hc.GetStringAsync("https://example.com/api/data").Result;
return body;
}
// 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;
}
// Web APIからデータを取得する
public async Task<string> FetchDataAsync()
{
var hc = new HttpClient();
return await hc.GetStringAsync("https://example.com/api/data");
}
Windows Formsは、画面上(Visual Studio)で、ユーザーインターフェイスのボタンやテキストボックスなどを視覚的に配置することが出来て、その結果(例えば座標とか色とか)が、C#のソースコードとして自動的に生成・変更され、ビルド時に一緒にコンパイルされることでGUIアプリケーションが完成する、というものでした。ここで:
UWPとその前身のWindows Store Appは、Windows 8でアプリケーションのモダン化を目指したものです。スマートフォンアプリケーションが、検証されて配布されることで一定の安心感を得られるのと同じように、MicrosoftがUWPで作られたアプリケーションを検証して公開することで、ユーザーが簡単に安全に利用できるようにしようとしました。
なお、Xamarin FormsはUWPを実行環境とすることも出来ます。これは、ビルドした結果がUWPアプリケーションとして、ストアアプリ展開可能という意味です。もちろん、同じXAML環境であっても、UWPとXamarin FormsのXAMLには互換性は無く、この方法で実装する場合はあくまでXamarin Formsとして開発する事になります。Epoxyはもちろん、Xamarin Forms on UWPにも移植しています。
欠点もあります。それは、AvaloniaのXAML編集時に、インテリセンスが全く効かない事です(Visual Studio 2019上)。XAMLのインテリセンスは、この記事で紹介するすべての環境で満足行く状態ではない(C#インテリセンスとの比較)と考えていますが、Avaloniaはそもそも全くインテリセンスを使えないため、注意が必要です。恐らくauthorは問題を認識しているでしょうから、将来改善される見込みはあると思います。
Uno platformは最近出てきたXAML環境です。当然マルチプラットフォーム対応を謳っているのですが、wasm (Web Assembly、つまりはChromeなどのウェブブラウザ上で動作する)に対応しているなど、かなり尖っています。そのためか、少し前に大きな話題となりました(その頃は私事で忙しく、試している余裕が無かった)。
以上はUno on UWP以外の環境の話であり、Uno on UWPの場合は、大半の実装はUWPの実装をそのまま使う。つまりUnoがUWP準拠なのは、実際にUWPを使う場合はほぼそのまま、それ以外の環境では半自動生成された実装が流し込まれて、UWPの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アプリが作れるぞ?」と思いますが、その後が続きません。
#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(...))
(もう少しいうと、static A recordの管理と、DHCPによる自動レコード更新をやりたいというのが背景にあります。それがなければルーターの代理応答だけでいい。ところで最初の1年ぐらいは、今は亡きcallweaver入れて、SIPサーバーとしてフレッツ光回線の電話側を収容していました。パフォーマンス的にはギリギリ、ちょっとプツプツノイズが入る感じでした)
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!");
}
}
ビデオの内容を読み込み、字幕がアレだったことから日本語字幕化する事を考え始めた。でも、SRTファイルをそのままGoogle Translatorにかけるとフォーマットが壊れてしまうのと、センテンスがつながっていないのが原因でダメな翻訳になってしまうこともあって考えあぐねていた(Azure Video Indexerで日本語訳出せばいいじゃん、と思うかもしれません。機能的にはありますが、まあ、察してください…)
Bait and switchテクニックは、実装を入れ替えることができると言う点で、インターフェイス分離とよく似ています。しかし、インターフェイス分離の場合は、設計者が最初から適切に分離設計をする必要があります。Bait and switchは、CLRがアセンブリを同一視する方法に乗じて、コードに全く手を入れずに、プラットフォーム依存処理を入れ替えることを可能にします。
以下にこの2つの分離手法を比較した表を示します:
手法
メリット
デメリット
インターフェイス分離
OOPの一般的な手法。誰でもリスクなしで使用でき、コンパイラが型チェックでエラーを検出してくれる
初めから使用する前提で設計する必要がある
Bait and switch
後から依存性の分離を行うことができる
型チェックはコンパイル時に行われないため、問題があると実行時エラーを起こす
かつてのPortable Class Libraryは、Bait and switch手法で互換性を実現しました。ここまでの解説で、PCLが何十というプロファイルを持っていたことを考えると、互換性を維持することがいかに困難であったかが想像出来ます。
Bait and switchを適用する場合は、メタデータの不一致を何らかの方法で開発時に検証できるようにするか、あるいはテンプレートコードの自動生成や合成ができるようにするなどの手法を用意し、開発の困難さを軽減することが重要です。
前章で述べた、インターフェイス分離設計やBait and switchテクニックをP/Invokeと組み合わせると、.NETでマルチプラットフォームの一貫したライブラリ設計を行うことができます。つまり、前章の依存コードの部分をP/Invokeを使って実装すれば、そのプラットフォーム固有のAPIがネイティブコードであったとしても、部品として使える共通のアセンブリを構築できる、と言うことです。
基本的に、今まで述べてきたテクニックが利用できる部分では、そのまま利用できます。例えば、System.IO.FileStreamクラスは、ファイルへのアクセスにWindowsとLinuxで異なるAPIを使用するはずです。しかし、インターフェイスで分離されているわけではないため、Bait and switchを利用して切り分け、P/InvokeでそれぞれのAPIを呼び出している、と想像出来ます。
前節では、プラットフォームごとの実装を切り分けるのに、System.Private.CoreLibアセンブリを基準として、Bait and switchで実行アセンブリが差し替わっていることを確認しました。例としてDebugクラスの実装を追いましたが、そこでは普通にP/Invoke機能を使ってネイティブライブラリを呼び出していました:
マルチプラットフォームの環境によっては、表面的に同じ操作を行うメソッドであっても、内部のネイティブコード連携では、QCall,FCall,HCall(場合によってはP/Invoke)を使い分けるということがあるかもしれません。そのような場合でも、Bait and switchテクニックで、System.Private.CoreLibアセンブリが差し替わることで、柔軟に対応できることがわかります。
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.
(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:
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:
Prepare for use the C# language.
Only LED blinking.
Handles button input and completes Blinker like code.
Improves by C# language style.
Comparison binaries between C language and C# language.
Enable C# language, C++ (VC++) language and NUnit3 test adapter from extension manager on Visual Studio 2017.
Open the il2c.sln file and build entire solution. If you can’t success building, you can skip next step for the unit tests.
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.)
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:
.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.
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.
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.
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.
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.
C:\>azsphere device claim
Claiming device.
Successfully claimed device ID '***********' into tenant '***********' with ID '***********'.
Command completed successfully in 00:00:04.6818769.
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.
/// <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;
}
Visual Studio 2017にC#, C++(VC++), NUnit3 test adapter(拡張機能から)が入っている必要があります。普通はVC++入れないと思いますが、Azure Sphere SDKを入れてあるなら入っているでしょう。
ルートにあるil2c.slnを開いてビルドします。どうしてもビルドが通らない場合は、3のテストは諦めることも出来ます(.NET Framework 4.0以降・.NET Core 1.0以降・.NET Standard 1.0以降の全てのバージョンに依存しているため、環境によってはビルドできないかも知れません。Visual Studio Installerを起動して、とにかく全てのコンポーネントをインストールしてみるのはありかも知れません。私は常に全部入りです)。
Test Explorerからテストを実行して全部パスすることを確認しておきます(結構時間がかかります。CPUコア数多いと速いです。i7-4790Kで5分ぐらい。あと、1GBぐらいのディスクスペースが必要で、初回実行時に一度だけMinGWのgcc toolchainをダウンロードするため、遅い回線だと辛いかもしれません)。masterブランチには安定的に動作するコードが入っているはずですが、テストを実行しておけば、少なくとも私が見た結果と同じものが得られるはずです。
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);
}
}
}
}
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);
}