Description
This is just an idea that I thought I'd ask about before investing too much time in expanding it further.
It seems to me that the runtime could be allowed to do quite a lot more that it is doing and allow more fine-grained end-application configuration. And it can also simplify the code generator.
C++ standard detection
For example, rather than adding options to the compiler to enable C++14 support here:
case UniqueAndRawPointers =>
s"std::unique_ptr<std::vector<$cppElType>>(new std::vector<$cppElType>())"
// TODO: C++14 with std::make_unique
}
The (default) runtime can supply:
namespace kaitai {
template<typename T>
std::unique_ptr<std::vector<T>> create_vector() {
#if __cplusplus == 201103L
return std::unique_ptr<std::vector<T>>(new std::vector<T>());
#else
return std::make_unique<std::vector<T>>();
#endif
}
} // namespace kaitai
and the compiler can be just:
s"kaitai::create_vector<T>()"
Reservation
While this can be done in the compiler, it also makes this more readable as it's just normal C++:
template<typename T>
std::unique_ptr<std::vector<T>> create_vector(size_t reserve) {
auto vec = std::make_unique<std::vector<T>>();
vec.reserve(reserve);
return vec;
}
Example: this appears twice as fast for a large array. A user can opt to pre-size the array rather than reserving, as long as they can tolerate the array being underfilled if the stream ends prematurely.
const int l_substructs = ((64 * 1024) * 1024); m_substructs = new std::vector<uint32_t>(l_substructs); for (int i = 0; i < l_substructs; i++) { (*m_substructs)[i] = m__io->read_u4be(); }
Custom implementations
This also allow the user to implement their own construction functions by adapting their runtime, transparently to the generated code. For example, if the default runtime implementation is
template<typename T>
std::unique_ptr<std::vector<T>> create_vector() {
return std::make_unique<std::vector<T>>();
}
The user can provide their own version for a specific type. In particular, they can provide a custom allocator, which might be especially handy for types like uint8_t where they can pre-allocate a pool. Or they can implement memory usage tracking.
template<>
std::unique_ptr<std::vector<T>> create_vector<uint8_t>() {
return std::make_unique<std::vector<T, ByteAllocator>>();
}
You could even provide a template over the parent type, so that the user can also choose to only provide custom implementations for certain structs:
template<typename PARENT_STRUCT, typename T>
std::unique_ptr<std::vector<T>> create_vector() {
return std::make_unique<std::vector<T>>();
}
User contexts
This would also allow users to inject their own logic into the runtime as they want, without the compiler needing to be aware, and without the default implementation needing to be anything non-trivial, so mostly it optimises away in the default case.
For example, a user can log global vector creation counts:
static size_t vector_creations = 0;
template<typename PARENT_STRUCT, typename T>
std::unique_ptr<std::vector<T>> create_vector() {
vector_creations++;
return std::make_unique<std::vector<T>>();
}
Passing the root kstruct in would allow the user to have a context per top-level struct.
And if the function took the parent, it could walk the tree for the struct location in the hierarchy:
template<typename PARENT_STRUCT, typename T>
std::unique_ptr<std::vector<T>> create_vector(kaitai::kstruct* p__parent)
return std::make_unique<std::vector<T>>();
}
And the struct creator is similar:
template<typename PARENT_STRUCT, typename ROOT, typename T>
std::unique_ptr<T> create_struct(kaitai::kstream* p__io, PARENT_STRUCT* p__parent, ROOT* p__root) {
return std::make_unique<T>(p__io, p__parent, p__root);
}
It seems that such a runtime would still only be a small handful of functions by default, and by default they'd just collapse into what we currently have.. Perhaps simply these few functions:
- vector create/delete
- struct create/delete