« Previous : 1 : 2 : 3 : 4 : 5 : ... 11 : Next »

1. 3차원 그래픽 시연 프로그램의 개편

지난 2003년부터 2005년 사이, 본인의 대학 재학 시절에 만들어진 뒤 10년이 넘게 버전업이 없었던 '3차원 그래픽 시연 프로그램'이 정말 오랜만에 업데이트 되었다. 무엇이 바뀌었냐 하면.. '시선 고정' 모드란 게 추가됐다.

이 프로그램은 지금까지 3D FPS 게임처럼 앞뒤 좌우 상하로 움직이는 것에만 최적화돼 있었는데, 시선 고정 모드에서는 카메라가 어디 있든지 시선이 언제나 기준점을 향해 고정되게 된다.
시선이 고정되니, 이때는 통상적인 좌우 화살표나 page up/down을 이용한 시점 변경이 동작하지 않는다. 끄덕끄덕(pitch)/설레설레(yaw) 말고 시선에 영향을 주지 않는 갸우뚱(roll)만 동작한다.

그리고 좌우 게걸음인 ZX와 상승/하강인 QA를 누르면 그냥 움직이는 게 아니라 기준점과 같은 거리를 유지하면서.. 기준점 주변을 상하 좌우로 돌게 된다.
요것이 본인이 원하던 바였다. 사실, 3차원 그래픽 편집 프로그램에서는 FPS 게임 같은 움직임보다는 이렇게 시선이 고정된 '빙글빙글' 앵글 이동을 더 흔히 볼 수 있었을 것이다. 이 동작을 내 프로그램도 뒤늦게 지원하게 됐다.

F를 누르면 시선 고정 모드를 켜거나 끌 수 있다. 아래의 상태 표시줄에 기준점의 좌표가 같이 나타난다.
그리고 D를 누르면 지금 카메라가 있는 위치를 기준점으로 지정한다. 초창기에는 (0, 0, 0) 원점이 기준점이다.
기준점을 파란색 동그라미 같은 별도의 부호로 표시해 주면 사용자에게 도움이 될 수 있겠지만.. 일단은 그런 걸 생략했다.

예제 데이터들 중에서는 구(sphere)가 제일 볼 만할 것이다. 이 구는 그렇잖아도 원점이 중심으로 만들어져 있다. 구에서 적당히 떨어진 뒤 시선 고정 모드를 켜고 QA/ZX를 누르면 우리가 인공위성이라도 된 것처럼 구 주변을 빙글빙글 돌게 된다.
그에 비해 토러스(torus)는 원점을 기준으로 만들어져 있지 않은 듯하니(구체적인 값은 기억이..) 적당히 다른 점을 기준으로 설정해야 튜브 안을 이탈하지 않고 빙글빙글 돌 수 있다.

사용자 삽입 이미지

지난 2016년에는 삼각형 오심 그리는 프로그램을 오랜만에 업데이트 했는데.. 이번에는 3차원 그래픽 프로그램을 손보게 되니 감회가 새롭다. '옛날 자료실'에 있는 프로그램들도 이런 식으로 최소한의 유지 보수는 여전히 하는 중이다.

2. 날개셋 개발 관련 미스터리

본인은 하루는 키보드 보안 ActiveX를 사용하는 어느 사이트에서 날개셋 한글 입력기 외부 모듈이 뻗는 걸 발견했다. 한글만 연달아 입력하는 것은 문제가 없는데, 그렇게 조합을 만들었다가 숫자나 마침표 같은 기호를 찍어서 조합을 중단하면.. 에러가 나고 브라우저 창이 다시 열리곤 했다.

마소 IME 같은 타 프로그램에서는 괜찮고 내 프로그램에서만 100% 재연 가능한 문제가 뻔히 발견되었는데.. 그렇다면 이 문제는 독 안에 든 쥐나 마찬가지이고 곧바로 원인을 추적해서 해결되어야 할 것이다. 그런데 믿어지지 않지만 도저히 그러지를 못했다. 디버깅에 필요한 모든 절차와 방법론을 IE와 보안 유틸리티가 원천봉쇄하고 있었기 때문이다.

exception handler를 지정해서 뻗었을 때 덤프 파일을 만들려고 해도, 윗선에서 예외 이벤트를 가로채기라도 하는지 덤프가 만들어지지 않았다. (덤프는 프로그램이 뻗은 지점이 소스 코드상으로 어디이고 그 당시 함수들의 호출 계층이 어떠한지에 대한 정보를 담고 있음. 원인 추적에 매우 중요!)

입력란이 떠 있는 IE 프로세스에다가 디버거를 붙이면.. 보안 유틸이 이를 감지하고 디버거를 끄라고 요구하면서 동작을 거부했다.
뻗었을 때 디버거를 붙여도 문제의 프로세스는 상황을 확인할 틈도 없이 혼자 싹 종료되어 버렸다.

결국은 무식하게 키 입력이 감지됐을 때.. 등 의심되는 모든 곳에다가 화면/파일로 로그를 찍어서 테스트를 해 봤다.
그런데 이거 뭐 내가 짠 코드는 모조리 정상 통과한 뒤에 이상한 데 엄한 데서 에러가 발생하는 것이었다.

이건 정황상 키보드 보안 유틸과 3rd-party IME와의 충돌이긴 하지만 내가 아는 방법으로는 도저히 문제의 원인이나 해결책을 파악할 수 없어서 이번 9.61 버전에서도 부득이하게 해결되지 못했다. 언젠가 여유가 있으면 그 보안 유틸의 개발사와도 협조를 구해서 합동 수사 공조라도 해야 하지 않을까 싶다.

3. 스레드

어느 플랫폼에서든 프로그램을 짜다 보면, 백그라운드 스레드에서 뭔가를 열심히 수행한 뒤에 결과값을 표시하는 마무리 작업은 반드시 main UI 스레드에서 실행해야 할 때가 생긴다. 이에 대해서 본인은 예전에 글을 쓴 적이 있다.

요즘 프로그래밍 언어들은 언어 차원에서 별도의 블록을 분리해서 이 블록 안의 코드는 별도의 스레드에서 비동기적으로 실행되다던가, main UI 스레드에서 실행시키는 식으로 간편하게 제약을 가할 수 있다. 요런 걸 macOS의 Objective C에서도 보고 Java, C# 등에서도 봤던 것 같다.
그런 게 지원되지 않는 언어나 플랫폼에서는 해당 기능을 직접 구현하게 되는데.. Windows라면 메시지를 보내는 것과 일맥상통한다. main UI 스레드라면 그 정의상 message loop을 돌리고 있을 것이기 때문이다.

그런데 Windows용 IME는 자기가 만들지 않은 남의 프로그램 창, 남의 스레드, 남의 message loop을 기반으로 돌아가기 때문에 거기에다가 자신만의 메시지와 자신만의 메시지 핸들러를 슬쩍 얹기가 좀 난감하다.
그나마 옛날에 프로토콜이 IME 방식이던 시절에는 IME가 제각기 자신만의 보이지 않는 윈도우가 있었기 때문에 내부적으로 custom 메시지를 얼마든지 처리할 수 있었다. 하지만 TSF로 바뀐 뒤에는 그런 윈도우가 존재하지 않는다.

IME는 키보드 포커스를 받는 남의 윈도우에다가 WM_USER+* 이상한 메시지를 함부로 보내서는 안 되고, 타이머 ID도 함부로 선점하지 않아야 한다. 그러면 윈도우 핸들 없이 콜백 함수를 바로 호출하는 타이머밖에 선택의 여지가 없는데.. 그렇게 하면 SetTimer를 호출하는 자신과는 다른 스레드 문맥에서 처리되는 타이머를 생성할 수가 없다.

이게 생각보다 굉장히 난감한 문제이다. 결국은 타 스레드에서 main UI 스레드 문맥으로 특정 코드를 실행하기 위해 본인은 TSF 모듈도 임시로 나만의 message-only 윈도우를 main UI 스레드에서 생성하고, 이 윈도우가 특정 메시지를 받았을 때 그 코드가 실행되도록 프로그램을 작성했다. 메시지라는 게 알고 보면 스레드 간 교통 정리에 큰 기여를 하고 있는 셈이다.

사실 스레드, 특히 콘솔 프로그램이 아니라 main UI가 있는 프로그램에서 스레드를 쓰는 건 십중팔구 사용자 입장에서 프로그램의 반응성을 올려 주기 위해서 하는 게 대부분이다. 일시불로 프로그램이 잠시 멈추고 뜸을 들이는 게 싫으니 이자를 감수하고라도 찔끔찔끔 할부를 선택하는 것이다.

스레드 그 자체는 메모리를 더 잡아먹고 컨텍스트 스위칭 오버헤드 때문에 전반적으로 CPU가 할 일을 더 늘리고 성능을 떨어뜨린다. 하지만 스레드를 만들어서 컴퓨터가 더 많이 고생할수록 사용자의 입장에서는 더 편리한 기능이 많이 구현되는 것이 사실이다.

4. 날개셋 외부 모듈과 입력 패드의 동작 차이

날개셋 한글 입력기의 구현체 프로그램 중에서 편집기는 오로지 자기 혼자서만 돌아가는 프로그램이니 다른 고민의 여지가 없는데.. 외부 모듈과 입력 패드는 본격적으로 타 프로그램에다 문자를 보내기 때문에 다양한 환경에서 최대한 동일한 동작을 보장해야 한다. 그게 불가능한 경우 불가능한 한계에 대해서 사용자에게 미리 고지를 해야 한다.

Windows용 IME의 입장에서 좀 별종인 환경은 전통적으로 (1) 로그인(잠금) 화면, 그리고 (2) 명령 프롬프트가 있다. 그리고 Windows 8/10부터는 (3) Metro UI도 추가됐다.
입력 패드는 원래 명령 프롬프트에서는 전혀 동작하지 못하다가 Windows 7에서부터는 동작 가능해졌다.

외부 모듈은 Metro UI에서는 조합 중인 한글을 보내는 일반적인 동작은 가능하지만 tab, enter 같은 글쇠 입력을 보내지는 못한다(날개셋문자 또는 각종 입력 도구의 버튼을 통해서).
그 외에 Metor UI에서는 프로그램이 외부의 데이터 파일을 참조하지 못해서 입력 설정이 데스크톱 환경과 동기화되어 있지 않다거나, 문자표 같은 입력 도구들도 제대로 동작하지 않는 한계가 있다. 입력 도구를 X 버튼을 눌러서 닫을 수도 없다. (우클릭 메뉴를 이용해야..)

한편, 입력 패드는 외부 모듈과 달리 자신이 독립된 프로세스이기 때문에 파일을 못 여는 한계는 없다. 단지, 반대로 Metro UI로 조합 문자 같은 입력 동작을 보낼 수 없을 뿐이지..;;
그런데 굉장히 의외로 글쇠 입력을 보낼 수는 있다. 가령, 화면 키보드에서 ㄱ, ㅏ 같은 한글 조합 글쇠는 못 보내지만 tab, enter 버튼을 누른 것은 전해진다는 뜻이다. 외부 모듈이 할 수 없는 일을 입력 패드가 예외적으로 할 수 있으니 흥미로운 차이점이다.

외부 모듈에서 글쇠 입력을 못 보냈거나 입력 패드에서 자체 입력을 못 보냈을 때, 못 보냈다는 에러 메시지라도 출력할 수 있으면 좋겠지만 일단은 방법이 없어 보인다.

외부 모듈과 입력 패드에는 이것 말고도 흥미로운 차이점이 더 있다.
외부 모듈은 Windows 메시지를 직접 받지 못한다는 특성 때문에 alt가 섞인 단축글쇠를 전혀 인식할 수 없다. 원래 alt는 운영체제 차원에서의 단축키나 메뉴 구동용으로 쓰이지 IME 같은 데서 가로챌 여지가 없기도 하고 말이다.

그 반면, 비록 프로토콜을 제대로 지원하는 소수의 프로그램 한정이긴 하지만 그래도 편집기와 다를 바 없는 온전한 A급 동작을 보장 가능한 것도 외부 모듈이다. 입력 패드로는 완성된 한글을 낱자 단위로 지우거나 달라붙는 자유자재 동작까지 지원하지는 못한다. 서로 일장일단이 있는 셈이다.

Posted by 사무엘

2018/12/09 08:35 2018/12/09 08:35
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1563

Trackback URL : http://moogi.new21.org/tc/trackback/1563

Leave a comment

컴퓨터 프로그램은 실행되는 과정에서 단순히 주메모리만 읽고 쓰는 게 아니라, 디스크처럼 기록이 영구적으로 남는 보조 기억 장치에다가도 정보를 기록한다.
여기에 기록되고 보관되는 것은 단순히 사용자가 직접 만들어 내서 Save라는 명령을 내려서 저장하는 문서 데이터만이 전부가 아니다. 사용자가 지정한 프로그램 자체의 각종 옵션· 설정값도 저장되어서 나중에 이 프로그램을 다시 시작할 때 그대로 보존되곤 한다. 세심한 프로그램이라면 직전에 프로그램 창을 띄워 놨던 크기와 위치 같은 시시콜콜한 정보도 몽땅 다 기억해 놓는다.

이런 '설정 정보'를 저장하기 위해 프로그램들은 예로부터 자신만의 cfg 내지 config.dat 같은 파일을 만들어 두곤 했다. 본인이 개발한 날개셋 한글 입력기도 imeconf.dat라는 파일을 운영체제의 사용자 계정별로 고정된 디렉터리에다 만들어 놓는다. (물론 날개셋 프로그램의 경우.. 입력 설정이라는 게 단순한 프로그램 setting 수준을 넘어 그 자체가 이미 사용자가 새로 창조하는 '문서 데이터'라는 성격도 지닌다만...)

Windows에서는 이런 설정 파일을 저장하고 불러오는 절차를 정형화하기 위해서 ini(초기화 정보)라는 텍스트 파일 포맷을 도입했으며, 이걸 읽고 쓰는 [Get/Write][Private]Profile[Int/String]이라는 API 함수를 제공해 왔다. [섹션] 구분과 함께 "이름=값" 이런 게 주욱 들어가 있는 그 파일 말이다.

이들 함수는 파일을 읽고 씀에도 불구하고 뭔가 열어서 핸들값을 얻었다가 나중에 닫는 절차가 없는 게 특징이었다. 구현하는 사람 입장에서는 그리 좋은 설계 형태가 아닐 텐데..
Private이 안 붙은 API를 사용하면 자기 ini 말고 심지어 운영체제의 설정 파일인 win.ini에다가도 정보를 기록할 수 있었다.

ini 파일은 당장 사용하기는 간편하지만 한계와 문제점이 많았다. 가장 먼저 XML 같은 다단계 계층 구조를 지원하지 않았다(과거 Windows 3.x의 프로그램 관리자의 그룹이 계층 구조가 아니었던 것처럼).
그리고 아까 언급했듯이 핸들/상태 정보가 없는 API의 설계 형태에다가 텍스트 포맷이라는 점까지 겹쳐서 데이터가 방대해질 때의 처리 성능도 썩 좋지 않았다.

응용 프로그램이 있는 디렉터리, 또는 Windows 디렉터리에 온갖 자잘한 ini 파일들이 쌓이면서 지저분해지는 점 역시 문제였다. 그리고 저장을 할 거면 사용자 설정 데이터들 전용 디렉터리라도 마련해 놔야지, 그게 프로그램 실행 파일들이 있는 곳에 버젓이 저장되는 건 오늘날의 보안 내지 권한 관점에서 봤을 때 좋지 못한 설계 형태였다.

그래서 성능, 보안, 효율 등등을 모두 잡기 위해서 마소에서는 운영체제와 응용 프로그램들의 설정 저장은 파일 형태로 노출시킬 게 아니라 이를 전담하는 별도의 거대한 중앙집권 데이터베이스를 만들 생각을 하게 됐다. 그래서 이를 레지스트리라는 이름으로 일찍부터 도입했다.
마치 디렉터리 경로처럼 역슬래시로 구분된 계층 구조의 key를 만들고, 그 아래에 파일처럼 여러 개의 value들을 집어넣을 수 있게 했다.

