Creating a New Omniverse Native Interface

Warning

A common misconception is that Omniverse Native Interfaces can be changed over time, but this is not correct. Once created and released, Omniverse Native Interfaces are immutable. To add functionality, an Interface can be inherited into a new Interface that adds functionality, or a new separate Interface can be created. The process of doing this is also described in Extending an Omniverse Native Interface.

Warning

Omniverse Native Interfaces are Beta software and can be difficult to use. Active development by the Carbonite team has paused. If other contributors wish to develop improvements, the Carbonite team is willing to evaluate Merge Requests.

Carbonite Interfaces are actively supported.

Setting up the projects and definitions of a new interface can be a daunting prospect. This will be clarified by following the steps below. Below we will walk through the creation of a set of interfaces in a (pointless) plugin called omni.meals. These don’t do anything particularly useful, but should at least be instructional.

Project Definitions

The first step is to create a new set of projects in a premake file (ie: premake5.lua). There will typically be three new projects added for any new Omniverse interface - the interface generator project, the C++ implementation project for the interface, and the python bindings project for the interface.

Interface Generator Project

The interface generator project definition typically looks along the lines of the listing below. This project is responsible for performing the code generation tasks by running each listed header through omni.bind. The resulting header files will be generated at the listed locations. Any time one of the C++ interface headers is modified, this project will regenerate the other headers automatically. It is important that this project be dependent on omni.core.interfaces and that all other projects for the new set of interfaces be dependent on this project. Note that if python bindings aren’t needed for this interface, the py= parts of each line can be omitted.

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" },
        -- add one more line for each other interface header in the project.
    }
    dependson { "omni.core.interfaces" }

Note that calling the omnibind function will implicitly make your project have the “StaticLib” kind under premake. For this reason it is a good idea to keep the code generation projects separate from other projects that depend on it. However, It is possible to make an omnibind call inside a project that also builds other code (ie: in cases where only one project depends on the interface’s generated code). The only fallout from it will be that the project’s ‘kind’ will have to be reset after the omnibind call(s) if it is not intended to be a static library.

C++ Implementation Project

The C++ implementation project definition looks very similar to any other C++ plugin project in Carbonite. This simply defines the important folders for the plugin’s implementation files, any dependent projects that need to be built first, and any additional platform specific SDKs, includes, build settings, etc. This should look similar to the listing below at its simplest. Initially the project does not need any implementation files. All of the .cpp files will be added later.

project "omni.meals.plugin"
    define_plugin { ifaces = "include/omni/meals", impl = "source/plugins/omni.meals" }
    dependson { "omni.meals.interfaces" }

Python Bindings Project

If needed, the python bindings project defines the location and source files for the generated python bindings. Note that omni.bind does not generate a .cpp that creates and exports the bindings. Instead it generates a header file that contains a set of inlined helper functions that define the bindings. It is the responsibility of the project implementor to call each of those inlined helpers inside a PYBIND11_MODULE(_moduleName, m) block somewhere in the project. This is left up to the implementor so that they can also add extra custom members or values to the bindings if needed. Each generated inline helper function will return a pybind11 class or enum object that can have other symbols, functions, values, or documentation added to them as needed.

project "omni.meals.python"
    define_bindings_python {
        name = "_meals", -- must match the module name in the PYBIND11_MODULE() block.
        folder = "source/bindings/python/omni.meals",
        namespace = "omni/meals"
    }
    dependson { "omni.meals.interfaces" }

Creating the C++ Interface Header(s)

Once the projects have been added, all of the C++ header files that were listed in the omni.meals.interfaces project need to be added to the tree and filled in. The headers that need to be created are specific to your new interface. Continuing with our example here, these headers should be created:

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

#include <omni/core/IObject.h>

