1. 존재를 부정 당하고 있는 IE 브라우저

지난 2010년대 초는 IE6을 작정하고 퇴출시키려던 시기였다. 그 뒤, 2020년대 초는 IE를 통째로 퇴출시키려는 시기가 됐다. 그 전에 플래시와 ActiveX부터 먼저 퇴출됐고 말이다.
그럼 지금은 키보드 보안이나 특수한 암호화 같은 게 필요하면 DLL 형태인 ActiveX 대신, EXE 실행 파일로 다 바뀐 건지? 그럼 요즘은 크롬에서도 인터넷 뱅킹이 가능한 건지?? 잘 모르겠다. 내 개인적으로는 여전히 뭔가 잘 안 돼서 PC에서 금융 거래를 할 때는 아직 "Edge + IE 모드"에 의존한다.

Media Player는 더 업데이트를 하지 않는 레거시로 남았을지언정 일부러 없애지는 않는 물건이다. 그러나 IE는 하루가 다르게 바뀌는 웹 프로그래밍 플랫폼이며 보안에 민감한 놈이다. 그래서 마소에서도 매우 적극적으로 존재 자체를 부정하고 없애려는 중이다.

그래서 작년 하반기쯤이었나? Windows 10의 어느 버전인가 업데이트를 설치한 뒤부터는 이제 Internet Explorer가 대놓고 실행되지 않게 됐다. 그냥 꺼져 버리고 Edge가 대신 뜬다.
과거 Windows Me가 여전히 도스를 완전히 벗어나지 못한 9x 커널 기반이었음에도 불구하고, 겉으로만 도스로 부팅하거나 도스로 돌아가는 기능을 없애 버린 것과 비슷한 조치 같다.

그러나 IE라는 껍데기 말고, IE가 사용하는 그 트라이던트 웹 렌더링 엔진과 윈도우 컨트롤은 사실상 Windows 운영체제의 일부나 마찬가지이다. 얘에 의존해서 돌아가는 기존 프로그램도 있기 때문에 그걸 통째로 없앨 수는 없다.
마치 Visual Basic 6처럼 말이다. 얘는 마소에서 21세기 이래로 줄기차게 없애고 .NET으로 대체하려 애썼지만 끝내 그러지 못했다. 게다가 Office 제품군의 매크로 언어인 VBA가 여전히 재래식 VB 기반이니.. 이것 때문에라도 완전히 없애는 건 불가능하다.

IE 엔진이 여전히 쓰이는 대표적인 분야는 도움말이지 싶다. 특히 HTML 도움말(*.chm) 말이다. 얘는 이미 10년 이상 전부터 추가적인 개발과 지원이 끊겼으며, 마소로부터 사실상 완전히 버려진 자식 신세이다.
그런데 마소에서 지난 2010년대부터는 '로컬 오프라인 환경에서 열람하는 도움말/문서'라는 개념 자체를 완전히 송두리째 폐기한 것 같다. IE나 HTML 도움말만 없앤 게 아니라는 뜻이다.

그러니 이제 메모장을 띄웠을 때 F1을 누르면.. '메모장 도움말'을 Bing에서 검색한 결과가 뜬다.;;; 이렇게 무심할 데가..
정말 믿어지지 않지만, 현재의 Windows 10/11은 로컬 도움말이란 게 사실상 존재하지 않는 것 같다. 오죽했으면 이젠 Windows\help 디렉터리가 거의 텅 비어 있다. 거기 그나마 들어가 있는 건 뭐 nvidia 제어판이나 개발 관련 듣보잡 문서이지, Windows 자체에 대한 일반 사용자용 도움말이 아니다.

개발툴도 예외가 아니어서 Visual Studio 2010/2012를 즈음해서는 10여 년의 역사를 자랑하던 msdn Document Explorer가 없어졌고.. 이제 내장 도움말 컨텐츠 다운로드 같은 것도 없어졌다.
이런 게 완전히 없어지기 전에는 chm이 아니어도 자기들만 쓰는 html + IE 엔진 기반의 개발 문서 조회 시스템이 있었는데.. 요즘은 그런 것 자체가 없다는 것이다. 걍 단순 정보 조회는 구글로, 문제 해결 정보는 Stack overflow를 뒤지라는 뜻인 듯..

안 그랬으면 HTML 도움말을 IE 대신 크로뮴 엔진 기반으로 다시 만드는 지원이라도 할 텐데 그런 것조차 없다.
근데 현실적으로는 다른 대안이 있나? Windows용 앱 내부에서 뭐 최신 반응형 웹 따위 바라지도 않고, 간단히 html 렌더링해서 표시하고 싶은데 IE ActiveX 컨트롤보다 더 간편한 방법이 있을까? 그건 의문이 든다.
아무리 웹이 기존 앱의 영역을 많이 잠식하긴 했어도, 그래도 앱 내부에서 웹페이지를 표시할 일도 있을 텐데 말이다.

그리고 또 하나 생각할 점은 웹페이지의 인쇄이다.
내 경험상.. 당장 이 블로그 웹페이지를 같은 pdf 드라이버로 인쇄해 보면 IE는 그럭저럭 최적화된 형태의 pdf가 만들어진다. 그 반면, 크롬은 훨씬 더 bloat된 이상한 형태로 pdf가 생성되며, 인쇄하는 데 시간도 더 오래 걸린다.
무작정 IE를 없애 버리기에는 제대로 된 대안· 대체제가 여전히 존재하지 않는 것 같다.

2. 이상하게 바뀐 메모장

올해 초쯤에 Windows 11의 업데이트를 설치하고 나자.. 프로그램 창의 ‘최대화’ 버튼의 동작이 살짝 달라졌다. 마우스로 얘를 가리키면 고전적인 전체 최대화 외에, 좌우 1:1:1 등 어느 레이아웃으로 최대화할지 고르는 타일 메뉴가 나타나기 시작했다.
Windows 7에서 창을 마우스로 화면 좌우상하 구석으로 끌었을 때 그렇게 배치하는 기능이 처음 추가되긴 했는데, 그게 13년쯤 뒤에 저렇게 강화됐다. 요건 뭐.. 나쁘지 않아 보이네.

그런데 그것 말고도 마소가 꽤 큰 사고를 쳤더라. 세상에.. 메모장이 메트로 UI 형태로 다시 만들어졌다.
본문이 운영체제 기본 에디트 컨트롤이 아니라.. 워드패드와 동일한 리치 에디트 기반으로 바뀌었다.
개인적으로 이거 완전 비호감이더라. 메모장은 단순한 앱의 상징 마지노 선으로 봉인 좀 시켜 놓지, 왜 저런 짓을 했나 모르겠다. 사람들이 메모장이 기능이 많아서 애용하는 줄 아는가??

그런 주제에 UTF-16랑 KS X 1001 인코딩의 파일이 열리지 않고, 우클릭 컨텍스트 메뉴에 알파벳 단축키가 먹히지 않는다. 예전에 되던 것이 안 되니 새 버전을 사용하기가 매우 꺼려지게 된다.
저런 쓸데없는 걸 건드리지 말고, sihost.exe가 CPU 다 쳐먹으면서 폭주하는 버그나 좀 고칠 것이지.. 그 문제는 여전한 걸 보고는 더 좌절했다.

