Skip to content

Request: Support for auto-registered functions on meta #1245

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
TerensTare opened this issue Apr 3, 2025 · 15 comments
Open

Request: Support for auto-registered functions on meta #1245

TerensTare opened this issue Apr 3, 2025 · 15 comments
Assignees
Labels
enhancement accepted requests, sooner or later I'll do it

Comments

@TerensTare
Copy link
Contributor

TerensTare commented Apr 3, 2025

In C++, we normally have several tools to customize functions/"interfaces" for types (think of serialize/deserialize), which are most of the time defined at compile time. However, in gamedev specifically there is often a need to have customization for data which is loaded at runtime and is often type-erased, which is why EnTT has meta to help with this part. However, meta needs the user to specify the functions supported by a certain type, which is not optimal as you need to be explicit about everything. Furthermore, the registration might be cumbersome when a function is overloaded (think of ADL scenarios).

For this reason, also based on the recent discussion on imgui-based editors that use meta, I think it would be great if meta had a mechanism to automatically detect certain interfaces and automatically "register" them for each type supporting them. These interfaces don't need to be a fixed set and should probably be tied to a meta context. The idea would be to have something like the following:

using namespace entt::literals;

struct hash_interface {
  template <typename T>
  inline void operator()(void *type_) const {
    auto &&type  = *(entt::meta_factory<T> *)type_;

    if constexpr (requires { std::hash<T>{}; }) // ok, std::hash is specialized for T, register hashing
        type.func<&std::hash<T>::operator()>("hash"_hs);
  }
};

struct foo
{
     int a;
    char b;
    float c;
};

// dummy setup
template <>
struct std::hash<foo>
{
    inline std::size_t operator()(foo const &) const { return 42; }
};

int main()
{
    entt::locator<entt::meta_ctx>::value_or()
        .register_auto_interface<hash_interface>(); // ok: hash_interface will be registered for all types that support it

    entt::meta_factory<foo>{} // all registered interfaces are checked here, in this case only `hash_interface`
        .data<&foo::a>("a"_hs)
        .data<&foo::b>("b"_hs)
        .data<&foo::c>("c"_hs);


    foo f{.a = 10, .b = 'b', .c = 0.1f};
    entt::meta_any any_foo{f};
    auto hash = entt::resolve<foo>().func("hash"_hs);

    // prints 42
    std::cout << hash(any_foo); // I can't remember if this is the syntax, but you get the idea
}
@skypjack skypjack self-assigned this Apr 4, 2025
@skypjack skypjack added the triage pending issue, PR or whatever label Apr 4, 2025
@skypjack
Copy link
Owner

skypjack commented Apr 4, 2025

How would the register_auto_interface function work though? I think the template token here makes it tricky to get it done:

template <typename T>
inline void operator()(void *type_) const;

We have two different layers of template machinery in this case: one when you register the interfaces, the other one on the intefaces.
I don't see a clear way to make it work, but I'm open to suggestions to push the discussion further. 🙂

@ZXShady
Copy link

ZXShady commented Apr 4, 2025

There is no way to register functions on a specific class at runtime without specifying all the intervfaces at compile time

entt::locator<entt::meta_ctx>::value_or()
        .register_auto_interface<hash_interface>(); // ok: hash_interface will be registered for all types that support it

needs to return a type that holds all template<class... Interfaces> . then you register by calling on that return value the functions

int main()
{
    auto locator = entt::locator<entt::meta_ctx>::value_or()
        .register_auto_interface<hash_interface>(); // ok: hash_interface will be registered for all types that support it
// have to register all states at this call by chaining maybe?

// passing locator
    entt::meta_factory<foo>{locator} // all registered interfaces are checked here, in this case only `hash_interface`
        .data<&foo::a>("a"_hs)
        .data<&foo::b>("b"_hs)
        .data<&foo::c>("c"_hs);


    foo f{.a = 10, .b = 'b', .c = 0.1f};
    entt::meta_any any_foo{f};
    auto hash = entt::resolve<foo>().func("hash"_hs);

    // prints 42
    std::cout << hash(any_foo); // I can't remember if this is the syntax, but you get the idea
}

you are essentially trying to solve the issue of arbitary std::any visitation without specifying all the interfaces to visit which is impossible.

