1. rc 파일의 유니코드화

Visual C++ 2008까지만 해도 안 그랬던 것 같은데.. 2010쯤부터는 새로 만드는 프로젝트들의 리소스 스크립트(*.rc) 파일의 기본 인코딩이 유니코드(UTF-16LE)로 바뀌었다는 걸 본인은 최근에야 알아차렸다. 어쩐지 구버전에서는 파일을 열지를 못하더라.
그러니 이런 rc 파일의 내부에는 #pragma code_page(949) 같은 구차한 지시문도 없다.

리소스는 Windows의 실행 파일 포맷 차원에서 유니코드인데, 그걸 생성하는 스크립트 파일은 왜 유니코드가 아닌지 본인은 오랫동안 의아하게 생각해 왔다. 물론, 다국어 리소스는 언어별로 다 따로 만들지, 한 리소스 내부에 갖가지 외국어가 섞여 들어갈 일은 거의 없기 때문에 이런 방식이 크게 문제 되지 않았을 뿐이다.

리소스 파일 관련 속성을 보면.. MFC 모드로 동작할지 말지를 지정하는 옵션이 있다. 이건 rc 파일 내부에 저장되는 추가적인 옵션/플래그 같은 건 아니고, 그냥 Visual C++ IDE의 동작 방식을 결정하는 것이다. 프로젝트 내부의 설정으로 저장되는 것 같다.

이 옵션의 지정 여부에 따라 대표적으로 달라지는 것은 초기에 콤보박스 내부에다 집어넣을 데이터 목록을 지정하는 기능이다. 이것은 Windows API가 자동으로 해 주는 게 아니라, MFC가 추가적으로 구현해 놓은 기능이다. 그렇기 때문에 리소스 파일이 MFC mode로 지정돼 있지 않으면 해당 기능을 사용할 수 없다.

리소스 파일을 들여다 본 분은 아시겠지만 이 초기화 데이터는 Dialog 리소스 템플릿 안에 내장돼 있는 게 아니라, 240 (RC_DLGINIT)이라는 custom 리소스 타입에 따로 들어있다.
할 거면 리스트박스에다가도 같은 기능을 넣어 줄 것이지 왜 하필 콤보박스에다가만 넣었는지는 잘 모르겠다.

굳이 MFC를 사용하는 프로젝트가 아니더라도, Visual C++ 리소스 에디터가 저장해 놓은 대로 콤보박스를 초기화하는 기능을 내 프로그램에다가 넣고 싶으면 MFC의 소스 코드를 참고해서 직접 구현하면 된다.

그런데 리소스 스크립트 전체의 포맷은 유니코드로 바뀌었음에도 불구하고, 이 초기화 데이터는 기존 코드/프로그램과의 호환성 문제 때문에 여전히 CP_ACP이니 참 애석하다.
MFC 소스를 보면 문자열을 CB_ADDSTRING 메시지로 등록하는 부분에서 의도적으로 SendDlgItemMessageA라고 A 버전을 호출한 것을 볼 수 있다.

Windows는 여러 모로 UTF-8과는 친화적이지 않은 게 느껴진다. "UTF-8 + 32비트 wchar_t"를 전혀 찾아볼 수 없는 환경이다.

2. 소스 코드의 유니코드화, UTF-8 지원

앞서 언급한 바와 같이, Windows는 유니코드 계열이건 그렇지 않은 계열이건 2바이트(...) 단위의 문자 인코딩을 굉장히 좋아해서 전통적으로 UTF-8에 친화적이지 않았다. 친화도는 "UTF-16 > BOM 있는 UTF-8 > BOM 없는 UTF-8"의 순이다.

물론 Visual Studio의 경우, 먼 옛날의 200x대부터 소스 코드를 UTF-8 방식으로 불러들이고 저장하고, 파일 형식을 자동 감지하는 것 자체는 잘 지원한다. 하지만 한글 같은 게 전혀 없고 BOM도 없어서 일반 ANSI 인코딩과 아무 차이가 없는 파일의 경우 기본적으로 UTF-8이 아니라 ANSI 인코딩으로 간주하며, 디폴트 인코딩 자체를 UTF-8로 맞추는 기능은 기본적으로 제공되는 않는다는 점이 아쉽다.

다시 말해 새로 만드는 소스 코드라든가, 처음엔 한글이 없었다가 나중에 한글· 한자가 추가된 파일의 경우 실수로 여전히 cp949 같은 재래식 인코딩으로 파일이 저장될 여지가 있다는 것이다. 일일이 저장 옵션을 바꿔 줘야 된다.

Windows 환경에서 UTF-8 인코딩의 C++ 소스 코드는 (1) 주석을 다국어로 작성해서 온전히 보존 가능하고, (2) 동일 파일을 xcode 같은 타 OS에서도 깨지는 문자 없이 공유 가능하다는 것 정도에나 의미를 둬야 할 것이다.
L"" 문자열이 아니라 printf나 WM_SETTEXTA 같은 곳에 쓰이는 "" 문자열은 소스 코드의 인코딩이 무엇이냐에 따라 값 자체가 달라지기 때문이다.

Windows가 진정한 UTF-8 친화적인 환경이 되려면 시스템 코드 페이지 자체를 65001 UTF-8로 지정할 수 있고 명령 프롬프트에서도 그게 지원돼야 할 것이다. 하지만 UTF-8은 (1) 특정 로케일이나 언어에 속해 있지 않다는 점, 그리고 (2) 기존 multibyte 인코딩들과는 달리 한 글자가 3바이트 이상의 길이를 가질 수 있다는 점으로 인해 Windows는 이를 지원하지 않고 있었다. 그래도 요즘 마소가 워낙 파격적으로 변화하고 있고 Windows 10로 하루가 다르게 달라지고 있으니 이런 금기가 앞으로 깨지지 말라는 법도 없을 것 같다.

3. 디버그 로그의 유니코드화

Windows가 10이 나온 이래로 많이 바뀌긴 했다. 완전히 새로운 기능들만 추가되는 게 아니라, 이미 만들어졌고 앞으로 영원히 바뀌지 않을 것처럼 여겨지던 기능까지도 말이다.

가령, OutputDebugString 함수는 통상적인 다른 API 함수들과는 반대로, W가 내부적으로 A 버전을 호출하고 유니코드를 수십 년째 전혀 지원하지 않고 있었다. 이 때문에 굉장히 불편했는데.. 언제부턴가 Visual C++의 디버그 로그 출력창에 surrogate(확장 평면)까지 포함해 유니코드 문자열이 안 깨지고 온전히 찍혀 나오는 걸 보고 개인적으로 굉장히 놀랐다. 나중에 개선된 거라고 한다.

그리고 Windows의 에디트 컨트롤은 개행 문자를 오로지 \r\n밖에 지원하지 않기 때문에 메모장에서 유닉스(\n) 방식의 파일을 열면 텍스트가 개행 없이 한 줄에 몽땅 몰아서 출력되는 문제가 있었다.
그런데 이것도 Windows 10의 최신 업데이트에서는 개선되어서 \n도 제대로 표시 가능하게 되었다.
수백 KB~수 MB 이상 큰 파일을 여는 게 너무 오래 걸리던 고질적인 문제가 개선된 데 이어, 또 장족의 발전이 이뤄졌다.

Windows가 제공하는 유니코드 관련 API 중에는 주어진 텍스트가 유니코드 인코딩처럼 보이는지 판별하는 IsTextUnicode도 있고, 한자 간체-번체를 전환하는 LCMapString 같은 함수도 있다.
IsTextUnicode의 경우, 1바이트 아스키 알파벳 2개로만 이뤄진 아주 짧은 텍스트를 UTF-16 한자 하나로 오진(?)하는 문제가 있어서 내 기억이 맞다면 한 2000년대 Windows XP 시절에 버그 패치가 행해지기도 했다. 사실, 이런 휴리스틱은 정답이 딱 떨어지는 문제가 아니기도 하다.

저런 식으로 알고리즘이 일부 개정되는 경우가 있긴 했지만, Windows에서 데이터 기반으로 동작하는 유니코드 API들은 대개가 한 1990년대 말, 겨우 Windows NT4와 유니코드 2.0 정도나 있던 시절 이후로 그 데이터와 알고리즘이 업데이트 된 내역이 없다.

그래서 간체/번체를 변환하는 테이블에 등재된 한자는 내가 세어 본 기억에 맞다면 2300개 남짓밖에 되지 않으며, IsTextUnicode도 21세기 이후에 유니코드에 새로 추가된 수많은 글자들까지 고려하지는 않고 동작한다.
이 와중에 그래도 Windows 10에 와서 OutputDebugStringW가 제 구실을 하기 시작하고 메모장의 동작이 바뀌기도 한 것이 놀랍게 느껴진다.

한편, UTF-8은 그래도 첫 바이트로 등장할 만한 글자와 그 이후 바이트로 등장할 만한 글자가 형태적으로 무조건 정해져 있다. 그래서 데이터니 통계니 휴리스틱 없이도 자기가 UTF-8이라는 게 딱 티가 나며, UTF-8을 타 인코딩으로 오인한다거나 타 인코딩을 UTF-8로 오인할 가능성이 거의 없는 것이 매우 큰 장점이다.

4. 문자형의 부호 문제

컴퓨터에서 문자열의 각 문자를 구성하는 단위 타입으로는 전통적으로 char가 쓰여 왔다. 그러다가 유니코드가 등장하면서 이보다 공간이 더 커진 wchar_t가 도입되었으며, 언어 표준까지 채택됐다. 값을 다룰 때는 같은 크기의 정수와 다를 게 없지만, 포인터로는 서로 곧장 호환되지 않게 type-safety도 강화되었다.

그런데, 처음에 1바이트짜리 문자열의 기본 타입을 왜 괜히 부호 있는 정수형인 char로 잡았을까 하는 게 아쉬움으로 남는다. 물론 매번 unsigned를 붙이거나 typedef를 하는 건 귀찮은 일이며, 영미권에서는 문자 집합 크기가 7비트만으로도 충분했다는 그 상황은 이해한다. 하지만 문자 코드를 저장할 때는 애초에 부호 따위는 전혀 필요하지 않다. char보다 더 큰 wchar_t도 결코 부호 있는 정수형과 대응하지 않는다.

크기가 겨우 8비트밖에 안 되는 '바이트'는 양수 음수를 따지기에는 너무 작은 타입이기도 하다. 파일이나 메모리 데이터를 바이트 단위로 읽으면서 2의 보수 기반의 부호를 따질 일이 과연 있던가..?

이런 구조로 인해.. UTF-8이건 -16이건 -32이건 모두 대응 가능한 문자열 템플릿을 만들 때, char형에 대해서만 코드값 범위 검사를 할 때 예외를 둬야 하는 불편한 상황도 생긴다. 템플릿 인자로 주어진 어떤 타입에 대해서, 크기는 동일하면서 부호만 없는 타입을 자동으로 되돌리는 방법이 C++ 언어에는 존재하지 않기 때문이다.

그러니 다른 타입에서는 다 간단하게 >= 0x80을 검사하면 되는데, char만 <0을 봐야 한다. 이런 거 로직이 꼬이면 모든 타입에서 0xF0이 저장되어야 하는데 딴 타입에서는 0xFFF0이 저장되는 식의 문제도 생길 수 있다. 그렇다고 임의로 제일 큰 부호 없는 정수형으로 typecast를 하는 건 무식한 짓이다.

회사에서 일을 하다가 하루는 이게 너무 짜증 나서 std::basic_string<unsigned char>로부터 상속받은 클래스를 새로 만들어 버렸다. 베이스 클래스의 명칭이 딱 한 토큰 한 단어가 아니라 저렇게 템플릿 인자가 덕지덕지 붙은 형태인 게 특이했다만.. 생성자 함수에서 기반 클래스를 호출할 때는 __super를 쓰지도 못하더라.

내부적으로는 모든 처리를 unsigned char를 기준으로 하는데, 생성자와 덧셈 연산, 형변환 연산에서만 부호 있는 const char*를 추가로 지원하는 놈을 구현하는 게 목적이었다. 이런 생각을 나만 한 건 절대 아닐 텐데..
개인적으로 + 연산자를 만드는 부분에서 좀 헤맸었다. 얘는 +=와 달리 완전한 내 객체를 새로 만들어서 되돌리는 것이기 때문에 컴파일러 에러를 피해서 제대로 구현하는 게 생각보다 nasty했다. 그렇다고 도저히 못 할 정도는 아니었고.. 뭐 그랬다.

그러고 보니 Java는 기본적으로 부호 있는 정수형만 제공하지만, char만은 문자 저장용으로 부호 없는 16비트 정수를 쓰는 걸로 본인은 알고 있다.
그런데 얘도 그 크기로는 BMP 영역 밖은 표현할 수 없다. Java 언어가 처음으로 설계되고 만들어지던 때는 1990년대 중반으로, 아직 유니코드 2.0과 확장 평면 같은 개념이 도입되기 전이었다.
결국 범용적인 글자 하나를 나타내려면 부호 있는 정수인 int를 써야 한다. 상황이 좀 복잡하다..;;

5. Windows 문자열 변환 함수의 함정

Windows API 중에서 WideCharToMultiByte와 MultiByteToWideChar는 운영체제가 내부적으로 사용하는 2바이트 단위 UTF-16 방식의 문자열과 타 인코딩 문자열(UTF-8, CP949 등..)을 서로 변환하는 고전적인 함수이다.

