« Previous : 1 : ... 2 : 3 : 4 : 5 : 6 : 7 : 8 : 9 : 10 : ... 31 : Next »

1. 그레이스케일

이건 이미지를 흑백 형태로 바꾸는 것이 핵심이다. 색을 구성하는 정보량의 차원이 줄어들고(3차원 → 1차원으로) 결과적으로 전체 색깔수가 줄어들기는 하지만(수천~수십만 종류의 색상 → 256단계의 회색), 그래도 아예 B&W 단색으로 바꾸는 건 아니다.
각 픽셀들은 색상과 채도가 제거되고 명도만 남아서 흑부터 백 사이에 다양한 명도의 회색으로 기계적으로 바뀐다. 같은 색의 픽셀이 인접 픽셀이 무엇이냐에 따라서 다르게 바뀐다거나 하지는 않는다.

그런데 그레이스케일 공식이 딱 하나만 존재하는 게 아니다. RGB 세 성분의 산술평균을 주면 될 것 같지만, 그렇게 그레이스케일을 하면 그림이 굉장히 칙칙하고 탁하게 보이게 된다.

똑같이 최대값 255를 주더라도 빨강(255,0,0), 초록(0,255,0), 파랑(0,0,255) 각 색별로 사람이 인지하는 명도는 서로 일치하지 않는다. VGA 16색 팔레트를 다뤄 본 사람이라면, 밝은 빨강이나 밝은 파랑을 바탕으로는 흰 글자가 어울리지만, 밝은 초록은 그 자체가 너무 밝아서 흰 글자가 어울리지 않는다는 것을 알 것이다.

사용자 삽입 이미지

그러니 공평하게 33, 33, 33씩 가중치를 주는 게 아니라 거의 30, 60, 10에 가깝게.. 초록에 가중치를 제일 많이 부여하고 파랑에는 가중치를 아주 짜게 주는 것이 자연스럽다고 한다.

이런 공식은 누가 언제 고안했으며 무슨 물리 상수처럼 측정 가능한 과학적인 근거가 있는지 궁금하다. 옛날에 흑백 사진을 찍으면 색깔이 딱 저 공식에 근거한 밝기의 grayscale로 바뀌었던가?
본인은 저 그레이스케일 공식을 태어나서 처음으로 접한 곳이 아마 QBasic 내지 QuickBasic의 컬러/팔레트 관련 명령어의 도움말이었지 싶다.

2. 디더링

이건 그레이스케일과 달리, 색깔수를 확 줄이는 것이 핵심이다. 그 대신 반드시 단색 흑백이어야 할 필요가 없다. 가령, 트루컬러를 16색으로 줄이는 것이라면 RGB 원색이 살아 있는 컬러라 할지라도 디더링의 범주에 든다. 이런 것처럼 말이다.

사용자 삽입 이미지

디더링에서는 어떤 원색을 직통으로 표현할 수 없을 때 근처의 비슷한 여러 색들을 고르게 흩뿌려서 원색과 비슷한 색감을 나타낸다. 팔레트까지 임의로 지정이 가능하다면 256색 정도만 돼도 생각보다 그럴싸하게 원래 이미지를 재현할 수 있다. 그런 게 아니라 그냥 흑백 단색 디더링이라면 아까 그레이스케일처럼 어떤 공식에 근거해서 명도를 추출할지를 먼저 결정해야 할 것이다.

0부터 1까지 가중치별로 점을 골고루 균등하게 뿌리는 공식은 이미 다 만들어져 있다. 이건 Ordered dither이라고 불리며, 보통 8*8 크기의 64단계 격자가 쓰이는 편이다. 그 구체적인 방식에 대해서는 수 년 전에 이 블로그에서 이미 다룬 적이 있다.

그레이스케일은 RGB(0.4,0.2,0.6)라는 색을 0.3이라는 명도로 바꾸는 것에 해당하는 기술이고, 단색 디더링은 이 색이 0.3, 0.3, 0.3 … 이렇게 쭉 이어지는 것을 1, 0, 0, 1, 0, 0 이런 식으로 이산적으로 표현하는 것과 같다.
그런데 원색을 기계적으로 이런 격자로 치환하기만 하면 보기가 생각보다 굉장히 좋지 않다.

원래 0.3을 표현해야 하는데 지금 지점에서 부득이하게 1을 찍어 버렸다면 0.7이라는 오차의 여파를 인접 픽셀에다가 떠넘겨서 거기서 계속해서 감당하게 해야 한다. 즉, 그레이스케일은 그냥 인접 픽셀을 신경 쓰지 않고 픽셀 대 픽셀 변환만 했지만 디더링은 그렇지 않다. ordered dither 내지 더 단순무식한 nearest color 찍기 신공이 아니라면.. 이전 픽셀에서 발생한 오차를 수습하는 error diffusion을 동원해야 부드러운 결과물이 나온다.

사용자 삽입 이미지

그런 지능적인(?) 디더링 알고리즘은 196~70년대에 컴퓨터그래픽이라는 분야가 등장한 초창기부터 연구되어 왔으며, 고안자의 이름을 따서 Floyd-Steinberg, Burks, Stucki 같은 알고리즘이 있다. 이미지를 다루는 사람이라면 포토샵 같은 그래픽 편집 프로그램에서 저런 명칭들을 본 적이 있을 것이다.
이런 알고리즘들은 ordered dither와 달리, 점들이 일정 간격으로 산술· 기계적으로 단순 투박하게 찍힌 게 아니라 뭔가 한땀 한땀 손으로 입력된 것 같고 부드러운 느낌이 든다. 그리고 무엇보다 색깔이 크게 바뀌는 경계 영역이 훨씬 더 선명하다.

3. 하프톤 (망점)

그레이스케일이 색깔 표현에 제약이 없는 아날로그 영상물(특히 흑백 필름 사진) 같은 느낌이 나고, 디더링은 초기에 해상도와 색상이 부족했던 디지털 영상과 관계가 있다면.. 하프톤은 색상이 부족한 대신 해상도가 높은 '인쇄물'과 관계가 깊은 기법이다.

사용자 삽입 이미지

하프톤은.. 일정 간격으로 망점을 두두두둑.. 찍고, 그 망점의 크기/굵기만으로 명도를 조절한다. 깨알같은 점들의 양과 배치 방식을 고민하는 통상적인 디더링과는 문제 접근 방식이 약간 다르다.
출력물의 해상도가 영 시원찮은 데서 하프톤을 동원하면 망점이 너무 커서 눈에 거슬릴 수 있다. 회색을 만들려고 했는데 하양과 검정이 그대로 눈에 띄게 된다는 것이다.

그러나 아무 인쇄물에서나 흑백이든 컬러든 문자 말고 음영(색깔 배경)이나 사진을 하나 보시기 바란다. 전부 촘촘한 점들로 구성돼 있다.
컴퓨터용 프린터나 전문 출판물 인쇄기들이 무슨 수십~수백 종에 달하는 물감을 갖고 있지는 않다. 잉크는 3원색에다가 검정 이렇게 4개만 갖고 있고, 나머지 색은 전부 얘들을 적절한 배율로 섞은 망점의 조합만으로 표현한다.

컴퓨터에서 보는 사진 이미지를 프린터로 출력하기 위해서는 가산 혼합 기반인 RGB 방식의 색을 감산 혼합 CMYK 방식으로 변환하고, 색 축별 망점 배합을 계산해야 할 것이다. 이건 디더링과는 다른 영역이다. 프린터 드라이버가 하는 일 중의 하나가 이것이며, 전문적인 사진이나 출판 프로그램 역시 색 축별로 인쇄 형태의 저수준 데이터를 export하는 기능을 제공한다.

그래도 요즘은 사진조차 필름 현상이 아니라 디지털 카메라로 찍은 뒤에 포샵질을 하고 고급 인화지에다 '인쇄'해서 만들어 내는 세상이니.. 컬러 인쇄 기술도 예전보다 굉장히 많이 좋아지고 저렴해졌다. 1990년대까지만 해도 가정용으로 컬러 레이저 프린터라는 걸 생각이나 할 수 있었겠는가?

유니코드 문자 중에 U+2591 ~ U+2593은 단계별 음영을 나타낸다.
굴림· 바탕 같은 통상적인 Windows 글꼴은 이를 하프톤 형태로 표현한 반면, 함초롬바탕은 디더링 형태로 표현했음을 알 수 있다.
윤곽선 글꼴의 래스터라이즈 방식의 특성상 디더링보다는 하프톤이 부담이 덜하기도 하다. 윤곽선 글꼴은 뭔가 덩어리· 군더더기가 늘어날수록 출력 성능이 급격히 떨어지기 때문이다. 그래서 매끄러운 곡선 말고 오돌토돌, 쥐 파먹은 효과 같은 걸 표현한 글꼴은 크기도 크고 처리하기 버거운 편이다.

사용자 삽입 이미지

이상이다. 이 사이트에도 그레이스케일, ordered 디더링, 기타 휴리스틱-_- 디더링, 그리고 하프톤까지 다양한 사례가 소개되어 있으니 관심 있는 분은 참고하시기 바란다.

우리가 사는 공간이 3차원일 뿐만 아니라, 시각 정보의 기본 단위인 색깔이라는 것도 어떤 방식으로 분류하든지 결국 3개의 독립된 축으로 귀착된다는 게 신기하지 않은가? 신학에서는 이게 하나님의 속성인 삼위일체와 관계가 있다고도 말하는데, 그건 뭐 결과론적인 해석이며 물증이라기보다는 심증 수준에서 만족해야 하지 싶다.

음악에서는 음계, 음정, 화성학 같은 이론이 수학과 연결되어 오래 전부터 연구돼 왔다.
그럼 일명 color theory라고 불리는 분야는 언제부터 누구에 의해 연구돼 왔을까? RGB, CMY, HSL 사이를 변환하는 공식 같은 것 말이다. 더디링은 컴퓨터그래픽 영역이겠지만 순수하게 색에 대한 수학적인 분석은 미술과 전산학 어디에도 딱 떨어지지 않아 보인다. 내가 알기로는 전파로 영상을 주고 받는 텔레비전 기술이 개발된 20세기 초쯤에야 이런 분야가 개척됐다.

본인이 색과 관련하여 감이 오지 않고 아직도 좀 알쏭달쏭한 걸 얘기하면서 글을 맺도록 하겠다.
모니터 화면 같은 건 그 본질이 빛이며, 빛은 색을 태생적으로 지니고 있다. 그러나 다른 세상 만물들, 특히 인쇄물은 색만 있지 빛은 지니고 있지 않다. 조명을 받아야만 자기 색을 비춰 보일 수 있다.

빛은 원색이 RGB라고 여겨지며, 섞일수록 더 밝아져서 최종적으로 white에 도달하는 ‘가산 혼합’을 한다. 그러나 빛이 없는 나머지 사물의 색들은 섞일수록 더 어두워져서 최종적으로 black에 도달하는 ‘감산 혼합’을 한다. 이 정도는 이미 초등학교 미술 시간에 배운다.

다만, 빛의 3축과 색의 3축은 노랑/초록 말고 빨강/파랑 부분도 서로 미묘하게 차이가 난다는 것부터는 고등교육 이상의 영역으로 보인다. 본인도 그걸 대학 이후에나 접했기 때문이다.
왜 그런 차이가 나는지에 대해서는 논외로 하더라도, 빛이건 색이건 두 색상을 물리적으로 섞지는 말고 디더링 하듯이 오밀조밀 가까이 배치시켜 놓으면 멀리서 볼 때 정확하게 어떤 혼색이 나타날까? 이걸 잘 모르겠다.

이건 빛이든 인쇄된 색이든 차이가 없어야 하지 않을까? 그리고 만약 차이가 없다면 그 결과는 감산 혼합이나 가산 혼합 중 어느 것과도 정확하게 일치하지 않을 것이다. 그 관계가 무엇일까? 마치 산술/기하/조화 평균처럼 서로 비슷하면서도 미묘하게 다른 결과가 나오지 않을까 싶다.

이해를 돕기 위해 아래 그림을 살펴보자. 반드시 확대/축소 왜곡되지 않은 원래 크기 형태로 볼 것!! (화면 확대 배율도 96DPI/100%로 맞춰야 함)
왼쪽은 그냥 RGB(0,0,0) 검정과 RGB(255,255,255) 하양을 1:1로 섞은 것이고, 오른쪽은 RGB(0,0,255) 파랑과 RGB(0,255,0) 초록을 섞어서 청록을 만든 것이다. 여기서 섞었다는 것은 픽셀 디더링을 말한다. 그리고 우측 하단에는 본인이 보기에 이들과 가장 비슷하다고 생각하는 순색(solid color)을 칠해 보았다.

사용자 삽입 이미지

255짜리 순색 파랑 255짜리 순색 초록을 섞어서 가산 혼합이 됐다면 RGB(0,255,255) 밝은 cyan 청록이 나타나야 하겠지만 실제로는 전혀 그렇게 되지 않았다. 그런데 그렇다고 해서 평균인 128짜리의 훨씬 어두운 색이 된 것도 아니다.
본인이 보기에 가장 비슷한 순색의 명도는 대략 180~184 정도이다. 생각보다 제법 밝다. 아.. 그래서 옛날에 VGA 팔레트도 어두운 색을 무식하게 128로 지정하지 않고 170 정도의 값을 줬는가 싶은 생각이 든다. 다른 색과 섞었을 때 재현 가능한 색이 나오라고 말이다.

이런 현상을 설명하기 위해 감마 보정 등 다른 어려운 이론들도 나온 게 아닌지 추측해 본다. 디더링 알고리즘이란 게 내가 생각했던 것보다 여러 단계로 나뉘고 더 어렵고 복잡할 수도 있겠다는 생각이 든다. 적절한 명도 추출하기, 적절히 대체색으로 분비하기, 오차 보정하기 등..;; 시각 디자인의 세계는 오묘하다.

Posted by 사무엘

2020/07/06 08:35 2020/07/06 08:35
, , , ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1770

1. 멀티스레드

지난 날개셋 한글 입력기 9.9에서는 어쩌다 보니 스레드와 관련된 작업이 좀 있었다. 각종 대화상자에서 스레드 작업이 행해지던 것을 중단시켰을 때 프로그램이 멎던 문제를 해결했으며, 반대로 사용자의 타이핑에 실시간으로 반응하며 동작하는 일부 입력 도구에다가는 랙 없이 깔끔하게 동작하기 위해 스레드를 사용하게 하는 옵션을 추가했다. 뭐, 요즘 컴퓨터에서 랙을 걱정할 지경은 아니긴 하지만 말이다.

오랜만에 멀티스레드를 다루다 보니 지금까지 이론으로만 알고 있던 동기화라는 것도 구현해야 했다. worker 스레드는 입력 데이터를 지지고 볶고 열심히 건드리고 있는데, 그걸 표시하는 UI 스레드도 아무 통제 없이 같이 돌아가게 놔 두면 프로그램은 당연히 메모리 에러가 나고 뻗는다.

Critical section을 생성하고 소멸하는 부분은 클래스의 생성자와 소멸자에다 담당시켰는데, 이걸 이용해서 실제로 락을 걸고 푸는 것까지 또 다른 클래스의 생성자와 소멸자로.. 그렇다고 기존 오브젝트의 포인터를 명시적으로 지정하지 않고 자동 구현하는 것은 C++에서 안 되는가 보다. Inner class에서 바깥 class의 non-static 멤버에 접근 가능하지 않으니까..

임계 구간을 잘 설정해야 crash를 막을 수 있다. 하지만 그렇다고 개나 소나 마구 락을 걸어 버리면 성능만 떨어지는 게 아니라 반대로 deadlock이라는 부작용이 발생해서 프로그램의 응답이 멎어 버릴 수 있다. Crash는 사람이 다쳐서 의식을 잃는 것이고, deadlock은 사람이 수술 후 마취에서 못 깨어나는 것과 비슷하다..;;

주로 어떤 상황이냐 하면, UI 스레드가 worker 스레드의 실행이 끝날 때까지 기다리게 됐는데 worker 스레드는 데이터를 고친 뒤 UI 스레드의 응답이 필요한 요청을 해 버릴 때 이렇게 된다. 특히 윈도우에다가 메시지를 보내고 응답을 기다리는 API 함수를 호출하는 것 말이다.

이런 주의 사항을 염두에 두면서 프로그램의 주요 구간에 스레드 동기화를 시켜 놓았다.
당연한 말이지만 한 critical section 오브젝트는 단일 구간이 아니라 코드의 여러 구간에다가도 배치해서 이들 모든 구간을 통틀어서 한 순간엔 한 스레드만이 접근 가능하게 제약을 걸 수 있다. 이게 강력한 면모인 것 같다.

그러고 보니 Windows 프로그래밍 헤더에 들어있는 구조체들 중에는 내부 구조가 딱히 공개돼 있지 않은 것이 좀 있다.
가령, PAINTSTRUCT 내부에 있는 rgbReserved 같은 멤버, STARTUPINFO 내부에 있는 cbReserved2, 그리고 CRITICAL_SECTION도 어쩌면 CPU 아키텍처별로 크기와 내부 멤버가 제각각 다를 가능성이 높은 비공개 구조체이다.
이런 예가 무엇이 더 있을 텐데 궁금하다. 당장은 떠오르지 않는다.

2. C 라이브러리와 스레드 생성 함수

Windows에서 스레드를 생성하는 제일 저수준 API는 잘 알다시피 CreateThread 함수이다.
요즘이야 C++도 C++11부터 언어 라이브러리 차원에서 플랫폼· 운영체제를 불문하고 멀티스레드 기능을 std::thread, std::mutex 같은 클래스로 제공하고 있지만, 이들도 내부적으로는 물론 Windows API 같은 운영체제 API를 호출하는 형태로 구현된다. 심지어 native handle값을 되돌리는 멤버 함수도 제공한다.

(그러고 보니 C++ 라이브러리는 C 라이브러리와 달리 라이브러리의 소스가 공개돼 있지 않은 듯하다.. 소스가 어쩔 수 없이 공개되는 템플릿 쪽은 도저히 알아볼 수 없는 난독화 급으로 작성돼 있고, 내부적으로 호출하는 함수 같은 것은 구현체 소스가 딱히 보이지 않는다.)

C++11 이전에는 Visual C++의 경우, 언어 확장 명목으로 CreateThread (와 ExitThread)를 얇게 감싼 _beginthread[ex] (+ _endthread[ex]) 함수를 독자적으로 제공해 왔다.
얘는 Windows SDK의 헤더와 라이브러리를 끌어들이지 않고도 스레드를 사용할 수 있게 해 주는 동시에.. C 라이브러리가 자체적으로 스레드별로 사용하는 메모리를 제때 할당하고 제때 해제하는 역할도 담당했다.
그렇기 때문에 C/C++ 함수도 사용하면서 Windows용 프로그램을 개발한다면 일반적으로는 운영체제 직통 함수 대신 이런 상위 함수를 사용해서 스레드를 생성해야 한다.

C 라이브러리가 스레드별로 무슨 메모리를 사용하는가 하면.. 전역변수로 단순하게 구현되었지만 지금 관점에서 보면 스레드별로 값이 제각기 보존되어야 하는 정보들 말이다. errno라든가, strtok 함수의 내부 state 및 context.. 비록 이런 것에 의존하는 함수가 그리 많은 건 아니지만 어쨌든 그런 예가 있다.

C 라이브러리에서 제공하는 begin/end 함수는 ex가 붙은 놈과 그렇지 않은 단순한 놈으로 나뉜다. ex는 받아들이는 인자가 Windows API의 그것과 완전히 동일하다. 그에 비해 단순한 놈은 평소에 잘 쓰이지 않는 인자들을 과감히 생략했기 때문에 사용하기가 더 쉽다.

예를 들어 begin 버전은 SECURITY_ATTRIBUTES 정보를 입력받는 부분이 생략됐고, 거의까지는 아니어도 잘 쓰이지 않는 thread ID를 받아들이는 부분도 생략됐다. 또한, 프로세스도 아니고 스레드의 실행 후 리턴값은 거의 쓰이지 않는다는 점을 감안하여 end 버전은 리턴값을 지정하는 부분도 생략되고 그냥 0으로 고정됐다.

참고로 Windows NT의 경우 ReadFile/WriteFile에서 실제로 읽거나 쓰는 데 성공한 양을 받는 DWORD 포인터, 그리고 CreateThread에서 생성된 스레드의 ID를 받는 DWORD 포인터를 생략하고 NULL을 줄 수 있다. 굳이 그 정보들이 필요하지 않다면 말이다. 하지만 과거의 Windows 9x는 저들 함수의 인자에서 NULL을 줄 수 없으며, 그냥 잉여 변수라도 하나 반드시 선언해서 넘겨줘야 한다는 차이가 있었다(NULL 주면 실행 실패).

뭐 그렇긴 한데.. C 라이브러리의 간편 버전은 사용법을 간소화하기 위해 여러 인자들을 생략했을 뿐만 아니라, 스레드 핸들을 CloseHandle로 닫는 것까지 임의로 자동으로 해 버린다. 스레드가 아주 잠깐 동안만 실행됐다가 종료돼 버리는 경우, _beginthread 함수의 리턴값이 나중의 시점에서 유효하다는 보장을 할 수 없다.

이건 사족에 가까운 간소화 조치로 여겨지며, 이 때문에 _beginthread는 사용이 그리 권장되지 않고 있다. 아니면 정말 뒷감당 할 필요 없고 한번 생성된 뒤에 “나는 책임 안 지니 니가 알아서 모든 뒷정리 하고 곱게 사라져라” 급인 스레드를 간편하게 생성하는 용도로나 적합하다. 그게 아니면 번거롭지만 결국은 _ex 함수를 쓰고 핸들은 내가 수동으로 닫아 줘야 한다.

3. 실행 주체들의 종료 리턴값

프로세스와 스레드는 종료될 때 정수를 하나 되돌릴 수 있는데, 아무 일 없이 잘 끝났으면 간단히 0을 되돌리는 게 관행이다. 옛날에 도스에서는 프로세스의 종료 코드를 ERROR_LEVEL이라는 독특한 명칭으로 불렀었다.

프로세스의 종료값은 그럭저럭 고유한 용도가 있으며, 여러 프로그램들을 순차적으로 실행하는 배치 파일이나 스크립트 같은 데서 많이 쓰인다. 종료되고 없는 먼젓번 프로그램의 실행이 성공했는지 여부를 밖에서 알 필요가 있기 때문이다.

하지만 앞에서 잠시 언급한 바와 같이.. 스레드의 종료값은 내 경험상 거의 쓰이지 않는다. 스레드의 종료값을 읽고 판단할 수 있는 코드 자체가 동일 프로세스 안의 다른 스레드밖에 없고.. 한 프로세스 안에서는 굳이 종료값 말고도 스레드의 실행 결과를 알 수 있는 방법이 매우 많기 때문이다. 애초에 같은 메모리 주소 안이니..

또한 스레드는 애초에 여러 작업을 동시에 수행하라고 만들어지지, 배치 파일처럼 여러 스레드를 순차적으로 실행하면서 실행 결과를 확인하는 식으로 운용되지도 않는다. 그럴 거면 그냥 한 스레드에서 순차 실행을 구현하고 말지.. 프로세스와는 여러 모로 사용 여건이 다르다.

그 와중에 Windows에서는 259라는 값이 STILL_ACTIVE라는 의미로 예약돼 있는지라.. 프로세스나 스레드의 종료 리턴값으로 쟤를 돌려주는 것이 금지되어 있다. 자기는 종료되어서 없는데 GetExitCodeProcess/Thread 함수가 저 값을 되돌리고 있으면 다른 프로세스나 스레드는.. 없는 프/스의 실행이 끝날 때까지 기다리면서 데드락 같은 상태에 빠지기 때문이다.

4. 타 프로세스에다 내 스레드 생성하기: CreateRemoteThread

Windows API 중에는 기본적인 기능을 제공하는 함수가 있고, 거기에 덧붙여 어느 프로세스 문맥에서 그 기능을 수행할 것인지를 지정하는 인자가 추가로 붙은 Ex버전이 또 존재하는 경우가 있다.
예를 들어 ExitProcess(현재 프로세스만)의 상위 호환인 TerminateProcess, 그리고 VirtualAlloc에다가 HANDLE hProcess가 추가된 VirtualAllocEx처럼 말이다.

그것처럼 스레드를 생성하는 CreateThread에 대해서도 target 프로세스를 지정할 수 있는 CreateRemoteThread 함수라는 게 있다.
이런 함수들은 내부적으로 구현도 하위 함수를 호출하면 상위 함수에다가 GetCurrentProcess()의 리턴값을 붙여서 호출하는 형태로 돼 있다.

그런데 남의 프로세스 주소 공간에다가 스레드를 생성한다니??
이건 물론 디버거나 보안 솔루션 같은 특수한 프로그램에서나 필요하지 자기 일만 하는 일반적인 프로그램에서 쓰일 일은 없는 기능이다.
그리고 이 함수는 각종 명령 인자들을 준비하는 게 몹시 까다롭기 때문에 타 프로세스에서는 매우 제한된 형태로밖에 활용을 할 수 없다.

제일 근본적으로는.. 실행되어야 할 스레드 함수 코드와 실행에 필요한 데이터들이 몽땅 그 target 프로세스의 메모리에 마련돼 있어야 하며 주소값 또한 걔네들의 문맥으로 줘야 하기 때문이다. 잘못되면? 요청을 한 우리가 아니라 멀쩡히 돌아가던 저 프로세스가 뻑나게 된다.
global hook 프로시저를 지정할 때처럼 DLL에다가만 분리해서 구현해 놓으면 그 DLL이 알아서 거기로 주입.. 그런 것도 없다.

다만, CreateRemoteThread를 이용해서 타 프로세스에다 내 코드를 주입하려면 어차피 DLL을 만들기는 해야 한다. 그 이유는 이렇다.
CreateRemoteThread로 할 수 있는 게 사실상.. 스레드 callback 함수로 LoadLibrary를 지정하는 것밖에 없기 때문이다.

이게 가능한 이유는 (1) LoadLibrary(+ FreeLibrary도)가 machine word 하나를 받아서 word 하나를 되돌린다는.. 스레드 callback 함수와 prototype이 일치하기 때문이며, 그리고 (2) LoadLibrary가 소속된 kernel32.dll은 어느 프로세스에서도 메모리에 load된 주소가 동일 고정이기 때문이다. 즉, LoadLibraryA/W 함수의 주소도 고정이며 예측 가능하다. 요게 핵심이다. 요즘 보안을 위한 대세인 ASLR에서 열외된 영역인 듯..

물론 LoadLibrary에다가 주는 인자는 평범한 숫자가 아니라 포인터이므로 넘겨줄 때 조심해야 한다.
VirtualAllocEx를 이용해서 그 프로세스 문맥에서의 메모리와 포인터 주소를 얻은 뒤, 문자열을 그쪽으로 써 넣을 때도 WriteProcessMemory를 호출해서 하면 된다. 얘는 그야말로 memcpy를 fwrite처럼 구현한 상위 호환 버전이라고 생각하면 되겠다.

