Introduction
Operating in a .NET environment, there are situations where leveraging the capabilities of external DLLs can be advantageous. Unfortunately, several online libraries, such as those involving mathematical or geometric operations, are often implemented in C++ code, making direct integration with .NET projects challenging.
To overcome this challenge, the concept of a "wrapper" in C++/CLI comes into play. A wrapper acts as an interface between native code written in C++ and the .NET environment, facilitating the use of native libraries within Common Language Runtime (CLR)-based applications.
Throughout this article, we will closely examine the nature of wrappers in C++/CLI, exploring reasons why creating one might be necessary and providing guidance on how to develop a wrapper to integrate a DLL written in native C++ code.
What is a Wrapper in C++/CLI?
A C++/CLI wrapper is a component that provides a managed interface in .NET around a native component written in C++. This allows managed and unmanaged code to interact seamlessly. C++/CLI is an extension of the C++ language that adds specific features for .NET, making it ideal for creating wrappers.
The wrapper acts as a bridge between managed code (C# or other .NET languages) and native code (C++). The main advantage is that managed code can call functions in the native DLL through the wrapper, enabling the integration of C++ libraries into .NET projects.
Why Create a Wrapper?
There can be several reasons to create a wrapper in C++/CLI. Some of them include:
Integration of .NET and C++: When you want to use a C++ DLL in a .NET project, a wrapper is necessary to facilitate communication between the two environments.
Hiding Complexity: The wrapper can conceal the complex implementation details of the native DLL, exposing only a simplified interface to managed code.
Memory Management: The wrapper can handle type conversion and memory management, simplifying the work for .NET programmers and preventing potential memory allocation and deallocation issues.
Direct Platform Access: Native C++ may have an advantage in low-level scenarios or when direct platform access is required without the overhead of the runtime.
How to Create a C++/CLI Wrapper for a Native DLL
Here is a detailed guide on how to develop a C++/CLI wrapper for a native C++ DLL. We will use as an example the native C++ DLL created in the context of the "Counter" project outlined in the article
DLLs and how to create them with C++
Through this practical example, we will learn to create a wrapper for the DLL, making it accessible from a C# application. This process can then be successfully applied to any other DLL written in native C++ code that you want to integrate into your .NET code.
Step 1: Create a C++/CLI Project
Open the Visual Studio program and create a new project by selecting the "File" -> "New Project..." menu and choosing "CLR Class Library (.NET)" as the project type.
Step 2: Add Reference to the Native DLL
To integrate any DLL into a C++ project, you need to add the path to the DLL header files to the compiler's search paths and import the .lib file for the linker. To perform this configuration, right-click on the project folder in the Solution Explorer and select Properties from the context menu. Next, in the C++ section, select General and add the path of the folder containing the "counter.h" file to the Additional Include Directories definition.
Now, select General under Linker and add the path of the counter.lib file to the Additional Library Directories definition.
Select the Input branch under Linker and add Counter.lib to the Additional Dependencies, then press OK to confirm.
Step 3: Define the Wrapper Interface
Creating a wrapper means creating a new class where you define function declarations that will act as an interface to the native DLL code. To add a new class to the project, right-click on the project folder and select "Add" -> "Class.".
In the window that appears, specify the class name, the header file name (.h), and the implementation file name (.cpp). Make sure the Managed checkbox is selected to indicate to the environment that the class being added is a managed C++ code class.
Step 4: Implement the Wrapper
First, define the wrapper functions in the header file (.h). Generally, each wrapper will contain the same functions as the corresponding native class. In our case, the file will be as follows.
#pragma once
#include "../Counter/Counter.h"
using namespace System;
namespace CounterNet {
public ref class CounterWrapper
{
private:
Counter* m_nativeCounterObject;
public:
CounterWrapper();
CounterWrapper(double initialValue);
~CounterWrapper();
double Add();
double Add(double value);
double Sub();
double Sub(double value);
property Double Value
{
double get();
void set(double value);
}
};
}
We can observe a peculiar aspect: the presence of a private pointer of the type of the native class.
Counter* m_nativeCounterObject;
Typically, in the wrapper's implementation file (.cpp), the functions will need to call the corresponding functions in the native DLL and possibly perform type conversions of arguments and return values between the managed and native versions.
#include "CounterWrapper.h"
CounterNet::CounterWrapper::CounterWrapper() {
m_nativeCounterObject = new Counter();
}
CounterNet::CounterWrapper::CounterWrapper(double initialValue) {
m_nativeCounterObject = new Counter(initialValue);
}
CounterNet::CounterWrapper::~CounterWrapper() {
delete m_nativeCounterObject;
m_nativeCounterObject = 0;
}
double CounterNet::CounterWrapper::Add() {
return m_nativeCounterObject->Add();
}
double CounterNet::CounterWrapper::Add(double value) {
return m_nativeCounterObject->Add(value);
}
double CounterNet::CounterWrapper::Sub() {
return m_nativeCounterObject->Sub();
}
double CounterNet::CounterWrapper::Sub(double value) {
return m_nativeCounterObject->Sub(value);
}
double CounterNet::CounterWrapper::Value::get() {
return m_nativeCounterObject->GetValue();
}
void CounterNet::CounterWrapper::Value::set(double value) {
m_nativeCounterObject->SetValue(value);
}
We observe that in the constructor and destructor of the managed class, the instance of the native class object is respectively created and destroyed.
CounterNet::CounterWrapper::CounterWrapper(Double initialValue) {
// The initialValue argument is passed to the Counter constructor to initialize it
m_nativeCounterObject = new Counter(initialValue);
}
CounterNet::CounterWrapper::~CounterWrapper() {
delete m_nativeCounterObject;
m_nativeCounterObject = 0;
}
The pointer to this object will serve as a connection to the native class, and we will use it to invoke its methods and, in general, interact with it.
In this case, a second constructor without arguments has been added.
CounterNet::CounterWrapper::CounterWrapper() {
// The counter constructor is called without arguments
m_nativeCounterObject = new Counter();
}
In fact, if we carefully observe the constructor of the native class, we see that the initialValue
argument has a default value of 0, which is used to initialize the counter to 0 when the constructor is called without arguments.
class Counter
{
public:
Counter(double initialValue = 0.0);
}
In managed C++ code, however, the use of default arguments is not allowed. One way to achieve the same result is to overload the constructor by creating two versions, one with and one without the initialValue
argument.
Now, let's compare the definition of the wrapper class's functions with those of the native code class.
class Counter
{
public:
double Add(double value = 1.0);
double Sub(double value = 1.0);
double GetValue();
void SetValue(double value);
}
We can also note here that the functions Add()
and Sub()
, have the value
argument with a default value of 1. For this reason, the Add()
and Sub()
functions have also been overloaded.
public ref class CounterWrapper
{
…
double Add();
double Add(double value);
double Sub();
double Sub(double value);
…
}
Furthermore, it's observed that in the wrapper class, the functions GetValue()
and SetValue()
have disappeared. Thanks to the use of managed code, it's possible to capitalize on an intrinsic feature of .NET classes, namely properties. Therefore, these two functions have been replaced by the Value
property.
public ref class CounterWrapper
{
…
property double Value
{
double get();
void set(double value);
}
}
The get()
and set()
functions in the implementation of the Value
property in the managed class respectively invoke the GetValue()
and SetValue()
functions of the native class.
Step 5: Compile and Use the Wrapper
At this point, we can compile everything and use the generated wrapper in a .NET project, such as a C# application.
Let's provide a small example. Create a console application in C# by selecting "File" -> "New Project..." and choosing "Console App" as the project type.
Press the Next button and fill in the required information. Once the project is created, configure the console application for the Windows target. Right-click on the project in the Solution Explorer and choose Properties from the context menu. Then, select General in the Application section and set the value to "Windows" in the Target OS section.
Now, add a reference to the just-compiled wrapper class by right-clicking on Dependencies under the project in the Solution Explorer and selecting Add reference from the menu.
We can now write the code for our test application.
CounterWrapper counter = new();
Console.WriteLine("Initial value!");
Console.WriteLine(counter.Value);
Console.WriteLine("We add 5 to the counter value");
counter.Add(5);
Console.WriteLine(counter.Value);
Console.WriteLine("We subtract 2 to the counter value");
counter.Sub(2);
Console.WriteLine(counter.Value);
Console.WriteLine($"The final value is {counter.Value}");
Console.WriteLine("We set the value to 10");
counter.Value = 10;
Console.WriteLine($"Now the value is {counter.Value}");
Run the program, and the result will be as follows.
Conclusion
Creating a C++/CLI wrapper for a native C++ DLL is an effective way to integrate existing code into .NET projects. The wrapper simplifies communication between managed and unmanaged code, providing a more user-friendly interface and handling aspects such as type conversion and memory management. By following the steps outlined above, it is possible to easily create a wrapper to enable the use of C++ DLLs in .NET applications.