Ghostty GTK 애플리케이션을 다시 작성했습니다
원문은 Mitchell Hashimoto님이 에 게재했습니다. 이 블로그 구독하기
우리는 방금 완료한 Ghostty GTK 애플리케이션 재작성을 통해 Zig에서 GObject 타입 시스템을 온전히 수용하고 모든 과정을 Valgrind로 검증했습니다. 그 결과 Linux와 BSD에서 더욱 풍부한 기능과 안정성, 유지보수성을 갖춘 Ghostty가 완성되었습니다.
이 과정에서 다룰 만한 흥미로운 기술적 주제는 여러 가지가 있지만, 여기서는 두 가지에 집중하고자 합니다. (1) Zig에서 GObject 타입 시스템과 연동하는 방법과 (2) Valgrind로 GTK 애플리케이션을 검증하고 Valgrind가 Zig 코드베이스에서 발견한 메모리 문제를 되돌아보는 것입니다.
배경
먼저 간단한 배경을 설명하겠습니다. Ghostty는 크로스 플랫폼(macOS, Linux, FreeBSD) 터미널 에뮬레이터입니다. Ghostty는 각 플랫폼마다 플랫폼 네이티브 애플리케이션 혹은 GUI 프레임워크를 사용한다는 점에서 다른 크로스 플랫폼 터미널 에뮬레이터와 차별화됩니다.1
macOS에서 Ghostty는 Xcode로 빌드된 수천 줄 규모의 Swift 애플리케이션입니다. Linux와 BSD에서 Ghostty는 수천 줄 규모의 GTK 애플리케이션으로, X11, Wayland 등에 대한 직접적인 통합을 활용합니다. 이 모든 것을 하나로 묶는 것은 C ABI 호환 API를 내보내는 Zig로 작성된 매우 큰 공유 코어입니다.
Ghostty가 이전에 왜 그런 구조였는지, 그리고 왜 지금 GTK 애플리케이션을 다시 작성하기로 결정했는지에 대한 전체 동기는 원래의 “gtk-ng” PR을 참고하시기 바랍니다. 이 글에서는 동기보다는 얻은 교훈에 집중하겠습니다.
GObject 타입 시스템과 Zig
OOP와 메모리 관리에 대해 어떤 생각을 갖고 있든, 현실은 GTK를 선택한다면 어떤 식으로든 GObject 타입 시스템과 연동해야 한다는 것입니다. 피할 수 없습니다.
물론 피할 수는 있습니다. 그리고 우리는 실제로 피했습니다. 그 결과 참조 카운트되지 않는 객체의 수명과 참조 카운트되는 객체의 수명을 묶으려다 엉망이 되었습니다. Ghostty GTK 애플리케이션에서는 기본적으로 “Zig 메모리는 해제됐지만 GTK 메모리는 해제되지 않았거나, 그 반대”로 요약할 수 있는 부류의 버그가 계속해서 발생했습니다.
정확성 문제 외에도, 객체 시스템을 회피하면서 signals(이벤트), properties(GUI 요소가 바인딩할 수 있는 속성), actions(원거리에서 단방향 동작을 호출하는 기능) 등 GTK 네이티브 기능도 사용하지 못하게 되었습니다.
구체적인 예를 들어보겠습니다. 바로 다시 불러올 수 있는 설정입니다. Ghostty의 설정은 Zig가 소유하는 Config 구조체로 표현됩니다. 창, 탭, 메뉴, 분할 등 GUI의 여러 부분이 설정을 인식해야 합니다.
설정을 다시 불러오는 작업은 복잡하고 (상대적으로) CPU를 많이 쓰며 오류가 발생하기 쉬웠습니다. 이전 Config를 해제하기 전에 전체 GUI가 업데이트되었는지 보장해야 했기 때문입니다.
이제 Zig의 Config 구조체는 참조 카운트되는 GhosttyConfig GObject로 감싸져 있습니다. 설정을 다시 불러올 때 우리는 프로퍼티를 덮어쓰고 GObject 프로퍼티 변경 알림 시스템이 애플리케이션 전체로 퍼져나가도록 둡니다(때로는 여러 이벤트 루프 틱에 걸쳐). 이전 설정에 대한 참조가 더 이상 없으면 해제됩니다. 개념적으로 훨씬 더 단순합니다.
메모리 관리 외에도, 이제 커스텀 GTK 위젯을 훨씬 더 쉽게 만들 수 있습니다. 이를 통해 Blueprint와 같은 최신 GTK UI 기술을 온전히 수용할 수 있었습니다. 예를 들어, 다음은 저희 터미널 창 Blueprint 파일입니다. 덕분에 새로운 GTK 타이틀바 탭 옵션이나 벨 울림 시 애니메이션 테두리 같은 GUI 기능을 훨씬 더 쉽게 도입할 수 있게 되었습니다.
GTK와 Zig에서의 Valgrind
이 주제만으로 별도의 블로그 글 하나를 쓸 만합니다. 요점은 첫 번째 PR부터 마지막 PR까지 모든 변경 사항과 Ghostty 기능을 Valgrind로 실행하며 메모리 누수, 정의되지 않은 메모리 접근 등이 없도록 모든 문제를 해결했다는 것입니다.
GTK 애플리케이션에서 Valgrind를 실행하는 것은 꽤 까다롭습니다. 꽤 큰 suppression 파일이 필요합니다. 양이 많다는 건 알지만, 그 파일의 80%는 GTK 자체에서 제공합니다. 나머지는 주로 서드파티 라이브러리와 GPU 드라이버에서 비롯됩니다. 한두 개 정도는 의심스럽다고 생각하는 suppression이 있을 텐데, 그렇게 주석도 달아두었습니다.
중요한 점은 그 과정에서 분명히 놓쳤을 버그들을 다수 찾아낼 수 있었다는 것입니다. 예를 들어, dispose 과정에서 GObject WeakRef를 비우지 않으면 미래의 어느 시점(몇 시간, 며칠 뒤가 될 수도 있습니다!)에 대상(참조된) 객체가 dispose될 때 정의되지 않은 메모리 접근을 일으킨다는 것을 알게 되었습니다. 그 정의되지 않은 메모리 접근은 99%의 경우 문제없이 넘어가지만, 가끔 크래시를 일으킵니다. 재미있죠! Valgrind는 이 문제를 아무 문제 없이 찾아냈습니다.
메모리 안전성은… 흠… 특정 논쟁을 촉발하는 것 같습니다. 그래서 두 가지를 말씀드리겠습니다:
우리 Zig 코드베이스에는 누수 하나와 정의되지 않은 메모리 접근 하나가 있었습니다. (좋은 의미로) 정말 놀라웠습니다. 우리 Zig 코드베이스는 크고 복잡하며, 쉽게 안전하지 않은 동작으로 이어질 수 있는 성능을 위한 수많은 메모리 트릭을 사용합니다. 솔직히 더 많은 문제가 있을 줄 알았습니다. 게다가 발견된 유일한 누수는 서드파티 C API를 호출하는 과정에서 발생한 것이라(Zig가 감지할 수 없었습니다) 이는 엄청난 성공입니다.
Zig는 Ghostty 프로젝트에서 디버그 및 테스트 빌드에서만 활성화되는 누수 감지 디버그 할당자와 다양한 안전 검사를 제공합니다. 또한 Zig는 Valgrind와 통합되어 있습니다. 예를 들어, Zig에서 값을
undefined(키워드)로 설정할 때마다 Zig는 해당 메모리를 undefined로 표시하라는 Valgrind 클라이언트 요청을 보냅니다. 이는 더 많은 문제를 찾는 데 도움이 됩니다.이번 경험은 릴리스 빌드에서 이러한 보호 기능이 없음에도 불구하고 이 메커니즘이 작동하고 있음을 정말 잘 보여주었습니다.
다른 모든 메모리 문제는 C API 경계에서 발생했습니다. 우리가 발견한 다른 모든 문제(수십 건에 달합니다)는 GObject 시스템의 복잡한 수명 주기 안에서나 C API 경계에서 직접 발생했습니다. 여기서 얻은 교훈은 C API를 넘나들어 안전하게 호출하려면(그 API가 C로 작성되지 않았더라도) Valgrind 같은 툴링이 반드시 필요하다는 것입니다.
대부분의 복잡한 라이브러리에서 C API는 객체 수명이 이전되거나 모호해지는 경계를 나타냅니다. 어떤 언어를 사용해 그와 연동하든, 보장되는 안전성은 API의 의미를 얼마나 잘 이해하고 좋은 래퍼를 작성하느냐에 달려 있습니다.
Zig가 제공하는 메모리 안전 관련 기능은 잘 문서화되어 있습니다. Zig가 무엇을 하고 무엇을 하지 않는지, 그리고 그것이 좋은지 나쁜지에 대해 학술적·이론적 논의가 많습니다. 그런 논의도 가치 있지만, 실증적 결과도 마찬가지입니다. 이 과정은 모든 개별 기능이 Valgrind의 엄격한 검증 하에 실행된 크고 복잡하며 멀티스레드, 멀티플랫폼인 Zig 프로젝트의 실증적 결과를 보여줍니다. 거기서 무엇을 얻어갈지는 여러분에게 맡기겠습니다. 논쟁을 시작하고 싶지는 않습니다!
앞으로도 모든 GTK PR을 Valgrind에서 실행하고, 유지관리자들과 기여자들이 함께 할 수 있도록 프로젝트 문서를 개선해 나갈 계획입니다(이미 몇 분이 실행하고 계십니다!).
결론
이로써 Ghostty의 GUI 부분을 처음부터 다시 작성한 것은 이번이 5번째입니다. GLFW로 한 번, macOS에서 SwiftUI로 한 번, 그다음 macOS에서 AppKit와 SwiftUI로 한 번, Linux에서 GTK를 절차적으로 한 번, 그리고 이제 Linux에서 GTK와 완전한 GObject 타입 시스템으로 한 번입니다.
매번 새롭고 가치 있는 것을 배웠고, 그 경험을 각 반복(그리고 플랫폼 간) 작업에 녹여냈습니다. 이번에도 macOS에 다시 가져갈 새로운 요령들을 배웠습니다.
전체 GTK 서브시스템 유지보수 팀이 재작성 완료를 돕기 위해 합류했다는 점도 강조하고 싶습니다. 그분들도 많은 작업을 해주셨습니다.
새롭게 재작성된 Ghostty GTK 애플리케이션은 이제 main에서 소스로부터 Ghostty를 빌드할 때 기본값이며, 몇 주 뒤에 출시될 1.2 릴리스에서 모든 사용자에게 제공될 예정입니다.
각주
“platform-native”라고 하면 Linux 사용자들은 정말 흥분하곤 합니다. Linux에는 그런 것이 존재하지 않지만, 합리적인 사람이라면 GTK 앱(또는 Qt) 같은 것이 다른 애플리케이션보다 대부분의 데스크톱에서 “네이티브”처럼 느껴진다는 데 동의합니다. ↩
글을 무작위로 읽기
댓글
로그인하고 댓글 남기기