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

Hệ thống plugin

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

  • Thiết kế giao diện đăng ký cho plugin
  • Kiểm tra phiên bản ABI trước khi tin một plugin
  • Quét thư mục và nạp mọi thư viện tìm được
  • Giải phóng đúng cách khi thoát

Người dùng thả một tệp vào thư mục plugins và chương trình có thêm một lệnh mới, không cần dịch lại gì. Đây là ứng dụng lớn nhất của việc nạp lúc chạy, và nó chỉ cần khoảng tám mươi dòng.

#Giao diện đăng ký

plugin.h
#ifndef PLUGIN_H
#define PLUGIN_H

#ifndef PLUGIN_ABI
#define PLUGIN_ABI 1
#endif

typedef struct {
    int         abi;
    const char *ten;
    const char *mo_ta;
    int       (*chay)(const char *doi_so);
} Plugin;

/* Moi plugin phai xuat ra dung mot ham nay. */
typedef const Plugin *(*HamDangKy)(void);

#endif

#Hai plugin

pl_hoa.c
#include <stdio.h>
#include <ctype.h>
#include "plugin.h"

static int chay(const char *s) {
    for (; *s; ++s) putchar(toupper((unsigned char)*s));
    putchar('\n');
    return 0;
}

static const Plugin P = { PLUGIN_ABI, "hoa", "Doi chuoi thanh chu hoa", chay };

__declspec(dllexport) const Plugin *dang_ky(void) { return &P; }
pl_dem.c
#include <stdio.h>
#include <string.h>
#include "plugin.h"

static int chay(const char *s) {
    printf("do dai = %d\n", (int)strlen(s));
    return 0;
}

static const Plugin P = { PLUGIN_ABI, "dem", "Dem so ky tu", chay };

__declspec(dllexport) const Plugin *dang_ky(void) { return &P; }
# Cach xuat ra ham dang_ky tren tung nen tang

#if defined(_WIN32)
  #define PLUGIN_XUAT __declspec(dllexport)
#else
  #define PLUGIN_XUAT __attribute__((visibility("default")))
#endif

PLUGIN_XUAT const Plugin *dang_ky(void) { return &P; }

# Dich:
gcc -shared pl_hoa.c -o plugins/pl_hoa.dll                   # Windows
gcc -fPIC -shared -fvisibility=hidden pl_hoa.c -o plugins/pl_hoa.so   # Linux

#Chương trình chính

chinh.c, phần nạp một plugin
#include <stdio.h>
#include <string.h>
#include <windows.h>

#include "plugin.h"

enum { TOI_DA = 16 };

static const Plugin *ds[TOI_DA];
static HMODULE       mo[TOI_DA];
static int           so;

static int nap_mot(const char *duong_dan) {
    HMODULE h = LoadLibraryA(duong_dan);
    if (h == NULL) {
        fprintf(stderr, "khong nap duoc %s (loi %lu)\n",
                duong_dan, (unsigned long)GetLastError());
        return -1;
    }

    HamDangKy f = (HamDangKy)(void *)GetProcAddress(h, "dang_ky");
    if (f == NULL) {
        fprintf(stderr, "%s: khong co ham dang_ky\n", duong_dan);
        FreeLibrary(h);
        return -1;
    }

    const Plugin *p = f();
    if (p == NULL || p->abi != PLUGIN_ABI) {
        fprintf(stderr, "%s: ABI %d, can %d\n",
                duong_dan, p ? p->abi : -1, PLUGIN_ABI);
        FreeLibrary(h);
        return -1;
    }

    ds[so] = p;
    mo[so] = h;
    ++so;

    printf("da nap: %-6s %s\n", p->ten, p->mo_ta);
    return 0;
}
chinh.c, phần quét thư mục
static void nap_thu_muc(const char *thu_muc) {
    char mau[512];
    snprintf(mau, sizeof mau, "%s\\*.dll", thu_muc);

    WIN32_FIND_DATAA fd;
    HANDLE h = FindFirstFileA(mau, &fd);
    if (h == INVALID_HANDLE_VALUE) return;

    do {
        char duong[512];
        snprintf(duong, sizeof duong, "%s\\%s", thu_muc, fd.cFileName);
        nap_mot(duong);
    } while (so < TOI_DA && FindNextFileA(h, &fd));

    FindClose(h);
}

int main(int argc, char **argv) {
    nap_thu_muc("plugins");

    printf("\nco %d plugin\n\n", so);

    if (argc >= 3) {
        for (int i = 0; i < so; ++i)
            if (strcmp(ds[i]->ten, argv[1]) == 0) {
                ds[i]->chay(argv[2]);
                break;
            }
    }

    for (int i = 0; i < so; ++i) FreeLibrary(mo[i]);

    return 0;
}
Bản POSIX của hàm quét thư mục
#include <dirent.h>
#include <string.h>

static void nap_thu_muc(const char *thu_muc) {
    DIR *d = opendir(thu_muc);
    if (d == NULL) return;

    struct dirent *e;
    while (so < TOI_DA && (e = readdir(d)) != NULL) {
        const char *duoi = strrchr(e->d_name, '.');
        if (duoi == NULL || strcmp(duoi, ".so") != 0) continue;

        char duong[512];
        snprintf(duong, sizeof duong, "%s/%s", thu_muc, e->d_name);
        nap_mot(duong);
    }

    closedir(d);
}

