Viết lại ứng dụng Ghostty GTK
(mitchellh.com)- Nhóm Ghostty đã viết lại hoàn toàn ứng dụng GTK và tích cực tận dụng hệ thống kiểu GObject
- Trong quá trình này, việc tích hợp với ngôn ngữ Zig và kiểm tra vấn đề bộ nhớ bằng Valgrind đóng vai trò quan trọng
- Việc áp dụng hệ thống GObject giúp quản lý bộ nhớ và triển khai widget tùy chỉnh trở nên đơn giản hơn trước
- Kết quả từ việc sử dụng Valgrind cho thấy độ an toàn bộ nhớ của Ghostty đã được cải thiện đáng kể
- Ghostty GTK mới đã trở thành mặc định cho các bản dựng từ mã nguồn và sẽ được đưa vào bản phát hành 1.2
Giới thiệu
- Ghostty là một trình giả lập terminal đa nền tảng hỗ trợ macOS, Linux, FreeBSD
- Mỗi nền tảng sử dụng framework GUI native riêng để tạo khác biệt
- macOS: ứng dụng quy mô lớn dựa trên Swift và Xcode
- Linux và BSD: ứng dụng dựa trên GTK, tích hợp trực tiếp với X11/Wayland
- Phần lõi dùng chung được viết bằng Zig và cung cấp API tương thích C ABI
- Có thể tham khảo PR gốc để biết lý do viết lại ứng dụng GTK trong cấu trúc cũ
- Bài viết này tập trung vào việc tích hợp với hệ thống kiểu GObject và các vấn đề bộ nhớ được xác minh bằng Valgrind
Hệ thống kiểu GObject và Zig
- Khi sử dụng GTK, về cơ bản phải tương tác với hệ thống kiểu GObject
- Trước đây, dự án cố tránh dùng hệ thống GObject và tự đồng bộ vòng đời của đối tượng Zig không có reference counting với đối tượng GObject, nhưng liên tục gặp vấn đề giải phóng bộ nhớ không đúng cách
- Ví dụ: bộ nhớ phía Zig đã được giải phóng nhưng bộ nhớ phía GTK vẫn còn tồn tại, hoặc ngược lại
- Cách tiếp cận này không chỉ gặp vấn đề về tính đúng đắn mà còn khiến việc sử dụng các tính năng đặc thù của GTK (event signal, property binding, action) trở nên khó khăn
- Một ví dụ cụ thể là khi reload struct cấu hình (config), mọi phần tử GUI liên quan đều phải được cập nhật nhất quán, nhưng quá trình này phức tạp và dễ lỗi
- Hiện tại, dự án quản lý nó bằng
GhosttyConfigGObject có reference counting bao bọc structConfigcủa Zig, và các thông báo thay đổi thuộc tính giúp thay đổi được lan truyền tự nhiên trong toàn bộ ứng dụng
- Hiện tại, dự án quản lý nó bằng
- Việc tạo widget GObject tùy chỉnh cũng trở nên dễ dàng hơn, cho phép dùng các công nghệ UI GTK hiện đại như Blueprint
- Gần đây, nhờ đưa vào Blueprint, việc bổ sung các tính năng mới như tab trên thanh tiêu đề GTK và viền chuông có hiệu ứng động đã trở nên dễ dàng hơn
Valgrind với GTK và Zig
- Trong toàn bộ quá trình phát triển, nhóm đã dùng Valgrind để kiểm chứng một cách có hệ thống các vấn đề như rò rỉ bộ nhớ và truy cập bộ nhớ chưa được định nghĩa
- Việc kiểm tra ứng dụng GTK bằng Valgrind khá khó khăn và cần tới các tệp suppression dung lượng lớn (80% là từ chính GTK, phần còn lại là thư viện bên thứ ba và driver GPU)
- Nhờ kiểm tra lặp đi lặp lại, nhóm có thể phát hiện sớm những lỗi bộ nhớ phức tạp chỉ xuất hiện trong một số trường hợp nhất định
- Ví dụ: nếu không khởi tạo đúng GObject
WeakRef, khi đối tượng đích được giải phóng về sau sẽ xảy ra truy cập bộ nhớ chưa được định nghĩa, và Valgrind đã phát hiện điều này từ trước
- Ví dụ: nếu không khởi tạo đúng GObject
- Trong trải nghiệm thực tế, bên trong codebase Zig chỉ có tổng cộng 2 vấn đề (1 rò rỉ, 1 truy cập chưa được định nghĩa), và cả hai đều phát sinh trong quá trình tích hợp với C API của bên thứ ba
- Allocator debug của Zig và khả năng tích hợp với Valgrind cũng đã chứng minh được hiệu quả thực tế
- Các vấn đề bộ nhớ khác được phát hiện hầu hết xuất phát từ ranh giới C API và việc quản lý vòng đời phức tạp của hệ thống GObject
- Kết luận là, để sử dụng an toàn C API của các thư viện phức tạp, cần những công cụ như Valgrind
- Các tính năng hỗ trợ an toàn bộ nhớ của Zig không chỉ hiệu quả trên lý thuyết mà còn được xác nhận qua trải nghiệm dự án thực tế
Kết luận
- Đây là lần thứ năm phần GUI của Ghostty được xây lại từ đầu
- Theo thứ tự: GLFW, macOS SwiftUI, macOS AppKit+SwiftUI, Linux GTK (thủ tục), Linux GTK + hệ thống kiểu GObject
- Qua mỗi lần viết lại lặp đi lặp lại, nhóm đều thu được bài học mới và sự trưởng thành về kỹ thuật
- Họ cũng có kế hoạch áp dụng một phần kinh nghiệm này cho dự án macOS
- Bài viết cũng nhấn mạnh sự hợp tác tích cực từ đội duy trì hệ thống Ghostty GTK
- Ứng dụng Ghostty GTK được viết lại nay đã trở thành mặc định cho các bản dựng từ mã nguồn và sẽ được áp dụng trong bản phát hành chính thức 1.2
1 bình luận
Ý kiến trên Hacker News
Tôi chưa từng làm việc trực tiếp với GTK, nhưng nghe mô tả thì cảm giác rất giống những vấn đề tôi gặp khi làm binding Godot bằng Zig. Godot có cực nhiều khái niệm OOP như class, virtual method, property, signal, v.v. Và nó cung cấp API C để xử lý toàn bộ các khái niệm đó cũng như cho phép tạo đối tượng và thuộc tính do người dùng định nghĩa. Nó tự quản lý vòng đời của các đối tượng trong engine, và còn có cả cấu trúc cây của các đối tượng dùng reference counting. Khi cố gắng gói các vấn đề về vòng đời này thành một API tối ưu, phù hợp với lối viết Zig, mọi thứ trở nên cực kỳ phức tạp. Trong lúc vật lộn với chuyện đó, tôi còn tạo ra cả thư viện oopz. API hiện vẫn ở mức này, và ví dụ thực tế có thể xem ở đây. Tôi cũng muốn thử làm frontend Ghostty dưới dạng Godot extension
Đây là ví dụ cho thấy lập trình tốt rốt cuộc là phải đi theo cách mà hệ thống cung cấp. Dù bạn nghĩ gì về OOP hay quản lý bộ nhớ, nếu dùng GTK thì bằng cách nào đó bạn vẫn phải thiết kế interface với hệ thống kiểu GObject. Muốn tránh cũng không tránh được. Nhưng chúng tôi đã cố tránh, và kết quả là một mớ hỗn loạn khổng lồ khi ràng buộc vòng đời của các đối tượng có reference counting với các đối tượng không có. Trong ứng dụng Ghostty GTK, lỗi kiểu giải phóng bộ nhớ Zig thì bộ nhớ GTK không được giải phóng, hoặc ngược lại, cứ lặp đi lặp lại
Bỏ qua quan điểm của tôi về OOP và quản lý bộ nhớ, tôi đồng ý rằng nếu dùng GTK thì không thể tránh bị cuốn vào hệ thống kiểu GObject. Vì vậy tôi quyết định không dùng GTK trực tiếp ngay từ đầu. Tôi hiểu giá trị của một giao diện có theme thống nhất, nhưng theo tôi thì các ưu điểm của GTK không đủ lớn để đáng phải trả cái giá đó. Từ kinh nghiệm từng đụng vào vùng ngoại vi của GTK trong các ứng dụng mã nguồn mở, tôi tin chắc rằng quan điểm của GTK và GObject không hợp với cách làm của mình. Tôi không ghét việc GTK tồn tại. Tôi chọn không dùng nó và thế là ổn, nhưng điều kỳ lạ là có người lại không xem đó là quyền lựa chọn của tôi. Nó chỉ là một trong vô số GUI toolkit, và dù xét về kỹ thuật thì đây là một toolkit rất trau chuốt, tôi vẫn tự hỏi nếu GTK có thị phần thấp hơn một chút thì biết đâu sự polish đó đã được đổ vào những toolkit khác có cấu trúc tốt hơn. Tất nhiên, thứ tôi cho là tốt không có nghĩa là tốt với tất cả mọi người. Tôi tò mò không biết trong số những người dùng GTK, có bao nhiêu người dùng miễn cưỡng và bao nhiêu người thực sự thấy đó là toolkit tốt nhất
Một điều thú vị là trong Ghostty và một số ứng dụng GTK khác, khi chuột đi ra ngoài cửa sổ rồi quay lại, cú click cuộn đầu tiên sẽ bị bỏ qua. Nguyên nhân là một lỗi rất cũ, được báo từ tận năm 2015. Link lỗi. Đến giờ vẫn chưa có kế hoạch sửa, và maintainer thì có quan điểm là cứ chờ Wayland
Ở đoạn “đã dùng Valgrind để kiểm chứng mọi bước”, thật ra đây là điều quá hiển nhiên nhưng tôi chưa từng thực sự làm, và cũng hiếm thấy lập trình viên nào khác làm vậy. Thường thì Valgrind chỉ được dùng khi đã xuất hiện bug cụ thể hoặc suy giảm hiệu năng. Nếu chủ động dùng Valgrind trong suốt quá trình phát triển, nhất là Memcheck và Helgrind, có lẽ độ ổn định của công cụ sẽ tăng lên rất nhiều, và bug cũng có thể bị bắt ngay lúc được đưa vào, thay vì sau đó phải lần mò qua hàng trăm commit
Khi dùng Ghostty, việc không thể dán nhiều dòng vào nano trên Mac rất bất tiện. Có vẻ liên quan đến cách terminal xử lý “bracketed pasting”, nhưng lạ là iterm2 hay term thì không gặp vấn đề này
Tôi tự hỏi nếu dùng Rust thay vì Zig thì liệu có tránh được lỗi bộ nhớ không. Vì phần lớn vấn đề đều đến từ tương tác Zig/C nên có lẽ Rust cũng sẽ tương tự. Tôi đoán từ góc nhìn của một lập trình viên Go, và cũng tò mò không biết khi phải tích hợp quy mô lớn với C thì có ngôn ngữ nào cung cấp nhiều công cụ an toàn hơn không
Khi dùng các ứng dụng dựa trên GPU như Ghostty, Alacritty, WezTerm, Zed, v.v., tôi thấy chúng nhanh hơn và dễ chịu hơn. Nhưng trớ trêu là cũng chính các ứng dụng này làm lộ ra giới hạn của driver Nvidia rõ hơn nhiều. Trước đây tôi gần như không dùng GPU nên không nhận ra, nhưng cả trong môi trường không có compositor như Regolith i3wm lẫn môi trường sway/wayland, các vấn đề như chia sẻ màn hình, khôi phục sau sleep, crash, v.v. với driver nvidia đều quá tệ. Tôi đã thử nhiều phiên bản khác nhau (550/560/575/580) mà vẫn như nhau. Chỉ gần đây tôi mới nhận ra hóa ra từ xưa đến giờ nó đã tệ như vậy
Tôi đã từng làm được một ứng dụng lớn mà hệ thống kiểu của GTK hầu như không ảnh hưởng đến code. Nhưng bù lại, thay vì kế thừa hay mở rộng class, tôi nối mọi thành phần với nhau chỉ bằng cách bind lambda. Kết quả cuối cùng không quá bừa bộn, nhưng có lẽ những lập trình viên quen phong cách GTK chính thống sẽ thấy khá khó hiểu
Tôi không hiểu nổi sự quan tâm có phần quá mức dành cho Ghostty. Với một UI chỉ có tab và context menu, tôi nghi ngờ liệu có đáng để làm tất cả công việc tích hợp và viết lại như vậy không. Tôi đoán có lẽ họ định thêm cả một môi trường GUI mạnh như iterm2. Kitty thì tự vẽ tab bằng OpenGL nên có thể tùy biến hoàn toàn, đồng thời khỏi mất thời gian tích hợp với framework phức tạp để sớm triển khai các tính năng rất thực dụng như bọc kết quả lệnh cuối vào pager để hiển thị. Kitty cũng hỗ trợ remote tốt