不同 C++ 命名空间中两个函数的 C 链接声明冲突

Conflicting C linkage declaration of two functions in different C++ namespaces

提问人:kingsjester 提问时间:5/2/2023 最后编辑:Ulrich Eckhardtkingsjester 更新时间:7/26/2023 访问量:345

问:

在代码中,我希望能够为我在运行时加载的两个不同版本的共享库包含两个标头(在 Linux 上,在 Windows 上)。C++CdlopendlsymGetProcAddress

对于一次执行,我只加载一个共享库(在 linux 上,在 windows 上),选择的版本由命令行上提供给我的程序的参数决定。.so.dll

对于每个版本的 C 库,我犹豫是否要包含一个用于函数声明的标头,或者包含另一个用于函数指针类型声明(或两者兼而有之)。

函数声明的标头采用以下形式:

#ifdef __cplusplus
extern "C" 
{
#endif

extern int func(int argInt);

#ifdef __cplusplus
}
#endif

我们称它的 2 个版本为 和 。my_header_old.hmy_header_new.h

函数指针类型声明的标头采用以下形式:

typedef int (*func)(int argInt)

我们称它的 2 个版本为 和 。my_header_ptr_types_old.hmy_header_ptr_types_new.h

第二种形式似乎是强制性的,因为我需要将类型为 / 的结果转换为函数指针类型。dlsymGetProcAddressvoid*

我的第一个问题是:

是否必须在我的 案例,或者我可以只将标题用于函数指针类型 声明?

由于标头中的声明非常相似,因此我尽量避免与命名空间冲突:

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.hmy_header_old.hmy_header_ptr_old.hmy_header_ptr_new.h

最终,我在评论中看到“C 链接对命名空间没有意义”,并且“当您在同一翻译单元中使用两个版本时”可能会发生一些“ABI 冲突”,我对有关此主题的来源很感兴趣。

C++ 外部 联动 DLSYM

评论

5赞 rustyx 5/2/2023
不能将 C 库的两个版本链接在一起。C 不是这样工作的。
3赞 273K 5/2/2023
不要在一个文件中包含函数的两个版本。如果一个代码使用旧版本,那么它就会这样做,如果另一个代码使用新版本,它就会这样做。它们的下界包括这两个文件。#include "admin_tcef_old.h"#include "admin_tcef.h"
3赞 user17732522 5/2/2023
即使您设法以您想要的方式解析声明:如何确保在同一翻译单元中使用两个版本时不会发生 ABI 冲突?这似乎非常冒险。
4赞 John Bollinger 5/2/2023
这是行不通的(正如你所设想的那样)。如果库的两个版本提供同名的函数,则只能将其中一个函数链接到程序中。实际上,您的 Linux 编译器认识到了这里的问题,这给我留下了深刻的印象,因为 C 链接对命名空间没有意义。
4赞 erik258 5/2/2023
“实际上,你们的Linux编译器认识到了这里的问题,这给我留下了深刻的印象,因为C链接对命名空间没有意义。这可能也是为什么“在 Windows 上,此代码编译良好,没有警告”的原因。不过,这并不意味着代码可以正常工作。

答:

1赞 Davis Herring 7/24/2023 #1

这仍然有点模糊,但也许这是最好的,因为它让答案涵盖了更广泛的用例。

当然,在运行时之前,您不能对库中的函数有任何未定义的引用。这本身并不意味着你不能使用相关的标头(例如,你可能需要定义),但如果你这样做,你必须验证两个库版本是否足够相似(“ABI 兼容”,至少在相关部分),使用来自一个版本的编译代码和来自另一个版本的(类型)声明不会出错。#includestruct

如果版本非常兼容,您可能只能使用其中一个头文件。函数指针变量方法是一种单独的便利性:它允许在加载正确的版本并安装函数指针后,编写代码的其余部分,就好像它将库作为普通依赖项一样。当您使用为实现跨版本兼容性而构建的标头时,请注意,您可能非常希望将函数指针放入命名空间中,以便它们不会与共享库中同名的 C 函数冲突。这也提供了一个仅使用一个标头的机会,也许表达了两个接口的交集,以避免意外地依赖于不可移植的东西。

如果这两个版本与 ABI 兼容,但使用(某些)相同的符号,事情就会变得非常有趣。建议在命名空间中包含这两个接口几乎完全适用于无用的情况:由于您不能引用函数或变量,因此只有类型可能会有所帮助,并且根据这些类型定义的任何接口的两个版本都会发生冲突,因为它们使用不同的类型(由命名空间建立)。

在这种情况下,安全的方法是将每个版本放在一个单独的翻译单元中(注意避免链接时优化,这可能会允许不一致的定义进行交互),基本上是围绕两个版本编写自己的兼容性包装器。程序的该组件将有自己的单个标头(可能带有函数指针变量),程序的其余部分是针对该标头编写的。#include

1赞 Paolo Crosetto 7/25/2023 #2

我不确定我是否理解您想要实现的目标,但如果您确定 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.sofunc1.soint func(int)

评论

0赞 kingsjester 7/26/2023
感谢您的回答!就我而言,这 2 个库不兼容 ABI,并且某些函数的签名不完全相同。此外,我需要一种方法来加载函数的类型,因为自己正确编写所有reinterpret_cast似乎很痛苦。if 版本 == 对应于我编写程序的方式。
1赞 dpronin 7/26/2023 #3

我会考虑以下方法,处理版本可能会更改且 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

评论

0赞 kingsjester 8/18/2023
谢谢!外部“C”在这里似乎不合适,因为您创建 .so 的文件是 C++ 的。我已经授予您赏金,因为在我看来,这是一个很好的答案,可以在运行时选择共享库,但它与问题并不完全对应。尽管如此,干杯!