Omniverse Native Interfaces in Extensions

Overview

This document outlines how Omniverse Native Interfaces can be used by the Extension System.

Omniverse Native Interfaces

Omniverse Native Interfaces (ONI) are a way to specify well defined, immutable interfaces to compiled binary chunks of code. This specification allows multiple implementations of each interface, implementations to be generated by different compiler tool chains, and the resulting shared objects to be used across multiple SDK releases.

In short, ONI defines a backwards compatible way for compiled binary chunks of code to talk to each other.

At ONI’s core is the specification of each interface’s Application Binary Interface (ABI). ABI’s in ONI are specified in a limited subset of C++, effectively defining an Interface Definition Language (IDL). These IDL files are processed by the omni.bind tool, generating a modern-C++ wrapper to the ABI (known as the API) and efficient bindings to various other languages (e.g. Python).

Additional information about ONI can be found at Omniverse Native Interfaces.

Extension System

The extension system is essentially a package manager and loader. While ONI primarily concerns itself with defining a way to reliably access compiled chunks of code, the extension system’s job is to package code, settings, documents, and tests into a coherent package. The extension system bundles:

  • Package description

  • Configuration settings

  • Compiled native code (e.g. C++)

  • Interpreted code (e.g. Python)

  • Dependency information (e.g. extension A depends on extension B)

  • Documentation

  • Tests

  • Versioning information

This document outlines:

  • How to build compiled ONI interfaces within an extension

  • How to build compiled ONI language bindings within an extension

More in-depth information about the extension system, such as packaging Python scripts and writing config files, can be found Extensions System.

Defining an Interface

Interfaces are specified in the extensions include/ directory and should follow the namespacing of the interface. For example, the interface omni::example::greet::IGreetInterface.h should be in the directory include/omni/example/greet/IGreetInterface.h.

Looking at IGreetSubject.h, we see the ABI for the IGreetSubject interface:

// Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
//
// NVIDIA CORPORATION and its licensors retain all intellectual property
// and proprietary rights in and to this software, related documentation
// and any modifications thereto.  Any use, reproduction, disclosure or
// distribution of this software and related documentation without an express
// license agreement from NVIDIA CORPORATION is strictly prohibited.
//
#pragma once

#include <omni/core/IObject.h>

namespace omni
{
namespace example
{
namespace greet
{

OMNI_DECLARE_INTERFACE(IGreetSubject);

//! Greets a given subject (e.g. "Hello Anton!")
class IGreetSubject_abi: public omni::core::Inherits<omni::core::IObject, OMNI_TYPE_ID("omni.example.greet.IGreetSubject")>
{
protected:
    //! Prints a greeting to stdout.
    virtual void greet_abi() noexcept = 0;

    // example-begin OMNI_ATTR
    //! Returns the current subject.
    //!
    //! Returned memory is valid until the next call to setSubject()
    //!
    //! Not thread safe.
    virtual OMNI_ATTR("c_str, not_null") const char* getSubject_abi() noexcept = 0;
    // example-end OMNI_ATTR

    //! Sets the subject.
    //!
    //! subject's memory is copied.
    //!
    //! Not thread safe.
    virtual void setSubject_abi(OMNI_ATTR("c_str, not_null") const char* subject) noexcept = 0;
};

} // namespace greet
} // namespace example
} // namespace oni

#include "IGreetSubject.gen.h"

The implementation of the interface can be found in plugins/omni.example.greet/GreetOniPlugin.cpp. The actual filename doesn’t matter. Here’s the implementation:

// This is our implemenation of IGreetSubject.
class GreetSubject: public omni::Implements<omni::example::greet::IGreetSubject>
{
public:
    GreetSubject(): m_subject("Omniverse")
    {
        CARB_LOG_INFO("constructing a GreetSubjectImpl at: 0x%p\n", this);
    }

    ~GreetSubject()
    {
        CARB_LOG_INFO("destructing a GreetSubjectImpl at: 0x%p\n", this);
    }

protected:
    void greet_abi() noexcept override
    {
        printf("Hello %s!\n", m_subject.c_str());
    }

    const char* getSubject_abi() noexcept override
    {
        return m_subject.c_str();
    }

    void setSubject_abi(const char* subject) noexcept override
    {
        CARB_LOG_INFO("changing subject from %s to %s\n", m_subject.c_str(), subject);
        m_subject = subject;
    }

private:
    std::string m_subject;
};

Defining a Plugin

