Windows XP에서부터 내가 생성하는 exe 바이너리의 내부에 xml 문서 리소스 형태의 메타데이터를 집어넣는 게 필수 관행이 됐다.
가장 대표적인 용도는 (1) 내가 사용하는 시스템 DLL의 버전을 지정해서 로딩 방식을 시스템 디렉터리 말고 딴 곳으로 강제로 바꾸는 것이다.

이 방식으로 (1-1) 공용 컨트롤을 6.0 버전을 사용한다고 명시해야 각종 컨트롤에 XP의 새끈한 테마가 적용되어 나왔다. 테마 기능을 추가하는 과정에서 comctl32.dll이 XP 이전과 XP 이후 것은 하위 호환성이 없이 서로 단절됐기 때문이라 한다.
내 프로그램의 비주얼이 시대에 뒤떨어진 구닥다리처럼 보이지 않게 하려면 이거 지정은 선택의 여지가 없는 필수이다.

XP 이후로 지금까지 comctl32.dll이 7.0으로 올라간다든가 해서 또 breaking changing을 겪지는 않았다.
Windows 8부터는 명목상 고전 테마와 표준 테마의 구분이 없어지긴 했지만, 그래도 공용 컨트롤 6.0 지정을 안 해도 되는 건 아니다. 구닥다리 비주얼 자체가 호환성 때문에 완전히 없어진 건 아니기 때문이다.

뭐 comctl32가 가장 대표적이긴 하지만 이 외에 (1-2) GDI+ (gdiplus.dll)와 비주얼 C++의 CRT DLL, 그리고 MFC DLL이 지난 200x년도에 이 방식을 사용해 로딩된 적이 있다.
Windows XP 시절엔 WinSxS 디렉터리에 파일이 몇 개 없었지만 Vista 이후부터는 도대체 뭐가 들어갔는지 여기에 디렉터리 수가 무려 수천~만수천 개에 달할 정도로 많아졌다. 199x년대에 한번 DLL hell을 겪은 뒤, 너무 난장판이 돼 가는 시스템 디렉터리를 이런 식으로 정리를 하려 했던 모양이다. 같은 DLL이라도 버전별로 쫙 따로 분류를...

하지만 DLL을 배포하기가 너무 불편하다는 원성이 빗발치면서 VC++ 201x부터는 CRT와 MFC DLL 로딩 방식이 다시 재래식 시스템 디렉터리 기반으로 복귀했다.
또한 GDI+도 오늘날은 기존 GDI만큼이나 재래식 유물로 전락했고... 요즘은 딱히 이 WinSxS 기반 DLL 로딩이 활발히 쓰이고 있는지 모르겠다.

DLL 버전 설명이 좀 길어졌는데, 이것 말고도 xml에 들어가는 정보로는
(2) 내 프로그램이 고해상도+가변 DPI를 인식한다는 플래그
(3) 반드시 관리자 권한이 필요하다는 식의 권한 플래그

가 Vista에서 추가되었다. 시스템 차원에서의 고해상도 DPI 설정이야 더 말할 것도 없고 Windows 8.1부터는(8도 아님) 실행 중에 on-the-fly로 모니터 단위 DPI 설정이 변경되는 것에도 추가로 대비가 돼 있어야 한다. 그런 표식이 없으면 그냥 그래픽 카드의 힘으로 프로그램 화면이 그대로 기계적으로 확대 축소된다.

먼 옛날, Windows NT 3.x 내지 95 시절엔 화면 해상도를 재부팅 없이 on-the-fly로 바꾸는 것만 해도 굉장한 혁신이었는데 참 격세지감이다. 시스템 DPI도 예전에는 재부팅이 필수였지만 그래도 Windows Vista쯤부터는 그냥 재로그인만 하면 되게 규제가 완화됐다.

그리고 Vista부터인지 7부터인지는 모르겠지만 (4) 이 프로그램이 인식하는 운영체제 목록도 xml에다 명시하게 되었다. PE 실행 파일 포맷에 명시된 OS 버전의 역할을 어느 정도 대신하게 됐다.
Windows 8.1부터는 GetVersionEx가 알 수 없는 이유로 인해 봉인되어서, 프로그램이 인식하는 걸로 등록되지 않은 최신 운영체제의 버전은 Windows 8 이상으로는 알려주지 않게 바뀌었다. 도대체 이런 만행을 왜 무슨 이유 때문에 저질렀는지 난 모르겠지만.. 도대체 버전을 숨겨서 뭐 하게?

그리고 Windows 7도.. 자기 버전이 명시되지 않은 EXE 프로그램이 이름이 setup이라거나 하면, 실행 후에 "이 프로그램은 혹시 설치 프로그램인가요? 뭔가가 제대로 설치/제거되지 않았을 수 있습니다" 이런 꼬장을 부린다. 무슨 호환성과 관련된 조치인 듯하다.

결국 처음에는 공용 컨트롤 때문에 사용하기 시작한 xml인데 이제는 무슨 실행 프로그램을 제대로 만들려면 메타데이터 xml을 집어넣는 건 거의 필수가 되었다. 이 프로그램이 미래에 운영체제에서 새로 도입되는 시스템이나 기능을 받아들일 준비가 됐다는 것을 나타내는 증서 역할을 하다 보니, 들어가는 내용이 점점 증가하고 있다. 저 네 가지 아이템 말고 내가 또 빠뜨린 게 있는지 모르겠다.
그리고, PE 헤더에 운영체제 버전과 서브시스템 버전은 왜 따로 존재하고 둘의 차이가 무엇인지도 본인은 궁금하다.

* 추가 설명

사실, 메모장이나 워드패드처럼 Windows에 포함되어 있는 기본 프로그램들은 내부의 매니페스트 XML을 열어 보면 공용 컨트롤, 고해상도 DPI, 실행 권한 같은 설정은 있지만 굳이 운영체제 정보가 들어있지는 않다. 특정 버전의 운영체제에 포함되어 있는 프로그램들이 또 그 운영체제 GUID가 내장되어 있다거나 하지는 않다.

이때는 PE 헤더 차원에서 명시된 운영체제 버전이 활용된다. 이 버전이 Windows 8.1에 달할 정도로 높은 값이 들어있으면 GetVersionEx 함수도 Windows 8.1의 버전도 정확하게 되돌려 준다. 다만 이 경우, 그 실행 파일은 Windows 8.1 미만의 운영체제에서는 전혀 실행되지 않는다는 것을 감안해야 한다. 심지어 7에서도 실행이 거부된다.

Posted by 사무엘

2015/09/03 08:30 2015/09/03 08:30
, , ,
Response
No Trackback , 3 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1134

Windows 10 사용기 외

1. Windows 10

그 이름도 유명한 Windows 10을 본인도 드디어 입수했다. Mac OS와 Windows 모두 10을 최종 메이저 버전으로 설정했다는 게 무척 흥미롭다.

개인 컴은 물론이고 회사 컴도 평소에 활발하게 쓰던 물건은 "Windows 10 다운로드 준비가 끝났는데 설치할까요?"가 진작부터 뜬 반면,
평소에 잘 안 쓰고 "여기에나 한번 10을 깔아 볼까?"라고 정작 비워 놨던 컴은 한참을 기다려도 Windows 10 설치 말이 없었다. "준비되면 알려 주세요"를 몇 번이나 클릭했는데도 감감 무소식. 이것도 평소에 로그인 횟수나 네트워크 트래픽을 감안해서 업데이트 순서를 짠 건지는 모르겠다.

참다못해 Windows 10 설치 프로그램을 직접 다운로드 해서 실행한 뒤에야 운영체제를 7에서 10으로 바꿀 수 있었다. 다운로드와 설치는 물론 금방 끝난 건 아니지만, 그렇다고 Windows 8.1을 설치하던 시절보다 터무니없이 오래 걸리지는 않았다.

설치 직후에는 바탕 화면의 personalize가 되지 않았다. 인증이 필요하대서.. "으잉? Windows 10은 완전 무료 아니었나? 자동 업데이트를 안 하고 수동 설치를 해서 그렇나?" 어리둥절했는데 재부팅을 한번 하고 나니 인증은 저절로 패스 됐고, 까맣던 배경 그림은 10을 설치하기 전의 모습으로 되돌아왔다.

비주얼에 대한 소감은..
프로그램의 제목이 가운데가 아니라 왼쪽 정렬로 돌아왔으며, 글씨 크기도 약간 더 크다가 다시 메뉴 글씨 크기와 동일하게 돌아왔다. 또한 화면을 부분만 차지하는 시작 메뉴가 되돌아왔으며 프로그램 창 주변에 그림자 그러데이션이 생긴 것은 일면 과거의 Windows Vista/7 시절로의 회귀를 떠올리게 한다.
창의 최소/최대화와 닫기 버튼이 굉장히 커지긴 했는데 정작 _ □ X 모양은 너무 대충 그려 넣은 듯. -_-;;

명령 프롬프트가 가로폭 조절이 가능해진 것이 무척 인상적이다. 하지만 글꼴은 여전히 제대로 customize가 안 된다. 하다못해 먼 옛날 9x 시절에는 코드 페이지가 무엇이냐에 따라 Courier New나 Lucida Console이라도 볼 수 있었는데 너무 칙칙한 글꼴은 오히려 NT 계열이 9x보다도 퇴보했다.

운영체제를 업데이트 한 직후, 이전까지 사용하던 프로그램들은 10에서도 거의 그대로 곧장 사용이 가능했다. 그러나 <날개셋> 한글 입력기는 편집기만 남아 있고 외부 모듈은 운영체제의 IME 목록에서 사라졌다.
허나 이건 프로그램을 재설치만 하면 해결되니 그리 심각한 문제는 아니다. 또한 지금까지 파악한 바로는 Windows 10은 문자 입력에 관해서 딱히 기술적으로 크게 바뀐 게 보이지 않는다. 심지어는 세벌식 파워업도 10의 MS 한글 IME를 대상으로 잘 동작한다.

2. <날개셋> 한글 입력기에서 발견된 문제

다만, <날개셋> 한글 입력기가 Windows 10에 대비하여 개선해야 할 사항은 크게 두 가지가 발견됐다.

(1) 첫째, Windows 10은 Metro 앱도 데스크톱에서 뭔가 일체형으로 연계하며 동작하게 된 듯하다. Windows 10의 상징이라 해도 과언이 아닌 엣지(Edge) 브라우저도 기술적으로는 Metro 앱이다. Metro 모드에서는 클래식 데스크톱 GUI를 사용하는 기능들이 동작하지 않는다. 고로 <날개셋> 제어판이나 About 대화상자 같은 것도 못 띄운다. (그걸 시도하면 그냥 씹히거나 프로그램 전체가 그냥 튕긴다)
8 시절에는 메트로 앱들은 데스크톱의 입력 도구모음줄에 접근을 할 수 없으니 신경 쓸 필요가 없었는데, 이제는 메트로 모드에서는 이런 기능들을 건드릴 수 없도록 도구모음줄 버튼이나 메뉴를 흐리게 처리해 줘야겠다.

날개셋 말고 다른 IME들은 이 문제를 어떻게 해결했느냐 하면 한글 IME는 아예 대화상자가 출력되는 명령들을 모조리 없애 버렸다. 심지어 8에는 있던 About 명령조차도 없앴다. 일본/중국어 IME는 GUI가 나오는 기능들은 몽땅 다른 프로그램을 통해 실행되게 되어 있다.

사실, 한글 IME도 부수/획수 한자나 필기 인식 같은 보조 입력 도구는 별도의 프로그램을 통해 구동한다. 그러나 내 프로그램은 이런 것들이 다 in-process 형태로 구동된다. 그래서 Metro 모드 같은 데서는 여러 제약이 걸리는 것 같다. 금방 해결 가능한 문제는 아닌 것 같고 프로그램 구조를 앞으로 어떻게 유지할지 생각을 해 봐야겠다.
참고로, 엣지 브라우저 자체는 크롬의 마소 버전이라고 생각해도 좋을 정도로 "빠르고" 산뜻해서 느낌이 굉장히 좋았다. 그거 하나는 인정한다.

(2) 둘째, 엣지에서 페이스북에 접속해서 댓글을 써 보니, 한참 글자를 입력하다가 비한글 문자를 입력하는 순간, 예전에 입력 중이던 글자가 몽땅 사라지는 문제가 있었다. "가을에." -> "에."만 남는 식.
또한 명령 프롬프트에서도 내 프로그램으로 한글을 입력해 보면, 조합하던 글자가 덮어써진다. "가" "가을" "가을에"가 되는 게 아니라 "가" "을" "에". 신기하게도 MS IME로 한글을 좀 입력하다가 다시 날개셋으로 돌아오면 명령 프롬프트는 이 문제가 없어짐.

100% 재연 가능하고, MS IME는 안 그런데 내 프로그램만 그렇고.. 현상 자체는 완벽하게 파악이 됐지만 이건 또 MS가 무슨 농간을 부린 건지 허무한 생각이 든다. 아마 둘 다 같은 원인 때문일 거라고 추측만 하는데.. 제일 골치 아픈 부류의 문제이며 해결이 쉽지 않을 수도 있다.

3. key의 인식 방법과 관련된 customization

이건 Windows 10과는 관계가 없는 다른 얘기이다.
<날개셋> 한글 입력기는 한글을 생성하기에 앞서서 key 입력을 인식하는 방식 자체를 사용자화 가능하다. 크게 다음과 같은 시나리오가 있다.

(1) 원래는 먹던 특정 key를 먹지 않게 할 수 있다: 표준 두벌식의 경우 오로지 A~Z 26개 key만 사용하기 때문에 나머지 숫자와 기호는 따로 IME가 가로채는 게 아니라 그냥 응용 프로그램으로 넘겨서 숫자나 기호를 입력하게 한다.

(2) 원래는 먹지 않던 특정 key를 IME 입력으로 바꿀 수 있다: space의 경우, 굳이 IME가 처리할 필요가 없는 key이다. 하지만 인디자인 버그 같은 걸 해결하기 위해서 IME가 굳이 가로채어 공백을 되돌리게 할 수 있다.

(3) 특정 key를 IME 입력 대신 다른 key로 바꿀 수 있다: 내 프로그램으로 드보락 글쇠배열을 사용하는 경우, 보통은 드보락 방식대로 IME가 알파벳을 보내게 한다. 하지만 글쇠 누름 날개셋문자를 사용하면 아예 해당 알파벳 key 입력을 응용 프로그램에다가 보내게 할 수도 있다.

쉽게 말해 비한글 알파벳이나 공백 같은 문자를 IME 방식으로 보내느냐, 그렇지 않고 더 원초적인 key 메시지 형식으로 보내느냐를 제어할 수 있다는 뜻이다.
단축키 같은 것은 key 입력 메시지 형태로 온 것만 인식되지 IME 방식으로 전달된 문자로는 인식되지 않는 경우가 많기 때문이다. MS Excel의 경우, 영문을 key 입력 형태로 입력했을 때에만 함수의 목록 자동 완성이 처리되지만 IME 방식으로 입력된 것에는 반응하지 않는다.

(1)과 (2)는 6.5 버전에서 기본 입력 스키마에 해당 기능이 추가되고 구현됐다. 한편, '글쇠 누름' 날개셋문자는 비교적 최근인 7.9 버전에서 추가되었다.
현재 개발 중인 8.2 버전은 (3)이 소개하는 '글쇠 누름' 날개셋문자의 사용을 홍보하기 위해, 제어판의 글쇠배열 편집 창에서 숫자/문자를 입력하는 '일반 문자' 날개셋문자를 그에 상응하는 '글쇠 누름' 날개셋문자로 자동으로 모두 바꾸는 기능을 추가했다.

그리고 더 간단히, 사용자가 누른 key에 해당하는 글쇠 누름 날개셋문자를 자동으로 입력시키는 모드도 추가했다. 예전에는 세벌식 한글, 두벌식 한글, 이동, 영문 알파벳 같은 모드가 있었는데 그런 것처럼 새로운 모드를 넣었다는 뜻이다. 아주 자연스럽게 기능 확장이 가능하다.

또한 (2)를 더 쉽게 설정할 수 있게 하기 위해, 단축글쇠를 입력받는 대화상자에다가는 지금 누른 가상 키코드의 문자값에 해당하는 '일반 문자' 날개셋문자를 자동으로 배당하는 기능도 추가했다.
알파벳이나 숫자를 key 입력으로 보내는 것과 IME 문자열로 보내는 것의 차이에 대해서는 도움말에다가도 더 자세한 설명을 넣을 예정이다.

