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).

Compiling the 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);

The C# P/Invoke Layer

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.

The string-marshalling minefield

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:

If something must cross the boundary as a binary blob, marshal it as byte[] with an explicit length parameter. Predictable layout, no encoding surprises.

The other path: C++/WinRT

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.

WinUI 3 and MVVM

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
    });
}

Try it yourself

Why this matters

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.

What's next

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.

Quick check

1. What does P/Invoke let C# code do?
  1. Call other .NET DLLs only
  2. Call unmanaged native DLLs via [DllImport]
  3. Make web API calls
Reveal Answer

Answer: B. P/Invoke is the bridge between managed (.NET) and unmanaged (C/C++) code on Windows.

2. When passing a string from C# to a C function expecting const char*, you should…
  1. Set CharSet = CharSet.Ansi (or .Utf8) on the [DllImport] — the runtime marshals automatically
  2. Do nothing — strings just work
  3. Use CharSet = CharSet.None
Reveal Answer

Answer: A. C# strings are UTF-16 internally. The runtime converts to the encoding the C side expects, given the CharSet attribute.

3. For a string the C++ side will fill with output, what's the safe pattern?
  1. Pass a C# 'string' (which is immutable)
  2. Pass a StringBuilder with pre-set capacity; the C++ side fills it
  3. Use a global variable
Reveal Answer

Answer: B. Strings are immutable in C#. StringBuilder is the canonical way to receive output from native code.