namespace omni
{
namespace meals
{

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

// the interface's name must end in '_abi'.
class ILunch_abi : public omni::core::Inherits<omni::core::IObject, OMNI_TYPE_ID("omni.meals.ILunch")>
{
protected:  // all ABI functions must always be 'protected' and must end in '_abi'.
    virtual bool isTime_abi() noexcept = 0;
    virtual void prepare_abi(OMNI_ATTR("c_str, in") const char* dish) noexcept = 0;
    virtual void eat_abi(OMNI_ATTR("c_str, in") const char* dish) 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 "ILunch.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::ILunch : public omni::core::Generated<omni::meals::ILunch_abi>
{
};

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

For the purposes of this example, we’ll assume that the other two headers look the same except that ‘lunch’ is replaced with either ‘breakfast’ or ‘dinner’ (appropriately capitalized too). The actual interfaces themselves are not important here, just the process for getting them created and building. Also note that for brevity in this example, the documentation for each of the ABI functions has been omitted here. All ABI functions must be documented appropriately.

The other two interface headers (include/omni/meals/IBreakfast.h and include/omni/meals/IDinner.h) should be very similar to include/omni/meals/ILunch.h and are left as an exercise for the reader.

Generating Code

After adding the new projects and the C++ interface declaration header(s) to your tree, the initial code generation step needs to be run. Follow these steps:

  1. Run the build once. This will likely fail due to the C++ implementation and python projects not having any source files in them yet. However, it should at least generate the headers. Running the build twice will also work around any errors generated about attempting to include a missing header file. The full build can be shortcut by simply pre-building the tree then building only the omni.meals.interfaces project in MSVC or VSCode.

  2. Verify that all expected header files are appropriately generated in the correct location(s). This includes the C++ headers and the python bindings header.

Adding Python Bindings

For many uses, adding the python bindings is trivial. Simply add a new .cpp file to the python bindings project folder with the same base name as the generated header (ie: PyIMeals.cpp). While this naming is not a strict requirement, it does keep things easy to find.

Implement the C++ source file for the bindings by creating a pybind11 module and calling the generated helper functions in it:

#include <omni/python/PyBind.h>

#include <omni/meals/IBreakfast.h>
#include <omni/meals/ILunch.h>
#include <omni/meals/IDinner.h>

#include "PyIBreakfast.gen.h"
#include "PyILunch.gen.h"
#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);
}

This bindings module should now be able to build on its own and produce the appropriate bindings. At this point there shouldn’t be any link warnings or errors in the python module. The C++ implementation project however will still have some errors due to a missing implementation.

Each of the generated inline helper functions will return a pybind11 class or enum object that can also be used to add other custom symbols, functions, values, or documentation to if needed.

Adding a C++ Implementation

The C++ implementation project is the last one to fill in. This requires a few files - a module startup implementation, a header to share common internal declarations, and at least one implementation file for each interface being defined. Note that having these files separated is not a strict requirement, but is good practice in general. If needed, both the interface implementation and the module startup code could exist in a single file.

Shared Internal Header File

Since multiple source files in this example will likely be referring to the same set of classes and types, they must be declared in a common internal header file. Even if each implementation source file were to be completely self contained, there would still need to be at the very least a creator helper function that can be referenced from the module startup source file.

// file 'source/plugins/omni.meals/Meals.h'.
#pragma once

#include <omni/core/Omni.h>

#include <omni/meals/IBreakfast.h>
#include <omni/meals/ILunch.h>
#include <omni/meals/IDinner.h>

namespace omni
{
namespace meals
{

class Breakfast : public omni::core::Implements<omni::meals::IBreakfast>
{
public:
    Breakfast();
    ~Breakfast();
    // ... other internal declarations here ...

protected:  // all ABI functions must always be overridden.
    bool isTime_abi() noexcept override;
    void prepare_abi(const char* dish) noexcept override;
    void eat_abi(const char* dish) noexcept override;

private:
    bool _needToast();

