« Previous : 1 : ... 17 : 18 : 19 : 20 : 21 : 22 : 23 : 24 : 25 : ... 31 : Next »

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

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

남이 짠 레거시 코드를 업무상 들여다보다가 우연히 발견한 건데,
아래의 코드는 비주얼 C++에서는 컴파일되지만 gcc 계열의 다른 컴파일러에서는 컴파일되지 않는다.

class A {
public:
    class B;
};

class A::B {
public:
    A::B() { }
};

일단, 클래스 내부의 하위 클래스를 저런 식으로 forward 선언했다가 나중에 다시 선언하는 문법 자체를 본인은 직접 구사한 적이 전혀 없었다.
그랬는데, 어찌된 일인지 생성자 함수를 선언할 때 클래스 A까지 다 써 주는 것을 비주얼 C++은 허용하지만 다른 컴파일러들은 그러하지 않았다.
왜 그런지, 이와 관련된 표준 규정은 없는지 궁금하다.

2.

Lyn Tono 님의 블로그를 보다가, 평소에 미처 생각도 못 했던 흥미로운 사실을 발견하여 이곳에다가도 소개하겠다.

void foo() { puts("global function"); }

template<typename T>
class C {
    T m;
public:
    //static이든 아니든 사실 상관은 없음
    static void foo() { puts("Member function"); }
};

template<typename T>
class D: public C<T> {
public:
    void bar() { foo(); }
};

위와 같은 함수와 템플릿 클래스를 선언한 뒤 D<int> q; q.bar(); 라고 실행하면,
비주얼 C++에서는 클래스에 소속된 foo 멤버 함수가 불리지만, 역시 gcc 계열의 다른 컴파일러에서는.. 놀랍게도 global 함수가 불린다!
이건 우선순위 문제도 아닌지라, global 함수를 없앤다고 해서 멤버 함수가 차선으로 지명되지도 않는다. 그 경우에도 클래스 멤버는 존재가 무시되고 그냥 컴파일 에러가 난다. -_-;;

그렇다. 템플릿 클래스는 기반 클래스의 멤버 지명이 비템플릿 클래스처럼 그렇게 쉽게 되질 않는다.
this-> (멤버. 심지어 this 포인터가 존재하지 않는 static 함수이더라도) 혹은 :: (전역)을 명시적으로 써 줘야 한다.

내가 생업을 위한 코딩을 하는데 비주얼 C++을 벗어날 일은 거의 없지만, 저 상황에서 당연히 멤버 함수가 불릴 거라고 예상한 게 다른 컴파일러에서는 그렇지 않을 수도 있다는 걸 염두에 둬야겠다.

3.

예전에 비주얼 C++이 지원하는 for each 문에 대해 소개를 했었고, 친절하게 의견을 남겨 주신 김 진 님으로부터 다른 컴파일러에도 range-based for 문에 대한 비슷한 문법이 존재한다는 보충 설명도 들었다.

그런데 비주얼 C++의 경우, for each() 내부에서 중괄호 없이 if...else문을 썼는데, else부터가 왜 인식이 안 되지? 최신 2012까지도. 아무래도 버그가 아닌지 의심된다. 아래의 예들을 보시라.

for(i=0; i<10; i++) if(a) b(); else c(); //원래 OK

for each(auto i in container) if(a) b(); else c(); //ERROR: 이것만 안 될 이유가 없잖아? 왜? 게다가 인텔리센스 컴파일러는 이를 에러로 지적하지 않음.

for each(auto i in container) { if(a) b(); else c(); } //중괄호를 해 주면 OK

for each(auto i in container) a ? b(): c(); //차라리 이렇게 하는 것도 OK

for(auto i: container) if(a) b(); else c(); //xcode의 이 문법도 당연히 아무 문제 없이 OK. 참고로 비주얼 C++도 2012부터는 for each뿐만 아니라 이 문법도 지원하기 시작했으며, else문에 이상이 없다.

웹 표준만큼이나 C++ 표준도 세밀한 부분에서의 이행 여부가 컴파일러마다 케바케인 것 같다.
옛날엔 IE6이 웹 표준을 안 지킨다고 욕 얻어먹은 것만큼이나 VC6도 C++ 표준 미준수 때문에 많은 비판을 받았었다. for 문 안에서 선언한 변수의 scope 문제가 제일 유명하고 말이다.

하지만 표준 자체가 모호하거나 아예 커버하지 않고 있는 사항이 있다면 그저 묵념..

여담이지만, 2차원 배열을 순회하는 경우 비주얼 C++의 for each는 배열 안의 각 원소를 하나씩 일일이 순회하는 반면, for(:) 문은 각 배열의 포인터를 변수에다 넘겨 주면서 여전히 1차원적으로만 순회한다는 차이도 있다.

4.

