Requires:
- compiler: gcc 3.1 later
이번은 gcc builtin 중 __builtin_choose_expr
에 대해서 살펴봅시다.
이 built-in function은 C++에는 없고 C에만 존재합니다.
마치 C의 3-way operator(? : operator)처럼 동작합니다.
그러나 run-time이 아니라 compile-time에 동작한다는 것을 명심해야 합니다.
이 built-in은 gcc-3.1에 ‘Aldy Hernandez’ patch를 통해 추가되었습니다.
gcc 문서의 설명을 확인해보겠습니다.
Built-in Function: type __builtin_choose_expr (const_exp, exp1, exp2) |
---|
You can use the built-in function __builtin_choose_expr to evaluate code depending on the value of a constant expression. This built-in function returns exp1 if const_exp, which is an integer constant expression, is nonzero. Otherwise it returns exp2. This built-in function is analogous to the ‘? :’ operator in C, except that the expression returned has its type unaltered by promotion rules. Also, the built-in function does not evaluate the expression that is not chosen. For example, if const_exp evaluates to true, exp2 is not evaluated even if it has side effects... ref. https://gcc.gnu.org/onlinedocs/gcc-7.4.0/gcc/Other-Builtins.html#Other-Builtins
|
gcc code를 살펴보면 이 builtin function이 어떻게 동작하는지에 대해 쉽게 이해하실 수 있을 겁니다.
gcc 7.4의 코드를 살펴보겠습니다.
-> git checkout gcc-7_4_0-release
$ cat ./gcc/c/c-parser.c
...
case RID_CHOOSE_EXPR:
{
...
e1_p = &(*cexpr_list)[0];
e2_p = &(*cexpr_list)[1];
e3_p = &(*cexpr_list)[2];
c = e1_p->value;
mark_exp_read (e2_p->value);
mark_exp_read (e3_p->value);
if (TREE_CODE (c) != INTEGER_CST
|| !INTEGRAL_TYPE_P (TREE_TYPE (c)))
error_at (loc,
"first argument to %<__builtin_choose_expr%> not"
" a constant");
constant_expression_warning (c);
expr = integer_zerop (c) ? *e3_p : *e2_p;
set_c_expr_source_range (&expr, loc, close_paren_loc);
...
코드들 자세히 보시면 3-way ooperator로 구현이 되었다는 것을 보실 수 있습니다.
expr = integer_zerop (c) ? *e3_p : *e2_p;
->
const_exp ? exp1 : exp2
Don’t confuse gcc builtin as a function called at compile time
첫 번째 argument는 const_exp
이어야 합니다.
다시 말해, variable이 compile time에 정해진다고 해도 argument로 사용할 수 없다는 뜻입니다.
-> can we use the variables for the first argument?
#include <stdio.h>
int main(void) {
const int cond = 1;
char *boolean = __builtin_choose_expr(cond, "true", "false");
printf("boolean: %s\n", boolean);
return 0;
}
-> gcc version 7.4.0 –target=x86_64-linux-gnu
$ gcc -o cond_expr cond_expr.c
cond_expr.c: In function ‘main’:
cond_expr.c:5:18: error: first argument to ‘__builtin_choose_expr’ not a constant
char *boolean = __builtin_choose_expr(cond, "true", "false");
compile error가 발생하는걸 볼 수 있습니다.
즉, 다음과 같이 const expr
만 사용할 수 있습니다.
-> use const expr for the first argument
#define BOOL_TO_STR(__x) \
__builtin_choose_expr(((__x)), "true", "false")
int main(void) {
printf("%s\n", BOOL_TO_STR(0));
printf("%s\n", BOOL_TO_STR(1));
return 0;
}
-> gcc version 7.4.0 –target=x86_64-linux-gnu
$ gcc -o bool_to_str bool_to_str.c
$ ./bool_to_str
false
true
-> assembly: x86_64 AT&T
int main(void) {
400526: 55 push %rbp
400527: 48 89 e5 mov %rsp,%rbp
printf("%s\n", BOOL_TO_STR(0));
40052a: bf d4 05 40 00 mov $0x4005d4,%edi
40052f: e8 cc fe ff ff callq 400400 <puts@plt>
printf("%s\n", BOOL_TO_STR(1));
400534: bf da 05 40 00 mov $0x4005da,%edi
400539: e8 c2 fe ff ff callq 400400 <puts@plt>
return 0;
40053e: b8 00 00 00 00 mov $0x0,%eax
}
400543: 5d pop %rbp
400544: c3 retq
400545: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
40054c: 00 00 00
40054f: 90 nop
$ strings -t x bool_to_str
...
5d4 false
5da true
__builtin_choose_expr
가 compile-time에 각각 exp1
또는 exp2
로 변환 된 것을 볼 수 있습니다.
const exp
만 사용할 수 있기 때문에, 불필요한 builtin function 아닌가 생각할 수도 있습니다.
그러나, 의외로 많은 활용을 할 수 있습니다.
예를 들어, 특정 bit가 설정되어 있는지 compile time에 확인하는 IS_MASKED macro를 다음과 같이 작성할 수 있습니다.
-> e.g. masked
#define IS_MASKED(__value) \
__builtin_choose_expr(((__value) & 0x1), 1, 0)
int main(void) {
printf("masked: %d\n", IS_MASKED(0xff));
printf("masked: %d\n", IS_MASKED(0x02));
printf("masked: %d\n", IS_MASKED(0x03));
}
-> gcc version 7.4.0 –target=x86_64-linux-gnu
$ gcc -o masked masked.c
$ ./masked
masked: 1
masked: 0
masked: 1
sizeof와 같이 활용할 수도 있습니다.
Kernel에서 eBPF code를 살펴보면, 아래와 같이 sizeof와 같이 활용한 예가 있습니다.
-> e.g. sizeof in BPF
$ cat ./include/trace/bpf_probe.h
/* cast any integer, pointer, or small struct to u64 */
#define UINTTYPE(size) \
__typeof__(__builtin_choose_expr(size == 1, (u8)1, \
__builtin_choose_expr(size == 2, (u16)2, \
__builtin_choose_expr(size == 4, (u32)3, \
__builtin_choose_expr(size == 8, (u64)4, \
(void)5)))))
#define __CAST_TO_U64(x) ({ \
typeof(x) __src = (x); \
UINTTYPE(sizeof(x)) __dst; \
memcpy(&__dst, &__src, sizeof(__dst)); \
(u64)__dst; })
}
이 builtin function 3-way operator와는 약간 다른 차이점이 있습니다.
3-way operator는 더욱 사이즈가 큰 type으로 type cast
되어 return 되지만, builtin function
은 exp1
또는 exp2
각자의 type
이 return 됩니다.
-> check return type
#include <stdio.h>
#define RET_TYPE(__x) \
__builtin_choose_expr((__x), ret_int(), ret_char())
int ret_int(void) {
return 1;
}
char ret_char(void) {
return 'a';
}
int main(void) {
/* return char */
printf("sizeof: %lu\n", sizeof(RET_TYPE(0)));
/* return int */
printf("sizeof: %lu\n", sizeof(RET_TYPE(1)));
return 0;
}
-> gcc version 7.4.0 –target=x86_64-linux-gnu
$ gcc -o return_type return_type.c
./return_type
sizeof: 1
sizeof: 4
__builtin_types_compatible_p
와 같이 사용하면 더욱 멋진 활용이 가능합니다만, 다음 post를 위해 아껴두겠습니다. :)
__builtin_choose_expr
은 const expr를 return 하는 다른 build-in function과 조합해서 많이 사용합니다.
예를 들어 __builtin_types_compatible_p
와 조합하면 C에서 불가능해만 보였던 type-arguments
을 위한 function overloading
구현도 가능합니다.
-> functon overloading
#include <stdio.h>
#define debug(var) \
printf( \
__builtin_choose_expr( \
__builtin_types_compatible_p(typeof(var), int) \
,"%d\n", __builtin_choose_expr( \
__builtin_types_compatible_p(typeof(var), char []) \
,"%s\n", "0x%x\n")), var);
int main(void) {
debug(1234);
debug("jooojub");
return 0;
}
-> gcc version 7.4.0 –target=x86_64-linux-gnu
$ gcc -o function_overloading function_overloading.c
$ ./function_overloading
1234
jooojub
__builtin_types_compatible_p
에 대해서는 다른 post에서 다루겠습니다.
또한 standard C에서도 macro를 이용해
number of arguments
를 구현할 수 있습니다. 기회가 된다면 이 또한 다른 post에서 다루겠습니다.
__builtin_choose_expr
는 쉽게 단순해서 이해하기 쉽지만, 유용하게 사용한다면 불필요한 코드를 줄일 수 있으므로, 많이 활용하길 바랍니다.