C語言開發函數庫時利用不透明指針對外隱藏結構體細節
1 模塊化設計要求庫接口隱藏實現細節
作為一個函數庫來說,盡力減少和其調用方的耦合,是最基本的設計標准。C語言,作為經典“程序=數據結構+算法”的踐行者,在實現函數庫的時候,必然存在大量的結構體定義,接口函數需要對這些結構體進行操作。同時,程序設計的模塊化要求庫接口盡量少的暴露其實現細節,接口參數盡量使用基本數據類型,盡量避免在形參中暴露庫內結構體的定義。
2 隱藏結構體的兩種方法
以筆者粗淺的認識,有兩種最常用的方法,可以實現庫內結構體定義的隱藏:接口函數形參使用結構體指針,接口函數形參使用句柄。
2.1 通過結構體指針引用結構體
為了說明方便,先給出使用VC++寫的一段例子代碼。
庫接口頭文件 MySDK.h
#pragma once
#ifdef MYSDK_EXPORT
#define MYSDK_API __declspec(dllexport)
#else
#define MYSDK_API __declspec(dllimport)
#endif
typedef struct _Window Window; /*預先聲明*/
#ifdef __cplusplus
extern "C" {
#endif
MYSDK_API Window* CreateWindow();
MYSDK_API void ShowWindow(Window* pWin);
#ifdef __cplusplus
}
#endif
庫實現文件MySDK.c
#define MYSDK_EXPORT
#include "MySDK.h"
#include <stdlib.h>
struct _Window
{
int width;
int height;
int x;
int y;
unsigned char color[3];
int isShow;
};
MYSDK_API Window* CreateWindow()
{
Window* p = malloc(sizeof(Window));
if (p) {
p->width = 400;
p->height = 300;
p->x = 0;
p->y = 0;
p->color[0] = 255;
p->color[1] = 255;
p->color[2] = 255;
p->isShow = 0;
}
return p;
}
MYSDK_API void ShowWindow(Window* pWin)
{
pWin->isShow = 1;
}
庫使用者代碼
#include <stdio.h>
#include "../myDll/MySDK.h"
#pragma comment(lib, "../Debug/myDll.lib")
int main(int argc, char** argv)
{
Window* pWin = CreateWindow();
ShowWindow(pWin);
return 0;
}
其中MySDK.h和MySDK.c是庫的實現; main.cpp是調用方程序實現。雙方使用了相同的接口頭文件MySDK.h。
但是從使用者角度,main.cpp裡面只知道庫中有名為Window的一種結構體類型,但是卻不能知道此機構體的實現細節(定義)。由於C/C++編譯器是延遲依賴型編譯器,只要源代碼中沒有涉及到Window結構體內存布局的代碼,編譯時不需要知道Window的完整定義,但是仍然能夠檢查類型名稱的正確性,比如如果客戶端代碼如下則會被編譯器檢查出問題:
int* p = 0;
ShowWindow(p);
編譯器雖然不知道ShowWindow(pWin)中pWin指向的結構體的實現細節,但是仍然能夠確保實參類型為Window*,這也方便了調用方檢查錯誤。
2.2 通過“句柄”(handle)來引用結構體
最先接觸句柄的概念,是在Win32API中。可以斷定Windows系統的內部定義了大量的結構體,如線程對象、進程對象、窗口對象、….。但是編程接口Win32API中卻很少提供這些結構體的定義,調用者通過一個稱為“句柄”的值來間接引用要使用的結構體對象。
Win32API 中的句柄
例如,如下Win32API
HWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
窗口類型在Windows中一定是一個非常復雜的結構體,為了隱藏其實現細節,微軟采取了窗口句柄的概念來間接引用窗口結構體對象。為了實現這種對應關系,庫內部必須維護句柄和結構體對象的對應關系。
Linux API中的句柄
句柄的概念也廣泛的應用在Linux平台API中。如
int open(const char *pathname, int flags);
ssize_t read(int fd, void *buf, size_t count);
在Linux內部,文件一定是通過一個復雜的結構體來表示,但是在API中使用了一個簡單整數對其進行引用,避免了向調用者暴露文件結構體的細節。
OpenGL API中的句柄
句柄同樣應用到了OpenGL庫中。如
void WINAPI glGenTextures(
GLsizei n,
GLuint *textures
);
void WINAPI glBindTexture(
GLenum target,
GLuint texture
);
紋理在OpenGL庫內部也是一個復雜的結構體,同樣使用句柄的概念對外隱藏了實現細節。
3 句柄和指針的比較
3.1 句柄的優勢與不足
句柄看起來真的不錯,那麼局部到底是如何映射到對應的結構體的呢?一個最容易想到的答案就是:直接把結構體對象的內存地址作為句柄。然而實際上,大多數的庫實現都不是這麼做的。之所以不直接把內存地址作為句柄的值,我個人認為有如下幾個原因:
從源碼保護角度,內存地址更容易被Hack。知道了結構體的內存地址,就能夠讀取這塊內存的內容,從而為猜測結構體細節提供了方便。
從程序穩定性角度,對於庫內部維護的對象,調用者只應該通過接口函數來訪問,如果調用者得到了對象的內存地址,那麼就有可能有意或無意的進行直接修改,從而影響庫的穩定運行。
從可移植性角度,指針類型在32位和64位系統中具有不同的長度,這樣就需要為定義兩個名稱重復的接口函數,造成各種不便。而例如OpenGL,使用int型作為句柄類型,則可以一個接口函數跨越多個平台。
從簡化接口頭文件角度,使用指針至少需要事先聲明結構體類型,如 struct Window; 而使用基本數據類型作為句柄,無需這樣做。
句柄存在的不足有:
編譯器無法識別具體的結構體類型
由於句柄的數據類型實際上是基本數據類型,所以編譯器只能進行常規的檢查,不能識別具體的結構體類型。如
SECURITY_ATTRIBUTES sa;
HANDLE h = CreateMutex(&sa, TRUE, L"Mutex");
ReadFile(h, NULL, 0, 0, 0);
上述代碼編譯器並不會報錯,因為互斥體對象和文件對象都是使用相同的句柄類型。
效率可能稍差
畢竟存在一個 根據句柄值-查找內存指針的過程,可能會稍稍影響運行效率。
3.2 指針的優勢與不足
其實指針和句柄是相對的,句柄的不足就是指針的優勢,句柄的優勢也是指針的不足。
4 如何選擇
對於大型跨平台庫的設計,采用句柄;對於專用小型庫,采用指針。
就我目前的項目而言,是一個小型的C庫工程,庫的目標群體也相對單一,所以本著簡單夠用的原則,我選擇了使用指針的方式對外隱藏庫內結構體的實現細節。