Một Số Cải Tiến Nhỏ Cho Thư Viện Vector
Gần đây tôi vừa hoàn thành một thư viện toán học 3D được các đồng nghiệp sử dụng và nhận được nhiều góp ý quý giá. Những phản hồi này đã thúc đẩy quá trình hoàn thiện hệ thống trong suốt thời gian qua.
Ban đầu, việc áp dụng ký pháp nghịch Ba Lan (Reverse Polish Notation) trong các phép toán vector khiến nhiều người thấy không quen. Có ý kiến đề xuất xây dựng một trình biên dịch biểu thức để thuận tiện hơn. Tuy nhiên sau vài ngày sử dụng, chúng tôi nhận thấy rằng việc này không thật sự cần thiết nếu đã quen với tư duy ngược. Nhưng có một điểm gây nhiều khó khăn hơn: quy ước dấu của ID để quản lý vòng đời đối tượng.
Việc tự quản lý vòng đời đối tượng quả thực rất phiền phức. Một số đối tượng cần duy trì suốt quá trình chạy chương trình, trong khi nhiều đối tượng khác chỉ cần tồn tại trong một khung hình render. Việc phân biệt rõ ràng giữa hai loại này trong quá trình sử dụng là điều gần như không khả thi.
Ở phiên bản đầu tiên, người dùng phải sử dụng lệnh đánh dấu ‘M’ trong chuỗi lệnh để chuyển đổi một đối tượng tạm thời thành đối tượng lâu dài. Điều này làm tăng đáng kể gánh nặng cho người sử dụng. Đặc biệt khi cập nhật đối tượng, phải hủy trạng thái lâu dài của phiên bản cũ trước khi đánh dấu phiên bản mới. Mặc dù hệ thống có cơ chế kiểm tra chặt chẽ để tránh xung đột, nhưng chỉ cần sơ suất nhỏ là có thể gặp lỗi runtime (ID đối tượng không hợp lệ).
Hôm nay, tôi đã thực hiện một cải tiến lớn: loại bỏ hoàn toàn cơ chế đánh dấu này và thay thế bằng cơ chế tham chiếu (reference semantics). Trước đây, tôi cho rằng việc duy trì ngữ nghĩa giá trị (value semantics) là đủ, nhưng thực tế đã chứng minh điều ngược lại. Việc triển khai ngữ nghĩa tham chiếu vốn phức tạp hơn nhiều, và phải mất một thời gian dài tôi mới tìm ra giải pháp phù hợp.
Giải pháp tôi chọn là triển khai các đối tượng tham chiếu dưới dạng userdata trong Lua. Cụ thể, khi một biến Lua như foobar cần tham chiếu đến một vector, chúng ta sẽ định nghĩa:
|
|
Hàm math3d.ref"vector"
sẽ tạo ra một đối tượng tham chiếu đặc biệt. Trong quá trình chạy chương trình, chúng ta có thể thay đổi giá trị mà foobar tham chiếu đến, nhưng giá trị của chính biến foobar trong Lua sẽ không thay đổi.
Để thực hiện gán giá trị cho biến tham chiếu này, tôi đã bổ sung lệnh =
vào hệ thống lệnh. Ví dụ, muốn gán vector {1,0,0,1}
cho foobar, người dùng sẽ viết:
|
|
Lệnh này thực hiện các bước sau:
- Đẩy biến tham chiếu foobar và vector hằng số
{1,0,0,1}
vào stack - Thực hiện phép gán từ phần tử đỉnh stack cho phần tử kế đỉnh
- Loại bỏ hai phần tử này khỏi stack
Trước đây, stack lệnh chỉ chứa ID số (chỉ số đối tượng), chuỗi lệnh hoặc bảng hằng số. Bây giờ, chúng tôi mở rộng thêm loại userdata đặc biệt để biểu diễn các đối tượng tham chiếu.
Để tiện lợi hơn, tôi còn bổ sung metatable cho các biến tham chiếu. Giờ đây:
- Gọi
foobar(obj)
tương đương với gán obj cho foobar - Dùng
~foobar
sẽ lấy con trỏ C (lightuserdata) của đối tượng được tham chiếu để truyền xuống tầng底层
Thiết kế mới này mang lại trải nghiệm sử dụng tốt hơn rõ rệt, nhưng cũng đặt ra nhiều thách thức trong quá trình triển khai. Đặc biệt, khi stack dữ liệu giờ đây chứa cả userdata Lua thay vì chỉ ID số như trước, việc tương tác với cấu trúc dữ liệu ở tầng C trở nên phức tạp hơn.
Để giải quyết vấn đề, tôi áp dụng giới hạn: các đối tượng tham chiếu chỉ có thể sử dụng trong chuỗi lệnh hiện tại. Nếu một đối tượng tham chiếu bị giữ lại trong stack mà không được xử lý trong chuỗi lệnh hiện tại, nó sẽ tự động chuyển về dạng giá trị thông thường sau khi chuỗi lệnh kết thúc.
Về mặt kỹ thuật, tôi không sửa đổi cấu trúc dữ liệu cũ mà thay vào đó xây dựng thêm một stack chuyên dụng để quản lý các đối tượng tham chiếu. Stack này ghi nhận vị trí của các đối tượng tham chiếu trong stack Lua (dưới dạng chỉ số số). Khi hàm kết thúc, các chỉ số này tự động失效, giúp đơn giản hóa quản lý vòng đời mà không làm ảnh hưởng đến hiệu năng.