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

1. Windows의 컴퓨터 비트 수 변화

과거에 주류 PC 환경이 (1) 16비트에서 32비트로 바뀌면서 소프트웨어 개발 환경이 크게 바뀌었다.
int와 WPARAM, handle, 포인터가 모두 4바이트 크기로 바뀌었고, 이로 인해 메시지도 몇몇은 스펙이 불가피하게 바뀌었다.
좌표계의 기본 단위도 다들 32비트로 확장됐고, 이로 인해 GDI 함수들이 상당수가 Ex 버전으로 바뀌었다. 왜냐하면 예전처럼 x, y 좌표 둘을 long 하나에다 묶어서 전달할 수가 없어졌기 때문이다.

하지만 선점형 멀티스레드가 지원되고 그 전에 모든 프로세스들이 자기만의 독립된 주소 공간을 갖는다는 건.. 과거엔 정말 상상도 못 할 혜택이다.
8비트야 거의 임베디드 급의 열악한 환경이니 멀티태스킹 따위는 별나라 얘기였다. 16비트 시절엔.. 어정쩡하게 아주 불편하고 힘들게 가능했던 반면.. 32비트가 되니 주소 공간도 넉넉하고 이제 좀 그럭저럭 할 만해진 것이다.

그리고 32비트에 와서는 예전에 깐깐하게 구분해야 했던 게 이제는 구분이 필요 없어지고(예: HINSTANCE vs HMODULE, far vs near), 예전에는 꼭 할당하고 해제해 줘야 했던 게 지금은 그럴 필요가 없는 등(resource 관련 API, MAKEPROC 따위).. 프로그래밍 하기가 전반적으로 더 간편해지고 편리해지기도 했다.

그에 비해 (2) 32비트에서 64비트로의 변화는 뭐.. int와 포인터의 크기가 달라진 것으로 인한 자잘한 충돌과 이식성 문제가 고작이다. 4GB 한계가 없어지기만 했을 뿐, 체감되는 변화는 아주 미미하다.
Windows의 경우, int는 물론 long조차도 여전히 32비트 크기로 유지된다. 그러나 WPARAM은 64비트로 확장됐다.

전에도 한번 얘기했듯이 게임기는 1990년대 후반, PC는 2000년대 후반, 스마트폰은 2010년대 후반이 돼서야 슬슬 64비트 시대에 들어섰다.
이런 곳은 비트 수가 점진적으로 늘어났기 때문에 기존 코드와의 호환성이 중요했다. 그렇기 때문에 포인터만 빼고 int나 long은 4바이트로 할지 8바이트로 할지 고민이 많은 편이었다.

그 반면.. 슈퍼컴퓨터 전용 아키텍처가 있던 시절 말이다. 197, 80년대에 처음부터 64비트로 시작했던 컴터 환경에서는 레거시 고민 따위 없었다. Cray 같은 플랫폼에서는 쿨하게 처음부터 int고 포인터고 몽땅 다 무식하게 64비트 모델을 채용한 곳도 있었다고 한다. 물론 오늘날이야 int까지 8바이트인 컴퓨팅 환경은 없다고 봐도 되지만..
그리고 저런 옛날 컴퓨터들은 데이터를 취급하고 연산하는 단위만 64비트였다. 아무리 슈퍼컴이라 해도 자기네 메모리 용량이 4GB에 미치지는 못했기 때문에 64비트 컴퓨팅이 곧 64비트 addressing을 의미하지는 않았다고 한다. addressing까지 다 되는 64비트 CPU는 1990년대가 돼서야 등장했다. (MIPS, DEC Alpha 따위) 아하~

얘기가 좀 옆길로 샜는데.. 아무튼 Windows는 16비트에서 32비트로 넘어갈 때 변화가 좀 있었고, 32에서 64비트로의 변화는 미미한 편이었다. 그럼 Windows의 역사상 16비트에서 32비트로의 전환만이 대격변이었던 것일까?
꼭 그렇지는 않았다. 오히려 더 옛날, (3) Windows 1 (+2)과 3 사이는 플랫폼 SDK의 변화, C 컴파일러의 변화 등의 단절이 더 심했다.

Windows 1과 2는 아직도 리얼 모드 내지 끽해야 286 표준 모드에서 멀티태스킹을 구현하던 정말 암울한 시절이다.
Windows의 오랜 역사를 좀 아는 guru라면, 20세기에 Windows에서 가장 혁신적인 변화는 바로 95나 NT도 아니고 3.0에서 "386 확장(enhanced) 모드"가 정식 도입되었던 사건이라고 말할 정도이다. (☞ 링크)

그랬기 때문에 Windows 1과 3은 같은 16비트 기계어에 같은 NE 포맷임에도 불구하고 1용 프로그램이 후대의 3 내지 9x에서 제대로 실행되지 않을 가능성이 매우 높았다.
게다가 저 1980년대의 구닥다리 C 컴파일러는 함수를 정의하는 문법조차 ANSI가 아닌 기괴한 K&R 방식이었다니.. 소스 레벨의 호환성도 기대하기 어렵겠다. 더 자세한 건 여기 글을 참고하시라. (☞ 링크)

2. 마소의 16비트 P-code 기술

마소와 관련된 옛날 이야기가 계속 이어진다. 이 블로그에서 본인이 지금까지 이 얘기를 한 번도 꺼낸 적이 없었다니 놀랍다.;;
네이티브 기계어가 아니라 다른 중립적인 바이트코드 기반으로 돌아가는 '가상 기계 프로그램'이라 하면 흔히 Java (JVM)나 C# (.NET, CLR) 같은 것만 떠올리기 쉽다. 이런 건 최소 32비트 이상의 컴퓨팅 환경에서 등장한 런타임 환경이다. 고유한 클래스 라이브러리도 갖고 있고 쓰레기 수집기도 제공한다.

하지만 마소는 창립하자마자 그 허접한 197, 80년대 8비트 컴퓨터로 제일 먼저 만들었던 게 BASIC 인터프리터였다. 현대적인 가상 머신 정도로 거창하지는 않지만, 그래도 고유한 바이트코드 가상머신 기술을 보유해서 16비트 컴퓨팅 시대까지 잘 써먹었다.

마소에서는 그 바이트코드를 스스로 P-code라고 불렀다. P는 pseudo-, portable, packed(조밀) 등을 뜻했다고 한다. 그리고 그걸 Basic뿐만 아니라 C/C++ 언어 컴파일러에다가도 접목했었다. 아니, 베이식은 그렇다 치지만 기계어 직통 컴파일이 당연시되는 언어이던 C/C++에다가는 성능(= 실행 속도) 희생까지 감수하면서 도대체 왜..?

이 바이트코드는 크기가 작았기 때문이다. 이게 packed의 의미이다.
같은 프로그램 소스를 비슷한 최적화 수준으로 컴파일 했을 때, 네이티브 x86 기계어 코드보다 훨씬 더 작은 크기로 표현할 수 있었다. 심지어 P-code를 해독하는 가상머신 코드의 오버헤드(9K 남짓?)를 포함시키더라도 수지맞는 장사였을 정도라니.. 이건 뭐 실행 파일 압축 기능까지 약간이나마 겸한 셈이었다.

컴퓨터 역사의 관점에서 볼 때 x86 자체도 골수 CISC 구조로서, 현대적인 아키텍처 대비 기계어 코드가 조밀하고 크기가 아주 작은 축에 드는 아키텍처라고 여겨진다. (그 대신 읽어들이고 디코딩하는 난이도가 쥐약이고, 저전력 모바일과 상극)
그런데 마소의 P-code는 그 악명 높던 x86 기계어보다도 더 조밀하고 작다니.. 그 시절에 얼마나 메모리가 비싸고 귀했고 메모리를 어떻게든 아껴야 했는지가 실감이 간다. PC에서도 386 486 같은 32비트 CPU는 진작에 등장하고 값도 내려갔지만.. 메모리가 아직 병목이었다. 이게 더 싸지고 풍부해진 뒤에야 본격적으로 Windows 95/NT가 쓰일 수 있었다.

Visual Basic이야 exe를 생성한다 해도 런타임 dll이 따로 필요하고 내부 코드는 P-code 기반이었다. 1997년에 출시된 5.0.. 최초로 32비트 전용으로 출시된 이 버전에 이르러서야 네이티브 코드 컴파일 기능이 도입됐다.
C/C++의 경우, MS C/C++ 7.0과 Visual C++ 1.x 시절.. 16비트 한정으로 이런 기능이 있다가 32비트부터는 폐기됐다. 그 대신, 16비트이기만 하면 플랫폼은 DOS와 Windows를 모두 지원했다.

따지고 보면 Windows NT의 32비트 PE (portable executable)는 저런 P-code와는 접점이 없었던 셈이다. 32비트 Visual Basic 5나 6을 쓰지 않는 한 말이다
자세한 것은 이 링크의 설명을 참고하시라. 마소의 전설적인 P-code에 대해서 구체적으로 소개한 글은 "Microsoft P-Code Technology" by Andy Padawer이 유일한 것 같다.

QuickBasic이나 GWBASIC은 소스 코드를 고유한 바이너리 포맷으로 저장하는 기능이 있었다. 이건 세상 그 어느 프로그램 개발 환경에서도 없는 기능이었지 싶다.
그 반면, 저 P-code는 소스 코드가 아니라 나름 기계어를 표방하고 컴파일된 코드였다는 차이가 있다.

3. 마소와 볼랜드 프로그래밍 툴의 Windows 지원 내력

(1) 아마 예전에 이 얘기를 한 적이 있었을 텐데..
1980년대 말부터 마소와 볼랜드에서는 주요 프로그래밍/개발툴을 내놓으면서 뭔가 교육용 저가 보급형 제품군에다가는 각각 Quick과 Turbo라는 스피디한 브랜드명을 붙였고, 기업용 기함급 모델에다가는 그냥 자기 회사 이름을 붙였었다.

(2) 1990년대 초엔 C 컴파일러에는 C++의 지원이 추가되었다. 그래서 지원 언어 표기가 C/C++이라고 바뀌었다.
마소의 경우, QuickC는 Microsoft C를 먼저 만들다가 곁다리로 병행하며 잠깐 만들었던 제품이다. 이건 C++ 지원 없이 겨우 2.0에서 맥이 끊겼다. 그 대신 이전부터 만들어 오던 MS C 6의 다음 버전이 MS C/C++ 7이 되었다(1992). 그리고 이거 다음 버전부터는 그 이름도 찬란한 Visual 브랜드가 시작됐고, C는 떼어낸 채 Visual C++ 1로 넘어갔다.

저 때는 1993년 무렵이었다. Visual C++은 Windows NT와도 역사를 함께한다. 이게 마소에서 최초로 내놓은 32비트 C/C++ 컴파일러이며, Windows NT 내부의 각종 프로그램들을 빌드하는 용도로, 즉 자체적으로도 쓰였기 때문이다.
물론 Visual C++도 1.5까지는 16비트 버전이 같이 나오긴 했었다. 그리고 대외적인 버전 번호는 1로 리셋됐지만 얘 역시 MS C를 계승한 제품이라는 흔적은 MSC_VER이던가 그 매크로 상수의 번호에 남아 있다.

(3) 한편, 볼랜드 진영에서는 Turbo C 2.0의 다음 작품이 Turbo C++ 1.0이 되었다. 제품명과 버전이 다 리셋됐다니 좀 이례적이다.
그리고 그 다음 버전인 Turbo C++ 2때부터 같은 버전의 Borland C++도 나란히 나오기 시작했다고는 하는데.. 실질적으로 Turbo와 Borland의 구분이 생겼다고 일반인들이 존재감을 인지하는 첫 버전은 3이다.
Turbo C++은 3인가 3.1에서 맥이 끊겼다. 그 뒤 적어도 4~5 버전부터는 Borland C++만 나오다가 RAD 툴인 C++ Builder 1로 넘어갔다.

(4) 그 시절 C/C++ 컴파일러 업계에서는 C++ 지원뿐만 아니라 Windows 플랫폼의 지원도 중요한 이슈였다.
마소는 DOS에 이어 Windows를 만들던 본가였고, C는 어셈블리어와 더불어 자기들 제품을 만들 때 사용되는 주력 언어이기도 했다. 그러니 MS C는 처음부터 Windows를 지원하는 게 너무 당연한 일이었다.

1980년대 중반, 정말 구닥다리 MS C 4~5 시절부터.. 그야말로 전설적인 Windows 1.x, 2.x 프로그램을 만들 수 있었다. 단, QuickC는 DOS용 버전 2.x대와 별개로 QuickC for Windows 1.0이 딱 한 번만 나오고 말았던 듯하다. 요컨대 마소는 QuickC의 Windows 버전만이 버전 리셋을 했고, 볼랜드는 C++ 컴파일러를 구현할 때 버전 리셋을 했다.

그에 비해 볼랜드 제품에서 Windows 지원이 추가된 건 버전 3.x부터로, C++까지 지원되고 난 이후의 일이다. 심지어 Win32의 지원은 Windows 95가 출시되고 4.x 정도는 된 뒤부터다. 후발주자 3rd-party 업체이니 이런 것 수용은 한 발 늦을 수 있다지만.. Windows용 32비트 extender까지 미리 만들었던 Watcom 같은 업체하고는 개발 방향이 많이 달랐던 것 같다. 그 대신 볼랜드에서는 OWL이라고 꽤 잘 만든 객체지향 프레임워크를 연구 개발했다.
이렇듯, Windows 지원과 관련해서는 볼랜드와 마소 개발툴 간에 이런 내력의 차이가 있었던 셈이다.

(5) 자, 그럼 C/C++ 다음으로 파스칼의 세계로 가면..
볼랜드에서 Turbo Pascal을 내놓으면서 1980년대를 호령하고 재미를 봤다. 도스 아니면 기껏해야 OS/2에서 말이다. 그러다가 1990년대 초, Turbo Pascal 6 타이밍 때 TP for Windows를 1.0과 1.5 두 차례 내놓았다. 아마 5.x던가 6이던가..
이때 ObjectPascal이라는 객체지향 문법이 언어에 도입되기도 했지만 이건 TP의 버전에 영향을 주지 않았다. 그 대신 Windows용을 1.0부터 다시 내놓았다는 점이 Turbo C++과는 다르다.

그러다가 Turbo Pascal 버전 7이 Borland Pascal 7과 나란히 출시됐으며.. 이 BP7은 TP for Windows 2를 통합· 포함한 형태가 됐다. 제품 라인업 한번 복잡하네..;;
TPW는 about 대화상자에 수학자 파스칼 얼굴이 그려져 있는 반면, BPW는 그렇지 않다는 차이가 있다.;;;

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

1992년에 출시된 Borland C++ 3.1, 그리고 Borland Pascal 7이 도스와 Win16을 풍미했던 장수만세 안정판으로 여겨진다.
Borland C++은 C++ Builder로 넘어가기 전 1993~1995년 사이에 자체적으로 버전이 4~5까지 올라가기도 한 반면, BP는 델파이로 넘어가기 전에 딱히 버전업이 없었다.
심지어 Delphi도 1995년의 첫 버전 1은 Win16, 16비트용이었고 버전 2부터 Win32로 넘어갔으니, 32비트화도 C++보다 늦은 셈이다.

