Omniverse Interface Bindings Generator

Overview

Omniverse interfaces are defined by a strict subset of C++ which outlines a binary safe way to both pass data and call methods between modules (e.g. DLLs). In practice, this subset of C++ is too limiting for modern development environments.

The Omniverse interface bindings generator, omni.bind, is a code generator that:

  • Generates a modern C++ layer on top of the ABI.

  • Generates Python bindings.

The code output is targeted toward C++ and Python users. Usage of the bindings/wrappers feels like using modern C++/Python and hides ABI details from the user.

In order to achieve this user bliss, the author of the interface must annotate the ABI with hints that the bindings generator can use to generate efficient and safe bindings.

Unlike other interface definition languages, the input to omni.bind is valid C++ code, not another file format such as .idl or .midl files. The author has fine grain control over which parts of the interface are wrapped. The author also has the ability to add “hand-written” bindings to the generated bindings.

Use of omni.bind is optional when authoring Omniverse interfaces.

The main construct used to markup the ABI interface is the OMNI_ATTR macro. Below is an example of an interface’s ABI (i.e. the input to omni.bind). Notice the OMNI_ATTR annotations.

#include <omni/IObject.h>
#include <omni/Types.h>

namespace omni::input
{
    OMNI_DECLARE_INTERFACE(IMouse);
    OMNI_DECLARE_INTERFACE(IMouseOnEventConsumer);

    class IMouse_abi : public Inherits<IObject, OMNI_TYPE_ID("omni.input.IMouse")>
    {
    protected:
        virtual void OMNI_ATTR("consumer=onMouseEvent_abi") addOnEventConsumer_abi(IMouseOnEventConsumer* consumer) noexcept = 0;
        virtual void removeOnEventConsumer_abi(IMouseOnEventConsumer* consumer) noexcept = 0;
        virtual bool isButtonDown_abi(MouseButton button) noexcept = 0;
        virtual Float2 getPosition_abi() noexcept = 0;
        virtual UInt2 getSize_abi() noexcept = 0;
        virtual Result addEvent_abi(OMNI_ATTR("in") const MouseEvent* event) noexcept = 0;
    };

    class IMouseOnEventConsumer_abi : public Inherits<IObject, OMNI_TYPE_ID("omni.input.IMouseOnEventConsumer")>
    {
    protected:
        virtual void onMouseEvent_abi(IMouse* mouse, OMNI_ATTR("in") const MouseEvent* event) noexcept = 0;
    };
}

#include "IMouse.gen.h"

A C++ user would use the interface as follows:

#include <omni/input/IMouse.h>

mouse->addOnEventConsumer(
    [](IMouse* /*mouse*/, const MouseEvent* e)
    {
        if (MouseEventType::eButtonPress == e->type)
        {
            std::cout << "mouse button press: " << unsigned(e->button) << '\n';
        }
    });

mouse->addEvent({
    .type = MouseEventType::eButtonPress,
    .button = MouseEventButton::eLeft,
    .modifiers = fMouseModifiersFlagNone
    });

How to Use This Document

This document is divided by the types of constructs the user is expected to annotate with attributes. Users should look at the construct they are working with and then simply go to that section in the document. Each section will list the attributes available to the construct. The sections are:

Interfaces

All interfaces must be forward declared, at the top of the file in which they are defined, with the OMNI_DECLARE_INTERFACE macro:

OMNI_DECLARE_INTERFACE(IKeyboard);
OMNI_DECLARE_INTERFACE(IKeyboardOnEventConsumer);

For each forward declaration of an interface, an ABI should be defined. The name of the interface’s ABI should be the same as the name passed to OMNI_DECLARE_INTERFACE but with _abi added to the end:

class IKeyboard_abi : public Inherits<IObject, OMNI_TYPE_ID("omni.input.IKeyboard")>
{
protected:
    // ...

All methods in the interface’s ABI are protected.

Interface types have several attributes that can be applied.

Attribute: no_py

no_py denotes that the interface should not have a Python binding. By default, all interface methods have a generated Python binding.

class OMNI_ATTR("no_py") IObject_abi /* ... */
{
    // ...
};

Methods

Methods in the ABI interface must conform to the following rules:

  • ABI methods must be protected.

  • ABI methods must be postfixed with _abi.

  • ABI methods must be pure virtual.

