问题描述
通常,在使用第三方库时,我发现自己需要编写胶水“代码”来处理在各个版本中发生变化的函数原型。
例如,以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在编译类型中选择了适当的调用,但是有两个问题:
(这里可能还缺少其他问题,但这是我认为最关键的要点)
我相信可以用一些宏魔术来代替它:如果将调用参数分开,则宏可以生成找到合适原型的代码,然后对所有给定的参数进行类型兼容性的测试。但是我认为使用起来会更加复杂,所以我正在寻找一个更简单的解决方案。
有任何想法如何通过适当的类型检查和促销来使其正常工作吗?我认为如果不使用编译器扩展就无法做到这一点,所以我的问题集中在针对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
从反汇编中我们可以看到,所有内容都在编译时进行了处理并内联-实际未创建任何临时数组等。