« Previous : 1 : 2 : 3 : 4 : Next »

1.
코코아, Win32 API, MFC 같은 플랫폼 종속적인 API를 전혀 쓰지 않고 순수하게 표준 C/C++ 라이브러리 함수만으로 백 엔드 엔진만 만들었다 해도 Windows + Visual C++로 작성한 코드가 안드로이드 내지 맥 같은 다른 플랫폼에서는 곧장 컴파일 되지 않거나, 빌드된 프로그램이 의도한 대로 동작하지 않을 수 있다.

개인적으로는 회사에서 wchar_t 때문에 굉장한 불편을 겪었다. 잘 알다시피 Windows에서는 이게 2바이트이지만 다른 플랫폼에서는 4바이트이다. 플랫폼을 불문하고 2바이트 문자 단위로 동작하는 strcpy, strcat, strlen, printf, atoi 등등은 직접 구현이라도 해야 하는지..? 특히 파일로 읽고 쓰려면 말이다.

C++ string 클래스야 typedef std::basic_string<unsigned short> string16; 부터 먼저 만들어 놓고 썼다지만 그렇게 처음부터 객체를 만드는 게 아니라 raw memory를 다루는 상황에서는 해결책이 되지 못한다.

이런 게 원초적인 애로사항이고 또, 소스 코드 내부에서 유니코드 문자열 상수를 표현하는 방식도 또 다른 난관이다.
언어가 제공하는 L"" 문법은 wchar_t형 기반이다. 그러니 wchar_t 말고 명시적으로 unsigned short 배열에다가는 문자열 상수를 쓸 수 없고 "가"를 { 0xac00, }로 표현하는 식의 삽질을 해야 한다.

거기에다 비주얼 C++은 C++ 소스 코드나 명령 프롬프트가 UTF8과 전혀 친화적이지 않다는 다른 문제점도 있어 더욱 불편하다. 유니코드가 등장하면서 플랫폼별로 문자열을 다루는 방식이 너무 심하게 파편화됐다는 생각이 든다.
문자열을 저장하고 메모리를 관리하는 방식이 난립하는 것 말고(string class!) 문자열을 구성하는 문자를 표현하는 방식 그 자체부터가 말이다.

2.
하루는 Visual C++에서 표준 C 함수만 사용해서 만들어 준 코드를 안드로이드 내지 맥 OS 플랫폼으로 넘겨 줬더니 컴파일 에러가 났다. wcslen 함수가 선언되지 않았다고 꼬장을 부리는데 도무지 원인을 알 수 없었다. strlen은 인식되는데 wcslen은 왜 인식이 안 되는 거지?

그런데 알고 보니 wcslen은 strlen과는 달리 string.h가 아니라 wchar.h에 선언되어 있었다.
Visual C++은 string과 wchar에 wcslen을 모두 선언해 줬지만 타 플랫폼은 그렇지 않았다. 흐음~ 나의 불찰이다.

malloc/free 함수는 stdlib.h에도 있고 malloc.h에도 있다.
memset/memcpy는 string.h에도 있고 memory.h에도 있다.
그런 예가 몇 가지 있는 건 알고 있었지만 wcs* 함수는 Visual C++에만 string/wchar 겸용으로 선언돼 있었던 듯하다.
C 인클루드 헤더는 한 함수가 오로지 한 헤더에만 유일하게 존재하지는 않기도 하다는 것이 흥미롭게 느껴진다.

3.
요즘 표준 라이브러리들의 헤더 파일을 보면 함수의 인자마다 타입과 이름만 있는 게 아니라 소스 코드 정적 분석을 위한 Annotation 정보가 같이 들어있다. 같은 포인터라 해도 이건 읽기 전용, 쓰기 전용.. 쓴다면 어떤 조건으로 얼마만지 써지는지(옆의 인자만큼~) 같은 거.

그래서 함수 하나만 봐도 선언도 정말 덕지덕지 길어졌다. 이 정보들이 처음부터 있지는 않았을 텐데, 그 수많은 API들의 선언에다 일일이 다 기입하는 건 완전 중노동이었을 것 같다.
한때는 정적 분석 기능은 개발툴의 유료 최상급(엔터프라이즈 같은) 에디션에서나 접근 가능한 고급 기능이었는데, 이것도 죄다 무료로 풀리는 듯하다. 유료 GUI 툴킷이 통째로 MFC에 들어갔듯이 말이다.

4.
요즘은 CPU 아키텍처야 x86 아니면 ARM만 살아 남아서 그런지, 이식성을 논할 때 비트 순서, 일명 endian-ness 얘기는 별로 안 나오는 것 같다. 우리 주변에서 흔히 볼 수 있는 x86은 요지부동 리틀 엔디언인 반면, 옛날에 매킨토시의 밑천이던 PowerPC는 빅 엔디언이었다. 트루타입 폰트 포맷이 빅 엔디언 기반인 건 이런 애플의 영향력이 닿아서 그랬던 걸까?

먼 옛날 대학 시절에 터미널에 원격 접속해서 거기서 C 컴파일러를 돌려 봤던 게 본인으로서는 빅 엔디언 컴퓨터를 직접 구경한 처음이자 마지막 경험이다. 큰 자릿수가 앞부분부터 저장되다니 굉장히 신기했다. 이건 앞으로 수동 변속기 차량이라든가 IA64 (Itanium) 컴퓨터만큼이나 앞으로 또 접할 일이 없는 초희귀템으로 남을 것 같다.

최신 CPU인 ARM은 하드웨어 차원에서 endian-ness를 모두 지원하기 때문에 아무 쪽으로든 취사 선택이 가능하다고 한다. 사람으로 치면 완벽한 양손잡이이고, 철도에다 비유하자면 좌측/우측통행 전용 복선 철도가 아니라 어느 쪽으로든 운용 가능한 단선병렬과 비슷한 격이다.
결국은 다 지원하는 것으로 가는구나. 한글 코드에서 조합형/완성형 논쟁, CPU 미시구조에서 CISC/RISC 논쟁, 리눅스에서 그놈/KDE 셸 논쟁도 다 비슷한 방식으로 종결됐듯이 말이다.

비트 순서 같은 하드웨어 특성을 타는 요소 말고 소프트웨어 플랫폼과 언어 차원에서.. 사소하지만 코드의 이식성을 은근히 저해하는 요소는 내 경험상 몇 가지 있었다. 그러니 GUI가 없고 특정 운영체제의 API를 사용하지 않았다고 해서 무작정 이식이 잘 될 거라고 기대할 수는 없다.

5.
당장 떠오르는 건, 64비트 상수를 나타내는 % 문자가 파편화돼 있다(%I64d, %lld). 그리고 long이 Windows에서는 64비트 플랫폼에서도 여전히 32비트이지만 타 플랫폼은 그렇지 않다. 그러니 이식성을 생각한다면, long은 파일 오프셋 계산에 영향을 주는 곳에서는 절대로 구조체 멤버로 쓰이거나 sizeof의 대상이 돼서는 안 된다. (앞서 논했던 char_t도 마찬가지이고!) 그런 데서는 정말 닥치고 int32, uint64처럼 비트수를 명시한 typedef를 쓰는 게 안전하다.

C#이나 Java, D는 아무래도 1990년대 중후반에 PC에서 32비트 CPU 정도는 확실하게 정착한 뒤에 등장한 최신 언어이다 보니, 32/64비트 플랫폼을 불문하고 long이 처음부터 일관되게 64비트였다. 하지만 C/C++은 그보다 훨씬 전부터 컴퓨터 하드웨어의 발전의 격변기와 동고동락했던 언어이다 보니, 저런 깔끔함을 기대할 수는 없는 노릇이 돼 있다.

그리고, fopen에다 주는 옵션에서 r/w/a (+)만 있고 b/t 모드가 지정되지 않았을 때..
Windows는 binary 모드로 동작하는 반면 맥에서는(타 플랫폼은 확인 안 해 봄) 디폴트가 text였다. 멀쩡한 코드가 완전 엉뚱하게 동작하고 파일이 쓰라는 대로 써지지 않고 읽으라는 대로 읽히지 않아서 한창 문제를 추적했더니.. 결국은 이런 데에서 차이가 있었다. 이것도 표준 규격이 정의돼 있지 않나 보다.

말이 나왔으니 말인데, Visual C++은 fopen조차 쓰지 말고 fopen_s를 쓰라고 권한다. printf_s, qsort_s 같은 *_s 물건은 안전하고 편리하긴 하지만 언제까지나 이식 불가능한 Visual C++만의 전유물로 남을지 궁금하다..

strdup와 _wcsdup는 표준처럼 생겼지만 진짜 표준인지 아닌지 알쏭달쏭한 놈이다. 앞에 괜히 밑줄이 있는 게 아니다. _wtoi 이런 것도 Windows를 벗어나면 컴파일되지 않을 가능성이 높은 지뢰이니 strtol, wcstol을 쓰는 게 안전하다.
strtok의 경우 Visual C++은 토큰 컨텍스트를 따로 받는 _s 버전을 추가한 반면, 타 플랫폼은 strtok, wcstok 함수 자체가 그렇게 고쳐진 것도 있다. 이런 것들도 너무 골치아프다.

6.
끝으로, 이건 이식성하고는 큰 관계가 없는 얘기다만..
형변환 연산자인 static_cast는 코드 생성 차원에서 하는 일이 전혀 없거나(base class* → derived_class*, enum → int), 뻔한 값 보정(float → int, char → int), 또는 다중 상속일 때는 컴파일 타임 때 결정된 고정된 상수만치 this 포인터 보정 정도만(derived_class_B* → base_class) 하는 걸로 으레 생각했다.

그런데 다중 상속을 다룰 때 꼭 그런 일만 하는 건 아니다. 포인터가 처음부터 NULL이었다면, 거기서 또 얼마를 뺄 게 아니라 cast된 포인터도 그냥 NULL을 주는 예외 처리를 해야 한다. 과연 생각해 보니 그렇다. 아래 코드를 생각해 보자.

struct A { int a,b; };

struct B { int c,d; };

struct C: public A, public B { int e,f; };

void foo(B *pm) { printf("Received %p\n", pm); }

int main()
{
    C m, *pm=&m;
    printf("Passing %p\n", pm); foo(pm);
    pm=NULL; printf("Passing %p\n", pm); foo(pm);
    return 0;
}

단일 상속과는 달리, 다중 상속에서 passing의 값과 received의 값이 서로 달라질 수 있다고 아는 것은 하나를 아는 것이다.
그러나 NULL일 때는 다중 상속이더라도 언제나 NULL이 유지된다는 것이 함정이다. 우와.. 지금까지 한 번도 그런 경우를 생각한 적이 없었는데.. 꽤 충격적이다. 간단하지만 다중 상속의 보이지 않는 오버헤드를 보여주는 요소 중 하나이다.

Posted by 사무엘

2016/05/13 08:29 2016/05/13 08:29
, ,
Response
No Trackback , 5 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1226

컴파일러의 경고 외

군대 유머, 관제탑 유머가 있는 것처럼 변호사를 소재로 한 블랙코미디 시리즈가 있다.
돈만 주면 자기 양심과 혼까지 팔아서 온갖 미사여구와 궤변(?)으로 범죄자의 형량을 감소시키고 심지어 무죄로 조작한다는.. 변호사에 대한 좀 과장되고 왜곡된 이미지가 들어가 있다.

천당과 지옥(혹은 천사와 악마)이 법정에서 소송이 붙으면 천당/천사 진영은 아마 승산이 없을 거라는 개드립조차 있다. 왜냐하면 유능한(=타락한-_-) 변호사들은 몽땅 지옥에 가 있어서 다 악마 편이기 때문에. -_-;; 물론 영적 법정에서 실제로 하나님이 어떤 편인지를 안다면, 그리고 성경에서 judgment라든가 judge라는 단어의 용례만 쭉 뽑아 보면 개드립은 그냥 개드립일 뿐이라는 걸 알 수 있다.

그런데 변호사가 굉장히 바보 같은 질문을 할 때가 있(었)는가 보다. 예를 들어..

  • 그림을 도둑맞던 당시에 선생님/고객님은 현장에 계셨습니까?
  • 그 일을 혼자 하셨나요? 아니면 단독 범행?
  • 충돌 당시에 두 차가 얼마나 떨어져 있었죠?
  • 그 스무 살 먹었다는 막내아들이 나이가 어떻게 된댔죠?
  • 전쟁에서 죽었다는 사람이 당신이었습니까, 아니면 당신 동생이었습니까?
  • 건망증을 앓고 계셨다면, 그럼 그 동안 잊어버린 것들의 예를 좀 들어 주시죠.

도대체 저 변호사 양반이 왕년에 그 무시무시한 사법 시험을 어떻게 통과했는지, 아니면 악착같은 공부 기계 괴수들이 몰리는 로스쿨을 어떻게 들어가서 졸업했고 어떻게 변호사 시험을 합격했는지를 의심케 하는 대목이 아닐 수 없다.

바보같은 질문은 그 변호사가 너무 격무에 시달린 나머지 (1) 정말로 뇌에 나사가 좀 풀려서 감을 잃었거나, (2) 정신 없어서 의뢰인을 완전 성의없게 대해서 나올 수 있다. 하지만 한편으로는 (3) 일부러 바보 같은 질문을 던져서 일종의 심문을 하려는 의도도 있다. 같은 내용을 비비 꼰 바보 같은 질문에 낚여서 진지하게 대답하다 보면, 일관성 없는 진술이 들통날 수 있기 때문이다.

뭐, 여기서 내가 법조인들의 심리나 심문 기법 같은 걸 얘기하려는 건 아니고.
중요한 건, 자연 언어뿐만 아니라 프로그램 코드도 사람이 작성하는 것이다 보니 저런 바보 같은 문장이 있을 수 있다는 것이다. 그리고 컴파일러가 그걸 지적해 주는 것을 우리는 '경고'라고 부른다.

간단한 예로는 선언만 해 놓고 사용하지 않은 변수, 초기화하지 않고 곧장 참조하는 변수, 한쪽에서는 class로 선언했는데 나중에 몸체를 정의할 때는 동일 명칭을 struct로 규정한 것이 있다. 딱히 에러까지는 아니고 코드 생성이 가능하지만, "혹시 다른 걸 의도한 게 아니었는지" 의심할 만한 부분이다.

더 똑똑한 컴파일러는 세미콜론이나 =/==사용이 아리까리해 보이는 것도 경고로 찍으며, 이런 것도 지적해 준다.
unsigned long p; (...) if(p<0) { }

unsigned 타입의 변수를 보고 "너 혹시 0보다 작니?"라고 묻는 건 그야말로 변호사가 "당신과 당신 동생 중 전쟁에서 죽은 사람이 누구라고 했죠?"라고 묻는 것이나 다름없다. 그러니 저 if 안에 있는 코드는 unreachable이라고 지적해 주는 건 적절한 조치이다.

사실, 사람이라 해도 처음부터 대놓고 저렇게 바보 같은 문장을 작성하는 경우는 드물다. 작성한 지 오래 된 코드를 나중에 리팩터링이나 다른 수정을 하게 됐는데, 같이 고쳐져야 하는 문장이 일부만 고쳐져서 일관성이 깨지는 경우가 더 많다. 남이 int를 기준으로 작성해 놓은 코드를 나중에 후임이 UINT로 고치면서 저 if문의 존재를 잊어버린다거나(알고 보니 이 값에 음수가 들어오거나 쓰일 일은 절대 없더라). 버그도 이런 식으로 생기곤 한다.

비주얼 C++에서 경고는 총 4단계가 있다. 1단계는 정말로 말이 안 되어 보이는 것만 출력하고, 4단계까지 가면 정말 미주알고주알 별걸 다 의심스럽다고 지적한다. 경고들을 그렇게 여러 단계로 분류한 기준은 딱히 표준이 있지는 않고 그냥 컴파일러 제조사의 임의 재량인 것으로 보인다.

비주얼 C++이 프로젝트를 만들 때 지정하는 디폴트는 3단계이다. 3단계를 기준으로 깔끔하게 컴파일되게 작성하던 코드를 4단계로 바꿔서 빌드해 보면 이름 없는 구조체를 포함해서 사용되지 않은 '함수 인자'들까지 온통 경고로 뜨기 때문에 output란이 꽤 지저분해진다. 물론, 특정 경고를 그냥 꺼 버리는 #pragma warning 지시문도 있지만, 그 자체가 소스 코드를 지저분하게 만드는 일이기도 하고.

그러니 어지간하면 3단계만으로 충분하지만, 4단계 경고 중에도 컴파일러가 잡아 주면 도움이 되겠다 싶은 일관성 미스 같은 것들이 있다.
그래서 모든 사람들이 코드의 모든 구조를 알지 못하는 공동 작업을 하는 경우.. (직감보다 시스템이 차지하는 비중이 더 커짐) 그리고 팀원/팀장 중에 좀 결벽증 강박관념이 있는 사람이 있는 경우, 4단계를 기준으로 프로젝트가 진행되며, 커밋하는 코드는 반드시 경고와 에러가 하나도 없어야 한다고 못을 박곤 한다. 심지어 경고도 에러와 동등하게 간주시켜서 빌드를 더 진행되지 않게 하는 컴파일 옵션을 사용하기도 한다.

변호사 유머를 보니까 컴파일러의 경고가 생각이 나서 글을 썼다.
내 생각엔 a=a++처럼 이식성 문제가 있고 컴파일러 구현체마다 다른 결과가 나올 수 있는 코드에 대해서나 경고가 좀 나와 줬으면 좋겠다. 저것도 초기화되지 않은 변수만큼이나 문제가 될 수 있기 때문이다. 비주얼 C++의 경고 level 4 옵션으로도 저건 그냥 넘어가는 듯하다.

예전에도 얘기한 적이 있듯, 법은 사람을 제어하는 일종의 선언/논리형 프로그래밍 언어로서 컴퓨터 사고방식으로 생각할 수도 있는 물건이다. 또한, 프로그램의 버전을 얼마나 올릴지 결정하는 게 형벌의 양형 기준과 비슷한 구석이 있다.
잡다한 기능들을 많이 추가한 것, 짧고 굵직한 기능을 구현한 것, 비록 작업량은 별로 많지 않지만 현실에서의 상징성과 의미가 굉장히 큰 것, 아니면 그냥 적당히 시간이 많이 흘렀기 때문에 숫자를 팍 올리는 것.

형벌이라는 것도 사람을 n명 죽인 것에 비례해서 징역이 올라가는 그런 관계는 당연히 아닐 테니, 상당히 많은 변수들이 감안된다.
이런 것들을 다 감안해서 다음 버전의 숫자를 결정하는데 이거 굉장히 복잡하다.

Posted by 사무엘

2015/12/10 08:33 2015/12/10 08:33
, , ,
Response
No Trackback , 3 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1169

Visual C++ 디버거 관련 생각

코딩으로 먹고 사는 프로그래머 내지 소프트웨어 개발자에게 필요한 것은 단순히 새로운 코드를 스스로 잘 작성하는 능력뿐만이 아니라, 문제가 생겼을 때 디버깅을 잘 하고 남이 만들어 놓은 코드를 신속하게 읽고 분석하는 능력이다. 아니, 업계에서는 어찌 보면 후자가 전자 이상으로 더 중요한지도 모른다. 왜냐하면 오늘날은 뭔가 완전히 새로운 솔루션을 천재 프로그래머 한 명에서 밑바닥부터 새로 만들어 낼 일은 거의 없어졌기 때문이다.

나의 영원한 친구는 비주얼 C++이고, 비주얼 C++ IDE는 예로부터 굉장히 편리한 디버깅 기능을 제공해 왔다. (일례로 Shift+F5는 엉덩국 홍콩행 C언어 병맛 만화에도 나올 정도로 유명한 비주얼 C++ 단축키이다. 디버그 중단 =_=)
특히 IDE가 32비트임에도 불구하고 64비트 프로세스를 아주 seamless하게 디버깅 해 내는 건 아무리 생각해도 대단해 보인다. 물론 이를 구현하기 위해 내부적으로는 64비트 디버그 서버 프로세스를 따로 만들고, 걔가 IDE와 디버기 프로그램 사이를 중재하고 있긴 하다. 그렇게 하는 것 말고는 기술적으로 다른 방법이 없다.

다만, 여러 편리한 기능에도 불구하고 본인이 일말의 아쉬움을 느끼는 점들을 나열하자면 다음과 같다.

1.
소스 코드에서 breakpoint를 여러 곳에 지정해 놓고서
한 breakpoint(A)가 적중한 뒤부터 다른 쪽 breakpoint(B)를 지났을 때 프로그램이 멈추게 하는 게 지원됐으면 좋겠다.

디버깅을 하고자 하는 지점이 평소에도 자주 지나는 곳이긴 하지만, 특정 조건이 만족된 뒤부터 실제로 의미를 갖는다는 뜻이다.
이런 상황에 대비해서 n회 이상 적중했을 때 멈춤, 특정 변수값이 변했을 때 멈춤 같은 여러 breakpoint 옵션이 있긴 하지만..
다른 breakpoint의 hit에 의존하여 그 뒤부터 멈추게 하는 기능은 Visual C++에서 지금까지 못 본 것 같다. 이거 회사일을 할 때와 <날개셋> 개발 중에 자주 필요성을 느꼈다.

IDE 내지 디버거가 이런 기능을 지원 안 해 주면 결국 사람이 해당 기능을 직접 코드에다 써 넣어야 한다.
bool 타입의 전역변수(bkpoint)를 하나 만든 뒤 A에 해당하는 지점에서는 bkpoint=true를 지정하고,
B에 해당하는 지점에서는 extern bkpoint; if(bkpoint) DebugBreak() 를 호출하는 식이다.
이런 긴급/땜빵 코드를 집어넣을 때는 굳이 클래스 따위 생각할 필요 없이 global scope이 존재하는 C/C++이 편리하게 느껴진다.

하지만 조건을 지정하는 코드와 멈추는 코드가 서로 다른 모듈에 있는 경우(static LIB, DLL, EXE 등) 여러 모듈을 고쳐서 재빌드해야 하고 일이 골치아파진다. 그러니 코드를 건드릴 필요 없이 이런 기능 정도는 개발툴이 바로 지원해 주는 게 속 편하다.

사실, 이런 쪽의 기능이 계속 추가되다 보면 디버거도 전처리기나 빌드 시스템처럼 일종의 프로그래밍 가능한 독자적인 시스템이 될지도 모르겠다. 사실은 <날개셋> 한글 입력기의 개발에서는 todo list를 분류하고 체계화하는 것부터가 전략이고 프로그래밍이다.

2.
디버그 로그를 찍는 API 함수는 OutputDebugString이며, 얘는 문자열을 받아들이는 여느 함수들과 마찬가지로 W 버전과 A 버전이 있다. 그러나 얘는 실제로는 오늘날의 NT 계열 운영체제에서도 유니코드를 지원하지 않는다.
다른 함수들은 A 버전이 문자열을 변환한 후 W 버전을 호출하는 형태이지만, 이 함수는 뜻밖에도 W 버전이 문자열을 변환한 후 내부적으로 A 버전을 호출한다.

물론 99%에 가까운 상황에서 프로그래머가 필요로 하는 로그 문자열은 단순히 알파벳과 숫자만으로 이뤄져 있어도 하등 지장이 없으며 충분하다. 그러나 본인처럼 문자 입력기 내지 마이너한 유니코드 문자/글꼴 쪽을 종종 연구하는 입장에서는.. 그런 문자열을 디버거로 곧장 확인할 수가 없어서 불편을 겪은 적이 생각보다 자주 있었다.

디버거 쪽이 여전히 1바이트 문자열 기반 프로토콜이 관행이어서 유니코드를 도입할 수 없다는 말도 변명에 지나지 않는다. 그런 용도로 쓰라고 엄연히 utf8이라는 물건이 있기 때문이다. 소프트웨어 국제화의 혜택이 사용자 인터페이스뿐만이 아니라 이런 데에까지 도달해야 하지 않을지?
직접 확인해 보지는 않았지만 C++말고 C#이나 자바는 디버그 로그가 유니코드를 지원 안 할 리가 없으리라고 생각한다.

3.
최신 201x 버전에서도 가끔은 프로젝트를 빌드하는 데 쓰였던 멀쩡한 소스 파일이 디버거에서 인식이 안 되는 경우가 가끔 있다. F9를 눌러도 해당 라인엔 빈 동그라미○만 생기지 breakpoint가 성공적으로 만들어졌음을 의미하는 ●가 생기지 않는다.
DebugBreak()를 손수 집어넣어서 강제로 세우더라도 그 지점에서 call stack 리스트가 제대로 생성돼 있지 않다. 또한 breakpoint는 만들어지지만 심벌 테이블이 좀 맛이 갔는지 변수값 조회가 동작하지 않을 때도 있다.

본인은 이 현상에 대해 정확한 문제 재연 조건과 원인, 해결 내지 예방 방법을 아직도 정확히 모른다. 프로젝트 전체를 재빌드하고 Visual C++ IDE를 재시작하고 나면 해결되기도 하고 안 그럴 때도 있었던 것 같다. VC++ 6의 고질병이던 허접 인텔리센스 ncb가 깨지는 문제는 오늘날 더 볼 일이 없지만, 디버깅은 여전히 완벽하지 못하다.

그러고 보니 디버그 심벌 데이터베이스는 IDE의 인텔리센스 데이터베이스와는 커버하는 영역이 정확하게 같을 수가 없겠다는 생각이 들었다. 전자는 우리 프로젝트 밖에서 빌드되어 LIB, DLL들에 존재하는 소스 코드와 그쪽 심벌까지 모두 연계해서 동작해야 하기 때문이다. (인텔리센스 정보가 없는 곳)

4.
이 외에도,
함수 안으로 들어가긴 하는데(F11), 그 함수의 인자와 관련된 함수 호출들은 모두 무정차로 건너뛰고서 들어가는 step in이 있었으면 좋겠다. 즉, A(b(), c()) 줄에서 시작한다면 b()나 c()로 들어가는 게 아니라 바로 A()의 몸체로 들어간다는 뜻이다.

그리고 디버깅과 직접적인 관계는 없지만, 텍스트를 검색하는데 주석 내용은 빼고 검색하거나 주석에서만 검색하는 기능도 있으면 좋겠다. #if 0과는 달리 주석 영역을 파악하는 건 단순 텍스트 패턴 매칭이므로 그리 어렵지 않을 것이다.

Posted by 사무엘

2015/07/01 19:31 2015/07/01 19:31
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1111

예전에도 몇 차례 얘기했듯이 비주얼 C++은 지금까지 내 인생에서 가장 재미있는 장난감이요, 친구요, 자아실현 매체요, 생계 수단 역할을 톡톡이 해 왔다.

비주얼 C++은 여느 프로그래밍 툴과는 다르게 뭐랄까, standalone, independent이고 자가생성이 가능하다. 쉽게 말해서 비주얼 C++ 자신과 같은 레벨의 컴파일러/런타임/IDE 같은 프로그램을 비주얼 C++로 또 만들 수 있다는 뜻이다. 실제로 마소에서 비주얼 C++은 이전 버전의 비주얼 C++로 만들고 있기도 하고. 이렇듯, 이 툴은 가장 배우기 어렵지만 가장 강력하고 군더더기 없는 프로그램을 만들 수 있다.

2014년 현재, 난 한 컴퓨터에 다음과 같은 세 버전을 깔아 놓는다. 제각기 필요와 쓸모가 있기 때문이다.

1. 2003

  • 2010에서 새로 도입된 Help Viewer가 완전 거지 같아서.. 단순 윈도 API나 MFC 레퍼런스를 조회하는 덴 200x 구버전 document explorer 기반의 msdn이 짱이다. (1) 색인이 처음에 뜨는 데 시간이 너무 오래 걸리는 것, (2) 가끔 목차/색인을 클릭해도 해당 항목 문서가 안 나타나는 것--정확히는 수 초 뒤에 한참 뒤에 뜸.. 이 두 버그 때문에 학을 뗐다. (단, 2012 이후의 Help Viewer 2.0은 불편하던 게 좀 개선된 거 같기도 하고..)
  • 2003은 MFC가 지금처럼 말도 안 되게 bloat되기 전이며, 굉장한 legacy 운영체제에도 돌아가는 바이너리를 만들 수 있는 버전이다. <날개셋> 타자연습을 여전히 10년 전의 구닥다리 컴파일러로 빌드하는 이유가 이것 때문이다.
  • 다만 2003은 IDE가 빌드 내지 리소스 편집 중에 잘 뻗는 편이고(불안정!) Vista 이후 OS에서는 일부 기능이 충돌도 함. 조심해서 써야 한다.

2. 2010

  • 닷넷 이래로 Visual Studio가 기본 제공하던 msi 설치/배포 프로젝트 기능이 2012에서 갑자기 없어져 버린 관계로, 2010을 도저히 제거할 수가 없게 됐다. 대체품이라는 InstallShield 번들 에디션은 어마어마한 덩치와 복잡한 사용법 때문에 곧바로 gg 치고 언인스톨해 버렸다.
  • 또한 <날개셋> 한글 입력기는 빌드와 관련된 특이한 이슈 때문에 2012가 아닌 2010 컴파일러 툴체인을 사용하고 있다.
  • 다만, 2010은 IDE의 비주얼이 역대 VC++ 역사상 제일 구리고 우중충 칙칙하고 안 좋았다. -_-;;

3. 2012

난 201x가 다음과 같은 점에서 마음에 든다. (1) 크게 강화된 인텔리센스 엔진 (2) 람다 같은 C++ 최신 문법 (3) 빌드나 리소스 편집 중에 IDE가 이제 거의 뻗지 않음
2012는 이를 바탕으로 2010보다 훨씬 더 깔끔한 GUI에, 신택스 컬러링도 훨씬 더 강화되어 몹시 마음에 든다. 몇 가지 크리티컬만 없었으면 2012가 2010을 완전히 대체할 수도 있었을 텐데. ㅜ.ㅜ
다만 2012 얘만 꼭 남겨 둘 이유 역시 없기 때문에 이것보다 더 최신 버전이 나오면 그걸로 대체할 수도 있다. 즉, 2012는 2003/2010과는 달리 고정 보존 상태는 아니다.

위와는 달리, 보존 대상에서 제외되고 안 쓰는 버전은 다음과 같다.

1. 6.0

VC6은 그야말로 개발툴계의 IE6이나 마찬가지다. 출시 시기는 다르지만 공교롭게도 버전 번호도 동일하고 말이다. IE가 윈도 비스타의 출시 지연 때문에 6 이후로 5년 가까이 버전업이 없었다면, VC는 닷넷이 첫 개발되느라 4년 가까이 6 이후로 버전업이 없었다. 그 뒤 지나치게 오랫동안 현역을 뛰어 왔다.

웹 개발자들이 제발 IE6 좀 퇴출시키자고 캠페인 하는 것만큼이나 PC 클라이언트 개발자들은 업계에서 VC6 좀 퇴출시키자고 캠페인이라도 해야 할 판이다. 단지, IE는 모든 PC 사용자들이 쓰는 웹브라우저인 반면, VC는 극소수 프로그래머만이 쓰는 개발툴이라는 점이 다르다.

VC6은 이제 해도 해도 너무하다 싶을 정도로 심하게 후지고 낡았다. IDE가 IME-aware하지도 않고, 특히 한글 윈도에서는 기본 글꼴이 윈도 3.1 스타일의 완전 추레한 System으로 나옴! 인텔리센스는 지금에 비하면 완전 안습 크리 수준이고. 최신 C++ 표준이나 멀티코어 같은 건 아웃 오브 안중이다.

VC6이 아니면 도저히 빌드시킬 수 없는 비표준 코드가 이미 수십만 줄 이상 작성되어 버려서 도저히 수습을 못 할 지경이 된, 한 20년 묵은 불가피한 프로젝트가 아니라면 아직까지 VC6을 고집할 이유란 없어야 정상일 것이다. for문 변수 scope 정도는 후대의 컴파일러로도 옵션을 바꿔서 수용시킬 수 있을 텐데.

굳이 장점을 찾자면, VC6은 생성되는 바이너리가 운영체제의 MSVCRT와 MFC42를 직통 지원한다는 점이 매우 유리하다. 그러나 이것도 어차피 64비트는 지원 안 하기 때문에 장점이 반쪽짜리 이하로 의미를 크게 상실한다.

2. 2005

MS 오피스 2003이 아닌 독자 GUI 비주얼을 선택한 첫 버전 되시겠다. (VC 2005가 오피스 2003 같은 시퍼런 비주얼 기반이었다면? 상상만 해도 ㅎㅎ)
난 얘는 일단 sp1과 운영체제 패치를 설치하는 시간이 2005 자체를 설치하는 데 걸리는 시간보다 더 길어서 인상이 매우 안 좋다. 게다가 CRT/MFC DLL 배포 방식도 구리게 바뀌었고. 장점은 어차피 (1) 2003이나(msdn 등) (2) 이후 버전(64비트 지원 등)에 다 포함돼 있기 때문에 굳이 얘가 필요하진 않다. out.

3. 2008

2005보다는 훨씬 더 괜찮은 물건이고 쓸 만하다. 그리고 은은한 연보라색 톤(비스타/7 기준)의 IDE 외형은 역대 버전들 중 가장 깔끔하고 괜찮았다고 생각한다.
200x 중에서는 가장 훌륭했지만, 역시 얘만 보존해야 할 필요는 존재하지 않는다. 플러스 팩의 등장과 함께 MFC가 완전 bloatware로 바뀌어 버렸고, CRT/MFC DLL 배포 방식은 여전히 아쉬운 점이다.

위의 두 카테고리 말고 본인이 special case로 예우하는 골동품 버전이 있는데, 그건 6.0보다도 더 옛날 버전인 4.2이다. mfc42의 원조인 바로 그 버전이다.
본인이 난생 처음으로 구경한 비주얼 C++ 버전이어서 애착이 간다.

Posted by 사무엘

2014/08/07 08:28 2014/08/07 08:28
, , ,
Response
No Trackback , 5 Comments
RSS :
http://moogi.new21.org/tc/rss/response/993

