Good Documentation¶
Tearing apart bad documentation may be cathartic, but there’s good documentation out there too. It deserves recognition. Today, I’m going to pick on SDL. I’m not talking about the quality of the API itself, just its documentation.
In the anti-cherry-picking spirit of the previous post, I’ll take the
first file I find. That’s SDL_assert.h
since SDL.h is just a bunch of #includes with 0 comments. Not
that there’s anything wrong with that.
The first thing I want to say is that I find Doxygen ugly. SDL doesn’t
actually use Doxygen, exactly, but they do use a lot of things that
look a lot like Doxygen, including /** and \param. I’m
holding this API up as an exemplar of good documentation in spite of
these things.
That said, if ever there were a use case for generated docs instead of just reading the headers, it would be in a widely used library that’s usually installed on desktop computers and just linked. Most people do not find themselves reading SDL’s headers, much less code. So the annotations that make the generated docs a bit easier to read at the cost of making the headers a bit harder to read are probably on the good end of the tradeoff scale.
File-level¶
So without further ado, here’s the first comment block in the file:
/**
* # CategoryAssert
*
* A helpful assertion macro!
*
* SDL assertions operate like your usual `assert` macro, but with some added
* features:
*
* - It uses a trick with the `sizeof` operator, so disabled assertions
* vaporize out of the compiled code, but variables only referenced in the
* assertion won't trigger compiler warnings about being unused.
* - It is safe to use with a dangling-else: `if (x) SDL_assert(y); else
* do_something();`
* - It works the same everywhere, instead of counting on various platforms'
* compiler and C runtime to behave.
* - It provides multiple levels of assertion (SDL_assert, SDL_assert_release,
* SDL_assert_paranoid) instead of a single all-or-nothing option.
* - It offers a variety of responses when an assertion fails (retry, trigger
* the debugger, abort the program, ignore the failure once, ignore it for
* the rest of the program's run).
* - It tries to show the user a dialog by default, if possible, but the app
* can provide a callback to handle assertion failures however they like.
* - It lets failed assertions be retried. Perhaps you had a network failure
* and just want to retry the test after plugging your network cable back
* in? You can.
* - It lets the user ignore an assertion failure, if there's a harmless
* problem that one can continue past.
* - It lets the user mark an assertion as ignored for the rest of the
* program's run; if there's a harmless problem that keeps popping up.
* - It provides statistics and data on all failed assertions to the app.
* - It allows the default assertion handler to be controlled with environment
* variables, in case an automated script needs to control it.
* - It can be used as an aid to Clang's static analysis; it will treat SDL
* assertions as universally true (under the assumption that you are serious
* about the asserted claims and that your debug builds will detect when
* these claims were wrong). This can help the analyzer avoid false
* positives.
*
* To use it: compile a debug build and just sprinkle around tests to check
* your code!
*/
So this is a file-level comment, explaining what’s in the file, why
you would want to use it, and how to use it. There’s an extra *
in the first line, a single comment-comment to tell the document
generator about wiki categories, and then zero further characters that
are not there for the benefit of a human reader (or required by the
compiler).
There’s no \brief nonsense; the short description is on the
first line, separated by whitespace, so a person can easily parse
it. The bulleted list simultaneously explains why this file duplicates
functionality in the standard library and what kinds of things someone
might use it to do. It talks about performance implications of using
the things in this file, and non-obvious implications of using it
(like Clang’s static analysis hints). All-around good documentation.
A Macro¶
The next documented symbol in this file is a constant
macro. Amusingly, it’s defined to SomeNumberBasedOnVariousFactors
inside the document generator’s context. The specifics of the factors
are pretty easily gleaned from the next few lines, but they’re also
pretty ugly and environment-dependent.
#ifdef SDL_WIKI_DOCUMENTATION_SECTION
/**
* The level of assertion aggressiveness.
*
* This value changes depending on compiler options and other preprocessor
* defines.
*
* It is currently one of the following values, but future SDL releases might
* add more:
*
* - 0: All SDL assertion macros are disabled.
* - 1: Release settings: SDL_assert disabled, SDL_assert_release enabled.
* - 2: Debug settings: SDL_assert and SDL_assert_release enabled.
* - 3: Paranoid settings: All SDL assertion macros enabled, including
* SDL_assert_paranoid.
*
* \since This macro is available since SDL 3.2.0.
*/
#define SDL_ASSERT_LEVEL SomeNumberBasedOnVariousFactors
#elif !defined(SDL_ASSERT_LEVEL)
#ifdef SDL_DEFAULT_ASSERT_LEVEL
#define SDL_ASSERT_LEVEL SDL_DEFAULT_ASSERT_LEVEL
#elif defined(_DEBUG) || defined(DEBUG) || \
(defined(__GNUC__) && !defined(__OPTIMIZE__))
#define SDL_ASSERT_LEVEL 2
#else
#define SDL_ASSERT_LEVEL 1
#endif
#endif
So here we have a short description of what the symbol is, a vague description of how its value is managed, a caveat about forward compatibility (including an implicit guarantee that existing values won’t change), and an enumerated list of possible values and their meanings.
Then there’s a tag for the documentation generation system
(since) and a statement about what versions of the library
support it.
I like how each setting is given a short name and an expanded description. I don’t love this as a way to make an enumeration, but we’re here to talk about the documentation, not the API being documented. And besides, this is intended to be configured in the build system, so this may actually be the cleanest way to do it in portable C.
Another Macro¶
Now we have a macro function: SDL_TriggerBreakpoint(). It looks
like a function, it’s named just like the other functions in
SDL. Let’s take a look at its documentation.
/**
* Attempt to tell an attached debugger to pause.
*
* This allows an app to programmatically halt ("break") the debugger as if it
* had hit a breakpoint, allowing the developer to examine program state, etc.
*
* This is a macro--not a function--so that the debugger breaks on the source
* code line that used SDL_TriggerBreakpoint and not in some random guts of
* SDL. SDL_assert uses this macro for the same reason.
*
* If the program is not running under a debugger, SDL_TriggerBreakpoint will
* likely terminate the app, possibly without warning. If the current platform
* isn't supported, this macro is left undefined.
*
* \threadsafety It is safe to call this macro from any thread.
*
* \since This macro is available since SDL 3.2.0.
*/
#define SDL_TriggerBreakpoint() TriggerABreakpointInAPlatformSpecificManner
Short description of what it does, slightly longer statement about why someone would use it, followed by several things a user should know when calling this function (macro).
Why is it implemented as a macro? “…so that the debugger breaks on the source code line that used SDL_TriggerBreakpoint and not in some random guts of SDL.”
What is the intended use for this? When the program is running under a debugger.
What will happen if it’s used outside that context? Something you probably don’t want your users to encounter.
What happens if it’s used where it’s not supported? Compile error.
Amazing. Then we have some annotation about thread safety and API version support. Useful!
Enumeration¶
After skipping over some more well-documented macros (and a nice
/* comment explaining to maintainers why something is the way it
is) we come to a typedef enum:
/**
* Possible outcomes from a triggered assertion.
*
* When an enabled assertion triggers, it may call the assertion handler
* (possibly one provided by the app via SDL_SetAssertionHandler), which will
* return one of these values, possibly after asking the user.
*
* Then SDL will respond based on this outcome (loop around to retry the
* condition, try to break in a debugger, kill the program, or ignore the
* problem).
*
* \since This enum is available since SDL 3.2.0.
*/
typedef enum SDL_AssertState
{
SDL_ASSERTION_RETRY, /**< Retry the assert immediately. */
SDL_ASSERTION_BREAK, /**< Make the debugger trigger a breakpoint. */
SDL_ASSERTION_ABORT, /**< Terminate the program. */
SDL_ASSERTION_IGNORE, /**< Ignore the assert. */
SDL_ASSERTION_ALWAYS_IGNORE /**< Ignore the assert from now on. */
} SDL_AssertState;
What can I say? It’s got a short description, pointers to related
functions, and what all of the values mean. It’s good, it’s
parsimonious, and instead of listing something like RETURNED_BY:
it suggests to the user why it exists and where to look to find how to
use it.
Data structure¶
The next thing to be defined in the file is a struct:
/**
* Information about an assertion failure.
*
* This structure is filled in with information about a triggered assertion,
* used by the assertion handler, then added to the assertion report. This is
* returned as a linked list from SDL_GetAssertionReport().
*
* \since This struct is available since SDL 3.2.0.
*/
typedef struct SDL_AssertData
{
bool always_ignore; /**< true if app should always continue when assertion is triggered. */
unsigned int trigger_count; /**< Number of times this assertion has been triggered. */
const char *condition; /**< A string of this assert's test code. */
const char *filename; /**< The source file where this assert lives. */
int linenum; /**< The line in `filename` where this assert lives. */
const char *function; /**< The name of the function where this assert lives. */
const struct SDL_AssertData *next; /**< next item in the linked list. */
} SDL_AssertData;
We know what the information is about, what the data structure’s used
for, what all the members mean, and that it’s a linked list. The names
of the fields are pretty good, so their descriptions are largely
redundant, but the boilerplate is minimal and most of the descriptions
do actually add information. At least trigger_count isn’t documented as “trigger count.”
Function¶
Finally we come to a function declaration. The first one,
SDL_ReportAssertion, says never to call it directly so we’ll skip
that. Instead, I want to talk about a real function that’s intended to
be used by API users.
/**
* Set an application-defined assertion handler.
*
* This function allows an application to show its own assertion UI and/or
* force the response to an assertion failure. If the application doesn't
* provide this, SDL will try to do the right thing, popping up a
* system-specific GUI dialog, and probably minimizing any fullscreen windows.
*
* This callback may fire from any thread, but it runs wrapped in a mutex, so
* it will only fire from one thread at a time.
*
* This callback is NOT reset to SDL's internal handler upon SDL_Quit()!
*
* \param handler the SDL_AssertionHandler function to call when an assertion
* fails or NULL for the default handler.
* \param userdata a pointer that is passed to `handler`.
*
* \threadsafety It is safe to call this function from any thread.
*
* \since This function is available since SDL 3.2.0.
*
* \sa SDL_GetAssertionHandler
*/
extern SDL_DECLSPEC void SDLCALL SDL_SetAssertionHandler(
SDL_AssertionHandler handler,
void *userdata);
So here we’re registering a callback. Why would anyone want this? What happens if the user never calls it? Both answered in the first paragraph.
Are there special things we should know about how handler will be
called? Yes! It can fire from any thread, but only from one thread at
a time.
Does this function have any surprising behavior? Yes. Its effects are
not reversed by calls to SDL_Quit().
Then it actually lists out the parameters. Interesting fact: passing
NULL as the handler will reset it to the default.
I could have guessed that userdata was going to get passed to
handler, but saying this explicitly is a lot better than \param
userdata user data.
We also added a “see also” annotation for the documentation generator. This becomes a link in the wikified docs, but it’s actually pretty useful in the header too. Note that this isn’t a list of functions in the call graph, but related a function that an API user might want to know about.
Conclusion¶
I hit one of each type of thing, but some of the other functions in
the document have other kinds of information given about them. For
example, SDL_GetAssertionHandler goes into detail about the
output argument, when it will be NULL and when it’s ok to pass in
NULL.
After that, SDL_GetAssertionReport has an example of how it’s
used and goes into detail about its return value. It even talks about
the specific way in which it is not threadsafe.
There’s also the fact that this is a header file, the correct place to document a C API.
I talked a lot about what was there, but just as important is what isn’t. Nowhere is the fact that a function has no return value pointed out. The comment blocks don’t duplicate the names of the functions (or the file) or just mechanically repeat the names of their arguments without adding any information.
There aren’t boilerplate fields that must be filled out, but whose
contents don’t matter. Nowhere is the author of a function named. The
description of the function is just a description; it doesn’t need 3
lines to say DESCRIPTION before telling me what it does. This
documentation treats the reader like a person who wants to know how to
use an API, not a person with a checklist who wants to make sure all
the documentation elements are present.
This is good documentation.
Comments¶
Normally, headers don’t have much in the way of comments on the implementation. They’re just there to provide an interface, after all. However, a header that provides assertion macros is necessarily going to have some implementation in it. As such, there are comments intended for maintainers. These are kept outside the
#ifdef SDL_WIKI_DOCUMENTATION_SECTIONblocks, and they begin with/*instead of/**. Here are a couple representative samples:This comment explains why we’re doing this weird thing. It’s to support a dumb compiler. Plus, it’s entertaining.
This is a pretty big block comment explaining why the code is the way it is. Note how it doesn’t say that this is the macro called when an assertion is disabled. That’s obvious, and the reader is a maintainer who should know that. Instead it talks about why it was implemented in this way.
It’s not API documentation, but good comments are good too.