I know you could get close with Stateful meta programming but I wouldn't like to see that used in a library as it is inconsistent and not portable

@skypjack
Copy link
Owner

skypjack commented Apr 4, 2025

Then why not an ADL-solved invocation from within meta_factory constructor or the like, on functions that can use concepts to filter their arguments and create bindings automatically? I think it would work as well, just without the register_auto_interface step. Makes sense?

@ZXShady
Copy link

ZXShady commented Apr 4, 2025

Then why not an ADL-solved invocation from within meta_factory constructor or the like, on functions that can use concepts to filter their arguments and create bindings automatically? I think it would work as well, just without the register_auto_interface step. Makes sense?

ADL is Arguement-Dependant-Lookup and if meta_factory contains no arguement how will it know what to call? my example passes the locator to it so it can know is this what you mean?

@TerensTare
Copy link
Contributor Author

First of all, my idea was to have a dense_set<interface_fptr> inside meta_ctx but like you said, I just realized you can't fully type-erase the function signature. 😅

Then why not an ADL-solved invocation from within meta_factory constructor or the like, on functions that can use concepts to filter their arguments and create bindings automatically? I think it would work as well, just without the register_auto_interface step. Makes sense?

That would work actually, I think it's cleaner/less error prone (?) + simpler to write overall. Maybe not an ADL solution, but a simple type trait would do I think.

// default implementation
template <typename>
struct inject_interfaces {
    template <typename T>
    inline static void inject(entt::meta_factory<T> &type) {} // do nothing by default
};

// specialize inject_interfaces<void> if you want your own implementation
template <>
struct inject_interfaces<void> {
    template <typename T>
    inline static void inject(entt::meta_factory<T> &type) {
        // similar to the hash_interface example earlier
    }
};

Also another alternative I was thinking of is to simply have functions that return the meta_factory<T> for you, but I don't think that composes well...

// how do you compose this with other traits?
template <typename T>
auto meta_hashable() {
    if constexpr (is-hashable<T>) {
        return entt::meta_factory<T>{}.func<&hash-func>("hash"_hs);
    } else {
        return entt::meta_factory<T>{};
    }
}

@ZXShady
Copy link

ZXShady commented Apr 4, 2025

First of all, my idea was to have a dense_set<interface_fptr> inside meta_ctx but like you said, I just realized you can't fully type-erase the function signature. 😅

Then why not an ADL-solved invocation from within meta_factory constructor or the like, on functions that can use concepts to filter their arguments and create bindings automatically? I think it would work as well, just without the register_auto_interface step. Makes sense?

That would work actually, I think it's cleaner/less error prone (?) + simpler to write overall. Maybe not an ADL solution, but a simple type trait would do I think.

// default implementation
template <typename>
struct inject_interfaces {
    template <typename T>
    inline static void inject(entt::meta_factory<T> &type) {} // do nothing by default
};

// specialize inject_interfaces<void> if you want your own implementation
template <>
struct inject_interfaces<void> {
    template <typename T>
    inline static void inject(entt::meta_factory<T> &type) {
        // similar to the hash_interface example earlier
    }
};

Also another alternative I was thinking of is to simply have functions that return the meta_factory<T> for you, but I don't think that composes well...

// how do you compose this with other traits?
template <typename T>
auto meta_hashable() {
    if constexpr (is-hashable<T>) {
        return entt::meta_factory<T>{}.func<&hash-func>("hash"_hs);
    } else {
        return entt::meta_factory<T>{};
    }
}

but wouldn't limitting it to 1 specilization disallow you to overload it if it is already overloaded by some other library?

@TerensTare
Copy link
Contributor Author

Yes, that's a problem. For me you don't want to do that either way because afaik it can and will cause ODR violations (ie. having specializations of a type/function and pick the "most recent" one). Maybe libraries should have something like a "register_traits" which you can/should call manually instead? I'm open to suggestions though, so let me know if you have a better idea.

@ZXShady
Copy link

ZXShady commented Apr 4, 2025

Yes, that's a problem. For me you don't want to do that either way because afaik it can and will cause ODR violations (ie. having specializations of a type/function and pick the "most recent" one). Maybe libraries should have something like a "register_traits" which you can/should call manually instead? I'm open to suggestions though, so let me know if you have a better idea.