물론 레지스트리는 구조적으로 장점도 있고 단점도 있다. 컴덕이나 프로그래머들 사이에서 호불호가 갈리며, 레지스트리를 아주 싫어하는 사람도 있다. macOS나 리눅스는 레지스트리 그딴 거 없이도 잘 돌아가고 더 안정적이기까지 하다고 주장한다.
하지만 레지스트리가 없다고 해서 그런 OS에 레지스트리가 하는 일 자체가 존재하지 않는 건 아니다. 그 OS에서도 운영체제나 응용 프로그램들이 자기 설정을 저장하는 공간이 있어야 할 텐데 도대체 어디에 저장되는 걸까?

프로그램을 제거한 뒤에도 그 프로그램이 써 놓은 레지스트리 데이터가 같이 지워지지 않아서 레지스트리가 갈수록 지저분해지고 통제불능이 된다는 식의 비판이 있다. 그런데 그건 ini/cfg 파일을 쌩으로 다룰 때도 부주의하면 어차피 똑같이 발생할 수 있는 문제이다. 한쪽에서 존재하는 문제는 대부분 다른쪽에서도 형태만 바뀐 채로 고스란히 존재할 수밖에 없다. 본인은 이런 논리를 근거로 레지스트리 무용론 급의 회의적인 시각에는 동의하지 않는다.

프로그램마다 설정 데이터라는 게 수십~수천 바이트, 정말 커 봤자 수만 바이트 남짓할 아주 작은 분량에 지나지 않는다. 그런 자잘한 데이터를 한데 관리해 주는 DB가 운영체제 차원에서 제공되면 편하면 편하지 상황이 더 나쁘지는 않을 것이다.

레지스트리는 딱 32비트 Windows 95/NT와 함께 등장했다. 이때부터 마소에서는 소프트웨어 개발자들로 하여금 구닥다리 INI 파일 함수를 쓰지 말고 ADVAPI32.DLL에 있는 Reg** 레지스트리 조작 함수를 사용할 것을 적극 권해 왔다.
다만, 이들 함수는 구닥다리 함수보다 받아들이는 인자가 많고 사용하기가 번거롭고 귀찮긴 하다. 마치 fopen과 CreateFile의 차이만큼이나 말이다.

그래서 별도의 클래스를 만들어서 쓰면 편하다. HKEY를 멤버로 가지면서 소멸자에서는 RegCloseKey를 해 주고, 각종 타입별로 인자를 다양하게 오버로딩한 Get/Set 함수를 만들고 말이다. 레지스트리 관련 API는 MFC에서도 의외로 클래스화를 전혀 하지 않았으니 이거 연구는 프로그래머들 개인 재량이다. MFC는 16비트 시절부터 있던 CWinApp 클래스의 [Get/Write]Profile* 함수를 상황에 따라 ini 대신 레지스트리 기록으로 대신하도록 동작을 확장했을 뿐이다.

사실, 16비트 Windows 시절에는 지금처럼 HKEY_* 어쩌구 하는 그런 형태의 레지스트리는 없었지만, 그 전신 비스무리한 건 있었다. Windows 3.1에서 그 이름도 유명한 OLE라는 기능 내지 개념이 추가됐기 때문이다.
응용 프로그램들의 설정 같은 건 몰라도 확장자별 연결 프로그램이라든가 OLE 서버 같은 정보들은 운영체제 차에서 관리하는 별도의 바이너리 데이터 파일에 등재되었다(Windows\reg.dat). 그리고 여기에 정보를 사용하는 Reg[Open/Create/Close]Key와 Reg[Query/Set]Value 같은 함수가 있었다.

16비트 시절부터 이런 초보적인 함수가 있었는데 이를 확장하여 오늘날의 레지스트리가 된 것이다. 그렇기 때문에 오늘날 사용되는 레지스트리 함수들은 대부분 뒤에 Ex가 추가돼 있다. 옛날에는 루트 키로 HKEY_CLASSES_ROOT 하나만이 존재했었다.
이 점을 생각하면 옛날에는 지금 같은 레지스트리가 있지도 않았는데 지금 왜 RegCreateKeyEx, RegQueryValueEx 등등 Ex 함수를 써야 하는지 이유를 알 수 있다.

지금까지 Windows의 레지스트리 개념과 관련 API를 살펴봤으니, 다음으로는 역대 버전별 레지스트리 편집기에 대해서 살펴보도록 하겠다.
레지스트리 편집기는 과거 도스 시절로 치면 디스크 내부 구조를 저수준에서 수정할 수 있는 노턴 유틸리티의 DiskEdit와도 비슷한 아주 저수준 유틸리티이다. 아예 없어서는 안 되지만 일상적으로 쓸 일은 없으며(없어야 하며), 초보자에 의한 섣부른 조작은 절대 권장되지 않는 위험한 프로그램이다. 이걸 조작함으로써 운영체제를 맛이 가게 만들고 컴퓨터 부팅조차 안 되게 만들 수도 있기 때문이다. 그러니 보조 프로그램 그룹에 정식으로 등재될 일도 없다.

우리가 흔히 알고 있는 레지스트리 편집기는 Windows 95 시절부터 지금까지 큰 변경 없이 이어져 오고 있는 탐색기(왼쪽에 트리, 오른쪽에 리스트 컨트롤) 비슷한 형태의 프로그램이다.

사용자 삽입 이미지

(1) Windows 9x에서는.. 레지스트리는 내부적으로 Windows\System.dat와 Windows\User.dat라는 두 파일에 저장되었다. 그리고 HKEY_DYN_DATA라는 predefined root key가 있어서 여기를 들여다보면 일부 시시각각 변하는 시스템 정보 같은 걸 얻을 수 있었다고 한다.

그리고 Windows 9x용 레지스트리 편집기는.. 단순히 This program cannot be run in DOS mode가 아니라 유효한 도스용 코드가 stub으로 같이 들어간, 다시 말해 DOS/Windows 겸용 프로그램이었다.
레지스트리에 이상이 생겨서 Windows 부팅이 안 되더라도 레지스트리의 백업과 복구 정도는 도스에서 할 수 있게 하기 위해서이다. regedit.exe를 순수 도스에서 실행하면 놀랍게도 이런 옵션 안내를 볼 수 있다.

사용자 삽입 이미지

마소에서 프로그램을 이런 식으로 특수하게 빌드한 예는 극히 드물다. 이는 레지스트리 편집기가 그만치 특수한 프로그램임을 의미한다.

(2) 그럼 Windows NT로 넘어가기 전에, 더 옛날 Windows 3.x를 잠시 살펴보도록 하겠다.
이때도 레지스트리의 전신이 있었고 비스무리한 API도 있었듯이, regedit.exe라는 프로그램 자체는 있었다.
하지만 얘 역시 그룹에 정식으로 등록되어 있지는 않았으며, 이때 registry란 보다시피 그냥 확장자 별 연결 프로그램과 관련 DDE 명령.. 이런 것이 전부였다. 아까 언급했던 Windows\reg.dat의 내용을 편집한다.

사용자 삽입 이미지

저 때는 Properties도 '등록정보'라고 번역하고 Registry도 '등록 정보'라고 번역했다니.. 참 므흣하다. 띄어쓰기 하나 차이밖에 없다.
Windows 95부터는 확장자 연결 설정은 그냥 탐색기의 옵션에서 별도의 탭으로 들어가 있다. 그리고 98을 넘어서 2000/XP즈음부터 Property의 한국 마소 공식 번역은 '속성'으로 완전히 바뀌어서 지금에 이르고 있다.
그리고 이 시절의 regedit.exe에는 95의 것과 같은 유의미한 도스 stub이 들어가 있지도 않았다.

(3) 이제 오늘날 사용되고 있는 NT 계열 차례이다.
일단, 레지스트리자 저장되는 파일은 Windows\system32\config에 있는 default, software, system, components 같은 확장자 없는 파일들이다. 크기가 다들 수십 MB씩 한다.
그리고 각 사용자별 정보는 사용자 계정의 루트에 있는 ntuser.dat 파일이다.
9x와 NT 계열이 파일 포맷이 동일한지는 모르겠다. 지금의 NT 계열은 도스 부팅 기능이 없고, 운영체제가 가동 중일 때는 이들 파일들이 언제나 열리고 잠겨 있어서 일반 프로그램이 레지스트리를 파일 차원에서 들여다볼 수가 없다.

Windows NT 계열은 HKEY_PERFORMANCE_DATA라는 전용 root key가 있다. HKEY_DYN_DATA처럼 실제 레지스트리 데이터는 아니지만 레지스트리 API를 통해 시스템 정보를 얻어 오는 용도인데.. 이것도 비슷한 기능이 9x와 NT 계열의 구현이 서로 파편화된 경우가 아닌가 싶다. 로드된 프로세스/DLL 정보를 얻는 API만 해도 과거에는 두 계열이 서로 달랐으니 말이다.

그리고 Windows NT는 초창기 3.1 시절부터 지금과 같은 형태의 레지스트리를 갖고 있었기 때문에 자체적인 레지스트리 편집기도 보유하고 있었다. 바로 regedt32.exe이다.

사용자 삽입 이미지

얘는 만들어진 시기가 시기이다 보니, 탐색기가 아니라 구닥다리 파일 관리자 스타일로 만들어져 있다. 왼쪽의 계층 트리와 오른쪽의 값 목록은 모두 재래식 리스트 컨트롤 기반이다. 또한, 루트별로 제각각 다른 레지스트리 창들이 MDI 형태로 구성되어 있다.
오늘날 쓰이는 공용 컨트롤 기반 regedit.exe는 바로 regedt32.exe를 베껴서 새로 만들어진 거나 마찬가지이다.

Windows 2000, 그리고 확인은 안 해 봤지만 NT4에는 regedit와 regedt32가 같이 들어있었다. 기능이 대등하긴 하지만 regedit는 오리지널 NT용 유틸리티에 있던 '레지스트리 하이브'를 통째로 불러들이는 기능이 없었다.
그러다가 regedit에 이 기능이 들어가서 regedt32를 완전히 대체하게 된 것은 Windows XP부터이다. NT의 regedt32를 보면 메뉴가 뭔가 많이 있는 것 같은데, 실제로 열어 보면 별 거 아니다.

Windows NT의 레지스트리 편집기는 9x의 것과 달리 값의 이름과 데이터뿐만 아니라 REG_SZ, REG_DWORD 같은 타입도 표시해 줬다. 9x에서는 어려운 전문 용어라고 일부러 뺐던 것 같다.
그리고 이런 새 GUI 기반의 레지스트리 편집기는 오랫동안 REG_MULTI_SZ라고 0으로 구분된 복수 문자열을 편집하는 기능이 없어서 그냥 바이너리 에디터 창이 떴었다. 그러다가 regedt32와 기능이 통합된 Windows XP에서부터 한 줄에 하나씩 복수 문자열을 편집하는 기능이 도입되었다.

이렇듯, regedit와 관련하여 Windows 3.1, NT 3, 9x 등.. 복잡한 사연이 많은 걸 알 수 있다.
이 프로그램에는 레지스트리의 일부 구간을 별도의 파일로 저장하거나 도로 불러오는 기능이 응당 들어있는데, reg 파일은 아이러니하게도 key 이름이 []로 둘러싸이고 값들이 a=b 이런 식으로 쓰인 게 마치 과거의 ini와 형태가 비슷하다.

한번 만들어진 뒤에 딱히 기능이 바뀔 일이 별로 없는 프로그램이다만.. Windows 2000부터는 reg 파일의 인코딩이 UTF-16으로 바뀌었다. 그리고 Windows 10 어느 업데이트부터는 현재의 레지스트리 주소(key 이름)이 아래의 상태 표시줄 대신에 위의 주소 표시줄에 표시되고, 사용자가 거기에 인터넷 주소 치듯이 수동으로 입력을 해서 원하는 key로 바로 찾아갈 수도 있게 UI가 편리해졌다. 나름 굉장히 바람직한 개편이다.

※ 부록: 레지스트리 편집기에 준하는 위상의 자매품

1. sysedit

과거 Windows 3.x 시절에는 sysedit라는 이름으로 config.sys, autoexec.bat, win.ini, system.ini라는 4대 시스템 설정 파일만 한데 모아서 편집할 수 있는 MDI 텍스트 에디터가 있었다. 이게 Windows 9x는 말할 것도 없고 NT 계열 제품에도 32비트 한정으로 포함돼 있었다.
Windows에 내장돼 있는 극소수의 16비트 프로그램이기 때문에 날개셋 한글 입력기를 테스트 할 때도 개인적으로 유용하게 사용했다.

사용자 삽입 이미지

물론 Windows 9x는 config.sys와 autoexec.bat를 거의 퇴출시켰고, NT 계열은 뒤이어 두 ini 파일까지 완전히 퇴출시킨 거나 마찬가지다.

2. msconfig

Windows 98부터 잘 알려지지 않았지만 꽤 유용한 유틸리티가 추가됐다.
얘는 9x용은 sysedit가 하는 일을 모두 포함하면서(해당 파일에 내용을 추가하거나 기존 내용을 간편하게 제거), 레지스트리에 등록돼 있는 시작 프로그램들의 실행 여부를 제어하고, 각종 시스템 관리 유틸리티도 실행하는 기능을 제공했다.

사용자 삽입 이미지

이 프로그램은 레지스트리 편집기와 마찬가지로 오늘날까지도 남아 있기 때문에 명령 프롬프트에서 실행 가능하다.

Posted by 사무엘

2018/11/22 08:33 2018/11/22 08:33
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1557

Trackback URL : http://moogi.new21.org/tc/trackback/1557

Leave a comment

Windows에서 돌아가는 GUI 프로그램은 커다란 자기 창을 띄우며, 그러면 작업 표시줄(task bar)에서도 그 창이 있다는 걸 표시해 준다.
그런데 작업 표시줄은 어떤 프로세스가 생성하는 여러 창들을 어떤 기준으로 선별하여 표시하는 걸까? 화면에만 표시되고 작업 표시줄에는 나타나지 않는 일명 '스텔스 윈도우'는 어떻게 만드는 걸까?

심지어 이 작업 표시줄에 등재된 창 목록은 Alt+tab을 눌렀을 때 나타나는 task 목록과도 완전히 일대일 대응하지는 않는 것 같다. 그 관계는 어떻게 될까?

일단, 작업 표시줄은 (1) child가 아니고(overlapped 또는 popup) owner(parent)가 NULL인 모든 윈도우를 자기 목록에 등재해서 표시해 준다. 대화상자건, 응용 프로그램이 클래스를 따로 등록한 윈도우건 모두 상관없다. 이건 대부분의 상황에서 충분히 합리적인 조치이다.

옛날에 대략 Windows XP 정도까지 쓰던 기억을 떠올려 보면, 디스플레이/키보드/마우스 같은 제어판 애플릿들은 분명 rundll32라는 독립된 프로세스로부터 구동된 대화상자임에도 불구하고 작업 표시줄에는 제목이 뜨지 않았다. 그 이유는 제어판 애플릿은 제어판 셸로부터 주어진 보이지 않는 부모 윈도우를 owner로 삼고 생성되기 때문이다.

그러므로 보이지 않는 윈도우를 생성하여 owner로 잡으면 대화상자뿐만 아니라 크기 조절 가능한 멀쩡한 overlapped 윈도우라도 작업 표시줄에 표시되지 않는 '스텔스' 형태로 만들 수 있다. 일반적으로 그런 건 만들 필요가 없고 그게 UI 디자인 상으로 권장되는 짓도 아니니 안 할 뿐이다.
그리고 저런 모델은 응용 프로그램의 입장에서는 자기 창이 하나가 아니라 두 개가 존재하는 셈이므로 창을 관리하는 게 약간 더 귀찮아진다.

한편, 위의 (1) 다음으로 몇 가지 단서가 있다. (2) WS_EX_TOOLWINDOW는 owner가 NULL이더라도 이 창이 무조건 작업 표시줄에 등록되지 않게 하고 심지어 Alt+tab 작업 목록에도 나타나지 않게 한다.
얘는 덤으로 제목 표시줄도 더 얇게 찍히게 한다(WS_CAPTION이 명시된 경우). 그래픽 에디터의 도구 팔레트처럼 작고 키보드 포커스도 안 받고, 화면에 언제나 표시되어 있는 그런 창을 찍으라고 있는 스타일인 것이다.

