Coding Note: Shared Object Library (example in C)

在軟體開發中,我們時常使用library來方便管理專案以及節省開發時間。在C語言中,我們常使用.a檔和.so檔,以下是他們的介紹:

Static Library (.a)

  • .a檔是靜態庫或靜態物件檔的檔案格式,同樣常見於UNIX系統。
  • 它包含了一組函數和數據,這些函數和數據被編譯成一個單一的檔案。
  • .a檔在編譯時被鏈接到應用程式中,因此它們在執行時不需要再次載入。
  • 因此執行速度通常較快,但每次更新庫時都需要重新編譯應用程式。
  • 每個應用程式都有自己的拷貝,因此可能會浪費一些磁盤空間

Dynamic Library (.so)

  • .so檔通常較小,因為它們被多個應用程式共享。
  • 共享物件檔在執行時動態載入,這意味著可以在不重新編譯應用程式的情況下更新庫。
  • 執行速度可能較慢
特性 Static Library Dynamic Library
附檔名
.a
.so
鏈結時機
編譯時
執行時
執行速度
較快
較慢
檔案容量
較大
較小

Overview

接下來,我們將撰寫 shared object library 的範例。假設我們今天要寫一支程式,可以在角度與弧度間做轉換,我們希望主程式中只包含呼叫API的部分,實作部分希望在lib這個目錄下實作,那麼我們的整體結構如下:

				
					// main.c
#include <stdio.h>
#include "lib/angle.h" // 主程式只引入library,直接使用API

int main(){
    
    double d = 180.0;
    double r = 1;

    printf("deg2rad(%.3f) = %.3f\n", d, deg2rad(d));
    printf("rad2deg(%.3f) = %.3f\n", r, rad2deg(r));

    return 0;
}
				
			
				
					// lib/angle.h
// 宣告API的格式
double rad2deg(double rad);
double deg2rad(double deg);
				
			
				
					// lib/angle.c
// 實做函式的地方
#include <math.h>

double deg2rad(double deg){
    return deg * M_PI / 180.0;
}

double rad2deg(double rad){
    return rad * 180.0 / M_PI;
}
				
			

Makefile

我們習慣使用makefile將編譯參數寫成腳本,因此這邊將會把把編譯的細節寫成makefile。

Object File (.o)

我們使用 gcc -c angle.c -fpic 來編譯出.o檔。 .o檔,也就是目標檔 (Object File),是編譯程式碼後產生的二進位檔案。目標檔包含了程式碼中的所有指令和數據,但它們還不能直接執行。目標檔需要被linker連結在一起,才能形成可執行檔。
  • -c:表示只編譯程式碼,不連結成可執行檔案。
  • -fpic:表示生成位置無關的代碼,可以提高程式的可移植性和可擴展性。

Shared Object File (.so)

我們使用 gcc angle.o -shared -o lib_angle.so 來編譯出.so檔。

  • -shared:告訴連結器生Shared Library
  • -o:輸出的檔,這裡輸出.so檔
最終我們在lib內的makefile如下:
				
					// lib/makefile
lib_angle.so: angle.o
	gcc angle.o -shared -o lib_angle.so // 產生 shared object file
	rm -f angle.o // 可以移除object file了

angle.o: angle.c
	gcc -c angle.c -fpic // 產生object file

clean:
	rm -f *.o *.so
				
			

從外部makefile

在專案目錄,也就是lib的上層,我們可以使用 make -C lib 來對 lib 內的程式進行編譯。

連結動態庫

我們先用 gcc -o main main.o 產生目標檔。再用 gcc -o main main.o -L./lib -l_angle 來連結 main.olib_angle.so
  • -o :指定輸出檔案的名稱。
  • -L:告訴連結器在哪個目錄下的搜尋.so檔。
  • -l:告訴連結器連結哪個.so檔。

最終我們在lib外的makefile如下:

				
					// makefile
all: main.o lib_angle.so
	gcc -o main main.o -L./lib -l_angle // 連結動態庫
	rm -f main.o // 可以刪掉main.o了

main.o: main.c
	gcc -c main.c -o main.o // 先編譯出目標檔

lib_angle.so:
	make -C lib // 移動到lib目錄編譯出.so檔

clean:
	make -C lib clean // 也可以這樣做clean
	rm -f main
				
			

Error!

前面有提到.so是在執行時動態載入的,然而現在執行檔找不到.so所在的目錄,因此才會有編譯成功執行失敗的情形。以下是兩種修正方式:

Method 1

把.so檔複製到 /usr/lib 下面,這個做法需要super user權限。

Method 2

我們在專案根目錄新增環境變數 LD_LIBRARY_PATH :
				
					export LD_LIBRARY_PATH=$(pwd)/lib:$LD_LIBRARY_PATH
				
			

如此一來,程式就可以正常運行了。