« Previous : 1 : ... 4 : 5 : 6 : 7 : 8 : 9 : 10 : 11 : 12 : ... 13 : Next »

회사 업무 때문에 구질구질한 재래식 Windows API 기반 네이티브 데스크톱 프로그램이 아니라 일련의 신문물들을 접할 일이 있었다. 바로 지금까지 말로만 듣던 Windows Phone 플랫폼 개발 때문이었다.

1. Windows 8.1

Windows 8로 넘어가면서 부팅부터 UEFI라는 기술이 도입되면서 뭐가 좀 바뀌었다. 운영체제를 다시 설치하려고 부팅 디스크 탐색 순서를 바꾸려고 해도 BIOS Setup에서 좀 번거로운 절차를 거쳐야 하게 되었다.

Windows Phone 에뮬레이터를 돌리려면 역시 BIOS Setup을 들어가서 기본적으로 꺼져 있는 CPU 가상화 기능을 켜야 하며, OS도 아무거나 쓰면 되는 게 아니라 8.1 Pro 이상급이 반드시 필요하다. Hyper-V 기능이 home급에서는 지원 안 되고 Pro나 엔터프라이즈 급 이상부터만 지원되기 때문이다.
Pro 이상에서만 지원되는 대표적인 기능이 바로 원격 데스크톱 서버 기능인데.. 그런 비슷한 기능이 하나 더 생긴 것이다.

요즘은 스마트폰 CPU도 PC와 별 차이 없을 정도로 굉장한 고사양이기 때문에 에뮬레이션을 위해서는 CPU 차원에서의 온갖 첨단 기능이 덩달아 필요해진 듯하다. 덕분에 이젠 가상 머신에서도 Windows Aero 효과가 돌아가는 세상이 되기도 했다.

Windows 8 이상의 그 각지고 단순해진 GUI를 보면, 비스타/7에 비해 퇴화한 것 같고 화면을 왜 저렇게 만들었나 싶은 생각이 처음에 들었다. Windows 8부터는 아시다시피 고전 테마가 없어지고 화면 scheme은 오로지 "표준 아니면 고대비"로 극도로 단순화됐다.
하지만 이젠 저것도 그럭저럭 적응이 돼 간다. 외형이 단순해진 대신, 단조로움을 덜기 위해 창틀의 색깔이 시시각각 변하는 기능이 생기기도 했고. ㅎㅎ

운영체제를 설치하는 중에 전체 화면에서 배경색이 서서히 알록달록하게 변하는 건, 마치 도스 시절 VGA mode 13h에서 전체 화면 게임 프로그램이 팔레트 스크롤을 하는 것 같은 느낌이 들었다.

2. Visual C++ 2013

외형이 별로 바뀐 게 없고 시간 간격도 2012에 비해 겨우 1년 차이밖에 안 나는지라 변화량을 만만하게 봤었는데, 실제로 써 보니 그렇게 만만하지는 않다. 아기자기한 기능들이 많이 강화되고 좋아졌다.

색깔 scheme이 하양과 검정 말고 '파랑'이 하나 더 생겼는데 파랑은 옛날의 우중충한 2010 분위기를 내는 스타일이어서 개인적으로는 비호감.
운영체제의 기본 컨트롤을 쓰는 게 아니라 모든 걸 자기가 직접 그린다는 특성상, 스크롤 바가 굉장히 똑똑해졌다. cursor가 속한 줄 위치가 스크롤 바에도 언제나 표시되어 나오고, 스크롤 중에 페이지 썸네일을 표시하는 기능도 있다.

옵션(프로젝트 옵션과 프로그램 옵션 모두) 대화상자가 드디어 크기 조절이 가능해졌으며, C++도 코딩 중의 자동 서식과 자동 완성 기능이 제법 강화되었다.

Visual Studio (C++ 포함)는 지난 2005 버전 때부터 Express라는 무료 버전이 정식으로 배포되고 있다. 그래서 예전에는 플랫폼 SDK(= 무료 배포)도 자체적으로 컴파일러를 포함하고 있었는데 그것까지 express 에디션으로 완전히 대체되었다. 상업용 버전과는 달리 2013 Express 버전은 Windows 8용 Metro/Phone 앱만 만들 수 있는 'for Windows' 에디션과, 예전의 재래식 native 프로그램만 만들 수 있는 'for Windows desktop' 에디션이 따로 나뉘었다.

3. C++/CX

드디어 그 이름도 유명한 '요물'을 만져 보게 됐다. 처음에는 단순히 C++을 닷넷용으로 마개조한 Managed C++와 C++/CLI의 후신인줄로만 알았는데 그렇지 않다. C++/CX와 Windows RT API는.NET 내지 C++/CLI하고 무늬는 비슷하지만 내부 구조는 완전히 다르다.
예를 들어, 옛날의 C++/CLI에서는 일반 C++ 개체(new)와 관리되는 새로운(__gc new) 개체는 서로 has-a 관계조차도 맺을 수 없었다. 취급되는 방식이 서로 다른 개체를 다른 개체의 멤버로 가질 수 없었다는 뜻이다. C++/CX는 그런 제약이 없다.

.NET 그쪽 바닥은 전통적인 바이트코드 기반 런타임이지만 C++/CX는 엄연한 네이티브 코드 기반이다. 가장 큰 차이로 후자에는 garbage collector가 없다. ref new로 할당하는 ^ 라는 이상한 포인터가 있긴 하지만 얘는 내부적으로 그냥 레퍼런스 카운팅으로 관리될 뿐이다. .NET과 비슷한 API를 차용하고, C#에서 partial도 가져오고 델리게이트나 boxing 같은 것도 가져왔지만, 내부는 여전히 native라는 게 참 인상적이다. 게다가 이제 퇴물 신세가 됐나 싶던 COM 인터페이스까지 다시 끄집어냈다니!

Visual C++ 200x 시절에만 해도 이제 MS가 C++을 버렸네(특히 MFC!!), 네이티브 코드 시절이 끝났네, 심지어 Windows 차기 버전은 닷넷 같은 바이트코드 기반으로 완전히 새로 만들어진다네 하는 온갖 낭설이 떠돌았는데.. 201x로 와서는 그런 낭설이 완전히 불식된 듯한 느낌이다. MFC는 2008 feature pack 때부터 잘 알다시피 환골탈태하였으며, C++ 언어 자체도 C++11 같은 온갖 확장 규격에 힘입어 한없이 강력하고 복잡해졌다. 거기에다 Windows RT의 코드 기반도 네이티브 코드에 힘을 실어 줬으니 C++은 예나 지금이나 건재한 언어 인증을 하게 됐다.

이런 요물의 등장으로 인해 도리어 .NET의 위상이 굉장히 어중간해졌다. 비슷한 시기에 등장한 GDI+도 너무 금세 버림받고 낙동강 오리알 신세가 됐고 말이다. 얘는 이제 하드웨어 가속 지원도 못 받는다니, 안티앨리어싱이 되는 그래픽이 필요하다면 얄짤없이 Direct2D라도 새로 공부해야 하게 생겼다.

뭐 내부 메커니즘이야 어떻든, 네이티브 코드 C++에서도 delete 따질 필요 없이 new를 막 남발해도 된다는 게 무척 신기하며, 마치 자동차로 치면 수동을 몰다가 자동을 모는 듯한 느낌이다. 하지만 세상에 다 썼으면 반드시 반납을 해야 하는 리소스가 메모리만 있는 건 아닌데, 파일이나 다른 커널 오브젝트들은 어떻게 관리되며 생명 주기가 어떻게 되는지 궁금해질 때도 있다. 참고로, 레퍼런스 카운팅도 GC에 비해서 마냥 가볍고 편리하기만 한 물건은 아니며 약점이 있다.

Windows RT API들은 정말 복잡한 namespace와 클래스, 추상 계층들이 넘쳐난다. 지저분한 Windows API를 정말 허접하게 감싼 MFC 정도를 생각했다가 요즘 프레임워크들을 보면 입이 떡 벌어질 수밖에 없다.
멤버뿐만 아니라 클래스 자체에다가도 public 같은 접근성을 지정할 수 있으며, 더 상속이 안 되게 하는 sealed 속성을 줄 수 있다. 일반 C++에서는 허용되지만 C++/CX에서는 “무슨 클래스에서는 생성자가 public이어서는 안 된다, 데이터 멤버가 뭐여서는 안 된다”는 식의 까다로운 제약도 굉장히 많아서 처음엔 답답함을 느꼈다. (상속이라는 게 없는 언어에다가도 클래스를 제공할 수 있게 하기 위해 들어간 제약이라 함.)

이 기회에 delegate라는 게 뭔지도 다시 살펴보게 되었다. 선언 자체는 C++로 치면 함수 포인터 내지 멤버 함수 포인터에 대한 typedef를 선언하는 것과 비슷한 개념이며, 이놈의 인스턴스는 따로 new로 선언해야 한다. 그때 생성자에다가 다른 함수 명칭라든가 람다 함수를 집어넣어 주면 된다.
람다의 경우 this를 캡처로 주면 자연스럽게 멤버 함수도 대리자가 될 수 있으니 매우 유연하다. 물론 그 유연성은 성능 대가를 치르고 얻어진 것이겠지만 말이다.

Windows RT API에는 비동기적으로 행해지는 동작이 많으며, Concurrency Runtime 라이브러리와 밀접한 관계가 있다. 표준 C++ 라이브러리의 일부인 모양인데 create_task에다가 할 일들을 넣어 주고, 그 일이 반드시 다 끝난 뒤에 수행할 일은 저 함수의 리턴값에다가 .then 메소드를 호출하고 거기에다가 또 람다 형태로 넣으면 된다. 기본적으로 코딩 패턴이 그러하다.
.wait 메소드를 이용해서 동기화를 시켜도 되지만, 이 경우 Windows Phone은 UI 스레드까지 멈춰 버려서 데드락이 발생하는 듯하다. 참고로 C#의 경우 언어 차원에서 await이라는 전용 키워드가 존재한다고 함.

도대체 저 라이브러리는 어떻게 구현되었나? 소스 내부에 CreateThread, WaitForSingleObject 같은 Windows API라도 썼는지 궁금했지만.. 디버거로 내부 추적이 전혀 되지 않을 뿐더러 온갖 암호 같은 복잡한 템플릿은 도저히 분석 가능하지 않았다. 그래서 분석을 포기했다.
아무튼, C++은 람다 함수가 도입되어서 코드를 값으로 집어넣는 게 가능해지고, 이게 템플릿과도 결합하는 바람에 그야말로 예전의 C++에서는 상상도 할 수 없던 무궁무진한 활용이 가능해지긴 했다.

