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.
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.
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.
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.
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:
OMNI_ATTR("py_get") virtual 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:
OMNI_ATTR("py_set") virtual 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:
OMNI_ATTR("py_name=get_default_app_window") virtual IWindow* getDefaultWindow_abi() noexcept = 0;
the following Python would be generated:
w = obj.get_default_app_window()
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:
void useInterface_abi(IMyInterface_abi* iface) noexcept = 0; // bad: never pass around _abi pointers
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 by nullptr. By default, it is assumed the pointer can be
nullptr.
void setFriend_abi(OMNI_ATTR("not_null") IFriend* friend) noexcept = 0;
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.
void isKeyDownEvent(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.
void fillEvent(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.
void setName(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.
void sort(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
).
void setKey(OMNI_ATTR("c_str") const char* key, uint32_t value) 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.
void loadModels(OMNI_ATTR("c_str, py_name=file") const char* path) noexcept = 0;
Pointers to Pointers as Parameters¶
Consider the following method signature:
void processPaths(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:
void processPaths(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:
void processPaths(
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;
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.
OMNI_ATTR("owner=this") const char* getTitle() noexcept = 0;
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
};
Enum¶
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:
bool fillIntWithOne(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:
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_
// ...
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 kind "StaticLib" 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.
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.