  • ABI methods must have the noexcept keyword.

Method signatures contain attributes helping the generator to create useful bindings.

Attribute: no_py

no_py denotes that the method should not have a Python binding. By default, all interface methods have a generated Python binding.

virtual OMNI_ATTR("no_py") void myMethod_abi() noexcept = 0;

Attribute: no_api

no_api denotes that the method should not have a C++ API wrapper. By default, all interface methods have a generated C++ API wrapper.

virtual OMNI_ATTR("no_api") void myMethod_abi() noexcept = 0;

Attribute: consumer

consumer denotes that the method is a consumer registration function. A consumer is a callback/user context pair, but in interface form.

The bindings generator will generate code that makes it easier to register language specific constructs as a consumer. For example, the C++ API generator produces code that allows std::function to be treated as a consumer. Likewise, the Python back end will generate bindings allowing for Python functions/lambdas to be treated as consumers.

The consumer attribute accepts the name of the consumer method to call (e.g. consumer=onEvent_abi). The callback method should always be an ABI method (i.e. always end in _abi).

class IKeyboardOnEventConsumer_abi : public Inherits<IObject, OMNI_TYPE_ID("IKeyboardOnEventConsumer")>
{
protected:
    virtual void onKeyboardEvent_abi(IKeyboard* keyboard, OMNI_ATTR("in, not_null") const KeyboardEvent* event) noexcept = 0;
};

class IKeyboard_abi : public Inherits<IObject, OMNI_TYPE_ID("IKeyboard")>
{
protected:
    // addOnEventConsumer_abi is the registration function.  onKeyboardEvent_abi is the callback method on the consumer
    virtual OMNI_ATTR("consumer=onKeyboardEvent_abi") void addOnEventConsumer_abi(IKeyboardOnEventConsumer* cons) noexcept = 0;
};

Attribute: not_prop

not_prop denotes that the method should not be treated as a getter/setter for a property.

By default, the code generator will treat methods prefixed with set, get, or is as getters/setters for an underlying property. This isn’t always desirable.

virtual OMNI_ATTR("not_prop") void setToTime_abi(const Time* t) noexcept = 0;

See also: py_not_prop.

Attribute: py_not_prop

py_not_prop denotes that the method should not be treated as a getter/setter for a property when binding to Python. Other languages may continue to treat the method as a getter/setter for a property.

By default, the code generator will treat methods prefixed with set, get, or is as getters/setters for an underlying property. This isn’t always desirable.

virtual OMNI_ATTR("py_not_prop") void setToTime_abi(const Time* t) noexcept = 0;

See also: not_prop.

Attribute: py_get

py_get denotes that the method, in addition to being treated as a property getter, should also have the raw get method exposed to Python.

Use of this attribute should be rare, as Python properties are preferred over get methods.

This attribute is useful when updating old interfaces to omni.bind, as many older bindings do not take advantage of Python properties.

For example, given the following method:

virtual OMNI_ATTR("py_get") IWindow* getDefaultWindow_abi() noexcept = 0;

the following Python would be generated:

# by default, get methods are treated as property getters (unless 'not_prop' was specified)
w = obj.default_window

# 'py_get' also exposes the "get" method, e.g. get_default_window:
w = obj.get_default_window()

See also: py_set, py_not_prop, not_prop.

Attribute: py_set

py_set denotes that the method, in addition being treated as a property setter, should also have the raw set method exposed to Python.

Use of this attribute should be rare, as Python properties are preferred over set methods.

This attribute is useful when updating old interfaces to omni.bind, as many older bindings do not take advantage of Python properties.

For example, given the following method:

virtual OMNI_ATTR("py_set") void setDefaultWindow_abi(IWindow* w) noexcept = 0;

the following Python would be generated:

# by default, set methods are treated as property setters (unless 'not_prop' was specified)
obj.default_window = win

# 'py_set' also exposes the "set" method, e.g. set_default_window:
obj.set_default_window(win)

See also: py_get, py_not_prop, not_prop.

Attribute: py_name

py_name denotes the name of the method’s desired Python binding name. This can be used to bind an ABI method to a different name in Python.

Use of this attribute should be rare and is only provided to ease the port of existing, incorrect bindings to omni.bind.

For example, omni::kit::IAppWindowFactory::getDefaultWindow() was incorrectly bound to get_default_app_window in the original bindings (notice the addition of _app_). This incorrect binding can be propagated by omni.bind as follows:

virtual OMNI_ATTR("py_name=get_default_app_window") IWindow* getDefaultWindow_abi() noexcept = 0;

the following Python would be generated:

w = obj.get_default_app_window()

Attribute: nodiscard

nodiscard denotes that the return value should not be ignored. When generating API code, CARB_NODISCARD is added to the method signature.

virtual OMNI_ATTR("nodiscard") omni::core::Result execute_abi() noexcept = 0;

Note, the name of the attribute is nodiscard rather than no_discard. The latter form would match the convention used by other attributes that start with no_ but the former form matches the [[nodiscard]] attribute introduced in C++17.

Attribute: not_null

not_null denotes that the return value will never be nullptr.

Currently, this attribute is exposition only. Users are encouraged to use it for the purposes of documentation and the possibility of future features to omni.bind.

virtual OMNI_ATTR("not_null") Foo* getFoo_abi() noexcept = 0;

Attribute: ref

ref denotes that the pointer returned by the ABI method will never be nullptr and should be returned as a reference in the API layer.

The following ABI code:

virtual OMNI_ATTR("ref") Foo* getFoo_abi() noexcept = 0;

would generate the following API code:

Foo& getFoo() noexcept;

Functions

Not yet implemented. ABI functions or inline functions must be manually bound.

Parameters

Annotations on parameters to functions/methods is by far where authors will spend most of their markup effort.

Parameters to an ABI method/function must be one of:

  • Plain-old-data (e.g int, float, etc.)

  • A pointer to a primitive type (e.g. float*, int*)

  • A pointer to an interface

  • A union/struct (see union/struct restrictions below)

  • A pointer to a union/struct (see union/struct restrictions below)

  • A pointer to one of the items above (e.g. a pointer to a pointer).

Each field in a union/struct must follow the rules above. See Union/Struct Fields for more information.

Interface Pointer Parameters

Because interfaces are reference counted by the ABI, interfaces passed as parameters need little to no markup.

All interface pointers are implicitly in, out and as such it is an error to pass an interface by const pointer.

When passing interface pointers, always pass the API version of the pointer, not the ABI version:

virtual void useInterface_abi(IMyInterface_abi* iface) noexcept = 0; //  bad: never pass around _abi pointers
virtual void useInterface_abi(IMyInterface* iface) noexcept = 0;     // good: pass the api pointer

Attribute: not_null

not_null denotes that the given interface pointer will never be nullptr. By default, it is assumed the pointer can be nullptr.

virtual void setFriend_abi(OMNI_ATTR("not_null") IFriend* friend) noexcept = 0;

Attribute: throw_if_null

A parameter with the throw_if_null attribute will cause the API layer to throw an exception if its value evaluates to nullptr.

virtual void setFriend_abi(OMNI_ATTR("throw_if_null") IFriend* friend) noexcept = 0;

// ...

me->setFriend(nullptr);     // will throw an exception

Attribute: no_acquire

By convention, any interface returned from a method, either via a return statement or an interface output pointer, must have omni::core::IObject::acquire() called on it by the method. no_acquire denotes than an interface output pointer (a pointer to an interface pointer) does not have omni::core::IObject::acquire() called on it before the method returns.

virtual omni::core::Result getFriendWithoutAcquire_abi(OMNI_ATTR("not_null, out, *no_acquire") IFriend** friend) noexcept = 0;

In the example above, IFriend** friend is the interface output pointer. In the OMNI_ATTR declaration, note the use of *no_acquire instead of no_acquire. The * is needed to tell omni.bind that it is the pointee of the pointer that will not have acquire() called on it.

When using no_acquire be careful to not place the returned pointer in an ObjectPtr, as ObjectPtr will eventually call an unmatched omni::core::IObject::release().

Use of no_acquire should be rare, as its use goes against convention, forcing the user to think about reference counting. However, no_acquire is useful when all of the following are true:

  • The method is called in a high-performance code path and the overhead of the atomic increment of the reference count degrades performance. This is very rare.

  • The lifetime of the returned object is documented.

  • The method clearly documents the returned pointer does not have acquire() called on it. Appending WithoutAcquire to the method’s name is a great start to such documentation.

Passing Parameters by Value

Primitives and structs passed by value require no markup.

Pointer Parameters

In order for the bindings generator to generate safe code, we must augment each pointer with its semantic usage. At a high-level, the generator needs to know:

  • Does the pointer point to more than one item (i.e. is the pointer really an array).

  • Is the memory read-only, write-only, or read-write.

  • Can the pointer be invalid (e.g. nullptr)?

  • The lifetime of the pointer.

  • Who owns the memory pointed to by the pointer.

All pointers passed to methods must have either the out or in attribute (or both). The one exception to this rule are interface pointers. Interface pointers are always assumed to be in,out.

Attribute: in

in denotes that the function will read the memory pointed to by the pointer. This flag can be combined with out to denote a pointer that is both read and written.

virtual void isKeyDownEvent_abi(OMNI_ATTR("in") const KeyboardEvent* event) noexcept = 0;

Attribute: out

out denotes that the function will write to the memory pointed to by the pointer. This flag can be combined with in to denote a pointer that is both read and written.

virtual void fillEvent_abi(OMNI_ATTR("out") KeyboardEvent* event) noexcept = 0;

Attribute: not_null

not_null denotes that the given pointer will never by nullptr. By default, it is assumed the pointer can be nullptr.

virtual void setName_abi(OMNI_ATTR("c_str, not_null") const char* name) noexcept = 0;

Attribute: count

count denotes that the given pointer points to an array. The value given to count is the name of a variable containing the number of items in the array. By default, the bindings generator assumes the pointer points to a single item.

virtual void sort_abi(OMNI_ATTR("in, out, count=itemCount") int* items, uint32_t itemCount) noexcept = 0;

Attribute: c_str

c_str denotes that the given pointer points to a null-terminated C-style string. By default, the code generator sees const char* pointers as pointing to a single char.

The string is assumed to be read-only (i.e. in) as the bindings generator does not currently support updating a string (i.e. out or in,out).

virtual void setKey_abi(OMNI_ATTR("c_str") const char* key, uint32_t value) noexcept = 0;

Attribute: throw_if_null

A parameter with the throw_if_null attribute will cause the API layer to throw an exception if its value evaluates to nullptr.

virtual void setName_abi(OMNI_ATTR("c_str, in, throw_if_null") const char* name) noexcept = 0;

// ...

me->setName(nullptr);     // will throw an exception

Attribute: ref

References in the ABI are not supported. However, references can be used in the API. The ref attribute is used to denote that a pointer in the ABI should be a reference in the API.

Consider the following ABI:

virtual void setInfo_abi(OMNI_ATTR("not_null, ref") const Info* info) noexcept = 0;

When passing the code above to omni.bind, the output is:

void setInfo(const Info& info) noexcept;

When using ref, not_null must also be used.

Pointers to Pointers as Parameters

Consider the following method signature:

virtual void processPaths_abi(const char** paths, uint32_t pathCount) noexcept = 0;

We want to tell the generator:

  1. paths is an array of strings.

  2. The number of items in the array is described by pathCount.

  3. The array can be nullptr.

  4. None of the strings in the array will ever be nullptr.

We’ve covered previously how to convey the first three attributes. However, we haven’t covered the case of a pointer to pointers. OMNI_ATTR allows the user to denote properties on what a pointer is pointing to with the * syntax:

virtual void processPaths_abi(
    OMNI_ATTR("in, count=pathCount, *c_str, *not_null") const char** paths,
    uint32_t pathCount) noexcept = 0;

As another example, here’s a method that accepts a pointer to a three dimensional array of pointers:

virtual void processPaths_abi(
    OMNI_ATTR("in, count=cols, *in, *not_null *count=rows, **in, **out, **not_null, **count=depth")
    uint8_t*** data, uint32_t cols, uint32_t rows, uint32_t depth) noexcept = 0;

Attribute: py_name

In generated Python, the parameters named are the C++ parameter names converted to snake_case. Specifying a py_name allows for a different value for the keyword.

virtual void loadModels_abi(OMNI_ATTR("c_str, py_name=file") const char* path) noexcept = 0;

Attribute: return

The return attribute can be specified on a method parameter to return the parameter in the API layer. This attribute is often used with throw_result to create methods that denote errors through exceptions. Consider the following:

virtual OMNI_ATTR("throw_result") omni::core::Result getModelAtIndex_abi(
    uint32_t index,
    OMNI_ATTR("not_null, *return") IModel** out) noexcept = 0;

The generated API code is:

omni::core::ObjectPtr<IModel> getModeAtIndex(uint32_t index);

Note, in the ABI code:

  • noexcept has been removed. The API layer will now throw an omni::core::ResultError exception if the ABI layer returns an omni::core::Result error code.

  • The return type is now omni::core::ObjectPtr<IModel> rather than IModel**.

  • *return is used rather than return. return would return the IModel** while *return dereferences the pointer to a pointer to return a IModel* (which is converted to ObjectPtr).

Attribute: default

The default attribute sets default values for parameters of a method.

virtual void myMethod_abi(OMNI_ATTR("default=123") int myArg) noexcept = 0;

In this example, myArg has a default value such that myMethod() is equivalent to myMethod(123).

If used, these must be located at the end of the param list, potentially alongside return or other similar attributes

Return Values

Return by Value

No markup is needed when returning by value (this include primitives and structs).

Returning Pointers

Pointers to memory returned from a method must have clear ownership.

Attribute: owner

owner denotes who owns the memory pointed to by the pointer.

Valid values for this attribute are:

  • this: The memory returned is owned by the interface.

  • caller: The memory returned is owned by the caller.

virtual OMNI_ATTR("owner=this") const char* getTitle_abi() noexcept = 0;

Returning Interface Pointers Without Calling acquire()

By convention, any interface returned from a method must have omni::core::IObject::acquire() called on it by the method. This behavior can be overridden in one of two ways:

  • Appending WithoutAcquire to the method’s name.

  • Adding the no_acquire attribute to the method.

Both methods are covered below followed by thoughts on when to use this feature.

WithoutAcquire

Consider the following ABI:

virtual IFriend* getFriendWithoutAcquire_abi() noexcept = 0;

By appending WithoutAcquire to the methods name, omni.bind knows that the returned pointer will not have acquire() called on it and therefore a raw pointer, rather than an ObjectPtr, should be returned. The resulting API code is:

IFriend* getFriendWithoutAcquire() noexcept;
Attribute: no_acquire

no_acquire denotes that the returned pointer does not have omni::core::IObject::acquire() called on it before the method returns.

virtual OMNI_ATTR("no_acquire") IFriend* getFriend_abi() noexcept = 0;

Again, omni.bind will generate API code that uses raw pointers rather than ObjetPtr:

IFriend* getFriend() noexcept = 0;
Thoughts on Returning Pointers Without Calling acquire()

When using no_acquire or WithoutAcquire, be careful to not place the returned pointer in an ObjectPtr, as ObjectPtr will eventually call an unmatched omni::core::IObject::release().

Use of no_acquire and WithoutAcquire should be rare, as its use goes against convention, forcing the user to think about reference counting. However, no_acquire and WithoutAcquire are useful when all of the following are true:

  • The method is called in a high-performance code path and the overhead of the atomic increment of the reference count degrades performance. This is very rare.

