macOS 프로그래밍

1.
근래에는 회사 업무 때문에 드디어 맥OS에다가 xcode까지 좀 건드릴 일이 있었다. 작년에 애플에서는 자기네 PC용 운영체제의 공식 명칭을 macOS라고 붙이면서 mac이라는 단어를 다시 복귀시켰던데, 이건 잘한 조치라고 생각된다. mac을 빼고 OS X라고 하는 건 영 아니었다. 무슨 OS/2도 아니고. 물론 걔네들 입장에서는 iOS 같은 타 기기용 운영체제와 명칭 표기를 통일하느라 mac을 소문자 형태로 살린 것이었다.

맥OS에서 메뉴를 꺼내는 단축키는 웬 뜬금없는 Ctrl+F2이구나(Win은 F10 또는 Alt). 그리고 한 프로그램 안에서 문서 창을 전환하는 단축키는 Cmd+` 이다(Win은 Ctrl+Tab 또는 Ctrl+F6). 이런 왕초보 기초부터 다시 시작했다.

Visual Studio와 C++과는 너무 다른 프로그래밍 방법론이 여전히 적응이 안 됐지만.. 나름 맥OS에 대한 이해가 예전보다는 더 깊어질 수 있었다. NextStep에서 딴 NS... 이런 명칭은 게임브리오 소스에 있는 NI... (넷이멀전) 접두사와 비슷한 느낌이 들었다. N으로 시작하고, 지금은 대외적으로 쓰이지 않는 이름. 마치 MFC의 Afx처럼 말이다.

한번은 각종 설정들을 이것저것 건드린 뒤부터 멀쩡한 프로젝트에서 정체를 알 수 없는 링크 에러가 나서 한참 고생한 적이 있었다.
링크 에러라면 당연히 extern "C"처럼 함수 호출 규약이나 심벌 decoration 방식의 충돌이 1순위로 의심되겠지만, 알고 보니 프로젝트 파일 리스트와는 별개로 관리되는 빌드 대상 목록에서 몇몇 소스 파일이 실수로 누락되어 벌어진 일이었다. 둘이 동일한 개념이 아니었 것이다.

하긴 Visual Studio도 각각의 파일들에 대해서 속성을 줘서 exclude from build를 지정하는 게 있긴 했다. 그걸 몰라서 딴 데서 한참을 헤맸다.
IDE에서 각종 경고를 출력하는 인텔리센스와 문맥 감지 색깔 처리가 정상적으로 되고 있어서 이 파일이 애초에 컴파일이 되지 않고 있다는 건 상상을 못 했었다.

2.
맥OS는 자기네 그래픽 비주얼은 그렇게도 뛰어나면서 정작 그래픽 툴을 제공하는 건 왜 그리 인색한지 모르겠다. 맥OS에는 Windows의 '그림판'에 해당하는 기본 프로그램이 내가 알기로 여전히 없다.
개발툴 중에서도 Visual Studio는 간단한 아이콘이나 비트맵 정도 편집할 수 있는 그래픽 에디터를 내장하고 있는 반면, xcode는 그런 거 없고 viewer만 있다. 비트맵 그래픽 편집을 어떻게 해야 할지 모르겠다.

그리고 또 인상적인 점으로는 맥 진영은 Windows에서는 거의 듣보잡이나 마찬가지이던 tif/tiff를 좋아하는 듯하다. 화면 캡처 기본 앱이 그림 파일을 tif로 저장할 때부터 뭔가 심상찮았는데.. 타 xcode 프로젝트들을 보니까 비트맵/아이콘에 역시 tif가 들어가 있구나.

그런데 tif도 다 같은 tif가 아닌지, Windows에서 돌아가는 타 그래픽 에디터에서 저장한 tif는 맥에서 못 읽는 것 같다.

3.
명령 프롬프트로 가 보면, 공백이 포함돼 있는 파일명을 명령 프롬프트에서 표현할 때 Windows는 파일명을 따옴표로 싸서 공백을 표현하는 반면, 맥은 그렇지 않은 듯하다. 역슬래시+공백이라는 탈출문자 기법을 사용한다. 그러니 "a b"냐 a\ b냐의 차이가 발생한다. 디렉터리 구분자부터가 슬래시이니 역슬래시를 저렇게 C스럽게 탈출문자 용도로 활용한다는 게 아주 흥미롭다.

명령 프롬프트가 현재 가 있는 디렉터리(폴더)를 기준으로 탐색기 또는 그에 준하는 파일 관리 셸을 여는 것도 자주 행해지는 작업이다. 숨김 속성 때문에 셸을 통해 접근할 수 없거나 접근 방법이 까다로운 폴더를 다루고 싶을 때 말이다. Windows에서는 start .이던데 맥은 open .이다. 리눅스는 어찌 되려나 궁금하다.

4.
그리고... 결정적으로 맥용 IME 예제도 다뤄 봤다. 골치 아픈 DLL이 아니라 쿨하게 EXE 형태이고, regsvr32 따위 할 필요 없이 그냥 프로그램을 특정 디렉터리에다 얹어 놓기만 하면 바로 IME가 동작하는 게 참 깔끔해 보였다. 여기에다가 날개셋 엔진만 얹으면 내 프로그램이 맥용으로 나오는 것도 불가능하지는 않겠다는 생각이 들었다. 물론 글자만 달랑 찍히는 수준을 넘어서 완성도 있는 제품을 만드는 건 지금 시간과 내 실력만으로는 아직 어림도 없는 요원한 일이다.

오래 전부터 인지했던 것이긴 하지만, Windows와 맥은 문자 입력 시스템을 설계한 형태가 서로 크게 다르다.
Windows는 IME가 또 내부적으로 한영 상태를 갖고 있고 자기 상태를 아이콘을 통해 출력하는 형태이다. 즉, Windows 8식 용어로 표현하자면 brand icon과 state icon이 따로 있다.
그러나 맥은 그렇지 않고 한글 입력 상태가 영문 상태만큼이나.. Windows식으로 표현하자면 별도의 input locale이다. 일단 한글 IME 상태에서 한영 키로 한영 전환을 하는 게 아니라, 입력 로케일 전환인 Ctrl+Shift가 한영 전환인 셈이다. state icon이 없고 brand 자체가 state 역할을 한다.

그러나 <날개셋> 한글 입력기는 자기 brand 하에서 2개 이상 열몇 개까지 입력 항목을 추가할 수 있는 형태이다. 이것부터 맥OS에서는 어떻게 표현을 해야 할지 모르겠다. 맥에서 <날개셋> 한글 입력기는 편집기 계층은 제대로 구현하지 못하고 그냥 입력기 계층 하나 수준에 머물러야 할 수도 있다.

맥에서는 IME가 독립된 프로그램이고 시스템 전체에서 동일한 한영 상태가 유지된다는 것도 매우 흥미로운 점이다. Windows도 IME가 애초에 이런 형태였으면 지금처럼 32비트와 64비트가 공존까지 하는 시대에 IME를 개발하기가 훨씬 더 깔끔해지지 않았을까 싶은 생각이 든다.
언제든지 자기 자신을 죽이고 재시작만 하면 업데이트도 아주 간편하게 할 수 있다. Windows는 DLL에다 memory-mapped file크리까지 겹쳐서 프로그램 강제 종료나 재시작 같은 지저분한 짓 없이는 IME의 업데이트라든가 전체 상태 동기화가 몹시 어렵다.

다만, 그 구조의 특성상 IME를 디버깅 하는 도중에 잠시 딴 프로그램에서 타 IME를 구동해서 문자를 입력하기가 좀 난감한 점은 있다. IME는 그 특성상 타 입력기로 대체만 될 뿐 '스스로 종료'라는 개념이 없는 프로그램인데, Windows에서는 자기 DLL을 사용하는 프로그램이 하나만 존재하면 그것만 끝내면 디버깅도 원활하게 종료되는 반면, 맥에서는 그런 것도 없어서 그냥 IME 프로그램을 강제 종료해야 한다.

그리고 IME 프로그램은 내 자신이 실행하는 게 아니라 운영체제가 on-demand로 구동해 주는 형태이다. 그러니 개발툴이 처음부터 IME를 디버깅 할 수 있는 게 아니라 이미 구동돼 있는 IME 프로세스에다 디버거가 붙는(attach) 식으로 디버깅을 해야 한다.
이렇게 붙으면 NSLog를 찍는 게 xcode의 output 창에 나타나질 않는 문제가 있더라. 그 이유는 모르겠다. 운영체제의 문자 입력 프로그램이라는 건 어떤 형태로 만들더라도 디버깅이 어려운 구석이 있는 듯하다. 동력분산식과 동력집중식만큼이나 서로 일장일단이 있는 셈이다.

5.
코딩을 하면 할수록 Objective C의 고유 문법과 일명 NSFramework 라이브러리는 독립된 언어라기보다는..
Windows로 치면 COM처럼, 그냥 API/라이브러리의 컴포넌트화, 그리고 운영체제-내 프로그램 간의 통신을 위한 바이너리 수준의 프로토콜에 가까운 물건이라는 생각이 든다.

쉽게 말해 NSObject는 IUnknown에, YES/NO는 S_OK, S_FALSE에, @문자열은 BSTR, SysAlloc/FreeString 등에, xib/nib는 리소스 겸 type library에 대응하는 식이다. 뭐 가상 머신이 따로 돌아가는 급은 아니지만 그래도 가벼운 garbage collector도 있다.

물론 기능 호출 방식은 서로 큰 차이가 있다. COM은 함수 포인터 기반인 C++과 더 비슷하지만 옵씨는 진짜 SendMessage 같은 방식이다.
그러니, NSObject에 뭐가 이렇게 오버라이드 가능한 메소드들이 많이 정의돼 있는지, 리스트를 보고 깜짝 놀라곤 했다. v-table 기반의 가상함수라면 상상도 못 할 일이다. MFC도 v-table 크기 부담 없이 운영체제 메시지 처리를 C++로 하기 위해 message map이라는 별도의 메커니즘을 도입한 것이다.

옵씨라고 해서 말 그대로 C만 쓸 수 있는 건 아니며 C++ 코드도 작성 가능하다. 그러니 [ ] 어쩌구로 시작하는 그쪽 ‘오브젝트’와 해당 문법은 운영체제로부터 호출을 받는 것에 대처할 일이 있을 때만 사용하게 되더라.
아무튼 지구를 떠나서 달이나 화성에서 사는 건 어렵고, Windows 홈그라운드를 떠나 타 OS에서 사는 건 여전히 몹시 어렵다!

Posted by 사무엘

2017/03/11 08:31 2017/03/11 08:31
,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1336

1. 색소폰 연주

아시다시피 본인은 Looking for you를 3천 번 들었다.
성경의 사무엘이 '사무엘아' 음성을 두 번 듣고 나서 세 번째 들은 뒤엔 출처를 공부한 뒤 들을 준비를 하고 잠자리에 누웠다. 네 번째 '사무엘아' 음성을 들은 뒤에야 하나님의 음성에 제대로 응답하게 됐다.

그것처럼 나도 새마을호에서 Looking for you를 두 번 듣고 나서 세 번째 들은 뒤엔 출처를 인터넷으로 검색했고, 다음엔 들을 준비를 하고 새마을호를 탔다. 네 번째 Looking for you가 흘러나왔을 때 나는 철도 안에서 거듭났고 철도를 내 개인의 교통수단으로 영접하는 놀라운 일이 벌어졌다.

그 뒤로 나는 Looking for you를 주선율, 주요 화음, 대략의 비트까지 다 청음 채보했다.
콩나물 대가리를 한땀 한땀 입력해 넣고 원곡과 대조하면서, 어느 기보가 원음에 더 근접한 정확한 기보인지를 고민하면서..
Looking for you 작곡자의 마음과 심정을 이해하는 자가훈련을 했다.

이 음악의 어느 부분이 나를 감화시켜서 나를 철덕으로 만들었는지, 왜 이런 결과가 야기될 수밖에 없는지를 연구했다.
그리고.. Looking for you의 주선율을 만든 악기 공부를 (잠깐 동안이지만) 시작했다.

사용자 삽입 이미지

작년 성탄절, 우리 교회 복음 전도 집회에서.
아, 교회에서 Looking for you 연주했다는 얘기는 아님. 오해 마시길..

2. 나의 등산 노트

사용자 삽입 이미지

조건부 서식이 있으니 올랐던 산들의 높이를 일목요연하게 파악할 수 있어서 매우 좋다.
이것도 중복 정보 없이 정규화가 잘된 구조로 구축하려면 산에 대한 테이블과 등산 세션과 관련된 테이블을 분리하긴 해야 하는데, 엑셀로 그것까지 하기에는 많이 귀찮지.

입산 지점에 최종적으로 어떤 교통수단을 이용해서 가서 어디로 하산했는지,
산 속에서 주로 본 게 무엇인지, 바깥 경치로 주로 무엇을 봤는지,
정상에는 무엇이 있었고 어떤 형태였는지, 산이 행정적으로 어떤 관리를 받고 있는지 같은 것을 일목요연하게 조회 가능하게 했다.

3. 코딩

그럼 이제 일상생활 얘기로 넘어가겠다.

사용자 삽입 이미지

(밝은 화면을 비추느라 명암차 때문에 주변이 어두워진 거지, 촬영 당시에 책상 주변은 실제로는 저 사진만치 어둡지 않았음)
화면이 미치도록 광활한 데스크톱 컴과,
눕든지 앉든지 편한 자세로 침대, 책상, 자동차 등 아무 데서나 사용 가능한 놋붉 컴 중
뭘로 코딩을 할지가 매우 고민된다. 일종의 행복한 고민.

참고로 노트북의 화면 전체와, 데스크톱 모니터의 오른쪽에 떠 있는 작은 프로그램 창하고 화면 크기(화소 수)가 동일하다. ㄲㄲㄲㄲㄲㄲㄲㄲ 미래의 리드미 문서를 작성하고 있는 날개셋 편집기의 화면임.
내가 지금까지 갖고 있던 그림과 동영상들이 화질이 얼마나 구린지를 까발리고 정죄하는 마법의 모니터다.

역시 프로그래머에게 화면이 큰 건 컴퓨터에게 램이 많은 것과 같다~! 정말 다다익선이다.
그도 그럴 것이 자꾸 창 전환이나 스크롤 하는 게(개발툴, 웹브라우저, 에디터, msdn 등등) 컴터로 치면 메모리 부족해서 하드디스크 스와핑 하는 것과 개념적으로 완전히 동일하기 때문이다.

무식하게 혼자 3~5K급으로 해상도가 너무 높은 모니터 하나냐, 혹은 걍 2K 해상도급 모니터 듀얼/트리플 중 어느 게 더 좋을지는 잘 모르겠다. 제각기 장단점이 있어 보인다.
참고로 배(선박)와 DLL(Windows 파일..;; )은 작은 놈 여럿보다는 큰 놈 하나가 성능면에서 더 효율적이다.

일체형 PC는 간지나고 공간 덜 차지하고 지저분한 선 없이 콘센트 하나만 꽂으면 모니터 본체 스피커가 전부 OK이니 정말 좋긴 하다.
다만, 이렇게 한번 세팅된 이후로 부품 업그레이드가 어려울 것이고 발열 제어도 곤란하니 엔드급 게임은 무리일 것이다.

구조적으로 볼 때 철도 차량의 동력분산 / 동력집중의 차이와 비슷해 보인다. 일체형 PC가 동력집중이 아니라 분산식에 대응한다. 그리고 트렁크· 캐빈· 엔진룸 따위의 구분이 없는 원박스 형태의 자동차도 일체형 PC와 비슷한 컨셉이라 볼 수 있겠다. (공간 활용 최대, 그러나 정비가 어렵다는 점에서 비슷)

4. 시간 부족과 일정 압박

CPU 클럭 속도 향상의 병목은 발열이고, 자동차 속도 향상의 병목은 공기 저항이다. 스마트폰 성능 향상의 병목은 배터리 용량이다.
그리고 날개셋 한글 입력기 개발에서 최악의 병목은 잠으로 인한 시간 부족 되시겠다.
난 오랜 경험상 매일 6시간이 정말 마지노선이고 그 이하로는 도저히 못 줄이겠다. 결국은 낮에 졸음과 집중력 저하로 인해 밤에 안 잔 것 이상의 대가를 치르게 되더라. -_-;;

어지간한 고시 준비생만치 시간을 분초 단위로 쪼개며 살아도 시원찮을 판에 이래 가지고 날개셋 9.0은 언제 완성할 것이며 박사 졸업은 도대체 언제 하나..;;
늦게 자고 늦게 일어나는 것보다는 일찍 자고 일찍 일어나는 것 선호함. 눈 감았다 뜨면 그냥 6시간이 싹 워프되고 개운 가뿐하게 일어나긴 한다. 천성적으로 남 눈치 안 보고 앞날 걱정을 미리 안 하는 체질이어서 그런지 스트레스는 적게 받는 편. 불면증 같은 게 어떻게 존재하는지 이해를 못 한다.

단지 잠을 더 줄일 수가 없을 뿐임.
이것도 기초체력 문제인가..? 잠 적으신 분이 굉장히 부럽다.

5. 덕질

논문 쓸 '꺼리, 아이템'들을 만들어내는 활동은 재미있지만 (코딩, 시스템 구현, 실험 등등)
그걸로 온갖 형식 갖춰서 실제로 논문을 쓰는 건 꽤 성가시고 번거롭다. =_=;;
그래도.. 잔인한 주인이 무자비하게 내린 온갖 복잡한 재귀호출 뺑뺑이와 자질구레한 메모리 할당· 해제 요청들을 컴퓨터는 진짜 순식간에 전광석화처럼 해낸다.

소프트웨어의 추상화 계층이 올라가면 코드를 유지보수하고 확장하기는 편해지지만 컴퓨터의 입장에서는 뭘 하나 하려 해도 포인터가 가리키는 대로 메모리를 여러 단계 요리조리 따라가야 하고, 캐시 미스가 나면 더 느린 메모리에 갔다가 와야 된다.

사용자가 '확인'을 누르거나 키보드를 하나 눌러서 화면에 글자 한 자가 찍힐 때까지 컴퓨터가 전자적으로 처리하는 일의 양이 도대체 얼마나 될까.
하물며 실존하지 않는 종이, 실존하지 않는 음악과 영상이 존재하는 것 같은 경험을 사람에게 제공하기 위해서 컴퓨터는 얼마나 많은 계산을 순식간에 해치우고 있을까?
프로그래머로서 이런 컴퓨터가 고맙고 대단하게 느껴질 때가 있다.

글자를 온통 배배 틀고 배경과 뒤섞어 놓은 일명 '캡챠'는 사람은 곧바로 알아보지만 컴퓨터가 알아보지 못하는 (걸 지향하는) 그림이다.
그러나 사람이 도무지 판독할 수 없는 랜덤 노이즈로 보이는 QR 코드 같은 건 컴퓨터가 곧바로 판독해 낸다.
예전에도 말했듯이 주석과 들여쓰기가 잘 된 코드와, IOCCC용 난독화 코드는 컴퓨터가 해석하는 데 아무런 차이가 없다.
이런 걸 생각해 봐도 사람과 기계는 근본적으로 특성이 달라도 이렇게 다르다는 걸 느낄 수 있다.

6. 컴퓨터 세팅

개인용 컴퓨터를 새로 지르거나 회사 같은 데서 내 업무용 컴터를 받았을 때 내가 기종을 불문하고 제일 먼저 하는 일은

  • 키보드 속도를 최고속으로 맞춘다. 보통 디폴트 값은 반복 속도가 늘 최고속에서 한 단계 낮은 걸로 돼 있는데.. 난 이게 최고속으로 돼 있지 않으면 답답하고 불편해서 못 쓴다. 키를 이 정도 시간 동안 눌렀으면 cursor나 선택 막대가 어느 정도로 이동해 있을 거라는 예상치와 기대치가 있기 때문이다. 지금 같은 '재입력/반복 키보드 속도 조절 체계'는 IBM PC AT 시절 이래로 변함없이 이어져 온 유구한 전통이다.
  • <날개셋> 한글 입력기를 설치한다. 내 홈페이지에 대외적으로 공개돼 있는 최신 버전이 아니라, 나 혼자만 갖고 있는 "개발 중"인 진짜 최신 버전이다. 한글을 내가 원하는 형태로 입력 가능하고 그 구닥다리 16*16 비트맵 폰트를 화면으로 좀 봐야 내가 심리적으로 안정된다.
  • Looking for you.mp3 복사해 넣는다. 음악 파일 중에서 묻지도 따지지도 않고 내가 무조건 제일 먼저 집어넣는 파일, 특히 사운드 테스트용으로 쓰는 파일은 답정너 looking for you이다. 이게 흘러나와야 내 개인용 컴퓨터라는 생각이 든다.

그나저나 노트북 내지 미니키보드들의 왼쪽 아래를 보면 Ctrl의 오른쪽에 Alt가 있는 것은 보장되지만 이것 말고 Fn, Win, 한자 키 같은 것은 생각보다 배치가 제멋대로이고 파편화가 심하다. 규격이 통일돼 있지 않다. 이것 때문에 한 키보드에 적응되고 나면 다른 키보드에서 modifier 키를 잘못 누르기 쉬워서 몹시 불편하다.

말이 나왔으니 하나 더.. 요즘 Windows 10은 사용자에게 선택의 여지를 안 주고 시도 때도 없이 강제 업데이트를 해서 꺼져야 할 때 바로 안 꺼지고, 켜져야 할 때 바로 안 켜지는 게 굉장히 싫다. 대규모 업데이트가 너무 잦고, 심지어 업데이트 후에 컴퓨터가 맛이 가는 것도 몇 번 겪어서 하기가 더욱 싫어진다. 그리고 컴퓨터를 오래 쓰고 나면 언제부턴가 시작 메뉴에서 앱들 검색이 제대로 동작하지 않기 시작한다. 나만 이러는 거 아니지?

그래서 인터넷을 뒤져서 이더넷 유선 랜도 데이터 요금이 부과되는 네트워크라고 속이는 레지스트리 패치를 적용시켰다. 그래야 운영체제가 제멋대로 깽판을 안 부린다. 제아무리 보안 업데이트도 인터넷 패킷 종량제 앞에서는 깨갱 할 수밖에 없으니까.

7. 삼각형의 오심을 그리는 프로그램

작년이니 엄청 옛날에 이미 작업된 사항이긴 한데, 막 중요한 건 아니어서 이제야 여기서 공지를 하게 됐다. 홈페이지의 '옛날 자료실'에 올라와 있는 '삼각형 오심을 그리는 프로그램'이 거의 10여 년 만에 기능이 크게 추가되고 보강됐다. 수학 강사인 교회 지인의 제안으로 행해진 작업이다.

삼각형의 오심이야 간단한 기하 알고리즘으로 (1) 두 직선의 교점과 (2) 두 변이 이루는 각을 이등분하는 변만 구할 줄 알면 컴퓨터로 아주 쉽게 구할 수 있다. 삼각형은 2차원 평면도형 중 가장 간단한 물건인데 얘의 모양에다 중심이라는 의미를 부여하는 방법도 이렇게 다양하다는 걸 알게 된다.

구체적인 개선 사항은 해당 웹페이지에도 나와 있지만, '구점원'이라는 걸 그리는 걸 추가했다. 삼각형 세 변들에 대해 변의 중점으로만 이뤄진 작은 삼각형의 외심원을 구한 것인데, 이게 또 방점과 접하고 수심을 지나기도 하는 등 기하학적인 의미가 장난이 아니다. 이걸 제6심이라고도 볼 수 있을지는 모르겠다.

그리고 내심을 제외한 수심, 구점원 중심, 무게중심, 외심 이렇게 네 점은 언제나 한 직선상에 있다는 게 보장된다..;; 이 오일러 직선을 그리는 기능도 추가했다.
또한 삼각형의 꼭지점만 마우스로 끌어서 이동시키는 게 아니라 삼각형 내부를 끌면 삼각형이 통째로 움직이게 했다. 한 점이 삼각형의 내부에 있는지 판별하는 건 세 점의 방향성 판별 공식을 이용해서 구현 가능하다.

웹브라우저에서 윤곽선 폰트 에디터까지 구동하는 세상인데 이런 간단한 그림을 그리는 프로그램쯤은 이제 플래시조차 필요 없고 HTML+(JS)로 다 만들 수 있을 것이다. 엔드 유저의 관점에서는 EXE 형태의 프로그램이 점점 필요 없어지고 있긴 한데, 일단 내가 아는 skill은 C++과 Windows API이니 저렇게 간다. GDI 말고 다른 API를 동원해서 선들을 안티앨리어싱도 좀 시킬 걸 하는 아쉬움도 남는다. 완벽하게 만들려고 욕심 부리면 뭐 한도 끝도 없다.

Posted by 사무엘

2017/02/26 19:33 2017/02/26 19:33
, , , ,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1332

1. WNDCLASS와 HCURSOR

GUI 환경에서 키보드로 글자 입력을 받기 위해 캐럿(caret, 혹은 cursor)이라는 깜빡이는 세로줄이 나타난다면, 마우스의 입력을 받기 위해서는 마우스 포인터라는 게 떠 있다. 키보드 문자 입력과 마우스는 상호 배타적인 관계이다 보니, 문자 입력이 시작되면 마우스 포인터는 화면을 가리지 말라고 쏙 사라지곤 한다. 그 반면, 키보드 단축키와 마우스는 전혀 배타적이지 않고 상호 보완적이므로 이 경우는 마우스 포인터가 사라질 필요가 없다. 간단히 말해 스타를 하는 경우를 생각하면 된다.

Windows 운영체제 내부에서 생성되는 모든 창(window)들은 마우스 포인터가 자기 영역을 지날 때 어떤 모양의 포인터를 표시할지를 자유롭게 지정할 수 있다. 가장 static하고 간단한 방법으로는 윈도우 클래스를 등록할 때 WNDCLASS의 hCursor 멤버에다가 지정해 주면 된다.

HCURSOR라는 타입은 마우스 포인터의 모양을 나타내는 자료구조의 포인터이다. 마우스 포인터는 아이콘(HICON)과 거의 동급으로 취급되며, 아이콘에다가 중심 위치(hot spot) 정보만이 추가되었을 뿐이다. 화살표 그림의 경우 화살표가 가리키는 뾰족한 지점이 바로 hot spot의 위치가 되는 것이다.

그리고 그 아이콘이라는 것은 개념적으로 AND 연산용 비트맵(마스크)과 XOR 연산용 비트맵(그리기)이 추가된 정사각형 비트맵(HBITMAP) 쌍이다.
마우스 포인터 자체를 프로그램 코드를 통해 동적으로 생성하고자 한다면 이런 관계에 대해서도 이해할 필요가 있다. 이런 구조 덕분에 배경색을 반전시키는 마우스 포인터도 만들 수 있다. 또한, Windows에서 아이콘과 마우스 포인터가 매우 유사하게 취급된다는 것은 GetIconInfo 함수나 ICONINFO 구조체의 스펙을 보면 금방 수긍할 수 있다.

색깔 중에 system color가 있고 DC 오브젝트들(브러시· 펜 따위) 중에도 stock object가 있으며, 클립보드 포맷 중에 표준 포맷(CF_TEXT ...)이 있는 것처럼.. 마우스 포인터 중에도 용도가 고정되었고 운영체제 차원에서 모양을 공통으로 관리하는 것이 몇 종류 있다. 이런 공용 포인터의 예로는 일반 화살표, 모래시계, 입력란용 I-beam 등 우리에게 친숙한 것이 있으며, 이들은 제어판을 통해 그 모양을 바꿀 수 있다. 응용 프로그램에서는 LoadCursor(NULL, IDC_*)를 호출해서 이들의 HCURSOR 값을 얻을 수 있으며 이를 응당 클래스 등록 시에 사용하면 된다.

그래픽 에디터라든가 게임 급으로 정말 아주 튀는 GUI를 제공하는 프로그램을 만드는 게 아니라면, 공용 포인터 말고 다른 독자적인 포인터를 쓸 일은 잘 없을 것이다. 하지만 튀지 않는 일반 업무용 프로그램에서도 custom 포인터가 필요한 경우가 가끔은 있다.

  • 워드 프로세서의 경우, IDC_IBEAM의 변형이 필요할 때가 있다. 이탤릭체 글자에서는 포인터의 모양도 살짝 기울어지며, 세로쓰기 모드에서는 포인터의 모양 역시 90도 돌아간다.
  • drag & drop 상태를 표시하기 위해, 화살표 밑에 사각형 테두리와 [+] 마크가 붙은 포인터가 필요할 때가 있다. 이것도 의외로 공용 포인터에는 존재하지 않으며, ole32.dll 내부에 있는 비공식 리소스를 몰래 뽑아 와서 쓰는 경우가 많다.
  • 먼 옛날, IDC_HAND가 존재하지 않던 Windows 95/NT4에서는 winhlp32.exe의 내부에 있는 손가락 링크 모양 비공식 리소스를 몰래 뽑아 와서 하이퍼링크를 구현할 때 쓰기도 했다.

LoadCursor는 원래 모듈(EXE/DLL)의 리소스로부터 마우스 포인터 그림을 추출하는 함수이다.
CreateCursor 함수는 HBITMAP을 받는 게 아니라 쌩짜 AND/XOR 비트맵 배열만을 입력받아서 포인터를 생성해 주는데, 그 말인즉슨 얘는 애초에 모노크롬 포인터밖에 못 만든다는 뜻이다. 컬러를 지원하지 않는다.

그러고 보니 마우스 포인터는 마치 GIF처럼 애니메이션 가능한 버전도 생겨서 단순 아이콘과 차별화가 이뤄지긴 했다. ico 파일에는 크기와 화질이 다른 여러 아이콘들이 있을 수 있다면, ani에는 동일 아이콘의 여러 프레임이 들어갈 수 있게 된 것이다. 교집합인 정보가 있지만 서로 완전히 호환되지는 않는 미묘한 관계가 됐다.

2. WM_SETCURSOR와 SetCursor 함수

윈도우 클래스를 등록할 때 hCursor 멤버에다가 NULL을 지정하면 그 윈도우는 마우스 포인터가 기본적인 화살표로 지정된다거나, 아니면 말 그대로 아무것도 없는 올투명 이미지가 지정되어서 포인터가 사라진다거나 하지 않는다.
어찌 되는가 하면, 이 윈도우 영역으로 들어오기 직전에 유지되었던 마우스 포인터가 변경 없이 그대로 유지된다..! 마치 C언어에서 초기화되지 않은 변수처럼 undefined 상태가 되는 것이다.

이런 동작을 원하는 프로그래머나 기대하는 사용자는 전무할 것이다. 그러므로 클래스 차원에서 지정된 기본 포인터가 없는 윈도우는 자신의 윈도우 프로시저 내부에서 매번 실시간으로 마우스 포인터를 지정해 줘야 한다. 어떻게? WM_SETCURSOR라는 메시지가 왔을 때 SetCursor라는 함수를 호출해서 하면 된다.
아니 사실은 클래스 포인터가 이미 지정돼 있는 창이라도 필요하다면 이렇게 마우스 포인터를 실행 중에 얼마든지 변경할 수 있다. 동일한 웹브라우저 창이라도 포인터가 링크 위를 가리키고 있을 때는 조건부로 손가락 모양으로 바뀌어야 할 테니까 말이다.

윈도우 안에서 마우스 포인터가 움직이면 WM_MOUSEMOVE만 오는 게 아니라 그 전에 WM_SETCURSOR부터 날아온다. 그에 반해 SetCursor는 굳이 WM_SETCURSOR 메시지 타이밍이 아니어도 아무 때나 언제든지 호출 가능하다. 이 함수 자체는 지금 포인터가 나 자신이(스레드 단위) 생성한 윈도우에만 있으면 위치 불문하고 포인터 모양을 즉시 바꿔 준다. WM_PAINT 타이밍 때에만 사용 가능한 BeginPaint/EndPaint처럼 특정 메시지에 매여 있는 게 아니라는 뜻이다.

그럼 왜 굳이 WM_SETCURSOR라는 메시지가 따로 있는 것일까? 그 이유는 저렇게 일상적으로 마우스 포인터가 움직였을 때 빼고는 얘는 WM_MOUSEMOVE와는 설계 철학과 생성 조건이 매우 다르기 때문이다.

  • 윈도우가 disable됐을 때는 그 윈도우로 마우스가 움직이더라도 통상적인 WM_MOUSEMOVE가 오지 않는다. 그러나 이때에도 WM_SETCURSOR는 전달하는 상황 정보(hit-test code)만 달라진 채 언제나 온다.
  • hit-test code가 같이 온다는 점에서 유추할 수 있듯, WM_SETCURSOR는 클라이언트와 논클라이언트를 가리지 않고 온다. 그에 반해 WM_MOUSEMOVE는 클라이언트 영역 전용이고 WM_NCMOUSEMOVE가 따로 있다.
  • 마우스가 capture된 뒤부터는 마우스가 움직이면 반대로 WM_MOUSEMOVE만 오지 WM_SETCURSOR는 오지 않는다. 마우스의 포커스가 포인터 위치와 무관하게 이 윈도우에 집중되었기 때문에 포인터의 모양도 잠시 고정된다.
  • 그리고 결정적으로.. WM_MOUSEMOVE는 지금 화면을 대면하고 있는 최하위 child 윈도우에 직통으로 전달되는 반면, WM_SETCURSOR는 최상위 parent 윈도우에 먼저 전달되어서 얘들이 처리를 포기/거부했을 때에만 child로 내려간다.

마지막 항목이 중요하다. 이런 메커니즘의 차이로 인해 두 메시지는 서로 호환성이 전혀 없으며 별도의 메시지로 분리되어야만 한다. 이 메시지가 그냥 이 시점에서 표시할 HCURSOR 값만 곱게 얻는 게 목적이라면 WM_SETCURSOR 메시지는 SET이 아니라 GET이라는 동사가 붙어서 WM_GETCURSOR, WM_QUERYCURSOR처럼 명명됐을 수도 있다. 대화상자의 WM_GETDLGCODE 메시지처럼 그냥 return (LRESULT)LoadCursor(...)의 형태.
그런데 그게 아니기 때문에 자기가 직접 마우스 포인터를 재지정할 의향이 있다면 WM_SETCURSOR가 올 때마다 SetCursor를 수동으로 매번 호출도 해야 하고, 그러면서 리턴값도 0이 아닌 값으로 되돌려야 한다. 특히 DefWindowProc를 호출해서는 안 된다.

DefWindowProc가 WM_SETCURSOR 때 하는 일 중에는 논클라이언트 영역에서 포인터를 화살표 내지 창의 크기 조절 손잡이 모양으로 바꾸는 것이 포함돼 있다.
하지만 클라이언트 영역에서 DefWindowProc은 "난 마우스 포인터 모양을 자체적으로 처리할 의향이 없으니, (1) 내 부모 윈도우에서 이의 없으면 (2) 최종 처리를 내 자식 윈도우에 맡기겠소"라는 의미가 된다. Def..없이 return 0은 (2)만을 담당한다.

참고로, SetCursor(NULL)을 하면 클래스 WNDCLASS::hCursor = NULL과는 달리 비로소 마우스 포인터가 화면에서 사라진다. 이것은 HideCursor / ShowCursor 함수와 비슷한 효과를 낸다. 이들 함수는 포인터의 레퍼런스 카운터를 1 증가나 감소시켜서 카운터가 양수이면 포인터를 계속 표시시키고, 그렇지 않으면 계속 감추고 있는다. 캐럿을 표시하거나 감추는 ShowCaret / HideCaret과 비슷한 원리로 동작한다.
그에 반해 SetCursor(NULL)은 효과가 일시적이므로 해당 윈도우가 WM_SETCURSOR에서 계속해서 SetCursor(NULL)을 해 줘야만 포인터가 없는 상태가 유지된다.

사소한 사항이다만, WM_MOUSEMOVE는 메시지 큐에 post 형태로 전해지는 반면, WM_SETCURSOR는 리턴값을 꼼꼼히 확인해야 하기 때문에 언제나 sent된다는 차이도 있다. 마우스 메시지 훅킹 같은 걸 한다면 요런 차이가 민감하게 와 닿을 것이다.

3. 대기 상태 표현하기

프로그램이 파일을 읽고 쓰고 복잡한 계산을 시작해서 대략 0.n초 정도 짤막하게 사용자의 응답(더 정확히는 운영체제 메시지)에 반응을 하지 않게 됐다면, 이에 대해 가장 간단하게 피드백을 주는 방법은 SetCursor(LoadCursor(NULL, IDC_WAIT))를 해서 마우스 포인터를 그 악명 높은 모래시계 모양으로 바꾸는 것이다.

물론 처리가 끝났다면 포인터 모양을 원상복구 해야 한다. 이것은 SetCursor의 리턴값을 보관하고 있다가 도로 전달하는 것으로 쉽게 구현 가능하며, 이렇게 시작과 끝을 생성자와 소멸자에다 넣어서 간단한 C++ 클래스를 구현할 수도 있다. MFC에 있는 CWaitCursor가 그 예이다.
모래시계로 변해 있던 동안 마우스 포인터가 조금이라도 다른 곳으로 이동했거나, 위치가 안 바뀌었더라도 그 사이에 포인터 아래의 윈도우가 바뀌었다면.. 프로그램이 의식을 회복(?)했을 때 WM_MOUSEMOVE와 그에 상응하는 WM_SETCURSOR도 오기 때문에 포인터 모양이 자동으로 갱신되긴 한다. 그러나 그런 외부적인 변화가 전혀 없었더라도 포인터 모양이 원상복귀 되어야 하니까 말이다.

마우스 포인터의 움직임은 일종의 하드웨어 인터럽트 형태로 발생하며, 응용 프로그램이 WM_SETCURSOR 메시지에 응답하지 않고 있더라도 포인터가 움직인 것에 대한 반응은 해야 한다. 그렇기 때문에 프로그램이 처리를 열심히 하고 있는 동안에는 좀 전에 지정된 모래시계 모양이 유지된다. 물론, 포인터가 정상적으로 응답 중인 다른 프로그램 창 위에 놓여 있으면 거기 모양으로 바뀌며, 한 프로그램이 수 초 이상 너무 오랫동안 응답을 안 하고 있으면 그건 그것대로 문제가 된다. 내 프로그램 창이 고스트 윈도우로 바뀌는 일은 없어야 한다.

시간이 굉장히 오래 걸리는 작업을 한다면 프로그램의 디자인 형태가 바뀐다. 작업은 백그라운드 스레드에다 담당시키고 프로그램은 현재 진행 상황을 출력하면서 UI 메시지 반응도 평소처럼 한다. progress 컨트롤이 장착된 대화상자가 이 역할을 하며, 사실 Windows Vista부터는 task dialog로 이걸 간단하게 띄울 수도 있게 됐다.
동영상 인코더처럼 input 데이터를 직접 생성하고 작성하는 기능은 없고, 이미 있는 데이터를 변환하는 일이 전부인 프로그램이라면 별도의 대화상자 없이 자기 main frame window 자체가 통째로 진행 상황을 표시하는 용도로 쓰이기도 한다. <날개셋> 변환기도 이런 형태의 프로그램이다.

이를 좀 더 일반화해서 생각하면 이렇다. 어떤 윈도우가 하는 역할이 자신과 별개이고 독립적인 타 작업의 진행 상황을 관찰하면서 표시하는 게 전부라면, 보통은 그 윈도우 내부의 마우스 포인터를 굳이 별도로 모래시계 모양으로 바꾸지 않는다. 설치 프로그램들이 그 예이다. 다만, Windows Installer 엔진의 경우 본격적으로 설치/제거를 수행하는 마법사가 뜨기 전에 준비 작업을 하느라 자그마한 대화상자가 떴을 때는 마우스 포인터를 거기로 가져가면 모래시계로 바뀐다.

사용자 삽입 이미지

요런 게 대화상자 윈도우에서 WM_SETCURSOR를 처리함으로써 구현 가능하다. 이 메시지는 부모-자식 top-to-bottom 형태로 내려가기 때문에, 부모에서 메시지를 가로채 버리면 자식 윈도우의 의도와 상관없이 마우스 포인터를 모래시계 모양으로 바꿀 수 있다. 밑에 지금 무슨 윈도우가 있는지 핸들도 wParam으로 친절하게 전달된다. 여기서 SetCursor 호출만 하고 리턴값으로 nonzero를 지정하지 않으면, 대화상자 배경들만 포인터가 바뀌고 버튼 같은 각종 컨트롤들은 바뀌지 않게 된다. (위의 스크린샷처럼)

이와 대조적으로, 키보드 메시지는 포커스를 잡고 있는 최하위 윈도우에 직통으로 전달되니(bottm-to-top), 그 위에서 공통 단축키 같은 걸 처리하려면 message loop 차원에서의 pre-processing이 필요한 것이다.

<날개셋> 변환기의 경우 변환하는 파일이 적으면 스레드 없이 그냥 비응답 상태로 빠진 채로 변환을 수행한다. 그러나 수십 개, 수MB 이상 분량 파일을 요청하면 대화상자의 모든 컨트롤들을 disable시키고 progress 컨트롤을 출력하고, 대화상자 내부의 마우스 포인터를 모래시계로 바꾼 뒤 변환을 수행한다. 이때는 어차피 대화상자의 다른 기능들을 전혀 사용할 수 없고 ESC나 [X]를 눌러 중간 취소만 가능하기 때문이다.

그리고 하나 더 생각할 만한 상황은.. 딴 작업이 아니라 대화상자 자기 내부에다 출력할 데이터들을 준비하고 초기화하는 작업이 시간이 좀 오래 걸릴 때이다. <날개셋> 한글 입력기 제어판의 대화상자에도 그런 경우가 몇 가지 있다.
이때는 문제의 콤보나 리스트박스가 빈 채로 먼저 대화상자를 출력한 뒤, 스레드를 만들고 마우스 포인터를 IDC_WAIT가 아니라 IDC_APPSTARTING 모양으로 바꿨다. 대화상자가 출력은 됐지만 아직 초기화가 덜 돼서 백그라운드에서 작업 중임을 이렇게 나타낸다.

요렇게 백그라운드의 스레드 작업이 끝난 뒤에는 마우스 포인터를 어떻게 원상복구 할지가 문제가 된다.
아까처럼 스레드 없던 시절에는 작업하던 사이에 포인터 위치가 바뀌었으면 WM_SETCURSOR와 WM_MOUSEMOVE가 자동으로 생겼다. 그러나 지금은 그렇지 않다. 작업이 수행되던 중에 포인터 이동에 대한 처리는 이미 다 이뤄졌기 때문이다.

마우스 포인터의 이동 없이 아래의 창에다가 WM_SETCURSOR를 인위적으로 생성해서 포인터 모양을 원래 것으로 갱신할 수 있어야 하는데.. 이것만 어떻게 하는지 잘 모르겠다.
일단 본인이 사용하는 방법은 GetCursorPos로 현재 포인터 위치를 얻은 뒤, 그거 그대로 SetCursorPos를 하는 것이다. 위치가 바뀐 게 없음에도 불구하고 이렇게 하면 WM_SETCURSOR와 WM_MOUSEMOVE가 생성되기는 하는 것 같더라.
이 정도면 Windows 프로그래밍에서 마우스 포인터 제어와 관련해서 어지간한 문제는 다 다룬 것 같다.

Posted by 사무엘

2017/02/06 08:35 2017/02/06 08:35
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1324

<날개셋> 한글 입력기는 잘 알다시피 16년 전에 개발된 1.0과 지금의 8.6이 요구하는 운영체제 사양(그리고 사실상 하드웨어 사양도)에 차이가 전혀 없는 좀 사기급의 프로그램이다. 32비트 에디션은 Windows 95/NT4 이상에서도 돌아간다. Win95쯤은 안드로이드 스마트폰 내부에서 가상 머신으로도 돌리는 지경이 됐는데도 말이다. 뭐, 내 프로그램은 게임처럼 딱히 최신 사양빨을 타는 분야의 프로그램이 아니며, 한글이 무슨 한자처럼 처리하는 데 메모리가 엄청 많이 든다거나 아랍· 태국 문자처럼 내부 메커니즘이 복잡한 것도 아니기 때문이다.

Windows는 API 함수들이 유니코드를 표방하는 2바이트 문자열을 취급하는 버전(W 함수)과 비유니코드 일명 'ANSI 인코딩'을 표방하는 1바이트 문자열을 취급하는 버전(A 함수)으로 나뉘어 있다. 맥이나 리눅스 같은 타 운영체제에서는 찾을 수 없는 독특한 형태이다. 물론 문자 집합이라는 건 굳이 인코딩 단위에 얽매여 있지는 않으니, 1바이트라는 단위는 그대로 놔 두고 UTF-8만 사용해도 유니코드 지원은 가능했다. 하지만 Windows는 호환성 때문인지 문자 집합과 함께 인코딩까지 완전히 바꿔 버리는 방식을 채택했다. 그래서 wchar_t도 4가 아닌 2바이트이며, UTF-16을 유난히 좋아한다.

Windows NT는 W가 기본이고 A도 호환성 차원에서 지원하지만 Windows 9x는 메모리 부족 문제로 인해 A만 지원하고 W는 아예 제공하지 않았다. 그러니 일반적으로는 Windows 9x를 지원하려다 보면 유니코드를 지원할 수 없어서 깨진 문자 크리 때문에 프로그램의 국제화에 애로사항이 꽃폈으며, 반대로 W 함수만 사용하면 가정에 NT 계열보다 더 많이 보급돼 있던 9x 계열 운영체제를 지원할 수 없었다.

이 딜레마를 해소하는 방법은 일단 프로그램은 W 함수 기반으로 개발한 뒤, 9x에서는 특별히 W 함수 진입로에서 함수 argument를 변환하고 나서 A 함수를 호출하는 일종의 훅/thunk DLL을 구동하는 것이었다. <날개셋> 한글 입력기는 이 테크닉을 사용한다.
훅 DLL의 소스 코드는 동작 방식의 특성상, import table상의 함수 이름 문자열과 거기에 대응하는 훅킹 함수 포인터를 명시한 테이블을 갖고 있다. 또한 기존 Windows API 함수와 프로토타입이 동일하지만, 하는 일에는 살짝 차이가 있는 함수도 즐겨 사용한다.
이런 걸 구현할 때는 C/C++ 언어에 존재하는 다음과 같은 기능들이 유용하게 쓰였다.

1.
함수 훅킹 테이블을 만들 때 #define과 더불어 #(문자열화)와 ##(토큰 연결)라는 전처리기 연산자를 즐겨 썼다.
_FUNC(SetWindowTextW) 하나로 { "SetWindowTextW", (FARPROC)My_SetWindowTextW } 요걸 표현할 수 있으니 전처리기 연산자를 써서 매크로를 정의하는 게 완전 딱이지 않은가?
C언어는 전처리기의 단항 연산자는 # 1개로, 이항 연산자는 # 2개로 표현해서 나름 직관성을 추구했다. 그리고 안 그래도 전처리기 연산자는 C/C++의 고유한 연산자와는 섞여서는 안 되는데 굳이 # 말고 다른 기호를 끌어다 쓰지 않아서 형태 구분이 잘 되게 했다.

그런데 여기서 문제가 하나 있다.
문자열화 연산자는 매크로 전개를 한 놈을 문자열로 바꾸는지, 아니면 언제나 주어진 인자를 문자 그대로 문자열로 바꾸는지를 본인은 엄밀하게 생각을 하지 않고 지냈다. #define ToString(a) #a라고 정의해 주면, ToString(SetWindowText)은 "SetWindowText"로 바뀌는지, 혹은 "SetWindowTextW"나 "SetWindowTextA"로 바뀌는지 궁금했다.

이에 대한 정답을 먼저 말하자면, # 연산자는 그 자체로는 매크로 전개를 전혀 하지 않는다. 그렇기 때문에 저 문제의 정답은 "SetWindowText"이다.
만약 W/A가 붙은 놈을 얻고 싶으면 매크로를 한 단계 더 거쳐 줘야 한다. #define ToString_Expanded(a) ToString(a)를 선언한 뒤, ToString_Expanded(SetWindowText)라고 명령을 내리면 그제서야 "SetWindowTextW"(또는 A)가 얻어진다.

물론 딱히 매크로가 없는 인자를 넘기면 ToString_Expanded는 그냥 ToString과 동일한 결과가 나온다. 이런 차이가 있다는 걸 근래에 알게 됐다.

C/C++ 코드에는 검증과 디버깅을 위해 assert 부류의 매크로를 볼 수 있는데, C 언어 표준 매크로 상수와 연산자들은 상당수가 얘를 구현하기 위해 만들어진 게 아닐까 싶을 정도이다.
상식적으로 생각해 봐도, 실행 파일 내부에 "result > 0이라는 수식의 assertion이 실패했습니다. 아무개.cpp n째 줄입니다." 정도의 검증 명령이 삽입되려면 딱 봐도 __FILE__, __LINE__이 들어가야 했을 것이고 검증 대상 수식은 # 연산자에 의해 문자열로 바뀌었을 거라는 걸 알 수 있다.

파일명과 줄번호는 바이너리 형태의 디버그 심벌에도 포함되긴 하지만, result > 0처럼 대놓고 코드를 구성하는 문자열은 # 연산자 없이는 답이 없다. 이런 사기급의 전처리 기능은 C/C++ 외의 다른 언어에서는 유례를 거의 찾을 수 없지 싶다.

2.
또한 decltype이라는 연산자가 있는 줄을 난생 처음 알았다. 연산자이긴 하지만 되돌리는 게 어떤 값이 아니라 타입 그 자체이다. typeid처럼 RTTI와 관계 있는 기능도 아니며, 컴파일 타임 때 결정되는 고정 타입이다. 그래서

auto x=3.4f;
decltype(3.4f) x = 3.4f;
float x=3.4f;

는 의미가 모두 동일하다. auto와도 어떤 관계인지 바로 알 수 있을 것이다.
sizeof는 값 또는 타입을 모두 받아들여서 값(크기. 고정된 정수)을 되돌리는 반면, decltype은 값을 받아서 타입을 되돌린다는 차이가 있다. 또한 sizeof와 decltype 모두 그 값을 실제로 실행(evaluate)하지는 않는다.

auto는 타입과 동시에 변수값 초기화를 할 때 번거로운 타이핑을 줄여 준다. decltype은 값을 동반하지 않고 타입 자체만을 명시할 때 매우 유용하다. 템플릿 인자를 명시하거나 형변환을 할 때, 길고 복잡한 namespace나 함수 포인터의 프로토타입을 쓰는 수고를 덜어 준다. typedef를 하자니 번거로운 이름을 떠올려야 하는데.. 그럴 필요도 없어진다. 가령,

CAPIPtr<int (*)(int flags, WPARAM wParam)> pfnAbout(hNgsLib, "ngsAbout");

라고 쓸 것을

CAPIPtr<decltype(&::ngsAbout)> pfnAbout(hNgsLib, "ngsAbout");

로 간편하게 대체 가능하다. 함수의 이름만으로 그 함수의 포인터의 프로토타입을 간단히 명시할 수 있으니 얼마나 편리한가? API 훅킹 라이브러리를 만들 때도 이런 문법이 매우 유용할 수밖에 없다. 훅킹 대상인 Wndows API들이야 헤더 파일에 프로토타입이 다 선언돼 있으므로 그걸 decltype의 피연산자로 주면 되기 때문이다..

또한, 과거에는 클래스에서 함수 포인터 형변환 연산자 함수를 선언할 때는 C++ 문법의 한계 때문에 반드시 그 함수 프로토타입을 typedef부터 해야 했다. 하지만 decltype은 여기서도 그런 번거로움을 응당 없애 준다. 아래 코드를 보면 차이를 알 수 있다.

class CMyTable {
    static int _Func();
public:
    //과거
    typedef int (*PFN)();
    operator PFN() { return _Func; }

    //현재
    operator decltype(&CMyTable::_Func)() { return _Func; }
};

decltype 연산자는 Visual C++ 2010부터 지원됐다. 함수 포인터에다가 람다를 바로 대입하는 건 2010은 아니고 2012부터 지원되기 시작했다. 물론 캡처가 없는 람다에 한해서. 람다는 함수 포인터보다 더 추상적인 놈이기 때문에 calling convention은 컴파일러가 알아서 다 해결해 준다.

C++은 잘 알다시피 A *B와 A B(), (A)+B 같은 문장이 A와 B의 정체가 무엇인지에 따라(타입? 값?) 파싱 방식이 완전히 달라진다. 템플릿이 추가된 뒤부터는 <와 >조차도 이항 연산자 vs 타입 명시용의 여닫는 괄호처럼 해석이 달라질 수 있게 되었고, 21세기에 와서는 템플릿 인자를 이중으로 닫을 때 굳이 > > 안 하고 >>로 써도 되게 문법이 바뀌었다. 저게 제대로 돌아가려면 값과 타입의 구분이 더욱 절실히 필요하다.

이런 특성 때문에 템플릿의 컴파일 편의를 위해 typename이라는 힌트 키워드가 도입되었으며, auto와 decltype도 동일한 용도는 아니지만 비슷한 맥락에서 type과 관련된 기술을 돕기 위해 등장한 게 아닌가 싶다.

3.
유니코드 API 훅킹 DLL을 만든다면, SetWindowTextW라면 WCHAR 문자열 형태로 전달된 인자를 char 문자열로 바꾼 뒤 A 함수에다 전달하고, GetWindowTextW라면 먼저 내부적으로 char 버퍼를 준비해서 A 함수를 호출한 뒤, 그걸 WCHAR로 변환해서 사용자에게 되돌리는 형태로 전달한다.

물론 용례가 무궁무진한 메시지를 주고받는 함수라든가 GetOpenFileName처럼 입· 출력 겸용 복잡한 구조체를 운용하는 함수, SystemParametersInfo처럼 PVOID 하나에 온갖 종류의 데이터를 주고받는 함수라면 훅킹 함수를 만들기가 아주 까다로워진다. 하지만 그 함수가 제공하는 모든 기능에다 일일이 변환 기능을 넣을 필요는 없다. 다양한 플래그와 기능들 중에서 내 프로그램이 실제로 사용하는 것에 대해서만 변환을 하면 된다.

그런데 훅킹 함수 중에는 의외로 아무 변환 없이 인자를 그대로 A 함수로 넘기기만 하고 리턴값도 아무 보정 없이 그대로 되돌리는 것도 있다. 훅킹 함수 단계에서 딱히 할 게 없다고 말이다.

그 대표적인 예로는 리소스를 리소스 ID가 아니라 메모리 포인터 차원에서 저수준으로 읽어들이는 DialogBoxIndirect와 LoadMenuIndirect가 있다.
얘들이 인자로 받아들이는 DLGTEMPLATE와 MENUTEMPLATE 구조체는 내부에 PCTSTR 같은 게 없으며, 애초에 A/W 구분이 없다. 왜냐하면 저 구조체는 메모리가 아니라 디스크에 저장되는 리소스 데이터 포맷을 기술하기 때문이다. Windows 9x용이든 NT계열용이든 실행 파일이야 서로 완전히 동일한 포맷이며 리소스들은 모두 유니코드 형태로 저장된다. 그러니 인자가 동일한데 저 두 함수도 원론적으로는 굳이 W/A 구분을 할 필요가 없다.

그럼에도 불구하고 이런 함수에도 굳이 A/W 구분이 존재하는 이유는 얘들이 내부적으로 대화상자와 메뉴 윈도우를 생성할 때 사용하는 CreateWindowEx 함수가 A/W 구분이 존재하며, 9x에서는 W 버전이 존재하지 않기 때문이다. 비록 리소스 데이터 상으로는 원래의 언어 텍스트가 들어있지만, 운영체제가 관리하는 윈도우의 텍스트 버퍼는 ANSI 기반이니 그걸 운영체제의 표준 기능만으로 제대로 표시할 방법도 없다.

그렇다면.. Windows 9x에서는 DialogBoxIndirectW나 LoadMenuIndirectW가 호출 됐을 때,
SetLastError(ERROR_CALL_NOT_IMPLEMENTED); return FALSE / NULL; 을 하지 말고..
return DialogBoxIndirectA( ... ) / LoadMenuIndirectA( ... ); 를 해도 되지 않았나 하는 의문이 남는다. 직통으로 A로 포워딩하는 거 말이다.
그럼 9x에서는 현 ANSI 인코딩으로 표현되지 않는 문자들은 비록 깨져서 출력되겠지만 최소한 메뉴나 대화상자가 뜨고 동작은 하지 않겠는가?

하지만 그건 별 의미가 없다고 생각돼서 조치를 취하지 않은 것 같다. GetOpenFileNameW, CreateFileW, CreateWindowExW, GetMessageW, SendMessageW 등등.. Windows 프로그램의 근간을 이루는 함수들이 유니코드 버전은 몽땅 동작하지 않는데 저런 것만 살려 놔서 뭘 하겠나? Windows 9x에서는 최소한의 유니코드 문자를 찍는 GDI 함수만이 제 기능을 하며, MessageBoxW는 인자들을 char 형태로 변환해서 예외적으로 지원해 주고 있다. 최소한의 에러 메시지를 찍고 종료하는 기능만은 유니코드 API 직통으로 동작하게 말이다. =_=;;

Posted by 사무엘

2017/01/02 08:25 2017/01/02 08:25
, , ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1312

오늘날 Windows에서 실행되는 모든 프로그램들.. exe, dll 따위는 잘 알다시피 portable executable이라는 형식으로 만들어져 있다. 하지만 이 파일 포맷도.. 처음 만들어지던 당시에 여전히 컴퓨터에서 현역이던 도스와 최소한의 호환성을 유지할 필요가 있었기 때문에, 맨 앞에 MZ로 시작하는 16비트 도스 헤더를 여전히 갖추고 있다.

호환성이란 게 딴 게 아니고, 도스에서 Windows용 프로그램이 실행됐을 때 컴퓨터가 다운되는 게 아니라 "이 프로그램은 도스용이 아닙니다" 같은 짤막한 에러 메시지라도 뜨게 하는 것 말이다.

옛날에 Win32s가 제대로 설치되지 않은 상태에서 32비트 프로그램을 Windows 3.1에서 실행했더니.. "상위 버전에서 실행해 주십시오 / Win32s를 다시 설치해 주십시오" 이런 말이 메시지 박스 형태로 뜨는 게 아니라 황당하게 This program cannot be run in DOS mode라고.. 지금 시스템이 아예 Windows가 아닌 듯한 자비심 없는 메시지가 도스창에 떴다. 20여 년 전에 그 인상이 무척 강렬했었다. 요즘은 32비트 OS에서 64비트 exe의 실행을 시도해도 에러 메시지가 그 정도로 막나가는 형태는 아니다.

Windows용 프로그램들은 빌드할 때 그렇게 도스에서 잘못 실행됐을 때를 대비해 짤막하게 대신 실행해 줄 도스용 일명 "stub" 프로그램을 링크 옵션으로 지정할 수 있다. 이름하여 /STUB. 이걸 지정하지 않으면 아까 같은 저런 짤막한 에러 메시지 한 줄만 찍는 기본 stub 프로그램이 들어간다.
16비트 시절에 Visual C++ 1.5x를 보면 그 예제 stub 프로그램 자체가 winstub.exe라고 있었다. 하지만 그 이후부터는 디폴트 stub 프로그램은 그냥 링커 내부에 내장되어 버렸는지 그런 게 따로 있지는 않다.

프로그램을 특수하게 빌드하면 그런 stub을 아예 전혀 집어넣지 않는 것도 가능하다. 맨 앞에 MZ, 그리고 0x3C 오프셋에 PE 헤더가 있는 지점만 들어있으면 되고 나머지 칸은 몽땅 0으로 채움. 심지어 PE 헤더가 0x3C 오프셋보다도 전에, 도스 EXE 헤더가 있어야 할 지점에서 바로 시작하는 것도 가능하다.

미래에 마소에서 빌드하는 EXE/DLL들은 번거로운 This program cannot be ... 메시지를 떼어내고 이렇게 만들어져 나올지도 모른다. 물론 이런 프로그램은 Windows 환경에서 실행하는 건 문제 없지만 만에 하나 어느 레트로 변태 덕후가 그걸 굳이 도스에서 실행해 보면 컴퓨터가 어찌 되는지 책임 못 지는 상태가 될 것이다.

반대로 기본 stub 대신에 꽤 규모 있는 16비트 프로그램을 집어넣어서 동일 EXE가 도스에서도 그럭저럭 기능을 하고 Windows에서도 GUI를 띄우며 제대로 실행되는 프로그램을 만든 경우가 있다. Windows 9x 시절엔 레지스트리 편집기가 그러했다. 이건 Windows에서 보기 드문 하이브리드 universal binary 형태의 프로그램인 것 같다.
16비트 프로그램이 자기 자신 EXE를 열어서 PE 헤더를 파싱해서 리소스 같은 걸 읽어들이는 코드가 같이 빌드되었다면.. 도스 파트가 나중에 합쳐진 Windows 파트와 더불어 한 리소스를 공유하는 형태로 실행될 테니 이 역시 무척 흥미로울 것이다.

이 시점에서 문득 궁금해졌다.
링커가 얹어 주는 기본 stub 프로그램은 명령어가 겨우 몇 바이트밖에 되지 않는다. 얘들은 무슨 의미를 갖고 있는지, 혹시 옛날 16비트 NE 시대와 지금의 PE 시대에 stub 프로그램에 차이가 있는지..?
그래서 오랜만에 도스 API와 8086 어셈블리 명령어 레퍼런스까지 찾아서 stub 프로그램을 분석해 봤다.

stub 프로그램의 코드는 이게 전부이다.

(1) 0E        PUSH CS
(2) 1F        POP DS
(3) BA 0E 00  MOV DX,000E
(4) B4 09     MOV AH,09
(5) CD 21     INT 21
(6) B8 01 4C  MOV AX,4C01
(7) CD 21     INT 21
"문자열"


(1), (2) 맨 앞의 PUSH와 POP은 데이터 세그먼트를 코드 세그먼트의 값과 맞추는(DS=CS) 일종의 초기화이다. 스택에다가 CS 값을 넣은 뒤 그걸 DS로 도로 가져오는 거니까.
지금 이 프로그램은 화면에다 찍을 에러 메시지도 기계어 코드와 정확하게 같은 영역에 있으므로 저건 수긍이 가는 조치이다.

(3) 그 다음으로 DX 레지스터에다가 16진수로 0xE, 즉 14를 기록한다. 저 stub 프로그램은 길이가 정확하게 14바이트이다. 이 값은 프로그램의 시작 지점을 기준(0)으로 해서 그로부터 14바이트 뒤에 있는 문자열을 가리킨다.

(4) AX 레지스터의 high byte에다가 9를 기록한다.

(5) 이렇게 기록된 AX와 DX 레지스터 값을 토대로 0x21 인터럽트를 날려서 도스 API를 호출한다. 도스 API 중 9는 DX가 가리키는 주소에 있는 문자열을 화면, 정확히는 표준 출력에다가 찍는 기능을 수행한다.
그런데 굉장히 기괴한 점이 있는데.. 얘가 받아들이는 문자열은 null-terminated가 아니라 $-terminated여야 한다!

믿어지지 않으면 아무 Windows용 EXE/DLL이나 헥사 에디터로 열어서 앞부분의 에러 메시지 텍스트가 무슨 문자로 끝나는지를 확인해 보시기 바란다.
왜 그렇게 설계되었는지 모르겠다. 파일이나 디렉터리 이름을 받는 도스 API들은 당연히 null-terminated 문자열인데 말이다.

(6) 그 다음, AX 레지스터에다가 0x4C (high)와 0x1 (low)을 기록하고..

(7) 또 도스 API를 호출한다. 0x4C는 프로그램을 종료하는 기능을 하며, 종료와 동시에 low byte에 있는 1이라는 값을 에러코드로 되돌린다. 정상 종료는 0인데 1은 뭔가 오류와 함께 종료되었음을 나타낸다.
사실, 도스 API 레퍼런스를 보면 AH 값으로 0도 프로그램을 종료시키는 역할을 하는 듯하다(도스 1.0때부터 최초). 하지만 모종의 이유로 인해 그건 오늘날은 사용이 별로 권장되지 않으며 0x4C가 원칙이라 한다(도스 2.0에서부터 추가됨).

이렇게 분석 끝. 정말 간결 단순명료하다.
참고로 도스 EXE에서 헤더를 제끼고 기계어 코드가 시작되는 부분은 0x8~0x9 오프셋에 있는 unsigned short값에다가 16을 곱한 오프셋부터이다. 가령, 거기에 04 00 이렇게 적혀 있으면 0x40 오프셋부터 디스어셈블링을 해 나가면 된다. EXE는 헤더에 고정 길이 구조체뿐만 아니라 가변 길이인 '재배치 섹션'이 나오고 그 뒤부터 코드가 시작되기 때문이다.

그럼 과거 16비트 Windows에서 쓰이던 stub은 어떻게 돼 있었을까?
거의 차이가 없긴 한데, 문자열이 들어있는 위치와 얘의 주소를 전하는 방법이 달랐다.

(1) E8 53 00  CALL 0056
"문자열"
20 20 20 20 .. padding 후
(2) 5A        POP DX
(3) 0E        PUSH CS
(4) 1F        POP DS
(5) B4 09     MOV AH,09
(6) CD 21     INT 21
(7) B8 01 4C  MOV AX,4C01
(8) CD 21     INT 21


(1) 맨 먼저 JMP도 아니고 웬 CALL 인스트럭션이 나온다. 기계어로 표기할 때는 인자값이 0x53이어서 3바이트짜리 자기 자신 인스트럭션 이후에 0x53바이트 뒤로 가라는 뜻이 되는데, 영단어로 바꿔서 표기할 때는 자기 자신 원래 위치 기준으로 0x56바이트 뒤가 된다. 이 위치는 그냥 바로 다음 (2) 명령이 있는 곳과 같다.

(2) 함수 호출을 했는데 RET를 하는 게 아니라 스택을 pop하여 DX 레지스터에다 가져온다. 그렇다. 아까 그 call에 대한 복귀 주소에 문자열이 담겨 있으니, 아까 같은 하드코딩이 아닌 요런 방식으로 문자열 주소를 얹었다.

(3) (4) 이제부터는 아까처럼 DS = CS 해 주고,

(5)~(8) 아까와 동일. 문자열을 찍은 뒤 프로그램을 종료한다.

이런 초간단 초미니 프로그램은 exe가 아니라 com 형태로도 만들지 말라는 법이 없어 보인다. com은 그 어떤 헤더나 시그니처도 없이 첫 바이트부터 바로 기계어 코드와 데이터를 써 주면 되는.. 정말 원시적이기 그지없는 바이너리 덤프일 뿐이기 때문이다. 빌드 날짜, 버전, 요구하는 아키텍처나 운영체제 등등 그 어떤 부가정보도 존재하지 않는다.

요즘 프로그래밍 언어들이 기본 제공하는 런타임들의 오버헤드가 너무 크다 보니, 이에 대항하여 세상에서 제일 작은 "Hello world" 프로그램 이런 것에 집착하는 덕후들이 있다. Windows 프로그램의 경우 프로그램을 특수하게 빌드하여 CRT 라이브러리는 당연히 떼어내고, 코드와 데이터도 한 섹션에다 우려넣고, 거기에다 후처리까지 해서 단 몇백 바이트만으로 MessageBoxA(NULL, NULL, "Hello, world!", 0) 하나만 호출하는 프로그램을 만든 예가 있다.

그러나 이런 것들도 com 앞에서는 몽땅 버로우 타야 한다. 얘는 아예 파일 포맷 자체가 없으니까. 이 이상 더 줄일 수가 없다. com 형태로 만든 Hello world 프로그램은 겨우 20몇 바이트가 전부이다.
무슨 명령어를 내렸는지 기억은 안 나지만 컴퓨터를 재시작시키는 com 파일이 있었는데, 얘는 크기가 겨우 2바이트에 불과했다.

(1) BA 0C 01  MOV DX,010C
(2) B4 09     MOV AH,09
(3) CD 21     INT 21
(4) B8 01 4C  MOV AX,4C01
(5) CD 21     INT 21
그 뒤에 "Hello, world!$" 같은 문자열. 따옴표는 제외하고.


com은 exe처럼 코드/데이터 세그먼트 DS=CS 따윈 전혀 신경 쓸 필요 없이, 바로 본론부터 들어가면 된다. 그 대신 com은 16비트 단일 세그먼트 안에서 코드와 데이터 크기 한계가 모두 64K라는 치명적인 한계를 갖는다. 메모리 모델로 치면 그 이름도 유명한 tiny 모델 되겠다. 애초에 exe가 16비트 CPU에서 저 한계를 극복하고, 또 멀티태스킹에 대비하여 재배치도 가능하게 하려고 만들어진 포맷이기도 하다.

아, 아주 중요한 사항이 있다. com에서는 첫 256바이트, 즉 0x100 미만의 메모리 주소는 시스템용으로 예약되어 있어서 사용할 수 없다. 내 코드와 데이터는 0x100부터 시작한다. 그렇기 때문에 저 프로그램의 코드 크기는 12바이트이고, 문자열은 0xC 오프셋부터 시작하긴 하는데 거기에다가 0x100을 더해서 DX에다가는 0x10C를 써 줘야 한다.

Windows PE에다 비유하자면 0x100이 고정된 base address값인 셈이다. 그리고 DX의 값은 그냥 VA이지 RVA가 아니다.
과거에 굴러다니던 exe/com 상호 변환 유틸리티들이 하던 주된 작업 중 하나도 이런 오프셋 재계산이었다. 그리고 com에서 exe라면 모를까 더 넓은 곳에서 좁은 곳으로 맞추는 exe -> com은 아무 exe에 대해서나 가능한 게 물론 아니었다. (단일 세그먼트 안에서만 놀아야..) 과거 도스에 exe2bin이라는 외부 명령어가 있었는데 걔가 사실상 exe2com의 역할을 했다.

아무튼, 저 바이너리 코드와 문자열을 헥사 에디터를 이용해서 입력한 뒤, 파일을 hello.com이라고 명명하여 저장한다. 이걸 도스박스 같은 가상화 프로그램에서 도스 부팅하여 실행하면 신기하게도 Hello, world!가 출력될 것이다.
고급 언어를 사용하지 않고 컴파일러 나부랭이도 전혀 동원하지 않고 가장 원초적인 방법으로 나름 네이티브 실행 파일을 만든 것이다. 사용 가능한 코드와 데이터 용량이 심각하게 작다는 것과, 요즘 64비트 Windows에서는 직통으로 실행조차 할 수 없다는 게 문제이긴 하지만. (네이티브 코드라는 의미가 없다~!)

이런 식으로 컴퓨터에 간단히 명령을 내리고 램 상주 프로그램이나 바이러스 같은 것도 만들기 위해 옛날에는 debug.com이라는 도스 유틸리티가 요긴하게 쓰였다. 간단한 어셈블러/디스어셈블러 겸 헥사 에디터로서 가성비가 뛰어났기 때문이다. edlin 에디터의 바이너리 버전인 것 같다.

오늘날 어셈블리어라는 건 극소수 드라이버/컴파일러 개발자 내지 악성 코드· 보안· 역공학 같은 걸 연구하는 사람들이나 들여다보는 어려운 물건으로 전락한 지 오래다. 하지만 이것도 알면 디버깅이나 코드 분석에 굉장한 도움이 될 듯하다.
디스어셈블리 자체는 주어진 규칙대로 바이트 시퀀스를 몇 바이트씩 떼어서 명령어로 분해해 주는 비교적 간단한 작업일 뿐이다. 파서(parser)가 아니라 스캐너(scanner) 수준의 작업만 하면 된다.

하지만 디스어셈블리가 골치 아프고 귀찮은 이유는 코드의 첫 실행 지점을 정확하게 잡아서 분해를 시작해야 하며, 그래도 어느 게 코드이고 어느 게 데이터인지가 프로그램 실행 문맥에 의해 시시각각 달라지고 무진장 헷갈리기 때문이다. 데이터는 백 날 디스어셈블링 해 봤자 아무 의미가 없고, 오히려 코드의 분석에 방해만 된다. 이런 역공학을 어렵게 하기 위해서 디스어셈블러를 엿먹이는 테크닉도 보안 분야에는 발달해 있다.
하긴, 코드와 데이터가 그렇게 경계 구분 없이 자유자재로 변할 수 있는 게 "폰 노이만 모델 기반의 튜링 기계"가 누리는 극한의 자유이긴 하다.

Posted by 사무엘

2016/12/17 08:34 2016/12/17 08:34
, , ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1306

Visual Studio 201x, MSDN 이야기

1. 도움말 시스템

Visual C++ (지금의 Visual Studio)이 개발된 이래로 IDE가 제공하는 도움말 및 API 레퍼런스 시스템은 다음과 같이 변모해 왔다.

  • 1세대 1.x~2.x: 그냥 평범한 WinHelp 기반 hlp
  • 2세대 4.x, 5: 리치 텍스트(RTF) 기반의 자체적인 도움말 시스템이 IDE 내부에 통합되어 제공. 같은 컴퓨터 사양에서 RTF 기반 엔진은 이후에 등장한 IE+HTML 기반 엔진보다 텍스트 표시와 스크롤 속도가 훨씬 더 빨랐다.
  • 3세대 6: RTF 대신 HTML 기반의 외부 도움말로 갈아탐. MSDN이라는 명칭 정립.
  • 4세대 200x (.NET ~ 2008): HTML 기반이지만 CHM 말고 다른 컨테이너를 사용하는 Document Explorer. 도움말을 IDE 내부에 구동할 수도 있고 외부에 구동할 수도 있음. 융통성이 생겼다.
  • 5세대 201x: Help Viewer 도입. 버전도 1.0부터 리셋 재시작.

하긴, 비주얼 C++의 프로젝트 파일 포맷도 이와 거의 비슷한 단계를 거치며 바뀌어 왔다. vcp(1세대), mdp(2세대), 3세대(dsw/dsp), 4세대(sln/vcproj), 5세대(sln/vcxproj)의 순. 단, 비주얼 C++ 5는 2세대 도움말 기반이지만 프로젝트 파일은 예외적으로 3세대 6.0과 동일한 dsw/dsp기반이다.

본인은 지금의 일명 5세대 도움말 시스템을 별로 좋아하지 않았다.
일단 5세대 시대를 처음으로 시작한 Visual Studio 2010은 후대 버전은 안 그런데 얘만 유독 무겁고 시동 속도가 무척 느렸다.
그리고 같이 내장된 Help Viewer 1은 '색인' 탭으로 가면 심한 랙이 걸려서 몹시 불편했다. 재래식 4세대 도움말에 비해 기능 차이는 별로 없는데 느리고 무거워지기만 해서 학을 뗐다.

그나마 2012부터는 IDE가 가벼워지고 도움말의 랙도 없어진 듯하다. 그 대신 2010에는 없던 다른 사이드 이펙트가 생겼다.
첫 구동되어서 Help Viewer 스플래시 화면이 뜰 때 마우스 포인터가 움직이지 않을 정도로 컴퓨터가 잠시 stun(멈칫)된다. 구닥다리 내 컴에서만 그런 줄 알았는데 회사의 초고성능 최신식 컴퓨터에서도 동일한 현상이 발생한다.

먼 옛날의 불안정한 유리몸이던 Windows 9x도 아니고 엄연히 7~10급의 최신 OS에서 하드웨어를 도대체 어떻게 건드렸길래 마우스 포인터조차 움직이지 않는 상태가 되나?

잘 알다시피 요즘 Visual Studio IDE는 평범한 Win32 API로 GUI를 만드는 게 아니라 닷넷 + Windows Presentation Foundation 기반으로 특수하게 하드웨어 가속도 받으면서 아주 뽀대나는 방식으로 그래픽을 출력한다.
글자를 찍는 계층도 뭐가 바뀌었는지, 텍스트 에디터에는 트루타입 글꼴만 지정되지 FixedSys 같은 비트맵 글꼴을 사용할 수 없게 바뀌었다. '굴림'은 트루타입이니 사용은 가능하지만 embedded 비트맵이 대신 찍히는 크기에서도 ClearType이 적용되어 색깔이 살짝 바뀌어 찍히며, 같은 글자끼리도 폭이 좀 들쭉날쭉하게 찍힌다.

이렇듯, 재래식 GDI API로 글자를 찍었다면 절대로 나타나지 않을 사이드 이펙트들이 좀 보인다.
그런 특수한 그래픽/GUI를 사용하기 위해서 마치 게임 실행 전처럼 하드웨어 초기화가 일어나고, 그때 마우스 포인터가 살짝 멈추는가 하는 별별 생각이 든다.

2. GDI API 설명은 어디에?

요즘(2010년대) Visual Studio의 MSDN 레퍼런스엔 왜 GDI API들이 누락돼 있는지 궁금하다. BitBlt, SetPixel 같은 것들. desktop app development에 해당하는 몇백 MB짜리 도움말을 분명히 설치했는데도 로컬 도움말에 포함되지 않아서 저것들 설명은 느린 인터넷 외부 링크로 대체된다.

VS 2010에서는 GDI 관련 API들이 색인으로는 접근 가능하지만 목차에서는 존재하지 않아서 접근불가였다. 그리고 MFC 레퍼런스도 단순한 API wrapper의 경우(가령 CDC::MoveTo) See also 란에 자신의 원래 API 함수에 대한 링크(가령 MoveToEx)가 있는데, 요건 내부 링크가 아니라 인터넷 MSDN 사이트의 외부 링크로 바뀌어 있었다.

즉, 그때부터 GDI API의 설명은 제외될 준비를 하고 있었던 듯하다. 그 뒤로 2012인가 2013 이후부터는 그것들이 색인에서도 제외되고 완전히 없어졌다. 2015도 마찬가지인 걸 보니 GDI의 누락은 단순 지엽적인 실수가 아니라 의도적인 계획인 것으로 보인다.

kernel32, user32, advapi32 등 나머지 API들은 다 남아 있는데 왜 GDI만 없앴는지, 얘는 정말로 완전히 deprecate 시킬 작정인지 알 길이 없다. Windows NT 3.1 초창기 때부터 20년이 넘게 운영체제의 중추를 구성해 온 놈인데 그걸 호락호락 없애는 게 가능할까? 게다가 BeginPaint, GetDC처럼 GDI를 다루지만 실제로는 USER 계층에 속해 있는 기초 필수 API조차 언급이 누락된 것은 좀 문제라고 여겨진다.

이런 것 때문에 본인은 Visual Studio는 옛날 Document Explorer 기반이던 200x도 여전히 한 카피 설치해 놓고 지낸다.
옛날에는 또 Visual C++ 2005의 MSDN만 TSF API 레퍼런스도 없고 뭔가 나사가 빠진 듯이 컨텐츠가 왕창 부실해서 내가 놀랐던 기억이 있다. 2003이나 2008은 안 그랬고 걔만 좀 이상했었다.

3. 프로젝트에 소속되지 않은 소스 코드도 심층 분석

Visual C++. 2013인지 2015인지 언제부턴가 프로젝트에 등재되지 않은 임의의 C/C++ 소스 코드를 열었을 때도 이 파일을 임시로 파싱해서 인텔리센스가 동작하기 시작했다. 이거 짱 유용한 기능이다.
전통적으로 프로젝트 소속이 아닌 파일은 문맥을 전혀 알 수 없으며 빌드 대상도 아니기 때문에 IDE에서의 대접이 박했다. 정말 기계적인(문맥 독립적이고 명백한) 신택스 컬러링과 자동 들여쓰기 외에는 자동 완성이나 인텔리센스 따위는 전혀 제공되지 않았다. 전혀 기대를 안 하고 있었는데 이제는 걔들도 miscellaneous file이라는 범주에 넣어서 친절하게 분석해 준다.

4. Spy++

Visual C++에는 프로그램 개발에 유용하게 쓰일 만한 아기자기한 유틸리티들이 같이 포함돼 있다.
'GUID 생성기'라든가 '에러 코드 조회'는 아주 작고 간단하면서도 절대로 빠질 일이 없는 고정 멤버이다.
옛날에는 'OLE/COM 객체 뷰어'라든가 'ActiveX 컨트롤 테스트 컨테이너'처럼 대화상자가 아닌 가변 크기 창을 가진 유틸리티도 있었는데 OLE 내지 ActiveX 쪽 기술이 인기와 약발이 다해서 그런지 6.0인가 닷넷 이후부터는 빠졌다.

그 반면, 기능이 제법 참신하면서 1990년대부터 지금까지 거의 20년 동안 변함없이 Visual C++과 함께 제공되어 온 장수 유틸리티는 단연 Spy++이다.
얘는 제공하는 기능이 크게 변한 건 없었다. 다만 아이콘이 초록색 옷차림의 첩보요원(4.x..!), 분홍색 옷차림(6.0~200x), 검정색 옷차림(2010~현재)으로 몇 차례 바뀌었으며, 운영체제의 최신 메시지가 추가되고 도움말이 hlp에서 chm으로 바뀌는 등 외형만이 최소한의 유지보수를 받아 왔다.

아, 훅킹을 사용한다는 특성상 2000년대 중반엔 64비트 에디션이 따로 추가되기도 했다. 하지만 GUI 껍데기는 x86용 하나만 놔두고 64비트 프로그램에 대해서는 내부적으로 64비트 서버 프로그램을 실행해서 얘와 통신을 하는 식으로 프로그램을 개발하면 더 깔끔했을 텐데 하는 아쉬움이 남는다. 그러면 사용자는 겉보기로 한 프로그램에서 32비트와 64비트 구분 없이 창을 마음대로 들여다보고 훅킹질을 할 수 있을 테니 말이다.

실제로 <날개셋> 입력 패드도 그런 식으로 동작하며, 당장 Visual C++ IDE도 내부적으로 64비트 IPC 서버를 따로 운용하기 때문에 IDE 자체는 32비트이지만 64비트 프로그램도 아무 제약 없이 디버깅이 가능하다. 하지만 안 그래도 훅킹을 하느라 시스템 성능을 잡아먹는 프로그램인데.. 성능 문제 때문에 깔끔하게 64비트 에디션을 따로 빌드한 것일 수도 있으니 Spy++ 개발자의 취향은 존중해 주도록 하겠다.

Spy++는 워낙 역사가 긴 프로그램이기 때문에 초창기 버전은 창/프로세스들의 계층 구조를 전용 트리 컨트롤이 아니라 리스트박스를 정교하게 서브클래싱해서 표현했다. 쉽게 말해 과거 Windows 3.1의 파일 관리자가 디렉터리 계층 구조를 표현한 방식과 비슷하다. 사실은 리스트박스에서 owner draw + user data로 계층 구조를 표현하고 [+/-] 버튼을 눌렀을 때 하부 아이템을 표시하거나 숨기는 건 1990년대 초반에 프로그래밍 잡지에서 즐겨 다뤄진 Windows 프로그래밍 테크닉이기도 했다.

그러다가 VC++ 2005인가 2008 사이쯤에서 Spy++은 운영체제의 트리 컨트롤을 사용하는 걸로 리팩터링이 됐다. 사용자의 입장에서는 기능상의 변화가 없지만 내부적으로는 창을 운용하는 방식이 완전히 바뀐 것이기 때문에 이건 내부적으로 굉장히 큰 공사였으리라 여겨진다.

그런데 VC++ 2010과 함께 제공된 Spy++는 일부 단축키들이 동작하지 않는 버그가 있었다. 전부 먹통인 것도 아니고 창 찾기 Alt+F3, 목록 새로 고침 F5, 속성 표시 Alt+Enter 같은 게 동작하지 않아서 프로그램을 다루기가 불편했다. 이 버그는 잠깐 있었다가 다시 2012 이후에 제공되는 Spy++부터는 고쳐졌다.

Posted by 사무엘

2016/12/03 08:31 2016/12/03 08:31
, ,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1301

1.
잘 알다시피 C언어는 원래 '이식성 있는 어셈블리'를 표방할 정도로 고수준 언어의 탈을 쓴 뭐랄까.. 안에서 돌아가는 모든 내부 과정이 있는 그대로 투명하게 노출되고, 프로그램이 메모리 내부에서 다루는 모든 물건들은 비트와 바이트 단위로 접근 가능한 식으로.. 모든 것을 프로그래머 재량에 맡기는 가볍고 이상야릇한 언어라는 성격이 강했다.

그래서 static/global을 제외하면 변수값의 초기화도 몽땅 수동으로 해야 하고, 배열 첨자 체크가 없고 심지어 문자열 타입도 없고.. 뭐 그랬다.
그 대신 공용체와 비트필드 같은 변태스러운 물건은.. C 말고 도대체 다른 어떤 언어에서 찾을 수 있겠는가? 단적인 예로, 부동소수점을 부호, 지수, 가수부별로 쪼개서 내부 구조가 어떻게 돌아가는지 보여주는 프로그램을 C 말고 다른 언어로 만드는 건 직관적이지 못하고 꽤 귀찮은 일이 될 것이다.

내가 직접 코드를 작성하지 않았는데 C가 언어 차원에서 자동으로 해 주는 일이라고는 환경변수 세팅이라든가 main 함수에 전달되는 argument의 파싱 같은 정말 최소한의 초기화밖에 없다시피했다.

하지만 C++은 언어 차원에서 몰래 하는 일이 더 있다. 우리가 빌드하는 프로그램에 코드가 추가되기 때문에 그 존재감과 오버헤드에 대해서 최소한의 인지는 하고 있어야 하는 것들이 종종 있다.
생성자와 소멸자, 임시 R-value 오브젝트, 암시적인 형변환 같은 건 그야말로 기본 중의 기본이고.. 가상 함수가 호출되는 원리도 아주 흔한 예다. C로 표현하자면 pData->vptr->pfnFuncXXX(pData, ...) 과 같은 급의 다단계 포인터 참조 오버헤드가 발생한다. 이런 건 C++ 한다는 사람이 아무리 초짜라도 절대로 몰라서는 안 된다.

가상 상속 정도면 가상 함수보다는 훨씬 볼 일이 없는 물건이다. 컴파일 타임 때 미리 계산된 오프셋으로 기반 클래스를 참조하는 게 아니라 기반 클래스의 위치 자체를 포인터를 통해 런타임 때 얻어 온다고 생각하면 된다.

더 어려운 걸로 내려가자면 pointer-to-member가 구현된 원리가 있는데, 이것도 forward 선언된 클래스 + 다중 상속이라는 변수를 만나면 내부 구현이 더럽게 복잡해지며 컴파일러간의 바이너리 호환성도 깨진다. C++에서 한번 홍역을 치른 뒤에 다른 언어에서는 별로 도입할 생각을 안 하고 있다.

global scope에 속한 객체들이 생성자와 소멸자가 호출되는 타이밍, 순서와 원리도 알아두면 좋다. 이식성을 위해서는 global 객체를 만들지 말고, 번거롭지만 차라리 포인터만 만들어 놓고 new와 delete를 프로그램이 수동으로 하는 게 권장되고 있다.

Exception이라는 것도 아주 요상한 물건이고...
끝으로, 언어 차원에서 지원되기 시작한 RTTI(런타임 시점에서의 타입 정보 인식) 기능도 있다. 하지만 이건 제대로 쓰이지는 않는 것 같다. dynamic_cast, typeid 같은 연산자 말이다. 가상 함수가 존재하는 모든 클래스들에는 자동으로 언어 차원에서의 타입 식별 정보가 추가된다.

얘는 구현 오버헤드가 만만찮으며, 언어의 기능에 의존하지 않고 자체적으로 RTTI를 구현한 레거시 코드도 많기 때문에 결국 컴파일러 옵션이 지정되었을 때만 지원되는 기능이 되었다. Visual C++의 경우 /GR 옵션이다.
개발 역사가 오래 됐고 다중 플랫폼을 지원하는 어지간한 프로젝트들은 이 기능을 사용하지 않는다. 마치 문자열 클래스만큼이나 파편화와 중복 구현이 난립해 있다.
사실, RTTI가 제대로 지원되려면 가상 함수가 존재하는 모든 오브젝트들이 공통으로 상속하는 베이스 클래스라는 개념도 있어서 그 베이스 클래스에서 타입 식별과 관련된 멤버들을 제공해야 하지 않나 싶다.

C++은 언어 차원에서의 개입을 최소화한다는 철학을 가진 언어에서 출발했는데 점점 기능이 비대해지고 언어 차원에서의 개입이 늘고 있다.

2.
포인터는 CPU가 메모리 위치를 식별할 때 사용하는 숫자로, 일반적으로는 machine word와 다를 바 없는 아주 가볍고(= 함수 인자로 값을 그대로 넘겨줄 수 있는) 단순한 자료형이다.
여느 자료형의 포인터는 정수와 reinterpret_cast로 형변환이 가능하다. 함수의 포인터는 + - 산술 연산이 되지 않지만 그래도 역시 정수와 교환이 된다.

하지만 포인터가 machine word 하나와 딱 대응하지 않을 때도 있다.
과거 16비트 시절에는 64KB보다 더 큰 영역의 메모리에 접근하기 위해 세그먼트 번호를 추가로 묶은 far pointer라는 게 있었으며 far은 예약어였다. 뭐 그래 봤자 이 포인터는 32비트 long 정수 하나에 대응했으니, Windows 프로그래밍에서는 L이라는 접두어로 원거리 포인터를 표현했다. LPSTR, LPVOID, LPCWSTR 등.

32/64비트로 오면서 그런 구분이 없어졌기 때문에 접두어 L은 불필요한 잉여가 되었다. 본인 역시 PSTR, PVOID, PCWSTR이라고만 쓴다.
단, PVOID는 winnt.h에 typedef로 정의돼 있는데 const void *는 왜 PCVOID라고 정의돼 있지 않고 여전히 LPCVOID만 있는지는 본인이 알 길이 없다. 믿어지지 않으면 한번 검색해 보시기 바란다. 정말 없다.

그리고 다음으로 machine word 하나와 딱 대응하지 않는 대표적인 기괴한 포인터는 아까도 잠깐 언급됐던 C++의 멤버 포인터이다. 다중 상속은 포인터간의 형변환이 일어났을 때 단순 언어적인 semantic뿐만 아니라 주소값 자체가 바뀔 수도 있는 상황을 만들었으며, pointer-to-member는 이를 보정하는 정보를 담느라 크기가 언제나 machine word 하나에 딱 들어가는 게 보장되지 않게 만들었다.

그래서 멤버 포인터는 신기하게도 reinterpret_cast나 C-style 캐스트로도 결코 숫자로 형변환이 되지 않는다. 숫자 하나가 아니라 구조체 같은 완전 생뚱맞은 자료형으로 취급된다. 크기와 내부 구현이 어떻게 가변적으로 달라질지 모르기 때문에 이것만은 C의 철학과는 정반대로 내부 구현과 접근을 프로그래머로부터 싹 감추고 숨겨 버렸다. 이거 굉장한 이질감이 느껴지지 않으신가?

자주 발생하는 일은 아니지만 구조체에서 어떤 멤버가 구조체의 시작 지점으로부터 정확하게 몇 바이트째 오프셋에 있는지 알고 싶을 때가 있다. 당연한 말이지만 이건 컴파일 타임 때 값이 결정되는 상수이다.
이럴 때 흔히 사용하는 방법은 &((STRUCTURE *)0)->member이다. 이렇게 해도 동작은 잘 하지만 그래도 더 깔끔한 방법이 있었으면 좋겠다는 생각이 든다.

개인적으로는 &STRUCTURE::member가 제일 직관적이고 깔끔한 형태라고 생각한다. 이건 pointer-to-member에 대입 가능한 멤버 주소를 얻을 때 사용하는 문법이다.
member가 static 데이터 멤버라면 저 값은 그놈 자신의 주소가 될 것이고, non-static이라면 메모리 주소가 아니라 자신의 오프셋이 된다. 비록 pointer-to-member(데이터 멤버)가 단순 오프셋의 superset으로서 그 이상의 추상적인 자료형이긴 하지만, 결국은 내부적으로도 오프셋을 갖고 있는 꼴이기 때문에 int형으로 reinterpret_cast도 됐으면 하는 생각이 든다. &((STRUCTURE *)0)->member을 안 써도 되게 말이다.

요즘 C++이 캡처가 없는 람다에 한해서 람다를 함수 포인터로 캐스트하는 것도 지원하듯이, 저것도 같은 맥락에서 정수형과 호환됐으면 하는 아쉬움이 남는다.

Posted by 사무엘

2016/11/22 08:33 2016/11/22 08:33
,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1297

1.
예전에도 한번 이런 비유를 꺼낸 적이 있었는데.. 라면을 소프트웨어 플랫폼에다 비유하자면 봉지 라면은 PC, 사발면은 태블릿, 컵라면은 스마트폰 정도에 대응하는 것 같다. 그래서 한 플랫폼에서 잘나가던 라면이 다른 플랫폼으로 종종 포팅되곤 한다(카카오톡 PC 버전, 오피스 안드로이드 버전처럼). 비록 둘이 맛이 완전히 동일하지는 않지만 말이다.

식당에서 주문해서 먹는 라면은 집 밖의 거대한 다른 가게에 들어가서(서버 접속) 먹는 것이니 서버 사이드 웹 애플리케이션일 것이며..
분식점 같은 식당 납품을 목적으로 라면 제조사가 면이나 스프만을 대량으로 따로 파는 건 '엔진' 같은 미들웨어 컴포넌트 내지 라이브러리에 대응한다고 볼 수 있겠다.

2.
스마트폰은 컴퓨터와 달리.. (1) 특별한 일이 없는 한 24시간 켜져 있고, (2) 열받고 뜨거워질지언정 그래도 팬 돌아가는 소리가 안 나고, (3) 보조 기억장치가 있지만 하드디스크 돌아가는 것 같은 소리는 전혀 없다.
그래서 (2)와 (3)을 종합하면 스마트폰은 아주 조용하다. 게다가 얇기까지 하다.
어찌 보면 세상에 어떻게 이런 컴퓨터가 존재 가능해졌는지 신기한 노릇이 아닐 수 없다. 그것도 화면은 옛날 구닥다리 액정 같은 단색이 아니라, 고해상도 천연색 그래픽을 찍어 낸다. CPU뿐만 아니라 디스플레이나 메모리까지 총체적으로 왕창 발전했기 때문에 스마트폰이 만들어질 수 있었다.

옛날에는 뭔가 영상이 표시되는 기계 자체가 굉장히 미래 하이테크의 상징이었다. 집 현관을 표시해 주는 인터폰이나 자동차 내비 같은 거 말이다.
텔레비전이나 컴퓨터 모니터는 아날로그 신호에 둥그런 브라운관 형태로나마 진작부터 천연색을 표현할 수 있었다. 하지만 들고 다닐 수 있는 소형 텔레비전이나 인터폰, CCTV 같은 건 원가 때문인지 무엇 때문인지, 의외로 흑백 버전이 2000년대까지 쓰였다. 본인은 몇 차례 이사를 다니며 집을 옮긴 적이 있지만, 컬러 화면이 나오는 인터폰 실물을 태어나서 지금까지 한 번도 구경을 못 해 봤다.

그런데 어느 샌가 갑자기 CCTV의 화질이 급격히 향상되고 차량들이 개나 소나 내비에 블랙박스까지 달고 다니면서 블랙박스에 찍힌 사고 영상만 모아서 보여 주는 TV 프로가 큰 인기를 모을 정도가 됐다. 사진과 동영상을 즉각 생성해서 남들 보는 사이버 공간에 용량과 트래픽 걱정 없이 올리는 게 너무 금방, 쉽게 가능해졌다. 이건 1980년대의 SF물들이 제대로 상상하지 못한 너무 엄청난 변화임이 틀림없다.

그리고 컴퓨터 자체도.. 이젠 스마트폰 내부에서 가상 머신을 돌려서 도스는 말할 것도 없고 과거의 Windows 9x를 구동할 수도 있게 됐다. 머리만 비교하면 스마트폰의 CPU가 일반 데스크톱 PC의 CPU와도 성능이 호각이 됐으며, 단지 PC에 비해 부족한 건 입력 장치와 하드디스크 정도밖에 없다고 한다. 발열이나 전원의 한계는 차치하고라도 말이다.

모바일 플랫폼이 등장하면서 PC에서 x86 계열 CPU + Windows 계열 운영체제를 총칭하는 '윈텔' 독점 구도도 상당 부분 흔들리게 됐다. 완전히 새로운 형태의 시장 수요를 창출해 냈으니까. x86은 30년을 넘게 거슬러 올라가는 유구한 하위 호환성을 자랑하지만, 그 때문에 저전력 모바일에서 빠릿빠릿 움직이는 용도로는 상당히 부적합한 CPU가 돼 버려서 말이다. Windows도 마찬가지다.

다만, 단순히 이미 만들어진 정보들을 받아 보기만 하는 인터넷 단말기 이상으로, 뭔가 글쓰기나 코딩 같은 생산적인 활동을 하기에는 스마트폰은 문자 입력이 너무 불편한 게 흠이다. 구닥다리 타자기의 인터페이스를 답습하고 있지만 그래도 문자 입력 분야에서 키보드만 한 가성비를 제공하는 물건은 아직까지 없다.

예전에 그나마 전화기 버튼이라도 있던 시절에는 3*4 배열이라는 틀은 고정돼 있었는데..
요즘 스마트폰은 화면의 절대적인 크기나 종횡비까지 전부 그냥 흰 도화지 수준인 거 같다. 인간에게 가장 적합한 글쇠 scheme은 어떤 형태일까? 블루 오션이다 보니 먼저 연구해서 표준 틀을 정착시키는 사람이 그냥 장땡이 돼서 혼자 다 해먹을 수 있을 것 같은 생각이 드는데.. 난 잘 모르겠다. 난 한글 입력 쪽은 글쇠배열이 아니라 일단은 근본 메커니즘 연구가 주 관심 분야인지라..

글쇠 수가 너무 많으면 안 그래도 작은 화면에 너무 작은 글쇠 버튼을 잘못 찍어서 오타를 내기 쉽고, 반대로 글쇠 수가 너무 적으면 타수가 늘어나고 이것저것 모드를 바꾸는 빈도가 잦아져서 그것대로 또 입력이 불편해진다.
구글 단모음을 한동안 써 보다가 불편해서 다시 나랏글로 돌아왔다. ㅎ, ㅔ 같은 자모를 한 번에 바로 입력할 수 있어서 편한 것보다, 오타가 나서 불편한 게 더 크게 느껴졌다. 개인적으로는 나랏글을 거의 2004년부터 10년 넘게 쓰기도 했고 말이다.

3.
스마트폰이 폭발적인 인기를 끌면서 오늘날과 같은 사진· 동영상 업로드 문화를 만들어 낸 건 두 말할 나위 없이 '디지털 카메라' 기능까지 전화기 안에 쏙 들어간 덕분에 가능했다.
오늘날 폰의 카메라가 단순 화소수와 색감만 따지자면 어지간한 보급형 디카의 성능을 다 따라잡고도 남는다. 하지만 폰 카메라가 전용 디지털 카메라를 결코 따라잡지 못하는 게 크게 둘 있는데, (1) 줌과 (2) 부팅 속도이다.

근본적으로 카메라의 형태로 적합하게 설계되지 않은 그 얇은 몸체에다 두꺼운 다기능 렌즈까지 우겨넣는 건 아무래도 무리다. 그렇기 때문에 폰 카메라는 줌 기능이 전문적인 카메라의 적수가 될 수 없다. 시야각도 한계를 받기 때문에 이걸 극복하려면 별도의 파노라마 합성 앱 같은 것의 도움을 받아야 한다.

또한 디지털 카메라는 사진을 찍을 때에만 잠시 켰다가 끄는 걸 스마트폰보다 훨씬 더 간편하게 할 수 있기 때문에 밖에서 사진을 몇백 장씩 산발적으로 찍을 일이 있을 때 전력 소모 부담이 훨씬 덜하다. 부팅도 아예 범용 컴퓨터인 스마트폰보다야 비교할 수 없이 더 빨리 되며, 전원을 켜자마자 거의 곧장 촬영 ready 상태가 된다. 그 반면 스마트폰은 이런 특성을 전혀 갖고 있지 못하다.

하긴, 피처폰이 스마트폰으로 바뀌고 스마트폰에 온갖 복잡 다양한 기능들이 추가될수록 사용자가 알게 모르게 치르는 대가로는 배터리 시간이라든가 폰의 물리적 내구성 같은 게 있다. 이와 비슷한 맥락에서 스마트폰도 켠 직후에 수 초 이내로 바로 쓸 수 있는 게 아니라, PC에 준하는 급의 부팅이 필요하고 엄청난 양의 초기화와 캐싱, pre-fetching을 해 줘야 쓸 수 있는 물건이 되고 있다. 예전에 PDA나 공학용 계산기가 그렇게 부팅 시간이 긴 물건은 아니었으니 말이다. 부팅이 존재하고 악성 코드 걱정을 해야 하는 기기는 다른 전자 기기와는 성격이 근본적으로 다르며 훨씬 더 능동적인 물건이다.

한때는 이런 작은 화면에 찍히는 글자는 초간단 비트맵 글꼴 기반인 게 당연시되었는데 그게 힌팅까지 적용된 미려한 윤곽선 글꼴로 바뀌었다는 것 하나만으로도 소프트웨어적으로는 예전에 비해 그야말로 엄청난 부담이 추가된 거나 다름없다. 윤곽선 글꼴은 캐싱 없이는 도저히 쓸 물건이 못 되며, 캐싱이라는 건 굉장한 양의 메모리를 요구하기 때문이다.

오늘날 컴퓨터 프로그램들이 같은 일을 해도 예전보다 메모리와 CPU를 훨씬 더 많이 요구하는 이유는 유지 관리 차원에서의 범용성과 추상성을 높인 대신에 오버헤드가 더 커지고 성능 희생을 감수한 게 매우 크게 작용한다(가상 머신, 가상 함수, 등등등등). 스마트폰의 전력 소비나 부팅 속도도 그런 맥락에서 살펴볼 수 있을 듯하다.

Posted by 사무엘

2016/11/05 08:37 2016/11/05 08:37
, , , ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1290

컴퓨터 프로그램에서 실행 제어라 하면 조건과 분기, 반복, 예외 처리 같은 것들이 있는데.. 절차형 프로그래밍 언어에서는 이런 게 예약어와 블록 구조 같은 걸로 표현되고, 단순 함수 호출이나 연산은 위에서부터 아래로 순차적으로 수행되는 편이다.

그런데 C 언어는 타 언어었으면 예약어를 써서 구현되었을 실행 흐름 제어도 다 함수로 구현되는 경우가 종종 있다. 몇 가지 예를 들면 이렇다. 코드 정적 분석 프로그램 같은 걸 만든다면 이런 건 함수 차원에서 예외적으로 다뤄져야 한다.
C가 저수준이라는 소리를 괜히 듣는 게 아닌 듯하다.

1. signal

시스템 차원에서 인터럽트가 발생했을 때 실행될 콜백 함수를 지정한다. Ctrl+C나 Ctrl+Break가 눌리는 것도 이런 상황에 포함되나, Windows의 경우 C 표준을 준수하느라 함수는 동일해도 Ctrl 키 인터럽트는 다른 인터럽트와는 꽤 다른 방식으로 따로 처리된다. (Windows API에 SetConsoleCtrlHandler이라는 함수가 있음) 사실, Windows는 자체적인 예외 처리 함수 지정 메커니즘도 제공한다.
현대의 언어라면 다 try ... catch로 처리했을 사항들이다. SIG* 상수들은 catch 구문에다 별도의 값이나 타입으로 전달되고 말이다.

2. setjmp/longjmp

C 언어에 이런 함수도 존재한다는 걸 처음 알았을 때 굉장히 놀랐었다. goto는 한 함수 안에서만 분기가 가능하지만 얘는 아예 함수의 경계를 초월하여 이전의 setjmp 실행 직후 상황으로 분기를 시켜 주기 때문이다. 이 함수는 다음과 같이 사용하면 된다. 개념적으로 운영체제의 '시스템 복원'을 생각해도 쉽게 이해할 수 있다.

#include <setjmp.h>
jmp_buf jb;

void Func(int n)
{
    printf("%d\n", n);
    if(n==5) longjmp(jb, 0); else Func(n+1);
}

int main()
{
    if(setjmp(jb)) {
        puts("recursion interrupted.");
    }
    else {
        puts("OK, try");
        Func(0);
    }
    return 0;
}

jmp_buf라는 버퍼 자료형을 선언한다. 얘는 배열에 대한 typedef이며, 함수의 인자로 전달될 때는 자동으로 포인터처럼 취급된다. 그렇기 때문에 setjmp, longjmp의 인자로 전달할 때 &를 붙일 필요가 없으며, 그리고 안 붙이더라도 언제나 내부 컨텐츠는 call by reference처럼 취급된다. jb는 매번 함수의 인자로 전달할 게 아니라면 그 특성상 전역변수로 선언해 놓는 게 속 편하다.

그럼, setjmp를 호출하여 되돌아가고 싶은 지점에 대한 스냅샷을 만든다. 스냅샷을 만든 직후에는 setjmp의 리턴값이 0인 것으로 간주된다. 그래서 위의 코드에서는 "OK, try"가 먼저 출력되고 Func가 호출된다.

나중에 Func가 굉장히 복잡하게 실행된 뒤에 이것들을 몽땅 한 큐에 종료해야겠다 싶으면 longjmp를 호출한다. 그러면 얘는 아까 setjmp를 호출한 곳에서 함수가 0이 아닌 값이 리턴된 상황으로 모든 컨텍스트가 '원상복귀' 된다. 그래서 "recursion interrupted"가 출력되고 실행이 끝난다.

구체적인 리턴값은 longjmp의 인자에다가 줄 수 있다. 다만, 여기에다가 0을 지정하면 setjmp가 처음 호출되어 0이 리턴된 것과 구분이 되지 않기 때문에 setjmp의 리턴값이 1인 것으로 값이 일부러 보정된다.

위의 코드는 예외 처리 구문을 사용한 다음 코드와 실행 결과가 완전히 동일하다. 이번에도 try, catch가 답이다. 언어 차원에서 예약어를 동원해서 구현했을 기능이 그냥 함수로 처리되어 있다는 얘기가 바로 이런 의미이다.

void Func(int n)
{
    printf("%d\n", n);
    if(n==5) throw 1; else Func(n+1);
}

int main()
{
    try {
        puts("OK, try");
        Func(0);
    }
    catch(int e) {
        puts("recursion interrupted.");
    }
}

setjmp/longjmp는 언어 차원에서 제공되는 기능이 아니다 보니, 저렇게 함수들을 이탈할 때 C++ 객체들의 소멸자 함수 처리가 제대로 되지 않는다는 한계도 있다. 가변 인자만큼이나 C와 C++의 기능이 서로 충돌하는 지점이다.
그래도 얘는 시스템 프로그래밍 차원에서 고유한 용도가 있다 보니, 이들 함수가 현대의 컴파일러에서 deprecate됐다거나, 뭔가 기능이 보강된 *_s 버전이 생겼다거나 하지는 않다.

3. fork

새로운 실행 주체를 생성하는 함수라는 점에서 Windows의 CreateProcess나 CreateThread와 얼추 비슷하다. 그러나 생성하는 방식은 완전히 다르다.
Windows에서는 프로세스를 생성할 때 파일명을 주며, 그 프로세스는 완전히 처음부터 다시 실행된다. 그리고 스레드는 콜백 함수를 지정해서 생성하며, 그 콜백 함수의 실행이 끝나면 스레드 역시 종료되어 사라진다.

그러나 fork는 지금 나 자신과 메모리 구조와 스택 프레임, 내부 상태 문맥 같은 게 완~전히 동일한 프로세스가 하나 또 실행된다. 그래서 fork를 처음 호출한 기존 프로세스는 fork의 리턴값이 nonzero인 것으로 간주되어 실행이 계속되며, 새로 생성된 프로세스는 리턴값이 0인 것처럼 간주되어 실행이 계속된다. 굉장히 신기한 결과인데, 함수의 디자인 방식이 setjmp와 미묘하게 비슷하다고 볼 수도 있는지는 잘 모르겠다.

//공통 처리 진행 후,
if(fork()==0) {
    //분기된 자식 프로세스 문맥. 하지만 공통 부분에서 만들어 뒀던 변수들에 접근 가능함.
}
else {
    //'공통'을 실행하던 부모 프로세스 문맥
}

(뭐, 정확히는 실행이 성공하면 양수가 돌아오고, 실패하면 음수가 돌아오니 이건 마치 GetMessage의 리턴값만큼이나 주의할 필요는 있다.)

Windows API에는 저렇게 모든 실행 문맥을 그대로 복제해서 자신의 분신 프로세스를 만드는 함수가 존재하지 않는다. 프로그램들 내부에 포인터들까지 있다는 점까지 감안하면 정말 주소 공간이 문자 단위로 정확하게 일치해야 할 텐데, 그걸 그대로 복제하는 건 성능 오버헤드가 크지 않겠나 하는 생각도 든다.

fork는 프로세스를 생성하는 놈이다 보니 CreateProcess와 마찬가지로 비동기적으로 실행된다. 앞서 소개한 signal도 인터럽트 함수가 이론적으로 비동기적으로 실행될 수 있다. set/longjmp는 하는 일은 기괴해도 그래도 프로세스/스레드를 넘나드는 물건은 아니니 대조적이다.

그래서 signal 핸들러나 fork를 사용하는 코드에서는 주의해야 할 점이 있는데, 버퍼를 사용하는 고수준 IO 함수를 사용해서는 안 된다. 쉽게 말해 Hello, world를 찍을 때도 간단하게 printf나 puts를 쓰지 말고 write(1, "Hello", 5)라고 좀 번거로운 방법을 써야 한다. 비동기적인 환경에서 여러 실행 단위가 고수준 IO에 동시에 접근하면 출력이 꼬일 수 있기 때문이다.

먼 옛날 대학 시절, 시스템 프로그래밍 숙제를 하던 시절에 어떤 수강생이 뭘 잘못 건드렸는지 자식 프로세스를 무한 생성하는 삽질을 했고, 이 때문에 학과 서버가 몽땅 다운되어서 수강생들이 과제를 할 수가 없어지는 사태가 벌어졌다. 이 정도면 그 학생뿐만 아니라 계정별로 자원 할당 한계 관리를 제대로 하지 않은 서버 관리자에게도 책임이 있지 않나 생각이 든다만, 어쨌든 이것 때문에 과제의 듀(제출 기한)까지 불가피하게 연장된 적이 있었다.

이것 때문에 빡친 모 친구의 메신저 대화명은 "포크 삽질하는 놈 포크로 찍어 버린다 -_-"였던 것을 본인은 지금도 기억하고 있다.

Posted by 사무엘

2016/07/21 08:39 2016/07/21 08:39
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1252

등산 이야기만 몇 콤보로 계속되는 와중에 오랜만에 또 프로그래밍 얘기를 좀 하겠다.

본인은 예전에 열차나 건물(대표적으로 영화관)에서 좌석 배당 알고리즘이 어떻게 될까 궁금해하면서 이와 관련된 썰을 푼 적이 있다. 그리고 이와 비슷한 맥락에서, 점을 최대한 균등하게 순서대로 뿌리는 ordered 디더링의 가중치, 다시 말해 흑백 음영 단계 테이블은 어떻게 만들어지는 것일까 하는 의문을 제기했다. 그 당시엔 의문 제기만 하고 더 구체적인 해답을 얻지는 못했다.

그래픽 카드가 천연색을 표현할 수 있게 되면서 이제 컴퓨터에서 선택의 여지가 없는 '생존형'(?) 디더링의 필요성은 전무해졌다. 비디오보다는 아주 열악한 네트워크 환경에서 그래픽의 용량을 극도로 줄일 필요가 있을 때에나 특수한 용도로 제한적으로 쓰이는 듯하다. 색상뿐만 아니라 해상도도 왕창 올라가면서 이제는 글꼴의 힌팅조차 존재감이 많이 위태로워졌을 정도이니 세상이 참 많이도 변했다.

하지만 ordered 디더링이라는 건 점을 평면이나 공간에 최대한 골고루 질서정연하게 뿌리는 순서를 구하는 문제이다 보니, 계산 알고리즘의 관점에서는 실용적인 필요성과는 별개로 굉장히 흥미로운 문제인 것 같다.

사용자 삽입 이미지
(이제는 이런 무늬 패턴을 볼 일 자체가 거의 없어졌다..)

흑과 백이 정확하게 반반씩 있는 50% 경우를 생각해 보면, 당연한 말이지만 흑과 백은 대각선으로 엇갈린 형태로 존재한다. 수평선이나 대각선 형태가 아니다. ▤나 ▥가 아니라 ▩에 가까운 것이다.

그러므로 아주 간단한 2*2 크기의 음영이라면
(1 4)
(3 2)

가 된다. 수평선인 (1 2)(3 4)나 수직선인 (1 4)(2 3)이 아니라, (1 4)(3 2)라는 것이다.
그러니 태극기의 괘는 패턴이 (3 5)(4 6)이기 때문에 수직선에 가깝다. 그리고 이거 무슨 승용차에서 운전사가 있을 때와 없을 때, 좌석의 위치별로 상석에서 말석 순서 테이블과 비슷하다는 느낌도 든다.. -_-;;

시작점인 1은 언제나 좌측 상단으로 고정해서 생각해도 일반성을 잃지 않는다. 그럼 다음 2의 위치는 1에서 가장 멀리 떨어진 대각선이므로 역시 자동으로 결정된다.
그럼 (1 4)(3 2) 대신 (1 3)(4 2)는 불가능한 방향이 아니긴 하지만, 관례적으로 2 다음에 위쪽이 아니라 왼쪽에다가 3을 찍는 걸 선호하는 듯하다.

자, 그럼 얘를 조금 더 키워서 4*4 음영은 어떻게 될까?

(1 ? 4 ?) - (1 ? 4 ?) - (1 13 4 16)
(? * ? *) - (? 5 ? 8) - (9 5 12  8)
(3 * 2 ?) - (3 ? 2 ?) - (3 15 2 14)
(? * ? *) - (? 7 ? 6) - (11 7 10 6)

테이블의 크기가 딱 두 배로 커지면 새로운 숫자들은 언제나 기존 테이블의 틈바구니에 삽입된다. 그래야 균형이 유지될 수 있다.
각각의 틈바구니에 대해서 원래 칸의 대각선 아래 (+1, +1), 그리고 바로 아래 (0, +1), 바로 옆 (+1, 0)의 형태로 (5~8), (9~12), (13~16)이 매겨진다. 그랬더니 무슨 짝수 마방진 같은 복잡난감한 퍼즐이 채워졌다.

컴퓨터그래픽에서 실용적으로 가장 많이 쓰이는 음영은 8*8 크기이다. 모노크롬/16색 시절에 단색 패턴 채우기 함수들은 전부 8*8 패턴을 사용했다. 그러므로 얘는 음영을 64단계까지 표현할 수 있다.

8*8 패턴은 역시 4*4 패턴의 틈바구니에 삽입된다. 16 다음에 17이 들어가는 위치는 어디일까? 1과 2 사이에 5가 삽입되었던 것처럼 1과 5의 사이에 17이 삽입된다. 그리고 패턴 크기의 절반인 4픽셀 단위로 n, n+1, n+2, n+3이 (x,y), (x+4,y+4), (x,y+4), (x+4,y)의 순으로 번호가 매겨지는 건 변함없다.

거의 난수표 수준의 복잡한 테이블이 완성됐다. 규칙성이 뭔가 감이 오시는지? 그래픽 라이브러리들은 마치 삼각함수 테이블만큼이나 미리 계산된 디더링 테이블을 내장하고 있다.
그런데 이런 식으로 16*16 256단계 음영 테이블은 어떻게 만들 수 있을까?
각 구간을 순서대로 각개격파하는 게 아니기 때문에 분할 정복이나 재귀호출은 아닌 것 같다.

이런 숫자를 생성하는 코드를 작성하기 위해, 먼저 다음과 같은 변수들을 클래스나 전역변수 형태로 정의하자.

int mtrix[N][N]; int cs, ce;
static const POINT PTR[4] = {
    {0,0}, {1,1}, {0,1}, {1,0}
};

void Draw(int y, int x, int delta)
{
    for(int i=0;i<4;i++)
        mtrix[y+PTR[i].y*delta][x+PTR[i].x*delta]=ce++;
}

Draw는 특정 지점에서 n 간격으로 (0,0), (n,n), (0,n), (n,0)의 순으로 ce부터 ce+3까지 번호를 매겨 주는 역할을 한다.
이를 이용하면 2*2의 경우는 Draw(0, 0, 1)을 통해 간단히 만들 수 있다.

void Case2()
{
    cs=2; ce=1; memset(mtrix, 0, sizeof(mtrix));
    Draw(0, 0, 1);
}

앞서 살펴보았던 4*4는 이런 형태가 되고..

void Case4()
{
    cs=4; ce=1; memset(mtrix, 0, sizeof(mtrix));
    for(int a=0;a<4;a++)
        Draw( PTR[a].y, PTR[a].x, 2 );
}

더 복잡한 8*8은 Draw를 어떤 순서대로 호출해야 할지 따져보면 결국 규칙성이 도출된다.
그렇다. 2중 for문이 만들어지며, 16*16은 3중 for문이 될 뿐이다.

void Case8()
{
    cs=8; ce=1; memset(mtrix, 0, sizeof(mtrix));
    for(int a=0; a<4; a++)
        for(int b=0; b<4; b++)
            Draw(PTR[a].y + PTR[b].y*2, PTR[a].x + PTR[b].x*2, 4);
}

void Case16()
{
    cs=16; ce=1; memset(mtrix, 0, sizeof(mtrix));
    for(int a=0; a<4; a++)
        for(int b=0; b<4; b++)
            for(int c=0; c<4; c++)
                Draw(PTR[a].y + (PTR[b].y<<1) + (PTR[c].y<<2),
                    PTR[a].x + (PTR[b].x<<1) + (PTR[c].x<<2), 8);
}

사용자 삽입 이미지

바로 이것이 우리가 원하는 정답이었다. 식을 도출하고 보니 규칙은 허무할 정도로 너무 간단하다. n중 for문을 재귀호출이나 사용자 스택 형태로 정리하는 건 일도 아닐 테고.
이 정도면 평면이 아니라 3차원 공간을 점으로 촘촘하게 채우는 것도 생각할 수 있다. PTR 테이블은 (0,0,0), (1,1,1)부터 시작해서 정육면체의 꼭지점을 순회하는 순서가 되므로 크기가 8이 될 것이다.

그리고 참고로 8*8 음영 행렬은 아래의 코드를 실행해서 생성할 수도 있다.

int db[8][8];
for (int y = 0; y < 8; y++)
    for (int x = 0; x < 8; x++) {
        int q = x ^ y;
        int p = ((x & 4) >> 2) + ((x & 2) << 1) + ((x & 1) << 4);
        q = ((q & 4) >> 1) + ((q & 2) << 2) + ((q & 1) << 5);
        db[y][x] = p + q + 1;
    }

내가 처음에 for문을 써서 작성한 코드는 함수로 치면 일종의 매개변수 함수이다. (t에 대해서 x(t)는 얼마, y(t)는 얼마)
그런데 저건 그 매개변수 함수를 y=f(t) 형태로 깔끔하게 정리한 것과 같다. 식이 뭘 의미하는지 감이 오시는가?

이런 걸 보면 난 xor이라는 비트 연산에 대해 뭔가 경이로움, 무서움을 느낀다.
덧셈이야 "니가 아무리 비비 꼬아서 행해지더라도 까짓거 덧셈일 뿐이지. 결과는 다 예측 가능해" 같은 생각이 드는 반면, xor에다가 비트 shift 몇 번 하고 나면 도저히 예측 불가능한 난수 생성 알고리즘이 나오고 암호화/해시 알고리즘이 만들어지기 때문이다. 지극히 컴퓨터스러운 연산이기 때문에 속도도 왕창 빠르고 말이다.

2002년에 우리나라에서 열렸던 국제 정보 올림피아드에서도 'xor 압축'이라는 제출형 문제가 나온 적이 있다. 임의의 비트맵 이미지가 주어졌을 때, 이걸 사각형 영역의 xor 연산만으로 생성하는 순서를 구하되, 연산 수행을 최소화하라는 게 목표이다.

한 점에 대해서 가로/세로로 인접한 점 3개를 추가로 조사하여 흑백 개수가 홀수 개로 차이가 나는 점을 일종의 '모서리'로 간주하여 각 모서리들에 대해 plane sweeping하듯이 xor을 시키면 그럭저럭 괜찮은 정답이 나온다. 단, 이것이 이론적인 최적해와 동일하다는 것은 보장되지 않는다. 그렇기 때문에 문제가 제출형으로 출제된 것이다.

재미있는 것은 모서리 판정도 xor로 하면 간단하게 해결된다는 것이다.
(pt[x][y]==1)^(pt[x+1][y]==1)^(pt[x][y+1]==1)^(pt[x+1][y+1]==1) 같은 식. 이유는 조금만 생각해 보면 알 수 있다.

난 Bisqwit이라는 필명을 쓰는 이스라엘의 무슨 괴수 그래픽 프로그래머의 코딩 동영상에서 저 코드가 흘러가는 걸 발견하고 가져왔다. 흐음..;; Creating a raytracer for DOS, in 16 VGA colors 뭐 이런 걸 올려서 시청자들을 경악시키는 분이긴 한데, 물론 레알 16비트 도스용 Turbo C나 QuickBasic 컴파일러로 저런 걸 돌린다는 소리는 아니다. 그건 알파고 AI를 개인용 데스크톱 컴퓨터로 돌리는 것만큼이나 불가능한 일이니 너무 쫄지 않아도 된다. (VGA 16색인 건 맞지만 메모리와 속도는 그 옛날 기계 기준이 결코 아님.)

엑셀에다가 저 16*16 음영 테이블을 입력한 뒤, 수식을 이용해서 숫자 n을 입력하면 그에 해당하는 음영이 생성되게 워크시트를 만들어 보니 재미있다. 이번에도 흥미로운 덕질을 했다.

 

사용자 삽입 이미지

Posted by 사무엘

2016/06/26 08:33 2016/06/26 08:33
, , ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1242

« Previous : 1 : ... 7 : 8 : 9 : 10 : 11 : 12 : 13 : 14 : 15 : ... 23 : Next »

블로그 이미지

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

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

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

Site Stats

Total hits:
2989547
Today:
1107
Yesterday:
1477