FoundationsofGTKDevelopment:Chapter 06
- 제 6 장 GLib 이용하기
GLib 이용하기
이제 GTK+를 비롯해 여러 단순한 위젯에 대해서는 충분히 이해했으니 또 다른 라이브러리를 학습할 때가 되었다. GTK+는 GLib라는 다용도의 라이브러리에 의존하는데, 이는 수많은 종류의 유틸리티 함수, 데이터 유형, 래퍼 함수를 제공한다. 사실 앞의 여러 장에서 GLib의 여러 측면들을 이미 사용한 바 있다.
GLib는 다른 라이브러리와 독립적으로 실행이 가능하여 이번 장에 소개되는 일부 예제들은 GTK+, GDK, Pango 라이브러리를 필요로 하지 않는다. 하지만 GTK+는 GLib에 의존하는 것이 사실이다.
이번 장에서 소개하는 주제를 모두 본문 뒷부분에서 사용하지는 않지만 실제 세계에서는 많은 GTK+ 애플리케이션에 유용하게 사용된다. 주제들 중 다수는 매우 구체적인 작업에 사용된다. 가령 GModule은 개발자의 애플리케이션에 플러그인 시스템을 생성하거나 바이너리 기호 테이블을 여는 데에 사용할 수 있다.
제 6장의 목표는 GLib에 관한 모든 것을 포괄적으로 소개하려는 것이 아니다. 이번 장에 표시된 기능을 이용할 때는 GLib의 API 문서에서 더 많은 정보를 참고해야 한다. 이번 장에서는 개발자로 하여금 GLib가 무엇을 제공하는지 일반적으로 이해하도록 주요 기능들을 소개하는 데에 목적을 둔다.
이번 장에서 학습할 내용은 다음과 같다.
- GLib가 제공하는 기본 데이터 유형, 매크로, 유틸리티 함수
- 개발자의 애플리케이션 내에서 발생한 오류와 경고에 대해 텍스트로 된 피드백을 사용자에게 제공하는 방법
- 메모리 슬라이스, g_malloc(), friends와 같이 GLib가 제공하는 메모리 관리 방식
- 타이밍, 파일 조작, 디렉터리 내용 읽기, 파일 시스템으로 작업하기 등 GLib가 제공하는 다양한 유틸리티 함수
- 메인 루프를 GLib에서 구현하는 방식과 그것이 timeout 및 idle 함수를 구현하는 방식
- 문자열, 연결 리스트, 바이너리 트리, 배열, 해시 테이블, 쿼크, 키가 있는(keyed) 데이터 리스트, n-ary 트리를 포함해 GLib가 제공하는 데이터 구조체
- 파일을 조작하는 파이프를 생성하는 데에 GIOChannel을 사용하는 방법과 비동기식 및 동기식 프로세스를 띄우는 방법
- GModule을 이용해 공유 라이브러리를 동적으로 로딩하는 방법
GLib 기본 내용
GLib는 다용도의 유틸리티 라이브러리로 많은 유용한 비그래픽 기능의 구현에 사용된다. GTK+에서는 GLib를 필요로 하지만 GLib 는 독립적으로 사용이 가능하다. 이처럼 GLib가 제공하는 많은 기능들 덕에 일부 애플리케이션은 GTK+ 를 비롯해 다른 지원하는 라이브러리 없이 GLib만 사용한다.
GLib의 사용 시 주요 장점 중 하나는 코드를 거의 재작성하지 않고도 지원하는 운영체제라면 어디서든 개발자의 코드를 실행할 수 있는 크로스 플랫폼적 인터페이스를 제공한다는 점이다. 이는 본 장의 나머지 부분에 제시된 예제에서 설명할 것이다.
기본 데이터 유형
앞의 여러 장에 걸쳐 개발자는 GLib에서 비롯된 많은 데이터 유형을 사용하였을 것이다. 이러한 데이터 유형은 다른 플랫폼으로 이식 가능한 공통된 데이터 유형 집합 뿐만 아니라 GTK+를 래핑하는 다른 프로그래밍 언어도 제공한다.
표 6-1은 GLib가 제공하는 기본 데이터 유형의 목록이다. 타입 정의는 gtypes.h 헤더 파일에서 모두 찾아볼 수 있다. 좀 더 복잡한 데이터 구조체는 후에 "데이터 유형" 절에서 다루겠다.
타입 | 설명 |
gboolean | C는 Boolean 데이터 유형을 제공하지 않으므로 GLib는 gboolean을 제공하며, 이는 TRUE 와 FALSE 중 하나로 설정된다. |
gchar(guchar) | 표준 C 문자 타입에 따라 부호가 있는 데이터 유형과 부호가 없는 데이터 유형. |
gconstpointer | 타입이 정해지지 않은 일정한(constant) 데이터를 가리키는 포인터. 이 데이터가 가리키는 데이터는 변경되어선 안 된다. 일반적으로는 함수가 그것이 가리키는 데이터를 변경하지 않을 것임을 나타내기 위해 함수 프로토타입에서 사용된다. |
gdouble | 표준 C double 타입에 상응하는 데이터 유형. -G_MAXDOUBLE과 G_MAXDOUBLE 사이의 값이 가능하다. G_MINDOUBLE은 gdouble이 보유할 수 있는 양의 최소값을 나타낸다. |
gfloat | 표준 C float 타입에 상응하는 데이터 유형. -G_MAXFLOAT와 G_MAXFLOAT 사이의 값이 가능하다. G_MINFLOAT는 gfloat이 보유할 수 있는 양의 최소값을 나타낸다. |
gint(guint) | 표준 C int 타입에 상응하는 부호가 있는 데이터 유형과 부호가 없는 데이터 유형. 부호가 있는 gint 값은 G_MININT와 G_MAXINT 사이의 값이어야 한다. 최대 guint 값은 G_MAXUINT에 의해 제공된다. |
gint8(guint8) | 부호가 있는 정수와 부호가 없는 정수로, 모든 플랫폼에서 8 비트로 설계된다. 부호가 있는 값은 -128과 127 (G_MININT8과 G_MAXINT8) 사이 값이고, 부호가 없는 값은 0과 255(G_MAXUINT8) 사이 값이어야 한다. |
gint16(guint16) | 부호가 있는 정수와 부호가 없는 정수로, 모든 플랫폼에서 16 비트로 설계된다. 부호가 있는 값은 -32,768과 32,767 (G_MININT16과 G_MAXINT16) 사이 값이고, 부호가 없는 값은 0과 65,535(G_MAXUINT16) 사이 값이어야 한다. |
gint32(guint32) | 부호가 있는 정수와 부호가 없는 정수로, 모든 플랫폼에서 32 비트로 설계된다. 부호가 있는 값은 -2,147,483,648과 2,147,483,647 (G_MININT32와 G_MAXINT32) 사이 값이고, 부호가 없는 값은 0과 4,294,967,295(G_MAXUINT16) 사이 값이어야 한다. |
gint64(guint64) | 부호가 있는 정수와 부호가 없는 정수로, 모든 플랫폼에서 64 비트로 설계된다. 부호가 있는 값은 -263과 263-1 (G_MININT64와 G_MAXINT32) 사이 값이고, 부호가 없는 값은 0과 264-1(G_MAXUINT64) 사이 값이어야 한다. |
glong(gulong) | 표준 C long 타입에 상응하는 부호가 있는 데이터 유형과 부호가 없는 데이터 유형. 부호가 있는 glong 값은 G_MINLONG과 G_MAXLONG 사이의 값이어야 한다. 최대 gulong 값은 G_MAXULONG에 의해 제공된다. |
gpointer | void*로 정의되는 일반적 타입이 정해지지 않은 포인터. 표준 void* 타입보다 단순히 더 매력적으로 보이는 것이 목적이다. |
gshort(gushort) | 표준 C short 타입에 상응하는 부호가 있는 데이터 유형과 부호가 없는 데이터 유형. 부호가 있는 gshort 값은 G_MINSHORT와 G_MAXSHORT 사이의 값이어야 한다. 최대 gushort 값은 G_MAXUSHORT에 의해 제공된다. |
gsize(gssize) | 부호가 없는 32-비트 정수와 부호가 있는 32-비트 정수로, 크기를 나타내기 위해 많은 데이터 구조체에서 사용된다. gsize 데이터 유형은 unsigned int로 정의되고 gssize는 signed int로 정의된다. |
표 6-1. GLib 데이터 유형 |
이전에는 G_HAVE_GINT64 매크로를 이용해 gint64와 guint64가 플랫폼에서 지원되었는지 확인할 수가 있었다. 하지만 GLib 2.0 버전의 배포판부터 64-비트 정수가 요구되었기 때문에 두 가지 데이터 유형은 물론이고 매크로 또한 항시 정의된다. 두 가지 데이터 유형은 아래와 같은 정의를 갖는다.
G_GNUC_EXTENSION typedef signed long long gint64;
G_GNUC_EXTENSION typedef unsigned long long guint64;
-pedantic과 같은 일부 옵션은 GNU C에서 확장(extensions)에 대해 경고하기도 한다. 표현식 앞에 __extension__를 입력하면 이러한 일을 방지할 수 있다. 따라서 G_GNUC_EXTENSION은 __extension__와 동일하다.
GLib는 G_GINT64_CONSTANT()와 G_GUINT64_CONSTANT()를 제공하는데, 이는 64-비트 리터럴을 소스 코드로 삽입하는 데에 사용된다. 가령 G_MAXINT64는 G_GINT64_CONSTANT(0x7fffffffffffffff)로 정의된다.
표준 매크로
기본 데이터 유형 외에 GLib는 개발자가 애플리케이션에 걸쳐 사용할 수 있는 사전 정의된 값과 표준 매크로를 다수 제공한다. 대부분의 애플리케이션에서는 모든 매크로를 광범위하게 사용하진 않겠지만 개발자의 삶을 수월하게 만드는 것이 목적이다. 가령 GLib 버전과 다양한 타입 규칙을 확인하는 매크로들이 있다.
때때로 특정 기능의 컴파일 유무를 결정하기 위해 사용자의 GLib 버전을 확인하길 원하는 경우가 있다. GLib는 표 6-2에 표시된 바와 같이 컴파일 시간과 런타임 시 사용하는 버전 정보를 제공한다.
값 | 설명 |
GLIB_MAJOR_VERSION | 포함된 GLib 헤더의 major 버전. 개발자가 연결한 라이브러리의 major 버전을 얻으려면 glib_major_version을 이용하면 된다. GLib 2.12.1 버전에서 "2"는 major 버전을 나타낸다. |
GLIB_MINOR_VERSION | 포함된 GLib 헤더의 minor 버전. 개발자가 연결한 라이브러리의 minor 버전을 얻으려면 glib_minor_version을 이용하면 된다. GLib 2.12.1 버전에서 "12"는 minor 버전을 나타낸다. |
GLIB_MINOR_VERSION | 포함된 GLib 헤더의 micro 버전. 개발자가 연결한 라이브러리의 micro 버전을 얻으려면 glib_micro_version을 이용하면 된다. GLib 2.12.1 버전에서 "1"은 micro 버전을 나타낸다. |
GLIB_CHECK_VERSION (major, minor, micro) |
개발자가 사용 중인 GLib 헤더 파일의 버전이 명시된 것과 동일하거나 명시된 것보다 최신 버전일 경우 TRUE를 리턴한다. 특정 기능을 컴파일할 때 사용자가 GLib의 호환 버전을 갖고 있도록 확보하는 데에 사용할 수 있다. |
표 6-2. GLib 버전 정보 |
표 6-2에 제시된 버전 정보 외에 glib_check_version()을 이용하면 런타임 시 현재 사용 중인 GLib의 버전을 확인할 수 있다. 라이브러리가 호환되면 해당 함수는 NULL을 리턴하고, 그렇지 않으면 비호환성에 관한 정보를 더 제시하는 문자열을 리턴한다. 해당 함수를 이용하면 런타임 버전이 GLib와 동일한 버전이거나 더 최신 버전이 되도록 확보한다.
const gchar* glib_check_version (guint major,
guint minor,
guint micro);
GLib는 수치 연산, 타입 변환, 메모리 참조부터 시작해 단순히 Boolean 값을 TRUE와 FALSE로 정의하기까지 모든 일을 수행하는 매크로를 추가로 제공하기도 한다. 이 중 유용한 매크로를 표 6-3에서 찾을 수 있다.
매크로 | 설명 |
ABS (a) | 인자 a의 절대값을 리턴한다. 음수일 경우 음수 부호 없이 리턴하고 양수는 어떤 일도 수행하지 않는다. |
CLAMP (a, low, high) | a가 low와 high 사이에 있도록 보장한다. a가 low와 high 사이에 있지 않은 경우 리턴된 값은 2에 가장 가까울 것이다. 그 외의 경우 리턴된 값은 변경되지 않은 채 남겨질 것이다. |
G_DIR_SEPARATOR G_DIR_SEPARATOR_S |
UNIX 머신에서 디렉터리는 슬래시(/)로 구분되고 윈도우 머신에서는 백슬래시(\)로 구분된다. G_DIR_SEPARATOR는 적절한 구분자를 문자로 리턴하고 G_DIR_SEPARATOR_S는 구분자를 문자열로 리턴할 것이다. |
GINT_TO_POINTER (i) GPOINTER_TO_INT (p) |
정수를 gpointer로, 또는 gpointer를 정수로 변환한다. 32 비트 정수만 저장될 것이기 때문에 해당 매크로를 사용할 때는 32 비트의 공간 이상을 차지하는 정수의 사용은 피해야 한다. 포인터를 정수에 저장할 수는 없음을 기억하라. 만일 정수로 저장 시 포인터로만 저장할 수 있을 것이다. |
GSIZE_TO_POINTER (s) GPOINTER_TO_SIZE (p) |
gsize 값을 gpointer로, 또는 gpointer를 gsize 값으로 변환한다. gsize 데이터 유형은 다시 본래대로 변환하려면 GSIZE_TO_POINTER()를 이용해 포인터로 저장되었을 것이다. GINT_TO_POINTER()에서 더 많은 정보를 참조한다. |
GUINT_TO_POINTER (u) GPOINTER_TO_UINT (p) |
부호가 없는 정수를 gpointer로, 또는 gpointer를 부호가 없는 정수로 변환한다. 다시 본래대로 변환하려면 GUINT_TO_POINTER()를 이용해 포인터로 저장될 것이다. GINT_TO_POINTER()에서 더 많은 정보를 참조한다. |
G_OS_WIN32 G_OS_BEOS G_OS_UNIX |
이 세 개의 매크로는 특정 플랫폼에서만 실행될 코드를 정의하도록 해준다. 사용자의 시스템에 해당하는 매크로만 정의될 것이기 때문에 #ifdef G_OS_*를 이용해 사용자의 운영체제에 특정적인 코드를 괄호로 묶을 수 있다. |
G_STRUCT_MEMBER (type, struct_p, offset) |
명시된 offset에 위치한 구조체의 member를 리턴한다. 해당 offset은 struct_p 내에 있어야 한다. type은 개발자가 검색 중인 필드의 데이터 유형을 정의한다. |
G_STRUCT_MEMBER_P (struct_p, offset) |
명시된 offset에 위치한 구조체의 member를 향하는 타입이 정해지지 않은 포인터를 리턴한다. offset은 struct_p 내에 있어야 한다. |
G_STRUCT_OFFSET (type, member) |
구조체 내에 member의 바이트 오프셋을 리턴한다. 구조체 타입은 type에 의해 정의된다. |
MIN (a, b) MAX (a, b) |
두 개의 인자 a와 b의 최소 및 최대값을 각각 계산한다. |
TRUE 와 FALSE | FALSE는 0으로 정의되고, FALSE를 제외한 논리에는 TRUE가 설정된다. 이러한 값은 gboolean 타입에 사용된다. |
표 6-3. 표준 GLib 매크로 |
GLib은 표준 수학 단위에 다수의 매크로를 제공하기도 하는데, 어떤 경우에는 그 정밀도가 소수점 50째 자리까지 해당하기도 한다. GLib 2.12에 포함된 매크로는 다음과 같다.
- G_E: 정밀도가 소수점 49째 자리인 자연 로그의 밑
- G_LN2: 정밀도가 소수점 50째 자리인 2의 자연 로그
- G_LN10: 정밀도가 소수점 49째 자리인 10의 자연 로그
- G_PI: 정밀도가 소수점 49째 자리인 파이(pi) 값
- G_PI_2: 정밀도가 소수점 49째 자리로, 파이를 2로 나눈 값
- G_PI_4: 정밀도가 소수점 50째 자리로, 파이를 4로 나눈 값
- G_SORT2: 정밀도가 소수점 49째 자리인 2의 제곱근
- G_LOG_2_BASE_10: 정밀도가 소수 20째 자리로, 밑이 10인 2의 로그
메시지 로깅
이번 장을 비롯해 앞으로 살펴볼 여러 장에서는 텍스트 오류, 정보, 경고를 사용자에게 보고할 방법이 필요할 것이다. 이 모든 메시지에 g_point()를 사용해도 되지만 GLib는 몇 가지 유용한 기능과 함께 로깅 시스템을 제공한다.
g_log()를 이용하면 어떤 타입의 텍스트 메시지든 전달할 수 있다. 해당 함수의 첫 번째 매개변수는 커스텀 로그 도메인을 정의하도록 해준다. 로그 도메인은 사용자가 다른 라이브러리들이 출력한 메시지로부터 개발자의 애플리케이션이 출력한 메시지를 구별하도록 도와주는 문자열로, GLogFunc로 전달된다.
void g_log (const gchar *log_domain,
GLogLevelFlags log_level,
const gchar *message,
...);
라이브러리를 생성하는 것이 아니라면 G_LOG_DOMAIN을 도메인으로 이용해야 한다. 로그 도메인 매개변수에 특정적인 텍스트라면 모두 출력되기 전에 메시지 시작 부분에 추가될 것이다. 로그 도메인을 명시하지 않으면 G_LOG_DOMAIN이 사용될 것이다. 가령 GTK+ 라이브러리는 "Gtk"를 도메인으로 명시하여 메시지가 어디서 발생하였는지 사용자가 알 수 있도록 한다.
g_log()의 두 번째 매개변수는 어떤 타입의 메시지가 보고되는지 명시하도록 해준다. 예를 들어, 애플리케이션을 종료해야 하는 오류 메시지를 보고하고 있다면 G_LOG_LEVEL_ERROR를 사용해야 한다. GLogLevelFlags의 리스트는 다음과 같다.
- G_LOG_FLAG_RECURSION: 재귀적 메시지에 사용되는 플래그.
- G_LOG_FLAG_FATAL: 이 플래그로 설정된 로그 수준은 호출 시 애플리케이션의 종료를 야기하고 코어(core)의 덤핑을 야기한다.
- G_LOG_LEVEL_ERROR: 언제나 치명적인 오류 타입.
- G_LOG_LEVEL_CRITICAL: 경고보다 중요한 비치명적 오류지만 애플리케이션이 종료될 필요는 없다.
- G_LOG_LEVEL_WARNING: 애플리케이션을 중단을 야기하지 않는 것에 대한 경고.
- G_LOG_LEVEL_MESSAGE: 중요하지 않은 일반 메시지의 로깅에 사용.
- G_LOG_LEVEL_INFO: 다른 수준에서 다루지 않는 메시지 타입들로, 일반 정보를 예로 들 수 있다.
- G_LOG_LEVEL_DEBUG: 디버깅 목적에 사용되는 일반 메시지.
- G_LOG_LEVEL_MASK: (G_LOG_FLAG_RECURSIONㅣG_LOG_FLAG_FATAL)과 동일.
예를 들어 g_malloc()은 메모리 할당이 실패하면 애플리케이션을 종료하는데, G_LOG_LEVEL_ERROR가 사용되기 때문이다. 반면 g_try_malloc()을 이용 시 할당이 실패하면 어떤 메시지도 출력하지 않을 것이다. 대신 NULL 포인터를 리턴한다.
g_log()에 보고되는 실제 오류 메시지는 g_print()로 보고된 포맷과 동일한 포맷이어야 한다.
편의상 GLib는 g_log()의 도메인 및 플래그 매개변수를 건너뛰도록 5개의 함수를 제공하기도 한다. 이러한 함수들이 제공하는 메시지 또한 g_print()와 동일한 방식으로 포맷팅되어야 한다.
이러한 함수들은 명시된 로그 플래그와 직접적으로 부합하며, G_LOG_DOMAIN 도메인에서 발생할 것이다. 함수와 그에 연관된 로그 플래그는 다음과 같다.
- void g_message (...); /* G_LOG_LEVEL_MESSAGE */
- void g_warning (...); /* G_LOG_LEVEL_WARNING */
- void g_critical (...); /* G_LOG_LEVEL_CRITICAL */
- void g_error (...); /* G_LOG_LEVEL_ERROR */
- void g_debug (...); /* G_LOG_LEVEL_DEBUG */
마지막으로, 개발자의 애플리케이션이 메시지를 처리하는 방식에 따라 다른 메시지 타입을 치명적(fatal)인 것으로 만들길 원하는 경우도 있다. 기본적으로는 G_LOG_LEVEL_ERROR 플래그만 애플리케이션을 종료시킬 것이다. 어떤 일이 일어나든 이 수준은 항상 치명적이다.
다른 메시지 타입을 치명적으로 만들기 위해서는 g_log_set_always_fatal()을 호출해야 한다. 이는 G_LOG_FLAG_FATAL 플래그를 명시된 수준과 연관시킬 것이다.
g_log_set_always_fatal (G_LOG_LEVEL_DEBUG | G_LOG_LEVEL_WARNING);
예를 들어, 앞의 명령 예제는 개발자가 사용자에게 디버깅 및 경고 메시지를 보고하면 강제로 애플리케이션을 종료할 것이다. 이러한 기능은 부득이한 경우에만 사용되어야 하는데, 모든 오류 또는 경고가 애플리케이션을 종료시키지는 않기 때문이다!
메모리 관리
메모리 관리는 어떤 애플리케이션에서든 극히 중요한 측면이며, 애플리케이션의 크기와 복잡성이 증가하면서 점점 더 중요해지기 때문이다. GLib는 메모리 관리를 위한 수많은 함수를 제공하지만 이번 절에서는 가장 자주 사용되는 함수만 몇 가지 다루도록 하겠다.
메모리 슬라이스
GLib 2.10 버전 이전에는 메모리 조각의 할당 시 메모리 할당자(memory allocator)와 메모리 청크를 이용했다. 하지만 최신 버전에는 메모리 슬라이스라고 불리는 훨씬 더 효율적인 방법이 소개되었다. 따라서 이번 절에서는 메모리 슬라이스라는 할당자 타입만 다루도록 하겠다. 어떤 연유에서든 GLib의 오래된 버전을 사용하고 있다면 API 문서에서 GMemChunk를 확인하도록 한다.
메모리 슬라이스를 이용 시 장점은 과도한 메모리 낭비를 피하고, 메모리 청크에 해가 되는 확장성(scalability) 및 성능 문제를 수정한다는 데에 있다. 이는 슬랩(slab) 할당을 이용하여 이루어진다.
메모리 슬라이스는 메모리를 매우 효율적으로 동일한 크기의 청크로 할당한다. 즉, 메모리 슬라이스를 이용하면 각 객체를 2개의 포인터 또는 동일한 크기로 된 다수의 객체만큼 작게 할당할 수 있다는 의미다.
메모리의 slab 할당
슬랩 할당자는 본래 Sun Microsystems의 Jeff Bonwick가 설계하였다. 이는 처음에 요청한 것보다 큰 메모리 블록을 할당하는 시스템에서 야기되는 내부 메모리의 단편화 문제를 줄이도록 도와주는 메모리 관리 방식이다.
슬랩 할당을 이해하려면 슬랩과 캐시의 의미를 문맥에서 이해할 필요가 있다. 슬랩은 하나의 메모리 할당을 나타내는 하나의 인접한 메모리 청크다. 캐시는 매우 효율적인 메모리 청크로, 하나의 데이터 유형만 보유하는 데에 사용된다. 각 캐시는 하나 또는 이상의 슬랩으로 구성된다.
각 객체는 처음에 free로 표시되는데, 이는 슬랩이 비어 있음을 의미한다. 프로세스가 커널로부터 새 객체를 요청하면 시스템은 부분적으로 채워진 슬랩에서 위치를 찾고자 시도할 것인데, 이는 객체를 위치시키는 데에 사용될 것이다. 객체에 들어맞는 부분적 슬랩을 찾을 수 없는 경우 인접한 물리적 메모리로부터 새로운 슬랩이 할당되고 해당 슬랩은 캐시로 추가된다. 슬랩이 가득 차면 사용된 것으로 표시된다.
슬랩할당에는 많은 장점이 있지만 요청된 메모리 할당 크기가 실제 할당 크기와 동일하다는 사실이 주요 장점이 된다. 따라서 메모리의 단편화를 피하고 매우 효율적인 할당이 가능하다. 더 많은 정보는 온라인에서 슬랩 할당자에 관한 Jeff Bonwick의 논문에서 찾을 수 있다.
큰 메모리 블록들을 할당해야 한다면 시스템의 malloc() 구현이 자동으로 사용될 것이다. g_malloc()과 그에 관련된 함수의 사용은 다음 절에서 간략하게 살펴볼 것이지만, 메모리 할당 후 객체의 크기 조정을 계획하지 않는 한 새 코드에서는 메모리 할당에 메모리 슬라이스를 사용해야 한다. 단, 메모리 슬라이스를 이용 시 메모리가 할당될 때와 해제될 때 객체의 크기가 동일해야 한다는 단점이 있다.
슬라이스 할당자를 이용하는 방법에는 두 가지가 있는데, 두 개의 포인터보다 큰 크기의 단일 객체를 할당하는 방법과, 동일한 크기로 된 다수의 객체를 할당하는 방법이 그것이다. 리스팅 6-1에 실린 코드는 다수의 객체를 어떻게 할당하는지를 보여주는데, 이는 슬라이스 할당을 이용해 100개의 객체를 할당한 후 해제한다.
리스팅 6-1. 다수의 객체 할당하기
#define SLICE_SIZE 10
gchar *strings[100];
gint i;
for (i = 0; i < 100; i++)
strings[i] = g_slice_alloc (SLICE_SIZE);
/* ... Use the strings in some way ... */
/* Free all of the memory after you are done using it. */
for (i = 0; i < 100; i++)
g_slice_free1 (SLICE_SIZE, strings[i]);
리스팅 6-1에서 g_slice_alloc()을 이용해 SLICE_SIZE 길이로 된 100개 문자열을 할당하였다. 슬라이스 할당은 매우 간단하여, 슬라이스에 해당해야 하는 메모리 크기를 제공하기만 하면 된다. malloc()과 비슷하게 해당 함수는 형변환되는 객체 대신 메모리로 gpointer를 리턴한다.
내부적으로 GLib는 슬랩 할당을 이용할 것인지 아니면 메모리 할당을 g_malloc()으로 위임할 것인지를 결정한다. 메모리 할당은 원하는 메모리 슬라이스가 매우 클 경우 g_malloc()에 의해 실행된다. GLib는 리턴된 메모리 청크를 0으로 초기화하는 g_slice_alloc0()을 제공하기도 한다.
메모리 슬라이스는 런타임 시 현재 상황에 가장 효율적인 메모리 할당 방식을 선택할 것인데, 슬랩 할당인 g_malloc(), 아니면 다른 방식을 선택한다. 하지만 G_SLIZE 환경 변수를 always-malloc으로 설정하면 강제로 g_malloc()을 선택하도록 할 수 있다.
메모리의 사용이 끝나면 g_slice_free1()을 이용해 해제해야만 애플리케이션의 다른 부분에서 사용할 수 있다. 해당 함수는 strins[i]에 위치한 SLICE_SIZE 크기의 메모리 블록을 해제한다.
g_slice_free1 (SLICE_SIZE, strings[i]);
내부적으로 메모리는 그에 할당된 것과 동일한 방식을 이용해 해제될 것이다. 따라서 해당 함수를 사용하려면 메모리를 g_slice_alloc() 또는 g_slice_alloc0()을 이용해 할당해야 한다.
객체의 단일 인스턴스만 할당해도 되는 경우라면 g_slice_new()를 이용할 수 있다. 하나의 객체를 할당하는 데에 이 함수를 사용하는 예제를 리스팅 6-2에 소개하겠다.
리스팅 6-2. 단일 객체 할당하기
typedef struct
{
GtkWidget *window;
GtkWidget *label;
} Widgets;
Widgets *w = g_slice_new (Widgets);
/* Use the structure just as you would any other structure. */
w->window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
w->label = gtk_label_new ("I belong to widgets!");
/* Free the block of memory of size "Widgets" so it can be reused. */
g_slice_free (Widgets, w);
리스팅 6-1에 제시된 방법 대신 슬라이스 할당을 이용해 메모리의 단일 블록을 할당해야 하는 경우 g_slice_new()를 호출하면 된다. 해당 함수는 아래와 같이 정의되며, g_slice_alloc()이 리턴한 값을 원하는 타입으로 형변환한다.
#define g_slice_new(type) ((type*) g_slice_alloc (sizeof (type))
GLib는 g_slice_new() 외에도 g_slice_alloc0()을 이용해 리턴된 슬라이스를 0으로 초기화하는 g_slice_new0()를 제공하기도 한다.
메모리 작업이 끝나면 이를 해제해야 한다. 리스팅 6-2에서 메모리 한 조각만 할당했기 때문에 g_slice_free()를 이용해 w 위치와 Widgets 크기로 된 메모리 한 조각을 해제하였다.
메모리 할당
GLib는 표준 C 라이브러리가 제공하는 기능을 래핑하는 다수의 함수들을 제공한다. 이러한 함수들 중 몇 가지를 이번 절에서 설명하고자 한다.
이후 호출이 성공적이었음을 확인할 필요는 없다는 사실을 기억하는 것이 중요하다. 메모리를 할당하는 호출이 하나라도 실패하면 애플리케이션은 자동으로 GLib에 의해 종료될 것이며, 메시지는 표준 오류로 출력되어 오류가 발생하였음을 표시할 것이다.
하나 또는 그 이상의 새 구조체를 할당하려면 g_new()를 이용해야 한다. 이 함수는 할당할 구조체의 개수와 데이터 유형을 수신한다. 이후 새 메모리에 대한 포인터를 리턴한다.
struct_type* g_new (struct_type, number_of_structs);
리턴된 데이터는 이미 올바른 타입으로 형변환되므로 객체를 다시 형변환할 필요는 없다. 초기화할 구조체를 모두 기본적으로 0으로 설정하길 원한다면 대신 g_new0()을 이용하면 된다.
대부분 C 프로그래머에게 익숙한 방법은 malloc()을 이용하는 것이다. GLib는 이 함수의 래핑된 버전인 g_malloc()을 제공한다. 이 함수는 할당할 바이트 수를 수신하고, 할당된 메모리에 대한 포인터를 리턴한다.
gpointer g_malloc (gulong number_of_bytes);
할당할 메모리 바이트 수를 가장 쉽게 계산하는 방법은 데이터 유형에서 sizeof() 함수를 이용하는 것이다. 리턴된 객체는 자동으로 형변환되지 않기 때문에 대부분의 경우 개발자는 즉시 형변환을 처리하길 원할 것이다. 새로 할당된 메모리를 0의 값으로 초기화하길 원할 경우를 대비해 g_malloc0() 함수 또한 제공된다.
g_malloc()을 이용한 메모리 할당이 실패하면 애플리케이션은 취소될 것이다. 그 때 g_try_malloc()을 이용하면 메모리 할당이 실패하더라도 애플리케이션이 실패하는 대신 NULL을 리턴할 것이다. 이는 성공하지 않은 메모리 할당에서 애플리케이션이 복구될 수 있을 때에만 사용되어야 한다. g_try_malloc()을 이용할 때에는 NULL case를 처리하는 것이 중요하다.
gpointer g_try_malloc (gulong number_of_bytes);
메모리 조각의 사용이 끝나면 다시 사용할 수 있도록 항상 해제시켜야 한다. 해제시키지 않으면 애플리케이션에 메모리 누수를 야기하는데, 이는 결코 좋은 징조가 되지 못한다. 메모리 조각을 해제하려면 g_free()를 호출할 수 있다. 이는 GTK+ API에서 이용 가능한 많은 함수에서 리턴된 문자열을 해제하는 데에 필요하다.
void g_free (gpointer memory);
해당 함수는 자체적으로 소멸 또는 해제 함수 호출을 제공하지 않는 객체들이나 개발자가 명시적으로 메모리를 할당한 객체에서만 사용되어야 한다. 예를 들어, 메모리 슬라이스를 이용해 할당된 메모리 청크에서는 절대 g_free()를 사용해선 안 된다. 데이터 조각이 고유의 free 함수를 제공할 경우 개발자는 항상 그 함수를 이용해야 한다. g_free()로 NULL 메모리가 전송되면 무시되고 함수가 리턴할 것이다.
또 한 가지 중요한 메모리 함수는 g_memmove()로, 메모리의 조각들을 이동하는 데에 사용된다. 예를 들어 g_memmove()를 잇따라 호출하면 pos에서 시작해 len 문자까지 계속되는 문자열 섹션을 제거할 수 있다.
g_memmove (str + pos, str + pos + len, strlen(str) - (pos + len));
str[strlen(str) - len] = 0;
g_memmove()만 제외하고 동일한 크기로 된 객체를 하나 또는 그 이상 할당할 때는 g_malloc()과 그 friends 대신 항상 메모리 슬라이스를 사용해야 함을 반복해 말한다.
메모리 프로파일링
GLib는 개발자의 애플리케이션 내에서 메모리 사용의 개요를 간단하게 출력하는 방법을 제공한다. 이는 애플리케이션에서 어느 시점이든 g_mem_profile()을 호출하면 된다.
메모리 프로파일링을 이용하기 전에는 GMemVTable을 항상 설정해야 한다. 리스팅 6-3은 기본 GMemVTable을 설정하는 방법과 애플리케이션 종료에 관한 메모리 프로파일링 정보를 출력하는 방법을 보여준다.
기본 GMemVTable을 이용하면 g_malloc(), g_free()와 그 friends의 호출만 계수될 것이다. malloc()과 free()의 호출은 계수되지 않을 것이다. 또 메모리 슬라이스를 프로파일하려면 G_SLICE 환경 변수를 always-malloc으로 설정해야만 강제로 항상 g_malloc()을 이용하도록 할 수 있다. GLib의 메모리 프로파일러는 슬랩 할당자를 이용하여 할당을 계수하지 않을 것이다. 모든 메모리를 감시하려면 Valgrind와 같은 외부 툴을 이용해야 한다.
리스팅 6-3. 메모리 프로파일링 (memprofile.c)
#include <glib.h>
int main (int argc,
char *argv[])
{
GSList *list = NULL;
/* Set the GMemVTable to the default table. This needs to be called before
* any other call to a GLib function. */
g_mem_set_vtable (glib_mem_profiler_table);
/* Call g_mem_profile() when the application exits. */
g_atexit (g_mem_profile);
list = (GSList*) g_malloc (sizeof (GSList));
list->next = (GSList*) g_malloc (sizeof (GSList));
/* Only free one of the GSList objects to see the memory profiler output. */
g_free (list->next);
return 0;
}
메모리 사용 개요를 출력하려면 g_mem_set_vtable()을 이용해 GMemVTable을 설정해야 한다. GMemVTable은 프로파일링을 활성화하여 메모리 할당 함수의 새로운 버전을 정의하므로 GLib가 추적할 수 있다. 이러한 함수로는 malloc(), realloc(), free(), calloc(), try_malloc(), try_realloc()이 있다.
자신만의 GMemVTable 생성이 가능하도록 GLib는 glib_mem_profiler_table이라는 사전에 빌드된 버전을 제공하기도 한다. 거의 모든 경우 기본 메모리 테이블이 사용된다.
GMemVTable을 정의하고 나면 리스팅 6-3은 g_atexit()를 이용하기 때문에 애플리케이션이 종료되면 g_mem_profile()이 호출된다. g_atexit()에 명시된 함수들은 매개변수를 수락하지 않고 어떤 값도 리턴하지 않아야 한다.
리스팅 6-3에서 애플리케이션의 출력은 아래와 같다. 이러한 출력은 개발자의 GLib 버전, 시스템 타입, 그 외 다양한 요인에 따라 달라질 것이다.
GLib Memory statistics (successful operations):
blocks of n_bytes|allocated n_times by malloc()|freed n_times by free()|allocated n_times by realloc()|freed n_times by realloc()|n_bytes remaining
===========|============|============|============|============|===========
8|2|1|0|0|+8
GLib Memory statistics (falling operations):
--- none ---
Total bytes: allocated=16, zero-initialized=0 (0.00%), freed=8 (50.00%), remaining=8
위의 표는 할당되는 메모리 크기와, 그 곳에서 mallo()이 몇 회 호출되는지를 표시한다. 8바이트로 된 두 개의 블록은 두 개의 GSList 객체가 할당되었음을 보여준다. 다음으로, free()를 이용해 얼마나 많은 메모리 블록이 해제되었고, realloc()를 이용해서는 얼마나 할당되었는지, realloc()을 이용해서는 다시 얼마나 해제되었는지 표시한다. 마지막 열은 해제되지 않은 메모리의 바이트 수를 표시한다. 하나의 GSList 객체만 해제되었으므로 8 바이트의 누수가 발생하였음을 알 수 있다.
애플리케이션 내에서는 어떤 것도 실패하지 않았기 때문에 표에서는 성공한 연산만 표시된다. 메모리 할당이나 메모리 반환에서 어떤 유형의 실패가 발생했다면 그러한 연산이 두 번째 표에 표시될 것이다.
출력의 끝에는 표에 표시된 모든 정보를 표시하는 개요가 제공된다.
유틸리티 함수
이미 눈치챘겠지만 GLib는 매우 광범위한 기능을 제공한다. 이번 절에서는 GTK+ 애플리케이션을 개발 시 꼭 필요한 라이브러리를 보여준다.
이번 절에서는 환경 변수, 타이머, 디렉터리 함수, 파일 조작으로의 접근성을 포함해 GLib가 제공하는 많은 유형의 기능을 학습할 것이다.
환경 변수
다수의 플랫폼에서 실행시킬 애플리케이션을 생성한다면 사용자의 홈 디렉터리나 호스트명과 같이 환경에 의존적인 값을 처리하는 일이 귀찮아지는 경우가 발생한다. 표 6-4는 중요한 환경 변수를 리턴하는 함수 몇 가지를 제공한다.
함수 | 설명 |
g_get_current_dir() | 현재 작업 디렉터리를 얻는다. 리턴된 문자열은 더 이상 필요하지 않을 때 해제되어야 한다. |
g_get_home_dir() | 현재 사용자의 홈 디렉터리를 얻는다. Windows에서는 HOME 또는 USERPROFILE 환경 변수가 사용되고, 어떤 것도 설정되지 않으면 루트 Windows 디렉터리가 사용된다. 유사 UNIX 시스템에서는 passwd에 있는 사용자 엔터리가 사용될 것이다. |
g_get_host_name() | 시스템의 호스트명을 얻는다. 시스템명을 결정할 수 없다면 localhost가 리턴된다. 관리자(administrator)는 일부 시스템에서 그들이 원하는 값으로 설정하므로 이 변수가 모든 시스템에서 일관되길 기대해선 안 된다. |
g_get_real_name() | 사용자의 실제 이름을 얻는다. 유사 UNIX 머신에서는 주로 passwd 파일 내 사용자 정보로부터 얻는다. 실제 이름을 결정할 수 없을 경우 "Unknown" 문자열이 리턴된다. |
g_get_tmp_dir() | 임시 파일을 저장하는 데 사용된 디렉터리를 얻는다. 환경 변수 TMPDIR, TMP, TEMP가 검사될 것이다. 셋 중 어떤 것도 정의되지 않은 경우 UNIX에서는 "/tmp"가 리턴되고 Windows에서는 "c:\"가 리턴될 것이다. |
g_get_user_name() | 현재 사용자의 사용자명을 얻는다. Windows에서 리턴된 문자열은 항상 UTF-8이 될 것이다. 유사 UNIX 시스템에서는 파일명에 선호되는 인코딩에 따라 좌우되고, 시스템에 따라 다를 것이다. |
표 6-4. 환경 유틸리티 변수 |
표 6-4에 소개된 함수 외에도 g_getenv()를 이용해 어떤 환경 변수의 값이든 검색할 수 있다. 환경 변수가 발견되지 않으면 NULL이 리턴된다. 리턴된 문자열은 g_getenv()를 다시 호출하여 오버라이드가 가능하므로, 계속 필요하다면 문자열의 새 복사본을 저장해야 함을 기억하라.
gboolean g_setenv (const gchar *variable,
const gchar *value,
gboolean overwrite);
g_setenv()를 이용해 새 값을 환경 변수로 제공하는 것도 가능하다. 값이 이미 존재하여 겹쳐쓰기를 원한다면 함수에 TRUE를 제공해야 한다. 환경 변수를 설정할 수 없다면 g_setenv()는 FALSE를 리턴할 것이다. 변수명을 수락하는 g_unsetenv()를 이용해 환경 변수를 설정해제(unset)할 수도 있다.
타이머
많은 애플리케이션에서 개발자는 경과 시간을 추적하고자 할 것이다. 해당 예로, 인터넷에서 파일을 다운로드하거나 복잡한 작업을 처리하는 애플리케이션을 들 수 있겠다. 이를 위해 GLib는 GTimer 구조체를 제공한다.
GTimer 객체는 경과 시간을 마이크로 초와 누적된 초(fractions of seconds)로 추적한다. 초 수를 검색하기 위해서는 리턴된 gdouble 값을 이용하면 된다. 이 값은 이후 경과된 분(minute) 시간을 계산하는 데에 사용되기도 한다. 시간은 마이크로 초로 계수되므로 높은 정밀도를 이용 가능하다.
리스팅 6-4는 두 번의 버튼 클릭 사이에 경과한 시간을 세는 간단한 타이머 예제를 제공한다. 타이머는 항상 계수하기 때문에 버튼을 클릭 시 시작 시간과 완료 시간을 저장함으로써 작동한다.
리스팅 6-4. 토글 간 경과 시간 (timers.c)
#include <gtk/gtk.h>
static void button_clicked (GtkButton*, GTimer*);
int main (int argc,
char *argv[])
{
GtkWidget *window, *button;
GTimer *timer;
gtk_init (&argc, &argv);
window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
gtk_window_set_title (GTK_WINDOW (window), "Timers");
gtk_container_set_border_width (GTK_CONTAINER (window), 10);
gtk_widget_set_size_request (window, 150, 75);
/* Initialize the timer. */
timer = g_timer_new ();
button = gtk_button_new_with_label ("Start Timer");
g_signal_connect (G_OBJECT (button), "clicked",
G_CALLBACK (button_clicked),
(gpointer) timer);
gtk_container_add (GTK_CONTAINER (window), button);
gtk_widget_show_all (window);
gtk_main ();
return 0;
}
/* Count the amount of elapsed time between two button clicks. */
static void
button_clicked (GtkButton *button,
GTimer *timer)
{
static gdouble start_time = 0.0;
static gdouble end_time = 0.0;
static gboolean running = FALSE;
if (!running)
{
start_time = g_timer_elapsed (timer, NULL);
gtk_button_set_label (button, "Stop Timer");
}
else
{
end_time = g_timer_elapsed (timer, NULL);
gtk_button_set_label (button, "Start Timer");
g_print ("Elapsed Time: %.2f\n", end_time - start_time);
}
running = !running;
}
타이머는 상대적으로 이해하기 쉬운 주제다. 이는 플랫폼마다 다르게 처리하지만 GLib는 이들을 처리하는 데에 이식 가능한 인터페이스를 제공한다. 새로운 타이머는 g_timer_new()를 이용해 생성된다. 새 타이머를 생성하면 이는 g_timer_start()를 호출하여 자동으로 시작된다.
g_timer_stop() 또는 g_timer_continue()는 각각 타이머를 중지하고 계속한다. 애플리케이션에서 언제든 g_timer_elapsed()를 이용하면 경과 시간을 검색할 수 있다.
gdouble g_timer_elapsed (GTimer *timer,
gulong *microseconds);
타이머가 시작되었으나 중지되지 않았다면 경과 시간은 시작 시간을 기반으로 계산될 것이다. 하지만 타이머를 재시작하는 데에 g_timer_continue()가 사용되었다면 두 종류의 시간을 더해 총 경과 시간이 계산된다.
g_timer_elapsed()의 리턴 값은 누적(fractional) 시간과 함께 경과된 초의 수를 나타낸다. microseconds 라는 매개변수는 경과한 마이크로 초로 거의 무용지물인데, 개발자는 초의 수를 이미 부동 소수점 값으로 검색할 수 있기 때문이다.
g_timer_reset()을 이용하면 타이머를 0초로 다시 설정할 수 있다. g_timer_start()를 이용해 타이머의 리셋도 가능하지만 타이머는 자동으로 계수를 계속할 것이다.
애플리케이션을 끝내기 전에 타이머 객체의 사용을 완료했다면 g_timer_destroy()를 이용해 타이머를 소멸시키고 연관된 자원을 모두 할당해제할 수 있다.
파일 조작
파일로부터 읽고 쓰는 일은 거의 모든 애플리케이션에서 매우 중요한 측면이다. GTK+에서 파일을 작업하는 방법에는 두 가지가 있는데, IO 채널을 이용하는 것과 파일 유틸리티 함수를 이용하는 것이다.
리스팅 6-5는 데이터를 읽고 이를 파일로 쓰는 데에 파일 유틸리티 함수를 이용하는 방법을 보여준다. 제시된 함수는 파일의 전체 내용을 읽고 파일의 전체 내용을 덮어 쓴다는 사실을 주목해야 한다. 따라서 이 방법이 모든 애플리케이션에서 해답이 되지는 못한다. 이 예제는 파일 테스트를 실행하는 방법도 소개한다.
리스팅 6-5. 파일을 쓰고 읽기 (files.c)
#include <glib.h>
static void handle_error (GError*);
int main (int argc,
char *argv[])
{
gchar *filename, *content;
gsize bytes;
GError *error = NULL;
/* Build a filename in the user's home directory. */
filename = g_build_filename (g_get_home_dir(), "temp", NULL);
/* Set the contents of the given file and report any errors. */
g_file_set_contents (filename, "Hello World!", -1, &error);
handle_error (error);
if (!g_file_test (filename, G_FILE_TEST_EXISTS))
g_error ("Error: File does not exist!");
/* Get the contents of the given file and report any errors. */
g_file_get_contents (filename, &content, &bytes, &error);
handle_error (error);
g_print ("%s\n", content);
g_free (content);
g_free (filename);
return 0;
}
static void
handle_error (GError *error)
{
if (error != NULL)
{
g_printf (error->message);
g_clear_error (&error);
}
}
파일 유틸리티 함수를 사용하기 전에 g_build_filename()을 이용해 원하는 파일의 경로를 빌드하였다. 이 함수는 파일명의 경로를 빌드하는 데에 NULL로 끝나는 문자열 리스트를 이용한다. 함수에서는 강제적으로 절대 경로를 만들기 위한 시도가 전혀 없었기 때문에 상대 경로로 빌드해도 괜찮다. 사용자의 플랫폼에 올바른 슬래시 타입을 사용할 것이다.
리스팅 6-5에서 g_file_set_contents()가 호출되어 파일에 "Hello World!"라는 문자열을 작성하였다. 파일의 내용이 이미 존재한다면 전체 내용이 겹쳐 작성될 것이다. 텍스트 문자열이 NULL로 종료되지 않을 경우 이 함수에서 텍스트 문자열의 길이는 개발자가 명시해야 한다. 그러한 경우 개발자는 문자열 길이를 -1로 명시할 수도 있다.
gboolean g_file_set_contents (const gchar *filename,
const gchar *contents,
gssize length,
GError **error);
g_file_set_contents()는 두 가지 오류 검사 방식을 제공한다. 액션이 성공적이면 TRUE를 리턴하고 실패하면 FALSE를 리턴한다. 또 G_FILE_ERROR 도메인에서 오류가 GError 매개변수를 통해 리턴될 것이다. 해당 오류 도메인에서 가능한 오류의 리스트는 부록 E에서 찾을 수 있다.
파일의 내용 읽기는 쓰기와 비슷한 방식으로 g_file_get_contents() 함수의 호출을 통해 실행된다. 액션이 성공하면 함수는 TRUE를 리턴하고, 실패하면 FALSE를 리턴한다. 파일로부터 읽은 텍스트 문자열의 길이 또한 함수가 설정한다. G_FILE_ERROR 도메인의 오류가 보고될 것이다.
gboolean g_file_get_contents (const gchar *filename,
gchar **contents,
gsize *length,
GError **error);
파일을 읽기 전에는 파일이 존재하는지 확보하도록 특정 검사를 수행하는 것이 좋다. 이를 위해 GLib는 g_file_test()를 이용한 파일 검사를 제공한다. 해당 함수는 파일명이나 디렉터리명을 비롯해 실행시킬 테스트 유형을 수락한다. 테스트가 성공적이면 TRUE를 리턴하고, 실패하면 FALSE를 리턴한다. 테스트 매개변수는 다음의 GFileTest 열거에 의해 제공된다.
- G_FILE_TEST_IS_REGULAR: 파일이 심볼릭 링크(symbolic link) 또는 디렉터리가 아니므로, 일반 파일이라는 의미다.
- G_FILE_TEST_IS_SYMLINK: 개발자가 명시한 파일이 사실상 심볼릭 링크다.
- G_FILE_TEST_IS_DIR: 디렉터리 위치를 가리키는 경로.
- G_FILE_TEST_IS_EXECUTABLE: 명시된 파일이 실행 가능하다.
- G_FILE_TEST_EXISTS: 일부 타입의 객체가 명시된 위치에 존재한다. 하지만 해당 테스트는 그것이 심볼릭 링크인지, 디렉터리인지, 일반 파일인지 알아내지는 않는다.
bitwise 연산을 이용해 다수의 테스트를 동시에 실행하는 것도 가능하다. 가령 경로가 디렉터리 또는 일반 파일을 가리키는 경우 (G_FILE_TEST_IS_DIR l G_FILE_TEST_IS_REGULAR)는 TRUE를 리턴할 것이다.
심볼릭 링크와 관련해 주의해야 할 사항들이 몇 가지 있다. 첫째, 모든 테스트는 심볼릭 링크를 완료한다. 따라서 심볼릭 링크가 일반 파일을 가리키면 G_FILE_TEST_IS_REGULAR는 TRUE를 리턴할 것이다.
파일이나 디렉터리에 어떤 유형의 액션을 실행해도 안전한지 검사하기 위해 g_file_test()를 사용 시에는 주의를 기울여야 한다. 액션을 실행하기 전에 파일의 상태가 변할 수 있어서 실제로 실행하기 전에는 그 액션이 허용되는지 확신할 방법이 없다. 그 때문에 리턴된 GError에서 G_FILE_ERROR_EXIST를 검사하는 것이 좋은 생각이다.
디렉터리
일부 애플리케이션에서는 디렉터리의 내용을 검색할 필요가 있다. 이를 수행하도록 C에서 함수들을 제공하지만 GLib의 GDir 구조체를 사용하는 편이 훨씬 더 수월하다. 리스팅 6-6은 사용자의 홈 디렉터리 내용 전체를 읽고 화면에 출력하는 방법을 보여준다.
리스팅 6-6. 디렉터리 내용 얻기 (directories.c)
#include <glib.h>
int main (int argc,
char *argv[])
{
/* Open the user's home directory for reading. */
GDir *dir = g_dir_open (g_get_home_dir (), 0, NULL);
const gchar *file;
if (!g_file_test (g_get_home_dir (), G_FILE_TEST_IS_DIR))
g_error ("Error: You do not have a home directory!");
while ((file = g_dir_read_name (dir)))
g_print ("%s\n", file);
g_dir_close (dir);
return 0;
}
디렉터리는 g_dir_open()을 이용해 열린다. 함수의 첫 번째 매개변수는 열어야 할 디렉터리를 명시한다. g_dir_open()의 두 번째 매개변수는 향후 사용을 위해 보류되며, 이 시점에는 0으로 설정되어야 한다. 마지막 매개변수는 GError를 리턴하는데, 디렉터리가 성공적으로 로딩되지 않을 경우 NULL이 리턴되므로 함수가 실패하면 개발자는 그 사실을 알게 될 것이다.
while ((file = g_dir_read_name (dir)))
g_print ("%s\n", file);
간단한 while 루프를 이용해 디렉터리 내 모든 파일과 폴더를 검색할 수 있다. g_dir_read_name()을 이용해 디스크에서 나타나는 요소의 순서대로 한 번에 하나씩 리스트 내 모든 요소가 리턴된다. 더 이상 엔트리가 존재하지 않으면 NULL이 리턴된다. 리턴된 문자열은 GLib가 소유하므로 해제해선 안 된다.
엔트리를 다시 살펴보기 위해 리스트에서 첫 번째 엔트리로 리턴해야 한다면 GDir 객체에서 g_dir_rewind()를 호출해야 한다. 그러면 구조체를 리셋하여 다시 첫 번째 파일이나 폴더를 가리킬 것이다.
GDir 객체에서 볼일이 끝났다면 언제나 g_dir_close()를 호출하여 GDir를 할당해제하고 연관된 자원을 모두 해제해야 한다.
파일 시스템
GLib는 UNIX 운영체제의 기능을 래핑하는 또 다른 유틸리티 함수도 몇 가지 제공한다. 이러한 함수들이 작동하려면 <glib/gstdio.h>를 포함시킬 필요가 있다. 가장 중요한 함수들 중 다수는 이번 절에서 소개할 것이다. 전체 리스트는 GLib API 문서의 "File Utilities(파일 유틸리티)" 절을 참조하길 바란다.
이번 절에 소개된 모든 함수들과 관련해, 액션이 성공하면 0이 리턴되고 성공하지 못하면 -1이 리턴된다.
파일이나 폴더를 새 위치로 이동시키기 위해서는 g_rename()을 이용한다. 오래된 파일명과 새 파일명이 같은 문자열일 경우 추가 액션 없이 0이 리턴될 것이다. 파일이 새 파일명 위치에 이미 존재하는 경우 UNIX 머신에서 파일이 대체될 것이다. 디렉터리와 파일에 대한 파일명은 혼합될 수 없다.
int g_rename (const gchar *old_filename,
const gchar *new_filename);
g_rename()을 둘러싼 권한 문제도 몇 가지 존재한다. 사용자는 파일을 포함하는 디렉터리와 파일을 소유한다. 사용자는 파일로 작성할 수 있어야 한다.
파일이나 디렉터리는 g_remove() 또는 g_rmdir()를 이용해 쉽게 제거가 가능하다. 사실상 g_remove()를 이용해 디렉터리를 제거할 수도 있는데, 이는 디렉터리 제거 함수를 호출할 것이기 때문이다. 하지만 다른 운영체제로 이식성을 위해서는 디렉터리를 제거 시 항상 g_rmdir()를 이용하도록 한다. 두 함수 모두 디렉터리가 비어 있지 않으면 실패할 것이다.
int g_remove (const gchar *filename);
int g_rmdir (const gchar *filename);
g_mkdir()를 이용하면 새 디렉터리를 생성할 수 있다. 권한은 네 자리 정수로 명시해야 한다. 가령 0755, 0700 식의 정수가 허용된다.
int g_mkdir (const gchar *filename,
int permissions);
이러한 파일 유틸리티 함수들 중 다수는 절대 경로 뿐만 아니라 상대 경로도 사용 가능하다. 하지만 상대 경로를 이용하려면 올바른 디렉터리에 위치하는지 확신해야 한다. 자신의 하드 드라이브의 디렉터리 구조체를 통해 이동시키려면 g_chdir()를 이용할 수 있다. 이 함수는 상대 경로와 절대 경로 모두 수락할 것이다.
int g_chdir (const gchar *path);
자신의 애플리케이션 내부에서 파일이나 폴더의 권한을 변경하길 원할지도 모른다. 이는 g_chmod()를 이용하면 가능하다. 권한을 나타내는 정수는 g_mkdir()에서와 마찬가지로 네 자리로 명시되어야 한다.
int g_chmod (const gchar *filename,
int permissions); n
메인 루프
앞의 여러 장에서 우리는 GLib가 고유의 메인 루프를 가진다는 사실을 생각하지 않고 GTK+의 메인 루프를 사용했다. 이는 다른 모든 예제에서 무시해도 좋은데, gtk_init()는 개발자 대신 GLib 메인 루프를 자동으로 생성할 것이기 때문이다.
사실 메인 루프 기능 대부분은 GLib에서 구현되고, GTK+ 는 시스템으로 위젯 시그널을 제공할 뿐이다. GTK+ 메인 루프는 GTK의 X 서버 이벤트를 GLib 시스템으로 연결하기도 한다.
메인 루프의 목적은 일부 이벤트가 발생할 때까지 sleep 상태로 두는 것이다. 그 시점에 콜백 함수를 이용 가능하다면 콜백 함수가 호출될 것이다. GLib의 메인 루프는 poll() 시스템 호출을 이용해 Linux에서 구현된다. 이벤트와 시그널은 파일 디스크립터와 연관되는데, 이는 poll()을 이용해 살펴볼 수 있다.
poll()을 이용 시 장점으로는 GLib가 새 이벤트를 계속해서 확인할 필요가 없다는 것이다. 오히려 어떤 시그널이나 이벤트가 발생할 때까지 sleep 상태로 유지될 수 있다. 이를 통해 애플리케이션은 프로세서 시간이 필요할 때까지는 어떠한 프로세스 시간도 시작하지 않을 것이다.
GTK+ 메인 루프는 gtk_main()을 이용해 호출된다. 해당 함수는 사실상 여러 번 호출이 가능하며, 스택의 최상위에서 호출은 gtk_main_quit()를 이용해 제거된다. 현재 메인 루프 수준은 gtk_main_level()을 이용해 검색 가능하다.
컨텍스트와 소스
GLib 메인 루프는 수많은 구조체로 구현되어 여러 인스턴스의 동시 실행을 가능하게 한다. GMainContext는 수많은 이벤트 소스를 나타내는 데 사용된다. 각 스레드는 고유의 컨텍스트를 가지며, g_main_context_get()을 이용해 검색할 수 있다. g_main_context_get_default()를 이용하면 기본 컨텍스트의 검색도 가능하다.
컨텍스트에서 각 이벤트 소스에는 우선순위가 주어지는데, 이는 G_PRIORITY_DEFAULT 또는 0으로 기본 설정된다. 우선순위가 높은 소스는 음의 우선순위를 가진 소스보다 우선할 것이다. 이벤트 소스의 예로는 timeout 및 idle 함수를 들 수 있다.
GLib는 GMainLoop를 제공하기도 하는데, 이는 메인 루프의 한 가지 인스턴스를 나타낸다. g_main_loop_new()를 이용해 새로운 메인 루프를 생성할 수 있는데, 여기서 NULL 컨텍스트는 기본값(default)을 이용할 것이다. is_running을 TRUE로 설정하면 메인 루프가 실행되고 있음을 나타내며, g_main_loop_run()을 호출하면 이것이 자동으로 설정될 것이다.
GMainLoop* g_main_loop_new (GMainContext *context,
gboolean is_running);
gtk_dialog_run() 함수는 g_main_loop_new()를 이용해 고유의 GLib 메인 루프를 생성함으로써 메인 루프가 계속되는 것을 막는다. 루프에서 g_main_loop_quit()가 호출될 때까지 계속 실행될 것이다.
GTK+ 메인 루프는 gtk_main()에서 기본 컨텍스트를 이용해 GMainLoop를 생성하여 GLib 메인 루프를 구현한다. 짧게 말해, GTK+에서 함수가 제공하는 메인 루프 기능이 GLib에서 구현된다는 말이다.
GLib는 새 이벤트 소스를 생성하는 기능을 지원한다. GSource에서 파생되어 새 소스를 생성한다. GLib는 g_timeout_source_new()와 g_idle_source_new()를 이용해 새로운 timeout 및 idle 함수의 소스를 생성하는 기능도 제공한다. 이러한 함수들은 개발자의 컨텍스트로 연관될 수 있다.
g_source_new()를 이용하면 커스텀 소스를 생성하는 것도 가능하다. 해당 함수는 함수로 구성된 테이블과 새 소스의 구조체 크기를 수락한다. 이 함수들은 새 소스 타입의 행위를 정의하는 데에 사용된다.
GSource* g_source_new (GSourceFuncs *source_funcs,
guint struct_size);
이제 g_source_attach()를 호출함으로써 GMainContext와 소스를 연관시켜야 한다. 그러면 컨텍스트 내부에서 소스의 유일한 정수 식별자를 리턴할 것이다.
본 저서의 범위에 부합하도록 지금쯤이면 이번 절의 나머지 부분에 실린 예제들을 이해할 수 있을 만큼 메인 루프를 충분히 학습하였을 것이다. 메인 루프의 복잡성에는 다룰 내용들이 훨씬 더 많지만 이 책에서는 다루지 않을 것이다. 따라서 자신만의 소스와 컨텍스트를 생성해야 한다면 GLib의 API 문서를 참조해야 할 것이다.
Timeouts
Timeout 함수는 FALSE가 리턴될 때까지 특정 시간 간격으로 호출되는 메서드다. 이들은 g_timeout_add_full() 또는 g_timeout_add()를 이용해 메인 루프로 추가된다.
리스팅 6-7은 1/10초마다 진행 막대가 움직이는 간단한 예를 보여준다. 진행 막대는 0.1의 펄스 단계(pulse step)로 설정되었기 때문에 진행 표시기가 진행 막대의 한쪽 끝에서 나머지 끝으로 이동하는 데에는 약 1초가 소요될 것이다. timeout은 25회의 호출 이후 제거된다.
리스팅 6-7. Timeout 추가하기 (timeouts.c)
#include <gtk/gtk.h>
static gboolean pulse_progress (GtkProgressBar*);
int main (int argc,
char *argv[])
{
GtkWidget *window, *progress;
gtk_init (&argc, &argv);
window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
gtk_window_set_title (GTK_WINDOW (window), "Timeouts");
gtk_container_set_border_width (GTK_CONTAINER (window), 10);
gtk_widget_set_size_request (window, 200, -1);
progress = gtk_progress_bar_new ();
gtk_progress_bar_set_pulse_step (GTK_PROGRESS_BAR (progress), 0.1);
g_timeout_add (100, (GSourceFunc) pulse_progress, (gpointer) progress);
gtk_container_add (GTK_CONTAINER (window), progress);
gtk_widget_show_all (window);
gtk_main ();
return 0;
}
/* Pulse the progress bar and return TRUE so the timeout is called again. */
static gboolean
pulse_progress (GtkProgressBar *progress)
{
static gint count = 0;
gtk_progress_bar_pulse (progress);
i++;
return (i < 25);
}
Timeout 함수를 추가할 때는 g_time_add() 또는 g_timeout_add_full()을 이용한다. 두 함수의 유일한 차이점은 후자의 경우 개발자가 GDestroyNotify를 명시하도록 해주어 개발자가 timeout 함수를 제거하기 위해 FALSE를 리턴할 때 호출될 것이란 점이다.
guint g_timeout_add_full (gint priority,
guint interval_in_milliseconds,
GSourceFunc timeout_function,
gpointer data,
GDestroyNotify destroy_function);
g_timeout_add_full()의 첫 번째 매개변수는 timeout의 우선순위를 정의할 수 있게 해준다. 대부분의 경우 개발자는 timeout 함수의 우선순위에 G_PRIORITY_DEFAULT를 사용하길 원할 것이다. 이용 가능한 우선순위 리스트는 다음과 같다.
- G_PRIORITY_HIGH: 이 우선순위는 GLib나 GTK+ 어디에서도 사용되지 않으므로 이러한 유형의 함수는 다른 함수들보다 우선할 것이다. 따라서 대부분의 경우 사용해선 안 되는데, CPU 자원을 많이 소모하는(CPU-intensive) 계산은 일시적으로 사용자 인터페이스의 무반응을 야기할 수 있기 때문이다.
- G_PRIORITY_DEFAULT: 이 우선순위는 GDK에서 대부분의 timeout과 X 이벤트에 사용된다. 애플리케이션이 필요로 하는 가장 중요한 함수 호출을 방해할 수 있으므로 idle 함수와 함께 사용되어선 안 된다.
- G_PRIORITY_HIGH_IDLE: 우선순위가 높은 idle 함수가 이를 사용한다. 다시 그리기(redrawing) 위젯이 이보다 약간 더 높은 우선순위를 갖고 있지만 대부분 GTK+ 액션의 속도를 늦추거나 간섭하지는 않을 것이다.
- G_PRIORITY_DEFAULT_IDLE: 대부분의 idle 함수에 이 우선순위를 사용해야 한다.
- G_PRIORITY_LOW: GLib 또는 GTK+ 어디에서도 사용되지 않으므로 무엇이든 이 액션보다 우선할 것이다.
g_timeout_add_full()의 두 번째 매개변수는 함수의 호출 간 간격을 밀리 초로 정의한다. 리스팅 6-7에서는 1/10초 또는 100 밀리초마다 timeout이 호출되었다.
Timeout 함수 호출이 겹치는 일은 염려하지 않아도 되는 것이, 이전 호출이 리턴할 때 그 다음 간격(interval)이 계산되기 때문이다. 따라서 timeout 함수가 리턴에 3초를 소요한다면 간격에 해당 시간이 추가될 것이다.
Timeout 함수는 우선순위가 더 높은 함수에 의해 지연되기도 하며, 콜백 함수를 실행하는 데에 소요되는 시간에 따라 지연되기도 한다. 따라서 정확한 타이밍의 소스로 의존해서는 안 된다. Timeout의 시간이 늦어지면 다음 호출은 간격을 재계산할 것이다. 함수는 지연된 시간만큼 메우려는 시도는 하지 않을 것이다.
g_timeout_add_full()에서 세 번째 매개변수는 실제 timeout 함수다. Timeout 함수는 gpointer를 수신하고, gboolean 값을 리턴한다. 타임아웃 함수로부터 FALSE를 리턴하면 함수가 제거될 것이다. gpointer 매개변수는 g_timeout_add_full()의 네 번째 매개변수에 의해 정의된다.
마지막 매개변수는 소멸(destroy) 함수를 정의하며 idle 함수가 제거될 때, 즉 idle 함수로부터 FALSE가 리턴될 때 호출되어야 한다. 해당 매개변수는 NULL로 설정하는 편이 안전하다.
g_timeout_add_full()에 정의된 소멸 함수는 어떤 값도 리턴하지 않지만 gpointer를 그들의 매개변수로서 수신한다. 이러한 gpointer는 timeout 함수가 본래 수신한 것과 동일한 값으로, 필요 시 메모리에서 해제할 기회를 개발자에게 제공한다.
Idle 함수
제 1장에 언급한 바와 같이 GLib는 높은 우선순위의 보류 중인 이벤트가 없을 때 호출되는 특수 타입의 함수인 idle 함수를 제공한다. 이는 메인 루프에서 더 이상 할 일이 없을 때까지 계속해서 실행된다.
Idle 함수는 g_idle_add() 또는 g_idle_add_full()를 통해 추가된다. 둘의 유일한 차이는 후자의 경우 G_PRIORITY_DEFAULT_IDLE의 기본값을 사용하는 대신 우선순위와 소멸 함수를 명시하도록 해준다는 데에 있다.
해당 함수의 첫 번째 매개변수는 idle 함수의 우선순위다. Idle 함수는 높은 우선순위의 보류 중인 이벤트가 없을 때에만 호출된다. 따라서 우선순위가 높을수록 함수가 자주 호출될 것이다. 거의 모든 경우 idle 함수는 G_PRIORITY_HIGH_IDLE 또는 G_PRIORITY_DEFAULT_IDLE 우선순위를 가질 것이다.
guint g_idle_add_full (gint priority,
GSourceFunc idle_function,
gpointer data,
GDestroyNotify destroy_function);
g_idle_add_full() 에서 두 번째 매개변수는 실제 idle 함수다. Timeout과 비슷하게 idle 함수도 gpointer를 수신하고 gboolean 값을 리턴한다. Idle 함수로부터 FALSE를 리턴하면 함수가 제거될 것이다. gpointer 매개변수는 g_idle_add_full()의 세 번째 매개변수에 의해 정의된다.
마지막 매개변수는 idle 함수에서 FALSE가 리턴되어 함수가 제거될 때 호출되는 소멸 함수를 정의한다. 해당 매개변수는 NULL로 설정하는 편이 안전하다.
g_idle_add_full()에 정의된 소멸 함수는 어떤 값도 리턴해선 안 되지만 gpointer를 매개변수로서 수신한다. 이러한 gpointer는 idle 함수가 본래 수신한 것과 동일한 값으로, 필요 시 메모리에서 해제할 기회를 개발자에게 제공한다.
콜백으로부터 FALSE를 리턴하여 idle 함수를 제거할 수도 있지만 g_idle_remove_data()를 이용하면 애플리케이션 어디서든 함수를 제거할 수 있다. 이 함수는 idle 함수의 콜백에 사용된 데이터를 수락하고, 성공적으로 제거되면 TRUE를 리턴할 것이다.
gboolean g_idle_remove_by_data (gpointer data);
콜백 함수 내부에서 idle 함수에 g_idle_remove_by_data()를 호출해서는 절대 안 된다. 이는 idle 함수 리스트에 충돌을 야기할 수 있다. 콜백 내부에서 idle 함수를 제거하려면 대신 FALSE를 리턴하라.
데이터 유형
GLib가 제공하는 가장 유용한 기능들 중 하나는 엄청난 규모의 데이터 유형들이다. 이번 장에서는 가장 중요한 데이터 유형을 소개할 것인데, 그 중 다수는 GTK+ 위젯에서도 사용된다. 그 중에서도 단일 및 이중 연결 리스트에 주의를 기울여야 하는데, GTK+에서도 많이 사용되기 때문이다.
이번 절의 내용을 훑어보면 본문에 제시된 데이터 유형들이 비슷한 방식으로 사용되었음을 눈치챌 것이다. API가 획일화되면 개발자가 학습해야 하는 디자인 패턴의 수가 줄어든다. 이를 통해 다수의 유형들이 비슷한 기능을 가진다. 하지만 이러한 유형에는 각각의 장·단점들이 있으니 어떤 유형을 이용할지 결정할 때는 주의를 기울여야 한다.
문자열
대부분의 프로그래머들에게 있어 문자열은 새로운 내용이 아니지만 GString 구조체는 C 프로그래머에게 매우 유용하게 작용할 수 있다. 이는 텍스트가 추가되면 자동으로 크기가 증가하는 문자열을 쉽게 생성시킬 수 있는 방법을 제공한다. 또 표준 C 문자열을 해칠 수 있는 버퍼 오버플로우와 기타 런타임 오류 등의 문제를 피하도록 도와준다.
Glib 문자열은 일부 메모리 관리, C 문자열의 현재 상태로 쉬운 접근성, 문자열 조작에 유용한 함수를 제공하기도 한다. 따라서 프로그래머는 GString을 이용하지 않을 때보다 훨씬 더 수월하게 C 문자열을 처리할 수 있다.
GString 구조체는 3개의 member로 구성되는데, 이는 문자열의 현재 상태를 보유하는 C 문자열, 끝나는 바이트를 제외한 str 의 길이, 현재 문자열에 할당된 메모리양에 해당된다. 문자열이 할당된 길이 이상으로 커져야 하는 경우 GString은 자동으로 메모리를 더 할당할 것이다.
typedef struct
{
gchar *str;
gsize len;
gsize allocated_len;
} GString;
GString의 str member로 영구 참조(permanent reference)를 만들어선 안 된다. 문자열에 할당된 길이가 변경되기 때문에 문자열에 텍스트가 추가, 삽입 또는 제거되면서 다른 위치로 이동할 수도 있다!
새로운 GString 객체를 생성하는 방법에는 세 가지가 있다. g_string_new()를 호출하면 초기 문자열에서 새 GString을 생성할 수 있다. GString은 initial_str의 내용을 복사하므로 개발자는 문자열이 더 이상 필요 없다면 해제할 수 있다. 초기 문자열을 NULL로 명시하면 g_string_new()는 자동으로 빈 GString을 생성할 것이다.
GString* g_string_new (const gchar *initial_str);
GString* g_string_new_len (const gchar *initial_str,
gssize length);
GString* g_string_sized_new (gsize default_size);
새 GString을 생성하는 또 다른 방법은 g_string_new_len()을 이용하는 것으로, 이는 initial_str 의 length 문자로 GString을 초기화하고 만일 length가 -1일 경우에는 전체 문자열을 초기화한다. GString을 이용 시에는 삽입된(embedded) 널 바이트를 처리할 수 있다는 또 다른 장점이 있다.
마지막으로 소개할 GString 초기화 함수는 g_string_sized_new()로, default_size의 길이로 된 새 문자열을 생성할 것이다. 이 함수를 이용하면 큰 문자열을 할당하여 너무 자주 재할당되지 않도록 할 수 있다.
한 가지 매우 유용한 함수로 g_string_printf()를 들 수 있는데, 이는 GString의 내용을 구성하는 데에 sprintf() 스타일의 포맷을 이용할 수 있게끔 해준다. 유일한 차이점은, GString의 경우 필요 시 자동으로 확장될 것이란 점이다. GString 버퍼가 이전에 포함했던 내용은 모두 소멸된다.
void g_string_printf (GString *string,
const gchar *format,
...);
현재 내용은 변경하지 않은 채 GString의 끝에 포맷팅된 문자열을 추가하는 g_string_append_printf()를 이용하는 수도 있다. 아래 예제에서와 같이 GString의 끝에 텍스트를 추가 시 사용할 수 있는 함수의 수는 많다. 이러한 함수들은 각각 val의 전체 내용, val의 첫 len 문자, 단일 문자, 단일 UCS-4 문자를 뒤에 추가하도록 해준다.
GString* g_string_append (GString *string,
const gchar *val);
GString* g_string_append_len (GString *string,
const gchar *str,
gssize len);
GString* g_string_append_c (GString *string,
gchar c);
GString* g_string_append_unichar (GString *string,
gunichar wc);
위의 네 개의 함수 외에도 GString의 앞에 추가하거나 삽입하는 데에 사용할 수 있는 버전들도 존재한다. 가령 g_string_prepend_c()는 GString의 시작 부분에 문자를 추가하고, g_string_insert()는 GString에 명시된 위치로 문자열을 삽입할 것이다. 이러한 함수들에 대한 자세한 정보는 GLib의 API 문서에서 "String(문자열)"과 관련된 절을 참고하도록 한다.
텍스트를 GString으로 삽입하는 기능이 있다면 유용하겠지만 텍스트를 제거하는 능력도 그에 못지 않게 중요하다. g_string_erase()를 호출하면 주어진 위치에서 시작해 GString으로부터 다수의 문자를 제거할 수 있다. 해당 함수는 공백을 채우도록 문자열의 끝을 전환하고, 끝나는 문자를 새로운 끝 위치에 위치시키며, 문자열의 길이를 업데이트한다.
GString* g_string_erase (GString *string,
gssize pos,
gssize len);
GString의 사용이 끝나면 g_string_free()를 이용해 메모리를 해제하도록 한다. free_segment를 TRUE로 설정하면 C 문자열도 해제하여 NULL이 리턴된다. 이를 제외한 경우는 C 문자열을 리턴하므로 개발자가 후에 스스로 해제시켜야 한다.
gchar* g_string_free (GString *string,
gboolean free_segment);
GString은 유용한 함수를 많이 제공하지만 문자열을 찾기 위해서는 GLib가 제공하는 표준 문자열 함수도 여전히 이용해야 할 것임을 명심해야 한다. GString은 처음부터 모든 작업을 다시 하는 수고를 피하기 위해 자신의 시스템에서 이용할 수 없는 함수들을 구현한다. 따라서 C 문자열과 직접적으로 상호작용하는 데에 익숙해질 필요가 있겠다.
연결 리스트
앞의 여러 장에 제시된 예제에서 GLib 연결 리스트의 예를 몇 가지 확인했을 것이다. GLib는 단일 연결 리스트와 이중 연결 리스트, 두 가지 유형을 제공한다. GLib는 각각 g_slist_foo()와 g_list_foo()의 접두사를 이용해 두 가지 데이터 유형에 대한 함수를 제공한다.
단일 연결 리스트(GSList)는 가장 단순한 형태의 연결 리스트로 각 노드는 데이터 조각과 다음 노드에 대한 포인터를 가진다. NULL을 가리키는 포인터는 마지막 노드를 나타낸다. GSList 구조체는 리스트 내 하나의 노드를 나타낸다.
typedef struct
{
gpointer data;
GSList *next;
} GSList;
이중 연결 리스트(GList)는 리스트에 이전 요소를 가리키는 포인터가 존재한다는 점을 제외하면 단일 연결 리스트와 동일한 기능을 제공한다. 이는 둘 중 어떤 방향으로든 순회(traverse)할 수 있도록 해준다. NULL을 향하는 이전(previous) 포인터는 리스트에서 첫 요소를 표시한다.
typedef struct
{
gpointer data;
GList *next;
GList *prev;
} GList;
이중 연결 리스트를 역방향으로 순회할 수 있는 기능을 제외하면 두 가지 유형의 리스트 모두 동일한 기능을 제공한다. 따라서 이번 절의 나머지 부분은 이중 연결 리스트에 관한 정보를 싣겠지만 개발자가 함수의 접두사(prefix)만 변경하면 단일 리스트에도 적용 가능하다.
뿐만 아니라 이번 절의 대부분 함수는 새로운 GList 포인터를 리턴한다. 이 값은 꼭 저장해야 하는데, 함수가 실행하는 특정 액션 때문에 리스트 시작 위치가 변경될지도 모르기 때문이다.
리스트 시작 위치에 새 요소를 추가하려면 g_list_prepend()를 이용한다. g_list_append()를 이용해 요소를 끝에 추가하는 것도 가능하지만 이 함수는 요소의 삽입 장소를 찾기 위해 리스트를 순회해야 하므로 사용을 권하지 않는다. 대신 모든 요소의 앞에 추가한 후 g_list_reverse()를 호출하면 리스트 순서를 역전한다.
GList* g_list_prepend (GList *list,
gointer data);
새 노드를 앞이나 뒤에 추가하는 기능 뿐만 아니라 g_list_insert()를 이용하면 임의의 리스트 위치에 노드를 삽입할 수 있다. 위치가 음수이거나 리스트 내 노드의 개수보다 큰 경우 g_list_append()로서 실행될 것이다. g_list_insert_before()를 이용해 노드 바로 직전에 새 노드를 삽입할 수도 있다. 리스트의 길이를 얻기 위해 g_list_length()를 이용하면 부호가 없는 정수를 리턴할 것이다.
GList* g_list_insert (GList *list,
gpointer data,
gint position);
g_list_remove()를 이용해 리스트로부터 요소를 제거하는 것도 가능하다. 매치하는 노드를 찾을 수 없다면 동일한 데이터를 포함하는 노드들 중 가장 먼저 마주치는 노드가 제거된다.
GList* g_list_remove (GList *list,
gconstgpointer data);
노드의 데이터를 해제하지 않고 노드를 제거하고 싶다면 g_list_remove_link()를 호출하면 되는데, 이는 제거하길 원하는 요소를 향하는 포인터를 수락한다. 이전 포인터와 다음 포인터는 NULL로 설정되므로 노드는 하나의 요소로 된 리스트가 된다.
g_list_remove()는 매치하는 데이터가 있는 첫 번째 노드만 제거하지만 g_list_remove_all()은 매치하는 데이터 member를 가진 노드를 모두 제거하는 데에 사용된다. 매치하는 노드가 발견되지 않으면 리스트에는 어떤 일도 발생하지 않는다.
연결 리스트를 모두 사용했다면 g_list_free()를 이용해 해제해야 한다. 개발자는 연결 리스트만 해제된다는 사실을 인지해야 한다. 따라서 해당 함수를 호출하기 전에는 동적으로 할당된 데이터가 모두 해제되도록 확보해야 하는데, 이를 어길 시 메모리 누수가 야기된다.
void g_list_free (GList *list);
연결 리스트는 g_list_sort() 덕에 매우 쉽게 정렬할 수 있다. 단지 GCompareFunc만 명시하면 된다. 비교 함수는 두 개의 상수 포인터(gconstpointer)를 수신하는데 이들은 현재 비교되는 두 개의 노드를 나타낸다. 두 개를 비교하여 첫 번째 노드가 두 번째보다 먼저 정렬되어야 한다면 음수를, 두 번째 노드가 먼저 정렬되어야 한다면 양수를, 둘이 동등하다면 0을 리턴한다.
GList* g_list_sort (GList *list,
GCompareFunc compare_func);
연결 리스트를 검색하도록 제공되는 함수에는 두 가지가 있다. 기본 함수는 g_list_find()로, 주어진 데이터의 첫 번째 요소를 리스트에서 찾을 것이다. 매치하는 노드가 발견되지 않으면 해당 함수는 NULL을 리턴한다.
GList* g_list_find (GList *list,
gconstpointer data);
각 항목이 복잡한 데이터 유형을 포함할 경우 g_list_find_custom()을 이용해 자신만의 검색(find) 함수를 명시하는 것도 가능하다. 이 방법은 g_list_sort()와 동일한 비교 함수의 포맷을 이용하고, GCompareFunc로부터 개발자가 0을 리턴하면 그에 상응하는 GList 노드를 리턴할 것이다. 매치하는 내용이 발견되지 않으면 해당 함수 또한 NULL을 리턴할 것이다.
연결 리스트와 관련된 문제를 이미 언급한 바 있는데, 큰 리스트를 처리할 때 정렬을 비롯해 여러 액션들이 매우 비효율적이라는 단점이 있다. 문제는 많은 함수들이 연결 리스트의 순회를 요하여 리스트 내에 노드의 수가 많을 때에는 오랜 시간이 소요될 수 있다는 데에 있다. 따라서 필요한 노드의 수가 많지 않을 것이라고 확신할 때에만 사용되어야 하는데, 그 때문에 라디오 그룹에서 사용되는 것이다.
하지만 가능한 한 리스트의 순회를 피하는 방법을 안다면 연결 리스트를 효율적으로 사용하는 것이 가능하다. 한 가지 가능한 해결책은 자신의 마지막 리스트 위치 또는 자주 사용될 위치를 남겨두는 방법이 있겠다. 이를 통해 특정 요소를 찾는 데 소요되는 시간을 줄일 수 있다.
연결 리스트의 순회를 완전히 피하는 것도 가능하다. 리스트 내 모든 요소마다 연산을 실행해야 한다면 g_list_foreach()를 이용해야 하는데, 이는 리스트 내 모든 노드마다 GFunc의 인스턴스를 호출 할 것이다.
void g_list_foreach (GList *list,
GFunc func,
gpointer data);
GFunc 프로토타입은 g_list_foreach()에 포함된 데이터 매개변수와 노드의 데이터 member를 수락한다. 이들은 연결 리스트를 여러 번 순회하는 것을 피하기 위해 여러 애플리케이션에서 효율적으로 활용될 수 있다.
균형 이진 트리
균형 이진 트리(balanced binary tree)는 자동으로 그 높이를 가능한 한 낮게 유지하도록 시도하는 트리다. 이를 통해 어떤 두 개의 요소든 거리를 최소화할 수 있다. 따라서 검색, 순회, 삽입, 제거에 소요되는 평균 시간을 최소한으로 유지한다.
연결 리스트이나 문자열과 달리 GTree 구조체에는 public member가 포함되어 있지 않다. 대신 트리에서 연산을 실행하는 데에 제공되는 함수를 사용해야 한다. 함수들은 트리를 변경하는 연산을 실행할 경우 이진 트리의 균형을 자동으로 처리할 것이다.
바이너리 트리에 포함된 각 노드는 하나의 키와 하나의 값으로 구성된다. 키는 트리 내에서 노드의 위치를 계산하는 데 사용되고, 값은 연관된 데이터를 보유하는 데 사용된다. 각 노드는 최대 2개의 자식을 가질 수 있다. 자식이 없는 노드는 leaf라고 부른다.
새 GTree를 생성하도록 제공되는 함수에는 세 가지가 있다. 가장 단순한 함수는 g_tree_new()로, 명시된 비교 함수를 이용해 새로운 빈 트리를 생성할 것이다. 해당 함수는 요소를 트리로 삽입하거나 노드를 재정렬할 때 키의 비교에 사용될 것이다. 첫 번째 요소가 두 번째 요소보다 적으면 음의 정수를, 두 번째 요소가 첫 번째 요소보다 적으면 양의 정수를, 둘이 같으면 0을 리턴한다.
GTree* g_tree_new (GCompareFunc key_compare_func);
비교 함수로 데이터를 전송해야 한다면 g_tree_new_with_data()를 이용해 이진 트리를 생성할 수도 있다. key_compare_data 포인터는 GCompareDataFunc가 정의한 비교 함수로 세 번째 매개변수로 전달될 것이다.
GTree* g_tree_new_with_data (GCompareDataFunc key_compare_func,
gpointer key_compare_data);
또 g_tree_new_full()을 이용해 새 트리를 생성할 수도 있는데, 해당 함수는 두 개의 추가 매개변수를 수락한다. 각각은 필요 시 키 또는 값을 소멸시키기 위해 호출되는 GDestroyNotify 함수다. 동적으로 할당된 키나 값을 사용 중이라면 이러한 함수들을 명시해야 한다. 이를 어기면 트리가 소멸 시 메모리가 손실될 것이다.
GTree* g_tree_new_full (GCompareDataFunc key_compare_func,
gpointer key_compare_data,
GDestroyNotify key_destroy_func,
GDestroyNotify value_destroy_func);
GTree는 새로운 키-값 쌍의 위치를 자동으로 계산하므로 GLib는 새 노드를 트리로 추가하는 데에 두 가지 함수만 제공하는데 그 중 하나는 g_tree_insert()다. 트리 내에 이미 키가 존재할 경우, 새 데이터가 존재하여 새 데이터가 오래된 데이터를 대체한다면 오래된 데이터는 소멸 함수를 이용해 해제될 것이다. 새 노드가 삽입되고 나면 트리는 자동으로 균형을 유지할 것이다.
void g_tree_insert (GTree *tree,
gpointer key,
gpointer value);
g_tree_replace()를 이용해 노드를 이진 트리로 추가할 수도 있다. g_tree_insert()와 비교할 때 이 함수의 유일한 차이점은 키가 이미 트리 내부에 존재할 경우 키 자체도 대체될 것이란 점이다. 키가 이미 존재하지 않는다면 트리에서 새 위치로 삽입되고, 트리는 자동으로 균형을 유지할 것이다.
때때로 트리의 구조체에 관한 기본 정보를 알고 싶을 때가 있다. 가령 g_tree_nnodes()를 이용하면 노드의 개수를 얻고, g_tree_height()를 이용하면 트리의 높이를 알아낼 수 있다. 이러한 두 개의 정보 조각을 이용하면 트리의 일반 구조체에 대해 알아낼 수 있을 것이다.
이진 트리에서 키와 연관된 값을 검색하려면 g_tree_lookup()을 호출할 필요가 있다. 키를 찾을 수 있다면 연관된 값이 리턴될 것이다. 키를 찾을 수 없다면 해당 함수는 NULL을 리턴할 것이다.
gpointer g_tree_lookup (GTree *tree,
gconstpointer key);
g_tree_lookup_extended()를 이용하는 대안적인 방법도 존재하는데, 이는 원본 키에 대한 포인터와 참조에 의해 연관된 값도 리턴할 것이다. 키를 발견할 수 없는 경우 해당 함수는 TRUE를 리턴할 것이다.
여기서 이진 트리의 큰 장점이 드러난다. 트리의 균형이 자동으로 유지되므로 리스트에 요소의 수가 많다 하더라도 키를 매우 빠른 속도로 찾을 수 있다는 점이다. 최악의 경우 노드를 찾는 데에 트리의 높이와 동일한 수만큼 비교가 이루어질 것이다.
이진 트리 내 노드마다 어떤 연산을 실행해야 한다면 g_tree_foreach()로 순회 함수를 명시해야 한다. GTraverseFunc 프로토타입은 키에 상응하는 g_tree_foreach()로부터 gpointer 매개변수, 그와 연관된 값, 사용자 데이터를 수락한다. 순회를 중단하려면 함수에서 TRUE를 리턴해야 한다. 트리의 노드는 정렬된 순서대로 순회한다.
void g_tree_foreach (GTree *tree,
GTraverseFunc func,
gpointer data);
연결 리스트와 마찬가지로 g_tree_search()를 이용해 이진 트리를 검색하는 수도 있다. 하지만 검색이 필요할 때 연결 리스트에서 이진 트리를 사용 시 주요 장점이 있다.
gpointer g_tree_search (GTree *tree,
GCompareFunc search_func,
gconstpointer value);
연결 리스트에서 요소를 찾을 때에는 매치가 발견될 때까지 모든 요소를 방문해야 할 것이다. 매치가 리스트의 마지막 노드라면 값은 리스트의 모든 요소와 비교될 것이다.
GList의 균형 이진 트리는 자동으로 정렬되므로 매치가 가능한 한 루트 노드와 가장 멀리 떨어진 leaf일 경우 최대 비교 횟수는 트리의 높이와 동일할 것이다. 트리가 32,000개 이상의 노드를 가진 경우 최대 16번의 비교만 가능하다! 이것이 바로 일치하는 데이터 구조체를 재빠르게 검색해야 할 때 균형 이진 트리를 사용해야 하는 이유다.
이진 트리를 이용 시 단점은 요소를 직접 참조하기 위해서는 노드의 키 값을 알아야 한다는 데에 있다. 특정 노드로 즉시 접근성을 얻어야 하는 경우, 색인 참조(index referencing)를 이용하는 데이터 구조체를 사용해야 한다.
리스트에서 항목을 제거해야 한다면 g_tree_remove()를 호출해야 한다. 리스트에서 키를 찾을 수 없다면 해당 함수는 TRUE를 리턴할 것이다. 노드가 제거되면 트리는 다시 균형을 잡을 것이다.
gboolean g_tree_remove (GTree *tree,
gconstpointer key);
트리의 사용이 끝나면 g_tree_destroy()를 호출해야 한다. 해당 함수는 트리를 그 모든 요소들과 함께 소멸시킬 것이다. 해당 함수가 호출되고 나면 어떠한 키나 값도 해제할 필요가 없다.
N-ary 트리
GLib가 제공하는 또 다른 트리 유형으로 n-ary 트리가 있는데, 이는 노드가 원하는 수만큼 자식 노드를 가질 수 있도록 해준다. 이러한 데이터 유형은 자동으로 균형이 유지되지 않으므로, 개발자가 그 구조체를 관리한다.
N-ary 트리는 사실 GNode 구조체의 집합체다. 그리고 각 구조체는 5개의 객체를 포함한다. 첫 번째 객체는 노드가 저장하는 실제 데이터 조각을 가리키는 포인터인 data다. GLib가 제공하는 대부분의 데이터 유형과 마찬가지로 어떤 포인터 데이터 타입이든 저장할 수 있다.
typedef struct
{
gpointer data;
GNode *next;
GNode *prev;
GNode *parent;
GNode *children;
} GNode;
다른 member들은 트리 내 다른 노드들을 가리킨다. 여기에는 동일한 수준에 위치한 다음(next) 노드, 동일한 수준에 존재하는 이전(previous) 노드, 부모 노드, 자식 노드가 있다. 이러한 관계의 이해를 돕도록 그림 6-1을 통해 단순한 연관관계를 보이겠다.
그림에는 하나의 루트 요소만 존재하며 아래에 3개의 자식을 갖고 있다. 첫 번째 자식은 또 두 개의 자식을 갖는다. 루트 노드는 첫 번째 자식만 가리키고 있다. 다른 자식으로 접근하기 위해 각 자식은 다음 자식과 이전 자식을 가리킨다. 각 자식은 부모 노드 또한 가리킬 것이다.
루트 자식과 그 두 번째 및 세 번째 자식 간에는 포인터가 없음을 눈치챌 것인데, 부모 노드가 첫 번째 자식만 가리키기 때문이다. 노드의 나머지 자식들로 접근하려면 next와 prev 포인터를 이용해야 할 것이다.
새로운 n-ary 트리는 g_node_new()를 이용해 생성되는데, 이 함수는 루트 노드가 하나인 트리를 생성한다. 처음에 새 트리에서는 모든 GNode 포인터가 NULL로 설정될 것이다. 따라서 개발자는 함수를 이용해 트리에서 모든 노드를 생성해야 한다.
GNode* g_node_new (gpointer data);
노드를 생성한 후에는 표 6-5에 표시된 함수들을 이용해 원하는 구조체로 된 트리를 구성할 수 있다.
함수 | 설명 |
g_node_append() | 노드를 부모 노드의 마지막 자식으로 추가한다. NULL의 형제(sibling) 노드로 g_node_insert_before()를 호출하는 것과 동일하다. |
g_node_append_data() | 명시된 데이터로 된 새 노드가 생성된다는 점만 제외하면 g_node_append()를 호출하는 것과 동일하다. |
g_node_insert() | 명시된 위치에 노드를 부모 노드의 자식으로 삽입한다. 위치가 -1이면 노드는 마지막 자식 다음에 추가될 것이다. |
g_node_insert_after() | 노드를 형제 노드 바로 뒤에 부모 노드의 자식 노드로 삽입한다. 형제 노드가 NULL로 설정되면 노드는 부모의 첫 번째 자식으로 앞에 추가될 것이다. |
g_node_insert_before() | 노드를 형제 노드 바로 앞에 부모 노드의 자식으로 삽입한다. 형제 노드가 NULL로 설정되면 노드는 부모의 마지막 자식으로 뒤에 추가될 것이다. |
g_node_insert_data() | 명시된 데이터로 된 새 노드가 생성된다는 점만 제외하면 g_node_insert()를 호출하는 것과 동일하다. |
g_node_insert_data_before() | 명시된 데이터로 된 새 노드가 생성된다는 점만 제외하면 g_node_insert_before()를 호출하는 것과 동일하다. |
g_node_prepend() | 노드를 부모 노드의 첫 번째 노드로 삽입한다. |
g_node_prepend_data() | 명시된 데이터로 된 새 노드가 생성된다는 점만 제외하면 g_node_prepend()를 호출하는 것과 동일하다. |
표 6-5. N-ary 트리 구성 함수 |
n-ary 트리의 구조체는 꽤 복잡해질 수도 있다. 따라서 GLib는 트리의 노드를 방문하여 각 노드마다 함수를 호출할 수 있도록 g_node_traverse()를 제공한다.
void g_node_traverse (GNode *root,
GTraverseType order,
GTraverseFlags flags,
gint max_depth,
GNodeTraverseFunc func,
gpointer data);
g_node_traverse()를 호출할 때는 먼저 검색을 시작할 루트 노드를 명시해야 한다. 꼭 트리의 루트 노드일 필요는 없다. 다음으로, 어떤 유형의 순회가 발생할지를 명시해야 하는데, 아래 리스트에 표시된 GTraverseType 열거에서 정의된다.
- G_IN_ORDER: 먼저 노드의 가장 왼쪽 자식부터 시작해 우측으로 이동한다. 비교 함수를 사용한 후에 정렬된 순서로 트리를 순회하고자 할 때 방문해야 하는 노드의 순서다.
- G_PRE_ORDER: 왼쪽과 오른쪽 하위트리 전에 루트 노드를 방문한다. 그 다음으로 하위 트리를 좌측에서 우측으로 방문한다.
- G_POST_ORDER: 노드의 자식들 다음에 루트 노드를 방문한다. 모든 노드를 방문하고, 루트 노드에서 끝날 것이다.
- G_LEVEL_ORDER: 노드, 그 모든 자식 노드, 자식의 자식 노드 식으로 방문한다. 이러한 순회 유형은 다른 것들에 비해 훨씬 효율적인데, 자연적인 재귀적 순회법을 따르지 않기 때문이다.
g_node_traverse()에서 다음 매개변수는 어떤 타입의 자식 노드를 방문할 것인지 명시하는데 이는 아래 GTraverseFlags 열거에서 정의된다.
- G_TRAVERSE_LEAVES: 자식이 없는 모든 leaf 노드를 방문한다. G_TRAVERSE_LEAFS 플래그와 같다.
- G_TRAVERSE_NON_LEAVES: 자식 노드를 가진 노드를 모두 방문한다. G_TRAVERSE_NON_FLAGS 플래그와 같다.
- G_TRAVERSE_ALL: 모든 노드를 방문한다. (G_TRAVERSE_LEAVESㅣG_TRAVERSE_NON_LEAVES의 bitwise mask와 같다.
- G_TRAVERSE_MASK: 모든 순회 플래그(traversal flag)를 포함시킨다.
g_node_traverse()의 네 번째 매개변수는 방문하게 될 루트 노드에서 시작해 자식 노드의 최대 깊이를 제공한다. 가령 깊이가 3이라면 루트 노드, 그의 자식, 자식의 자식까지만 방문할 것이다. 최대 깊이를 -1로 설정하면 모든 자식을 방문한다.
그리고 나서 순회한 노드마다 GNodeTraverseFunc 콜백을 명시해야 할 것이다. 이 함수는 g_node_traverse()에서 포인터 데이터 매개변수와 현재 노드에 해당하는 GNode를 수락한다. 순회 함수에서 TRUE가 리턴되면 순회가 중단될 것이다. FALSE를 리턴하면 아직 방문하지 않은 노드가 있을 경우 순회가 계속될 것이다.
이진 트리와 마찬가지로 n-ary 트리의 이용이 끝나면 개발자는 루트 노드에서 g_node_destroy()를 호출해야 한다. 함수를 호출하면 트리 내 자식의 자식 등 모든 요소를 재귀적으로 소멸시킬 것이다.
g_node_destroy (node_root);
GLib는 GNode 객체의 트리와 상호작용하기 위한 함수들도 많이 제공한다. GNode 객체가 필요하다면 GNode 데이터 타입에 관한 API 문서를 참조해야 한다.
배열
GLib에서 제공하는 배열 데이터 타입에는 세 가지 유형이 있는데, 이는 포인터, 바이트, 또는 임의의 데이터 타입을 저장하는 데 사용된다. GLib에서 배열을 사용 시에는 여러 가지 장점이 있다. 첫째, 직접 색인(direct indexing)이 지원되어 메모리 접근 속도가 매우 빠르다. 이는 GArray 구조체가 내부 배열(internal array)에 데이터를 저장하기 때문이다.
GLib 배열 타입의 또 다른 장점은, 새 요소의 크기가 들어맞지 않을 경우 자동으로 크기를 확장시킨다는 점이다. 하지만 개발자가 배열 내 요소의 개수를 변경할 때마다 g_memmove()와 memcpy()가 호출되기 때문에 너무 자주 사용하면 수고가 많이 들게 됨을 명심해야 한다. 따라서 GLib 배열은 지속적으로 요소를 추가하거나 제거해야 하는 애플리케이션은 선택할 수 없다.
GArray
GLib가 제공하는 세 가지 타입의 배열은 각각 비슷한 API를 갖는다. 따라서 본문에서는 GArray만 상세히 다루겠다. GPtrArray와 GByteArray에 관한 상세한 정보는 본문에 실린 지침을 비롯해 각 데이터 타입의 API 문서를 보완하여 참조하면 되겠다.
GArray 구조체는 두 개의 public member, 즉 배열이 저장한 요소 데이터에 대한 포인터와 요소 내 현재 배열의 길이를 포함한다. 배열이 저장하는 요소의 개수를 변경하면 data는 일관된 상태로 유지되지 않을지도 모른다는 사실을 기억해야 한다. 따라서 이 포인터로 영구적 참조를 만들어선 안 된다. 또한 배열 내 모든 요소는 항상 길이가 같아야 한다.
typedef struct
{
gchar *data;
guint len;
} GArray;
GLib는 새로운 GArray를 생성하기 위해 두 가지 함수를 제공한다. g_array_sized_new()는 초기 요소의 개수가 (reserved_size) 이미 할당된 배열을 생성하도록 해준다. 이는 배열을 너무 많이 재할당하지 않도록 해준다.
GArray* g_array_sized_new (gboolean zero_terminated,
gboolean set_to_zero,
guint element_size
guint reserved_size);
zero_terminated를 TRUE로 설정하면 추가로 하나의 요소가 배열에 추가되고 모든 비트는 0으로 설정된다. set_to_zero를 TRUE로 설정하면 할당 시 배열 내 모든 비트가 0이 될 것이다. 개발자는 모든 요소에 할당될 크기도 명시해야 한다. 모든 요소의 크기는 항상 element_size 이하여야 한다.
위를 대신해 초기에 0으로 할당된 크기의 요소들을 이용해 g_array_sized_new()를 호출하는 g_array_new()를 통해 새로운 GArray를 생성하는 방법도 있다. 이러한 초기화 함수는 너무 많은 요소를 배열로 추가하지 않을 때에만 사용해야 하는데, 요소를 많이 추가할 경우 여러 번 재할당되기 때문이다.
GArray* g_array_new (gboolean zero_terminated,
gboolean set_to_zero,
guint element_size);
여러 개의 새 요소를 GArray 뒤에 추가하려면 g_array_append_vals()를 호출해야 한다. 해당 함수는 data에서 len개 만큼의 요소를 배열의 끝에 추가할 것이다. 배열에 하나의 요소를 추가하려면 g_array_append_val()을 사용하면 된다. 이는 아래 매크로에 의해 정의되는데, 길이가 1일 때 g_array_append_vals()를 호출하는 것과 전혀 다를 바가 없다.
GArray* g_array_append_vals (GArray *array,
gconstpointer data,
guint len);
g_array_append_val()을 이용하면 값 매개변수를 참조하기 때문에 13과 같은 리터럴 값을 GArray로 추가할 수 없다. 요소를 배열로 추가할 때는 항상 변수를 사용해야 한다!
GLib는 값을 뒤에 추가하는 것 외에도 g_array_append_val()이나 g_array_append_vals()와 같은 방식으로 단일 값 또는 다중 값을 앞에 추가하거나 삽입하기 위한 함수를 제공한다.
#define g_array_append_val(a,v) g_array_append_vals (a, &(v), 1)
g_array_remove_index()를 이용하면 주어진 색인의 요소를 제거할 수 있다. 제거하고 나서 해당 함수는 제거된 요소 다음에 위치한 모든 요소를 한 자리 앞으로(forward) 이동시킬 것이다. 따라서 개발자는 GArray 객체의 새로운 위치를 저장해야 한다.
GArray* g_array_remove_index (GArray *array,
guint index);
g_array_remove_index_fast()를 이용하면 제거된 요소의 위치에 마지막 요소를 이동시킬 것이다. 해당 함수는 g_array_remove_index()에 비해 엄청나게 빠르지만 배열의 순서는 유지하지 않을 것이다. 따라서 이 방법은 항상 선택할 수 있는 해답은 아니다.
요소의 블록을 한 번의 호출만으로 제거하고 싶다면 g_array_remove_range()를 사용해야 한다. 해당 함수는 index부터 시작해 length 요소들을 제거하고, 그 뒤에 따른 요소들을 빈 공간으로 이동시킨다. 해당 함수는 g_array_remove_index()보다 훨씬 적은 메모리 이동을 요하므로 요소를 제거할 때 가능하다면 이것을 사용하도록 한다.
GArray* g_array_remove_range (GArray *array,
guint index);
guint length);
GArray를 사용할 때 개발자는 색인별로 요소에 접근할 가능성이 가장 크다. 이러한 데이터 구조체의 한 가지 장점은 색인(indexing)이 매우 빠르게 실행된다는 점인데, 요소의 크기가 균일하기 때문이다. g_array_index()를 이용해 요소를 색인할 수 있는데, 이는 GArray 객체, 리턴값의 형변환에 사용될 데이터 타입, 요소 색인을 수락한다. 리턴된 값은 개발자가 두 번째 매개변수에 제공한 데이터 타입으로 자동 형변환될 것이다.
GLib의 다른 데이터 타입과 마찬가지로 g_array_sort()를 이용해 GArray를 정렬할 수 있다. 해당 함수는 두 개의 요소를 비교하는 데 사용되는 표준 GCompareFunc 콜백을 수락한다. 추가적으로 포인터 데이터 매개변수를 비교 함수로 전송하는 데에는 g_array_sort_data()를 이용할 수 있다.
void g_array_sort (GArray *array,
GCompareFunc compare_func);
GArray 객체의 이용이 끝나면 g_array_free()를 이용해 해제시켜야 한다. 다른 데이터 구조체와 마찬가지로 배열이 만일 동적으로 할당된 메모리를 포함할 경우 해당 함수를 호출하기 전에 객체를 해제해야 한다.
gchar* g_array_free (GArray *array,
gboolean free_segment);
free_segment를 true로 설정하면 요소 메모리도 해제되고 함수는 NULL을 리턴할 것이다. 그 외의 값으로 설정하면 함수는 내부 요소 배열을 리턴할 것이다. 이에 따라 GArray 객체가 해제된 후에도 객체를 어디서든 계속 사용할 수 있다.
포인터 배열
GPtrArray는 API의 GArray와 비슷하지만 구조체가 포인터의 배열을 저장한다는 점에서 다르다. 즉, 배열 내에서 각 요소가 어떤 데이터 타입을 보유하는지는 중요하지 않기 때문에 크기가 균일하지 않아도 된다는 의미다. GPtrArray 구조체는 포인터의 내부 배열과 현재 배열의 길이를 보유한다.
typedef struct
{
gpointer *pdata;
guint len;
} GPtrArray;
요소를 GPtrArray로 삽입하는 데에는 하나의 함수 g_ptr_array_add()만 제공된다. 이 함수는 배열 요소를 리스트의 끝에 추가한다.
요소를 제거하는 방식도 매우 비슷하지만 제거에는 두 가지 함수, g_ptr_array_remove()와 g_ptr_array_remove_fast()가 제공된다. 요소를 색인별로 제거하는 대신 이 함수들은 주어진 데이터에 매치하는 요소를 제거한다. 요소를 성공적으로 발견하면 TRUE가 리턴되고 요소가 제거된다.
gboolean g_ptr_array_remove (GPtrArray *array,
gpointer data);
GPtrArray는 추가로 g_ptr_array_foreach()라는 함수를 제공하는데, 이는 배열 내 모든 요소마다 foreach_fun() 을 호출할 것이다. 해당 함수는 현재 요소와 연관된 포인터와 g_ptr_array_foreach() 사용자 데이터 매개변수를 수락한다.
void g_ptr_array_foreach (GPtrArray *array,
GFunc foreach_func,
gpointer data);
포인터 배열의 사용이 끝나면 g_ptr_array_free()를 이용해 해제한다. 이 함수는 내부 요소 배열을 해제할 것인지 리턴할 것인지에 대한 선택권을 개발자에게 맡긴다.
바이트 배열
바이트 배열은 guint8을 저장하는 GArray 타입이다. 하나의 요소를 뒤에 추가하거나 앞에 추가하도록 함수가 제공된다. GArray와는 달리 GByteArray는 요소의 삽입을 위한 함수는 제공하지 않지만 새로운 요소를 앞에 추가하거나 뒤에 추가할 수 있는 함수를 제공한다.
typedef struct
{
guint8 *data;
guint len;
} GByteArray;
삽입 함수, 그리고 다수의 요소를 앞이나 뒤에 추가하도록 해주는 함수가 없다는 점만 제외하면 GByteArray는 GArray와 정확히 동일하다. 사실 GByteArray는 구현에 내부적으로 GArray 함수를 사용한다.
바이트 배열의 사용이 끝나면 g_byte_array_free()를 이용해 해제해야 한다. 내부 바이트 배열을 해제하거나 그것이 함수에서 리턴되도록 하거나 둘 중 어떤 것을 선택하는지는 개발자에게 달려 있다. 해제하도록 명시할 경우 g_byte_array_free()는 NULL을 리턴할 것이다.
해시 테이블
해시 테이블은 요소를 매우 빠르게 검색할 수 있는 최적화된 데이터 타입이다. 데이터는 수많은 키-값 쌍으로 저장된다. 사실상 GLib에서 어떤 키나 값도 사실상 GHashTable에 의해 저장되지 않기 때문에 이러한 쌍은 해시 테이블의 수명 주기 내내 존재해야 한다.
즉, GTK+ 위젯으로부터 리턴한 것과 같은 임의의 문자열을 사용해선 안 된다는 말이다. 임의 문자열을 사용할 경우 문자열의 영구적 사본(copy)을 만들기 위해 g_strdup()을 호출해야 한다.
해시 테이블은 평균적으로 일정한 검색(lookup) 시간을 제공하기 때문에 대량의 요소를 저장 시 유용하다. 이러한 평균 시간은 요소의 개수와 무관하다. 일부 사례에서는 검색 시간이 오래 소요될 수 있지만 극히 드물다.
GLib에서 새로운 해시 테이블은 g_hash_table_new()를 이용해 생성되는데, 이는 두 개의 함수를 수락한다. 해시 함수는 키에서 새로운 해시 값을 생성하는 데 사용되는데, NULL 값도 가능하다. 두 번째 함수는 두 개의 키가 서로 동일한지 확인하는 데 사용된다.
GHashTable* g_hash_table_new (GHashFunc hash_func,
GEqualFunc key_equal_func);
해시 함수는 앞서 보았듯 GHashFunc에 대한 함수 프로토타입에 의해 정의된다. 이는 키 값을 수락하고 그에 상응하는 해시 값을 리턴한다. 개발자는 자신만의 해시 함수를 작성해도 좋지만 GLib에서는 이미 자주 사용되는 값을 세 가지 제공하고 있다. 이들은 g_direct_hash(), g_int_hash(), g_str_hash()로, 키가 각각 gpointer, gint, 문자열일 때 사용할 수 있겠다.
guint (*GHashFunc) (gconstpointer key);
키 비교 함수는 아래 GEqualFunc 프로토타입에 의해 정의된다. a와 b가 동일하면 해당 함수는 TRUE를, 동일하지 않으면 FALSE를 리턴한다. GLib는 이미 gpointer, gint, 문자열 타입의 키에 대한 비교 함수 역할을 하도록 g_direct_equal(), g_int_equal(), g_str_equal() 함수를 제공한다.
gboolean (*GEqualFunc) (gconstpointer a,
gconstpointer b);
GLib는 g_hash_table_new() 외에도 해시 테이블에서 키와 값을 제거할 때 그에 대한 소멸 콜백(destroy callback) 함수를 제공하도록 해주는 g_hash_table_new_full() 또한 제공한다.
새로운 키-값 쌍을 해시 테이블로 삽입하는 방법에는 두 가지가 있다. 첫 번째는 g_hash_table_insert()를 호출하는 방법이다. 테이블 내에 이미 키가 존재한다면 value가 현재 값을 대체할 것이다. 또 다른 방법은 g_hash_table_replace()를 이용하는 방법으로, 앞의 것과 동일한 기능을 제공한다. 하지만 테이블 내에 이미 키가 존재할 경우 키 객체와 값 객체가 모두 대체될 것이다.
void g_hash_table_insert (GHashTable *hash_table,
gpointer key,
gpointer value);
해시 테이블에서 키-값 쌍을 제거할 때는 g_hash_table_remove()를 이용한다. 키와 값에 대한 소멸 함수를 제공했다면 이 때 호출될 것이다. 개발자가 이러한 함수를 제공하지 않았다면 동적으로 할당된 데이터를 개발자가 직접 소멸하도록 확보해야 한다. 키가 성공적으로 제거되면 해당 함수는 TRUE를 리턴할 것이다. g_hash_table_remove_all()을 이용해 해시 테이블에서 모든 키-값 쌍을 제거할 수도 있다.
gboolean g_hash_table_remove (GHashTable *hash_table,
gconstpointer key);
앞서 언급하였듯 해시 테이블을 이용 시 한 가지 장점은 요소의 개수와 상관없이 값의 검색 시간이 일정하다는 데에 있다. g_hash_table_lookup()으로 주어진 키에 해당하는 값을 검색할 수 있다. 연관된 값이 리턴되겠지만, 키를 찾을 수 없다면 NULL이 리턴될 것이다.
gpointer g_hash_table_lookup (GHashTable *hash_table,
gconstpointer key);
그 외에도 g_hash_table_lookup_extended()라는 함수를 호출할 수도 있다. GHashTable에서 키가 발견되면 TRUE를 리턴한다. 이후 원본 키와 값을 설정할 것인데, 이들은 함수로 전송되는 두 개의 추가 매개변수에 해당한다.
해시 테이블의 이용이 끝나면 g_hash_table_destroy()를 이용해 해제되어야 한다. 키와 값에 대한 소멸 함수를 제공했다면 이 때 각 객체마다 호출될 것이다.
void g_hash_table_destroy (GHashTable *hash_table);
이 함수가 유일한 이유는 모든 키와 값을 소멸시킬 뿐만 아니라 참조 계수를 하나씩 감소시키기 때문이다. 해시 테이블의 참조 계수는 g_hash_table_ref()와 g_hash_table_unref()를 이용해 증가 및 감소시킬 수 있다. 따라서 참조 계수가 0에 도달하지 않은 경우 해시 테이블은 여전히 객체로서 존재할 수 있다.
쿼크
쿼크(Quark)는 32 비트 정수와 문자열 간의 양방향 연관관계를 의미한다. 다시 말해 개발자는 정수를 이용해 문자열로 접근하고, 문자열을 이용해 정수로 접근할 수 있다는 말이다. 쿼크는 런타임 시 계산되며, 애플리케이션에서 전역적으로 이용이 가능하다. 자신의 프로그램에서 어느 시점에서든 새로운 쿼크를 설정 가능하며, 애플리케이션의 어떤 측면에서든 이용 가능하다.
쿼크는 내부적으로 문자열의 배열과 해시 테이블로서 구현된다. 쿼크 자체는 배열의 색인으로 사용되는 32 비트 정수로, 연관된 문자열을 검색 시 사용된다. 문자열은 해시 테이블에서 쿼크를 찾을 때 사용된다. 따라서 모든 문자열과 정수는 유일해야(unique) 한다.
typedef guint32 GQuark;
주어진 문자열로 된 GQuark를 얻으려면 g_quark_from_string()을 이용한다. 문자열이 GQuark과 이미 연관되지 않은 경우 문자열의 복사본을 이용하면 새로운 연관관계가 생성될 것이다. 문자열이 항상 존재할 것이라고 확신할 경우 g_quark_from_string_static()을 이용하면 문자열의 복사본 대신 문자열 자체를 이용할 것이다.
GQuark g_quark_from_string (const gchar *string);
문자열에 연관된 쿼크가 있는지 확인하려면 g_quark_try_string()을 이용하면 된다. 이 함수는 문자열을 수락하고 연관된 GQuark를 리턴한다. 문자열이 추가되었다면 0을 리턴할 것이다.
문자열이 전역적 테이블 내에 존재한다면 g_quark_to_string()을 이용해 쿼크에서 검색할 수 있다. 쿼크가 존재하지 않으면 해당 함수는 NULL을 리턴할 것이다.
const gchar* g_quark_to_string (GQuark quark);
키가 있는 데이터 리스트
키가 있는 데이터 리스트는 연결 리스트의 특수 유형으로, 색인에 쿼크를 사용한다. GData에는 public member가 없다. GData의 private member로는 쿼크, 데이터 포인터, 노드를 제거 시 호출하는 선택적 소멸 함수, 다음 노드를 가리키는 포인터가 있다.
데이터 리스트는 다른 임의의 데이터 타입을 저장하기 위해 쿼크 관계를 이용하는데, 이는 문자열과 쿼크 중 하나를 명시함으로써 검색이 가능하다. 키가 있는 데이터 리스트는 g_datalist_init()를 이용해 빈 문자열로 초기화된다. GData 객체가 NULL이 아닌 경우 해당 함수는 실패할 것이다.
void g_datalist_init (GData **datalist);
GLib는 데이터 리스트로 요소를 추가하고 그로부터 제거하는 데 사용할 수 있는 함수를 대량으로 제공하지만 그 중 대부분은 단순히 g_datalist_id_set_data_full()에 대한 호출로 정의된다. key_id가 이미 데이터 리스트에 존재한다면 이전 데이터가 제거되고 새로운 포인터로 대체될 것이다.
void g_datalist_id_set_data_full (GData **datalist,
GQuark key_id,
gpointer data,
GDestroyNotify destroy_func);
위의 함수는 노드가 제거되면 호출되어야 할 GDestroyNotify 콜백 함수도 명시할 수 있도록 해준다. GDestroyNotify 콜백 함수는 노드의 데이터 member를 향하는 포인터를 수락한다.
g_datalist_id_set_data_full()을 이용해 데이터 매개변수로 NULL을 명시하여 데이터 리스트에서 요소를 제거하는 수도 있다. 여러 가지의 프로토타입으로 g_datalist_id_set_data_full()이 제공하는 기능을 래핑하는 다른 함수들도 많이 제공된다.
g_datalist_id_set_data_full()을 이용해 항목을 제거할 때 소멸 알림(destroy-notify) 콜백 함수가 설정되어 있다면 그것이 실행될 것이다. 하지만 이러한 함수의 호출을 방지하고 싶다면 g_datalist_id_remove_no_notify()를 이용해 노드를 제거할 수 있다. 이 함수는 노드를 제거하고(이 부분에 'This function will the node'라고 되어 있는데, 동사가 빠져 있습니다. 문맥상 '제거하다'라고 번역하겠습니다.) 해당 위치로 명시되어 저장된 데이터를 리턴할 것이다.
gpointer g_datalist_id_remove_no_notify (GData **datalist,
GQuark key_id);
데이터 리스트의 모든 노드를 순회해야 할 경우 g_datalist_foreach()를 이용해야 한다. GDataForeachFunc 프로토타입은 g_datalist_foreach()에 명시된 데이터 포인터를 비롯해 하나의 노드에 대한 요소 데이터와 쿼크를 수락한다.
void g_datalist_foreach (GData **datalist,
GDataForeachFunc func,
gpointer user_data);
키가 있는 데이터 리스트의 사용이 끝나면 g_datalist_clear()를 이용해 모든 요소를 삭제해야 한다. 그래야만 다음 사용을 위해 리스트를 준비시킬 수 있다. 이 때 데이터 리스트에서 소멸 함수를 호출하면 메모리를 소모하므로 호출할 필요가 없다.
void g_datalist_clear (GData **datalist);
요소가 제거되는 동안 소멸 함수를 명시할 경우 데이터 매개변수에서 소멸 함수가 호출될 것이다.
입출력 채널
GIOChannel 구조체는 파일, 파이프, 소켓을 처리할 수 있도록 해준다. 지금부터 여러 절에 걸쳐 파일 및 파이프와 함께 구조체를 이용하는 방법을 다룰 것이다. 유사 UNIX 운영체제에서 작업할 때는 파이프와 관련된 절에서 다루었던 방법만 사용할 것인데, Windows에서는 파일 디스크립터와 소켓 영역이 겹치기 때문이다.
"프로세스 띄우기" 부분에서는 프로세스를 띄우는 대안적 방법을 제공하고, 동기식 프로세스와 비동기식 프로세스를 모두 다룰 것이다.
GIOChannels와 파일
새로운 입출력(IO) 채널을 생성하는 한 가지 방법으로 g_io_channel_new_file()의 이용을 들 수 있다. 이 방법은 UNIX 파일 디스크립터를 사용해야 하는 필요성을 없애기 때문에 UNIX가 아닌 운영체제에서 안전하게 사용이 가능하다.
해당 함수는 새로운 파일이나 파이프를 GIOChannel로 연다. 리스팅 6-8에서는 g_io_channel_new_file() 함수를 두 번 사용하고 있다. 첫 번째는 몇몇 초기 텍스트와 함께 파일이 생성된다. 이후 두 번째 채널이 파일을 열어 그 내용을 읽고 텍스트를 표준 출력으로 결과를 내보낸다. 파일 유틸리티 함수를 이용해 동일한 기능을 구현하는 리스팅 6-5와 비교해보라.
리스팅 6-8. 파일에 입출력 채널 이용하기 (files2.c)
#include <glib.h>
static void handle_error (GError*);
int main (int argc,
char *argv[])
{
gchar *filename, *content;
GIOChannel *write, *read;
GError *error = NULL;
gsize bytes;
/* Build a filename in the user's home directory. */
filename = g_build_filename (g_get_home_dir(), "temp", NULL);
/* Set the contents of the given file and report any errors. */
write
= g_io_channel_new_file (filename, "w", &error);
handle_error (error);
g_io_channel_write_chars (write, "Hello World!", -1, &bytes, NULL);
g_io_channel_close (write);
if (!g_file_test (filename, G_FILE_TEST_EXISTS))
g_error ("Error: File does not exist!\n");
/* Get the contents of the given file and report any errors. */
read = g_io_channel_new_file (filename, "r", &error);
handle_error (error);
g_io_channel_read_to_end (read, &content, &bytes, NULL);
g_print ("%s\n", content);
g_io_channel_close (read);
g_free (content);
g_free (filename);
return 0;
}
static void
handle_error (GError *error)
{
if (error != NULL)
{
g_print (error->message);
g_clear_error (&error);
}
}
g_io_channel_new_file()의 두 번째 매개변수는 모드를 명시한다. 이러한 모드는 fopen()에 명시된 것과 같은 포맷으로 된 문자열로 이루어진다. g_io_channel_new_file()이 수락하는 모드를 표 6-6에 표시하였다. g_io_channel_new_file()을 이용해 열린 파일은 G_FILE_ERROR 도메인에서 오류가 발생하기도 한다.
모드 | 설명 |
r | 파일을 읽기 전용으로 열고 포인터를 파일 시작 부분에 위치시킨다. |
w | 파일을 쓰기 전용으로 열고 포인터를 파일 시작 부분에 위치시킨다. 파일은 삭제되어 길이가 0개의 문자가 되고, 파일이 존재하지 않으면 생성된다. |
a | 파일을 쓰기 전용으로 열고 포인터를 파일의 끝 부분에 위치시켜 새로운 텍스트가 뒤에 추가된다. 파일이 존재하지 않으면 생성하라. |
r+ | 파일을 읽기와 쓰기용으로 열고 포인터를 파일의 시작 부분에 위치시킨다. |
w+ | 파일을 읽기와 쓰기용으로 열고 포인터를 파일의 시작 부분에 위치시킨다. 파일은 삭제되어 길이가 0개의 문자가 되고, 파일이 존재하지 않으면 생성된다. |
a+ | 파일을 읽기와 쓰기용으로 열고 포인터를 파일의 끝 부분에 위치시켜 새로운 텍스트가 뒤에 추가된다. 파일이 존재하지 않으면 생성하라. |
표 6-6. GIOChannel 파일 모드 |
개발자가 w, a, r+, w+, a+ 중 하나를 모드로 설정했다면 g_io_channel_write_chars()를 이용해 텍스트를 파일로 작성할 수가 있다.
GIOStatus g_io_channel_write_chars (GIOChannel *channel,
const gchar *text,
gssize size_of_buffer,
gsize *bytes_written,
GError **error);
해당 함수는 5개의 매개변수, 즉 열린 IO 채널, 파일에 작성하거나 파일 뒤에 추가할 텍스트, 텍스트 크기, 작성된 바이트 수를 저장하기 위한 정수, GError 구조체를 취한다. 텍스트 문자열이 NULL로 끝날 경우 버퍼 크기는 -1로 설정할 수 있다. 파일에 작성된 바이트 수는 함수에 의해 설정된다.
IO 채널의 이용이 끝나면 개발자는 g_io_channel_shutdown()을 이용해 종료해야 한다. 두 번째 매개변수를 TRUE로 설정하면 어떠한 보류 데이터든 제거될(flushed) 것이다. 그 외의 경우 GLib는 현재 액션을 계속하고 액션이 완료되면 채널을 종료할 것이다. 세 번째 매개변수는 어떤 GIOChannelError 타입의 오류가 발생하든 포착할 것이다.
GIOStatus g_io_channel_shutdown (GIOChannel *channel,
gboolean flush,
GError **error);
GIOChannel은 단일 문자, 전체 행, 또는 전체 파일을 읽는 데 사용 가능한 함수를 제공한다. 파일을 다룰 때에는 g_io_channel_read_to_end()를 이용해 파일의 전체 내용을 읽을 수 있다. 해당 함수는 리턴될 텍스트의 길이를 설정하기도 한다. GIOChannelError 또는 GConvertError 타입의 오류가 발생할 수 있다.
GIOStatus g_io_channel_read_to_end (GIOChannel *channel,
gchar **text,
gsize *length,
GError **error);
GIOChannels와 파이프
UNIX 운영체제에서 대부분의 객체는 파일로 취급되기 때문에 바로 앞 절에서 다룬 방법을 이용하면 파이프도 열 수가 있다. 파이프는 애플리케이션들 간 통신을 허용한다. 유일한 차이점은 개발자가 watch를 추가해야만 언제 파이프에서 데이터를 읽고 파이프로 데이터를 쓸 준비가 되었는지 알 수 있다는 점이다.
본 저서는 세부적인 지침서가 아니기 때문에 UNIX 파이프에 대한 소개는 제공하지 않는다. 해당 절의 내용을 읽고 나서 자신이 선택한 C 프로그래밍 언어에 관한 지침서에서 파이프에 관해 더 학습하길 권한다.
파이프를 이용한 프로세스 간 통신
UNIX에서 프로세스는 기본적으로 고유의 스택, 메모리 페이지, 파일 디스크립터 테이블을 가진 하나의 실행 애플리케이션이다. 프로세스가 실행될 때는 프로세스 ID(pid)라고 불리는 유일한 식별자가 주어진다. 수많은 함수와 함께 새로운 프로세스가 생성될 수 있지만 모두들 fork() 명령으로 호출한다.
여러 개의 프로세스가 동시에 동일한 애플리케이션의 인스턴스로서 실행이 가능하기 때문에 프로세스는 프로그램이 아니다. 예를 들어, 개발자는 자신의 웹 브라우저의 다중 인스턴스를 동시에 열 수 있다.
forking은 단일 프로세스를 두 개의 동일한 프로세스, 즉 부모와 자식으로 나눈다. 이후 다양한 UNIX 명령을 이용해 forked 프로세스에서 다른 애플리케이션을 실행할 수도 있지만, 본문에 실린 예제에서는 동일한 애플리케이션을 두 번 생성하도록 하겠다.
switch (fork())
{
case -1:
g_error ("Error: The fork() failed!");
exit (1);
case 0:
g_message (We are currently running the child process!");
exit (0);
default:
gint status_of_child;
wait (&status_of_child);
}
프로세스가 forked되고 나면 프로세스 식별자를 리턴한다. 이 식별자가 -1일 경우 함수가 실패했다는 의미다. 식별자가 0이면 개발자가 현재 자식 프로세스에서 실행 중임을 알려주는 셈이다. 개발자는 자식 프로세스에 원하는 기능을 실행할 수 있다. 기본적으로 주요 애플리케이션을 포착(catch)하고, 자식 프로세스가 종료될 때까지 기다린다.
개발자가 애플리케이션을 fork할 때는 보통 자식 프로세스와 통신하는 방법을 원하는데 이 때 파이프가 사용된다. 파이프는 pipe() 명령을 호출하여 만들어진다. pipe()는 두 개의 정수로 된 배열을 수락하고, 성공 시 0을, 실패 시 -1을 리턴한다. 초기화가 끝나면 배열에서 첫 번째 정수는 읽기(read) 파이프를, 두 번째 정수는 쓰기(write) 파이프를 참조한다.
자식 프로세스와 부모 프로세스 간 양방향 통신을 원한다면 두 개의 파이프 집합을 만들어야 한다. 하나의 파이프에 데이터를 쓰면 애플리케이션의 다른 인스턴스에 의해 읽히는데, 그 반대도 가능하다. 우리 예제에서는 프로세스를 forking하고 파이프를 생성하는 UNIX의 방식을 사용하겠지만 파이프와 상호작용하는 데 제공되는 GLib의 함수도 사용할 것이다.
리스팅 6-9는 GIOChannel 구조체가 제공하는 함수와 함께 UNIX가 파이프를 생성 시 사용하는 방법을 이용한다. 이를 위해 watch가 생성되었다.
watch는 스스로를 GLib의 메인 루프로 통합하여 발생할 이벤트를 기다리기 때문에 시그널과 같다. 이후 콜백함수를 호출할 것이다. Watch 이벤트의 유형으로는 파이프가 읽을 준비가 된 데이터를 갖고 있을 때, 그리고 쓸 준비가 된 데이터를 수락할 때를 들 수 있겠다.
리스팅 6-9는 GtkEntry 위젯을 이용해 부모 및 자식 프로세스를 생성한다. 둘 중 하나의 엔트리 위젯에 입력을 시도할 경우 새로운 내용이 파이프로 작성된다. 그러면 나머지 엔트리는 첫 엔트리와 동일한 내용을 갖도록 설정된다.
파이프가 준비되고 애플리케이션이 UNIX에 특정적인 방식으로 fork되었음을 눈치챌 것이다. 다음 절에서는 파이프를 준비시키고 여러 플랫폼에 걸쳐 지원되는 애플리케이션을 fork하는 방법을 보이겠다.
리스팅 6-9. 파이프에 입출력 채널 이용하기 (iochannels.c)
#include <gtk/gtk.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
static void entry_changed (GtkEditable*, GIOChannel*);
static void setup_app (gint input[], gint output[], gint pid);
static gboolean iochannel_read (GIOChannel*, GIOCondition, GtkEntry*);
gulong signal_id = 0;
int main (int argc,
char* argv[])
{
gint child_to_parent[2], parent_to_child[2], pid, ret_value;
/* Set up read and write pipes for the child and parent processes. */
ret_value = pipe (parent_to_child);
if (ret_value == -1)
{
g_error ("Error: %s\n", g_strerror (errno));
exit (1);
}
ret_value = pipe (child_to_parent);
if (ret_value == -1)
{
g_error ("Error: %s\n", g_strerror (errno));
exit (1);
}
/* Fork the application, setting up both instances accordingly. */
pid = fork ();
switch (pid)
{
case -1:
g_error ("Error: %s\n", g_strerror (errno));
exit (1);
case 0:
gtk_init (&argc, &argv);
setup_app (parent_to_child, child_to_parent, pid);
break;
default:
gtk_init (&argc, &argv);
setup_app (child_to_parent, parent_to_child, pid);
}
gtk_main ();
return 0;
}
/* Set up the GUI aspects of each window and setup IO channel watches. */
static void
setup_app (gint input[],
gint output[],
gint pid)
{
GtkWidget *window, *entry;
GIOChannel *channel_read, *channel_write;
window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
entry = gtk_entry_new ();
gtk_container_add (GTK_CONTAINER (window), entry);
gtk_container_set_border_width (GTK_CONTAINER (window), 10);
gtk_widget_set_size_request (window, 200, -1);
gtk_widget_show_all (window);
/* Close the unnecessary pipes for the given process. */
close (input[1]);
close (output[0]);
/* Create read and write channels out of the remaining pipes. */
channel_read = g_io_channel_unix_new (input[0]);
channel_write = g_io_channel_unix_new (output[1]);
if (channel_read == NULL || channel_write == NULL)
g_error ("Error: The GIOChannels could not be created!\n");
/* Watch the read channel for changes. This will send the appropriate data. */
if (!g_io_add_watch (channel_read, G_IO_IN | G_IO_HUP,
iochannel_read, (gpointer) entry))
g_error ("Error: Read watch could not be added to the GIOChannel!\n");
signal_id = g_signal_connect (G_OBJECT (entry), "changed",
G_CALLBACK (entry_changed),
(gpointer) channel_write);
/* Set the window title depending on the process identifier. */
if (pid == 0)
gtk_window_set_title (GTK_WINDOW (window), "Child Process");
else
gtk_window_set_title (GTK_WINDOW (window), "Parent Process");
}
/* Read the message from the pipe and set the text to the GtkEntry. */
static gboolean
iochannel_read (GIOChannel *channel,
GIOCondition condition,
GtkEntry *entry)
{
GIOStatus ret_value;
gchar *message;
gsize length;
/* The pipe has died unexpectedly, so exit the application. */
if (condition & G_IO_HUP)
g_error ("Error: The pipe has died!\n");
/* Read the data that has been sent through the pipe. */
ret_value = g_io_channel_read_line (channel, &message, &length, NULL, NULL);
if (ret_value == G_IO_STATUS_ERROR)
g_error ("Error: The line could not be read!\n");
/* Synchronize the GtkEntry text, blocking the changed signal. Otherwise, an
* infinite loop of communication would ensue. */
g_signal_handler_block ((gpointer) entry, signal_id);
message[length-1] = 0;
gtk_entry_set_text (entry, message);
g_signal_handler_unblock ((gpointer) entry, signal_id);
return TRUE;
}
/* Write the new contents of the GtkEntry to the write IO channel. */
static void
entry_changed (GtkEditable *entry,
GIOChannel *channel)
{
gchar *text;
gsize length;
GIOStatus ret_value;
text = g_strconcat (gtk_entry_get_text (GTK_ENTRY (entry)), "\n", NULL);
/* Write the text to the channel so that the other process will get it. */
ret_value = g_io_channel_write_chars (channel, text, -1, &length, NULL);
if (ret_value = G_IO_STATUS_ERROR)
g_error ("Error: The changes could not be written to the pipe!\n");
else
g_io_channel_flush (channel, NULL);
}
IO 채널 준비하기
유사 UNIX 머신에서 작업 중이라면 새로운 파일 디스크립터를 생성하는 데에 pipe() 함수를 이용할 수 있다. 리스팅 6-9에서는 두 쌍의 파이프가 준비되었는데, 하나는 부모 프로세스에서 자식 프로세스로, 나머지 하나는 자식에서 부모로 메시지를 전송하는 데 사용된다. 부모와 자식 프로세스에서 아래 함수를 호출하면 이러한 파일 디스크립터로부터 두 개의 GIOChannels를 생성할 수 있다.
파이프가 생성되고 나면 애플리케이션은 fork()를 이용해 fork된다. fork가 성공하면 자식 및 부모 프로세스를 위한 애플리케이션이 준비된다.
setup_app() 내에서 우리는 close()를 이용해 자식 또는 부모 애플리케이션이 필요로 하지 않는 파이프를 먼저 닫을 것이다. 각 프로세스는 메시지를 전송하고 수신하는 데 필요한 읽기 파이프 하나와 쓰기 파이프 하나만 필요로 할 것이다.
그 다음으로 각 애플리케이션에 남은 두 개의 파이프를 이용해 각각에 대한 GIOChannel 을 준비한다. 다른 프로세스로부터 데이터를 수신하기 위해서는 channel_read를, GtkEntry의 새로운 내용을 전송하기 위해서는 channel_write를 사용할 것이다.
channel_read = g_io_channel_unix_new (input[0]);
channel_write = g_io_channel_unix_new (output[1]);
자신의 IO 채널을 초기화하고 나면 channel_read에서 watch를 준비시켜야 한다. watch는 g_io_add_watch()를 이용해 명시된 이벤트에 대한 채널을 감시할 것이다.
guint g_io_add_watch (GIOChannel *channel,
GIOCondition condition,
GIOFunc func,
gpointer data);
g_io_add_watch()의 두 번째 매개변수는 살펴보아야 하는 이벤트를 하나 또는 그 이상 추가한다. 각 채널마다 올바른 조건을 준비하도록 확보해야 한다. 데이터 쓰기에 사용된 채널에서 G_IO_IN 이벤트를 얻을 수는 없을 것이므로 해당 이벤트를 감시하는 것은 쓸모 없는 일이다. GIOCondition 열거에 가능한 값은 아래와 같으며, g_io_add_watch()의 조건 매개변수로 pipe될 수 있다.
- G_IO_IN: 읽기 데이터가 보류 중이다.
- G_IO_OUT: 블록킹을 염려하지 않고 데이터를 쓸 수 있다.
- G_IO_PRI: 읽기 데이터가 보류 중이며 긴급하다.
- G_IO_ERR: 오류가 발생하였다.
- G_IO_HUP: 연결이 끊기거나 단절되었다.
- G_IO_NVAL: 첫 번째 디스크립터가 열리지 않아 유효하지 않은 요청이 발생하였다.
명시된 조건들 중 하나가 발생하면 GIOFunc 콜백 함수가 호출된다. 마지막 매개변수는 콜백 함수로 전달될 데이터를 제공한다. IO 채널 콜백 함수는 세 개의 매개변수, 즉 GIOChannel, 발생한 조건, g_io_add_watch()로부터 전달된 데이터를 수신한다. 개발자가 제거를 원하지 않는 한 콜백 함수에서는 항상 TRUE가 리턴된다. 함수 프로토타입은 아래와 같다.
gboolean (*GIOFunc) (GIOChannel *source, GIOCondition condition, gpointer data);
GIOChannel로부터 읽고 쓰는 일은 그 대상이 파일이든 파이프든 상관없이 동일한 방식으로 이루어진다. 따라서 앞의 절에서 다룬 g_io_channel_read_(*)와 g_io_channel_write_*() 함수를 계속 이용할 수 있겠다.
GIOChannel 함수들 중 다수는 오류 검사에 두 가지 방식을 제공한다. 첫 번째는 앞의 여러 장에 걸쳐 사용한 GError 구조체다. 두 번째로는 많은 함수들이 GIOStatus 값을 리턴하는데, 가능한 값으로는 아래 네 가지가 있다.
- G_IO_STATUS_ERROR: 특정 타입의 오류가 발생하였다. 이 값을 확인하는 중에도 오류를 추적해야 한다.
- G_IO_STATUS_NORMAL: 액션이 성공적으로 완료되었다.
- G_IO_STATUS_EOF: 파일의 끝에 도달하였다.
- G_IO_STATUS_AGAIN: 자원을 임시적으로 이용할 수 없다. 나중에 다시 시도한다.
GIOStatus 값에 따라 개발자는 계속할 수도 있고 오류 메시지를 제공해야 할 수도 있다. 유일한 예외로 G_IO_STATUS_AGAIN의 경우 개발자가 메인 루프에서 poll()을 리턴하고 파일 디스크립터가 준비될 때까지 기다려야 한다.
데이터를 읽기 버퍼로 전송하기 위해서는 g_io_channel_flush()를 이용해 GIOChannel의 쓰기 버퍼를 지워야(flush) 한다. 이번 절에서 다룬 모든 함수들과 함께 이 함수는 GIOChannelError 타입의 오류를 야기하기도 한다.
GIOStatus g_io_channel_flush (GIOChannel *channel,
GError **error);
프로세스 띄우기
앞 절에서 소개한 GIOChannel 예제에서는 애플리케이션 간 통신을 준비하는 데에 pipe()와 fork()를 사용했다. 하지만 이 예제에서 몇몇 명령은 Microsoft Windows에서 지원하지 않기 때문에 크로스 플랫폼적이라 할 수 없다.
여러 플랫폼에서 지원하는 방식으로 프로세스를 띄우기 위해 GLib는 3개의 함수를 제공한다. 세 가지 모두 비슷한 방식으로 작동하므로 g_spawn_async_with_pipies()라는 함수만 설명하겠다.
gboolean g_spawn_async_with_pipes (const gchar *working_directory,
gchar **argv,
gchar **envp,
GSpawnFlags flags,
GSpawnChildSetupFunc child_setup,
gpointer data,
GPid *child_pid,
gint *standard_input,
gint *standard_output,
gint *standard_error,
GError **error);
해당 함수는 자식 프로그램을 비동기식으로 실행하므로, 자식 프로그램이 종료되지 않아도 프로그램은 계속 실행할 것이다. 첫 번째 매개변수는 자식 프로세스에 대한 작업 디렉터리를 명시하거나 부모 프로그램의 작업 디렉터리로 설정하려면 NULL을 명시한다.
argv 리스트는 NULL로 끝나는 문자열의 배열이다. 리스트의 첫 번째 문자열은 애플리케이션명이고, 그 뒤에 추가 매개변수가 따라온다. 개발자가 본문 뒷부분에서 살펴볼 G_SPAWN_SEARCH_PATH 플래그를 사용하지 않는 이상 애플리케이션은 전체 경로로 되어야 한다. 또 다른 NULL로 끝나는 문자열의 배열로 envp이라는 것이 있는데, 각각이 KEY=VALUE 형태로 되어 있다. 이들은 자식 프로그램의 환경 변수로서 설정될 것이다.
이제 개발자는 다음 중 하나 또는 그 이상의 GSpawnFlags 를 명시할 수 있다.
- G_SPAWN_LEAVE_DESCRIPTORS_OPEN: 자식이 부모의 open file 디스크립터를 상속할 것이다. 이 플래그를 설정하지 않으면 표준 입력과 출력, 오류를 제외한 모든 파일 디스크립터가 닫힐 것이다.
- G_SPAWN_DO_NOT_REAP_CHILD: 자식이 자동으로 reap되지 못하도록 한다. waitpid()를 호출하거나 SIGCHLD를 처리하지 않으면 zombie가 될 것이다.
- G_SPAWN_SEARCH_PATH: 이 플래그를 설정할 경우, argv[0]가 절대적 위치가 아니라면 이것이 사용자의 경로에서 검색될 것이다.
- G_SPAWN_STDOUT_TO_DEV_NULL: 자식으로부터 표준 출력을 제거한다. 이 플래그를 설정하지 않으면 부모의 표준 출력과 동일한 위치로 갈 것이다.
- G_SPAWN_STDERR_TO_DEV_NULL: 자식에 대한 표준 오류를 제거한다.
- G_SPAWN_CHILD_INHERITS_STDIN: 이 플래그를 설정하지 않으면 자식에 대한 표준 입력이 /dev/null로 추가된다. 이 플래그를 이용해 자식이 부모의 표준 입력을 상속하도록 할 수 있다.
- G_SPAWN_FILE_AND_ARGV_ZERO: 첫 번째 인자를 실행 파일로 이용하고 나머지 문자열만 실제 인자로서 전달한다. 이 플래그를 설정하지 않으면 argv[0] 또한 실행 파일로 전달될 것이다.
g_spawn_async_with_pipes()의 그 다음 매개변수는 GSpawnChildSetupFunc 콜백 함수로, GLib가 파이프를 준비시킨 후에 실행되겠지만 exec()를 호출하기 전에 실행될 것이다. 해당 함수는 g_spawn_async_with_pipes()로부터 data 매개변수를 수락한다.
그 다음 네 가지 매개변수는 새로운 자식 프로세스에 관한 정보를 검색하도록 해준다. 이는 자식의 프로세스 식별자, 표준 입력, 표준 출력, 표준 오류가 해당된다. 네 가지 매개변수 중에 무시하고 싶은 것이 있다면 NULL로 설정하면 되겠다.
애플리케이션이 성공적으로 시작되면 g_spawn_async_with_pipes()는 TRUE를 리턴할 것이다. 그 외의 경우 GSpawnError 도메인에서 오류가 설정되고 FALSE를 리턴할 것이다.
GPid의 사용을 완료하면 g_spawn_close_pid()를 이용해 닫아야 한다. 이는 Microsoft Windows에서 프로세스를 띄울 때 특히 중요하다.
void g_spawn_close_pid (GPid pid);
동적 모듈
GLib가 제공하는 한 가지 매우 유용한 기능으로 라이브러리를 동적으로 로딩하여 GModule 구조체를 이용해 그러한 라이브러리로부터 함수를 명시적으로 호출할 수 있는 기능을 들 수 있다. 이러한 기능은 플랫폼 간 동일한 방식으로 실행되지는 않지만 동적 라이브러리를 위한 크로스 플랫폼적 해답이 있다면 훨씬 수월할 것이다. 이 기능 자체만으로도 플러그인 시스템의 생성을 가능하게 한다. 리스팅 6-10에서 간단한 이론적 플러그인 시스템이 생성될 것이다.
예제는 두 개의 파일로 나뉘는데, 하나는 플러그인용이고, 나머지 하나는 메인 애플리케이션이 된다. 이러한 애플리케이션을 실행하려면 우선 modules-plugin.c를 컴파일하고 라이브러리로서 연결할 필요가 있겠다. 아래의 두 행을 이용해 라이브러리를 생성하고 표준 위치로 설치한다.
gcc -shared modules-plugin.c -o plugin.so `pkg-config --libs glib-2.0` \
`pkg-config --cflags glib-2.0`
sudo mv plugin.so /usr/lib
라이브러리는 보통 GNU 링커(ld)에 의해 생성되지만 -shared 플래그를 이용 시 GCC는 공유 라이브러리를 생성할 수 있다. 그리고 일부 시스템에서는 플러그인 라이브러리가 등록되려면 그것을 이동시킨 후 ldconfig를 실행해야 한다. 이는 GModule을 이용해 로딩하는 목적 이외에 라이브러리를 사용하고자 할 때 필요하겠다.
리스팅 6-10. 플러그인 (modules-plugin.c)
#include <glib.h>
#include <gmodule.h>
G_MODULE_EXPORT gboolean
print_the_message (gpointer data)
{
g_printf ("%s\n", (gchar*) data);
return TRUE;
}
G_MODULE_EXPORT gboolean
print_another_one (gpointer data)
{
g_printf ("%s\n", (gchar*) data);
return TRUE;
}
플러그인 소스는 메인 애플리케이션이 로딩하는 하나 또는 그 이상의 함수만 포함한다. 따라서 플러그인의 소스 파일에 main() 함수를 포함시킬 이유가 없다.
플러그인 파일에서 중요한 한 가지 측면은, 개발자가 내보내고자 하는 함수 앞에 G_MODULE_EXPORT를 포함시켜야 한다는 것이다. 해당 매크로를 사용하지 않으면 GModule은 라이브러리로부터 함수를 로딩할 수 없을 것이다.
라이브러리에서 동적으로 로딩된 함수를 심볼(symbol)이라 부른다. 심볼은 라이브러리 내 함수를 가리키는 포인터에 불과하다. 심볼 함수는 다른 여느 함수를 호출할 때와 같은 방식으로 호출할 수 있다. 유일한 차이는, 함수를 호출하면 GLib가 라이브러리에서 실제 함수를 검색하고 그 곳에서 실행한다는 점이다.
이러한 방법을 이용 시 장점은 다수의 애플리케이션이 동시에 라이브러리를 로딩할 수 있다는 것이다. 다수의 애플리케이션에서 로딩되는 것을 허용하는 라이브러리를 공유 라이브러리라고 부른다. Linux에서 컴파일되는 라이브러리 대부분이 바로 공유 라이브러리다.
리스팅 6-11에서 메인 파일을 컴파일할 때는 수정된 컴파일 행도 사용해야 하는데, GModule 라이브러리에 대해서도 연결해야 하기 때문이다.
gcc modules.c -o modules `pkg-config --cflags --libs glib-2.0` \
`pkg-config --cflags --libs gmodule-2.0`
GModule은 컴파일 명령에 `pkg-config --cflags --libs gmodule-2.0`을 추가하면 쉽게 포함된다. 아래의 예제에서는 방금 생성하여 설치한 라이브러리를 로딩하는 방법을 설명하고 있다. 리스팅 6-11은 리스팅 6-10의 동적 모듈을 활용하는 애플리케이션이다.
리스팅 6-11. 플러그인 로딩하기 (modules.c)
#include <gmodule.h>
#include <glib.h>
typedef gboolean (* PrintMessageFunc) (gpointer data);
typedef gboolean (* PrintAnotherFunc) (gpointer data);
int main (int argc,
char *argv[])
{
GModule *module;
PrintMessageFunc print_the_message;
PrintAnotherFunc print_another_one;
gchar *text = "This is some text";
/* Make sure module loading is supported on the user's machine. */
g_assert (g_module_supported ());
/* Open the library and resolve symbols only when necessary. Libraries on
* Windows will have a .dll appendix. */
module = g_module_open ("/usr/lib/plugin.so", G_MODULE_BIND_LAZY);
if (!module)
{
g_error ("Error: %s\n", (gchar*) g_module_error ());
return -1;
}
/* Load the print_the_message() function. */
if (!g_module_symbol (module, "print_the_message",
(gpointer*) &print_the_message))
{
g_error ("Error: %s\n", (gchar*) g_module_error ());
return -1;
}
/* Load the destroy_the_evidence() function. */
if (!g_module_symbol (module, "print_another_one",
(gpointer*) &print_another_one))
{
g_error ("Error: %s\n", (gchar*) g_module_error ());
return -1;
}
/* Run both loaded functions since there were no errors reported loading
* neither the module nor the symbols. */
print_the_message ((gpointer) text);
print_another_one ("Another Message!");
/* Close the module and free allocated resources. */
if
(!g_module_close (module))
g_error ("Error: %s\n", (gchar*) g_module_error ());
return 0;
}
모든 플랫폼이 GModule 구조체를 지원하는 것은 아니다. 따라서 다중 플랫폼용으로 컴파일될 애플리케이션을 생성 중이라면 지원 여부를 확신할 필요가 있겠다.
GModule의 지원은 g_module_supported()를 이용해 확인 가능하며, 기능을 이용할 수 있을 경우 TRUE가 리턴된다. 모듈 열기가 실패하면 함수는 NULL을 리턴한다. 하지만 실패 전에 함수가 로딩될 라이브러리를 찾기 위해 주어진 라이브러리명으로 된 여러 포맷을 시도할 것이다. 시스템의 기본 라이브러리 접두사인 G_MODULE_SUFFIX를 명시된 경로 앞에 추가하는 일도 이에 포함된다.
GModule* g_module_open (const gchar *library,
GModuleFlags flags);
g_module_open()에서 두 번째 매개변수는 하나 또는 그 이상의 모듈 플래그를 명시하는데, 이는 심볼의 처리 방법을 GModule에게 지시한다. 현재 이용 가능한 GModuleFlags 열거 값은 세 가지가 있다.
- G_MODULE_BIND_LAZY: 모듈이 로딩될 때 기본적으로 심볼이 모두 바인딩되어야 한다. 하지만 GLib에게는 필요할 때만 심볼을 결정(resolve)하도록 알린다.
- G_MODULE_BIND_LOCAL: 대부분의 시스템에서 기본값과 달리 여기서는 전역적 네임스페이스에 심볼을 위치시키지 않는다.
- G_MODULE_BIND_MASK: 모든 GModule 플래그를 가린다(mask).
애플리케이션에서 언제든 g_module_error()를 호출하면 마지막으로 발생한 오류를 설명하는 문자열을 사람이 읽을 수 있도록 리턴할 것이다. 어떤 함수든 예상치 못한 값을 리턴할 경우 이 메시지를 화면으로 출력하는 것이 좋겠다.
모듈이 성공적으로 로딩되면 g_module_symbol()을 이용해 라이브러리에 어떤 함수든 로딩이 가능한데, 이러한 함수들은 G_MODULE_EXPORT를 이용해 이용 가능하다. 심볼이 성공적으로 로딩되면 함수는 TRUE를 리턴할 것이다.
gboolean g_module_symbol (GModule *module,
const gchar *symbol_name,
gpointer *symbol);
g_module_symbol()의 두 번째 매개변수는 라이브러리로부터 로딩하고자 하는 함수의 전체 이름이어야 한다. 마지막 매개변수는 메모리에서 함수를 찾아야 할 위치를 저장하는 포인터다. 개발자는 로딩된 함수와 포인터에 대한 매개변수와 리턴 값을 동일하게 명시해야 하는데, 이를 어길 경우 문제가 발생할 것이다.
애플리케이션이 종료되거나 플러그인이 언로딩되어 GModule 객체의 이용이 끝나면 g_module_close()를 호출해야 한다. 객체가 성공적으로 소멸되면 TRUE가 리턴된다.
모듈이 절대 언로딩되지 않도록 하려면 g_module_make_resident()를 호출함으로써 g_module_close()에 대한 모든 호출을 무시하는 방법이 있다. 이 함수를 호출하고 나면 모듈의 언로딩이 불가능할 것이기 때문에 사용 시 주의해야 한다!
자신의 이해도 시험하기
이번 장에는 광범위한 주제를 다루었기 때문에 학습한 내용에 대해 모두 연습문제를 제공하기란 너무 번거로울 것이다. 따라서 두 개의 연습문제에 더해 이번 장에서 학습한 다양한 주제를 이용해 자신만의 애플리케이션을 생성한다면 좋은 실습이 되겠다.
두 연습문제를 비롯해 자신만의 예제를 만든다면 뒤에 실린 내용을 훨씬 쉽게 학습할 수 있는 경험을 가질 것이다. 아래 두 예제는 파일 관리, 오류 처리, 메시지 보고, timeout 함수의 실습을 도와준다.
연습문제 6-1. 파일로 작업하기
이번 연습문제에서는 GtkEntry 위젯을 포함하는 창을 생성한다. 엔트리는 사용자가 원하는 텍스트는 무엇이든 포함할 수 있다. 창은 GtkFleChooserButton을 포함하여 사용자가 폴더를 선택하도록 해준다.
세 번째 위젯인 버튼은 창 내부에 위치해야 한다. 버튼을 클릭하면 폴더 내에서 GtkFileChooserButton이 선택한 임의의 파일에 GtkEntry로부터 얻은 텍스트가 작성된다. 연습문제에서 발생할 수 있는 오류는 모두 처리하도록 한다.
연습문제 6-1은 매우 간단하다. 우선 일반적인 GTK+ 애플리케이션을 생성해야 한다. 메인 창에는 엔트리, 파일 선택자 버튼, Save 버튼이 GtkVBox에 의해 추가 및 패킹된다. 연습문제의 해답은 부록 F에 실려 있다.
버튼이 클릭되면 개발자는 엔트리의 텍스트를 파일로 저장해야 한다. 해당 파일은 자신이 선택한 이름으로 명시된 위치에서 생성되어야 한다. 그리고 파일이 성공적으로 생성되었는지 확인하기 위해 GError 구조체를 이용해야 한다.
연습문제 6-2. Timeout 함수
이번 연습문제에서 GtkLabel과 버튼을 포함하는 창을 생성한다. 라벨은 처음에 "0"이란 숫자를 표시한다. timeout 함수는 매초마다 호출되어 라벨을 한 자리 수까지 증가시킨다. 버튼을 누르면 카운터가 리셋되어 다시 계수를 시작한다.
앞에 언급한 바와 같이 정확도를 원한다면 시간을 세는 데에 절대 timeout을 사용해서는 안 된다. 따라서 타이머를 이용해 이번 예제를 재구현해야 한다. 두 개의 라벨을 창에 위치시키되 하나는 계수에 timeout 함수를 이용하고 나머지 하나는 타이머를 이용한다고 간주해보자. 예제를 어떻게 완료할 수 있겠는가?
연습문제 6-2는 6-1보다는 조금 더 복잡한데, GtkLabel과 현재 계수를 timeout 함수로 잇는 방법을 알아낼 필요가 있다. 물론 전역 변수를 이용할 수도 있지만 대부분의 경우 선호되는 방법은 아니다.
부록 F에 실린 해답을 참조하면, 두 가지 요소는 timeout 함수로 쉽게 전달될 수 있는 구조체에 저장되었다. 이는 크기가 커지더라도 쉽게 관리할 수 있도록 해주기 때문에 대부분의 애플리케이션에서는 이 방법을 사용해야 한다.
애플리케이션의 목적은 timeout 함수를 이용함으로써 지나간 시간을 초로 계수하는 것이다. Clear 버튼을 클릭할 때마다 각 버튼의 계수는 0으로 리셋되어야 한다.
두 가지 연습문제 모두 자신의 상상력을 자극하도록 만들어졌다. 앞의 여러 장을 비롯해 이번 장에서 학습한 내용은 엄청나다. 이제 기존에 학습했던 GTK+에 대한 지식을 이번 장에서 다룬 주제와 통합하는 연습을 할 차례다.
요약
이번 책에서 가장 긴 장을 무사히 마친 것을 축하하는 바이다! 이번 장에서는 GLib가 제공하는 가장 중요한 기능들 중 다수를 상세히 살펴보았다.
물론 본문에서 다루지 않은 주제들도 있으며, 이번 장의 예제에 표시되지 않은 옵션을 제공하는 주제들도 있다. 따라서 이러한 기능들이 애플리케이션에서 필요하다면 API 문서에서 추가 정보를 참조해야 한다.
이번 장의 시작 부분에서는 데이터 타입, 매크로, 메시지 로깅, 환경 변수, 타이머, 파일 조작, 디렉터리 관리, 파일 시스템을 비롯해 GLib의 기본이 어떻게 작용하는지를 빠르게 살펴보았다. GLib에서 메모리를 관리하는 방법도 학습하였다. malloc()과 friends의 래핑에 더해 GSlice가 제공하는 슬랩 할당자를 사용하는 방법도 제시하였다. 또 GLib는 애플리케이션 내에서 메모리 사용을 프로파일링하는 방법도 제공한다.
이번 장에서 다룬 또 다른 중요한 주제로 메인 루프를 들 수 있겠다. 메인 루프는 사실상 GLib에서 GMainLoop, GMainContext, GSource에 의해 구현됨을 학습하였다. 이미 내장된 두 가지 소스 타입으로 timeout 함수와 idle 함수가 있다. Timeout 함수는 사전에 정의된 시간 간격으로 호출되고, idle 함수는 우선순위가 더 높은 액션 중에 실행되어야 할 액션이 없을 때에 호출된다.
GLib가 제공하는 데이터 유형의 범위는 다양하다. 본문에서는 아래와 같은 열 가지 데이터 유형을 학습하였다.
- 문자열은 텍스트가 추가되면 자동으로 커지는 문자 배열을 제공한다. 이는 C++의 표준 템플릿 라이브러리가 제공하는 문자열 클래스와 유사하다.
- 연결 리스트는 임의의 타입으로 된 대규모의 데이터 리스트를 순회, 검색, 정렬하도록 해준다. GLib에서는 단일 연결 리스트와 이중 연결 리스트가 제공된다.
- 균형 이진 트리는 순회와 검색에 최적화된 트리 구조체다. N-ary 트리는 각 노드가 개발자가 원하는 수만큼 branch를 갖도록 해준다. 급속도고 복잡해질 수 있다.
- 쿼크는 연관된 문자열에 대한 정수 포인터를 제공한다. 키가 있는 데이터 리스트는 임의의 타입으로 된 데이터를 저장하기 위해 쿼크를 참조로 이용한다.
- 해시 타이블은 연결 리스트와 비슷하지만, 임의의 타입으로 된 포인터를 통해 항목으로 접근한다는 점에 차이가 있다. 최적화되므로 데이터를 매우 빠르게 찾을 수 있다.
GLib는 파일 및 디렉터리 유틸리티 함수를 다수 제공한다. 파일의 읽기와 쓰기, 디렉터리의 내용 읽기, UNIX 파일 시스템 기능 래핑하기가 포함된다. GIOChannel 구조는 파일이나 파이프를 처리하는 데에 사용되어 프로세스 간 통신을 제공한다.
플러그인 시스템을 쉽게 생성하는 방법은 GLib의 GModule 구조체를 사용하는 방법이다. 해당 구조체는 라이브러리를 동적으로 로딩하고 파일로부터 심볼을 검색할 수 있게끔 해준다. 애플리케이션을 좀 더 모듈식(modular)으로 만들 때 사용되기도 한다.
지금쯤이면 여러 가지 중요한 GTK+ 위젯과 GLib 기능을 어느 정도 이해했을 것이다. 기능 중 다수는 뒤의 여러 장에서 좀 더 개선된 위젯에서 사용될 것이다.
제 7장은 GtkTextView라고 불리는 다중행 텍스트 엔트리 위젯에 관해 설명할 것이다. 그 외에도 클립보드와 GtkSourceView 라이브러리의 주제를 다룰 것이다.