cursor/caret 이야기

기왕 말이 나온 김에 또 문자 입력과 관련된 사용자 인터페이스 얘기를 좀 계속해 보자.

글자 입력란에서 깜빡거리고 있는 길쭉한 세로줄(|)을 우리는 보통 '커서'라고 부르고 영어로 cursor이라고 한다.
그러나 이 명칭은 우리말로는 용언 '크다'의 활용형으로 보여서 보기가 안 좋고, 영어로는 마우스 포인터조차 cursor이라고 부르는 경우가 있다. 실제로 Windows API에서 사용되는 LoadCursor, ShowCursor 같은 단어는 전부 마우스 포인터를 가리키며, 내부 공식 용어는 캐럿(caret)이다. 함수명도 ShowCaret, HideCaret따위다.
용어가 대단히 혼란스럽다는 점을 인정하지 않을 수 없다. 이 글에서는 편의상 그냥 cursor이라고 적도록 하겠다.

텍스트 모드가 존재하던 도스 시절에는 이 cursor가 반각 폭을 차지하는 밑줄 모양(_)이었다. 그리고 일부 환경에서는 겹침 모드일 때는 cursor가 약간 두툼해지곤 했다. 그 일부 환경 중의 하나로는 GWBASIC의 대화식 환경도 포함돼 있다.
그때도 cursor를 보이게 하거나 감추는 도스 API가 있었다. 그리고 심지어 cursor의 굵기를 바꾸는 기능도 있어서 옛날 노턴 유틸리티의 구성원 중 하나이던 NCC(Norton Control Center)라는 프로그램을 통해 변경을 할 수 있었다.

그러고 보니 정말 옛날이구나.
NCC의 스크린샷이 궁금했는데 구글을 뒤져도 관련 그림 하나 찾을 수 없을 정도로 완전히 역사 속으로 묻혀 버렸다.
얘는 cursor 모양을 포함해서 키보드와 마우스의 속도를 바꾸는 액세서리 기능도 있었다.
지금은 Windows의 제어판을 통해서 키보드의 속도를 바꾸지만 도스 시절에는 외부 명령인 MODE CON을 이용하거나 별도의 그런 유틸리티를 썼다. 그런 프로그램들 역시 내부적으로는 도스 API를 호출하는 형태였겠지만 말이다.

GUI 기반인 Windows에도 cursor는 응당 존재한다.
기술적으로 볼 때 cursor는 특정 시간 간격으로 운영체제가 DC에다 비트맵을 XOR이라는 래스터 연산을 적용하여 그려 주는 동작일 뿐이다. WM_CREATE 때 cursor를 생성하고, WM_DESTROY 때 파괴한다. 그리고 WM_SETFOCUS 때 화면에 표시를 해 주고 WM_KILLFOCUS 때 감춰 주면 된다.

숨김/감춤 요청이 교대가 아니라 중첩해서 발생할 경우, 운영체제는 일종의 reference counting을 해 준다.
하지만 개인적으로는 창이 WM_SETFOCUS와 WM_KILLFOCUS에 대한 처리를 운영체제가 자동으로 하게 API를 설계하는 게 더 나았을 거라는 생각을 한다. 언제나 단 한 프로그램에서만 cursor가 깜빡이는 게 보장되도록 말이다.

cursor만을 위해 문서화되지 않은 전용 타이머 메시지가 쓰인다는 것은 Spy++ 같은 프로그램을 통해 이미 아는 개발자들이 많을 것이다. 0x118인데, 편의상 흔히 WM_SYSTIMER라고 이름을 붙이는 듯하다.

한글을 조합 중일 때 cursor가 조합 중인 한글 전체를 감싸며 깜빡이는 것은, 도스 시절의 자체한글 프로그램들로부터 전해지던 전통이다. 나름 우리나라에서만 볼 수 있는 관행인데 MS가 로컬라이즈 차원에서 이런 시각 피드백을 Windows에다가도 적극 도입했다. MS 워드 6.0이 이를 첫 지원했으며 Windows 95때부터 운영체제의 에디트 컨트롤까지 지원이 확대되었다.
그냥 일본어/중국어를 입력할 때처럼 조합 중인 한글을 대충 밑줄로 표시하는 걸로 때워도 됐을 텐데 이를 특별히 배려한 것이다. Windows 3.x 내지 맥 OS에서는 깜빡이는 네모 cursor를 볼 수 없다.

과거에 훈민정음 워드 프로세서는 한글 모드일 때 cursor가 흰색 배경에 대해서 붉은색으로 변하곤 했다.
그리고 MS 오피스 2010은 수평이 아닌 임의의 각도로 기울어진 텍스트 상자에 글을 입력할 때... 입력란도 실제로 그 기울어진 각도를 반영하여 동작하는 직관적인 UI를 구현해 냈다. 이때 cursor도 당연히 기울어진 상태로 나타난다. 이런 cursor는 어떻게 만들었을까?

사용자 삽입 이미지

이것들도 다 CreateCaret 함수에서 크기만 달랑 지정하는 게 아니라 비트맵을 지정해 줌으로써 구현 가능하다.
비트맵 인자로 NULL을 넣으면 해당 영역의 모든 비트맵의 비트가 1이어서 전체 영역이 반전된다. 즉 InvertRect 함수를 호출하는 것과 동일한 효과가 난다.
그리고 MSDN에서 볼 수 있듯이, (HBITMAP)1을 집어넣어 주면 비트 0과 비트 1이 번갈아가며 나타나는 그 50% gray가 지정된다.

이와 같은 맥락으로, 옥색으로 채워진 비트맵을 지정하면 흰색 바탕에서는 보색인 빨강 cursor가 나타나며
검정 배경에 기울어진 흰색 사각형 모양의 비트맵을 지정하면 기울어진 cursor도 만들 수 있다.
요컨대 운영체제의 caret은 무조건 직교좌표계를 따르는 직사각형만 가능한 게 아니다.
임의의 비트맵과 XOR 연산을 하는 형태로 구현되기 때문에 임의의 모양의 cursor를 만들 수 있다.

다만, 별도의 마스크 비트맵이 존재하지 않기 때문에 마우스 포인터나 아이콘 같은 완전히 opaque한 스프라이트를 깜빡이게 할 수는 없으며, 알파채널 같은 걸 줄 수 없을 뿐이다. 그런 효과를 만들려면 프로그래머가 직접 cursor의 깜빡임을 타이머를 써서 구현해야 할 것이다.

Posted by 사무엘

2014/01/29 19:44 2014/01/29 19:44
, ,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/925

지금 여러분의 PC에는 비트맵 그래픽 에디터로 무엇이 설치되어 있는가?
포토샵, 페인트샵, 페인트 닷넷, 심지어 그림판, 비주얼 스튜디오가 자체 제공하는 그래픽 에디터 등등...
이런 것들을 떠올리면서 각 프로그램들이 텍스트를 삽입하는 텍스트 도구가 어떤 형태로 구현되어 있는지 생각해 보시기 바란다.

보통은 텍스트 도구를 실행하면 텍스트를 삽입할 지점 내지 영역을 지정할 수 있고, 그 뒤에는 어지간해서는 텍스트 입력란이 별도의 대화상자나 창을 통해서 뜨게 된다.
그런데 Windows가 기본 제공하는 그림판은 다소 참신하게 설계되어 있다.

사용자 삽입 이미지

텍스트를 입력받는 영역이 그림 내부에 일체형으로 생긴다. 그림 내부에 cursor가 생겨서 텍스트를 곧바로 입력할 수 있으며 심지어 블록까지 잡을 수도 있는데,
일괄적인 단색이 아니라 기존 그림이 그대로 배경에 깔린다. 즉, 텍스트를 입력했다가 지우면 원래 그림이 도로 보존된다는 뜻이다. 별로 깜빡거리는 현상도 없다. 게다가 윈도 95 시절부터 그림판은 이렇게 동작해 오고 있었다. 참고로 페인트 닷넷이나 과거 윈도 3.1 시절의 페인트는 텍스트가 그림에 바로 삽입은 되지만, 블록까지 잡을 수는 없다.

이건 보통일이 아님을 알 수 있다. 아무래도 운영체제의 일반 에디트 컨트롤로는 불가능한 일인 것 같다.
WS_EX_TRANSPARENT라는 스타일이 있고 자체적으로 배경을 지우지 않게 WM_ERASEBKGND 메시지를 서브클래싱한다 해도, 일단 글자에 가려졌던 배경을 깜빡거림 없이 지능적으로 복구하는 일은 해당 컨트롤이 알아서 해 줘야 하기 때문이다.

Spy++로 살펴보면, 텍스트를 입력받는 동안엔 그림 클라이언트 영역 내부에 리치 에디트 컨트롤이 하나 생기긴 한다.
잘은 모르겠지만 얘는 일반 에디트 컨트롤보다 기능이 더 많고 전문적인 만큼, 옵션을 줌으로써 투명하게 동작하는 것에 대비되어 있는 게 아닌가 싶다.

과거의 그림판은 글꼴이나 크기, 색깔을 바꾸면 그게 모든 텍스트에 일괄 적용되었다. 하지만 7의 그림판은 리치 에디트답게 속성을 글자별로 다 따로 줄 수 있다. 뭐, 일괄 적용되던 시절에도 어차피 리치 에디트 기반인 건 동일했으니 그건 단순히 개발 난이도를 낮추기 위해 사용되었던 정책인 것 같다. 그래픽 에디터는 전문적인 텍스트 에디터가 아니니까 말이다.

상황을 좀 더 일반화해서 생각해 보자.
사실, GUI 위젯의 구성요소로는 각종 버튼들, 리스트 박스, 콤보 박스 등 여러 물건들이 있는데,
그 중 기술적으로 가장 만들기 힘든 것은 단연 에디트 컨트롤이다.
키보드 입력을 가장 정교하게 처리해야 하고, 텍스트의 변경으로 인해 텍스트의 전체 레이아웃이 바뀌는 것을 그때 그때 처리해야 하며 그러면서 화면상으로 바뀐 부분만 다시 그리거나 스크롤해 줘야 한다.

에디트 컨트롤을 만들어야 하는데 이런 어렵고 복잡한 내부 처리는 다른 컴포넌트에다 맡기고, 주어진 레이아웃대로 화면에 글자를 출력하는 것만 사용자가 customize할 수는 없을까?

예를 들어서 게임을 만들 때 말이다. 채팅창이 있는데 글자는 그림자 같은 일반적인 리치 에디트 포맷에 없는 특수한 효과가 적용되고, 배경으로는 게임 배경 화면이 알파 채널로 겹쳐진다. 이런 그래픽 출력을 일반 윈도우 DC를 이용해서 할 수는 없는 노릇이다.

이런 경우, 별 수 없이 게임 내부의 GUI 라이브러리를 개발하는 사람이 야메로 에디트 컨트롤을 직접 구현한다.
직접 만들면 세세한 제어가 가능하니 속 편한 경우도 있지만, 외국 시장까지 생각했을 때 아무래도 높은 완성도를 기대하기 어렵다. 중국어· 일본어 IME의 시각 피드백을 출력한다거나 아랍어가 뒤섞인 텍스트를 제대로 처리하는 에디트 컨트롤을 혼자 다 만든다는 건 불가능하며 그럴 필요도 없다.

본인이 개발한 <날개셋> 타자연습에도 기술적으로 완전 구닥다리이긴 하지만 그래도 DirectDraw surface를 만들어서 동작하는 게임이 있다.
내 원래 계획은 입력 중인 글자는 게임 배경에 같이 오버랩되어 표시되는 것이었다.
하지만 그것까지 구현하는 게 어려워서 그냥 화면 하단에 날개셋 에디트 컨트롤을 때려박아 넣는 형태로 프로그램이 만들어졌다. 에디트 컨트롤의 내부 알고리즘이 내린 지시를 custom 출력 매체에다 효과적으로 임베딩하는 방법이 필요하다.

이런 생각을 마소의 개발자가 안 했을 리가 없다.
그래서 Windowless Rich Edit Control이라는 게 있다. 리치 에디트 컨트롤의 내부 알고리즘을 COM 객체 형태로 만든 것이다. 얘를 이용하면 굳이 독립된 윈도우를 만들지 않고도 리치 에디트 컨트롤처럼 동작하는 객체를 손쉽게 구현할 수 있다.

물론, 스펙을 보면 알겠지만 우리 쪽에서 처리해야 할 요청이 한두 개가 아닌 관계로, 마냥 쉽게만 사용할 수 있는 물건은 아닐 수도 있다.
우리는 ITextHost라는 인터페이스를 구현하여 “어디에다 cursor를 만들어라, 무슨 크기를 얻어 와라” 같은 요청를 처리하면 되고, 운영체제의 기본 서비스가 필요하면 ITextServices 인터페이스를 호출하면 된다. 이들 객체는 CreateTextServices 함수를 통해 주고받으면 된다.

본인은 그렇잖아도 문자 입력과 관련된 프로그래밍 경험이 많은 관계로, 운영체제에 이런 API가 있는 것은 진작부터 알고 있었고, 이걸 실전에서 활용도 몇 차례 해 봤다. 단, 정작 그림판은 이런 windowless 오브젝트를 사용한 게 아니라 내부에 특수 처리된 리치 에디트 컨트롤 윈도우를 직접 생성했다는 게 의외로 놀랍다.
text services라는 단어 자체는 이 때부터 있었던 셈이다. 그러니 IME를 대체하는 Windows의 차세대 문자 입력 프로토콜이 text services framework, 즉 TSF라고 명명된 것은 우연이 아니라 하겠다.

Posted by 사무엘

2014/01/26 19:31 2014/01/26 19:31
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/924

16비트 시절에는 잘 알다시피 int 포함 machine word의 크기가 말 그대로 2바이트였다. 그리고 근거리/원거리 포인터가 존재했으며 복잡한 메모리 모델을 따져야 했다. 여기까지는 도스든 윈도든 공통이다. 그럼, 그 시절에 Windows 프로그래밍은 어떠했을까?

16비트 Windows는 잘 알다시피 모든 프로그램이 단일 스레드를 공유하면서 단일 메모리 주소 공간에서 실행되었다. 이런 열악한 환경에서 멀티태스킹을 구현하는 것은 결코 쉬운 일이 아니었다.
그래서 그 시절에는 콜백 함수를 운영체제에다 지정해 주는 것조차도 보통일이 아니었다. 오늘은 오랜만에 이 부분에 대해서 예전에 몰랐다가 최근에 본인이 추가로 알게 된 이야기를 좀 하겠다.

Windows API는 C언어 함수 형태로 만들어졌으니 그때는 지금으로 치면 C++ 가상 함수가 할 일을 함수 포인터로 다 처리하고 있었다. 윈도우 프로시저, 대화상자 프로시저 같은 것 말이다.

그런데 같은 프로그램이 두 번 중첩 실행되면, 코드는 동일하더라도 그 코드가 다루는 스택(데이터) context는 결국 프로세스별로 서로 달라질 수 있게 된다. 이를 어떻게 구분해야 할까?
32비트 이후부터는 가상 메모리 기술이 워낙 발달하여 그 context가 CPU 차원에서 알아서 따로 잘 처리된다. 그러나 16비트 기계엔 그런 개념이 없었다.

결국은, 실제 콜백 함수가 호출되기 전에 그 콜백 함수가 처리할 자신의 데이터 세그먼트 영역을 CPU 레지스터에다 써 주는 stub, 일명 썽킹(thunking) 코드를 소프트웨어적으로 별도로 만들어야 했다. C++로 치면, 클래스 멤버 함수 호출을 위해 this 포인터의 값을 EAX 레지스터에다 써 주는 것과 개념적으로 비슷하다.

우리가 만든 콜백 함수는 그 썽킹 함수를 통해서 호출해야 한 것이다. 운영체제에다가도 당연히 썽킹 함수를 알려 주고 말이다.
이것이 바로 16비트 시절 코드를 보면 밥 먹듯이 보이는 MakeProcInstance와 FreeProcInstance API 함수의 정체이다.
별도의 메모리를 추가로 할당해서 기계어 코드를 추가해 준 것이기 때문에, 메모리의 해제까지 해 줘야 한다.

대화상자 하나를 출력하더라도 아래처럼 말이다.

FARPROC pfnThunkProc = MakeProcInstance( (FARPROC)My_Actual_Dialog_Proc, hInstance); //내 대화상자 프로시저가 우리 인스턴스 핸들을 context로 동작하게 한다
DialogBox(hInstance, "Dialog_Resource_Name", hWndParent, (DLGPROC)pfnThunkProc );
FreeProcInstance(pfnThunkProc);

요즘 같으면 아래의 절차 하나만으로도 충분했을 텐데 말이다.

DialogBox(hInstance, "Dialog_Resource_Name", hWndParent, My_Actual_Dialog_Proc );

결국 My_Actual_Dialog_Proc라는 코드는 시스템 전체를 통틀어서 단 하나 유일하게 존재하겠지만,
이 프로그램을 여러 번 실행하더라도 각 프로그램들은 그 함수로부터 파생된 자신만의 고유한 콜백 함수를 통해서 대화상자를 호출하게 된다.

단, Windows 3.1에서는 함수명을 export해 주는 것만으로 이렇게 콜백 함수에 대한 썽킹을 매번 해 줘야 하는 번거로움을 생략할 수 있었다고 한다.

지금이야 프로그래밍에서 export 키워드라 하면, C++에서 템플릿의 선언과 정의를 번역 단위를 넘나들며 공유할 수 있게 하려 했던 비현실적인 흑역사 키워드일 뿐이다. Windows 플랫폼으로 문맥을 넓힐 경우, export는 클래스나 함수 심벌을 빌드되는 파일 차원에서 외부로 노출하여 GetProcAddress 함수로 노출이 되게 하는 속성 지정자이다. 비주얼 C++ 기준으로 __declspec(dllexport) 라고 쓴다.

지금이야 이런 export 속성은 말 그대로 DLL을 만들 때에나 쓰인다.
그러나 16비트 시절에는 EXE도 운영체제로부터 호출을 받아야 하는 콜백 함수를 넘겨 줄 목적으로 심벌 export를 즐겨 활용했었다.
export된 함수는 외부로부터 호출받는 게 당연시될 거라 여겨졌으므로, 링커와 운영체제가 자체적으로 데이터 세그먼트 보정을 하는 전처리 루틴을 추가해 줬다. 따라서 코드가 훨씬 더 깔끔해진다.

이런 이유로 인해 Windows 3.x 실행 파일들을 헥스 에디터로 들여다보면 대체로 ...WNDPROC, ...DLGPROC 같은 식으로 끝나는 웬 이상한 함수 이름들이 내부에 들어있는 걸 확인할 수 있다. 32비트 이후부터는 찾을 수 없는 관행이다. 자기 내부에서만 쓰는 콜백 함수들을 왜 저렇게 노출해 놓겠는가?

어떤 함수에 export 속성을 부여하기 위해서는 결국 C/C++ 언어에다가 없는 문법을 새로 추가하거나, 아니면 소스 코드는 그대로 놔 두고 컴파일러가 아닌 링커만이 인식하는 부가 정보 같은 게 주어져야 할 것이다. 전자는 소스 코드의 이식성을 떨어뜨린다는 부담이 있지만, 그래도 뒤끝이 없고 한 소스에 빌드에 필요한 모든 정보가 한데 모이니 편리하다. 그래서 그 당시에는 __export라는 비표준 MS 확장 키워드가 도입되었고, 이를 보통 EXPORT라는 매크로로 감싸서 사용하곤 했다. 이런 식으로 말이다.

long FAR PASCAL EXPORT MyWindowProc(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam); //그 당시 콜백 함수는 반드시 '파스칼' 방식이라는 함수 호출 규약대로 선언되어야만 했다.

한편 후자는 바로 그 이름도 유명한 DEF(모듈 정의) 파일이다.
지금도 DEF 파일이 안 쓰이는 건 아니지만 정말 DLL을 만들 때가 고작이다. 그 반면 16비트 시절에는 DEF가 훨씬 더 중요하게 쓰였던 모양이다.
export하는 콜백 함수뿐만 아니라 이 프로그램에 대한 소개문, 그리고 필요한 스택/힙 크기까지 정확하게 명시되어야 했다.

32비트 이후부터는 스택/힙이 부족하면, 프로그램 로딩 시간이 좀 더 걸릴 뿐이지 실시간으로 추가 할당하여 working set의 확장이 가능한 반면, 16비트 시절에는 메모리 양도 부족하고 또 한 주소 공간에서 여러 프로그램들이 뽁짝뽁짝 복잡하게 지내다 보니 이런 게 핀트가 어긋나면 프로그램 실행이 아예 불가능했을 것 같다.

옛날의 New Executable 포맷에는 자체적으로 name table이라 불리는 범용적인 문자열 테이블이 있었고, import/export하는 심벌 이름은 물론 이 EXE/DLL에 대한 간단한 소개문까지 그런 데에 들어갈 수 있었다.
지금으로 치면 버전 리소스에 들어가는 description과 유사한데 역할 중복인 셈이다. 그런 것까지 DEF 파일에다 명시해 줬었다.

자.. 설명을 들으니 어떤 생각이 드시는지?
16비트 Windows용 실행 파일을 분석하는 도구는 그리 충분치 않다. 일단 개발자들의 친구인 Dependency Walker가 이를 전혀 지원하지 않으며, 그나마 괜찮은 물건으로는 옛날에 Windows 98에 내장되어 있던 QuickView라는 프로그램이 적격이었다. 기본적인 사항은 다 잘 출력해 줬다.

16비트 시절에는 잘 알다시피 HINSTANCE는 각 프로그램의 데이터 세그먼트를 식별하는 번호표에 가까웠고, 개념적으로 지금 같은 포인터가 아니라 오늘날로 치면 오히려 프로세스를 식별하는 HANDLE처럼 널리 쓰였다. (비록 Close를 할 일은 없었겠지만 말이다) 게다가 EXE는 HINSTANCE, DLL은 HMODULE로 성격도 달랐다. 그래서 LoadLibrary 함수의 리턴값은 분명 HMODULE이다.

그때는 EXE/DLL로부터 리소스를 얻어 오는 과정도 정말 복잡했고 Load/Free 절차뿐만 아니라 lock/unlock까지 있었다. 메모리의 절대적인 양이 충분치 않기도 하고 가상 메모리 같은 시스템이 없던 관계로, 단편화를 방지하기 위해 평소에 즉각 쓰지 않는 메모리 블록은 재배치가 가능하게 놔 두는 관행이 더 보편적이었기 때문이다.

한편으로 그때는 시스템 전체에 영향을 끼치기가 쉬웠다. 특히 DLL이 이 분야에 관한 한 지존이었다.
DLL에서 선언한 전역변수는 모든 프로세스가 한데 공유되었으며, DLL이 실행하는 코드는 저런 EXE처럼 여러 컨텍스트에서 중첩 실행될 일이 없고 애초에 데이터 세그먼트 구분을 할 필요가 없었다!

그리고 16비트 시절엔 애초에 콜백 함수를 지정해 주는 과정이 처음부터 저렇게 번거로웠던 만큼, 시스템 전체의 동작 방식을 바꾸는 global 훅을 설치하더라도 훅 프로시저를 반드시 DLL에다가만 만들어야 한다는 제약이 없었다.
그럼에도 불구하고 16비트 시절은 32비트 시절보다 편한 것보다는 불편한게 더 많았다는 것이 부인할 수 없는 사실이다.

끝으로 여담.
이렇듯, 본인은 초등학교 아주 어릴 적부터 프로그래밍을 시작한 관계로, 그 시절에 대한 향수가 있고 '레트로'-_- 컴퓨팅 쪽으로 관심이 많다.
그런데, 그런 것에서조차도 양덕후들의 기상은 갑이다. the old new things 같은 블로그는 이 바닥의 지존이라고 봐야 하고, Windows 3.x로도 모자라서 2.x나 1.x용 프로그램을 만들었다고 인증샷 올리는 사람도 있다.

1985년에 출시된 Windows 1.x의 경우, VGA 카드조차도 없던 시절에 만들어진 관계로 가장 좋은 비디오 모드가 640*350 해상도짜리 EGA이다. 그런데 그걸 640*480 VGA 내지 심지어 800*600 SVGA에서 동작하게 드라이버 패치를 만든 사람도 있다.

참고로 Windows 1.x 정도 되면.. 프로그램 개발을 위한 컴파일러는 그야말로 1986년에 나온 MS C 4.x를 써야 하고, 얘는 함수 호출 인자도 ANSI 스타일이 아니라 K&R 스타일만 써야 하는 진짜 무지막지한 골동품이라고 한다. Windows 1.x는 파스칼로 개발되었다고 들었는데 그래도 그때부터 C 형태의 SDK는 있었던 모양이다.

Posted by 사무엘

2013/11/28 08:29 2013/11/28 08:29
, ,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/903

자고로 프로그래밍 언어는 구문이나 예약어 같은 원론적인 것만 정의하는 게 아니라, 그 문법을 토대로 프로그램의 작성에 필요한 각종 기본적인 자료구조와 알고리즘, 입출력 기능들도 라이브러리 형태로 제공한다. 후자의 디자인도 프로그래밍 언어의 정체성에서 차지하는 비중이 매우 크다.

예를 들어 printf, qsort, malloc, fopen 같은 함수를 사용했다면 그 함수들의 몸체도 당연히 프로그램 어딘가에 빌드되어 들어가야 한다. 아니, 애초에 main 함수나 WinMain 함수가 호출되고 각종 인자를 전해 주는 것조차도 그냥 되는 일이 아니다. 이런 것은 C/C++ 언어가 제공하는 런타임 라이브러리가 해 주는 일이며, 우리가 빌드하는 프로그램에 어떤 형태로든 코드가 링크되어 들어간다.

C/C++은 그나마 기계어 코드를 생성하는 언어이기 때문에 그런 런타임의 오버헤드가 작은 편이다. 그러나 비주얼 베이직(닷넷 이전의 클래식)이라든가 델파이는 RAD 툴이고 언어가 직접 GUI까지 다 커버하다 보니 언어가 제공하는 런타임 오버헤드가 크다.
자바나 C#으로 넘어가면 런타임이 단순 코드 오버헤드로 감당 가능한 정도가 아니라 아예 가상 기계 수준이다. 프로그램이 기계어 코드로 번역되지도 않으며, garbage collector까지 있다.

그렇게 프로그래머의 편의를 많이 봐 주는 언어들에 비해 '작은 언어'를 추구하는 C/C++은 도스나 16비트 윈도 시절에는 런타임 라이브러리가 static 링크되는 게 당연시되곤 했다. 오버헤드 자체가 수천~만 몇천 바이트밖에 되지 않을 정도로 매우 작은 편이고, 저수준 언어인 C/C++은 당연히 standalone 바이너리를 만들어야지 무슨 다른 고급 언어처럼 런타임 EXE/DLL에 의존하는 바이너리를 생성한다는 건 좀 안 어울려 보이는 게 사실이었다.

그래서 C/C++로 개발된 EXE 파일은 내부를 들여다보면 대체로, 링크되어 들어간 런타임 라이브러리의 이름과 개발사를 나타내는 문자열이 들어있었다. 볼랜드나 마이크로소프트 따위. 그래서 이 프로그램이 어느 컴파일러로 만들어졌는지를 얼추 짐작도 할 수 있었다.

C 런타임 라이브러리도 static이 아닌 DLL 형태로 제공하려는 발상은 Windows의 경우 아무래도 32비트 NT의 개발과 함께 시작된 듯하다. 그래서 윈도 NT 3.1의 바이너리를 차용해서 개발되었다는 과거의 Win32s를 보면 crtdll.dll이라는 파일이 있는데 이것이 운영체제가 기본 제공하는 프로그램들이 공용하는 C 런타임 DLL인 듯하다. 즉, 메모장, 문서 작성기, 그림판 등의 프로그램들이 호출하는 sprintf 같은 C 함수들의 구현체가 거기에 담겨 있다는 뜻이다.

재미있게도 윈도 NT의 경우, kernel32보다도 먼저 로딩되는 ntdll.dll이 내부적으로 또 각종 C 함수들의 구현체를 제공한다. 커널이 제대로 로딩되기도 전에 실행되는 프로그램이 공유하는 C 함수들이라고 한다.

과거의 윈도 9x의 프로그램들은 비록 32비트이지만 운영체제가 자체적으로 공유하는 C 런타임 DLL은 없다.
다만, 윈도 NT 3.x 이후로 비주얼 C++이 32비트 개발툴로 자리매김하면서 이 개발툴의 버전에 따라 msvcrt20.dll, msvcrt40.dll이 제공되기 시작했고 이들은 윈도 9x에서도 기본 내장되었다. 비록 같은 C 런타임이지만 버전별로 미묘한 차이가 있는 모양이다.

그러다가 1996년에 출시된 비주얼 C++ 4.2부터 C 런타임 DLL은 더 변경을 안 하려는지 파일 이름에서 버전 숫자가 빠지고, 그 이름도 유명한 msvcrt.dll이라는 이름이 정착되어 버전 6까지 쭉 이어졌다.
이 이름은 비주얼 C++ 4.2 ~ 6이 생성한 바이너리가 사용하는 C 런타임인 동시에, NT 계열에서는 기존의 crtdll을 대신하여 운영체제의 기본 제공 프로그램들이 공유하는 C 런타임의 명칭으로도 정착했다.

그리고 9x 계열도 윈도 98부터는 mfc42.dll과 더불어 msvcrt.dll을 기본 제공하기 시작했다.
NT와는 달리 운영체제가 msvcrt.dll을 직접 쓰지는 않지만, 비주얼 C++로 개발된 프로그램들을 바로 실행 가능하게 하기 위해서 편의상 제공한 것이다. 과거의 유물인 msvcrt40.dll은 msvcrt.dll로 곧바로 redirection된다.
그 무렵부터는 오피스, IE 같은 다른 MS 사의 프로그램들도 C 런타임을 msvcrt로 동적 링크하는 것이 관행으로 슬슬 정착해 나갔다.

그렇게 윈도, VC, 오피스가 똑같이 msvcrt를 사용하는 구도가 한동안 이어졌는데..
21세기에 비주얼 C++이 닷넷으로 넘어가면서 그 균형은 다시 깨졌다.
C 런타임 라이브러리도 한 번만 만들고 끝이 아니라 계속 버전업이 되어야 한 관계로 msvcr70, msvcr71 같은 DLL이 계속해서 만들어진 것이다. 결국, 비주얼 C++ 최신 버전으로 개발한 프로그램은 C 라이브러리를 동적 링크할 경우, DLL 파일을 배포해야 하는 문제를 새로 떠안게 되었다.

이것이 비주얼 C++ 2005/2008에서는 더욱 복잡해졌다. C 라이브러리를 side-by-side assembly 방식으로 배포하는 것만 허용했기 때문이다. 쉽게 말해, 레거시 공용 컨트롤과 윈도 XP의 비주얼 스타일이 적용된 공용 컨트롤(comctl32.dll)을 구분할 때 쓰이는 그 기술을 채택했다는 뜻이다.

그래서 msvcr80/msvcr90.dll은 윈도 시스템 디렉터리에만 달랑 넣는다고 프로그램이 실행되지 않는다. 이들 DLL은 DLLMain 함수에서 자신이 로딩된 방식을 체크하여 이상한 점이 있으면 고의로 false를 되돌린다. 그렇기 때문에 이들은 반드시 Visual C++ 재배포 런타임 패키지를 통해 정식으로 설치되어야 한다.

런타임 DLL간의 버전 충돌을 막기 위한 조치라고 하나 이것은 한편으로 너무 불편하고 번거로운 조치였다. C 라이브러리가 좀 업데이트돼 봤자 메이저도 아니고 마이너 버전끼리 뭐가 그렇게 버전 충돌이 있을 거라고..;; 나중에는 여러 프로그램들을 설치하다 보면 같은 비주얼 C++ 2005나 2008끼리도 빌드 넘버가 다른 놈들의 재배포 패키지가 설치된 게 막 난립하는 걸 볼 수 있을 정도였다. 가관이 따로 없다. 당장 내 컴에 있는 것만 해도 2008 기준으로 9.0.32729까지는 똑같은데 마지막 숫자가 4148, 6161, 17, 4974... 무려 네 개나 있다.

개발자들로부터도 불편하다고 원성이 빗발쳤다. MS Office나 Visual Studio급으로 수십 개의 모듈로 개발된 초대형 소프트웨어를 개발하는 게 아니라면, 꼬우면 그냥 C 라이브러리를 static 링크해서 쓰라는 소리다.
그래서 비주얼 C++ 2010부터는 C 라이브러리 DLL은 다시 윈도 시스템 디렉터리에다가만 달랑 집어넣는 형태로 되돌아갔다. 다시 옛날의 msvcrt20, msvrt40, msvcr71처럼 됐다는 뜻이다.

윈도 비스타 타임라인부터는 운영체제, VC, 오피스 사이의 관계가 뭔가 규칙성이 있게 바뀌었다.
오피스는 언제나 최신 비주얼 C++ 컴파일러가 제공하는 C 라이브러리 DLL을 사용하며, 운영체제가 사용하는 msvcrt도 이름만 그럴 뿐 사실상 직전의 최신 비주얼 C++가 사용하던 C 라이브러리 DLL과 거의 같다.

그래서 오피스 2007은 msvcr80을 사용하며, 오피스 2010은 비주얼 C++ 2008에 맞춰진 msvcr90을 사용한다. C 런타임 DLL도 꾸준히 버전업되고 바뀌는 물건이라는 것을 드디어 의식한 것이다. 비스타 이전에 윈도 2000/XP의 EXE/DLL들은 헤더에 기록된 링커 버전을 보면, 서로 사용하는 컴파일러가 다르기라도 했는지 통상적인 비주얼 C++이 생성하는 EXE/DLL에 기록되는 버전과 일치하지 않았었다. 9x는 더 말할 필요도 없고.

그럼에도 불구하고 msvcrt는 운영체제 내부 내지 디바이스 드라이버만이 사용하고, 비주얼 C++로 개발된 여타 응용 프로그램들은 언제나 msvcr##을 사용하게 용도가 확실하게 이원화되었다.
그래서 심지어는 똑같이 MS에서 개발한 한글 IME임에도 불구하고 Windows\IME 디렉터리에 있는 운영체제 보급용은 msvcrt를 사용하고, 한글 MS Office가 공급하는 IME는 Program Files에 있고 msvcr##을 사용한다. 둘은 거의 똑같은 프로그램인데도 말이다.

이것이 Windows와 C 런타임 라이브러리 DLL에 얽힌 복잡한 역사이다. 여담이지만 C 라이브러리 중에서 VC++ 2003이 제공한 msvcr71은 Windows 95를 지원한 마지막 버전이다. 2005가 제공한 msvcr80부터는 일부 보안 관련 코드에서 IsDebuggerPresent라는 API 함수를 곧장 사용함으로써 Windows 95에 대한 지원을 중단하였으며, 최하 98은 돼야 동작한다. VC++ 2008부터는 9x 계열에 대한 지원 자체가 중단됐고 말이다.

자, 여기서 질문이 생긴다. 그럼 C++은 사정이 어떠할까?

C보다 생산성이 훨씬 더 뛰어난 C++로 주류 프로그래밍 언어가 바뀌면서 표준 C++ 라이브러리에 대한 동적 링크의 필요성도 응당 제기되었다. 그러나 이것은 C 라이브러리보다는 시기적으로 훨씬 더 늦게 진행되었기 때문에 가장 먼저 등장하는 비주얼 C++의 DLL 이름이 msvcp50과 msvcp60이다. 즉, 비주얼 C++ 5.0 이전에는 선택의 여지 없이 static 링크만 있었다는 뜻이다.

더구나 이것은 전적으로 비주얼 C++ 소속이지, 운영체제가 따로 C++ 라이브러리 DLL을 제공하지는 않는다. MS에서 만들어진 제품들 중에 msvcp##를 사용하는 물건은 비주얼 C++ IDE와 컴파일러 그 자신밖에 없다.

C++ 라이브러리는 대부분 템플릿 형태이기 때문에 알고리즘 뼈대는 내 바이너리에 static 링크되는 게 사실이다. 그러나 그렇게 덧붙여지는 라이브러리 코드가 별도로 호출하는 함수 중에 DLL에 의존도를 지니는 게 있다. cout 객체 하나만 사용해도, 그리고 STL 컨테이너 하나만 사용한 뒤 빌드해 봐도 형체를 알 수 없는 이상한 메모리 관리 쪽 함수에 대한 의존도가 생긴다.

그래도 C 라이브러리 DLL은 사용한 함수에 따라서 printf, qsort 등 링크되는 심벌이 명확한 반면
C++ 라이브러리는 내부 구조는 이거 뭐 전적으로 컴파일러가 정하기 나름이고 외부에서 이들 심벌을 불러다 쓰기란 사실상 불가능하다는 큰 차이가 있다.

<날개셋> 한글 입력기는 C++ 라이브러리를 사용하지 않으며, 빌드 후에 생성된 바이너리를 인위로 수정하여 msvcr## 대신 강제로 운영체제의 msvcrt를 사용해서 동작하게 만들어져 있다.

Posted by 사무엘

2013/11/19 08:29 2013/11/19 08:29
, , ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/900

지뢰찾기 연구

요즘 팔자에도 없던 지뢰찾기에 살짝 재미가 붙었다.
본인은 비슷한 학력· 경력으로 IT 업계에 종사하는 여느 사람들과는 달리, 머리 싸움을 즐기는 스타일이 전혀 아니었으며 복잡한 퍼즐 게임 따위와도 담을 쌓고 지내는 편이었다. 이런 점에서 본인은 완전 퍼즐 게임 매니아인 T모 님과는 성향이 다르다.

그래도 지뢰찾기 정도면 요령을 알고 나니까 은근히 재미있다. 초보 레벨로는(9*9, 지뢰 10개) 40초~1분 남짓한 시간 동안 대략 60~70%대의 승률로 깨겠다. 처음엔 초보 레벨조차도 5분이 넘게 끙끙대기도 했으나, 마치 경부선 서울-부산 열차의 운행 시간이 17시간에서 6시간대~4시간대로 줄어들듯이 시간이 단축되었다.

사용자 삽입 이미지
지뢰찾기는 소련에서 개발된 테트리스와 더불어 시간 죽이기용으로 상당히 적절한 컴퓨터용 퍼즐인 거 같다. 여느 보드 게임과는 달리, 물건이 먼저 존재하다가 컴퓨터로 옮겨진 게 아니라 처음부터 컴퓨터용으로 만들어진 게임이라는 차이가 있다.

맥북의 터치패드 격인 트랙패드로는 도저히 게임을 할 수 없는 듯했다.
두 손가락을 동시에 누르거나 패드 우측 하단을 지그시 누르면 우클릭이 되긴 하는데, 이게 생각보다 정확하게 인식되지가 않는 듯하기 때문이었다.

지뢰가 있다는 깃발만 꽂으려고 우클릭을 했는데, 그게 좌클릭으로 인식되어 지뢰를 밟고 장렬히 죽는 참사가 한두 번 발생하는 게 아니어서 말이다. 단, Windows Vista 이후부터 새로 개발된 지뢰찾기는 Shift+클릭으로 우클릭, 더블클릭으로 좌우 클릭도 돼서 조작이 훨씬 더 편해졌다.

키보드로는 Space는 셀 개봉(좌클릭)이고, Shift+Space가 깃발(우클릭)이다.
그런데 이번엔 깃발이 꽂힌 것을 제외한 모든 인접 셀들을 한꺼번에 개봉하는 건 키보드로 어떻게 하는지 모르겠다. 게임에 익숙해지고 나면 셀 개봉은 하나씩 클릭하는 것보다 저렇게 개봉을 훨씬 더 즐겨 하게 되는데 말이다.

지뢰찾기라는 게임은 풀이 순서를 논리적으로 명확하게 유추 가능한 상황이 대부분이지만, 가끔은 주어진 정보만으로는 정확한 지뢰 배치를 알 수 없어서 찍기(guessing)를 해야 하는 경우도 있다. 지뢰가 정확하게 어떤 조건으로 배치되어 있을 때 그런 상황이 생기는지는 잘 모르겠다.

숫자 정보로부터 유추 가능한 지뢰 배치 가짓수는 기본적으로 폭발적으로 증가할 수 있으며, 어떻게 될 수 있는지 백트래킹으로 일일이 하나하나 때려박아 넣으며 추적을 하는 수밖에 없다. 뭔가 네모네모 로직을 푸는 것 같은 느낌이 들기도 한다. 이 때문인지 이 문제는 전산학적으로 봤을 때 NP 완전 문제라는 것까지 증명되었다.

그리고 찍기가 필요 없는 명확한 상황일 때 사람이 지뢰를 찾는 절차는 의외로 아주 명료하고 기계적이다.
딱 이 정도 영역이 개봉되고 인접 셀의 지뢰 정보가 이렇게 주어졌을 때, '명백한 해법' 하나만 동원해서라도 컴퓨터가 게임 진행을 충분히 도와 줄 수 있겠다는 생각이 들었다.

그래서, 막간을 이용해 지뢰찾기를 푸는 프로그램을 짜 봤다.
초-중급 수준의 간단한 클래스 설계와 알고리즘 구현이 동원되니 심심풀이 땅콩 코딩용으로 꽤 적절한 거 같다!
'명백한 해법'을 적용할 수 없어서 '찍기'를 해야 할 때, 지뢰가 있을 만한 위치를 가장 유력하게 유추하는 것 정도까지 구현해야 비로소 중급-고급 사이를 넘볼 수 있지 싶다.

대략의 코딩 내역은 이러하다.
지뢰 답을 알고 있는 MineSource 클래스(각 칸마다 지뢰 여부를 실제로 담고 있는 2차원 배열),
그리고 그 MineSource에다가 쿼리를 해 가며 1~8 숫자와 자기가 찾아낸 지뢰 위치 정보만을 알고 있는 MineSolver 클래스를 만들었다.
이들은 다 2차원 배열과 배열의 크기는 공통 정보로 갖고 있으므로 MapData라는 동일 기반 클래스를 설정했다.

MineSource는 특정 위치 x,y에 대한 쿼리가 온 경우, MineSolver에다가 인접 셀들의 지뢰 개수를 써 준다. 인접 셀에 지뢰가 하나도 없다면 여느 지뢰찾기 프로그램이 그러는 것처럼 인접 셀 8개도 한꺼번에 개봉하면서 flood fill을 한다.
곧바로 지뢰를 찍었다면 당연히 곧바로 게임 오버라고 알려 준다. 그리고 요즘 지뢰찾기 게임 구현체들이 그런 것처럼, 첫 턴에서는 절대로 지뢰를 찍는 일이 없게 내부 보정을 하는 것도 이 클래스에서 하는 일이다.

지뢰찾기의 '명백한 해법'은 딱 두 가지이다.

  1. 열리지 않은(지뢰 마크가 있는 놈 포함) 인접 셀의 개수와 자기 숫자가 '같은' 셀은 주변 미개봉 셀이 다 지뢰임이 100% 확실하므로 그것들을 전부 지뢰 마크(깃발)로 표시한다.
  2. 깃발이 꽂힌 주변 셀의 개수와 자기 숫자가 같은 셀의 경우, 지뢰 마크가 없는 나머지 열리지 않은 인접 셀은 지뢰가 아닌 게 100% 확실하다. 따라서 전부 개봉한다.
  3. (위의 명백한 해법만으로 개봉할 만한 셀이 존재하지 않는 건 운이 나쁜 케이스다. 패턴을 기반으로 랜덤 추측을 해야 하는데, 이건 일단 보류.)

텍스트 모드에서 자기 스스로 무작위하게 지뢰밭을 만들고 지뢰찾기를 풀기도 하는 자문자답 프로그램을 만드니, 200줄이 좀 안 되는 코드가 완성되었다.
이 프로그램은 인접 셀에 대해서 뭔가 조건을 만족하는 셀의 개수를 세거나, (getCount) 일괄적으로 동일한 조치를 취하는(doAction) 패턴이 많다.

이걸 그냥 for(j=y-1; j<=y+1; j++) for(i=x-1; i<=x+1; i++)이라는 2중 for문만으로 돌리기에는 i나 j가 boundary 밖인 경우도 고려해야 하고, (i,j)가 (x,y)와 같은 위치인 경우도 피해 가야 하기 때문에 loop 자체가 생각보다 복잡하다.
그러니, 그 loop 자체만 하나만 짜 놓고 loop 안에서 하는 일을 그때 그때 달리 지정하는 것은 템플릿-람다로 깔끔하게 설계했다.

다음은 프로그램의 간단한 실행 결과이다.

after the first turn:
+ + 1 . . . . . .
+ + 1 . 1 1 1 . .
+ + 1 . 1 + 2 1 .
+ + 1 . 1 2 + 1 .
1 1 1 . . 1 + 2 1
. . . . 1 1 + + +
. . . . 1 + + + +
. 1 1 2 2 + + + +
. 1 + + + + + + +

(중간 과정 생략)

picking 7 9
@ @ 1 . . . . . .
2 2 1 . 1 1 1 . .
1 1 1 . 1 @ 2 1 .
1 @ 1 . 1 2 @ 1 .
1 1 1 . . 1 2 2 1
. . . . 1 1 3 @ 2
. . . . 1 @ 3 @ 2
. 1 1 2 2 2 2 1 1
. 1 @ 2 @ 1 . . .
You Won!


이 정도 초보적인 지뢰 찾기 풀이 프로그램은 이미 다 개발되고도 남았으니,
유튜브를 뒤지면 신의 경지 수준의 속도를 자랑하는 지뢰찾기 TAS (매크로 프로그램 내지 역공학을 동원한 게임 스피드런) 동영상들이 나돌고 있다.

여담이다만, 지뢰찾기를 하다가 지뢰를 밟아서 게임 오버가 될 때 본인은 깜짝 깜짝 잘 놀란다. =_= 마치 옛날에 페르시아의 왕자를 하는데 타이밍을 잘못 잡아서 왕자가 쇠톱날(chopper)에 두 동강 나서 죽는 것 같은 그런 느낌이다.

Posted by 사무엘

2013/09/26 08:32 2013/09/26 08:32
, , , ,
Response
No Trackback , 4 Comments
RSS :
http://moogi.new21.org/tc/rss/response/881

지금으로부터 수십 년 전에는 동네마다 컴퓨터 학원이 있었고, 꼬꼬마가 프로그래밍을 공부하겠다고 하면 으레 GWBASIC부터 시작하곤 했다. 베이직은 16비트 MS-DOS뿐만 아니라 각종 가정용 8비트 컴퓨터에도 특유의 인터프리터 환경이 내장되어 있기도 해서 접하기가 한결 쉬웠다.

그때와는 달리 오늘날의 컴퓨터 교육은 이미 만들어진 소프트웨어들을 활용만 하는 실무에만 치우친 편이다.
지금 컴퓨터 프로그래밍을 처음부터 공부하고 싶다면 무엇부터 시작하는 게 좋을까?
이 질문에 대한 답변은 그 사람이 무슨 프로그램을 작성하고 컴퓨터로 무엇을 하고 싶은지 목적에 따라 크게 달라진다.
이 글은 나의 지극히 좁은 편견만을 반영하고 있으므로, 당연히 프로그래머마다 생각이나 견해가 다를 수 있다.

1. 비전문가/비전공자로서 그냥 최소한의 시간 투자로 개인적인 컴퓨터 활용도만 높이고 싶다면(고급 계산기 + 기초 알고리즘 실습 + 파일 자동 조작 + 매크로/자동화 도구 등)

개인적으로 파이썬을 추천한다. 복잡한 자료형을 다루기가 쉬워서 여타 언어들에 비해 짧은 코드만으로 복잡한 일을 한번에 끝낼 수 있다. 방대한 크기의 파일을 읽어서 내가 원하는 처리를 한 뒤 출력을 뱉어내는 수십 줄 남짓한 프로그램만 짤 줄 알아도 인생이 굉장히 편해질 수 있다.
좀 수학 덕후 기질이 있다면, 함수형 프로그래밍 언어를 건드려 봐도 될 듯.

2. 1보다는 좀 더 나아가서 가성비가 뛰어난 개발 환경에서 최소한의 GUI까지라도 만들어 보고 싶으면

Windows 플랫폼 한정으로 C#급 언어가 가장 좋겠다.

3. 웹브라우저에서 어지간한 애니메이션이나 프레젠테이션을 다 띄우고, 글이나 그림을 계산 결과로서 출력하고 싶으면

HTML + 자바스크립트.
요즘은 HTML이 단순히 화면에 뿌려지는 글과 그림, 하이퍼텍스트 문서일 뿐이라고 생각하는 건 큰 오산이다.
문서에다 서식을 주는 건 이제 CSS라는 방대한 별도의 규격으로 독립해 나가고, HTML은 문서 반 코드 반이다. 실시간으로 내용이 업데이트되고 화면 끝까지 스크롤됐을 때 추가로 컨텐츠를 로딩하고.. 사용자의 조작에 반응하여 그림을 뿌려 주는 등, 예전에는 ActiveX나 하다못해 플래시라도 써야 했을 컨텐츠들이 지금은 저것만으로 다 된다.

웹브라우저가 거의 플랫폼 독립적인 프로그램 구동 플랫폼처럼 바뀌었으니, 이를 활용할 줄 알면 역시 컴퓨터 활용 능력이 크게 향상될 수 있다. 웹 프로그램은 다른 언어나 런타임, IDE 같은 걸 설치할 필요조차 없이, 그냥 메모장에서 코딩 후 웹브라우저에서 곧바로 돌려보면 된다.

4. 맥 OS용 응용 프로그램이나 아이폰 앱을 개발하고 싶다면

Objective C + xcode + COCOA API 등등으로 고고씽이다. 일단 맥북을 장만해야 할 것이고 Windows와는 너무 이질적인 개발 환경 때문에 처음에 고생 많이 할 것이다.

5. 안드로이드 스마트폰용 앱을 개발하고 싶다면

자바 + 이클립스 IDE에 익숙해져야 할 것이다. Java는 요즘 스마트폰 앱 개발용 언어로 입지가 확 되살아난 듯하다. 이게 완전한 웹용 언어도 아니고(자바스크립트는 자바와 전혀 다른 언어임), 자바 애플릿이 플래시/ActiveX를 완전히 대체하는 RIA (rich internet application) 프레임워크로 자리잡은 것도 아니고, 로컬에서는 느리고 성능이 안 좋다 보니 전통적인 기계어 프로그램들에 밀려서.. 예전까지는 위상이 좀 어정쩡했기 때문이다. 로컬에서는 일부 크로스플랫폼 소프트웨어의 GUI(프런트 엔드)를 돌릴 때나 좀 쓰이곤 했다.

6. 끝으로, PC + Windows 환경에서 네이티브 코드 + standalone으로 실행되는 프로그램을 개발하고 싶다면

아래아한글이나 <날개셋> 한글 입력기나 어지간한 온라인 게임과 같은 급의 기계어 실행 파일을 만들고 싶다면..
역시나 재래식 Visual C++이나 최소한 델파이 같은 툴로 가야 한다.
개발 환경, 언어 문법과 기본 라이브러리, Windows API, 그 뒤 개발 분야에 따라 추가적인 라이브러리 공부까지 산 넘어 산이다.

오늘날 프로그램 개발 환경이 결국 로컬 + 웹 + 앱이라는 세 양상으로 구분된다고 예전에 글을 쓴 적이 있는데.. 그것과도 관계가 있다.
프로그래밍, 더 나아가 소프트웨어 개발에 관심이 있는 분이라면 이런 아이템들을 참고하면 되겠다.
그런데 정작 이런 글을 쓴 본인은 6만 빼고 나머지 분야는 여전히 너무 모른다는 게 함정..ㅎㅎ

Posted by 사무엘

2013/09/17 08:37 2013/09/17 08:37
,
Response
No Trackback , 13 Comments
RSS :
http://moogi.new21.org/tc/rss/response/878

수학에서 행렬은 굉장히 흥미로운 물건이다.
행렬끼리의 덧셈이나 행렬의 상수배는 어려울 게 없는 쉬운 연산이지만, 행렬끼리의 곱셈은 그렇지 않다. 행렬 A와 B사이의 곱셈은 A의 가로 크기와 B의 세로 크기가 같아야 정의되며, 새로 생기는 행렬의 크기(dimension)는 반대로 B의 가로 크기와 A의 세로 크기로 결정된다.

이런 특성상 행렬의 크기는 세로, 즉 row부터 먼저 써 주는 게 직관적이다. 세로 x줄 가로 y줄짜리 x,y 행렬과 y,z 행렬의 곱은 x,z 크기가 된다고 표기가 가능하기 때문이다.

또한, 앞에 있는 행렬과 뒤에 있는 행렬이 원소가 서로 연산되는 방향이 다르기 때문에 행렬의 곱셈은 교환 법칙이 성립하지 않는다. A×B가 일반적으로 B×A와 같지 않다는 뜻. 그러나 결합 법칙은 성립한다. (A×B)×C와 A×(B×C)는 동일하므로, 같은 방향만 유지하면 아무 순서로나 행렬을 곱해 줘도 된다.

그래서 이것과 관련하여 흥미로운 문제가 하나 있다.
크기가 들쭉날쭉 다르지만 순서대로 곱셈은 가능한(= 인접한 행렬끼리는 앞 행렬의 가로 크기와 뒤 행렬의 세로 크기가 일치) N개의 행렬들이 있다. 우리는 이들을 모두 최소의 계산량만으로 곱하고 싶다.

역행렬이나 행렬식 값을 구하는 비용에 비할 바는 아니겠지만 행렬의 곱셈은 꽤 비싼 연산이다. 일반적으로 x,y 크기와 y,z 크기의 행렬을 곱하는 데는 원소들간에 x*y*z회의 곱셈이 필요하다. n 크기의 정사각행렬의 경우 이는 n^3으로 귀착된다. (뭐, 분할 정복법을 활용하여 n^2.x승으로 줄이는 복잡한 알고리즘이 있긴 하지만 이것은 초기 준비 오버헤드가 굉장히 크기 때문에 행렬이 무진장 클 때에나 의미가 있다.)

예를 들어 A는 4*2 크기, B는 2*3 크기, C는 3*1크기의 행렬/벡터라고 치자.
이것을 A*B*C 순으로 진짜 순서대로만 곱하면 A*B를 곱하는 데 4*2*3=24회의 곱셈이 동원되고, 그 결과물인 4*3 행렬을 C와 곱하느라 12회의 곱셈이 필요해서 계산량은 총 36이 된다.

사용자 삽입 이미지

그러나 B*C부터 먼저 곱한 뒤 A를 거기에다 곱하면 열수가 적은 C 덕분에 B*C는 겨우 6회 만으로 끝나고, 거기에다 4*2*1=8회의 곱셈이 추가되어 총 14의 계산량만으로 A*B*C를 구할 수 있다. 답은 결국 똑같은데도 (AB)C보다 A(BC)가 훨씬 더 나은 전략인 것이다.

신기하지 않은가? 그래서 이런 configuration을 일반화하여 {4, 2, 3, 1}이라고 표현하고, 더 나아가 n>=3인 n개의 자연수라고 치자.
이 입력에 대해서 최소 곱셈 횟수와 실제 곱셈 순서를 구하는 것이 문제이다.

정올 공부를 한 분이라면 아시겠지만, 이것은 다이나믹 프로그래밍, 혹은 동적 계획법이라는 알고리즘 설계 방법론을 학습하면서 예시로 다뤄지는 아주 기본 문제이다. 다이나믹 프로그래밍은 다음과 같은 경우에 유용하다.

  • 전체 구간에 대한 최적해가 부분 구간의 최적해에다가 추가 연산을 함으로써 구하는 게 가능하다.
  • 그리고 한번 답을 구해 놓은 부분 구간의 최적해는 더 바뀌지 않는다는 게 보장된다.

이 행렬의 곱셈 문제에서 가장 작은 구간은 3이며, 이때의 답은 그냥 두 말할 나위 없이 세 정수의 곱이다.
그리고 전체 구간 [1..n]에 대해서 최적해는 바로..

  • 1을 [2..n]과 곱했을 때의 계산량 (맨 앞의 행렬과 나머지)
  • [1..n-1] 과 n을 곱했을 때의 계산량 (앞의 행렬들과 맨 뒤의 행렬)

중 더 작은 놈이라고 간주하면 된다.

그럼 [2..n]과 [1..n-1]은? 각 구간에 대해서 또 동일한 해법을 적용하여 재귀적으로 구간을 계속 쪼개 나가는 것이다. 언제까지? 구간의 길이가 3이 될 때까지 말이다.
이렇듯, 다이나믹 프로그래밍은 재귀성을 띠고 있다. 이것은 수학적으로는 점화식으로 표현되며, 코드로는...

const int dat[]={4,2,3,1,2,6,5,8,3,2}; //배열

int GetMin(int f, int t)
{
    int i=t-f, j;
    if(i<3) return 0; //should not reach here
    else if(i==3) return dat[f]*dat[f+1]*dat[f+2]; //obvious case
    else {
        //사실은 i가 3인 경우도 이 조건의 특수한 케이스라고 간주할 수 있다.
        //단지 GetMin값이 0이고, t-2와 f+1이 동일한 값이 될 뿐이다.
        i=GetMin(f,t-1) + dat[f]*dat[t-2]*dat[t-1]; //(A*B)*C
        j=GetMin(f+1,t) + dat[f]*dat[f+1]*dat[t-1]; //A*(B*C)
        return i<j ? i:j;
    }
}

int answer = GetMin(0, 10);

과연 이렇게 하면 답이 구해질까?
프로그램을 돌려 보면, 10개의 정수로 표현된 9개의 서로 다른 크기의 행렬들의 곱은..
146회의 곱셈만으로 계산이 가능하다고 나온다.

구체적인 계산 순서는 이러하다.

4 (2 (3 (((((1 2 6) 5) 8) 3) 2)))

이 경우, 각 단계별 계산 순서는 다음과 같이 되기 때문에,

x y z x*y*z
1 2 6 12
1 6 5 30
1 5 8 40
1 8 3 24
1 3 2 6
3 1 2 6
2 3 2 12
4 2 2 16

곱을 전부 합하면 진짜로 146이 맞다!
참고로, 이런 전략을 쓰지 않고 진짜 FM대로 앞에서부터 뒤로 행렬을 순서대로만 곱하면 계산량은 최적해의 세 배를 넘는 492에 달한다.
이것이 바로 알고리즘이 만들어 내는 차이이다.

다이나믹 프로그래밍에는 반드시 수반되어야 하는 작업이 있다. 바로 예전에 구했던 구간 계산값들을 배열에다 저장해 두는 것이다. 그렇게 하지 않으면, 마치 피보나치 수열을 f(x) = f(x-1)+f(x-2)라고만 구현하는 것만큼이나 계산량이 n이 커짐에 따라 기하급수적으로 커지게 된다. 그것도 예전에 한번 했던 똑같은 계산을 매번 반복하느라 말이다.
그래서 이 방법을 사용한 알고리즘은 대체로 시간 복잡도와 공간 복잡도가 모두 O(n^2)이 된다. 시간 복잡도가 지수함수에서 그래도 다항함수로 바뀐다.

구간별로 최적해 자체뿐만이 아니라 구간 분할을 어떻게 했는지에 대한 정보도 따로 보관해 놓으면 아까와 같은 구체적인 계산 순서도 그 정보를 추적함으로써 구할 수 있다.

정올에서 다이나믹 프로그래밍의 중요성은.. 두 말하면 잔소리이다.
본인은 20세기에 정올 공부를 한 세대인지라 그 시절의 문제밖에 기억을 못 한다만..

1997년 한국 정보 올림피아드의 고등부 3번인 벽장 문제는 최적해를 구하고자 할 경우 공간과 시간 복잡도가 O(n^3)인 다이나믹 프로그래밍으로 풀 수 있다. 이 때문에, 16비트 환경임을 감안하더라도 이 문제는 입력의 범위가 작다. 벽장의 개수와 벽장 사용 순서가 최대 겨우 20까지밖에 안 올라가는 소규모이다. 실용적인 상황에서는 이런 부류의 시뮬레이션 문제는 휴리스틱이 동원되어야 할 것이다.

이 외에,

1999년 고등부 1번 검은 점 흰 점 연결,
2000년 고등부 1번 수열 축소

도 다이나믹으로 푸는 문제이다.
국제 정보 올림피아드의 기출 문제 중에는
10회(1998)의 둘째 날 마지막 문제인 폴리곤 게임,
11회(1999)의 첫째 날 첫 문제인 꽃 진열이 기억에 남는다. 특히 꽃 진열은 상당히 기초적인 다이나믹 프로그래밍 문제로, <날개셋> 타자연습의 문장 정확도 측정도 이와 거의 같은 발상의 알고리즘을 사용하고 있다.

난 이 바닥은 손 놓은 지가 너무 오래 돼서 기억이 가물가물하다.
정보 올림피아드에서 경시와 공모는 마치 과학과 공학, 어학과 문학의 차이와 비슷한 것 같다.

Posted by 사무엘

2013/08/14 08:34 2013/08/14 08:34
, ,
Response
No Trackback , 8 Comments
RSS :
http://moogi.new21.org/tc/rss/response/866

이제는 딱히 새삼스러울 것도 없지만, Windows용 응용 프로그램들의 현대화 수준을 나타내는 지표로는 다음과 같은 것들이 있다.

1. 유니코드: 완전 기본 필수. 시대가 어느 시댄데 시스템 로케일(로캘?)이 한국어로 지정되어 있지 않은 운영체제에서 한글 UI가 ?로 죄다 깨진다거나 한글로 된 파일을 인식하지 못하는 프로그램은 처지가 참으로 안습하다. 한글 로케일에서도 상용 한자 4888자 이외의 한자를 인식할 수 없는 프로그램이라면 역시 무효임.

2. 64비트: 프로그램이 혼자서만 동작하는 EXE라면 32비트만 있어도 큰 상관이 없겠지만, 여타 프로세스 내부에서 동작하는 DLL(셸 확장, 훅, IME, 드라이버 등등)이라면 64비트 바이너리가 반드시 있어야 한다.

3. 멀티코어: 빡세게 많은 작업을 하는 프로그램이라면 요즈음의 컴퓨터에서 CPU를 최대 겨우 10~20%대밖에 안 쓰는 비효율적인 형태로 동작해서는 안 된다. 여러 코어가 작업을 어떻게 분담할지를 염두에 두고 프로그램이 개발되어야 한다.

4. 사용자 계정 컨트롤: Program Files 디렉터리 밑에다 개념 없이 사용자 데이터를 써 넣지 말며, XP 이하 OS에서는 신경 쓸 필요가 없던 권한 부족 에러가 제대로 처리되어야 한다. 레지스트리나 디렉터리가 redirection되는 일 없이 동작해야 한다.

5. 고해상도: 이제는 고해상도 모니터가 많이 보급되면서 종래의 100dpi가 아닌 120dpi 정도를 쓰는 빈도가 증가하고 있다. 이런 환경에서도 UI 화면은 적당하게 확대되어 나오거나 차라리 시종일관 동일한 픽셀 크기로 나오지, 글자가 깨지거나 GUI 요소가 들쭉날쭉 뒤죽박죽으로 배치되는 일은 없어야 한다.

이런 이슈들 중, 본인은 현재 5번을 주목하고 있다.
사실, 화면의 논리적 해상도를 바꾸는 건 엄청 옛날에 Windows 9x 시절부터도 있었던 기능이다. 하지만 그 당시는 화면 해상도가 겨우 800*600이나 1024*768이 고작이었기 때문에, 안 그래도 화면이 작아 죽겠는데 배율을 더 키우는 기능은 사실상 전혀 필요하지 않았다.

그러니 이건 정말 누가 쓰나 싶은 잉여로 전락했고, 수많은 프로그램들은 운영체제에 그냥 표준 해상도인 96dpi밖에 존재하지 않는 걸로 가정한 채 각종 좌표들을 하드코딩한 채로 개발되었다.

그랬는데 요즘 컴퓨터의 모니터들은 가로 해상도가 1500을 넘어가고 세로 해상도가 1000을 넘어가니, 이제는 화면을 좀 더 큼직하게 써도 되는 시대가 도래했다. 컴퓨터의 성능과 직접적인 관계가 있는 메모리와 CPU뿐만 아니라, 이런 디스플레이 기술의 발전도 컴퓨터의 발전에 큰 기여를 했음이 분명하다.

이제는 아이패드 같은 모바일 태블릿 기기조차 화면 해상도가 2000*1500을 넘어서 있다. 그러나 기술 발전을 아주 점진적으로 경험하여 legacy의 역사가 긴 PC 환경에서는, 고해상도를 고려하지 않고 설계된 프로그램들에게 재앙이 시작되었다. 논리 해상도에 따라 자동으로 크기가 조절되는 요소(시스템 글꼴 크기, 그리고 대화상자 크기)와 그렇지 않은 요소가 뒤섞이면 GUI 외형이 개판이 될 수밖에 없다.

현재 화면의 논리적 해상도는 데스크톱 화면의 DC를 얻어 온 뒤 GetDeviceCaps(hDC, LOGPIXELSX)를 하면 구할 수 있다. X뿐만 아니라 Y도 존재하는데, X축 값과 Y축 값이 서로 달라지는 경우는 (사실상) 없다고 생각하면 된다. 일반 배율인 100%일 때의 리턴값은 96이고, 125%일 때는 120이 돌아온다..

Windows에서 화면 DPI의 변경은 완전히 on-the-fly로 자유롭게 되는 작업은 아닌지라, 운영체제 재시작이나 최소한 로그오프가 필요한 이벤트이다. 그래서 그런지 Windows Vista는 전무후무하게 화면 DPI 변경을 '관리자 권한이 필요한 작업'으로 규정했었으나, 그 규제가 7 이후부터는 풀렸다. 또한, XP 이하의 버전은 100% (96dpi)보다 작은 값으로 변경하는 것도 가능했지만, Vista 이래로 더 작은 값으로는 지정 가능하지 않게 바뀌었다.

본인이 개발하는 <날개셋> 한글 입력기의 경우, 보조 입력 도구들은 옛날에 급조하느라 각종 버튼들의 좌표가 하드코딩되어 있었다. 다음 7.0 버전부터는 고해상도일 때는 전반적인 외형도 그에 비례해서 더 큼직하게 나오게 바뀔 예정이다.

하지만 편집기는 논리적 해상도에 관계없이 글자가 언제나 무조건 16*16 고정된 픽셀 크기로만 출력되며, 이것은 쉽게 개선되기 어려운 약점이다. 글꼴 자체는 16*16 비트맵만 쓰는 게 불가피하더라도, 고해상도에서는 그 상태 그대로 글자를 살짝 확대해서 찍어 주는 기능이 필요할 것 같다. 물론 anti-aliasing을 적용해서 부드럽게 확대해서 말이다.

고해상도 환경은 아이콘을 관리하는 것도 무척 까다롭게 만들었다. Windows 95/NT4 이전에는 아이콘은 오로지 32*32 크기밖에 없었는데 나중에 16*16 작은 크기가 추가되었다. 요즘은 그것도 모자라서 20*20이나 24*24 크기도 쓰이고 있다. 그래서 한 아이콘은 여러 크기의 아이콘 이미지들의 family 내지 컬렉션처럼 되었다고 본인이 예전 글에서 언급한 적이 있다.
예전엔 고해상도 모드에서 그냥 화면 왜곡을 감수하고라도 16*16 아이콘을 살짝 확대해서 보여주는 걸로 때웠지만, 이젠 안 그러고 20*20 크기용 아이콘도 직접 만들어 넣어 주는 셈이다.

사실 FM대로라면 운영체제가 사용하는 표준 아이콘 크기도 매번 GetSystemMetrics(SM_CXICON) 같은 식으로 쿼리를 해서 써야 고해상도 환경에서도 유연하게 대비를 할 수 있을 것이다. 하지만 맨날 봐 온 게 32나 16 같은 고정된 크기여서 하드코딩된 값을 쓰다가 나중에 그 코드를 고쳐야 하게 되면 대략 정신이 난감해질 수밖에 없다. 그리고 이것도 X값과 Y값이 서로 달라지는 일이 과연 존재할지 궁금하다.

그런데 문제는 Windows API는 아이콘이 여전히 단일 불변 크기만 있을 거라는 사상을 전제로 하고 설계되어 있다는 점이다.
HICON 은 여전히 그냥 단일 크기에 해당하는 아이콘 하나만을 나타내는 핸들이다. 즉, 한 아이콘 컬렉션 전체를 나타내는 자료형이 아니다. 그래서 LoadIcon이나 DrawIcon 같은 함수를 보면 아이콘의 크기를 받는 인자가 전혀 존재하지 않으며, 이 한계를 보완하는 LoadImage와 DrawIconEx 함수가 나중에 뒤늦게 추가되었음을 알 수 있다.

하지만 draw 기능은 몰라도 load 기능은 리소스 ID를 지정해 주면 그 ID가 가리키는 모든 크기의 아이콘을 다 로딩하게 하는 게 간편하지 않겠나 싶다. 그래서 draw 명령을 내리면, 원하는 크기와 가장 가까운 크기를 운영체제가 알아서 골라서 출력해 주는 것이다.

API의 기능이 그렇게 설계되었다면 윈도우 클래스를 등록할 때도 WNDCLASS에 이어서 굳이 작은 아이콘 핸들 hIconSm이 추가된 WNDCLASSEX 구조체가 번거롭게 또 만들어질 필요가 없었을 것이다. 그리고 응용 프로그램들이 고해상도용 아이콘을 지원하기도 훨씬 더 쉬워졌을 것이다. LoadIcon은 그냥 표준 크기 아이콘을 로딩하는 것만 지원하고, LoadImage는 아이콘을 로딩할 때 크기를 사용자가 일일이 지정해 줘야 하니 둘 다 불편한 구석이 좀 있다.

여담이지만, 응용 프로그램이나 운영체제별로 자신들이 설정하는 논리적 해상도는 제각기 좀 차이가 있다.
과거 도스용 아래아한글은 16*16 픽셀에 대응하는 글자가 10포인트였다. 그러나 Windows는 96dpi가 표준 해상도이며, 여기서는 12포인트가 16*16 크기이다.
한편, 맥 OS는 12포인트의 픽셀수가 Windows나 아래아한글보다 더 작다. 다시 맥 OS로 부팅해서 살펴보면 구체적인 비율을 알 수 있지만, 지금은 귀찮아서 생략.

이런 미묘한 문화 차이를 보면, FreeType API에서 FT_Set_Char_Size 함수에 굳이 상대 해상도 dpi값까지 인자로 받는 이유를 얼추 짐작할 수 있을 것이다. 번거롭지만 그런 것까지 다 수용할 수 있는 계층을 제공하기 위해서이다.

Posted by 사무엘

2013/06/20 19:19 2013/06/20 19:19
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/845

요즘은 컴퓨터의 인터넷 접근성이 워낙 좋아져서 응용 프로그램의 도움말은 그냥 개발사의 웹페이지에 기재된 문서 링크를 여는 걸로 대체하는 경우가 많다. 그러나 사용자의 컴퓨터에 직접 저장되어 있는 형태의 도움말 시스템도 여전히 필요하며 수요가 있다.

Windows가 98 시절부터 도입한 CHM, 즉 HTML 도움말은 여러 HTML 문서와 그림들을 한 파일로 묶어서 단일 컬렉션 파일을 만들 수 있다. 그렇기 때문에 소프트웨어의 도움말뿐만이 아니라 웹 문서 아카이브로도 활용할 수 있고 대단히 유용하다. 그 잠재적 유용성에 비해서 MS가 이 기술을 너무 홀대하고 있다는 생각이 든다.

평소에야 HtmlHelp 함수를 호출할 때 부모 윈도우의 핸들로 내 창을 넘겨 주면 알아서 도움말 창이 잘 생성된다. 그런데 내 프로그램은 별도로 창을 만들지 않으면서 HTML 도움말만 띄우고 싶으면 어떻게 하면 좋을까?
가령, 프로그램을 /?라는 인자를 주고 실행하면 옵션 사용법 도움말만 HTML 도움말 형태로 나온 뒤 프로그램을 바로 종료하게 하고 싶을 때 말이다.

일단, 운영체제는 HH.EXE라고 간단히 HTML 도움말을 띄워 주는 껍데기 프로그램을 제공하며, CHM 확장자는 기본적으로 이 프로그램에 연결되어 있다. 그렇기 때문에 ShellExecute 함수로 내 도움말 파일을 "open" 구동을 하면 도움말이 바로 뜨긴 한다.

그러나 이 방식은 도움말을 띄우는 것 자체 말고는 도움말 창에 대해서 그 어떤 제어도 할 수 없다. 가령, index.htm 같은 기본 시작 화면이 아니라 도움말 파일 내부에 있는 특정 문서를 바로 열게 하고 싶으면 도움말을 열지 말고 HH.EXE를 열고, 옵션에다가 xxxx.chm::/yyyy.htm 같은 식으로, chm 파일과 내부의 문서 파일을 이어서 특이하게 줘야 한다.

또한, HH.EXE의 실행이 끝날 때까지 기다렸다가 다른 후속 처리를 하게 하려면 이 프로세스의 핸들을 얻어야 할 텐데, 그러려면 ShellExecute보다 사용하기가 훨씬 더 까다로운 CreateProcess를 써야 할 것이다.

사실, WinHlp32.exe로 구동되던 과거의 HLP 도움말과는 달리, HTML 도움말은 hhctrl.ocx라는 DLL을 통해 in-process로 구동된다는 큰 차이가 있다. 이 특성을 살려, 굳이 외부 껍데기 프로세스인 HH.EXE를 호출하지 않고 내 프로세스가 직접 HTML 도움말 창 하나만 띄웠다가 곱게 종료할 수는 없을까?

부모 윈도우에다가 NULL을 주고 그냥 HtmlHelp 함수만 호출한 뒤 프로그램을 종료해 버리면, 도움말 창이 한 0.1초가량 눈에 비쳤다가 곧바로 사라져 버린다.
이 함수는 도움말 창을 띄워 주는 CreateWindowEx 함수와 개념상 거의 같다고 생각하면 된다. 이 함수도 생성된 도움말 창의 핸들값을 되돌리며, 창을 만든 뒤에는 그 창을 실제로 동작하게 하는 message loop을 돌려 줘야 한다.

HWND hMyWnd=::HtmlHelp(NULL, _T("xxxx.chm"), 0, 0);
ASSERT(hMyWnd!=NULL);

MSG m;
while(::GetMessage(&m,NULL,0,0)>0) {
    ::TranslateMessage(&m); ::DispatchMessage(&m);
}

이렇게 하면 도움말 창이 나타나긴 하나..
이번엔 도움말 창을 닫아도 프로그램이 종료되지 않고 '작업 관리자'에 내 프로세스가 언제까지나 표시되어 보인다는 문제가 발생한다.

내가 직접 창을 띄우고 윈도우 클래스를 등록하고 윈도우 프로시저를 구현하였다면, WM_DESTROY 메시지에서 응당 PostQuitMessage 함수를 호출해 줘서 GetMessage가 while문을 종료하게 했을 것이다.
그러나 도움말 창은 일반적으로 닫는다고 해서 응용 프로그램을 종료시키는 용도로 쓰는 물건이 아니다. 그래서 도움말 창만 단독으로 띄울 때 이런 문제가 생기는 것이다.

HTML 도움말 창이 없어질 때 프로그램도 정상적으로 종료되게 하는 방법은 크게 두 가지이다.
첫째는, 도움말 창이 WM_DESTROY 메시지를 받는 시점을 우리 프로그램이 잡아내어 그때 인위로 PostQuitMessage 함수를 호출하는 것이다. 훅킹(SetWindowsHookEx) 또는 서브클래싱(SetWindowLongPtr)을 생각할 수 있는데, 훅킹까지 쓰는 건 너무 오버인 것 같고, 내 경험상 이럴 때는 WM_DESTROY에 대해서 추가 처리만 살짝 해 주는 서브클래싱이 무난하다.

일반적으로 서브클래싱은 대화상자 안에 있는 각종 자식 컨트롤들의 동작을 미묘하게 바꾸기 위해서 하는데, 이렇게 큼직한 프레임 윈도우도 서브클래싱이 가능하다. 뭐, 서브클래싱을 쓰든 훅킹을 쓰든 어쨌든 콜백 함수를 정의해 줘야 하고 콜백 함수에게 context를 제공하기 위한 전역 변수나 TLS 슬롯이 필요하니 일이 여러 모로 복잡해진다.

다음 둘째는 첫째보다 더 정석적인 방법이다.
사실은 HTML 도움말 시스템 자체에, 도움말 창이 종료될 때 WM_QUIT 메시지를 보내게 하는 옵션이 있다. 딱 한 번만 옵션을 지정해 주고 나면 뒤끝 없이 OK이고 훅킹이고 뭐고 같은 지저분한 루틴이 없으니 아주 좋다. 그러나 옵션을 지정해 주는 방법이 생각보다 굉장히 지저분하다. API가 좀 구리게 설계되었다.

HH_WINTYPE hwt, *pwt=NULL;
::HtmlHelp(NULL, _T("xxxx.chm>main"), HH_GET_WIN_TYPE, (DWORD_PTR)&pwt);
if(pwt) {
    hwt=*pwt;
    hwt.fsValidMembers=HHWIN_PARAM_PROPERTIES;
    hwt.fsWinProperties=pwt->fsWinProperties|HHWIN_PROP_POST_QUIT;
    ::HtmlHelp(NULL, NULL, HH_SET_WIN_TYPE, (DWORD_PTR)&hwt);
}

이미 도움말 창이 떠 있는 상태에서 HtmlHelp 함수를 또 호출한다. 그런데, 도움말 창에 대한 정보를 얻기 위해서 창 핸들을 넘기는 게 아니라 또 도움말 파일을 길게 지정하고(중복 과잉 정보 공급), 그 뒤에 창의 내부 이름을 지정해 줘야 한다. 창의 내부 이름은 그 도움말 파일을 만든 사람이 지정해 준 명칭이다(저 예에서는 main).

핵심은 property에다가 HHWIN_PROP_POST_QUIT라는 속성을 추가로 지정해 주는 것이다. 이 상수는 불행히도 MSDN에 제대로 문서화도 돼 있지 않은 완전 잉여이다. 덕분에 이 명칭으로 구글링을 해도 수 페이지에 걸쳐서 이 이름의 값이 선언된 헤더 파일만 잔뜩 걸려 나올 뿐, 더 자세한 설명은 사실상 존재하지 않는다. HTML 도움말을 이런 식으로 깊숙하게(?) 다룰 생각을 하는 사람도 없을 테고 말이다.

나도 htmlhelp.h 파일을 뒤지다가 이걸 정말 우연히 발견했다. 그래도 이걸 써 주니 도움말 창을 닫을 때 프로그램이 바로 종료되게 할 수 있었다. Windows 98부터 8까지 다 잘 동작한다. HTML 도움말을 만든 개발팀에서 이 도움말 창만 단독으로 뜨는 상황도 생각을 안 한 건 아니었던 것이다.

공용 컨트롤을 다루면서 LVITEM 같은 구조체를 다룬 경험이 있는 분이라면, 저건 API 설계가 좀 특이하다는 걸 알 수 있을 것이다. 보통은 구조체를 선언하고, 구조체의 크기(t.cbSize=sizeof(t))와 얻고 싶은 정보를 나타내는 비트 플래그를 지정한 뒤, 구조체의 주소를(&t) get 함수에다 넘겨 준다.

그런데 HtmlHelp의 GetWinType는 아예 내부 포인터를 받게 돼 있다.
그리고 내가 지정하는 값은 property밖에 없음에도 불구하고 set을 할 때 일단은 구조체의 모든 멤버들의 값을 넘겨 줘야 한다(hwt=*pwt). 안 그러니까 프로그램이 에러가 나더라. 여러 모로 형태가 이상하다.

사실, HTML 도움말에는 저런 옵션을 지정할 필요가 없이 부모 윈도우에다가 여러 이벤트를 알려 주는 기능이 있다. 도움말 창이 처음으로 뜰 때(HHN_WINDOW_CREATE), 각종 페이지 이동 버튼을 누를 때(HHN_TRACK), 어떤 페이지를 성공적으로 열었을 때(HHN_NAVCOMPLETE) 이렇게 세 개가 정의되어 있는데, 사용자가 X 버튼을 눌러서 도움말 창이 소멸하는 시점을 알려 주는 기능이 없는 것은 개인적으로 굉장히 뜻밖이다. 왜 정작 필요한 이벤트는 없는 걸까? 본인이 개인적으로 가장 직관적으로 생각한 형태는 이런 것이었는데 말이다. 물론, 메시지를 받으려면 나도 윈도우를 하나 만들어야 하는 번거로움이 있긴 하지만 말이다.

EXE의 형태로 독립적으로 돌아가는 응용 프로그램이 아니라 DLL 형태인 IME들도 도움말을 표시하는 기능이 있다. 그러나 IME들은 안정성이나 키보드 포커스 같은 이유로 인해, 또 다른 DLL을 주입시키는 HtmlHelp 함수를 호출하는 게 아니라 앞서 소개했던 HH.EXE 프로세스를 수동으로 띄우는 원시적인 방식을 사용한다.
그래서 도움말 명령을 여러 번 내리면 도움말 창이 한도 끝도 없이 여러 개 생기며, IME를 사용하는 응용 프로그램을 종료하더라도 도움말 창은 같이 없어지지 않는다. Microsoft가 제공하는 기본 한중일 3개 국어 IME들이 모두 그렇게 동작하며, <날개셋> 한글 입력기 역시 외부 모듈은 그 관행을 따르고 있다.

본인을 포함해 HTML 도움말을 사용하는 많은 개발자들이 잊고 사는 사항인데, HTML 도움말도 원래는 사용 전에 초기화가 필요하다. HH_INITIALIZE 및 HH_UNINITIALIZE를 해 줘야 하고, 심지어는 message loop에다가도 원래는 HH_PRETRANSLATEMESSAGE를 해 줘야 한다. 하지만 현실적으로 그런 것까지 신경 쓰는 프로그램은 거의 없다. in-process 형태인 대신에 WinHelp 시절보다 번거로운 게 많아졌으며, IME의 경우 그런 것을 응용 프로그램에서 다 기대할 수 없으니 도움말을 외부 프로세스 형태로 실행해 주는 게 실제로 더 안전할지도 모르겠다.

HTML 도움말은 다형성을 지닌 인자에다가 typecasting을 하면서 여러 명령을 전달한다는 점, 초기화 및 해제가 필요하고 state를 지닌 변수가 존재한다는 점 등으로 인해 나름 클래스 라이브러리로 만들기에 적절한 면모가 있다. 물론 이 클래스의 인스턴스는 딱 단일체(singleton) 형태로만 존재해도 충분할 테고. 앞서 언급했던 자체 message loop을 도는 기능 역시 이 클래스의 멤버 함수로 추가해서 제공하는 것도 디자인 차원에서 생각해 볼 만하다.
이 글에서는 어쩌다 보니 HTML 도움말 하나만으로 일반적인 Windows 프로그래밍 이슈를 비롯해 다양한 이야기가 나왔다. ^^

Posted by 사무엘

2013/05/21 08:30 2013/05/21 08:30
,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/833

Windows 운영체제가 인식하는 실행 파일은 구조적으로 편의성의 상징인 GUI 프로그램과, 강력한 자동화의 상징인 콘솔(명령 프롬프트) 프로그램이라는 두 갈래로 나뉘어 있다. 이것은 SUBSYSTEM이라는 링커 옵션으로 지정 가능하다.

이 옵션이 콘솔로 되어 있으면 빌드 과정에서 링커는 C 라이브러리에서 main 함수를 찾아 호출하는 startup 코드를 연결하며, GUI로 지정되어 있으면 잘 알다시피 WinMain 을 호출하는 startup 코드를 연결한다. 해당 함수들은 물론 프로그래머가 따로 구현해 놓아야 한다.

어차피 GUI든 콘솔이든 EXE 파일이 제일 먼저 실행되는 지점은 실행 파일의 entry point에 지정된 주소이며 원래는 운영체제로부터 아무 인자도 전달되지 않는다. 그 대신, C 라이브러리가 GetModuleHandle, GetStartupInfo, GetCommandLine 등의 여러 기초적인 함수들을 먼저 호출하여 리턴값들을 WinMain에다가 전달해 줄 뿐이다.
콘솔 버전인 main도 마찬가지이다. 명령 옵션을 API 함수로 얻어 온 뒤, 그걸 C 라이브러리가 파싱하여 main에다가 argc와 argv의 형태로 전해 준다.

빌드 관점이 아닌 실제 실행의 관점에서 봐도, Windows는 콘솔 프로그램과 GUI 프로그램을 서로 약간 다른 방식으로 실행해 준다. 콘솔 프로그램의 경우 이미 명령창 같은 콘솔에서 실행되었다면 기존 콘솔을 자동으로 연결시키고, 프로그램이 탐색기 같은 GUI 환경에서 실행되어 콘솔이 없는 경우 “콘솔을 언제나 자동으로 생성”한다. 그 반면, GUI 프로그램에는 그런 조치를 취하지 않는다.

다만, 콘솔 프로그램이라고 해서 GUI 윈도우를 만들거나 메시지 loop을 돌지 말라는 법은 전혀 없으며, 반대로 GUI 프로그램도 추후에 자기만의 콘솔을 얼마든지 따로 생성해서 쓸 수 있다. 콘솔과 GUI를 적절한 혼용하면 유용한 경우가 의외로 매우 많다.

GUI 프로그램의 경우 디버깅 메시지를 찍기 위해 별도의 콘솔을 이용하는 것은 매우 흔한 테크닉이다. DOSBox가 대표적인 경우이다. 그리고 반대로 평소에는 명령창으로 문자열만을 취급하더라도, 가끔 그래프 같은 시각화된 결과물을 보여 줄 필요가 있을 때 제한적으로 GUI 윈도우를 생성하는 프로그램도 생각할 수 있다.

결국 GUI와 콘솔이 완벽하게 혼합된 프로그램이라면 이런 것도 가능해야 할 것이다.
프로그램을 아무 인자 없이 실행하거나, 또는 콘솔이 아닌 GUI 환경에서 실행하면 GUI가 나타난다. 반대로 콘솔에서 실행하거나 /? 같은 명령 옵션을 줘서 실행하면 콘솔로 메시지가 나타나고, 이미 콘솔이 있는 경우 그 콘솔을 사용한다. 압축 유틸리티 같은 게 이런 식으로 개발되어 있으면 아주 편리하지 않겠는가?

그런데 문제는 이 정도로 유연한 GUI/콘솔 하이브리드 프로그램을 만들기는 대단히 어려우며, 운영체제가 구조적으로 그런 것까지 고려하여 만들어지지는 않았다는 점이다. GUI와 콘솔 모두 2% 부족한 면모가 있다.

(1) 프로그램을 콘솔 방식으로 빌드하면, GUI 형태로 실행되어야 할 때에도 언제나 빈 콘솔창이 생겨 버린다. 프로그램이 실행되자마자 곧바로 API 함수를 호출하여 이 콘솔을 죽일 수는 있지만, 콘솔 창 같은 게 깜빡인 것이 사용자에게 그대로 드러나 보이기 때문에 이런 방식은 용납될 수 없다.

(2) 반대로 프로그램을 GUI 방식으로 빌드하면, 콘솔 환경에서 콘솔 형태로 실행되었을 때 기존 콘솔을 연결하는 방법이 없다. 콘솔 프로그램과는 달리 GUI 프로그램에서는 운영체제가 이것을 자동으로 해 주지 않는다. 콘솔에다 메시지를 찍는 것은 새로운 콘솔에다가만 가능하다. 기존 콘솔을 연결하는 AttachConsole이라는 함수가 차후에 추가되기는 했지만 방법이 완전하지 않다.

결국, 어느 방식을 선택하더라도 문제가 완전히 없을 수가 없다. 콘솔창을 필요할 때만 생성하면서 콘솔이 이미 존재하는 경우 기존 콘솔과 자동으로 연결이 되는 프로그램을 만들 수는 없는 것일까?

Visual Studio IDE인 devenv 프로그램은 이 문제를 해결한 듯해 보인다.
아무 인자를 안 주고 실행하면 잘 알다시피 커다란 IDE 창이 생긴다.
그러나 /? 를 주고 실행하면 각종 명령 옵션 사용법이 기존의 콘솔에다가 깔끔하게 찍힌다. 그냥 대충 도움말 창 하나 띄우고 끝인 게 아니다.
마소에서는 이것을 어떻게 구현하였을까?

그 비결은 너무 허무할 지경이다.
IDE 실행 파일이 있는 디렉터리를 가 보면, devenv 프로그램은 .exe도 있고 .com도 있어서 두 종류가 있다.

Windows는 도스 시절의 전통을 물려받았기 때문에 명령 프롬프트에서 사용자가 확장자 없이 실행 파일을 지정하면 EXE보다 COM을 먼저 실행한다. 그래서 COM은 /?  옵션 같은 걸 받아들이는 콘솔 프로그램으로 만들고, EXE를 GUI 프로그램으로 드는 꼼수를 쓴 것이다! devenv /?가 아니라 devenv.exe /? 라고 확장자를 강제 지정하면 명령 옵션 리스트가 역시나 대화상자 GUI 형태로 출력되는 걸 볼 수 있다. ^^

사용자 삽입 이미지사용자 삽입 이미지
도스 시절에 COM은 잘 알다시피 EXE보다 더 작고 단순한 실행 파일이다. 실행 파일 자체의 헤더나 파일 포맷 같은 게 존재하지 않으며, 메모리 재배치도 없이 최대 64KB의 크기 안에 x86 기계어 코드와 데이터가 모두 들어가고 컴퓨터의 고정된 메모리 주소에 그대로 주입되어 실행되었다.

요즘이야 COM이나 EXE나 모두 동일한 실행 파일이다. 오히려 COM 확장자를 사칭하여, 사용자가 의도한 프로그램 대신 악성 코드를 먼저 실행시키는 보안 위험이 문제되고 있는 지경이다. 마치 autorun 기능을 막듯이 COM의 실행을 막아 버리면 속 시원할지 모르나, 과거 프로그램과의 호환성 차원에서 그게 속 시원하게 가능할지는 모르겠다. 그래도 64비트 Windows는 아예 16비트 프로그램을 실행하는 기능 자체가 없어진 지 오래인데..

어쨌든, 실행 파일의 확장자로 콘솔용과 GUI용 프로그램을 구분시킨 건 Windows에서 배치 파일을 이용하여 자기 자신을 제거하는 프로그램을 만드는 것만큼이나 참 기발한 꼼수인 것 같다. 세상에 그런 방법을 쓸 줄은 몰랐다.

※ 추가 설명

1. Windows용 qt 라이브러리를 사용한 프로그램은 GUI 프로그램임에도 불구하고 main 함수에서 실행이 시작된다. 이것은 물론 qt 라이브러리의 내부에 WinMain 함수가 있어서 그게 사용자의 main 함수를 또 호출하기 때문일 것이다. MFC 라이브러리도 자체적인 WinMain 함수가 내부에 존재한다는 점을 감안하면 이는 충분히 수긍이 가는 디자인이다.

더구나 Windows를 제외한 다른 운영체제들은 실행 파일의 성격을 Windows처럼 GUI 아니면 콘솔 형태로 이분화하지 않으며 똑같이 main 함수를 쓴다. 그렇기 때문에, 크로스 플랫폼을 지향하는 qt는 응당 Windows에서의 프로그래밍 방식도 main을 기준으로 맞췄다고 볼 수 있다.

2. 과거의 16비트 Windows 시절에는 말 그대로 도스 프롬프트만이 있었을 뿐 콘솔이라는 게 없었다. 이것만으로도 그때 Windows는 구조적으로 기능이 굉장히 빈약했음을 알 수 있다.

Posted by 사무엘

2013/05/01 19:24 2013/05/01 19:24
, ,
Response
No Trackback , 4 Comments
RSS :
http://moogi.new21.org/tc/rss/response/825

« Previous : 1 : ... 15 : 16 : 17 : 18 : 19 : 20 : 21 : 22 : 23 : Next »

블로그 이미지

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

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

«   2024/11   »
          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

Site Stats

Total hits:
2988905
Today:
465
Yesterday:
1477