进程间通信:传递 C 风格结构与 C++ 对象

Interprocess communication: passing C-style structs vs C++-objects

Warning/Disclaimer:

这个问题里面有说法,但是我在过去半个小时左右的小调查中找不到下面声明的答案。我只是好奇这里是否有人已经知道这件事。

本题无代码。只是技术问题。

背景:

我有一个遗留应用程序,它使用在进程之间传递的 C 样式结构进行 进程间通信。这工作得很好,并且已经工作了很多年,甚至在我来到这个星球之前很久:P。

我应该编写一个将成为此应用程序一部分的新流程。不知不觉中,我用 C++ 编写了它,假设我们使用的任何 IPC 都可以处理这个问题。不幸的是,后来我(从同事那里)发现现有的基础设施只能传递 C 风格的结构。

'Unverified' claims/statements:

此外,一位同事列出了以下原因,说明为什么 C++ 在这种情况下是一个糟糕的选择。

  1. C++ 对象有虚表。 C 风格的结构只是变量和值。因此,C 风格的结构可以在进程中传递,而 C++ 对象不能。

  2. 使用 C 风格的结构,我们可以嵌入结构大小等信息,以便双方都知道期望什么和发送什么,但是对于 C++ 对象,这是不可能的,因为 'the size of the vtable could vary'.

  3. 'If we change compilers, then it is even worse. We would have even more permutations to deal with for the case of C++ objects.'

调查索赔:

不用说,这位同事对C有点偏见,但他比我更有经验,大概知道他在说什么。 我是语言不可知论者。但这立即让我开始思考。怎么用C++不能进行进程间通信呢?我用谷歌搜索,第一个点击总是来自 Whosebug,比如这个:

Inter-Process Communication Recommendation

并且我查看了此处列出的不同 IPC 方法。 https://en.wikipedia.org/wiki/Inter-process_communication#Approaches

我的意思是,我跟进了列表中的每个方法,如管道或共享内存等,每个人都不断指出的唯一警告是,指针(duh!当然)不能传递像这样,一些同步问题可能会逐渐出现 - je nachdem。

但是我找不到任何可以反驳或证实他的 'claims' 的东西。 (当然,我可以继续挖掘一天剩下的时间。:P)

问题:

  1. 他的三个说法是真的还是只是 FUD?考虑到这一点,我想要传递的那些对象中的所有内容也只有 POD 变量和一些 STL 容器,如 std::vectorstd::pair 及其值(没有指针或任何东西),以及这些变量的吸气剂。 除了虚析构函数外没有虚函数,它的存在是因为我继承了一个基本消息class的所有消息,因为当时我想可能有一些通用的基本功能。 (我现在可以很容易地摆脱这个基数 class,因为直到现在那里还没有真正常见的东西!谢天谢地,出于某种原因,我将消息的解析和格式化保存在一个单独的 class 中。运气好或有远见?:D )

  2. 这实际上也让我想知道,编译器如何知道结构何时是 C 风格结构,因为我们对整个项目都使用 g++ 编译器?是不是使用了'virtual'关键字?

  3. 我不是在为我的案子寻求解决方案。我可以将这些对象的结果包装到结构中并通过 IPC 传递它们,或者我可以摆脱基础 class 和虚拟析构函数,如上面 'my' 点 1 中所述。

不需要 Boost 或任何 C++11 的东西或任何处理它的库。在这方面的任何建议都与手头的问题无关。

(p.s。既然我发布并重新阅读了我发布的内容,我想将可能在阅读的任何 reader 脑袋中蔓延的想法扼杀在萌芽状态这个,那个...我问这个是为了我的知识,而不是为了和那个同事争论。怀疑是好的,但如果我们都假设其他人有良好的意图,那么对社区来说会很好。:) )

  1. 他的三个说法是真的还是只是 FUD?考虑到这一点,我想要传递的那些对象中的所有内容也只有 POD 变量和一些 STL 容器,如 std::vector 和 std::pair 及其值(没有指针或任何东西),以及这些变量的吸气剂。除了虚析构函数之外没有虚函数,它的存在是因为我从一个基本消息 class 继承了所有消息,因为当时我在想可能有一些共同的基本功能。 (我现在可以很容易地摆脱这个基数 class,因为直到现在那里还没有真正常见的东西!谢天谢地,出于某种原因,我将消息的解析和格式化保存在一个单独的 class 中。运气好或有远见?:D)