LoadLibrary에다가 넘겨주는 문자열 인자로 바로 상대방의 프로세스에서 수행할 작업(뭔가 엿보기, 훅킹하기 등등)이 구현된 내 DLL의 주소를 넣으면 된다. DllMain 함수에서 돌아가는 것이니 몸 사리면서 조심해서 실행돼야 한다.
혹시 내 DLL을 실행하면서 다른 DLL을 읽어 놓아야 한다면 내 DLL을 주입한 것과 동일한 테크닉으로 그런 dependency DLL들을 미리 읽어 놓는 게 좋을 것이다. LoadLibrary를 수행하던 스레드가 실행이 끝났다고 해서 그 DLL들이 해제되는 건 아니기 때문이다.

주입되었던 내 DLL을 빼내는 것도.. 간단하다.
스레드 함수에다가 FreeLibrary를 주고, 인자로는 저 target 프로세스에서 되돌려진 내 DLL의 핸들값을 주면 된다.
target 프로세스에서 되돌려진 핸들값이야 내 DLL의 DllMain 함수의 인자로 들어왔을 테니 모를 수가 없을 테고, 그걸 호스트에다가 전하는 건 어려운 일이 아닐 것이다.

이런 식으로 저 함수에다가 LoadLibrary 대신 ExitProcess를 주면 그 프로세스가 알아서 자기 자신을 강제 종료하게 되니, 사실상 TerminateProcess와 같은 효과도 낼 수 있다.

16비트 시절에는 단일 address 공간에서 시스템 전체에 영향을 끼치는 프로그램을 만드는 게 용이했기 때문에 그에 상응하는 점프/복귀 주소 변조, x86 어셈블리어 삽입 같은 꼼수 테크닉이 유행했었다. 그러나 32비트 이후부터는 방법론이 이렇게 더 고차원적으로 확 바뀌었다.;;

Posted by 사무엘

2020/06/28 08:36 2020/06/28 08:36
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1767

Windows 운영체제에서 GUI 요소로서 화면에 표시되는 윈도우들은 독자적으로 고유한 위치와 크기를 갖는 popup 및 overlapped 윈도우, 아니면 다른 창의 내부에 부속으로 딸려 있는 child 윈도우라는 두 형태로 나뉜다. 전자는 독립 윈도우이고 후자는 종속 윈도우라고 생각해도 될 것 같다.

그리고 전자는 운영체제가 자동으로 표시해 주는 제목 표시줄, 시스템 메뉴, 최소화/최대화 버튼 같은 걸 가질 수 있고 메뉴도 가질 수 있다. 물론 갖지 않는 것도 자유이며 해당 창의 재량이다.
그럼 반대로 후자는 사정이 어떨까? 차일드 윈도우가 자기가 속한 부모 윈도우 내부에서 또 제목 표시줄과 시스템 메뉴, 최소화/최대화 버튼 따위를 가질 수 있을까?

이에 대해서 결론부터, 답부터 말하자면 다음과 같다.
"외형만 그렇게 나오도록 할 수는 있다. 하지만 운영체제는 child 윈도우를 대상으로는 여느 popup/overlapped 윈도우에서 수행하는 기본 처리를 자동으로 해 주지는 않는다. 그렇기 때문에 창을 그렇게 만들어 놓은 뒤에 우리가 원하는 자연스러운 동작을 구현할 수는 없다. 이건 운영체제에서 의도한 보편적인(?) 방식대로 창을 만들고 활용하는 게 아니기 때문이다."

본인은 마소에서 보편적이지 않은 동작이 존재하는 프로그램을 만든 것을 지난 20여 년에 달하는 세월 동안 딱 하나 발견했다.
바로 HTML 도움말 파일을 생성해 주는 HTML Help Workshop이라는 툴에 내장된 그래픽 에디터이다.

도움말을 띄우면 HTML 도움말 창이 별도의 독립된 창으로 뜨는 게 아니라 자신의 도킹 패널 내부에.. child 형태로 뜬다! 시스템 메뉴와 캡션, 최소/최대화 버튼까지 있는 당당한 독립 윈도우가 무슨 MDI child 윈도우처럼 다른 윈도우 내부에 이상하게 끼여 있는 게 심히 기괴하다. 이런 것도 Windows API를 사용해서 이론적으로 만들 수 있다는 것이다.

사용자 삽입 이미지

그래서 본인도 간단히 실험을 해 봤다.
다음은 껍데기 overlapped 윈도우와 "동일한" 클래스 이름으로 자기 안에 child 윈도우를 또 만들어 돌린 모습이다. 윈도우를 생성할 때 WS_CHILD에다가 WS_CAPTION| WS_SYSMENU| WS_MINIMIZEBOX| WS_MAXIMIZEBOX| WS_THICKFRAME만 주면 됐다.

사용자 삽입 이미지

얘는 언뜻 보기에 그냥 평범한 클래식 MDI 앱처럼 생겼지만 실제로는 그렇지 않다.
자신이 껍데기일 때는 클라이언트 영역을 회색 GetSysColor(COLOR_APPWORKSPACE)로 칠하고, 차일드일 때는 흰색 COLOR_WINDOW로 칠하면 된다. 껍데기와 차일드 둘 다 동일 윈도우 프로시저가 수행한 결과물이다. 자기 창이 껍데기인지 차일드인지의 여부는 WS_CHILD 스타일의 존재 여부만으로 간단히 판별할 수 있다.

그럼 제목 표시줄이 달린 차일드 윈도우들을 저렇게 만들어 놓으면 부모 윈도우라는 틀 안에 종속된 overlapped/popup 윈도우처럼 매끄럽게 동작하는가?
안타깝지만 답은 "전혀 그렇지 않다"이다. 저 상태에서 창들을 마우스로 단 몇 분 동안만 조작해 봐도 이상한 점이 잔뜩 발견될 것이다.

마우스로 child 윈도우를 클릭하면 지금 화면의 제일 겉에 드러난 창이 아니라 엉뚱한 창이 인식된다. 윈도우를 드래그 해 보면 화면에 잔상이 생긴다.
WM_LBUTTONDOWN 메시지를 받은 child 윈도우에다가 SetFocus를 해도 제목 표시줄이 활성화/비활성화 처리가 되지도 않는다.
child 윈도우를 최대화한 모습은 꽤 어정쩡하며, 그 상태로 프로그램 창의 크기를 키워도 child 윈도우는 크기가 갱신되지 않는다.

이런 문제가 발생하는 이유는.. Windows라는 운영체제는 근본적으로 child 윈도우에 대해서는 이들이 서로 겹쳐져 있고 포개져 있을 때의 Z-order 처리를 overlapped/popup 윈도우처럼 정교하게 해 주지 않기 때문이다.
child 윈도우라는 건 저렇게 제목 표시줄과 시스템 메뉴가 있는 윈도우가 아니라.. 그냥 리스트 컨트롤, 에디트 컨트롤, 버튼 같은 물건들이다. 에디트 컨트롤이나 버튼 같은 게 서로 겹쳐질 일이 있고 포커스를 받았을 때 Z-order가 바뀌어야 할 일이 도무지 있는가? 그렇지 않다는 것이다.

사용자 삽입 이미지

더구나 child 윈도우의 제목 표시줄은 Windows 8/10에서도 옛날 Windows Vista/7의 기본 테마 모양 그대로이며 업데이트도 안 돼 있다. 푸르스름하고 모서리가 둥근 그 시절 디자인 말이다. 그만큼 이쪽 외형은 마소에서도 관심이 없으며 더 지원을 안 하고 손을 놨다는 뜻이다.

저 상황에서 화면 잔상이 발생하는 걸 조금이라도 줄이려면 이 윈도우의 클래스에다가는 화면 refresh를 적극적으로 하라고 CS_HREDRAW|CS_VREDRAW 스타일을 줘야 하더라.
그리고 마우스 메시지를 받았을 때 SetWindowPos를 호출해서 이 창의 Z-order를.. HWND_BOTTOM으로 지정해야 하더라. 왜 top이 아니라 bottom인지는 모르겠다.

저것만 한다고 해서 모든 문제가 해결되는 건 아니다. 이런 시행착오를 겪어 보면.. MDI 프로그램에서 일개 child 윈도우인 문서창들이 나름 MDI client 영역 내부에서 껍데기 독립 윈도우인 것처럼 유연하게 처리되는 게, 절대로 그냥 저절로 되는 일이 아님을 짐작할 수 있다. 운영체제 API 차원에서 추가적인/예외적인 보정을 굉장히 많이 해 주는 덕분이지 싶다.

실제로 MDI를 구현하기 위해 사용되는 윈도우 프로시저는 DefFrameProc와 DefMDIChildProc로 감싸져 있으며, 키보드 전처리도 TranslateMDISysAccel로 따로 있다. 대화상자에 고유한 윈도우 프로시저와 키보드 전처리(IsDialogMessage)가 있는 것과 비슷한 맥락이다.

MDI 문서창들은 WS_EX_MDICHILD라는 extended 스타일이 지정돼 있다. 물론 우리야 CreateMDIWindow 함수나 WM_MDICREATE 메시지로 생성 요청만 하기 때문에 저 스타일을 직접 지정할 일은 없다.
내부적으로 뭔가 큰 의미를 갖는 스타일이지 싶은데.. 진짜 MDI 문서창이 아닌 임의의 child 윈도우를 생성할 때 저 스타일을 일부러 줘 보면 CreateWindowEx 함수의 실행이 실패한다. 그러니 쟤는 비록 헤더 파일에 선언은 돼 있지만 사용법과 의미가 제대로 문서화되지 않은 내부 API나 마찬가지이다.

그 밖에 WS_CLIPCHILDREN이라든가 WS_CLIPSIBLINGS, WS_EX_TRANSPARENT는 child 윈도우가 영역이 여럿 겹쳐 있을 때 창들을 refresh 하는 방식이나 성능을 좌우하는 옵션일 텐데 본인은 구체적인 의미나 차이를 잘 모르겠다. 평소에는 몰라도 전혀 상관 없지만 child 윈도우들을 MDI 문서창처럼 정교하게 다루기 위해서는 알아야 할 기능이지 싶다.
이런 창을 마우스로 클릭하면 WM_CHILDACTIVATE라는 메시지도 온다는데 본인은 태어나서 지금까지 한 번도 안 써 봤다. 저런 메시지가 있다는 사실도 처음 알게 됐다.

오늘 이야기의 결론은 MDI 형태의 프로그램을 밑바닥부터 직접 구현하는 건 대단히 어렵고 삽질스럽다는 것이다.. =_=;; child 윈도우를 popup/overlapped 윈도우처럼 다뤄 줘야 하기 때문이다.
그런 데다가 한 프로그램 창 안에서 여러 문서창을 다루는 것은 오늘날 같은 멀티 모니터 환경하고도 그리 어울리지 않는다.

그러니 마소 프로그램들의 경우, Word는 거의 20년 전부터 문서창을 프로그램 창처럼 따로 따로 생성하는 형태로 바뀌었으며 Visual Studio IDE도 문서창을 새 탭 아니면 새 창으로 자유자재로 만들 수 있게 바뀌었다.
그래도 그 정도보다는 규모가 작은 아담한 프로그램에게는 여전히 MDI만 한 대안이 없으니 클래식 레거시 MDI 기능도 오늘날까지 완전히 멸종하지는 않고 명맥을 유지하고 있다.

Posted by 사무엘

2020/06/20 08:35 2020/06/20 08:35
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1764

원소가 0 아니면 1 두 종류밖에 없는 N*N 크기의 어느 정사각행렬 M(N)이 있다. N은 2의 거듭제곱 형태로만 가능하며, M(N)의 생성 규칙은 이러하다.

일단, 크기가 1인 M(1)은 그냥 {0} 하나뿐이다.
그 뒤, M(2*n)은 M(n)의 각 원소들에서 0과 1을 뒤바꾼 놈을 M(n)의 왼쪽과 위쪽, 그리고 좌측 상단에 총 3개 카피를 씌우는 형태로 생성된다. 그러므로 M(2)는

(1 1)
(1 0)

이 되며, 다음 M(4)는 쟤를 뒤바꾼 놈을 또 덧붙여서

(0 0 0 0)
(0 1 0 1)
(0 0 1 1)
(0 1 1 0)

이 된다. 생성 규칙이 이런 식이다.
이런 식으로 M(16), 더 나아가 M(256)을 그림으로 나타내면 이런 모양이 된다.

사용자 삽입 이미지

흐음. 무슨 프랙탈 같기도 하고..
이런 행렬은 고안자의 이름을 따서 Walsh 행렬이라고 부른다.
그런데, 모노크롬이나 16색을 어렵지 않게 접할 수 있던 옛날에 이런 무늬를 화면에다 깔아 놓으면 꽤 근사해 보였을 것 같다~!

이걸 갖고 더 재미있는 장난을 칠 수도 있다.
옛날옛적에 학교에서 전자· 컴공을 전공했던 분이라면 혹시 그레이 코드(gray code)라고 기억하는 분 계신가?
통상적인 2진법과 달리, 숫자가 1씩 증가할 때 언제나 1개의 비트만이 바뀌게 숫자를 특이하게 표현하는 방식이다. 즉, 01111이다가 10000으로 바뀌는 식의 격변이 없다는 것이다. 물론 이런 숫자 인코딩을 갖고 진지한 산술 연산을 할 수는 없지만, 다른 용도로 쓸모는 있다고 한다.

사용자 삽입 이미지

0 1 3 2 6 7 5 4 ...
난 저걸 봐도 비트를 flip하는 규칙 자체를 잘 모르겠다. 숫자가 커질수록 긴가민가 헷갈린다. 솔까말 이거 규칙을 찾는 걸로 IQ 테스트 문제를 내도 될 것 같다.
그런데 일반 숫자를 그에 상응하는 그레이 코드로 바꾸는 공식은 어이없을 정도로 간단하다. x^(x>>1), 다시 말해 자신의 절반값과 자신을 xor 하면 된다.

자, 여기서 끝이 아니다.
저 수열에서 각 숫자들을 구성하는 2진법 비트의 배열 순서를 뒤집어 보자. 10진법으로 치면 1024를 4201로 바꾸는 것과 같다.

