基于函数原型的调用参数的编译时决策

问题描述

通常,在使用第三方库时,我发现自己需要编写胶水“代码”来处理在各个版本中发生变化的函数原型。

例如,以Linux内核为例:这是一个常见的情况-假设我们具有函数int my_function(int param),并且在某个时候我们需要添加可选的void *data。不会破坏API,而是通过以下方式添加新参数:

int __my_function(int param,void *data);

static inline my_function(int param) {
    return __my_function(param,NULL);
}

这很好:它对不需要的人隐藏了新参数和API损坏。现有代码可以与旧原型一起继续使用my_function

但是,情况并非总是如此(在Linux和其他库中都不如此),我最终得到了这些片段,以处理我遇到的所有可能的版本:

#if LIBRARY_VERSION > 5
my_function(1,2,3);
#elif LIBRARY_VERSION > 4
my_function(1,2);
#else
my_function(1);
#endif

所以我在想,对于简单的原型更改(参数重新排序,“认”参数的添加/删除等),最好让编译器自动执行。

我希望自动完成此操作(无需指定确切的版本),因为有时很难找出引入更改的确切库/内核版本。因此,如果我不关心新参数,而仅使用0,我希望编译器在需要时使用0

我所能得到的最远的是:

#include <stdio.h>

#if 1
int f(int a,int b) {
    return a + b;
}
#else
int f(int a) {
    return a + 5;
}
#endif

int main(void) {
    int ret = 0;
    int (*p)() = (int(*)())f;
    if (__builtin_types_compatible_p(typeof(f),int(int,int)))
        ret = p(3,4);
    else if (__builtin_types_compatible_p(typeof(f),int(int)))
        ret = p(1);
    else
        printf("no matching call\n");
    printf("ret: %d\n",ret);
    return 0;
}

