Abstracting C Platform Query Calls in Type and Memory Safe C++ Containers a.k.a. RAII Using Enums and Templates
While intergrating native platform C APIs (e.g. on windows) into a third-party C++ application, I found myself having to make sure that query functions, that allocate and fill results of different data types, are called with the right paramters. And the inquired data is also properly disposed of after the call. This, and the fact that saftey is on the forefront of the collective unconcious, served as the inspiration for this post and the publishing of a small demo, highlighting the architecture proposed in this article. Hope you enjoy reading.
The goal was, to make the abstraction able to select the appropriate data for the query call on the basis of a type indicated by an enum value. The desired usage of the abstraction should look like the following:
Given the operation type native_query_is_valid the query should yield a boolean value after its execution. It should also be possible to ask the query for its state after executing the native call and if the result has a valid value.
Mock native platform API
Let’s pretend we are working with a native platform, that provides some feature in form of a C interface. The feature queries the platform on the basis of an enum value and provides you with a result, which can be of various data types.
The two available types in this example, are a boolean and a data structure called NATIVE_DESCRIPTION. To indicate the query type, the API defines an enum called NATIVE_OPERATION_CODE. To query the system, you call SomeNativeQueryMethod, which expects a handle, the type of operation we are looking to execute, the size of the data provided and a pointer to a pointer of type void. When calling the function, the memory pointed to by the variable you passed to the paramter Data will be filled with either a boolean or description data structure, depending on which operation type you passed to the Operation paramter.
The journey to abstraction
Let’s start by abstracting the way we interact with the memory which will be filled by the query function.
This container is created in the spirit of ‘resource acquisition is initialization’ (RAII) principal. The base idea is, that an objects life cycle, should coincide with the allocation and deallocation of the internal memory used. In our case, the focus is primarly on the deallocation part, as can be seen in the deconstructor.
Next we setup an interface to encapsulate a query. It abstracts the execution of the native query method, by story the type of operation and has memory to store the result in. It also knows how to get the size of the result data and check if the data is valid. Now there is no actual way, to access the result data. Which we will be looking at next.
Templating our way to type safety
With the base class NativeQueryBase handling all the non type specific functionallity, we know have to find a way to access our data depending on which enum value the query value has. This is done in two steps. First we declare a template class NativeQuery, with the default type of pointer to void.
Now as specified in the beginning, we want to expose two types of native queries, native_query_is_valid with the resulting data being a boolean and native_query_description with the resulting data being a custom structure. For this purpse, we create two macros. DECLARE_QUERY_TYPE will declare a template specialization, of a certain operation type (our platform enum) and the corresponding return type. The return type is used to declare a get methods which has the expected/appropriate type for its operation. The second is the defintion of the template specialization class’ constructor, specifying the size of the data type and implementing the actual getter, which merely casts the data to the expected type. This allows us to now declare two types of query operations, with their respective return type.
And then define the implementation in any compilation unit we want.
Tying it all together
Now to run an actual query, all we have to do, is create a query object and specify the type of query we want to execute. The compiler will validate and choose the appropriate template implementation for us and will bless us with an error if there is something wrong. We also don’t have to worry about the result data, as it is managed be the life cylcle of the query object.
This is it for now 🎉 Until next time my friends. Thank you for reading!