« Previous : 1 : 2 : 3 : 4 : 5 : Next »

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

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

Trackback URL : http://moogi.new21.org/tc/trackback/833

Comments List

  1. nyam 2013/05/21 09:54 # M/D Reply Permalink

    늘 깊이있는 지식을 공유해주심에 감사드립니다.
    저도 요즘 프로그램들이 인터넷 연결을 당연시하고 있다는 말씀에 동의합니다..
    그래도 도움말의 특성상 사용자들은 빠른 시간 안에 자신이 원하는 것에 대한 정보를 얻어내기를 원할텐데
    로컬에 저장되어있어 빠르고 일반적으로 목차가 함께 제공되어 직관적으로 접근 가능한 CHM은 이에 분명 아직도 장점이 많다고 생각됩니다.

    사실 그 이전에 정말 빠릿빠릿하게 열리는 HLP에 대해 Microsoft가 운영체제에서 WinHlp32.exe를 제거(사실 제거라기보다는 junk 프로그램을 넣어놨죠..)한 것도 전 매우 아쉽다는 생각이 듭니다.
    굳이 다양한 HTML의 기능을 사용하지 않는다면 HLP 만으로도 충분하겠죠..

    헌데 사실 뜯어보면 htmlhelp.lib 안의 코드도 참 재미있더군요..
    HtmlHelpA(W)에 대해 static하게 link하는 정보를 가진 것이 아니라
    LoadLibrary를 사용해 동적으로 hhctrl.ocx를 로드해서 GetProcAddress로 HtmlHelpA(W) 함수의 주소를 얻어와서 호출하더군요.
    제 생각으로는 HTML 도움말(hhctrl.ocx)이 설치되어있지 않은 과거 Windows 시스템에서 hhctrl.ocx의 static link로 인한 전체 프로그램 실행 불가능 사태를 방지하고자 했던 것 같은데 나름의 배려가 느껴지더군요..
    제 판단이 틀렸을 수도 있겠지만요.. ^^;;

    어쨌든 HLP를 사용하다가 CHM을 쓰면서.. 속도나 접근성 면에서 불만을 가질 때가 엊그제 같은데
    이제는 CHM도 점점 그리워지는 상황이 오고 있는 것 같습니다.. ^^;;

    1. 사무엘 2013/05/21 18:47 # M/D Permalink

      의견 남겨 주셔서 고맙습니다. ^^
      WinHelp는 Windows 3.0과 함께 처음 도입되었을 때는 정말 획기적이고 엄청난 물건이었지 싶습니다.

      운영체제 API에 정식으로 함수가 등재될 정도였고, Windows 95/98의 컴퓨터 시절에는 CHM보다 속도도 넘사벽급으로 빠르고 가볍고 좋았지요. 하지만 HLP도 ActiveX나 다름없는 각종 플러그 인을 얹고 구조를 확장할 수 있다는 점이 보안이라든가 64비트 대응 관점에서는 악재였던 것 같습니다. 옛날에 Robo HelpOffice던가 이런 툴을 쓰면 HLP로도 CHM 스타일의 목차 화면까지 만들 수 있었지요.

      그리고 HtmlHelp 함수는 말씀하신 것처럼 DLL로부터 함수 import를 직접 하는 게 아니라, hhctrl.ocx를 간접적으로 GetProcAddress하는 코드의 형태로 링크됩니다. 잘 알려진 사실이죠. HTML 도움말 시스템이 없는 IE 4 이하의 컴에서도 아예 로딩부터 실패하지는 않게 배려한 정책입니다. ^^;;;

Leave a comment

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

Trackback URL : http://moogi.new21.org/tc/trackback/825

Comments List

  1. nyam 2013/05/02 09:15 # M/D Reply Permalink

    저도 GUI 프로그램에 대해 콘솔에서 /? 명령 인자를 줘서 실행했을 때는 콘솔에 지원 명령 인자들에 대한 도움말을 출력하도록 노력해본 경험이 있어서 정말 반가운 포스트입니다.
    AttachConsole API를 써보니 기존에 열려(할당되어)있는 콘솔로 출력은 되는데.. 커서 위치가 업데이트가 안 되더군요.. (혹시 해결책을 알게 되시면 저도 좀 가르쳐주세요.. ㅠ.ㅠ)
    게다가 Windows XP 이후부터 추가된 API라는 점도 좀.. 그렇긴 하더라구요..

    요새 64-bit Windows를 사용하다보니 예전의 커맨드라인 유틸리티들 중 상당수가 MS-DOS 실행파일이라 실행조차 되지 않는 불상사가.. OTL
    그냥 cygwin 깔아놓고 PATH 연결해서 사용하는게 나은 것 같기도 합니다..;;

    항상 전문적인 지식을 공유해주셔서 감사드립니다..

    1. 사무엘 2013/05/02 09:52 # M/D Permalink

      nyam 님, 반갑습니다. 잘 지내시죠? ^^;;
      AttachConsole 함수가 완전하지 않다는 게 그런 문제 때문입니다. MSDN 저널에도 언급돼 있구요.
      외국 사이트들을 뒤져 봐도 다른 해결책은 저도 아직까지 못 봤습니다. ㅜ.ㅜ
      VS도 2012 버전에 갈 때까지 여전히 com/exe 꼼수를 쓰고 있는 이유도 그것 때문인 것 같습니다.
      그에 비하면, XP 이상만 지원한다는 접근성 단점이야 이제는 워낙 시간이 많이 지났으니 그리 큰 단점이 아니게 됐죠.

  2. 삼각형 2013/05/04 22:00 # M/D Reply Permalink

    이제까지 메인으로 Java를 사용하다 대학에서 C를 좀 쓸 일이 있어서 봤는데 (gcc 사용)

    의외로 콘솔 버전이라도 windows.h만 include하면 GUI를 쉽게 띄울 수 있더군요. 그래봐야 MessageBox 함수로 예, 아니요 입력 받는 수준으로 사용하지만요. (일반 메시지 출력은 괜찮은데 콘솔에서 y,n 입력을 상당히 불편해 하더군요.)

    콘솔 프로그램이기에 실행을 콘솔창에서 시켜서 잘 몰랐지만 이걸 Shell에서 실행시키니 콘솔창을 안쓸때도 콘솔이 떠버리는 문제가 있었네요.

    WinMain을 entry point로 잡으려면 gcc에서는 -mwindows 라는 컴파일러 옵션을 주고 하니 WinMain에서 실행되고 콘솔도 안뜹니다.

    리눅스 계열에서는 GUI에서 콘솔 컴포넌트가 있는지 window 안에서 콘솔 출력이 폰트까지 아주 예쁘게 출력됬던 것 같은데 말이죠.

    1. 사무엘 2013/05/06 10:41 # M/D Permalink

      네, 말씀하신 대로 콘솔 프로그램이라도 Windows API는 아무 문제 없이 사용할 수 있고 MessageBox 수준이 아니라 아예 message loop도 돌릴 수 있습니다.

      필요할 때만 콘솔을 띄우고, 이미 콘솔이 존재한다면 그걸 재활용까지 하는 형태의 프로그램을 만드는 게 Windows에서는 영 잘 안 되는 것 같더군요. (AttachConsole 함수 =_=)

      Windows의 경우, 콘솔 글꼴을 자유롭게 지정 못 하고 도스 시절과의 호환성 유지 때문인지 폭이 80칼럼으로 고정돼 있는 게 많이 아쉬운 점입니다. 그래서 기존 콘솔을 대체하는 PowerShell이라는 게 나온 거겠죠?

Leave a comment

컴퓨터 소프트웨어의 GUI 요소 중에는 잘 알다시피 체크 박스와 라디오 박스가 있다.
전자는 n개의 항목을 제각각 복수 선택할 수 있기 때문에 선택의 가짓수가 2^n개가 가능하다.
그 반면 후자는 n개의 항목 중 하나만 선택할 수 있기 때문에 선택의 가짓수가 딱 n이 된다.

그리고 이런 개념은 사실 메뉴에도 존재한다.
메뉴 항목은 사용 가능 여부(enabled)와 더불어 체크 여부(checked)라는 상태가 존재하여, 자신이 체크된 것처럼 보이는 시각적 피드백을 줄 수 있다.

Windows는 초창기엔(=16비트 시절) 말 그대로 √ 1종류만이 존재했다. 이를 제어하는 함수는 CheckMenuItem이다.
그러다가 Windows 95/NT4에서부터는 ● 모양의 체크를 표시해 주는 CheckMenuRadioItem 함수도 추가되었다. 이로써 각각의 항목들을 따로 체크할 수 있는 메뉴와, 여러 개 중 한 모드만 선택할 수 있는 메뉴의 구분이 가능해졌다.
CheckMenuRadioItem는 특정 메뉴 항목 하나의 속성을 바꾸는 여타 함수들과는 달리, 메뉴 항목들을 여러 개 한꺼번에 지정한 뒤 하나만 체크를 하고 나머지는 체크를 모두 자동으로 해제하는 형태로 동작한다.

그런데 재미있는 것은, MFC는 95/NT4 이전의 16비트 시절에서부터 메뉴에다 custom 비트맵을 지정하는 독자적인 방식으로 라디오 박스를 자체 지원해 왔다는 점이다.
운영체제에 CheckMenuRadioItem가 추가된 뒤에도 내부적으로 그 함수를 쓰지 않는다. 이것은 비주얼 C++ 2012의 최신 MFC도 변함이 없다.

MFC는 동일한 명령 ID에 대해서 메뉴, 도구모음줄 등 여러 GUI 요소에 대해 일관되게 checkd/enabled 상태를 관리할 수 있게 이 계층만을 CCmdUI라는 클래스로 따로 뽑아 냈다. 그리고 윈도우 메시지의 처리가 끝난 idle 시점 때 모든 GUI 상태들을 업데이트한다.
MFC 소스를 보면, CCmdUI::SetCheck는 CheckMenuItem 함수를 호출하는 형태이다. 그러나 CCmdUI::SetRadio는 운영체제의 API를 쓰는 게 아니라 자체 생성한 bullet 모양 비트맵을 SetMenuItemBitmaps로 지정하는 좀 더 힘든 방법을 쓴다.

고전 테마를 포함해 심지어 Windows XP의 Luna에서도 운영체제가 그려 주는 radio 그림과 MFC가 그려 주는 radio 그림은 차이가 거의 없었다. 둘 다 그냥 글자와 동일한 모양으로 동그란 bullet을 그리는 게 전부였다. 그렇기 때문에 두 구현이 따로 노는 건 그리 문제될 게 없었다.

그러나 문제는 Vista 이후에서부터이다. 운영체제가 그리는 radio 그림은 더 알록달록해지고 배경까지 가미되어 화려해진 반면, MFC가 그리는 radio 그림은 아직까지 단색의 단조로운 bullet이 전부이다. 그래서 시각적으로 이질감이 커졌다. 그것도 일반 체크(√) 항목은 괜찮은데 라디오(●) 그림만 차이가 생긴 것이다.

사용자 삽입 이미지사용자 삽입 이미지

이해를 돕기 위해 그림을 첨부한다. Windows Vista 이후에 운영체제가 메뉴에다 그려 주는 라디오 체크는 배경에 은은한 무늬가 생겨 있다(왼쪽). 그러나 MFC가 그리는 라디오 체크는 여전히 옛날 스타일대로 단색 동그라미밖에 없으며, 일반 체크와도 형태가 다르다(오른쪽). 오른쪽의 프로그램은 본인이 예전에 MFC 기반으로 개발했던 오목 게임이다. ㅋㅋ

MFC는 운영체제의 새로운 함수를 왜 쓰지 않는 걸까?
그냥 이런 사소한 데에까지 신경을 안 써서 그런 것일 수도 있고, 또 CCmdUI는 각각의 메뉴 항목에 대해 개별적으로 호출되는 반면 CheckMenuRadioItem는 그 자체가 여러 메뉴 항목의 상태를 한꺼번에 바꾸는 함수이기 때문에 기능의 구현 형태가 서로 맞지 않아서 도입하지 않은 것일 수도 있다.

물론, SetMenuItemInfo라는 만능 함수를 쓰면, 개별적으로 라디오 체크 상태를 바꾸는 것도 불가능하지는 않다. 다만, 구조체를 준비해야 하는 데다, 상태(state)만 옵션으로 간단히 바꾸면 되는 게 아니라 메뉴의 유형(type)까지 바꿔야 하니 일이 좀 번거로운 건 사실이다.

다만, 요즘은 MFC에도 잘 알다시피 MS Office나 Visual Studio의 모양대로 GUI 외형을 싹 바꿔 주는 툴킷이 도입되었고, 이런 상태에서는 어차피 메뉴의 요소들이 무조건 모조리 자체적으로 그려진다. 그러니 저런 SetRadio와 SetCheck의 동작 방식의 차이 같은 것도 존재하지 않으며, 그런 걸 논하는 게 아무 의미가 없다. 저건 오로지 운영체제 표준 GUI를 쓸 때만 발생하는 이슈이기 때문이다. ^^

* 글을 맺으며..

