Search code examples
c++pluginsfunctional-programmingc++17variadic-templates

Expose variadic functions through a plugin interface


I have a framework, that has a type-erased function type Function that maps std::any instances to an std::any instance and a function registry std::unordered_map<std::string, Function>. This framework has a plugin system, where plugin authors can register functions within this function registry. This way, new functions can be integrated into the framework at runtime.

A very boiled-down implementation of the framework could look like this:

#include <any>
#include <vector>
#include <memory>
#include <functional>
#include <iostream>

class Function
{
public:

    template <typename R, typename... Args>
    Function(R(*f)(Args...)) : impl(std::make_unique<FunctionImpl<R, Args...>>(f)) {}


    template <typename... Args>
    std::any operator()(Args&&...args) const
    {
        std::vector<std::any> args_vec({std::forward<Args>(args)...});
        return impl->invoke(args_vec);
    }

private:

    struct FunctionProxy {
        virtual std::any invoke(std::vector<std::any>&) const = 0;
        virtual ~FunctionProxy(){}
    };

    template <typename R, typename... Args>
    class FunctionImpl : public FunctionProxy
    {
    public:
        FunctionImpl(R(*f)(Args...)) : fun{f} {}

        std::any invoke(std::vector<std::any>& args) const override
        {
            return invoke_impl(args, std::make_index_sequence<sizeof...(Args)>{});
        }
    private:
        template <size_t... I>
        std::any invoke_impl(std::vector<std::any>& args, std::index_sequence<I...>) const
        {
            if constexpr (std::is_void_v<R>) {
                fun(std::any_cast<Args>(args[I])...);
                return std::any{};
            } else {
                return std::any(fun(std::any_cast<Args>(args[I])...));
            }
        }

        std::function<R(Args...)> fun;
    };

    std::unique_ptr<FunctionProxy> impl;
};

class FunctionRegistry
{
public:
    template <typename R, typename... Args>
    void register_function(R(*fun)(Args...), std::string name)
    {
        functions.emplace(std::make_pair(name, Function(fun)));
    }

    Function const& resolve(std::string const& name){
        return functions.at(name);
    }
private:
    std::unordered_map<std::string, Function> functions;
};

/***********************************************************************************/

double add(double x, double y) {
    return x+y;
}

template <typename... Args>
double sum(Args... args) {
    return (0 + ... + args);
}

int main()
{
    // code called in void init() function provided by plugin
    FunctionRegistry functions;
    functions.register_function(&add, "add");

    //....

    // code called in the framework at runtime
    auto const& f = functions.resolve("add");
    std::cout << std::any_cast<double>(f(5.,3.));
    return 0;
}

https://godbolt.org/z/aT4n36v6M