이 스타일 한 방이면 스텔스 윈도우를 아주 쉽게 만들 수 있다. 단지 제목 표시줄이 꼭 필요하고 그걸 다른 평범한 윈도우처럼 두껍게 만들고 싶을 때에만 owner 편법을 쓰면 될 듯하다.

그리고 다음으로.. WS_EX_TOOLWINDOW와 상극인 WS_EX_APPWINDOW 스타일이 있다. (3) 얘는 자기가 owner가 지정되었다 하더라도 반드시 작업 표시줄에 표시되게 한다.
Windows Vista인가 7부터는 디자인이 바뀌었는지 제어판 애플릿이 작업 표시줄에 나타나는 걸 볼 수 있다. 얘들은 여전히 owner 윈도우가 따로 있음에도 불구하고 저 스타일이 지정되었기 때문에 작업 표시줄에도 보인다. 중간에 뭔가 디자인 정책이 바뀐 듯하다.

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

내가 표시하는 대화상자나 창이 작업 표시줄을 건드리지 않고 조용히 떴다가 없어질지, 아니면 독립된 응용 프로그램처럼 뜰지 결정하는 것은 개발자의 재량이다. 다만, 작업 표시줄에다가도 나타나게 할 거면 창에다가 적절한 아이콘도 넣어 주는 게 좋을 것이다.

하지만 어지간해서는 owner가 NULL인 것만으로도 작업 표시줄에 창이 자동으로 등록되니 이 스타일이 굳이 따로 쓰일 일은 내 경험상 별로 없다.
WS_OVERLAPPEDWINDOW나 WS_POPUPWINDOW는 여러 기존 스타일들의 조합이지만 WS_EX_*WINDOW는 그렇지 않고 자신만의 고유한 값이다.

그리고 마지막으로 하나 살펴볼 게 있다.

MS Office Excel의 경우, 워크시트 문서창이 응용 프로그램 창의 내부에 소속된 child이다. 단독의 popup 윈도우 같은 게 아니다.
그럼에도 불구하고 한 프로그램에서 여러 파일을 열면 그 문서창들이 작업 표시줄에 제각각 나타난다. 이게 먼 옛날 Office 2000쯤부터 그렇게 되기 시작했는데.. 어떻게 이런 일이 가능한지 신기하지 않으신가?

이건 윈도우 스타일 조작만으로 가능한 동작이 아니고 별도의 API를 사용해서 구현한다.
셸 API들, 특히 작업 표시줄 근처에 있는 트레이(notification) 아이콘을 조작하는 API는 다 SH_*로 시작하는 고전적인 C 함수인 반면, 작업 표시줄을 조작하는 API는 ITaskbarList라는 COM 인터페이스 형태이다.

ITaskbarList를 얻어 온 뒤 HrInit를 호출해서 초기화하고, MDI 문서 창이 생성되면 자기 자신에 대해 AddTab + ActivateTab을 호출한다(ActiveTab도 반드시 해 줘야 됐음). 그리고 문서 창이 닫힐 때는 DeleteTab를 하면 된다. 이렇게만 하면 당장 날개셋 편집기조차도 얼추 Excel처럼 문서 창을 작업 표시줄에서 곧장 접근 가능하게 수정할 수 있다.

사용자 삽입 이미지

다만, 일이 마냥 간단하지만은 않다. 문서 창뿐만 아니라 기존 날개셋 편집기 자체의 창이 등록된 것도 같이 관리해야 하기 때문이다.
문서 창이 없거나 하나밖에 없으면 그냥 프로그램 자체의 창 하나만 유지하고 있고, 문서 창이 2개 이상이 되면 그때부터 프로그램 창은 날리고 문서 창들이 작업 표시줄에 나타나게 해야 한다. 불가능한 일은 아니지만 자동화가 돼 있지 않아 귀찮다. 마치 클립보드 viewer chain을 관리하는 것처럼 말이다.

요컨대, owner 윈도우, WS_EX_TOOL/APPWINDOW 스타일, 그리고 ITaskBarList 인터페이스만 기억하고 있으면 창과 작업 표시줄의 연계는 다 마스터했다고 볼 수 있겠다.
참고로 ITaskBarList는 초창기에는 이렇게 탭을 수동으로 등록하고 삭제하는 원시적인 기능만 있었다. 아마 IE 4나 5 시기쯤, 웹 브라우저와 운영체제 셸이 얽혀서 마개조되고 DLL hell 현상이 악명을 떨치던 20여년 전에 첫 등장했다.

그러다가 얘가 온갖 기능이 추가되어 ITaskBarList3으로 발전한 게 2000년대 말의 Windows 7 타이밍이다. 작업 표시줄에다가 응용 프로그램의 고유한 썸네일을 표시하고, 백그라운드 작업 진행률을 나타내고, 재생기의 경우 간단한 재생/멈춤 같은 버튼까지 갖다박는 UI 요소가 그때 추가됐기 때문이다. 그런 기능들을 바로 저 API를 통해 사용할 수 있다.

Posted by 사무엘

2018/10/17 08:34 2018/10/17 08:34
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1544

Trackback URL : http://moogi.new21.org/tc/trackback/1544

Leave a comment

1. 실행 파일의 자가 업데이트

내가 개인적으로 만들어서 배포하는 날개셋 프로그램들에는 서버 트래픽, IME의 갱신과 관련된 Windows 특유의 기술적 난항, 개발자의 귀차니즘(..) 등의 이유로 인해 자동 업데이트 기능이 없다. 하지만 회사에서 상업용으로 개발하는 프로그램에다가는 프로그램의 자가갱신 기능을 구현할 일이 좀 있었다. 이를 위해서 Windows에서 자기 자신을 삭제하는 실행 파일을 만들 때와 비슷한 방식의 시나리오를 설정했다.

  1. 업데이트 대상인 app.exe는 새 버전인 app_new.exe를 다운로드하여 임시 디렉터리에다 보관한다.
  2. app.exe는 별도의 updater를 실행하고 자기 프로세스 ID와 임시 파일 이름을 명령 인자로 전한다. 그 뒤 자신은 신속히 종료한다.
  3. updater는 app.exe가 종료할 때까지 기다린 뒤, 실제로 종료되면 app.exe를 삭제한다. 그리고 임시 파일을 app.exe로 옮기고 개명한다.
  4. 그 뒤 updater는 새로 받은 app.exe를 실행한다.

그렇게 본 프로그램과 업데이터까지 잘 만들었다. 업데이터는 Program Files 아래의 디렉터리에서 파일을 지우고 생성할 것이기 때문에 관리자 권한으로만 실행되게 매니페스트 설정도 물론 해 놨다.

그런데 이렇게 만들어진 프로그램은 개발 환경을 포함해 여러 PC에서 업데이트 기능이 동작했지만, 일부 PC나 가상 머신에서는 제대로 동작하지 않았다.
관리자 권한을 분명히 줬는데 도대체 어디서 문제가 발생하는지, 그리고 같은 Windows 10에서 컴퓨터마다 성공 여부에 왜 차이가 발생하는지 기괴하기 그지없었다. 디버깅 결과 다음과 같은 복병을 발견하게 되었다.

(1) GetModuleFileNameEx는 만능이 아니다. 경로 정보를 얻고자 하는 모듈이 존재하는 프로세스의 비트수(32 또는 64비트)와.. 정보를 요청하는 나 자신의 비트수가 일치해야만 경로를 얻을 수 있더라.
난 이 함수가 그런 이유로 인해 실패할 수 있다고는 꿈에도 생각하지 않고 있었다. 문서에 딱히 언급이 없었으니까... 비트수가 안 맞으면 ERROR_PARTIAL_COPY라는 굉장히 낯설고 기괴한 에러 코드와 함께 함수의 실행이 실패했다. (Only part of a ReadProcessMemory or WriteProcessMemory request was completed.)

(2) 그리고 더 뒷목 잡는 상황은 따로 있었다.
업데이터는 app이 종료될 때까지 기다렸다가 파일을 대체하도록 만들어졌다. 그런데 컴퓨터에 따라서는 그 반대편으로 극단적인 상황도 발생했다. 바로, app이 너무 일찍 종료되거나 업데이터가 너무 늦게 실행되는 바람에 정작 OpenProcess 요청부터가 실패하여 app에 대한 정보를 전혀 얻지 못한 것이다.

그러니 이때는 app과 업데이터가 서로 공유하는 이벤트 같은 오브젝트라도 만들어서 app과 업데이터가 공존하는 타이밍이 반드시 존재하게 할 수도 있고, 아니면 app이 자신의 족적을 공유 메모리나 아예 임시 파일 같은 다른 방법으로 넘겨서 업데이터에다가 전달하게 문제를 회피해야 했다. 뭐, 이런 일이 있었다.

요즘은 어지간히 대단하고 전문적인 제품이 아닌 한, 소프트웨어를 받아서 사용하는 것 자체만으로 최종 사용자에게 금전적인 대가를 요구하는 관행은 사실상 없어졌다. 소프트웨어는 그냥 고객을 확보하고 개발사의 인지도를 올려서 더 교묘한 수단으로 수익을 추구하는 통로일 뿐이다.

소프트웨어를 무료로 뿌리는 대신에 개발사는 사용자로부터 그 소프트웨어의 사용 형태를 최대한 미주알고주알 수집하려 하는 게 요즘 추세이다. 단순히 (1) 최신 버전 체크/업데이트라든가 프로그램이 뻗어서 (2) 메모리 덤프 같은 디버깅 정보를 제출하는 용도로 서버와 교신하는 게 아니라, 자주 사용하는 기능 같은 전반적인 행동 데이터를 종종 개발사로 보낸다는 것이다.
물론 이건 사용자에 따라서 불쾌한 사생활 침해로 여겨질 수 있으니 사용자가 동의했을 때에만 보내게 돼 있다. MS Office, Visual Studio, Android Studio 등의 제품에 다 이런 옵션이 존재한다.

2. 경과 시간을 되돌리는 함수

자동 업데이트 기능은 마지막으로 업데이트 체크를 했던 시각을 레지스트리에다 저장해 둔다. 그리고 프로그램이 처음 실행되면 현재 시각이 레지스트리에 기록된 시각으로부터 1주 이상 경과했는지 검사한다. 조건이 만족되면 서버에 등록된 최신 버전을 지금 내 버전과 비교하고.. 이하 생략의 순서대로 구현되었다.

그런데 프로그램을 다 만들고 테스트를 해 보니, 아무리 서버에 더 높은 버전 정보가 등록됐고 그로부터 1주가 넘는 시간이 경과해도 이 프로그램은 자동 업데이트가 행해지지 않았다.
도대체 뭐가 문제인지 코드를 한참을 들여다 본 뒤에야 "아차...!" 하고 탄식을 내뱉었다.

GetTickCount()는 1970년 1월 1일 같은 고정된 epoch으로부터 경과한 시간을 되돌리는 게 아니라, 그냥 시스템이 부팅이 끝난 이래로 경과한 시간을 되돌리는 함수이지 않던가.
그러니 한번 컴퓨터를 켜 놓고 1주일 이상 버티지 않는 이상, 업데이트 체크가 행해질 리가 없었던 것이다.
내가 뭘 잘못 먹고 착각을 했는지, 코딩 하다가 졸았는지, 왜 그 상황에서 저 함수를 쓸 생각을 했는지는 알 길이 없다.

3. message loop

Windows에서 MFC를 안 쓰고 응용 프로그램 GUI를 구현하다 보면 결국 (a. PeekMessage) → GetMessage → (b. TranslateMDISysAccel, TranslateAccelerator) → (c. IsDialogMessage) → TranslateMessage → DispatchMessage 등의 순으로 함수를 호출하는 message loop도 손수 만들게 된다.
괄호를 친 a~c는 필수는 아닌 함수들이다. a는 idle time processing이 필요할 때만 쓰면 되고, b는 MDI 프로그램이라거나 자체 단축키가 있을 때, 그리고 c는 혹시 modeless 대화상자가 존재할 때에만 쓰면 된다.

본인이 개발하던 문제의 프로그램은 타 프로세스로부터 받은 메시지에 반응하여 대화상자를 표시하는 기능이 있었다. 그런데 만들어 놓고 보니 당장은 동작하는 것 같지만 뭔가 미묘하게 정상이 아닌 상태 같았다.
대화상자가 떠 있는 동안에도 우리 쪽에서 정의한 custom 메시지를 처리할 수 있게 하기 위해 번거롭지만 대화상자를 modal 대신 modeless 형태로 바꿨다.

그런데 tab을 눌러서 컨트롤간 포커스를 이동시켜 보면 이상한 비프음이 나면서 이동하고, Alt+Space로 시스템 메뉴를 꺼내서 창을 닫으면 제대로 동작하지 않고 프로그램 동작이 수 초간 멎는다거나..
owner(parent) 윈도우를 타 프로세스의 윈도우로 지정해서 그런가, 처음에는 쓸데없는 부분을 의심하며 한참을 헤맬 수밖에 없었다.

이것도 코드를 한참을 들여다 본 뒤에야 내가 무슨 삽질을 했는지를 깨닫고 경악하게 됐다. message loop을 이런 식으로 짰기 때문이었다.

while(GetMessage(&msg, NULL, 0, 0)>0) {
    for(HWND hDlg: m_hModelessDlgs)
        if(hDlg && ::IsDialogMessage(m_hDlg, &msg)) continue;
    ::TranslateMessage(&msg); ::DispatchMessage(&msg);
}

꺼내 놓을 수 있는 modeless 대화상자가 여러 종류가 추가되면서 나름 저렇게 for문을 넣었는데..
난 저 continue가 여전히 while문 전체를 제낄 수 있다고 생각했던 것이다.
그러니 대화상자로만 가야 할 메시지가 여전히 DispatchMessage로도 이중으로 전해지고 있었고, 이 때문에 뭔가 동작은 하는 것 같은데 잡다한 부작용까지 덩달아 발생하고 있었다.

내가 Windows 프로그래밍 경력은 10년을 훌쩍 넘기지만.. 그래도 정신이 없을 때는 아직까지 이런 초보적인 실수도 한다.

4. 대화상자를 종료하는 법

대화상자를 하나 구현했다.
사용자는 메뉴를 거쳐서 그 대화상자를 열고 닫는데, 그걸 한번 닫은 뒤부터는 메뉴를 선택해도 그 대화상자가 다시 열리지 않는 버그가 있었다. 그것도 언제나 그러는 것도 아니고 특정 기능을 사용해서 간접적인 방법으로 대화상자를 닫았을 때 문제가 발생했다.

디버깅 끝에 밝혀진 원인은 바로.. modeless 대화상자를 EndDialog 함수를 이용해서 닫았기 때문이었다.
modal 대화상자를 훗날 modeless 형태로 개조하면서 발생한 부작용의 연장선이다. 대화상자를 닫는 부분을 그에 맞게 제대로 고치지 않았었다.

EndDialog를 호출했더니 그 대화상자는 ShowWindow(hDlg, SW_HIDE)를 한 것처럼 화면에서 사라지기만 할 뿐, 실제로 파괴되고 없어지지는 않았다. modeless 대화상자는 여느 윈도우를 없앨 때처럼 DestroyWindow 함수를 직접 호출하든가 WM_CLOSE를 날려서 없애야 한다. 오오.. 이것도 의외로 자주 할 수 있는 실수로 보인다.

그럼 반대로 modal 대화상자의 프로시저에서 자기 자신을 DestroyWindow로 날려 버리면 어떤 일이 벌어질지가 궁금해진다. EndDialog와 달리 DestroyWindow는 IDOK나 IDCANCEL 같은 리턴값을 지정할 수 없다. 그렇기 때문에 이때 DialogBox 계열 함수의 리턴값은 그냥 '취소'를 눌러 종료된 것과 동급으로 취급되어야 하지 않나 싶다.

5. 맺는 말

이런 일련의 경험을 통해, "타 프로세스로부터 받은 메시지에 반응하여 대화상자를 표시하기" 이게 생각보다 더욱 기괴한 상황이란 걸 실감할 수 있었다.

프로세스 A가 프로세스 B로 메시지를 send로 보냈는데 B가 대화상자나 메시지 박스 같은 modal UI를 표시하고, 그게 종료된 뒤에야 return을 해 준다면.. 그 동안 프로세스 A는 자기 메시지를 처리하지 못하는 '응답 없음' block 상태가 돼 버린다. 특히 그 상태에서 B가 A로 역으로 메시지를 보내기라도 한다면 둘 다 꼼짝없이 dead lock에 빠진다.

메시지를 주고받는 주체와 객체가 다 동일 프로세스의 동일 스레드 소속이라면 걱정할 것 없다. 그 modal UI를 돌리는 B쪽의 메시지 loop에서 A에 소속된 윈도우에게 WM_PAINT 같은 메시지가 온 것도 같이 처리를 해 주기 때문이다. 하지만 프로세스, 아니 스레드 소속만 달라도 이런 일이 저절로 같이 행해지지 않는다.

그렇기 때문에 프로세스와 스레드 경계를 넘나들며 UI를 표시할 때는 대화상자는 최대한 modeless 형태로 만들고, 메시지도 post가 아니라 반드시 send로 보내야겠다면 ReplyMessage를 호출해서 "선응답 후UI" 형태로 프로그램 UI의 반응성을 보장해야 한다.

사실, 같은 프로세스이더라도 자기와 소속이 다른 스레드에 속한 윈도우로는 SetFocus를 할 수 없으며 심지어 DestroyWindow조차도 직통으로 먹히지 않는다. AttachThreadInput를 써서 입출력 연결을 한 뒤에나 포커스 지정이 가능하며, 윈도우의 파괴는 WM_CLOSE를 보내는 방식으로 간접적으로 해야 한다. WM_CLOSE는 WM_DESTROY와 달리, 자신이 파괴되는 것을 해당 윈도우 프로시저가 거부할 수 있다.

Posted by 사무엘

2018/05/26 08:34 2018/05/26 08:34
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1493

Trackback URL : http://moogi.new21.org/tc/trackback/1493

Leave a comment

Windows API에서 BitBlt는 DC간에 비트맵 블록을 찍어 주는 아주 중요한 함수이다.
장치 독립 비트맵인 DIB라는 게 컬러 비트맵의 원활한 처리를 위해서 Windows 3.0에서 처음 도입되었지, DDB와 관련된 CreateBitmap, BitBlt 같은 함수, 그리고 컬러 brush 자체는 Windows의 초창기부터 있었다.
단순히 memcpy나 memmove 같은 함수와는 달리, BitBlt는 1차원이 아니라 2차원 평면을 표현하는 메모리 영역을 취급하는 관계로 동작 방식이 더 복잡하다.

BitBlt의 처리 대상인 두 DC는 내부 픽셀 포맷 같은 게 당연히 서로 호환이 돼야 한다.
원시적인 모노크롬 비트맵의 경우, 위치와 크기에 해당하는 좌표의 x축이 바이트 경계(8의 배수)로 딱 떨어지지 않을 때의 복잡한 보정이 필요하다.
그리고 원본과 타겟 DC가 동일한 경우, memmove 같은 overlap 처리도 x축과 y축 모두 고려하여 memmove보다 더 복잡한 상황 가짓수를 처리해야 한다.

BitBlt는 비트맵을 그냥 찍는 게 아니라 원본(S), 타겟(D)에 대해서 비트 단위 연산을 시킨 결과를 집어넣도록 아주 범용적으로 설계돼 있다. 일명 raster operation이다.
게다가 래스터 연산의 피연산자가 저 둘만 있는 게 아니라 타겟 DC에 지정되어 있는 브러시 패턴(P)까지.. 무려 세 개나 존재한다. 그래서 BitBlt는 PatBlt라든가 InvertRect 함수가 하는 일을 다 할 수 있을 정도로 범용적이다.

원본 S를 있는 그대로 복사해 넣는 건 SRCCOPY이고, 그냥 타겟 비트맵을 반전만 시키는 건 ~D이다.
그리고 마스크 비트맵(흰 배경에 검은 실루엣) M과 그림 비트맵(검은 배경에 실제 그림) S에 대해서 D&M|S (각각 and, or 연산)를 해 주면 보다시피 직사각형 모양이 아닌 스프라이트를 찍을 수도 있다. 래스터 연산을 갖고 할 수 있는 일이 이렇게 다양하다.

BitBlt가 사용하는 래스터 연산은 3비트짜리 정보(S, D, P)에 대해서 임의의 1비트(0 또는 1) 값을 되돌리는 함수라고 볼 수 있다. 이 함수가 받을 수 있는 인자의 종류는 8가지(2^3)이고.. 서로 다른 래스터 연산 함수는 2^(2^3)인 총 256가지가 존재할 수 있다.
그리고 SRCCOPY, SRCPAINT 같은 것들은 그렇게 존재 가능한 래스터 연산 함수를 나타내는 값이다. 각 변수별로 S는 11110000, D는 11001100, P는 10101010 이런 식으로 정해 놓으면 00000000부터 11111111까지가 S|D, D&~P 등 각 변수들을 조작한 모든 가짓수를 나타내게 된다.

그런데 컴퓨터에서 범용성과 성능은 대체로 동전의 양면과도 같아서 하나를 살리다 보면 다른 하나를 희생해야 하는 관계이다.
the old new thing 블로그의 설명에 따르면.. 과거 16비트 시절에 BitBlt는 사용자의 요청을 파악해서 좌표 보정 같은 전처리 준비 작업을 한 뒤, 실제로 for문을 돌면서 점을 찍는 부분은 내부 템플릿으로부터 기계어 코드를 실시간으로 생성해서 돌렸다고 한다. 이게 무슨 말인가 하면.. 단순히

void Loop(int l, int t, int r, int b);
Loop(r.left, r.top, r.right, r.bottom);

수준이 아니라

template<int LEFT, int TOP, int RIGHT, int BOTTOM>
void Loop()
{
    for(int j=TOP; j<BOTTOM; j++)
        for(int i=LEFT; i<RIGHT; i++)
            어쩌구저쩌구;
}

Loop<r.left, r.top, r.right, r.bottom>();

이런 걸 추구했다는 뜻이다.
비트맵을 찍을 때 범위 체크는 매 픽셀마다 그야말로 엄청나게 자주 행해지는 일이다. 그러므로 그 한계값을 컴퓨터의 입장에서 변수가 아닌 상수로 바꿔 버리면 레지스터도 아끼고 성능 향상에 도움이 될 수 있다.

16비트 Windows에는 32비트 OS 같은 가상 메모리 관리자라는 게 없으며, Java/.NET 같은 가상 머신과 garbage collector도 없었다. 그 대신 (1) 메모리의 단편화를 방지하기 위해 moveable한 메모리 블록들의 주소를 수동으로 한데 옮기고 (2) discardable한 메모리 블록을 해제하는 동작이 있었다.

가상 머신이 없으니 just-in-time 컴파일이라는 개념도 있을 리 없다. 하지만 BitBlt의 저런 동작은 Java 내지 JavaScript의 JIT 같아 보이기도 한다. 물론 진짜 JIT 기술보다는 코드 생성 패턴이 훨씬 더 정향화돼 있고 단순하지만 말이다. (뭐, BitBlt의 세부 알고리즘 자체가 단순하다는 뜻은 아님)
그리고 그 시절엔 DEP도 없었다. 메모리에 데이터가 담겼건 실행 가능한 코드가 담겼건, 아무런 차별이 없었다.

게다가.. 저 때는 그래픽 출력과 관련된 하드웨어 지원조차도 없었다. 1990년대 일반 VGA 화면에서는 화면이 갱신될 때 마우스 포인터의 잔상이 남지 않게 하는 처리조차도 소프트웨어적으로 해야 했다. IBM 호환 PC는 전통적으로 게임기용 CPU에 비해서 멀티미디어 친화적이지 않은 컴퓨터로 정평이 나 있었으며, 그나마 좀 미려한 그래픽 애니메이션을 보려면 한 프로그램이 하드웨어 자원을 독점하는 도스밖에 답이 없었다. 그러니 BitBlt 같은 함수는 CPU 클럭을 하나라도 줄이려면 정말 저런 눈물겨운 최적화라도 해야 했던 것이다.

이런 여러 이유로 인해 16비트 Windows 시절에는 지금의 32/64비트보다 어셈블리어라든가 실시간 코드 생성 테크닉이 확실히 더 즐겨 쓰였던 것 같다.
외부에서 호출 가능한 콜백 함수를 지정하기 위해 껍데기 썽킹 함수를 생성해 주는 MakeProcInstance (해제하는 건 FreeProcInstance)부터가 그 예이며..

또 그때는 API 훅킹도 대놓고 훅킹 대상 함수 메모리 주소에다가 내 함수로 건너뛰는 인스트럭션을 덮어쓰는 식으로 행해졌다. 지금이야 이식성 빵점에 가상 메모리와 프로세스 별 메모리 보호, 멀티스레드 등 여러 이유 때문에 위험성이 크고 사용이 강력히 비추되는 테크닉으로 봉인됐지만 말이다.

Windows 95는 비록 32비트 명령어와 32비트 메모리 주소 공간을 사용하지만 GDI 계층은 여전히 16비트 코드를 쓰고 있으니 내부적으로 과거의 테크닉이 그대로 쓰였다. 그 당시의 PC 환경에서는 최고의 성능을 발휘했겠지만, 그리기 코드 자체의 32비트화, 좌표계의 32비트 확장이라든가 멀티스레드 대비 같은 건 전혀 불가능한 레거시로 전락할 수 밖에 없다.

그에 반해 Windows NT의 BitBlt는.. 이식성이 전혀 없는 기계어 코드 실시간 생성 같은 테크닉이 쓰였을 리가 만무하며, 어느 플랫폼을 대상으로나 동일하게 적용 가능한 C 코드로만 구현되었을 것이다. 겉으로 하는 동작은 비슷해 보여도 내부 구현은 완전히 달랐으며, 같은 사양의 PC에서 속도가 더 느린 것은 어쩔 수 없었다. 그 대신 NT의 코드는 플랫폼과 시대를 뛰어넘어 살아 남을 수 있었다.

뭐, 1990년대에는 OS/2도 얼리어답터들이 관심을 갖던 레알 32비트 운영체제이긴 했는데.. 얘는 Windows NT와 달리 32비트 계층도 코드가 전반적으로 그리 portable하지 않았다고 한다. 그러니 타 CPU로 포팅은 고사하고 훗날 같은 CPU에서 64비트에 대처하는 것도 유연하게 되기 어려웠으리라 여겨진다.

그에 반해, OS가 아니라 게임이긴 하다만 Doom은 Windows NT와 비슷하게(약간만 타이밍이 더 늦은) 1993년 말에 첫 출시됐는데.. 세계를 놀라게 한 3차원 그래픽을 실현했음에도 불구하고 어셈블리어를 거의 사용하지 않고 순수하게 C 코딩만 한 거라는 제작사의 증언에 업계가 더욱 충격에 빠졌다. 사운드처럼 상업용 라이브러리를 사용한 부분의 내부 구현을 제외한 전체 소스 코드가 수 년 뒤에 공개되면서 이 말이 사실이었음이 입증되었다.

도스에서 Doom을 가능케 한 것은 쑤제 어셈블리어 튜닝이 아니라 Watcom 같은 최적화 잘 해 주는 32비트 전용 C 컴파일러였다.
엔진 코드가 C로 나름 이식성 있게 깔끔하게 작성된 덕분에 Doom은 소스가 공개되자마자 오픈소스 진영의 덕후들에 의해 온갖 플랫폼으로 이식되면서 변종 엔진과 게임 MOD들이 파생돼 나올 수 있었다. 물론 소스 공개 이전에도 상업용으로 갖가지 플랫폼에 출시되기도 했고 말이다.

오늘날이야 컴퓨터 아키텍처라는 게 2, 30년 전 같은 춘추전국시대가 아니며, 가상 머신이라든가 웹 같은 환경도 발달해 있다. 그러니 "C언어는 이식성이 뛰어나다" 이런 식의 진술이 뭐 거짓말은 아니지만 약간 어폐가 있다. 하지만 BitBlt API부터 시작해서 이식성 있는 코드와 그렇지 않은 코드가 궁극적으로 어떤 상태가 되었는지를 생각해 보니 이 또한 의미 있는 일인 것 같다.

다시 Windows API 얘기로 돌아와서 글을 맺자면..

  • BitBlt는 비트맵 출력 API 중에서는 그나마 가장 기본적인 형태이다. StretchBlt는 비트맵을 크기를 변형(확대· 축소)해서 찍을 수 있다. 이들의 DIB 버전에 대응하는 것은 각각 SetDIBitsToDevice와 StretchDIBits이다.
  • TransparentBlt와 AlphaBlend는 아까 같은 AND/OR 래스터 연산 대신 color key 내지 알파 채널을 적용해서 투명색이 적용된 비트맵을 찍어 주는 함수이다. Windows 98/2000에서 새로 추가됐다. 본인은 사용해 본 적이 없다.
  • PatBlt는 원본 DC의 지정이 없이 브러시 패턴과 타겟 DC와의 래스터 연산만이 가능한 마이너 버전이다.
  • PlgBlt와 MaskBlt는 마스크 비트맵까지 한꺼번에 받아서 스프라이트 처리가 가능한 버전이다. 거기에다 PlgBlt는 일차변환을 적용해서 직사각형이 아닌 임의의 평행사변형 모양으로 비트맵을 찍을 수도 있는데.. Windows 9x에서는 지원되지 않고 NT에서만 존재해서 그런지 본인 역시 이런 함수가 있다는 걸 아주 최근에야 알게 됐다.

실무에서는 이렇게 비트맵을 한꺼번에 찍어 주는 함수를 쓰지, SetPixel이라든가 무식한 FloodFill 같은 기능은 그래픽 출력에서 쓸 일이 거의 없는 것 같다.
BitBlt과 유사 계열의 비트맵 출력 GDI 함수들은 비트 연산을 다루는 시대 배경에서 만들어진 만큼, 요즘 PNG 이미지처럼 비트맵 내부에 들어있는 알파 채널을 제대로 취급하지 못한다. 그리고 비트맵을 확대해서 출력할 때의 안티앨리어싱도 부드럽게 처리를 못 한다. 레거시 코드에다가 그런 기능까지 플래그로 넣기에는 너무 복잡하고 지저분해져서 그렇지 싶다. 그도 그럴 것이 GDI는 하드웨어 통합적으로 얼마나 추상적으로 설계되었던가?

현대의 화면 래스터 그래픽에서 필요로 하는 최신 기능들은 한때 GDI+가 따로 담당하다가 요즘은 그것도 너무 느리다고 도태됐고 Direct2D 같은 다른 패러다임으로 옮겨 갔다.

Posted by 사무엘

2018/03/10 08:37 2018/03/10 08:37
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1466

Trackback URL : http://moogi.new21.org/tc/trackback/1466

Leave a comment

컴퓨터 프로그램의 GUI 구성요소들 중에는 여러 아이템들을 한데 나열하는 리스트 박스(list box)라는 게 있고, 고정된 한 문장에 대해서 예/아니요, 참/거짓 여부를 지정하는 체크 박스(check box)라는 게 있다.

체크 박스는 프로그램이 고정 붙박이 형태로 제공하는 기능이나 옵션 하나에 대한 설정을 할 수 있다. 그리고 리스트 박스는 보통은 가변적인 개수의 항목들 중에 하나를 선택할 때 쓰인다.
그런데 가끔은 이 두 물건의 기능을 한데 합치고 싶은 상황이 생긴다. 리스트 박스의 각 아이템들에 대해서 1비트짜리 정보를 배당해서 선택 여부를 지정하는 것 말이다.

뭐, Windows의 리스트박스 컨트롤은 모든 아이템들에 대해서 1비트도 아니고 그냥 machine word 크기 하나로 custom 정보 data를 지정하는 기능이 있다. 또한 필요하다면 하나가 아닌 복수 개 multi-selection 모드로 동작하게 할 수 있고, 각 아이템에 대한 custom drawing도 가능하다.

