The problem of using a C++ library compiled with Compiler A, from a program compiled with Compiler B has been a problem for a while. This is especially true on Windows where Visual C++ generally breaks binary compatibility from release to release. Shipping a library for Windows involves shipping several versions for Visual C++ as well now often for mingw gcc.
Some of the problems C++ has in regards to binary compatibility across different compilers are:name mangling, object layout, exception support.
There are several ways to get around this:
There are whole books written on COM, so I won’t try to go into too many details. A brief overview in regards to the binary interface is here. The basic idea is that you define an interface like this:
Interface Definition
struct Interface;
struct InterfaceVtable{
int (*Function1)(struct Interface*);
int (*Function2)(struct Interface*, int);
};
struct Interface{
struct InterfaceVtable* pTable;
};
It can be used like this.
Using an Interface
struct Interface* pInterface = GetInterfaceSomehow();
int a = pInterface->pTable->Function1(pInterface);
Implementing an interface
like this is painful and will be left as an exercise to the reader .
Fortunately, (and by design), Microsoft Visual C++ and most Windows C++ compilers will generate something compatible to the above with an abstract
base class using pure virtual
functions.
Interface using C++ (MSVC)
struct InterfaceCpp{
virtual int Function1() = 0;
virtual int Function2(int) = 0;
};
You can implement and use like this:
struct InterfaceImplementation:public InterfaceCpp{
virtual int Function1(){return 5;}
virtual int Function2(int i){return 5 + i;}
};
InterfaceImplementation imp;
InterfaceCpp* pInterfaceCpp = &imp;
std::cout << pInterfaceCpp->Function2(5) << std::endl;
The reason for this is that the version with function pointers was doing a vtable
and a vptr
by hand and this version is letting the compiler do it. For more information about vtable
and vptr
, see the excellent article by Dan Saks in Dr. Dobbs.
While the above solution works on Windows (generally), this is not guaranteed to always work A more general cross-platform solution is presented in Matthew Wilson’s Imperfect C++ in chapters 7 and 8. He basically provides a way and macros that allow you to define the above structure manually (i.e., define your own vtable
s).
By using either COM style interfaces with compilers that have a compatible vtable
layout or rolling your own, you can have cross-compiler binary compatible interfaces. However, you do not have:
- Exceptions
- Due to not having exceptions, you often have to use error codes and thus do not have real return values
- Standard C++ types such as
vector
and string
(use arrays and const char*
)
In fact, in an article explaining why Microsoft created C++/CX Jim Springfield stated one of the problems with COM even with libraries such ATL was “There is no way to automatically map interfaces from low-level to a higher level (modern) form that throws exceptions and has real return values.”
During this series of posts, I will discuss the development of a C++11 library that has the following benefits:
- Able to use
std::string
and std::vector
as function parameters and return values - Use exceptions for error handling
- Compatible across compilers – able to use MSVC to create .exe and g++ to create .dll on Windows, and g++ for executable and clang++ to create .so on Linux
- Works on Linux and Windows
- Written in Standard C++11
- No Macro magic
- Header only library
As we progress, we will talk about some of the disadvantages and areas for improvements and possible alternatives. Here is how we would define an interface
DemoInterface
. Note jrb_interface
is the namespace
of the library.
using namespace jrb_interface;
template<bool b>
struct DemoInterface
:public define_interface<b,4>
{
cross_function<demointerface,0,int(int)> plus_5;
cross_function count_characters;
cross_function say_hello;
cross_function(std::string)>
split_into_words;
template
DemoInterface(T t):DemoInterface::base_t(t),
plus_5(t), count_characters(t),say_hello(t),split_into_words(t){}
};
In this library, all interface
s are actually templates that take a bool parameter. The reason for this will become clear as we discuss the implementation in later posts.
All interfaces inherit from define_interface
which takes a bool
parameter (just use the bool
passed in to the template) and an int
parameter specifying how many functions are in the interface
. If you pass in a too small number, you will get a static_assert
telling you that the number is too small.
To define a function in the interface, use the cross_function
template.
The first parameter is the interface in this case DemoInterface
. The second parameter is the 0 based position of the function. The first function is 0, the second is 1, the third 2, etc. The third and final parameter of cross_function
is the signature of the function is the name style as std::function
.
Finally, all interfaces need a templated constructor that takes a value t
and passes it on to the base class as well as each function. For convenience, the define_interface
template defines a typedef
base_t
that you can use in your constructor initializer.
To implement an interface
, you would do this:
struct DemoInterfaceImplemention:
public implement_interface<demointerface>{
DemoInterfaceImplemention(){
plus_5 = [](int i){
return i+5;
};
say_hello = [](std::string name)->std::string{
return "Hello " + name;
};
count_characters = [](std::string s)->int{
return s.length();
};
split_into_words =
[](std::string s)->std::vector{
std::vector ret;
auto wbegin = s.begin();
auto wend = wbegin;
for(;wbegin!= s.end();wend = std::find(wend,s.end(),' ')){
if(wbegin==wend)continue;
ret.push_back(std::string(wbegin,wend));
wbegin = std::find_if(wend,s.end(),
[](char c){return c != ' ';});
wend = wbegin;
}
return ret;
};
}
};
To implement an interface
, you derive from implement_interface
specifying your Interface
as the template parameter. Then in your constructor, you assign a lambda with the same signature you specified in the definition of the interface
to each of the cross_function
variables.
To use an interface
, you construct use_interface
providing the Interface
as the template parameter.
int i = iDemo.plus_5(5);
int count = iDemo.count_characters("Hello World");
std::string s = iDemo.say_hello("John");
std::vector words = iDemo.split_into_words("This is a test");
You then call the functions just as you would with any class object. Note the use of . instead of –>
Thank you for taking the time to read this post. I hope this has piqued your interest. In future posts, we will explore how we create this library, and how we can extend this library to do more. I hope you will join me.
You can find compilable code at https://github.com/jbandela/cross_compiler_call. The code has been tested on:
- Windows with compiling the executable with MSVC 2012 Milan (Nov CTP) and the DLL with mingw g++ 4.7.2
- Ubuntu 12.10 with compiling the executable with g++ 4.7.2 and the .so file with clang++ 3.1
Instructions on how to compile are included in the README.txt file.
Please let me know what you think in the comments section.