在 C++ 中调用任意函数

Calling arbitrary function in C++

我正在用 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 中必须使用类似的东西,但是在这么大的库中找到它是相当棘手的...

感谢您的建议。

克拉西克

您可以使用 std::function but your main trouble is what is the signature (i.e. number and type of arguments, type of result) of the registered function, and how to know it both at compile-time and at runtime (and also, how to call a function of arbitrary, runtime known, signature). Notice that C++ is generally erasing types(它们在运行时是 "forgotten")。

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

你可以决定你有一些通用值类型(也许将来 std::experimental::any). Or (simpler, but much less general) you could define some abstract superclass MyValue (it could be QVariant from Qt, or inspired by it) and handle only functions mapping a single std::vector<MyValue> (conceptually representing the arguments of your RPC) to a MyValue result. Then you would register only lambda-expressionsstd::function<MyValue(std::vector<MyValue>))> 兼容,并要求它们在运行时检查数量和类型。

或者,您可以决定限制自己使用几个签名,例如只接受不超过 4 个参数的函数,每个参数要么是 std::string 要么是 int(因此您将一一处理 31 种不同的签名)。

您还有一个与 serialization of arbitrary value sharing some common pointer (or subvalue). Look into libs11n 有关的问题。

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

您可能会调查 libffi。调用具有任意签名的任意原始函数可能是相关的。

如果你的库足够通用,它就不会很小。你可能会限制自己,例如JSON values and representations. See JSONRPC.

您可能在 Linux 上有一个 metaprogramming approach, e.g. give the signature of registered functions (in some defined format), generate at runtime (initialization) the C++ code of a plugin for their glue code, and compile and dynamically load that plugin (e.g. using dlopen(3)

另请查看 CORBA & ONCRPC & Boost.RPC

PS。我假设你的 C++ 至少是 C++11。顺便说一句,如果您想要一个通用的解决方案,您就低估了目标的难度。您可能会为此花费数月或数年。

基本思路

基本思想是您希望将您的函数封装在一些包装器对象中,该对象将处理一些通用 input/output 并将它们映射到您的底层函数所期望的。

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

// Dummy implementation which only works for some type.
class Value {
  long value_;
public:
  template<class T>
  T get()
  {
    return (T) value_;
  }  
  template<class T>
  Value& operator=(T const& x)
  {
    value_ = x;
    return *this;
  }
};

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

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

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

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>());
  }
};

integer_sequencemake_integer_sequence 是标准 C++17 库的一部分,但您可以编写自己的实现。

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

class 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);
  }
};

我们可以使用它:

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;
}

具有虚拟RPC通信功能:

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>());
}

我们得到:

8 9
17

当然,这是高度简化的,如果你想概括它,你会面临很多问题:

  • 您可能还想传播异常;

  • 整数类型(例如long)在不同平台上的大小不同;

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

  • 您必须为您正在使用的所有类型中的 serialization/deserialization 添加代码。

序列化

处理序列化的方法是对 serialization/deserialization:

使用泛型编程
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 执行 serialization/deserialization。