/* Chu y: duong dan PHAI co dau gach cheo, neu khong dlopen se tim
   trong LD_LIBRARY_PATH thay vi trong thu muc cua ban. Bai 39.10. */

#Chạy thử

terminal
gcc -shared pl_hoa.c -o plugins/pl_hoa.dll && gcc -shared pl_dem.c -o plugins/pl_dem.dll && gcc -std=c11 -Wall -Wextra chinh.c -o chinh.exe
./chinh.exe hoa "xin chao the gioi"
da nap: dem    Dem so ky tu
da nap: hoa    Doi chuoi thanh chu hoa

co 2 plugin

XIN CHAO THE GIOI
./chinh.exe dem "xin chao the gioi"
da nap: dem    Dem so ky tu
da nap: hoa    Doi chuoi thanh chu hoa

co 2 plugin

do dai = 17

#Bốn điều cần thêm cho bản thật

  1. Hàm khởi tạo và dọn dẹp cho plugin

    Plugin có thể cần mở tệp, cấp phát, đăng ký với hệ thống. Thêm hai con trỏ hàm khoi_tao và don_dep vào struct, và gọi don_dep trước khi gỡ thư viện.

  2. Bảng hàm để plugin gọi ngược lại

    Plugin cần ghi nhật ký, đọc cấu hình, cấp phát bằng bộ cấp phát của chương trình chính. Truyền một struct chứa các con trỏ hàm đó vào khoi_tao.

  3. Kiểm tra tên trùng

    Hai plugin cùng đăng ký tên hoa thì cái nào thắng? Từ chối cái thứ hai và báo rõ, đừng để im lặng.

  4. Cô lập lỗi

    Một plugin sập là cả chương trình sập. Nếu điều đó không chấp nhận được thì phải chạy plugin trong tiến trình riêng và giao tiếp qua đường ống, như cách trình duyệt làm.

Giao diện đầy đủ hơn
#define PLUGIN_ABI 2

/* Bang ham chuong trinh chinh cung cap cho plugin */
typedef struct {
    void  (*ghi_log)(int muc, const char *tin);
    void *(*cap_phat)(size_t n);
    void  (*giai_phong)(void *p);
    const char *(*cau_hinh)(const char *khoa);
} DichVu;

typedef struct {
    int         abi;
    const char *ten;
    const char *mo_ta;
    const char *phien_ban;

    int  (*khoi_tao)(const DichVu *dv);   /* tra ve 0 neu thanh cong */
    void (*don_dep)(void);
    int  (*chay)(const char *doi_so);
} Plugin;
Vòng đời đầy đủ trong chương trình chính
/* Nap */
const Plugin *p = f();
if (p == NULL || p->abi != PLUGIN_ABI) { FreeLibrary(h); return -1; }

/* Kiem tra trung ten */
for (int i = 0; i < so; ++i)
    if (strcmp(ds[i]->ten, p->ten) == 0) {
        fprintf(stderr, "%s: ten '%s' da co, bo qua\n", duong_dan, p->ten);
        FreeLibrary(h);
        return -1;
    }

/* Khoi tao */
if (p->khoi_tao != NULL && p->khoi_tao(&g_dich_vu) != 0) {
    fprintf(stderr, "%s: khoi tao that bai\n", duong_dan);
    FreeLibrary(h);
    return -1;
}

ds[so] = p; mo[so] = h; ++so;

/* ... chuong trinh chay ... */

/* Go, theo thu tu NGUOC lai thu tu nap */
for (int i = so; i-- > 0; ) {
    if (ds[i]->don_dep != NULL) ds[i]->don_dep();
    FreeLibrary(mo[i]);          /* don_dep TRUOC, go SAU */
}

Tự làm thử

  1. Dựng đủ hệ thống plugin trong bài và chạy được cả hai lệnh.
  2. Viết plugin thứ ba và xác nhận chương trình chính không cần dịch lại.
  3. Dịch một plugin với -DPLUGIN_ABI=2 và xác nhận nó bị từ chối.
  4. Bỏ FreeLibrary ở một nhánh lỗi và tìm rò rỉ bằng công cụ.
  5. Thêm khoi_tao, don_dep và bảng dịch vụ.
  6. Gọi don_dep sau FreeLibrary và quan sát chương trình sập.

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

  • Một hàm xuất ra duy nhất trả về một struct mô tả, và nạp plugin không chạy mã nghiệp vụ nào.
  • Trường abi phải ở độ dời 0 để đọc được an toàn kể cả khi phần còn lại của struct có bố cục khác.
  • Mọi thứ trong plugin đều static trừ hàm đăng ký, và nạp với RTLD_LOCAL.
  • Mỗi nhánh lỗi phải gọi FreeLibrary, nếu không mỗi lần quét thư mục lại rò rỉ một tay cầm.
  • Khởi tạo xuôi, dọn dẹp ngược, và luôn gọi don_dep trước khi gỡ thư viện.