Windows RT 환경에서는 재래식 Windows API는 쓸 수 있는 것도 있고 그럴 수 없는 것도 있다. 이걸 일일이 다 분류하는 것도 마소의 엔지니어들의 입장에서는 엄청 고된 일이었겠다.
실행을 잠깐 멈추는 Sleep 함수도 누락되고 없기 때문에 Concurrency::wait를 써야 한다고 한다. 난 저걸 알기 전에는 이벤트 오브젝트를 만들어서 WaitForSingleObjectEx 함수를 쓰곤 했다.

끝으로, Windows RT의 C++/CX 환경은 네이티브 코드를 표방하는 관계로 재래식 static library와 DLL을 모두 만들어 쓸 수 있다. 단, 불가능하지는 않지만 static library의 경우 링커가 경고를 띄운다. 그건 오로지 같은 C++ 프로젝트에서만 활용 가능하니 재사용성이 크게 떨어지기 때문이라고.

RT 환경에서는 Windows Runtime Component라는 특수한 형태의 DLL을 만들어서 코드를 재사용하는 것이 권장되는 방법이다. Windows RT계의 COM 같은 물건인데, 그렇다고 COM 정도로 문법이 크게 제약되고 경직된 건 아니다. C#으로 표현 가능한 언어 요소들을 모두 표현할 수 있고, 아무래도 원시적인 인클루드와 라이브러리보다는 더 깔끔한 빌드/재사용 시스템인 듯하다.

이런 것들을 경험하고 나니 뭔가 미래에 갔다 온 듯한 느낌이었다.
XAML은 Win32 개발로 치면 rc 파일 같은 것이고
public ref class는 Win32에서 __declspec(dllexport) 같은 건가?

예나 지금이나 완전한 형태의 Windows 프로그램을 만들기 위해서는 무슨 언어든 문법 확장이 불가피했지만 지금은 더 체계적이고 조직적이고 더 노골적으로 하는 듯하다.
시대를 불문하고 불변인 자기만의 전문 영역이 있어야겠지만, 한편으로 최신 시대 조류도 놓치지 말고 따라갈 줄 알아야겠다는 생각이 새삼 들었다.

'독립 개발자 네트워크'를 운영하고 계신 깁뿔 님께서 오래 전에 Windows 8 개발 공부를 하면서 올려 놓으신 팁들을 이 기회에 뒤늦게나마 유용하게 활용했다.

Posted by 사무엘

2014/10/07 08:36 2014/10/07 08:36
, ,
Response
No Trackback , 6 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1015

예전에 에디트 컨트롤에 대해서 글을 한번 쓴 적이 있었는데 그것들 말고도 또 재미있는 이야깃거리가 많아서 글을 추가로 올리게 됐다.

1.
Windows의 에디트 컨트롤에는 ES_AUTOHSCROLL, ES_AUTOVSCROLL이라는 옵션이 있어서 이 옵션이 없으면 에디트 컨트롤은 가로나 세로로 스크롤이 되지 않는다. 그리고 스크롤만 안 되는 게 아니라 지금 화면 영역을 벗어나는 크기로는 텍스트가 입력 자체가 전혀 되지 않게 된다. 가령, 가변폭 글꼴을 쓴다면 W는 몇 개 입력 못 하지만 i는 꽤 많이 집어넣을 수 있다.

차라리 W든 i든 글자 수 자체에 대한 제약을 거는 거라면 모를까, 저런 제약 기능이 실생활에서 쓸 일이 있는지는 본인은 좀 회의적이다. 한 줄짜리 에디트 컨트롤이 메모리 상의 글자 수도 아니고 픽셀 길이가 초과했는데 스크롤이 안 되는 경우는 거의 찾을 수 없기 때문이다. (물론, 글자 수에 제약을 거는 방법은 EM_SETLIMITTEXT라고 방법이 따로 있긴 하다)

2.
에디트 컨트롤은 잘 알다시피 자체적으로 Ctrl+C, X, V 글쇠를 처리하여 텍스트에 대한 Copy/Cut/Paste 기능을 제공한다. 그런데 운영체제나 프로그램에 따라서는 "텍스트 전체 선택"을 의미하는 Ctrl+A도 지원되는 것 같기도 하고 안 되는 것 같기도 하다. 도대체 어찌 된 일일까?

실상은 이러하다. 내가 여러 조건을 달리하여 실험을 해 보니, 공용 컨트롤 6.0이 제공하는 새로운 에디트 컨트롤만이 single-line 방식에 한해서 Ctrl+A도 자체 처리한다. 나머지 일반 에디트나 multi-line 에디트는 아마 호환성 차원에서 이를 지원하지 않는다.

물론, 응용 프로그램이 Ctrl+A를 액셀러레이터에다 등록해서 자체적으로 에디트 컨트롤에다가 EM_SETSEL(0, -1)을 날려 준다면 어디서나 Ctrl+A가 동작하게 된다. 컨트롤이 아니라 그 컨트롤을 사용하는 응용 프로그램이 직접 Ctrl+A를 구현한 대표적인 예는 바로 메모장이다.

3.
에디트 컨트롤은 자신이 키보드 포커스를 받으면 텍스트 전체를 선택해 놓는다. 사용자가 기존 텍스트를 완전히 무시하고 입력을 새로 시작할지, 아니면 기존 입력을 그대로 유지하거나 살짝만 고칠지를 선택 가능하게 하자는 차원에서이다.
그런데 대화상자에서 굳이 tab 키로 포커스를 바꿨을 때뿐만이 아니라 Alt+? 액셀러레이터를 눌렀을 때도 이 동작이 일어나며, 심지어 지금 포커스를 받고 있는 동일한 컨트롤을 Alt+?로 다시 선택했을 때도 동일한 동작이 일어난다. 이것은 WM_SETFOCUS나 WM_KILLFOCUS하고는 관계가 없는 동작인데 어떻게 이런 일이 가능할까?

정답은 에디트 컨트롤이 WM_GETDLGCODE 메시지에 대해서 DLGC_HASSETSEL 비트 플래그를 되돌리기 때문이다.
대화상자는 자기 밑에 있는 컨트롤들에 대해서 이런 세부적인 메시지를 보내어 정보를 파악하는데, 저 플래그가 있는 컨트롤은 문자열 입력란으로 간주하여 액셀러레이터 키를 받았을 때도 EM_SETSEL 메시지를 보내 준다. 저 플래그만 쓰면, 운영체제의 표준 에디트 컨트롤이 아니어도 똑같은 동작을 하는 컨트롤을 얼마든지 만들 수 있다. <날개셋> 한글 입력기의 자체 에디트 컨트롤도 당연히 이 방식을 따랐다.

Posted by 사무엘

2014/09/20 08:38 2014/09/20 08:38
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1009

1. WM_QUERYDRAGICON 메시지

제목에 언급돼 있는 저 메시지는 도대체 뭘 하는 물건일까?
얘는 20여 년 전에 Winows 95가 등장한 이래로 쓸 일이 사실상 전혀 없어진 잉여이다.

그 주된 이유로는 첫째, 그때부터 minimized icon이라는 개념 자체가 운영체제에서 완전히 없어졌기 때문이다.
95 이래로 바탕 화면에는 '내 컴퓨터'나 '휴지통' 같은 걸 제외하면, 바탕 화면이라는 디렉터리에 있는 파일들만이 표시된다. 자주 쓰는 프로그램의 바로가기 정도나 바탕 화면에 표시되며 그것들도 엄밀히 말해서는 그 디렉터리에 있는 파일의 일종인 것이다.

최소화된 프로그램은 작업 표시줄의 제목 말고는 화면에 아무것도 보이지 않는다. 시작 메뉴와 작업 표시줄을 구동하는 explorer 셸 자체가 죽었다면 최소화된 프로그램이 진짜로 제목 한 줄만 달랑 보이는 최소화 상태로 있을 수 있지만, 이건 운영체제가 완전 막장이 됐을 때에나 발생하는 상황이며, 그냥 그 제목 텍스트를 드래그하면 되지 별도의 드래그용 아이콘이 필요하지는 않다.

둘째로, WM_SETICON / WM_GETICON이 그나마 남아 있던 아이콘 관련 기능을 완벽하게 대체해 버렸기 때문이다.
클래스를 등록하던 시절에 대표 아이콘이 지정되지 않았던 윈도우라 하더라도 가끔은 외형에 별도의 custom 아이콘이 필요할 때가 있다. 대화상자 단독으로 달랑 실행되는 프로그램이 대표적인 예다. 대화상자는 윈도우 프로시저는 거의 언제나 우리가 지정한 custom 버전이 쓰이지만, 그 윈도우 자체의 클래스 등록은 우리가 한 게 아니기 때문이다.

Windows 3.x 시절에는 창의 아이콘이 표시될 때가 최소화됐을 때 정도밖에 없지만, 95부터는 창 제목 왼쪽의 시스템 메뉴가 있는 곳에 창의 아이콘이 언제나 표시되어 있다. 그렇기 때문에 클래스 아이콘과 다른 아이콘을 별도로 공급하는 것은 운영체제가 나중에 응용 프로그램에다가 메시지를 보내는 형태가 아니라, 응용 프로그램이 사전에 운영체제에다 메시지를 보내는 것으로 디자인이 바뀌었다. 그 변경의 산물이 바로 WM_SETICON. 이 아이콘이 대외적으로 표시되며 심지어 Alt+Tab을 누른 동안 프로그램 리스트에도 뜨게 된다.

그럼에도 불구하고 MSDN과 구글 따위를 뒤져 보면, 이 메시지에 대해서는 20년도 더 전에나 유효하던 낡은 설명만이 기계적으로 그대로 소개되어 있으며, 이 정보는 오늘날 outdated됐다는 말은 어디에도 없다. 심지어 Visual C++ 2012의 MFC 마법사에서 대화상자 기반 응용 프로그램을 만들면, CDialog(Ex)의 파생 클래스는 저 메시지에 대한 핸들러도 여전히 참 친절하게도 만들어 준다. 뭐지 이건..?

2. 잉여 WM_SIZE 파라메터

