-
Notifications
You must be signed in to change notification settings - Fork 3.7k
DAWN-478 ⁃ Dynamic Tables #354
Description
Background
Currently, EOS.IO is required to support a table schema for all the schemas a contract developer may want to use. Actually, the schema can ignore any "columns" of the table that are not acting as part of an index for the table. The EOS.IO system only needs to understand the type of the object of a table to the extent that it needs to be able to sort it for the table's indices. The rest of the fields (or "columns") of the object can be treated as a dumb vector of bytes by the EOS.IO system, and they only have meaning to the contract code.
So, for example, currently EOS.IO supports three types of tables. The first is a table supporting a single 64-bit key. The second is a table supporting two 128-bit keys. The third is a table supporting three 64-bit keys. The second table, as an example, provides two indices: the first sorts according to the primary key having priority over the secondary key (meaning the secondary key is used only to break ties in the primary key); the second sorts according to the secondary key have priority over the primary key.
Problem and Motivation
If the existing tables do not meet the requirements of contract developers, they are either out of luck or EOS.IO developers need to add a new table type to support the contract developers' needs and upgrade EOS.IO (possibly as a hard fork?) to allow use of that new table type.
For example, say a developer wanted to sort an object by a primary key in descending order and break ties with a secondary key in ascending order. In this example, the primary key could perhaps be a timestamp and the secondary key an account name. A contract developer could using hacky solutions to implement this with the currently available table types. For example, they could use the table with three 64-bit keys, and in particular use the second index of that table that sorts according to the secondary key in ascending order and then breaks ties with the tertiary key (again in ascending order). The contract developers would have to negate the timestamp (so that they can approximate descending order using a key that is sorted according to ascending order) and store it in a 64-bit key (wasting 32-bit of space). The primary key would also be entirely wasted. Finally, the account name would be stored as the tertiary key.
In the previous example, the contract developer was able to find a solution (though a very ugly one) with the existing table types. But what if they really needed a table with 3 128-bit indices? Or what if they needed to be able to store a more general string (not just the restricted strings that can be stored as uint64_t Names) that was sorted in lexicographical ordering? Then they would be out of luck, unless EOS.IO developers added a specific table type to meet their needs.
The EOS.IO platform would be far more powerful and useful to contract developers if it had dynamic table support with a sophisticated type system backing it which allows the contract developer to specify very permissive table schemas in their ABI and makes it possible for the EOS.IO system dynamically support it at run-time.
This document describes a design of a possible system that achieves that goal.
Table implementation
Currently, EOS.IO tables are implemented using the Boost.MultiIndex library. The new dynamic tables proposed in this document will continue using Boost.MultiIndex to implement the tables. This can be possible despite the dynamic nature of the schema because of a few features of Boost.MultiIndex.
One feature that could be of use in implementing dynamic tables using Boost.MultiIndex, is user-defined key extractors. A user-defined key extractor is a functor that receives a const reference to the object being considered within the table and returns another object of the key type. Since each instantiation of a boost::multi_index_container can have its own unique instantiation of the key extractor, it is possible to provide a functor that knows how to deal with the particular type associated with a given dynamic table, even if at the C++ compilation level Boost.MultiIndex treats all the object types as a dumb vector of bytes.
The key extractor does have to return the same particular type (known during compilation) for all the dynamic tables. So the object it returns would itself need to contain a vector of bytes holding the actual instance of the dynamic object but also the metadata needed to know how to compare it against other dynamic objects of the same dynamic type.
Using a user-defined key extractor as described above is one approach to implementing dynamic tables, but is not the approach taken for the proposal in this document. The user-defined key extractor approach requires copying bytes from the dynamic object instance to the dynamic key instance each time a key must be extracted by Boost.MultiIndex. And Boost.MultiIndex extracts a key for each object in boost::multi_index_container that it needs to do a comparison with in order to figure out where to place a new (or modified) object within the container or to simply find an existing object within the container. This approach would add unneccessary overhead and hurt performance.
The approach proposed in this document is to instead take full advantage of another feature of Boost.MultiIndex: custom comparison predicates. Custom functors are specified as part of the definition of the boost::multi_index_container and associated with each index (instead of using a user-defined key extractor, the identity extractor is used for all indices). This functor know how to compare two dynamic objects within the context of some other key type. In other words, although it may be comparing two dynamic objects of type T, it is really using the sorting information of a key type K (in which a definition for how type K can "look into" type T is provided in the ABI) to determine how to compare the two objects of type T. The functor instance needs to be made specific for not only for the table object type T but also for the key type K associated with a particular index, which means it needs access to all the metadata generated from the ABI for a K-type-shaped window into T. When instantiating a boost::multi_index_container, ctor_args_list is used to match the particular instance of the functor to the appropriate index of the container.
This approach works well when adding new objects or modifying existing ones. But one other piece of the puzzle is needed to allow lookups. A contract developer trying to find some object (or get an iterator to some point within a table index) would almost never want to specify the entire object as the lookup key. Instead other key types are used for lookups, and in fact each index of a table has a specific key type that must be used to do lookups (which of course must be specified as part of the contract ABI). To support lookups by custom keys, this proposal suggests the use of another Boost.MultiIndex feature: special lookup operations.
Special lookup operations allow specifying an alternative comparison predicate for lookups within an index as long as they follow a compatible sorting criteria. Since these comparison predicates would be generated by the EOS.IO system from the contract ABI, it can guarantee that the sorting criteria is compatible with that of the index the lookup is being done within. This alternative comparison predicate is another functor that allows comparing two different types. In this case, the two different types would actually be the same type (at the C++ compilation level): a dynamic object. However, one of them would be representing the object type of the table and the other would be representing the type of the key associated with the particular index the lookup is being done within. To properly compare the two instances of dynamic objects, the functor would need metadata for the key-type-shaped window into the table object type (much like in the case of the custom comparison predicates described earlier) as well as the metadata for the key type itself.
New EOS.IO type system
From the previous section, it should be clear that metadata is required which describes the various types involved in the dynamic tables. There should be sufficient metadata to allow the comparison predicates to do their job in comparing two compatible dynamic types.
Though this document has not yet described what sort of types will be supported (that will be discussed shortly within this section), it should be clear that the comparison predicates will eventually need to compare certain primitive types with each other. These primitive types include types such as: int8_t, uint8_t, int16_t, ..., uint64_t, and possibly additional types like bool. The way the values of these types are stored in the raw vector of bytes has a significant impact on the performance of the comparisons (and thus on things like table lookups or adding new objects to a table). Using the existing fc::pack and fc::unpack to, respectively, serialize types to and deserialize types from the raw vector of bytes would lead to very bad performance. Even if the comparison predicate needs to compare a single uint32_t somewhere within a struct, it would be forced to first unpack the entire struct (for both the left-hand side and right-hand side objects of the comparison) to then just compare integers.
Instead, the proposed design in this document takes inspiration from serialization libraries like Cap'n Proto such that the serialization outputs a vector of bytes which allow direct memory mapping of the data to a struct (at least for simple structs that do not involve dynamically-size vectors). This means care has to be paid to how the serialized types are actually laid out in memory so that the a particular integer type has appropriate alignment for fast access by the CPU. The layout algorithm for this design retains the flexibility to reorder fields within a struct type defined by a contract developer for more efficient packing (less padding required for alignment reasons), however, it still has strict constraints imposed to allow a contract developer to map the raw byte data of the serialization to a compatible C struct (these structs could be automatically generated from the ABI using code generators) without any deserialization overhead. Note that a good reference that I used to better understand C struct packing is: The Lost Art of C Structure Packing. Again following the design of Cap'n Proto, reader helper functions would still be necessary to traverse through the serialized type when it is more complicated than a simple struct (for example if it has dynamically-size vectors or sum types such as variant or optional), but this would only add very minimal overhead.
As a side note, I looked into just using Cap'n Proto for the type system rather than building a new one, but ultimately decided it was not the best choice. Cap'n Proto itself does not handle any comparison predicates automatically, so all of that custom code would need to be written anyway. That comparison code would then be forced to deal with the Reader interface of Cap'n Proto and deal with all of the edge cases of Cap'n Proto types that we would not care to support in EOS.IO (Cap'n Proto types have some additional features than the EOS.IO type system design proposed in this document does not). The Cap'n Proto C++ type compiler would have to be heavily modified to disallow the usage of the additional features we do not wish to support (including floating point types). So in terms of developer time it was not at all clear that using it would provide an advantage. In addition, Cap'n Proto does not currently support statically-sized arrays, which I thought was an important feature to have as part of the EOS.IO type system. Statically-sized arrays would need to be implemented as dynamically-sized lists in Cap'n Proto which comes with additional overhead (both in memory and in computation time) and due to the way the layout works would likely mean more cache misses. Statically-sized arrays can be particularly useful when using it as a fixed-size byte array to hold some type that is not directly supported in the EOS.IO type system. For example an std::array<uint8_t, 32> could be used as a substitute for a SHA256 hash. A contract developer may wish to store SHA256 hashes in a table with an index guaranteeing uniqueness (in that case comparison of the byte array would be done according to lexicographical sorting). In the EOS.IO type system design proposed in this document, storing such a hash would just take 32 extra bytes (ignore potential padding for alignment requirements) and be stored inline within the struct. Whereas with Cap'n Proto, the raw 32 bytes of data of the hash would be stored further away from data of the struct and at least 16 extra bytes (two 64-bit words) would be needed as overhead.
Supported types
The new EOS.IO type system proposed in this document supports a few classes of types.
The first class are the builtin types which include the primitive types (like the various integers and bool) and non-primitive types that are added because of the usefulness they are predicted to provide to contract developers, such as String (a NUL-terminated C string), Bytes (a vector of bytes), Rational, and larger sized integers, to name a few.
The second class are product types (to use the jargon of algebraic data types) which are essentially just structs. Note that structs are allowed to inherit from at most one other struct.
The third class are sum types (again using the jargon of algebraic data types): for example a variant (actually a static_variant) or an optional. The variant and optional types are laid out in-place, meaning there is enough space allocated with an appropriate alignment to fit the worst-case type that may be set for that sum type. For that reason, the variant has to be a static_variant, meaning that all of its possible case types must be specified as part of the ABI. For a "dynamic variant" a contract developer would instead want to use an Any type, which is actually part of the builtin class of types.
In the case of the Any type, the actual type it is instantiated to is located outside of the struct or container in which the Any belongs in, and the actual field within struct or container is a 64-bit word which not only holds the offset of the actual type instance but also a type_id which identifies which type the instance has.
The fourth and final class are homogeneous contiguous list containers of which there are two: an array container (which has a fixed size defined in the ABI) and a vector container (which is dynamically-sized). As mentioned earlier, arrays are stored in-place (within a struct or another container) but vectors are not. Vector data is stored somewhere in the raw serialized output but away from the actual struct or container the Vector type exists in. The actual field of the Vector type within the struct or container is a 64-bit word which holds the number of elements in the vector and the offset to the start of the actual data of the vector.
The type system prevents cycles that would lead to infinite sizes. So, for example, a struct cannot inherit from itself or contain itself (either directly or indirectly). It also cannot contain an array of itself, an optional of itself, or a variant that includes the struct as a possible case (again, in all cases, either directly or indirectly). However, vectors (and Any types) are the exception to this rule. A struct can contain as a field a vector of itself. This is because the vector only takes up a 64-bit word within the actual struct and that 64-bit word contains the size of the vector and the offset to the start of the vector data (the data of the vector is stored elsewhere within the serialized data).
Notice that there is no support for containers such as sets and maps. The EOS.IO type system proposed in this document only concerns itself with the size/alignment and comparison aspects of types.
First and foremost, the type system wants to know how much space it needs to hold the instance of the type and what its alignment requirements need to be for fast access to the instance. It also cares about how EOS.IO is supposed to compare two instances of the same type, which is why, for example, there is both a type for uint16_t and int16_t. Both of those types have the same size and alignment requirements, but they have different comparison requirements. A contract developer wants to know that, for example, -1 is sorted before 2 in ascending order. But if all 32-bit integers were treated the same (ignoring its sign) then 2 (or 0x0002 when represented as a uint16_t) would come before -1 (or 0xFFFF when represented as a uint16_t) in ascending order. It is also the reason for a Rational builtin type. From a size/alignment perspective only, a contract developer could use:
struct not_quite_a_rational
{
int64_t numerator;
uint64_t denominator;
};
But the EOS.IO type system would sort this in lexicographical order. So, for example, the rational 4/5 would be sorted before 5/8 in ascending order (even though 0.8 > 0.625) if a contract developer used the custom not_quite_a_rational struct rather than using the EOS.IO rational type. As one can imagine, this would be a major problem if those rationals were, for example, representing prices in an order book table of an exchange contract.
The focus on just the size/alignment and comparison aspects of EOS.IO types however means that certain types that one is used to using in programming languages do not actually need a corresponding EOS.IO type. For example, a map can be represented using vector<pair<key, value>> (the pair type is just another product type and could be represented with a custom struct). Even if the map type is implemented using a red-black tree within the programming environment of the contract code, when it comes time to serialize it to a raw vector of bytes, it would be treated like vector<pair<key, value>> sorted in the appropriate order. The EOS.IO system would then allow comparisons between instances of vector<pair<key, value>> using the straightforward lexicographical ordering, which is good enough if all the contract developer cares about is (as an example) ensuring that each map in the table is unique. It usually does not make sense to compare two sets or two maps for some purpose other than checking for equality, hence the EOS.IO type system proposed in this document does not bother with supporting sets and maps through some special type with a special comparison predicate.
Comparisons between instances of the supported types
Each of the supported types has an appropriate comparison defined.
The array and vector types are sorted in lexicographical order, in which the comparison function of their element type is used to figure out how to compare any two elements within the lists.
The optional type is sorted as follows: two empty optionals are equal; an empty optional is less than a non-empty optional; and, if comparing two non-empty optionals the comparison is delegated to the comparison function of the contained type of the optional.
The variant type is sorted as follows: if the case index of two variants are not the same, the one with the smaller case index is less than the one with the larger case index; otherwise, if both variants have the same case index, then the comparison is delegated to the comparison function of the contained type of the variants.
The Any type is sorted as follows: two empty Anys are equal; an empty Any is less than a non-empty Any; if comparing two non-empty Anys of different types, the comparison is based on the comparison of their type_ids (definition of type_id comparison will be left for later); and, if comparing two non-empty Anys of the same type, the comparison is delegated to the comparison function of the actual type of the Anys.
The Bytes and String types of the builtin types class are sorted in lexicographical order similar to the array and vector types.
The primitive integer types (and bool) of the builtin types class are sorted in the usual way integers are sorted.
The rational type of the builtin types class is sorted according to: lhs.numerator * rhs.denominator < rhs.numerator * lhs.denominator.
Struct types are defined in the ABI with a particular sorting specification. The sorting specification describes which members of the struct are to be compared (in a particular order) and in which manner (ascending or descending order) when comparing two instances of the same struct type. In this case, "members" can refer to either one of the fields of the struct or the base of a derived struct.
Table specification
Tables are specified in the ABI by specifying a particular struct type as the main object of the table, and specifying one or more table indices. The table indices specify another type (either a struct type or a type from the builtin class) which acts as the key type for that index. The ABI for the table index also specifies whether it is a unique or non-unique index and whether to reverse the sorting order of the key type (i.e. make it in descending order rather than its default of ascending order).
In order for the table index to specify a key type different from the main object type of the table, the table index specification also needs to include a mapping between the main object type and the key type. This mapping associates to each of the sorted members of the key type a particular member of the main object type. This mapping provides the metadata needed to get a "key-type-shaped window into the table object type."
Contract developer interface
The goal is to provide a convenient interface to the contract developers to handle most of the complexity of this type system for them. A contract developer should be able to use the regular types of their programming environment and just add some extra metadata to them (either through annotations or perhaps some macros) to reflect those types to their corresponding EOS.IO types and provide the sorting specification when appropriate. Then tools provided by EOS.IO developers would take this information and automatically generate the ABI for the contract as well as the interfaces in the contract developers programming environment that allows the developers to read/create/modify and generally interact with these EOS.IO types and the dynamic table system.
One possible approach, and the approach taken so far in the implementation of this system, which is specific to a C++ contract development environment, is to use macros and advanced C++ template features to generate code during regular compilation that automatically discovers the types used by the contract developer and their association with corresponding EOS.IO types, and provides a type initialization function (to be called during the init stage of the EOS.IO contract) which automatically generates a TypeManager object to help handle the management of the types.
In particular, the TypeManager object along with the helper functions that are automatically generated during contract compilation allow the contract developer to easily deserialize to the C++ types that they use from the raw vector of bytes representing the type instance that is provided by the EOS.IO system via the WASM interface. It also allows the contract developer to take the C++ types they use and serialize them to the raw vector of bytes which they then pass to the EOS.IO system via the WASM interface (for example to create a new object in the table or modify an existing one).
Other more sophisticated interfaces for dealing with the EOS.IO types are possible as well, which can provide a more performant way of accessing or manipulating the data under certain situations. There could be support added for immutable access to the type via the raw data directly, meaning no deserialization step at all (this is similar to the approach taken with Cap'n Proto).
Another option that could be supported is mutable window into the type via the raw data (again no deserialization step). In this case, though there would be mutability, it would be a limited mutability. In particular, no vectors would be allowed to be resized, even though the contents of the vectors could be swapped and modified as the contract developer wishes. This would be a very useful option for parts of contracts that, for example, just need to modify a few primitive fields in a struct and do not bother with more complicated types such as vectors. For this case, the contract developer gets a big performance boost by using this limited mutability interface rather than going through the overhead of deserialization and reserialization. Instead the raw vector of bytes received from the EOS.IO system is modified in-place directly by the contract and then returned back to the EOS.IO system to actually make the change occur in the database.
But if the contract developer needs to modify an existing object in a way that requires changing the size of vectors contained within the object, they would be forced to go through the deserialization step so that they have a normal C++ object they can manipulate as they wish, before eventually serializing back to the raw vector of bytes that is passed via the WASM interface to the EOS.IO system.
The ability to deserialize from raw data to a C++ type and then reserialize back to raw data is the more general approach that will work in all situations. The additional interfaces of the immutable access interface and the limited mutability interface are nice features to eventually have because they provide a performance benefit to contract developers. But since the advantage they provide is only in performance, it is not a priority to support those interfaces in the beginning. Those interfaces can be added later. The EOS.IO type system proposed in this document is already designed to support these interfaces and so adding them would not require a redesign or a hard fork. In fact, EOS.IO developers need not even be the ones to add such interfaces. These can be added by third-party developers as better tooling to support contract developers in their favorite programming environment (assuming it compiles to WebAssembly of course).
Implementation done so far
As of September 5, 2017, I have developed a C++ implementation for most of this new type system. One important class in the implementation is TypeConstructor. TypeConstructor provides an interface to add various types into the TypeConstructor object as well as to add tables. Here is look at the interface for some of the relevant methods:
struct table_index
{
type_id key_type;
bool unique;
bool ascending;
vector<uint16_t> mapping;
};
type_id::index_t forward_declare(TypeNameConstRef declared_name);
type_id::index_t add_struct(TypeNameConstRef name, const vector<pair<type_id, int16_t>>& fields, type_id base = type_id(), int16_t base_sort = 0 );
type_id::index_t add_array(type_id element_type, uint32_t num_elements);
type_id::index_t add_vector(type_id element_type);
type_id::index_t add_optional(type_id element_type);
type_id::index_t add_variant(const type_list& cases);
type_id::index_t add_table(type_id::index_t object_index, const vector<table_index>& indices);
This is an interface that could be used by, for example, compiler plugins, or, in the case of my implementation, by macros and C++ template visitors to actually build the metadata for the set of types used by the contract. That metadata is held as two vectors in another class called TypeManager:
class TypeManager
{
vector<uint32_t> types;
vector<field_metadata> fields;
// Rest omitted...
};
The field_metadata structure includes a type_id (which is represented in a 32-bit number) and an additional 32-bit of data storing the offset of the field as well as sorting information. The TypeManager object can be extracted from the TypeConstructor object assuming that all the types (as they are constructed within the TypeConstructor object) validate.
The TypeManager object has all the metadata needed by the EOS.IO system to do comparisons between dynamic objects and implement dynamic tables.
The TypeManager object also has the metadata needed by the contract code to serialize and deserialize C++ objects to and from raw data.
Rather than using TypeConstructor directly, my implementation uses macros and template visitors to automatically discover the relevant types (after being given some information by the contract developer via macros) and create a type initialization function that can be called to build the TypeConstructor appropriately.
While I still have changes planned for the interface of this mechanism, I can show a small example of how it works today:
#include <eos/types/reflect.hpp>
#include <iostream>
#include <iterator>
using std::vector;
using std::array;
struct type1
{
uint32_t a;
uint64_t b;
vector<uint32_t> c;
vector<type1> v;
};
struct type2 : public type1
{
int16_t d;
array<type1, 2> arr;
};
EOS_TYPES_REFLECT_STRUCT( type1, (a)(b)(c)(v), ((c, asc))((a, desc)) )
// struct name, struct fields, sequence of pairs for sorted fields (field name, asc/desc) where asc = ascending order and desc = descending order.
EOS_TYPES_REFLECT_STRUCT_DERIVED( type2, type1, (d)(arr), ((d, desc))((type1, asc)) )
// struct name, base name, struct fields, sequence of pairs for sorted members (as described above), however notice the base type name can also be included as member
EOS_TYPES_REGISTER_STRUCTS((type2))
// Only need to bother registering type2 because the reflection system will automatically discover type1 since type2 uses it.
int main()
{
auto tc = eos::types::initialize_types();
std::cout << "type1:" << std::endl;
auto type1_index = eos::types::get_struct_index<type1>();
tc.write_type(std::cout, eos::types::type_id::make_struct(type1_index));
auto sa1 = tc.get_size_align_of_struct(type1_index);
std::cout << "Size: " << sa1.get_size() << std::endl;
std::cout << "Alignment: " << (int) sa1.get_align() << std::endl;
std::cout << "Sorted members (in sort priority order):" << std::endl;
for( auto f : tc.get_sorted_members(type1_index) )
std::cout << f << std::endl;
std::cout << std::endl;
std::cout << "type2:\n";
auto type2_index = eos::types::get_struct_index<type2>();
tc.write_type(std::cout, eos::types::type_id::make_struct(type2_index));
auto sa2 = tc.get_size_align_of_struct(type2_index);
std::cout << "Size: " << sa2.get_size() << std::endl;
std::cout << "Alignment: " << (int) sa2.get_align() << std::endl;
std::cout << "Sorted members (in sort priority order):" << std::endl;
for( auto f : tc.get_sorted_members(type2_index) )
std::cout << f << std::endl;
std::cout << std::endl;
auto tm = tc.destructively_extract_type_manager();
eos::types::serialization_region r(tm);
type1 s1{ .a = 8, .b = 9, .c = {1, 2, 3, 4, 5, 6} };
type2 s2;
s2.a = 42; s2.b = 43; s2.d = -1;
s2.arr[0] = s1;
s2.arr[1].a = 1; s2.arr[1].b = 2; s2.arr[1].c.push_back(1); s2.arr[1].c.push_back(2);
r.write_struct(s1);
std::cout << "Raw data of serialization of s1:\n" << std::hex;
for( auto b : r.get_raw_data() )
std::cout << (int) b << " ";
std::cout << std::dec << std::endl;
std::ostream_iterator<uint32_t> oi(std::cout, ", ");
std::cout << "Reading back struct from raw data." << std::endl;
type1 s3;
r.read_struct(s3);
std::cout << "a = " << s3.a << ", b = " << s3.b << ", and c = [";
std::copy(s3.c.begin(), s3.c.end(), oi);
std::cout << "]\n" << std::endl;
r.clear();
r.write_struct(s2);
std::cout << "Raw data of serialization of s2:\n" << std::hex;
for( auto b : r.get_raw_data() )
std::cout << (int) b << " ";
std::cout << std::dec << std::endl;
std::cout << "Reading back struct from raw data." << std::endl;
type2 s4;
r.read_struct(s4);
std::cout << "a = " << s4.a << ", b = " << s4.b << ", c = [";
std::copy(s4.c.begin(), s4.c.end(), oi);
std::cout << "]";
std::cout << ", d = " << s4.d << ", and arr = [";
for( auto i = 0; i < 2; ++i )
{
std::cout << "{a = " << s4.arr[i].a << ", b = " << s4.arr[i].b << ", and c = [";
std::ostream_iterator<uint32_t> oi(std::cout, ", ");
std::copy(s4.arr[i].c.begin(), s4.arr[i].c.end(), oi);
std::cout << "]}, ";
}
std::cout << "]\n" << std::endl;
return 0;
}
And that code generates the following output:
type1:
struct type1 /* struct(4) */
{
UInt32 f0; // Sorted in descending order
UInt64 f1;
Vector<UInt32> f2; // Sorted in ascending order
Vector<T1> /*[T1 = struct(4)]*/ f3;
};
Size: 32
Alignment: 8
Sorted members (in sort priority order):
Vector<UInt32> (sorted in ascending order) located at offset 0x8
UInt32 (sorted in descending order) located at offset 0x18
type2:
struct type2 /* struct(0) */ : type1 /* struct(4) */ // Base sorted in ascending order
{
Int16 f0; // Sorted in descending order
Array<T1, 2> /*[T1 = struct(4)]*/ f1;
};
Size: 104
Alignment: 8
Sorted members (in sort priority order):
Int16 (sorted in descending order) located at offset 0x60
T [T = struct(4)] (sorted in ascending order) located at offset 0x0
Raw data of serialization of s1:
9 0 0 0 0 0 0 0 6 0 0 0 20 0 0 0 0 0 0 0 0 0 0 0 8 0 0 0 0 0 0 0 1 0 0 0 2 0 0 0 3 0 0 0 4 0 0 0 5 0 0 0 6 0 0 0
Reading back struct from raw data.
a = 8, b = 9, and c = [1, 2, 3, 4, 5, 6, ]
Raw data of serialization of s2:
2b 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2a 0 0 0 0 0 0 0 9 0 0 0 0 0 0 0 6 0 0 0 68 0 0 0 0 0 0 0 0 0 0 0 8 0 0 0 0 0 0 0 2 0 0 0 0 0 0 0 2 0 0 0 80 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 ff ff 0 0 0 0 0 0 1 0 0 0 2 0 0 0 3 0 0 0 4 0 0 0 5 0 0 0 6 0 0 0 1 0 0 0 2 0 0 0
Reading back struct from raw data.
a = 42, b = 43, c = [], d = -1, and arr = [{a = 8, b = 9, and c = [1, 2, 3, 4, 5, 6, ]}, {a = 1, b = 2, and c = [1, 2, ]}, ]
As of now, my implementation of the macro reflection shown above does not supporting the following types (although my implementation of TypeConstructor and TypeManager do support these types): variants, Any, Rational, String, Bytes. I also do not yet have support (either at the macro reflection level or the TypeConstructor/TypeManager level) for builtin integers larger in size than 64-bits. Also, as of now, I have not yet completed work on the comparison predicates for Boost.MultiIndex using this new type system needed to make dynamic tables possible, although I have tested using comparison predicates in an earlier and extremely rough prototype just to prove to myself that it was in fact possible to do what I want to do.