Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is a good way to cast and call variable argument functions from a map dynamically at runtime WITHOUT using an external library

Tags:

c++

Let's say I multiple functions with variable arguments:

void greetWorld() {
    cout << "Hello World!" << endl;
}

void greetName(const string& name) {
    cout << "Hello " << name << "!" << endl;
}

void printAddition(const int lhs, const int rhs) {
    cout << "Addition: " << to_string(lhs + rhs) << endl;
}

And these are stored in a map of std::strings to functions (functions being stored as a polymorphic class).

template<typename... Args>
class DerivedFunction;

class BaseFunction {
public:
    template<typename... Args>
    void operator()(Args... args) const {
        (*static_cast<const DerivedFunction<Args...>*>(this))(args...);
    }
};

template<typename... Args>
class DerivedFunction : public BaseFunction {
public:
    DerivedFunction(void(*function)(Args...)) {
        this->function = function;
    }
    void operator()(Args... args) const {
        function(args...);
    }
private:
    void(*function)(Args...);
};

template<typename... Args>
unique_ptr<DerivedFunction<Args...>> make_function(
    void(*function)(Args...)
) {
    return std::make_unique<DerivedFunction<Args...>>(function);
}

int main() {
    unordered_map<string, unique_ptr<BaseFunction>> function_map;
    function_map.insert({ "greetWorld",    make_function(&greetWorld)    });
    function_map.insert({ "greetName",     make_function(&greetName)     });
    function_map.insert({ "printAddition", make_function(&printAddition) });

    ...
}

I can call the functions at compile time like:

int main() {
    ...

    (*function_map.at("greetWorld"))();
    (*function_map.at("greetName"))("Foo"s);
    (*function_map.at("printAddition"))(1, 2);
}

If I then have a string, or stream like:

greetWorld
greetName     string Foo
printAddition int    1   int 2

What would be a good way to call the functions? I can not figure out any way to cast a type at runtime.


Why?

I am trying to implement some kind of remote call procedure for learning purposes. I do not want to use an external library as I am trying to learn how to implement this with the C++ standard library for the sake of understanding C++ more.


What have I tried?

Not much. I've tested creating functions that take a std::vector of std::anys as an argument, and then had the function any_cast them to the type they are. Whilst this does work, it does not look nice, it requires duplicates of all functions, I would rather be able to write functions with meaningful arguments than ambigious.


Minimum Example

#include <iostream>
#include <string>
#include <unordered_map>
#include <memory>
using namespace std;

void greetWorld() {
    cout << "Hello World!" << endl;
}

void greetName(const string& name) {
    cout << "Hello " << name << "!" << endl;
}

void printAddition(const int lhs, const int rhs) {
    cout << "Addition: " << to_string(lhs + rhs) << endl;
}

template<typename... Args>
class DerivedFunction;

class BaseFunction {
public:
    template<typename... Args>
    void operator()(Args... args) const {
        (*static_cast<const DerivedFunction<Args...>*>(this))(args...);
    }
};

template<typename... Args>
class DerivedFunction : public BaseFunction {
public:
    DerivedFunction(void(*function)(Args...)) {
        this->function = function;
    }
    void operator()(Args... args) const {
        function(args...);
    }
private:
    void(*function)(Args...);
};

template<typename... Args>
unique_ptr<DerivedFunction<Args...>> make_function(
    void(*function)(Args...)
) {
    return std::make_unique<DerivedFunction<Args...>>(function);
}

int main() {
    unordered_map<string, unique_ptr<BaseFunction>> function_map;
    function_map.insert({ "greetWorld",    make_function(&greetWorld)    });
    function_map.insert({ "greetName",     make_function(&greetName)     });
    function_map.insert({ "printAddition", make_function(&printAddition) });

    cout << "Calling functions at compile time." << endl << endl;

    (*function_map.at("greetWorld"))();
    (*function_map.at("greetName"))("Foo"s);
    (*function_map.at("printAddition"))(1, 2);

    //cout << endl << "Calling functions at runtime." << endl << endl;
    //string runtime =
    //  "greetWorld\n"
    //  "greetName     string Foo\n"
    //  "printAddition int    1   int 2";
    //
    // todo: call functions
}



Solved.

If you apply the accepted solution, you can call functions from the text like I had wanted. Here is new code for an example Tcp server and client. The client sends function names and arguments as a string to the server. The server then executes these. Exactly what I wanted.