잉여 요소가 의외의 가까운 곳에 또 있다.
WM_SIZE야 Windows 프로그래머치고 모르는 사람이 있을 수가 없는데.. wParam에는 최소화/최대화와 관련된 부가 정보가 따라온다. 최소화되었다면 SIZE_MINIMIZED가, 최소화되었다면 SIZE_MAXIMIZED가 오며, 그 밖의 일반 상황에서는 SIZE_RESTORED (0)가 된다. 딱 이 정도만 알고 있으면 된다.

그런데
SIZE_MAXHIDE: Message is sent to all pop-up windows when some other window is maximized.
SIZE_MAXSHOW: Message is sent to all pop-up windows when some other window has been restored to its former size.

라고 문서화돼 있는 이 값이 온 걸 본 적 있으신 프로그래머는 한번 손 들어 보시길..
저 조건을 최대한 만들어서 디버거 붙이거나 Spy++로 확인해 봐도 저런 건 좀체 안 온다.
어떤 프로그램 창이 최대화됐거나 해제됐다고 해서 다른 프로그램 창에 저 메시지가 올 거라고 생각한다면 경기도 오산이다.

구글, MSDN 다 뒤져도.. 저 기계적인 설명 말고 다른 용례는 안 나온다. 외국의 포럼에서 딱 하나 질문이 올라온 게 있긴 한데, 딱히 답변 없다. (☞ 링크 클릭)

Windows 운영체제의 레거시들 분석에는 세계 톱급의 전문가라 할 수 있는 레이몬드 챈 아저씨의 블로그, MFC와 Windows GUI 프로그래밍에서 한 가닥 했던 Paul DiLascia 등.. 거기에 설명이 없으면 아무데도 없는 거다.;;
나도 진지하게 굉장히 궁금하다. 저 설명과 매크로 상수값은 그저 잉여인지를? WINE 같은 데서는 저게 실제로 구현돼 있을까?

Posted by 사무엘

2014/08/22 08:21 2014/08/22 08:21
,
Response
No Trackback , 4 Comments
RSS :
http://moogi.new21.org/tc/rss/response/998

라벨, 에디트, 리스트 박스, 콤보 박스, 일반 버튼, 라디오/체크 박스.
이름만 들어도 친숙한 이 물건들은 응용 프로그램의 대화상자에 들어가는 그야말로 필수 GUI 구성요소들이다.

여기에다 리스트 컨트롤, 트리 컨트롤 같은 더 고급스러운 공용 컨트롤까지 있으니 실무에서는 사실 99%에 가까운 일은 기존 컨트롤만으로 다 해치울 수 있다.
사소하게 마음에 안 들고 동작 방식을 좀 고쳐야 하는 건, (1) 이들이 제공하는 owner-draw 옵션(주로 외형 외주) 내지 일부 메시지에 대한 (2) 윈도우 프로시저의 서브클래싱으로 customize 가능하다.

그러나 아주 가끔은, 기존 컨트롤과는 동작 방식이 완전히 다른 새로운 윈도우를 내가 머리부터 발끝까지 직접 구현하여 대화상자에다 집어넣어야 하는 경우가 있다. 기존 컨트롤을 약간만 고치는 정도로는 성이 안 찬다.

본인이 개발한 <날개셋> 한글 입력기의 GUI를 보면 이런 custom 컨트롤들이 적지 않게 보인다. 당장 비트맵 글꼴을 사용하는 자체 에디트 컨트롤은 편집기의 클라이언트 영역뿐만 아니라 대화상자에서 입력란으로도 쓰인다. 그리고 한글 낱자를 뽑아 오는 독특한 스타일의 콤보 박스라든가 문자표 리스트, 글쇠배열 편집 윈도우도 다 자체 개발한 custom 컨트롤이다.

꼭 그렇게까지 특이한 용도의 윈도우가 아니더라도.. 이걸 생각해 보자.
자체적인 키보드 포커스를 받으며, 화살표 key나 마우스 휠로 2차원적인 스크롤이 가능한 형태로 그림 같은 owner-draw 컨텐츠를 출력하는 컨트롤.
쉽게 말해 MFC로 치면 CScrollView에 해당하는 컨트롤이다. 특이할 것 없는 아주 기본적인 기능임에도 불구하고 기존 컨트롤의 서브클래싱만으로는 구현 가능하지 않다.

키보드 포커스를 받지 않으며 스크롤 같은 것도 없이 고정적인 owner draw 그림만을 화면에 달랑 그리는 거라면 쉽다. 기성 컨트롤인 STATIC 윈도우에다 owner draw 스타일을 줘서 WM_DRAWITEM 메시지를 처리하거나, 아예 서브클래싱을 해서 WM_PAINT 메시지를 통째로 가로채면 된다.

그러나 스스로 키보드와 마우스 입력을 처리하는 윈도우라면 독립된 형태로 자체 구현이 불가피하다. 여러 메시지들에 대한 동작을 인위로 고쳐야 하기 때문이다.
<날개셋> 편집기에서 화면 인쇄를 시켰을 때 화면 인쇄 결과를 출력하는 윈도우가 바로 이런 용도로 만들어진 custom control이다.

custom control을 MFC를 써서 개발한다면 응당 CWnd에서 파생된 새로운 클래스를 떠올리게 될 것이다. 이 C++ 클래스는 자신만의 윈도우 클래스 이름을 갖고 있을 것이고 자신을 운영체제에다 등록하는(RegisterClass 호출) 함수도 static 형태로 갖추고 있어야 할 것이다. (응용 프로그램이 실행 초기에 호출함)
그리고 대화상자 리소스에는 그 클래스 이름을 지정해 놓은 custom 컨트롤을 마음껏 생성해 놓는다.

그런데 대화상자의 컨트롤들은 내가 CWnd::Create를 호출하여 만드는 게 아니라 운영체제의 대화상자 관련 함수들이 CreateWindowEx에다가 윈도우 클래스 이름만 달랑 줘서 만들어진다. 이렇게 C++ 언어 체계의 통제를 받지 않고 밖에서 만들어진 HWND에다가 MFC 기반의 C++ 개체는 어떤 메커니즘을 통해 상호 연결시켜야 할까?

단순히 CWnd::FromHandle 함수 같은 걸로 임시 CWnd만 달랑 생성해서 연결하는 게 아니다. 이 custom 컨트롤은 구현체 클래스가 존재하니, 그냥 CWnd가 아니라 내가 만든 전용 CWnd 파생 클래스가 정확히 연결돼야 한다. 그렇게 해야 응용 프로그램 해당 윈도우끼리는 type-safe하지 않고 불편한 메시지를 쓸 필요 없이 C++ 멤버 함수/변수에 직통으로 가능해지니 좋다.

연결하는 방법은 크게 두 가지가 있다. 먼저, C++ 클래스의 생명 주기를 윈도우 자체의 생명 주기와 일치시키는 것이다. 만드는 윈도우 클래스에다가 윈도우 프로시저를 아래와 같이.. MFC의 AfxWndProc와 거의 똑같은 형태로 작성해 준다.

LRESULT CALLBACK CHeapCtrl::_MyWndProc(HWND hWnd, UINT msg, WPARAM w, LPARAM l)
{
    CWnd *pWnd = CWnd::FromHandlePermanent(hWnd);
    if (pWnd==NULL) { pWnd=new CHeapCtrl; pWnd->Attach(hWnd); }
    return AfxCallWndProc(pWnd, hWnd, msg, w, l);
}

이 개체는 언제나 new 연산자를 통해 heap에만 생성된다. 그러므로 PostNcDestroy 함수를 다음과 같이 구현하여 윈도우와 함께 C++ 개체도 같이 소멸되게 해 줘야 한다.

void CHeapCtrl::PostNcDestroy()
{
    delete this;
}

또한 대화상자 내부에서는 CHeapCtrl을 언제나 포인터를 통해 접근해야 한다.

CHeapCtrl *p = DYNAMIC_DOWNCAST(CHeapCtrl, GetDlgItem(IDC_MYCONTROL));

둘째 방법은.. 대화상자이니까 가능한 더 간단한 방법이다. 대화상자 클래스에다가 해당 컨트롤 클래스의 개체를 선언한다. 윈도우 클래스를 등록할 때 WNDCLASS 구조체에다가 윈도우 프로시저는 그냥 쿨하게 DefWindowProc이라고 주면 된다.

CStackCtrl m_wndCustomCtrl;

그리고 윈도우와 C++ 개체를 그냥 이렇게 컨트롤 변수로 연결해 버린다.

void CMyDialog::DoDataExchange(CDataExchange* pDX)
{
    CDialog::DoDataExchange(pDX);
    DDX_Control(pDX, IDC_MYCONTROL, m_wndCustomCtrl);
}

이 경우, CStackCtrl은 스택, 아니 최소한 대화상자 클래스와 동일한 메모리에 생성된다. 그래서 개체를 포인터를 거치지 않고 더 간편하게 접근할 수 있을 뿐만 아니라, 대화상자 같은 윈도우 개체가 사라진 뒤에도 C++ 객체가 갖고 있던 내부 정보에 접근을 할 수 있다. PostNcDestroy 처리가 필요하지 않음은 두 말할 나위도 없고. 여러 모로 더 속 편하다.

윈도우 클래스상으로 이 윈도우는 프로시저가 아무 특수한 일도 하지 않는 DefWindowProc로 지정되어 있지만, DDX_Control 함수는 윈도우 프로시저를 서브클래싱하고 저 윈도우의 핸들을 우리가 준 CStackCtrl에다가 연결해 준다. 그래서 각종 메시지가 발생했을 때 CStackCtrl의 메시지 맵에 등록된 핸들러 함수가 호출되는 것이다.

윈도우 클래스와 MFC 클래스를 연결한 건 시작일 뿐이고 본격적인 코딩은 지금부터이다. 그야말로 산 넘어 산이다.
WM_PAINT와 필요하다면 WM_SIZE를 처리해야 할 것이고, 키보드 포커스를 받는 물건으로 계획을 했으니 WM_GETDLGCODE를 잡아서 스크롤을 위해 최소한 DLGC_WANTARROWS 정도는 되돌려야 할 것이다. 다른 단축키 같은 걸 추가로 인식하려면 DLGC_WANTCHARS도 필요할 테고.

스크롤이 되는 윈도우라면 요즘은 마우스 휠 인식은 필수다. WM_MOUSEWHEEL을 처리해야 한다. SystemParametersInfo(SPI_GETWHEELSCROLLLINES, ...) 을 호출하여 마우스 휠의 움직임 단위를 감지하여 동작할 필요가 있다. 특히 n줄이냐, 아예 페이지 단위냐(WHEEL_PAGESCROLL)를 잘 판단해야 한다.

