Omni String

omni::string is an ABI-safe, DLL-Boundary-safe string class intended to be used in Omniverse Native Interfaces that provides all of the functionality of std::string. The layout of omni::string satisfies std::is_standard_layout and will be the same across all compilers. Using omni::string requires carb.dll/.so because of the Memory functions used, which are provided by the Carbonite Framework.

Why omni::string?

Strings are frequently used in Carbonite and Omniverse interfaces, however there is not an ABI safe string implementation available for interfaces to use. This leads to char* and size parameters being used, which are both error prone and cumbersome to use. For example:

// This creates a URL from the pieces provided.
// "bufferSize" is an in-out parameter; set it to the size of the buffer before calling this function
// and it will be set to the actual size when the function returns. If the size required is more
// than the size provided, this function returns null, otherwise it returns 'buffer'.
char* omniClientMakeUrl(struct OmniClientUrl const* url, char* buffer, size_t* bufferSize);

This interface requires a confusing combination of char* and size_t* parameters. It requires users to preallocate a character array which may or may not be large enough, and then may returns a nullptr, which could lead to program crashes if users don’t check the return value. With omni::string, this interface could be simplified to:

// This creates a URL from the pieces provided.
omni::string omniClientMakeUrl(struct OmniClientUrl const* url);

With omni::string, there’s no more dealing with buffer sizes or returning nullptr which may or may not actually get checked.

Why not std::string?

Why go through the trouble of implementing and maintaining our own string class instead of using std::string? There are two major reasons that std::string is not suitable for use in Omniverse interfaces. First, std::string is not ABI safe. The ABI of std::string was broken in C++11, and is not guaranteed to not be broken again in the future. Second, std::string is not safe to pass across DLL boundaries due to memory allocation. A std::string that is created with allocated storage in one module should not be passed to another module which may free the memory.

For these reasons, omni::string was created to be the ABI-safe, DLL-boundary-safe string class in Omniverse.

Details

Small String Optimization

omni::string provides small string optimization (SSO), so that an allocation is not required for small string. In omni::string, strings up to 31 characters long will be stored in the string’s internal buffer rather than in allocated storage. Note that omni::string is still only 32 bytes in size, so user’s do not have to pay a penalty in stack space to get this optimization. Profiling typical workflows indicates that close to 50% of strings used in Kit will be able to take advantage of being small string optimized.

No Templates

Unlike the Standard Library which provides a templated std::basic_string<CharT, Traits, Allocator> class and then typedefs std::string, omni::string has no template arguments. This was done primarily for ABI safety and complexity concerns. Omniverse only uses UTF-8 characters, so the CharT template option was removed because char is the only type that should be used with omni::string. Similarly then, there was no need for a Traits type. Finally, to maintain DLL boundary safety, omni::string uses the allocation methods provided by Memory, so the Allocator template parameter was also omitted.

A template parameter for changing the size of the SSO buffer was also originally considered, however it was ultimately rejected. This parameter would allow for the ABI of an interface to be broken simply by changing that template parameter, which could lead to difficult to diagnose issues. This parameter also led to more complex implementation, and it was decided that the benefits did not outweigh the costs.

std::string-compatible Interface

omni::string provides an interface as close to that of std::string as possible. All member functions and non-member functions provided by std::string up to C++20 are available for omni::string. This should make it easy and familiar to use. There are a few subtle differences however.

First, in C++17, an overload was added to most functions that took a generic T parameter and implicitly converted it to a std::string_view, and performed the operation with that std::string_view. omni::string does not currently implement these overloads because Carbonite headers must be compatible with C++14, which does not have the std::string_view type. These functions can be added in the future.

Second, most functions in std::string became constexpr in C++20. omni::string makes an effort to make as many functions as possible constexpr, but it cannot match std::string in this regard. std::string takes advantage of C++20 features that allow transient allocations via operator new to be constexpr, which allows any std::string method that may grow the string, and thus trigger a reallocation, to be constexpr. Because omni::string uses the Memory management functions, which are not constexpr, it is unable to make functions that may grow the string constexpr.

Python Bindings

omni::string is only intended to be used in C++, and therefore will not have Python bindings generated for the class. Python users should continue to use Python’s native strings. Omni.bind will be extended to automatically convert Python strings to omni::string for interfaces that use omni::string and have Python bindings. This will allow Python users to seamlessly interact with interfaces using omni::string without requiring bindings for the omni::string class itself.