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. AppendingWithoutAcquire
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:
paths
is an array of strings.The number of items in the array is described by
pathCount
.The array can be
nullptr
.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 anomni::core::ResultError
exception if the ABI layer returns anomni::core::Result
error code.The return type is now
omni::core::ObjectPtr<IModel>
rather thanIModel**
.*return
is used rather thanreturn
.return
would return theIModel**
while*return
dereferences the pointer to a pointer to return aIModel*
(which is converted toObjectPtr
).
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
struct
s 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 |
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 |
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.