만드는 컨트롤에 확대/축소 배율이 존재한다면 Ctrl+휠로 배율 조절을 시키는 게 요즘 트렌드다.
또한, 전통적인 세로 스크롤용 마우스 휠뿐만이 아니라 가로 마우스 휠 WM_MOUSEHWHEEL (wheel 앞에 H 추가)이라는 게 있다는 것도 알아 두면 좋다. 마우스 현물보다는 손가락 제스처를 이용한 스크롤 기능이 있는 노트북 터치패드로 생성되는 듯하다.

어디 그 뿐이랴? 표준 인터페이스에는 아예 마우스 휠을 눌러서 구동하는 '자동 스크롤' 모드도 있다. WM_MBUTTONDOWN(마우스 가운데 버튼) 되시겠다. 이건 아까와는 반대로 마우스 실물이 아닌 노트북 터치패드로는 구경하기 힘든 모드이다.

.마우스로 화면을 끌어서 스크롤이 되게 하려면 WM_LBUTTONDOWN, WM_MOUSEMOVE 같은 메시지들을 처리하면 될 것이고.
아, 이런 것보다 더 중요하면서도 스크롤 윈도우에서 상당히 번거로운 작업이 하나 있는데 그건 바로 WM_HSCROLL 및 WM_VSCROLL 메시지이다. 우리의 편견과는 달리,처음에 SetScrollInfo 함수 하나로 전체 스크롤 크기와 영역만 지정해 준다고 해서 그 다음부터 모든 스크롤 처리가 자동으로 되는 게 아니기 때문이다.

스크롤 메시지는 사용자가 스크롤 바의 화살표(한 칸씩)를 눌렀을 때, 스크롤 바를 드래그하고 있을 때, 스크롤 바 옆을 눌렀을 때(한 페이지 씩) 등등의 상황별로 서로 다른 정보가 담긴 채로 전달된다. 그리고 이때 실제로 화면을 얼마만치 스크롤시키고 어떤 처리를 할지는 전적으로 해당 응용 프로그램에 달려 있다. 운영체제가 자동으로 해 주는 일은 없다. 이걸 처리하지 않으면 사용자가 스크롤 바를 눌러도 다른 반응이 발생하지 않는다.

바로 이런 특성 때문에 사용자가 스크롤 바를 끌고 있는 동안 화면이 바로 갱신될지, 혹은 곧장은 아니고 스크롤 바의 드래그가 끝난 뒤에야 화면을 갱신할지 같은 것도 응용 프로그램마다 달리 동작할 수 있다. 물론 반응성이 좋은 프로그램이라면 어지간하면 즉각 화면이 갱신되는 게 좋겠지만 말이다.

그리고 요즘은 화면 캡처 유틸리티들이 스크롤 캡처를 지원하는데, 이 역시 생각보다 꼼수를 써서 구현돼 있다. 화면에다 별도의 표식을 그려 넣은 뒤, 캡처 대상 윈도우에다 스크롤 메시지를 보내고 그 표시가 얼마나 이동했는지를 수동으로 점검한다. 위와 같은 높은 자유도로 인해, 저렇게 하지 않으면 그 스크롤 분량을 정량적으로 알아낼 수 없기 때문이다. (스크롤 바가 이동한 양과 그에 따라 지금 화면이 실제로 이동한 픽셀수 사이의 인과관계)

화면이 스크롤되면 ScrollWindowEx 함수를 호출해서 이미 그려진 화면은 운영체제가 제공하는 스크롤 기능으로 넘기고 새로 칠해져야 하는 최소한의 부분만 새로 칠하는 '평범한 프로그램'이라면.. 저 꼼수가 통한다.
그러나 스크롤 될 때마다 화면을 몽땅 지우고 새로 그리는 프로그램이라면, 저 표식도 지워지기 때문에 스크롤 캡처를 할 수 없게 된다.

이상이다. 윈도우의 스크롤 기능까지 얘기하다 보니 말이 또 길어졌다. 여기에도 뭔가 정량적인 동작 패턴이 분명 있는 듯한데, 그것만 추려내기는 쉽지 않아 보인다.
공통된 기능을 운영체제가 API 함수로든, 공용 컨트롤 윈도우로든 뭘로든 좀 제공을 하지 않는다면 역시나 프로그래머들이 비슷한 기능을 여전히 자체적으로 중복 구현할 수밖에 없을 것이다.

Posted by 사무엘

2014/07/22 08:36 2014/07/22 08:36
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/987

오래 전, 본인은 PE 방식이라고 불리는 32비트 Windows 실행 파일의 내부 구조에 대해 처음 알아 가던 시절에 굉장히 신기해한 사실이 하나 있었다. 그건 바로 파일 내부에 자신이 호출하는 API 함수의 이름이 다 나와 있다는 점이었다. 그러면 이 프로그램이 대충 무슨 기능을 활용하며 만들어졌는지도 얼추 분석이 가능해질 텐데? 16비트 바이너리에는 이런 정보가 존재한 적이 없었다. (오히려 EXE가 윈도우 프로시저 같은 콜백 함수 이름을 노출하고 있었음)

static library가 그러한 것처럼 DLL도 프로그래머가 작성한 클래스/함수가 이름이 그대로 외부로 노출된다. 그 이름을 GetProcAddress에다 전달하면 이름에 해당하는 함수 주소를 얻을 수 있다.

그러나 DLL이 제공하는 심벌들은 이름뿐만 아니라 ordinal이라고 불리는 번호도 제각각 다르게 부여받는다. 그렇기 때문에 ordinal로 주소를 얻는 것 역시 가능하다.
이 ordinal은 index나 number이 아니라 ID에 가까운 개념이다. 반드시 0부터 N까지 조밀하게 분포해 있어야 할 필요가 없으며 1000부터 시작해도 되고 중간에 빈 번호가 있어도 괜찮다.

단, 범위는 16비트로 한정이다. GetProcAddress 함수는 인자의 정수값이 16비트보다 크면 포인터로 간주하여 문자열 검색을 하며, 그보다 작은 값이면 ordinal로 간주하여 숫자 검색을 하는 방식으로 동작한다.
다시 말해 Windows의 DLL은 구조적으로 65536개 이상의 심벌을 export할 수는 없다. 물론 그렇다 해도 이것은 현실적으로 아무런 한계가 없는 것이나 마찬가지다.

16비트 시절에는 DLL의 심벌 탐색이 이름이 아닌 오로지 ordinal 방식만 지원되었던 모양이다. (그럼 GetProcAddress 함수도 인자가 PCSTR이 아니라 그냥 UINT였나?)
문자열을 비교하는 것보다는 숫자를 비교하는 게 속도도 더 빠르고 공간도 더 적게 차지하니 좋다. 그러나 ordinal 방식은 두 가지 단점이 있는데, 먼저 보안이 좀 더 안 좋으며, 그리고 ordinal 관리가 매우 까다롭다는 점이다.

보안 이슈는 쉽게 비유하자면 이렇다.
GetProcAddress("My_unique_function_name")은 내가 직접 만들지 않은 DLL 에서는 성공할 확률이 거의 없다. 그 반면, GetProcAddress((PCSTR)5)는 함수깨나 있다 싶은 아무 어중이떠중이 DLL에서도 어지간해서는 성공하게 된다.

즉, 엉뚱한 DLL을 잘못 불러왔을 때, 이후 동작이 안전하고 깔끔하게 실패하는 게 아니라 그 상황을 사전 감지를 못 하고 나중에 crash로 도질 가능성이 높다는 뜻이다.
물론, 여기서 보안이라는 건 프로그램 실행과 관련된 보안이다. ReadFile, CreateWindow 이라는 함수 이름 대신 #35, #107 식의 암호 같은 ordinal은 프로그램의 역공학을 어렵게 하는 보안(?)은 더 뛰어날 수도 있으니 말이다.

ordinal 관리 문제는 생각보다 더 까다로운 문제이다.
어떤 DLL이 개발이 한창 진행 중이어서 수시로 함수가 추가되거나 삭제된다고 생각해 보자. 그렇더라도 한번 번호가 부여된 함수는 번호가 절대 고정불변이어야만 그 DLL을 사용하는 프로그램과의 하위 호환성이 보장될 수 있다.

같은 함수라도 DLL의 다음 버전에서 ordinal이 달라져 버리면 기존 프로그램은 그 DLL을 사용할 수 없게 된다. 그런데 수백, 수천 개의 ordinal간에 결번이 생기고 영역이 추가 할당되는 것, 과연 번호 관리가 그렇게 호락호락 수월하게 가능할까?

이런 이유로 인해 32비트 이래로 DLL 심벌은 ordinal이 아닌 문자열로 import/export하는 게 관행이 되었다. 16비트 시절에는 DLL을 하나 만들려면 DEF 파일을 무조건 반드시 만들어야 했고 export하는 심벌에 대한 ordinal을 수동으로 기입해야 했다.
그러나 32비트부터는 export하는 심벌만 쭉 기입해 주면, ordinal은 그냥 이름들의 ABC순으로 0부터 N까지 자동으로 생성된다. 별로 중요하지 않은 정보가 됐기 때문이다.

그러나 오늘날에도 ordinal이 전혀 불필요하고 쓸데없느냐 하면 그렇지는 않다.
딱히 컴포넌트화를 지향하지 않고 내가 만드는 프로그램에서나 내부적으로 몰래 쓰는 소형 private DLL이라면, export하는 함수의 이름이 전혀 중요하지 않을 테니 그냥 이름을 노출할 필요도 없이 ordinal 직통을 쓰면 된다. 하는 일이 붙박이로 정해져 있고 앞으로 프로토타입이 바뀔 일이 절대로 없는 물건이라면 금상첨화. 훅 프로시저 DLL 같은 게 좋은 예 되겠다.

혹은, 심벌 개수가 수천~수만 개로 너무 많은 대형 DLL의 경우, 로딩 시간을 조금이라도 단축하기 위해서 의도적으로 이름 대신 ordinal 기반 로딩 방식을 고집하기도 한다.
당장 MFC 라이브러리, 그리고 MS Office가 내부적으로 사용하는 공용 라이브러리인 mso.dll도 전부 ordinal 기반이다. MFC를 DLL 링크한 프로그램이라고 해서 export 섹션에 CWnd, CWinApp 이런 클래스 심벌들이 주룩 노출돼 있는 거 아니다.

