mercredi 27 octobre 2021

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

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)


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.


Questions:

Thus leading to the question in the title:

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

But also to :

  • Am I mistaken in the way Vulkan handles the .pNext members?

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


Design proposition:

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

  • .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.
  • Cons:

    • This is not as flexible. 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.

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?

Aucun commentaire:

Enregistrer un commentaire