How We Added Raspberry Pi Support to Our .NET SDK

Ian Lavery
Picovoice
Published in
3 min readMay 17, 2021

When I first heard about .NET on RPi, I was skeptical. I had always thought of .NET as an enterprise framework, so it just seemed unlikely that it could find a home on a single-board machine like the Pi. However, collared shirts tucked into pleated khakis was the .NET of the past; modern developers and hobbyists alike have discovered that dotnet (as it’s now stylized) is a highly flexible framework, capable of some impressive cross-platform applications.

With this in mind, we decided to add Raspberry Pi as a support to the dotnet SDK for our Porcupine Wake Word Engine. Previously, we were targeting .NET Standard 2.0, which allowed us to support .NET Framework and .NET Core applications on Windows, macOS, and Linux. With Linux as a supported OS, you’d think supporting the Pi would be trivial. However, the core of our software is in fixed-point ANSI C, which is compiled for particular architectures. This allows us to optimize the engine for performance and power consumption specifically for each chipset.

How do we go about loading the right library for the right chip? This turned out to be surprisingly challenging.

A Smorgasbord of Libs

A standard way of integrating native code into a dotnet project is to compile it into a dynamic library and use good ol’ DllImportAttribute to access the exported functions from the C library:

The catch: the logic for finding the correct lib file is non-negotiable, and the library name must be a constant. For desktop OS applications, this works just fine: DllImport looks in the application directory for a libpv_porcupine.dll on Windows, a libpv_porcupine.dylib on macOS, and a libpv_porcupine.so on Linux. That’s contrasted with Raspberry Pi, where we support six different Arm chip architectures, all compiled into separate .so files. DllImport will no longer be able to find the correct lib because the library is no longer constant.

The Main Course, NativeLibrary

In .NET Core 3.0, NativeLibrary was introduced to solve the exact problem I encountered. It allows a developer to take control of the logic applied to loading a native library. NativeLibrary can get complex (you can entirely ditch DllImport), but thankfully it exposes a simple function SetDllImportResolver that will allow us to change the import logic, and we can still use the DllImportAttribute to declare our C interface. This suits us: now we can target .NET Standard 2.0 with vanilla DllImport, and target .NET Core 3.0+ with NativeLibrary!

First, we change the Porcupine.csproj file to target multiple frameworks:

Then we change our import logic to this:

Now that we have integrated our own import logic, we need to find out what Arm CPU chip we’re running on at runtime

Bag of Chips

Machines running Linux have the /proc/cpuinfo file that we can read to determine the chip on which we’re running. For each core, /proc/cpuinfo provides a set of properties that look like this:

processor : 0
model name : ARMv7 Processor rev 4 (v7l)
BogoMIPS : 38.40
Features : half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae evtstrm crc32
CPU implementer : 0x41
CPU architecture: 7
CPU variant : 0x0
CPU part : 0xd03
CPU revision : 4

We’re interested in the CPU part property: this hex ID maps to different chips in the Arm CPU family. We’ll extract this from the file and map it to the chip name.

We’re now loading the correct libraries for each RPi variant, and can make calls into the C library to run optimized native code!

The Pudding

Here’s a video of us using our Porcupine .NET microphone demo on an RPi 3 with an Arm Cortex-a53 chip:

Try the demo for yourself here. For more information regarding Picovoice’s SDKs and products, visit the website, docs or explore the Github repositories. Sign up for the Picovoice Console to obtain a Picovoice AccessKey and create custom wake words.

--

--