심벌을 이름이 아닌 ordinal로 식별하게 DLL과 import library를 만들려면 빌드 시에 DEF 파일을 만들어서 심벌에 대한 특성과 ordinal 번호를 수동으로 지정해 줘야 한다.
그런데 C가 아닌 C++ 스타일의 클래스나 함수를 ordinal로 지정하는 법은 잘 모르겠다. 비주얼 C++ 스타일로 복잡하게 decorate된 명칭들을 일일이 다 열거하면서 @번호 NONAME 속성을 다 줬으려나? 그것도 보통일이 아닐 텐데.

Posted by 사무엘

2014/06/12 08:33 2014/06/12 08:33
, ,
Response
No Trackback , 3 Comments
RSS :
http://moogi.new21.org/tc/rss/response/973

예전에 본인은 windowless 리치 에디트 컨트롤에 대해서 글을 쓴 적이 있었는데, 이번에는 눈에 보이는 컨트롤에 대해서 다루도록 하겠다. 난 일종의 텍스트 에디터 윈도우를 처음부터 끝까지 만들어 본 사람이다 보니, 이런 세부적인 디테일에 눈길과 관심이 간다.
여러분은 PC 사용자로서, 혹은 더 전문적인 프로그래머로서 Windows 운영체제의 에디트 컨트롤에 대해서 얼마나 알고 계신가? 그 이름도 유명한 메모장이 얘를 기반으로 동작하는 걸로 잘 알려져 있다.

얘는 서식이 없는 plain 소규모 텍스트에 대한 아주 기본적인 입력 기능만 제공하라고 만들어진 물건이다.
그래서 전문적인 에디터들이 내부적으로 줄 단위 연결 리스트 자료구조를 사용하는 것과 달리, 얘는 단일 배열 버퍼 기반이라는 게 가장 큰 특징 되겠다. 복잡한 메모리 관리 메커니즘이 전혀 없이 텍스트는 전체가 커다란 배열이며, 삽입이나 삭제는 진짜 직관적이고 단순하게 밀고 당기고 전체 메모리를 재할당하는 식으로 행해진다.

Windows 9x 시절까지만 해도 이 에디트 컨트롤은 64KB가 좀 덜 되는, 6만여 바이트 이상의 텍스트는 불러들일 수 없었다. 아무리 16비트 시절의 잔재라지만 이건 말도 안 되는 제약이었다.

오늘날의 운영체제에서야 그 정도의 막장 제약은 존재하지 않는다. 그래도 단일 버퍼 기반이며 근본적으로 대용량 텍스트를 다루는 데 최적화되지 않은 구조인 것은 변함없다. 그렇기 때문에 메모장에서 수 MB 이상의 파일을 불러들이고 편집하는 건, 불가능하지만 않을 뿐 여전히 무리다. 다른 프로그램을 써야 한다. 가령, 확장판인 리치 에디트 컨트롤은 연결 리스트 기반이며 에디트 컨트롤과 같은 제약이 없다.

간단한 물건답게 실행 취소(undo)는 직전의 동작 딱 하나만을 취소하거나 도로 철회하는 게 가능하다. 요즘 유행인 다단계는 지원하지 않는다.
텍스트 전체를 왼쪽뿐만 아니라 가운데나 오른쪽으로 정렬하여 출력하기, 숫자만 받아들이기, 알파벳은 대문자나 소문자 한 형태로만 받아들이기, 입력되는 텍스트를 '암호'로 처리하여 화면에 숨기기 같은 아기자기한 옵션이 있다. 최대 글자 수 제한을 걸 수도 있다.

얘의 동작 방식을 사소하게 바꿔서 배경이나 글자색을 바꾸고, 특정 문자는 안 받아들이게 하고, 문자열을 자동 완성한다거나 우편번호/시리얼 번호처럼 특정 형식을 만족하는 문자열만 입력되게 하는 것은... 아주 전형적인 Windows 프로그래밍 주제였다. 윈도우 프로시저를 서브클래싱하면 된다.
단일 버퍼라는 특성상 에디트 컨트롤의 메모리 핸들에 곧장 접근하여 조작하는 API도 있긴 하나, 실제로 사용되는 경우는 거의 없다. 사용이 권장되지도 않는다.

multi-line 모드의 경우 줄 바꿈 문자는 오로지 정확하게 \r\n만 지원하는 걸로 잘 알려져 있다. 그래서 유닉스 계열의 \n 텍스트를 불러와 보면 \n 문자가 실제로 보이고 줄 바꿈 처리가 되지 않아 텍스트를 제대로 볼 수 없다.
이 역시 리치 에디트 컨트롤과는 다른 점이다. 텍스트를 불러들이는 과정에서 딱히 줄 바꿈 처리를 전혀 하지 않고 normalize도 전혀 하지 않는 단순함을 추구했기 때문이랄까?

그리고 Windows GUI 프로그래밍을 좀 해 본 사람이라면 경험적으로 아는 중요한 특징이 하나 있다.
multi-line 모드에서 word-wrap(자동 줄바꿈) 옵션은 컨트롤이 생성될 때 한번 지정되고 난 뒤부터는 변경되지 않는다. 스타일을 바꿔도 바뀐 대로 동작하지 않는다.

그렇기 때문에 당장 메모장에도 있는 '자동 줄 바꿈' 옵션은 에디트 컨트롤을 파괴했다가 다시 생성하는 방식으로 구현되어 있다! 물론 창이 갖고 있던 내용을 따로 보관했다가 다시 가져와야 할 테고. 이건 Windows 95 이래로 지금까지 동작 방식이 시종일관 변함없다.

심지어는 닷넷이 제공하는 에디트 컨트롤의 관련 속성조차도 내부 구현은 창을 다시 생성하는 걸 Spy++ 같은 프로그램으로 확인할 수 있다고 한다. 핸들값이 바뀌니까 말이다.
응용 프로그램에다가 꼼수를 추가하면 추가했지, 에디트 컨트롤은 절대로 건드리지 않기로 작정하고 봉인을 시킨 모양이다. 그러니 앞으로 Windows 9, 10이 나오더라도 에디트 컨트롤이 연결 리스트 기반으로 바뀐다거나 하는 일은 없을 것으로 보인다.

한 에디트 창을 single line으로 쓰느냐 multi line으로 쓰느냐 같은 건 프로그램 실행 중에 동적으로 바뀔 일이 거의 없다. 그러니 그런 건 고정불변으로 둘 만도 하다.
하지만 multi line에서 자동 줄 바꿈 여부는 텍스트 에디터를 만드는 사람이라면 누구나 공감하듯이 동적으로 바뀔 여지가 있다. 그런데 그걸 불변으로 굳혔다는 것은, 에디트 컨트롤을 갖고 뭔가 진지하게 대용량 텍스트 에디터를 만드는 건 설계 차원에서 정말로 고려하지 않았다는 걸 의미한다.

뭐, 단순하긴 해도 운영체제가 원초적으로 제공하는 텍스트 입출력 기술의 혜택은 그대로 받는지라, 오늘날 에디트 컨트롤은 마냥 단순하기만 한 것도 아니다. Uniscribe를 사용하여 나름 아랍어 같은 complex script의 입력과 위치 계산도 기본적인 건 그럭저럭 처리해 낸다. 우클릭했을 때 나타나는 메뉴를 보면, 텍스트의 기본 진행 방향을 L2R로 할지 R2L로 할지 지정하는 옵션이 있다.

마지막으로 입력 쪽을 살펴보면, Windows에는 TSF라는 입력 기술이 도입됐는데, 에디트 컨트롤은 기본적으로는 얘를 완벽하게 지원하지 않는다. 문자 입력기가 모든 텍스트를 자유자재로 조작할 수 있는 TSF 프로토콜은 역시나 리치 에디트 컨트롤만이 Windows XP sp1 시절부터 도입했다. 비록 이것도 모든 리치 에디트에 자동 적용되는 건 아니고, 최신 버전의 컨트롤을 사용하고 전용 메시지를 사용해야 하지만 말이다. (리치 에디트는 일반 에디트와는 달리 업그레이드가 쭉쭉 되어 온 대신, 버전 내력이 꽤 꼬여 있다..)

그렇기 때문에 이 에디트 컨트롤을 기준으로는 TSF와 관련된 두 가지 접근이 있어 왔다.
첫째는, 에디트 컨트롤을 사용하는 응용 프로그램이 윈도우 프로시저를 서브클래싱하여, 적절한 시기에 TSF API를 호출하고 TSF 인터페이스를 직접 구현하는 것이다. TSF로부터 특정 문자열을 읽거나 쓰라는 요청을 받으면, 그걸 EM_SETSEL, EM_GETSEL 메시지 같은 걸로 요청해서 결과를 되돌리면 되는 거다. 에디트 컨트롤은 단일 버퍼 기반 구조여서 텍스트 오프셋 계산은 다행히 훨씬 쉬우니까 말이다.

물론 이것은 에디트 컨트롤이 native하게 TSF를 지원하는 것보다는 효율이 훨씬 떨어지며, 오동작의 여지도 많다. 그냥 이런 발상도 가능하다는 걸 시연해 보이는 데 의미가 있을 뿐. 지금도 있는지 모르겠는데 과거에 TsfAPP라는 예제 프로그램이 바로 이걸 구현한 프로그램이었는데, 버그도 많았다.

다음 둘째로는, Windows Vista부터는 응용 프로그램이 아닌 그 밑에서 돌아가는 IME가 특수하게 요청할 경우에 한하여 에디트 컨트롤을 TSF 지원 모드로 바꿔 동작시키는 기능이 도입되었다. <날개셋> 한글 입력기는 TSF 인터페이스가 지원될 경우 기능 활용의 폭이 훨씬 더 넓어지는 관계로, 이 기능을 실제로 사용하는 옵션이 있다.
이 모드를 사용하면 한글 입력 중일 때 cursor가 깜빡이는 네모가 아니라 일반 블록 색깔로 바뀐다. 그리고 에디트 컨트롤을 특수하게 조작하는 프로그램에서 잠재적으로 오동작이 발생할 가능성도 생긴다.

이 TSF 확장 기능은 에디트 컨트롤뿐만 아니라, legacy 리치 에디트 컨트롤(=자체적으로 TSF를 지원하지는 않는 구버전)과 IE 웹브라우저 엔진이 제공하는 에디트 컨트롤에도 그대로 적용된다. 즉, 마이크로소프트가 구현한 표준 에디트 관련 컨트롤에는 거의 다 적용되므로 사용의 폭이 넓은 편이다.

