C# and C++ Interop Demonstration
One interesting feature of C# is that you can directly use unmanaged dlls from the managed code. There are different ways to do this, such as: 1) using a unsafe block and write unmanaged code directly. It is tested that the performance of these unmanaged codes is somewhate better than managed code. 2) use Marshall and dllimport from C# and dllexport from C++; 3) use 3rd party libraries such as swig.exe. Here I am going to introduce using dllimport because I think this is the most native way to deal with interop requirements.
The C++ source files need to be specially wrapped to expose function interfaces to managed code: in the header file: (headers.hh), need to
- Wrap all the function signatures in
extern "C"
because we don’t want C++ compiler to mangle the function name. - append
__declspec(dllexport)
before function declearation.
a basic example, first creat an empty C++ dll project and add two files: headers.hh and source.cc. In header.hh file, add the following lines:
1 | #ifndef CPPLIB_HEADERS_HH |
And in source.cc file, add the following lines:
1 | #include "headers.hh" |
Here we are passing a simple int from C# to C++ and print out.
In a separate C# project, created in the same solution, add the following lines
1 | [DllImport("CppLib.dll", CallingConvention = CallingConvention.Cdecl)] |
Run the project, we should be able to see “Number: “5”” from console window.
Getting return value is also very straight-forward. we add another line or function declearation into the extern “C”:
1 | __declspec(dllexport) double return_simple_type(double num); |
Then implement the above function in source.cc
1 | double return_simple_type(double num) { |
In C#, do the following to catch the return value:
1 | [DllImport("CppLib.dll", CallingConvention = CallingConvention.Cdecl)] |
Run the above code, you should see “From RetriveSimple we get: “24.5”
The above examples are both quite easy to deal with, what’s more interesting is dealing with customized data structures and arries, here we need to use functions in the Marshall class. Suppose we want to calculate on a double[] array and return a double[], we can not just write double[] calculate(double[] input);
because in C++, we have no idea how large is the double array because what we are passing into C++ got converted to a double *.. Also C++ can only return a double *, and it up to us to figure out how large is the double[]. Therefore, we need to find a way to specify the size of the array.
The input arguments are simple, we only need to add another int size
to the argument list. To pass back the return size of the array, we need to define same struct in both C# and C++.
in C++:
1 | struct OneDimRetArray |
in C#
1 | struct OneDimRetArray |
Notice that all the pointer types in C++ got converted to System.IntPtr in C#.
Now let’s define some functions:
In C++:
1 | __declspec(dllexport) OneDimRetArray_t* make_1d_array(const double* input, int size); |
Notice since we use pointers and we dynamically allocated memory, we need another function to clean the memory;
1 | OneDimRetArray_t * make_1d_array(const double * input, int size) { |
in C#, we need to call the above function:
1 | [DllImport("CppLib.dll")] |
Run the whole project, you should see the following output.
You can find the source code on GitHub
Comment (09/01/2015): I think there is something miss about releasing memory when I first wrote it. Now i think the correct way
to do it is by using the follong Marshal functions: Marshal.DestroyStructure
, Marshal.FreeHGlobal
.
For example, in my previous example, I created a struct called OneDimRetArray
. To proper release the memory, I need the folling function:
1 | struct OneDimRetArray |