비트를 shift나 rotate하는 게 아니라 reverse 하는 건 내가 알기로 왕도가 없다. 그냥 for문 돌려서 1비트씩 차근차근 처리하는 수밖에 없다. 마치 주어진 숫자를 2진법으로 표현했을 때 1의 개수를 구하는 것처럼 말이다.
비트 수가 고정돼 있고 속도가 무진장 빨라야 한다면 미리 계산된 테이블을 참조해야 할 것이다.

N=8이라면 비트 자릿수는 3일 테고.. 0 1 3 2는 2진법으로 각각 000, 001, 011, 010일 텐데,
이걸 뒤집으면 차례대로 000, 100, 110, 010.. 즉 0 4 6 2 ... 형태로 바뀐다. 이 정도면 완전 난수표 수준으로 숫자를 뒤섞은 거나 마찬가지로 보인다.

저 수열이 확보됐으면 그걸 토대로 기존 Walsh 행렬을 개조해 보자. 즉, 지금 줄 배열이 0 1 2 3 4..의 순인데, 그걸 0 4 6 2 ... 즉, 둘째 행(1)에다가 다섯째 행(4)을 집어넣고, 다음 행에다가 일곱째 행(6)을 넣는 식으로 순서를 바꾼다. 그러면..

사용자 삽입 이미지

이런 인상적인 모양의 무늬가 만들어진다. 이것도 크기별로 규칙성이 있다. 좌측 상단은 사각형이 큼직하고, 우측 하단으로 갈수록 픽셀이 조밀해진다.
얘는 여러 명칭으로 불리는데, 통상적으로는 Walsh 행렬의 Hadamard 변환이라고 불린다.
실제 저 행렬/테이블을 구하는 건 아무 프로그래밍 언어로나 10분이면 코딩 가능할 것이다. 참고로 비트 reverse 같은 난감한 동작 없이 저 행렬을 얻는 다른 계산법도 존재한다.

이런 무늬 행렬은 전자공학 신호 처리 쪽에서 쓰인다고는 하는데.. 난 그쪽을 전공하지 않아서 잘 모르겠다. 단지 0과 1만 갖고도 인간 두뇌의 잉여력이 얼마나 폭발할 수 있는지에 경이로움을 느낄 따름이다.

수 년 전에 디더링 패턴의 생성 규칙에 대해 글을 쓰고 나서 이런 재귀적인 무늬를 주제를 다룬 건 무척 오랜만이다. xor은 온갖 난수와 암호도 만들어 내는 이산수학과 정보 이론의 진수임이 틀림없어 보인다. 세상에 이런 게 있다는 걸 알게 됐다는 차원에서 간단히 글을 남기게 됐다.

(xor은 x-y 2차원 평면에다가 진리표를 나타냈을 때 0과 1 결과를 직선 하나로 단순 분류할 수 없는 유일한 연산이기도 하다. 일명 XOR 문제..;; 마치 특정 그래프에 대해 한붓그리기가 불가능한 것과 비슷한 인상을 주는데.. 과거에 한때는 퍼셉트론 무용론과 함께 AI 겨울을 야기한 이력이 있다.)

Posted by 사무엘

2020/06/09 08:35 2020/06/09 08:35
, , ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1760

Windows에는 우리가 생각하는 것보다 훨씬 더 세밀한 UI 옵션들이 가득하다.
사용자의 취향과 컴퓨터의 성능을 고려하여 처음에는 옵션으로 도입됐다가 나중에는 그냥 '답정너 무조건 적용'이나 다름없게 바뀐 옵션의 대표적인 예는 "마우스로 창의 위치나 크기를 변경하는 것을 즉시 반영"이 있다.

마우스가 움직일 때마다 큼직한 창을 매번 다시 그리는 건 198, 90년대의 PC 사양으로 감당하기에는 계산량과 부하가 버거운 작업이었다. 그래서 처음에는 XOR 연산으로 그려진 반전 윤곽선만 마우스 궤적을 따라 그려 주다가 왼쪽 버튼이 떼어졌을 때 실제로 반영하는 형태로 move/resize 기능이 구현되었다. 이 동작을 기억하는 분도 계실 것이다.
그러다가 Windows 95에 와서야.. 그것도 Microsoft Plus!라는 확장팩을 설치한 뒤에 '즉시 반영'이 옵션 형태로 추가됐다.

그리고.. Windows XP에서는 타이핑을 시작했을 때 마우스 포인터를 화면에서 잠시 감춰 주는 자잘한 옵션이 추가되었다. 이건 딱히 성능하고 관계가 있는 옵션은 아니다만.. 텍스트 편집 기능을 자체 구현한 프로그램이라면 운영체제 차원에서 저 옵션이 켜졌는지 여부를 확인해서 저 동작을 지원해 줘야 할 것이다.

운영체제 제어판의 프로그래밍 버전 역할을 담당하는 함수는 SystemParametersInfo이다. SPI_GETMOUSEVANISH를 주면 포인터 숨기기 옵션이 켜졌는지 여부를 알 수 있다. 저 함수가 제공하는 거의 100가지가 넘는 옵션 상수들을 살펴보면 Windows의 UI의 변천 내력을 알 수 있다.
그리고 오늘 이 글에서 논하려 하는 것은 바로 그런 자잘한 UI 옵션 중의 하나인 UI state이다.

마소에서는 사용자에게 온갖 정보를 표시하는 것뿐만 아니라, 불필요한 정보를 가능한 한 숨기고 꼭 필요할 때만 표시하는 것에도 많은 관심을 갖고 연구한 듯하다.
그래서 사용자가 키보드 타이핑을 할 때는 마우스 포인터를 숨기고, 반대로 마우스를 주로 사용하는 것으로 보일 때는 키보드 조작과 관련된 시각 요소들을 숨기는 게 낫겠다는 생각을 하게 됐다.

키보드 조작과 관련된 시각 요소라니? 두 종류가 있다. 하나는 메뉴나 대화상자에서 Alt 단축키를 나타내는 글자 밑의 밑줄이고.. 다른 하나는 현재 키보드 포커스를 나타내는 점선 사각형이다.

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

이런 것들은 없는 게 시각적으로 깔끔하며, 사실 맥OS에는 저런 게 전혀 존재하지 않는다. 하지만.. 걔네들도 키보드 조작에 도움을 주는 정보이니 하루아침에 싹 없애 버릴 수는 없다.
그러니 절충안은.. 일단 숨겼다가 필요할 때만 표시하는 것으로 귀착된다.

하긴, 메모장이나 날개셋 편집기처럼 운영체제의 표준 메뉴를 사용하는 프로그램들을 살펴보면.. "파일(F) 편집(E)" 같은 항목에서 F, E에 쳐진 밑줄도 평소엔 보이지 않다가 사용자가 Alt를 눌렀을 때에만 표시되는 걸 볼 수 있다. 뭐, 요즘은 아예 메뉴가 통째로 보이지 않다가 사용자가 Alt를 눌렀을 때만 표시되는 프로그램도 있지만 말이다.

그리고 아래아한글은 대화상자의 단축키들이 Alt를 누르고 있는 동안만 잠깐 보이고, Alt에서 손을 떼는 순간 사라진다.
MS Office 프로그램은 문서 편집 화면에서 Alt를 단독으로 눌렀다가 떼면 리본의 기능들에 할당된 단축키들이 표시되는데, 이건 대화상자보다는 메뉴의 단축키 동작을 흉내 낸 것에 가깝다.

UI state는 저런 것들과 완전히 같지는 않지만 비슷한 상태와 동작을 다룬다.
Windows에서 돌아가는 모든 윈도우들은 UI state라는 숫자 형태의 속성을 갖는다. 이걸 얻어 오려면 자기 윈도우에 대해서 WM_QUERYUISTATE 메시지를 보내면 된다. 그럼 운영체제가 답변해 준다(wParam, lParam 없음. 리턴값만 확인하면 됨). 딱히 우리가 customize할 필요가 없는 메시지이니, SendMessage 대신 그냥 DefWindowProc에다가 바로 줘 버려도 된다.

리턴값은 비트 플래그 형태이다. 포커스 테두리를 그리지 말 것을 지정하는 UISF_HIDEFOCUS (1), 그리고 단축키 밑줄을 그리지 말 것을 지정하는 UISF_HIDEACCEL (2)를 살펴보면 된다.
Windows XP부터는 UISF_ACTIVE (4)라는 것도 추가됐다고 하지만 개인적으로는 의미나 용도를 전혀 모르겠다. “A control should be drawn in the style used for active controls.”라는 설명만 봐서는 이해가 잘...

static/label 컨트롤은 자체적인 포커스가 없으니 Alt 단축키 밑줄만 신경 쓰면 될 것이고, 아이템을 표시하는 리스트박스, 트리 컨트롤 같은 물건은 포커스 테두리만 신경 쓰면 될 것이다.
그에 비해 버튼(push, radio, checkbox 모두)은 단축키 밑줄과 포커스 테두리를 모두 관리해야 할 것이다. 반대로 에디트 컨트롤은 저런 걸 신경 쓸 필요가 전혀 없을 것이고..

포커스 테두리야 DrawFocusRect 함수를 이용하면 간편하게 그릴 수 있고, &가 섞인 문자열에 대해서 단축키 밑줄을 같이 그리거나 생략하거나(DT_HIDEPREFIX) 심지어 단축키 밑줄만(DT_PREFIXONLY) 그리는 건 DrawText의 여러 플래그들을 사용해서 간편히 구현 가능하다.

그런데 사용자가 대화상자에서 alt나 tab(포커스 테두리) 같은 글쇠를 누르면 그 대화상자 내부의 컨트롤 윈도우들은 감춰 놨던 보조 요소들을 표시하도록 UI state가 바뀌게 된다. tab은 포커스 테두리만 건드리지만 alt는 포커스 테두리와 단축글쇠 밑줄을 모두 건드린다. 한번 표시된 보조 요소들은 다시 숨겨지지 않는다.

이렇게 UI 상태가 바뀌었음을 나타내는 메시지는 바로 WM_UPDATEUISTATE이다. 얘는 우리가 생성하는 게 아니라 운영체제가 보내 주는 메시지이다. 어떤 플래그가 켜지거나(UIS_SET) 꺼졌는지(UIS_CLEAR)를 wParam 값을 통해 알 수 있으니 자세한 사항은 검색해서 구체적인 스펙을 찾아보면 된다.

이 메시지를 DefWindowProc에다가 넘겨 주면 우리 윈도우의 UI state값이 그렇게 변경되며, 동일 메시지가 우리의 child 윈도우들로 전파된다. 다시 말해 WM_QUERYUISTATE의 리턴값은 WM_UPDATEUISTATE를 DefWindowProc에다 요청하기 전과 후가 서로 달라진다는 것이다.
이 메시지를 받았을 때 우리 윈도우가 해야 할 일은, 우리 윈도우의 외형 중에서 그런 UI state의 영향을 받는 게 있는 경우 해당 부분을 다시 그리는 것이 전부이다. 해당사항 없으면 그냥 무시하면 된다.

alt와 tab이야 대화상자에서의 공통 단축글쇠이다. 이때 윈도우의 UI state를 변경하는 것은 IsDialogMessage 함수가 알아서 처리하는 것이라고 쉽게 유추할 수 있다.
그것 말고 우리 윈도우 내부에서도 자체적으로 UI state를 변경해 줘야 할 때가 있다. 사용자가 화살표 키 같은 걸 눌렀을 때 말이다. 이때는 WM_CHANGEUISTATE라는 메시지를 나 자신에게 보내면 된다. wParam에다가는 WM_UPDATEUISTATE와 동일한 스타일로 변경된 플래그를 주도록 한다.

DefWindowProc에서는 이 메시지를 부모 윈도우로 보낸 뒤, 최상위 윈도우에서는 메시지를 WM_UPDATEUISTATE로 바꿔서 자식들로 다시 전파한다. 이런 절차를 거쳐서 대화상자에 있는 모든 윈도우들의 UI state가 동기화된다.

지금까지 얘기했던 것들을 정리하면 이렇게 요약된다.
UI state를 인식해서 동작하는 컨트롤을 직접 구현하고 싶은 경우, WM_QUERYUISTATE를 호출하면 자신의 UI state 값을 얻을 수 있다. 그리고 WM_UPDATEUISTATE가 왔을 때 적절히 화면을 다시 표시하면 된다. 그리고 자기 자신이 UI state를 변경하고 싶은 경우, WM_CHANGEUISTATE를 보내면 된다.

모든 윈도우 메시지들은 DefWindowProc으로 가도록 하면 된다.
대화상자의 경우, 마우스 클릭으로 명령을 내려서 연 경우 저런 보조 요소들이 숨겨진 상태로 시작하며, 그렇지 않고 키보드로 열었다면 이들이 표시되어 있다.

이런 UI state 변경 메시지들과 관련 기능들은 Windows 2000에서 최초로 도입됐다. 9x/ME/NT4 시절에는 존재하지 않았다는 뜻이다. layered window, 마우스 포인터 아래의 그림자, fade out되며 사라지는 메뉴도 다들 2000에서 도입된 기능이다. 2000이 XP 같은 테마는 없었지만 그래도 소소하게 바뀐게 많다는 걸 알 수 있다.

이런 동작은 SystemParametersInfo(SPI_GETKEYBOARDCUES)에 값이 설정돼 있을 때만 행해진다. 제어판에서는 "Alt 키를 누를 때까지 키보드 탐색에 사용할 문자 숨기기"라는 이름의 옵션으로 존재한다. 이게 꺼져 있으면 UI state는 언제나 0으로 고정되며, WM_UPDATEUISTATE 같은 메시지가 오지 않는다.
처음에는 얘의 명칭이 SPI_GETMENUUNDERLINES이었다. 하지만 보다시피 단축키 밑줄뿐만 아니라 포커스 테두리 같은 요소도 추가되면서 명칭이 더 범용적인 'keyboard cue'라고 수정되었다.

이 기능 내지 API와 관련된 본인의 생각을 두 가지 정도 첨언하고 글을 맺도록 하겠다.

1.
라틴 알파벳처럼 음소 풀어쓰기 형태이고 키보드 글쇠와 글자가 일대일 대응하는 문자라면.. 간단하게 해당 문자를 곧장 단축글쇠로 지정해서 아래에 밑줄을 쳐 주면 된다. 하지만 한글· 한자 같은 문자는 그렇지 못하기 때문에 문자열 뒤에 단축글쇠를 별도로 추가해 줘야 한다. 파일(F) 편집(E)처럼 말이다.

이런 환경에서는 단축글쇠를 숨기려면 밑줄만 숨기는 게 아니라 (F), (E)를 통째로 감춰 버리는 게 훨씬 나을 것이다. 하지만 이건 문자열의 내용과 길이를 통째로 변경해야 하니 좀 난감할 수도 있다. 단축글쇠 영역이 기존 UI 문자열의 폭을 건드리지 않도록 별도로 돌출된 툴팁 같은 데다 표시해야 할 것이다.

2.
지금까지 글을 읽은 분이라면 눈치 채시겠지만,
UI state라는 건 앞서 언급했던 라벨, 버튼, 리스트 같은 공용 컨트롤이나 그에 준하는 물건을 새로 구현하지 않는 한.. 프로그래머가 접하거나 신경 쓸 일이 거의 없는 개념이다.
한 대화상자 아래에서 컨트롤들이 UI state가 제각각 달라야 할 일도 없고, 사용자가 WM_***UISTATE 메시지를 DefWindowProc에다가 전달하지 않고 동작을 override해야 할 일도 없다.

사용 패턴이 뻔히 정해져 있고 자주 쓰일 일도 없음에도 불구하고 관련 메시지가 안 그래도 공간이 부족한 시스템 메시지 영역에 3개나 들어가 있는 건 개인적으로 생각하기에 좀 낭비인 것 같다.
UI state는 그냥 GetWindowLongPtr 함수로(GWL_UISTATE) 얻게 해도 되고, WM_CHANGEUISTATE는 메시지 대신 함수가 담당하게 했어도 될 것 같다. 아니면 그 메시지조차도 SetWindowLongPtr로 대체하고 말이다.

내가 보기에 메시지 형태가 꼭 필요한 건 WM_UPDATEUISTATE뿐인 것 같다.

Posted by 사무엘

2020/05/17 08:32 2020/05/17 08:32
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1752

Windows에는 문자열을 입력받는 일반 에디트 컨트롤뿐만 아니라 글자마다 서로 다른 서식(글꼴, 크기, 진하게/이탤릭, 탭, 문단 정렬, 하이퍼링크..)을 주고 그림이나 표를 집어넣을 수도 있는 리치(rich) 에디트 컨트롤이라는 게 있다.

그야말로 소형 워드 프로세서가 통째로 윈도우 컴포넌트로 만들어진 셈이니 이건 굉장한 혁신이었다. 심지어 특정 용지 크기에 맞게 위지윅(장치 독립 레이아웃)을 지원하기 위해 기준으로 참조할 DC를 지정하는 기능도 있고.. 아예 윈도우 없이 에디팅 엔진의 동작만 뽑아서 쓰라고 windowless rich edit control이라는 라이브러리도 제공된다. 이 정도면 작정하고 굉장히 세심하게 만들어진 셈이다.
Windows의 기본 예제 프로그램 중 하나인 워드패드가 얘를 써서 만들어진 것으로 유명하며, 초창기에는 Visual C++에 워드패드의 소스가 통째로 제공되기도 했다.

얘의 내부 자료구조는 RTF라는 파일 포맷으로 제정되어서 마소뿐만 아니라 애플 같은 타 회사에서도 쓰기 시작했다. 단적인 예로 macOS의 TextEdit도 이 포맷을 지원한다.
다만, RTF는 HTML이라는 완벽한 상위 호환이 등장하면서 존재감이 굉장히 묻혀 버린 감이 있다. 당장 도움말만 해도 16비트 Windows 시절에는 RTF 기반의 hlp였지만 곧장 HTML 기반으로 대체됐으니 말이다.

상업용 워드 프로세서보다는 기능이 빈약해도 리치 에디트도 엄연히 워드 프로세서에 준하는 물건이니.. 얘는 단독으로 덩치가 굉장히 컸다. 공용 컨트롤 comctl32 패밀리의 멤버 형태로 제공되지 않았으며, 자신만의 전용 DLL과 버전업 체계를 갖추고 있다. 게다가 역사적으로 형태도 몇 차례 바뀌었다.

초창기 1.0은 riched32.dll이었고 윈도우의 공식 클래스 이름은 RICHEDIT였다. Windows 95와 함께 제공되었다.
그러다가 리치 에디트 2.0은 riched20.dll로 바뀌고 클래스 이름도 RichEdit20A 또는 W로 바뀌었다. 짐작하다시피 이때 유니코드가 최초로 지원되기 시작했고 다단계 undo도 지원되기 시작했다. 저 둘은 텍스트 에디터를 밑바닥부터 다시 만들어야 할 명분이 충분한 대공사가 맞다. 얘는 Windows 98과 함께 제공되었다.

나중에 Windows 2000/ME 타이밍 때는 3.0이 나왔는데, 3.0은 프로그래머의 입장에서 API가 바뀐 것이 전혀 없이 2.0을 상위 호환 명목으로 아주 부드럽고 자연스럽게 대체하게 됐다. 그리고 기존 1.0의 생성 요청조차도 그냥 3.0 엔진을 기반으로 호환성 모드로 동작하게 바뀌었다.
지금도 Windows의 system32 디렉터리를 가 보면 riched32.dll은 있긴 하지만 크기가 달랑 10KB밖에 되지 않는다. 실질적인 기능은 riched20.dll에서 수행되기 때문이다.

그랬는데 수 년 뒤, Windows XP sp1에서 리치 에디트 컨트롤은 형태가 또 바뀌었다. 목적은 TSF를 지원하기 위해서다. 얘 역시 내부의 모든 동작을 저 스펙에 맞게 수정해야 하는 엄청난 대공사였다.
얘는 모듈 이름이 영 생소한 msftedit.dll로 바뀌고, 버전도 공식적으로는 4.1이지만 클래스 이름은 RICHEDIT50W이라고 정해졌다. 어디서는 4.1이었다가 저기서는 5라고 표기하면서 혼란스럽다.

리치 에디트 컨트롤은 이렇게 두 번 격변을 거친 뒤에는 딱히 단절적인 변화 없이 지금까지 전해져 오고 있다. MFC에서는 리치 에디트 컨트롤을 초기화하는 AfxInitRichEdit() 계열의 전용 함수를 두고 있다. 2와 5가 붙은 버전도 있다.
그래도 일반적인 대화상자에서 리치 에디트 컨트롤을 집어넣어야 할 일은 그리 많지는 않을 것이며, 굳이 넣더라도 서식이 동원된 문서나 데이터를 “읽기 전용”으로 표시하기 위해서일 것이다.

Visual C++ IDE의 리소스 에디터가 지원하는 것은 버전 2 (사실상 3)에 머물러 있다. 굳이 버전 5를 집어넣으려면 custom control을 삽입해서 RICHEDIT50W를 수동으로 지정해야 한다.
그래도 Visual C++ 201x대의 최신 MFC는 CRichEditView 클래스에 대해 버전 5를 집어넣게 돼 있다. 하긴 4.1인지 5인지 최신 버전이 나온 지가 이미 10년이 넘었는데, 진작에 지원했어야지..

5.0의 가장 큰 존재 의의라 할 수 있는 TSF를 사용하기 위해서는 SES_USECTF 스타일을 지정하는 코드 단 한 줄만을 실행해 주면 된다. SendMessage(hRichEdit, EM_SETEDITSTYLE, SES_USECTF, SES_USECTF)
글쎄, TSF를 제대로 지원하려면 원래는 응용 프로그램에서 COM을 초기화하고 message loop에도 TSF 오브젝트에다가 선처리를 먼저 맡기는 등 해야 할 일이 많다. 이 때문에 날개셋 편집기는 TSF 사용 여부 옵션을 변경한 것이 프로그램을 재시작해야만 적용된다. 그걸 다 무시하고 일반 앱에서 이렇게 간단하게 TSF 지원이 정말 가능한지는 잘 모르겠다.

이걸 해 주면 리치 에디트 컨트롤에서 IME에서 단어 단위 한자 변환이 되며, 날개셋의 경우 다른 고급 특수 기능들도 모두 아무 제약 없이 사용할 수 있다.

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

그 밖에 리치 에디트 컨트롤이 사용 측면에서 기존 에디트 컨트롤과 다른 점은 다음과 같다.

  • 기존 에디트 컨트롤은 단일 배열 버퍼 기반이지만 리치 에디트는 문자열의 연결 리스트 기반으로, 처음부터 대규모 텍스트 편집에 최적화돼 있다. Windows 9x 시절에는 기존 컨트롤은 편집 가능한 텍스트의 크기도 64K 남짓으로 제한돼 있었지만 리치는 그런 한계가 없다.
  • 리치 에디트 컨트롤은 기존 에디트 컨트롤과 달리, 자체적인 우클릭 메뉴가 없다. 우클릭 이벤트 때 할 일은 전적으로 부모 윈도우의 동작에 달려 있다.
  • 기존 에디트 컨트롤은 텍스트의 드래그 드롭을 지원하지 않지만 리치는 지원한다.
  • 기존 컨트롤은 블록이 언제나 짙은 파랑 highlight색으로만 표시된다. 그러나 리치 에디트는 그냥 반전색 또는 요즘 유행인 옅은 파랑으로 표시되며, 사용자 정의를 할 수 있다.
  • 리치는 트리플 클릭(3연타...)으로 텍스트 전체를 선택할 수 있다. 기존 컨트롤은 그런 동작이 지원되지 않는다.

서로 지향하는 목표와 설계 방식이 생각보다 많이 차이가 난다는 걸 알 수 있다. 에디트 컨트롤을 두 종류 따로 만들 만도 하다.
리치 에디트 컨트롤의 다른 사용법들이야 기존 문서를 참고하면 되니 여기서 다룰 필요가 없다. 이 글에서는 역사, TSF 지원, 그리고 한 가지 더.. 중요하지만 다른 문서에서 다루지 않는 특성을 하나 더 다룬 뒤 글을 맺도록 하겠다. 바로.. 경계 테두리이다.

리치 에디트 컨트롤은 공용 컨트롤 계열의 물건이 아니다. 그래서 그런지 공용 컨트롤 6 테마가 적용되었더라도 경계 테두리가 일반 에디트 컨트롤 같은 새끈한 모양으로 안 나오고 그냥 고전 테마의 투박한 모양으로 그려진다. 위의 스크린샷에서 보는 바와 같다. 어찌 된 영문일까? 답을 말하자면 상황이 좀 복잡하다.

윈도우 스타일 중에는 WS_BORDER (검고 가는 테두리), WS_DLGFRAME (버튼 같은 볼록 두툼한 테두리), WS_EX_CLIENTEDGE (오목 두툼한 테두리), WS_EX_STATICEDGE (오목 가는 테두리) 처럼 운영체제 차원에서 윈도우 주변에 non-client 영역을 확보하고 테두리를 치는 스타일들이 몇 가지 있다.

여기서 볼록이라 함은 좌측과 상단은 밝은 계열, 우측과 하단은 어두운 색인 테두리를 말하며, 오목은 순서가 그 반대이다. WS_DLGFRAME(볼록 테두리)을 지정하면 대부분의 다른 테두리 스타일들이 무시되지만, 그래도 WS_EX_CLIENTEDGE와 동시 지정은 가능하다. 그러면 꽤 흥미로운 테두리가 만들어진다. 이 역시 위의 스크린샷에서 묘사된 바와 같다.

이 테두리가 그려지는 모양은 테마의 적용 여부와 무관하게 언제나 동일하다. 그렇기 때문에 특별히 하는 일이 없다면 원래는 리치 에디트 컨트롤처럼 투박하게 그려지는 게 맞다.

테마가 적용된 공용 컨트롤 6들은 WS_EX_CLIENTEDGE(오목하고 두툼한 테두리)가 존재할 경우, WM_NCPAINT 메시지를 자체 처리하여 DrawThemeBackgroundEx 같은 theme API를 호출해서 테두리를 그린다. 자세히 보면 심지어 포커스를 받았을 때와 그렇지 않을 때 테두리 색깔이 달라지는 것도 확인할 수 있다. 하지만 리치 에디트 컨트롤은 저런 처리를 안 하기 때문에 테두리가 고전 테마 모양 그대로인 것이다.

그러니 컨트롤 자신이 테두리를 제대로 그리지 않으면 응용 프로그램이 강제로 그려 주는 수밖에.. 리치 에디트 컨트롤의 테두리 미관을 개선하려면 해당 컨트롤을 서브클래싱 해서 WM_NCPAINT 처리를 직접 하는 것 말고는 선택의 여지가 없다. 이것도 뭔가 운영체제 차원에서의 자동화 절차가 필요해 보인다.

본인이 이런 테두리 그리기와 관련해서 알게 된 굉장히 놀라운 사실이 있다.
오늘날 Windows에서 대화상자를 꺼내는 DialogBox, CreateDialog 계열 함수들은 대화상자 리소스에서 WS_BORDER이라고 지정된 스타일을 무시한다. 그걸 무조건 WS_EX_CLIENTEDGE로 치환해 버린다.

오목하고 두툼한 테두리는 대화상자 내부에서 사용자가 뭔가 아이템을 선택하고 문자를 입력하는 '작업 공간'을 나타내는 시각 피드백이다. 그에 비해 볼록/오목 효과가 없이 그냥 flat한 검정 단색 테두리(WS_BORDER)는 대화상자에 회색 입체 효과가 없던 Windows 3.x 시절 비주얼의 잔재로 여겨진 것이다.

어쩐지 옛날에도 WS_BORDER이랑 WS_EX_CLIENTEDGE가 차이가 없는 것 같았는데 그땐 그저 그러려니 하고 넘겼었다. 관계가 정확하게 저렇다는 걸 본인도 이제야 직접 조사해 보고 알게 됐다. 대부분의 경우 WS_BORDER는 그냥 WS_EX_CLIENTEDGE로 포워딩 되는 호환성 옵션으로 전락했다.

다만, 테마가 적용된 뒤에는 윈도우의 외형이 다시 옛날 같은 flat 스타일로 돌아간지라.. 검정 단색 테두리가 회색 단색 테두리로 바뀌었을 뿐이다. 그래서 볼록/오목 효과가 역으로 오래되고 촌스럽게 보이는 촌극이 빚어져 있다. 역사는 이런 식으로 돌고 돈다! =_=;;;

이상이다. 그러고 보니 리치 에디트는 최신 버전인 5(또는 4.1)에 대해 공용 컨트롤 6처럼 side-by-side assembly를 적용하는 게 충분히 일리가 있음에도 불구하고 전혀 그런 조치가 취해지지 않았다. 그렇기 때문에 얘는 사용자가 DLL과 윈도우 클래스 이름을 달리하는 원시적인 방법으로 버전 구분을 해야 한다.

즉, 리치 에디트는 Windows XP와 동시대에 개발됐음에도 불구하고 같이 개발되던 관련 신기술 두 종류와는 완전히 열외된 셈이다. (테두리 테마, SxS assembly) 아쉬운 대목이 아닐 수 없다. 리치 에디트 팀의 관심사에 든 XP의 신기술은 오로지 TSF뿐이었다.

본인이 개발한 날개셋 한글 입력기에도 자체 에디트 컨트롤과 텍스트 에디터가 있다. 먼 옛날에 2.x에서 3.0으로 넘어갈 때 프로그램이 내부 구조를 다 뜯어고치고 완전히 새로 개발되었는데, 이때 유니코드 기반, 다단계 undo, 그리고 TSF까지.. 리치 에디트 컨트롤이 1에서 2, 3에서 5로 갈 때의 공사를 몽땅 진행했다. 리치 에디트와 비슷하다면 비슷한 전철을 밟은 셈이다.

Posted by 사무엘

2020/04/27 08:35 2020/04/27 08:35
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1745

1. 호출과 사용

Windows API 중에는 IsBadWritePtr이라고 해서 주어진 포인터가 가리키는 메모리가 올바른지를 되돌리는 함수가 있다. 하지만 이 함수는 모든 경우를 맞게 판단하지 않을 뿐만 아니라 다른 코드에서 처리해야 하는 exception을 가로채서 다른 곳에서 문제를 일으킬 가능성을 높인다.

그리고 올바르지 않은 포인터가 발생했을 정도라면 이런 부류의 함수를 갖고 성공 여부를 어설프게 간보는 게 애초에 별 의미도 없다. 그 즉시 깔끔하게 뻗고 프로그램을 종료시키는 게 차라리 더 안전하며, 문제의 원인을 탐색하는 데도 더 도움이 된다.

이런 이유로 인해 과거에 the old new thing 블로그에서는 IsBadWritePtr should be called XXXX..라는 제목의 글이 올라왔는데, 본인은 제목의 진짜 의미를 파악하지 못해서 한동안 멈칫했다. 함수 이름 다음에 be called가 나오니 이 함수의 호출과 실행, 다시 말해 프로그래머가 이 함수를 사용하는 방법과 관련이 있는 제목인 줄 알았기 때문이다.

하지만 제목의 실제 의미는 그게 아니다. "IsBadWritePtr은 실제로는 XXX라는 이름으로 명명되었어야 한다. 하는 일이 '요게 잘못된 포인터인지 판단'이 아니라 그냥 '프로그램을 랜덤하게 뻗게 하라'이기 때문이다." 요게 제목의 의도이다.
하긴, Windows API 중에는 이름이 좀 므흣하게 지어진 게 내 기억으로 몇 가지 있다. 가령, IsDialogMessage는 동사가 Is가 아니라 Translate가 되는 게 훨씬 더 적절하다는 식으로 말이다.

call이 '명칭 부여'라는 뜻도 있고 '실행, 사용'이라는 뜻도 있다. 옛날에 GWBASIC의 Illegal function call이라는 에러 메시지가 한국어로는 "기능호출 사용이 잘못되었읍니다"라고 호출과 사용을 모두 넣어서 번역됐던 게 떠오른다.

2. 목적어가 자체 포함된 타동사

정확한 출처와 문맥은 기억이 안 난다만.. 본인은 어느 프로그래밍 라이브러리 문서에서 "The XXXX function does not return." 형태의 문장을 본 적이 있다. 언뜻 보기에는 이 함수가 실행이 끝나지 않고 무한루프에 빠진다는 말 같지만, 거기서의 의미는 그렇지 않았다. 저 함수는 그냥 리턴 타입이 void, 즉 리턴값이 없다는 뜻이었다.
이건 영어에서 마치 이런 식의 의미 차이를 보는 것 같다.

  • I can't read. 난 문맹이다. (시력이나 조명에 문제가 있어서 못 읽는 게 아니라)
  • XXXX is a word. 스크래블 게임 같은 데서, XXXX는 아무 글자 나열이 아니라 스펠링이 맞는 정식 단어이다.

read가 단독으로 '글을' 읽는다라는 뉘앙스를 포함하고 있고, word라고만 해도 자동으로 '올바른' 단어라는 뜻이 포함돼 있다.
사실, 이건 생소한 개념이 아니다. dream, design, sleep 같은 다른 동사도 마찬가지이며 얘들은 아예 명사도 된다. 심지어 dream a dream, design a design, sleep a sound sleep 같은 말도 의도적으로 쓰인다.

옛날 영어에서는 심지어 kill/slay, send 같은 타동사도 목적어를 생략한 채로 쓰였다는 것을 예전 글에서 언급한 바 있다. 그러니 굳이 murder이라고 안 쓰고 thou shalt not kill이라고만 해도 "살인하지 말지니라"가 된다.
return도 그런 식의 유도리 용례가 있는 것으로 보인다.

3. 한국어와 영어의 재귀 구조

한국어는 주어, 보어, 목적어, 부사어 등의 격이 체언 뒤에 달라붙는 온갖 조사들에 의해 구분된다. 어순에 의존하지 않고 격조사가 따로 존재하기 때문에 어순의 도치가 '비교적' 자유로운 편이다.
하지만 종결어미가 들어있는 서술어는 예외이다. 절대적으로 무조건 마지막에 와야 한다. 그리고 그 어떤 문장도 종결어미가 등장해서 말을 완전히 끝맺기 전에는 끝난 게 아니다.

이런 특성으로 인해 한국어를 외국어로서 공부하는 사람들 사이에서 한국어는 끝까지 들어 보지 않으면 뭔 말인지 도무지 종잡을 수 없는 스펙터클한 반전 언어라는 평이 지배적이다.
"내가 무릎을 꿇은 건 추진력을 얻기 위함이었다...는 소식이 나돈다..고 하지만 그건 사실이 아니다" ... 앞서 언급되었던 내용들이 막판에서 순식간에 전면 부정되거나 매트릭스 안에 들어가 버리기 때문이다. 이런 막장 어순을 영어로 실시간 동시 통역해야 한다면 통역자의 멘탈에 얼마 못 가 과부하가 걸릴 것이다.

이런 한국어와 달리, 영어는 SVO형 언어답게 보어건 목적어건 객체를 문장의 뒤에다 꽝 찍은 뒤, 그 객체를 수식하는 문구들이 관계대명사와 함께 꼬리에 꼬리를 물고 끝없이 이어질 수 있다. 즉, 뒤에 이어지는 말들은 앞의 문맥을 더 구체화하는 역할을 한다.
성경으로 치면 롬 8:1처럼 말이다. 그들은 정죄가 없는데 그들이란 바로 그리스도 예수 안에서 걷는 자들이고, 그런 자들은 육체가 아니라 성령을 따라 걷는다.. them, who가 말을 쭉쭉 이어 준다.

한국어는 저런 영어의 특기를 그대로 따라하기가 난감하다. 관형어가 체언의 뒤가 아니라 앞에 등장하며, 길이가 한없이 길어지기 어렵기 때문이다. 말을 저렇게 만들면 십중팔구 번역투가 돼 버리니.. 문장을 둘로 나누거나 어순을 재배치한다든가 해야 한다. 한국어에 가주어 it 같은 개념이 있지도 않으니 말이다.

한국어는 인용문 안에 또 인용이 등장하는 식으로 문장을 n중으로 안았다면 안은 문장을 끝내는 종결어미도 n중으로 역순으로 스택 pop 하듯이 나와 줘야 한다. 그래서 성경에도 “... 있나이다, 하라, 하고” (창 32:18) 같은 문장이 종종 등장한다.

영어는 그런 제약이 없다. 지금 문장이 몇 중으로 깊게 인용돼 있건, 끝나는 건 인용이 없을 때와 아무런 차이가 없다. 이는
if() for() for() while() ...;
if() { for() { for() { while() { ... } ... } ... } ... }
의 차이와도 비슷해 보인다.
혹은, 전자는 굳이 스택을 사용하지 않는 선형적인 최적화가 가능한 tail recursion 구조인 반면, 후자는 그렇지 않은 느낌?

지금까지 별 잡생각이 다 튀어나왔는데, 정리하고 결론을 내리자면 이렇다. 한국어와 영어는 말을 길게 이어 나갈 때 화자의 관점 내지 사고 전개 방식이 서로 근본적으로 다르다는 것이다.
어느 것이 구조적으로 더 나은지 굳이 우열을 따질 필요는 없을 것이다. 비트 배열 순서(엔디언)나 함수 인자 전달 방식에 절대적인 우열이 존재하지는 않을 테니 말이다.

단지, 언어간에 이런 차이점이 존재한다는 것을 염두에 둔다면 외국어 학습이나 자연스러운 번역에도 도움이 될 것이다. 그리고 차이점들을 프로그래밍 언어의 특성과 연계해서도 생각해 볼 수 있다는 게 이 글의 요지이다.

Posted by 사무엘

2020/04/24 19:35 2020/04/24 19:35
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1744

1. 코드의 입체적 배치

C/C++에는 여느 프로그래밍 언어들과 마찬가지로 if else 조건문이란 게 있고 이게 여러 단계로 중첩될 수 있다. 단계가 깊어질수록 코드에서 들여쓰는 왼쪽 여백이 증가한다.

그런데 C/C++에는 전처리기 지시라는 것도 있어서 컴파일되는 실제 코드와는 완전히 별개의 다른 문맥과 차원에서 해석된다. 희곡에서 다른 코드들이 연극 대사라면 전처리기 지시는 괄호 안의 상황 설명지시와 비슷한 존재 같다. 결정적으로는 전처리기에도 조건부 컴파일을 지시하는 #if #else #endif 같은 물건이 있다.

전처리기의 #if도 여러 단계로 중첩되면 알아보기가 상당히 힘들어진다.
그러니 문득 드는 생각은.. 소스 코드의 들여쓰기도 3차원 입체로 표현 가능하면 어떨까 싶다. 통상적인 if else 등의 들여쓰기는 지금처럼 왼쪽 여백으로 표현하고, #if의 단계가 증가하면 그 부분의 코드가 몽땅 X, Y가 아닌 Z축으로.. 전방으로 한 단계 돌출되는 것이다. 해당 부분이 끝나면 다시 쑥 들어가고..
그러고 보니 전처리기 중에는 #if 말고도 #pragma pack처럼 스택 기반으로 설정을 저장하는 것들이 더 있기도 하다.

컴퓨터야 1차원적인 메모리 셀에서 코드와 데이터를 죽어라고 읽고 쓰고 계산하는 기계이겠지만, 그걸 기술하는 프로그램 코드라는 건 색깔(syntax coloring)과 XYZ 축 공간을 모두 이용해서 인간이 최대한 알아보기 편하게 시각화를 할 수 있다. 하지만 이런 수단을 몽땅 동원해도 남이 만든 코드는 선뜻 읽기가 어렵다.;;.

2. 컴파일러의 경고

C/C++ 코딩을 하다 보면 컴파일러가 뱉어 주는 경고 메시지의 도움을 종종 받곤 한다.
제일 자주 보는 건 아무래도 선언만 해 놓고 사용되지 않은 변수, 초기화하지 않고 사용한 변수, void형 함수가 아닌데 return으로 실행이 종료되는 구간 따위이다. 이런 건 에러로 치면 단순 스펠링 오타나 {}() 호응 미스, type mismatch만큼이나 흔하다. 아 하긴 type mismatch는 가벼운 건 warning 형태도 있긴 하다.

경고의 민감도를 상향 조정하면 if문에서 괄호 없이 대입(=) 연산자가 쓰인 것(혹시 비교 연산 ==이랑 헷갈린 거 아니냐?), 우선순위가 아리까리 한 << 나 & 같은 연산자가 괄호 없이 마구 섞여 쓰인 것, 심지어 for이나 if문이 뒷부분 없이 그냥 세미콜론으로 종결된 것까지도 실수가 아닌지 의심스럽다고 일단 지적해 주기도 한다.
글쎄, 컴파일러가 그 정도로 민감하다면.. 본인이 예전에도 언급한 적이 있지만 a=a++이나 a>>-2처럼 이식성이 없는(즉, 컴파일러마다 결과가 다를 수 있는 undefined behavior) 수식이야말로 안 쓰는 게 좋다고 경고를 띄워 줘야 하지 않나 싶다.