WinMain 함수를 포함해 윈도우 클래스 등록, 프로시저 구현을 전부 직접 하면서 Windows용 응용 프로그램을 밑바닥부터 만들어 본 사람이라면, MFC가 내부적으로 프로그래머에게 몰래 해 주는 일이 얼마나 많은지를 어렴풋이 짐작할 수 있다.

  • 대화상자를 창의 가운데에다 배치해 주는 것,
  • 프레임 윈도우와 뷰 윈도우 사이의 경계에 깔끔한 입체 모양 테두리 넣는 것,
  • 고대비 모드일 때 도구 아이콘의 검은색을 흰색으로 바꾸는 것,
  • 심지어 콤보 박스 내부에 디폴트 데이터(리소스 에디터에서 만들어 넣었던)들을 집어넣는 것,
  • 프레임 윈도우가 키보드 포커스를 얻었을 때 그 아래의 view 윈도우로 포커스를 옮기는 것,
  • 프로퍼티 시트의 내부에 들어가는 프로퍼티 페이지들의 글꼴을 운영체제 시스템 글꼴로 바꾸는 것 등..

이런 사소한 것들도 공짜가 아니라 죄다 MFC가 내부에서 해 주는 일들이다.
Windows API만 써서 프로그램을 만드는 방식은 최고의 작고 가볍고 성능 좋은 프로그램을 만들 수 있지만 생산성도 미칠 듯한 저질이기 때문에, 인제 와서 이런 불편한 방식으로 프로그램을 만들 프로그래머는 거의 없을 것이다. 요즘 세상에 C++도 아닌 C는 사실상 어셈블리나 마찬가지다.

Posted by 사무엘

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

Trackback URL : http://moogi.new21.org/tc/trackback/824

Comments List

  1. 김재주 2013/04/29 19:03 # M/D Reply Permalink

    이제 사실상 윈도 API를 직접 사용해서 C로 코딩해야 하는 상황은 커널에 붙어서 동작해야 할 드라이버 말고는 없다고 봐야겠죠. 아직까지는 Native Code가 performance 측면에서 장점을 가지고 있기 때문에 MFC와 C++이 여전히 쓰이고 있습니다만 C#과 JAVA로 대세가 넘어가는 것은 시간 문제가 아닐까 저는 생각하고 있습니다.

    이미 JIT 컴파일러가 실시간으로 profile을 통해 생성해내는 바이너리 코드가 static하게 생성된 프로그램을 뛰어넘는 경우가 보고되고 있으니까요. 특히 입력의 특성에 따라서 그때그때 분기의 흐름이 아주 다양하게 변화할 수 있는 프로그램의 경우엔 static profile guided optimization의 성능이 dynamic 버전을 따라가기 어려우니까요. 물론 이 방법에도 단점은 존재하지만...

  2. Lyn 2013/04/29 19:40 # M/D Reply Permalink

    jit은 이론상 스태틱한 컴파일러의 성능을 뛰어 넘고 실제로 최근들어 그런 강력한 jit 컴파일러들이 속속 등장하고 있지만 GC가 속도 다 까먹고있죠 (...)

    뭐 요즘은 드라이버나 펌웨어도 C++로 짜니...

  3. Lyn 2013/04/29 19:45 # M/D Reply Permalink

    그나저나 UI 프로그램을 전혀 안하다보니 저런것들이 다른지는 몰랏네요

  4. 사무엘 2013/04/29 23:20 # M/D Reply Permalink

    김재주, Lyn: 요즘은 MFC를 비롯해 C/C++의 라이브러리들 오버헤드도 너무 커져 보인다는 게 함정.
    그러니 차라리 그런 공통된 오버헤드를 운영체제가 미리 다 담당하고 있는 더 상위 계층의 프레임워크의 존재 명분도 섭니다.
    하지만 IME 쪽은 만년 네이티브 코드 강세 지대랍니다. ^^;;

    그리고 아무리 성능 크리티컬한 환경이라도 요즘 어지간해서는 C++조차도 없이 C로만 코딩을 해야 하는 곳은 진짜 거의 없어지지 않았을까 싶네요.

    라디오 체크 모양의 차이는 저도 최근에 발견하고는 흥미로워서 블로그 글로 정리했습니다.
    IME 개발하려면 운영체제의 기본 UI하고는 쫙 밀착을 해야 한답니다. =_=

  5. 김재주 2013/05/01 11:42 # M/D Reply Permalink

    임베디드 쪽에선 아직 C로 짜야 하는 일이 흔합니다. 예를 들어 SD나 CF카드 내부의 FTL같은 곳이 그렇죠. 이쪽은 RAM도 SRAM이고 1메가에도 턱없이 모자라는 메모리 위에서 동작하는데다 턱없이 적은 소비전력만으로 동작해야 하니까요..

    PC에서라면 말씀하신 것처럼 이제 C만으로 코딩할 일은 정말 없겠죠.

  6. Lyn 2013/05/02 09:04 # M/D Reply Permalink

    PC가 아니라 어지간한데도 다 ...

    이미 예전에 임베디드라고 불리던 급의 기계들도 다 범용 OS 깔리게된지 한참이니 ...

  7. 사무엘 2013/05/02 09:44 # M/D Reply Permalink

    음, 그런데 자원이 너무 열악해서 C++ 컴파일러조차 돌릴 수 없는 모바일이라 해도 크로스 컴파일은 가능하지 않을까요? 그것조차도 안 돼서 C만 써야 하면 정말 암울할 듯.

  8. 김재주 2013/05/03 13:11 # M/D Reply Permalink

    ARM을 쓰는 환경이라도 정말 천차만별이라.. 플래시 메모리 카드 같이 칩을 엄청 소형화해야 하는 경우에는 메모리와 CPU마저도 원칩에 통합시켜야 할 경우가 있습니다. 모바일 DRAM으로는 커버가 안 될 정도로요. 이 경우 SRAM을 사용해야 하는데 보통 96KB(!) 정도를 사용합니다.

    C++을 크로스 컴파일해서 코드를 생성하는 것 자체는 가능한데, 이런 환경에서는 가상 생성자라든지 아무튼 C++에서 지원하는 고급 프로그래밍 언어 기술을 사용하기 위해서 구조체에 추가적으로 붙게 되는 그 약간의 공간마저도 상당히 아깝다는 거죠.

    그리고 이런 칩에 들어가는 프로그램들은 말 그대로 어떤 특수한 목적을 위한 전용 코드에 가깝기 때문에 OOP를 사용해도 그다지 이점이 없다는 것도 있습니다. 기껏해야 수천에서 수만줄밖에 안되는 프로그램이니까요.

Leave a comment

1. 메뉴 -- 긴 역사를 자랑하는 GUI 구성요소

'메뉴'(menu)라는 단어는 순우리말로는 흔히 차림표라고 하고, 식당의 음식 메뉴 아니면 컴퓨터 소프트웨어의 GUI 요소라는 꽤 이질적인 두 심상이 결합해 있는 독특한 단어이다. 이런 점에서 '메뉴'는 '마우스'하고도 비슷한 구석이 있는 것 같다.

메뉴는 GUI라는 개념이 컴퓨터에 도입된 이래로 굉장히 오랜 시간을 인간과 함께해 왔다. 워낙 중요하고 필수적인 기능이기 때문에 Windows 운영체제는 아예 API 차원에서 창을 하나 만들 때 메뉴 핸들을 같이 넘겨 줄 수 있게 돼 있다. (CreateWindowEx 함수) Windows는 그래도 보급 메뉴(?) 지원을 무시하고 GUI 툴킷이 자체 구현한 싸제 메뉴를 붙일 여지라도 있지만, Mac OS는 메뉴 bar가 무조건 화면 위에 붙박이로 고정이고 게다가 운영체제의 시스템 메뉴와 일심동체로 통합되어 있기 때문에 싸제 메뉴 같은 건 있을 수 없다.

물론, 너무 무난하고 밋밋한 관계로 요즘 만들어지는 응용 프로그램에서는 메뉴가 천덕꾸러기처럼 취급되는 면모가 없지는 않다. 메뉴+툴바가 리본 UI로 대체된 것은 물론이고, 메뉴가 있더라도 메뉴 bar를 평소에는 감춰 버리고 Alt키를 눌러야만 마지못해 보여 준다. 글쎄, 이러다가 나중에 또 복고풍으로 메뉴로 돌아가지는 않을지?
그리고 어떤 경우든 사각형 안에서 선택막대로 기능을 선택하는 전통적인 메뉴 개념 자체가 없어지는 일은 없을 것이다.

난 닷넷 프레임워크는 그냥 운영체제의 보급 메뉴를 자기 고유 API로 감쌌는줄 알았는데, 그렇지 않다는 걸 알게 되어 개인적으로 놀란 적이 있다. 닷넷 기반 GUI 프로그램은 기본적으로 Office XP 스타일을 적당히 따라 한 싸제 메뉴가 나온다.

보급이든 싸제든, 어쨌든 GUI에서 전통적인 메뉴는 F10을 눌렀을 때 화면 상단에 나타나는 가로줄 메뉴, 혹은 main 메뉴를 가리키는 경우가 많다.
그러나 이것 외에 어떤 개체를 마우스로 우클릭했을 때 나타나는 Context 메뉴, 혹은 팝업 메뉴는 좀 더 나중에, 1990년대 중반에 도입되었다. 윈도우 95 이전에 3.x 시절에는 그림판으로 두 색깔을 번갈아가며 쓸 때 말고는 마우스를 우클릭할 일 자체가 거의 없었던 것 같다. 팝업 메뉴를 띄우는 기능 자체는 3.x 시절에도 있었을 텐데도 불구하고 말이다.

2. HMENU

자, 그럼 Windows 플랫폼 프로그래밍의 관점에서 운영체제의 메뉴 개체에 대해서 좀 더 살펴보자.

이 메뉴라는 놈을 관리하는 개체는 바로 HMENU이다. 얘는 메뉴에 표시시킬 각종 아이템들과 그것들의 상태들을 보관하고 있는 일종의 연결 리스트의 포인터라고 생각하면 된다. 어떤 메뉴 항목에는 또 부메뉴가 딸려 있을 수 있으므로 메뉴는 일종의 재귀성까지 갖추고 있다.

메뉴는 잘 알다시피 리소스의 형태로 쉽게 만들어 내장시킬 수도 있다. 그러나 HMENU 값은 아이콘이나 액셀러레이터, 마우스 포인터 같은 여타 리소스들과는 달리, read-only 리소스가 아니다. 이게 무슨 말인지 배경을 좀 설명하자면 이렇다.

16비트 Windows 시절에는 EXE/DLL에 있는 리소스 데이터를 얻기 위해서 별도로 파일을 열고 메모리를 할당하고 고정하는 등의 절차가 필요했다. 그러나 운영체제가 32비트 환경으로 바뀌면서 실행 파일의 로딩 방식이 memory mapping 방식으로 바뀌었기 때문에, 모듈에 내장된 리소스를 찾는 건 그냥 이미 로딩된 메모리의 주소만 되돌리는 형태로 아주 간단해졌다.

그래서 예전과는 달리, 이제는 한번 fetch해 온 리소스 데이터에 대해서 FreeResource 같은 함수를 호출할 필요가 없어졌다. 그 리소스를 제공하는 EXE의 실행이 종료되거나 DLL이 Unload될 때 어차피 자동으로 한꺼번에 해제되기 때문이다.

일반적인 읽기 전용 리소스는 그런 간소화의 혜택을 입게 되었다.
그러나 메뉴의 경우는 모듈에 내장된 메뉴 데이터의 포인터만 얻어 오는 걸로 끝이 아니라, 그 데이터를 토대로 메뉴 연결 리스트를 별도로 재구성한다. 사용자는 그 연결 리스트의 데이터를 변경함으로써 메뉴에 별도의 항목을 추가하거나 삭제하고, 체크 표시나 disable 처리를 할 수 있다.

그렇기 때문에 LoadIcon, LoadCursor 등의 리턴값은 Free를 할 필요가 없지만, LoadMenu 핸들의 리턴값은 반드시 DestroyMenu를 해 줘야 한다. (물론, 아이콘 같은 리소스라 해도 모듈 내장이 아니라 직접 동적으로 생성한 놈이라면 Destroy*함수를 호출해서 수동으로 소멸해야 하는 건 변함없음.)

HMENU는 내부적으로 딱히 reference counting을 하지는 않는 단순한 구조이다.
윈도우와 연결되어 있는 메뉴는 윈도우가 소멸될 때 같이 자동으로 소멸되며(물론 부메뉴들도 재귀적으로 다 같이), 한 메뉴 인스턴스가 여러 윈도우에서 공유되지는 않는다. '이동', '닫기' 같은 명령이 있는 시스템 메뉴가 있는데, 필요하다면 사용자가 이 메뉴 역시customize할 수 있다.

3. API 디자인

(1) Windows API의 설계 관점에서 흥미로운 것은, 정수로 식별하는 ID를 받는 곳에다가 필요에 따라 메뉴 핸들도 같이 집어넣게 한 게 종종 보인다는 점이다.
CreateWindowEx 함수의 경우, HMENU는 생성하려는 윈도우가 팝업 같은 메이저 윈도우이면 메뉴 핸들이고, 메뉴를 갖는 게 의미가 없는 자그마한 마이너 자식 윈도우이면 정수 ID를 의미한다.

