Creating a New Carbonite Interface

Overview

Carbonite interfaces are simple yet powerful when used appropriately. There are a few considerations to be made when choosing to make a Carbonite interface versus an ONI one. Carbonite interfaces are best used in situations where global data or state can be used or when an object hierarchy is not necessary. Using a Carbonite interface for these projects can simplify the workflow, but also has some drawbacks and restrictions.

Some benefits of Carbonite interfaces are:

  • Interfaces are simple and can be more efficient to call into. There does not need to be an API wrapper layer in the interface and there are not as many restrictions on what can be passed to functions.

  • Interfaces are quick and easy to implement. A new plugin for a Carbonite interface can be created in a matter of minutes.

  • They match better to some usage cases.

Some of the main drawbacks with Carbonite interfaces are:

  • Interfaces are not reference counted. Since a Carbonite interface is nothing more than a v-table containing function pointers, every caller that attempts to acquire a given interface will receive the same object. However, plugins can create reference-counted objects, such as carb.events.plugin.

  • Interfaces cannot have data associated with them. Contextual object information returned from a Carbonite interface must go through opaque pointers that then get passed back to other functions in the interface to operate on. This often ends up making the interface flat and monolithic instead of hierarchical.

  • ABI safety and backward compatibility between interface versions is difficult to maintain without close attention to detail when making changes. It is easy to break compatibility with previous versions of an interface without necessarily realizing it. This was one of the main motivations for creating ONI and its toolchain.

  • Historically, a given Carbonite plugin may only implement a single version of any given interface. While a plugin may export multiple versions of an interface now, earlier plugins may only support the latest interface version.

In this guide we’ll make an interface called carb::stats::IStats to collect and aggregate simple statistical values.

Project Definitions

The first step in creating a new Carbonite interface (and a plugin for it) is to add a project new definition to the premake5.lua script:

    project "example.carb.stats.plugin"
        define_plugin { ifaces = "source/examples/example.stats/include/carb/stats", impl = "source/examples/example.stats/plugins/carb.stats" }
        defines { "carb_stats_IStats=carb_stats_IStats_latest" }
        dependson { "carb" }
        includedirs { "source/examples/example.stats/include" }
        filter { "system:linux" }
            buildoptions { "-pthread" }
            links { "dl", "pthread" }
        filter {}

This adds a new plugin project called example.carb.stats.plugin. When built, this will produce a dynamic library such as example.carb.stats.plugin.dll (on Windows). The plugin is set to implement some or all of the interfaces in the source/examples/example.stats/include/carb/stats/ folder (link) with its implementation files in source/examples/example.stats/plugins/carb.stats/ (link).

Note the line defines { "carb_stats_IStats=carb_stats_IStats_latest" } for this plugin definition. This ensures that building the plugin always uses the latest version of the IStats interface. A similar line will need to be specified for each interface that the plugin exports. See below for further explanation.

Example implementation files:

Define the Interface API

The next step is to create the interface’s main header. According to the coding standard for Carbonite, this should be located in a directory structure that matches the namespace that will be used (ie: include/carb/stats/ in this case). The interface’s header file itself should have the same name as the interface (ie: IStats.h here).

The bare minimum requirements for declaring an interface are that the carb/Interface.h header be included, a #pragma once guard is used, and at least one struct containing a call to the CARB_PLUGIN_INTERFACE or CARB_PLUGIN_INTERFACE_EX macro be made.

It is recommended to prefer CARB_PLUGIN_INTERFACE_EX for new interfaces, and to switch existing interfaces to this method when they need to change. The first step is to define the latest and default versions near the top of the file:

//! Latest IStats interface version
#define carb_stats_IStats_latest CARB_HEXVERSION(1, 1)
#ifndef carb_stats_IStats
//! The current default IStats interface version
#    define carb_stats_IStats CARB_HEXVERSION(1, 0)
#endif

In this example, the latest version of the API available is 1.1 but by default consumers will use the default version: 1.0. These macros also correspond to the defines line from premake5.lua above, where the module always overrides the version to be the latest version.

Any requirements for the interface (ie: enums, parameter structs, constants, etc) should be declared either before the interface’s struct or in another header that is included at the top of the file.

Now the interface itself can be declared:

/**
 * (Example--not a real interface)
 *
 * A simple global table of statistics that can be maintained and aggregated.  Statistics
 * can be added and removed as needed and new values can be aggregated into existing
 * statistics.  Each statistic's current value can then be retrieved at a later time for
 * display or analysis.
 */
struct IStats
{
    CARB_PLUGIN_INTERFACE_EX("carb::stats::IStats", carb_stats_IStats_latest, carb_stats_IStats)

    /** Adds a new statistic value to the table.
     *
     *  @param[in] desc The descriptor of the new statistic value to add.
     *  @returns An identifier for the newly created statistic value if it is successfully added.
     *           If this statistic is no longer needed, it can be removed from the table with
     *           removeStat().
     *  @retval kBadStatId if the new statistic could not be added or an error occurred.
     *
     *  @thread_safety This operation is thread safe.
     */
    StatId(CARB_ABI* addStat)(const StatDesc& desc);

    /** Removes an existing statistic value from the table.
     *
     *  @param[in] stat The identifier of the statistic to be removed.  This identifier is
     *                  acquired from the call to addStat() that originally added it to the
     *                  table.
     *  @returns `true` if the statistic is successfully removed from the table.
     *  @returns `false` if the statistic could not be removed or an error occurred.
     *
     *  @thread_safety This operation is thread safe.
     */
    bool(CARB_ABI* removeStat)(StatId stat);

    /** Adds a new value to be accumulated into a given statistic.
     *
     *  @param[in] stat     The identifier of the statistic to be removed.  This identifier is
     *                      acquired from the call to addStat() that originally added it to the
     *                      table.
     *  @param[in] value    The new value to accumulate into the statistic.  It is the caller's
     *                      responsibility to ensure this new value is appropriate for the given
     *                      statistic.  The new value will be accumulated according to the
     *                      accumulation method chosen when the statistic was first created.
     *  @returns `true` if the new value was successfully accumulated into the given statistic.
     *  @returns `false` if the new value could not be accumulated into the given statistic or the
     *           given statistic could not be found.
     *
     *  @thread_safety This operation is thread safe.
     */
    bool(CARB_ABI* addValue)(StatId stat, const Value& value);

    /** Retrieves the current accumulated value for a given statistic.
     *
     *  @param[in]  stat    The identifier of the statistic to be removed.  This identifier is
     *                      acquired from the call to addStat() that originally added it to the
     *                      table.
     *  @param[out] value   Receives the new value and information for the given statistic.  Note
     *                      that the string values in this returned object will only be valid
     *                      until the statistic object is remvoed.  It is the caller's
     *                      responsibility to ensure the string is either copied as needed
     *                      or to guarantee the statistic will not be removed while its value is
     *                      being used.
     *  @returns `true` if the statistic's current value is successfully retrieved.
     *  @returns `false` if the statistic's value could not be retrieved or the statistic
     *           identifier was not valid.
     *
     *  @thread_safety It is the caller's responsibility to ensure the given statistic is
     *                 not removed while the returned value's strings are being used.  Aside
     *                 from that, this operation is thread safe.
     */
    bool(CARB_ABI* getValue)(StatId stat, StatDesc& value);

#if CARB_VERSION_ATLEAST(carb_stats_IStats, 1, 1)
    /** Retrieves the total number of statistics in the table.
     *
     *  @returns The total number of statistics currently in the table.  This value can change
     *           at any time if a statistic is removed from the table by another thread.
     *
     *  @thread_safety Retrieving this value is thread safe.  However, the actual size of the
     *                 table may change at any time if another thread modifies it.
     */
    size_t(CARB_ABI* getCount)();
#endif
};

