Omniverse Doxygen Guide

Before diving into this guide, be sure to read Omniverse Documentation Guidelines to get the big picture on how to write effective documentation.

C++ APIs in Omniverse are documented using Doxygen. This has several benefits:

  • API documentation is embedded inline, with minimal markup, in actual API headers. This makes documentation easy to read when browsing source.

  • The API documentation can be transformed to an intermediate representation (XML) that Omniverse’s documentation system can ingest. This intermediate representation can be converted to HTML with full cross-referencing/hyperlinking of not only API entities but also entities defined outside of Doxygen.

The Omniverse documentation system supports most of Doxygen’s commands. It even adds new abilities that allow the embedding of reStructuredText in Doxygen-style comments.

ExampleDoxygen.h provides a short-guide on useful Doxygen commands. You can find its output at ExampleDoxygen.h. Its source follows:

// Copyright (c) 2021, NVIDIA CORPORATION. All rights reserved.
//
// NVIDIA CORPORATION and its licensors retain all intellectual property
// and proprietary rights in and to this software, related documentation
// and any modifications thereto. Any use, reproduction, disclosure or
// distribution of this software and related documentation without an express
// license agreement from NVIDIA CORPORATION is strictly prohibited.
//

//! @file ExampleDoxygen.h
//!
//! @brief All headers to be included by Doxygen must have a <a
//! href="https://www.doxygen.nl/manual/commands.html#cmdfile">\@file</a> statement like this.
#pragma once

#include "../../../include/carb/Defines.h"

//! Namespaces can be documented like any other entity.  In fact, namespace must be documented in order for variables
//! and functions to appear in Doxygen's output.  Only a single instance of the namespace should be documented.
namespace example
{

//! This is a class.  The text up to the first empty line is considered the "brief" comment.  It can span multiple
//! sentences and lines, much like this comment.
//!
//! Anything after the first "paragraph" is consider the "detailed" description.  This can contain multiple paragraphs
//! and directives.
//!
//! See @rstref{Omniverse Doxygen Documentation Guide <doxygen_guide>} to see the raw input to this document.
//!
//! Referencing other entities is done via the <a href="https://www.doxygen.nl/manual/commands.html#cmdref">\@ref</a>
//! directive.  For example, the first non-constructor in this class is @ref exampleMethod.
//!
//! Technically, Doxygen will try to auto-link tokens that match known entities even without <a
//! href="https://www.doxygen.nl/manual/commands.html#cmdref">\@ref</a>.  The benefit of using <a
//! href="https://www.doxygen.nl/manual/commands.html#cmdref">\@ref</a> is that if Doxygen can't find the reference,
//! you'll get a build time error.  As such, the use of <a
//! href="https://www.doxygen.nl/manual/commands.html#cmdref">\@ref</a> is highly encouraged to make sure Doxygen is
//! doing what you think it's doing.
//!
//! The full list of Doxygen commands can be found at: https://www.doxygen.nl/manual/commands.html (note: this link was
//! auto-linked).
class ExampleDoxygen
{
public:
    //! This is the brief comment for the example method.  It's a nice comment.
    //!
    //! We now start the "full" documentation after a blank line.
    //!
    //! Docs can have multiple paragraphs.  Paragraphs are defined by blank lines.  Many small paragraphs (each getting
    //! a single point across) are preferred over giant paragraphs.  This is technical documentation, not Shakespere.
    //!
    //! Parameters are documented with the <a href="https://www.doxygen.nl/manual/commands.html#cmdparam">\@param</a>
    //! command.
    //!
    //! Parameters can be referenced in docs with <a href="https://www.doxygen.nl/manual/commands.html#cmdp">\@p</a>.
    //! For example, check out @p x.
    //!
    //! It's useful to reference other entities in docs.  In @ref ExampleDoxygen's docs, we showed how to use \@ref.
    //! Another useful command is <a href="https://www.doxygen.nl/manual/commands.html#cmdsee">\@see</a>, which creates
    //! a special section in the docs.
    //!
    //! Doxygen supports a dedicated statement to denote the return value of a function: <a
    //! href="https://www.doxygen.nl/manual/commands.html#cmdreturns">\@returns</a>.
    //!
    //! Text markup is a lot like Markdown:
    //!
    //! - `Inline Code`: \`Inline Code\`
    //! - *Italics*: \*Italic\*
    //! - **Bold**: \**Bold\**
    //!
    //! | Right | Center | Left  |
    //! | ----: | :----: | :---- |
    //! | 10    | 10     | 10    |
    //! | 1000  | 1000   | 1000  |
    //!
    //! There are <a href="https://www.doxygen.nl/manual/markdown.html#mddox_emph_spans">limits</a> to the Markdown like
    //! syntax above.  When those fail, old-school HTML commands like ``<i>`` and ``<b>`` can be used.
    //!
    //! Code examples can also be embedded.  For example:
    //!
    //! @code{.cpp}
    //!
    //! // use this class as such
    //! auto example = new DoxygenExample;
    //! auto value = example->exampleMethod("i'm an example", 1, 2, 3);
    //!
    //! @endcode
    //!
    //! Commands like <i>\@see</i>, <i>\@returns</i>, and <i>\@param</i> should come at the end of the documentation
    //! block in order to avoid confusing Doxygen.
    //!
    //! @see See @ref myProtectedFunction for another method to call.  This was generated with <i>\@see</i>.
    //!
    //! @returns Returns a pointer to a useful `float`, that totally means something... useful.  Could return `nullptr`
    //! if something went wrong, which is likely.  This was generated with <i>\@returns</i>.
    //!
    //! @param name Name of the thing to pass to the method.  Must not be `nullptr`.  This was generated with
    //! <i>\@param.</i>
    //!
    //! @param x, y, z Multiple parameters can be given the same description by using a `,`.  This was generated with
    //! <i>\@param</i>.
    //!
    float* exampleMethod(const char* name, int x, int y, int z);

    //! Shows advanced usage of the Omniverse documentation system.
    //!
    //! @ref exampleMethod shows vanilla Doxygen.  Omniverse adds some other features to Doxygen to better integrate
    //! Doxygen with its documentation system.
    //!
    //! Use the <i>\@thread_safety</i> command to denote the thread safety of an entity.
    //!
    //! A block of ReStructuredText can be embedded with <i>\@rst</i> and <i>\@endrst</i>:
    //!
    //! @rst
    //!
    //! This text is in ReStructuredText, but looks like it's a part of Doxygen.  We're about to show an admonition.
    //!
    //! .. tip:: ReStructuredText can be added with ``@ rst`` and ``@ endrst``.
    //!
    //! This text is also in ReStructuredText.  It supports links to other ReStructuredText docs like
    //! :ref:`documenting`.
    //!
    //! @endrst
    //!
    //! Inline ReStructuredText can be embedded via <i>\@inlinerst</i> and <i>\@endrst</i>.
    //!
    //! Linking to a ReStructuredText reference is handled with <i>\@rstref{}</i>.  For example, this is a link to the
    //! ReStructuredText document on @rstref{How to Unlock the Full Power of ReStructuredText <rst_guide>}. Note,
    //! that these links need both the ReStructuredText reference in `<>` and the text to display (before the
    //! `<>`).
    //!
    //! Links to Markdown files can be inserted with <i>\@rstdoc{}</i>.  For example, to link to repo_docs's *Change
    //! Log*: @rstdoc{CHANGES.md <../../../../CHANGES>}  The path given to <i>\@rstdoc</i> is relative to the
    //! documentation's build directory, which for repo_docs is <i>_build/docs/repo_docs/latest/</i>.  This filename
    //! should not contain a file extension.
    //!
    //! @thread_safety Unlike the rest of Omniverse, this method is not thread safe.  This was generated with
    //! <i>\@thread_safety</i>.
    //!
    void advancedExampleMethod();

    // Functions that are public but you want to specifically leave undocumented can be made private with \@private.
    //!
    //! @private
    void internalOnlyFunction();

protected:
    //! Protected functions are included in the Doxygen output.
    void myProtectedFunction();

private:
    // Implementation details, like private data, are not included in the Doxygen output.
    int _myPrivateData;
};

// There are times when Doxygen should either not document an entity or is unable to do so (e.g. Doxygen's support for
// template specialization is poor).  In these cases, use the DOXYGEN_BUILD preprocessor symbol.
#ifndef DOXYGEN_BUILD
void myUndocumentedFunction();
#endif

//! @struct example::OpaqueStruct
//!
//! @brief Opaque C structs (such as carb::tasking::Counter) will not be processed by Doxygen as forward declarations.
//! Use the \@struct command to explicitly document the opaque struct.
struct OpaqueStruct;

} // namespace example

Doxygen Tips

  • Use @ref when referencing an entity. There’s no downside. If the reference can’t be found, you’ll get an warning, which you’ll have to fix. You may be tempted to rely on Doxygen’s automatic link generation. However, if automatic link generation fails, it will do so silently.

  • Prefer using //! over /** ... */. The //! is visually distinctive, and clearly denotes the comment is for Doxygen. Additionally, you save two lines over the common pattern of putting /** and */ on their own line.

  • Prefer prefixing Doxygen commands with @ rather than \. @ has the benefit of being neither an escape character nor a valid C++ token character.

  • Doxygen’s @param command supports flags such as in, out, etc. You’re free to use these flags, though they are not a requirement. We consider these flags to be harmful, as there is nothing validating their correctness. In the future, we expect better integration of Doxygen with OMNI_ATTR, which provides a much richer semantic meaning to paramters.

  • Doxygen does not require that all parameters to a function be documented. Parameters that are pointers should be documented, since a pointer’s lifetime, ownership, and validity can be ambiguous.

Documenting Old Code

When building Omniverse, a primary goal of its build system is to flag any compiler warnings as errors. While the compiler may be able to continue when a warning is encountered, warnings represent something that may be amiss. Rather than ignoring the potential problem, we address it. We avoid technical debt.

Following this philosophy of avoiding technical debt, the goal of our documentation build system is to produce zero warnings.

Tip

A warning produced by the documentation build system likely represents an issue you have introduced. You are expected to contribute documentation that produces zero build warnings.

Reach out to the #ct-omni-docs for help with dealing with warnings.

As noble as this goal is, it’s a recent goal. Omniverse has years of documentation technical debt built within its codebase. It will take many man-months to pay.

How do we meet our goal of zero warnings but also avoid having to stop everything to pay this technical debt?

In short, we pay the debt a little a time. In repo.toml, we’re selective about which files we feed to Doxygen (see doxygen_input). As we document both old and new code, we add it to doxygen_input.

Unfortunately, old code tends to reference additional old code that we’ve yet to fully document. That code will produce warnings. In order to silence warnings, we add an entry to include/Undocumented.h. While this produces a subpar user experience, it does eliminate warnings in the documentation build and gives the developer more signal as to when they are doing something wrong. Said differently, include/Undocumented.h allows the developer to be in the healthy mindset of “If I see a warning, it’s my fault.” That mindset leads to the reduction of technical debt and, with time, a quality user experience.

Troubleshooting Doxygen

WARNING: Explicit markup ends without a blank line; unexpected unindent.

This is likely due to a bug in Exhale. Ensure that your file has a @file and @brief comment. For example:

//! @file
//!
//! @brief Defines the base class for ABI-safe interfaces.

warning: unable to resolve reference to '<some entity>' for @ref command

Following is a checklist of approaches to fix this warning:

  • In order for entities in the global namespace, like macros, to be documented by Doxygen, the file in which they appear must have a proper @file entry. For example:

    //! @file
    //!
    //! @brief Defines the base class for ABI-safe interfaces.
    
  • If a @file entry exists, qualify the entity with its namespace. For example ITypeFactory may not be found, but omni::core::ITypeFactory will.

  • For functions, if the function is not found, add the parameter types to the @ref. For example: @ref carb::Framework::acquireInterface(const void*).

  • Functions and variables in a namespace will appear in Doxygen’s output only if all of the owning namespaces are documented.

  • Finally, make sure the reference is spelled correctly. 😉

_docs/enum_AudioTypes_8h_1adbc8db68d94518154fcea3b12398ae56.rst:: WARNING: Could not lex literal_block as "c++". Highlighting skipped.

This occurred on the following documentation:

/** The Opus codec.
 *
 *  This is a lossy codec that is designed to be suitable for almost any
 *  application.
 *  ...
 */

This was caused due to the blank line after the first line, combined with the 2 spaces used to align the text on the third line. The line break meant that the third line increased its tabbing by one space, which was detected as a code block.

This can be solved by removing the blank line or reducing the alignment by one space.

(!) Unabridged API: unexpected kind 'page' (IGNORED)

This occurs when you use an unsupported Doxygen command, such as @deprecated.

@rst blocks render as bullet points.

If you’ve tried to use a @rst block, such as the one below, you may encounter unexpected bullet points. This occurs because of the * characters, since @rst takes all text until the @endrst character.

/**  @rst
 *  :ref:`carbonite-audio-label`
 *  @endrst
 */

Cross-reference errors due to [in]

Error in cross-reference.
If shorthand ref:
  Invalid C++ declaration: Expected identifier in nested name. [error at 0]
    [in]
    ^

This occurs when you use [in] on parameters to macros.

Strange namespaces such as omni.structuredlog::@25 shows up in the documentation

Doxygen will document anonymous namespaces as @[0-9]+. You can either ifndef out the namespace with DOXYGEN_SHOULD_SKIP_THIS or you can document the namespace and specify that it’s an anonymous namespace.

Failing to resolve references on private member variables:

If documentation contains a reference to a private member variable, Doxygen will report a failure to resolve the symbol by default. This is because it does not emit documentation for private member variables by default so none of their references can be resolved. This is however also unfortunately the case when the documentation for a private member variable contains a reference to another private member variable in the same class.

To work around this, the @ref reference could be changed to either @p or @a to silence the warning.

Briefs for types, enums, or classes don’t show up in API lists:

This is often caused by one or more of the namespaces containing the documented symbol not being documented itself. When the full chain of namespaces containing the symbol is not documented, odd omissions can result even if there aren’t any other reference resolution warnings from Doxygen.

To fix this, simply add a one-line brief documentation for each of the namespaces containing the symbol. Note that a namespace only needs to be documented once for references to symbols inside it to be resolved. If all occurrences of a namespace are to be documented, it should be the same text each time for consistency.