본인이 예전에 글로 썼듯, 비주얼 C++ 201x의 IDE는 소스 코드의 구문 체크 및 인텔리센스를 제공하기 위해 백그라운드에서 완전한 형태의 컴파일러를 실시간으로 돌린다. ncb 파일을 사용하던 200x 시절에는 불완전한 모조 컴파일러였지만 201x부터는 그렇지 않다. 컴파일은 그걸로 하고, 자료 저장은 아예 별도의 DB 엔진으로 하니 계층이 전문화된 셈이다.

그런데 실시간으로 돌리는 컴파일러는, MS가 자체적으로 빌드를 위해 구동하는 컴파일러하고는 다른 별개의 종류이다. 이 개발툴로 오래 개발을 해 본 분은 이미 아시겠지만 같은 문법 에러에 대해서도 메시지가 서로 미세하게 다르고 심지어 문법 해석 방식이 불일치하는 경우도 있다. 마치 MS Office의 리본 UI와 MFC의 리본 UI는 구현체가 서로 별개이고 다르듯이 말이다.

그럼 이 보이지 않는 백그라운드 컴파일러의 정체는 뭘까? 이건 ‘에디슨 디자인 그룹(Edison Design Group)’이라고 유수 프로그래밍 언어들의 컴파일러 ‘프런트 엔드’만 미들웨어 형태로 전문적으로 개발하여 라이선스를 판매하는 어느 벤처기업의 작품이다. MS에서는 이 물건을 구입하여 자기 제품에다 썼다.

컴파일러를 만드는 것은 오토마타 같은 계산 이론부터 시작해서 어려운 자료구조와 알고리즘, 컴퓨터 아키텍처 지식이 총동원되는 매우 까다롭고 어려운 과정이다. 그렇기에 컴파일러는 전산학의 꽃이라 불리며, 대학교 전산학과에서도 4학년에 가서야 맛보기 수준으로만 다뤄진다.

그리고 컴파일 메커니즘은 프런트 엔드와 백 엔드라는 두 단계로 나뉜다. 소스 코드의 구문을 분석하여 문법 오류가 있으면 잡아 내고 각종 심벌 테이블과 parse tree를 만드는 것이 전자요, 이를 바탕으로 각종 최적화를 수행하고 실제 기계어 코드를 생성하는 건 후자이다.

굳이 코드 생성까지 하지 않아도 구문을 분석하여 인텔리센스를 구현하는 것까지는 프런트 엔드만 있어도 충분할 것이다. 프런트 엔드를 담당하는 쪽은 언어의 문법을 직접적으로 다루고 있으니, C++11 표준이 뭐가 바뀌는 게 있는지를 늘 매의 눈으로 감시하고 체크해야 한다. 그리고 그런 엔지니어들이 역으로 표준의 제정에 관여하기도 한다.

에디슨 디자인 그룹은 5명의 베테랑 프로그래머들로 구성된 아주 작은 회사이다. (홈페이지부터 디자인이 심하게 단촐하지 않던가?) 하지만 세계를 움직이는 굴지의 IT 회사들에 자기 솔루션을 납품하고 있다. 작지만 기술이 강한 이런 회사야말로 컴퓨터 공돌이들이 꿈꾸는 이상적인 사업 모델이 아닐 수 없으니 매우 부럽다. 개인이 아닌 기업이나 교육 기관이 고객이며, 한 솔루션의 소스 코드를 납품하는 라이센스 비용은 수만~수십만 달러에 달한다.

마이크로소프트 컴파일러는 인텔리센스만 이 회사의 솔루션으로 구현한 반면,
Comeau C++ 컴파일러는 프런트 엔드가 이것 기반이다. Comeau라 하면, C++의 export 키워드까지 다 구현했을 정도로 표준을 가장 충실하게 따른 걸로 유명한 그 컴파일러 말이다.

굳이 백 엔드와 연결된 컴파일러가 아니어더라도, 프런트 엔드가 만들어 낸 소스 코드 parse tree는 IDE의 인텔리센스를 구현한다거나 소스 코드의 정적 분석, 리팩터링, 심벌 브라우징(browsing), 난독화 등의 용도로 매우 다양하게 쓰일 수 있다. 나름 이것도 황금알을 낳는 거위 같은 기술이라는 뜻이다.

한편, 전세계 유수의 컴파일러들에 C++ 라이브러리를 공급하는 회사는 Dinkumware이라는 걸 난 예전부터 알고 있었다. 헤더 파일의 끝에 회사 설립자인 P.J. Plauger 이름이 늘 들어가 있었기 때문이다. 난독화가 따로 없는 그 암호 같은 복잡한 템플릿들을 다 저기서 만들었다 이 말이지?
비주얼 C++이라는 그 방대한 제품은 당연한 말이지만 모든 부품이 MS 독자 개발은 아니라는 걸 알 수 있다.

그나저나, 비주얼 C++ 201x의 백그라운드 컴파일러는 C 코드에 대해서도 언제나 C++ 문법을 기준으로만 동작하더라.. ㅎㅎ

Posted by 사무엘

2013/04/16 08:40 2013/04/16 08:40
, , , ,
Response
No Trackback , 3 Comments
RSS :
http://moogi.new21.org/tc/rss/response/818

MFC와 View 오브젝트 이야기

1. 들어가는 말: MFC에 대한 큰 그림

MFC는 Windows API를 단순히 C++ 클래스 형태로 재포장만 한 게 아닌 독창적인 기능이 다음과 같이 최소한 세 가지 정도는 있다.

  • 가상 함수가 아니라 멤버 함수 포인터 테이블을 이용하여 메시지 핸들러를 연결시킨 메시지 맵. MFC 프로그래머 치고 BEGIN/END_MESSAGE_MAP()을 본 사람이 없다면 간첩일 것이다.
  • 운영체제가 제공하는 핸들 자료형들과 C++ 개체를 딱 일대일로 연결시키고, 특히 MFC가 자체적으로 생성하지 않은 핸들이라도 임시로 C++ 개체를 생성해서 연결했다가 나중에 idle time 때 자동으로 소멸을 시켜 주는 각종 handle map 관리자들. 절묘하다.
  • 20년도 더 전의 MFC 1.0 시절부터 있었던 특유의 document-view 아키텍처. 상당히 잘 만든 디자인이다.

양념으로 CPoint, CRect, CString 같은 클래스들도 편리한 물건이긴 하지만, 그건 너무 간단한 거니까 패스.

사실, MFC는 Windows API를 객체지향적으로 재해석하고 포장한 수준은 그리 높지 않다. 본디 API가 prototype이 구리게 설계되었으면, MFC도 해당 클래스의 멤버 함수도 똑같이 구린 prototype을 답습하고 내부 디테일을 그대로 노출했다.

이와 관련하여 내가 늘 드는 예가 하나 있다. 당시 경쟁작 라이브러리이던 볼랜드의 OWL은 radio button과 check button을 별도의 클래스로 분리했다. 그러나 MFC는 그렇게 하지 않았다. 운영체제 내부에서 둘은 똑같은 버튼 윈도우이고 스타일값만 다를 뿐이기 때문이다. 그러니 MFC로는 동일한 CButton이다. 그리고 CStatic도 마찬가지.
아마 기존 응용 프로그램의 포팅을 용이하게 하려고 의도적으로 이런 식으로 설계한 것 같긴 하지만, 이것 때문에 MFC를 비판하는 프로그래머도 물론 적지 않았던 게 사실이다.

