Some thoughts on the memory safety of the C++ api #530
RoyAwesome
started this conversation in
Ideas
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
As I've been diving deeper and deeper into modern C++, I've come around to the points made by Bjarne Stroustrup in his paper "P2410 Type-and-resource safety in modern C++" (listed here: cplusplus/papers#1080 ). The main points I see in this paper in regards to flecs is 5 and 6,
every reference through a pointer is not through the nullptr
andevery access through a subscripted pointer is in-range
. I feel like there are parts of the flecs c++ api that don't adhere to these principles, namely around the flecs::entity::get*() calls and the flecs::system::iter callback parameters, and I've been thinking of some ideas how to move to a more resource-safe api for flecs. I feel like if you learn the rules, these issues are minor... but I also think that if you break the rules you should fail to compile or crash, and the API does not guarantee that behavior.When declaring a system, you are presented with two choices, the
each
function call, which requires a function with the form:or, the
iter
function call, which requires a function of the form:When using flecs, I tend to stick to the
each
syntax as much as possible, except when I need to do things before or after the iteration of the components that that system matches against. Witheach
, I know that my component reference will be valid. I have no fear accessing it, mutating it (if not const), or storing it as a copy. I know not to store it as a reference because I know that the reference may die after my scope ends (any issues around storing a reference are issues with C++ as a whole, so while there are exceptions, they aren't something the flecs api can fix). Withiter
, however, the fact that the component array for that iterator is presented to you as a raw pointer runs afoul with a few of the philosophies regarding memory safety.First, that pointer contains no size information. I know that you can look up the size from the
iter
parameter, but the array itself gives you nothing. It's impossible for me to know if, while iterating through the array, if an access likecomp[3]
will even succeed or not. Will it go out of bounds? Will it access components that aren't even matched with the query? It's completely unknown what that code will do.Secondly, that pointer can escape. If I stored that component array pointer into some static variable, or in another component, I have no idea how long that pointer will survive after my function returns. The obvious answer is "Don't Do That", but I'm of the opinion that APIs should be very clear about who owns what, and be extremely explicit about the lifetime of pointers.
A possible option here is to use a
span
like object to bringiter
systems into the same level of safety aseach
systems. If there was some typespan<Component>
, you could do:cpp20 adds std::span<> objects, and it might be worth it to add support for functions that take std::span parameters for
iter
callbacks if span is available. You can check if__cpp_lib_span
macro is defined, and if so, std::span is available. It might be worth it to define a flecs::span object for backwards compatibility, or just only allow the usage of std::span in cpp20 scenarios and fall back on the raw pointer behavior if such facilities aren't available.The second thing I've run into is regarding the
flecs::entity::get*()
family of functions for component getters. These return pointers to the underlying components... Valid if it exists, nullptr if it doesn't. I do not like the use of raw pointers here because those pointers can escape the execution of that function, and I know that flecs may move components around at the end of frames and that memory access can be invalid in the future. Also, using pointers here opens up a whole class of syntactically correct operations that would cause massive issues if done. For example, given some entity e, you could doe.get_mut<MyComponent>()[3]
and you would have no idea what that does. I would assume it doesn't immediately crash due to how flecs packs components and organizes memory, but you wouldn't want to do anything with that!It seems to me that really there are two possible return values for a get* function, and that is "The Component" or "Some indication that the entity does not have that component". Some options to improve the safety of get*() functions could be:
[const] Component&
for get*() functions, and throw an exception if the component doesn't exist. (I'm not a fan of this)std::optional<[const] std::reference_wrapper<Component>>
for get* functions.expected<[const] Component&, flecs::AccessError>
for get* functions, where flecs::AccessError is some enum that indicates the reason why a component failed to be retrieved.I personally prefer option 3 as a way to make get calls safer, as you guarantee a valid component access, unless the expected wrapper is in error. The
expected
pattern is currently making it's way through the C++ standardization process, and there is a relatively good implementation here: https://github.com/TartanLlama/expected. The only issue is storing the reference and preventing it's escape, but issues there are once again issues with C++ in general and probably not something flecs can fix.All in all, this is just some musing about the C++ api, and I made this post with the hope to start some discussion on the topic.
Beta Was this translation helpful? Give feedback.
All reactions