首页 > 解决方案 > 基于函数原型的调用参数编译时决策

问题描述

经常在使用 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但我没有运气:/

标签: cgcc

解决方案


您可以在标准 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此答案中提供。)


推荐阅读