그러나 인간이 하루 하루 숨만 쉬고 똥만 만드는 기계가 아니듯, MFC는 단순한 API 포장 껍데기가 아니라 다른 곳에서 더 수준 높은 존재감을 보여준다. 오늘 이 글에서는 document-view 아키텍처 쪽으로 얘기를 좀 해 보겠다.

2. view가 일반적인 윈도우와 다른 점

MFC는 뭔가 문서를 생성하여 작업하고 불러오거나 저장하는 일을 하는 업무용 프로그램을 만드는 일에 딱 최적화되어 있다. 그렇기 때문에 MFC AppWizard가 FM대로 생성해 주는 기본 코드는 아주 간단한 화면 데모 프로그램만 만들기에는 구조가 필요 이상으로 복잡하고 거추장스러워 보인다.
그냥 프레임 윈도우의 클라이언트 영역에다 바로 그림을 그려도 충분할 텐데 굳이 그 내부에 View라는 윈도우를 또 만들었다. 그리고 View는 Document 계층과 분리돼 있기 때문에, 화면에 그릴 컨텐츠는 따로 얻어 와야 한다.

이런 계층 구분은 소스 코드가 몇십~몇백만 줄에 달하는 전문적인 대형 소프트웨어를 개발할 걸 염두에 두고 장기적인 안목에서 해 놓은 것이다.
먼저, View와 Document를 구분해 놓은 덕분에, 동일한 Document를 여러 View가 자신만의 다양한 설정과 방법으로 화면에 동시에 표시하는 게 가능하다. 텍스트 에디터의 경우, 한 문서의 여러 지점을 여러 창에다 늘어놓고 수시로 왔다 갔다 하면서 편집할 수 있다. 한 창에서 텍스트를 고치면 수정분이 다른 창에도 다같이 반영되는 것이 백미.

일례로, MS 워드는 기본, 웹, 읽기, 인쇄, 개요 등 같은 문서를 완전히 다른 방식으로 렌더링하는 모드가 존재하지 않던가(물론, MS 워드가 MFC를 써서 개발됐다는 얘기는 아님). 게다가 이 중에 실제로 위지윅이 지원되고 장치 독립적인 레이아웃이 사용되는 모드는 인쇄 모드뿐이다. 인쇄를 제외한 다른 모드들은 인쇄 모드보다 문서를 훨씬 덜 정교하게 대충 렌더링하는 셈이다.

이렇듯, view는 그 자체만으로 독립성이 충분한 특성을 가진 계층임을 알 수 있다. view는 프레임 윈도우와도 분리되어 있는 덕분에, 한 프레임 윈도우 내부에 splitter를 통해 하위 view 윈도우가 여러 개 생성될 수도 있다.
CWnd의 파생 클래스인 CView는 윈도우 중에서도 바로 저런 용도로 쓰이는 윈도우를 나타내는 클래스이며, 부모 클래스보다 더 특화된 것은 크게 두 가지이다. 하나는 CDocument와의 연계이고 다른 하나는 화면 출력뿐만 아니라 인쇄와 관련된 기능이다.

SDI형 프로그램에서는 view 윈도우 자체는 계속 생성되어 있고 딸린 document만 수시로 바뀌기 때문에, document를 처음 출력할 때 view가 추가적인 초기화를 하라고 OnInitalUpdate라는 유용한 가상 함수가 호출된다. 그리고 화면 표시와 프린터 출력을 한꺼번에 하라고 WM_PAINT (OnPaint) 대신 OnDraw라는 가상 함수가 호출된다. 하지만 프린터 출력이 화면 출력과 기능면에서 같을 수는 없으니 CDC::IsPrinting이라든가 OnPrepareDC 같은 추가적인 함수도 갖고 있다.

그러고 보니 MFC의 view 클래스는 운영체제에 진짜 존재하는 '유사품' 메시지인 WM_PRINT 및 WM_PRINTCLIENT와는 어떻게 연계하여 동작하는지 모르겠다. 화면의 invalidate 영역과 긴밀하게 얽혀서 BeginPaint와 EndPaint 함수 호출을 동반해야 하는 WM_PAINT와는 달리, PRINT 메시지는 invalidate 영역과는 무관하게 그냥 창 내용 전체를 주어진 DC에다가 그리면 된다는 차이가 존재한다. 거의 쓰일 일이 없을 것 같은 메시지이지만, AnimateWindow 함수가 창 전환 효과를 위해 창 내용 이미지를 미리 내부 버퍼에다 저장해 놓을 때 꽤 유용하게 쓰인다.

3. CView의 파생 클래스들

MFC에는 CView에서 파생된 또 다른 클래스들이 있다. 유명한 파생 클래스 중 하나인 CCtrlView는 MFC가 자체 등록하는 클래스 말고 임의의 클래스에 속하는 윈도우를 그대로 view로 쓰게 해 준다.
그래서 운영체제의 시스템 컨트롤을 view로 사용하는 CTreeView, CListView, CEditView, CRichEditView 등등은 다 CCtrlView의 자식들이다.

  • 프로그램의 클라이언트 영역에다 CTreeView와 CListView를 splitter로 나란히 배열하면 '탐색기' 내지 레지스트리 편집기 같은 외형의 프로그램을 금세 만들 수 있다.
  • <날개셋> 편집기가 MFC를 써서 개발되던 버전 2.x 시절에는 문서 창을 CCtrlView로부터 상속받아 만들었다.

CCtrlView 말고 CView의 또 다른 메이저 파생 클래스로는 CScrollView가 있다. 얘는 이름에서 유추할 수 있듯, view에다가 스크롤과 관련된 기본 구현들이 들어있다. 텍스트 에디터 같은 줄 단위 묶음 스크롤 말고, 픽셀 단위로 컨텐츠의 스크롤이 필요한 일반 워드 프로세서, 그래픽 에디터 같은 프로그램의 view를 만들 때 매우 유용하다. 마우스 휠과 자동 스크롤 모드(휠 클릭) 처리도 다 기본 구현돼 있다.

인쇄 미리 보기 기능은 온몸으로 scroll view를 써 달라고 외치는 기능이나 다름없으며, 실제로 MFC가 내부적으로 구현해 놓은 '인쇄 미리 보기' view인 CPreviewView 클래스도 CScrollView의 자식이다.
단, 요즘은 Ctrl+휠을 굴렸을 때 확대/축소 기능도 구현하는 게 대세인데 배율까지 관리하는 건 이 클래스의 관할이 아닌 듯하다. 그건 사용자가 직접 구현해야 한다.

그럼 스크롤 가능한 view로는 오로지 자체 윈도우만 설정할 수 있느냐 하면 그렇지는 않다. CFormView는 대화상자를 view 형태로 집어넣은 클래스인데 그냥 CView가 아니라 CScrollView의 파생 클래스이다. 워낙 설정할 게 많아서 환경설정 대화상자 자체가 세로로 쭈욱 스크롤되는 프로그램은 여러분의 기억에 낯설지 않을 것이다.

옛날에 윈도우 3.x 시절의 PIF 편집기처럼 클라이언트 영역에 대화상자 스타일로 각종 설정을 입력 받는 게 많은 프로그램을 만들 때 CFormView는 대단히 편리하다. 대화상자는 여느 윈도우들과는 달리, 자식으로 추가된 컨트롤들에 대해 tab 키 순환과 Alt+단축키 처리가 메시지 처리 차원에서 추가되어 있다.

4. CScrollView 다루기

