As people might know I frequent Reddit, and StackOverflow, but mostly Reddit. I do this obviously for investing advice, but secondly to share programming thoughts and ideas. I think giving back to community is an important part of life, and of the things I have to offer, programming is my best.
There are a great deal of resources out there that outline how to call native libraries from C#. From P/Invoke, to tutorials on interoperating using C++/CLI. I’d hazard a guess this is because C# is of the top 8 (59.7%) of our beloved languages according to StackOverflows annual developer survey in 2020. Much to my dismay, C++ falls somewhere in the basement, with a measly 43.4% of developers who actually enjoy using the language. So if you’re a C# developer, and you’re interested in calling some black-box C++ library written by your ancestors (or our modern day C++ superstars!) you’ve got plenty of resources. However, if you’re a C++ dev, and you’re looking to add a bit of .NET to your life. You might have a hard time finding resources on how to go about it. Here’s my attempt to fill that
void.
As a disclaimer, I’ll need you to know that this should be a last resort. You should research whether there are other options, like COM or a different native option. When we talk about C++, we think zero overhead abstraction, we think performance, we think light weight. What we’re doing isn’t any of that. When you use this technique, you’re pulling in the CLR into your application. So this is a sledgehammer solution, use it as such.
Calling C# libraries from C++ is actually easier than you’d think. C++/CLI is mostly used to go the reverse direction, and expose your native libraries into your managed. However, you can use it to expose managed code into C++. To be completely honest, I don’t know how this all works under the hood. I’d love if someone could share!
You can find the supplemental code for this here. To keep my wordiness to a minimum. I’ll just describe the setup in this post.
First create a C++/CLI project in Visual Studio. Then add your native class and export it.
// native_to_managed.h #pragma once #include <string> #include <memory> #ifdef DLL_EXPORT #define DLL_API _declspec(dllexport) #else #define DLL_API _declspec(dllimport) #endif class DLL_API native_to_managed { public: native_to_managed(); ~native_to_managed(); void write_to_console(const std::string& message); private: class impl; std::unique_ptr<impl> impl_; };
You’ll notice here the use of the familiar pimpl pattern, or in other words a “compiler firewall”. This is necessary, because this
.h will be included by our purely native project, and will need to have hidden the fact that it holds handles into the CLR. You’ll notice the minimal include dependencies. This is the purpose of the firewall. This interface will be purely C++. If you need to pass complex objects which are also managed, you’ll need to do the same for every object you want to export. You then implement the native_to_managed as per the
pimpl pattern, and forward the calls into the impl.
// native_to_managed.cpp #include "native_to_managed.h" #include "native_to_managed.impl.h" native_to_managed::native_to_managed() :impl_(std::make_unique<impl>()) { } // You need this in the compilation unit so it knows about the impl. native_to_managed::~native_to_managed() = default; void native_to_managed::write_to_console(const std::string &message) { impl_->write_to_console(message); }
You’ll notice that we’ve pulled the definition of the destructor into the .cpp file. You’ll need to do this, because inside this cpp is where the compiler gets an understanding of what the actual “impl” is, via the include of
native_to_managed.impl.h. Otherwise, because we’re using
std::unique_ptr we’ll get an error saying something about it not knowing how to delete. I’ve done this by doing
=default which will use the compiler default generated destructor. I read somewhere that this is optimal over
~native_to_managed(){}, in what the compiler actually generates. Since then, I always opt for the
=default option when I have default dtors. But most people don’t know you can do this in the .cpp, not just in the .h.
Inside the impl is where the magic happens. (do people still say that?)
//native_to_managed.impl.h #pragma once #include "native_to_managed.h" #include <gcroot.h> #include <msclr/marshal_cppstd.h> using namespace dotnet_interop_net_lib; class native_to_managed::impl { public: void write_to_console(const std::string &message) { managed_->WriteToConsole(msclr::interop::marshal_as<System::String ^>(message)); } private: gcroot<ManagedClass ^> managed_; };
Technically, you can split the declaration and implementation up into a .h/.cpp. I’m just lazy, and chose to do them both in the .h. Follow your heart, or your company coding standards, or what your boss says to do, not what she does.
The most important thing to notice is the inclusion of the
gcroot<T> a class that allows you to hang on to a Garbage Collector reference (handle) from your native class. You can see I’m holding on to a
ManagedClass^ the hat represents in C++/CLI that this is reference to a GC object. You can use the
gcroot to dereference and call into your managed class.
You’ll also notice that I’ve used
msclr::interop::marshal_as<System::String ^> to translate from our unmanaged memory (std::string) to our managed memory (System.String), again note the hat. Depending on your situation, you’ll need to just understand your own marshalling requirements.
The last very important thing, is that your standard Visual Studio project referencing won’t work. If you were using C# and you referenced this project, the library would get copied to the project output. However, if you do the reverse, Visual Studio fails to setup the references. So you have to setup the
additional library dependencies to point to the lib.
Edit: To clarify, this will be in the linker input of your native project.
Given this short setup — you’ll be able to call into a C#/Managed DLL. Remember, this is a sledgehammer so use it accordingly.
As always, Happy Coding!
I suppose it is tempting, if the only tool you have is a hammer, to treat everything as if it were a nail. — Abraham Maslow