Extending an Omniverse Native Interface

Overview

Once released, an Omniverse Native Interface’s ABI may not be changed. This guarantees that any library or plugin that was dependent on a previous version of the interface will always be able to access it even if newer versions of the interface become available later. The implementation of an interface may change, but the interface’s ABI layer itself may not change. A change to an ABI may for instance mean adding a new function, changing the prototype of an existing function, or removing an existing function. None of these may occur on a released version of the interface since that would break released apps that make use of the interface.

If additional functionality is needed in an ONI interface, a new version of the interface can still be added. The new interface may either inherit from the previous version(s) or may be entirely standalone if needed. In cases where it is possible, it is always preferrable to have the new version of the interface inherit from the previous version.

Note that it is possible to add new enum or flag values to an existing interface’s header without breaking the ABI. However, care must still be taken when doing that to ensure that the behavior added by the new flags or enums is both backward and forward compatible and safe. For example, if an older version of the plugin is loaded, will it either fail gracefully or safely ignore any new flag/enum values passed in. Similarly, if a newer version of the plugin is loaded in an app expecting an older version, will the expected behavior still be supported without the new enums or flags being passed in. In general though, adding new flags or enums without adding a new interface version as well should be avoided unless it can be absolutely guaranteed to be safe in all cases.

The process for adding a new version of an ONI interface will be described below. This will extend the example interfaces used in Creating a New Omniverse Native Interface and assumes that document has already been read. This assumes the reader’s familiarity with and extends the examples presented there. More information on ONI can be found in Omniverse Native Interfaces.

If further examples of extending a real ONI interface are needed, omni::platforminfo::IOsInfo2_abi (in include/omni/platforminfo/IOsInfo2.h) or omni::structuredlog::IStructuredLogSettings2_abi (in include/omni/structuredlog/IStructuredLogSettings2.h) may be used for reference. In both these cases, the new interface inherits from the previous one.

Defining the New Interface Version

The main difference between the previous version of an interface and its new version are that should inherit from the previous ABI interface class instead of omni::core::IObject. The new interface version must also be declared in its own new C++ header file. The new interface version must also have a different name than the previous version. Adding a version number to the new interface version’s name is generally suggested.

It is always suggested that the implementation of the new version(s) of the interface be added to the same C++ plugin as the previous version(s). This reduces code duplication, internal dependencies, and allows all versions of an interface to be present in a single location.

To extend our ILunch interface to be able to ask if the user would like salad with lunch, a new version of the interface could be added to the C++ header include/omni/meals/ILunch2.h as follows:

// file 'include/omni/meals/ILunch2.h'
#pragma once

#include "ILunch.h"

namespace omni
{
namespace meals
{

enum class OMNI_ATTR("prefix=e") Dressing
{
    eNone,
    eRaspberryVinaigrette,
    eBalsamicVinaigrette,
    eCaesar,
    eRanch,
    eFrench,
    eRussian,
    eThousandIsland,
};

// we must always forward declare each interface that will be referenced here.
class ILunch2;

// the interface's name must end in '_abi'.
class ILunch2_abi : public omni::core::Inherits<omni::meals::ILunch, OMNI_TYPE_ID("omni.meals.lunch2")>
{
protected:  // all ABI functions must always be 'protected' and must end in '_abi'.
    virtual void addGardenSalad_abi(Dressing dressing) noexcept = 0;
    virtual void addWedgeSalad_abi(Dressing dressing) noexcept = 0;
    virtual void addColeSlaw_abi() noexcept = 0;
};

} // namespace meals
} // namespace omni

// include the generated header and declare the API interface.  Note that this must be
// done at the global scope.
#define OMNI_BIND_INCLUDE_INTERFACE_DECL
#include "ILunch2.gen.h"

// this is the API version of the interface that code will call into.  Custom members and
// helpers may also be added to this interface API as needed, but this API object may not
// hold any additional data members.
 class omni::meals::ILunch2 : public omni::core::Generated<omni::meals::ILunch2_abi>
{
};

#define OMNI_BIND_INCLUDE_INTERFACE_IMPL
#include "ILunch2.gen.h"

Once created, this new header also needs to be added to the omnibind call in the premake script. This should be added to the same interface generator project that previous versions used:

project "omni.meals.interfaces"
    location (workspaceDir.."/%{prj.name}")
    omnibind {
        { file="include/omni/meals/IBreakfast.h", api="include/omni/meals/IBreakfast.gen.h", py="source/bindings/python/omni.meals/PyIBreakfast.gen.h" },
        { file="include/omni/meals/ILunch.h", api="include/omni/meals/ILunch.gen.h", py="source/bindings/python/omni.meals/PyILunch.gen.h" },
        { file="include/omni/meals/IDinner.h", api="include/omni/meals/IDinner.gen.h", py="source/bindings/python/omni.meals/PyIDinner.gen.h" },

        -- new header(s) added here:
        { file="include/omni/meals/ILunch2.h", api="include/omni/meals/ILunch2.gen.h", py="source/bindings/python/omni.meals/PyILunch2.gen.h" },
    }
    dependson { "omni.core.interfaces" }