이 두 함수는 크게 두 가지 모드로 동작한다. (1) 사용자가 넘겨준 문자열 버퍼 포인터에다가 변환을 수행하거나.. (2) 아니면 그렇게 write 동작을 하지는 않고, 이 원본 문자열을 몽땅 변환하는 데 필요한 버퍼의 크기만을 되돌린다.
일단 (1)처럼 동작하기 시작했는데 사용자가 넘겨준 버퍼 크기가 원본 문자열을 모두 변환해 넣기에 충분하지 못하다면 함수의 실행은 실패하고 0이 돌아온다.

이때도 기존 버퍼의 크기만치 변환을 하다가 만 결과는 확인할 수 있다. 하지만 원본 문자열의 어느 지점까지 변환하다가 끊겼는지를 이 함수가 알려 주지는 않는다. 그렇기 때문에 그 정보를 유의미하게 활용하기 어려우며, 이는 개인적으로 아쉽게 생각하는 점이다.

그러니 버퍼가 얼마나 필요한지 알 수 없는 일반적인 경우라면, WideCharToMultiByte를 기준으로 함수를 사용하는 방식은 이런 형태가 된다. 함수를 두 번 호출하게 된다.

const wchar *pSrcBuf = L"....";
int len = WideCharToMultiByte(CP_***, pSrcBuf, -1, NULL, 0, ...); //(2) 크기 측정
char *pTgtBuf = new char[len];
WideCharToMultiByte(CP_***, pSrcBuf, -1, pTgtBuf, len, ...); //(1) 실제로 변환
...
delete []pTgtBuf;

그런데, 이들 함수가 (1)과 (2) 중 어느 모드로 동작할지 결정하는 기준은 버퍼 포인터(pTgtBuf)가 아니라, 버퍼의 크기(len)이다.
보통은 크기를 측정할 때 포인터도 NULL로 주고 크기도 0으로 주니, 함수가 내부적으로 둘 중에 뭘 기준으로 동작하든 크게 문제될 건 없다.

하지만 한 버퍼에다가 여러 문자열을 변환한 결과를 취합한다거나 해서 포인터는 NULL이 아닌데 남은 크기가 우연히도 딱 0이 될 수 있는 상황이라면 주의해야 한다. 포인터와 버퍼 크기가 pTgtBuf + pos, ARRAYSIZE(pTgtBuf) - pos 이런 식으로 정해진다거나 할 때 말이다.

저 식에서 pos가 우연히도 배열의 끝에 도달했다면, 남은 크기가 0이니까 프로그래머가 의도하는 건 이 함수의 실행이 무조건 실패하고 0이 돌아오는 것이다. 그럼 프로그램은 버퍼 공간이 부족해졌다는 걸 인지하여 지금까지 쌓인 버퍼 내용을 딴 데로 flush한 뒤, 변환을 재시도하면 된다.

하지만 이 함수는 프로그래머가 의도한 것처럼 동작하지 않는다. 문자열 변환을 하지 않지만, 마치 실행에 성공한 것처럼 변환에 필요한 버퍼 크기를 되돌린다. 쉽게 말해 "(1)에 대한 실패"가 아니라 "(2)에 대한 성공"으로 처리된다는 것이다.

이 문제를 피하려면, 버퍼를 관리하는 프로그램은 WideChar...함수가 실패했을 때뿐만 아니라 포인터가 버퍼의 끝에 도달했는지의 여부도 따로 체크해야 하며, 그 경우 버퍼를 flush해 줘야 한다. 변환 함수가 후자까지 같이 체크해 주지 않기 때문이다.
날개셋 편집기가 9.5의 이전 버전에, 바로 이것 때문에 대용량 파일을 저장할 때 낮은 확률로 데이터를 날려먹는 버그가 있었다.

Windows API가 입력값이 0일 때에 대해 일관된 유도리가 없어서 불편한 예가 GDI 함수에도 있다.
주어진 문자열의 픽셀 단위 길이와 높이를 구하는 GetTextExtentPoint32의 경우, 문자열 길이에다 0을 주면 가로는 0이고 세로 크기만 좀 구해 줬으면 좋겠는데.. 그러질 않고 그냥 실행이 실패해 버린다.
높이만 구하고 싶으면, 공백 하나라도 dummy로 전해 준 뒤 리턴값의 cx 부분은 무시하고 cy를 사용해야 한다.

Posted by 사무엘

2018/09/29 08:35 2018/09/29 08:35
, ,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1537

Trackback URL : http://moogi.new21.org/tc/trackback/1537

Comments List

  1. 방문객 2018/09/29 14:43 # M/D Reply Permalink

    안 그래도 Windows에서 UTF-8을 지원하려는 움직임이 있는 것 같습니다. Insider 버전에서 UTF-8 사용이라는 설정을 찾아볼 수 있었습니다: https://puu.sh/BCKTc/160cf41d7f.png

    1. 사무엘 2018/09/29 20:41 # M/D Permalink

      아하 그렇군요~! 머지않아 chcp 65001이 가능해지고, Windows의 A 함수와 W 함수는 UTF-8과 UTF-16의 차이밖에 없어지는 날이 올 듯합니다. ^^;;
      레거시 프로그램들과의 호환은.. 지금 마소에서 per-process, per-monitor등 정밀한 high DPI 지원을 위해서 기를 쓰고 노력하는 정도의 샌드박스 계층만 만들면 얼마든지 가능하지 않겠나 개인적으로 생각해요. manifest 파일에 native UTF8을 지원한다고 명시해 주면 되겠죠.

Leave a comment

1. R-value 임시 객체를 일반 참조자 형태로 함수 인자로 전달하기

C/C++ 프로그래밍 요소 중에는 잘 알다시피 & 연산자를 이용해서 주소를 추출할 수 있으며 값 대입이 가능한 L-value라는 게 있고, 그렇지 않고 값 자체만을 나타내는 R-value라는 게 있다.
C언어 시절에는 R-value라는 게 함수의 리턴값이 아니면 프로그램 소스 코드 차원에서 리터럴 형태로 하드코딩되는 숫자· 문자열밖에 없었다. 하지만 C++로 와서는 생성자와 소멸자가 호출되는 클래스에 대한 임시 인스턴스 개체도 그런 범주에 속할 수 있게 되었다.

다른 변수에 딱히 대입되지 않은 함수의 리턴값 내지, 변수로 선언되지 않고 함수의 인자에다가 곧장 Class_name(constructor_arg) 이런 형태로 명시해 준 객체 선언은 모두 R-value이다.

R-value를 그 타입에 대한 포인터형으로 함수 인자로 전달하는 것이야 가능하지 않다. 주소를 얻을 수 없으니 말이다. C/C++에 &100 이런 문법 같은 건 존재하지 않는다.
하지만 참조자는 외형상 값으로 전달하는 것과 동일하기 때문에 저런 제약이 없다. 그런데 참조자가 내부적으로 하는 일은 포인터와 완전히 동일하다. 그럼 뭔가 모순/딜레마가 발생하지 않을까?

이런 오류를 막기 위해, R-value를 일반 참조자형으로 전달하는 것은 원래 금지되어 있다. 참조자 내지 포인터는 자신이 가리키는 대상이 당연히 대입 가능한 L-value라는 것을 전제로 깔기 때문이다.
임시 개체를 함수 인자로 전하려면..

  • 그냥 값으로 전달해야 한다. 이 방법은 객체의 크기가 크거나 복사 생성자에서 하는 일이 많다면, 성능 오버헤드가 우려된다.
  • 아니면 const 참조자형으로 전달해야 한다. R-value는 대입이고 변경이고 불가능한 놈이니 const 제약을 줘서 취급하는 것이 이치에 맞다.
  • 아니면 얘는 임시 R-value이지만 값이 보존되지 않아도 아무 상관 없다는 표식이 붙은, C++11의 R-value 참조자 &&를 써서 전달하면 된다.

사실, 정수 같은 아주 작고 간단한 타입이라면 모를까, 커다란 객체는 임시 객체라 해도 어차피 자신만의 주소를 갖고 있는 것이나 다름없다.
그래서 그런지 Visual C++은 200x 언제부턴가 R-value를 통째로 일반 참조자로 전달하는 것을 허용하기 시작했다. xcode에서는 에러가 나지만 Visual C++은 컴파일 된다. 이렇게 해 주는 게 특히 템플릿을 많이 쓸 때 더 편하긴 하다. VC++도 내 기억으로 6.0 시절에는 안 이랬다.

사실, 이걸 허용하나 안 하나.. &로 전달하나 &&로 전달하나 컴파일러 입장에서는 크게 달라지는 게 없다. 템플릿 인자로 들어간 타입이 객체가 아니라 정수라면 좀 문제가 되겠지만 어차피 C++은 서로 다른 템플릿 인자에 대해서 같은 템플릿을 매번 새로 컴파일 하면서 코드 생성과 최적화를 매번 새로 하는 불편한(?) 언어이다. 그러니 각 상황별로 따로 처리해 주면 된다.
물론 R-value 참조자가 C++에 좀 일찍 도입됐다면 Visual C++이 저런 편법을 구현하지 않아도 됐을 것 같아 보인다.

2. C++에서 함수 선언에다 리턴 타입 생략하기

엄청 옛날 유물 얘기이긴 하지만.. Visual C++이 2003 버전까지는 아래 코드가 컴파일이 됐었다는 걸 우연한 계기로 뒤늦게 알게 됐다. 바로 함수를 선언· 정의할 때 리턴 타입을 생략하는 것 말이다.

class foo {
public:
    bar(int x); //생성자나 소멸자가 아닌데 클래스 멤버 함수의 리턴 타입을 생략!! 여기서는 경고는 뜸
};

foo::bar(int x) //여기서도 생략했지만 그래도 int로 자동 간주됨
{
    return x*x;
}

main() { return 0; }

C++은 본격적인 클래스 다루는 거 말고 일상적인 코딩 분야에서 C와 달라지는 차이점이 몇 가지 있다. 그 차이점은 대부분 C보다 type-safety가 더 강화되고 엄격해진다는 것이다.

  • 임의의 type의 포인터에다가 void*를 대입하려면 형변환 연산자 필수. (경고이던 것이 에러로)
  • 함수도 prototype을 반드시 미리 선언해 놓고 사용해야 함. (경고이던 것이 에러로)
  • 그리고 함수의 선언이나 정의에서 리턴 타입을 생략할 수 없음. 한편, 인자 목록에서 ()은 그냥 임의의 함수가 아니라 인자가 아무것도 없는 함수, 즉 void 단독과 동치로 딱 정립됨.
  • 그 대신 C++은 지역 변수(+객체)를 굳이 {} 블록의 앞부분이 아니라 실행문 뒤에 아무 데서나 선언해도 된다.

이 개념이 어릴 적부터 머리에 완전히 박혀 있었는데, VC6도 아니고 2003의 컴파일러에는 C의 잔재가 아직 저렇게 남아 있었구나. 몰랐다.
cpp 소스에다가도 int main()대신 main()이라고 함수를 만들어도 되고, 심지어 클래스의 멤버 함수에다가도 리턴 타입을 생략할 수 있었다니... 완전 적응 안 된다.

물론 익명 함수(일명 람다)를 선언할 때는 꼭 리턴값 타입을 안 써 줘도 된다. void나 int 같은 간단한 것은 함수 내부의 return문을 통해서 컴파일러가 그럭저럭 유추해 주기 때문이다.
함수형이라는 완전히 다르고 새로운 패러다임이 C++에 한참 뒤에 추가되다 보니 일관성이 깨졌다면 깨진 듯이 보인다.

3. C/C++ 컴파일러 및 언어 문법 자체의 변화

내 경험상, 지난 2000년대에는 Visual C++ 컴파일러의 버전이 올라가면서 명칭의 scope 인식이 좀 더 유연해지곤 했다.
가령, 클래스 A의 내부에 선언된 구조체 B를 인자로 받는 멤버 함수 C의 경우, C의 몸체를 외부에다 정의할 때 프로토타입 부분에서 B를 꼭 A::B라고 써 줘야 됐지만, VC6 이후 버전부터는 그냥 B라고만 써도 되게 됐다.

람다가 최초로 지원되기 시작한 미래의 VC++ 2010에도 비슷한 한계가 있었다.
클래스 멤버 함수 내부에서 선언된 람다 안에서는 그 클래스가 자체적으로 정의해 놓은 타입이나 enum값에 곧장 접근이 안 됐다. A::value 이런 식으로 써 줘야 했는데.. 2012와 그 후대 버전부터는 곧바로 value라고만 써도 되게 바뀌었다. 2012부터 람다가 함수 포인터로 cast도 가능해졌고 말이다.

내 기억이 맞다면 템플릿 인자가 공급된 템플릿 함수가 함수 포인터로 곧장 연결되는 게 VC++ 2003쯤부터 가능해졌다. 상식적으로 당연히 가능해야 할 것 같지만 VC6 시절에는 가능하지 않았었다.
2000년대가 generic이라면 2010년대는 functional 프로그래밍이 도입된 셈이다.

아.. 또 무슨 얘기를 더 할 수 있을까?
C는 옛날에, 원래 처음에 1980년대까지는 K&R 문법인지 뭔지 해서 함수의 인자들의 타입을 다음과 같이 지정할 수도 있었다고 한다.

int foo(a, b) int a; float b; { return 0; }