하지만 딱 부러지게 아이템들 앞에 자동으로 운영체제의 check box 그림을 그려 주고 체크 박스의 리스트를 구현하는 기능 자체는 없다. 필요하면 사용자가 그걸 직접 구현해서 쓰게 여건만 만들어 놨을 뿐이다.
그래서 MFC의 경우 기존 리스트박스를 서브클래스 해서 CCheckListBox라는 걸 제공한다. owner drawing만 구현하는 것으로는 충분치 않고, space를 누른 키보드 입력과 check 버튼 주위를 누른 마우스 클릭도 감지하게 메시지 몇 개를 서브클래스 했다.

자고로 화면에 뭔가 길다란 리스트를 만들고 아이템들을 복수 선택할 수 있게 해 놓은 프로그램의 원조는 PC-Tools나 MDIR, Norton Commander, 심지어 Windows 3.x의 파일 관리자 같은 파일 관리 유틸리티이지 싶다. 복수 개의 파일을 복사하거나 삭제하는 기능을 제공해야 하니 리스트의 복수 선택 기능이 무조건 필수이기 때문이다.

그런데, selection이라는 것과 highlight 선택막대의 관계를 어떻게 보느냐에 따라서 프로그램의 동작이 달라지곤 했다. 도스용 프로그램들은 selection과 선택막대가 서로 따로 논다고 본 반면, Windows는 selection이 곧 선택막대의 연장선이라고 봤다.

그래서 Windows의 리스트박스는 화살표 키를 누르는 순간 기존 selection들이 다 사라지면서 선택막대가 움직이곤 했다. Shift+화살표로 연속된 영역을 한꺼번에 선택하는 것 말고 불연속적인 영역을 취사선택하려면 Shift+F8부터 눌러서 선택막대가 아닌 포커스 테두리가 깜빡거리는 상태로 들어간 뒤, 포커스 테두리만 움직이면서 Space로 아이템들을 선택하면 됐다.

굉장히 특이한 동작인데 Windows에서는 이게 기본이다. 기본적으로 포커스 테두리만 움직이게 하는 모드는 extended 플래그(LBS_EXTENDEDSEL)로 따로 있었다.
그에 비해 평소에는 선택막대와 selection이 다같이 움직이고 Ctrl+화살표로 포커스 테두리를 움직여서 Space로 선택하는 비교적 '직관적인 방법'은 훗날 리스트뷰 컨트롤이 도입하게 된다. 아이템을 복수 선택하는 방식은 이 두 컨트롤이 서로 호환되지 않는다.

또한, 각 아이템들에 대해 체크 플래그를 지원하는 건 아이템을 그냥 복수 선택할 수 있게 하는 것과는 UI의 관점이 다르다. 비록 내부적으로 본질적으로는 아이템별로 1비트짜리 boolean 정보를 지정한다는 점에서는 차이가 없겠지만 용도가 같지 않다는 것이다.

복수 선택은 대체로 아이템들이 진짜 가변적이고 사용자에 의해 아이템을 추가하거나 삭제까지 할 수 있는 상황에서 쓰이겠지만 체크 리스트는 그렇지 않은 경우가 대부분이다. 단순히 응용 프로그램이 제공하는 기능과 옵션이 많기 때문에 리스트 형태로 만들었을 뿐이다. 체크 리스트는 복수 선택과 달리, 선택 막대 selection과는 완전히 별개로 관리되기도 해야 할 것이고 말이다.

다음은 MFC의 CCheckListBox를 사용했던 먼 옛날 날개셋 한글 입력기 1.x의 옵션 대화상자이다.
Windows XP부터는 테마도 등장했기 때문에 지금 상황에 따라 체크 박스를 그리는 방법 역시 더 복잡해졌다.

사용자 삽입 이미지

다음은 리스트 박스가 아니라 무려 트리 컨트롤(공용 컨트롤)을 사용했던 날개셋 2.x의 옵션 대화상자의 모습이다.
Internet Explorer가 4인가 5에서부터 인터넷 고급 옵션들을 이렇게 트리 컨트롤로 구현해서 오늘날 최후의 11 버전에까지 이어져 오고 있다. 지원하는 옵션이 너무 많기 때문이다. 본인 역시 이 스타일을 따라해 보았다.

사용자 삽입 이미지

공용 컨트롤들은 owner-draw 안 쓰고도 자체적으로 아이템별 비트맵을 지정할 수 있으며, 더구나 트리 컨트롤은 아이템들을 카테고리별로 분류도 할 수 있으니 더욱 좋다.
날개셋 3과 그 이후부터는 이들 옵션이 상당수가 오토마타와 글쇠의 수식, 별도의 카테고리 옵션 등으로 떨어져나간 관계로, 저렇게 트리 컨트롤까지 써야 할 정도로 긴 옵션 리스트를 만들 일이 없어졌다.

사실, 트리 컨트롤은 IE 4 타이밍에서 TVS_CHECKBOXES라는 스타일이 추가되기도 했다. 기존 이미지 스타일을 활용하는 게 아니라 그건 놔두고 옆에 체크 박스를 별도로 추가해 주는 형태이다.

트리 컨트롤에서 체크 박스는 설치 프로그램에서 어떤 소프트웨어 제품의 구성요소들을 계층 구조로 나열한 뒤 설치· 제거할 부분을 선택받는 부분에서 유용하게 쓰일 듯하다. 이런 데서는 자식 노드가 하나라도 선택되면 부모 노드들은 중간 상태로 바뀌고, 부모 노드를 선택하거나 해제하면 자식들도 한꺼번에 선택이나 해제되는 동작이 필요할 것이다.

하지만 트리 컨트롤의 체크박스 기능은 깔끔하게 구현되지 않아서 잡음이 많다. 스타일을 윈도우를 생성한 뒤에 SetWindowLongPtr로 런타임 때, 그리고 아이템을 하나라도 추가하기 전에 적절한 타이밍에만 지정할 수 있다.
레이먼드 챈 아저씨는 저건 차라리 스타일이 아니라 메시지 형태로 구현하는 게 더 나았을 정도라면서 API 설계 구조를 비판한 바 있다. (☞ 링크) 실제로 콤보 박스의 extended UI 여부는 스타일이 적절해 보임에도 불구하고 덩그러니 CB_SETEXTENDEDUI라는 메시지를 통해 지정하게 돼 있다.

한편, 트리 컨트롤은 처음 도입됐을 때부터 지금까지 체크 박스와는 달리, '복수 선택'은 지원하지 않고 있는 것으로 유명하다. 리스트뷰 컨트롤처럼 아이콘을 Shift 및 Ctrl을 이용하여 복수 선택할 수 있지 않다는 뜻이다.
Windows 운영체제는 탐색기에서 볼 수 있듯이, UI 디자인 철학이 "트리로는 분야를 하나 선택만 하고", "리스트에다가 그 분야에 속하는 아이템들을 출력한 뒤 복수 선택해서 지지고 볶는다" 형태이긴 했다.

계층 구조를 나타낼 수 있는 복잡한 UI 컨트롤에서 복수 선택까지 가능하면 프로그램의 기능이 매우 복잡해지며, 리스트도 아니고 트리 컨트롤이 굳이 복수 선택까지 가능해야 할 일은 매우 드문 것도 사실이다.
하지만 그럼에도 불구하고 당장 Visual Studio IDE부터가 클래스· 리소스· 솔루션 뷰의 트리 목록이 진작부터 복수 선택을 지원한다. 걔들은 4.0 시절부터 공용 컨트롤 없이 진작부터 자체 구현 트리 컨트롤을 써 왔기 때문이다.

끝으로, 체크와 다중 선택을 짬뽕한 듯한 기괴한 UI가 Windows의 역사상 단 한 번, 8의 리스트뷰 컨트롤에서 잠시 등장한 적이 있었다.
아이템의 좌측 상단 같은 특정 부위를 마우스로 가리키고 있으면 체크 박스가 나타나고, 그걸 클릭하면 아이템을 복수 선택할 수 있었던 것이다.
보통 아이템을 클릭하면 기존 selection들은 다 없어지고 그것'만' 선택되곤 하는데, 체크 박스를 선택하면 기존 selection들을 놔두고 그걸 추가로 선택할 수 있었다.

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

그 당시엔 아마 터치 장치를 염두에 두고.. Ctrl/Shift+클릭이나 드래그 없이 클릭만으로도 아이템들을 복수 선택할 수 있게 고심 끝에 저런 기능을 넣었던 듯하다.
하지만 반응이 좋지 않았는지, 이런 기능은 내 기억이 맞다면 Windows 8.1에서 곧장 없어졌고 다시 등장하지 않았다. 하긴, Windows 8은 저 정도면 약과이지, 아예 시작 버튼을 없애 버렸을 정도로 엄청 과격한 모험을 한 물건이기도 했으니까.

이렇듯, 리스트 박스, 리스트뷰 컨트롤, 트리 컨트롤을 두고 아이템의 복수 선택 및 체크 선택과 관련하여 할 말이 무척 많은 걸 알 수 있다. 복수 선택은 단수 선택만치 일상적으로 자주 쓰이는 기능은 아닐 뿐더러 어떤 방식으로 구현할지 동작의 customization의 폭도 넓은 편이다. 그래서 운영체제의 GUI가 곧장 직통으로 지원하지 않고 구현을 사용자에게 맡기는 편이었던 것 같다.

Posted by 사무엘

2017/12/20 08:36 2017/12/20 08:36
, ,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1439

Trackback URL : http://moogi.new21.org/tc/trackback/1439

Comments List

  1. 방문자 2017/12/21 20:01 # M/D Reply Permalink

    Windows 10에서도 탐색기에서 체크박스의 표출 여부가 옵션으로 남아 있습니다. 8.1에서 기본값이 바뀌었던 걸까요?

    1. 사무엘 2017/12/21 22:16 # M/D Permalink

      아하~ '기본적으로 꺼져 있는 상태'가 됐을 뿐, 그 UI가 완전히 없어진 건 아니었군요.
      옵션들 제~~일 밑에.. "확인란을 사용하여 항목 선택" (이것도 트리 컨트롤 기반 체크 리스트이군요~)...
      저는 처음 알게 됐습니다. 그렇다면 8.1에서 기본값이 바뀐 것이지 싶습니다. ^^

Leave a comment

1. 주 UI 스레드와 배경 작업 스레드

대화상자를 텅 빈 깡통 상태로 일단 띄워 놓은 뒤, 시간이 좀 오래 걸릴 수 있는 초기화 작업을 백그라운드에서 하고서 그게 끝나면 작업 결과를 대화상자의 각종 컨트롤에다가 반영하고 표시하기..
본인은 이런 형태로 동작하는 GUI를 구현한 적이 있었다. 리스트/콤보 박스에 들어갈 아이템들을 파일을 탐색하면서 수집하는 것이 대표적인 예이며, 당장 날개셋 한글 입력기 프로그램에도 이렇게 동작하는 UI가 한두 군데 있다.

그런데 그 백그라운드 작업이 언제나 수행되는 건 아니고, 조건에 따라서는 전혀 해당사항이 없는 경우도 있었다. 이때는 작업 결과의 표시와 관계가 있는 컨트롤들은 그냥 숨기거나 disable 시켜 놓으면 됐다.

그러니, 그런 컨트롤은 괜히 만들었다가 도로 숨기는 게 아니라, 스레드 함수가 자기 작업이 다 끝나고 마지막 부분에서 대화상자에다가 동적으로 생성하게 로직을 고치는 게 어떨까 생각을 했는데... 그렇게 하다가 더 피봤다.
당연한 말이지만 특정 스레드가 생성한 윈도우는 그 스레드의 실행이 끝남과 동시에 소멸되기 때문이다. 머리에 나사가 하나 빠지기라도 했는지 왜 그걸 생각을 못 했나 순간 "아차~!" 했다.

컨트롤 자체는 주 UI 스레드에서 미리 만들어 놓은 뒤, 백그라운드 작업 스레드에서는 그걸 ShowWindow / EnableWindow 정도의 제어만 할 수 있다. 컨트롤을 굳이 조건부로 동적 생성하고 싶다면, 백그라운드에서는 주 UI로 하여금 컨트롤을 생성하라고 메시지나 타이머 요청 정도를 보내는 간접적인 방법만 사용 가능할 것이다.

이렇게 윈도우의 생명 주기는 스레드와 관계 있는 반면, 접근 가능한 윈도우 클래스의 범위는 드물게 스레드가 아니라 RegisterClass를 호출한 모듈과 관계가 있다. 한 프로세스 안의 모든 모듈에서 접근 가능한 윈도우 클래스를 구현하려면 클래스 스타일에 CS_GLOBALCLASS를 지정해 줘야 한다. CreateWindowEx 함수는 현 스레드의 함수 호출 스택을 추적해서 자신을 호출한 모듈이 무엇인지를 따지기라도 하는가 보다. (인자로 받은 HINSTANCE 값은 무시하고 사용하지 않음.)

Windows 말고 안드로이드 프로그래밍을 해 보니 거기서는(Java)는 네트워크 통신은 무조건 백그라운드 스레드에서만 가능하고, 각종 GUI 요소의 조작은 반대로 주 스레드에서만 가능하게 해 놓았다. 이 규칙을 어기면 바로 예외가 발생한다. 그래서 Windows에서와 같은 혼동이 발생할 일이 없게 해 놨지만.. 간단한 통신 결과가 왔을 때 이를 GUI에다 표시하는 걸 한 함수에서 바로 못 하고 매번 스레드에, 메시지+핸들러로 실행 주체를 분리해야 하는 게 좀 번거로웠다.

2. 스레드의 강제 종료와 스택 상태

프로세스가 종료되는 가장 무난하고 좋은 방법은 main / WinMain 함수가 실행이 끝나서 자연스럽게 return하는 것이다. 그와 마찬가지로 스레드가 종료되는 가장 무난하고 좋은 방법 역시 해당 스레드 함수가 실행이 끝나서 자연스럽게 return하는 것이다.
하지만 Windows API에는 Exit...내지 Terminate...로 시작하는 프로세스· 스레드 종료 함수가 따로 있다.

두 단어 모두 뜻은 비슷하나, 전자는 자동사이고 후자는 목적어를 받는 타동사이다. 이로부터 유추할 수 있듯, 전자는 그 함수를 호출하는 자신을 종료하고 후자는 임의의 다른 프로세스나 스레드를 강제 종료시킨다.
어떤 프로세스가 이런 함수에 의해 종료되면 그 프로세스 하에서 돌아가던 모든 스레드들은 강제 셧다운된다. 그리고 반대로, 어떤 프로세스에서 모든 스레드들이 종료되어서 돌아가는 스레드가 하나도 없는 지경이 되면 빈 껍데기 프로세스는 자동 종료되고 소멸된다.

main 함수의 실행만 끝나면 자동으로 주변 잔여 스레드들의 실행도 강제로 다 끝나는 것 같지만 원래부터 그렇지는 않다. main을 호출한 하단의 C 런타임 라이브러리가 내부적으로 ExitProcess를 호출하기 때문에 그렇게 되는 것일 뿐이다. 운영체제 차원에서는 모든 스레드들의 실행이 끝나야 프로세스가 종료된다.

어쨌든, 프로세스나 스레드 같은 실행 주체는 자기 스스로 곱게 종료하는 게 좋다. 강제 종료 대상인 프로세스나 스레드는 자신이 강제 종료 당한다는 어떤 통지도 받지 못하며 이를 회피· 거부할 수도 없다. 뭐, 강제 종료를 막는 방법 뒷구멍이 있다면 악성 코드가 이를 마음껏 오· 남용, 악용할 것이니 저건 불가피한 면모도 있다. 강제 종료를 요청하는 프로세스가 적절한 권한만 갖고 있다면 강제 종료 작업 자체는 성공이 반드시 보장된다.

강제 종료는 파일이나 메모리, 스레드 동기화 오브젝트를 포함해 해당 스레드가 할당하고 선점해 놓은 그 어떤 자원도 제대로 수습· 회수하지 않은 채 말 그대로 해당 실행 주제만 없앤다. 그러니 엄청난 리소스 누수를 야기한다. 그나마 프로세스는 독립성이 높은 실행 단위인 덕분에 강제 종료되더라도 자기가 사용하던 모든 자원들이 자동으로 반납되는 게 보장이라도 되는 반면, 스레드는 그렇지 않다.

