Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 39.828 phút đọc

Kiểu mờ và cách giữ ABI ổn định

Sau bài này bạn sẽ làm được

  • Thiết kế một API hoàn toàn bằng kiểu mờ
  • Giải thích vì sao kiểu mờ làm ABI bất biến
  • Cân nhắc cái giá: thêm một lần cấp phát và một tầng gián tiếp
  • Dùng trường dự phòng khi bắt buộc phải lộ struct

Nếu người dùng không biết struct của bạn to bao nhiêu thì họ không thể viết mã phụ thuộc vào điều đó, và bạn đổi nó thoải mái. Đó là toàn bộ ý tưởng, và nó là kỹ thuật giữ ABI mạnh nhất trong C.

#Ý tưởng

Kiểu mờ (opaque type)
Một struct được khai báo trong header nhưng chỉ được định nghĩa trong tệp .c của thư viện. Người dùng chỉ cầm được con trỏ tới nó, không biết bên trong có gì và không biết sizeof là bao nhiêu.
Struct lộ ra
/* cauhinh.h: struct LO ra */

typedef struct {
    int rong;
    int cao;
} CauHinh;

void ch_in(const CauHinh *c);

/* Nguoi dung viet: */
CauHinh c = { 1920, 1080 };     /* cap phat tren ngan xep cua HO */
c.rong = 800;                   /* truy cap truong truc tiep */
ch_in(&c);

/* Bay gio kich thuoc va bo cuc cua CauHinh la mot phan ABI
   cua ban, mai mai. Bai 39.7 da do hau qua. */
Kiểu mờ
/* cauhinh.h: chi KHAI BAO */

typedef struct CauHinh CauHinh;   /* khong co than */

CauHinh *ch_tao(void);
void     ch_huy(CauHinh *c);
int      ch_dat_rong(CauHinh *c, int rong);
int      ch_dat_cao(CauHinh *c, int cao);
int      ch_rong(const CauHinh *c);
int      ch_cao(const CauHinh *c);
void     ch_in(const CauHinh *c);

/* Nguoi dung viet: */
CauHinh *c = ch_tao();
ch_dat_rong(c, 1920);
ch_dat_cao(c, 1080);
ch_in(c);
ch_huy(c);

/* Ho khong biet sizeof(CauHinh), khong the cap phat tren ngan xep,
   khong the truy cap truong. Nen ban doi bo cuc thoai mai. */
cauhinh.c: định nghĩa nằm ở đây
#include <stdlib.h>
#include <string.h>
#include "cauhinh.h"

struct CauHinh {         /* than struct chi co trong tep nay */
    int rong;
    int cao;
    int sau;             /* them thoai mai, khong ai o ngoai biet */
};

CauHinh *ch_tao(void) {
    CauHinh *c = calloc(1, sizeof *c);
    if (c == NULL) return NULL;

    c->rong = 640;       /* gia tri mac dinh */
    c->cao  = 480;
    return c;
}

void ch_huy(CauHinh *c) { free(c); }

int ch_dat_rong(CauHinh *c, int rong) {
    if (c == NULL || rong <= 0) return -1;
    c->rong = rong;
    return 0;
}

int ch_rong(const CauHinh *c) { return c ? c->rong : 0; }
terminal
# Người dùng cố truy cập bên trong
gcc -c app.c -o /dev/null
app.c: In function 'main':
app.c:5:13: error: storage size of 'c' isn't known
     CauHinh c;
             ^
app.c:7:6: error: dereferencing pointer to incomplete type 'CauHinh' {aka 'struct CauHinh'}
     p->rong = 800;
      ^~

#Thiết kế một API mờ hoàn chỉnh

Sáu nhóm hàm cần có
/* 1. VONG DOI: tao va huy */
CauHinh *ch_tao(void);
CauHinh *ch_tao_tu(const CauHinh *goc);      /* sao chep */
void     ch_huy(CauHinh *c);