같은 명칭을 이중으로 명시하고 체크하느라 괜히 분량 길어지고 파싱이 힘들어지고..
무슨 Ada나 Objective C처럼 함수를 호출할 때 foo(b=20, a=1)처럼 인자들 자체의 명칭을 지정하는 것도 아닌데..
저 문법이 도대체 무슨 의미가 있고 왜 저런 게 존재했는지 나로서는 좀 이해가 안 된다. C는 파이썬 같은 dynamic 타입 언어도 전혀 아닌데 말이다.

C 말고 C++도 1980년대까지는 문법이나 라이브러리가 제대로 표준화되지도 않았었고, 지금으로서는 상상하기 어려운 관행이 많았다고 한다. 유명한 예로는 소멸자가 존재하는 클래스 객체들의 배열을 delete 연산자로 제거할 때는 원소 개수를 수동으로 공급해 줘야 했다거나, static 멤버를 선언한 뒤에 굳이 몸체를 또 정의할 필요가 없었다거나 하는 것 말이다.

사실, 후자의 경우 또 정의할 필요가 없는 게 더 직관적이고 프로그래머의 입장에서 편하긴 하지만.. 컴파일러와 링커의 입장에서는 생성자 함수를 호출하는 실행문이 추가되는 부분이 따로 들어가는 게 직관적이니 불가피하게 생긴 변화이긴 하다.
이런 저런 변화가 많은데 C++은 표준화 이전의 격변의 초창기 시절에 대한 자료를 구하기가 어려워 보인다.

객체를 생성하는데 메모리 주소는 정해져 있고 거기에다 생성자 함수만 호출해 주고 싶을 때..
지금이야 placement new라는 연산자가 라이브러리의 일부 겸 사실상 언어의 일부 요소로 정착해서 new(ptr) C라고 쓰면 되지만, 옛날에는 ptr->C::C() 이런 표기가 유행이었다. . ->를 이용해서 생성자와 소멸자 함수를 직접 호출하는 건 지금도 금지되어 있지는 않지만.. 사용이 권장되는 형태는 아닌 듯하다.

4. macOS와 Windows의 관점 차이

macOS의 GUI API(Cocoa)와 Windows의 GUI API는 뭐 뿌리나 설계 철학에서 같은 구석을 찾을 수 없을 정도로 완전히 다르다.
좌표를 지정하는데 화면이 수학 좌표계처럼 y축이 값이 커질수록 위로 올라가며, NSRect는 Windows의 RECT와 달리 한쪽이 절대좌표가 아니라 상대좌표이다. 즉, 두 개의 POINT로 구성된 게 아니라 POINT와 SIZE로 구성된 셈이다.

또한 Windows의 사고방식으로는 도저히 상상할 수 없는 float 부동소수점을 남발하며, 처음부터 픽셀이라는 개념은 잊고 추상적인 좌표계를 쓴다. 얘들은 이 정도로 장치· 해상도 독립적으로 시스템을 설계했으니 high DPI 지원도 Windows보다 더 유연하게 할 수 있었겠다는 생각이 들었다.

그리고 대화상자나 메뉴 같은 리소스를 관리하는 방식도 둘은 서로 다르다.
Windows는 철저하게 volatile하다. 이런 것은 생성되는 시점에서 매번 리소스로부터 처음부터 load와 copy, 초기화 작업이 반복되며, 사용자가 해당 창을 닫은 순간 메모리에서 완전히 사라진다.

그 반면, 맥은 그런 것들이 사용자가 닫은 뒤에도 이전 상태가 계속 남아 있다.
내가 뭔가 설정을 잘못 건드렸는지는 모르겠지만, Windows로 치면 [X]에 해당하는 닫기 버튼을 눌러서 대화상자를 닫았는데도 호락호락 창이 사라지지 않고 DestroyWindow보다는 ShowWindow(SW_HIDE)가 된 듯한 느낌? 대화상자의 이전 상태가 계속 살아 있는 것 같다.

특히 메뉴의 경우 Windows는 내부 상태가 완전히 일회용이다. 메뉴가 화면에 짠 표시될 때가 되면(우클릭, Alt+단축키 등) 매번 리소스로부터 데이터가 로딩된 뒤, 체크 또는 흐리게 같은 다이나믹한 속성은 그때 그때 새로 부여된다.
그 반면 mac에서는 메뉴가 화면에 표시되지 않을 때에도 내부 상태가 쭉 관리되고 있는 게 무척 신기했다.

그러니 개발자는 시간과 여유만 된다면 프로그래밍 언어와 환경을 많이 알면 사고의 범위가 넓어질 수 있고 각각의 환경을 설계한 사람의 관점과 심정을 이해할 수도 있다.
하지만 뒤집어 말하면 프로그래머는 은퇴하는 마지막 순간까지도 공부하고 뒤따라가야 할 게 너무 많다.. -_-;; 이거 하나 적응했다 싶으면 그새 또 다른 새로운 게 튀어나와서 그거 스펙을 또 공부해야 된다.

Posted by 사무엘

2018/09/08 08:37 2018/09/08 08:37
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1530

Trackback URL : http://moogi.new21.org/tc/trackback/1530

Leave a comment

컴퓨터 프로그램의 GUI 구성요소들 중에는 여러 아이템들을 한데 나열하는 리스트 박스(list box)라는 게 있고, 고정된 한 문장에 대해서 예/아니요, 참/거짓 여부를 지정하는 체크 박스(check box)라는 게 있다.

체크 박스는 프로그램이 고정 붙박이 형태로 제공하는 기능이나 옵션 하나에 대한 설정을 할 수 있다. 그리고 리스트 박스는 보통은 가변적인 개수의 항목들 중에 하나를 선택할 때 쓰인다.
그런데 가끔은 이 두 물건의 기능을 한데 합치고 싶은 상황이 생긴다. 리스트 박스의 각 아이템들에 대해서 1비트짜리 정보를 배당해서 선택 여부를 지정하는 것 말이다.

뭐, Windows의 리스트박스 컨트롤은 모든 아이템들에 대해서 1비트도 아니고 그냥 machine word 크기 하나로 custom 정보 data를 지정하는 기능이 있다. 또한 필요하다면 하나가 아닌 복수 개 multi-selection 모드로 동작하게 할 수 있고, 각 아이템에 대한 custom drawing도 가능하다.

하지만 딱 부러지게 아이템들 앞에 자동으로 운영체제의 check box 그림을 그려 주고 체크 박스의 리스트를 구현하는 기능 자체는 없다. 필요하면 사용자가 그걸 직접 구현해서 쓰게 여건만 만들어 놨을 뿐이다.
그래서 MFC의 경우 기존 리스트박스를 서브클래스 해서 CCheckListBox라는 걸 제공한다. owner drawing만 구현하는 것으로는 충분치 않고, space를 누른 키보드 입력과 check 버튼 주위를 누른 마우스 클릭도 감지하게 메시지 몇 개를 서브클래스 했다.

자고로 화면에 뭔가 길다란 리스트를 만들고 아이템들을 복수 선택할 수 있게 해 놓은 프로그램의 원조는 PC-Tools나 MDIR, Norton Commander, 심지어 Windows 3.x의 파일 관리자 같은 파일 관리 유틸리티이지 싶다. 복수 개의 파일을 복사하거나 삭제하는 기능을 제공해야 하니 리스트의 복수 선택 기능이 무조건 필수이기 때문이다.

그런데, selection이라는 것과 highlight 선택막대의 관계를 어떻게 보느냐에 따라서 프로그램의 동작이 달라지곤 했다. 도스용 프로그램들은 selection과 선택막대가 서로 따로 논다고 본 반면, Windows는 selection이 곧 선택막대의 연장선이라고 봤다.

그래서 Windows의 리스트박스는 화살표 키를 누르는 순간 기존 selection들이 다 사라지면서 선택막대가 움직이곤 했다. Shift+화살표로 연속된 영역을 한꺼번에 선택하는 것 말고 불연속적인 영역을 취사선택하려면 Shift+F8부터 눌러서 선택막대가 아닌 포커스 테두리가 깜빡거리는 상태로 들어간 뒤, 포커스 테두리만 움직이면서 Space로 아이템들을 선택하면 됐다.

굉장히 특이한 동작인데 Windows에서는 이게 기본이다. 기본적으로 포커스 테두리만 움직이게 하는 모드는 extended 플래그(LBS_EXTENDEDSEL)로 따로 있었다.
그에 비해 평소에는 선택막대와 selection이 다같이 움직이고 Ctrl+화살표로 포커스 테두리를 움직여서 Space로 선택하는 비교적 '직관적인 방법'은 훗날 리스트뷰 컨트롤이 도입하게 된다. 아이템을 복수 선택하는 방식은 이 두 컨트롤이 서로 호환되지 않는다.

또한, 각 아이템들에 대해 체크 플래그를 지원하는 건 아이템을 그냥 복수 선택할 수 있게 하는 것과는 UI의 관점이 다르다. 비록 내부적으로 본질적으로는 아이템별로 1비트짜리 boolean 정보를 지정한다는 점에서는 차이가 없겠지만 용도가 같지 않다는 것이다.

복수 선택은 대체로 아이템들이 진짜 가변적이고 사용자에 의해 아이템을 추가하거나 삭제까지 할 수 있는 상황에서 쓰이겠지만 체크 리스트는 그렇지 않은 경우가 대부분이다. 단순히 응용 프로그램이 제공하는 기능과 옵션이 많기 때문에 리스트 형태로 만들었을 뿐이다. 체크 리스트는 복수 선택과 달리, 선택 막대 selection과는 완전히 별개로 관리되기도 해야 할 것이고 말이다.

다음은 MFC의 CCheckListBox를 사용했던 먼 옛날 날개셋 한글 입력기 1.x의 옵션 대화상자이다.
Windows XP부터는 테마도 등장했기 때문에 지금 상황에 따라 체크 박스를 그리는 방법 역시 더 복잡해졌다.

사용자 삽입 이미지

다음은 리스트 박스가 아니라 무려 트리 컨트롤(공용 컨트롤)을 사용했던 날개셋 2.x의 옵션 대화상자의 모습이다.
Internet Explorer가 4인가 5에서부터 인터넷 고급 옵션들을 이렇게 트리 컨트롤로 구현해서 오늘날 최후의 11 버전에까지 이어져 오고 있다. 지원하는 옵션이 너무 많기 때문이다. 본인 역시 이 스타일을 따라해 보았다.

사용자 삽입 이미지

공용 컨트롤들은 owner-draw 안 쓰고도 자체적으로 아이템별 비트맵을 지정할 수 있으며, 더구나 트리 컨트롤은 아이템들을 카테고리별로 분류도 할 수 있으니 더욱 좋다.
날개셋 3과 그 이후부터는 이들 옵션이 상당수가 오토마타와 글쇠의 수식, 별도의 카테고리 옵션 등으로 떨어져나간 관계로, 저렇게 트리 컨트롤까지 써야 할 정도로 긴 옵션 리스트를 만들 일이 없어졌다.

사실, 트리 컨트롤은 IE 4 타이밍에서 TVS_CHECKBOXES라는 스타일이 추가되기도 했다. 기존 이미지 스타일을 활용하는 게 아니라 그건 놔두고 옆에 체크 박스를 별도로 추가해 주는 형태이다.

트리 컨트롤에서 체크 박스는 설치 프로그램에서 어떤 소프트웨어 제품의 구성요소들을 계층 구조로 나열한 뒤 설치· 제거할 부분을 선택받는 부분에서 유용하게 쓰일 듯하다. 이런 데서는 자식 노드가 하나라도 선택되면 부모 노드들은 중간 상태로 바뀌고, 부모 노드를 선택하거나 해제하면 자식들도 한꺼번에 선택이나 해제되는 동작이 필요할 것이다.

하지만 트리 컨트롤의 체크박스 기능은 깔끔하게 구현되지 않아서 잡음이 많다. 스타일을 윈도우를 생성한 뒤에 SetWindowLongPtr로 런타임 때, 그리고 아이템을 하나라도 추가하기 전에 적절한 타이밍에만 지정할 수 있다.
레이먼드 챈 아저씨는 저건 차라리 스타일이 아니라 메시지 형태로 구현하는 게 더 나았을 정도라면서 API 설계 구조를 비판한 바 있다. (☞ 링크) 실제로 콤보 박스의 extended UI 여부는 스타일이 적절해 보임에도 불구하고 덩그러니 CB_SETEXTENDEDUI라는 메시지를 통해 지정하게 돼 있다.

한편, 트리 컨트롤은 처음 도입됐을 때부터 지금까지 체크 박스와는 달리, '복수 선택'은 지원하지 않고 있는 것으로 유명하다. 리스트뷰 컨트롤처럼 아이콘을 Shift 및 Ctrl을 이용하여 복수 선택할 수 있지 않다는 뜻이다.
Windows 운영체제는 탐색기에서 볼 수 있듯이, UI 디자인 철학이 "트리로는 분야를 하나 선택만 하고", "리스트에다가 그 분야에 속하는 아이템들을 출력한 뒤 복수 선택해서 지지고 볶는다" 형태이긴 했다.

계층 구조를 나타낼 수 있는 복잡한 UI 컨트롤에서 복수 선택까지 가능하면 프로그램의 기능이 매우 복잡해지며, 리스트도 아니고 트리 컨트롤이 굳이 복수 선택까지 가능해야 할 일은 매우 드문 것도 사실이다.
하지만 그럼에도 불구하고 당장 Visual Studio IDE부터가 클래스· 리소스· 솔루션 뷰의 트리 목록이 진작부터 복수 선택을 지원한다. 걔들은 4.0 시절부터 공용 컨트롤 없이 진작부터 자체 구현 트리 컨트롤을 써 왔기 때문이다.

