ThePhD / sol2

Sol3 (sol2 v3.0) - a C++ <-> Lua API wrapper with advanced features and top notch performance - is here, and it's great! Documentation:
http://sol2.rtfd.io/
MIT License
4.06k stars 492 forks source link

Is it possible to create a user type in Sol3 by ignoring the template arguments? #1568

Closed GasimGasimzada closed 5 months ago

GasimGasimzada commented 6 months ago

I want to implement a Signal-Slot pattern that I can use within C++ and in Sol:

template<class... TArgs>
class Signal {
  using Handler = std::function<void(TArgs &...)>;
public:
  void connect(Handler &&handler) {
    handlers.push_back(handler);
  }

  void connectLua(sol::protected_function fn) {
    handlers.push_back([fn](TArgs &...args) {
      fn(args...);
    });
  }

  void notify(TArgs... args) {
    for (auto handler: handlers) {
      handler(args...);
    }
  }

private:
  std::vector<Handler> handlers;
};

int main() {
    sol::state state;
    state.open_libraries();

    auto usertype = state.new_usertype<Signal<float, uint32_t>>("Signal", sol::no_constructor);
    usertype["connect"] = &Signal<float, uint32_t>::connectLua;

    Signal<float, uint32_t> signal;

    state["mySignal"] = std::reference_wrapper(signal);
    signal.connect([](float a, uint32_t b) {
      std::cout << "App: " << a << " " << b << "\n";
    });

    state.script(R"(
      mySignal:connect(function(a, b)
        print('Lua: ', a, b)
      end)
    )");

    signal.notify(10.0f, 20);
    return 0;
}

(Godbolt link: https://godbolt.org/z/a7Mqn4c9v)

This works well but I have a lot of signals in my application and I do not want to create a separate usertype for each type. Is there any way that I can create a usertype in Lua only once and use a template class instance to generate the value?

Rochet2 commented 6 months ago

As I see it in your code, the important part from lua seems to be that you need to only be able to bind arbitrary lua function to a signal regardless of what that signal is as long as the signal is exposed to lua.

A problem is that you cannot access templated classes without knowing the template parameters, which are the types that you cannot get from lua. You are basically required to do any binding for templated classes beforehand or you must know/have a predetermined logic to come up with the template signature in the function you bind. There are different kinds of things you can do depending on what you need. There might be some way to get around to what you need using templates, but off the top of my head I didnt come up with any that did not seem impractical (for example make a map of template instanciations that are then available to lua through a map of sorts).

Instead, here is one simple example that utilizes a inheritance to implement a non-templated base class for lua while maintaining the typed C++ counterpart in the templated subclass. It allows you to bind the functionality required by lua through the base class only once, enabling you to connect functions to any signal exposed into lua regardless of its types. Maybe its what you need, or maybe it gives you inspiration for something cooler :)

```cpp #include #include #define SOL_ALL_SAFETIES_ON 1 #include #include class SignalBase { public: void connectLua(sol::protected_function fn) { luaHandlers.push_back(fn); } protected: std::vector luaHandlers; }; template class Signal : public SignalBase { using Handler = std::function; public: void connect(Handler &&handler) { handlers.push_back(handler); } void notify(TArgs... args) { for (auto handler: handlers) { handler(args...); } for (auto handler: luaHandlers) { handler(args...); } } private: std::vector handlers; }; int main() { sol::state state; state.open_libraries(); auto usertype = state.new_usertype("Signal", sol::no_constructor); usertype["connect"] = &SignalBase::connectLua; Signal signal; state["mySignal"] = std::reference_wrapper(signal); // NOTE we have to cast or explicitly state here that we want SignalBase as otherwise sol will think its a different type because of the template class signal.connect([](float a, uint32_t b) { std::cout << "App: " << a << " " << b << "\n"; }); state.script(R"( mySignal:connect(function(a, b) print('Lua: ', a, b) end) )"); signal.notify(10.0f, 20); return 0; } ```
GasimGasimzada commented 6 months ago

I ended up doing a a weird implementation with lambdas. One of the things that I wanted to also do was to make the lua signal "proxy" to consume the internal Signal (the example below is simplified but I pass more than just the original signal to the SolSignal class):

template<class... TArgs>
class Signal {
  using Handler = std::function<void(TArgs &...)>;
public:
  void connect(Handler &&handler) {
    auto id = handlers.size();

    handlers.push_back(handler);
  }

  void notify(TArgs... args) {
    for (auto handler: handlers) {
      handler(args...);
    }
  }

private:
  std::vector<Handler> handlers;
};

class SolSignal {
public:
  template<class... TArgs>
  SolSignal(Signal<TArgs...> &signal) {
    mConnector = [&signal](sol::protected_function fn) {
        return signal.connect([fn](TArgs &...args) {
            fn(args...);
        });
    };
  }

  SignalSlot connect(sol::protected_function fn) {
    return mConnector(fn);
  }

private:
  std::function<SignalSlot(sol::protected_function)> mConnector;
};

int main() {
    sol::state state;
    state.open_libraries();

    auto usertype = state.new_usertype<SolSignal>("Signal", sol::no_constructor);
    usertype["connect"] = &SolSignal::connect;

    Signal<float, uint32_t> signal;
    state["mySignal"] = SolSignal(signal);

    signal.connect([](float a, uint32_t b) {
      std::cout << "App: " << a << " " << b << "\n";
    });

    state.script(R"(
      mySignal:connect(function(a, b)
        print('Lua: ', a, b)
      end)
    )");

    signal.notify(10.0f, 20);
    return 0;
}

But I might try inheritance as well. Maybe I can make it work with it since I have kind of made my own dynamic dispatch here and if I can, I would want to use the language feature itself :)