How to Call a C# .NET 10 Library from a Native MFC C++ Application
Migrating legacy MFC applications from .NET Framework to modern .NET (such as .NET 10) is a common architectural challenge. In the .NET Framework era, you could simply compile your MFC executable with the /clr flag and directly reference C# assemblies. However, if you try this with modern .NET, you will encounter the compiler error: C++/CLI projects targeting .NET Core must be dynamic libraries.
Why Did This Change in Modern .NET?
In .NET Framework, the Windows OS loader was tightly integrated with the CLR (via mscoree.dll), allowing native EXEs compiled with /clr to automatically bootstrap and load the runtime. Modern .NET (.NET Core and later) is cross-platform and uses a completely redesigned, modular hosting model. Because of this, C++/CLI is only supported for dynamic link libraries (DLLs) when targeting modern .NET. Your main executable must remain purely native, and it is responsible for hosting or loading the managed runtime.
Top 3 Ways to Call .NET 10 from Native MFC
Here are the three most reliable and supported architectural patterns to bridge your native MFC application with a C# .NET 10 library.
Method 1: Native AOT (The Modern, High-Performance Way)
Introduced in recent .NET versions and highly optimized in .NET 10, Native AOT (Ahead-Of-Time) compiles your C# code directly into a native machine-code DLL. It does not require a heavy .NET runtime installation on the target machine, and it exposes standard C-style exports that your MFC app can call using standard LoadLibrary and GetProcAddress.
1. Configure the C# Project
Add the <PublishAot>true</PublishAot> property to your C# .csproj file:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<PublishAot>true</PublishAot>
</PropertyGroup>
</Project>
2. Export C# Methods
Use the [UnmanagedCallersOnly] attribute to expose your C# methods to native C++:
using System.Runtime.InteropServices;
public class MyLibrary
{
[UnmanagedCallersOnly(EntryPoint = "AddNumbers")]
public static int AddNumbers(int a, int b)
{
return a + b;
}
}
3. Call from MFC C++
In your native MFC application, load the compiled DLL and invoke the function dynamically:
typedef int (*AddNumbersFunc)(int, int);
void CallCSharpDll()
{
HMODULE hModule = LoadLibrary(L"MyLibrary.dll");
if (hModule)
{
AddNumbersFunc AddNumbers = (AddNumbersFunc)GetProcAddress(hModule, "AddNumbers");
if (AddNumbers)
{
int result = AddNumbers(5, 10);
// Use the result in your MFC app
}
FreeLibrary(hModule);
}
}
Method 2: C++/CLI Bridge DLL (Best for Complex OOP Models)
If you need to pass complex objects, classes, or handle garbage-collected references, a C++/CLI Bridge DLL is the most practical solution. Your MFC EXE remains native, but it links against a C++/CLI wrapper DLL compiled with /clr:netcore. This wrapper acts as a bridge to the C# .NET 10 DLL.
The architecture looks like this:
MFC.exe (Native) -> Bridge.dll (C++/CLI with /clr:netcore) -> MyLibrary.dll (C# .NET 10)
How to set it up:
- MFC EXE: Keep compile settings purely native (no
/clr). - Bridge DLL: Create a new C++ Dynamic Link Library project. In configuration properties, set Common Language Runtime Support to
.NET Core Runtime Support (/clr:netcore)and target.NET 10.0. - C# Library: A standard Class Library targeting
net10.0. Reference this project from your Bridge DLL.
In the Bridge DLL, expose native C++ classes or standard C-style functions to the MFC EXE, and implement them using managed C++/CLI code to call the C# assembly.
Method 3: Native Hosting APIs (nethost / hostfxr)
If you want to avoid C++/CLI entirely but still want to run standard (non-AOT) .NET 10 assemblies, you can use the official .NET Hosting APIs. This approach manually initializes the .NET runtime inside your native process.
Microsoft provides the nethost library and coreclr_delegates.h headers to locate the runtime, boot it, and fetch function pointers to managed methods. While highly flexible and robust, it requires a fair amount of boilerplate code to initialize and resolve dependencies.
Summary: Which Approach Should You Choose?
- Choose Native AOT if you want the highest performance, minimal runtime dependencies, and simple C-style function exports. This is the modern standard for interoperability.
- Choose C++/CLI Bridge DLL if you are migrating a massive legacy codebase that relies heavily on C++/CLI classes, or if you need to pass complex objects between managed and unmanaged boundaries.
- Choose Hosting APIs if you need to dynamically load different .NET runtimes or plug-ins at runtime without relying on C++/CLI compiler support.