惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

人人都是产品经理
人人都是产品经理
Apple Machine Learning Research
Apple Machine Learning Research
云风的 BLOG
云风的 BLOG
罗磊的独立博客
博客园 - 三生石上(FineUI控件)
量子位
GbyAI
GbyAI
腾讯CDC
T
Tailwind CSS Blog
博客园 - Franky
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
D
Docker
G
Google Developers Blog
aimingoo的专栏
aimingoo的专栏
The GitHub Blog
The GitHub Blog
Microsoft Security Blog
Microsoft Security Blog
Stack Overflow Blog
Stack Overflow Blog
Hugging Face - Blog
Hugging Face - Blog
小众软件
小众软件
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
N
Netflix TechBlog - Medium
Jina AI
Jina AI
IT之家
IT之家
Y
Y Combinator Blog

C

C 语言快速通关教程 2026(共 96 集) - V2EX 各位大佬, C 語言該怎麼練習啊 分享一个代码优化导致的死循环 人再笨还能写不出内存安全的 C? 想念 C C11 的 _Generic,现实当中用得多不多?(尤其是公司项目) 坑爹的 GBK:大家都应该去用 UTF-8 分享一个用 AI 学习 C 语言的例子 一个简单的 C 程序,但是不明白区别在哪里 我这段 C 代码可以在编译时候输出结构体的大小,你们还有什么好点子, show me the code! 这段话是否正确?「取余这个运算,只有 Python 是对的。当初 C 这个老师教错了,那么一大票学生也就只敢跟着老师错。只有 Python 敢于站出来坚持正确答案。」 C 中可变参数如何直接传递到 printf() C 的内存打印实现 函数能否实现透传不定长度参数,最终由 printf 打印 请大佬帮忙修改一份 elf 文件里的数值 Linux 上 C 的程序遇到个异常退出问题,局部变量大小有限制?? C 语言新手求助:如何在 vscode 中使用第三方库? 将资源嵌入到可执行文件中并保持目录结构 一个简单实用的 C 工程示例, 附简洁的 Makefile 关于 C 语言的相关问题 轮子更新: C/C++ 跨平台小工具库 为什么下列程序进行的是无符号乘法? 用 riscv64- Linux -gnu-gcc 编译的 c 文件为啥能在 x8664 下运行? char *s = "0123"和 char s[] = "0123"的区别 有人能完整地解释一下 int (*daytab) [13]和 int *daytab[13]吗 在 c 语言中, int a;是 declaration 还是 definition 一个简单(奇怪)的 C 语言问题 c 语言中打印指针的值打印的是 OS 分配的虚拟地址的值吗?要怎么知道 OS 给这个 c 程序进程分配的虚拟地址的大小呢? gcc 是怎么找到 system 函数的定义(实现)的? 用 C 实现轻量级表达式完成策略定制化和模板内容生成
C 语言中的面向切面编程(AOP)
monkeyNik · 2024-03-07 · via C

概念

首先给出一段由 ChatGPT 给出的简短的 AOP 概念:

AOP 是一种编程方法,用来将在程序中多处重复出现的代码(比如日志、权限控制)从主要业务逻辑中抽取出来,提高代码的模块化和可维护性。

抽取后的代码会在原始的业务逻辑代码中特定的位置执行,这些位置由切点( Pointcut )定义。通常会在方法执行前、执行后、抛出异常时等特定点执行抽取出的代码,这些点被称为连接点( Join Point )。

概述

在 C 语言中,编译器所提供的编译期和执行期的能力相较于 java 或者其他语言来说会弱一些,这也许就是可能很少听到在 C 语言中搞面向切面编程的原因之一吧。

从上面的概念上来看,AOP 一般是在一些函数(或类方法)执行前后做一些额外处理,例如调用前增加一些权限控制,调用后增加一些日志记录。从这些行为上来说,任何语言其实都可以做到。我们可以简单的在一个函数的开始加一段逻辑或调用某个函数来实现权限验证,在函数返回前调用某个函数添加日志等等。类似如下代码:

void foo(void)
{
  if (!verify_identity())
    return;

  //...
  
  log("end");
}

但很显然,这么做会在程序的很多个函数中添加很多重复的代码(例如本例的verify_identitylog),以至于代码变得比较臃肿。

那么有没有什么办法来瘦身呢?

这就是 AOP 擅长的领域了。

写在示例之前

C 语言编译器没有提供很完整的 AOP 支持,因此我们需要自行手动实现,或者使用一些现有的库来实现。

本文将使用开源 C 语言库 Melon 的函数模板来实现上面的效果。

在 Melon 提供的函数模板组件中,实现了若干宏函数,这些宏函数都是用来定义不同类型的函数的。这些用宏来定义的函数和我们原生 C 语言中的函数的区别,简单来说就是,在我们实际要执行的函数逻辑外,再封装一个函数,这个函数会在我们指定的函数逻辑开始前和结束后调用一个回调函数(即函数的入口回调函数出口回调函数)。

基于函数模板的这一特性,Melon 中实现了一个 span 组件,用来度量使用函数模板定义的函数的时间开销。

但如果事情仅限于此,那么这种 AOP 很显然能做到的事情也基本仅限于此了。

