Linux 上的 C++ 插件 ABI 问题

Posted

技术标签:

【中文标题】Linux 上的 C++ 插件 ABI 问题【英文标题】:C++ Plugins ABI issues on Linux 【发布时间】:2019-09-17 13:19:40 【问题描述】:

我正在开发一个插件系统来替换共享库。

我知道在为共享库设计 API 时会出现 ABI 问题,并且应该仔细设计库中的入口点(例如导出的类)。

例如,添加、删除或重新排序导出类的私有成员变量可能会导致不同的内存布局和运行时错误(据我了解,这就是 Pimpl 模式可能有用的原因)。当然,在修改导出的类时还需要避免许多其他陷阱。

我在这里建立了一个小例子来说明我的问题。

首先,我为插件开发者提供以下标头:

// character.h
#ifndef CHARACTER_H
#define CHARACTER_H

#include <iostream>

class Character

public:
    virtual std::string name() = 0;
    virtual ~Character() = 0;
;

inline Character::~Character() 

#endif

然后将插件构建为共享库“libcharacter.so”:

#include "character.h"
#include <iostream>

class Wizard : public Character

public:
    virtual std::string name() 
        return "wizard";
    
;

extern "C"

    Wizard *createCharacter()
    
        return new Wizard;
    

最后是使用插件的主应用程序:

#include "character.h"
#include <iostream>
#include <dlfcn.h>

int main(int argc, char *argv[])

    (void)argc, (void)argv;

    using namespace std;

    Character *(*creator)();

    void *handle = dlopen("../character/libcharacter.so", RTLD_NOW);

    if (handle == nullptr) 
        cerr << dlerror() << endl;
        exit(1);
    

    void *f = dlsym(handle, "createCharacter");
    creator = (Character *(*)())f;

    Character *character = creator();
    cout << character->name() << endl;

    dlclose(handle);

    return 0;

定义一个抽象类是否足以解决所有 ABI 问题?

【问题讨论】:

【参考方案1】:

定义一个抽象类是否足以解决所有 ABI 问题?

简答:

没有。

我不建议将 C++ 用于插件 API(请参阅下面的更长答案),但如果您决定坚持使用 C++,那么:

    不要在插件 API 中使用任何标准库类型。 例如,Character::name() 返回一个std::string。如果std::string 的实现发生变化(and it has in the past in GCC),那么这将导致未定义的行为。实际上,任何您无法控制的东西(任何第三方库)都不应该在 API 中使用。 不要跨插件边界使用异常或 RTTI。在 Linux 上,如果您使用 RTLD_GLOBAL 加载插件(对于插件来说不是一个好主意)并且主机和插件都使用相同的运行时,则 RTTI 可能会起作用。但一般来说,您要么无法从另一个模块捕获异常,要么它们甚至可能导致堆损坏(如果它们是由不同的运行时分配的)。 只在抽象类的末尾添加函数,否则一切都会因为 vtable 布局的变化而静默中断(这很难诊断)。 始终从同一模块分配和解除分配对象。我注意到您没有 destroyCharacter() 函数(main() 实际上泄漏了字符,但这是另一个问题)。始终为不同模块(共享库或插件)创建的资源提供对称的 createdestroy 函数。 我相信在带有 GCC 的 Linux 上,主机应用程序的 operator newoperator delete 会正确传播到加载的插件(通过弱符号),但如果你希望它在 Windows 上工作,那么不要假设 operator new 和 @987654339 @ 在宿主应用程序和插件中是相同的。静态链接的运行时,尤其是使用 LTO 构建的运行时,也可能会出现这种情况。

更长的答案:

从插件导出 C++ API 时可能会出现很多问题。 一般来说,如果关于用于构建主机应用程序的工具链和插件的任何内容不同,则无法保证它可以正常工作。这可以包括(但不限于)编译器、语言版本、编译器标志、预处理器定义等。

关于插件的普遍看法是使用纯 C89 API,因为所有常见平台上的 C ABI非常稳定。 保持 C89 和 C++ 的公共子集意味着主机和插件可以使用不同的语言标准、标准库等。除非主机或插件是用一些奇怪的(并且可能不符合标准的)API 构建的,否则这应该是相当安全的。显然,您仍然必须小心数据结构布局。