요즘 컴파일러는 printf/scanf에서 %문자와 실제 인지의 대응이 맞는지까지도 체크한다. printf 출력일 때는 float건 double이건 %f만 써도 충분하지만(float도 어차피 double로 값이 promote되므로), scanf 입력일 때는 둘은 %f와 %lf로 정확하게 구분돼야 한다.
가변 인자는 그야말로 type-safety의 완벽한 사각지대인데 이런 실수를 컴파일러가 잡아 준다면 프로그래머에게 굉장히 도움이 된다. 원래 전문적인 '정적 분석'용으로 쓰이는 함수의 인자별 annotation 정보까지 컴파일러가 활용하는 것 같다.

그런데 내가 지금까지 본 컴파일러 경고들 중 제일 계륵 같은 건 코드를 32비트에서 64비트로 포팅 하면서 생겨난 수많은.. type mismatch이지 싶다. 이제 int의 크기와 포인터의 크기가 일치하지 않게 되고, 덕분에 int와 INT_PTR의 구분이 생겼기 때문이다.
일단, 이 경고는 레거시 코드에서 발생하는 양이 어마어마하게 많은 편이다. 그리고 (1) 치명적인 것하고 (2) 그다지 치명적이지 않은 것이라는 분명한 구분이 존재한다.

int에다가 포인터를 곧장 대입하는 부분은 전자에 속한다. 이건 번거롭더라도 int를 당연히 INT_PTR로 바꿔 줘야 한다.
그러나 두 포인터의 차이를 대입하는 부분은 상대적으로 덜 치명적인 부분에 속한다. 왜냐 하면 64비트 환경이라 해도 작정하고 프로 연구자가 컴퓨터를 굴리는 게 아닌 한, 단순 end-user급에서 대놓고 2GB, 4GB를 넘는 데이터를 취급할 일은 거의 없다시피하기 때문이다.

특히 문자열의 길이를 구하는 strlen, wcslen 같은 함수 말이다. 리턴 타입이 size_t이지만.. 난 경고를 없애기 위해 그냥 대놓고 #define _strlen32(x) static_cast<int>(strlen(x)) 이런 것도 만들어서 썼다.
주변의 int 변수를 몽땅 확장하기에는 내 함수의 인자와 리턴값, 구조체 멤버 등 영향 받는 게 너무 많고 귀찮고, 그 반면 세상에 문자열 길이가 4GB를 넘어갈 일은 없기 때문이다.

대부분의 경우 무시해도 상관은 없지만 그래도 경고가 뜨는 게 마음에 걸리고, 그렇다고 기계적이고 무의미한 typecast 땜빵을 하고 싶지도 않으니.. 이건 64비트 컴퓨팅이 선사한 계륵 같은 경험이었다.

3. #include 절대경로 표시

요즘 개발툴 IDE, 에디터들은 코드에서 각종 명칭을 마우스로 가리키기만 하면 그게 선언된 곳이 어딘지를 친절하게 알려 준다. #define 매크로도 다 파악해서 이게 전개된 결과가 무엇인지도 툴팁 형태로 표시해 준다.
이와 관련해서 개인적으로는.. #include 다음에 이어지는 토큰을 마우스로 가리키고 있으면 얘가 무슨 파일을 가리키는지도 절대경로를 알려 주는 기능이 있으면 좋겠다. 이 파일을 여는 기능은 Visual Studio건 xcode건 이미 다 제공되고 있으니, 그렇게 알아낸 파일명을 가만히 표시만 해 주면 된다.

C/C++의 include 경로 찾기 규칙은 꽤나 복잡한 구석이 있기 때문이다. #define이 정의돼 있는 줄 모르고 삽질하는 것처럼, 예상하지 않은 다른 디렉터리에 있는 동명의 파일이 잘못 인클루드 되어 착오가 발생할 가능성이 있다.
이는 마치 경로를 생략하고 파일명만 달랑 입력했을 때 실행 파일 디렉터리를 탐색하는 순서라든가 LoadLibrary 함수가 DLL의 경로를 탐색하는 순서와도 비슷한 면모이다.

#include로 지정하는 경로는 C/C++ 문법의 지배를 받는 문자열 리터럴이 아니다. ""로 둘러싸기 때문에 문자열 리터럴처럼 보이지만 <>로 둘러싸는 것도 가능하고.. 여기서는 역슬래시 탈출문자가 적용되지 않기 때문에 디렉터리 구분자를 지정할 때 \를 번거롭게 두 번 쓸 필요도 없다. 애초에 #include는 컴파일러가 전혀 아닌 전처리기의 영역이니 당연한 소리인데.. 가끔은 당연한 사실이 당연하게 와 닿지가 않는다.

그런데 #include 경로명에다가 매크로 상수를 지정할 수는 있다. 내가 지금까지 이런 기괴한 용례를 본 건 지금까지 FreeType 라이브러리의 소스가 유일하다. IDE가 이런 것까지 다~~ 파악해서 실제로 인클루드 되는 헤더 파일을 사용자에게 알려준다면 코드를 분석하는 데 큰 도움이 될 것이다.

4. 리터럴 형태의 표현

프로그래밍 언어가 표현력이 좋으려면, 함수 코드든지 재귀성을 지난 복잡한 데이터든지(리스트, 배열 테이블 등) 불문하고 하나의 리터럴 내지 값(value)로 취급 가능하며, 굳이 이름을 붙여서 변수로 할당하지 않아도 함수 인자나 리턴값으로 자유자재로 주고 받고 대입 가능해야 한다.
쉽게 말해 함수 포인터가 들어갈 곳에 이렇게 함수 몸체가 곧장 들어갈 수 있어야 하며..

qsort(pData, nElem, nSize, [](const void *a, const void *b) { return 어쩌구저쩌구 } );

데이터가 들어갈 곳에도 이렇게 배열을 즉석에서 지정할 수 있어야 한다는 얘기이다.

memcpy(prime_tbl, {2,3,5,7, 11}, sizeof(int)*5);

하지만 C/C++은 이런 쪽의 유연한 표현력이 매우 취약했다.
함수 쪽은 machine word 하나에 딱 대응하는 것 이상의 context를 담은 추상적인 포인터를 지원하지 않는 관계로 클로저나 함수 안의 함수 따위를 지원하지 않는다.

그리고 언어 차원에서 복합 자료형을 built-in type으로 직접 지원하는 게 없다. 전부 프로그래머나 라이브러리의 구현에 의존하지..
그렇기 때문에 복잡한 데이터 리터럴은 변수를 초기화하는 이니셜라이저 형태로나 아주 제한적으로 표현 가능하며 이마저도 구조체· 배열의 초기화만으로 한정이다. 리터럴 형태 표현 가능한 배열 비스무리한 건 읽기 전용 null-terminated 문자열이 고작이다.

함수를 리터럴 형태로 표현하는 건 C++에 람다와 함수형 패러다임이 도입되면서 상황이 많이 나아졌다.
그 뒤 복합 자료형을 리터럴 형태로 표현하는 것도 C++1x 이후로 하루가 다르게 새로운 문법이 도입되면서 바뀌고 있긴 하다.

이름을 일일이 붙이지 않고 아무 테이블 및 계층 자료구조, 그리고 함수를 마음대로 선언해서 함수의 인자나 리턴값으로 주고받을 수 있는 것은..
마치 메신저나 이메일로 스크린샷 그림을 주고받을 때 매번 그림을 파일로 저장하고 그 파일을 선택하는 게 아니라 간단히 print screen + 클립보드 붙여넣기만으로 그림 첨부가 되는 것과 비슷하다. 의식의 흐름을 매우 편리하고 직관적으로 코딩으로 표현 가능하게 해 준다.

디자인 근본적인 차이로 인해 C++이 무슨 파이썬 수준의 유연함을 갖는 건 무리이겠지만 저 정도만으로도 엄청난 변화이며 한편으로 컴파일러를 구현하기에는 굉장히 난감할 것이다. 저수준 성능과 고수준 추상화 범용성이라는 두 모순되는 토끼를 몽땅 잡아야 하기 때문이다. 특히 그런 문법이 템플릿 내지 캐사기 auto와 결합하면.. 복잡도가 끔찍할 수준일 것 같다.

5. 기타

(1) 변수나 함수를 선언할 때는 type을 지정하면서 각종 modifier나 storage class를 같이 써 주게 된다. 거기에 들어가는 단어 중에는 static과 const, 그리고 int와 __declspec(..)처럼 대충 순서가 바뀌어도 되는 것이 있다.
그런데 long unsigned a도 된다는 것은 지난 20여 년 동안 본인이 한 번도 시도해 본 적이 없었다. 영어 어순 직관과 어울리지 않으니까.. 하긴, 2[a]도 되는 언어에서 저 정도쯤이야 이상할 게 없다.

(2) void main(void) {}은 컴파일은 되지만 void가 뭔가 권장되지 않고 바람직하지 않은 형태로 쓰이는 전형적인 예시라 하겠다. main 함수의 프로토타입도 그렇고, 또 함수에 인자가 없음을 나타낼 때는 C/C++ 가리지 않고 ()만 써도 충분하기 때문이다.

(3) 잘 실감이 나지 않겠지만 요즘은 C와 C++은 서로 따로 제 갈 길 가고 있다. 특히 C99와 C++1x부터 말이다. 그렇기 때문에 세월이 흐를수록 C 코드를 C++ 컴파일러에서 곧바로 돌리기는 어려워질 것이다.

보통은 C가 C++에 있던 // 주석, inline 키워드, C++ 라이브러리에 있는 몇몇 기능들을 자기 스타일로 도입하는 형태였지만 최신 C에서 C++과 무관하게 독자적으로 도입한 기능 중 하나는 restrict 키워드이다. 얘가 가리키는 메모리 주소는 딴 데서 건드리지 않으니 마음 놓고 최적화해도 된다는 일종의 힌트이다. volatile과는 반대 의미인 듯하다.

컴파일러에 따라서는 C++에서도 얘를 __restrict 이런 형태의 비표준 확장으로 도입한 경우가 있다. 하지만 Visual C++은 내가 알기로는 그리하지 않은 것 같다. 마소 컴파일러는 C 단독은 거의 없는 자식 취급하고 C99 지원도 안 하고 있으니 말이다.

(4) 방대한 C/C++ 코드에 정적 분석을 돌려 보면, 아무 type-safety 단서 없이 무데뽀로 아무 메모리에다 임의의 바이트만치 쓰기를 허용하는 memset, memcpy 계열의 함수에 실수와 버그가 들어간 경우가 생각보다 굉장히 많다고 한다.
배열 크기만큼 써야 하는데 포인터 크기(4/8바이트!)만치만 기록해 버리는 건 약과다. 둘째 인자와 셋째 인자의 순서가 바뀌어서 0을 기록하는 게 아니라 0바이트만치만 기록한다거나..;;

sizeof(A)*b라고 써야 할 것을 실수로 sizeof(A*b)라고 써 버려서 역시 4/8바이트 고정과 같은 효과가 나기도 한다. 전체 바이트 수를 써야 하는 곳과 배열의 원소 수만 써야 하는 곳을 헷갈리는 것도 미터나 피트 같은 단위를 헷갈려서 착오를 일으킨 것과 비슷하다.
문제는 저런 건 잘못 써도 언어 문법상으로는 아무 잘못이 없고 철저하게 합법이라는 것이다. 컴파일러가 잡아 주지 못하니 더 고차원적으로 문맥을 읽는 정적 분석에 의존해야 한다.

Posted by 사무엘

2020/04/17 08:36 2020/04/17 08:36
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1741

Windows API에는 현재 실행 중인 프로세스 및 스레드의 정체를 알려 주는 GetCurrent[Process/Thread]{Id}라는 함수쌍이 있다. Current 다음에 Process가 오느냐 Thread가 오느냐, 그리고 그 뒤에 Id가 붙느냐 안 붙느냐에 따라 2*2 총 4가지 조합이 존재한다.

뒤에 Id가 붙은 함수는 시스템에서 실행 중인 모든 프로세스 및 스레드를 유일하게 식별하는 32비트 정수형(DWORD) 번호를 되돌린다. 그리고 그게 없으면 이들 함수는 HANDLE이라는.. 성격이 좀 다른 번호를 되돌린다. 명목상 포인터 크기와 동일하지만, 64비트에서도 얘 역시 여전히 사실상 32비트 크기만치만 활용된다.

HANDLE로는 ID처럼 프로세스나 스레드를 유일하게 식별할 수 없는 걸까? HANDLE과 ID의 차이는 무엇이며, 둘의 구분은 왜 존재하는 걸까?

답을 얘기하자면 HANDLE은 ID 이상으로 더 무겁고 복잡하며 상태 의존적인 별개의 존재이다.
HANDLE은 일단 커널 오브젝트이다. 값을 얻기 위해 뭔가 운영체제로부터 자원을 할당받고 나중에 반납을 해야 한다. 사용한 뒤에는 마치 열었던 파일을 닫듯이 CloseHandle을 호출해서 닫아 줘야 한다. 단순 ID에는 이런 과정이 필요하지 않다.

그리고 이 HANDLE은 뮤텍스나 이벤트 같은 동기화 오브젝트 중의 하나이다. WaitForSingleObject 함수에다 넘겨서 이 프로세스나 스레드의 실행이 끝날 때까지 기다리는 용도로 사용할 수 있다.
심지어 HANDLE이 가리키는 그 프로세스나 스레드가 실행이 종료됐더라도 그 핸들 자체는 정식으로 닫아 주기 전까지는 여전히 살아 있다.

또한, 값이 다른 여러 HANDLE이 동일한 프로세스나 스레드를 참조할 수 있으며, 동일한 그런 개체에 대해서도 한번 닫았다가 핸들을 다시 얻은 리턴값은 달라질 수 있다. 마치 메모리 할당 함수의 실행 결과처럼 말이다. 그러므로 프로세스나 스레드 실체만을 유일하게 식별하려면 ID를 살펴보는 게 정답이다.