이상이다.
옛날에 MSDN에서 Kyle Marsh 아저씨가 16비트 Windows 3.x 기준으로 에디트 컨트롤의 모든 기술적 디테일을 미주알고주알 늘어 놓은 글을 본 기억이 난다. 얘는 동작 방식과 규격이 20년 전이나 지금이나 완전히 굳어져 버려서 호환성 유지 차원에서 더 바꿀 수가 없고, MS가 신기술 투자는 리치 에디트에다가 집중적으로 한다고 보면 얼추 맞겠다. 그래서 리치 에디트에는 드래그 & 드롭도 있고, 하이퍼링크를 밑줄로 표시해 주는 기능도 있지만, 일반 에디트는 그런 거 없다. Ctrl+Bksp를 눌렀을 때 단어 단위로 지우는 기능 역시 리치 에디트급 이상에만 있다.

아울러, 일반 에디트는 우클릭하면 표준 메뉴가 나타나는 반면, 리치 에디트는 우클릭했을 때 제공되는 기본 메뉴가 없고 그걸 전적으로 사용자의 customization에 맡기고 있다는 차이가 있다.

Posted by 사무엘

2014/05/29 08:40 2014/05/29 08:40
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/968

요즘 운영체제들에 GUI 셸이 없는 물건은 없고, GUI에는 그림보다도 먼저 문자를 찍는 기능이 반드시 필요하다. 옛날에는 그런 출력 기능이 겨우 비트맵 글꼴밖에 지원되지 않았지만, 오늘날은 트루타입(TTF)이라고 불리는 규격의 윤곽선 글꼴이 세계를 평정한 지 오래다(오픈타입은 TTF의 superset에 해당함). 심지어 재래식 비트맵 글꼴이 필요하다 해도 일단 TTF 방식으로 저장하고서 출력한다.

게임처럼 완전 독자적인 GUI 노선을 가는 프로그램이 아닌 이상, 거의 모든 응용 프로그램들은 운영체제가 제공하고 운영체제에 기본으로 설정되어 있는 글꼴만을 사용하여 글자를 출력한다. 새로운 글꼴을 받아서 설치하는 건 사용자의 몫이다. 그러나 가끔은 응용 프로그램이 직접 글꼴을 설치해서 써야 하는 경우도 있다.

워드 프로세서 같은 오피스 프로그램이라면 운영체제 전체에 새로운 글꼴을 번들로 제공할 수 있다. 이건 global한 글꼴 추가이다. 한편, 자기 프로그램 내부에서만 특수한 custom 글꼴을 추가해서 쓰는 건 local (private)한 글꼴 추가이다.

윤곽선 글꼴 출력 엔진은 힌팅과 캐싱 기능이 곁들여진 일종의 고성능 범용 단색 벡터 그래픽 엔진이다. 그렇기 때문에 다른 프로그램들에 노출되지 않는 local 글꼴도 용도가 매우 다양하다. 수식이나 악보에서 쓰이는 비문자 기호를 찍는 건 물론이고, ‘입꼴 워드’처럼 자기만 사용하는 특수한 문자를 찍을 때도 전용 글꼴을 활용하면 된다.

당장 운영체제 자신도 이걸 잘 활용하고 있다. 테마가 도입되기 전에 Windows 창에 달린 사각형 모양의 최소화(_)/최대화/닫기(X) 그림은 글꼴 출력이고, Visual Studio 같은 데서 창을 도킹시키는 주사기/핀 모양의 그림도 글꼴이다. 아마 본인이 옛날에 블로그 글에서 언급한 적이 있을 것이다.

Windows 8은 부팅 시나 작업 중일 때 다섯 개의 구슬이 동그란 궤도를 그리면서 슝슝 돌아가는 애니메이션이 출력되는데, 이것도 애니메이션 GIF나 플래시 같은 기술이 아니라 글꼴 출력이다~! 구슬이 싹 들어갔다가 나오기도 하는 게 은근히 복잡하며, 이 애니메이션은 무려 100수십 프레임에 달한다. 유니코드 PUA 영역에다 미리 계산된 각 프레임의 모양을 그려 넣은 뒤, 그 글자를 순서대로 찍은 것이다. 놀랍지 않은가?

보급이 아닌 싸제 글꼴의 등록 및 해제를 위해 Windows는 AddFontResource와 RemoveFontResource라는 간단한 함수를 제공한다. 인자로는 등록하거나 해제하고 싶은 글꼴 파일의 경로만 달랑 주면 된다. '일단은' 말이다. 그러다 나중에는--Windows 9x 라인 말고 2000에서부터-- 두 종류의 함수가 더 추가되었다.

첫째, 바로 저 두 함수의 이름 끝에다 Ex가 붙은 버전이다. Ex 버전은 인자를 두 개 더 받는데, 하나는 아직 reserved 상태니 별 의미가 없고, 다른 하나는 사소한 비트 플래그들이다. 등록하는 이 글꼴을 시스템 전체가 아니라 우리 프로세스 내부에서만 사용하게 하는 FR_PRIVATE 옵션, 그리고 글꼴의 접근 가능 여부를 떠나서 일단 이게 EnumFontFamilies(Ex)에서 집계가 되지 않게 하는 FR_NOT_ENUM 옵션이다. 즉, 이 글꼴의 독특한 이름을 아는 프로그램만 이 글꼴을 사용할 수 있게 되는 것이다.

그리고 둘째로, 글꼴을 파일 이름이 아니라 아예 메모리 상의 데이터로 받는 AddFontMemResourceEx도 추가되었다. 이 함수로 추가되는 글꼴은 파일로 실체가 존재하지도 않고 특정 프로세스의 주소 공간에 매여 있으므로 극도로 private하며, FR_PRIVATE|FR_NOT_ENUM 속성이 언제나 선택의 여지 없이 붙는다.

요컨대 글꼴을 좀 더 가볍게 private 형태로 추가하는 기능은 Windows 2000에 와서야 새로 도입된 셈이다. 여담이지만, 이것 말고도 Windows 2000은 9x/NT4 시절에 비해 프로그램의 국제화 수준이 크게 강화된 첫 버전인지라 다국어 IME와 complex script를 포함해 글꼴을 저수준에서 조작하는 API들도 크게 추가되었다.
트루타입 글꼴의 테이블 데이터를 있는 그대로 뽑아 내는 GetFontData라든가, 글꼴이 지원하는 문자 집합을 유니코드 번호로 얻어 오는 GetFontUnicodeRanges도 이때의 산물임.

뭐 그건 그렇고 다시 글꼴 등록 얘기로 돌아오자면..
local/private 말고 전통적인 global한 글꼴 추가도 여전히 필요한 절차이다.
그런데 문제는 이게 사실 함수 호출만 한다고 완전히 끝나는 게 아니라는 것이다. 절차가 생각보다 굉장히 지저분하며 문서화가 제대로 돼 있지 않다.

1. global 글꼴은 Windows\Fonts 디렉터리에 있어야 한다. 결국 파일을 복사해 넣어야 하는데, 이 디렉터리에 read가 아닌 write를 하려면 관리자 권한이 필요하다.

2. Fonts 디렉터리에 복사된 파일을 상대로 AddFontResource(Ex) 함수를 호출한다.

3. 이 글꼴이 다음 부팅 때에도 제대로 인식되게 하려면, 글꼴 리스트를 레지스트리에다가도 등록해 줘야 한다. 위치는 다음과 같다.
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts
과거 9x 시절에는 Windows NT 대신 그냥 Windows이고. 저 레지스트리도 read가 아닌 write를 하려면 관리자 권한이 필요하다.


레지스트리에 등록하는 형식은 대충 보면 짐작할 수 있지만, 문제는 이 뻔한 패턴의 작업을 자동으로 대행해 주는 함수가 없다는 것이다. 등록하고자 하는 TTF 파일을 직접 파싱해서 name 테이블에 있는 이름을 얻어 와야 하나? ActiveX 컨트롤을 등록해 주는 regsvr32 유틸리티처럼 글꼴을 명령 프롬프트에서 바로 설치하거나 제거하는 유틸리티도 운영체제에 있어야 할 것 같다.

옛날에는 트루타입 글꼴을 설치하려면 CreateScalableFontResource 같은 이상한 함수도 호출해서 ttf에 대응하는 *.fot 파일이라는 걸 만들어야 했던 모양이다. 완전 불편하기 그지없는데 지금은 그럴 필요까지는 없는 듯. 20년 전 엄청 옛날의 Windows 3.1 시절에는 ttf/fot 파일 쌍이 필요했지만 95 이후로는 그런 건 없다.

반대로 이 글꼴을 제거하려면 먼저 RemoveFontResource(Ex)를 호출해 주고, 이게 성공하면 레지스트리 제거와 파일 제거를 수행하면 된다.
그런데 Windows는 파일 자체를 가상 메모리 주소 공간에다 직통으로 대응해서 쓰는(MMF) 걸 좋아하는 운영체제인지라, 시스템 공용 파일을 지우기가 더럽게 까다로운 운영체제다. 글꼴도 예외가 아니어서 파일 삭제는 access deny 에러가 뜨면서 잘 되지 않을 수도 있다. 이때는 MoveFileEx(file, NULL, MOVEFILE_DELAY_UNTIL_REBOOT)을 줘서 다음 재부팅 때라도 파일이 삭제되게 플래그를 주면 될 것이다.

local과는 달리 global 글꼴 등록과 삭제는 이렇게 번거로운데, 게다가 관리자 권한까지 필요하니 더욱 번거롭다.
관리자 권한은 한 프로세스가 필요한 때만 잠시 사용자의 동의 하에 취득했다가 반납하는 게 없다. 애초에 자기 프로그램을 더 높은 권한으로 재실행해야 한다.
잠시 다음 상황을 생각해 보자.

  •  어떤 일을 하는 동안에도 GUI는 매끄럽게 반응하고, 작업이 취소 가능하거나 진행 상황 같은 걸 별도로 표시해야 하는 경우: 작업 부분을 별도의 스레드로 떼어 내야 한다.
  • 다른 프로세스를 훅킹해서 정보를 얻어 오거나 실행을 조작해야 하는 경우: 훅 프로시저는 반드시 별도의 DLL로 만들어야 한다. DLL은 32비트와 64비트를 모두 신경 써서 만들어야 하니 더욱 번거롭다.

그리고,

  • 평소에는 일반 모드로 실행되지만, 잠시 관리자 권한을 얻어 와서 민감한 디렉터리나 레지스트리의 내용을 변경해야 하는 경우: 그 부분만 별도의 EXE(프로세스)로 만들어서 실행해야 한다. 물론 나 자신을 특수한 인자를 주고 재실행하는 것도 괜찮다.

참고로 권한이 낮은 프로그램은 권한이 높은 프로그램에다 메시지를 못 보낸다. 그러니 프로그램 간의 통신 메커니즘도 잘 생각해 봐야 한다. =_=;;

어지간하면 골치아플 일 없이 단일 모듈, 단일 스레드만으로 모든 일을 처리하고 싶은데 그럴 수가 없는 상황이 이렇게 요약되었다.
프로세스, 스레드, DLL이 시나리오별로 다 등장했다. 글꼴 설치는 '프로세스' 분리가 필요한 작업인 것이다.

Posted by 사무엘

2014/05/26 08:23 2014/05/26 08:23
, , ,
Response
No Trackback , 3 Comments
RSS :
http://moogi.new21.org/tc/rss/response/967

GetMessage는 Windows 프로그래밍에서 윈도우 message loop을 구현할 때 쓰이는 함수이다.
이 함수는 명목상 리턴값이 BOOL이며, 평소에는 nonzero를 되돌리다가 WM_QUIT가 접수되어서 응용 프로그램이 종료되어야 할 때 FALSE가 된다.

그러나 이 함수의 리턴값이 이것이 전부가 아니다.
정상적으로 한 메시지를 끄집어 왔을 때는 nonzero이긴 한데 양수이며, argument가 올바르지 않다거나 해서 함수의 실행 자체가 실패했을 때는 음수 -1을 되돌린다.

그렇기 때문에 메시지 loop을 while( GetMessage(&msg, NULL, 0, 0)) { }  이런 식으로 구현하면, 메시지를 아예 가져오질 못했는데도 loop의 조건이 만족되며 프로그램은 무한 루프에 빠진다.
!=0으로는 불충분하니, 반드시 while( GetMessage(&msg, NULL, 0, 0) >0)이라고... >0을 명시해야 한다.

(1) 이 함수 말고도 타입이 BOOL인데 사실은 TRUE/FALSE라는 순수한 흑백 논리값 말고 다른 의미 있는 값도 되돌리는 페이크 BOOL 함수가 또 있었던 것 같으나, 당장은 기억이 안 난다. 이런 지저분한 이슈도 있고, 또 Windows API의 기반 언어인 C가 어지간한 건 그냥 machine word 정수로 처리하는 관행이 있기도 하니(문자 상수의 크기도 char이 아닌 int!), 프로그래밍에서도 BOOL은 C++의 bool이 아니라 그냥 int에다 대응시켜 놓은 것 같다.

(2) COM에도 이와 비슷한 얘깃거리가 있다. HRESULT는 원래 0과 양수가 '성공'을 나타내고, 음수가 실패를 나타낸다. 하지만 현실에서는 대부분 그냥 hr==S_OK (0) 여부만으로 성공/실패 여부를 판단한다.
거의 모든 COM 인터페이스 함수들은 실행이 성공했을 때 어차피 S_OK라는 단일한 값만을 되돌리기 때문에 이것이 현실에서 당장 크게 문제가 되지는 않는다. 그러나 원칙적으로는 어지간해서는 hr==S_OK를 쓸 곳에 SUCCEEDED(hr)을 써야 한다. 이것은 hr>=0 여부를 체크하는 매크로이다. hr!=S_OK를 대신해서는 FAILED(hr)이 바람직하고 말이다.

음수도 아니고 0도 아닌 대표적인 리턴값은 S_FALSE이다. 이것은 해당 함수가 의미 있는 동작을 하지는 않았지만 어쨌든 오류가 발생했거나 실패한 상황도 아닐 때 돌아온다. 가령, 뭔가 객체를 enum하고 있는데, 포인터가 이미 끝에 도달해서 더 fetch할 게 하나도 없으면 보통 &ulFetched는 0이 돌아오고 함수 리턴값은 S_FALSE가 된다. 하나라도 fetch된 게 있으면 S_OK이고 말이다.
따라서 이 경우, loop의 종료 조건을 지정하려면 SUCCEEDED와 더불어 fetch된 개수도 체크해야 한다.

(3) 다시 GetMessage 얘기로 돌아온다.
얘는 메시지를 수집하는 윈도우, 그리고 필터링할 메시지의 최소값과 최대값을 인자로 받는다. 하지만 PeekMessage도 아니고 GetMessage에다가 뭔가 동작의 범위를 제한하는 유의미한 값을 지정하는 것은 사실상 거의 쓸데없는 짓이다. 언제나 NULL, 0, 0을 하는 게 맞다. (레이몬드 챈 선생도 인증한 사실임)

이 함수는 뭔가 메시지를 얻을 때까지 실행이 끝나지 않고 계속 기다린다. 어떤 GUI 프로그램이 실행되면 굳이 자신이 아니어도 그 스레드 소속으로 남이 생성한 각종 잡다한 윈도우가 붙는다. 이들 윈도우도 메시지 큐로부터 메시지를 받아야 하는데, GetMessage에다가 필터링을 걸면 해당 윈도우는 메시지를 받지 못하며 그 동안 우리 프로그램도 실행되지 못하게 된다. 쉽게 말해 deadlock에 빠진다.

따라서 아무 윈도우로 전달된 아무 메시지라도 일단은 받아서 윈도우 프로시저로 Dispatch를 시켜야 한다. 정 특정 메시지만 필터링을 하고 싶다면 아까도 말했듯이 PeekMessage를 쓰는 게 훨씬 더 안전하고 바람직하다. 얘는 그래도 한 번만 체크 후 실행이 곧장 끝나기라도 하니까 말이다.

Posted by 사무엘

2014/04/30 08:31 2014/04/30 08:31
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/957

Windows API에서 LoadLibrary는 잘 알다시피 자기 프로세스 공간에다가 다른 DLL을 추가로 매핑시키는 매우 중요한 함수이다. 코드 실행이 아니라 단순 리소스 추출만이 목적인 경우, EXE 내지 현재의 기계가 지원하지 않는 다른 아키텍처로 빌드된 모듈을 열 수도 있다.
Windows에서 어떤 모듈(EXE/DLL)은 타 모듈을 말 그대로 자신이 실행되는 로드타임(load-time) 때 곧장 불러와서 여타 DLL에 대한 함수 링킹을 할 수 있으며, 반대로 실행 중인 런타임(run-time) 때 동적으로 할 수도 있다.

로드타임 로딩은 간편하긴 하지만 모듈 파일 이름 이상으로 불러들이고자 하는 디렉터리 위치를 세밀하게 제어할 수 없다. 그리고 모듈이 단 하나라도 존재하지 않거나 해당 파일에 함수 심벌이 단 하나라도 존재하지 않으면 프로그램이 전혀 실행되지 않는다. 그런 예외 상황이 발생했을 때의 제어도 사용자가 전혀 할 수 없기 때문에 프로그램의 유연성 내지 융통성이 떨어진다는 단점이 있다. 내 프로그램의 로드 자체가 실패해 버리니 말이다.

그래서 새 운영체제에 들어있는 API 함수를 쓰긴 하는데, 그 함수가 존재하는지 체크를 미리 해서 구형 운영체제와의 호환성도 유지하기 위해서는.. LoadLibrary와 GetProcAddress를 이용하는 런타임 로딩 기법을 써야 한다. 로드타임 로딩이라면 운영체제가 그 기능을 내부적으로 자동으로 제공해 줬을 텐데 런타임 로딩은 함수 포인터를 수동으로 관리해야 하니 좀 번거롭긴 하다.

이럴 때, 비주얼 C++ 6때부터 도입된 delay-loading은 로드타임 로딩과 런타임 로딩의 장점을 취합한 굉장히 괜찮은 대안이 될 수 있다. COM은 런타임 로딩을 규격화된 인터페이스라는 형태로 좀 더 깔끔하게 정형화한 바이너리 표준일 테고 말이다.

DLL을 불러올 때, LoadLibrary에다가 파일의 절대 경로를 주지 않고 파일 이름만 달랑 넘겨 주면 이 함수는 프로그램이 실행된 current 디렉터리, 프로그램이 존재하는 디렉터리, 운영체제의 시스템 디렉터리, PATH에 등록된 디렉터리 등 다양한 순서대로 파일을 탐색한다. 로드타임 로딩 때도 대상 DLL은 이런 순서대로 탐색된다.

이것은 프로그램의 동작에 유연성을 부여하고 한 DLL을 여러 프로그램들 사이에서 최대한 쉽게 공유가 되게 하기 위함이나, 오늘날에 와서는 이런 정책은 보안에 악영향을 끼치게 되었다. 이름만 같고 우선순위가 더 높은 디렉터리에 존재하는 악의적인 DLL이 잘못 선택되어 로딩될 수 있기 때문이다. 또한 하위 호환성이 지켜지지 않는 시스템 DLL이 덮어써짐으로써 DLL hell 같은 고전적인 문제도 야기될 수 있다. 갈수록 난잡해지고 포화 상태로 치닫는 시스템 디렉터리는 큰 골칫덩어리이다.

LoadLibrary의 디렉터리 탐색 방식 자체를 바꿀 수는 없다. 그 방식에 의존하여 동작하는 수많은 기존 프로그램들과의 호환성을 유지해야 하기 때문이다. 그렇기 때문에 오늘날 마소에서는, 새로 개발되는 프로그램에서는 LoadLibrary를 호출할 때 저런 알고리즘에 의존하지 말고 반드시 DLL의 전체 경로를 명시해 줄 것을 권고하고 있다. 또한, 전면 금지하는 건 불가능하겠지만, 운영체제의 시스템 디렉터리에다가는 가능한 한 자기 싸제 DLL을 집어넣지 말 것을 권고한다.

일례로, 예전에 qt 라이브러리를 DLL 링크한 프로그램을 돌려 봤다. 이 DLL은 운영체제의 시스템 디렉터리에 있는 것도 아니었는데 어떻게 DLL을 찾는지 궁금했는데... 에구, 그냥 PATH 지정이더라. 운영체제의 환경변수를 대놓고 바꿔 버리는 건 굉장히 안 좋은 방법인데 어쩔 수 없다. 이제 윈도 XP가 나온 지도 10년이 넘었으니 WinSXS 매니페스트 방식이라도 써야 하지 않나 싶다.

