Memory leaks in C++/CLI

How to fix memory leaks in C++/CLI?

Contents

Managed and unmanaged leaks in C++/CLI.

C++/CLI is a version of the C++ language that was made to allow developers to write programs in C++ for .NET. It was created in the early days of .NET, and the reason was simple: a huge code base had already been written in C++, and Microsoft just couldn’t exclude C++ developers from the .NET world. C#, created as the primary language for the new platform, was absolutely new!

The compiler of C++/CLI produces mixed assemblies that contain both managed and unmanaged (native) code. A code in C++/CLI can allocate managed objects as well as memory in an unmanaged heap (like usual C++). That is why it is worth checking an application written in C++/CLI for unmanaged memory leaks and for managed ones also.

In this guide, we will create a simple console application in C++/CLI with intentional leaks. We will show how to locate leaks with the help of Deleaker, a special memory debugger.

Unmanaged leaks in C++/CLI.

Start Visual Studio, choose CLR Console App (.NET Framework), name the project as CppCliLeaks, and let Visual Studio create initial files. Then open CppCliLeaks.cpp and modify it as shown below:

#include "pch.h"
#include <string>
#include <iostream>

using namespace System;

int main(array<System::String ^> ^args)
{
    auto unmanagedString = new std::wstring(L"Hello, world!");
    std::wcout << *unmanagedString << std::endl << L"Press any key to exit";
    std::getchar();
    return 0;
}

In this code, a new instance of std::wstring is allocated; its content is displayed to the user. Build the project and run the application to ensure it is working as expected:

The application is working as expected

Great! Now it is time to catch the leak. Close Visual Studio and download the memory leak checker from the official website: https://www.deleaker.com/download.html.

The setup detects installed IDEs including Visual Studio as well as Qt Creator and RAD Studio.

After the installation completed, open the project in Visual Studio and enable Deleaker (click to ExtensionsDeleakerEnable Deleaker):

Enable Deleaker in Visual Studio

Being enabled, Deleaker becomes a part of the standard Visual Studio debugger. With the help of hooks, Deleaker gathers information about allocated memory and other resources such as GDI objects and handles.

Start debugging, press ENTER to exit the application and allow Deleaker to take the final snapshot. Final snapshots always contain resources that were not explicitly disposed; usually, we call such allocations “leaks”.

Switch to the Deleaker window (click to click to ExtensionsDeleakerDeleaker Window) and review the snapshot:

Deleaker shows a snapshot

The snapshot contains the instance of std::wstring and points to the correct point at the source file (CppCliLeaks.cpp, line 9). You can easily navigate to the source code directly from the snapshot: just right-click any stack entry or double-click an allocation entry:

Navigate to source code

Conclusion

C++/CLI, that was born as a compromise in the situation when new C# was absolutely new for developers, while the C++ codebase was giant, is still alive. Checking for memory leaks is still a viable task for C++/CLI developers.

Deleaker is a memory profiler that catches both managed and unmanaged leaks in C++/CLI. Also, it helps finding other kinds of leaks: GDI and USER objects, and handles.

Deleaker can work as a standalone profiler, but also it integrates with your favorite IDE; at any moment you can take a snapshot to review live allocations. For each allocation, a comprehensive information is provided: size, type, hit count, and call stack. With call stack, you can find where a particular allocation has been made. For further reviews, it’s possible to save a snapshot to a file.

Deleaker is available for downloading from our website: https://www.deleaker.com/download.html

Appendix: What is the difference between managed and unmanaged allocations?

In C++/CLI, .NET objects are allocated by gcnew, while unmanaged objects are allocated by the usual operator new. Look at how the instances of std::wstring and System.String are instantiated:

#include "pch.h"
#include <string>

using namespace System;

int main(array<System::String ^> ^args)
{
    auto unmanagedString = new std::wstring();
    String^ managedString = gcnew String("");

    return 0;
}

Although both calls look similar, there is a big difference between them. As you probably know, when the .NET runtime is going to execute a managed method for the first time, the method is compiled into unmanaged code that can be executed on a target CPU. The method main is a .NET method, so gcnew is converted to the opcode newobj, but the operator new can’t be converted to newobj because std::wstring is not a managed object. Let’s call ildasm to check the generated code:

.method assembly static int32  main(string[] args) cil managed
{
  // Code size       29 (0x1d)
  .maxstack  2
  .locals ([0] valuetype std.'basic_string<wchar_t,std::char_traits<wchar_t>,std::allocator<wchar_t> >'* V_0,
           [1] int32 V_1,
           [2] valuetype std.'basic_string<wchar_t,std::char_traits<wchar_t>,std::allocator<wchar_t> >'* unmanagedString)
  IL_0000:  ldc.i4.0
  IL_0001:  stloc.1
  IL_0002:  ldc.i4.s   40
  IL_0004:  conv.i8
  IL_0005:  call       void* modopt([mscorlib]System.Runtime.CompilerServices.CallConvCdecl) new(uint64)
  IL_000a:  stloc.0
  IL_000b:  ldloc.0
  IL_000c:  brfalse.s  IL_0016

  IL_000e:  ldloc.0
  IL_000f:  call       valuetype std.'basic_string<wchar_t,std::char_traits<wchar_t>,std::allocator<wchar_t> >'* modopt([mscorlib]System.Runtime.CompilerServices.CallConvCdecl) 'std.basic_string<wchar_t,std::char_traits<wchar_t>,std::allocator<wchar_t> >.{ctor}'(valuetype std.'basic_string<wchar_t,std::char_traits<wchar_t>,std::allocator<wchar_t> >'* modopt([mscorlib]System.Runtime.CompilerServices.IsConst) modopt([mscorlib]System.Runtime.CompilerServices.IsConst))
  IL_0014:  br.s       IL_0018

  IL_0016:  ldc.i4.0
  IL_0017:  conv.i8
  IL_0018:  stloc.2
  IL_0019:  ldc.i4.0
  IL_001a:  stloc.1
  IL_001b:  ldloc.1
  IL_001c:  ret
} // end of global method main

Indeed, the code is a bit complicated. First, it allocates memory for the instance of std::wstring using new:

IL_0005:  call       void* modopt([mscorlib]System.Runtime.CompilerServices.CallConvCdecl) new(uint64)

After that, it calls the constructor to initialize the instance of std::wstring:

IL_000f:  call       valuetype std.'basic_string<wchar_t,std::char_traits<wchar_t>,std::allocator<wchar_t> >'* modopt([mscorlib]System.Runtime.CompilerServices.CallConvCdecl) 'std.basic_string<wchar_t,std::char_traits<wchar_t>,std::allocator<wchar_t> >.{ctor}'(valuetype std.'basic_string<wchar_t,std::char_traits<wchar_t>,std::allocator<wchar_t> >'* modopt([mscorlib]System.Runtime.CompilerServices.IsConst) modopt([mscorlib]System.Runtime.CompilerServices.IsConst))

It looks really tricky compared with newobj but this is the only way to deal with unmanaged objects in a managed code. Actually, the same happens in an unmanaged code when it is instantiated a C++ object: firstly, a memory is allocated, and then the constructor is called.