Home » Articles » C++ »The Basics »Tutorials » Binary Compatible C++ Interfaces (Part 1)

Binary Compatible C++ Interfaces (Part 1)

By Jim Susoy - July 20, 2014 @ 2:58 pm

c++-175x117

Introduction

This is the first part of a multi-part series on the challenges and techniques for creating Binary Compatible C++ Interfaces (BCCI). In researching this topic, I often found the issues enumerated without (or with very little) explanation as to why something should be avoided. As a result I have tried to include the “why” along with the “what” in this series of posts.

This first post will go through the causes of incompatibilities and the reasons why. The second post will summarize these into a set of rules, and discuss various ways to prevent or work around the incompatibilities. The third installment will cover implementing binary compatible C++ interfaces using some of the techniques discussed.

One of C++’s greatest short comings (at least with regards to interoperability) is the lack of an Application Binary Interface (ABI) as part of the standard. C++ is a language specification after all, and many would argue that is out of the scope of the standard to define specific implementations. The result is that binaries produced with one compiler, are not necessarily compatible with those produced by another compiler (or even by different versions of the same compiler!). This simple fact greatly hampers our ability as developers to easily create C++ libraries for distribution to a broad audience. This leaves developers with only two distribution options: (1) provide source for their libraries, or (2) provide multiple binaries, one for each version of each compiler that a customer might use. The matrix for that can get out of hand quickly.

A solution to this is to attempt to export all public APIs using a mechanism that creates a stable and cross-compiler compatible binary interface. Doing this does come with compromises, however you will have to decide if they are worth it or not for your specific situation.

Compiler Incompatibilities

Compiler incompatibilities typically boil down to the C++ standard defining a particular feature’s implementation as being … “implementation defined”. The compiler writers are left to implement language features however they like as long as the functionality conforms to the standard. So what “features” cause these incompatibilities, and is it possible to work around them to produce a binary with a stable and interoperable ABI? Let’s talk first about the various things that cause problems. Other than the standards lack of definition what specifically causes the problems for sharing binary interfaces?

Calling Conventions

Problems with mismatched calling conventions is an issue that is common to both C and C++. This issue is not a problem with implementation, but rather with defaults that are undefined by the standards. The caller of a function or method must understand how it needs to call that function/method or things will break … badly. What order are the arguments placed on the stack? Are they even placed on the stack, or in registers, or in both? Who is responsible for popping them off the stack when the called method or function returns? Below is a partial table of calling conventions that illustrates some of the differences.

Keyword Stack Cleanup Argument Passing Decoration Case Translation
__cdecl Caller Pushes arguments on the stack in reverse order (right to left) Underscore character (_) is prefixed to names, except when __cdecl functions that use C linkage are exported. None
__stdcall Callee Pushes arguments on the stack in reverse order (right to left) An underscore (_) is prefixed to the name. The name is followed by the at sign (@) followed by the number of bytes (in decimal) in the argument list. Therefore the function declared as int func( int a, double b ) is decorated as follows: _func@12 None
__fastcall (MSFT) Callee The first two DWORD or smaller arguments that are found in the argument list from left to right are passed in ECX and EDX registers; all other arguments are passed on the stack from right to left. At sign (@) is prefixed to names; an at sign followed by the number of bytes (in decimal) in the parameter list is suffixed to names. None
__thiscall (MSFT) Callee Pushed on stack right to left (x86: this pointer passed in via ECX). By default all class methods use this calling convention [ 1 ] igorsk. “Reversing Microsoft Visual C++ Part II: Classes, Methods and RTTI,” September 21, 2006. Section "Calling Conventions and Class Methods", http://www.openrce.org/articles/full_view/23. None None
x64 Callee 4 register fast-call calling convention, with stack-backing for those registers. Any argument that doesn’t fit in 8 bytes, or is not 1, 2, 4, or 8 bytes, must be passed by reference. The arguments are passed in registers RCX, RDX, R8, and R9. If the arguments are float/double, they are passed in XMM0L, XMM1L, XMM2L, and XMM3L. [ 2 ] Michael Matz et. al, "System V Application Binary Interface AMD64 Architecture Processor Supplement", October 2013, http://www.x86-64.org/documentation/abi-0.99.pdf None None

Different compilers are free to use different default calling conventions. That is, if you declare a function or method without explicitly specify the calling convention, each compiler is free to choose which one they will use. This of course can lead to incompatibilities when using binaries compiled with different compilers. As you can see in the table above, there are also some compiler specific calling conventions that would cause obvious problems if you attempted to use them with a compiler that does not implement them.

Padding

Like Calling Conventions, padding (a.k.a alignment) is another interoperability issue that C and C++ have in common. The C++ standard has this to say about alignment:

“An alignment is an implementation-defined integer value representing the number of bytes between successive addresses at which a given object can be allocated.” [ 3 ] “Programming Languages — C ++ (Draft Standard),”  May 15, 2013. Section 3.11.1 Alignment, https://isocpp.org/files/papers/N3690.pdf

The default alignment is left up to the implementer and this results in different compilers using different default values. The same data structure compiled with different padding results in structures layouts that are not equivalent and therefore not compatible.

Name Mangling

Name mangling (a.k.a. “function name encoding)” was originally included as part of the C++ specification to enable function overloading. However, today’s compilers also use it to enable type-safe linkage. [ 4 ] Ellis, Margaret A., and Bjarne Stroustrup. The Annotated C++ Reference Manual. Reading, Mass: Addison-Wesley Professional, 1990. Section 7.2.1c, pg. 122 While the standard provides an  example mangling scheme, it leaves the details up to the compiler’s implementers.  As a result, different compilers typically do not use the same scheme. This even happens with different versions of the same compiler (usually between major version changes), making an executable compiled with one version of a compiler potentially incompatible with a shared library compiled with a different version of the same compiler.

Exception Handling

By its nature, exception handling is very platform specific, and not surprisingly, different compilers use very different implementations. Some compilers use what is known as the Zero-Cost Strategy where exception records are kept in side tables. DWARF debugging information is then used to unwind the stack when an exception is thrown. The advantage here is that entering try/catch blocks are fast, but executing a throw is very slow. GCC and LLVM implement this strategy.

Microsoft’s compilers on the other hand use Structured Exception Handling (SEH) to implement C++ exceptions by managing a chain of EXCEPTION_REGISTRATION_RECORDs embedded on the stack [ 5 ] Roger Orr. “Microsoft Visual C++ and Win32 Structured Exception Handling,” October 2004. http://www.howzatt.demon.co.uk/articles/oct04.html and then calling the operating system RaiseException() function with “special” arguments. SEH is an operating system feature [ 6 ] “A Crash Course on theDepths of Win32 Structured Exception Handling, MSJ January 1997.” Accessed July 24, 2014. http://www.microsoft.com/msj/0197/Exception/Exception.aspx. that Microsoft’s compilers use to implement C++ exception handing.

Given an executable compiled with GCC and a shared library compiled with MSVC, if the shared library were to throw a C++ exception that it did not handle, the different implementations would prevent it from working and the program would likely crash. This would happen even if the GCC compiled executable had the correct C++ exception handling to catch the exception.

Run Time Type Information (RTTI)

RTTI is a feature which provides run-time type information for polymorphic classes (classes that have one or more virtual methods) through the dynamic_cast<> and typeid() language features. It provides functionality that a pointer to a derived class to be retrieved given the base class. The C++ standard indicates that the RTTI implementation is  implementation defined. You can see where this is going…

For polymorphic types, compilers will typically store type information in an object by placing a pointer to a separate “type information object” in the object’s vtable [ 7 ] Stroustrup, Bjarne. The C++ Programming Language: Special Edition. 3 edition. Reading, Mass: Addison-Wesley Professional, 2000. Section 15.4.1, pg. 409 . Microsoft’s Visual Studio compiler, however, stores a pointer to the “Complete Object Locator” (which is an implementation of the “type information object” the standard speaks of) at a negative offset in the vtable.[ 8 ] igorsk. “Reversing Microsoft Visual C++ Part II: Classes, Methods and RTTI,” September 21, 2006. Section “RTTI Implementation”, http://www.openrce.org/articles/full_view/23

