-
Notifications
You must be signed in to change notification settings - Fork 3.8k
DAWN-478 ⁃ Dynamic Tables #354
Comments
Things that are missing from this:
I would find your code above more readable if you removed std:: everywhere and spaced things out a bit more. |
It may not be possible to do 0 copy because the WASM code can only access its address space. That said, I think we can do 1 copy rather than 2 copies and we can avoid dynamic memory allocation for parsing types from the DB. Unfortunately, for dynamically sized types created by WASM code we will need to normalize and ensure it is all located in contiguous memory before we can store it. Is your code checked in anywhere it can be reviewed? |
I have been redesigning a few things in my implementation to meet some of the requirements I have learned are necessary for the type system to be used in EOS.IO. Also, I continued to add the functionality needed to compare instances of compatible types, which is critical to make Dynamic Tables work, ABIInitially, I just had a So now I have designed a tentative ABI that can be passed to the EOS.IO platform. I haven't written up a formal description of it yet, but just to get an idea of what it currently looks like, here is the definition of the
Much of it is self-explanatory, but a few points are worth mentioning. The The ABI includes extra information that I did not bother including before such as the name of the structs and the name of their fields. This information is not necessary to make dynamic tables work nor to allow interoperability between the contract code and the EOS.IO platform. It is only there to allow the platform to convert the structs to/from JSON. I believe the ABI should have everything needed to be able convert the objects/keys of the table (and any types they contain within) to/from JSON dynamically (although I have not yet started working on the actual implementation of that). A table must specify one struct as its object type. A given struct type cannot be used in more than one table. The table inherits the name of the struct object it is defined for. A table can have 1 or more indices (currently I have a hard cap of 255 indices for a table). The index specifies a This Comparing compatible objects according to sorting specification of a table indexThe table indices specified in the ABI above tell the Dynamic Table system how to compare two table objects according to a custom sorting rule. It is as if the I have implemented both of these comparison algorithms for this new type system. They are accessible through two new methods of the
They both take references to the raw data (handled in the
Updated code showing new interface and new featuresOnce again, the interface of the C++ implementation will likely change. It already has changed a bit since last week. Now the global As before the The idea behind these macros and the tools provided in the current C++ implementation is that they can be used in an executable (which runs on the contract developer's machine) that automatically discovers the types used in the contract and generates the serialized The executable could also generate the C++ files to be included by the contract code which provides the serialization/deserialization code to allow the contract to interface with the Dynamic Table system conveniently. While the current implementation is capable of generating that serialization/deserialization code during C++ compilation (and the metadata that compiled code needs to do its job needs to be generated during an initialization stage at runtime) that may not ultimately be a viable approach since a lot of that code uses exceptions and C++ STL and Boost libraries, which apparently will not be available in the WebAssembly environment, So, it may just be easier to do the harder "type compilation" in a fuller C++ environment (for developer convenience) and generate much simpler serialization/deserialization C++ code that can be compiled to WebAssembly and run in that environment without problems. Note: Even with this code generation approach, the strict WebAssembly environment puts a lot of developer burden on the implementation of the serialization/deserialization tools. First, all the existing error handling in my serialization/deserialization implementation has to be redesigned to be C-style, since apparently exceptions are not allowed. Second, I at least need a vector class; so a minimal vector class similar to the C++ STL vector needs to be reimplemented for the WebAssembly environment (this of course means we need malloc and free #129). Third, some of the type traits feature of the STL need to be reimplemented. And all of this is assuming optional and variant types are not initially supported in the serialization/deserialization implementation (though they would still be supported by the Dynamic Tables system). If optional and variant types were to be supported as well, that would require even more reimplementation of existing libraries. Example code:
That code generates the following output:
Sorry for posting all this code in the GitHub issue. I am getting close to cleaning up what I currently have and posting it in some repo for review (though I expect a lot of what I currently have to change a lot). For the time being it will be easier to just keep it in separate repo (not eos) as I redesign/refactor, test, and finish implementing the remaining features to actually demonstrate a Boost.MultiIndex table sorting dynamic objects. Then after that I will be figure out how to integrate it into eos code base and ChainBase. Tasks to do
|
Today I cleaned up some of my existing code, found and corrected a few bugs, and added serialization/deserialization support for the I also wrote another example program demonstrating these new features:
That code produces the following output:
Over the next day or two I should be getting a repo up with the current code. |
I cleaned up the existing code and put it up on GitHub. You can find the repository here: https://github.com/arhag/eos-dynamic-tables My work on this issue will continue within that repository, so if you are interested in the details of the progress of that work, make sure to check out the commits there. However, I will still make comments here (though less frequently) summarizing larger changes and milestones achieved regarding this issue. |
* Modularized `intialize_types` to allow more than one subset of the types available to the program to be initialized into their own ABI. This was needed to bootstrap serializing the ABI itself, but also will be helpful in later unit tests. * Changed deserializer for custom builtins to use conversion constructor (and copy/move assignment operator) rather than the custom assignment operator I was initially using to allow the same code to work with enumerations as well. * Added support to reflect pairs/tuples which are just treated as structs (using mangled C++ type name) with anonymous fields (in ABI the fields are named f0, f1, ... ). * With the above changes, I had the necessary tools to reflect `ABI`. Now the `ABI` struct is reflected with the same type system which it can describe, and so a serialized version of a contracts ABI can be included in a transaction (as a possible alternative to using `fc::pack` and `fc::unpack`). I also demonstrated constructing the `types_manager` from a serialized ABI by modifying the `reflection_test1` example.
…s struct) (for EOSIO/eos#354): * Modified ABI to allow tuple types which do not require specifying a name for the type nor names for the "fields". * Updated types_constructor to support the new tuple type. * Updated reflection macros to still automatically work with std::pair and std::tuple but now without depending on implementation-specific C++ type names. * Found and corrected a small bug in `abi_constructor::add_variant`. * Tidying up.
* `types_constructor` now creates two different versions of types managers: - One is a minimal one, `types_manager`, which has just what will be needed by EOS.IO to compare objects and make dynamic tables work on the platform side. - The other is the full one, `full_types_manager`, which has enough information to do serialization/deserialization between raw data and C++ types, as well as serialization/deserialization between raw data and JSON (to be implemented later). * Both versions have a lot of common functionality, and this functionality is contained in the class `types_manager_common` which both versions inherit. * The C++ contract code would need a types manager version that has all struct fields (which `types_manager` does not) rather than just the sorted fields. Right now that is `full_types_manager` but it also includes extra information that are not needed for serialization/deserialization between raw data and C++ types and so it is information that the C++ contract code would presumably not need (such as field names). I am considering adding a third (middleground) version of types manager to be used exclusively by the C++ contract code. Maybe this third version will be very different in implementation than the other two (perhaps the source might even be automatically generated by a tool) in order to satisfy the stricter programming constraints imposed by the WebAssembly environment.
* Fixed bug where a missing static keyword led to undefined behavior which happen to result in all sorted fields being treated as if they were all specified in the ascending order. * Now `full_types_manager` has field names and sort order information. * This enables more informative printing of types, which was in fact implemented along with making the printed struct formatting look nicer. * While printing the structure of types is not super important, the above changes provide the framework necessary for conversion to/from JSON using just: the raw data, the `type_id` associated with that raw data, and the information available within the `full_types_manager` class.
I just want to remind any interested observers of this issue that you can look at the messages of commits in my While my cross-referenced commit messages are pretty descriptive, it may sometimes be worth it to go over the current state of dynamic tables project in more detail every once in a while as a comment on this issue. The last time I did this was a little over two weeks ago, so it is probably a good time to do that now. ABIThe ABI has been slightly changed since two weeks ago:
It now includes a tuple type, which is specified in a very similar way (within the ABI data structure) as the variant type. However, remember that the variant is a sum type, meaning an instance of it can only have one of the available case types at a time, whereas the tuple is a product type (similar to a struct), meaning any given instance of the tuple type (or struct type) involves all of the specified sequence of types (they make up the "fields" of the product type). Prior to this change in the ABI, tuples/pairs were supported (and in fact the serialization/deserialization implementation for them was done 6 days ago in commit 8a095d0) but they had to be treated as structs. This meant giving the type a name and also giving its fields names (my implementation chose the names More features in serialization/deserialization toolThe C++ code which handles serialization of C++ types to raw data (compatible with the new EOS.IO type system) and deserialization of that raw data back to C++ types has been greatly improved over the past couple weeks. Modularize
|
…ork in restricted C++ environment (for EOSIO/eos#354)
… standard lib to work in restricted C++ environment (no exceptions, no standard lib) in preparation for later getting the serialization/deserialization code to run within WebAssembly (for EOSIO/eos#354)
@arhag @bytemaster @thomasbcox Pretty cool idea. Can you wrap EOS with python and use standard lib numpy/pandas/vector furthermore generate/store these data type using some sort of serialization? |
@arhag is this done and the issue can be closed? |
@arhag , @wanderingbort, or @heifner My plan is to close this, since if it has any validity still, that small portion should be removed and captured in a new ticket since this is a very large writeup. Please indicate if you disagree. |
Code has gone through multiple refactorings since this was written, so it is OBE |
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 aboost::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 likebool
. 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 existingfc::pack
andfc::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 singleuint32_t
somewhere within astruct
, it would be forced to firstunpack
the entirestruct
(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 simplestruct
s 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 astruct
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 Cstruct
(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 simplestruct
(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 thestruct
. Whereas with Cap'n Proto, the raw 32 bytes of data of the hash would be stored further away from data of thestruct
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
struct
s. Note thatstruct
s are allowed to inherit from at most one otherstruct
.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 astatic_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 anAny
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 thestruct
or container in which theAny
belongs in, and the actual field withinstruct
or container is a 64-bit word which not only holds the offset of the actual type instance but also atype_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 actualstruct
or container theVector
type exists in. The actual field of theVector
type within thestruct
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 thestruct
as a possible case (again, in all cases, either directly or indirectly). However, vectors (andAny
types) are the exception to this rule. Astruct
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
set
s andmap
s. 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
andint16_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 before2
in ascending order. But if all 32-bit integers were treated the same (ignoring its sign) then2
(or0x0002
when represented as auint16_t
) would come before-1
(or0xFFFF
when represented as auint16_t
) in ascending order. It is also the reason for aRational
builtin type. From a size/alignment perspective only, a contract developer could use:But the EOS.IO type system would sort this in lexicographical order. So, for example, the rational
4/5
would be sorted before5/8
in ascending order (even though0.8 > 0.625
) if a contract developer used the customnot_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 usingvector<pair<key, value>>
(thepair
type is just another product type and could be represented with a customstruct
). Even if themap
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 likevector<pair<key, value>>
sorted in the appropriate order. The EOS.IO system would then allow comparisons between instances ofvector<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 emptyAny
s are equal; an emptyAny
is less than a non-emptyAny
; if comparing two non-emptyAny
s of different types, the comparison is based on the comparison of theirtype_id
s (definition oftype_id
comparison will be left for later); and, if comparing two non-emptyAny
s of the same type, the comparison is delegated to the comparison function of the actual type of theAny
s.The
Bytes
andString
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 samestruct
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 astruct
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 theTypeConstructor
object as well as to add tables. Here is look at the interface for some of the relevant methods: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
:The
field_metadata
structure includes atype_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. TheTypeManager
object can be extracted from theTypeConstructor
object assuming that all the types (as they are constructed within theTypeConstructor
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 theTypeConstructor
appropriately.While I still have changes planned for the interface of this mechanism, I can show a small example of how it works today:
And that code generates the following output:
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.The text was updated successfully, but these errors were encountered: