用c ++设计接口

时间:2015-05-26 07:13:41

标签: c++ interface

我正在开发一个可以用作动态加载的接口。它也应该是独立于编译器的。所以我想导出接口。 我现在面临以下问题..

问题1:接口函数将一些自定义数据类型(基本上是类或结构)作为In \ Out参数。我想使用构造函数初始化这些类的成员使用默认值。如果我这样做,则不可能动态加载我的库,它变得依赖于编译器。如何解决这个问题。

问题2:某些接口将元素的列表(或映射)返回给client。我正在使用std容器来实现此目的。但这也是编译器相关的(并且编译器版本也有时)。

感谢。

3 个答案:

答案 0 :(得分:3)

以不同方式编译的代码只有在对参数和返回值使用的类型集采用相同的Application Binary Interface (ABI)时才能一起工作。 ABI在更深层次上具有重要意义 - 名称修改,虚拟调度表等等,但我的观点是,如果您的编译器支持允许调用简单类型的函数,那么您至少可以考虑对更复杂类型的一些支持进行黑客攻击比如标准容器和用户定义类型的特定于编译器的实现。

您必须研究ABI支持您的编译器提供的内容,并推断您可以继续提供哪些内容。

如果您想支持超出相关ABI标准的其他类型,选项包括:

  • 使用更简单的类型来暴露更复杂类型的内部结构

    • 传递[{1}} constchar*提取的size_tmy_std_string.data()&my_std_string[0],类似于{{1} }}

    • 序列化数据并使用接收器的数据结构对其进行反序列化(可能很慢)

  • 提供一组函数指针,指向由创建数据类型的对象实现的简单访问器/ mutator函数

    • e.g。经典C my_std_string.size()函数接受指向元素比较函数的指针的方式

答案 1 :(得分:2)

由于我通常会关注多线程,所以我大部分时间会对你的第二个问题大肆宣传。

您已经意识到,通过API传递容器的元素似乎与编译器有关。它实际上更糟糕了:它的头文件& C ++ - 依赖于库,所以至少对于Linux来说,你已经遇到了两个不同的集合:libstc ++(源自gcc)和libcxx(源自clang)。 因为部分容器是头文件而部分是库代码,所以几乎不可能实现ABI独立。

我更担心的是你实际上想过传递容器元素。这是一个巨大的线程安全问题:STL容器 线程安全 - 按设计。

通过在接口上传递引用,您将传递"指向封装知识的指针"您的API的用户可以对您的内部结构进行假设并开始修改指向的数据。在单线程环境中,这通常已经非常糟糕,但在多线程环境中会变得更糟。

其次,你提供的指针可能会陈旧,也不好。

确保返回内部知识的副本,以防止用户修改您的结构。

传递事物const是不够的:const可以被抛弃,你仍然暴露你的内脏。

所以我的建议是:隐藏数据类型,只传递你完全控制的简单类型和/或结构(即不依赖于STL或boost)。

答案 2 :(得分:2)

设计具有最广泛ABI兼容性的API是复杂的主题,当涉及C ++而不是C时更是如此。

然而,有更多的理论类型问题并不像在实践中听起来那么糟糕。例如,理论上,调用约定和结构填充/对齐听起来像是可能是主要问题。实际上它们并不是那么多,您甚至可以通过向第三方指定其他构建指令或使用指示适当调用约定的宏来装饰SDK函数来事后解决此类问题。不是那么糟糕"在这里,我的意思是他们可以绊倒你,但他们不会让你回到绘图板并重新设计你的整个SDK作为回应。

"实用"我想关注的问题是可以让您重新审视绘图板并重做整个SDK的问题。我的清单也不详尽,但我认为你应该首先牢记这些清单。

您还可以将SDK视为由两部分组成:动态链接的部分实际上导出其实现对客户端隐藏的功能,以及静态(内部)链接的便利库部分,它在顶部添加C ++包装器。如果您将SDK视为具有这两个不同的部分,那么您可以在静态链接库中使用更多C ++机制。