처음에는 CView로부터 상속받은 view를 만들어서 프로그램을 열심히 만들고 있다가, 뒤늦게 view에다가 스크롤 기능을 추가해야 할 필요가 생기는 경우가 종종 있다.
이미 수많은 프로그래밍 블로그에 해당 테크닉이 올라와 있듯, 이것은 대부분의 경우 base class를 CView에서 CScrollView로 문자적으로 일괄 치환하고 몇몇 추가적인 코드만 작성하면 금세 구현할 수 있다.

클래스 이름을 치환한 뒤 가장 먼저 해야 할 일은 스크롤의 기준이 될 이 view의 실제 크기를 SetScrollSizes 함수로 지정해 주는 것이다. OnInitialUpdate 타이밍 때 하면 된다. 안 해 주면 디버그 버전의 경우 아예 assertion failure가 난다.

여기까지만 하면 반은 먹고 들어간다. OnDraw 함수의 경우, 전달되는 pDC가 아예 스크롤 기준대로 좌표 이동이 되어 있다! 즉, 내부적으로 (30, 50) 위치에다가 점을 찍는 경우, 현재 스크롤 시작점이 (10, 20)으로 잡혀 있으면 화면상으로 이 위치만치 뺀 (20, 30)에 점이 찍힌다는 뜻이다. 내가 수동으로 스크롤 좌표 보정을 할 필요가 없다. 아, 이 얼마나 편리한가! invalid 영역의 좌표도 화면 기준이 아닌 내부 기준으로 다 이동된 채로 전달된다.

그러니 CView 시절에 짜 놓은 그리기 코드를 어지간하면 수정 없이 CScrollView에다 곧바로 써먹을 수 있다. 다만, 최적화만 좀 신경 써 주면 된다. 당장 화면에 표시되는 영역은 수백 픽셀에 불과한데 수천 픽셀짜리의 전체 그림을 몽땅 불필요하게 계산해서 그리는 루틴을 OnDraw에다 때려박지 않도록 주의해야 한다.
이때 유용한 함수는 RectVisible이다. 이 영역이 invalidate되었기 때문에 반드시 그려 줘야 하는지의 여부를 알 수 있다.

그 다음으로 신경을 좀 써야 하는 부분은 마우스 클릭이다.
마우스 좌표는 화면 기준으로 오지 내부 기준으로 오지는 않으므로, 내부 개체에 대한 hit test를 하려면 마우스 좌표에다가 GetScrollPosition(현재 스크롤 위치) 함수의 값을 더하면 된다.
그리고 화살표 키로 무슨 아이템을 골랐다면, 그 아이템의 영역이 지금의 화면 범위를 벗어났을 경우 스크롤을 시켜 줘야 한다. 수동 스크롤은 ScrollToPosition 함수로 하면 된다.

화면의 일부 영역을 다시 그리도록 invalidate하는 것도 스크롤 위치 반영이 아닌 그냥 지금 화면 기준의 좌표를 지정하면 된다. 그러면 OnDraw 함수에서는 스크롤 위치가 반영된 내부 좌표 기준으로 refresh 위치가 전달된다.

끝으로, 마우스로 어떤 개체나 텍스트를 눌러서 끌든, 혹은 단순 selection rectangle을 만들든 그 상태로 포인터가 화면 밖으로 나갔을 때, 타이머를 이용한 자동 스크롤도 구현해야 할 것이다. 이 역시 자동화하기에는 customization의 폭이 너무 넓기 때문에 MFC가 알아서 해 주는 건 없다. 알아서 구현할 것. 이 정도면 이제 스크롤 기능을 그럭저럭 넣었다고 볼 수 있을 것이다.

이 정도면 어지간한 개발 이슈들은 다 나온 것 같다.
참, 혹시 재래식 GDI API가 아니라 GDI+를 쓰고 있는 프로젝트라면 CScrollView로 갈아타는 걸 신중히 해야 할 것 같다. GDI+는 MFC가 맞춰 놓은 GDI 방식의 기본 스크롤 좌표를 무시하고 DC의 상태를 난장판으로 만들어 버리기 때문이다. GDI+는 재래식 GDI보다 느리지만 곡선의 안티앨리어싱과 알파 블렌딩이 뛰어나니 아무래도 종종 사용되게 마련인데..

간단한 해결책 중 하나는, GDI+ 그래픽은 CreateCompatibleDC / CreateCompatibleBitmap을 이용한 메모리 DC에다가 따로 그리고, 본디 화면에다가는 그 결과를 Bitblt로 뿌리기만 하는 것이다. 그렇게 하면 아무 문제가 발생하지 않고, 심지어는 속도도 내 체감상으로는 더 빨라지는 것 같다.

Posted by 사무엘

2013/03/13 19:34 2013/03/13 19:34
, , ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/806

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

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

C++의 for each, in 키워드

심각한 뒷북인지 모르겠는데,
본인은 비주얼 C++에서 이런 문법이 가능하다는 걸 아주 최근에야 알게 되었다.

DATA container[N];

for each(DATA elem in container) {
    do_with(elem);
}

저건 언어 차원에서 제공하는 새로운 문법이기 때문에 STL <algorithm>의 for_each 함수와는 다르다.

배열을 순회하기 위한 별도의 임시 변수(일회용 int i나 거추장스러운 포인터)를 선언할 필요 없이, 코드를 굉장히 깔끔하게 만들 수 있어서 좋다. 이것의 주 용도는 C++을 상당한 고수준 언어로 끌어올린 C++/CLI 환경이지만, 네이티브 환경에서도 정적 배열과 STL 컨테이너 정도에서는 아주 요긴하게 쓰일 수 있다.

DATA가 int 같은 아주 기본적인 자료형이라면 그냥 저렇게 써 주면 되고, 개당 수십~수백 바이트씩 하는 무거운 구조체라면 const DATA&를 하면 된다. 그리고 순회 중인 배열 자체의 데이터를 루프 안에서 고쳐야 한다면 물론 DATA&라고 써 주면 된다.
저건 C++11 같은 급도 아니고, 생각보다 굉장히 오래 된 비주얼 C++ 2005에서부터 지원되기 시작했다고 한다.

컴파일러가 언어 표준에 없는 변칙 문법이나 키워드를 지원하는 것은 특정 CPU나 운영체제에 종속적인 기능을 추가로 제공하기 위해서이다.
하지만 for each는 그런 범주에 속하지 않으며, 전통적인 C/C++ 언어의 토큰 나열과 비교했을 때 문법도 굉장히 이질적이다. 그럼에도 불구하고 비주얼 C++이 이것을 제공하는 것이 신기하기 그지없다.

그리고 또 하나 생각할 점은, 저기서 each와 in은 문맥 의존적인 임시 예약어(키워드)라는 것이다. for 다음에 이어졌을 때만 키워드이며, 다른 곳에서는 사용자가 each나 in을 일반적인 변수/함수명으로 얼마든지 쓸 수 있다는 뜻.

언어 설계 차원에서 C/C++은 원래 임시 예약어라는 게 없는 언어이다. 한번 예약어로 찜해진 단어는 그 어떤 곳에서도 명칭으로 결코 쓰일 수 없다. 다른 구문이나 수식을 파싱하는 데는 문맥 의존적인 어려운 문법이 많지만, 예약어 식별만은 단순하게 만들려고 했는가 보다.

