« Previous : 1 : ... 9 : 10 : 11 : 12 : 13 : 14 : 15 : 16 : 17 : ... 31 : Next »

운영체제가 기본 제공하는 프레임과 제목 표시줄이 있는 윈도우라면, 사용자가 그 제목 표시줄을 좌클릭+드래그 하여 창을 다른 곳으로 옮길 수 있다.
그런데, 그런 프레임이나 제목 표시줄이 없는 특수한 형태의 윈도우를 만들었다. (Custom 스킨이 씌워진 리모콘이나 TV 모양의 동영상 재생기 같은..) 사용자가 이 창의 아무 표면이나 특정 부위를 드래그 해서 창의 위치를 옮길 수 있게 하려면 어떻게 하면 좋을까?

(1) 가장 단순무식한 방법은 WM_LBUTTONDOWN, WM_MOUSEMOVE, WM_LBUTTONUP을 받아서 해당 기능을 직접 구현하는 것이다. 즉, LBUTTONDOWN 때 마우스를 캡처하고 마우스 포인터의 위치가 창의 화면 좌표에서 얼마나 떨어져 있는지를 파악한다. 그리고 캡처가 있는 상태에서 MOUSEMOVE가 오면 새 포인터의 위치에 상응하는 위치로 창의 위치를 옮긴다(SetWindowPos). 이 기능은 각각의 메시지 핸들러 함수에다 구현해도 되고, WM_LBUTTONDOWN 안에다가만 자체적인 message loop을 돌려서 구현해도 된다.

이건 드래그 앤 드롭 기능을 구현하는 절차와 비슷하다. 한 윈도우의 내부에서 그려지는 각종 그래픽 오브젝트에 대해서 드래그+이동을 구현하려면 저렇게 직접 코딩을 해 줘야 한다. 그러나 창 자체에 대해서 드래그+이동만을 구현하는 것은 사실 다음과 같이 더 간단한 방법도 있다. 이미 존재하는 기능을 운영체제에다가 요청만 한다는 것이 핵심 아이디어이다.

(2) 그 창에서 WM_NCHITTEST 메시지를 받아서 DefWindowProc의 리턴값이 HTCLIENT인 지점에 대해서도 HTCAPTION을 되돌린다.
그러면 운영체제는 이 창의 클라이언트 영역을 클릭+드래그 한 것도 제목 표시줄을 클릭+드래그 한 것과 동일한 것으로 간주한다. 그래서 드래그 시 창을 자동으로 이동시키게 된다.

이건 대부분의 경우에 굉장히 깔끔한 방법이긴 하지만, 창을 이동시키는 데 쓰이는(HTCAPTION으로 인식되는) 영역에 대해서 더 세부적인 제어를 하기가 어렵다는 게 흠이다. 즉, 거기를 우클릭 한다거나 더블클릭 한 것처럼, 이동과 관계 없는 다른 동작을 취한 것을 우리가 인식할 수 없다. 거기는 마우스 동작에 관한 한, 애초에 클라이언트 영역이 아닌 것으로 간주되어 버렸으니 말이다. 만약 그런 제어까지 해야 한다면 다음과 같은 또 다른 방법을 사용하면 된다.

(3) WM_LBUTTONDOWN이 왔을 때, 창을 이동시키는 기능에 해당하는 시스템 명령을 전달한다.
가장 간단하게 생각할 수 있는 방법은 PostMessage(m_hWnd, WM_SYSCOMMAND, SC_MOVE, 0); 이다. 이것은 Alt+Space를 눌러서 나오는 창의 시스템 메뉴에서 '이동'을 선택하는 것과 같은 효과를 낸다. 창에 제목 표시줄이나 시스템 메뉴가 없다고 해서 시스템 메뉴에 해당하는 기능 자체가 없어지지는 않기 때문이다.

단, 이것은 창을 끌어다 놓는 것과 정확하게 같은 기능은 아니다. 일단 마우스 포인터는 모양이 사방의 화살표 모양으로 바뀌고, 사용자의 key 입력을 기다리는 상태가 된다. 사용자가 ESC가 아닌 다른 key를 누르면 그때부터 마우스 이동으로 해당 창이 이동되는 모드가 된다. 심지어 좌클릭을 한 상태가 아니어도 된다.

SC_MOVE보다 더 직관적인 방법은.. 마소에서 정식으로 문서화하여 공개한 기능은 아니지만 사실상 공공연한 비밀이 돼 버린 기능을 사용하는 것이다. 좌클릭 메시지가 왔을 때 SC_MOVE (0xF010) 대신,
PostMessage(m_hWnd, WM_SYSCOMMAND, 0xF012, 0); 이라고 하면... 마우스를 누르고 있는 동안 창 이동이 아주 깔끔하게 구현된다. 직접 시도해 보시라. 이것이 SC_MOVE와 SC_MOVE+2의 차이이다.

시스템 명령 중에는 SC_MOVE나 SC_SIZE처럼 메뉴에 등재된 명령뿐만 아니라 해당 메뉴 명령을 누른 뒤에 부가적으로 실행되는 기능도 비공개 내부 ID가 부여돼 있다. 가령, SC_SIZE+1 (0xF001)부터 SC_SIZE+8 (0xF008)은 마우스 드래그로 창의 크기를 조절하는 명령을 바로 실행시킨다. 1부터 8까지 순서가 어떻게 되는가 하면 left, right, top, top-left, top-right, bottom, bottom-left, bottom-right이다. 해당 위치의 크기 조절 모서리와 대응한다는 뜻.
이거 배열 순서는 WM_NCHITTEST의 리턴값인 HTLEFT (10)와 HTBOTTOMRIGHT (17)와도 동일하다. 그러니 이해하는 데 어려움이 없을 것이다.

이 주제/테크닉과 관련하여 생각할 수 있는 다른 이야기들을 늘어놓자면 다음과 같다.

1. 추억.
과거에는 운영체제의 자체 기능을 사용해서 창의 위치를 옮기면, 창이 이동되는 동안에 창의 내용이 실시간으로 업데이트 되는 게 아니라 창의 경계 테두리만이 XOR 연산되어 그려졌다. 당연히 창을 일일이 다시 그리는 게 그 시절 옛날 컴퓨터로는 부담스러운 연산이었기 때문이다.
그러다가 1990년대 말, Windows 95를 넘어 98/2000으로 넘어갈 시기부터 창을 실시간으로 업데이트 하는 옵션이 추가되었고, 후대부터는 그게 당연한 관행이 됐다.

창의 테두리만 이동하고 있는 중에는 운영체제가 응용 프로그램으로 WM_MOVING (또는 WM_SIZING)이라는 메시지를 보냈는데, 이때 그냥 SetWindowPos로 창의 위치를 바꿔 버리면 운영체제의 옵션과 무관하게 '실시간 업데이트'를 시전할 수 있긴 했다.
하긴, 옛날에는 스크롤 막대조차도 스크롤 하는 동안 막대의 테두리만 이동하지 스크롤 대상 화면은 업데이트 되지 않는 경우가 있었다.

도스 시절도 마찬가지. 화면 전체의 업데이트가 키보드 연타 속도를 따라가지 못할 경우를 대비해서 일부 프로그램들은 화면을 표시하는 중에도 키보드 입력을 체크하곤 했다. 그래서 상하 화살표가 눌렸으면 화면을 다 업데이트 하지 않고 다시 스크롤을 했다. 그렇게 하지 않으면 나중에 키보드 버퍼가 꽉 차서 삑삑 소리가 났다.. ^^;;

2. Windows에는 이런 식으로 아기자기한 비공개 API가 더 있다.
캐럿의 깜빡임 주기를 나타내는 메시지 0x118는 흔히 WM_SYSTIMER이라고 표현하는 사람도 있는데, 어쨌든 유명한 유령 메시지이다. 이 메시지의 출현에 의존해서 동작하는 프로그램이 설마 있으려나 모르겠다.

또한,
::SendMessage( ::ImmGetDefaultIMEWnd(hWnd), WM_IME_CONTROL, 5, 0 );
이라고 하면 hWnd가 자신과 동일한 프로세스/스레드이든 불문하고 해당 창에 있는 Windows IME의 한영 상태를 얻어 올 수 있다고 한다. 리턴값이 1이면 한글, 그렇지 않으면 영문이다.
보통은 한영 상태를 얻으려면 해당 윈도우에 소속된 IME context 값을 ImmGetContext로 얻어 와야 하는데, 이거 내 기억이 맞다면 프로세스는 물론이고 스레드 경계도 넘지 못한다. 그런데 ImmGetContext나 ImmGetConversionStatus 호출 없이 저렇게 간단한 메시지로 한영 상태를 query할 수 있다니 신기한 노릇이 아닐 수 없다.

MSDN이고 Windows DDK고 어디든지 WM_IME_CONTROL을 찾아 보면, 거기에 문서화돼 있는 IMC_* 명칭들 중에 5라는 값을 가진 물건은 없다. 하지만 저 기능은 Windows 95 이래로 모든 운영체제에서 사용 가능하다. 게다가 5 대신 2를 주면 한영 상태를 바꿀 수도 있는 듯하다. (lParam에다가 새 값을 설정하고)
이런 것들은 마치 인터넷 지도에서 있는 그대로 표시되지 않고 숲으로 가려진 지대를 보는 듯한 느낌이다.

3.
창을 드래그 해서 옮기는 것이야 제목 표시줄을 단 1픽셀이라도 끌면 창이 바로 반응해서 움직인다.
하지만 일반적으로 텍스트나 아이콘을 '드래그 앤 드롭'을 해서 옮기는 건 그렇게 곧장 반응하지는 않게 돼 있다. 창의 위치만을 옮기는 것과는 달리, 일반적인 드래그 앤 드롭에는 파일을 복사하거나 옮기고 텍스트 문서의 내용을 변경하는 등 더 크리티컬한 결과를 초래하는 동작을 수반할 수도 있기 때문이다.

Windows에서 UI 가이드라인 상으로는, 마우스를 클릭해서 약 2픽셀이던가 그 이상 포인터가 가로 또는 세로로 실제로 움직였을 때.. 혹은 움직이지 않았더라도 클릭 후 1초 가까이 시간이 지났을 때에야 드래그가 시작되게 돼 있다. 드래그 인식을 위한 최소 한계치는 GetSystemMetrics(SM_CXDRAG) / SM_CYDRAG를 통해 얻을 수 있다.

허나, 이걸 일일이 코딩하는 건 드래그를 곧장 인식하는 것보다 굉장히 번거롭고 귀찮은 일이다. 그래서 Windows에는 아예 DragDetect라는 함수가 있다. WM_LBUTTONDOWN이 왔을 때 요 함수를 먼저 호출해서 OK가 오면 그때부터 드래그 모드로 진입하면 된다. DragDetect는 자체적으로 메시지 loop을 돌면서 마우스가 표준 규격 이상만치 움직였는지, 시간이 경과했는지, 사용자가 무슨 key를 눌렀는지 등을 총체적으로 판단해서 드래그 모드로 진입할지 여부를 알려 준다.
이런 함수도 있다는 걸 알면 GUI를 구현할 일이 있을 때 도움이 많이 될 것이다.

Posted by 사무엘

2016/04/09 08:28 2016/04/09 08:28
, ,
Response
No Trackback , 4 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1212

과거 Windows 9x 시절에는 내부의 16비트 코드가 gdi/user 계층에서 사용하는 64KB짜리 구닥다리 힙으로 인해 일명 '리소스' 제약이란 게 있었다. 그래서 램이 수백, 수천 MB으로 아무리 많더라도, 프로그램을 많이 띄워서 UI와 관련된 오브젝트들을 이것저것 생성하다 보면 리소스가 바닥 나고 운영체제가 패닉에 빠지곤 했다.

지금으로서는 정말 말도 안 되는 황당한 제약이다. 9x에서는 메모장이 60KB를 조금만 넘는 파일도 열 수 없었던 것처럼 말이다. 숫자 세는 단위 자체가 16비트로 제한돼 있으니, 실제 메모리가 아무리 썩어 넘쳐도 셀 수 없는 영역은 몽땅 그림의 떡이었던 것이다.

사용자 삽입 이미지

꼴랑 64KB짜리 중에서 메모리가 몇만 바이트 남았다고 출력하는 건 좀 민망했는지, 남은 리소스의 양은 퍼센트 비율로 출력되었으며, Windows 기본 프로그램들의 About 대화상자에서 값을 간단히 확인할 수 있었다.
그런데 이 퍼센티지를 얻어 오는 API는 무엇일까? Windows 3.x에서 도입된 GetFreeSystemResources라는 함수가 그 주인공이었다. 얘는 0~2 사이의 정수 인자도 받아서 시스템 전체, GDI, user 종류도 얻을 수 있었다.

Windows 3.1 SDK에서 windows.h를 열어 보면 저 함수는 #if WINVER >= 0x030a 안에 고이 감싸진 채 선언되어 있었다. 즉, 초창기부터 처음부터 존재하지는 않았다는 뜻이다. Windows 1, 2 시절에는 샘플 프로그램들의 About 대화상자를 보면 그냥 남은 주메모리의 양(수백 KB)과 주 하드디스크의 남은 용량만 출력했지, 저런 비율을 따로 알려 주지는 않았었다. NT 계열이 아니라 도스 위에서 돌아가던 16비트 시절에도 말이다.

저 함수의 공식적인 수명은 Windows 3.x에서 그대로 끝났다. 32비트 Windows API에는 정식으로 이식되지 않았으며, 여전히 16비트 user.exe를 통해서만 제공되었다. 그렇기 때문에 32비트 프로그램이 시스템 정보 같은 기능을 구현할 일이 있어서 남은 리소스 퍼센티지를 얻으려면... 원래는... 마치 32/64비트 훅 DLL을 따로 만들듯이 16비트 DLL을 만들어서 그 DLL이 16비트 API를 호출하여 값을 얻고.. 32비트 프로그램은 그 DLL과 flat 썽킹을 해서 의사소통을 해야 했다. 썽킹에 대해서는 지난번에 한번 다룬 적이 있다.

이런 번거로운 일이 필요한 이유는 32비트 프로그램이 user.exe로 직통으로 API 호출을 할 수는 없기 때문이었다. 일단은 말이다.
옛날에 한컴사전이 노클릭 단어 인식 기능을 구현하기 위해 그래픽 API 훅킹을 했었는데, 훅킹용으로 32비트 DLL과 16비트 DLL이 모두 있었던 것이 기억에 남아 있다. 32비트 gdi32.dll뿐만 아니라 16비트 gdi.exe로 직통으로 들어가는 그래픽 API 호출까지 잡아 내서 거기 문자열을 얻기 위해서 만든 거지 싶다. 그러니 32비트 DLL엔 훅 프로시저가 들어있고 16비트 DLL엔 썽킹 루틴이 들어있었을 것이다.

