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



在使用第三方库时,我经常发现自己需要编写胶水"代码";以处理跨版本更改的函数的原型。

以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 calln");
printf("ret: %dn", ret);
return 0;
}

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

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

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

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

有什么想法可以通过正确的类型检查+促销来实现这一点吗?我认为如果不使用编译器扩展就无法做到这一点,所以我的问题集中在针对Linux的现代GC上。

编辑:在对其进行宏观化+添加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_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: %dn", ret);
return 0;
}

这与我注意到的2点有关——它给出了类型检查警告,并且正确地执行了升级但是它还针对所有";未选择的";呼叫站点: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 (*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("%dn", MyFunction(foo, 3, 4));
printf("%dn", 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("%dn", foo(3, 4));

(这里使用的Sanitize的定义由Artyer在这个答案中提供。)

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

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 calln");

但实际上,这并没有什么好处,因为无论如何都有#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, 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: %dn", ret);
ret = p(1);
printf("ret: %dn", ret);
return 0;
}

输出:

ret: 7
ret: 1

gcc x86-O3

f:
lea     eax, [rdi+rsi]
ret
.LC0:
.string "ret: %dn"
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

正如我们从反汇编中看到的那样,所有的事情都是在编译时处理的,并且是内联的——实际上没有创建临时数组等。

最新更新