Interop in .NET nanoFramework

Have you ever faced the situation of needing to add support for a specific hardware? Or to perform some computing intensive task that would be more efficiently executed in C/C++ rather than with managed C# code?

This is possible with the support that .NET nanoFramework has to plug “code extensions”. It’s called Interop.

What exactly does this? Allows you to add C/C++ code (any code, really!) along with the correspondent C# API.
The C/C++ code of the Interop library is added to an nanoFramework image along with the rest of the nanoCLR.
As for the C# API: that one is compiled into a nice .NET nanoFramework library that you can reference in Visual Studio, just like you usually do.

The fact that this is treated as an extension of the core is intended and, in fact, very positive and convenient. A couple of reasons:

  • Doesn’t require any changes in the main core code (which can be broken or may prove difficult to merge with changes from the main repository).
  • Keeps your code completely isolated from the rest. Meaning that you can mange and change it as needed without breaking anyone’s stuff.

How cool is this? 🙂

For the purpose of this post we are going to create an Interop project that includes two features:

  • Hardware related: reads the serial number of the CPU (this will only work on ST parts).
  • Software only related: implementing a super complicated and secret algorithm to crunch a number.

Note: it’s presumed that you have properly setup your build environment and toolchain and are able to build a working nanoFramework image. If you don’t suggest that you take a look the documentation about it here and here.

Before we start coding there are a few aspects that you might want to consider before actually start the project.

Consider the naming of the namespace(s) and class(es) that you’ll be adding. Those should have meaningful names. You’ll see latter on that these names will be used by Visual Studio to generate code and other bits of the Interop project. If you start with something and keep changing it you might find yourself in trouble because your version control system will find diferences. Not to mention that other users of your Interop library (or even you) might start getting breaking changes in the API that you are providing them. (You don’t like when others do that to you, do you? So… be a pal and pay attention to this OK? 🙂 )

Creating the C# (managed) Library

Create a new .NET nanoFramework project in Visual Studio

This is the very first step. Open Visual Studio, File, New Project.
Navigate to C# nanoFramework folder and select a Class Library project type.
For this example we’ll call the project “NF.AwesomeLib”.

nanoframework-interop-sample-01

Go to the Project properties (click the project icon in the Solution explorer an go to the Properties Window) and navigate to the nanoFramework configuration properties view. Set the “Generate stub files” option to YES and the root name to NF.AwesomeLib.

nanoframework-interop-sample-02

Now rename the Class1.cs that Visual Studio adds by default to Utilities.cs. Make sure that the class name inside that file gets renamed too. Add a new class named Math.cs. On both make sure that the class is public.

Your project should now look like this.

nanoframework-interop-sample-03.png

Adding the API methods and the stubs

The next step will be adding the methods and/or properties that you want to expose on the C# managed API. These are the ones that will be called on a C# project referring your Interop library.
We’ll add an HardwareSerial property to Utilities class and call to the native method that supports the API at the native end. Like this.

using System.Runtime.CompilerServices;

namespace NF.AwesomeLib
{
    public class Utilities
    {
        private static byte[] _hardwareSerial;
        ///

        /// Gets the hardware unique serial ID (12 bytes)
        /// 

        public static byte[] HardwareSerial
        {
            get
            {
                if (_hardwareSerial == null)
                {
                    _hardwareSerial = new byte[12];
                    NativeGetHardwareSerial(_hardwareSerial);
                }
                return _hardwareSerial;
            }
        }

        #region Stubs

        [MethodImpl(MethodImplOptions.InternalCall)]
        private static extern void NativeGetHardwareSerial(byte[] data);

        #endregion stubs
    }

}

A few explanations on the above:

  • The property HardwareSerial
  • has a only a getter because we are only reading the serial from the processor. As that can’t be written, it doesn’t make sense providing a setter, right?
  • The serial number is being stored in a backing field to be more efficient. When it’s read the first time it will go and read it from the processor. On subsequent accesses that won’t be necessary.
  • Note the summary comment on the property. Visual Studio uses that to generate an XML file that makes the awesome IntelliSense show that documentation on the projects referencing the library.
  • The serial number of the processor is handled as an array of bytes with length of 12. This was taken from the device manual.
  • A stub method must exist to enable Visual Studio to create the placeholder for the C/C++ code. So you need to have one for each stub that is required.
  • The stub methods must be implemented as extern and be decorated with the MethodImplAttribute attribute. Otherwise Visual Studio won’t be able to do it’s magic.
  • You may want to find a working system for you regarding the stub naming and where you place them in the class. Maybe you want to group them in a region, or you prefer to keep them along the caller method. It will work on any of those ways, just a hint on keep things organized.

Moving on to the Math class. We’ll now add an API method called SuperComplicatedCalculation and the respective stub. It will look like this:

using System.Runtime.CompilerServices;

namespace NF.AwesomeLib
{
    public class Math
    {
        ///

        /// Crunches value through a super complicated and secret calculation algorithm .
        /// 

        /// Value to crunch.
        ///
        public double SuperComplicatedCalculation(double value)
        {
            return NativeSuperComplicatedCalculation(value);
        }

        #region Stubs

        [MethodImplAttribute(MethodImplOptions.InternalCall)]
        private static extern double NativeSuperComplicatedCalculation(double value);

        #endregion stubs
    }
}

And this is all what’s required on the managed side. Build the project and look at the project folder using VS Code for example. This is how it looks like after a successful build:

nanoframework-interop-sample-04.png

From the top, you can see in the bin folder (debug or release flavor) the .NET library that should be referenced in other projects. Please note that besides the .dll file there is the .xml file (the one that will allow IntelliSense to it’s thing), the .pdb file and another one with a .pe extension.
When distributing the Interop library make sure that you supply all four files. Failing to do so will make Visual Studio complain that the project can’t build. You can add all those in a ZIP or even better, as a Nuget package.

Working on the C/C++ (native) code

Moving to the Stubs folder we find a bunch of files and a .cmake file. All those are required when building the nanoCLR image that will add support for your Interop library.
Look at the file names: they follow the namespace and classes naming in the Visual Studio project.
Something very, very important: don’t even think on renaming or messing with the content of those files. If you do that you risk that the image build will fail or you can also end up with the Interop library not doing anything. This can be very frustrating and be very hard to debug. So, again, DO NOT mess around with those!

The only exception to that will be, of course, the ones that include the stubs for the C/C++ code that we need to add. Those are the .cpp files that end with the class name.
In our example those are: NF_AwesomeLib_NF_AwesomeLib_Math.cpp and
NF_AwesomeLib_NF_AwesomeLib_Utilities.cpp.

You’ve probably have noted that there are a couple of other files with a similar name but ending with _mshl. Those are to be left alone. Again DO NOT change them.

Let’s look at the stub file for the Utilities class. That’s the one that will read the processor serial number.

void Utilities::NativeGetHardwareSerial( CLR_RT_TypedArray_UINT8 param0, HRESULT &hr )
{
}

This an empty C++ function named after the class and the stub method that you’ve placed in the C# project.

Let’s take a moment to understand what we have here.

  • The return value of the C++ function matches the type of the C# stub method. Which is void in this case.
  • The first argument has a type that is mapping between the C# type and the equivalent C++ type. A array of bytes in this case.
  • The last argument is an HRESULT type who’s purpose is to report the result of the code execution. We’ll get back to this so don’t worry about it for now. Just understand what’s the purpose of it.

According to the programming manual STM32F4 devices have a 96 bits (12 bytes) unique serial number that is stored starting at address 0x1FFF7A10. For STM32F7 that address is 0x1FF0F420. In other STM32 series the ID may be located in a different address. Now that we know were it is stored we can add code to read it. I’ll start with the code first and then walk through it.

void Utilities::NativeGetHardwareSerial( CLR_RT_TypedArray_UINT8 param0, HRESULT &hr )
{
    if (param0.GetSize() < 12)
    {
		hr=CLR_E_BUFFER_TOO_SMALL;
		return;
	}

    memcpy((void*)param0.GetBuffer(), (const void*)0x1FFF7A10, 12);
}

The first if statement is a sanity check to be sure that there is enough room in the array to hold the serial number bytes. Why is this important?
Remember that here we are not in the C# world anymore where the CRL and Visual Studio take care of the hard stuff for us. In C++ things are very different! On this particular example if the caller wouldn’t have reserved the required 12 bytes in memory to hold the serial array, when writing onto there the 12 bytes from the serial could be overwriting something that is stored in the memory space ahead of the argument address. For types other than pointers such as bytes, integers and doubles this check is not required.

Still on the if statement you can see that, if there is not enough room we can’t continue. Before the code returns we are setting hr to CLR_E_BUFFER_TOO_SMALL (that’s the argument that holds the execution result, remember?). This is to signal that something went wrong and give some clue on what that might be. There is still more to say about this result argument, so we’ll get back to it.

In the next piece of code is were – finally – we are reading the serial from the device.
As the serial number is accessible in a memory address we can simply use a memcpy to copy it from its memory location to the argument.
A few comments about the argument type (CLR_RT_TypedArray_UINT8). It acts like a wrapper for the memory block that holds the array (or a pointer if you prefer). The class for that type provides a function – called GetBuffer() – that returns the actual pointer that allows direct access to it. We need that because we have to pass a pointer when calling memcpy. This may sound a bit complicated, I agree. If you have curiosity on the implementation details or want to know how it works I suggest that you delve into the nanoFramework repo code and take a look at all this.

And that’s it! When this function returns the CPU serial number will be in the argument pointer and will eventually pop up in the C# managed code in that argument with the same name.

For the Math class there won’t be any calls to hardware or any other fancy stuff, just a complicated and secret calculation to illustrate the use of Interop for simple code execution.
Visual Studio has already generated a nice stub for us to fill in with code. Here’s the original stub:

double Math::NativeSuperComplicatedCalculation( double param0, HRESULT &hr )
{
    double retVal = 0;
    return retVal;
}

Note that the stub function, again, matches the declaration of it’s C# managed counterpart and, again, has that hr argument to return the execution result.
Visual Studio was kind enough to add there the code for the return value so we can start coding on that. Actually that has to be exactly there otherwise this code wouldn’t even compile. 😉

Where is the super complicated and secret algorithm:

double Math::NativeSuperComplicatedCalculation( double param0, HRESULT &hr )
{
    double retVal = 0; 

    retVal = param0 + 1;

    return retVal;
}

And with this we complete the “low level” implementation of our Interop library.

Adding the Interop library to a nanoCLR image

The last step that is missing is actually adding the Interop source code files to the build of a nanoCLR image.

You can place the code files pretty much anywhere you want it… the repo has a folder named Interop that you can use for exactly this: holding the folders of the Interop assemblies that you have. Any changes inside that folder won’t be picked up by Git.
To make it simple we’ll follow that and we just copy what is in the Stubs folder into a new folder InteropAssemblies\NF_AwesomeLib\.

The next file to get our attention is FindINTEROP-NF.AwesomeLib.cmake. nanoFramework uses CMake to generate the build files. Skipping the technical details suffice that you know that as far as CMake is concerned the Interop assembly  is treated as a CMake module and, because of that, the file name to have it properly included in the build it has to be named FindINTEROP-NF.AwesomeLib.cmake and be placed inside the CMake\Modules folder.

Inside that file the only thing that requires your attention is the first statement where the location of the source code folder is declared.


(...)
# native code directory
set(BASE_PATH_FOR_THIS_MODULE "${BASE_PATH_FOR_CLASS_LIBRARIES_MODULES}/NF.AwesomeLib")
(...)

If you are placing it inside that Interop folder the required changes are:


(...)
# native code directory
set(BASE_PATH_FOR_THIS_MODULE "${PROJECT_SOURCE_DIR}/InteropAssemblies/NF.AwesomeLib")
(...)

And this is it! Now to the build.

If you are using the CMake Tools module to build inside VS Code you need to declare that you want this Interop assembly added to the build. Do so by opening the cmake-variants.json file and navigate to the settings for the image you want it added.
There you need to add the following CMake option (in case you don’t already have it there

"NF_INTEROP_ASSEMBLIES" : [ "NF.AwesomeLib" ],

A couple of notes about this:

  • The NF_INTEROP_ASSEMBLIES option expects a collection. This is because you can have as many Interop assemblies as you need to the nanoCLR image.
  • The name of the assembly must match exactly the class name. Dots included. If you screw up this you’ll notice it in the build.

In case you are calling CMake directly from the command prompt you have to add this option to the call like this

-DNF_INTEROP_ASSEMBLIES=["NF.AwesomeLib"]

It’s important to stress this: make sure you strictly follow the above.
Mistakes such as: failing to add the CMake find module file to the modules folder; having it named something else; having the sources files in a directory other that the one that was declared; will lead to errors or the library wont’ be included in the image. This will can lead very quickly to frustration. So, please, be very thorough with this part.

The following task is launching the image build. It’s assumed that you have properly setup your build/toolchain so go ahead and launch that build!

Fingers crossed that you wont’ got any errors… 😉

First check is on the CMake preparation output you should see the Interop library listed:

nanoframework-interop-sample-05

A successful CMake preparation stage (that include the Interop assembly as listed above) will end with:

nanoframework-interop-sample-06

After the build completes successfully, you should be seeing something similar to this:

nanoframework-interop-sample-09

Reaching this step is truly exciting, isn’t it?! 🙂
Now go and load the image on a real board!

The next check after loading a target with the nanoCLR image that includes the Interop library is seeing it listed in the Native Assemblies listing. After booting the target is listed in Visual Studio Device Explorer list and after you click on the Device Capabilities button you’ll see it in the output window like this:

nanoframework-interop-sample-07.png

Congratulations, you did it! 😀 Let’s go now and start using the Interop library.

Using an Interop library

This works just like any other .NET library that you use everyday. In Visual Studio open the Add reference dialog and search for the NF.AwesomeLib.dll file that was the output result of building the Interop Project. You’ll find it in the bin folder. As you are going through that note the companion XML file with the same name. With that file there you’ll see the documentation comments showing in IntelliSense as you code.

This is the code to test the Interop library. On the first part we read the CPU serial number and output it as an hexadecimal formatted string. On the second we call the method that crunches the input value.


namespace TestInteropMFConsoleApplication
{
    public class Program
    {
        public static void Main()
        {
            // testing cpu serial number
            string serialNumber = "";

            foreach (byte b in NF.AwesomeLib.Utilities.HardwareSerial)
            {
                serialNumber += b.ToString("X2");
            }

            Console.WriteLine("cpu serial number: " + serialNumber);

            // test complicated calculation
            NF.AwesomeLib.Math math = new NF.AwesomeLib.Math();
            double result = math.SuperComplicatedCalculation(11.12);

            Console.WriteLine("calculation result: " + result);
        }
    }
}

Here’s a screen shot of Visual Studio running the test app. Note the serial number and the calculation result in the Output window (in green). Also the DLL listed in the project references (in yellow).

nanoframework-interop-sample-10.png

Supported types in interop method calls

Except for strings, you’re free to use any of the standard types in the arguments of the Interop methods. It’s OK to use arrays of those too.

As for the return data, in case you need it, you are better using arguments passed by reference and update those in C/C++. Just know that arrays as returns types or by reference parameters are not supported.

Follows a table of the supported types and correspondence between platforms/languages.

CLR Type C/C++ type C/C++ Ref Type
(C# ref)
C/C++ Array Type
System.Byte uint8_t UINT8* CLR_RT_TypedArray_UINT8
System.UInt16 uint16_t UINT16* CLR_RT_TypedArray_UINT16
System.UInt32 uint32_t UINT32* CLR_RT_TypedArray_UINT32
System.UInt64 uint64_t UINT64* CLR_RT_TypedArray_UINT64
System.SByte int8_t Char* CLR_RT_TypedArray_INT8
System.Int16 int16_t INT16* CLR_RT_TypedArray_INT16
System.Int32 int32_t INT32* CLR_RT_TypedArray_INT32
System.Int64 int64_t INT64* CLR_RT_TypedArray_INT64
System.Single float float* CLR_RT_TypedArray_float
System.Double double double* CLR_RT_TypedArray_double

Final notes

To wrap this up I would like to point out some hints and warnings that can help you further when dealing with this Interop library stuff.

  • Not all CLR types are supported as arguments or return values for the Interop stubs in the C# project. If the project doesn’t build and shows you an enigmatic error message, that’s probably the reason.
  • Every time the Interop C# project is build the stub files are generated again. Because of this you may want to keep on a separate location the ones that you’ve been adding code to. Using a version control system and a proper diff tool will help you merge any changes that are added because of changes in the C# code. Those can be renames, adding new methods, classes, etc.
  • When Visual Studio builds the Interop C# project a fingerprint of the library is calculated and included in the native code. You can check this in the NF_AwesomeLib.cpp file (in the stub folder). Look for the Assembly name and an hexadecimal number right bellow. This is what .NET nanoFramework uses to check if the native counterpart for that particular assembly is available in the device before it deploys an application. And when I say that I mean it. If you change anything that might break the interface (such a method name or an argument) it will. On the “client” project Visual Studio will complain that the application can’t be deployed. Those changes include the project version in the C# Interop project too, so you can use this as you do with any project version number.
  • The hr (return parameter) is set to S_OK by default, so if nothing goes wrong in the code you don’t have to change it. When there are errors you can set it to an appropriate value that will surface in the C# as an exception. You may want to check the src\CLR\Include\nf_errors_exceptions.h file in the nanoFramework repo.
  • Feel free to mix managed code to your C# Interop project too. If you have a piece of C# code that helps you achieve the library goal, just add it there. As long as it builds anything is valid either before or after the calls to the C/C++ stubs. If it helps and makes sense to be in the library, just add it there. You can even get crazy and call as many C/C++ stubs as you want inside a C# method.

And that’s all! You can find all the code related with this blog post in nanoFramework samples repo.

With all this I expect I was able to guide you through this very cool (and handy!) feature of .NET nanoFramework. Enjoy it!

Note: this post is a reviewed version of the original one published back in 2016. That post was about the now defunct .NETMF.

Advertisements

One thought on “Interop in .NET nanoFramework

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s