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.
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.
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.
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.
Identifiers are not to be sequentially generated in this case.
As an example:
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.
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.
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.
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.
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.
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.
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.
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:
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.
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.
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.
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.
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.