← Back to blog
C# / C++2025-08-02 · 1 min

C# native interop: 10 years of lessons with Box2DCS and Crunch

I have spent ten years making C# call into C and C++. Three of those bindings are open source: Box2DCS, ManagedCrunch, and ManagedXZLZMA. The hard part was never exposing the first function. It was keeping the boundary reliable as the code, runtimes, and platforms changed.

These are the rules I kept coming back to.


Pick the method first

Start with the native library, not the wrapper. The shape of its API usually makes the choice clear.

  • P/Invoke over a flat C API is simple and portable. If you control the library, this is the easy path. Keep the boundary in extern "C" and keep classes out of it.
  • C++/CLI wraps complex C++ fast, but it locks you to Windows.
  • DNNE and NativeAOT are the modern path on .NET 7 and later.

Choose before you write the wrapper. Changing direction later can cost more than a rewrite.


Copy less, measure more

A binding usually slows down at one boundary: buffer copies. Move big data with Span<T>, Memory<T> and fixed pointers. Copy once, not twice.

Then measure. BenchmarkDotNet tells the truth, and your instinct does not.


Say who frees the memory

The bugs I remember came from lifetime mistakes, not missing methods.

Decide who owns each buffer, and write it down. I let the native side allocate, and C# disposes with IDisposable and SafeHandle. A clear rule stops the crashes.


Ship for the runtimes you support

Wrap the native library per RID: win-x64, linux-x64, osx-arm64. Test the packages in CI, not on your machine.

One tool that saved me was PdbRewriter. It let me read a PDB when an obfuscated build broke a stack trace.

The code is at github.com/jacano.