물론 메뉴 핸들과 ID가 동시에 쓰일 일은 없는 건 사실이다. 윈도우의 ID는 대화상자의 차일드 컨트롤들을 식별할 때에나 쓰는 것이니 말이다.
하지만 어째 이 둘을 실제로 공유시킬 생각을 했는지 궁금하다. 어지간하면 그냥 내부 구조체에다 별도의 멤버를 따로 둘 법도 한데, Windows 1.x 시절의 헝그리 정신을 살려, 메모리 절약을 위해 공용체를 썼는가 보다.

또한 메뉴 API도 AppendMenu나 InsertMenu를 보면, 일반 메뉴 아이템에 대해서는 명령 ID를 전달하는 항목에, MF_POPUP이 지정된 하위 메뉴 아이템에 대해서는 또 HMENU를 typecast하여 전달하게 되어 있다.

(2) CreateMenu와 CreatePopupMenu 함수를 왜 따로 만들어 놨는지 영 이해가 안 된다. HINSTANCE와 HMODULE만큼이나 사실상 의미 없는 구분이 돼 있다.
응용 프로그램의 main 메뉴나 우클릭 팝업 메뉴는 화면에 보이는 형태만 다를 뿐, 부메뉴를 가질 수 있는 재귀적인 형태인 것도 똑같고 내부 자료 구조가 달라야 할 것은 없다.
하긴, 그러고 보니 HCURSOR도 HICON하고 내부적으론 거의 같은 자료구조라고 하지. (핫스팟 위치만 추가됐을 뿐)

(3) 메뉴의 상태를 나타낼 때 MF_GRAYED와 MF_DISABLED를 따로 만들어 놓은 건 개인적으로 무척 기괴하게 여겨진다.
MF_GRAYED는 우리가 흔히 보는 '사용할 수 없는' 메뉴 아이템이다. 흐리게 표시되고 선택도 되지 않는다. 그러나 MF_DISABLED는 선택만 안 될 뿐 흐린 표시는 아니다.
이건 솔직히 말해서 잉여력이 넘치는 구분이다.

그래서 심지어는 MS 내부의 개발자들조차도 이를 혼동해 있다.
고전 테마를 쓰고 있을 때는 MF_DISABLED를 설정한 메뉴가 '일반 글자'로 표시된다.
그러나 Luna나 Aero 같은 테마가 적용되어 있을 때는 이게 MF_GRAYED와 동일하게 '흐린 글자'로 표시된다! 문서화된 바와도 다르고 일관성 없게 동작한다는 뜻이다. 내 말이 믿어지지 않으면 당장 프로그램을 짜서 확인해 보기 바란다.
일상생활에서는 MF_DISABLED는 전혀 신경 쓸 필요 없고 MF_GRAYED만 쓰면 될 것 같다.

(4) RemoveMenu, DeleteMenu, DestroyMenu의 차이가 뭘까?
먼저 DestroyMenu는 HMENU 자체를 완전히 소멸시키는 함수이다. 메뉴와 부메뉴들이 모두 다 사라지고 해당 핸들은 사용할 수 없게 된다.
RemoveMenu와 DeleteMenu는 메뉴 안에 있는 한 항목을 제거한다. 제거할 항목을 순서 인덱스 또는 명령 ID로 지정할 수 있다. 부메뉴를 가진 항목이나 항목 구분용 separator는 명령 ID를 갖고 있지 않으므로 반드시 순서 인덱스만 지정 가능할 것이다.

둘의 차이는 딱 하나. 부메뉴를 가진 항목을 지울 때 부메뉴 핸들을 재귀적으로 destroy하느냐(Delete) 안 하느냐(Remove)이다. 마치 '프로젝트 목록에서 파일 제거'와, '파일 제거 + 실제로 디스크 상에서도 삭제'의 차이와 비슷한 맥락이다.

(5) 사실, Windows의 메뉴 API가 좀 더 객체지향적으로 설계되었다면, HMENU뿐만 아니라 각각의 메뉴 아이템을 나타내는 HMENUITEM 같은 자료형도 또 만들었을 것이다.
지금은 그렇지 않기 때문에 메뉴 아이템을 식별할 때마다 매번 HMENU와 UINT nID, 그리고 nID가 명령 ID인지, 순서 인덱스인지를 나타내는 플래그를 넘겨줘야 한다. 메뉴 항목을 편집하거나, 어디 뒤에 삽입하거나 삭제하는 함수들이 전부 저 인자들을 일일이 받는다. 내가 보기엔 무척 지저분하다.

또한 동일한 기능을 하는 API가 구 API, 그리고 좀 더 기능이 확장되고 구조체를 인자로 받는 신 API가 섞여서 중구난방스러운 것도 어쩔 수 없는 일이다. 가령, 예전에는 CheckMenuItem 같은 함수가 있었지만 지금은 SetMenuItemInfo가 있는 식. 새로운 함수는 범용적이긴 하지만 매번 구조체를 만들어서 초기화해 주는 작업이 몹시 성가신 것도 사실이다.

32비트 Windows부터는 각각의 메뉴 아이템에 대해서 명령 ID와는 별개로 임의의 UINT_PTR 데이터 값을 갖는 게 가능해졌다. 마치 리스트박스에서 item data와 비슷한 맥락이다. 이 값을 읽고 쓰는 함수로 지저분하게 SetMenuItemData 같은 함수를 또 추가하느니, 차라리 메뉴와 관련된 모든 속성을 읽고 쓸 수 있는 SetMenuItemInfo라는 종결자 함수를 만들게 됐을 것이다.

Posted by 사무엘

2013/03/10 19:15 2013/03/10 19:15
, ,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/805

Trackback URL : http://moogi.new21.org/tc/trackback/805

Comments List

  1. Lyn 2013/03/13 17:01 # M/D Reply Permalink

    윈32 API가 계속 확장하다보니 중구난방인건 사실이지만 ...

    그렇다고 맥이 카본버리고 코코아 간것처럼 했다간 개발자들 단체 버서커모드 될듯 (...)

    1. 사무엘 2013/03/13 20:07 # M/D Permalink

      “소프트웨어 개발자들의 폭력성을 실험하기 위해 플랫폼 API를 다 갈아엎어 보았습니다. 순간적인 변화를 참지 못하고...” 꼴. =_=;; ㅋㅋㅋㅋ

Leave a comment

1.

본인은 비주얼 C++ 2012로 갈아탄 뒤부터 예전에는 본 적이 없는 이상한 현상을 겪곤 했다. 내가 만들고 있는 프로그램을 IDE에서 곧장 실행하자(Ctrl+F5 또는 F5) 프로세스는 분명히 실행되어 있는데 창이 화면에도, 작업 표시줄에도 전혀 나타나 보이지 않았다.

Spy++를 돌려 보니 프로그램 창이 생기긴 생겼는데 어찌 된 일인지 WS_VISIBLE 스타일이 없이 숨겨져 있다는 걸 알게 되었고, 문제의 원인은 생각보다 금방 발견할 수 있었다.
프로세스에 전달되는 STARTUPINFO 구조체의 wShowWindow 멤버 값은, dwFlags에 STARTF_USESHOWWINDOW 플래그가 있을 때에만 유효하다는 걸 깜빡 잊고 있었던 것이다.

일반적으로 프로그램을 실행할 때 운영체제가 그 구조체에다 ShowWindow 플래그를 안 넣는 적은 사실상 없기 때문에 지금까지 그 로직이 별로 문제가 되지 않았었다. 하지만 비주얼 C++ 2012는 이례적으로 그 구조체의 거의 모든 멤버들을 그냥 0으로만 집어넣은 채 프로세스를 생성하고, 0은 SW_HIDE와 같기에 창이 화면에 나타나지 않았다.

2.

<날개셋> 한글 입력기 외부 모듈을 debug 형태로 빌드한 뒤 디버거를 붙여서 실행해 보면, 때에 따라서는 호스트 프로세스가 종료될 때 memory leak 로그가 뜨는 경우가 종종 있었다. 하지만 이것이 항상 나타나는 건 아니고 leak의 양이 심각하게 많은 건 아니었기 때문에, 본인은 크게 신경 쓰지는 않았다.

그런데 우연히 추가 디버깅을 한 결과, 응용 프로그램에 따라서 아예 COM 개체들의 reference count가 달라지고 TSF 모듈의 소멸자 함수의 실행 여부가 달라지는 걸 발견하였고, 이에 본인은 이 현상에 대해 좀 더 심혈을 기울여 디버깅을 실시하게 되었다.

이건 꽤 특이한 현상이었다. <날개셋> 편집기에서도 leak이 발생했기 때문에 가장 먼저 'TSF A급 지원' 옵션을 꺼 봤다. 그리고 외부 모듈은 아예 날개셋 커널을 로딩하지 않고 아무 기능도 사용할 수 없는 panic 상태로 구동했다. 그렇게 프로그램의 주요 기능들을 다 끄고 절름발이로 만들었는데도 <날개셋> 외부 모듈을 한 번이라도 로딩을 하고 나면 leak이 없어지지 않았다.

이런 식으로 COM 오브젝트의 reference count가 꼬이는 버그는 여간 골치 아픈 문제가 아니기에 각오 단단히 하고 디버깅을 계속할 수밖에 없었다. 그 결과 무척 신기한 점을 발견했다. MFC를 사용하는 GUI 프로그램과, MFC든 무엇이든 대화상자(DialogBox)를 사용하는 프로그램에서는 leak이 안 생기는데, Windows API로 message loop을 직접 돌리면서 윈도우를 구동하는 프로그램에서는 memory leak이 발생한다는 것이었다.

오히려 방대하고 복잡한 MFC를 쓰는 프로그램에서 메모리가 새면 샜지, 왜 더 간단한 프로그램에서 문제가 발견되는 걸까?
이 정도까지 밝혀지니 궁금해 미칠 지경이 됐다. leak이 있는 프로그램과 없는 프로그램을 종료할 때 외부 모듈 개체의 Release 함수가 어떻게 호출되고 reference count가 어떻게 변하는지를 검토했다.

그리고 드디어 leak이 있는 프로그램과 없는 프로그램의 차이가 밝혀졌다.
MFC는 프로그램 창이 WM_CLOSE 메시지를 받아서 창의 소멸 단계로 들어서기 전에, 프로그램 창을 강제로 한번 감춰 주고 있었다( ShowWindow(SW_HIDE) ). CFrameWnd::OnClose()에서 CWinApp::HideApplication을 호출함. 이걸 함으로써 운영체제의 TSF 시스템 내부는 객체에 대한 Release가 일어나고 메모리 해제가 완전히 이뤄졌다. 소스가 없는 대화상자도(DialogBox 함수) 잘은 모르지만 종료될 때 비슷한 call stack을 갖는 Release 호출이 있었다.

그 반면 창이 없어질 때 따로 별다른 처리를 하지 않는 프로그램에서는 외부 모듈 개체의 reference count가 1 남게 되었고, 이것이 memory leak으로 이어졌다. MS에서 직접 만든 다른 입력 프로그램들도 마찬가지다. 도대체 왜 그럴까?.

MFC가 WM_CLOSE에서 자기 창을 감추는 이유는 그냥 자식 윈도우들이 순서대로 닫히는 모습이 사용자에게 티가 나 보이지 않게 하고, 겉보기로 창이 당장 없어져 버렸으니 프로그램 종료에 대한 사용자 반응성을 향상시키려는 목적으로 보인다. 그게 반드시 필수는 아니다. 내가 보기에 그렇게 하지 않는 게 잘못이라 볼 수는 없다.

OS별로 살펴보니, 이런 leak은 윈도우 XP와 비스타에서는 없었다가 그 후대인 7과 8에서 생겼다. 즉, XP/Vista에서는 hide를 안 해 줘도 원래 leak이 없는데 7부터는 hide를 해 줘야 한다는 뜻. 아무튼 난 여러 모로 윈7의 문자 입력 체계가 별로 마음에 안 든다. 이쪽 부분 담당자가 갑자기 바뀌었는지, 혹은 대대적인 리팩터링을 한 후유증이기라도 한지 자잘한 버그들이 너무 많이 들어갔기 때문이다.

결국 이것은 IME 문제가 아니라 운영체제 내지 응용 프로그램의 문제라는 결론을 내리고 편집기의 소스를 고쳤다. 문제를 피해 가는 법을 발견하긴 했으나 뒷맛이 개운하지 못하다.

* Windows 환경에서의 4대 디버깅 도구와 테크닉

  • 문자열을 printf 스타일로 포맷하여 OutputDebugString 함수로 전달하는 TRACE 함수 (디버거 로그)
  • 별도의 디버거 로그가 아니라 그냥 화면 desktop DC에다가 로그를 찍는 깜짝 함수
  • 프로그램이 특이한 환경에서 뻗을 때 call stack을 확인할 수 있는 miniDumpWriteDump와 SetUnhandledExceptionFilter 함수
  • memory allocation number에다가 breakpoint를 거는 _crtBreakAlloc 변수. 정체불명의 memory leak 잡을 때 필수

Posted by 사무엘

2013/03/02 19:24 2013/03/02 19:24
, , ,
Response
No Trackback , 4 Comments
RSS :
http://moogi.new21.org/tc/rss/response/802

Trackback URL : http://moogi.new21.org/tc/trackback/802

Comments List

  1. wafe 2013/03/03 02:12 # M/D Reply Permalink

    오호... 신기한 일일세. win7도 그렇지만 win8에서는 더더욱 크게 입력기 쪽이 바뀐 것 아닌가 싶은데, 그래서인지 새나루나 날개셋이나 정상적으로 동작하지 않게 되어서 참 아쉽구먼.

    1. 사무엘 2013/03/03 04:44 # M/D Permalink

      Windows 8은 저같은 프로그래머를 상당히 번거롭게 한 변화가 많았지요. =_=
      참고로 <날개셋> 한글 입력기는 지난달에 나온 6.8 버전이 Win8을 Modern UI까지 제대로 지원하기 시작했답니다.

  2. Lyn 2013/03/11 11:32 # M/D Reply Permalink

    전 어차피 UI는 안만지다 보니 무조건 안정적이고 성능만 빠른 걸 찾게되네요 ㅎㅎ

    그런의미에서 8/2012는 대만족

    1. 사무엘 2013/03/11 13:48 # M/D Permalink

      똑같이 프로그래머라 해도 종사하는 분야는 너무 다양하고 많고.. 넓죠.
      저도 비주얼 C++ 2012의 IDE와 코딩 편의 기능은 대만족이에요. 몇몇 다운그레이드된 기능들, 변경된 컴파일 방식 때문에 불편하지만 2010을 완전히 언인스톨은 못 하겠지만, 201x가 2005나 2008은 완전히 대체 가능한 것 같습니다.

      윈8은 지금으로부터 최하 1~2년은 뒤에 노트북이나 데스크톱 컴퓨터를 새로 장만할 때나 가상 기계가 아닌 main으로 쓰게 될까요?
      어쩌면 곧바로 윈9로 갈아타게 될지도?

Leave a comment

마이크로소프트 Windows라는 운영체제는 GUI 요소인 '창'(window)에서 모티브를 따서 작명되었다. 그 이름이 암시하듯, Windows는 창을 만들고 제어하는 것이 프로그래밍에서 큰 비중을 차지하며, 창과 창끼리의 의사소통은 메시지라는 놈을 통해서 행해진다. 이건 프로그래머라면 이미 다 잘 아는 내용일 것이다.

메시지는 굳이 GUI를 만들지 않더라도 응용 프로그램간에 데이터를 공유하고 스레드 동기화가 갖춰진 통신을 하는 데 상당히 유용한 수단이다. 오늘날 같은 보호 모드 멀티태스킹/멀티스레드 환경에서도 과거의 16비트 시절 같은 직관적인 통신 메커니즘을 제공하기 위해 운영체제가 밑에서 알아서 신경 써 주는 게 많기 때문이다. 그래서 그 기능만 쓰라고 message-only 윈도우라는 것도 있다.

메시지는 자신이 어떤 메시지인지를 나타내는 정수와, 덧붙일 수 있는 추가 숫자 정보 두 종류로 구성된다. 일명 wParam, lParam인데, 16비트 시절에는 메시지, wParam, lParam의 크기가 각각 16, 16, 32비트였다. 그것이 32비트 기계에서는 모두 32비트 크기로 확장되었고, 64비트에 와서는 msg만 그대로이고 나머지 둘은 64비트로 더 커졌다.

이론적으로 아무 숫자나 담아서 메시지로 전달할 수 있다. 그러나 운영체제는 내부적으로 다음과 같은 방식으로 메시지의 용도를 정해 놓고 있다. 이는 마치 운영체제가 메모리 주소의 용도를 영역별로 나눠서 정해 놓은 것과 동일한 맥락이다. (MS-DOS 호환용, 응용 프로그램용, 커널용 등)

첫째, 0부터 WM_USER-1까지 총 1024개의 메시지는 시스템 메시지로서 그 의미가 예약되어 있다.
0인 WM_NULL은 의도적으로 아무 일도 하지 않는 메시지로 비워 놨지만, 그 뒤부터 WM_CREATE(1), WM_DESTROY(2) 같은 것은 아마 윈도우 1.0 시절부터 있었을 기초 메시지들이다..

글자 입력란에는 cursor라고 하여, 공식적으로는 caret이라고 불리는 반전 사각형이 깜빡거린다. 이건 WM_TIMER로 구현했을 법도 해 보이는데 Spy++ 같은 프로그램으로 확인해 보면 그렇지 않다. 메시지 코드는 0x118이고 winuser.h에 WM_* 형태로 문서화되지 않은 비공개 내부 메시지에 따라 동작한다. 신기하지 않은가? (그 주변의 0x117이나 0x119대엔 당연히 공개된 WM_*메시지들이 꽉 차 있음.) 게다가 의미가 뭔지는 모르겠지만 wParam과 lParam에도 그냥 0이 아니라 뭔가 메모리 주소처럼 보이는 값들이 있다.

사용자는 0x1000 이내의 영역에 있는 숫자에다가 나만의 의미를 부여해서는 안 된다. 지금은 쓰이지 않아도 나중에 운영체제가 찜할 가능성이 있다. 가령, 마우스 휠의 움직임을 감지하는 WM_MOUSEWHEEL은 윈도우 98에서 정식으로 새로 추가되었고, 터치스크린 입력을 감지하는 WM_TOUCH 같은 메시지는 윈도우 7에서 추가되었다.

이런 식으로 Windows가 버전업되면서, 메시지가 미래에 자꾸 추가될 수 있다. 개인적으로 최소한 4096개도 아니고 1024는 공간이 너무 부족하지 않나 하는 생각도 든다. 나중에는 이 공간이 메시지들로 다 차 버리고, 추가 메시지는 WM_EXTEND_MSG 같은 최후의 메시지 하에서 부가 정보는 wParam과 lParam에 담겨 오게 되지 않을까? =_=;;

운영체제 메시지 중에는 WM_SETTEXT, WM_GETTEXT이라든가 심지어 WM_COPYDATA처럼 포인터를 통한 데이터 전달이 필요한 것도 있다. 운영체제의 SendMessage 함수는 그런 메시지를 다른 프로세스에다가 보내라고 사용자가 요청할 경우, 자체적으로 공유 메모리를 생성하여 메모리 주소 변환을 하고, 텍스트의 경우 심지어 ANSI/유니코드 변환까지 자동으로 한다. 그러니, lParam을 포인터로 인식하는 시스템 메시지에다가 엉뚱한 숫자를 집어넣어서 보냈다간 큰일난다. 아울러 포인터를 전달해야 하는 메시지는 SendMessage로만 전달 가능하지, PostMessage로는 되지 않게 운영체제가 막는다.

또한 일부 메시지는 반드시 특정 방법만 이용하여 생성해야 하는 것도 있다. 가령, WM_PAINT는 invalidate region을 만드는 함수를 호출해서 운영체제가 생성하도록 해야 하지, 응용 프로그램이 메시지 자체만을 인위적으로 만들어 내서는 안 된다. 실제로 실험을 해 보지는 않았지만, 없는 WM_PAINT를 페이크로 사칭하여 생성하는 것은 운영체제가 아마 안전을 위해 금지하지 않을까 싶다.

요컨대 WM_USER 이내의 메시지는 용도가 운영체제에 의해 예정되고 그에 따른 특수 처리가 추가될 여지가 있는 영역이므로, 사용자가 사칭하거나 조작해서는 안 된다.

그 다음 둘째 계층은 WM_USER부터 WM_APP까지 3만여 개 남짓한 영역이다.
이 메시지는 각 윈도우들이 자체적으로 의미와 용도를 마음대로 정해서 쓸 수 있다. 즉, 윈도우 클래스(RegisterClass)별로 의미가 완전히 private하다.

내가 뭔가 새로운 커스텀 컨트롤을 개발해서 이 컨트롤을 조작하는 수단을 윈도우 메시지라는 형태로 제공하고 싶다면, 각종 커스텀 메시지들을 (WM_USER + xxx)의 형태로 정의하면 된다.
임의의 크기의 데이터를 다른 프로세스끼리 전달하려면 프로그래머가 알아서 주소 marshalling를 하든가, WM_COPYDATA로 주고받을 구조체 스펙을 정하든지, 아니면 짤막한 문자열만 잠시 주고받으려면 atom에다 등록하여 atom 번호만 주고받든지 해야 한다. 뭐, atom은 오늘날에 와서는 거의 구닥다리 메커니즘으로 전락하긴 했지만.

리스트 박스나 콤보 박스는 Windows 1.0 시절부터 있었던 워낙 붙박이이다 보니 LB_ADDSTRING이나 CB_GETCURSEL 같은 메시지는 놀랍게도 앞의 시스템 메시지 영역에 들어있다. 그러니 그 메시지는 값만 보고도 대상 윈도우가 뭔지 볼 필요도 없이 문맥 독립적으로 용도를 추측할 수 있다. 대상 윈도우가 무엇이든 간에 LB_ADDSTRING의 lParam에는 언제나 포인터가 들어있다고 가정할 수 있다.

그러나 사용자 메시지부터는 얘기가 달라진다. WM_USER+1이라는 값을 갖는 메시지는 어느 윈도우가 받느냐에 따라서 처리가 완전히 달라진다. 붙박이 시스템 컨트롤 말고, 32비트 시절에 나중에 도입된 공용 컨트롤도 이제는 아이템을 추가하고 삭제하는 등의 자신의 메시지들은 시스템 영역에 있지 않고 이 사용자 영역에 있다.

따라서 메시지가 하는 일에 따라 부가정보를 변조하는 hook 같은 걸 만든다면, 메시지의 값만 볼 게 아니라 그 메시지를 받는 대상 윈도우의 클래스 이름도 확인해야 한다. 이건 철저하게 문맥 의존적인 메시지인 셈이다.

운영체제(시스템) 메시지, 그리고 사용자 메시지 이렇게 둘이 갖춰지면 끝인 것 같은데 플랫폼 SDK를 보니 셋째 계층인 WM_APP라는 것도 있다. 이건 도대체 뭘까?
이것은 내부적인 처리 방식의 차이에 따른 구분이 아니라 그냥 용도에 따른 명분상의 구분이다.

결론부터 말하자면 이 계층은 응용 프로그램이 어떤 컨트롤에다 서브클래싱을 한 뒤, 응용 프로그램이 새로운 윈도우 프로시저에다 보내 주는 '반사'(reflect) 메시지를 여타 메시지들과 구분하기 위해 존재하는 영역이다. 에디트 컨트롤을 예로 들면, 글자색과 배경색을 바꾼다거나 25자리 제품 시리얼 번호를 입력받는데 5자리마다 '-'를 자동으로 추가하는 것 같은 자잘한 동작 방식을 변경하고 싶을 때 서브클래싱을 이용한다.

일반적으로 컨트롤은 어떤 일이 일어났다는 통지를 부모 윈도우에다 WM_COMMAND(붙박이 컨트롤)나 WM_NOTIFY(공용 컨트롤)의 형태로 보내 주는데, 그때 해야 하는 처리가 천편일률적으로 정해져 있기 때문에 부모 윈도우가 아니라 해당 컨트롤의 서브클래스 프로시저 자신이 도로 받아서 알아서 하게 하고 싶을 때가 있다.

이때 그 통지 메시지는 WM_APP 이후의 영역으로 더해서 보내고, 그 메시지에 대한 처리를 내 custom 윈도우 프로시저에다 넣으면 된다. 이 영역의 메시지는 WM_USER 영역의 메시지, 즉 기존 컨트롤의 메시지와 겹치지 않는다는 보장이 있기 때문이다.

요컨대 시스템 메시지는 그냥 닥치고 global, WM_USER 메시지가 RegisterClass에 종속이라면, WM_APP 메시지는 CreateWindow 종속이라고 생각하면 된다. WM_USER급 메시지의 경우, 해당 윈도우 클래스가 CS_GLOBAL 스타일이 있다면 그 윈도우를 사용하는 모든 프로그램들에서 global 종속이 보장될 것이다.

다음 넷째 계층은 RegisterWindowMessage 함수를 통해 등록된 custom 메시지들에 배당된다.
운영체제 전체를 통틀어서 uniqueness가 보장되는 나만의 고유 메시지를 만들고 싶으면 아무래도 숫자만으로는 무리가 있다. Windows 메시지가 무슨 방대한 128비트짜리 GUID급도 아니니 말이다. 그래서 문자열로부터 0xC000 ~ 0xFFFF 영역에 있는 숫자를 메시지 값으로 얻어 낸다. 아마 hash 연산 같은 걸 쓰겠지.

단, 같은 문자열을 등록하더라도 돌아오는 숫자는 그때 그때 다르다. 그렇기 때문에 RegisterWindowMessage의 리턴값은 프로그램의 컴파일 시점 때 하드코딩으로 박을 수 없다. C++ 언어로 치면 switch문으로 판단을 할 수 없으며 번거롭지만 if를 써야 한다. 하지만 한번 등록된 값은 운영체제가 부팅되어 있는 한 불변이므로, 전역변수의 초기값으로 지정하는 것 정도는 가능하다.

이 custom 메시지는 상당히 유용하다.
시스템 전체에다 메시지 hook을 걸어서 나만의 처리를 하는 프로그램을 만들었다고 치자. 그리고 hook을 건 응용 프로그램과 여타 프로세스의 주소 공간에 침투한 hook 프로시저 사이에 통신을 해야 하는데 이때 가장 효과적으로 쓰일 수 있는 수단이 바로 custom 메시지이다. 내가 만든 프로그램이니 나만 아는 문자열로 custom 메시지를 생성하고, 그걸로 EXE와 hook DLL이 통신을 하면 된다는 뜻이다.

뭐, EXE로 보낼 때야 그냥 WM_USER나 WM_APP급의 고정된 상수만으로 충분하겠지만, 다른 수많은 임의의 프로세스들을 상대하는 훅 DLL로 보내는 건 여타 메시지들과 전혀 충돌하지 않는 게 보장되는 고유 메시지를 써야 할 테니 말이다.

윈도우 95/NT4 초창기 시절에 WM_MOUSEWHEEL 메시지가 운영체제 차원에서 없었던 시절엔, 마우스 휠을 인식하는 드라이버 내지 추가 프로그램을 실행한 뒤, 휠이 굴렀다는 메시지 값을 RegisterWindowMessage(MSWHEEL_ROLLMSG)로부터 얻게 하던 시절이 있었다. 이 문자열의 값은 다음과 같았다.
#define MSWHEEL_ROLLMSG  _T("MSWHEEL_ROLLMSG")

그리고 오늘날 custom 메시지가 쓰이는 또 다른 대표적인 분야는 시스템 트레이라고 불리는 notification area이다. 트레이에다가 자기 아이콘을 추가하는 프로그램들은 _T("TaskbarCreated")라는 메시지를 받았을 때 아이콘을 다시 등록해 줘야 한다.

운영체제의 셸은 자기가 갖고 있던 아이콘들을 자체 보관하지 않는다. explorer 프로세스가 에러가 나서 뻗었거나 강제 종료되었다가 다시 실행되었다면, 아이콘들이 싹 다 날아가게 된다. 이때 셸은 모든 프로그램들을 대상으로 저 메시지를 보내서, 프로그램들로 하여금 알아서 트레이에다 아이콘을 다시 등록하게 한다. 마치 WM_PAINT 메시지를 받았을 때 창이 알아서 자기 내용을 다시 그려야 하듯이 말이다.

저건 너무 유명한 메시지가 되어 버렸기 때문에 장기적으로는 WM_TASKBAR_CREATED 같은 시스템 메시지로 승격이라도 돼야 하지 않나 싶다. 그리고 응용 프로그램들이 늘어날수록 이런 custom 메시지의 공간도 부족해지지는 않으려나 우려가 된다. 16000여 개만으로 충분하겠지? custom 클립보드 포맷이라든가 스레드별로 할당되는 TLS 슬롯의 개수와 비슷한 맥락으로 공간의 한계가 존재하는 영역이라고 볼 수 있다.

Objective C는 언어 차원에서 생으로 문자열 메시지를 객체들 사이에 주고받는 걸 지원한다. C++ 일반 멤버 함수를 호출하는 것보다 오버헤드는 당연히 훨씬 더 크지만, 함수 프로토타입이 하나 바뀌었다고 프로그램 모듈간의 바이너리 호환성이 박살 난다거나, 재컴파일을 해야 하는 그런 불편함은 없다. 내가 옵C를 잘은 모르지만 Windows의 custom 메시지를 보니 문득 옵C 생각도 났다.

이렇게 윈도우 메시지의 계층 4개를 모두 살펴보았다. 시스템 메시지만 1024개로 영역이 매우 좁아서 WM_USER의 영역이 넓은 편인 반면, 나머지 계층은 16비트 정수에서 1/4에 해당하는 16384개를 사이좋게 나눠 쓰고 있다.
그리고 메시지를 담는 공간 자체는 진작부터 32비트로 커졌지만, Windows는 16비트 크기의 범위를 벗어나는 영역은 여전히 예약만 해 놓고 쓰지 않고 있다.

허나, 개인적인 생각은 이들 중에서 그래도 custom(registered) 메시지가 16비트 이상의 범위로 확장되거나 이동하기 가장 용이한 영역이 아닌가 싶다.
일단 얘는 upper bound가 없는 가장 마지막 계층인 데다, MSG 구조체를 포함해서 메시지 값을 담는 모든 자료형이 32비트 UINT로 이미 다 확장되어 있고, custom 메시지는 언제나 함수가 되돌리는 변수값으로 활용하지 하드코딩이 없으니, 확장에 가장 유동적으로 대처 가능하기 때문이다.

Posted by 사무엘

2013/02/25 08:39 2013/02/25 08:39
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/800

Trackback URL : http://moogi.new21.org/tc/trackback/800

Leave a comment

※ 윈도우 프로그래머라면 누구나 다 알 만한 내용에 대한 정리이다.
보면 아시겠지만 1~5까지 등장하는 기술들은 서로 동등한 차원의 관계에 있는 것들이 아니다.

1. 윈 API

kernel32, gdi32, user32를 주축으로 운영체제가 응용 프로그램에다 자신의 기능을 제공하는 가장 원초적인 매체이다. 우리에게 친근한 CreateWindowEx, DispatchMessage, CreateFile 등등등! 20년에 달하는 역사를 자랑하며, Windows라는 운영체제와 PC 데스크톱 애플리케이션이라는 영역 자체가 존속하는 한 결코 없어지지 않는다. 과거의 도스 API는 그냥 인터럽트 호출을 그대로 노출하던 반면, 윈도우 API는 C언어 함수 호출 형태를 근간으로 만들어져 있다.

2. MFC

윈 API만 쓰면 생산성이 크게 떨어지고 불편한 관계로, 1990년대 초에 응용 프로그램의 주 개발 언어가 C에서 C++로 넘어가던 시기에 기존 API를 C++ 라이브러리 형태로 적당히 wrapping하기 위해 이 물건이 개발되었다.
생성자와 소멸자, 오버로딩과 상속, message map 같은 것들 덕분에 생API보다야 개발 생산성이 크게 향상되는 건 사실이나, 이걸 제대로 쓰려면 윈 API도 알아야 되고 객체지향 이념과 MFC가 새로 도입된 개념까지 다 알아야 하기 때문에 초기 학습자의 부담이 커진다. 또한 MFC 자체가 부과하는 오버헤드도 만만찮다.

MS C 7.0의 다음 버전인 비주얼 C++ 1.0때부터 application frameworks라는 이름으로 존재하고 있었다. 16비트 시절부터 존재했으니 역사가 제법 길다.

3. COM

함수 호출 규약, 메모리 할당과 해제 방식, 문자열의 처리 방식, 특정 기능이 담겨 있는 객체를 식별하고 외부에 노출하는 방식 같은 아주 기본적인 바이너리 수준에서의 소프트웨어 컴포넌트 제조 규격을 범언어적으로 통일하는 스펙이다. 가령, 윈API가 DLL 로딩을 위해 전통적으로 지저분한 LoadLibrary(파일명), GetProcAddress나 import library 같은 저수준 방법을 썼다면, COM의 사고방식으로는 CoCreateInstance와 깔끔한 class ID만으로 끝인 것이다.

이건 1990년대 중반의 32비트 윈도우 이래로 도입되었다. 지금은 옛날보다야 중요도가 크게 떨어진 게 사실이지만 DirectX, 탐색기 셸, 드래그 드롭 같은 일부 분야의 API는 이 COM 방식으로 제공되기 때문에 프로그래머아면 COM의 개발 취지와 기본 개념 정도는 알 필요가 있다. 한편, MFC도 이런 COM 규격을 만족하는 컴포넌트를 새로 구현하는 데 쓰이는 공통 필수 기능을 지원한다.

4. GDI+

클래식 윈 API 중에서 GDI 계층을 계승하는 그래픽 라이브러리로, MS가 제공하는 API로는 드물게 C와 더불어 순수 C++ 기반으로 만들어졌다. 또한 사용하는 자료형이나 명칭들이 윈 API와는 완전히 다르며 서로 관련이 없다는 특징이 있다. 비록 GDI+는 기존 GDI보다 느리고 오버헤드가 크지만, 알파 블렌딩, 그러데이션 같은 최신 그래픽 카드를 활용하는 고급 그래픽 기능에 더욱 특화되어 있으며, 일부 그리기 기능은 반드시 GDI+만 써야 가능한 것도 있다.

가령, 안티앨리어싱이 적용된 글자를 찍는 건 재래식 GDI로도 가능하지만 안티앨리어싱이 적용된 선을 그리는 건 GDI+를 써야만 가능하다. 그리고 윈도우 비스타/7의 glass 영역에다가 알파 채널이 적용된 그림/글자를 제대로 그리는 것도 역시 GDI+로만 가능하다.

5. .NET

기계어가 아닌 바이트코드 가상 기계(common language runtime)를 기반으로 하면서, 운영체제 API를 객체지향 위주로 완전히 새로 설계한 윈도우 프로그래밍 플랫폼이다. 예전에는 비주얼 베이직이 얼추 이런 개발 환경을 지향하고 있었지만 닷넷은 그보다 스케일이 범언어적으로 훨씬 더 커졌다. .NET 환경에서의 주력 개발 언어인 C#은 최신 언어답게 디자인이 깔끔하고 빌드 생산성이 우수하다. 하지만 네이티브 기계어 프로그램만치 빠르거나 운영체제 내부를 세밀하게 지어하지는 못하며, 닷넷 프레임워크 위에서만 돌아갈 수 있다는 한계도 있다.

.NET에서는 기본 그래픽 API가 GDI+이다. 둘 다 윈도우 XP부터는 기본 내장이고, 윈도우 98부터 2000/ME까지는 운영체제에 배포판을 추가 설치해서 쓸 수는 있다. 다만, 윈95는 지원을 끊었다.
윈도우 8에서는 닷넷조차도 다른 언어와 플랫폼으로 대체되었는지 WinRT라는 플랫폼이 등장하며, C++ 언어도 C++/CX라고 대대적으로 칼질이 가해졌다. 이게 앞으로 6번으로 추가되어야 할 듯하다.

맥 OS는 운영체제의 API가 저런 식의 내력을 거친 게 있으려나 궁금하다. 코코아, 카본 같은 건 어느 위상에 속할까?

Posted by 사무엘

2013/01/03 08:38 2013/01/03 08:38
, , ,
Response
No Trackback , 4 Comments
RSS :
http://moogi.new21.org/tc/rss/response/778

Trackback URL : http://moogi.new21.org/tc/trackback/778

Comments List

  1. 김재주 2013/01/03 14:18 # M/D Reply Permalink

    WinRT가 ARM용 윈도우 플랫폼을 말하는 것 아닌가요?

    1. 사무엘 2013/01/03 15:33 # M/D Permalink

      어.. 그러니까 저 문맥에서 WinRT란, 윈8에서 Metro용 프로그램을 만드는 데 쓰이는 새로운 API/프레임웍 체계를 가리키는 개념입니다.
      재주 님이 말씀하시는 게 뭔지는 저도 알아요. 그건 단품으로 소비자에게 팔지 않고 ARM 기반 기기에 OEM 내장용으로만 공급되는 특화된 Windows 8 에디션이죠.

  2. Lyn 2013/01/07 20:15 # M/D Reply Permalink

    GDI+ 이후 Direct2D가 새 그래픽 API로 추가되었습니다.

    윈도 7 이후 GDI/GDI+는 Direct2D 에뮬레이션으로 작동합니다.

    1. 사무엘 2013/01/08 00:11 # M/D Permalink

      네, 그렇습니다. GDI뿐만 아니라 GDI+까지 그렇게 완전 legacy가 됐다는 걸 생각하면 격세지감을 느낍니다.
      다만 Direct2D API도 그 매개체는 COM 기반 인터페이스 아니면 재래식 C언어 함수 형태를 벗어나지는 않죠. 순수 C++ 기반 인터페이스를 제공하는 건 GDI+가 역사적으로 이례적인 경우인 것 같네요.

Leave a comment

1. 애니메이션 컨트롤

윈도우 운영체제가 제공하는 GUI용 컨트롤 중에는 애니메이션 컨트롤이라는 게 있다. 이것은 명령 버튼이나 에디트 컨트롤, 리스트 및 콤보 박스처럼 윈도우 1.x 시절부터 있었던 완전 native는 아니고, 1990년대 중반에 운영체제가 32비트로 갈아 타던 95/NT 3.5 시기에 도입된 '공용 컨트롤'에 속한다.

이 공용 컨트롤 덕분에 도구모음줄, 트리 구조, 진행 상황(progress) 표시, 슬라이더처럼 과거에는 '싸제'로 자체 구현을 해야 했던 고급 GUI가 상당수 운영체제 '보급품'만으로 바로 구현이 가능해졌다고 본인은 예전 글에서 설명을 한 적이 있다. 예전 글은 공용 컨트롤을 소개하면서 유독 애니메이션 컨트롤만 설명을 거의 빼먹고 넘어갔던 듯하다.

애니메이션 컨트롤은 컴퓨터가 무슨 작업을 하고 있을 때, 작업 중임을 간단한 '움짤'을 통해 사용자에게 시각적으로 피드백을 주는 역할을 한다. 즉, progress 컨트롤과 같이 쓰이는 경우가 많으며, 그 작업이 굉장히 길거나 언제 끝날지 예측할 수 없는 상황일 때 애니메이션이 더욱 유용해진다.

게다가 애니메이션은 단순한 눈요기 이상으로 컴퓨터가 지금 내부적으로 하는 작업이 무슨 의미를 지니는지를 사용자에게 상징적으로 일깨워 주는 효과도 있다! 탐색기에서 파일을 복사 중일 때 종이가 이쪽 서류가방에서 저쪽 서류가방으로 날아가는 것, 삭제 중일 때 종이가 날아가면서 인수분해-_-되는 모습, 다운로드 중일 때 지구본에서 사용자의 컴퓨터로 종이가 날아가는 모습 등이 좋은 예이다.

그런데, 의문이 생긴다. 이 애니메이션 컨트롤은 어떤 형식의 파일을 사용할까?
오늘날이야 '움짤' 하면 인터넷에서 움짤계의 양대 산맥을 구성하고 있는 플래시나 최소한 애니메이션 GIF를 응당 떠올릴 것이다.
하지만 이 공용 컨트롤은 뜻밖에도 그런 것들을 전혀 지원하지 않으며, 그 대신 MS가 WAV와 더불어 전통적으로 좋아해 온 멀티미디어 포맷인 AVI 파일만을 받아들인다. color key 기반으로 투명색 처리까지 지원한다.

물론 AVI 자체는 컨테이너 포맷일 뿐이기 때문에 아무 방식으로나 압축된 AVI를 다 받아들이는 건 물론 아니며, run-length (RLE) 방식이라는 아주 단순하고 원시적인 알고리즘으로 압축된 극소수 AVI만을 지원한다. 이미지 파일 포맷에다 비유하자면,
디코딩 난이도는 GIF에도 못 미치고 지금은 역사 속으로 사라진 PCX급밖에 되지 않을 것이다. 게다가 소리 재생은 전혀 지원하지 않는다.

이 컨트롤은 왜 이렇게 허접하게 설계된 걸까? 첫째는 이것이 전문적인 동영상 재생을 목적으로 만들어진 게 아니며, 둘째는 이것이 처음 개발되던 시절에는 그것조차도 당대의 컴퓨터에서 돌리기에는 너무 무거웠기 때문이다.

1994~95년이면 모자이크나 넷스케이프 같은 WWW 기반 그래픽 웹브라우저가 이제 막 만들어졌던 시절이고, 386~486급 컴퓨터로는 JPG는커녕 GIF 디코더를 돌리는 것도 다소 부담스러웠었다. 또한 그림판조차 BMP와 PCX 이외의 파일 포맷은 읽고 쓰는 걸 지원하지 않았었는데 GIF를 운영체제의 공용 컨트롤이 지원할 거라고는 전혀 기대할 수 없을 것이다.

플래시야 얼마나 덩치가 크고 무거운지 더 설명이 필요하지 않을 것이다. 또한, 플래시 무비를 재생하기 위해서는 굳이 애니메이션 컨트롤을 쓸 필요 없이 플래시 ActiveX 자체를 삽입하면 끝이다. 하지만 간단한 애니메이션 재생용으로는 GIF가 굉장히 대중화되어 있는 만큼, 애니메이션 컨트롤이 GIF 정도는 지원해 주면 좋을 것 같다. 오늘날의 컴퓨터 환경에서는 GIF가 그렇게 무거운 포맷이 전혀 아니니 말이다.

2. 세월이 흐르면서 사라지는 컴포넌트들

과거 윈도우 3.x 시절에는 SysEdit.exe라는 프로그램이 있었다. 도스의 환경 구성 파일인 autoexec.bat와 config.sys, 그리고 과거 16비트 시절의 윈도우 운영체제의 구성 파일인 win.ini와 system.ini 파일을 한데 열어서 편집할 수 있고, 사실 그 네 파일만 편집할 수 있는 특이한 형태의 MDI 에디터이다.

윈도우 95 이후로 그 네 파일들은 잉여가 되었으며, 자연히 그런 에디터도 전혀 필요가 없어졌다. 그럼에도 불구하고 그 SysEdit 프로그램은 놀랍게도 무려 최신 윈도우 7에서도 윈도우 시스템 디렉터리에 건재하다!
물론 32비트 에디션 한정으로 말이다. 64비트 에디션에는 그런 거 없다.

32비트용으로 포팅되었다거나 한 것도 아니고, 윈도우 3.1 시절의 16비트 EXE 형태 그대로 무슨 이유인지는 모르겠지만 아직 까지 남아 있다. 다음 버전인 윈도우 8에까지 남아 있을지는 장담 못 하겠다. 물론 이제는 32비트 에디션 자체도 점점 보기 힘들어지고 있긴 하지만 말이다.

윈도우 7의 시스템 디렉터리에 남아 있는 SysEdit 프로그램 파일을 보면, 마치 서울 지하철 1호선 신설동 역에 의도적으로 남겨 두고 있는 1974년 개통 당시의 “차 타는 곳” 표지판을 보는 듯한 느낌이다. 40여 년 전 특유의 꾸질꾸질한 타이포그래피로 만들어진 그 표지판 말이다.

비스타 이전 버전의 운영체제들은 SysEdit 말고도 윈도우 3.x 시절의 프로그램 관리자(ProgMan), 파일 관리자 같은 16비트 프로그램을 더 내장하고 있기도 했다. 이들 프로그램이 존재하는 걸로 가정하고 동작하는 여타 프로그램과의 호환성을 유지하기 위해서이다.

특히 '프로그램 관리자'는 윈도우 XP 때까지 남아 있다가 비스타에 와서야 제외되었는데, ProgMan.exe가 예전부터 내장하고 있는 아이콘을 추출해서 쓰는 프로그램들이 있었기 때문에 '아이콘 셔틀'용으로라도 이게 필요했기 때문이다.

그러다가 비스타부터는 아예 16비트 EXE/DLL로부터 리소스를 추출하는 기능 자체가 보안상의 이유로 삭제되었기 때문에 ProgMan도 퇴출되었다. 이제 16비트 모듈을 취급하는 프로그램은 거의 없어진 반면, 그 옛날(legacy) 코드를 오늘날의 강화된 보안 기준을 만족할 정도로 리팩터링해야 할 명분이 서지 않기 때문이다. 비용이 수지가 안 맞는다.
그래서 윈도우 7에까지 존속하는 최후의 16비트 프로그램은 이제 SysEdit밖에 안 남았다.

참고로, Control(제어판), Write(문서 작성기/워드패드) 같은 프로그램은 파일 이름만 똑같고 오늘날 운영체제에서 동일한 기능에 해당하는 프로그램을 구동만 해 주는 redirection용 32/64비트 껍데기일 뿐이다. 16비트 프로그램이 아니며 리소스 셔틀하 고도 관계가 없으므로 주의할 필요가 있다.

리소스 셔틀은 그렇게 생소한 테크닉이 아니다. 한 가지 예를 들면, 내장 마우스 포인터가 있다. 평상시의 화살표 모양, 텍스트 입력란을 지날 때의 I자 모양, 작업 중일 때의 모래시계 모양처럼, 운영체제는 상황별로 응용 프로그램이 지정할 수 있는 다양한 마우스 포인터들을 제공한다.

그런 것 중 하나로, 지금은 웹브라우저가 대중화하면서 링크를 가리키는 손가락 모양 포인터도 운영체제가 내장하고 있다. 그러나 윈도우 95는 아직 인터넷 시대가 되기 전에 출시되었기 때문에 그걸 아직 지원하지 않았었다.

그래서 윈도우 95에서는 손가락 모양 포인터를 표시하기 위해 WinHlp32.exe, 다시 말해 도움말 프로그램에 내장되어 있는 리소스로부터 손가락 모양 포인터를 가져와서 표시하게 하는 로직이 흔히 쓰였다. 웹브라우저가 없던 시절에도 윈도우 운영체제의 도움말은 하이퍼링크가 있었고, 프로그램이 자체적으로 그런 링크용 포인터를 내장하고 있었기 때문이다.

그런데 지금은 운영체제의 도움말 시스템이 바뀌어서 WinHlp32.exe 자체가 흑역사로 전락했으니, 이것도 참 격세지감이 아닐 수 없다.

Posted by 사무엘

2012/11/17 08:43 2012/11/17 08:43
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/757

Trackback URL : http://moogi.new21.org/tc/trackback/757

Leave a comment

문자의 집합인 문자열(string)은 어지간한 프로그래밍 언어들이 기본으로 제공해 주는 기본 중의 기본 자료형이지만, 그저 기초라고만 치부하기에는 처리하는 데 내부적으로 손이 많이 가기도 하는 자료형이다.

문자열은 그 특성상 배열 같은 복합(compound) 자료형의 성격이 다분하며, 별도의 가변적인 동적 메모리 관리가 필요하다. 또한 문자열을 어떤 형태로 메모리에 저장할지, 복사와 대입은 어떤 형태로 할지(값 내지 참조?) 같은 전략도 구현체에 따라서 의외로 다양하게 존재할 수 있다.

그래서 C 언어는 컴퓨터 자원이 열악하고 가난하던 어셈블리 시절의 최적화 덕후의 정신을 이어받아, 언어 차원에서 따로 문자열 타입을 제공하지 않았다. 그 대신 충분히 크게 잡은 문자의 배열과 이를 가리키는 포인터를 문자열로 간주했다. 그리고 코드값이 0인 문자가 문자열의 끝을 나타내게 했다.

그 이름도 유명한 null-terminated string이 여기서 유래되었다. 오늘날까지 쓰이는 역사적으로 뿌리가 깊은 운영체제들은 응당 어셈블리나 C 기반이기 때문에, 내부 API에서 다 이런 형태의 문자열을 사용한다.
그리고 파일 시스템도 이런 문자열을 사용한다. 오죽했으면 이를 위해 MAX_PATH (=260)같은 표준 문자열 길이 제약까지 있을 정도이니 말 다 했다. 그렇기 때문에 null-terminated string은 앞으로 결코 없어지지 않을 것이며 무시할 수도 없을 것이다.

딱히 문자열만을 위한 별도의 표식을 사용하지 않고 그저 0 문자를 문자열의 끝으로 간주하게 하는 방식은 매우 간단하고 성능면에서 효율적이다. 지극히 C스러운 발상이다. 그러나 이는 buffer overflow 보안 취약점의 근본 원인을 제공하기도 했다.

또한 이런 문자열은 태생적으로 문자열 자기 내부엔 0문자가 또 들어갈 수 없다는 제약도 있다. 하지만 어차피 사람이 사용하는 표시용 문자열에는 코드 번호가 공백(0x20)보다 작은 제어 문자들이 사실상 쓰이지 않기 때문에 이는 그리 심각한 제약은 아니다. 문자열은 어차피 문자의 배열과는 같지 않은 개념이기 때문이다.

문자열을 기본 자료형으로 제공하는 언어들은 대개 문자열을 포인터 형태로 표현하고, 그 포인터가 가리키는 메모리에는 처음에는 문자열의 길이가 들어있고 다음부터 실제 문자의 배열이 이어지는 형태로 구현했다. 그러니 문자열의 길이를 구하는 요청은 O(1) 상수 시간 만에 곧바로 수행된다. (C의 strlen 함수는 그렇지 않다)

그리고 문자열의 길이는 대개 machine word의 크기와 일치하는 범위이다. 다만, 과거에 파스칼은 이례적으로 문자열의 크기를 16비트도 아닌 겨우 8비트 크기로 저장해서 256자 이상의 문자열을 지정할 수 없다는 이상한 한계가 있었다. 더 긴 문자열을 저장하려면 다른 특수한 별도의 자료형을 써야 했다.

과거에 비주얼 베이직은 16비트 시절의 버전 3까지는 “포인터 → (문자열의 길이, 포인터) → 실제 문자열”로 사실상 실제 문자열에 접근하려면 포인터를 이중으로 참고하는 형태로 문자열을 구현했다. 어쩌면 VB의 전신인 도스용 QuickBasic도 문자열의 내부 구조가 그랬는지 모르겠다.

그러다가 마이크로소프트는 훗날 OLE와 COM이라는 기술 스펙을 제정하면서 문자열을 나타내는 표준 규격까지 제정했는데, COM 기반인 VB 4부터는 문자열의 포맷도 그 방식대로 바꿨다.

일단 기본 문자 단위가 8비트이던 것이 16비트로 확장되었다. 마이크로소프트는 자기네 개발 환경에서 ANSI, wide string, 유니코드 같은 개념을 한데 싸잡아 뒤죽박죽으로 재정의한 것 때문에 문자 코드 개념을 좀 아는 사람들한테서 많이 까이고 있긴 하다. 뭐, 재해석하자면 유니코드 UTF16에 더 가깝게 바뀐 셈이다.

OLE 문자열은 일단 겉보기로는 null-terminated wide string을 가리키는 포인터와 완전히 호환된다. 하지만 그 메모리는 OLE의 표준 메모리 할당 함수로만 할당되고 해제된다. (아마 CoTaskMemAlloc) 그리고 포인터가 가리키는 메모리의 앞에는 문자열의 길이가 32비트 정수 형태로 또 들어있기 때문에 문자열 자체가 또 0문자를 포함하고 있을 수 있다.

그리고 문자열의 진짜 끝부분에는 0문자가 1개가 아니라 2개 들어있다. 윈도우 운영체제는 여러 개의 문자열을 tokenize할 때 double null-termination이라는 희대의 괴상한 개념을 종종 사용하기 때문에, 이 관행과도 호환성을 맞추기 위해서이다.

2중 0문자는 레지스트리의 multi-string 포맷에서도 쓰이고, 또 파일 열기/저장 공용 대화상자가 사용하는 확장자 필터에서도 쓰인다. MFC는 프로그래머의 편의를 위해 '|'(bar)도 받아 주지만, 운영체제에다 전달을 할 때는 그걸 다시 0문자로 바꾼다. ^^;;;

요컨대 이런 OLE 표준 문자열을 가리키는 포인터가 바로 그 이름도 유명한 BSTR이다. 모든 BSTR은 (L)PCWSTR과 호환된다. 그러나 PCWSTR은 스택이든 힙이든 아무 메모리나 가리킬 수 있기 때문에 그게 곧 BSTR이라고 간주할 수는 없다. 관계를 알겠는가? BSTR은 SysAllocString 함수를 통해 생성되고 SysFreeString 함수를 통해 해제된다.

'내 문서', '프로그램 파일' 등 운영체제가 특수한 용도로 예정하여 사용하는 디렉터리를 구하는 함수로 SHGetSpecialFolderPath가 있다. 이 함수는 MAX_PATH만치 확보된 메모리 공간을 가리키는 문자 포인터를 입력으로 받았으며, 특수 폴더들을 CSIDL이라고 불리는 일종의 정수값으로 식별했다.

그러나 윈도우 비스타에서 추가된 SHGetKnownFolderPath는 폴더들을 128비트짜리 GUID로 식별하며, 문자열도 아예 포인터의 포인터 형태로 받는다. 21세기에 도입된 API답게, 이 함수가 그냥 메모리를 따로 할당하여 가변 길이의 문자열을 되돌려 준다는 뜻이다. 260자 제한이 없어진 것은 좋지만, 이 함수가 돌려 준 메모리는 사용자가 따로 CoTaskMemFree로 해제를 해 줘야 한다. SysFreeString이 아님. 메모리만 COM 표준 함수로 할당했을 뿐이지, BSTR이 돌아오는 게 아닌 것도 주목할 만한 점이다.

예전에 FormatMessage 함수도 FORMAT_MESSAGE_ALLOCATE_BUFFER 플래그를 주면 자체적으로 메모리가 할당된 문자열의 포인터를 되돌리게 할 수 있는데, 이놈은 윈도우 NT 3.x 시절부터 있었던 함수이다 보니, 받은 포인터를 LocalFree로 해제하게 되어 있다.

이렇게 운영체제 API 차원에서 메모리를 할당하여 만들어 주는 문자열 말고, 프로그래밍 언어가 제공하는 문자열은 메모리 관리에 대한 센스가 추가되어 있다. 대표적인 예로 MFC 라이브러리의 CString이 있다.

CString 자체는 BSTR과 마찬가지로 언뜻 보기에 PCWSTR 포인터 하나만 멤버로 달랑 갖고 있다. 그래서 심지어 printf 같은 문자열 format 함수에다가 "%s", str처럼 개체를 명시적인 형변환 없이 바로 넘겨 줘도 괜찮다(권장되는 프로그래밍 스타일은 못 되지만).

그런데 그 포인터의 앞에 있는 것이 단순히 문자열 길이 말고도 더 있다. 바로 레퍼런스 카운트와 메모리 할당 크기. 그래서 문자열이 단순 대입이나 복사 생성만 될 경우, 그 개체는 동일한 메모리를 가리키면서 레퍼런스 카운트만 올렸다가, 값이 변경되어야 할 때만 실제 값 복사가 일어난다. 이것을 일명 copy-on-modify 테크닉이라고 하는데, MFC 4.0부터 도입되어 오늘날에 이르고 있다. 이는 상당히 똑똑한 정책이기 때문에 이것만 있어도 별도로 r-value 참조자 대입 최적화가 없어도 될 정도이다.

메모리 할당 크기는 문자열에 대해 덧셈 같은 연산을 수행할 때 메모리 재할당이 필요한지를 판단하기 위해 쓰이는 정보이다. MFC는 표준 C 라이브러리에 의존적이기 때문에 이때는 응당 malloc/free가 쓰인다. 재할당 단위는 보통 예전에 비해 배수 단위로 기하급수적으로 더 커진다.

CString이 그냥 포인터와 크기가 같은 반면, 표준 C++ 라이브러리에 존재하는 string 클래스는 비주얼 C++ 2010 x86 기준 개체 하나의 크기가 28바이트나 된다. 길이가 16 이하인 짧은 문자열은 그냥 자체 배열에다 담고, 그보다 긴 문자열을 담을 때만 메모리를 할당하는 테크닉을 쓰기 때문이다. 그리고 대입이나 복사를 할 때마다 CString 같은 reference counting을 하지 않고, 일일이 메모리 재할당과 값 복사를 한다.

글을 맺겠다.
C/C++이 까이는 여러 이유 중 하나는 라이브러리가 지저분하고 동일 기능의 중복 구현이 너무 많아서 혼란스럽다는 점이다. 문자열도 그 범주에 정확하게 속하는 요소일 것이다. 메모리 할당과 해제 자체부터가 구현체 중복이 한둘이 아니니... 어지간히 덩치와 규모가 있는 프레임워크 라이브러리는 그냥 자신만의 문자열 클래스 구현체를 갖고 있는 게 이상한 일이 아니다. 하지만 그건 C/C++이 쓰기 편리한 고급 언어와 시스템 최적화 오덕질이라는 두 토끼를 모두 잡으려다 어쩔 수 없이 그리 된 것도 강하다.

문자열에 대한 이야기 중에서 일부는 내가 예전 블로그 포스트에서도 한 것도 있지만, 이번 글에 처음으로 언급한 내용도 많을 것이다. 프로그래밍 언어 중에는 문자열을 다루기가 기가 막히게 편리한 것이 있는데, 그런 것도 내부적으로는 다 결국은 컴퓨터가 무진장 고생해서 결과물을 만들어 내는 것이다.
컴퓨터가 받아들이고 뱉어내는 문자열들이 내부적으로 어떤 구현체에 의해 어떤 처리를 거치는지를 생각해 보는 것도 프로그래머로서는 의미 있는 일일 것이다.

Posted by 사무엘

2012/10/13 08:26 2012/10/13 08:26
, , , ,
Response
No Trackback , 8 Comments
RSS :
http://moogi.new21.org/tc/rss/response/743

Trackback URL : http://moogi.new21.org/tc/trackback/743

Comments List

  1. 주의사신 2012/10/13 09:40 # M/D Reply Permalink

    1. 조엘 온 소프트웨어를 읽다 보면 저자가 해 본 프로젝트 중에서 CF***edString이라는 자료형이 있었다고 합니다. 얼마나 만드는 사람이 화가 났으면 이름을 그렇게 지었을까 싶은 문자열 클래스입니다.

    2. C++에 이제는 문자열 자료형을 포함하지 않으면 안 되는 이유가 없을 것 같은데, 어떠한 이유인지 전혀 관련된 논의를 하지 않는 것 같더군요.

    3. std::string의 경우에는 modify-on-copy를 하지 않는데, 이렇게 하는 이유가 Multithread 프로그래밍을 하게 되면, string 관련 연산을 할 때 Lock을 자주 걸어 주어야 해서 성능 저하가 엄청나기 때문에 그랬다고 합니다. (Code Craft라는 책에 그렇게 나와 있었습니다.)

    1. 사무엘 2012/10/13 10:10 # M/D Permalink

      1. 하하하!!! 흠좀무스러운 작명이군요.

      2. string은 C++ 라이브러리에 포함된 것으로도 감지덕지지, 개념적으로 customization의 여지가 너무 많아서 C++ 같은 언어의 기본 자료형으로 들어가는 건 이제 무리일 것 같습니다. 그리고 넣어 봤자 이제는 이미 중복 구현체들이 역할을 대신 수행하고 있고요.
      당장 wchar_t만 해도 int만큼이나 크기가 들쭉날쭉.. Windows에서는 UTF16이 짱입니다만, 유닉스 계열로 가면 아예 1글자당 4바이트인 UTF32도 많이 쓰이지요?

      3. std::string이 그렇게 설계된 것은 스레드 안전성 같은 걸 차지하고라도, 문자열을 뭔가 serious한 객체라기보다는 primitive한 약간 덩치 큰 기본 자료형으로 간주한 사상이 크게 작용해서인 것 같습니다.

      아무튼, 문자열이라는 물건은 프로그래머로서 생각할 주제가 굉장히 많은 주제임은 분명합니다. ^^

  2. kippler 2012/10/13 12:51 # M/D Reply Permalink

    * 이러니 저러니 해도 저는 개인적으로 CString 이 제일 좋더군요. 효율성도 좋고, %s 파라메터로 쓸 수 있는것도 좋고, 편리한 메쏘드도 많이 갖춰져 있고..

    * windows 2000 이전은 UCS2 였고, 2000 부터는 공식적으로 UTF-16이라고 하더군요. 사실 그 당시는 아직 유니코드가 UCS2 만 있고, UCS4 는 없던 시절이라....

    * 그리고 utf16 이 유닉스의 ucs4 보다는 확실히 편한듯 합니다. 어차피 ucs2 를 넘어가는 문자열 다룰일이 많지 않고, 메모리도 절약되니깐요.

    * 고급언어만 쓰던 개발자는 문자열 타입을 추상적으로 생각하는 경우가 많더군요. 그래서 replace 같은 함수가 얼마나 cpu를 많이 쓰는지에 대해서는 생각해 본 적이 없는 경우도 많고요. 그래서 신입사원 뽑을때 문자열 관련 함수를 직접 구현해 보게 시키는것(strlen, strcpy..) 이 c 의 로우레벨이나 포인터에 대한 이해도를 측정하기 좋더군요.

    1. 사무엘 2012/10/13 21:25 # M/D Permalink

      앗, 유명하신 그분께서...! (형님, 반갑습니다.. 이렇게 불러 드려도 되죠? ^^)
      저도 문자열 클래스라는 걸 제일 먼저 접한 게 CString이다 보니, 그 디자인이 가장 무난하고 마음에 드는 것 같습니다. 사실, thread safety라는 게 절대적으로 보장돼야 하는 상황은 그리 많지 않거든요. 멀티스레딩을 하는 상위 소프트웨어 계층에서 동기화를 알아서 해 줘야겠죠. 굳이 포인터 단위의 저수준 조작이 필요하면 GetBuffer 함수를 써서 내부 버퍼를 변형 가능한 형태로 잠시 까 볼 수도 있다는 점도 더욱 좋습니다.

      UCS와 UTF 사이의 관계는 말씀하신 대로이구요. 진짜로 UC2 범위를 넘어가는 문자를 일상생활에서 접할 일은 거의 없는 게 사실이죠. 아무리 메모리가 남아 돈다고 해도, 한 글자당 32비트는 솔직히 정서적으로 기억장소 낭비가 너무 심한 감이 있죠.

      또한, 마치 실수가 정수보다 다루기 힘들듯, 문자열은 단순 문자나 숫자에 비해 컴퓨터가 다루기 힘들어하는 타입이라는 것을 프로그래머라면 알 필요가 있습니다. ^^;;

  3. 김 기윤 2012/10/13 13:13 # M/D Reply Permalink

    옛날부터 단순히 char* 또는 char[] 형 만 계속해서 다루다 보니까
    std::string 이나 CString 같은 것을 쓰다가도 문자열 조작을 하려면
    c_str() 같은 것으로 char* 로 변환 한 뒤 변환하고 다시 원래대로 되돌리는 등의 연산을 하는
    삽질(..)을 하는 저입니다.

    std::string 이나 CString 쪽도 익숙해져야 할텐데, 그냥 버릇이 버릇이다보니 기회를 잡지를 못하고 있네요..;

    1. 사무엘 2012/10/13 21:25 # M/D Permalink

      포인터 덕질을 하다가 string 클래스로 갈아타자니, 내가 원하는 조작을 마음대로 못 하는 게 좀 마음에 걸리기도 하지요.
      저의 <날개셋> 한글 입력기도 자체적인 문자열 클래스 구현체가 있는데요, 주어진 구간의 문자열을 특정 문자열로 대체하는 Substitute 함수가 있고(대체 대상 문자열은 굳이 null-terminate가 아니어도 됨!), Format된 문자열을 기존 문자열의 앞이나 뒤에다 추가하는 FormatBefore, FormatAfter라는 함수를 제 식대로 만들어서 제공하고 있답니다.

  4. Lyn 2012/10/13 15:47 # M/D Reply Permalink

    copy-on-modify 보다는 copy-on-write 가 좀더 일반적으로 쓰이는 명칭일듯 합니다

    1. 사무엘 2012/10/13 21:25 # M/D Permalink

      제가 CString 클래스의 메커니즘을 처음으로 알게 된 곳이 바로 MSDN에서 전설적인 개발자였던 Paul DiLascia의 글인데요, 거기서 처음부터 'modify'라는 용어를 쓰더군요. 저는 그냥 그 영향을 받은 것입니다.

      http://www.microsoft.com/msj/archive/S1F0A.aspx
      First off, if you're not using CStrings, shame on you! Come on guys, the year 2000 is almost upon us!

      “세상에 아직도 CString도 모르다니, 부끄러운 줄 아셈!”... 1996년에 작성된 글입니다. ㅎㅎ