그렇기 때문에 TerminateThread는 TerminateProcess보다도 가능한 한 더욱 사용하지 말아야 한다.
I/O 관련 병목이나 데드락 같은 게 걸려서 해당 스레드의 코드 자체가 전혀 돌아가지 않을 때.. 옛날 같았으면 컴퓨터 리셋을 했을 피치 못할 상황에서나 극도로 제한적으로 사용해야 한다. 자기 스스로 실행 가능한 스레드라면 외부에서는 중단· 종료 플래그만 걸어 주고, 그 스레드가 알아서 실행이 종료되게 하는 것이 절대적으로 바람직하다.

그렇기 때문에 작업 관리자 같은 유틸에서 응용 프로그램을 강제 종료하는 기능은 일단 그 프로그램의 주 윈도우에다 WM_CLOSE만 살짝 보내 보고, 그 프로그램이 거기에 불응하면 API 차원의 극약 처분을 내리는 식으로 동작한다. 기왕이면 주먹보다는 말로 곱게 해결하는 게 좋으니까...

스레드가 강제 종료된 경우, 코드 실행 차원에서 발생하는 리소스 leak이야 어쩔 수 없는 귀결이다. 그런데 Windows는 전통적으로 exit 말고 terminate로 강제 종료된 스레드에 대해서는 그 스레드가 사용하던 스택에 속하는 메모리 주소도 해제· 재사용하지 않고 내버려 뒀다. 그러니 heap이 아닌 stack에 속하는 메모리가 leak이 발생하게 됐다.

이것은 스레드가 강제 종료되더라도 그 스레드의 스택에 속하는 메모리를 참조하던 다른 스레드가 뻗지 않게 하려고 성능보다는 안전을 고려해서 시행한 정책이었다. 어차피 TerminateThread를 할 정도이면 온갖 리소스들이 누출되었을 가능성이 높고 이왕 버린 몸에 비정상적인 상황이니 스택도 해제하지 않고 일부러 놔뒀던가 보다.
그러나 이 정책이 Windows Vista부터는 바뀌어서 이제는 terminate된 스레드의 스택도 곧장 해제된다. 흥미로운 점이다.

3. 열악하던 시절에 동시작업 구현하기

CPU 차원에서의 멀티스레드라는 게 없던 시절에 UI와 백그라운드 작업이 동시에 돌아가는 프로그램을 짜는 건 상당한 고역이었다.
옛날에는 컴퓨터 하드웨어 차원에서 관리되는 키보드 버퍼라는 게 있었다. 컴퓨터가 바빠서(busy) 정신없는 상태에서 사람이 키보드를 누르면 그게 일단 버퍼에 들어갔으며, 나중에 컴퓨터가 정신을 차리면 먼저 온 글쇠부터 밀린 처리를 했다. 일종의 queue 자료구조처럼 말이다.

이 키보드 버퍼는 크기가 15타 남짓밖에 안 됐다. 그러니 컴퓨터가 바쁜 상태에서 키보드를 조금만 많이 누르면 그 글쇠는 버퍼에조차 추가가 못 되고 컴퓨터가 시스템 전체를 잠시 멈추면서 높은 톤의 '삐~' 경고음을 냈다. "나 건드리지 마세요..!" 물론 ctrl, shift 같은 비문자 글쇠 말고 문자 글쇠들 한정으로. pause 키를 누르면 컴퓨터 전체의 실행을 일시정지 시킬 수 있던 시절의 얘기이다.

Windows 시대가 되면서 하드웨어와 소프트웨어 사이에 무슨 계층이 덧붙여졌는지, 컴퓨터에서 저런 걸 볼 일은 없어졌다. 하지만 9x 시절에는 운영체제가 대미지를 심하게 입고 반쯤 뻗어서 다운+재부팅 징조가 농후할 때면, 윈도우들이 메시지 큐가 다 차 버리고 메시지에 아무런 응답도 처리도 할 수 없는 상태가 되곤 했다. 이때는 그 윈도우로 마우스 포인터를 갖다대서 옮기기만 해도 짤막한 비프음이 났다. 이게 옛날에 키보드 버퍼가 다 차서 경고음이 나던 것과 같은 맥락의 현상이다.

옛날에 컴퓨터 속도가 왕창 느릴 때는 사용자가 화살표 키를 눌러서 화면을 스크롤 하던 도중에도 끊임없이 글쇠 입력 체크를 해야 했다. 그래서 화면 갱신 속도가 글쇠 연타 속도를 따라가지 못한다면 지금 갱신하던 것은 때려치우고 글쇠 처리부터 모두 한 뒤, 이로 인해 화면 위치가 바뀌었으면 스크롤을 처음부터 다시 하고, 변동 사항이 없으면 아까 하다 말았던 스크롤을 마저 계속하게 코딩을 했다.

오늘날은 단순히 2차원 스크롤을 위해서 저렇게 헝그리 코딩을 할 필요는 없을 것이다. 그러나 화면에다 아주 복잡한 3D 그래픽을 점진적으로 렌더링 하거나, 고해상도 만델브로트 집합 같은 프랙탈 그래픽을 실시간으로 그린다면.. 동일한 테크닉이 여전히 필요할 것이다.

CPU를 많이 소모하는 계산 위주의 작업 말고, 주변 기기와의 I/O 비중이 큰 작업도 생각해 보자. 워드 프로세서에서는 인쇄 중 동시작업(일명 스풀링), PC 통신 프로그램에서는 전화 연결 중에, 업· 다운로드 중에 동시작업.. 지금은 너무 당연해서 일도 아닌 게 옛날 도스 시절에는 해당 프로그램의 완전 첨단 고급 기능이라고 소개되곤 했지 않는가?

Windows는 여러 프로그램들을 동시에 띄워서 구조적으로 동시작업이 기능하다고 하지만, 16비트 시절엔 여건이 도스에 비해 막 좋을 건 없었다. 빡센 작업을 하는 중에도 여전히 사용자 반응성을 잃지 않으려면 message loop 차원에서 PeekMessage와 OnIdle 같은 로직이 추가돼야 하고, 작업 역시 UI의 반응성을 해치지 않을 정도로 연산을 짧게 끊어서 찔끔찔끔 해야 하는 건 변함없었다. 이런 정신없는 상태에서 트리 구조 순회나 순열 생성 같은 건 당연히 쌩 재귀호출로 구현할 수 없으며, 사용자 스택 자체 구현이 필수였다.

더구나 이런 idle time processing은 내가 아닌 Windows 내부의 고유한 message loop 하에서 구동되는 modal 대화상자 내지 메뉴 표시 중에는 중단된다는 문제가 있다. 타이머 메시지는 저렇게 modality와 관련된 끊김 현상은 없지만, CPU를 활용하는 효율이 일반적인 idle time processing 메커니즘에 비해 좋지 못하다.

이런 걸 생각하면 멀티스레드가 없었으면 지금처럼 사용자가 입력하는 텍스트의 맞춤법을 실시간으로 검사해서 빨간줄을 그어 주는 기능, C++ 같은 문맥 의존적인 언어 코드를 사용자가 입력하는 걸 인클루드 파트까지 실시간으로 구문 분석해서 자동 완성과 syntax coloring을 구현하는 건 불가능에 가깝게 힘든 일이 될 수밖에 없을 것이다.

4. 파이버: 스레드의 변종

사실, time slicing을 운영체제가 자기 재량껏 하는 게 아니라 내가 원할 때 하도록 thread의 변종인 fiber라는 게 있다. Windows의 경우, 일단 자기 자신을 일반 스레드에서 fiber로 먼저 변환해서(ConvertThreadToFiber) 초기화를 한 뒤, 다른 파이버들을 생성하고(CreateFiber), 파이버들끼리 필요한 타이밍 때 서로 전환(SwitchToFiber)을 하면서 열심히 제 할 일을 하면 된다.

이 경우 스레드 동기화 같은 건 전혀 필요하지 않으며, 멀티스레딩을 표방하면서도 프로그래밍 패러다임은 멀티스레드 성향이 전혀 아니게 된다. 사실, 이건 유닉스 기반의 서버 프로그램의 포팅을 돕기 위해 일부러 도입된 기능이지 실용적으로 딱히 쓰일 일도 없다. 하지만 복잡한 재귀호출이 여러 곳에서 동시다발적으로 일어날 때 자기 스택 상태를 고스란히 보존하면서 작업 context들을 원하는 때에 전환할 수 있다는 점에서는 뭔가 독특한 용도가 있을 것 같기도 하다.

개인적으로 thread가 원래 실타래라는 뜻이니, 컴퓨터 용어로서 thread는 '일타래'라고 번역해서 쓰면 꽤 그럴싸하겠다고 생각해 왔다. 서로 꼬일 수 있는 것까지도 동일한 개념이니까. 그런데 thread의 변종으로서 아예 '섬유'라는 뜻인 fiber는 우리말로 어찌 번역해야 할지 모르겠다. 우리말 순화· 번역이라는 게 이런 추가적인 조어력과 확장성까지 갖추지는 못하는 편이니 대부분 실패하곤 한다.

오늘날 운영체제에서 module이라는 건 EXE, DLL 등 실행 가능한 코드와 데이터, 리소스가 담긴 한 이미지 파일을 식별하는 개념이다. process는 자신만의 독립된 주소 공간을 가진 실행 공간으로, EXE만이 새로 생성할 수 있다. thread는 한 process 안에서 하나 이상 존재할 수 있는 실행 주체이다.
이들에 비해 instance, task는 좀 16비트스러운 용어이다. 32비트 이상부터는 프로세스들이 기본적으로 자기 주소에서 다 혼자 따로 노는 형태이기 때문에 한 모듈(HMODULE)의 여러 instance (HINSTANCE)라는 개념 구분이 별 의미가 없어져 있다.

운영체제에 따라서는 여러 개의 프로세스도 parent/child 관계를 맺고 job이라는 집단을 형성할 수 있다. Windows도 이를 API 상으로 흉내는 내는 걸 지원하지만 막 널리 쓰이지는 않는다. 마치 C가 함수 안에 함수를 공식적으로 지원하지 않는 것처럼(람다 내지 지역 클래스 같은 편법 말고..), 프로세스들도 굳이 계층 구조가 존재하지 않더라도 뭔가 심각하게 불편하거나 불가능해지는 건 없기 때문으로 보인다.

Posted by 사무엘

2017/11/20 08:38 2017/11/20 08:38
,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1429

Trackback URL : http://moogi.new21.org/tc/trackback/1429

Comments List

  1. 비밀방문자 2017/11/22 13:42 # M/D Reply Permalink

    관리자만 볼 수 있는 댓글입니다.

    1. 사무엘 2017/11/22 15:31 # M/D Permalink

      우와, 그 책 얼마 만에 다시 듣나 모르겠다..? (10몇 년 전 대학 시절의 전공 서적..)
      님도 컴공이 일취월장하고 있는 게 느껴지는구나? ㅋㅋ

Leave a comment

본인은 초딩 시절에 어렴풋이 경험했던 옛날 컴퓨터 환경의 추억을 회상하는 일에 관심이 많다. 16비트 환경은 베이직 이외에 C/C++ 같은 급의 언어로 프로그램을 직접 개발한 경험이 전무하기 때문에 더 관심이 간다.
그래서 블로그에서 API 썽킹 얘기도 한번 했고, 1년 남짓 전에는 Windows 9x 시절에 악명 높던 리소스 퍼센티지를 32비트 프로그램에서 직접 구하는 방법까지 옛날 책을 뒤져가며 복습해 봤다. 이번에는 이와 관련하여 지금까지 다룬 적이 없었던 다른 기술 얘기를 좀 해 보겠다.

시대를 풍미했던 Windows 9x는 왜 그리도 블루 스크린(BSOD)이 자주 뜨고 불안정했을까? 흔히 지저분한 16비트 코드 잔재 때문이라고들 그런다.
80386급 이상의 CPU에서 제공되는 보호 모드, 가상 메모리 같은 기술을 적극적으로 활용하면 굳이 그렇게 허접한 운영체제가 만들어질 일이 없다. 하지만 문제는 단 하나.. CPU가 원론적으로 제공하는 기능들을 제대로 활용해서 이상적인 운영체제를 만들려면, 당시 서민들이 범접할 수 없던 엄청난 메모리와 속도빨이 필요하다는 것이다.

Windows 9x는 Windows NT와 같은 최신 32비트 선점형 멀티태스킹 환경뿐만 아니라 기존 16비트 도스/Windows 프로그램과의 호환성도 놓치지 말아야 하고, 이런 모든 미래와 과거 이념을 Windows 3.1과 크게 차이 나지 않는 1990년대 중반의 서민들 컴에서 그럭저럭 구현해야 했다. 그러니 얘는 태생적으로 아주 기괴한 방향으로 개발될 수밖에 없었다.

그래서 파일 시스템이나 메모리처럼 반드시 32비트 덕을 봐야 하는 엔진 부분인 kernel은 32비트 코드 위주로 개발했지만, user나 gdi처럼 운영체제의 단순 외형과 관련된 부분은 16비트 코드를 그대로 답습했다. 이쪽 함수는 비록 32비트 프로그램에서 user32.dll, gdi32.dll을 통해 호출했다 하더라도 결국은 argument들을 thunk 해서 16비트 user.exe와 gdi.exe로 내려가서 기능이 수행된다는 뜻이다.

개량된 트루타입 글꼴 래스터라이저처럼 GDI 계층에도 32비트 코드로 다시 작성된 게 없지는 않지만 기본적인 틀은 여전히 16비트 기반이다.
Windows NT는 16비트 썽킹은커녕 훨씬 더 안정적인(하지만 속도는 좀 느려지는) 별개의 API layer가 있는 지경인데.. 9x와 NT가 서로 설계 이념과 처지가 얼마나 극과 극인지를 알 수 있다.

이런 설계 방식 때문에 Windows 9x는 16비트 heap 크기에 맞춰진 리소스 퍼센티지 한계가 여전히 걸려 있으며, GDI 좌표계도 기본적으로 32비트가 아닌 16비트 크기이다. 그래도 이런 건 그냥 한계와 제약일 뿐, 딱히 운영체제의 안정성과 관련된 문제는 아니다.
Windows 9x는 메모리 절약과 하위 호환성을 위해 (1) 16비트 코드를 많이 재활용했는데, 이걸 온전한 가상 머신 샌드박스 안에서 구동하는 게 아니라 (2) 32비트 코드와 동일한 주소 공간과 위상에서 동일한 권한으로 섞인 채 최대 성능으로 실행하는 것을 허용했다. 16비트 코드가 운영체제 차원에서 대놓고 돌아가는 부분도 있으니 저렇게 안 해 주면 안 된다.

그리고 이를 구현하는 과정에서 Windows 9x는 NT와 같은 급의 (3) 엄격한 메모리 보호를 포기했다. 스레드 동기화나 가상 메모리 같은 현대적인 개념이 없이 쑤제 어셈블리어로 코딩된 레거시 코드가 한둘이 아니니, 걔네들이 있는 그대로 돌 수 있게 시스템의 안정성을 일부 희생하게 된 것이다.

32비트 프로세스가 독자적으로 사용하는 최신식 private 메모리야 9x도 NT의 동작 방식을 물려받았기 때문에 각 프로세스별로 동일하게 보호가 잘 된다.
그러나 9x에서는 32비트 프로세스라 해도 16비트 프로그램과의 호환을 위해 남겨놓고 있는 4MB 이내 주소대의 영역은 보호가 적용되지 않으며, 반대로 운영체제 시스템 DLL이 로딩되는 상위 영역도 성능 오버헤드 간소화를 위해 모든 프로그램들이 씨크하게 같은 주소로 공유된다. 보호 같은 거 없다.