Notice that the getCount() function is wrapped by an #if block with a macro: CARB_VERSION_ATLEAST. This indicates that the getCount() function is only usable with the specified interface version (1.1) or higher.

The CARB_PLUGIN_INTERFACE_EX macro call adds some simple boilerplate code to the interface that facilitates discovery in plugins. It needs to know the name of the plugin (with the full namespace) as well as the version number of the interface (major and minor version numbers). Carbonite uses semantic versioning. This line also uses the version macros that were defined above: carb_stats_IStats_latest and carb_stats_IStats.

See Interface Design Considerations below for more information on how to best design your interface, stick to our coding standard, and avoid various pitfalls.

Add an Implementation for the Interface

A Carbonite interface may have multiple implementations. Typically these are done so that each implementation handles a specific type of data (ie: carb.dictionary.serializer-json.plugin versus carb.dictionary.serializer-toml.plugin) or each implementation provides functionality from a different backend library (ie: carb.profiler-cpu.plugin versus carb.profiler-tracy.plugin versus carb.profiler-nvtx.plugin). By convention, the backend provider or data handling context is reflected in the plugin’s name itself.

Source files

A simple interface like carb::stats::IStats could theoretically be implemented in a single source file. However for example purposes here, we’ll split it up into multiple files as though it was a more complex interface. The following files will be used here:

  • source/examples/example.stats/plugins/carb.stats/Stats.h: A common internal header file for the implementation of the plugin. This should contain any functional definitions needed to implement the interface. This file is relatively straightforward and can simply be examined.

  • source/examples/example.stats/plugins/carb.stats/Stats.cpp: The actual implementation of the carb::stats::IStats interface as a C++ class. This class implementation is relatively straightforward and can simply be examined.

  • source/examples/example.stats/plugins/carb.stats/Interfaces.cpp: The definition of the Carbonite interface itself. This file typically acts as the point where all interfaces in the plugin are exposed to the Carbonite framework. This file includes the simple C wrapper functions used to call through to the C++ implementation class.

Boilerplate Code Generation

The plugin’s information and interface description must be exported. This is typically done by a macro. But first, a interface-design-considerations-label must be made if your plugin is going to support multiple versions or not. It is highly recommended to support multiple versions.

Multiple Interface Versions

All of the interfaces exported by the plugin must be listed in the CARB_PLUGIN_IMPL_EX macro. This will generate exported functions that the Carbonite Framework expects when it loads the plugin. See CARB_PLUGIN_IMPL_EX for more information.

The macro expects as its first parameter a carb::PluginImplDesc–a descriptor for the plugin.

For each interface exported by the plugin, a template<class T> bool fillInterface(carb::Version*, void*) explicit template specialization must be provided. The framework will call this with a carb::Version to construct the interface of that version. If the plugin cannot construct the version, it may return false to indicate to the Framework that the version is not supported.

/// Define the descriptor for this plugin.  This gives the plugin's name, a human readable
/// description of it, the creator/implementor, and whether 'hot' reloading is supported
/// (though that is a largely deprecated feature now and only works properly for a small
/// selection of interfaces).
const struct carb::PluginImplDesc kPluginImpl = { "example.carb.stats.plugin", "Example Carbonite Plugin", "NVIDIA",
                                                  carb::PluginHotReload::eDisabled, "dev" };

/// Define all of the interfaces that are implemented in this plugin.  If more than one
/// interface is implemented, a comma separated list of each [fully qualified] plugin
/// interface name should be given as additional arguments to the macro.  For example,
/// `CARB_PLUGIN_IMPL_EX(kPluginImpl, carb::stats::IStats, carb::stats::IStatsUtils)`.
CARB_PLUGIN_IMPL_EX(kPluginImpl, carb::stats::IStats)

