interop-cpp-csharp-nanoframework

Interop in nanoFramework: Calling C++ from C#

  • 4 min

Native interoperability is the ability to call C++ firmware code from a C# application.

If C# is a luxury car with assisted driving, C++ is the combustion engine underneath. Normally you don’t need to open the hood, but if you want to tune the engine to win races, you have to get your hands greasy.

In nanoFramework, C# code runs on a virtual machine (CLR) written in C++. Interop is the mechanism that allows us to “jump” from the virtual machine to the bare metal.

Why would I want to do this?

The rule of thumb is to use interop only when there is a concrete need that the managed APIs don’t cover.

Some possible cases are:

  1. Intensive processing: DSP, audio, or image algorithms with measured performance requirements.
  2. Non-existent drivers: A manufacturer’s library is only available in C or C++.
  3. Hardware access: You need a native function that the current firmware doesn’t expose.

The [MethodImpl] attribute

From the C# side, the call is declared with a special tag. We tell the compiler: “Hey, don’t look for the code of this function here. Look for it inside the chip’s firmware.”

using System.Runtime.CompilerServices;

namespace MiLibreriaNativa
{
    public class CalculadoraRapida
    {
        // Declare the method as 'extern'
        // Use the InternalCall attribute
        [MethodImpl(MethodImplOptions.InternalCall)]
        public static extern int SumarNativo(int a, int b);
    }
}
Copied!

When you call CalculadoraRapida.SumarNativo(2, 3), the CLR will look for a registered C++ function with a specific name generated from the namespace and class.

The native side: C++ and CLR_RT

This is where things get technical. For C++ to understand C# data types (which are managed objects), we need to use some special macros from the nanoFramework runtime.

You don’t write int suma(int a, int b). You write a function that receives a “Stack Frame” (the memory stack of the virtual machine) and you have to extract the arguments from there.

A simplified example of what the C++ code looks like in the firmware:

// MiLibreriaNativa_CalculadoraRapida_mshl.cpp

HRESULT Library_MiLibreriaNativa_CalculadoraRapida::SumarNativo___STATIC__I4__I4__I4( CLR_RT_StackFrame& stack )
{
    NANOCLR_HEADER(); // Standard start macro

    // 1. Retrieve arguments from the stack
    // Argument 0 is the first (a), argument 1 is the second (b)
    INT32 param0 = stack.Arg0().NumericByRef().s4;
    INT32 param1 = stack.Arg1().NumericByRef().s4;

    // 2. Perform the native operation (This is where you gain speed)
    INT32 resultado = param0 + param1;

    // 3. Put the result in the stack's return value
    stack.SetResult_I4( resultado );

    NANOCLR_NOCLEANUP(); // Closing macro without special cleanup
}
Copied!

Workflow

The process is not trivial. These are the steps Core developers follow:

  1. Define in C#: You create your library project in Visual Studio and write the extern methods.
  2. Generate Stubs: You compile the library. You use a special tool (Metadata Processor) that reads your DLL and automatically generates the “skeleton” .h and .cpp files (Stubs) so you don’t have to write the macros by hand.
  3. Implement C++: You fill those .cpp files with your native logic.
  4. Integrate into Firmware: You copy those files into the source code of nf-interpreter (the nanoFramework GitHub repository).
  5. Compile Firmware: You use CMake or Docker to compile the entire operating system again, generating a nanoCLR.bin file.
  6. Flash: You install YOUR firmware on the ESP32.
  7. Execute: Now, when you deploy your C# app from Visual Studio, calling the function will work.

Check existing APIs first

Before diving into this firmware compilation mess, remember that nanoFramework already exposes many native capabilities through C# classes.

  • Need to control fast WS2812 LED strips? Use System.Device.Spi or the RMT Transmitter class.
  • Need to count pulses very fast? Use PulseCounter.

Most of the time, what you think needs C++ is already solved in an optimized official NuGet library.