나쁜 마음 먹은 프로그램이 이런 16비트 호환 영역이나 시스템 DLL 영역 주소를 0으로 덮어써 버린다거나 하면 운영체제를 BSOD와 함께 곧장 다운시킬 수 있다. Windows NT에서는 메모리 덮어쓰기만으로는 이런 사태가 절대로 발생하지 않고 문제의 프로그램만 강제 종료되고 운지하는 것으로 끝난다. 하지만 9x는 그 정도로 튼튼하지 못했다. 메모리 보호 강도가 반쪽짜리일 뿐이라는 뜻이다. 반쯤은 응용 프로그램이 선할 거라고 믿고 곧이곧대로 돌려 주는 셈이었다.

이렇게 메모리 보호 말고 Windows 9x가 안정성이 결정적으로 취약한 분야는 멀티스레드와 관련하여 또 있었다.
Windows 9x/NT는 3.1에서는 꿈도 꿀 수 없던 선점형 멀티태스킹이라는 것을 도입했다. 한 프로그램이 자발적으로 CPU 자원을 반납하지 않고 무한 뺑뺑이를 돌더라도 운영체제가 강제로 CPU 자원을 뺏어서 다른 프로그램에게 골고루 분배해 줄 수 있다. 또한 한 프로그램 안에서 UI 스레드 따로, 작업 스레드 따로 운용이 가능하다. 작업 스레드가 재귀호출까지 마음대로 하는 동안 사용자의 입력에도 매끄럽게 응답할 수 있다는 뜻이다.

즉, 가상 메모리가 메모리 주소를 잘못 건드리는 것만으로 타 프로그램이나 운영체제를 뻗게 하지 않게 하는 공간 보호막이라면, 선점형 멀티태스킹은 한 프로그램이 CPU를 독식해서 시스템 전체의 동작을 먹통으로 만들지 않게 하는 일종의 시간 보호막이다.

선점형 멀티태스킹 환경에서는 내 스레드가 받고 있던 CPU 포커스가 싹 바뀌어서 타 스레드로 이동하는 게 정말 예고 없이 불시에 될 수 있다. 컴퓨터의 모든 코드가 자기 자신의 스택과 지역변수만 갖고 놀면서 고립된 채 돌아가는 게 아니며, 한 자원을 여러 스레드들이 공유하는 경우가 많다. 그런 코드에 둘 이상의 실행 주체가 동시에 진입했다간 프로그램 실행이 왕창 꼬여 버린다. 이건 마치 화장실에 문을 잠그는 기능이 없어서 누가 볼일이 아직 안 끝났는데 아무 예고 없이 문이 확 열리고 딴 사람이 들어오는 것과도 같다.

결국 스레드 사이에는 교통 정리 기법이 필요하며, 이를 위해 크리티컬 섹션, 뮤텍스 등 다양한 커널 오브젝트들이 존재한다. 여기까지는 아무 문제가 없다.
그런데 문제는.. 저 16비트 레거시 코드들도 선점형 멀티태스킹 통제의 대상이라는 것이며, 그 코드들은 레거시답게 멀티스레드 동기화 같은 대비가 전혀 돼 있지 않다는 점이다. 그런 걸 고려할 필요가 없는 환경에서 개발되고 구현된 코드이니까 말이다.

이것도 메모리와 마찬가지로 16비트 코드를 완전히 자기 가상 머신에서 따로 돌게 하면 별 문제될 게 없으며, Windows NT는 실제로 ntvdm이라는 16비트 가상 머신을 돌렸다.
하지만 Windows 9x는 가상 머신을 돌릴 여력이 안 되는 PC를 대상으로 성능을 얻는 대신 안정성을 희생하는 방법을 택했다. 바로, 16비트 GUI 쪽 코드는 GetMessage, PeekMessage 같은 함수로 명시적으로 CPU 자원을 반납하지 않는 동안에는.. 전체를 동기화 오브젝트로 둘러싸서 어떤 경우에도 여러 스레드들의 동시 진입이 되지 않게 한 것이다. 이름하여 Win16Mutex라는 무시무시한 시스템 동기화 메커니즘이다.

이게 무슨 뜻인가 하면.. Windows 9x도 16비트 프로그램만 줄곧 돌린다면 과거의 Windows 3.x와 별 차이 없이 동작하게 된다는 뜻이다. 제 기능을 활용할 수 없다.
물론 32비트 코드도 16비트로 thunk하는 gdi/user 함수를 호출할 수 있다. 걔네들은 그 함수가 실행되는 동안에만 Win16Mutex 안에 잠시 들어갔다가 나온다. 그러나 16비트 프로그램은 Get/PeekMessage를 호출하지 않고 실행되고 있는 동안 계속해서 Win16Mutex를 붙들고 있게 된다. 그리고 그 동안 운영체제의 그 어떤 프로그램도 16비트 코드를 수행할 수 없으며, 앞 프로그램의 실행이 끝날 때까지 기다려야 한다.

어떤 프로그램이 창을 띄웠다가 while(true) 같은 데라도 빠져서 응답이 멎었다고 치자. 그렇다면 32비트 프로그램은 ctrl+alt+del을 누른 뒤 작업 목록에서 그럭저럭 강제 종료를 할 수 있었다. 이 기능 자체는 NT뿐만 아니라 9x 계열에서도 있었다.
하지만 옛날 기억을 꺼내 보면, 16비트 프로그램은 그렇게 깔끔하게 강제 종료가 잘 되지 않았다. 16비트 프로그램이 응답 불가 상태가 되면 운영체제 전체가 실행이 불안정해졌으며, 해당 프로그램을 강제 종료한 뒤에도 매우 높은 빈도로 블루 스크린이 계속되다가 운영체제가 뻗었다. 그 이유가 바로 (4) 16비트 코드는 애초에 선점형 멀티태스킹 대상이 아니며, 16비트 코드의 실행이 32비트 코드의 gdi/user계층 기능에까지 직통으로 영향을 끼치기 때문이다.

이제야 퍼즐 조각이 끼워맞춰진다. 저건 메모리 보호와는 별개의 영역의 문제이다.
그럼에도 불구하고 Windows 95는 안정성이 더 헬게이트이던 Windows 3.x보다 많이 나아지지는 못해도 최소한 더 나쁘게 만든 건 없다. 그러니 제품으로 나와서 1990년대 중반의 PC 환경을 획기적으로 바꿔 놓을 수 있었다.

물론 Windows 9x도 Windows API와 무관하게 완전히 분리된 환경인 도스용 프로그램에 대해서는 그럭저럭 샌드박스를 지원하고 있었다. Windows NT의 ntvdm가 지원하지 못하는 더 다양한 도스용 프로그램을 직통으로 구동해 주면서도 말이다. Windows 3.x때부터 386 확장 모드가 도입되면서 Virtual 8086 모드를 이용해 도스창을 여러 개 동시에 여는 게 가능해졌다. 9x부터는 도스창을 강제 종료도 할 수 있게 됐다.

단지, 하드웨어를 너무 저수준으로 제어하는 도스용 프로그램이라면 대책 없으며, 16비트 Windows GUI 프로그램은 32비트 프로그램과 자원을 어중간하게 직통으로 공유하다 보니, 운영체제 전체에 여파가 끼치는 잠재적인 위험이 상존했던 것이다.

Windows NT, 9x, 그리고 더 과거의 win32s를 비교하면 다음과 같다.

  Windows NT Windows 9x Windows 3.1 + Win32s
PE 방식의 32비트 EXE/DLL 실행, 32비트 메모리 접근 O O O
프로세스 별 고정되고 독립된 주소 공간 보장 O O X
DLL도 인스턴스별 독립된 공간 보장 O O X
선점형 멀티태스킹과 멀티스레드 O O X
레지스트리, 32비트 파일 시스템, 최신 TTF 래스터라이저 O O X
온전한 메모리 보호 O △ (private area만. 도스 호환 영역과 커널 영역은 보호되지 않음) X
유니코드 API O △ (코드 변환, 메시지, 기본적인 문자 찍기 정도만 한정) X
유니코드 기반 O X X
user, gdi 계층이 완벽하게 32비트. 리소스 제약 없음 O X X
16비트 프로그램 가상화 O (안정적임) X (도스용 프로그램만 가상화. 느리고 메모리 적은 컴 친화적) X
하드웨어 계층 추상화 O (이식성. 하지만 느림) X (x86 전용, 저사양에서 성능 최적화) X
NTFS 파일 시스템 O X X
시스템 요구사항 압도적으로 제일 높음 win32s보다 더 높지만, NT보다는 훨씬 낮음 제일 낮음

이상.
지금 생각해 보면 컴퓨터가 성능이 정말 열악하던 시절에 어째 저런 유리몸 Windows를 어떻게 쓰고 지냈나 싶다. 특히 9x 계열 중에서도 1호인 Windows 95는 다음과 같은 점에서도 안정성이 굉장히 안습했으며, 지나치게 고성능인 컴퓨터를 감당(?)하지 못하는 물건이었다.

  • 잘 알다시피 디렉터리명에 CON 같은 예약어가 들어간 채로 파일 목록 조회 같은 요청을 하면 운영체제 전체가 뻗는다. 탐색기이든 dir 명령이든 마찬가지. 95/98에서는 별도의 버그 패치가 나왔고, ME에서야 버그가 처음부터 완전히 수정되었다.
  • 부팅 후에 밀리초 단위로 부호 없는 32비트 정수 범위를 초과하는 대략 7주(49.x일) 이상 계속 켜져 있으면 역시 뻗음. Windows 95는 알고 보면 7주짜리 시한폭탄이었던 셈이다. 하지만 이게 NT도 아니고, 무려 7주가 지나기 전에 십중팔구 어차피 다른 이유로 다운되고 재부팅이 일어났기 때문에 이 문제가 별로 부각되지 않았을 뿐이다. 이 문제 역시 별도의 버그 패치가 추후 공개되었다.
  • 저사양 PC에서 가상 메모리를 관리하는 방식의 한계로 인해, 램이 512MB보다 더 많은 컴에서는.. 단순히 초과 잉여 영역이 인식되지 않고 무시되는 게 아니라 그냥 부팅되지 않고 뻗는다.
  • 클럭 속도가 대략 2.1GHz보다 더 높은 컴에서는 Network Driver Interface Specification라는 계층에서 오류가 발생하고 뻗었다고 한다. 너무 빠른 컴퓨터에서 레거시 코드가 문제를 일으킨 유명한 다른 사례로는 볼랜드 파스칼의 crt 유닛이 266MHz보다 더 빠른 컴에서 오류를 일으켰던 것이 있다. 이런 것들은 대체로 내부적으로 시간 측정을 하다가 0으로 나누기 오류가 발생하는 형태인데, Windows의 저 문제도 마찬가지였던 것 같다.

그러니 옛날부터 컴덕들은 OS/2나 Windows NT 같은 신세계를 갈망하긴 했지만, 그걸 돌리려면 흙수저 가정에서는 엄두를 못 낼 정도의 비싼 고성능 컴퓨터가 필요했다. 윈95가 나왔던 시절에는 램 16MB도 돈지랄 감지덕지이던 시절이고 32~64MB는 가히 꿈의 영역이라 여겨졌었으니 말이다.
그러다 1990년대 후반에 가정용 보급형 PC들도 램 용량이 급증하여 100수십 MB급이 되었다. 그제서야 하드디스크 스와핑/thrashing을 볼 일이 없어졌으며, 마소의 입장에서는 NT와 별개로 굳이 헝그리한 9x 커널이 존재해야 할 명분이 사라졌다.

지금까지 옛날 이야기를 많이 늘어놓았는데..
본인은 우리나라 역사 내지 이념을 Windows의 개발 내력에다 비유해서 설명하기도 한다. 매우 적절한 비유라고 개인적으로 생각하기 때문이다.

우리나라가 이 승만 대통령을 비롯해 핵심 내각은 독립 운동가· 광복군 위주로 철저하게 깨끗하게 건국되었음에도 불구하고 중간과 말단의 군· 경 간부는 친일 부역자들도 불가피하게 그대로 재등용하여 운용되었다. 독립 운동가를 적극 무료 변론하다가 일제에게 찍혀서 면허 정지까지 당했던 애산 이 인 선생이 해방 후에 법무부 장관이 되고 나서는 오히려 반민특위의 해체에 앞장섰을 정도였다.

이게 Windows 95가 일단 겉으로는 32비트 코드 기반으로 개발되고 32비트 주소 공간과 선점형 멀티태스킹을 제공함에도 불구하고 내부적으로 16비트 코드를 잔뜩 재등용하고 메모리 보호가 완전하지 못했던 이유, 수시로 파란 화면 뜨고 불안정했던 이유와 정확하게 일맥상통한다.

한 마디로 그 시절에 가정용 컴퓨터에서 Windows NT 같은 운영체제를 돌리는 건 불가능했기 때문이다. 아직 조선과 일제 강점기 사고방식에 쩔었고 공산주의가 뭔지도 모르던 사람이 태반이던 시절에.. 게다가 북괴의 위협과 비열한 공작까지 횡행하던 시절에, 일제 치하에서 행정 유경험자를 재등용하지 않고서 치안을 유지하고 자유 민주주의 국가를 FM대로 이상적으로 세우고 굴리는 것도 동급으로 절대 불가능했다.

Windows 95의 한계에 대해서 정리해 보니 이 생각이 더욱 굳건하게 확신이 든다. 그 시절의 운영체제든, 194~50년대의 우리나라 사정이든 흑역사와 한계는 불가피하게 발생했던 것이지 악의적으로 일부러 발생한 것이 절대 아니었다. 아폴로 17호 이후로 인간이 달에 안/못 가고 있는 이유는 다른 악의적인 거짓이나 음모 때문이 아니라, 그저 단순히 재정이 부족하고 가성비가 안 맞기 때문인 것과도 같은 이치이다.

쓸데없이 자국 비하를 조장하고 사람 정신건강을 해치고 일제보다 더 나쁜 악을 정당화하고 무마하는 이 악하고 해로운 생각은 보이는 족족 반박하고 뿌리뽑아야 하며, 거기에 사로잡혀 있는 사람들을 산업화하고 일깨워 줘야 한다.

Posted by 사무엘

2017/10/14 08:36 2017/10/14 08:36
, ,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1416

Trackback URL : http://moogi.new21.org/tc/trackback/1416

Comments List

  1. 비밀방문자 2017/10/19 04:09 # M/D Reply Permalink

    관리자만 볼 수 있는 댓글입니다.

    1. 사무엘 2017/10/19 12:46 # M/D Permalink

      오랜만이구나, 반갑다? 잘 지내고 계신가!
      이 2, 30년 묵은 옛날 Windows 프로그래밍 배경 얘기가 이해가 될 정도이면 대단한 한편으로 님도 슬슬 아재력이 늘어 가는 것 같다? ㅋㅋㅋ

Leave a comment

Windows에서 메뉴, 리스트박스, 콤보박스처럼 세부 항목이 존재하는 고전적인 UI 컨트롤에는 기본 글꼴로 문자열을 찍는 기능뿐만 아니라 임의의 크기로 임의의 그림도 그리는 owner draw 기능이 있다. 한두 개 정도 특수하게 쓰이는 owner draw 기능이라면 해당 UI 컨트롤을 구동하는 대화상자 등 부모 윈도우에서 메시지를 받아서 처리한다.

그러나 매 아이템들마다 check box가 달린 리스트라든가, 트리 계층 구조를 owner draw 기능을 이용해서 얼추 구현한 리스트처럼.. 특정 owner draw 기능과 동작을 컴포넌트화해서 여러 곳에서 동시에 사용하고 싶다면 그 UI 컨트롤 자체가 개조 대상이 된다. 윈도우 프로시저를 서브클래싱한 후, owner draw 메시지를 부모 윈도우로부터 되받아서 자신이 직접 처리하면 된다. 이건 뭐 16비트 시절부터 존재해 온 아주 고전적인 Windows 프로그래밍 테크닉이다.

owner draw는 개념적으로 모든 아이템의 크기가 동일한 owner-draw fixed와, 각각의 아이템 크기가 모두 다를 수 있는 owner-draw variable이 존재하는데, 개인적으로 후자는 전혀 다뤄 본 적이 없다.