因此,Melon 支持了 c99 ,并利用 c99 提供的宏特性,实现了将函数模板定义的函数的实参以可变参数的形式传递到入口和出口回调函数中。这就意味着,入口和出口回调函数可以访问函数的参数,并对参数的内容作出修改(主要针对指针指向的内存中的数据)。

这样,就给我们在回调函数中提供了更多的可操作空间。我们可以针对不同的函数,修改其参数值,从而来影响后续函数调用中的执行逻辑。例如前面的权限验证,我们可以将其大致简化为如下形式:

void entry_callback(char *file, char *func, int line, ...)
{
  va_list args;
  va_start(args, line);
  int *a = (int *)va_arg(args, int *);
  va_end(args);
  if (!verify_identity())
    *a = 0;
}

void exit_callback(char *file, char *func, int line, ...)
{
  va_list args;
  va_start(args, line);
  int *a = (int *)va_arg(args, int *);
  va_end(args);
  log("%d\n", *a);
}

void foo(int *a)
{
  if (!*a)
    return;

  //...
}

int bar(int *d, int e)
{
  if (!*d)
    return -1;

  //...
  return 0;
}

这里的代码只是一个示意,后面会给出一个实际可用的示例。

我们可以随意增加函数,这些函数都会利用同一对入口和出口函数来实现身份验证。

示例

下面就给出一个可用的使用函数模板实现 AOP 的 C 语言代码。

//a.c

#include "mln_func.h"
#include <stdio.h>
#include <string.h>
#include <stdarg.h>

MLN_FUNC_VOID(static, void, foo, (int *a, int b), (a, b), {
    printf("in %s: %d\n", __FUNCTION__, *a);
    *a += b;
})

MLN_FUNC(static, int, bar, (void), (), {
    printf("%s\n", __FUNCTION__);
    return 0;
})

static void my_entry(const char *file, const char *func, int line, ...)
{
    if (strcmp(func, "foo"))
        return;

    va_list args;
    va_start(args, line);
    int *a = (int *)va_arg(args, int *);
    va_end(args);

    printf("entry %s %s %d %d\n", file, func, line, *a);
    ++(*a);
}

static void my_exit(const char *file, const char *func, int line, ...)
{
    if (strcmp(func, "foo"))
        return;

    va_list args;
    va_start(args, line);
    int *a = (int *)va_arg(args, int *);
    va_end(args);

    printf("exit %s %s %d %d\n", file, func, line, *a);
}

int main(void)
{
    int a = 1;

    mln_func_entry_callback_set(my_entry);
    mln_func_exit_callback_set(my_exit);

    foo(&a, 2);
    return bar();
}

这段函数中,我们使用MLN_FUNCMLN_FUNC_VOID来定义了两个函数,即foobar。两个函数的逻辑很简单,就是 printf 输出当前函数名以及参数值(如果有参数的话)。同时,我们也使用了mln_func_entry_callback_setmln_func_exit_callback_set定义了两个全局回调函数,用来在函数调用开始和结束时调用。

我们可以看到,回调函数中使用strcmp对进入回调的函数做了过滤,仅对foo函数做额外处理。在入口回调中输出函数信息及第一个参数的值,随后修改参数指针指向的内存中的值。在出口回调中输出函数信息和参数值。

我们来编译一下(我们假定这个代码文件名为a.c):

cc -o a a.c -I /usr/local/melon/include/ -L /usr/local/melon/lib/ -lmelon -std=c99 -DMLN_C99 -DMLN_FUNC_FLAG

这里:

  • /usr/local/melon是 Melon 库的默认安装路径。
  • -std=c99是启用 c99 。
  • -DMLN_C99是定义一个名为MLN_C99的宏,这个宏用来启用函数模板组件中 C99 下才有的特性。
  • -DMLN_FUNC_FLAG用来定义一个名为MLN_FUNC_FLAG的宏,这个宏用来启用函数模板功能。是的,如果没有这个宏,上面的那些使用MLN_FUNC定义的函数就是普通的 C 语言函数,也不会触发入口和出口回调函数的调用。

执行一下看看效果:

entry a.c foo 6 1
in __mln_func_foo: 2
exit a.c foo 6 4
__mln_func_bar

可以看到:

  • 入口回调函数中,foo 的第一个参数指向的内存中的值为1
  • 进入foo的实际函数逻辑中,printf 输出当前的函数名为__mln_func_foo,以及此时看到的第一个参数的值为2,不再是1了,因为在入口回调函数中被修改了。__mln_func_foo这个函数执行的才是我们定义的逻辑,而foo是对__mln_func_foo的一个封装。
  • 出口回调函数中,我们看到第一个参数的值变为了4,因为它在我们给出的函数逻辑中做了修改。
  • 最后输出的是bar的实际执行逻辑所在的函数名,与foo的形式一致。

最后,我们去掉MLN_FUNC_FLAG这个宏再次编译一次:

cc -o a a.c -I /usr/local/melon/include/ -L /usr/local/melon/lib/ -lmelon -std=c99 -DMLN_C99

然后执行一下看看输出结果:

in foo: 1
bar

可以看得出,此时foobar不再是封装函数,而是我们定义的函数逻辑的函数名,即普通的 C 语言函数。

读到这里的都是真爱,感谢阅读!