/** Main interface filling function for IStats.
 *
 *  @remarks This function is necessary to fulfill the needs of the CARB_PLUGIN_IMPL_EX() macro
 *           used above.  Since the IStats interface was listed in that call, a matching
 *           fillInterface() for it must be implemented as well.  This fills in the vtable
 *           for the IStats interface for use by callers who try to acquire that interface.
 */
template <>
bool fillInterface<carb::stats::IStats>(carb::Version* v, void* iface)
{
    using namespace carb::stats;
    switch (v->major)
    {
        case IStats::getLatestInterfaceDesc().version.major:
            *v = IStats::getLatestInterfaceDesc().version;
            *static_cast<IStats*>(iface) = { addStat, removeStat, addValue, getValue, getCount };
            return true;

        default:
            return false;
    }
}

Single Interface Version

Historically, Carbonite plugins could only support semantic versions for one major version. This method is still supported but not recommended. However, it may be ideal to create the first version with this simpler method and transition to Multiple Version support when the version changes to a new major version.

All of the interfaces exported by the plugin must be listed in the CARB_PLUGIN_IMPL macro. This will generate exported functions that the Carbonite Framework expects when it loads the plugin. See CARB_PLUGIN_IMPL for more information.

The macro expects as its first parameter a carb::PluginImplDesc–a descriptor for the plugin.

For each interface exported by the plugin, a void fillInterface(InterfaceType&) function must be provided (where InterfaceType is the interface type exported by the plugin).

/// Define the descriptor for this plugin.  This gives the plugin's name, a human readable
/// description of it, the creator/implementor, and whether 'hot' reloading is supported
/// (though that is a largely deprecated feature now and only works properly for a small
/// selection of interfaces).
const struct carb::PluginImplDesc kPluginImpl = { "example.carb.stats.plugin", "Example Carbonite Plugin", "NVIDIA",
                                                  carb::PluginHotReload::eDisabled, "dev" };

/// Define all of the interfaces that are implemented in this plugin.  If more than one
/// interface is implemented, a comma separated list of each [fully qualified] plugin
/// interface name should be given as additional arguments to the macro.  For example,
/// `CARB_PLUGIN_IMPL_EX(kPluginImpl, carb::stats::IStats, carb::stats::IStatsUtils)`.
CARB_PLUGIN_IMPL(kPluginImpl, carb::stats::IStats)

/** Main interface filling function for IStats.
 *
 *  @remarks This function is necessary to fulfill the needs of the CARB_PLUGIN_IMPL() macro
 *           used above.  Since the IStats interface was listed in that call, a matching
 *           fillInterface() for it must be implemented as well.  This fills in the vtable
 *           for the IStats interface for use by callers who try to acquire that interface.
 */
void fillInterface(carb::stats::IStats& iface)
{
    using namespace carb::stats;
    iface = { addStat, removeStat, addValue, getValue, getCount };
}

Dependencies

Plugins declare their dependencies with the CARB_PLUGIN_IMPL_DEPS macro. This is a list of interfaces required by this plugin. If the Framework is unable to provide these dependencies, your plugin will not load. For dependencies declared in this way, your plugin is free to use carb::Framework::acquireInterface() to acquire the interface.

It is recommended that your plugin limit dependencies, and instead attempt to acquire interfaces using carb::Framework::tryAcquireInterface() which allows them to fail. Your plugin would need to then handle the situation where the other interface is not found.

Keep in mind that dependencies are at the plugin level, not the interface level. If your plugin changes to add a dependency this may break backwards compatibility.

If your plugin has no dependencies, you can optionally state this explicitly with CARB_PLUGIN_IMPL_NO_DEPS:

/// Mark that this plugin is not dependent on any other interfaces being available.
CARB_PLUGIN_IMPL_NO_DEPS()

Exported Callback Functions

It is generally recommended to include carbOnPluginStartup() (or carbOnPluginStartupEx() if startup may fail) and carbOnPluginShutdown() functions, but these are not required.

Warning

