Một Số Kỹ Thuật Khi Viết Thư Viện Mở Rộng C Trong Lua
Hôm nay, một đồng nghiệp trong nhóm Ark đã hỏi tôi vài câu hỏi về Lua, chủ yếu liên quan đến việc phát triển thư viện mở rộng C. Tôi nhận thấy một số mẹo kỹ thuật khá thú vị nên muốn chia sẻ với mọi người thông qua bài viết này.
Thông thường, khi viết mã C cho thư viện mở rộng, chúng ta thường cần lưu trữ một số dữ liệu trong máy trạng thái Lua. Lua cung cấp cơ chế bảng đăng ký (registry) để thực hiện việc này. Tuy nhiên, như tài liệu chính thức đã cảnh báo, vì bảng đăng ký là không gian toàn cục được chia sẻ, việc chọn khóa (key) cần được thực hiện cẩn trọng. Các khóa là số nguyên đã được hệ thống reference sử dụng sẵn, do đó chúng ta thường ưu tiên dùng chuỗi ký tự làm khóa.
Việc đẩy chuỗi ký tự từ C vào Lua không phải là phương pháp hiệu quả nhất, bởi mỗi lần đưa chuỗi bên ngoài vào máy trạng thái đều phải thực hiện băm (hash) và kiểm tra tính duy nhất. Về vấn đề này, tôi đã từng viết một bài blog trước đây thảo luận cách tối ưu (xin lưu ý: giải pháp cũ không phải là tối ưu nhất nhưng vẫn có giá trị
Một cách trực quan, khóa đảm bảo duy nhất và tiện lợi nhất chính là sử dụng userdata nhẹ (light userdata). Điều này đã được đề cập trong cả tài liệu hướng dẫn chính thức lẫn cuốn “Programming in Lua”.
Chúng ta có thể dùng địa chỉ của một biến static làm khóa để lưu trữ giá trị trong bảng đăng ký. Ví dụ:
|
|
Đoạn mã trên đã lưu giá trị số myNumber
từ C vào bảng đăng ký. Khi cần truy xuất giá trị này sau này:
|
|
Phương pháp này sử dụng địa chỉ của biến static làm khóa, đảm bảo không xung đột với thư viện mở rộng khác. Tuy nhiên, nhược điểm là khóa tồn tại dưới dạng biến toàn cục, không được đẹp về mặt kiến trúc. Hơn nữa, khi cần truy cập khóa này ở các hàm khác, bạn phải dùng extern
hoặc tạo một hàm singleton để lấy khóa, điều này có thể ảnh hưởng đến hiệu suất.
Để giải quyết vấn đề này, tôi xin giới thiệu một giải pháp trung hòa: Sử dụng một userdata nhẹ làm khóa, đồng thời liên kết khóa này với một chuỗi duy nhất. Trong mỗi hàm cần truy cập khóa, ta thực hiện khởi tạo lười biếng (lazy initialization) như sau:
|
|
Với cách này, khóa sẽ được khởi tạo chỉ khi cần thiết. Đoạn mã trên có thể được đóng gói thành một hàm hoặc macro để tiện tái sử dụng.
Một mẹo nhỏ ít được đề cập nữa: Khi sử dụng userdata đầy đủ (full userdata) để biểu diễn cấu trúc C phức tạp hoặc đối tượng C++, chúng ta thường cần liên kết dữ liệu Lua với userdata. Để đảm bảo cơ chế thu gom rác (GC) hoạt động đúng, cần thực hiện một số thao tác bổ sung.
Giải pháp kém tối ưu nhất là lưu trữ reference đến bảng Lua trong userdata và thêm một phương thức GC trong metatable. Khi sự kiện GC xảy ra, ta giải phóng bảng này. Tuy nhiên, cách này gây ra nhiều overhead và phức tạp trong quản lý.
Giải pháp trung bình là tạo một bảng ánh xạ trong bảng đăng ký với tính chất weak table. Mỗi khi tạo userdata, ta dùng userdata làm khóa để lưu dữ liệu liên quan trong bảng này. Lua sẽ tự động dọn dẹp khi userdata bị thu hồi. Nhược điểm là mỗi lần truy cập dữ liệu, ta phải truy vấn bảng đăng ký (có thể áp dụng kỹ thuật đã đề cập ở phần trước để tối ưu).
Giải pháp tối ưu nhất chính là tính năng được thiết kế sẵn trong Lua: Mỗi userdata đầy đủ có thể sở hữu một bảng môi trường (environment table) riêng. Bảng này không có ý nghĩa với Lua nhưng tham gia vào quá trình GC. Ta chỉ cần dùng lua_getfenv
và lua_setfenv
để đọc/ghi bảng này là đủ! 😊