« Previous : 1 : ... 10 : 11 : 12 : 13 : 14 : 15 : 16 : 17 : 18 : ... 31 : Next »

몇 가지 C++ 이야기

1. 람다 함수와 내부 클래스 사이의 관계

본인은 몇 년 전엔 Visual C++ 2010으로 갈아탄 기념으로, <날개셋> 한글 입력기의 소스 코드 내부에서도 한 함수 안에 임시로 사용되던 잡다한 매크로 함수들을 대부분 람다나 템플릿 함수로 바꿨다. 이건 무식한 #define의 사용을 최소화한다는 코딩 컨벤션과 부합하는 리팩터링이었다. #define은 조건부 컴파일이나 ##처럼 정말로 컴파일러의 영역을 초월하여 전처리기의 도움이 필요한 영역에서나 제한적으로 사용할 것이다.

그 당시엔 매크로가 함수 안의 지역변수들을 건드리는 건 대부분 캡처로 기계적으로 치환해서 집어넣었다. 허나 무분별한 캡처 난발도 지금 보니 무척 지저분하게 보인다는 것을 깨닫는 데는 그리 긴 시간이 걸리지 않았다.
스케일만 한 함수 안으로 좁아졌을 뿐, 그 안에서 또 온갖 전역변수들을 덕지덕지 참고하여 결합도가 개판이 된 함수를 보는 듯한 느낌이어서 말이다. 소프트웨어공학 용어로는 common coupling에 해당한다.

여러 람다 함수가 비슷한 지역 변수들을 참조하는 경우, 그건 데이터와 함수를 몽땅 묶어서 함수 내부에 있는 local 클래스 형태로 떼어 내 버렸다. 어차피 람다의 캡처가 내부적으로는 그렇게 가상의 클래스 멤버 형태로 구현되니까 말이다.
그런데 클래스의 멤버 함수 내부에 있는 람다 함수가 지역 변수들도 참조하고 this 포인터까지 참조하는 경우.. 이건 좀 소속을 어디에다 둘지 좀 난감하긴 했다.

Visual C++ 2012부터는 드디어 캡처가 없는 람다에 한해 일반 함수 포인터로 typecast도 가능해졌다.

2. 템플릿 인자의 성격 분류

다음으로 C++ 템플릿 얘기를 하겠다.
지금까지 템플릿을 매크로 함수의 대체 용도로 사용했지만 한편으로는 이것을 공용체의 대체품으로도 잘 활용하고 있다.
예를 들어 클립보드의 내용을 type-safe하게 액세스 해 주는 클래스를 이런 식으로 사용한다.

CClipboard<WCHAR> cb(CF_UNICODETEXT);
CClipboard<DROPFILES> cb(CF_DROP);

예전에는 CClipboard라는 한 클래스 안에
union { PCWSTR pszText; DROPFILES *hDropFiles; };

라고 union을 쓰던 것을 typename T *pData 요렇게 바꾼 것이다.
어차피 한 클립보드 포맷 하에서 다양한 타입을 섞어서 쓸 일은 없으므로, 꼭 공용체가 필요한 상황이 아니기 때문이다.

그리고 템플릿 인자의 명칭을 용도별로 통일하고 있다. 크게 다음과 같은 네 가지가 있더라.

(1) 저것처럼 일반적인 임의의 type은 T이다.
(2) 참조 타입은 R이다. 이건 T 자체는 아니지만 call by value로도 전달할 수 있을 정도로 가볍고 T의 값을 온전히 나타낼 수 있는 key이다. T의 생성자에 단독 인자로 들어오는 게 가능하다.
가령, T가 String이라면 R은 const char*가 될 수 있다. 혹은 그냥 복사 생성자 격인 const T&도 상관없고.
R은 컨테이너 클래스다 추가하거나 검색하는 값을 지정할 때 쓰일 수 있다.

(3) 그리고 정수 인자는 N. 클래스에 소속된 static 배열의 원소 개수나 enum 값을 정의할 때 쓰인다.
(4) 끝으로, 람다든 functor든 함수 포인터든.. 어쨌든 실행 코드가 들어가는 부분은 O이다. F를 쓸까 하다가 기분 탓에 O가 됐다.

template<typename T, size_t N> class CStaticArray { T m_dat[N]; };
template<typename T, typename R=const T&> class CLinkedList { };
template<typename O> void EnumData(O func);

같은 식이다. 이것 말고 템플릿 인자의 용도는 또 뭐가 있을지 궁금하다.

다만, C++이 언어 문법 차원에서 템플릿 인자를 명백히 구분하는 건 정수형(N) 아니면 typename(T, R, O) 이렇게 둘뿐이다. 그러니 완전히 귀에 걸면 귀걸이이고 코에 걸면 코걸이이다. 템플릿 코드 자체만으로는 컴파일러가 문법 오류를 잡을 수 없으며, 그 템플릿에다 실제로 인자를 줘서 타입을 인스턴스화한 뒤에야 문제를 발견할 수 있다. 이것보다는 쓰임이 제한적이더라도, 좀 덜 와일드하고 통제 가능하고 쓰임을 예측 가능한 템플릿을 만들 수는 없나 싶은데 아직까지는 갈 길이 요원해 보인다.

아, 그나저나 템플릿 선언은 오로지 global scope 안에서만 가능하다. 한 함수 안에 있는 지역 클래스는 템플릿 형태로 선언될 수 없으며 일부 멤버 함수만이 템플릿 형태로 선언되는 것조차도 불가능하다.
람다 함수도 템플릿 형태로 선언될 수 없다. 미래의 C++ 문법에서는 이 정도 한계는 없어질 수도 있지 않겠나 하는 생각이 든다.

3. 전처리기에서 sizeof 연산자 떡밥

전처리기의 #if 조건식에서는 어지간한 정수 연산이 가능한 반면, 잘 알다시피 sizeof 연산자는 사용할 수 없다. sizeof의 값에 따라 곧장 조건부 컴파일이 가능하면 참 좋겠지만 그건 순리에 어긋나는 일이다. 컴파일러에게 넘겨 줄 코드를 전처리 하는 중인데, 임의의 구조체의 크기처럼 소스 코드를 컴파일해야만 값을 구할 수 있는 연산자를 전처리기에다가 요구할 수는 없기 때문이다.

과거에 C++에서 컴파일러와 링커 사이의 순리를 거스르는 시도를 하다 템플릿 export 흑역사가 발생했듯, sizeof도 전처리기와 컴파일러 사이에서 "닭이 먼저냐 계란이 먼저냐" 싸움이 될 수 있다.
전처리기를 이용한 조건부 컴파일러는 소스 코드의 이식성에 도움이 되는 기능인데, 정작 기계 종속적인 이식성 관련 정보를 무작정 표준 매크로 심벌로 집어넣을 수는 없다니 이거 참 역설이 아닐 수 없다.

물론 int나 void*의 크기 정도야 전처리기에다가 곧장 박아 넣을 수도 있다. 그러나 근본적으로 전처리기는 코드의 의미나 타겟 플랫폼 따위는 전혀 모른 채 시종일관 lexical한 치환만 하면서 완전 동일하게 동작하는 물건이며, 포인터 같은 기계 종속적인 사항에 대해서는 완전히 아오안이어야 한다.

이런 이유로 인해 포인터의 크기나 비트 endian-ness 같은 타겟 플랫폼 정보는 있으면 유용할 것 같음에도 불구하고 표준 predefined 매크로로 제공되지 않는다. 조건부 컴파일을 위해 그런 정보가 필요하다면 필요한 사람이 해당 매크로 심벌을 별도의 헤더나 컴파일러 옵션을 통해 지정해 놔야 한다. 전처리기가 월권 행위를 저지르지 않아도 되게 해 주기 위해서이다.

다만, 전처리기가 아닌 컴파일러의 입장에서는 sizeof는 컴파일 타임 때 값이 결정되는 아주 static하고 깔끔한 연산자이다. sizeof의 피연산자로는 타입 이름과 값이 모두 올 수 있는데, 값은 '직접 실행이 되지 않는다'. 쉽게 말해 sizeof( *((int *)NULL) )을 해도 뻑이 안 난다는 뜻.
그렇기 때문에 sizeof의 값은 static 배열의 크기를 결정하는 데 쓰일 수 있고, 또 case 문이나 템플릿의 정수값 인자에도 얼마든지 들어갈 수 있다.

Posted by 사무엘

2015/08/31 08:39 2015/08/31 08:39
,
Response
No Trackback , 6 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1133

C++의 클래스에는 여느 객체지향 언어와 마찬가지로, 정적 바인딩이 아닌 실행 시간 동적 바인딩을 거쳐 호출되는 가상 함수라는 개념이 있다.
일반 함수들은 선언만 해 놓고 정의를 안 했다면, 그 함수를 실제로 호출한 곳이 있을 때만 링크가 시도되고 링크 에러가 난다. (물론 DLL을 만들 때처럼 외부로 export된다고 명시된 함수라면, 우리 모듈 내부에서 당장 안 쓰이더라도 당연히 몸체가 존재해야 함. 이건 논외로 한다.)

이런 일반 함수와는 달리 가상 함수는 해당 클래스에 속하는 개체가 생성되어 가상 함수 테이블이 만들어질 때 이미 참조가 발생한다. 그렇기 때문에 일단 선언을 한 이상, 코드 내부에서 직접적으로 호출을 안 하더라도 반드시 정의가 돼 있어야 한다. 그래야 링크 에러가 안 난다.
가상 함수이지만 자기 몸체가 존재하지 않아도 되는 예외적인 경우는 단 하나, 순수 가상 함수뿐이다.

순수 가상 함수가 존재하는 클래스는 반쯤은 불완전한 타입과 비슷하다. 불완전하기 때문에 그 클래스의 인스턴스를 직접 만들 수 없다. 누군가가 얘를 상속하여 파생 클래스를 만든 뒤, 모든 순수 가상 함수들에 대한 몸체를 구현해 줘야만 한다. 몸체를 안 만들고 선언만 해 놓으면 컴파일은 되지만 그제서야 링크 에러가 나게 된다. 즉, 순수 가상 함수는 가상 함수의 링크와 관련된 문제를 파생 클래스로 보류하는 역할을 한다.

순수 가상 함수가 들어있는 추상 클래스의 인스턴스를 직접 만드는 것은 컴파일러에 의해 원천 봉쇄되고 금지되어 있다. 그런데 아주 특수한 상황에서는 이렇게 몸체가 없는 추상 클래스에 대한 직접 호출이 일어날 때가 있다. 예를 들어 아래와 같은 경우이다.

class CDerived;
class CBase {
    CDerived *m_pt;
public:
    CBase(CDerived *p);
    virtual void Pure() = 0;
};

class CDerived: public CBase {
public:
    CDerived(): CBase(this) {}
    void Pure() { puts("Ahh"); }
};

CBase::CBase(CDerived *p): m_pt(p) { p->Pure(); }

int main() { CDerived obj; }

생성자에서 자기 자신이 아직 완전히 초기화되지 않은 상태일 때 Pure()을 호출해 버리니, 마치 열차가 탈선한 것처럼, 지뢰찾기에서 지뢰를 밟은 것처럼 허를 찔리는 것이다.
사실 forward 선언까지 동원해 가면서 굳이 m_pt를 CDerived의 포인터로 지정하지 않아도 된다. 그냥 기반 클래스와 동일한 CBase *로 지정해도 에러가 나는 건 마찬가지이다.

참고로, CBase의 생성자에서 저렇게 번거롭게 p->Pure()을 하지 않고 그냥 this에 대해서 Pure()을 호출하려 시도하면 그건 순수 가상 함수임에도 불구하고 동적 바인딩이 아니라 정적 바인딩이 일어난다. CBase에 Pure 함수가 정의돼 있지 않다고 링크 에러가 난다. 그걸 별도의 포인터 값으로 억지로 우회함으로써 아직 몸체가 없는 순수 가상 함수에 도달할 수 있다. 위의 코드는 굉장히 인위적인 예이지만, 멀티스레드 race condition 같은 데서도 드물게 이런 일이 벌어질 수 있다.

C#은 생성자에서도 동적 바인딩이 잘 처리되는 게 보장되는 반면, C++은 옛날에 만들어지기도 해서 성능 보전을 위해서인지 설계 관점이 다소 보수적이다. 기반 클래스의 생성자는 자신이 파생 클래스의 생성 과정의 일부라 하더라도 파생 클래스에 대한 단서를 전혀 얻을 수 없고 언제나 기반 클래스의 형태로만 동작한다.
여러 모로 생성자에서 가상 함수를 호출하는 건 좋지 않다는 걸 알 수 있다. DllMain 안에서 또 DLL을 로딩하지 말고 가능한 한 거기서는 몸을 사리고 있어야 하는 것과 비슷한 맥락이다.

초기화가 되기 전에 가상 함수 테이블이 가리키는 주소값이 NULL이었다면, 그냥 더 생각할 것도 없이 운영체제 차원에서 '잘못된 연산' 에러가 나고 그걸로 끝이 날 것이다. 그러나 그렇게만 하는 건 재미(?)가 없고 데이터를 조작하다가 발생한 여느 null pointer 에러와 구분도 어려우니 컴파일러는 가상 함수 테이블에다가 순수 가상 함수에 대한 디폴트 핸들러의 주소를 넣어 준다. 주소를 한 번만 넣어 주면 끝이니 딱히 성능에 영향을 받을 일이 없고, 따라서 debug뿐만 아니라 release 빌드에도 못 넣을 이유가 없다.

개체가 정상적으로 초기화됐다면 그 주소는 사용자가 작성한 파생 클래스의 함수로 당연히 대체된다. 그러나 그게 아직 바뀌지 않았다면 핸들러 함수로 간다. 얘는 assert(0)과 비슷한 급의 예외를 발생시키는 것 말고 딱히 하는 일은 없다. 다만, 이 일이 벌어졌을 때 사용자가 지정해 준 함수가 호출되게 할 수는 있다.

가상 함수가 몸체가 온전하지 않으면 컴파일 에러(순수 가상 함수에 대한 클래스 선언)와 링크 에러(...)뿐만 아니라 이렇게 런타임 에러까지 드물게나마 발생할 수 있다는 걸 알 수 있다. 런타임 에러라니 왠지 C++답지 않아 보이지만, 이건 그래도 운영체제나 컴퓨터 차원으로 갈 런타임 에러를 언어 시스템 차원에서 수습 가능한 런타임 에러로 바꿔 주는 체계까지는 갖춰져 있다. 배열 첨자 초과나 division by 0 에러는 전자로 바꾸려면 매번 값을 사전에 점검해야 하기 때문에 오버헤드가 큰 반면, 순수 가상 함수 호출은 그냥 디폴트 함수 주소만 설정해 주면 되니까 말이다.

Posted by 사무엘

2015/08/26 08:34 2015/08/26 08:34
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1131

한때 Windows에서 바탕 화면에 배경 그림을 표시하는 방식은 '바둑판, 화면 중앙, 화면 크기에 맞춤'이라는 딱 세 가지 중 하나를 선택할 수 있었다.
이것은 GDI에서 직사각형 영역에다 비트맵을 뿌리는 함수로 치면 각각 PatBlt, BitBlt, 그리고 StretchBlt에 대응한다. 지금은 몇 가지 방식이 더 나오는데, 그건 그림의 종횡비와 화면의 종횡비가 다를 때 확대를 어떻게 할지를 결정하는 것이므로 개념적으로 StretchBlt에 대응하는 셈이다.

GDI에서 비트맵 그래픽을 표현하는 추상적인 핸들 자료형은 잘 알다시피 HBITMAP이다. 그러나 위의 세 *Blt 함수들 중 어느 것도 HBITMAP을 인자로 받지 않는다. 이는 어찌 된 일일까?
이들은 비트맵을 자기들이 처리하기 용이한 형태로 바꾼 파생 자료형을 대신 사용한다. PatBlt를 사용하려면 뿌리려는 비트맵을 브러시로 바꿔야 하며, BitBlt와 StretchBlt는 해당 비트맵에 대한 그래픽 조작이 가능한 메모리 DC를 추가로 준비해야 한다. 그럼 그 구체적인 내역을 살펴보자.

모노크롬 아니면 16색 그래픽이 있던 시절, 도스용 그래픽 라이브러리에는 8바이트로 표현되는 8*8 단색 패턴이라는 게 있었다. 그 작은 공간으로도 벽돌, 사선 등 생각보다 기하학적으로 굉장히 기발한 무늬를 표현할 수 있었다.
Windows는 2000/ME까지만 해도 배경 그림은 오로지 BMP만 지원했으며(액티브 데스크톱을 사용하지 않는 한), 배경 그림이 차지하지 않는 나머지 영역은 그런 무늬로 도배하는 기능이 있었다. 물론 이것은 트루컬러 그래픽과는 영 어울리지 않는 낡은 기능이기에, XP부터는 깔끔하게 없어졌다.

사용자 삽입 이미지

PatBlt는 직사각형 영역을 주어진 브러시로 채우는 함수이다. 즉, 이 함수가 사용하는 원천은 함수의 별도 인자가 아니라 해당 DC에 선택되어 있는 브러시이다. 그럼 얘는 Rectangle이나 FillRect와 하는 일이 거의 차이가 없는 것 같아 보인다. 이 세 함수의 특성을 표로 일목요연하게 정리하면 다음과 같다.

  PatBlt FillRect Rectangle
경계선을 current pen으로 그음 X X O (유일)
경계면을 current brush로 채움 O No, brush를 따로 인자로 받음 O
사각형 좌표 지정 방식 x, y, 길이, 높이 RECT 구조체 포인터 x1, y1, x2, y2 (RECT 내용을 풀어서)
래스터 오퍼레이션 지정 O (유일) X (= PATCOPY만) X (왼쪽과 동일)

다들 개성이 넘쳐 보이지 않는가? =_=;;
Rectangle은 선을 긋는 기능이 유일하게 존재하며, FillRect는 유일하게 사용할 브러시를 매번 인자로 지정할 수 있다. 그 반면 PatBlt가 유일하게 갖추고 있는 기능은 래스터 오퍼레이션인데, 사실 이것이 이 함수의 활용도를 크게 끌어올려 주는 기능이다. 이에 대해서도 앞으로 차차 살펴보도록 하겠다.

브러시는 '2차원 면을 바둑판 형태로 채우는 어떤 재질'을 나타내는 GDI 개체이다. 가로선· 세로선· 대각선 같은 간단한 무늬는 CreateHatchBrush로 지정 가능하지만 이건 오늘날에 와서는 영 쓸 일이 별로 없는 모노크롬 그래픽의 잔재이다.
CreateSolidBrush는 아무 무늬가 없는 순색 브러시를 표방하긴 하지만, 그래도 16/256컬러 같은 데서 임의의 RGB 값을 넘겨 주면 단순히 가장 가까운 단색이 아니라 ordered 디더링이 된 무늬가 생성된다.

그리고 다음으로 비트맵으로부터 브러시를 생성하는 함수는 바로 CreatePatternBrush이다.
여기에서 사용할 비트맵은 가장 간단하게는 CreateBitmap이라는 함수를 통해 생성할 수 있다. 이 함수가 인자로 받는 건 비트맵의 가로· 세로 크기와 픽셀 당 색상 수, 그리고 초기화할 데이터가 전부이다. 아주 간단하다.

그러나 이 비트맵은 그냥 2차원 배열 같은 픽셀 데이터 덤프 말고는 그 어떤 정보도 담겨 있지 않으며, 이걸로 만들 수 있는 건 구조가 극도로 단순해서 어느 그래픽 장비에서나 공통으로 통용되는 모노크롬 비트맵뿐이다. 즉 그 도스 시절의 8*8 패턴 같은 극도로 단순한 비트맵만 만들 수 있다. 오늘날에 와서 CreateBitmap은 모노크롬 비트맵 생성 전용이라고만 생각하면 된다.

모노크롬 비트맵을 기반으로 만들어진 DC나 브러시는 다른 solid/hatched 브러시와는 달리 자체적으로 색상 정보가 담겨 있지 않다. 그렇기 때문에 이때는 그래픽을 뿌리는 DC가 갖고 있는 텍스트의 글자색(값이 0인 곳)과 배경색이(값이 1인 곳) 양 색깔로 선택된다는 점도 참고하자. MSDN에 명시되어 있다. (0과 1 중 어느 게 글자인지 이거 은근히 헷갈린다. 빈 배경에서 뭔가 정보가 있다는 관점에서는 1이 글자 같아 보이기도 하니 말이다.)

그리고 브러시는 origin이라는 게 있어서 어떤 경우든 이를 원점으로 하여 바둑판 모양으로 뿌려진다. oxoxoxox라는 무늬가 있다면, 0,0부터 8,0까지 뿌린다면 oxoxoxox로 뿌려지지만 1,0부터 9,0까지 뿌린다면 ox가 아니라 xoxoxoxo가 된다는 뜻이다.

모노크롬이 아닌 컬러 비트맵을 저장하고 찍는 절차는 좀 복잡하다. 이미 컬러를 표현할 수 있는 DC로부터 CreateCompatibleDC와 CreateCompatibleBitmap을 거쳐서 비트맵을 생성해야 한다. 아니면 CreateDIBitmap를 써서 DIB라 불리는 '장치 독립 비트맵' 정보로부터 HBITMAP을 생성하든가.. 얘는 그냥 비트맵 데이터뿐만 아니라 팔레트 정보 같은 것도 담긴 헤더를 인자로 받는다. 출력할 그래픽 데이터와 출력 매체의 픽셀 구조가 다를 때를 대비해서 추상화 계층이 추가된 것이다.

원래 패턴 브러시는 8*8의 아주 작은 비트맵만 취급할 수 있었다. 그러나 NT 내지 95 이후의 버전부터는 그 한계가 없어지면서 브러시와 오리지널 비트맵 사이의 경계가 좀 모호해졌다. 그래도 PatBlt는 작은 비트맵 무늬 위주의 브러시를 래스터 오퍼레이션을 적용하여 그리는 용도에 원래 최적화돼 있었다는 점을 알아 두면 되겠다.
윈도우 클래스를 등록할 때 우리는 WNDCLASS의 hbrBackground 멤버를 흔히 (HBRUSH)(COLOR_WINDOW+1) 이런 식으로 때워 버리곤 하는데, 여기에다가도 저런 패턴 브러시를 지정해 줄 수 있다. 그러면 그 윈도우 배경에는 자동으로 바둑판 모양의 비트맵이 배경으로 깔리게 된다. 이런 식의 활용도 얼마든지 할 수 있다.

한편, 비트맵을 찍는 동작에는 그냥 있는 그대로 뿌리는 것뿐만이 아니라 래스터 오퍼레이션을 통해 반전을 해서 찍기(PATINVERT), 타겟 화면을 무조건 반전시키기(DSTINVERT), 타겟 화면을 무조건 검거나 희게 바꾸기 같은 세부 방식의 차이가 존재할 수 있는데, 앞서 언급한 FillRect뿐만 아니라 InvertRect나 DrawFocusRect 같은 함수도 사실은 PatBlt의 기능을 이용하여 다 구현 가능하다. cursor를 깜빡거리는 건 두 말할 나위도 없고 말이다.

임의의 색깔로 음영을 표현하는 것이라든가, 특히 이동이나 크기 조절을 나타내는 50% 반투명 검은 음영 작대기/테두리는 모두 이 함수의 xor 래스터 오퍼레이션으로 표현된다. 그걸 구현하는 데는 PatBlt 말고는 선택의 여지가 없다는 뜻. 흑백을 xor 연산 시키면 "원래 색 & 반전색"이 교대로 나타나니까 말이다.

사용자 삽입 이미지
물론 요즘은 (1) 걍 테두리 없이 해당 개체를 즉시 이동이나 크기 조절시키는 것으로 피드백 또는 (2) 알파 블렌딩을 이용한 음영이 대세가 되면서 전통적인 xor 음영은 점점 비중이 줄어들고 있긴 하지만, PatBlt 함수는 그래도 이렇게 유용한 구석이 있다.

이런 PatBlt에 반해 BitBlt는 비트맵을 SelectObject시킨 DC를 원본 데이터로 사용하기 때문에 컬러 비트맵의 출력에 더 최적화되어 있다. PatBlt처럼 비트맵을 바둑판 모양으로 반복 출력하는 기능은 없으며, 딱 원본 데이터의 크기만큼만 출력한다. PatBlt와는 달리 고정 origin이 없고 사용자가 찍으라고 한 위치가 origin이 된다. StretchBlt는 거기에다가 확대/축소 기능이 추가됐고 말이다.

이 정도면 비트맵 API에 대한 개념이 충분히 숙지될 수 있을 것이다. 각종 아이콘과 마우스 포인터들도 다 마스크 비트맵 AND와 컬러 비트맵 XOR이라는 래스터 오퍼레이션을 통해 투명 배경 내지 반전을 구현한다는 건 두 말하면 잔소리이다. 물론 오늘날은 알파 채널로 투명도를 구현하면서 래스터 오퍼레이션의 의미는 다소 퇴색했지만 말이다.
그럼 이제 비트맵 API들에 대한 개인적인 의문점과 아쉬운 점을 좀 나열하며 글을 맺겠다.

(1) GDI는 후대에 등장한 다른 그래픽 API들과는 달리, 글꼴을 제외하면 벡터와 래스터 모든 분야에서 안티앨리어싱과는 담을 싼 구닥다리 API로 전락해 있다. 그러니 비트맵을 정수 배가 아닌 확대/축소를 좀 더 부드럽게 하거나, 아예 임의의 일차변환을 한 모양으로 출력하려면 최소한 GDI+ 같은 다른 대체제를 써야 한다.

