gpt4 book ai didi

c++ - Linux上的C++插件ABI问题

转载 作者:塔克拉玛干 更新时间:2023-11-02 23:43:18 24 4
gpt4 key购买 nike

我正在使用插件系统来替换共享库。

在为共享库设计API时,我知道ABI的问题,应该谨慎设计库中的入口点(例如导出的类)。

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

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

首先,我为插件开发人员提供以下 header :

// 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问题是否足够?

最佳答案

Is it sufficient to define an abstract class to get rid of all ABI issues?



简短答案:

没有。

我不建议将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 newoperator delete是相同的。静态链接的运行时,尤其是用LTO构建的运行时,也可能对此感到困惑。

  • 更长的答案:

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

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

    然后,您可以为C API提供丰富的C++仅 header 包装器,以处理生存期和错误代码/异常转换等。
    令人高兴的是,大多数语言都可以生产和使用C API,这可能使插件作者不仅可以使用C++。

    实际上,即使在C API中也存在很多陷阱。如果我们要学究,那么唯一安全的事情是带有固定大小的参数和返回类型(指针, size_t[u]intN_t)的函数-甚至不一定是内置类型( shortintlong,...)或枚举。例如。 in GCC: -fshort-enums可以更改枚举的大小, -fpack-struct[=n]可以更改结构内的填充。
    因此,如果您确实希望安全,则不要使用枚举并打包所有结构或不直接公开它们(而是公开访问器函数)。

    其他注意事项:

    这些与问题并不严格相关,但是在提交特定样式的API之前,一定要考虑这些因素。

    错误处理:是否使用C++,您将需要替代异常的方法。
    这可能是某种形式的错误代码。然后,一旦您进入C++ Realm ,就可以使用C++中的 std::error_code包裹原始的enum / int,并且如果API使用C++,则可以使用具有稳定ABI的类似于 std::expected 或类似于 Boost.Outcome的类型。

    加载插件并导入符号:使用抽象类很容易-您只需要一个简单的工厂函数。使用传统的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"讨论作为进一步的“阅读”。
    当然,关于此事还有其他好的演讲和文章(我不记得我的脑海了)。

    关于c++ - Linux上的C++插件ABI问题,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57975166/

    24 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com