난 지금까지도 솔직히 Win10이랑 11의 차이를 모르겠다. 업데이트 해야 할 필요를 느끼지 못한다.
10에서는 작업 표시줄을 우클릭해서 바로 작업 관리자에 들어갈 수 있었는데 11에서는 못 하고..
정말 필요한 기능이 아니라 그냥 괜히 바꾸기 위해서 바꾼 게 많다. 이럴 거면 걍 10의 업데이트나 낼 것이지 왜 11이라고 차별화를 한 걸까?

난 멀쩡히 잘 돌아가는 기능들에다가 쓸데없이 자꾸 메트로 UI를 도입하는 것도 근본적으로 마음에 안 든다. 현실에서 멀쩡한 길거리 인도 보도블럭을 쓸데없이 교체하는 것과 비슷한 느낌이 든다.
아 그래, 25년쯤 전에 마소에서 Windows UI 셸을 전부 IE4 Active desktop으로 도배하려는 것과 비슷하게 느껴지기도 한다.

Posted by 사무엘

2022/06/11 08:36 2022/06/11 08:36
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/2030

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

2021년이 된 와중에 문득 생각을 해 보니.. Windows 8과 8.1은 정말 존재감 없이 소리소문 없이 싹 묻히고 사라진 것 같다.
XP, 7 다음에 바로 10이다. 8 계열은 20년 전의 ME만치 망한 건 아니지만 Vista보다는 더 흑역사이다.

사실 난 개인적으로는 옛날에 XP 다음의 Vista는 그 정도로 욕 먹을 퀄리티는 아니었으며, 일부는 오해와 누명을 쓴 것도 있다고 생각한다. 5년 만에 출시됐다 보니 갑자기 시스템 요구 사항이 너무 높아지고 일부 하드웨어와 호환이 깨지고, 사용자 계정 컨트롤이 도입된 게 반발을 일으켰을 뿐.. 그건 시간이 해결해 줄 수 있고 사후 재평가의 여지가 있는 이질감이었다. 7이라도 XP 다음에 바로 갑툭튀였다면 맞았을 질타를 미리 맞아 준 것의 비중이 크다.

그럼 8 계열은 사정이 어땠을까? 잠시 10여 년 전 당시 상황으로 되돌아가 보자.
이때는 나름 격변기였다. 아이폰이니 안드로이드니 하면서 본격적으로 스마트폰이라는 새로운 컴퓨팅 생태계가 조성되고 있었으며, 마소에서도 초대 원로 경영진(빌 게이츠, 스티브 발머)이 대거 은퇴하고 세대가 교체되고 있었다. 사내 분위기가 크게 요동칠 수밖에 없었을 것이다.

이 와중에 얼리어답터들은 “Windows가 7 이후로 2010년대엔 도대체 어디까지 변모할까? 마소는 이 시점에서 어떤 결정을 내릴 것이며 PC와 모바일과의 접목은 어떻게 이룰까?”라고 기대와 주목을 잔뜩 하고 있었다. 그때는 다음 버전이 심지어 재래식 NT 커널을 버리고 처음부터 다시 개발된다는 루머까지 나돌았다. (실제로는 그럴 리가.. NT 커널은 지금 Windows 10에서도 건재함! 없어질 수가 없다)

그랬는데.. Windows 7의 차기 버전인 8은 결과적으로 뭔가 구심점 없고 나사 빠진 듯한 망작이 되었다.
비주얼이 다시 심플해진 것은 그렇다 친다만, 갑자기 시작 메뉴가 없어지고 전체 화면으로 바뀐 것은.. PC와 스마트폰의 크기와 용도의 차이에 대한 고찰이 결여된 자충수였다. 게다가 8은 시작 메뉴 버튼 자체가 없었다~! 이런 제품이 어째 QA나 사용성 테스트를 통과하고 출시됐는지 모르겠다.

더 옛날인 2000년대엔 새 밀레니엄 컴퓨팅을 표방하면서 인텔 Itanium이라는 64비트 프로세서가 출시되었고 Windows는 2000뿐만 아니라 ME도 나왔었는데.. 아시다시피 Itanium과 ME는 대차게 망했다. 그런 것처럼 2010년대를 개막했던 Windows 8은 사용성 면에서 큰 혹평을 받으면서 XP와 7의 아성을 넘지 못했다.

그리고 사실은 스마트폰 OS의 쟁탈전도 매우 싱겁게 끝났다. 지금 다들 경험하시는 바와 같이 안드로이드 아니면 iOS로 양분되었고, 마소는 이 바닥에서의 주도권을 완전히 빼앗겼다. 심지어 웹 브라우저마저 IE 독점은 진작에 물 건너갔고 Edge 브라우저도 자체 개발을 포기하고 크로뮴 엔진에 흡수되지 않았는가?
국내로 치면 모바일에서 주도권을 완전히 주도권을 빼앗기고 삼성 전자와는 정반대의 처지가 된 LG 전자와 비슷하다고 하겠다..;

그러니 8 시절부터 Windows에 도입된 Metro 앱이니 하는 것 역시 폰에서는 볼 일이 없어졌다. Universal이라는 수식어가 무색해졌고 지금은 그냥 스마트폰 같은 큼직한 글자의 UI 엔진이 제공되는 특이한 프로그래밍 플랫폼? 가상 머신?처럼 됐을 뿐이다. NT 커널 없이 새로 개발된다는 엔진이 이런 곳에 적용된 거라고 보면 되겠다.

그렇게 결판이 난 뒤 마소에서는 스마트폰용 OS가 아니라 그냥 기존의 데스크톱용 Windows 10을 스마트폰용 CPU인 ARM64에다가 포팅하는 식으로.. 즉, 자기 정체성을 유지하면서 모바일 환경에다 손을 내민 상태이다. 요즘 PC와 스마트폰의 경계가 많이 모호해지긴 했지만 그래도 PC는 안 쓸 때도 24시간 내내 켜 놓는 물건은 아니며, 떨어뜨려도 될 정도로 튼튼한 물건도 아니라고 여겨진다. 그리고 스마트폰은 프린터나 재래식 하드디스크 같은 장치와는 여전히 친숙하지 않다.

그렇게 Windows 8 내지 8.1은 2010년대 초반 마소의 시행착오가 깃들어 있는 비운의 작품이 됐다.
개인적으로는 Windows 제어판이 Metro 기반인 ‘설정’으로 야금야금 대체된 건 글쎄.. 그냥 reinvent the wheel 삽질 같고 멀쩡한 인도 보도블록을 교체하는 듯한 느낌이다. 기존 제어판에 익숙한 사용자를 괜히 헷갈리게만 하고 말이다.
지금도 키보드의 반복 속도를 최대로 조절하는 것만 해도 설정에서 가능하지 않기 때문에 재래식 제어판으로 가야 한다.