한편, 마소는?? 처음에 Microsoft Pascal을 1980년대에 4.x 버전까지 개발했었다. 하지만 이건 Turbo Pascal과의 경쟁에서 승산이 없다고 판단했는지 접었다. 그렇게 접기 직전에 경쟁사 제품처럼 뽀대나는 IDE를 얹은 QuickPascal 1.0을 최후의 발악 차원에서 한번 내놓았을 뿐이다. Windows 지원 같은 것도 당연히 없었고 제품의 맥이 끊겼다.
볼랜드에서는 Turbo Basic을 만들었다가 반대로 마소의 QuickBasic 대비 승산이 없다고 생각해서 포기해 버렸으니.. 행보가 서로 정반대인 셈이다.

Posted by 사무엘

2024/03/30 08:35 2024/03/30 08:35
, , ,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/2281

1. 컴포넌트화의 필요성

전산학 중에서 소프트웨어공학이라는 것은 방대한 소프트웨어를 인간이 여전히 유지보수 가능하게 복잡도를 제어하며 설계하기, 기능과 파트별로 역할을 잘 분담시켜서 각 파트만 재사용하거나 딴 걸로 교체를 쉽게 가능하게 하기, 소프트웨어의 분량· 작업량· 품질을 정확하게 측정하고 효율적인 개발 절차를 정립하기처럼..
응용수학이나 전자공학보다는 산업공학과 가까운 측면이 있다. 단지, 얘는 유형의 제품이 아니라 무형의 코드 형태이기 때문에 여느 공산품과는 성격이 약간 다르게 취급될 뿐이다.

20세기 후반에 인공지능 연구 업계에 "AI 겨울"이 있었고 게임 업계에 "아타리 쇼크"라는 재앙이 있었던 것처럼, 프로그래밍 업계에도 "소프트웨어의 위기"라는 게 이미 1970년대에부터 있었다.

  • 코딩을 너무 중구난방으로 하고 나니, 일정 규모 이상의 프로젝트에서는 도대체 유지보수가 되질 않고 그냥 처음부터 다시 새로 만드는 게 더 나을 지경이 된다.
  • 이놈의 빌어먹을 스파게티 코드는 하는 일도 별로 없는데 쓸데없이 너무 복잡해서.. 처음에 작성했던 사람 말고는 알아먹을 수가 없고 maintainable하지가 않다.
  • 소프트웨어의 개발 속도가 오히려 하드웨어의 발전 속도를 따라가지 못한다.
  • 작업 기간을 줄이기 위해서 사람을 더 뽑았는데.. 웬걸, 신입들을 가르치느라 시간이 더 소요된다;;;
이런 문제들이 체계적인 소프트웨어공학이라는 이론의 도입 필요성을 촉진시킨 것이다.

그래서일까..??
"무식한 goto문 사용을 자제하자"라는 구조화 프로그래밍 이후로 객체지향 프로그래밍이란 게 프로그래밍 언어와 코딩 패러다임을 완전히 정복했다. 요즘 주류 언어들 중에 '클래스', 그리고 '상속'이라는 게 없는 언어는 찾을 수 없을 것이다.;; 이게 캡슐화, 은닉, 재사용성 등 소프트웨어공학적으로 여러 바람직한 이념을 코드에다 자연스럽게 반영해 주기 때문이다.

실험적으로 시도됐던 초창기의 순수 객체지향 언어들은 유연하지만 느린 런타임 바인딩 기반의 메시지로 객체 메소드를 호출한다거나.. 심지어 정수 하나 같은 built-in type에다가도 몽땅 타입 정보 같은 걸 덧붙이며 객체지향을 구현하느라 성능 삽질이 많은 경우도 있었다.
그러나 C++은 객체지향 이념에다가 C의 저수준, 그리고 빌드타임 바인딩(경직되지만 빠른..)을 지향하는 현실 절충형 디자인 덕분에 상업적으로 굉장히 성공한 객체지향 언어로 등극했다.

2. 마소의 실험

자 그래서..
1990년대에 마소에서는 고유 브랜드인 Windows가 대히트를 치고 소프트웨어 OEM (IBM 납품..)으로 그럭저럭 먹고 살던 처지를 완전히 벗어나니.. 당장 먹고 사는 고민보다 더 본질적이고 고차원적인, 소프트웨어공학적인 고민을 시작했던 것 같다.
얘들 역시 소프트웨어를 재사용 가능한 컴포넌트 형태로 만드는 것에 관심을 많이 기울였다. 그래서 재사용을 위해 바이너리 수준의 공통 규약, 프로토콜을 만들어서 자기들의 운영체제 차원에서 밀어붙이고 홍보하기 시작했다.

이때가 마침 C에 이어 C++ 컴파일러를 개발하고 MFC라는 라이브러리도 만들고, 코딩 스타일에 본격적으로 '객체지향'이란 게 가미되기도 했던 때이다. 하지만 마소에서 추구했던 것은 단순히 언어나 개발툴 차원에서 함수나 클래스의 모음집인 라이브러리 SDK 만들고 DLL 만드는 것 이상의 수준이었다.

제일 먼저.. (1) Windows라는 이름답게, 특정 기능을 수행하는 윈도 컨트롤을 컴포넌트화한다. 리치 에디트 컨트롤을 비롯해 각종 공용 컨트롤, 웹브라우저 컨트롤 같은 것 말이다. 이 사고방식이 극대화되어 "컴포넌트를 내 폼에다가 끌어다 놓고, 프로퍼티를 설정하고 이벤트 핸들러를 구현해서 응용 프로그램을 곧바로 만든다" RAD라는 개념이 완성되었으며.. Visual Basic이라는 정말 똘끼 충만한 개발툴이 만들어지게 됐다.

도스 시절의 GWBASIC이나 QuickBasic에서 참신한 점은 그 특유의 대화식 환경이었는데, Visual Basic은 또 다른 새로운 돌풍을 일으켰다. 경쟁사인 볼랜드에서는 이런 개발 스타일을 파스칼과 C++에다가도 도입하게 됐다.

(2) 그리고 마소에서는 서로 다른 응용 프로그램에서 만든 결과물을 문서에 자유롭게 삽입할 수 있게 했다. 이름하여 OLE라는 기술이다.
가령, Windows의 워드패드는 아래아한글이나 MS Office Word에 비하면 아주 허접한 프로그램일 뿐이다. 하지만 문서 안에 그림판에서 만든 비트맵 이미지를 집어넣고, 엑셀에서 만든 차트를 집어넣을 수 있다.

별도의 수학 수식 편집기에서 만든 수식, 악보 편집기에서 만든 악보, 그리고 WordArt/글맵시 같은 프로그램으로 만든 각종 글자 꾸임 배너까지..
단순히 무식한 그림 형태로 집어넣는 게 아니라는 것이 핵심이다. 이것들은 벡터 이미지로 취급되기 때문에 크기를 키워도 화질이 깔끔하게 유지된다.

그리고 그런 출력 이미지 자체뿐만 아니라, 각 프로그램에서 취급하는 내부 원본 데이터, 즉 소스가 그대로 보존된다. 그렇기 때문에 만들었던 객체를 손쉽게 수정도 할 수 있다.
그 객체를 더블 클릭하면 프로그램 내부에서 그림판이나 악보 편집기, 수식 편집기 등이 잠시 실행돼서 객체를 수정하는 상태가 된다..;;

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

서로 다른 프로그램이 이런 식으로 서로 분업 협업한다니.. 신기하지 않은가? 도스 시절에는 상상도 못 한 일일 것이다.

3. 프로그래밍의 관점

그러니 Windows에서 워드처럼 뭔가 인쇄 가능한 출력물을 만드는 업무 프로그램이라면 OLE 지원은 그냥 닥치고 무조건 필수였다. 다른 OLE 프로그램의 결과물을 삽입하든(클라), 아니면 다른 프로그램에다 자기 결과물을 제공하든(서버), 혹은 둘 다 말이다. macOS나 리눅스에는 비슷한 역할을 하는 규격이나 기술이 있는지 궁금하다.

Windows 프로그래밍을 다루는 책은 고급 topic에서 OLE를 다루는 것이 관행이었다. 다만, Windows API만으로 OLE 지원을 저수준 구현하는 건 굉장히 노가다가 심하고 귀찮았다. 그래서 MFC 같은 라이브러리 내지 아예 VB 같은 상위 런타임이 이 일을 상당수 간소화해 줬었다. MFC 앱 신규 프로젝트 세팅 마법사의 경우, OLE 지원 기능의 추가 여부를 선택하는 옵션이 응당 제공되었다.

Windows라는 플랫폼의 프로그래밍에 입문하려면 창(윈도)의 스타일과 특성, 메시지 메커니즘을 알아야 할 것이고 그래픽 API라든가 셸의 구조에 대해서도 알아야 할 것이다.
그런데 그런 것뿐만 아니라 이 바닥도 완전히 독립된 별개의 프로그래밍 분야이며, 기초부터 고급까지 한데 연결된 Windows 프로그래밍의 정수라고 생각된다.

이건 제일 간단하게는 확장자 연결이나 클립보드, drag & drop 구현과도 연결고리가 있다. 이 분야 API를 제공하다 보니 COM이라는 IUnknown이 어떻고 type library가 어떻고 하는 규격이 제정되었다.
사실, Windows에 레지스트리라는 것도 맨 처음엔 확장자 연결이나 OLE 클라/서버 정보만 저장하기 위해서 만들어졌다가.. 나중에 ini를 대체하는 응용 프로그램 설정 저장 DB로 용도가 확장된 것이다.

COM 형태로 제공되는 운영체제 기능을 사용하려면 CoCreateInstance를 호출해야 하고, 이런 프로그램은 처음에 CoInitialize라는 함수를 호출해 줘야 한다. 즉, 운영체제를 상대로도 별도의 초기화가 필요하다는 것이다.
그런데 OLE 기능을 사용하려면 OleInitialize라는 함수를 사용하게 돼 있는데, 얘가 하는 일은 CoInitialize의 상위 호환이다. OLE가 COM의 형태로 구현돼 있기 때문에 그렇다. 둘의 관계가 이러하다.

굳이 OLE 관련 기능뿐만 아니라 가까이에는 DirectX, 그리고 날개셋 한글 입력기와도 관계가 있는 TSF 문자 입력 인터페이스도 다 COM 기반이다. 하지만 문자 입력은 굳이 COM이나 OLE 따위 기능을 사용하지 않는 프로그램에서도 관련 기능을 접근할 수 있어야 하기 때문에 COM 초기화 없이 관련 인터페이스들을 바로 생성해 주는 함수를 별도로 제공하는 편이다.

4. ActiveX

과거에 인터넷 환경에서 마소 IE 브라우저의 지저분한 독점과 비표준 ActiveX는 정말 악명 높았다. 그런데 ActiveX라는 건 도대체 무슨 물건인 걸까??

마소에서는 앞서 컴포넌트화했던 그 윈도 컨트롤들을 데스크톱 앱뿐만 아니라 인터넷 웹에서도 그대로 돌려서 그 당시 1990년대 중후반에 각광받고 있던 Java applet에 대항하려 했다. Visual Basic 폼 내지, 특정 프로그램의 내부에서 플래시나 IE 컨트롤 생성하듯이 꺼내 쓸 법한 물건을 웹에서 HTML object 태그를 지정해서 그대로 띄운다는 것이다.

그때는 컴퓨터의 성능이 지금처럼 좋지 못했고 지금 같은 방대한 웹 표준이 존재하지도 않았었다.
그러니 웹브라우저에서 동영상도 보고 초고속으로 돌아가는 게임도 하고, 특히 무엇보다도 금융 거래를 위한 각종 암호화 기능을 돌리기 위해서는 닥치고 웹에서 그냥 생짜 x86 native 앱을 돌리는 게 제일 편했다.

이런 컴포넌트의 이름이 OLE Control이었는데, 이걸 웹에다 특화된 형태로 신비주의 마케팅 명칭을 붙인 게 ActiveX 컨트롤이다. 아마 마소 역사를 통틀어 길이 남을 엽기적인 작명이 아닐까? 하긴, DirectX도 비슷한 시기의 작명이니까 말이다. ㅡ,.ㅡ;;

하지만 웹에서 가상 머신이 아니라 특정 플랫폼의 네이티브 코드를 직접 구동하는 건 너무 무식하고 이식성도 떨어지고 표준 친화적이지 못하니 ActiveX는 마소에서도 버림받고 늦어도 2010년대부터는 완전히 퇴출 단계에 들어섰다.
지금은 Java applet도 완전히 멸망했고, 이들의 대체제는 정말 눈부시게 성능이 향상된 JavaScript 가상 머신이라고 보면 될 것이다.

5. OLE와 관련된 과거 유행: embed된 형태로 실행

저렇게 마소에서 COM/OLE니 ActiveX를 막 밀고 양성하던 1990년대 말~2000년대 초에는 어떤 프로그램이 다른 프로그램의 내부에 embed된 형태로 실행된 모습을 지금보다 훨씬 더 자주 볼 수 있었던 것 같다. 그와 관련해서 ActiveDocument (!!)라는 기술도 있긴 했다.

당장, MS Office 97에 있었던 Binder라는 유틸은 여러 Word, Excel 따위의 문서를 한데 묶어서 자기 안에서 해당 프로그램을 띄워서 내용을 편집하는 유틸이었다. 대단한 기술이 동원됐을 것 같지만 그래도 쓸모는 별로 없었는지 후대에는 짤리고 없어졌다.

Visual C++ 6의 IDE는 “새 파일” 대화상자를 보면 통상적인 텍스트 파일이나 프로젝트/Workspace뿐만 아니라 맨 끝에 Other documents라는 탭도 있어서 MS Office 문서를 자기 IDE 안에서 열어서 편집할 수 있었다. 당연히 MS Office가 설치돼 있는 경우에만 한해서.. 아까 그 Binder처럼 말이다.
그런데 이 역시 현실에서는.. 그냥 Word/Excel을 따로 띄우고 말지 굳이 워드/엑셀 문서를 왜 Visual C++ IDE에서 편집하겠는가? 그 기능은 후대엔 없어졌다.

ActiveX 기술의 본가인 IE 브라우저야 더 말할 것도 없다.
저런 MS Office 문서를 다운로드 해서 열면 해당 앱이 따로 열리는 게 아니라, 문서 보기/편집창이 웹페이지 화면에 떴었다. 별도의 프로세스로 말이다. 그 기술이 최초로 도입된 건 IE4가 아니라 1996년의 IE3부터였다.
파워포인트 슬라이드는 그 웹페이지 화면에서 곧장 슬라이드 쇼가 시작됐다. 이건 괜찮은 기능인 것 같다.

옛날에 pdf를 보기 위해서 Acrobat Reader를 쓰던 시절엔, 이 앱도 OLE 기술을 이용해서 IE 내부에 embed된 상태로 뜨는 걸 지원했었다. 지금이야 브라우저가 자체적으로 PDF를 표시해 주는 시대이지만 말이다.;;

요즘 컴터 다루면서 OLE 개체 삽입 기능을 쓸 일이 과연 얼마나 될까?
요즘은 개체 삽입으로 프로그램을 실행했을 때, 예전처럼 프로그램이 embed 형태로 실행되는 게 아니라 그냥 별도의 창으로 따로 뜨는 편인 것 같다. 앞서 소개했던 XP 시절의 모습과는 좀 다르다.
따로 실행됐을 때는 원래 문서에 표시된 컨텐츠와 자기가 다루는 컨텐츠가 따로 노니, 전자는 검은 빗금을 쳐셔 구분해야 한다는 UI 가이드라인도 있긴 하다.

사용자 삽입 이미지

이런 것들이 다 20여 년 전의 아련한 추억이고 한물 간 유행이다.
그나저나 인터넷으로 ppt 슬라이드를 받으면 바로 열리지 않아서 불편하다. 속성을 꺼내서 ‘신뢰할 수 없는 파일 차단’을 해제해야 볼 수 있다. 이것도 chm 도움말 파일을 바로 열리지 않는 것처럼 보안 강화를 위해서 취해진 조치인지 모르겠다.

Posted by 사무엘

2023/03/16 08:35 2023/03/16 08:35
, , , , ,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/2137

Windows는 태생적으로 ‘유니코드 = 2바이트 단위 인코딩’이라는 걸 전제에 깔고 만들어졌다.
거기에다 유니코드라는 게 없던 쌍팔년도 도스 시절과의 호환성을 너무 중요시해서 그런지, 2바이트가 아닌 1바이트 단위 인코딩 쪽은 일명 ANSI라 불리는 국가별 지역구 문자 코드에 오랫동안 얽매여 있었다. (cp949 따위)

그래서 이쪽 진영은 ‘유니코드의 1바이트 단위 인코딩’에 속하는 UTF-8의 지원이 맥이나 리눅스 같은 타 운영체제에 비해 굉장히 미흡한 편이었다.
가령, 파일의 경우 앞에 BOM을 꼭 넣어야만 ANSI가 아닌 UTF-8이라고 인식했는데.. 그러면 이건 말짱 도루묵이어서 지원하지 않는 것과 별 차이 없었다.

이러니 한 git 저장소에다가 넣고 여러 플랫폼에서 공통으로 사용하는 소스 파일의 경우, 영문이 아닌 한글로 주석은 무서워서 넣지도 못할 지경이었다.
Windows만 ANSI cp949를 선호하니 이건 타 운영체제의 IDE에서는 인코딩을 번거롭게 수동 지정하지 않는 한, 제대로 인식을 못 했다. 거기서 다시 저장을 하면 한글 내용은 당연히 다 날아갔다.

Windows에서도 UTF-8로 인식시키려면 파일 앞에다 BOM을 집어넣어야 하는데, 이러면 Windows 말고 타 컴파일러에서는 이게 배탈을 일으켰다.
정말 거지 같은 상황이었다. Windows는 1993년 NT 첫 버전부터 나름 유니코드를 염두에 두고 설계된 물건임에도 불구하고, 이런 분야에서는 전혀 유니코드에 친화적이라는 티가 느껴지지 않았다.

무려 2010년대 중후반이 돼서야 Visual C++ 2017인가 2019쯤에서야 드디어 BOM이 있건 없건 소스 파일의 인코딩을 다 UTF-8로 인식시키는 옵션이 추가됐다. 아마 202x 버전쯤에서는 이게 디폴트 옵션이 돼야 할 것이다.
그리고 언제부턴가 메모장이 편집하는 파일의 기본 저장 인코딩이 ANSI 대신 UTF-8로 바뀌었다.

응용 프로그램뿐만 아니라 Windows 자체도 10의 후대 패치를 통해 일단 명령 프롬프트의 인코딩에 UTF-8 지정이 가능해졌다. CHCP 65001 말이다.
단, 이런 명령 말고 프로그램 상으로 UTF-8 기반의 명령 프롬프트 환경을 어떻게 생성하는지는 잘 모르겠다. 검색해 보면 있겠지.. 배치 파일과 명령 argument를 몽땅 다 유니코드로 줄 수 있어야 진정한 유니코드화일 텐데 말이다.

다음으로 2019년쯤엔가 굉장히 큰 변화가 생겼는데..
유니코드를 지원하지 않는 구닥다리요 과거 Windows 9x의 잔재로나 여겨지던 각종 ...A 함수 말이다.
A 함수도 ANSI가 아닌 UTF-8 인코딩으로 문자열을 취급함으로써 유니코드를 지원하게 하는 통로가 뚫렸다.
그래.. 내가 원하던 게 이거였다. 진작에 좀 지원해 줄 것이지..!!

물론 Windows가 내부적으로는 문자열을 몽땅 UTF-16 방식으로 처리하고 있고, 2000년대부터는 ..A 함수 같은 건 만들지도 않는다. 그러니 ..A 함수의 유니코드화가 막 획기적으로 대단한 일은 아닐 것이다.
그러나 이렇게 해 주면 1바이트 단위로 문자열을 취급하는 각종 오픈소스 라이브러리에 대해서 골치 아프게 문자열을 변환하고 W 함수를 호출하는 thunk를 만들지 않아도 유니코드 파일명에 접근할 수 있어서 기존 코드의 포팅이 굉장히 수월해진다.

이 ANSI 코드 페이지라는 개념은 원래 시스템 global한 설정이며, 변경한 뒤에는 재부팅이 필요할 정도로 보수적인 속성이었다.
그런데 이걸 응용 프로그램마다 샌드박스를 씌워서 다른 값으로 가상화할 수 있고 심지어 UTF-8로 지정 가능해진 것은 고해상도 DPI 설정과 양상이 굉장히 비슷하다. 이것도 시스템 global이다가 응용 프로그램 단위로, 심지어 모니터 단위로 세부 지정과 변경이 가능해졌기 때문이다.

응용 프로그램의 매니페스트 정보를 통해 지정한다는 점마저도 동일하다. application → windowsSettings에 있다~!

<activeCodePage xmlns="http://schemas.microsoft.com/SMI/2019/WindowsSettings">UTF-8</activeCodePage>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>

20여 년 전에는 마소에서 unicows라고, 응용 프로그램이 Windows 9x에서 ...W 함수를 호출하면 문자열들을 변환해서 A 함수로 재호출해 주는 호환 layer를 개발· 배포한 적이 있었다. 한 프로그램이 2000/XP에서는 유니코드를 지원하고, 9x에서는 유니코드를 지원하지 않아도 기본적인 실행만은 되라고 말이다.
이제는 A 함수로도 UTF8 인코딩을 통해 유니코드에 접근하는 통로가 생겼다니, 참 오래 살고 볼 일이다.

또한, 이렇게 세월이 흐르면 Windows에서도 2바이트 완성형 CP949는 2바이트 조합형만큼이나 점점 보기 힘들어지고 역사 속으로 사라지지 싶다. 마치 플래시나 IE6, 보안이 안 좋은 http가 퇴출되듯이 말이다.
Windows가 일찍부터 유니코드를 지원했다고는 하지만 실질적으로 재래식 1바이트 인코딩의 퇴출을 가능하게 한 것은 UTF-8의 도입이라고 봐야 할 것이다.

한편, 웹이야 살아 있는 프로그램이 아니라 문서이니.. EUC-KR이니 CP949이 더 오래 남아 있을 것이다. 그러고 보니 내 홈페이지부터가 블로그 말고 HTML 페이지는 다 구닥다리 ANSI 인코딩을 쓰고 있구나. =_=

※ 여담: 2바이트 인코딩의 문자 집합 크기

우리나라의 KS X 1001 완성형 2바이트 한글 코드는 ISO/IEC 2022라는 옛날 규격에 맞춰서 94*94 = 8836 크기의 격자 안에 완성형 한글 2350자와 상용 한자 4888자, 그리고 나머지 1000여 자에 달하는 특수문자를 배당해 놓았다.

그 뒤 CP949, 일명 마소 확장완성형은 현대 한글 11172자에서 2350자를 제외한 나머지 한글 8822자를 KS X 1001이 사용하지 않는 2바이트 문자 조합에다가 억지로 집어넣었다.
KS X 1001이 lead byte와 tail byte 공히 0xA1부터 0xFE까지만을 사용하는 반면, CP949는 영역이 더 넓다. 특히 tail byte로는 알파벳 A~Z, a~z까지 사용한다.

그런데 이 ISO/IEC 2022 격자 크기 8836과, 비완성형 한글 수 8822는 값이 놀라울정도로 비슷하다. 우연인지, 의도된 결과인지 모르겠다.;;
한글 글자 수 11172와, 16*16픽셀 8*4*4벌 도깨비 한글 폰트의 크기 11520도 꽤 비슷하게 느껴진다. 이건 진짜로 의미가 서로 전혀 무관하기는 하다만 말이다.

Posted by 사무엘

2022/08/13 08:35 2022/08/13 08:35
, , ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/2054

Windows 운영체제에서 제공하는 GUI용 컨트롤 중에는 애니메이션 컨트롤이라는 게 있다. 이것은 명령 버튼이나 에디트 컨트롤, 리스트 및 콤보 박스처럼 Windows 1.x 시절부터 있었던 완전 native가 아니고, 1990년대 중반에 운영체제가 32비트로 갈아 타던 95/NT 3.5 시기에 도입된 '공용 컨트롤'에 속한다. 즉, 도구모음줄, 리스트뷰 컨트롤, 트리 컨트롤, 진행 상황(progress) 표시 컨트롤, 슬라이더와 같은 급이다.

애니메이션 컨트롤은 컴퓨터가 무슨 작업을 하고 있을 때, 작업 중임을 간단한 '움짤'을 통해 사용자에게 시각적으로 피드백을 주는 역할을 한다. 즉, progress 컨트롤과 같이 쓰이는 경우가 많으며, 그 작업의 소요 시간이 굉장히 길거나 언제 끝날지 예측할 수 없는 상황일 때 애니메이션이 더욱 유용해진다.

게다가 애니메이션은 단순한 눈요기 이상으로 컴퓨터가 지금 내부적으로 하는 작업이 무슨 의미를 지니는지를 사용자에게 상징적으로 일깨워 주는 효과도 있다!

사용자 삽입 이미지

  • 탐색기에서 파일을 복사 중일 때 종이가 이쪽 서류가방에서 저쪽 서류가방으로 날아가는 모습
  • 삭제 중일 때 종이가 날아가면서 인수분해-_-되는 모습
  • 다운로드 중일 때 지구본에서 사용자의 컴퓨터로 종이가 날아가는 모습
  • 디스크 조각 모음을 실행할 때, 흩어졌던 건물 블록들이 짠~ 다시 한데 조립되는 모습

등이 좋은 예이다.
Windows 8 이후로 등장한 그 뱅글뱅글 돌아가는 동그라미들, 슉~ 중앙에 나타났다가 다시 슉~ 사라지는 동그라미들도 당연히 애니메이션에 속한다. 단, 얘들은 내부 작업의 의미를 시각화하는 건 없고 그냥 기하학적인 눈요기가 전부라 하겠다.

그럼 이 애니메이션 컨트롤은 어떤 형식의 파일을 사용할까?
컴퓨터 GUI에는 복잡한 코덱으로 디코딩해야 하는 전문적인 멀티미디어 동영상 말고, 그보다 가벼운 '움짤' 애니메이션 데이터라는 카테고리가 존재한다. 자동차에다 비유하면 버스보다 작은 승합차 정도에 대응할 것 같다.

  • 전문 동영상에 비해 파일 구조가 훨씬 더 단순하고, 프레임 크기는 작은 편이다.
  • 16/256색 같은 저색상도 지원한다. 저색상은 각 프레임을 무손실 압축으로 저장한다.
  • 오디오는 지원하지 않는다. 그 대신 투명색· 알파 채널을 지원한다. 영화 같은 전문 동영상에서는 이런 개념이 반대로 전혀 필요하지 않을 것이다.

카카오톡 이모티콘의 애니메이션이라든가 심지어 마우스 포인터의 애니메이션도 딱 이런 범주에 든다.
한때는 이런 움짤 저장용으로는 플래시(swf) 아니면 애니메이션 GIF가 널리 쓰였다. 그러나 플래시는 기능이 너무 많이 추가되면서 플레이어 런타임도 너무 무거워졌고.. 또 결정적으로 2010년대 중반부터는 완전히 퇴출됐다. gif야 뭐.. 256색의 한계를 벗어나지 못한 구닥다리일 뿐이고..

1990년대엔 오토데스크 사에서 개발한 flc/ fli라는 파일 포맷도 전문 동영상이라기보다는 애니메이션에 가까운 물건이었다. 심지어 Windows 매체 재생기의 초창기 버전이 재생을 지원하기도 했었다. 하지만 얘 역시 개인적으로는 실제 파일을 본 적이 전혀에 가까이 없으며, 소리소문 없이 듣보잡으로 전락하며 묻혔다.;;

Windows에서는 *.ani라고 애니메이션이 들어간 마우스 포인터 파일도 지원하긴 했지만.. 얘는 일반적인 비트맵이 아니라 아이콘에 대한 애니메이션이다 보니 담을 수 있는 그림에 대한 제약이 크다.
그러니 아주 오랜 세월이 지난 2010년대가 돼서야 png에다가 애니메이션이 추가된 apng, 그리고 jpg의 대체제로 개발된 webp에다가도 애니메이션이 추가된 Animated WebP가 뒤늦게 각광받는 중이다.

하지만 Windows 애니메이션 공용 컨트롤은 처음 도입되었던 1990년대 중반 이후로 시간이 완전히 정지한 채 시대에 너무 뒤쳐져 있다. 저런 최신 기술들을 전혀 지원하지 않고 오로지 avi만 지원하는데.. 제~~~일 단순하고 원시적인 run-length (RLE) 방식으로 압축된 256색 이하의 색상 영상만을 받아들인다.

얘의 디코딩 난이도를 이미지 파일 포맷에다 비유하자면, GIF에도 못 미치고 지금은 역사 속으로 사라진 PCX급밖에 되지 않는다.
저색상 기반답게 color key 기반으로 투명색 처리도 지원하긴 하지만.. 그럴 거면 gif라도 좀 지원할 것이지 하는 아쉬움이 남는다.

애니메이션 컨트롤은 왜 이렇게 허접하게 설계된 걸까? 전문적인 동영상 재생을 목적으로 만들어진 게 아니며, 탐색기에 들어가는 자그마한 애니메이션을 재생할 정도로만 극도로 최소주의 최적화 정신에 입각하여 기능이 구현됐기 때문이다.

1994~95년이면 모자이크나 넷스케이프 같은 WWW 기반 그래픽 웹브라우저가 이제 막 만들어졌던 시절이고, 386~486에 램 겨우 4~8MB급 컴퓨터로는 JPG는커녕 GIF 디코더를 돌리는 것도 다소 부담스러웠었다. 또한 그림판조차 BMP와 PCX 이외의 파일 포맷은 읽고 쓰는 걸 지원하지 않았었는데 GIF를 운영체제의 공용 컨트롤이 지원할 거라고는 전혀 기대할 수 없을 것이다.

물론 그땐 그랬다 치지만 지금까지도 애니메이션 컨트롤이 너무 빈약한 것은 변명의 여지가 없다고 하겠다. 그래서 이제는 탐색기 같은 운영체제 셸조차 애니메이션 컨트롤을 사용하지 않고 있다. 공용 컨트롤이란 게 원래는 셸에서 쓰던 물건을 보편적인 컴포넌트로 확장한 것이었는데 이건 참 아이러니한 현상이 아닐 수 없다.

요즘이야 탐색기에서 파일을 복사할 때는 전송 속도 그래프가 종전의 애니메이션을 대신하고 있다. 하지만 저 그림에서 보듯, Vista인가 7까지만 해도, 뭔가 서류 갈은 게 복사본이 짠~ 생기는 걸 형상화한 애니메이션이 떴었다. 파일을 삭제할 때도 비슷한 컨셉의 애니메이션을 볼 수 있었다.

