Creating a New Omniverse Interface

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 interfaes 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"
    kind "StaticLib"
    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" }

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 specifc 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.

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.
OMNI_DECLARE_INTERFACE(ILunch);

class ILunch_abi : public omni::core::Inherits<omni::core::IObject, OMNI_TYPE_ID("omni.meals.ILunch")>
{
protected:  // all ABI functions must always be 'protected'.
    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

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.

Note above we intentionally are not including the generated header yet. This is simply because it doesn’t exist yet. If we try to include it now, the code generation step with omni.bind would simply fail with a fatal ‘missing include file’ error. We could shortcut that and just create a dummy empty header in the expected location however.

Generating Code

After adding the new projects and the C++ interface declation 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. This full build can be shortcutted by simply prebuilding the tree then building only the omni.meals.interfaces project in MSVC or VSCode.

  2. add a #include line for the generated C++ header to the end of each interface header. Note that this include line must be at the global namespace scope.

  3. 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 "PyIMeals.gen.h"

#include <stdexcept>

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.

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 can exist in the same file.

Shared Internal Header File

Since multiple source files 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();
    // ... class declaration 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 makeToast();
    bool m_withToast;
};

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

} // namespace meals
} // namespace omni

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", "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);

    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:

  • "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, 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 inteface 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.