(2) 운영체제가 가로줄, 세로줄 같은 몇몇 known pattern에 대해서 CreateHatchBrush 함수를 제공하긴 하는데, 50% 음영 정도는 오늘날에도 많이 쓰이기 때문에 known 패턴이 좀 제공되어야 하지 않나 싶다. 그게 없어서 수많은 프로그램들이 내부에 0x55, 0xAA 배열을 일일이 생성해서 패턴을 만드는 것은 낭비이다.
오히려 cursor는 CreateCaret 함수에 (HBITMAP)1을 줘서 50% 음영을 만드는 기능이 있는데, 정작 그건 별로 쓸 일이 없다.

(3) 브러시 말고 펜으로 선을 그리는 걸 xor 반전 연산으로 하는 기능은 없는지 궁금하지 않으신지? 임의의 사선이나 원 테두리를 그렇게 그리는 건 그래픽 에디터를 만들 때도 반드시 필요하니 말이다.
물론 그런 기능이 없을 리가 없다. SetROP2라는 함수로 그리기 모드를 바꿔 주면 된다. 단, 여기서 입력받는 래스터 오퍼레이션 코드는 BitBlt가 사용하는 코드 체계와는 다르다. 비트맵 전송 API들은 화면의 원본 픽셀(D), 그리려는 픽셀(S)뿐만 아니라 패턴(P)이라는 변수가 또 추가되어서 원래는 3변수 코드를 사용한다. BitBlt는 PatBlt가 하는 일까지 다 할 수 있는 모양이다.

Posted by 사무엘

2015/08/12 08:33 2015/08/12 08:33
, , , ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1126

타이머 API 이야기

컴퓨터 프로그램이라는 건 원래 처음부터 끝까지 컴퓨터가 그야말로 눈 깜짝할 사이에 전속력으로 실행해 버리는 물건이다. 그러나 컴퓨터에는 정밀한 시간 측정 기능이 있으며, 프로그램이 원하는 경우 자신이 실행되는 주기를 그에 맞춰 인위로 조절할 수 있다.
일명 타이머 기능인데, 이것은 컴퓨터가 액세서리 차원에서 제공하는 부가 기능이 아니라 컴퓨터 자체의 내부 동작 방식의 특성상 컴퓨터가 반드시 갖추고 있는 기능이다. 단적인 예로 난수 생성을 위한 씨앗(= 매번 달라야 하는 초기값)도 내부적으로 재고 있는 시각으로부터 얻을 정도이다.

컴퓨터가 속도가 매우 느리고 자원이 부족하고, 한 프로그램이 컴퓨터의 전체 자원을 독점할 수 있던 옛날에는 매번 타이머를 측정하면서 0.n초가 경과하는 것을 프로그램이 일일이 감시하는 방식으로, 즉 polling 방식으로 동작했겠지만, 지금은 그건 어림도 없는 소리이다. 자기에게 time slice를 주는 운영체제에다가 '알람' 요청을 해서 알람이 왔을 때 동작하게 해야 한다.

Windows에서 이 기능을 사용하는 아주 대표적인 방법은 타이머 API이다. SetTimer, KillTimer와 그 이름도 유명한 WM_TIMER 메시지.
타이머는 그 성격에 따라 게임이나 멀티미디어 재생기 등에서 프레임 간격 유지를 위해 1초에 수십 번씩 돌아가는 (1) 아주 정밀한 놈부터 시작해, 수백 밀리초~수 초 정도의 간격으로 사용하는 (2) 일반적인 타이머, 그리고 드물게는 수 시간~수 일 주기를 갖는 (3) 장기 타이머도 있다. 운영체제의 보급 타이머는 일단은 가성비가 적당히 우수한 '일반적인' 용도에 가장 적합하게 설계돼 있다. 이게 무슨 뜻인지를 설명하면 이렇다.

보급 타이머도 명목상으로는 수십 밀리초 정도의 정밀도를 지원한다. 하지만, WM_TIMER는 WM_PAINT만큼이나 메시지 큐에서 처리 우선순위가 무척 낮은 메시지이기 때문에 컴퓨터가 아주 바쁘고 윈도우 메시지 트래픽이 아주 많을 때는 정밀도가 떨어질 수 있다. 더구나 Windows는 근본적으로 리얼타임 운영체제가 아닌 관계로, 커널의 시간 스케줄링을 초월해서까지 무조건적인 초정밀도는 애초에 보장되지도 않는다. 타이머의 정밀도가 올라갈수록 필요한 시스템 자원과 부하도 더 커질 테니, 초정밀 타이머가 필요하다면 QueryPerformanceCounter나 멀티미디어 타이머 같은 다른 전문 API를 쓰고 동기화도 커널 오브젝트 같은 다른 방법을 써서 해야 한다.

한편, 다른 쪽 극단에 있는 장기 타이머는 응용 프로그램 자체의 동작이라기보다는 업데이트 주기를 체크하거나 사용자에게 적당히 덜 성가신 주기로 뭔가를 알리는 용도로 사용된다. 이 정도면 타이머라기보다는 알람에 더 가깝다.
개인적으로는 지금으로부터 "5000밀리초 간격으로" 같은 것 말고, 절대적인 시각.. 예를 들어 1970년 1월 1일 0시 정각 이래로 40억 5800만 초가 딱 경과했을 때처럼 절대적인 시각을 기준으로 trigger되는 진정한 '알람' 타이머도 필요하다고 생각한다.

시계 프로그램을 만들 때는 이런 타이머 API가 더 유용하지 않겠는가? 그리고 장기 타이머를 사용할 정도의 상황이라면 지금으로부터 시간이 얼마만치 지났는지보다는 매일 몇 시가 됐는지가 더 중요한 경우가 많을 것이기 때문이다.

이렇게 극단적으로 짧은 주기의 타이머나 극단적으로 긴 타이머 말고, 보통 주기의 타이머는 여러가지 용도로 쓰인다. 가령, 키보드는 누르고 있는 동안 키 입력이 하드웨어 차원에서 자동으로 반복 전달되는 반면 마우스는 그런 게 없는데, 마우스를 누르고 있는 동안 자동 스크롤이 되는 것은 타이머로 처리가 가능하다. 그리고 간단한 비동기적인 처리를 위해서도 타이머가 약방의 감초처럼 쓰인다.

이게 도스의 제약인지 아니면 인텔 x86 CPU 차원의 제약인지 구체적인 내역은 기억이 안 나지만, 도스 시절에는 컴퓨터의 타이머 해상도가 1/18.2초여서 최소 주기가 약 55밀리초였던 것 같다. Windows 9x 시절에만 해도 운영체제의 타이머의 정밀도는 그 정도였다고 MSDN에 기록돼 있었는데 NT 계열은 하드웨어를 또 어떻게 튜닝했는지 타이머가 그것보다 훨씬 더 정밀해졌다.

자, 그럼 이 글에서는 Windows의 일반 타이머 API에 대해서 더 자세히 알아보자.
SetTimer 함수의 인자로는 타이머의 발동 주기뿐만 아니라 (1) 타이머를 메시지로 받을지 아니면 함수 호출로 받을지, (2) 그리고 메시지로 받는 경우 동일 메시지에서 이 타이머만을 식별할 번호를 지정하면 된다. SetTimer 함수는 사용하는 방법이 생각보다 좀 복잡하다.

(1) SetTimer에다가 뭔가 윈도우 핸들을 전해 주는 경우, 타이머는 메시지로 받을 수도 있고 콜백 함수로 받을 수도 있다. 두 가지 선택의 여지가 있으며, 타이머 식별 번호는 우리가 임의의 자연수로 일괄 지정해 줄 수도 있다.
(2) 그 반면 윈도우 핸들이 없이, 윈도우를 전혀 생성하지 않고도 타이머를 사용할 수 있다. 그 대신 이때는 몇 가지 제약이 따른다. 메시지가 아닌 콜백 함수로만 통지를 받을 수 있으며, 타이머 식별자는 우리가 지정할 수 없다. SetTimer 함수가 되돌린 값을 별도의 변수에다 보관하고 있어야 한다.

마치 에디팅 엔진의 기능만을 따로 떼어서 windowless 리치 에디트 컨트롤이 존재하는 것처럼 타이머도 windowless 타이머가 존재하는 셈이다. 물론 SetTimer가 무슨 스레드를 만들기라도 해서 따로 돌아가는 건 아니기 때문에, 비록 windowless 타이머를 사용한다 하더라도 메시지 loop은 돌리고 있어야 타이머가 동작할 수 있다.

개인적으로는 (1)과 (2)의 특징을 취합하는 방법이 없는 게 아쉽다. 윈도우 핸들을 지정해 줘서 WM_TIMER 메시지를 받는데 타이머 식별자는 내가 일괄 지정한 게 아니라 운영체제가 기존 타이머들과의 충돌을 피해서 동적으로 배당한 값이 오는 형태 말이다.
서브클래싱 내지 후킹을 한 윈도우에 대해서 타이머를 걸 때는 하드코딩된 타이머 ID를 써서는 안 된다. 원래의 윈도우 프로시저가 사용하는 고정 타이머 ID와 충돌을 일으킬 수 있기 때문이다. 마치 윈도우 메시지가 서로 충돌하는 것처럼 말이다.
이때는 충돌이 없음이 보장되는 windowless 타이머를 써야 한다. 하지만 windowless 타이머는 다음과 같은 이유로 인해 사용이 불편하다.

첫째, 콜백 함수에 user data를 넘겨 주는 추가 인자가 없다. 그래서 user data는 전역 변수나 TLS 값 같은 불편한 방법으로 얻어 와야 한다.
둘째, 윈도우가 붙은 타이머는 같은 ID값으로 타이머를 지정하는 경우, 기존 타이머가 새 타이머로 자동으로 대체된다. 그러나 windowless 타이머는 그런 기능이 없기 때문에 기존 ID에 대해서 KillTimer를 하고 다시 SetTimer를 해서 새 ID를 얻는 작업을 수동으로 해 줘야 한다. 다시 말해 기존 타이머의 재지정이 어렵다.

결국, 충돌을 피하기 위해서는 windowless 타이머를 써야 하는데 이 타이머도 윈도우가 붙은 타이머하고 비슷하게 동작하도록 추가 군더더기 기능을 구현한 클래스를 만든 뒤에야 그럭저럭 쓸 만하게 됐다.
윈도우가 있는 타이머와 없는 타이머에서 서로 필요한 기능을 취합하는 방법이 없어서 불편하다는 걸 다시 한 번 확인할 수 있었다.

그나저나 SetTimer 함수에서 ID를 받는 부분은 포인터나 핸들을 넘기는 용례가 없는데 자료형이 왜 UINT가 아닌 UINT_PTR로 잡혀 있는지 이것도 개인적으로는 의문이다.

Posted by 사무엘

2015/08/09 08:21 2015/08/09 08:21
, , ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1125

Windows의 역사 회상

1. 공용 대화상자

먼 옛날, Windows 3.0은 최초로 VGA를 지원하고 팔레트 API, 장치 독립 비트맵, MDI 관련 API가 추가되고, RTF 기반 winhelp 도움말이 추가되고, 버튼이 3D 회색으로 바뀌고 시스템 글꼴까지 가변폭으로 바뀌는 등 장족의 발전을 이뤘다. (386 확장 모드는 2.1때 미리 도입됐다고 하니 그건 논외로 하더라도)
그런데, 3.0에 없다가 3.1에서 새로 추가된 기능들도 만만찮았다. 트루타입 글꼴과 OLE야 워낙 잘 알려진 3.1의 신규 기능이다만.. 이것 말고도 오늘날 당연하게 여겨지고 있는 '공용 대화상자' 컬렉션들이 역시 3.1에서 처음 도입되었다.

3.1 이전에는 GetOpenFileName 함수가 Windows API에 없었다는 뜻이다. 파일 열기/저장 대화상자는 응용 프로그램들이 전부 직접 따로 구현해야 했다. MS Office 제품들이 한동안 독자적인 파일 열기/저장 대화상자를 갖추고 있었던 건 운영체제도 Windows 3.1 이전까지는 어차피 해당 기능을 제공하지 않았기 때문이지 싶다. Word, Excel은 이미 1980년대부터 개발되었던 프로그램이니까.
그리고 파일 대화상자뿐만 아니라 색깔 선택, 텍스트 검색, 인쇄 같은 잘 알려진 공용 대화상자들도 3.1에서 처음으로 도입됐다.

옛날 도스 시절에 TUI 내지 GUI를 직접 구현하면서 파일 열기/저장 대화상자도 손수 만들어 본 프로그래머라면 공용 대화상자가 얼마나 혁신적인 물건인지 이해가 될 것이다.
그리고 내 생각엔 아마 ShellAbout 함수도 3.1에 와서야 용례가 완전히 정립되지 않았나 싶다. 3.0 때는 응용 프로그램별로 About 대화상자도 서로 다르게 생긴 경우가 있었기 때문이다.