그리고 프로그램의 제목 표시줄에 제목이 가운데 정렬로 표시되었던 Windows는 과거의 16비트 시절 1~3.x 아니면 후대의 8/8.1밖에 없다~!! 거기 말고는 MS Office가 뜬금없이 2007부터 현재까지 가운데 정렬을 하며 따로 노는 중이다.

나머지 Windows 95, NT4부터 7, 그리고 지금의 Windows 10은 다 왼쪽 정렬이다.
Windows의 역사상 32/64비트 시대에 프로그램 제목을 가운데에다 표시했던 버전, 그리고 시작 버튼이 없던 버전이 있었다는 걸 기억하는 사람이 얼마나 있을까? 문득 궁금해진다.

한글 IME의 개발이라는 관점에서 보면 Windows 8 계열은 메트로 앱에 대해서는 키보드 포커스를 얻었을 때 가/A 한영 상태를 IME가 근처에다 팝업 창으로 수동으로 잠깐 띄워주고, 상태가 바뀌었을 때도 그걸 띄우는 수고를 해 줘야 하는.. 약간 원시적인 조치가 필요하던 유일한 운영체제였다.
메트로 앱은 전체 화면에서 실행되는 관계로, 운영체제의 기존 도구모음줄(language bar)의 보조를 전혀 받을 수 없기 때문이다.

이것도 아무리 생각해 봐도 뻘짓이었다. 도구모음줄을 IME가 제각각으로 구현해야 했던 1990년대도 아니고.. Windows 10부터는 메트로 앱도 창 모드로 실행 가능하고, 입력기의 상태를 작업 표시줄(task bar)에서 확인 가능하기 때문에 저렇게 할 필요가 없어졌다. 즉, 저 관행 역시 일회적인 이벤트로 끝났다는 것이다.

이상. 이 글에서는 Windows 8 계열에 대해 주로 다뤘지만, 걔네 말고 Windows라는 운영체제 제품에 대해서 개인적으로 의아하거나 궁금한 점을 더 꼽자면 다음과 같다.

1. 비현실적인 권장 사양

마소에서는 Windows 7 64비트 정도의 시점을 마지막으로.. 자기 운영체제의 동작을 위한 최소 PC 사양과 권장 사양을 더 올려서 기재하지 않고 있다.
7이 램 최소 1GB 이상, 권장 2GB 이상인데.. 이게 Windows 8과 10까지 동일하다. 하지만 이건 개인적으로 생각하기에 완벽한 허위· 과장 광고라고 여겨진다.

도대체 어느 정도 돌아가는 게 최소 사양이고 어느 정도로 여유가 남는 게 권장인지 정확한 정의는 잘 모르겠다만.. 요즘 Windows 10은 램 8GB에서 돌려도 부팅 직후에 크롬 브라우저 창 몇 개나 MS Office 프로그램, Visual Studio 같은 걸 열면 메모리가 거덜난다. 정말 못 해도 16GB는 돼야 넉넉하다는 느낌이 든다.

Windows 7에서야 최소 1GB, 권장 2GB는 수긍할 만하며 램 8GB는 펄펄 날아다니는 용량이다. 하지만 10은 절대 그렇지 않다. 아무리 못 해도 최소 2, 권장 4 이상으로는 수정돼야 하리라 여겨진다. 그리고 요즘도 32비트 에디션이 나오고 있나 모르겠는데.. 걔는 이제 단종시켜도 될 것이다.

2. 서버 제품군의 존재감

Windows의 개인 사용자용 제품군은 아시다시피 2015년부터 10이라는 단일 브랜드 하에서 주기적인 업데이트를 통해 프로그램을 진화(?)시키는 형태로 바뀌었다. 하지만 서버 제품군은 여전히 2008, 2012, 2016 등의 연도 브랜드를 쓰고 있어서 다소 이질적이다. 이런 이원화된 버전 넘버링이 언제까지 계속될지.. 마치 Office 365와 기존 Office 20xx 패키지의 관계만큼이나 궁금해진다.

그리고 더 결정적으로는 기업 같은 데서 서버 제품군을 쓰는 곳이 있긴 한지..?? 이건 마치 Itanium 컴퓨터만큼이나 본인이 태어나서 지금까지 한 번도 구경해 보지 못했다.
왜냐하면 서버 제품군을 쓸 정도의 컴퓨터라면 차라리 유닉스 터미널이 내장돼 있는 리눅스나 맥OS를 쓰고 말지 Windows를 쓰지는 않기 때문이다.

서버 에디션은 원래 Windows NT나 2000까지는 동일 버전의 바리에이션 형태로만 존재해 왔다. 그랬는데 XP 때는 웬일인지 Server 2003이라고 제품명은 물론 내부 버전 번호까지 다르게 붙일 정도로(5.1 vs 5.2) 차별화를 했었다. 그 대신 XP는 개인용 에디션이 홈 vs 프로로 더 세분화됐다.
그 뒤로 서버 제품군은 버전 번호는 클라이언트와 동일하지만 제품명은 저렇게 Server 20xx 식으로 나가고 있다.

3. 자잘한 UI 버그

  • 가끔씩 재래식 TSF 입력 도구모음줄과, 작업 표시줄 쪽의 Windows 10 스타일 입력 상태 아이콘이 동시에 나타나는 버그는 꽤 유명했는데 19xx나 20xx 사이에서 드디어 고쳐진 것 같다. 요즘은 눈에 띄지 않는다.
    하지만 caret (cursor)이 여섯 번 정도 깜빡이다가 깜빡임이 중단되는 버그는 여전히 건재한 듯하다. Windows 3.1 이래로 이런 특이한 현상은 처음이다.

  • 가~~~끔. 컴을 절전 모드에서 꺼냈을 때나 프로그램 창을 전환했을 때.. 이미 띄워져 있는 프로그램 창들이 부르르 다시 그려지면서 깜빡이고 떨리는 현상.
    내 개인용 컴과 회사 컴이 모두 그러는데 좀 보기 거슬린다.
    윈10 초창기에는 이런 현상이 없었고, 특정 컴에서만 그러는 것 같지는 않은데 왜 그러나 모르겠다. (2004대 버전 기준)

  • 컴퓨터에 따라서 케바케이긴 하지만.. 메뉴가 '파일, 편집' 같은 라벨의 우측 하단 ┗ 모양이 아니라 ┛ 왼쪽으로 펼쳐지는 기괴한 현상이 왜 발생하는지 모르겠다. 아랍권도 아닌 멀쩡한 한국어/영어에서 말이다.
    로컬라이즈나 사용 접근성을 위해 필요한 옵션이라면 제어판에라도 옵션을 정식으로 갖다 놓든지..? 저런 동작이 왜 존재하는지도 모르겠고, 또 사용자의 동의 없이 불쑥 바꿔 놓고는 원상복구를 왜 레지스트리 수정을 통해서 해야 하는지 모르겠다.

Posted by 사무엘

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

1. 비트 연산 관련 버그