끝으로, 체크와 다중 선택을 짬뽕한 듯한 기괴한 UI가 Windows의 역사상 단 한 번, 8의 리스트뷰 컨트롤에서 잠시 등장한 적이 있었다.
아이템의 좌측 상단 같은 특정 부위를 마우스로 가리키고 있으면 체크 박스가 나타나고, 그걸 클릭하면 아이템을 복수 선택할 수 있었던 것이다.
보통 아이템을 클릭하면 기존 selection들은 다 없어지고 그것'만' 선택되곤 하는데, 체크 박스를 선택하면 기존 selection들을 놔두고 그걸 추가로 선택할 수 있었다.

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

그 당시엔 아마 터치 장치를 염두에 두고.. Ctrl/Shift+클릭이나 드래그 없이 클릭만으로도 아이템들을 복수 선택할 수 있게 고심 끝에 저런 기능을 넣었던 듯하다.
하지만 반응이 좋지 않았는지, 이런 기능은 내 기억이 맞다면 Windows 8.1에서 곧장 없어졌고 다시 등장하지 않았다. 하긴, Windows 8은 저 정도면 약과이지, 아예 시작 버튼을 없애 버렸을 정도로 엄청 과격한 모험을 한 물건이기도 했으니까.

이렇듯, 리스트 박스, 리스트뷰 컨트롤, 트리 컨트롤을 두고 아이템의 복수 선택 및 체크 선택과 관련하여 할 말이 무척 많은 걸 알 수 있다. 복수 선택은 단수 선택만치 일상적으로 자주 쓰이는 기능은 아닐 뿐더러 어떤 방식으로 구현할지 동작의 customization의 폭도 넓은 편이다. 그래서 운영체제의 GUI가 곧장 직통으로 지원하지 않고 구현을 사용자에게 맡기는 편이었던 것 같다.

Posted by 사무엘

2017/12/20 08:36 2017/12/20 08:36
, ,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1439

Trackback URL : http://moogi.new21.org/tc/trackback/1439

Comments List

  1. 방문자 2017/12/21 20:01 # M/D Reply Permalink

    Windows 10에서도 탐색기에서 체크박스의 표출 여부가 옵션으로 남아 있습니다. 8.1에서 기본값이 바뀌었던 걸까요?

    1. 사무엘 2017/12/21 22:16 # M/D Permalink

      아하~ '기본적으로 꺼져 있는 상태'가 됐을 뿐, 그 UI가 완전히 없어진 건 아니었군요.
      옵션들 제~~일 밑에.. "확인란을 사용하여 항목 선택" (이것도 트리 컨트롤 기반 체크 리스트이군요~)...
      저는 처음 알게 됐습니다. 그렇다면 8.1에서 기본값이 바뀐 것이지 싶습니다. ^^

Leave a comment

Visual Basic 6은 이제 개발사로부터 지원이 중단된 지 무려 10년이 돼 가는데(나온 지는 20년..!) 아직도 현업에서 쓰는 경우가 있는지 모르겠다. Visual C++ 6도 업계에서 도를 넘는 노인학대를 당해 온 물건이긴 하지만, 그래도 얘는 이제는 거의 은퇴한 듯하다. 그리고 VB6과 VB .NET은 VC6과 VC .NET하고는 처지가 완전히 딴판으로 다르다.

비주얼 베이직이 오늘날까지 인류에게 남긴 독보적인 GUI 유산은 바로 property grid이지 싶다. 이거 원조가 바로 VB이다.

사용자 삽입 이미지

이건 운영체제의 공용 컨트롤로 제공되지는 않는다. 하지만 닷넷에서는 자체 구현한 컴포넌트가 있는 듯하며, 네이티브 환경에서는 그냥 3rd-party GUI 툴킷에서 구현해 놓은 레플리카 내지 짝퉁이 쓰인다.

property grid는 오늘날까지 Visual Studio IDE에서 Alt+Enter 속성 창과 프로젝트 속성 대화상자에서 고스란히 볼 수 있다. 수십 개의 설정들이 추가되더라도 번거롭게 대화상자를 디자인할 필요 없이 설정을 뒤에다 추가만 하면 되니 참 편하다.
이에 비해 VC6의 옛날 속성 대화상자는 얼마나 추레하게 생겼는가?

단, 외형이 깔끔하긴 해도 너무 사무적이고 재미없게 생겨서 그런지, 개발툴이나 DBMS 말고 일반 사용자용 Office 제품 같은 데서는 property grid가 등장하는 걸 여전히 본 적이 없는 것 같다.

Visual Basic은 1991년 5월에 Windows용으로 1.0이 첫 출시됐다. 드래그 앤 드롭 방식으로 폼을 디자인하고 곧장 이벤트를 추가하는 방식으로 코딩을 하는 굉장히 획기적인 개발툴이라고 찬사를 받았음이 틀림없다. Windows용의 호평에 힘입어 그 해 9월에는 처음이자 마지막으로 도스용 비베도 1.0이 나와서 QuickBasic과 MS Basic PDS의 라인을 종결시켰다. 하지만 VB의 UI 엔진은 경쟁작이던 볼랜드 Turbo Vision 라이브러리에 비해서는 인지도가 매우 낮다.

그 뒤 VB 2와 3은 16비트 Windows용으로 나와서 인기를 얻다가 95년에 나온 4.0은 16비트용과 32비트용이 나란히 동시에 출시되었다. 마소에서 제품을 이런 식으로 동일 버전을 16비트용과 32비트용으로 동시에 내놓는 건 극히 드물었고 아마 VB4가 거의 유일했다. Office나 VC++는 그냥 상위 버전에서 곧장 32비트용이 나오면서 16비트 지원을 중단하는 형태였기 때문이다.
물론 VB도 5부터는 당연히 32비트 전용으로 갈아탔다. VB6 이후의 .NET에 맞춘 언어 마개조의 역사는 굳이 여기서 더 말할 필요가 없을 것이다.

델파이(네이티브 코드 지원 RAD), Java(압도적으로 넓은 플랫폼 지원, 인지도, 점유율)와 C#(닷넷 지원 킹왕짱) 같은 경쟁 솔루션이 너무 쟁쟁한테 비주얼 베이직 프로그래머 수요가 국내에 얼마나 되는지는 잘 모르겠다. 그나저나 ASP도 비베와 비슷한 문법인 걸로 아는데 그건 살아 있나?
또한 비베가 .NET 으로 바뀌면서, 기존 Office와 Visual Studio IDE에서 제공되던 VBA 매크로 언어까지 반쯤 낙동강 오리알 레거시로 전락한 것도 좀 아쉬운 점이다. 덕분에 Visual Studio 201x 최신 IDE는 지금도 제대로 된 키/스크립트 기반 매크로가 없는 걸로 본인은 기억한다.

이런 비주얼 베이직과 달리 C/C++ 컴파일러 라인은 원래 IDE 같은 게 없다 보니 도스/Windows 플랫폼은 그리 타지 않았다. C/C++은 베이직과는 완전히 다른 저수준 고성능 시스템 프로그래밍 언어이지 않던가? Windows는 NT 이전엔 애초에 자체적인 명령 프롬프트라는 게 없던 물건이었고, C 컴파일러는 도스 환경에서 스위치만 바꿔서 도스뿐만 아니라 Windows, 그리고 그 당시 중요한 플랫폼이던 OS/2용 프로그램을 크로스 컴파일했다.

그러다 1990년대 초에 이쪽은 C++ 언어 추가 → MFC 도입 → MS C/C++ 8.0 대신 Visual C++ 1.0으로 명칭 변경 같은 중요한 사건을 겪었으며, 리소스 편집기와 간단한 소스 코드 에디터가 16비트 Windows용으로 나왔다.
그리고 1993년, Windows NT가 출시되면서 NT용 32비트 Visual C++ 1.0이 별도로 나왔지만 이때는 NT는 시장 점유율이 아주 미미했으니 별 재미를 못 봤다.

그 뒤 1993~94년 사이에 Visual C++은 16비트와 32비트가 서로 약간 엇갈린 길을 갔다. 16비트용은 1.5 ~ 1.52c가 나온 뒤 지원이 중단됐고, 32비트용으로는 2.0이 나왔다. 하지만 아직 Windows 95도 없던 시절에 NT밖에 지원하지 않는 32비트용 VC++ 2는 정말 존재감이 없다. 이 32비트 바이너리를 Windows 3.1에서도 아쉬운 대로 돌릴 수 있게 하기 위해 Win32s라는 런타임이 이 시기에 개발되기 시작했는데, 얘 역시 본격적으로 이름이 부각된 건 Windows 95가 나온 뒤부터였다. 요컨대 Win32s는 95의 등장 이전부터 NT 3.1과 오리지널 3.1 사이의 gap을 메우기 위해 존재해 왔던 물건이다.

그 뒤, Windows 95가 나오고 1995년 말에 출시된 Visual C++ 4가 대박을 치면서 마소의 개발툴이 볼랜드 같은 타사 컴파일러를 슬슬 제치기 시작했다. Developer Studio라는 통합 IDE도 이때 처음으로 등장했다(텍스트 에디터, 리소스 에디터, 디버거, 빌드 툴, 도움말 레퍼런스 모두 한데 통합). VC4 시절에는 UI상으로 생뚱맞게도 맥용 크로스 컴파일이 있었던 모양이나, 본인이 직접 써 본 적은 없다.

이 당시에는 지금 같은 인터넷 기반 제품 업데이트가 없다 보니 소숫점 첫째나 둘째 자리가 0이 아닌 제품 버전을 심심찮게 볼 수 있었다. Win32s는 Visual C++ 4.1까지 지원되다가 96년 가을에 출시된 4.2에서부터 지원이 중단됐다. 설치할 때부터 "이 버전부터는 Win32s를 지원하지 않으니 이걸 타겟으로 개발하려면 구버전을 쓰고 이건 설치하지 마세요"라고 확인 질문이 뜬다.

비베는 4.0에서야 32비트 에디션이 등장하고 16비트와 32비트가 공존했던 반면, C++은 진작부터 32비트가 존재했고 그 대신 Win32s라는 과도기를 거쳤다는 차이가 있다.
또한 비베는 21세기부터는 닷넷 기반 언어로 완전히 탈바꿈해 버린 반면, C++은 이전부터 위상이 위상이다 보니 닷넷의 공세에 영향을 받지 않있다. 차라리 C++/CLI 같은 파생형 확장이 나오면 나왔지, 네이티브 코드 개발 부분은 바뀐 게 없다.

비베는 5와 6에서 잠시 MS Office 97 기반 GUI 엔진을 사용했고, 닷넷 200x에서는 그 기반을 계승하여 Office XP 및 파생 변종 GUI를 사용했다. VC++의 4~6에서 쓰인 IDE는 MFC를 써서 Office와 비슷한 외형이 나오게 자체적으로 만든 GUI 엔진 기반이었다.
그러던 것이 Visual Studio 201x부터는 WPF 기반의 완전히 독자적인 고유한 GUI를 사용하여 오늘날에 이르고 있다. 버전이 올라갈 때마다 매번 외형을 바꾸던 것도 이제는 지쳤는지(?) 2013 이후쯤부터는 안 하고 있다.

Posted by 사무엘

2017/10/12 08:35 2017/10/12 08:35
, , , ,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1415

Trackback URL : http://moogi.new21.org/tc/trackback/1415

Comments List

  1. 비밀방문자 2017/10/19 04:10 # M/D Reply Permalink

    관리자만 볼 수 있는 댓글입니다.

    1. 사무엘 2017/10/19 12:46 # M/D Permalink

      임베디드나 그런 바닥은 기계의 수명이 다할 때까지 소프트웨어 업데이트/업그레이드란 없을 테니 그럴 수도 있을 것이다? 뭐, 인터넷 연결 같은 것도 없고 애초에 안정화돼서 잘 쓰던 프로그램만 죽도록 쓰면 될 테고.

Leave a comment

임시 파일 다루기

수 년 전에 회사에서 만들어 놨던 코드가 업무상 다시 필요해져서 새 컴퓨터에서 돌려 봤다. 빌드 과정에서는 별 문제가 없었고 실행도 잘 되는 듯했으나.. 데이터 내용을 파일로 잠시 직렬화 덤프한 뒤에 서버로 전송하는 부분이 동작하지 않고 있었다.
문제를 추적해 보니 개발 당시에는 전혀 볼 일이 없었던 엉뚱한 파일명이 내부에 생성된 것이 원인이었다.

그리고 최종적으로 밝혀진 근본 원인은 이러했다. tmpnam_s 함수가 Visual C++ 2015부터는 동작 방식이 싹 바뀌었기 때문이다.
원래 tmpnam은 \ 로 시작하는 파일명만 달랑 되돌렸다. 그러나 2015부터는 운영체제의 공인 임시 디렉터리까지 포함한 전체 경로를 되돌리게 됐다.
예전에는 tmpnam_s의 결과에다가 또 임시 파일 저장용 디렉터리를 붙이는 후처리를 해야 했으나 지금은 그럴 필요 없다. 문자열의 형태가 달라져 버렸으니 기존 코드는 당연히 오동작을 하게 된 것이다.

