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

Nạp thư viện lúc chạy

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

  • Nạp một thư viện và lấy con trỏ hàm lúc chạy
  • Xử lý lỗi của dlopen và dlsym cho đúng
  • Giải thích vì sao phải ép kiểu qua void ** trong C89 và C99
  • So sánh RTLD_LAZY với RTLD_NOW

Cho tới giờ, mọi thư viện đều được nạp tự động lúc khởi động, và chương trình chết ngay nếu thiếu. Bài này là cách còn lại: chương trình tự quyết định nạp gì, lúc nào, và xử lý được khi thiếu.

#Hai cách nạp thư viện

Liên kết lúc dịchNạp lúc chạy
Còn gọi làImplicit linkingExplicit linking, dlopen
Khi nào nạpTrước khi main chạyKhi bạn gọi hàm nạp
Thiếu thư viện thìChương trình không khởi động đượcHàm nạp trả về NULL, bạn xử lý
Biết tên thư viện lúcDịchChạy, có thể đọc từ tệp cấu hình
Kiểm tra kiểuTrình biên dịch làmKhông ai làm, bạn tự chịu
Cần header của thư việnCóKhông bắt buộc
Dùng choPhụ thuộc bắt buộcPlugin, tính năng tùy chọn

#dlopen trên POSIX

Bốn hàm, và đó là toàn bộ API
#include <dlfcn.h>

void *dlopen(const char *duong_dan, int co);
void *dlsym(void *tay_cam, const char *ten);
int   dlclose(void *tay_cam);
char *dlerror(void);

/* Dich voi -ldl tren Linux. Tren macOS va cac he BSD moi thi
   khong can, chung nam san trong libc. */
gcc main.c -ldl -o app
Mẫu đầy đủ, kể cả xử lý lỗi
#include <stdio.h>
#include <dlfcn.h>

int main(void) {
    dlerror();                          /* xoa loi cu, xem chu thich duoi */

    void *h = dlopen("./libplugin.so", RTLD_NOW);
    if (h == NULL) {
        fprintf(stderr, "dlopen: %s\n", dlerror());
        return 1;
    }

    int (*xu_ly)(const char *);
    *(void **)(&xu_ly) = dlsym(h, "xu_ly");

    const char *loi = dlerror();
    if (loi != NULL) {
        fprintf(stderr, "dlsym: %s\n", loi);
        dlclose(h);
        return 1;
    }

    xu_ly("du lieu");

    dlclose(h);
    return 0;
}
Cờ của dlopenNghĩa
RTLD_LAZYGiải ký hiệu khi lần đầu dùng tới. Nạp nhanh hơn
RTLD_NOWGiải hết ký hiệu ngay lúc nạp. Chậm hơn nhưng lỗi lộ ra sớm
RTLD_GLOBALKý hiệu của thư viện này dùng được cho các thư viện nạp sau
RTLD_LOCALKý hiệu chỉ dùng qua dlsym. Mặc định, và thường là cái đúng
RTLD_NODELETEdlclose không thật sự gỡ khỏi bộ nhớ
RTLD_NOLOADChỉ kiểm tra thư viện đã nạp chưa, không nạp mới
# Ba duong dan dac biet cua dlopen

dlopen("libmath.so.1", RTLD_NOW);   /* tim theo quy tac thong thuong:
                                       rpath, LD_LIBRARY_PATH, ld.so.cache */

dlopen("./libmath.so", RTLD_NOW);   /* co dau gach cheo -> DUONG DAN,
                                       khong tim o dau khac */

dlopen(NULL, RTLD_NOW);             /* tay cam toi CHINH CHUONG TRINH.
                                       Cho phep dlsym tim ky hieu trong
                                       ban thuc thi va moi thu vien da nap */

/* Cach cuoi rat huu ich: */
void *ta = dlopen(NULL, RTLD_NOW);
void *f  = dlsym(ta, "ham_nguoi_dung_dinh_nghia");
if (f != NULL) { /* nguoi dung co cung cap ban thay the */ }

