|
|
- # Push EnTT across boundaries
-
- <!--
- @cond TURN_OFF_DOXYGEN
- -->
- # Table of Contents
-
- * [Introduction](#introduction)
- * [Named types and traits class](#named-types-and-traits-class)
- * [Do not mix types](#do-not-mix-types)
- * [Macros, macros everywhere](#macros-macros-everywhere)
- * [Conflicts](#conflicts)
- * [Allocations: the dark side of the force](#allocations-the-dark-side-of-the-force)
- <!--
- @endcond TURN_OFF_DOXYGEN
- -->
-
- # Introduction
-
- `EnTT` has historically had a limit when used across boundaries on Windows in
- general and on GNU/Linux when default visibility was set to _hidden_. The
- limitation is due mainly to a custom utility used to assign unique, sequential
- identifiers to different types. Unfortunately, this tool is used by several core
- classes (the `registry` among the others) that are thus almost unusable across
- boundaries.<br/>
- The reasons for that are beyond the purposes of this document. However, the good
- news is that `EnTT` also offers now a way to overcome this limit and to push
- things across boundaries without problems when needed.
-
- # Named types and traits class
-
- To allow a type to work properly across boundaries when used by a class that
- requires to assign unique identifiers to types, users must specialize a class
- template to literally give a compile-time name to the type itself.<br/>
- The name of the class template is `name_type_traits` and the specialization must
- be such that it exposes a static constexpr data member named `value` having type
- either `ENTT_ID_TYPE` or `entt::hashed_string::hash_type`. Its value is the user
- defined unique identifier assigned to the specific type.<br/>
- Identifiers are not to be sequentially generated in this case.
-
- As an example:
-
- ```cpp
- struct my_type { /* ... */ };
-
- template<>
- struct entt::named_type_traits<my_type> {
- static constexpr auto value = "my_type"_hs;
- };
- ```
-
- Because of the rules of the language, the specialization must reside in the
- global namespace or in the `entt` namespace. There is no way to change this rule
- unfortunately, because it doesn't depend on the library itself.
-
- The good aspect of this approach is that it's not intrusive at all. The other
- way around was in fact forcing users to inherit all their classes from a common
- base. Something to avoid, at least from my point of view.<br/>
- However, despite the fact that it's not intrusive, it would be great if it was
- also easier to use and a bit less error-prone. This is why a bunch of macros
- exist to ease defining named types.
-
- ## Do not mix types
-
- Someone might think that this trick is valid only for the types to push across
- boundaries. This isn't how things work. In fact, the problem is more complex
- than that.<br/>
- As a rule of thumb, users should never mix named and non-named types. Whenever
- a type is given a name, all the types must be given a name. As an example,
- consider the `registry` class template: in case it is pushed across boundaries,
- all the types of components should be assigned a name to avoid subtle bugs.
-
- Indeed, this constraint can be relaxed in many cases. However, it is difficult
- to define a general rule to follow that is not the most stringent, unless users
- know exactly what they are doing. Therefore, I won't elaborate on giving further
- details on the topic.
-
- # Macros, macros everywhere
-
- The library comes with a set of predefined macros to use to declare named types
- or export already existing ones. In particular:
-
- * `ENTT_NAMED_TYPE` can be used to assign a name to already existing types. This
- macro must be used in the global namespace even when the types to be named are
- not.
-
- ```cpp
- ENTT_NAMED_TYPE(my_type)
- ENTT_NAMED_TYPE(ns::another_type)
- ```
-
- * `ENTT_NAMED_STRUCT` can be used to define and export a struct at the same
- time. It accepts also an optional namespace in which to define the given type.
- This macro must be used in the global namespace.
-
- ```cpp
- ENTT_NAMED_STRUCT(my_type, { /* struct definition */})
- ENTT_NAMED_STRUCT(ns, another_type, { /* struct definition */})
- ```
-
- * `ENTT_NAMED_CLASS` can be used to define and export a class at the same
- time. It accepts also an optional namespace in which to define the given type.
- This macro must be used in the global namespace.
-
- ```cpp
- ENTT_NAMED_CLASS(my_type, { /* class definition */})
- ENTT_NAMED_CLASS(ns, another_type, { /* class definition */})
- ```
-
- Nested namespaces are supported out of the box as well in all cases. As an
- example:
-
- ```cpp
- ENTT_NAMED_STRUCT(nested::ns, my_type, { /* struct definition */})
- ```
-
- These macros can be used to avoid specializing the `named_type_traits` class
- template. In all cases, the name of the class is used also as a seed to generate
- the compile-time unique identifier.
-
- ## Conflicts
-
- When using macros, unique identifiers are 32/64 bit integers generated by
- hashing strings during compilation. Therefore, conflicts are rare but still
- possible. In case of conflicts, everything simply will get broken at runtime and
- the strangest things will probably take place.<br/>
- Unfortunately, there is no safe way to prevent it. If this happens, it will be
- enough to give a different value to one of the conflicting types to solve the
- problem. To do this, users can either assign a different name to the class or
- directly define a specialization for the `named_type_traits` class template.
-
- # Allocations: the dark side of the force
-
- As long as `EnTT` won't support custom allocators, another problem with
- allocations will remain alive instead. This is in fact easily solved, or at
- least it is if one knows it.
-
- To allow users to add types dynamically, the library makes extensive use of type
- erasure techniques and dynamic allocations for pools (whether they are for
- components, events or anything else). The problem occurs when, for example, a
- registry is created on one side of a boundary and a pool is dynamically created
- on the other side. In the best case, everything will crash at the exit, while at
- worst it will do so at runtime.<br/>
- To avoid problems, the pools must be generated from the same side of the
- boundary where the object that owns them is also created. As an example, when
- the registry is created in the main executable and used across boundaries for a
- given type of component, the pool for that type must be created before passing
- around the registry itself. To do this is fortunately quite easy, since it is
- sufficient to invoke any of the methods that involve the given type (continuing
- the example with the registry, a call to `reserve` or `size` is more than
- enough).
-
- Maybe one day some dedicated methods will be added that do nothing but create a
- pool for a given type. Until now it has been preferred to keep the API cleaner
- as they are not strictly necessary.
|