끝으로 결정적으로... GetCurrent**** 함수는 핸들이긴 하지만 좀 특이한 값을 되돌린다. 바로.. 그 함수를 호출하는 프로세스 및 스레드 자기 자신을 의미하는 고정된 상수만을 되돌리기 때문이다. IP 주소로 치면 localhost처럼 말이다. 이 상수 핸들은 CloseHandle을 하지 않아도 된다.

자기 자신 프로세스를 의미하는 상수는 -1 (0xFFFFFFFF)이고, 자기 자신 스레드를 의미하는 상수는 -2 (0xFFFFFFFE)이다.
이 정도면 #define HANDLE_CURRENT_PROCESS 이런 식으로 함수 대신 그냥 매크로 상수로 박아 넣어도 되고.. 프로세스 핸들과 스레드 핸들이 서로 섞여 쓰일 일도 없으니 -1과 -2로 구분조차 하지 않아도 된다. 하지만 Windows API가 처음 만들어질 때 그렇게 되지는 않았다.

비록 저 함수가 고정된 상수만 되돌린다는 것이 공공연한 비밀에 20년이 넘는 관행이 돼 버리긴 했지만, 미래에 이 함수의 리턴값이 바뀔 수도 있으니 꼬박꼬박 함수를 호출해서 핸들값을 사용해 달라는 것이 마소의 방침이다.
Windows NT가 개발된 지 30년이 돼 가는 지금 시점에서 이들 함수의 리턴값이 달라질 가능성은 사실상 0으로 수렴했지만.. 그래도 세상일은 알 수 없으니 말이다.

자기 자신 말고 타 프로세스의 유효 핸들은 아무래도 기존 프로세스 ID로부터 얻는 게 제일 직관적이다. 프로세스 ID는 프로세스 전체를 조회하는 EnumProcesses로부터 얻을 수도 있고 윈도우 핸들로부터 GetWindowThreadProcessId를 호출해서 얻을 수도 있다. 당연히 그 윈도우를 생성한 주체를 얻는다.

그렇게 해서 얻은 프로세스 ID에 대해서 OpenProcess를 호출하면 프로세스 핸들을 얻을 수 있다. 그럼 이 핸들에 대해서는 프로세스를 강제 종료하는 Terminate**** 함수, 아까처럼 실행이 끝날 때까지 기다리는 WaitFor**** 함수, 얘가 64비트인지 여부를 얻는 IsWow64Process, 실행 파일 이름을 얻는 GetModuleFileNameEx 등.. 할 수 있는 일이 몇 가지 있다.

CreateProcess 함수는 새로운 프로그램을 실행하면서 PROCESS_INFORMATION 구조체에다가 새 프로세스의 핸들과 ID, 그리고 primary 스레드의 핸들과 ID 이렇게 네 정보를 모두 쿨하게 되돌려 준다. 그러니 좋긴 하지만.. 이것들을 사용하지 않는다면 즉시 CloseHandle도 잊지 말고 해 줘야 resource leak를 방지할 수 있다.

스레드에 대해서도 프로세스와 비슷하게 스레드 ID로부터 유효 핸들을 얻는 OpenThread라는 함수가 있다. 하지만 저 함수는 OpenProcess와 달리, 본인이 지난 수십 년의 프로그래밍 커리어 전체를 통틀어 한 번도 사용한 적이 없었다.

일단, 내 코드가 생성한 스레드라면 그냥 CreateThread의 리턴값을 받아 두면 되니, 별도의 방법으로 스레드 핸들을 얻을 필요가 없기 때문이다. 저렇게 스레드 핸들을 얻는 건 무슨 시스템 유틸리티를 만들고 있어서 내가 생성하지 않은 듣보잡 프로세스 내지, 내 프로세스 안에서도 타인의 듣보잡 스레드를 건드려야 할 때나 필요하다. 그리고 그런 일은 일반적으로는 잘 없다.

그리고 스레드 핸들은 그냥 끝날 때까지 대기할 때(WaitFor***), 아니면 우선순위를 조절할 때(SetThreadPriority) 정도..?? 프로세스 핸들만치 무슨 정보를 얻고 쓸 일이 별로 없기도 하다. 그러니 자기 자신을 가리키는 가짜 핸들을 얻는 GetCurrentThread도 쓸 일이 거의 없다. 강제 종료 역시 TerminateThread는 TerminateProcess보다 훨씬 더 위험하며 훨씬 더 비추되는 짓이고 말이다.

프로세스나 스레드의 실행이 종료되는 것하고 해당 프/스를 가리키던 핸들이 완전히 해제되는 것은 완전히 별개의 일이다. 심지어 Terminate*를 호출해서 강제로 실행을 중단시켰더라도 거기에다 넘겨줬던 핸들은 CloseHandle을 따로 해 줘야 한다.

AttachThreadInput이라든가 SetWindowsHookEx 같은 UI 함수에서 스레드를 지정할 때는 그냥 간편하게 ID를 지정하는 것만으로 충분하다. 굳이 핸들값을 주지 않아도 된다.
이런 여러 이유들로 인해 스레드 핸들은 프로세스 핸들보다 쓰이는 빈도가 낮다.

이상이다.
이런 것들은 Windows 프로그래밍에서 완전 기초 내용이다. 하지만 기본기 복습 차원에서 프로세스와 스레드, 그리고 핸들과 ID의 관계를 이렇게 한번 정리해 놓고 싶다는 생각이 코딩 중에 문득 들었다.

Posted by 사무엘

2020/03/15 08:34 2020/03/15 08:34
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1728

오늘날 마이크로소프트는 운영체제와 오피스뿐만 아니라 개발툴 분야도 세계를 석권해 있다.
걔들은 과거에 운영체제 쪽은 맥 내지 IBM OS/2와 경쟁했었고, 오피스는 로터스, 워드퍼펙, 한컴(...)과 경쟁했으며.. 개발툴 쪽은 볼랜드라는 쟁쟁한 기업과 경쟁했다.

마소와 볼랜드가 내놓았던 프로그램 개발툴은.. 먼저

1. IDE까지 있는 도스용 대중 보급형의 브랜드가 있었다.
볼랜드는 터보, 마소는 퀵.. 뭔가 스피디한 단어를 썼다는 공통점이 있다.
그리고 볼랜드는 브랜드명-언어명 사이를 띄었지만, 마소는 둘을 붙여 썼다.;;

Turbo Basic, Turbo C, Turbo Pascal
QuickBasic, QuickC, QuickPascal

다음은 볼랜드 말고 '마소'에서 개발했던 QuickC와 QuickPascal IDE의 스크린샷이다. 보기에 참 생소하다. 출처는 유명한 고전 소프트웨어 라이브러리인 WinWorld이다.

사용자 삽입 이미지

사용자 삽입 이미지

마소는 QuickBasic만 건지고 나머지는 다 망했다. QuickBasic이야.. 뭐 무료 축소판 QBasic을 MS-DOS와 Windows에다 포함시키기까지 했을 정도이고 말이다. 빌 게이츠가 베이식 언어를 아주 좋아했다.
그 반면 볼랜드는 Turbo Basic만 망하고 C와 Pascal을 건졌다. Turbo Basic의 개발진은 볼랜드를 퇴사하고 따로 회사를 차려 PowerBasic을 만들게 됐다.

2. 다음으로, 본가에 속하는 최상위 플래그십 제품군에는 그냥 자기 회사명을 붙였다.

Borland Pascal, C++
Microsoft Basic, C/C++

1990년대에 C에 이어 C++ 컴파일러가 개발되면서 자기 제품의 공식 명칭을 아예 C++이라고 바꿔 붙이는 곳이 있는가 하면, C와 겸용임을 내세우면서 C/C++이라고 붙이는 곳도 있었다.

볼랜드의 경우 C++을 C와는 완전 별개로 취급했는지 버전까지 1.0으로 도로 리셋하면서 Turbo C++ 내지 Borland C++이라고 작명했지만.. 마소는 C++을 기존 C 컴파일러의 연장선으로 보고 MS C 6.0 다음으로 7.0을 MS C/C++ 7.0이라고 작명했다. 사실, 연장선이라고 보는 게 더 일반적인 관행이었다.

참고로 왓콤 역시 Watcom C 9.0의 다음 버전이 Watcom C/C++ 9.5가 돼서 마소와 비슷하게 작명과 버전 넘버링을 했다. 왓콤은 제품이 짬이 길다는 인상을 주기 위해 첫 버전을 일부러 1이 아닌 6.0부터 시작하는 기행을 벌였었다! 볼랜드의 버전 넘버링과 비교하면 극과 극 그 자체였다.

터보 C++이랑 볼랜드 C++의 차이는.. 더 덩치 큰 상업용 프로그램 개발을 위한 OWL/Turbo Vision 같은 자체 프레임워크 라이브러리를 제공하느냐 여부 정도였지 싶다. 프로페셔널 에디션이냐 엔터프라이즈 에디션이냐의 차이처럼 말이다. 그리고 이때쯤 Windows용 지원도 시작됐다.

3. 그랬는데, 1990년대 이후부터는 그 플래그십 제품군도 Windows 전용의 더 고급 브랜드로 대체됐다.

볼랜드는 90년대 중반의 Delphi와 C++Builder로,
마소는 그 이름도 유명한 비주얼 브랜드로 말이다. Visual Basic, Visual C++.
그리고 마소도 Visual C++부터는 C/C++ 대신 C++만 내걸기 시작했으며,

관계가 이렇게 된다.
Visual C++이 과거 MS C/C++을 계승한 거라는 흔적은 _MSC_VER 매크로 값이 Visual Studio 자체의 버전보다 더 크다는 점을 통해서나 유추할 수 있다.

1이 2를 거쳐 3으로 바뀌는 동안 주변에서는 C 대신 C++이 대세가 되고, 주류 운영체제가 도스에서 Windows로 완전히 넘어가고 거대한 프레임워크 라이브러리가 등장하는 등의 큰 변화가 있었다. 개발 환경도 단순히 코딩용 텍스트 에디터와 디버거 수준을 넘어서 RAD까지 추구하는 수준으로 발전했다.

또한, 이 3단계가 주류가 될 즈음부터 마소의 Visual 툴들이 볼랜드를 완전히 꺾고 제압해 버렸다.
마소가 운영체제 홈그라운드라는 이점을 갖고 있기도 했거니와, 또 근본적으로는 파스칼이라는 언어 자체가 볼랜드의 창업자인 필립 칸이 선호하거나 예상한 것만치 프로그래밍계의 주류가 되지 못하고 마이너로 밀려난 것이 크게 작용했다. 네이티브 코드 생성이 가능하면서 빌드 속도가 왕창 빠른 건 개인적으로 무척 마음에 들었는데 말이다..;;

그에 반해 마소의 베이식은 파스칼보다 그리 나은 구석이 없는 언어임에도 불구하고 자사 운영체제의 닷넷빨 있지, 레거시 베이식도 자사 오피스의 VBA 매크로 언어가 있으니 망할 일이 없는 지위에 올라 있다.

한때(1990년대 후반??)는 파스칼이 언어 구조가 더 깔끔하고 좋다면서 정보 올림피아드 같은 데서라도 각광 받았지만.. 지금은 그런 것도 없다. 그 바닥조차도 닥치고 그냥 C/C++이다.
델파이를 기반으로 이미 만들어진 유틸리티나 각종 DB 연계 프로그램들(상점 매출 관리 등등..), SI 쪽 솔루션을 제외하면 파스칼은 마치 아래아한글만큼이나 입지가 좁아져 있지 않나 싶다..;;.

범언어적인 통합 개발 환경이라는 개념을 내놓은 것도 마소가 더 일렀다. Visual Studio가 나온 게 무려 1997년이니까.. 개발툴계의 '오피스'인 셈이다. (Word, Excel 등 통합처럼 Basic, C++ 통합). 그에 비해 볼랜드 진영에서 Delphi와 C++Builder를 통합한 RAD Studio를 내놓은 것은 그보다는 훨씬 나중의 일이다.

Windows NT야 이미 있던 16비트 Windows와 버전을 맞추기 위해서 3.1부터 시작했는데, Visual Studio의 경우, 공교롭게도 1990년대 중반까지 Visual Basic과 Visual C++의 버전이 모두 4.x대였다.
그래서 첫 버전인 Visual Studio 97은 각각의 툴 버전과 Studio 버전이 모두 깔끔하게 5로 맞춰졌으며, 이듬해에 나온 차기 버전은 어째 98이라는 연도 대신, 버전인 6으로 맞춰질 수 있었다.

2010년대 이후로 C++이 워낙 미친 듯이 바뀌고 발전하고 있으니.. D 같은 동급 경쟁 언어들조차 기세가 꺾이고 버로우 타는 중이다. 도대체 지난 2000년대에 C++98, C++03 시절에는 C++ 진영이 export 병크 삽질이나 벌이면서 왜 그렇게 침체돼 있었나 의아할 정도이다. 그 사이에 Java나 C# 같은 가상 머신 기반 언어들이 약진하니, 뭘 모르는 사람들은 겁도 없이 "C++은 이제 죽었네" 같은 소리를 태연히 늘어놓을 지경까지 갔었다. (2000년대 중반이 Windows XP에, IE6에... PC계가 전반적으로 좀 '고인물'스러운 분위기로 흘러가던 때였음) 한때 잠시 그러던 시절이 있었다.

Posted by 사무엘

2020/01/20 08:34 2020/01/20 08:34
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1707

« Previous : 1 : ... 2 : 3 : 4 : 5 : 6 : 7 : 8 : 9 : 10 : ... 31 : Next »

블로그 이미지

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

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

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

Site Stats

Total hits:
3041598
Today:
1225
Yesterday:
1700