4. 고기 먹고 싶음 -_-

아직 어쩔 수 없는 여름이긴 하지만 그래도 날씨가 예전보다 덜 더워져서 무척 좋다.
난 어릴 때부터 왜 태양이 긍정적인 심상이고 그늘이 부정적인 심상인지를 이해를 못 했을 정도로 몸에 열이 많고 더위를 싫어했다.

이제 좀 가을이 되고 나면 날씨 탓할 일 없이 더 집중해서 코딩을 진행하고 싶은데.. 프로그램 완성의 꿈은 요원하기만 하다.
도깨비불 현상 없는 입력 방식이 심리적으로 무척 도움이 된다. 세벌식은 없어질래야 없어질 수 없고 없어져서는 안 되는 한글 입력 방식이다. 뭐 그건 그렇고..

사용자 삽입 이미지
나만 저렇게 생각하는 게 아니었구나.
돼지는 훌륭한 단백질 공급원이고, 단백질은 훌륭한 스트레스 해소원이다.

사용자 삽입 이미지
그래서 위대한 령도자 세종대왕도 약을 빨거나 한 건 아니고 고기를 드시면서 한글을 창제하시였다. 한글이 없었으면 내 프로그램도 없었겠지.
(다만, 그 덕분에 저분은 세자 시절부터 굉장한 비만이었으며, 당뇨 같은 성인병도 평생 달고 살았다고 함. 본인도 키에 비해서는 좀 과체중인 상태..;;)

Posted by 사무엘

2015/08/28 08:31 2015/08/28 08:31
, , ,
Response
No Trackback , a comment
RSS :
http://moogi.new21.org/tc/rss/response/1132

한때 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

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

컴퓨터로 뭔가 input을 받아들여서 output을 내는 나만의 프로그램을 개발한다면, 그 결과물이 단순히 화면으로만 잠깐 나타났다가 사라지는 걸 원하지는 않을 것이다. 꼭 프린터로 출력까지는 아니더라도 파일로 저장하여 사용자의 컴퓨터에 (반)영구적으로 남는 정도는 가능해야 할 것이다.

일반적인 텍스트/그림 파일뿐만이 아니라 내 프로그램만이 인식할 수 있는 고유한 파일 포맷을 제정하고, 그 포맷이 널리 쓰이게 되는 것은 분명 해당 파일 포맷을 만든 사람에게는 기분 좋은 일일 것이다. 새로운 이미지 파일 포맷이라든가 압축 파일 포맷처럼 말이다. 본인의 경우는 <날개셋> 한글 입력기의 글쇠배열/입력 설정 파일이 이런 창조물의 범주에 속하게 됐다.

파일 포맷이라는 건 지금 당장 공간 낭비 없이 읽고 쓰기 빠르게 만드는 효율도 중요하지만, 범용성과 확장성도 대단히 중요하다. 지금 만들고 있는 프로그램이 구조와 기능이 앞으로 어떻게 바뀔지 알 수 없기 때문이다. 마치 프로그래밍 언어가 하드웨어 친화와 사용자 친화라는 양 이념 사이의 tradeoff로 떨어지듯, 파일 포맷도 위의 두 이념 사이의 tradeoff를 고려하여 제정된다.

또한 파일 포맷은 거의 필수적으로 앞부분에 헤더가 들어간다. 이 파일이 요런 파일 포맷으로 된 파일이라는 것을 나타내며, 헤더가 일치하지 않으면 파일을 더 읽지 말고 에러를 출력하라는 일종의 배려이다. 헤더의 앞에는 식별자가 있는데, 요것이 또 파일 포맷마다 아주 개성이 넘쳤다. 도스 실행 파일(EXE)은 MZ, ZIP 압축 파일은 PK 등.

도스에서 파일의 내용을 보여주는 type 명령은 end-of-file을 나타내는 아스키 문자인 0x1A를 만나면 뒷부분에 텍스트가 더 있어도 표시를 멈췄기 때문에, 파일 시그니처의 끝에다가도 저 문자를 넣어 주는 게 일종의 센스쟁이 관행이었다. 딱 HWP Document File v3.0 요까지만 출력하고 멈추게 할 수 있으니까 말이다. 0x1A는 10진수로 26인데, 이것이 바로 지금도 copy con 다음에 종결을 위해 입력하는 Ctrl+Z와 대응한다. Z는 알파벳 26째 마지막 문자이니까 말이다.