did you see my code I posted about returning a local variable?

@TerensTare
Copy link
Contributor Author

Yes, what about it?

@skypjack
Copy link
Owner

skypjack commented Apr 9, 2025

What I meant is this, that is, a straightforward mechanism that allows users to define meta setup functions in user space. With concepts they are waaaaaay nicer than what I did in the tests too.
Roughly speaking, this isn't even required, because the following achieves the same:

entt::meta_factory<MyType> factory{};
meta_setup(factory); // defined in user space
factory.do_something_else();

However, the factory.setup().do_something_else() feels nicer to me. Since it's also easy to maintain overall, why not. 🙂
Makes sense? Thoughts? It's on the wip branch, we've plenty of time to refine or drop it eventually. 👍

@skypjack skypjack added enhancement accepted requests, sooner or later I'll do it and removed triage pending issue, PR or whatever labels Apr 9, 2025
@TerensTare
Copy link
Contributor Author

Ok, now I see what you meant with ADL. Seems we are on the same page on what it should look like then, but I still have one last thing to ask.

Why should .setup() be a separate function? Wouldn't it be better if you just call meta_setup(*this) from within the constructor of meta_factory (if meta_setup exists/is found) instead? What am I missing here? 🤔

@skypjack
Copy link
Owner

Yes and no. With the setup function you decide. With an implicit call from the constructor, you don't decide.
If I want to introduce a fallback function that asserts when a setup function does not exist, it could cause problems in the second case.
Maybe. I guess one can get around it somehow, but you know, I'm all about being explicit and in control rather than voted to hidden implicit calls. 🙂

That said, I reverted the ADL changes because they failed and I didn't get why. 🤔
Any suggestions? I'm probably missing something obvious with them... an extra pair of 👀 would help.

@TerensTare
Copy link
Contributor Author

Well not exactly ADL, but you can get it to work if you specialize meta_setup inside entt. Here's a link to show what I mean. My understanding is that meta_factory is an EnTT type, so meta_setup is looked up within the parent namespace, that is, entt. So the user would have to overload entt::meta_setup unless there is some parameter from another namespace passed to meta_setup.

I'm all about being explicit and in control rather than voted to hidden implicit calls. 🙂

Ok, I see. I would still argue to at least have some tag that says "call this setup automatically in the constructor for me". Something in the lines of:

// user should write this
void meta_setup(entt::meta_factory<foo> &factory, entt::auto_setup_t) { /* ... */ }

// then on meta_factory<T> constructor
if constexpr (requires { meta_setup(*this, auto_setup_t{}); }) {
    meta_setup(*this, auto_setup_t{});
}

Let me know what you think.

@ZXShady
Copy link

ZXShady commented Apr 11, 2025

Well not exactly ADL, but you can get it to work if you specialize meta_setup inside entt. Here's a link to show what I mean. My understanding is that meta_factory is an EnTT type, so meta_setup is looked up within the parent namespace, that is, entt. So the user would have to overload entt::meta_setup unless there is some parameter from another namespace passed to meta_setup.

I'm all about being explicit and in control rather than voted to hidden implicit calls. 🙂

Ok, I see. I would still argue to at least have some tag that says "call this setup automatically in the constructor for me". Something in the lines of:

// user should write this
void meta_setup(entt::meta_factory<foo> &factory, entt::auto_setup_t) { /* ... */ }

// then on meta_factory<T> constructor
if constexpr (requires { meta_setup(*this, auto_setup_t{}); }) {
    meta_setup(*this, auto_setup_t{});
}

Let me know what you think.

the issue I see is with UB because what if the user doesn't include this overload with their class type? it will cause ODR voilations.

I don't hace an idea to prevent it....

@TerensTare
Copy link
Contributor Author

I think that happens with specializing traits too, right? There's no good way around it as far as I can tell neither unless we have meta_setup work as following:

struct foo {
    static void meta_setup(entt::meta_factory<foo> &factory) {
        // ...
    }
};

This is similar to on_construct & such on auto signals side, but the problem is it doesn't work for types not owned by you, in which case you're forced with a function or trait.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement accepted requests, sooner or later I'll do it
Projects
None yet
Development

No branches or pull requests

3 participants