之前笔者写了一篇博文 C++实现Go的defer功能,介绍了如何在C++语言中实现Go的defer功能,那在C语言中是否也可以实现这样的功能呢?本文就将介绍一下如何在C语言中实现Go的defer功能。

我们还是使用 C++实现Go的defer功能中的示例:

 1void test()
 2{
 3	FILE* fp = fopen("test.txt", "r");
 4	if (nullptr == fp)
 5		return;
 6
 7	defer(fclose(fp));
 8	if (...)
 9	{
10		return;
11	}
12	if (...)
13	{
14		return;
15	}
16	if (...)
17	{
18		return;
19	}
20}

为了实现该功能,需要借助编译器的扩展功能,GCC/Clang的cleanup属性,微软目前的编译器不支持该扩展属性,所以本文介绍的方法不适用于微软编译器。

一、GCC编译器

GCC的cleanup属性,可以参见GCC官方 文档var-attr-cleanup-cleanup_function或者 文档index-cleanup-variable-attribute

GCC下的cleanup属性用法:

1static void foo(int *p) { printf("foo called\n"); }
2static void bar(int *p) { printf("bar called\n"); }
3void baz(void)
4{
5  int x __attribute__((cleanup(foo)));
6  { int y __attribute__((cleanup(bar))); }
7}

GCC支持函数嵌套,cleanup属性也支持不带参数的函数调用(注意:C23以前是支持不带参数,C23还是必须要有一个参数):

1void test()
2{
3  void ff() { printf("ff called\n"); }
4  int z __attribute__((cleanup(ff)));
5}

所以要实现前面示例中的写法也很简单,使用宏实现一个嵌套函数来执行自定义的表达式,再定义一个变量带上cleanup属性即可:

1#define __CONCAT0__(a, b) a##b
2#define __CONCAT__(a, b) __CONCAT0__(a, b)
3#define __DEFER__(exp, COUNTER)                                                \
4  void __CONCAT__(_DEFER_FUNC_, COUNTER)() { exp; }                            \
5  int __CONCAT__(_DEFER_VAR_, COUNTER)                                         \
6      __attribute__((cleanup(__CONCAT__(_DEFER_FUNC_, COUNTER))))
7#define defer(exp) __DEFER__(exp, __COUNTER__)

二、Clang编译器

Clang的cleanup属性,可以参见Clang官方 文档。由于Clang目前不支持函数嵌套,但提供了一种叫做Block类型的语法扩展,参见 文档,有点类似C++的Lambda 表达式。

Clang的cleanup属性调用的函数,必须带有一个参数,否则会报错:

1error: 'cleanup' function 'ff' must take 1 parameter

Clang需要写成:

1void ff(int* p) { printf("ff called\n"); }
2void test()
3{
4  int z __attribute__((cleanup(ff)));
5}

但是前面示例的写法是:

1defer(fclose(fp));

函数内部的表达式无法写到外部去,所以就需要借助Clang的Block类型语法扩展了:

 1//需要先定义一个外部函数,且必须带一个参数,这个参数的类型是一个`Block`类型
 2static inline void __cleanup__(void (^*fn)(void)) { (*fn)(); }
 3void test() {
 4  // 定义一个ff变量,该变量是一个`Block`类型
 5  void (^ff)() = ^{
 6    printf("ff called\n");
 7  };
 8  // 可以像函数一样使用,直接调用
 9  ff();
10  // 定义一个`Block`类型的x变量,它的值一个是表达式,且拥有`cleanup`属性,调用外部函数__cleanup__
11   __attribute__((cleanup(__cleanup__))) void (^x)() = ^{
12    printf("x called\n");
13  };
14}

编译时必须使用-fblocks参数进行编译,同时链接BlocksRuntime库。Ubuntu下可以使用下面的命令安装BlocksRuntime库:

1sudo apt install libblocksruntime-dev

如果不安装也可以自己定义一下变量:

1void *_NSConcreteGlobalBlock[32] = {0};
2void *_NSConcreteStackBlock[32] = {0};

注意:MinGW下目前没有可用的BlocksRuntime库,在链接时会报错:

1undefined reference to `__imp__NSConcreteGlobalBlock'
2undefined reference to `__imp__NSConcreteStackBlock'

这就必须自己定义一下变量了:

1void *__imp__NSConcreteGlobalBlock[32] = {0};
2void *__imp__NSConcreteStackBlock[32] = {0};

三、跨GCC/Clang编译器

综上,为了让Clang与GCC都能支持前面示例中的写法,可以使用宏来分别处理:

 1#define __CONCAT0__(a, b) a##b
 2#define __CONCAT__(a, b) __CONCAT0__(a, b)
 3
 4#if defined(__clang__)
 5#if defined(__MINGW32__) || defined(__MINGW64__)
 6void *__imp__NSConcreteGlobalBlock[32] = {0};
 7void *__imp__NSConcreteStackBlock[32] = {0};
 8#elif defined(__LINUX__)
 9void *_NSConcreteGlobalBlock[32] = {0};
10void *_NSConcreteStackBlock[32] = {0};
11#endif
12static inline void __cleanup__(void (^*fn)(void)) { (*fn)(); }
13#define defer(exp)                                                             \
14  __attribute__((cleanup(__cleanup__))) void (                                 \
15      ^__CONCAT__(_DEFER_VAR_, __COUNTER__))(void) = ^{                        \
16    exp;                                                                       \
17  }
18#elif defined(__GNUC__)
19#define __DEFER__(exp, COUNTER)                                                \
20  void __CONCAT__(_DEFER_FUNC_, COUNTER)(int *p) { exp; }                      \
21  int __CONCAT__(_DEFER_VAR_, COUNTER)                                         \
22      __attribute__((cleanup(__CONCAT__(_DEFER_FUNC_, COUNTER))))
23#define defer(exp) __DEFER__(exp, __COUNTER__)
24#else
25#error "compiler not support!"
26#endif

下面给出完整代码:

main.c:

 1#include <stdio.h>
 2#include <stdlib.h>
 3
 4#define __CONCAT0__(a, b) a##b
 5#define __CONCAT__(a, b) __CONCAT0__(a, b)
 6
 7#if defined(__clang__)
 8#if defined(__MINGW32__) || defined(__MINGW64__)
 9void *__imp__NSConcreteGlobalBlock[32] = {0};
10void *__imp__NSConcreteStackBlock[32] = {0};
11#elif defined(__LINUX__)
12void *_NSConcreteGlobalBlock[32] = {0};
13void *_NSConcreteStackBlock[32] = {0};
14#endif
15static inline void __cleanup__(void (^*fn)(void)) { (*fn)(); }
16#define defer(exp)                                                             \
17  __attribute__((cleanup(__cleanup__))) void (                                 \
18      ^__CONCAT__(_DEFER_VAR_, __COUNTER__))(void) = ^{                        \
19    exp;                                                                       \
20  }
21#elif defined(__GNUC__)
22#define __DEFER__(exp, COUNTER)                                                \
23  void __CONCAT__(_DEFER_FUNC_, COUNTER)(int *p) { exp; }                      \
24  int __CONCAT__(_DEFER_VAR_, COUNTER)                                         \
25      __attribute__((cleanup(__CONCAT__(_DEFER_FUNC_, COUNTER))))
26#define defer(exp) __DEFER__(exp, __COUNTER__)
27#else
28#error "compiler not support!"
29#endif
30
31void ff(int *p) { printf("ff\n"); }
32
33void myfree(char **p) {
34  if (*p) {
35    printf("myfree:%p\n", *p);
36    free(*p);
37  }
38}
39
40int main(int argc, char *argv[]) {
41  FILE *fp = fopen("test.txt", "r");
42  defer(printf("close file\n"); if (fp) fclose(fp));
43  char *__attribute__((cleanup(myfree))) p = malloc(10);
44  char *p1 [[gnu::cleanup(myfree)]] = malloc(10);
45
46  int a [[gnu::cleanup(ff)]] = 10;
47  defer(printf("call defer1, a = %d\n", a));
48  a = 20;
49  defer(printf("call defer2, a = %d\n", a));
50
51  return 0;
52}

CMakeList.txt

 1cmake_minimum_required(VERSION 3.25.0)
 2project(t)
 3
 4if(CMAKE_C_COMPILER_ID MATCHES "Clang")
 5add_compile_options(
 6    -gdwarf-4
 7    -fblocks
 8)
 9endif()
10aux_source_directory(. SRC)
11add_executable(${PROJECT_NAME} ${SRC})

运行结果如下:

在这里插入图片描述

有了编译器的这一扩展属性的支持,写C语言也可以减轻程序员释放资源的心智负担了,不用担心某个分支遗忘了释放资源了。而且也可以像C++那样写Lambda 表达式了,而且默认是全部捕获函数内的所有变量。

以上代码在MinGW下使用GCC 14.2/Clang 18.1.8、Ubuntu下使用GCC 11.4/Clang 17.0.6、MacOS下使用Clang 9.0编译运行通过。

如果本文对你有帮助,欢迎点赞收藏!