Object Layout

While the C++ standard does impose some restrictions on object layout, it leaves many important aspects unspecified and therefore up to the compiler implementation to determine. According to the C++ Standard, non-static data members in an object with the same access control as represented in memory, “are allocated so that later members have higher addresses within a class object”. [ 9 ] “Programming Languages — C ++ (Draft Standard),”  May 15, 2013. Section 9.2.13 Class Members, https://isocpp.org/files/papers/N3690.pdf. The result of that wording is that members do not have to be contiguous. It also specifies in the same section that the “order of allocation of non-static data members with different access control is unspecified” and that two members might not to be allocated immediately after each other as the result of alignment requirements, space for managing virtual functions (vtables) and virtual base classes.

If you were to allocate a class in a DLL compiled with Compiler A and were to export that class (for the sake of this example, assume both compilers use the exact same name mangling strategy) to an executable compiled with Compiler B, they would most likely have different layouts in memory. The result would most likely be a crash or some other undefined behavior.

Non-POD Types

Exporting non-pod (Plain Old Data) types from your DLLs interface or passing them as arguments to the functions that you export is another problematic area. Mostly for reasons already discussed, they will have name mangling issues, object layout issues, etc. POD types conform to standard layouts that are C compatible and more interoperable as a result.

Virtual Methods

Again there is no C++ ABI and compilers are free to implement this functionality in any way they choose.  In Stroustrup’s orginal object model, non-static data members are allocated directly within each class object and a table of virtual methods (typically referred to as the “vtable”) is created for each class with a pointer to the vtable (usually called the “vptr”) being inserted directly into the class object. [ 10 ] Lippman, Stanley B. Inside the C++ Object Model. 1 edition. Reading, Mass: Addison-Wesley Professional, 1996. Chapter 1, pg. 8 If RTTI is enabled, a pointer to the type_info structure is usually placed into the first slot of the vtable (in the RTTI section above we already know there are multiple ways to implement RTTI).

This is just one way of implementing virtual methods; different compilers are free to choose.

Overloaded Virtual Methods

The standard does not specify the order in which the overloaded methods are listed in the vtable. It not necessarily the order of declaration.

DLL Boundary Issues

All DLL boundary issues are the result of different state and/or different code interpretations on side of the DLL boundary. Following are discussions on how memory allocation, resource usage and code implementation may differ on each side of the boundary, sometimes in only subtle ways.

Memory Allocation

On some platforms (Windows), you cannot allocate memory on one side of a DLL boundary and then free it on the other side, even when using the same compiler. When using different compilers, this can be an issue on all platforms. This is the result of there actually being different heaps on each side of the boundary when shared libraries are statically linked with the C/C++ runtime libraries, or dynamically linked to different versions of the runtime libraries. [ 11 ] Raymond Chen. “Allocating and Freeing Memory across Module Boundaries,” September 15, 2006. http://blogs.msdn.com/b/oldnewthing/archive/2006/09/15/755966.aspx.

Mixed Implementations

With each side of the boundary using its own heap implementation, (via calls to malloc(), free(), new, new[], delete, delete[], etc) undefined behavior ensues. Take the operator new [] and delete []  implementations. They allocate/free an array of objects, potentially constructing and destructing the objects along the way. The implementation on each side of the boundary could store information about how many objects were allocated in the array differently. When using a different delete [] implementation the result could be a crash or performing some other undefined behavior like calling the incorrect address when attempting to destruct an object in the array.

Using standard containers across DLL boundaries presents another, more subtle manifestation of this same problem. Standard containers are not designed to conform to a common ABI (if it existed), and therefore suffer from all the issues previously discussed. Additionally, the implementation of the container itself, iterators, and other related templates could be completely different on each side of the boundary.

For example, given this code that sorts a vector:

#include <vector>
#include <iostreams>
#include <string>

