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.
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:
- 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.
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:
- 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:
<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.