공용 대화상자에 이어 리스트/트리 컨트롤 같은 추가적인 "공용 컨트롤"은 Windows 3.1보다 한 박자 뒤인 Windows 95 내지 NT 3.51과 함께 도입됐다.
물론 일반 사용자에게 와 닿는 Windows 3.0과 3.1의 큰 차이는 저런 기술적인 요소가 아니라... 보조 프로그램으로 리버시(오델로 게임)가 짤리고 그 이름도 유명한 '지뢰찾기'가 대신 도입된 게 아닌가 싶다.

2. 9x와 NT가 따로 놀던 API

과거에 Windows 95와 NT가 공존하던 시절에는 일반적으로 95의 API는 NT의 API에 부분집합으로서 완전히 포함되는 것으로 여겨졌다. 보안이나 유니코드, 일부 고급 기능들이 빠져 있을 뿐, 공통 기능은 동일한 형태로 사용 가능하다는 것이다.
하지만 일부 기능은 95에도 전혀 없는 건 아닌데 NT와는 완전히 다른 형태로 따로 구현되어 API가 파편화되고, 이 때문에 프로그래머들 사이에서 번거로움으로 인해 악명을 떨치기도 했다. 그만큼 Windows 95팀과 NT 팀이 마치 MFC 팀과 Office 팀(리본 UI), Windows 팀과 Visual C++ 팀(CRT DLL)만큼이나 생각만치 교류가 없었다는 뜻이다. 이거 무슨 일본군 육군과 해군도 아니고.

그런 기능으로 무엇이 있느냐 하면 첫째는 사용 중인 파일을 다음 재부팅 때 지우도록 예약하는 기능이요, 둘째는 실행 중인 프로세스와 모듈들을 조회하고 heap 메모리 상태를 조회하는 기능이다.
전자는 NT에서는 MoveFileEx 함수를 쓰면 됐지만 95에서는 그 함수가 지원되지 않았다. 95에서는 wininit.ini라는 살생부 리스트를 수동으로 건드려 줘야 했는데, 이게 처리가 Windows가 아닌 도스 계층에서 행해지는지라 긴 파일 이름을 쓸 수 없어서 더욱 불편했다.

다음 후자의 경우, NT는 커널 API의 연장선 차원에서 EnumProcesses, EnumProcessModules, HeapLock, HeapWalk 같은 함수가 제공되었다. 카테고리의 명칭은 Process status API (PSAPI)라고 불렸다.
그러나 95는 Tool Helper라는 특수한 디버그용 라이브러리 개념으로 CreateToolhelp32Snapshot 이후 [Heap/Module/Process/Thread]32[First/Next] 이런 식으로 함수를 제공했다. 함수를 초기화하고 사용하는 방법이 서로 완전히 딴판이라는 얘기다.

공교롭게도 이 두 기능은 모두 설치/제거 프로그램을 만들 때 필요한 기능이다. "이 DLL은 다음 프로그램이 사용하고 있습니다. 다음 재부팅 때 제거하시겠습니까?"를 구현하려면 말이다. Windows Installer 런타임은 당연히 9x용과 NT용이 이런 점을 감안하여 제각각 구현되어 있었을 것이다.
결국 Windows 2000에 가서야 지금까지 9x에만 있던 tool help library를 NT 계열이 마저 흡수하는 걸로 문제가 종결되었다. 마치 95에서 첫 도입되었던 Plug & play를 드디어 2000이 수용했듯이 말이다. 게다가 궁극적으로는 9x 계열 자체가 없어지기도 했고.

3. 그래픽과 사운드 성능 향상

1990년대 중후반에서 2000년대 초반에 이르기까지 컴퓨터의 성능이 향상됨으로써 Windows에 생긴 3대 변화를 들자면 난 다음을 꼽는다. 예전에 한 번씩은 다 언급한 적이 있었을 것이다.

(1) 화면이 막 고쳐지는 곳으로 마우스 포인터를 가져가도 깜빡임이 없게 되었다. 그래픽 카드가 마우스 포인터 주변은 건드리지 않게 하드웨어적인 처리를 진작부터 하기 시작했기 때문이다. 이것은 요즘 형광등이 깜빡임 없이 바로 켜지기 시작한 것만큼이나 신기한 일이다.

초창기에는 흑백의 기본 포인터만 처리가 되지, 컬러 내지 심지어 애니메이션이 있는 custom 포인터, 그리고 마우스 포인터 자취까지는 차마 깜빡임 방지 처리를 다 못 했다. 그러나 이것도 2000년대부터는 제약이 없어졌다.
Windows 2000은 아예 안전 모드에서 16컬러 VGA로 동작할 때에도 마우스 포인터의 깜빡임이 없는 게 무척 신기하다. NT가 원래 그랬는지 아니면 2000부터 그렇게 된 건지는 모르겠다.

(2) 멀티웨이브가 되기 시작한 것도 아주 신기한 일이다. 지금으로서는 도저히 믿을 수 없는 일이지만 Windows에 사운드/멀티미디어 지원이 처음으로 도입됐던 3.1/95 초창기에는 한 번에 한 프로그램만 사운드 카드의 사용이 가능했다. 그리고 다른 프로그램은 사운드를 이용할 수 없었다! PC에 사운드 카드가 버젓이 달려 있음에도 불구하고 사운드 초기화가 실패하는 상황에 대한 대비를 해야 했던 것이다.

9x 시절에는 일부 고급형 사운드 카드만이 멀티웨이브가 가능했다가 2000부터는 드디어 그냥 아무데서나 멀티웨이브가 가능해졌다. 이쯤에서 미디 역시 노래방 수준의 소프트웨어 신시사이저로 대체되었고 XP쯤부터는 오디오 CD까지 모든 사운드의 음원이 waveform으로 통합되었으며, Vista부터는 장치가 아닌 스피커/응용 프로그램별로 구분해서 볼륨을 지정하는 게 가능해졌다.

오늘날도 PC에 따라서는 출력 단자에 헤드폰/스피커 같은 게 전혀 연결돼 있지 않으면 사운드의 초기화가 실패하는 경우가 있다. 물론 PC 자체에 스피커가 달려 있는 노트북 PC에서는 해당사항이 없는 얘기. 옛날에도 입력 단자를 감지해서 녹음 버튼의 성공/실패를 감지하는 것 정도는 가능했던 것 같다.

(3) 그리고 제일 늦게 생겼고 Windows Vista가 이뤄낸 쾌거 중 하나는 역시 동영상 장면도 Print screen으로 간단히 캡처가 가능해졌다는 점이다. 창을 움직였는데 동영상 영역은 제대로 움직이지 않는다거나, 화면 캡처를 하면 그냥 컬러 키를 나타내는 이상한 단색만 캡처된다거나.. 이런 것도 이미 10년쯤 전부터 옛날 추억이 됐다.
기술적으로 따지고 보면 동영상만 추가적인 하드웨어 가속을 받는 게 아니라 아예 모든 그래픽이 동등하게 하드웨어 가속을 받기 시작했기 때문이다. GDI조차도 그 위에서 돌아가니까 BitBlt 같은 GDI API로 간단하게 캡처가 되기 시작한 것이다. 게다가 Vista가 처음으로 선보인 flip3d나 live preview에도 동영상이 실시간으로 표시되기 시작했다.

4. Windows 10

그리고 그 Windows 95가 출시된 지 거의 20년이 지난 지금, Windows 10이 출시되었다. 95 출시 당시에 중학생이던 본인은 뭐 이미 30대 중반의 성인이 됐고.
2015년에 마소 소프트웨어의 최대의 이슈는 단연 새 운영체제와 새 개발툴이다. Windows 10과 Visual Studio 2015.

IE가 11에서 종결되고 Edge로 넘어가는 것만큼이나 마소에서는 Windows 10이 독립된 브랜드 형태로는 Windows의 마지막 버전이 될 것이고 그 뒤로는 그때 그때 인터넷 업데이트만으로 유지보수를 할 것이라고 밝혔댄다.. 그 정책이 실제로 언제까지나 유지될지는 모르겠다.

하긴, 매번 XP, Vista 같은 브랜드명에다 숫자에다.. 이런 발상 자체가 식상해지고 아이디어가 고갈될 때도 되긴 했다.
허나 과거에 마소 내부에서는 IE 팀이 Windows 팀으로 합병될 뻔한 적도 있었고, 또 이미 윈도 7 시절부터 이건 NT 커널 기반 Windows의 마지막 버전이고 그 뒤로는 Midori던가 뭐던가 완전히 새로운 기반의 운영체제가 나온다는 식의 설레발도 나돌았다. 트렌드라는 건 언제든지 얼마든지 바뀔 수 있는 것이니 변화를 신중하게 지켜봐야겠다.

그래도 마소에서 이번 Windows 10을 뭔가 완결판이라는 컨셉을 두고 만들었다는 티가 벌써부터 팍팍 느껴진다.
외형이 8하고 별 차이가 없는 줄 알았는데, 프로그램의 제목이 가운데 정렬이던 것이 다시 왼쪽으로 복귀한 건 좀 사소한 점일 테고. ㅋㅋ
그리고 운영체제의 버전뿐만 아니라 커널의 내부 버전 번호도 Vista 이래로 지금까지 6이던 것이 7~9를 건너뛰고 10으로 맞춰졌다.
Windows 10이 저런다고 하니까 마치 Mac OS X 같은 느낌도 든다. 저 X도 10을 나타내니까.. 인터넷을 뒤져 보니 당연히 나만 그렇게 생각한 게 아니었다.

한편, Visual Studio의 경우, 2012 이래로 외형 색상의 변화는 크게 없다. 그럼 그렇지, 매 버전마다 비주얼을 다 뒤집어 엎는 것도 언제까지나 가능한 건 아니겠지 싶었다. ^^ 2013 커뮤니티 에디션이 나온 것부터가 굉장히 놀라웠는데, 갈수록 개방적으로 바뀌는 한편으로 이클립스 내지 xcode의 전통적인 영역까지 넘보고 있다.
운영체제, 브라우저, 개발툴에서 모두 마소가 종전의 소프트웨어 개발 방식 내지 패러다임을 종결하고 단절하겠다는 의지를 표현한 듯하다. 확실히 변해야만 살아남을 수 있다.

Posted by 사무엘

2015/08/03 19:38 2015/08/03 19:38
, , ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1123

* 서로 관계가 없는 여러 글들이긴 한데, 따로 따로 올리기는 좀 짧고 정보량이 적은 편이고, 귀찮은 구석이 있기도 해서 한데 묶었다.

1. 이미 실행돼 있는 프로그램을 스스로 종료한 뒤에 제거하기

본인은 설치· 배포 패키지를 만들 때 Visual Studio가 기본 제공하는 설치/배포 패키지를 사용하고 있다.
얘는 마소에서 직접 제공하는 물건이다 보니 정말 기본적인 퀄리티는 보장되고 기능이 나쁘지는 않다. 하지만 버그나 불편한 점이 아주 없는 건 아니고, 또 동작 customize의 폭이 충분하지 못해서 불편한 구석도 많다. Windows Installer라는 API가 제공하는 기능의 극히 일부만을 템플릿화해서 제공하는 형태이기 때문이다.

잘 알다시피 Windows는 실행 중인 EXE나 DLL은 이름을 바꿀 수는 있어도 지울 수가 없어서 뒤끝 없이 깔끔하게 제거하는 게 어려운 구석이 있다. 프로그램이 한번 실행했다가 간단히 종료가 가능한 EXE가 아니라 <날개셋> 한글 입력기처럼 IME가 포함돼 있다거나, 혹은 서비스/데몬류라면 참 난감하다.

EXE라면 자신을 종료하는 명령을 갖추고 있어야 한다. 즉 A라는 프로세스가 이미 돌아가고 있는데, /U나 /Q 같은 옵션으로 A가 다시 실행됐다면 그 A의 인스턴스는 이미 실행돼 있는 다른 A의 인스턴스를 찾아서 거기에다가 이벤트로든 윈도우 메시지로든 종료 명령을 내린 뒤 종료한다. 그럼 이미 실행돼 있는 A는 그 신호를 받고서 자신도 곧장 종료한다. 물론 A라는 한 프로그램의 소스에는 자기가 각각 다른 상황으로 실행되었을 때의 분기 처리가 모두 갖춰져 있어야 한다.

윈도우라면 WM_CLOSE 메시지가 있고 콘솔 프로그램에는 Ctrl+C 인터럽트가 있는데, 콘솔도 아니고 윈도우도 안 만든 채 다른 이벤트를 대기만 하고 있는 프로그램을 상대로 범용적인 종료 신호를 보내는 방법이 있는지 모르겠다. TerminateProcess라는 아주 무식하고 극단적인 방법을 쓰기보다는 그 프로그램이 직접 자신을 종료하도록 유도하는 게 바람직하기 때문이다.

