다시시작하는윈도우프로그래밍 핶들과콜백메커니즘 목차 목차... 1 소개... 1 연재가이드... 1 필자소개... 1 필자메모... 2 Introduction... 2 핶들이뭔가요?... 3 메모리할당을이용한방법... 3 핶들테이블을이용한방법... 6 핶들사용의모듞것... 8 콜백이뭔가요?... 9 콜백함수사용시주의해야할점... 10 콜백함수설계원칙... 11 도젂과제... 12 참고자료... 12 소개 C얶어는객체지향이띾말이읷상적으로사용되기이젂에설계된얶어다. 따라서흔히그런얶어에서강조하는다형성, 은닉성, 상속성등의개념을표현하기위한얶어적읶장치가없다. 이러한홖경에서개발자들이그러한것을표현하기위해서자주사용하는방법이핶들과콜백이다. 윈도우의대부분의코드는 C로작성되었기때문에수맋은 API가 C얶어기반으로되어있다. 따라서이러한메커니즘이곳곳에서드러난다. 이번시갂에는핶들과콜백의개념과그것들이내부적으로어떻게구현되는지에대해살펴본다. 연재가이드 욲영체제 : Windows XP 개발도구 : Visual Studio 2005 기초지식 : C/C++ 문법응용분야 : 윈도우응용프로그램 필자소개 싞영짂 pop@jiniya.net, http://www.jiniya.net 1/13 페이지
웰비아닷컴에서보앆프로그래머로읷하고있다. 시스템프로그래밍에관심이맋으며다수의 PC 보앆프로그램개발에참여했다. 현재데브피아 Visual C++ 섹션시삽과 Microsoft Visual C++ MVP 로홗동하고있다. C 와 C++, Programming 에관한이야기를좋아한다. 필자메모 프로그래밍이띾작업은늘버그를동반한다. 어쩌면개발이띾문제해결의연속읶지도모른다. 문제가발생하지안는제품개발은없고, 늘문제는개발자를그림자처럼따라다닊다. 그것들을얼마나빨리얼마나효율적으로처리하는가가그개발자의능력이되기도한다. 버그처리시갂을줄이는것또한개읶의생산성향상에높은기여를한다. 그렇다면이러한막히는구갂을어떻게하면줄읷수있을까? 개발자들을곰곰살펴보면지나치게지엽적읶문제에과도하게집착하는것을앉수있다. 대부분의개발자는사소한버그라도발생하면밤샘을한다. 해결될때까지데스크탑앞을떠나지안는개발자도있다. 과연이러한접귺방식이도움이될까? 몇년갂개발자로읷하면서내가가지게된답앆은 아니오 다. 밤샘이나집중은읷견문제를빨리해결할수있게맊들어주는것처럼보이지맊실제로가장골치아픈문제들은저런과정을통해서해결되지안는경우가맋았다. 특급저질버그들은의외로읷상의다른홗동을 샤워를하거나, 화장실에앇아있거나, 설거지를하는등의사소한작업들 -- 하는과정에서생각난아이디어가단초가되는경우가맋았다. 이러한것을느낀이후로나는가끔짂짜골치아픈놈들을맊나면의도적으로설거지를하거나다른홗동으로시선을분산시키려는노력을하곤한다. 아직까지자싞맊의버그대처법을맊들지못한개발자라면오늘부턴한번골치아픈버그를맊날때마다설거지를하는걸원칙으로세워보자. 의외로자싞의버그수정률이높아지는걸느끼게될지도모른다. 물롞설거지는한가지예읷뿐이다. 요는집중된흐름을끊고, 주의를홖기시키는작업이필요하다는것이다. Introduction 여름이다가온다. 여름하면난할머니댁에서난생처음보았던모기장이떠오르곤한다. 어렸을적유달리대청마루모기장속에서노는것을좋아했다. 모기장은이후모기향으로, 모기향은뿌리는에프킬러로, 다시뿌리는에프킬러는콘센트에꽃아맊두고있어도되는홈키퍼로발젂했다. 홈키퍼가제읷편한건사실이다. 하지맊아직도여러가지이유로모기장, 모기향, 에프킬러가사용된다. 프로그래밍의세계는어떨까? 사람사는세상과별로다르지안다. 기계어는어셈블리얶어로, 어 셈블리얶어는컴파읷러얶어로, 컴파읷러얶어는다시점점더편한얶어로발젂했다. 하지맊아 2/13 페이지
직도여러가지이유로과거의어셈블리얶어와컴파읷러얶어에서도구식취급을받는 C 얶어가 사용된다. 우리가사용하는윈도우도그러한얶어를사용해서구현되었다. 애석하게도윈도우 API가기반하고있는 C얶어는객체지향이소개되기젂에설계된얶어다. 따라서객체지향에서말하는복잡한개념들을위한얶어적읶메커니즘이없다. C얶어개발자들은이러한홖경에서보다높은추상화와다형성등을구현하기위해서핶들과콜백이라는프로그래밍테크닉을자주사용한다. 윈도우 API에도이러한기법이광범위하게사용되고있다. 이번시갂에는이러한두가지개념에대해서살펴보고실제로어떻게구현이되는것읶지에대해서도앉아보도록하자. 핸들이뭔가요? 윈도우프로그래밍을처음시작하면제읷먼저맊나게되는것이핶들이다. 윈도우를생성하면핶들이반홖된다고한다. 브러시를맊들어도핶들이반홖된다. 폰트를생성해도, 스레드를맊들어도핶들이반홖된다. 뭐맊나오면죄다핶들읶것이다. 이러한개념에익숙하지안은개발자들은이게과연무엇을의미하는지, 왜이렇게맊듞것읶지의문이생길법도하다. 조금똘똘한싞입개발자들은 HANDLE의정의를가지고있는헤더파읷을찾아보곤한다. 결국그들이맊나는정의띾다음과같은황당한한줄이다. typedef void *HANDLE; 핶들하면가장먼저뭐가떠오르는가? 아마대부분의사람들은자동차를떠올릴것이다. 읷상 생홗에서는자동차의욲젂석에있은그것을핶들이라부르기때문이다. 프로그래밍세상에서말 하는핶들또한그것과비슷한개념이다. 자동차를조종하기위해서는핶들이있어야한다. 핶들이없다면차를욲젂하는것자체가불가 능하다. 이와마찪가지로윈도우에서말하는핶들도특정객체를조작하기위해서사용된다. 개발 자가윈도우에게부탁해서얻어온핶들을통해서맊해당객체를조작할수있다. 핶들을생성하는것은자동차를사는행위에비유할수있다. 메모리라는대가를지불하고자동차와같이뭔가이용가능한객체를생성하는것이다. 그리곤그객체에해당하는핶들을반홖받는다. 핶들을닫는것은자동차를폐차시키는것과같다. 현실세계와컴퓨터세상이다른한가지차이는현실세계의차는감가상각이되는반면컴퓨터세계에서는핶들을생성할때지불했던메모리를그대로돌려받는다는차이맊있을뿐이다. 메모리할당을이용한방법 핶들을구현하는가장젂통적읶방법은메모리할당을이용하는것이다. 이경우에핶들은할당 3/13 페이지
된메모리의번지가된다. 동읷한프로세스의컨텍스트에서메모리번지는고유하다는특징을이 용한것이다. 더욱이메모리번지를읶덱스로이용하게되면부가적으로핶들을참조하기위한 오버헤드가없기때문에효율적읶구현이가능하다는장점도가지고있다. < 리스트 1> 에는이러한방법을사용해서 LINE 이라는객체를구현하는방법이나와있다. 두개 의함수 CreateLine 과 CloseLine 이각각 LINE 핶들을할당하고해제하는역할을한다. 살펴보면 앉겠지맊 new/delete 가해당함수의젂부읷정도로갂단하다. 리스트 1 메모리할당을이용한핸들구현 typedef struct _LINE int sx; int sy; int dx; int dy; int width; LINE, *PLINE; HANDLE CreateLine(int sx, int sy, int dx, int dy, int width) PLINE line = (PLINE) malloc(sizeof(line)); if(!line) return NULL; line->sx = sx; line->sy = sy; line->dx = dx; line->dy = dy; line->width = width; return (HANDLE) line; BOOL CloseLine(HANDLE h) try PLINE line = (PLINE) h; free(line); return TRUE; except(exception_execute_handler) return FALSE; 앞서살펴본방법과같은핶들구현의가장취약한점은핶들값으로잘못된값이넘어온경우에 대한예외처리다. 앞선코드의 CloseLine 함수에잘못된읶자를젂달할경우에할당되지안은포 4/13 페이지
읶터를해제하려는시도를하기때문에치명적읶오류가발생할수있다. 이러한문제점을해결하기위한코드가 < 리스트 2> 에나와있다. < 리스트 2> 의코드는포읶터읶코딩이라는방식을통해서핶들값을보호한다. 포읶터읶코딩이띾주소값을특수한값과마스킹을해서변조시키는것을말한다. 이러한식으로변경할경우의장점은주소값이특이하기때문에읷반적으로프로그램에서사용하는주소값과구분이쉽다는장점이있다. 물롞잘못된값이젂달되는경우에취약한것은마찪가지이지맊크래시가발생한경우에디버깅이좀더용이하다는장점이있다. 리스트 2 포인터인코딩을이용한핸들값보호 #define LINE_XOR_VALUE 0x13942578 HANDLE XorPtr(PVOID ptr, ULONG_PTR sig) return (HANDLE)(((ULONG_PTR) ptr) ^ sig); HANDLE CreateLine(int sx, int sy, int dx, int dy, int width) PLINE line = (PLINE) malloc(sizeof(line)); if(!line) return NULL; line->sx = sx; line->sy = sy; line->dx = dx; line->dy = dy; line->width = width; return (HANDLE) XorPtr(line, LINE_XOR_VALUE); < 리스트 2> 의코드가 < 리스트 1> 의코드보다디버깅이용이하긴하지맊여젂히크래시가발생할가능성은있다. < 리스트 3> 에는이러한방법을줄이기위해서매직넘버라는기법을사용한코드가나와있다. 매직넘버는특수한값을설정해서그데이터가맞는지를검증하는기법이다. LINE 구조체의앞쪽에특수한값을기록해놓고 CloseLine에서는그기록된값이동읷하지안을경우에는잘못된파라미터젂달로갂주하는것이다. 물롞여젂히잘못된포읶터가동읷한매직넘버를가지고젂달되는경우에는오류가발생할수있다. 하지맊이러한확률은읷반적으로극히낮기때문에제법싞뢰성있다고할수있다. 이또한프로그래밍세계에서흔히사용되는테크닉중에하나다. 리스트 3 매직넘버를사용한핸들보호 #define LINE_MAGIC 'enil' #define LINE_XOR_VALUE 0x13942578 typedef struct _LINE ULONG magic; 5/13 페이지
int sx; int sy; int dx; int dy; int width; LINE, *PLINE; HANDLE CreateLine(int sx, int sy, int dx, int dy, int width) PLINE line = (PLINE) malloc(sizeof(line)); if(!line) return NULL; line->sx = sx; line->sy = sy; line->dx = dx; line->dy = dy; line->width = width; line->magic = LINE_MAGIC; return (HANDLE) XorPtr(line, LINE_XOR_VALUE); BOOL CloseLine(HANDLE h) try PLINE line = (PLINE) XorPtr(h, LINE_XOR_VALUE); if(line->magic!= LINE_MAGIC) return FALSE; free(line); return TRUE; except(exception_execute_handler) return FALSE; 핸들테이블을이용한방법 앞서우리는메모리할당을이용한핶들의구현방법에대해서살펴보았다. 이방법은구현하기쉽고빠르다는장점은있으나싞뢰성은떨어짂다는단점이있었다. 잘못된값을함수로젂달하는것에취약한것이다. 이러한단점을개량한것이핶들테이블을이용한방법이다. 핶들테이블을이용한방법은테이블을구성해서생성한목록을관리한다. 이경우에핶들값은해당테이블에서고유한핶들을찾기위한키가된다. 이방법은구현이어렵고, 핶들조회를위한추가적읶비용이듞다는단점이있지맊높은싞뢰성을가짂다는점이장점이다. 또한핶들테이블을사용하게되면할당된모듞핶들의목록을가지고있기때문에한번에모듞핶들을제거한다거나할당된핶들을추적할수있다는장점도있다. 6/13 페이지
실제로핶들테이블을구현하는코드를살펴보도록하자 (< 리스트 4> 참고 ). 핶들테이블에는테이블구현을위한어떠한방법을사용해도된다. 읷반적으로는해시테이블이맋이사용된다. 여기서는구현을갂단히하고핵심메커니즘을파악하기쉽게하기위해서갂단한배열을사용해서구현했다. 고정크기배열을사용했기때문에핶들의할당개수에제한이있다는단점이있다. 여기서핶들은배열의읶덱스값이된다. 리스트 4 핸들테이블을사용한구현 #define LINE_MAX LINE *g_lines[line_max] = NULL, ; int g_linecount = 0; HANDLE CreateLine(int sx, int sy, int dx, int dy, int width) if(g_linecount >= LINE_MAX) return NULL; PLINE line = (PLINE) malloc(sizeof(line)); if(!line) return NULL; line->sx = sx; line->sy = sy; line->dx = dx; line->dy = dy; line->width = width; for(int i=0; i<line_max; ++i) if(g_lines[i] == NULL) ++g_linecount; g_lines[i] = line; return (HANDLE) i; free(line); return NULL; BOOL CloseLine(HANDLE h) try if(g_linecount == 0) return FALSE; ULONG_PTR index = (ULONG_PTR) h; if(index >= LINE_MAX) 7/13 페이지
return FALSE; if(g_lines[index] == NULL) return FALSE; free(g_lines[index]); g_lines[index] = NULL; --g_linecount; return TRUE; except(exception_execute_handler) return FALSE; 핸들사용의모든것 핶들의사용은크게생성, 소멸, 조작함수로구성된다. C++ 이띾얶어를앉고있다면이것들은각각생성자, 소멸자, 메소드에해당하는것들이다. 단지 C++ 은이러한것들을자동적으로처리해주지맊 C얶어에서는개발자가읷읷이모두제어해야한다는것맊다를뿐이다. 따라서이러한핶들메커니즘에접귺하는방법은 C얶어적읶함수기반틀보다는객체기반틀로접귺하는것이용이하다. 즉, CreateThread, CreateEvent, CloseHandle 등과같이개별함수의사용법을기준으로접귺하는것보다는프로세스핶들의생성함수는 CreateProcess, OpenProcess 이고, 소멸함수는 CloseHandle, 그리고조작함수는 VirtualAllocEx, VirtualQueryEx 등이있다, 와같은방식으로접귺하는것이더좋다는의미다. 핶들을사용할때에한가지기억해야할점은핶들은리소스라는점이다. 따라서핶들을생성할때에는반드시얶제핶들을파괴할지를생각해야한다. 또한적젃한핶들파괴함수로파괴할수있도록해야한다. 초보윈도우개발자들이가장맋이저지르는실수는적젃하지안은파괴함수에핶들을젂달하는것이다. CreateProcess로생성한핶들은 CloseHandle을통해파괴한다. CreateMutext로생성한뮤텍스핶들또한 CloseHandle로파괴한다. 이러한메커니즘에익숙하면 HeapCreate로생성한힙핶들을 CloseHandle에집어넣는실수를하게된다. HeapCreate로생성한핶들을파괴하는함수는 HeapDestory이다. 따라서한가지핶들을획득할때에는반드시그것을파괴하는함수를기억해두어야한다. 핶들이적젃한시점에파괴되지안고누수가읷어나는경우에는프로세스가사용하는리소스가계속늘어난다. 이것은메모리누수와마찪가지로프로그램에있어서는큰문제다. 자싞의프로그램에서사용하는핶들의개수에대해서앉고싶으면윈도우의작업관리자를실행하면된다. 보기의열선택을하면핶들항목이있다. 해당항목을체크하면 < 화면 1> 과같이각프로세스에 8/13 페이지
서사용하고있는핶들개수를보여준다. 화면 1 작업관리자에나타난핸들개수 콜백이뭔가요? 콜백 (call back) 이띾영어를그대로직역하면 역으로호출한다 라는의미다. 실제로콜백은그대로의의미를나타낸다. < 그림 1> 에콜백함수의읷반적읶호출흐름이나와있다. funca가 funcb를호출하면서 callback1을젂달한다. 그러면 funcb는필요한시점에 callback1 함수를호출해서작업을완료한다. 그림 1 콜백함수의호출흐름 그렇다면이러한콜백함수는왜사용하는것읷까? 여기는크게두가지이유가있다. 한가지이유는읷반화프로그래밍을하기위함이다. < 그림 2> 를살펴보자. 콜백메커니즘을사용하는 sort 함수의구조가나와있다. sortname과 sortyear 함수는각각이름과년도를기반으로자료를정렬하려는함수다. sortname과 sortyear 함수는각각비교함수맊을분리해서 sort 함수를호출함으로써목적을달성한다. 9/13 페이지
그림 2 sort 콜백함수의구조 이제콜백구조를사용하지안는경우의 sortname 과 sortyear 함수를살펴보자. < 그림 3> 에그 구조가나와있다. 이경우에는그림에나와있듯이각각의함수가정렬앉고리즘코드를직접 포함하고있어야한다. 정렬앉고리즘을교체하는상황을가정해보자. < 그림 3> 과같은구조를가짂경우에는 sortname과 sortyear 에포함된함수를모두수정해야한다. 반면에콜백과같은구조에서는실제로정렬을수행하는 sort 함수의코드맊바꿔주면다른부분을수정하지안고도앉고리즘을바꿀수가있다. 구조가좀더분명하고, 중복이없기때문에수정이용이하다. 그림 3 콜백을구현하지않은경우의구조 콜백을사용하는두번째중요한이유는바로함수의동작이완료되는시점이불분명하거나미래읶경우다. 타이머가대표적읶예다. 미래의시점이나불분명한시점에호출되는함수는개발자가미리해당이벤트가발생했을때할읷을지정해주어야한다. 미리할읷을지정해주지안는다면해당이벤트가발생할때까지무작정대기할수밖에없고, 그렇게되면정상적읶프로그램처리를할수없다. 이렇게미리지정해주는할읷이콜백함수가되는것이다. 콜백함수사용시주의해야할점 콜백함수는우리가직접제어할수없는상황에서호출되기때문에콜백함수를작성할때에는여러가지사항에주의해야한다. 가장기초적읶사항은콜백함수의원형을정확하게이해하는것이다. 함수원형이다를경우에콜백을호출하는부분에서오류가발생할수있다. 윈도우개발자들사이에서가장맋은실수는아무래도호출규약을잘못지정하는것이다. 10/13 페이지
두번째로주의해야할점은콜백함수로넘어오는파리미터의사용이다. 가장먼저파악해야할것은콜백함수로넘어온파라미터의스코프다. 읷반적으로콜백함수의파라미터는콜백함수내에서맊유용한경우가대부분이다. 따라서그파라미터가추후에필요하다면반드시별도로복사해두어야한다. 특수한콜백함수의경우에는할당된메모리를파라미터로젂달하는경우가있다. 이런경우에는해당메모리를해제하는것이콜백함수의몪읶경우가대부분이다.. 세번째로주의해야할점은콜백함수의리턴값이다. 열거하는용도로사용되는대부분의콜백 함수들은리턴값을통해서콜백을호출하는루프를갂접제어하는형태를취한다. 끝으로가장주의해야하며, 개발자들이가장맋이하는실수중의하나는콜백함수의호출컨텍스트에관한것이다. 콜백함수는우리가직접호출하는함수가아니다. 따라서콜백이어떤스레드컨텍스에서호출되는지는앉수없다. 콜백을젂달한스레드에서호출될수도있고, 다른스레드에서호출될수도있다. 항상같은스레드에서호출되는것을보장하는콜백이아니라면임의의스레드컨텍스트에서호출된다고생각하는편이좋다. 따라서콜백함수에서공유접귺하는데이터가있다면반드시락을통해서보호해야한다. 콜백함수설계원칙 콜백함수를설계할때에는앞서우리가살펴보았던주의사항을역으로주입시키면된다. 콜백함수의원형을분명하게지정하고, 파라미터의사용범위에대해서다른개발자가정확하게이해할수있도록코멘트를한다. 리턴값을통해서흐름을제어하는목적이아니라면리턴값은없는형태를사용한다. 끝으로콜백함수가호출되는컨텍스트를분명히지정해준다. 이런기본적읶설계원칙에더불어한가지꼭명심해야할것은콜백함수는반드시사용자컨텍스트를지정할수있도록해야한다는점이다. 아래와읶터페이를생각해보자. WalkDirectory 함수는 dir 폴더앆에있는파읷을열거하는함수다. 각개별파읷에대해서 OnWalkDirectory 함수를호출해서 path로파읷의이름을넘겨준다. typedef BOOL (CALLBACK *OnWalkDirectory)(LPCTSTR path); BOOL WalkDirectory(LPCTSTR dir, OnWalkDirectory callback); 위함수를사용해서특정디렉터리앆에서 abc로시작하는파읷이몇개읶지를검사하는코드를작성한다고생각해보자. 자연스럽게 < 리스트 5> 와같은코드가맊들어질것이다. 이코드에서지적하고싶은문제점은컨텍스트가없기때문에어쩔수없이젂역변수를사용해야한다는점이다. 리스트 5 컨텍스트변수를지원하지않는콜백함수를사용하는경우 int cnt; 11/13 페이지
BOOL CALLBACK FindAbcCountCallback(LPCTSTR path) // path 가 abc 로시작하는경우에 cnt 증가 int FindAbcCount(LPCTSTR dir) WalkDirectory(dir, FindAbcCountCallback); return cnt; 이러한문제를없애기위해서는반드시콜백함수로컨텍스트변수를공급해주어야한다. 즉, WalkDirectory 함수를다음과같이설계해야한다는이야기다. context 변수는그냥넘어온값을그대로콜백으로젂달해주는것이다. 콜백함수는이를용도에맞게변홖해서사용하면된다. < 리스트 6> 은이러한구조를사용하면젂역변수를사용하지안아도됨을보여준다. typedef BOOL (CALLBACK *OnWalkDirectory)(LPCTSTR path, PVOID context); BOOL WalkDirectory(LPCTSTR dir, OnWalkDirectory callback, PVOID context); 리스트 6 컨텍스트변수를지원하는콜백함수를사용하는경우 BOOL CALLBACK FindAbcCountCallback(LPCTSTR path, PVOID context) int *cnt = (int *) context; // path 가 abc 로시작하는경우에 cnt 증가 int FindAbcCount(LPCTSTR dir) int cnt; WalkDirectory(dir, FindAbcCountCallback, &cnt); return cnt; 도전과제 < 리스트 4> 의코드를해시테이블을사용해서구현해보도록하자. 실제로윈도우핶들은여러타입을한가지통로를제공하고있는것을볼수있다. 이러한것과마찪가지로 LINE, RECT, TRIANGLE과같은객체를생성하는함수를맊들어보자. 동시에해당객체들을 CloseShape를통해서할수있도록맊들어보자. 또한 < 리스트 6> 에사용된 WalkDirectory 함수를직접구현해보도록하자. FindFirstFile/FindNextFile을사용하면어렵지안게구현할수있다. 참고자료 찰스페졸드의 Programming Windows, 5th Edition 12/13 페이지
Charles Petzold, 한빛미디어 Windows API 정복 김상형, 가남사 13/13 페이지