提问人:kingsjester 提问时间:5/2/2023 最后编辑:Ulrich Eckhardtkingsjester 更新时间:7/26/2023 访问量:345
不同 C++ 命名空间中两个函数的 C 链接声明冲突
Conflicting C linkage declaration of two functions in different C++ namespaces
问:
在代码中,我希望能够为我在运行时加载的两个不同版本的共享库包含两个标头(在 Linux 上,在 Windows 上)。C++
C
dlopen
dlsym
GetProcAddress
对于一次执行,我只加载一个共享库(在 linux 上,在 windows 上),选择的版本由命令行上提供给我的程序的参数决定。.so
.dll
对于每个版本的 C 库,我犹豫是否要包含一个用于函数声明的标头,或者包含另一个用于函数指针类型声明(或两者兼而有之)。
函数声明的标头采用以下形式:
#ifdef __cplusplus
extern "C"
{
#endif
extern int func(int argInt);
#ifdef __cplusplus
}
#endif
我们称它的 2 个版本为 和 。my_header_old.h
my_header_new.h
函数指针类型声明的标头采用以下形式:
typedef int (*func)(int argInt)
我们称它的 2 个版本为 和 。my_header_ptr_types_old.h
my_header_ptr_types_new.h
第二种形式似乎是强制性的,因为我需要将类型为 / 的结果转换为函数指针类型。dlsym
GetProcAddress
void*
我的第一个问题是:
是否必须在我的 案例,或者我可以只将标题用于函数指针类型 声明?
由于标头中的声明非常相似,因此我尽量避免与命名空间冲突:
namespace lib_old
{
#include "my_header_ptr_old.h"
}
namespace lib_new
{
#include "my_header_ptr_new.h"
}
我的第二个问题是:
在这种情况下,以这种方式声明函数指针类型是否正确?
我可以对第一种形式的标头做同样的事情,但根据上面的第一个问题,我不确定它是否有用。不过,如果我在 Windows 上尝试它,它可以在没有警告的情况下编译良好。不幸的是,在 linux 上我得到:
my_header_new.h:警告:冲突的 C 语言链接声明“int lib_new::func(int)”
my_header_old.h:注意:之前的声明 'int lib_old::func(int)'
根据这个问题的答案,警告似乎很重要。此外,没有一个答案旨在解决。
由于我没有找到任何在不修改标头和中函数原型的情况下解决冲突问题的方法,我认为更好的方法是仅使用第二种形式(和)来解决问题。my_header_new.h
my_header_old.h
my_header_ptr_old.h
my_header_ptr_new.h
最终,我在评论中看到“C 链接对命名空间没有意义”,并且“当您在同一翻译单元中使用两个版本时”可能会发生一些“ABI 冲突”,我对有关此主题的来源很感兴趣。
答:
这仍然有点模糊,但也许这是最好的,因为它让答案涵盖了更广泛的用例。
当然,在运行时之前,您不能对库中的函数有任何未定义的引用。这本身并不意味着你不能使用相关的标头(例如,你可能需要定义),但如果你这样做,你必须验证两个库版本是否足够相似(“ABI 兼容”,至少在相关部分),使用来自一个版本的编译代码和来自另一个版本的(类型)声明不会出错。#include
struct
如果版本非常兼容,您可能只能使用其中一个头文件。函数指针变量方法是一种单独的便利性:它允许在加载正确的版本并安装函数指针后,编写代码的其余部分,就好像它将库作为普通依赖项一样。当您使用为实现跨版本兼容性而构建的标头时,请注意,您可能非常希望将函数指针放入命名空间中,以便它们不会与共享库中同名的 C 函数冲突。这也提供了一个仅使用一个标头的机会,也许表达了两个接口的交集,以避免意外地依赖于不可移植的东西。
如果这两个版本与 ABI 不兼容,但使用(某些)相同的符号,事情就会变得非常有趣。建议在命名空间中包含这两个接口几乎完全适用于无用的情况:由于您不能引用函数或变量,因此只有类型可能会有所帮助,并且根据这些类型定义的任何接口的两个版本都会发生冲突,因为它们使用不同的类型(由命名空间建立)。
在这种情况下,安全的方法是将每个版本放在一个单独的翻译单元中(注意避免链接时优化,这可能会允许不一致的定义进行交互),基本上是围绕两个版本编写自己的兼容性包装器。程序的该组件将有自己的单个标头(可能带有函数指针变量),程序的其余部分是针对该标头编写的。#include
我不确定我是否理解您想要实现的目标,但如果您确定 2 个库与 ABI 兼容并且函数具有相同的签名,您可能不需要标头声明。extern "C"
例如,这个简单的代码
#include <dlfcn.h>
#include <iostream>
int main(int argc, char** argv){
int version = std::atoi(argv[1]);
void* lib;
if(version == 0)
lib = dlopen("func0.so", RTLD_NOW);
if(version == 1)
lib = dlopen("func1.so", RTLD_NOW);
auto f = reinterpret_cast<int(*)(int)>(dlsym(lib, "func"));
std::cout<<f(0)<<"\n";
}
应加载 或 取决于 Linux 上的运行时值,并调用与签名匹配的符号。在文献中,“组件配置器”设计模式可能描述了我所理解的问题的解决方案。func0.so
func1.so
int func(int)
评论
我会考虑以下方法,处理版本可能会更改且 ABI 可能会更改的共享对象:
Foo 版本 1 (foo.1.cpp -> foo.1.so):
#include "foo.h"
#include <iostream>
#define VERSION 1
namespace foo::v1 {
void bar() {
std::cout << "version: " << VERSION << std::endl;
}
}
namespace {
call_table_v1 const ct = {
.bar = foo::v1::bar,
};
call_table_description const ctd = {
.version = VERSION,
.call_table = &ct,
};
}
call_table_description get_call_table_description(void)
{
return ctd;
}
FOO 版本 2 (foo.2.cpp -> foo.2.so):
#include "foo.h"
#include <iostream>
#define VERSION 2
namespace foo::v2 {
void bar(int param) {
std::cout << "version: " << VERSION << ", param: " << param << std::endl;
}
}
namespace {
call_table_v2 const ct = {
.bar = foo::v2::bar,
};
call_table_description const ctd = {
.version = VERSION,
.call_table = &ct,
};
}
call_table_description get_call_table_description(void)
{
return ctd;
}
foo generic 头文件访问不同版本:
#ifndef FOO_H_
#define FOO_H_
#ifdef __cplusplus
extern "C" {
#endif /* __cplusplus */
struct call_table_v1 {
void (*bar)(void);
};
struct call_table_v2 {
void (*bar)(int);
};
struct call_table_description {
int version;
void const *call_table;
};
struct call_table_description get_call_table_description(void);
#ifdef __cplusplus
}
#endif /* __cplusplus */
#endif /* FOO_H_ */
访问库的主要程序:
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include "foo.h"
int main(int argc, char **argv) {
int const version = atoi(argv[1]);
void *lib;
switch (version) {
case 1:
lib = dlopen("./foo.1.so", RTLD_NOW);
break;
case 2:
lib = dlopen("./foo.2.so", RTLD_NOW);
break;
default:
fprintf(stderr, "unsupported version %i of library\n", version);
return EXIT_FAILURE;
}
if (!lib) {
perror("could not open library");
return EXIT_FAILURE;
}
typeof(get_call_table_description) *call_table_description_getter_f =
dlsym(lib, "get_call_table_description");
struct call_table_description const ctd = call_table_description_getter_f();
assert(ctd.version == version);
switch (ctd.version) {
case 1: {
struct call_table_v1 const *pct_v1 = ctd.call_table;
pct_v1->bar();
} break;
case 2: {
struct call_table_v2 const *pct_v2 = ctd.call_table;
pct_v2->bar(42);
} break;
default:
assert(0);
}
dlclose(lib);
return 0;
}
生成并运行以检查它:
dpronin-gentoo➜ dlopen ᐅ g++ -shared -fPIC foo.1.cpp -ofoo.1.so
dpronin-gentoo➜ dlopen ᐅ g++ -shared -fPIC foo.2.cpp -ofoo.2.so
dpronin-gentoo➜ dlopen ᐅ gcc main.c -g -omain
dpronin-gentoo➜ dlopen ᐅ ./main 1
version: 1
dpronin-gentoo➜ dlopen ᐅ ./main 2
version: 2, param: 42
评论
#include "admin_tcef_old.h"
#include "admin_tcef.h"