인간 세계에서도 마찬가지다. 말이 안 통하는 미치광이가 만취한 상태로 자동차 운전대나 총칼 같은 위험한 물건을 잡고서 인질극 벌이고 행패를 저지르는 상황이 아닌 이상, 마취총을 쏘거나 머리를 벽돌로 내리쳐서 기절-_-시키거나 최악의 경우 저격을 하는 것보다는 곱게 말로 행동을 저지시키는 게 나은 것이다.
회사에서 필요 없는 사람을 짜를 때도 지방 한직 발령에다 빈 책상만 달랑 세팅해 놓고 아무 업무도 안 주면 그 사람이 더는 못 견디고 알아서 사표 쓰고 나가게 된다. 어지간해서는 대놓고 "너 해고. 내일부터 나오지 마" 이러는 일은 극히 드물다. 그건 고용주의 입장에서도 부담스러운 일이기 때문이다.

갑자기 쓸데없는 얘기가 좀 길어졌다만..
회사 업무 때문에 저런 성격의 EXE를 만들 일이 있었다.
DLL이라면 DllUnregisterServer이라고 원래는 COM용이지만 굳이 그 용도로만 쓰지는 않아도 되는 표준 인터페이스가 존재하지만, 얘는 EXE이다 보니 자신을 종료하여 제거 준비를 완료시키는 옵션을 구현했다. 그리고 패키지가 제공되기 전에는 당연히 그 옵션이 실행되게 이벤트도 넣어 줬다.

그러나 그럼에도 불구하고 이 프로그램을 설치하고 실행한 뒤에 제거를 하자, MSI는 "요런 프로그램이 실행 중이어서 제거를 제대로 할 수 없습니다" 대화상자를 띄우며 꼬장을 부렸다. 헐...;;
거기서 '무시'를 누르면 되긴 됐다. 그러면 종료/제거 스크립트가 실행돼서 "안 돼"가 "돼"로 바뀌었다. 실행 중인 자기 자신을 제거하는 테크닉이야 배치 파일을 이용해서 그리 어렵지 않게 구현 가능하기도 하고.

하지만 저런 대화상자가 뜨는 일은 반드시 막아야 했다. 저건 사용자가 부주의하게 띄워 놓은 게 아니라 우리 소프트웨어 제품이 정상적으로 일부러 띄워 놓은 프로그램이기 때문이다.
먼저 종료/제거 스크립트를 실행부터 좀 하고 나서 아직도 프로그램이 실행되고 있는 게 있으면 그걸 지적하면 되는데 저거 순서만 좀 바꿀 수가 없는지, Visual Studio에는 그런 기능이 없나 하는 아쉬움이 들었다. 얘로는 그럼 서비스 같은 건 제대로 배포하고 제거할 수가 없는 건지?

결국은 어떻게 했는가 하면 자기 자신을 다른 이름으로 복사해서 그놈을 대신 상주시키는 방법으로 문제를 피해 갔다. 그러면 MSI가 실행되어 있는 시점에서 자신이 제거해야 하는 프로그램은 실행돼 있지 않고, 새로 실행되는 동일체 프로그램이 자기 분신을 종료시켜 주기 때문에 모든 요구 사항을 만족할 수 있다. 하지만 굳이 안 해도 되는 삽질이 필요해졌다는 점에서는 여전히 아쉬움이 남는다.

2. 운영체제의 GUI 기본 글꼴 얻기

Windows에서 GUI의 기본 글꼴은 영문판 기준으로 System (불변폭) → System (가변폭) → MS Sans Serif → Tahoma → Segoe UI의 순으로 바뀌어 왔다. 한글 쪽은 명조 내지 바탕체가 들러리로 꼈다가 95/NT 시절부터 MS Sans Serif 대신 굴림으로 10년 가까이 장수한 뒤, 지금은 맑은 고딕이 대세가 됐다.
맑은 고딕과 Segoe UI의 경우 같은 서체이지만 윈도 비스타/7 시절과 8 이후 시절에 글자 모양이 미세하게 바뀌기도 한 것은 눈썰미 있는 분이라면 아실 것이다.

테마가 고전에서 Aero로 바뀌는 과도기를 거친 뒤, Windows 8부터는 자체 테마가 Aero시절에 비해 곡선 테두리나 그러데이션 같은 게 없어지고 굉장히 단촐해진 대신, 이 테마가 과거의 고전 테마를 완전히 대체하게 됐다. 자연히 UI 글꼴도 굴림이 밀려나고 맑은 고딕이 대세가 됐다.

그런데, 운영체제의 언어나 버전에 관계 없이 지금 시스템에 기본으로 지정돼 있는 글꼴을 얻어 오는 방법은 없을까? 이런 건 LOGFONT 값을 얻어 오거나 아예 바로 사용 가능한 stock HFONT 형태로라도 존재해야 하지 않을까? 시스템 색상에 대해서는 solid color 브러시를 얻어 오는 GetSysColorBrush 함수가 있는데 말이다.

실제로 요즘 프로그램 중에는 시스템의 기본 GUI 글꼴에 맞춰서 대화상자를 출력하는 것들도 많다. 비록 그렇게 동작하는 게 필수 관행은 아니지만 말이다. Visual Studio가 대표적인 예이고 <날개셋> 한글 입력기 프로그램들의 대화상자도 마찬가지. 기본 글꼴을 얻어 올 수 있어야 이렇게 동작을 할 수 있을 것이다.

Windows에는 이와 관련된 API가 물론 있긴 하지만 내력이 좀 꼬여 있다.
GetStockObject 함수를 보면 기본 펜이나 브러시 말고 글꼴을 되돌리는 아이템이 있다. 그러나 SYSTEM_FONT, SYSTEM_FIXED_FONT 이런 것들은 트루타입 글꼴과는 하등 관계가 없으며 말 그대로 System, FixedSys, MS Sans Serif, Terminal 같은 25년이 넘는 짬밥을 자랑하는 골동품 구닥다리 고정 봉인 비트맵 글꼴밖에 나오지 않는다.

그나마 유일하게 Windows 95/NT4에서 트루타입 글꼴을 되돌리는 stock 아이템이 딱 하나 추가되긴 했는데 그건 바로 DEFAULT_GUI_FONT이다. 얘는 한글판에서는 굴림 9포인트에, 그리고 아마 영문판에서는 Tahoma 정도에 매핑된다.
그럼 얘를 쓰면 되느냐 하면 그렇지는 않다. 얘는 좀 만들다가 만 물건-_-처럼 됐다. Windows 95 이래로 8에 이르기까지 그냥 굴림으로 고정돼 버렸다. Aero 테마라고 해서 맑은 고딕이 돌아오는 게 아니다.

실질적으로 현업에서 지금 운영체제의 기본 글꼴을 얻어 오는 방법은 SystemParametersInfo 함수를 쓰는 것이다. 아이템 인덱스로 SPI_GETICONTITLELOGFONT를 주면 기본 글꼴의 명세가 LOGFONT 형태로 돌아온다. 이를 토대로 HFONT는 우리가 수동으로 만들어서 사용하고, 다 쓴 뒤엔 해제를 해야 한다. 물론 대화상자의 글꼴을 바꾸는 건 GDI 개체를 만드는 게 아니라 대화상자 템플릿의 내용을 바꾸는 것이므로 방법이 약간 다르다.

3. 64비트 바이너리의 디렉터리 배치에 대한 생각

Windows는 잘 알다시피 Program Files 디렉터리가 32비트용과 64비트용이 나뉘어 있다. SHGetFolderPath 함수는 기본적으로 호출하는 프로그램의 비트수에 해당하는 디렉터리를 되돌리며, 이로써 32비트 프로그램 바이너리(EXE/DLL)와 64비트 프로그램 바이너리가 서로 자연스럽게 분리되어 따로 놀게 해 놓았다.

하지만 응용 프로그램의 바이너리 구분이 그렇게 마냥 깔끔하게만 되지는 않는 경우도 많다.
32비트와 64비트용 Program Files 디렉터리 구분은 편의상 존재하는 구분일 뿐이다. 32비트 디렉터리 아래에 64비트 프로그램이 있다거나 혹은 그 반대의 상황이 됐을 때 그 프로그램의 실행이 구조적으로 거부된다거나 하지는 않는다. 그러니 너무 강박관념적으로 구분하려고 애쓰지는 않아도 된다.

가령, 프로그램 자체는 전반적으로 32비트이지만 탐색기 셸 extension이나 시스템 훅 같은 일부 프로그램만 64비트인 경우..
그냥 Program Files (x86) 밑의 동일한 프로그램 디렉터리에다가 64비트 DLL도 이름을 달리해서 집어넣는다 해도 이상할 것 없다. 한두 개보다는 파일 개수가 많다면, 그 아래에다 x64 같은 별도의 디렉터리를 만들어서 말이다.

Visual Studio도 컴파일러는 32비트용 32비트 타겟뿐만 아니라 32비트용 64비트 크로스 컴파일, 그리고 64비트용 64비트 타겟 같은 컴파일러들이 모두 Program Files (x86) 아래에 있으며, Spy++ 같은 유틸도 32비트와 64비트 프로그램이 EXE와 훅 DLL 모두 한 디렉터리에 있다.
32비트 devenv.exe IDE에서 64비트 프로그램을 디버깅 하기 위해 중재 역할을 하는 64비트 원격 디버깅 서버 프로그램은 그 아래의 x64 디렉터리 안에 들어 있다. 오로지 걔들만을 위해 굳이 64비트 Program Files 디렉터리를 또 건드리지는 않았다.

그 반면, 64비트 바이너리가 전체 제품의 일부 형태로 있는 게 아니라 32비트와 완전히 대등하게 있는 경우라면 그때는 32/64비트 프로그램 디렉터리 아래에 대등한 파일과 디렉터리 구조를 갖추고 있는 게 바람직하다.
그리고 프로그램의 비트 수와 관계 없이 공유하는 데이터는 ProgramData라는 또 다른 공용 디렉터리의 아래에다 두면 된다.

<날개셋> 한글 입력기는 64비트 에디션이 처음으로 만들어지던 4.8 시절에 저런 식으로 디렉터리 구조를 싹 바꿨다. 아무래도 외부 모듈이 있다 보니 32비트와 64비트 바이너리는 애초에 대등한 구조가 되어야만 했으며, 그래서 32/64비트 프로그램 디렉터리를 모두 적절히 사용하게 만들어졌다. 프로그램마다 이런 차이가 있다는 걸 생각하면 되겠다.
하긴, 요즘은 관리자 권한을 요구하지 않고 간편하다고 해서 Program Files가 아니라 아예 사용자 계정 디렉터리에다가 프로그램을 설치하는 경우도 있으니 이건 32/64비트 구분이 더욱 모호해진 경우에 속한다.

4. 비주얼 C++ 솔루션의 중복 로딩 감지

Visual C++ IDE는 잘 알다시피 솔루션 단위로 동작한다. 한 솔루션 안에는 여러 관련 프로젝트들이 있을 수 있다. 솔루션은 프로젝트들의 묶음 컬렉션일 뿐이기 때문에 프로젝트를 바로 열면 그 프로젝트를 감싸는 껍데기 솔루션이 자동으로 만들어지기도 한다.
다만 다수의 솔루션들을 동시에 여는 것은 IDE의 능력 범위를 벗어나는 일이다. 그러니 IDE를 여러 개 실행해서 제각각 다른 솔루션을 열어야 한다.

그런데 비주얼 C++을 여러 개(=여러 인스턴스) 띄워서 여러 솔루션을 열어 놓고 작업을 하다 보면, 한 인스턴스에서 이미 열어 놓은 솔루션을 깜빡 잊고 다른 인스턴스에서 또 여는 일이 생기곤 한다. 뭐 그런다고 해서 프로그램이 뻑나거나 데이터가 날아가는 급의 큰 사고가 벌어지는 건 아니지만, 그래도 약간 불편한 일이 벌어진다.

인텔리센스 DB 파일에 공유 충돌이 발생하기 때문이다. 소스 코드의 상태와 인텔리센스 DB 상태를 언제나 동기화시키기 위해 IDE가 해당 파일을 열어 놓은 채 읽고 쓰는 동작을 완전히 독점하는 듯하다. 그래서 솔루션의 복수 중복 로딩을 시도하면, 되긴 하지만 나중에 연 쪽에서는 Class view라든가 인텔리센스가 동작하지 않는다.
이것은 과거 ncb 기반의 비주얼 C++ 200x 시절이든 지금의 201x 시절이든 동작이 동일하다. 다만 201x부터는 파일 쓰기가 가능한 임시 fallback 경로를 지정해서 문제를 좀 더 지능적으로 회피할 뿐이다.

하지만 에러 메시지를 출력하거나 대체 경로를 지정할 필요 없이,
비주얼 C++의 다른 인스턴스에서 해당 솔루션이 열려 있을 경우, 그 인스턴스로 이동만 시켜 주는 게 훨씬 더 월등히 나은 해결책이다. 그게 99.99% 사용자가 원하는 반응이 아닐까? "아, 이미 이 솔루션을 열어 놨었지!"
도대체 동일 솔루션을 중복 로딩해야 할 이유나 필요가 실무에서 무엇이 있겠으며, 이 상황에서 DB 파일을 건드리고 있을 프로그램은 비주얼 C++ IDE의 다른 인스턴스 말고는 다른 선택의 여지가 무엇이 있겠는가? 비주얼 C++의 inter-process 차원에서의 배려가 아쉬운 대목이다.