然后,您可以为处理生命周期和错误代码/异常转换等的 C API 提供丰富的仅包含 C++ 标头的包装器。 作为一个很好的奖励,大多数语言都可以生产和使用 C API,这可以让插件作者不仅仅使用 C++。

实际上,即使在 C API 中也存在不少缺陷。如果我们是迂腐的,那么唯一安全的东西是具有固定大小参数和返回类型(指针,size_t[u]intN_t)的函数——甚至不一定是内置类型(shortint,@ 987654344@, ...) 或枚举。例如。 in GCC: -fshort-enums 可以改变枚举的大小,-fpack-struct[=n] 可以改变结构内的填充。 因此,如果您真的想要安全,请不要使用枚举,也不要打包所有结构或不直接公开它们(而是公开访问器函数)。

其他注意事项:

这些与问题并不严格相关,但绝对应该在提交特定样式的 API 之前加以考虑。

错误处理:无论您是否使用 C++,您都需要异常的替代方法。 这可能是某种形式的错误代码。只要你在 C++ 领域,C++ 中的std::error_code 就可以用来包装原始枚举/int,如果 API 使用 C++,那么 std::expected-like 或 Boost.Outcome-like 类型具有稳定的 ABI可以用。

加载插件和导入符号: 使用抽象类很容易——只需一个简单的工厂函数即可。使用传统的 C API,您最终可能需要导入数百个符号。一种处理方法是在 C 中模拟 vtable。使每个具有关联函数的对象都以指向调度表的指针开头,例如

typedef struct game_string_view  const char *data; size_t size;  game_string_view;

typedef enum game_plugin_error_code  game_plugin_success = 0, /* ... */  game_plugin_error_code;

typedef struct game_plugin_character_impl *GamePluginCharacter; // handle to a Character

typedef struct game_plugin_character_dispatch_table  // basically a vtable
    void (*destroy)(GamePluginCharacter character); // you could even put destroy() here
    game_string_view (*name)(GamePluginCharacter character);
    void (*update)(GamePluginCharacter character, /*...*/, game_plugin_error_code *ec); // might fail
 game_plugin_character_dispatch_table;

typedef struct game_plugin_character_impl 
    // every call goes through this table and takes GamePluginCharacter as it's first argument
    const game_plugin_character_dispatch_table *dispatch;
 game_plugin_character_impl;

未来的可扩展性和兼容性:您应该设计 API,知道您将来会想要更改它并保持兼容性。 IMO,C API 非常适合这一点,因为它迫使您在公开的内容中非常精确。该插件应该能够以向前和向后兼容的方式向主机公开其 API 版本。

在设计每个函数签名时考虑可扩展性是个好主意。例如。如果一个结构是通过指针(而不是值)传递的,那么它的大小可以在不破坏兼容性的情况下扩展(只要在运行时调用者和被调用者都同意它的大小)。

可见性:也许可以在 Linux 和其他平台上查看visibility。这实际上不是 API 设计的问题,只是有助于清理从共享库导出的符号。


以上所有内容绝不是广泛的。 我建议将谈话"Hourglass Interfaces for C++ APIs" 作为进一步的“阅读”。 当然还有其他关于此事的好演讲和文章(我不记得了)。

【讨论】:

以上是关于Linux 上的 C++ 插件 ABI 问题的主要内容,如果未能解决你的问题,请参考以下文章

通过 void 指针通过 C ABI 传递 C++ 对象(可能具有多重虚拟继承)

C ++中的安全交叉编译器ABI?

Android studio插件,检查依赖的so是否支持64位(或者其他的abi)

Android Gradle 插件 Splits 配置 ② ( Splits#abi{} 脚本块配置 | 根据 CPU 架构进行分包 | AbiSplitOptions 配置简介 )

Fabric 插件未安装在 linux 系统上的 android studio 1.5 中

QML插件扩展2(基于C++的插件扩展)