프로그래머가 살면서 설마 컴파일러의 버그를 볼 일이 얼마나 될까? 이건 마치 버스· 트럭· 택시 등 운전으로 먹고 사는 기사 아저씨가 잘 가다가 차량의 엔진 결함이나 급발진을 경험하는 것만큼이나 끔찍한 경험일 것이다.

본인은 최적화 옵션을 빡세게 주고 나면 Visual C++ 컴파일러가 비트 연산 쪽으로 유난히도 말귀를 못 알아먹는 현상을 종종 목격했다.
7년쯤 전에 VC++ 2010 기준으로 (1) bit rotate 연산을 <<, >> | 따위로 구현한 게 제대로 동작하지 않는 것을 목격했다. 그 함수만 #pragma를 줘서 최적화를 강제로 꺼야 오류가 발생하지 않았다.

그리고 2019년쯤에는 (2) WORD, BYTE 따위를 비슷한 연산으로 한데 합쳐서 DWORD를 만들려고 했는데.. 이것도 변수 내용을 강제로 로그를 찍으면 문제가 없지만 간단하게 값만 되돌리게 하면 틀린 값이 돌아왔다.
인라인 함수, 매크로 함수, 최적화 강제 해제 등 별별 방법을 써도 소용없어서 결국은 무식하게 memcpy로 값을 오프셋별로 강제 복사해서 문제를 회피해야 했다.

그 뒤, 19.5.x급으로 그 당시로서는 최신 업데이트가 적용됐던 Visual C++ 2019에서 더욱 황당한 일을 겪었다.
내가 하고 싶은 일은 8비트 char 값을 그대로 부호 없는 형태로만 바꿔서.. 즉, -3을 253으로만 바꾼 뒤 다른 산술 연산 처리를 하는 것이었다. 그런데 (3) 컴파일러가 말귀를 못 알아듣고 숫자를 32비트로 취급하면서 앞에 0xFFFFFF00를 제멋대로 붙였다.

숫자는 내가 기대한 것보다 엄청나게 큰 값으로 바뀌었으며, 프로그램은 이 때문에 오프셋 계산을 잘못해서 메모리 오류가 발생했다. 내가 아무리 강제 형변환 연산을 집어넣어 줘도 오류는 없어지지 않았다. 계산값에다가 원래는 할 필요가 없는 &0xFF 필터링을 강제로 하거나, 이 역시 최적화를 꺼야만 오류가 사라졌다. 이런..

이 세 사례는 모두 비트 연산 + 최적화와 관련된 컴파일러의 난독증이라는 공통점이 있었다. 2010으로 32비트 코드를 빌드하던 시절이나, 2019로 64비트 코드를 빌드하던 시절이나 마찬가지이니.. 딱히 버전과 아키텍처를 가리지도 않는 것 같다.

더 자세한 정황을 나열하지 못하는 이유는 이것들이 전부 방대한 회사의 코드를 취급하다가 발생한 일이기 때문이다. 그래서 동일 문제를 재연할 수 있는 최소한의 케이스를 따로 분리할 수가 없다. 그 함수만 텅 빈 프로젝트에다가 떼어내서 돌리면 당연히 문제가 발생하지 않는다.
하지만 동일 코드를 사용하여 macOS, 안드로이드 등 타 플랫폼에서 돌아가는 제품에서는 버그가 발생하지 않으니 이건 일단 Visual C++만의 문제라고 봐야 할 듯하다.

2. UTF-8 지원 여부와 미스터리한 오동작

Windows는 전통적으로 ANSI 인코딩(?) 천국이던 운영체제였다. 그래서 유니코드 자체는 진작부터 지원했지만 UCS-2 내지 UTF-16 같은 별도의 2바이트 단위 인코딩 형태로만 지원하는 것을 선호했다. 1바이트 단위 인코딩인 UTF-8의 형태로 지원하는 것에는 대단히 보수적이고 인색했다.

오죽했으면 Visual C++이 취급하는 리소스 스크립트 *.rc라든가 resource.h의 기본 포맷도 유니코드 기반으로 바뀌긴 했는데.. UTF-8이 아니라 UTF-16으로 바뀌었다. 거 참..

그래도 세월이 흐르니 마소에서도 대세를 거스를 수 없는지라, 명령 프롬프트에서 제한적이나마 65001 UTF-8 코드 페이지를 지원하기 시작했다. Windows 10 19xx 버전부터는 메모장이 기본으로 지정하는 텍스트 저장 인코딩이 UTF-8로 바뀌기도 했다.
심지어 Visual C++ 컴파일러 역시 UTF-8 인코딩의 소스 코드를 인식하기 시작했다. 단...!! 이건 2% 부족한 아쉬운 면모가 좀 있다.

바로.. 파일 앞부분에 BOM이 있을 때만 UTF-8로 인식한다는 것이다. 그렇지 않으면 그냥 ANSI이다.
소스 코드의 인코딩을 강제로 지정하는 옵션이 소스 코드 내부에 #pragma 같은 형태로 좀 있었으면 좋겠지만 그렇지는 않다. #pragma code_page라는 게 있긴 한데, C 문법을 일부 빌려 온 리소스 스크립트에만 쓰인다.
파일 내부 대신, 컴파일러의 옵션으로 /source-charset:utf-8 요런 게 존재하고, 줄여서 그냥 /utf-8이라고만 해도 된다.

생각해 보면 설정이 하나만 있는 것으로 충분하지 않다. 소스 코드 자체는 인코딩이 UTF-8인데 그 안에서 L로 둘러싸이지 않은 "한글"이라는 문자열 리터럴은 KS X 1001로, 즉 길이가 4바이트이고 전체 크기가 5바이트인 문자열을 의도한 것일 수 있다. 그렇게 실제로 의도된 인코딩을 지정하는 옵션은 /execution-charset이라고 따로 있으며, /utf-8은 두 charset을 모두 utf-8로 지정한 것과 같은 효과를 낸다.

그런데 컴파일러는 그렇게 인식시키면 되지만 에디터의 동작에 여전히 함정이 남아 있다.
BOM도 없고 딱히 한글· 한자 같은 문자도 없이 모든 문자열이 간단한 1바이트 숫자· 알파벳 따위로만 구성된 평범한 파일의 경우, Visual Studio IDE는 얘를 기본적으로 ANSI 인코딩 파일로 간주한다. 그 파일에 나중에 한글· 한자가 부주의하게 추가된다면 인코딩이 영락없이 잘못 지정될 수 있다. 이 기본 동작을 고치는 방법이 있는지는 난 아직 모르겠다.

그런데 그렇다고 BOM을 넣어 버리면..?? BOM은 Windows 동네에서나 통용되지, 리눅스 등 타 운영체제에서는 그냥 민폐 덩어리인 문자이다. 소스 파일의 앞에 저런 문자가 떡 있으면 컴파일러가 잘못 먹고 체하는 수가 있다.
그러니 한 소스를 여러 플랫폼에서 공유하는 경우, 모든 코드의 인코딩은 그냥 닥치고 BOM 없는 UTF-8로 통일하는 게 안전하다. 이 문제에 관한 한은 Visual C++이 타 빌드 툴들의 표준 관행에 맞춰 줘야 한다. BOM는 이식성을 저해하기 때문이다.

