-
Notifications
You must be signed in to change notification settings - Fork 310
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add RAII support with VULKAN_HPP_NO_EXCEPTIONS #1498
Comments
That's an interesting approach, indeed! Thanks a lot for this suggestion. Besides the constructors, there are also all those functions that might throw on error. When introducing VULKAN_HPP_NO_EXCEPTIONS to the raii-classes, those functions would have to be changed to return something plus the result code. |
I did some experiments on top of @jmacnak's code (see my WIP branch is here. No guarantee it actually runs) With a careful choice of defines, it's possible to make the it work with both the // with tuple
#define VULKAN_HPP_RAII_EXPECTED_CLASS std::tuple
#define VULKAN_HPP_RAII_EXPECTED(obj) std::make_tuple(obj, VULKAN_HPP_NAMESPACE::Result::eSuccess )
#define VULKAN_HPP_RAII_UNEXPECTED(type, error) std::make_tuple(type(nullptr), error)
// with std::expect (untested)
#define VULKAN_HPP_RAII_EXPECTED_CLASS std::expect
#define VULKAN_HPP_RAII_EXPECTED(obj) obj
#define VULKAN_HPP_RAII_UNEXPECTED(type, error) std::unexpected(error) However, I spent some time porting my codebase over using tuples for error returns, and it's less than ideal. Very tempting to use But an alternative proposal for supporting older versions of C++ |
Yep, that's the way to go! |
I might have a go at implementing it and see if I get something workable.
I figure keep it out of the |
Interested in this as well! |
same here |
C++23 has been finalised, and Perhaps Vulkan-Hpp could consider the other option: simply add overloads for every function/constructor returning |
@asuessenbach, any thoughts on this discussion since, or my suggestion? |
Well, I think we have waited long enough, 'til The main problem with VULKAN_HPP_NO_EXCEPTIONS are the constructors of the vk::raii classes. As they potentially throw, they would have to be guarded with some Only the create-functions would need some substantial modifications.
With some new macros like
There are some functions throwing a LogicError. That would have to be changed to a VULKAN_HPP_ASSERT, just as it's done in the vk-namespace, like this:
Does that sound reasonable? |
That looks pretty good! How would the constructors of the various RAII handles, e.g. |
I thought about backing the constructors by the construction functions as well. But I think, it's easier to just keep them as they currently are, calling the actual vkCreate-function and throwing on an error code. |
Some more elaborated version of the macros here could look like this:
That is, if you don't externally specify your expected type, some type is selected, depending on your C++-Version. The higher that version, the better that type fits to the task. |
Well, unfortunately the enumerating functions (like I have to admit, I have no good idea how to handle that. In any case, if VULKAN_HPP_NO_EXCEPTIONS is defined, those functions would need to be removed.
In both cases, the user would need to do the enumerating boiler-plate code on its own. And as the enumerated classes already have constructors taking the enumerated C-type and don't actually own that resource, it should at least work... |
class PhysicalDevices : public std::vector<VULKAN_HPP_NAMESPACE::VULKAN_HPP_RAII_NAMESPACE::PhysicalDevice>
{
public:
PhysicalDevices( VULKAN_HPP_NAMESPACE::VULKAN_HPP_RAII_NAMESPACE::Instance const & instance )
{
VULKAN_HPP_NAMESPACE::VULKAN_HPP_RAII_NAMESPACE::InstanceDispatcher const * dispatcher = instance.getDispatcher();
std::vector<VkPhysicalDevice> physicalDevices;
uint32_t physicalDeviceCount;
VULKAN_HPP_NAMESPACE::Result result;
do
{
result = static_cast<VULKAN_HPP_NAMESPACE::Result>(
dispatcher->vkEnumeratePhysicalDevices( static_cast<VkInstance>( *instance ), &physicalDeviceCount, nullptr ) );
if ( ( result == VULKAN_HPP_NAMESPACE::Result::eSuccess ) && physicalDeviceCount )
{
physicalDevices.resize( physicalDeviceCount );
result = static_cast<VULKAN_HPP_NAMESPACE::Result>(
dispatcher->vkEnumeratePhysicalDevices( static_cast<VkInstance>( *instance ), &physicalDeviceCount, physicalDevices.data() ) );
}
} while ( result == VULKAN_HPP_NAMESPACE::Result::eIncomplete );
if ( result == VULKAN_HPP_NAMESPACE::Result::eSuccess )
{
VULKAN_HPP_ASSERT( physicalDeviceCount <= physicalDevices.size() );
this->reserve( physicalDeviceCount );
for ( auto const & physicalDevice : physicalDevices )
{
this->emplace_back( instance, physicalDevice );
}
}
else
{
detail::throwResultException( result, "vkEnumeratePhysicalDevices" );
}
} At least how the the raii::PhysicalDevice constructor is written now it could be converted into fallible constructor with a THROW macro which either expands into EDIT: sorry, this was about returning |
https://youtu.be/SJlyMV_oTpY?si=oAVrnMMS9VGgQhc6&t=927 presents an approach to wrap a C library with exceptions or std::expected, nonstd::unexpected depending on user choice using the same implementation for the exception and non-exception path |
Yes, exactly. |
... so, it seams there are different opinions available on using |
In a lot of environments (mine included), memory allocation is just a hard failure/a case we don't consider, so What does the standard library do with exceptions disabled do? Does it also hard-error on creation? One option is to add yet another option which is to either ignore allocation errors, or to propagate them as part of the |
Another solution, though it means we can't quite use
Although this not declared There is a paper to subsume these narrow-contract functions under As for |
I think, it throws. But as nobody will catch it, it will ultimately call abort().
I don't think, stl-exceptions could always be mapped to vk::Result error codes. @sharadhr That is, just not marking |
Have we also considered using a private constructor for the plural types, and using a factory function? This could solve the issue nicely. |
I think, I don't get what you mean... could you provide some pseudo-code to describe? |
So, something like... struct PhysicalDevices
{
std::vector<VULKAN_HPP_NAMESPACE::VULKAN_HPP_RAII_NAMESPACE::PhysicalDevice> physicalDevices{};
std::expected<std::vector<VULKAN_HPP_NAMESPACE::VULKAN_HPP_RAII_NAMESPACE::PhysicalDevice>, VULKAN_HPP_NAMESPACE::Result> Create( VULKAN_HPP_NAMESPACE::VULKAN_HPP_RAII_NAMESPACE::Instance const & instance );
private:
PhysicalDevices( VULKAN_HPP_NAMESPACE::VULKAN_HPP_RAII_NAMESPACE::Instance const & instance )
{
VULKAN_HPP_NAMESPACE::VULKAN_HPP_RAII_NAMESPACE::InstanceDispatcher const * dispatcher = instance.getDispatcher();
VkPhysicalDevice* pPhysicalDevices;
uint32_t physicalDeviceCount;
VULKAN_HPP_NAMESPACE::ArrayProxyNoTemporaries<VkPhysicalDevice> physicalDevicesSpan;
VULKAN_HPP_NAMESPACE::Result result;
do
{
result = static_cast<VULKAN_HPP_NAMESPACE::Result>(
dispatcher->vkEnumeratePhysicalDevices( static_cast<VkInstance>( *instance ), &physicalDeviceCount, pPhysicalDevices ) );
if ( ( result == VULKAN_HPP_NAMESPACE::Result::eSuccess ) && physicalDeviceCount > 0 )
{
physicalDevicesSpan = ArrayProxyNoTemporaries( physicalDeviceCount, pPhysicalDevices );
break;
}
} while ( result == VULKAN_HPP_NAMESPACE::Result::eIncomplete );
if ( result == VULKAN_HPP_NAMESPACE::Result::eSuccess )
{
// do we still need this assert?
VULKAN_HPP_ASSERT( physicalDeviceCount <= physicalDevicesSpan.size() );
try
{
for ( auto const & physicalDevice : physicalDevicesSpan )
{
physicalDevices.emplace_back( instance, physicalDevice );
}
}
catch(const std::exception& e)
{
// or something else from vk::Result? eOutOfHostMemory?
detail::throwResultException( VULKAN_HPP_NAMESPACE::Result::eErrorInitializationFailed, e.what() );
}
}
else
{
detail::throwResultException( result, "vkEnumeratePhysicalDevices" );
}
}
public:
PhysicalDevices( std::nullptr_t ) {}
// note: don't delete the default constructor!
PhysicalDevices( PhysicalDevices const & ) = delete;
PhysicalDevices( PhysicalDevices && rhs ) = default;
PhysicalDevices & operator=( PhysicalDevices const & ) = delete;
PhysicalDevices & operator=( PhysicalDevices && rhs ) = default;
std::expected<std::vector<VULKAN_HPP_NAMESPACE::VULKAN_HPP_RAII_NAMESPACE::PhysicalDevice>, VULKAN_HPP_NAMESPACE::Result> Create( VULKAN_HPP_NAMESPACE::VULKAN_HPP_RAII_NAMESPACE::Instance const & instance )
{
try
{
this = PhysicalDevices( instance );
return physicalDevices;
}
// probably need a more elegant way to handle
catch(const VULKAN_HPP_NAMESPACE::SystemError& e)
{
return std::unexpected( e );
}
}
}; Then, rewrite VULKAN_HPP_NODISCARD VULKAN_HPP_INLINE std::expected<std::vector<VULKAN_HPP_RAII_NAMESPACE::PhysicalDevice>, vk::Result> Instance::enumeratePhysicalDevices() const noexcept
{
return VULKAN_HPP_RAII_NAMESPACE::PhysicalDevices( ).Create( *this );
} |
Looking at this interesting discussion as we are considering |
My understanding is that this is fairly common in library code. Macros are the most straightforward way to provide similar functionality across various C++ versions. They also make generated code easier to deal with, from what I've seen. Templated types can potentially explode compile times, and Vulkan-Hpp is already huge as it is (I've literally never seen a larger header file, we are approaching 100K lines). We actually do have constexpr functions, but these are for the users to use. |
Google / Android does not use exceptions (see here). However, the RAII classes are very useful!
Could we update the RAII generator to guard the throwing constructors for handle types using:
guard the
throwResultException()
function definition, and introduce static "create" functions that return astd::expected<HandleType, vk::Result>
using:Bonus points if we can use custom defines such as
VULKAN_HPP_RAII_EXPECTED_CLASS
/VULKAN_HPP_RAII_UNEXPECTED_CLASS
to use custom "expected" implementations when compiling for C++ version < 23 (for example,android::base::expected
). If theseVULKAN_HPP_RAII_EXPECTED_CLASS
/VULKAN_HPP_RAII_UNEXPECTED_CLASS
defines are not defined, potentially need to guard thecreate()
functions based on the version.See https://android-review.googlesource.com/c/device/google/cuttlefish/+/2404731 for a quick attempt at this.
The text was updated successfully, but these errors were encountered: