LÀM QUEN VỚI DLL Văn Chí Nam
[email protected] Khoa Công nghệ Thông tin, trường ĐH KHTN TP.HCM Phiên bản cập nhật ngày 15/11/2005
Giới thiệu DLL là gì? DLL là viết tắt của cụm từ Dynamic Link Library. DLL là một file nhị phân chứa một hoặc nhiều hàm thực thi cho phép các ứng dụng khác sẽ có thể gọi đến. Một DLL có thể được nhiều ứng dụng sử dụng đồng thời. Các hàm trong DLL được phân chia làm 2 loại: export (hàm xuất, những hàm cho phép các ứng dụng bên ngoài sử dụng), internal (hàm nội bộ, những hàm chỉ được sử dụng trong nội bộ DLL). Ưu điểm của việc sử dụng DLL Sử dụng DLL sẽ làm cho kích thước của ứng dụng nhỏ. Nhiều ứng dụng có thể dùng chung 1 DLL, do đó, tiết kiệm bộ nhớ (thông thường, các ứng dụng có dữ liệu riêng, nhưng có thể chia sẻ mã lệnh) Khi không còn sử dụng, có thể giải phóng DLL khỏi bộ nhớ. Khi cần nâng cấp, chỉ cần thay thế file DLL, các file chương trình khác không bị ảnh hưởng. Khuyết điểm của việc sử dụng DLL Đòi hỏi lập trình viên phải thực hiện nhiều thao tác hơn bình thường
Phân loại DLL MFC hỗ trợ 2 loại DLL: DLL dạng mở rộng (Extension DLL) và dạng bình thường (Regular DLL). DLL dạng mở rộng (Extension DLL) Extension DLL cho phép sử dụng lại các lớp từ MFC cũng như cho phép các ứng dụng khác sử dụng các lớp (dạng “export”) của nó.
Các ứng dụng MFC có định nghĩa _AFXEXT chỉ sử dụng được với Extension DLL mà thôi. Extension DLL liên kết với các DLL của MFC theo dạng liên kết động. Regular DLL Regular DLL có thể sử dụng được với các ứng dụng MFC và các ứng dụng dạng Win32 Application. Regular DLL chỉ cho phép “export” các hàm dạng C/C++ chứ không cho phép “export” các lớp có trong DLL. Regular DLL có thể liên kết với các DLL của MFC theo 1 trong 2 cách: liên kết tĩnh (statically linking) và liên kết động (dynamic linking). Liên kết tĩnh có nghĩa là các đoạn mã trong DLL của MFC sẽ được chèn vào trong Regular DLL mỗi khi hàm tương ứng được sử dụng đến. Việc liên kết tĩnh với các thư viện MFC sẽ làm cho DLL (Regular DLL) có kích rất lớn. Để giảm kích thước của DLL xuống, chúng ta có thể liên kết dạng liên kết động. Với dạng này, các đoạn mã của thư việc MFC sẽ không chèn trực tiếp vào trong Regular DLL và chỉ nạp lên khi cần được sử dụng đến. Tuy nhiên, với dạng liên kết này, chúng ta phải kèm các DLL của MFC cần sử dụng với Regular DLL của chúng ta.
Cách thức gọi DLL trong ứng dụng Gọi lúc ứng dụng được nạp (load-time) Với cách thức gọi DLL theo kiểu này, ứng dụng sẽ nạp DLL lên bộ nhớ bất kể ứng dụng có sử dụng đến những hàm thực thi của DLL trong quá trình thực thi hay không. Chính vì vậy, điều này có thể dẫn đến những bất tiện: tốn bộ nhớ nạp DLL, ứng dụng có thể không thể hoạt động được trong trường hợp DLL không tồn tại. Tuy nhiên, cách này rất dễ thực hiện đối với những lập trình viên mới bắt đầu sử dụng DLL. Gọi lúc thực thi (run-time) Cách gọi DLL kiểu này vượt qua những khó khăn gặp phải của kiểu gọi DLL lúc ứng dụng được nạp (load-time): lập trình viên sẽ quyết định khi nào cần phải nạp DLL lên bộ nhớ, ứng dụng có thể thực thi (với những chức năng bổ sung khác) nếu DLL không tồn tại. Tuy nhiên, điểm khó khăn của cách gọi này nằm ở chỗ người lập trình viên sẽ tốn nhiều công sức cho việc nạp DLL, gọi hàm trong DLL.
Minh hoạ xây dựng một regular DLL Trong minh hoạ dưới đây, chúng tôi minh hoạ cách tạo lập một Regular DLL trong Visual C++ 6.0 (sử dụng dạng liên kết động với các DLL của MFC). DLL này sẽ chứa hai hàm tính toán (cộng và nhân) cho phép các ứng dụng khác gọi đến. Minh hoạ dưới đây có 2 hàm xuất (export). Tạo ứng dụng DLL dạng Regular - Chọn New\Projects\MFC AppWizard (dll) - Đặt tên project (Project name) và chọn lựa vị trí thư mục thích hợp (Location).
Chọn Regular DLL using shared MFC DLL tại Step 1 of 1
Nhấn Finish để hoàn tất việc tạo lập project dạng Regular DLL. Visual C++ 6.0 sẽ phát sinh một số tập tin và một số đoạn mã cho ứng dụng.
Khai báo hàm xuất Dạng hàm tổng quát extern "C" __declspec(dllexport) TenKieuTraVe TenHam(CacThamSo);
Đối với ứng dụng có nhiều hàm cần xuất, chúng ta có thể định nghĩa một giá trị thay thế như sau: #define extern "C" __declspec(dllexport) EXPORT
Khi đó, hàm tổng quát có dạng: EXPORT TenKieuTraVe TenHam(CacThamSo);
Ví dụ: Khai báo hai hàm xuất Cong, và Nhan extern "C" __declspec(dllexport) int Cong(int a, int b); extern "C" __declspec(dllexport) int Nhan(int a, int b);
Hoặc EXPORT int Cong(int a, int b); EXPORT int Nhan(int a, int b);
Khai báo các hàm xuất có thể được đặt trong tập tin header (.h) hoặc tập tin thi hành (.cpp). Không nhất thiết đặt các khai báo hàm xuất trong tập tin header. Khai báo hàm nội bộ Hàm nội bộ trong DLL không có khai báo gì đặc biệt hơn so với việc khai báo hàm trong các ứng dụng thông thường. Viết phần thực thi cho hàm xuất Khi viết phần thực thi cho các hàm xuất cần lưu ý đặt macro AFX_MANAGE_STATE đầu tiên trong thân hàm . Macro này cần thiết đối với những hàm có gọi (sử dụng) những đối tượng từ các thư viện MFC. Trong những hàm xuất không có sử dụng đến các đối tượng của MFC thì có thể không cần sử dụng macro này. Tuy nhiên, lời khuyên là: “nên đạt macro này vào đầu mỗi thân hàm xuất”. Ví dụ: Phần thực thi cho hàm Cong.
extern "C" __declspec(dllexport) int Cong(int a, int b) { AFX_MANAGE_STATE(AfxGetStaticModuleState()); return a + b; }
Phần thực thi cho hàm Nhan. extern "C" __declspec(dllexport) int Nhan (int a, int b) { AFX_MANAGE_STATE(AfxGetStaticModuleState()); return a*b; }
Biên dịch DLL giống như biên dịch các ứng dụng bình thường khác. Sau khi biên dịch thành công, tập tin .dll tương ứng được tạo ra và đi kèm với tập tin này sẽ có tập tin thư viện .lib. Chúng tôi sẽ minh hoạ cách sử dụng hai tập tin này ở phần sau của bài viết này.
Minh hoạ tạo một ứng dụng sử dụng DLL Phần tiếp theo của bài viết này sẽ minh hoạ việc sử dụng DLL vừa được tạo ở phần trên. Cách thức gọi DLL trong ứng dụng này là gọi DLL lúc nạp ứng dụng (kiểu loadtime). Đây là cách thức nạp DLL đơn giản, dễ thực hiện. Tuy nhiên, hạn chế của cách thức nạp DLL này là khi không tìm thấy DLL tương ứng trong lúc nạp ứng dụng thì ứng dụng sẽ thoát và không thực hiện gì tiếp theo. Tạo lập ứng dụng Ứng dụng được tạo lập theo dạng dialog-based với các control đặt trên dialog như mô tả dưới đây
Đặt các biến đại diện tương ứng với các control nhập trên màn hình
Thêm tập tin .lib vào trong project ứng dụng Như đã đề cập ở phần trên, sau khi biên dịch Regular DLL, có 2 tập tin quan trọng cần lưu giữ: .lib và .dll. File .lib chứa các khai báo của các hàm có trong DLL, file này được sử dụng đối với cách gọi DLL kiểu load-time, nó hỗ trợ cho quá trình biên dịch ứng dụng. File .dll sẽ chứa mã thực thi các hàm của DLL, các hàm này sẽ được nạp vào bộ nhớ khi .dll được gọi. Đầu tiên, cần phải chép các file .lib và .dll vào cùng thư mục chứa project của ứng dụng gọi DLL. Thêm file .lib vào trong project bằng cách: - Chọn Project\Add to Project\ Files. - Chọn Files of type là Library Files (.lib). - Chọn file cần thêm vào (RegularDLL.lib) và nhấn OK.
Khai báo các hàm nhập trong ứng dụng Những hàm xuất (export) trong DLL chính là những hàm nhập (import) trong ứng dụng. Chúng ta có thể chỉ cần khai báo những hàm nhập cần thiết, không phải khai báo tất cả những hàm mà DLL “xuất”. Dạng hàm khai báo tổng quát extern "C" __declspec(dllimport) TenKieuTraVe TenHam(CacThamSo);
Ví dụ: extern "C" __declspec(dllimport) int Cong(int a, int b);
Trong trường hợp, khai báo hàm xuất trong DLL được đặt trong file header (.h) thì có thể include file đó vào trong project. Tuy nhiên, nhớ chỉnh dllexport trong khai báo xuất thành dllimport trong khai báo nhập. Viết các mã thực thi cho các chức năng Hàm xử lý sự kiện nhấn vào nút Cong. void CUsingRegularDLLDlg::OnCong() { // TODO: Add your control notification handler code here UpdateData(); m_iZ = Cong(m_iX, m_iY); UpdateData(FALSE); }
Hàm xử lý sự kiện nhấn vào nút Nhan. void CUsingRegularDLLDlg::OnNhan() { // TODO: Add your control notification handler code here
UpdateData(); m_iZ = Nhan(m_iX, m_iY); UpdateData(FALSE); }
Hàm xử lý sự kiện nhấn vào nút Thoat. void CUsingRegularDLLDlg::OnThoat() { // TODO: Add your control notification handler code here CDialog::OnCancel(); }
Lưu ý Mỗi khi nội dung các hàm của DLL có thay đổi chúng ta phải cập nhật DLL mới cho ứng dụng bằng cách ghi đè các file DLL cũ đang được ứng dụng sử dụng. Đối với kiểu gọi DLL load-time, mỗi khi DLL có sự thay đổi về cách khai báo các hàm xuất, chúng ta phải xoá .lib cũ khỏi project ứng dụng và thêm (cập nhật) .lib mới cho project ứng dụng.
Minh hoạ tạo ứng dụng gọi DLL kiểu run-time Sử dụng kiểu gọi DLL loại run-time có nhiều đặc điểm thuận lợi so với gọi DLL kiểu load-time: - Không cần file .lib đi kèm lúc tạo project ứng dụng. - DLL được nạp lên khi cần được sử dụng (thay vì nạp lúc ứng dụng được gọi thực thi như kiểu load-time). Việc này dẫn đến khả năng uyển chuyển khi sử dụng DLL: chương trình có thể thực hiện với một số chức năng bị thiếu (do không có DLL), thay đổi DLL khi ứng dụng đang thực thi,… Tuy nhiên, cách gọi này có bất tiện là tốn nhiều công sức cho việc viết phần thực thi gọi hàm. Tạo ứng dụng Ứng dụng được tạo ra kiểu dialog-based giống phần minh hoạ phía trên.
Các hàm nhập cần thiết Trong ứng dụng này, chúng ta sẽ sử dụng hai hàm xuất của DLL là: Cong, và Nhan. Hai hàm này có dạng: int Cong(int a, int b); int Nhan(int a, int b);
Chính vì vậy, chúng ta sẽ định nghĩa con trỏ hàm MYPROC cho hai hàm này như sau: typedef int (*MYPROC)(int, int);
Cách thức nạp hàm từ DLL: - Nạp DLL bằng hàm LoadLibrary. Kiểm tra xem DLL có tồn tại không bằng cách xem kết quả trả về có khác NULL hay không - Nạp hàm cần thiết bằng hàm GetProcAddress. Kiểm tra việc gọi hàm thành công hay không. - Giải phóng DLL khỏi bộ nhớ bằng hàm FreeLibrary. Ví dụ: HINSTANCE hInstance; hInstance = LoadLibrary(TENTAPTINDLL); MYPROC Ham; if (hInstance == NULL) { //Tap tin DLL khong ton tai return; } Ham = (MYPROC)GetProcAddress(hInstance, HamCanNap); if (Ham == NULL) { //Ham khong ton tai hoac khong dung return; }
FreeLibrary(hInstance);
Viết phần thực thi cho các hàm Hàm xử lý sự kiện nhấn nút Cong. void CUsingRegularDLLRuntimeDlg::OnCong() { // TODO: Add your control notification handler code here HINSTANCE hInstance; hInstance = LoadLibrary("RegularDLL.dll"); MYPROC Cong;
if (hInstance == NULL) { MessageBox("Tap tin RegularDLL.dll khong ton tai","Loi"); return; } Cong = (MYPROC)GetProcAddress(hInstance, "Cong"); if (Cong == NULL) { MessageBox("Khong ton tai ham Cong trong DLL.","Loi"); return; } UpdateData(); m_iZ = Cong(m_iX, m_iY); UpdateData(FALSE);
FreeLibrary(hInstance); }
Hàm xử lý sự kiện nhấn nút Nhan. void CUsingRegularDLLRuntimeDlg::OnNhan() { // TODO: Add your control notification handler code here HINSTANCE hInstance; hInstance = LoadLibrary("RegularDLL.dll"); MYPROC Nhan; if (hInstance == NULL) { MessageBox("Tap tin RegularDLL.dll khong ton tai","Loi"); return; } Nhan = (MYPROC)GetProcAddress(hInstance, "Nhan"); if (Nhan == NULL) { MessageBox("Khong ton tai ham Nhan trong DLL.","Loi"); return; } UpdateData(); m_iZ = Nhan(m_iX, m_iY); UpdateData(FALSE);
FreeLibrary(hInstance); }
Thử nghiệm Thực hiện thử nghiệm dưới đây để hiểu rõ hơn cách gọi DLL theo kiểu run-time. - Không chép DLL vào cùng vị trí của ứng dụng sử dụng DLL. Chạy ứng dụng được không? Có thực thi được các chức năng (Cong, Nhan) của ứng dụng không? - Không thoát ứng dụng. Chép DLL vào cùng vị trí của ứng dụng sử dụng DLL. Thực thi được các chức năng của ứng dụng không?
Debug với DLL Khi cần debug hàm trong DLL, chúng ta sẽ thực thi DLL dựa trên một ứng dụng sử dụng DLL. Trong ví dụ minh hoạ dưới đây, chúng tôi sử dụng DLL (Regular.dll) và ứng dụng (UsingRegularDLL.exe) được tạo lập sẵn để thực hiện việc debug. - Tạo breakpoint tại vị trí cần debug (hàm Cong). - Thực thi DLL ở chế độ Debug. Khi thực thi, môi trường lập trình sẽ yêu cầu chọn lựa ứng dụng sử dụng DLL để thực hiện.
Có thể xuất hiện lỗi sau:
Lỗi đó xuất hiện do ứng dụng không tìm thấy DLL để thực thi. Cần phải chép DLL vào cùng thư mục của ứng dụng và chạy debug lại cho DLL lại. Khi đó, có thể xuất hiện màn hình debug cho DLL như sau:
Tài liệu tham khảo 1. “DLL tutorial”, (MindCracker) 2. MSDN