lib.md 5.6 KB

Push EnTT across boundaries

Table of Contents

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

As an example:

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.
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.
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.

    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.

    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.

    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:

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.
Unfortunately, there is no safe way to prevent it. If this happens, it will be enough to give a different name to one of the conflicting types to solve the problem. To do this, users can either assign a different name to the class or define directly a specialization for the shared_traits class template.

Note

I'm still working hard to make everything work across boundaries.
The classes affected by the problem were registry, dispatcher and emitter. Currently, only the registry class fully support shared types. Using dispatcher and emitter across boundaries isn't allowed yet and can result in unexpected behavior on Windows in general and on GNU/Linux when default visibility is set to hidden.

Stay tuned for future updates.