Linux上C++插件的ABI问题
我正在开发一个插件系统来取代共享库 我知道为共享lib设计API时会遇到ABI问题,应该仔细设计libs中的入口点,例如导出类 例如,根据我的理解,添加、删除或重新排序导出类的私有成员变量可能会导致不同的内存布局和运行时错误,这就是Pimpl模式可能有用的原因。当然,在修改导出的类时,还有许多其他陷阱需要避免 我在这里建立了一个小例子来说明我的问题 首先,我为插件开发人员提供以下标题:Linux上C++插件的ABI问题,c++,linux,gcc,plugins,abi,C++,Linux,Gcc,Plugins,Abi,我正在开发一个插件系统来取代共享库 我知道为共享lib设计API时会遇到ABI问题,应该仔细设计libs中的入口点,例如导出类 例如,根据我的理解,添加、删除或重新排序导出类的私有成员变量可能会导致不同的内存布局和运行时错误,这就是Pimpl模式可能有用的原因。当然,在修改导出的类时,还有许多其他陷阱需要避免 我在这里建立了一个小例子来说明我的问题 首先,我为插件开发人员提供以下标题: // character.h #ifndef CHARACTER_H #define CHARACTER_H
// 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
最后是使用插件的主要应用程序:
#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问题就足够了吗
定义一个抽象类来消除所有ABI问题就足够了吗
简短答复:
没有
我不推荐使用C++来做插件API,请看下面的长答案,但是如果你决定坚持C++,那么:
不要在插件API中使用任何标准库类型。
例如,Character::name返回一个std::字符串。如果std::string的实现发生了变化,那么这将导致未定义的行为。实际上,API中不应该使用任何您不控制任何第三方库的内容。
不要跨插件边界使用异常或RTTI。在Linux例外情况下,如果使用RTLD_GLOBAL加载插件,RTTI可能会起作用。这对于插件来说不是一个好主意,而且主机和插件都使用相同的运行时。但一般来说,您要么无法捕获来自另一个模块的异常,要么如果它们由不同的运行时分配,甚至可能导致堆损坏。
只在抽象类的末尾添加函数,否则一切都会因为vtable布局的更改而悄无声息地中断,这很难诊断。
始终从同一模块分配和取消分配对象。我注意到你没有一个destroyCharacter函数main实际上泄露了这个角色,但这是另一个问题。始终为不同模块共享库或插件创建的资源提供对称的创建和销毁函数。
我相信在使用GCC的Linux上,主机应用程序的运算符new和运算符delete可以通过弱符号正确地传播到加载的插件,但是如果您希望它在Windows上工作,那么不要假设主机应用程序和插件中的运算符new和运算符delete是相同的。静态链接的运行时(尤其是使用构建的)也可能会弄乱这一点。
详细回答:
当从插件导出C++ API时,可能存在很多问题。
一般来说,如果用于构建宿主应用程序和插件的工具链有任何不同,则无法保证它能够正常工作。这包括但不限于编译器、语言版本、编译器标志、预处理器定义等
关于插件的常识是使用纯C89API,因为所有通用平台上的C ABI都非常稳定。
保持C89和C++的公共子集意味着主机和插件可以使用不同的语言标准、标准库等。除非主机或插件是用一些奇怪的和可能不符合标准的API来构建的,否则应该是相当安全的。显然,您仍然必须小心数据结构布局
您可以为处理生命周期和错误代码/异常转换的C API提供一个丰富的C++头包包。 作为一个很好的奖励,C API是大多数语言可生产和可消费的,这可以使插件作者不仅使用C++。 事实上,即使在C API中也存在不少陷阱。如果我们是学究,那么唯一安全的事情就是具有固定大小参数和返回类型指针、size\u t、[u]intN\u t的函数——甚至不一定是内置类型short、int、long、…,或enum。例如:-fshort enum可以更改枚举的大小,-fpack struct[=n]可以更改结构中的填充。 因此,如果您真的希望安全,那么就不要使用枚举,要么打包所有结构,要么不直接公开它们,而是公开访问器函数
其他考虑: 这些与问题没有严格的关系,但在提交特定样式的API之前,绝对应该考虑这些问题 错误处理:不管你是否使用C++,你都需要一个例外的替代。 这可能是某种形式的错误代码。STD::C++中的Error代码可以在C++中使用,以包装原始EnUM/int,如果API使用C++,则可以使用类似ABI或类似类型的ABI。 加载插件和导入符号:使用抽象类很容易——只需一个简单的工厂函数即可。使用传统的C API,您可能最终需要导入数百个符号。解决这一问题的一种方法是效仿 e 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,知道您将来会想要更改它并保持兼容性。在我看来,C API很适合这种情况,因为它迫使您对所暴露的内容非常精确。插件应该能够以向前和向后兼容的方式向主机公开其API版本
在设计每个函数签名时,最好考虑可扩展性。例如,如果一个结构是通过指针而不是通过值传递的,那么只要在运行时调用方和被调用方都同意它的大小,就可以在不破坏兼容性的情况下扩展它的大小
可见性:也许可以在Linux和其他平台上查看。这实际上不是API设计的问题,只是帮助清理从共享库导出的符号
以上所述绝不是广泛的。
我建议把这次谈话作为进一步阅读。
当然,还有其他关于这个问题的好的谈话和文章,我记不得了
定义一个抽象类来消除所有ABI问题就足够了吗
简短答复:
没有
我不推荐使用C++来做插件API,请看下面的长答案,但是如果你决定坚持C++,那么:
不要在插件API中使用任何标准库类型。
例如,Character::name返回一个std::字符串。如果std::string的实现发生了变化,那么这将导致未定义的行为。实际上,API中不应该使用任何您不控制任何第三方库的内容。
不要跨插件边界使用异常或RTTI。在Linux例外情况下,如果使用RTLD_GLOBAL加载插件,RTTI可能会起作用。这对于插件来说不是一个好主意,而且主机和插件都使用相同的运行时。但一般来说,您要么无法捕获来自另一个模块的异常,要么如果它们由不同的运行时分配,甚至可能导致堆损坏。
只在抽象类的末尾添加函数,否则一切都会因为vtable布局的更改而悄无声息地中断,这很难诊断。
始终从同一模块分配和取消分配对象。我注意到你没有一个destroyCharacter函数main实际上泄露了这个角色,但这是另一个问题。始终为不同模块共享库或插件创建的资源提供对称的创建和销毁函数。
我相信在使用GCC的Linux上,主机应用程序的运算符new和运算符delete可以通过弱符号正确地传播到加载的插件,但是如果您希望它在Windows上工作,那么不要假设主机应用程序和插件中的运算符new和运算符delete是相同的。静态链接的运行时(尤其是使用构建的)也可能会弄乱这一点。
详细回答:
当从插件导出C++ API时,可能存在很多问题。
一般来说,如果用于构建宿主应用程序和插件的工具链有任何不同,则无法保证它能够正常工作。这包括但不限于编译器、语言版本、编译器标志、预处理器定义等
关于插件的常识是使用纯C89API,因为所有通用平台上的C ABI都非常稳定。
保持C89和C++的公共子集意味着主机和插件可以使用不同的语言标准、标准库等。除非主机或插件是用一些奇怪的和可能不符合标准的API来构建的,否则应该是相当安全的。显然,您仍然必须小心数据结构布局
您可以为处理生命周期和错误代码/异常转换的C API提供一个丰富的C++头包包。 作为一个很好的奖励,C API是大多数语言可生产和可消费的,这可以使插件作者不仅使用C++。 事实上,即使在C API中也存在不少陷阱。如果我们是学究,那么唯一安全的事情就是具有固定大小参数和返回类型指针、size\u t、[u]intN\u t的函数——甚至不一定是内置类型short、int、long、…,或enum。例如:-fshort enum可以更改枚举的大小,-fpack struct[=n]可以更改结构中的填充。 因此,如果您真的希望安全,那么就不要使用枚举,要么打包所有结构,要么不直接公开它们,而是公开访问器函数
其他考虑: 这些与问题没有严格的关系,但在提交特定样式的API之前,绝对应该考虑这些问题 错误处理:不管你是否使用C++,你都需要一个例外的替代。 这可能是某种形式的错误代码。STD::C++中的Error代码可以在C++ C++中被用来包装原始EnUM/int,如果API使用C++ 可以使用具有稳定ABI的类a型或类a型 加载插件和导入符号:使用抽象类很容易——只需一个简单的工厂函数即可。使用传统的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,知道您将来会想要更改它并保持兼容性。在我看来,C API很适合这种情况,因为它迫使您对所暴露的内容非常精确。插件应该能够以向前和向后兼容的方式向主机公开其API版本
在设计每个函数签名时,最好考虑可扩展性。例如,如果一个结构是通过指针而不是通过值传递的,那么只要在运行时调用方和被调用方都同意它的大小,就可以在不破坏兼容性的情况下扩展它的大小
可见性:也许可以在Linux和其他平台上查看。这实际上不是API设计的问题,只是帮助清理从共享库导出的符号
以上所述绝不是广泛的。
我建议把这次谈话作为进一步阅读。
当然,还有其他关于这个问题的好的谈话和文章,我记不得了