C模塊化編程
模塊化編程是指程序核心部分定義好功能的介面,而具體的實現留給各個模塊去做。舉個現實世界的例子:我們可以在電腦的PCI插槽上安裝顯卡、音效卡或者網卡,原因就是這些硬體都按照PCI介面的規範來製造的。
模塊化編程也一樣,程序核心部分定義好介面,各個模塊按照介面的定義去實現功能,然後把各個模塊掛載到程序上即可,這個有點像Java的面向介面編程。如下圖:
(圖一)
模塊化編程的好處就是最大化靈活性,程序的核心部分不用關心功能的具體實現,只需要調用模塊提供的介面即可得到相應的結果。因為各個模塊的具體實現各不相同,所以得到的結果也是多樣化的。
使用C進行模塊化編程
用過C語言編程的人都知道C語言是沒有介面的,所以怎麼使用C語言進行模塊化編程呢?使用C語言的結構體和函數指針可以模擬出Java介面的特性,我們只需定義一個由多個函數指針構成的結構體,然後功能模塊實現這個結構體里的函數即可。
例如我們定義一個名為Car的結構體,而這個結構體有兩個函數指針,分別是run()和stop():
car.h
#ifndef __CAR_H
#define __CAR_H
struct Car
{
void (*run)();
void (*stop)();
};
#endif
從上面定義可以知道,實現了run()和stop()方法的模塊都可以被稱為Car(汽車)。現在我們來編寫兩個模塊,分別是Truck和Van。
Truck模塊如下(truck.c):
#include <stdlib.h>
#include <stdio.h>
#include "car.h"
static void run()
{
printf("I am Truck, running...
");
}
static void stop()
{
printf("I am Truck, stopped...
");
}
struct Car truck = {
.run = &run,
.stop = &stop,
};
Van模塊如下(van.c):
#include <stdlib.h>
#include <stdio.h>
#include "car.h"
static void run()
{
printf("I am Van, running...
");
}
static void stop()
{
printf("I am Van, stopped...
");
}
struct Car van = {
.run = &run,
.stop = &stop,
};
這樣我們編寫了兩個模塊,一個是卡車模塊(Truck),一個是客車模塊(Van)。為了簡單起見,我們只是簡單的列印一段文字。現在可以說我們的車是變形金剛了,因為可以隨時變成卡車或者客車(嘻嘻)。
我們把模塊都寫好了,但是怎麼把模塊應用到程序的核心部分呢?這時候我們需要一個註冊機制。因為核心部分不知道我們到底編寫了什麼模塊,所以就需要這個註冊機制來告訴核心部分。註冊機制很簡單,只需要一個函數即可(main.c):
#include "car.h"
extern struct Car van;
extern struct Car truck;
struct Car *car;
void register_module(struct Car *module)
{
car = module;
}
int main(int argc, char *argv[])
{
register_module(&truck);
car->run();
car->stop();
return 0;
}
編譯運行後我們會得到以下的結果:
如果把 register_module(&truck); 改為 register_module(&van); 會得到以下結果:
從上面的結果可以看到,我們可以註冊不同的模塊來提供不同的服務,模塊化編程就這樣實現了。
Are you kidding me?
C的模塊化編程的確是這麼簡單,但是我們可以實現更強大的功能:使用動態鏈接庫來實現模塊化。
使用動態鏈接庫進行模塊化編程
Linux提供一種叫動態鏈接庫的技術(Windows也有類似的功能),可以通過系統API動態載入.so文件中的函數或者變數。動態鏈接庫的好處是把程序劃分成多個獨立的部分編譯,每個部分的編譯互補影響。例如我們有動態鏈接庫A、B、C,如果發現A有bug,我們只需要修改和重新編譯A即可,而不用對B和C進行任何的改動。
下面我們使用動態鏈接庫技術來重寫上面的程序。
其實要使用動態鏈接庫技術,只需要把模塊編譯成.so文件,然後核心部分使用操作系統提供的dlopen()和dlsym()介面來載入模塊即可。
1. 把模塊編譯成.so文件
首先我們修改van.c文件,主要是增加一個讓核心部分獲取模塊介面的方法get_module():
#include <stdlib.h>
#include <stdio.h>
#include "car.h"
static void run()
{
printf("I am Van, running...
");
}
static void stop()
{
printf("I am Van, stopped...
");
}
struct Car module = {
.run = &run,
.stop = &stop,
};
sturct Car *get_module()
{
return &module;
}
然後我們需要把庫的源文件編譯成無約束位代碼。無約束位代碼是存儲在主內存中的機器碼,執行的時候與絕對地址無關。
$gcc -c -Wall -Werror -fpic van.c
現在讓我們將對象文件變成共享庫。我們將其命名為van.so:
$ gcc -shared -o van.so van.o
這樣我們就把van.c編譯成動態鏈接庫了。我們使用相同的方法把truck.c編譯成truck.so。
2. 在核心部分載入動態鏈接庫
使用動態鏈接庫介面來修改核心部分代碼,如下:
#include "Car.h"
#include <dlfcn.h>
#include <stdlib.h>
struct Car *car;
struct Car *register_module(char *module_name)
{
struct Car *(*get_module)();
void *handle;
handle = dlopen(module_name, RTLD_LAZY);
if (!handle) {
return NULL;
}
get_module = dlsym(handle, "get_module");
if (dlerror() != NULL) {
dlclose(handle);
return NULL;
}
dlclose(handle);
return get_module();
}
int main(int argc, char *argv[])
{
struct Car *car;
if ((car = register_module("./van.so")) == NULL) {
return -1;
}
car->run();
car->stop();
return 0;
}
使用以下命令編譯代碼:
$ gcc -rdynamic -o car main.c -ldl
運行程序後得到結果:
修改 register_module("./van.so") 為 register_module("./ truck .so") 得到結果:
可以看到我們成功使用動態鏈接庫改寫了程序。
總結
由於模塊化編程的靈活性和可擴展性非常好,所以很多流行的軟體也提供模塊化特性,如:Nginx、PHP和Python等。而這些軟體使用的方法與本文介紹的大致相同,有興趣可以查看這些軟體的實現。