Quản Lý Đối Tượng C Trong Lua - nói dối e blog

Quản Lý Đối Tượng C Trong Lua

Quản lý đối tượng C trong Lua
Hôm nay, một đồng nghiệp gặp phải bài toán khi thiết kế giao diện script cho engine: làm thế nào để lưu trữ con trỏ đối tượng C trong Lua, cho phép Lua giữ lại và truyền cho các module khác? Đây là tình huống quen thuộc khi mở rộng Lua bằng C. Dù vẫn tồn tại những vấn đề phức tạp liên quan đến việc export phương thức đối tượng từ C sang Lua, nhưng trọng tâm của tôi hôm nay là về cách quản lý vòng đời của đối tượng C.

Giải pháp ban đầu là export luôn cả hàm hủy đối tượng sang Lua, giao trách nhiệm quản lý bộ nhớ cho lập trình viên script. Đây là tư duy quen thuộc của lập trình C: “ai tạo thì người đó hủy”. Tuy nhiên trong môi trường có cơ chế garbage collection (GC), cách tiếp cận này không còn phù hợp và dễ gây lỗi.

Chúng tôi mong muốn script có tính ổn định cao hơn, không cần quan tâm đến việc giải phóng tài nguyên. Vì vậy tối qua tôi đã suy nghĩ và cải tiến lại phần này.
Về mặt hiệu suất, có thể chia thành hai trường hợp:

Trường hợp 1: Đối tượng C có thể đảm bảo hiệu lực suốt thời gian sống của Lua state machine. Ví dụ như các đối tượng được tạo trước khi khởi tạo Lua và chỉ hủy khi Lua state machine kết thúc. Lúc này chỉ cần dùng lightuserdata để đẩy con trỏ vào stack là đủ.

Trường hợp 2: Đối tượng C được script tạo hoặc thu thập được. Khi không còn reference nào trong script, đối tượng cần được tự động hủy. Đây là lúc nên dùng fulluserdata kết hợp với metatable đăng ký hàm __gc để Lua tự động thu gom.

Tuy nhiên vấn đề trở nên phức tạp khi reference đến đối tượng C có thể đến từ cả script và mã C. Trong khi Lua state machine tự quản lý reference từ script thì cơ chế GC của Lua lại không biết liệu mã C còn giữ reference hay không. Đây chính là điểm mấu chốt cần giải quyết.

Python giải quyết vấn đề này qua các hàm tăng/giảm reference count cho PyObject từ bên C. Nhưng Lua sử dụng cơ chế đánh dấu quét (mark-and-sweep) dựa trên root objects, đồng nghĩa với việc không hỗ trợ reference count trong C API. Đây là lý do tôi phải tự xây dựng cơ chế riêng.

Giải pháp của tôi là:

  1. Tạo một weak table trong Lua registry với mode “v” (value yếu, key mạnh) để lưu mapping giữa con trỏ C, fulluserdata tương ứng và số
  2. Xây dựng cặp hàm API để tăng/giảm reference count này
  3. Khi reference count về 0, xóa mục khỏi bảng
  4. Khi GC thu gom fulluserdata, nếu reference count từ C đã bằng 0 thì tiến hành hủy đối tượng thật sự

Các chi tiết kỹ thuật có thể tham khảo code tôi chia sẻ trên wiki. Một số điểm bổ sung:

  • Giữa các fulluserdata, chúng ta nên chia sẻ chung một metatable chứa hàm __gc. Đây là cách tối ưu bộ nhớ hiệu quả. Metatable này cũng được lưu trong weak table nói trên.
  • Mỗi khi tạo fulluserdata từ con trỏ C, cần kiểm tra xem đã tồn tại trước đó chưa để đảm bảo chỉ có duy nhất một fulluserdata đại diện, tránh việc đếm reference sai lệch
  • Code được viết nhanh nên chưa qua kiểm thử nghiêm ngặt. Ai dùng thử phát hiện lỗi xin báo lại để tôi sửa chữa!

Một điểm thú vị cần lưu ý: Mỗi lần đẩy CFucntion vào stack Lua đều tạo ra object mới trong bộ nhớ. Vì vậy cần tối ưu bằng cách cache và tái sử dụng CFucntion khi có thể.

Hiện tại giải pháp này đã đáp ứng được yêu cầu về tính tự động trong quản lý bộ nhớ, đồng thời đảm bảo đối tượng C chỉ bị hủy khi không còn reference từ bất kỳ phía nào, dù là từ script hay từ mã C.

0%