모종의 이유로 인해 Visual C++에서 소스 코드의 인코딩이 잘못 인식되면 빌드 과정에서 깨진 문자가 있다고 C4819라는 경고가 발생한다. 깨진 문자가 주석 내지 조건부 컴파일에 걸려서 어차피 빌드되지 않는 영역에 있을 때는 저게 딱히 문제될 게 없다. 단지, 문자열 리터럴 내부에 들어있던 한글· 한자가 깨지면 심각한 문제가 될 것이다.

그런데 내 경험상.. 주 번역 단위에 해당하는 소스 파일과, 걔가 인클루드 하는 헤더 파일 간에 인코딩이 다를 때도 상당히 골치 아픈 문제가 발생하곤 했다.
C4819 말고도 C4828이라고 파일의 줄 수가 아닌 오프셋 운운하면서 굉장히 기괴한 경고가 떴다. 최신 컴파일러에서는 이 경고가 삭제되었는지 조회되지도 않더라.

그리고 정말 믿을 수 없지만 컴파일러가 완전히 뜬금없는 에러를 내면서 동작을 멈췄다. 실제로 문법 오류가 전혀 없는 구문에서도 쓸데없는 에러가 발생했으며, 그 소스 파일에 실제로 존재하지 않는 칸 번호를 언급하기도 했다.
이렇게만 말하는 나도 황당하고 읽는 분들도 상황을 받아들이지 못하시겠지만.. 내가 실제로 겪은 상황이 저랬다.

이 역시 회사에서만 겪었기 때문에 정확· 엄밀하게 재연 케이스를 만들지는 못하겠다. 아까 얘기했듯이 (1) /utf-8 옵션을 global하게 준 상태에서 소스와 헤더 파일들의 인코딩이 충돌 난 것, 그리고 아마도 (2) precompiled 헤더를 쓰는 소스와 그렇지 않은 소스가 한 프로젝트 안에서 좀 뒤섞여 있는 것, (3) namespace와 using이 좀 복잡하게 얽혀서 인텔리센스도 오락가락 하는 상황인 것이 다 조금씩 영향을 주지 않았을까 생각된다.

이 난국은 모든 코드의 인코딩을 BOM 없는 UTF-8로 정리하고, 모든 코드에다가 한글로 dummy string을 만들어서 Visual Studio IDE가 파일을 ANSI (cp949) 인코딩으로 잘못 저장하는 일이 없게 조치를 취함으로써 해결되긴 했지만..
그때 그 문제가 왜 발생했으며 그 상황을 어떻게 재연할 수 있는지는 모른 채 미스터리로 남게 되었다.

회사에서는 길지 않은 기간 동안에도 이 정도의 이상한 버그를 몇 차례 경험했는데.. 개인적으로 날개셋 한글 입력기를 20여 년 가까이 만들어 온 동안은 컴파일러의 버그를 경험한 적이 거의 없다는 것이 참 신기하다. IDE야 불필요하게 다운되거나 뻗는 버그를 여럿 경험했지만 컴파일러가 문제를 일으킨 적은 없었다.
모든 코드가 깔끔하게 KS X 1001 레거시 인코딩이고, 회사 코드보다는 규모가 작고 모듈 구조가 깔끔하고, 전부 precompiled 헤더를 사용하기 때문이 아닌가 생각한다.

소스 코드의 인코딩이 UTF-8이 아니거나, UTF-8이더라도 앞에 BOM이 있는 것 자체를 경고로 처리하는 건 너무 과격할까? 그리고 #include에서 경로 지정을 /가 아닌 \로 한 걸 경고로 처리하는 옵션도 있으면 좋겠다. 이런 건 Windows 환경에서나 통용되지 밖에서는 전부 민폐 에러 요인이 되기 때문이다. 본인이 직장의 공동 작업 과정에서 종종 실수했던 적도 있는 사항들이다.

3. 인텔리센스의 오동작

끝으로, 이건 실제로 생성된 exe/dll의 동작과 관계 있는 치명적인 문제는 다행히 아니지만.. Visual C++ IDE가 텍스트 에디터에서 사용하는 인텔리센스도 일부 특이한 상황에서는 말귀를 못 알아듣고 오동작할 때가 있다.

본인이 겪은 경우는 클래스(가령 A)의 선언 내부에 MFC의 DECLARE_DYNAMIC 같은 복잡한 custom 매크로를 넣은 뒤, 곧장 private/public/protected 같은 접근 권한 지정자가 나올 때이다. 그러면 인텔리센스가 그 뒤에 이어지는 멤버 및 내부 enum/class (가령 B) 따위 선언을 파싱을 제대로 못 한다. ClassView를 보면 A의 멤버 목록에 B의 멤버들이 잘못 표시되며, B 선언 이후에 등장하는 A의 진짜 멤버들은 전혀 인식되지 않는다.

ClassView뿐만 아니라 텍스트 에디터에다 불러온 소스 코드에서도 각종 경고와 에러 밑줄이 A의 멤버들이 누락된 것처럼 쭈루룩 뜬다.
그렇기 때문에 A 클래스의 구현부에서는 인텔리센스와 자동 완성, 심벌 위치 조회 같은 기능들을 활용하지 못하면서 코딩을 꽤 불편하게 해야 한다.

이런 초보적인 문제는 Visual C++ 6 ncb 시절에나 보던 게 아니었나? 왜 발생하는지 모르겠다.
최신 업데이트를 적용한 Visual C++ 2019에서도 동일하게 발생한다. 본 컴파일러가 아니라 인텔리센스 컴파일러이니 딱히 특정 Visual C++ 컴파일러 툴킷만의 문제도 아닐 것이다.

뾰족한 해결책은 없고, 인텔리센스를 헷갈리게 하는 그 문제의 매크로를 클래스 선언의 맨 앞이 아니라 맨 뒤로 옮김으로써 문제를 회피할 수 있었다. 흠...

4. 도킹 하다가 뻗음

역시 컴파일러가 아닌 IDE 얘기이고, 옛날 버전에서만 발생하는 문제이기 때문에 지금 큰 의미는 없지만..
Windows 10 19xx대 버전부터인가 Visual Studio 2013 (그리고 아마 2015도)에서 각종 문서 편집 창이나 보조 윈도우(출력, 속성, 디버그 등등)를 어디에든지 도킹을 해서 붙이면 프로그램이 뻗어 버린다.

2010이 언제부턴가 실행될 때 Microsoft.Vsa.tlb 파일이 없다는 에러를 내는 것과 비슷한 현상인 것 같다. 그래도 얘는 정상 실행은 되고 프로그램 사용에 문제가 없는 반면, 저건 창을 내 마음대로 배치할 수 없게 만들고 프로그램이 뻗기까지 하기 때문에 상당히 심각한 문제이다.
저런 단순 UI는 운영체제건 VS건 한번 만들고 나서는 고칠 일이 없는 기능일 것 같은데.. 둘 다 내부적으로 뭘 건드리길래 이런 부작용이 발생하는 걸까..??

하긴, 더 옛날엔 Visual Studio 2005도 Windows Vista에서 실행하려면 sp1에다가 Vista 지원 추가 패치까지 설치해야 겨우 돌릴 수 있었다. 아래아한글 2005와 2007도 Vista 이후의 운영체제에서 실행하려면 업데이트부터 대판 설치해야 했었으니 이런 예가 전혀 없지는 않구나.

어떤 프로그램이 후대의 운영체제에서 단순히 GUI나 외형의 glitch 정도가 발생하는 걸 넘어 아예 뻗고 실행이 안 되는 건.. 대부분 보안 강화 때문이지 싶다. 문서화되지 않고 미래에 얼마든지 달라질 수 있는 특성이나 동작에 의존하게 프로그램이 만들어진 경우야 걔의 잘못이겠지만, 흔한 경우는 아닐 것이다.

Posted by 사무엘

2021/05/19 08:35 2021/05/19 08:35
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1889

프로그래밍에서 메모리를 가리키는 포인터라는 건.. 그 특성상 돌아가는 컴퓨터의 machine word와 크기가 동일하다. 하지만 현실에서 포인터(= 메모리 주소)를 구성하는 모든 비트가 골고루 쓰이는 일은 몹시 드물었다.

먼저, 컴퓨터의 실제 메모리 양이 포인터가 가리킬 수 있는 범위보다 훨씬 적다. Windows의 경우, 32비트 시절에는 user mode에서는 대부분의 경우 포인터의 상위 비트가 언제나 0이었던 것이 잘 알려져 있다(하위 2GB까지만 사용).
하물며 64비트는 공간이 커도 너무 크기 때문에 가상 메모리 관리 차원에서도 아직은 40~48비트까지만 사용한다. 상위의 무려 16비트가량이 쓰이지 않는다는 것이다. 램이 32GB여도 겨우 35비트면 충분하니까..

가난하고 배고프던 20세기 16비트 시절에는.. 반대로 포인터 하나만으로 겨우 몇백 KB~수 MB 남짓한 메모리도 한번에 다루지 못했다. 그래서 far 포인터니 huge 포인터니 별 삽질을 다 해야 했는데 그때에 비하면 지금은 격세지감이 따로 없다.

저렇게 상위 비트뿐만 아니라 하위 비트도 마찬가지이다. padding, align 같은 이유로 인해, 메모리 할당 함수의 포인터 리턴값이 홀수가 될 일은 일반적으로 없다. 아니, 겨우 2의 배수가 아니라 4나 8의 배수가 될 수도 있으며, 이 경우 하위 2~3개 비트도 0 이외의 값을 가질 일이 없게 된다.

그러니 포인터를 저장하는 공간에서 0 이외의 값이 들어올 일이 없는 비트에다가 자신만의 정보를 넣는 꼼수를 부리는 프로그램이 예로부터 줄곧 존재해 왔다.
이거 무슨 변태 같은 짓인가 싶지만.. 이제 막 32비트로 넘어가긴 했지만 아직 가정용 컴퓨터들의 평균적인 메모리 양이 수 MB대밖에 안 됐던 시절이 있었다. 이때는 메모리가 부족해서 하드디스크 스와핑이 일상이었다. RAM을 1바이트라도 더 아끼는 최적화가 필수였다.

가령, 다재다능한 자료구조인 빨강-검정 나무를 생각해 보자.
노드의 색깔을 나타내는 겨우 1비트짜리 정보를 위해서 굳이 bool 멤버를 추가하는 건 굉장한 낭비라는 생각이 들지 않는가? 단 1비트 때문에 구조체 패딩까지 감안하면 무려 2~4바이트에 달하는 공간이 매 노드마다 허비되기 때문이다.
안 그래도 노드의 내부엔 left/right 같은 딴 노드 포인터가 있을 것이고, 포인터 내부에 쓰이지 않는 1비트 공간이 있으면 거기에다 색깔 정보를 박아 넣고 싶은 생각이 들 수밖에 없다. 비트필드와 포인터의 union 써서 말이다.

물론, 그렇게 0으로만 채워지던 공간을 운영체제에서도 나중에 유의미하게 사용하기 시작하면.. 그 꼼수 프로그램은 재앙을 맞이하게 된다.
대표적인 예로 마소에서는 32비트 기준으로 사용자:커널이 통상적인 2GB:2GB가 아니라 3GB:1GB로 주소 공간을 분할하는 기능을 Windows에다가 추가했다.

이러면 사용자 모드의 포인터도 2GB가 넘는 영역에 접근할 수 있으며 최상위 비트가 1이 될 수 있다. 그런데 포인터의 최상위 비트를 자기 멋대로 사용하고 있는 프로그램은.. 뭐 메모리 뻑나고 죽을 수밖에 없다.
64비트 환경에서는 겨우 1비트가 아니라 상위 word 전체를 다른 용도로 전용해도 당장 이상이 없으며 이 추세가 앞으로 몇 년은 가지 싶다. 컴퓨터의 램이 256~512GB나 1테라까지 간다면 모를까..

요즘 컴퓨터야 메모리가 워낙 많고 풍족하니, 굳이 저런 꼼수를 동원하는 프로그램은 별로 없을 것이다.
하지만 저 때가 되면 또 꼼수 부리는 말썽꾸러기 프로그램과의 호환성 때문에 주소 공간을 옛날처럼 상위 16~32GB까지로 봉인하는 옵션 같은 게 또 등장할지도 모른다.;;; HIGH_DPI_AWARE처럼 LARGE_ADDRESS_AWARE 시즌 2 말이다.

여담이지만 Windows의 경우, 실행 파일은 시작 주소가 언제나 64KB의 배수 단위로 부여되기 때문에 HINSTANCE/HMODULE은 아래쪽은 무려 word 덩어리가 언제나 0이 된다. 이 특성을 이용해서 운영체제의 LoadLibraryEx 함수도 하위 몇 비트를 자기 마음대로 활용하기도 한다.

※ 나머지 메모

(1) unsigned 타입에 대해서 단항 연산자 -를 적용해서 -a 이런 값을 구하는 코드를 우연히 보고는 개인적으로 신박하다는 생각이 들었다. 흐음~ Visual C++의 경우 이건 원래 경고인데, 요즘 버전에서는 더 엄격하게 에러로 처리하는가 보다.
-a는 2의 보수의 특성상 ~a+1과(비트 not보다 1 크게) 완전히 동일한 효과를 내며, 앞에 0을 붙여서 이항 연산자로 만들어도 에러를 회피할 수 있다.

(2) ANSI C에서는 함수의 prototype을 선언할 때 매개변수 리스트에 타입만 써 넣고 이름을 빼먹으면 안 된다는 걸 최근에야 알게 됐다.
아니 도대체 왜..? 거기서 매개변수의 이름은 거의 잉여 옵션에 불과할 텐데.. void func(int);라고만 쓰면 틀리고 void func(int x);라고 아무 이름이라도 붙여야 된다는 것이다.
이건 먼 옛날에 C언어에서 void func(a) int a; 같은 구닥다리 문법이 쓰이던 시절의 잔재인 것 갈다.