#LoadLibrary trên Windows

Bốn hàm tương ứng
#include <windows.h>

HMODULE LoadLibraryA(const char *ten);
FARPROC GetProcAddress(HMODULE h, const char *ten);
BOOL    FreeLibrary(HMODULE h);
DWORD   GetLastError(void);

/* Khong can co dich them: chung nam trong kernel32, luon co san. */
Mẫu đầy đủ
#include <stdio.h>
#include <windows.h>

int main(void) {
    HMODULE h = LoadLibraryA("./libplugin.dll");
    if (h == NULL) {
        fprintf(stderr, "LoadLibrary that bai, loi %lu\n",
                (unsigned long)GetLastError());
        return 1;
    }

    int (*xu_ly)(const char *);
    xu_ly = (int (*)(const char *))(void *)GetProcAddress(h, "xu_ly");

    if (xu_ly == NULL) {
        fprintf(stderr, "khong co ham xu_ly, loi %lu\n",
                (unsigned long)GetLastError());
        FreeLibrary(h);
        return 1;
    }

    xu_ly("du lieu");

    FreeLibrary(h);
    return 0;
}
ViệcPOSIXWindows
Nạpdlopen(ten, RTLD_NOW)LoadLibraryA(ten)
Tìm ký hiệudlsym(h, ten)GetProcAddress(h, ten)
Đóngdlclose(h)FreeLibrary(h)
Lỗidlerror(), trả về chuỗiGetLastError(), trả về số
Kiểu tay cầmvoid *HMODULE
Cần cờ liên kết-ldl trên LinuxKhông
Tay cầm tới chính mìnhdlopen(NULL, ...)GetModuleHandle(NULL)

#Vấn đề ép kiểu con trỏ hàm

Gán thẳng hoặc ép trực tiếp
int (*f)(const char *);

f = dlsym(h, "xu_ly");
/* warning: assignment to 'int (*)(const char *)' from incompatible
   pointer type 'void *' [-Wincompatible-pointer-types]  */

f = (int (*)(const char *))dlsym(h, "xu_ly");
/* Voi -Wpedantic:
   warning: ISO C forbids conversion of object pointer to function
   pointer type [-Wpedantic]  */
Ghi qua địa chỉ
int (*f)(const char *);

*(void **)(&f) = dlsym(h, "xu_ly");

/* Thu thuat: thay vi chuyen GIA TRI, ta ghi vao DIA CHI cua bien f
   thong qua mot con tro void *.

   Khong co canh bao nao, ke ca voi -Wpedantic.

   Cach nay duoc ghi thang trong tai lieu POSIX cua dlsym, nen
   no la cach chinh thuc. */
terminal
# Ba cách viết sai, ba cảnh báo khác nhau
gcc -std=c11 -Wall -Wextra -Wpedantic -c nap.c
nap.c:10:9: warning: cast between incompatible function types from 'FARPROC' {aka 'long long int (*)()'} to 'int (*)(const char *)' [-Wcast-function-type]
     f = (int (*)(const char *))GetProcAddress(h, "xu_ly");
         ^
nap.c:12:15: warning: ISO C forbids initialization between function pointer and 'void *' [-Wpedantic]
     void *p = GetProcAddress(h, "xu_ly");
               ~~~~~~~~~~~~~~
nap.c:13:30: warning: ISO C forbids conversion of object pointer to function pointer type [-Wpedantic]
     int (*g)(const char *) = (int (*)(const char *))p;
                              ^
Ba cách viết cho gọn
/* 1. Macro, cach hay dung nhat */
#define NAP_HAM(bien, tay_cam, ten) \
    (*(void **)(&(bien)) = dlsym((tay_cam), (ten)))

int (*xu_ly)(const char *);
NAP_HAM(xu_ly, h, "xu_ly");