/* 2. DAT: moi truong mot ham, tra ve ma loi */
int ch_dat_rong(CauHinh *c, int rong);
int ch_dat_cao(CauHinh *c, int cao);
int ch_dat_ten(CauHinh *c, const char *ten);  /* thu vien tu sao chep chuoi */

/* 3. DOC: khong bao gio that bai, tra ve gia tri mac dinh khi c la NULL */
int         ch_rong(const CauHinh *c);
int         ch_cao(const CauHinh *c);
const char *ch_ten(const CauHinh *c);         /* thu vien van so huu chuoi */

/* 4. THAO TAC */
int ch_luu(const CauHinh *c, const char *tep);
int ch_nap(CauHinh *c, const char *tep);

/* 5. KICH THUOC, neu nguoi dung can tu cap phat */
size_t ch_co(void);        /* tra ve sizeof(struct CauHinh) */

/* 6. PHIEN BAN, de kiem tra luc chay */
unsigned ch_phien_ban(void);
Mẫu quản lý chuỗi bên trong kiểu mờ
struct CauHinh {
    int   rong;
    int   cao;
    char *ten;           /* thu vien so huu */
};

int ch_dat_ten(CauHinh *c, const char *ten) {
    if (c == NULL || ten == NULL) return -1;

    size_t n = strlen(ten) + 1;
    char *moi = malloc(n);
    if (moi == NULL) return -1;      /* c->ten CON NGUYEN, khong ro ri */

    memcpy(moi, ten, n);

    free(c->ten);                    /* giai phong ban cu SAU khi cap thanh cong */
    c->ten = moi;
    return 0;
}

const char *ch_ten(const CauHinh *c) {
    return (c != NULL && c->ten != NULL) ? c->ten : "";
}

void ch_huy(CauHinh *c) {
    if (c == NULL) return;
    free(c->ten);
    free(c);
}

/* Chu y thu tu trong ch_dat_ten: cap phat moi TRUOC, giai phong cu SAU.
   Neu lam nguoc lai va malloc that bai thi ban vua mat ten cu
   va khong co ten moi. Bai 33.3 da noi ve mau nay. */

#Cái giá

Struct lộ raKiểu mờ
Cấp phátTrên ngăn xếp, miễn phímalloc, một lần cấp phát
Đọc một trườngMột lệnh movMột lời gọi hàm
Nội tuyến đượcCóKhông, trừ khi liên kết tĩnh có LTO
Số dòng mã thư việnÍtNhiều hơn, mỗi trường hai hàm
ABI ổn địnhKhôngCó
Người dùng phải nhớ hủyKhôngCó
Cách nhiều thư viện thật làm
/* SQLite: hoan toan mo */
typedef struct sqlite3 sqlite3;
typedef struct sqlite3_stmt sqlite3_stmt;

/* zlib: LO ra, vi nguoi dung phai cap phat tren ngan xep */
typedef struct z_stream_s {
    const Bytef *next_in;
    uInt         avail_in;
    ...
    /* va zlib phai giu bo cuc nay tu nam 1995 */
} z_stream;

/* libcurl: mo, va co ham dat theo TEN TUY CHON thay vi theo truong */
CURL *curl_easy_init(void);
CURLcode curl_easy_setopt(CURL *h, CURLoption opt, ...);
curl_easy_setopt(h, CURLOPT_URL, "https://a.b");

/* Cach cua libcurl dang chu y: mot ham dat duy nhat voi tham so
   thay doi. Them tuy chon moi chi la them mot hang so enum vao cuoi,
   khong them ham nao, khong pha ABI. */

#Ba cách giảm cái giá

Một: cho người dùng tự cấp phát

/* Van giu kieu mo, nhung cho phep dat tren ngan xep: */

size_t ch_co(void);        /* tra ve sizeof(struct CauHinh) */
int    ch_khoi_tao(void *bo_nho);   /* khoi tao vao vung nguoi dung cap */

