Week 46 · Phase 6 — The Bridge
P/Invoke and DLLs: How C# consumes your C++ engine to build modern Windows apps.
On Windows, the primary language for application development is C#. But C# runs in a managed environment (the .NET runtime), while our C++ engine runs directly on the metal. To bridge this gap, we use a technology called Platform Invocation Services, or P/Invoke. It allows C# code to call unmanaged functions exported from a Dynamic Link Library (DLL).
First, we must package our C++ engine as a DLL. We use the __declspec(dllexport) attribute on our bridge functions (which we defined in Week 44) to tell the Windows linker to make these functions visible to outside applications.
// In your C++ bridge header
#define EXPORT extern "C" __declspec(dllexport)
EXPORT void* create_engine();
EXPORT void destroy_engine(void* engine);
EXPORT const char* get_best_move(void* engine, const char* fen);
In our C# app, we declare these same functions using the [DllImport] attribute. This tells the .NET runtime to look for a specific file (engine.dll) and find the function with the matching name.
using System.Runtime.InteropServices;
public static class NativeEngine {
[DllImport("engine.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr create_engine();
[DllImport("engine.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern void destroy_engine(IntPtr engine);
[DllImport("engine.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr get_best_move(IntPtr engine, string fen);
}
DLLs are the universal building blocks of Windows. By exporting a C-style interface, your engine becomes a library that any Windows tool can use.
Most P/Invoke crashes are about strings. C# strings are UTF-16 + length. C strings are bytes + null terminator. The CharSet attribute and the parameter type tell the runtime how to convert — and getting it wrong gives you garbled text or an instant access violation.
Two patterns that always work:
string and add CharSet = CharSet.Ansi (or CharSet.Unicode if your C++ side uses wchar_t*). The runtime allocates and frees the conversion buffer for you.StringBuilder with a known capacity, and have the C++ side fill it. Returning a const char* from C++ works too, but only if the buffer outlives the call — never return a pointer to a local std::string.If something must cross the boundary as a binary blob, marshal it as byte[] with an explicit length parameter. Predictable layout, no encoding surprises.
If you'd rather skip C# and write the UI directly in C++, Windows offers C++/WinRT — modern C++ projection of the Windows Runtime APIs, including WinUI 3. The XAML stays the same; the codebehind is C++ instead of C#. You give up some C# productivity (no LINQ, less reflection, more verbose async) but you also lose the entire P/Invoke layer — there is no boundary. For a chess engine where the C++ already exists, this is genuinely tempting and the build is one project instead of two.
For most teams the C# + DLL split is still the practical default — XAML tools, hot reload, and the .NET ecosystem are hard to give up — but knowing C++/WinRT exists keeps you from feeling boxed in.
Finally, we build the interface using WinUI 3 and the Model-View-ViewModel (MVVM) pattern. The ViewModel handles the interaction with the native engine, while the View (defined in XAML) displays the game board and pieces.
// In your ViewModel
public void RequestMove(string currentFen) {
Task.Run(() => {
IntPtr movePtr = NativeEngine.get_best_move(_engineHandle, currentFen);
string move = Marshal.PtrToStringAnsi(movePtr);
// Update UI properties on the main thread
});
}
[DllImport] declarations to a helper class.This is how Visual Studio, Office, Photoshop, and every AAA game on Windows actually ship. A C++ core compiled to a DLL, a managed shell consuming it through P/Invoke or COM, and a XAML or WinForms UI layered on top. The pattern hasn't really changed since 2002 — the wire format between native and managed is the most stable contract in the Windows ecosystem. Anything you build on it will still link and load a decade from now.
We've built the engine, the bridge, and the native wrappers for both Mac and Windows. Next week, we bring it all together. We'll look at how to structure a cross-platform project so that one core engine powers all three of our games across multiple operating systems.
Week 47 is Linking the Apps.
Answer: B. P/Invoke is the bridge between managed (.NET) and unmanaged (C/C++) code on Windows.
Answer: A. C# strings are UTF-16 internally. The runtime converts to the encoding the C side expects, given the CharSet attribute.
Answer: B. Strings are immutable in C#. StringBuilder is the canonical way to receive output from native code.