How to set the location of .NET for C++/CLI project

In our application, we decided to keep a copy of the .NET 6 runtime with our application. The main reason for that is that we don’t need to install it and deal with all related issues. Here is a short list of issues we have to deal with:

  • Antivirus or security applications can block that installation. In many cases, they block it silently and as a result, our application will fail to start.
  • There could be something strange on that computer and installation can fail.
  • We need to test with cases when this version is already installed.

Each of these issues can take a lot of time to investigate and find a solution. It all takes valuable time from our support team and in general it is not our problem, but we must help to find a solution because, in the eyes of a customer, it is our problem. As a result, we decided to bundle it up with our application, and no installation is required.

Our main application is a native one and it is not a problem to set the location of the .NET runtime, but we also have a single C++/CLI library that fails without .NET runtime installed. Basically what it does is just provide a native exported function for native code and load .NET assembly and call function from it. It is called from a native library that is loaded from a native application.

All that is needed to specify the .NET runtime location is to set the DOTNET_ROOT environment variable. Also, you need to ensure that file host\fxr\<version of .NET>\hostfxr.dll exists at that location. You also need to place the file Ijwhost.dll from the same version to the same directory as an application that eventually loads it. Make sure that all of them are the same version as your .NET runtime or you may have very strange issues.

Normally, I would get the path to the current module and add a relative path of .NET to build the full path to .NET runtime. So I created a new file and added the following code:

#pragma unmanaged

BOOL APIENTRY DllMain(HMODULE hModule, DWORD  ul_reason_for_call, LPVOID lpReserved)
{
    switch (ul_reason_for_call)
    {
        case DLL_PROCESS_ATTACH:
        {
            wchar_t dllPath[MAX_PATH];

            // Get the full path of the current Dll
            if (GetModuleFileNameW(hModule, dllPath, MAX_PATH) == 0) break;

Then using C++ runtime functions that find the last slash or backslash and append a constant path to the .NET runtime. Easy stuff. Except that it does not work. It took me quite some time because Visual Studio did not want to debug this code at all.

It turns out that using any C++ runtime library will automatically initialize the .NET runtime. Every C++ runtime function has a jmp instruction to the .NET initialization code. As a result, I had to do it the old-fashioned way, like our ancestors did, and to write all these functions by myself. After that everything works as a charm and the application starts normally.

I hope it helps someone.