struct FunctionNameAndArguments {
    string function_name;
    vector<RPC> arguments;
};

FunctionNameAndArguments parseFunctionNameAndArguments(
    const string& function_name_and_arguments_string
) {
    istringstream ss(function_name_and_arguments_string);
    FunctionNameAndArguments function_name_and_arguments;
    // function name
    ss >> function_name_and_arguments.function_name;
    // arguments
    auto& arguments = function_name_and_arguments.arguments;
    while (!ss.eof()) {
        string function_type;
        ss >> function_type;
        // integer
        if (function_type == "int") {
            int value;
            ss >> value;
            arguments.push_back(value);
        }
        // string
        else if (function_type == "string") {
            string value;
            ss >> value;
            arguments.push_back(value);
        }
        else {
            throw exception("unknown argument type");
        }
    }
    return function_name_and_arguments;
}

int main() {
    unordered_map<string, RPCHandler> functions = {
        { "greetWorld", make_invoker(&greetWorld) },
        { "greetName", make_invoker(&greetName) },
        { "printAddition", make_invoker(&printAddition) }
    };

    char server;
    cout << "Server? (y/n): " << endl;
    cin >> server;
    // server
    if (server == 'y') {
        // accept client
        TcpListener listen;
        listen.listen(25565);
        TcpSocket client;
        listen.accept(client);

        size_t received;
        // receive size of string
        size_t size;
        client.receive(&size, sizeof(size), received);
        // receive function name and arguments as string
        string function_name_and_arguments_string;
        function_name_and_arguments_string.resize(size);
        client.receive(
            function_name_and_arguments_string.data(),
            size,
            received
        );
        // go through each line
        istringstream lines(function_name_and_arguments_string);
        string line;
        while (getline(lines, line)) {
            // parse function name and arguments
            auto [function_name, arguments] = parseFunctionNameAndArguments(
                line
            );
            // call function
            functions.at(function_name)(
                arguments
            );
        }
        
    }
    // client
    else {
        // connect to server
        TcpSocket server;
        server.connect("localhost", 25565);

        // function calls string
        const string function_calls =
            "greetWorld\n"
            "greetName     string Foo\n"
            "printAddition int    1   int 2";
        size_t size = function_calls.size();
        // send size of string
        server.send(&size, sizeof(size));
        // send function calls string
        server.send(function_calls.data(), size);
    }
}
like image 833
Lily Avatar asked Nov 05 '20 10:11

Lily


1 Answers

Let us assume you have a list of types (taking int and string as an example) usable in RPC, we can combine them in a RPC type and associated RPCHandler as follows:

using RPC = std::variant<int, std::string>;
using RPCHandler = std::function<void(std::vector<RPC>)>;

You want to create a std::map<std::string, RPCHandler> dispatch so you can do (given a std::vector<RPC> args):

dispatch[command](args);

This map can be constructed as follows:

void test0();
void test2(int, std::string);

std::map<std::string, RPCHandler> dispatch = {
  { "test0", make_invoker(test0) },
  { "test2", make_invoker(test2) },
};

where make_invoker returns a lambda of the correct shape. The body of this lambda passes the function pointer, argument vector, and a std::index_sequence to invoke_rpc:

template<class... Arg>
RPCHandler make_invoker(void (*f)(Arg...)) {
   return [f](std::vector<RPC> args) {
      invoke_rpc(f, args, std::index_sequence_for <Arg...>{});
   };
}

Finally, invoke_rpc uses std::get on each argument in turn to convert it into the expected type. It does this by expanding the two given template parameter packs in parallel. Intuitively, this expands to f(std::get<Arg0>(args.at(0), std::get<Arg1>(args.at(1)) with as many arguments to f as it expects (since the index sequence has the same length Args...).

template<class... Arg, std::size_t... I>
void invoke_rpc(void (*f)(Arg...), std::vector<RPC> args, std::index_sequence<I...>) {
    f(std::get<Arg>(args.at(I))...);
}

If the vector is too short you get a std::out_of_range error, if there is an argument mismatch you get a std::bad_variant_access. You can improve error handling by checking the size of args before calling f, and using std::holds_alternative to see if all passed values match their proscribed type.

like image 184
Botje Avatar answered Nov 07 '22 12:11

Botje