我正在用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
答案 0 :(得分:4)
您可以使用std::function,但您的主要问题是注册函数的签名(即参数的数量和类型,结果类型),以及如何在编译时和运行时都知道它(以及如何调用任意,运行时已知,签名的函数)。请注意,C ++通常是erasing types(它们在运行时被“遗忘”)。
请注意,函数的签名在C和C ++(对于编译器)中很重要,因为calling conventions和ABI可能需要不同的机器代码来调用它们。
您可以决定使用某种通用值类型(可能是未来std::experimental::any)。或者(更简单,但更不通用)你可以定义一些抽象的超类MyValue
(它可以是来自Qt的QVariant,或者受其启发)并且只处理映射单个std::vector<MyValue>
的函数(概念性地表示您的RPC的参数到MyValue
结果。然后,您只需注册与std::function<MyValue(std::vector<MyValue>))>
兼容的lambda-expressions,并要求他们在运行时检查arity和类型。
或者,您可以决定将自己限制为几个签名,例如只接受不超过4个参数的函数,每个参数都是std::string
或int
(因此,您将逐个处理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_sequence
和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);
}
};
是标准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
进行序列化/反序列化。