/* 2. Union, ro rang hon voi nguoi doc */
union { void *du_lieu; int (*ham)(const char *); } u;
u.du_lieu = dlsym(h, "xu_ly");
int (*xu_ly)(const char *) = u.ham;

/* 3. memcpy, khong the che trach gi ve mat chuan */
void *p = dlsym(h, "xu_ly");
int (*xu_ly)(const char *);
memcpy(&xu_ly, &p, sizeof xu_ly);

/* Ca ba deu cho ra cung mot ma may. Chon cach nao la chuyen
   phong cach, nhung dung dung phep ep truc tiep. */

#Một lớp bao cho cả hai

thuvien.h
#ifndef THUVIEN_H
#define THUVIEN_H

typedef struct TayCam TayCam;

/* Nap mot thu vien. Tra ve NULL neu that bai. */
TayCam *tv_nap(const char *duong_dan);

/* Tim mot ham. Tra ve NULL neu khong co. */
void *tv_ham(TayCam *h, const char *ten);

/* Dong. Chap nhan NULL. */
void tv_dong(TayCam *h);

/* Thong bao loi cua lan goi that bai gan nhat. Khong bao gio NULL. */
const char *tv_loi(void);

/* Macro nap ham an toan ve kieu */
#define TV_HAM(bien, tay_cam, ten) \
    (*(void **)(&(bien)) = tv_ham((tay_cam), (ten)))

#endif
thuvien.c
#include <stdio.h>
#include <string.h>
#include "thuvien.h"

static char g_loi[512];

#if defined(_WIN32)

#include <windows.h>

struct TayCam { HMODULE h; };

static void ghi_loi(const char *viec) {
    DWORD ma = GetLastError();
    char *tin = NULL;

    FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER
                 | FORMAT_MESSAGE_FROM_SYSTEM
                 | FORMAT_MESSAGE_IGNORE_INSERTS,
                   NULL, ma, 0, (char *)&tin, 0, NULL);

    snprintf(g_loi, sizeof g_loi, "%s: loi %lu: %s",
             viec, (unsigned long)ma, tin ? tin : "(khong ro)");
    LocalFree(tin);
}

TayCam *tv_nap(const char *duong_dan) {
    HMODULE h = LoadLibraryA(duong_dan);
    if (h == NULL) { ghi_loi("nap"); return NULL; }

    TayCam *t = malloc(sizeof *t);
    if (t == NULL) { FreeLibrary(h); return NULL; }

    t->h = h;
    return t;
}

void *tv_ham(TayCam *t, const char *ten) {
    if (t == NULL) return NULL;

    void *p = (void *)GetProcAddress(t->h, ten);
    if (p == NULL) ghi_loi("tim ham");
    return p;
}

void tv_dong(TayCam *t) {
    if (t == NULL) return;
    FreeLibrary(t->h);
    free(t);
}

#else

#include <dlfcn.h>
#include <stdlib.h>

struct TayCam { void *h; };

TayCam *tv_nap(const char *duong_dan) {
    dlerror();

    void *h = dlopen(duong_dan, RTLD_NOW | RTLD_LOCAL);
    if (h == NULL) {
        snprintf(g_loi, sizeof g_loi, "nap: %s", dlerror());
        return NULL;
    }

    TayCam *t = malloc(sizeof *t);
    if (t == NULL) { dlclose(h); return NULL; }

    t->h = h;
    return t;
}

void *tv_ham(TayCam *t, const char *ten) {
    if (t == NULL) return NULL;

    dlerror();
    void *p = dlsym(t->h, ten);

    const char *loi = dlerror();
    if (loi != NULL) {
        snprintf(g_loi, sizeof g_loi, "tim ham %s: %s", ten, loi);
        return NULL;
    }
    return p;
}

void tv_dong(TayCam *t) {
    if (t == NULL) return;
    dlclose(t->h);
    free(t);
}

#endif

const char *tv_loi(void) { return g_loi[0] ? g_loi : "khong co loi"; }