不,一旦 stl 容器在您的结构中,您就不能像 POD 数据一样传递它们。未指定 stl 容器的实现,它们可能(并且在大多数情况下确实)包含用于内部目的的指针。

  1. 这实际上也让我想知道,编译器如何知道结构何时是 C 风格结构,因为我们对整个项目都使用 g++ 编译器?是使用了'virtual'关键字吗?

只要你的 struct/class 只有 POD 数据而没有虚函数,它就会被存储为 POD,但如果你的 IPC 的另一端是用另一个编译器编译的,对齐差异可能是一个问题and/or不同的编译器设置或不同的对齐指令(如#pragma pack等)。

  1. 我不是在为我的案子寻求解决方案。我可以将这些对象的结果包装到结构中并通过 IPC 传递它们,或者我可以摆脱基础 class 和虚拟析构函数,如上面 'my' 第 1 点所述。

将这些对象的结果包装到结构中并通过 IPC 传递它们对我来说听起来不错,就我个人而言,这就是我要做的。另一个解决方案也不错,没有上下文很难说哪个更好。

only caveat that everyone keeps on pointing out, is, that pointers (duh! of course) cannot be passed like this

指针值(以及对内存和资源的其他引用)在进程间确实没有意义。这显然是虚拟内存的结果。

另一个警告是,虽然 C 标准为结构指定了精确的(特定于平台的)内存布局,但 C++ 标准一般不保证 classes 的特定内存布局。例如,一个进程不一定与另一个进程就成员之间的填充量达成一致——即使在同一系统中也是如此。 C++ 仅保证标准布局类型的内存布局 - 并且此保证布局与 C 结构相匹配。


... and some STL containers like std::vector ... (no pointers or anything)

除了std::array之外的所有标准容器都在内部使用指针。它们必须这样做,因为它们的大小是动态的,因此必须动态分配数据结构。此外,其中 none 是标准布局 classes。此外,一个标准库实现的 class 定义不能保证与另一个实现相匹配,并且两个进程可以使用不同的标准库——这在 Linux 上并不少见,其中一些进程可能使用 libstdc++(来自GNU)而其他人可能使用 libc++(来自 Clang)。

There are no virtual functions except the virtual destructor

换句话说:至少有一个虚函数(析构函数),因此有一个指向虚表的指针。而且也没有保证的内存布局,因为 classes 与虚函数从来不是标准布局 classes.


所以回答问题:

  1. 大部分情况下没有 FUD,尽管有些说法在技术上有点不准确:

    1. C++ 对象可能有虚表;并非所有人都这样做。 C 结构可以有指针,所以也不是所有的 C 结构都可以共享。一些 C++ 对象可以跨进程共享。具体来说,可以共享标准布局 classes(假设没有指针)。
    2. 确实无法共享具有 vtables 的对象。
    3. 标准布局 classes 有保证的内存布局。只要您将自己限制在标准布局 classes 上,更改编译器不是问题。如果您运气不好,尝试共享其他 classes 可能会奏效,但是当您开始混合编译器时,您可能会遇到问题。
  2. C++ 标准定义了 class 是标准布局的确切条件。所有 C 结构定义都是 C++ 中的标准布局 classes。编译器知道这些规则。

  3. 这不是问题。


结论:您可以 将 C++ 用于 IPC,但您只能在该界面中使用标准布局 classes。这使您无法使用许多 C++ 功能,例如虚函数、访问说明符等。但不是全部:例如,您仍然可以拥有成员函数。

但是请注意,使用 C++ 功能可能会导致进程间接口仅适用于 C++。许多语言可以与 C 接口,但几乎没有任何语言可以与 C++ 接口。

此外:如果您的 "interprocess" 通信超出了系统的边界 - 即跨网络 - 即使是 C 结构或标准布局 class 也不是一个好的表示。在那种情况下你需要序列化。

'Unverified' claims/statements:

C++ objects have vtables. C-style structs are just variables and values. Therefore C-style structs can be passed around processes, while C++ objects cannot be.

这个说法部分正确,但被误导了。

并非所有 C++ 对象都有 vtable(从技术上讲,C++ 标准根本不需要 vtable,尽管它是用于支持虚函数调度的常见实现技术,因为它提供了各种优点)。

如果您查找 this SO question and various answers,您会发现有关 C++ 中聚合和 POD 类型的讨论。问题是定义在 C++ 标准之间演变(反映在对该问题的各种回答中)。在 C++11 中,POD 类型的概念发生了变化,并有效地替换为普通和标准布局类型的概念。

POD 类型(C++11 之前)和标准布局类型(C++11 及更高版本)可以在 C++ 和 C 之间互换(即从一种语言编写的代码传递到另一种语言编写的代码,因为内存布局是兼容的)。

的确,带有任何虚函数的 C++ 对象属于不能与 C 互换的对象。指针通常不能被复制,这阻止了(大部分)C++ 标准容器的使用。

With C-style structs we can embed information like size of the struct, so that both sides know what to expect and what to send, but for C++ objects this is not possible since 'the size of the vtable could vary'.

这个说法是错误的,因为有些类型没有 vtable,而且类型可以在 C++ 和 C 之间互换。

If we change compilers, then it is even worse. We would have even more permutations to deal with for the case of C++ objects.

同样,只要在 C++ 代码中正确选择类型,就可以将它们与 C 互换。

这种说法 - 对于 C 与 C++ 的互操作是正确的 - 对于 C 也是正确的。C 类型的大小是在 C 中正式定义的实现,就像在 C++ 中一样。 intlongfloatdouble 等类型不能保证在不同的编译器中具有相同的大小。有些编译器的设置会更改某些或所有基本类型的大小(例如,具有不同浮点选项的编译器,具有影响 int 是 16 位还是 32 位的设置的编译器等)。

struct C 中的类型也可能在成员之间有填充,并且填充可能因 C 编译器而异。许多编译器都有影响填充的编译选项,这会影响 struct 类型的大小。这可能会导致 C 中相同 struct 类型的布局不兼容。

那么这里可能发生了什么?

进程间通信的设计可能假设它总是在 C 代码之间,使用相同(或兼容)的编译器构建。 IPC 机制可能非常简单:例如,一个进程在管道的指定内存位置喷射一定量的数据,接收方将在该管道的另一端接收到的数据复制到等效的数据结构中。

隐含的假设是可以通过这种方式直接复制数据。这取决于两个程序中兼容的数据类型布局。

问题在于,由于 IPC 机制是在假设兼容 C 编译器的情况下设计的,因此您现在被告知这是因为 C 优于 C++(或其他语言)。不是。它是 IPC 如何完成的人工产物。

IPC 方法可能非常有限,但您的 C++ 代码可以通过 IPC 机制发送和接收数据,只要您在 C++ 中以适当的类型(例如标准布局)打包数据代码。另一个进程是用 C 还是 C++ 编写的也无关紧要。在 C++ 中可能需要做更多的工作(例如,将来自 C++ class 的数据打包到标准布局结构中,然后将该结构发送到另一个进程——或者如果接收数据则相反),但这当然是可能的。

无论如何,您都需要使用兼容的编译器。

并且假设您不能更改进程间通信的方式(例如,设计一个进程间通信协议,而不是盲目地将数据从内存位置向下复制到另一个进程,然后接收进程将数据复制回兼容的数据结构)。如果需要的话,有一些执行 IPC 的方法可以更好地支持一系列编程语言——尽管有不同的权衡(例如,通信带宽、转换数据以便发送的代码以及接收数据和转换数据的代码)返回数据结构)。