所以,让我们开始使用那些实用的头痛诱导剂:

<强> 1。 vtable的二进制布局在编译器之间不一定一致。

在我看来,这是最大的陷阱之一。我们通常在运行时查看从一个模块到另一个模块访问功能的两种主要方式:函数指针(包括由dylib符号查找提供的那些)和包含虚函数的接口。后者在C ++中可以更加方便(无论是实现者还是使用接口的客户端),但不幸的是,在API中使用虚拟函数,旨在与最广泛的编译器二进制兼容,就像在一片土地上玩扫雷一样。

我建议完全避免使用虚拟功能,除非你的团队由知道所有这些问题的扫雷专家组成。尝试再次为这些公共接口部件爱上C并开始建立对由函数指针组成的这类接口的喜爱是有用的:

struct Interface
{
    void* opaque_private_data;
    void (*func1)(struct Interface* self, ...);
    void (*func2)(struct Interface* self, ...);
    void (*func3)(struct Interface* self, ...);
};

这些问题远没有那么多,并且远不如变化那么脆弱(例如:​​你完全可以做一些事情,比如在不影响ABI的情况下在结构底部添加更多的函数指针)。

<强> 2。用于dylib符号查找的存根库是特定于链接器的(通常是所有静态库)。

在与#1结合使用之前,这似乎不是什么大不了的事。当您为了导出接口而抛出虚函数时,下一个重要的诱惑是经常导出整个类或通过dylib选择方法。

不幸的是,使用手动符号查找执行此操作会很快变得非常笨拙,因此通常只需链接到相应的存根即可自动执行此操作。

当你的目标是尽可能多地支持编译器/链接器时,这也会变得笨拙。在这种情况下,您可能必须拥有许多编译器,并为每种可能性构建和分发不同的存根。

所以这可能会让你陷入一个不再是非常实用的出口类定义的角落。此时,您可以简单地使用C链接导出独立函数(以避免C ++名称重整,这是另一个令人头疼的问题)。

其中一个显而易见的事情是,如果我们的目标是通用的二进制兼容性而不会打开太多的蠕虫,我们就会越来越多地倾向于支持类似C或类似C的API。< / p>

第3。不同的模块有不同的堆

如果您在一个模块中分配内存并尝试在另一个模块中释放它,那么您将尝试从不匹配的堆中释放内存并调用未定义的行为。

即使在普通的旧C中,很容易忘记这个规则,而malloc在一个导出的函数中只返回指向它的指针,期望客户端从不同的模块访问内存完成后free。这又一次调用了未定义的行为,我们必须导出第二个函数来间接地从分配它的同一个模块中释放内存。

这可能会成为C ++中一个更大的问题,我们经常会有类模板,这些模板具有隐式执行内存管理的内部链接。例如,即使我们推出自己的std::vector - 像List<T>这样的序列,我们也可以遇到客户端创建列表的场景,通过引用将它传递给我们的API,我们使用的函数可以分配/释放内存(如push_backinsert)并与这个不匹配的堆/免费存储问题对接。因此,即使这个手动滚动的容器应该确保它从同一个中心位置分配和释放内存,如果它将在模块中传递,并且在实现这样的容器时,placement new将成为你的朋友。

<强> 4。传递/返回C ++标准对象与ABI不兼容。

这包括您已经猜到的C ++标准容器。在将std::vector作为另一个包含<vector>时,确保一个编译器将使用operator[]之类的兼容表示并不是真正实用的方法。因此,如果你的目标是广泛的二进制兼容性,那么传递/返回表示不在你控制范围内的标准对象通常是不可能的。

这些甚至不一定在相同编译器构建的两个项目中具有兼容的表示形式,因为它们的表示可能会根据构建设置以不兼容的方式变化。

这可能会让你觉得你现在应该手动滚动所有类型的容器,但我建议在这里使用KISS方法。如果您因函数返回了可变数量的元素,那么我们就不需要多种容器类型。我们只需要一个动态数组类型的容器,它甚至不必是一个可增长的序列,只需要具有适当的复制,移动和破坏语义。