컴퓨터 화면에 그려진 어떤 물건(주로 사각형 모양)의 경계 테두리에다 마우스를 갖다대면 포인터가 그 경계 테두리와 수직인 방향의 화살표로 변한다. 그 상태에서 마우스를 클릭하여 끌면, 그 물건의 크기를 마우스 포인터가 움직이는 방향으로 변경할 수 있다.
이것은 GUI에서 매우 흔히 볼 수 있는 기능이다. 특히 크기 조절 가능한 창--운영체제가 정식으로 제공하는 GUI 구성요소--의 경우 이런 기능은 운영체제가 non-client 영역에서 알아서 자동으로 처리해 준다.

그런 창이야 운영체제가 처리를 알아서 해 주지만, 대화상자 내부의 임의의 영역에 대해서, 혹은 클라이언트 영역에 내가 그려 주는 임의의 객체에 대해서 이런 처리를 구현하려면 어떻해야 할까? 뭔가 공통된 패턴의 알고리즘을 처리해야 할 텐데 코딩량이 적지는 않으며 왠지 귀찮고 번거로워 보인다.

크기 조절 내지 화면 분할 UI와 관련해서는 다음과 같은 여러 상황을 생각할 수 있다.

1. 한 윈도우 내부에 그려지는 개체의 크기 조절

2차원 벡터 그래픽 프로그램 내지 RAD 툴의 폼 에디터가 정확하게 여기에 속한다. 요즘은 워드 프로세서도 자체적인 벡터 그래픽이나 하다못해 OLE 개체라도 취급하니 마우스를 이용한 크기 조절 기능을 제공해야 한다. 개체를 클릭하면 8군데의 앵커 사각형이 생기며, 경계 아무 곳이라기보다는 그 앵커 사각형을 드래그했을 때 크기 조절이 된다. 다른 곳을 드래그하면 크기 조절이 아니라 이동이 되고. 그리고 요즘 MS 오피스 제품은 회전용 앵커까지 덤으로 제공한다.

비주얼 C++에는 CRectTracker라는 고전적인 클래스가 있어서 마우스 드래그로 임의의 사각형 영역을 화면에다 그리고 마우스가 클릭됐을 때의 일체의 처리를 알아서 해 준다. 이것만 쓸 줄 알아도 상당히 편리한데, 본인은 지금까지 그런 분야의 프로그램을 개발할 일이 없다 보니 실제로 써 본 적은 전혀 없다. 이런 게 있다는 것만 안다.

그리고, 지금도 있나 모르겠다만, 비주얼 C++에는 DrawCli라고 딱 개체 기반 벡터 드로잉과 드래그 드롭, 크기 조절과 이동을 모두 시연해 놓은 걸출한 예제 프로그램이 있다.
학창 시절 나의 친구였던 <비주얼 C++ 완벽 가이드>(김 용성)에도 '트래커'라는 예제 프로그램이 있으니 참고할 것.

사용자 삽입 이미지

참고로, 저렇게 테두리를 그리고 사각형 안의 내용물은 대각선 사선으로 칠하는 건.. embed된 OLE 개체를 외부 프로그램이 수정 중일 때 클라이언트 프로그램이 표시하는 표준 모양이기도 하다.

사용자 삽입 이미지

2. 부모 윈도우가 자식 윈도우를 동적으로 분할하고 관리

규모깨나 좀 있는 문서 편집 프로그램을 보면, view를 분할하는 기능이 있다. 이것은 한 문서 컨텐츠에 대해 뷰 윈도우를 여러 개 둬서 한 컨텐츠를 여러 가지 다른 방식, 다른 위치를 표시할 수 있게 한다. 사용자의 입장에서 매우 편리한 기능이지만, 컨텐츠와 뷰 윈도우가 완전히 일심동체라고 전제하고 만들어져 버린 프로그램이라면 추후에 이러 기능을 추가하기가 쉽지 않을 것이다. MDI 프로그램이라면 아예 별도의 독립된 창을 만드는 기능도 있겠지만 SDI에서는 한 창을 분할하는 것만 가능하다.

이건.. 생각보다 만들기 어려운 기능이다. 경계(splitter) 부분을 마우스로 끌었을 때의 처리도 처리거니와--이를테면 XOR 연산으로 자취를 그렸다가 지우는 것도..--, 내가 무엇보다도 힘들겠다고 느끼는 건 스크롤 바를 자체적으로 따로 만들어서 관리하는 부분이다. 이게 무슨 뜻인지를 설명하자면 이렇다.

Windows 운영체제에는 어떤 윈도우가 자체적으로 스크롤 바를 가질 수 있고, 한편으로 스크롤 바 자체가 별도의 컨트롤로 존재할 수 있다.
윈도우가 자체적으로 갖는 native 스크롤 바는 당연히 운영체제가 모든 처리를 알아서 해 준다. 창의 크기가 바뀌어도 스크롤 바를 자동으로 우측(상하) 내지 하단(좌우)에 배치해 주고, 스크롤을 할 필요가 없어지면 알아서 스크롤 바가 없어지고 그 영역까지 클라이언트 영역이 확대된다.

그러나 splitter가 존재하는 view를 보면, 스크롤 바가 있던 자리의 구석 일부에 창을 분할시키는 앵커가 자리잡고 있다. 이건 native 스크롤 바로 구현 가능하지 않기 때문에 스크롤 바 컨트롤을 따로 만들어서 앵커 밑이나 옆에다 두고, 스크롤 바의 위치· 크기와 관련된 모든 처리를 수동으로 해야 한다. 이 얼마나 복잡하고 손이 많이 갈까? 그러니 MFC가 CSplitterWnd라는 클래스에다 전부 구현해 놨다.

사용자 삽입 이미지

MFC AppWizard에서 '뷰 분할 기능 사용'을 체크하면, 프레임 윈도우는 밑에다 저 splitter 윈도우를 생성하고 걔가 또 자기 밑에다 어떤 view를 생성할지를 따로 지정해 준다. 다시 말해 프레임 윈도우와 view 사이에 splitter라는 중간 계층 윈도우가 하나 또 생긴다는 것이다. 그리고 요놈이 스크롤 바와 앵커의 위치를 관리하고 앵커 드래그에 대한 처리도 담당한다.

사용자 삽입 이미지

내가 MFC의 splitter 윈도우에 대해 꽤 놀란 것은, 분할을 2개씩뿐만 아니라 그 이상도 얼마든지 할 수 있다는 점이다. 위의 그림처럼 가로 3개, 세로 3개를 분할해서 무려 9개나 되는 view 윈도우를 뻥튀기시킬 수도 있다. 가로와 세로의 splitter 중 어느 것 하나의 위치가 바뀌었을 때, 혹은 창 전체의 크기가 바뀌었을 때 splitter가 총체적으로 조율하고 해야 할 일의 양도 그에 비례해서 많아질 것이다.

그런데 각각의 창들이 다 독자적인 가로· 세로 스크롤 바를 갖는 건 아니고.. 위의 스크린샷에 보듯이 한 column에 해당하는 view들은 가로 스크롤 바를 하나 다같이 공유한다. 그리고 한 row에 해당하는 view들은 세로 스크롤 바를 다같이 공유한다. 특이한 점임.

3. 부모 윈도우가 자식 윈도우를 '정적'으로 분할하고 관리

위의 2번과 마찬가지로 부모 윈도우가 자식 윈도우의 분할을 관리하는 경우이긴 한데, 위처럼 성격이 비슷한 윈도우가 아니라 별개의 윈도우를 고정적으로 관리하는 경우를 추가적으로 생각할 수 있다. 처음엔 1개였다가 2개 이상으로 자유자재로 분할되는 건 아니기 때문에, 스크롤 바나 앵커 같은 건 없다.

왼쪽에 트리 컨트롤, 오른쪽에 리스트 컨트롤을 두고 가로로 크기 조절이 가능한 탐색기 같은 프로그램도 좋은 예이고, 그보다 좀 더 복잡한 경우로는 개발자들의 친구인 Dependency Walker가 있다.

사용자 삽입 이미지

Spy++ 같은 프로그램으로 들여다보면, 창 구조가 생각보다 복잡하다는 걸 알 수 있다.
가장 겉에 있는 창은 화면을 상하로 나눈다. 위에 있는 창은 화면을 또 좌우로 나눠서 왼쪽은 모듈 트리 구조가 나오며, 오른쪽은 또 상하로 나누어서 각각 이 모듈이 import하는 심벌들, 그리고 대상 모듈이 export 하는 전체 심벌들이 표시된다.
한편, 아래의 화면은 또 상하로 나뉘어서 위에는 전체 모듈 리스트가 있고 아래에는 메시지 log가 있다.

즉, 한 윈도우에 여러 개의 컨트롤들이 sibling 관계로 대등하게 늘어서 있는 게 아니라, 또 분할 윈도우가 있고 그 아래에 또 분할 윈도우가 자식 윈도우로 있는 형태다. 한 분할 윈도우는 언제나 좌우로든 상하로든 2개의 윈도우만을 담당한다.

이렇게 이분법적으로 접근하면, 제아무리 복잡하게 화면이 좌우 상하로 분할되어 있는 창이라 해도 전체 크기가 바뀌었다거나 할 때 자기가 맡은 두 개의 창만 비율 분배를 잘 하고 나머지는 자가반복적인 재귀 처리에 맡기면 되니 문제가 단순해진다는 장점이 있다.

저런 윈도우가 활용의 자유도가 더욱 올라간다면 아예 별도의 창으로 분리하거나 docking까지 가능해진다. Visual Studio의 각종 보조 윈도우들처럼 말이다. 그건 우리 같은 평범한 프로그래머가 밑바닥부터 할 짓이 못 되며, 이미 있는 GUI 라이브러리의 사용법을 익히는 것만으로도 충분할 것이다. MFC 없이 Windows API만으로 docking toolbar를 구현한 소스를 외국 사이트에서 본 적이 있는데, 가히 근성이 느껴졌다.

사용자 인터페이스라는 건 컴퓨터로 하여금 의미 있는 작업을 하게 만드는 실질적인 알고리즘이 아니다. 없던 걸 처음 시도하여 만드는 게 아닌 이상, 베끼기만 하느라 시간 낭비할 필요는 없을 것이다.

4. 대등한 위상의 윈도우끼리 분할 관리를 해야 하는 경우

어휴, 글을 이렇게 길게 쓸 생각은 없었는데... 마지막 아이템도 언급을 안 할 수가 없구나. -_-;;
자, 지금까지 얘기한 것들을 정리하자면 1번은 그냥 한 윈도우 내부에서 그리기를 하는 것뿐이며, 2번과 3번은 부모 윈도우가 자식 윈도우들의 공간 관리를 하는 경우를 특별히 MFC의 document-view 아키텍처의 예를 들어 소개한 것이다.

그러나 실무에서는 그것만이 전부가 아니다. 대화상자처럼 document-view 아키텍처가 적용되지 않는 창에 대해서도 수평/수직 splitter 같은 물건을 만들어야 할 때가 있다.
예를 들어, <날개셋> 제어판 같은 경우, '분야'를 나타내는 왼쪽의 트리 컨트롤과, 오른쪽의 여타 컨트롤들 사이에 수직 splitter를 둬서, 트리 컨트롤의 폭을 좀 더 넓힌다거나 반대로 좁히는 경우를 생각할 수 있다.
이 경우 splitter는 여타 컨트롤들과 마찬가지로 대화상자 안에 존재하는 여러 자식 윈도우의 하나일 뿐이지 나머지 컨트롤들을 모두 통솔하는 부모 윈도우의 지위는 아니게 된다. Spy++로 들여다보면, 가로나 세로로 길쭉한 고유한 splitter 윈도우가 잡히는 걸 볼 수 있다.

