This repository showcases a pattern/technique called Hourglass for implementing C APIs, and header only C++ APIs, for libraries implemented in C++. The problem that this pattern solves is the lack of standard ABI in C++. What this means is that each compiler, and even different versions of the same compiler, can have non-compatible ABIs. What this means in practise is that if you compile a C++ library, that exposes functionality of the library using a C++ API, using one compiler, and then you compile and link the application code using another compiler, you most likely will end up having linking problems. Maintainers of commercial C++ libraries, where only binaries are distributed, solve this problem by implementing C APIs for their libraries.
Hourglass technique for avoiding ABI problems.This is inconvenient, since C lacks the expressive power of C++, meaning that you force the user of the C++ library to interact with it using C. The Hourglass technique solves this problem by implementing a header only C++ API, that is based on the C API, allowing users of the library to interact with it using C++. This is also where the pattern gets its name from. Hourglass technique first narrows C++ to C and then widens (expressiveness) back to C++. The original concept of the Hourglass C API technique is from Stefanus DuToit. He discusses it in this video from the 2014 CppCon.
This repository implements a LibStoryBoard
library that implements a storyboard object that contains note objects. Each note exposes the following functions:
std::string getTitle()
std::string getText()
std::vector<std::string> getTags()
, whereas a storyboard exposes the following functions:
void addNote(const Note& newNote)
void addNote(Note&& newNote)
int deleteNote(const Note& deleteMe)
int searchByTitle(const std::string& title, note_cont_t& container)
int searchByText(const std::string& text, note_cont_t& container)
int searchByTag(const tag_cont_t& tags, note_cont_t& container)
The library, after having been built and installed using cmake, exposes both C- and C++-APIs. The C++ API is a header only, based on the C API. Original C++ objects are not exposed directly.
One problem that arises when you go from C++ to C, and back to C++, is related to containers and the STL-library. The STL-library implements many different containers, like the std::vector. The problem is how to deal with API calls that return arrays. The Hourglass pattern deals with this in a quite innovative way: it calls a callback function for each element in the container and let's the application code decide what to do. One of the parameters given for the callback function is client data. This can be, for example, a pointer to a std::vector that then calls push_back for each element.
Let's take a look at how this is implemented in the Hourglass pattern. LibStoryBoard
implements note- and storyboard classes in C++. The library
has both C API and C++ API (header only, based on the C API). Following is a simplification of the Note
object from the library, implemented in C++.
Actual implementation can be found in storyboard.cpp.
class Note
{
public:
std::vector<std::string> getTags();
private:
std::vector<std::string> tags_container;
}
The C API, available at storyboardCppAPI.hpp, defines a note_query_handler
function pointer that takes a void*
pointer and a const char*
pointer. note_get_tags
is the function, defined in the C API, that is
used for querying the tags of a given note_in
, where the note_in
is an opaque pointer.
typedef void (*note_query_handler)(void* client_data, const char* tag);
int32_t note_get_tags(const note_t note_in, note_query_handler handler, void* client_data, error_t_* out_error);
Implementation of note_get_tags
calls the handler
function for each element of the std::vector
returned by the getTags()
function.
int32_t note_get_tags(const note_t note_in, note_query_handler handler, void* client_data, error_t_* out_error)
{
if (!note_in)
{
*out_error = new error{"note_in not initialized"};
return 0;
}
translateExceptions(out_error, [&] {
std::vector<std::string> query_result(note_in->actual.getTags());
for (auto&& elem : query_result)
{
handler(client_data, elem.c_str());
}
});
return note_in->actual.getTags().size();
}
It is left to the user of the C API to decide what the handler
function should do in the application code. If, for example, he/she wants it to
print the tags, the handler
function could be as follows:
void query_tags(void* client_data, const char* tag)
{
printf("%s ", tag);
}
Example of application code that uses the C API is available from c_executable.c.
Now, using this very same C API, we have implemented a header only C++ API, available at storyboardCppAPI.hpp, that uses this same approach for querying note's tags as follows:
class Note
{
public:
std::vector<std::string> getTags() const
{
std::vector<std::string> tags_container;
auto callback = [](void* client_data, const char* tag) {
((std::vector<std::string>*)client_data)->push_back(std::string(tag));
};
note_get_tags(m_opaque, callback, &tags_container, ThrowOnError{});
return tags_container;
}
}
This repository contains the following directories.
- LibStoryBoard
- LibStoryBoard library is written in C++, but it also has a C API, implemented using the hourglass approach.
- Uses modern CMake as a build system
- C executable
- A C executable that uses LibStoryBoard via the C API.
- Uses modern CMake as a build system
- C++ executable
- A C++ executable that uses LibStoryBoard via the hourglass C++ API.
- Uses modern CMake as a build system
You first need to build and install the LibStoryBoard library and then build either one of the executables for testing. The library and the executables use modern CMake as the build system.
- build.sh
- Builds everything and executes the C and C++ executables. You need to install C and C++ compilers, and cmake>=3.24 before running the script.
- format_code.sh
- Runs
clang-format
on all the c, cpp, h and hpp files. Intended to be used before committing code. GitHub actions uses Ubuntu 20.04 to test the code in the PRs.
- Runs