그 반면, 파스칼은 begin, end, if, for 같은 단어야 절대적인 예약어이겠지만 forward(함수 전방 선언용)를 포함해 몇몇 키워드는 일정 문맥에서 별도의 의미를 갖는 임시 예약어이다. 그리고 객체지향 개념이 추가된 오브젝트 파스칼의 경우 virtual 같은 함수 modifier, 그리고 클래스 내부에서 public/protected 같은 멤버 접근성 modifier도 임시 예약어이다. C++은 그렇지 않다.

비주얼 C++은 for each, in뿐만 아니라 abstract, override, delegate 등 몇몇 비표준 임시 예약어를 더 두고 있기도 한데, 이것은 대개가 C++/CLI용이고 네이티브 환경에서는 쓰일 일이 별로 없다. 일반적인 경우라면 비표준 확장 예약어는 앞에 __를 붙여서 명칭 충돌의 여지를 없앤 뒤에 절대적인 예약어로 추가하는 게 관행일 텐데, 저것들은 그렇게 하지 않았다.

끝으로, for each, in에다가 2차원 배열을 넘겨 주면 어떻게 될까 궁금해서 시도를 해 봤는데, 이때도 각 원소들이 하나씩 순서대로 순회되더라.
각 배열을 배열의 포인터로 받으면서 1차원적으로 순회되지는 않는가 보다.

비주얼 C++ 2010은 인텔리센스 컴파일러와 실제 컴파일러의 동작이 서로 다르기라도 했는지, IDE에서는 이때 each 변수가 2차원 배열과 서로 호환이 되지 않는다고 빨간줄 에러를 뱉은 반면, 실제 컴파일은 됐다.
2012에서는 그것이 개선되어 IDE에서도 빨간줄이 생기지 않는다.

Posted by 사무엘

2013/02/16 08:17 2013/02/16 08:17
, , ,
Response
No Trackback , 4 Comments
RSS :
http://moogi.new21.org/tc/rss/response/796

비주얼 C++ 2012 사용기

비주얼 C++ 2010에 이어 2012까지.. 세상 참 많이도 변했다. 먼저 외형부터 얘기하자면,
  • 과거의 아래아한글 97을 떠올리게 하는 독자적인 IDE 외형. 2010은 그래도 non-client에라도 운영체제의 표준 껍데기가 붙어 있었는데 2012는 그런 것조차 없다. 그저 허연 화면.
  • 윈도우 8 스타일로 아이콘과 각종 배색은 다 단순한 16컬러 solid color 스타일로 돌아갔다. 하지만 스타일이 그렇게 단순해졌다는 뜻이지 확대해 보면 안티 앨리어싱이 있다. 색상 자체가 아예 16컬러로 회귀한 건 아님.
  • 함수 인자와 #define 매크로, 기존 선언 명칭 따위를 다른 색깔로 표시해 주니 굉장히 놀랍고 편리함을 느낀다. fuzzy logic까지 썼다나? 이 기능은 프로젝트를 열어서 IDE가 소스 코드에 대한 모든 문맥을 파악하고 있을 때에만 지원된다.
  • Find in files나 솔루션 탐색기 같은 데서 파일을 열람할 때, 탭이 오른쪽에 생기면서 파일을 임시로 일회용으로 살짝 열어 보는 기능이 추가됨. good!

다음은 코딩과 빌드 같은 본연의 기능.

  • 익히 듣던 대로, 훨씬 더 편리해진 인텔리센스와 자동 완성, 더 현란해진 syntax 컬러링. file view에서도 해당 파일별 클래스를 미리 볼 수 있다. 코딩이 더욱 즐거워졌다.
  • 정적 코드 분석 기능이 드디어 번들로 제공되는 경지에 도달!
  • 똑같은 옵션으로 돌려도 컴파일/링크 빌드 속도가 2010보다는 확실히 더 빨라졌다.
  • 똑같이 최고 수준으로 최적화를 했을 때, 생성되는 코드 크기는 2008 이래로 2012로 이어질수록 x86의 코드는 더 커지고, x64의 코드는 더 작아지는 추세가 명확하다.
  • 2010의 프로젝트 파일은 별도의 변환이나 업그레이드 강요 없이 곧바로 불러올 수 있는가 보다. 다행이다. 아울러, 2010때부터 컴파일러 툴셋을 취사선택 할 수 있게 된 것도 아주 좋은 점이다.

다음은 불편한 점, 황당한 점.

  • 닷넷 시절부터 있었던 설치/배포 패키지 프로젝트 기능이 아무 예고 없이 사라져 버려서 멘붕. 도대체 왜 없앤 거지? 대체제인 InstallShield 프로젝트 기능이 있긴 하지만, 처음부터 곧바로 제공되는 건 아니고 난 거기까지는 아직 안 써 봤다.
  • 2010때부터 IDE가 WPF 기반으로 다시 만들어지면서 메뉴와 도구모음줄을 customize하기가 무지무지하게 불편해졌는데... 이건 2012도 하나도 나아진 게 없다. (개발 인력과 시간이 부족해서 그 부분은 대충 그렇게 때운 거라고 함..; )
  • 텍스트 에디터에서의 문자열 검색은 요즘 추세인 증분(incremental) 검색 스타일로 바뀌었는데, 바이너리 에디터나 리소스의 string table 에디터 같은 다른 곳에서 텍스트 검색 기능이 모두 사라져 버렸다! 바이너리 에디터의 경우, 한 문자열을 입력하면 1바이트 인코딩과 2바이트 인코딩 기준으로 모두 찾아 주는 대단히 편리한 기능이었는데 왜 없앤 거지? 도로 넣어 주셈.
  • 이미지 에디터에서 각종 툴을 사용할 때 마우스 포인터 모양이 hot spot 위치를 짐작할 수 없는 이상한 모양으로 바뀐 게 있다. 그리고 32비트 아이콘을 편집하는 기능도 앞으로 좀 부탁..;;

요컨대.. 코딩과 빌드 본연의 기능은 확실히 더 나아졌지만, 10년 전부터 굳건히 있던 작지만 유용한 IDE 기능들 일부가 예고도 없이 갑자기 쏙 빠져 버린 건 내게 당혹감을 선사한다. 또한 고질적인 불편한 점은 여전히 해소되지 않은 것도 있다.
일단 <날개셋> 한글 입력기 6.71은 2012 대신 여전히 2010으로 빌드해서 공개했다.

Posted by 사무엘

2013/02/01 08:26 2013/02/01 08:26
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/790

※ 윈도우 프로그래머라면 누구나 다 알 만한 내용에 대한 정리이다.
보면 아시겠지만 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

들어가는 말

  • 프로젝트 단위: 말 그대로 한 개의 결과물을 생성하는 것을 목표로 하는 한 비주얼 C++ 프로젝트당 하나씩만 생성되는 파일이다. 리소스는 특수한 경우가 아니면 보통 프로젝트마다 하나만 있기 때문에, per-프로젝트인 것으로 간주된다.
  • configuration 단위: 한 프로젝트 내에서 debug나 release 별로 따로 생성되고, x86이나 x64 같은 플랫폼별로 다 따로 생성되는 파일이다.
  • 소스 단위: 번역 단위(translation unit)별로 다 제각각 생성되는 파일이다. configuration에도 물론 종속적이며, 다 따로 생성된다.

※ 프로젝트를 열면 생성되는 것

APS (프로젝트 단위)

전통적인 윈도우용 실행 파일(EXE/DLL)을 빌드하기 위해서는 잘 알다시피 컴파일된 코드뿐만 아니라 리소스도 같이 들어가는데, 그 리소스를 명시해 주는 '리소스의 소스', 일명 리소스 스크립트는 바로 *.rc 파일이다. 그리고 *.rc와 일반 소스 코드 *.cpp는 resource.h에 정의된 심벌들을 통해 동일 리소스를 식별하게 된다.