이런 상황에 대해서는 MFC는 딱히 제공해 주는 있는 클래스가 없다. codeguru 같은 데서 splitter dialog 정도로 검색해 보면 예제 소스나 관련 튜토리얼들이 쭉 나온다. 예전에 아주 괜찮은 코드를 하나 구해서 유용하게 쓴 적이 있었는데 지금 다시 검색하려니까 못 찾겠다.

이런 일을 하는 범용적인 클래스를 만들 때 염두에 둬야 하는 사항으로는, 좌우나 상하든 보장해 줘야 하는 최소 크기를 인자로 받아야 할 것이고, 좌우 상하 중 한쪽에다 뒀으면 하는 윈도우를 배열 같은 자료구조로 관리해야 한다. 윈도우 핸들이 아닌 ID로 받으면.. ID로부터 실제 핸들값(HWND)을 얻어야 하는 번거로움이 있지만, 그 컨트롤이 중간에 재생성된다거나 해도 여전히 식별이 가능하기 때문에 범용성이 좀 더 향상된다.

다른 splitter조차 자기의 크기 조절에 영향을 받게 하고 WM_SIZE 메시지에 반응한다면, 아까 Dependency Walker 같은 복잡다단 splitter도 얼마든지 구현 가능하다.
splitter를 구현하려면 당장 크기를 조절하는 것 처리는 둘째치고라도, 창의 크기가 바뀌었을 때 각 분할 화면들의 공간 배분을 어떻게 할지 같은 것도 생각해야 하니 여러 모로 골치가 아픈 건 사실이다.

끝으로 언급하고 싶은 이슈가 있다. 1~4번들은 다 마우스가 클릭되었을 때 캡처를 잡고 마우스 움직임을 추적하다가 버튼이 떼졌을 때 마무리 처리를 한다는 공통점이 있다.
이 경우, WM_LBUTTONDOWN, WM_MOUSEMOVE, WM_LBUTTONUP을 모두 메시지 맵에다 등록하고 각 상황별 코드를 메시지 핸들러 함수에다 제각기 따로 작성하는 방법을 생각할 수 있지만..

좀 더 능숙한 프로그래머라면, 그런 드래그 드롭 처리 정도면 WM_LBUTTONDOWN에다가 아예 별도의 message loop을 만들어서 거기에다 WM_MOUSEMOVE와 WM_LBUTTONUP에 해당하는 코드를 다 집어넣는 방법을 선택한다. 한 함수에다가 한 기능에 대한 처리를 몰아서 넣는 게 훨씬 더 깔끔하기 때문이다.

Posted by 사무엘

2014/07/02 08:32 2014/07/02 08:32
, ,
Response
No Trackback , 6 Comments
RSS :
http://moogi.new21.org/tc/rss/response/980

MFC 프로그래밍 잡설

1. IE 웹브라우저 윈도우 삽입

내 프로그램에다가 로컬이든 웹이든 HTML 페이지 내용을 표시해야 할 일이 생겼다. 이 경우 가장 간단한 해결책은 Internet Explorer 웹브라우저 윈도우를 삽입하는 것이다.

그런데 얘는 ActiveX 컨트롤이다. 흔히 웹페이지 내부에 들어가는 각종 ActiveX 컨트롤들이 웹 표준을 위배하고 사용자 접근성을 저해한다는 식으로 말이 많지만, 사실은 Window의 웹브라우저 자체부터가 ActiveX 형태로 제공되는 컴포넌트인 것이다. 그리고 이미 다들 아시겠지만, 플래시도 기술적으로는 ActiveX이다. 단지 이건 너무 전세계적으로 널리 퍼진 관계로 반쯤 웹 표준인 것처럼 인정받고 있을 뿐이다. (뭐, 이것도 HTML5의 등장으로 인해 지위가 좀 위태로워지긴 했지만)

어쨌든 이런 구조적인 차이로 인해, 웹브라우저 윈도우는, 리치 에디트 같은 여느 custom control과는 달리 CreateWindowEx 함수에다가 클래스 이름만 달랑 넘겨 준다고 선뜻 만들 수 있는 물건이 아니다.
MFC에서 ActiveX 컨트롤을 생성하는 코드를 보면 CWnd::CreateControl로 내려가는데, 내부 메커니즘은 각종 COM API가 동원되며 미치도록 복잡하다. 사실, 난 MFC의 도움 없이 API만으로 ActiveX 컨트롤을 생성해 본 적이 없으며, 요즘 같은 세상에 굳이 그래야 할 필요도 없을 것이다.

예전에 비주얼 C++에는 Component Gallery라는 게 있어서 (1) 스플래시 윈도우나 '알고 계십니까' 팁 대화상자처럼 몇몇 자주 쓰이는 MFC 클래스를 프로젝트에다 자동으로 등록해 주는 템플릿, (2) 그리고 특정 ActiveX 컨트롤에 대한 wrapper 클래스를 자동 생성해 주는 기능이 있었다. 6.0의 이후 버전부터는 그런 걸 못 본 것 같다.
(1)은 그렇다 쳐도 (2)는 해당 ActiveX 컨트롤의 type library를 참고하여 이 컨트롤을 생성하는 함수, 그리고 걔가 원래 제공하는 속성과 메소드들을 그대로 C++ 클래스 형태로 옮겨 주는 기능이다. CWnd의 파생 클래스인 것은 두 말할 나위도 없고.

Component Gallery가 없으니 요즘 (2)를 수행하려면 좀 우회 경로를 가야 한다. 대화상자를 하나 만든 뒤 거기서 우클릭하여 원하는 ActiveX 컨트롤을 삽입하고, 그걸 또 우클릭하여 클래스를 추가하면 된다.

다른 것도 아니고 IE 웹브라우저 윈도우는 굉장히 유명한 ActiveX 컨트롤인 관계로, 사실은 MFC에도 이미 전용 클래스가 준비되어 있다. 바로 CHtmlView 되시겠다. 이름에서 알 수 있듯 얘는 CWnd가 아닌 CView로부터 상속을 받아서 MFC의 view-document 아키텍처에 최적화되어 있다.
즉, 대화상자의 여느 컨트롤들과는 달리 스택이 아닌 heap에 생성되고, PostNcDestroy 함수에 delete this가 구현되어 있다. 그래서 대화상자 같은 데에서 간단히 사용하기에는 어려움이 있다. (뭐, 불가능한 건 아니다. 대화상자 위에다 아예 CView를 만들지 말라는 법도 없으니)

한편, CHtmlEditCtrl이라는 클래스도 있다.
IE 윈도우는 단순히 HTML을 표시만 하는 게 아니라 위지윅 HTML 편집기 기능도 갖추고 있다. 얘는 IE 윈도우를 viewer가 아닌 editor 모드로 열어 준다.
IE가 여러 모로 리치 에디트 컨트롤과도 경쟁 구도가 된 듯하다. 물론 리치 에디트가 훨씬 더 빠르고 가볍지만, 텍스트에다 서식을 입히는 데 RTF보다야 HTML이 압도적으로 더 유명한 대세가 된 건 부인할 수 없다. 그래서 도움말조차 RTF 기반인 재래식 HLP는 진작에 밀려 사라지기도 했고 말이다.

이 CHtmlEditCtrl은 CView가 아닌 CWnd 기반이다. 그래서 CDialog 파생 클래스에다가 멤버로 선언하여 대화상자의 child control로도 비교적 쉽게 사용할 수 있다. view 버전은 CHtmlEditView와 CHtmlEditDoc이 따로 있는 듯.

하지만 에디트 기능이 없는 일반 IE 윈도우를 CWnd를 기반으로 간단히 스택에다가 생성하는 건 여전히 MFC의 기존 클래스로 가능하지 않은 것 같다. 그래서 본인은 그냥 ActiveX 컨트롤 type library로부터 CWnd 파생 클래스를 추출한 후 그걸 사용하는 재래식 방법을 동원했다.

2. MFC 액셀러레이터 버그(?)

Windows API에는 메뉴 단축키를 자동으로 처리해 주는 액셀러레이터라는 게 있다. MFC에서는 CFrameWnd::LoadFrame 함수에서 자기 프레임 윈도우 ID값에 해당하는 액셀러레이터를 불러들인다.

그런데 거기에 있는 단축키를 좀 수정하고, 메뉴에다 새로운 기능을 추가하여 단축키도 액셀러레이터 테이블에다가 배당했는데, 아무리 수정을 해 줘도 새로운 단축키가 동작하질 않고 단축키가 예전 방식으로만 동작한다.
혹시 액셀러레이터 리소스가 잘못 빌드됐나 싶어서 빌드된 EXE 파일의 내부 리소스를 살펴보기도 했지만 딱히 이상이 없다.

그렇다고 해서 해당 리소스를 아예 지워 버리면 모든 단축키가 먹통이 된다. 그러나 리소스가 있으면 단축키가 있는 그대로 인식되지 않는다. 어찌 된 영문일까?

이것은 비주얼 C++ 2008 이후부터 도입된 일명 feature pack의 추가 기능 때문에 벌어지는 현상으로, 엄밀히 말해 버그는 아니다.
알다시피 MFC feature pack에서는 CWinApp, CFrameWnd 같은 전통적인 클래스에 Ex가 붙었고, MS Office처럼 프로그램의 모든 기능의 단축키를 customize하는 기능이 추가되었다. 그래서 한번 프로그램을 사용하고 나면, 그 뒤엔 프로그램이 리소스에 있는 액셀러레이터 테이블을 참조하는 게 아니라 레지스트리에 저장된 단축키를 따라 동작하게 된다. CKeyboardManager라는 클래스를 보신 적이 있을 것이다.

그렇기 때문에 프로그램 개발 과정에서 새로운 메뉴 명령이나 단축키가 추가되어 이를 테스트하고 싶다면, 프로그램을 실행한 후에 Customize 대화상자를 꺼내서 단축키를 reset시키면 된다. 아니면 해당 레지스트리를 수동으로 날리거나 레지스트리를 날리는 코드를 추가해 주면 된다. 이에 대한 자세한 정보는 구글링하면 다 나온다.

단축키와 도구모음줄을 싹 다 customize하는 기능이 필요할 정도로 규모가 방대한 프로그램을 개발할 일은 사실 그리 많지 않다.
그러니 그냥 옛날처럼 feature pack 기능을 사용하지 않는 아주 간단한 프로그램만 만들고 싶은데 요즘 MFC 마법사는 그냥 선택의 여지가 없이 Ex 클래스만 사용하여 코드를 생성해 주는 듯하다.

요즘은 MFC DLL은 이제 ansi 버전은 기본 배포조차 안 해 준다고 하지?
그나저나 (1) DLL의 덩치가 커져도 너무 커진 것, 그리고 확장팩이 그나마 MS Office나 Visual Studio의 UI를 정확하게 고증하여 재연한 것도 아니고 (2) 동작 방식이나 글꼴, 색상이 들쭉날쭉 차이가 나면서 짝퉁 티가 팍팍 나는 것을 생각하면...
MFC의 변화 양상에 대해서 본인은 불만이 좀 있다. -_-;;

