在使用第三方库时,我经常发现自己需要编写胶水"代码";以处理跨版本更改的函数的原型。
以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在编译类型中选择了适当的调用,但它有两个问题:
- 通过";"无类型";函数指针,我们失去了类型检查。所以
p("a", "b")
是合法的,并且给出了垃圾结果 - 标准类型的提升似乎没有发生(再次,可能是因为我们通过无类型指针进行调用)
(我可能遗漏了其他问题,但这些是我认为最关键的一点)
我相信它可以通过一些宏魔术来实现:如果调用参数是分开的,宏可以生成找到合适原型的代码,然后逐一测试所有给定参数的类型兼容性。但我认为使用起来会复杂得多,所以我正在寻找一个更简单的解决方案。
有什么想法可以通过正确的类型检查+促销来实现这一点吗?我认为如果不使用编译器扩展就无法做到这一点,所以我的问题集中在针对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
),使我们能够通过定义两个函数(foo
和bar
)来演示这一点。对于问题中的情况,只有一个函数定义,并且只有一个名称,因此我们可以简化宏。我们也可以使用函数名称作为宏名称。(在预处理过程中,它不会被递归替换,这既是因为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
正如我们从反汇编中看到的那样,所有的事情都是在编译时处理的,并且是内联的——实际上没有创建临时数组等。