그런데, 없는 길을 부분적으로나마 만들어 낸 용자가 그 시절에 이미 있었다.
Windows 9x의 kernel32.dll이 제공하는 비공개, 봉인, 문서화되지 않은 API를 이용해서 32비트 프로그램이 user.exe를 직통으로 호출해서 리소스를 얻어 온 것이다. Windows 95 Programming Secret의 저자인 Matt Pietrek가 그 용자이다.

마소 내부에서만 사용할 목적으로 만들어진 듯한 비공개 API 중에는 16비트 바이너리를 로딩할 수 있는 일명 LoadLibrary16 / GetProcAddress16 / FreeLibrary16 세트가 있다. 얘는 kernel32.dll의 export table에 이름이 노출돼 있지도 않아서 ordinal 번호로만 접근이 가능한데.. 이 번호를 근성의 리버스 엔지니어링으로 일단 알아 냈다. 참고로 얘들은 Generic 썽킹용으로 쓰이는 LoadLibraryEx32W처럼 뒤에 32W가 붙은 함수하고는 다른 물건이므로 혼동하지 말 것.

그런데 알아 냈다고 전부가 아니다. Windows 9x의 GetProcAddress에는 특별한 보정 코드가 들어 있어서 kernel32만은 예외적으로 ordinal을 이용한 함수 주소 요청을 고의로 막았다! 고로 이름이 없이 ordinal만 존재하고 운영체제 내부에서만 사용되는 비공개 API를 제3자 프로그램이 멋대로 사용하는 걸 자연스럽게 차단했다.

이런 조치를 취한 심정을 이해 못 하는 바는 아니다. 같은 함수라도 운영체제의 버전이 바뀜에 따라 ordinal이 수시로 바뀔 수 있으니 일반적인 함수라면 어차피 번호가 아닌 이름만으로 import하는 게 맞다.
또한 프로그램들이 비공개 API를 무단으로 사용하다가 Windows의 버전이 바뀌면 그 프로그램들이 호환성이 깨져서 동작하지 않게 되는데, 이 경우 사용자는 프로그램의 제작사가 아니라 마소를 비난하는 편이었다. 신제품을 팔아 먹으려고 일부러 프로그램의 동작을 막았네 뭐네 하는 음모론의 희생양이 되는 것이다. 마소에서도 이런 힐난에 이골이 났는지 더 방어적인 조치를 취하게 됐다.

그래도 이런 비공개 API들을 끝끝내 끄집어내서 사용하려면
(1) 로드 타임 차원: kernel32.dll의 비공개 API ordinal을 직결로 연결하는 import library를 직접 만들거나,
(2) 런 타임 차원: PE 파일 포맷을 분석해서 GetProcAddress 함수를 손으로 직접 구현하면 된다. 메모리에 로드된 kernel32.dll 내부의 export table을 수동으로 뒤지면 된다는 뜻이다.

L(로드), F(해제), G(함수 탐색) 함수의 ordinal은 1부터 시작하는 번호 기준으로 35~37이라고 한다. Windows 95부터 ME까지 변함이 없다. 어차피 더 바뀌어야 할 이유가 없는 번호이기도 하고.

이렇게 얻어 낸 HMODULE (WINAPI* pfnLoadLibrary16)(PCSTR)을 호출해서 "user.exe"를 로드한다.
그리고 GetProcAddress에다가 "GetFreeSystemResources"를 하면 드디어 우리가 원하는 함수 포인터를 얻을 수 있는데, 얘는 바로 호출 가능하지가 않다. kernel32에 존재하는 또 다른 비공개 API인 QT_Thunk를 거쳐서 함수를 호출해야 하는데, 이 함수는 또 기계어 차원에서 호출 방식이 반드시 일치해야 하기 때문에 대략 다음과 같은 인라인 어셈블리를 넣어야 한다.

_asm {
    push 0~2  ; 시스템, GDI, user. 얻고 싶은 리소스 타입
    mov edx, [pfnGetFreeSystemResources] ; 32비트 주소
    call QT_Thunk ; kernel32에 대해 "QT_Thunk"를 GetProcAddress 한 결과
    mov [ret_val], ax ; 함수의 실행 결과를 받을 16비트 WORD 변수
}

이렇게 하면 32비트 프로그램이 일단 16비트 API를 호출해서 리소스 값을 얻어 올 수 있다. (참고로 Windows NT 계열은 QT_Thunk 함수가 존재하지 않는다.)
그런데 내가 실험해 본 바로는.. 저거 사용하는 게 굉장히 까다롭다.
저 어셈블리 코드에 도달할 때까지 각종 DLL를 로드하고 여러 단계에 걸쳐서 여러 함수들의 포인터를 얻는 등 절차가 복잡한데, 클래스를 만들어서 중간 단계의 결과들을 저장해 놓거나 절차를 여러 단계의 함수로 분리하면.. asm 부분이 갑자기 동작하지 않게 된다.

비공개 API가 내부에서 썽킹을 수행하는 동안 프로그램의 스택이라든가 내부 상태를 이상하게 건드리는 것 같다. 컴파일러의 최적화 옵션의 영향을 받기도 하고.. 그렇지 않고서야 위의 저 간단한 어셈블리 코드가 딱히 뻑이 날 리가 없는데 말이다.

16비트 DLL을 따로 만들지 않고 편법을 동원해서 16비트 API를 호출하고 구체적으로는 리소스 퍼센티지를 얻는 방법을 알아 봤는데, 참 어렵긴 하다는 걸 느꼈다. 사실, 과거에 thunk 컴파일러가 하는 일 중 하나도 내부적으로 UT_Thunk를 호출하는 중간 계층 코드를 생성하는 것이었다. 더 들여다보니 말로만 듣던 ThunkConnect32 같은 함수도 쓰는 듯했다.

비공개라고 해서 무슨 ntdll 같은 하위 계층도 아니고 참 신기한 노릇이다. 어차피 Windows 9x는 kernel32가 최하위 계층이지 ntdll 같은 추가적인 하위 계층은 없으니 말이다.
Windows Programming Secret 책을 당시의 마소 Windows 95 팀의 엔지니어들이 직접 봤다면..
블리자드에서 스타크래프트를 직접 개발한 프로그래머들이 스탑 럴커처럼 자신조차 상상하지 못한 컨트롤과 테크닉을 구사하는 프로게이머를 보는 것과 비슷한 느낌을 받았지 싶다.

리소스를 되돌리는 함수 정도야 간단한 정수 하나만을 인자로 받고 역시 정수 하나를 되돌리는 아주 단순한 형태이다. 그러니 이런 테크닉을 구현하는 것에도 큰 무리가 없다. 구조체나 문자열의 포인터가 동원되기라도 했다면 메커니즘이 훨씬 더 복잡해지며, 그냥 정석적인 썽크 컴파일러를 쓰는 것밖에 답이 없지 싶다.
그러고 보니 문득 든 생각인데, 과거에 GWBASIC이 처음 구동되었을 때 Ok 프롬프트 앞에 "6만 몇천 바이트 남았습니다(6xxxx bytes free)"라고 메시지가 떴던 게 저런 리소스와 성격이 좀 비슷한 것 같이 느껴진다.

저런 식으로 프로그램이 시작된 직후, 혹은 프로그램의 도움말이나 About 대화상자 한 구석에다가 간단하게 남은 메모리/자원의 양을 표시하는 건 오랫동안 소프트웨어 업계에 남아 있던 관행이었다. 심지어 도스 시절부터 말이다.
그랬는데 요즘은 메모리가 너무 많아지고 숫자 단위가 커져서 그런지 Windows의 작업 관리자는 남은 메모리의 양을 KB 단위 대신 비율로 표시하기 시작했다. 옛날에는 64KB짜리 리소스는 스케일이 너무 작고 민망해서 퍼센트로 표시한 게 아닐까 의심될 지경이었는데 이제는 반대로 너무 커져서 세부적인 숫자가 무의미한 지경이 됐으니 다시 퍼센트로 복귀한 걸로 생각된다.

Posted by 사무엘

2016/03/06 08:35 2016/03/06 08:35
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1200

* 이 글의 내용은 클립보드 API, const 이야기의 연장선상에 있다. 관심 있으신 분은 예전에 썼던 글을 먼저 읽어 보시기 바란다.

1.
Windows API에는 GetPriorityClipboardFormat라는 함수가 있다. 이것은 클립보드로부터 '붙여넣기'(paste) 기능이 있는 프로그램이 사용할 만한 함수이다. 이 함수가 지목하는 포맷에 해당하는 클립보드 내용을 붙여넣으면 되며, 사실은 그보다도 먼저 이 함수의 실행 결과에 따라 '붙여넣기' 메뉴를 enable/disable시킬지를 결정하면 된다.

내 프로그램이 지원하는 클립보드 포맷이 단 하나밖에 없다면 IsClipboardFormatAvailable을 쓰면 된다. 그렇지 않고 여러 종류의 포맷을 지원하는데, 현재 클립보드에 존재하는 포맷 중에 우선순위가 가장 높은 놈은 무엇인지 알고 싶다면 말 그대로 저 priority라는 단어가 들어간 함수를 써서 요청을 해야 한다. 이것은 마치 커널 오브젝트의 동기화에서 쓰이는 WaitForSingleObject와 WaitForMultipleObjects 함수쌍의 관계와 일면 비슷하다.

그런데 내가 궁금한 점은 이 함수의 첫째 인자가 const UINT *가 아니라 왜 그냥 UINT *냐는 것이다.
첫째 인자는 함수로 전달하는 복수 개의 클립보드 포맷 리스트의 포인터이며, 속성이 엄연히 out이 아닌 in이라고 명시되어 있다. 저 함수 역시 인자로 받은 리스트 내용을 참조만 할 것이니, 포인터가 read-only가 아니라 일반 read-write 형태여야 할 하등의 이유가 없다. WaitForMultipleObjects만 해도 HANDLE의 배열 포인터는 분명히 const HANDLE *로 잡혀 있지 않던가?

저게 const라면,

static const table[] = { CF_UNICODETEXT, CF_HDROP }; //사람이 텍스트로 입력한 파일명 & 탐색기에서 복사해 놓은 파일명을 모두 받아들임
int res = GetPriorityClipboardFormat(table, ARRAYSIZE(table));

이런 식으로 간편하게 static const 배열을 함수에다 넘겨 줄 수 있을 텐데, const가 아니기 때문에 일이 좀 귀찮아진다. table에 const 속성을 제거하든지, 아니면 함수에다 넘겨 줄 때 const_cast 연산자를 써 줘야 한다. 내 경험상, 강제로 const 포인터로 고친다고 해서 딱히 프로그램 동작에 문제가 생긴다거나 하는 건 아니더라.

하지만 내 프로그램 내부에서 값을 고칠 일이 절대로 없기 때문에 const로 엄연히 봉인해 버리고 싶은 테이블을 봉인하지 못한다니, 썩 보기 좋아 보이지 않는다. 또한 const_cast 같은 연산자가 늘어나는 건 내 소스 코드에서 무질서도가 쓸데없이 증가하는 듯한 느낌을 준다.

물론, 동일한 유형의 데이터에 대해서 클립보드 포맷을 복수 개 지원한다는 건 대개가 custom 포맷과 표준 포맷을 혼용하는 경우이다. 리스트의 모든 원소가 상수가 아니며, 우선적으로 선택되었으면 하는 앞부분은 어차피 RegisterClipboardFormat의 리턴값인 경우가 많다. 워드 프로세서라면 모든 정보가 보관되어 있는 고유 포맷일 것이고, 텍스트 에디터라면 칼럼 블록을 명시한 고유 포맷이다.

그러니 저 함수의 인자로 전달되는 배열 포인터는 애초부터 const가 아니라 일반 배열일 가능성이 더 높다. 메시지만 해도, 시스템 차원에서의 유일성을 보장받기 위해 RegisterWindowMessage로 특수하게 등록한 메시지는 WM_USER+x와는 달리 가변적인 값이다. 그렇기 때문에 윈도우 프로시저에서 switch/case를 이용해 걸러낼 수가 없으며 if문을 써서 비교해야 하지 않던가. (이 두 Register* 함수는 프로토타입이 UINT Foo(PCTSTR name)로 완전히 동일하며, 명칭을 hash하여 숫자로 바꿔 준다는 공통점이 있어서 더욱 동질감이 느껴진다)

그런데.. 그런 식으로 치자면 WaitForMultipleObjects가 받아들이는 HANDLE도 NULL이나 INVALID_HANDLE_VALUE 같은 오류 상황 말고는 고정불변 상수값이 존재할 여지가 전혀 없는 타입이다. 값의 가변/불변성과는 무관하게 해당 함수가 값들을 읽기만 한다면 함수 인자가 응당 const 포인터로 잡혀 있어야 할 텐데, 클립보드의 저 함수는 왜 저렇게 설계되었으며 디자인에 문제를 제기하는 사람이 왜 없는지 궁금하다.

2.
서식이 없고 칼럼 블록도 없는 일반 텍스트 에디터에서 붙이기를 구현한다면 굳이 custom 포맷을 등록할 필요는 없을 것이고 애초에 복수 개의 클립보드 포맷을 살펴볼 필요도 없을 것이다. 단, 여기에도 원래는 유니코드/ANSI와 관련된 미묘한 문제가 있다. wide 문자 기반인 CF_UNICODETEXT와, ansi 문자 기반인 CF_TEXT가 따로 존재하기 때문이다.

Windows NT는 이들 사이에서 '유도리'를 제공한다.
응용 프로그램이 유니코드 텍스트 형태(CF_UNICODETEXT)로 클립보드에다 텍스트를 복사해 놓은 뒤, 나중에 유니코드를 지원하지 않는 프로그램이 클립보드로부터 CF_TEXT 포맷을 요청하는 경우 운영체제는 CF_TEXT도 있다고 답변을 하며, CF_UNICODETEXT로부터 CF_TEXT 데이터도 자동으로 만들어 준다. 그 반대도 물론 해 준다. (복사는 CF_TEXT로, 붙여넣기는 CF_UNICODETEXT로)