It is very important that no Carbonite functionality (logging, Framework, profiling, etc.) is used prior to when the Framework calls the carbOnPluginStartup() function at plugin initialization time! This includes but is not limited to static initializers! Prior to this function call, the plugin pointers (such as g_carbFramework or g_carbLogging) have not been initialized.

See the Framework Overview section on finding plugins for a full list of callback functions that the Framework looks for in a plugin.

CARB_EXPORT void carbOnPluginStartup()
{
    /// Do any necessary global scope plugin initialization here.  In this case nothing needs
    /// to be done!
}

CARB_EXPORT void carbOnPluginShutdown()
{
    /// Do any necessary global scope plugin shutdown here.  This should include resetting any
    /// global variables or objects to their defaults, to a point where they can be safely
    /// reinitialized in the next carbOnPluginStartup() call.
    ///
    /// Note that because of some dynamic linking behavior on Linux, it is possible that on
    /// unload this module could be shutdown but not actually unloaded from memory.  This would
    /// prevent any global variables from being reset or cleaned up during C++ static
    /// deinitialization.  However, if the plugin were loaded into the process again later,
    /// C++ static initialization would not occur since the module was not previously unloaded.
    /// Thus it is important to perform any and all global and static cleanup explicitly here.
    ///
    /// In our case here, the only thing to do would be to empty the global stats table so that
    /// it's in the expected empty state should the plugin be loaded again.

    g_stats.clear();
}

The plugin callbacks are exported from the plugin module itself. These are declared with CARB_EXPORT in a .cpp file to indicate that they are to be exported from the dynamic library. The optional callbacks are:

  • carbOnPluginStartup(): If this callback is exported and carbOnPluginStartupEx() is not exported, this callback is performed to notify the plugin that it has been loaded by the Carbonite framework. This callback is intended to handle any initialization tasks that are needed by the library before any interfaces from it are acquired. This is guaranteed to only be called once per module load instance and called before any interfaces in the plugin can be acquired. If the module is unloaded, the startup callback function will be called again if the next time it is loaded. See carbOnPluginStartup() for more info.

  • carbOnPluginStartupEx(): This callback is a newer version of the carbOnPluginStartup() callback function. If exported, it will be preferentially called instead of the older version. It has a boolean return value, which allows it to fail gracefully. See carbOnPluginStartupEx() for more info.

  • carbOnPluginShutdown(): If this callback is exported, it will be called whenever the plugin module is unloaded from the process. This may occur at any time the module is intentionally unloaded, not necessarily just at process exit time. The only exception to this is that it is not called when the process is terminated by carb::quickReleaseFrameworkAndTerminate(). This callback is intended to be the point where the plugin should handle its full set of shutdown tasks. This may include releasing or freeing system resources, flushing data to disk, or cleaning up global objects.

  • carbOnPluginQuickShutdown(): If this callback is exported, it will be called when the host app shuts down via carb::quickReleaseFrameworkAndTerminate(). If this function is not exported, nothing else is called and the application terminates. This function is intended to notify the application of a quick shutdown–the plugin should immediately flush and close files or save any persistent state, as quickly as possible. The plugin should not worry about freeing memory or checking for leaks.

Once these files are in place, the plugin can be built. It should produce a plugin module that the Carbonite framework can load and discover the carb::stats::IStats interface in.

Using the New Interface

Now that the new plugin has been built, a host app will be able to discover and load it. Let’s create a simple example app. A simple example host app can be found at source/examples/example.stats/example.stats/main.cpp (link). Much of this example app is specific to the carb::stats::IStats interface itself. The only parts that really need some explanation are:

  • CARB_GLOBALS must be called at the global scope. This adds all of the global variables needed by the Carbonite framework to the host app. This will include symbols to support functionality such as logging, structured logging, assertions, profiling, and localization. This must be called in exactly one spot in the host app.