그런 것들은 딱 봐도 알겠지만 Windows 9x 시절 같은 16~256컬러 나부랭이의 단순한 애니메이션이 아니다. 그리고 그건 애니메이션 공용 컨트롤로 재생하는 게 아니라는 것이다.;;
파일 내용을 표시하는 제일 중요한 부분조차 Windows 7의 탐색기부터는 리스트뷰 컨트롤을 사용하지 않는 것처럼 말이다.

(단, Spy++로 확인해 보면, 트리 컨트롤은 여전히 사용하고 있음. 외형을 많이 마개조해서 말이다.
반대로 Visual Studio는 먼 옛날 6.0 시절부터 지금까지.. 프로젝트/리소스 view에서 트리 컨트롤을 사용한 적이 없었다. 단적인 예로 트리 구조에서 ctrl+클릭으로 multiple selection이 동작하는 건 공용 컨트롤에서 전혀 지원되지 않는 기능이다. 흥미로운 사실이다.)

이상이다.
Windows 95 이후로 지금은 셸의 GUI와 공용 컨트롤 사이의 격차가 너무 많이 벌어져 있다.
9x 시절엔 작업 표시줄(taskbar)에 표시되는 각종 프로그램들 제목이 '탭 컨트롤'로 구현돼 있었다는 거 아는 분 계시려나.. 하지만 얼마 못 가.. 아무리 늦게 잡아도 XP때부터는 뭐 없이 당연히 자체 구현으로 바뀌었다.

Windows 10부터는 절대 안 바뀔 것 같은 메모장도 큰 파일의 로딩 속도가 획기적으로 개선됐고, \n 같은 줄 바꿈 문자 처리도 개선됐다.
에디트 컨트롤 같은 그 극도의 고인물 썩은물 코드도 마소에서 마음만 먹으면 개선될 수 있다. 그런 것처럼 시대 추세의 변화에 따라 애니메이션 컨트롤도 좀 개선이 됐으면 좋겠다는 게 개인적인 생각이다.

애니메이션 컨트롤과 관련된 기술적인 여담을 몇 가지 늘어놓으며 글을 맺도록 하겠다.

(1) 비트맵, 아이콘 따위는 응용 프로그램에서 자주 쓰이는 물건이다 보니 RT_BITMAP, RT_GROUP_ICON 같은 번호 기반의 표준 리소스 포맷도 있다. 그러나 애니메이션은 쓰이는 빈도가 압도적으로 낮다 보니 표준 리소스 포맷 번호가 제정돼 있지 않고, 그냥 "AVI"라는 포맷 문자열만 제정돼 있다. 거의 폰트(RT_FONT) 급으로 마이너하지 싶은데 말이다.

(2) 애니메이션 컨트롤은 전통적으로 백그라운드 스레드를 사용해서 애니메이션을 출력했는가 보다. 그리하지 않고 UI 스레드와 동일한 스레드에서 타이머만 사용해서 출력하게 하는 옵션은 ACS_TIMER라고 따로 있었는데..
Windows XP에서 도입된 공용 컨트롤 6에 들어간 애니메이션 컨트롤은 스레드 기능이 완전히 삭제되고 언제나 타이머 기반으로만 동작하게 됐다.

뭐, 애니메이션을 출력할 만한 상황이라면 작업은 어차피 백그라운드 스레드에서 진행되고 있을 것이고, UI 스레드는 당연히 살아 있어야 한다. UI 스레드가 응답 없이 block돼 있는데 애니메이션 컨트롤이 별도의 스레드로 혼자 살아서 UI 쪽에 접근하면.. 응답을 못 받고 같이 멎어 버리는 deadlock에 빠질 것이다.
애니메이션 컨트롤은 성능과 안정성 같은 요인을 감안해서 멀티스레드 기능을 빼 버린 것으로 보인다.

(3) progress 컨트롤은 공용 컨트롤 6 시절부터 marquee 애니메이션을 출력하는 기능이 추가됐다. 즉, 전체 작업량을 예측할 수 없어서 기약 없이 기다려야 할 때.. 프로그램이 작업 중이고 뻗지 않았다는 사실만 알려주는 뱅글뱅글 애니메이션 말이다.
요것만으로도 별도의 애니메이션 컨트롤을 사용해야 할 필요를 많이 줄여 주긴 했다. 완전히 대체해 버린 건 당연히 아니지만 말이다.

Posted by 사무엘

2022/04/04 08:35 2022/04/04 08:35
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/2005

1. Dependency Walker

Dependency Walker라고.. Windows용 실행 파일에서 export 심벌과 import 심벌들을 재귀적으로 분석해서 모듈 간의 전체 의존 관계를 그래프 형태로 출력해 주는 굉장히 유용한 유틸리티가 있다. macOS나 리눅스 같은 타 OS에도 모듈 간 의존이라는 개념이 응당 있을 텐데, 타 OS용 실행 파일을 분석하는 프로그램도 좀 있었으면 좋겠다.

얘는 15년쯤 전, Windows Vista의 출시와 비슷한 시기에 마지막 버전이 나온 뒤부터는 원저자에 의한 개발과 유지 보수가 사실상 중단됐다. 뭐, 지금도 그럭저럭 쓸 만하긴 하지만 한 가지 문제가 있다.
Windows 7인지 8인지 10쯤부터는 모듈을 열어 보면 내부적으로 무한 루프에 빠져서 분석하는 데 시간이 굉장히 오래 걸리고 불필요한 정보가 너무 많이 걸려 나오는 경우가 있다.

사용자 삽입 이미지

자세한 속사정은 모르겠지만, 요즘 마소에서는 운영체제 API DLL들을 분야별로 최대한 잘게 쪼개고 있다. Windows 7에서는 kernel32.dll 하나가 제일 먼저 시범타로 쪼개졌었다. 가령 api-ms-win-core-heap, processthreads, memory, file 따위로 말이다.
그랬는데 요즘은 다른 dll들도 마찬가지이다. 레지스트리 API는 전통적으로 advapi32에 있었는데 그건 api-ms-win-core-registry로 가고, gdi32조차 ext-ms-win-gdi-draw, font, paint, path 등등으로 리모델링 됐다.

응용 프로그램들이야 과거와의 호환성을 위해 여전히 kernel32, gdi32 따위로 링크 되겠지만, 이 운영체제에 내장된 기본 프로그램들은 저런 잘게 쪼개진 dll을 직통으로 사용하는 형태로 빌드 된다.
쪼개진 dll들은 시스템 디렉터리에 있지도 않고, winsxs 아래로 도무지 정체를 알 수 없는 길고 복잡한 디렉터리 한구석에 처박히는데.. 딱히 매니페스트가 있지도 않아 보이구만 어떤 원리로 직통 연결되는지 나로서는 모르겠다.

내가 보아하니, Dependecy Walker가 어떤 PC에서는 이런 쪼개진 stub DLL을 모종의 이유로 인해 제대로 분석하지 못하는 것 같다. 거기서 loop cut을 못 하고 위의 스샷에서 표시된 바와 같이 무한 순환 오동작을 일으킨다.
차라리 그 파일을 찾지 못해서 넘어가는 것이면 다행인데, 이것도 100% 올바른 동작이 아닌 건 마찬가지이다.
이런 게 고쳐졌으면 하지만, 저 프로그램은 현재 버전업이 중단된 상태이다. 그렇기 때문에 모 오픈소스 진영에서는 Dependency Walker의 클론을 직접 만들고 있기도 하다.

참고로, 과거의 Windows 9x에서는 kernel32.dll이 원초적인 dll이었다. 즉, 심벌을 export만 하지, 자신은 실행 과정에서 다른 dll을 import 하는 것이 없었다.
그러나 오늘날의 Windows는 ntdll.dll이 원초적인 dll이다.

2. 32비트 프로그램에서 실행 중인 64비트 프로그램의 경로 얻기

GetModuleFileNameEx는 현재 컴퓨터에서 실행 중인 다른 프로세스, 혹은 거기 안에 같이 load된 dll의 전체 파일 경로를 얻어 오는 함수이다.
그런데 얘는 전통적으로 32비트 프로그램에서 64비트 프로그램을 대상으로 정보를 요청하는 건 잘 되지 않는 것으로 유명했다.

그냥 단칼에 실행이 실패하는 것도 아니고, 경로를 되돌리기는 하는데 온전한 형태가 아니라 뒷부분이 짤린 일부만을 되돌렸다. 그리고 에러 코드도 ERROR_PARTIAL_COPY라고 당당히 되돌렸다.
32비트 프로그램이 64비트 프로세스의 주소 공간에 접근하는 게 기술적으로 쉬운 일은 아니겠지만 그건 걔네들 사정일 뿐이다. 사용자 내지 프로그래머의 입장에서는 겨우 이런 간단한 정보 하나 온전히 얻으려고 IPC용 64비트 exe를 따로 만들어야 하나.. 멀쩡한 함수가 무용지물이니 우회 경로를 뚫느라 굉장한 불편을 겪을 수밖에 없었다.

그랬는데 오늘 우연히 이 함수를 호출해 보니 Windows 10의 20xx이후의 업데이트 버전에서는 이 문제가 해결된 것 같다. 32비트 프로그램에서 다른 32비트나 64비트 프로그램의 전체 경로를 얻는 것.. 반대로 64비트 프로그램에서 다른 32비트나 64비트 프로그램의 전체 경로를 얻는 것 모두 아무 문제 없다.
Windows 10 구버전이나 Windows 7, 8 같은 거 64비트 에디션이 있으면 같은 프로그램을 구동해서 결과를 확인하고 비교할 수 있겠다만.. 대조군을 구하지 못해서 그것까지 실험은 못 해 봤다.

옛날에는 도대체 무슨 한계 때문에 이 함수가 제대로 동작하지 않았는지, 그리고 지금은 무엇이 해결되었는지 이 함수와 관련된 사연을 좀 알고 싶다.
이 함수는 원래 psapi.dll에 있던 시스템 정보 조회용 부가 액세서리에 가까운 물건이었으나..
앞서 언급한 바와 같이 Windows 7 즈음부터 시작된 API 재배치 정리 작업 과정에서 kernel32의 세부 카테고리로 본진이 이동한 듯하다. 사실, GetModuleFileName이 있던 곳과 같은 곳에 있는 게 논리적으로 훨씬 더 타당하기도 하다.

이런 커널 API 말고 GDI 쪽에서도.. 옛날에 AlphaBlend처럼 Windows 98에서 처음 추가된 그러데이션 그리기 함수들은 msimg32.dll이라는 별도의 DLL에 들어가 있다가 Windows XP인지 Vista인지 그때쯤부터 gdi32로 자리를 옮긴 적이 있었다.
새로 추가된 함수가 이런 식으로 재분류되는 게 완전히 새로운 관행은 아니었던 셈이다.

3. 파일 대화상자의 동작과 current directory

Windows에서 제공하는 파일 열기/저장 공용 대화상자는 사용자가 선택한 파일이 있는 곳으로 프로그램의 current directory도 같이 바꿔 버린다.
그래서 어떤 프로그램에서 USB 메모리 안에 있는 파일을 열기 대화상자로 골라서 열고 나면, 그 파일을 닫은 뒤에도 계속해서 USB 메모리가 사용 중이라면서 안전하게 제거가 되지 않는 불상사가 벌어진다. 파일을 열었던 프로그램을 통째로 종료하거나, 열기 대화상자를 꺼내서 다른 드라이브에 있는 파일을 열면 문제를 해결할 수 있다.

사실, 아주 극단적으로 특이하게 동작하는 물건이 아닌 이상, GUI 프로그램은 자기가 작업하는 파일의 절대 경로를 갖고 있다. 상대 경로를 통해 다른 파일을 참조한다 하더라도 기준이 되는 절대 경로가 따로 있지, 프로그램의 current directory 정보에 의존할 일은 없다. 게다가 current directory는 스레드가 아니라 프로세스마다 하나씩만 보관되는 정보이기 때문에 thread-safe 하지도 않다.

그러니 파일 대화상자가 굳이 저렇게 동작할 필요가 전혀 없어 보이는데도 current directory를 변경하는 이유는.. (1) 레거시 프로그램과의 호환도 있고.. (2) 그리고 다음에 파일 대화상자를 또 열 때 사용자가 마지막으로 선택했던 파일이 있는 곳을 current directory라는 수단을 통해 기억하고 공유하기 위해서이지 싶다.

도스 같은 명령 프롬프트 환경에서는 사용자의 타이핑 수고를 덜기 위해서 current directory라는 개념이 반드시 필요했으며, 그때는 아예 각 드라이브별로 current directory를 다 기억하고 있었을 정도였다. 지금 Windows 환경은 그 정도까지는 아니다.
그리고 어떤 프로그램은 불러들이는 문서 파일이 있는 디렉터리와 current directory가 동일하다는 게 보장돼야만 제대로 동작하는가 보다.

하지만 파일 대화상자도 OFN_NOCHANGEDIR라는 플래그가 있어서 사용자가 어느 파일을 선택하건 current directory를 건드리지 않게 하는 옵션 자체는 있다.
그리고 내부 동작도 바뀌어서 굳이 current directory에 의존하지 않고 자체적으로 사용자가 마지막으로 파일을 선택했던 위치를 기억해서 보여준다.

그러니 오늘날 새로 개발되는 프로그램들은 파일 대화상자를 꺼낼 때 가능한 한 OFN_NOCHANGEDIR를 사용하는 게 좋을 것 같다.
또한, 이런 조치와는 별개로 current directory 때문에 USB 메모리가 안전하게 제거되지 않는 문제를 운영체제 차원에서도 좀 최소화해야 하지 않나 싶다.

이건 모니터를 2~3대 연결해서 컴퓨터를 잘 쓰다가 일부 모니터의 선을 뽑아 버린 것과 비슷한 상황이다. 이 경우, 운영체제에서는 없어진 모니터의 영역에 있던 프로그램 창들을 재주껏 다른 모니터로 잘 옮겨 줘야 한다. 그런 것처럼 USB 메모리가 뽑혔다면, 거기를 current directory로 참조하던 프로그램은 다른 디렉터리를 참조하도록 상태가 적절히 바뀌어야 할 것이다.

4. 그래픽 뷰어

끝으로, 이건 프로그래밍과 큰 관계 없이 특정 앱에만 해당되는 사항인데..
요즘 Windows 10에 기본 내장돼 있는 그래픽 뷰어 말이다. 오랫동안 사용해 본 내 경험에 따르면, 얘는 좀 불안정한 것 같다. 창을 여러 개 띄워 놓다 보면(5개 이상 여러 파일)..

  • 종종 뻗으면서 지금까지 띄웠던 창들이 한꺼번에 싹 없어진다.
  • 혼자 CPU를 잔뜩 소모하면서 노트북 PC를 열받게 하기도  한다.
  • 사진 파일을 더블 클릭했는데 프로그램이 실행만 되고 창이 뜨지 않고 먹통이 되기도 한다. "파일 시스템 오류 (-805305975)" 이러면서 아예 실행이 안 된다.

2004/2009대 버전으로 개인용 컴과 회사 컴에서 모두 동일한 현상이 발생하니, 이건 내 환경만이 문제가 아닌 것으로 보인다.
한 가지 확실한 건 얘는 화면 표시에 그래픽 카드의 하드웨어 가속 기능을 적극 활용한다는 것이다. 뻗는 것도 여느 프로그램들 같은 메모리 버그 때문에 뻗는 게 아니며, 그래픽 카드 드라이버와의 충돌 내지 그쪽의 오류 때문에 뻗는다.

내 기억이 맞다면 Windows XP~7 사이의 기본 그래픽 뷰어는 256색 GIF에 대해서는 트루컬러 JPG와 달리 부드러운 확대/축소를 지원하지 않는다거나, GIF/APNG 애니메이션을 지원하지 않는다거나 하는 한계가 있었다.

지금 뷰어는 그런 한계가 다 없어지고 한 프로그램에서 사진과 동영상을 모두 취급할 수 있어서 기능 면에서는 제일 좋아졌다. 하지만 반대로 구버전에 비해서 안정성은 명백하게 떨어져 있는 게 아쉽다. 특히 앱이 실행되지 않기 시작하면 운영체제를 재시작/재로그인 하는 것 말고는 다른 해결책이 없다.

5. CPU 점유 문제

요즘 누구든지 컴퓨터나 폰을 다루면서 겪는 굉장히 성가신 문제 중 하나는.. 어떤 불필요한 프로세스/서비스가 나도 모르는 사이에 CPU 자원을 독식해서 기기가 갑자기 혼자 열받고 팬 돌아가고 배터리가 눈에 띄게 빨리 닳기 시작하는 것이다. 그러고 보니 스마트폰은 팬이 없구나~!

개인적으로는 이럴 때 CPU 도둑을 감지해서 “이놈이 지금 폭주 중인데 죽일까요?” 안내를 해 주는 유틸/툴이 있어야 한다고 생각한다.
도스 시절로 치면 memmaker, 윈도 XP 시절에 잠깐 있었던 바탕화면 정리 마법사, 어지간한 악성코드 진단 유틸 같은 명목으로 말이다.

물론 어지간한 컴잘알이라면 이럴 때 작업 관리자를 띄워서 CPU 사용량이 높은 놈을 강제 종료시킬 것이다. 하지만 범인이 평범한 프로그램이 아니라 서비스 같은 부류라면 뭐가 문제인지를 진단하기 어렵다.

본인의 과거 경험을 떠올려 보면 Windows Update와 관련된 서비스가 폭주한 적도 있었고, 크롬 브라우저가 쓸데없이 폭주한 적도 있었고.. 요 근래에는 WMI provider host인지 뭔지 하는 놈도 폭주해서 강제 종료시킨 적이 있었다.
자고로 업데이트는 CPU를 최하 우선순위로 받으면서 민폐를 절대 끼치지 않고 몰래 몰래 돌아가야 할 것이다. 사용자가 명시적으로 시키지 않은 작업이 저 따위로 돌아가는 건 어떤 경우든 디자인 결함이고 버그이지 않을까?

6. 프로그램 창의 떨림 현상

끝으로, 이건 프로그램이 뻗는 급의 치명적인 문제나 불편한 현상은 아니지만..
요즘 컴퓨터를 쓰면서 프로그램 창을 전환하다 보면, 아주 가끔씩 프로그램들이 non-client 영역(제목 표시줄, 메뉴 따위)이 수십 번 다시 그려지는지.. 수 초 동안 부르르 깜빡거리고 떨리는 경우가 있다.

이 역시 2004/2009급 Windows 10이 깔린 개인용과 회사용 컴퓨터에서 모두 목격하는 현상이다. 201x년대에는 딱히 본 적이 없었던 새로운 현상이다.
정확하게 어떤 조건 하에서 왜 발생하는지는 모르겠다.
내가 모르는 사이에 업데이트가 깔리면서 운영체제의 아랫단은 알게 모르게 많이 바뀌는데, 버그나 부작용도 슬그머니 들어갔다가 또 잠수함 패치되기도 하는 것 같다.

Posted by 사무엘

2022/01/23 08:34 2022/01/23 08:34
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1978

Windows API에서 LoadLibrary는 말 그대로 실행 파일(exe/dll)을 현재 프로세스의 주소 공간에다 불러들여서 거기 있는 코드를 실행하거나 리소스를 추출하게 해 주는 함수이다.
그리고 얘의 심화 버전은 LoadLibraryEx이다. Ex 버전은 옵션을 추가로 받아서 절대 경로 없이 파일명만 주어졌을 때 디렉터리를 탐색하는 순서를 지정할 수 있고, 파일이 이미 load되어 있을 때 레퍼런스 카운트 변경 여부 같은 것도 수동 지정할 수 있다.

하지만 그런 옵션들은 현업에서 잘 쓰이지 않는다. 저 함수에서 실질적으로 자주 쓰이는 옵션은.. DLL에서 리소스를 추출할 준비만 하고, 코드를 실행할 준비--기준 주소 재배치, DllMain 함수 실행--는 생략해서 로딩 속도를 좀 더 향상시키는 LOAD_LIBRARY_AS_DATAFILE이다. 특히 x86, x64, ARM 같은 아키텍처를 불문하고 동일 DLL에 있는 리소스 데이터를 추출하려면 이 '간소화' 플래그를 반드시 지정해야 한다(다국어 UI 리소스 같은..).

그런데 문제는.. 이 DATAFILE 간소화 로딩이란 게, 과거에는 "리소스 추출에만 특화"이라는 자기 본연의 기능에도 모종의 이유로 인해 뭔가 2% 부족한 구석이 있었다는 것이다.

Windows 9x 시절에는 이 제약이 제일 심했다. 간소화 로딩된 DLL 핸들에 대해서는 (1) 리소스를 제일 저수준에서 탐색하는 EnumResourceLanguages/Names/Times 및 Enum/Find/LoadResource 계열 함수만 사용할 수 있었다. 이들보다 상위 계층에서 동작하는 Load*계열 함수들은(string, menu, bitmap, image 따위) 지원되지 않았다. 그러니 간소화 로딩의 활용성이 부족했으며, 여전히 기존 full(?) 방식 로딩을 해야 하는 경우도 있었다.

허나, 한편으로는 저 제약이 그렇게까지 본질적이고 치명적인 문제가 아니었다.
Windows 프로그램에서 리소스 전용 DLL을 사용하는 주 목적은 다국어 UI 제공.. 아니면 대화상자· 메뉴 같은 표준 리소스가 아니라 자기 자신만 사용하는 custom 데이터의 저장이기 때문이다.

그리고 표준 리소스들도 특정 언어에 속하는 놈을 지정하려면 "DLL 핸들 + 리소스 ID"만으로는 어차피 충분치 않다. FindResourceEx와 LoadResource의 결과값인 메모리 포인터를 줘야 하며, 함수도 LoadMenuIndirect, DialogBoxIndirect처럼 뒤에 indirect라는 단어가 붙은 '저수준 버전'을 써야 한다.

그렇기 때문에 리소스 추출용 간소화 방식으로 load한 DLL은 저수준 함수로만 다룰 수 있더라도 그럭저럭 사용할 만했다. 그런데 여기에는 다른 이상한, 자잘한 문제도 있었다.

DialogBoxIndirect 함수는 대화상자 리소스를 "모듈(인스턴스) 핸들 + 리소스 ID"가 아니라 대화상자 템플릿 포인터 하나로만 곧장 지정함에도 불구하고, 모듈 핸들을 여전히 인자로 받는다. 내부적으로 CreateWindowEx 함수를 호출할 때 모듈 핸들이 필요하기 때문이다(대화상자 자신, 그리고 내부의 child 컨트롤들 생성).

그런데 (2) 이때 리소스 추출 간소화 방식으로 load한 DLL의 핸들을 주면.. 구형 운영체제에서는 여러 문제들이 발생했다.
일단, 자기 자신이 내부적으로 사용하는 커스텀 컨트롤--표준 컨트롤이 아니고, CS_GLOBALCLASS 등록된 커스텀 컨트롤도 아닌 놈--이 만들어지지 않는다. 이건 CreateWindowEx 함수의 특성상 자연스러운 귀결이지만, 그 이상으로..

내 기억이 맞다면 대화상자의 배경색이 일반적인 회색이 아니라 흰색으로 바뀌고 좀 만지다 보면 프로그램이 뻗었다. Windows 9x뿐만 아니라 나름 NT 계열인 2000에서도 말이다.
그 이유는 딱히 알 수 없었다. 그저 경험적으로 이런 DLL 핸들을 집어넣어서는 안 된다고 날개셋 한글 입력기 소스의 주석에도 엄청 옛날에 적혀 있었다.

물론 이 역시 본질적이고 치명적인 문제는 아니다.
윈도우의 생성과 관련해서 전달하는 인스턴스/모듈 핸들은 그 윈도우의 클래스를 등록한 주체를 식별하는 용도이다. 애초부터 리소스가 전혀 아니라 코드와 관계가 있다. 그러니 여기는 애초에 리소스 추출 간소화 방식으로 load된 DLL이 들어갈 자리가 아니다. 그런 DLL을 집어넣은 것은 사실상 프로그래머의 실수에 지나지 않는다.

하지만 이쯤 되니 의문이 생긴다. 프로그래머가 아무리 실수할 수 있기로서니, 그걸 넘겨주면 단순히 custom 컨트롤이 생성되지 않는 것 이상으로 왜 다른 이상한 부작용까지 발생한 것일까? 차라리 깔끔하게 에러와 실패 처리를 하는 것도 아니고 말이다.
DLL을 일반적인 방식으로 load하는 것과 datafile(리소스 특화 간소화) 방식으로 load하는 것은 내부적으로 무슨 차이가 있는 걸까?

오늘날의 32비트 및 64비트 Windows 환경에서는 DLL을 로딩한 결과 핸들(HMODULE / HINSTANCE)은 그 파일 내용을 가리키는 데이터 포인터와 거의 동급이라고 여겨진다. 파일을 memory-mapped file 형태로 통째로, 혹은 약간의 보정만 거쳐서 읽어들인 첫 지점이다. 쉽게 말해 그 핸들이 가리키는 메모리에는 EXE 파일 시그니처인 MZ부터 쭉 나온다는 것이다.

그리고 실행 파일은 메모리 주소가 언제나 64KB의 배수 단위로만 배당된다는 것도 이 바닥에서 프로그래밍 좀 한 사람들은 아실 것이다. 그 말인즉슨, 일반적으로 HMODULE 내지 HINSTANCE의 값은 64KB의 배수이며, 하위 word가 언제나 0이 된다는 뜻이다.
하지만 특수한 상황에서는 이런 조건을 만족하지 않는 핸들도 있을 수 있다.

(1) 먼저, 과거의 Windows 9x 환경에서는 16비트 프로그램에서 호출한 LoadLibrary의 리턴값이 대표적인 예이다. 얘들은 핸들의 크기 자체가 16비트밖에 안 되니 리턴값과 내부 의미 역시 32비트 프로그램과는 완전히 다른 형태여야 한다.
물론 이미 32비트 형태로 빌드된 프로그램이야 이런 거 신경 쓸 필요가 전혀 없으며, 16비트와 32비트 프로그램을 모두 한데 관리하는 운영체제의 관점에서나 구분이 필요하다.

(2) 그리고 LoadLibraryEx + datafile 방식으로 불러들인 dll 핸들도 형태가 약간 달라진다. 운영체제의 버전에 따라 차이가 있지만 일단 해당 DLL의 preferred base는 완전히 무시되며, 굳이 64KB라는 큼직한 단위로 주소가 배당되지 않는다.
결정적으로는 최하위 비트에 1이 추가돼서(= 홀수!!) 얘는 datafile 방식으로 생성되었다는 것을 나타낸다. 메모리 주소로서의 DLL 핸들은 하위 16비트에 어차피 유의미한 정보가 담겨 있지 않으니.. 그 잉여 공간에다 이런 정보를 보관한다는 뜻이다.

요컨대 HMODULE / HINSTANCE는 16비트 프로그램 또는 datafile 방식에 한해서는 64KB의 배수 단위인 깔끔한 포인터가 아니게 된다. 그런데 과거에는 운영체제 내부에서 이런 변칙적인 핸들을 취급하는 방식이 서로 충돌했던가 보다.

kernel32는 이 DLL이 datafile 방식으로 load되었다는 것을 식별하기 위해서 핸들값에다가 1을 추가했다. 하지만 user32의 대화상자 표시 함수는 datafile 방식에 대해서는 전혀 관심이 없으며, 이 핸들값이 하위 16비트가 비영인 것을 보고는 이건 16비트 모듈이라고 인식해 버렸다. 그리고 16비트 프로그램과의 하위 호환을 위한 보정 처리를 수행했다.

그 보정 처리 중에는 대화상자 내부의 각 에디트 컨트롤들에 대해 고유한 데이터 세그먼트를 생성하는 것도 있었다.
아시다시피 에디트 컨트롤, 특히 multiline으로 동작하는 놈은 혼자서 수백, 수만 바이트에 달하는 텍스트를 저장할 수 있다. 모든 컨트롤들이 한 64KB 데이터 세그먼트를 공유할 게 아니라 각각이 고유한 세그먼트를 갖는 게 낫다. 이것을 대화상자 표시 함수가 내부적으로 해 줬다.

(그럼 이건 특별히 메모리가 많이 필요한 에디트 컨트롤에 대해서 고유한 스타일을 줘서 그 컨트롤이 알아서 처리하면 되지, 이런 걸 왜, 어떻게 상위 윈도우인 대화상자에서 처리하는지는 잘 모르겠다. 그리고 그런 식이면 에디트 컨트롤뿐만 아니라 리스트나 콤보박스도 수천 개의 아이템을 추가하느라 메모리가 많이 필요할 때가 있을 텐데 걔네들은 어떻게 처리되는지도.. 개인적으로는 잘 모르겠다. ㄲㄲ)

어쨌든.. 대화상자를 생성할 때 datafile DLL의 핸들이 지정되면 저런 복잡한 이유로 인해 16비트 보정이 수행되는데.. 실제로 대화상자를 돌리는 이 프로그램은 16비트 프로그램이 아니다. 그래서 보정 처리가 제대로 되지 않고 프로그램이 죽는 등 갖가지 오동작과 이상 현상이 발생한다고 한다. 그래서 그랬던 것이군~!! (☞ 더 자세한 설명)

대화상자에도 스타일이 있다. 하지만 이건 윈도우 스타일의 형태로 지정해 주는 게 아니고 DialogBox 계열 함수에다가 인자로 전하는 것도 아니며, 그냥 대화상자 리소스 템플릿에 박혀 들어가는 값일 뿐이다. 그러니 다른 스타일 플래그들에 비해 인지도가 매우 낮으며 프로그램 코드에서 볼 일이 없다시피하다.

이 대화상자가 다른 대화상자의 child로 들어갈 수 있음을 나타내는 DS_CONTROL, 용도가 좀 모호하긴 하지만 [?] 모양의 도움말 버튼을 우측 상단에다 표시하는 DS_CONTEXTHELP 같은 건.. 오늘날까지도 유효하다. 하지만 16비트 시절의 잔재이고 오늘날은 아무 의미 없는 플래그도 있다.

대표적으로 DS_3DLOOK은.. Windows 95/NT4부터는 대화상자들이 처음부터 기본적으로 버튼과 동일한 은색/회색이고 각종 테두리도 양각 음각 입체(?) 효과가 적용되어 나오므로 존재의 의미가 없어졌다.
그리고 DS_LOCALEDIT라는 놈이 있는데.. 얘는 자기 내부의 모든 에디트 컨트롤들이 고유한 데이터 세그먼트가 아니라 기본 제공되는 단일 64K 세그먼트를 공유하게 해서 메모리를 아끼는 플래그이다. 에디트 컨트롤에 많아야 수십~수백 자밖에 들어가지 않는다는 게 보장되면 사용해 볼 만한 옵션이었다. 32비트 이후부터는 아무런 의미가 없어졌지만..