예전에도 말했지만, (1)은 걍 운영체제의 내장 mfc42.dll을 직통으로 사용하는 classic legacy 모드 같은 거 좀 넣어 주면 안 되나 싶고,
(2)는.. 운영체제의 보급 메뉴 말고 싸제 메뉴가 흔히 저지르는 실수 하나만 좀 지적하고 넘어가겠다. 업계 관계자가 내 글을 보게 될 가능성은 별로 없지만..;;

메뉴가 튀어나왔을 때는 프로그램이 자체적으로 IME를 꺼야 한다. 그래서 한글 모드일 때도 Alt를 누르지 않고 그냥 누르는 메뉴 항목에 대한 단축키(액셀러레이터 키)가 먹혀야 한다. 그 글쇠가 안 먹히고 화면 한 구석에 ㅇ, ㅂ 같은 조합 윈도우가 튀어나오는 건 프로그램의 버그이다.
이것도 MS 오피스의 싸제 메뉴는 처리를 한 반면에, 요즘 MFC가 라이선스한 싸제 메뉴는 그런 처리도 안 돼 있다. 보면 볼수록 품질이 실망스럽다. 아니, Visual Studio조차도 MS Office 라이브러리가 아니라 WPF 기반으로 새로 제작된 2010 이후의 IDE는 메뉴에 저 버그가 존재한다.

<날개셋> 한글 입력기야 MFC를 사용하지 않고, 그나마 타자연습은 나온 지 10년도 더 된 구닥다리 Visual C++ 2003을 아직도 사용하며 빌드되고 있다. MFC의 배포 방식과 덩치 때문에 업그레이드를 할 처지가 못 돼서 말이다. 아니면 차라리 WTL 같은 더 가벼운 프레임워크로 갈아타야 되나 싶다.
위의 두 아이템들은 내 개인 프로젝트가 아니라 회사 일을 하면서 발견하고 느낀 것들을 글로 옮긴 것이다. 이것 말고도 기억에 남는 게 좀 있는데.. 마저 나열하면서 글을 맺도록 하겠다.

3. ShowWindow(SW_HIDE) 하니까 창이 없어져 버렸던 것. 동일한 영역의 창에 IE ActiveX 컨트롤과 여타 윈도우를 상황에 따라 교대로 보이거나 숨기는 UI를 만들 일이 있었다. 그런데 프로그램이 자꾸 이상하게 동작하고 assertion failure가 나기에 디버깅을 해 봤더니, 이게 웬걸, IE 윈도우를 ShowWindow(SW_HIDE)를 해서 숨기는 순간 컨트롤 자체가 완전히 파괴되고 m_hWnd 값이 NULL이 되는 것이었다.

검색을 해 보니 이것은 아주 잘 알려진 문제. 처음에 Create로 생성을 할 때 WS_VISIBLE가 지정되지 않았던 IE 컨트롤은 나중에 또 ShowWindow를 통해 숨겨질 때 내부 로직에 의해 destroy되어 버리는 모양이었다.
이 문제를 피해 가려면 그 윈도우에 대해서 MFC의 CWnd::ShowWindow를 호출하지 말고 그냥 Windows API 함수를 쓰면 된다고 한다. 내부 사정은 알 수 없는 노릇. 스레드를 사용할 때 이래로 MFC 클래스 대신 Windows API의 사용이 강제되는 또 다른 상황을 만났다.

4. 내 프로그램에다 삽입시킨 IE 컨트롤로 각종 자바스크립트를 사용하는 웹페이지에 접속을 하다 보면.. 스크립트 오류가 난다. gmail만 해도 로그인을 하고 나면 동일 증상을 확인할 수 있음.
이것은 IE가 보안 때문에 취한 조치인 듯하다.
HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_BROWSER_EMULATION

요 key를 만들어서 그 밑에 이름은 "자기 프로그램.exe"이고 데이터는 10진수로 IE 버전 곱하기 1000 (=0이 3개 붙은)인 REG_DWORD를 집어넣어 주면 된다.

5. MFC 라이브러리와 표준 C++ 라이브러리를 같이 사용한 상태로 프로그램을 static link 형태로 빌드하고 나면..
operator new/delete가 중복 정의되었다고 링크 에러가 나는 경우가 있다. (DLL link는 상관 없음)
이 역시 구글링을 하면 정보가 곧바로 걸려 나올 정도로 잘 알려진 문제이다. 귀찮지만 라이브러리를 링크하는 순서를 좀 바꿔 주면 해결 가능하다. 구체적인 해결책은 지금 이 개인용 컴퓨터에 들어있지 않아서 설명을 생략하겠다. -_-

Posted by 사무엘

2014/05/20 08:18 2014/05/20 08:18
, , ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/965

1. 특정 명칭(클래스, 함수, 변수 등등)의 선언지로 곧바로 찾아가기.
(1) 소스 코드에서 cursor나 마우스 포인터가 가리키고 있는 명칭에 대해서는 현재 소속되어 있는 클래스나 namespace 문맥을 감지하여 동작해야 하며, (2) 그냥 임의의 심벌을 타이핑하여 조회하는 기능도 있어야 한다. 둘 다 필요하다.

2. 디렉터리를 불문하고 프로젝트에 있는 특정 파일 이름을 곧바로 타이핑으로 조회하여 파일 열기. 시작하는 단어와 중간에 있는 단어가 모두 지원되어야 한다.

3. 그리고 명칭이 아닌 임의의 문자열을 검색하는 Find in files인데, 다음과 같은 범위에서 모두 가능할 것.
(a) 소스(=번역 단위)든 헤더든 프로젝트에 정식으로 등록돼 있는 파일
(b) 프로젝트에 정식으로 등록은 안 돼 있지만, 등록된 파일로부터 인클루드에 의해 한 번이라도 엮이는 파일들
(c) 프로젝트 파일이 하나라도 존재하는 디렉터리에 덩달아 있는 모든 소스 파일들

즉, 3은 파일 내부의 문자열 검색이고 2는 파일 이름 자체의 검색이다. 2의 경우 일단은 검색 도메인이 (a)만으로 한정이지만, 2도 (b)나 (c)가 옵션에 따라 지원된다면 금상첨화다.

Visual Studio IDE의 경우, 1은 진작부터 인텔리센스 엔진을 통해 지원되어 왔다. 그러나 2는 2012에 와서야 가능해졌으며, 3은 (a)만 가능하다. (c)를 하려면 결국 프로젝트 경로를 수동으로 직접 입력해야만 가능하여 매우 불편함. 프로젝트에 존재하지는 않지만 같은 디렉터리에 있는 파일들을 덩달아 찾아야 할 때도 있는데도 말이다.

물론 (b)는 소스 코드를 컴파일까지는 아니어도 전처리기 수준의 파싱은 해야 구현 가능하기 때문에, 좀 어려울지 모른다. #include를 제대로 처리하려면 프로젝트 차원의 인클루드 디렉터리 관리자가 있어야 하며, 조건부 컴파일뿐만 아니라 인클루드 대상 자체에 대해서도 매크로 상수 전개가 필요할 때가 있으니 말이다.

c/cpp 같은 소스 코드가 그 자체로 온전한 번역 단위를 구성하는 게 아니라, 다른 소스 코드에 또 인클루드되어 쓰이는 경우가 있다. 물론 프로젝트에 등록되지 않은 채로 말이다.
이런 파일은 (a) 형태의 문자열이나 파일명 검색이 되지도 않아 대단히 불편하며, IDE가 구문 분석을 하는 것도 굉장히 복잡하고 어렵게 만든다. C/C++에서 인클루드는 정말 양날 달린 검인 게 실감이 간다.

끝으로 (b)와 관련된 여담 하나 좀 남기겠다.
과거 비주얼 C++ 6 시절엔 프로젝트 파일 리스트에 External dependencies라고 해서, 정식으로 프로젝트에 포함돼 있지는 않지만 프로젝트 파일에 의해 인클루드되는 파일을 대충, 얼추 계산해서 표시해 주는 기능이 있었다. '대충, 얼추'라는 말은 그 동작이 100% 정확하지는 않았다는 뜻이다. 그러던 것이 닷넷으로 넘어가면서 이 얼렁뚱땅 불완전한 기능은 삭제되었다.

그 뒤, 버전이 201x으로 넘어가면서 이 기능은 부활했다. 온전한 컴파일러가 소스 코드를 머리부터 발끝까지 다 분석하면서, MFC와 플랫폼 SDK가 중첩 인클루드하는 수십, 수백 개의 헤더 파일들을 하나도 빠짐없이 정확하게 나열해 주는 무시무시한 기능으로 다시 태어난 것이다. 비주얼 C++ IDE는 변화가 없는 것 같아도 내부적으로 이렇게 변모하고 있다.
모든 파일들의 의존도 정보를 파악하고 있다는 소리이니, 이를 바탕으로 함수 호출 tree처럼 파일들의 include 계층 다이어그램(includes / included by)을 그려 주는 기능은 IDE에 혹시 없나 궁금하다.

Posted by 사무엘

2014/04/21 08:28 2014/04/21 08:28
, ,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/954

자고로 프로그래밍 언어는 구문이나 예약어 같은 원론적인 것만 정의하는 게 아니라, 그 문법을 토대로 프로그램의 작성에 필요한 각종 기본적인 자료구조와 알고리즘, 입출력 기능들도 라이브러리 형태로 제공한다. 후자의 디자인도 프로그래밍 언어의 정체성에서 차지하는 비중이 매우 크다.

예를 들어 printf, qsort, malloc, fopen 같은 함수를 사용했다면 그 함수들의 몸체도 당연히 프로그램 어딘가에 빌드되어 들어가야 한다. 아니, 애초에 main 함수나 WinMain 함수가 호출되고 각종 인자를 전해 주는 것조차도 그냥 되는 일이 아니다. 이런 것은 C/C++ 언어가 제공하는 런타임 라이브러리가 해 주는 일이며, 우리가 빌드하는 프로그램에 어떤 형태로든 코드가 링크되어 들어간다.

C/C++은 그나마 기계어 코드를 생성하는 언어이기 때문에 그런 런타임의 오버헤드가 작은 편이다. 그러나 비주얼 베이직(닷넷 이전의 클래식)이라든가 델파이는 RAD 툴이고 언어가 직접 GUI까지 다 커버하다 보니 언어가 제공하는 런타임 오버헤드가 크다.
자바나 C#으로 넘어가면 런타임이 단순 코드 오버헤드로 감당 가능한 정도가 아니라 아예 가상 기계 수준이다. 프로그램이 기계어 코드로 번역되지도 않으며, garbage collector까지 있다.

그렇게 프로그래머의 편의를 많이 봐 주는 언어들에 비해 '작은 언어'를 추구하는 C/C++은 도스나 16비트 윈도 시절에는 런타임 라이브러리가 static 링크되는 게 당연시되곤 했다. 오버헤드 자체가 수천~만 몇천 바이트밖에 되지 않을 정도로 매우 작은 편이고, 저수준 언어인 C/C++은 당연히 standalone 바이너리를 만들어야지 무슨 다른 고급 언어처럼 런타임 EXE/DLL에 의존하는 바이너리를 생성한다는 건 좀 안 어울려 보이는 게 사실이었다.

그래서 C/C++로 개발된 EXE 파일은 내부를 들여다보면 대체로, 링크되어 들어간 런타임 라이브러리의 이름과 개발사를 나타내는 문자열이 들어있었다. 볼랜드나 마이크로소프트 따위. 그래서 이 프로그램이 어느 컴파일러로 만들어졌는지를 얼추 짐작도 할 수 있었다.

