序列化函数对象

19

在C++中,是否可以将std :: function、函数对象或一般的闭包序列化和反序列化?如何做到这一点?C++11是否支持此功能?是否有任何库可用于此任务(例如Boost)?

例如,假设一个C++程序有一个需要与位于另一台机器上的另一个C++程序通信(通过TCP / IP套接字)的std :: function。在这种情况下,您有什么建议?


编辑:

为了澄清,要移动的函数应该是纯函数,没有副作用。因此,我没有安全性或状态不匹配问题。

解决问题的方法是构建一个小型嵌入式特定领域语言并对其抽象语法树进行序列化。我希望能够找到一些语言/库支持以移动机器独立的函数表示。


2
算了吧。查一下“远程过程调用”的概念以及流行的实现方式。 - Kerrek SB
@luc-danton,您能详细解释一下吗?如果我将我的小型嵌入式DSL的语法树序列化并传输作为解决问题的方法,可能会出现什么问题? - shaniaki
2
@shaniaki 在一般情况下,安全考虑是无处不在的。如果您将问题简化为仅具有有限功能的DSL,则可能不适用(或者至少不适用于同样的程度)- 如果您坚持使用该解决方案,则可能需要修改您的问题。在两种情况下,您都需要考虑操作应该应用于哪些状态,特别是当它们不是纯净的并且可以达到某些状态时-何时以及如何在机器之间同步此状态? - Luc Danton
@n.m. 如果我们编写一个库来读取机器代码,将它们作为二进制数据发送,使用一些库进行二进制翻译并执行它,这是否可行呢?如果我们只针对几种广泛使用的架构,似乎是可能的。(例如,该库仅支持Linux x86+x86_64)(但是,我也同意现在应该使用RPC而不是发送二进制代码。) - recolic
@recolic 你想要实现这个吗?在发送之前,你需要反编译代码并包含它引用的任何对象、调用的任何代码以及被调用代码引用的任何对象,递归地。我不抱太大希望。 - n. m.
显示剩余6条评论
3个回答

14

对于函数指针和闭包,可以使用。但对于 std::function 不行。

函数指针是最简单的——它只是像任何其他指针一样的指针,因此您可以将其读作字节:

template <typename _Res, typename... _Args>
std::string serialize(_Res (*fn_ptr)(_Args...)) {
  return std::string(reinterpret_cast<const char*>(&fn_ptr), sizeof(fn_ptr));
}

template <typename _Res, typename... _Args>
_Res (*deserialize(std::string str))(_Args...) {
  return *reinterpret_cast<_Res (**)(_Args...)>(const_cast<char*>(str.c_str()));
}                   

但我惊讶地发现,即使没有重新编译,在程序的每次调用中函数的地址也会发生改变。如果您想传输该地址,则没有太大用处。这是由于 ASLR。在Linux上使用setarch $(uname -m) -LR your_program启动your_program可以关闭它。

现在,您可以将函数指针发送到运行相同程序的不同计算机,并调用它!(这不涉及传输可执行代码。但除非您正在运行时生成可执行代码,否则我认为您并不需要那个。)

一个lambda函数则是完全不同的。

std::function<int(int)> addN(int N) {
  auto f = [=](int x){ return x + N; };
  return f;
}

f的值将是捕获的int N。它在内存中的表示与int相同!编译器为lambda生成一个无名类,其中f是一个实例。该类已重载了我们代码的operator()

无名类会对序列化造成问题。这也会对从函数返回lambda函数造成问题。后者可以通过使用std::function来解决。

据我所知,std::function是通过创建一个模板包装类来实现的,该类通过模板类型参数有效地保存对lambda函数背后的无名类的引用。(这是functional中的_Function_handler。)std::function获取对此包装类的静态方法(_M_invoke)的函数指针,并存储那个加上闭包值。

不幸的是,所有内容都被深藏在private成员中,而闭包值的大小未被存储。(因为lambda函数知道自己的大小,所以不需要存储。)

因此,std::function并不适合于序列化,但作为蓝图却很有效。我跟随它的做法,简化了它(我只想序列化lambda函数,而不是其他各种可调用的东西),在size_t中保存了闭包值的大小,并添加了(反)序列化方法。 它能正常运行!


2
但这将取决于架构。例如,从x86到arm就不起作用。 - benathon
@daniel,你能放上你使用std::function的最后一部分代码吗? - ssb
我在工作中完成了这个,所以我必须先请求版权释放。我会回报的! - Daniel Darabos
1
@DanielDarabos,非常好的技术回答。在C++11中,std::function有一个名为target()的成员函数,它“返回指向存储的可调用函数目标的指针。” https://en.cppreference.com/w/cpp/utility/functional/function/target 。即使没有这个函数,你也可以假设你可以使用reinterpret_cast从std::function获取指针并继续做出假设。虽然不可移植,但毕竟,你的答案已经依赖于平台了。 - alfC

9

不。

C++没有内置的序列化支持,并且从未考虑过将代码从一个进程传输到另一个进程,更不用说从一台机器传输到另一台机器了。能够做到这一点的语言通常都具有IR(中间表示代码,与机器无关)和反射。

因此,您必须自己编写协议来传输所需的操作,并且DSL方法当然是可行的...取决于您希望执行的任务种类以及性能需求。

另一种解决方案是使用现有语言。例如,Redis NoSQL数据库嵌入了LUA引擎并可以执行LUA脚本,您可以执行相同的操作,并在网络上传输LUA脚本。


0

不,但有一些受限制的解决方案。

最好的情况是在某种全局映射中注册函数(例如使用键字符串),该映射对发送代码和接收代码都是公共的(可以在不同计算机上或序列化之前和之后)。然后,您可以序列化与函数相关联的字符串,并在另一侧获取它。

具体来说,库HPX实现了类似于HPX_ACTION的东西。

这需要很多协议,并且对代码更改非常脆弱。

但毕竟这与试图序列化具有私有数据的类没有什么不同。从某种意义上说,函数的代码是其私有部分(参数和返回接口是公共部分)。

留给您一线希望的是,根据您组织代码的方式,这些“对象”可以是全局或公共的,如果一切顺利,它们可以通过某种预定义的运行时间接性在序列化和反序列化期间可用。

这是一个简单的示例:

序列化器代码:

// common:
class C{
  double d;
  public:
  C(double d) : d(d){}
  operator(double x) const{return d*x;}
};
C c1{1.};
C c2{2.};
std::map<std::string, C*> const m{{"c1", &c1}, {"c2", &c2}};
// :common

main(int argc, char** argv){
   C* f = (argc == 2)?&c1:&c2;
   (*f)(5.); // print 5 or 10 depending on the runtime args
   serialize(f); // somehow write "c1" or "c2" to a file
}

反序列化代码:

// common:
class C{
  double d;
  public:
  operator(double x){return d*x;}
};
C c1;
C c2;
std::map<std::string, C*> const m{{"c1", &c1}, {"c2", &c2}};
// :common

main(){
   C* f;
   deserialize(f); // somehow read "c1" or "c2" and assign the pointer from the translation "map"
   (*f)(3.); // print 3 or 6 depending on the code of the **other** run
}

(代码未经测试)。

请注意,这会强制执行许多常见和一致的代码,但根据环境,您可能可以保证此代码。代码中最微小的更改可能会导致难以检测的逻辑错误。

另外,我在这里使用全局对象(可用于自由函数),但是可以使用作用域对象来完成同样的操作,更棘手的是如何在本地建立映射(在本地作用域内包含#include常见代码?)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接