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ịch | Nạp lúc chạy | |
|---|---|---|
| Còn gọi là | Implicit linking | Explicit linking, dlopen |
| Khi nào nạp | Trước khi main chạy | Khi bạn gọi hàm nạp |
| Thiếu thư viện thì | Chương trình không khởi động được | Hàm nạp trả về NULL, bạn xử lý |
| Biết tên thư viện lúc | Dịch | Chạy, có thể đọc từ tệp cấu hình |
| Kiểm tra kiểu | Trình biên dịch làm | Không ai làm, bạn tự chịu |
| Cần header của thư viện | Có | Không bắt buộc |
| Dùng cho | Phụ thuộc bắt buộc | Plugin, 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 appMẫ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 dlopen | Nghĩa |
|---|---|
| RTLD_LAZY | Giải ký hiệu khi lần đầu dùng tới. Nạp nhanh hơn |
| RTLD_NOW | Giải hết ký hiệu ngay lúc nạp. Chậm hơn nhưng lỗi lộ ra sớm |
| RTLD_GLOBAL | Ký hiệu của thư viện này dùng được cho các thư viện nạp sau |
| RTLD_LOCAL | Ký hiệu chỉ dùng qua dlsym. Mặc định, và thường là cái đúng |
| RTLD_NODELETE | dlclose không thật sự gỡ khỏi bộ nhớ |
| RTLD_NOLOAD | Chỉ 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ệc | POSIX | Windows |
|---|---|---|
| Nạp | dlopen(ten, RTLD_NOW) | LoadLibraryA(ten) |
| Tìm ký hiệu | dlsym(h, ten) | GetProcAddress(h, ten) |
| Đóng | dlclose(h) | FreeLibrary(h) |
| Lỗi | dlerror(), trả về chuỗi | GetLastError(), trả về số |
| Kiểu tay cầm | void * | HMODULE |
| Cần cờ liên kết | -ldl trên Linux | Không |
| Tay cầm tới chính mình | dlopen(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)))
#endifthuvien.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ử
- 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 đó.
- 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.
- Thử ép kiểu trực tiếp với
-Wpedanticvà đọc cảnh báo. - So
RTLD_LAZYvớiRTLD_NOWtrên một thư viện thiếu ký hiệu. - Viết lớp bao chạy được trên cả POSIX lẫn Windows.
- 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
dlsymphải quadlerror, 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_NOWcho plugin để lỗi lộ ra ngay lúc nạp,RTLD_LAZYcho 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.