대체 경로를 지정해 주는 건 CD-ROM 같은 읽기 전용 매체에 저장된 솔루션을 열었을 때에나 유의미한 편의를 제공할 것으로 여겨진다.

5. 하이퍼-V

하이퍼-V인지, CPU 가상화인지 뭔지 잘은 모르겠지만 예전에 Windows Phone 플랫폼 개발을 위해서는 Hyper-V platform이라고 명명된 기능을 모두 켜야 했다.
그런데 VirtualBox가 멀쩡한 64비트 호스트 OS에서도 64비트 게스트 가상 머신을 만들지를 못하고 꼬장을 부리고 있어서 검색을 해 보니.. 이번엔 반대로 Hyper-V platform을 꺼야 했다.

Windows Phone 에뮬레이터도 일종의 가상 머신을 돌리는 것이고 64비트 OS에서만 돌아가는 기능이었는데 Hyper-V에 대해서 도대체 왜 이런 차이가 존재하는지를 잘 모르겠다.
그리고 VirtualBox는 64비트 호스트에서 64비트 게스트를 어떤 이유로든 만들 수 없다면, 그렇게 아무 말도 없이 슬쩍 감추기만 하지 말고 "64비트 게스트를 만들려면 Hyper-V를 꺼 주세요"라고 친절하게 메시지라도 좀 출력해 주지 하는 생각이 들었다.

Posted by 사무엘

2015/07/26 08:32 2015/07/26 08:32
, ,
Response
No Trackback , 3 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1120

Windows 운영체제에서 GUI의 핵심 구성 요소인 윈도우라는 물건은 자신만의 위치, 스타일, 소속 클래스, 부모 윈도우, ID(또는 메뉴 핸들) 등 여러가지 공통 정보를 갖는다. 이들은 숫자 형태로 표현되며, 대부분 GetWindowLongPtr 함수를 이용해 값을 얻을 수 있다.

그런데 그 공통 정보 중에는 숫자뿐만 아니라 문자열인 것도 있으니, 바로 '텍스트'이다. static 컨트롤이나 버튼들이 출력하고 있는 문장이나 단어들이 바로 자신이 갖고 있는 텍스트이다. 그리고 작업 표시줄에 걸려 있는 응용 프로그램들의 제목(Caption)도 다 윈도우의 내부 텍스트이다.

윈도우에 소속된 텍스트를 읽고 쓰는 함수는 잘 알다시피 Get/SetWindowText이다. 그러나 이것의 실제 구현은 그저 C++ 클래스에서

String getText() const { return m_strText; }
void setText(String t) { m_strText=t; }

요렇게 달랑 코딩하는 것만치 단순하지 않으며, 그보다 추상화 계층이 많고 사연이 훨씬 더 복잡하다.

(1) 먼저, 윈도우는 단순히 자신의 내부 버퍼를 토대로 텍스트를 지정하거나 되돌릴지, 아니면 이런 기본 동작을 무시하고 매번 자신이 능동적으로 반응을 할지를 자기 윈도우 프로시저가 재량껏 결정할 수 있다. 이것은 마치 WM_PAINT 메시지를 이미 저장돼 있는 버퍼를 바로 뿌리는 것으로 처리하는지, 아니면 매번 그래픽 API로 그려서 처리하는지의 차이와 같다.

아까도 얘기했던 static이나 버튼 컨트롤을 포함해 대부분의 윈도우들은 기본 내부 버퍼를 기반으로 동작하는 것만으로도 충분할 것이다. 그러나 에디트 컨트롤은 get의 경우 사용자가 입력한 문자열을 되돌리며 set은 자기가 편집하고 있는 텍스트를 변경한다. 이 텍스트는 당연히 기본 내부 버퍼가 아니라 에디트 컨트롤이 자체적으로 관리하는 자료 구조로부터 얻어진 데이터이다.

그럼 그런 customize를 어떻게 하는가? Get/SetWindowText는 대상 윈도우에다가 WM_GETTEXT, WM_GETTEXTLEN, WM_SETTEXT 메시지를 보내어 그 응답을 받는 방식으로 구현된다. 윈도우가 이 메시지를 처리하지 않고 그냥 DefWindowProc으로 넘기면 내부 버퍼에 대한 문자열 get/set인 기본 기능이 구현된다. 이 메시지는 자기가 에디트 컨트롤을 직접 구현하고 있을 때 정도에나 직접 처리하면 된다.

WM_CREATE에서 넘어온 CREATESTRUCT 구조체를 들여다보면, 이 윈도우를 생성한 CreateWindowEx에서 지정해 준 window name 정보가 있는데, 이것부터 자기 자료 구조에다가 복사를 해 둬야 할 것이다.
만약 내 윈도우가 대용량 텍스트를 다루고 텍스트의 일부 구간이 빈번하게 수정된다면, 저런 원시적인 메시지에만 의존하지 말고 해당 동작만을 위한 custom 메시지를 직접 구현해서 그걸 이용하는 게 효율적이다.

(2) 서로 다른 응용 프로그램끼리는 너무 당연한 말이지만 포인터를 주고받을 수 없다. 서로 메모리 주소 체계가 완전히 다르며, 상대방 프로세스의 메모리 내부를 들여다볼 수 없기 때문이다. 그렇기 때문에 이런 프로그램들끼리 가변 길이의 임의의 데이터를 주고 받으려면 WM_COPYDATA라는 특수한 메시지를 써야 하는데, 이건 의외로 굉장히 유용하고 괜찮은 물건이긴 하다.

그런데 WM_GET/SETTEXT 메시지도 비슷한 구석이 있다. 서로 다른 응용 프로그램간에도 마치 동일 프로그램인 것처럼 텍스트를 얻어 오거나 지정할 수 있다. 이 메시지에 대해서도 운영체제가 예외적인 변환 처리를 해 주기 때문이다.
단, 여기에서는 Get/SetWindowText 함수는 동작에서 약간 차이를 보인다. 같은 프로세스끼리 get/set을 하는 것은 메시지를 날리는 것과 동일하게 동작하는 반면, 다른 프로세스에 있는 윈도우에다가 get/set을 하면.. 그 윈도우의 윈도우 프로시저를 실행하는 게 아니라 그냥 그 윈도우에 있는 내장 버퍼의 문자열만 가져오는 걸로 끝난다. 왜 그렇게 된 걸까?

이것은 보안을 위해서이다. 해당 윈도우를 관할하는 응용 프로그램이 현재 메시지에 반응하지 않고 뻗어 있는데(hang) 이때 또 텍스트를 얻거나 지정하는 메시지를 보내면.. 그 요청을 한 우리 응용 프로그램까지 응답을 기다리느라 뻗어 버릴 수 있기 때문이다. 그래서 이때는 언제나 실행이 정상적으로 즉시 끝나는 게 보장되는 윈도우 내부 버퍼만을 건드리는 걸로 끝난다.

단순 내부 텍스트가 아니라 위험을 감수하고라도 해당 프로그램의 윈도우 프로시저가 실시간으로 실행한 텍스트를 얻고 싶으면, 함수만 달랑 호출하지 말고 직접 WM_GETTEXT 메시지를 보내라는 것이 설계 의도이다. 뻗을 걱정을 최소화하면서 메시지를 보내는 함수로는 SendMessageCallback이라든가 SendMessageTimeout 같은 물건들도 있으니 말이다.

사실, 현재 시스템에 떠 있는 모든 윈도우들을 조회하는 Spy++ 같은 급의 특수한 프로그램을 만드는 게 아닌 이상, 다른 프로그램들에서 제일 겉의 프레임 윈도우도 아니고 내부의 에디트 컨트롤까지 시시콜콜하게 내용을 다 들여다봐야 할 일은 거의 없을 것이다. 그러니 16비트 API에서 최초로 32비트 멀티스레드 환경으로 넘어갈 때, 텍스트를 얻는 동작은 함수 호출에 대해서만 저런 조치를 취하는 것이 적절한 조치라고 마소의 엔지니어들이 판단했다.

(3) 프로세스 장벽에 비해서는 아주 사소한 문제에 불과하지만, 사실은 문자열 인코딩 장벽도 있다.
어떤 윈도우가 있어서 요즘 추세대로 유니코드를 사용하며 WM_GETTEXT에 대해서는 L"abc" 같은 wide 문자열만 되돌린다. 그런 윈도우를 대상으로 GetWindowTextA 함수를 호출했다 하더라도 문자열 변환은 운영체제가 몰래 알아서 해 준다. 해당 프로그램은 ansi 문자열인 "abc"를 받는다.
요청하는 측에서는 GetWindowText나 SendMessage를 A와 W 중 어느 버전으로 호출했는지로 판단을 하고, 받는 윈도우는 RegisterClass(Ex)에서 A와 W 중 어느 버전을 이용해서 등록했는지로 판단한다.

그럼, 텍스트 지정 API와 관계가 있는 다른 분야 얘기를 또 둘 늘어놓고 글을 맺도록 하겠다.

부록 1. 창을 없애는 것과의 관계

윈도우의 텍스트를 얻어 올 때 함수를 쓰느냐 메시지를 보내느냐 하는 건, 마치 윈도우를 닫을 때 직통으로 DestroyWindow를 호출하느냐 아니면 WM_CLOSE 메시지를 보내느냐 하는 것과 비슷한 구석이 있어 보인다.

WM_CLOSE는 마우스로 X 버튼 누르거나 키보드로 Alt+F4를 누른 것과 같다. 이 메시지를 더 처리하지 않고 DefWindowProc으로 넘기면 얘가 DestroyWindow를 호출해 준다. 응용 프로그램의 경우 "이 문서를 저장하시겠습니까?"라고 질문 메시지를 출력하는 게 이 메시지를 받았을 때이다. 물론 '취소'를 누르고 메시지를 씹으면 실제로 닫히지는 않는다.

그 반면, WM_DESTROY는 이미 자기가 닫히고 없어지는 건 막을 수 없는 지경이 됐고, 그 전에 마무리 작업이나 하라는 통지이다. 아직 자기의 자식 윈도우들도 다 남아 있다. 이 메시지는 운영체제로부터 자연스럽게 받게 해야지 사용자가 인위로 생성하지는 말아야 한다.
이게 끝나고 자식 윈도우까지 다 사라진 뒤에 정말 마지막으로 오는 메시지가 바로 WM_NCDESTROY이다. 이때 하는 일은 C++ 클래스와 연결된 윈도우 프로시저에서 delete this를 하는 것 정도가 전부이다.

Get/SetWindowText가 여타 프로세스의 윈도우에 대해서는 다소 방어적으로 동작을 하듯, DestroyWindow에도 약간의 방어적인 제약이 있다. MSDN의 설명에 따르면, 얘는 다른 프로세스 정도가 아니라 아예 다른 스레드에 의해 생성된 윈도우를 파괴하지는 못한다. 그런 창을 닫으려면 그냥 WM_CLOSE라는 간접적인 방법만을 써야 한다.

앞서 말했듯이 WM_CLOSE는 응용 프로그램이 회피· 거부가 가능하기 때문에 응용 프로그램의 입장에서는 다른 스레드/프로세스의 특정 윈도우를 무조건적으로 없애지는 못한다. 굳이 그렇게 해야 할 필요도 없을 테고.
멀티스레드 환경에서 Windows의 창 관리자가 어떤 식으로 API를 설계했는지를 살펴보면 편의와 안정성을 상호 절충하기 위해 여러가지 생각을 했다는 점을 발견할 수 있다.

부록 2. 아이콘과 글꼴은 어떻게?

앞서 살펴본 것처럼 한 윈도우의 속성 중에 텍스트(TEXT)는 운영체제가 DefWindowProc을 통해 내부 텍스트를 관리하기도 하고 한편으로 값을 읽고 쓰는 동작을 customize하는 방법도 제공한다.
그럼, 윈도우와 관련된 부가 정보 중에는 아이콘과 글꼴은 어떻게 관리되는 걸까? 얘들은 함수가 아니라 메시지로만 값을 지정하고 얻어 온다는 공통점도 있다. WM_(GET/SET)(ICON/FONT)라고 말이다.

아이콘은 기본적으로 윈도우가 클래스에 소속돼 있으며, 그 클래스를 기반으로 만들어진 윈도우들이 한 아이콘을 공유하는 형태이다. 그러나 클래스 아이콘과는 별개로 필요하다면 각각의 윈도우도 자기 아이콘을 예외적으로 변경할 수 있다. 이는 특정 윈도우 클래스가 아니라 운영체제 차원에서 기본으로 제공되는 기능이다.
코드로 표현하자면 대충 이런 구조. commonIcon뿐만 아니라 myIcon도 있다는 뜻이다.

