1. Overview
GLib은 C 프로그래밍을 정말 쉽게 만들어주는 또다른 유틸리티다. GLib의 기능과 이점을 소개한다.
GLib은 C프로그래밍을 좀더 즐길수 있도록 해주는 유틸리티 라이브러리다. 예전부터 이런 라이브러리는 있었지만, 대중적이지도 않았고, 중심이 잘 잡히지도 않았고, 일관된 모습을 가지지도 않았었다. GTK+/GNOME을 통해 GLib을 알기 전에는 C의 기본함수를 신뢰했었다. C프로그래머가 되었을 때, 난 펄이나 C++ 의 STL과 좋은 컨테이너, 데이타 스토리지 기능들을 부러워 했었다. C의 기본 라이브러리는 불행히도 일관되지 않은 많은 로우레벨의 함수들을 가졌는데, 다른 플랫폼에서 동작하기가 어렵거나, 특정한 플랫폼에서만 구현이 되었다.
GLib은 이런 문제에대한 해답을 제시한다.C사용자를 종종 찌푸리게하는 세가지 기본적인 문제(With respect to C, it addresses three fundamental problematic issues C users often face):
- Data containers
- Portability
- Utility
GLib의 가장 두드러지고 강력한 점은 컨테이너(containers) 다. 포터블한 코드를 작성하는데 매우 좋다는 것도 그렇지만 말이다. Glib을 쓴다면, 모든 대상 플랫폼에서 동작하게 하기 위해 특정한 기능들만 사용하게 요구하지 않고, 다른 플랫폼에서 당신이 짠 코드가 만족하게 돌아가도록 자주 기도하는 모습을 스스로 발견하지 않아도 된다. C와 유사한 개념으로, C의 기본 라이브러리보다 더 일관된 유틸리티를 제공한다. Beyond all this, GLib touts the more exotic features of a simple lexical analyzer and a main loop functionality for event-driven applications.
2. Typedefs
GLib의 typedef들은 넓은 범위를 가진다. 어떤 것은 타이핑을 줄이기 위해서 어떤 것은 이식성을 위해서, 어떤 것은 단순성과 이름의 일관성을 위해서 디자인 되었다. 타이핑을 줄이기 위해서 unsigned 타입을 볼 수 있다. unsigned int 대신에 guint형을 쓸 수 있다. 이 방식은 char 타입과 모든 int 타입에 적용된다.
이식성있는 typedef은 특정한 크기의 정수형을 사용하는 문제에 사용한다. 예를 들어, 16비트 정수는 gint16, 부호없는 8비트 정수는 guint8이다. 크기는 8, 16, 32 그리고 머신과 컴파일러가 64비트 정수를 지원할 경우 64를 쓸 수 있다. 물론 G_HAVE_INT64라는 매크로를 이용해서 검사해 보고 사용해야 한다.
GLib은 코드의 간결성을 위해서 두가지 기본 typedef을 제공한다. 하나는 void * 형인 gpointer이고 다른 하나는 int형인 gboolean이다. 후자는 코드의 가독성을 증가시켜주고, automatic code scanners 에게 true/false를 검출 할 수 있게 한다. As a matter of taste rather than functionality, GLib은 모든 기본 타입앞에 "gint"처럼 "g"를 붙인다. 난 개인적으로 사용하지는 않지만, 어떤 사람들은 이런 타입들을 사용해서 일관성을 증가시킨다.
3. Memory allocation
기본 콤포넌트, 메모리 할당부터 시작해 보자. standard malloc 과 free 는 완전하지 않다. free는 충돌할 수도 있는데 예를 들어, 널값을 넘기면 malloc은 널값을 리턴한다. 널값을 체크하지 않으면 어플리케이션이 어떤 동작을 할 지 모른다. GLib은 동등한 g_malloc과 g_free를 정의한다. g_malloc은 리턴메모리를 보장한다. 메모리 할당을 더이상 할수 없으면 프로그램이 정지한다. 이것은 나중에 세그먼트 폴트를 일으키며 제대로 동작하지 않는 것 보다 낫다. g_free 는 널값을 전달할수 있는데, 아무것도 하지 않고 , free를 해야하는 많은 포인터를 가진 오브젝트면 이것을 해제하는 cleaner function을 실행한다.
g_malloc은 nice 한데, g_new와 g_new0는 더 많이 효용이 있다. 뒤의 두개는 nice하고 간단한 매크로다. 둘다 오브젝트 타입과 디자인된 여러 타입의 할당을 할 수 있다. g_new0는 할당한 메모리를 0으로 채운다. 이 함수의 장점은 쓰레기 값보다 0으로 채워진 것이 초기화 되지 않고 사용되는 에러를 찾기에 더 쉽다는 것이다. 아래는 20개의 정수 배열을 할당하고 나중에 해제하는 예제이다.
Allocating and freeing an array
int *array g_new0(int, 20);
...
g_free(array);
Utility functions
당신은 C에서 문자열을 작업을 할 것이다. C에서 문자열 작업은 축복이자 저주다(-_-;). GLib의 문자열 유틸함수는 안좋은 부분을 다룰수 있게 한다. 넘긴 문자열를 조작하는 함수를 보자. 전달한 문자열의 대소문자 변환을 위해서 g_strup과 g_strdown이 있다. 처음이나 끝의 공백제거를 할 수도 있다. 처음부분의 공백을 제거하기 위해서 g_strchug을, 끝부분의 공백제거를 위해 g_strchomp를, 둘다를 위해서 g_strtrip 이 있다. 이 3가지 함수는 직접 문자열을 조작한다. 바보같이 새로운 문자열을 할당하지는 않는다. 조작된 문자열은 이런 함수들사이에 쉽게 연결되도록 한다. 파일을 읽어서 모든 시작부분의 공백을 제거하고 대문자로 바꾸는 다음 예제를 생각해 보자 :
FILE *fp fopen("some-file.foo", "r");
char buf[256];
while(fgets(buf, sizeof(buf), fp)) {
g_strup(g_strchug(buf));
}
새로운 문자열을 할당하는 함수를 보자. g_strdup는 기존의 strdup를 g_malloc을 이용해서 구현한 것이다. 이것은 문자열을 새로 할당하고 복사해서 리턴한다. 특정한 개수의 문자열을 복사하고 싶을 때는 g_strndup가 있다(개수를 지정하는 인수를 받는다). g_strconcat라는 다른 함수는 NULL로 끝나고 지정되지 않은 길이의 문자열을 인수로 받는다. 그리고 문자열들을 합친후 새로운 메모리를 할당해서 리턴한다. 다음은 문자열에 디렉토리 이름을 추가하고 확장하는 예이다 :
static void
foo(char *string)
{
char *file;
file g_strconcat("/some/path/", string, ".txt", NULL);
...
g_free(file);
}
불안전한 sprintf를 효과적으로 대체하는 두 함수가 있다. 첫째는 g_snprintf로, snprintf를 이식성있게 구현한 것이다. 첫번째 인수로 버퍼를 받고 두번째 인수로 버퍼의 크기를 받는다. 그러나 이것은 buffer overrun을 일으키지 않으면서, sprintf와 정확히 같은 동작을 한다. 종종 얼마나 많은 크기의 버퍼를 필요로 할지 모를 때가 있는데, 이럴 경우 g_strdup_prtinf를 사용할 수 있다. 이 함수는 format string만을 받아들이고, 새로운 버퍼에 할당해서 리턴된다.
g_snprintf와 g_strdup_printf의 사용 예
char foo[256];
char *bar;
g_snprintf(foo, 256, "The number pi: %g", M_PI);
bar g_strdup_printf("The number pi: %g\n", M_PI);
물론 이외에도 많은 GLib 유틸 함수가 존재한다. 모든 함수의 리스트를 보기 원한다면 GLib문서를 보면 된다.
4. Containers
기본적인 것을 보았으니 컨테이너들을 파헤쳐 보자. 대부분의 컨테이너는 일반적인 한가지를 가는데, 데이터 저장을 위한 void pointer이다. 이것은 엄청난 유연성을 제공하지만, 컴파일러의 강제적인 type safety 는 없다. 당신이 늘 한가지 타입의 한가지 컨테이너를 사용하고 컨테이너를 overload하지 않거나 어떤 종류의 실시간 타입체크 메카니즘을 사용한다면 가능하긴 하다. 아직 난 GLib의 컨테이너를 사용했다는 이유로 인한 버그는 없는데, 큰 문제가 없다고 보인다. void pointer를 사용해서 생기는 한가지 문제는 primitive type을 사용하기 힘들다는 것이다. 그러나 당신이 생각할 수 있는 모든 플랫폼에서 포인터는 단지 정수형과 같은 크기의 공간을 차지하고, 당신은 포인터 대신에 정수를 저장할 수도 있다. 사실 GLib은 gpointer와 int간의 정확한 형변환을 하게 해주는 GINT_TO_POINTER와 GPOINTER_TO_INT를 정의한다. 이런 매크로를 통해서 당신의 코드는 쓸데없는 경고 없이 다른 플랫폼으로 이식할 수 있다.
예제 :
gpointer foo GINT_TO_POINTER(50);
...
int bar GPOINTER_TO_INT(foo);
5. Linked lists
Linked list는 단연 GLib에서 가장 많이 쓰이는(어쩌면 남용되는) 컨테이너다. 이것의 이름은 GList와 GSList다. GList는 이중 연결리스트를 구현한 것이고, GSList는 단일 연결 리스트를 구현한 것이다. 함수에 있어서 둘다 매우 유사하며, 특별한 포인터에 대해 공간제약이 있지 않다면, 대부분의 경우 GList를 사용할 수 있다. GSList는 한쌍의 함수가 없지만, 나머지는 GList의 함수와 같다. GList에서 함수 이름을 g_lsit_로 시작하는 것을, GSList에서는 g_slist_로 시작하면 된다.
GList의 구조는 모든 리스트가 포인터로 이루어져 있지 않고 단지 하나의 노드일 뿐이다. 즉 모든 노드가 사실 GList의 구조체인 것이다. 그래서 각각의 구조체는 세개의 포인터를 가지는데, "data"는 이 노드의 데이터를 저장하고, "next"는 다음 노드를, 그리고 "prev"는 이전 노드를 가리킨다. GSList는 물론 "prev" 노드가 없다. 연결리스트에 저장하기 위해 당신은 첫번째 노드의 GList에 포인터를 저장하면 된다. NULL 포인터는 비어있는 리스트를 나타내고, 모든 GList함수는 NULL값을 받아들일 수 있으며 비어있는 리스트처럼 정확히 조작된다.
6. Basic functions
대부분의 기본 함수는 g_lsit_append와 g_list_prepend 다. 첫번째 인수는 현재의 리스트를 가리킨다. 두번째는 리스트에 append나 prepend할 데이터다. 리턴되는 것은 결과로서 생기는 리스트의 첫번째 노드의 포인터다. 다음은 세개의 문자열에 대한 리스트를 생성하고, printf로 출력하는 예제이다 :
1 GList *list NULL;
2 GList *li;
3
4
5 list g_list_append(list, "foo1");
6 list g_list_append(list, "foo2");
7 list g_list_append(list, "foo3");
8
9
10 for(li list; li ! NULL; li li->next)
11 printf("STRING: %s\n", (char *)li->data);
리스트를 해제하기 위해서 g_list_free를 사용한다. 이 해제는 현재의 노드 뿐만 아니라 리스트의 모든 노드를 해제한다. 당신의 리스트의 데이터를 해제할 때 주의해서 사용해야 한다. 이것과 같이 동작하는 g_lsit_foreach 라는 유틸함수가 하나 있다. 이 함수는 첫번째 인수로 리스트를 받아들이고 두번째 인수로 GFunc 타입의 포인터를, 세번째 인수로 사용자 데이터 포인터(위에 설명이 있다) 를 받는다. GFunc는 실제로 두개의 인수를 가진다 : 리스트의 데이터 값의 포인터일 경우, 사용자 데이터 포인터는 g_list_foreach의 마지막 인수로 전달된 것이다.
대부분의 경우 함수에 전달하는 사용자 데이터 인수는 신경쓰지 않는다. 아마도 당신은 단지 모든 노드의 데이터 포인터에 대해 g_free를 원할 수도 있다. 당신의 함수가 단지 하나의 인수(the data pointer) 만을 가진다면, GFunc로 전달 할 수 있다. 당신의 함수는 단지 명시된 인수를 무시할 것이다. 하지만 그렇게 하면 당신은 당신의 함수를 GFunc로 캐스트 해야 한다. 주의 : 캐스팅을 할 때 당신은 C에게 당신이 무엇을 할지 알려주어야 하는데, 다른 타입의 함수를 전달 하지 않아야 한다. (Most of the time you don't care about passing the user_data argument to the function. You perhaps just want to call g_free on the data pointer of every node. As long as your function only takes one argument (the data pointer), you can just pass that function as a GFunc. Your function will just ignore the extra argument. However, you have to then cast your function to GFunc. Be careful; with casting you are telling C that you know what you are doing, so don't pass a function of a different type.) 모든 노드에 대해서 g_free를 실행하고 그 다음에 리스트를 해제하는 예제이다 :
1 g_list_foreach(list, (GFunc)g_free, NULL);
2 g_list_free(list);
어떤때는 리스트의 특정한 노드로 가야할 필요가 있을 것이다. 리스트에서 n번째 GList포인터를 얻는 방법에 대해 이야기해 보자. g_list_nth를 호출하고, 리스트 포인터와 n번째를 가리키는 정수 n을 주면 해당 GList 포인터를 리턴한다. 하지만 대부분의 경우 당신은 해당 노드의 데이터 포인터를 리턴하는 g_list_nth_data를 사용하길 원할 것이다. 두 함수는 잘못된 인덱스 번호를 넘긴 경우등의 에러일때 NULL을 반환한다. 이제 어떻게 특정한 데이터 포인터의 인덱스를 얻을 것인가? 리스트와 데이터의 포인터로 g_list_index를 호출하면, 리스트 첫 노드로 부터의 위치(인덱스)를 정수로 반환한다. 리스트안의 특정한 GList의 인덱스를 얻기 위해서 g_list_position을 호출하고 데이터 포인터 대신에 GList의 포인터를 넘겨주면 된다. 둘 모두 에러시에 -1을 반환한다. 모든 인덱스는 보통 C에서의 zero-base이다( All indexes are zero-based as is usual for C.). 여기에 예제가 있다:
1 GList *list NULL;
2 char *foo "foo";
3 char *bar "bar";
4
5
6 list g_list_append(list, foo);
7 list g_list_append(list, bar);
8
9
10
11 printf("This should be bar: '%s'\n", (char *)g_list_nth_data(list, 1));
12
13
14
15 printf("This should be 0: '%d'\n", g_list_index(list, foo));
16
리스트의 특정한 엘리먼트를 찾고 싶을 수도 있다. 리스트와 데이터의 포인터를 인수로서 g_list_find를 호출하면, 그런 데이터를 가진 노드의 GList의 포인터가 리턴 된다. 그러나 대부부의 경우 포인터가 완전히 다르면, 문자열 같은 실제 데이터가 같을 수도 있다. 그럴 경우 규칙적인 비교가 필요한데, g_list_find_custom 함수가 제공 된다. 단지 g_list_find 와 같지만, GCompareFunc 타입의 함수 포인터를 특별한 인수로 더 가진다. 이 함수는 두 개의 포인터를 인수로 받아서 정수를 리턴한다. 숫자가 0이면 두 포인터가 같고, 0보다 작으면 첫번째 포인터가 두번째 포인터 보다 작으며, 0보다 크면 첫번째 포인터가 더 크다. 이것은 기본 strcmp 함수와 같은 의미며, strcmp를 사용한다. 그렇지만 GCompareFunc는 두개의 void pointer를 인수로 하고 strcmp는 두개의 character pointer(이건 사실 같은 것이다.)를 인수로 하므로 형 변환이 필요하다.
정렬을 목적으로 GCompareFunc를 g_list_sort에서 사용할 수 있다. 이 함수는 리스트와 비교함수를 인수로 받아서 새로운 리스트 포인터를 반환한다.
GCompareFunc 의 사용 예 :
1 GList *list NULL;
2 GList *list2;
3
4
5 list g_list_append(list, "foo");
6 list g_list_append(list, "bar");
7
8
9 list2 g_list_find_custom(list, "bar", (GCompareFunc)strcmp);
10
11
12
13 list g_list_sort(list, (GCompareFunc)strcmp);
14
아마 몇개의 다른 함수들도 알아챘을 것이다. 순서를 역전하는 g_list_reverse, 모든 리스트의 노드를 카피하는 g_list_copy (데이터를 복사하지는 않는다. 그건 직접 해야 한다.)같은 것들 말이다. 또한 처음 노드와 마지막 노드를 찾아주는 g_list_last, g_list_first 도 있다. 이 모든 함수는 GList 포인터를 인수로 받고 GList 타입의 포인터를 반환한다.
7. A different container: GString
다른 컨테이너를 살펴보자. GString은 제멋대로인 데이터를 위해 크기를 바꿀수 있는 컨테이너다. 이것은 표준 C 문자열 보다 높은 수준에서 조작이 되지만, 보통의 C 문자열이기 때문에 항상 내부 버퍼를 얻을 수 있다. 이것은 GString으로 C 문자열을 만든는데 매우 유용하게 한다. GString은 str과 len, 두개의 엘리먼트를 가진다. str은 단지 문자열의 내부 버퍼(비록 직접 조작할 필요는 없지만, 다른 C의 문자열 처럼 사용할 수 있다)이고, len은 스트링의 길이를 나타내는 정수다.
새로운 GString을 만들기 위해 g_string_new 함수를 호출하면 된다. 이 함수는 GString을 초기화 하기위해 필요한 C문자열을 인수로 받는다. NULL값을 넘기면, empty문자열로 인식한다. GString 구조체의 'str' 엘리먼트는 항상 유효한 C 문자열이며, g_string_new에 NULL 값을 넘겨도 반환된 자신은 절대로 NULL이 아니다. 문자열을 해제하기 위해서 g_string_free를 호출하며 놀랍게도 GString 포인터와 내부 메모리를 해제할지를 지정하는 boolean값을 인수로 받는다. 완전히 문자열을 지우고 싶으면 TRUE값을 넘기면 된다. 내부 메모리를 일반 C문자열 처럼 남겨두고 GString구조체만을 해제할 때는 FALSE를 넘기면 된다. 다음은 GString을 이용해서 어떻게 C문자열을 만드는지 보여준다 :
Example code using GString
1 static char *
2 build_a_string(void)
3 {
4 GString *gstring;
5 char *tmp;
6
7
8
9 gstring g_string_new(NULL);
10
11
12
13 ...
14
15
16
17 tmp gstring->str;
18
19
20
21 g_string_free(gstring, FALSE);
22
23
24
25 return tmp;
26 }
문자열 같은 것을 어떻게 만드는지 보자. 시작하기 전에, GString함수는 GString pointer 를 반환한다는 것을 알아두자. 그렇다고 새로운 문자열 구조체를 반환하는 것은 아니다. 주어진 것과 정확히 같은 포인터를 반환한다. 반환되는 같은 포인터는 몇개의 함수를 연결하거나 비슷한 기교를 가능하게 해준다. 대부분의 경우 이것은 매우 유용하지는 않으며 당신은 안전하게 무시할 수 있다.
기본 동작은 append와 prepend다. 각각 두가지의 경우가 있는데 C문자열을 더하건나 단일 문자를 더하는 경우다. g_string_append는 GString pointer와 C문자열을 받고 C문자열을 GString뒤에 붙인다. 반면에, g_string_append_c는 두번째 인수로 단일 문자를 받으며 단일 문자를 뒤에 추가한다. g_string_prepend와 g_string_prepend_c 도 같은 방식이며, 뒤에 추가하는 대신에 GString의 앞쪽에 추가한다.
Appending and prepending
1 GString *gstring g_string_new("foo");
2
3
4 g_string_append(gstring, "bar");
5 g_string_append_c(gstring, '!');
6 g_string_prepend_c(gstring, '!');
7
8
9
종종 어떤 문자열의 중간에 다른 문자열을 삽입해야 하는 경우가 있다. 이것을 하는 함수는 g_string_insert와 g_string_insert_c이다. 둘다 세개의 인수를 받아들이는데, GString pointer, 삽입할 위치(0 based) 그리고 마지막으로, 삽입하고자 하는 것이다.
1 Using g_string_insert_c
2 GString *gstring g_string_new("foobar");
3 g_string_insert_c(gstring, 3, '-');
4
문자열의 부분을 지워고 싶을 때도 있다. g_string_erase를 사용하고 인수는 (GString pointer 와) 시작 위치를 나타내는 정수 그리고 지우고자 하는 부분의 길이를 받는다.
1 Using g_string_erase
2
3 GString *gstring g_string_new("fooBLAHbar");
4
5
6 g_string_erase(gstring, 3, 4);
7
8
9
여기에 GString을 위해 sprintf와 비슷한 정말 깔끔한 함수가 있다. 두가지 선택이 있는데, sprintf와 같이 문자열을 덮어쓰는 g_string_sprintf와 덮어쓰지않고 뒤에 추가하는 g_string_sprintfa가 있다. 추가하는 것은 목록을 만들거나 할때 매우 유용하다(Appending is very useful for building, say, lists of things). 콤마로 분리된 1부터 10까지의 숫자 리스트를 만드는 것을 생각해 보자:
1 Using g_string_sprintfa
2
3 int i;
4 GString *gstring g_string_new(NULL);
5
6
7 for(i 1; i < 10; i++) {
8 if(i > 1)
9 g_string_sprintfa(gstring, ",%d", i);
10 else
11 g_string_sprintfa(gstring, "%d", i);
12 }
문자열을 모두 대문자나 소문자로 만드는 함수도 있고(g_string_up and g_string_down), 특정 길이의 문자열로 자르는 함수(g_string_truncate -- 문자열의 길이는 두번째 인수다), 그리고 새로운 문자열을 지정하는 함수(g_string_assign)도 있다.
1 Using g_string_assign
2
3 GString *gstring g_string_new("fooBAR");
4
5
6
7 g_string_up(gstring);
8
9
10
11 g_string_down(gstring);
12
13
14
15 g_string_truncate(gstring, 3);
16
17
18
19 g_string_assign(gstring, "string");
GLib에는 이 문서에서 다루지 못한 많은 것들이 있다. the hashes, the trees, relations, the lexical scanner, the main loop, file descriptor io 등을 다룬 문서에 관심이 있다면 나에게 알려줘~!Resources