Raspberry Pi is a small single-board computer that has gained popularity in recent years. Its low cost, small size, and versatility make it an attractive option for developers who want to experiment with IoT, robotics, and other embedded systems projects. However, until recently, using Raspberry Pi with .NET wasn't easy. Fortunately, with the release of .NET Core, it's now possible to write .NET code that runs on Raspberry Pi. In this article, we'll explore how we added Raspberry Pi support to our .NET SDK.
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:
private const string LIBRARY = "libpv_porcupine";
[DllImport(LIBRARY, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
private static extern PorcupineStatus pv_porcupine_init(
string modelPath,
int numKeywords,
string[] keywordPaths,
float[] sensitivities,
out IntPtr handle);
[DllImport(LIBRARY, CallingConvention=CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
private static extern void pv_porcupine_delete(IntPtr handle);
[DllImport(LIBRARY, CallingConvention=CallingConvention.Cdecl, CharSet=CharSet.Ansi)]
private static extern PorcupineStatus pv_porcupine_process(
IntPtr handle,
short[] pcm,
outint keywordIndex);
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:
#if NETCOREAPP3_0_OR_GREATER
static Porcupine()
{
NativeLibrary.SetDllImportResolver(typeof(Porcupine).Assembly, ImportResolver);
}
private static IntPtr ImportResolver(
string libraryName,
Assembly assembly,
DllImportSearchPath? searchPath)
{
IntPtr libHandle=IntPtr.Zero;
NativeLibrary.TryLoad(PvLibraryPath(libraryName), out libHandle);
return libHandle;
}
private static string PvLibraryPath(string libName)
{
string assemblyDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
if (RuntimeInformation.ProcessArchitecture==Architecture.X64)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return Path.Combine(assemblyDir, $"lib/windows/amd64/
{libName}.dll");
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
return Path.Combine(assemblyDir, $"lib/mac/x86_64/
{libName}.dylib");
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
return Path.Combine(assemblyDir, $"lib/linux/x86_64/
{libName}.so");
}
else if((RuntimeInformation.ProcessArchitecture ==
Architecture.Arm||
RuntimeInformation.ProcessArchitecture==Architecture.Arm64) &&
RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
string armChip=GetArmChip();
return Path.Combine(assemblyDir, $"lib/raspberry-pi/{armChip}/{libName}.so");
}
throw new PlatformNotSupportedException(
$"{RuntimeInformation.OSDescription}
({RuntimeInformation.OSArchitecture}) "+
"is not currently supported."+
"Visit https://picovoice.ai/docs/api/porcupine-dotnet "+
"to see a list of supported platforms."
);
}
#endif
private const string LIBRARY = "libpv_porcupine";
[DllImport(LIBRARY, CallingConvention = CallingConvention.Cdecl,
CharSet=CharSet.Ansi)]
private static extern PorcupineStatus pv_porcupine_init(
string modelPath,
int numKeywords,
string[] keywordPaths,
float[] sensitivities,
out IntPtr handle);
[DllImport(LIBRARY, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
private static externint pv_sample_rate();
[DllImport(LIBRARY, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
private static externvoid pv_porcupine_delete(IntPtr handle);
[DllImport(LIBRARY, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
private static extern PorcupineStatus pv_porcupine_process(
IntPtr handle,
short[] pcm,
out int keywordIndex);
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.
private static string GetArmChip()
{
string archInfo = RuntimeInformation.ProcessArchitecture ==
Architecture.Arm64?
"-aarch64":"";
string cpuInfo = File.ReadAllText("/proc/cpuinfo");
string[] cpuPartList = cpuInfo.Split('\n').Where(x =>
x.Contains("CPU part")).ToArray();
if (cpuPartList.Length == 0)
throw new PlatformNotSupportedException($"Unsupported
CPU.\n{cpuInfo}");
string cpuPart = cpuPartList[0].Split(' ').Last().ToLower();
switch (cpuPart)
{
case"0xb76": return"arm11"+archInfo;
case"0xc07": return"cortex-a7"+archInfo;
case"0xd03": return"cortex-a53"+archInfo;
case"0xd07": return"cortex-a57"+archInfo;
case"0xd08": return"cortex-a72"+archInfo;
default:
Console.WriteLine(
$"WARNING: Please be advised that this device (CPU part
= {cpuPart}) "+
"is not officially supported by Picovoice. "+
"Falling back to the armv6-based (Raspberry Pi Zero)
library. "+
"This is not tested nor optimal.\n For the model, use
Raspberry Pi\'s models");
return"arm11"+archInfo;
}
}
We’re now loading the correct libraries for each RPi variant, and can make calls into the C library to run optimized native code!
Comments