编写库时最重要的规则和最佳实践之一就是放置所有符号
库成为特定于库的命名空间。由于namespace
关键字,C ++使这变得简单。在
C通常的方法是在标识符前加上一些特定于库的前缀。
C标准的规则对这些规则施加了一些限制(用于安全编译):C编译器可能只查看第一个
标识符的8个字符,因此foobar2k_eggs
和foobar2k_spam
可能会被解释为相同
标识符有效 - 但是在我们这个时代,每个现代编译器都允许任意长标识符
(21世纪)我们不应该为此烦恼。
但是如果你面对一些你无法改变符号名称/标识符的库呢?也许你有 只有一个静态二进制文件和标题,或者不想,或者不允许自己调整和重新编译。
答案 0 :(得分:121)
至少在静态库的情况下,您可以非常方便地解决它。
考虑库 foo 和 bar 的那些标题。为了本教程的目的,我还将为您提供源文件
int spam(void);
double eggs(void);
int the_spams;
double the_eggs;
int spam()
{
return the_spams++;
}
double eggs()
{
return the_eggs--;
}
int spam(int new_spams);
double eggs(double new_eggs);
int the_spams;
double the_eggs;
int spam(int new_spams)
{
int old_spams = the_spams;
the_spams = new_spams;
return old_spams;
}
double eggs(double new_eggs)
{
double old_eggs = the_eggs;
the_eggs = new_eggs;
return old_eggs;
}
我们希望在foobar程序中使用那些
#include <stdio.h>
#include "foo.h"
#include "bar.h"
int main()
{
const int new_bar_spam = 3;
const double new_bar_eggs = 5.0f;
printf("foo: spam = %d, eggs = %f\n", spam(), eggs() );
printf("bar: old spam = %d, new spam = %d ; old eggs = %f, new eggs = %f\n",
spam(new_bar_spam), new_bar_spam,
eggs(new_bar_eggs), new_bar_eggs );
return 0;
}
立即出现一个问题:C不知道超载。所以我们有两次两个函数 相同的名称,但签名不同。所以我们需要一些方法来区分它们。无论如何,让我们看看是什么 编译器不得不说:
example/ex01/ $ make
cc -c -o foobar.o foobar.c
In file included from foobar.c:4:
bar.h:1: error: conflicting types for ‘spam’
foo.h:1: note: previous declaration of ‘spam’ was here
bar.h:2: error: conflicting types for ‘eggs’
foo.h:2: note: previous declaration of ‘eggs’ was here
foobar.c: In function ‘main’:
foobar.c:11: error: too few arguments to function ‘spam’
foobar.c:11: error: too few arguments to function ‘eggs’
make: *** [foobar.o] Error 1
好的,这并不奇怪,它只是告诉我们,我们已经知道的,或者至少是怀疑的。
因此我们可以在不修改原始库的情况下以某种方式解决该标识符冲突 源代码或标题?事实上我们可以。
首先让我们解决编译时问题。为此我们围绕标题包括一个
一堆预处理器#define
指令,它们为库导出的所有符号添加前缀。
后来我们用一些不错的舒适包装头做了这个,但仅仅是为了展示
正在进行的是在 foobar.c 源文件中逐字记录:
#include <stdio.h>
#define spam foo_spam
#define eggs foo_eggs
# include "foo.h"
#undef spam
#undef eggs
#define spam bar_spam
#define eggs bar_eggs
# include "bar.h"
#undef spam
#undef eggs
int main()
{
const int new_bar_spam = 3;
const double new_bar_eggs = 5.0f;
printf("foo: spam = %d, eggs = %f\n", foo_spam(), foo_eggs() );
printf("bar: old spam = %d, new spam = %d ; old eggs = %f, new eggs = %f\n",
bar_spam(new_bar_spam), new_bar_spam,
bar_eggs(new_bar_eggs), new_bar_eggs );
return 0;
}
现在如果我们编译这个......
example/ex02/ $ make
cc -c -o foobar.o foobar.c
cc foobar.o foo.o bar.o -o foobar
bar.o: In function `spam':
bar.c:(.text+0x0): multiple definition of `spam'
foo.o:foo.c:(.text+0x0): first defined here
bar.o: In function `eggs':
bar.c:(.text+0x1e): multiple definition of `eggs'
foo.o:foo.c:(.text+0x19): first defined here
foobar.o: In function `main':
foobar.c:(.text+0x1e): undefined reference to `foo_eggs'
foobar.c:(.text+0x28): undefined reference to `foo_spam'
foobar.c:(.text+0x4d): undefined reference to `bar_eggs'
foobar.c:(.text+0x5c): undefined reference to `bar_spam'
collect2: ld returned 1 exit status
make: *** [foobar] Error 1
......首先看起来事情变得更糟。但仔细一看:实际上是编译阶段 很好。它只是现在抱怨有符号碰撞的链接器 它告诉我们发生这种情况的位置(源文件和行)。我们可以看到 那些符号没有前缀。
让我们看看带有 nm 实用程序的符号表:
example/ex02/ $ nm foo.o
0000000000000019 T eggs
0000000000000000 T spam
0000000000000008 C the_eggs
0000000000000004 C the_spams
example/ex02/ $ nm bar.o
0000000000000019 T eggs
0000000000000000 T spam
0000000000000008 C the_eggs
0000000000000004 C the_spams
所以现在我们对这些符号在一些不透明的二进制文件中加上前缀的练习提出了挑战。是的我知道 在这个例子的过程中,我们有源,可以在那里改变它。但就目前而言,只是假设 你只有那些 .o 文件,或者 .a (实际上只是一堆 .o )。
我们有一个特别有趣的工具: objcopy
objcopy适用于临时文件,因此我们可以像使用它一样使用它。有一个 选项/操作名为 - 前缀符号,你有3个猜测它的作用。
所以,让我们把这个家伙扔到我们顽固的图书馆:
example/ex03/ $ objcopy --prefix-symbols=foo_ foo.o
example/ex03/ $ objcopy --prefix-symbols=bar_ bar.o
nm 告诉我们这似乎有效:
example/ex03/ $ nm foo.o
0000000000000019 T foo_eggs
0000000000000000 T foo_spam
0000000000000008 C foo_the_eggs
0000000000000004 C foo_the_spams
example/ex03/ $ nm bar.o
000000000000001e T bar_eggs
0000000000000000 T bar_spam
0000000000000008 C bar_the_eggs
0000000000000004 C bar_the_spams
让我们尝试将这一切联系起来:
example/ex03/ $ make
cc foobar.o foo.o bar.o -o foobar
确实,它有效:
example/ex03/ $ ./foobar
foo: spam = 0, eggs = 0.000000
bar: old spam = 0, new spam = 3 ; old eggs = 0.000000, new eggs = 5.000000
现在我把它作为练习留给读者来实现一个自动提取的工具/脚本 使用 nm 的库的符号,写入结构的包装头文件
/* wrapper header wrapper_foo.h for foo.h */
#define spam foo_spam
#define eggs foo_eggs
/* ... */
#include <foo.h>
#undef spam
#undef eggs
/* ... */
并使用 objcopy 将符号前缀应用于静态库的目标文件。
原则上,共享库也可以这样做。但是,共享库,名称告诉它, 在多个程序之间共享,因此以这种方式搞乱共享库并不是一个好主意。
你不会到处写蹦床包装。更糟糕的是,您无法链接到共享库 在目标文件级别,但被迫进行动态加载。但这应该是它自己的文章。
请继续关注并快乐编码。
答案 1 :(得分:7)
C标准规则对这些规则施加了一些约束(用于安全编译):AC编译器可能只查看标识符的前8个字符,因此foobar2k_eggs和foobar2k_spam可以被有效地解释为相同的标识符 - 但是每个现代编译器允许任意长的标识符,所以在我们这个时代(21世纪),我们不应该为此烦恼。
这不仅仅是现代编译器的扩展;当前的C标准也要求编译器支持合理长的外部名称。我忘记了确切的长度,但如果我没记错的话,现在就是31个字符。
但是如果你面对一些你无法改变符号名称/标识符的库呢?也许你只有一个静态二进制文件和标题,或者不想,或者不允许自己调整和重新编译。
然后你被卡住了。抱怨图书馆的作者。我曾经遇到过这样一个错误:我的应用程序的用户无法在Debian上构建它,因为Debian的libSDL
链接libsoundfile
,其中(至少在当时)使用{{{}等变量严重污染全局命名空间{1}}(我没骗你!)。我向Debian抱怨,并且他们修复了他们的包并将修复程序发送到上游,我认为它已经应用了,因为我再也没有听说过这个问题。
我认为这是最好的方法,因为它解决了每个人的问题。你做的任何本地黑客都会将问题留在库中,以便下一个不幸的用户再次遇到并与之抗争。
如果你真的需要快速修复,并且你有源代码,你可以在makefile中添加一堆dsp
等来修复它。如果没有,请使用您找到的-Dfoo=crappylib_foo -Dbar=crappylib_bar
解决方案。
答案 2 :(得分:2)
如果您正在使用GCC,则--allow-multiple-definition链接器开关是一个方便的调试工具。这会使链接器变为使用第一个定义(而不是抱怨它)。关于它的更多信息here。
这有助于我在开发过程中获得供应商提供的库的源代码,并且由于某种原因需要跟踪到库函数。该开关允许您编译和链接源文件的本地副本,并仍链接到未修改的静态供应商库。一旦发现航程完成,不要忘记将开关从制造符号中拉出来。具有故意名称空间冲突的发布代码容易出现陷阱,包括无意的名称空间冲突。