PNG 그래픽 파일은 이 시그니처를 상당히 머리를 써서 만든 것으로 잘 알려져 있다. 마냥 텍스트 파일로 오인하지 않게 의도적으로 맨 앞은 0x89라고 128보다 큰 문자를 집어넣고, 그 다음 PNG를 찍고 줄 바꿈 문자를 찍은 뒤 0x1A로 종결시킨다.

옛날에 아래아한글이 도스용으로 1~2.x 버전이던 시절엔 이런 미래 확장 가능성을 꼼꼼히 설계를 안 했는지 파일 포맷이 수시로 바뀌어서 하위 호환성이 깨지곤 했다. 뭐, 2.1 때는 최초로 압축 저장 기능이 생겼고 도중에 암호 체계가 뚫리는 해프닝이 있어서 불가피하게 포맷이 바뀌어야 하기도 했지만 말이다.
그나마 3.0 포맷이 도스와 Windows 공용으로 무려 97 버전까지 변경 없이 잘 쓰이다가 그래도 지금은 무려 워디안 이래로 포맷이 바뀌지 않고 꿋꿋이 잘 나가고 있다. 안정화가 됐다.

그런 최소한의 융통성을 갖춘 파일 포맷을 만들려면, 결국 어떤 용도의 포맷을 만들든지간에 버전 정보를 남기고 섹션, 구획(혹은 chunk)을 설정하는 정도의 추상화는 공통으로 필요하다. 내가 아는 chunk의 정보만 읽어들이고 모르는 건 무시할 수 있게, 하위 호환이 되게 말이다. PE라고 불리는 Windows용 실행 파일에서도 이런 구획이 있고(text, rdata, data, rsrc 등), TTF 폰트 파일에도 내부에 구획이 있다(cmap, glyf, head 등). 미디(mid) 음악 파일도 온갖 구획들이 합쳐진 컨테이너 포맷이다.

그렇게 외부에서 구획을 표현하는 방식은 파일 알멩이 포맷 이전에 껍데기 '컨테이너' 포맷이라는 공통 규격으로 바뀌는 게 요즘 추세이다. 매 프로그램마다 GUI 프로그래밍을 제각각 할 필요가 없듯, 껍데기를 일일이 새로 만들 필요는 없으니 말이다. 무손실 압축 파일 포맷도 컨테이너와 압축 알고리즘을 분리해서 생각하는 건 상식 중의 상식이고, 손실 압축 알고리즘의 각축장인 동영상/소리 파일 포맷도 컨테이너와 내부 컨텐츠 포맷은 계층이 분리돼 있다.

컨테이너는 아예 human-readable한 텍스트 방식과, 그것보다는 성능을 더 중요시한 바이너리 방식 둘로 나뉜다.
텍스트는 xml이 대세를 평정하는가 싶었는데 요즘은 json도 급부상하고 있다. json은 프로그래밍 언어에서 배열이나 튜플 같은 복합 자료형을 표기하는 방식을 그대로 가져왔다는 점이 무척 참신하다. 배열스러운 나열과 key-value 형태의 데이터를 모두 표기할 수 있으며, 그 덕분에 바이너리 덤프 같은 것도 xml보다는 덜 부담스럽게 집어넣을 수 있고 공간 효율도 더 좋다.

바이너리 차원에서의 컨테이너 포맷으로 요즘 굉장히 많이 쓰이는 건 zip 압축 포맷이다. 수많은 압축 알고리즘들이 존재하지만 역시 오픈소스 앞에서는 답이 없다. zip이 세상을 평정했다. 가장 친숙하게는 MS Office 2007 이후의 문서 파일 포맷, 그리고 오픈오피스 문서 파일 포맷이 내부적으로는 zip 압축 파일이다. Java의 jar 라이브러리, 그리고 안드로이드 adb 패키지도 zip이다.