C 런타임 라이브러리도 static이 아닌 DLL 형태로 제공하려는 발상은 Windows의 경우 아무래도 32비트 NT의 개발과 함께 시작된 듯하다. 그래서 윈도 NT 3.1의 바이너리를 차용해서 개발되었다는 과거의 Win32s를 보면 crtdll.dll이라는 파일이 있는데 이것이 운영체제가 기본 제공하는 프로그램들이 공용하는 C 런타임 DLL인 듯하다. 즉, 메모장, 문서 작성기, 그림판 등의 프로그램들이 호출하는 sprintf 같은 C 함수들의 구현체가 거기에 담겨 있다는 뜻이다.

재미있게도 윈도 NT의 경우, kernel32보다도 먼저 로딩되는 ntdll.dll이 내부적으로 또 각종 C 함수들의 구현체를 제공한다. 커널이 제대로 로딩되기도 전에 실행되는 프로그램이 공유하는 C 함수들이라고 한다.

과거의 윈도 9x의 프로그램들은 비록 32비트이지만 운영체제가 자체적으로 공유하는 C 런타임 DLL은 없다.
다만, 윈도 NT 3.x 이후로 비주얼 C++이 32비트 개발툴로 자리매김하면서 이 개발툴의 버전에 따라 msvcrt20.dll, msvcrt40.dll이 제공되기 시작했고 이들은 윈도 9x에서도 기본 내장되었다. 비록 같은 C 런타임이지만 버전별로 미묘한 차이가 있는 모양이다.

그러다가 1996년에 출시된 비주얼 C++ 4.2부터 C 런타임 DLL은 더 변경을 안 하려는지 파일 이름에서 버전 숫자가 빠지고, 그 이름도 유명한 msvcrt.dll이라는 이름이 정착되어 버전 6까지 쭉 이어졌다.
이 이름은 비주얼 C++ 4.2 ~ 6이 생성한 바이너리가 사용하는 C 런타임인 동시에, NT 계열에서는 기존의 crtdll을 대신하여 운영체제의 기본 제공 프로그램들이 공유하는 C 런타임의 명칭으로도 정착했다.

그리고 9x 계열도 윈도 98부터는 mfc42.dll과 더불어 msvcrt.dll을 기본 제공하기 시작했다.
NT와는 달리 운영체제가 msvcrt.dll을 직접 쓰지는 않지만, 비주얼 C++로 개발된 프로그램들을 바로 실행 가능하게 하기 위해서 편의상 제공한 것이다. 과거의 유물인 msvcrt40.dll은 msvcrt.dll로 곧바로 redirection된다.
그 무렵부터는 오피스, IE 같은 다른 MS 사의 프로그램들도 C 런타임을 msvcrt로 동적 링크하는 것이 관행으로 슬슬 정착해 나갔다.

그렇게 윈도, VC, 오피스가 똑같이 msvcrt를 사용하는 구도가 한동안 이어졌는데..
21세기에 비주얼 C++이 닷넷으로 넘어가면서 그 균형은 다시 깨졌다.
C 런타임 라이브러리도 한 번만 만들고 끝이 아니라 계속 버전업이 되어야 한 관계로 msvcr70, msvcr71 같은 DLL이 계속해서 만들어진 것이다. 결국, 비주얼 C++ 최신 버전으로 개발한 프로그램은 C 라이브러리를 동적 링크할 경우, DLL 파일을 배포해야 하는 문제를 새로 떠안게 되었다.

이것이 비주얼 C++ 2005/2008에서는 더욱 복잡해졌다. C 라이브러리를 side-by-side assembly 방식으로 배포하는 것만 허용했기 때문이다. 쉽게 말해, 레거시 공용 컨트롤과 윈도 XP의 비주얼 스타일이 적용된 공용 컨트롤(comctl32.dll)을 구분할 때 쓰이는 그 기술을 채택했다는 뜻이다.

그래서 msvcr80/msvcr90.dll은 윈도 시스템 디렉터리에만 달랑 넣는다고 프로그램이 실행되지 않는다. 이들 DLL은 DLLMain 함수에서 자신이 로딩된 방식을 체크하여 이상한 점이 있으면 고의로 false를 되돌린다. 그렇기 때문에 이들은 반드시 Visual C++ 재배포 런타임 패키지를 통해 정식으로 설치되어야 한다.

런타임 DLL간의 버전 충돌을 막기 위한 조치라고 하나 이것은 한편으로 너무 불편하고 번거로운 조치였다. C 라이브러리가 좀 업데이트돼 봤자 메이저도 아니고 마이너 버전끼리 뭐가 그렇게 버전 충돌이 있을 거라고..;; 나중에는 여러 프로그램들을 설치하다 보면 같은 비주얼 C++ 2005나 2008끼리도 빌드 넘버가 다른 놈들의 재배포 패키지가 설치된 게 막 난립하는 걸 볼 수 있을 정도였다. 가관이 따로 없다. 당장 내 컴에 있는 것만 해도 2008 기준으로 9.0.32729까지는 똑같은데 마지막 숫자가 4148, 6161, 17, 4974... 무려 네 개나 있다.

개발자들로부터도 불편하다고 원성이 빗발쳤다. MS Office나 Visual Studio급으로 수십 개의 모듈로 개발된 초대형 소프트웨어를 개발하는 게 아니라면, 꼬우면 그냥 C 라이브러리를 static 링크해서 쓰라는 소리다.
그래서 비주얼 C++ 2010부터는 C 라이브러리 DLL은 다시 윈도 시스템 디렉터리에다가만 달랑 집어넣는 형태로 되돌아갔다. 다시 옛날의 msvcrt20, msvrt40, msvcr71처럼 됐다는 뜻이다.

윈도 비스타 타임라인부터는 운영체제, VC, 오피스 사이의 관계가 뭔가 규칙성이 있게 바뀌었다.
오피스는 언제나 최신 비주얼 C++ 컴파일러가 제공하는 C 라이브러리 DLL을 사용하며, 운영체제가 사용하는 msvcrt도 이름만 그럴 뿐 사실상 직전의 최신 비주얼 C++가 사용하던 C 라이브러리 DLL과 거의 같다.

그래서 오피스 2007은 msvcr80을 사용하며, 오피스 2010은 비주얼 C++ 2008에 맞춰진 msvcr90을 사용한다. C 런타임 DLL도 꾸준히 버전업되고 바뀌는 물건이라는 것을 드디어 의식한 것이다. 비스타 이전에 윈도 2000/XP의 EXE/DLL들은 헤더에 기록된 링커 버전을 보면, 서로 사용하는 컴파일러가 다르기라도 했는지 통상적인 비주얼 C++이 생성하는 EXE/DLL에 기록되는 버전과 일치하지 않았었다. 9x는 더 말할 필요도 없고.

그럼에도 불구하고 msvcrt는 운영체제 내부 내지 디바이스 드라이버만이 사용하고, 비주얼 C++로 개발된 여타 응용 프로그램들은 언제나 msvcr##을 사용하게 용도가 확실하게 이원화되었다.
그래서 심지어는 똑같이 MS에서 개발한 한글 IME임에도 불구하고 Windows\IME 디렉터리에 있는 운영체제 보급용은 msvcrt를 사용하고, 한글 MS Office가 공급하는 IME는 Program Files에 있고 msvcr##을 사용한다. 둘은 거의 똑같은 프로그램인데도 말이다.

이것이 Windows와 C 런타임 라이브러리 DLL에 얽힌 복잡한 역사이다. 여담이지만 C 라이브러리 중에서 VC++ 2003이 제공한 msvcr71은 Windows 95를 지원한 마지막 버전이다. 2005가 제공한 msvcr80부터는 일부 보안 관련 코드에서 IsDebuggerPresent라는 API 함수를 곧장 사용함으로써 Windows 95에 대한 지원을 중단하였으며, 최하 98은 돼야 동작한다. VC++ 2008부터는 9x 계열에 대한 지원 자체가 중단됐고 말이다.

자, 여기서 질문이 생긴다. 그럼 C++은 사정이 어떠할까?

C보다 생산성이 훨씬 더 뛰어난 C++로 주류 프로그래밍 언어가 바뀌면서 표준 C++ 라이브러리에 대한 동적 링크의 필요성도 응당 제기되었다. 그러나 이것은 C 라이브러리보다는 시기적으로 훨씬 더 늦게 진행되었기 때문에 가장 먼저 등장하는 비주얼 C++의 DLL 이름이 msvcp50과 msvcp60이다. 즉, 비주얼 C++ 5.0 이전에는 선택의 여지 없이 static 링크만 있었다는 뜻이다.

더구나 이것은 전적으로 비주얼 C++ 소속이지, 운영체제가 따로 C++ 라이브러리 DLL을 제공하지는 않는다. MS에서 만들어진 제품들 중에 msvcp##를 사용하는 물건은 비주얼 C++ IDE와 컴파일러 그 자신밖에 없다.

C++ 라이브러리는 대부분 템플릿 형태이기 때문에 알고리즘 뼈대는 내 바이너리에 static 링크되는 게 사실이다. 그러나 그렇게 덧붙여지는 라이브러리 코드가 별도로 호출하는 함수 중에 DLL에 의존도를 지니는 게 있다. cout 객체 하나만 사용해도, 그리고 STL 컨테이너 하나만 사용한 뒤 빌드해 봐도 형체를 알 수 없는 이상한 메모리 관리 쪽 함수에 대한 의존도가 생긴다.

그래도 C 라이브러리 DLL은 사용한 함수에 따라서 printf, qsort 등 링크되는 심벌이 명확한 반면
C++ 라이브러리는 내부 구조는 이거 뭐 전적으로 컴파일러가 정하기 나름이고 외부에서 이들 심벌을 불러다 쓰기란 사실상 불가능하다는 큰 차이가 있다.

<날개셋> 한글 입력기는 C++ 라이브러리를 사용하지 않으며, 빌드 후에 생성된 바이너리를 인위로 수정하여 msvcr## 대신 강제로 운영체제의 msvcrt를 사용해서 동작하게 만들어져 있다.

Posted by 사무엘

2013/11/19 08:29 2013/11/19 08:29
, , ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/900

간단한 인트린직 이야기

요즘 컴파일러에는 인트린직(intrinsic)이라고 정의된 내장 함수들의 집합이 있다. 그래서 그런 함수를 호출한 것은 실제 함수 호출이 아니라 특정 기계어 코드로 곧장 치환된다. 함수의 몸체가 직접 삽입된다는 점에서는 함수 인라이닝과 비슷하지만, 인트린직은 그 몸체가 컴파일러에 의해 내장되어 있으니 개념과 용도가 그것과는 살짝 다르다.

인트린직은 굳이 인라인 어셈블리 같은 거창한 문법 없이 간단한 C 함수 호출 스타일로 특정 기계어 인스트럭션을 곧장 집어넣거나 컴파일러의 확장 기능을 사용하기 위한 목적이 강하다. #pragma가 특수한 의미를 지닌 지시를 내리기만 하는 전처리기로 비실행문이라면, 인트린직은 실행문이다.

또한, 기존의 표준 C 함수가 인트린직 형태로 몰래 처리되기도 한다. memset, strcpy처럼 간단한 메모리 조작은 컴파일러가 아예 직통 대입 코드를 집어넣는 식으로 최적화를 하기도 하며, 각종 수학 함수들도 FPU 명령 하나로 곧장 치환되는 게 보통이다. 가령 제곱근을 구하는 sqrt함수는 곧바로 fsqrt 인스트럭션으로 말이다.