class Window {
    static Icon commonIcon;
    Icon myIcon;
public:
    Window() { myIcon=commonIcon; }
    Icon getIcon() { return myIcon; }
    void setIcon(Icon i) { myIcon=i; }
};

그렇기 때문에 대화상자는 윈도우 클래스는 동일하지만 응용 프로그램이 WM_SETICON을 보냄으로써 자신만의 아이콘으로 customize를 할 수가 있다. 그리고 이 기능은 대화상자에만 있는 게 아니라 임의의 top-level 윈도우가 다 갖추고 있다.

이런 아이콘에 비해 글꼴은 운영체제의 윈도우 내부 자료구조에 정보가 자동으로 저장되지 않는다. 내가 custom 컨트롤을 만들고 있고 거기에 대화상자의 기본 글꼴대로 문자를 찍는 기능이 있다면 내부적으로 HFONT 핸들을 멤버로 추가하고 WM_GET/SETFONT 메시지를 직접 구현해 줘야 한다. 즉, 운영체제에는 인터페이스만 정의되어 있을 뿐이다.
요약하자면 text는 내부 자료구조와 custom 동작이 모두 존재하고, icon은 내부 기본 동작만으로 충분하고, font는 기본 동작이 없기 때문에 필요한 경우 자체 구현을 해야 한다는 뜻이다.

Posted by 사무엘

2015/07/23 08:39 2015/07/23 08:39
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1119

1. Windows와 Office의 IME → 독립했다가 도로 운영체제로 합병

오~ 그러고 보니 MS Office 2013부터는 Office IME가 없어졌다는 걸 이제야 확인했다.
한때 웹브라우저인 Internet Explorer가 3~5버전 시절엔 자기가 운영체제의 셸을 뜯어고치고 comctl32, shell32 같은 시스템 DLL을 마구 업데이트 했던 것처럼, 문자 입력 쪽은 아무래도 오피스 제품의 기술 수준이 더 앞서 있었다.

그래서 IME 기반이던 다국어 입력 기능을 TSF라는 인터페이스로 바꾸는 것도 2001년에 MS Office XP가 최초로 도입했다. 그때부터 Office를 설치하면 한국어/일본어판은 자국어 IME도 새 걸로 바뀌기 시작했다. CJK 쪽이 아닌 라틴 알파벳 언어에서는 필기/음성 인식 같은 기능이 이 인터페이스를 기반으로 첫 도입됐다.

TSF 자체는 Windows XP도 운영체제 차원에서 완전히 내장돼 들어갔다. 그러나 이번엔 MS Office 2003 한글판이 제공하는 IME에서 글자가 아닌 단어 단위로 한자 변환을 하는 기능이 추가됐다. 이것은 운영체제의 IME에는 없던 기능이고 비스타에 가서야 추가됐다. 즉, 운영체제가 Office보다 한 박자씩 늦었던 것이다.

그러나 문자 입력 관련 기술들이 다 상향평준화하고 발전이 정체되면서, 굳이 Office가 IME를 또 제공할 필요가 없어졌다. 실제로 윈도 비스타 내지 Office 2007부터는 Office의 한글 IME나 Windows의 한글 IME나 차이는 거의 없어졌다. 괜히 똑같은 프로그램이 중복으로 존재하는 셈이 됐고 그 관행이 2010에까지 이어졌다가 2013부터는 드디어 Office IME는 없어졌다. 2000년대를 풍미했던 관행이 끝났다.

2. Windows와 IE → 합병하려다가 철회

운영체제의 셸의 발전을 주도하던 IE는 2000년대 중반까지 버전 6으로 90%에 달하는 점유율로 리즈 시절을 찍자, 발전이 정체되면서 역설적으로 Office IME와 비슷한 운명을 맞이할 뻔했다.
물론 IE의 버전업을 완전히 중단하는 건 아니고, IE 팀을 해체하고 Windows 팀에다가 합병시켜서 유지보수 비용을 줄이는 것이다. 그래서 IE는 독자적인 프로그램이 아니라 걍 운영체제의 일부로서 Windows와 함께 같이 유지보수를 하겠다는 것이 계획이었다. 웹브라우저는 전면 무료화가 돼 버리는 바람에 오피스나 개발툴과는 달리, 어차피 독자적인 고유 수입도 없으니 말이다.

그렇게 안일하게 생각했는데 파이어폭스의 급부상으로 인해 MS도 생각을 고쳐 먹었고, 급히 IE7을 만들게 됐다. 그리고 IE는 '셸 통합'이라는 예전 트렌드와는 달리 Windows 탐색기와는 다른 길을 가기 시작했다. Windows XP + IE6 시절에만 해도, 탐색기 창이 곧바로 IE 창으로 바뀌고, IE 주소 창에서 내 컴퓨터 디렉터리를 때리면 그 창이 곧바로 탐색기로 바뀌곤 했었는데.. 이것도 참 아련한 추억이다.
물론 지금은 아예 너무 누더기가 된 IE의 개발을 끝내고 마소가 브라우저를 처음부터 다시 만드는 지경까지 갔고 말이다.

3. Windows와 Office의 파일 대화상자 → 독립했다가 도로 운영체제로 합병

지금은 벌써 세월이 많이 지났지만, 2007년경엔 요런 일도 있었다.
원래 MS Office는 운영체제가 제공하는 파일 열기 대화상자 대신 독자 개발한 대화상자를 썼는데, 이젠 Office도 운영체제의 표준 대화상자로 복귀했다. 비슷한 시기에 출시된 Visual Studio 2008도 동일한 조치를 취했다.

운영체제가 보급으로 제공하는 대화상자는 기능이 너무 빈약하다는 이유로, 혹은 별 이유 없이 잉여력이 넘쳐서 Office 팀에서는 같은 기능을 또 만들어서 썼다. 대표적으로 favorite 폴더를 바로 클릭해서 이동하는 기능은 Windows에서는 2000/ME급에서야 도입됐지만 Office에서는 97 때부터 있었다.

하지만 Windows Vista급쯤 되니까 보급 표준 대화상자도 기능이 충분히 강력해졌고, 굳이 둘을 따로 만들 이유가 전혀 없어졌으니 그 시기에 팀간의 코드 통합이 이뤄졌다.

4. Windows와 Visual C++의 CRT → 통합 불가, 영구 독립

Visual C++은 운영체제 자체만큼이나 그야말로 전세계를 석권한 컴파일러이며, 마소 내부에서도 많이 쓴다. 그러나 전부 얘만 쓰는 건 아니었다.
Windows 개발팀이 자체적으로 보유하고 있는 컴파일러와 C 라이브러리 DLL, 그리고 비주얼 C++이 제공하는 컴파일러와 라이브러리가 처음엔 호환되었지만 시간이 갈수록 서로 호환되지 않게 되면서 문제가 심각한 지경이 됐다.

20년 전이나 지금이나 printf, strcpy, qsort 같은 표준 함수의 구현체가 한번 만들어 놓은 뒤에 도대체 바뀔 게 있나 싶지만..
보안 강화 버전이 도입되고 자료형이 32에서 64비트로 확장되는 등, C 라이브러리도 영구 봉인을 하기엔 바뀌는 게 굉장히 많았다.

그래서 Visual C++ 6.0과 Windows 98 시기를 마지막으로 C 라이브러리는 msvcrt(운영체제) 계열과 msvcr???(VC++) 계열로 서로 완전히 이원화가 돼 버렸다. 그 전에는 이름조차도 crtdll(운영체제)과 msvcrt(VC)로 따로 놀았는데 그건 운영체제가 crtdll을 버리고 msvcrt로 간 것이었다. Windows, Visual Studio, Office, IE 등 마소를 구성하는 핵심 부서들간에는 코드의 공유가 생각보다 원활하게 이뤄지지는 않고 있는 듯하다.

마소에서 만든 Windows나 Office의 구버전 바이너리들은 링커 버전 같은 내부 구조를 보면 일반적인 Visual C++ 컴파일러가 생성해 주는 형태가 아니었다. 그러던 것이 Vista 타이밍이 되면서 그 직전에 나온 가장 최신 Visual C++의 버전이 찍히게 되었으며 Office의 경우 Windows가 아닌 VC++의 CRT 라이브러리를 쓰는 형태로 바뀌었다.

그러고 보니 옛날에는 플랫폼 SDK 내지 DDK에 내장돼 있는 무료 컴파일러와 Visual C++ 컴파일러도 서로 달랐다. 그러나 이 역시 지금은 통합이 이뤄졌다. Visual C++이 일부 기능이 빠진 무료 express 에디션이 2003~2005 사이부터 나오기 시작했으며, 2013부터는 아예 MFC와 리소스 컴파일러까지 다 포함된 Community 에디션이 통째로 조건부로나마 무료로 풀렸으니 말이다. 플랫폼 SDK에 내장된 무료 C/C++ 컴파일러는 비주얼 C++ 원판에 비해서는 최적화 성능이 떨어지는 물건이었으나 그것도 비주얼 C++의 오리지널 컴파일러가 대체를 하게 됐다.

마소 내부에서 컴파일러는 비주얼 C++로 이렇게 교통 정리가 돼 가고 있으나, 워낙 다양한 버전들이 난립하고 있으니 요즘은 혼란의 여지를 원천봉쇄하려고 Visual C++ 2015부터는 "CRT 정도는 어지간해서는 각자 걍 static link하세요. 디스크 용량도 많은데.. ㄲㄲㄲ" 이러는 추세이다. 어찌 보면 CRT의 DLL 링크라는 개념이 존재하지 않던 16비트 시절로 회귀하는 것이기도 하다.

Posted by 사무엘

2015/07/20 19:21 2015/07/20 19:21
,
Response
No Trackback , 5 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1118

사용자의 마우스 클릭에 동작하는 GUI 요소들은 일단은 버튼의 down이 아니라 up 시점 때 반응을 한다. 모든 버튼(push, check, radio 공히)이나 하이퍼링크들이 이렇게 동작하며, 우클릭 메뉴도 오른쪽 버튼을 뗐을 때 튀어나온다. 그리고 이 타이밍 때 그 이름도 유명한 BN_CLICKED라는 notify 메시지가 부모 윈도우에 전달된다.

그에 반해, down 때 바로 반응을 하는 것은 스크롤 바나 슬라이더나 스핀(옆의 숫자를 증가시키거나 감소시키는 up-down 컨트롤)처럼 누르고 있는 동안 '연타'의 여지가 있는 버튼, 아니면 어떤 명령이 즉시 실행되는 게 절대로 아님이 보장되는 첫 단계 메뉴(파일, 편집, 보기) 정도로 국한된다.

그런데 이건 어떨까? 버튼인데, 사용자가 누르고 있는 동안만 풍선 도움말이나 다른 추가적인 정보가 튀어나오고, 버튼을 떼면 그것들이 사라진다. down과 up만 감지하면 되고 그 사이에 연타 개념은 없다.
이건 일단 Windows의 표준 컨트롤에는 없는 기능인 듯하다. 하지만 의외로 요런 UI가 쓰인 경우가 존재한다.

가장 먼저 떠오르는 예는 2007 버전이 나오기 전, 과거의 MS Excel이다. 선에 안티앨리어싱이 없는 건 그렇다 치더라도 색깔이 요즘 것에 비해서 우중충하고 덜 예뻐 보인다.

사용자 삽입 이미지

옛날에 엑셀에는 차트 마법사를 통해서 차트를 만들곤 했다. 1단계는 차트의 종류를 고르는 단계였는데, 아래의 길쭉한 버튼을 살포시 누르고 있으면 지금 사용자의 데이터가 어떤 형태의 차트로 표시되는지 preview를 잠시 볼 수 있었다.
버튼을 한번 눌렀다 뗄 때마다 preview 모드와 선택 모드가 toggle 형태로 바뀌는 게 아니라는 점을 주목하자. 상당히 독특한 UI가 아닐 수 없다. 왜 이렇게 만들었을까?

어지간하면 그냥 대화상자의 왼쪽에 차트의 type과 sub-type을 트리 컨트롤 같은 걸로 모두 때려박고, 오른쪽 전체에 preview 화면을 할당해도 될 듯하지만 그렇게 하기에는 오버헤드가 너무 크고, 또 차트의 타입 자체를 그림으로 표시하기 위한 공간도 많이 필요하니 이런 식으로 preview는 필요할 때 잠깐만 볼 수 있게 UI를 만든 듯하다.

참고로 Office 2007과 그 이후부터는 전통적인 차트 마법사가 없어지긴 했지만, 차트의 종류를 선택하는 대화상자 자체는 남아 있다. 단, 이렇게 버튼을 누르고 있는 동안 preview를 잠깐 보는 기능은 없어졌다.