알고 보니 tmpnam은 Visual C++ 2015 문서의 breaking changes에도 응당 명시돼 있는 아이템이다. 난 보통은 이런 거 꼼꼼히 다 읽어보는 편인데 이 함수는 어쩌다 보니 놓쳤다.
breaking changes는 단순히 어떤 함수· 변수를 제거하거나 형태를 바꾸는 것들이 대부분이기 때문에 기존 코드에 대한 여파는 명백한 컴파일 경고· 에러나 링크 에러 형태로 드러나는 게 대부분이다. 하지만 외형의 변경 없이 내부 동작만 잠수함 패치되어서 동작이 달라지는 식의 변화는 드물다. 프로그램을 실제로 돌려 보기 전까지는 부작용을 알 수 없기 때문이다.

이 코드가 나중에 어디서 또 어떻게 쓰일지 알 수 없는 관계로, 결국은 tmpnam을 감싸는 함수를 만들어야 했다. 얘의 몸체는 #if _MSC_VER >= 1900 이냐 아니냐로 구분해서 어느 VC++에서나 동일한 결과가 나오게 조치를 취했다.
귀찮은 일을 겪긴 했지만 임시 파일이라는 건 십중팔구 전용 임시 디렉터리에다 잠시 만들었다가 지우는 게 바람직하다. 임시 파일과 임시 디렉터리는 마치 바늘과 실처럼, 정수 나눗셈에서 몫과 나머지만큼이나 서로 따라다니는 명칭인 셈이다. 그러니 VC++ 2015에서의 변화는 궁극적으로는 긍정적인 변화이다.

프로그램을 개발하다 보면 임시 파일을 만들어야 할 때가 있다. 하긴, 옛날에 컴퓨터에 메모리가 아주 부족하던 시절에는 페이지 스왑 파일도 임시 파일의 범주에 들었는데 이건 아무래도 응용 프로그램 개발자가 직접 건드리는 파일은 아니다. 디렉터리 이름으로 TEMP라는 명칭을 본인이 최초로 본 게 아래아한글 2.0의 임시 파일 디렉터리였다.
디렉터리 트리 구조, 글꼴 캐시 파일 같은 건 없어도 실행에 지장은 없지만 그래도 반영구적으로 보관하고 참조하라고 만들어진 임시 파일이라는 점에서 성격과 용도가 약간 다르다.

이 정도로 저수준 시스템스러운 것이 아니더라도 특정 API나 기능에 접근하기 위해서, 입력 데이터를 반드시 파일 형태로 줘야 할 때 임시 파일을 만들게 된다. <날개셋> 한글 입력기의 경우 내부적으로 <날개셋> 변환기를 잠시 호출해서 구버전 입력 설정 파일을 변환할 때, 키보드 드라이버 관련 레지스트리 값을 변경하기 위해 레지스트리 편집기를 호출할 때 이런 테크닉을 쓴다.

tmpnam 같은 C 표준 함수 말고 운영체제 API에도 임시 파일과 디렉터리 이름을 얻어 오는 함수가 존재한다.
먼저 디렉터리는... 무슨 C:\asfa\zfdaaf 이렇게 무슨 악성 코드마냥 임의로 생성해서 쓰는 건 아니고, '내 문서', 'Program Files'처럼 임시 파일들의 생성과 보관을 위한 known 위치가 각 사용자 계정별로 따로 있다. GetTempPath 함수를 호출하면 이 위치를 얻어 올 수 있다. 하긴, 사용자 계정이라는 개념이 없던 시절엔 위치가 무슨 시스템 디렉터리처럼 쿨하게 Windows\temp이긴 했었다.

임시 디렉터리는 모든 프로그램들이 한데 공유하는 일종의 공공장소이다. 그래서 임시 파일을 많이 생성하는 프로그램이라면 그 디렉터리 밑에다가 자기 회사나 제품명으로 디렉터리를 또 만들어서 거기에다 파일을 저장하기도 한다. 그 정도로 복잡한 일을 하는 프로그램이 얼마나 될지는 모르겠지만 말이다. 참고로 <날개셋> 한글 입력기는 일부 기능에서 끽해야 파일 하나만 달랑 만들었다가 곧장 지우며, 임시 파일의 생존 주기가 함수 하나의 실행 주기를 벗어나지 않는다.

그럼 디렉터리 다음으로 파일 이름을 구체적으로 어떻게 지을지가 문제로 남는다. 무작위하게 이름을 붙이되, 그게 이미 있는 파일과 겹치지 않는다는 게 보장되어야 한다. 굳이 다른 프로그램이 아니어도 나 자신도 여러 인스턴스 형태로 동시에 실행될 수 있기 때문이다.
그렇기 때문에 임시 파일의 이름은 "자기 고유 명칭 + 숫자"의 형태로 붙곤 한다. 그래서 이 이름의 파일이 이미 존재하면 중복이 없을 때까지 숫자를 1식 증가시켜서 다시 시도한다.

GetTempFileName 함수가 정확하게 이런 일을 한다. 본인은 이 함수의 존재를 알기 전에 저 알고리즘을 수동으로 구현해서 임시 파일 이름을 생성했는데, 나중에 전용 함수에 대해 알게 되자 적지 않게 놀랐다.
이 함수는 '자기 고유 명칭'에 해당하는 접두사를 딱 세 글자 길이까지 받는다. 그 뒤 번호를 인자로 받는데, 유니크한 임시 파일 이름을 생성하는 게 목적이라면 번호는 그냥 0으로 주면 된다. 그러면 생성된 번호를 리턴값으로 돌려주며, 그 이름의 텅 빈 0바이트 파일을 실제로 생성도 해서 '찜'해 준다. 파일 이름을 얻고 파일을 여는 그 짧은 순간에도 혹시나 다른 프로세스나 스레드가 이 이름을 새치기로 찜하지 못하게 하기 위해서이다. 철두철미한 놈..;;

혹시 한 프로그램이 생성해 놓은 임시 파일을 다른 프로그램이 참조해야 한다면 참조하는 프로그램에다가 저 무작위하게 생성된 번호만 전해 주면 된다. 그럼 거기서는 GetTempFileName에다 동일한 접두사와 동일한 디렉터리를 넘기되, 번호는 0이 아니라 외부로부터 받은 그 값을 주면 그 임시 파일의 전체 경로와 이름을 얻을 수 있다.

지금도 어느 컴퓨터에서든 Users\계정명\AppData\Local|Temp 디렉터리에 가 보면 수백· 수천 개의 정체를 알 수 없는 임시 파일들을 볼 수 있다. 특히 "3글자 + 4자리 16진수.tmp"인 파일들은 100% GetTempFileName 함수에 의해 작명된 파일이다. 심지어 Visual C++도 실행해서 프로젝트를 열어 놓은 중에는 edgXXXX.tmp라는 수십 MB에 달하는 임시 파일을 여기에다 만들어서 사용하더라. 저건 Edison Design Group의 이니셜이니 인텔리센스 컴파일러가 사용하는 듯. IDE를 종료하면 물론 지워지고 없어진다.

GetTempFileName는 임시 파일 이름을 생성하는 것과 이미 생성된 명칭을 얻는 것이 모두 가능하며 나름 편리하게 잘 만들어져 있긴 하다. 다만, 파일의 확장자 지정이 안 되고 언제나 tmp로 고정되는 건 약간 불편하다.
(1) 임시 파일을 이름을 무작위 생성해서 파일도 새로 생성하기 또는 (2) 이미 있는 파일을 이름부터 id로부터 얻어 와서 열기 이건 일종의 정형화된 패턴이 있어서 본인은 클래스를 만들어서 사용하고 있다.

이 클래스의 소멸자는 임시 파일을 삭제도 해 준다. 임시 파일의 처리가 별도의 스레드에서 행해진다면 클래스 개체를 스택이 아닌 heap에다 new로 선언해서 개체의 delete 처리를 스레드 함수에게 시키면 된다. 뭐, 별도의 프로세스라면 내가 delete를 해서는 안 될 것이고.
삭제를 제대로 안 해 주면 이것도 일종의 메모리 leak 같은 부작용을 야기할 것이다. 시간이 흐를수록 임시 파일 디렉터리는 수천 개의 쓰레기들이 쌓여서 난장판이 될 테니 말이다. 요즘이야 하드디스크가 용량이 워낙 방대하니 디스크 용량 고갈보다는 파일 관리 성능· 효율 저하 문제가 더 크게 와 닿을 것으로 보인다.

이상. 이렇듯, 디스크의 파일은 메모리와는 달리 기록 효과가 영구적이며, 모든 프로세스에서 32/64비트도 가리지 않고 동일하게 공유 가능하기 때문에 프로세스 간의 데이터 공유와 통신 수단으로도 쓰일 수 있다.
단, 프로세스 사이의 통신 수단으로는 WM_COPYDATA라는 아주 유용한 물건도 있다. 그렇기 때문에 두 프로그램이 모두 윈도우를 생성해 있고 그 창의 주소를 알고 있다면 굳이 임시 파일을 만들었다가 지울 필요 없이 메시지만 주고받아도 된다.

<날개셋> 편집기와 입력 패드는 자기 프로그램이 중복 실행되었을 때 자기가 받아서 갖고 있던 명령줄을 기존 인스턴스에다가 넘겨 주기만 하고 자신은 실행을 종료하는 기능이 있다. 파일을 여는 등의 작업 요청은 기존 인스턴스가 받아서 대신 수행하게 된다. 예전에는 커스텀 메시지 + 임시 파일을 이용해서 명령줄을 전달했으나, 근래에는 훨씬 더 간편한 WM_COPYDATA 기반으로 구현 형태를 변경했다. 왜 진작부터 이 메시지를 안 썼나 모르겠다.

단, 명령줄을 자신의 타 인스턴스로 전달할 때 주의해야 할 점이 있다. 사용자가 명령줄로 전달하는 건 대체로 파일과 경로이다. 이게 절대경로인 경우는 흔치 않으니, 나의 current directory도 같이 전해서 저 경로가 무엇에 대한 상대경로인지를 알 수 있게 해야 한다. 안 그러면 내 쪽에서는 찾을 수 있는 파일을 명령줄을 받는 기존 인스턴스에서는 못 찾게 될 수도 있다. current directory는 프로세스 단위로 고유하게 갖고 있는 상태 정보이다.

Posted by 사무엘

2017/03/30 08:39 2017/03/30 08:39
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1344

Trackback URL : http://moogi.new21.org/tc/trackback/1344

Leave a comment

<날개셋> 한글 입력기는 잘 알다시피 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

Trackback URL : http://moogi.new21.org/tc/trackback/1312

Leave a comment

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

Trackback URL : http://moogi.new21.org/tc/trackback/1301

Comments List

  1. Lyn 2016/12/03 19:30 # M/D Reply Permalink

    MS는 레거시 API 없애고 UWP 로 통일하고 싶어하나봐요 ...

    1. 사무엘 2016/12/04 00:46 # M/D Permalink

      걔네들 생각 같아서는 아마 그러고도 남겠죠~ ㅎㅎ

Leave a comment

Windows 10 이야기

1. 메트로 앱

Windows 10이 나온 지 1년이 좀 넘었고, 마소에서 그 1년간 시행하던 사상 초유의 OS 메이저 버전간의 무료 업그레이드 기간도 끝났다.
처음부터 Windows 7 이하의 구형 OS를 쓰고 있었고 컴의 사양도 빠듯하다면 모를까, 8.1을 쓰는 중에 10으로는 업그레이드를 마다할 이유가 확실히 전혀 없다고 여겨진다.

잘 알다시피 시작 메뉴와 메트로 앱이 쓸데없이 전체 화면을 점유하는 게 아니라 창 형태로 실행 가능해진 것은 아주 환영할 만한 변화이다. 왜 진작에 이렇게 안 만들었나 모르겠다.
결국 PC용 Windows의 입장에서는 재래식 데스크톱 UI뿐만 아니라 외형이 뭔가 flat하고 modern하고 stylish(?)하고, 모바일에 친화적이고 보안 제약이 강하게 걸린 UI 모드가 하나 더 생긴 셈이다. 마소에서는 그걸 최종적으로 Universal Windows app이라고 이름을 붙였으며 같은 기능을 하는 프로그램들을 이 형태로도 여럿 만들었다. 대표적인 게 Edge 브라우저이고.

하지만 개인적으로는 같은 기능을 하는 프로그램이 두 버전으로 중복 구현돼 있는 게 별로 마음에 안 든다. 특히 제어판도 기존 제어판에 덧붙여 '설정'이라는 메트로 앱과 이중 구도로 바뀌었다. 화면 해상도를 바꾸는 기능과 DPI를 바꾸는 기능만 해도 데스크톱 버전으로 갔다가 메트로 버전으로 갔다가 하면서 찾는 등 좀 혼란스러워진 느낌이다.

데스크톱 UI는 전통적으로 키보드가 주류이고 마우스가 옵션인 구도이다. 그리고 640*480 내지 800*600처럼 지금으로서는 상상도 할 수 없는 열악한 저해상도 디스플레이와 비트맵 글꼴 환경에서 시작해서 차근차근 발전해 왔다. 그렇기 때문에 글자 크기도 전통적으로 작은 편이다. 사실, 업무 환경에서는 한 화면에서 작은 글씨로 정보가 많이 표시되는 것도 중요하기도 하니까.

