원문 캡처 · spurint.org
원문 캡처 · spurint.org

C, Type Systems, GLib, and API Design

xfce4-session에서 특정 상황에만 발생하던 세그폴트의 원인은 GQueue의 목록 머리 노드를 가져오는 API를 잘못 선택한 데 있었다. 이 사례를 바탕으로 글쓴이는 C의 약한 타입 시스템과 제네릭 부재, GLib의 일관되지 않은 API 이름이 실수를 찾기 어렵게 했다고 지적한다.

GQueue에서 g_queue_peek_head()는 목록 노드가 아니라 큐 첫 항목에 저장된 데이터 포인터를 반환한다. 목록 순회를 시작할 노드가 필요했다면 g_queue_peek_head_link()를 사용해야 했다. 글쓴이는 g_queue_peek_head()의 반환값인 gpointer를 GList *에 대입했지만, C에서는 void *를 다른 포인터 유형에 경고나 형변환 없이 대입할 수 있어 컴파일러가 오류를 알리지 않았다.

문제는 xfce4-session을 재시작하는 xfwl4의 코드를 시험하던 중 드러났다. 프로세스를 직접 종료하자 재시작된 세션이 다시 충돌했지만, Valgrind와 GCC 주소 검사기, GDB 감시점을 사용하면 재현되지 않았다. 반복 실행에서도 충돌하지 않아 글쓴이는 타이밍에 따라 달라지는 문제로 보고 코어 덤프와 코드를 살폈다. GLM 5.3 Flash와 문제를 살펴본 뒤, 다음 날 비-Flash 버전으로 새 대화를 시작해 이전 조사 내용을 전달했고, 모델은 약 5분 만에 잘못된 API 사용을 짚었다고 글쓴이는 설명한다.

글쓴이는 실수의 책임이 자신에게도 있다고 인정하면서, C의 약한 타입 검사와 제네릭 부재가 타입 혼동을 막지 못했다고 말한다. GLib의 이름에도 혼란을 겪었다. GList에는 노드를 반환하는 g_list_nth()와 데이터를 반환하는 g_list_nth_data()가 있지만, GQueue에서는 노드 포인터를 반환하는 함수가 g_queue_peek_head_link()이고 데이터 포인터를 반환하는 함수가 g_queue_peek_head()다. 글쓴이는 GQueue 쪽 이름이 더 낫다고 생각하면서도 두 자료구조 사이의 일관성이 더 중요하다고 본다.

글쓴이는 강한 타입 시스템과 제네릭을 갖춘 Rust에서는 이와 같은 타입 혼동이 사실상 불가능할 정도로 줄어든다고 평가한다. C의 _Generic도 알고 있지만, 미리 사용할 유형을 모두 선언해야 하므로 라이브러리가 임의의 사용자 유형을 지원하는 진정한 제네릭 자료형을 만드는 데는 적합하지 않다고 덧붙인다.

관련 문서: GQueue, GList, g_queue_peek_head(), g_queue_peek_head_link(), g_list_nth(), g_list_nth_data(), C의 _Generic, g_queue_peek_nth(), g_queue_peek_nth_link().