An ONI plugin must have the following macro called once within the shared object:

// Required: Define globals needed by each plugin.
OMNI_MODULE_GLOBALS("omni.example.greet.plugin", "Example ONI Hello World plugin.");

The first argument to the macro is the name of default logging channel for the module while the second argument is the channel’s description.

When loading a plugin, omni::core::ITypeFactory looks for an exported function named omniModuleGetExports. To ensure the module exports said function, OMNI_MODULE_API is used to decorate the function:

// Required: This is the main entry point into the plugin.  The purpose of this function is for the plugin to tell the
// type system (omni::core::ITypeFactory) about itself and also convey requirements (e.g. I require Carbonite logging).
// See omni/core/ModuleExports.h for more docs.
OMNI_MODULE_API omni::Result omniModuleGetExports(omni::ModuleExports* out)
{
    // example-begin omniModuleGetExports-required
    // Required: Macro to setup a bunch of pointers defined by OMNI_MODULE_GLOBALS().  Treat this as opaque.
    OMNI_MODULE_SET_EXPORTS(out);

    // Required: The onLoad function tells the type system about this plugin's types.
    OMNI_MODULE_ON_MODULE_LOAD(out, onLoad);
    // example-end omniModuleGetExports-required

    // example-begin omniModuleGetExports-carb
    // Optional: The OMNI_MODULE_REQUIRE_* macros will fail the module load if the given subsystem is not present.  Use
    // this macro to require that a subsystem be present.
    //
    // Since this example plugin uses CARB_LOG_*, we require the ILogging interface.
    OMNI_MODULE_REQUIRE_CARB_ILOGGING(out); // ensure CARB_LOG_ works
    // example-end omniModuleGetExports-carb

    return omni::kResultSuccess;
}

Within omniModuleGetExports the following code is necessary:

// Required: Macro to setup a bunch of pointers defined by OMNI_MODULE_GLOBALS().  Treat this as opaque.
OMNI_MODULE_SET_EXPORTS(out);

// Required: The onLoad function tells the type system about this plugin's types.
OMNI_MODULE_ON_MODULE_LOAD(out, onLoad);

The rest of the code in the function is dedicated to enabling Carbonite’s legacy C-Interfaces, which we’ll cover in Using C-Style Carbonite Interfaces in ONI Plugins).

Above, OMNI_MODULE_ON_MODULE_LOAD points to a function that omni::core::ITypeFactory will call to gather type information from the module. Our module exposes two implementations, one for IGreetSubject and the another for IExtensionHooks. We’ll cover IExtensionHooks later. For now, consider the following type export declaration:

{
    "omni.example.greet.IGreetSubject-greet", // implementation type id
    []() { // creation function
        return static_cast<omni::IObject*>(new GreetSubject);
    },
    1, // implementation version
    interfacesImplementedGreetSubject,CARB_COUNTOF32(interfacesImplementedGreetSubject)
},

The name of the implementation is “omni.example.greet.IGreetSubject-greet”. Wordy, but it’s best that this name is unique so that users can explicitly instantiate this implementation (this is a rare need):

// it's rare to instantiate a specific implementation (shown here just for clarity)
auto greet = omni::core::createType<IGreetSubject>(OMNI_TYPE_ID("omni.example.greet.IGreetSubject-greet"));

Next is the function invoked to instantiate the type. Note, an omni::core::IObject must be returned.

After the instantiation function is the version number of the implementation. The version number allows multiple versions of an implementation to exist within a process. By default, omni::core::createType will instantiate the latest version of an implementation; though this behavior can be changed.

Finally, we have the array of interfaces implemented by the implementation. Here, it’s only “omni.example.greet.IGreetSubject” but more than one interface could be implemented via multiple inheritance. In effect, this array tells the type system, “Whenever a user requests an omni::example::greet::IGreetSubject, instantiating an GreetSubject is appropriate.”

You may wonder what happens when there are multiple implementations of an interface and how does omni::core::ITypeFactory resolve which implementation to instantiate? There are rules, and they’re a bit much to cover here. See omni::core::ITypeFactoy’s documentation for detailed information.

Using C-Style Carbonite Interfaces in ONI Plugins

If your plugin requires a Carbonite interface, you can specify this requirement with OMNI_MODULE_REQUIRE_CARB_*.

// Optional: The OMNI_MODULE_REQUIRE_* macros will fail the module load if the given subsystem is not present.  Use
// this macro to require that a subsystem be present.
//
// Since this example plugin uses CARB_LOG_*, we require the ILogging interface.
OMNI_MODULE_REQUIRE_CARB_ILOGGING(out); // ensure CARB_LOG_ works

Above, we call OMNI_MODULE_REQUIRE_CARB_ILOGGING() to ensure that the underlying Carbonite framework supports the carb::logging::ILogging interface. Said differently, we want to require that we’re able to call the CARB_LOG_* macros. If that requirement can’t be met, the plugin will fail to load.

We can also require access carb::Framework via OMNI_MODULE_REQUIRE_CARB_FRAMEWORK(). You can see that we’re able to access carb::Framework in ExtensionHooks::onStartup():

auto fs = carb::getFramework()->tryAcquireInterface<carb::filesystem::IFileSystem>();
if (fs)
{
    printf("[ext: %s] App directory: %s\n", ext->getId(), fs->getAppDirectoryPath());
}

See ../../../../_build/target-deps/carb_sdk_plugins/include/omni/core/ModuleExports.h for more OMNI_MODULE_REQUIRE_CARB_* macros.

Ensure your calls to OMNI_MODULE_REQUIRE_CARB_* are minimal. As we phase out Carbonite’s C-style interfaces, we’ll create interposer interfaces to emulate old C-style interfaces. This is easy for interfaces like carb::logging::ILogging but hard for monolithic interfaces like carb::Framework.

Defining Python Bindings

Python bindings are chunks of C++ code that expose functions and methods to the Python interpreter. Said differently, a binding allows you to call C++ code from within Python as if the code was defined in Python. Using omni.bind, this chunk of code can be largely generated for you.

To define bindings, the developer must create a PyModule that defines the bindings. For our omni.example.greet module, this is done in python/omni.example.greet/PyGreetModule.cpp (we’ll see later the name of the .cpp file doesn’t matter):

// Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
//
// NVIDIA CORPORATION and its licensors retain all intellectual property
// and proprietary rights in and to this software, related documentation
// and any modifications thereto.  Any use, reproduction, disclosure or
// distribution of this software and related documentation without an express
// license agreement from NVIDIA CORPORATION is strictly prohibited.
//

// Suggested: Sends binding log messages/errors to a dedicated logging channel.
#define OMNI_LOG_CHANNEL "example.omni.greet-pyd"

#include <omni/core/Omni.h>
#include <omni/python/PyBind.h>

#include <omni/example/greet/IGreetSubject.h>

// example-begin omni.bind-include
#include "PyIGreetSubject.gen.h" // generated file
// example-end

// example-begin OMNI_PYTHON_GLOBALS
// Required: Sets up logging channel information.
OMNI_PYTHON_GLOBALS("omni.example.greet-pyd", "Python bindings for omni::example::greet");
// example-end

// example-begin pymodule
// Required: This macro defines the main entry-point into the shared object.
PYBIND11_MODULE(_greet, m)
{
    // This function is defined by PyIGreetSubject.gen.h and was generated by omni.bind.
    //
    // It's up to the dev to make sure that each "binding" function in generated headers are called.
    bindIGreetSubject(m);
}
// example-end

You must include a call OMNI_PYTHON_GLOBALS:

// Required: Sets up logging channel information.
OMNI_PYTHON_GLOBALS("omni.example.greet-pyd", "Python bindings for omni::example::greet");

Likewise, you must include a call to PYBIND11_MODULE:

// Required: This macro defines the main entry-point into the shared object.
PYBIND11_MODULE(_greet, m)
{
    // This function is defined by PyIGreetSubject.gen.h and was generated by omni.bind.
    //
    // It's up to the dev to make sure that each "binding" function in generated headers are called.
    bindIGreetSubject(m);
}

Note the first argument to PYBIND11_MODULE: _greet. This is the last token in the Python module’s name: omni.example.greet. Though weird, the underscore is suggested. Don’t worry, it will mostly be hidden from the user in __init__.py.

The second thing to note in PYBIND11_MODULE is the call to bindIGreetSubject(m). This is what actually tells the Python interpreter about the bindings. The neat thing is that bindIGreetSubject()’s definition was generated by omni.bind by looking at IGreetSubject.h. This definition was included with:

#include "PyIGreetSubject.gen.h" // generated file

Later we’ll see how to tell the build system how to generate this header.

