Simulation / Modeling / Design

Preferring Compile-time Errors over Runtime Errors with Vulkan-hpp

One of the most important aspects in professional software development is to detect errors as early as possible. Of course, the best case would be if we couldn’t even write erroneous code. The next best thing is errors that the compiler can detect.

The worst cases are runtime errors. The hardest ones are hidden in code that only runs under certain circumstances. Murphy’s law says that those circumstances happen in the customer’s environment for the first time.

If you’re using Vulkan, there are a few ways to create runtime errors. Even with the great validation layers available with Vulkan, you must run that part of the code to detect such errors. By the way, I recommend that you not even try to program with Vulkan without using the validation layers!

When you use Vulkan-hpp, some of those runtime errors are turned into compile-time errors. Vulkan-hpp is a header-only C++ binding for the Vulkan API. It is maintained by Khronos as part of the Vulkan ecosystem and can be found on GitHub at KhronosGroup/Vulkan-Hpp. It is part of the LunarG Vulkan SDK as well. For more information, see Vulkan C++ Bindings Reloaded and Open-Source Vulkan C++ API.

Features that help move errors to compile time

Vulkan-hpp helps eliminate runtime errors with the following features:

  • Enum classes compared to plain enums
  • Helper class vk::Flags
  • Struct constant member sType
  • vk::StructureChain
  • Handle type safety in 32-bit builds

Enum classes

With Vulkan, you get lots of enum types. Apart from VkResult, they are all constructed using the following naming scheme:

typedef enum VkEnumName {
    VK_ENUM_NAME_VALUE_A = 0,
    VK_ENUM_NAME_VALUE_B = 1,
    …
} VkEnumName;

With Vulkan-hpp, you get an enum class for each of those enum types:

namespace vk
{
    …
    enum class EnumName
    {
        eValueA = VK_ENUM_NAME_VALUE_A,
        eValueB = VK_ENUM_NAME_VALUE_B,
        …
    };
    …
}

First, they all reside in the namespace vk. You can adjust that namespace by defining VULKAN_HPP_NAMESPACE. The enum class itself then does not have the prefix Vk, as that would be redundant with the namespace. Finally, each value of an enum class skips the prefix VK_ENUM_NAME, as that in turn would be redundant with the namespace and the enum class name. They are prefixed with a lowercase ‘e’ and contain a camelCase version of the actual enum value name. An enum class value is not allowed to begin with a digit, so the ‘e’ prefix prevents that. For example, wherever you would use VK_ENUM_NAME_VALUE_A in your C-code, use vk::EnumName::eValueA instead in your C++-code.

So, what do you get from the enum classes in Vulkan-hpp? After all, due to the enum value naming scheme in Vulkan, there can’t be two enum values with the same name. That wouldn’t even be your problem but the problem of the Vulkan guys at Khronos. Moreover, it’s unlikely that you would want to have a variable or function named just as one of the enum values, even though those names are exported to the global scope. Who knows? Someone might like function names like VK_STRUCTURE_TYPE_PIPELINE_TESSELLATION_DOMAIN_ORIGIN_STATE_CREATE_INFO.

The important point here is that there is no implicit conversion to int with the scoped enum classes in Vulkan-hpp. You can’t assign an enum class value to an int type, at least not accidentally. You also can’t compare two enum class values from different enum classes. Some compilers produce a warning when you compare two enum values from different enums. That might be compiler-dependent and, of course, a warning is much easier to miss than an error.

Helper class vk::Flags

With Vulkan, there are a couple of data type pairs, using the following naming scheme:

typedef enum VkEnumNameFlagBits = {
    VK_ENUM_NAME_VALUE_A_BIT = 0x00000001,
    VK_ENUM_NAME_VALUE_B_BIT = 0x00000002,
     …
} VkEnumNameFlagBits;
typedef VkFlags VkEnumNameFlags;

Here, VkFlags is just an uint32_t, and VkEnumNameFlags is supposed to hold zero or more values of the corresponding enum VkEnumNameFlagBits by ORing the appropriate enum values from VkEnumNameFlagBits. As there is no real connection between *FlagBits and *Flags besides their common name part, the compiler can’t help with that. You’re allowed to apply bitwise operators on arbitrary enum values or integers. If that erroneously composed *Flags value happens to be a valid combination of bits, even though they probably are not what you meant them to be, even a validation layer might fail to catch that. It might feel like you are in undefined-behavior land, even though the program does exactly what you told it to do. It’s just not what you wanted to tell it to do.

With Vulkan-hpp, you get corresponding pairs:

namespace vk
{
    …
    enum class EnumNameFlagBits : VkEnumNameFlagBits
    {
        eValueA = VK_ENUM_NAME_VALUE_A_BIT,
        eValueB = VK_ENUM_NAME_VALUE_B_BIT,
        …
    };
    using EnumNameFlags = Flags<EnumNameFlagBits>;
    …
}

With that construct, such awkward situations can’t happen. You can’t apply bitwise operators on enum class values. The vk::EnumNameFlags enum knows about the corresponding vk::EnumNameFlagBits. The helper class vk::Flags provides functionality that allows you to apply bitwise operators on enum class values from the same enum class, but only those values. You can’t combine them with values from a different enum class. At compile time, you construct your flags only with allowed flag bits.

