Creating modern C++ API docs using Sphinx on Read-the-Docs

By Nulltek

C++ is a language thats popularity has dwindled over the last decade. With the rise of powerhouse high level languages such as Javascript, Ruby and Python, alongside the advances in modern day compute hardware, it can be difficult to justify using a language like C++. There are a few exceptions such as extremely performant critical software or embedded systems development, the later something I am well acquainted with. With the rest of the industry we are seeing rapid advancements in development pipelines, tending more and more towards automation. Due to its lacklustre growth C++ often gets pushed to the sidelines by many of these toolchains. In this article I document my process taking a C++ project, and using an automated pipeline for building and serving API docs.

Being an Avid python developer I am familiar with using sphinx autodoc to generate API documentation from inline doc strings. The sphinx framework is also trivial to serve on the-read-docs platform, as it is natively supported and free for open-source projects. So why is it not that simple for c++? Well… it kind of can be that simple, and I’m going to use one of my old c++ projects to prove it.

Doxygen has been the defacto tool for generating C++ API docs for about the last 2 decades, and it’s still maintained to this day, albeit showing its age. You can use doxygen to produce raw xml representations of your sources API, then all you need a middle-man to translate this format into something sphinx can understand. This is where breathe comes in, breathe is a tool that specifically acts as a bridge between sphinx and doxygen.

Okay so there are quite a few layers here, but lets not get overwhelmed, the first thing to do is ensure you can successfully build your APIs documentation via Doxygen. Doxygen is available natively via most unix package managers, and on windows it can be obtained by binary installer or (my preference) using the chocolatey package manager. Doxygen gets its configuration from a Doxyfile, which is a plain text file with no extension. This file can be templated using doxygen -g Doxyfile. Be warned this config is very verbose, but there are only a few values you need to tweak. The key changes are below:


# Tell it the name of your project
PROJECT_NAME           = "Null Packet Comms Arduino"

# Tell it where to find your source code relative to the doxyfile
INPUT                  = ../../src/

# Give us the API data in a useful form
GENERATE_XML           = YES

I like to leave the HTML output enabled, that way it’s easy to view the build at this stage to debug any issues with the API content, you can see my Doxyfile here. Provided you have documented your C++ code using doxygen compliant comments, by simply running doxygen you’ll get some nice documentation that looks like it has come straight out of the mid 2000s. So now let’s get that looking a little more modern!

First we need to set a sphinx project in the same manner as if we were doing so for python. We’ll need a conf.py however we’re going to have to create an index restructured text file to serve as the main page and define a few additional things for breathe support.

extensions = ["breathe"]
master_doc = "index"

# Tell breathe where the doxygen xml is
breathe_projects = {
    "NullPacketComms": "../doxygen/xml"
}
# Tell breathe what doxygen project it's building.
breathe_default_project = "NullPacketComms"

# Tell sphinx what the primary language being documented is.
primary_domain = 'cpp'

# Tell sphinx what the pygments highlight language should be.
highlight_language = 'cpp'

# Then it's always worth tossing on the RTD theme to get it looking sharp

html_theme = "sphinx_rtd_theme"

And that’s all there is to it. Now you are free to drop your c++ API documentation wherever suits in a normal sphinx .rst hierarchy using breathe directives. You can then just build your doc sources using sphinx and you’ll get a nice modern version of the doxygen documentation.

# I use following directive to display the API for a c++ class
.. doxygenclass:: NullPacketComms
   :members:

Now we just have to automated this via read-the-docs post-commit hooks, which again turns out to be surprisingly easy. Add the normal read the docs templated .yaml file and point it to your documentation directory. Then we simply need to add the following code into our conf.py, this checks if it is being built in the read the docs environment, and if so builds via doxygen at the start of the sphinx build. Fortunately read-the-docs, while not officially supporting it, comes with doxygen installed out-the-box.

if os.environ.get('READTHEDOCS', None) == 'True':
    # We are building live docs, doxygen needs to be run first.
    subprocess.call('cd ../doxygen; doxygen', shell=True)

Wow, actually not that complicated and now I can build good looking c++ documentation using pretty much my normal python pipeline. Check out my final docs here.