Note that the example is simplified for the sake of this question (no error checking, the real code doesn't use std::any but a proper reflection library, it is not restricted to function pointers and std::function, the first part of the main function would be implemented as part of a plugin....)

The code works fine with non-template functions. But now I would like to enable users to provide variadic functions in a plugin. I realize that it is not possible in the current setup, because templates are initialized at the call site, which is not compiled as part of the plugin.

Is there any way at all to allow users to register variadic functions in this kind of framework?

I am open for all solutions, any part of the code including the API can be altered as long as the existing functionality stays intact.

The only requirement is, that the registered function is truely variadic: I know I could just explicitly instantiate the variadic function for 1,2,3,....,n arguments in the plugin and register each instantiation individually.


Solution

  • As already stated in the question, for template<typename ...T> void for(T...); variadic function (or in general - any function template) you can register any instance of it - like

    fwk.register("foo_ifi", &foo<int,float,int>);

    And that's all it can be done. Unless the problem is to not to provide template parameter - then you can use lambda to wrap your function template:

    template <typename ...T> auto foo(T... x) { .... }
    
    std::function<void(int,int,int)> f = 
     [](auto ...x) ->decltype(auto) { return foo(x...); };
    

    Solution for fixed number of arguments:

    If you can limit your framework to some maximum number of arguments and some fixed number of types - then it is doable - see an example.

    Just few helper classes:

    namespace detail
    {
    template <typename, typename>
    struct FunctionBuilder;
    template <std::size_t ...I, typename V>
    struct FunctionBuilder<std::index_sequence<I...>, V>
    {
        using type = std::function<V(decltype(I,std::declval<V>())...)>;
    };
    
    template <typename, typename>
    struct MapsBuilder;
    template <std::size_t ...I, typename V>
    struct MapsBuilder<std::index_sequence<I...>, V>
    {
        using type = std::tuple<
            std::map<std::string, 
                     typename FunctionBuilder<std::make_index_sequence<I>, V>::type>...>;
    };
    
    }
    

    And the framework itself - with some template magic, tuple, variant, function and lambdas - it can be done pretty much automatically:

    
    template <std::size_t MaxArg, typename ...T>
    struct Framework {
        using Value = std::variant<T...>;
    
        using Maps = detail::MapsBuilder<std::make_index_sequence<MaxArg>, Value>::type;
        Maps maps;
    
        template <typename R, typename ...A>
        void registerFunction(std::string name, R (*fun)(A...))
        {
            std::get<sizeof...(A)>(maps)[std::move(name)] = [fun](auto ...x) -> Value {
                return fun(std::get<A>(x)...);
            };
        }
    
        template <std::size_t N, typename Callable>
        void registerCallable(std::string name, Callable fun)
        {
            std::get<N>(maps)[std::move(name)] = [fun](auto ...x) -> Value {
                return std::visit([&](auto ...a) -> Value { return fun(a...); },  x...);
            };
        }
    
        template <typename Callable>
        void registerVariadicCallable(std::string name, Callable fun)
        {
            auto funForValues = [fun](auto ...x) -> Value {
                return std::visit([&](auto ...a) -> Value { return fun(a...); },  x...);
            };
            std::apply([&](auto& ...m) { ((m[name] = funForValues), ...); }, maps);
        }
    
        template <typename ...A>
        Value call(std::string const& name, A ...a) {
            return std::get<sizeof...(A)>(maps)[name](Value(a)...);
        }
    
    };
    

    And the proof it works:

    template <typename ...T>
    auto sum(T ...a) {
        return (a + ... + 0);
    }
    
    int sum2(int a, int b)
    {
        return a + b;
    }
    
    
    int main() {
        Framework<5, int, float> fwk;
        fwk.registerFunction("sum2", sum2);
        fwk.registerCallable<2>("sum2t", [](auto ...x){ return sum(x...); });
        fwk.registerVariadicCallable("sum", [](auto ...x){ return sum(x...); });
    
        auto r1 = std::get<int>(fwk.call("sum2", 1, 2));
        auto r2 = std::get<float>(fwk.call("sum2t", 1, 2.1f));
        auto r3 = std::get<float>(fwk.call("sum", 1, 2.1f, 3));
        std::cout << r1 << '\n';
        std::cout << r2 << '\n';
        std::cout << r3 << '\n';
    }
    
    
    
    

    Solution for any number of arguments:

    If you want to register this function template itself, to make it callable for any arguments - you need to make framework also a template that knows its "functions". Something like this

    Note: all of this requires C++20, but it is doable in C++17 and even C++11 - but requires a little more effort.

    template <auto NamedCallable, auto ...NamedCallables>
    class Framework;
    
    template <auto NamedCallable>
    class Framework<NamedCallable> {
    public:
        template <typename ...T>
        static decltype(auto) call(std::string_view name, T... params)
        {
            if (NamedCallable.name == name)
                return NamedCallable(std::forward<T>(params)...);
            throw std::runtime_error("Not found caller: " + std::string(name));
        }
    };
    
    template <auto NamedCallable, auto ...NamedCallables>
    class Framework : Framework <NamedCallables...> {
    public:
        template <typename ...T>
        static decltype(auto) call(std::string_view name, T... params)
        {
            if (NamedCallable.name == name)
                return NamedCallable(std::forward<T>(params)...);
            return Framework<NamedCallables...>::call(name, std::forward<T>(params)...);
        }
    };
    

    To make it a little easier to make such registration - some helper classes would be needed:

    template<size_t N>
    struct StringLiteral {
        constexpr StringLiteral(const char (&str)[N]) {
            std::copy_n(str, N, value);
        }
        constexpr operator std::string_view() const { return value; }
        
        char value[N];
    };
    
    template <auto Name, auto Callable>
    struct NamedCallable
    {
        static constexpr std::string_view name = Name;
        template <typename ...T>
        decltype(auto) operator()(T... a) const {
            return Callable(std::forward<T>(a)...);
        }
    };
    

    And some example - how it can be used:

    // A variadic function
    template <typename ...T>
    void foo(T... a) {
        std::cout << "foo:";
        ((std::cout << " " << a), ...);
        std::cout << "\n";
    }
    // A regular function
    void bar(int a, int b, int c) {
        std::cout << "bar: " << a << ' ' << b << ' ' << c << '\n';
    }
    
    using MyFramework = Framework<
        NamedCallable<StringLiteral{"foo"}, [](auto ...x) { return foo(x...); }>{},
        NamedCallable<StringLiteral{"bar"}, bar>{}>;
                                 
    

    As you can see - such variadic needs to be enclosed in lambda anyway - because only lambda is something we can provide as non-template things - however its call operator is a method template.

    And how it works:

    int main() {
        MyFramework fwk;
        fwk.call("foo", 1, 2, 3);
        fwk.call("bar", 1, 2, 3);
    }