/* Nguoi dung: */
void *bo_dem = alloca(ch_co());     /* hoac malloc, hoac mang tinh */
ch_khoi_tao(bo_dem);
CauHinh *c = bo_dem;

/* Van tranh duoc mot lan malloc, ma van khong lo bo cuc.

   Nhung chu y: kich thuoc gio thanh mot phan ABI luc CHAY, chu khong
   luc dich. Neu thu vien moi can nhieu cho hon, no phai bao loi
   chu khong duoc ghi tran. */

/* Bien the an toan hon, dung mang byte du lon: */
typedef struct { _Alignas(max_align_t) char _[128]; } CauHinhCho;
int ch_khoi_tao_vao(CauHinhCho *cho);   /* bao loi neu 128 khong du */

Hai: hàm truy cập dạng static inline trong header

/* Chi lam duoc khi ban CHAP NHAN lo mot phan bo cuc.
   Do la danh doi, khong phai cach lam co ca hai. */

/* cauhinh.h */
typedef struct CauHinh CauHinh;

/* Lo ra DU DOI cua hai truong doc nhieu nhat: */
struct CauHinh_cong_khai { int rong; int cao; };

static inline int ch_rong(const CauHinh *c) {
    return ((const struct CauHinh_cong_khai *)c)->rong;
}

/* Va trong cauhinh.c, chot rang bo cuc khop: */
struct CauHinh { int rong; int cao; /* ... them gi cung duoc sau day */ };
_Static_assert(offsetof(struct CauHinh, rong) == 0, "ABI");
_Static_assert(offsetof(struct CauHinh, cao)  == 4, "ABI");

/* Cach nay giu duoc kha nang them truong o CUOI, va cho doc nhanh
   hai truong dau. Duoc dung trong mot vai thu vien do hoa. */

Ba: liên kết tĩnh với tối ưu toàn chương trình

gcc -flto -O2 app.c libmath.a -o app

/* Voi LTO va lien ket TINH, trinh bien dich thay ca ma cua thu vien,
   nen no noi tuyen ch_rong thanh mot lenh mov nhu the struct lo ra.

   Ban duoc ca hai: ABI on dinh cho ban .so, va toc do cua struct lo ra
   cho ban .a.

   Khong ap dung cho thu vien dong: qua ranh gioi .so thi khong noi tuyen
   duoc, va do la ban chat cua viec "va thu vien khong dich lai chuong trinh". */

#Khi nào không dùng

Tự làm thử

  1. Chuyển một struct lộ ra thành kiểu mờ và đọc thông báo lỗi khi người dùng cố truy cập trường.
  2. Viết đủ sáu nhóm hàm cho một kiểu mờ.
  3. Thêm một trường vào struct mờ, dịch lại chỉ thư viện, xác nhận chương trình cũ vẫn đúng.
  4. Đo chênh lệch tốc độ giữa truy cập trường trực tiếp và qua hàm, trên một triệu phần tử.
  5. Viết ch_co và ch_khoi_tao cho phép cấp phát trên ngăn xếp.
  6. Thêm một con số kiểm tra vào struct mờ để bắt con trỏ treo.

Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.

Tóm tắt

  • Kiểu mờ là struct khai báo trong header, định nghĩa trong tệp .c. Người dùng chỉ cầm con trỏ.
  • Trình biên dịch thi hành ranh giới giúp bạn, nên không cần tài liệu xin người dùng đừng chạm vào.
  • Cái giá là một lần cấp phát và một lời gọi hàm cho mỗi lần truy cập trường, và nó thật với kiểu nhỏ dùng trong vòng lặp nóng.
  • Kiểu nhỏ và bất biến thì lộ ra rồi chốt bằng _Static_assert; kiểu có trạng thái thì làm mờ.
  • Ngoài ABI, kiểu mờ còn bảo đảm bất biến, cho phép thay cài đặt, và cho phép thêm cơ chế gỡ lỗi bất cứ lúc nào.