그러나 메트로 UI는 그런 레거시 배경이 없으며, 반대로 터치스크린을 염두에 두고 있기 때문에 각종 글자나 GUI 위젯이 큼직하다. 키보드를 배려한 지저분한 focus rectangle 점선이나 액셀러레이터 문자 밑줄이 없다. 사실 마소는 데스크톱 UI에서도 진작부터 저걸 시각적으로 지저분하다고 인지했다. 하지만 그걸 대놓고 없애 버릴 수는 없으니, 고육지책으로 마우스만 사용할 때는 저걸 기본적으로 표시하지 않으려고 세심한 신경을 썼다. WM_UPDATEUISTATE 같은 메시지가 추가된 건 무려 Windows 2000 시절부터이다.

과거에 닷넷이 C++보다 생산성이 더 뛰어나고 단순 바이너리 레벨에서의 API 통합 규격인 COM보다 규모가 더 큰 언어 통합 바이트코드 실행 환경을 추구했다면, 메트로는 PC와 모바일 기기간의 통합 UI를 추구했다고 볼 수 있다. 메트로와 닷넷은 큰 관련이 없으며 메트로 앱도 C++ 네이티브 코드 기반으로 얼마든지 만들 수 있다는 게 의외의 면모이다.

하지만 난 컴퓨터에서는 걍 데스크톱 앱만 있는 게 좋다. 모니터에 가로/세로 피벗 기능이 있는 건 봤어도 멀티터치 기능이 있는 건 난 지금까지 한 번도 못 봤다. 정작 멀티터치 API 자체는 Windows 7부터 도입됐는데도 말이다. 멀티터치는 문자 입력과도 밀접한 관계가 있는 인터페이스임에도 불구하고 날개셋 역시 그쪽 지원은 전무하다. 지원되는 기기를 지금까지 전혀 못 봤고, 고로 지원할 필요를 못 느껴서.
터치스크린은 호주머니에 넣고 들고 다니는 기기만으로 족하지, 커다란 모니터에다가 지저분한 지문 묻히고 싶지는 않더라.

2. 에디트 컨트롤

아 그나저나 굉장히 뜻밖인 점인데, Windows 10은 에디트 컨트롤이 내부적으로 대대적인 리모델링을 거쳤는지 메모장이 수~10수MB에 달하는 파일을 순식간에 읽고 편집할 수 있게 됐다. 아주 최근에야 알았다. 직전의 8.1만 해도 안 이랬는데.
Windows에서 에디트 컨트롤은 전통적으로 단일 버퍼 기반이기 때문에 아주 큰 파일을 읽은 뒤 맨 앞부분에서 글자를 삽입하거나 지우면 랙이 장난 아니게 발생했다. 평생 영원히 안 고쳐질 줄 알았는데.. 이건 뜻밖의 긍정적인 변화가 아닐 수 없다.

먼 옛날, Windows 9x에서 NT로 넘어가면서 일단 황당한 64KB 제약은 없어졌다. 하지만 2000/XP급에서도 16비트 기준에 맞춰졌던 비효율적인 내부 알고리즘은 여전했기 때문에 메모장이 편집할 수 있는 실질적인 파일 크기는 겨우 몇백KB 수준에 머물러 있었다. 그게 Windows 10에 와서야 완전히 개선돼서 한계가 없어졌다. 참 오래도 걸렸다.

3. 마우스 휠의 적용 대상

마우스 포인터의 움직임이나 버튼 누름 메시지는 너무 당연한 말이지만 포인터의 바로 아래에 깔려 있는 윈도우로 전달된다.
그러나 휠 굴림 메시지는 사정이 약간 다르다. 맥 OS는 여전히 바로 아래의 윈도우로 전달되는 반면, Windows는 전통적으로 키보드 포커스를 받고 있는 윈도우로 전달되곤 했다.

그랬는데.. Windows 10에서는 휠 메시지 전달을 어느 방식으로 할지를 지정할 수 있다. 내가 본 기억이 맞다면, 제어판의 마우스 카테고리엔 없고, '설정'이라는 메트로 앱으로 가야 한다.
콤보 박스에서 drop list는 열지 않고 키보드 포커스만 갖다 놓은 뒤 휠을 굴렸는데 콤보 박스의 selection이 바뀌지 않아서 마우스에 문제가 생겼나 의아해했는데 사실은 이렇게 동작이 바뀌었기 때문이었다.

둘을 절충해서 일단 마우스 포인터가 놓인 창부터 먼저 고려하되, 그 창에 스크롤 바 같은 게 없어서 휠에 반응할 여지가 없으면 그 다음 순위로 키보드 포커스가 있는 창을 스크롤 시키는 것도 괜찮지 않을까 싶다.

4. 두벌식/세벌식 전환

세벌식 자판 사용자에게는 참 난감한 일이지만, Windows라는 운영체제는 기본 한글 IME에서 두벌식/세벌식을 전환하는 절차가 버전업을 거칠수록 더욱 복잡해져 왔다.

  • 98/2000/ME: 이때가 제일 나았음. 한영 상태 버튼을 우클릭했을 때 나오는 메뉴에서 글자판을 바로 고를 수 있었다.
  • 95: 한영 상태 버튼 우클릭 메뉴에서 '환경설정' 대화상자를 꺼낼 수 있었고, 거기서 글자판을 고르면 됐다.
  • Windows XP/Vista/7: 우클릭 메뉴에서 "텍스트 서비스 및 입력 언어" 대화상자를 꺼낸 뒤, 거기서 한 단계 거쳐야 MS 한글 IME의 환경설정 대화상자를 열 수 있다. 즉, 예전보다 한 단계 더 거쳐야 글자판을 바꿀 수 있다.
  • Windows 8 ~ 10: IME 브랜드 아이콘을 클릭 후 맨 아래의 '설정'을 고른 뒤, '한국어'를 골라야 MS 한글 IME를 찾을 수 있고, 거기서 또 '옵션'을 클릭하면 환경설정 대화상자를 열 수 있다. 이제는 두 단계를 거쳐야 된다.

요약하자면 XP 시절에 TSF라는 체계가 추가되면서 글자판 전환 절차가 급 까다로워졌으며, 8~10에서는 더 번거로워졌다.
사실 이건 TSF 자체의 문제는 아니다. MS 한글 IME가 옛날과는 달리 자체적으로 글쇠배열을 간편하게 전환하는 버튼이나 메뉴를 제공하지 않는 바람에, 운영체제 제어판 애플릿을 일일이 꺼내야 하는 구조가 된 것이 근본 원인이다. 마소에서는 두벌식/세벌식 전환을 꼭 그렇게까지 기능을 노출해 줄 필요가 있을 정도로 자주 행해지는 동작은 아니라고 판단한 것이다..;;

어쨌든 이런 이유로 인해 Windows 10 시절에도 본인의 세벌식 파워업 프로그램에 대한 수요는 없어지지 않고 있다.
사용자 차원에서 글쇠배열 전환 절차는 복잡한 편이지만, 그래도 Windows Vista 이래로 마소에서는 내부적인 두세벌 정보 저장 방식은 쓸데없이 이랬다 저랬다 바꾸지 않고 있다. 그 덕분에 거의 10여 년간 세벌식 파워업 프로그램도 핵심적인 동작 알고리즘이 크게 바뀔 필요는 없었다.

5. 프로그램 외형

Windows 10은 데스크톱 앱의 창 껍데기가 알다시피 전반적으로 하얗게 밝은 회색 계열로 바뀌었다. 8 시절에는 non-client 영역의 두꺼운 테두리가 배경 그림의 분위기에 맞춰 형형색색으로 바뀌곤 했는데 그건 없어졌다.
Visual Studio와 Office도 최신 버전이 다 그런 색으로 바뀐 걸 보면 이게 2010년대 마소의 디자인 트렌드인 듯하다. 다만, 활성화된 창과 비활성화된 창이 껍데기나 제목 표시줄에 배경색의 차이가 서로 전혀 없고 글자색만 살짝 달라지는 건 좀 아쉬움으로 남는다. 상태를 분간하기 어려워서다.

어쩌면 저 디자인이 마소가 데스크톱 앱에다 선보이는 마지막 디자인인가 하는 생각도 든다.
1990년대부터 2000년대까지 마소는 운영체제와 VS, 오피스 공히, 메이저 버전이 바뀔 때마다 프로그램 비주얼과 아이콘을 왕창 뜯어고치는 게 유행이었다. 맥OS 진영에서는 상상도 못 할 일..;;

그런데 그 관행이 이제 약발이 다해 가나 보다.
VS 2013과 2015, 오피스 2013과 2015는 웬일로 비주얼이 큰 차이가 없고 프로그램들 아이콘도 바뀌지 않았다. 마소 제품들에서 전반적으로 발견되는 추세이다.
심지어 미플이라든가 IE는 잘 알다시피 개발을 중단하고 유사 기능의 메트로 앱으로 대체한다는 선언까지 된 상태이다. 진작에 개발이 중단되어 명맥만 유지되고 있는 Html Help를 보는 듯한 느낌이다.

그래도 또 2010년대 후반이나 2020년대로 가면 프로그램 외형이 또 어떻게 바뀔지 알 수 없는 노릇이다. 머리를 쥐어짜면서 미래를 개척한다는 것 참 힘든 일이다.

6. 도움말

Windows 10은 로컬 도움말이란 게 사실상 완전히 없어졌는가 보다.
메모장 같은 기본 제공 프로그램에서 F1을 누르면 HTML 도움말이 뜨지도 않고 자기네들이 또 따로 만든 도움말 창이 뜨지도 않고 그냥 Edge 브라우저로 웹사이트 기반 도움말만이 달랑 뜬다. 인터넷에 연결돼 있지 않으면 도움말을 열람할 수 없다. 도움말이 일체의 전용 프로그램이 없이 아예 이런 형태로 싹 바뀌어 버린 건 10이 처음인 듯하다.

덕분에 C:\Windows\Help 디렉터리를 보면 XP까지만 해도 예전엔 chm 파일들이 즐비했으며 웹페이지/플래시 기반의 신제품 데모 같은 볼거리도 있었다. 그러나 지금은 죄다 옛날 추억이 됐다.
PC 사용자들의 평균적인 컴퓨터 실력이 충분히 향상됐으니, 어차피 읽지도 않을 구질구질한 도움말들을 다 삭제한 건지는 모르겠다. 허나 Vista/7 때는 아예 '에니악'까지 소개하면서 컴맹을 대상으로 컴퓨터 기초를 일일이 소개하는 로컬 도움말이 있었는데 이건 너무 과격한 변화가 아닌가 싶다.

Posted by 사무엘

2016/09/10 08:32 2016/09/10 08:32
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1270

Trackback URL : http://moogi.new21.org/tc/trackback/1270

Leave a comment

1. C++의 new/delete 연산자

C++의 new와 delete 연산자에 대해서는 먼 옛날에 한번 글을 쓴 적이 있고,  연산자 오버로딩에 대해서 글을 쓸 때도 다룬 적이 있다.
new/delete 연산자는 메모리를 할당하고 해제하는 부분만 따로 떼어내서 operator new / opertor delete라는 함수를 내부적으로 호출하는 형태이며, 이건 클래스별로 오버로딩도 가능하다. 그리고 객체 하나에 대해서만 소멸자를 호출하는 일명 스칼라 new/delete와, 메모리 내부에 객체가 몇 개 있는지를 따로 관리하는 벡터 new[]/delete[]가 구분되어 있다는 점이 흥미롭다.

new는 메모리 할당이 실패할 경우 한 1990년대까지는 NULL을 되돌렸지만 요즘은 예외를 되돌리는 게 malloc과는 다른 점이라고 한다. 하긴, 요즘 세상에 메모리 할당 결과를 무슨 파일 열기처럼 일일이 NULL 체크하는 건 굉장히 남사스럽긴 하다.

1980년대의 완전 초창기, 한 터보 C++ 1.0 시절에는 벡터 delete의 경우, 원소 개수를 수동으로 써 주기까지 해야 했다고 한다. pt = new X[3] 다음에는 delete[3] pt처럼. 안 그래도 가비지 컬렉터도 없는데, 이건 너무 불편한 정도를 넘어 객체지향 언어의 기본적인 본분(?)조차 안 갖춰진 막장 행태로 여겨진지라 곧 시정됐다. 객체의 개수 정도는 언어 차원에서 메모리 내부에다 자동으로 관리하도록 말이다.

그런데 스칼라이건 벡터이건 메모리를 n바이트 할당하거나 해제하는 동작 자체는 서로 아무 차이가 없는데 operator new/delete와 operator new[]/delete[]가 따로 존재하는 이유는 난 여전히 잘 모르겠다.

new char[100]을 하면 operator new(100)이 호출되고, 생성자와 소멸자가 있는 new TwentyFour_byte_object[4]를 호출하면 x86 기준으로 24*4+4인 operator new[](100)이 호출된다.
operator new[]라고 해서 딱히 내가 할당해 준 메모리에 저장되는 객체의 개수나 크기를 알 수 있는 것도 아니다. 단지, new[]의 경우 내가 되돌려 준 메모리 바로 그 지점에 객체가 바로 저장되지는 않는다는 차이가 존재할 뿐이다. 맨 앞에는 오브젝트의 개수 4가 저장되기 때문.

즉 다시 말해 벡터 new[]는 operator new[]가 되돌린 포인터 값과, new operator[]를 호출한 호스트 쪽에서 받는 포인터 값에 미묘하게 차이가 생기며 서로 일치하지 않게 된다. 마치 다중 상속으로 인해서 this 포인터가 보정되는 것처럼 말이다.
그래도 스칼라/벡터 처리는 operator new/delete가 전혀 신경 쓸 필요가 없는 영역이며, 여전히 new/delete operator가 자동으로 하는 일일 뿐인데 그것 때문에 메모리 할당 계층 자체가 둘로 구분되어야 할 필요가 있는지는 여전히 개인적으로 의문이다.

그리고 하나 더.
operator new/delete는 오버로딩이 가능하다고 아까 얘기했었다.
global scope에서 오버로딩을 해서 오브젝트 전체의 메모리 할당 방식을 바꿀 수 있으며, new의 경우 추가적인 인자를 집어넣어서 placement new 같은 걸 만들 수도 있다. "메모리 할당에 대한 답은 정해져 있으니 너는 저 자리에다가 생성자만 호출해 주면 된다"처럼.. (근데 new와는 달리 delete는 왜 그게 가능하지 않은지 모르겠다만..)

global scope의 경우, Visual C++에서는 operator new/delete 하나만 오버로딩을 해도 new[], delete[] 같은 배열 선언까지도 메모리 할당과 해제는 저 new/delete 함수로 자동으로 넘어간다. 물론 new[]/delete[]까지 오버로딩을 하면 스칼라와 벡터의 메모리 요청 방식이 제각기 따로 놀게 된다.

그러나 클래스는 operator new/delete 하나만 오버로딩을 하면 그 개체의 배열에 대한 할당과 해제는 그 함수로 가지 않고 global 차원의 operator new[]/delete[]로 넘어간다.
이것도 표준에 규정된 동작 방식인지는 잘 모르겠다. 결정적으로 xcode에서는 global도 클래스일 때와 동일하게 동작하여 스칼라와 벡터 사이의 유도리가 동작하지 않았다.
메모리 할당이라는 기본적인 주제를 갖고도 C++은 내부 사연이 무척 복잡하다는 걸 알 수 있다.

2. trigraph

아래와 같은 코드는 보기와는 달리 컴파일되는 올바른 C/C++ 코드이다. 그리고 Foo()를 호출하면 화면에는 What| 이라는 문자열이 찍힌다.

void Foo()
??<
    printf( "What??!\n" );
??>

그 이유는 C/C++엔 trigraph라는 문자열 치환 규칙이 '일단' 표준으로 정의돼 있기 때문이다.
아스키 코드에서 Z 뒤에 나오는 4개의 글자 [ \ ] ^ 와, z 뒤에 나오는 4개의 글자 { | } ~, 그리고 #까지 총 9개의 글자는 ?? 로 시작하는 탈출문자를 통해 등가로 입력 가능하다.
이런 치환은 전처리기 차원에서 수행되는데, #define 매크로 치환과는 달리 일반 영역과 문자열 리터럴 안을 가리지 않고 무조건 수행된다. 그래서 문자열 리터럴 안에서 연속된 ?? 자체를 표현하려면 일부 ?를 \? 로 구분해 줘야 한다.

이런 게 들어간 이유엔 물론 까마득히 먼 역사적인 배경이 있다. 천공 카드던가 뭐던가, 저 문자를 한 글자 형태로 입력할 수 없는 프로그래밍 환경에서도 C언어를 지원하게 하기 위해서이다. 1950~70년대 컴퓨팅 환경을 겪은 적이 없는 본인 같은 사람으로서는 전혀 이해할 수 없는 환경이지만 말이다.
C(와 이거 호환성을 계승한 C++도)는 그만치 오래 된 옛날 레거시 언어인 것이다. 그리고 C는 그렇게도 암호 같은 기호 연산자들을 많이 제공하는 언어이지만 $ @처럼 전혀 사용하지 않는 문자도 여전히 있다.

오늘날 PC 기반 프로그래밍 환경에서 저런 trigraph는 전혀 필요 없어진 지 오래다. 그래서 Visual C++도 2008까지는 저걸 기본 지원했지만 2010부터는 '기본 지원하지는 않게' 바뀌었다. 이제 저 코드는 기본 옵션으로는 컴파일되지 않는다. /Zc:trigraphs 옵션을 추가로 지정해 줘야 한다.

C/C++ 코드를 가볍게 구문 분석해서 함수 블록 영역이나 변수 같은 걸 표시하는 IDE 엔진들은 대부분이 trigraph까지 고려해서 만들어지지는 않았다. 그러니 trigraph는 IDE가 사용하는 가벼운 컴파일러들을 교란시키고 혼동시킨다. 한편으로 이 테크닉은 소스 코드를 의도적으로 괴상하게 바꾸는 게 목표인 IOCCC 같은 데서는 오늘날까지 유용하게 쓰인다. 함수 선언을 void foo(a) int a; { } 이렇게 하는 게 옛날 원래의 K&R 스타일이었다고 하는데 그것만큼이나 trigraph도 옛날 유물이다.

차기 C/C++ 표준에서는 trigraph를 제거하자는 의견이 표준 위원회에서 제안되었다. 그런데 여기에 IBM이 적극적인 반대표를 던진 일화는 유명하다. 도대체 얼마나 케케묵은 옛날 코드들에 파묻혀 있으면 '지금은 곤란하다' 상태인지 궁금할 따름이다. 하지만 IBM 혼자서 대세를 거스르는 게 가능할지 역시 의문이다.

3. Visual C++ 2015의 CRT 리팩터링

도스 내지 16비트 시절에는 C/C++ 라이브러리를 DLL로 공유한다는 개념이 딱히 없었던 것 같다. 다음과 같은 이유에서다.

  • 도스의 경우, 근본적으로 DLL이나 덧실행 같은 걸 쉽게 운용할 수 있는 운영체제가 아니며,
  • 메모리 모델이 small부터 large, huge까지 다양하게 존재해서 코드를 한 기준으로 맞추기가 힘들고,
  • 옛날에는 C/C++ 라이브러리가 딱히 공유해야 할 정도로 덩치가 크지 않았음.
  • 예전 글에서 살펴 보았듯이, 16비트 Windows 시절엔 DLL이 각 프로세스마다 자신만의 고유한 기억장소를 갖고 있지도 않았음. 그러니 범시스템적인 DLL을 만드는 게 더욱 까다롭고 열악했다.

모든 프로세스들이 단일 주소 공간에서 돌아가긴 했겠지만, small/tiny 같은 64K 나부랭이 메모리 모델이 아닌 이상, sprintf 하나 호출을 위해서 코드/세그먼트 레지스터 값을 DLL 문맥으로 재설정을 해야 했을 것이고 그게 일종의 썽킹 오버헤드와 별 차이가 없었지 싶다. 마치 콜백 함수를 호출할 때처럼 말이다. 이러느니 그냥 해당 코드를 static link 하고 만다.

그 반면 32비트 운영체제인 Windows NT는 처음부터 CRT DLL을 갖춘 상태로 설계되었고, 그 개념이 Visual C++을 거쳐 Windows 9x에도 전래되었다. 1세대는 crtdll, msvcrt10/20/40이 난립하던 시절이고 2세대는 Visual C++ 4.2부터 6까지 사용되던 msvcrt, 그리고 3세대는 닷넷 이후로 msvcr71부터 msvcr120 (VC++ 2013)이다. 2005와 2008 (msvcr80과 90)은 잠시 매니페스트를 사용하기도 했으나 2010부터는 그 정책이 철회됐다.

그런데 매니페스트를 안 쓰다 보니 Visual C++의 버전이 올라갈 때마다 운영체제의 시스템 디렉터리는 온갖 msvcr??? DLL로 범람하는 폐단이 생겼고, 이에 대한 조치를 취해야 했다. C/C++ 라이브러리라는 게 생각보다 자주 바뀌면서 내부 바이너리 차원에서의 호환성이 종종 깨지곤 했다. 이런 변화는 함수 이름만 달랑 내놓으면 되는 C보다는 C++ 라이브러리 쪽이 더 심했다.

그 결과 Visual C++ 2015와 Windows 10에서는 앞으로 변할 일이 없는 인터페이스 부분과, 내부 바이너리 계층을 따로 분리하여 CRT DLL을 전면 리팩터링을 했다. 본인은 아직 이들 운영체제와 개발툴을 써 보지 않아서 자세한 건 모르겠는데 더 구체적인 내역을 살펴봐야겠다.

사실 C++ 라이브러리는 대부분의 인터페이스가 템플릿 형태이기 때문에 코드들이 전부 해당 바이너리에 static 링크된다. 하지만 그래도 모든 코드가 static인 건 아니다. 메모리 할당 내지 특정 타입에 대한 템플릿 specialization은 여전히 DLL 링크가 가능하다.
C++ 라이브러리가 어떤 식으로 DLL 링크되는지는 마치 함수 타입 decoration 방식만큼이나 그야말로 표준이 없고 구현체마다 제각각인 춘추전국시대의 영역이지 싶다.

4. Windows의 고해상도 DPI 관련 API

요즘이야 컴퓨터 화면의 해상도가 PC와 모바일을 가리지 않고 워낙 높아져서 프로그램의 UI 요소나 각종 아이콘, 그래픽도 크기 조절에 유연하게 대처 가능하게 만드는 게 필수 조건이 됐다. 폰트의 경우 저해상도에 최적화된 힌팅이 필요 없어질 거라는 전망까지 나온 지 오래다.
그러나 태초에 컴퓨터, 특히 IBM 호환 PC라는 건 텍스트 모드만 있다가 그래픽 모드라는 게 나중에 추가됐다. 그것도 그래픽 모드는 320*200이라는 막장급의 낮은 해상도에 4색짜리인 CGA에서 첫 시작을 했다.

시작은 심히 미약했다. 이런 저해상도 저성능 컴퓨터에서는 쑤제 도트 노가다로 최적화된 그래픽이나 비트맵 글꼴이 속도와 메모리 면에서 모두 우월했기 때문에 그게 세상을 평정했다.
그러나 컴퓨터 화면이 커지고 해상도가 크게 올라가면서 단순히 픽셀보다 더 고차원적인 단위를 도입할 필요가 생겼다. 물론 예나 지금이나 메뉴와 아이콘, 프로그램 제목 표시줄의 글자 크기는 제어판에서 간단히 고칠 수 있었지만 영향을 받는 건 오로지 그것뿐. 대화상자 같은 다른 요소들의 크기는 변하지 않았다.

그 고차원적인 단위를 일명 시스템 DPI라고 부른다.
평소에야 이 단위는 언제나 관례적으로 100%로 맞춰져 있었으며, 이게 125나 150% 같은 큰 값으로 맞춰져 있으면 응용 프로그램은 창이나 글자의 크기도 원칙적으로는 이에 비례해서 키워서 출력해야 한다.

대화상자는 픽셀이 아니라 내부적으로 DLU라는 추상적인 단위를 사용해서 컨트롤들을 배치하며 이 단위는 시스템 DPI를 이미 반영하여 산정된다. 하지만 CreateWindowEx를 써서 픽셀 단위로 컨트롤을 수동으로 생성하는 코드들이 이런 시스템 DPI를 고려하지 않고 동작한다면 프로그램의 외형이 많이 이상하게 찍히게 된다.

여기까지가 Windows 95부터 8까지 오랫동안 지속된 프로그래밍 트렌드이다. 시스템 DPI는 단순히 메뉴와 아이콘의 글자 크기와는 달리 운영체제 전체에 끼치는 여파가 매우 크다. 이건 값을 변경하려면 운영체제를 재시작하거나 최소한 모든 프로그램을 종료하고 현 사용자가 로그인을 다시 해야 했다.

시스템 DPI라는 개념 자체에 대한 대비가 안 된 프로그램도 널렸는데, 응용 프로그램들이 시스템 DPI의 실시간 변화에까지 대비하고 있기를 바라는 건 좀 무리였기 때문이다. 시스템 메트릭이 싹 바뀌기 때문에 이미 만들어져 있는 윈도우들이 다 재배치돼야 할 것이고 후유증이 너무 크다.

그런데 지난 Windows 8.1은 이 시스템 DPI에 대해서 또 어마어마한 손질을 가했다.
간단히 결론부터 말하자면 사용자가 재부팅 없이도 DPI를 막 변경할 수 있게 했다. 실행 중에 DPI가 변경되면 WM_DPICHANGED라는 새로운 메시지가 온다. 그리고 응용 프로그램은 자신이 실시간 DPI 변경에 대응 가능한지 여부를 운영체제에 별도의 API 내지 매니페스트 정보를 통해 지정 가능하게 했다.

DPI 변경에 대응 가능하지 않은 레거시 프로그램들은 시스템 DPI가 바뀌었는지 알지도 못하고 virtualize된 샌드박스 속에서 지낸다. DPI가 150%로 바뀌면서 사용자의 화면에 보이는 창 크기가 100에서 150으로 늘었지만, 응용 프로그램은 여전히 자신의 최대 크기가 100인줄로 안다. 그래서 100*100 크기로 그림을 찍으면 그건 운영체제에 의해 1.5배 비트맵 차원에서 크게 확대되어 출력된다.

