使用变量声明/定义来实现弱链接是否可移植?

时间:2018-10-10 10:33:14

标签: c

我最近了解到:

int a;
文件范围内的

是变量声明,默认情况下具有外部链接。
因此,我可以使用它来实现诸如弱符号/功能链接之类的东西:

cat >lib.c <<'EOF'
#include "lib.h"
#include <stdio.h>
#include <stdint.h>

// This is declaration
// It will be initialized to NULL in case no definition is found
void (* const lib_callback_pnt)(int);

void lib_callback_default(int a) 
{
   printf("%s %d\n", __func__, a);
}

void lib_call(int a)
{
   printf("%s calling %p\n", __func__, 
    // this is not really portable
    (void*)(uintptr_t)(intmax_t)lib_callback_pnt
   );
   // call callback
   void (* const lib_callback_to_call)(int) = 
      lib_callback_pnt == NULL
      ? lib_callback_default
      : lib_callback_pnt;
   lib_callback_to_call(a);
}

EOF

cat >lib.h <<'EOF'
#ifndef LIB_H_
#define LIB_H_

extern void (* const lib_callback_pnt)(int);

void lib_callback_default(int a);
void lib_call(int a);

#endif
EOF

cat >main1.c <<EOF
#include "lib.h"

int main() {
    lib_call(42);
}

EOF

cat >main2.c <<'EOF'
#include "lib.h"
#include <stdio.h>

static void my_lib_callback(int a)
{
    printf("Hah! Overwritten lib callback!\n");
}

// this is definition
void (* const lib_callback_pnt)(int) = my_lib_callback;

int main() {
    lib_call(42);
}

EOF

cat >Makefile <<'EOF'
CC=gcc
CFLAGS=-Wall -Wextra -pedantic -std=c11
all:
    $(CC) $(CFLAGS) lib.c main1.c -o main1
    $(CC) $(CFLAGS) lib.c main2.c -o main2
EOF

lib.c库中,我声明了一个函数指针void (* const lib_callback_pnt)(int),它用作回调。函数指针未在lib.c中初始化,并且将默认初始化为NULL(导致静态存储持续时间)。

然后我有两个程序或用户应用程序,分别是main1.cmain2.c

main1.c只需调用库函数即可调用回调-回调未在任何地方初始化,因此默认情况下初始化为NULL-我可以在库中进行比较并适当地调用默认回调/ select动作。

然而main2.c声明了带有初始化的函数指针lib_callback_pnt-这是一个定义。在所有源文件中,此变量只有一个定义,因此链接器不会抱怨多个符号定义。当我们调用库时,指针已初始化,因此用户应用程序main2已成功覆盖了回调。

我们可以编译:

$ make
gcc -Wall -Wextra -pedantic -std=c11 lib.c main1.c -o main1
gcc -Wall -Wextra -pedantic -std=c11 lib.c main2.c -o main2

然后致电:

$ ./main1 
lib_call calling (nil)
lib_callback_default 42
$ ./main2 
lib_call calling 0x5627c07871cf
Hah! overwritten lib callback!

问题:

  • 这是便携式的吗?这符合C标准吗?
  • 是否存在/哪个库使用此类方法来使用户应用程序能够传递用户定义的回调和/或参数?为什么不经常使用这种方法?

2 个答案:

答案 0 :(得分:2)

您的lib.c实际上确实定义了lib_callback_pnt。 C11在6.9.2p2说:

  

具有文件范围的对象(没有初始化程序),存储类说明符或存储类说明符static的对象的标识符声明构成临时定义 。如果翻译单元包含一个或多个标识符的临时定义,并且翻译单元不包含该标识符的外部定义,则该行为就好像该翻译单元包含该标识符的文件范围声明,且复合类型为转换单元末尾的值,初始值设定为0。

因此lib.c中lib_callback_pnt的声明是一个临时定义。由于该转换单元不包含任何其他lib_callback_pnt显式定义它的声明,因此该行为应与使用“ = 0”对其进行初始化的真实定义相同。

默认情况下,带有ELF输出的gcc显然不完全符合此要求。
 在我的Linux系统上,如果我gcc -c lib.c; nm lib.o | grep lib_callback_pnt,我得到:

0000000000000008 C lib_callback_pnt

我的man nm文档解释说,“ C”表示“公共符号”:

  

“ C”该符号是常见的。通用符号是未初始化的数据。链接时,可能会出现多个具有相同名称的通用符号。如果符号定义在任何地方,则将通用符号视为未定义的引用。

因此您的方法确实适用于gcc / ELF,但根据C标准,它是不正确的,因此您不能指望它可以与其他编译器一起使用。

答案 1 :(得分:1)

lib.c中,void (* const lib_callback_pnt)(int);是对象(在此情况下为指针)的标识符的声明,该对象的文件范围没有初始化程序且没有存储类说明符。然后C 2018 6.9.2 2告诉我们:

  

具有文件范围且没有初始化程序,没有存储类说明符或具有存储类说明符 static 的对象的标识符声明构成临时定义< / em>。如果翻译单元包含一个或多个标识符的临时定义,并且翻译单元不包含该标识符的外部定义,则该行为就好像该翻译单元包含该标识符的文件范围声明,且复合类型为转换单元末尾的值,初始值设定为0。

这告诉我们在标准C中,lib_callback_pnt被初始化为零,即使它是在其他转换单元中定义的也是如此。而且,它的行为就好像它具有一个初始化程序一样,这意味着它是在lib.c中定义的。

此外,在lib_callback_pnt中定义了main2.c时,这违反了C 2018 6.9 5:

  

如果在表达式中使用了用外部链接声明的标识符(不是结果为整数常量的 sizeof _Alignof 运算符的操作数的一部分) ,在整个程序中的某个位置,标识符应该只有一个外部定义;否则,最多只能有一个。

注意

Unix中有一种传统,允许在一个翻译单元中声明int foo;,而在另一翻译单元中声明int foo = 1;。从技术上讲,这违反了C标准,但在编译器和链接器中通常可以使用。