156 lines
6.6 KiB
Markdown
156 lines
6.6 KiB
Markdown
# Push EnTT across boundaries
|
|
|
|
<!--
|
|
@cond TURN_OFF_DOXYGEN
|
|
-->
|
|
# Table of Contents
|
|
|
|
* [Introduction](#introduction)
|
|
* [Shared types and traits class](#shared-types-and-traits-class)
|
|
* [Do not mix types](#do-not-mix-types)
|
|
* [Macros, macros everywhere](#macros-macros-everywhere)
|
|
* [Conflicts](#conflict)
|
|
* [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.
|
|
|
|
# Shared 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 `shared_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::shared_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 shared 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 shared and non-shared types. Whenever
|
|
a type is made shareable, all the types should be made shareable. As an example,
|
|
consider the `registry` class template. In case it is pushed across boundaries,
|
|
all the types of components should be shareable 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 shared types
|
|
or export already existing ones. In particular:
|
|
|
|
* `ENTT_SHARED_TYPE` can be used to make shareable already existing types. This
|
|
macro must be used in the global namespace even when types to make shareable
|
|
are not.
|
|
|
|
```cpp
|
|
ENTT_SHARED_TYPE(my_type)
|
|
ENTT_SHARED_TYPE(ns::another_type)
|
|
```
|
|
|
|
* `ENTT_SHARED_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_SHARED_STRUCT(my_type, { /* struct definition */})
|
|
ENTT_SHARED_STRUCT(ns, another_type, { /* struct definition */})
|
|
```
|
|
|
|
* `ENTT_SHARED_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_SHARED_CLASS(my_type, { /* class definition */})
|
|
ENTT_SHARED_CLASS(ns, another_type, { /* class definition */})
|
|
```
|
|
|
|
Nested namespaces are supported out of the box as well in all cases. As an
|
|
example:
|
|
|
|
```cpp
|
|
ENTT_SHARED_STRUCT(nested::ns, my_type, { /* struct definition */})
|
|
```
|
|
|
|
These macros can be used to avoid specializing the `shared_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 `shared_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.
|