  • The lifetime of the returned object is documented.

Of the two methods, using WithoutAcquire is suggested over using no_acquire, as WithoutAcquire is self-documenting.

Attribute: throw_result

ABI methods are unable to throw exceptions, and must rely on return codes to denote if an error ocurred. Return codes are problematic, in that user often do not check the returned value. Exceptions are one way to force users to handle errors.

throw_result denotes that the returned omni::core::Result return code should be converted to an exeception if the Result denotes an error.

virtual OMNI_ATTR("throw_result") omni::core::Result methodCanFail_abi() noexcept = 0;

The resulting API code generated by omni.bind will be:

void methodCanFail();

Above, note noexcept has been removed. If ABI layers return an error, it will be converted to an omni::core::ResultError exception by the ABI layer.

Furthermore, the return type is now void. See the return attribute to change the return type of a method that uses throw_result.

Unions/Structs

Union and struct definitions appearing in an interface header are available to the binding backends.

Attribute: no_py

no_py denotes that the union/struct should not be accessible in Python.

struct KeyboardEvent OMNI_ATTR("no_py')
{
    uint32_t keycode;
    float time;
};

Attribute: vec

vec denotes that the struct/union should be treated as a math vector type.

For example, given:

union UInt2 OMNI_ATTR("vec")
{
    struct
    {
        union
        {
            OMNI_ATTR("init_arg") uint32_t x;
            uint32_t u;
            uint32_t s;
            uint32_t w;
        };

        union
        {
            OMNI_ATTR("init_arg") uint32_t y;
            uint32_t v;
            uint32_t t;
            uint32_t h;
        };
    };
    OMNI_ATTR("no_py") uint32_t data[2];
};

the Python back end will generate code to treat UInt2 in a more Pythonic manner:

my_obj.position = (2, 3) # sequences can be converted to an UInt2
y = my_object.position[1] # indexing (and slicing) into UInt2

Attribute: opaque

opaque marks the forwardly declared struct/union as opaque, meaning the definition of the struct is private/hidden.

Opaque structs/unions are often used to hide implementation details.

A given struct/union may be forward declared multiple times. However, the opaque attribute should only appear on a single forward declaration (usually in the header defining the interface which uses the opaque struct).

Usage of opaque should be rare when using Omniverse interfaces. However, use of opaque structs is a common pattern found when using Carbonite interfaces.

When passing an opaque pointer as a parameter, neither in nor out should be supplied as an attribute.

For example:

namespace carb
{
    namespace input
    {
        struct OMNI_ATTR("opaque") Gamepad; // opaque should be placed on a single forward declaration of Gamepad
    }
}

Union/Struct Fields

A union or struct field must follow the same rules as Parameters do, with one addition - a field may also be a fixed length character array (ie: a C string).

Each field in a union/struct can have its own attributes. Supported attributes on fields are listed below.

Attribute: no_py

no_py denotes that the field should not be accessible in Python.

struct KeyboardEvent
{
    uint32_t keycode;
    OMNI_ATTR("no_py") char character[4];
};

Attribute: c_str

The c_str attribute denotes that the field should be treated as a C string. The field must be of type char and must be a fixed size array. Fixed size arrays of other types are not allowed since they may lead to data loss due to truncation when passing between C++ and Python. Note that a string field may also be truncated when passing between Python and C++ in this way. However, the string will always be null terminated even if truncated so it will still be valid to use on both sides. If a string is left unterminated on the C++ side, it will be truncated at the size of the array less one character.

For example:

struct Foo
{
    char badString[32];                     // bad: not tagged as 'c_str'.
    char OMNI_ATTR("c_str") goodString[32]; // good: tagged properly, correct type.
    int badArray[32];                       // bad: not a 'char' array and could lead to data loss.
    int OMNI_ATTR("c_str") badArray2[32];   // bad: only 'char' arrays may be tagged as 'c_str'.
};

Attribute: init_arg

init_arg denotes that the field should be exposed as a parameter to the union/structs’s initialization function. This flag is needed only when disambiguating union fields.

Given the following ABI defintition:

union UInt2
{
    union
    {
        OMNI_ATTR("init_arg") uint32_t x;
        uint32_t u;
    };

    union
    {
        OMNI_ATTR("init_arg") uint32_t y;
        uint32_t v;
    };

    float z; // init_arg not need here
};

the Python back end generates code to allow:

pos = UInt2(x=3, y=2, z=1)

Classes

By default classes that are not interfaces will be ignored. However, bind_class can be used to attempt to make the bindings available to backends.

Most attributes that work for Unions/Structs work with classes annotated with bind_class.

Attribute: bind_class

bind_class makes a non-interface class available to backends. By default, non-interface classes are not made available to the binding backends.

Not all fields within the class will be made available, rather, only public data members will be given to the binding backends. Classes must meet the requirements of standard layout.

Most attributes that work for Unions/Structs work with classes annotated with bind_class.

class OMNI_ATTR("bind_class") MyClass
{
public:
    int x;

    MyClass(int x_): x(x_) { } // ignored because not a data member

    int getX() { return x; } // ignored because not a data member
};

class OMNI_ATTR("bind_class") MyOtherClass
{
    MyOtherClass(int x_): x(x_) { } // ignored because not a data member

    int getX() { return x; } // ignored because not a data member

private:
    int x; // ignored because not public
};

Enums

Enums found in an interface header will be available to the generator back ends.

Attribute: prefix

prefix’s value denotes how each value in the enum is prefixed. Binding back ends may choose to elide this prefix. For example, given the following ABI:

enum class OMNI_ATTR("prefix=e") KeyboardEventType : uint32_t
{
    eKeyPress,
    eKeyRelease
};

the Python generator exposes the following:

KeyboardEventType.KEY_PRESS
KeyboardEventType.KEY_RELEASE

Flags

A flag type is a typedef whose values represent bit flags.

Attribute: flag

A typedef (or using) may contain the flag attribute. Any constant with the type will be treated as bit pattern for the flag.

using KeyboardModifierFlags OMNI_ATTR("flag, prefix=fKeyboardModifierFlag") = uint32_t;

constexpr KeyboardModifierFlags fKeyboardModifierFlagShift = 1 << 0; //!< Shift
constexpr KeyboardModifierFlags fKeyboardModifierFlagControl = 1 << 1; //!< Control
constexpr KeyboardModifierFlags fKeyboardModifierFlagAlt = 1 << 2; //!< Alt
constexpr KeyboardModifierFlags fKeyboardModifierFlagSuper = 1 << 3; //!< Super (Windows Key)
constexpr KeyboardModifierFlags fKeyboardModifierFlagCapsLock = 1 << 4; //!< Caps Lock
constexpr KeyboardModifierFlags fKeyboardModifierFlagNumLock = 1 << 5; //!< Num Lock
constexpr uint32_t fKeyboardModifierFlagCount = 6;

Attribute: prefix

prefix’s value denotes how each value in the flag is prefixed. Binding back ends may choose to elide this prefix.

using KeyboardModifierFlags OMNI_ATTR("flag, prefix=fKeyboardModifierFlag") = uint32_t;

constexpr KeyboardModifierFlags fKeyboardModifierFlagShift = 1 << 0;
constexpr KeyboardModifierFlags fKeyboardModifierFlagControl = 1 << 1;
constexpr KeyboardModifierFlags fKeyboardModifierFlagAlt = 1 << 2;
constexpr KeyboardModifierFlags fKeyboardModifierFlagSuper = 1 << 3;
constexpr KeyboardModifierFlags fKeyboardModifierFlagCapsLock = 1 << 4;
constexpr KeyboardModifierFlags fKeyboardModifierFlagNumLock = 1 << 5;
constexpr uint32_t fKeyboardModifierFlagCount = 6;

Constant Groups

A constant group is a collection of related values. Constant groups are identified with a typedef (or using) with the constant attribute.

Attribute: constant

constant denotes the typedef/using as a constant group. Any variable with the given type will be considered a part of the group.

using Result OMNI_ATTR("constant, prefix=kResult") = uint32_t;
constexpr Result kResultSuccess = 0;
constexpr Result kResultNotImplemented = 0x80004001;
constexpr Result kResultOperationAborted = 0x80004004;
constexpr Result kResultFail = 0x80004005;
constexpr Result kResultNotFound = 0x80070002;
constexpr Result kResultInvalidState = 0x80070004;
constexpr Result kResultAccessDenied = 0x80070005;
constexpr Result kResultOutOfMemory = 0x8007000E;
constexpr Result kResultNotSupported = 0x80070032;
constexpr Result kResultInvalidArgument = 0x80070057;
constexpr Result kResultInsufficientBuffer = 0x8007007A;
constexpr Result kResultTryAgain = 0x8007106B;

Attribute: prefix

prefix’s value denotes how each value in the constant group is prefixed. Binding back ends may choose to elide this prefix.

The following ABI code:

using Result OMNI_ATTR("constant, prefix=kResult") = uint32_t;
constexpr Result kResultSuccess = 0;
constexpr Result kResultNotImplemented = 0x80004001;
constexpr Result kResultOperationAborted = 0x80004004;
constexpr Result kResultFail = 0x80004005;
constexpr Result kResultNotFound = 0x80070002;
constexpr Result kResultInvalidState = 0x80070004;
constexpr Result kResultAccessDenied = 0x80070005;
constexpr Result kResultOutOfMemory = 0x8007000E;
constexpr Result kResultNotSupported = 0x80070032;
constexpr Result kResultInvalidArgument = 0x80070057;
constexpr Result kResultInsufficientBuffer = 0x8007007A;
constexpr Result kResultTryAgain = 0x8007106B;

will produce the following Python constants:

Result.SUCCESS
Result.NOT_IMPLEMENTED
Result.OPERATION_ABORTED
# ...

Generator Back End Details

Each bindings generator back end will treat the attribute information differently.

C++ API Back End

For the most part, the C++ back end is the simplest.

Interface Pointers

Interface pointers given to methods or returned by methods are wrapped in a smart pointer. This smart pointer ensures the interface’s internal reference count is managed correctly.

References

in,not_null pointers are converted to const references.

Adding Custom Methods to the API

Custom methods can be added to the generated API wrapper object by declaring inline methods in a class defined by the OMNI_DEFINE_INTERFACE_API macro:

OMNI_DEFINE_INTERFACE_API(omni::windowing::IWindow)
{
public:
    inline ObjectPtr<input::IKeyboardOnEventConsumer> addOnKeyboardEventConsumer(
        std::function<void(input::IKeyboard*, const input::KeyboardEvent*)> callback) noexcept
    {
        return getKeyboard()->addOnEventConsumer(std::move(callback));
    }

    inline ObjectPtr<input::IMouseOnEventConsumer> addOnMouseEventConsumer(
        std::function<void(input::IMouse*, const input::MouseEvent*)> callback) noexcept
    {
        return getMouse()->addOnEventConsumer(std::move(callback));
    }
};

Python API Back End

The Python back end poses several challenges:

  • C and Python have different memory layouts for arrays.

  • Python has no no concept of pass by reference for primitive types.

  • Python has no concept of const.

  • All object in Python are reference counted, with a garbage collector running periodically in the background. Omniverse interfaces are reference counted at the ABI layer, but objects such as structs are not.

Creating a Python Module

Each interface (or group of related interfaces) should have a generated .pyd file. The Python module should call PYBIND11_MODULE to generate the neccessary Python entry point into the DLL. Additionally, each binding function should be called (this must be done manually). An example PyModule.cpp is as follows:

#include <omni/Types.h>
#include <omni/IObject.h>
#include <omni/ITypeFactory.h>

// this file contains hand written bindings
#include "PyOmni.h"

// these are generated files
#include "PyTypes.gen.h"
#include "PyIObject.gen.h"
#include "PyITypeFactory.gen.h"

// the module name should be prefixed with _ and be the last token in the module name.  for example, "omni.input"'s
// module name would be _input.
PYBIND11_MODULE(_omni, m)
{
    bindOmni(m); // manually written bindings in "PyOmni.h"
    bindResult(m); // generated by omni.bind: exposes omni::Result
    bindUInt2(m); // generated by omni.bind: exposes omni::UInt2
    bindITypeFactory(m); // generated by omni.bind: exposes omni::ITypeFactory
}

Naming

C++’s pascal/camelCase will be transformed into Python’s snake_case naming scheme. In short:

  • Interfaces: IKeyboard -> IKeyboard (interface names are unchanged)

  • Structs: UInt2 -> UInt2 (union/struct names are unchanged)

  • Methods: IInterface::thisIsMyMethod -> obj.this_is_my_method

  • Enums, Flags, Constants: KeyboardKey::eNumpadEnter -> KeyboardKey.NUMPAD_ENTER

Interfaces

Interfaces are passed by reference to all methods/functions.

Interface Instantiation

Interfaces can be instantiated in Python. For example, to instantiate the IWindowSystem object:

ws = IWindowSystem()
Interface Casting

Interfaces are not duck typed in the Python bindings based on the interface implementation (which may implement many interfaces). Rather, the methods exposed for an object is based on the interface requested.

Given an object, one can cast the interface to another interface. Internally, this runs omni::cast<>.

For example, to cast from an IWindowSystem to an INightLight.

ws = IWindowSystem()

# ...

light = INightLight(ws)
if light is not None: # the cast may fail
    # use light...

out Pointers

out pointers passed to an ABI method/function map to Python in the following way:

  • The parameter is removed from the method/function signature.

  • The result of the ABI function appears in the result tuple returned by the Python method.

Given the following method:

virtual bool fillIntWithOne_abi(OMNI_ATTR("out") int* x) noexcept = 0;

one would call the method as follows in Python:

success, out = obj.fill_int_with_one()

This patterns applies to primitive types and unions/structs. It does not apply to interfaces, which are always passed by reference in the Python bindings.

in, out parameters follow the same scheme except that the Python method takes a parameter as input as well.

Properties

Methods prefixed with set, get, or is will be mapped to a read, write, or read/write property. This behavior can be disabled by passing the not_prop attribute to each setter/getter method.

Adding Custom Bindings

The Python back end will generate a function for each entity wrapped. For example, the generator will create a bindIKeyboard function for the IKeyboard interface. Each binding function returns an object that can be used to further extend the bindings:

// bindIKeyboard is a generated function which binds all of the methods in IKeyboard.
bindIKeyboard()
    // the following calls to .def() add additional "hand coded" bindings
    .def("press_all_keys", [](omni::input::IKeyboard* self) {
        using namespace omni::input;
        for (uint32_t i = 1; i < KeyboardKey::eCount; ++i)
        {
            self->addEvent({ KeyboardEventType::eKeyPress, KeyboardKey(i), fKeyboardModifierFlagsNone });
        }
    })
    .def(/* more bindings here */)
    ;

Best Practices

The following sections cover best practices when defining an interfaces ABI.

Pass Structs by const* Rather Than Value

When passing a struct (whose sizeof is greater than or equal to sizeof(void*)) to a method, prefer passing the struct by const* and with the in,not_null attributes. The binding code will:

  • Generate an API wrapper that accepts a const&. This makes it look like the caller is passing the struct by value.

  • Reduces the number of copies needed when transitioning data between C++ and Python.

Running omni.bind

Running omni.bind is straightforward:

> tools/omni.bind/omni.bind.sh include/omni/ITypeFactory.h \
   -Iinclude \
   -I_build/target-deps/fmt/include \
   -D__MY_DEFINE__=1 \
   --api include/omni/ITypeFactory.gen.h \
   --py source/bindings/python/omni/PyITypeFactory.gen.h

The flags above will change based on your environment.

Q&A

Why does OMNI_ATTR take a string instead of tokens?

Early version of OMNI_ATTR accepted tokens:

virtual void setFriend_abi(OMNI_ATTR(in, not_null) Friend* friend) noexcept = 0;

However, OMNI_ATTR tended to use small tokens with common names, such as in, out, not_null, etc. A risk existed that a header #define’d one of these tokens:

#define in _In_

// ...


virtual void setFriend_abi(OMNI_ATTR(in, not_null) Friend* friend) noexcept = 0; // omni.bind now fails

Two potential fixes were conceived:

  • Prefix attributes with omni_ (e.g. omni_in, omni_out, omni_not_null).

  • Make the argument to OMNI_ATTR a string (strings cannot be touched by the preprocessor).

The latter was chosen since it led to fewer characters typed.

Building fails on the first try, but succeeds on the second

Your build system’s dependency tree is incorrect. :slightly_frowning_face:

When using premake, make sure:

  • Interfaces are generated in their own project:

        project "omni.input.interfaces" -- this project has no cpp files, only .h files
            location (workspaceDir.."/%{prj.name}")
            omnibind {
                { file="include/omni/input/IKeyboard.h", api="include/omni/input/IKeyboard.gen.h", py="source/bindings/python/omni.input/PyIKeyboard.gen.h" },
                { file="include/omni/input/IGamepad.h", api="include/omni/input/IGamepad.gen.h", py="source/bindings/python/omni.input/PyIGamepad.gen.h" },
                { file="include/omni/input/IMouse.h", api="include/omni/input/IMouse.gen.h", py="source/bindings/python/omni.input/PyIMouse.gen.h" },
            }
            dependson { "omni.core.interfaces" } -- all interfaces depend on the core interfaces
    
  • Your project depends on the interfaces. For example:

        project "omni.input.python"
            define_bindings_python {
                name = "_input",
                folder = "source/bindings/python/omni.input",
                namespace = "omni/input"
            }
            dependson { "omni.input.interfaces" } -- this is the dependency line
    

Previously generated bindings are parsed. Why?

Interfaces include their API layer bindings. When generating the API layer, the previously generated API bindings are considered. Why?

Ignoring the generated API layer bindings is easy. However, there are cases where not ignoring them is useful. In particular, when the user has created an overload to an API layer method. In this case, the user must use a using statement to expose the API layer generated method. If omni.bind is not able to find the source of the using (which is in the generated API layer), it will notify with an error.

OMNI_DEFINE_INTERFACE_API(omni::windowing::IWindow)
{
public:
    inline void setCursor(ObjectParam<windowing::ICursor> cursor) noexcept
    {
        return setCustomCursor_abi(cursor.get());
    }

    // We must expose setCursor(ICursorType) since setCursor(ICursor*) hides it.
    //
    // Since this method is generated by omni.bind, if omni.bind is unable to see this method, it will throw an error.
    using omni::core::Generated<omni::windowing::IWindow_abi>::setCursor;
};

Debugging omni.bind

omni.bind uses clang as a C++ parser. omni.bind will output any C++ compile errors identified by clang. Addressing these issues is a necessary first step to generate bindings successfully.

When using premake, each project will output a script that can be invoked from the command-line:

_compiler\vs2019\omni.core.interfaces\omni.bind.bat

A slew of debugging information can be printed by passing the -v flag to the script.

clang’s internal abstract syntax tree (AST) can also be printed out with the --ast flag. This is useful for understanding which constructs clang is having trouble parsing.

Premake Environmental Variables

Omniverse’s premake-based build system supports the following environmental variables to help debug omni.bind across all projects in a build. These variables must be set when running build’s generation step (i.e. running without -b or with -g):

Variable

Purpose

OMNI_BIND_VERBOSE=1

Enable verbose output. This is the same as passing -v on the command-line.

OMNI_BIND_TOUCH_OUTPUT_IF_SAME=1

Touch the output of files whose dependencies have changed but the contents of the resulting generated file did not. This is a useful when debugging build system ordering issues (e.g. when a project is missing a dependson statement in their premake5.lua causing the project to use another project’s .gen.h file before it is regenerated).

Conclusion

This document provides an overview of the constructs found in an interface’s ABI and how to annotate those constructs such that efficient and safe bindings can be automatically generated.

The bindings generator is a work-in-progress and we’re interested in making it more useful. Contact #ct-carbonite with feature request and ideas.