2

经常在使用 3rd 方库时,我发现自己需要编写胶水“代码”来处理跨版本更改的函数原型。

以 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

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

我希望自动完成此操作(无需指定确切的版本),因为有时要查明引入更改的确切库/内核版本是过多的工作。因此,如果我不关心新参数并且可以使用 eg 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 在编译类型中选择适当的调用,但它有 2 个问题:

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

(我可能在这里遗漏了其他问题,但这些是我认为最关键的点)

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

有什么想法可以通过适当的类型检查+促销来实现吗?我认为如果不使用编译器扩展就无法完成,所以我的问题集中在针对 Linux 的现代 GCC。

编辑:在宏化它+为演员添加橡子的想法之后,我只剩下这个了:

#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_type, params, args, ret_value) else START_COMPAT_CALL(f, ret_type, params, args, 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, (int), (1), ret);
    ELSE_COMPAT_CALL(f, int, (char, char), (5, 9999), ret);
    ELSE_COMPAT_CALL(f, int, (float, int), (3, 5), ret);
    ELSE_COMPAT_CALL(f, int, (float, char), (3, 5), ret);
    END_COMPAT_CALL();
    printf("ret: %d\n", ret);
    return 0;
}

这适用于我注意到的两点 - 它提供类型检查警告并正确执行提升。它也会对所有“未选择”的呼叫站点发出警告:warning: function called through a non-compatible type. 我尝试用它来结束通话,__builtin_choose_expr但我没有运气:/

4

3 回答 3

3

您可以在标准 C 中使用_Generic

//  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 (*Type0)(int, int);
typedef int (*Type1)(int, int, int );

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

    However, the unchosen items in the _Generic would contain function calls
    with mismatched arguments.  Even though these are never evaluated, they
    violate translation-time contraints for function calls.  One solution would
    be to cast the function (automatically converted to a function pointer) to
    the type being used in the call expression.  However, GCC sees through this
    and complains the function is called through a non-compatible type, even
    though it never actually is.  To solve this, the Sanitize macro is used.

    The Sanitize macro is given a function type and a function.  It uses a
    _Generic to select either the function (when it matches the type) or a null
    pointer of the type (when the function does not match the type).  (And GCC,
    although unhappy with a call to a function with wrong parameters, is happy
    with a call to a null pointer.)
*/
#define Sanitize(Type, f)   \
    _Generic((f), Type: (f), default: (Type) 0)
#define MyFunction(f, a, b)                                      \
    _Generic(f,                                                  \
        Type0:  Sanitize(Type0, (f)) ((a), (b)),   \
        Type1:  Sanitize(Type1, (f)) ((a), (b), 0) \
    )


#include <stdio.h>


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

上面使用参数化函数名称 ( f) 来允许我们用定义的函数 (foobar) 来演示这一点。对于问题中的情况,函数定义只有一个,名字也只有一个,所以我们可以简化宏。我们也可以使用函数名作为宏名。(在预处理过程中它不会递归替换,因为 C 不会递归替换宏,并且因为名称没有出现在替换文本中,并且后面有左括号。)这看起来像:

#define Sanitize(Type, f)   \
    _Generic((f), Type: (f), default: (Type) 0)
#define foo(a, b)                                  \
    _Generic(foo,                                  \
        Type0:  Sanitize(Type0, foo) ((a), (b)),   \
        Type1:  Sanitize(Type1, foo) ((a), (b), 0) \
    )
…
    printf("%d\n", foo(3, 4));

Sanitize此处使用的定义由Artyer此答案中提供。)

于 2020-08-13T22:29:31.183 回答
1

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

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 扩展。

于 2020-08-13T21:38:24.580 回答
1

假设:

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]or结束int[1],相应的_Generic子句将被选中(或者如果没有匹配,编译器错误)。

这段代码的主要优点是调用者代码不需要修改,它们可以p()与 1 或 2 个参数一起使用。

完整示例:

#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, 0);
}

#define p(...) _Generic( &(int[]){__VA_ARGS__},       \
                 int(*)[2]: f,                        \
                 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, OFFSET FLAT:.LC0
        xor     eax, eax
        call    printf
        xor     eax, eax
        add     rsp, 8
        ret

正如我们从反汇编中看到的,一切都在编译时处理并内联——实际上没有创建临时数组等。

于 2020-08-14T09:51:20.080 回答