这有效-GCC在编译类型中选择了适当的调用,但是有两个问题:

  1. 通过“无类型”函数指针进行调用,我们将丢失类型检查。因此p("a","b")是合法的,并给出垃圾结果。
  2. 似乎没有进行标准类型升级(再次,可能是因为我们通过无类型指针进行了调用

(这里可能还缺少其他问题,但这是我认为最关键的要点)

我相信可以用一些宏魔术来代替它:如果将调用参数分开,则宏可以生成找到合适原型的代码,然后对所有给定的参数进行类型兼容性的测试。但是我认为使用起来会更加复杂,所以我正在寻找一个更简单的解决方案。

有任何想法如何通过适当的类型检查和促销来使其正常工作吗?我认为如果不使用编译器扩展就无法做到这一点,所以我的问题集中在针对Linux的现代GCC上。

编辑:将其宏观化后加上Acorn的演员表想法,剩下的是:

#define START_COMPAT_CALL(f,ret_type,params,args,ret_value) if (__builtin_types_compatible_p(typeof(f),ret_type params)) (ret_value) = ( ( ret_type(*) params ) (f) ) args
#define ELSE_COMPAT_CALL(f,ret_value) else START_COMPAT_CALL(f,ret_value)
#define END_COMPAT_CALL() else printf("no matching call!\n")

int main(void) {
    int ret = 0;

    START_COMPAT_CALL(f,int,(int,int),(999,9999),ret);
    ELSE_COMPAT_CALL(f,(int),(1),(char,char),(5,(float,(3,5),ret);
    END_COMPAT_CALL();
    printf("ret: %d\n",ret);
    return 0;
}

这对我已经指出的两点有效-它提供类型检查警告,并且可以正确执行升级但是还会针对所有“未选定”的呼叫站点发出警告:warning: function called through a non-compatible type。我尝试用__builtin_choose_expr包装通话,但我不走运:/

解决方法

您可以使用_Generic在标准C语言中完成此操作:

//  Define sample functions.
int foo(int a,int b)        { return a+b;   }
int bar(int a,int b,int c) { return a+b+c; }

//  Define names for the old and new types.
typedef int (*TypeVersion0)(int,int);
typedef int (*TypeVersion1)(int,int,int );

/*  Given an identifier (f) for a function and the arguments (a and b) we want
    to pass to it,test which type the function is and call it with
    appropriate arguments.

    Although only one call is evaluated during program execution (and indeed,only one is compiled into the executable because the compiler does the
    matching during compilation),the compiler requires that the calls all
    match the type of the function being used in the call.  So we cast the
    function to the type for the call it appears in.

    That appeases Clang (Apple Clang 11),but GCC 10.2 still complains,apparently tracking the function type through the cast. To work around
    this,we put the function pointer into a compound literal.
*/
#define MyFunction(f,a,b)                                          \
    _Generic(f,\
        TypeVersion0:  ((TypeVersion0) {(TypeVersion0) f}) (a,b),\
        TypeVersion1:  ((TypeVersion1) {(TypeVersion1) f}) (a,b,0) \
    )


#include <stdio.h>


int main(void)
{
    printf("%d\n",MyFunction(foo,3,4));
    printf("%d\n",MyFunction(bar,4));
}

您还可以使用函数名称作为宏名称,并删除函数标识符参数(f);我仅出于显示目的而实现了如上所示的方法。 (使用函数名称作为宏名称不会导致它被递归替换,这是因为C不会递归替换宏,而且该名称不会出现在替换文本中,并在其后带有左括号。) / p>

#define foo(a,b)                                                      \
    _Generic(foo,\
        TypeVersion0:  ((TypeVersion0) {(TypeVersion0) foo}) (a,\
        TypeVersion1:  ((TypeVersion1) {(TypeVersion1) foo}) (a,0) \
    )
…
    printf("%d\n",foo(3,4));

补充

这里是一个可以说是编译器永远不要抱怨的版本:

int NeverCalled();
#define Sanitize(Type,f)   \
    _Generic(f,Type: f,default: NeverCalled)
#define MyFunction(f,\
        TypeVersion0:  Sanitize(TypeVersion0,f) (a,\
        TypeVersion1:  Sanitize(TypeVersion1,0) \
    )

在此代码中,即使编译器内部扩展了所有_Generic运算符的所有分支,每个调用中的函数指针还是匹配的类型(已由{{1中的_Generic选择}}或为Sanitize。如果为NeverCalled,则编译器无法,因为它没有原型,因此无法检查参数是否匹配,并且只有在求值并且参数与实际函数不匹配的情况下,此行为才能不确定根据C 2018 6.5.2.2的定义。6.由于没有NeverCalled的定义,编译器甚至无法分析调用该函数时可能发生的情况。

理论上,性能不佳的编译器可能会在生成的目标代码中包含从未使用过的对NeverCalled的引用,然后链接程序会抱怨。可以通过定义最小的NeverCalled函数,甚至定义任何在可执行文件中占用零空间的符号(例如,将其别名为NeverCalled)来解决此问题。

,

最好的标准方法是编写一个包含#ifdef s测试的包装器:

int my_function(int param)
{
#if LIBRARY_VERSION > 5
    return lib_my_function(param,2,3);
#elif LIBRARY_VERSION > 4
    return lib_my_function(param,2);
#else
    return lib_my_function(param);
#endif
}

然后您拨打以下电话:

my_function(1);

不需要宏魔术,类型检查将照常进行,库中将来的API更改都已本地化,IDE和同事不会感到困惑,等等。


旁注:如果您真的想使用建议的解决方案,则可以通过为每个替代方案创建适当的指针来保持类型检查:

if (__builtin_types_compatible_p(typeof(f),int(int,int)))
    ret = ((int(*)(int,int)) f)(3,4);
else if (__builtin_types_compatible_p(typeof(f),int(int)))
    ret = ((int(*)(int)) f)(1);
else
    printf("no matching call\n");

但是,实际上,这样做没有任何优势,因为无论如何,您都有#ifdef来定义适当的f。在这一点上,您最好像上面显示的那样定义它,并避免所有这些,而且不需要GNU扩展。

,

假设:

int f (int a) {         // OLD API
    return a + 5;
}

int f (int a,int b) {  // NEW API
    return a + b;
}

然后,您可以编写一个宏来检查传递的参数数量,然后基于该宏调用实际函数或包装器:

static int f_wrapper (int a)
{
  return f(a,0);
}

#define p(...) _Generic( &(int[]){__VA_ARGS__},\
                 int(*)[2]: f,\
                 int(*)[1]: f_wrapper) (__VA_ARGS__)

这里(int[]){ __VA_ARGS__}以复合文字的形式创建了一个虚拟int[]数组。根据呼叫者代码,它将获得1或2个项目。存储到该伪数组期间的类型安全性与函数调用期间的类型安全性相同-接受可转换为int的参数/表达式,其他将产生编译器错误。

&(int[]) ...使用此虚拟复合文字的地址来生成数组指针类型-我们不能在_Generic内使用数组类型(因为传递给_Generic的数组将衰减为指针)但是我们可以有不同的数组指针类型。因此,取决于我们是否以int[2]int[1]结尾,将选择相应的_Generic子句(如果没有匹配项,则发生编译器错误)。

此代码的主要优点是调用者代码无需修改,它们可以使用带有1个或2个参数的p()

完整示例:

#include <stdio.h>

/* 
int f (int a) {         // OLD API
    return a + 5;
}
*/

int f (int a,int b) {  // NEW API
    return a + b;
}


static int f_wrapper (int a)
{
  return f(a,\
                 int(*)[1]: f_wrapper) (__VA_ARGS__)

int main (void) 
{
  int ret = 0;
  ret = p(3,4);
  printf("ret: %d\n",ret);
  ret = p(1);
  printf("ret: %d\n",ret);
  return 0;
}

输出:

ret: 7
ret: 1

gcc x86 -O3

f:
        lea     eax,[rdi+rsi]
        ret
.LC0:
        .string "ret: %d\n"
main:
        sub     rsp,8
        mov     esi,7
        mov     edi,OFFSET FLAT:.LC0
        xor     eax,eax
        call    printf
        mov     esi,1
        mov     edi,eax
        call    printf
        xor     eax,eax
        add     rsp,8
        ret

从反汇编中我们可以看到,所有内容都在编译时进行了处理并内联-实际未创建任何临时数组等。