Vulkan introduces a member .pNext
of type void*
in all its core struct
s 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 struct
s 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
#ifdef
s 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
#ifdef
s checking the API version may be required in the commands using those struct
s.
- Generating
extension_flags
mask can be a problem, especially if they are more than 64 possible things. It can however be split in several enum
s, 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?