원래 운영체제의 표준 버튼은 아까도 얘기했듯이 눌렀다 뗀 뒤의 BN_CLICKED 이벤트만 있지, 저렇게 눌러진 것에 대한 이벤트는 제공하지 않는다. 그러니 저런 기능을 구현하려면 일반적으로는 윈도우 프로시저를 서브클래싱하여 마우스 좌클릭과 Space/Enter 누름을 감지해서 좀 불편하게 구현해야 한다.
하지만 MS Office 제품 중에 Word와 Excel은 대화상자 컨트롤들을 운영체제 함수 대신 자체 GUI 엔진으로 구현했기 때문에 편법 없이 저런 기능들이 처음부터 자연스럽게 구현이 가능했을 것이다.

그리고 또 다른 예는 아주 최신 프로그램이다. 바로 Internet Explorer 11.

사용자 삽입 이미지

IE는 최신 버전이자 아마 마지막 버전이 될 것으로 보이는 11이 굉장한 쇄신을 한 것 같다. 예전보다야 가벼워지고 속도가 빨라지고, 텍스트 입력란이 TSF A급으로 바뀌고, 굴림체로 찍히던 기본 글꼴이 맑은 고딕으로 바뀌는 등 변화가 많다.

얘는 웹사이트 내부의 입력란에서 ID와 비번을 입력하기 시작했을 때 오른쪽에 자그마한 버튼이 뜬다. ID 입력란의 오른쪽 끝에는 X 버튼이 생겨서 이걸 누르면 ID가 싹 다 지워진다. 물론 지우는 동작은 눌렀다가 '뗐을 때' 행해진다.
그런데, 비번 입력란의 오른쪽에는 눈알 모양의 버튼이 생기며, 이 버튼을 누르고 있으면 동그라미로 표시되는 암호 문자열의 실제 문자열이 잠시 보인다. 이것 역시 마우스 버튼을 떼는 순간 원상복귀된다!

(1) 엑셀의 차트 미리보기와, (2) IE에서 비번 훔쳐보기가 이 동작에 상당히 적절하게 배치된 경우라고 생각된다. peek라는 동작을 표현한다고나 할까. 이것 말고 요런 동작이 유용하게 쓰일 만한 상황이나 이미 적용된 예가 무엇이 있는지 궁금하다.

마우스 포인터를 갖다대고 있는 동안 뭔가 도움말이나 추가 정보가 나타나는 UI는 그리 새삼스러운 물건이 아니다. 이미 20년 전부터 툴팁이라고 불리는 풍선 도움말이 그 역할을 하고 있으니 말이다.
단, 터치스크린에서는 click과 hover가 구분이 없기 때문에 저렇게 가리키고 있는 것을 표현할 수 없으니 누르고 있는 동안만 뭔가 추가 정보를 표시하는 기능이 더욱 필요할 것이다.

이렇게 글을 바로 맺기는 아까우니 MS Office에서만 볼 수 있는 독특한 UI를 두 가지 좀 늘어놓도록 하겠다.

1. 먼저, 탭 컨트롤의 각각에 Alt 액셀러레이터가 붙은 모양이다. 난 워드나 엑셀을 쓸 때마다 늘 신기하다고 생각해 왔다.

사용자 삽입 이미지

Alt 액셀러레이터는 IsDialogMessage라는 대화상자 전용 메시지 처리 함수가 구현해 주는 것이고, Alt는 윈도우 텍스트를 기반으로 한 윈도우당 한 글자씩만 배당된다. 그러므로 저게 정석적으로 가능하려면 각각의 탭 헤더가 마치 라디오 버튼처럼 독립된 윈도우를 구성하고 있어야 하나, 운영체제의 탭 컨트롤은 그런 구조를 하고 있지 않다.

Alt+단축키를 눌렀을 때 탭 컨트롤의 탭이 어떻게든 전환되게 하는 것 자체는 불가능하지 않다. 하지만 그러려면 윈도우 프로시저 수준이 아니라 해당 응용 프로그램의 메시지 loop 차원에다가 예외적인 처리가 필요하다. MFC로 치면 해당 대화상자의 PreTranslateMessage 함수를 굳이 새로 구현해야 한다.
그래서 본인은 MS Office의 저런 프로퍼티 시트 대화상자를 보면 신기하다는 생각이 든다.

2. 그리고, 라디오 버튼을 더블 클릭하면 대화상자가 OK(확인)으로 종료되는 것도 꽤 독특하다.
요즘 Office 프로그램들이야 워낙 기능이 방대하기 때문에 공간을 아끼기 위해 콤보 박스를 주로 쓰지, 라디오 버튼을 보기는 힘들지만.. 대표적으로 '화면 확대'라든가 탭의 종류를 고르는 화면에서 이를 확인할 수 있다.
아.. 그러고 보니 리스트 박스에서 아이템을 더블 클릭했을 때 대화상자가 재량껏 OK로 종료되는 건 생소하지 않다. 하지만 라디오 버튼을...? 한번 생각해 보시기 바란다.

Posted by 사무엘

2015/07/13 08:45 2015/07/13 08:45
,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1115

Visual C++ 디버거 관련 생각

코딩으로 먹고 사는 프로그래머 내지 소프트웨어 개발자에게 필요한 것은 단순히 새로운 코드를 스스로 잘 작성하는 능력뿐만이 아니라, 문제가 생겼을 때 디버깅을 잘 하고 남이 만들어 놓은 코드를 신속하게 읽고 분석하는 능력이다. 아니, 업계에서는 어찌 보면 후자가 전자 이상으로 더 중요한지도 모른다. 왜냐하면 오늘날은 뭔가 완전히 새로운 솔루션을 천재 프로그래머 한 명에서 밑바닥부터 새로 만들어 낼 일은 거의 없어졌기 때문이다.

나의 영원한 친구는 비주얼 C++이고, 비주얼 C++ IDE는 예로부터 굉장히 편리한 디버깅 기능을 제공해 왔다. (일례로 Shift+F5는 엉덩국 홍콩행 C언어 병맛 만화에도 나올 정도로 유명한 비주얼 C++ 단축키이다. 디버그 중단 =_=)
특히 IDE가 32비트임에도 불구하고 64비트 프로세스를 아주 seamless하게 디버깅 해 내는 건 아무리 생각해도 대단해 보인다. 물론 이를 구현하기 위해 내부적으로는 64비트 디버그 서버 프로세스를 따로 만들고, 걔가 IDE와 디버기 프로그램 사이를 중재하고 있긴 하다. 그렇게 하는 것 말고는 기술적으로 다른 방법이 없다.

다만, 여러 편리한 기능에도 불구하고 본인이 일말의 아쉬움을 느끼는 점들을 나열하자면 다음과 같다.

1.
소스 코드에서 breakpoint를 여러 곳에 지정해 놓고서
한 breakpoint(A)가 적중한 뒤부터 다른 쪽 breakpoint(B)를 지났을 때 프로그램이 멈추게 하는 게 지원됐으면 좋겠다.

디버깅을 하고자 하는 지점이 평소에도 자주 지나는 곳이긴 하지만, 특정 조건이 만족된 뒤부터 실제로 의미를 갖는다는 뜻이다.
이런 상황에 대비해서 n회 이상 적중했을 때 멈춤, 특정 변수값이 변했을 때 멈춤 같은 여러 breakpoint 옵션이 있긴 하지만..
다른 breakpoint의 hit에 의존하여 그 뒤부터 멈추게 하는 기능은 Visual C++에서 지금까지 못 본 것 같다. 이거 회사일을 할 때와 <날개셋> 개발 중에 자주 필요성을 느꼈다.

IDE 내지 디버거가 이런 기능을 지원 안 해 주면 결국 사람이 해당 기능을 직접 코드에다 써 넣어야 한다.
bool 타입의 전역변수(bkpoint)를 하나 만든 뒤 A에 해당하는 지점에서는 bkpoint=true를 지정하고,
B에 해당하는 지점에서는 extern bkpoint; if(bkpoint) DebugBreak() 를 호출하는 식이다.
이런 긴급/땜빵 코드를 집어넣을 때는 굳이 클래스 따위 생각할 필요 없이 global scope이 존재하는 C/C++이 편리하게 느껴진다.

하지만 조건을 지정하는 코드와 멈추는 코드가 서로 다른 모듈에 있는 경우(static LIB, DLL, EXE 등) 여러 모듈을 고쳐서 재빌드해야 하고 일이 골치아파진다. 그러니 코드를 건드릴 필요 없이 이런 기능 정도는 개발툴이 바로 지원해 주는 게 속 편하다.

사실, 이런 쪽의 기능이 계속 추가되다 보면 디버거도 전처리기나 빌드 시스템처럼 일종의 프로그래밍 가능한 독자적인 시스템이 될지도 모르겠다. 사실은 <날개셋> 한글 입력기의 개발에서는 todo list를 분류하고 체계화하는 것부터가 전략이고 프로그래밍이다.

2.
디버그 로그를 찍는 API 함수는 OutputDebugString이며, 얘는 문자열을 받아들이는 여느 함수들과 마찬가지로 W 버전과 A 버전이 있다. 그러나 얘는 실제로는 오늘날의 NT 계열 운영체제에서도 유니코드를 지원하지 않는다.
다른 함수들은 A 버전이 문자열을 변환한 후 W 버전을 호출하는 형태이지만, 이 함수는 뜻밖에도 W 버전이 문자열을 변환한 후 내부적으로 A 버전을 호출한다.

물론 99%에 가까운 상황에서 프로그래머가 필요로 하는 로그 문자열은 단순히 알파벳과 숫자만으로 이뤄져 있어도 하등 지장이 없으며 충분하다. 그러나 본인처럼 문자 입력기 내지 마이너한 유니코드 문자/글꼴 쪽을 종종 연구하는 입장에서는.. 그런 문자열을 디버거로 곧장 확인할 수가 없어서 불편을 겪은 적이 생각보다 자주 있었다.

디버거 쪽이 여전히 1바이트 문자열 기반 프로토콜이 관행이어서 유니코드를 도입할 수 없다는 말도 변명에 지나지 않는다. 그런 용도로 쓰라고 엄연히 utf8이라는 물건이 있기 때문이다. 소프트웨어 국제화의 혜택이 사용자 인터페이스뿐만이 아니라 이런 데에까지 도달해야 하지 않을지?
직접 확인해 보지는 않았지만 C++말고 C#이나 자바는 디버그 로그가 유니코드를 지원 안 할 리가 없으리라고 생각한다.

3.
최신 201x 버전에서도 가끔은 프로젝트를 빌드하는 데 쓰였던 멀쩡한 소스 파일이 디버거에서 인식이 안 되는 경우가 가끔 있다. F9를 눌러도 해당 라인엔 빈 동그라미○만 생기지 breakpoint가 성공적으로 만들어졌음을 의미하는 ●가 생기지 않는다.
DebugBreak()를 손수 집어넣어서 강제로 세우더라도 그 지점에서 call stack 리스트가 제대로 생성돼 있지 않다. 또한 breakpoint는 만들어지지만 심벌 테이블이 좀 맛이 갔는지 변수값 조회가 동작하지 않을 때도 있다.

본인은 이 현상에 대해 정확한 문제 재연 조건과 원인, 해결 내지 예방 방법을 아직도 정확히 모른다. 프로젝트 전체를 재빌드하고 Visual C++ IDE를 재시작하고 나면 해결되기도 하고 안 그럴 때도 있었던 것 같다. VC++ 6의 고질병이던 허접 인텔리센스 ncb가 깨지는 문제는 오늘날 더 볼 일이 없지만, 디버깅은 여전히 완벽하지 못하다.

그러고 보니 디버그 심벌 데이터베이스는 IDE의 인텔리센스 데이터베이스와는 커버하는 영역이 정확하게 같을 수가 없겠다는 생각이 들었다. 전자는 우리 프로젝트 밖에서 빌드되어 LIB, DLL들에 존재하는 소스 코드와 그쪽 심벌까지 모두 연계해서 동작해야 하기 때문이다. (인텔리센스 정보가 없는 곳)

4.
이 외에도,
함수 안으로 들어가긴 하는데(F11), 그 함수의 인자와 관련된 함수 호출들은 모두 무정차로 건너뛰고서 들어가는 step in이 있었으면 좋겠다. 즉, A(b(), c()) 줄에서 시작한다면 b()나 c()로 들어가는 게 아니라 바로 A()의 몸체로 들어간다는 뜻이다.

그리고 디버깅과 직접적인 관계는 없지만, 텍스트를 검색하는데 주석 내용은 빼고 검색하거나 주석에서만 검색하는 기능도 있으면 좋겠다. #if 0과는 달리 주석 영역을 파악하는 건 단순 텍스트 패턴 매칭이므로 그리 어렵지 않을 것이다.

Posted by 사무엘

2015/07/01 19:31 2015/07/01 19:31
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1111

« Previous : 1 : ... 10 : 11 : 12 : 13 : 14 : 15 : 16 : 17 : 18 : ... 31 : Next »

블로그 이미지

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

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

«   2024/03   »
          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:
2619846
Today:
2845
Yesterday:
1544