마치 Get/SetWindowText, WM_GET/SETTEXT에서 운영체제가 A/W 문자열 보정을 하듯이, 클립보드에 대해서도 그런 보정을 해 준다. 재미있지 않은가? 그러니 오늘날의 Windows에서는 유니코드를 지원하는 프로그램은 어떤 경우든 CF_UNICODETEXT만 생각하고 요걸로만 읽고 쓰면 유니코드를 지원하지 않는 구시대 프로그램과도 의사소통에 아무 문제가 없다.

하지만 문제가 되는 건 내부적으로 유니코드를 사용하는 프로그램이 Windows 9x까지 고려해서 만들어질 때이다. 9x 계열에서는 CF_UNICODETEXT는 응용 프로그램이 굳이 요청해서 자기들끼리 쓰지 않는 한, 운영체제 차원에서 공식적으로는 '없는 포맷', 지원하지 않는 유령/공기 포맷이다. 고로 NT 계열과 같은 타입 자동 변환이 없다.

텍스트 에디터가 자기는 내부적으로 유니코드 기반이라고 해서 자신이 native로 사용하는 CF_UNICODETEXT 형태로만 데이터를 복사해 놓으면, 기존 메모장 같은 프로그램에서는 텍스트 붙이기를 할 수 없게 된다.
그렇기 때문에 9x 계열과 유니코드라는 두 토끼를 모두 잡으려면 응용 프로그램은 클립보드로부터 데이터를 읽을 때도 IsClipboardFormatAvailable가 아니라 저 GetPriorityClipboardFormat를 써서 CF_UNICODETEXT와 CF_TEXT를 모두 살펴봐야 한다. 물론 1순위는 유니코드로 하고 말이다.

그리고 빈 클립보드에다 텍스트를 복사해 넣을 때에도 CF_UNICODETEXT부터 먼저 집어넣은 뒤, CF_TEXT 타입도 자동으로 생겨 있는지를 IsClipboardFormatAvailable로 체크하고, 없으면 ansi 텍스트도 수동으로 넣어 줘야 한다. Windows NT에서는 유니코드 텍스트만 집어넣어도 CF_TEXT로 붙여넣는 게 저절로 가능하지만 9x에서는 그렇지 않기 때문이다.

참 얼마 만에 클립보드 얘기를 또 꺼내게 됐는지 모르겠다. 지금이야 먼 과거의 유물이 됐다만, Windows 9x 시절에는 텍스트 하나를 유니코드 형태로 주고받을 때는 이렇게 미묘하게 귀찮은 면모가 좀 있었다.

클립보드에다 데이터를 지정하는 SetClipboardData는 GMEM_MOVEABLE을 지정해서 GlobalAlloc으로 평범하게 할당한 메모리 핸들을 넘겨 주는 방식으로 사용하면 된다. 함수를 호출한 뒤부터는 그 메모리 핸들은 운영체제의 관할로 넘어가기 때문에 우리가 임의로 해제해서는 안 된다.
이것과 굉장히 비슷한 개념으로 운용되는 다른 함수는 SetWindowRgn인 것 같다. 평범한 HRGN을 넘겨 주지만 이제부터 이 region은 운영체제의 관할이 되기 때문이다. 우리가 더 건드려서는 안 된다.

3.
C/C++은 여느 언어와 마찬가지로 const라는 속성이 존재하는데, 이것은 (1) 완전히 붙박이 상수가 될 수 있으며, 아니면 (2) 런타임 때 가변적인 초기값이 붙는 것도 가능하지만, 한번 값이 정해진 뒤에는 불변이라는 의미도 될 수 있다.
이거 혼동하기 쉬운 개념이다. C/C++의 const는 (1)은 딱히 보장하지 않고 일단은 (2)만 보장한다.

const형 변수를 초기화하는 초기값으로는 반드시 case 레이블이나 static 배열의 크기처럼 컴파일 타임 때 값이 결정되는 상수만 와야 하는 게 아니다. 변수도 얼마든지 초기값으로 지정할 수 있으며, 이건 그냥 const뿐만 아니라 static const에 대해서도 마찬가지이다. 애초에 const는 타입에 제약이 없으니까. (1)을 정수 명칭에 한해서 보장해 주는 건 enum, 아니면 템플릿 인자이다.

그래서 C#은 이 두 개념을 const와 readonly로 확실하게 구분해 놓았는데, 이건 상당히 매력적인 조치라 여겨진다. 배열· 테이블도 const로 지정이 가능하니까 말이다. 또한 붙박이 불변 const는 스택에든 어디든지 여러 인스턴스가 중복해서 존재할 이유가 하등 없으니 static 속성도 자동으로 포함하는 걸로 간주된다.

한편, 포인터가 const라면(가령 p) p 자신의 값을 바꿀 수 없는지, 아니면 p가 가리키는 메모리의 값을 바꿀 수 없는지(*p 혹은 p->)가 문제가 된다. const TYPE *p라고 하면 *p가 잠기고, TYPE * const p라고 하면 p가 잠긴다. const TYPE * const p라고 하면 짐작하시겠지만 둘 다 잠긴다.
문법이 좀 복잡한데, const T와 T const는 의미상 동일하다. 그리고 포인터값 자체를 잠글 때는 * 뒤에다가 const를 추가로 붙인다고 생각하면 비교적 직관적으로 이해할 수 있다.

하지만 포인터라는 게 지니는 값은 NULL 같은 특수한 경우가 아니고서야 언제나 가변적인 메모리 주소이기 때문에.. 포인터에다 const는 p 자신이 아닌 *p를 잠그는 용도로 훨씬 더 많이 쓰인다. 클래스 멤버 함수에 붙는 const도 this 포인터에 대해서 *this를 읽기 전용으로 잠그는 역할을 한다. this는 예약어이다 보니 this의 주소값 자체를 변경하는 건 어떤 경우든 원래부터 가능하지 않으니 말이다.

그리고 *p가 잠긴 const 포인터에 대해서 그 잠김을 일시적으로 해제하는 형변환 연산자는 잘 알다시피 const_cast이다. 다만, const가 사라진 TYPE *라는 타입명을 그 뒤의 <> 안에다가 수동으로 일일이 써 줘야 한다. 이름은 const_cast이지만 얘는 '쓰기 금지'뿐만 아니라 언어 차원에서의 타입과 "무관하게" 변수 접근 방식과 관련하여 붙어 있는 추가적인 제약 속성을 제거할 때에도 쓰인다. volatile이 대표적인 예이고, 비주얼 C++의 경우 IA64 아키텍처 기준으로 __unaligned 속성도 이걸로 제거할 수 있다. (IA64는 기본적으로 machine word 단위로 align이 된 곳만 메모리 접근이 가능한지라..)

Windows API에는 포인터의 const-ness 구분 때문에 불편한 게 몇 군데 있다. 대표적인 건 CreateProcess 함수.
실행할 프로세스를 가리키는 명령줄은 PCTSTR이 아니라 PTSTR이다. 함수가 내부적으로 실행될 때는 파일명과 인자명을 구분하기 위해서 문자열의 중간에 \0을 삽입해서 토큰화를 하기 때문이다. 즉, strtok 함수가 동작하는 것처럼 토큰화를 하며 문자열을 변경한다.

이 함수는 실행 후에 문자열 내용을 다시 원래대로 되돌려 놓긴 하지만, 명령줄 문자열을 나타내는 메모리는 일단 read-only가 아니라 쓰기가 가능한 놈이어야 한다. *a+=0; 을 수행하려면 비록 *a의 값이 실제로 변경되지는 않지만 그래도 가리키는 메모리가 변경 가능해야 하는 것과 비슷한 이치이다.
Windows NT 계열에서 CreateProcessA 함수를 호출하면 얘는 어차피 문자열을 쓰기 가능한 문자열에다가 유니코드로 변환해서 내부적으로 W 함수가 사용된다. 그렇기 때문에 A함수에다가 (PSTR)"notepad.exe sample.txt" 이런 식으로 줘도 안전하긴 하다. 그러나 범용성을 고려하면 이는 그렇게 깔끔한 코드라고 보기 어렵다.

다음으로 DrawText도 기능을 제공하는 형태가 조금 지저분하다.
얘는 문자열을 받아들여서 출력하는데 폭이 부족할 경우 문자열을 있는 그대로 출력하는 게 아니라 "Abc..."라고 줄여서 출력하는 옵션이 있다. 그런데 사용자가 원한다면 자기가 받은 문자열 자체를 그렇게 출력되는 형태로 '수정해' 주는 옵션이 있다(DT_MODIFYSTRING). 이 옵션을 사용한다면 str은 PCTSTR이라는 프로토타입과는 달리 쓰기 가능한 포인터여야 한다.
또한 RECT *인자도 좀 아쉬운 구석이 있다. 일단은 in/out 겸용이어서 RECT *이지만, 이것도 out 없이 문자열을 찍을 영역만 지정하는 in 전용이 가능했으면 좋겠다.

끝으로, 리스트/트리 같은 공용 컨트롤에 데이터를 추가하는 LVITEM이 있다. 이들 구조체의 pszText는 PCTSTR이 아니라 PTSTR이다. 문자열을 얻어 올 때와 지정할 때 모두 동일한 구조체가 쓰이기 때문에 const 속성이 없는 것이다.
이 때문에 컨트롤에다 데이터를 처음 추가할 때 item.pszText=const_cast<PSTR>("Hello") 같은 연산이 부득이 쓰이기도 한다.

C++과는 달리 자바에는 클래스 멤버 함수라든가, call-by-reference로 전달되는 데이터에 대해서 const-ness를 보증하는 개념이 없다. 클래스에서 Get*으로 시작하는 메소드라고 해서 const를 따로 지정하지는 않으며, final 키워드는 함수의 오버라이드 불가, 값의 변경 불가 같은 '종결자' 용도로만 쓰인다. 과연 const가 없어도 괜찮은 걸까?

그런데 포인터의 단계가 복잡해지다 보면, const가 있어 봤자 가리키는 놈이 const인지, 나 자신이 const인지 일일이 따지고 추적해서 내부 상태의 불변성을 보장하겠다는 시도가 별 의미가 없어지긴 한다. 즉, this가 this와 *this까지 몽땅 봉인되어서 나 자신의 멤버 값은 변경할 수 없지만.. 내가 가리키는 메모리 영역의 값을 한 단계 거칠 수가 있으며 거기 값을 변경하는 게 가능하다면.. 궁극적으로 그 개체의 속성/상태는 바뀌었다고 볼 수 있기 때문이다.

어떤 코드를 실행해 보지 않고 메모리 접근이 안전한지 입증하는 정적 분석 기능이 만들기 까다로운 것도 본질적으로 이런 이유 때문이다. 컴파일러의 경고가 동작을 예측하고 비정상을 지적해 줄 수 있는 것도 간단한 지역변수 수준일 뿐이지, 몇 단계짜리 포인터에 구조체가 줄줄이 따라오면.. 답이 없다.

거기에다 클래스의 인터페이스 상으로는 Set*이 아니라 Get*처럼 "read-only"이고 값이 변하지 않는 기능만 사용한다고 하더라도, 클래스의 내부 상태는 얼마든지 바뀔 수가 있다. 한번 구해 놓은 계산값을 다음에는 빠르게 되돌리기 위해서 그 값 자체 내지 중간 상태를 캐싱하는 것처럼 말이다. 이런 경우가 워낙 많으니 C++에서는 나중엔 멤버의 const-ness에 예외를 허용하는 mutable 키워드도 도입하지 않을 수 없었다.

이건 마치 고유명사와 보통명사의 경계와도 비슷한 맥락의 문제인 것 같다. 로마자 알파벳이 한글과는 달리 대문자가 있고 고유명사는 첫 글자를 대문자로 적게 돼 있어서 일면 편리한 건 사실이다. 하지만 좀 깊게 생각을 해 보면, 새로운 명칭이 만들어졌을 때 그걸 고유명사로 볼지 기준이 생각만치 엄밀하지는 않은 경우가 많다. 기준이 생각만치 엄밀하지 않으며 그냥 자의적이다. 번역하지 않고 음역하는 놈은 죄다 고유명사인 걸까? 실체가 지구상에 오로지 하나밖에 없는 놈? 그렇다면 실물이 존재하지 않는 무형의 개념은? 그걸 정하는 기준은 무엇?

지금 우리가 보통명사로 별 생각 없이 쓰는 명칭들도 그 명칭이 처음 등장했던 시절엔 첫 글자를 대문자로 적는 유니크한 고유명사이기도 했다. const 개념 구분도, 대소문자 구분도 있건 없건 어느 하나가 다른 하나보다 절대적으로 우월하고 좋은 솔루션은 아니어 보인다는 생각이 든다.

에휴, 클립보드부터 시작해서 유니코드, const 등 별별 이야기가 다 이어져 나왔다. 이상~~ ^^

Posted by 사무엘

2016/02/12 08:36 2016/02/12 08:36
, , , ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1192

C/C++에서 대괄호 [ ]는 일단 배열과 관련이 있는 문자이다.
타입을 선언하는 문맥에서 a[...]라고 하면 a가 배열임을 나타내고 선언한다. [] 안에는 배열의 크기를 나타내는 정수 상수(= 컴파일 타임 때 바로 값이 결정되는)만이 올 수 있다.
그 반면 값을 계산하는 문맥에서 a[...]는 일단은 배열이나 포인터를 역참조하는 *(a+...)와 동치가 되기 때문에, 상수가 아닌 임의의 변수값이 들어올 수 있다. 게다가 C++에서는 []를 연산자로서 오버로드도 할 수 있으므로 정수가 아닌 다른 타입의 값이 들어오는 게 가능하다.

그럼 속에 아무것도 없이 말 그대로 []만 달랑 오는 건 언제 가능할까?
일단 값을 계산하는 실행 문맥에서는 전혀 가능하지 않다. 함수 호출이 아닌 일반 수식에서 일반 괄호가 ()요렇게만 달랑 올 수가 없는 것과 같은 이치이다. 또한 인자가 전혀 없는 형태로 [] 연산자 함수를 오버로딩 하는 것도 안 된다.
얘가 쓰이는 가장 유용하고 편리하고 직관적인 상황은, 이니셜라이저로부터 크기를 자동으로 유추할 수 있는 배열을 선언하여 곧장 초기화할 때이다.

char pt[] = "Hello"; //null 문자까지 합해서 6이라는 크기가 자동으로 유추됨.
static const int samp[] = {1, 5, 7, 8}; //4라는 크기가 자동으로 유추됨.

