Lots of libraries offer interfaces in C mainly to be universally callable from any programming language. This is absolutely fantastic! It lets any language with a FFI (Foreign Function Interface) to hook into and use a large number of C libraries. But the downsides? The code is verbose, memory needs to be manually managed and it’s really easy to forget the crucial bits.
C++, being extremely close to C, can easily call into a C library if needed.
And the vice-verse is also true if extern "C"
is used (instruct the compiler
to not mangle function names). So, should you even go through the trouble of
wrapping it in C++? If so, what’s a good way to do it? This post explores these
questions and offers an insight into the hell called cross-language
interoperability.
Motivation
RAII (Resource Acquisition Is Initialization) is one of the most common C++
idioms that exists. C libraries, on the other hand, use functions like resource_open()
,
resource_destroy()
. The function names are ugly (ahem) and the algorithm
gets lost within the endless memory management. Try reading the source code of
the Linux kernel, for example.
C++ greatly improves semantics with its destructors, copy constructors, operator
overloads, templates and as of C++11, move constructors, resulting in code that
is very easy to read, understand and not too hard to reason about. Moreover, the
introduction of features like lambdas makes C-style function pointers and
void*
look like code from the 90s.
This discussion begs the question: why wouldn’t you wrap C code to be called from C++ in a lightweight wrapper? Possible reasons: time/resource constraints, and possibility of introduction of bugs and new attack vectors.
Time/Resource Constraints
If the library exposes a few functions that are simple enough to be used well and idiomatically in modern C++, then there’s nothing more that needs to be done. C++ can call directly into C without any problem so you’re good to go.
However, as soon as the library hits 2 dozen or so functions and is written in
an OOP style (think CURL), wrapping it in C++ becomes a necessity. If a typical
program using your library is an endless mess of resource_init
and resource_free
,
again, you are better off wrapping it in C++ than manually having to investigate
Valgrind’s complaints. The 2-4 days you spend wrapping a C library using modern
C++ is going to save hours of headache each and every day for months to come.
Possibility of Introduction of Bugs
Bugs will always exist in programs and abstractions are bound to introduce even more of them. If you’re trying to expose an interface very different from what the library currently offers, I can guarantee you: you will bang your head on the wall trying to fight with the compiler and the patterns that GoF taught you. You’re dealing with a badly written library and are probably better off with a complete re-write.
But, if the library already offers an OOPish interface, a wrapper around most functions would not require more than 2-3 lines of code per function. And as we all know: lesser the code, fewer the bugs.
Again, depending on the library, it can be very difficult/impossible to test individual functions in isolation. This could be because you don’t have access to the internals of the library, the library communicates with the outside world (network) and/or causes side-effects (talking to the kernel). This could be the result of a bad design or the very nature of the component. 100% test coverage, in this case, is much more effort than it’s worth. The best you could do to ensure that no bugs creeped in is pair programming or getting your code reviewed by other programmers. And of course, pray.
New Attack Vectors
Security vulnerabilities are a special type of bugs that could potentially be exploitable and capable of bringing a system (or the internet?) down to its knees and have exposed organizations running for cover (and much, much more). But should this be a deterrent to a well-read software engineer? Of course not! It should rather be a warning: you should always be security-conscious when you write your code.
Fortunately, code written in C++ in arguably safer than the same thing written in C. This is because C++ gives you the power of abstractions that take the responsibility of the nasty bits away from the caller and towards the callee. For example, common issues like buffer overflows and format string vulnerabilities can be easily avoided by using the C++ standard library functions instead of their C stdlib counterparts.
Very few rookie programmers, if any at all, use snprintf
instead of sprintf
.
Wrapping everything in C++ and making it a point (across your team) to use the
standard library/Boost as much as possible avoids a whole mountain of security
issues.
The Process
So you’re convinced that the C library you’re going to use in your quest for world domination needs to be wrapped in C++. Great. But how do you do it? It really depends on the library. For a few libraries, it might be as simple as creating classes with the appropriate methods, adding/deleting the move, copy and other constructors, operators and the destructor.
However, sometimes the picture isn’t as rosy. The library maybe exposes
functions that accept function pointers to provide callbacks and notifications
and you want to provide compatibility with std::function
(hint: lambdas that
capture variables cannot be cast to function pointers). Or maybe it returns a
status
enum value for each and every function and you want a uniform
way of handling these values (exceptions).
You will need to jump through various hoops, write various helper functions and think like an API user to be create a useful interface that works. At times, it will be boring and tedious (who likes mapping C-style enums to C++ enum classes?). Other times, it could leave you scratching your head (the curious case of function pointers).
But if done right, the beauty of code written in C++ using your wrapper will make you orgasm. I promise.
P.S.: cppnats is a C++ wrapper that I
made for a medium-sized library (cnats). It includes simple tests that highlight
usage. It isn’t perfect and there are a few things I couldn’t manage to
translate to C++ (read: wasn’t motivated enough) like non-RAII pointer
encapsulation of natsConnection
and natsSubscription
but it suits my current
needs well.