在C ++中调用任意函数

时间:2016-07-13 12:14:50

标签: c++ rpc

我正在用C ++开发一个非常小的RPC库。我想注册这样的RPC函数:

void foo(int a, int b) {
    std::cout << "foo - a: " << a << ", b: " << b << std::endl;
}

myCoolRpcServer->registerFnc("foo", foo(int,int))

客户端请求将作为函数名和参数数组到达。 服务器将检查是否已注册相应的功能,如果是,则执行该功能。

MyCoolRpcServer::handleRequest(string fnc, vector<FncArg> args)
{
    // Check if we have the function requested by the client:
    if (!this->hasFunction(fnc)) {
        throw ...
    }

    if (!this->function(fnc).argCnt != args.count()) {
        throw ...
    }

    // I think this is the hardest part - call an arbitrary function:
    this->function(fnc)->exec(args); // Calls the function foo()               
}

我的问题是如何存储函数引用(包括参数类型)以及如何再次调用它。我知道当我调用SLOT(...)宏时必须在Qt中使用类似的东西,但在如此大的库中找到它是相当棘手的...

感谢您的建议。

Klasyc

2 个答案:

答案 0 :(得分:4)

您可以使用std::function,但您的主要问题是注册函数的签名(即参数的数量和类型,结果类型),以及如何在编译时和运行时都知道它(以及如何调用任意,运行时已知,签名的函数)。请注意,C ++通常是erasing types(它们在运行时被“遗忘”)。

请注意,函数的签名在C和C ++(对于编译器)中很重要,因为calling conventionsABI可能需要不同的机器代码来调用它们。

您可以决定使用某种通用值类型(可能是未来std::experimental::any)。或者(更简单,但更不通用)你可以定义一些抽象的超类MyValue(它可以是来自Qt的QVariant,或者受其启发)并且只处理映射单个std::vector<MyValue>的函数(概念性地表示您的RPC的参数到MyValue结果。然后,您只需注册与std::function<MyValue(std::vector<MyValue>))>兼容的lambda-expressions,并要求他们在运行时检查arity和类型。

或者,您可以决定将自己限制为几个签名,例如只接受不超过4个参数的函数,每个参数都是std::stringint(因此,您将逐个处理31个不同的签名)。

您还遇到与serialization相关的任意值共享一些常见指针(或子值)的问题。查看libs11n

您也可以使用一些机器来注册签名。您可以利用现有的元数据机制(例如Qt元对象协议)。您可以对类型和签名进行一些文本描述,并编写一些处理它们的C ++代码生成器。

您可以查看libffi。使用任意签名调用任意原始函数可能是相关的。

如果图书馆足够通用,你的图书馆就不会很小。你可能会限制自己,例如JSON值和表示。请参阅JSONRPC

您可能采用metaprogramming方法,例如给出已注册函数的签名(以某种定义的格式),在运行时(初始化)生成plugin的C ++代码用于他们的粘合代码,并编译和dynamically load该插件(例如使用{{3}在Linux上)。

同时查看dlopen(3)&amp; CORBA&amp; ONCRPC

PS。我假设你的C ++至少是C ++ 11。顺便说一下,如果你想要一个通用的解决方案,你低估了你的目标是多么困难。你可能花费数月或数年时间。

答案 1 :(得分:1)

基本理念

基本思想是你想要将你的函数封装在一些包装器对象中,该对象将处理一些通用输入/输出并将它们映射到你的底层函数所期望的内容。

首先让我们创建一个存储任何值的类型:

typedef std::function<Value(std::vector<Value>&)> Function;

让我们使用泛型参数隐藏我们的函数:

template<class F> class FunctionImpl;

template<class R, class... T>
class FunctionImpl<R(*)(T...)>
{
  R(*ptr)(T... args);
  template<std::size_t... I>
  Value call(std::vector<Value>& args, integer_sequence<std::size_t, I...>)
  {
    Value value;
    value = ptr(args[I].get< typename std::tuple_element<I, std::tuple<T...>>::type >()...);
    return value;
  }
public:
  FunctionImpl(R(*ptr)(T... args)) : ptr(ptr) {}
  Value operator()(std::vector<Value>& args)
  {
    constexpr std::size_t count = std::tuple_size<std::tuple<T...>>::value;
    if (args.size() != count)
      throw std::runtime_error("Bad number of arguments");
    return call(args, make_integer_sequence<std::size_t, std::tuple_size<std::tuple<T...>>::value>());
  }
};

我们现在想要包装任何函数指针,以符合此签名。包装器函数应该解包参数,调用实际函数并将结果包装在Value:

integer_sequence

make_integer_sequenceclass Functions { private: std::unordered_map<std::string, Function> functions_; public: template<class F> void add(std::string const& name, F f) { functions_[name] = FunctionImpl<F>(std::move(f)); } Value call(std::string name, std::vector<Value>& args) { return functions_[name](args); } }; 是标准C ++ 17库的一部分,但您可以编写自己的实现。

我们现在定义一个用于注册可调用函数的类型:

int foo(int x, int y)
{
  std::printf("%i %i\n", x, y);
  return x + y;
}

int main()
{
  Functions functions;
  functions.add("foo", &foo);

  std::pair<std::string, std::vector<Value>> request = parse_request();
  Value value = functions.call(request.first, request.second);
  generate_answer(value);

  return 0;
}

我们可以使用它:

std::pair<std::string, std::vector<Value>> parse_request()
{
  std::vector<Value> args(2);
  args[1] = 8;
  args[0] = 9;
  return std::make_pair("foo", std::move(args));
}

void generate_answer(Value& value)
{
  std::printf("%i\n", value.get<int>());
}

使用虚拟RPC通信功能:

8 9
17

我们得到:

long

当然,这是高度简化的,如果您想要概括它,您将面临许多问题:

  • 您可能也希望传播异常;

  • 整数类型(例如template<class T> class Type {}; typedef std::vector<char> Buffer; // I'm clearly not claiming this would be efficient, but it gives // the idea. In pratice, you might want to consume some streaming I/O // API. class Value { Buffer buffer_; public: template<class T> T get() { return deserialize(Type<T>(), buffer_); } template<class T> Value& operator=(T const& x) { serialize(x, buffer_); return *this; } }; inline std::uint32_t deserialize(Type<std::uint32_t>, Buffer const& buffer) { if (buffer.size() != sizeof(std::uint32_t)) throw std::runtime_error("Could not deserialize uint32"); std::uint32_t res; memcpy(&res, buffer.data(), sizeof(std::uint32_t)); return be32toh(res); } inline void serialize(std::uint32_t value, Buffer const& buffer) { buffer.resize(sizeof(std::uint32_t)); value = htobe32(value); memcpy(buffer.data(), &value, sizeof(std::uint32_t)); } )在不同平台上的大小不同;

  • 如果你想处理指针和引用(你可能不应该),它开始变得复杂;

  • 您必须为您正在使用的所有类型添加序列化/反序列化代码。

序列化

在处理序列化的过程中,将使用泛型编程进行序列化/反序列化:

Function

另一种可能性是使用泛型编程并让var Doc = new XmlDocument(); Doc.AppendChild(Doc.CreateXmlDeclaration("1.0", "utf-8", null)); var productsNode = Doc.AppendChild(Doc.CreateElement("products")); //do other stuff 进行序列化/反序列化。