C/C++에서 작은따옴표는 문자 상수를 나타낸다. sizeof('a')의 값이 C에서와 C++에서 서로 다르다는 건 이미 잘 알려진 사실. 그런데 작은따옴표 안에 탈출문자가 아닌 일반 문자가 둘 이상 중첩되는 게 문법적으로 가능하며, 에러가 아니다. 그리고 더욱 기괴한 것은, 그렇게 중첩되었을 때 이 문자 상수가 갖는 값은 표준으로 정해져 있지 않고 컴파일러 구현체가 해석하기 마음대로라는 것! 일부러 그렇게 규격을 '미정'으로 남겨 놨다.

대부분의 컴파일러에서는 'ab'를 0x4142라고 합성해서 인식하는 식의 배려는 해 주고 있다. 그러나 이것은 애초에 표준 동작이 아니다 보니, 컴파일러의 기반 아키텍처 또는 코드 생성 대상 아키텍처의 엔디언에 따라 세부적인 동작이 달라진다. 다시 말해 이것은 이식성을 전혀 보장받을 수 없는 지뢰밭 같은 테크닉이며, 그렇기 때문에 IOCCC 같은 대회에서 써먹을 수도 없다.

그럴 거면 둘 이상의 문자는 차라리 깔끔하게 경고나 에러 처리라도 해 주지 하는 아쉬움이 있다.
<날개셋> 한글 입력기의 수식은 문자 상수로 둘 이상의 문자를 집어넣으면 문법 에러로 간주한다.

Posted by 사무엘

2013/04/25 08:38 2013/04/25 08:38
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/822

본인이 예전에 글로 썼듯, 비주얼 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

IOCCC라고, 사람이 가장 알아 보기 힘들고 충공깽스러운 형태로 작성된 C 프로그램 코드를 접수받는 공모 대회가 있다.
단순 코더가 아니라 전산학 내공과 해커 기질이 충만한 레알 베테랑 프로그래머라면 이미 들어서 알 것이다.

입상작들은 내가 보기에 크게 (1) 아스키 아트형, 아니면 (2) 크기 줄이기 암호형이라는 두 갈래로 나뉜다. 대회에 공식적으로 이런 식으로 참가 부문이 나뉘어 있는 건 아니지만, 여기 참가자들이 추구하는 오덕질의 목표가 대체로 이 둘 중 한 갈래로 나뉘기 때문이다.

전자는 영락없이 아스키 문자로 사람 얼굴이나 문자 같은 그림을 그려 놨는데 그건 컴파일 되는 올바른 C 코드이다. 그뿐만이 아니라 그걸 실행하면 기가 막힌 유의미한 결과물이 나온다. 간단한 게임이라든가 원주율값 계산 같은 것부터 시작해 심지어 CPU 에뮬레이터나 간단한 컴파일러, 운영체제까지 들어있는 경우도 있다.

후자는 수단과 방법을 가리지 않고 길이를 줄이기 위해 들여쓰기, 주석, 헝가리언 표기법 따위는 다 쌈싸먹고 진짜 정체를 알 수 없는 이상한 숫자와 기호와 문자로 범벅이 된 코드인데, 빌드해 보면 역시 소스 코드의 길이에 비해 믿을 수 없는 퀄리티의 동작이 나온다. 자바스크립트 같은 코드를 난독화 처리한 것과 비슷한 형태가 된다.

어떤 언어에서 소스 코드 자신을 출력하는 프로그램을 콰인(Quine)이라고 부른다. GWBASIC이라면 언어에 LIST라는 명령이 있으니 쉽겠지만, 일반적인 컴파일 기반 언어에서는 그걸 만드는 게 보통일이 아니다. 그런데 이 IOCCC 대회 입상작 중에는 A라는 코드가 있는데 그걸 실행하면 B라는 소스 코드가 출력되고, B를 빌드하여 실행하면 C라는 소스 코드가 나오고, 다음으로 C를 빌드하면 다시 A가 나오는... 중첩 콰인을 실현한 충격과 공포의 프로그램도 있었다. 그것도 A, B, C는 다 형태가 완전히 다르고 인간이 인식 가능한 아스키 아트! Don Yang이라는 사람이 만든 2000년도 입상작이다.

역대 수상작들을 보면 프로그래머로서 인간의 창의력과 잉여력, 변태스러움이 어느 정도까지 뻗칠 수 있는지를 알 수 있다. 그리고 이런 대회는 한 프로그래밍 언어의 극악의 면모를 시험한다는 점에서 전산학적으로도 나름 의미가 있다. 들여쓰기와 긴 변수명과 풍부한 주석이 갖춰진 깔끔한 코드든, 저런 미친 수준의 난독화 코드든 컴파일러의 입장에서는 어차피 아무 차이 없는 똑같은 코드라는 게 아주 신기하지 않은가?

다른 언어가 아니라 C는 시스템 레벨에서 프로그래머의 권한이 강력하다. 그리고 전처리기를 제외하면 특정 공백 문자에(탭, 줄바꿈 등) 의존하지 않는 free-form 언어이며, 언어 디자인 자체가 온갖 복잡한 기호를 좋아하는 오덕스러운 형태인 등, 태생적으로 난독화에 유리하다. 게다가 도저히 C 코드라고 볼 수 없을 정도로 코드의 형태와 의미를 완전히 엉뚱하게 뒤바꿔 버리는 게 가능한 매크로라는 비장의 무기까지 있다!

심지어는 C++보다도 C가 유리하다. 함수를 선언할 때 리턴 타입을 생략하고 함수 정의에서는 리턴 문을 생략할 수 있다. 가리키는 대상 타입이 다른 포인터를 형변환 없이 바로 대입할 수 있으며, 또한 인클루드를 생략하고 표준 함수를 바로 사용할 수도 있다. C++이었다면 바로 에러크리이지만, C에서는 그냥 경고만 먹고 끝이니 말이다. C의 지저분한 면모가 결국 더 짧고 알아보기 힘든 코드를 만드는 데 유리하다는 뜻 되겠다.

현업에서는 거의 언제나 C++만 써 와서 잘 실감을 못 했을 뿐이지, C는 우리가 생각하는 것보다 저 정도로 꽤 유연(?)한 언어이긴 하다. IOCCC 참가자의 입장에서 C++이 C보다 언어 구조적으로 더 유리한 건, 아무데서나 변수 선언을 자유롭게 할 수 있다는 것 정도일 것이다.

그러나 겨우 그 정도로는 불리한 점이 여전히 유리한 점보다 더 많은 것 같다. 생성자와 소멸자, 오버로딩, 템플릿 등으로 더 알아보기 힘든 함축적인 코드를 만드는 건 상당한 규모가 있는 큰 프로그램에서나 위력을 발할 것이고, 긴 선언부의 노출이 불가피하여 무리일 듯.

옛날에는 대회 규정의 허를 찌른 엽기적인 꼼수 작품도 좀 있었다.
이 대회는 1984년에 처음 시작되었는데, 그때 입상작 중에는 main 함수를 함수가 아니라 기계어 명령이 들어있는 배열로 선언해 놓은 프로그램이 있었다(1984/mullender). 이건 기계 종류에 종속적일 뿐만 아니라 요즘 컴파일러에서는 링크 에러이기 때문에, 그 뒤부터는 대회 규정이 바뀌어 이식성 있는 코드만 제출 가능하게 되었다.

그리고 1994년에는 콰인이랍시고 0바이트 소스 코드가 출품되었다(1994/smr). 소스가 0바이트이니, 아무것도 출력하지 않아도 콰인 인증..;; 이건 충분히 참신한 덕분에 입상은 했지만 그 뒤부터는 역시 소스 코드는 1바이트 이상이어야 한다는 규정이 추가되었다. 빈 소스 파일을 빌드하려면 빌드 옵션도 좀 미묘하게 변경을 해야 했다고 한다.

이런 코드를 작성하기 위해서는 모든 변수와 함수를 한 글자로 표현하는 것부터 시작해서 평범한 계산식을 온갖 포인터와 비트 연산자로 배배 틀기, 숫자 테이블 대신 문자열 리터럴을 배열로 참고하기(가령, "abcd"[n]) 같은 건 기본 중의 기본 테크닉이다. 그리고 그걸 아스키 아트로 바꾸는 능력이라든가, 원래 오리지널 프로그램을 기가 막히게 짜는 기술은 별개이다. 이런 코드를 만드는 사람은 정말 코딩의 달인 중의 달인이 아닐 수 없다.

이 대회는 전통적으로 외국 해커 덕후들의 각축장이었다. 그러나 지난 2012년도 대회에서는 자랑스럽게도 한국인 입상자가 한 명 배출되었는데, 본인의 모 지인이다. 그가 출품한 프로그램은 영어로 풀어 쓴 숫자를 입력하면(가령, a hundred and four thousand and three hundred and fifty-seven) 그걸 아라비아 숫자로 바꿔 주는 프로그램(104357). 코드를 보면 저게 어딜 봐서 숫자 처리 프로그램처럼 생겼는가. -_-

코드를 대충 살펴보면, long long이 바로 등장하는 데서 알 수 있듯, 나름 32비트 범위를 벗어나는 큰 자리수까지 지원한다. 문자열 리터럴을 배열로 참고하는 것도 곧바로 쓰였음을 알 수 있다.
그리고 옛날의 C 시절에 허용되었던 관행이었다고 하는데, 함수의 인자들을 아래와 같은 꼴로 선언하는 게 이 대회 출품작에서는 종종 쓰인다고 한다.

int func(a,b) int a, char *b; { ... }

하긴, C/C++이 기괴한 면모가 자꾸 발견되는 건 어제오늘 일이 아니다.
a[2]뿐만이 아니라 2[a]도 가능하다든가,
#include 대상으로 매크로 상수도 지정 가능하다든가,
C++의 default argument로 0이나 -1 같은 것뿐만 아니라 사실은 아예 함수 호출과 변수 지정도 가능하다는 것..
switch문의 내부에 for 같은 다른 반복문이 나온 뒤에 그 안에 case가 있다던가..;;