如果你只是在一个计算一个的函数中返回一个集合或一个映射,它可能看起来更好并且可以节省一些周期,但是我建议忘记返回这些更复杂的结构并转换为/从此基本动态转换数组种类的表示。它很少是您可能认为转移到连续表示/从连续表示转移的瓶颈,如果您实际上遇到了这样的热点,您实际上是从现实世界的合法分析会话中获得的例如,您可以随时以非常独立和有选择的方式向SDK中添加更多内容。

您还可以始终将像map这样更复杂的容器包装到类似C的函数指针接口中,该接口将地图句柄视为不透明,远离客户端。对于像二元搜索树这样的更为庞大的数据结构,支付一个间接层的成本通常是非常微不足道的(对于像随机访问连续序列这样的简单结构,它通常不是微不足道的,特别是如果你的读操作像T*涉及间接调用)。

另一件值得注意的事情是,到目前为止,我所讨论的所有内容都与SDK的导出动态关联方有关。内部链接的静态便捷库可以自由接收和返回标准对象,以便为使用您的库的第三方提供便利,前提是您实际上并未在导出的界面中传递/返回它们。您甚至可以避免直接滚动自己的容器,只需对导出的接口采用C风格的思维模式,返回需要释放的std::vector<T>原始指针,而便利库会自动执行此操作并将内容传输到{{ 1}},例如

<强> 5。跨模块边界抛出异常是不明确的。

当我们无法确保两个模块中的兼容构建设置时,我们通常不应该将一个模块中的异常抛入另一个模块中,更不用说同一个编译器了。因此,在这种情况下,抛出API中的异常以指示输入错误通常是不可能的。

相反,我们应该在模块的入口点捕获所有可能的异常,以避免将它们泄漏到外部世界,并将所有这些异常转换为错误代码。

静态链接的便捷库仍然可以调用您的一个导出函数,检查错误代码,如果失败,则抛出异常。这里完全没问题,因为该便利库内部链接到使用此库的第三方模块,因此它有效地从第三方模块中抛出异常以被同一第三方模块捕获。

<强>结论

虽然这绝不是一个详尽的清单,但这些是一些注意事项,如果不加注意,可能会在最广泛的API设计中引发一些最大的问题。事后看来,这些类型的设计级问题可能比实现类型问题成本更高,因此它们通常应该具有最高优先级。

如果您对这些主题不熟悉,那么您可能会偏袒C或类似C的API。您仍然可以使用大量的C ++实现它,并且还可以在顶部构建C ++便利库(您的客户甚至不必使用除内部链接的便利库提供的C ++接口之外的任何东西)。

使用C语言,您通常会在基线级别上查看更多工作,但这些灾难性的设计级问题可能会少得多。使用C ++,您可以在基线级别查看较少的工作,但更多可能是灾难性的惊喜场景。如果您偏向后一种方式,您通常希望确保您的团队在ABI问题上的专业知识更高,更大的编码标准文档将大部分内容用于这些潜在的ABI陷阱。

针对您的具体问题:

  

问题1:接口函数正在采用一些自定义数据类型   (基本上是类或结构)作为In \ Out参数。我想   使用默认值初始化这些类的成员   constructors。如果我这样做,就无法加载我的库   动态地,它变得依赖于编译器。如何解决这个问题。

这是静态链接的便利库可以派上用场的地方。您可以静态链接所有方便的代码,如类与构造函数,并仍然以更原始的原始形式将其数据传递到导出的接口。另一个选择是有选择地内联或静态链接构造函数,以便其代码不会像其他类一样导出,但如果您的目标是最大二进制兼容性,并且您可能不希望如上所示导出类。不想要太多陷阱。

  

问题2:某些接口返回元素的列表(或映射)   client.I我正在使用std容器。但这也是一次   再次编译器依赖(和编译器版本也有一些)。

这里我们必须至少在导出的API级别放弃那些标准容器好东西。您仍然可以在具有内部链接的便利图书馆级别使用它们。