그리고 string 버퍼를 사용하는 owner-draw가 있고(LBS_HASSTRINGS 내지 CBS_HASSTRINGS 스타일), 그런 게 없는 owner-draw도 있다. 문자열의 옆에다가 아이콘 같은 걸 추가로 그리거나 문자열 자체를 좀 색다른 색깔과 폰트로 출력하기 위해서 owner-draw를 사용하는 것이라면 전자를 선택해야 할 것이고, 그게 아니라 완전히 생판 다른 그림만을 찍거나, 자체 버퍼에 있는 문자열을 직통으로 찍으려면 후자를 선택하면 된다.
문자열 없는 owner draw 리스트박스는 일일이 LB_ADDSTRING을 호출할 필요 없이 LB_SETCOUNT만으로 간단하게 아이템 수를 뻥튀기할 수도 있다.

owner draw 컨트롤이 동작을 시작하면 아이템을 손수 직접 그리라는 WM_DRAWITEM 메시지가 오기에 앞서, 그림을 그릴 영역을 정하기 위해 WM_MEASUREITEM 메시지가 부모 윈도우로 날아온다. 그런데 여기서 꽤 재미있는 동작 특성이 있다. WM_MEASUREITEM는 DRAWITEM과는 달리, 굉장히 일찍 날아온다. 대화상자의 경우, MEASUREITEM은 WM_INITDIALOG보다도 먼저 날아온다.

WM_INITDIALOG는 대화상자 내부의 모든 컨트롤들이 생성되었고 모든 준비가 완료되어서 대화상자가 화면에 표시되기 직전에 날아온다. 그러나 MEASUREITEM은 그렇게 내부 컨트롤이 생성될 때마다, WM_CREATE 타이밍에서 자신의 스타일에 owner draw 속성이 주어져 있으면 곧장 부모 윈도우로 전달된다고 생각하면 된다. 그러니 자기 주변의 다른 대화상자 컨트롤들이 다 생성되기도 전의 굉장히 이른 타이밍에 날아온다.

대화상자 윈도우(HWND)를 그에 상응하는 C++ 개체 같은 사용자 정의 오브젝트(LPARAM)와 연결하기 위해서는 CreateDialog나 DialogBox 같은 함수에다가 연결할 그 오브젝트 포인터를 넘겨주는 편이다. 그리고 HWND와 LPARAM이 실제로 만나는 타이밍이 WM_INITDIALOG이다. 즉, 이 메시지가 대화상자계에서 WM_CREATE나 마찬가지인 셈이다.

하지만 WM_MEASUREITEM은 이런 통상적인 초기화 메커니즘이 수행되기 전에 부모 윈도우로 호출된다. 그렇기 때문에 MFC 말고 자체적인 Windows API 프레임워크를 구현하고 있다면 이 메시지의 처리를 좀 특수하게 해 줄 필요가 있다.
리스트박스나 콤보박스가 좀 지연 초기화를 지원해서 대화상자의 초기화가 다 끝나고, 자기가 WM_PAINT를 받아서 화면에 그려지기 직전(WM_DRAWITEM)처럼 정말로 폭을 알아야 할 때에나 저런 메시지를 보냈으면 사용자가 UI 프로그래밍을 하기 약간 더 수월했을 텐데 싶은 아쉬운 생각이 좀 든다.

그리고 WM_MEASUREITEM의 도착 타이밍이 너무 일러서 부담된다면, 아이템의 폭을 꼭 이때 지정해 주지 않아도 된다. 뒤늦게라도 부모 윈도우에서 LB_SETITEMHEIGHT(리스트박스), CB_SETITEMHEIGHT(콤보박스) 메시지를 보내서 아이템 전체(ower-draw fixed), 또는 개별 아이템(owner-draw variable)의 폭을 지정해 줄 수 있다.
리스트박스의 경우 경험상 둘의 차이는 거의 없다. 콤보 박스는 WM_MEASUREITEM 메시지의 결과에 따라서 drop list 내부에서의 아이템 높이뿐만 아니라 한 줄짜리 자기 본체의 높이도 그에 맞춰 자동으로 조절되는 반면, CB_SETITEMHEIGHT 메시지는 그런 효과까지는 없다는 차이가 있다.

또한, 메뉴야 대화상자의 내부 컨트롤 같은 존재가 아니니 저런 대체제가 존재하지 않으며 owner-draw 메뉴 아이템의 폭을 지정하는 타이밍은 WM_MEASUREITEM밖에 선택의 여지가 없다. 딱히 MENUITEMINFO 같은 구조체에 자신의 높이를 지정하는 곳은 존재하지 않는다.

요즘 운영체제의 옵션에 따라서는 콤보 박스의 drop list가 튀어나올 때, 또는 메뉴가 출력될 때 바로 툭 튀어나오는 게 아니라 fade in으로 서서히 나타나거나 위-아래 내지 대각선 방향으로 슬라이딩 하듯이 튀어나오곤 한다. 이건 임의의 윈도우에 대해서 AnimateWindow라고 이런 애니메이션 효과를 구현해 주는 함수가 따로 있다.

그런데, 과거의 Windows 9x에서는 owner-draw 아이템이 들어있는 콤보박스나 메뉴에 대해서는 그런 애니메이션이 지원되지 않았다. 기본 스타일로 문자열을 출력하는 컨트롤만 애니메이션이 나오던 것이 2000/XP 같은 NT 계열에 와서야 owner-draw 방식의 컨트롤에 대해서도 동등하게 애니메이션이 지원되기 시작했다. 그림을 화면에다 바로 그리는 게 아니라 내부 버퍼 DC에다가 그려 놓고 그런 처리를 하게 된 듯하다.

참고로 AnimateWindow는 애니메이션 대상인 윈도우에다가 WM_PRINTCLIENT라고 좀 생소하게 생긴 메시지를 보낸다. 이것은 WM_PAINT와 비슷하게 창의 내용을 그리라는 메시지이지만, WM_PAINT 때와는 달리 BeginPaint나 EndPaint 호출이 필요하지 않다. invalid 영역이나 클리핑 처리 같은 개념도 없으며 주어진 DC에다가 언제나 윈도우 내용을 처음부터 끝까지 그려 주면 된다.

Posted by 사무엘

2017/09/18 08:37 2017/09/18 08:37
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1406

Trackback URL : http://moogi.new21.org/tc/trackback/1406

Leave a comment

본인은 몇 년 전에 쓴 글을 통해 Windows API에서 비트맵을 출력할 때 사용하는 GDI API 몇 개를 브러시와 비트맵의 관계라는 관점에서 비교하고 살펴본 적이 있었다. 이번에는 픽셀 포맷과 DDB/DIB라는 관점에서 관련 API들과 이들의 특성을 살펴보도록 하겠다.

1.
먼저, 비트맵은 CPU의 관점에서 봤을 때 빅 엔디언 형태이다.
모노크롬 비트맵에서는 128, 64 같은 큰 비트 자리수가 왼쪽을 나타내고 작은 비트로 갈수록 오른쪽으로 간다.
색깔을 나타내는 RGB야 숫자의 대소 구분이 무의미하겠지만, 일단 RGB 매크로(메모리)에서의 색상 배열 순서와 RGBQUAD 구조체(파일 저장)에서의 색상 배열 순서는 서로 정반대이다. 전자는 R이 최하위 비트이지만 후자는 R이 최상위 비트이다. 그러니 여기서도 이념이 빅 엔디언임을 확인할 수 있다.

2.
일반적으로 비트맵 폰트 파일 내부의 비트맵들은 한 줄이 바이트 단위로 align이 돼 있다. 그러나 CreateBitmap 함수가 받아들이는 DDB(장치 종속 비트맵)는 역사적인 이유 때문인지, 한 줄이 2바이트, word 단위로 align돼 있어야 한다.
compatible bitmap이 아니라 CreateBitmap으로 직통으로 만들 수 있는 비트맵이 사실상 모노크롬밖에 없다는 점을 감안하면, 저기에 전달되는 가로 크기는 사실상 언제나 16의 배수 단위여야 한다.

한편, BMP 파일과 직통 대응하는 DIB(장치 독립 비트맵)는 이런 제약이 더 커져서 한 줄이 4바이트 단위로 align돼 있어야 하며, 얘는 또 상하가 뒤집혀 있기까지 하다. y축 양수가 위로 올라가는 좌표계를 염두에 뒀기 때문이다. DIB를 취급하는 함수들은 다 이런 형태의 비트맵을 입력으로 받는다.

3.
Create(Compatible)Bitmap 함수로 만들어진 비트맵은 성능이 가장 좋고 속도가 빠르지만, 한번 초기화한 뒤에 내부 비트맵 메모리에 직접 저수준 접근을 할 수 없다. GetDIBits 같은 함수로 내부 메모리 컨텐츠에 대한 복사본만을 얻을 수 있을 뿐이며, 이 내부 메모리는 철저하게 장치 종속적이다. 즉, portable하지 않다. 컨텐츠를 조작하는 건 BitBlt 같은 타GDI 함수를 써서 해야 한다.

비트맵을 출력하는 다른 함수로는 SetDIBitsToDevice가 있다. 얘는 받는 인자가 많고 사용이 좀 복잡하긴 하지만, BitBlt와는 정반대로 그냥 아무 메모리가 가리키는 임의의 BMP 헤더와 컨텐츠를 통째로 받아서 그 내용을 화면에다 찍어 준다. 원본 비트맵에 대해서 뭐 메모리 DC 만들고 비트맵 만들고 SelectObject 할 필요가 없으며, 메모리에 직통으로 접근해서 픽셀, 팔레트 테이블, 크기 따위의 수정도 얼마든지 가능해서 매우 좋다.

하지만 BMP 헤더를 매번 해석해서 DIB를 DDB로 변환해서 찍을 준비를 해야 하기 때문에 이 함수는 비트맵을 뿌리는 속도가 DDB 전용 함수만치 빠르지는 않다. 구형 운영체제의 16/256색 구닥다리 비디오 환경에서는 성능 열화의 폭이 더욱 크다.

그런데 알고 보니 저 둘의 중간 역할을 하는 함수도 있다.
CreateDIBSection은 내부적으로 반쯤 DIB로 취급되는 HBITMAP을 되돌린다. 이 비트맵을 사용하기 위해서는 BitBlt를 쓸 때처럼 원본 메모리 DC를 만들고 SelectObject를 해 줘야 한다. 하지만 픽셀을 직접 조작할 수 있는 메모리 포인터도 되돌리기 때문에 이를 응용 프로그램이 사용 가능하다.

이 메모리는 운영체제가 내부적으로 직접 할당해서 준 것이다. SetDIB*처럼 아무 메모리에 있는 비트맵을 찍을 수 있는 게 아니며, 그림의 크기나 색상 수 같은 헤더 정보는 한번 정해진 뒤에 변경 가능하지 않다. (그게 달라진다면 그냥 비트맵을 새로 만들어야..) 단지 픽셀 데이터에만 접근 가능하며, 색깔 변경은 SetDIBColorTable라는 별도의 함수로 해야 한다.

하지만 픽셀 데이터에 직접 접근과 조작이 가능한 것만 해도 어디냐. 기존 HBITMAP의 특성은 다 가지고 있기 때문에 BitBlt, DrawText, LineTo 같은 GDI 함수들을 고스란히 사용하면서 그림이 그려진 결과를 메모리 포인터 레벨에서 바로 확인 가능하니 실로 놀라운 일이 아닐 수 없다. 이런 DIB의 특성을 반쯤 가지면서 비트맵을 뿌리는 성능도 SetDIB*보다는 약간 더 좋다.

지금까지 얘기했던 이 세 가지 API를 표로 정리하면 다음과 같이 요약된다.

  CreateBitmap + BitBlt SetDIBitsToDevice CreateDIBSection + BitBlt
픽셀 포맷 2바이트 패딩 4바이트 패딩 + 상하 반전 4바이트 패딩 + 상하 반전
사용하는 메모리 내부 전용 사용자 임의 지정 가능 내부 전용
픽셀 메모리에 직접 접근 가능 X O O
BMP 헤더에 직접 접근 가능 X O X
단색 비트맵의 색깔 지정 SetTextColor / SetBkColor BMP 헤더 구조체 값 직통 수정 SetDIBColorTable
성능 제일 빠름 제일 느림 약간 느림

* 참고로, CreateDIBitmap은 DIB 함수들처럼 BMP 헤더를 인자로 받긴 하지만, HDC까지 인자로 받아서 DIB를 완전히 DDB 형태로 변환해 버린다. 이 함수를 통해 생성된 HBITMAP은 외부에서 내용 수정이 가능하지 않다.

* 그리고 HBITMAP의 내부 컨텐츠를 얻어 오는 함수로 GetDIBits 말고 GetBitmapBits도 있는데, 얘는 그냥 레거시 잔재이다. BITMAPINFO 헤더 정보를 받는 부분이 없기 때문에 그냥 모노크롬 비트맵 데이터를 얻을 때나 쓰는 간소화 버전이라고 생각하면 된다.

예전에 Windows 95부터 2000/ME까지는 시스템 종료 명령을 내리면 화면 전체에 50% 검은 음영 픽셀이 깔리면서 시스템 종료, 재시작 같은 세부 기능을 선택하는 대화상자가 떴다. 지금은 그런 효과는 관리자 권한을 요청하는 UAC 확인 대화상자가 뜰 때에나 그렇게 배경이 어두워질 텐데 그때는 시스템 종료 대화상자가 그 비주얼 이펙트 역할을 담당했다. (XP에서는 그 효과가 "흑백으로 서서히 fade out"이라는 더 화려한 형태로 바뀌었다가, 후대 버전부터는 이펙트가 사라졌다.)

그런데.. 그렇게 50% 검은 음영을 뿌리는 게 바로 래스터 오퍼레이션을 가미한 BitBlt 내지 PatBlt 실행으로 구현되었다. 최신(당대 기준) 그래픽 카드에서야 즉시 전체 화면에 음영 뿌려졌겠지만, 하드웨어 가속 없이 640*480 VGA 내지 그에 준하는 구린 그래픽 환경에서는 음영이 위에서 아래로 뿌려지는 게 눈으로 보일 정도로 속도가 느렸다. 그건 나름 수십만 개에 달하는 픽셀이 바뀌는 거니까..

그리고 그게 바로.. 그 컴퓨터에서 BitBlt 함수로 화면을 가득 채우는 속도와 같다 생각하면 된다. 그때는 이 따위 느린 그래픽 함수로는 답이 없으니, Windows에서 게임을 돌리려면 발상의 전환을 달리한 DirectX 같은 API를 만들어야겠다는 생각을 응당 안 할 수 없었을 것이다. 하드웨어 계층 추상화+통합이 아니라, 하드웨어 직통 제어를 지원하게 말이다.

DirectX 쪽 그래픽 프로그래밍이 재래식 GDI 그래픽 프로그래밍과 다른 점은..

  • 하드웨어의 발전에 따라 프로그래밍 방법론의 변화 기복이 매우 큼.
  • 하려는 일(도형 그리기, 글자 찍기..)보다는 그래픽 하드웨어의 기능 위주로 API가 설계돼 있다. 사실, 이걸 수용하라고 애초부터 이념이 이런 식인 API를 따로 만든 거다.
  • 이런 이유로 인해, GDI처럼 프린터, 플로터, 메타파일 같은 디바이스까지 다 통합하는 추상화 계층 건 전혀 안중에 없음. 오로지 화면 아니면 화면 출력용 메모리 버퍼 위주이다.
  • BeginPaint/EndPaint로 대표되는 invalid 영역 그딴 개념이 없고, 그 대신 '서피스 소실'이라는 개념이 존재한다.

정도로 요약되겠다.
예전에는 GDI와는 완전히 다른 기술 계층을 거쳤기 때문에 화면 캡처도 특수한 프로그램을 써서 했을 정도이지만 이제는 그런 유별난 점이 점점 없어지고 통합돼 가고 있는 것도 인상적이다.

Posted by 사무엘

2017/09/15 19:31 2017/09/15 19:31
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1405

Trackback URL : http://moogi.new21.org/tc/trackback/1405

Leave a comment
« Previous : 1 : 2 : 3 : 4 : 5 : ... 11 : Next »

블로그 이미지

철도를 명절 때에나 떠오르는 4대 교통수단 중 하나로만 아는 것은, 예수님을 사대성인· 성인군자 중 하나로만 아는 것과 같다.

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

«   2018/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:
1073480
Today:
465
Yesterday:
603