sType member of structs

Moving slightly away from enums, there are a lot of structs in Vulkan that hold a member sType of the enum type VkStructureType as their first element. For each of those structs, you must set that member to exactly the value specified for that struct. In the following code example, the member sType must be set to VK_STRUCTURE_TYPE_STRUCT_NAME.

typedef struct VkStructName {
    VkStructureType sType;
    …
} VkStructName;

It’s not that difficult, but you must do it right. Do not forget to set it, and don’t set it to the wrong value by copying from another location in your code. For example, the following values are visually close:

  • VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO
  • VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO

With Vulkan-hpp, you have the following stricture on structs:

namespace vk
{
  …
  struct StructName
  {
    …
    const vk::StructureType sType = vk::StructureType::eStructName;
    …
  };
  …
}

It’s as simple as that. When you instantiate a struct of type StructName, you don’t have to worry about setting the right value for the member sType as it’s already set. Because it’s a const member, you can’t accidentally overwrite it.

As a side note for the interested template meta-programmer: the struct StructName also provides a static member structureType, that is just that vk::StructureType value. There’s a type trait named CppType that gets you the type of the struct from a vk::StructureType value.

Helper class vk::StructureChain

A lot of structures in Vulkan have pNext as their second member:

typedef struct VkStructName {
    VkStructureType sType;
    const void*     pNext;
    …
} VkStructName;

Some structures are specified to extend other structures. The pNext pointer is used to create a chain of structures. Some structures can be part of that chain multiple times, with different instances. Your code might look like the following example:

ChainedStruct chained = {};
chained.sType = VK_STRUCTURE_TYPE_CHAINED_STRUCT;
// set other values of chained

AnchorStruct anchor = {};
anchor.sType = VK_STRUCTURE_TYPE_ANCHOR_STRUCT;
anchor.pNext = &chained;
// set other values of anchor

With this approach, there are a couple of potential errors. For example, you might chain a struct to AnchorStruct instance that’s not specified to be in the chain. Or you might accidentally chain multiple instances of a struct where only one is allowed. Even memory management can lead to unexpected behavior. For example, when you have a function that creates a chain using local variables, as in the earlier code example, When the function finally returns the anchor by value, you’re already doomed. The chained structure pointed to by anchor.pNext is gone.

With vulkan-hpp, that code looks nearly identical:

namespace vk
{
  …
  struct StructName
  {
    …
    const vk::StructureType sType = vk::StructureType::eStructName;
    const void *            pNext = {};
    …
  }
  …
}

You can use it the same way as you use the Vulkan counterpart, with the same sources of potential errors. The helper class vk::StructureChain helps the compiler here, as in the following code example:

vk::StructureChain<vk::AnchorStruct, vk::ChainedStruct> chain
(
    { /* set other values of anchor */ }
    { /* set other values of chained */ }
);

Of course, that chain might get arbitrarily long. The compiler can check if all chained structs are specified to extend AnchorStruct. If the same chained struct occurs multiple times, the compiler checks whether that is allowed. Accessing the elements of such a chain would be slightly different than you might be used to with the pure C-type chain of structures:

vk::AnchorStruct const & anchorStruct = chain.get<vk::AnchorStruct>();
vk::ChainedStruct const & chainedStruct = chain.get<vk::ChainedStruct>();

If you must remove elements from your structure chain at runtime, you can do so by using the member function vk::StructureChain::unlink. That way, the memory footprint of the structure chain does not change but the now unused parts are skipped by not being pointed to by any of the pNext pointers in that chain. To re-link, use vk::StructureChain::relink.

Type safety

Finally, here’s a look at a slightly different topic, type safety. This is an issue with Vulkan, especially with 32-bit builds. On a 32-bit build, all non-dispatchable handles like VkBuffer, VkImage, and VkSemaphore are just a typedef on uint64_t:

#define VK_DEFINE_NON_DISPATCHABLE_HANDLE(object) typedef uint64_t object;
…
VK_DEFINE_NON_DISPATCHABLE_HANDLE(VkBuffer)
VK_DEFINE_NON_DISPATCHABLE_HANDLE(VkImage)

You can call vkCreateBuffer and pass in a pointer to VkImage as the last argument. You can also assign VkImage to VkBuffer or compare them, all without any errors!

As the corresponding types in Vulkan-hpp are separate classes without any inheritance relationship whatsoever, the compiler does not allow any of those operations. You just can’t get it wrong.

Preferring edit-time error prevention over runtime errors

As I said earlier, the best scenario would be not even being able to write erroneous code. Vulkan-hpp, in conjunction with probably every modern IDE helps you with setting the members of a struct. Because each vk::struct has a constructor with an argument list of every member of that struct, besides the already mentioned sType and pNext, the IDE probably leads you through all those arguments at edit-time, making it harder to miss any errors.

Switching to C++ for your Vulkan-based project

These are some of the Vulkan-hpp features that substantially ease coding with Vulkan. The features are available with zero runtime overhead. It’s just the compiler that must work a bit more. You still must program (nearly) as explicitly as with plain Vulkan, but the compiler can check your code much better. That can save you a lot of time!

Discuss (3)

Tags