그 프로그램은 처음부터 시스템이 150% DPI인 것을 알았으면 그에 맞춰 실행되었을 수도 있다. 그러나 실행 중의 DPI 변경까지 예상하지는 못하며, 그런 API가 도입되기 전에 개발되었기 때문에 운영체제가 그래픽 카드의 성능을 활용하여 그런 보정을 해 주는 것이다.
물론 이렇게 확대된 결과는 계단 현상만 뿌옇게 보정된 채 출력되기 때문에 화질이 좋지 못하다. 응용 프로그램이 고해상도 DPI 변화를 인식하여 직접 150*150으로 최적화된 그림을 다시 그리는 게 바람직하다.

그리고 시스템 DPI는 제어판 설정의 변경을 통해서만 바뀌는 게 아니다.
Windows 8.1부터는 모니터별로 시스템 DPI를 다르게 지정할 수 있다. 그래서 100%(96dpi)짜리 모니터에서 돌아가고 있던 프로그램 창을 125%(120dpi)짜리 커다란 모니터로 옮기면 거기서는 동일 프로그램이 그 DPI에 맞춰서 동작해야 한다. 물론 DPI가 바뀌었다는 메시지는 운영체제가 보내 준다.

이렇듯, 응용 프로그램은 처음에는 (1) 고해상도 DPI를 인식할 것만이 요구되었다가 나중에는 (2) 실행 중에 DPI가 변경되는 것에도 대비가 되어야 하는 것으로 요구 조건이 추가되었다.
옛날에는 시스템 전체의 화면 해상도나 색상수를 재부팅 없이 실시간으로 바꾸는 것도 보통일이 아니었는데 이제는 DPI의 변경도 그 범주에 속하게 되었다.

재부팅이 필요하다는 이유 때문에 그런지 Windows Vista는 전무후무하게 DPI의 변경에 마치 시스템의 시각 변경처럼 '관리자 권한' 딱지가 붙어 있기도 했는데 이것도 참 격세지감이다.

Posted by 사무엘

2016/06/02 08:32 2016/06/02 08:32
, , ,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1233

Trackback URL : http://moogi.new21.org/tc/trackback/1233

Comments List

  1. Dev 2016/06/03 12:07 # M/D Reply Permalink

    글 잘봤습니다.

    저도 1차원 배열에 대해 delete [] 이런식으로 해야하는지 의문을 가진적이 있어요
    char(1) 나 char(100)나 관리되는 방법에 차이가 없을거라 생각했었는데요ㅋ
    그치면 "1차원배열에 delete 만해도 된다" 라고 확인된바는 없다보니 정석으로 하고있지요 ㅎ
    연속되지 않은 공간에 배열을 할당 할 수도 있다고 하니 delete[] 를 반드시 써야한다고하는 글도 본적은 있습니다.

    오래전에 서진택님이 써놓은 자료를 보면 Borland C++은 DOS에서 new로 메모리 할당시
    할당한 메모리의 size까지 할당해서 앞에 기록하고 바로뒤의 포인터를 리턴한다고 본적이 있네요

    1. 사무엘 2016/06/03 13:22 # M/D Permalink

      delete operator의 형태야 스칼라형과 벡터형이 문법 차원에서 달라야 한다는 점에 저 역시 이의가 없답니다.
      소멸자 함수 호출하는 횟수 때문에 메모리 할당량뿐만 아니라 실제 원소 개수 정보도 필요해져서 저런 차이가 발생한 거지요. 앞부분에다 개수를 따로 저장하는 테크닉 정도는 도스용 Borland C++뿐만 아니라 요즘 컴파일러들도 동일하게 사용합니다.

      그에 반해 생성자 소멸자나 가상함수 따위가 없는 built-in type이야 스칼라/벡터 delete 뭘 쓰나 딱히 유의미한 차이가 없긴 합니다. C/C++은 강타입+정적 스코핑이기 때문에 그 정도 타입 판단은 컴파일 타임 때 가능하니까요.

Leave a comment

Windows에서 C/C++ 언어로 EXE를 만들 때는 시작점으로 WinMain이라는 함수가 쓰인다.
얘는 먼 옛날 16비트 시절과, 지금의 32/64비트 사이에 바뀐 게 거의 없다. HINSTANCE hInst, HINSTANCE hPrevInst, PSTR pszCmdLine, int nCmdShow 라는 네 종류의 인자 중에서 32비트로 오면서 바뀐 것은 hPrevInst이 언제나 NULL이라는 것밖에 없다. 그것도 과거에는 복잡하던 게 더 간결해진 변화이기 때문에 실질적으로 신경 쓸 필요가 없다.

옛날 16비트 시절에 HINSTANCE는 파일 차원에서 동일한 프로그램이 중복 실행되었을 때 각 실행 문맥을 구분하는 일종의 메모리 번호표였다. 한 프로그램이 완전히 처음 실행될 때는 hPrevInst가 NULL인데 두 번째 실행되면, 먼저 실행된 프로그램이 받았던 hInstance가 다음 인스턴스의 WinMain 함수에서 hPrevInst로 전달되고..
세 번째 중첩 실행되면 아까 그 두 번째 프로그램의 신규 핸들이 거기의 hPrevInst로 전달되는 형태였다. 단일 방향 연결 리스트의 head 노드 같은 느낌이다.
자기 자신 말고는 주변에 무엇이 있는지 일부러 특수한 API를 써서 조회를 하지 않으면 도무지 알 수 없는 32비트 이상 보호 모드에서는 정말 상상하기 힘든 관행이다.

EXE는 그렇고 그럼 DLL은 어떨까? DllMain이라는 기본적인 형태는 동일하지만 16비트 시절에는 아무래도 멀티스레드 같은 건 존재하지 않았으니까 DLL_PROCESS_(ATTACH/DETACH)만 있었고, 나중에 DLL_THREAD_*가 추가된 정도일까?

사실은 그렇지 않다.
옛날에는 BOOL DllMain(HINSTANCE hInst, DWORD fdwReason, PVOID pReserved)라는 형태의 함수 자체가 없었다.
그 대신 완전히 다른 int FAR PASCAL LibMain(HANDLE hInst, WORD wDataSeg, WORD wHeapSize, LPSTR lpszCmdLine) 라는 함수가 있었으며, DLL이 처음 로드되었을 때에 이게 한 번만 호출되곤 했다.

16비트 시절에 DLL은 프로세스 독립성이 보장되지 않았다.
지금이야 B.DLL을 사용하는 A.EXE가 두 번 중첩 실행되면 두 인스턴스에 대해서 B.DLL이 제각각 로드되어 DLL_PROCESS_ATTACH가 오지만..
옛날에는 A.EXE가 중첩 실행되었더라도 B.DLL에서 LibMain은 첫 로딩될 때 한 번만 실행되었다. 그리고 자신이 A의 두 번째 인스턴스에 의해 중첩 로드되었다는 사실을 알 길이 없었다. A가 B.DLL에 별도로 정의되어 있는 초기화 함수 같은 것을 호출하지 않는다면 말이다.

LibMain 함수의 인자를 살펴보면, 첫 인자는 자기 자신을 식별하는 인스턴스 핸들이다.
하지만 16비트 시절에는 DLL은 중첩 로딩이 되지 않고 자신의 전역변수 값이 모든 프로그램에서 공유되었다. 그렇기 때문에 저 값은 EXE의 WinMain에서 전달되는 인스턴스 핸들과는 달리 딱히 변별성은 없었을 것이다. 시스템 전체를 통틀어 같은 값이 들어왔으리라 생각된다.

그 다음 wDataSeg와 wHeapSize는 딱 보기만 해도 16비트스러운 암울한 값이다. 이게 어떤 의미를 갖고 이것으로 무엇을 할 수 있는지 잘 모르겠다.
데이터 세그먼트(DS) 레지스터 값은 뭐 어쩌라는 건지 잘 모르겠지만 어쨌든 실행할 때마다 다른 값이 들어올 수는 있어 보인다. 그 반면 wHeapSize는 이 DLL을 빌드할 때 def 파일에다가 지정해 줬던 로컬 힙의 크기이다. 즉, 이 DLL이 지금 형태 그대로 존재하는 한 언제나 고정된 값이 넘어온다.

마지막으로 lpszCmdLine은 더욱 기괴하다. EXE도 아니고 DLL을 어떻게 인자를 줘서 로딩한단 말인가? LoadLibrary 함수에 인자를 전달하는 기능이 있지도 않은데 말이다. 호스트 EXE에 전달된 인자를 되돌리는 것도 아닌 듯하다. 실제로 거의 대부분의 경우 이 인자의 값은 어차피 그냥 NULL이라고 한다.

16비트 DLL의 첫 관문인 LibMain은 기괴한 점이 여기저기서 발견된다.
DLL에 배당되어 인자로 전달된 데이터 세그먼트는 앞으로 빈번하게 사용되는 것을 염두에 두고 메모리 상의 주소가 바뀌지 않게 lock이 걸린다고 한다. 운영체제는 아니고 컴파일러가 lock을 거는 코드를 기본적으로 추가해 넣는 듯하다.
그래서 옛날 소스 코드를 보니, 이유는 알 수 없지만 LibMain에 보통 이런 코드가 들어갔다고 한다.

if (wHeapSize > 0) UnlockData (0);

즉, 아직은 lock을 걸지 말고 도로 재배치 가능한 상태로 놔 두겠다는 뜻이다. 그리고 LockData/UnlockData는 Windows 3.1의 windows.h에 이렇게 매크로로 정의돼 있다.

#define LockData(dummy)     LockSegment((UINT)-1)
#define UnlockData(dummy)   UnlockSegment((UINT)-1)

옛날에는 (Un)LockSegment라는 함수가 있었다. 그리고 Windows 3.x보다도 더 옛날에는 (Un)LockData라는 함수도 별도로 있었는데, 용례가 간소화돼서 Data의 기능이 Segment로 흡수된 듯하다. (가상 메모리라는 게 없던 Windows 2.x 리얼 모드 시절의 잔재라고 함.) 그러니 Data는 레거시 호환을 위해 매크로로 바뀌고, 인자 역시 쓰이지 않는 dummy로 바뀐 것이다.
평소에는 특정 세그먼트 lock/unlock을 하는데, (UINT)-1을 주면 모든 영역을 그렇게 하는 것 같다. 어떤 경우든 wDataSeg의 값이 직접 쓰이지는 않는다.

LibMain은 초기화가 성공하면 1을 되돌리고 그렇지 않으면 0을 되돌려서 DLL의 로딩을 취소하게 돼 있었다. 이것은 오늘날의 DllMain과 동일한 점이다.
그럼 16비트 시절에는 시작 다음으로 DLL의 종료 시점을 감지하려면 어떻게 해야 했을까? EXE와는 달리 DLL은 main 함수의 종료가 곧 프로그램의 종료는 아니니까 말이다.
또한 16비트 시스템의 특성상 비록 매 프로세스의 종료 시점을 감지하는 건 불가능하겠지만, 그래도 아까 중복 실행되었던 A가 최후의 인스턴스까지 모두 종료되어서 B.DLL이 메모리에서 사라져야 하는 시점이 언젠가는 올 테니 말이다.

이것도 방법이 굉장히 기괴했다. DLL이 메모리에서 제거되기 전에 운영체제는 해당 DLL에서 'WEP'라는 이름을 가진 함수를 export 테이블에서 찾아서 그걸 호출해 줬다.

//16비트 시절에 _export는 오늘날의 __declspec(dllexport) 와 비슷한 단어임.
int FAR PASCAL _export WEP (int nExitCode);

이 함수 역시 성공하면 nonzero를 되돌리게 돼 있지만, 어차피 프로그램이 일방적으로 종료되는 상황에서 함수의 인자나 리턴값은 무시되다시피할 뿐 거의 의미가 없었다.
하다못해 오늘날 DllMain의 DLL_PROCESS_DETACH처럼 자신이 FreeLibrary에 의해 해제되는지, 프로세스의 종료에 의해 일괄 해제되는지라도 알 수 있으면 좋을 텐데 그 시절에 그런 정보를 바랄 수는 없었다.
참고로 WEP는 그냥 Windows Exit Procedure의 약자였다. -_-;;

이렇듯, 형태가 거의 바뀐 게 없는 WinMain과는 달리, DLL의 입구 함수는 16비트 시절과 지금이 달라도 너무 달라서 문화 충격이 느껴질 정도이다. 예전에도 16비트 Windows 프로그래밍에 대해서 글을 종종 쓰고 DLL에 대해서도 언급한 적이 있었는데 이런 내역에 대해서 정리한 적은 없었기 때문에 또 글을 남기게 됐다. 옛날에는 이렇게 불편한 환경에서 도대체 프로그램을 어떻게 만들었나 싶다.

LibMain과 WEP를 DllMain으로 통합한 것은 백 번 잘한 조치였다.
16/32비트 이식성을 염두에 둔 코드라면 DllMain에다가 LibMain과 WEP를 호출하고, 반대로 LibMain과 WEP에서 적절하게 서로 다른 인자를 줘서 DllMain을 호출하는 계층도 생각할 수 있으며, 과거에는 이런 관행이 실제로 존재했다고 한다. 마치 윈도우 프로시저와 대화상자 프로시저의 형태를 통합한 계층을 따로 만들어 썼듯이 말이다.

Posted by 사무엘

2016/05/27 08:38 2016/05/27 08:38
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1231

Trackback URL : http://moogi.new21.org/tc/trackback/1231

Leave a comment
« Previous : 1 : 2 : 3 : 4 : 5 : ... 10 : Next »

블로그 이미지

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

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

«   2018/10   »
  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:
1040666
Today:
171
Yesterday:
781