정말 약 빨고 만든 언어에다 약 빨고 코딩한 개발자라고밖에 볼 수 없다.
나로서는 범접할 수조차 없는 이상한 프로그래밍 대회에 한동안 엄청 관심을 갖더니 결국 입상해 버린 그의 오덕력에 경의를 표한다. 그저 놀라울 뿐이다. 이 정도로 소개하고 띄워 줬으니, 그분이 이 자리에 댓글로 소환되는 걸 기대해 보겠다. 아무래도 한국인 다윈 상 수상자가 배출된 것보다는 훨씬 더 자랑스러운 일을 해낸 친구이지 않은가. ㄲㄲㄲㄲㄲㄲㄲ

뭐, 입상했다고 당장 크게 부와 명예가 뒤따르는 건 아니겠지만, 팀장이나 임원이 IOCCC에 대해서 아는 개발자 출신인 회사에 지원할 때 “나 이 대회 입상자요!”라고 이력서에다 써 넣으면 그 이력서의 메리트는 크게 올라갈 수밖에 없을 것이다. 실제로 IOCCC 같은 잉여로운 대회에 참가하는 geek 중에는 구글, MS급 회사 직원도 있고, 사실 이런 대회에 입상할 정도의 guru급 프로그래머가 일자리를 못 구해 걱정할 일은 절대 없을 테고 말이다.

이런 대회에 더 관심 있으신 분은, IOCCC의 국내 저변 확대를 위해 애쓰고 있는 저 친구의 소개 페이지를 참고하시기 바란다.

Posted by 사무엘

2013/04/10 19:20 2013/04/10 19:20
, , , ,
Response
No Trackback , 6 Comments
RSS :
http://moogi.new21.org/tc/rss/response/816

지금으로부터 거의 3년 전, 이 블로그가 개설된 지 얼마 되지 않았던 시절에 본인은 C++의 매우 기괴-_-한 문법인 다중 상속멤버 포인터(pointer-to-member)에 대해서 제각각 따로 글로 다룬 적이 있었다.
이제 오늘은, 그 기괴한 두 물건이 한데 합쳐지면 언어의 디자인이 얼마나 더 흉악해지는지를 보이도록 하겠다.
그 내력을 알면, C++ 이후의 객체지향 언어에서 다중 상속이 왜 봉인되어 버렸는지를 이해할 수 있을 것이다. 뭐, 이미 다 아는 분도 있겠지만 복습 차원에서.

클래스의 멤버 포인터는 그 가리키는 대상이 변수이냐 함수이냐에 따라서 내부 구조가 크게 달라진다는 말을 예전에 했었다. 함수일 때는 포인터답게 말 그대로 실행될 함수의 메모리 위치를 가리키지만, 변수일 때는 이 멤버가 this로부터 얼마나 떨어져 있는지를 나타내는 정수 오프셋에 불과하다. &POINT::x 는 0, &POINT::y는 4 같은 식.
그래서 비주얼 C++은 x64 플랫폼에서도 단순 클래스의 멤버 변수 포인터는 뜻밖에도 8바이트가 아닌 4바이트로 처리한다. UNT_PTR이 아니라 그냥 unsigned int라고 본 것이다.

그런데 다중 상속이 동반된 클래스는 '단순' 클래스라고 볼 수가 없어지며, 그런 클래스를 대상으로 동작하는 멤버 포인터는 내부 메커니즘이 굉장히 복잡해진다. 멤버 변수야 오프셋이 바뀌니까 그렇다 치지만, 멤버 함수의 포인터도 데이터 오프셋의 영향을 받는다. 비록 함수 자체는 오프셋을 타지 않고 고정된 메모리 주소이긴 하지만, 멤버 포인터가 어느 함수를 가리켜 부르느냐에 따라 그때 그때 this 포인터를 잘 보정해서 줘야 하기 때문이다.

