Ferrum is a domain-agnostic native-compute framework for .NET MAUI. It provides the scaffolding that lets any C/C++ static library be called from a MAUI app with zero-copy buffer passing, NativeAOT-compatible P/Invoke, and a code-generation tool that automates the binding layer.
Ferrum does not ship a native core of its own. It provides the plumbing; the consuming application provides the native library.
┌───────────────────────────────────────────────────────────────────────┐
│ Consumer MAUI App │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Application code (C#) │ │
│ │ Uses NativeBuffer<T> for zero-copy buffer passing │ │
│ │ Calls generated [LibraryImport] bindings │ │
│ └────────────────────────────┬────────────────────────────────────┘ │
│ │ P/Invoke (source-generated, AOT-safe) │
│ ┌────────────────────────────▼────────────────────────────────────┐ │
│ │ Ferrum.Framework (src/Framework/) │ │
│ │ • NativeBuffer<T> — pinned Span<T>/Memory<T> bridge │ │
│ │ • InteropConstants — platform-aware library name │ │
│ └────────────────────────────┬────────────────────────────────────┘ │
│ │ [LibraryImport] / __Internal (iOS) │
│ ┌────────────────────────────▼────────────────────────────────────┐ │
│ │ Consumer Native Library (any C/C++17 static lib) │ │
│ │ Built by CMake templates in native/ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────────┐
│ Offline tools │
│ ferrum-codegen (tools/codegen/) │
│ Reads: consumer's .h file │
│ Writes: [LibraryImport] .cs bindings + blittable struct defs │
└────────────────────────────────────────────────────────────────────┘
| File / directory | Purpose |
|---|---|
native/CMakeLists.txt |
Top-level project; includes helpers, optionally builds the test fixtures |
native/cmake/ferrum_helpers.cmake |
ferrum_add_static_library() macro that sets common flags |
native/cmake/ios.toolchain.cmake |
Minimal iOS cross-compilation toolchain (no external deps) |
native/test_stub/ |
Trivial ferrum_add(int, int) C library used to validate the pipeline |
native/dsp_fixture/ |
ferrum_dsp_scale(float*, int32_t, float) + ferrum_dsp_stats(const float*, int32_t, FerrumDspStats*) — validates the float-buffer and struct-out-param patterns |
native/scripts/build_ios.sh |
Builds all three iOS slices and packages them as an XCFramework |
native/scripts/build_android.sh |
Builds .so for all four Android ABIs using the NDK toolchain |
iOS builds use CMake's built-in iOS support (CMAKE_SYSTEM_NAME=iOS) with
our thin toolchain wrapper. Three separate CMake configurations are produced:
OS64— arm64 device librarySIMULATORARM64— arm64 simulator library (Apple Silicon Macs)SIMULATOR64— x86_64 simulator library (Intel Macs)
The two simulator slices are fat-lipo'd together, then both the device slice
and the fat simulator slice are assembled into an XCFramework using
xcodebuild -create-xcframework.
The resulting .xcframework is added as a <NativeReference> in the MAUI
.csproj file.
Android builds use the NDK-bundled android.toolchain.cmake. We target
android-24 (API 24, Android 7.0) across four ABIs:
arm64-v8a, armeabi-v7a, x86_64, x86.
The resulting .so files are placed under artifacts/android/jniLibs/<abi>/
and referenced as <AndroidNativeLibrary> in the MAUI .csproj.
A zero-copy, pinned buffer abstraction. The generic constraint where T : unmanaged
is enforced at compile time to guarantee blittability. No reflection is involved.
// Typical usage
float[] audio = new float[4096];
using var buf = new NativeBuffer<float>(audio.AsMemory());
MyNativeLib.ProcessAudio(buf.Pointer, buf.Length);
// Pin is released on Dispose — GC can move the array againKey design points:
MemoryHandle.Pin()keeps the underlying memory at a fixed address.Pointerreturnsvoid*;TypedPointerreturnsT*for typed access.Dispose()releases the pin; using the buffer after disposal throwsObjectDisposedException.- Allocation is explicit (
new NativeBuffer<T>(length)) or wrap-existing (new NativeBuffer<T>(Memory<T>)).
Provides the FerrumLibrary constant used as the first argument to [LibraryImport]:
- iOS / Mac Catalyst:
"__Internal"(NativeAOT static linkage — the symbol is resolved inside the current process, no dynamic loading) - Android + all other platforms:
"ferrum_test_stub"(resolved via the platform's native library loader at runtime)
ferrum-codegen parses a C header file and emits ready-to-use
[LibraryImport] bindings and [StructLayout(LayoutKind.Sequential)] struct
definitions.
-
Fail loudly, never silently. Any construct that cannot be safely bound (function pointers,
char*, platform-ambiguouslong, array declarators) causes an immediate error with a human-readable message pointing to the source line. No incorrect bindings are ever emitted. -
NativeAOT-first. The generated code uses
[LibraryImport](source-generated at build time) instead of[DllImport](which uses reflection at runtime). NoMarshalAsattributes are emitted. -
Simple, dependency-free parser. The parser is a strict tokenizer — not a full C frontend. It handles the subset of C headers needed for P/Invoke: function prototypes and POD structs. Anything it does not recognise is rejected immediately (see constraint 1). A full libclang integration was considered and rejected: it would add ~40–50 MB of native binary dependencies to the dotnet tool package without materially improving correctness within the blittable-only constraint. See open-questions.md item 5 for the full rationale.
| C type | C# type | Notes |
|---|---|---|
void |
void |
|
bool / _Bool |
byte |
1 = true, 0 = false |
char |
byte |
Raw byte, not a string |
int8_t / signed char |
sbyte |
|
uint8_t / unsigned char |
byte |
|
int16_t / short |
short |
|
uint16_t / unsigned short |
ushort |
|
int32_t / int |
int |
|
uint32_t / unsigned int |
uint |
|
int64_t / long long |
long |
|
uint64_t / unsigned long long |
ulong |
|
float |
float |
|
double |
double |
|
void* |
void* |
Requires unsafe context |
T* (blittable) |
T* |
Pointer to any supported primitive |
size_t |
nuint |
|
ptrdiff_t / intptr_t |
nint |
|
long |
ERROR | Platform-dependent size — use int32_t/int64_t |
char* |
ERROR | Non-blittable — use byte* with explicit encoding |
| Function pointers | ERROR | Bind manually with [UnmanagedFunctionPointer] |
Every component is designed to work with iOS NativeAOT:
[LibraryImport]is resolved at build time by the Roslyn source generator; no reflection is needed at runtime.NativeBuffer<T>uses only value-type generics andSystem.Runtime.InteropServicesAPIs that are NativeAOT-safe.- The codegen tool generates code that contains no
MarshalAs, noType.GetType(), no dynamic invocation. - The
Ferrum.Framework.csprojsets<UseInterpreter>false</UseInterpreter>on iOS to catch any inadvertent use of the interpreter during CI.
Ferrum is a framework, not an application. Contributions must be domain-agnostic. If a change would only be useful to one specific native library or application, it belongs in the consuming project, not here.
Indicators of scope creep (flag these in code review):
- A function or type name references a specific application domain (audio, video, ML model name, etc.)
- The codegen tool learns about specific library ABIs by name
- Any native code beyond the trivial test fixtures is added to
native/
The in-tree test fixtures (test_stub/, dsp_fixture/) are explicitly exempt
from the scope rule — they exist solely to prove the framework plumbing works
with different parameter patterns, not to implement any application logic.