Leave a comment

요즘 코딩 잡설

1.

<날개셋> 한글 입력기의 개발 작업은 단순히 새로운 기능을 구현하거나 알려진 버그를 수정하는 것 말고도, 멀쩡히 동작하는 기능의 내부 구현 형태를 바꾸는 리팩터링도 무시 못 할 비중을 차지하고 있다.

이미 지금도 문제가 없긴 하지만, 열기-닫기 내지 할당-해제 같은 패턴은 어지간하면 클래스의 생성자와 소멸자가 알아서 하게 바꿔서 리소스 누수(leak)를 컴파일러 차원에서 원천적으로 차단하고 있으며,
최근에는 비주얼 C++ 2010으로 갈아탄 덕분에 지저분한 임시 #define 함수들을 지역 변수 형태의 람다 함수로 교체하는 재미가 쏠쏠하다. 예를 들어 이런 것 말이다.

BEFORE
#define PickNumber(e) ((e)[1] ? wcstol((e), &f, 16): *(e))

AFTER
auto PickNumber = [&f](PCWSTR e) -> int { return e[1] ? wcstol(e, &f, 16): *e; };

별도의 함수로 추가하기에는 너무 지엽적이고 한 함수 안에서만 잠깐 쓰고 마는 반복적인 루틴들은 람다로 싸 주는 게 딱이다. type-safety가 보장되고, scope도 엄격하게 정해지고, 이 루틴을 매번 인라인으로 expand할지 아니면 그냥 함수 호출로 처리해서 코드 크기를 줄일지를 컴파일러가 좀 더 유연하게 판단할 수 있기 때문에 아주 좋다.

예전에는 C++에 대해서 C with classes라고 배웠겠지만, 이제는 C++은 C with classes라고만 정의하기에는 설명에 누락된 요소가 너무 많아졌다.
람다 함수를 전역 변수로 선언하는 건 문법적으로 불가능하지는 않으나, 그럴 바에야 그냥 재래식 형태의 함수를 하나 선언하고 말지 아무런 특별한 의미가 없을 것이다.

2.

그런데, 이렇게 리소스 누수를 막기 위해서 노력하고 있지만 구조체에다 함께 넘어온 핸들이나 메모리 포인터는 그것만 따로 클래스의 소멸자가 자동으로 소멸하게 할 수 없으니 구조적으로 여전히 누수 위험이 존재한다.

예를 들어 CreateProcess 함수는 실행 후 해당 프로세스에 대한 핸들을 PROCESS_INFORMATION 구조체에다가 되돌려 준다. 이 값을 이용해서 프로그램은 자신이 새로 실행한 프로그램이 실행이 끝날 때까지 기다린다거나 할 수 있다. 하지만 실행된 프로세스가 종료되더라도 그 프로세스를 가리키던 핸들은 해제되지 않는다. 호스트 프로그램이 핸들을 닫아 줘야만 완전히 해제된다.

CreateProcess 함수를 자주 쓴다면 핸들 해제까지 모든 작업을 자동화해 주는 클래스를 만들어서 쓰는 게 효과적이다. 사실, 이 함수는 받아들이는 인자가 많고 기능 한번 쓰는 게 번거로운 편이기 때문에 클래스를 쓸 법도 하지만, 어쩌다 한 번 쓰고 마는 특수한 함수를 전부 클래스로 감싸는 건 좀 낭비처럼 보이는 게 사실이다.

<날개셋> 편집기에는 있으나마나한 잉여이긴 하지만 명색이 텍스트 에디터이다 보니 인쇄 기능이 있다.
그런데 한때는 인쇄를 한 뒤에 자신이 사용한 프린터 DC를 해제하지 않아서 GDI 개체 누수가 생기는 버그가 있었다.
물론 이건, 리소스 제한이 있는 윈도우 9x에서 이 프로그램을 한 번 실행한 후, 프린터 드라이버를 이용한 인쇄(화면 인쇄 말고) 명령을 연달아 몇백, 몇천 번쯤 내려야(한 번에 몇백, 몇천 페이지를 인쇄하는 것과는 무관) 여파가 나타날 버그이니, 현실적으로는 아무런 위험이 아닌 것이나 마찬가지이다.

이 문제의 원인은 PrintDlg 함수가 PRINTDLG 구조체에다가 넘겨준 hDC 멤버(프린터 DC)를 해제하지 않아서였다.
그런데 이런 실수가 들어갈 법도 했던 게, MSDN에서 해당 함수나 해당 구조체에 대한 설명 어디에도, 사용이 끝난 DC를 처분하는 것에 대해서는 언급이 없었다.
이거 혹시 해제해야 하는 게 아닌지 미심쩍어서 내가 우연히 잉여질 차원에서 다른 예제 코드를 뒤져 본 뒤에야 DeleteDC로 해제를 해야 한다는 걸 알게 되었고, 예전 코드에 리소스 누수 버그가 있음을 깨달았다.

하긴, 내 기억이 맞다면, COM 오브젝트도 프로그래머가 Release를 제대로 안 해서 개체 누수가 하도 많이 생기다 보니 MS에서도 골머리를 썩을 정도였다고 하더라. 현실은 이상대로 되질 않는가 보다.

3.

윈도우 운영체제의 device context에 대해서 보충 설명을 좀 할 필요를 느낀다.
DC라는 건 그림을 그리는 매체가 (1) 화면, (2) 메모리(대부분은 화면에다 내보낼 비트맵을 보관하는 용도), 아니면 (3) 프린터 이렇게 셋으로 나뉜다. 화면용 DC는 GetDC나 GetWindowDC를 통해 HWND 오브젝트로부터 얻어 오고 해제는 ReleaseDC로 한다.

그러나 나머지 두 DC는 화면 DC와는 달리, DeleteDC로 해제한다는 차이가 있다. 화면용 DC는 운영체제가 통합적으로 관리하는 성격이 강한 반면, 나머지는 전적으로 사용자 프로그램의 재량에 달린 비중이 커서 그런 것 같다.

메모리 DC는 화면 같은 다른 물리적인 매체 DC와 연계를 할 목적으로 만들어지는 가상의 DC이므로, 보통 CreateCompatibleDC를 통해 이미 만들어진 DC를 레퍼런스로 삼아서 생성된다. 레퍼런스 DC가 무엇이냐에 따라서 하다못해 pixel format 같은 거라도 차이가 날 수 있기 때문이다.

그 반면 프린터 DC는 대개 가장 수준이 낮은 CreateDC를 통해 만들어지는데, 응용 프로그램이 특정 디바이스를 지목해서 DC를 하드코딩으로 생성하는 경우는 거의 없고 보통은 사용자에게 인쇄 대화상자를 출력한 뒤에 운영체제의 GUI가 생성해 주는 DC를 그대로 사용하면 된다.

사실, 프린터야 인쇄 전과 인쇄 후에 프린터에다 초기화 명령을 내리고 종이를 빼내는 등 여러 전처리· 후처리 작업이 필요하고 그런 저수준 명령은 프린터 하드웨어의 종류에 따라 다 다르다.
메모리는 프린터만치 하드웨어를 많이 가리지는 않겠지만, 그래도 그래픽을 보관하기 위해 메모리를 할당하고 나중에 해제하는 작업이 필요하다.

그에 반해 단순히 화면에다가 그림을 찍는 건 각 context별로 좌표를 전환하고 클리핑 영역 설정을 바꾸는 것 외에는 별다른 오버헤드가 존재하지 않는다. 도스 시절의 그래픽 라이브러리는 그런 DC 같은 추상화 계층 자체가 아예 존재하지도 않았으니 말이다.
그런 오버헤드의 위상이 ReleaseDC와 DeleteDC의 차이를 만든 것 같다.

Posted by 사무엘

2012/09/19 19:32 2012/09/19 19:32
,
Response
No Trackback , 8 Comments
RSS :
http://moogi.new21.org/tc/rss/response/734

Trackback URL : http://moogi.new21.org/tc/trackback/734

Comments List

  1. Lyn 2012/09/19 20:08 # M/D Reply Permalink

    제 요즘 코드는 shared_ptr<T> 떡칠 (...)

  2. 김 기윤 2012/09/20 01:50 # M/D Reply Permalink

    vector<T>::iterator it = ~~~;
    로 되어 있는 것을 auto it = ~~~;
    로 바꾸기만 해도 소스코드가 보기 편해지죠!

  3. 사무엘 2012/09/20 03:00 # M/D Reply Permalink

    다들 C++11 덕을 톡톡히 보고 있군요. ^^
    auto는 실제로 iterator 선언하는 일도 정말 간결하게 만들어 줍니다.

  4. 주의사신 2012/09/20 10:02 # M/D Reply Permalink

    1. 회사에서 지원자의 실력을 보기 위해서 Project를 요구했는데요. 원하는 것을 만들어 보라는 이야기를 하기에, 제가 제일 잘 아는 "Sonic the Hedgehog"를 간단하게 만들어 보고 있는 중입니다.

    롤러 코스터처럼 360도를 회전해야 하는데, 그 부분이 생각보다 많이 어렵네요. 대개 하루 이상 끈 기능이 별로 없었는데, 이번 기능은 좀 오래 걸립니다.


    2. Visual Studio 뭘 써야 하는지에 대해서 면접 시에 질문을 했는데, 회사에서 라이센스 문제로 2008을 쓰고 있으니, 2008을 쓰라고 해서 2008 쓰고 있는 중입니다. STL 쓰는데, 람다가 참 많이 그립습니다.

    슬램덩크라는 만화 한 장면 패러디해서, "면접관님, 람다가 쓰고 싶어요."라는 생각이 들 정도였답니다...


    3. 이제 만들고 있는 프로그램은 다음 주 월요일에 회사 가서 공개하게 됩니다.

    1. 사무엘 2012/09/22 14:26 # M/D Permalink

      1, 3. 예전에 게임기용으로 판매되던 2D 아케이드 게임을 만들어 본다는 뜻인가요? (정확히는 게임 엔진 데모)
      대단하십니다. 데모야 저도 주의사신 님 실력을 익히 아니, 별 문제 있겠습니까.
      졸업 후 진로가 꼭 잘 정해지면 좋겠습니다.

      2. 기업은 아무래도 2010으로 갈아탄 곳이 아직 별로 없을 수도 있어요.

  5. 김재주 2012/09/21 00:02 # M/D Reply Permalink

    auto가 보기도 좋고 쓰기도 편하긴 하지만
    막상 나중에 코드를 볼 때라든지
    다른 개발자가 쓴 코드를 읽을 때
    이게 대체 무슨 타입에 대한 auto 선언인지 몰라서 헤매는 경우도
    가끔은 생길 것 같네요

    1. Lyn 2012/09/21 10:10 # M/D Permalink

      auto 는 다른 언어의 var과 다르게 아무데나 쓸수있는게 아니라서 막상 써보면 사용처가 극단적으로 한정되어 있지만 반대로 오해가 생길 여지가 적습니다.

    2. 사무엘 2012/09/22 14:27 # M/D Permalink

      연산자 오버로딩도 그런 부작용이 우려되는 경우가 있긴 하지만, 그래도 당장 코드를 아주 우아하고 함축적으로 보이게 해 주는 장점도 있지요.
      또한 코드 읽을 때야 요즘은 IDE들의 인텔리센스도 아주 똑똑하니까 크게 문제될 건 없을 겁니다. ^^

Leave a comment
« Previous : 1 : 2 : 3 : 4 : 5 : Next »

블로그 이미지

철도를 명절 때에나 떠오르는 4대 교통수단 중 하나로만 아는 것은, 예수님을 사대성인· 성인군자 중 하나로만 아는 것과 같다.

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

«   2013/06   »
            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:
166098
Today:
22
Yesterday:
171