Whenever a function is called across a managed-unmanaged boundary, a so-called thunk automatically performs a managed-unmanaged transition. To understand both directions of managed-unmanaged transitions, it is important to realize the roles that the software developer, the build tools (compiler and linker), and the CLR play to create a thunk as follows:
• The developer's role: Due to the power of C++/CLI interoperability, the developer's task is often the easiest one: ensuring that the target function is declared. This is typically done by including a header file with the function declaration. For fine-tuning, it is also possible to write some extra code dedicated to controlling managed-unmanaged transitions.
• The build tool's role: At build time, the compiler and the linker embed metadata containing interoperability information into the generated assembly. To do this, they combine different pieces of information from the code the developer has provided, the headers that the developer included, and the target library.
• The CLR's role: The CLR reads the interoperability metadata from the assembly and uses it to generate the thunk when needed.
The metadata generated for calls from native code to managed code is fundamentally different than the metadata for calls from managed code to native code. There are also differences in the way thunks for both directions of interoperability are created. Thunks that enable native callers to call managed functions are created when an assembly is loaded, whereas thunks that can call native code from managed code are dynamically created on demand by the JIT compiler. For this reason, I'll discuss both directions of interoperability separately.
It is important to be aware of these managed-unmanaged transitions because calling a function without such a transition is faster. Avoiding unnecessary managed-unmanaged transitions is an important performance optimization strategy. C++/CLI interoperability gives you many more options for minimizing transitions than other languages. For example, you have a lot of options to refactor your code to effectively reduce the number of calls across interoperability boundaries. Sometimes it is sufficient to move a function implementation from a file that is compiled to native code to a file compiled with /clr to replace many transitions with just one.
However, there are other optimization options, too. To use these optimization options, it is necessary to understand what the compiler and the runtime do in different scenarios of calls across interoperability boundaries.
In order to avoid distracting side information and to concentrate only on important aspects like the generated metadata, I start with simple C functions, not C++ classes. Understanding how C functions are called across interoperability boundaries is helpful for understanding how member functions of C++ classes are called. For the same reasons, I will discuss interoperability via function pointers before I cover virtual function calls across interoperability boundaries.
Was this article helpful?