Generating reasonable bindings to Python is hard since Python has a different memory model, threading model, and typing system than C++. omin.bind is an ally here, but needs help via the OMNI_BIND macro. The OMNI_BIND macro helps omni.bind understand the semantics of the data you’re passing around. For example, here we use OMNI_ATTR to tell omni.bind that the return pointer does not point to a single char, but rather a \0-terminated string:

//! Returns the current subject.
//!
//! Returned memory is valid until the next call to setSubject()
//!
//! Not thread safe.
virtual OMNI_ATTR("c_str, not_null") const char* getSubject_abi() noexcept = 0;

See omni.bind User Guide for an in-depth guide on omni.bind and OMNI_ATTR.

Finally, you must define an __init__.py:

import omni.core
from ._greet import *


def test():
    print("I'm Python code.")
    g = IGreetSubject()
    g.greet()
    g.subject = "Omnivores"
    g.greet()
    print("Leaving Python.")


test()

The call to import omni.core is a must since it pulls in bindings needed by all other bindings.

If you get errors along the lines of “module not found” ensure that the module name in the import statement matches the module name defined in PYBIND11_MODULE.

Note that the Python bindings look a little different from the C++ API. First, the getSubject and setSubject methods have been replaced with the .subject property. Second, instead of calling omni::core::createType to instantiate a type, you simply call its constructor (e.g. IGreetSubject). In general, the omni.bind’s Python bindings try to make dealing with interfaces more Pythonic.

Building

Building an ONI extension is broken into three parts:

  • Running omni.bind

  • Building bindings

  • Building plugins

All of these steps are defined in the extensions premake5.lua:

-- Use folder name to build extension name and tag.
local ext = get_current_extension_info()
local plugin_name = "omni.example.greet"

project_ext(ext)

-- example-begin interfaces
project_ext_omnibind(ext, plugin_name..".interfaces")
    includedirs { "include" }
    omnibind {
        {
            file = "include/omni/example/greet/IGreetSubject.h",
             api = "include/omni/example/greet/IGreetSubject.gen.h",
              py = "python/omni.example.greet/PyIGreetSubject.gen.h"
        }
    }
-- example-end

-- example-begin bindings
project_ext_bindings {
    ext = ext,
    project_name = plugin_name..".python",
    src = "python/"..plugin_name,
    module = "_greet",
    target_subdir = "omni/example/greet",
    iface_project = plugin_name..".interfaces" -- ensures omni.bind generates code first
}
    includedirs { "include" }
-- example-end

-- example-begin plugin
project_ext_plugin(ext, plugin_name..".plugin")
    add_files("impl", "plugins/"..plugin_name) -- find .cpp files
    includedirs { "include" }
    dependson(plugin_name..".interfaces") -- ensures omni.bind generates code first
-- example-end

Running omni.bind

The first step to building is ensuring that omni.bind runs. omni.bind generates the interface’s API layer and language bindings (e.g. Python bindings).

project_ext_omnibind(ext, plugin_name..".interfaces")
    includedirs { "include" }
    omnibind {
        {
            file = "include/omni/example/greet/IGreetSubject.h",
             api = "include/omni/example/greet/IGreetSubject.gen.h",
              py = "python/omni.example.greet/PyIGreetSubject.gen.h"
        }
    }

The important call is to omnibind, which defines an array of dictionaries where each dictionary defines an interface. We suggest the following patterns:

  • API code (e.g. IGreetSubject.gen.h) lives in the same directory as the ABI code (e.g. IGreetSubject.h)

  • Python bindings live in the Python directory (e.g. python/omni.example.greet/PyIGreetSubject.gen.py)

  • Generated files are checked in.

The last point may seem suspect, but in practice enables a better debugging experience via Source Linking.

Building the Bindings

Build the Python bindings with the following code:

project_ext_bindings {
    ext = ext,
    project_name = plugin_name..".python",
    src = "python/"..plugin_name,
    module = "_greet",
    target_subdir = "omni/example/greet",
    iface_project = plugin_name..".interfaces" -- ensures omni.bind generates code first
}
    includedirs { "include" }

The output of these commands will be a .pyd shared object that contains the C/C++ code needed to tell the Python interpreter about your interfaces.

Be sure to point iface_project to your omni.bind interface project as this ensures the build system will build coded in the proper order.

When loading the module at runtime, if you get errors along the lines of “module not found”, ensure that the module key is set to the same name as defined in PYBIND11_MODULE and __init__.py. In this case _greet.

Building the Plugins

Finally, build the plugins as follows:

project_ext_plugin(ext, plugin_name..".plugin")
    add_files("impl", "plugins/"..plugin_name) -- find .cpp files
    includedirs { "include" }
    dependson(plugin_name..".interfaces") -- ensures omni.bind generates code first

Accelerating the Build

To build:

build

The build can be accelerated by just building either the debug build -d or the release build -r.

build -d

As a first step, the build generates project files. This only has to be done once (or when premake5.lua files are updated). This generation can be skipped after the first build with the -b flag:

build -d -b

We can also tell the build system only to build our plugin with the -t flag:

build -d -b -t extensions\omni.example.greet\omni.example.greet.plugin

This last invocation is significantly faster than running build without any arguments.

Adding Extension Hooks

By default, ONI plugins are standalone chunks of code. They do not know about the extension system. This can be changed by providing an implementation of the omni::ext::IExtensionHooks interface in your plugin:

// Optional: These hooks allow this plugin to understand what the extension manager is doing.
class ExtensionHooks : public omni::Implements<omni::ext::IExtensionHooks>
{
protected:
    void onStartup_abi(omni::ext::IExtensionData* ext) noexcept override
    {
        printf("[ext: %s] Starting greet plugin\n", ext->getId());
        printf("[ext: %s] Extension directory: %s\n", ext->getId(), ext->getDirectory());
        printf("[ext: %s] Plugin directory: %s\n", ext->getId(), omniGetModuleDirectory());

        // example-begin carb-acquire
        auto fs = carb::getFramework()->tryAcquireInterface<carb::filesystem::IFileSystem>();
        if (fs)
        {
            printf("[ext: %s] App directory: %s\n", ext->getId(), fs->getAppDirectoryPath());
        }
        // example-end carb-acquire
    }

    void onShutdown_abi(omni::ext::IExtensionData* ext) noexcept override
    {
        printf("[exit:%s ] Shutting down greet plugin\n", ext->getId());
    }
};

This object will be instantiated for each extension that loads the plugin.

In order for the extension system to see your IExtensionHooks implementation, you must register it via onLoad:

{
    "omni.ext.IExtensionHooks-greet", // implementation type id
    []() { // creation function
        return static_cast<omni::IObject*>(new ExtensionHooks);
    },
    1, // implementation version
    interfacesImplementedExtensionHooks, CARB_COUNTOF32(interfacesImplementedExtensionHooks)
},

Advanced: Supporting Stand-Alone Plugins in Extensions

Outside of the extension system, ONI plugins have an optional startup hook that can be defined via OMNI_MODULE_ON_MODULE_STARTED. This hook is called by omni::core::ITypeFactory after ITypeFactory has made the plugins types available to the rest of the system. This leads to a race condition whereby a type in the plugin may be instantiated by another thread before or while the loading thread calls the module’s “started” function.

omni::core::ITypeFactory’s documentation outlines this scenario and suggests that startup code be thread safe and lazily evaluated. In short, when calling the module’s “startup” function or when creating a type, check if the module’s startup code has been executed, and if not, do so in a thread safe manner.

A similar race happens with omni::ext::IExtensionHooks::onStartup: other threads can instantiate types from the plugin before omni::ext::IExtensionHooks::onStartup is called. Much like the previous race, lazy evaluation of startup code is suggested.

Example Output

Load the plugin by:

  1. Clicking on the Window menu followed by Extensions.

  2. Type “Omniverse Native Extensions” in the search box.

  3. Click the Enable button.

The extension will be loaded and text printed to the console. We first see the extension system trying to load the extension:

[48.038s] [ext: omni.example.greet] startup

Next, we see IExtensionHooks::onStartup being called:

ext: omni.example.greet] Starting greet plugin
[ext: omni.example.greet] Extension directory: c:/users/ncournia/dev/kit/kit/_build/windows-x86_64/debug/exts/omni.example.greet
[ext: omni.example.greet] Plugin directory: c:/users/ncournia/dev/kit/kit/_build/windows-x86_64/debug/exts/omni.example.greet/bin
[ext: omni.example.greet] App directory: c:/users/ncournia/dev/kit/kit/_build/windows-x86_64/debug

Finally, we see our Python module load (e.g. python/omni.example.greet/__init__.py):

I'm Python code.
Hello Omniverse!
Hello Omnivores!
Leaving Python.

If we go back into the extension manager and click the Disable button, we see the extension code’s shutdown code run:

[473.541s] [ext: omni.example.greet] shutdown
[exit:omni.example.greet ] Shutting down greet plugin