2

Vulkan introduces a member .pNext of type void* in all its core structs allowing to create handles. This member purpose is to allow to extend the structure by passing a pointer another one.

This is mainly used for extensions, so that the same struct can be extended by two different extensions without any need to modify the original members. You simply check .sType of the .pNext (the first few bytes), and apply a std::any_cast / reinterpret_cast (or equivalent) to get the other members depending on the value.

I find this idea very elegant, as it not only allows flexible, extensible design, but also is very convenient when you try to maintain backwards compatibility.

(See also the references for more context, or if you are not familiar with how Vulkan xxCreateInfo structs work)


Problem:

There is however something that bugs me in this design: this void* (or std::any for that matters). You must trust the user, who must put a valid member, and cannot assume anything about the type. Consider the following case:

struct extension_a
{
    structure_type_t sType;
    void* pNext;
    //... Other members
 };
 
struct extension_b
{
    structure_type_t sType ;
    void* pNext;
    //Other members MATCHING THOSE OF A IN SIZE, member per member
}

So two structs with members matching in size and order, only the valid sType differs. If user U does the following :

extension_b ext 
{
    .sType = extension_a_identifier, //<-------------------
    .pNext = nullptr,
    //other members initialized
};

const create_info creator 
{
    //...
    .pNext = &ext;
    //... Other members
};

framework_command(something, &creator, otherthing);

It is very likely the data of ext will be reinterpreted as if the one from a extension_a, resulting in either undefined behaviour or unexpected results as it is supposed to be extension_b data.


Question:

Thus leading to the question in the title:

How could Vulkan pNext design be implemented in a "safer" way?

I understand my "definition" 'safer' is not really well-thought in itself. I am not sure this is completely relevant to type-safety only, but it clearly has this aspect in it. Feel free to edit this paragraph if you find better wording.

*Note that my question is not about reimplementing this in Vulkan *. This would be completely stupid, Vulkan devs and devs using Vulkan have other things to do. It is more about porting the design to another API.


Design proposition:

I leave this made-in-5-minutes pattern here as I thought this might give some ideas.

(This is expressed in the C++ way for readability)

Do not pay attention to the accessibility of the members, this is pseudo-code.

  • .pNext is split into other members. An additional member, .extensions specifies the active extension(s).
struct create_info
{
    extension_a* pExtensionA = nullptr;
    extension_b* pExtensionB = nullptr;
    
    extension_flags extensions = extension_flags::nothing;
    
    //... Other members
    
    void add(extension_a* const ext)
    {
        pExtensionA = ext;
        
        if(pExtensionA != nullptr)
        {
            extensions  |= extension_a_identifier;
        }
        else
        {
            //Reset the part of extension notifying extension_a_identifier
        }
    }
    
    //... Similar for extension_b
};

Where extension_flags is taken from a VkStructureType-like enum allowing to build unique flag combination (using binary OR for example).

  • Should you need to restrict to one extension of the struct, you can proceed this way
struct create_info_2
{
    extension_a* pExtensionA = nullptr;
    extension_b* pExtensionB = nullptr;
    
    extension_flags extensions = extension_flags::nothing;
    
    //... Other members
    
    void extend(extension_a* const ext)
    {
        pExtensionA = ext;
        pExtensionB = nullptr;
        
        extensions  = extension_a_identifier;
    }
    
    void extend(extension_b* const ext)
    {
        pExtensionA = nullptr;
        pExtensionB = ext;
        
        extensions  = extension_b_identifier;
    }
    
};

  • Finally, in functions using create_info, you check extension to know what to do and only use the appropriate member:
if(creator != nullptr)
{
    switch(creator.extensions)
    {
        //...
    }
}
  • Pros:

    • Well, this forces the user to put something valid.
    • Solves the problem explained above.
    • Does not require a chain, but still allows it.
  • Cons:

    • This is not as flexible as what is done in Vulkan. It works for API versions (simply #ifdefs with the additional possible members) but not for extensions, which surely do not have access to this part of the code.
    • It is somewhat a pain to maintain. Additional #ifdefs checking the API version may be required in the commands using those structs.
    • Generating extension_flags mask can be a problem, especially if they are more than 64 possible things. It can however be split in several enums, one per structure extension_flags_xxx_create_info for example.

Needless to say, I am not exactly satisfied by such a design, as it looses in flexibility but more importantly in maintainability and extensibility. But I think this may be a good basis or at least give some ideas.

References

Vulkan: What is the point of sType in vk*CreateInfo structs?

Purpose of pNext in Vulkan structures

Why do some struct types let us set members that can only be a certain value?

1 Answer 1

2

There is no "safer" way, because languages like C don't allow for such a thing.

Vulkan is not defined as a C API by accident. This is done deliberately to allow access to Vulkan by any language that can speak C. This happens to be most languages that you'd want to have direct access to Vulkan. So Vulkan's extensible API design means that it must live within the limitations of C.

And C has no mechanism to automatically do any of the things you're talking about. You might develop a wrapper in <insert-language-here> that is safer within that language. But C doesn't have that. You have to do everything manually.

The only real alternative design that fits C would be something much uglier: a generic, function-based mechanism to build objects. It might look like this:

void *img_params = vkCreateParamObject(VK_IMAGE_CREATE_PARAM); //Creates an empty, default object of the given type.
vkObjSetParami(img_params, VK_IMAGE_CREATE_WIDTH, width);
vkObjSetParami(img_params, VK_IMAGE_CREATE_HEIGHT, height);
...

void *img_ext = vkCreateParamObject(VK_SOME_IMAGE_EXTENSION);
vkObjSetExtension(img_params, img_ext);

VkImage * img = vkCreateImage(img_params);
vkDestroyObject(img_params);
vkDestroyObject(img_ext);

Basically, all of the extensible structures would be created through this generic, function-based API. This is obviously hideously ugly and neigh unreadable nonsense. But it's also pretty dangerous, as you have to match the given values to the type of each parameter manually.

It's basically the OpenGL approach, but you bake the object into an immutable form afterwards.

2
  • Yes, and this is a completely standard and accepted mechanism for doing such things in C APIs and everyone should be used to it, and know how to wrap it (in the language of their choice) for as much safety as they want. The thing new (to me) is the innovation of having a type-id enum in a field right next to the void*. That's nice (if you have a mechanism to agree on the type-ids). Contrast that with the APIs in some well-known operating systems where the way you tell what's being pointed to is to look at the first few bytes of it which encode a length. 22bytes? Must be a CFoo_v3!
    – davidbak
    Commented Oct 29, 2021 at 22:23
  • @davidbak To be fair, CFOO_vLAST is the only one ever modified, by extending it at that, and they all come from the same source, so the size actually is the most sensible constant to use, especially as it avoid the need to define yet another constant, and someway match it to the right structure. Commented Jan 19, 2023 at 9:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.