概念
首先給出一段由ChatGPT給出的簡短的AOP概念:
AOP是一種編程方法,用來將在程序中多處重複出現的代碼(比如日誌、權限控制)從主要業務邏輯中抽取出來,提高代碼的模塊化和可維護性。
抽取後的代碼會在原始的業務邏輯代碼中特定的位置執行,這些位置由切點(Pointcut)定義。通常會在方法執行前、執行後、拋出異常時等特定點執行抽取出的代碼,這些點被稱為連接點(Join Point)。
概述
在C語言中,編譯器所提供的編譯期和執行期的能力相較於java或者其他語言來説會弱一些,這也許就是可能很少聽到在C語言中搞面向切面編程的原因之一吧。
從上面的概念上來看,AOP一般是在一些函數(或類方法)執行前後做一些額外處理,例如調用前增加一些權限控制,調用後增加一些日誌記錄。從這些行為上來説,任何語言其實都可以做到。我們可以簡單的在一個函數的開始加一段邏輯或調用某個函數來實現權限驗證,在函數返回前調用某個函數添加日誌等等。類似如下代碼:
void foo(void)
{
if (!verify_identity())
return;
//...
log("end");
}
但很顯然,這麼做會在程序的很多個函數中添加很多重複的代碼(例如本例的verify_identity和log),以至於代碼變得比較臃腫。
那麼有沒有什麼辦法來瘦身呢?
這就是AOP擅長的領域了。
寫在示例之前
C語言編譯器沒有提供很完整的AOP支持,因此我們需要自行手動實現,或者使用一些現有的庫來實現。
本文將使用開源C語言庫Melon的函數模板來實現上面的效果。
在Melon提供的函數模板組件中,實現了若干宏函數,這些宏函數都是用來定義不同類型的函數的。這些用宏來定義的函數和我們原生C語言中的函數的區別,簡單來説就是,在我們實際要執行的函數邏輯外,再封裝一個函數,這個函數會在我們指定的函數邏輯開始前和結束後調用一個回調函數(即函數的入口回調函數和出口回調函數)。
基於函數模板的這一特性,Melon中實現了一個span組件,用來度量使用函數模板定義的函數的時間開銷。
但如果事情僅限於此,那麼這種AOP很顯然能做到的事情也基本僅限於此了。
因此,Melon支持了c99,並利用c99提供的宏特性,實現了將函數模板定義的函數的實參以可變參數的形式傳遞到入口和出口回調函數中。這就意味着,入口和出口回調函數可以訪問函數的參數,並對參數的內容作出修改(主要針對指針指向的內存中的數據)。
這樣,就給我們在回調函數中提供了更多的可操作空間。我們可以針對不同的函數,修改其參數值,從而來影響後續函數調用中的執行邏輯。我們也可以利用回調函數的處理結果來決定是否應該調用實際函數(也就是過濾功能)。例如前面的權限驗證,我們可以將其大致簡化為如下形式:
int entry_callback(char *file, char *func, int line, ...)
{
va_list args;
va_start(args, line);
int *id = (int *)va_arg(args, int *);
va_end(args);
return !verify_identity(id)? -1: 0;
}
void exit_callback(char *file, char *func, int line, ...)
{
va_list args;
va_start(args, line);
int *id = (int *)va_arg(args, int *);
va_end(args);
log("%d\n", *id);
}
void foo(int *id)
{
//...
}
int bar(int *id, int e)
{
//...
return 0;
}
這裏的代碼只是一個示意,後面會給出一個實際可用的示例。
我們可以隨意增加函數,這些函數都會利用同一對入口和出口函數來實現身份驗證。
示例
下面就給出一個可用的使用函數模板實現AOP的C語言代碼。
#include "mln_func.h"
#include <stdio.h>
#include <string.h>
#if defined(MLN_C99)
#include <stdarg.h>
#endif
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 int my_entry(const char *file, const char *func, int line, ...)
{
if (strcmp(func, "foo")) {
printf("%s won't be executed\n", func);
return -1;
}
#if defined(MLN_C99)
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);
#else
printf("entry %s %s %d\n", file, func, line);
#endif
return 0;
}
static void my_exit(const char *file, const char *func, int line, ...)
{
if (strcmp(func, "foo"))
return;
#if defined(MLN_C99)
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);
#else
printf("exit %s %s %d\n", file, func, line);
#endif
}
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_FUNC和MLN_FUNC_VOID來定義了兩個函數,即foo和bar。兩個函數的邏輯很簡單,就是printf輸出當前函數名以及參數值(如果有參數的話)。同時,我們也使用了mln_func_entry_callback_set和mln_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 8 1
in __mln_func_foo: 2
exit a.c foo 8 4
bar won't be executed
可以看到:
- 入口回調函數中,foo的第一個參數指向的內存中的值為
1。 - 進入
foo的實際函數邏輯中,printf輸出當前的函數名為__mln_func_foo,以及此時看到的第一個參數的值為2,不再是1了,因為在入口回調函數中被修改了。__mln_func_foo這個函數執行的才是我們定義的邏輯,而foo是對__mln_func_foo的一個封裝。 - 出口回調函數中,我們看到第一個參數的值變為了
4,因為它在我們給出的函數邏輯中做了修改。 - 最後輸出的是在入口回調中輸出的,表示
bar被攔截了,不會執行實際的函數功能。
最後,我們去掉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
可以看得出,此時foo和bar不再是封裝函數,而是我們定義的函數邏輯的函數名,即普通的C語言函數。
讀到這裏的都是真愛,感謝閲讀!