Posted by 사무엘

2021/05/15 08:35 2021/05/15 08:35
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1887

컴퓨터그래픽에서 벡터 그래픽의 반의어로 픽셀과 비트맵을 다루는 체계를 래스터 그래픽이라고 흔히 부른다. 종이가 아니라 해상도가 상대적으로 낮은 모니터 화면이 주 무대이고, 면을 채우는 기본 단위가 scan line(주사선)이라는 관점에서 정립된 용어이다.

그리고 2D 비트맵(더 정확한 명칭은 래스터..?) 그래픽 API를 보면 어떤 플랫폼용 어떤 언어의 라이브러리이든지 점과 직선, 곡선을 그리는 함수가 있고, 사각형과 원을 그리는 함수가 있다. 이게 기본이다.
점이나 사각형이야 그리는 방식이 너무 trivial하니 제끼고, 원이나 곡선을 빠르게 그리는 원리는 기하 알고리즘의 일종으로 다뤄지기도 한다. 그 단순한 직선조차도 굵기가 2픽셀 이상이 되면 중심점을 생각해야 할 것이고, 무거운 부동소수점 연산 없이 anti-aliasing까지 하면서 그린다는 조건이 추가되면 결코 쉽지 않은 일이 된다.

그리기 기능 중에서 특정 픽셀부터 시작하는 flood fill은 무척 독특한 동작이다. 기하 알고리즘이라기보다는 스택 메모리를 동원해서 컴에게 길 찾기 재귀호출 노가다를 시키는 코딩의 영역이다. 빼곡한 미로의 내부에 있는 한 점에서 flood fill을 시켜 보면 이건 본질적으로 길 찾기와 다를 바 없다는 걸 알 수 있을 것이다.

글쎄, flood fill은 그래픽 에디터에서 사용자가 내리는 채우기 명령을 구현하는 형태로나 쓰이지, 직선과 곡선, 사각형과 원처럼 그림을 그리는 구성요소로서 프로그램이 내부적으로 사용할 일은.. 정말 아주 특수한 상황이 아니라면 없을 것이다. 도형 자체를 처음부터 내부가 채워진 형태로 그려야지, 도형의 윤곽만 그린 뒤에 도형 내부의 임의의 점을 따로 주고 채우는 건 몹시 비효율적이기 때문이다.

그래서 그래픽 라이브러리에는 다각형을 그리는 함수가 있다. 다각형의 경계선만 찍찍 그리는 것이야 LineTo만으로 얼마든지 할 수 있으므로, 이런 함수는 내부가 채워진 다각형을 그리는 것이 핵심이다. 그러니 이 함수는 다른 함수와 달리, 반드시 다각형의 꼭지점들이 담긴 배열을 전달받아야 한다.
옛날 도스 시절의 베이식은 타 언어들에 비해 그래픽 모드의 접근성이 좋았지만, 정작 다각형을 그리는 API는 없었다.

그럼 다각형을 채우는 기능은 어떤 방식으로 동작하는 걸까?
이걸 구현하기 위해서는 어떤 점이 다각형의 내부에 속하는지를 판단해야 한다. 더 나아가서 이 점에서 한쪽으로 scan line을 그어 나갈 때 어디까지가 동일하게 다각형의 내부 또는 외부인지를 판단해야 한다.

이걸 판단하는 방법은 의외로 간단하다. 그 점으로부터 아무 방향으로(예: x축 양의 방향) 한없이 직선을 그을 때, 그 선이 다각형을 구성하는 선분과 얼마나 몇 번이나 마주치는지를 판단하면 되며, 이걸 판단하는 방법도 크게 두 갈래로 나뉜다. 바로 (1) 홀짝 아니면 (2) 0여부이다.

홀짝법은 마주친 선분이 짝수 개이면 다각형의 외부이고, 홀수 개이면 내부라고 판단한다. 다시 말하지만 이 가상의 선은 정말 아무 방향으로나 그리면 된다. 다각형이 모든 방향으로 닫혀서 내부에 공간이 존재한다는 사실 자체가 이 판별법의 correctness를 보장해 준다.

0여부는.. 홀짝보다 더 절묘하다. 초기값이 0인 가중치라는 걸 두는데, 마주친 선분이 우리가 그은 가상의 선을 위에서 아래로 교차한다면 가중치에 1을 더한다. 그렇지 않고 아래에서 위로 교차한다면 1을 뺀다.
이렇게 해서 최종적으로 가중치가 양수든 음수든 0이 아닌 값이 나온 점은 다각형의 내부라고 간주하고, 0인 점은 외부라고 간주한다.

0이나 홀짝이나 그 말이 그 말 같은데.. 실제로 자기네 선분끼리 배배 꼬아서 교차하지 않는 일반적인, 평범한 오목/볼록다각형이라면 어느 판별법을 사용하든 결과에는 아무 차이가 없다.
하지만 당장 오각형 별표를 한붓그리기로 그린 궤적을 줘 보면 둘은 서로 차이를 보인다.

사용자 삽입 이미지
Windows API에서는 SetPolyFillMode라는 함수가 있어서 두 방식을 모두 사용해 볼 수 있다. 더 단순한 홀짝법이 ALTERNATE이고 기본값이다. 0여부는 WINDING... Windows 1.x 시절부터 존재해 온 오래된 고전 API여서 그런지, 매크로 상수의 앞에 접두사가 붙어 있지도 않다(PFM_* 같은?? ㅎㅎ).

오각형 별표에서 별의 중앙에 생긴 공간을 보면.. 그 옆으로 다각형 경계를 나타내는 선이 어느 방향이든 두 개가 존재한다(짝수). 그런데 이들은 방향이 둘 다 오르막 아니면 둘 다 내리막이며, 이 때문에 winding value는 nonzero가 된다. 그러니 ALTERNATE일 때는 이 공간이 비워지지만 WINDING일 때는 공간이 채워지는 것이다.

그 위의 더 복잡한 꼬인 사각형도 상황이 비슷하다. 잘 살펴보면 이 궤적도 홀수점이란 게 전혀 존재하지 않으며 한붓그리기가 가능하다.
그런데 WINDING일 때는 궤적이 꼬여서 생긴 내부의 사각형 공간 둘 중에서 좌측 하단 한 곳만 채워져 있다. 그 이유는 역시 저기서만 winding value가 nonzero이기 때문이다.

일반적으로 WINDING(0여부)이 판정하는 다각형 영역은 ALTERNATE(홀짝)의 상위 호환이다. ALTERNATE가 판정하는 영역을 100% 포함하면서 일부 영역을 추가적으로 더 판정한다는 뜻이다. 그렇다고 해서 모든 닫힌 영역을 한 치의 예외 없이 몽땅 내부라고 판정하는 건 아니다.