C 라이브러리 DLL인 msvcr*.dll은 여전히 수학 함수 심벌들을 제공한다. 그러나 요즘 컴파일러가 수학 연산을 인트린직 대신 일일이 그런 함수 호출 형태로 곧이곧대로 사용하는 경우란, 수학 함수들을 함수 포인터 형태로 접근해야 할 때밖에 없다.

비주얼 C++이 제공하는 여러 인트린직 함수 중에는 '일을 하지 않음'(no operation)을 의미하는 __noop이라는 함수가 있다. x86의 nop 인스트럭션(코드 바이트 0x90)과 비슷한 발상인데, nop를 생성하기라도 하는 것도 아니다. 컴파일러는 파싱만 해 주지 코드 생성은 안 하고 넘긴다. 파이썬으로 치면 pass와 비슷한 물건이다.

파이썬은 세미콜론 같은 구분자도 없고 공백 들여쓰기가 유효한 의미를 갖기 때문에 empty statement를 표현하려면 pass 같은 별도의 문법이 반드시 필요하다. 그러나 C/C++은 ; 하나만 찍어 줌으로써 empty statement쯤이야 얼마든지 만들 수 있는데 굳이 저런 잉여로운 인트린직은 왜 필요한 걸까?

__noop의 주 용도는, 가변 인자를 받는 디버그 로그를 찍는 함수의 컴파일 여부를 제어하는 것이다.

#ifdef _DEBUG
    #define WRITE_LOG   WriteLog
#else
    #define WRITE_LOG   __noop
#endif

WRITE_LOG("Start operation");
WRITE_LOG("Operation ended with code %d", nErrorCode);

함수가 가변 인자가 아니라면, WRITE_LOG 자체를 매크로 상수가 아닌 매크로 함수로 선언하면 된다.

#ifdef _DEBUG
    #define WRITE_LOG1(msg,a1)  WriteLog(msg,a1)
#else
    #define WRITE_LOG1(msg,a1)  0
#endif

이렇게 해 주면 디버그가 아닌 릴리즈 빌드에서는 WriteLog가 호출되지 않을 뿐더러, 매개변수들의 값 평가도 전혀 발생하지 않게 된다.
그러나 매크로 함수는 가변 인자를 받을 수 없기 때문에 임의의 개수의 인자를 모두 커버하려면 결국 함수 이름 자체만 치환하는 매크로 상수를 써야 한다. 매크로 상수는 함수의 호출만 없앨 수 있지 함수로 전달되는 매개변수들의 값 평가를 없앨 수는 없다. 뭐, 상수들의 나열이야 컴파일러의 최적화 과정에서 제거되겠지만 side effect가 남는 함수 호출은 어떡하고 말이다.

이런 이유로 인해 가변인자를 받는 디버그 함수를 제어하는 매크로는 범용적인 버전뿐만 아니라 매개변수 고정 버전도 같이 딸려 나오는 게 관행이었다. 대표적인 게 MFC의 TRACE 매크로이다. TRACE0~TRACE3도 있다. 한번 함수를 호출할 때 전달되는 매개변수의 개수가 런타임 때 매번 바뀌는 것도 아니니, 가능하면 고정 버전을 쓰는 게 프로그래밍 언어의 관점에서 더 안전했기 때문이다.

그런데 비주얼 C++의 __noop은, 하는 일은 없으면서 0개부터 n개까지 아무 개수로 그 어떤 type의 인자를 넘겨줘도 되는 훌륭한 페이크 함수이다. 그래서 매크로 상수를 이용해서 그 어떤 함수도 __noop로 치환하면 그 함수의 호출은 깔끔하게 무시되고 코드가 생성되지 않는다.

게다가 코드는 생성되지 않지만 컴파일러가 각 토큰에 대한 구문 분석은 해 준다는 점에서 __noop은 매크로 상수뿐만 아니라 매크로 함수보다도 더 우월하다.
WRITE_LOG1에다가 아예 얼토당토 않은 선언되지 않은 변수를 집어넣을 경우, 이것을 그냥 0으로만 치환해 버리는 코드는 디버그 버전에서만 에러가 발생하고 릴리즈 버전은 그냥 넘어간다.

그러나 __noop으로 치환하면 효과는 0 치환과 동일하면서 릴리즈 버전에서도 컴파일 에러가 뜬다. 그러니 더욱 좋다.
이 인트린직은 #pragma once만큼이나, 그야말로 기존 C/C++ 문법만으로는 뭔가 2% 부족한 면모가 있는 걸 채워 준 물건이 아닐 수 없다. 컴파일러 개발사들이 괜히 비표준 꼼수를 집어넣는 게 아니다. 그 꼼수가 정말 타당하다고 여겨지면 다음 표준 때 정식으로 반영까지 될 테고.

아무 일도 안 하는 null instruction은 코드 바이트도 0으로 하는 게 직관적이지 않나 싶은데..
아까도 얘기했듯이 x86은 의도적으로 쉽게 떠오르지 않는 값에다 그 명령을 할당했다.
크기 최적화를 하지 않고 프로그램을 빌드하는 경우(디버그 또는 속도 최적화), 비주얼 C++은 machine word align을 맞추거나 edit and continue용 공간을 확보해 두기 위해 코드 바이트를 약간 듬성듬성하게 배치하는데, 과거의 6.0은 그 빈 틈에 nop(0x90)를 집어넣었다.

그러나 언제부턴가 닷넷부터는 그 버퍼 코드가 int 3(0xCC)으로 바뀌었다.
정상적인 경우라면 거기는 도달해서는 안 되는 곳인데, 그냥 아무 일 없이 nop로 넘어가는 게 아니라 breakpoint가 걸리게 보안을 강화한 게 아닌가 싶다.

말이 나왔으니 말인데, int 3을 집어넣어 주는 인트린직도 응당 존재한다. 바로 __debugbreak 함수이다. 이건 사실 Windows API에도 DebugBreak()로 있기도 하고 말이다.
이걸 쓰면 비주얼 C++ IDE에서 F9를 누른 것과 동일한 효과를 낼 수 있다. 디버거를 붙여서 실행할 경우, 그 지점에서 프로그램의 실행이 멈춘다.

Posted by 사무엘

2013/09/20 08:30 2013/09/20 08:30
, , , ,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/879

본인이 예전에 글로 썼듯, 비주얼 C++ 201x의 IDE는 소스 코드의 구문 체크 및 인텔리센스를 제공하기 위해 백그라운드에서 완전한 형태의 컴파일러를 실시간으로 돌린다. ncb 파일을 사용하던 200x 시절에는 불완전한 모조 컴파일러였지만 201x부터는 그렇지 않다. 컴파일은 그걸로 하고, 자료 저장은 아예 별도의 DB 엔진으로 하니 계층이 전문화된 셈이다.

그런데 실시간으로 돌리는 컴파일러는, MS가 자체적으로 빌드를 위해 구동하는 컴파일러하고는 다른 별개의 종류이다. 이 개발툴로 오래 개발을 해 본 분은 이미 아시겠지만 같은 문법 에러에 대해서도 메시지가 서로 미세하게 다르고 심지어 문법 해석 방식이 불일치하는 경우도 있다. 마치 MS Office의 리본 UI와 MFC의 리본 UI는 구현체가 서로 별개이고 다르듯이 말이다.

그럼 이 보이지 않는 백그라운드 컴파일러의 정체는 뭘까? 이건 ‘에디슨 디자인 그룹(Edison Design Group)’이라고 유수 프로그래밍 언어들의 컴파일러 ‘프런트 엔드’만 미들웨어 형태로 전문적으로 개발하여 라이선스를 판매하는 어느 벤처기업의 작품이다. MS에서는 이 물건을 구입하여 자기 제품에다 썼다.

컴파일러를 만드는 것은 오토마타 같은 계산 이론부터 시작해서 어려운 자료구조와 알고리즘, 컴퓨터 아키텍처 지식이 총동원되는 매우 까다롭고 어려운 과정이다. 그렇기에 컴파일러는 전산학의 꽃이라 불리며, 대학교 전산학과에서도 4학년에 가서야 맛보기 수준으로만 다뤄진다.

그리고 컴파일 메커니즘은 프런트 엔드와 백 엔드라는 두 단계로 나뉜다. 소스 코드의 구문을 분석하여 문법 오류가 있으면 잡아 내고 각종 심벌 테이블과 parse tree를 만드는 것이 전자요, 이를 바탕으로 각종 최적화를 수행하고 실제 기계어 코드를 생성하는 건 후자이다.

굳이 코드 생성까지 하지 않아도 구문을 분석하여 인텔리센스를 구현하는 것까지는 프런트 엔드만 있어도 충분할 것이다. 프런트 엔드를 담당하는 쪽은 언어의 문법을 직접적으로 다루고 있으니, C++11 표준이 뭐가 바뀌는 게 있는지를 늘 매의 눈으로 감시하고 체크해야 한다. 그리고 그런 엔지니어들이 역으로 표준의 제정에 관여하기도 한다.

에디슨 디자인 그룹은 5명의 베테랑 프로그래머들로 구성된 아주 작은 회사이다. (홈페이지부터 디자인이 심하게 단촐하지 않던가?) 하지만 세계를 움직이는 굴지의 IT 회사들에 자기 솔루션을 납품하고 있다. 작지만 기술이 강한 이런 회사야말로 컴퓨터 공돌이들이 꿈꾸는 이상적인 사업 모델이 아닐 수 없으니 매우 부럽다. 개인이 아닌 기업이나 교육 기관이 고객이며, 한 솔루션의 소스 코드를 납품하는 라이센스 비용은 수만~수십만 달러에 달한다.

마이크로소프트 컴파일러는 인텔리센스만 이 회사의 솔루션으로 구현한 반면,
Comeau C++ 컴파일러는 프런트 엔드가 이것 기반이다. Comeau라 하면, C++의 export 키워드까지 다 구현했을 정도로 표준을 가장 충실하게 따른 걸로 유명한 그 컴파일러 말이다.

굳이 백 엔드와 연결된 컴파일러가 아니어더라도, 프런트 엔드가 만들어 낸 소스 코드 parse tree는 IDE의 인텔리센스를 구현한다거나 소스 코드의 정적 분석, 리팩터링, 심벌 브라우징(browsing), 난독화 등의 용도로 매우 다양하게 쓰일 수 있다. 나름 이것도 황금알을 낳는 거위 같은 기술이라는 뜻이다.

한편, 전세계 유수의 컴파일러들에 C++ 라이브러리를 공급하는 회사는 Dinkumware이라는 걸 난 예전부터 알고 있었다. 헤더 파일의 끝에 회사 설립자인 P.J. Plauger 이름이 늘 들어가 있었기 때문이다. 난독화가 따로 없는 그 암호 같은 복잡한 템플릿들을 다 저기서 만들었다 이 말이지?
비주얼 C++이라는 그 방대한 제품은 당연한 말이지만 모든 부품이 MS 독자 개발은 아니라는 걸 알 수 있다.

그나저나, 비주얼 C++ 201x의 백그라운드 컴파일러는 C 코드에 대해서도 언제나 C++ 문법을 기준으로만 동작하더라.. ㅎㅎ

Posted by 사무엘

2013/04/16 08:40 2013/04/16 08:40
, , , ,
Response
No Trackback , 3 Comments
RSS :
http://moogi.new21.org/tc/rss/response/818

« Previous : 1 : 2 : 3 : 4 : Next »

블로그 이미지

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

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

«   2020/02   »
            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

Site Stats

Total hits:
1331102
Today:
114
Yesterday:
377