그리고 이렇게 DS_LOCALEDIT 옵션이 적용된 대화상자는 아까처럼 Windows 9x에서 datafile DLL 핸들을 지정해 주더라도 16비트 보정 처리가 행해지지 않기 때문에 오동작· 오류도 발생하지 않았다고 한다.
물론 이 문제는 Windows NT 계열을 넘어 16비트 프로그램 자체가 존재하지 않는 64비트 운영체제의 관점에서는 더욱 무의미한 지나간 옛날 추억이 되었을 뿐이다.

16비트에서 32비트로 넘어갈 때는 16비트 환경에서도 far이니 huge니 하면서 어떻게든 16비트 코드에서 64KB를 초과하는 메모리 영역을 다루려고 애썼으며, 반대로 32비트 주소 공간에서 16비트 코드를 수용하고 실행하려고 온갖 발악을 했었다. 하지만 32비트와 64비트는 서로 완벽하게 격리된 채 공존할 뿐, 상대방 영역을 전혀 건드리지 않는다는 차이가 있다.

이상이다.
여담이지만 날개셋 한글 입력기의 소스를 뒤져 보니.. 어떤 DLL을 datafile 방식으로 읽어들인 상태에서는 그 DLL에 대해서 VerQueryValue 같은 버전 정보 확인 API도 제대로 동작하지 않았다는 주석이 적혀 있다. 그래서 버전 리소스를 수동으로 직접 파싱하는 방식으로 기능을 구현했다.
Windows Vista 이상 또는 심지어 9x 계열에서도 괜찮았으며 2000/XP에서만 문제가 발생했다고 하는데.. 이 역시 LoadLibraryEx 함수의 부작용이 아니었나 추측해 본다. 과거에 일반 로딩과 datafile 특화 로딩은 내부 동작이 여러 모로 차이가 컸던 모양이다.

Posted by 사무엘

2021/10/15 08:34 2021/10/15 08:34
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1943

Windows에서 리스트뷰 컨트롤은 아이템 기반의 정보를 나열하는 용도로 굉장히 편리하고 유용한 물건이다. 본인은 수 년 전에 얘에 대해서 전문적으로 리뷰를 한 적도 있다. (☞ 이전 글 보기)
하지만 그럼에도 불구하고 얘에 대해서 본인은 Windows 프로그래머로서 다음과 같은 점을 아쉽거나 의아하게 생각한다.

(1) 먼저, ‘작은 아이콘’ 모드라는 게 정체성이 너무 어정쩡 모호한 건 둘째치고라도, 아이템 배치와 관련된 제어가 제대로 안 된다는 것이다.
얘는 작은 아이콘의 옆에 텍스트가 한 줄 붙는다는 점에서는 목록 모드와 매우 비슷하다. 하지만 얘는 그래도 횡대와 종대 아무 방향으로나 align이 되고, group도 적용된다는 것이 목록 모드와의 차이점이다. 즉, 나름의 쓸모도 있다는 것이다.

그런데.. 얘는 아이템의 간격 내지 폭을 어떻게 조절해야 할지 모르겠다.
LVM_SETICONSPACING은 큰 아이콘 모드의 간격이고 LVM_SETCOLUMNWIDTH는 목록 모드의 폭이고, LVM_SETTILEVIEWINFO는 타일 모드에서의 크기이다.
그런데 작은 아이콘 모드는 내가 아는 한 아무리 눈을 씻고 찾아 봐도 이런 API가 존재하지 않는다.

사용자 삽입 이미지

아이템이 이렇게 제멋대로 막장으로 출력되는 걸 막을 길이 없더라.
내가 날개셋 제어판의 외부 모듈 목록에다가 ‘작은 아이콘’ 모드도 추가해 볼까 하다가 이걸 보고는 단념했다.

(2) 그리고 악명 높은 화면 잔상 버그 말이다.
check list를 건드렸을 때 선택 막대가 진해지는 문제는 이미 이전 글에서도 지적했던 바 있다.
스타일을 바꿔서 우회하는 방법도 있긴 하지만 완전한 해결책이 아니며, 원래는 그렇게 우회하지 않더라도 그런 문제가 발생하지 않아야 한다. Windows 10 이전에는 문제가 없었기 때문이다.

사용자 삽입 이미지
(자세히 모드에서 칼럼의 폭을 조절해도 이렇게 해당 칼럼이 덧칠되는 문제가..)

게다가 ‘작은 아이콘’ 모드일 때는 아이템에다가 마우스를 가져가기만 해도 글자가 사라져 버리는 치명적인 문제가 존재한다.
이건 뭔가 내부 계산 로직이 대놓고 잘못된 거나 마찬가지이다. 마소에서도 자체적으로 작은 아이콘 모드를 쓰지를 않기 때문에 버그를 인지하지 못하는 게 아닌가 하는 생각이 들 지경이다.

사용자 삽입 이미지

Windows는 내 컴퓨터 내지 탐색기 UI에서 전통적으로 리스트뷰 컨트롤을 사용해 왔다.
초창기 Windows 95/98은 이 컨트롤이 제공하는 ‘큰 아이콘, 작은 아이콘, 목록, 자세히’라는 네 가지 보기 모드를 그대로 제공했었다.
그러다가 2000/ME에서는 그림이나 문서의 내용 thumbnail이 표시되는 미리 보기 모드가 추가되어 5개가 되었다. 이건 기술적으로는 물론 ‘큰 아이콘’의 확장판이었다.

XP에서는 정체성이 어정쩡한 작은 아이콘 모드가 삭제되고, 제5의 모드인 ‘타일 모드’가 ‘큰 아이콘’이라는 이름으로 등장했다. 기존의 ‘큰 아이콘’은 그냥 ‘아이콘’으로 바뀌었고.. 그러니 전체 개수는 5개로 변함없었다. 원래 있던 작은 아이콘 모드는 마소에서도 완전히 버린 자식 취급하고 있는 게 틀림없다.

사용자 삽입 이미지

그 다음 Vista/7 이후부터는 아이콘의 크기를 매우 다양하게 조절할 수 있게 되었다. 게다가 개념적으로 예전의 ‘작은 아이콘’에 해당하는 모드도 부활했다.
하지만 이건 리스트뷰 컨트롤이 제공하는 ‘작은 아이콘’으로 구현한 게 아니다. 내부적으로는 그냥 ‘타일 모드’에다가 아이콘만 작은 걸 준 게 아닐까..?? 작은 아이콘에서는 이렇게 길다란 아이템을 뒷부분을 생략해서 표시하는 게 가능하지 않다. 타일에서만 가능하다.

사용자 삽입 이미지

그리고 굉장한 뒷북인 놀라운 사실이 있다.
무려 Windows 7부터는 운영체제의 탐색기와 파일 공용 대화상자에서 애초에 리스트뷰 공용 컨트롤을 사용하지 않고 있다. 일반인에게 스펙이 공개되지 않은 별개의 자체 구현 컨트롤을 쓴다..!!

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

아.. 어쩐지~~
(1) 원래 리스트뷰 컨트롤의 '목록' 모드는 이렇게 카테고리라고 해야 하나 그룹 분류 기능을 지원하지 않는다. 목록 모드라는 것은 스크롤바가 세로가 아니라 가로로 난 것을 통해 알 수 있음..
탐색기의 저런 모양은 공용 컨트롤로는 구현 불가능하다. 또한, 저렇게 칼럼마다 폭이 유동적으로 다른 목록도 공용 컨트롤은 지원하지 않는다.

사용자 삽입 이미지

(2) 아울러, '내용'이라는 이 특이한 보기 모드도 기존 리스트뷰 컨트롤로는 구현 불가능하다.
이런 걸 어떻게 구현했을지, 온통 owner draw 개조로 도배했을지 궁금했는데.. 답은 간단했구나. 그냥 자체 컨트롤을 만든 것이었다.;; 영문 위키백과에서도 Windows 7에서부터 바뀌거나 사라진 기능 중 하나로 다음과 같이 언급되어 있다.

An undocumented incompatible Item view control replaces the List view control used in Windows Explorer... The Item view also does not support custom positioning, custom ordering, or hyperlinks, which the Windows Vista list view did support.


아.. 옛날엔 Office 팀에서 파일 열기/저장 대화상자들 자체 제작해서 썼더니만, 그건 없어졌고 이젠 운영체제 셸 팀에서 뷰 컨트롤을 자체 제작해서 쓰기 시작했군...

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

Windows Vista와 7의 탐색기는 외형상 굉장히 비슷해 보인다.
그런데 "Hard Disk Drives / Devices with Removable Storage"라고 아이템을 분류해 놓은 카테고리 부분을 보자.
아이템들을 몽땅 숨기거나 펼칠 수 있는 삼각형 마커가 Vista는 오른쪽 끝에 있는 반면, 7과 그 이후(현재의 10까지 포함)부터는 왼쪽 끝에 있다. 왼쪽 끝에 있으니 무슨 트리 컨트롤을 다루는 것 같은 느낌이 든다.

(3) 마커가 왼쪽에 있을 뿐만 아니라 일반 아이템보다 글자 크기도 더 큰 건.. 내가 알기로 공용 컨트롤 리스트의 기본 옵션만 바꿔서 구현할 수 있지 않다. 비슷해 보여도 Vista는 리스트뷰 컨트롤을 썼지만 7부터는 그렇지 않다는 것이다.

트리와 리스트뷰 컨트롤을 적당히 배합해서 '탐색기' 짝퉁을 만드는 게 Windows UI/셸 프로그램의 정석 코스로 통용되곤 했는데.. 이젠 100% 동일하게 동작하는 탐색기를 이런 식으로 만드는 건 근본적으로 불가능해졌다.

단, 바탕 화면은 그냥 아이콘들만 늘어놓으면 되니 지금도 여전히 기존 리스트뷰 컨트롤 기반이다.
결론을 내리자면.. 마소에서는 자기들도 리스트뷰 컨트롤의 한계를 인지했는지 탐색기에서 얘를 더는 사용하고 있지 않다. 작은 아이콘 모드의 활용성을 좀 강화하고, 목록 모드에서도 그룹 구분을 지원하고, 화면 잔상 버그들을 좀 고쳤으면 좋겠다.

Posted by 사무엘

2021/10/06 08:36 2021/10/06 08:36
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1940

1. 아이콘 불러오기

창(그 자체 또는 클래스)에다가 아이콘을 지정하기 위해 흔히 LoadIcon 함수가 쓰인다.
얘는 원래 고정된 32*32 크기의 기본 아이콘 하나만을 달랑 가져오는 함수로 출발했다. 허나 Windows 95부터는 글자 크기와 같은 16*16 작은 아이콘이라는 것도 추가됐고, 나중에 XP쯤부터는 24*24, 48*48 같은 다양한 중간 크기가 도입됐다.

거기에다 화면 DPI까지 가변화가 가능하지, 256픽셀 대형 아이콘까지 도입됐지.. 이거 뭐 아이콘이라는 건 이제 도저히 단일 크기 이미지라고 볼 수 없는 물건으로 바뀌었다. 한 아이콘이 다양한 크기와 색상 버전을 가질 수 있다는 점에서 과거의 비트맵 글꼴과 약간 비슷한 위상이 됐다.

한편, 원래 마우스 포인터(cursor)와 아이콘은 기술적인 원천과 본질이 거의 같은 물건이었다. 작은 정사각형 크기의 이미지 비트맵과 마스크 비트맵의 쌍으로 표현된다는 점에서 말이다. 마우스 포인터는 거기에다가 hot spot 위치 정보가 추가됐을 뿐이었다.
그랬는데 마우스 포인터는 애니메이션이라는 바리에이션이 생겼고, 아이콘은 크기 바리에이션이 생겼다고 보면 되겠다. 동일한 특성을 같이 공유하다가 서로 다른 방향으로 기능이 추가된 것이다.

Windows 95에서는 창이나 창 클래스에다가 아이콘을 지정할 때 큰 아이콘과 작은 아이콘을 구분해서 지정할 수 있게 했다. 그래서 WNDCLASS에는 멤버가 하나 더 추가된 Ex버전이 만들어졌다. WM_SETICON 메시지도 아이콘의 대소 종류를 지정하는 부분이 wParam에 추가됐다.

그리고 LoadIcon 함수 자체도.. Ex가 추가된 건 아니고, 비트맵, 아이콘, 포인터까지 다양한 크기를 모두 처리할 수 있는 완벽한 상위 호환 LoadImage에 흡수되었다. 스펙을 보면 알겠지만 기능이 정말 많다.

하지만 내 경험상, 굳이 Ex 버전을 쓰지 않고 WNDCLASS의 hIcon에다가 큰 아이콘만 LoadIcon으로 지정해 주더라도.. 동일한 ID의 아이콘에 큰 아이콘과 작은 아이콘이 모두 있다면 별도의 처리가 없어도 괜찮았다. 프로그램 타이틀 창에 작은 아이콘은 그 별도의 작은 아이콘으로 자동으로 지정되는 듯하다. 큰 아이콘을 흐리멍텅하게 resize한 놈이 지정되는 게 아니라는 뜻이다.

그래서 본인은 지금까지 프로그램을 개발하면서 굳이 WNDCLASSEX와 RegisterClassEx를 사용한 적이 없었다. 큰 아이콘과 작은 아이콘이 ID까지 다른 서로 완전히 다른 아이콘일 때에나 이런 전용 함수가 필요한 듯하다.
단, 윈도우 클래스를 등록하는 상황이 아니라 대화상자 같은 데서 WM_SETICON으로 아이콘을 지정할 때는 큰 아이콘과 작은 아이콘을 LoadImage 함수로 구분해서 일일이 지정해 줘야 했다.

참고로 Windows에서 아이콘이라는 건 메모리 관리 형태가 크게 세 종류로 나뉜다. (1) 메시지박스에서 흔히 볼 수 있는 ! ? i 표지처럼 시스템 공통 공유 아이콘, (2) 응용 프로그램의 아이콘 리소스를 직통으로 가리키기만 하는 공유 아이콘, (3) 그게 아니라 자체 메모리를 할당하여 동적으로 독자적으로 생성된 놈.

(3)만이 나중에 DestroyIcon을 호출해서 제거해 줘야 한다. (2)는 해당 모듈의 생존 주기와 동일하게 관리된다. (1)이야 뭐 언제 어디서나 유비쿼터스이고..
그리고 RegisterClass 계열 함수가 특례를 보장해 주는 건 역시 리소스 기반인 (2) 한정이다.
wndClass.hIcon = LoadIcon(hInst, IDI_MYICON) 이렇게 돼 있던 곳에서 LoadIcon(...)의 결과를 CopyIcon( LoadIcon(...))으로 감싸서 아이콘의 형태를 (3)으로 바꿔 보시라. 그러면 그 프로그램의 제목 표시줄에 표시된 작은 아이콘은 큰 아이콘을 resize한 뭉개진 모양으로 곧장 바뀔 것이다. 이것이 차이점이다.

사실, Visual Studio의 리소스 에디터 상으로는 구분이 잘 되지 않지만, 응용 프로그램 모듈(EXE/DLL)에 저장되는 리소스 차원에서는 단순 아이콘(RT_ICON)과 아이콘 집합(RT_GROUP_ICON)이 서로 구분되어 있다. 후자는 전자의 상위 호환이다. RegisterClass는 이를 감안해서 동작하지만 HICON 자료형이나 LoadIcon 같은 타 함수들은 일반적으로 그렇지 않은 것으로 보인다.

이럴 거면 wndClass.hbrBackground에 (HBRUSH)(COLOR_WINDOW+1)이 있는 것처럼 hIcon에도 (HICON)IDI_MYICON 이런 게 허용되는 게 더 깔끔하겠다는 생각도 든다.

자, 이 정도면 아이콘 지정에 대해서 더 다룰 게 없어야 하겠지만.. 그렇지 않다. LoadImage 함수에 약간의 버그가 있다.
얘는 (1) 시스템 공용 아이콘에 대해서는 요청한 크기에 맞는 버전을 되돌리지 않고 가장 큰 놈 또는, 걔네들 용어로는 캐시에 보관돼 있는 크기의 이미지만을 되돌린다. 즉, 기존 LoadIcon과 다를 바 없이 동작한다.

특정 크기에 해당하는 아이콘을 정확하게 되돌리라고 별도의 함수까지 만들었는데 그건 (2), (3) 계층에 해당하는 custom 아이콘에 대해서만 동작한다. (1)에 대해서는 글쎄, 성능 때문인지 호환성 때문인지 잘못된 동작을 일부러 방치해 버리고는 더 고치지 않는 듯하다.

그렇기 때문에 시스템 공용 아이콘의 16픽셀급 작은 버전을 이 함수로 얻을 수 없다.
Windows Vista부터는 사용자 계정 컨트롤이라는 보안 기능이 추가되어서 관리자 권한을 나타내는 방패 아이콘(IDI_SHIELD)이 추가되었다. 얘도 UI 텍스트와 함께 작은 크기로 그려야 할 텐데.. LoadImage로는 256픽셀짜리 대형 아이콘만 얻을 수 있기 때문에 이걸 16픽셀로 줄여서 그리면 보기가 흉하다.

마소에서는 LoadImage 함수의 버그를 고친 게 아니라 Vista부터 LoadIconMetric이라는 함수를 추가했다.
얘를 사용하면 시스템 공용 아이콘에 대해서도 정확한 크기를 얻을 수 있다.
얘는 아이콘을 언제나 (3)번 형태로 동적 할당해서 되돌리기 때문에 다 사용하고 나서는 DestroyIcon을 해 줘야 한다. 처리하기 간편한 shared, read-only 속성을 포기하고 정확한 동작을 하도록 로직을 바꾼 것 같다.

그 외에 SHGetStockIconInfo라는 함수도 있어서 얘를 사용하면 한 마디로 탐색기에서 쓰이는 각종 디스크 드라이브, 폴더, 돋보기, 네트워크 등의 표준 셸 아이콘을 얻을 수 있다.

2. DrawFocusRect

Windows에서 대화상자를 키보드로 조작하다 보면, 현재 포커스를 받아 있는 각종 버튼(라디오/체크 박스 포함)이라든가 리스트 아이템에 가느다란 점선 테두리가 쳐진 것을 볼 수 있다. 이것은 DrawFocusRect라는 함수를 이용해서 그려진 것이다.

마소에서는 키보드 포커스를 받아 있는 GUI 구성요소에다가는 요 함수를 호출해서 점선으로 테두리를 그려 줄 것을 GUI 디자인 표준으로 명시하고 있다. 뭐, 일반 프로그래머라면 버튼 같은 커스텀 컨트롤을 직접 구현하거나 owner-draw 리스트박스를 만들 때에나 숙지할 만한 개념이다. 다른 요소들을 다 그리고 나서 맨 마지막으로 focus 테두리를 그려 주면 된다.
다만, 에디트 컨트롤은 애초에 깜빡이는 캐럿(caret; cursor)이 포커스에 대한 시각 피드백 역할을 하고 있기 때문에 또 점선 테두리를 그려 줄 필요가 없다.

이 점선은 이미 아시겠지만 xor 연산을 가미한 반전색이다. 원래 색과 반전 색이 교대로 등장하는 아주 단순한 패턴이다.
요즘 세상에 테두리는 그냥 알파 채널을 가미한 옅은 실선으로 그려도 될 것 같지만, 이 분야는 구닥다리 GDI 레거시 API와의 호환 문제도 있어서 그런지 여전히 옛날 그래픽 패러다임이 쓰이고 있다. 이 xor 테두리는 계산량 적고 간편할 뿐만 아니라, 다시 한번 그리라는 명령을 내리면 싹 사라지고 원래 이미지로 돌아온다는 특성도 있어서 더욱 편리하다.

이 테두리는 두께가 오랫동안 1픽셀로 고정되어 있었다. 하지만 1픽셀만으로는 너무 가늘어서 눈에 잘 띄지 않고 시각 장애인의 접근성에 좋지 않다는 의견이 제기되었다. 게다가 모니터의 해상도가 갈수록 올라가고 100%보다 더 높은 확대 배율도 등장하다 보니, 1픽셀 고정 두께의 한계는 더욱 두드러지게 됐다.

이 때문에 Windows XP부터는 제어판 설정에 따라 2픽셀 이상의 focus 테두리도 등장할 수 있게 됐다.
이 조치가 응용 프로그램에서 특별히 문제가 될 일은 거의 없겠지만, DrawFocusRect로 평범한 직사각형을 안 그리고 1~2픽셀 남짓한 두께의 수직선· 수평선을 그려 왔다면 선이 의도했던 대로 그려지지 않을 수도 있다. 같은 영역에 선이 두 번 그려지면서 점선이 없어져 버리기 때문이다.

DrawFocusRect는 기술적으로 사각형 테두리 모양으로 50% 흑백 음영 비트맵을 브러시로 만들어서 PatBlt() 한 것과 완전히 동일하다. raster operation은 PATINVERT (흑백 xor target)이고 말이다. 그러면 원래색 / 반전색이 교대로 등장한다.
xor이 아니라 and라면 과거 Windows 9x/2000의 시스템 종료 대화상자의 배경처럼 "검정 / 원래색"이 교대로 등장하면서 화면이 반쯤 어두워지는 걸 연출할 수 있을 텐데.. 이 래스터 연산 코드는 따로 정의돼 있지 않은 것 같다.

그런데.. Windows의 GDI API에서 흑백 비트맵은 자체적인 색이나 팔레트 따위가 없으며, 현재 DC의 글자색과 배경색이 DC에 select된 비트맵의 색깔로 쓰인다.
그렇기 때문에 DrawFocusRect로 정확하게 반전 점선 테두리를 그리려면 호출 당시에 해당 DC의 글자색과 배경색을 반드시 black & white로 해 줘야 한다. 시스템 색상 따질 것 없이 RGB(0,0,0)과 RGB(255,255,255)로 하드코딩하면 된다.

이렇게 해 주지 않으면 마지막으로 텍스트를 찍던 당시의 글자색 및 배경색이 무엇이냐에 따라서 focus 테두리의 색깔이 정확하게 반전색이 되는 게 아니라 들쭉날쭉 날뛰고 지저분해질 수 있다.
이건 꽤 중요한 사항인데 왜 MSDN 같은 문서에 전혀 소개되어 있지 않았나 모르겠다. 나도 10수 년째 모르고 있다가 요 얼마 전에야 깨달았다.

또한 50% 음영은 굉장히 단순하고 자주 쓰이는 패턴인데.. 브러시나 비트맵을 stock object로 제공을 좀 해 주지, 왜 안 하나 모르겠다. 요즘 같은 트루컬러, 알파채널 이러는 시대보다도 모노크롬, 16색 이러던 옛날에 더 필요했을 텐데 말이다.
CreateCaret 함수로 caret을 생성할 때는 일반적인 비트맵 핸들 대신 특수한 상수를 넣어서 50% 음영 모양을 지정하는 게 있는데.. caret보다는 다른 형태로 쓰이는 경우가 더 많다.

다음은 파란 배경에 대해서 잘못 그려진 테두리(위: 반전색+검정)와, 맞게 그려진 테두리(아래: 반전색+원래색)의 예시이다.

사용자 삽입 이미지

3. 비트맵 윤곽으로부터 region을 곧바로 생성하는 방법의 부재

Windows에서 region은 사각형이 아닌 임의의 비트맵 영역을 scan line들의 집합 형태로 표현하는 자료구조이며, 창을 사각형이 아닌 임의의 모양으로 만드는 데 쓰이는 수단이기도 하다. 이 블로그에서 예전에 한번 집중적으로 다룬 적이 있다. (☞ 예전 글)
Windows에서는 사각형이 아닌 임의의 복잡한 모양의 region을 생성하기 위해서 다각형, 원, 모서리간 둥근 사각형 등 여러 API를 제공하며, 집합 연산 비스무리하게 기존 region과 영역을 합성하는 CombineRgn이라는 함수도 제공한다.

그런데 이것만으로는 여전히 좀 2% 부족한 구석이 있다.
region을 생성할 때 사용되는 원· 다각형 그리기 함수의 결과와, 실제 DC에다 원· 다각형을 그리는 함수의 결과가 픽셀 단위로 100% 정확하게 일치하지 않을 때가 있다. 그래서 딱 정확하게 영역 안에다가 테두리를 깔끔하게 그리는 게 난감하다.

그리고 아예 만화 캐릭터 같은 모양의 창을 만들 때는.. 저렇게 벡터 이미지가 아니라 임의의 마스크 비트맵으로부터 그 윤곽 영역대로 region을 바로 생성할 수 있는 게 좋은데 의외로 그런 함수가 없다.

뭐, region의 내부 자료구조에 접근해서 복잡한 region을 직통으로 생성하는 방법도 없지는 않지만(정말 생짜 직사각형들의 집합..;; ) 이 역시 귀찮다는 건 어쩔 수 없다.
이 때문에 비트맵 그림으로부터 region을 생성하는 코드를 보면.. 비트맵 내용대로 한 줄 한 줄 CombineRgn(RGN_OR)로 한눈에 보기에도 정말 느리고 비효율적인 방법을 쓰고 있다.

layered window의 color key를 쓰면 투명색을 더 편리하게 구현할 수 있긴 하다. 허나, 창 아래의 그림자(CS_DROPSHADOW)는 region을 통해 지정된 경계하고만 정확하게 연계한다. 그렇기 때문에 애니메이션이 아닌 데서는 구닥다리 region도 여전히 필요하다.

이 분야는 다른 그래픽 API 같은 대안이 있는 것도 아닌데 마소에서 GDI API의 지원에 왜 이리 인식한지 모르겠다.;;

Posted by 사무엘

2021/03/28 08:35 2021/03/28 08:35
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1870

Windows 환경에서는 프로그램이 자기 화면(창)에다 뭔가를 그리고 표시하는 걸 보통은 WM_PAINT 메시지가 왔을 때 한다.
하지만 반드시 그때만 그림을 그릴 수 있는 건 아니다. 키보드나 마우스 입력(특히 뭔가 드래그)이 들어와서 특정 지점에 대한 시각 피드백만 즉각 주고 싶을 때, 혹은 타이머를 걸어서 일정 시간 주기로 반드시 뭔가를 그리고 싶을 때는 InvalidateRect라든가 WM_PAINT에 의존하지 않고, 프로그램이 직통으로 DC를 얻어 와서 그림을 그려도 된다.

화면 그리기뿐만 아니라 키보드 입력 인식도 마찬가지이다.
반드시 WM_KEYDOWN/UP 메시지를 통해서만 키보드 입력을 인식할 수 있는 건 아니다. 마우스 메시지를 처리 중일 때도 shift나 ctrl 같은 modifier key가 같이 눌렸는지, 혹은 caps/num/scroll lock 램프가 현재 켜져 있는지를 함수 호출 하나로 간편하게 알 수 있다.
그런 modifier 글쇠조차 매번 WM_KEYDOWN/UP때만 감지할 수 있다면.. 응용 프로그램이 지역 변수의 범위를 넘어서는 지저분한 key state 관리자를 둬야 할 것이고, 코딩이 굉장히 번거롭고 불편해질 것이다.

옛날에 도스 시절에 키 입력을 감지하는 건 꽤 번거로웠던 걸로 본인은 기억한다.
문자가 아닌 화살표, home/end, page up/down 같은 글쇠에 대해서는 0번(null) 문자가 prefix 명목으로 오고, 동일 함수를 한번 더 호출해서 실제 값(아마 스캔 코드)을 얻는 형태였다. 그러고 보니 저건 나름 dead key라는 개념이 구현된 셈이다.

그것 말고 ctrl이나 shift, 각종 lamp 글쇠는 저런 방식으로도 잡히지 않았기 때문에 또 다른 도스 API를 동원해야 했다. 요것들은 키보드 버퍼를 차지하지 않고, 컴퓨터가 바쁠 때 아무리 누르고 있어도 삑삑 소리를 발생시키지 않는 조용한 특수글쇠이기 때문이다.;;

글쇠를 누르는 것 말고 떼는 것을 감지하는 것도 본인은 도스 시절에 개인적으로 경험한 적이 없다.
글쇠를 누르고 있는 동안 해당 문자를 일정 간격으로 반복해서 접수해 주는 것은 컴퓨터 하드웨어 차원에서 행해지는 일인데.. 그런 키보드 속도에 구애받지 않고 누른 것과 뗀 것 자체만을 감지하는 건 특히 게임 만들 때의 필수 테크닉이다.
그랬는데 Windows에서는 모든 글쇠들이 한 치의 차별 없이 WM_KEYDOWN과 WM_KEYUP 메시지 앞에서 평등해지고 가상 키코드값을 부여받게 됐다니~! 정말 혁명 그 자체였다. 프로그래밍 패러다임이 싹 바뀌었다.

가상 키코드는 기반이 전적으로 소프트웨어에 있는 계층이기 때문에 같은 하드웨어에서도 차이가 날 수 있다. 가령, 같은 글쇠에다 가상 키코드를 부여하는 방식은 Windows와 mac이 서로 다를 수 있으며, Windows는 사용하는 키보드 드라이버에 따라서도 차이가 날 수 있다.

Windows의 가상 키코드는 caps lock 내지 shift의 영향을 받지는 않기 때문에 a건 A건 코드값이 같다. 하지만 num lock의 영향은 받기 때문에 키패드 0~9 숫자와 키패드 화살표의 코드값이 서로 다르다. 키패드 numlock 숫자는 진짜 숫자키의 숫자와도 가상 키코드가 다르다.
가상 키코드와 달리 스캔 코드는 각각의 물리적인 글쇠들에 고정불변으로 부여되어 있다. 좌우로 두 개 있는 shift처럼 가상 키코드가 동일한 글쇠는 스캔 코드로 방향을 구분할 수 있다.

요컨대 스캔 코드는 저수준이고 가상 키코드는 고수준이다. 여기에다가 문자 글쇠는 message loop에서 TranslateMessage 함수를 거침으로써 caps lock(대소문자)까지 고려한 실제 입력 문자가 담긴 WM_CHAR로 바뀐다.
WM_CHAR가 생성되는 과정(가상 키코드와 스캔 코드로부터 문자를 얻기)이 별도의 함수로 제공되기도 한다. 바로 ToUnicode 내지 ToAscii이다.

배경 설명이 좀 길어졌는데..
현재 어떤 글쇠가 눌러졌는지 여부를 알려주는 대표적인 함수는 GetKeyState이다. 인자로는 가상 키코드를 주면 되고, 리턴값으로는 2비트의 유의미한 정보가 담긴 BYTE값이 돌아온다.
최상위 비트 0x80은 이 key가 지금 눌렸는지의 여부이고, 최하위 비트 1은 눌렸다 뗐다 toggle 여부이다. 3대 lock들의 램프 점등 여부는 &1을 해 보면 알 수 있다.

심지어 GetKeyboardState 함수는 모든 가상 키코드값에 대한 키보드 상태를 배열 형태로 한꺼번에 되돌려 준다.
컴퓨터 키보드의 글쇠는 많아야 100여 개이지만 가상 키코드의 범위는 0~255라는 바이트 규모이므로 가상 키코드를 할당할 공간은 아주 넉넉한 셈이다.

그런데 Windows에는 GetAsyncKeyState라는 함수도 있다. 무엇이 비동기적이라는 얘기이며 GetKeyState와는 어떤 차이가 있는 걸까..?
GetKeyState는 현재 스레드의 메시지/input 큐 기준으로 WM_KEYDOWN/UP 메시지가 마지막으로 처리되었던 그 순간의 키보드 상태를 일관되게 쭉 되돌린다. 한 메시지가 처리되던 도중에 사용자가 어떤 글쇠를 누르거나 떼더라도 값이 변함없다.
한 컴퓨터에 키보드야 하나만 존재하겠지만, 각 응용 프로그램의 UI 스레드별 키보드 상태는 이론적으로 서로 제각각으로 다를 수 있다.

그 반면, GetAsyncKeyState는 그런 것과 상관없이 시스템 전체의 현재 키보드 상태를 실시간으로 반영해서 알려준다. 그리고 이유는 알 수 없지만 GetKey*는 최상위 bit 크기가 BYTE인 반면, GetAsyncKey*는 최상위 bit 크기가 WORD이다.
둘 다 함수의 리턴 타입은 short로 잡혀 있다. 하지만 전자는 눌려 있는 글쇠를 0x80으로 표현하는 반면, 후자는 0x8000으로 표현한다.

그러면 마우스 휠을 Ctrl을 누른 채로 굴렸는지 감지하고 싶을 때 GetKey*와 GetAsyncKey* 중 무엇을 쓰는 게 좋을까?
프로그램이 사용자의 키보드· 마우스 입력에 0.1초 안으로 정상적으로 반응하고 있는 상태라면 두 함수는 유의미한 차이를 보이지 않는다.

GetAsyncKey*는 내 프로그램이 작업을 하느라 수 초 동안 응답이 멎은 중에 사용자가 ESC를 누른 것 정도나 잡아내는 용도로 쓰면 된다. 아니면 애초에 자기 GUI 창이 없는 콘솔 프로그램에서 키 입력을 감지하는 것 말이다. 얘는 심지어 포커스가 다른 프로그램에 가 있을 때에도 특정 글쇠가 눌린 것을 감지할 수 있다.

이와 달리 GetKey*는 메시지 처리 단위로 실행 결과가 '동기화'돼 있으며, 정확하게 자기 스레드의 UI에 포커스가 가 있을 때 글쇠가 눌린 것만 감지해 준다. 그러니 일반적인 상황에서 우리에게 필요한 것은 대체로 GetAsyncKey*가 아니라 그냥 GetKey*이다.

Async가 붙은 놈이건 안 붙은 넘이건, 이들 함수는 글쇠가 눌린 것을 감지만 하지, 그걸 처리한 것으로 퉁쳐 주지는 않는다. 내 작업 루틴에서 ESC가 눌린 것을 감지해서 하던 작업을 중단했다 하더라도 UI에서 WM_KEYDOWN + VK_ESCAPE 메시지가 가는 것은 변함없다.
이럴 거면 GetAsyncKey*를 호출할 게 아니라 Peek/Get/DispatchMessage로 메시지를 정식으로 처리하는 게 더 낫다. GetAsyncKey*는 쓸 일이 더욱 줄어드는 셈이다.

Posted by 사무엘

2021/03/20 08:35 2021/03/20 08:35
, , ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1867

1. DLL 주소 재배치와 ASLR의 관계

Windows XP 내지 Vista 이후로 (1) 커널 API와 C 런타임 라이브러리 함수 심벌들이 한 DLL 몰빵이 아니라 분야별로 재분류되어 배치되기 시작한 것, (2) 시스템 DLL들이 이제 전혀 rebase되지 않고 고정된 단일 preferred base 주소를 갖기 시작한 것을 보면 참 격세지감이 느껴진다.

위의 둘은 (1) 자잘한 DLL 여러 개보다 큰 DLL 하나가 더 효율적이다(선박처럼??). (2) DLL들은 로딩되는 주소가 겹치지 않게 빌드 후에 반드시 rebasing을 해 줘라
이런 전통적인 고정관념을 역행하는 변화이기 때문이다.

보안 강화를 위해 10여 년 전 Windows Vista 때부터 ASLR (시작 주소 랜덤화)이 도입되면서 DLL은 물론이고 EXE조차도 반드시 자기 preferred base에 고정적으로 로딩이 되지 않게 되었다. 이 때문에 요즘은 EXE도 과거 Win32s 프로그램들처럼 끝에 재배치 정보가 다시 포함돼 들어가고 있다.

하지만 이런 ASLR을 위한 재배치 때는 말 그대로 메모리 오프셋 수정만 행해질 뿐, 재배치의 치명적인 페널티라고 여겨지는 가상 메모리 페이지 파일 재기록이라든가 재사용 불가(여러 프로세스에서 동일 DLL 로딩 시에도 shallow가 아닌 deep copy 발생) 까지 발생하지는 않는다. 운영체제의 보안 기능이 그 정도로 바보는 아니다.

그러므로 오늘날은 DLL을 미리 rebase 하건 안 하건 실행 성능이 달라지는 것은 없다. rebase를 해도 이익을 얻는 것은 없지만 반대로 손해를 보는 것 역시 없다. rebase라는 게 빌드 타임이 아닌 런타임의 영역으로 바뀐 셈이다.

정말 재수가 없어서 엄청 많은 자잘한 DLL들이 로딩되다 보니 한 DLL이 프로세스 A에서는 ASLR 배당 주소로 로딩됐지만 프로세스 B에서는 그 주소로 로딩이 못 되게 됐다면.. 그때는 통상적인 페널티가 부과되는 재배치가 발생할 것이다. 하지만 광대한 주소 공간을 자랑하는 64비트 환경에서는 그럴 가능성이 더욱 희박해졌다.

2. EXE를 LoadLibrary 하기

LoadLibrary 함수는 실행 가능한 코드가 담긴 DLL을 불러오거나 혹은 EXE/DLL로부터 리소스를 얻고자 할 때 즐겨 쓰인다.
그런데 여기서 의문이 든다. LoadLibrary를 호출해서 exe의 단순 리소스가 아니라 코드를 내 프로세스 공간에 가져와 실행하는 게 가능할까?

사실, 기술적으로 볼 때 EXE와 DLL의 차이는 그리 크지 않다. 심지어 EXE도 DLL처럼 심벌 export를 할 수 있다.
그리고 EXE를 LoadLibrary로 그냥 쌩으로 불러와도, 의외로 일단 성공은 한다. GetProcAddress를 해서 심벌을 요청하면 주소값이 돌아오기까지 한다.
하지만 그 함수를 호출해 보면 십중팔구 access violation 에러가 난다. 여기서 대부분의 사람들은 '안 되나 보다'라고 생각하고 단념하게 된다. 왜 이런 현상이 발생하는 것이며, 문제를 해결할 방법은 없는 걸까?

DLL이 아닌 EXE를 LoadLibrary 하면 운영체제는 얘를 반쯤 데이터로 취급하는가 보다. GetProcAddress를 호출했을 때 심벌 검색 결과를 되돌려 주지만 그 포인터가 가리키는 코드를 실행 가능한 상태로 만들어 놓지는 않는다.
특히 (1) 주소 재배치와 관련된 그 어떤 조치도 취하지 않는다. 구체적으로는.. EXE가 사용하는 import table의 주소를 패치하지 않기 때문에 그 EXE의 코드가 실행되면서 Windows API 같은 걸 호출하면 그대로 뻑이 나게 된다.

그리고 (2) EXE의 진입점 함수를 전혀 실행하지 않는다.
EXE건 DLL이건 무조건 맨 먼저 실행할 부분을 가리키는 진입점이란 게 있는데.. 그게 EXE는 int func() 형태이고, DLL은 BOOL func(HMODULE, UINT, PVOID) 형태이다.

즉, EXE는 처음엔 아무 인자 없이 실행됐고 C 라이브러리가 GetStartupInfo 같은 API 함수를 호출해서 실행 인자를 준비한 뒤에 main이나 WinMain을 또 호출하는 형태이다. 그러나 DLL은 진입점 함수의 형태가 DllMain과 완전히 동일하다. 즉, DLL_PROCESS_ATTACH 같은 이벤트 명칭은 이 함수의 호출 인자가 아니면 딴 데서 알아낼 곳이 없다.
LoadLibrary는 원래 DllMain을 호출하게 돼 있는데 EXE는 받아들이는 함수 prototype이 다르므로 아예 호출을 안 하는 것이다.

그러므로 LoadLibrary된 exe의 코드를 강제로 실행한다면 IAT 테이블의 주소가 패치되지 않고 C 라이브러리가 전혀 초기화되지 않은 상태에서 덥석 실행된다. 그 함수에서 내부적으로 전역변수 C++ 객체 같은 걸 사용한다면.. 역시나 제대로 실행되지 못하고 높은 확률로 뻑나게 된다.

IAT 주소를 패치하는 방법까지는 어느 용자가 찾아낸 게 인터넷에 이미 굴러다닌다. (☞ 링크) 이거 패치가 제대로 되려면 EXE는 애초부터 재배치 정보가 들어간 상태로 빌드돼야 한다.
하지만 각종 부작용 없이 C 라이브러리만 감쪽같이 초기화하고 EXE의 export 함수를 실행하는 건.. 굉장히 삽질스럽고 가성비가 낮다. 그냥 EXE와 DLL의 차이가 이러하며 LoadLibrary(EXE)가 기술적으로 왜 권장되지 않는지 이론으로만 알고 넘어가면 될 듯하다.

3. 재빠르게 대체된 파일에 대한 creation date 보정

응용 프로그램 중에는 안전을 위해 문서 저장 기능을 임시 파일을 생성하는 형태로 구현한 것이 있다.
기존 파일을 곧장 덮어써서 저장하는 게 아니라.. 임시 파일에다가 저장을 한 뒤, 기존 파일을 지우고 임시 파일을 기존 파일의 이름으로 바꾼다. 이렇게 하면 저장하는 중에 컴퓨터에 전기가 나가는 등의 이상 현상이 발생하더라도 최소한 기존 자료가 송두리째 날아가는 일은 막을 수 있다.

그런데 이렇게 기존 파일을 덮어쓰는 게 아니라 파일 자체를 딴 것으로 대체하는 식으로 저장을 하면 기존 파일이 갖고 있는 creation time이 보존되지 않게 된다. 그렇기 때문에 기존 파일의 creation time을 따로 얻어 놓은 뒤, 저장을 마친 새 파일에 대해서 creation time을 SetFileTime 함수로 따로 지정해 줘야 한다.

단, Windows NT 계열의 경우, 놀랍게도 보정 동작을 진작부터 지원하고 있었다. 어떤 프로그램이 A라는 파일을 삭제한 뒤에 다른 파일의 이름을 A로 신속하게 거의 곧장 변경한 경우, 그 파일에다가 삭제된 A의 creation time을 자동으로 지정해 줬던 것이다~!

이런 보정을 위해서는 파일 삭제와 개명 알고리즘에다가 삭제된 파일의 생성 시각을 백업해 놓고, 시간차를 감지해서 이 renaming이 기존 파일을 승계하는 동작인지 판단하는 등 여러 귀찮은 작업이 필요할 것이다. 하지만 마소에서는 임시 파일 방식으로 저장하면서 creation time을 관리하지 않는 프로그램이 많은 것을 감안하여 운영체제 차원에서 이런 보정 기능을 구현했다고 한다.

이 보정은 NT 계열에서만 지원되어 왔으며, 9x 계열에서는 존재하지 않는다.

4. 스레드 동기화 deadlock 자동 감지

복잡한 메모리 문제를 잡아내기 위해 C 라이브러리 차원에서 저런 다양한 안전 장치와 디버깅 편의 기능이 제공되듯, 멀티스레드 동기화 오브젝트에도 디버그 버전용은 데드락 정도는 assertion failure 에러를 내면서 곧장 감지하는 기능이 있으면 좋겠다는 생각이 든다.

“당신이 지금 취득을 위해 대기하려는 뮤텍스는 현재 다른 스레드가 잡고 있는데, 문제는 그 스레드도 지금 당신이 요 스레드에서 잡고 있는 뮤텍스를 얻으려고 대기 중이다. 그러니 이 상태로는 상호 무한 대기 교착 상태가 됨.”

이건 레퍼런스 카운트 기반인 오브젝트에서 순환 참조 오류를 감지하는 기능을 구현하는 것과 기술적으로 완전히 동급이다.
Hash 같은 컨테이너를 둬서 스레드 ID별로 각각 현재 진입해 있는 뮤텍스에 대한 기록을 관리하고, 뮤텍스 오브젝트를 감싸는 클래스에다가 현재 자신을 잡고 있는 스레드 정보도 같이 보관하는 정도의 수고만 하면 큰 어려움 없이 구현 가능하다.

하지만 PC용 프로그램에서 돌아가는 스레드의 개수가 무슨 할당된 동적 메모리 블록 개수처럼 많을 리는 없을 것이고, 프로그램의 응답이 멎었을 때 데드락 부위를 찾는 것은 도구의 도움 없이 도저히 못 할 일은 아닐 것이다. 유용성에 비해 저런 기능을 갖추는 건 속도와 메모리 오버헤드가 너무 커서 가성비가 맞지 않으니 데드락 자동 감지 기능은 운영체제나 프로그래밍 언어 런타임이 제공해 주지 않는 듯하다.

개인적으로 직장에서는 심지어 자기 스레드 자신의 실행이 끝나기를 기다리는.. C++ 오브젝트로 치면 delete this.. 무슨 자살이나 다름없는 deadlock도 경험한 적이 있었다.
프로그램의 실행이 종료되어 UnInit() 함수가 호출될 때는 백그라운드 작업 스레드에 대해서도 작업을 중단시키고 작업 스레드의 실행이 끝날 때까지 기다리게 했는데, 뭔가 로직이 꼬여서 작업 스레드에서 UnInit()를 호출하는 상황이 발생한 것이다.

Uninit이 무슨 loop 안에서 1초에 수십, 수백 번씩 실행되어서 성능이 중요한 함수인 건 아니다. 그러니 자기 자신이 무슨 스레드 문맥에서 실행되었는지 검사해서 deadlock을 피할 수도 있다.
하지만 그것보다는 Uninit이 스레드 함수가 아니라 의도했던 대로 main thread에서만 실행되도록 프로그램 구조를 고치는 것이 훨씬 더 나은 해결책이었다.

Posted by 사무엘

2021/02/03 19:36 2021/02/03 19:36
,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1850

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

블로그 이미지

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

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

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

Site Stats

Total hits:
3044500
Today:
1692
Yesterday:
2435