/// This macro call must be made at the global namespace level to add all of the Carbonite
/// Framework's global symbols to the executable.  This only needs to be used in host apps
/// and should only be called once in the entire module.  The specific name given here is
/// intended to name the host app itself for use in logging output from the Carbonite
/// framework.
CARB_GLOBALS("example.stats");
  • Before the interface can be used, it must be acquired by the host app. This can be done in one of many ways with the Carbonite framework. The commonly suggested method is to use the carb::getCachedInterface() helper functions. These will be the quickest way to find and acquire an interface pointer since the result will be cached internally once found. If the framework unloads the plugin that a cached interface came from, it will automatically be reacquired on the next call to carb::getCachedInterface(). The particular usage of each interface differs. Please see each interface’s documentation for usage specifics.

  • Once the interface pointer has been successfully acquired, it may be used by the host app until the framework is shut down. If a particular implementation of an interface is needed and multiple are loaded, the default behavior is to acquire the ‘best’ version of the interface. This often comes down to the one with the highest version number.

  • If a specific version of an interface is required (ie: from a specific plugin), the interface could be acquired either using carb::Framework::acquireInterface() directly or using the second template parameter to the carb::getCachedInterface() template to specify the plugin name. This allows a plugin name or a specific version number to be passed in.

    OMNI_CORE_INIT(argc, argv);

    stats = carb::getCachedInterface<carb::stats::IStats>();

    if (stats == nullptr)
    {
        fputs("ERROR-> failed to acquire the IStats interface.\n", stderr);
        return EXIT_FAILURE;
    }
  • Add a config file for the example app. This lets the Carbonite framework know which plugins it should try to find and load automatically and which default configuration options to use. This is specified in the TOML format. The only portion of it that is strictly important is the pluginsLoaded= list. In order for this config file to be picked up automatically, it must have the same base name as the built application target (ie: “example.stats” in this case) with the “.config.toml” extension and must be located in the same directory as the executable file.

# Specify the list of plugins that this example app depends on and should be found and
# loaded by the Carbonite framework during startup.
pluginsLoaded = [
    "example.carb.stats.plugin",
]

Building and Running the Example App

Building the example app is as easy as running build.bat (Windows) or build.sh (Linux). Alternatively, it can be built directly from within either VS Code (Linux or Windows) or MSVC (Windows).

The example app builds to the location _build/<platform>/<configuration>/example.stats<exe_extension>. This example app doesn’t require any arguments and can be run in one of many ways:

  • Directly on command line or in a window manager such as Explorer (Windows) or Nautilus (Ubuntu).

  • in MSVC on Windows by setting examples/example.stats as the startup app in the Solution Explorer window (right click on the project name and choose “Set As Startup Project”), then running (“Debug” menu -> “Start Debugging” or “Debug” menu -> “Start Without Debugging”).

  • Under VS Code on either Windows or Linux by selecting the example.stats launch profile for your current system.

Running the example app under the debugger and stepping through some of the code may be the most instructional way to figure out how different parts of the app and the example plugin work.

Interface Design Considerations

