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

컴퓨터 소프트웨어의 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

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

마이크로소프트 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

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

문자의 집합인 문자열(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

요즘 코딩 잡설

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

프로그래밍 언어가 제공하는 기본 라이브러리에는 단순히 자주 쓰이는 자료 구조나 알고리즘 외에도, 운영체제에다 요청을 해야 지원받을 수 있는 기능이 일부 있다. 메모리를 할당하거나 파일을 읽고 쓰는 작업이 대표적인 예이다. C/C++ 라이브러리라 해도 그런 기능은 궁극적으로 Windows API 같은 저수준 API를 호출함으로써 제공하는 셈이다.

그러니 프로그래머로서는 굳이 이식성을 염두에 두고 작성하는 코드가 아니라면, 언어가 제공하는 API보다 운영체제가 제공하는 API를 직통으로 쓰는 게 성능면에서 낫지 않나 하는 생각을 하게 된다.
이게 완전히 잘못된 생각은 아니다. 그러나 그렇지 않은 경우도 있으므로 주의해야 한다.

예를 들어, 윈도우 API에 있는 ReadFile/WriteFile과, C 라이브러리에 있는 fread와 fwrite를 생각해 보자.
C 라이브러리의 소스를 보신 분은 있겠지만, 일례로 fwrite는 내부적으로 _write 함수를 호출하는 형태이고, 두 함수만 해도 소스 코드가 수백 줄에 달한다. 뭔가 추상화 계층을 거치는 게 있고 복잡하다. 그러면서 _write 함수의 한쪽 구석에 결국은 WriteFile 함수를 호출하는 부분이 있다. fwrie가 WriteFile 직통보다 빠를래야 빠를 수가 없어 보인다.

그런데 윈도우 환경에서 프로그래밍을 오래 해 본 분은 경험적으로 아시겠지만, 몇 바이트짜리 소량의 I/O를 수백, 수천 번씩 반복해서 시켜 보면 fread/fwrite가 ReadFile/WriteFile보다 훨씬 더 빠르게 수행된다.
그렇다. C 함수는 내부적으로 버퍼링? 캐싱?을 해서 소량의 I/O는 뭉쳤다가 몰아서 한꺼번에 하는 반면, 운영체제 API는 곧이곧대로 매번 오버헤드를 감수하면서 I/O를 직통으로 하기 때문이다.

물론, 요즘은 운영체제가 자체적으로 디스크 캐싱을 다 하는 게 대세이지만, C 함수는 더 상위 계층에서도 캐싱을 하는 걸로 보인다. 이게 성능 차이가 굉장히 많이 난다.
<날개셋> 한글 입력기에서 1년 전쯤에 공개된 지난 6.2 버전의 README를 보면, 편집기의 파일 저장 및 변환기의 변환 속도가 훨씬 더 빨라졌다고 적혀 있다. 이것의 비결이 바로 저 특성을 이용해서 파일 I/O 속도를 향상시킨 것이었다.

메모리 할당도 마찬가지이다.
운영체제는 프로세스마다 heap이라는 가상 메모리를 둬서 프로그램이 다수의 작은 메모리 덩어리를 동적으로 요청할 때 빨리 빨리 반응할 수 있게 하고 있다. 연결 리스트나 트리 같은 자료구조는 메모리 할당이 잽싸게 안 되면 성능이 크게 떨어질 테니 말이다.
(이때 heap은 자료 구조 heap하고는 전혀 관계 없는 개념이므로 혼동하지 말 것.) 그래서 윈도우 운영체제에서 C 라이브러리의 malloc 계열 함수는 HeapAlloc이라는 API 함수를 호출하는 상위 계층이다.

내 경험상으로는 요즘의 NT 커널 윈도우는 HeapAlloc와 malloc, 그리고 HeapFree와 free가 성능 차이가 거의 느껴지지 않는다. 그러나 과거의 윈도우 9x 시절에는 그렇지 않았다.
“윈도우 9x에서는 이 함수는 진짜로 작은 메모리 블록에만 최적화되어 있기 때문에, 이걸로 수 MB에 달하는 메모리를 한꺼번에 여러 번 할당하면 성능이 크게 떨어지고 프로그램이 느려짐. 그 경우엔 다른 메모리 할당 함수를 쓰기 바람.”이라는 경고문이 MSDN에 명시되어 있었다.

내부적으로 그 함수가 어떻게 구현되어 있는지는 잘 모르겠지만, 내가 테스트 해 보니 진짜 그랬다. 9x에서는 프로그램이 뻗은 게 아닌가 싶을 정도로 도저히 견딜 수 없이 느려졌다.
이때에도 윈도우 API가 아닌 C 라이브러리의 malloc 함수는 랙 없이 잘 동작했다. 대용량 메모리 할당 요청이 왔을 때 가상 메모리 주소를 다시 잡는 등 대비가 되어 있어서 그런 것 같다.

원론적으로야 추상화 계층이 있는 언어 API보다는 운영체제 API 직통이 더 빠를 수밖에 없는 게 맞다. 사실, Windows API로도 모자라서 NTDLL처럼 아예 문서화되어 있지도 않은 곳에 있는 native API를 사용하는 프로그램이 있기도 하고 말이다.

그러나 프로그램의 이식성까지 희생하면서 굳이 직통 API를 쓰고자 한다면, 위에서 예를 들었듯이, 그 API의 특성을 잘 알고 쓰는 게 무엇보다도 중요하다고 하겠다. C++ 라이브러리야 객체지향 구현을 위해서 bloat되는 게 불가피하다고 쳐도, 그보다는 더 단순한 C 라이브러리의 추상화 계층은 그저 불필요한 잉여밖에 없는 건 아닐 것이기 때문이다.

Posted by 사무엘

2012/08/20 08:25 2012/08/20 08:25
, ,
Response
No Trackback , 4 Comments
RSS :
http://moogi.new21.org/tc/rss/response/722

※ 프로세서 정보 얻기

현재 컴퓨터의 CPU 아키텍처 종류를 얻는 대표적인 함수는 GetSystemInfo이다. SYSTEM_INFO 구조체의 wProcessorArchitecture 멤버의 값을 확인하면 된다.
그런데, 64비트 컴퓨터에서 64비트 운영체제를 실행하고 있더라도 32비트 프로그램은 언제나 이 값이 0, 즉 32비트 x86이 돌아온다. 이는 호환성 차원에서 취해진 조치이다. 기존의 32비트 x86용 프로그램은 새로운 API를 쓰지 않으면 자신이 64비트 x64에서 돌아가고 있다는 걸 까맣게 모르며, 전혀 알 수 없다.

자신이 돌아가고 있는 환경이 진짜 x64인지 확인하려면 GetNativeSystemInfo라는 새로운 함수를 써야 한다. 이건 Windows가 최초로 x64 플랫폼을 지원하기 시작한 윈도우 XP에서 추가되었다. 이 함수가 존재하지 않는 운영체제라면 당연히 64비트 환경이 아니다.

64비트 프로그램이라면 그냥 기존의 GetSystemInfo만 써도 x64를 의미하는 9가 돌아온다. GetNativeSystemInfo는 동일한 코드가 32비트와 64비트로 컴파일되더라도 모두 정확한 동작을 보장한다는 차이가 존재할 뿐이다.

또한, 같은 64비트라도 아이테니엄(IA64) 환경에서는 기존 GetSystemInfo도 32비트 x86 프로그램에서 아키텍처를 x86이라고 속이지 않고 정확하게 IA64라고 알려 준다. 왜냐하면 IA64는 x86과는 명백하게 다른 환경이기 때문에 다르다는 걸 알려 줄 필요가 있기 때문이다. 뭐, 지금은 IA64는 완전히 망했기 때문에 일반인이 접할 일이 없겠지만 말이다.

※ 시스템 메모리 정보 얻기

메모리 양을 얻는 전통적인 함수는 GlobalMemoryStatus이다.
그러나 32비트 프로그램이라도 현재 컴이 64비트 운영체제를 사용하여 램이 4GB보다 많이 있는 걸 제대로 감지해서 표시하려면, 윈도우 2000에서 새로 추가된 GlobalMemoryStatusEx 함수를 써야 한다.

그리고 빌드되는 실행 파일의 헤더에 large address aware 플래그가 켜져 있어야 한다. 비주얼 C++ 기준 Linker → System → Enable Large Addresses를 yes로 지정해 주면 된다. 64비트 플랫폼에서는 이 값이 기본적으로 yes이지만, 32비트 플랫폼에서는 기본값이 no이다.
large address aware이 켜져 있지 않으면 32비트에서는 사용 가능한 가상 메모리가 아예 4GB가 아닌 2GB로 반토막이 난 채 표시된다. 포인터의 최상위 1비트를 비워 준다.

그리고 64비트 바이너리에 대해서는 사용 가능한 가상 메모리의 양이야 언제나 있는 그대로 운영체제가 알려 주지만, 해당 바이너리에 이 플래그가 없으면, 운영체제는 아예 상위 32비트를 비워 줘서 DLL 같은 걸 LoadLibrary해도 언제나 32비트 영역 안에서만 주소를 잡는다. 포인터까지 4바이트짜리 int와 구분 없이 작성된 구식 코드들의 64비트 포팅을 수월하게 해 주기 위한 조치이다.

참고로 64비트 전용 프로그램이라면 Ex 대신 기존의 GlobalMemoryStatus만 써도 괜찮다. 받아들이는 구조체의 크기가 int가 아니라 SIZE_T이기 때문에, 32비트 플랫폼에서는 32비트이지만 64비트 플랫폼에서는 자동으로 64비트가 설정되기 때문이다. Ex 함수는 플랫폼의 비트 수에 관계없이 숫자의 크기가 언제나 64비트 크기를 보장해 줄 뿐이다.

※ 32비트 프로그램이 지금 내가 64비트 운영체제에서 동작하고 있는지 감지하기

딱 그 목적을 위해 IsWow64Process라는 함수가 있다. 이것 역시 윈도우 XP 이상에서 추가되었다.

※ 윈도우 시스템 디렉터리에 접근하기

64비트 운영체제는 잘 알다시피 시스템 디렉터리가 64비트용과 32비트용으로 두 개 존재한다.
32비트와 64비트 프로그램에 관계없이 GetSystemDirectory는 언제나 C:\Windows\system32를 되돌린다.
그리고 윈도우 XP에서 추가된 GetSystemWow64Directory라는 함수가 있어서 역시 32비트와 64비트에 관계없이 C:\Windows\SysWow64를 되돌린다. 다만, 운영체제 자체가 64비트가 아닌 32비트 에디션이라면, 후자의 함수는 에러를 리턴한다.

그러니 의외로 이 함수는 플랫폼에 관계없이 절대적으로 같은 결과를 되돌리는 듯한데, 문제는 64비트 운영체제는 32비트 프로그램에 대해 시스템 디렉터리를 기본적으로 redirection한다는 것이다. 즉, 64비트 운영체제는 32비트 프로그램이 C:\Windows\System32를 요청한다고 해도 SysWow64의 내용을 보여주지 진짜 64비트용 시스템 디렉터리의 내용을 보여주지 않는다.

만약 32비트 기반으로 응용 프로그램 설치 관리자나 파일 유틸리티 같은 걸 만들 생각이어서 진짜로 64비트 시스템 디렉터리에 접근을 하고 싶다면, 운영체제에다 별도의 함수를 호출해서 요청을 해야 한다. 그래서 처음에는 Wow64EnableWow64FsRedirection라는 함수가 추가되었다. 이걸로 잠시 예외 요청을 한 뒤, 내가 할 일이 끝난 뒤엔 다시 설정을 원상복귀해야 했다. 왜냐하면 64비트 시스템 디렉터리에 접근 가능하게 해 놓은 예외 동작을 그대로 방치하면, 나중에 다른 32비트 모듈들이 32비트 시스템 디렉터리에 접근하지 못하게 되기 때문이다.

그런데 MS에서는 함수 디자인을 저렇게 한 것을 후회하고, 위의 함수의 기능을 Wow64DisableWow64FsRedirection과 Wow64RevertWow64FsRedirection 쌍으로 대체한다고 밝혔다. MSDN을 읽어 보면 알겠지만, 64비트 접근 여부 설정치를 마치 stack처럼 다단계로 저장했다가 다시 원상복귀를 더 쉽게 할 수 있게 만들려는 의도이다.

※ Program Files 디렉터리에 접근하기

64비트 운영체제는 응용 프로그램 디렉터리도 64비트용과 32비트용이 두 개 존재한다.
운영체제가 사용하는 특수 디렉터리의 위치를 얻어 오는 함수의 원조는 SHGetSpecialFolderPath이며, 이것은 윈도우 운영체제의 셸의 구조가 크게 바뀌었던 인터넷 익스플로러 4 시절에 처음 도입되었다. 그때는 특수 디렉터리들을 CSIDL이라는 그냥 정수 ID로 식별했다.

그랬는데 윈도우 비스타부터는 이 함수의 역할을 대체하는 SHGetKnownFolderPath라는 함수가 추가되었고, 이제는 식별자가 아예 128비트짜리 GUID로 바뀌었다. 문자열 버퍼도 구닥다리 260자짜리 고정 배열 포인터를 받는 게 아니라, 깔끔하게 별도의 동적 할당 형태가 되었다.

64비트 운영체제에서 64비트 프로그램은 64비트와 32비트용 Program Files 위치를 아주 쉽게 얻어 올 수 있다. 32비트를 가리키는 식별자가 따로 할당되어 있기 때문이다. 그러나 32비트 프로그램이 64비트 운영체제의 64비트 위치를 얻는 것은 위의 두 함수로 가능하지 않다. SpecialFolder 함수는 64비트만을 가리키는 식별자 자체가 없으며, KnownFolder함수도 32비트 프로그램에서 FOLDERID_ProgramFilesX64 같은 64비트 식별자를 사용할 경우 에러만 돌아오기 때문이다.

32비트 프로그램이 64비트 Program Files 위치를 얻는 거의 유일한 공식적인 통로는 의외의 곳에 있다. 바로 환경변수이다.

::ExpandEnvironmentStrings(_T("%ProgramW6432%\\"), wt, 256);

위의 환경변수를 사용한 코드는 32비트와 64비트에서 동일하게 64비트용 Program Files 위치를 되돌려 준다.


결론

이렇듯 32비트에서 64비트로 넘어가면서 윈도우 API의 복잡도와 무질서도는 한층 더 높아졌음을 우리는 알 수 있다. 가능한 한 급격한 변화와 단절을 야기하지 않으면서 새로운 기능을 조심스럽게 추가하려다 보니 지저분해지는 건 어쩔 수 없는 귀결이다.

프로그램 배포 패키지를 32비트 exe 하나만 만들어서 64비트와 32비트 플랫폼에서 모두 쓸 수 있게 하면 좋을 것 같다. 32비트 플랫폼에서는 32비트 바이너리만 설치되고, 64비트 플랫폼에서는 비록 32비트 EXE라도 64비트 프로그램 디렉터리들을 모두 건드릴 수 있어야 한다. 그런 프로그램을 만들려면 이 글에서 언급된 테크닉들을 모두 알아야 할 것이다. 설치 프로그램이니 UAC 관리자 권한이 필요하다는 manifest flag도 내부적으로 넣어 주고 말이다.

아, 그러고 보니, 윈도우 9x 시절에는 시스템 디렉터리가 16비트와 32비트로 나뉘어 있지도 않았다. NT 계열로 와서야 system과 구분하기 위해서 system32가 별도로 생기긴 했지만, 16비트용 시스템 디렉터리의 위치를 얻는 별도의 API는 존재하지 않았으며, 사실 필요하지도 않았다. 16비트 프로세스는 이제 NTVDM 밑에서 돌아가는 완전 고립된 별세계로 전락했기 때문이다.

Posted by 사무엘

2012/06/14 08:22 2012/06/14 08:22
, ,
Response
No Trackback , 5 Comments
RSS :
http://moogi.new21.org/tc/rss/response/695

GDI+에 대하여

GDI+는 잘 알다시피 전통적인 윈도우 API가 제공하는 GDI에서 더 나아가, 더 향상된 그래픽과 더 깔끔해진 프로그래밍 패러다임을 제공하는 그래픽 API이다. 사실, 닷넷에서는 애초에 기본 그래픽 API가 GDI+이다. (System.Drawing 네임스페이스)

GDI+는 벌써 10년 전, 닷넷 프레임워크가 첫 등장하고 윈도우와 오피스에 XP라는 브랜드가 붙던 시절에 도입되었다. MS가 제공하는 API로는 흔치 않게 C언어 함수도 아니고 그렇다고 DirectX 같은 COM도 아닌, C++ 언어 형태로 되어 있다. 물론, 그래도 실제로 링크를 해 보면 symbol들은 다 C언어 함수 호출 형태로 된다. C++ 클래스는 단순히 C 함수를 호출하는 wrapper인 것이다.

GDI+는 여러 편리한 기능을 많이 제공하지만, 무엇보다도 벡터 그래픽에 안티앨리어싱을 넣기 위해서라도 쓰지 않을 수 없다. 이런 간단한 기능은 그냥 기존 GDI 함수에다가도 옵션을 확장해서 좀 넣어 주지 하는 아쉬움이 있다. 재래식 GDI로도 안티앨리어싱된 텍스트는 얼마든지 찍을 수 있는 것처럼 말이다. (LOGFONT 구조체에 글꼴의 품질을 지정하는 추상화 계층이 있기 때문에, 나중에 추가된 안티앨리어싱 기능도 얼마든지 지정 가능)

재래식 GDI는 열악하던 컴퓨터 환경에서 최대한 장치 독립적인 추상적인 그래픽 계층을 구현하는 게 목표였다. 그래서 래스터 그래픽보다는 벡터 그래픽에, 애니메이션보다는 정적인 그래픽에 초점이 가 있었다. 그 추상화 계층이 확장성 측면에서 편리한 점은 분명 있었지만, 색깔을 하나 바꾸려고 해도 펜이나 브러시를 다시 만들고, 도스 시절의 그래픽 프로그래밍 때는 할 필요가 없었던 GDI 객체 관리를 해야 하니 상당히 불편했다.

그래서 GDI는 게임 그래픽용으로는 적합하지 않은 구석이 있었다. 물론, 지금과 같은 틀을 유지하면서도 JPG/PNG 이미지를 지원하고, 알파 채널 비트맵 Blit이나 별도의 gradient fill 함수를 추가하고, 윈도우 2000처럼 펜이나 브러시의 색깔을 손쉽게 바꿀 수 있는 DC pen/DC brush 같은 기능을 stock object로 넣는 등, 기능 개선이 꾸준히 진행돼 왔다. 하지만 MS 측에서는 이에 만족하지 못하고 이 참에 API를 근본적으로 갈아엎고 싶다는 욕망을 느꼈던 모양이다.

GDI+는 모든 API가 자신만의 별도의 namespace 안에 선언되어 있으며, POINT 같은 간단한 자료형도 자신만의 것을 재정의하여 쓸 정도로 기존 윈도우 API와는 거리를 두고 만들어졌다. 그리고 C++답게 같은 함수도 다양한 overload 버전이 존재하며, 좌표는 정수뿐만이 아니라 실수로도 받기 때문에 편리하다.

사소한 것이다만, 글자를 찍을 때 null-terminated string에 대해서 글자 길이 지정을 생략해도 되는 것 역시 마음에 든다.
전통적으로 윈도우의 GDI 함수들은 글자를 찍는 함수들은 문자열 길이를 반드시 지정해 주게 되어 있다. 왜냐하면 한 null-terminated string을 부분적으로 여러 줄에 걸쳐 찍어야 할 일도 있기 때문이다.

그러니 그런 API 디자인이 수긍은 가지만, 어차피 한 줄밖에 찍을 일이 없는 문자열을 매번 _wcslen 해 주는 것도 귀찮지 않은가. 예전에는 gdi가 아니라 user 계층에 있는 DrawText 같은 고수준 함수나 문자열 길이 지정을 -1로 생략이 가능했던 반면, GDI+는 이 정책이 좀 더 확대되었다.

GDI+는 GDI에 비해서 state machine으로서의 의미가 크게 퇴색했다. 그래서 그리기에 필요한 모든 정보들을 함수 호출 때 매개변수로 일일이 전달해 줘야 하는 경우가 많다. 가령, current position이라는 개념이 없기 때문에 MoveTo와 LineTo 따로가 아니며, SelectOjbect라는 개념도 없어져서 그리기 함수 때 매번 펜이나 브러시에 해당하는 개체를 따로 공급해 줘야 한다.

이런 디자인은 편리한 점도 있지만, 당장 화면에 뭔가를 찍는 드로잉 말고 벡터 path를 기록한다거나 메타파일 같은 걸 만들 때는, 내가 보기에 좀 불편하게 작용하는 점도 있는 것 같다. 가령, GDI에서는 똑같이 HDC이고 여기에다가 BeginPath를 해 주면 그때부터 path 그리기 모드로 GDI가 상태 관리를 하면서 동작한다. 그러던 것이 GDI+에서는 Graphics와 GraphicsPath라고 클래스가 아예 갈라졌다. 두 개체를 상태별로 분리한 건 분명 잘한 디자인이라는 거 인정한다.

하지만 Graphics 말고 GraphicsPath는 어차피 예전 위치에서 계속해서 이어서 그래픽을 기술하는 게 많은 만큼, 재래식 GDI처럼 current position이 있는 게 편리하지 않을까 싶다. 지금 API 체계에서는 직전 위치에 대한 정보를 응용 프로그램이 계속 공급해 줘야 한다.

또한, 복잡한 path를 화면에다 그릴 때, 예전 GDI는 지금 DC가 가지고 있는 펜과 브러시로 윤곽선을 그리고 내부를 채우는 것을 함수 호출 한 번으로 동시에 할 수 있었다. 그러나 GDI+는 선을 그리는 것과 내부를 채우는 것을 따로 해야 한다. path의 경계를 추출하여 래스터라이즈하는 것은 상당히 복잡한 계산이 필요한 작업인데, 동일한 작업이 비효율적으로 중복 적용되는 건 아닌지 우려된다.

즉, 본인은 GDI+에 대해서 참신한 기능은 분명 마음에 든다. 이 글에서 언급된 것 말고도 여러 고급 기능들이 있다. 윈도우 비스타 Aero와 연동하는 일부 드로잉 기능(가령, 클라이언트 영역에도 반투명 Aero 효과를 추가하고, 거기에다 글자를 찍는 것)은 오로지 GDI+로만 접근해야 하는 것도 있다.

하지만 (1) 그냥 재래식 GDI API에다가 옵션을 추가하는 형태로 구현했어도 충분해 보이는 것, (2) GDI+가 바꿔 놓은 API 디자인이 오히려 좀 불편하고 비효율적일 수도 있겠다 싶은 것에 대해 비판적인 안목을 갖고 있다. 속도가 재래식 GDI보다 꽤 느린 건 차치하고라도 말이다.

Posted by 사무엘

2012/04/28 08:39 2012/04/28 08:39
, ,
Response
No Trackback , 4 Comments
RSS :
http://moogi.new21.org/tc/rss/response/675

« Previous : 1 : ... 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:
2679106
Today:
1190
Yesterday:
2484