뭐.. 현실의 벡터 그래픽에서 이 따위 선끼리 교차하는 배배 꼬인 폴리곤을 생성하는 것은 애초부터 권장되지 않는 금지 사항이다. 가령, 속이 빈 오각별을 그리고 싶으면 저렇게 보이는 대로 삼각형 다섯 개로 풀어서 표현하라는 것이다. 윤곽선 폰트 등 벡터 그래픽 편집기들은 그렇게 폴리곤의 모양을 자동으로 수정해 주는 기능도 제공한다.
그러니 이렇게 fill mode의 차이점을 미주알고주알 관찰할 일이 현업에서는 거의 없을 것이고, 이런 건 그냥 학교에서 컴퓨터그래픽스 기초를 공부할 때 이런 방식도 있다는 걸 알기만 하고 넘어가면 될 것 같다.

하지만 그게 전부가 아니다. 다각형 채우기의 기능이 더 확장되면 다음 영역에도 도달하는데, 이때 fill mode의 차이점이 다시 드러나게 된다.

1. 여러 다각형을 한꺼번에 그리기
이건 내부에 구멍이 뚫린 다각형을 그릴 수 있다는 것에 의의가 있다. 구멍은 Polygon 함수를 연달아 호출하는 것으로는 표현할 수 없기 때문이다.

Windows에는 여러 다각형을 한꺼번에 그리는 PolyPolygon이라는 함수가 있다. 그런데 아까처럼 한 다각형에서 변들이 서로 교차하고 꼬였을 때뿐만 아니라, 변은 꼬이지 않았고 여러 다각형들의 영역이 서로 겹칠 때에도 fill mode의 차이는 유의미한 동작의 차이를 만들어 낸다.

사용자 삽입 이미지

위의 그림은.. 뭐 이론적으로는 한붓그리기가 가능하기 때문에 역시 꼬인 단일 다각형으로 궤적을 나타낼 수 있다. 하지만 앞서 예를 들었던 오각별이나 그 사각형 그림과 달리, 일부 점과 점이 겹치는 건 피할 수 없을 것이다. 무슨 말인가 하면, 저 궤적을 꼭지점 좌표의 배열로 기술했을 때, 4개의 선분과 만나는 점은 두 번 등장하는 부분이 생긴다는 것이다.

꼬인 단일 다각형이 아니라 영역이 일부 겹치는 사각형과 삼각형을 서로 떼어서 PolyPolygon으로 그린 경우.. ALTERNATE(홀짝)에서는 짝수 개의 다각형에 속하는 영역은 비우고, 홀수 개에 속하는 영역만 칠한다. 그러고 보니 동작이 뭔가 XOR스러워 보인다. 각 다각형들의 꼭지점이 기술된 방향은 어느 쪽이건 무관하다 (시계 or 반시계 방향)

그러나 WINDING(0여부)일 때는 그 특성상 방향이 같은 다각형들은 겹치더라도 영역을 모두 칠한다. 겉의 껍데기가 시계 방향이라면.. 그 안의 구멍은 반시계 방향으로.. 다른 방향으로 칠해져야 구멍이 비게 된다! 다시 말하자면, WINDING에서도 위의 그림의 왼쪽처럼 중앙이 비어진 그림을 그리고 싶다면 사각형과 삼각형의 좌표 방향이 서로 반대여야 한다.
꼬인 단일 다각형에서 fill mode의 차이점을 설명하는 프로그래밍 서적들이.. 다중 다각형까지 연계해서 동일 개념을 설명하는 경우는 내가 딱히 못 본 것 같다.

2. 직선뿐만 아니라 베지어 곡선까지 포함된 궤적의 내부를 채우기
위와 같은 구멍 감지에다가 곡선 지원까지 포함되면.. 이건 뭐 윤곽선 글꼴 래스터라이저가 번듯하게 완성된다. 물론 본격적인 폰트 엔진은 거기에다 작은 크기에 대비한 정교한 안티앨리어싱과 힌팅, 글꼴 글립 캐시, 더 나아가 복잡한 유니코드 문자 형태 분석까지 추가되는데 이것들 하나하나가 별개의 전문 영역일 정도이다.

FreeType 라이브러리는 그 중에서 제일 저수준인 그리기, 안티앨리어싱, 힌팅까지만 담당한다. 요즘 소프트웨어들은 글자 하나를 찍는 것도 겨우 8*16, 16*16 비트맵 글꼴 찍던 시절과는 차원이 다르게 더 복잡해져 있는 셈이다.
그건 그렇고.. Windows API에는 직선과 곡선이 포함된 도형을 한꺼번에 그리는 것은 윤곽선만으로 한정이다. PolyDraw라는 함수가 있다.

내부를 채우는 것은 한 함수로 지원되지 않으며, path라는 걸 써야 한다. 얘는 Windows GDI가 제공하는 강력한 벡터 그래픽 라이브러리로, 직선, 베지어 곡선, 원과 원호, 심지어 다른 트루타입 글꼴의 글립까지 몽땅 궤적으로 표현해서 한꺼번에 내부를 채울 수 있다. 구멍 처리도 물론 된다.
BeginPath (그리기) CloseFigure (그리기) EndPath 이런 식으로 말이다. 위의 1과 2를 모두 할 수 있다.

내 경험상 트루타입 폰트는 WINDING 방식으로 래스터라이징을 한다. 글꼴 글립을 그릴 때부터 제일 밖의 path는 시계 방향이고, 그 안의 구멍 윤곽을 기술하는 path는 반시계 방향이고, 구멍 안의 칠하는 영역은 또 시계 방향.. 이런 식으로 디자인을 해야 한다.

허나, 예전에 MS Office 2003 이하 버전에서 제공되던 클래식 WordArt는 이 원칙을 지키지 않고 트루타입 글꼴도 홀짝 ALTERNATE 방식으로.. 짝수 회 overlap 영역은 무조건 비웠던 것 같다.
그래서 composite glyph 형태로 표현되는 비완성형 한글 글꼴에서 글립이 겹칠 수 있는 복잡한 글자를 찍어 보면 저렇게 흰 부위 glitch가 발생하곤 했다. (아래 그림에서 ㅆ, ㅠ, ㅔ 부분 참고)

사용자 삽입 이미지

Office 2007 이상부터 제공되는 WordArt는 이 문제가 해결됐다. 그리고 아래아한글의 글맵시도 0여부 WINDING 방식으로 맞게 색칠을 하기 때문에 glitch가 발생하지 않는다.

그러고 보니.. MS Office는 지난 2007때부터 그래픽 엔진이 크게 바뀌었다. 워드아트의 글자 장식 기능도 리뉴얼 됐고 PowerPoint 같은 데서도 직통으로 사용 가능해졌는데, 정작 본가인 Word에서는 2003 이하의 클래식 워드아트가 제공됐다. 다음 버전인 Office 2010부터 Word에서도 동일하게 리뉴얼된 워드아트가 제공되기 시작했다.

Posted by 사무엘

2021/05/12 08:35 2021/05/12 08:35
, , ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1885

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

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

블로그 이미지

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

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

«   2022/08   »
  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:
1855574
Today:
431
Yesterday:
598