이 정도야 []는 명백하게 배열을 나타내며, 그 의미를 충분히 납득할 수 있다. 헷갈릴 게 없다.
그리고 지역변수 말고 다른 번역 단위에 있는 배열을 요런 게 있다고 선언만 해 놓는 문맥일 때도 구체적인 크기가 생략된 arr[] 같은 구문이 올 수 있다. 이 경우 클래스로 치면 forward 선언과 얼추 비슷한 역할을 한다. 배열을 참조하는 코드를 생성할 수는 있지만 sizeof 연산자는 쓸 수 없게 된다. 온전한 배열은 아니지만 포인터도 아니기 때문에 여기에 다른 임의의 주소를 대입할 수는 없다.

그런데 []는 일부 제한된 문맥과 범위에서는 그냥 포인터를 나타낼 때도 있다. 이거 다시 보니 굉장히 요물스럽게 느껴진다.

void foo(char ptr[]);
void foo(char *p);

위와 아래의 함수 선언은 완전히 동치이다. 그렇기 때문에 저 두 쌍으로는 함수 오버로딩을 할 수 없다.
이게 요물스럽게 느껴지는 이유는, (1) 첫째, 함수의 인자에서만 사용 가능한 문법이기 때문이다.
일반 변수를 선언할 때야 char ptr[]은 포인터가 아니라 배열로 인식된다. 그렇기 때문에 이니셜라이저 없이 저렇게만 달랑 써 놓으면 크기를 알 수 없다고 컴파일 에러가 난다.
그에 반해 함수 인자에서는 배열을 선언하거나 초기화 따위 할 일이 없으니 ptr[]은 *p와 동일하게 인식된다.

우리는 프로그램으로부터 명령 인자를 받기 위해 main 함수로부터 argc, argv 인자를 살펴본다. 그런데 정말 이상한 건 argv는 거의 언제나 관행적으로 *argv[]라고 선언해 놓고 써 왔다는 것이다. 이건 알고 보면 **argv와 본질적으로 완전히 동치일 뿐인데도 말이다. 하지만 *argv[]와 **argv는 왠지 느낌상 서로 동일해 보이지가 않는다.

함수 인자와는 달리 리턴값은 char *bar()만 가능하지 char []bar 이렇게 쓸 수 없다. 함수 인자 말고는 리턴값이나 지역 변수 선언, 형변환 연산자, typedef 등 타입을 명시하는 그 어떤 문맥에서도 []를 저런 식으로 쓰는 건 문법적으로 가능하지 않다.

(2) 둘째, []는 그렇다고 해서 함수 인자이기만 하면 *와 완전히 등가 교환이 가능한 것도 아니다.
[]의 지원 범위는 오로지 1중 포인터 한정이다. 2중 포인터인 char **a를 a[][] 따위로 대체할 수는 없으며 오로지 한 단계만 *a[]로 대체할 수 있을 뿐이다. 한 단계 포인터를 대체할 수 있다는 점에서는 C++의 참조자 &를 살짝 닮은 것 같기도 하다.

그럼 함수 인자에서는 왜 저걸 예외적으로 가능하게 해 놓은 것일까?
정확한 이유는 알 수 없지만 C/C++은 배열은 함수 인자로 전달할 때 값이 새로 복제되는 게 아니라, 언제나 주소(= 포인터) 형태로만 전달한다는 점을 부각시키기 위해서 저렇게 한 게 아닌가 싶다. N차원 배열은 언제나 N-1차원 배열의 1중 포인터가 된단 얘기다. 다차원 배열을 선언해서 함수로 전달하는 상황을 생각해 보자.

int table[][2][4]={
    {{1,2,3,4}, {3,4,5,6}}, {{2,6,2,3}, {5,6,3,4}}, {{1,}, {2,}}
};

foo(table);

사실 C/C++은 엄밀히 말하면 다차원 배열이라는 것도 없고 그저 '배열의 배열'이 있을 뿐이다.
2*4 배열의 배열을 선언한다는 건 생략할 수 없고, 유일하게 생략할 수 있는 건 제일 겉에 있는 최종 원소의 개수(저기서는 3)이다.
이런 3차원 배열을 인자로 받는 함수는 프로토타입을 어떻게 만들면 좋을까?

void foo(int a[][2][4]);

table은 함수의 인자로 전달할 때는 그대로 2*4 배열의 포인터가 되고, 그걸 함수 인자로 표현하는 걸 배열 변수를 선언할 때와 동일하게 [][2][4]로 할 수가 있다. 함수 인자에서 []의 의미는 바로 여기에 있다고 보는 게 타당할 것이다. 함수 인자에서도 [] 안에 숫자를 생략할 수 있는 건 제일 겉의 차원수 하나뿐이다.

위의 함수의 인자는 개념적으로 아래와 완전히 동일하다. *(a+2)와 a[2]가 동일한 것만큼이나 서로 동일하다.

void Fune(int (*a)[2][4]);

a[][2][4]를 사용하지 않았으면 그냥 포인터나 다중 포인터가 아니라 '배열의 포인터'를 표현하기 위해 저렇게 괄호를 쳐야 했을 텐데 [] 표기는 괄호가 또 들어갈 필요를 없애 주고 표현을 간결하게 만들어 준다.
배열 선언 table[][2][4]에서는 []는 원소 개수가 이니셜라이저에 명시돼 있다는 뜻이고, 함수 인자 a[][2][4]에서는 이게 포인터라는 뜻이라니..;;

이건 무슨 최신 C++ 기능도 아니고 C의 완전 기초 필수 아이템인데도... 본인조차 []의 의미와 용도에 대해서 제대로 진지하게 생각을 안 했던 것 같다. 신기하기 그지없다.

Posted by 사무엘

2016/02/04 08:27 2016/02/04 08:27
,
Response
No Trackback , 4 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1189

지금 와서 가만히 생각해 보니, 컴퓨터 알고리즘을 동원하여 푸는 문제들은 다음과 같은 세 범주로 나눌 수 있는 것 같다. 뒤로 갈수록 설명이 길어진다.

1. 최적해를 다항 시간 만에 구할 수 있으며, 직관적인 brute-force 알고리즘과 뭔가 머리를 쓴 알고리즘이 시간 복잡도 면에서 충분히 유의미한 차이를 보이는 문제

간단한 발상의 전환으로 인해서 속도가 드라마틱하게 빨라질 수 있고, 알고리즘에 대한 정량적인 분석도 어렵지 않게 다 되는 경우이다. 요런 게 알고리즘 중에서는 가장 무난하다. 정보 올림피아드에도 이런 부류가 가장 많이 나온다.
가장 전형적인 예는 시간 복잡도 O(n^2)가 O(n log n)으로 바뀐다거나, 지수함수 복잡도가 O(n^2)로, 혹은 O(n^3)이 O(n^2)로 바뀌는 것이다. 물론 시간 복잡도를 줄이기 위해서는 공간 복잡도가 시공간 trade-off 차원에서 추가되는 경우가 대부분이다. 중간 계산 결과들을 모두 저장해 놓는 다이나믹 프로그래밍 문제가 대표적인 예이다.

정렬, common subsequence 구하기, 그래프에서 최단거리 찾기 같은 깔끔하고 고전적인 문제들이 많다. 기하 분야로 가면 convex hull 구하기, 거리가 가까운 두 점 구하기도 있다. 하지만 세상에 산적한 문제들 중에는 이 1번 부류에 속하지 않는 것도 많다.

2. 최적해를 다항 시간 만에 구하는 것이 가능하지 않은 (것으로 여겨지는) 문제

P에는 속하지 않지만 NP에는 속하는 급의 문제이다. 이건 다항 시간 만에 원천적으로 풀 수 없는 문제를 말하는 게 아니며 개념과 관점이 사뭇 다르다. 비결정성 튜링 기계라는, 실물이 없는 이론적인 계산 기계에서는 그래도 다항 시간 안에 풀 수 있다는 뜻이다.

입력 데이터의 개수 n에 비례해서 상수의 n승 내지 n 팩토리얼 개수의 가짓수를 일일이 다 따져야 하는 문제라면 다항 시간 만에 풀 수가 없다. 그런데 실생활에는 이런 무지막지하게 어려운 문제가 은근히 많이 존재한다. 진짜 말 그대로 n!개짜리 뺑이를 쳐야 하는 외판원 문제가 대표적이고, 그래프에도 '해밀턴 경로 문제'처럼 이런 어려운 문제가 산적해 있다. 이런 분야의 문제는 소위 말하는 NP-complete, NP-hard이기도 하다.

요런 문제는 brute force 알고리즘으로는 대용량 데이터를 도저히 감당할 수 없고 그렇다고 다항 시간 최적해 알고리즘이 있는 것도 아니기 때문에, 이런 문제는 100% 최적해는 포기하고 그 대신 95+n%짜리로 절충하고 시간 복잡도는 O(n^2)로.. 뭔가 손실 압축스럽게 tradeoff를 하게 된다.
국제 정보 올림피아드에는 이런 문제가 많이는 안 나오지만 전혀 안 나오는 건 아니다. 출제된다면 답은 최적해와의 비율로 점수가 매겨지며, 프로그램 실행이 아닌 그냥 제출형으로 출제되기도 한다.

P와 NP 사이의 관계는 전산학계에서 만년 떡밥이다. 현실에서는 마치 장기간 실종자를 법적으로 사망한 것과 마찬가지로 간주하듯이 P와 NP는 서로 같지 않다고 여겨지고 있다. 이를 전제로 깔고 발표된 연구 논문들도 수두룩하다. 하지만 그게 정말로 딱 그러한지는 전세계의 날고 기는 수학자들이 여전히 완벽하게 규명을 못 하고 있다.

엔하위키에는 P!=NP임을 증명하는 사람은 전산학 전공 서적에 이름이 실릴 것이고, P=NP임을 증명하는 사람은 아예 초등학생 위인전에 등재될 것이라고 얘기를 했는데... 적절한 비유인 것 같다. 지수함수 brute force 말고는 답이 없는 문제가 좀 있어야 암호와 보안 업계도 먹고 살 수 있을 텐데..!

3. 최적해를 다항 시간 만에 구할 수 있음이 명백하고, naive 알고리즘도 실생활에서 그럭저럭 나쁘지 않은 결과가 나오지만, 그래도 미시적· 이론적으로는 최적화 여지가 더 있는 심오한 문제

말을 이렇게 어렵게만 써 놓으면 실감이 잘 안 가지만 이 그룹에 속하는 문제의 예를 보면 곧장 "아~!" 소리가 나올 것이다. 이 분야에도 어려운 문제들이 은근히 많다.

(1) 문자열 검색
실생활에서는 그냥 단순한 알고리즘이 장땡이다. 원본 문자열을 한 글자씩 훑으면서 그 글자부터 시작하면 대상 문자열과 일치하는지 처음부터 일일이 비교한다. 실생활에서 텍스트 에디터는 대소문자 무시, 온전한 단어 같은 복잡한 옵션들이 존재하며 각 글자들의 변별성도 높다(대상 문자열과 일치하지 않는 경우 첫 한두/두세 글자에서 곧바로 mismatch가 발생해서 걸러진다는 뜻). 그 때문에 그냥 이렇게만 해도 딱히 비효율이 발생할 일이 없다.

하지만 문자열 검색이라는 건 실무가 아닌 이론으로 들어가면 생각보다 굉장히 심오하고 난해한 분야이다. 원본과 대상 문자열이 자연어 텍스트가 아니라 오로지 0과 1로만 이뤄진 엄청 길고 빽빽하고 아무 치우침이 없는 엔트로피 최강의 난수 비트라고 생각하자. 그러면 예전에 패턴이 어디서부터 어긋났는지를 전혀 감안하지 않은 채 오로지 1글자씩만 전진하는 방식은 효율이 상당히 떨어진다. 이제야 좀 더 똑똑한 문자열 검색 알고리즘이 필요해진다.

퀵 정렬의 중간값(pivot) 선택 알고리즘을 의도적으로 엿먹이는 '안티' 데이터 생성 알고리즘만큼이나..
특정 문자열 검색 알고리즘을 엿먹여서 언제나 최악의 경우로 한 글자씩만 전진하게 만드는 문자열 데이터를 생성하는 안티 알고리즘도 있을 것이다.

(2) 팬케이크 정렬
a1부터 a_n까지 임의의 수 배열이 존재하는데, 우리가 이 수열에 대해 취할 수 있는 동작은 여느 정렬 알고리즘처럼 임의의 두 원소끼리의 교환이 아니다. 1~2, 1~3 또는 1..m (m<=n)처럼 첫째부터 m째의 원소들을 모조리 역순으로 뒤집는 것만 가능하다. 1 7 4 2였으면 2 4 7 1로 바꾼다는 것. n개의 임의의 수열이 있을 때 수열을 정렬하기 위해 필요한 이론적인 최대 뒤집기 횟수는 정확하게 얼마나 될까? 한꺼번에 몇 개를 뒤집건 한번 뒤집는 데 걸리는 시간은 무조건 상수라고 가정하고, 뒤집기 자체 외에 다른 계산의 비용(가령, 현 구간에서 maximum 값을 찾는 것)은 전혀 고려하지 않아도 된다.

본인은 아주 어렸을 때 GWBASIC 교재에서 이 팬케이크 정렬 문제와 같은 방식으로 수열을 뒤집어서 "사람으로 하여금 문제를 풀게 하는" 프로그램을 본 기억이 있다. 프로그램의 이름이 REVERSE였다.
이 문제는 마치 선택 정렬과 비슷한 방식으로 명백한 해법이 존재한다. 가장 큰 수가 m째 원소에 존재한다면 m만치 뒤집어서 가장 큰 수가 맨 처음에 오게 한 뒤, 판 전체를 뒤집어서(n만치) 그 수가 맨 뒤로 가게 하면 된다. 이 과정을 그 다음 둘째, 셋째로 큰 수에 대해서 계속 적용하면 된다.

그렇게 명백한 해법의 계산 횟수는 최대 2*n-3으로 알려져 있다. 하지만 이것은 그렇게 뒤집은 여파가 다음으로 큰 수들을 정렬하는 데 끼치는 영향이 감안되어 있지 않다. 물론 여기서 좀더 머리를 써 봤자 2n이던 계수가 1.xx 정도로나 바뀌지 그게 n 내지 심지어 log n급으로 확 바뀌지는 못한다. 비록 O(n) 표기상으로는 동일하지만 그렇게 상수 계수를 조금이라도 줄이는 최적화이다 보니, 알고리즘이 더 까다롭고 머리가 아프다.