그런데 매번 일반 텍스트 형태로 된 rc 파일을 resource.h와 엮어서 파싱하자니 불편하다. 리소스 스크립트는 텍스트 에디터를 써서 사람이 손으로 편집한 뒤 컴파일하기에는 적합하지만, IDE 같은 소프트웨어가 자동으로 다뤄 주기에는 비효율적인 구조인 것이다.

그래서 비주얼 C++은 리소스 ID까지 포함하여 리소스 스크립트의 바이너리 representation을 따로 만들어 두고 지낸다. APS 파일이 존재하고 이게 RC나 H 같은 텍스트 소스에 비해 outdate되지 않았다면, 프로그램은 매번 텍스트를 파싱하는 게 아니라 APS 파일을 곧장 읽는다.

비주얼 C++에서 프로젝트를 처음으로 열어서 리소스 뷰로 리소스들을 처음 열람하면, 프로그램이 리소스 컴파일러를 가동해서 뭘 파싱하면서 시간이 오래 걸린다. 하지만 다음에 열 때부터는 리소스가 곧바로 빨리 열리는데, 이것이 바로 APS 파일 덕분이다.

CLW (프로젝트 단위) deprecated

이것은 비주얼 C++ 4~6 사이에, 그 이름도 유명한 MFC Class Wizard (클래스 마법사) 때문에 도입되었던 부가정보 파일이다.
MFC 클래스에서 파생된 윈도우 클래스 같은 데서 메시지 핸들러(마법사의 용도가 굳이 메시지 핸들러뿐인 건 아니지만)를 추가하려면 일단 헤더 파일에 afx_msg void OnXXXX가 추가되어야 하고, 메시지 맵 BEGIN_MESSAGE_MAP() 밑에 ON_MESSAGE_***가 추가되어야 하고, 끝으로 소스 파일에 해당 멤버 함수의 몸체가 추가되어야 한다.

