Hành Trình Tạo Ra Một Máy Ảo
Trong thế giới lập trình, không có điều gì là không thể thực hiện được - chỉ có những thứ bạn chưa nghĩ đến hoặc chưa muốn làm mà thôi.
Cách đây không lâu, tôi từng đau đầu không biết nên chọn ngôn ngữ kịch bản nào để tích hợp vào game. Tôi đã mày mò qua mã nguồn của Lua vài lần, đọc lướt qua Python, thậm chí còn thử cả lcc và cái gì đó tên là ch. Nhưng cuối cùng trong các dự án thực tế, tôi chỉ dùng đến Lua và Python. Riêng tôi có chút thiên vị với Lua hơn. Càng đọc sâu vào source code, tôi càng thấy danh hiệu “ngôn ngữ kịch bản nhanh nhất thế giới” của nó hoàn toàn xứng đáng.
Hồi mới học lập trình, tôi từng nghĩ việc hiển thị hình ảnh là điều gì đó cực kỳ phức tạp, cho đến khi phát hiện ra nó đơn giản đến bất ngờ. Tôi cũng từng sợ hãi trước hợp ngữ, rồi sau đó nhận ra nó chẳng đáng ngại. Khi mới tiếp xúc C++, tôi lo lắng không biết có học nổi không, giờ đây đã có thể xử lý thuần thục. Những thư viện như STL, MFC hay các công nghệ như Perl, PHP cũng vậy - lúc chưa hiểu thì thấy như núi, khi đã nắm rõ rồi mới thấy hóa ra cũng chỉ là chuyện thường.
Trong lĩnh vực game, dù là đồ họa 3D hay lập trình mạng, tất cả đều là những kỹ thuật có thể chinh phục được nếu bạn đầu tư đủ thời gian. Lần này tôi chọn thử thách mới: xây dựng máy ảo kịch bản. Dù đã ấp ủ ý tưởng này từ lâu nhưng mãi đến gần đây mới bắt tay vào thực hiện. Ban đầu tôi lo lắng sẽ gặp vô số khó khăn, nhưng khi bắt tay vào mới thấy mọi chuyện không quá phức tạp như tưởng tượng.
Khởi nguồn từ ý tưởng nhen nhóm vào thứ Sáu tuần trước, việc tự xây dựng ngôn ngữ kịch bản mang lại cho tôi hai lợi ích lớn: Thứ nhất, được cảm giác hào hứng khi tự tay code từ đầu; Thứ hai, tôi có toàn quyền kiểm soát mọi thứ - muốn thêm tính năng nào thì thêm, muốn sửa gì thì sửa, thậm chí còn có thể thiết kế lại ngôn ngữ theo ý mình. Về hiệu năng, sau khi nghiên cứu nhiều dự án mã nguồn mở, tôi đã có những định hướng rõ ràng. Và quan trọng không kém là yếu tố bảo mật - trong game online, vấn đề chống hack và reverse engineering luôn là mối quan tâm hàng đầu dù có thể bị giới học thuật coi thường.
Ban đầu tôi quyết định dùng thuần C để thực hiện, không phải vì lý do gì khác ngoài việc muốn rèn luyện kỹ năng lập trình C. Trong hai ngày một đêm, tôi đã viết được 1500 dòng code. Đến Chủ Nhật, khi bắt đầu triển khai phần garbage collection (GC), tôi mới thấy rõ sự bất tiện. So với những dự án C trước đây thì tiến bộ rõ rệt, nhưng so với code C++ quen thuộc thì vẫn còn kém xa. Không chần chừ, tôi quyết định refactor toàn bộ sang C++.
Đến tối hôm qua, phần cốt lõi đã hoàn thành: hệ thống logic cơ bản, hỗ trợ stack, table, function… với tổng cộng hơn 2000 dòng code. Module GC được viết khá thuận lợi nhờ thuật toán đánh dấu và sắp xếp (mark-compact). Để tối ưu hiệu năng, tôi thiết kế cơ chế phân trang nội bộ nhằm giảm thiểu việc di chuyển các khối nhớ lớn. Ban đầu tưởng là ý tưởng thông minh, nhưng chính cơ chế này đã khiến tôi mất ăn mất ngủ sau này.
Khi hoàn thành GC, tôi viết một chương trình nhỏ tính giai thừa bằng đệ quy dưới dạng bytecode, đồng thời thử nghiệm tính năng lồng hàm. Kết quả là hàng loạt lỗi phát sinh. Những năm gần đây code quá suôn sẻ khiến tôi có chút chủ quan, luôn tin tưởng vào khả năng viết code không lỗi sau khi đã suy nghĩ kỹ. Dù đã ấp ủ ý tưởng từ cuối năm ngoái và chuẩn bị kỹ lưỡng, tôi vẫn không ngờ gặp phải bug nghiêm trọng suốt đêm.
Vấn đề đầu tiên nằm ở chính thiết kế bytecode. Tôi đã thiết kế bytecode như một đối tượng có thể bị GC thu gom và di chuyển. Hậu quả là khi một đoạn code đang chạy dở, GC kích hoạt có thể khiến con trỏ lệnh bị “bay mất”, nửa đoạn code sau không còn ở vị trí ban đầu. Đây là sai sót duy nhất trong thiết kế mà tôi nhận ra sau này. Từ đây bắt đầu cuộc chiến debug gian khổ.
Vì sử dụng cơ chế GC phân trang phức tạp, mọi con trỏ đều có thể thay đổi bất kỳ lúc nào sau mỗi lần GC chạy. Việc theo dõi bộ nhớ trở nên cực kỳ khó khăn. Đặc biệt, trang đầu tiên còn là trang đặc biệt dùng chung với stack, dù giúp loại bỏ hoàn toàn phân mảnh bộ nhớ nhưng khiến logic GC trở nên rối rắm đến mức ám ảnh.
Trong tình trạng mệt mỏi và đầu óc không còn tỉnh táo giữa đêm, tôi đành đánh bừa bằng cách thêm hàng loạt assert. May mắn là test case được chuẩn bị kỹ nên càng thêm assert, lỗi mới càng ít xuất hiện. Đến giữa trưa hôm sau, tôi mới hoàn toàn yên tâm mọi vấn đề đã được giải quyết.
Qua trải nghiệm này, tôi rút ra vài bài học quý giá: Một lập trình viên có thể viết code C đẹp mắt sẽ trở thành bậc thầy C++ nếu biết tận dụng đúng cách. Từ nay tôi sẽ dành nhiều thời gian hơn cho lập trình thuần C để nâng cao trình độ. Làm việc xuyên đêm có thể mang lại cảm hứng sáng tạo, nhưng debug hệ thống phức tạp vào lúc đó là điều nên tránh. Cuối cùng, dù lý thuyết có phức tạp đến đâu, chỉ cần hiểu thấu đáo bản chất thì mọi thứ đều có thể giải quyết được - và việc tự mình mày mò nghiên cứu luôn mang lại hiệu quả hơn là chỉ đọc sách.
Giai đoạn tiếp theo sẽ là viết compiler - một thử thách mới đang chờ đợi!