Type safety macros in libpmemobj
The PMEMoid plays the role of a persistent pointer in a pmemobj pool. It consist of a shortened UUID of the pool which the object comes from and an offset relative to the beginning of the pool:
|
|
Operating on such persistent pointers is equivalent to operating on raw pointers to volatile objects represented by void *. This approach is error prone and such errors are very hard to find.
There is a real need to provide some mechanism which would associate a persistent pointer with a type. The libpmemobj provides a set of macros which allows to avoid such situations and generates compile-time errors when trying to assign a PMEMoids of different types.
As an example it is totally acceptable to perform the following operation:
|
|
This code compiles fine, however the programmer probably didn’t intend for that that to happen and it will probably lead to hard to debug unexpected behavior.
Another problem with untyped pointers is accessing the fields of a structure or a union. To do this it is required to convert a PMEMoid to a pointer of the desired type and only after that the fields may be accessed:
|
|
This leads to a situation where each object must have two representations in the code: the PMEMoid and a typed pointer.
Anonymous unions
One of the possible solutions is to use anonymous unions which contain the PMEMoid and information about the type of the object - a pointer to the desired type. This pointer may be used for type checking in assignments and for conversion from PMEMoid to a pointer of the desired type.
The macro which declares the anonymous union looks like this:
|
|
When using the OID_TYPE() macro the following code would generate a compile-time error:
|
|
The conversion from PMEMoid to the typed pointer may be achieved using the DIRECT_RW() and DIRECT_RO() macros for read-write and read-only access respectively:
|
|
The definition of DIRECT_RW() and DIRECT_RO() macros look like this:
|
|
No declaration
Contrary to the later mentioned named unions, the anonymous unions don’t need a declaration. The OID_TYPE() macro may be used for every type at any time. This makes using the anonymous unions simple and clear.
Assignment
The assignment of typed persistent pointers must be performed using special macro. The two anonymous unions which consist of fields with exactly the same types are not compatible and generates a compilation error:
error: incompatible types when assigning to type ‘union <anonymous>’
from type ‘union <anonymous>’
The OID_ASSIGN_TYPED() looks like the following:
|
|
It utilizes the gcc builtin operator __builtin_types_compatible_p which checks the compatibility of types represented by typed persistent pointers. If the types are compatible the actual assignment is performed. Otherwise the fake assignment of _type fields is performed in order to get clear message about the error:
|
|
error: assignment from incompatible pointer type [-Werror]
(lhs._type = rhs._type))
^
note: in expansion of macro ‘OID_ASSIGN_TYPED’
OID_ASSIGN_TYPED(car, pen);
Passing typed persistent pointer as a function parameter
Passing a typed persistent pointer as a function parameter generates a compile-time error:
|
|
error: incompatible type for argument 1 of ‘stop’
stop(car);
^
note: expected ‘union <anonymous>’ but argument is of type
‘union <anonymous>’
stop(OID_TYPE(struct car) car)
Type numbers
The libpmemobj requires a type number for each allocation. Associating an unique type number for each type requires to use type numbers as separate defines or enums when using anonymous unions. It could be possible to embed the type number in the anonymous union but it would require to pass the type number every time the OID_TYPE() macro is used.
Named unions
The second possible solution for type safety mechanism are named unions. The idea behind named unions is the same as for anonymous unions but each type allocated from persistent memory should have a corresponding named union which holds the PMEMoid and type information.
The macro which declares the named union may look like this:
|
|
The TOID_DECLARE() macro is used to declare a named union which is used as a typed persistent pointer. The TOID() macro is used to declare a variable of this type:
|
|
The name of such a declared union is obtained by concatenating the desired type name with a toid prefix and a _toid postfix. The prefix is required to handle the two token type names like struct name, union name and enum name. In such case the macro expands to two tokens in which the first one is declared as an empty macro thus avoiding the compilation errors which would appear, if only postfix or prefix was used. For example in case of the struct car the TOID() macro will expand to the following:
|
|
The _toid_struct token and analogous for enum car and union car may be removed by declaring the following empty macros:
|
|
In result the typed persistent pointer for struct car will be named car_toid. In case of one-token types the name of union will consist of both prefix and postfix. For example in case of size_t type, the TOID() macro will expand to the following:
|
|
Using such mechanism it is possible to declare named unions for two-token types.
The definition of D_RW() and D_RO() macros are the same as in case of anonymous unions.
Assignment
In case of named unions there is no issue with assignments encountered in anonymous unions. The assignment may be performed without using any additional macro:
|
|
The above example compiles without any errors but the following code would generate an error:
|
|
error: incompatible types when assigning to type ‘union car_toid’ from
type ‘union pen_toid’
car = pen;
^
Which clearly points where the problem is.
Passing typed persistent pointer as a function parameter
It is also possible to pass the named union as a function parameter:
|
|
Passing typed persistent pointer of a different type generates a clear error message:
|
|
error: incompatible type for argument 1 of ‘stop’
stop(pen);
Type numbers
Since the named union must be declared before using it, the type number may be assigned to the type in the declaration. The type number shall be assigned at compilation time and it can be embedded in the typed persistent pointer by modifying the TOID_DECLARE() macro:
|
|
The type id may be obtained using the sizeof () operator both from type and an object:
|
|
The declaration of such typed persistent pointer may look like this:
|
|
It is also possible to use macros or enums to declare a type id:
|
|
This solution requires to assign the type id explicitly at declaration time. Since the set of types allocated from the pmemobj pool is well known at compilation time it is possible to declare all types by declaring a pool’s layout without explicitly assigning the type id. The layout declaration looks like this:
|
|
Using such declaration of layout all types declared inside the POBJ_LAYOUT_BEGIN() and POBJ_LAYOUT_END() macros will be assigned with consecutive type ids.
Summary
The following table contains a summary of both described solutions:
Feature | Anonymous unions | Named unions |
---|---|---|
Declaration | + | - |
Assignment | - | + |
Function parameter | - | + |
Type numbers | - | + |