요컨대 Windows는 (1) 제3자 응용 프로그램이 자기끼리 공유하는 공용 DLL을 손쉽게 찾아 로딩하는 법, 그리고 (2) 운영체제의 시스템 DLL을 보안 문제 없이 간편히 로딩하는 법을 완전히 이원화하여 체계적으로 제공할 필요가 있다. 확장 버전인 LoadLibraryEx 함수에라도 그런 기능을 좀 추가할 수 없나 궁금하다.

(1)의 경우 아까도 말했듯이 delay-loading이나 WinSXS가 그럭저럭 해결책이다. 이게 중요한 문제이기 때문에 윈도 XP부터는 SetDllDirectory 함수가 추가되긴 했으나, 이것은 로드타임 로딩을 제어할 수 없기 때문에 여전히 근본적인 해결책이 아니다.

(2)는 known DLL 리스트라는 게 레지스트리에 존재한다. 그래서 "kernel32", "user32" 같은 건 DLL계의 예약어(reserved word)처럼 돼서 주변에 kernel32.dll, user32.dll 같은 동일 명칭의 사칭 파일이 있다 해도 언제나 운영체제의 시스템 디렉터리에 있는 놈이 로딩된다는 게 보장된다.

그러나 개인적인 생각은 그걸 API 차원에서 좀 더 엄밀하게 했으면 좋겠다. 아까 말했듯이, API 하위 호환성 유지를 위해 운영체제의 시스템 DLL을 수동으로 로딩하는 경우는 빈번하게 존재한다. 그리고 운영체제가 버전업되면서 새로운 시스템 DLL이 앞으로 얼마나 더 추가될지 모른다. 그러니 오로지 시스템 DLL만 로딩하고 다른 싸제 DLL은 유사품이 있다 해도 거부하는 명령이 있었으면 좋겠다.

앗싸리 시스템 DLL까지 몽땅 시스템 디렉터리 전체 경로를 지정해 줘서 로딩시켜 보기도 했다. 그런데 이렇게 했더니 한 가지 복병에 걸리는 게 있다. 바로 comctl32.dll이다.
얘는 WinSXS 매니페스트로 넘어가서 길고 이상한 다른 디렉터리에 있는 놈이 로딩되기 때문이다. 이걸 무시하고 운영체제 시스템 디렉터리에 있는 놈을 로딩하면 TaskDialog처럼 공용 컨트롤 6.0 이상에서만 지원되는 기능들을 사용할 수 없다.

그러니 시스템 DLL은 경로 다 생략하고 그냥 "comctl32", "kernel32" 이렇게만 로딩하는 게 정답인 듯하다.
문득 스프레드 시트 프로그램인 엑셀이 생각난다. 엑셀은 구조적인 이유로 인해 이름이 동일하고 서로 다른 디렉터리에 존재하는 여러 파일을 동시에 열 수 없다. 수식에서 다른 워크시트 파일을 참조할 때 디렉터리 대신 오로지 파일 이름만을 토대로 탐색을 하기 때문이다.

Posted by 사무엘

2014/04/27 08:25 2014/04/27 08:25
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/956

Windows API에는 FreeLibraryAndExitThread라는 함수가 있다.
얘가 하는 일은 이름이 암시하는 바와 같다. 주어진 모듈에 대해 FreeLibrary를 한 뒤 ExitThread를 호출하여 지금 실행 중인 자기 스레드를 종료한다.

어지간하면 응용 프로그램이 두 함수를 차례로 직접 호출해 주면 되며, 둘을 나란히 호출해야 할 일 자체도 실무에서는 매우 드물다. DLL을 제거하는 건 대체로 응용 프로그램이 종료될 때 같이 행해지지, 특정 스레드의 실행이 끝나자마자 칼같이 행해지는 일은 없기 때문이다. 그런데 왜 이런 함수가 정식으로 별도로 제공되는 걸까(Windows NT 3.1 첫 버전은 아니고 3.5에서 추가됨)? 다 이유가 있기 때문이다.

어떤 DLL이 별도의 스레드를 생성해서 자신의 코드를 동시에 실행하고, 실행 후에는 자기 자신을 스스로 깔끔하게 제거하여 사라지게 설계되어 있다고 치자. 즉, DLL을 로딩한 모듈이 그걸 수동으로 따로 해제하는 게 아니라, 그 DLL이 자기 임무를 다한 후 스스로 사라진다는 것이다. 이건 비록 흔한 디자인 형태는 아니지만 말이다.

이 경우, DLL의 코드가 자신에 대해서 FreeLibrary(hMyDLL)을 함부로 호출해 버려서는 곤란하다. 그러면 자기 스레드가 활동하던 모듈이 메모리에서 사라져 버리기 때문에 그 다음에 실행될 스레드 실행 종료 부분에 해당하는 코드(return 0 내지 ExitThread)까지 사라진다. 스레드는 정상적으로 종료되지 못하고 곧장 access violation이 발생한다.

그렇다고 ExitThread를 먼저 호출하면? 이 DLL을 실행하는 주체인 스레드의 실행이 끝나 버리기 때문에 FreeLibrary가 호출될 기회가 없다. 마치 실행 중인 EXE 자신을 제거하는 코드를 실행하는 것과 비슷한 맥락의 딜레마가 발생한다.

모듈(공간) 해제와 스레드(시간) 종료가 동시에 행해져야 할 경우, 이것은 운영체제가 알아서 동시에 수행해 줘야 한다.
FreeLibraryAndExitThread를 호출한 경우, 내부적으로 FreeLibrary가 됐더라도 그 다음으로 ExitThread를 호출하는 코드는 그 DLL이 아니라 운영체제가 책임을 지기 때문에 안전하다.

한편, Windows에서는 EXE나 DLL의 로딩이 파일 자체를 가상 메모리 주소에다 곧장 연결하는 memory-mapped file 기법으로 행해진다. 그렇기 때문에 실행 중인 프로그램 파일이 중간에 지워지는 것은 운영체제가 허용하지 않는다. 즉 DLL과 스레드 문제로 비유하자면 FreeLibrary 단계에서 이미 실패한다는 것이다.

그렇다고 자신을 먼저 종료해 버리면 자기 자신 파일을 지우는 코드가 실행되지 못할 테니, 이 역시 동시에 충족될 수 없는 모순이 된다. ExitProcessAndDeleteFile 같은 전용 함수라도 있어야 할 것 같은데.. Windows API에는 그런 건 없다.

다만 EXE와는 달리 실행 중인 배치 파일은 자기 자신을 제거하는 게 가능하다. 그래서 설치 제거 프로그램 같은 데서는 내부적으로 EXE를 지우고 자기 자신도 삭제하는 배치 파일을 만들어서 이걸 내부적으로 실행한 뒤, 자기 프로그램은 최대한 신속하게 종료함으로써 흔적 없는 '자폭'을 한다. 배치 파일은 당연히 IF EXISTS와 GOTO loop가 있어서 파일이 완전히 지워질 때까지 삭제 시도를 반복한다.

배치 파일은 자가삭제가 가능하긴 하지만 삭제된 뒤의 명령들은 실행되지 못하고 에러와 함께 실행이 끝난다. 옛날에 4DOS 같은 MS-DOS 대체 명령 프롬프트에는 BTM (Batch to memory)처럼 모든 내용을 메모리로 읽은 뒤에 실행하는 특수한 배치 파일이 있긴 하지만 이것은 MS 기반의 오리지널 셸에 있는 기능은 아니다.

물론 설치/제거 프로그램이라면 요즘은 직접 짜는 게 아니라 Windows Installer를 사용하는 게 대세이니 저런 꼼수를 직접 구현해야 할 필요는 더욱 없어지긴 했다. 자기 정체를 요리 조리 교묘하게 숨겨야 하는 악성 코드나 돼야 필요할까?

거의 모든 Windows API 함수들은 뭘 실행하더라도 성공/실패 여부를 되돌리는 리턴값이 있다. 리턴값이 없는 void형 함수는 정말 실패를 절대로 걱정할 필요가 없는 예외적인 물건을 제외하면 매우 드물다.
그러나 ExitProcess 내지 ExitThread처럼 뭔가 자신의 실행을 종료하고, 실행 후에 리턴값을 받을 주체 자체가 없어지는 함수라면 리턴값이 응당 void이다. 종료하는 동작이 실패할 리도 없을 테고 말이다.

FreeLibraryAndExitThread도 예외가 아니다. 모듈 핸들을 잘못 줄 경우 FreeLibrary 부분은 실행이 실패할 수 있지만, 후반부의 ExitThread 부분은 언제나 성공이 보장되기 때문이다.

그러고 보니 메시지 큐에다가 WM_QUIT를 넣어 주는 PostQuitMessage도 종료와 관계가 있는 전용 함수이며 void 형이다. WM_QUIT 메시지는 GetMessage 함수로 구성된 message loop을 끝내는 역할을 하며, 윈도우 프로시저를 통해 전달되지는 않는다.
MSDN은 저 메시지는 반드시 PostQuitMessage라는 전용 함수를 통해서(주로 메인 윈도우의 WM_DESTROY 타이밍 때) 넣어야지, 수동으로 PostMessage(hWnd, WM_QUIT, 0, 0) 같은 식으로 넣어서는 안 된다고 명시한다.

사실 저건 특정 윈도우가 대상이 아니라 스레드의 메시지 큐 자체가 대상이기 때문에 hWnd에 아예 NULL을 주거나, PostThreadMessage(GetCurrentThreadId(), WM_QUIT, 0, 0)과 비슷하다.
다만, WM_QUIT은 여느 메시지와 동일하게 취급되는 게 아니라 스레드의 내부 상태 차원에서 종료 플래그가 붙은 것으로 특수하게 처리되기 때문에 PostQuitMessage 같은 별도의 함수가 존재하는 것이다. 제거 딜레마의 해결 필요 때문은 아니다.

Posted by 사무엘

2014/04/15 08:25 2014/04/15 08:25
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/952

« Previous : 1 : ... 4 : 5 : 6 : 7 : 8 : 9 : 10 : 11 : 12 : ... 13 : Next »

블로그 이미지

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

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

«   2026/07   »
      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:
3945047
Today:
260
Yesterday:
2081