Managed C++ wrappers for legacy C++ libraries

2020-02-08 09:48发布

问题:

We're looking at writing a .Net-callable wrapper for some legacy C++ libraries using managed C++.

It all looks pretty easy. Is there anything we need to watch out for?

回答1:

I found it generally quite easy to wrap some existing C++ libraries in C++/CLI and encountered comparatively few pitfalls. The ones I can remember were:

  • Mixing unmanaged C++ code and C++/CLI code in the same executable/DLL is a really bad idea. I've run into problems with competing memory managers at shutdown time that way (basically the .NET runtime and the regular C++ runtime where stepping on each other's toes when it came to cleaning up memory on shutdown, resulting in non-deterministic behaviour as to which one freed what). Instead of linking the static legacy C++ library into the C++/CLI library, I created a DLL containing the legacy C++ and linked that against the C++/CLI DLL, which solved the problem once and for all.
  • If your code is using enums, you have to wrap those in the appropriate C++/CLI enum classes, otherwise other .NET languages won't be able to see and use them.
  • C++/CLI objects can only hold pointers to unmanaged C++ objects. Unfortunately in certain cases, this means you will have to create thin wrapper layers to handle certain objects. My "favourite" was that I had to either wrap boost::shared_ptrs that way (and thus add another layer of indirection) or put them into shared_ptrs with null deleters after crossing the .NET/native boundary. Neither is very nice when you have to deal with APIs that use this sort of construct a lot. RAII doesn't cross this boundary, so be warned, you will have to invest some time into tweaking it to match the .NET way.
  • C++/CLI doesn't do multiple inheritance, so if your legacy library is making use of that you might have to model this using interfaces etc.
  • The internal marshalling code seems to be able to handle most POD conversions, but you'll have find/borrow code that converts std::strings etc. This code is around, a few minutes on Google should bring it up (sorry, don't have any links handy here at the moment).


回答2:

It is pretty straight-forward and works well. It is a lot easier than PInvoke.

The big thing you need to watch out for is not having any unmanaged members in your managed headers, including private members, method signatures, etc. It is okay to have private members that are pointers to managed types though, just use forward declarations for your classes.

Also, watch out for object lifetime. It is easy to introduce memory leaks since many .NET programmers are not used to cleaning up after themselves. Make sure any wrapper classes that you create are disposable if they contain pointers and make sure you dispose of them in your managed code. The syntax for IDisposable in managed C++ is also weird, but it is in the docs.

Also, remember that you incur a slight hit every time you cross the managed/unmanaged boundary, so try to plan your interface accordingly. If anything gets called repeatedly in loops, it is probably better to move that loop across the boundary so you only cross once. Don't worry too much about this though unless you are talking millions of calls.

This article goes the other way, but it has some useful points.

Use Our ManWrap Library to Get the Best of .NET in Native C++ Code

See also

Managed Code in Visual Studio 2005 and
Deleting Managed Objects, Wrapping a Library, and More



回答3:

Just some issues which we encountered:

  • Memory/Resource lifetime management (GC/IDisposable vs. Destructors). I think this is well-known, and Rob's post has a couple of things about it, so I won't go into detail here...
  • String encoding/decoding. If your native code is an UNICODE build, you won't have to worry about this a lot, but if not, be think careful about encodings when converting between native strings and .Net Strings.
  • C++ doesn't honor [Conditional("Debug")], meaning that Debug.Assert, Debug.Trace etc. will also be called in release builds. Use the traditional C++ macros instead.
  • 64-Bit support: .Net by default generates 32 or 64-Bit code depending on the platform, but your native code will probably be 32-Bit only...


回答4:

I'll just add to what everyone has already said,

pin_ptr wch = PtrToStringChars(string); (where string is a System::String)

will become your friend.

You can't directly include a non-managed class into a managed class but you can put a pointer to the unmanaged class and new it in your constructor and delete it in your destructor.

I haven't had the problems Timo Geusch mentioned with mixing C++ and C++/CLI code in one DLL. My DLL uses both extensively without problems.

C++/CLI is not difficult (if you know C++ and .NET) and works great.



回答5:

As said by others: 98% of the time it just works, its debuggable, and fast.

What I encountered beyond mentiond so far:

  • Don't compile all your legacy c++ code as managed. There were some articles suggesting that would be useful, but it is usually just slower.
  • Don't forget to catch unmanaged exceptions and rethrow them as managed ones.
  • If you use MFC, note that you you can't use the .lib runtime, so you'll be deploying MFC runtimes as well.
  • OpenMP (a threading library) won't run in C++/CLI.
  • we had some build issues in VS2005 when we made the C++/CLI dll's dependent on C# dlls from our own code.

It even worked so well that I started on writing C++/CLI code to run unit tests on C++ code. NUnit/Resharper will happily find and run the unit test in a C++/CLI DLL, that can directly call into your native code at ANY LEVEL, even test your template container classes.