1

假设我们使用的是 gcc/g++ 和一个随机委员会指定的 C API。本规范定义了功能

void foo(void);

现在,根据本规范有几种实现方式。让我们选择两个作为示例并将它们称为nfooand xfoo(由 libnfoo 和 libxfoo 分别作为静态和动态库提供)。

现在,我们要为foo-API 创建一个 C++ 框架。因此,我们指定一个抽象类

class Foo
{
  public:
    virtual void foo(void) = 0;
};

和相应的实现

#include <nfoo.h>
#include "Foo.h"

class NFoo : public Foo
{
  public:
    virtual void foo(void) 
    {
      ::foo(); // calling foo from the nfoo C-API
    }
};

#include <xfoo.h>
#include "Foo.h"

class XFoo : public Foo
{
  public:
    virtual void foo(void) 
    {
      ::foo(); // calling foo from the xfoo C-API
    }
};

现在,我们面临一个问题:我们如何将所有内容创建(即链接)到一个库中?

我看到将与fooC API 实现的函数符号发生符号冲突。

我已经尝试将 C++ 包装器实现拆分为单独的静态库,但后来我(再次)意识到静态库只是未链接目标文件的集合。所以这根本行不通,除非有办法将 C 库完全链接到包装器中并删除/隐藏它们的符号。

建议高度赞赏。

更新:最佳解决方案应同时支持这两种实现。

注意:代码并不意味着可以正常工作。将其视为伪代码。

4

5 回答 5

2

您能否在运行时使用dlopen / dlsym来解决您的 foo 调用。

类似于来自链接的示例代码(可能无法编译):

void    *handle,*handle2;
void (*fnfoo)() = null;
void (*fxfoo)() = null;


/* open the needed object */
handle = dlopen("/usr/home/me/libnfoo.so", RTLD_LOCAL | RTLD_LAZY);
handle2 = dlopen("/usr/home/me/libxfoo.so", RTLD_LOCAL | RTLD_LAZY);

fnfoo = dlsym(handle, "foo");
fxfoo = dlsym(handle, "foo");


/* invoke function */
(*fnfoo)(); 
(*fxfoo)();

// 不要忘记 dlclose() 的

否则,需要修改库中的符号。这不能移植到 Windows。

于 2013-03-28T18:32:25.483 回答
1

首先,如果您要在 C++ 代码中封装 C API,您应该将该依赖项隐藏在编译防火墙后面。这是为了 (1) 避免使用来自 C API 的名称污染全局命名空间,以及 (2) 将用户代码从依赖项中释放到第三方标头。在此示例中,可以进行相当简单的修改以隔离对 C API 的依赖关系。你应该做这个:

// In NFoo.h:

#include "Foo.h"

class NFoo : public Foo
{
  public:
    virtual void foo(void);
};

// In NFoo.cpp:

#include "NFoo.h"

#include <nfoo.h>

void NFoo::foo(void) {
  ::foo();   // calling foo from the nfoo C-API
}

上面的重点是 C API 头文件<nfoo.h>,仅包含在 cpp 文件中,而不包含在头文件中。这意味着用户代码不需要提供 C API 标头来编译使用您的库的代码,C API 中的全局命名空间名称也不会与正在编译的任何其他内容发生冲突。此外,如果您的 C API(或任何其他外部依赖项)在使用 API 时需要创建许多东西(例如句柄、对象等),那么您也可以将它们包装在 PImpl(指向前向声明的实现类,仅在 cpp 文件中声明定义)以实现相同的外部依赖隔离(即“编译防火墙”)。

现在,基本的东西已经不碍事了,我们可以转移到手头的问题:同时使用名称冲突符号链接到两个 C API。这是一个问题,没有简单的出路。上面的编译防火墙技术实际上是关于在编译期间隔离和最小化依赖关系,通过这种方式,您可以轻松编译依赖于两个名称冲突的 API 的代码(在您的版本中不是这样),但是,您仍然会受到打击到达链接阶段时出现 ODR(一个定义规则)错误。

这个线程有一些有用的技巧来解决 C API 名称冲突。总之,您有以下选择:

  • 如果您可以访问两个 C API 中的至少一个的静态库(或目标文件),那么您可以使用objcopy(在 Unix/Linux 中)之类的实用程序为该静态库(目标文件)中的所有符号添加前缀),例如,使用命令objcopy --prefix-symbols=libn_ libn.o为 libn.o 中的所有符号添加前缀libn_. 当然,这意味着您需要在 API 的头文件中的声明中添加相同的前缀(或仅使用您需要的内容制作一个简化版本),但从维护的角度来看,这不是问题,只要因为您为该外部依赖项设置了适当的编译防火墙。

  • 如果您无权访问静态库(或目标文件)或不想在上述方法中执行此操作(有些麻烦),则必须使用动态库。然而,这并不像听起来那么微不足道(而且我什至不会进入DLL Hell的主题)。您必须使用动态链接库(或共享对象文件)的动态加载,而不是更常见的静态加载。也就是说,您必须使用 LoadLibrary / GetProcAddress / FreeLibrary(适用于 Windows)和 dlopen / dlsym / dlclose(所有类 Unix 操作系统)。这意味着您必须为您希望使用的每个函数单独加载和设置函数指针地址。同样,如果在代码中正确隔离了依赖项,这将只是编写所有这些重复代码的问题,但这里并没有太大的危险。

  • 如果您对 C API 的使用比 C API 本身简单得多(即,您只使用数百个函数中的几个函数),那么创建两个动态库可能会容易得多,每个 C API 一个,它只导出有限的函数子集,赋予它们唯一的名称,包装对 C API 的调用。然后,您的主应用程序或库可以直接链接到这两个动态库(静态加载)。当然,如果您需要对 C API 中的所有函数执行此操作,那么经历所有这些麻烦是没有意义的。

因此,您可以选择对您来说更合理或更可行的方法,毫无疑问,这需要大量的手动工作来解决这个问题。

于 2013-03-28T17:12:24.690 回答
0

如果您一次只想访问一个库实现,一种自然的方法是作为动态库

在 Windows 中也可以一次访问两个或多个库实现,因为 Windows 动态库提供了对内部任何内容的完全封装

于 2013-03-28T14:37:53.320 回答
0

IIUCifdef是您所需要的

放入#define _NFOO nfoo lib 和#define XFOOxfoo lib。

还要记住,如果 nfoo lib 和 xfoo lib 都有一个名为 Foo 的函数,那么编译过程中就会出错。为避免这种情况,GCC/G++ 通过名称修饰使用函数重载。

然后您可以使用 ifdefs 检查 xfoo 是否已链接

#ifdef XFOO
//call xfoo's foo()
#endif
于 2013-03-28T14:38:35.767 回答
0

链接器无法区分同一符号名称的两个不同定义,因此如果您尝试使用具有相同名称的两个函数,则必须以某种方式将它们分开。

分离它们的方法是将它们放入动态库中。您可以选择从动态库中导出哪些内容,这样您就可以导出包装器,同时隐藏底层 API 函数。您还可以在运行时加载动态库并一次绑定到一个符号,因此即使在多个中定义了相同的名称,它们也不会相互干扰。

于 2013-03-28T15:30:07.520 回答