Building the interface generator project should then result in the new header files being generated.

Adding the New Python Bindings

The new interface’s python bindings would be added to the python binding project just as they were before. This would simply require including the new generated header and calling the new generated inlined helper function. Note that the above header file will now generate two inlined helper functions in the bindings header. One helper function will add python bindings for the new version of the interface and one will add python bindings for the Dressing enum.

#include <omni/python/PyBind.h>

#include <omni/meals/IBreakfast.h>
#include <omni/meals/ILunch.h>
#include <omni/meals/ILunch2.h>     // <-- include the new API header file.
#include <omni/meals/IDinner.h>

#include "PyIBreakfast.gen.h"
#include "PyILunch.gen.h"
#include "PyILunch2.gen.h"      // <-- include the new generated bindings header file.
#include "PyIDinner.gen.h"

OMNI_PYTHON_GLOBALS("omni.meals-pyd", "Python bindings for omni.meals.")

PYBIND11_MODULE(_meals, m)
{
    bindIBreakfast(m);
    bindILunch(m);
    bindIDinner(m);

    // call the new generated inlined helper functions.
    bindILunch2(m);
    bindDressing(m);
}

Implementing the New Interface

In most cases, implementing the new version of the interface is as simple as changing the implementation object from inheriting from the previous version API (ie: omni::meals::ILunch in this case) to inheriting from the new version (ie: omni::meals::ILunch2) instead, then adding the implementations of the new methods. If the new interface version does not inherit from the previous version, this can still be handled through inheritence in the implementation, but appropriate casting must occur when returning the new version’s object from the creator function.

Once the new version’s implementation is complete, a new entry needs to be added to the plugin’s interface implementation listing object. This object is retrieved by the type factory from the plugin’s onLoad() function. To add the new interface, the following simple changes would need to be made:

omni::core::Result onLoad(const omni::core::InterfaceImplementation** out, uint32_t* outCount)
{
    // clang-format off
    static const char* breakfastInterfaces[] = { "omni.meals.IBreakfast" };
    static const char* lunchInterfaces[] = { "omni.meals.ILunch", "omni.meals.ILunch2" }; // <-- add new interface name.
    static const char* dinnerInterfaces[] = { "omni.meals.IDinner" };
    static omni::core::InterfaceImplementation impls[] =
    {
        {
            "omni.meals.breakfast",
            []() { return static_cast<omni::core::IObject*>(new Breakfast); },
            1, // version
            breakfastInterfaces, CARB_COUNTOF32(breakfastInterfaces)
        },
        {
            "omni.meals.lunch",

            // switch this to create the new interface version's object instead.  Callers can then
            // cast between the new and old interface versions as needed.
            []() { return static_cast<omni::core::IObject*>(new Lunch2); },
            1, // version
            lunchInterfaces, CARB_COUNTOF32(lunchInterfaces)
        },
        {
            "omni.meals.dinner",
            []() { return static_cast<omni::core::IObject*>(new Dinner); },
            1, // version
            dinnerInterfaces, CARB_COUNTOF32(dinnerInterfaces)
        },
    };
    // clang-format on

    *out = impls;
    *outCount = CARB_COUNTOF32(impls);

    return omni::core::kResultSuccess;
}

Note that the structure of this interface implementation listing object can differ depending on how the implementation class is structured. For example, if all interfaces in the plugin are implemented as a single class internally where the class inherits from all interfaces in its omni::core::Implements invocation, only a single entry would be needed for the omni::core::InterfaceImplementation listing. This case would look similar to this:

omni::core::Result onLoad(const omni::core::InterfaceImplementation** out, uint32_t* outCount)
{
    // clang-format off
    static const char* interfacesImplemented[] = { "omni.meals.ITable",
                                                   "omni.meals.IWaiter",
                                                   "omni.meals.IKitchenStaff" };
    static omni::core::InterfaceImplementation impls[] =
    {
        {
            "omni.meals.IRestaurant",
            []() -> omni::core::IObject* {
                omni::meals::Restaurant* obj = new omni::meals::Restaurant::getInstance();

                // cast to `omni::core::IObject` before return to ensure a good base object is given.
                return static_cast<omni::core::IObject*>(obj->cast(omni::core::IObject::kTypeId));
            },
            1, // version
            interfacesImplemented, CARB_COUNTOF32(interfacesImplemented)
        },
    };
    // clang-format on

    *out = impls;
    *outCount = CARB_COUNTOF32(impls);

    return omni::core::kResultSuccess;
}

When the caller receives this object from omni::core::createType(), it will then be able to use omni::core::IObject::cast() to convert the returned object to the interface it needs instead of having to explicitly create each interface object provided through the plugin using multiple calls to omni::core::createType(). This typically ends up being a better user experience for developers.