#Bốn tình huống dùng tới

Một: hệ thống plugin

Người dùng thả tệp thư viện vào một thư mục, chương trình tự tìm và nạp. Đây là ứng dụng lớn nhất, và Bài 39.11 dành cả bài cho nó.

Hai: tính năng tùy chọn

/* Chuong trinh chay duoc khong can thu vien nay, nhung co thi tot hon */

static int (*nen_du_lieu)(const void *, size_t, void *, size_t *);
static TayCam *h_nen;

void khoi_tao_nen(void) {
    h_nen = tv_nap("libzstd.so.1");
    if (h_nen == NULL) {
        printf("khong co libzstd, tat tinh nang nen\n");
        return;
    }
    TV_HAM(nen_du_lieu, h_nen, "ZSTD_compress");
}

int luu(const void *d, size_t n) {
    if (nen_du_lieu != NULL) return luu_co_nen(d, n);
    return luu_khong_nen(d, n);
}

/* Uu diem so voi lien ket luc dich: goi cua ban khong PHU THUOC
   vao libzstd, nen cai duoc tren may khong co no. */

Ba: chọn cài đặt theo phần cứng

/* Nap ban toi uu cho CPU dang co */

const char *ban;
if (co_avx512())      ban = "libtoan_avx512.so";
else if (co_avx2())   ban = "libtoan_avx2.so";
else                  ban = "libtoan_sse2.so";

TayCam *h = tv_nap(ban);

/* Cach nay duoc dung trong thu vien xu ly anh, mat ma va hoc may.
   C con co mot co che khac cho viec nay, la ifunc cua GNU, nhung
   dlopen de hieu va de go loi hon nhieu. */

Bốn: nạp lại mã khi đang chạy

/* Sua ma game, dich lai, va thay ngay ma khong khoi dong lai:

     while (dang_chay) {
         if (tep_da_doi("game.so")) {
             luu_trang_thai(&tt);
             tv_dong(h);
             h = tv_nap("game.so");
             TV_HAM(cap_nhat, h, "cap_nhat");
             khoi_phuc_trang_thai(&tt);
         }
         cap_nhat(&tt, dt);
     }

   Ky thuat nay ten la hot reload. Dieu kien: MOI trang thai phai
   nam trong mot struct do CHUONG TRINH CHINH so huu, khong phai
   trong bien toan cuc cua thu vien. Vi thu vien bi go va nap lai,
   moi bien toan cuc cua no bi dat lai. */

Tự làm thử

  1. Viết chương trình nạp một thư viện lúc chạy và gọi một hàm trong đó.
  2. Xóa thư viện đi và xác nhận chương trình vẫn khởi động, chỉ báo thiếu tính năng.
  3. Thử ép kiểu trực tiếp với -Wpedantic và đọc cảnh báo.
  4. So RTLD_LAZY với RTLD_NOW trên một thư viện thiếu ký hiệu.
  5. Viết lớp bao chạy được trên cả POSIX lẫn Windows.
  6. Thử dlopen(NULL, RTLD_NOW) rồi tìm một hàm trong chính chương trình của bạn.

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

  • Bốn hàm là đủ: nạp, tìm ký hiệu, đóng, đọc lỗi. Cả POSIX lẫn Windows đều vậy.
  • Kiểm tra lỗi của dlsym phải qua dlerror, vì NULL có thể là một giá trị hợp lệ.
  • Ép con trỏ dữ liệu sang con trỏ hàm là hành vi không xác định theo chuẩn C; cách của POSIX là ghi qua địa chỉ: *(void **)(&f) = dlsym(...).
  • RTLD_NOW cho plugin để lỗi lộ ra ngay lúc nạp, RTLD_LAZY cho thư viện lớn dùng ít hàm.
  • Trạng thái phải nằm ở chương trình chính, vì biến toàn cục của thư viện bị đặt lại mỗi lần nạp lại.