그런데 이 일을 모든 소스 코드를 일일이 파싱하면서 추가 지점을 찾아서 하기란 여간 어려운 일이 아닐 수 없다.
C++은 선언 따로, 정의 따로이고(C#, 자바는 그렇지 않다) 정의부가 반드시 어느 번역 단위에 존재해야 한다는 제약이 전혀 없다. FM대로 하는 건 15년 전의 펜티엄 컴으로는 무리였다.

그래서 편의상 클래스의 선언부와 메시지 맵의 주변엔 클래스 마법사만이 식별하는 문자열이 들어간 주석이 있고, 클래스 마법사는 그 구간을 대상으로만 작업을 신속하게 했다. 그리고 그걸로도 부족해서 클래스 마법사의 파싱 결과가 CLW 파일에 들어갔다. 식별자 주석을 건드리면 클래스 마법사가 제대로 동작하지 못했다.

21세기에 나온 비주얼 C++ .NET과 그 이후 버전은 CLW 파일을 만들거나 사용하지 않으며, 클래스 마법사 주석 없이도 멤버 함수나 핸들러의 추가를 그럭저럭 정확하게 해낸다. 사실 클래스 마법사 자체가 비주얼 C++ 200x에서는 사라졌다가 2010에서부터 부활했다.

※ 빌드하면 생성되는 것

OBJ (소스 파일 단위)

비주얼뿐만이 아니라 전세계의 어느 C/C++ 컴파일러를 돌리더라도, 소스 코드를 컴파일하면 이것이 매 소스, 즉 번역 단위별로 생성된다. 소스 코드를 번역한 기계어 코드가 obj 파일 포맷에 맞게 들어있는데, 때로는 기계어 코드뿐만 아니라 각종 디버깅 정보와 링크 때 링커가 참고할 만한 메타데이터도 잔뜩 가미된다.

static library라고 불리는 LIB는 별개의 포맷이 아니라, 그냥 여러 번역 단위들을 컴파일한 obj들의 컬렉션일 뿐이다. obj를 단순히 lib로 합치기만 할 때는 링크 에러가 나지 않는다(즉, 선언된 심벌들이 반드시 정의되어야 할 필요가 없다.)

RES (configuration 단위)

리소스 스크립트를 컴파일하여 생성되는 결과물이다. 리소스 스크립트의 바이너리 최적화 형태인 APS와 무엇이 다르냐고 물으신다면, 차이가 적지 않다.
APS는 리소스 스크립트 파일의 표현 형태만 메모리 친화적으로 바꾼 것이기 때문에 ID_RADIO1 같은 상수 명칭의 문자열 원형과 심지어 조건부 컴파일을 위한 스펙까지 다 보존되어 있으며, 참조하는 비트맵 같은 데이터 파일도 파일명 형태로 존재한다. APS 파일로부터 RC 파일과 resource.h 파일을 복원해 낼 수 있다.

그러나 RES는 상수는 다 숫자로 박히고 참조하는 데이터 파일도 모두 내부에 embed되었으며, 이 상태 그대로 실행 파일에다 링크되어 들어가기만 하면 되는 상태인 것이다.

PCH (configuration 단위)

pre-compiled header인 stdafx.h와, 이에 대응하는 번역 단위인 stdafx.cpp를 컴파일하여 얻은 각종 컴파일 context들, 즉 함수와 클래스 선언, #define 명칭 등등을 바이너리 형태로 보관하고 있는 파일들이다. 이게 있으면 stdafx.h를 인클루드하라는 명령은 실제 헤더 파일을 파싱하는 게 아니라 그냥 pch 파일을 참조하는 것으로 대체된다.

컴파일러의 버전이 올라가고 각종 플랫폼 SDK의 크기가 커질수록 이 파일의 크기도 야금야금 커져 왔다. 이거 없이는 C++은 살인적인 인클루드질 때문에, 느린 빌드 속도를 도저히 감당할 수 없다.

PDB (configuration 단위)

빌드 결과 만들어진 EXE/DLL에서 기계어 코드의 어느 부분이 어느 소스의 몇째 줄에 대응하는지(소스 코드 자체는 없고 소스의 경로만), 이 함수에서 이 지역변수의 이름이 무엇인지 등을 담고 있는 디버그 정보 데이터베이스이다.

디버그 모드가 아니라 릴리스 모드로 빌드한 최적화된 실행 파일이라도, PDB 파일을 참조하게 하는 최소한의 정보만이라도 남겨 두면, 나중에 프로그램이 뻗는다거나 할 때 소스상으로 최소한 어느 지점에서 뻗었는지를 개발자의 컴에서 확인해 볼 수 있다. 개발자의 컴엔 직전에 이 바이너리를 빌드하면서 같이 생성된 PDB 파일이 존재하기 때문이다.

ILK (configuration 단위. 대개 디버그 빌드에서만)

증분 링크(incremental link)를 위한 context 정보가 들어있다.
이것은 프로그램의 빌드 속도를 올리기 위한 테크닉이다. 매번 링크를 처음부터 일일이 새로 하는 게 아니라, 처음에 빌드할 때 바이너리를 좀 여분을 둬서 듬성듬성 큼직하게 만들어 두고, 다음부터는 바뀐 obj 파일 내용만 기존 바이너리의 자기 지점에다 대체하는 방식으로 빌드를 신속하게 끝낸다. 혹은 뒷부분에다가 새로운 빌드 내용을 계속 추가해 넣기만 하고, 예전 빌드 내용을 무효화시키는 방법도 쓴다.

요즘 디버그 빌드가 단순히 최적화를 안 한 것 이상으로 릴리스 빌드보다 빌드된 바이너리의 크기가 유난히 큰 이유가 여기에 있다. 게다가 Edit and continue 기능을 위해서도 여분 공간이 필요하기 때문에 크기가 커질 수밖에 없다. 디버그 빌드 바이너리를 바이너리 에디터로 들여다보면, 온통 0xCC (no op)으로 도배가 되고 내부가 헐렁함을 알 수 있다.

MS 오피스도 2007 이전 버전을 보면 방대한 워드/엑셀 문서를 편집할 때 바뀐 내용만 짤막하게 저장하는 옵션이 있었다. 그게 일종의 증분 저장 기능이다. 지금은 그게 보안상으로 문제가 되기도 하고 문서 파일 포맷이 크게 바뀌었으며, 굳이 증분 저장을 안 써도 될 정도로 PC 성능이 좋아졌다고 여겨져서 그런 기능이 없어졌지만 말이다.
증분 링크는 보통은 디버그 모드 빌드에서만 쓰인다.

VC???.idb (configuration 단위. 대개 디버그 빌드에서만)

ILK 파일과 마찬가지로 빌드 시간의 단축을 위해 존재하는 파일이다.
디버그 모드로 빌드를 해 보면, 헤더 파일이 바뀌었더라도 해당 헤더를 인클루드하는 cpp 파일들이 전부 리빌드되는 게 아니라 가끔 'Skipping.. (no relevant changes detected)'이러면서 넘어가는 파일도 있다. 그리고 대체로 이런 컴파일러의 판단이 맞다. 헤더 파일을 고쳤더라도 클래스의 선언부 같은 크리티컬한 부분이 아니라 그냥 주석 같은 trivial한 부분만 바뀌었기 때문에 굳이 리빌드가 필요하지 않다는 걸 어떻게 판단할까?

컴파일러가 제공하는 Enable Minimal Rebuild (/Gm) 옵션 때문에 가능하다. 이게 지정되면 빌드 과정에서 프로젝트명이 아니라 고정된 이름의 의존성 판단용 부가정보 파일이 생긴다. ???는 해당 비주얼 C++의 버전이다. 2008의 경우 90, 2010의 경우 100.

정리하자면, 빌드와 함께 생성되는 파일들 중, 실제로 링커에 의해 EXE/DLL 따위를 만드는 데 동원되는 파일은 OBJ, RES이다.
빌드 시간을 단축시키는 데 쓰이는 파일은 PCH, IDB, ILK이다.
PDB는 프로그램의 문제 추적을 위해 추후에 쓰이는 파일이다.

※ 편의 기능 + 빌드

SBR (소스 파일 단위), BSC (configuration 단위)

자, 이 파일은 빌드를 하면 생성되지만, 프로그램의 빌드나 디버깅을 위해서 반드시 생성해야만 하는 파일은 아니다.
방대한 양의 소스 코드를 컴파일하고 나면 컴파일러는 그 소스 코드의 모든 내부 구조에 대해서 알게 된다. 그걸 알아야만 기계어 코드를 생성할 수 있을 테니까.

컴파일이 끝났다고 그 정보를 그냥 버리는 건 아깝기 때문에, 일정한 파일 포맷을 제정하여 이것을 소스 코드에 대한 browsing에 활용할 수 있다. 가령, 이 클래스 멤버 함수의 정의는 어디에 있고, 이 함수가 호출하는 함수와, 이 함수를 호출하는 함수와의 그래프 관계는 어떻고 하는 것 말이다. 소스 코드가 텍스트라면, browse 정보는 정교하게 짜여진 색인인 셈이다.

이 개념과 파일 포맷은 비주얼 C++의 아주 초창기 시절부터 존재했다.
그리고 비주얼 C++은 버전 6까지는, 프로젝트를 빌드할 때 browse 정보도 같이 이렇게 덤으로 빌드되게 해서 browse 정보를 조회하는 기능을 갖추고 있었다. SBR과 BSC의 관계는 C/C++ 소스 코드에서 OBJ와 EXE의 관계와 정확히 같다. 한 번역 단위를 컴파일하면 한 SBR이 생겼고, SBR들을 뭉쳐서 BSC 파일이 생성되었다.

물론 이렇게 하면 빌드 시간이 더욱 길어졌고, 굳이 browse 기능을 쓰지 않는 사람도 있었기 때문에 이 기능은 철저히 선택사항이었다. 그리고 닷넷부터는 이 정보를 만들지 않더라도, 뒤에서 설명할 인텔리센스 정보만으로 IDE 차원에서 browse 대체 기능을 갖추기 시작했다.

※ 인텔리센스

NCB (프로젝트 단위) deprecated

sbr/bsc보다는 나중에, 시기적으로는 clw와 비슷한 타이밍(비주얼 C++ 4)에 만들어진 파일 포맷이다.
바야흐로 비주얼 C++ 4에서는 최초로 Class View라는 게 생겨서 프로젝트에 존재하는 모든 클래스와 멤버, 전역 변수/함수들을 표시하는 기능이 추가되었다. ncb는 browse 정보를 만드는 것만치 소스를 심도 있게 일일이 다 까 보지는 않고, 그보다는 단순하게 코드를 파싱하여 해당 기능을 빠르게 구현하는 데 필요한 부가 정보를 저장했다.

Class View가 도입되었던 초창기에는 소스 코드를 매번 빌드는 아니어도 저장을 해야만 컨텐츠가 업데이트되었다. 그나마 저장하지 않고도 실시간으로 업데이트가 되기 시작한 건 VC 6부터이다.
그리고 VC 6에서는 잘 알다시피 초보적인 수준의 인텔리센스 및 멤버 표시/자동 완성 기능이 구현되었고, 그 정보 역시 ncb 파일에다 저장되었다. 당연히 같은 프로젝트를 만들어도 ncb 파일의 크기는 더욱 커지게 됐다.

비주얼 C++이 버전업되면서 인텔리센스는 성능이 더욱 강력해졌다. 바로 닷넷에서부터는 #define 심벌이 추가로 인텔리센스의 혜택을 입기 시작했으며 템플릿도 제대로 지원되기 시작했다. 오동작 빈도도 더욱 줄었다.

그러나 이 모든 것은 여전히 10년 전의 ncb 파일을 기반으로, 진품이 아닌 가짜 parser를 임기응변 식으로 확장하면서 구현된 것이기 때문에, 어느 수준 이상의 정확도를 낼 수는 없었으며 복잡한 C++ 문법의 모든 것을 수용하는 데에도 근본적인 한계가 있었다.

가령, 클래스 멤버 함수의 선언이 복잡한 #define 매크로 안에 숨어 있으면 Class View에 이것이 제대로 나타나지 않았다. 갑자기 빌드 configuration이나 플랫폼을 확 바꿔 버리면 인텔리센스가 멘붕을 일으켰으며, 복잡한 조건부 컴파일 구간에 숨어 있는 코드도 인텔리센스가 상황 파악을 제대로 못 하는 경우가 많았다. 멘붕의 정도가 심하면 인텔리센스가 아예 동작을 멎어 버리기도 했기 때문에, 수시로 ncb 파일을 지우고 다시 만들어 주는 건 필수 작업이었다.

SDF (프로젝트 단위), IPCH (configuration 단위)

위와 같은 기존 ncb 기반 인텔리센스의 문제를 극복하고자 비주얼 C++ 2010은 안 그래도 C++11 때문에 문법도 대폭 확장해야 하는데 이 기회에 인텔리센스 엔진을 완전히 갈아 엎었다. SQL server compact edition이라는 전문 DB 엔진을 쓰기 시작했다.

2010부터는 가짜 parser가 아니라 진짜 컴파일러와 똑같은 수준의 parser가 background에서 모든 소스와 헤더 파일들을 일일이 파싱하여 실시간으로 심벌 정보를 고친다. 정확한 문맥을 파악하고 있기 때문에 100% 정확한 인텔리센스가 제공되며, 예전처럼 좀 오동작한다 싶어도 잠시 기다려서 파싱 정보가 갱신되고 나면 곧장 똑바로 동작하기 시작한다.

다만, 이런 첨단 기술이 공짜로 된 건 아니기 때문에, 어지간한 C++ 프로젝트는 이제 인텔리센스 파일만 수십~100수십 MB씩 디스크를 쳐묵쳐묵 하는 대가를 감수해야 한다. 어느 프로젝트를 열든지 동일하게 공유되는 MFC나 플랫폼 SDK의 인텔리센스 정보는 여러 프로젝트들이 한데 공유만 할 수 있어도 인텔리센스의 용량이 크게 줄어들 텐데, 무척 아쉽다.

그래도 비주얼 C++ 제작진에서 일말의 배려를 했다 싶은 대목은, 인텔리센스 DB 파일이 생성되는 곳만 한 곳에 따로 대체 지정이 가능하다는 것이다. 프로젝트-옵션이 아니라 도구-옵션에서 “텍스트 편집기-C/C++/고급”으로 가면 fallback location을 지정하는 옵션이 있으며, 이것만 해 주면 비주얼 C++로 만드는 모든 프로젝트들의 인텔리센스 DB는 거기 아래로 한데 모이게 된다.

이렇듯, 비주얼 C++ IDE나 컴파일러가 생성하는 보조 파일들의 용도와 배경에 대해서 공부하면 C/C++ 언어의 특성을 알 수 있고, 프로그래밍 언어에 대한 비판적인 안목, 그리고 언어의 비효율을 극복하고 조금이라도 개발 도구의 생산성을 올리기 위해 해당 제작진이 어떤 꼼수를 동원했는지에 대해서도 알 수 있다.

Posted by 사무엘

2012/10/16 08:30 2012/10/16 08:30
,
Response
No Trackback , 4 Comments
RSS :
http://moogi.new21.org/tc/rss/response/744

요즘 코딩 잡설

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

1. 심벌 검색 기능의 퇴화(?)

예전에도 글에서 언급한 적이 있지만, 비주얼 C++에는 Alt+F12를 누르면 심벌 검색을 할 수 있다. 주어진 프로젝트의 소스 코드에 등장하는 모든 명칭들(클래스, 함수, 전역 변수 등등)의 선언과 정의가 있는 곳을 곧바로 찾아갈 수 있으니 이건 매우 편리한 기능이 아닐 수 없다.

이 기능이 특히 강력한 이유는 내가 해당 프로젝트의 내부에서 선언한 명칭뿐만 아니라, 인클루드 파일에 있는 명칭들도 전부 조회할 수 있기 때문이다. 따라서 C/C++ 라이브러리에 있는 함수나 윈도우 플랫폼 SDK 내지 MFC 라이브러리에 있는 방대한 명칭들도 다 조회가 되어서 해당 명칭의 출처를 쉽게 알아낼 수 있다.

어차피 소스 코드를 빌드하여 precompiled header나 인텔리센스 정보를 만들 때 이런 정보들을 다 한 번씩 파싱을 하기 때문에, 심벌 검색은 최적화된 자체 데이터베이스를 대상으로 신속하게 행해진다. 무식하게 수백, 수천 개의 헤더와 소스 파일들을 텍스트 형태로 찾는 find in files 형태가 아니다.

그런데, 비주얼 C++ 2010을 보니 심벌 검색은 해당 프로젝트에서 직접 선언한 명칭만 가능하고, 그 프로젝트가 stdafx.h에다가 인클루드하여 사용하는 플랫폼 SDK, MFC 같은 것들의 명칭은 조회되지 않는다.
200x 시절과 동일하게 '참조에서 찾기' 옵션을 켜고, 검색 범위를 'All components'로 바뀌었는데도 여전하다. 이 기능에 무슨 문제가 생겼는지 궁금하다.

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

(WM_CREATE 위치가 뜨는 2003 좌, 하지만 뜨지 않는 2010 우)

물론, 소스 코드에서 MFC나 플랫폼 SDK의 명칭을 참조하는 부분에서 F12를 눌러 보면 여전히 해당 명칭의 선언부로 가긴 간다. 하지만 명칭을 직접 입력해서 찾는 심벌 검색 기능은 왜 그게 불가능해진 걸까?

보아하니 그저 닷넷 프레임워크 라이브러리의 명칭을 조회하는 기능에만 신경 쓰느라, C++ 네이티브 개발 쪽은 지원이 간과되기라도 한 건지? 2010은 그렇잖아도 인텔리센스에다 빌드 보조 파일들이(*.sdf, *.ipch) 예전에 비하면 기겁을 할 정도로 방대해졌는데 편의 기능은 도리어 없어지면 어떡하냐 말이다.

2. 메뉴 편집기의 우클릭

C++ 프로젝트를 새로 만들거나 열어서 리소스에서 메뉴 편집기를 연다. 아, 프로젝트를 만들 필요 없이 그냥 리소스 템플릿만 하나 만들어서 메뉴를 생성해도 되겠다.

열었으면 클라이언트 화면의 빈 공간을 아무 데나 우클릭하여 메뉴 편집기에 대한 컨텍스트 메뉴를 연다. 그 후 마우스로 다른 곳을 클릭하거나, 명령을 선택하거나, ESC를 눌러서 컨텍스트 메뉴를 없앤다.
그러면 컨텍스트 메뉴가 화면 좌측 상단에 한 번 또 나타나서 사용자를 성가시게 할 것이다.

이는 명백한 버그이다. 대화상자 같은 다른 리소스 편집기에서는 우클릭을 해도 이런 현상이 생기지 않는다.
2010뿐만이 아니라 무려 2003에서도 동일한 현상이 발견된다. 거의 10년 묵은 버그라는 뜻인데 아무도 신경을 안 쓰는지 지금까지 고쳐지지 않았다.
설마 6.0에서까지 이랬을 것 같지는 않은데 잘 모르겠다. 아직도 6.0 쓰시는 분이 계시면 확인 요망.

여담이지만 마우스가 아니라 Shift+F10 같은 키보드로 컨텍스트 메뉴를 열면 이런 현상이 생기지 않는다.
그리고 화면 빈 공간이 아니라 편집 중인 메뉴 항목의 경우 우클릭하더라도 역시 그 현상이 생기지 않는다.
이건 아주 사소한 코딩 실수로 보이고, 몇 라인만 고치면 바로 제거할 수 있는 버그이다만, 10년에 가까운 시간 동안 발견하고 지적한 사람이 없었나 보다.

C#이나 VB, C++/CLI 같은 닷넷 환경의 경우, 폼(네이티브 개발 환경으로 치면 대화상자)에다가 메뉴 컴포넌트를 집어넣으면 그 자리에서 바로 메뉴를 편집할 수 있게 되어 있으니 네이티브 개발과는 환경이 꽤 다르다.
닷넷 프로그램도 기본 메뉴는 일반 윈도우 운영체제가 제공하는 표준 네이티브 메뉴 형태로 나오지 않겠나 하고 생각해 왔는데, 놀랍게도 그렇지 않다. 비주얼 스튜디오 200x와 비슷한 형태인 싸제 메뉴이다.

3. 툴바 편집기의 화면 잔상

이뿐만이 아니다.
리소스 중에서 툴바 편집기를 보면, 툴바 아이템들을 순서대로 하나씩 찍어 보기만 해도 예전 selection 흔적이 지저분한 잔상으로 잔뜩 남는다. 저건 절대로 multiple selection을 나타내는  게 아니며, WM_PAINT 메시지만 다시 받아도 잔상은 싹 없어진다.

사용자 삽입 이미지
열기, 저장, 모두 저장, 인쇄 아이콘의 테두리에 생긴 잔상들을 보라.
그리고 믿어지지 않겠지만 이건 비주얼 C++ 2003 시절부터 변함없던 버그이다!
전세계에서 압도적인 인지도와 점유율을 자랑하는 개발툴에 이런 초보적인 버그가 있다는 게 믿어지는가? 6.0은 그렇지 않았던 걸로 난 기억한다.

아이콘의 배치 순서를 조정하거나 중간에 여백을 넣기 위해서 드래그 드롭만 해도 잔상이 잔뜩 쌓인다. 구체적으로 재연 조건과 증상을 일일이 기술하기에는 구차하나, 잔상 현상은 2010에서 조금 더 심해졌다.

4. 속성 대화상자

비주얼 C++ 6.0까지는 전통적으로 가로로 길쭉한 자신만의 context-sensitive한(문맥 민감. 사용자가 키보드 포커스를 두거나 선택한 개체나 문서에 따라서 대화상자 내부 내용이 수시로 동적으로 바뀌는) 속성 대화상자가 있어서 Alt+Enter를 누르면 언제든지 그게 떴었다. old timer라면 추억의 옛날 스타일 대화상자를 기억하실 것이다.

사용자 삽입 이미지
그게 닷넷부터는 비주얼 베이직 스타일의 프로퍼티 그리드로 다 바뀌었다.
특히 프로젝트 설정 대화상자(VC6 표준 단축키 기준 Alt+F7)도 이 형태로 리모델링된 것 여러분들 다 아실 것이다.

그러나 프로퍼티 그리드가 커버하지 못하는 UI가 있었으니 그것은 바로 preview 기능이다.
비트맵, 대화상자, 메뉴 등 리소스들을 일일이 열 필요 없이 찍어 보기만 해도 이놈이 대략 어떻게 생겼는지 간략히 표시해서 보여주는 기능인데,
이건 2차원적인 공간에다 뭔가를 그려야 하기 때문에 기존 프로퍼티 그리드로 커버할 수가 없다.

그래서 별도의 버튼을 누르면 결국 과거 6.0 시절의 속성 대화상자와 비스무리하게 생긴 대화상자가 떠서 미리보기를 보여주는 기능이 들어갔다. 뭐, 여기까지는 뭐 나쁘지 않다. 메뉴나 대화상자가 좀 더 깔끔하게 그려졌으면 좋겠는데 10년 전이나 지금이나 하나도 바뀐 게 없이 똑같이 엉성하다는 건 아쉽지만 말이다.

그런데 과거의 200x 시절에는 미리보기를 보는 중에도 키보드 포커스는 각종 리소스들을 고르는 화면에서 계속 유지가 되어서 위· 아래 화살표를 누르며 리소스들을 조회할 수 있었는데,
2010부터는 뭔가를 선택하고 나면, 키보드 포커스가 미리보기 대화상자로 바뀌어 버린다. 그래서 마우스로 해당 아이템들을 일일이 찍어야 한다.

역사적으로 비주얼 C++은 4.0 때 Developer Studio (MSDEV)라는 첫 UI가 갖춰진 이래로 닷넷으로 넘어갈 때 대대적인 리모델링을 거쳤고, 2010 때는 WPF 기반으로 또 IDE의 구현체가 크게 바뀌었다.

요즘 다시 C++11 지원처럼 C++ 지원이 강화되고는 있다지만, 기존 코드들이 리팩터링되는 과정에서 예전에는 없던 사소한 버그들이 끼어 들어가는 게, MS에서 닷넷에 비해 네이티브 환경 개발에 점점 소심해지고 있다는 생각이 들어서 아쉽다. 닷넷과 관련된 개발 환경이라면 저런 버그가 들어갔을 리가 없을 텐데 말이다.

다음은 버그까지는 아니고, 비주얼 C++과 관란하여 추가로 떠오르는 생각들이다.

1. 비주얼 C++은 32비트 시절 이래로(무려 4.x부터) 80비트 초정밀 부동소숫점인 long double을 무시하고, 이것도 일반 double과 완전히 동일한 64비트 부동소숫점으로만 제공하는 것으로 잘 알려져 있다.
난 32비트 CPU에서는 10바이트 단위로 정보를 처리하는 게 불편해저서 long double이 도태한 게 아니겠나 정도로만 생각해 왔다.
그런데 나중에 알고 보니 인텔 CPU엔 80비트 부동소숫점을 연산하는 명령 자체는 존재한다고 한다. 단지, MS 컴파일러가 이를 활용하지 않는다고.

이것까지 지원해야 하면 %타입 문자부터 시작해서 언어 라이브러리에도 그야말로 대대적인 칼질이 가해져야 하는 건 사실일 것이다. 그런데 그렇다고 해서 있는 CPU의 기능을 컴파일러가 활용하지 않는 건 좀 문제가 있어 보이는데?
인텔 컴파일러 같은 다른 벤더 제품 중에는 long double을 쓸 수 있는 놈이 있는지 궁금하다.

2. 오늘날 거의 모든 IDE와 에디터들은 탭을 customize할 수 있다.
화면에 표시되는 탭 길이를 조절하고(보통 거의 다 4를 쓰지만), 코딩용 자동 들여쓰기를 할 때 공백을 삽입할지 탭을 삽입할지를 지정할 수 있다. 그리고 언어별로 어떤 탭 설정을 사용할지도 지정 가능하다.

그런데 여기서 한 발 더 나아가서, 읽어들이는 소스 코드의 형태를 보고 탭 컨벤션을 자동 감지하게 할 수는 없나?
space로 맞춰져 있는 소스 코드에다가 눈치 없게 탭으로 들여쓰기를 삽입한다거나 혹은 그 반대로 하는 것. 불편하다.

자동 들여쓰기를 구현했을 정도라면 앞뒤의 중괄호가 어떻게 돼 있고 whitespace들이 space인지 tab인지 주변 context들은 다 파악했다는 뜻이다.
따라서 조금만 더 센스 있게 동작하게 만드는 것은 마치 코드의 줄바꿈 문자의 종류를 자동 감지하는 것만큼이나 그렇게 어려운 일이 아니리라 여겨진다.

Posted by 사무엘

2012/07/29 08:33 2012/07/29 08:33
, ,
Response
No Trackback , 8 Comments
RSS :
http://moogi.new21.org/tc/rss/response/713

« Previous : 1 : ... 4 : 5 : 6 : 7 : 8 : 9 : 10 : 11 : 12 : ... 13 : Next »

블로그 이미지

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

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

«   2024/04   »
  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:
2676033
Today:
601
Yesterday:
2124