int main() {
  std::vector<string> *pVec = getVectorFromDll();

  std::sort(vec->begin(), vec->end());
  for (int i = 0; i < vec.size(); ++i) 
     std::cout << vec[i] << ' ';
}

From the executable, a pointer to the vector allocated and populated in the DLL is retrieved by a call to the getVectorFromDll() function. This function is exported by the DLL. Next std::sort() is called to sort the contents of the vector. However, the std:sort() that is called is actually the EXEs version of std::sort(). If the EXE and DLL were compiled with different compilers (or even different versions of the same compiler), the implementation of std::sort() in the EXE most certainly does not match the one in the DLL. The results are undefined.

If this were a container that implemented its own sort method such as std::list<>, calling the containers own sort method would be safe because it would use the correct implementation in the DLL.

Resource Utilization

Abstractions for resources such as file handles, sockets, etc can also be stored as part of the “state” that may be maintained separately on each side of the boundary.  This could mean that a file handle might represent different files on each side of the boundary or be invalid one side and not the other.

Conclusion

As you can see there is a lot working against creating a stable, cross-compiler compatible binary interface that can be used on multiple platforms or even just a single platform. The lack of an ABI standard for C++ is the driving factor, however the lack of a standard ABI gives compiler vendors more leeway to innovate features and tune performance in their offerings. We have seen that various language features such as exceptions and RTTI break compatibility as well as fundamental architectural differences like object layout and name mangling. In the next installment, we will discuss various strategies to work around these issues. The goal is to enumerate the various strategies, understand how and why each work, and what compromises are made by each.

If I have missed anything, you find errors or you have questions/feedback, please leave a comment and I will respond or update the article appropriately.

References

1.
igorsk. “Reversing Microsoft Visual C++ Part II: Classes, Methods and RTTI,” September 21, 2006. Section “Calling Conventions and Class Methods”, http://www.openrce.org/articles/full_view/23.
2.
Michael Matz et. al, “System V Application Binary Interface AMD64 Architecture Processor Supplement”, October 2013, http://www.x86-64.org/documentation/abi-0.99.pdf
3.
“Programming Languages — C ++ (Draft Standard),”  May 15, 2013. Section 3.11.1 Alignment, https://isocpp.org/files/papers/N3690.pdf
4.
Ellis, Margaret A., and Bjarne Stroustrup. The Annotated C++ Reference Manual. Reading, Mass: Addison-Wesley Professional, 1990. Section 7.2.1c, pg. 122
5.
Roger Orr. “Microsoft Visual C++ and Win32 Structured Exception Handling,” October 2004. http://www.howzatt.demon.co.uk/articles/oct04.html
6.
“A Crash Course on theDepths of Win32 Structured Exception Handling, MSJ January 1997.” Accessed July 24, 2014. http://www.microsoft.com/msj/0197/Exception/Exception.aspx.
7.
Stroustrup, Bjarne. The C++ Programming Language: Special Edition. 3 edition. Reading, Mass: Addison-Wesley Professional, 2000. Section 15.4.1, pg. 409
8.
igorsk. “Reversing Microsoft Visual C++ Part II: Classes, Methods and RTTI,” September 21, 2006. Section “RTTI Implementation”, http://www.openrce.org/articles/full_view/23
9.
“Programming Languages — C ++ (Draft Standard),”  May 15, 2013. Section 9.2.13 Class Members, https://isocpp.org/files/papers/N3690.pdf.
Lippman, Stanley B. Inside the C++ Object Model. 1 edition. Reading, Mass: Addison-Wesley Professional, 1996. Chapter 1, pg. 8
Raymond Chen. “Allocating and Freeing Memory across Module Boundaries,” September 15, 2006. http://blogs.msdn.com/b/oldnewthing/archive/2006/09/15/755966.aspx.

"Binary Compatible C++ Interfaces (Part 1)" was published on July 20th, 2014 and is listed in C++, The Basics, Tutorials.

Follow comments via the RSS Feed | Leave a comment | Trackback URL


Leave Your Comment