마이크로소프트의 창립자인 그 빌 게이츠가 1979년에 바로 이 문제의 계산 횟수를 최적화하는 알고리즘을 (공동) 연구하여 이산수학 학술지에다 투고했었다. 이 사람의 기록은 그로부터 거의 30년이 지난 2008년에야 더 정교한 알고리즘이 나옴으로써 깨졌다. 이것은 빌이 단순히 비즈니스맨이기만 한 게 아니라 엔지니어 기질도 얼마나 뛰어났고 수학 쪽으로도 얼마나 천재였는지를 짐작케 하는 대목이다. 학부 중퇴 학력만으로도 이미 전산학 석· 박사급의 걸출한 리서치를 했으니 말이다.

(3) 행렬의 곱셈
갑자기 팬케이크 정렬 얘기가 좀 길어졌는데 다음 항목으로 넘어가자면.. 계산 관련 알고리즘도 이런 급에 속한다. 대표적으로 행렬.

일반적으로야 두 개의 n*n 정방행렬끼리 곱셈을 하는 데 필요한 계산량, 정확히 말해 두 수 사이의 곱셈 횟수는 정확하게 O(n^3)에 비례해서 증가한다. 그러나 거대한 행렬을 2*2 형태의 네 개로 쪼개고, 덧셈을 늘리는 대신 곱셈을 줄이는 방식으로 최적화를 하는 게 가능하다. 게다가 쪼개진 행렬이 여전히 크다면 그걸 또 재귀적으로 쪼갤 수 있다.
a+bi와 c+di라는 복소수의 곱셈을 위해서 통상적으로는 ab, ac, bc, bd라고 곱셈이 총 4회 필요하다고 여겨지지만 실은 덧셈을 더 하는 대신에 곱셈은 ac, bd와 (a+b)*(c+d)로 3회로 줄일 수 있지 않은가? 그런 식으로 줄인 것이다.

그렇게 해서 O(n^3)보다 이론상 작은 시간 복잡도가 최초로 제안된 게 1969년에 나온 슈트라센 알고리즘이다. 대략 O(n^2.8). 정확하게 2.8인 건 아니고 지수 자체가 로그 n 이런 형태로 떨어진다. 프랙탈의 차원 수가 로그로 표현되는 것처럼 말이다.
여기서 2.8x의 정확한 의미는 log[2] 7이다. 원래 2*2 행렬 두 개를 곱하기 위해서는 상수 곱셈이 8회 필요한데, 중간 과정의 공식들을 궁극의 캐사기 테크닉을 동원하여 변형했다. 어마어마한 양의 우회 연산을 통해 덧셈은 횟수가 왕창 늘었지만 곱셈이 8회에서 7회로 딱 1회 줄었다! (도대체 무슨 약 빨고 연구해서 이런 걸 생각해 냈을까? ㄷㄷ) 이 여파가 분할 정복법의 특성상 재귀· 연쇄적으로 적용된 덕분에 전체 시간 복잡도가 감소한 것이다.

그리고 이 바닥도 발전에 발전을 거듭한 덕분에 오늘날은 무려 O(n^2.4)대까지 곤두박질쳤다. 덧셈과는 달리 곱셈은 이런 최적화의 여지가 존재한다는 사실 자체가 아주 신기하지 않은가? 크기가 서로 다른 행렬들의 최소 곱셈 횟수를 구하는 다이나믹 프로그래밍 문제하고는 완전 별개의 영역이다.

아래의 그림을 보자(움짤임). RGB라는 세 대의 차량이 서로 부딪치지 않고 G는 그대로 위로, R과 B는 서로 좌우가 엇갈리게 빠져나가려면 어떻게 하면 좋을까? 아래의 중앙은 길이 막혔기 때문에 횡단을 할 수 없다.
결국 가운데 G는 곧이곧대로 위로 나가서는 안 되며, R과 B의 경로를 피해서 몇 배나 더 긴 우회를 해야 한다. 하지만 그래도 RGB 모두 신호 대기가 없이 서로 엇갈리는 방향으로 술술 소통이 가능하다.

사용자 삽입 이미지

자연에는 관성이라는 게 존재하니, 다리가 아니라 바퀴가 달린 자동차나 열차에게는 우회를 하더라도 이게 훨씬 더 나은 방법인 것이다.
행렬도 덧셈이라는 우회가 아무리 몇 배로 더 늘어 봤자, 아주 큰 행렬(차량 소통이 엄청 많을 때)에 대해서는 곱셈이 눈꼽만치라도 줄어드는 게 도로로 치면 신호 대기가 없어지는 것에 맞먹는 이익이 될 수 있다는 생각이 든다.

물론 행렬의 곱셈 시간 복잡도가 O(n^2)보다 더 낮아질 리는 없으며, 저런 알고리즘들은 지수를 줄이는 대신 공간 복잡도(스택 사용..) 같은 다른 오버헤드가 왕창 커졌다는 점을 감안해야 한다. 크기가 몇십~몇백 정도 되는 초대형 행렬에서 두각을 발휘하지, 그냥 3차원 그래픽용으로나 간단히 쓰이는 3*3이나 끽해야 4*4 행렬에서 적용할 만하지는 않다.

Posted by 사무엘

2016/01/12 08:30 2016/01/12 08:30
, , ,
Response
No Trackback , 9 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1181

Windows 프로그래밍의 관점에서 볼 때 대화상자는 참 독특하면서도 중요한 위상을 차지하는 GUI 구성요소이다.
대화상자에는 누구나 공통으로 갖추고 있어야 하는 동작과 처리가 있으면서 한편으로 각종 자식 컨트롤로부터 notification을 처리하는 건 대화상자마다 제각각 customize가 가능해야 한다.

그래서 대화상자 함수는 customization을 위해 사용자가 작성한 대화상자 메시지 처리 콜백 함수를 인자로 받는다. 그리고 WM_SETICON 같은 메시지를 통해 아이콘조차도 클래스 차원이 아니라 윈도우 메시지 차원에서 필요하다면 변경할 수 있게 해 놓았다.
그럼 custom 프로시저가 가로채지 않은 나머지 공통 처리들은.. 마치 DefWindowProc처럼 DefDlgProc 같은 대화상자 윈도우 프로시저를 호출하는 것만으로 전부 가능해야 할 것 같지만, 사실은 그렇지 않다. Alt+단축키 처리, 그리고 포커스 이동 시에 '확인' 같은 default 버튼의 비주얼을 바꾸기 같은 것은 대화상자 윈도우 자체가 메시지를 받는 게 아니라 각 child 컨트롤들이 키보드 메시지를 받고 있을 때 그걸 가로채서 해치워야 한다. 대화상자 윈도우가 무슨 훅킹이라도 하고 있지 않은 이상 말이다.

이런 이유로 인해 대화상자의 동작을 위해서는 message loop 차원에서 메시지를 가로채는 로직이 필요하다. 그래서 IsDialogMessage라는 함수가 그 코드에 들어가야 한다. 물론 DialogBox 함수로 modal 대화상자를 만들었을 때는 해당 함수가 자체적으로 message loop을 돌리면서 그 처리를 당연히 알아서 해 주니, 저런 별도의 함수는 우리 쪽에서 modeless 대화상자를 운용할 때에나 필요하다.

사실 저 함수는 용도를 생각하면 Is..가 아니라 TranslateDialogMessage 정도로 작명이 됐어야 했다. 그래야 오해가 없다. 단순히 쿼리에 대한 판별값을 되돌리는 게 아니라 실제로 무슨 일을 수행하기 때문이다.

뭐, message loop은 그렇다 치고, 대화상자를 대상으로 생성된 메시지는 우리가 지정한 대화상자 프로시저 → 대화상자 자체의 윈도우 프로시저(DefDlgProc) → 운영체제가 제공하는 DefWindowProc 이런 순으로 메시지를 처리하게 된다. 앞 계층에서 처리하지 않은 메시지가 다음 계층으로 간다는 뜻이다.
C++이라면 이건 깔끔한 클래스의 상속 관계로 표현할 수 있을 것이다. 그러나 Windows가 처음 개발됐을 때는 C++이 쓰이지 않았었다. 그리고 결정적으로, 모종의 이유로 인해 대화상자 프로시저와 윈도우 프로시저는 형태가 미묘하게 서로 달라져서 온전한 일관성이 보장되지 않는다.

오리지널 윈도우 프로시저는 내가 메시지의 처리를 한다면 리턴값을 바로 되돌리면 되고, 내가 처리하지 않은 메시지에 대해서는 DefWindowProc()를 해 주면 된다.

return ret; //처리한 경우
return DefWindowProc(hWnd, msg, wParam, lParam); //처리하지 않은 경우

그러나 대화상자 프로시저는 자기가 이미 DefDlgProc라는 디폴트 처리 함수(= 대화상자의 원래 윈도우 프로시저)로부터 호출을 받은 구도이다. 이것이 디자인 상으로 가장 본질적인 차이점이다.
그래서 리턴값으로는 이 메시지를 우리가 처리했는지의 여부만을 BOOL 형태로 되돌리고, 메시지 리턴값은 따로 꽤 번거롭게 넣어야 한다.

SetWindowLongPtr(hDlg, DWLP_MSGRESULT, ret); return TRUE; //처리한 경우
return FALSE; //처리하지 않은 경우

저럴 거면 차라리 함수의 인자에다가 LRESULT *pnResult 같은 포인터를 추가해서 거기로 되돌릴 수 있게라도 하지 하는 아쉬움이 남는다.

*pnResult=ret; return TRUE; //처리한 경우 -- 대안 1
*pbEaten=TRUE; return ret; //처리한 경우 -- 대안 2

그런데, 대화상자 프로시저에도... 형태가 간단한 극소수의 예외적인 메시지는 리턴값을 SetWindowLongPtr이 아니라 일반 윈도우 프로시저처럼 자신의 리턴값으로 되돌린다. 그렇기 때문에 대화상자 프로시저는 대부분의 경우 TRUE/FALSE만을 되돌림에도 불구하고 리턴값이 쿨하게 BOOL이 아니라 INT_PTR로 지정되어 있다. LRESULT도 아니고 이거 참.. 지저분하다면 지저분하다.

그 예외로는 자식 컨트롤의 배경을 칠할 브러시를 되돌리는 WM_CTL*, owner-draw 컨트롤에서 아이템 간의 대소를 비교하는 WM_COMPAREITEM, 그리고 역시 owner-draw 컨트롤에서 아이템의 바로가기 key를 지정하는 WM_(CHAR/VKEY)TOITEM이 여기에 해당한다. COM 함수라고 해도 실패를 할 우려가 전혀 없고 리턴값도 BOOL 값 달랑 하나 같은 건 굳이 BOOL *pfResult라는 인자로 주기보다는 간단히 S_OK, S_FALSE라는 자기 리턴값으로 되돌리는 게 더 나은 것과 같은 이치이다.

그리고 저 예외 메시지들은 owner-draw처럼 애초에 custom 동작을 염두에 두고 만들어진 메시지이므로 default로 넘기느냐 마느냐를 따지는 게 전혀 무의미하다. 그러니 리턴값을 바로 직통으로 사용하는 것이 더 간편하고 낫다. WM_CTL*의 경우도 컨트롤을 서브클래싱할 때에나 쓰이니 이 역시 customization과 관계가 있다.

왜 저렇게 예외가 만들어졌는지 이제 이해는 되지만, 그래도 대화상자 프로시저의 인터페이스가 구리고 지저분하고 복잡해 보이는 건 어쩔 수 없는 사실이다. 그래서 대화상자 프로시저도 윈도우 프로시저와 비슷한 구조로 만들 수 있게 하는 일종의 코딩 디자인 템플릿이 있다. MFC 같은 C++ 프레임워크는 밑바닥에서 당연히 이런 일도 다 해 주고 있지만, C만 쓴다거나 MFC보다 더 가벼운 Windows API 프레임워크를 우리가 직접 만드는 상황이라면 그 내부 디테일을 알 필요가 있다.

기본적인 아이디어는 이렇다.
운영체제의 DefDlgProc는 우리 프로시저를 먼저 호출하여 메시지의 처리 여부를 묻는다. 우리 프로시저는 그 메시지를 처리한 경우라면 리턴값 + TRUE만 되돌리면 되니 끝이다. 그 반면 처리하지 않은 경우, 내부적인 재귀호출 플래그를 설정한 뒤 DefDlgProc을 또 호출한다.
그럼 DefDlgProc는 아까처럼 우리를 또 호출하는데, 이때 우리 프로시저는 이미 설정된 재귀호출 플래그를 보고는 비로소 더 처리를 하지 않는 FALSE를 되돌린다.

또한, 리턴값을 지정할 때 메시지 종류에 따라 곧이곧대로 리턴(일부 예외 메시지들)하거나 SetWindowLongPtr을 호출하는 것은 메시지 핸들러 말고 그 아래의 static 콜백 함수에서 하면 된다.
이 아이디어를 의사코드로 표현하면 다음과 같다.

class CMyDialog {
    static INT_PTR CALLBACK DialogProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam);
    BOOL m_bRecur;
protected:
    virtual LRESULT DlgProc(UINT msg, WPARAM wParam, LPARAM lParam);
public:
    CMyDialog(): m_bRecur(FALSE) { }
};

//클래스에서는 아마 private로 지정되어 있을 우리 대화상자 프로시저 callback.
INT_PTR CALLBACK CMyDialog::DialogProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam)
{
    //HWND로부터 C++ 오브젝트 얻기. 훅킹을 해서 WM_NCCREATE를 잡든가 아니면 WM_INITDIALOG일 때
    //hDlg와 C++ 오브젝트를 연결하는 작업을 할 것. 이 코드에서는 그걸 생략함
    CMyDialog *obj = GetObject(hDlg);

    //BOOL값. 저 값이 true이면 재귀 상태라는 뜻이므로 그걸 false로 바꾸고, 함수도 false로 종료.
    CheckDefDlgRecursion(&obj->m_bRecur);
    //msg가 예외에 속하면 리턴값을 바로 되돌리고, 아니면 SetWindowLongPtr로 리턴값을 지정해 줌.
    return SetDlgMsgResult(hdlg, msg, obj->DlgProc(msg, wParam, lParam));
}

//각 클래스별로 오버라이드 하면 되는 가상 함수.
//윈도우 핸들은 우리 C++ 클래스의 멤버에 포함되어 있다고 가정함.
LRESULT CMyDialog::DlgProc(UINT msg, WPARAM wParam, LPARAM lParam)
{
    //재귀호출 플래그를 켠 뒤에 DefDlgProc를 호출한다.
    return DefDlgProcEx(m_hWnd, msg, wParam, lParam);
}

windowsx.h를 보면 SetDlgMsgResult, DefDlgProcEx, CheckDefDlgRecursion 같은 매크로 함수가 이런 디자인 패턴을 구현하라고 만들어진 물건들이다. 16비트 시절부터 있었고 MFC만큼이나 역사와 내력이 대단히 길다.

<날개셋> 한글 입력기는 지난 3.0때부터 GUI가 그냥 Windows API와 자체 제작 프레임워크를 사용해서 만들어졌지만, 그땐 본인은 이런 기법을 미처 생각하지 못했었다. 그래서 대화상자 프로시저들은 대충 return 1에 SetWindowLongPtr 등을 뒤죽박죽 섞어서 만들어져 있다. 그러나 저런 방법을 진작에 알았으면 대화상자 메시지 처리기도 마치 윈도우 프로시저 스타일로 일관성 있게 만들 수 있었겠다. 이제 와서 수많은 대화상자 프로시저들을 저 기준대로 고치는 것은 무의미한 지경이 됐지만 말이다.

Posted by 사무엘

2016/01/09 08:26 2016/01/09 08:26
, , ,
Response
No Trackback , 6 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1180

컴퓨터 소프트웨어에서 뭔가 기능은 동일하지만 프로토콜(입출력)이 다른 두 시스템을 최상위 계층에서 중재하여 서로 이어 주는 메커니즘을 '썽킹(thunking)'이라는 용어로 표현하는 것 같다. 영한사전에 제대로 등재돼 있지도 않은 신조어인데 정확한 어원이 궁금하다. 영문 위키백과는 a subroutine that is created, often automatically, to assist a call to another subroutine라고 풀이를 하며, 그래서

  • Windows 9x에서 유니코드(W) API 호출 요청이 왔을 때, 매개변수 값을 적당히 조절해서 그에 상응하는 Ansi API를 대신 호출하고 출력 결과도 wide string 기준으로 보정하는 것
  • C++의 멤버 함수 포인터에서 다중 상속된 클래스의 멤버 함수를 호출하기 전에 this 포인터의 오프셋을 보정해 주는 전처리 함수 (보정 정보가 포인터의 내부에 있지 않고 그냥 또 다른 함수를 생성해서 때우는 경우)

이런 것들도 다 넓게는 썽크/썽킹이라고 부른다. 그리고 썽킹을 수행하는 함수를 썽크 함수라고 한다. 다만 Windows에서 썽킹은 컴퓨터 아키텍처의 장벽을 극복하는 호환성 유지 작업을 가리킬 때 주로 쓰인다. 16비트와 32비트 사이, 그리고 요즘은 32비트와 64비트 사이에서 말이다.
그래서 그런 썽킹 시스템을 WoW라고 부른다. 월드 오브 워크래프트...가 아니라 Windows on Windows의 약자로, 말 그대로 Windows 위에서 Windows를 또 가상으로 구동한다는 뜻이다.

오늘날 우리가 사용하고 있는 x86-64 아키텍처는 과거 32비트 IA32의 superset 구도로 설계되었다. 그래서 32비트 x86 코드도 에뮬레이션이 아니라 네이티브 직통으로 실행 가능하기 때문에 Windows 입장에서는 소프트웨어적으로 32비트와 64비트를 모두 지원하기 위해 특별히 힘든 일을 해 줘야 할 건 없다. 64비트 시대에도 어마어마한 32비트 유물들은 비록 바이너리 형태가 아무리 지저분하더라도 결코 외면할 수 없는 물건임이 입증된 셈이다. 비록 성능을 추구했다고는 하지만 x86 코드를 거북이처럼 에뮬레이션으로 돌리고 다른 문제점도 많았던 IA64는 정말 시원하게 망해서 인텔의 흑역사가 됐다.;;

32비트와 64비트는 한 주소 공간 안에서 코드가 섞이는 게 전혀 불가능하며, 서로 교류하는 방법은 API나 커널 오브젝트 차원에서의 IPC 메커니즘밖에 선택의 여지가 없다. 특히 WM_COPYDATA 메시지는 16, 32, 64비트까지 세대를 넘어 두루 통용되는 대단히 훌륭한 해결사이다.
그러나 16비트와 32비트가 공존하던 시절에는 상황이 훨씬 더 지저분했다. 기존 소프트웨어와의 호환성, 그리고 부족한 PC 메모리 같은 현실적인 한계 때문이었다.

그 시절에는 기술적으로 다음과 같은 형용사가 붙은 썽킹들이 존재했다. 썽킹이 무슨 CPU 에뮬레이션까지 하는 건 물론 아니고, 주로 한 일은 최신 32비트 가상 메모리 주소와 구닥다리 16비트의 세그먼트-오프셋 주소를 그냥 상호 변환하는 것이었다.

1. Universal: 16비트 플랫폼에서 16비트 EXE가 32비트 DLL을 실행
이건 가장 원초적인 썽킹이며, 쉽게 말해 Win32s의 기술 수준이 여기에 해당한다.

2. Generic: 32비트 플랫폼에서 16비트 EXE가 32비트 DLL을 실행
이것은 32비트 Windows 9x/NT 계열이 모두 지원하는 썽크로, 방향이 universal과는 반대이다.
본인은 IME 개발자로서 이 썽킹의 존재를 자연스럽게 인지하고 있었다. 16비트 프로그램에서도 <날개셋> 한글 입력기 외부 모듈이 동작했기 때문이다. 물론 곧 오류가 나고 제대로 사용하기는 어렵지만.

Windows 9x의 경우, 16비트 프로세스 내부에서 돌아가는 32비트 코드는 마치 Win32s처럼 극도로 가난한 16비트 컴퓨터의 끔찍한 제약들이 고스란히 적용되었다. 스레드를 생성할 수 없으며 스택 크기도 기본 1MB 이상이 아니라 64KB 미만으로 팍 줄었다. 그러니 복잡한 재귀호출이나 큰 배열조차도 함부로 만들면 안 됐다.

Windows NT는 모든 운영체제의 코드가 32비트 이상이다. 하지만 그렇다고 해서 16비트 코드를 실행하는 기능 자체가 아예 없는 건 아니었으니 16비트 프로그램이 32비트 코드를 사용하기 위한 최소한의 썽킹 계층은 마련하고 있다.

16비트 EXE 안에서 동작하는 32비트 dll이 GetModuleFileName(NULL)을 해 보면 9x 계열의 경우, 16비트 EXE 이름이 아니라 그냥 kernel32.dll이 돌아온다. 그러나 NT 계열은 ntvdm.exe가 돌아온다. 16비트 코드는 아예 샌드박스 안에서 고립된 채 돌아가고, 얘와 연계하여 동작하는 32비트 DLL은 여전히 32비트 문맥이 완전히 보장된다.

3. Flat: 32비트 플랫폼에서 32비트 EXE가 16비트 DLL을 실행
이것은 Windows 9x에만 존재한다. 20여 년 전에 Windows 95가 나오고 제품의 32비트 에디션을 출시하긴 해야 하는데 방대한 16비트 기반 DLL 엔진 같은 걸 차마 다 32비트로 포팅할 시간이 없을 때, 차선책으로 쓰라고 도입된 솔루션이다.

Universal은 그냥 32비트로 빌드만 하면 혜택을 입을 수 있고 Generic도 해당 16비트 EXE에서 LoadLibrary32Ex32W나 CallProc32W 같은 썽킹 API만 새로 사용하면 썽킹이 가능한 반면, Flat 썽킹을 활용하려면 thunk 컴파일러를 이용해서 상호 교신하고자 하는 32/16비트 바이너리들에 썽크 중재용 DLL을 추가로 넣어 줘야 한다.

1~3은 성격과 용도가 제각각 다르다는 걸 알 수 있다.
과거에 아래아한글이 최초의 Windows 버전인 3.0이 개발되었을 때, 시행착오로 인해 universal 썽크만 생각했지 flat 썽크를 고려하지는 못한 듯하다. 그래서 내가 듣기로는 Windows 3.1 + win32s에서는 실행되었는데 정작, 곧 출시된 Windows 95에서는 어처구니없게도 동작하지 않았다고 한다. (NT는 모르겠음)

아래아한글이 100% 완벽하게 32비트로만 개발됐으면 이런 일이 없었을 텐데 아마 내부적으로 호환성 때문에 16비트 코드가 일부 존재했던 모양이다. 그리고 내가 들은 이런 소문들이 사실이라면, Universal 썽크는 Win32s + 32비트 EXE에서 도로 16비트 DLL을 로딩하는 것도 지원하긴 했나 보다. Windows 95에서 추가로 해야 하는 썽킹이 없이도 뭔가 되는 일이 있었으니까 말이다.
어쨌든 이 때문에 Windows 95가 발매된 1995년 말엔 그 문제를 해결한 3.0b가 신속하게 나와야 했다. 소문에 따르면 3.0a도 있었는데 이건 3.0 원판보다도 존재감이 "더" 없는 것 같다.

이런 썽킹은 커널 레벨에서 발생하기 때문에 뭔가 정보를 잘못 줘서 에러가 나더라도 애플리케이션 레벨에서는 디버깅조차 할 수 없다. 더 깊게 들어가지 않은 채로 그냥 에러가 난다는 뜻이다. 이런 건 커널 레벨 디버거를 써서 살펴봐야 한댄다.
비록 제대로 안 돌아가는 제품을 돈 주고 사서 쓰다 고생한 사용자들에게서 욕도 많이 먹었겠지만, 그래도 그 열악한 환경에서 그 방대한 도스용 프로그램을 그 제한된 시간과 예산 하에서 Windows용으로 뚝딱 포팅을 한 건 대단한 일이긴 해 보인다.

Windows 9x 시절에 썽킹과 관련된 대표적인 테크닉 중 하나는 32비트 프로그램이 지금 시스템에 남은 리소스 퍼센티지를 얻어 오는 것이었다. 그 값을 얻으려면 32비트 프로그램이 32비트 user32.dll이 아니라 16비트 user.exe의 함수를 호출해야 했기 때문에 일종의 flat 썽킹이 필요했다.

물론 운영체제가 제공하는 rsrc32.dll이 제공하는 함수를 호출하는 방법도 있지만(얘는 rsrc16.dll로 내려가고), 걔네들이 하는 일을 별도의 dll 없이 직통으로 수행하는 꼼수가 있는데.. 이건 분량이 길어지는 관계로 나중에 다시 다루도록 하겠다.
자, 그럼 썽킹과 관련된 다른 얘기도 슬슬 좀 꺼내 보겠다.

본인은 에디트, 리스트 같은 기성 컨트롤들에 보이는 명령 메시지들의 값이 16비트 시절과 32비트 이후 시절이 서로 다르다는 것을 비교적 최근에 알고서 놀랐다.
과거에는 LB_ADDSTRING, EM_SETSEL 같은 메시지들이 WM_USER 이후 영역에 있었다. 그러나 32비트에서는 이것들이 WM_USER 이내의 시스템 메시지 영역으로 옮겨졌다. 아마 대화상자 내부에서 다른 사용자 메시지들과의 충돌을 막으려는 의도도 있고, 또 얘는 WM_GET/SETTEXT처럼 프로세스 간에 문자열을 주고받을 때 운영체제가 자동으로 메모리 보정을 해 준다는 의미에서 시스템 메시지로 승격된 게 아닌가 싶다.

대화상자에서 어떤 컨트롤이 WM_GETDLGCODE 메시지에 대해 DLGC_HASSETSEL를 되돌리면 걔는 포커스를 받았을 때 텍스트 전체를 선택하라는 EM_SETSEL 메시지를 받는다. 자신이 에디트 컨트롤을 자체 구현하고 있다면 이 메시지를 저렇게 처리하면 된다.
그런데 Windows 9x에서는 저 메시지가 안 오고 WM_USER+1에 속하는 정체불명의 괴메시지가 오곤 했다. 알고 보니 저건 16비트 시절의 EM_SETSEL과 같은 값이었다.

이런 레거시들 사연들은 모를 때보다 알 때가 프로그래밍에 훨씬 더 도움이 된다. 물론 그렇다고 해도 32비트 프로그램에다가도 Windows 9x는 왜 16비트 기준의 메시지를 보내는지는 알 수 없는 일이다. Windows의 썽킹 계층은 메모리 주소/포인터 변환뿐만 아니라 GUI에서는 이런 메시지의 변환까지도 도맡아 했다고 한다.

지금은 공용 컨트롤들의 메시지가 WM_USER 밖의 영역에 있다. 얘들은 애초부터 프로세스간의 메모리 보호가 잘 되는 32비트 운영체제와 함께 등장하기도 했고 또 알다시피 복잡한 플래그들이 들어간 복잡한 구조체를 주고받다 보니, 운영체제가 inter-process간에도 메모리 보정을 해 주지 않는다.
다른 프로세스에 있는 리스트/트리 컨트롤에다가 데이터를 등록하려면 얄짤없이 훅 프로시저를 써서 그 프로세스의 문맥 안에서 해당 컨트롤을 조작해야 한다. 사실, 남의 프로세스의 GUI 컨트롤을 조작하는 변태적인 작업이 왜 필요한지는 모르겠지만 말이다.

잘 알다시피 16비트 시절엔 DLL 전역변수들이 한데 공유됐으며, 제2, 제3 EXE들이 연결됐을 때 PROCESS_ATTACH 통지가 없었다고 한다. 그러니 DLL 함수는 exe들이 자기 DLL의 context를 매번 함수 인자로 전해 줘야 했겠다. 초기화/해제도 당연히 수동으로 직접 해야 했으나.. crash가 발생해서 해제를 제대로 못 하고 레퍼런스 카운트가 꼬이면 그건 그대로 시스템 자원 누수로 이어졌다.

요즘은 레퍼런스 카운트 관리가 엉망이더라도 프로세스가 종료되면 그 프로세스가 갖고 있던 자원은 100% 회수되는 게 보장된다. 그리고 요즘은 실행이 강제 종료 당한 스레드에서 스택 메모리가 자동 회수되지 않는 것 정도만이 그나마 in-process leak인 반면, 그 시절엔 툭하면 시스템 차원에서의 자원 누수와 고갈을 아주 쉽게 야기할 수 있었다. Windows 9x는 도스와 시스템 영역 메모리가 보호되지 않는 것 정도 때문에만 불안정했지만 3.1은 이보다 훨씬 더 막장이었다.

컴퓨터 환경을 더 좋게 만들기 위한 노력은 단순히 물리적으로 회로 집적도를 높이는 것만으로 되는 게 아니며 그 자원을 효율적으로 활용할 수 있게 소프트웨어적으로도 머리를 엄청 많이 써야 했다는 걸 알 수 있다.

Posted by 사무엘

2016/01/06 08:36 2016/01/06 08:36
, , , ,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1179

Windows에서 응용 프로그램의 창을 최대화하면 그 창은 화면에 말 그대로 꽉 차서 자신 주변의 테두리는 보이지 않게 된다. 이렇게 말이다.

사용자 삽입 이미지

그런데 머언 옛날, Windows 95에서는 일부 프로그램이 그렇게 일반적인 형태로 최대화가 되지 않았다. 테두리가 화면의 밖으로 밀려난 게 아니라 화면에 “포함된” 형태로 최대화되곤 했다. 그림판, 그리고 Internet Explorer 2가 대표적인 예였다. 너무 옛날 운영체제와 옛날 프로그램이어서 기억이나 실감을 못 하는 분도 계시겠지만 실제로 그랬다.

사용자 삽입 이미지

도대체 이 프로그램들은 왜 이렇게 동작했던 것일까? 그리고 어떻게 하면 이런 동작을 구현할 수 있을까?

Windows에는 창의 크기 내지 최대화 상태와 관련된 동작을 제어하는 WM_GETMINMAXINFO라는 요긴한 메시지가 있다. 이 메시지를 통해 이 창의 최소 및 최대 크기를 지정할 수 있으며, 창이 최대화되었을 때에 화면을 차지할 영역 범위도 지정할 수 있다.

크기 조절이 가능한 대화상자를 만들었는데 특정 한계 이하로는 가로나 세로 크기가 더 줄어들지 않게 하고 싶을 때, 그리고 가로로만 키울 수 있고 세로로는 더 키울 수 없게 하고 싶을 때 이 메시지를 처리하면 된다.

요즘은 좀 드물어졌지만 옛날에 Visual Basic 4라든가 델파이 같은 RAD 툴은 프로그램의 메인 윈도우 자체는 메뉴와 도구상자, 컴포넌트 팔레트 같은 것만 있었다. 폼 디자이너나 코딩 에디터는 메인 윈도우와 대등한 위상인 별도의 창으로 존재했다. 이 메인 윈도우는 가로로만 크기 조절이 되지 세로로는 되지 않았다.

사용자 삽입 이미지

그리고 과거의 ‘매체 재생기’ 역시 동영상은 별도의 창으로 출력됐지 메인 윈도우에는 메뉴와 위치 슬라이더, 재생 버튼 같은 것만 있었기 때문에 크기 조절은 가로로만 가능했다.
이런 프로그램들은 최대화가 가능하지 않거나, 최대화를 하더라도 가로로만 최대화가 됐다. 이런 동작을 위해서 WM_GETMINMAXINFO에다 크기 한계치를 지정해 주면 된다.

또한 이 메시지는 한계를 지정하는 것뿐만 아니라 한계를 초월하는 창 크기를 지정할 때도 쓰인다. ‘전체 화면’ 기능을 구현하는 게 대표적인 예이다.

위와 같은 활용을 하기 위해서는 메시지와 함께 전달된 MINMAXINFO 구조체에서 ptMaxSize, ptMinTrackSize, ptMaxTrackSize의 값을 고치면 된다. 다만 이들은 개념적으로 POINT(x, y)가 아니라 SIZE(cx, cy)에 해당하는 값인데 왜 구조체를 POINT로 지정했는지는 개인적으로 모르겠다. 시각과 시간을 헷갈린 것과 비슷한 격이다.

자, 그럼 크기 조절이 아니라 처음 주제인 최대화 이야기로 돌아온다. Windows 95의 그림판이나 IE2와 같은 동작을 하려면 ptMaxPosition이라는 멤버의 값을 고치면 된다. 창이 최대화됐을 때 이 창이 있을 곳을 지정하는 정보이므로 얘는 SIZE가 아닌 POINT의 정의에 부합하며, 디폴트로는 테두리를 가리기 위해서 화면을 살짝 벗어난 음수값이 설정되어 있다. 이것을 (0, 0)으로 설정하면 테두리가 화면에 보이게 된다.

MINMAXINFO *lpMMI = reinterpret_cast<MINMAXINFO *>(lParam);
lpMMI->ptMaxSize.x += lpMMI->ptMaxPosition.x*2;
lpMMI->ptMaxSize.y += lpMMI->ptMaxPosition.y*2;
lpMMI->ptMaxTrackSize=lpMMI->ptMaxSize;
lpMMI->ptMaxPosition.x=lpMMI->ptMaxPosition.y=0;

그런데 놀랍게도 여기에는 반전이 있다.
95 이후 오늘날의 Windows에서는 ptMaxPosition의 x, y 값을 모두 0으로 지정하면 값이 인식되지 않는다. 테두리가 밖으로 가려지는 디폴트 방식으로 창이 최대화된다. (0, 0)일 때만 의도적으로 보정을 한다는 것은 (0, 1), (1, 0) 이나 (-1, -1) 같은 비슷한 값을 줘 보면 금방 눈치챌 수 있다.

오로지 Windows 95만이 (0, 0)을 주면 창의 최상단 좌측 꼭지점이 말 그대로 (0, 0)으로 잡힌다. 이것은 본인이 프로그램을 직접 작성해서 돌려 보면서 확인한 사항이다.

이런 이유 때문인지, Windows 98(그 이후도 물론 포함)의 그림판은 95와 외형과 기능의 차이가 거의 없음에도 불구하고 최대화했을 때 95처럼 테두리가 보이는 형태로 커지지 않는다.
쉽게 말해 저건 오로지 95에서만 볼 수 있었던 추억의 특이한 동작인 것이다. NT4는 사정이 어떠했나 모르겠다.

사용자 삽입 이미지

WM_GETMINMAXINFO는 DefWindowProc의 영향을 받지 않는다는 점을 참고하도록 하자.
우리가 이 메시지를 받은 순간부터 lParam이 가리키는 MINMAXINFO 구조체에는 디폴트 값들이 이미 들어있으며, 우리는 필요한 경우 이 값들을 고치기만 하면 된다.
DefWindowProc가 구조체에다 디폴트 값을 넣어 준다거나 하는 건 없기 때문에 이 메시지는 그리로 전달을 하건 말건 동작이 달라지지 않는다.

훗날 Windows XP에서는 응답이 없이 죽어 버린 프로그램 창에 대해서 최소한의 반응성을 보장하기 위해 ghost 윈도우라는 걸 도입했다. 그런데 최대화된 상태에서 고스트가 됐다가 다시 살아난 윈도우는 최대화 이전 상태의 크기 정보가 사라져서 딱 저 95처럼 화면에 테두리가 보이는 최대 크기로 바뀌는 버그가 있었다. 물론 sp1 무렵에 곧바로 고쳐지긴 했다.

Posted by 사무엘

2015/12/28 19:37 2015/12/28 19:37
, ,
Response
No Trackback , 4 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1176

컴파일러의 경고 외

군대 유머, 관제탑 유머가 있는 것처럼 변호사를 소재로 한 블랙코미디 시리즈가 있다.
돈만 주면 자기 양심과 혼까지 팔아서 온갖 미사여구와 궤변(?)으로 범죄자의 형량을 감소시키고 심지어 무죄로 조작한다는.. 변호사에 대한 좀 과장되고 왜곡된 이미지가 들어가 있다.

천당과 지옥(혹은 천사와 악마)이 법정에서 소송이 붙으면 천당/천사 진영은 아마 승산이 없을 거라는 개드립조차 있다. 왜냐하면 유능한(=타락한-_-) 변호사들은 몽땅 지옥에 가 있어서 다 악마 편이기 때문에. -_-;; 물론 영적 법정에서 실제로 하나님이 어떤 편인지를 안다면, 그리고 성경에서 judgment라든가 judge라는 단어의 용례만 쭉 뽑아 보면 개드립은 그냥 개드립일 뿐이라는 걸 알 수 있다.

그런데 변호사가 굉장히 바보 같은 질문을 할 때가 있(었)는가 보다. 예를 들어..

  • 그림을 도둑맞던 당시에 선생님/고객님은 현장에 계셨습니까?
  • 그 일을 혼자 하셨나요? 아니면 단독 범행?
  • 충돌 당시에 두 차가 얼마나 떨어져 있었죠?
  • 그 스무 살 먹었다는 막내아들이 나이가 어떻게 된댔죠?
  • 전쟁에서 죽었다는 사람이 당신이었습니까, 아니면 당신 동생이었습니까?
  • 건망증을 앓고 계셨다면, 그럼 그 동안 잊어버린 것들의 예를 좀 들어 주시죠.

도대체 저 변호사 양반이 왕년에 그 무시무시한 사법 시험을 어떻게 통과했는지, 아니면 악착같은 공부 기계 괴수들이 몰리는 로스쿨을 어떻게 들어가서 졸업했고 어떻게 변호사 시험을 합격했는지를 의심케 하는 대목이 아닐 수 없다.

바보같은 질문은 그 변호사가 너무 격무에 시달린 나머지 (1) 정말로 뇌에 나사가 좀 풀려서 감을 잃었거나, (2) 정신 없어서 의뢰인을 완전 성의없게 대해서 나올 수 있다. 하지만 한편으로는 (3) 일부러 바보 같은 질문을 던져서 일종의 심문을 하려는 의도도 있다. 같은 내용을 비비 꼰 바보 같은 질문에 낚여서 진지하게 대답하다 보면, 일관성 없는 진술이 들통날 수 있기 때문이다.

뭐, 여기서 내가 법조인들의 심리나 심문 기법 같은 걸 얘기하려는 건 아니고.
중요한 건, 자연 언어뿐만 아니라 프로그램 코드도 사람이 작성하는 것이다 보니 저런 바보 같은 문장이 있을 수 있다는 것이다. 그리고 컴파일러가 그걸 지적해 주는 것을 우리는 '경고'라고 부른다.

간단한 예로는 선언만 해 놓고 사용하지 않은 변수, 초기화하지 않고 곧장 참조하는 변수, 한쪽에서는 class로 선언했는데 나중에 몸체를 정의할 때는 동일 명칭을 struct로 규정한 것이 있다. 딱히 에러까지는 아니고 코드 생성이 가능하지만, "혹시 다른 걸 의도한 게 아니었는지" 의심할 만한 부분이다.

더 똑똑한 컴파일러는 세미콜론이나 =/==사용이 아리까리해 보이는 것도 경고로 찍으며, 이런 것도 지적해 준다.
unsigned long p; (...) if(p<0) { }

unsigned 타입의 변수를 보고 "너 혹시 0보다 작니?"라고 묻는 건 그야말로 변호사가 "당신과 당신 동생 중 전쟁에서 죽은 사람이 누구라고 했죠?"라고 묻는 것이나 다름없다. 그러니 저 if 안에 있는 코드는 unreachable이라고 지적해 주는 건 적절한 조치이다.

사실, 사람이라 해도 처음부터 대놓고 저렇게 바보 같은 문장을 작성하는 경우는 드물다. 작성한 지 오래 된 코드를 나중에 리팩터링이나 다른 수정을 하게 됐는데, 같이 고쳐져야 하는 문장이 일부만 고쳐져서 일관성이 깨지는 경우가 더 많다. 남이 int를 기준으로 작성해 놓은 코드를 나중에 후임이 UINT로 고치면서 저 if문의 존재를 잊어버린다거나(알고 보니 이 값에 음수가 들어오거나 쓰일 일은 절대 없더라). 버그도 이런 식으로 생기곤 한다.

비주얼 C++에서 경고는 총 4단계가 있다. 1단계는 정말로 말이 안 되어 보이는 것만 출력하고, 4단계까지 가면 정말 미주알고주알 별걸 다 의심스럽다고 지적한다. 경고들을 그렇게 여러 단계로 분류한 기준은 딱히 표준이 있지는 않고 그냥 컴파일러 제조사의 임의 재량인 것으로 보인다.

비주얼 C++이 프로젝트를 만들 때 지정하는 디폴트는 3단계이다. 3단계를 기준으로 깔끔하게 컴파일되게 작성하던 코드를 4단계로 바꿔서 빌드해 보면 이름 없는 구조체를 포함해서 사용되지 않은 '함수 인자'들까지 온통 경고로 뜨기 때문에 output란이 꽤 지저분해진다. 물론, 특정 경고를 그냥 꺼 버리는 #pragma warning 지시문도 있지만, 그 자체가 소스 코드를 지저분하게 만드는 일이기도 하고.

그러니 어지간하면 3단계만으로 충분하지만, 4단계 경고 중에도 컴파일러가 잡아 주면 도움이 되겠다 싶은 일관성 미스 같은 것들이 있다.
그래서 모든 사람들이 코드의 모든 구조를 알지 못하는 공동 작업을 하는 경우.. (직감보다 시스템이 차지하는 비중이 더 커짐) 그리고 팀원/팀장 중에 좀 결벽증 강박관념이 있는 사람이 있는 경우, 4단계를 기준으로 프로젝트가 진행되며, 커밋하는 코드는 반드시 경고와 에러가 하나도 없어야 한다고 못을 박곤 한다. 심지어 경고도 에러와 동등하게 간주시켜서 빌드를 더 진행되지 않게 하는 컴파일 옵션을 사용하기도 한다.

변호사 유머를 보니까 컴파일러의 경고가 생각이 나서 글을 썼다.
내 생각엔 a=a++처럼 이식성 문제가 있고 컴파일러 구현체마다 다른 결과가 나올 수 있는 코드에 대해서나 경고가 좀 나와 줬으면 좋겠다. 저것도 초기화되지 않은 변수만큼이나 문제가 될 수 있기 때문이다. 비주얼 C++의 경고 level 4 옵션으로도 저건 그냥 넘어가는 듯하다.

예전에도 얘기한 적이 있듯, 법은 사람을 제어하는 일종의 선언/논리형 프로그래밍 언어로서 컴퓨터 사고방식으로 생각할 수도 있는 물건이다. 또한, 프로그램의 버전을 얼마나 올릴지 결정하는 게 형벌의 양형 기준과 비슷한 구석이 있다.
잡다한 기능들을 많이 추가한 것, 짧고 굵직한 기능을 구현한 것, 비록 작업량은 별로 많지 않지만 현실에서의 상징성과 의미가 굉장히 큰 것, 아니면 그냥 적당히 시간이 많이 흘렀기 때문에 숫자를 팍 올리는 것.

형벌이라는 것도 사람을 n명 죽인 것에 비례해서 징역이 올라가는 그런 관계는 당연히 아닐 테니, 상당히 많은 변수들이 감안된다.
이런 것들을 다 감안해서 다음 버전의 숫자를 결정하는데 이거 굉장히 복잡하다.

Posted by 사무엘

2015/12/10 08:33 2015/12/10 08:33
, , ,
Response
No Trackback , 3 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1169

Windows의 공용 컨트롤 열전.
날짜/시간 컨트롤에 이어 오늘은 툴바(toolbar)라고 불리는 '도구상자/도구모음줄' 컨트롤에 대해서 좀 얘기를 해 보겠다.

메모장 같은 급의 초간단 프로그램이 아닌 이상, 표준 GUI 기반인 대다수의 프로그램들은 상단에 클릭 가능한 그림 버튼들이 가로로 쭉 늘어서 있다. 도구모음줄은 바로 그 그림 버튼들의 표시를 책임진다. 각 그림 내지 아이콘은 그 프로그램에서 자주 쓰이는 명령들을 나타내며, 이를 클릭하면 일일이 메뉴를 열어서 글자 형태로 된 명령문을 읽고 선택하는 것보다 명령을 더 빠르게 내릴 수 있다.
자주 쓰이는 명령에 대해서 키보드에 단축키가 있다면 마우스에는 도구모음줄이 있는 셈이다.

사용자 삽입 이미지

사실, 도구모음줄은 컴퓨터의 성능 관점에서는 그렇게 효율적인 도구가 아닐지도 모른다. 요즘이야 제약이 덜해지긴 했지만, 과거에 화면 해상도가 충분하지 못하던 시절에는 텍스트나 그림 같은 문서 컨텐츠를 한 줄 표시할 공간이 아까운데 도구모음줄을 늘어놓는 건 화면 낭비였다. 수십 종류에 달하는 명령 아이콘들도 다 메모리를 수십 KB 이상씩 잡아먹는다는 건 역시 두 말할 나위가 없고.

하지만 어떤 프로그램을 실행했더니 그냥 빈 화면에 cursor만 달랑 깜빡이는 것보다는, 아무래도 컬러풀한 아이콘들이 가득한 도구모음줄 하나라도 좀 놓여 있는 게 사용자에게 친근하다. 이것이 마우스 사용자에게는 뭔가 클릭할 거리를 제공함으로써 프로그램을 더 편리하게 사용하는 데 실질적인 도움이 된다.

1990년대 초, Windows 3.x 시절에도 MS Word나 Excel의 까마득한 옛날 버전을 보면 파일, 편집, 보기 같은 자주 쓰인 명령이 등재된 도구모음줄이 있었다. 도구모음줄이라는 개념은 그때 처음으로 등장한 것으로 보인다. 그 시절에는 도구모음줄은 자체 구현이었고 MFC조차도 그걸 자체 구현해 줬었는데, Windows 95로 넘어오면서 운영체제의 공용 컨트롤로 형태가 바뀌었다. MFC의 ToolBar 클래스도 4.0 32비트 버전부터는 운영체제가 제공하는 놈을 쓰는 걸로 형태가 바뀌었다.

그리고 1990년대 말, MS Office 97부터는 버튼의 모양이 마우스로 가리키고 있는 놈만 얇은 입체 테두리가 나타나는 flat 스타일로 바뀌었으며, 메뉴도 도구모음줄 버튼이 있는 명령은 왼쪽에 그 도구모음줄 아이콘이 같이 뜨게 되었다. 이건 당시로서는 나름 굉장히 참신한 디자인이었다.
Office야 운영체제의 표준 GUI를 안 쓰는 걸로 악명(?)이 높았으니, flat 스타일은 Windows 98 타이밍 때 공용 컨트롤에도 도입되었다.

사용자 삽입 이미지

운영체제 차원에서 공용 컨트롤이 등장했으니 Visual C++과 MFC가 독자적으로 하는 일은 이제 없어졌느냐 하면 여전히 그렇지 않다. MFC가 하는 일은 다음과 같다.
(1) 먼저, 도구모음줄의 컨테이너격인 Control bar를 제공한다. 도구모음줄의 폭을 자유롭게 지정하고 위치도 자유롭게 옮기고 심지어 부모 윈도우의 상하좌우 등 어디든 자유롭게 붙이거나 떼는 것은 운영체제가 알아서 해 주는 일이 아니다. MFC의 도움 없이 직접 구현하는 건 머리에 쥐가 나는 노가다이다. 심지어 드래그하기 편하게 왼쪽에 그려 주는 gripper 세로줄 공간도 MFC가 그려 준 결과물이다.

개발하는 프로그램이 덩치가 MS 오피스 내지 포토샵 같은 상업용 프로그램 급으로 커지면 도구모음줄이 2개 이상 존재하게 된다. 보기 메뉴의 '도구모음줄' 항목은 체크 하나만 달랑 있는 게 아니라 '표준, 서식, 그리기' 등 도구모음줄의 종류를 가리키는 부메뉴를 갖게 된다.
도구모음줄이 하나밖에 없을 때는 겨우 그것만 이리저리 옮기고 붙였다 떼는 기능이 좀 잉여스럽게 느껴지겠지만, 그게 여러 개가 존재하게 되면 이들의 위치를 관리하는 기능은 필수가 된다. MFC는 그런 필수 기능을 구현해 준다.

도구모음줄이 한두 개도 아니고 무려 10~20개씩 달려 있는 방대한 프로그램은 도구모음줄의 버튼들을 사용자 정의(customize)하는 기능도 전문적으로 갖추고 있다. 공용 컨트롤이 기본으로 제공하는 customize 기능도 있지만, 그건 전체 아이콘들 집합에서 자기 도구모음줄에다가 추가할 버튼을 선택하고 순서를 바꾸는 것 정도가 전부이다. 그 반면 MS Office의 경우, 2007 이전 버전은 메뉴의 텍스트, 도구모음줄의 버튼 그림까지 전부 사용자가 바꿀 수 있어서 가히 개발자의 근성을 짐작케 하는 엄청난 customize 기능을 제공했다. 나중에는 MS Office는 리본 UI 기반으로 바뀌고 Visual Studio도 WPF 기반으로 UI가 싹 바뀌면서 이런 기능은 더 찾아보기 어렵게 됐다.

이런 컨테이너 기능 말고도 또 Visual C++가 MFC와 연계하여 제공하는 기능은 바로 (2) IDE가 제공하는 리소스 편집기이다.
MFC로 응용 프로그램을 만든다면 우리는 리소스 편집기를 이용해서 리소스에다가 Toolbar를 추가하고 도구 버튼과 아이콘, 연계 명령들을 넣곤 한다. 그런데 이 Toolbar라는 리소스 카테고리는 Windows가 제공하는 표준 리소스 포맷이 아니다. 비트맵, 아이콘, 메뉴, 문자열과는 달리 표준 포맷이 아니며, MFC가 자체적으로 정의해서 사용하는 포맷이다. 여기에 지정된 데이터를 바탕으로 도구모음줄을 초기화하는 것은 응당 MFC의 몫이다. LoadIcon, LoadMenu, LoadString 따위와는 달리, LoadToolBar는 MFC 클래스의 멤버 함수로나 존재하지 Windows API에는 없다.

게다가 이 toolbar 리소스는 단독으로 있는 것도 아니다. 얘가 정의하는 것은 한 도구모음줄에 몇 개의 버튼이 있고 각 버튼이 의미하는 명령 ID는 무엇인지, 혹은 이것이 구분자인지 같은 정보가 전부이다. 그 도구모음줄이 참조하는 비트맵은 같은 ID의 Bitmap 리소스에 있다.
하지만 Visual C++ IDE는 도구모음줄과 연계하는 비트맵은 비록 비트맵이라 할지라도 표준 비트맵 리소스에서 따로 표시를 하지 않으며, 그 비트맵은 도구모음줄 리소스를 편집하는 곳에서 버튼 구조와 함께 편집하게 돼 있다. 프로그램이 내부적으로 이런 보정 처리까지 하고 있는 것이다.

내부적으로 MFC는 한 프로그램 윈도우에 대해서 한 리소스 ID를 부여하여 이걸로 문자열(프로그램 제목), 아이콘, 액셀러레이터 단축키, 표준 도구모음줄(비트맵 포함)까지 한데 관리를 하기까지 한다. 이것이 바로 CFrameWnd::LoadFrame 함수가 하는 일이다. 참 대단한 발상이다.

다음으로, 도구모음줄에 대해서 프로그래머가 반드시 짚고 넘어가야 할 기술적인 사항이 하나 있다.
도구모음줄의 버튼 그림은 작고 아담한 게 아이콘을 닮았지만, 실제로 이건 아이콘이나 마우스 포인터와는 달리 그냥 비트맵이다.
'아이콘'은 이미지 비트맵과 마스크 비트맵으로 구성되어서 태생적으로 래스터 오퍼레이션을 통해 투명 배경이나 반전 같은 걸 표현할 수 있다. 그러나 이미지 비트맵 한 장만 갖고는 그런 걸 표현할 수 없다. 그렇다면 도구모음줄 버튼은 어떤 방식으로 투명색을 표현하는 걸까?

이 방식은 생각보다 굉장히 원시적이고 단순무식하다. TB_ADDBITMAP 메시지라는 재래식 방식을 쓰는 경우, 도구모음줄은 이미지 비트맵 한 장만 달랑 받아들이고 투명 처리 같은 걸 해 주지 않는다. 비트맵을 생으로 있는 그대로 출력만 한다.
그렇기 때문에 MFC의 도구모음줄 클래스는 자신의 리소스 비트맵 이미지에 대해서 보정을 한다. 비트맵에 RGB(192,192,192)에 속하는 은색/회색 픽셀이 있으면 그걸 현재 운영체제의 COLOR_BTNFACE 시스템 컬러로 바꾸고, 은색 말고도 짙은 회색이나 검정, 하양은 그에 상응하는 시스템 컬러로 바꾼다. 그렇게 보정된 비트맵을 도구모음줄로 보내서 은색이 편의상 투명 배경색인 것처럼 보이게 한다. 보정을 안 하면 바로 이런 꼴 난다..;;

사용자 삽입 이미지

시스템 색상이 바뀌어서 WM_SYSCOLORCHANGE 메시지가 오면? 당연히 도구모음줄 비트맵도 매번 다시 만들어서 지정한다.
MFC를 뒤져 보면 이 일을 하는 AfxLoadSysColorBitmap라는 함수가 bartool.cpp에 있다. 아니, 이렇게 색깔 치환을 한 비트맵을 생성하는 함수가 Windows 95 시절부터 comctl32.dll에 CreateMappedBitmap이라고 있어 왔다. 도구모음줄 전용이기 때문에 user32도, gdi32도 아닌 comctl32에 있는 것이다.

그리고 이런 색깔 보정이 마냥 삽질인 것만은 아닌 것이..
시스템 색상이 고대비 검정 같은 걸로 바뀌었을 때는 검은색을 흰색으로 바꾸는 작업도 어차피 필요하기 때문이다. 도구모음줄의 비트맵은 반쯤은 이런 유동적인 환경 변화에도 대비가 돼 있어야 한다는 점이 흥미롭다.

도구모음줄용 비트맵으로 원시적인 생 비트맵뿐만 아니라 image list를 지정하는 TB_SETIMAGELIST 메시지는 Internet Explorer 4는 아니고 3과 함께 약간 나중에 추가됐다.
image list는 자체적으로 마스크 정보도 포함할 수 있으니 예전보다는 상황이 좀 낫다. 또한 ImageList_LoadImage 함수는 은색이 아닌 임의의 색깔을 투명색으로 지정할 수 있고, 아예 default로 (0,0) 화면 최상단 좌측 픽셀을 투명색으로 지정하게 할 수도 있다.
평소에는 흑백 이미지이다가 마우스 포인터가 가리키고 있는 버튼만 컬러 이미지로 출력하는 일명 hot image를 지정하는 것은 이렇게 image list 형태로만 지정 가능하다.

이렇게 특정 한 색깔을 투명색으로 끌어다 쓰는 건 아무래도 16색/256색 시절의 그래픽 패러다임을 벗어나지 못한 발상이다. MFC feature pack을 이용해서 트루컬러 아이콘이 들어간 도구모음줄을 만들면 당장은 보기 좋지만 시스템 색상을 고대비 검정으로 바꿔 보면 경계가 완전 뭉개지고 보기 좋지 않은 걸 알 수 있다. 어느 배경색에도 경계가 부드럽게 나오려면 근본적으로 알파채널이 쓰여야 할 텐데 그렇지 못하기 때문이다.

요컨대 오늘날 도구모음줄은 아이콘들이 그런 것처럼 트루컬러와 알파 채널에 대비가 돼 있어야 하고, 그러면서 고대비 모드도 지원해야 하며, 더 욕심을 부리자면 고해상도 DPI에서는 약간 큰 비트맵도 준비돼 있어야 한다. MS Office의 리본 UI는 고대비 모드에서는 그냥 16컬러로 간략화한 비트맵을 대신 출력하는 것 같다.
그리고 <날개셋> 편집기는 겨우 그 덩치의 프로그램에서 저런 것까지 일일이 대비하는 건 너무 낭비라 여겨지기 때문에 도구모음줄의 아이콘은 그냥 16*16 크기의 16색에 머물러 있다.. ^^

그나저나,

  • 공용 컨트롤을 쓰지 않고 자체 구현된 도구모음줄은 대화상자를 띄우는 명령을 클릭한 경우, 대화상자가 떠 있는 동안에 버튼이 여전히 눌러져 있기도 했던 것 같다. MS Office 2007 이전의 옛 버전과 Visual C++ 6 등이 그 예다.
  • 전무후무하게 도구모음줄 배경에 solid color가 아니라 무늬가 있었던 IE 3의 도구모음줄은 어떻게 구현되었는지가 새삼 궁금해진다. 얘는 표준 공용 컨트롤 기반인데, 아마 얘 때문에 image list 기능이 도입되었지 싶다. 이쯤 되면 버튼 비트맵에 자체적으로 마스크 정보가 있어야만 도구모음줄 배경과도 합성이 가능하지 않았겠는가.

사용자 삽입 이미지

사용자 삽입 이미지

Posted by 사무엘

2015/11/16 08:36 2015/11/16 08:36
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1160

« Previous : 1 : ... 9 : 10 : 11 : 12 : 13 : 14 : 15 : 16 : 17 : ... 31 : Next »

블로그 이미지

그런즉 이제 애호박, 단호박, 늙은호박 이 셋은 항상 있으나, 그 중에 제일은 늙은호박이니라.

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31        

Site Stats

Total hits:
3047987
Today:
1149
Yesterday:
2058