다음 코드를 생각해 보자.
참고로, class 대신 struct를 쓴 이유는 public: 을 따로 써 주는 귀찮음을 해소하기 위해서일 뿐이다. (C#은 struct와 class의 용도가 구분되어 있는 반면, C++은 전혀 그렇지 않으므로.)

struct B {
    int valB; void functionB() { printf("functionB: %p\n", this); }
};
struct C {
    int valC; void functionC() { printf("functionC: %p\n", this); }
};

struct D: public B, public C {
    int valD; void functionD() { printf("functionD: %p\n", this); }
};

그 뒤,

D ob;
void (D::*fp)();
printf("this is %p\n", &ob);
printf("sizeof pointer-to-member is %d\n", sizeof(fp));

fp = &D::functionB; (ob.*fp)();
fp = &D::functionC; (ob.*fp)();
fp = &D::functionD; (ob.*fp)();

코드를 실행해 보면, 놀라운 결과를 볼 수 있다.
이제 fp의 크기가 포인터 하나의 크기보다 더 커졌다.
비주얼 C++ 기준으로, '포인터+int'의 합이 된다. 그래서 x86에서는 8바이트, x64에서는 12바이트.

게다가 중요한 건, functionC를 실행했을 때만 this의 값이 달라져 있다는 것이다.
이건 뭐 다중 상속의 특성상 어쩔 수 없는 면모이며, 멤버 함수를 ob.functionC()라고 직접 호출할 때는 컴파일러가 알아서 처리해 주는 기능이긴 하다.
하지만, 직접 호출이 아니라 멤버 포인터를 통한 간접 호출을 할 때는 이걸 어떻게 구현해야 할까?

결국은 멤버 함수 포인터 자체에 추가 정보가 들어갈 공간이 있어야 하고, 그 정보는 포인터에다가 함수에 대한 대입이 일어날 때 implicit하게 따로 공급되어야 한다.
다중 상속을 받은 클래스의 멤버 함수를 가리키는 포인터는 this 보정을 위한 정수 오프셋이 내부적으로 추가된다. 이제 fp는 단일 포인터 변수라기보다는 구조체처럼 바뀌었다는 뜻이다.

이 fp에다가 functionB나 functionD를 대입하면 그 멤버 함수의 주소만 대입되는 게 아니라, 숨겨진 오프셋 변수에다가도 0이 들어가며(보정할 필요가 없으므로), functionC를 대입하면 그 주소와 함께 오프셋 변수에다가도 0이 아닌 값이 같이 대입된다. 그리고 실제로 fp 호출을 할 때는 this 포인터에다가 보정이 된 값이 함수로 전달된다.

이야기는 여기서 끝이 아니다. 설상가상으로 가상 상속까지 추가된다면?
내가 클래스를 A가 아니라 B에서부터 시작한 게 이것 때문이다. 맨 앞에다가 드디어 다음 코드를 추가하고,

struct A {
    int valA;
    void functionA() { printf("functionA: %p\n", this); }
};

앞에서 썼던 B와 C도 A로부터 가상 상속을 받게 고쳐 보자.

struct B: virtual public A { ... }
struct C: virtual public A { ... }

이것도 물론 추가하고.

fp = &D::functionA; (ob.*fp)();

이렇게 해 보면..
비주얼 C++ 기준 fp의 크기는 더욱 커져서 '포인터+정수 2개' 크기가 된다. x86에서는 12바이트, x64에서는 16바이트.
다중 상속만 있을 때는 함수 말고 변수의 멤버 포인터는 크기가 변함없었던 반면, 가상 상속이 가미되면 변수 멤버 포인터도 이렇게 '크기 할증'이 발생한다. 대입 연산이나 함수 호출 때 몰래 같이 발생하는 일도 더욱 많아지며, 이 현상을 좀 유식하게 표현하면 cost가 커진다.

그 이유는 어렴풋이 유추할 수 있을 것이다. 가상 상속이라는 건 말 그대로 기반 클래스의 오프셋이 클래스의 인스턴스별로 동적으로 변할 수 있다는 뜻이다. this 포인터 보정이 뒷부분 파생 클래스의 정확한 위치를 파악하기 위해서 발생하는 일이라면, 가상 상속 보정은 앞부분 기반 클래스의 위치를 파악하는 것이 목적이다.

이런 사정으로 인해 functionA()도 원래 개체의 주소와는 다른 주소를 받으며, 이것은 functionC()가 받는 주소와는 또 다르다.
다만, pointer-to-member는 가상 함수와는 기술적으로 전혀 무관하게 동작하기 때문에, 가상 함수가 존재하는 클래스라고 해서 오버헤드가 추가되는 건 없다. 함수 멤버 포인터로 가상 함수를 가리키면, 아예 가상 함수 테이블을 참조하여 진짜 함수를 호출하는 wrapper 함수가 따로 만들어져서 그걸 가리키고 있게 된다.

요컨대 비주얼 C++은 단순 클래스, 다중 상속만 있는 클래스, 거기에다 가상 상속까지 있는 클래스라는 세 등급에 따라 멤버 포인터를 관리한다. 다만, 함수가 아닌 변수 멤버 포인터는 가상 상속 여부에 따라 두 등급으로만 나누는 듯하다. 이 정도면, 이 글을 쓰는 본인부터 이제 머리가 핑그르르 도는 것 같다.

이제 마지막으로 생각해 볼 문제가 있다. C++은 클래스의 명칭 선언만 하는 게 가능하다는 점이다.

class UnknownBase;
class UnknownDerived;

굳이 클래스의 몸체를 몰라도 이 클래스에 대한 포인터 정도는 선언이 가능하다. 그렇기 때문에 명칭 선언은 컴파일 때 헤더 파일간의 의존도를 줄이고 모듈간의 독립성을 높일 때 요긴하게 쓰이는 테크닉이다.
다만, 여러 클래스들을 명칭 선언만 하면 이들간의 상속 관계도 아직 밝혀지지 않기 때문에, 실질적인 기반 클래스와 파생 클래스 사이에 암시적인 형변환이나 static_cast, dynamic_cast 따위를 쓸 수 없다는 점도 주의해야 한다.

게다가 이렇게 명칭만 달랑 선언된 클래스에 대해서 멤버 포인터를 선언하면..
컴파일러는 이 클래스가 다중 상속이 존재하는지, 가상 상속이 존재하는지 같은 걸 알지 못한다!
그렇다고 무식하게 에러 처리하며 멤버 포인터의 선언을 거부할 수도 없는 노릇이니,
컴파일러는 가장 보수적으로 이 클래스가 어려운 요소들은 모두 갖추고 있을 거라고 생각하고 가장 덩치 크고 복잡한 등급을 선택할 수밖에 없다.

나중에 사용자가 추가 인클루드를 통해 클래스의 몸체를 선언하여, 이 클래스는 단순한 놈이라는 게 알려지더라도 한번 복잡하게 결정되어 버린 타입 구조는 다시 바뀌지 않는다.
게다가 이렇게 unknown 클래스에 대한 멤버 포인터는 단순히 '가상 상속 클래스' 등급이 아니라, 메타 정보가 추가로 붙는지 비주얼 C++에서는 함수 기준으로 x86에서 무려 16바이트를 차지하며, x64에서는 24바이트를 차지하게 된다. 포인터 둘, int 둘의 합이다.

printf("%d\n", sizeof(void (UnknownDerived::*)() ));

물론, 멤버 포인터부터가 굉장한 레어템인데, 몸체도 없이 명칭 선언만 된 클래스에 대해서 멤버 포인터를 덥석 들이대는 코딩을 우리가 실생활에서 직접 할 일은 극히 드물다. 하지만 딱 machine word와 동일한 크기를 기대했던 멤버 함수 포인터가 3~4배 크기로 갑자기 뻥튀기되고 생각도 못 했던 오버헤드가 추가되는 일은 없어야 하겠기에, 비주얼 C++은 역시 비표준 확장을 통해서 이 문제에 대한 해결책을 제시하고 있다.

그것은 바로 _single_inheritance, _multiple_inheritance, _virtual_inheritance라고 참 길게도 생긴 키워드.
클래스를 명칭 선언만 할 때

class _single_inheritance UnknownDerived;

이런 식으로 써 줌으로써 “이놈은 다중 상속 같은 귀찮은 요소가 없는 클래스이다. 따라서 얘에 대한 멤버 포인터는 추가 오프셋이 없는 제일 간단한 등급으로 만들어도 OK다”라는 힌트를 컴파일러에다 줄 수 있다.
복잡한 놈이라고 예고를 해 놓고 단순한 형태로 클래스를 선언하는 건 괜찮으나, 간단한 놈이라고 예고를 해 놓고 나중에 다중 상속이나 가상 상속을 쓰면 물론 컴파일 에러가 발생하게 된다.

아마 스타크래프트를 만든 사람도 스탑 럴커 같은 전술은 생각을 못 하지 않았을까.
저런 판타지 같은 면모는 C++을 설계한 사람이나 추후에 기능을 확장한 표준 위원회 사람들도 생각을 못 했을 가능성이 높아 보인다.
그렇기 때문에 비주얼 C++처럼 단순, 다중, 가상으로 세 등급을 나눠서 포인터에다 할증 제도를 넣고, 관련 예약어까지 추가한 건 전적으로 표준이 아니라 컴파일러 구현하기 나름이다.

아, 자세한 건 이 사이트 내용을 좀 공부하고 글을 쓰려고 했는데 도저히 다 읽을 엄두가 안 난다.
관심 있는 분들은 알아서 탐독해 보시길.
언뜻 보니, 다중 상속이 멤버 포인터보다 시기적으로 나중에 등장했다. 그래서 둘을 한꺼번에 구현하는 게 이 정도로 복잡하게 꼬인 셈이다.

Posted by 사무엘

2013/04/02 08:33 2013/04/02 08:33
,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/813

C/C++의 const 이야기

1.

C/C++에서 const라는 키워드는 어떤 변수를 선언할 때 타입과 함께 지정해 줄 수 있는 modifier 속성이다. 이와 비슷한 위상인 키워드로 volatile도 있다.

이 const의 큰 의미와 용도는 C와 C++에서 모두 동일하다. 바로, 한번 값이 정해지고 나면 그 뒤로 값이 또 바뀔 수 없다는 걸 뜻한다. 비슷한 용도로 쓰이는 매크로 상수나 enum과는 달리, const 개체는 엄연히 상수 역할을 하는 '변수'이기 때문에 L-value의 특성도 껍데기나마 지니며, 자기 주소를 & 연산자로 얻을 수 있다는 특징이 있다. (자기 주소가 있는데 왜 대입을 못 하니 ㄲㄲㄲ)

그런데 const라는 의미를 언어 차원에서 실현하는 방식이 C는 다소 느슨한 편이다.
C 언어도 const 변수에다가 대놓고 대입 연산자를 들이대는 시도 정도는 컴파일러가 에러로 대응하며 막아 준다. 그러나 강제로 const 속성을 없애는 형변환+포인터 연산 같은 것까지 저지하지는 못한다.

이는 마치, C/C++ 코드에서 변수를 초기화하지 않고 사용하는 걸 간단한 지역 변수 정도는 컴파일러가 알아서 발견하여 경고로 처리해 주지만, 복잡한 배열이나 포인터, 구조체의 경우를 일일이 체크하지는 못하는 것과 비슷한 맥락. 그래서

const int MARK = 100;
const int *p = &MARK;

printf("%d %d\n", MARK, *p);
*const_cast<int *>(&MARK) = 50;
printf("%d %d\n", MARK, *p); //이것이 문제.

이런 코드를 돌려 주면 C에서는 MARK가 처음에는 100이다가 나중에는 50이 되어 버린다! 이런 이유로 인해 C에서 const int는 껍데기만 const이지 case 문의 상수로 쓰이지도 못한다. 아, C 언어는 const_cast라는 연산자가 없으니, 그냥 *((int *)&MARK) = 50; 이라고 해야겠지만 말이다.

허나, C++은 이 정책이 바뀌어서 const를 다루는 방식이 좀 더 엄밀해졌다. 사실, 객체 지향 언어이다 보니 상수값을 취급하는 방식이 더 정확하고 엄밀해져야만 하는 게 마땅하다. 무작정 C 같은 '고수준 어셈블리' 패러다임만 추구해서는 곤란할 터이다.

C++은 MARK 변수가 차지하는 메모리에 들어있는 값과 상관없이 소스 코드에서 MARK가 그대로 쓰인 곳은 언제나 100을 대응시켜 준다. 다시 말해 위의 경우 100과 50이 출력된다. MARK와 *(&MARK)의 값이 달라지는 한이 있더라도 MARK는 언어 차원에서 처음 선언해 준 값이 그대로 유지되며, 진짜 매크로 상수처럼 쓰일 수 있다는 뜻이다. 신기하지 않은가? C와 C++ 사이의 교묘한 차이 중 하나이다. C/C++ 프로그래머라면 이 정도는 이미 아는 분이 많을 것이다.

2.

C/C++은 잘 알다시피 '선언 따로, 정의 따로'라는 좀 원시적이라면 원시적인 디자인 철학을 따르는 언어이다. 그래서 헤더에 들어간 선언은 그 선언을 사용하는 모든 번역 단위들이 include를 “매번” 해 줘야 하고, 그 선언에 대한 정의는 아무 번역 단위에다가 “한 번만” 써 주면 링크 때 알아서 말 그대로 '연결'이 된다. 그렇다, 걔네들은 원래 그런 언어이다.

자바나 C#은 클래스의 선언과 정의가 일심동체이고 그 클래스가 곧 번역 단위이다. 뭐, C++도 클래스를 선언하면서 멤버 함수의 몸체까지 헤더 파일 안에다 같이 써 주는 게 불가능하지는 않지만, 그건 간단한 인라인 함수를 만들 때에나 제한적으로 쓰이는 관행이다. 아니면 어차피 모든 클래스의 몸체가 헤더에 들어가야만 하는 템플릿일 때 정도.

자, 이런 이중적인 구조로 인해 C++은 static 멤버 변수의 정의조차도 클래스의 선언과 동시에 할 수가 없다.
여러 번역 단위에서 매번 인클루드되는 '선언부'에다가 한 번만 등장해야 하는 '정의부'가 동시에 들어갈 수는 없기 때문이다.
자바나 C#은 클래스 안에다가 static int MAX = 100; 같은 문장을 아무렇지도 않게 넣을 수 있으나, C++은 굳이 static int MAX;int CFoo::MAX = 100; 을 분리해서 써 줘야 한다.

그럼, C++의 클래스에서 멤버를 선언할 때 대입 연산자가 들어갈 일이란 오로지 순수 가상 함수를 선언할 때 쓰이는 = 0밖에 없는 걸까? (자바와 C#은 순수 가상 함수는 오히려 pure이나 abstract 같은 키워드를 따로 써서 표현함!)

놀랍게도 그렇지는 않다.
딱 하나 예외적으로, static const라는 속성을 지닌 간단한 '정수 계열'의 멤버는 클래스 안에다 선언과 함께 초기화를 하는 게 가능하다. 즉, 클래스 안에다가 static const int MAX = 100; 정도는 C++도 허용해 준다는 뜻이다.

물론 제약이 몹시 심하다.
static과 const 중 속성이 하나라도 빠져서는 안 된다. 그리고 배열이나 구조체의 초기화는 어림도 없다. static const WCHAR NAME[] = L"foo"; 같은 거 안 된다.

쉽게 말해 정수 정도면, 심벌이 있는 곳의 메모리 주소를 참고하는 게 아니라 심벌의 값 자체를 매번 집어넣어 주는 게 어차피 이득이니까 예외적으로 클래스 내부에서의 정의와 초기화가 허용되는 셈이다. 그러니 static const 정수는 그냥 메모리 주소를 얻는 게 가능한 enum 수준에 불과하다.

정수 계열은 심지어 __int64도 허용되지만 포인터는 허용되지 않는다. 그리고 부동소수점도 안 된다. static const double PI = 3.141592; 는 안 된다는 뜻이다. 이건 현재 GNU 계열 컴파일러에서만 지원하는 extension일 뿐, 표준은 아니다.

3.

한 소스 파일에다가 const 속성을 가진 커다란 정수 테이블 배열을 전역변수 형태로 만들었다. 그건 난수표가 될 수도 있고 time-critical한 실시간 계산 프로그램(게임이라든가)에서는 삼각함수나 로그값 테이블이 될 수도 있고 문자 코드 변환 테이블이 될 수도 있다.

그런데 다른 번역 단위에서는 그 테이블의 명칭을 extern으로 선언해 놓고 참고하여 사용했는데, 링크할 때는 그 명칭을 찾을 수 없다고 에러가 나는 것이었다. 본인은 그 이유를 알 수 없었다. 경험적으로 const 속성을 제거하면 문제를 피해 갈 수 있긴 했으나, 값을 변경하지 않는 상수 테이블을 일반 배열로 취급할 수도 없는 노릇이었다.

링크가 되지 않던 이유를 난 한참 뒤에야 알게 됐다.
C가 아닌 C++에서는 static이나 extern 명시가 없이 const로 선언된 전역변수는 기본적으로 extern이 아니라 static 속성이 부여된다. 그러니 그 번역 단위 내부에서만 쓸 수 있지, 외부로 명칭이 노출되지 않으며, 따라서 링크 에러가 난다.

왜 그렇게 정책이 바뀌었냐 하면 const 개체에 대해서는 이 글의 1번 항목에서 명시한 것과 같은 무결성을 보장하기 위해서인 듯하다.
심벌이 가리키는 메모리 주소는 값이 언제 바뀌어 있을지 모르니, const 개체의 값은 매 번역 단위마다 컴파일러가 소스 코드로부터 읽어들여서 확인하기 위해서이다.

이 조치를 무시하고 const 개체의 값을 다른 번역 단위에서도 사용하려면 extern을 명시적으로 지정해 줘야 한다.

extern const TYPE TABLE = ... 라고 바로 써 줘도 되고, external const TYPE TABLE; 이라고 먼저 선언만 한 뒤에 나중에 const TYPE TABLE = ... 을 쓰면 TABLE은 여느 전역변수와 마찬가지로 다른 번역 단위에서 참조가 가능한 extern 변수가 된다.

4.

Windows 환경에서 개발을 하다 보면 지금 설치되어 있는 운영체제의 SDK에 기본 내장되어 있지 않은 GUID를 수동으로 추가해서 사용해야 할 때가 있다.

GUID는 코드가 아니라 128비트짜리 난수가 들어있는 구조체에 불과하지만, 엄연히 const 전역변수들의 집합이기 때문에 선언부와 정의부가 따로 있다. 그리고 주요 GUID의 실제 값들은 플랫폼 SDK의 라이브러리 디렉터리에 있는 uuid.lib에 들어있다. kernel32, user32, gdi32만큼이나 딱히 우리가 지정을 안 해도 자동으로 링크되는 기본 라이브러리이기 때문에, 파일의 존재감을 모르는 분도 많을 것이다.

그런데 이놈의 GUID 하나 좀 쓰자고 헤더 파일과 소스/라이브러리 파일을 다 구비해 줘야 하는 걸까? 여간 번거로운 일이 아닐 수 없다. 귀찮다고 헤더 파일에다가 GUID 값을 몸체(정의)를 다 써 주면, 이론적으로는 그 헤더를 인클루드하여 사용하는 모든 번역 단위에 동일한 GUID의 몸체들이 obj 파일 내부에 중복 기재될 위험이 있기 때문이다.

결국 이 문제는 MS 컴파일러의 경우 자기만의 언어 확장을 만듦으로써 우격다짐으로 해결했다. DLL 심벌을 만들거나 사용할 때 __declspec(dllexport/dllimport)를 사용하는 것처럼 __declspec(selectany)라는 속성도 있다. 이것이 지정된 전역 변수는 여러 object에서 중첩 기재된 심벌이라도 링크 때 딱 한 몸체만 임의 선택된다.

여러 소스 코드에서 공통으로 쓰이는 GUID를 새로 추가하고 싶으면 #include <initguid.h>를 해 준 뒤, DEFINE_GUID 매크로로 새 GUID의 명칭과 값을 써 주면 된다. 이 매크로는 내부적으로 selectany 지정자를 사용한다.

결국 이것은 전역 변수 선언계의 #pragma once나 마찬가지이다. 중복 인클루드 방지에 이어 심벌 몸체의 중복 링크 방지 마크이다. 이게 다 C/C++에는 간편히 끌어다 쓰는 패키지 개념이 없이, 원시적인 헤더/라이브러리에만 의존하느라 컴파일러 제조사가 부득이 추가한 꼼수인 셈이다.

내가 늘 느끼는 거지만..
C++ 님 좀 짱이다. 10년이 넘게 파 왔지만 아직도 지금까지 몰랐던 사실들이 계속 발견된다.

Posted by 사무엘

2013/03/22 08:29 2013/03/22 08:29
, ,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/809

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

« Previous : 1 : ... 17 : 18 : 19 : 20 : 21 : 22 : 23 : 24 : 25 : ... 31 : Next »

블로그 이미지

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

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

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

Site Stats

Total hits:
3042889
Today:
81
Yesterday:
2435