다만, 저런 프로그램들은 zip 안에다가 자기 방식으로 고유한 메타데이터도 집어넣곤 한다. 그렇기 때문에 이들 파일의 압축을 풀었다가 다시 압축을 했다고 해서 그것들이 해당 오피스 문서나 패키지로 인식되지는 않는 경우가 많다.

멀티미디어 파일 포맷 중에는 avi/wav가 동일하게 RIFF(리소스 교환 파일 포맷)라는 컨테이너 기반이다.
한편 Windows 세계에서는 의외로 많이 쓰이는 공용 바이너리 컨테이너 포맷이 있는데.. 그것은 바로 OLE Compound Binary이다. 이름에서 알 수 있듯이 바이너리 규격에서 여러 프로그래밍 규격들의 통일을 시도했던 OLE/COM 기술과 역사를 같이하는 포맷인 것 같다. 난 잘 모르겠지만 아마 이 파일을 읽고 쓰는 I*** 하는 인터페이스 API도 있으리라 여겨진다.

이 방식의 파일은 D0 CF 11 E0 A1 B1 1A E1이라는 8바이트짜리 시그니처로 시작한다. 의도적으로 128 이하의 텍스트나 제어 문자는 제외한 듯하다. 그리고 앞부분엔 0xFF 문자가 수십~수백 개 나온다.
MS Office가 2007 버전이 등장하기 전에 재래식 doc/xls/ppt가 이 컨테이너 하에서 자기 데이터를 저장하곤 했다. 그리고 지금도 일반적으로는 xml+zip 기반의 docx/xlsx/pptx이지만 암호를 걸어서 저장하면 여전히 예전처럼 이 compound binary를 사용한다. 이건 그리 널리 알려져 있지 않을 것이다.

엑셀의 경우 대용량의 데이터를 빠르게 저장하기 위해 예외적으로 xml 대신 바이너리 포맷을 쓰는 xlsb도 지원하긴 하는데, 이때에도 컨테이너는 여전히 zip이다.
하지만 암호를 걸면 xls든 xlsb든 동일하게 컨테이너가 저 OLECB 방식으로 회귀한다.

OLECB는 Office 문서에서만 쓰이는 게 아닌 범용적인 컨테이너 포맷이기 때문에 Windows의 내부에서는 thumbs.db에서도 쓰이고 심지어 msi 패키지도 이 방식으로 만들어져 있다.
국내에서는 아래아한글이 워디안 이후 새로운 hwp 포맷이 이 컨테이너를 사용하는 중이다. 몇 년 전에 hwp 파일의 포맷이 부분적으로나마 공개되면서 요 방식도 같이 주목받은 편이었다. 워디안의 개발 당시에 OLECB를 사용하기로 한 것은 21세기에 아래아한글의 향후 행로를 결정한 매우 중대한 결정이었을 것이다.

파일 포맷이란 건 한번 정해지고 그게 대중화돼 버린 뒤에는 마치 전기 전압이나 교통수단의 통행 방향처럼 다른 방식으로 덥석 고치기가 거의 불가능하다. 프로그램의 구조가 아주 간단하고 기능 구현만 빨랑 해야 할 때는 숫자/문자열 몇 개를 덥석 텍스트 형태로 덤프하거나, 구조체가 차지하는 메모리 형태를 파일로 통째로 써 버렸을지 모른다. 하지만 그 파일을 남과 주고받게 되고 프로그램을 지속적으로 발전시켜야 한다면 본격적으로 파일 포맷을 고민해야 하는 날이 온다.

이걸 처음에 신중하게 생각을 안 하면 파일 포맷은 legacy들이 가득한 누더기가 돼 가고, 참다못해 파일 포맷을 다 갈아엎게 되고 그러면서 사용자들로부터 욕도 먹을 것이다. 컴터쟁이 프로그래머로서 파일 포맷은 참 재미있는 주제인 것 같다. 그 어떤 파일 포맷이라도 결국은 튜링 기계가 인식할 수 있는 형식 언어와 문법에 속하는 방식으로 귀착된다는 점 역시 생각할 점이고 말이다.

Posted by 사무엘

2015/06/26 08:35 2015/06/26 08:35
, ,
Response
No Trackback , 3 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1109

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

블로그 이미지

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

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

«   2024/11   »
          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:
2989277
Today:
837
Yesterday:
1477