    bool m_withToast;
};

// ... repeat for other internal implementation class declarations here ...

} // namespace meals
} // namespace omni

In the above example, each class is implemented separately internally. This is perfectly acceptable. However, all related objects may also be implemented with a single internal class if it makes logical sense or saves on code duplication. In this case, merging the implementations into a single class does not work since all of the interfaces need to implement the same three methods. However, if all the interfaces being implemented have mutually exclusive function names, a single class that simply inherits from all of the interfaces could be used. The only modification to the above example would be to add multiple API class names to the omni::core::Implements invocation in the class declaration.

Module Startup Source

The task of the module startup source file is to define startup and shutdown helper functions, define any additional callbacks such as ‘on started’ and ‘can unload’, and define the module exports table. These can be done with code along these lines:

// file 'source/plugins/omni.meals/Interfaces.cpp'.
#include "Meals.h"

OMNI_MODULE_GLOBALS("omni.meals.plugin", "plain text brief omni.meals plugin description");

namespace   // anonymous namespace to avoid unnecessary exports.
{

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" };
    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",
            []() { return static_cast<omni::core::IObject*>(new Lunch); },
            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;
}

void onStarted()
{
    // ... do necessary one-time startup tasks ...
}

bool onCanUnload()
{
    // ... return true if unloading the module is safe ...
}

void onUnload()
{
    // ... do necessary one-time shutdown tasks ...
}

};

OMNI_MODULE_API omni::core::Result omniModuleGetExports(omni::core::ModuleExports* out)
{
    OMNI_MODULE_SET_EXPORTS(out);
    OMNI_MODULE_ON_MODULE_LOAD(out, onLoad);
    OMNI_MODULE_ON_MODULE_STARTED(out, onStarted);
    OMNI_MODULE_ON_MODULE_CAN_UNLOAD(out, onCanUnload);
    OMNI_MODULE_ON_MODULE_UNLOAD(out, onUnload);

    // the following two lines are needed for Carbonite interface interop.  This includes any implicit use of
    // other Carbonite interfaces such as logging, assertions, or acquiring other interfaces.  If no Carbonite
    // interface functionality is used, these can be omitted.
    OMNI_MODULE_REQUIRE_CARB_CLIENT_NAME(out);
    OMNI_MODULE_REQUIRE_CARB_FRAMEWORK(out);

    return omni::core::kResultSuccess;
}

C++ Interface Implementation Files

All that remains is to add the actual implementation file(s) for your new interface(s). These should not export anything, but should just provide the required functionality for the external ABI. The details of the implementation will be left as an exercise here since they are specific to the particular interface being defined.

Loading an Omniverse Interface Module

An Omniverse interface module can be loaded as any other Carbonite plugin would be loaded. This includes searching for and loading wildcard modules during framework startup, or loading a specific library directly. Once loaded, the interfaces offered in the library should be registered with the core type factory automatically. The various objects offered in the library can then be created using the omni::core::createType<>() helper template.

Troubleshooting

When creating a new Omniverse interface and all of its related projects, there are some common problems that can come up. This section looks to address some of those potential problems:

  • "warning G041F212F: class template specialization of 'Generated' not in a namespace enclosing 'core' is a Microsoft extension [-Wmicrosoft-template]": this warning on MSVC is caused by including the generated C++ header from inside the namespace of the object(s) being declared. The generated header should always be included from the global namespace scope level.

  • when moving an interface declaration from one header to another or renaming a header, omni.bind will error out on the first build because it wants to process the previous API implementation first to check for changes. It will report that the deleted interface no longer exists. However, in this case the new header will still be generated correctly and the rest of the project will still build successfully. Running the build again, including the omni.bind step, will succeed. Since an existing ABI should never be removed, the only legitimate use case for this situation is in changes during initial development or in splitting up a header that originally declared multiple interfaces.

  • structs that are passed into omni interface methods as parameters may not have any members with default initializers. If a member of the struct has a default initializer, omni.bind will give an error stating that the use of the struct is not ABI safe. This is because the struct no longer has a trivial layout when it has a default initializer and is therefore not ABI safe.