La interoperabilidad nativa es la capacidad de llamar código C++ del firmware desde una aplicación C#.
Si C# es un coche de lujo con conducción asistida, C++ es el motor de combustión que hay debajo. Normalmente no necesitas abrir el capó, pero si quieres tunear el motor para ganar carreras, tienes que mancharte las manos de grasa.
En nanoFramework, el código C# se ejecuta sobre una máquina virtual (CLR) escrita en C++. La Interop es el mecanismo que nos permite “saltar” de la máquina virtual al metal desnudo.
¿Por qué querría hacer esto?
La regla práctica es usar interop solo cuando exista una necesidad concreta que las APIs administradas no cubran.
Algunos casos posibles son:
- Procesamiento intensivo: algoritmos DSP, audio o imagen con requisitos medidos de rendimiento.
- Drivers inexistentes: una biblioteca del fabricante solo está disponible en C o C++.
- Acceso al hardware: necesitas una función nativa que el firmware actual no expone.
El atributo [MethodImpl]
Desde el lado de C#, la llamada se declara con una etiqueta especial. Le decimos al compilador: “Oye, no busques el código de esta función aquí. Búscalo dentro del firmware del chip”.
using System.Runtime.CompilerServices;
namespace MiLibreriaNativa
{
public class CalculadoraRapida
{
// Declaramos el método como 'extern'
// Usamos el atributo InternalCall
[MethodImpl(MethodImplOptions.InternalCall)]
public static extern int SumarNativo(int a, int b);
}
}Cuando llames a CalculadoraRapida.SumarNativo(2, 3), el CLR buscará una función C++ registrada con un nombre específico generado a partir del namespace y la clase.
El lado nativo: C++ y CLR_RT
Aquí es donde la cosa se pone técnica. Para que C++ entienda los tipos de datos de C# (que son objetos gestionados), necesitamos usar unas macros especiales del runtime de nanoFramework.
No escribes int suma(int a, int b). Escribes una función que recibe un “Stack Frame” (la pila de memoria de la máquina virtual) y tienes que extraer los argumentos de ahí.
Un ejemplo simplificado de cómo se ve el código C++ en el firmware:
// MiLibreriaNativa_CalculadoraRapida_mshl.cpp
HRESULT Library_MiLibreriaNativa_CalculadoraRapida::SumarNativo___STATIC__I4__I4__I4( CLR_RT_StackFrame& stack )
{
NANOCLR_HEADER(); // Macro de inicio estándar
// 1. Recuperar argumentos desde la pila (Stack)
// El argumento 0 es el primero (a), el 1 es el segundo (b)
INT32 param0 = stack.Arg0().NumericByRef().s4;
INT32 param1 = stack.Arg1().NumericByRef().s4;
// 2. Hacer la operación nativa (Aquí es donde ganas velocidad)
INT32 resultado = param0 + param1;
// 3. Poner el resultado en el valor de retorno de la pila
stack.SetResult_I4( resultado );
NANOCLR_NOCLEANUP(); // Macro de cierre sin limpieza especial
}Flujo de trabajo
El proceso no es trivial. Estos son los pasos que siguen los desarrolladores del Core:
- Definir en C#: Creas tu proyecto de librería en Visual Studio y escribes los métodos
extern. - Generar Stubs: Compilas la librería. Usas una herramienta especial (Metadata Processor) que lee tu DLL y genera automáticamente los archivos
.hy.cpp“esqueleto” (Stubs) para que no tengas que escribir las macros a mano. - Implementar C++: Rellenas esos archivos
.cppcon tu lógica nativa. - Integrar en Firmware: Copias esos archivos dentro del código fuente de
nf-interpreter(el repositorio de GitHub de nanoFramework). - Compilar Firmware: Usas CMake o Docker para compilar todo el sistema operativo de nuevo, generando un archivo
nanoCLR.bin. - Flashear: Instalas TU firmware en el ESP32.
- Ejecutar: Ahora, cuando desde Visual Studio despliegues tu app C#, al llamar a la función, funcionará.
Revisar primero las APIs existentes
Antes de meterte en este lío de compilar firmwares, recuerda que nanoFramework ya expone muchas capacidades nativas a través de clases C#.
- ¿Necesitas controlar tiras LED WS2812 rápidas? Usa
System.Device.Spio la claseTransmitterde RMT. - ¿Necesitas contar pulsos muy rápido? Usa
PulseCounter.
La mayoría de las veces, lo que crees que necesita C++ ya está resuelto en una librería NuGet oficial optimizada.