A keen observer may have noticed some weaknesses in the design of the carb::stats::IStats API. These could be overcome with some redesign of the interface or at least some more care in the implementation of the interface. Some things that could use improvements:

  • Only ABI safe types may be used in the arguments to Carbonite interface functions. These rules should be followed as a list of what types are and are not allowed:

    • The language primitives (ie: int, long, float, char, etc.) and pointers or references to the primitive types are always allowed.

    • Locally defined structs, classes and enums, pointers or references to locally defined types, or enums are always allowed. Structs and classes must contain only ABI-safe types and use standard layout (std::is_standard_layout<>::value must be true).

    • Carbonite types which declare themselves ABI-safe such as omni::string and carb::RString are always allowed.

    • Variadic arguments are allowed, but only ABI-safe types must be passed through them.

    • C++ class objects should never be passed into an interface function. They may be returned from one as an opaque pointer as long as the only way to operate on it is for the caller to pass it back into another interface function.

    • No Carbonite class types should be passed into or returned from interface functions except for the ones that are explicitly marked as being ABI safe such as omni::string or carb::RString.

    • If an ABI unsafe type (ie: STL container) is to be used, its use must be entirely restricted to inlined code in the header. The object itself (or anything containing it) should never be passed into or returned from an interface function.

    No Carbonite interface function or any struct passed to it should ever use an ABI unsafe type such as an STL object, or anything from a third party library. Any types or enums from third party libraries should be abstracted out with locally defined Carbonite types.

  • Instead of using the carb::stats::Value struct, a carb::variant::Variant could be used. This would allow for a wider variety of data types and give an easier way to specify both the data’s type and its value.

  • The carb::stats::IStats::getValue function has some thread safety (and general usability) concerns. The name and description strings it returns in the output parameter could be invalidated at any time if another thread (or even the same calling thread later on) removes that particular statistic from the table while it’s still being used. This puts an undue requirement for managing all access to the statistics table on the host app. This could be improved in one of the following ways:

    • Change the strings to be omni::string objects instead. This would allow for an ABI safe way of both retrieving and storing the strings for the host app. One consideration would be that the memory may need to be allocated and the strings copied on each access.

    • Change the object stored internally in the table to be reference counted. This would prevent the strings from being de-allocated before any caller was done with accessing it. The down side would be that the caller would then be required to release the value object each time it was finished with it.

    • Change the string storage to be in a table that persists for the lifetime of the plugin (or use carb::RString which essentially does the same thing), but at process scope. This would make the retrieved strings safe to access at any point, but could also lead to the table growing in an unmaintainable way with certain usage patterns.

  • If an interface function needs to return a state object to be operated on by other interface functions, it must do so by using an opaque pointer to the object. The caller may not be allowed to directly inspect or operate on the returned object. Only other functions in the same interface or another cooperative interface in the same plugin may know how to access and manipulate the object. If two plugins both offer the same interface, it is undefined behavior to pass a state object created by one plugin’s interface to the other plugin’s interface, unless this is explicitly allowed. Likewise, when a plugin supports multiple versions of an interface it is undefined behavior to pass a state object created by one version of the interface to a different version of that same interface, unless explicitly allowed.

Documentation on an interface is always very important. This is how other developers will discover and figure out how to use your shiny new interface. All public headers should be fully documented:

  • The ideal documentation is such that any possible questions about a given function or object’s usage has at least been touched on.

  • All function documentation should mention any thread safety concerns there may be to calling it, all parameters and return values must be documented.

  • All interfaces should have overview documentation that covers what it does and roughly how it is intended to be used.

  • All API documentation should be in the Doxygen format.

  • No matter how straightforward it is, code _never_ documents itself.

Expanding the Interface Later

Once an interface has been released into the wild and has been used in several places, it becomes more likely that users will request bug fixes, improvements, or changes to the functionality. When doing so, there are a few guidelines to follow to ensure old software doesn’t break if it hasn’t updated to the latest version of the interface’s plugin yet:

  • When adding new functions to the interface, always add to the end of the struct. Software that is still expecting to use an older version of the interface will still work with the newer versions as long as it hasn’t broken the existing ABI at the start of the struct. If a new function is added in the middle of the struct or an existing function is removed, all functions in the interface will be shifted up or down in the v-table of any software expecting to an older version of the interface.

  • Never change parameters or return values on any existing functions in a released interface when making changes to it. This could cause code build for an older version of the interface to behave erratically or incorrectly when calling into those interface functions. Instead, add a new version of the function with the different parameters or return values at the end of the interface struct.

  • When making any change to an interface, its version numbers should be bumped. A minor change that doesn’t break backward compatibility with older versions should simply increment the minor version number. If a change needs to occur that may break older software including deprecating some functions, the major version should be incremented and the minor version reset to zero. If this is the case, consider continuing to support the old version in template<class T>bool fillInterface(carb::Version*, void*).