程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> 關於C語言 >> C語言開發函數庫時對外接口隱藏庫內結構體實現細節的方法

C語言開發函數庫時對外接口隱藏庫內結構體實現細節的方法

編輯:關於C語言

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 

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 
#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庫工程,庫的目標群體也相對單一,所以本著簡單夠用的原則,我選擇了使用指針的方式對外隱藏庫內結構體的實現細節。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved